@onesi/genesis-cli 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  `genesis`, the OneOS command line. It works against the Genesis API at `https://os.one.ml/api`.
4
4
 
5
5
  ```sh
6
- npx -y @onesi/genesis-cli@0.2.0 --help
6
+ npx -y @onesi/genesis-cli@0.2.2 --help
7
7
  ```
8
8
 
9
9
  ## First run
package/dist/flocks.js CHANGED
@@ -91,13 +91,20 @@ export function registerFlocks(program, client, emit, env = process.env, readCod
91
91
  // Account, grant, instance and job commands redact secret-looking fields; enrollment commands (join-token) must print theirs.
92
92
  const showSafe = (run, pretty) => show(async (g) => scrub(await run(g)), pretty);
93
93
  flocks.command('readiness').description('Check whether node enrollment is currently available').action(() => show(g => g.flocks.readiness.get()));
94
- flocks.command('nodes').description('List your org nodes').action(() => show(g => g.flocks.nodes.list()));
94
+ const nodes = flocks.command('nodes').description('List your org nodes; `nodes activity <id>` shows one node').action(() => show(g => g.flocks.nodes.list()));
95
95
  const paging = (command) => command.option('--limit <n>', 'page size, 1–100').option('--cursor <cursor>', 'next_cursor from the previous page');
96
96
  const query = (opts) => ({
97
97
  ...(opts.limit === undefined ? {} : { limit: Number(opts.limit) }),
98
98
  ...(opts.cursor === undefined ? {} : { cursor: opts.cursor }),
99
99
  ...(opts.agentId === undefined ? {} : { agent_id: opts.agentId }),
100
100
  });
101
+ paging(nodes.command('activity <id>').description("One node's presence (online, heartbeat age, harnesses) and the runs recorded on it, newest first"))
102
+ .action((id, opts) => show(g => g.flocks.nodes.activity(id, query(opts)), (a) => [
103
+ `${safe(a.node.node_id)} ${safe(a.node.nickname || a.node.name)} ${safe(a.node.platform)} ${a.node.online ? 'online' : 'offline'} ${safe(a.node.state)} heartbeat ${a.node.heartbeat_age_seconds}s ago`,
104
+ a.node.harnesses?.length ? 'harnesses ' + a.node.harnesses.map((h) => `${safe(h.name)}${h.version ? ' ' + safe(h.version) : ''}${h.ready ? '' : ' (not ready)'}`).join(', ') : 'no harnesses reported',
105
+ a.items.length ? table(a.items, ['id', 'state', 'agent_id', 'harness', 'created_at', 'error_code']) : 'no runs on this node',
106
+ a.next_cursor ? `More: --cursor ${safe(a.next_cursor)}` : '',
107
+ ].filter(Boolean).join('\n')));
101
108
  paging(flocks.command('harnesses').description('List the harnesses each node reports, with version and readiness'))
102
109
  .action((opts) => show(g => g.flocks.harnesses.list(query(opts))));
103
110
  const agents = flocks.command('agents').description('Harness agents, and canonical agent execution (a_<ULID>; needs a signed-in session, not an API key)');
@@ -118,6 +125,20 @@ export function registerFlocks(program, client, emit, env = process.env, readCod
118
125
  paging(runs.command('list').description('List runs').option('--agent-id <id>', 'only runs of this agent'))
119
126
  .action((opts) => show(g => g.flocks.runs.list(query(opts))));
120
127
  runs.command('get <run_id>').action((id) => show(g => g.flocks.runs.get(id)));
128
+ runs.command('create <agent_id>').description('Start a run of an agent; on an instance with a bound inference account it is dispatched with the next heartbeat')
129
+ .requiredOption('--task <text>', 'what the agent should do (1-4096 bytes)').option('--node <id>', 'run on this node instead of the agent\'s pinned one')
130
+ .option('--wait', 'follow the run until it completes or fails; exits non-zero if it failed')
131
+ .action(async (id, o) => {
132
+ const g = await client();
133
+ let run = await g.flocks.runs.create(id, { task: o.task, ...(o.node ? { node_id: o.node } : {}) });
134
+ if (o.wait)
135
+ run = await g.flocks.runs.wait(run.id, { intervalMs: Number(env.FLOCKS_POLL_INTERVAL_MS) > 0 ? Number(env.FLOCKS_POLL_INTERVAL_MS) : 2000 });
136
+ emit(run, () => [`${safe(run.id)} ${safe(run.state)} agent ${safe(run.agent_id)} harness ${safe(run.harness)} node ${safe(run.node_id ?? '')}`,
137
+ run.inference_account_id ? `account ${safe(run.inference_account_id)} (owner ${safe(run.account_owner_org)}, billed to ${safe(run.grantee_org)})` : '',
138
+ run.run_seconds != null ? `${run.run_seconds}s` : '', run.error_code ? `error ${safe(run.error_code)}` : ''].filter(Boolean).join('\n'));
139
+ if (o.wait && run.state === 'failed')
140
+ throw new Error(`Run ${safe(run.id)} failed${run.error_code ? ': ' + safe(run.error_code) : ''}.`);
141
+ });
121
142
  flocks.command('join-token').option('--ttl <seconds>', 'lifetime (60–600 seconds)', '600').action((opts) => {
122
143
  const ttl = Number(opts.ttl);
123
144
  if (!Number.isInteger(ttl) || ttl < 60 || ttl > 600)
@@ -245,6 +266,8 @@ function registerAccounts(flocks, client, show, emit, env, readCode, isJson) {
245
266
  }
246
267
  emit(job, () => jobText(job) + (job.state === 'running' ? `\nFinish with: genesis flocks jobs get ${safe(job.id)} --wait` : ''));
247
268
  });
269
+ accounts.command('reset-identity <account_id>').description('Clear the identity recorded for an account (owner only); the next verified switch on one of your own instances records it again')
270
+ .action((id) => show(g => g.flocks.accounts.resetIdentity(id), a => accountRows([a])));
248
271
  accounts.command('remove <account_id>').description('Sign the account out of every bound instance, then revoke its credential')
249
272
  .option('--wait', 'follow the logout job to the end')
250
273
  .action((id, o) => show(async (g) => withJobId(g, await g.flocks.accounts.delete(id), o.wait), r => `account ${safe(r.account?.id)} ${safe(r.account?.status)}; logout job ${safe(r.job_id)}` + (r.job ? '\n' + jobText(r.job) : '')));
@@ -1,5 +1,6 @@
1
1
  import { hivesSseWindow, idempotencyHeaders, operationTable } from '@onesi/genesis';
2
2
  import { readFunctionJson } from './function-input.js';
3
+ import { bytes } from './hives-resources.js';
3
4
  /** `genesis hives <words>` -> Hives operation. Positional arguments are the path parameters, query
4
5
  * parameters become options, a JSON body comes from --body-file. Groups that already exist
5
6
  * (functions, deployments, environment, domains, logs) are extended, never replaced. */
@@ -19,9 +20,12 @@ export const HIVES_COVERAGE_COMMANDS = {
19
20
  'environment values': 'hives_get_project_env',
20
21
  'environment set': 'hives_set_project_env',
21
22
  'environment delete': 'hives_delete_project_env',
23
+ 'environment delete-key': 'hives_delete_project_env_key',
22
24
  'domains get': 'hives_get_domain',
23
25
  'domains delete': 'hives_delete_domain',
24
26
  'domains verification-record': 'hives_get_verification_record',
27
+ 'cells objects put-download': 'hives_upload_object_named_download',
28
+ 'cells objects remove-download': 'hives_delete_object_named_download',
25
29
  };
26
30
  const DESCRIPTIONS = {
27
31
  flights: 'Flights: WASM HTTP workloads served at their own address',
@@ -71,6 +75,19 @@ export function registerHivesCoverage(hives, deps) {
71
75
  .action(async (project, names) => output(await (await deps.client()).request(id, { project_id: project, body: names })));
72
76
  continue;
73
77
  }
78
+ if (id === 'hives_upload_object_named_download') {
79
+ // Raw object bytes from a file, through the same Cells checks as `cells objects put-flat`.
80
+ parent.command(`${parts.at(-1)} <project_id> <bucket_id>`).description(`${op.summary} (${op.method} ${op.path})`)
81
+ .requiredOption('--file <path>', 'Exact binary bytes up to 10 MiB').option('--content-type <value>')
82
+ .option('--idempotency-key <key>', '1-128 printable ASCII; reuse it only to replay the same write (a fresh key is sent when omitted)')
83
+ .action(async (project, bucket, o) => output(await (await deps.client()).hives.cells.objects.putDownload(project, bucket, await bytes(o.file), { content_type: o.contentType, idempotencyKey: o.idempotencyKey })));
84
+ continue;
85
+ }
86
+ if (id === 'hives_delete_object_named_download') {
87
+ parent.command(`${parts.at(-1)} <project_id> <bucket_id>`).description(`${op.summary} (${op.method} ${op.path})`)
88
+ .action(async (project, bucket) => { await (await deps.client()).hives.cells.objects.removeDownload(project, bucket); output({ acknowledged: true }); });
89
+ continue;
90
+ }
74
91
  const command = parent.command([parts.at(-1), ...op.pathParams.map((p) => `<${p}>`)].join(' '))
75
92
  .description(`${op.summary} (${op.method} ${op.path})`);
76
93
  for (const q of op.queryParams) {
@@ -11,12 +11,12 @@ export async function closeHivesNodeTransport() {
11
11
  await current?.destroy();
12
12
  }
13
13
  export function hivesOperationBudget(id) {
14
- if (['hives_list_buckets', 'hives_create_bucket', 'hives_get_bucket', 'hives_delete_bucket', 'hives_list_objects', 'hives_upload_object', 'hives_upload_object_by_key', 'hives_delete_object', 'hives_delete_object_by_key', 'hives_download_object_by_key', 'hives_get_cors_config', 'hives_set_cors_config', 'hives_generate_presigned_url', 'hives_reconcile'].includes(id))
14
+ if (['hives_list_buckets', 'hives_create_bucket', 'hives_get_bucket', 'hives_delete_bucket', 'hives_list_objects', 'hives_upload_object', 'hives_upload_object_by_key', 'hives_upload_object_named_download', 'hives_delete_object', 'hives_delete_object_by_key', 'hives_delete_object_named_download', 'hives_download_object_by_key', 'hives_get_cors_config', 'hives_set_cors_config', 'hives_generate_presigned_url', 'hives_reconcile'].includes(id))
15
15
  return 100_000;
16
16
  if (id === 'hives_cancel_deployment' || id === 'hives_delete_deployment' || id === 'hives_stop_deployment')
17
17
  return 330_000;
18
18
  // A live SSE subscription is bounded by its caller's window or --follow limit (at most 300 s).
19
- if (id === 'hives_stream_logs')
19
+ if (id === 'hives_stream_logs' || id === 'hives_stream_deployment_logs')
20
20
  return 330_000;
21
21
  if (['hives_create_deployment', 'hives_upload_deployment', 'hives_invoke_deployment', 'hives_invoke_function', 'hives_invoke_function_async'].includes(id))
22
22
  return 40_000;
@@ -5,5 +5,6 @@ type Deps = {
5
5
  emit: (data: unknown, pretty: () => string) => void;
6
6
  readRequest?: (path: string, limit: number) => Promise<Record<string, unknown>>;
7
7
  };
8
+ export declare function bytes(path: string): Promise<Uint8Array>;
8
9
  export declare function registerHivesResources(hives: Command, deps: Deps): void;
9
10
  export {};
@@ -2,7 +2,7 @@ import { registerHivesMarketplace } from './hives-marketplace.js';
2
2
  import { createReadStream } from 'node:fs';
3
3
  import { stat, writeFile } from 'node:fs/promises';
4
4
  import { readFunctionJson } from './function-input.js';
5
- async function bytes(path) {
5
+ export async function bytes(path) {
6
6
  const metadata = await stat(path);
7
7
  if (!metadata.isFile() || metadata.size > 10 * 1024 * 1024)
8
8
  throw new Error('Choose a regular file of at most 10 MiB.');
@@ -23,6 +23,8 @@ async function bytes(path) {
23
23
  }
24
24
  return Uint8Array.from(Buffer.concat(chunks, total));
25
25
  }
26
+ /** Funded writes (topic publish, Cells object writes) carry one Idempotency-Key; without the option a fresh one is sent. */
27
+ const FUNDED_KEY = '1-128 printable ASCII; reuse it only to replay the same write (a fresh key is sent when omitted)';
26
28
  function number(value, min, max) { if (value === undefined)
27
29
  return undefined; if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < min || Number(value) > max)
28
30
  throw new Error('Invalid bounded numeric option.'); return Number(value); }
@@ -43,7 +45,7 @@ export function registerHivesResources(hives, deps) {
43
45
  const objects = cells.command('objects');
44
46
  objects.command('list <project_id> <bucket_id>').option('--prefix <value>').option('--delimiter <value>').option('--continuation-token <value>').option('--max-keys <number>').action(async (project, id, o) => output(await (await deps.client()).hives.cells.objects.list(project, id, { prefix: o.prefix, delimiter: o.delimiter, continuation_token: o.continuationToken, max_keys: number(o.maxKeys, 1, 1000) })));
45
47
  for (const action of ['put', 'put-flat'])
46
- objects.command(`${action} <project_id> <bucket_id> <key>`).requiredOption('--file <path>', 'Exact binary bytes up to 10 MiB').option('--content-type <value>').action(async (project, id, key, o) => { const client = await deps.client(); output(await client.hives.cells.objects[action === 'put' ? 'put' : 'putFlat'](project, id, key, await bytes(o.file), { content_type: o.contentType })); });
48
+ objects.command(`${action} <project_id> <bucket_id> <key>`).requiredOption('--file <path>', 'Exact binary bytes up to 10 MiB').option('--content-type <value>').option('--idempotency-key <key>', FUNDED_KEY).action(async (project, id, key, o) => { const client = await deps.client(); output(await client.hives.cells.objects[action === 'put' ? 'put' : 'putFlat'](project, id, key, await bytes(o.file), { content_type: o.contentType, idempotencyKey: o.idempotencyKey })); });
47
49
  for (const action of ['remove', 'remove-flat'])
48
50
  objects.command(`${action} <project_id> <bucket_id> <key>`).action(async (project, id, key) => { await (await deps.client()).hives.cells.objects[action === 'remove' ? 'remove' : 'removeFlat'](project, id, key); output({ acknowledged: true }); });
49
51
  objects.command('download <project_id> <bucket_id> <key>').requiredOption('--output-file <path>', 'Create a new file; existing files are never overwritten').action(async (project, id, key, o) => { const response = await (await deps.client()).hives.cells.objects.download(project, id, key); const data = new Uint8Array(await response.arrayBuffer()); await writeFile(o.outputFile, data, { flag: 'wx', mode: 0o600 }); output({ saved: true, bytes: data.byteLength }); });
@@ -75,7 +77,7 @@ export function registerHivesResources(hives, deps) {
75
77
  for (const action of ['get', 'remove'])
76
78
  topics.command(`${action} <project_id> <topic_id>`).action(async (project, id) => output(await (await deps.client()).hives.topics[action](project, id)));
77
79
  topics.command('create <project_id>').requiredOption('--request-file <path>', 'JSON topic name').action(async (project, o) => output(await (await deps.client()).hives.topics.create(project, await read(o.requestFile, 4096))));
78
- topics.command('publish <project_id> <topic_id>').requiredOption('--request-file <path>', 'Exactly body or body_base64 plus optional string attributes; no automatic retry').action(async (project, id, o) => output(await (await deps.client()).hives.topics.publish(project, id, await read(o.requestFile, 512 * 1024))));
80
+ topics.command('publish <project_id> <topic_id>').requiredOption('--request-file <path>', 'Exactly body or body_base64 plus optional string attributes; no automatic retry').option('--idempotency-key <key>', FUNDED_KEY).action(async (project, id, o) => output(await (await deps.client()).hives.topics.publish(project, id, await read(o.requestFile, 512 * 1024), { idempotencyKey: o.idempotencyKey })));
79
81
  const subscriptions = topics.command('subscriptions').description('Same-project standard queue destinations; removing a subscription retains queued messages');
80
82
  subscriptions.command('list <project_id> <topic_id>').action(async (project, id) => output(await (await deps.client()).hives.topics.subscriptions.list(project, id)));
81
83
  subscriptions.command('add <project_id> <topic_id> <queue_id>').action(async (project, id, queue) => output(await (await deps.client()).hives.topics.subscriptions.add(project, id, queue)));
package/dist/index.js CHANGED
@@ -7,11 +7,11 @@ import { registerIamDirectory } from './iam-directory.js';
7
7
  import { registerOperationCommands, registerProducts } from './products.js';
8
8
  import { registerOrgs } from './orgs.js';
9
9
  import { Command, CommanderError } from 'commander';
10
- import { Genesis, GenesisError, errorCatalog, errorOutcome, isMindsOperation, HivesOperationError, HivesQueueOperationError, MindsAgentError, MindsOwnerError, hivesResponseBytes, MAX_FUNCTION_BODY_BYTES, MAX_FUNCTION_JSON_BYTES, DEFAULT_BASE_URL, requireSecureUrl } from '@onesi/genesis';
10
+ import { Genesis, GenesisError, errorBody, errorCatalog, errorOutcome, isMindsOperation, HivesOperationError, HivesQueueOperationError, MindsAgentError, MindsOwnerError, hivesResponseBytes, MAX_FUNCTION_BODY_BYTES, MAX_FUNCTION_JSON_BYTES, DEFAULT_BASE_URL, requireSecureUrl } from '@onesi/genesis';
11
11
  import { authConfig, completeLogin, selectSavedContext, savedLoginProvider, startLogin } from './auth.js';
12
12
  import { completeDeviceLogin, startDeviceLogin } from './device-login.js';
13
13
  import { keychainStore } from './store.js';
14
- import { c, listOf, safe, stdio, table } from './output.js';
14
+ import { c, errorText, listOf, printable, redactMessage, safe, stdio, table } from './output.js';
15
15
  import { spawn } from 'node:child_process';
16
16
  import { addHenryCommands } from './henry.js';
17
17
  import { registerFleets } from './fleets.js';
@@ -40,7 +40,7 @@ export function buildProgram(deps = {}) {
40
40
  const auth = { ...authConfig(env, f), sleep: deps.sleep };
41
41
  const program = new Command('genesis')
42
42
  .description('One CLI for every Genesis service.')
43
- .version('0.2.0')
43
+ .version('0.2.2')
44
44
  .option('--json', 'machine-readable JSON output')
45
45
  .option('--api-key <key>', 'use this API key (default: GENESIS_API_KEY, then your `genesis login`)')
46
46
  .option('--base-url <url>', 'gateway URL (default: GENESIS_BASE_URL or https://os.one.ml/api)')
@@ -56,7 +56,7 @@ export function buildProgram(deps = {}) {
56
56
  const opts = {
57
57
  apiKey: o.apiKey ?? env.GENESIS_API_KEY, token: env.GENESIS_TOKEN,
58
58
  baseUrl: o.baseUrl ?? env.GENESIS_BASE_URL, routing: o.direct || env.GENESIS_ROUTING === 'direct' ? 'direct' : 'gateway',
59
- fetch: f, userAgent: 'genesis-cli/0.2.0',
59
+ fetch: f, userAgent: 'genesis-cli/0.2.2',
60
60
  credentials: store ? savedLoginProvider(auth, store) : undefined,
61
61
  };
62
62
  return new Genesis(hivesTransport ? { ...opts, fetch: createHivesNodeTransport(opts, deps.signal) } : opts);
@@ -247,6 +247,12 @@ export function buildProgram(deps = {}) {
247
247
  .action((name, o) => show(b => b.delegates.create({ name, delegated_variables: o.variable, ttl_seconds: Number(o.ttl), max_depth: Number(o.maxDepth) }, key(o))));
248
248
  delegates.command('revoke <delegate_id>').option(...keyOption).action((id, o) => show(b => b.delegates.revoke(id, key(o))));
249
249
  blinds.command('activity').description('Latest 200 Blinds audit events').action(() => show(b => b.activity({ signal: deps.signal })));
250
+ blinds.command('proxy').description('One HTTPS call made by Blinds under a delegation you hold; values never leave Blinds')
251
+ .requiredOption('--body-file <path>', 'JSON {delegate_id, request_id, method, url, headers?, body?}; templates as {{VARIABLE}}').option(...keyOption)
252
+ .action(async (o) => {
253
+ const body = await readFunctionJson(o.bodyFile, 64 * 1024);
254
+ await show(b => b.proxy(body, key(o)));
255
+ });
250
256
  const owner = blinds.command('owner-enrollment')
251
257
  .description('Original-human enrollment only; preserve operation and locator IDs after unknown outcomes');
252
258
  for (const [action, operation] of Object.entries({ reserve: 'blinds_ownerReserve', sign: 'blinds_ownerSign', attach: 'blinds_ownerAttach', 'p256-ensure': 'blinds_ownerP256Ensure' })) {
@@ -271,7 +277,7 @@ export function buildProgram(deps = {}) {
271
277
  if (!key)
272
278
  throw new Error('Give the enrollment key with --key-file, --key-stdin or GENESIS_ENROLLMENT_KEY; it is never accepted as an argument.');
273
279
  const body = await readFunctionJson(o.bodyFile, 16 * 1024);
274
- const result = await new Genesis({ baseUrl: g().baseUrl ?? env.GENESIS_BASE_URL, fetch: f, userAgent: 'genesis-cli/0.1.13' }).iam.agents.enroll(key, body);
280
+ const result = await new Genesis({ baseUrl: g().baseUrl ?? env.GENESIS_BASE_URL, fetch: f, userAgent: 'genesis-cli/0.2.2' }).iam.agents.enroll(key, body);
275
281
  emit(result, () => JSON.stringify(result, null, 2));
276
282
  });
277
283
  // ---- auth ------------------------------------------------------------
@@ -325,7 +331,8 @@ export function buildProgram(deps = {}) {
325
331
  if (!saved || g().apiKey || env.GENESIS_API_KEY)
326
332
  throw e;
327
333
  // Saved login exists but One account verify isn't reachable yet (auth-v4-backend PR #30 not deployed).
328
- emit({ user: saved.user, organization_id: saved.organization_id, verified: false, reason: e.message }, () => `${c.bold('User')} ${saved.user?.email ?? saved.user?.id}\n${c.bold('Org')} ${saved.organization_id ?? '-'}\n${c.yellow('!')} ${c.dim('From your saved login; One account verify unavailable: ' + e.message)}`);
334
+ const reason = errorText(e);
335
+ emit({ user: saved.user, organization_id: saved.organization_id, verified: false, reason }, () => `${c.bold('User')} ${saved.user?.email ?? saved.user?.id}\n${c.bold('Org')} ${saved.organization_id ?? '-'}\n${c.yellow('!')} ${c.dim('From your saved login; One account verify unavailable: ' + reason)}`);
329
336
  }
330
337
  });
331
338
  // `orgs usage|observations|billing|payment-methods|service` and the Orgs (legal entity) commands are registered by registerProducts and registerOrgs.
@@ -803,7 +810,7 @@ export function buildProgram(deps = {}) {
803
810
  const url = `${base.href.replace(/\/+$/, '')}/v1/status`;
804
811
  let response;
805
812
  try {
806
- response = await f(url, { method: 'GET', headers: { accept: 'application/json', 'user-agent': 'genesis-cli/0.2.0' }, signal: AbortSignal.timeout(15_000) });
813
+ response = await f(url, { method: 'GET', headers: { accept: 'application/json', 'user-agent': 'genesis-cli/0.2.2' }, signal: AbortSignal.timeout(15_000) });
807
814
  }
808
815
  catch (e) {
809
816
  throw new Error(`Gateway unreachable at ${safe(url)}: ${safe(e.message)}`);
@@ -853,20 +860,24 @@ export async function main(argv, deps = {}) {
853
860
  // Dials needs a project, workspace and environment: from flags, a selected context or an org key's binding.
854
861
  const dialsScopeHint = e.operationId?.startsWith('dials_') && /invalid_scope/i.test(`${e.code ?? ''} ${e.message} ${e.type}`)
855
862
  ? ' Pass --workspace, --project and --environment, select a context (genesis context select), or use an org API key bound to a project.' : '';
863
+ const receipt = e instanceof HivesOperationError || e instanceof HivesQueueOperationError;
864
+ const code = errorCatalog(e).code, body = json ? errorBody(e) : undefined;
856
865
  if (json)
857
- out.err(JSON.stringify({ error: { type: e.type, status: e.status, message: e.message, request_id: e.requestId, ...errorCatalog(e),
866
+ out.err(JSON.stringify({ error: { type: e.type, status: e.status, message: redactMessage(e.message), request_id: e.requestId, ...errorCatalog(e),
858
867
  ...((e instanceof HivesOperationError || e instanceof HivesQueueOperationError) ? { operation: e.operation } : {}),
859
868
  ...(e.operationId && isMindsOperation(e.operationId) && errorOutcome(e.body) === 'unknown' ? { outcome: 'unknown' } : {}),
860
869
  ...(e instanceof MindsAgentError || e instanceof MindsOwnerError ? { code: e.code, ...(e instanceof MindsAgentError ? { dpop_nonce: e.dpopNonce } : {}),
861
- ...(errorOutcome(e.body) !== undefined ? { outcome: errorOutcome(e.body) } : {}) } : {}) } }));
870
+ ...(errorOutcome(e.body) !== undefined ? { outcome: errorOutcome(e.body) } : {}) } : {}),
871
+ // The service's whole error body (redacted and bounded), e.g. the failed Flocks `run` beside a 503.
872
+ ...(receipt || body === undefined ? {} : { body }) } }));
862
873
  else
863
- out.err(`${c.red('✗')} ${safe(e.message)}${dialsScopeHint}${(e instanceof HivesOperationError || e instanceof HivesQueueOperationError) ? ` Operation: ${safe(e.operation.operation_id)} (${safe(e.operation.state)}).` : ''}${e.status ? c.dim(` (${e.status} ${safe(e.type)})`) : ''}${e.requestId ? `\n${c.dim(`request_id: ${safe(e.requestId)}`)}` : ''}`);
874
+ out.err(`${c.red('✗')} ${printable(e.message)}${dialsScopeHint}${(e instanceof HivesOperationError || e instanceof HivesQueueOperationError) ? ` Operation: ${safe(e.operation.operation_id)} (${safe(e.operation.state)}).` : ''}${e.status ? c.dim(` (${e.status} ${safe(e.type)})`) : ''}${code ? `\n${c.dim(`code: ${safe(code)}`)}` : ''}${e.requestId ? `\n${c.dim(`request_id: ${safe(e.requestId)}`)}` : ''}`);
864
875
  return 1;
865
876
  }
866
- const msg = e.message ?? String(e);
877
+ const msg = redactMessage(e.message ?? String(e));
867
878
  const net = /fetch failed|ENOTFOUND|ECONNREFUSED/.test(msg + (e.cause?.code ?? ''));
868
879
  out.err(json ? JSON.stringify({ error: { type: net ? 'network' : 'error', message: msg } })
869
- : `${c.red('✗')} ${net ? `Could not reach the Genesis API gateway (${safe(msg)}). Try --direct to call service hosts directly.` : safe(msg)}`);
880
+ : `${c.red('✗')} ${net ? `Could not reach the Genesis API gateway (${printable(msg)}). Try --direct to call service hosts directly.` : printable(msg)}`);
870
881
  return 1;
871
882
  }
872
883
  }
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { hostname } from 'node:os';
10
10
  import { spawn } from 'node:child_process';
11
- import { safe } from '../output.js';
11
+ import { errorText, safe } from '../output.js';
12
12
  import { detect, probe, systemPreferences } from './caps.js';
13
13
  import { Term, Interrupted } from './term.js';
14
14
  import { Style, welcome, select, confirm, input, pick, waitBeside, delight, busy, typeLines } from './ui.js';
@@ -98,7 +98,7 @@ export async function runSetup(d) {
98
98
  if (e instanceof Interrupted)
99
99
  throw e;
100
100
  // One step failing never ends setup: say why, keep it for next time, and carry on.
101
- term.say(`${style.error(caps.unicode ? '✗' : 'x')} ${safe(e.message)}`);
101
+ term.say(`${style.error(caps.unicode ? '✗' : 'x')} ${errorText(e)}`);
102
102
  term.say(style.muted(`This step stays open; run genesis setup ${id} to try it again.`));
103
103
  result = { state: 'skipped', note: 'failed' };
104
104
  }
@@ -157,7 +157,7 @@ class Steps {
157
157
  store = await this.getStore();
158
158
  }
159
159
  catch (e) {
160
- this.note(`${safe(e.message)}`);
160
+ this.note(`${errorText(e)}`);
161
161
  return { state: 'skipped', note: 'keychain unavailable' };
162
162
  }
163
163
  const saved = await store.get();
@@ -265,7 +265,7 @@ class Steps {
265
265
  const token = () => d.login.savedLoginProvider(d.auth, store)().then((t) => t ?? '');
266
266
  const iam = new Iam(secureUrl(d.env.GENESIS_IAM_URL ?? 'https://iam.l1fe.ai', 'GENESIS_IAM_URL'), d.fetch, token);
267
267
  const orgs = await busy(term, style, 'Finding your organizations', iam.list('/v4/iam/orgs').catch((e) => {
268
- this.note(`Your organizations could not be listed (${safe(e.message)}); using the one you signed in to.`);
268
+ this.note(`Your organizations could not be listed (${errorText(e)}); using the one you signed in to.`);
269
269
  return [];
270
270
  }));
271
271
  const known = orgs.length ? orgs : saved.organization_id ? [{ id: saved.organization_id, name: saved.organization_id }] : [];
@@ -297,7 +297,7 @@ class Steps {
297
297
  }));
298
298
  }
299
299
  catch (e) {
300
- this.note(`Projects could not be read (${safe(e.message)}). Choose one later with:`);
300
+ this.note(`Projects could not be read (${errorText(e)}). Choose one later with:`);
301
301
  this.note(COMMANDS.context);
302
302
  return { state: 'skipped', note: 'iam unavailable' };
303
303
  }
@@ -415,7 +415,7 @@ class Steps {
415
415
  }
416
416
  catch (e) {
417
417
  okAll = false;
418
- term.say(`${style.error('✗')} ${c.name} ${style.muted(safe(e.message))}`);
418
+ term.say(`${style.error('✗')} ${c.name} ${style.muted(errorText(e))}`);
419
419
  }
420
420
  }
421
421
  if (chosen.includes('claude-desktop'))
@@ -432,7 +432,7 @@ class Steps {
432
432
  if (!r.ok)
433
433
  throw new Error(`HTTP ${r.status}`);
434
434
  return await r.json();
435
- })()).catch((e) => { this.note(`The gateway did not answer (${safe(e.message)}). Try genesis status later.`); return null; });
435
+ })()).catch((e) => { this.note(`The gateway did not answer (${errorText(e)}). Try genesis status later.`); return null; });
436
436
  if (!report?.services?.length)
437
437
  return { state: 'skipped' };
438
438
  // Only what the gateway is documented to send reaches the terminal: names and states through safe(), latency
package/dist/orgs.js CHANGED
@@ -7,7 +7,7 @@ const MEDIA_TYPES = {
7
7
  '.pdf': 'application/pdf', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
8
8
  '.txt': 'text/plain', '.md': 'text/markdown', '.markdown': 'text/markdown', '.json': 'application/json',
9
9
  };
10
- const DOCUMENT_KINDS = ['articles', 'certificate', 'operating_agreement', 'bylaws', 'resolution', 'filing_receipt', 'tax_letter', 'other'];
10
+ const DOCUMENT_KINDS = ['agent_consent', 'articles', 'certificate', 'operating_agreement', 'bylaws', 'resolution', 'filing_receipt', 'tax_letter', 'other'];
11
11
  const group = (parent, name) => parent.commands.find(c => c.name() === name) ?? parent.command(name);
12
12
  /**
13
13
  * The Orgs commands that are more than one JSON request: uploading a document from a file, downloading
package/dist/output.d.ts CHANGED
@@ -21,7 +21,19 @@ export declare const c: {
21
21
  * operators (U+2060-2064), Mongolian vowel separator (U+180E), soft hyphen
22
22
  * (U+00AD) and tag characters (U+E0000-E007F).
23
23
  */
24
- export declare function safe(v: unknown): string;
24
+ export declare function safe(v: unknown, keepLines?: boolean): string;
25
+ /**
26
+ * A message cleaned for redaction: terminal controls and invisible characters are stripped first (line breaks
27
+ * kept, so header lines still match), so none can split a secret and hide it from the patterns.
28
+ */
29
+ export declare function redactMessage(message: string): string;
30
+ /**
31
+ * A message as the terminal prints it: redacted, made safe, then redacted again, because `safe` drops the tabs and
32
+ * line breaks redaction kept, which could join the pieces of a split secret back together.
33
+ */
34
+ export declare function printable(message: string): string;
35
+ /** The message of a caught error, redacted (tokens, card numbers, emails...) and safe to print. */
36
+ export declare function errorText(e: unknown): string;
25
37
  /** Render rows as an aligned table; every cell is passed through safe(). */
26
38
  export declare function table(rows: Record<string, unknown>[], cols: string[]): string;
27
39
  /** Pull the array out of the various list envelopes ({data}, {zones}, {records}, bare array). */
package/dist/output.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { redactErrorText } from '@onesi/genesis';
1
2
  export const stdio = { log: (s) => process.stdout.write(s + '\n'), err: (s) => process.stderr.write(s + '\n') };
2
3
  const tty = () => !!process.stdout.isTTY && !process.env.NO_COLOR;
3
4
  export const c = {
@@ -18,14 +19,32 @@ export const c = {
18
19
  * operators (U+2060-2064), Mongolian vowel separator (U+180E), soft hyphen
19
20
  * (U+00AD) and tag characters (U+E0000-E007F).
20
21
  */
21
- export function safe(v) {
22
+ export function safe(v, keepLines = false) {
22
23
  return String(v)
23
24
  .replace(/(?:\x1b\]|\x9d)[^\x07\x1b\x9c]*(?:\x07|\x1b\\|\x9c)?/g, '')
24
25
  .replace(/(?:\x1b\[|\x9b)[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]?/g, '')
25
26
  .replace(/\x1b[\x20-\x2f]*[\x30-\x7e]?/g, '')
26
- .replace(/[\x00-\x1f\x7f-\x9f]/g, '')
27
+ .replace(keepLines ? /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g : /[\x00-\x1f\x7f-\x9f]/g, '')
27
28
  .replace(/[\u202a-\u202e\u2066-\u2069\u200b-\u200f\u061c\ufeff\u2028\u2029\u2060-\u2064\u180e\u00ad\u{e0000}-\u{e007f}]/gu, '');
28
29
  }
30
+ /**
31
+ * A message cleaned for redaction: terminal controls and invisible characters are stripped first (line breaks
32
+ * kept, so header lines still match), so none can split a secret and hide it from the patterns.
33
+ */
34
+ export function redactMessage(message) {
35
+ return redactErrorText(safe(message, true));
36
+ }
37
+ /**
38
+ * A message as the terminal prints it: redacted, made safe, then redacted again, because `safe` drops the tabs and
39
+ * line breaks redaction kept, which could join the pieces of a split secret back together.
40
+ */
41
+ export function printable(message) {
42
+ return redactErrorText(safe(redactMessage(message)));
43
+ }
44
+ /** The message of a caught error, redacted (tokens, card numbers, emails...) and safe to print. */
45
+ export function errorText(e) {
46
+ return printable(e instanceof Error ? e.message : String(e));
47
+ }
29
48
  /** Render rows as an aligned table; every cell is passed through safe(). */
30
49
  export function table(rows, cols) {
31
50
  if (!rows.length)
package/dist/products.js CHANGED
@@ -1,5 +1,6 @@
1
- import { writeFile } from 'node:fs/promises';
2
1
  import { idempotencyHeaders, ifMatchHeaders, mindsEngineCommands, operationTable } from '@onesi/genesis';
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ import { basename } from 'node:path';
3
4
  import { readFunctionJson } from './function-input.js';
4
5
  /** Command words -> operation. Positional arguments are the operation's path parameters, query
5
6
  * parameters become options, a body comes from --body-file, and --idempotency-key carries the
@@ -117,6 +118,15 @@ export const PRODUCT_COMMANDS = {
117
118
  command: 'zaps_zapCommand', 'kv command': 'zaps_command', 'kv pipeline': 'zaps_pipeline', stats: 'zaps_getStats', metrics: 'zaps_getMetrics',
118
119
  'audit search': 'zaps_searchAudit', 'audit stats': 'zaps_getAuditStats',
119
120
  } },
121
+ brews: { description: 'Brews: agent recipes, the exchange, decisions and selections (Sandbox; org API key or One account login; scope comes from the selection)', commands: {
122
+ create: 'brews_createBrew', list: 'brews_listBrews', get: 'brews_getBrew', card: 'brews_getBrewCard', compile: 'brews_compileBrew',
123
+ run: 'brews_runBrew', eval: 'brews_evalBrew', 'bench run': 'brews_runBrewBench', 'tools call': 'brews_callTool',
124
+ 'exchange public': 'brews_listPublicExchange', 'exchange list': 'brews_listExchange', 'exchange get': 'brews_getExchangeListing',
125
+ 'exchange install': 'brews_installBrew',
126
+ 'decisions status': 'brews_getDecisionStatus', 'decisions create': 'brews_createDecision', 'decisions get': 'brews_getDecision',
127
+ 'decisions reconcile': 'brews_reconcileDecision',
128
+ 'selections create': 'brews_createSelection', 'selections get': 'brews_getSelection', 'selections recover': 'brews_recoverSelection',
129
+ } },
120
130
  flares: { description: 'Flares: agent notifications, conversations, commitments, presence, trust and choreography (Sandbox; One account login)', commands: {
121
131
  list: 'flares_listFlares', get: 'flares_getFlare', send: 'flares_sendFlare', 'send-batch': 'flares_sendFlareBatch',
122
132
  acknowledge: 'flares_acknowledgeFlare', act: 'flares_actOnFlare',
@@ -161,6 +171,7 @@ export const PRODUCT_COMMANDS = {
161
171
  messages: 'foundry_createMessage', responses: 'foundry_createResponse', embeddings: 'foundry_createEmbedding', rerank: 'foundry_createRerank',
162
172
  'images create': 'foundry_createImage', 'images models': 'foundry_listImageModels', transcribe: 'foundry_createAudioTranscription',
163
173
  'receipts get': 'foundry_getGenerationReceipt', 'receipts by-request': 'foundry_getGenerationReceiptByRequest', 'receipts verification-key': 'foundry_getReceiptVerificationKey',
174
+ 'receipts verification-keys': 'foundry_listReceiptVerificationKeys',
164
175
  'keys list': 'foundry_listApiKeys', 'keys create': 'foundry_createApiKey', 'keys delete': 'foundry_deleteApiKey', 'keys rotate': 'foundry_rotateApiKey', 'keys current': 'foundry_getCurrentApiKey',
165
176
  'reporting request-logs': 'foundry_listRequestLogs', 'reporting provider-credentials': 'foundry_getProviderCredentialSummary',
166
177
  'activity list': 'foundry_getActivity', 'activity export': 'foundry_exportActivity', 'analytics meta': 'foundry_getAnalyticsMeta', 'analytics query': 'foundry_queryAnalytics',
@@ -218,6 +229,173 @@ export const PRODUCT_COMMANDS = {
218
229
  } },
219
230
  // `genesis minds engine <family> <verb> <mind_id> [params]`: every Minds engine route, generated from the grant table.
220
231
  minds: { description: 'Minds: your Sandbox Minds (One account login)', commands: mindsEngineCommands },
232
+ boxes: { description: 'Boxes: mail, docs, files, sheets, slides, calendar and identity (org API key; GET boxes:read, writes boxes:write)', commands: {
233
+ // boxes:begin
234
+ 'mail addresses create': 'boxes_mail_addresses_create',
235
+ 'mail addresses list': 'boxes_mail_addresses_list',
236
+ 'mail domains addresses create': 'boxes_mail_domains_addresses_create',
237
+ 'mail domains create': 'boxes_mail_domains_create',
238
+ 'mail domains list': 'boxes_mail_domains_list',
239
+ 'mail domains verify': 'boxes_mail_domains_verify',
240
+ 'mail inbox list': 'boxes_mail_inbox_list',
241
+ 'mail mailboxes list': 'boxes_mail_mailboxes_list',
242
+ 'mail messages get': 'boxes_mail_messages_get',
243
+ 'mail messages delete': 'boxes_mail_messages_delete',
244
+ 'mail messages update': 'boxes_mail_messages_update',
245
+ 'mail send': 'boxes_mail_send',
246
+ 'mail threads get': 'boxes_mail_threads_get',
247
+ 'mail threads list': 'boxes_mail_threads_list',
248
+ 'mail agent batch-send': 'boxes_mail_agent_batch_send',
249
+ 'mail agent webhooks create': 'boxes_mail_agent_webhooks_create',
250
+ 'mail agent webhooks list': 'boxes_mail_agent_webhooks_list',
251
+ 'mail agent webhooks delete': 'boxes_mail_agent_webhooks_delete',
252
+ 'mail agent activities list': 'boxes_mail_agent_activities_list',
253
+ 'mail agent state list': 'boxes_mail_agent_state_list',
254
+ 'mail agent state update': 'boxes_mail_agent_state_update',
255
+ 'mail attachments content get': 'boxes_mail_attachments_content_get',
256
+ 'mail attachments get': 'boxes_mail_attachments_get',
257
+ 'mail attachments delete': 'boxes_mail_attachments_delete',
258
+ 'mail attachments upload': 'boxes_mail_attachments_upload',
259
+ 'mail messages attachments list': 'boxes_mail_messages_attachments_list',
260
+ 'mail messages delivery get': 'boxes_mail_messages_delivery_get',
261
+ 'mail search': 'boxes_mail_search',
262
+ 'mail stream sse': 'boxes_mail_stream_sse',
263
+ 'mail batch': 'boxes_mail_batch',
264
+ 'mail delegations revoke': 'boxes_mail_delegations_revoke',
265
+ 'mail extraction message get': 'boxes_mail_extraction_message_get',
266
+ 'mail extraction thread get': 'boxes_mail_extraction_thread_get',
267
+ 'mail folders create': 'boxes_mail_folders_create',
268
+ 'mail folders list': 'boxes_mail_folders_list',
269
+ 'mail governance audit list': 'boxes_mail_governance_audit_list',
270
+ 'mail intelligence thread get': 'boxes_mail_intelligence_thread_get',
271
+ 'mail labels create': 'boxes_mail_labels_create',
272
+ 'mail labels list': 'boxes_mail_labels_list',
273
+ 'mail labels delete': 'boxes_mail_labels_delete',
274
+ 'mail mailboxes delegations list': 'boxes_mail_mailboxes_delegations_list',
275
+ 'mail memory events submit': 'boxes_mail_memory_events_submit',
276
+ 'mail memory recall': 'boxes_mail_memory_recall',
277
+ 'mail memory search': 'boxes_mail_memory_search',
278
+ 'mail memory store': 'boxes_mail_memory_store',
279
+ 'mail personas list': 'boxes_mail_personas_list',
280
+ 'mail templates list': 'boxes_mail_templates_list',
281
+ 'files list': 'boxes_files_list',
282
+ 'files search': 'boxes_files_search',
283
+ 'files recent list': 'boxes_files_recent_list',
284
+ 'files trash list': 'boxes_files_trash_list',
285
+ 'files shared-with-me list': 'boxes_files_shared_with_me_list',
286
+ 'files upload': 'boxes_files_upload',
287
+ 'files folders create': 'boxes_files_folders_create',
288
+ 'files get': 'boxes_files_get',
289
+ 'files update': 'boxes_files_update',
290
+ 'files delete': 'boxes_files_delete',
291
+ 'files download': 'boxes_files_download',
292
+ 'files restore': 'boxes_files_restore',
293
+ 'files purge': 'boxes_files_purge',
294
+ 'files share': 'boxes_files_share',
295
+ 'files shares list': 'boxes_files_shares_list',
296
+ 'files unshare': 'boxes_files_unshare',
297
+ 'docs documents list': 'boxes_docs_documents_list',
298
+ 'docs documents create': 'boxes_docs_documents_create',
299
+ 'docs folders create': 'boxes_docs_folders_create',
300
+ 'docs search': 'boxes_docs_search',
301
+ 'docs recent list': 'boxes_docs_recent_list',
302
+ 'docs trash list': 'boxes_docs_trash_list',
303
+ 'docs templates list': 'boxes_docs_templates_list',
304
+ 'docs shared-with-me list': 'boxes_docs_shared_with_me_list',
305
+ 'docs mcp action': 'boxes_docs_mcp_action',
306
+ 'docs documents get': 'boxes_docs_documents_get',
307
+ 'docs documents update': 'boxes_docs_documents_update',
308
+ 'docs documents delete': 'boxes_docs_documents_delete',
309
+ 'docs documents purge': 'boxes_docs_documents_purge',
310
+ 'docs documents versions list': 'boxes_docs_documents_versions_list',
311
+ 'docs documents share': 'boxes_docs_documents_share',
312
+ 'docs documents shares list': 'boxes_docs_documents_shares_list',
313
+ 'docs documents unshare': 'boxes_docs_documents_unshare',
314
+ 'docs documents collaborators list': 'boxes_docs_documents_collaborators_list',
315
+ 'docs documents presence update': 'boxes_docs_documents_presence_update',
316
+ 'docs documents operations list': 'boxes_docs_documents_operations_list',
317
+ 'docs documents operations apply': 'boxes_docs_documents_operations_apply',
318
+ 'docs documents events stream': 'boxes_docs_documents_events_stream',
319
+ 'sheets cells batch': 'boxes_sheets_cells_batch',
320
+ 'sheets cells delete': 'boxes_sheets_cells_delete',
321
+ 'sheets cells get': 'boxes_sheets_cells_get',
322
+ 'sheets cells list': 'boxes_sheets_cells_list',
323
+ 'sheets cells set': 'boxes_sheets_cells_set',
324
+ 'sheets cells query': 'boxes_sheets_cells_query',
325
+ 'sheets cells range get': 'boxes_sheets_cells_range_get',
326
+ 'sheets search': 'boxes_sheets_search',
327
+ 'sheets shared-with-me list': 'boxes_sheets_shared_with_me_list',
328
+ 'sheets spreadsheets create': 'boxes_sheets_spreadsheets_create',
329
+ 'sheets spreadsheets list': 'boxes_sheets_spreadsheets_list',
330
+ 'sheets spreadsheets delete': 'boxes_sheets_spreadsheets_delete',
331
+ 'sheets spreadsheets get': 'boxes_sheets_spreadsheets_get',
332
+ 'sheets spreadsheets update': 'boxes_sheets_spreadsheets_update',
333
+ 'sheets spreadsheets purge': 'boxes_sheets_spreadsheets_purge',
334
+ 'sheets spreadsheets share': 'boxes_sheets_spreadsheets_share',
335
+ 'sheets spreadsheets shares list': 'boxes_sheets_spreadsheets_shares_list',
336
+ 'sheets spreadsheets unshare': 'boxes_sheets_spreadsheets_unshare',
337
+ 'sheets tabs create': 'boxes_sheets_tabs_create',
338
+ 'sheets tabs list': 'boxes_sheets_tabs_list',
339
+ 'sheets tabs delete': 'boxes_sheets_tabs_delete',
340
+ 'sheets tabs get': 'boxes_sheets_tabs_get',
341
+ 'sheets tabs update': 'boxes_sheets_tabs_update',
342
+ 'slides presentations create': 'boxes_slides_presentations_create',
343
+ 'slides presentations list': 'boxes_slides_presentations_list',
344
+ 'slides presentations delete': 'boxes_slides_presentations_delete',
345
+ 'slides presentations get': 'boxes_slides_presentations_get',
346
+ 'slides presentations update': 'boxes_slides_presentations_update',
347
+ 'slides presentations share': 'boxes_slides_presentations_share',
348
+ 'slides presentations shared-with-me list': 'boxes_slides_presentations_shared_with_me_list',
349
+ 'slides presentations shares list': 'boxes_slides_presentations_shares_list',
350
+ 'slides presentations unshare': 'boxes_slides_presentations_unshare',
351
+ 'slides slides create': 'boxes_slides_slides_create',
352
+ 'slides slides list': 'boxes_slides_slides_list',
353
+ 'slides slides delete': 'boxes_slides_slides_delete',
354
+ 'slides slides get': 'boxes_slides_slides_get',
355
+ 'slides slides update': 'boxes_slides_slides_update',
356
+ 'slides slides reorder': 'boxes_slides_slides_reorder',
357
+ 'slides templates create': 'boxes_slides_templates_create',
358
+ 'slides templates list': 'boxes_slides_templates_list',
359
+ 'slides templates delete': 'boxes_slides_templates_delete',
360
+ 'slides templates get': 'boxes_slides_templates_get',
361
+ 'slides templates update': 'boxes_slides_templates_update',
362
+ 'calendar availability get': 'boxes_calendar_availability_get',
363
+ 'calendar calendars create': 'boxes_calendar_calendars_create',
364
+ 'calendar calendars list': 'boxes_calendar_calendars_list',
365
+ 'calendar calendars delete': 'boxes_calendar_calendars_delete',
366
+ 'calendar calendars get': 'boxes_calendar_calendars_get',
367
+ 'calendar calendars update': 'boxes_calendar_calendars_update',
368
+ 'calendar events attendees list': 'boxes_calendar_events_attendees_list',
369
+ 'calendar events create': 'boxes_calendar_events_create',
370
+ 'calendar events list': 'boxes_calendar_events_list',
371
+ 'calendar events delete': 'boxes_calendar_events_delete',
372
+ 'calendar events get': 'boxes_calendar_events_get',
373
+ 'calendar events update': 'boxes_calendar_events_update',
374
+ 'calendar events rsvp': 'boxes_calendar_events_rsvp',
375
+ 'calendar events search': 'boxes_calendar_events_search',
376
+ 'calendar scheduling-links book': 'boxes_calendar_scheduling_links_book',
377
+ 'calendar scheduling-links by-slug get': 'boxes_calendar_scheduling_links_by_slug_get',
378
+ 'calendar scheduling-links create': 'boxes_calendar_scheduling_links_create',
379
+ 'calendar scheduling-links list': 'boxes_calendar_scheduling_links_list',
380
+ 'calendar scheduling-links delete': 'boxes_calendar_scheduling_links_delete',
381
+ 'calendar scheduling-links get': 'boxes_calendar_scheduling_links_get',
382
+ 'calendar scheduling-links update': 'boxes_calendar_scheduling_links_update',
383
+ 'identity me get': 'boxes_identity_me_get',
384
+ 'identity tenants current get': 'boxes_identity_tenants_current_get',
385
+ 'identity users get': 'boxes_identity_users_get',
386
+ 'identity users list': 'boxes_identity_users_list',
387
+ 'identity workspaces list': 'boxes_identity_workspaces_list',
388
+ 'identity workspaces get': 'boxes_identity_workspaces_get',
389
+ 'identity workspaces members list': 'boxes_identity_workspaces_members_list',
390
+ 'mcp sessions context set': 'boxes_mcp_sessions_context_set',
391
+ 'mcp sessions create': 'boxes_mcp_sessions_create',
392
+ 'mcp sessions list': 'boxes_mcp_sessions_list',
393
+ 'mcp sessions delete': 'boxes_mcp_sessions_delete',
394
+ 'mcp sessions get': 'boxes_mcp_sessions_get',
395
+ 'mcp tools call': 'boxes_mcp_tools_call',
396
+ 'mcp tools list': 'boxes_mcp_tools_list',
397
+ // boxes:end
398
+ } },
221
399
  };
222
400
  const BODY_LIMIT = 1024 * 1024;
223
401
  const flag = (name) => name.replace(/_/g, '-').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
@@ -260,8 +438,21 @@ export function registerOperationCommands(root, commands, dependencies) {
260
438
  else
261
439
  command.option(`--${flag(q)} <value>`);
262
440
  }
263
- if (op.hasBody)
441
+ // Boxes only: multipart uploads, and raw bytes or event streams read through a --duration window.
442
+ // Every other service keeps the plain path below (Foundry infer-stream is read to its end).
443
+ const boxes = op.service === 'boxes';
444
+ const multipart = boxes && op.requestContentType === 'multipart/form-data';
445
+ const boxesRaw = boxes && !!op.rawResponse;
446
+ if (multipart) {
447
+ command.option('--file <part=path>', 'File part, repeatable: part name and local path', (v, all = []) => [...all, v]);
448
+ command.option('--field <part=value>', 'Text part, repeatable', (v, all = []) => [...all, v]);
449
+ }
450
+ else if (op.hasBody)
264
451
  command[bodyRequired ? 'requiredOption' : 'option']('--body-file <path>', 'JSON request body, up to 1 MiB');
452
+ // Boxes raw answers use the generic --out below; event streams also take a --duration window.
453
+ if (boxesRaw) {
454
+ command.option('--duration <seconds>', 'For event streams: stop reading after this many seconds (default 10)');
455
+ }
265
456
  if (op.ifMatch)
266
457
  command.requiredOption('--if-match <version>', 'The version you read (the version field of the object you are changing, or its ETag); a stale one is refused');
267
458
  if (op.idempotencyKey === 'required')
@@ -282,6 +473,50 @@ export function registerOperationCommands(root, commands, dependencies) {
282
473
  if (options.bodyFile)
283
474
  input.body = await readFunctionJson(options.bodyFile, BODY_LIMIT);
284
475
  const headers = { ...idempotencyHeaders(id, options.idempotencyKey), ...(op.ifMatch ? ifMatchHeaders(id, options.ifMatch) : {}) };
476
+ const extra = options;
477
+ if (multipart) {
478
+ const form = new FormData();
479
+ const split = (entry) => { const at = entry.indexOf('='); if (at < 1)
480
+ throw new Error(`Expected part=value, got "${entry}"`); return [entry.slice(0, at), entry.slice(at + 1)]; };
481
+ for (const entry of extra.file ?? []) {
482
+ const [part, file] = split(entry);
483
+ form.append(part, new Blob([new Uint8Array(await readFile(file))]), basename(file));
484
+ }
485
+ for (const entry of extra.field ?? [])
486
+ form.append(...split(entry));
487
+ input.body = form;
488
+ }
489
+ if (boxesRaw) {
490
+ const stream = new AbortController();
491
+ const signal = dependencies.signal ? AbortSignal.any([dependencies.signal, stream.signal]) : stream.signal;
492
+ const response = await (await dependencies.client()).raw(id, input, { headers, signal });
493
+ if (!response.ok)
494
+ throw new Error(`HTTP ${response.status}: ${(await response.text()).slice(0, 500)}`);
495
+ const type = response.headers.get('content-type') ?? '';
496
+ const timer = type.startsWith('text/event-stream') ? setTimeout(() => stream.abort(), Math.max(1, Number.isFinite(Number(extra.duration)) ? Number(extra.duration) : 10) * 1000) : undefined;
497
+ const chunks = [];
498
+ try {
499
+ for await (const chunk of response.body ?? [])
500
+ chunks.push(chunk);
501
+ }
502
+ catch (e) {
503
+ if (!stream.signal.aborted)
504
+ throw e;
505
+ }
506
+ finally {
507
+ clearTimeout(timer);
508
+ }
509
+ const bytes = Buffer.concat(chunks);
510
+ if (extra.out) {
511
+ await writeFile(extra.out, bytes);
512
+ dependencies.emit({ ok: true, path: extra.out, bytes: bytes.length, contentType: type }, () => `wrote ${bytes.length} bytes to ${extra.out}`);
513
+ }
514
+ else if (/^(text\/|application\/json)/.test(type))
515
+ dependencies.emit(bytes.toString('utf8'), () => bytes.toString('utf8'));
516
+ else
517
+ throw new Error(`The response is ${type || 'binary'}; pass --out <path> to save it.`);
518
+ return;
519
+ }
285
520
  const result = await (await dependencies.client()).request(id, input, { headers, signal: dependencies.signal });
286
521
  if (result instanceof Response) {
287
522
  // With --out, a raw answer (e.g. a Panes screenshot) is written to the file as bytes.
@@ -5,6 +5,7 @@ export const domainsCommands = {
5
5
  'registrar domains get': 'relays_getRegistrarDomain',
6
6
  'domains analyze': 'relays_analyzeDomain',
7
7
  'domains suggest': 'relays_suggestDomains',
8
+ 'domains specials': 'relays_listDomainSpecials',
8
9
  'forwarding list': 'relays_listDomainForwardingRules',
9
10
  'forwarding create': 'relays_createDomainForwardingRule',
10
11
  'forwarding delete': 'relays_deleteDomainForwardingRule',
@@ -1,5 +1,7 @@
1
1
  import type { OperationId } from '@onesi/genesis';
2
2
  /** `genesis relays ...` words for the zones family (openapi/specs/relays/zones.openapi.yaml).
3
3
  * DNSSEC disable is `disable-dnssec`, beside the hand-written `dnssec <zone>` read and the
4
- * `create-zone` / `delete-zone` commands, so it does not shadow `dnssec <zone>`. */
4
+ * `create-zone` / `delete-zone` commands, so it does not shadow `dnssec <zone>`. Zone files and
5
+ * bulk records follow the same verb-noun form (`export-zone`, `import-zone`, `bulk-records`),
6
+ * so they never collide with the hand-written `zone <zone>` and `records ...` commands. */
5
7
  export declare const zonesCommands: Readonly<Record<string, OperationId>>;
@@ -1,9 +1,16 @@
1
1
  /** `genesis relays ...` words for the zones family (openapi/specs/relays/zones.openapi.yaml).
2
2
  * DNSSEC disable is `disable-dnssec`, beside the hand-written `dnssec <zone>` read and the
3
- * `create-zone` / `delete-zone` commands, so it does not shadow `dnssec <zone>`. */
3
+ * `create-zone` / `delete-zone` commands, so it does not shadow `dnssec <zone>`. Zone files and
4
+ * bulk records follow the same verb-noun form (`export-zone`, `import-zone`, `bulk-records`),
5
+ * so they never collide with the hand-written `zone <zone>` and `records ...` commands. */
4
6
  export const zonesCommands = {
5
7
  'caa list': 'relays_listCaaRecords',
6
8
  'caa create': 'relays_createCaaRecord',
7
9
  'disable-dnssec': 'relays_disableDnssec',
8
10
  'nameservers list': 'relays_listNameservers',
11
+ 'export-zone': 'relays_exportZone',
12
+ 'import-zone': 'relays_importZone',
13
+ 'bulk-records': 'relays_bulkRecords',
14
+ 'propagation check': 'relays_checkDnsPropagation',
15
+ 'propagation global': 'relays_checkGlobalDnsPropagation',
9
16
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onesi/genesis-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/l1felabs/genesis-api.git",
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "commander": "^14.0.0",
35
35
  "undici": "6.29.0",
36
- "@onesi/genesis": "0.2.0"
36
+ "@onesi/genesis": "0.2.2"
37
37
  },
38
38
  "optionalDependencies": {
39
39
  "@napi-rs/keyring": "^1.2.0"