@lifesi/genesis-cli 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/beacons.js CHANGED
@@ -1,5 +1,49 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { createPrivateKey, sign } from 'node:crypto';
1
3
  export function registerBeacons(program, client, emit) {
2
- const devices = program.command('beacons').description('Beacons: Agent-native mesh').command('devices').description('Manage Bridge devices in your signed-in organization');
4
+ const beacons = program.command('beacons').description('Beacons: Agent-native mesh');
5
+ beacons.command('fleets').description('Provision Bridge metadata in your verified selection')
6
+ .command('provision').requiredOption('--request-id <uuid>', 'Stable UUID; reuse with the same name on retry')
7
+ .option('--name <name>', 'Optional fleet name')
8
+ .action(async (opts) => {
9
+ const result = await (await client()).beacons.fleets.provision(opts.requestId, opts.name === undefined ? {} : { name: opts.name });
10
+ emit(result, () => JSON.stringify(result, null, 2));
11
+ });
12
+ const devices = beacons.command('devices').description('Manage Bridge devices in your signed-in organization');
13
+ devices.command('challenge <fleet-id>').requiredOption('--proof-key <key>', 'Base64url Ed25519 public key').requiredOption('--tunnel-key <key>', 'Base64url WireGuard public key').option('--transport <transport>', 't1_full or t2_sandboxed', 't1_full')
14
+ .action(async (fleetId, opts) => {
15
+ if (opts.transport !== 't1_full' && opts.transport !== 't2_sandboxed')
16
+ throw new Error('Unsupported device transport.');
17
+ const result = await (await client()).beacons.devices.challenge({ fleet_id: fleetId, proof_key: opts.proofKey, tunnel_key: opts.tunnelKey, transport: opts.transport });
18
+ emit(result, () => JSON.stringify(result, null, 2));
19
+ });
20
+ devices.command('enroll <challenge-file>').requiredOption('--key-file <path>', 'Local Ed25519 PEM private key; never uploaded')
21
+ .action(async (challengeFile, opts) => {
22
+ const challenge = JSON.parse(await readFile(challengeFile, 'utf8'));
23
+ const fields = ['version', 'id', 'nonce', 'issuer', 'organization', 'principal', 'workspace', 'project', 'environment', 'binding_revision', 'transport', 'fleet_id', 'proof_key', 'tunnel_key', 'issued_at', 'expires_at'];
24
+ if (Object.keys(challenge).length !== fields.length || fields.some(field => challenge[field] === undefined))
25
+ throw new Error('Invalid challenge file.');
26
+ const canonical = Object.fromEntries(fields.map(field => [field, challenge[field]]));
27
+ const key = createPrivateKey(await readFile(opts.keyFile));
28
+ if (key.asymmetricKeyType !== 'ed25519')
29
+ throw new Error('An Ed25519 device key is required.');
30
+ const signature = sign(null, Buffer.from('Genesis Beacons device enrollment v1\n' + JSON.stringify(canonical)), key).toString('base64url');
31
+ const result = await (await client()).beacons.devices.enroll({ challenge, signature });
32
+ emit(result, () => JSON.stringify(result, null, 2));
33
+ });
34
+ devices.command('revocations <id>').description('Poll durable transport teardown commands').action(async (id) => {
35
+ const result = await (await client()).beacons.devices.revocations(id);
36
+ emit(result, () => JSON.stringify(result, null, 2));
37
+ });
38
+ devices.command('ack <id> <version>').requiredOption('--action <action>', 'Applied interface_torn_down or peer_key_removed; apply transport action first').action(async (id, version, opts) => {
39
+ const numeric = Number(version);
40
+ if (!Number.isSafeInteger(numeric) || numeric < 1)
41
+ throw new Error('Invalid revocation version.');
42
+ if (opts.action !== 'interface_torn_down' && opts.action !== 'peer_key_removed')
43
+ throw new Error('Invalid transport action.');
44
+ const result = await (await client()).beacons.devices.acknowledge(id, numeric, { action: opts.action });
45
+ emit(result, () => JSON.stringify(result, null, 2));
46
+ });
3
47
  devices.command('list', { isDefault: true }).action(async () => {
4
48
  const result = await (await client()).beacons.devices.list();
5
49
  emit(result, () => JSON.stringify(result, null, 2));
package/dist/dials.js CHANGED
@@ -28,13 +28,60 @@ export function registerDials(program, dependencies) {
28
28
  numbers.command('drift').action(async () => { const { client, scope } = await context(); emit(await client.dials.numbers.drift(scope)); });
29
29
  dials.command('overview').action(async () => { const { client, scope } = await context(); emit(await client.dials.overview.summary(scope)); });
30
30
  dials.command('operating-surface').action(async () => { const { client, scope } = await context(); emit(await client.dials.overview.operatingSurface(scope)); });
31
+ for (const [name, list, create] of [['people', 'dials_listHumanProfiles', 'dials_createHumanProfile'], ['agents', 'dials_listAgents', 'dials_createAgent'], ['voice', 'dials_listVoiceProfiles', 'dials_createVoiceProfile']]) {
32
+ const profiles = dials.command(name).description('Saved profile configuration in the selected scope');
33
+ profiles.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.call(list, scope)); });
34
+ profiles.command('create').requiredOption('--body <json-or-@file>', 'Unassigned profile fields and actorId; same-name POST updates the stored profile').action(async (options) => {
35
+ const { client, scope } = await context();
36
+ const text = options.body.startsWith('@') ? await readFile(options.body.slice(1), 'utf8') : options.body;
37
+ const body = JSON.parse(text);
38
+ if (!body || typeof body !== 'object' || Array.isArray(body) || 'phoneNumberId' in body)
39
+ throw new Error('Supply unassigned profile fields; use numbers assign after saving.');
40
+ emit(await client.call(create, { body: { ...body, scope } }));
41
+ });
42
+ }
43
+ const readBody = async (value) => {
44
+ const body = JSON.parse(value.startsWith('@') ? await readFile(value.slice(1), 'utf8') : value);
45
+ if (!body || typeof body !== 'object' || Array.isArray(body))
46
+ throw new Error('Dials body must be a JSON object.');
47
+ return body;
48
+ };
49
+ const messages = dials.command('messages').description('Saved messages, templates and pending registration configuration');
50
+ messages.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.dials.messages.list(scope)); });
51
+ messages.command('send').requiredOption('--body <json-or-@file>', 'Explicit approvals, actor, sender, destination, body and idempotency key')
52
+ .action(async (options) => { const { client, scope } = await context(); const body = await readBody(options.body); emit(await client.call('dials_createOutboundMessage', { body: { ...body, scope } })); });
53
+ for (const [name, list, create] of [
54
+ ['templates', 'dials_listMessageTemplates', 'dials_createMessageTemplate'],
55
+ ['brands', 'dials_listMessagingBrands', 'dials_createMessagingBrand'],
56
+ ['campaigns', 'dials_listMessagingCampaigns', 'dials_createMessagingCampaign'],
57
+ ]) {
58
+ const group = messages.command(name).description('Scoped saved configuration; creation is not provider approval');
59
+ group.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.call(list, scope)); });
60
+ group.command('create').requiredOption('--body <json-or-@file>', 'Configuration fields and actorId')
61
+ .action(async (options) => { const { client, scope } = await context(); const body = await readBody(options.body); emit(await client.call(create, { body: { ...body, scope } })); });
62
+ if (name === 'campaigns')
63
+ group.command('assign').requiredOption('--campaign <id>', 'Owned campaign ID').requiredOption('--body <json-or-@file>', 'Owned phoneNumberId and actorId')
64
+ .action(async (options) => { const { client, scope } = await context(); const body = await readBody(options.body); emit(await client.call('dials_assignCampaignNumber', { campaign_id: options.campaign, body: { ...body, scope } })); });
65
+ }
66
+ const routing = dials.command('routing').description('Managed owned-number inbound policies; workflow documents are separate');
67
+ routing.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.dials.routing.list(scope)); });
68
+ routing.command('create').requiredOption('--body <json-or-@file>', 'Owned source, supported action, priority and persisted original UUID; creates an enabled policy')
69
+ .action(async (options) => { const { client, scope } = await context(); const body = await readBody(options.body); emit(await client.call('dials_createRoutingRule', { body: { ...body, scope } })); });
70
+ routing.command('disable').requiredOption('--rule <id>', 'Owned managed policy ID; record is retained')
71
+ .action(async (options) => { const { client, scope } = await context(); emit(await client.dials.routing.disable(options.rule, { scope })); });
72
+ const workflows = dials.command('workflows').description('Persisted routing descriptors; execution is verified separately');
73
+ workflows.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.dials.workflows.list(scope)); });
74
+ workflows.command('deploy').requiredOption('--body <json-or-@file>', 'Workflow graph and actorId')
75
+ .action(async (options) => { const { client, scope } = await context(); const body = await readBody(options.body); emit(await client.call('dials_deployWorkflow', { body: { ...body, scope } })); });
76
+ 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')
77
+ .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 } : {}) })); });
31
78
  const calls = dials.command('calls').description('Call activity and approved outbound calls');
32
79
  calls.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.dials.calls.list(scope)); });
33
80
  // @file keeps structured approval and idempotency fields out of shell history.
34
81
  for (const [parent, name, operation] of [
35
82
  [numbers, 'caller-name', 'dials_updateNumberCallerName'], [numbers, 'reconcile', 'dials_reconcileNumberInventory'],
36
83
  [numbers, 'provision', 'dials_provisionNumber'], [numbers, 'assign', 'dials_assignNumber'], [numbers, 'release', 'dials_releaseNumber'], [numbers, 'e911', 'dials_registerEmergencyAddress'],
37
- [calls, 'validate', 'dials_validateOutboundCall'], [calls, 'create', 'dials_createOutboundCall'],
84
+ [calls, 'validate', 'dials_validateOutboundCall'], [calls, 'create', 'dials_createOutboundCall'], [calls, 'recover', 'dials_recoverOutboundCall'],
38
85
  ]) {
39
86
  parent.command(name).requiredOption('--body <json-or-@file>', 'JSON input including actorId and required operation fields')
40
87
  .action(async (options) => {
package/dist/fleets.js CHANGED
@@ -20,7 +20,14 @@ export async function readFoundryKey(input = process.stdin) {
20
20
  return key;
21
21
  }
22
22
  export function registerFleets(program, client, emit, keyInput = readFoundryKey) {
23
- const fleets = program.command('fleets').description('Fleets: managed agent tasks');
23
+ const fleets = program.command('fleets').description('Fleets: managed Henry instances and tasks');
24
+ const instances = fleets.command('instances').description('Actual admitted Henry Host status');
25
+ instances.command('list').option('--limit <number>').option('--after <instance_id>').action(async (options) => {
26
+ emit(await (await client()).fleets.instances.list({ ...(options.limit ? { limit: Number(options.limit) } : {}), ...(options.after ? { after: options.after } : {}) }));
27
+ });
28
+ instances.command('get <instance_id>').action(async (id) => emit(await (await client()).fleets.instances.get(id)));
29
+ instances.command('stop <instance_id>').description('Stop admitted Henry compute and retain its data').option('--idempotency-key <uuid>', 'canonical UUID4 to retain after an uncertain reply').action(async (id, options) => emit(await (await client()).fleets.instances.stop(id, { idempotencyKey: options.idempotencyKey })));
30
+ instances.command('start <instance_id>').description('Connect already admitted Henry compute').option('--idempotency-key <uuid>', 'canonical UUID4 to retain after an uncertain reply').action(async (id, options) => emit(await (await client()).fleets.instances.start(id, { idempotencyKey: options.idempotencyKey })));
24
31
  const connection = fleets.command('connection').description('Organization-owned Foundry connection');
25
32
  connection.command('get').action(async () => emit(await (await client()).fleets.connection.get()));
26
33
  connection.command('set').description('Read a Foundry key from stdin and encrypt it with Genesis KMS').action(async () => {
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ import type { Genesis } from '@lifesi/genesis';
3
+ export declare function registerFlocks(program: Command, client: () => Promise<Genesis>, emit: (data: unknown, pretty: () => string) => void, env?: Record<string, string | undefined>): void;
package/dist/flocks.js ADDED
@@ -0,0 +1,46 @@
1
+ export function registerFlocks(program, client, emit, env = process.env) {
2
+ const flocks = program.command('flocks').description('Flocks: Agents on your hardware');
3
+ const secret = (name) => {
4
+ const value = env[name];
5
+ if (!value)
6
+ throw new Error(`Set ${name} in the environment; do not pass Flocks credentials as command-line arguments.`);
7
+ return value;
8
+ };
9
+ const show = async (run) => {
10
+ const data = await run(await client());
11
+ emit(data, () => JSON.stringify(data, null, 2));
12
+ };
13
+ flocks.command('readiness').description('Check whether node enrollment is currently available').action(() => show(g => g.flocks.readiness.get()));
14
+ flocks.command('nodes').description('List your org nodes').action(() => show(g => g.flocks.nodes.list()));
15
+ flocks.command('join-token').option('--ttl <seconds>', 'lifetime (60–600 seconds)', '600').action((opts) => {
16
+ const ttl = Number(opts.ttl);
17
+ if (!Number.isInteger(ttl) || ttl < 60 || ttl > 600)
18
+ throw new Error('TTL must be 60–600 seconds');
19
+ return show(g => g.flocks.joinTokens.create(ttl));
20
+ });
21
+ flocks.command('rename <id> <nickname>').action((id, nickname) => show(g => g.flocks.nodes.rename(id, nickname)));
22
+ flocks.command('revoke <id>').action((id) => show(g => g.flocks.nodes.revoke(id)));
23
+ flocks.command('enroll <name> <platform> <cpus>').description('Enroll this node with FLOCKS_JOIN_TOKEN from the environment').action((name, platform, cpus) => {
24
+ const cpu_count = Number(cpus);
25
+ if (!Number.isInteger(cpu_count) || cpu_count < 1 || cpu_count > 65536)
26
+ throw new Error('CPUs must be 1–65536');
27
+ const token = secret('FLOCKS_JOIN_TOKEN');
28
+ return show(g => g.flocks.nodes.enroll({ token, name, platform, cpu_count }));
29
+ });
30
+ flocks.command('heartbeat <id>').description('Send presence using FLOCKS_NODE_BINDING from the environment').action((id) => {
31
+ const binding = secret('FLOCKS_NODE_BINDING');
32
+ return show(g => g.flocks.nodes.heartbeat(id, binding));
33
+ });
34
+ 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') })));
35
+ flocks.command('activate <id> <name> <platform> <cpus>').description('Activate the saved node key with FLOCKS_NODE_NONCE').action((id, name, platform, cpus) => {
36
+ const cpu_count = Number(cpus);
37
+ if (!Number.isInteger(cpu_count) || cpu_count < 1 || cpu_count > 65536)
38
+ throw new Error('CPUs must be 1–65536');
39
+ return show(g => g.flocks.nodes.activate(id, { name, platform, cpu_count, node_nonce: secret('FLOCKS_NODE_NONCE') }));
40
+ });
41
+ flocks.command('rotate <id> <action>').description('prepare→persist→commit, or cancel; use saved FLOCKS_NODE_NONCE/FLOCKS_ROTATION_NONCE').action((id, action) => {
42
+ if (!['prepare', 'commit', 'cancel'].includes(action))
43
+ throw new Error('Action must be prepare, commit or cancel');
44
+ return show(g => g.flocks.nodes.rotateCredential(id, { action: action, node_nonce: secret('FLOCKS_NODE_NONCE'), operation_nonce: secret('FLOCKS_ROTATION_NONCE') }));
45
+ });
46
+ }
@@ -11,6 +11,8 @@ export async function closeHivesNodeTransport() {
11
11
  await current?.destroy();
12
12
  }
13
13
  export function hivesOperationBudget(id) {
14
+ if (['hives_list_buckets', 'hives_create_bucket', 'hives_get_bucket', 'hives_delete_bucket', 'hives_list_objects', 'hives_upload_object', 'hives_upload_object_by_key', 'hives_delete_object', 'hives_delete_object_by_key', 'hives_download_object_by_key', 'hives_get_cors_config', 'hives_set_cors_config', 'hives_generate_presigned_url', 'hives_reconcile'].includes(id))
15
+ return 100_000;
14
16
  if (id === 'hives_cancel_deployment' || id === 'hives_delete_deployment')
15
17
  return 330_000;
16
18
  if (['hives_create_deployment', 'hives_upload_deployment', 'hives_invoke_deployment', 'hives_invoke_function', 'hives_invoke_function_async'].includes(id))
@@ -0,0 +1,9 @@
1
+ import { Command } from 'commander';
2
+ import type { Genesis } from '@lifesi/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
+ export declare function registerHivesOperations(hives: Command, deps: Deps): void;
9
+ export {};
@@ -0,0 +1,61 @@
1
+ import { registerHivesResources } from './hives-resources.js';
2
+ import { Option } from 'commander';
3
+ import { readFunctionJson } from './function-input.js';
4
+ function metricsOptions(command) {
5
+ return command.option('--start <time>', 'inclusive UTC timestamp').option('--end <time>', 'exclusive UTC timestamp')
6
+ .addOption(new Option('--resolution <value>', 'time bucket').choices(['minute', 'fiveminutes', 'fifteenminutes', 'hour', 'day']))
7
+ .addOption(new Option('--aggregation <value>', 'supported aggregation').choices(['sum', 'avg', 'min', 'max', 'count', 'p50', 'p90', 'p95', 'p99']));
8
+ }
9
+ const integer = (value, name, max) => {
10
+ if (value === undefined)
11
+ return undefined;
12
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1 || Number(value) > max) {
13
+ throw new Error(`${name} must be an integer from 1 to ${max}.`);
14
+ }
15
+ return Number(value);
16
+ };
17
+ const paging = (command) => command.option('--page <number>').option('--per-page <number>');
18
+ const pagination = (o) => ({ page: integer(o.page, '--page', 10000), per_page: integer(o.perPage, '--per-page', 100) });
19
+ export function registerHivesOperations(hives, deps) {
20
+ registerHivesResources(hives, deps);
21
+ const output = (data) => deps.emit(data, () => JSON.stringify(data, null, 2));
22
+ const read = deps.readRequest ?? readFunctionJson;
23
+ const settings = hives.command('settings').description('Stored project preferences and actual engine capabilities');
24
+ settings.command('get <project_id>').action(async (project) => output(await (await deps.client()).hives.settings.get(project)));
25
+ settings.command('update <project_id>').requiredOption('--request-file <path>', 'JSON settings patch')
26
+ .action(async (project, o) => output(await (await deps.client()).hives.settings.update(project, await read(o.requestFile, 64 * 1024))));
27
+ const crons = hives.command('crons').description('Waggle schedules and persisted run outcomes');
28
+ paging(crons.command('list <project_id>')).action(async (project, o) => output(await (await deps.client()).hives.crons.list(project, pagination(o))));
29
+ crons.command('get <project_id> <cron_id>').action(async (project, cron) => output(await (await deps.client()).hives.crons.get(project, cron)));
30
+ crons.command('create <project_id>').requiredOption('--request-file <path>', 'JSON name, schedule, handler, timezone and enabled preference')
31
+ .action(async (project, o) => {
32
+ const body = await read(o.requestFile, 64 * 1024);
33
+ if (typeof body.name !== 'string' || typeof body.schedule !== 'string' || typeof body.handler !== 'string')
34
+ throw new Error('Cron JSON requires name, schedule and handler strings.');
35
+ output(await (await deps.client()).hives.crons.create(project, { ...body, name: body.name, schedule: body.schedule, handler: body.handler }));
36
+ });
37
+ crons.command('update <project_id> <cron_id>').requiredOption('--request-file <path>', 'JSON schedule patch')
38
+ .action(async (project, cron, o) => output(await (await deps.client()).hives.crons.update(project, cron, await read(o.requestFile, 64 * 1024))));
39
+ for (const action of ['delete', 'pause', 'resume', 'trigger']) {
40
+ crons.command(`${action} <project_id> <cron_id>`)
41
+ .description(action === 'trigger' ? 'Admit one run; inspect runs for completion. Does not retry.' : `${action} an owned schedule`)
42
+ .action(async (project, cron) => output(await (await deps.client()).hives.crons[action](project, cron)));
43
+ }
44
+ paging(crons.command('runs <project_id> <cron_id>')).addOption(new Option('--status <state>', 'persisted run status').choices(['pending', 'running', 'success', 'failed', 'timeout', 'cancelled']))
45
+ .option('--since <time>').option('--until <time>')
46
+ .action(async (project, cron, o) => output(await (await deps.client()).hives.crons.runs(project, cron, { ...pagination(o), status: o.status, since: o.since, until: o.until })));
47
+ const metrics = hives.command('metrics').description('Retained completed API observations; unavailable measurements remain null');
48
+ for (const kind of ['project', 'requests', 'latency', 'errors', 'resources']) {
49
+ metricsOptions(metrics.command(`${kind} <project_id>`))
50
+ .action(async (project, q) => output(await (await deps.client()).hives.metrics[kind](project, q)));
51
+ }
52
+ metrics.command('dashboard <project_id>').addOption(new Option('--period <value>').choices(['1h', '24h', '7d', '30d']))
53
+ .action(async (project, q) => output(await (await deps.client()).hives.metrics.dashboard(project, q)));
54
+ for (const kind of ['function', 'invocations']) {
55
+ metricsOptions(metrics.command(`${kind} <project_id> <function_id>`))
56
+ .action(async (project, id, q) => output(await (await deps.client()).hives.metrics[kind](project, id, q)));
57
+ }
58
+ metricsOptions(metrics.command('deployment <project_id> <deployment_id>'))
59
+ .action(async (project, id, q) => output(await (await deps.client()).hives.metrics.deployment(project, id, q)));
60
+ metrics.command('realtime <project_id>').action(async (project) => output(await (await deps.client()).hives.metrics.realtime(project)));
61
+ }
@@ -0,0 +1,9 @@
1
+ import { Command } from 'commander';
2
+ import type { Genesis } from '@lifesi/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
+ export declare function registerHivesResources(hives: Command, deps: Deps): void;
9
+ export {};
@@ -0,0 +1,71 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import { stat, writeFile } from 'node:fs/promises';
3
+ import { readFunctionJson } from './function-input.js';
4
+ async function bytes(path) {
5
+ const metadata = await stat(path);
6
+ if (!metadata.isFile() || metadata.size > 10 * 1024 * 1024)
7
+ throw new Error('Choose a regular file of at most 10 MiB.');
8
+ const stream = createReadStream(path);
9
+ const chunks = [];
10
+ let total = 0;
11
+ try {
12
+ for await (const chunk of stream) {
13
+ const value = Buffer.from(chunk);
14
+ total += value.byteLength;
15
+ if (total > 10 * 1024 * 1024)
16
+ throw new Error('Object file exceeds 10 MiB.');
17
+ chunks.push(value);
18
+ }
19
+ }
20
+ finally {
21
+ stream.destroy();
22
+ }
23
+ return Uint8Array.from(Buffer.concat(chunks, total));
24
+ }
25
+ function number(value, min, max) { if (value === undefined)
26
+ return undefined; if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < min || Number(value) > max)
27
+ throw new Error('Invalid bounded numeric option.'); return Number(value); }
28
+ export function registerHivesResources(hives, deps) {
29
+ const output = (data) => deps.emit(data, () => JSON.stringify(data, null, 2));
30
+ const read = deps.readRequest ?? readFunctionJson;
31
+ const cells = hives.command('cells').description('Owned GCS buckets and objects; uncertain mutations require readback/reconciliation');
32
+ cells.command('list <project_id>').option('--page <number>').option('--per-page <number>').action(async (project, o) => output(await (await deps.client()).hives.cells.list(project, { page: number(o.page, 1, 10000), per_page: number(o.perPage, 1, 100) })));
33
+ cells.command('get <project_id> <bucket_id>').action(async (project, id) => output(await (await deps.client()).hives.cells.get(project, id)));
34
+ cells.command('create <project_id>').requiredOption('--request-file <path>', 'JSON name, region, visibility and CORS').action(async (project, o) => output(await (await deps.client()).hives.cells.create(project, await read(o.requestFile, 512 * 1024))));
35
+ for (const action of ['remove', 'reconcile'])
36
+ cells.command(`${action} <project_id> <bucket_id>`).action(async (project, id) => output(await (await deps.client()).hives.cells[action](project, id)));
37
+ const cors = cells.command('cors');
38
+ cors.command('get <project_id> <bucket_id>').action(async (project, id) => output(await (await deps.client()).hives.cells.cors.get(project, id)));
39
+ cors.command('set <project_id> <bucket_id>').requiredOption('--request-file <path>', 'JSON object with rules array').action(async (project, id, o) => { const body = await read(o.requestFile, 512 * 1024); if (!Array.isArray(body.rules))
40
+ throw new Error('CORS JSON requires rules.'); output(await (await deps.client()).hives.cells.cors.set(project, id, body.rules)); });
41
+ const objects = cells.command('objects');
42
+ objects.command('list <project_id> <bucket_id>').option('--prefix <value>').option('--delimiter <value>').option('--continuation-token <value>').option('--max-keys <number>').action(async (project, id, o) => output(await (await deps.client()).hives.cells.objects.list(project, id, { prefix: o.prefix, delimiter: o.delimiter, continuation_token: o.continuationToken, max_keys: number(o.maxKeys, 1, 1000) })));
43
+ for (const action of ['put', 'put-flat'])
44
+ objects.command(`${action} <project_id> <bucket_id> <key>`).requiredOption('--file <path>', 'Exact binary bytes up to 10 MiB').option('--content-type <value>').action(async (project, id, key, o) => { const client = await deps.client(); output(await client.hives.cells.objects[action === 'put' ? 'put' : 'putFlat'](project, id, key, await bytes(o.file), { content_type: o.contentType })); });
45
+ for (const action of ['remove', 'remove-flat'])
46
+ objects.command(`${action} <project_id> <bucket_id> <key>`).action(async (project, id, key) => { await (await deps.client()).hives.cells.objects[action === 'remove' ? 'remove' : 'removeFlat'](project, id, key); output({ acknowledged: true }); });
47
+ objects.command('download <project_id> <bucket_id> <key>').requiredOption('--output-file <path>', 'Create a new file; existing files are never overwritten').action(async (project, id, key, o) => { const response = await (await deps.client()).hives.cells.objects.download(project, id, key); const data = new Uint8Array(await response.arrayBuffer()); await writeFile(o.outputFile, data, { flag: 'wx', mode: 0o600 }); output({ saved: true, bytes: data.byteLength }); });
48
+ cells.command('presign <project_id> <bucket_id>').requiredOption('--request-file <path>', 'Explicit GET/PUT capability request; PUT requires exact content_length').action(async (project, id, o) => output(await (await deps.client()).hives.cells.presign(project, id, await read(o.requestFile, 64 * 1024))));
49
+ const queues = hives.command('queues').description('Buzz durable messages and possessed temporary delivery receipts');
50
+ for (const action of ['list'])
51
+ queues.command(`${action} <project_id>`).action(async (project) => output(await (await deps.client()).hives.queues[action](project)));
52
+ for (const action of ['get', 'stats', 'consumers', 'pause', 'resume', 'purge', 'remove', 'dead-letter-stats', 'purge-dead-letters']) {
53
+ const names = { 'dead-letter-stats': 'deadLetterStats', 'purge-dead-letters': 'purgeDeadLetters' };
54
+ queues.command(`${action} <project_id> <queue_id>`).action(async (project, id) => { const client = await deps.client(); const method = action in names ? names[action] : action; output(await client.hives.queues[method](project, id)); });
55
+ }
56
+ queues.command('create <project_id>').requiredOption('--request-file <path>').action(async (project, o) => output(await (await deps.client()).hives.queues.create(project, await read(o.requestFile, 512 * 1024))));
57
+ queues.command('configure <project_id> <queue_id>').requiredOption('--request-file <path>').action(async (project, id, o) => output(await (await deps.client()).hives.queues.configure(project, id, await read(o.requestFile, 512 * 1024))));
58
+ queues.command('send <project_id> <queue_id>').requiredOption('--request-file <path>', 'Exactly body or body_base64; decoded body 256 KiB').action(async (project, id, o) => output(await (await deps.client()).hives.queues.send(project, id, await read(o.requestFile, 512 * 1024))));
59
+ queues.command('receive <project_id> <queue_id>').option('--max-messages <number>').option('--wait-seconds <number>').action(async (project, id, o) => output(await (await deps.client()).hives.queues.receive(project, id, { maxMessages: number(o.maxMessages, 1, 10), waitSeconds: number(o.waitSeconds, 0, 10) })));
60
+ for (const action of ['ack', 'nack'])
61
+ queues.command(`${action} <project_id> <queue_id> <message_id>`).requiredOption('--receipt-file <path>', 'JSON receiptHandle; not a credential/header/query').action(async (project, id, message, o) => { const body = await read(o.receiptFile, 4096); if (typeof body.receiptHandle !== 'string')
62
+ throw new Error('Receipt JSON requires receiptHandle.'); output(await (await deps.client()).hives.queues[action](project, id, message, body.receiptHandle)); });
63
+ queues.command('dead-letters <project_id> <queue_id>').option('--limit <number>').option('--offset <number>').action(async (project, id, o) => output(await (await deps.client()).hives.queues.deadLetters(project, id, { limit: number(o.limit, 1, 10), offset: number(o.offset, 0, 8192) })));
64
+ for (const action of ['replay', 'delete-dead-letter'])
65
+ queues.command(`${action} <project_id> <queue_id> <message_id>`).action(async (project, id, message) => output(await (await deps.client()).hives.queues[action === 'replay' ? 'replay' : 'deleteDeadLetter'](project, id, message)));
66
+ const runtime = hives.command('runtimes').description('Compiled serving implementation metadata; not available capacity');
67
+ runtime.command('list').action(async () => output(await (await deps.client()).hives.runtimes.list()));
68
+ runtime.command('project <project_id>').action(async (project) => output(await (await deps.client()).hives.runtimes.project(project)));
69
+ runtime.command('get <name>').action(async (name) => output(await (await deps.client()).hives.runtimes.get(name)));
70
+ runtime.command('select').requiredOption('--wasi <value>').requiredOption('--runner <value>').requiredOption('--artifact <value>').action(async (input) => output(await (await deps.client()).hives.runtimes.select(input)));
71
+ }
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export interface Deps {
15
15
  readWasmFile?: (path: string) => Promise<Blob>;
16
16
  writeBytes?: (bytes: Uint8Array) => void;
17
17
  writeOutputFile?: (path: string, bytes: Uint8Array) => Promise<void>;
18
+ readHivesRequest?: (path: string, limit: number) => Promise<Record<string, unknown>>;
18
19
  }
19
20
  export declare function buildProgram(deps?: Deps): Command;
20
21
  export declare function main(argv: string[], deps?: Deps): Promise<number>;
package/dist/index.js CHANGED
@@ -8,8 +8,10 @@ import { c, listOf, stdio, table } from './output.js';
8
8
  import { spawn } from 'node:child_process';
9
9
  import { addHenryCommands } from './henry.js';
10
10
  import { registerFleets } from './fleets.js';
11
+ import { registerFlocks } from './flocks.js';
11
12
  import { registerRunner } from './runner.js';
12
13
  import { createHivesNodeTransport } from './hives-node-transport.js';
14
+ import { registerHivesOperations } from './hives-operations.js';
13
15
  import { readFunctionFile, readFunctionJson, writeFunctionOutput } from './function-input.js';
14
16
  import { basename } from 'node:path';
15
17
  import { readWasmFile } from './wasm-input.js';
@@ -30,7 +32,7 @@ export function buildProgram(deps = {}) {
30
32
  const auth = { ...authConfig(env, f), sleep: deps.sleep };
31
33
  const program = new Command('genesis')
32
34
  .description('One CLI for every Genesis service.')
33
- .version('0.1.1')
35
+ .version('0.1.2')
34
36
  .option('--json', 'machine-readable JSON output')
35
37
  .option('--api-key <key>', 'use this API key (default: GENESIS_API_KEY, then your `genesis login`)')
36
38
  .option('--base-url <url>', 'gateway URL (default: GENESIS_BASE_URL or https://console.life.one/api)')
@@ -46,7 +48,7 @@ export function buildProgram(deps = {}) {
46
48
  const opts = {
47
49
  apiKey: o.apiKey ?? env.GENESIS_API_KEY, token: env.GENESIS_TOKEN,
48
50
  baseUrl: o.baseUrl ?? env.GENESIS_BASE_URL, routing: o.direct || env.GENESIS_ROUTING === 'direct' ? 'direct' : 'gateway',
49
- fetch: f, userAgent: 'genesis-cli/0.1.1',
51
+ fetch: f, userAgent: 'genesis-cli/0.1.2',
50
52
  credentials: store ? savedLoginProvider(auth, store) : undefined,
51
53
  };
52
54
  return new Genesis(hivesTransport ? { ...opts, fetch: createHivesNodeTransport(opts, deps.signal) } : opts);
@@ -177,7 +179,8 @@ export function buildProgram(deps = {}) {
177
179
  });
178
180
  const hivesClient = () => client(true);
179
181
  // ---- hives -----------------------------------------------------------
180
- const hives = program.command('hives').description('Hives: projects, secrets, deployments, functions');
182
+ const hives = program.command('hives').description('Hives: projects, secrets, deployments, functions, schedules and activity');
183
+ registerHivesOperations(hives, { client: hivesClient, emit, readRequest: deps.readHivesRequest });
181
184
  const deployments = hives.command('deployments').description('Real WASM artifacts and deployment lifecycle');
182
185
  const emitDeployment = (data) => emit(data, () => table(data && typeof data === 'object' && !Array.isArray(data) && !('data' in data) ? [data] : listOf(data), ['id', 'version', 'status', 'module_hash', 'created_at']));
183
186
  deployments.command('list <project_id>')
@@ -284,6 +287,52 @@ export function buildProgram(deps = {}) {
284
287
  return (deps.readSecretInput ?? readSecretInput)(options, limit);
285
288
  };
286
289
  const emitSecret = (data) => emit(data, () => table(listOf(data), ['id', 'name', 'version', 'description', 'updated_at']));
290
+ const context = hives.command('context').description('Explicit Hives binding for the current verified interactive Genesis selection');
291
+ context.command('get').action(async () => {
292
+ const r = await (await hivesClient()).hives.context.get();
293
+ emit(r, () => table([r.project], ['id', 'name', 'slug', 'kind', 'status']));
294
+ });
295
+ context.command('create <name>').option('--kind <kind>', 'worker|function|site|api', 'worker').option('--description <text>', 'project description')
296
+ .action(async (name, o) => {
297
+ if (!['worker', 'function', 'site', 'api'].includes(o.kind))
298
+ throw new Error('Choose worker, function, site or api.');
299
+ if (o.description && o.description.length > 500)
300
+ throw new Error('Project descriptions must contain at most 500 characters.');
301
+ const r = await (await hivesClient()).hives.context.set({ create: { name: projectName(name), kind: o.kind, description: o.description } });
302
+ emit(r, () => table([r.project], ['id', 'name', 'slug', 'kind', 'status']));
303
+ });
304
+ context.command('link <project_id>').description('Link a Hives project owned by the verified org')
305
+ .action(async (id) => { const r = await (await hivesClient()).hives.context.set({ link: { project_id: id } }); emit(r, () => table([r.project], ['id', 'name', 'slug', 'kind', 'status'])); });
306
+ context.command('detach').description('Remove this selection binding; retain the Hives project and its resources')
307
+ .action(async () => { const r = await (await hivesClient()).hives.context.detach(); emit(r, () => 'Detached selection; retained Hives project ' + r.hivesProjectId + '.'); });
308
+ const environment = hives.command('environment').description('Encrypted project environment; names-only metadata and explicit KMS reveal');
309
+ environment.command('list <project_id>').option('--page <number>', 'page number (1–10000)').option('--per-page <number>', 'items per page (1–100)')
310
+ .action(async (id, o) => {
311
+ const r = await (await hivesClient()).hives.environment.list(id, { page: positiveInteger(o.page, '--page', 10000), per_page: positiveInteger(o.perPage, '--per-page', 100) });
312
+ emit(r, () => table(r.data, ['name']));
313
+ });
314
+ environment.command('upsert <project_id>').description('Encrypt a flat JSON map; empty string values are allowed')
315
+ .option('--file <path>', 'UTF-8 JSON input file').option('--stdin', 'read JSON from stdin')
316
+ .action(async (id, o) => {
317
+ const text = await readInput(o, 2 * 1024 * 1024);
318
+ let body;
319
+ try {
320
+ body = JSON.parse(text);
321
+ }
322
+ catch {
323
+ throw new Error('Environment input must be a flat JSON map of names to string values.');
324
+ }
325
+ const r = await (await hivesClient()).hives.environment.upsert(id, body);
326
+ emit(r, () => table(r.updated, ['name']));
327
+ });
328
+ environment.command('reveal <project_id> <key>').description('Explicit audit-logged environment reveal; requires --stdout')
329
+ .option('--stdout', 'write sensitive plaintext to stdout (avoid retained output)')
330
+ .action(async (id, key, o) => {
331
+ if (!o.stdout)
332
+ throw new Error('Environment reveal requires --stdout. Avoid logging or retaining the plaintext.');
333
+ const r = await (await hivesClient()).hives.environment.reveal(id, key);
334
+ emit(r, () => r.value);
335
+ });
287
336
  secrets.command('list <project_id>').description('List secret metadata without values')
288
337
  .option('--page <number>', 'page number (1–10000)').option('--per-page <number>', 'items per page (1–100)')
289
338
  .action(async (id, o) => {
@@ -396,6 +445,7 @@ export function buildProgram(deps = {}) {
396
445
  });
397
446
  addHenryCommands(program, client, value => out.log(JSON.stringify(value, null, 2)));
398
447
  registerFleets(program, client, (data) => emit(data, () => JSON.stringify(data, null, 2)));
448
+ registerFlocks(program, client, (data) => emit(data, () => JSON.stringify(data, null, 2)), env);
399
449
  registerBeacons(program, client, emit);
400
450
  // ---- status ----------------------------------------------------------
401
451
  program.command('status').description('Which Genesis services are wired up').action(async () => {
package/dist/runner.js CHANGED
@@ -2,6 +2,21 @@
2
2
  export function registerRunner(program, client, emit) {
3
3
  const runner = program.command('runner').description('Runner: agent task execution');
4
4
  const output = (value) => emit(value, () => JSON.stringify(value, null, 2));
5
+ runner.command('permission-grant').description('Request only your current native human Sandbox execution permission; preserve operation/scope/expiry on retry')
6
+ .requiredOption('--organization <id>', 'current approved Life organization assertion')
7
+ .requiredOption('--project <id>', 'current approved project assertion')
8
+ .requiredOption('--workspace <id>', 'current approved workspace assertion')
9
+ .requiredOption('--operation-id <uuid>', 'stable canonical UUIDv4 for this immutable authorization operation')
10
+ .requiredOption('--request-id <uuid>', 'fresh canonical UUIDv4 correlation for this attempt')
11
+ .requiredOption('--expires-at-ms <ms>', 'exact retained future integer expiry, at most 30 minutes and current session/access expiry')
12
+ .action(async (o) => {
13
+ if (!/^[1-9]\d*$/.test(o.expiresAtMs) || !Number.isSafeInteger(Number(o.expiresAtMs)))
14
+ throw new TypeError('Runner permission expiry must be a positive safe integer literal');
15
+ const input = { schemaVersion: 1, organizationId: o.organization, projectId: o.project,
16
+ workspaceId: o.workspace, platformId: 'pf_console', fleetId: null, resourceEnvironment: 'sandbox',
17
+ operationId: o.operationId, requestId: o.requestId, expiresAtMs: Number(o.expiresAtMs) };
18
+ output(await (await client()).runner.permission.grant(input));
19
+ });
5
20
  runner.command('catalog').description('Authorized workspaces and execution routes')
6
21
  .action(async () => output(await (await client()).runner.catalog()));
7
22
  const jobs = runner.command('jobs').description('Jobs belonging to your verified identity');
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@lifesi/genesis-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/l1felabs/genesis-api.git",
7
+ "directory": "packages/cli"
8
+ },
4
9
  "description": "genesis: one CLI for every Genesis service",
5
10
  "type": "module",
6
11
  "bin": {
@@ -13,7 +18,7 @@
13
18
  "dependencies": {
14
19
  "commander": "^14.0.0",
15
20
  "undici": "6.29.0",
16
- "@lifesi/genesis": "0.1.1"
21
+ "@lifesi/genesis": "0.1.2"
17
22
  },
18
23
  "optionalDependencies": {
19
24
  "@napi-rs/keyring": "^1.2.0"