pylot-cli
v0.3.0
Published
pylot — first-class CLI for the Pylot gateway API. Replaces curl workflows for operators and humans.
Maintainers
Readme
pylot — the Pylot gateway CLI
First-class CLI for the Pylot gateway API (epic #1802). Replaces every curl
workflow — dispatching missions, tailing logs, team/admin/secrets management —
for both AI operators (claude -p sessions, subagents) and humans.
pylot dispatch "fix the flaky gateway-cache test" --agent infra.lead --repo fellowship-dev/pylot --wait
pylot ps
pylot missions logs <job-id> --follow
pylot deploy --wait --env stagingInstall
npm install -g pylot-cliPublished on npm as pylot-cli
(Node 20+). The package ships a single self-contained bundle and no
credentials: it only talks to the gateway URL you configure, and every
request is authorized by a token your own gateway deployment mints. There is
no hosted signup or OAuth flow — if you don't hold a token for a gateway, the
CLI can't do anything ("closed by default"). Self-hosters: see
SELF-HOSTING.md for deploying a gateway and minting
the dispatch token.
Inside Pylot's own containers no install is needed — the operator image bakes
the bundle in at /usr/local/bin/pylot.
Design contract
- JSON on non-TTY stdout (or
--json): exactly one JSON document per command — the verbatim gateway response body — newline-terminated. All progress notes, hints, and errors go to stderr.pylot missions list | jq .always works. While following logs, entries stream as NDJSON. - Tables on TTY for list commands; pretty-printed JSON otherwise.
- Exit codes:
0success,1API/runtime error,2usage error.missions wait/dispatch --waitexit0only when the mission endsdone. - Destructive commands (
kill,cancel,delete,revoke, …) require--forceand send nothing without it. - Single-file bundle (
dist/index.mjs, esbuild) — no runtimenode_modules.commander+confare compiled in (a global install pulls zero packages); no interactive/prompt libraries.
Configuration
Everything resolves from flags, then environment, then the config file
(pylot config set …, stored via conf; env vars always win).
| | resolution order |
|---|---|
| URL | --url → PYLOT_DISPATCH_URL¹ → PYLOT_API_URL → PYLOT_GATEWAY_URL → PYLOT_API → config file |
| Token | --token → PYLOT_API_TOKEN → PYLOT_SESSION_JWT → PYLOT_BROKER_TOKEN → PYLOT_DISPATCH_TOKEN → config file |
¹ historically holds the full …/dispatch endpoint URL — the CLI strips the
suffix automatically.
--env staging switches to PYLOT_STAGING_URL + PYLOT_STAGING_DISPATCH_TOKEN
(config keys staging-url / staging-token).
Inside Pylot containers the right credential is already injected (token plane
#1884): chat sessions get a session JWT (PYLOT_API_TOKEN), mission workers a
broker JWT (PYLOT_BROKER_TOKEN, scoped to git-token + heartbeat — it cannot
dispatch), operators the master token. pylot auth status shows which source
won and what the gateway thinks of it. On a 403 insufficient_scope the CLI
tells you which scope is missing and how to get it.
Command groups
Run pylot --help (or pylot <group> --help) for the full tree.
| Group | Covers |
|---|---|
| health stats events | liveness, metrics, event ledger |
| dispatch | POST /dispatch with --wait / --follow |
| missions (ps) | list/view/wait/logs/session/deps/audit/report/complete/heartbeat/wake/update/cancel/kill |
| costs executor audits pricing logs | cost reporting, executor sweeps, audit corpus, model pricing, infra logs |
| workers devboxes | devbox workers and standalone devboxes |
| conversations (convo) | full conversation surface: messages, wakes, subscriptions, resources, devboxes, analytics |
| context | one-shot repo context resolver (#2329): owning team/role, playbook (bytes/sha256/complete), operator instructions + skills, execution config, resolved secret names; --check readiness gate. Replaces the manual multi-command startup recipe, not the underlying admin commands |
| teams route | team config, operator instructions, role skills (teams skills — team-first assignment matrix, #2328), crons, ledger, playbooks, goals. teams list shows the composed runtime view; --raw the normalized records (#2330) |
| admin | config, audit, org caps, migrations |
| deploy | POST /admin/deploy + CodeBuild polling (--wait), image builds |
| secrets | path-based ASM bundle/key operations |
| skills automations providers orgs assets | skills catalog/sync (skills list --team — catalog-first assignment discovery, #2328), automation rules, provider chains, repo enablement, asset presign/publish |
| auth config login | identity (auth status), scoped tokens, git tokens, local config |
Notes on log streaming: SSE endpoints return 501 on the deployed (Lambda)
gateway, so missions logs --follow polls
GET /missions/:id/logs/cloudwatch?since_time=<cursor> — including 206
partial-fetch draining — and stops when the mission goes terminal. Identical
boundary timestamps can rarely duplicate a line (CloudWatch ms resolution).
Deliberately not covered (internal plumbing): POST /webhook, /internal/*,
POST /admin/skills/commit, POST /cost-report, worker-daemon turn routes
(workers/poll|heartbeat|report), /admin/worker-keys, /admin/test/*.
PAT-free Git & gh setup
pylot auth setup-git wires short-lived GitHub App installation tokens into
your local git and (optionally) gh so you never need a Personal Access Token.
It writes a credential helper shim and updates ~/.gitconfig — all idempotent
and fully reversible.
Prerequisites
- A configured Pylot CLI. On a machine whose only credentials come from
pylot auth login(per-org tokens in~/.pylot/credentials), this works with no env token: the credential helper derives the repo's org (org/repo→org) and authenticates the mint with that org's stored token — nopylot config set orgneeded (#2332). An explicit--org,PYLOT_API_TOKEN/session token, orpylot config set url/tokenstill take precedence where present. - The Pylot GitHub App installed on the target org/repo
Install
# Wire git credential helper
pylot auth setup-git
# Also install gh shim (opt-in — per-invocation App token for every gh call)
pylot auth setup-git --gh-shim
# Remove a conflicting github.com helper left by `gh auth setup-git` (opt-in;
# it otherwise runs BEFORE the pylot helper and can answer with a stale login)
pylot auth setup-git --fix
# Custom helper install dir (default: ~/.local/bin)
pylot auth setup-git --bin-dir /usr/local/binAfter running, git push to any repo where the App is installed works without
a stored PAT. Commits show as pylot-app[bot].
Without --fix, a conflicting credential.https://github.com.helper (typically
from gh auth setup-git) is detected and warned about, never removed
silently. --fix removes it and records it so --uninstall restores it.
Diagnose (pylot auth doctor)
pylot auth doctor # read-only: mutates nothingReports which credential surface git, gh, and the CLI each resolve
from (env / ~/.pylot/credentials / legacy config), whether the pylot helper is
installed with useHttpPath=true, and flags stale or conflicting helpers (with
the --fix hint). Use it first when git/gh mint the wrong token or fail.
Programmatic token access
# Raw JSON (token, repo, committer identity)
pylot auth git-token --repo fellowship-dev/pylot
# Shell eval — sets GH_TOKEN for a script block
eval $(pylot auth git-token --export --repo fellowship-dev/pylot)
# Used as a git credential helper binary (invoked by git directly)
pylot auth git-token --credential-helperThe --credential-helper mode implements the full git credential protocol:
reads protocol/host/path from stdin, returns username=x-access-token\n
password=<token> for github.com, exits 0 with no output for any other host
(so existing helpers for other services keep working), and caches tokens for
~50 minutes to avoid a round-trip on every git operation.
Uninstall
pylot auth setup-git --uninstallReads the install record saved at setup time, removes the helper from
~/.gitconfig, deletes the shim files, and restores any previous
credential.helper value.
Development
cd modules/cli
npm ci
npm run typecheck # tsc --noEmit (strict)
npm run build # esbuild → dist/index.mjs (executable)
node dist/index.mjs --helpIntegration tests live at the repo root (tests/gateway-cli-*.test.mts) and
run in the corpus gate (./scripts/test.sh corpus): they spawn a real hermetic
gateway (PGlite) and execute the built CLI against it — no mocked fetch.
scripts/test.sh builds the bundle before the test pass.
The operator image bakes the bundle in as /usr/local/bin/pylot
(see Dockerfile.operator); rebuild via /image-builder operator.
Publishing to npm (maintainers)
The package is public on npm as pylot-cli. Releases are pipeline-driven:
- In your PR, bump
versioninmodules/cli/package.json(semver — the bundle embeds it via__CLI_VERSION__, sopylot --versionreports whatever you publish). A PreToolUse hook (.claude/doc-coverage.json) reminds any agent editing gateway route files to do this. - Merge → deploy. Every prod deploy ends with a drift check
(
scripts/ci-cli-publish-trigger.shinbuildspec-cdk-deploy.yml): if the deployed tree's version isn't on npm, it triggers the publish build automatically. Nothing to do. - Manual trigger (same build):
pylot deploy publish-cli --wait(POST/admin/publish-cli→buildspec-cli-publish.yml→scripts/publish-cli.shon the pylot-builder CodeBuild project). Idempotent — if the version is already on npm it no-ops.--dry-runrehearses without the token; staging builders are always forced dry-run (npmlatesttracks prod/main only). - Verify:
npm view pylot-cli versionandnpx -y pylot-cli@latest --version.
Credentials: NPM_TOKEN (npm Automation token — bypasses 2FA, publish
rights on pylot-cli) lives in ASM pylot/npm; the CodeBuild service
role reads it at run time. That bundle is deliberately outside the gateway
secrets hierarchy (the 4-layer path validator rejects pylot/npm), so no
team/role/repo scope — and therefore no mission container — can ever resolve
it. It is never a repo secret, never an env override, and never logged. Local manual publish still works from modules/cli/ with
NPM_TOKEN in your env (npm publish --access public — prepublishOnly
runs typecheck + fresh build; the files allowlist keeps the tarball to
dist/index.mjs + README).
Version discipline: any change to gateway routes that the CLI wraps should land with a CLI bump in the same PR train, so the published CLI never lags the deployed API surface.
