pylot-cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -15
- package/dist/index.mjs +52 -18
- package/package.json +3 -5
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pylot — the Pylot gateway CLI
|
|
2
2
|
|
|
3
3
|
First-class CLI for the Pylot gateway API (epic #1802). Replaces every curl
|
|
4
|
-
workflow — dispatching missions, tailing logs,
|
|
4
|
+
workflow — dispatching missions, tailing logs, team/admin/secrets management —
|
|
5
5
|
for both AI operators (`claude -p` sessions, subagents) and humans.
|
|
6
6
|
|
|
7
7
|
```
|
|
@@ -41,7 +41,8 @@ the bundle in at `/usr/local/bin/pylot`.
|
|
|
41
41
|
- **Destructive commands** (`kill`, `cancel`, `delete`, `revoke`, …) require
|
|
42
42
|
`--force` and send nothing without it.
|
|
43
43
|
- Single-file bundle (`dist/index.mjs`, esbuild) — no runtime `node_modules`.
|
|
44
|
-
|
|
44
|
+
`commander` + `conf` are compiled in (a global install pulls zero packages);
|
|
45
|
+
no interactive/prompt libraries.
|
|
45
46
|
|
|
46
47
|
## Configuration
|
|
47
48
|
|
|
@@ -78,7 +79,7 @@ Run `pylot --help` (or `pylot <group> --help`) for the full tree.
|
|
|
78
79
|
| `costs` `executor` `audits` `pricing` `logs` | cost reporting, executor sweeps, audit corpus, model pricing, infra logs |
|
|
79
80
|
| `workers` `devboxes` | devbox workers and standalone devboxes |
|
|
80
81
|
| `conversations` (`convo`) | full conversation surface: messages, wakes, subscriptions, resources, devboxes, analytics |
|
|
81
|
-
| `
|
|
82
|
+
| `teams` `route` | team config, operator instructions, role skills, crons, ledger, playbooks, goals |
|
|
82
83
|
| `admin` | config, audit, org caps, migrations |
|
|
83
84
|
| `deploy` | POST /admin/deploy + CodeBuild polling (`--wait`), image builds |
|
|
84
85
|
| `secrets` | path-based ASM bundle/key operations |
|
|
@@ -115,18 +116,33 @@ The operator image bakes the bundle in as `/usr/local/bin/pylot`
|
|
|
115
116
|
|
|
116
117
|
## Publishing to npm (maintainers)
|
|
117
118
|
|
|
118
|
-
The package is public on npm as `pylot-cli`.
|
|
119
|
-
|
|
120
|
-
1.
|
|
121
|
-
via `__CLI_VERSION__`, so `pylot --version` reports
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
119
|
+
The package is public on npm as `pylot-cli`. Releases are pipeline-driven:
|
|
120
|
+
|
|
121
|
+
1. In your PR, bump `version` in `modules/cli/package.json` (semver — the
|
|
122
|
+
bundle embeds it via `__CLI_VERSION__`, so `pylot --version` reports
|
|
123
|
+
whatever you publish). A PreToolUse hook (`.claude/doc-coverage.json`)
|
|
124
|
+
reminds any agent editing gateway route files to do this.
|
|
125
|
+
2. Merge → deploy. Every **prod** deploy ends with a drift check
|
|
126
|
+
(`scripts/ci-cli-publish-trigger.sh` in `buildspec-cdk-deploy.yml`): if the
|
|
127
|
+
deployed tree's version isn't on npm, it triggers the publish build
|
|
128
|
+
automatically. Nothing to do.
|
|
129
|
+
3. Manual trigger (same build): `pylot deploy publish-cli --wait`
|
|
130
|
+
(POST `/admin/publish-cli` → `buildspec-cli-publish.yml` →
|
|
131
|
+
`scripts/publish-cli.sh` on the pylot-builder CodeBuild project).
|
|
132
|
+
Idempotent — if the version is already on npm it no-ops. `--dry-run`
|
|
133
|
+
rehearses without the token; staging builders are always forced dry-run
|
|
134
|
+
(npm `latest` tracks prod/main only).
|
|
135
|
+
4. Verify: `npm view pylot-cli version` and `npx -y pylot-cli@latest --version`.
|
|
136
|
+
|
|
137
|
+
Credentials: `NPM_TOKEN` (npm **Automation** token — bypasses 2FA, publish
|
|
138
|
+
rights on `pylot-cli`) lives in ASM **`pylot/npm`**; the CodeBuild service
|
|
139
|
+
role reads it at run time. That bundle is deliberately outside the gateway
|
|
140
|
+
secrets hierarchy (the 4-layer path validator rejects `pylot/npm`), so no
|
|
141
|
+
team/role/repo scope — and therefore no mission container — can ever resolve
|
|
142
|
+
it. It is never a repo secret, never an env override, and never logged. Local manual publish still works from `modules/cli/` with
|
|
143
|
+
`NPM_TOKEN` in your env (`npm publish --access public` — `prepublishOnly`
|
|
144
|
+
runs typecheck + fresh build; the `files` allowlist keeps the tarball to
|
|
145
|
+
`dist/index.mjs` + README).
|
|
130
146
|
|
|
131
147
|
Version discipline: any change to gateway routes that the CLI wraps should
|
|
132
148
|
land with a CLI bump in the same PR train, so the published CLI never lags
|
package/dist/index.mjs
CHANGED
|
@@ -15177,23 +15177,23 @@ function registerConversations(program3) {
|
|
|
15177
15177
|
});
|
|
15178
15178
|
}
|
|
15179
15179
|
|
|
15180
|
-
// src/commands/
|
|
15180
|
+
// src/commands/teams.mts
|
|
15181
15181
|
async function readText(file) {
|
|
15182
15182
|
const { readFileSync } = await import("node:fs");
|
|
15183
15183
|
return file === "-" ? readFileSync(0, "utf8") : readFileSync(file, "utf8");
|
|
15184
15184
|
}
|
|
15185
|
-
function
|
|
15186
|
-
const
|
|
15187
|
-
|
|
15185
|
+
function registerTeams(program3) {
|
|
15186
|
+
const teams = program3.command("teams").description("team config, operators, playbooks, goals, ledger");
|
|
15187
|
+
teams.command("list").description("team roster (GET /crew); --db for the raw DB view (GET /crew/_db)").option("--db", "DB-authoritative view (GET /crew/_db)").action(async (flags, cmd) => {
|
|
15188
15188
|
const { opts, cfg } = await ctx(cmd);
|
|
15189
15189
|
emit(opts, await request(cfg, flags.db ? "/crew/_db" : "/crew"));
|
|
15190
15190
|
});
|
|
15191
|
-
|
|
15191
|
+
teams.command("update").description("patch team config fields (PATCH /admin/crew/:team)").argument("<team>").argument("[fields...]", "key=value pairs (e.g. max_concurrent=3 enabled=true)").option("--file <path>", "JSON body from file ('-' for stdin)").action(async (team, fields, flags, cmd) => {
|
|
15192
15192
|
const { opts, cfg } = await ctx(cmd);
|
|
15193
15193
|
const body = await bodyFromArgs(fields, flags.file);
|
|
15194
15194
|
emit(opts, await request(cfg, `/admin/crew/${encodeURIComponent(team)}`, { method: "PATCH", body }));
|
|
15195
15195
|
});
|
|
15196
|
-
|
|
15196
|
+
teams.command("delete").description("delete a team (DELETE /admin/crew/:team?confirm=true)").argument("<team>").option("--force", "required to confirm deletion").action(async (team, flags, cmd) => {
|
|
15197
15197
|
const { opts, cfg } = await ctx(cmd);
|
|
15198
15198
|
if (!flags.force) throw new UsageError("deleting a team is destructive \u2014 re-run with --force");
|
|
15199
15199
|
emit(
|
|
@@ -15204,7 +15204,7 @@ function registerCrew(program3) {
|
|
|
15204
15204
|
})
|
|
15205
15205
|
);
|
|
15206
15206
|
});
|
|
15207
|
-
|
|
15207
|
+
teams.command("instructions").description("operator instructions text (GET /admin/crew/:team/operators/:role/instructions); --set --file to replace").argument("<team>").argument("<role>").option("--set", "replace the instructions (PUT, raw text body)").option("--file <path>", "text from file ('-' for stdin; required with --set)").action(async (team, role, flags, cmd) => {
|
|
15208
15208
|
const { opts, cfg } = await ctx(cmd);
|
|
15209
15209
|
const path5 = `/admin/crew/${encodeURIComponent(team)}/operators/${encodeURIComponent(role)}/instructions`;
|
|
15210
15210
|
if (flags.set) {
|
|
@@ -15214,7 +15214,7 @@ function registerCrew(program3) {
|
|
|
15214
15214
|
}
|
|
15215
15215
|
emit(opts, await request(cfg, path5));
|
|
15216
15216
|
});
|
|
15217
|
-
|
|
15217
|
+
teams.command("skills-add").description("add one skill to an operator (POST /admin/crew/:team/operators/:role/skills, body {skill, repo?, ref?})").argument("<team>").argument("<role>").argument("<skill>").option("--repo <org/repo>", "source repo \u2014 required when the skill is not yet in the org catalog").option("--ref <ref>", "source ref for fetch-on-add").action(async (team, role, skill, flags, cmd) => {
|
|
15218
15218
|
const { opts, cfg } = await ctx(cmd);
|
|
15219
15219
|
const body = { skill };
|
|
15220
15220
|
if (flags.repo) body.repo = flags.repo;
|
|
@@ -15227,7 +15227,7 @@ function registerCrew(program3) {
|
|
|
15227
15227
|
})
|
|
15228
15228
|
);
|
|
15229
15229
|
});
|
|
15230
|
-
|
|
15230
|
+
teams.command("skills-remove").description("remove one skill from an operator (DELETE /admin/crew/:team/operators/:role/skills/:name)").argument("<team>").argument("<role>").argument("<skill>").option("--force", "required to confirm removal").action(async (team, role, skill, flags, cmd) => {
|
|
15231
15231
|
const { opts, cfg } = await ctx(cmd);
|
|
15232
15232
|
if (!flags.force) throw new UsageError("removing a skill is destructive \u2014 re-run with --force");
|
|
15233
15233
|
emit(
|
|
@@ -15239,7 +15239,7 @@ function registerCrew(program3) {
|
|
|
15239
15239
|
)
|
|
15240
15240
|
);
|
|
15241
15241
|
});
|
|
15242
|
-
|
|
15242
|
+
teams.command("crons").description("enable/disable one named cron entry (PATCH /admin/crew/:team/crons/:name)").argument("<team>").argument("<name>").option("--enable", "set enabled: true").option("--disable", "set enabled: false").action(async (team, name, flags, cmd) => {
|
|
15243
15243
|
const { opts, cfg } = await ctx(cmd);
|
|
15244
15244
|
if (Boolean(flags.enable) === Boolean(flags.disable)) {
|
|
15245
15245
|
throw new UsageError("pass exactly one of --enable / --disable");
|
|
@@ -15252,7 +15252,7 @@ function registerCrew(program3) {
|
|
|
15252
15252
|
})
|
|
15253
15253
|
);
|
|
15254
15254
|
});
|
|
15255
|
-
|
|
15255
|
+
teams.command("ledger").description("query team ledger entries (GET /admin/crew/:team/ledger)").argument("<team>").option("--type <type>", "heartbeat | dispatch | completion | event").option("--actor <actor>", "filter by actor").option("--days <n>", "lookback days (default 1)", (v) => parseInt(v, 10)).option("--limit <n>", "max rows (default 20)", (v) => parseInt(v, 10)).action(async (team, flags, cmd) => {
|
|
15256
15256
|
const { opts, cfg } = await ctx(cmd);
|
|
15257
15257
|
emit(
|
|
15258
15258
|
opts,
|
|
@@ -15261,14 +15261,14 @@ function registerCrew(program3) {
|
|
|
15261
15261
|
})
|
|
15262
15262
|
);
|
|
15263
15263
|
});
|
|
15264
|
-
|
|
15264
|
+
teams.command("ledger-add").description("insert a ledger entry (POST /admin/crew/:team/ledger)").argument("<team>").requiredOption("--type <type>", "heartbeat | dispatch | completion | event").requiredOption("--summary <text>", "entry summary").option("--actor <actor>", "attributed actor").option("--metadata-file <path>", "JSON metadata from file ('-' for stdin)").action(async (team, flags, cmd) => {
|
|
15265
15265
|
const { opts, cfg } = await ctx(cmd);
|
|
15266
15266
|
const body = { type: flags.type, summary: flags.summary };
|
|
15267
15267
|
if (flags.actor) body.actor = flags.actor;
|
|
15268
15268
|
if (flags.metadataFile) body.metadata = JSON.parse(await readText(flags.metadataFile));
|
|
15269
15269
|
emit(opts, await request(cfg, `/admin/crew/${encodeURIComponent(team)}/ledger`, { method: "POST", body }));
|
|
15270
15270
|
});
|
|
15271
|
-
|
|
15271
|
+
teams.command("playbook").description("repo playbook text (GET /admin/playbooks/:org/repo); --set --file to replace").argument("<org/repo>").option("--set", "replace the playbook (PUT, raw text body)").option("--file <path>", "text from file ('-' for stdin; required with --set)").action(async (repo, flags, cmd) => {
|
|
15272
15272
|
const { opts, cfg } = await ctx(cmd);
|
|
15273
15273
|
const path5 = `/admin/playbooks/${repo}`;
|
|
15274
15274
|
if (flags.set) {
|
|
@@ -15278,7 +15278,7 @@ function registerCrew(program3) {
|
|
|
15278
15278
|
}
|
|
15279
15279
|
emit(opts, await request(cfg, path5));
|
|
15280
15280
|
});
|
|
15281
|
-
|
|
15281
|
+
teams.command("goals").description(
|
|
15282
15282
|
"team goals text (GET /admin/goals/:team); --set --file replaces (PUT raw text), --update --file appends the file text (PATCH JSON {append})"
|
|
15283
15283
|
).argument("<team>").option("--set", "replace the goals (PUT, raw text body)").option("--update", "append to the goals (PATCH, JSON {append: <file text>})").option("--file <path>", "text from file ('-' for stdin; required with --set/--update)").action(async (team, flags, cmd) => {
|
|
15284
15284
|
const { opts, cfg } = await ctx(cmd);
|
|
@@ -15382,6 +15382,17 @@ async function waitForBuild(opts, cfg, buildId, { timeoutMs, promoteKey }) {
|
|
|
15382
15382
|
emit(opts, resp);
|
|
15383
15383
|
process.exit(ok ? 0 : 1);
|
|
15384
15384
|
}
|
|
15385
|
+
function withParentBuildOpts(flags, cmd) {
|
|
15386
|
+
const parent = cmd.parent;
|
|
15387
|
+
if (!parent) return flags;
|
|
15388
|
+
const parentOpts = parent.opts();
|
|
15389
|
+
for (const key of ["sourceVersion", "wait", "timeout"]) {
|
|
15390
|
+
if (cmd.getOptionValueSource(key) !== "cli" && parent.getOptionValueSource(key) === "cli") {
|
|
15391
|
+
flags[key] = parentOpts[key];
|
|
15392
|
+
}
|
|
15393
|
+
}
|
|
15394
|
+
return flags;
|
|
15395
|
+
}
|
|
15385
15396
|
function registerDeploy(program3) {
|
|
15386
15397
|
const deploy = program3.command("deploy").description(
|
|
15387
15398
|
"trigger a cdk deploy via CodeBuild (POST /admin/deploy) \u2014 a 409 means a deploy is already running (deploy lock held: {stage, held_by, since, build_id}); wait for it or poll `pylot deploy status`"
|
|
@@ -15418,6 +15429,7 @@ function registerDeploy(program3) {
|
|
|
15418
15429
|
deploy.command("build-operator").description(
|
|
15419
15430
|
'rebuild the operator image (POST /admin/build-operator). With --wait: SUCCEEDED alone is not "promoted" \u2014 completion requires operator_promote with no failures'
|
|
15420
15431
|
).option("--source-version <ref>", "git ref to build (default: gateway's stage branch)").option("--wait", "poll until promoted (exit 0 only on SUCCEEDED + clean operator_promote)").option("--timeout <seconds>", "max seconds to wait (default 1500)", (v) => parseInt(v, 10), 1500).action(async (flags, cmd) => {
|
|
15432
|
+
flags = withParentBuildOpts(flags, cmd);
|
|
15421
15433
|
const { opts, cfg } = await ctx(cmd);
|
|
15422
15434
|
const body = {};
|
|
15423
15435
|
if (flags.sourceVersion) body.source_version = flags.sourceVersion;
|
|
@@ -15434,9 +15446,31 @@ function registerDeploy(program3) {
|
|
|
15434
15446
|
note(opts, `operator build started: ${buildId}`);
|
|
15435
15447
|
await waitForBuild(opts, cfg, buildId, { timeoutMs: flags.timeout * 1e3, promoteKey: "operator_promote" });
|
|
15436
15448
|
});
|
|
15449
|
+
deploy.command("publish-cli").description(
|
|
15450
|
+
"publish modules/cli to npm as pylot-cli (POST /admin/publish-cli). Idempotent: no-ops when the tree's version is already on npm. Staging builders always dry-run"
|
|
15451
|
+
).option("--source-version <ref>", "git ref to publish from (default: gateway's stage branch)").option("--dry-run", "typecheck + build + npm publish --dry-run (no token, nothing published)").option("--wait", "poll until the build finishes (exit 0 only on SUCCEEDED)").option("--timeout <seconds>", "max seconds to wait (default 900)", (v) => parseInt(v, 10), 900).action(async (flags, cmd) => {
|
|
15452
|
+
flags = withParentBuildOpts(flags, cmd);
|
|
15453
|
+
const { opts, cfg } = await ctx(cmd);
|
|
15454
|
+
const body = {};
|
|
15455
|
+
if (flags.sourceVersion) body.source_version = flags.sourceVersion;
|
|
15456
|
+
if (flags.dryRun) body.dry_run = true;
|
|
15457
|
+
const resp = await request(cfg, "/admin/publish-cli", { method: "POST", body });
|
|
15458
|
+
if (!flags.wait) {
|
|
15459
|
+
emit(opts, resp);
|
|
15460
|
+
return;
|
|
15461
|
+
}
|
|
15462
|
+
const buildId = resp.json?.build_id;
|
|
15463
|
+
if (!buildId) {
|
|
15464
|
+
emit(opts, resp);
|
|
15465
|
+
process.exit(1);
|
|
15466
|
+
}
|
|
15467
|
+
note(opts, `pylot-cli publish build started: ${buildId}`);
|
|
15468
|
+
await waitForBuild(opts, cfg, buildId, { timeoutMs: flags.timeout * 1e3 });
|
|
15469
|
+
});
|
|
15437
15470
|
deploy.command("build-worker").description(
|
|
15438
15471
|
"build a repo's worker image (POST /admin/build-worker, body {repo}); --batch --file posts {repos: [...]} to /admin/build-worker/batch. With --wait: completion requires worker_promote with no failures"
|
|
15439
15472
|
).argument("[org/repo]", "repo to build (omit with --batch)").option("--source-version <ref>", "git ref to build the worker overlay from").option("--batch", "batch mode (POST /admin/build-worker/batch)").option("--file <path>", "JSON body from file ('-' for stdin; required with --batch)").option("--wait", "poll until promoted (exit 0 only on SUCCEEDED + clean worker_promote)").option("--timeout <seconds>", "max seconds to wait (default 1500)", (v) => parseInt(v, 10), 1500).action(async (repo, flags, cmd) => {
|
|
15473
|
+
flags = withParentBuildOpts(flags, cmd);
|
|
15440
15474
|
const { opts, cfg } = await ctx(cmd);
|
|
15441
15475
|
if (flags.batch) {
|
|
15442
15476
|
if (!flags.file) throw new UsageError('--batch requires --file <path|-> with {repos: ["org/repo", ...]}');
|
|
@@ -15760,12 +15794,12 @@ function registerAssets(program3) {
|
|
|
15760
15794
|
}
|
|
15761
15795
|
|
|
15762
15796
|
// src/index.mts
|
|
15763
|
-
var VERSION = true ? "0.1.
|
|
15797
|
+
var VERSION = true ? "0.1.2" : "0.0.0-dev";
|
|
15764
15798
|
installEpipeGuard();
|
|
15765
15799
|
var program2 = new Command();
|
|
15766
15800
|
program2.name("pylot").description(
|
|
15767
|
-
"Pylot gateway CLI \u2014 dispatch missions, tail logs, manage
|
|
15768
|
-
).version(VERSION).option("--url <url>", "gateway base URL (default: PYLOT_DISPATCH_URL / PYLOT_API_URL / PYLOT_GATEWAY_URL)").option("--token <token>", "bearer token (default: PYLOT_API_TOKEN / PYLOT_SESSION_JWT / PYLOT_BROKER_TOKEN / PYLOT_DISPATCH_TOKEN)").option("--env <env>", "target environment: prod | staging (staging uses PYLOT_STAGING_URL + PYLOT_STAGING_DISPATCH_TOKEN)").option("--json", "force JSON output even on a TTY").option("-q, --quiet", "suppress progress notes on stderr");
|
|
15801
|
+
"Pylot gateway CLI \u2014 dispatch missions, tail logs, manage teams/admin/secrets without curl.\nJSON on non-TTY stdout (pipe-friendly for agents and jq); tables on TTY."
|
|
15802
|
+
).allowExcessArguments(false).configureHelp({ sortSubcommands: true }).version(VERSION).option("--url <url>", "gateway base URL (default: PYLOT_DISPATCH_URL / PYLOT_API_URL / PYLOT_GATEWAY_URL)").option("--token <token>", "bearer token (default: PYLOT_API_TOKEN / PYLOT_SESSION_JWT / PYLOT_BROKER_TOKEN / PYLOT_DISPATCH_TOKEN)").option("--env <env>", "target environment: prod | staging (staging uses PYLOT_STAGING_URL + PYLOT_STAGING_DISPATCH_TOKEN)").option("--json", "force JSON output even on a TTY").option("-q, --quiet", "suppress progress notes on stderr");
|
|
15769
15803
|
program2.exitOverride();
|
|
15770
15804
|
program2.configureOutput({ outputError: (str, write) => write(str) });
|
|
15771
15805
|
registerCore(program2);
|
|
@@ -15777,7 +15811,7 @@ registerObservability(program2);
|
|
|
15777
15811
|
registerWorkers(program2);
|
|
15778
15812
|
registerDevboxes(program2);
|
|
15779
15813
|
registerConversations(program2);
|
|
15780
|
-
|
|
15814
|
+
registerTeams(program2);
|
|
15781
15815
|
registerAdmin(program2);
|
|
15782
15816
|
registerDeploy(program2);
|
|
15783
15817
|
registerSecrets(program2);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pylot-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "pylot — first-class CLI for the Pylot gateway API. Replaces curl workflows for operators and humans.",
|
|
6
6
|
"bin": {
|
|
@@ -34,12 +34,10 @@
|
|
|
34
34
|
"typecheck": "tsc --noEmit",
|
|
35
35
|
"prepublishOnly": "npm run typecheck && npm run build"
|
|
36
36
|
},
|
|
37
|
-
"dependencies": {
|
|
38
|
-
"commander": "^12.1.0",
|
|
39
|
-
"conf": "^13.1.0"
|
|
40
|
-
},
|
|
41
37
|
"devDependencies": {
|
|
42
38
|
"@types/node": "^22.10.0",
|
|
39
|
+
"commander": "^12.1.0",
|
|
40
|
+
"conf": "^13.1.0",
|
|
43
41
|
"esbuild": "^0.25.0",
|
|
44
42
|
"typescript": "^5.7.2"
|
|
45
43
|
}
|