/ docsCLIllms.txt
On this page
InstallAuthCommandsExit codesCI recipe

CLI — nth

Trigger runs and read results from CI or a script, on a gp_… API key. Its reason to exist is trigger --wait: block until the run reaches a verdict, then set an exit code your pipeline can gate on.

Included with Pro and Team. The CLI and the MCP server both authenticate with a workspace API key, and the key only authenticates while the workspace's plan carries API access — checked on every request, not just when the key was minted.

Install

With Node.js 18 or newer, from npm: npm installs the self-contained program for your computer — macOS, Linux (glibc or musl) or Windows — so nothing else needs installing:

bash
npx @nthcase/cli@0.1.6 --help
npm install -g @nthcase/cli@0.1.6

With .NET 10 or newer, as a .NET tool, from NuGet:

bash
dotnet tool install -g nthcase \
  --version 0.1.6
nth --help

Auth — environment-only, by design

bash
export NTH_API_KEY=gp_your_key   # mint in Settings → API keys
export NTH_API_URL=https://…     # optional: https://api.nthcase.com by default; https:// only
There is deliberately no --api-key flag. A flag leaks the secret into shell history, ps, and CI logs. The key never appears in output, in an error, or in a --verbose line.

Commands

Every command takes --json (the real API DTO shape) and --verbose.

CommandDoes
nth whoamithe key's workspace {id, name} — verifies the key + config
nth scenarios listthe projected lookup table (id, name, state, source, stepCount, updatedAt)
nth scenarios show <scenarioId>one scenario, full definition, and each action that stands between it and a schedule, with its reason
nth trigger <scenarioId> [--wait] [--timeout <sec>] [--model <m>] [--device-spec <json>]trigger a run — the CI command
nth runs list [--scenario <id>] [--status <s>] [--failed]recent runs (paged)
nth runs show <runId>one run summary (status, timing, cost)
nth runs logs <runId> [--since <n>]redacted device/server logs
nth runs network <runId>captured network entries (sensitive headers and saved secrets masked), or why there are none
nth runs cancel <runId> [--force]stop a run — a queued run cancels outright; an executing one needs --force. The record goes terminal at once; the device finishes the step it is already on before it stops
nth scenarios compile <scenarioId> [--wait] [--timeout <sec>] [--model <m>] [--operation-id <guid>]compile a scenario into the plan its runs replay — COSTS CREDITS, and the new plan is what future runs (scheduled ones included) replay by default. --wait exits 0 Completed / 1 Failed / 3 timeout
nth scenarios compile-run <scenarioId> <compileRunId>one compile-run — the authoritative outcome of a compile; poll until terminal
nth scenarios plans <scenarioId>every compiled plan version plus the replay pointer; an unreadable version's UNTRUSTED prints `unreadable`, never 0
nth scenarios set-plan <scenarioId> [--version <n>]choose which compiled version unnamed runs replay — omit --version to clear back to 'follow the newest'
nth scenarios decisions <scenarioId> [--version <n>]the compile pipeline's per-action audit trail (--json for the full detail)
nth devices list [--platform <p>]the farm's leasable devices, so a run targets one that exists
nth catalog [--platform <p>]the provisionable capability catalog (valid deviceSpec values per platform)
nth schedules list [--search <s>] [--status <s>]list schedules — check here BEFORE creating one; a duplicate runs forever on its own cadence
nth schedules show <scheduleId>one schedule AND which scenarios it runs (state + pinned plan per member); the right call after a create that timed out
nth schedules preview <cron> <tz> [--count <n>]the next firings of a cron + timezone, computed server-side — the check on the timezone, not just the cron
nth schedules estimate <scenarioIds> [--model <m>] [--cron <c>] [--tz <t>]the advisory price tag of a schedule before it exists — per run, per cycle, and per month when cron+tz are given
nth schedules create --name <n> --cron <c> --tz <t> [--scenarios <ids>] [--member <id:ver>] […]create an UNATTENDED RECURRING SPEND — no operation id: a retried timeout makes a SECOND schedule; recover with list/show, never a retry
nth schedules update <scheduleId> --name <n> --cron <c> --tz <t> […]FULL REPLACE (members wholesale; the firing clock recomputes from now); enabled/paused state untouched
nth schedules pause <scheduleId>stop future cron firings (idempotent, no confirmation) — a paused schedule is NOT inert: run-now still fires it, deliberately
nth schedules resume <scheduleId>re-enable — the next run recomputes FROM NOW; missed cycles are never fired retroactively; an unevaluable cron/tz is refused honestly
nth schedules run-now <scheduleId>fire ONE Manual cycle through the same launcher and billing gates as cron — A SPEND (check `schedules estimate` first); works on a PAUSED schedule; NO idempotency across calls: every call is a NEW billed cycle — check `schedules show` before retrying
nth discovery trigger <targetId> [--wait] [--timeout <sec>] [--focus <f>] [--avoid <a>] [--max-steps <n>] [--max-tokens <n>] [--max-pages <n>] [--model <m>] [--operation-id <guid>] [--device-spec <json>]start a discovery — COSTS CREDITS and holds a real device for minutes; budgets are clamped server-side and the response echoes what was authorized. --wait exits 0 Completed / 1 Failed / 2 Error / 4 Cancelled / 3 timeout
nth discovery cancel <discoveryId>stop a discovery and stop it spending — the device is released; the partial map is kept but can never ground a compile. Already finished = benign, exit 0
nth discovery list [--target <id>] [--page <n>] [--page-size <n>]list discovery runs, newest first — the only way to obtain a discoveryId
nth discovery show <discoveryId>one discovery's live state — the phase line says what it is doing while it runs
nth discovery detail <discoveryId> [--include-elements]the full report; element lists withheld unless asked for (they are large — the same shaping the MCP applies)
nth discovery logs <discoveryId> [--since <n>] [--source <s>]REDACTED device/server logs, delta pages like runs logs
nth discovery pages <discoveryId> [--seq <n>]the page sources a phone discovery kept, with typed values taken out; --seq prints one page's XML whole to stdout and its facts to stderr
nth nodesTHIS workspace's connected machines and their health — not a device picker; the value is WHY a phone is not usable yet: the default output carries each outcome's reason and remedy, the unusable devices, and the phones the machine has
nth runs detail <runId>one run WITH its per-step results; --json carries each step's full record
nth scenarios create --file <path|->create a scenario from JSON (the same DTO the MCP tool takes) — only ever a DRAFT: compile before it runs, approving stays human-only
nth scenarios update <scenarioId> --file <path|->edit a scenario from JSON — RESETS it to DRAFT; it cannot run until compiled again (costs credits)
nth scenarios unpin <scenarioId> <stepOrder> --selector <s>take a pin off one step: the selector (or drop end) it carries now, which the server checks before it changes anything — RESETS the scenario to DRAFT; compile it again before relying on a run
nth scenarios baselines <scenarioId>every picture a person approved as the scenario's visual baselines — per step, one per screen, with who approved it and through which door, and the run it came from; --json adds each run's device and the picture's address, which opens for minutes. Approving stays a person's, on the run's page
nth scenarios delete-baseline <scenarioId> <baselineId> --yesdelete one approved baseline — CANNOT BE UNDONE: the next run of that step on that screen asks for its picture to be approved again; without --yes nothing is deleted
nth apps register --file <path|-> [--build <buildId>]register a NEW app — created PENDING and inert until a workspace member attests ownership in the app UI (that endpoint takes no API key: hand off to a person). --build names a build uploaded first
nth apps grounding <targetId>which discovery map a compile grounds in — mode, pin, and what they resolve to; pinning stays human
nth apps suggestions <targetId>the AI-proposed scenarios awaiting review, each with its SOURCE (the scope requirement or the discovery) — approving stays human
nth builds upload <path> --platform <Android|iOS>upload a build from this machine (.apk, .ipa, or a .zip holding one simulator .app), streamed as it is read; prints the build's id and the app id and version read from the file. The size limit is read first, so a file over it is never sent
nth apps builds <targetId>the app's uploaded builds, newest first (* marks the one it runs) — name, app id, version, size, when
nth apps update <targetId> --build <buildId>make an uploaded build the one the app runs ("Use this build"); a build of a different app or platform is refused in the server's words. Only the build changes
nth devices provision --platform <p> [--os-version <v>] [--form-factor <f>] [--profile <p>] [--wait] [--timeout <sec>]ask the farm to BUILD an emulator — spends the operator's MACHINE (two per node), not money; NO idempotency key: poll before retrying. --wait exits 0 Ready / 1 Failed / 3 timeout
nth devices provision-status <provisioningId>poll a provision: Queued / Provisioning / Ready (deviceId) / Failed (the farm's own reason)
nth noticesprint the third-party notices: the licences of the open-source code inside the CLI. Needs no key and sends nothing

Exit codes

console
$ nth trigger 7c2f9a14-5b3e-4d8a-a1f0-3e9b6c2d8f51 --wait
Run 4e70d5d5-8f1c-4a3b-9d62-0b7e5c1a9f34 Passed. 2 credits. https://nthcase.com/runs/4e70d5d5-8f1c-4a3b-9d62-0b7e5c1a9f34
$ echo $?
0
console
$ nth trigger 9b1f3c57-2e84-4d6a-a0c9-6e5d4b3a2f18 --wait
Run 3a7e19c4-5b2d-4f60-9c81-d24e6b0f7a35 Failed. 2 credits. https://nthcase.com/runs/3a7e19c4-5b2d-4f60-9c81-d24e6b0f7a35
  step 5 failed (assert): Verify the cart reads "Cart (1)"
  reason: Text of [id="cart-count"] is not exactly "Cart (1)"
$ echo $?
1

Under GitHub Actions a run that failed or ended in Error also prints one ::error :: line to stderr, so the reason shows on the job's summary.

ExitMeaning
0--wait: run Passed; or any non-wait command succeeded
1--wait: run Failed (the test-failure signal); or a non-wait command errored
2--wait: run Error (no verdict: engine/infra broke, or the run could not start — not a test verdict)
3--wait: --timeout elapsed before the run was terminal
4--wait: run Cancelled — somebody stopped it. Neither a pass, a failure, nor an engine error
10usage / bad arguments
11auth/config: missing key, or 401 (invalid/revoked/expired key — or a key whose workspace plan doesn't include API access); an NTH_API_URL that would send the key in cleartext; a redirect; an API that moved on and needs a newer nth; or, from npm, a computer with no program or an install that left its program out
12API unreachable (transport)

Stopped by Ctrl-C or a stop signal, nth exits 130, the shell's own number for an interrupt, and says that a run it already started keeps running until it ends or is cancelled.

CI recipe

Gate a GitHub Actions job on a run's verdict — the job fails on any non-zero exit:

yaml
- name: Smoke test on a real device
  env:
    NTH_API_KEY: ${{ secrets.NTH_API_KEY }}
  run: npx @nthcase/cli@0.1.6 trigger 22222222-2222-2222-2222-222222222222 --wait --timeout 900
  # the job fails on any non-zero exit — the verdict codes are the table above.
The same step in a GitHub workflow, with the key in a repository secret: a failed run's log names the step, the reason, the cost and the run's link, the run's summary shows the reason as an error, and after the fix the job passes. 0:47