@onesi/genesis-cli 0.0.1-stage → 0.2.0

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.
Files changed (93) hide show
  1. package/README.md +64 -0
  2. package/bin/genesis.js +3 -0
  3. package/dist/auth.d.ts +30 -0
  4. package/dist/auth.js +273 -0
  5. package/dist/beacons.d.ts +3 -0
  6. package/dist/beacons.js +162 -0
  7. package/dist/device-login.d.ts +25 -0
  8. package/dist/device-login.js +173 -0
  9. package/dist/dials.d.ts +7 -0
  10. package/dist/dials.js +99 -0
  11. package/dist/fleets.d.ts +4 -0
  12. package/dist/fleets.js +102 -0
  13. package/dist/flocks.d.ts +9 -0
  14. package/dist/flocks.js +326 -0
  15. package/dist/function-input.d.ts +3 -0
  16. package/dist/function-input.js +49 -0
  17. package/dist/henry.d.ts +7 -0
  18. package/dist/henry.js +85 -0
  19. package/dist/hives-coverage.d.ts +12 -0
  20. package/dist/hives-coverage.js +116 -0
  21. package/dist/hives-marketplace.d.ts +10 -0
  22. package/dist/hives-marketplace.js +29 -0
  23. package/dist/hives-node-transport.d.ts +6 -0
  24. package/dist/hives-node-transport.js +150 -0
  25. package/dist/hives-operations.d.ts +10 -0
  26. package/dist/hives-operations.js +63 -0
  27. package/dist/hives-platform.d.ts +13 -0
  28. package/dist/hives-platform.js +133 -0
  29. package/dist/hives-resources.d.ts +9 -0
  30. package/dist/hives-resources.js +94 -0
  31. package/dist/iam-directory.d.ts +15 -0
  32. package/dist/iam-directory.js +71 -0
  33. package/dist/index.d.ts +23 -0
  34. package/dist/index.js +872 -0
  35. package/dist/lifeid-native.d.ts +24 -0
  36. package/dist/lifeid-native.js +113 -0
  37. package/dist/minds-owner.d.ts +3 -0
  38. package/dist/minds-owner.js +42 -0
  39. package/dist/minds.d.ts +3 -0
  40. package/dist/minds.js +125 -0
  41. package/dist/onboard/art.d.ts +50 -0
  42. package/dist/onboard/art.js +139 -0
  43. package/dist/onboard/brand.d.ts +56 -0
  44. package/dist/onboard/brand.js +134 -0
  45. package/dist/onboard/caps.d.ts +90 -0
  46. package/dist/onboard/caps.js +232 -0
  47. package/dist/onboard/encode.d.ts +23 -0
  48. package/dist/onboard/encode.js +235 -0
  49. package/dist/onboard/eye.d.ts +103 -0
  50. package/dist/onboard/eye.js +147 -0
  51. package/dist/onboard/index.d.ts +61 -0
  52. package/dist/onboard/index.js +562 -0
  53. package/dist/onboard/mcp.d.ts +35 -0
  54. package/dist/onboard/mcp.js +118 -0
  55. package/dist/onboard/raster.d.ts +43 -0
  56. package/dist/onboard/raster.js +253 -0
  57. package/dist/onboard/sound.d.ts +2 -0
  58. package/dist/onboard/sound.js +54 -0
  59. package/dist/onboard/state.d.ts +24 -0
  60. package/dist/onboard/state.js +48 -0
  61. package/dist/onboard/term.d.ts +69 -0
  62. package/dist/onboard/term.js +292 -0
  63. package/dist/onboard/ui.d.ts +97 -0
  64. package/dist/onboard/ui.js +615 -0
  65. package/dist/orgs.d.ts +16 -0
  66. package/dist/orgs.js +89 -0
  67. package/dist/output.d.ts +28 -0
  68. package/dist/output.js +46 -0
  69. package/dist/products.d.ts +18 -0
  70. package/dist/products.js +311 -0
  71. package/dist/relays-commands/certificates.d.ts +3 -0
  72. package/dist/relays-commands/certificates.js +20 -0
  73. package/dist/relays-commands/domains.d.ts +3 -0
  74. package/dist/relays-commands/domains.js +23 -0
  75. package/dist/relays-commands/index.d.ts +4 -0
  76. package/dist/relays-commands/index.js +10 -0
  77. package/dist/relays-commands/monitoring.d.ts +3 -0
  78. package/dist/relays-commands/monitoring.js +41 -0
  79. package/dist/relays-commands/telemetry.d.ts +3 -0
  80. package/dist/relays-commands/telemetry.js +28 -0
  81. package/dist/relays-commands/zones.d.ts +5 -0
  82. package/dist/relays-commands/zones.js +9 -0
  83. package/dist/relays-enrollment.d.ts +3 -0
  84. package/dist/relays-enrollment.js +7 -0
  85. package/dist/runner.d.ts +4 -0
  86. package/dist/runner.js +57 -0
  87. package/dist/secret-input.d.ts +14 -0
  88. package/dist/secret-input.js +85 -0
  89. package/dist/store.d.ts +39 -0
  90. package/dist/store.js +33 -0
  91. package/dist/wasm-input.d.ts +2 -0
  92. package/dist/wasm-input.js +31 -0
  93. package/package.json +54 -1
package/dist/flocks.js ADDED
@@ -0,0 +1,326 @@
1
+ import { Option } from 'commander';
2
+ import { readFunctionJson } from './function-input.js';
3
+ import { safe, table } from './output.js';
4
+ /** Keys that must never reach a terminal or log, even if a server one day returned them. */
5
+ const SECRET_KEY = /^(?:credential_ref|sealed|envelope|ciphertext|enc|ct|token|access_token|refresh_token|secret|password|api_key|api_key_secret_ref|key_material)$/i;
6
+ export function scrub(value) {
7
+ if (Array.isArray(value))
8
+ return value.map(scrub);
9
+ if (value && typeof value === 'object')
10
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, SECRET_KEY.test(k) ? '[redacted]' : scrub(v)]));
11
+ return value;
12
+ }
13
+ /** A reference to a key the org already holds in Blinds or Secret Manager (never the key itself). */
14
+ export function checkSecretRef(ref) {
15
+ // A Blinds variable name in your org, written NAME or blinds:NAME. Secret Manager names are not supported (Blinds answers 422).
16
+ if (!/^(?:blinds:)?[A-Za-z][A-Za-z0-9_]{0,127}$/.test(ref) || /^(?:sk_|AIza|ghp_|xox[a-z]_)/i.test(ref) || ref.length >= 40)
17
+ throw new Error('--api-key-secret-ref takes the name of a Blinds variable your org already holds (NAME or blinds:NAME), never the key itself.');
18
+ return ref;
19
+ }
20
+ /** One line from stdin. On a terminal the keystrokes are read raw and never echoed; piped input is read to its end. */
21
+ export async function readPastedCode() {
22
+ const stdin = process.stdin;
23
+ let text = '';
24
+ if (stdin.isTTY) {
25
+ process.stderr.write('Paste the authorization code (input hidden), then press Enter: ');
26
+ stdin.setRawMode(true);
27
+ stdin.resume();
28
+ stdin.setEncoding('utf8');
29
+ await new Promise((resolve, reject) => {
30
+ const onData = (chunk) => {
31
+ for (const ch of chunk) {
32
+ if (ch === '\r' || ch === '\n') {
33
+ done();
34
+ return resolve();
35
+ }
36
+ if (ch === '\u0003') {
37
+ done();
38
+ return reject(new Error('Cancelled.'));
39
+ }
40
+ if (ch === '\u007f' || ch === '\b')
41
+ text = text.slice(0, -1);
42
+ else if (ch >= ' ')
43
+ text += ch;
44
+ }
45
+ };
46
+ const done = () => { stdin.off('data', onData); stdin.setRawMode(false); stdin.pause(); process.stderr.write('\n'); };
47
+ stdin.on('data', onData);
48
+ });
49
+ }
50
+ else {
51
+ for await (const chunk of stdin) {
52
+ text += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
53
+ if (text.length > 8192)
54
+ break;
55
+ }
56
+ }
57
+ return checkCode(text);
58
+ }
59
+ export function checkCode(raw) {
60
+ const code = raw.split(/\r?\n/)[0].trim();
61
+ if (!code || code.length > 512 || /[\u0000-\u001f\u007f]/.test(code))
62
+ throw new Error('Expected the authorization code on one line (at most 512 characters).');
63
+ return code;
64
+ }
65
+ const JOB_WAIT_DEFAULT_MS = 600_000;
66
+ const parseLabel = (value, previous = {}) => {
67
+ const i = value.indexOf('=');
68
+ if (i < 1)
69
+ throw new Error('--label takes key=value.');
70
+ return { ...previous, [value.slice(0, i)]: value.slice(i + 1) };
71
+ };
72
+ const bounded = (value, name, min, max) => {
73
+ if (value === undefined)
74
+ return undefined;
75
+ if (!/^\d+$/.test(value) || Number(value) < min || Number(value) > max)
76
+ throw new Error(`${name} must be an integer from ${min} to ${max}.`);
77
+ return Number(value);
78
+ };
79
+ export function registerFlocks(program, client, emit, env = process.env, readCode = readPastedCode, isJson = () => false) {
80
+ const flocks = program.command('flocks').description('Flocks: your machines and cloud instances with a harness installed (nodes, harnesses, agents and runs)');
81
+ const secret = (name) => {
82
+ const value = env[name];
83
+ if (!value)
84
+ throw new Error(`Set ${name} in the environment; do not pass Flocks credentials as command-line arguments.`);
85
+ return value;
86
+ };
87
+ const show = async (run, pretty) => {
88
+ const data = await run(await client());
89
+ emit(data, () => (pretty ? pretty(data) : JSON.stringify(data, null, 2)));
90
+ };
91
+ // Account, grant, instance and job commands redact secret-looking fields; enrollment commands (join-token) must print theirs.
92
+ const showSafe = (run, pretty) => show(async (g) => scrub(await run(g)), pretty);
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()));
95
+ const paging = (command) => command.option('--limit <n>', 'page size, 1–100').option('--cursor <cursor>', 'next_cursor from the previous page');
96
+ const query = (opts) => ({
97
+ ...(opts.limit === undefined ? {} : { limit: Number(opts.limit) }),
98
+ ...(opts.cursor === undefined ? {} : { cursor: opts.cursor }),
99
+ ...(opts.agentId === undefined ? {} : { agent_id: opts.agentId }),
100
+ });
101
+ paging(flocks.command('harnesses').description('List the harnesses each node reports, with version and readiness'))
102
+ .action((opts) => show(g => g.flocks.harnesses.list(query(opts))));
103
+ const agents = flocks.command('agents').description('Harness agents, and canonical agent execution (a_<ULID>; needs a signed-in session, not an API key)');
104
+ paging(agents.command('list').description('List agents')).action((opts) => show(g => g.flocks.agents.list(query(opts))));
105
+ agents.command('create <name> <harness>').description('Create an agent on a harness')
106
+ .option('--node <id>', 'pin the agent to one of your online nodes')
107
+ .action((name, harness, opts) => show(g => g.flocks.agents.create({ name, harness, ...(opts.node === undefined ? {} : { node_id: opts.node }) })));
108
+ agents.command('get <agent_id>').action((id) => show(g => g.flocks.agents.get(id)));
109
+ agents.command('execute <agent_id>').description('Execute a task on a canonical agent; the JSON request needs idempotency_key, task, driver, provider_account_id, target_roost and external_correlation')
110
+ .requiredOption('--body-file <path>', 'JSON request body, up to 64 KiB')
111
+ .action(async (id, opts) => { const body = await readFunctionJson(opts.bodyFile, 64 * 1024); return show(g => g.flocks.agents.execute(id, body)); });
112
+ agents.command('status <agent_id>').description('Observe the latest canonical execution').action((id) => show(g => g.flocks.agents.status(id)));
113
+ agents.command('cancel <agent_id>').description('Cancel the canonical execution admitted under an idempotency key')
114
+ .requiredOption('--idempotency-key <key>', 'the key the execution was admitted with')
115
+ .action((id, opts) => show(g => g.flocks.agents.cancel(id, opts.idempotencyKey)));
116
+ flocks.command('activity').description('Your recent agent sessions and events (needs a signed-in session, not an API key)').action(() => show(g => g.flocks.activity.get()));
117
+ const runs = flocks.command('runs').description('Recorded agent runs');
118
+ paging(runs.command('list').description('List runs').option('--agent-id <id>', 'only runs of this agent'))
119
+ .action((opts) => show(g => g.flocks.runs.list(query(opts))));
120
+ runs.command('get <run_id>').action((id) => show(g => g.flocks.runs.get(id)));
121
+ flocks.command('join-token').option('--ttl <seconds>', 'lifetime (60–600 seconds)', '600').action((opts) => {
122
+ const ttl = Number(opts.ttl);
123
+ if (!Number.isInteger(ttl) || ttl < 60 || ttl > 600)
124
+ throw new Error('TTL must be 60–600 seconds');
125
+ return show(g => g.flocks.joinTokens.create(ttl));
126
+ });
127
+ flocks.command('rename <id> <nickname>').action((id, nickname) => show(g => g.flocks.nodes.rename(id, nickname)));
128
+ flocks.command('revoke <id>').action((id) => show(g => g.flocks.nodes.revoke(id)));
129
+ flocks.command('enroll <name> <platform> <cpus>').description('Enroll this node with FLOCKS_JOIN_TOKEN from the environment').action((name, platform, cpus) => {
130
+ const cpu_count = Number(cpus);
131
+ if (!Number.isInteger(cpu_count) || cpu_count < 1 || cpu_count > 65536)
132
+ throw new Error('CPUs must be 1–65536');
133
+ const token = secret('FLOCKS_JOIN_TOKEN');
134
+ return show(g => g.flocks.nodes.enroll({ token, name, platform, cpu_count }));
135
+ });
136
+ flocks.command('heartbeat <id>').description('Send presence using FLOCKS_NODE_BINDING from the environment').action((id) => {
137
+ const binding = secret('FLOCKS_NODE_BINDING');
138
+ return show(g => g.flocks.nodes.heartbeat(id, binding));
139
+ });
140
+ flocks.command('exchange-join').description('Exchange FLOCKS_JOIN_TOKEN and FLOCKS_NODE_NONCE; save the returned key privately before activation').action(() => show(g => g.flocks.joinTokens.exchange({ token: secret('FLOCKS_JOIN_TOKEN'), node_nonce: secret('FLOCKS_NODE_NONCE') })));
141
+ flocks.command('activate <id> <name> <platform> <cpus>').description('Activate the saved node key with FLOCKS_NODE_NONCE').action((id, name, platform, cpus) => {
142
+ const cpu_count = Number(cpus);
143
+ if (!Number.isInteger(cpu_count) || cpu_count < 1 || cpu_count > 65536)
144
+ throw new Error('CPUs must be 1–65536');
145
+ return show(g => g.flocks.nodes.activate(id, { name, platform, cpu_count, node_nonce: secret('FLOCKS_NODE_NONCE') }));
146
+ });
147
+ flocks.command('rotate <id> <action>').description('prepare→persist→commit, or cancel; use saved FLOCKS_NODE_NONCE/FLOCKS_ROTATION_NONCE').action((id, action) => {
148
+ if (!['prepare', 'commit', 'cancel'].includes(action))
149
+ throw new Error('Action must be prepare, commit or cancel');
150
+ return show(g => g.flocks.nodes.rotateCredential(id, { action: action, node_nonce: secret('FLOCKS_NODE_NONCE'), operation_nonce: secret('FLOCKS_ROTATION_NONCE') }));
151
+ });
152
+ registerAccounts(flocks, client, showSafe, emit, env, readCode, isJson);
153
+ }
154
+ const ago = (t) => (typeof t === 'number' ? new Date(t * (t < 1e12 ? 1000 : 1)).toISOString().slice(0, 19) + 'Z' : '');
155
+ const items = (data) => (Array.isArray(data) ? data : Array.isArray(data?.items) ? data.items : []);
156
+ const accountRows = (data) => table(items(data).map(a => ({ ...a, owner: a.granted ? `${a.owner_org} (granted)` : 'you' })), ['id', 'provider', 'label', 'auth_kind', 'identity', 'status', 'owner']);
157
+ const jobText = (job) => {
158
+ const t = job.totals ?? {};
159
+ const head = `${safe(job.id)} ${safe(job.kind)} ${safe(job.state)}` + (job.account_id ? ` account ${safe(job.account_id)}` : '') + ` succeeded ${t.succeeded ?? 0}/${(job.items ?? []).length}`
160
+ + (t.failed ? `, failed ${t.failed}` : '') + (t.timed_out ? `, timed out ${t.timed_out}` : '') + (t.rolled_back ? `, rolled back ${t.rolled_back}` : '') + (t.unsupported ? `, unsupported ${t.unsupported}` : '');
161
+ const rows = (job.items ?? []).map((i) => ({ ...i, login: undefined }));
162
+ const logins = (job.items ?? []).filter((i) => i.login).map((i) => i.login.url && i.login.code
163
+ ? `Open ${safe(i.login.url)} and enter code ${safe(i.login.code)} (instance ${safe(i.instance_id)})`
164
+ : i.login.url
165
+ ? `Open ${safe(i.login.url)}, sign in, then send the code it shows with: genesis flocks jobs login-input ${safe(job.id)} --instance ${safe(i.instance_id)}`
166
+ : `Waiting for the sign-in code from instance ${safe(i.instance_id)}...`);
167
+ return [head, rows.length ? table(rows, ['instance_id', 'state', 'previous_account_id', 'identity', 'error_code']) : '', ...logins].filter(Boolean).join('\n');
168
+ };
169
+ const nodeRows = (data) => table(items(data).map(n => ({
170
+ id: n.id, nickname: n.nickname, state: n.state, flock: n.labels?.flock ?? '',
171
+ labels: Object.entries(n.labels ?? {}).filter(([k]) => k !== 'flock').map(([k, v]) => `${k}=${v}`),
172
+ accounts: (n.accounts ?? []).map((a) => `${a.provider}:${a.identity ?? a.account_id ?? 'none'}`),
173
+ })), ['id', 'nickname', 'state', 'flock', 'labels', 'accounts']);
174
+ function registerAccounts(flocks, client, show, emit, env, readCode, isJson) {
175
+ const interval = Number(env.FLOCKS_POLL_INTERVAL_MS) > 0 ? Number(env.FLOCKS_POLL_INTERVAL_MS) : 2000;
176
+ const finish = (job) => {
177
+ if (job.state === 'partial' || job.state === 'failed')
178
+ throw new Error(`Job ${safe(job.id)} finished ${safe(job.state)}: ${job.totals?.failed ?? 0} failed, ${job.totals?.timed_out ?? 0} timed out, ${job.totals?.rolled_back ?? 0} rolled back. Exit status 1.`);
179
+ };
180
+ /** Show the job the service accepted; with --wait, follow it to the end and fail the command when any instance failed. */
181
+ const showJob = async (start, wait) => {
182
+ const g = await client();
183
+ let job = scrub(await start(g));
184
+ if (wait && job.state === 'running')
185
+ job = scrub(await g.flocks.jobs.wait(job.id, { intervalMs: interval, timeoutMs: JOB_WAIT_DEFAULT_MS }));
186
+ emit(job, () => jobText(job));
187
+ if (wait)
188
+ finish(job);
189
+ };
190
+ const withJobId = async (g, r, wait) => {
191
+ // delete and revoke answer { account|grant, job_id }
192
+ if (!wait || !r?.job_id)
193
+ return r;
194
+ const job = await g.flocks.jobs.wait(r.job_id, { intervalMs: interval, timeoutMs: JOB_WAIT_DEFAULT_MS });
195
+ return { ...r, job };
196
+ };
197
+ flocks.command('providers').description('Inference providers, the login kinds each supports, and which plans fit sharing one login').action(() => show(g => g.flocks.providers.list(), data => items(data).map(p => `${safe(p.id)} ${safe(p.display_name)} (${(p.auth_kinds ?? []).map(safe).join(', ')})\n ${safe(p.plan_guidance)}`).join('\n')));
198
+ const accounts = flocks.command('accounts').description('Inference accounts: one login (or API key) many instances can run under');
199
+ accounts.command('list').description('Accounts your org owns plus accounts granted to it')
200
+ .option('--provider <id>', 'only this provider').option('--limit <n>', 'page size, 1-100').option('--cursor <cursor>', 'next_cursor from the previous page')
201
+ .action((o) => show(g => g.flocks.accounts.list({
202
+ ...(o.provider ? { provider: o.provider } : {}), ...(o.limit ? { limit: bounded(o.limit, '--limit', 1, 100) } : {}), ...(o.cursor ? { cursor: o.cursor } : {}),
203
+ }), accountRows));
204
+ accounts.command('add <provider> <label>').description('Add an account. subscription_oauth accounts are signed in afterwards with `accounts login`; api_key accounts take a secret reference only')
205
+ .option('--auth-kind <kind>', 'subscription_oauth or api_key', 'subscription_oauth')
206
+ .option('--api-key-secret-ref <ref>', 'api_key accounts: the name of a key your org already holds in Blinds (NAME or blinds:NAME)')
207
+ .addOption(new Option('--api-key <value>').hideHelp())
208
+ .action((provider, label, o) => {
209
+ if (o.apiKey !== undefined)
210
+ throw new Error('Raw API keys are never accepted on the command line. Store the key in Blinds or Secret Manager and pass --api-key-secret-ref.');
211
+ if (!['subscription_oauth', 'api_key'].includes(o.authKind))
212
+ throw new Error('--auth-kind must be subscription_oauth or api_key.');
213
+ if (o.authKind === 'api_key' && !o.apiKeySecretRef)
214
+ throw new Error('api_key accounts need --api-key-secret-ref (the name of a Blinds variable your org already holds).');
215
+ if (o.authKind !== 'api_key' && o.apiKeySecretRef)
216
+ throw new Error('--api-key-secret-ref is only for --auth-kind api_key.');
217
+ const ref = o.apiKeySecretRef === undefined ? undefined : checkSecretRef(o.apiKeySecretRef);
218
+ return show(g => g.flocks.accounts.create({ provider, label, auth_kind: o.authKind, ...(ref ? { api_key_secret_ref: ref } : {}) }), a => accountRows([a]));
219
+ });
220
+ accounts.command('get <account_id>').description('One account, with the number of instances bound to it')
221
+ .action((id) => show(g => g.flocks.accounts.get(id), a => accountRows([a]) + `\nbound instances: ${a.bound_instances ?? 0}`));
222
+ accounts.command('login <account_id>').description('Sign an account in from one of your instances; prints the device url and code (only you receive them)')
223
+ .requiredOption('--instance <id>', 'the instance that runs the provider login')
224
+ .option('--timeout <seconds>', 'how long to wait for the code', '60')
225
+ .action(async (id, o) => {
226
+ const g = await client();
227
+ const seconds = bounded(o.timeout, '--timeout', 0, 600);
228
+ let job = scrub(await g.flocks.accounts.login(id, o.instance));
229
+ const deadline = Date.now() + seconds * 1000;
230
+ const awaiting = (j) => (j.items ?? []).find((i) => i.login?.url && !i.login.code && i.state !== 'succeeded' && i.state !== 'failed');
231
+ while (job.state === 'running' && !(job.items ?? []).some((i) => i.login?.url) && Date.now() < deadline) {
232
+ await new Promise(r => setTimeout(r, interval));
233
+ job = scrub(await g.flocks.jobs.get(job.id));
234
+ }
235
+ const waiting = job.state === 'running' ? awaiting(job) : undefined;
236
+ if (waiting) {
237
+ // The provider printed a URL and waits for the pasted code: show the URL, then read the code without echoing it.
238
+ if (!isJson())
239
+ emit(job, () => jobText(job)); // --json prints one document, after the code is sent
240
+ const code = await readCode();
241
+ await g.flocks.jobs.submitLoginInput(job.id, waiting.instance_id, code);
242
+ job = scrub(await g.flocks.jobs.get(job.id));
243
+ emit(job, () => `Code sent.\n` + jobText(job) + (job.state === 'running' ? `\nFinish with: genesis flocks jobs get ${safe(job.id)} --wait` : ''));
244
+ return;
245
+ }
246
+ emit(job, () => jobText(job) + (job.state === 'running' ? `\nFinish with: genesis flocks jobs get ${safe(job.id)} --wait` : ''));
247
+ });
248
+ accounts.command('remove <account_id>').description('Sign the account out of every bound instance, then revoke its credential')
249
+ .option('--wait', 'follow the logout job to the end')
250
+ .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) : '')));
251
+ const grants = flocks.command('grants').description('Let another org bind its instances to one of your accounts');
252
+ grants.command('list').description('Grants you issued or received')
253
+ .option('--direction <dir>', 'issued or received', 'issued').option('--account <id>', 'only this account')
254
+ .action((o) => {
255
+ if (!['issued', 'received'].includes(o.direction))
256
+ throw new Error('--direction must be issued or received.');
257
+ return show(g => g.flocks.grants.list({ direction: o.direction, ...(o.account ? { account_id: o.account } : {}) }), d => table(items(d).map(x => ({ ...x, created: ago(x.created_at), revoked: ago(x.revoked_at) })), ['id', 'account_id', 'owner_org', 'grantee_org', 'status', 'note', 'created', 'revoked']));
258
+ });
259
+ grants.command('add <account_id> <grantee_org>').description('Grant an account you own to another org')
260
+ .option('--note <text>')
261
+ .action((id, org, o) => show(g => g.flocks.grants.create(id, { grantee_org_id: org, ...(o.note ? { note: o.note } : {}) })));
262
+ grants.command('revoke <grant_id>').description('Revoke a grant: new runs stop at once and grantee instances are signed out and wiped')
263
+ .option('--wait', 'follow the logout job to the end')
264
+ .action((id, o) => show(async (g) => withJobId(g, await g.flocks.grants.revoke(id), o.wait), r => `grant ${safe(r.grant?.id)} ${safe(r.grant?.status)}; logout job ${safe(r.job_id)}` + (r.job ? '\n' + jobText(r.job) : '')));
265
+ const instances = flocks.command('instances').description('Your instances and the inference account each runs under');
266
+ instances.command('list').description('Instances with their flock, labels and active accounts').action(() => show(g => g.flocks.nodes.list(), nodeRows));
267
+ instances.command('label <instance_id> [labels...]').description('Set labels (key=value) on an instance; the key flock groups instances. --remove drops a key')
268
+ .option('--remove <key...>', 'label keys to remove')
269
+ .action((id, pairs, o) => show(async (g) => {
270
+ const nodes = (await g.flocks.nodes.list());
271
+ const current = nodes.find(n => n.id === id);
272
+ if (!current)
273
+ throw new Error(`No instance ${id} in your org.`);
274
+ let labels = { ...(current.labels ?? {}) };
275
+ for (const p of pairs)
276
+ labels = parseLabel(p, labels);
277
+ for (const k of o.remove ?? [])
278
+ delete labels[k];
279
+ return g.flocks.nodes.update(id, { labels });
280
+ }, n => nodeRows([n])));
281
+ instances.command('account <instance_id>').description('Which account is bound to an instance, per provider')
282
+ .action((id) => show(g => g.flocks.instances.getAccount(id), d => table((d.accounts ?? []).map((a) => ({ ...a, updated: ago(a.updated_at) })), ['provider', 'account_id', 'identity', 'auth_state', 'binding_state', 'desired_account_id', 'updated'])));
283
+ const selecting = (command) => command
284
+ .option('--flock <name>', 'every instance in this flock (label flock)').option('--label <k=v>', 'instances carrying this label (repeatable)', parseLabel)
285
+ .option('--from-account <id>', 'instances currently bound to this account')
286
+ .option('--concurrency <n>', 'instances processed at once, 1-20 (bulk)')
287
+ .option('--wait', 'follow the job to the end; exits non-zero if any instance failed');
288
+ const selector = (ids, o) => {
289
+ const sel = { ...(ids.length ? { instance_ids: ids } : {}), ...(o.flock ? { flock: o.flock } : {}), ...(o.label ? { labels: o.label } : {}), ...(o.fromAccount ? { from_account_id: o.fromAccount } : {}) };
290
+ if (!Object.keys(sel).length)
291
+ throw new Error('Name instances, or select with --flock, --label or --from-account.');
292
+ return sel;
293
+ };
294
+ selecting(instances.command('switch-account [instance_ids...]').description('Switch instances to an account: drain, install, verify identity, swap (a failed verify rolls back)')
295
+ .requiredOption('--account <id>', 'the account to switch to').option('--provider <id>', 'provider when the account spans several')
296
+ .option('--force', 'cancel in-flight runs instead of waiting for them').option('--drain-timeout <seconds>', 'wait for in-flight runs, default 120'))
297
+ .action((ids, o) => {
298
+ const drain = bounded(o.drainTimeout, '--drain-timeout', 0, 3600);
299
+ const concurrency = bounded(o.concurrency, '--concurrency', 1, 20);
300
+ const extra = { ...(o.force ? { force: true } : {}), ...(drain === undefined ? {} : { drain_timeout_secs: drain }) };
301
+ const single = ids.length === 1 && !o.flock && !o.label && !o.fromAccount && concurrency === undefined;
302
+ return showJob(g => single
303
+ ? g.flocks.instances.switchAccount(ids[0], { account_id: o.account, ...(o.provider ? { provider: o.provider } : {}), ...extra })
304
+ : g.flocks.instances.bulkSwitchAccount({ selector: selector(ids, o), account_id: o.account, ...(concurrency === undefined ? {} : { concurrency }), ...extra }), o.wait);
305
+ });
306
+ selecting(instances.command('logout [instance_ids...]').description('Sign instances out of a provider account (files deleted; the provider login itself is untouched)')
307
+ .requiredOption('--provider <id>', 'the provider to sign out of')
308
+ .option('--force', 'cancel in-flight runs instead of waiting for them'))
309
+ .action((ids, o) => {
310
+ const concurrency = bounded(o.concurrency, '--concurrency', 1, 20);
311
+ const single = ids.length === 1 && !o.flock && !o.label && !o.fromAccount && concurrency === undefined;
312
+ return showJob(g => single
313
+ ? g.flocks.instances.logout(ids[0], o.provider, o.force)
314
+ : g.flocks.instances.bulkLogout({ selector: selector(ids, o), provider: o.provider, ...(o.force ? { force: true } : {}), ...(concurrency === undefined ? {} : { concurrency }) }), o.wait);
315
+ });
316
+ const jobs = flocks.command('jobs').description('Asynchronous account jobs (login, switch, sign-out)');
317
+ jobs.command('get <job_id>').description('A job and its per-instance results')
318
+ .option('--wait', 'poll until the job is no longer running; exits non-zero if any instance failed')
319
+ .action((id, o) => showJob(g => g.flocks.jobs.get(id), o.wait));
320
+ jobs.command('login-input <job_id>').description('Send the authorization code a headless sign-in is waiting for; the code is read from stdin, never from arguments')
321
+ .requiredOption('--instance <id>', 'the instance running the sign-in')
322
+ .action(async (id, o) => {
323
+ const code = await readCode();
324
+ await show(g => g.flocks.jobs.submitLoginInput(id, o.instance, code), () => 'Code sent. Follow the job with: genesis flocks jobs get ' + safe(id) + ' --wait');
325
+ });
326
+ }
@@ -0,0 +1,3 @@
1
+ export declare function readFunctionFile(path: string, limit: number): Promise<Uint8Array>;
2
+ export declare function readFunctionJson(path: string, limit: number): Promise<Record<string, unknown>>;
3
+ export declare function writeFunctionOutput(path: string, bytes: Uint8Array): Promise<void>;
@@ -0,0 +1,49 @@
1
+ import { open, writeFile } from 'node:fs/promises';
2
+ export async function readFunctionFile(path, limit) {
3
+ let file;
4
+ try {
5
+ file = await open(path, 'r');
6
+ }
7
+ catch {
8
+ throw new Error('Could not open the function input file. Check its path and permissions.');
9
+ }
10
+ try {
11
+ const stat = await file.stat();
12
+ if (!stat.isFile() || stat.size > limit)
13
+ throw new Error('Choose a regular function input file within the documented size limit.');
14
+ const bytes = new Uint8Array(limit + 1);
15
+ let size = 0;
16
+ for (;;) {
17
+ const { bytesRead } = await file.read(bytes, size, bytes.length - size, null);
18
+ if (!bytesRead)
19
+ break;
20
+ size += bytesRead;
21
+ if (size > limit)
22
+ throw new Error('Function input file exceeds the documented size limit.');
23
+ }
24
+ return bytes.slice(0, size);
25
+ }
26
+ finally {
27
+ await file.close();
28
+ }
29
+ }
30
+ export async function readFunctionJson(path, limit) {
31
+ const bytes = await readFunctionFile(path, limit);
32
+ try {
33
+ const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
34
+ if (!value || typeof value !== 'object' || Array.isArray(value))
35
+ throw new Error();
36
+ return value;
37
+ }
38
+ catch {
39
+ throw new Error('Function request file must contain a JSON object. No input value is echoed.');
40
+ }
41
+ }
42
+ export async function writeFunctionOutput(path, bytes) {
43
+ try {
44
+ await writeFile(path, bytes, { flag: 'wx', mode: 0o600 });
45
+ }
46
+ catch {
47
+ throw new Error('Could not create the output file. Choose a new path with write permission. Existing files are preserved.');
48
+ }
49
+ }
@@ -0,0 +1,7 @@
1
+ import type { Command } from 'commander';
2
+ import type { Genesis } from '@onesi/genesis';
3
+ /** `line` prints one NDJSON line (henry events --follow); `signal` stops a follow. */
4
+ export declare function addHenryCommands(program: Command, client: () => Promise<Genesis>, output: (value: unknown) => void, stream?: {
5
+ line: (text: string) => void;
6
+ signal?: AbortSignal;
7
+ }): void;
package/dist/henry.js ADDED
@@ -0,0 +1,85 @@
1
+ import { readFoundryKey } from './fleets.js';
2
+ const RUN_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
3
+ const DELIVERIES = ['steer', 'queue', 'interrupt'];
4
+ /** `line` prints one NDJSON line (henry events --follow); `signal` stops a follow. */
5
+ export function addHenryCommands(program, client, output, stream = { line: text => process.stdout.write(`${text}\n`) }) {
6
+ const number = (value, name) => {
7
+ if (value === undefined)
8
+ return undefined;
9
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
10
+ throw new Error(`${name} must be a non-negative integer`);
11
+ return Number(value);
12
+ };
13
+ const henry = program.command('henry').description('Henry: agent runtime, live activity and approvals');
14
+ henry.command('config').action(async () => output(await (await client()).henry.config()));
15
+ henry.command('credential').description('Read a Foundry API key from stdin and encrypt it in Genesis KMS')
16
+ .action(async () => {
17
+ const apiKey = await readFoundryKey();
18
+ output(await (await client()).henry.credential(apiKey));
19
+ });
20
+ henry.command('models').description('List the Foundry models the stored organization key can run').action(async () => output(await (await client()).henry.models()));
21
+ henry.command('runs').option('--page-token <token>').action(async (options) => output(await (await client()).henry.runs.list(options.pageToken)));
22
+ henry.command('run').requiredOption('--model <model>').requiredOption('--task <text>').option('--run-id <uuid>', 'UUIDv4 for concurrent activity polling')
23
+ .action(async (options) => output(await (await client()).henry.runs.create({ model: options.model, task: options.task, ...(options.runId ? { run_id: options.runId } : {}) })));
24
+ henry.command('get <run_id>').action(async (runId) => output(await (await client()).henry.runs.get(runId)));
25
+ henry.command('events <run_id>').description('Page a run\'s activity events, or follow them live with --follow')
26
+ .option('--after-seq <n>', 'return events after this sequence number').option('--limit <n>', 'page size, 1-500')
27
+ .option('--follow', 'stream events as NDJSON lines until the run ends (reconnects every 120 s stream)')
28
+ .action(async (runId, options) => {
29
+ const afterSeq = number(options.afterSeq, '--after-seq'), limit = number(options.limit, '--limit');
30
+ if (options.follow) {
31
+ if (limit !== undefined)
32
+ throw new Error('--limit pages events; --follow streams them');
33
+ const events = (await client()).henry.runs.streamEvents(runId, { ...(afterSeq === undefined ? {} : { afterSeq }), ...(stream.signal ? { signal: stream.signal } : {}) });
34
+ for await (const { event, data } of events)
35
+ stream.line(JSON.stringify({ event, ...data }));
36
+ return;
37
+ }
38
+ output(await (await client()).henry.runs.events(runId, { ...(afterSeq === undefined ? {} : { after_seq: afterSeq }), ...(limit === undefined ? {} : { limit }) }));
39
+ });
40
+ henry.command('message <run_id>').description('Send a message into a run: steer, queue (default) or interrupt')
41
+ .requiredOption('--text <text>').option('--delivery <delivery>', 'steer, queue or interrupt')
42
+ .action(async (runId, options) => {
43
+ if (options.delivery !== undefined && !DELIVERIES.includes(options.delivery))
44
+ throw new Error('--delivery must be steer, queue or interrupt');
45
+ output(await (await client()).henry.runs.message(runId, { text: options.text, ...(options.delivery ? { delivery: options.delivery } : {}) }));
46
+ });
47
+ henry.command('fork <run_id>').description('Start a new run from a past turn (0 = the original task; default the latest, which needs --text)')
48
+ .option('--turn <n>', 'turn to fork from').option('--text <text>', 'user message to append').option('--model <model>').option('--run-id <uuid>', 'UUIDv4 for the new run (idempotency key)')
49
+ .action(async (runId, options) => {
50
+ const turn = number(options.turn, '--turn');
51
+ if (options.runId !== undefined && !RUN_ID.test(options.runId))
52
+ throw new Error('--run-id must be a lowercase UUIDv4');
53
+ output(await (await client()).henry.runs.fork(runId, { ...(turn === undefined ? {} : { turn }), ...(options.text === undefined ? {} : { text: options.text }),
54
+ ...(options.model === undefined ? {} : { model: options.model }), ...(options.runId === undefined ? {} : { run_id: options.runId }) }));
55
+ });
56
+ henry.command('replay <run_id>').description('Read every event of a run and fold them into turns (each turn\'s inputs and answer)')
57
+ .action(async (runId) => output(await (await client()).henry.runs.replay(runId, stream.signal ? { signal: stream.signal } : {})));
58
+ henry.command('delete <run_id>').description('Delete a finished run (cancel active runs first)').action(async (runId) => output(await (await client()).henry.runs.delete(runId)));
59
+ henry.command('receipt-key').description('Print the current Henry receipt verification key').action(async () => output(await (await client()).henry.receipts.verificationKey()));
60
+ henry.command('blinds-proxy').description('Send an HTTPS request through the organization\'s Blinds machine')
61
+ .requiredOption('--method <method>', 'GET, POST, PUT, PATCH or DELETE').requiredOption('--url <https-url>')
62
+ .option('--header <name:value...>', 'request header, repeatable').option('--body <text>').option('--request-id <id>')
63
+ .action(async (options) => {
64
+ const headers = {};
65
+ for (const entry of options.header ?? []) {
66
+ const at = String(entry).indexOf(':');
67
+ if (at < 1)
68
+ throw new Error('--header must be name:value');
69
+ headers[entry.slice(0, at).trim()] = entry.slice(at + 1).trim();
70
+ }
71
+ output(await (await client()).henry.blinds.proxy({ method: String(options.method).toUpperCase(), url: options.url,
72
+ ...(Object.keys(headers).length ? { headers } : {}), ...(options.body === undefined ? {} : { body: options.body }), ...(options.requestId ? { request_id: options.requestId } : {}) }));
73
+ });
74
+ henry.command('command <run_id> <action>').option('--text <text>')
75
+ .action(async (runId, action, options) => {
76
+ if (!['pause', 'resume', 'cancel', 'input'].includes(action))
77
+ throw new Error('Action must be pause, resume, cancel or input');
78
+ output(await (await client()).henry.runs.command(runId, action, options.text));
79
+ });
80
+ henry.command('approval <run_id> <approval_id> <decision>').action(async (runId, approvalId, decision) => {
81
+ if (!['approve', 'reject'].includes(decision))
82
+ throw new Error('Decision must be approve or reject');
83
+ output(await (await client()).henry.runs.approval(runId, approvalId, decision));
84
+ });
85
+ }
@@ -0,0 +1,12 @@
1
+ import type { Command } from 'commander';
2
+ import { type Genesis, type OperationId } from '@onesi/genesis';
3
+ /** `genesis hives <words>` -> Hives operation. Positional arguments are the path parameters, query
4
+ * parameters become options, a JSON body comes from --body-file. Groups that already exist
5
+ * (functions, deployments, environment, domains, logs) are extended, never replaced. */
6
+ export declare const HIVES_COVERAGE_COMMANDS: Record<string, OperationId>;
7
+ /** Register after every other `hives` group so existing groups are found and extended. */
8
+ export declare function registerHivesCoverage(hives: Command, deps: {
9
+ client: () => Promise<Genesis>;
10
+ emit: (data: unknown, pretty: () => string) => void;
11
+ readRequest?: (path: string, limit: number) => Promise<unknown>;
12
+ }): void;
@@ -0,0 +1,116 @@
1
+ import { hivesSseWindow, idempotencyHeaders, operationTable } from '@onesi/genesis';
2
+ import { readFunctionJson } from './function-input.js';
3
+ /** `genesis hives <words>` -> Hives operation. Positional arguments are the path parameters, query
4
+ * parameters become options, a JSON body comes from --body-file. Groups that already exist
5
+ * (functions, deployments, environment, domains, logs) are extended, never replaced. */
6
+ export const HIVES_COVERAGE_COMMANDS = {
7
+ 'flights list': 'hives_list_flights',
8
+ 'flights create': 'hives_create_flight',
9
+ 'flights get': 'hives_get_flight',
10
+ 'flights files': 'hives_list_flight_files',
11
+ 'flights read-file': 'hives_read_flight_file',
12
+ 'flights processes': 'hives_list_flight_processes',
13
+ 'flights env': 'hives_list_flight_env',
14
+ 'flights debug-metrics': 'hives_list_flight_debug_metrics',
15
+ 'functions logs': 'hives_query_function_logs',
16
+ 'deployments stream-logs': 'hives_stream_deployment_logs',
17
+ 'logs aggregate': 'hives_aggregate_logs',
18
+ 'logs search': 'hives_search_logs',
19
+ 'environment values': 'hives_get_project_env',
20
+ 'environment set': 'hives_set_project_env',
21
+ 'environment delete': 'hives_delete_project_env',
22
+ 'domains get': 'hives_get_domain',
23
+ 'domains delete': 'hives_delete_domain',
24
+ 'domains verification-record': 'hives_get_verification_record',
25
+ };
26
+ const DESCRIPTIONS = {
27
+ flights: 'Flights: WASM HTTP workloads served at their own address',
28
+ logs: 'Project logs',
29
+ domains: 'Custom domains for a project',
30
+ };
31
+ const BODY_LIMIT = 1024 * 1024;
32
+ const flag = (name) => name.replace(/_/g, '-').toLowerCase();
33
+ const key = (name) => flag(name).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
34
+ const integer = (value, name, max) => {
35
+ if (value === undefined)
36
+ return undefined;
37
+ if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > max)
38
+ throw new Error(`${name} must be an integer from 1 to ${max}.`);
39
+ return Number(value);
40
+ };
41
+ const subcommand = (parent, name) => parent.commands.find((c) => c.name() === name) ?? parent.command(name).description(DESCRIPTIONS[name] ?? name);
42
+ function queryValue(schema, value) {
43
+ const type = schema?.type;
44
+ if (type === 'integer') {
45
+ if (!/^\d+$/.test(value))
46
+ throw new Error(`Expected an integer, got "${value}".`);
47
+ return Number(value);
48
+ }
49
+ if (type === 'boolean') {
50
+ if (value !== 'true' && value !== 'false')
51
+ throw new Error(`Expected true or false, got "${value}".`);
52
+ return value === 'true';
53
+ }
54
+ return value;
55
+ }
56
+ /** Register after every other `hives` group so existing groups are found and extended. */
57
+ export function registerHivesCoverage(hives, deps) {
58
+ const read = deps.readRequest ?? readFunctionJson;
59
+ const output = (data) => deps.emit(data ?? { ok: true }, () => JSON.stringify(data ?? { ok: true }, null, 2));
60
+ for (const [words, id] of Object.entries(HIVES_COVERAGE_COMMANDS)) {
61
+ const op = operationTable[id];
62
+ const parts = words.split(' ');
63
+ let parent = hives;
64
+ for (const part of parts.slice(0, -1))
65
+ parent = subcommand(parent, part);
66
+ const properties = (op.inputSchema.properties ?? {});
67
+ const required = op.inputSchema.required;
68
+ if (id === 'hives_delete_project_env') {
69
+ // The body is a JSON array of names: take them as arguments instead of a file.
70
+ parent.command(`${parts.at(-1)} <project_id> <names...>`).description(`${op.summary} (${op.method} ${op.path})`)
71
+ .action(async (project, names) => output(await (await deps.client()).request(id, { project_id: project, body: names })));
72
+ continue;
73
+ }
74
+ const command = parent.command([parts.at(-1), ...op.pathParams.map((p) => `<${p}>`)].join(' '))
75
+ .description(`${op.summary} (${op.method} ${op.path})`);
76
+ for (const q of op.queryParams) {
77
+ const description = properties[q]?.description;
78
+ command[required.includes(q) ? 'requiredOption' : 'option'](`--${flag(q)} <value>`, description);
79
+ }
80
+ if (op.hasBody)
81
+ command[required.includes('body') ? 'requiredOption' : 'option']('--body-file <path>', 'JSON request body, up to 1 MiB');
82
+ if (op.idempotencyKey)
83
+ command.option('--idempotency-key <key>', 'Required when slug is omitted; reuse it to get the original result');
84
+ if (op.stream) {
85
+ command.option('--max-events <number>', 'stop after this many events (1-1000, default 100)')
86
+ .option('--seconds <number>', 'stop after this many seconds (1-15, default 10)');
87
+ }
88
+ if (id === 'hives_get_project_env')
89
+ command.option('--stdout', 'write every decrypted value to stdout (avoid retained output)');
90
+ command.action(async (...args) => {
91
+ const options = args[op.pathParams.length];
92
+ if (id === 'hives_get_project_env' && !options.stdout)
93
+ throw new Error('Reading every environment value requires --stdout. Avoid logging or retaining the plaintext.');
94
+ const input = Object.fromEntries(op.pathParams.map((p, i) => [p, args[i]]));
95
+ for (const q of op.queryParams) {
96
+ const value = options[key(q)];
97
+ if (value === undefined)
98
+ continue;
99
+ input[q] = typeof value === 'boolean' ? value : queryValue(properties[q], value);
100
+ }
101
+ if (typeof options.bodyFile === 'string')
102
+ input.body = await read(options.bodyFile, BODY_LIMIT);
103
+ const client = await deps.client();
104
+ if (op.stream) {
105
+ const response = await client.raw(id, input, { headers: { accept: 'text/event-stream' } });
106
+ output(await hivesSseWindow(response, {
107
+ maxEvents: integer(options.maxEvents, '--max-events', 1000),
108
+ seconds: integer(options.seconds, '--seconds', 15),
109
+ }));
110
+ return;
111
+ }
112
+ const headers = idempotencyHeaders(id, options.idempotencyKey);
113
+ output(await client.request(id, input, { headers }));
114
+ });
115
+ }
116
+ }
@@ -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
+ };
8
+ /** Environment values belong in a bounded private JSON file, never command-line options. */
9
+ export declare function registerHivesMarketplace(hives: Command, deps: Deps): void;
10
+ export {};