@standards/cli 1.0.0-alpha.419 → 1.0.0-alpha.425

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/dist/bin.mjs CHANGED
@@ -4,30 +4,57 @@ import { t as discoverBundles } from "./discover-bundles-DPpEZVv0.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { styleText } from "node:util";
6
6
  import { BundleConflictError, StandardsRequestError, createTransport, syncBundle } from "@standards/client";
7
- import { Command } from "commander";
8
- import { BUNDLE_LOCALES, COUNT_MODES, MEETING_RESPONSES, RECORDING_OVERRIDES, RECORD_ACCESS_ROLES, RESOURCE_VISIBILITIES, ValidationError, canonicalStringify, diffSchemaState, formatByteSize, parseRecordAccessRole, parseSchemaState, parseSkillSource, schemaStateSourcePayload } from "@standards/schema";
9
- import Table from "cli-table3";
10
- import { createAppClient } from "@standards/client/app";
7
+ import { Command, Option } from "commander";
8
+ import { BUNDLE_LOCALES, COUNT_MODES, DECISION_TYPE_AGENT_SETUP, DECISION_TYPE_BUNDLE_SETUP, ENVIRONMENT_HARNESSES, ENVIRONMENT_IMAGES, ENVIRONMENT_SIZES, LINK_MAX_MS, NotFoundError, PROVIDER_NAMES, RECORD_ACCESS_ROLES, RESOURCE_VISIBILITIES, SchemaError, SchemaErrorCode, ValidationError, canonicalStringify, diffSchemaState, environmentHarness, formatByteSize, formatDecisionLink, isEnvironmentHarnessId, isPlainRecord, parseRecordAccessRole, parseSchemaState, parseSkillSource, schemaStateSourcePayload } from "@standards/schema";
11
9
  import { createWriteStream } from "node:fs";
12
- import { chmod, mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
10
+ import { chmod, lstat, mkdir, readFile, readdir, readlink, stat, symlink, unlink, writeFile } from "node:fs/promises";
13
11
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
14
- import { createHash } from "node:crypto";
12
+ import Table from "cli-table3";
13
+ import { StandardsRequestError as StandardsRequestError$1, TURN_END_TYPES, createAppClient, openAgentStream, schemaApiErrorCode } from "@standards/client/app";
14
+ import { homedir, hostname } from "node:os";
15
15
  import { Readable } from "node:stream";
16
16
  import { pipeline } from "node:stream/promises";
17
17
  import { buildOperationPath, recordsOperations } from "@standards/schema/api";
18
- import { stdin, stdout } from "node:process";
18
+ import { stdin } from "node:process";
19
19
  import { createInterface } from "node:readline/promises";
20
- import { homedir } from "node:os";
21
- import { isSafeSkillFilePath, skillFileText } from "@standards/constants";
20
+ import { CLI_CLIENT_ID, CLI_DEVICE_LABEL_MAX, CLI_LOGIN_SCOPE, isSafeSkillFilePath, skillFileText, slugify } from "@standards/constants";
21
+ import * as oauth from "oauth4webapi";
22
+ import { createHash, randomUUID } from "node:crypto";
23
+ // src/discovery/order-bundles.ts
24
+ /**
25
+ * Orders discovered bundles so each comes after the bundles it `uses`.
26
+ * Bundles a `uses` names that were not discovered are already the host's concern.
27
+ */
28
+ async function orderByUses(discovered) {
29
+ const byName = new Map(discovered.map((entry) => [entry.bundle.name, entry]));
30
+ const ordered = [];
31
+ const visited = /* @__PURE__ */ new Set();
32
+ async function visit(entry) {
33
+ if (visited.has(entry.bundle.name)) return;
34
+ visited.add(entry.bundle.name);
35
+ for (const used of (await entry.bundle.build()).uses) {
36
+ const dependency = byName.get(used);
37
+ if (dependency) await visit(dependency);
38
+ }
39
+ ordered.push(entry);
40
+ }
41
+ for (const entry of discovered) await visit(entry);
42
+ return ordered;
43
+ }
22
44
  // src/output.ts
23
45
  const MAX_CELL_WIDTH = 50;
24
46
  function truncate(value, maxLength) {
25
47
  if (value.length <= maxLength) return value;
26
48
  return `${value.slice(0, maxLength - 1)}…`;
27
49
  }
28
- function escapeCsvValue(value) {
50
+ /** A cell's text: a decoded `Date` as the ISO string the server sent, an object as JSON. */
51
+ function cellText(value) {
29
52
  if (value === null || value === void 0) return "";
30
- const str = typeof value === "object" ? JSON.stringify(value) : String(value);
53
+ if (value instanceof Date) return value.toISOString();
54
+ return typeof value === "object" ? JSON.stringify(value) : String(value);
55
+ }
56
+ function escapeCsvValue(value) {
57
+ const str = cellText(value);
31
58
  if (str.includes(",") || str.includes("\"") || str.includes("\n")) return `"${str.replace(/"/g, "\"\"")}"`;
32
59
  return str;
33
60
  }
@@ -50,11 +77,7 @@ function formatTable(items) {
50
77
  }
51
78
  });
52
79
  for (const item of items) {
53
- const row = headers.map((h) => {
54
- const val = item[h];
55
- if (val === null || val === void 0) return "";
56
- return truncate(typeof val === "object" ? JSON.stringify(val) : String(val), MAX_CELL_WIDTH);
57
- });
80
+ const row = headers.map((h) => truncate(cellText(item[h]), MAX_CELL_WIDTH));
58
81
  table.push(row);
59
82
  }
60
83
  return table.toString();
@@ -110,9 +133,56 @@ function parseCount(flag, value) {
110
133
  if (!Number.isInteger(parsed) || parsed < 0) throw new ValidationError(`${flag} must be a non-negative integer`, []);
111
134
  return parsed;
112
135
  }
136
+ /** A `<n><unit>` duration in milliseconds; an unknown unit or one outside `[min, max]` is refused. */
137
+ function parseDuration(flag, value, rules) {
138
+ const { units, min = 1, max = Number.POSITIVE_INFINITY } = rules;
139
+ const match = new RegExp(`^(\\d+)(${Object.keys(units).join("|")})$`).exec(value.trim());
140
+ const unit = match?.[2] === void 0 ? void 0 : units[match[2]];
141
+ const ms = match && unit !== void 0 ? Number(match[1]) * unit : NaN;
142
+ if (!(ms >= min && ms <= max)) throw new ValidationError(`${flag} takes a duration such as ${rules.example}, ${rules.limit}: got "${value}"`, []);
143
+ return ms;
144
+ }
145
+ /** The `--harness` value that names the built-in agent: no harness at all. */
146
+ const STANDARDS_HARNESS = "standards";
147
+ /** `--harness` ids: catalogue ids, or `standards` alone (the built-in agent), which is `null`. */
148
+ function parseHarnessIds(ids) {
149
+ if (ids.length === 1 && ids[0] === "standards") return null;
150
+ const unknown = ids.filter((id) => !isEnvironmentHarnessId(id));
151
+ if (ids.length === 0 || unknown.length > 0) {
152
+ const known = [...ENVIRONMENT_HARNESSES.map((harness) => harness.id), STANDARDS_HARNESS];
153
+ throw new ValidationError(`--harness takes ${known.join(", ")}${unknown.length > 0 ? `: got "${unknown.join(", ")}"` : ""}`, []);
154
+ }
155
+ return ids.filter(isEnvironmentHarnessId);
156
+ }
157
+ /** Every item of a cursor-paged read, following `nextCursor` to the end. */
158
+ async function readAllPages(read) {
159
+ const items = [];
160
+ let cursor;
161
+ do {
162
+ const page = await read(cursor);
163
+ items.push(...page.items);
164
+ cursor = page.nextCursor ?? void 0;
165
+ } while (cursor);
166
+ return items;
167
+ }
168
+ /**
169
+ * Every machine `GET /machines` lists for `query`, every page. A host with no
170
+ * machines answers `CONFIGURATION_REQUIRED`, which reads as none.
171
+ */
172
+ async function readAllMachines(client, query = {}) {
173
+ try {
174
+ return await readAllPages((cursor) => client.machines.list({
175
+ ...query,
176
+ ...cursor && { cursor }
177
+ }));
178
+ } catch (error) {
179
+ if (schemaApiErrorCode(error) === SchemaErrorCode.CONFIGURATION_REQUIRED) return [];
180
+ throw error;
181
+ }
182
+ }
113
183
  function getClientFromCommand(cmd) {
114
184
  const root = getGlobalOptions(cmd);
115
- if (!(root.apiUrl && root.apiKey)) throw new Error("No Standards instance configured. Run \"standards login\" or pass --api-key.");
185
+ if (!(root.apiUrl && root.apiKey)) throw new ValidationError("No Standards instance configured. Run \"standards login\" or pass --api-key.", []);
116
186
  return createClient({
117
187
  apiUrl: root.apiUrl,
118
188
  apiKey: root.apiKey,
@@ -124,99 +194,80 @@ function getAppClientFromCommand(cmd) {
124
194
  return createAppClient(getClientFromCommand(cmd));
125
195
  }
126
196
  /**
127
- * The `--tenant` guard every admin command shares. The API takes the tenant in
128
- * the body as a confirmation of intent, never as a target selector: it refuses
129
- * any value other than the authenticated tenant. The CLI cannot invent it, so
130
- * it asks for it rather than guessing the primary tenant.
131
- *
132
- * `message` is passed whole rather than composed: each command's exact wording
133
- * is what its users (and its tests) read.
134
- */
135
- function requireTenant(cmd, message) {
136
- const { tenant } = getGlobalOptions(cmd);
137
- if (!tenant) throw new Error(message);
138
- return tenant;
139
- }
140
- /**
141
197
  * The `--yes` guard for a destructive or outbound command: shared rather than
142
198
  * inlined so the refusal is a typed `ValidationError`, not a raw `Error`. The
143
199
  * commands that still inline their own check belong here when they move.
144
200
  *
145
- * Like `requireTenant`, `message` is passed whole rather than composed: each
146
- * command's exact wording is what its users (and its tests) read.
201
+ * `message` is passed whole rather than composed: each command's exact
202
+ * wording is what its users (and its tests) read.
147
203
  */
148
204
  function requireConfirmation(yes, message) {
149
205
  if (!yes) throw new ValidationError(message, []);
150
206
  }
207
+ /**
208
+ * A page of the web app, on the API's origin: a profile's API URL is
209
+ * `https://<host>/v1`, and the app is served at `https://<host>`.
210
+ */
211
+ function appUrl(cmd, path = "/") {
212
+ const { apiUrl } = getGlobalOptions(cmd);
213
+ if (!apiUrl) throw new ValidationError("No Standards instance configured. Run \"standards login\" or pass --api-url.", []);
214
+ return new URL(path, new URL(apiUrl).origin).toString();
215
+ }
216
+ /** A conversation's page in the app: the chat router's `/chat/<id>`. */
217
+ function conversationUrl(cmd, sessionId) {
218
+ return appUrl(cmd, `/chat/${encodeURIComponent(sessionId)}`);
219
+ }
220
+ /** A command's success line on stdout: a green check, then `message`. */
221
+ function printDone(message) {
222
+ process.stdout.write(`${styleText("green", "✓")} ${message}\n`);
223
+ }
151
224
  /** Renders any caught value as a message string — shared by every command's top-level `catch`. */
152
225
  function messageOf(error) {
153
226
  return error instanceof Error ? error.message : String(error);
154
227
  }
155
- // src/commands/ai.ts
156
- const SCOPES = ["me", "workspace"];
157
- /** `GET /ai/usage` reads inclusive UTC days; anything else reaches the API as a 400. */
158
- const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/u;
159
- function parseDay(flag, value) {
160
- if (!ISO_DAY.test(value)) throw new ValidationError(`${flag} must be a YYYY-MM-DD date`, []);
161
- return value;
162
- }
163
228
  /**
164
- * The window's own rules (maximum span, ordering) belong to the server, which
165
- * owns the calendar; the CLI only rejects what it can judge on its own.
229
+ * The one name lookup of the CLI: the single item whose `field` equals `name`.
230
+ * None → `NotFoundError` (`No <noun> named "x"`); several → `ValidationError`
231
+ * listing them, so the caller passes an id instead of the CLI guessing.
166
232
  */
167
- function parseScope(scope) {
168
- if (SCOPES.includes(scope)) return scope;
169
- throw new ValidationError(`--scope must be one of: ${SCOPES.join(", ")}`, []);
170
- }
171
- function registerAiCommand(program) {
172
- const ai = program.command("ai").description("Inspect AI usage and credit state");
173
- ai.command("usage").description("Aggregate the AI journal over an inclusive UTC day window").requiredOption("--from <day>", "first day of the window (YYYY-MM-DD)").requiredOption("--to <day>", "last day of the window, inclusive (YYYY-MM-DD)").option("--scope <scope>", "me or workspace", "me").action(async (opts, cmd) => {
174
- const params = {
175
- from: parseDay("--from", opts.from),
176
- to: parseDay("--to", opts.to),
177
- scope: parseScope(opts.scope ?? "me")
178
- };
179
- formatOutput(await getAppClientFromCommand(cmd).ai.usage(params), getFormat(cmd));
180
- });
181
- ai.command("wallet").description("Show the tenant's billing mode and AI credit state").action(async (_opts, cmd) => {
182
- formatOutput(await getAppClientFromCommand(cmd).ai.wallet({}), getFormat(cmd));
183
- });
233
+ function findNamed(items, field, name, noun, describe = (item) => item.id) {
234
+ const matches = items.filter((item) => item[field] === name);
235
+ const [match] = matches;
236
+ if (!match) throw new NotFoundError(noun, name, void 0, `No ${noun} named "${name}"`);
237
+ if (matches.length > 1) throw new ValidationError(`${matches.length} ${noun}s are named "${name}": ${matches.map(describe).join(", ")}. Pass the id.`, []);
238
+ return match;
239
+ }
240
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
241
+ /** An environment's id from its id or its name (`findNamed` over `environments.list`). */
242
+ async function resolveEnvironmentId(client, nameOrId) {
243
+ if (UUID.test(nameOrId)) return nameOrId;
244
+ const { environments } = await client.environments.list({});
245
+ return findNamed(environments, "name", nameOrId, "environment", (env) => `${env.id} (${env.ownedBy})`).id;
184
246
  }
185
- // src/commands/auth.ts
186
- function registerAuthCommand(program) {
187
- program.command("auth").description("Inspect CLI authentication").command("whoami").description("Show current configuration and validate API key").action(async (_opts, cmd) => {
188
- const root = getGlobalOptions(cmd);
189
- const client = getAppClientFromCommand(cmd);
190
- const write = (msg) => process.stdout.write(`${msg}\n`);
191
- write(styleText("bold", "Standards CLI Configuration"));
192
- write(` API URL: ${root.apiUrl}`);
193
- write(` API Key: ${root.apiKey ? `${root.apiKey.slice(0, 8)}...` : styleText("red", "not set")}`);
194
- write("");
195
- const keys = await client.apiKeys.list({});
196
- write(styleText("green", "✓ API key is valid"));
197
- if (Array.isArray(keys) && keys.length > 0) write(` Active keys: ${keys.filter((key) => !key.revokedAt).length}`);
198
- });
247
+ /**
248
+ * Whether a streaming or overview command writes for a person: `--format` was
249
+ * left at its default. An explicit `--format json` asks for machine output.
250
+ */
251
+ function isHumanOutput(cmd) {
252
+ let root = cmd;
253
+ while (root.parent) root = root.parent;
254
+ return root.getOptionValueSource("format") === "default";
199
255
  }
200
- // src/discovery/order-bundles.ts
201
256
  /**
202
- * Orders discovered bundles so each comes after the bundles it `uses`.
203
- * Bundles a `uses` names that were not discovered are already the host's concern.
257
+ * A secret's value, from piped stdin only: a TTY is refused (no masked prompt
258
+ * exists, and a visible one would leak to scrollback), argv is never read.
204
259
  */
205
- async function orderByUses(discovered) {
206
- const byName = new Map(discovered.map((entry) => [entry.bundle.name, entry]));
207
- const ordered = [];
208
- const visited = /* @__PURE__ */ new Set();
209
- async function visit(entry) {
210
- if (visited.has(entry.bundle.name)) return;
211
- visited.add(entry.bundle.name);
212
- for (const used of (await entry.bundle.build()).uses) {
213
- const dependency = byName.get(used);
214
- if (dependency) await visit(dependency);
215
- }
216
- ordered.push(entry);
217
- }
218
- for (const entry of discovered) await visit(entry);
219
- return ordered;
260
+ async function readPipedValue(command) {
261
+ if (process.stdin.isTTY) throw new ValidationError(`Pipe the value: printf %s "$VALUE" | standards ${command}`, []);
262
+ const chunks = [];
263
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
264
+ const value = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
265
+ if (!value) throw new ValidationError("The piped value is empty", []);
266
+ return value;
267
+ }
268
+ /** Whether a person sits at this terminal: both stdin and stdout are TTYs. Agents and CI are not. */
269
+ function isInteractive() {
270
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
220
271
  }
221
272
  // src/commands/bundles.ts
222
273
  const HASH_PREFIX_LENGTH = 7;
@@ -270,6 +321,15 @@ function collectDecision(value, decisions) {
270
321
  function collectPieceId(value, pieceIds) {
271
322
  return [...pieceIds, value];
272
323
  }
324
+ /**
325
+ * An add or update applies exactly the plan a `--dry-run` printed: its
326
+ * fingerprint is required, checked here so the refusal is typed and nothing
327
+ * reaches the API.
328
+ */
329
+ function requireFingerprint(fingerprint, dryRun) {
330
+ if (fingerprint) return fingerprint;
331
+ throw new ValidationError(`Pass the fingerprint of the plan to apply: run "${dryRun}" first, then "--fingerprint <fingerprint>".`, []);
332
+ }
273
333
  /** The input `preview` and `add` share: whole bundles, in one locale. */
274
334
  function addInput(names, opts) {
275
335
  return {
@@ -285,8 +345,8 @@ function withAddOptions(command) {
285
345
  /**
286
346
  * The workspace's bundles: sync the ones the project declares, list what is
287
347
  * installed and where each stands, remove one, add a bundle the workspace may
288
- * add as a copy it owns (`preview`, then `add` with the preview's fingerprint)
289
- * and update such a copy (`update-preview`, then `update` with its fingerprint).
348
+ * add as a copy it owns (`add --dry-run`, then `add` with the plan's fingerprint)
349
+ * and update such a copy (`update --dry-run`, then `update` with its fingerprint).
290
350
  */
291
351
  function registerBundlesCommand(program) {
292
352
  const bundles = program.command("bundles").description("Sync and manage the workspace's bundles");
@@ -308,29 +368,35 @@ function registerBundlesCommand(program) {
308
368
  const { available } = await getAppClientFromCommand(cmd).bundles.list({ locale: parseLocale(opts.locale) });
309
369
  formatOutput(available, getFormat(cmd));
310
370
  }));
311
- withAddOptions(bundles.command("preview").description("Plan an add: what is new, what collides, and the plan's fingerprint")).action((names, opts, cmd) => run(async () => {
312
- formatOutput(await getAppClientFromCommand(cmd).bundles.preview(addInput(names, opts)), getFormat(cmd));
313
- }));
314
- withAddOptions(bundles.command("add").description("Add previewed bundles as copies the workspace owns").requiredOption("--fingerprint <fingerprint>", "the fingerprint `bundles preview` returned").option("--decision <pieceId=choice>", "how to settle a collision: bind, skip or rename:<name> (repeatable)", collectDecision, {})).action((names, opts, cmd) => run(async () => {
315
- formatOutput(await getAppClientFromCommand(cmd).bundles.add({
316
- ...addInput(names, opts),
317
- decisions: opts.decision,
318
- fingerprint: opts.fingerprint
319
- }), getFormat(cmd));
320
- }));
321
- bundles.command("update-preview").description("Plan an added bundle's update, piece by piece, and the plan's fingerprint").argument("<name>", "bundle name").action((name, _opts, cmd) => run(async () => {
322
- formatOutput(await getAppClientFromCommand(cmd).bundles.updatePreview({ name }), getFormat(cmd));
323
- }));
324
- bundles.command("update").description("Update an added bundle as its preview showed; nothing is removed").argument("<name>", "bundle name").requiredOption("--fingerprint <fingerprint>", "the fingerprint `bundles update-preview` returned").option("--take <pieceId>", "take the new version of a piece you changed (repeatable)", collectPieceId, []).option("--skip-missing <pieceId>", "do not reinstall a missing piece (repeatable)", collectPieceId, []).option("--decision <pieceId=choice>", "how to settle a collision: bind, skip or rename:<name> (repeatable)", collectDecision, {}).action((name, opts, cmd) => run(async () => {
325
- const { applied } = await getAppClientFromCommand(cmd).bundles.update({
326
- name,
327
- fingerprint: opts.fingerprint,
328
- take: opts.take,
329
- skipMissing: opts.skipMissing,
330
- decisions: opts.decision
371
+ withAddOptions(bundles.command("add").description("Add bundles as copies the workspace owns (--dry-run prints the plan)").option("--dry-run", "print what is new, what collides, and the plan's fingerprint").option("--fingerprint <fingerprint>", "the fingerprint `bundles add --dry-run` printed").option("--decision <pieceId=choice>", "how to settle a collision: bind, skip or rename:<name> (repeatable)", collectDecision, {})).action((names, opts, cmd) => {
372
+ const fingerprint = opts.dryRun ? void 0 : requireFingerprint(opts.fingerprint, "bundles add --dry-run");
373
+ return run(async () => {
374
+ const client = getAppClientFromCommand(cmd);
375
+ formatOutput(fingerprint === void 0 ? await client.bundles.preview(addInput(names, opts)) : await client.bundles.add({
376
+ ...addInput(names, opts),
377
+ decisions: opts.decision,
378
+ fingerprint
379
+ }), getFormat(cmd));
331
380
  });
332
- console.info(`${name} updated, ${applied} applied`);
333
- }));
381
+ });
382
+ bundles.command("update").description("Update an added bundle as its plan showed; nothing is removed (--dry-run prints the plan)").argument("<name>", "bundle name").option("--dry-run", "print the update plan, piece by piece, and its fingerprint").option("--fingerprint <fingerprint>", "the fingerprint `bundles update --dry-run` printed").option("--take <pieceId>", "take the new version of a piece you changed (repeatable)", collectPieceId, []).option("--skip-missing <pieceId>", "do not reinstall a missing piece (repeatable)", collectPieceId, []).option("--decision <pieceId=choice>", "how to settle a collision: bind, skip or rename:<name> (repeatable)", collectDecision, {}).action((name, opts, cmd) => {
383
+ const fingerprint = opts.dryRun ? void 0 : requireFingerprint(opts.fingerprint, `bundles update ${name} --dry-run`);
384
+ return run(async () => {
385
+ const client = getAppClientFromCommand(cmd);
386
+ if (fingerprint === void 0) {
387
+ formatOutput(await client.bundles.updatePreview({ name }), getFormat(cmd));
388
+ return;
389
+ }
390
+ const { applied } = await client.bundles.update({
391
+ name,
392
+ fingerprint,
393
+ take: opts.take,
394
+ skipMissing: opts.skipMissing,
395
+ decisions: opts.decision
396
+ });
397
+ console.info(`${name} updated, ${applied} applied`);
398
+ });
399
+ });
334
400
  bundles.command("remove").description("Remove a synced bundle; its pieces are demoted, never deleted. An added bundle is refused: uninstall it from the Bundles window").argument("<name>", "bundle name").option("--yes", "remove without confirmation").action((name, opts, cmd) => run(async () => {
335
401
  requireConfirmation(opts.yes, `Removing bundle "${name}" demotes its objects to workspace-owned. Re-run with "--yes" to confirm.`);
336
402
  await getAppClientFromCommand(cmd).bundles.remove({ name });
@@ -405,50 +471,210 @@ function registerConnectorsCommand(program) {
405
471
  id,
406
472
  ...opts.removeImported ? { removeImported: "true" } : {}
407
473
  });
408
- process.stdout.write(`${styleText("green", "✓")} Connector connection ${id} disconnected.\n`);
474
+ printDone(`Connector connection ${id} disconnected.`);
475
+ });
476
+ }
477
+ // src/commands/decisions.ts
478
+ function isOption(value) {
479
+ return isPlainRecord(value) && typeof value.value === "string" && typeof value.label === "string";
480
+ }
481
+ function questionsOf(decision) {
482
+ const raw = decision.questions?.questions;
483
+ if (!Array.isArray(raw)) return [];
484
+ return raw.flatMap((entry) => {
485
+ if (!(isPlainRecord(entry) && typeof entry.id === "string")) return [];
486
+ return [{
487
+ id: entry.id,
488
+ question: typeof entry.question === "string" ? entry.question : "",
489
+ multiSelect: entry.multiSelect === true,
490
+ options: Array.isArray(entry.options) ? entry.options.filter(isOption) : []
491
+ }];
492
+ });
493
+ }
494
+ function setupVariablesOf(decision) {
495
+ if (decision.type !== DECISION_TYPE_AGENT_SETUP) return [];
496
+ const raw = decision.questions?.items;
497
+ if (!Array.isArray(raw)) return [];
498
+ return raw.flatMap((item) => {
499
+ if (!(isPlainRecord(item) && item.kind === "env_var" && typeof item.key === "string")) return [];
500
+ const scope = item.scope === "agent" || item.scope === "workspace" ? item.scope : void 0;
501
+ return [{
502
+ key: item.key,
503
+ label: typeof item.label === "string" ? item.label : item.key,
504
+ scope
505
+ }];
409
506
  });
410
507
  }
411
- // src/drift/shared-state.ts
412
- async function readSchemaState(client) {
413
- const state = parseSchemaState(await client.schemaState.get({}));
414
- if (createHash("sha256").update(canonicalStringify(schemaStateSourcePayload(state.entries))).digest("hex") !== state.sourceFingerprint) throw new ValidationError("Invalid schema state: sourceFingerprint does not match deployed source", []);
415
- return state;
508
+ /**
509
+ * The command that sets one variable a setup decision asks for: a workspace
510
+ * variable reaches every environment, any other is the caller's own.
511
+ */
512
+ function secretCommand(variable) {
513
+ const scope = variable.scope === "workspace" ? "--environments all" : "--private";
514
+ return `printf %s "$VALUE" | standards secrets set ${variable.key} ${scope}`;
515
+ }
516
+ function numberedOptions(question) {
517
+ return question.options.map((option, index) => `${index + 1}. ${option.label}`);
518
+ }
519
+ /** The session a decision is about, when it is one's. */
520
+ function sessionIdOf(decision) {
521
+ return decision.links.find((link) => link.targetType === "session")?.targetId;
522
+ }
523
+ /** Where the app shows a decision: its conversation, or the app's home (the decisions inbox). */
524
+ function decisionUrl(cmd, decision) {
525
+ const sessionId = sessionIdOf(decision);
526
+ return sessionId ? conversationUrl(cmd, sessionId) : appUrl(cmd);
527
+ }
528
+ /** A decision as one block of text: title, why, change, numbered options, setup commands. */
529
+ function renderDecision(decision) {
530
+ const lines = [`${decision.title} (${decision.type}, ${decision.id})`];
531
+ if (decision.why) lines.push(` why: ${decision.why}`);
532
+ if (decision.change) lines.push(` change: ${decision.change}`);
533
+ for (const [index, question] of questionsOf(decision).entries()) {
534
+ const multi = question.multiSelect ? " (several: 1,3)" : "";
535
+ lines.push(` Q${index + 1}. ${question.question}${multi}`);
536
+ for (const option of numberedOptions(question)) lines.push(` ${option}`);
537
+ }
538
+ for (const variable of setupVariablesOf(decision)) lines.push(` ${variable.label}: ${secretCommand(variable)}`);
539
+ if (decision.links.length > 0) lines.push(` links: ${decision.links.map(formatDecisionLink).join(", ")}`);
540
+ lines.push(` answer: standards decisions answer ${decision.id} …`);
541
+ return `${lines.join("\n")}\n`;
416
542
  }
417
- function prettyCanonical(value) {
418
- return JSON.stringify(JSON.parse(canonicalStringify(value)), null, 2);
543
+ function summarizeDecision$1(decision) {
544
+ return {
545
+ id: decision.id,
546
+ type: decision.type,
547
+ kind: decision.kind,
548
+ title: decision.title,
549
+ sender: decision.sender?.label ?? null,
550
+ questions: questionsOf(decision).map((question) => ({
551
+ id: question.id,
552
+ question: question.question,
553
+ ...question.multiSelect && { multiSelect: true },
554
+ options: numberedOptions(question)
555
+ }))
556
+ };
419
557
  }
420
- // src/commands/diff.ts
421
- async function runDiffCommand(client) {
422
- try {
423
- const diff = diffSchemaState(await readSchemaState(client));
424
- return {
425
- exitCode: diff.changes.length ? 1 : 0,
426
- diff
427
- };
428
- } catch (error) {
558
+ function describeDecision(cmd, decision) {
559
+ return {
560
+ ...summarizeDecision$1(decision),
561
+ why: decision.why ?? null,
562
+ change: decision.change ?? null,
563
+ links: decision.links.map(formatDecisionLink),
564
+ setup: setupVariablesOf(decision).map((variable) => ({
565
+ key: variable.key,
566
+ label: variable.label,
567
+ command: secretCommand(variable)
568
+ })),
569
+ url: decisionUrl(cmd, decision)
570
+ };
571
+ }
572
+ /** The decisions of one `decisions.list` state, every page; `undefined` is everything not archived. */
573
+ async function readDecisions(client, state, link) {
574
+ return (await readAllPages((cursor) => client.decisions.list({
575
+ ...state && { state },
576
+ ...link && { link },
577
+ ...cursor && { cursor }
578
+ }))).map((item) => item.decision);
579
+ }
580
+ /** The caller's open decisions, every page; with `link` (`session:<id>`), only those about it. */
581
+ function readOpenDecisions(client, link) {
582
+ return readDecisions(client, "open", link);
583
+ }
584
+ /** No `decisions.get` route: the open decisions first, then every state, until the id shows. */
585
+ async function findDecision(client, id) {
586
+ for (const state of ["open", void 0]) {
587
+ const found = (await readDecisions(client, state)).find((decision) => decision.id === id);
588
+ if (found) return found;
589
+ }
590
+ throw new NotFoundError("decision", id, void 0, `No decision "${id}"`);
591
+ }
592
+ /** An option number or label (case-insensitive) as the option's value; anything else is free text. */
593
+ function optionValue(question, raw) {
594
+ const trimmed = raw.trim();
595
+ if (/^\d+$/.test(trimmed)) {
596
+ const option = question.options[Number(trimmed) - 1];
597
+ if (option) return option.value;
598
+ }
599
+ const lower = trimmed.toLowerCase();
600
+ return question.options.find((option) => option.label.toLowerCase() === lower || option.value.toLowerCase() === lower)?.value ?? raw;
601
+ }
602
+ function plural(count, noun) {
603
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
604
+ }
605
+ function oneWord(args, words) {
606
+ const [word, ...rest] = args;
607
+ if (word === void 0 || rest.length > 0 || !words.includes(word)) throw new ValidationError(`Answer with one of: ${words.join(", ")}`, []);
608
+ return word;
609
+ }
610
+ function questionAnswer(decision, args, flags) {
611
+ if (flags.skip) {
612
+ if (args.length > 0) throw new ValidationError("--skip takes no answer", []);
429
613
  return {
430
- exitCode: 2,
431
- errorMessage: messageOf(error)
614
+ kind: "answers",
615
+ skipped: true,
616
+ answers: {}
432
617
  };
433
618
  }
619
+ const questions = questionsOf(decision);
620
+ if (args.length !== questions.length) throw new ValidationError(`${plural(questions.length, "question")}, ${plural(args.length, "answer")}: pass one answer per question, in order`, []);
621
+ return {
622
+ kind: "answers",
623
+ skipped: false,
624
+ answers: Object.fromEntries(questions.map((question, index) => {
625
+ const raw = args[index] ?? "";
626
+ const value = question.multiSelect ? parseCsvList(raw).map((part) => optionValue(question, part)) : optionValue(question, raw);
627
+ return [question.id, value];
628
+ }))
629
+ };
434
630
  }
435
- function renderDiff(diff) {
436
- return ["Deployed source vs shared runtime", ...diff.changes.length ? diff.changes.map((entry) => `${entry.change} ${JSON.stringify(entry.address)}`) : ["No differences."]].join("\n");
437
- }
438
- function registerDiffCommand(program) {
439
- program.command("diff").description("Compare deployed source vs shared runtime; excludes unbuilt local TypeScript (exit 0 equal, 1 differences, 2 error)").option("--json", "Emit canonical machine-readable differences").action(async (opts, cmd) => {
440
- let result;
441
- try {
442
- result = await runDiffCommand(getAppClientFromCommand(cmd));
443
- } catch (error) {
444
- result = {
445
- exitCode: 2,
446
- errorMessage: messageOf(error)
631
+ /**
632
+ * The answer body by the decision's kind, as the app's card builds it. Kinds
633
+ * the CLI cannot answer (fyi, report, a connector to reconnect) are refused
634
+ * before any call, with where the app answers them.
635
+ */
636
+ function answerFor(cmd, decision, args, flags) {
637
+ switch (decision.kind) {
638
+ case "approval": return {
639
+ kind: "approval",
640
+ approved: oneWord(args, ["yes", "no"]) === "yes",
641
+ ...flags.reason && { reason: flags.reason }
642
+ };
643
+ case "question": return questionAnswer(decision, args, flags);
644
+ case "review": return {
645
+ kind: "review",
646
+ outcome: oneWord(args, ["yes", "changes"]) === "yes" ? "approved" : "changes",
647
+ ...flags.feedback && { feedback: flags.feedback }
648
+ };
649
+ case "handoff":
650
+ if (decision.type === DECISION_TYPE_AGENT_SETUP) return {
651
+ kind: "setup",
652
+ action: oneWord(args, ["continue", "skip"]) === "continue" ? "continue" : "skip"
447
653
  };
448
- }
449
- if (result.exitCode === 2) console.error(styleText("red", `✗ ${result.errorMessage}`));
450
- else console.info(opts.json ? prettyCanonical(result.diff) : renderDiff(result.diff));
451
- process.exit(result.exitCode);
654
+ if (decision.type === DECISION_TYPE_BUNDLE_SETUP) {
655
+ oneWord(args, ["configure"]);
656
+ return { kind: "configure" };
657
+ }
658
+ }
659
+ throw new ValidationError(`This decision is answered in the app: ${decisionUrl(cmd, decision)}`, []);
660
+ }
661
+ function registerDecisionsCommand(program) {
662
+ const decisions = program.command("decisions").description("List, read and answer the decisions agents wait on");
663
+ decisions.command("list").description("List open decisions, options numbered from 1").option("--all", "every decision not archived, answered ones included").action(async (opts, cmd) => {
664
+ formatOutput((await readDecisions(getAppClientFromCommand(cmd), opts.all ? void 0 : "open")).map(summarizeDecision$1), getFormat(cmd));
665
+ });
666
+ decisions.command("show").description("Show a decision: why, change, numbered options, links, setup commands").argument("<id>", "decision id").action(async (id, _opts, cmd) => {
667
+ formatOutput(describeDecision(cmd, await findDecision(getAppClientFromCommand(cmd), id)), getFormat(cmd));
668
+ });
669
+ decisions.command("answer").description("Answer a decision: yes/no, an option number or label per question, changes, continue/skip, configure").argument("<id>", "decision id").argument("[answer...]", "the answer, one per question for a question").option("--reason <reason>", "why, with an approval").option("--feedback <feedback>", "what to change, with a review's changes").option("--skip", "skip every question").action(async (id, args, flags, cmd) => {
670
+ const client = getAppClientFromCommand(cmd);
671
+ const decision = await findDecision(client, id);
672
+ const answer = answerFor(cmd, decision, args, flags);
673
+ await client.decisions.answer({
674
+ decisionId: decision.id,
675
+ answer
676
+ });
677
+ printDone(`Decision ${decision.id} answered.`);
452
678
  });
453
679
  }
454
680
  // src/commands/documents.ts
@@ -524,33 +750,237 @@ function registerDocumentsCommand(program) {
524
750
  process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
525
751
  });
526
752
  }
527
- // src/commands/embeddings.ts
528
- function registerEmbeddingsCommand(program) {
529
- program.command("embeddings").description("Maintain record and email embeddings").command("drain").description("Embed every pending record and email of the tenant").action(async (_opts, cmd) => {
530
- const tenant = requireTenant(cmd, "An embedding drain names its tenant explicitly. Pass \"--tenant <id>\".");
531
- await getAppClientFromCommand(cmd).embeddingsAdmin.drain({ tenantId: tenant });
532
- process.stdout.write(`${styleText("green", "✓")} Embedding drain accepted for tenant ${tenant}.\n`);
533
- });
534
- }
535
- // src/commands/folders.ts
536
- function registerFoldersCommand(program) {
537
- program.command("folders").description("Manage record drive folders").command("reconcile-paths").description("Create a folder tree on a record drive from paths").argument("<object>", "object name (e.g. contacts)").argument("<recordId>", "record ID owning the drive").option("--path <path>", "folder path to create (repeatable)", (val, acc) => {
538
- acc.push(val);
539
- return acc;
540
- }, []).option("--paths-file <file>", "file containing one path per line").option("--mode <mode>", "reconcile mode: repair or dryRun", "repair").action(async (objectName, recordId, opts, cmd) => {
541
- const paths = [...opts.path];
542
- if (opts.pathsFile) {
543
- const filePaths = (await readFile(opts.pathsFile, "utf-8")).split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
544
- paths.push(...filePaths);
545
- }
546
- if (paths.length === 0) throw new Error("No paths provided. Pass at least one --path or a non-empty --paths-file.");
547
- formatOutput(await getAppClientFromCommand(cmd).folders.reconcilePaths({
548
- recordId,
549
- objectName,
550
- paths,
551
- mode: opts.mode
753
+ // src/commands/environments.ts
754
+ /** `--harness`: catalogue ids, or `standards` (no harness), which is `null`. */
755
+ function parseHarnesses(value) {
756
+ return parseHarnessIds(parseCsvList(value));
757
+ }
758
+ function collect(value, previous = []) {
759
+ return [...previous, value];
760
+ }
761
+ /** The body fields `create` and `update` share, from the same flags. */
762
+ function sharedFields(opts) {
763
+ if (opts.visibility === "workspace" && !opts.confirmShared) throw new ValidationError("A workspace environment shares its machines with every member: add --confirm-shared", []);
764
+ return {
765
+ ...opts.name !== void 0 && { name: opts.name },
766
+ ...opts.repo && { repositories: opts.repo.map((fullName) => ({ fullName })) },
767
+ ...opts.size && { size: opts.size },
768
+ ...opts.maxSize && { maxSize: opts.maxSize },
769
+ ...opts.image && { image: opts.image },
770
+ ...opts.visibility && { visibility: opts.visibility },
771
+ ...opts.visibility === "workspace" && { confirmSharedMachine: true }
772
+ };
773
+ }
774
+ /** The flags `create` and `update` share. */
775
+ function withEnvironmentFlags(command) {
776
+ return command.option("--repo <owner/name>", "a repository it clones (repeatable)", collect).addOption(new Option("--size <size>", "the size its machines start at").choices(ENVIRONMENT_SIZES)).addOption(new Option("--max-size <size>", "the largest size a run may ask for").choices(ENVIRONMENT_SIZES)).addOption(new Option("--image <image>", "its base image").choices(ENVIRONMENT_IMAGES)).addOption(new Option("--visibility <visibility>", "who may use it").choices(["private", "workspace"])).option("--confirm-shared", "confirm a workspace environment shares its machines");
777
+ }
778
+ function registerEnvironmentsCommand(program) {
779
+ const environments = program.command("environments").description("Manage the environments agent machines run in");
780
+ environments.command("list").description("List the defaults and the environments you can use").action(async (_opts, cmd) => {
781
+ formatOutput(await getAppClientFromCommand(cmd).environments.list({}), getFormat(cmd));
782
+ });
783
+ environments.command("get").description("Show an environment with its scripts and last build log").argument("<env>", "environment name or id").action(async (env, _opts, cmd) => {
784
+ const client = getAppClientFromCommand(cmd);
785
+ const id = await resolveEnvironmentId(client, env);
786
+ formatOutput(await client.environments.get({ id }), getFormat(cmd));
787
+ });
788
+ environments.command("harnesses").description("List the harnesses an environment can accept, with their models").action(async (_opts, cmd) => {
789
+ const client = getAppClientFromCommand(cmd);
790
+ formatOutput(await Promise.all(ENVIRONMENT_HARNESSES.map(async (harness) => ({
791
+ id: harness.id,
792
+ label: harness.label,
793
+ models: (await client.environments.harnessModels({ harness: harness.id })).models
794
+ }))), getFormat(cmd));
795
+ });
796
+ withEnvironmentFlags(environments.command("create").description("Create an environment").requiredOption("--name <name>", "its name").requiredOption("--harness <id,…|standards>", "the harnesses it accepts, or standards")).action(async (opts, cmd) => {
797
+ const body = {
798
+ ...sharedFields(opts),
799
+ name: opts.name,
800
+ acceptedHarnesses: parseHarnesses(opts.harness)
801
+ };
802
+ formatOutput(await getAppClientFromCommand(cmd).environments.create(body), getFormat(cmd));
803
+ });
804
+ withEnvironmentFlags(environments.command("update").description("Update an environment").argument("<env>", "environment name or id").option("--name <name>", "its new name").option("--harness <id,…>", "the harnesses it accepts")).action(async (env, opts, cmd) => {
805
+ const acceptedHarnesses = opts.harness === void 0 ? void 0 : parseHarnesses(opts.harness);
806
+ if (acceptedHarnesses === null) throw new ValidationError("A Standards environment is chosen at creation only", []);
807
+ const client = getAppClientFromCommand(cmd);
808
+ const id = await resolveEnvironmentId(client, env);
809
+ formatOutput(await client.environments.update({
810
+ id,
811
+ ...sharedFields(opts),
812
+ ...acceptedHarnesses && { acceptedHarnesses }
813
+ }), getFormat(cmd));
814
+ });
815
+ environments.command("build").description("Start building an environment's image").argument("<env>", "environment name or id").action(async (env, _opts, cmd) => {
816
+ const client = getAppClientFromCommand(cmd);
817
+ const id = await resolveEnvironmentId(client, env);
818
+ await client.environments.build({ id });
819
+ printDone(`Build started for environment ${id}.`);
820
+ });
821
+ environments.command("archive").description("Archive an environment").argument("<env>", "environment name or id").option("--yes", "archive without confirmation").action(async (env, opts, cmd) => {
822
+ requireConfirmation(opts.yes, "Archiving an environment is explicit. Re-run with \"--yes\" to confirm.");
823
+ const client = getAppClientFromCommand(cmd);
824
+ const id = await resolveEnvironmentId(client, env);
825
+ await client.environments.archive({ id });
826
+ printDone(`Environment ${id} archived.`);
827
+ });
828
+ }
829
+ // src/commands/files.ts
830
+ function registerFilesCommand(program) {
831
+ const files = program.command("files").description("Upload, list, download and delete files");
832
+ files.command("list").description("List files (with --ids: exactly those, in that order)").option("--ids <ids>", "comma-separated file IDs; the other filters and the page do not apply").option("--limit <n>", "max files to return").option("--offset <n>", "number of files to skip").option("--folder <path>", "only files in this folder path").option("--mime-type <type>", "only files of this MIME type").action(async (opts, cmd) => {
833
+ formatOutput(await getAppClientFromCommand(cmd).files.list({
834
+ ...opts.ids !== void 0 ? { ids: parseCsvList(opts.ids).join(",") } : {},
835
+ ...opts.limit !== void 0 ? { limit: parseCount("--limit", opts.limit) } : {},
836
+ ...opts.offset !== void 0 ? { offset: parseCount("--offset", opts.offset) } : {},
837
+ ...opts.folder ? { folderPath: opts.folder } : {},
838
+ ...opts.mimeType ? { mimeType: opts.mimeType } : {}
552
839
  }), getFormat(cmd));
553
840
  });
841
+ files.command("get").description("Get a file by ID").argument("<id>", "file ID").action(async (id, _opts, cmd) => {
842
+ formatOutput(await getAppClientFromCommand(cmd).files.get({ id }), getFormat(cmd));
843
+ });
844
+ files.command("upload").description("Upload local files (private); prints the stored files and their IDs").argument("<paths...>", "local file paths (at most 10)").action(async (paths, _opts, cmd) => {
845
+ const uploads = await Promise.all(paths.map(async (path) => new File([await readFile(path)], basename(path))));
846
+ formatOutput(await getAppClientFromCommand(cmd).files.upload({ files: uploads }), getFormat(cmd));
847
+ });
848
+ files.command("content").description("Download a file's bytes (to stdout, or -o <file>)").argument("<id>", "file ID").option("-o, --output <file>", "write to a file instead of stdout").action(async (id, opts, cmd) => {
849
+ const { blob } = await getAppClientFromCommand(cmd).files.content({ id });
850
+ const bytes = new Uint8Array(await blob.arrayBuffer());
851
+ if (opts.output) return await writeFile(opts.output, bytes);
852
+ process.stdout.write(bytes);
853
+ });
854
+ files.command("delete").description("Delete a file you uploaded and its stored bytes (not a file in a document pack)").argument("<id>", "file ID").option("--yes", "delete without confirmation").action(async (id, opts, cmd) => {
855
+ requireConfirmation(opts.yes, "Deleting a file purges its bytes. Re-run with \"--yes\" to confirm.");
856
+ await getAppClientFromCommand(cmd).files.delete({ id });
857
+ printDone(`File ${id} deleted.`);
858
+ });
859
+ }
860
+ // src/commands/guide.ts
861
+ const PACKAGE_NAME = "@standards/cli";
862
+ const DEFAULT_TOPIC = "core";
863
+ /**
864
+ * The `guide/` directory shipped next to the package's `package.json`, found
865
+ * through the package's own export of its manifest: it resolves the same from
866
+ * `src/commands/` (tests), from `dist/bin.mjs` and from any `dist/` chunk.
867
+ */
868
+ function guideDirectory() {
869
+ const manifest = createRequire(import.meta.url).resolve(`${PACKAGE_NAME}/package.json`);
870
+ return join(dirname(manifest), "guide");
871
+ }
872
+ /** The topic names, from the guide directory's `*.md` files, sorted. */
873
+ async function guideTopics(dir) {
874
+ return (await readdir(dir)).filter((file) => file.endsWith(".md")).map((file) => file.slice(0, -3)).sort();
875
+ }
876
+ /** `standards guide [topic]`: the usage guide matching the installed version. */
877
+ function registerGuideCommand(program) {
878
+ program.command("guide").description("Print the usage guide for this version of the CLI (core by default)").argument("[topic]", "guide topic", DEFAULT_TOPIC).option("--list", "list the topics, one per line").action(async (topic, opts) => {
879
+ const dir = guideDirectory();
880
+ const topics = await guideTopics(dir);
881
+ if (opts.list) {
882
+ process.stdout.write(topics.map((name) => `${name}\n`).join(""));
883
+ return;
884
+ }
885
+ if (!topics.includes(topic)) throw new ValidationError(`Unknown guide topic "${topic}". Topics: ${topics.join(", ")}`, []);
886
+ process.stdout.write(await readFile(join(dir, `${topic}.md`)));
887
+ });
888
+ }
889
+ // src/commands/init.ts
890
+ /** The line that marks a `SKILL.md` as this command's own, the only one it overwrites. */
891
+ const SKILL_STUB_MARKER = "<!-- standards-cli-stub -->";
892
+ const SKILL_DIR = join(".agents", "skills", "standards");
893
+ const LINK_PATH = join(".claude", "skills", "standards");
894
+ /** Relative, so the link survives a moved or mounted home or project. */
895
+ const LINK_TARGET = join("..", "..", SKILL_DIR);
896
+ const SKILL_STUB = `---
897
+ name: standards
898
+ description: Drive a Standards workspace from the shell with the \`standards\` CLI. It covers records and documents, schema and bundles, skills, MCP servers and connectors, and Standards agents (environments and their secrets, machines and their links, sessions on the claude-code, codex or cursor harnesses, the decisions they stop on, monitoring with status and watch). Use it whenever a task reads or changes Standards data, configures or starts an agent session, answers an agent's decision, or follows running sessions.
899
+ ---
900
+
901
+ ${SKILL_STUB_MARKER}
902
+
903
+ # standards
904
+
905
+ This file is a discovery stub, not the usage guide. Before running any \`standards\` command, load the guide from the CLI:
906
+
907
+ \`\`\`bash
908
+ standards guide core # start here: login, global flags, output, exit codes, the agent loop
909
+ standards guide --list # every topic
910
+ \`\`\`
911
+
912
+ For agent work (environments, secrets, sessions, decisions, machines, links, watch), also load:
913
+
914
+ \`\`\`bash
915
+ standards guide agents
916
+ \`\`\`
917
+
918
+ The CLI serves the guide matching its installed version, so it never goes stale. This stub cannot change between releases, which is why it only points at \`standards guide\`.
919
+
920
+ Re-run \`standards init\` (or \`standards init --global\`) to refresh it.
921
+ `;
922
+ async function lstatOrNull(path) {
923
+ try {
924
+ return await lstat(path);
925
+ } catch (error) {
926
+ if (error.code === "ENOENT") return null;
927
+ throw error;
928
+ }
929
+ }
930
+ async function isOwnStub(file) {
931
+ if (!(await lstatOrNull(file))?.isFile()) return false;
932
+ return (await readFile(file, "utf8")).includes(SKILL_STUB_MARKER);
933
+ }
934
+ function refuse(path, reason) {
935
+ throw new ValidationError(`${path} ${reason}: "standards init" does not overwrite it. Move it away and re-run.`, []);
936
+ }
937
+ /** What already sits at the link path: nothing, our link, our copy, or something to refuse. */
938
+ async function linkState(link) {
939
+ const stats = await lstatOrNull(link);
940
+ if (!stats) return "missing";
941
+ if (stats.isSymbolicLink()) {
942
+ const target = await readlink(link);
943
+ if (target === LINK_TARGET) return "linked";
944
+ refuse(link, `is a symbolic link to ${target}`);
945
+ }
946
+ if (stats.isDirectory() && await isOwnStub(join(link, "SKILL.md"))) return "copied";
947
+ return refuse(link, "already exists");
948
+ }
949
+ /**
950
+ * Writes the stub under `root` and links `.claude/skills/standards` to it. Every
951
+ * check runs before the first write, so a refusal leaves the disk untouched.
952
+ * Returns the lines to print.
953
+ */
954
+ async function installSkillStub(root) {
955
+ const skillDir = join(root, SKILL_DIR);
956
+ const skillFile = join(skillDir, "SKILL.md");
957
+ const link = join(root, LINK_PATH);
958
+ const dirStats = await lstatOrNull(skillDir);
959
+ if (dirStats && !dirStats.isDirectory()) refuse(skillDir, "is not a directory");
960
+ if (await lstatOrNull(skillFile) && !await isOwnStub(skillFile)) refuse(skillFile, "was not written by standards init");
961
+ const state = await linkState(link);
962
+ await mkdir(skillDir, { recursive: true });
963
+ await writeFile(skillFile, SKILL_STUB);
964
+ const lines = [`Wrote ${skillFile}`];
965
+ if (state === "linked") return [...lines, `Kept ${link} -> ${LINK_TARGET}`];
966
+ if (state === "missing") {
967
+ await mkdir(dirname(link), { recursive: true });
968
+ try {
969
+ await symlink(LINK_TARGET, link, "dir");
970
+ return [...lines, `Linked ${link} -> ${LINK_TARGET}`];
971
+ } catch {
972
+ await mkdir(link, { recursive: true });
973
+ }
974
+ }
975
+ await writeFile(join(link, "SKILL.md"), SKILL_STUB);
976
+ return [...lines, `Copied the stub to ${join(link, "SKILL.md")}`];
977
+ }
978
+ /** `standards init [--global]`: installs the skill stub coding agents discover. */
979
+ function registerInitCommand(program) {
980
+ program.command("init").description("Install the standards skill stub for coding agents (.agents/skills and .claude/skills)").option("--global", "install under your home directory instead of the current one").action(async (opts) => {
981
+ const lines = await installSkillStub(opts.global ? homedir() : process.cwd());
982
+ process.stdout.write(lines.map((line) => `${line}\n`).join(""));
983
+ });
554
984
  }
555
985
  // src/commands/invitations.ts
556
986
  /** Invitation ids are `randomUUID` server-side, so anything else is a caller mistake. */
@@ -592,12 +1022,10 @@ function registerInvitationsCommand(program) {
592
1022
  }
593
1023
  // src/commands/keys.ts
594
1024
  async function resolveRoleId(client, name) {
595
- const match = (await client.permissions.listRoles({})).find((role) => role.name === name);
596
- if (!match) throw new Error(`Unknown role "${name}". Roles are listed in the app's People settings.`);
597
- return match.id;
1025
+ return findNamed(await client.permissions.listRoles({}), "name", name, "role").id;
598
1026
  }
599
1027
  function registerKeysCommand(program) {
600
- const keys = program.command("keys").description("Manage Standards API keys");
1028
+ const keys = program.command("keys").description("Manage Standards API keys").addHelpText("after", "\nA browser-login profile cannot create keys nor revoke another key: log in with --key, or manage keys in Settings → API keys.");
601
1029
  keys.command("list").description("List API keys").action(async (_opts, cmd) => {
602
1030
  formatOutput(await getAppClientFromCommand(cmd).apiKeys.list({}), getFormat(cmd));
603
1031
  });
@@ -617,7 +1045,63 @@ function registerKeysCommand(program) {
617
1045
  keys.command("revoke").description("Revoke an API key").argument("<id>", "API key ID").option("--yes", "revoke without confirmation").action(async (id, opts, cmd) => {
618
1046
  if (!opts.yes) throw new Error("Revoking API keys is explicit. Re-run with \"--yes\" to confirm.");
619
1047
  await getAppClientFromCommand(cmd).apiKeys.revoke({ id });
620
- process.stdout.write(`${styleText("green", "✓")} API key ${id} revoked.\n`);
1048
+ printDone(`API key ${id} revoked.`);
1049
+ });
1050
+ }
1051
+ // src/commands/links.ts
1052
+ const UNIT_MS$1 = {
1053
+ m: 6e4,
1054
+ h: 36e5,
1055
+ d: 864e5
1056
+ };
1057
+ /** `30m`, `2h`, `1d` in milliseconds; anything else, zero or past `LINK_MAX_MS` is refused. */
1058
+ function parseLinkDuration(value) {
1059
+ return parseDuration("--for", value, {
1060
+ units: UNIT_MS$1,
1061
+ example: "30m, 2h or 1d",
1062
+ limit: `at most ${LINK_MAX_MS / UNIT_MS$1.h}h`,
1063
+ max: LINK_MAX_MS
1064
+ });
1065
+ }
1066
+ function registerLinksCommand(program) {
1067
+ const links = program.command("links").description("Open and manage the links machines publish");
1068
+ links.command("open").description("Print the link's URL with a single-use, 60-second token: open it, never share it").argument("<linkId>", "link id").action(async (id, _opts, cmd) => {
1069
+ const { url } = await getAppClientFromCommand(cmd).links.open({ id });
1070
+ process.stdout.write(`${url}\n`);
1071
+ });
1072
+ links.command("relaunch").description("Rerun the link's command, its machine woken first").argument("<linkId>", "link id").action(async (id, _opts, cmd) => {
1073
+ formatOutput(await getAppClientFromCommand(cmd).links.relaunch({ id }), getFormat(cmd));
1074
+ });
1075
+ links.command("extend").description("Make the link expire a duration from now").argument("<linkId>", "link id").requiredOption("--for <duration>", "30m, 2h, 1d…").action(async (id, opts, cmd) => {
1076
+ const durationMs = parseLinkDuration(opts.for);
1077
+ formatOutput(await getAppClientFromCommand(cmd).links.setDuration({
1078
+ id,
1079
+ durationMs
1080
+ }), getFormat(cmd));
1081
+ });
1082
+ }
1083
+ // src/commands/machines.ts
1084
+ function registerMachinesCommand(program) {
1085
+ const machines = program.command("machines").description("Inspect and destroy agent machines");
1086
+ machines.command("list").description("List the machines you can read, every page").option("--env <env>", "only one environment's machines (name or id)").action(async (opts, cmd) => {
1087
+ const client = getAppClientFromCommand(cmd);
1088
+ const environmentId = opts.env ? await resolveEnvironmentId(client, opts.env) : void 0;
1089
+ formatOutput(await readAllMachines(client, environmentId ? { environmentId } : {}), getFormat(cmd));
1090
+ });
1091
+ machines.command("get").description("Show a machine: its lease, pilot and links").argument("<id>", "machine id").action(async (id, _opts, cmd) => {
1092
+ formatOutput(await getAppClientFromCommand(cmd).machines.get({ id }), getFormat(cmd));
1093
+ });
1094
+ machines.command("runs").description("List the runs and conversation turns of a machine, every page").argument("<id>", "machine id").action(async (id, _opts, cmd) => {
1095
+ const client = getAppClientFromCommand(cmd);
1096
+ formatOutput(await readAllPages((cursor) => client.machines.runs({
1097
+ id,
1098
+ ...cursor && { cursor }
1099
+ })), getFormat(cmd));
1100
+ });
1101
+ machines.command("destroy").description("Destroy a machine").argument("<id>", "machine id").option("--yes", "destroy without confirmation").action(async (id, opts, cmd) => {
1102
+ requireConfirmation(opts.yes, "Destroying a machine is explicit. Re-run with \"--yes\" to confirm.");
1103
+ await getAppClientFromCommand(cmd).machines.destroy({ id });
1104
+ printDone(`Machine ${id} destroyed.`);
621
1105
  });
622
1106
  }
623
1107
  // src/commands/mcp.ts
@@ -676,12 +1160,12 @@ function registerMcpCommand(program) {
676
1160
  id,
677
1161
  trusted
678
1162
  });
679
- process.stdout.write(`${styleText("green", "✓")} MCP server ${id} ${trusted ? "trusted" : "untrusted"}.\n`);
1163
+ printDone(`MCP server ${id} ${trusted ? "trusted" : "untrusted"}.`);
680
1164
  });
681
1165
  mcp.command("disconnect").description("Disconnect (delete) an MCP server").argument("<id>", "MCP server ID").option("--yes", "disconnect without confirmation").action(async (id, opts, cmd) => {
682
1166
  if (!opts.yes) throw new ValidationError("Disconnecting an MCP server is explicit. Re-run with \"--yes\" to confirm.", []);
683
1167
  await getAppClientFromCommand(cmd).mcpServers.delete({ id });
684
- process.stdout.write(`${styleText("green", "✓")} MCP server ${id} disconnected.\n`);
1168
+ printDone(`MCP server ${id} disconnected.`);
685
1169
  });
686
1170
  }
687
1171
  // src/commands/meetings.ts
@@ -703,18 +1187,6 @@ function buildQuery(opts) {
703
1187
  if (opts.offset !== void 0) query.offset = parseCount("--offset", opts.offset);
704
1188
  return query;
705
1189
  }
706
- /** "needs_action" is no answer, so the API takes only the schema's answers. */
707
- function parseResponse(response) {
708
- if (MEETING_RESPONSES.includes(response)) return response;
709
- throw new ValidationError(`response must be one of: ${MEETING_RESPONSES.join(", ")}`, []);
710
- }
711
- /** `--auto` clears the override; `--force` / `--skip` set it. Exactly one flag. */
712
- function parseOverride(opts) {
713
- const chosen = RECORDING_OVERRIDES.filter((override) => opts[override]);
714
- if (opts.auto) chosen.push(null);
715
- if (chosen.length !== 1) throw new ValidationError("record needs exactly one of --force, --skip or --auto", []);
716
- return chosen[0] ?? null;
717
- }
718
1190
  /** The query flags `list` and `for-record` share — both take the same body shape. */
719
1191
  function withQueryOptions(command) {
720
1192
  return command.option("--search <text>", "match title, description and location").option("--since <iso>", "inclusive ISO lower bound on the start").option("--before <iso>", "exclusive ISO upper bound on the start").option("--addresses <emails>", "comma-separated participant addresses to restrict to").option("--order <order>", "asc for what is coming next, desc to read history").option("--limit <n>", "max meetings to return").option("--offset <n>", "number of meetings to skip");
@@ -738,72 +1210,6 @@ function registerMeetingsCommand(program) {
738
1210
  meetings.command("transcript").description("Read a meeting's completed transcript").argument("<id>", "meeting ID").action(async (id, _opts, cmd) => {
739
1211
  formatOutput(await getAppClientFromCommand(cmd).meetings.transcript({ meetingId: id }), getFormat(cmd));
740
1212
  });
741
- meetings.command("record").description("Force, skip, or hand back to the workspace policy the recording of a meeting").argument("<id>", "meeting ID").option("--force", "record this meeting whatever the workspace policy says").option("--skip", "never record this meeting").option("--auto", "clear the override and follow the workspace policy").action(async (id, opts, cmd) => {
742
- const override = parseOverride(opts);
743
- formatOutput(await getAppClientFromCommand(cmd).meetings.setRecording({
744
- meetingId: id,
745
- override
746
- }), getFormat(cmd));
747
- });
748
- meetings.command("respond").description("Answer a meeting invitation").argument("<id>", "meeting ID").argument("<response>", `one of: ${MEETING_RESPONSES.join(", ")}`).option("--notify-organizer", "send the answer to the organizer").action(async (id, response, opts, cmd) => {
749
- formatOutput(await getAppClientFromCommand(cmd).meetings.respond({
750
- meetingId: id,
751
- response: parseResponse(response),
752
- notifyOrganizer: opts.notifyOrganizer === true
753
- }), getFormat(cmd));
754
- });
755
- meetings.command("stop-recording").description("Stop the recording of a meeting in progress").argument("<id>", "meeting ID").action(async (id, _opts, cmd) => {
756
- formatOutput(await getAppClientFromCommand(cmd).meetings.stopRecording({ meetingId: id }), getFormat(cmd));
757
- });
758
- meetings.command("grants-recompute").description("Recompute the attendee grants of every private meeting of the tenant").action(async (_opts, cmd) => {
759
- const tenant = requireTenant(cmd, "A meeting grants recompute names its tenant explicitly. Pass \"--tenant <id>\".");
760
- await getAppClientFromCommand(cmd).meetingsAdmin.recomputeGrants({ tenantId: tenant });
761
- process.stdout.write(`${styleText("green", "✓")} Meeting grants recompute accepted for tenant ${tenant}.\n`);
762
- });
763
- }
764
- // src/commands/memory.ts
765
- function registerMemoryCommand(program) {
766
- program.command("memory").description("Maintain agent memory").command("backfill").description("Rebuild agent memory from the tenant's last 30 days of sessions and meetings").option("--yes", "backfill without confirmation").action(async (opts, cmd) => {
767
- if (!opts.yes) throw new ValidationError("A memory backfill spends model calls on the last 30 days of evidence. Re-run with \"--yes\" to confirm.", []);
768
- const tenant = requireTenant(cmd, "A memory backfill names its tenant explicitly. Pass \"--tenant <id>\" with the tenant you are authenticated in.");
769
- await getAppClientFromCommand(cmd).memoryAdmin.backfill({ tenantId: tenant });
770
- process.stdout.write(`${styleText("green", "✓")} Memory backfill queued for tenant ${tenant}. It runs in the background — follow the server logs for progress.\n`);
771
- });
772
- }
773
- // src/drift/reconciliation-prompt.ts
774
- function renderReconciliationPrompt(state) {
775
- const lines = [
776
- "# Standards schema reconciliation",
777
- "",
778
- "Compare deployed source with shared runtime. This excludes unbuilt local TypeScript and personal preferences.",
779
- "Review the shared changes, update the fluent declarations, then deploy. Copies stay retained after a deploy: reset them in architect mode to return to the source.",
780
- "Use content with detailView/listView and .tab or .aside for view contents. Reading this prompt changes no files or workspace state.",
781
- ""
782
- ];
783
- const { changes } = diffSchemaState(state);
784
- if (changes.length === 0) lines.push("No differences.");
785
- for (const entry of changes) lines.push(`## ${entry.change} ${JSON.stringify(entry.address)}`, `Status: ${entry.status}${entry.status === "copy" ? " (retained)" : ""}`, "```json", prettyCanonical(entry.effective ?? entry.source), "```", "");
786
- return `${lines.join("\n").trimEnd()}\n`;
787
- }
788
- // src/commands/pull.ts
789
- function registerPullCommand(program) {
790
- program.command("pull").description("Read deployed source vs shared runtime; excludes unbuilt local TypeScript").option("--json", "Emit canonical shared state JSON instead of guidance").action(async (opts, cmd) => {
791
- let code = 0;
792
- try {
793
- const state = await readSchemaState(getAppClientFromCommand(cmd));
794
- console.info(opts.json ? prettyCanonical(state) : renderReconciliationPrompt(state).trimEnd());
795
- } catch (error) {
796
- console.error(styleText("red", `✗ ${messageOf(error)}`));
797
- code = 2;
798
- }
799
- process.exit(code);
800
- });
801
- }
802
- // src/commands/quotas.ts
803
- function registerQuotasCommand(program) {
804
- program.command("quotas").description("Inspect the tenant's remaining capacity").command("list").description("List one state per quota key (limit is null when nothing is enforced)").action(async (_opts, cmd) => {
805
- formatOutput(await getAppClientFromCommand(cmd).quotas.list({}), getFormat(cmd));
806
- });
807
1213
  }
808
1214
  // src/commands/records.ts
809
1215
  function parseSortFlag(sort) {
@@ -845,12 +1251,16 @@ function registerRecordsCommand(program) {
845
1251
  id
846
1252
  }), getFormat(cmd));
847
1253
  });
848
- records.command("create").description("Create a new record").argument("<object>", "object name").option("--data <json>", "record data as JSON string").option("--parent <id>", "parent record ID (hierarchical objects only)").action(async (objectName, opts, cmd) => {
1254
+ records.command("create").description("Create a new record").argument("<object>", "object name").option("--data <json>", "record data as JSON string").option("--parent <id>", "parent record ID (hierarchical objects only)").option("--metadata <json>", "record metadata as a JSON object (application state beside its values)").action(async (objectName, opts, cmd) => {
849
1255
  const data = opts.data ? JSON.parse(opts.data) : {};
1256
+ const options = {
1257
+ ...opts.parent ? { parentId: opts.parent } : {},
1258
+ ...opts.metadata !== void 0 ? { metadata: JSON.parse(opts.metadata) } : {}
1259
+ };
850
1260
  formatOutput(await getAppClientFromCommand(cmd).records.create({
851
1261
  objectName,
852
1262
  data,
853
- ...opts.parent ? { options: { parentId: opts.parent } } : {}
1263
+ ...Object.keys(options).length > 0 ? { options } : {}
854
1264
  }), getFormat(cmd));
855
1265
  });
856
1266
  records.command("update").description("Update a record").argument("<object>", "object name").argument("<id>", "record ID").option("--data <json>", "fields to update as JSON string").action(async (objectName, id, opts, cmd) => {
@@ -861,32 +1271,37 @@ function registerRecordsCommand(program) {
861
1271
  values: data
862
1272
  }), getFormat(cmd));
863
1273
  });
864
- records.command("delete").description("Delete a record").argument("<object>", "object name").argument("<id>", "record ID").action(async (objectName, id, _opts, cmd) => {
865
- await getAppClientFromCommand(cmd).records.delete({
866
- objectName,
867
- id
868
- });
869
- process.stdout.write(`Record ${id} deleted.\n`);
870
- });
871
- records.command("deletion-count").description("Count what deleting records would take: broken links and descendants").argument("<object>", "object name").argument("<ids...>", "record IDs").action(async (objectName, ids, _opts, cmd) => {
872
- formatOutput(await getAppClientFromCommand(cmd).records.deletionCount({
1274
+ records.command("delete").description("Delete records (--dry-run counts what the delete would take)").argument("<object>", "object name").argument("<ids...>", "record IDs").option("--dry-run", "count the broken links and descendants, delete nothing").action(async (objectName, ids, opts, cmd) => {
1275
+ const client = getAppClientFromCommand(cmd);
1276
+ if (opts.dryRun) {
1277
+ formatOutput(await client.records.deletionCount({
1278
+ objectName,
1279
+ ids
1280
+ }), getFormat(cmd));
1281
+ return;
1282
+ }
1283
+ const [id] = ids;
1284
+ if (id !== void 0 && ids.length === 1) {
1285
+ await client.records.delete({
1286
+ objectName,
1287
+ id
1288
+ });
1289
+ process.stdout.write(`Record ${id} deleted.\n`);
1290
+ return;
1291
+ }
1292
+ formatOutput(await client.records.bulkDelete({
873
1293
  objectName,
874
1294
  ids
875
1295
  }), getFormat(cmd));
876
1296
  });
877
- records.command("move-impact").description("Show who would gain and lose access if a record moved").argument("<object>", "object name").argument("<id>", "record ID").option("--parent <id>", "new parent record ID").option("--top-level", "move to the top level").action(async (objectName, id, opts, cmd) => {
878
- formatOutput(await getAppClientFromCommand(cmd).records.moveImpact({
879
- objectName,
880
- recordId: id,
881
- parentId: parseMoveTarget(opts)
882
- }), getFormat(cmd));
883
- });
884
- records.command("move").description("Move a record and its descendants under a parent (preview with move-impact)").argument("<object>", "object name").argument("<id>", "record ID").option("--parent <id>", "new parent record ID").option("--top-level", "move to the top level").action(async (objectName, id, opts, cmd) => {
885
- formatOutput(await getAppClientFromCommand(cmd).records.move({
1297
+ records.command("move").description("Move a record and its descendants under a parent (--dry-run shows the access impact)").argument("<object>", "object name").argument("<id>", "record ID").option("--parent <id>", "new parent record ID").option("--top-level", "move to the top level").option("--dry-run", "show who would gain and lose access, move nothing").action(async (objectName, id, opts, cmd) => {
1298
+ const client = getAppClientFromCommand(cmd);
1299
+ const body = {
886
1300
  objectName,
887
1301
  recordId: id,
888
1302
  parentId: parseMoveTarget(opts)
889
- }), getFormat(cmd));
1303
+ };
1304
+ formatOutput(opts.dryRun ? await client.records.moveImpact(body) : await client.records.move(body), getFormat(cmd));
890
1305
  });
891
1306
  records.command("search").description("Full-text search records").argument("<object>", "object name").requiredOption("--query <q>", "search query string").option("--limit <n>", "max records to return").option("--offset <n>", "number of records to skip").option("--sort <sort>", "sort rule as \"attribute:direction\"").option("--filter <json>", "filter state as JSON string").option("--fields <names>", "comma-separated reference attributes to hydrate (empty for none, omit for all)").option("--count <mode>", COUNT_OPTION_DESCRIPTION).action(async (objectName, opts, cmd) => {
892
1307
  const body = {
@@ -940,6 +1355,16 @@ function registerRecordsCommand(program) {
940
1355
  files: new File([fileContent], fileName)
941
1356
  }), getFormat(cmd));
942
1357
  });
1358
+ records.command("attach-file").description("Attach an already uploaded file to a record").argument("<object>", "object name (e.g. contacts)").argument("<recordId>", "record ID").requiredOption("--file-id <fileId>", "ID of a private uploaded file").option("--attribute <attr>", "document attribute (defaults to the record's attachments)").option("--document-id <documentId>", "append to this document's pack instead of creating one").option("--title <title>", "title of a new document (defaults to the file name without its extension)").action(async (objectName, recordId, opts, cmd) => {
1359
+ formatOutput(await getAppClientFromCommand(cmd).records.attachFile({
1360
+ objectName,
1361
+ id: recordId,
1362
+ fileId: opts.fileId,
1363
+ ...opts.attribute ? { attributeName: opts.attribute } : {},
1364
+ ...opts.documentId ? { documentId: opts.documentId } : {},
1365
+ ...opts.title ? { title: opts.title } : {}
1366
+ }), getFormat(cmd));
1367
+ });
943
1368
  const access = records.command("access").description("Share a private record with other actors (readers and editors)");
944
1369
  access.command("list").description("Show who can reach a private record beyond its owner").argument("<object>", "object name").argument("<id>", "record ID").action(async (objectName, id, _opts, cmd) => {
945
1370
  formatOutput(await getAppClientFromCommand(cmd).recordAccess.list({
@@ -973,9 +1398,6 @@ function registerRecordsCommand(program) {
973
1398
  // src/config.ts
974
1399
  const DEFAULT_API_URL = "http://localhost:4100/v1";
975
1400
  const ENV_PROFILE_NAME = "sandbox";
976
- function getDefaultApiUrl() {
977
- return DEFAULT_API_URL;
978
- }
979
1401
  function getConfigPath() {
980
1402
  const configDir = process.env.STANDARDS_CONFIG_DIR ?? join(homedir(), ".standards");
981
1403
  return join(configDir, "config.json");
@@ -1008,7 +1430,9 @@ async function upsertProfile(input) {
1008
1430
  const config = await readConfig();
1009
1431
  config.profiles[input.name] = {
1010
1432
  apiUrl: input.apiUrl,
1011
- apiKey: input.apiKey
1433
+ apiKey: input.apiKey,
1434
+ ...input.keyId ? { keyId: input.keyId } : {},
1435
+ ...input.keyName ? { keyName: input.keyName } : {}
1012
1436
  };
1013
1437
  config.currentProfile = input.name;
1014
1438
  await writeConfig(config);
@@ -1019,6 +1443,7 @@ async function setCurrentProfile(name) {
1019
1443
  config.currentProfile = name;
1020
1444
  await writeConfig(config);
1021
1445
  }
1446
+ /** Removes a profile, the current one by default. */
1022
1447
  async function removeProfile(name) {
1023
1448
  const config = await readConfig();
1024
1449
  const profileName = name ?? config.currentProfile;
@@ -1043,22 +1468,6 @@ async function listProfiles() {
1043
1468
  });
1044
1469
  return profiles;
1045
1470
  }
1046
- async function getActiveProfile() {
1047
- const config = await readConfig();
1048
- if (!config.currentProfile) {
1049
- const envProfile = getEnvProfile();
1050
- return envProfile ? {
1051
- name: ENV_PROFILE_NAME,
1052
- ...envProfile
1053
- } : null;
1054
- }
1055
- const profile = config.profiles[config.currentProfile];
1056
- if (!profile) return null;
1057
- return {
1058
- name: config.currentProfile,
1059
- ...profile
1060
- };
1061
- }
1062
1471
  function getEnvProfile() {
1063
1472
  if (!process.env.STANDARDS_API_KEY) return null;
1064
1473
  return {
@@ -1087,57 +1496,297 @@ async function resolveCliConfig(options) {
1087
1496
  apiKey: process.env.STANDARDS_API_KEY
1088
1497
  };
1089
1498
  }
1499
+ // src/commands/device-login.ts
1500
+ /** The pre-registered public client of the CLI on every Standards instance. */
1501
+ const CLIENT = {
1502
+ client_id: CLI_CLIENT_ID,
1503
+ token_endpoint_auth_method: "none"
1504
+ };
1505
+ /** RFC 8628 §3.5: a `slow_down` adds five seconds to the polling interval. */
1506
+ const SLOW_DOWN_SECONDS = 5;
1507
+ const DEFAULT_INTERVAL_SECONDS = 5;
1508
+ /** The server refuses more `replaces` ids than this (`cliLoginBody`). */
1509
+ const MAX_REPLACES = 20;
1510
+ /** A login that ends without a profile: the line to print and the exit code (spec, Terminal). */
1511
+ var LoginFailure = class extends Error {
1512
+ constructor(message, exitCode) {
1513
+ super(message);
1514
+ this.exitCode = exitCode;
1515
+ this.name = "LoginFailure";
1516
+ }
1517
+ };
1518
+ /** This machine's name as the server accepts a device label: 1-64 printable ASCII characters. */
1519
+ function deviceLabel() {
1520
+ return hostname().replace(/[^\x20-\x7E]/g, "").trim().slice(0, CLI_DEVICE_LABEL_MAX).trim() || "unknown device";
1521
+ }
1522
+ /**
1523
+ * `standards login` through the browser (RFC 8628): discovers the instance's `cli`
1524
+ * resource, shows the code, polls until the person approves, then mints the CLI's
1525
+ * personal key once with the access token and stores it as a profile.
1526
+ */
1527
+ async function deviceLogin(options) {
1528
+ const { signal } = options;
1529
+ try {
1530
+ const resource = new URL(`${options.apiUrl.replace(/\/+$/, "")}/cli`);
1531
+ const http = {
1532
+ signal,
1533
+ [oauth.allowInsecureRequests]: resource.protocol === "http:"
1534
+ };
1535
+ const as = await discoverAuthorizationServer(resource, options.apiUrl, http);
1536
+ const authorization = await oauth.processDeviceAuthorizationResponse(as, CLIENT, await oauth.deviceAuthorizationRequest(as, CLIENT, oauth.None(), {
1537
+ scope: CLI_LOGIN_SCOPE,
1538
+ resource: resource.href,
1539
+ device: options.device
1540
+ }, http));
1541
+ print(`To connect, open ${authorization.verification_uri} and enter the code:\n\n`);
1542
+ print(` ${authorization.user_code}\n\n`);
1543
+ print("Press Enter to open the browser…\n");
1544
+ options.waitForEnter().then(() => options.openBrowser(authorization.verification_uri).catch(() => print("Open the link above on any device.\n"))).catch(() => void 0);
1545
+ print("Waiting for you to approve… (Ctrl-C to cancel)\n");
1546
+ return await storeKey(options, await pollForToken(as, authorization, http));
1547
+ } catch (error) {
1548
+ if (signal.aborted && !(error instanceof LoginFailure)) throw new LoginFailure("Login cancelled. Nothing was saved.", 130);
1549
+ throw error;
1550
+ }
1551
+ }
1552
+ /** RFC 9728 metadata of the `cli` resource, then RFC 8414 metadata of its authorization server. */
1553
+ async function discoverAuthorizationServer(resource, apiUrl, http) {
1554
+ const response = await oauth.resourceDiscoveryRequest(resource, http);
1555
+ if (response.status === 404) throw new LoginFailure(`This instance does not offer browser login. Create a personal key in Settings → API keys, then run standards login --url ${apiUrl} --key <key>.`, 1);
1556
+ const issuerUrl = (await oauth.processResourceDiscoveryResponse(resource, response)).authorization_servers?.[0];
1557
+ if (!issuerUrl) throw new Error(`The ${resource.href} metadata names no authorization server.`);
1558
+ const issuer = new URL(issuerUrl);
1559
+ return oauth.processDiscoveryResponse(issuer, await oauth.discoveryRequest(issuer, {
1560
+ ...http,
1561
+ algorithm: "oauth2"
1562
+ }));
1563
+ }
1564
+ /** Polls the token endpoint at the server's interval, slower after each `slow_down`. */
1565
+ async function pollForToken(as, authorization, http) {
1566
+ let interval = authorization.interval ?? DEFAULT_INTERVAL_SECONDS;
1567
+ for (;;) {
1568
+ await sleep$2(interval * 1e3, http.signal);
1569
+ try {
1570
+ const response = await oauth.deviceCodeGrantRequest(as, CLIENT, oauth.None(), authorization.device_code, http);
1571
+ return (await oauth.processDeviceCodeResponse(as, CLIENT, response)).access_token;
1572
+ } catch (error) {
1573
+ if (!(error instanceof oauth.ResponseBodyError)) throw error;
1574
+ switch (error.error) {
1575
+ case "authorization_pending": continue;
1576
+ case "slow_down":
1577
+ interval += SLOW_DOWN_SECONDS;
1578
+ continue;
1579
+ case "access_denied": throw new LoginFailure("✗ Login was declined in the browser. Run standards login to try again.", 1);
1580
+ case "expired_token": throw new LoginFailure("✗ The code expired. Run standards login to get a new one.", 1);
1581
+ default: throw error;
1582
+ }
1583
+ }
1584
+ }
1585
+ }
1586
+ /**
1587
+ * Mints the key once with the access token (sent as the transport's bearer), then
1588
+ * writes the profile. `replaces` names the keys of every profile on the same instance
1589
+ * (D8); a profile whose key the server revoked goes with it. The access token is not kept.
1590
+ */
1591
+ async function storeKey(options, accessToken) {
1592
+ const config = await readConfig();
1593
+ const replaces = Object.values(config.profiles).filter((profile) => profile.apiUrl === options.apiUrl && profile.keyId).map((profile) => profile.keyId).slice(0, MAX_REPLACES);
1594
+ let minted;
1595
+ try {
1596
+ minted = await createAppClient(createClient({
1597
+ apiUrl: options.apiUrl,
1598
+ apiKey: accessToken
1599
+ })).cliLogin.createKey({
1600
+ device: options.device,
1601
+ ...replaces.length ? { replaces } : {}
1602
+ }, { signal: options.signal });
1603
+ } catch (error) {
1604
+ if (options.signal.aborted) throw new LoginFailure(`Login cancelled. A key may have been created; revoke "Standards CLI - ${options.device}" in Settings → API keys if so.`, 130);
1605
+ throw new LoginFailure(`✗ Approved, but the key could not be created (${error instanceof StandardsRequestError && error.status > 0 ? String(error.status) : "network error"}). Run standards login again.`, 1);
1606
+ }
1607
+ const profileName = options.name ?? (slugify(minted.workspace.name) || "standards");
1608
+ await upsertProfile({
1609
+ name: profileName,
1610
+ apiUrl: options.apiUrl,
1611
+ apiKey: minted.apiKey.secret,
1612
+ keyId: minted.apiKey.id,
1613
+ keyName: minted.apiKey.name
1614
+ });
1615
+ const replaced = new Set(minted.replaced);
1616
+ const removedProfiles = Object.entries(config.profiles).filter(([name, { keyId }]) => name !== profileName && keyId !== void 0 && replaced.has(keyId)).map(([name]) => name);
1617
+ for (const name of removedProfiles) await removeProfile(name);
1618
+ return {
1619
+ profileName,
1620
+ workspace: minted.workspace.name,
1621
+ replacedProfile: profileName in config.profiles,
1622
+ removedProfiles
1623
+ };
1624
+ }
1625
+ /** A wait that Ctrl-C cuts short. */
1626
+ function sleep$2(ms, signal) {
1627
+ return new Promise((resolve, reject) => {
1628
+ if (signal.aborted) return reject(signal.reason);
1629
+ const timer = setTimeout(() => {
1630
+ signal.removeEventListener("abort", onAbort);
1631
+ resolve();
1632
+ }, ms);
1633
+ function onAbort() {
1634
+ clearTimeout(timer);
1635
+ reject(signal.reason);
1636
+ }
1637
+ signal.addEventListener("abort", onAbort, { once: true });
1638
+ });
1639
+ }
1640
+ function print(line) {
1641
+ process.stdout.write(line);
1642
+ }
1090
1643
  // src/commands/root.ts
1091
- async function promptForMissingValue(label) {
1092
- const rl = createInterface({
1644
+ /** `login` targets hosted Standards unless `--url` says otherwise (D3); other commands keep `DEFAULT_API_URL`. */
1645
+ const HOSTED_API_URL = "https://api.standards.new/v1";
1646
+ /** Over SSH the browser `open` would start is on the wrong machine: the person opens the link themselves. */
1647
+ async function openBrowser(url) {
1648
+ if (process.env.SSH_CONNECTION || process.env.SSH_TTY) throw new Error("No local browser");
1649
+ const { default: open } = await import("open");
1650
+ await open(url);
1651
+ }
1652
+ /** Runs the browser login with Ctrl-C and Enter wired to this terminal, and exits with the spec's code on failure. */
1653
+ async function loginThroughBrowser(apiUrl, name) {
1654
+ if (!isInteractive()) {
1655
+ console.error(styleText("red", "✗ Non-interactive: set STANDARDS_API_KEY or run standards login --key <key>."));
1656
+ process.exit(2);
1657
+ }
1658
+ const controller = new AbortController();
1659
+ const cancel = () => controller.abort();
1660
+ process.once("SIGINT", cancel);
1661
+ const enter = createInterface({
1093
1662
  input: stdin,
1094
- output: stdout
1663
+ terminal: false
1095
1664
  });
1096
1665
  try {
1097
- return (await rl.question(`${label}: `)).trim();
1666
+ const result = await deviceLogin({
1667
+ apiUrl,
1668
+ name,
1669
+ device: deviceLabel(),
1670
+ openBrowser,
1671
+ waitForEnter: () => new Promise((resolve) => enter.once("line", () => resolve())),
1672
+ signal: controller.signal
1673
+ });
1674
+ printDone(`Connected to ${result.workspace}. Profile "${result.profileName}" selected.`);
1675
+ if (result.replacedProfile) process.stdout.write(` Replaced the previous profile "${result.profileName}".\n`);
1676
+ for (const name of result.removedProfiles) process.stdout.write(` Removed profile "${name}": its key was replaced.\n`);
1677
+ } catch (error) {
1678
+ if (!(error instanceof LoginFailure)) throw error;
1679
+ console.error(error.exitCode === 130 ? error.message : styleText("red", error.message));
1680
+ process.exit(error.exitCode);
1098
1681
  } finally {
1099
- rl.close();
1682
+ process.off("SIGINT", cancel);
1683
+ enter.close();
1100
1684
  }
1101
1685
  }
1102
- function registerRootCommands(program) {
1103
- program.command("login").description("Connect the CLI to a Standards instance").option("--url <url>", "Standards API URL", getDefaultApiUrl()).option("--key <key>", "API key to store for this instance").option("--name <name>", "local instance name", "local").action(async (opts) => {
1104
- const apiKey = opts.key ?? await promptForMissingValue("API key");
1105
- if (!apiKey) throw new Error("API key is required");
1686
+ /** `--key`: validate the pasted key against the instance, then store it (no `keyId`: logout cannot revoke it). */
1687
+ async function loginWithKey(apiUrl, apiKey, name) {
1688
+ await createAppClient(createClient({
1689
+ apiUrl,
1690
+ apiKey
1691
+ })).apiKeys.list({});
1692
+ await upsertProfile({
1693
+ name,
1694
+ apiUrl,
1695
+ apiKey
1696
+ });
1697
+ printDone(`Standards instance "${name}" saved and selected.`);
1698
+ process.stdout.write(` API URL: ${apiUrl}\n`);
1699
+ process.stdout.write(` API Key: ${apiKey.slice(0, 8)}...\n`);
1700
+ process.stdout.write(` Use it with: standards --instance ${name} <command>\n`);
1701
+ }
1702
+ /** How long `logout` waits on the revocation before warning and removing the profile anyway. */
1703
+ const REVOKE_TIMEOUT_MS = 1e4;
1704
+ /** D11: revoke the profile's key with that key itself, best effort: a failure only warns. */
1705
+ async function revokeKey(profile) {
1706
+ if (!profile?.keyId) return;
1707
+ try {
1106
1708
  await createAppClient(createClient({
1107
- apiUrl: opts.url,
1108
- apiKey
1109
- })).apiKeys.list({});
1110
- await upsertProfile({
1111
- name: opts.name,
1112
- apiUrl: opts.url,
1113
- apiKey
1114
- });
1115
- process.stdout.write(`${styleText("green", "✓")} Standards instance "${opts.name}" saved and selected.\n`);
1116
- process.stdout.write(` API URL: ${opts.url}\n`);
1117
- process.stdout.write(` API Key: ${apiKey.slice(0, 8)}...\n`);
1118
- process.stdout.write(` Use it with: standards --instance ${opts.name} <command>\n`);
1709
+ apiUrl: profile.apiUrl,
1710
+ apiKey: profile.apiKey
1711
+ })).apiKeys.revoke({ id: profile.keyId }, { signal: AbortSignal.timeout(REVOKE_TIMEOUT_MS) });
1712
+ } catch {
1713
+ const keyName = profile.keyName ?? `Standards CLI - ${deviceLabel()}`;
1714
+ console.error(styleText("yellow", `! The key could not be revoked on the server. Revoke "${keyName}" in Settings → API keys.`));
1715
+ }
1716
+ }
1717
+ function registerRootCommands(program) {
1718
+ program.command("login").description("Connect the CLI to a Standards instance through the browser, or with --key").option("--url <url>", "Standards API URL", HOSTED_API_URL).option("--key <key>", "API key to store for this instance (self-hosted instances, scripts)").option("--name <name>", "profile name (defaults to the workspace name, or \"local\" with --key)").action(async (opts) => {
1719
+ if (opts.key) {
1720
+ await loginWithKey(opts.url, opts.key, opts.name ?? "local");
1721
+ return;
1722
+ }
1723
+ await loginThroughBrowser(opts.url, opts.name);
1119
1724
  });
1120
1725
  program.command("use").description("Select the active Standards instance").argument("<name>", "instance name").action(async (name) => {
1121
1726
  await setCurrentProfile(name);
1122
- process.stdout.write(`${styleText("green", "✓")} Standards instance "${name}" selected.\n`);
1727
+ printDone(`Standards instance "${name}" selected.`);
1123
1728
  });
1124
1729
  program.command("instances").description("List configured Standards instances").action(async (_opts, cmd) => {
1125
1730
  formatOutput(await listProfiles(), getFormat(cmd));
1126
1731
  });
1127
- program.command("current").description("Show the active Standards instance").action(async () => {
1128
- const profile = await getActiveProfile();
1129
- if (!profile) {
1130
- process.stdout.write(`No Standards instance selected. Run "standards login".\n`);
1131
- return;
1132
- }
1133
- process.stdout.write(`${profile.name} ${profile.apiUrl}\n`);
1732
+ program.command("whoami").description("Show the active profile, API URL and key prefix, and validate the key").action(async (_opts, cmd) => {
1733
+ const { profileName, apiUrl, apiKey } = getGlobalOptions(cmd);
1734
+ process.stdout.write(`Profile: ${profileName ?? "none (--api-key)"}\n`);
1735
+ process.stdout.write(`API URL: ${apiUrl}\n`);
1736
+ process.stdout.write(`API Key: ${apiKey?.slice(0, 8)}...\n`);
1737
+ await getAppClientFromCommand(cmd).apiKeys.list({});
1738
+ printDone("API key is valid");
1134
1739
  });
1135
- program.command("logout").description("Remove a Standards instance from local CLI config").argument("[name]", "instance name, defaults to current").action(async (name) => {
1136
- await removeProfile(name);
1137
- process.stdout.write(`${styleText("green", "✓")} Standards instance removed.\n`);
1740
+ program.command("logout").description("Remove a Standards instance from local CLI config and revoke its key").argument("[name]", "instance name, defaults to current").action(async (name) => {
1741
+ const config = await readConfig();
1742
+ const profileName = name ?? config.currentProfile;
1743
+ await revokeKey(profileName ? config.profiles[profileName] : void 0);
1744
+ await removeProfile(profileName);
1745
+ printDone("Standards instance removed.");
1138
1746
  });
1139
1747
  }
1748
+ // src/drift/shared-state.ts
1749
+ async function readSchemaState(client) {
1750
+ const state = parseSchemaState(await client.schemaState.get({}));
1751
+ if (createHash("sha256").update(canonicalStringify(schemaStateSourcePayload(state.entries))).digest("hex") !== state.sourceFingerprint) throw new ValidationError("Invalid schema state: sourceFingerprint does not match deployed source", []);
1752
+ return state;
1753
+ }
1754
+ function prettyCanonical(value) {
1755
+ return JSON.stringify(JSON.parse(canonicalStringify(value)), null, 2);
1756
+ }
1757
+ // src/drift/reconciliation-prompt.ts
1758
+ function renderReconciliationPrompt(state) {
1759
+ const lines = [
1760
+ "# Standards schema reconciliation",
1761
+ "",
1762
+ "Compare deployed source with shared runtime. This excludes unbuilt local TypeScript and personal preferences.",
1763
+ "Review the shared changes, update the fluent declarations, then deploy. Copies stay retained after a deploy: reset them in architect mode to return to the source.",
1764
+ "Use content with detailView/listView and .tab or .aside for view contents. Reading this prompt changes no files or workspace state.",
1765
+ ""
1766
+ ];
1767
+ const { changes } = diffSchemaState(state);
1768
+ if (changes.length === 0) lines.push("No differences.");
1769
+ for (const entry of changes) lines.push(`## ${entry.change} ${JSON.stringify(entry.address)}`, `Status: ${entry.status}${entry.status === "copy" ? " (retained)" : ""}`, "```json", prettyCanonical(entry.effective ?? entry.source), "```", "");
1770
+ return `${lines.join("\n").trimEnd()}\n`;
1771
+ }
1140
1772
  // src/commands/schema.ts
1773
+ async function runDiffCommand(client) {
1774
+ try {
1775
+ const diff = diffSchemaState(await readSchemaState(client));
1776
+ return {
1777
+ exitCode: diff.changes.length ? 1 : 0,
1778
+ diff
1779
+ };
1780
+ } catch (error) {
1781
+ return {
1782
+ exitCode: 2,
1783
+ errorMessage: messageOf(error)
1784
+ };
1785
+ }
1786
+ }
1787
+ function renderDiff(diff) {
1788
+ return ["Deployed source vs shared runtime", ...diff.changes.length ? diff.changes.map((entry) => `${entry.change} ${JSON.stringify(entry.address)}`) : ["No differences."]].join("\n");
1789
+ }
1141
1790
  function registerSchemaCommand(program) {
1142
1791
  const schema = program.command("schema").description("Inspect schema objects and attributes");
1143
1792
  schema.command("list").description("List all schema objects").action(async (_opts, cmd) => {
@@ -1149,28 +1798,598 @@ function registerSchemaCommand(program) {
1149
1798
  schema.command("groups").description("List the bundles that own objects, with their label").action(async (_opts, cmd) => {
1150
1799
  formatOutput(await getAppClientFromCommand(cmd).schemaGroups.list({}), getFormat(cmd));
1151
1800
  });
1801
+ schema.command("pull").description("Read deployed source vs shared runtime; excludes unbuilt local TypeScript (--format json for the canonical state)").action(async (_opts, cmd) => {
1802
+ let code = 0;
1803
+ try {
1804
+ const state = await readSchemaState(getAppClientFromCommand(cmd));
1805
+ console.info(isHumanOutput(cmd) ? renderReconciliationPrompt(state).trimEnd() : prettyCanonical(state));
1806
+ } catch (error) {
1807
+ console.error(styleText("red", `✗ ${messageOf(error)}`));
1808
+ code = 2;
1809
+ }
1810
+ process.exit(code);
1811
+ });
1812
+ schema.command("diff").description("Compare deployed source vs shared runtime; excludes unbuilt local TypeScript (exit 0 equal, 1 differences, 2 error)").action(async (_opts, cmd) => {
1813
+ let result;
1814
+ try {
1815
+ result = await runDiffCommand(getAppClientFromCommand(cmd));
1816
+ } catch (error) {
1817
+ result = {
1818
+ exitCode: 2,
1819
+ errorMessage: messageOf(error)
1820
+ };
1821
+ }
1822
+ if (result.exitCode === 2) console.error(styleText("red", `✗ ${result.errorMessage}`));
1823
+ else console.info(isHumanOutput(cmd) ? renderDiff(result.diff) : prettyCanonical(result.diff));
1824
+ process.exit(result.exitCode);
1825
+ });
1152
1826
  }
1153
- // src/commands/search.ts
1154
- function registerSearchCommand(program) {
1155
- program.command("search").description("Maintain the Standards search index").command("reindex").description("Rebuild the tenant search index beside the live one, then switch").option("--yes", "reindex without confirmation").option("--emails", "hand the tenant's stored emails back to the email indexer instead (nothing is cleared)").option("--after-id <emailId>", "with --emails, resume after the last email id a failed run reported").option("--rebuild", "with --emails and --yes, rebuild the mail search index from the stored mail instead (lifts a quarantine)").action(async (opts, cmd) => {
1156
- if (opts.afterId !== void 0 && !opts.emails) throw new ValidationError("\"--after-id\" resumes an email reindex. Pass it with \"--emails\".", []);
1157
- if (opts.rebuild && !opts.emails) throw new ValidationError("\"--rebuild\" rebuilds the mail search index. Pass it with \"--emails\".", []);
1158
- if (opts.rebuild && opts.afterId !== void 0) throw new ValidationError("\"--after-id\" resumes a requeue; a mail rebuild reads every stored email. Pass one of them.", []);
1159
- if (opts.emails) {
1160
- const tenant = requireTenant(cmd, "An email reindex names its tenant explicitly. Pass \"--tenant <id>\" with the tenant you are authenticated in.");
1161
- if (opts.rebuild && !opts.yes) throw new ValidationError("A mail rebuild re-reads every stored email. Re-run with \"--yes\" to confirm.", []);
1162
- await getAppClientFromCommand(cmd).searchAdmin.emailReindex({
1163
- tenantId: tenant,
1164
- ...opts.afterId !== void 0 && { afterId: opts.afterId },
1165
- ...opts.rebuild && { rebuild: true }
1166
- });
1167
- process.stdout.write(opts.rebuild ? `${styleText("green", "✓")} Mail index rebuild accepted. Mail search keeps answering from the current index until the rebuilt one replaces it — follow the server logs for progress.\n` : `${styleText("green", "✓")} Email reindex accepted for tenant ${tenant}. It runs in the background — follow the server logs for progress.\n`);
1827
+ // src/commands/secrets.ts
1828
+ /** The owner flags are judged before stdin or any call: `--private` and `--env` exclude each other. */
1829
+ function checkOwnerFlags(opts) {
1830
+ if (opts.private && opts.env) throw new ValidationError("--private and --env name different owners: pass one", []);
1831
+ if (opts.repository && !opts.env) throw new ValidationError("--repository is only accepted with --env <env>", []);
1832
+ }
1833
+ async function resolveOwner(client, opts) {
1834
+ if (opts.private) return { kind: "private" };
1835
+ if (!opts.env) return { kind: "workspace" };
1836
+ const id = await resolveEnvironmentId(client, opts.env);
1837
+ return opts.repository ? {
1838
+ kind: "environment",
1839
+ id,
1840
+ repository: opts.repository
1841
+ } : {
1842
+ kind: "environment",
1843
+ id
1844
+ };
1845
+ }
1846
+ /** A listed secret without its `value`: the CLI never prints one, sensitive or not. */
1847
+ function withoutValue(entry) {
1848
+ if (entry.kind !== "variable") return entry;
1849
+ const { value: _omitted, ...rest } = entry;
1850
+ return rest;
1851
+ }
1852
+ async function listOwnerSecrets(client, owner) {
1853
+ switch (owner.kind) {
1854
+ case "workspace": return await client.envVars.list({});
1855
+ case "private": return await client.privateEnvVars.list({});
1856
+ case "environment": return await client.environmentEnvVars.list({ id: owner.id });
1857
+ }
1858
+ }
1859
+ /** What a new workspace secret reaches: `all`, or each named environment's id. */
1860
+ async function resolveReach(client, value) {
1861
+ const names = parseCsvList(value);
1862
+ if (names.length === 1 && names[0] === "all") return "all";
1863
+ if (names.includes("all")) throw new ValidationError("--environments takes \"all\" alone or environment names", []);
1864
+ return await Promise.all(names.map((name) => resolveEnvironmentId(client, name)));
1865
+ }
1866
+ async function setSecret(client, owner, entry, environments) {
1867
+ switch (owner.kind) {
1868
+ case "private": return await client.privateEnvVars.upsert(entry);
1869
+ case "environment": return await client.environmentEnvVars.batch({
1870
+ id: owner.id,
1871
+ set: [owner.repository ? {
1872
+ ...entry,
1873
+ repository: owner.repository
1874
+ } : entry],
1875
+ remove: []
1876
+ });
1877
+ case "workspace": return await client.envVars.upsert(environments === void 0 ? entry : {
1878
+ ...entry,
1879
+ environments: await resolveReach(client, environments)
1880
+ });
1881
+ }
1882
+ }
1883
+ async function deleteSecret(client, owner, key) {
1884
+ const variables = (await listOwnerSecrets(client, owner)).filter((entry) => entry.kind === "variable");
1885
+ if (owner.kind === "environment") {
1886
+ findNamed(variables.filter((entry) => entry.repository === owner.repository), "key", key, "secret");
1887
+ await client.environmentEnvVars.batch({
1888
+ id: owner.id,
1889
+ set: [],
1890
+ remove: [owner.repository ? {
1891
+ key,
1892
+ repository: owner.repository
1893
+ } : key]
1894
+ });
1895
+ return;
1896
+ }
1897
+ const { id } = findNamed(variables, "key", key, "secret");
1898
+ if (owner.kind === "private") await client.privateEnvVars.delete({ id });
1899
+ else await client.envVars.delete({ id });
1900
+ }
1901
+ function stripped(result) {
1902
+ return Array.isArray(result) ? result.map(withoutValue) : withoutValue(result);
1903
+ }
1904
+ /** A workspace secret that reaches no environment yet; an update keeps the key's reach. */
1905
+ function reachesNoEnvironment(result) {
1906
+ if (Array.isArray(result)) return false;
1907
+ const { environments } = result;
1908
+ return environments === void 0 || Array.isArray(environments) && environments.length === 0;
1909
+ }
1910
+ function registerSecretsCommand(program) {
1911
+ const secrets = program.command("secrets").description("Manage secrets: the workspace's, your own, or an environment's");
1912
+ secrets.command("list").description("List secrets (never their values)").option("--private", "your own secrets").option("--env <env>", "an environment's secrets (name or id)").action(async (opts, cmd) => {
1913
+ checkOwnerFlags(opts);
1914
+ const client = getAppClientFromCommand(cmd);
1915
+ formatOutput((await listOwnerSecrets(client, await resolveOwner(client, opts))).map(withoutValue), getFormat(cmd));
1916
+ });
1917
+ secrets.command("set").description("Set a secret from piped stdin: printf %s \"$VALUE\" | standards secrets set KEY").argument("<KEY>", "secret key").allowExcessArguments(false).option("--private", "your own secret").option("--env <env>", "an environment's secret (name or id)").option("--repository <owner/name>", "scope an environment's secret to one repository").option("--environments <env,…|all>", "the environments a new workspace secret reaches").option("--plain", "store it as non-sensitive").action(async (key, opts, cmd) => {
1918
+ checkOwnerFlags(opts);
1919
+ if (opts.environments !== void 0 && (opts.private || opts.env)) throw new ValidationError("--environments only applies to a workspace secret", []);
1920
+ const value = await readPipedValue(`secrets set ${key}`);
1921
+ const client = getAppClientFromCommand(cmd);
1922
+ const owner = await resolveOwner(client, opts);
1923
+ const result = await setSecret(client, owner, {
1924
+ key,
1925
+ value,
1926
+ isSensitive: !opts.plain
1927
+ }, opts.environments);
1928
+ formatOutput(stripped(result), getFormat(cmd));
1929
+ if (owner.kind === "workspace" && reachesNoEnvironment(result)) process.stderr.write("Reaches no environment yet: re-run with --environments <env,…|all>\n");
1930
+ });
1931
+ secrets.command("delete").description("Delete a secret").argument("<KEY>", "secret key").option("--private", "your own secret").option("--env <env>", "an environment's secret (name or id)").option("--repository <owner/name>", "the repository's value of an environment's secret").option("--yes", "delete without confirmation").action(async (key, opts, cmd) => {
1932
+ checkOwnerFlags(opts);
1933
+ requireConfirmation(opts.yes, "Deleting a secret is explicit. Re-run with \"--yes\" to confirm.");
1934
+ const client = getAppClientFromCommand(cmd);
1935
+ await deleteSecret(client, await resolveOwner(client, opts), key);
1936
+ printDone(`Secret ${key} deleted.`);
1937
+ });
1938
+ }
1939
+ // src/follow-turn.ts
1940
+ /** Exit codes of a followed turn: completed, failed or cancelled, parked on a decision. */
1941
+ const TURN_EXIT = {
1942
+ completed: 0,
1943
+ failed: 1,
1944
+ decision: 3
1945
+ };
1946
+ /** The waits between reads of a parked turn's decisions, opened just after its commit. */
1947
+ const DECISION_RETRY_MS = [
1948
+ 200,
1949
+ 500,
1950
+ 1e3,
1951
+ 2e3
1952
+ ];
1953
+ /**
1954
+ * Consecutive stream opens that may fail before a follow gives up (about 40 s
1955
+ * with the reader's back-off): a browser tab may wait out an outage, a shell
1956
+ * agent needs an exit. A deploy blip stays under it.
1957
+ */
1958
+ const MAX_FAILED_OPENS = 10;
1959
+ /** The newest history page: its `committedSeq` is the cursor a stream opens after. */
1960
+ async function readHistoryPage(client, sessionId, limit) {
1961
+ const page = await client.agentMessages.list({
1962
+ id: sessionId,
1963
+ history: "1",
1964
+ ...limit !== void 0 && { limit }
1965
+ });
1966
+ if (Array.isArray(page)) throw new SchemaError("The messages route did not answer a history window", SchemaErrorCode.INVARIANT_VIOLATION);
1967
+ return page;
1968
+ }
1969
+ function sleep$1(ms) {
1970
+ return new Promise((resolve) => setTimeout(resolve, ms));
1971
+ }
1972
+ /** The session's open decisions, read again until `wanted` exist or the retries run out. */
1973
+ async function waitForDecisions(client, sessionId, wanted) {
1974
+ const link = `session:${sessionId}`;
1975
+ let decisions = await readOpenDecisions(client, link);
1976
+ for (const delay of DECISION_RETRY_MS) {
1977
+ if (decisions.length >= wanted) break;
1978
+ await sleep$1(delay);
1979
+ decisions = await readOpenDecisions(client, link);
1980
+ }
1981
+ return decisions;
1982
+ }
1983
+ /**
1984
+ * Prints the decisions a parked session waits on and answers exit 3; when
1985
+ * none is open yet, says where to look.
1986
+ */
1987
+ function reportDecisions(decisions, options) {
1988
+ if (decisions.length === 0) {
1989
+ process.stderr.write("decision not opened yet: run \"standards decisions list\"\n");
1990
+ return TURN_EXIT.decision;
1991
+ }
1992
+ for (const decision of decisions) options.write(options.human ? `\n${renderDecision(decision)}` : `${JSON.stringify({
1993
+ type: "decision",
1994
+ decision
1995
+ })}\n`);
1996
+ return TURN_EXIT.decision;
1997
+ }
1998
+ /** A writer that knows whether the text so far ends a line, so markers start their own. */
1999
+ function createTurnWriter(options) {
2000
+ let atLineStart = true;
2001
+ return {
2002
+ text(text) {
2003
+ if (text === "") return;
2004
+ if (options.human) {
2005
+ options.write(text);
2006
+ atLineStart = text.endsWith("\n");
2007
+ } else options.write(`${JSON.stringify({
2008
+ type: "text",
2009
+ text
2010
+ })}\n`);
2011
+ },
2012
+ /** A line of its own in human mode, an NDJSON object otherwise. */
2013
+ marker(line, json) {
2014
+ if (!options.human) {
2015
+ options.write(`${JSON.stringify(json)}\n`);
2016
+ return;
2017
+ }
2018
+ options.write(`${atLineStart ? "" : "\n"}${line}\n`);
2019
+ atLineStart = true;
2020
+ },
2021
+ endLine() {
2022
+ if (options.human && !atLineStart) options.write("\n");
2023
+ atLineStart = true;
2024
+ }
2025
+ };
2026
+ }
2027
+ /**
2028
+ * The end a session's state records once no turn is running: what a follow
2029
+ * resolves with when a 410 skipped the end event. `undefined` while running.
2030
+ */
2031
+ function endFromState(state, inputRequests) {
2032
+ if (state.phase === "running") return void 0;
2033
+ const outcome = state.outcome ?? "completed";
2034
+ return {
2035
+ type: state.phase === "parked" ? "turn.completed" : `turn.${outcome}`,
2036
+ data: {
2037
+ outcome,
2038
+ phase: state.phase
2039
+ },
2040
+ inputRequests
2041
+ };
2042
+ }
2043
+ function includes(ids, id) {
2044
+ return id !== void 0 && (ids ?? []).includes(id);
2045
+ }
2046
+ /**
2047
+ * Streams one turn to its end: adopts the turn (the one carrying our
2048
+ * `clientMessageId`, the named `turnId`, or the next to start), writes its
2049
+ * text and tool calls, and resolves with its end event. The shared reader
2050
+ * reconnects at the cursor; a 410 reopens at the newest page's `committedSeq`,
2051
+ * unless the session no longer runs a turn: its state then says how ours ended.
2052
+ * A refused stream (401 included: the CLI never re-authenticates) rejects.
2053
+ */
2054
+ function streamTurn(client, sessionId, options) {
2055
+ const out = createTurnWriter(options);
2056
+ let cursor = options.cursor;
2057
+ let turnId = options.turnId;
2058
+ let inputRequests = 0;
2059
+ let failedOpens = 0;
2060
+ const toolCalls = /* @__PURE__ */ new Set();
2061
+ function adopt(event) {
2062
+ if (event.turnId === void 0) return;
2063
+ const ours = options.clientMessageId;
2064
+ if (ours === void 0) {
2065
+ if (event.type === "turn.started") turnId = event.turnId;
1168
2066
  return;
1169
2067
  }
1170
- if (!opts.yes) throw new ValidationError("A rebuild re-reads every record of the tenant. Re-run with \"--yes\" to confirm.", []);
1171
- const tenant = requireTenant(cmd, "A rebuild names its tenant explicitly. Pass \"--tenant <id>\" with the tenant you are authenticated in.");
1172
- await getAppClientFromCommand(cmd).searchAdmin.fullReindex({ tenantId: tenant });
1173
- process.stdout.write(`${styleText("green", "✓")} Index rebuild accepted for tenant ${tenant}. Search keeps answering from the current index until the rebuilt one replaces it — follow the server logs for progress.\n`);
2068
+ if ((event.type === "turn.started" || event.type === "message.consumed") && includes(event.clientMessageIds, ours)) turnId = event.turnId;
2069
+ }
2070
+ function foldChunk(data) {
2071
+ if (!isPlainRecord(data)) return;
2072
+ if (data.type === "text-delta" && typeof data.delta === "string") {
2073
+ out.text(data.delta);
2074
+ return;
2075
+ }
2076
+ if ((data.type === "tool-input-start" || data.type === "tool-input-available") && typeof data.toolName === "string" && typeof data.toolCallId === "string" && !toolCalls.has(data.toolCallId)) {
2077
+ toolCalls.add(data.toolCallId);
2078
+ out.marker(`→ tool ${data.toolName}`, {
2079
+ type: "tool",
2080
+ name: data.toolName
2081
+ });
2082
+ }
2083
+ }
2084
+ return new Promise((resolve, reject) => {
2085
+ function fail(error) {
2086
+ handle.close();
2087
+ reject(error);
2088
+ }
2089
+ function finish(end) {
2090
+ handle.close();
2091
+ out.endLine();
2092
+ resolve(end);
2093
+ }
2094
+ const handle = openAgentStream({
2095
+ open: async (id, after, signal, onRequest) => {
2096
+ try {
2097
+ const response = await client.agentSessions.stream({
2098
+ id,
2099
+ after
2100
+ }, {
2101
+ signal,
2102
+ onRequest,
2103
+ headers: { Accept: "text/event-stream" }
2104
+ });
2105
+ failedOpens = 0;
2106
+ return response;
2107
+ } catch (error) {
2108
+ failedOpens += 1;
2109
+ if (failedOpens >= MAX_FAILED_OPENS) fail(new ValidationError(`The session stream did not open after ${MAX_FAILED_OPENS} attempts`, []));
2110
+ throw error;
2111
+ }
2112
+ },
2113
+ sessionId,
2114
+ cursor: () => cursor,
2115
+ onEvent: (event) => {
2116
+ cursor = event.seq;
2117
+ if (turnId === void 0) adopt(event);
2118
+ if (turnId === void 0 || event.turnId !== turnId) return;
2119
+ if (TURN_END_TYPES.has(event.type)) {
2120
+ finish({
2121
+ type: event.type,
2122
+ data: isPlainRecord(event.data) ? event.data : {},
2123
+ inputRequests
2124
+ });
2125
+ return;
2126
+ }
2127
+ if (event.type === "chunk") foldChunk(event.data);
2128
+ else if (event.type === "step.reset") out.marker("↺ retrying step", { type: "reset" });
2129
+ else if (event.type === "input.requested") inputRequests += 1;
2130
+ },
2131
+ onControl: () => void 0,
2132
+ onStatus: (status, error) => {
2133
+ if (status !== "error" && status !== "auth-blocked") return;
2134
+ fail(error ?? new ValidationError("The session stream was refused", []));
2135
+ },
2136
+ onExpired: async () => {
2137
+ const page = await readHistoryPage(client, sessionId, 1);
2138
+ cursor = page.committedSeq;
2139
+ if (page.messages.some((message) => message.waiting)) return;
2140
+ const end = endFromState(page.sessionState ?? (await client.agentSessions.get({ id: sessionId })).state, inputRequests);
2141
+ if (end) finish(end);
2142
+ }
2143
+ });
2144
+ });
2145
+ }
2146
+ function failureText(data) {
2147
+ if (typeof data.error === "string") return `: ${data.error}`;
2148
+ if (typeof data.code === "string") return `: ${data.code}`;
2149
+ return "";
2150
+ }
2151
+ /**
2152
+ * Follows one turn of a session to its end (spec §3) and answers its exit
2153
+ * code: 0 completed, 1 failed or cancelled, 3 parked on a decision (printed).
2154
+ * Never exits the process: the command does.
2155
+ */
2156
+ async function followTurn(client, sessionId, options) {
2157
+ const end = await streamTurn(client, sessionId, options);
2158
+ const parked = end.type === "turn.completed" && end.data.phase === "parked";
2159
+ const outcome = typeof end.data.outcome === "string" ? end.data.outcome : end.type.replace("turn.", "");
2160
+ if (!options.human) options.write(`${JSON.stringify({
2161
+ type: "end",
2162
+ outcome,
2163
+ ...parked && { phase: "parked" }
2164
+ })}\n`);
2165
+ if (end.type === "turn.cancelled") {
2166
+ if (options.human) process.stderr.write("✗ cancelled\n");
2167
+ return TURN_EXIT.failed;
2168
+ }
2169
+ if (end.type !== "turn.completed") {
2170
+ if (options.human) process.stderr.write(`✗ turn failed${failureText(end.data)}\n`);
2171
+ return TURN_EXIT.failed;
2172
+ }
2173
+ if (!parked) return TURN_EXIT.completed;
2174
+ return reportDecisions(await waitForDecisions(client, sessionId, Math.max(1, end.inputRequests)), options);
2175
+ }
2176
+ // src/commands/sessions.ts
2177
+ /** A session as one line of a list: what a model reads to pick the next one. */
2178
+ function summarizeSession(session) {
2179
+ return {
2180
+ id: session.id,
2181
+ name: session.name ?? null,
2182
+ phase: session.state.phase,
2183
+ outcome: session.state.outcome ?? null,
2184
+ updatedAt: session.updatedAt
2185
+ };
2186
+ }
2187
+ /** The caller's sessions running or parked on a decision, optionally narrowed by `q` and `limit`. */
2188
+ async function readActiveSessions(client, query = {}) {
2189
+ return (await Promise.all(["running", "parked"].map((phase) => client.agentSessions.list({
2190
+ ...query,
2191
+ phase
2192
+ })))).flat();
2193
+ }
2194
+ /** `--harness`: a catalogue id, or `standards` (the built-in agent), which sends none. */
2195
+ function parseHarness(value) {
2196
+ return value === void 0 ? void 0 : parseHarnessIds([value])?.[0];
2197
+ }
2198
+ function isProviderName(value) {
2199
+ return PROVIDER_NAMES.includes(value);
2200
+ }
2201
+ /** `--model`: a model of the harness's catalogue, or `<provider>/<model>` without a harness. */
2202
+ function parseModel(harness, value) {
2203
+ if (harness) return {
2204
+ provider: environmentHarness(harness).provider,
2205
+ model: value
2206
+ };
2207
+ const slash = value.indexOf("/");
2208
+ const provider = slash > 0 ? value.slice(0, slash) : "";
2209
+ const model = value.slice(slash + 1);
2210
+ if (!(isProviderName(provider) && model)) throw new ValidationError(`--model takes <provider>/<model> without --harness (providers: ${PROVIDER_NAMES.join(", ")}): got "${value}"`, []);
2211
+ return {
2212
+ provider,
2213
+ model
2214
+ };
2215
+ }
2216
+ /** A shell word: the task as `sessions send` would take it back. */
2217
+ function shellQuote(value) {
2218
+ return `'${value.replace(/'/g, `'\\''`)}'`;
2219
+ }
2220
+ function write(text) {
2221
+ process.stdout.write(text);
2222
+ }
2223
+ /** The cursor a follow opens after: the newest page's `committedSeq`, read before sending. */
2224
+ async function cursorBeforeSend(client, id) {
2225
+ return (await readHistoryPage(client, id, 1)).committedSeq;
2226
+ }
2227
+ /** Sends `content` with a fresh `clientMessageId`, the id its turn is recognised by. */
2228
+ async function sendMessage(client, id, content) {
2229
+ const clientMessageId = randomUUID();
2230
+ await client.agentMessages.send({
2231
+ id,
2232
+ content,
2233
+ clientMessageId
2234
+ });
2235
+ return clientMessageId;
2236
+ }
2237
+ /** After a send: `{ id, url }` when detached, else the turn followed to its end. */
2238
+ async function followOrDetach(cmd, client, id, sent, detach) {
2239
+ const url = conversationUrl(cmd, id);
2240
+ if (detach) {
2241
+ formatOutput({
2242
+ id,
2243
+ url
2244
+ }, getFormat(cmd));
2245
+ return;
2246
+ }
2247
+ process.stderr.write(`session ${id} ${url}\n`);
2248
+ const code = await followTurn(client, id, {
2249
+ ...sent,
2250
+ human: isHumanOutput(cmd),
2251
+ write
2252
+ });
2253
+ if (code !== 0) process.exitCode = code;
2254
+ }
2255
+ /** The last assistant text of a history page, or `null`. */
2256
+ function lastReplyOf(page) {
2257
+ for (const message of [...page.messages].reverse()) {
2258
+ if (message.role !== "assistant") continue;
2259
+ const text = message.parts.flatMap((part) => part.type === "text" ? [part.text] : []).join("");
2260
+ if (text) return text;
2261
+ }
2262
+ return null;
2263
+ }
2264
+ function hasWaitingMessage(page) {
2265
+ return page.messages.some((message) => message.role === "user" && message.waiting === true);
2266
+ }
2267
+ /**
2268
+ * `sessions follow`: the running turn, or the next one to start; an idle
2269
+ * session with nothing waiting, or a parked one with its decisions open,
2270
+ * answers at once. The page is read before the session, so a turn that ends
2271
+ * between the two reads still has its end after the cursor.
2272
+ */
2273
+ async function followSession(cmd, client, id) {
2274
+ const human = isHumanOutput(cmd);
2275
+ const page = await readHistoryPage(client, id, 10);
2276
+ const { state } = await client.agentSessions.get({ id });
2277
+ const follow = (turnId) => followTurn(client, id, {
2278
+ cursor: page.committedSeq,
2279
+ ...turnId && { turnId },
2280
+ human,
2281
+ write
2282
+ });
2283
+ if (state.phase === "running") return follow(state.runningTurnId);
2284
+ if (state.phase === "parked") {
2285
+ const decisions = await waitForDecisions(client, id, 1);
2286
+ return decisions.length > 0 ? reportDecisions(decisions, {
2287
+ human,
2288
+ write
2289
+ }) : follow();
2290
+ }
2291
+ if (hasWaitingMessage(page)) return follow();
2292
+ const outcome = state.outcome ?? "completed";
2293
+ if (human) process.stderr.write(`session ${id} is idle (${outcome})\n`);
2294
+ else write(`${JSON.stringify({
2295
+ type: "end",
2296
+ outcome
2297
+ })}\n`);
2298
+ return outcome === "completed" ? TURN_EXIT.completed : TURN_EXIT.failed;
2299
+ }
2300
+ function isNotFound(error) {
2301
+ return error instanceof StandardsRequestError$1 && error.status === 404;
2302
+ }
2303
+ function registerSessionsCommand(program) {
2304
+ const sessions = program.command("sessions").description("Start, follow and steer agent sessions (exit 0 done, 1 failed, 3 decision)");
2305
+ sessions.command("start").description("Start a session on a task and follow its first turn").argument("<task>", "what the agent should do").option("--harness <id>", "claude-code, codex, cursor, or standards (the built-in agent)").option("--env <env>", "the environment it runs in (name or id)").addOption(new Option("--size <size>", "the machine size of its turns").choices(ENVIRONMENT_SIZES)).option("--model <id>", "a model of the harness (or <provider>/<model> without --harness)").option("--name <name>", "the conversation's title").option("--detach", "send and return { id, url } without following").action(async (task, opts, cmd) => {
2306
+ const client = getAppClientFromCommand(cmd);
2307
+ const harness = parseHarness(opts.harness);
2308
+ const model = opts.model === void 0 ? void 0 : parseModel(harness, opts.model);
2309
+ const environmentId = opts.env ? await resolveEnvironmentId(client, opts.env) : void 0;
2310
+ const id = randomUUID();
2311
+ await client.agentSessions.create({
2312
+ mode: "interactive",
2313
+ id,
2314
+ ...harness && { harness },
2315
+ ...environmentId && { environmentId },
2316
+ ...opts.size && { size: opts.size },
2317
+ ...model && { inferenceOverrides: { model } },
2318
+ ...opts.name && { name: opts.name }
2319
+ });
2320
+ let clientMessageId;
2321
+ try {
2322
+ clientMessageId = await sendMessage(client, id, task);
2323
+ } catch (error) {
2324
+ process.stderr.write(`${JSON.stringify({
2325
+ id,
2326
+ url: conversationUrl(cmd, id)
2327
+ })}\n`);
2328
+ process.stderr.write(`Session created, message not sent: standards sessions send ${id} ${shellQuote(task)}\n`);
2329
+ throw error;
2330
+ }
2331
+ await followOrDetach(cmd, client, id, {
2332
+ cursor: 0,
2333
+ clientMessageId
2334
+ }, opts.detach);
2335
+ });
2336
+ sessions.command("send").description("Send a message to a session and follow the turn it starts").argument("<id>", "session id").argument("<message>", "the message").option("--detach", "send and return { id, url } without following").action(async (id, message, opts, cmd) => {
2337
+ const client = getAppClientFromCommand(cmd);
2338
+ await followOrDetach(cmd, client, id, {
2339
+ cursor: await cursorBeforeSend(client, id),
2340
+ clientMessageId: await sendMessage(client, id, message)
2341
+ }, opts.detach);
2342
+ });
2343
+ sessions.command("follow").description("Follow the running or next turn; returns at once when idle or waiting on a decision").argument("<id>", "session id").action(async (id, _opts, cmd) => {
2344
+ const code = await followSession(cmd, getAppClientFromCommand(cmd), id);
2345
+ if (code !== 0) process.exitCode = code;
2346
+ });
2347
+ sessions.command("list").description("List your sessions").option("--running", "only sessions running or parked on a decision").option("--q <text>", "match the title").option("--limit <n>", "at most n sessions (per phase with --running)").action(async (opts, cmd) => {
2348
+ const client = getAppClientFromCommand(cmd);
2349
+ const query = {
2350
+ ...opts.q && { q: opts.q },
2351
+ ...opts.limit !== void 0 && { limit: parseCount("--limit", opts.limit) }
2352
+ };
2353
+ formatOutput((opts.running ? await readActiveSessions(client, query) : await client.agentSessions.list(query)).map(summarizeSession), getFormat(cmd));
2354
+ });
2355
+ sessions.command("get").description("Show a session: phase, last reply, machine, links, open decisions").argument("<id>", "session id").action(async (id, _opts, cmd) => {
2356
+ const client = getAppClientFromCommand(cmd);
2357
+ const [session, page, machine, decisions] = await Promise.all([
2358
+ client.agentSessions.get({ id }),
2359
+ readHistoryPage(client, id),
2360
+ client.machines.forSession({ id }).catch((error) => {
2361
+ if (isNotFound(error)) return null;
2362
+ throw error;
2363
+ }),
2364
+ readOpenDecisions(client, `session:${id}`)
2365
+ ]);
2366
+ formatOutput({
2367
+ session,
2368
+ phase: session.state.phase,
2369
+ lastReply: lastReplyOf(page),
2370
+ waiting: hasWaitingMessage(page),
2371
+ machine,
2372
+ links: machine?.links ?? [],
2373
+ decisions
2374
+ }, getFormat(cmd));
2375
+ });
2376
+ sessions.command("messages").description("The last messages of a session").argument("<id>", "session id").requiredOption("--last <n>", "how many, newest last (at most 100)").action(async (id, opts, cmd) => {
2377
+ const last = parseCount("--last", opts.last);
2378
+ if (last < 1) throw new ValidationError("--last must be at least 1", []);
2379
+ formatOutput((await readHistoryPage(getAppClientFromCommand(cmd), id, last)).messages, getFormat(cmd));
2380
+ });
2381
+ sessions.command("cancel").description("Stop the running turn, or a parked session's waits").argument("<id>", "session id").action(async (id, _opts, cmd) => {
2382
+ const client = getAppClientFromCommand(cmd);
2383
+ const { state } = await client.agentSessions.get({ id });
2384
+ const turnId = state.runningTurnId ?? state.lastTurnId;
2385
+ formatOutput(await client.agentSessions.cancel({
2386
+ id,
2387
+ ...turnId && { turnId }
2388
+ }), getFormat(cmd));
2389
+ });
2390
+ sessions.command("archive").description("Archive a session").argument("<id>", "session id").action(async (id, _opts, cmd) => {
2391
+ await getAppClientFromCommand(cmd).agentSessions.archive({ id });
2392
+ printDone(`Session ${id} archived.`);
1174
2393
  });
1175
2394
  }
1176
2395
  // src/commands/skills.ts
@@ -1310,17 +2529,13 @@ function catalogRow(result) {
1310
2529
  installs
1311
2530
  };
1312
2531
  }
1313
- /** `standards skills search`: our index, or skills.sh with `community`. */
2532
+ /**
2533
+ * `standards skills search`: with a query, our index or skills.sh with
2534
+ * `community`; with none, skills.sh's all-time leaderboard, or the last 24
2535
+ * hours with `trending`, keeping each row's weekly installs.
2536
+ */
1314
2537
  async function runSkillsSearch(client, query, options = {}) {
1315
- return (await client.skillCatalog.search({
1316
- query,
1317
- scope: options.community === true ? "community" : "official",
1318
- limit: searchLimit(options.limit)
1319
- })).map(catalogRow);
1320
- }
1321
- /** `standards skills top`: skills.sh's all-time leaderboard, or the last 24 hours with `trending`. */
1322
- async function runSkillsTop(client, options = {}) {
1323
- return (await client.skillCatalog.search({
2538
+ if (!query) return (await client.skillCatalog.search({
1324
2539
  query: "",
1325
2540
  scope: options.trending === true ? "trending" : "top",
1326
2541
  limit: searchLimit(options.limit)
@@ -1328,8 +2543,13 @@ async function runSkillsTop(client, options = {}) {
1328
2543
  ...catalogRow(result),
1329
2544
  weeklyInstalls: result.weeklyInstalls
1330
2545
  }));
2546
+ return (await client.skillCatalog.search({
2547
+ query,
2548
+ scope: options.community === true ? "community" : "official",
2549
+ limit: searchLimit(options.limit)
2550
+ })).map(catalogRow);
1331
2551
  }
1332
- /** `standards skills preview`: what a source would install, the source checked here first. */
2552
+ /** `standards skills add --dry-run`: what a source would install, the source checked here first. */
1333
2553
  async function runSkillsPreview(client, source) {
1334
2554
  parseSkillSource(source);
1335
2555
  return await client.skillCatalog.preview({ source });
@@ -1405,17 +2625,15 @@ function registerSkillsCommand(program) {
1405
2625
  for (const warning of outcome.result.warnings) console.warn(styleText("yellow", `! ${warning}`));
1406
2626
  formatOutput(outcome.result, getFormat(cmd));
1407
2627
  });
1408
- skills.command("search").description("Find skills to add: the official catalogue, or skills.sh with --community").argument("<query>", "what the skill should do").option("--community", "search skills.sh instead of the official catalogue").option("--limit <n>", "max results to return (up to 50)").action(async (query, opts, cmd) => {
2628
+ skills.command("search").description("Find skills to add: the official catalogue, skills.sh with --community, or with no query skills.sh's leaderboard").argument("[query]", "what the skill should do").option("--community", "search skills.sh instead of the official catalogue").option("--trending", "with no query, rank by the last 24 hours instead of all time").option("--limit <n>", "max results to return (up to 50)").action(async (query, opts, cmd) => {
2629
+ if (!(query || opts.community)) throw new ValidationError("Pass a query, or \"--community\" to list skills.sh's leaderboard", []);
1409
2630
  await printAnswer(cmd, () => runSkillsSearch(getAppClientFromCommand(cmd), query, opts));
1410
2631
  });
1411
- skills.command("top").description("List skills.sh's most installed skills, or the last 24 hours with --trending").option("--trending", "rank by the last 24 hours instead of all time").option("--limit <n>", "max results to return (up to 50)").action(async (opts, cmd) => {
1412
- await printAnswer(cmd, () => runSkillsTop(getAppClientFromCommand(cmd), opts));
1413
- });
1414
- skills.command("preview").description("Show what a source would install, without installing it").argument("<source>", "owner/repo@skill or owner/repo@skill#ref").action(async (source, _opts, cmd) => {
1415
- await printAnswer(cmd, () => runSkillsPreview(getAppClientFromCommand(cmd), source));
1416
- });
1417
- skills.command("add").description("Install a skill from a GitHub repository: what its preview read lands, and an official skill then follows stable").argument("<source>", "owner/repo@skill or owner/repo@skill#ref").action(async (source, _opts, cmd) => {
1418
- await printAnswer(cmd, () => runSkillsAdd(getAppClientFromCommand(cmd), source));
2632
+ skills.command("add").description("Install a skill from a GitHub repository: what its preview read lands, and an official skill then follows stable").argument("<source>", "owner/repo@skill or owner/repo@skill#ref").option("--dry-run", "show what the source would install, without installing it").action(async (source, opts, cmd) => {
2633
+ await printAnswer(cmd, () => {
2634
+ const client = getAppClientFromCommand(cmd);
2635
+ return opts.dryRun ? runSkillsPreview(client, source) : runSkillsAdd(client, source);
2636
+ });
1419
2637
  });
1420
2638
  skills.command("update").description("Bring an installed catalogue skill to the latest version of its source").argument("<name>", "name of the installed skill").option("--discard-changes", "overwrite the edits the workspace made to the skill").action(async (name, opts, cmd) => {
1421
2639
  await printAnswer(cmd, () => runSkillsUpdate(getAppClientFromCommand(cmd), name, opts));
@@ -1424,6 +2642,261 @@ function registerSkillsCommand(program) {
1424
2642
  await printAnswer(cmd, () => runSkillsRefresh(getAppClientFromCommand(cmd)));
1425
2643
  });
1426
2644
  }
2645
+ // src/commands/status.ts
2646
+ function summarizeDecision(decision) {
2647
+ return {
2648
+ id: decision.id,
2649
+ type: decision.type,
2650
+ title: decision.title,
2651
+ sessionId: sessionIdOf(decision) ?? null
2652
+ };
2653
+ }
2654
+ function summarizeMachine(detail) {
2655
+ return {
2656
+ id: detail.machine.id,
2657
+ owner: detail.owner.label,
2658
+ pilot: detail.pilot,
2659
+ links: detail.links.map((link) => ({
2660
+ id: link.id,
2661
+ slug: link.slug,
2662
+ url: link.url
2663
+ }))
2664
+ };
2665
+ }
2666
+ /** Sessions running or parked, and open decisions: the database reads `watch` makes every poll. */
2667
+ async function readSessionsAndDecisions(client) {
2668
+ const [sessions, decisions] = await Promise.all([readActiveSessions(client), readOpenDecisions(client)]);
2669
+ return {
2670
+ sessions: sessions.map(summarizeSession),
2671
+ decisions: decisions.map(summarizeDecision)
2672
+ };
2673
+ }
2674
+ /** The running machines with their links, every page (the provider's listing, read sparingly). */
2675
+ async function readRunningMachines(client) {
2676
+ return (await readAllMachines(client)).filter((item) => item.machine.status === "running").map(summarizeMachine);
2677
+ }
2678
+ function quoted(text) {
2679
+ return JSON.stringify(text);
2680
+ }
2681
+ function sessionTransition(previous, next) {
2682
+ if (previous === void 0) return next.phase === "idle" ? [] : [`session ${next.id} → ${next.phase}`];
2683
+ if (previous.phase === next.phase) return [];
2684
+ if (next.phase === "parked") return [`session ${next.id} → parked`];
2685
+ if (next.phase === "idle" && next.outcome !== null && next.outcome !== "completed") return [`session ${next.id} ${next.outcome}`];
2686
+ return [`session ${next.id} ${previous.phase}→${next.phase}`];
2687
+ }
2688
+ /** The session lines alone: a phase change is what makes `watch` read the machines. */
2689
+ function diffSessions(previous, next) {
2690
+ const before = new Map(previous.map((session) => [session.id, session]));
2691
+ return next.flatMap((session) => sessionTransition(before.get(session.id), session));
2692
+ }
2693
+ function linksOf(machines) {
2694
+ return machines.flatMap((machine) => machine.links);
2695
+ }
2696
+ /**
2697
+ * The transitions from one snapshot to the next, one line each, in order:
2698
+ * sessions, decisions, links, machines. Equal snapshots answer `[]`.
2699
+ */
2700
+ function diffStatus(previous, next) {
2701
+ const decisionsBefore = new Set(previous.decisions.map((decision) => decision.id));
2702
+ const decisionsAfter = new Set(next.decisions.map((decision) => decision.id));
2703
+ const linksBefore = new Set(linksOf(previous.machines).map((link) => link.id));
2704
+ const machinesAfter = new Set(next.machines.map((machine) => machine.id));
2705
+ return [
2706
+ ...diffSessions(previous.sessions, next.sessions),
2707
+ ...next.decisions.filter((decision) => !decisionsBefore.has(decision.id)).map((decision) => `decision ${decision.id} opened ${quoted(decision.title)}`),
2708
+ ...previous.decisions.filter((decision) => !decisionsAfter.has(decision.id)).map((decision) => `decision ${decision.id} answered`),
2709
+ ...linksOf(next.machines).filter((link) => !linksBefore.has(link.id)).map((link) => `link ${link.slug} published ${link.url}`),
2710
+ ...previous.machines.filter((machine) => !machinesAfter.has(machine.id)).map((machine) => `machine ${machine.id} stopped`)
2711
+ ];
2712
+ }
2713
+ /** The current state as `snapshot` lines: what `watch` prints on its first poll. */
2714
+ function snapshotLines(snapshot) {
2715
+ return [
2716
+ ...snapshot.sessions.map((session) => `snapshot session ${session.id} ${session.phase}`),
2717
+ ...snapshot.decisions.map((decision) => `snapshot decision ${decision.id} ${quoted(decision.title)}`),
2718
+ ...snapshot.machines.map((machine) => `snapshot machine ${machine.id} ${quoted(machine.owner)}`),
2719
+ ...linksOf(snapshot.machines).map((link) => `snapshot link ${link.slug} ${link.url}`)
2720
+ ];
2721
+ }
2722
+ function section(title, rows) {
2723
+ return `${title} (${rows.length})\n${rows.length === 0 ? " none\n" : rows.map((row) => ` ${row}\n`).join("")}`;
2724
+ }
2725
+ function renderStatus(snapshot) {
2726
+ return [
2727
+ section("Sessions", snapshot.sessions.map((session) => `${session.id} ${session.phase} ${session.name ?? ""}`)),
2728
+ section("Decisions", snapshot.decisions.map((decision) => `${decision.id} ${decision.type} ${decision.title}`)),
2729
+ section("Machines", snapshot.machines.map((machine) => [`${machine.id} ${machine.owner}`, ...machine.links.map((link) => ` ${link.url}`)].join("\n ")))
2730
+ ].join("\n");
2731
+ }
2732
+ function registerStatusCommand(program) {
2733
+ program.command("status").description("Sessions running or parked, open decisions, running machines and their links").action(async (_opts, cmd) => {
2734
+ const client = getAppClientFromCommand(cmd);
2735
+ const [active, machines] = await Promise.all([readSessionsAndDecisions(client), readRunningMachines(client)]);
2736
+ const snapshot = {
2737
+ ...active,
2738
+ machines
2739
+ };
2740
+ if (isHumanOutput(cmd)) process.stdout.write(renderStatus(snapshot));
2741
+ else formatOutput(snapshot, "json");
2742
+ });
2743
+ }
2744
+ // src/commands/usage.ts
2745
+ const SCOPES = ["me", "workspace"];
2746
+ /** `GET /ai/usage` reads inclusive UTC days; anything else reaches the API as a 400. */
2747
+ const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/u;
2748
+ /**
2749
+ * The window's own rules (maximum span, ordering) belong to the server, which
2750
+ * owns the calendar; the CLI only rejects what it can judge on its own.
2751
+ */
2752
+ function parseDay(flag, value) {
2753
+ if (!ISO_DAY.test(value)) throw new ValidationError(`${flag} must be a YYYY-MM-DD date`, []);
2754
+ return value;
2755
+ }
2756
+ /** The current UTC month so far: its first day through today. */
2757
+ function currentMonth(now) {
2758
+ const today = now.toISOString().slice(0, 10);
2759
+ return {
2760
+ from: `${today.slice(0, 8)}01`,
2761
+ to: today
2762
+ };
2763
+ }
2764
+ /**
2765
+ * A read the caller may be refused (no `billing:read`, say) is `null` with its
2766
+ * reason on stderr; the command fails only when every read does.
2767
+ */
2768
+ function settled(name, result) {
2769
+ if (result.status === "fulfilled") return result.value;
2770
+ process.stderr.write(`${styleText("yellow", `! ${name}: ${messageOf(result.reason)}`)}\n`);
2771
+ return null;
2772
+ }
2773
+ function registerUsageCommand(program) {
2774
+ program.command("usage").description("Show AI usage over a UTC day window, the AI wallet and the quotas").option("--from <day>", "first day of the window (YYYY-MM-DD, default: first of the month)").option("--to <day>", "last day of the window, inclusive (YYYY-MM-DD, default: today)").addOption(new Option("--scope <scope>", "me or workspace").choices(SCOPES).default("me")).action(async (opts, cmd) => {
2775
+ const month = currentMonth(/* @__PURE__ */ new Date());
2776
+ const params = {
2777
+ from: parseDay("--from", opts.from ?? month.from),
2778
+ to: parseDay("--to", opts.to ?? month.to),
2779
+ scope: opts.scope
2780
+ };
2781
+ const client = getAppClientFromCommand(cmd);
2782
+ const [usage, wallet, quotas] = await Promise.allSettled([
2783
+ client.ai.usage(params),
2784
+ client.ai.wallet({}),
2785
+ client.quotas.list({})
2786
+ ]);
2787
+ if (usage.status === "rejected" && wallet.status === "rejected" && quotas.status === "rejected") throw usage.reason;
2788
+ formatOutput({
2789
+ usage: settled("usage", usage),
2790
+ wallet: settled("wallet", wallet),
2791
+ quotas: settled("quotas", quotas)
2792
+ }, getFormat(cmd));
2793
+ });
2794
+ }
2795
+ // src/commands/watch.ts
2796
+ /** The shortest interval between two polls. */
2797
+ const MIN_INTERVAL_MS = 2e3;
2798
+ /** Machines (the provider's listing) are read at most this often unless a session changed phase. */
2799
+ const MACHINES_EVERY_MS = 6e4;
2800
+ /** Consecutive failed polls after which `watch` gives up. */
2801
+ const MAX_FAILURES = 5;
2802
+ const UNIT_MS = {
2803
+ ms: 1,
2804
+ s: 1e3,
2805
+ m: 6e4
2806
+ };
2807
+ /** `10s`, `2s`, `1m`, `500ms`; under `2s` is refused. */
2808
+ function parseInterval(value) {
2809
+ return parseDuration("--interval", value, {
2810
+ units: UNIT_MS,
2811
+ example: "10s or 1m",
2812
+ limit: "at least 2s",
2813
+ min: MIN_INTERVAL_MS
2814
+ });
2815
+ }
2816
+ function sleep(ms) {
2817
+ return new Promise((resolve) => setTimeout(resolve, ms));
2818
+ }
2819
+ /**
2820
+ * The sessions that left the running and parked lists since the last poll,
2821
+ * read once more so their new phase (idle, failed) shows; one gone for good
2822
+ * (archived, deleted) is dropped.
2823
+ */
2824
+ async function readLeftSessions(client, previous, current) {
2825
+ const listed = new Set(current.map((session) => session.id));
2826
+ const left = previous.filter((session) => session.phase !== "idle" && !listed.has(session.id));
2827
+ return (await Promise.all(left.map((session) => client.agentSessions.get({ id: session.id }).then(summarizeSession, () => null)))).filter((session) => session !== null);
2828
+ }
2829
+ function isOpenedDecision(line) {
2830
+ return line.startsWith("decision ") && line.includes(" opened ");
2831
+ }
2832
+ function isSnapshotDecision(line) {
2833
+ return line.startsWith("snapshot decision ");
2834
+ }
2835
+ /** The decision lines last: the one a waiting agent acts on ends the output. */
2836
+ function decisionsLast(lines, isDecision) {
2837
+ return [...lines.filter((line) => !isDecision(line)), ...lines.filter(isDecision)];
2838
+ }
2839
+ /**
2840
+ * Polls until a decision is open (`--until decision`, 3: one already open at
2841
+ * the first poll counts) or 5 polls fail in a row (1).
2842
+ */
2843
+ async function watch(client, options) {
2844
+ const emit = (type, line) => process.stdout.write(options.human ? `${line}\n` : `${JSON.stringify({
2845
+ type,
2846
+ line
2847
+ })}\n`);
2848
+ let previous;
2849
+ let machinesReadAt = Number.NEGATIVE_INFINITY;
2850
+ let failures = 0;
2851
+ for (;;) {
2852
+ try {
2853
+ const active = await readSessionsAndDecisions(client);
2854
+ const sessions = previous ? [...active.sessions, ...await readLeftSessions(client, previous.sessions, active.sessions)] : active.sessions;
2855
+ const phaseChanged = previous === void 0 || diffSessions(previous.sessions, sessions).length > 0;
2856
+ let machines = previous?.machines ?? [];
2857
+ if (phaseChanged || Date.now() - machinesReadAt >= MACHINES_EVERY_MS) {
2858
+ machines = await readRunningMachines(client).catch((error) => {
2859
+ emit("machines-failed", `machines read failed: ${messageOf(error)}`);
2860
+ return previous?.machines ?? [];
2861
+ });
2862
+ machinesReadAt = Date.now();
2863
+ }
2864
+ const next = {
2865
+ sessions,
2866
+ decisions: active.decisions,
2867
+ machines
2868
+ };
2869
+ failures = 0;
2870
+ if (previous === void 0) {
2871
+ const waiting = options.untilDecision && next.decisions.length > 0;
2872
+ const lines = snapshotLines(next);
2873
+ for (const line of waiting ? decisionsLast(lines, isSnapshotDecision) : lines) emit("snapshot", line);
2874
+ if (waiting) return TURN_EXIT.decision;
2875
+ } else {
2876
+ const lines = diffStatus(previous, next);
2877
+ for (const line of decisionsLast(lines, isOpenedDecision)) emit("transition", line);
2878
+ if (options.untilDecision && lines.some(isOpenedDecision)) return TURN_EXIT.decision;
2879
+ }
2880
+ previous = next;
2881
+ } catch (error) {
2882
+ failures += 1;
2883
+ emit("poll-failed", `poll failed: ${messageOf(error)}`);
2884
+ if (failures >= MAX_FAILURES) return TURN_EXIT.failed;
2885
+ }
2886
+ await sleep(options.intervalMs);
2887
+ }
2888
+ }
2889
+ function registerWatchCommand(program) {
2890
+ program.command("watch").description("One line per change of sessions, decisions, machines and links (exit 3 on --until decision)").option("--interval <duration>", "time between polls, at least 2s", "10s").addOption(new Option("--until <event>", "exit 3 when this happens").choices(["decision"])).action(async (opts, cmd) => {
2891
+ const intervalMs = parseInterval(opts.interval);
2892
+ const code = await watch(getAppClientFromCommand(cmd), {
2893
+ intervalMs,
2894
+ untilDecision: opts.until === "decision",
2895
+ human: isHumanOutput(cmd)
2896
+ });
2897
+ if (code !== 0) process.exitCode = code;
2898
+ });
2899
+ }
1427
2900
  // src/version.ts
1428
2901
  /**
1429
2902
  * The installed package's version, read from its own manifest: `src/` and the
@@ -1444,13 +2917,13 @@ const PUBLIC_COMMANDS = /* @__PURE__ */ new Set([
1444
2917
  "login",
1445
2918
  "use",
1446
2919
  "instances",
1447
- "current",
1448
2920
  "logout",
1449
- "help"
2921
+ "help",
2922
+ "init",
2923
+ "guide"
1450
2924
  ]);
1451
2925
  function isPublicCommand(actionCommand) {
1452
- if (PUBLIC_COMMANDS.has(actionCommand.name())) return true;
1453
- return actionCommand.parent?.name() === "auth" && actionCommand.name() === "help";
2926
+ return PUBLIC_COMMANDS.has(actionCommand.name());
1454
2927
  }
1455
2928
  function createProgram() {
1456
2929
  const program = new Command();
@@ -1458,23 +2931,26 @@ function createProgram() {
1458
2931
  registerRootCommands(program);
1459
2932
  registerRecordsCommand(program);
1460
2933
  registerSchemaCommand(program);
1461
- registerPullCommand(program);
1462
- registerDiffCommand(program);
1463
2934
  registerDocumentsCommand(program);
1464
- registerFoldersCommand(program);
2935
+ registerFilesCommand(program);
1465
2936
  registerKeysCommand(program);
1466
2937
  registerInvitationsCommand(program);
1467
- registerAuthCommand(program);
1468
2938
  registerBundlesCommand(program);
1469
2939
  registerConnectorsCommand(program);
1470
2940
  registerMcpCommand(program);
1471
2941
  registerMeetingsCommand(program);
1472
- registerMemoryCommand(program);
1473
- registerSearchCommand(program);
1474
2942
  registerSkillsCommand(program);
1475
- registerEmbeddingsCommand(program);
1476
- registerAiCommand(program);
1477
- registerQuotasCommand(program);
2943
+ registerUsageCommand(program);
2944
+ registerEnvironmentsCommand(program);
2945
+ registerSecretsCommand(program);
2946
+ registerMachinesCommand(program);
2947
+ registerLinksCommand(program);
2948
+ registerSessionsCommand(program);
2949
+ registerDecisionsCommand(program);
2950
+ registerStatusCommand(program);
2951
+ registerWatchCommand(program);
2952
+ registerInitCommand(program);
2953
+ registerGuideCommand(program);
1478
2954
  program.hook("preAction", async (_thisCommand, actionCommand) => {
1479
2955
  const raw = program.opts();
1480
2956
  const resolved = await resolveCliConfig({