@standards/cli 0.0.0-stage → 1.0.0-alpha.384

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 ADDED
@@ -0,0 +1,2056 @@
1
+ #!/usr/bin/env node
2
+ /*! © 2025-2026 AND YOU CREATE SAS (RCS Paris 940 190 614). Proprietary — see LICENSE (Standards SDK License 1.0). */
3
+ import { createRequire } from "node:module";
4
+ import { styleText } from "node:util";
5
+ import { StandardsRequestError, createTransport } from "@standards/client";
6
+ import { Command } from "commander";
7
+ import { DEVICE_CHANNEL_WORKER_CLOSE_PROTOCOL_VIOLATION, RECORDING_OVERRIDES, RECORD_ACCESS_ROLES, RESOURCE_VISIBILITIES, SchemaError, SchemaErrorCode, ValidationError, canonicalStringify, computeReconnectDelay, computeRetryDelay, deviceChannelUrl, diffSchemaState, formatByteSize, parseRecordAccessRole, parseSchemaState, parseServerFrame, schemaStateSourcePayload } from "@standards/schema";
8
+ import Table from "cli-table3";
9
+ import { createAppClient } from "@standards/client/app";
10
+ import { homedir, hostname } from "node:os";
11
+ import { createHash, randomUUID } from "node:crypto";
12
+ import { access, chmod, mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
13
+ import { basename, dirname, isAbsolute, join } from "node:path";
14
+ import { WebSocket } from "ws";
15
+ import { spawn } from "node:child_process";
16
+ import { constants, createWriteStream } from "node:fs";
17
+ import { StringDecoder } from "node:string_decoder";
18
+ import { Readable } from "node:stream";
19
+ import { pipeline } from "node:stream/promises";
20
+ import { buildOperationPath, recordsOperations } from "@standards/schema/api";
21
+ import { stdin, stdout } from "node:process";
22
+ import { createInterface } from "node:readline/promises";
23
+ //#region src/output.ts
24
+ const MAX_CELL_WIDTH = 50;
25
+ function truncate(value, maxLength) {
26
+ if (value.length <= maxLength) return value;
27
+ return `${value.slice(0, maxLength - 1)}…`;
28
+ }
29
+ function escapeCsvValue(value) {
30
+ if (value === null || value === void 0) return "";
31
+ const str = typeof value === "object" ? JSON.stringify(value) : String(value);
32
+ if (str.includes(",") || str.includes("\"") || str.includes("\n")) return `"${str.replace(/"/g, "\"\"")}"`;
33
+ return str;
34
+ }
35
+ function formatJson(data) {
36
+ return JSON.stringify(data, null, 2);
37
+ }
38
+ function formatCsv(items) {
39
+ if (items.length === 0) return "";
40
+ const headers = Object.keys(items[0]);
41
+ return [headers.join(","), ...items.map((item) => headers.map((h) => escapeCsvValue(item[h])).join(","))].join("\n");
42
+ }
43
+ function formatTable(items) {
44
+ if (items.length === 0) return "No results.";
45
+ const headers = Object.keys(items[0]);
46
+ const table = new Table({
47
+ head: headers.map((h) => styleText("bold", h)),
48
+ style: {
49
+ head: [],
50
+ border: []
51
+ }
52
+ });
53
+ for (const item of items) {
54
+ const row = headers.map((h) => {
55
+ const val = item[h];
56
+ if (val === null || val === void 0) return "";
57
+ return truncate(typeof val === "object" ? JSON.stringify(val) : String(val), MAX_CELL_WIDTH);
58
+ });
59
+ table.push(row);
60
+ }
61
+ return table.toString();
62
+ }
63
+ function formatOutput(data, format) {
64
+ if (format === "json") {
65
+ process.stdout.write(`${formatJson(data)}\n`);
66
+ return;
67
+ }
68
+ const items = Array.isArray(data) ? data : [data];
69
+ if (format === "csv") {
70
+ process.stdout.write(`${formatCsv(items)}\n`);
71
+ return;
72
+ }
73
+ process.stdout.write(`${formatTable(items)}\n`);
74
+ }
75
+ //#endregion
76
+ //#region src/client.ts
77
+ /** The CLI's transport: the profile's bearer key and tenant header over the shared HTTP transport. */
78
+ function createClient(config) {
79
+ return createTransport({
80
+ baseUrl: config.apiUrl,
81
+ apiKey: config.apiKey,
82
+ tenantId: config.tenantId
83
+ });
84
+ }
85
+ //#endregion
86
+ //#region src/commands/common.ts
87
+ function getGlobalOptions(cmd) {
88
+ return cmd.optsWithGlobals();
89
+ }
90
+ function getFormat(cmd) {
91
+ return getGlobalOptions(cmd).format;
92
+ }
93
+ /** Parse a comma-separated flag value into a trimmed, non-empty list. */
94
+ function parseCsvList(value) {
95
+ return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
96
+ }
97
+ function getClientFromCommand(cmd) {
98
+ const root = getGlobalOptions(cmd);
99
+ if (!(root.apiUrl && root.apiKey)) throw new Error("No Standards instance configured. Run \"standards login\" or pass --api-key.");
100
+ return createClient({
101
+ apiUrl: root.apiUrl,
102
+ apiKey: root.apiKey,
103
+ tenantId: root.tenant
104
+ });
105
+ }
106
+ /** The operation-contract client over the same transport, for the routes the contract covers. */
107
+ function getAppClientFromCommand(cmd) {
108
+ return createAppClient(getClientFromCommand(cmd));
109
+ }
110
+ /**
111
+ * The `--tenant` guard every admin command shares. The API takes the tenant in
112
+ * the body as a confirmation of intent, never as a target selector: it refuses
113
+ * any value other than the authenticated tenant. The CLI cannot invent it, so
114
+ * it asks for it rather than guessing the primary tenant.
115
+ *
116
+ * `message` is passed whole rather than composed: each command's exact wording
117
+ * is what its users (and its tests) read.
118
+ */
119
+ function requireTenant(cmd, message) {
120
+ const { tenant } = getGlobalOptions(cmd);
121
+ if (!tenant) throw new Error(message);
122
+ return tenant;
123
+ }
124
+ /**
125
+ * The `--yes` guard for a destructive or outbound command: shared rather than
126
+ * inlined so the refusal is a typed `ValidationError`, not a raw `Error`. The
127
+ * commands that still inline their own check belong here when they move.
128
+ *
129
+ * Like `requireTenant`, `message` is passed whole rather than composed: each
130
+ * command's exact wording is what its users (and its tests) read.
131
+ */
132
+ function requireConfirmation(yes, message) {
133
+ if (!yes) throw new ValidationError(message, []);
134
+ }
135
+ /** Renders any caught value as a message string — shared by every command's top-level `catch`. */
136
+ function messageOf(error) {
137
+ return error instanceof Error ? error.message : String(error);
138
+ }
139
+ //#endregion
140
+ //#region src/commands/ai.ts
141
+ const SCOPES = ["me", "workspace"];
142
+ /** `GET /ai/usage` reads inclusive UTC days; anything else reaches the API as a 400. */
143
+ const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/u;
144
+ function parseDay(flag, value) {
145
+ if (!ISO_DAY.test(value)) throw new ValidationError(`${flag} must be a YYYY-MM-DD date`, []);
146
+ return value;
147
+ }
148
+ /**
149
+ * The window's own rules (maximum span, ordering) belong to the server, which
150
+ * owns the calendar; the CLI only rejects what it can judge on its own.
151
+ */
152
+ function parseScope(scope) {
153
+ if (SCOPES.includes(scope)) return scope;
154
+ throw new ValidationError(`--scope must be one of: ${SCOPES.join(", ")}`, []);
155
+ }
156
+ function registerAiCommand(program) {
157
+ const ai = program.command("ai").description("Inspect AI usage and credit state");
158
+ 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) => {
159
+ const params = {
160
+ from: parseDay("--from", opts.from),
161
+ to: parseDay("--to", opts.to),
162
+ scope: parseScope(opts.scope ?? "me")
163
+ };
164
+ formatOutput(await getAppClientFromCommand(cmd).ai.usage(params), getFormat(cmd));
165
+ });
166
+ ai.command("wallet").description("Show the tenant's billing mode and AI credit state").action(async (_opts, cmd) => {
167
+ formatOutput(await getAppClientFromCommand(cmd).ai.wallet({}), getFormat(cmd));
168
+ });
169
+ }
170
+ //#endregion
171
+ //#region src/commands/auth.ts
172
+ function registerAuthCommand(program) {
173
+ program.command("auth").description("Inspect CLI authentication").command("whoami").description("Show current configuration and validate API key").action(async (_opts, cmd) => {
174
+ const root = getGlobalOptions(cmd);
175
+ const client = getAppClientFromCommand(cmd);
176
+ const write = (msg) => process.stdout.write(`${msg}\n`);
177
+ write(styleText("bold", "Standards CLI Configuration"));
178
+ write(` API URL: ${root.apiUrl}`);
179
+ write(` API Key: ${root.apiKey ? `${root.apiKey.slice(0, 8)}...` : styleText("red", "not set")}`);
180
+ write("");
181
+ const keys = await client.apiKeys.list({});
182
+ write(styleText("green", "✓ API key is valid"));
183
+ if (Array.isArray(keys) && keys.length > 0) write(` Active keys: ${keys.filter((key) => !key.revokedAt).length}`);
184
+ });
185
+ }
186
+ //#endregion
187
+ //#region src/commands/bundles.ts
188
+ /**
189
+ * System bundles (admin / workspace:manage). A bundle is a code-defined,
190
+ * versioned set of schema that can be assigned to — and updated on — a tenant.
191
+ */
192
+ function registerBundlesCommand(program) {
193
+ const bundles = program.command("bundles").description("Manage system bundles assigned to the current tenant (admin)");
194
+ bundles.command("list").description("List the bundles actively assigned to the current tenant").action(async (_opts, cmd) => {
195
+ formatOutput(await getAppClientFromCommand(cmd).bundles.listAssigned({}), getFormat(cmd));
196
+ });
197
+ bundles.command("updatable").description("List assigned bundles that have a newer version available").action(async (_opts, cmd) => {
198
+ formatOutput(await getAppClientFromCommand(cmd).bundles.listUpdatable({}), getFormat(cmd));
199
+ });
200
+ bundles.command("assign").description("Assign (install) a bundle to the current tenant").argument("<bundleId>", "bundle ID to assign").action(async (bundleId, _opts, cmd) => {
201
+ formatOutput(await getAppClientFromCommand(cmd).bundles.assign({ bundleId }), getFormat(cmd));
202
+ });
203
+ bundles.command("uninstall").description("Uninstall a bundle: deactivate it and demote its now-orphaned objects").argument("<bundleId>", "bundle ID to uninstall").option("--yes", "uninstall without confirmation").action(async (bundleId, opts, cmd) => {
204
+ if (!opts.yes) throw new Error("Uninstalling a bundle is explicit. Re-run with \"--yes\" to confirm.");
205
+ formatOutput(await getAppClientFromCommand(cmd).bundles.uninstall({ bundleId }), getFormat(cmd));
206
+ });
207
+ bundles.command("update").description("Apply assigned bundles — picks up newer bundle versions (additive sync)").action(async (_opts, cmd) => {
208
+ formatOutput(await getAppClientFromCommand(cmd).bundles.update({}), getFormat(cmd));
209
+ });
210
+ }
211
+ //#endregion
212
+ //#region src/commands/codemod.ts
213
+ /** Rewrites consumer code for a breaking SDK change. */
214
+ function registerCodemodCommand(program) {
215
+ const codemod = program.command("codemod").description("Rewrite code for a breaking SDK change");
216
+ codemod.command("builder-shorthand").description("Rewrite text({ name, label }) builders to text(name, label)").argument("[paths...]", "files or directories to rewrite", ["."]).option("--dry-run", "report without writing").option("--relative-imports", "also treat relative imports as Standards builders — for the Standards monorepo itself").action(async (paths, opts) => {
217
+ const { runBuilderShorthand } = await import("./builder-shorthand-DPVmGTec.mjs");
218
+ const report = await runBuilderShorthand(paths, {
219
+ write: !opts.dryRun,
220
+ relativeImports: opts.relativeImports === true
221
+ });
222
+ const verb = opts.dryRun ? "Would rewrite" : "Rewrote";
223
+ process.stdout.write(`${verb} ${report.changed} calls in ${report.files} files.\n`);
224
+ for (const call of report.skipped) process.stdout.write(`${call.file}:${call.line}:${call.column} ${call.callee} ${call.reason}\n`);
225
+ if (report.skipped.length > 0) {
226
+ process.stdout.write(`${report.skipped.length} calls need a manual rewrite to fn(name, label), then re-run "standards codemod builder-shorthand".\n`);
227
+ process.exitCode = 1;
228
+ }
229
+ });
230
+ codemod.command("palette").description("Rewrite old colour ids (blue, green, ...) to the Standards palette").argument("[paths...]", "files or directories to rewrite", ["."]).option("--dry-run", "report without writing").action(async (paths, opts) => {
231
+ const { runPalette } = await import("./palette-D63ag-eX.mjs");
232
+ const report = await runPalette(paths, { write: !opts.dryRun });
233
+ const verb = opts.dryRun ? "Would rewrite" : "Rewrote";
234
+ process.stdout.write(`${verb} ${report.changed} colours in ${report.files} files.\n`);
235
+ if (opts.dryRun) for (const entry of report.rewritten) process.stdout.write(` ${entry.file}: ${entry.changed}\n`);
236
+ for (const failure of report.failures) process.stderr.write(`${failure.message}\n`);
237
+ if (report.failures.length > 0) process.exitCode = 1;
238
+ });
239
+ codemod.command("image").description("Rewrite .imageShape(x) to .image({ shape: x }) and appearance.imageFileId to appearance.image?.fileId").argument("[paths...]", "files or directories to rewrite", ["."]).option("--dry-run", "report without writing").action(async (paths, opts) => {
240
+ const { runImage } = await import("./image-Ca-XlG3r.mjs");
241
+ const report = await runImage(paths, { write: !opts.dryRun });
242
+ const verb = opts.dryRun ? "Would rewrite" : "Rewrote";
243
+ process.stdout.write(`${verb} ${report.changed} references in ${report.files} files.\n`);
244
+ for (const { file, line, column } of report.skipped) process.stdout.write(`${file}:${line}:${column} appearance.imageFileId a write, rewrite by hand\n`);
245
+ for (const failure of report.failures) process.stderr.write(`${failure.message}\n`);
246
+ if (report.skipped.length > 0 || report.failures.length > 0) process.exitCode = 1;
247
+ });
248
+ codemod.command("engine-imports").description("Move imports of names that left @standards/schema to @standards/engine").argument("[paths...]", "files or directories to rewrite", ["."]).option("--dry-run", "report without writing").option("--root <dir>", "where @standards/schema and @standards/engine are installed", ".").action(async (paths, opts) => {
249
+ const { runEngineImports } = await import("./engine-imports-K-yySYo3.mjs");
250
+ const report = await runEngineImports(paths, {
251
+ write: !opts.dryRun,
252
+ root: opts.root
253
+ });
254
+ if (typeof report === "string") {
255
+ process.stderr.write(`${report}\n`);
256
+ process.exitCode = 1;
257
+ return;
258
+ }
259
+ const verb = opts.dryRun ? "Would move" : "Moved";
260
+ process.stdout.write(`${verb} ${report.moved} imports in ${report.files} files.\n`);
261
+ for (const { file, name } of report.unresolved) process.stdout.write(`${file} ${name} not moved, check by hand\n`);
262
+ for (const failure of report.failures) process.stderr.write(`${failure.message}\n`);
263
+ if (report.failures.length > 0) process.exitCode = 1;
264
+ });
265
+ codemod.command("scopes").description("Move every @stndrds/* package to @standards/* and point @standards at the licensed registry").argument("[paths...]", "files or directories to rewrite", ["."]).option("--dry-run", "report without writing").option("--root <dir>", "where the project's .npmrc lives", ".").action(async (paths, opts) => {
266
+ const { runScopes } = await import("./scopes-Bqw1v3wB.mjs");
267
+ const report = await runScopes(paths, {
268
+ write: !opts.dryRun,
269
+ root: opts.root
270
+ });
271
+ const verb = opts.dryRun ? "Would rewrite" : "Rewrote";
272
+ process.stdout.write(`${verb} ${report.changed} references in ${report.files} files.\n`);
273
+ if (report.npmrc !== void 0) {
274
+ const action = opts.dryRun ? "Would point" : "Pointed";
275
+ process.stdout.write(`${action} @standards at the licensed registry in ${report.npmrc}.\n`);
276
+ }
277
+ if (!report.licensed) {
278
+ process.stdout.write("Then reinstall to refresh the lockfile.\n");
279
+ return;
280
+ }
281
+ process.stdout.write([
282
+ "Your licence key goes in your user ~/.npmrc, never the project one (pnpm ignores it there):",
283
+ " npm config set \"//registry.standards.run/:_authToken\" \"$STANDARDS_TOKEN\"",
284
+ "In CI, add it to $HOME/.npmrc before installing, with STANDARDS_TOKEN set from a secret:",
285
+ ` echo '//registry.standards.run/:_authToken=\${STANDARDS_TOKEN}' >> "$HOME/.npmrc"`,
286
+ "Then reinstall to refresh the lockfile.",
287
+ ""
288
+ ].join("\n"));
289
+ });
290
+ }
291
+ //#endregion
292
+ //#region src/commands/connectors.ts
293
+ const PROVIDERS = Object.keys({
294
+ github: true,
295
+ slack: true,
296
+ gmail: true,
297
+ outlook: true,
298
+ "google-calendar": true,
299
+ "outlook-calendar": true
300
+ });
301
+ const CAPABILITIES = Object.keys({
302
+ modify: true,
303
+ send: true
304
+ });
305
+ /** Parse a comma-separated `--capabilities` flag; unknown tiers are refused before the request. */
306
+ function parseCapabilities(raw) {
307
+ if (raw === void 0) return void 0;
308
+ const requested = raw.split(",").map((c) => c.trim()).filter((c) => c.length > 0);
309
+ if (requested.filter((c) => !CAPABILITIES.includes(c)).length > 0) throw new ValidationError(`--capabilities must be a comma-separated list of: ${CAPABILITIES.join(", ")}`, []);
310
+ return requested;
311
+ }
312
+ /** Coerce a free-form scope flag to the two values the API accepts. */
313
+ function resolveScope(scope) {
314
+ return scope === "actor" ? "actor" : "tenant";
315
+ }
316
+ function registerConnectorsCommand(program) {
317
+ const connectors = program.command("connectors").description("Manage connectors (OAuth connections)");
318
+ connectors.command("providers").description("List the connector providers this deployment holds credentials for").action(async (_opts, cmd) => {
319
+ formatOutput(await getAppClientFromCommand(cmd).connectors.providers({}), getFormat(cmd));
320
+ });
321
+ connectors.command("list").description("List connector connections").option("--scope <scope>", "connection scope (tenant or actor)", "tenant").action(async (opts, cmd) => {
322
+ formatOutput(await getAppClientFromCommand(cmd).connectors.list({ scope: resolveScope(opts.scope) }), getFormat(cmd));
323
+ });
324
+ connectors.command("connect").description("Start an OAuth flow and return the authorization URL to open").requiredOption("--provider <provider>", `connector provider (${PROVIDERS.join(", ")}); run "connectors providers" for the ones this deployment configured`).option("--scope <scope>", "connection scope (tenant or actor)", "tenant").option("--capabilities <list>", `comma-separated capability tiers beyond read (${CAPABILITIES.join(", ")})`).action(async (opts, cmd) => {
325
+ if (!PROVIDERS.includes(opts.provider)) throw new ValidationError(`--provider must be one of: ${PROVIDERS.join(", ")}`, []);
326
+ const capabilities = parseCapabilities(opts.capabilities);
327
+ formatOutput(await getAppClientFromCommand(cmd).connectors.startAuth({
328
+ provider: opts.provider,
329
+ scope: resolveScope(opts.scope),
330
+ ...capabilities ? { capabilities } : {}
331
+ }), getFormat(cmd));
332
+ });
333
+ connectors.command("disconnect").description("Disconnect (delete) a connector connection").argument("<id>", "connection ID").option("--yes", "disconnect without confirmation").option("--remove-imported", "also remove what a mail or calendar connection imported (kept by default)").action(async (id, opts, cmd) => {
334
+ if (!opts.yes) throw new Error("Disconnecting a connector is explicit. Re-run with \"--yes\" to confirm.");
335
+ await getAppClientFromCommand(cmd).connectors.delete({
336
+ id,
337
+ ...opts.removeImported ? { removeImported: "true" } : {}
338
+ });
339
+ process.stdout.write(`${styleText("green", "✓")} Connector connection ${id} disconnected.\n`);
340
+ });
341
+ }
342
+ //#endregion
343
+ //#region src/config.ts
344
+ const DEFAULT_API_URL = "http://localhost:4100/v1";
345
+ const ENV_PROFILE_NAME = "sandbox";
346
+ function getDefaultApiUrl() {
347
+ return DEFAULT_API_URL;
348
+ }
349
+ function getConfigPath() {
350
+ const configDir = process.env.STANDARDS_CONFIG_DIR ?? join(homedir(), ".standards");
351
+ return join(configDir, "config.json");
352
+ }
353
+ async function readConfig() {
354
+ try {
355
+ const raw = await readFile(getConfigPath(), "utf8");
356
+ const parsed = JSON.parse(raw);
357
+ return {
358
+ ...parsed.installationId ? { installationId: parsed.installationId } : {},
359
+ currentProfile: parsed.currentProfile,
360
+ profiles: parsed.profiles ?? {}
361
+ };
362
+ } catch (error) {
363
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return { profiles: {} };
364
+ throw error;
365
+ }
366
+ }
367
+ async function writeConfig(config) {
368
+ const path = getConfigPath();
369
+ const directory = dirname(path);
370
+ await mkdir(directory, {
371
+ recursive: true,
372
+ mode: 448
373
+ });
374
+ await chmod(directory, 448);
375
+ await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
376
+ await chmod(path, 384);
377
+ }
378
+ function normalizeApiOrigin(apiUrl) {
379
+ try {
380
+ return new URL(apiUrl).origin;
381
+ } catch {
382
+ return apiUrl.replace(/\/+$/, "");
383
+ }
384
+ }
385
+ async function upsertProfile(input) {
386
+ const config = await readConfig();
387
+ const existingProfile = config.profiles[input.name];
388
+ const device = existingProfile?.device && normalizeApiOrigin(existingProfile.apiUrl) === normalizeApiOrigin(input.apiUrl) ? existingProfile.device : void 0;
389
+ config.profiles[input.name] = {
390
+ apiUrl: input.apiUrl,
391
+ apiKey: input.apiKey,
392
+ ...device ? { device } : {}
393
+ };
394
+ config.currentProfile = input.name;
395
+ await writeConfig(config);
396
+ }
397
+ async function getOrCreateInstallationId() {
398
+ const config = await readConfig();
399
+ if (config.installationId) return config.installationId;
400
+ const installationId = randomUUID();
401
+ config.installationId = installationId;
402
+ await writeConfig(config);
403
+ return installationId;
404
+ }
405
+ async function getStoredDeviceCredential(profileName) {
406
+ return (await readConfig()).profiles[profileName]?.device ?? null;
407
+ }
408
+ async function storeDeviceCredential(profileName, credential) {
409
+ const config = await readConfig();
410
+ const profile = config.profiles[profileName];
411
+ if (!profile) return false;
412
+ profile.device = credential;
413
+ await writeConfig(config);
414
+ return true;
415
+ }
416
+ async function removeDeviceCredential(profileName) {
417
+ const config = await readConfig();
418
+ const profile = config.profiles[profileName];
419
+ if (!profile?.device) return false;
420
+ profile.device = void 0;
421
+ await writeConfig(config);
422
+ return true;
423
+ }
424
+ async function setCurrentProfile(name) {
425
+ const config = await readConfig();
426
+ if (!config.profiles[name]) throw new Error(`Unknown Standards instance "${name}"`);
427
+ config.currentProfile = name;
428
+ await writeConfig(config);
429
+ }
430
+ async function removeProfile(name) {
431
+ const config = await readConfig();
432
+ const profileName = name ?? config.currentProfile;
433
+ if (!profileName) return;
434
+ delete config.profiles[profileName];
435
+ if (config.currentProfile === profileName) config.currentProfile = void 0;
436
+ await writeConfig(config);
437
+ }
438
+ async function listProfiles() {
439
+ const config = await readConfig();
440
+ const profiles = Object.entries(config.profiles).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({
441
+ name,
442
+ apiUrl: profile.apiUrl,
443
+ current: config.currentProfile === name
444
+ }));
445
+ const envProfile = getEnvProfile();
446
+ if (envProfile) profiles.push({
447
+ name: ENV_PROFILE_NAME,
448
+ apiUrl: envProfile.apiUrl,
449
+ current: !config.currentProfile,
450
+ source: "env"
451
+ });
452
+ return profiles;
453
+ }
454
+ async function getActiveProfile() {
455
+ const config = await readConfig();
456
+ if (!config.currentProfile) {
457
+ const envProfile = getEnvProfile();
458
+ return envProfile ? {
459
+ name: ENV_PROFILE_NAME,
460
+ ...envProfile
461
+ } : null;
462
+ }
463
+ const profile = config.profiles[config.currentProfile];
464
+ if (!profile) return null;
465
+ return {
466
+ name: config.currentProfile,
467
+ ...profile
468
+ };
469
+ }
470
+ function getEnvProfile() {
471
+ if (!process.env.STANDARDS_API_KEY) return null;
472
+ return {
473
+ apiUrl: process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
474
+ apiKey: process.env.STANDARDS_API_KEY
475
+ };
476
+ }
477
+ async function resolveCliConfig(options) {
478
+ if (options.apiUrl || options.apiKey) return {
479
+ apiUrl: options.apiUrl ?? process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
480
+ apiKey: options.apiKey ?? process.env.STANDARDS_API_KEY
481
+ };
482
+ const config = await readConfig();
483
+ const profileName = options.instance ?? config.currentProfile;
484
+ if (profileName) {
485
+ const profile = config.profiles[profileName];
486
+ if (!profile) throw new Error(`Unknown Standards instance "${profileName}"`);
487
+ return {
488
+ apiUrl: profile.apiUrl,
489
+ apiKey: profile.apiKey,
490
+ profileName
491
+ };
492
+ }
493
+ return {
494
+ apiUrl: process.env.STANDARDS_API_URL ?? DEFAULT_API_URL,
495
+ apiKey: process.env.STANDARDS_API_KEY
496
+ };
497
+ }
498
+ //#endregion
499
+ //#region src/device/worker-client.ts
500
+ /** How long a connection may wait for `ready` after sending `authenticate`. */
501
+ const REQUEST_TIMEOUT_MS = 3e4;
502
+ /** The code the SERVER closes an authenticated-but-not-ready socket with. */
503
+ const SERVER_POLICY_VIOLATION_CLOSE_CODE = 1008;
504
+ const NORMAL_CLOSE_CODE = 1e3;
505
+ const BROWSER_COMMAND_REFUSED = "Shell worker cannot run browser commands";
506
+ const EMPTY_SHELL_COMMAND_REFUSED = "Shell command frame carries no command";
507
+ var DeviceWorkerRequestError = class extends SchemaError {
508
+ constructor(statusCode, message, code = SchemaErrorCode.REPOSITORY_QUERY_FAILED) {
509
+ super(message, code, { statusCode });
510
+ this.statusCode = statusCode;
511
+ this.name = "DeviceWorkerRequestError";
512
+ }
513
+ };
514
+ var DeviceCredentialRejectedError = class extends DeviceWorkerRequestError {
515
+ constructor() {
516
+ super(401, "Device credential rejected; run standards device pair again", SchemaErrorCode.ACCESS_DENIED);
517
+ this.name = "DeviceCredentialRejectedError";
518
+ }
519
+ };
520
+ /** The lease poll's own curve: 250 ms doubling up to 10 s, well under the channel's. */
521
+ const LEASE_RETRY = {
522
+ initialMs: 250,
523
+ maxMs: 1e4
524
+ };
525
+ function computeRetryDelay$1(attempt, random = Math.random) {
526
+ return computeRetryDelay(attempt, random, LEASE_RETRY);
527
+ }
528
+ function abortError() {
529
+ return new DOMException("Aborted", "AbortError");
530
+ }
531
+ function isAbortError$1(error) {
532
+ return error instanceof DOMException && error.name === "AbortError";
533
+ }
534
+ function sleepWithAbort(milliseconds, signal) {
535
+ if (signal?.aborted) return Promise.reject(abortError());
536
+ return new Promise((resolve, reject) => {
537
+ function abort() {
538
+ clearTimeout(timer);
539
+ reject(abortError());
540
+ }
541
+ const timer = setTimeout(() => {
542
+ signal?.removeEventListener("abort", abort);
543
+ resolve();
544
+ }, milliseconds);
545
+ signal?.addEventListener("abort", abort, { once: true });
546
+ });
547
+ }
548
+ function withAbort(promise, signal) {
549
+ if (!signal) return promise;
550
+ if (signal.aborted) return Promise.reject(abortError());
551
+ return new Promise((resolve, reject) => {
552
+ const abort = () => reject(abortError());
553
+ signal.addEventListener("abort", abort, { once: true });
554
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
555
+ });
556
+ }
557
+ function transmit(socket, frame) {
558
+ return new Promise((resolve, reject) => {
559
+ socket.send(JSON.stringify(frame), (error) => {
560
+ if (error) reject(new DeviceWorkerRequestError(0, "Device channel send failed"));
561
+ else resolve();
562
+ });
563
+ });
564
+ }
565
+ function leasedCommand(frame, command) {
566
+ return {
567
+ id: frame.id,
568
+ leaseToken: frame.leaseToken,
569
+ intent: frame.intent,
570
+ command,
571
+ timeoutMs: frame.timeoutMs,
572
+ ...typeof frame.cwd === "string" ? { cwd: frame.cwd } : {}
573
+ };
574
+ }
575
+ /**
576
+ * One WebSocket to the device channel. The first frame authenticates the
577
+ * device; commands are queued as they arrive and handed to `lease()`.
578
+ * A dropped connection reconnects with backoff; a credential rejection
579
+ * (`revoked`, or close 1008 before `ready`) is terminal.
580
+ */
581
+ var DeviceWorkerClient = class {
582
+ constructor(options) {
583
+ this.lifecycle = new AbortController();
584
+ this.queue = [];
585
+ this.waiters = /* @__PURE__ */ new Set();
586
+ this.cancellations = /* @__PURE__ */ new Map();
587
+ this.socket = null;
588
+ this.ready = false;
589
+ this.connection = null;
590
+ this.rejection = null;
591
+ this.closed = false;
592
+ this.url = deviceChannelUrl(options.apiUrl);
593
+ this.credential = `Device ${options.token}`;
594
+ this.sleep = options.sleep ?? sleepWithAbort;
595
+ this.random = options.random ?? Math.random;
596
+ this.now = options.now ?? Date.now;
597
+ }
598
+ /** Stops reconnecting and closes the socket; pending and later leases reject with an AbortError. */
599
+ close() {
600
+ this.closed = true;
601
+ this.lifecycle.abort();
602
+ this.socket?.close(NORMAL_CLOSE_CODE);
603
+ this.failWaiters(abortError());
604
+ }
605
+ lease(signal) {
606
+ if (this.rejection) return Promise.reject(this.rejection);
607
+ if (this.closed || signal?.aborted) return Promise.reject(abortError());
608
+ this.connection ??= this.maintainConnection().catch((error) => this.failWaiters(error));
609
+ const queued = this.queue.shift();
610
+ if (queued) return Promise.resolve(queued);
611
+ return new Promise((resolve, reject) => {
612
+ const settle = () => {
613
+ this.waiters.delete(waiter);
614
+ signal?.removeEventListener("abort", abort);
615
+ };
616
+ const waiter = {
617
+ resolve: (command) => {
618
+ settle();
619
+ resolve(command);
620
+ },
621
+ reject: (error) => {
622
+ settle();
623
+ reject(error);
624
+ }
625
+ };
626
+ const abort = () => waiter.reject(abortError());
627
+ signal?.addEventListener("abort", abort, { once: true });
628
+ this.waiters.add(waiter);
629
+ });
630
+ }
631
+ async markRunning(commandId, leaseToken) {
632
+ await this.send({
633
+ type: "running",
634
+ id: commandId,
635
+ leaseToken
636
+ });
637
+ }
638
+ async heartbeat(commandId, leaseToken, signal) {
639
+ const cancelled = this.cancellations.get(commandId);
640
+ if (cancelled) return {
641
+ cancel: true,
642
+ reason: cancelled
643
+ };
644
+ await withAbort(this.send({
645
+ type: "heartbeat",
646
+ id: commandId,
647
+ leaseToken
648
+ }), signal);
649
+ const reason = this.cancellations.get(commandId);
650
+ return reason ? {
651
+ cancel: true,
652
+ reason
653
+ } : { cancel: false };
654
+ }
655
+ async output(commandId, leaseToken, stdout, stderr) {
656
+ if (!(stdout || stderr)) return;
657
+ await this.send({
658
+ type: "output",
659
+ id: commandId,
660
+ leaseToken,
661
+ ...stdout ? { stdout } : {},
662
+ ...stderr ? { stderr } : {}
663
+ });
664
+ }
665
+ async complete(commandId, leaseToken, result) {
666
+ await this.send({
667
+ type: "complete",
668
+ id: commandId,
669
+ leaseToken,
670
+ result: {
671
+ stdout: result.stdout,
672
+ stderr: result.stderr,
673
+ exitCode: result.exitCode,
674
+ success: result.success,
675
+ ...result.errorCode ? { errorCode: result.errorCode } : {}
676
+ }
677
+ });
678
+ this.cancellations.delete(commandId);
679
+ }
680
+ send(frame) {
681
+ const socket = this.socket;
682
+ if (!(socket && this.ready && socket.readyState === WebSocket.OPEN)) return Promise.reject(new DeviceWorkerRequestError(0, "Device channel is not connected"));
683
+ return transmit(socket, frame);
684
+ }
685
+ reject(error) {
686
+ this.rejection = error;
687
+ this.failWaiters(error);
688
+ }
689
+ failWaiters(error) {
690
+ for (const waiter of [...this.waiters]) waiter.reject(error);
691
+ }
692
+ deliver(command) {
693
+ const [waiter] = this.waiters;
694
+ if (waiter) waiter.resolve(command);
695
+ else this.queue.push(command);
696
+ }
697
+ async maintainConnection() {
698
+ const signal = this.lifecycle.signal;
699
+ let attempt = 0;
700
+ let lastDelay = 0;
701
+ while (!(signal.aborted || this.rejection)) {
702
+ const { ready, readyAt } = await this.connectOnce();
703
+ if (signal.aborted || this.rejection) return;
704
+ if (ready && readyAt !== null && this.now() - readyAt > lastDelay) attempt = 0;
705
+ lastDelay = computeReconnectDelay(attempt, this.random);
706
+ try {
707
+ await this.sleep(lastDelay, signal);
708
+ } catch (error) {
709
+ if (isAbortError$1(error)) return;
710
+ throw error;
711
+ }
712
+ attempt += 1;
713
+ }
714
+ }
715
+ /** Opens one socket and resolves once it has closed, telling whether and when `ready` was seen. */
716
+ connectOnce() {
717
+ return new Promise((resolve) => {
718
+ const signal = this.lifecycle.signal;
719
+ const socket = new WebSocket(this.url);
720
+ let authenticated = false;
721
+ let ready = false;
722
+ let readyAt = null;
723
+ const stop = () => socket.close(NORMAL_CLOSE_CODE);
724
+ const readyTimer = setTimeout(stop, REQUEST_TIMEOUT_MS);
725
+ signal.addEventListener("abort", stop, { once: true });
726
+ this.socket = socket;
727
+ socket.on("open", () => {
728
+ authenticated = true;
729
+ transmit(socket, {
730
+ type: "authenticate",
731
+ credential: this.credential
732
+ }).catch(stop);
733
+ });
734
+ socket.on("error", () => void 0);
735
+ socket.on("message", (data) => {
736
+ let frame;
737
+ try {
738
+ frame = parseServerFrame(data.toString());
739
+ } catch {
740
+ socket.close(DEVICE_CHANNEL_WORKER_CLOSE_PROTOCOL_VIOLATION, "DEVICE_CHANNEL_PROTOCOL");
741
+ return;
742
+ }
743
+ if (frame.type === "ready") {
744
+ clearTimeout(readyTimer);
745
+ ready = true;
746
+ readyAt = this.now();
747
+ this.ready = true;
748
+ return;
749
+ }
750
+ this.handleFrame(socket, frame);
751
+ });
752
+ socket.on("close", (code) => {
753
+ clearTimeout(readyTimer);
754
+ signal.removeEventListener("abort", stop);
755
+ this.socket = null;
756
+ this.ready = false;
757
+ if (code === SERVER_POLICY_VIOLATION_CLOSE_CODE && authenticated && !ready) this.reject(new DeviceCredentialRejectedError());
758
+ resolve({
759
+ ready,
760
+ readyAt
761
+ });
762
+ });
763
+ });
764
+ }
765
+ handleFrame(socket, frame) {
766
+ switch (frame.type) {
767
+ case "command":
768
+ this.receiveCommand(socket, frame);
769
+ return;
770
+ case "cancel":
771
+ this.cancellations.set(frame.id, frame.reason);
772
+ return;
773
+ case "reauthenticate":
774
+ transmit(socket, {
775
+ type: "authenticate",
776
+ credential: this.credential
777
+ }).catch(() => socket.close(NORMAL_CLOSE_CODE));
778
+ return;
779
+ case "revoked":
780
+ this.reject(new DeviceCredentialRejectedError());
781
+ socket.close(NORMAL_CLOSE_CODE);
782
+ return;
783
+ case "ping":
784
+ transmit(socket, { type: "pong" }).catch(() => socket.close(NORMAL_CLOSE_CODE));
785
+ return;
786
+ }
787
+ }
788
+ receiveCommand(socket, frame) {
789
+ if (frame.kind === "browser") {
790
+ this.refuse(socket, frame, BROWSER_COMMAND_REFUSED);
791
+ return;
792
+ }
793
+ if (frame.command === void 0) {
794
+ this.refuse(socket, frame, EMPTY_SHELL_COMMAND_REFUSED);
795
+ return;
796
+ }
797
+ this.deliver(leasedCommand(frame, frame.command));
798
+ }
799
+ /** Completes a command the shell worker cannot run, without handing it to the loop. */
800
+ refuse(socket, frame, stderr) {
801
+ const { id, leaseToken } = frame;
802
+ transmit(socket, {
803
+ type: "running",
804
+ id,
805
+ leaseToken
806
+ }).then(() => transmit(socket, {
807
+ type: "complete",
808
+ id,
809
+ leaseToken,
810
+ result: {
811
+ stdout: "",
812
+ stderr,
813
+ exitCode: 1,
814
+ success: false,
815
+ errorCode: "execution_failed"
816
+ }
817
+ })).catch(() => void 0);
818
+ }
819
+ };
820
+ //#endregion
821
+ //#region src/device/process-runner.ts
822
+ const OUTPUT_LIMIT = 3e4;
823
+ const TERMINATION_GRACE_MS = 5e3;
824
+ const TERMINATION_POLL_MS = 50;
825
+ var BoundedTextBuffer = class {
826
+ constructor() {
827
+ this.decoder = new StringDecoder("utf8");
828
+ this.decoderFlushed = false;
829
+ this.value = "";
830
+ this.truncated = false;
831
+ }
832
+ append(chunk) {
833
+ if (this.truncated) return;
834
+ const text = typeof chunk === "string" ? chunk : this.decoder.write(Buffer.from(chunk));
835
+ this.appendText(text);
836
+ }
837
+ appendText(text) {
838
+ const remaining = OUTPUT_LIMIT - this.value.length;
839
+ if (text.length <= remaining) {
840
+ this.value += text;
841
+ return;
842
+ }
843
+ this.value += text.slice(0, Math.max(remaining, 0));
844
+ this.truncated = true;
845
+ }
846
+ toString() {
847
+ if (!this.decoderFlushed) {
848
+ this.decoderFlushed = true;
849
+ this.appendText(this.decoder.end());
850
+ }
851
+ return this.truncated ? `${this.value}\n... output truncated to ${OUTPUT_LIMIT} chars` : this.value;
852
+ }
853
+ };
854
+ function isMissingProcess(error) {
855
+ return error instanceof Error && "code" in error && error.code === "ESRCH";
856
+ }
857
+ function signalProcessGroup(pid, signal, kill) {
858
+ try {
859
+ kill(-pid, signal);
860
+ return true;
861
+ } catch (error) {
862
+ if (isMissingProcess(error)) return false;
863
+ throw error;
864
+ }
865
+ }
866
+ function beginProcessGroupTermination(pid, deps = {}) {
867
+ const kill = deps.kill ?? process.kill.bind(process);
868
+ const setTimer = deps.setTimer ?? setTimeout;
869
+ const clearTimer = deps.clearTimer ?? clearTimeout;
870
+ return new Promise((resolve, reject) => {
871
+ let settled = false;
872
+ let escalationTimer;
873
+ let pollTimer;
874
+ function clearTimers() {
875
+ if (escalationTimer) clearTimer(escalationTimer);
876
+ if (pollTimer) clearTimer(pollTimer);
877
+ }
878
+ function finish() {
879
+ if (settled) return;
880
+ settled = true;
881
+ clearTimers();
882
+ resolve();
883
+ }
884
+ function fail(error) {
885
+ if (settled) return;
886
+ settled = true;
887
+ clearTimers();
888
+ reject(error);
889
+ }
890
+ function poll() {
891
+ if (settled) return;
892
+ try {
893
+ if (!signalProcessGroup(pid, 0, kill)) {
894
+ finish();
895
+ return;
896
+ }
897
+ pollTimer = setTimer(poll, TERMINATION_POLL_MS);
898
+ } catch (error) {
899
+ fail(error);
900
+ }
901
+ }
902
+ try {
903
+ if (!signalProcessGroup(pid, "SIGTERM", kill)) {
904
+ finish();
905
+ return;
906
+ }
907
+ escalationTimer = setTimer(() => {
908
+ try {
909
+ if (!signalProcessGroup(pid, 0, kill)) {
910
+ finish();
911
+ return;
912
+ }
913
+ signalProcessGroup(pid, "SIGKILL", kill);
914
+ } catch (error) {
915
+ fail(error);
916
+ }
917
+ }, TERMINATION_GRACE_MS);
918
+ pollTimer = setTimer(poll, TERMINATION_POLL_MS);
919
+ } catch (error) {
920
+ fail(error);
921
+ }
922
+ });
923
+ }
924
+ async function resolveShell(shell = process.env.SHELL) {
925
+ if (!(shell && isAbsolute(shell))) return "/bin/sh";
926
+ try {
927
+ if (!(await stat(shell)).isFile()) return "/bin/sh";
928
+ await access(shell, constants.X_OK);
929
+ return shell;
930
+ } catch {
931
+ return "/bin/sh";
932
+ }
933
+ }
934
+ function terminalResult(reason, stdout, stderr) {
935
+ return {
936
+ stdout: stdout.toString(),
937
+ stderr: stderr.toString(),
938
+ exitCode: reason === "timed_out" ? 124 : 130,
939
+ success: false,
940
+ errorCode: reason
941
+ };
942
+ }
943
+ async function runDeviceProcess(input) {
944
+ if (input.signal?.aborted) return {
945
+ stdout: "",
946
+ stderr: "",
947
+ exitCode: 130,
948
+ success: false,
949
+ errorCode: "cancelled"
950
+ };
951
+ const shell = await resolveShell();
952
+ if (input.signal?.aborted) return {
953
+ stdout: "",
954
+ stderr: "",
955
+ exitCode: 130,
956
+ success: false,
957
+ errorCode: "cancelled"
958
+ };
959
+ const stdout = new BoundedTextBuffer();
960
+ const stderr = new BoundedTextBuffer();
961
+ return new Promise((resolve) => {
962
+ let settled = false;
963
+ let terminationReason;
964
+ let termination;
965
+ let child;
966
+ function finish(result) {
967
+ if (settled) return;
968
+ settled = true;
969
+ clearTimeout(timeout);
970
+ input.signal?.removeEventListener("abort", cancel);
971
+ resolve(result);
972
+ }
973
+ function finishTerminationFailure(error) {
974
+ finish({
975
+ stdout: stdout.toString(),
976
+ stderr: error instanceof Error ? error.message : String(error),
977
+ exitCode: 1,
978
+ success: false,
979
+ errorCode: "execution_failed"
980
+ });
981
+ }
982
+ function terminate(reason) {
983
+ if (settled || terminationReason || !child?.pid) return;
984
+ terminationReason = reason;
985
+ termination = beginProcessGroupTermination(child.pid);
986
+ termination.catch(finishTerminationFailure);
987
+ }
988
+ function cancel() {
989
+ terminate("cancelled");
990
+ }
991
+ const timeout = setTimeout(() => terminate("timed_out"), input.timeoutMs);
992
+ try {
993
+ child = spawn(shell, ["-lc", input.command], {
994
+ cwd: input.cwd,
995
+ env: process.env,
996
+ detached: true,
997
+ stdio: [
998
+ "ignore",
999
+ "pipe",
1000
+ "pipe"
1001
+ ]
1002
+ });
1003
+ } catch (error) {
1004
+ finish({
1005
+ stdout: "",
1006
+ stderr: error instanceof Error ? error.message : String(error),
1007
+ exitCode: 1,
1008
+ success: false,
1009
+ errorCode: "execution_failed"
1010
+ });
1011
+ return;
1012
+ }
1013
+ input.signal?.addEventListener("abort", cancel, { once: true });
1014
+ if (input.signal?.aborted) cancel();
1015
+ const spawned = child;
1016
+ spawned.stdout.on("data", (chunk) => stdout.append(chunk));
1017
+ spawned.stderr.on("data", (chunk) => stderr.append(chunk));
1018
+ spawned.once("error", (error) => {
1019
+ finish({
1020
+ stdout: stdout.toString(),
1021
+ stderr: error.message,
1022
+ exitCode: 1,
1023
+ success: false,
1024
+ errorCode: "execution_failed"
1025
+ });
1026
+ });
1027
+ spawned.once("close", (code) => {
1028
+ if (terminationReason) {
1029
+ const reason = terminationReason;
1030
+ (termination ?? Promise.resolve()).then(() => finish(terminalResult(reason, stdout, stderr)), finishTerminationFailure);
1031
+ return;
1032
+ }
1033
+ const exitCode = code ?? 1;
1034
+ finish({
1035
+ stdout: stdout.toString(),
1036
+ stderr: stderr.toString(),
1037
+ exitCode,
1038
+ success: exitCode === 0,
1039
+ ...code === null ? { errorCode: "execution_failed" } : {}
1040
+ });
1041
+ });
1042
+ });
1043
+ }
1044
+ //#endregion
1045
+ //#region src/device/worker-loop.ts
1046
+ const HEARTBEAT_INTERVAL_MS = 2e3;
1047
+ function isAbortError(error) {
1048
+ return error instanceof DOMException && error.name === "AbortError";
1049
+ }
1050
+ /** The channel client reconnects on its own; only a transport failure (status 0) is retried here. */
1051
+ function isRetryableIdleError(error) {
1052
+ return error instanceof DeviceWorkerRequestError && error.statusCode === 0;
1053
+ }
1054
+ function isLeaseConflict(error) {
1055
+ return error instanceof DeviceWorkerRequestError && error.statusCode === 409;
1056
+ }
1057
+ async function executeLease(command, input, runProcess, sleep) {
1058
+ try {
1059
+ await input.client.markRunning(command.id, command.leaseToken);
1060
+ } catch (error) {
1061
+ if (error instanceof DeviceCredentialRejectedError) throw error;
1062
+ return;
1063
+ }
1064
+ input.onEvent?.({
1065
+ type: "running",
1066
+ commandId: command.id,
1067
+ intent: command.intent
1068
+ });
1069
+ const commandController = new AbortController();
1070
+ const heartbeatController = new AbortController();
1071
+ let completionAllowed = true;
1072
+ let heartbeatFailure;
1073
+ function abortCommand() {
1074
+ commandController.abort();
1075
+ }
1076
+ input.signal.addEventListener("abort", abortCommand, { once: true });
1077
+ const processPromise = runProcess({
1078
+ command: command.command,
1079
+ cwd: command.cwd,
1080
+ timeoutMs: command.timeoutMs,
1081
+ signal: commandController.signal
1082
+ });
1083
+ const heartbeatPromise = (async () => {
1084
+ while (!heartbeatController.signal.aborted) try {
1085
+ await sleep(HEARTBEAT_INTERVAL_MS, heartbeatController.signal);
1086
+ const heartbeat = await input.client.heartbeat(command.id, command.leaseToken, heartbeatController.signal);
1087
+ if (heartbeat.cancel) {
1088
+ if (heartbeat.reason === "revoked") completionAllowed = false;
1089
+ commandController.abort();
1090
+ return;
1091
+ }
1092
+ } catch (error) {
1093
+ if (heartbeatController.signal.aborted && isAbortError(error)) return;
1094
+ heartbeatFailure = error;
1095
+ completionAllowed = false;
1096
+ commandController.abort();
1097
+ return;
1098
+ }
1099
+ })();
1100
+ let result;
1101
+ try {
1102
+ result = await processPromise;
1103
+ } catch (error) {
1104
+ result = {
1105
+ stdout: "",
1106
+ stderr: error instanceof Error ? error.message : String(error),
1107
+ exitCode: 1,
1108
+ success: false,
1109
+ errorCode: "execution_failed"
1110
+ };
1111
+ } finally {
1112
+ heartbeatController.abort();
1113
+ await heartbeatPromise;
1114
+ input.signal.removeEventListener("abort", abortCommand);
1115
+ }
1116
+ if (heartbeatFailure instanceof DeviceCredentialRejectedError) throw heartbeatFailure;
1117
+ if (!completionAllowed || isLeaseConflict(heartbeatFailure)) return;
1118
+ try {
1119
+ await input.client.output(command.id, command.leaseToken, result.stdout, result.stderr);
1120
+ await input.client.complete(command.id, command.leaseToken, result);
1121
+ } catch (error) {
1122
+ if (error instanceof DeviceCredentialRejectedError) throw error;
1123
+ return;
1124
+ }
1125
+ input.onEvent?.({
1126
+ type: "completed",
1127
+ commandId: command.id,
1128
+ exitCode: result.exitCode
1129
+ });
1130
+ }
1131
+ async function runDeviceWorker(input) {
1132
+ const runProcess = input.runProcess ?? runDeviceProcess;
1133
+ const sleep = input.sleep ?? sleepWithAbort;
1134
+ const random = input.random ?? Math.random;
1135
+ let retryAttempt = 0;
1136
+ while (!input.signal.aborted) {
1137
+ let command;
1138
+ try {
1139
+ command = await input.client.lease(input.signal);
1140
+ retryAttempt = 0;
1141
+ } catch (error) {
1142
+ if (input.signal.aborted) return;
1143
+ if (error instanceof DeviceCredentialRejectedError) throw error;
1144
+ if (!isRetryableIdleError(error)) throw error;
1145
+ try {
1146
+ await sleep(computeRetryDelay$1(retryAttempt, random), input.signal);
1147
+ } catch (sleepError) {
1148
+ if (input.signal.aborted && isAbortError(sleepError)) return;
1149
+ throw sleepError;
1150
+ }
1151
+ retryAttempt += 1;
1152
+ continue;
1153
+ }
1154
+ if (!command) continue;
1155
+ await executeLease(command, input, runProcess, sleep);
1156
+ }
1157
+ }
1158
+ //#endregion
1159
+ //#region src/commands/device.ts
1160
+ const NAMED_PROFILE_REQUIRED = "Device pairing requires a named Standards profile";
1161
+ const SUPPORTED_PLATFORMS_REQUIRED = "Device workers support macOS and Linux only";
1162
+ var DeviceCommandError = class extends SchemaError {
1163
+ constructor(message) {
1164
+ super(message, SchemaErrorCode.VALIDATION_FAILED);
1165
+ this.name = "DeviceCommandError";
1166
+ }
1167
+ };
1168
+ function requireSupportedPlatform(platform) {
1169
+ if (platform === "darwin" || platform === "linux") return platform;
1170
+ throw new DeviceCommandError(SUPPORTED_PLATFORMS_REQUIRED);
1171
+ }
1172
+ function requireNamedProfile(command) {
1173
+ const options = getGlobalOptions(command);
1174
+ if (!(options.profileName && options.apiUrl && options.apiKey)) throw new DeviceCommandError(NAMED_PROFILE_REQUIRED);
1175
+ return {
1176
+ apiUrl: options.apiUrl,
1177
+ apiKey: options.apiKey,
1178
+ profileName: options.profileName,
1179
+ tenantId: options.tenant
1180
+ };
1181
+ }
1182
+ function isRecord(value) {
1183
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1184
+ }
1185
+ function parseDeviceView(value) {
1186
+ if (!isRecord(value)) throw new DeviceCommandError("Invalid device response");
1187
+ const { id, name, platform, arch, hostname: deviceHostname } = value;
1188
+ const { status } = value;
1189
+ if (typeof id !== "string" || typeof name !== "string" || platform !== "darwin" && platform !== "linux" || typeof arch !== "string" || typeof deviceHostname !== "string" || status !== "online" && status !== "offline" && status !== "revoked") throw new DeviceCommandError("Invalid device response");
1190
+ return {
1191
+ id,
1192
+ name,
1193
+ platform,
1194
+ arch,
1195
+ hostname: deviceHostname,
1196
+ status
1197
+ };
1198
+ }
1199
+ function parsePairDeviceResponse(value) {
1200
+ if (!isRecord(value) || typeof value.token !== "string") throw new DeviceCommandError("Invalid device pairing response");
1201
+ return {
1202
+ device: parseDeviceView(value.device),
1203
+ token: value.token
1204
+ };
1205
+ }
1206
+ function parseDeviceList(value) {
1207
+ if (!Array.isArray(value)) throw new DeviceCommandError("Invalid device list response");
1208
+ return value.map(parseDeviceView);
1209
+ }
1210
+ async function pairDevice(name, command) {
1211
+ const platform = requireSupportedPlatform(process.platform);
1212
+ const profile = requireNamedProfile(command);
1213
+ const installationId = await getOrCreateInstallationId();
1214
+ const client = createClient({
1215
+ apiUrl: profile.apiUrl,
1216
+ apiKey: profile.apiKey,
1217
+ tenantId: profile.tenantId
1218
+ });
1219
+ const response = parsePairDeviceResponse(await createAppClient(client).devices.pair({
1220
+ installationId,
1221
+ name,
1222
+ platform,
1223
+ arch: process.arch,
1224
+ hostname: hostname()
1225
+ }));
1226
+ if (!await storeDeviceCredential(profile.profileName, {
1227
+ id: response.device.id,
1228
+ name: response.device.name,
1229
+ token: response.token
1230
+ })) throw new DeviceCommandError(NAMED_PROFILE_REQUIRED);
1231
+ process.stdout.write(`Paired ${response.device.name} (${response.device.id})\n`);
1232
+ }
1233
+ async function showDeviceStatus(command) {
1234
+ const profile = requireNamedProfile(command);
1235
+ const credential = await getStoredDeviceCredential(profile.profileName);
1236
+ if (!credential) throw new DeviceCommandError("No paired device; run standards device pair first");
1237
+ const client = createClient({
1238
+ apiUrl: profile.apiUrl,
1239
+ apiKey: profile.apiKey,
1240
+ tenantId: profile.tenantId
1241
+ });
1242
+ const pairedDevice = parseDeviceList(await createAppClient(client).devices.list({})).find((device) => device.id === credential.id);
1243
+ formatOutput(pairedDevice ? [pairedDevice] : [{
1244
+ id: credential.id,
1245
+ name: credential.name,
1246
+ status: "revoked"
1247
+ }], getFormat(command));
1248
+ }
1249
+ async function revokeDevice(command) {
1250
+ const profile = requireNamedProfile(command);
1251
+ const credential = await getStoredDeviceCredential(profile.profileName);
1252
+ if (!credential) throw new DeviceCommandError("No paired device; run standards device pair first");
1253
+ const client = createClient({
1254
+ apiUrl: profile.apiUrl,
1255
+ apiKey: profile.apiKey,
1256
+ tenantId: profile.tenantId
1257
+ });
1258
+ await createAppClient(client).devices.revoke({ id: credential.id });
1259
+ await removeDeviceCredential(profile.profileName);
1260
+ }
1261
+ async function serveDevice(command) {
1262
+ const platform = requireSupportedPlatform(process.platform);
1263
+ const profile = requireNamedProfile(command);
1264
+ const credential = await getStoredDeviceCredential(profile.profileName);
1265
+ if (!credential) throw new DeviceCommandError("No paired device; run standards device pair first");
1266
+ const userClient = createClient({
1267
+ apiUrl: profile.apiUrl,
1268
+ apiKey: profile.apiKey,
1269
+ tenantId: profile.tenantId
1270
+ });
1271
+ const device = parseDeviceList(await createAppClient(userClient).devices.list({})).find((candidate) => candidate.id === credential.id);
1272
+ if (!device) throw new DeviceCommandError("Device is unavailable");
1273
+ if (device.platform !== platform) throw new DeviceCommandError("Device platform does not match paired device");
1274
+ const controller = new AbortController();
1275
+ const stop = () => controller.abort();
1276
+ process.once("SIGINT", stop);
1277
+ process.once("SIGTERM", stop);
1278
+ process.stdout.write(`Connected as ${credential.name}\n`);
1279
+ process.stdout.write("Waiting for commands — press Ctrl+C to disconnect\n");
1280
+ const client = new DeviceWorkerClient({
1281
+ apiUrl: profile.apiUrl,
1282
+ token: credential.token
1283
+ });
1284
+ try {
1285
+ await runDeviceWorker({
1286
+ client,
1287
+ signal: controller.signal,
1288
+ onEvent: (event) => {
1289
+ if (event.type === "running") {
1290
+ process.stdout.write(`Running ${event.commandId}: ${event.intent}\n`);
1291
+ return;
1292
+ }
1293
+ process.stdout.write(`Completed ${event.commandId} with exit code ${event.exitCode}\n`);
1294
+ }
1295
+ });
1296
+ } finally {
1297
+ client.close();
1298
+ process.removeListener("SIGINT", stop);
1299
+ process.removeListener("SIGTERM", stop);
1300
+ }
1301
+ }
1302
+ function registerDeviceCommand(program) {
1303
+ const device = program.command("device").description("Pair and run this computer as a device");
1304
+ device.command("pair").description("Pair this computer with Standards").requiredOption("--name <name>", "device name").action(async (options, command) => pairDevice(options.name, command));
1305
+ device.command("status").description("Show the paired device status").action(async (_options, command) => showDeviceStatus(command));
1306
+ device.command("revoke").description("Revoke this computer's device credential").action(async (_options, command) => revokeDevice(command));
1307
+ device.command("serve").description("Run the foreground device worker").action(async (_options, command) => serveDevice(command));
1308
+ }
1309
+ //#endregion
1310
+ //#region src/drift/shared-state.ts
1311
+ async function readSchemaState(client) {
1312
+ const state = parseSchemaState(await client.schemaState.get({}));
1313
+ if (createHash("sha256").update(canonicalStringify(schemaStateSourcePayload(state.entries))).digest("hex") !== state.sourceFingerprint) throw new ValidationError("Invalid schema state: sourceFingerprint does not match deployed source", []);
1314
+ return state;
1315
+ }
1316
+ function prettyCanonical(value) {
1317
+ return JSON.stringify(JSON.parse(canonicalStringify(value)), null, 2);
1318
+ }
1319
+ //#endregion
1320
+ //#region src/commands/diff.ts
1321
+ async function runDiffCommand(client) {
1322
+ try {
1323
+ const diff = diffSchemaState(await readSchemaState(client));
1324
+ return {
1325
+ exitCode: diff.changes.length ? 1 : 0,
1326
+ diff
1327
+ };
1328
+ } catch (error) {
1329
+ return {
1330
+ exitCode: 2,
1331
+ errorMessage: messageOf(error)
1332
+ };
1333
+ }
1334
+ }
1335
+ function renderDiff(diff) {
1336
+ return ["Deployed source vs shared runtime", ...diff.changes.length ? diff.changes.map((entry) => `${entry.change} ${JSON.stringify(entry.address)}`) : ["No differences."]].join("\n");
1337
+ }
1338
+ function registerDiffCommand(program) {
1339
+ 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) => {
1340
+ let result;
1341
+ try {
1342
+ result = await runDiffCommand(getAppClientFromCommand(cmd));
1343
+ } catch (error) {
1344
+ result = {
1345
+ exitCode: 2,
1346
+ errorMessage: messageOf(error)
1347
+ };
1348
+ }
1349
+ if (result.exitCode === 2) console.error(styleText("red", `✗ ${result.errorMessage}`));
1350
+ else console.info(opts.json ? prettyCanonical(result.diff) : renderDiff(result.diff));
1351
+ process.exit(result.exitCode);
1352
+ });
1353
+ }
1354
+ //#endregion
1355
+ //#region src/commands/documents.ts
1356
+ /**
1357
+ * Human-readable table row for a pack file. `id` is kept (unlike other
1358
+ * dropped fields) because `remove-file <documentId> <fileId>` requires it —
1359
+ * this table is the only interactive way to discover it. Files are otherwise
1360
+ * anonymous: position is the display identity, never the (provenance-only)
1361
+ * filename.
1362
+ */
1363
+ function toFileRow(file) {
1364
+ return {
1365
+ id: file.id,
1366
+ position: file.position,
1367
+ type: file.mimeType,
1368
+ size: formatByteSize(file.size),
1369
+ ocrStatus: file.ocrStatus
1370
+ };
1371
+ }
1372
+ function registerDocumentsCommand(program) {
1373
+ const documents = program.command("documents").description("Manage documents");
1374
+ documents.command("list").description("List documents").option("--limit <n>", "max documents to return").option("--offset <n>", "number of documents to skip").action(async (opts, cmd) => {
1375
+ formatOutput(await getAppClientFromCommand(cmd).documents.list({
1376
+ limit: opts.limit,
1377
+ offset: opts.offset
1378
+ }), getFormat(cmd));
1379
+ });
1380
+ documents.command("get").description("Get a document by ID").argument("<id>", "document ID").action(async (id, _opts, cmd) => {
1381
+ formatOutput(await getAppClientFromCommand(cmd).documents.get({ id }), getFormat(cmd));
1382
+ });
1383
+ documents.command("create").description("Create a new document on a record attribute").requiredOption("--title <title>", "document title").requiredOption("--object <name>", "object of the record the document is created on").requiredOption("--record <id>", "record the document is created on").requiredOption("--attribute <name>", "document attribute of that record").action(async (opts, cmd) => {
1384
+ formatOutput(await getAppClientFromCommand(cmd).documents.create({
1385
+ title: opts.title,
1386
+ objectName: opts.object,
1387
+ sourceRecordId: opts.record,
1388
+ attributeName: opts.attribute
1389
+ }), getFormat(cmd));
1390
+ });
1391
+ documents.command("update").description("Rename a document").argument("<id>", "document ID").requiredOption("--title <title>", "new title").action(async (id, opts, cmd) => {
1392
+ formatOutput(await getAppClientFromCommand(cmd).documents.rename({
1393
+ id,
1394
+ name: opts.title
1395
+ }), getFormat(cmd));
1396
+ });
1397
+ documents.command("delete").description("Delete a document").argument("<id>", "document ID").action(async (id, _opts, cmd) => {
1398
+ await getAppClientFromCommand(cmd).documents.delete({ id });
1399
+ process.stdout.write(`Document ${id} deleted.\n`);
1400
+ });
1401
+ documents.command("preview").description("Get preview file for a document").argument("<id>", "document ID").action(async (id, _opts, cmd) => {
1402
+ formatOutput(await getAppClientFromCommand(cmd).documents.preview({ id }), getFormat(cmd));
1403
+ });
1404
+ documents.command("files").description("List files in a document's pack (position-ordered)").argument("<id>", "document ID").action(async (id, _opts, cmd) => {
1405
+ const sorted = [...await getAppClientFromCommand(cmd).documents.files({ id })].sort((a, b) => a.position - b.position);
1406
+ const format = getFormat(cmd);
1407
+ formatOutput(format === "table" ? sorted.map(toFileRow) : sorted, format);
1408
+ });
1409
+ documents.command("add-file").description("Append a file to a document's pack").argument("<id>", "document ID").requiredOption("--file-id <fileId>", "file ID").action(async (id, opts, cmd) => {
1410
+ formatOutput(await getAppClientFromCommand(cmd).documents.addFile({
1411
+ id,
1412
+ fileId: opts.fileId
1413
+ }), getFormat(cmd));
1414
+ });
1415
+ documents.command("remove-file").description("Remove a file from a document's pack").argument("<id>", "document ID").argument("<fileId>", "file ID").action(async (id, fileId, _opts, cmd) => {
1416
+ await getAppClientFromCommand(cmd).documents.removeFile({
1417
+ id,
1418
+ fileId
1419
+ });
1420
+ process.stdout.write(`File ${fileId} removed.\n`);
1421
+ });
1422
+ documents.command("content").description("Print a document's aggregated OCR text (every pack file, position order)").argument("<id>", "document ID").action(async (id, _opts, cmd) => {
1423
+ const { blob } = await getAppClientFromCommand(cmd).documents.content({ id });
1424
+ const text = await blob.text();
1425
+ process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
1426
+ });
1427
+ }
1428
+ //#endregion
1429
+ //#region src/commands/embeddings.ts
1430
+ function registerEmbeddingsCommand(program) {
1431
+ program.command("embeddings").description("Maintain record embeddings").command("drain").description("Embed every pending memory and skill record of the tenant").action(async (_opts, cmd) => {
1432
+ const tenant = requireTenant(cmd, "An embedding drain names its tenant explicitly. Pass \"--tenant <id>\".");
1433
+ await getAppClientFromCommand(cmd).embeddingsAdmin.drain({ tenantId: tenant });
1434
+ process.stdout.write(`${styleText("green", "✓")} Embedding drain accepted for tenant ${tenant}.\n`);
1435
+ });
1436
+ }
1437
+ //#endregion
1438
+ //#region src/commands/folders.ts
1439
+ function registerFoldersCommand(program) {
1440
+ 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) => {
1441
+ acc.push(val);
1442
+ return acc;
1443
+ }, []).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) => {
1444
+ const paths = [...opts.path];
1445
+ if (opts.pathsFile) {
1446
+ const filePaths = (await readFile(opts.pathsFile, "utf-8")).split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
1447
+ paths.push(...filePaths);
1448
+ }
1449
+ if (paths.length === 0) throw new Error("No paths provided. Pass at least one --path or a non-empty --paths-file.");
1450
+ formatOutput(await getAppClientFromCommand(cmd).folders.reconcilePaths({
1451
+ recordId,
1452
+ objectName,
1453
+ paths,
1454
+ mode: opts.mode
1455
+ }), getFormat(cmd));
1456
+ });
1457
+ }
1458
+ //#endregion
1459
+ //#region src/commands/invitations.ts
1460
+ /** Invitation ids are `randomUUID` server-side, so anything else is a caller mistake. */
1461
+ const INVITATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1462
+ /**
1463
+ * Refuses a malformed id before any request, with a message naming it. The
1464
+ * contract already keeps any id inside its own route segment (#4218); this is
1465
+ * the friendlier answer to a typo.
1466
+ */
1467
+ function requireInvitationId(id) {
1468
+ if (!INVITATION_ID.test(id)) throw new ValidationError(`"${id}" is not a valid invitation ID.`, []);
1469
+ return id;
1470
+ }
1471
+ /**
1472
+ * Tenant invitations — the explicit lifecycle behind adding people to a workspace.
1473
+ * Acceptance is not exposed: it needs the invitee's own verified bearer, never an API key.
1474
+ */
1475
+ function registerInvitationsCommand(program) {
1476
+ const invitations = program.command("invitations").description("Invite people to the current tenant and manage pending invitations");
1477
+ invitations.command("list").description("List tenant invitations").action(async (_opts, cmd) => {
1478
+ formatOutput(await getAppClientFromCommand(cmd).tenantInvitations.list({}), getFormat(cmd));
1479
+ });
1480
+ invitations.command("create").description("Invite an email address with a role; an email already in the tenant is refused").requiredOption("--email <email>", "email address to invite").requiredOption("--role <roleId>", "role id granted on acceptance").option("--yes", "send without confirmation").action(async (opts, cmd) => {
1481
+ requireConfirmation(opts.yes, "Sending an invitation is explicit. Re-run with \"--yes\" to confirm.");
1482
+ formatOutput(await getAppClientFromCommand(cmd).tenantInvitations.create({
1483
+ email: opts.email,
1484
+ roleId: opts.role
1485
+ }), getFormat(cmd));
1486
+ });
1487
+ invitations.command("resend").description("Resend a pending invitation email").argument("<id>", "invitation ID").action(async (id, _opts, cmd) => {
1488
+ const invitationId = requireInvitationId(id);
1489
+ formatOutput(await getAppClientFromCommand(cmd).tenantInvitations.resend({ id: invitationId }), getFormat(cmd));
1490
+ });
1491
+ invitations.command("revoke").description("Revoke a pending invitation").argument("<id>", "invitation ID").option("--yes", "revoke without confirmation").action(async (id, opts, cmd) => {
1492
+ requireConfirmation(opts.yes, "Revoking an invitation is explicit. Re-run with \"--yes\" to confirm.");
1493
+ const invitationId = requireInvitationId(id);
1494
+ formatOutput(await getAppClientFromCommand(cmd).tenantInvitations.revoke({ id: invitationId }), getFormat(cmd));
1495
+ });
1496
+ }
1497
+ //#endregion
1498
+ //#region src/commands/keys.ts
1499
+ async function resolveRoleId(client, name) {
1500
+ const match = (await client.permissions.listRoles({})).find((role) => role.name === name);
1501
+ if (!match) throw new Error(`Unknown role "${name}". Roles are listed in the app's People settings.`);
1502
+ return match.id;
1503
+ }
1504
+ function registerKeysCommand(program) {
1505
+ const keys = program.command("keys").description("Manage Standards API keys");
1506
+ keys.command("list").description("List API keys").action(async (_opts, cmd) => {
1507
+ formatOutput(await getAppClientFromCommand(cmd).apiKeys.list({}), getFormat(cmd));
1508
+ });
1509
+ keys.command("create").description("Create a personal API key (acts with your access); --workspace for an integration key").requiredOption("--name <name>", "name for the API key").option("--role <name>", "role the key holds (default: member)").option("--workspace", "create a workspace key that acts with its own roles, not yours").option("--expires <date>", "expiration date (ISO 8601)").option("--yes", "create without confirmation").action(async (opts, cmd) => {
1510
+ if (!opts.yes) throw new Error("Creating API keys is explicit. Re-run with \"--yes\" to confirm.");
1511
+ const client = getAppClientFromCommand(cmd);
1512
+ const body = { name: opts.name };
1513
+ if (!opts.workspace) body.personal = true;
1514
+ if (opts.expires) body.expiresAt = opts.expires;
1515
+ if (opts.role) body.roleId = await resolveRoleId(client, opts.role);
1516
+ formatOutput(await client.apiKeys.create(body), getFormat(cmd));
1517
+ });
1518
+ keys.command("detach").description("Make a workspace key act with its own roles, independent of its creator").argument("<id>", "API key ID").option("--yes", "detach without confirmation").action(async (id, opts, cmd) => {
1519
+ if (!opts.yes) throw new Error("Detaching an API key is explicit. Re-run with \"--yes\" to confirm.");
1520
+ formatOutput(await getAppClientFromCommand(cmd).apiKeys.detach({ id }), getFormat(cmd));
1521
+ });
1522
+ keys.command("revoke").description("Revoke an API key").argument("<id>", "API key ID").option("--yes", "revoke without confirmation").action(async (id, opts, cmd) => {
1523
+ if (!opts.yes) throw new Error("Revoking API keys is explicit. Re-run with \"--yes\" to confirm.");
1524
+ await getAppClientFromCommand(cmd).apiKeys.revoke({ id });
1525
+ process.stdout.write(`${styleText("green", "✓")} API key ${id} revoked.\n`);
1526
+ });
1527
+ }
1528
+ //#endregion
1529
+ //#region src/commands/mcp.ts
1530
+ /** The auth types `POST /mcp-servers` accepts today — `oauth` is rejected by the API. */
1531
+ const AUTH_TYPES = ["none", "header"];
1532
+ /** Build the `auth` block the API expects, failing before the request when a header auth is incomplete. */
1533
+ function buildAuth(opts) {
1534
+ const type = opts.authType ?? "none";
1535
+ if (!AUTH_TYPES.includes(type)) throw new ValidationError(`--auth-type must be one of: ${AUTH_TYPES.join(", ")}`, []);
1536
+ if (type === "none") return { type: "none" };
1537
+ if (!(opts.headerName && opts.secret)) throw new ValidationError("--auth-type \"header\" requires both --header-name and --secret.", []);
1538
+ return {
1539
+ type: "header",
1540
+ headerName: opts.headerName,
1541
+ secret: opts.secret
1542
+ };
1543
+ }
1544
+ function resolveVisibility(visibility) {
1545
+ if (visibility === void 0) return "workspace";
1546
+ if (!RESOURCE_VISIBILITIES.includes(visibility)) throw new ValidationError(`--visibility must be one of: ${RESOURCE_VISIBILITIES.join(", ")}`, []);
1547
+ return visibility;
1548
+ }
1549
+ function registerMcpCommand(program) {
1550
+ const mcp = program.command("mcp").description("Manage the MCP servers your workspace's agents can be granted");
1551
+ mcp.command("list").description("List connected MCP servers").action(async (_opts, cmd) => {
1552
+ formatOutput(await getAppClientFromCommand(cmd).mcpServers.list({}), getFormat(cmd));
1553
+ });
1554
+ mcp.command("catalog").description("List the vendored MCP catalog entries available to connect").action(async (_opts, cmd) => {
1555
+ formatOutput(await getAppClientFromCommand(cmd).mcpServers.catalog({}), getFormat(cmd));
1556
+ });
1557
+ mcp.command("connect").description("Connect a remote MCP server and mount its tools").requiredOption("--slug <slug>", "unique slug for the server").requiredOption("--name <name>", "display name").requiredOption("--url <url>", "remote MCP endpoint URL").option("--visibility <visibility>", "workspace or private", "workspace").option("--catalog-entry <id>", "catalog entry ID this server instantiates").option("--auth-type <type>", "none or header", "none").option("--header-name <name>", "header name when --auth-type is \"header\"").option("--secret <secret>", "header value when --auth-type is \"header\"").action(async (opts, cmd) => {
1558
+ const auth = buildAuth(opts);
1559
+ const visibility = resolveVisibility(opts.visibility);
1560
+ formatOutput(await getAppClientFromCommand(cmd).mcpServers.connect({
1561
+ slug: opts.slug,
1562
+ name: opts.name,
1563
+ url: opts.url,
1564
+ visibility,
1565
+ catalogEntryId: opts.catalogEntry ?? null,
1566
+ auth
1567
+ }), getFormat(cmd));
1568
+ });
1569
+ mcp.command("refresh").description("Re-discover a server's tools and refresh its status").argument("<id>", "MCP server ID").action(async (id, _opts, cmd) => {
1570
+ formatOutput(await getAppClientFromCommand(cmd).mcpServers.refresh({ id }), getFormat(cmd));
1571
+ });
1572
+ mcp.command("rename").description("Rename a server's display name (its slug, the tool-name prefix, never changes)").argument("<id>", "MCP server ID").argument("<name>", "new display name").action(async (id, name, _opts, cmd) => {
1573
+ if (name.trim() === "") throw new ValidationError("The new name cannot be empty.", []);
1574
+ formatOutput(await getAppClientFromCommand(cmd).mcpServers.rename({
1575
+ id,
1576
+ name
1577
+ }), getFormat(cmd));
1578
+ });
1579
+ mcp.command("trust").description("Mark a server trusted so its tools run without per-call approval").argument("<id>", "MCP server ID").option("--revoke", "revoke trust instead of granting it").action(async (id, opts, cmd) => {
1580
+ const trusted = !opts.revoke;
1581
+ await getAppClientFromCommand(cmd).mcpServers.setTrusted({
1582
+ id,
1583
+ trusted
1584
+ });
1585
+ process.stdout.write(`${styleText("green", "✓")} MCP server ${id} ${trusted ? "trusted" : "untrusted"}.\n`);
1586
+ });
1587
+ mcp.command("disconnect").description("Disconnect (delete) an MCP server").argument("<id>", "MCP server ID").option("--yes", "disconnect without confirmation").action(async (id, opts, cmd) => {
1588
+ if (!opts.yes) throw new ValidationError("Disconnecting an MCP server is explicit. Re-run with \"--yes\" to confirm.", []);
1589
+ await getAppClientFromCommand(cmd).mcpServers.delete({ id });
1590
+ process.stdout.write(`${styleText("green", "✓")} MCP server ${id} disconnected.\n`);
1591
+ });
1592
+ }
1593
+ //#endregion
1594
+ //#region src/commands/meetings.ts
1595
+ const ORDERS = ["asc", "desc"];
1596
+ /** Reject a typo instead of letting it fall back to the API default silently. */
1597
+ function parseOrder(order) {
1598
+ if (ORDERS.includes(order)) return order;
1599
+ throw new ValidationError(`--order must be one of: ${ORDERS.join(", ")}`, []);
1600
+ }
1601
+ /**
1602
+ * A bound the API reads as a number: `Number("soon")` is `NaN`, which serializes
1603
+ * to `null` in JSON and would reach the server as an absent bound rather than as
1604
+ * the mistake it is.
1605
+ */
1606
+ function parseCount(flag, value) {
1607
+ const parsed = Number(value);
1608
+ if (!Number.isInteger(parsed) || parsed < 0) throw new ValidationError(`${flag} must be a non-negative integer`, []);
1609
+ return parsed;
1610
+ }
1611
+ /** Build the `MeetingsQuery` body every meetings route accepts. Absent flags stay absent. */
1612
+ function buildQuery(opts) {
1613
+ const query = {};
1614
+ if (opts.search !== void 0) query.search = opts.search;
1615
+ if (opts.since !== void 0) query.since = opts.since;
1616
+ if (opts.before !== void 0) query.before = opts.before;
1617
+ if (opts.addresses !== void 0) query.addresses = parseCsvList(opts.addresses);
1618
+ if (opts.order !== void 0) query.order = parseOrder(opts.order);
1619
+ if (opts.limit !== void 0) query.limit = parseCount("--limit", opts.limit);
1620
+ if (opts.offset !== void 0) query.offset = parseCount("--offset", opts.offset);
1621
+ return query;
1622
+ }
1623
+ /** `--auto` clears the override; `--force` / `--skip` set it. Exactly one flag. */
1624
+ function parseOverride(opts) {
1625
+ const chosen = RECORDING_OVERRIDES.filter((override) => opts[override]);
1626
+ if (opts.auto) chosen.push(null);
1627
+ if (chosen.length !== 1) throw new ValidationError("record needs exactly one of --force, --skip or --auto", []);
1628
+ return chosen[0] ?? null;
1629
+ }
1630
+ /** The query flags `list` and `for-record` share — both take the same body shape. */
1631
+ function withQueryOptions(command) {
1632
+ 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");
1633
+ }
1634
+ function registerMeetingsCommand(program) {
1635
+ const meetings = program.command("meetings").description("Read calendar meetings and the records each one concerns");
1636
+ withQueryOptions(meetings.command("list").description("List meetings, newest window first by default")).action(async (opts, cmd) => {
1637
+ formatOutput(await getAppClientFromCommand(cmd).meetings.search(buildQuery(opts)), getFormat(cmd));
1638
+ });
1639
+ meetings.command("get").description("Get one meeting, with its participants and linked records").argument("<id>", "meeting ID").action(async (id, _opts, cmd) => {
1640
+ formatOutput(await getAppClientFromCommand(cmd).meetings.get({ meetingId: id }), getFormat(cmd));
1641
+ });
1642
+ withQueryOptions(meetings.command("for-record").description("List the meetings a record's email addresses appear in").argument("<recordId>", "record ID").option("--email-attributes <ids>", "comma-separated email attribute IDs to match participants on")).action(async (recordId, opts, cmd) => {
1643
+ formatOutput(await getAppClientFromCommand(cmd).meetings.forRecord({
1644
+ recordId,
1645
+ ...opts.emailAttributes !== void 0 && { emailAttributeIds: parseCsvList(opts.emailAttributes) },
1646
+ query: buildQuery(opts)
1647
+ }), getFormat(cmd));
1648
+ });
1649
+ meetings.command("transcript").description("Read a meeting's completed transcript").argument("<id>", "meeting ID").action(async (id, _opts, cmd) => {
1650
+ formatOutput(await getAppClientFromCommand(cmd).meetings.transcript({ meetingId: id }), getFormat(cmd));
1651
+ });
1652
+ 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) => {
1653
+ const override = parseOverride(opts);
1654
+ formatOutput(await getAppClientFromCommand(cmd).meetings.setRecording({
1655
+ meetingId: id,
1656
+ override
1657
+ }), getFormat(cmd));
1658
+ });
1659
+ meetings.command("grants-recompute").description("Recompute the attendee grants of every private meeting of the tenant").action(async (_opts, cmd) => {
1660
+ const tenant = requireTenant(cmd, "A meeting grants recompute names its tenant explicitly. Pass \"--tenant <id>\".");
1661
+ await getAppClientFromCommand(cmd).meetingsAdmin.recomputeGrants({ tenantId: tenant });
1662
+ process.stdout.write(`${styleText("green", "✓")} Meeting grants recompute accepted for tenant ${tenant}.\n`);
1663
+ });
1664
+ }
1665
+ //#endregion
1666
+ //#region src/commands/memory.ts
1667
+ function registerMemoryCommand(program) {
1668
+ const memory = program.command("memory").description("Maintain agent memory");
1669
+ 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) => {
1670
+ 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.", []);
1671
+ const tenant = requireTenant(cmd, "A memory backfill names its tenant explicitly. Pass \"--tenant <id>\" with the tenant you are authenticated in.");
1672
+ await getAppClientFromCommand(cmd).memoryAdmin.backfill({ tenantId: tenant });
1673
+ process.stdout.write(`${styleText("green", "✓")} Memory backfill queued for tenant ${tenant}. It runs in the background — follow the server logs for progress.\n`);
1674
+ });
1675
+ memory.command("activity").description("Compute the tenant's weekly activity digest now (observed practices)").option("--yes", "run without confirmation").action(async (opts, cmd) => {
1676
+ if (!opts.yes) throw new ValidationError("The activity digest reads the last 30 days of the workspace's activity and spends one model call per active person. Re-run with \"--yes\" to confirm.", []);
1677
+ const tenant = requireTenant(cmd, "The activity digest names its tenant explicitly. Pass \"--tenant <id>\" with the tenant you are authenticated in.");
1678
+ await getAppClientFromCommand(cmd).memoryAdmin.activity({ tenantId: tenant });
1679
+ process.stdout.write(`${styleText("green", "✓")} Activity digest queued for tenant ${tenant}. The digest runs in the background.\n`);
1680
+ });
1681
+ }
1682
+ //#endregion
1683
+ //#region src/drift/reconciliation-prompt.ts
1684
+ function renderReconciliationPrompt(state) {
1685
+ const lines = [
1686
+ "# Standards schema reconciliation",
1687
+ "",
1688
+ "Compare deployed source with shared runtime. This excludes unbuilt local TypeScript and personal preferences.",
1689
+ "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.",
1690
+ "Use content with detailView/listView and .tab or .aside for view contents. Reading this prompt changes no files or workspace state.",
1691
+ ""
1692
+ ];
1693
+ const { changes } = diffSchemaState(state);
1694
+ if (changes.length === 0) lines.push("No differences.");
1695
+ 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), "```", "");
1696
+ return `${lines.join("\n").trimEnd()}\n`;
1697
+ }
1698
+ //#endregion
1699
+ //#region src/commands/pull.ts
1700
+ function registerPullCommand(program) {
1701
+ 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) => {
1702
+ let code = 0;
1703
+ try {
1704
+ const state = await readSchemaState(getAppClientFromCommand(cmd));
1705
+ console.info(opts.json ? prettyCanonical(state) : renderReconciliationPrompt(state).trimEnd());
1706
+ } catch (error) {
1707
+ console.error(styleText("red", `✗ ${messageOf(error)}`));
1708
+ code = 2;
1709
+ }
1710
+ process.exit(code);
1711
+ });
1712
+ }
1713
+ //#endregion
1714
+ //#region src/commands/quotas.ts
1715
+ function registerQuotasCommand(program) {
1716
+ 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) => {
1717
+ formatOutput(await getAppClientFromCommand(cmd).quotas.list({}), getFormat(cmd));
1718
+ });
1719
+ }
1720
+ //#endregion
1721
+ //#region src/commands/records.ts
1722
+ function parseSortFlag(sort) {
1723
+ const [attribute, direction = "asc"] = sort.split(":");
1724
+ return [{
1725
+ attribute,
1726
+ direction
1727
+ }];
1728
+ }
1729
+ const COUNT_MODES = [
1730
+ "none",
1731
+ "estimated",
1732
+ "exact"
1733
+ ];
1734
+ /**
1735
+ * The API opts out of totals by default: an absent or unrecognized `countMode`
1736
+ * yields `page.total: null`. Reject unknown modes here so a typo surfaces as an
1737
+ * error instead of a silently missing total.
1738
+ */
1739
+ function parseCountFlag(count) {
1740
+ if (COUNT_MODES.includes(count)) return count;
1741
+ throw new Error(`Invalid --count value "${count}". Expected one of: ${COUNT_MODES.join(", ")}.`);
1742
+ }
1743
+ /** Build the shared list/search request body from common query flags. */
1744
+ function buildQueryBody(opts) {
1745
+ const body = {};
1746
+ if (opts.limit) body.limit = Number(opts.limit);
1747
+ if (opts.offset) body.offset = Number(opts.offset);
1748
+ if (opts.sort) body.sorts = parseSortFlag(opts.sort);
1749
+ if (opts.filter) body.filters = JSON.parse(opts.filter);
1750
+ if (opts.fields !== void 0) body.fields = parseCsvList(opts.fields);
1751
+ if (opts.count) body.countMode = parseCountFlag(opts.count);
1752
+ return body;
1753
+ }
1754
+ const COUNT_OPTION_DESCRIPTION = "total count mode (none, estimated, exact); omit for no total in page.total";
1755
+ function registerRecordsCommand(program) {
1756
+ const records = program.command("records").description("Manage records (CRUD + search)");
1757
+ records.command("list").description("List records for an object").argument("<object>", "object name (e.g. contacts)").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) => {
1758
+ const body = buildQueryBody(opts);
1759
+ formatOutput(await getAppClientFromCommand(cmd).records.list({
1760
+ objectName,
1761
+ ...body
1762
+ }), getFormat(cmd));
1763
+ });
1764
+ records.command("get").description("Get a record by ID").argument("<object>", "object name").argument("<id>", "record ID").action(async (objectName, id, _opts, cmd) => {
1765
+ formatOutput(await getAppClientFromCommand(cmd).records.get({
1766
+ objectName,
1767
+ id
1768
+ }), getFormat(cmd));
1769
+ });
1770
+ records.command("create").description("Create a new record").argument("<object>", "object name").option("--data <json>", "record data as JSON string").action(async (objectName, opts, cmd) => {
1771
+ const data = opts.data ? JSON.parse(opts.data) : {};
1772
+ formatOutput(await getAppClientFromCommand(cmd).records.create({
1773
+ objectName,
1774
+ data
1775
+ }), getFormat(cmd));
1776
+ });
1777
+ 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) => {
1778
+ const data = opts.data ? JSON.parse(opts.data) : {};
1779
+ formatOutput(await getAppClientFromCommand(cmd).records.update({
1780
+ objectName,
1781
+ id,
1782
+ values: data
1783
+ }), getFormat(cmd));
1784
+ });
1785
+ records.command("delete").description("Delete a record").argument("<object>", "object name").argument("<id>", "record ID").action(async (objectName, id, _opts, cmd) => {
1786
+ await getAppClientFromCommand(cmd).records.delete({
1787
+ objectName,
1788
+ id
1789
+ });
1790
+ process.stdout.write(`Record ${id} deleted.\n`);
1791
+ });
1792
+ 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) => {
1793
+ const body = {
1794
+ q: opts.query,
1795
+ ...buildQueryBody(opts)
1796
+ };
1797
+ formatOutput(await getAppClientFromCommand(cmd).records.search({
1798
+ objectName,
1799
+ ...body
1800
+ }), getFormat(cmd));
1801
+ });
1802
+ records.command("export").description("Export records as CSV (filters, sort and columns like the app)").argument("<object>", "object name").requiredOption("--columns <list>", "comma-separated attribute names, in order").option("--query <q>", "full-text query").option("--filter <json>", "filter state as JSON string").option("--sort <attr:dir>", "sort, e.g. name:asc").option("--timezone <zone>", "IANA time zone dates are written in (default: this machine's)").option("-o, --output <file>", "write to a file instead of stdout").action(async (objectName, opts, cmd) => {
1803
+ const client = getClientFromCommand(cmd);
1804
+ const { filters, sorts } = buildQueryBody({
1805
+ filter: opts.filter,
1806
+ sort: opts.sort
1807
+ });
1808
+ const response = await client.postRaw(buildOperationPath(recordsOperations.export, { objectName }), {
1809
+ q: opts.query ?? "",
1810
+ columns: parseCsvList(opts.columns),
1811
+ ...filters ? { filters } : {},
1812
+ ...sorts ? { sorts } : {},
1813
+ timezone: opts.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone
1814
+ });
1815
+ if (!response.body) throw new StandardsRequestError(response.status, "The export response had no body");
1816
+ const source = Readable.fromWeb(response.body);
1817
+ if (!opts.output) return await pipeline(source, process.stdout);
1818
+ try {
1819
+ await pipeline(source, createWriteStream(opts.output));
1820
+ } catch (error) {
1821
+ await unlink(opts.output).catch(() => void 0);
1822
+ throw error;
1823
+ }
1824
+ });
1825
+ records.command("attach-document").description("Upload and attach a local file to a record").argument("<object>", "object name (e.g. contacts)").argument("<recordId>", "record ID").requiredOption("--file <path>", "local file path to upload").option("--attribute <attr>", "attribute name to attach the document to").option("--parent-id <folderId>", "parent folder ID").option("--title <title>", "document title (defaults to filename)").action(async (objectName, recordId, opts, cmd) => {
1826
+ const fileContent = await readFile(opts.file);
1827
+ const fileName = basename(opts.file);
1828
+ formatOutput(await getAppClientFromCommand(cmd).records.attachDocument({
1829
+ objectName,
1830
+ id: recordId,
1831
+ title: opts.title ?? fileName,
1832
+ ...opts.attribute ? { attributeName: opts.attribute } : {},
1833
+ ...opts.parentId ? { parentId: opts.parentId } : {},
1834
+ files: new File([fileContent], fileName)
1835
+ }), getFormat(cmd));
1836
+ });
1837
+ const access = records.command("access").description("Share a private record with other actors (readers and editors)");
1838
+ 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) => {
1839
+ formatOutput(await getAppClientFromCommand(cmd).recordAccess.list({
1840
+ objectName,
1841
+ recordId: id
1842
+ }), getFormat(cmd));
1843
+ });
1844
+ access.command("grant").description("Share a private record you own or edit with an actor").argument("<object>", "object name").argument("<id>", "record ID").argument("<actorId>", "actor ID to share with").requiredOption("--role <role>", `access role (${RECORD_ACCESS_ROLES.join(", ")})`).action(async (objectName, id, actorId, opts, cmd) => {
1845
+ const role = parseRecordAccessRole(opts.role);
1846
+ formatOutput(await getAppClientFromCommand(cmd).recordAccess.grant({
1847
+ objectName,
1848
+ recordId: id,
1849
+ actorId,
1850
+ role
1851
+ }), getFormat(cmd));
1852
+ });
1853
+ access.command("revoke").description("Remove your manual share of a private record").argument("<object>", "object name").argument("<id>", "record ID").argument("<actorId>", "actor ID to stop sharing with").action(async (objectName, id, actorId, _opts, cmd) => {
1854
+ formatOutput(await getAppClientFromCommand(cmd).recordAccess.revoke({
1855
+ objectName,
1856
+ recordId: id,
1857
+ actorId
1858
+ }), getFormat(cmd));
1859
+ });
1860
+ access.command("clear").description("Make a shared record private again: remove every manual share").argument("<object>", "object name").argument("<id>", "record ID").action(async (objectName, id, _opts, cmd) => {
1861
+ formatOutput(await getAppClientFromCommand(cmd).recordAccess.clear({
1862
+ objectName,
1863
+ recordId: id
1864
+ }), getFormat(cmd));
1865
+ });
1866
+ }
1867
+ //#endregion
1868
+ //#region src/commands/root.ts
1869
+ async function promptForMissingValue(label) {
1870
+ const rl = createInterface({
1871
+ input: stdin,
1872
+ output: stdout
1873
+ });
1874
+ try {
1875
+ return (await rl.question(`${label}: `)).trim();
1876
+ } finally {
1877
+ rl.close();
1878
+ }
1879
+ }
1880
+ function registerRootCommands(program) {
1881
+ 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) => {
1882
+ const apiKey = opts.key ?? await promptForMissingValue("API key");
1883
+ if (!apiKey) throw new Error("API key is required");
1884
+ await createAppClient(createClient({
1885
+ apiUrl: opts.url,
1886
+ apiKey
1887
+ })).apiKeys.list({});
1888
+ await upsertProfile({
1889
+ name: opts.name,
1890
+ apiUrl: opts.url,
1891
+ apiKey
1892
+ });
1893
+ process.stdout.write(`${styleText("green", "✓")} Standards instance "${opts.name}" saved and selected.\n`);
1894
+ process.stdout.write(` API URL: ${opts.url}\n`);
1895
+ process.stdout.write(` API Key: ${apiKey.slice(0, 8)}...\n`);
1896
+ process.stdout.write(` Use it with: standards --instance ${opts.name} <command>\n`);
1897
+ });
1898
+ program.command("use").description("Select the active Standards instance").argument("<name>", "instance name").action(async (name) => {
1899
+ await setCurrentProfile(name);
1900
+ process.stdout.write(`${styleText("green", "✓")} Standards instance "${name}" selected.\n`);
1901
+ });
1902
+ program.command("instances").description("List configured Standards instances").action(async (_opts, cmd) => {
1903
+ formatOutput(await listProfiles(), getFormat(cmd));
1904
+ });
1905
+ program.command("current").description("Show the active Standards instance").action(async () => {
1906
+ const profile = await getActiveProfile();
1907
+ if (!profile) {
1908
+ process.stdout.write(`No Standards instance selected. Run "standards login".\n`);
1909
+ return;
1910
+ }
1911
+ process.stdout.write(`${profile.name} ${profile.apiUrl}\n`);
1912
+ });
1913
+ program.command("logout").description("Remove a Standards instance from local CLI config").argument("[name]", "instance name, defaults to current").action(async (name) => {
1914
+ await removeProfile(name);
1915
+ process.stdout.write(`${styleText("green", "✓")} Standards instance removed.\n`);
1916
+ });
1917
+ }
1918
+ //#endregion
1919
+ //#region src/commands/schema.ts
1920
+ function registerSchemaCommand(program) {
1921
+ const schema = program.command("schema").description("Inspect schema objects and attributes");
1922
+ schema.command("list").description("List all schema objects").action(async (_opts, cmd) => {
1923
+ formatOutput(await getAppClientFromCommand(cmd).schemaObjects.list({}), getFormat(cmd));
1924
+ });
1925
+ schema.command("inspect").description("Inspect an object (attributes, types, views)").argument("<object>", "object name or ID").action(async (objectName, _opts, cmd) => {
1926
+ formatOutput(await getAppClientFromCommand(cmd).schemaObjects.get({ objectIdOrName: objectName }), getFormat(cmd));
1927
+ });
1928
+ schema.command("groups").description("List the bundles and schema sources that own objects, with label and icon").action(async (_opts, cmd) => {
1929
+ formatOutput(await getAppClientFromCommand(cmd).schemaGroups.list({}), getFormat(cmd));
1930
+ });
1931
+ }
1932
+ //#endregion
1933
+ //#region src/commands/search.ts
1934
+ function registerSearchCommand(program) {
1935
+ 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) => {
1936
+ if (opts.afterId !== void 0 && !opts.emails) throw new ValidationError("\"--after-id\" resumes an email reindex. Pass it with \"--emails\".", []);
1937
+ if (opts.rebuild && !opts.emails) throw new ValidationError("\"--rebuild\" rebuilds the mail search index. Pass it with \"--emails\".", []);
1938
+ 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.", []);
1939
+ if (opts.emails) {
1940
+ const tenant = requireTenant(cmd, "An email reindex names its tenant explicitly. Pass \"--tenant <id>\" with the tenant you are authenticated in.");
1941
+ if (opts.rebuild && !opts.yes) throw new ValidationError("A mail rebuild re-reads every stored email. Re-run with \"--yes\" to confirm.", []);
1942
+ await getAppClientFromCommand(cmd).searchAdmin.emailReindex({
1943
+ tenantId: tenant,
1944
+ ...opts.afterId !== void 0 && { afterId: opts.afterId },
1945
+ ...opts.rebuild && { rebuild: true }
1946
+ });
1947
+ 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`);
1948
+ return;
1949
+ }
1950
+ if (!opts.yes) throw new ValidationError("A rebuild re-reads every record of the tenant. Re-run with \"--yes\" to confirm.", []);
1951
+ const tenant = requireTenant(cmd, "A rebuild names its tenant explicitly. Pass \"--tenant <id>\" with the tenant you are authenticated in.");
1952
+ await getAppClientFromCommand(cmd).searchAdmin.fullReindex({ tenantId: tenant });
1953
+ 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`);
1954
+ });
1955
+ }
1956
+ //#endregion
1957
+ //#region src/commands/sources.ts
1958
+ /** Remote schema sources — definitions pushed by an external codebase through @standards/client. */
1959
+ function registerSourcesCommand(program) {
1960
+ const sources = program.command("sources").description("Inspect and remove remote schema sources on the current tenant");
1961
+ sources.command("list").description("List remote schema sources: id, hash, last apply, object names").action(async (_opts, cmd) => {
1962
+ formatOutput(await getAppClientFromCommand(cmd).schemaSources.list({}), getFormat(cmd));
1963
+ });
1964
+ sources.command("remove").description("Remove a remote schema source: its objects are demoted to custom, records are kept").argument("<sourceId>", "source id declared in code").option("--yes", "remove without confirmation").action(async (sourceId, opts, cmd) => {
1965
+ if (!opts.yes) throw new Error("Removing a schema source is explicit. Re-run with \"--yes\" to confirm.");
1966
+ await getAppClientFromCommand(cmd).schemaSources.remove({ sourceId });
1967
+ formatOutput({ removed: sourceId }, getFormat(cmd));
1968
+ });
1969
+ }
1970
+ //#endregion
1971
+ //#region src/version.ts
1972
+ /**
1973
+ * The installed package's version, read from its own manifest: `src/` and the
1974
+ * built `dist/` both sit one level under it, and npm always ships
1975
+ * `package.json`. A version literal in `program.ts` went stale for 209
1976
+ * releases (#4314).
1977
+ *
1978
+ * No try/catch, unlike `adapter-posthog`'s `packageVersion()`: a missing
1979
+ * manifest here is a packaging bug, and failing loudly keeps it visible to the
1980
+ * sandbox e2e that runs `dist/bin.mjs`. An `"unknown"` would hide it.
1981
+ */
1982
+ function cliVersion() {
1983
+ const manifest = createRequire(import.meta.url)("../package.json");
1984
+ return typeof manifest === "object" && manifest !== null && "version" in manifest && typeof manifest.version === "string" ? manifest.version : "unknown";
1985
+ }
1986
+ //#endregion
1987
+ //#region src/program.ts
1988
+ const PUBLIC_COMMANDS = /* @__PURE__ */ new Set([
1989
+ "login",
1990
+ "use",
1991
+ "instances",
1992
+ "current",
1993
+ "logout",
1994
+ "help"
1995
+ ]);
1996
+ function isPublicCommand(actionCommand) {
1997
+ if (PUBLIC_COMMANDS.has(actionCommand.name())) return true;
1998
+ return actionCommand.parent?.name() === "auth" && actionCommand.name() === "help";
1999
+ }
2000
+ function createProgram() {
2001
+ const program = new Command();
2002
+ program.name("standards").description("CLI to interact with Standards API").version(cliVersion()).option("--format <format>", "output format (json, table, csv)", "json").option("--api-url <url>", "API base URL").option("--api-key <key>", "API key for authentication").option("--instance <name>", "Standards instance profile to use").option("--tenant <id>", "Tenant ID (multi-tenant setups, defaults to primary tenant)");
2003
+ registerRootCommands(program);
2004
+ registerRecordsCommand(program);
2005
+ registerSchemaCommand(program);
2006
+ registerPullCommand(program);
2007
+ registerDiffCommand(program);
2008
+ registerDocumentsCommand(program);
2009
+ registerDeviceCommand(program);
2010
+ registerFoldersCommand(program);
2011
+ registerKeysCommand(program);
2012
+ registerInvitationsCommand(program);
2013
+ registerAuthCommand(program);
2014
+ registerBundlesCommand(program);
2015
+ registerSourcesCommand(program);
2016
+ registerCodemodCommand(program);
2017
+ registerConnectorsCommand(program);
2018
+ registerMcpCommand(program);
2019
+ registerMeetingsCommand(program);
2020
+ registerMemoryCommand(program);
2021
+ registerSearchCommand(program);
2022
+ registerEmbeddingsCommand(program);
2023
+ registerAiCommand(program);
2024
+ registerQuotasCommand(program);
2025
+ program.hook("preAction", async (_thisCommand, actionCommand) => {
2026
+ const raw = program.opts();
2027
+ const resolved = await resolveCliConfig({
2028
+ apiUrl: raw.apiUrl,
2029
+ apiKey: raw.apiKey,
2030
+ instance: raw.instance
2031
+ });
2032
+ program.setOptionValue("apiUrl", resolved.apiUrl);
2033
+ program.setOptionValue("apiKey", resolved.apiKey);
2034
+ program.setOptionValue("profileName", resolved.profileName);
2035
+ program.setOptionValue("tenant", raw.tenant);
2036
+ if (!(resolved.apiKey || isPublicCommand(actionCommand))) {
2037
+ console.error(styleText("red", `✗ Error: No Standards instance configured. Run "standards login" or pass --api-key.`));
2038
+ process.exit(2);
2039
+ }
2040
+ });
2041
+ return program;
2042
+ }
2043
+ async function runProgram(argv = process.argv) {
2044
+ await createProgram().parseAsync(argv).catch((error) => {
2045
+ if (error instanceof StandardsRequestError) {
2046
+ if (error.status > 0) console.error(styleText("red", `✗ Error (${error.status}): ${error.message}`));
2047
+ else console.error(styleText("red", `✗ Error: ${error.message}`));
2048
+ } else if (error instanceof Error) console.error(styleText("red", `✗ Error: ${error.message}`));
2049
+ process.exit(1);
2050
+ });
2051
+ }
2052
+ //#endregion
2053
+ //#region src/bin.ts
2054
+ await runProgram();
2055
+ //#endregion
2056
+ export {};