@onesi/genesis-cli 0.2.7 → 0.2.8

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.7 --help
6
+ npx -y @onesi/genesis-cli@0.2.8 --help
7
7
  ```
8
8
 
9
9
  ## First run
package/dist/fleets.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { Command } from 'commander';
2
2
  import type { Genesis } from '@onesi/genesis';
3
- export declare function readFoundryKey(input?: AsyncIterable<Uint8Array>): Promise<string>;
4
- export declare function registerFleets(program: Command, client: () => Promise<Genesis>, emit: (data: unknown) => void, keyInput?: () => Promise<string>): void;
3
+ /** Read one key from stdin (at most 4096 bytes, one trailing newline dropped); `label` names it in errors. */
4
+ export declare function readStdinKey(label: string, input?: AsyncIterable<Uint8Array>): Promise<string>;
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;
package/dist/fleets.js CHANGED
@@ -1,11 +1,12 @@
1
- export async function readFoundryKey(input = process.stdin) {
1
+ /** Read one key from stdin (at most 4096 bytes, one trailing newline dropped); `label` names it in errors. */
2
+ export async function readStdinKey(label, input = process.stdin) {
2
3
  const chunks = [];
3
4
  let size = 0;
4
5
  for await (const chunk of input) {
5
6
  const bytes = Buffer.from(chunk);
6
7
  size += bytes.length;
7
8
  if (size > 4098)
8
- throw new Error('Foundry key must contain at most 4096 bytes.');
9
+ throw new Error(`${label} must contain at most 4096 bytes.`);
9
10
  chunks.push(bytes);
10
11
  }
11
12
  let key;
@@ -13,13 +14,23 @@ export async function readFoundryKey(input = process.stdin) {
13
14
  key = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(chunks)).replace(/\r?\n$/, '');
14
15
  }
15
16
  catch {
16
- throw new Error('Invalid UTF-8 Foundry key on stdin.');
17
+ throw new Error(`Invalid UTF-8 ${label} on stdin.`);
17
18
  }
18
19
  if (!key || Buffer.byteLength(key) > 4096 || /[\r\n\0]/.test(key))
19
- throw new Error('Invalid Foundry key on stdin.');
20
+ throw new Error(`Invalid ${label} on stdin.`);
20
21
  return key;
21
22
  }
22
- export function registerFleets(program, client, emit, keyInput = readFoundryKey) {
23
+ export const readFoundryKey = (input = process.stdin) => readStdinKey('Foundry key', input);
24
+ const readComputerKey = () => readStdinKey('Computer key');
25
+ const SEAT_TYPES = ['helper', 'operator', 'always_on'];
26
+ const seatType = (value) => {
27
+ if (!SEAT_TYPES.includes(value))
28
+ throw new Error('Seat type must be helper, operator or always_on.');
29
+ return value;
30
+ };
31
+ /** Whole-number flags go to the SDK as numbers; it refuses anything that is not a whole number before sending. */
32
+ const count = (value) => /^\d+$/.test(value) ? Number(value) : NaN;
33
+ export function registerFleets(program, client, emit, keyInput = readFoundryKey, computerKeyInput = readComputerKey) {
23
34
  const fleets = program.command('fleets').description('Fleets: managed Henry instances and tasks');
24
35
  const agents = fleets.command('agents').description('Persistent Henry agents on Fleets-managed compute');
25
36
  agents.command('list').action(async () => emit(await (await client()).fleets.agents.list()));
@@ -83,6 +94,86 @@ export function registerFleets(program, client, emit, keyInput = readFoundryKey)
83
94
  emit(await (await client()).fleets.connection.set(apiKey, options.model !== undefined ? { model: options.model } : {}));
84
95
  });
85
96
  connection.command('delete').action(async () => emit(await (await client()).fleets.connection.delete()));
97
+ const seats = fleets.command('seats').description('Agent seats: a persistent Henry agent with a credit purse and computer minutes (plan prices are display only)');
98
+ seats.command('plans').description('List the seat plans').action(async () => emit(await (await client()).fleets.seats.plans()));
99
+ seats.command('hire <type>').description('Hire a seat: helper, operator or always_on')
100
+ .option('--name <name>').option('--borrow-ceiling <credits>', 'overdraft the seat may run up before it pauses (default 0)')
101
+ .option('--pool <pool_id>', 'carve the purse under this pool').option('--no-fund', 'open the purse empty, to fund with top-up')
102
+ .requiredOption('--idempotency-key <key>', 'stable key to reuse on retry')
103
+ .action(async (type, options) => emit(await (await client()).fleets.seats.hire({
104
+ type: seatType(type),
105
+ ...(options.name !== undefined ? { name: options.name } : {}),
106
+ ...(options.borrowCeiling !== undefined ? { borrow_ceiling_credits: count(options.borrowCeiling) } : {}),
107
+ ...(options.pool !== undefined ? { pool_id: options.pool } : {}),
108
+ ...(options.fund === false ? { fund: false } : {}),
109
+ idempotency_key: options.idempotencyKey,
110
+ })));
111
+ seats.command('list').action(async () => emit(await (await client()).fleets.seats.list()));
112
+ seats.command('get <seat_id>').action(async (id) => emit(await (await client()).fleets.seats.get(id)));
113
+ seats.command('update <seat_id>').description('Rename a seat or change its borrow ceiling')
114
+ .option('--name <name>').option('--borrow-ceiling <credits>')
115
+ .action(async (id, options) => {
116
+ const body = {
117
+ ...(options.name !== undefined ? { name: options.name } : {}),
118
+ ...(options.borrowCeiling !== undefined ? { borrow_ceiling_credits: count(options.borrowCeiling) } : {}),
119
+ };
120
+ if (!Object.keys(body).length)
121
+ throw new Error('Provide --name or --borrow-ceiling.');
122
+ emit(await (await client()).fleets.seats.update(id, body));
123
+ });
124
+ seats.command('top-up <seat_id> <credits>').description('Add credits to the seat purse (overdraft is repaid first; pooled seats: top up the pool)')
125
+ .requiredOption('--idempotency-key <key>', 'stable key to reuse on retry')
126
+ .action(async (id, credits, options) => emit(await (await client()).fleets.seats.topUp(id, count(credits), options.idempotencyKey)));
127
+ seats.command('upgrade <seat_id> <type>').description('Move to a larger plan (no downgrade)')
128
+ .action(async (id, type) => emit(await (await client()).fleets.seats.upgrade(id, seatType(type))));
129
+ seats.command('hold <seat_id>').description('No new work; a running task is cancelled with a signed receipt').action(async (id) => emit(await (await client()).fleets.seats.hold(id)));
130
+ seats.command('resume <seat_id>').description('Resume a held or killed seat').action(async (id) => emit(await (await client()).fleets.seats.resume(id)));
131
+ seats.command('kill <seat_id>').description('Cancel running work now and stop the agent (resume allowed)').action(async (id) => emit(await (await client()).fleets.seats.kill(id)));
132
+ seats.command('release <seat_id>').description('Settle the purse and delete the agent (terminal)').action(async (id) => emit(await (await client()).fleets.seats.release(id)));
133
+ seats.command('ledger <seat_id>').description('Read the purse ledger').action(async (id) => emit(await (await client()).fleets.seats.ledger(id)));
134
+ const seatTasks = seats.command('tasks').description('Tasks given to a seat');
135
+ seatTasks.command('list <seat_id>').action(async (id) => emit(await (await client()).fleets.seats.tasks.list(id)));
136
+ seatTasks.command('submit <seat_id> <input>').option('--kind <kind>', 'research or computer', 'research').option('--timeout <seconds>', 'task deadline in seconds (1–3600)')
137
+ .option('--approve-computer', 'open the computer without a separate approval')
138
+ .requiredOption('--idempotency-key <key>', 'stable key to reuse on retry')
139
+ .action(async (id, input, options) => {
140
+ if (!['research', 'computer'].includes(options.kind))
141
+ throw new Error('Task kind must be research or computer.');
142
+ emit(await (await client()).fleets.seats.tasks.submit(id, {
143
+ input, kind: options.kind,
144
+ ...(options.timeout !== undefined ? { timeout_seconds: count(options.timeout) } : {}),
145
+ ...(options.approveComputer ? { approve_computer: true } : {}),
146
+ idempotency_key: options.idempotencyKey,
147
+ }));
148
+ });
149
+ seatTasks.command('get <seat_id> <task_id>').action(async (id, task) => emit(await (await client()).fleets.seats.tasks.get(id, task)));
150
+ for (const decision of ['approve', 'deny']) {
151
+ seatTasks.command(`${decision} <seat_id> <task_id> <approval_id>`).description(`${decision === 'approve' ? 'Approve' : 'Deny'} an action the task is waiting on`)
152
+ .action(async (id, task, approval) => emit(await (await client()).fleets.seats.tasks.decide(id, task, approval, decision)));
153
+ }
154
+ const pools = fleets.command('pools').description('Seat pools: a shared purse and computer minutes for seats hired with --pool');
155
+ pools.command('list').action(async () => emit(await (await client()).fleets.pools.list()));
156
+ pools.command('create').option('--name <name>').requiredOption('--credits <credits>').option('--computer-minutes <minutes>', 'default 0')
157
+ .requiredOption('--idempotency-key <key>', 'stable key to reuse on retry')
158
+ .action(async (options) => emit(await (await client()).fleets.pools.create({
159
+ ...(options.name !== undefined ? { name: options.name } : {}),
160
+ credits: count(options.credits), ...(options.computerMinutes !== undefined ? { computer_minutes: count(options.computerMinutes) } : {}), idempotency_key: options.idempotencyKey,
161
+ })));
162
+ pools.command('get <pool_id>').action(async (id) => emit(await (await client()).fleets.pools.get(id)));
163
+ pools.command('top-up <pool_id>').requiredOption('--credits <credits>').option('--computer-minutes <minutes>')
164
+ .requiredOption('--idempotency-key <key>', 'stable key to reuse on retry')
165
+ .action(async (id, options) => emit(await (await client()).fleets.pools.topUp(id, {
166
+ credits: count(options.credits),
167
+ ...(options.computerMinutes !== undefined ? { computer_minutes: count(options.computerMinutes) } : {}),
168
+ idempotency_key: options.idempotencyKey,
169
+ })));
170
+ pools.command('ledger <pool_id>').description('Read the pool purse ledger').action(async (id) => emit(await (await client()).fleets.pools.ledger(id)));
171
+ pools.command('release <pool_id>').description('Settle the pool (terminal)').action(async (id) => emit(await (await client()).fleets.pools.release(id)));
172
+ const computer = fleets.command('computer-connection').description('Organization key seat computer tasks use for Panes and Shells');
173
+ computer.command('get').action(async () => emit(await (await client()).fleets.computerConnection.get()));
174
+ computer.command('set').description('Read the key from stdin and encrypt it with Genesis KMS (never returned)')
175
+ .action(async () => emit(await (await client()).fleets.computerConnection.set(await computerKeyInput())));
176
+ computer.command('delete').action(async () => emit(await (await client()).fleets.computerConnection.delete()));
86
177
  const usage = fleets.command('usage').description('Usage observations derived from your tasks');
87
178
  usage.command('events').description('List task lifecycle and execution-time usage events').option('--limit <n>').option('--before <task_id>')
88
179
  .action(async (options) => emit(await (await client()).fleets.usage.events({ ...(options.limit ? { limit: Number(options.limit) } : {}), ...(options.before ? { before: options.before } : {}) })));
package/dist/index.js CHANGED
@@ -25,6 +25,12 @@ import { readFunctionFile, readFunctionJson, writeFunctionOutput } from './funct
25
25
  import { basename } from 'node:path';
26
26
  import { readWasmFile } from './wasm-input.js';
27
27
  import { readSecretInput, validateSecret, validateSecretValue, parseSecretBatch } from './secret-input.js';
28
+ /** A key or context environment: exactly sandbox or live, never a default for anything else. */
29
+ function keyEnvironment(value) {
30
+ if (value !== 'sandbox' && value !== 'live')
31
+ throw new Error('--environment must be sandbox or live.');
32
+ return value;
33
+ }
28
34
  function defaultOpen(url) {
29
35
  const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
30
36
  const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
@@ -41,7 +47,7 @@ export function buildProgram(deps = {}) {
41
47
  const auth = { ...authConfig(env, f), sleep: deps.sleep };
42
48
  const program = new Command('genesis')
43
49
  .description('One CLI for every Genesis service.')
44
- .version('0.2.7')
50
+ .version('0.2.8')
45
51
  .option('--json', 'machine-readable JSON output')
46
52
  .option('--api-key <key>', 'use this API key (default: GENESIS_API_KEY, then your `genesis login`)')
47
53
  .option('--base-url <url>', 'gateway URL (default: GENESIS_BASE_URL or https://os.one.ml/api)')
@@ -57,7 +63,7 @@ export function buildProgram(deps = {}) {
57
63
  const opts = {
58
64
  apiKey: o.apiKey ?? env.GENESIS_API_KEY, token: env.GENESIS_TOKEN,
59
65
  baseUrl: o.baseUrl ?? env.GENESIS_BASE_URL, routing: o.direct || env.GENESIS_ROUTING === 'direct' ? 'direct' : 'gateway',
60
- fetch: f, userAgent: 'genesis-cli/0.2.7',
66
+ fetch: f, userAgent: 'genesis-cli/0.2.8',
61
67
  credentials: store ? savedLoginProvider(auth, store) : undefined,
62
68
  };
63
69
  return new Genesis(hivesTransport ? { ...opts, fetch: createHivesNodeTransport(opts, deps.signal) } : opts);
@@ -278,7 +284,7 @@ export function buildProgram(deps = {}) {
278
284
  if (!key)
279
285
  throw new Error('Give the enrollment key with --key-file, --key-stdin or GENESIS_ENROLLMENT_KEY; it is never accepted as an argument.');
280
286
  const body = await readFunctionJson(o.bodyFile, 16 * 1024);
281
- const result = await new Genesis({ baseUrl: g().baseUrl ?? env.GENESIS_BASE_URL, fetch: f, userAgent: 'genesis-cli/0.2.7' }).iam.agents.enroll(key, body);
287
+ const result = await new Genesis({ baseUrl: g().baseUrl ?? env.GENESIS_BASE_URL, fetch: f, userAgent: 'genesis-cli/0.2.8' }).iam.agents.enroll(key, body);
282
288
  emit(result, () => JSON.stringify(result, null, 2));
283
289
  });
284
290
  // ---- auth ------------------------------------------------------------
@@ -355,12 +361,14 @@ export function buildProgram(deps = {}) {
355
361
  });
356
362
  const selection = program.command('context').description('Select an authorized Genesis organization, workspace and project');
357
363
  selection.command('select').requiredOption('--organization <id>').requiredOption('--workspace <id>').requiredOption('--project <id>')
358
- .description('Bind the saved human login to a Console Sandbox context and atomically replace its credentials')
364
+ .option('--environment <environment>', 'sandbox or live', 'sandbox')
365
+ .description('Bind the saved human login to a Console context (Sandbox unless --environment live) and atomically replace its credentials')
359
366
  .action(async (options) => {
367
+ const environmentId = keyEnvironment(options.environment);
360
368
  const target = { organizationId: options.organization, workspaceId: options.workspace, projectId: options.project,
361
- platformId: 'pf_console', fleetId: null, environmentId: 'sandbox' };
369
+ platformId: 'pf_console', fleetId: null, environmentId };
362
370
  const next = await selectSavedContext(auth, await getStore(), target, deps.signal);
363
- emit({ principal_id: next.auth?.principal_id, ...target }, () => `Selected ${target.organizationId} / ${target.workspaceId} / ${target.projectId} (Sandbox).`);
371
+ emit({ principal_id: next.auth?.principal_id, ...target }, () => `Selected ${target.organizationId} / ${target.workspaceId} / ${target.projectId} (${environmentId === 'live' ? 'Live' : 'Sandbox'}).`);
364
372
  });
365
373
  // ---- keys ------------------------------------------------------------
366
374
  const keys = program.command('keys').description('Org API keys (One account / WorkOS)');
@@ -368,16 +376,31 @@ export function buildProgram(deps = {}) {
368
376
  const gen = await client();
369
377
  const org = await orgId(gen);
370
378
  const r = await gen.keys.list(org);
371
- emit(r, () => table(r.data, ['id', 'name', 'obfuscated_value', 'scopes', 'last_used_at', 'expires_at']));
379
+ // environment is the one Auth bound the key to (its binding); a key with no recorded binding is the org default, Sandbox.
380
+ const rows = r.data.map(k => ({ ...k, environment: k.binding?.resourceEnvironment ?? 'sandbox' }));
381
+ emit(r, () => table(rows, ['id', 'name', 'environment', 'obfuscated_value', 'scopes', 'last_used_at', 'expires_at']));
372
382
  });
373
383
  keys.command('create <name>').description('Create a key (shown once), or with --blinds hand it straight to Blinds so nobody ever sees it')
374
384
  .option('--scopes <list>', 'comma-separated scopes, e.g. hives:read,relays:write')
375
385
  .option('--blinds', 'store the key in Blinds; the raw key is never shown to anyone')
376
386
  .option('--expires <iso>', 'expiry, e.g. 2027-01-01T00:00:00Z')
377
- .action(async (name, o) => {
387
+ .option('--environment <environment>', 'bind the key to sandbox or live (never changes later)', 'sandbox')
388
+ .option('--project <id>', 'bind the key to this project (default: the organization default project)')
389
+ .option('--workspace <id>', 'the project\'s workspace (needs --project)')
390
+ .action(async (name, o, cmd) => {
391
+ const resourceEnvironment = keyEnvironment(o.environment);
392
+ if (o.workspace && !o.project)
393
+ throw new Error('--workspace needs --project.');
394
+ // No --environment, --project or --workspace: send no binding, so Auth applies its own default (the org default
395
+ // project in sandbox; an org with no default project still gets a key, as before).
396
+ const chosen = cmd.getOptionValueSource('environment') === 'cli' || !!o.project;
397
+ const binding = chosen ? { resourceEnvironment, ...(o.project ? { projectId: o.project } : {}), ...(o.workspace ? { workspaceId: o.workspace } : {}) } : undefined;
378
398
  const gen = await client();
379
399
  const org = await orgId(gen);
380
- const k = await gen.keys.create(org, { name, custody: o.blinds ? 'blinds' : 'reveal', ...(o.scopes ? { scopes: o.scopes.split(',').map((x) => x.trim()).filter(Boolean) } : {}), ...(o.expires ? { expires_at: o.expires } : {}) });
400
+ const k = await gen.keys.create(org, { name, custody: o.blinds ? 'blinds' : 'reveal', ...(o.scopes ? { scopes: o.scopes.split(',').map((x) => x.trim()).filter(Boolean) } : {}), ...(o.expires ? { expires_at: o.expires } : {}), ...(binding ? { binding } : {}) });
401
+ const bound = k.binding?.resourceEnvironment;
402
+ if (binding && bound !== resourceEnvironment)
403
+ out.err(c.yellow(`Auth bound this key to ${bound ?? 'an unreported environment'}, not ${resourceEnvironment}. Revoke it if that is not what you want.`));
381
404
  emit(k, () => o.blinds
382
405
  ? `${c.green('✓')} Key ${c.bold(k.id)} created and stored in Blinds as ${c.bold(String(k.blinds_credential_ref))}.\n ${c.dim('No one, human or agent, will ever see the raw key.')}`
383
406
  : `${c.green('✓')} Key ${c.bold(k.id)} created. Copy it now; it won't be shown again:\n\n ${c.bold(String(k.value))}\n`);
@@ -813,7 +836,7 @@ export function buildProgram(deps = {}) {
813
836
  const url = `${base.href.replace(/\/+$/, '')}/v1/status`;
814
837
  let response;
815
838
  try {
816
- response = await f(url, { method: 'GET', headers: { accept: 'application/json', 'user-agent': 'genesis-cli/0.2.7' }, signal: AbortSignal.timeout(15_000) });
839
+ response = await f(url, { method: 'GET', headers: { accept: 'application/json', 'user-agent': 'genesis-cli/0.2.8' }, signal: AbortSignal.timeout(15_000) });
817
840
  }
818
841
  catch (e) {
819
842
  throw new Error(`Gateway unreachable at ${safe(url)}: ${safe(e.message)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onesi/genesis-cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
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.7"
36
+ "@onesi/genesis": "0.2.8"
37
37
  },
38
38
  "optionalDependencies": {
39
39
  "@napi-rs/keyring": "^1.2.0"