@onesi/genesis-cli 0.2.13 → 0.2.15

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.13 --help
6
+ npx -y @onesi/genesis-cli@0.2.15 --help
7
7
  ```
8
8
 
9
9
  ## First run
@@ -0,0 +1,13 @@
1
+ import type { Command } from 'commander';
2
+ import { type Genesis } from '@onesi/genesis';
3
+ type Dependencies = {
4
+ client: () => Promise<Genesis>;
5
+ emit: (data: unknown, pretty: () => string) => void;
6
+ signal?: AbortSignal;
7
+ };
8
+ /**
9
+ * `genesis agents search <q...>` and `genesis agents sitemap`: public agent search (Live agents their owners made
10
+ * public). No login is needed; no credential is sent and nothing is metered.
11
+ */
12
+ export declare function registerAgents(program: Command, deps: Dependencies): void;
13
+ export {};
package/dist/agents.js ADDED
@@ -0,0 +1,41 @@
1
+ import { AGENT_SEARCH_LIMIT_MAX, AGENT_SEARCH_Q_MAX } from '@onesi/genesis';
2
+ import { c, safe, table } from './output.js';
3
+ /** One hit as the terminal prints it; every server-provided field goes through safe(). */
4
+ function hitLines(hit) {
5
+ const head = `${c.bold(safe(hit.name ?? hit.agentId))} ${c.dim(safe(hit.agentId))}${hit.owner ? ` ${safe(hit.owner.name)}` : ''}`;
6
+ const lines = [head, ` ${c.dim(safe(hit.did))}`];
7
+ if (hit.description)
8
+ lines.push(` ${safe(hit.description)}`);
9
+ if (hit.createdAt)
10
+ lines.push(` ${c.dim(`created ${new Date(hit.createdAt).toISOString().slice(0, 10)}`)}`);
11
+ return lines.join('\n');
12
+ }
13
+ /**
14
+ * `genesis agents search <q...>` and `genesis agents sitemap`: public agent search (Live agents their owners made
15
+ * public). No login is needed; no credential is sent and nothing is metered.
16
+ */
17
+ export function registerAgents(program, deps) {
18
+ const agents = program.command('agents').description('Public agent search: Live agents their owners made public (no login needed, never metered)');
19
+ agents.command('search <q...>')
20
+ .description(`Search public agents (GET /v1/agents/search); the words are joined into one query of 1 to ${AGENT_SEARCH_Q_MAX} characters`)
21
+ .option('--limit <n>', `most results, 1 to ${AGENT_SEARCH_LIMIT_MAX} (default ${AGENT_SEARCH_LIMIT_MAX})`)
22
+ .action(async (words, o) => {
23
+ const q = words.join(' ');
24
+ if (q.trim() === '' || q.length > AGENT_SEARCH_Q_MAX)
25
+ throw new Error(`The query must be 1 to ${AGENT_SEARCH_Q_MAX} characters.`);
26
+ let limit;
27
+ if (o.limit !== undefined) {
28
+ if (!/^[0-9]{1,2}$/.test(o.limit) || Number(o.limit) < 1 || Number(o.limit) > AGENT_SEARCH_LIMIT_MAX)
29
+ throw new Error(`--limit must be an integer from 1 to ${AGENT_SEARCH_LIMIT_MAX}.`);
30
+ limit = Number(o.limit);
31
+ }
32
+ const result = await (await deps.client()).agents.search(q, { ...(limit === undefined ? {} : { limit }), ...(deps.signal ? { signal: deps.signal } : {}) });
33
+ deps.emit(result, () => result.results.length ? result.results.map(hitLines).join('\n\n') : c.dim('(no public agents match)'));
34
+ });
35
+ agents.command('sitemap')
36
+ .description('Every public agent id with when it last changed (GET /v1/agents/sitemap)')
37
+ .action(async () => {
38
+ const result = await (await deps.client()).agents.sitemap(deps.signal ? { signal: deps.signal } : {});
39
+ deps.emit(result, () => table(result.agents.map(a => ({ agentId: a.agentId, updatedAt: new Date(a.updatedAt).toISOString() })), ['agentId', 'updatedAt']));
40
+ });
41
+ }
package/dist/dials.js CHANGED
@@ -27,6 +27,34 @@ export function registerDials(program, dependencies) {
27
27
  throw new Error('Unsupported Dials search filter. Scope is selected by the command context.');
28
28
  emit(await client.dials.numbers.search(scope, { ...filters, ...(options.areaCode ? { areaCode: options.areaCode } : {}) }));
29
29
  });
30
+ numbers.command('voice-handler').requiredOption('--number <id>', 'Owned SignalWire number ID').requiredOption('--handler <name>', 'one_phone (the One phone line) or default')
31
+ .action(async (options) => {
32
+ if (options.handler !== 'one_phone' && options.handler !== 'default')
33
+ throw new Error('--handler must be one_phone or default.');
34
+ const { client, scope } = await context();
35
+ emit(await client.dials.numbers.setVoiceHandler({ scope, phoneNumberId: options.number, handler: options.handler }));
36
+ });
37
+ // A person's own One number: the person's own token, no Dials scope (Dials keys it to the verified person).
38
+ const oneNumbers = dials.command('one-numbers').description('The phone number of your own One (signed in as yourself)');
39
+ oneNumbers.command('mine', { isDefault: true }).action(async () => { const client = await dependencies.client(); emit(await client.dials.oneNumbers.mine()); });
40
+ oneNumbers.command('suggestions').option('--area-code <code>', 'three digit area code').option('--text <text>', 'search text')
41
+ .option('--lat <n>', 'latitude (with --lng, from the device after permission)').option('--lng <n>', 'longitude (with --lat)')
42
+ .action(async (options) => {
43
+ const client = await dependencies.client();
44
+ const num = (value, name) => { if (value === undefined)
45
+ return undefined; const n = Number(value); if (!Number.isFinite(n))
46
+ throw new Error(`${name} must be a number.`); return n; };
47
+ if ((options.lat === undefined) !== (options.lng === undefined))
48
+ throw new Error('--lat and --lng go together.');
49
+ // Coordinates travel in the JSON body, never the URL.
50
+ const body = { ...(options.areaCode !== undefined ? { areaCode: options.areaCode } : {}), ...(options.text !== undefined ? { text: options.text } : {}),
51
+ ...(options.lat !== undefined ? { lat: num(options.lat, '--lat') } : {}), ...(options.lng !== undefined ? { lng: num(options.lng, '--lng') } : {}) };
52
+ emit(await client.dials.oneNumbers.suggestions(body));
53
+ });
54
+ oneNumbers.command('choose').requiredOption('--e164 <number>', 'a number from the suggestions, for example +15555550123').requiredOption('--idempotency-key <key>', 'reuse it to retry the same choice')
55
+ .action(async (options) => { const client = await dependencies.client(); emit(await client.dials.oneNumbers.choose({ e164: options.e164, idempotencyKey: options.idempotencyKey })); });
56
+ oneNumbers.command('release').requiredOption('--yes', 'confirm: the number is released at the carrier and cannot be recovered')
57
+ .action(async () => { const client = await dependencies.client(); emit(await client.dials.oneNumbers.release()); });
30
58
  numbers.command('reconciliations').action(async () => { const { client, scope } = await context(); emit(await client.dials.numbers.reconciliations(scope)); });
31
59
  numbers.command('drift').action(async () => { const { client, scope } = await context(); emit(await client.dials.numbers.drift(scope)); });
32
60
  dials.command('overview').action(async () => { const { client, scope } = await context(); emit(await client.dials.overview.summary(scope)); });
@@ -76,6 +104,8 @@ export function registerDials(program, dependencies) {
76
104
  workflows.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.dials.workflows.list(scope)); });
77
105
  workflows.command('deploy').requiredOption('--body <json-or-@file>', 'Workflow graph and actorId')
78
106
  .action(async (options) => { const { client, scope } = await context(); const body = await readBody(options.body); emit(await client.call('dials_deployWorkflow', { body: { ...body, scope } })); });
107
+ workflows.command('delete').requiredOption('--workflow <id>', 'Owned workflow ID; deletes it and its routing descriptor (409 while a held number is assigned)')
108
+ .action(async (options) => { const { client, scope } = await context(); emit(await client.dials.workflows.delete(options.workflow, { scope })); });
79
109
  numbers.command('delete').requiredOption('--number <id>', 'Owned number ID to release at the carrier').requiredOption('--actor <id>', 'Actual actorId').option('--reason <text>', 'Release reason').option('--platform <id>', 'Selected data platform; omitted defaults to dials')
80
110
  .action(async (options) => { const { client, scope } = await context(); emit(await client.dials.numbers.delete({ ...scope, ...(options.platform !== undefined ? { platformId: options.platform } : {}), number_id: options.number, actorId: options.actor, ...(options.reason !== undefined ? { reason: options.reason } : {}) })); });
81
111
  const calls = dials.command('calls').description('Call activity and approved outbound calls');
package/dist/fleets.d.ts CHANGED
@@ -3,4 +3,4 @@ import type { Genesis } from '@onesi/genesis';
3
3
  /** Read one key from stdin (at most 4096 bytes, one trailing newline dropped); `label` names it in errors. */
4
4
  export declare function readStdinKey(label: string, input?: AsyncIterable<Uint8Array>): Promise<string>;
5
5
  export declare const readFoundryKey: (input?: AsyncIterable<Uint8Array>) => Promise<string>;
6
- export declare function registerFleets(program: Command, client: () => Promise<Genesis>, emit: (data: unknown) => void, keyInput?: () => Promise<string>, computerKeyInput?: () => Promise<string>): void;
6
+ export declare function registerFleets(program: Command, client: () => Promise<Genesis>, emit: (data: unknown) => void, keyInput?: () => Promise<string>, computerKeyInput?: () => Promise<string>, webKeyInput?: () => Promise<string>): void;
package/dist/fleets.js CHANGED
@@ -22,6 +22,7 @@ export async function readStdinKey(label, input = process.stdin) {
22
22
  }
23
23
  export const readFoundryKey = (input = process.stdin) => readStdinKey('Foundry key', input);
24
24
  const readComputerKey = () => readStdinKey('Computer key');
25
+ const readWebKey = () => readStdinKey('Web key');
25
26
  const SEAT_TYPES = ['helper', 'operator', 'always_on'];
26
27
  const seatType = (value) => {
27
28
  if (!SEAT_TYPES.includes(value))
@@ -30,7 +31,7 @@ const seatType = (value) => {
30
31
  };
31
32
  /** Whole-number flags go to the SDK as numbers; it refuses anything that is not a whole number before sending. */
32
33
  const count = (value) => /^\d+$/.test(value) ? Number(value) : NaN;
33
- export function registerFleets(program, client, emit, keyInput = readFoundryKey, computerKeyInput = readComputerKey) {
34
+ export function registerFleets(program, client, emit, keyInput = readFoundryKey, computerKeyInput = readComputerKey, webKeyInput = readWebKey) {
34
35
  const fleets = program.command('fleets').description('Fleets: managed Henry instances and tasks');
35
36
  const agents = fleets.command('agents').description('Persistent Henry agents on Fleets-managed compute');
36
37
  agents.command('list').action(async () => emit(await (await client()).fleets.agents.list()));
@@ -175,6 +176,11 @@ export function registerFleets(program, client, emit, keyInput = readFoundryKey,
175
176
  computer.command('set').description('Read the key from stdin and encrypt it with Genesis KMS (never returned)')
176
177
  .action(async () => emit(await (await client()).fleets.computerConnection.set(await computerKeyInput())));
177
178
  computer.command('delete').action(async () => emit(await (await client()).fleets.computerConnection.delete()));
179
+ const web = fleets.command('web-connection').description('Key (scopes exactly traces:read) seat research tasks use to search and read the web through Traces');
180
+ web.command('get').action(async () => emit(await (await client()).fleets.webConnection.get()));
181
+ web.command('set').description('Read the key from stdin and encrypt it with Genesis KMS (never returned)')
182
+ .action(async () => emit(await (await client()).fleets.webConnection.set(await webKeyInput())));
183
+ web.command('delete').action(async () => emit(await (await client()).fleets.webConnection.delete()));
178
184
  const usage = fleets.command('usage').description('Usage observations derived from your tasks');
179
185
  usage.command('events').description('List task lifecycle and execution-time usage events').option('--limit <n>').option('--before <task_id>')
180
186
  .action(async (options) => emit(await (await client()).fleets.usage.events({ ...(options.limit ? { limit: Number(options.limit) } : {}), ...(options.before ? { before: options.before } : {}) })));
@@ -1,3 +1,8 @@
1
- export declare function readFunctionFile(path: string, limit: number): Promise<Uint8Array>;
2
- export declare function readFunctionJson(path: string, limit: number): Promise<Record<string, unknown>>;
1
+ /** `-` as the path means standard input: the bytes are read from a pipe or redirect (never from a terminal) and never touch disk. */
2
+ export declare const STDIN_PATH = "-";
3
+ export declare function readFunctionStdin(limit: number, input?: AsyncIterable<Uint8Array | string> & {
4
+ isTTY?: boolean;
5
+ }): Promise<Uint8Array>;
6
+ export declare function readFunctionFile(path: string, limit: number, stdin?: Parameters<typeof readFunctionStdin>[1]): Promise<Uint8Array>;
7
+ export declare function readFunctionJson(path: string, limit: number, stdin?: Parameters<typeof readFunctionStdin>[1]): Promise<Record<string, unknown>>;
3
8
  export declare function writeFunctionOutput(path: string, bytes: Uint8Array): Promise<void>;
@@ -1,5 +1,29 @@
1
1
  import { open, writeFile } from 'node:fs/promises';
2
- export async function readFunctionFile(path, limit) {
2
+ /** `-` as the path means standard input: the bytes are read from a pipe or redirect (never from a terminal) and never touch disk. */
3
+ export const STDIN_PATH = '-';
4
+ export async function readFunctionStdin(limit, input = process.stdin) {
5
+ if (input.isTTY)
6
+ throw new Error('Standard input is a terminal. Pipe the request body into the command, for example: echo \'{}\' | genesis ... --body-file -');
7
+ const chunks = [];
8
+ let size = 0;
9
+ for await (const chunk of input) {
10
+ const bytes = typeof chunk === 'string' ? new TextEncoder().encode(chunk) : chunk;
11
+ size += bytes.length;
12
+ if (size > limit)
13
+ throw new Error('Function input on standard input exceeds the documented size limit.');
14
+ chunks.push(bytes);
15
+ }
16
+ const out = new Uint8Array(size);
17
+ let at = 0;
18
+ for (const c of chunks) {
19
+ out.set(c, at);
20
+ at += c.length;
21
+ }
22
+ return out;
23
+ }
24
+ export async function readFunctionFile(path, limit, stdin) {
25
+ if (path === STDIN_PATH)
26
+ return readFunctionStdin(limit, stdin);
3
27
  let file;
4
28
  try {
5
29
  file = await open(path, 'r');
@@ -27,8 +51,8 @@ export async function readFunctionFile(path, limit) {
27
51
  await file.close();
28
52
  }
29
53
  }
30
- export async function readFunctionJson(path, limit) {
31
- const bytes = await readFunctionFile(path, limit);
54
+ export async function readFunctionJson(path, limit, stdin) {
55
+ const bytes = await readFunctionFile(path, limit, stdin);
32
56
  try {
33
57
  const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
34
58
  if (!value || typeof value !== 'object' || Array.isArray(value))
@@ -1,5 +1,6 @@
1
1
  import { registerHivesResources } from './hives-resources.js';
2
2
  import { registerHivesPlatform } from './hives-platform.js';
3
+ import { registerHivesTimers } from './hives-timers.js';
3
4
  import { Option } from 'commander';
4
5
  import { readFunctionJson } from './function-input.js';
5
6
  function metricsOptions(command) {
@@ -20,6 +21,7 @@ const pagination = (o) => ({ page: integer(o.page, '--page', 10000), per_page: i
20
21
  export function registerHivesOperations(hives, deps) {
21
22
  registerHivesResources(hives, deps);
22
23
  registerHivesPlatform(hives, { client: deps.client, emit: deps.emit, line: deps.line });
24
+ registerHivesTimers(hives, deps);
23
25
  const output = (data) => deps.emit(data, () => JSON.stringify(data, null, 2));
24
26
  const read = deps.readRequest ?? readFunctionJson;
25
27
  const settings = hives.command('settings').description('Stored project preferences and actual engine capabilities');
@@ -32,8 +34,8 @@ export function registerHivesOperations(hives, deps) {
32
34
  crons.command('create <project_id>').requiredOption('--request-file <path>', 'JSON name, schedule, handler, timezone and enabled preference')
33
35
  .action(async (project, o) => {
34
36
  const body = await read(o.requestFile, 64 * 1024);
35
- if (typeof body.name !== 'string' || typeof body.schedule !== 'string' || typeof body.handler !== 'string')
36
- throw new Error('Cron JSON requires name, schedule and handler strings.');
37
+ if (typeof body.name !== 'string' || typeof body.schedule !== 'string' || (typeof body.handler !== 'string' && !(body.handler && typeof body.handler === 'object' && body.handler.type === 'callback')))
38
+ throw new Error('Cron JSON requires name and schedule strings and a handler: a function id string or {"type":"callback","target_id":"tgt_…","payload":{…}}.');
37
39
  output(await (await deps.client()).hives.crons.create(project, { ...body, name: body.name, schedule: body.schedule, handler: body.handler }));
38
40
  });
39
41
  crons.command('update <project_id> <cron_id>').requiredOption('--request-file <path>', 'JSON schedule patch')
@@ -11,6 +11,10 @@ export const HIVES_PROJECT_COMMANDS = {
11
11
  'settings update': 'hives_update_project_settings',
12
12
  'crons list': 'hives_list_crons',
13
13
  'crons create': 'hives_create_cron',
14
+ 'timers list': 'hives_listTimers',
15
+ 'timers create': 'hives_createTimer',
16
+ 'timers targets list': 'hives_listTimerTargets',
17
+ 'timers targets create': 'hives_createTimerTarget',
14
18
  'metrics project': 'hives_get_project_metrics',
15
19
  'metrics requests': 'hives_get_request_metrics',
16
20
  'metrics latency': 'hives_get_latency_metrics',
@@ -0,0 +1,10 @@
1
+ import { Command } from 'commander';
2
+ import type { Genesis } from '@onesi/genesis';
3
+ type Deps = {
4
+ client: () => Promise<Genesis>;
5
+ emit: (data: unknown, pretty: () => string) => void;
6
+ readRequest?: (path: string, limit: number) => Promise<Record<string, unknown>>;
7
+ line?: (text: string) => void;
8
+ };
9
+ export declare function registerHivesTimers(hives: Command, deps: Deps): void;
10
+ export {};
@@ -0,0 +1,112 @@
1
+ import { open, unlink } from 'node:fs/promises';
2
+ import { Option } from 'commander';
3
+ import { readFunctionJson } from './function-input.js';
4
+ const STATES = ['scheduled', 'firing', 'fired', 'failed', 'cancelled'];
5
+ const SECRET_NOTE = 'The signing secret is shown once. Choose --print-secret-only (stdout is the secret and nothing else, e.g. `| gcloud secrets create NAME --data-file=-`) or --secret-file <path> (new file, mode 0600). The CLI never prints it any other way.';
6
+ /** Where the one-time signing secret goes. Exactly one sink must be chosen before anything is sent. */
7
+ async function secretSink(o, line) {
8
+ if (Boolean(o.printSecretOnly) === Boolean(o.secretFile))
9
+ throw new Error(`Choose exactly one of --print-secret-only or --secret-file. ${SECRET_NOTE}`);
10
+ if (o.printSecretOnly)
11
+ return { print: true, file: undefined, path: undefined, line };
12
+ // Create the file first (wx: never overwrite) so a bad path fails BEFORE Hives mints a secret that nothing could keep.
13
+ let file;
14
+ try {
15
+ file = await open(o.secretFile, 'wx', 0o600);
16
+ }
17
+ catch {
18
+ throw new Error('Could not create the secret file. It must be a new file in a writable directory.');
19
+ }
20
+ return { print: false, file, path: o.secretFile, line };
21
+ }
22
+ /** Close the sink's file and remove it: nothing was written, and an empty file would block a retry with the same path. */
23
+ async function discardSink(sink) {
24
+ if (!sink.file)
25
+ return;
26
+ await sink.file.close().catch(() => undefined);
27
+ await unlink(sink.path).catch(() => undefined);
28
+ }
29
+ /** The signing secret leaves the answer here and goes to exactly one place. Returns the answer without it. */
30
+ async function deliverSecret(sink, answer) {
31
+ const { signing_secret: secret, ...rest } = (answer ?? {});
32
+ if (typeof secret !== 'string' || !secret) {
33
+ await discardSink(sink);
34
+ throw new Error('Hives answered without a signing secret, so none was written. Nothing was echoed.');
35
+ }
36
+ if (sink.print) {
37
+ sink.line(secret);
38
+ return undefined;
39
+ }
40
+ try {
41
+ await sink.file.writeFile(secret + '\n');
42
+ }
43
+ catch (e) {
44
+ await discardSink(sink);
45
+ throw e;
46
+ }
47
+ await sink.file.close();
48
+ return { ...rest, signing_secret: `[written to ${sink.path}]` };
49
+ }
50
+ export function registerHivesTimers(hives, deps) {
51
+ const output = (data) => deps.emit(data, () => JSON.stringify(data, null, 2));
52
+ const line = deps.line ?? ((text) => process.stdout.write(text + '\n'));
53
+ const read = deps.readRequest ?? readFunctionJson;
54
+ const timers = hives.command('timers').description('One-shot timers that fire a signed callback (hives-callback-v1) at a registered target');
55
+ timers.command('create <project_id>').requiredOption('--request-file <path>', 'JSON {at, target_id, payload, idempotency_key}')
56
+ .action(async (project, o) => {
57
+ const body = await read(o.requestFile, 8 * 1024);
58
+ if (typeof body.at !== 'string' || typeof body.target_id !== 'string' || typeof body.idempotency_key !== 'string')
59
+ throw new Error('Timer JSON requires at, target_id and idempotency_key strings.');
60
+ output(await (await deps.client()).hives.timers.create(project, { ...body, at: body.at, target_id: body.target_id, idempotency_key: body.idempotency_key }));
61
+ });
62
+ timers.command('get <project_id> <timer_id>').action(async (project, timer) => output(await (await deps.client()).hives.timers.get(project, timer)));
63
+ timers.command('list <project_id>').option('--idempotency-key <key>').addOption(new Option('--state <state>').choices(STATES))
64
+ .option('--limit <number>', '1 to 100 (default 50)').option('--cursor <cursor>')
65
+ .action(async (project, o) => {
66
+ if (o.limit !== undefined && (!/^\d+$/.test(o.limit) || Number(o.limit) < 1 || Number(o.limit) > 100))
67
+ throw new Error('--limit must be an integer from 1 to 100.');
68
+ output(await (await deps.client()).hives.timers.list(project, { idempotency_key: o.idempotencyKey, state: o.state, limit: o.limit === undefined ? undefined : Number(o.limit), cursor: o.cursor }));
69
+ });
70
+ timers.command('cancel <project_id> <timer_id>').description('Cancel a scheduled timer (a fired or failed timer is returned unchanged)')
71
+ .action(async (project, timer) => output(await (await deps.client()).hives.timers.cancel(project, timer)));
72
+ const targets = timers.command('targets').description('Callback targets and their signing secrets');
73
+ targets.command('create <project_id>').description(`Register an https callback URL. ${SECRET_NOTE}`)
74
+ .requiredOption('--url <url>').option('--name <name>')
75
+ .option('--print-secret-only', 'print ONLY the signing secret (one trailing newline) and nothing else')
76
+ .option('--secret-file <path>', 'write the signing secret to a new 0600 file; the answer is printed without it')
77
+ .action(async (project, o) => {
78
+ const sink = await secretSink(o, line);
79
+ let answer;
80
+ try {
81
+ answer = await (await deps.client()).hives.timers.targets.create(project, { url: o.url, ...(o.name ? { name: o.name } : {}) });
82
+ }
83
+ catch (e) {
84
+ await discardSink(sink);
85
+ throw e;
86
+ }
87
+ const rest = await deliverSecret(sink, answer);
88
+ if (rest)
89
+ output(rest);
90
+ });
91
+ targets.command('list <project_id>').action(async (project) => output(await (await deps.client()).hives.timers.targets.list(project)));
92
+ targets.command('get <project_id> <target_id>').action(async (project, target) => output(await (await deps.client()).hives.timers.targets.get(project, target)));
93
+ targets.command('rotate <project_id> <target_id>').description(`Issue a new signing secret; the old one verifies for 24 hours. ${SECRET_NOTE}`)
94
+ .option('--print-secret-only', 'print ONLY the new signing secret (one trailing newline) and nothing else')
95
+ .option('--secret-file <path>', 'write the new signing secret to a new 0600 file; the answer is printed without it')
96
+ .action(async (project, target, o) => {
97
+ const sink = await secretSink(o, line);
98
+ let answer;
99
+ try {
100
+ answer = await (await deps.client()).hives.timers.targets.rotate(project, target);
101
+ }
102
+ catch (e) {
103
+ await discardSink(sink);
104
+ throw e;
105
+ }
106
+ const rest = await deliverSecret(sink, answer);
107
+ if (rest)
108
+ output(rest);
109
+ });
110
+ targets.command('delete <project_id> <target_id>').description('Delete a target (409 target_in_use while a scheduled timer or enabled cron references it)')
111
+ .action(async (project, target) => output(await (await deps.client()).hives.timers.targets.delete(project, target) ?? { deleted: true, target_id: target }));
112
+ }
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { registerIamDirectory } from './iam-directory.js';
7
7
  import { registerOperationCommands, registerProducts } from './products.js';
8
8
  import { registerOrgs } from './orgs.js';
9
9
  import { registerConnections } from './connections.js';
10
+ import { registerAgents } from './agents.js';
10
11
  import { Command, CommanderError } from 'commander';
11
12
  import { Genesis, GenesisError, errorBody, errorCatalog, errorOutcome, isMindsOperation, HivesOperationError, HivesQueueOperationError, MindsAgentError, MindsOwnerError, hivesResponseBytes, MAX_FUNCTION_BODY_BYTES, MAX_FUNCTION_JSON_BYTES, DEFAULT_BASE_URL, requireSecureUrl, redactUserinfo } from '@onesi/genesis';
12
13
  import { authConfig, completeLogin, selectSavedContext, savedLoginProvider, startLogin } from './auth.js';
@@ -48,7 +49,7 @@ export function buildProgram(deps = {}) {
48
49
  const auth = { ...authConfig(env, f), sleep: deps.sleep };
49
50
  const program = new Command('genesis')
50
51
  .description('One CLI for every Genesis service.')
51
- .version('0.2.13')
52
+ .version('0.2.15')
52
53
  .option('--json', 'machine-readable JSON output')
53
54
  .option('--api-key <key>', 'use this API key (default: GENESIS_API_KEY, then your `genesis login`)')
54
55
  .option('--base-url <url>', 'gateway URL (default: GENESIS_BASE_URL or https://os.one.ml/api)')
@@ -64,7 +65,7 @@ export function buildProgram(deps = {}) {
64
65
  const opts = {
65
66
  apiKey: o.apiKey ?? env.GENESIS_API_KEY, token: env.GENESIS_TOKEN,
66
67
  baseUrl: o.baseUrl ?? env.GENESIS_BASE_URL, routing: o.direct || env.GENESIS_ROUTING === 'direct' ? 'direct' : 'gateway',
67
- fetch: f, userAgent: 'genesis-cli/0.2.13',
68
+ fetch: f, userAgent: 'genesis-cli/0.2.15',
68
69
  credentials: store ? savedLoginProvider(auth, store) : undefined,
69
70
  };
70
71
  return new Genesis(hivesTransport ? { ...opts, fetch: createHivesNodeTransport(opts, deps.signal) } : opts);
@@ -276,6 +277,7 @@ export function buildProgram(deps = {}) {
276
277
  registerProducts(program, { client, emit, signal: deps.signal });
277
278
  registerOrgs(program, { client, emit, signal: deps.signal });
278
279
  registerConnections(program, { client, emit, signal: deps.signal });
280
+ registerAgents(program, { client, emit, signal: deps.signal });
279
281
  // Agent enrollment sends only the grant's enrollment key (mek_...), never this CLI's login or API key.
280
282
  (program.commands.find(c => c.name() === 'iam').commands.find(c => c.name() === 'agents')).command('enroll')
281
283
  .description('Enroll an agent under an agent enrollment grant (POST /v1/iam/agents/enroll); the credential secret is shown once')
@@ -286,7 +288,7 @@ export function buildProgram(deps = {}) {
286
288
  if (!key)
287
289
  throw new Error('Give the enrollment key with --key-file, --key-stdin or GENESIS_ENROLLMENT_KEY; it is never accepted as an argument.');
288
290
  const body = await readFunctionJson(o.bodyFile, 16 * 1024);
289
- const result = await new Genesis({ baseUrl: g().baseUrl ?? env.GENESIS_BASE_URL, fetch: f, userAgent: 'genesis-cli/0.2.13' }).iam.agents.enroll(key, body);
291
+ const result = await new Genesis({ baseUrl: g().baseUrl ?? env.GENESIS_BASE_URL, fetch: f, userAgent: 'genesis-cli/0.2.15' }).iam.agents.enroll(key, body);
290
292
  emit(result, () => JSON.stringify(result, null, 2));
291
293
  });
292
294
  // ---- auth ------------------------------------------------------------
@@ -838,7 +840,7 @@ export function buildProgram(deps = {}) {
838
840
  const url = `${base.href.replace(/\/+$/, '')}/v1/status`;
839
841
  let response;
840
842
  try {
841
- response = await f(url, { method: 'GET', headers: { accept: 'application/json', 'user-agent': 'genesis-cli/0.2.13' }, signal: AbortSignal.timeout(15_000) });
843
+ response = await f(url, { method: 'GET', headers: { accept: 'application/json', 'user-agent': 'genesis-cli/0.2.15' }, signal: AbortSignal.timeout(15_000) });
842
844
  }
843
845
  catch (e) {
844
846
  throw new Error(`Gateway unreachable at ${safe(url)}: ${safe(e.message)}`);
@@ -1,4 +1,4 @@
1
- import { readFile, stat } from 'node:fs/promises';
1
+ import { readFunctionJson } from './function-input.js';
2
2
  export function registerMindsOwner(minds, client, emit, signal) {
3
3
  const intent = (command, revision = true) => {
4
4
  command.requiredOption('--operation-id <uuid>', 'retain this non-nil UUID when reconciling the same intent');
@@ -23,19 +23,8 @@ export function registerMindsOwner(minds, client, emit, signal) {
23
23
  attachments.command('list <mind_id>').action(async (mind) => emit(await (await client()).minds.attachments.list(mind, { signal })));
24
24
  attachments.command('attach <mind_id>').requiredOption('--request-file <path>', 'closed JSON attachment intent with operationId, expectedVersion, subject, permissions and expiresAt')
25
25
  .action(async (mind, options) => {
26
- const metadata = await stat(options.requestFile);
27
- if (!metadata.isFile() || metadata.size > 8192)
28
- throw new Error('Attachment request must be at most 8 KiB.');
29
- const bytes = await readFile(options.requestFile);
30
- if (bytes.length > 8192)
31
- throw new Error('Attachment request must be at most 8 KiB.');
32
- let request;
33
- try {
34
- request = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
35
- }
36
- catch {
37
- throw new Error('Attachment request must contain valid JSON.');
38
- }
26
+ // The shared reader: a regular file or "-" for standard input, 8 KiB limit, no content echoed.
27
+ const request = await readFunctionJson(options.requestFile, 8192);
39
28
  emit(await (await client()).minds.attachments.attach(mind, request, { signal }));
40
29
  });
41
30
  intent(attachments.command('detach <mind_id> <attachment_id>')).action(async (mind, attachment, options) => emit(await (await client()).minds.attachments.detach(mind, attachment, body(options), { signal })));
package/dist/minds.js CHANGED
@@ -44,6 +44,39 @@ export function registerMinds(program, client, emit, agentClient = client, signa
44
44
  .addOption(new Option('--profile-id <profile>', 'existing Pro profile').choices(['F1', 'D1', 'D2']).makeOptionMandatory())
45
45
  .requiredOption('--idempotency-key <uuid>', 'persist this UUID4 and original generation; no automatic retry')
46
46
  .action(async (id, options) => emit(await (await client()).minds.resize(id, Number(options.expectedGeneration), options.profileId, { idempotencyKey: options.idempotencyKey })));
47
+ minds.command('profiles').description('List the Mind instance profiles (F1/D1/D2), the plans and environments each is offered in, and whether a change restarts the Mind')
48
+ .action(async () => emit(await (await client()).minds.profiles()));
49
+ const scaling = minds.command('scaling').description('Per-Mind scaling policy: idle window and scale to zero (sizes stay on resize); setting needs an org API key with minds:manage');
50
+ scaling.command('get <mind_id>').description('Read the stored policy (or the effective default, generation 0) and the current scaling state')
51
+ .action(async (id) => emit(await (await client()).minds.scaling.get(id)));
52
+ scaling.command('set <mind_id>').description('Set the policy from a JSON file; 409 SCALING_GENERATION_CONFLICT means it changed since you read it')
53
+ .requiredOption('--generation <number>', 'the stored policy generation from scaling get (0 when none is stored)')
54
+ .requiredOption('--file <path>', 'JSON policy {vertical, horizontal, idle}, at most 8 KiB')
55
+ .option('--idempotency-key <uuid>', 'optional UUID4 sent as Idempotency-Key')
56
+ .action(async (id, options) => {
57
+ if (!/^(?:0|[1-9][0-9]{0,15})$/.test(options.generation))
58
+ throw new Error('--generation must be a non-negative integer.');
59
+ const metadata = await stat(options.file);
60
+ if (!metadata.isFile() || metadata.size > 8192)
61
+ throw new Error('Policy file must be at most 8 KiB.');
62
+ const bytes = await readFile(options.file);
63
+ if (bytes.length > 8192)
64
+ throw new Error('Policy file exceeds 8 KiB.');
65
+ let policy;
66
+ try {
67
+ policy = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
68
+ }
69
+ catch {
70
+ throw new Error('Policy file must contain valid JSON.');
71
+ }
72
+ if (!policy || typeof policy !== 'object' || Array.isArray(policy))
73
+ throw new Error('Policy file must contain a JSON object.');
74
+ emit(await (await client()).minds.scaling.set(id, { generation: Number(options.generation), policy }, options.idempotencyKey === undefined ? {} : { idempotencyKey: options.idempotencyKey }));
75
+ });
76
+ scaling.command('events <mind_id>').description('List scaling decisions (policy sets, idle stops, wakes, resizes), newest first')
77
+ .option('--cursor <cursor>', 'next_cursor from the previous page')
78
+ .option('--limit <number>', '1–100 events (default 50)')
79
+ .action(async (id, options) => emit(await (await client()).minds.scaling.events(id, { ...(options.cursor === undefined ? {} : { cursor: options.cursor }), ...(options.limit === undefined ? {} : { limit: Number(options.limit) }) })));
47
80
  const deliveries = minds.command('webhook-deliveries').description('Durable webhook receipts for an owned Mind; a stopped-idle Mind is woken');
48
81
  deliveries.command('create <mind_id>').description('Record one delivery; replay the same delivery_id and body to read the original receipt')
49
82
  .requiredOption('--delivery-id <uuid>', 'caller-chosen non-nil UUID; retain it after an uncertain response')
package/dist/products.js CHANGED
@@ -8,7 +8,8 @@ import { readFunctionJson } from './function-input.js';
8
8
  export const PRODUCT_COMMANDS = {
9
9
  rigs: { description: 'Rigs: your desktops (One account login or org API key with a selected project)', commands: { list: 'rigs_listRigs', get: 'rigs_getRig', create: 'rigs_createRig', delete: 'rigs_deleteRig', action: 'rigs_actOnRig', term: 'rigs_openRigTerm' } },
10
10
  panes: { description: 'Panes: your browsers (One account login or org API key with a selected project)', commands: { list: 'panes_listPanes', get: 'panes_getPane', create: 'panes_createPane', delete: 'panes_deletePane', action: 'panes_actOnPane', term: 'panes_openPaneTerm', screenshot: 'panes_getPaneScreenshotData' } },
11
- shells: { description: 'Shells: your sandboxes (One account login or org API key with a selected project)', commands: { list: 'shells_listShells', get: 'shells_getShell', create: 'shells_createShell', delete: 'shells_deleteShell', action: 'shells_actOnShell', term: 'shells_openShellTerm', exec: 'shells_execShell' } },
11
+ shells: { description: 'Shells: your sandboxes (One account login or org API key with a selected project)', commands: { list: 'shells_listShells', get: 'shells_getShell', create: 'shells_createShell', delete: 'shells_deleteShell', action: 'shells_actOnShell', term: 'shells_openShellTerm', exec: 'shells_execShell',
12
+ egress: 'shells_getShellEgress', 'egress-log': 'shells_getShellEgressLog', 'egress-request': 'shells_requestShellEgress', 'egress-status': 'shells_getShellEgressRequest', 'egress-revoke': 'shells_revokeShellEgress' } },
12
13
  gateway: { description: 'Genesis gateway: status, the verified caller, usage, the error catalog and the served OpenAPI document (answered by the gateway itself)', commands: {
13
14
  status: 'gateway_getStatus', whoami: 'gateway_whoami', usage: 'gateway_getUsage', errors: 'gateway_listErrors', openapi: 'gateway_getOpenApi',
14
15
  } },
@@ -32,12 +33,14 @@ export const PRODUCT_COMMANDS = {
32
33
  'formation-jobs steps': 'orgs_list_formation_steps', 'formation-jobs cancel': 'orgs_cancel_formation_job',
33
34
  'record list': 'orgs_get_record', 'record verify': 'orgs_verify_record',
34
35
  } },
35
- billing: { description: 'Your organization billing: account, contacts, tax IDs, quotas, Minds account, invoices, usage report and the service catalog (login session, or an org API key with genesis.billing.read / .manage / genesis.usage.read). Nothing here pays or charges.', commands: {
36
+ billing: { description: 'Your organization billing: account, contacts, tax IDs, quotas, Minds account, invoices, usage report, the service catalog and OneOS Pro pricing (pricing and the catalog are public; the rest take a login session, or an org API key with genesis.billing.read / .manage / genesis.usage.read). Nothing here pays or charges.', commands: {
36
37
  'account get': 'gateway_getBillingAccount', 'account update': 'gateway_updateBillingAccount', 'account activity': 'gateway_listBillingActivity',
37
38
  'contacts list': 'gateway_listBillingContacts', 'contacts create': 'gateway_createBillingContact', 'contacts delete': 'gateway_deleteBillingContact',
38
39
  'tax-ids list': 'gateway_listBillingTaxIds', 'tax-ids create': 'gateway_createBillingTaxId', 'tax-ids delete': 'gateway_deleteBillingTaxId',
40
+ 'subscription get': 'gateway_getGenesisProSubscription', 'subscription create': 'gateway_createGenesisProSubscription',
41
+ 'card setup': 'gateway_createBillingCardSetupSession', 'card default': 'gateway_setBillingDefaultPaymentMethod',
39
42
  'quotas get': 'gateway_getOrgQuotas', 'quotas requests list': 'gateway_listQuotaRequests', 'quotas requests create': 'gateway_createQuotaRequest',
40
- 'minds-account': 'gateway_getMindsAccount', invoices: 'gateway_listOrgInvoices', 'usage-report': 'gateway_getOrgUsageReport', catalog: 'gateway_listBillingServices',
43
+ 'minds-account': 'gateway_getMindsAccount', invoices: 'gateway_listOrgInvoices', 'usage-report': 'gateway_getOrgUsageReport', catalog: 'gateway_listBillingServices', pricing: 'gateway_getBillingPricing',
41
44
  } },
42
45
  credits: { description: 'Your One credits: balance, catalog, ledger, usage, settings, checkout, wallet payment and subscription (login session, or an org API key with genesis.billing.read / .manage). Stripe test mode: checkout opens checkout.stripe.com and nothing is charged until you pay there.', commands: {
43
46
  balance: 'gateway_getCredits', catalog: 'gateway_getCreditCatalog', entries: 'gateway_listCreditEntries', usage: 'gateway_listCreditUsage', settings: 'gateway_updateCreditSettings',
@@ -238,6 +241,10 @@ export const PRODUCT_COMMANDS = {
238
241
  'lineage challenge': 'taps_createAgentLineageChallenge', 'lineage requests create': 'taps_createAgentLineageRequest',
239
242
  'lineage requests list': 'taps_listAgentLineageRequests', 'lineage requests get': 'taps_getAgentLineageRequest',
240
243
  } },
244
+ one: { description: 'One: your own One, read, stop and resume it and manage its wakes (One account login only; never an org API key or an agent)', commands: {
245
+ 'instance get': 'one_getInstance', 'instance stop': 'one_stopInstance', 'instance resume': 'one_resumeInstance',
246
+ 'wakeups list': 'one_listWakeups', 'wakeups create': 'one_createWakeup', 'wakeups cancel': 'one_cancelWakeup',
247
+ } },
241
248
  // `genesis minds engine <family> <verb> <mind_id> [params]`: every Minds engine route, generated from the grant table.
242
249
  minds: { description: 'Minds: your Minds in the selected environment (One account login)', commands: mindsEngineCommands },
243
250
  boxes: { description: 'Boxes: mail, docs, files, sheets, slides, calendar and identity (org API key; GET boxes:read, writes boxes:write, mail domains boxes:admin)', commands: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onesi/genesis-cli",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/l1felabs/genesis-api.git",
@@ -21,7 +21,7 @@
21
21
  "ai-agents",
22
22
  "api"
23
23
  ],
24
- "author": "One Superintelligence, Inc.",
24
+ "author": "L1fe AI Inc.",
25
25
  "homepage": "https://os.one.ml/docs",
26
26
  "engines": {
27
27
  "node": ">=20.19"
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "commander": "^14.0.0",
35
35
  "undici": "6.29.0",
36
- "@onesi/genesis": "0.2.13"
36
+ "@onesi/genesis": "0.2.15"
37
37
  },
38
38
  "optionalDependencies": {
39
39
  "@napi-rs/keyring": "^1.2.0"