Metadata-Version: 2.5
Name: qualia-cli
Version: 0.3.0rc12
Summary: CLI for Qualia robot data curation: upload, curate, version and export datasets
Project-URL: Homepage, https://qualiastudios.dev
Project-URL: Documentation, https://docs.qualiastudios.dev
Author: Qualia Studios
License-Expression: MIT
Keywords: cli,data-curation,datasets,qualia,robotics
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: click>=8.1.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: qualia-sdk>=0.8.0rc12
Requires-Dist: rich>=13.0.0
Requires-Dist: typer>=0.15.0
Description-Content-Type: text/markdown

# qualia-cli — terminal client for Qualia robot data: upload, curate, version, export

**Status:** beta source candidate (0.3.0rc12) · **Distribution:** PyPI, `qualia-cli`

Wraps the public Qualia API: authenticate once, then manage projects, import and curate datasets,
set up robots and grippers, and follow running work from a shell. Every command takes `--json`, so
the same surface works interactively and in CI. Fine-tuning is not in the CLI; train from the
dashboard.

The CLI and the endpoint it talks to negotiate the upload protocol exactly, so a deployment running
ahead of the public release needs the matching pre-release wheel. See Install below.

## Install

<!-- pre-release until the first stable release on prod; drop --pre / --prerelease then -->

```sh
pip install --pre qualia-cli
```

Or with [uv](https://docs.astral.sh/uv/):

```sh
uv tool install --prerelease allow qualia-cli
```

The stable `qualia-cli` on PyPI (0.1.2) predates `qualia dataset upload`; every working release is a
pre-release for now.

### Pre-release builds

Pin the wheel that matches the endpoint's generation. This source candidate is
`0.3.0rc12` and requires `qualia-sdk>=0.8.0rc12` for workspace-bound saved upload
destinations, the handheld setup commands, `qualia robot`, the `mcap_ros2`
layout, the one `qualia dataset download` and its partner layouts (`pi_mcap_v1`,
`dreamlabs_v2_1`). Use these commands after the
candidates are published and your deployment supports the merged-upload
lifecycle; otherwise select the matching published version.

```sh
pip install 'qualia-cli==0.3.0rc12'
```

uv does not resolve a pre-release through a dependency, even one that names it, so uv needs the
flag as well as the pin:

```sh
uv tool install --prerelease=allow 'qualia-cli==0.3.0rc12'
```

Removed in 0.3.0rc10: fine-tuning (`qualia job list`, `launch`, `get`, `items`, `logs`, `cancel`,
`hyperparams`). `qualia status list --running` shows a launched pipeline run.

## Draft Tasks from selected episodes (staged)

Manual creation remains available: `qualia task create --name "Pick a box"`
needs no dataset or episode. For a model proposal, choose every reference
explicitly (1–3 success episodes; 1–2 per named failure/recovery mode):

```sh
qualia task create --name "Pick a box" \
  --success accounts/<a>/datasets/<d>/episodes/<e> \
  --failure slip=accounts/<a>/datasets/<d>/episodes/<other-e>
qualia task references <task>
qualia task draft <task> --estimate          # price, no run or reservation
qualia task draft <task> --wait              # bill for the stored proposal
qualia task draft-get <draft>                # review all text and the graph
qualia task draft-apply <draft> --file reviewed.yaml
qualia task publish <task>                  # separate person-controlled action
```

Pick or drop references one at a time. An API key acts as the person who
created it.

```sh
qualia task reference add <task> <episode-name> --role failure --mode slip
qualia task reference rm <task> <reference-id> --yes  # the episode is untouched
qualia task drafts <task>                             # every draft run and its state
```

No reference is sampled, suggested or silently omitted. The apply command
saves an editable proposal, never publishes; the file may edit its text and
procedure. `--max-cost` caps credit reservation, and `task draft-get` can
resume review after a lost connection. The `vlm-task-draft` template is
currently disabled: estimates and runs will fail until rollout is authorized.

## Usage

```sh
# Authenticate (pick one)
qualia auth login                        # interactive, saves to config
export QUALIA_API_KEY="your-api-key"     # env var
qualia --token <KEY> <command>           # inline, for CI/scripts
# With no usable key, a command that calls the API stops with one instruction:
# log in or set QUALIA_API_KEY. --account is optional; without it the key's own
# account is used, and `qualia auth whoami` shows its id.

# Who is this key? Account, workspace, key scope and expiry
qualia auth whoami
qualia auth status                       # the stored config, plus the key's scope

# What happened: a workspace's activity (--workspace or QUALIA_WORKSPACE), or
# the whole organization's
qualia --workspace <workspace-id> activity list
qualia activity list --org --filter 'verb = KEEP' --json
qualia activity show <event-id> --details
qualia activity export -o events.jsonl   # audit:read, an admin's key

# The organization's Hugging Face tokens (integrations:write, a Contributor's key or above).
# The token is read from stdin or a silent prompt, never argv, and never printed again.
qualia integration list
qualia integration add huggingface --name ci --secret-stdin < hf.key
qualia integration default <hf-token-id>

# One workspace, and renaming it
qualia project show <project-id>
qualia project update <project-id> --name "Arms"

# People, roles and invitations (a key with members:read / members:write)
qualia member list --project <project-id>
qualia member role <user-id> contributor
qualia invitation send new@example.com --role viewer --project <project-id>
qualia invitation withdraw <invitation-id> --yes

# List available models
qualia model list

# List GPU instances
qualia instance list

# Create a project
qualia project create "My Robot"

# The camera slots a training run maps your cameras onto
qualia model slots pi05

# Launch any template the dashboard's launcher offers
qualia pipeline templates
qualia pipeline launch <template> --project <id> --set dataset=<version-id> --set hours=1

# Running work in a workspace (--workspace or QUALIA_WORKSPACE; --org for the
# organization), and stop one
qualia status list --running
qualia status cancel <operation-id>

# Upload a recorded MCAP session; a session no bag can derive is refused
# before upload unless you pass --allow-underivable
qualia dataset upload ./session dk1-kitchen --source-format mcap_ros2
# exits 0 published/degraded, 1 failed or refused, 3 stored but no episode derived

# A teleop arm needs no config when its state is a JointState topic named
# joint_state(s) and its action is a JointTrajectory topic, or a JointState or
# MultiArray topic named cmd, command or action. In <session>/meta/ingest.toml,
# declare [actions] alone when a topic named like a command is operator input
# (a joystick), and both [state] and [actions] when a second JointState topic
# (a leader arm) would be read as state, or the rig publishes poses and has no
# JointState:
#   schema = 1
#   [state]
#   topics = ["/controller/left/pose", "/controller/right/pose"]
#   [actions]
#   topics = []   # no action column
# Fields are carried as recorded, with no rotation conversion. [clock] takes
# row_clock_topic or fixed_hz, not both.
# Guide: https://docs.qualiastudios.dev/global/guides/data-collection
# A successful upload prints the topics that fed state and action, and says
# "action: none (no action column)" when there is none. --quiet still prints them.
# While bytes travel it prints "X / Y sent": source bytes already in storage,
# before the server verifies them. `qualia dataset uploads` shows how far each
# upload in flight has got, counting stored as well as verified bytes, as the
# dashboard card does.

# LeRobot trees are checked before upload too. A structural problem the server
# always refuses (bad meta/info.json, codebase_version not matching
# --source-format, no parquet under data/, v2.1 without episodes.jsonl or
# tasks.jsonl) exits 1 with nothing uploaded; --allow-underivable cannot
# override it. A missing video only warns, naming the episode and camera; the
# upload continues and that episode will be flagged (you should probably
# exclude it).
#
# The format is read from the tree when --source-format is left out.
qualia dataset upload ./my-recording my-dataset

# Save a default merged-dataset destination for uploads in this local config.
# It is bound to this API host, account, and workspace; changing it affects
# future upload intents, not uploads already in flight.
qualia dataset destination set accounts/<account-uuid>/datasets/<merged-uuid>
qualia dataset destination show
qualia dataset destination clear

# Override that default for one upload, or keep one upload standalone.
qualia dataset upload ./week-2 week-2 --source-format lerobot_v3 \
  --merge-target accounts/<account-uuid>/datasets/<merged-uuid>
qualia dataset upload ./calibration calibration --source-format lerobot_v3 --standalone

# Convert a handheld (UMI) recording at a gripper's TCP as it lands. The gripper
# is its name, id or rig key; an upload into a merged dataset whose uploads
# agree on one rig takes that rig without --rig. Your local files are untouched.
qualia dataset upload ./umi-session umi-session --source-format mcap_ros2 --rig umi-handheld

# Workspace selection is the gateway's X-Qualia-Workspace context. An explicit
# selection overrides the workspace bound to the saved destination; a mismatch
# leaves this upload standalone instead of sending it into another workspace.
qualia --workspace <project-uuid> dataset upload ./week-3 week-3 \
  --source-format lerobot_v3
export QUALIA_WORKSPACE=<project-uuid>  # alternatively, select it for this shell

# If append needs action, the upload remains published and the command prints
# its dataset ID and reason. A retry after an explicit revert keeps that intent
# reverted; it never silently appends again.

# List your uploads, named as the dashboard names them; Verdict is published | degraded |
# underivable. With your account set (see Tasks below) it reads qualia.v1 and --json prints
# qualia.v1 Datasets (reason: verdict_reason); without one it reads the older route, which
# has no names and shows the repository's (reason: qds.verdict_reason).
qualia dataset list

# Filter by collection tags (repeat for alternatives).
qualia dataset list --tag umi --tag gello

# Change classifications through this dataset's saved metadata. A merged
# dataset keeps its choices on append. Customs alone preserve classifications.
qualia dataset tags <dataset-uuid> --tag umi --tag real-world --tag human --tag gello
qualia dataset tags <dataset-uuid> --tag warehouse-a
# --clear clears customs and explicitly makes every collection dimension Unknown.
qualia dataset tags <dataset-uuid> --clear
# Unchanged classifications with edited customs change only customs. Echo the
# current classifications, when present, to remove customs while preserving inferred methods.
# The SDK reads the owner and fences the write with its etag unless --etag is supplied.
# The older `dataset methods` command edits the six-name compatibility view.

# Collection semantics: docs/operations/qds-service.md#collection-metadata
# Calibrated export: docs/operations/qds-service.md#calibrated-handheld-lerobot-state

# Trims, subtasks and splits are drawn on each episode's draft, as the
# episode studio draws them, and never change the dataset.
# Explicit --start/--end trims preserve the draft's other end and guard
# its etag; a drawn split is left alone. Each command
# prints "drafted N; publish a version to apply", since only a version
# published from a Publish preview holds the edit. An episode someone is
# editing in the studio is left alone ("being edited by <name>").
# `pin DATASET TAG` measures a frozen selection of takes kept by a person,
# saved drawings included, then publishes the actual immutable DatasetVersion.
# Merged datasets work the same way; mixtures tag head.
qualia dataset version-preview <dataset> --include kept --include flagged
qualia dataset version-preview <dataset> --json  # every window, analysis and metric summary
qualia dataset pin <dataset> 1.0 --preview <preview-name-or-id>
qualia dataset pin <dataset> 1.1 --include kept --allow-left-out
# Without --allow-left-out, unprocessable takes refuse publication with names/reasons.
# A later publication refusal still stops the command; it never publishes less silently.
qualia dataset pin <dataset> recorded-1 --as-recorded  # head; leave drawings out
# --digest <reviewed-head-digest> also tags head, fenced against head changes.
# Selection/head flags are mutually exclusive. No --include means the SDK's
# default, including for mixtures; `pin DEFINITION_UUID` remains a definition pin.
# `dataset version create` accepts the same publication flags.

# Split episodes at their markers, or, where the recorder marked nothing, at
# instants (seconds from the start of the recording, the same instants in
# each named episode). --dry-run plans without drawing. The pieces become
# episodes only in a published version, so there are no children to select;
# `dataset unsplit` discards a drawn split. A trimmed episode is cut inside
# the window it keeps. A request cuts at markers or at instants, not both.
qualia dataset split <episode> -d <dataset> --cut-at success --dry-run
qualia dataset split <episode> -d <dataset> --at 12.5 --at 31
qualia dataset unsplit <episode> -d <dataset>

# Trim to markers (--clear draws the full extent again), and name subtask
# spans from them.
qualia dataset trim <episode>... -d <dataset> --start-at-first success
qualia dataset subtasks <episode>... -d <dataset> --at grasp --at place

# Edit many episodes (up to 500 of one upload): the same fields on each, or
# per-episode edits from a JSON list (- reads stdin). Labels are edited on the
# dataset in one call, all of them or none, one revision each. A trim or
# subtasks in --from are drawn on each take's draft in a call of their own,
# sent first; they stay drawn when the labels are then refused.
qualia dataset edit <episode>... -d <dataset> --tag slow --outcome FAILURE
qualia dataset edit -d <dataset> --from edits.json

# List, open and count a dataset's episodes, then keep, reject, reset or
# restore them by id or by filter. A take a person has open in the studio is
# left as it was and listed on stderr with who holds it; the call exits 0.
qualia dataset episodes <dataset> --filter 'state = UNREVIEWED'
qualia dataset episode <episode> -d <dataset>
qualia dataset summary <dataset> --facet open_issue_codes --metric metrics.duration_seconds
qualia dataset keep <dataset> --filter 'state = UNREVIEWED AND open_issue_codes:"jerky"'
qualia dataset reject <dataset> <episode>... --reason "operator in frame"
qualia dataset reset <dataset> <episode>...
qualia dataset restore <dataset> <episode>... --reason "the fps metadata was right"

# One episode's own Task link, its edit history, and its camera videos.
qualia dataset link-episode <episode> -d <dataset> --task <task>
qualia dataset history <episode> -d <dataset>
qualia dataset videos <episode> -d <dataset> --download ./videos

# Download a dataset version in a format (lerobot_v3 default, lerobot_v2_1,
# mcap_ros2), or every file uploaded to it, byte for byte (original: no
# version, trims and splits not applied, an organization key for now).
# No version means the newest: stable, or v0 when nothing is tagged yet (it
# says so). A version is a tag, stable, v0, the DatasetVersion id an upload
# prints, or a digest or id prefix. Default folder: ./<slug>-<tag>, plus
# -v21 or -mcap; ./<slug>-original for original. Progress goes to stderr; a
# stopped run resumes from the files it checked. Ends with:
# Saved shelf:1.0 as mcap_ros2 to /path (12 files, 1.2 GB)
qualia dataset download <dataset>[:<version>] [dest] --format lerobot_v2_1
qualia dataset download <dataset> --format original
# pi_mcap_v1 and dreamlabs_v2_1 are files to upload to Physical Intelligence or
# Dream Labs with your own account: the version's episodes that are not
# rejected (Dream Labs: successful ones only). A failed episode is listed in
# the bundle's manifest and README. Default folder: -pi or -dreamlabs added.
qualia dataset download <dataset>:<tag> --format pi_mcap_v1

# Versions: list, read, compare, tag, annotate; a merged dataset's merges and
# what was done in a version's round (its curation record)
qualia dataset version list <dataset>
qualia dataset version diff <dataset> --base 1.0
qualia dataset version create <dataset> 1.1 --note "dropped 3 jerky takes"
qualia dataset version record <dataset>:1.0

# Mixtures: datasets kept up to date from other datasets, labels or tasks
qualia mixture create "Folding" --source dataset=<a> --source dataset=<b>@1.2
qualia mixture sources <mixture>
qualia mixture readers <dataset>

# Import an exact Hugging Face dataset commit. HF_TOKEN is read from the
# environment or the Hub's protected local credential store, never an argv flag.
export HF_TOKEN="..."  # omit for public datasets
qualia dataset import-hf \
  qualia-robotics/openarm-cube-in-box-v1 \
  17c29c487d886878ddcb3c5649101b2860fbc047 \
  cube-in-box \
  --source-format lerobot_v3
```

### Tasks

Tasks say what a robot should do and how success shows, as five pieces of
prose: description, scene, steps (one per line), success and failure. They live
under your account. Every command acts in the API key's own account (sent as
`accounts/-`); `--account <UUID>` and `QUALIA_ACCOUNT` remain as an optional
override.

```sh
qualia task create --name "Restock shelf" -d "Move every box from the tray to the shelf"
qualia task edit <task-id> --steps $'Pick a box from the tray\nPlace it upright on the shelf' \
  --success "The tray is empty and every box stands upright"
                                               # just those fields; "" clears one

# Or author the five fields as a file, as `qualia task get <id> --json` prints
# a task (displayName, etag, draft) or as a bare spec:
cat > shelf.yaml <<'EOF'
description: Move every box from the tray to the shelf
scene: A tray of six boxes beside a two-level wire shelf
steps: |
  Pick a box from the tray
  Place it upright on the shelf
success: The tray is empty and every box stands upright
failure: A box falls over or drops to the floor
EOF
qualia task edit <task-id> shelf.yaml          # replaces the whole draft

# A spec file may also hold an optional procedure: the task as a state machine of
# steps and transitions, checked when the task is published.
cat >> shelf.yaml <<'EOF'
procedure:
  steps:
    - {id: start, kind: START, description: Boxes in the tray}
    - {id: place, description: Place a box upright on the shelf, min_count: 6, max_count: 6}
    - {id: shelved, kind: SUCCESS, description: All boxes shelved}
    - {id: fallen, kind: FAILURE, description: A box fell over}
  transitions:
    - {source: start, target: place}
    - {source: place, target: shelved}
    - {source: place, target: fallen}
EOF
qualia task edit <task-id> shelf.yaml

qualia task get <task-id>                      # each field under its heading, then the procedure
qualia task publish <task-id> -m "first cut"   # needs a description and a success
qualia task revisions <task-id>
qualia task get <task-id> --revision 1
qualia task list
qualia task delete <task-id> [--force]         # account owners only
```

Copy a dataset into another workspace, or move it to another workspace of this
organization. Each prints what it did, as the dashboard does.

```sh
qualia dataset copy <dataset> --to Bench                  # a workspace of this organization, by name or id
qualia dataset copy <dataset> --to <org-id>/<workspace-id> --version v3 --name "Fold bench"
                                               # another organization: by ids; a personal key acts as its creator
qualia dataset move <dataset> --to Bench                  # keeps its id, history and links
# Copied 4.0 GiB, 34 episodes in 0.8 s
```

Copy and move results print cleared robot names and reference-range changes. A move back
also says which robots were restored, or could not be restored after deletion or an edit.

A tagged version that left episodes out rewrites the files that mix kept and
left-out episodes, and can run past its call. `copy` waits and prints how far it
got on stderr; stopping it stops nothing. The request id prints before the first
request, and `--request-id` picks the copy up again within 24 hours. It never
starts a copy: if the server no longer holds that id, it says so and exits 1.

```sh
qualia dataset copy <dataset> --to Bench --version v3 --no-wait    # returns at once with how far it got
qualia dataset copy <dataset> --to Bench --version v3 --request-id <id>
# Copied 1.6 GiB (1.5 GiB rewritten, 3.0 GiB written), 26 episodes in 3 min
```

Link recorded data to the task it was recorded against (CLI 0.3.0rc3). The
label stays the text training reads; a link changes no revision or version, and
deleting the task unlinks.

```sh
qualia dataset link-task <upload-id> <task-id>             # every episode without its own link reads it
qualia dataset link-task <upload-id> <task-id> --relink    # also moves those linked to the old task; --unlink removes it
qualia dataset link-task <merged-id> <task-id>             # a merged dataset: every upload it holds
qualia dataset assign-task -d <upload-id> --task <task-id> --filter 'state = KEPT'
                                               # links and labels them with the task's name
qualia dataset assign-task <episode-id>... -d <upload-id> --label "restock shelf"
                                               # a label alone leaves links as they are
qualia task link-episodes <task-id> --dry-run  # unlinked labels that name it: exact and near misses
qualia task link-episodes <task-id> -l "restock shelf" -l "Restock shelf."
                                               # links those labels' episodes and uploads
qualia task linked <task-id> [--episodes]      # the datasets, or episodes, linked to it
qualia task link <task-id> --episode <episode-name> [--unlink]
                                               # one episode; its label stays
```

Publishing a task links, on its own, the unlinked episodes and uploads whose
label is exactly its name. `task link-episodes` lists those beside the near
misses (case, spacing, punctuation) in your workspace and links the labels you
pass; a link already set never changes.

What prepare found in an upload: the tasks discovery sorted the takes into
(each with its Task, a draft until published), the dataset's own task names,
warnings. A found task with no Task prints the `task create` line that makes one.

```sh
qualia dataset preparation <upload-id>                   # or its slug; --json for the whole record
qualia dataset confirm-task-names <upload-id> --use ts_… # the rest a person's no; --all uses every one
```

### Checks

Run a check over a dataset and read its findings. The VLM check reads each
episode against its linked, published task; `estimate` says what a run would
reserve without starting or charging anything.

```sh
qualia check estimate <dataset> --max-episodes 5 --max-cost 20  # estimate a small trial
qualia check run <dataset> --max-episodes 5 --max-cost 20 --wait  # same sample and credit ceiling
qualia check run <dataset> --model <model> --wait  # --check episode-rules|mixture|reach; - for defaults
qualia check get vlm                             # can this deployment run the VLM check?
qualia check status <run-name>                   # QUEUED, RUNNING, SUCCEEDED or FAILED, with counts
qualia check runs --check vlm -d <dataset> --state FAILED
qualia check findings <dataset>                  # issues and notes, with verdict, confidence and moments
qualia check reach-status <dataset>              # Run solver: its settings and how far the last run got
qualia check reach-cancel <dataset>              # stop the run under way
```

`--max-episodes` and `--max-cost` are positive VLM-only limits. Match both on
`estimate` and `run` to see what the trial reserves before it starts. Omit
them to check the whole eligible scope without a credit ceiling.

`--wait` (here, on `dataset checks run` and on `task draft`) prints each started
run first (to stderr with `--json`), then sits out up to five failed reads in a
row (any 5xx, a 408, a timeout, a dropped connection); any other 4xx, a 429
included, stops it at once. If it still fails, the run keeps
going: follow it with `qualia check status <run-name>` (a draft: `qualia task
draft-get <draft>`).

The checks catalog (versioned checks such as `task.graph`) runs a selection as
one run, one model call per episode for every selected VLM check:

```sh
qualia dataset checks list <dataset>                       # by category: runnable, last result
qualia dataset checks run <dataset> --check task.graph --category quality --validate-only
qualia dataset checks run <dataset> --all --max-episodes 50 --max-cost 120 --wait
qualia dataset checks findings <dataset> --verdict FAIL    # per check and episode
```

`--check` and `--category` repeat and combine with `--all` as a union. A run
reads one kind of check: the kind `--check` names, else VLM when a VLM check can
run here, else the heuristics. It lists the checks it leaves out, with why, but
not `graph.observe` and `graph.evaluate`: they run only inside `qualia check run`.
A run needs `--max-cost` at or above the bound `--validate-only` shows. A last result
that assessed nothing reads `not assessed, 0 of 5 episodes` with the check's
reason, never `0 issues`; a partial one names how many episodes it assessed.

Thresholds and reach:

```sh
qualia check ranges --interface-hash b3:<hex>     # the fences per metric
qualia check run <dataset> --check reach          # start the reach solver
qualia check reach-status <dataset>               # settings, key, last run, stale
qualia check reach-cancel <dataset>               # stop it; solved takes keep their results
qualia check results <dataset> --state SUCCEEDED  # where the robot cannot follow each take
```

### Issues

Read and settle the issues and notes on a dataset. An issue blocks a keep until
it is dismissed or its episode is ruled; a note never blocks.

```sh
qualia dataset issues summary <dataset>                    # open, episodes and dismissed per code
qualia dataset issues list <dataset> --filter 'state = OPEN' -e <episode>
qualia dataset issues dismiss <dataset> --filter 'code = "idle_tail"' --reason "fine for this task"
qualia dataset issues reopen <dataset> <issue>...          # a ruled episode is unreviewed again
qualia dataset issues add <dataset> "Gripper slips" -e <episode> --at 12.5   # --note never blocks
qualia dataset issues reply <issue> "Trimmed it away" -d <dataset>
qualia dataset issues replies <issue> -d <dataset>
qualia dataset issues delete <issue> -d <dataset> --yes    # only what you (or this key) raised
```

`dismiss` and `reopen` print how many issues changed, then the takes another
person is editing, with who holds them: their issues are left as they were.

### Set up a handheld (UMI) dataset

Everything the dashboard's Setup strip does is a command: pick the robot and the
solver, move the poses to the robot's TCP, run the solver and follow it.

```sh
qualia dataset poses <dataset>                   # root | tcp | recorded, the calibration, the capture gripper
qualia dataset setup <dataset> --robot <robot> --solver pyroki   # or dls; robot follows the robot's model
qualia dataset setup <dataset> --base 0 0 0 --heading 0          # placement: metres, degrees
qualia dataset setup <dataset> --robot <robot> --wait            # stay for the resolve
qualia dataset setup <dataset> --run-solver                      # press Run solver (never started by itself)
qualia check reach-status <dataset>              # settings, last run, takes solved, stale
qualia check reach-cancel <dataset>              # stop it; solved takes keep their results
```

Only what you pass is written, and repeating a command writes nothing. Picking
a robot with `--robot` also **starts the resolve at the TCP** when the dataset
is an upload with handheld poses, every arm of the robot has a TCP, the
workspace has a gripper for the upload's rig with a calibration, and the poses
are not at the TCP yet (or are there with another calibration). The resolve
uses that capture gripper's calibration, not the robot's own gripper's; a robot
with no TCP leaves the poses at the gripper root and says so. `--no-resolve`
never resolves, `--resolve` resolves whatever the robot.

A resolve updates the saved pose file and publishes the result. Without `--wait` the
command prints the operation and returns; follow it later, or move the poses by
hand:

```sh
qualia dataset poses <dataset> --follow <operation-id>           # wait for a started resolve
qualia dataset poses <dataset> --at tcp --wait                   # resolve (again) at the TCP
qualia dataset poses <dataset> --at tcp --gripper <gripper-id>   # with another gripper's calibration
qualia dataset poses <dataset> --at root                         # back to the gripper root
```

Progress goes to stderr, so `--json` stays one document on stdout.

For a merged dataset, picking a robot does not move its uploads. Check them and
then explicitly move eligible uploads using the revision printed by the check:

```sh
qualia dataset poses-precheck <merged-dataset>
qualia dataset poses-move <merged-dataset> --revision <revision>
qualia dataset poses-move <merged-dataset> --revision <revision> --member <upload> # retry one
qualia dataset poses-remove <merged-dataset> <upload> --revision <revision>
```

The check reports each upload and its blockers. Each move uses that upload's
capture rig calibration; uploads without a saved pose file cannot use this
path. The precheck currently requires a write-scoped credential; read-scoped
prechecks remain a follow-up. Removal makes the upload standalone again and records the removal;
its data and review decisions, and the merged dataset's old versions, remain.
A move of an upload with split episodes is blocked
until pose correction can preserve its pieces ([QUA-3786](https://linear.app/qualia-studios/issue/QUA-3786)).
Run a fresh check after membership changes.



### Robots and their grippers

`qualia robot` registers robots, imports their URDFs and gives a robot a gripper of
its own to line up and set the jaw of; copy the robot to try another gripper.
`qualia gripper` keeps the workspace's gripper designs, which robots copy;
`qualia gripper list` shows those copies too, named like their source, with the
robot that carries each in the Owner column. A name means the design, so name a
copy by its ID. A few
actions have two names, such as `qualia robot show` and `get`: both work, and the
command reference lists the pairs. Every subcommand and option is in the
[command reference](https://docs.qualiastudios.dev/cli/commands/#qualia-robot).

### JSON output

All commands support `--json` for machine-readable output:

```sh
qualia project list --json
qualia credit --json
```

### Agents and scripts

Everything a person does in the dashboard, an API key can do from here, inside that person's role:
https://docs.qualiastudios.dev/global/guides/agents

`examples/agent_path.sh` runs that path end to end with `--json` against the stack named by
`QUALIA_BASE_URL` and `QUALIA_API_KEY`, on data it makes and deletes. An upload's repository and
stored bytes stay, because no command deletes them, and the script prints a line for each. It prints
a line per step and exits 1 on any failure. It needs an organization key whose creator is an owner.

```sh
QUALIA_BASE_URL=https://dev-api.qualiastudios.dev QUALIA_API_KEY=... ./examples/agent_path.sh
```

## Tests

```sh
cd sdk/cli
../../proto/gen.sh sdk                       # the SDK's generated qualia.v1 models
uv run --locked --all-groups pytest tests/   # builds the SDK's native extension (needs Rust)
```

CI runs them in `test-cli` on every MR that touches the CLI or the SDK.

## API

The CLI is a client for the public Qualia API. Request and response shapes for every command are
in the API reference: https://docs.qualiastudios.dev

Working in the repo instead? `sdk/cli/qualia-cli.spec.md` carries the same contract, and the
gateway generates the OpenAPI spec it is drawn from. Neither ships in the wheel.

## License

MIT, Qualia Studios.

A saved merge destination is used only with a known matching account, normalized
API host and workspace. An environment API key alone does not confirm the saved
account; supply the matching account configuration explicitly. Explicit
`--merge-target` and `--standalone` retain their per-call meaning.
