@lifesi/genesis-cli 0.0.0-stage → 0.1.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.
package/bin/genesis.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../dist/index.js';
3
+ main(process.argv).then((code) => { process.exitCode = code; });
package/dist/auth.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { CredentialStore, SavedLogin } from './store.js';
2
+ /**
3
+ * Browser/device login via LifeID (WorkOS AuthKit CLI Auth, RFC 8628).
4
+ * Docs: https://workos.com/docs/authkit/cli-auth
5
+ * POST {authApi}/user_management/authorize/device {client_id}
6
+ * POST {authApi}/user_management/authenticate {grant_type: device_code | refresh_token, ...}
7
+ * client_id is a public identifier (not a secret). Set GENESIS_CLIENT_ID to the LifeID (staging) AuthKit client.
8
+ * GENESIS_AUTH_API lets this point at an auth.life.one proxy once one exists; default is WorkOS directly.
9
+ */
10
+ export interface AuthConfig {
11
+ clientId?: string;
12
+ authApi: string;
13
+ fetch: typeof fetch;
14
+ sleep?: (ms: number) => Promise<void>;
15
+ }
16
+ export declare function authConfig(env?: NodeJS.ProcessEnv, f?: typeof fetch): AuthConfig;
17
+ export interface DeviceCode {
18
+ device_code: string;
19
+ user_code: string;
20
+ verification_uri: string;
21
+ verification_uri_complete: string;
22
+ expires_in: number;
23
+ interval: number;
24
+ }
25
+ export declare function startDevice(c: AuthConfig): Promise<DeviceCode>;
26
+ export declare function jwtExp(token: string): number | undefined;
27
+ export declare function pollDevice(c: AuthConfig, d: DeviceCode): Promise<SavedLogin>;
28
+ export declare function refresh(c: AuthConfig, s: SavedLogin, organizationId?: string): Promise<SavedLogin>;
29
+ /** Credential provider for the SDK: saved login, refreshed if within 60s of expiry. */
30
+ export declare function savedLoginProvider(c: AuthConfig, store: CredentialStore, now?: () => number): () => Promise<string | undefined>;
package/dist/auth.js ADDED
@@ -0,0 +1,79 @@
1
+ export function authConfig(env = process.env, f = fetch) {
2
+ return { clientId: env.GENESIS_CLIENT_ID, authApi: (env.GENESIS_AUTH_API ?? 'https://api.workos.com').replace(/\/+$/, ''), fetch: f };
3
+ }
4
+ function needClient(c) {
5
+ if (!c.clientId)
6
+ throw new Error('Login is not configured: set GENESIS_CLIENT_ID to the LifeID AuthKit client id (staging), or use GENESIS_API_KEY.');
7
+ return c.clientId;
8
+ }
9
+ async function post(c, path, body) {
10
+ const res = await c.fetch(`${c.authApi}${path}`, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' }, body: new URLSearchParams(body).toString() });
11
+ const json = await res.json().catch(() => ({}));
12
+ return { ok: res.ok, status: res.status, json };
13
+ }
14
+ export async function startDevice(c) {
15
+ const r = await post(c, '/user_management/authorize/device', { client_id: needClient(c) });
16
+ if (!r.ok)
17
+ throw new Error(`Could not start login (${r.status}): ${r.json.error_description ?? r.json.message ?? r.json.error ?? 'unknown error'}`);
18
+ return r.json;
19
+ }
20
+ export function jwtExp(token) {
21
+ try {
22
+ return JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString()).exp;
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ }
28
+ function toSaved(c, j) {
29
+ return { access_token: j.access_token, refresh_token: j.refresh_token, user: j.user, organization_id: j.organization_id, expires_at: jwtExp(j.access_token), client_id: needClient(c) };
30
+ }
31
+ export async function pollDevice(c, d) {
32
+ const sleep = c.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
33
+ let interval = (d.interval || 5) * 1000;
34
+ const deadline = Date.now() + (d.expires_in || 300) * 1000;
35
+ while (Date.now() < deadline) {
36
+ await sleep(interval);
37
+ const r = await post(c, '/user_management/authenticate', { grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code: d.device_code, client_id: needClient(c) });
38
+ if (r.ok)
39
+ return toSaved(c, r.json);
40
+ const err = r.json.error;
41
+ if (err === 'authorization_pending')
42
+ continue;
43
+ if (err === 'slow_down') {
44
+ interval += 5000;
45
+ continue;
46
+ }
47
+ if (err === 'access_denied')
48
+ throw new Error('Login was denied in the browser.');
49
+ if (err === 'expired_token')
50
+ break;
51
+ throw new Error(`Login failed (${r.status}): ${r.json.error_description ?? err ?? 'unknown error'}`);
52
+ }
53
+ throw new Error('Login code expired. Run `genesis login` again.');
54
+ }
55
+ export async function refresh(c, s, organizationId) {
56
+ if (!s.refresh_token)
57
+ throw new Error('Session expired. Run `genesis login`.');
58
+ const body = { grant_type: 'refresh_token', refresh_token: s.refresh_token, client_id: s.client_id };
59
+ if (organizationId)
60
+ body.organization_id = organizationId;
61
+ const r = await post(c, '/user_management/authenticate', body);
62
+ if (!r.ok)
63
+ throw new Error(r.json.error === 'invalid_grant' ? 'Session expired. Run `genesis login`.' : `Could not refresh session (${r.status}): ${r.json.error_description ?? r.json.error}`);
64
+ return { ...toSaved({ ...c, clientId: s.client_id }, r.json), user: r.json.user ?? s.user };
65
+ }
66
+ /** Credential provider for the SDK: saved login, refreshed if within 60s of expiry. */
67
+ export function savedLoginProvider(c, store, now = () => Date.now() / 1000) {
68
+ return async () => {
69
+ let s = await store.get();
70
+ if (!s)
71
+ return undefined;
72
+ const exp = s.expires_at ?? jwtExp(s.access_token);
73
+ if (exp && exp - 60 < now()) {
74
+ s = await refresh(c, s, s.organization_id);
75
+ await store.set(s);
76
+ }
77
+ return s.access_token;
78
+ };
79
+ }
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ import type { Genesis } from '@lifesi/genesis';
3
+ export declare function registerBeacons(program: Command, client: () => Promise<Genesis>, emit: (data: unknown, pretty: () => string) => void): void;
@@ -0,0 +1,20 @@
1
+ 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');
3
+ devices.command('list', { isDefault: true }).action(async () => {
4
+ const result = await (await client()).beacons.devices.list();
5
+ emit(result, () => JSON.stringify(result, null, 2));
6
+ });
7
+ devices.command('update <id>').option('--name <name>', 'Device name').option('--nickname <nickname...>', 'Replace nicknames').option('--clear-nicknames', 'Remove all nicknames')
8
+ .action(async (id, opts) => {
9
+ if (opts.clearNicknames && opts.nickname)
10
+ throw new Error('Choose --nickname or --clear-nicknames.');
11
+ if (opts.name === undefined && opts.nickname === undefined && !opts.clearNicknames)
12
+ throw new Error('Supply --name, --nickname or --clear-nicknames.');
13
+ const result = await (await client()).beacons.devices.update(id, { ...(opts.name === undefined ? {} : { name: opts.name }), ...(opts.clearNicknames ? { nicknames: [] } : opts.nickname ? { nicknames: opts.nickname } : {}) });
14
+ emit(result, () => JSON.stringify(result, null, 2));
15
+ });
16
+ devices.command('revoke <id>').description('Request durable mesh teardown').action(async (id) => {
17
+ const result = await (await client()).beacons.devices.revoke(id);
18
+ emit(result ?? { status: 'revoked' }, () => JSON.stringify(result ?? { status: 'revoked' }, null, 2));
19
+ });
20
+ }
@@ -0,0 +1,7 @@
1
+ import { Command } from 'commander';
2
+ import type { Genesis } from '@lifesi/genesis';
3
+ export declare function registerDials(program: Command, dependencies: {
4
+ client: () => Promise<Genesis>;
5
+ orgId: (client: Genesis) => Promise<string>;
6
+ emit: (data: unknown, pretty: () => string) => void;
7
+ }): void;
package/dist/dials.js ADDED
@@ -0,0 +1,34 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ export function registerDials(program, dependencies) {
3
+ const dials = program.command('dials').description('Dials: phone numbers and calls for agents')
4
+ .requiredOption('--workspace <id>', 'selected workspace')
5
+ .requiredOption('--project <id>', 'selected project')
6
+ .requiredOption('--environment <id>', 'selected environment');
7
+ const context = async () => {
8
+ const client = await dependencies.client();
9
+ const options = dials.opts();
10
+ const scope = { organizationId: await dependencies.orgId(client), workspaceId: options.workspace, projectId: options.project, environmentId: options.environment };
11
+ return { client, scope };
12
+ };
13
+ const emit = (data) => dependencies.emit(data, () => JSON.stringify(data, null, 2));
14
+ const numbers = dials.command('numbers').description('Carrier number inventory');
15
+ numbers.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.dials.numbers.list(scope)); });
16
+ numbers.command('search').option('--area-code <code>', 'three digit area code').action(async (options) => { const { client, scope } = await context(); emit(await client.dials.numbers.search(scope, { areaCode: options.areaCode, limit: 10 })); });
17
+ const calls = dials.command('calls').description('Call activity and approved outbound calls');
18
+ calls.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.dials.calls.list(scope)); });
19
+ // @file keeps structured approval and idempotency fields out of shell history.
20
+ for (const [parent, name, operation] of [
21
+ [numbers, 'provision', 'dials_provisionNumber'], [numbers, 'assign', 'dials_assignNumber'], [numbers, 'release', 'dials_releaseNumber'], [numbers, 'e911', 'dials_registerEmergencyAddress'],
22
+ [calls, 'validate', 'dials_validateOutboundCall'], [calls, 'create', 'dials_createOutboundCall'],
23
+ ]) {
24
+ parent.command(name).requiredOption('--body <json-or-@file>', 'JSON input including actorId and required operation fields')
25
+ .action(async (options) => {
26
+ const { client, scope } = await context();
27
+ const text = options.body.startsWith('@') ? await readFile(options.body.slice(1), 'utf8') : options.body;
28
+ const body = JSON.parse(text);
29
+ if (!body || typeof body !== 'object' || Array.isArray(body))
30
+ throw new Error('Dials body must be a JSON object.');
31
+ emit(await client.call(operation, { body: { ...body, scope } }));
32
+ });
33
+ }
34
+ }
@@ -0,0 +1,4 @@
1
+ import type { Command } from 'commander';
2
+ import type { Genesis } from '@lifesi/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;
package/dist/fleets.js ADDED
@@ -0,0 +1,44 @@
1
+ export async function readFoundryKey(input = process.stdin) {
2
+ const chunks = [];
3
+ let size = 0;
4
+ for await (const chunk of input) {
5
+ const bytes = Buffer.from(chunk);
6
+ size += bytes.length;
7
+ if (size > 4098)
8
+ throw new Error('Foundry key must contain at most 4096 bytes.');
9
+ chunks.push(bytes);
10
+ }
11
+ let key;
12
+ try {
13
+ key = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(chunks)).replace(/\r?\n$/, '');
14
+ }
15
+ catch {
16
+ throw new Error('Invalid UTF-8 Foundry key on stdin.');
17
+ }
18
+ if (!key || Buffer.byteLength(key) > 4096 || /[\r\n\0]/.test(key))
19
+ throw new Error('Invalid Foundry key on stdin.');
20
+ return key;
21
+ }
22
+ export function registerFleets(program, client, emit, keyInput = readFoundryKey) {
23
+ const fleets = program.command('fleets').description('Fleets: managed agent tasks');
24
+ const connection = fleets.command('connection').description('Organization-owned Foundry connection');
25
+ connection.command('get').action(async () => emit(await (await client()).fleets.connection.get()));
26
+ connection.command('set').description('Read a Foundry key from stdin and encrypt it with Genesis KMS').action(async () => {
27
+ const apiKey = await keyInput();
28
+ emit(await (await client()).fleets.connection.set(apiKey));
29
+ });
30
+ connection.command('delete').action(async () => emit(await (await client()).fleets.connection.delete()));
31
+ const tasks = fleets.command('tasks');
32
+ tasks.command('list').option('--limit <number>').option('--before <task_id>').action(async (options) => {
33
+ emit(await (await client()).fleets.tasks.list({ ...(options.limit ? { limit: Number(options.limit) } : {}), ...(options.before ? { before: options.before } : {}) }));
34
+ });
35
+ tasks.command('submit <input>').option('--timeout <seconds>', 'task deadline in seconds', '300').requiredOption('--idempotency-key <key>', 'stable key to reuse on retry').action(async (input, options) => {
36
+ const timeout = Number(options.timeout);
37
+ if (!Number.isInteger(timeout) || timeout < 1 || timeout > 3600)
38
+ throw new Error('Timeout must be 1–3600 seconds.');
39
+ emit(await (await client()).fleets.tasks.submit({ input, timeout_seconds: timeout, idempotency_key: options.idempotencyKey }));
40
+ });
41
+ tasks.command('get <task_id>').action(async (id) => emit(await (await client()).fleets.tasks.get(id)));
42
+ tasks.command('cancel <task_id>').action(async (id) => emit(await (await client()).fleets.tasks.cancel(id)));
43
+ tasks.command('receipt <task_id>').action(async (id) => emit(await (await client()).fleets.tasks.receipt(id)));
44
+ }
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ import type { Genesis } from '@lifesi/genesis';
3
+ export declare function addHenryCommands(program: Command, client: () => Promise<Genesis>, output: (value: unknown) => void): void;
package/dist/henry.js ADDED
@@ -0,0 +1,31 @@
1
+ export function addHenryCommands(program, client, output) {
2
+ const henry = program.command('henry').description('Henry: agent runtime, live activity and approvals');
3
+ henry.command('config').action(async () => output(await (await client()).henry.config()));
4
+ henry.command('credential').description('Read a Foundry API key from stdin and encrypt it in Genesis KMS')
5
+ .action(async () => {
6
+ const chunks = [];
7
+ let size = 0;
8
+ for await (const chunk of process.stdin) {
9
+ size += Buffer.byteLength(chunk);
10
+ if (size > 4096)
11
+ throw new Error('Foundry API key exceeds limit');
12
+ chunks.push(Buffer.from(chunk));
13
+ }
14
+ output(await (await client()).henry.credential(Buffer.concat(chunks).toString('utf8').trim()));
15
+ });
16
+ henry.command('runs').option('--page-token <token>').action(async (options) => output(await (await client()).henry.runs.list(options.pageToken)));
17
+ henry.command('run').requiredOption('--model <model>').requiredOption('--task <text>').option('--run-id <uuid>', 'UUIDv4 for concurrent activity polling')
18
+ .action(async (options) => output(await (await client()).henry.runs.create({ model: options.model, task: options.task, ...(options.runId ? { run_id: options.runId } : {}) })));
19
+ henry.command('get <run_id>').action(async (runId) => output(await (await client()).henry.runs.get(runId)));
20
+ henry.command('command <run_id> <action>').option('--text <text>')
21
+ .action(async (runId, action, options) => {
22
+ if (!['pause', 'resume', 'cancel', 'input'].includes(action))
23
+ throw new Error('Action must be pause, resume, cancel or input');
24
+ output(await (await client()).henry.runs.command(runId, action, options.text));
25
+ });
26
+ henry.command('approval <run_id> <approval_id> <decision>').action(async (runId, approvalId, decision) => {
27
+ if (!['approve', 'reject'].includes(decision))
28
+ throw new Error('Decision must be approve or reject');
29
+ output(await (await client()).henry.runs.approval(runId, approvalId, decision));
30
+ });
31
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from 'commander';
2
+ import { type CredentialStore } from './store.js';
3
+ import { type Out } from './output.js';
4
+ export interface Deps {
5
+ out?: Out;
6
+ store?: () => Promise<CredentialStore>;
7
+ fetch?: typeof fetch;
8
+ env?: Record<string, string | undefined>;
9
+ openBrowser?: (url: string) => void;
10
+ sleep?: (ms: number) => Promise<void>;
11
+ }
12
+ export declare function buildProgram(deps?: Deps): Command;
13
+ export declare function main(argv: string[], deps?: Deps): Promise<number>;
package/dist/index.js ADDED
@@ -0,0 +1,222 @@
1
+ import { registerBeacons } from './beacons.js';
2
+ import { registerDials } from './dials.js';
3
+ import { Command, CommanderError } from 'commander';
4
+ import { Genesis, GenesisError, services } from '@lifesi/genesis';
5
+ import { authConfig, pollDevice, refresh, savedLoginProvider, startDevice } from './auth.js';
6
+ import { keychainStore } from './store.js';
7
+ import { c, listOf, stdio, table } from './output.js';
8
+ import { spawn } from 'node:child_process';
9
+ import { addHenryCommands } from './henry.js';
10
+ import { registerFleets } from './fleets.js';
11
+ import { registerRunner } from './runner.js';
12
+ function defaultOpen(url) {
13
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
14
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
15
+ try {
16
+ spawn(cmd, args, { stdio: 'ignore', detached: true }).on('error', () => { }).unref();
17
+ }
18
+ catch { /* print-only fallback */ }
19
+ }
20
+ export function buildProgram(deps = {}) {
21
+ const out = deps.out ?? stdio;
22
+ const env = deps.env ?? process.env;
23
+ const f = deps.fetch ?? fetch;
24
+ const getStore = deps.store ?? keychainStore;
25
+ const auth = { ...authConfig(env, f), sleep: deps.sleep };
26
+ const program = new Command('genesis')
27
+ .description('One CLI for every Genesis service.')
28
+ .version('0.1.0')
29
+ .option('--json', 'machine-readable JSON output')
30
+ .option('--api-key <key>', 'use this API key (default: GENESIS_API_KEY, then your `genesis login`)')
31
+ .option('--base-url <url>', 'gateway URL (default: GENESIS_BASE_URL or https://console.life.one/api)')
32
+ .option('--direct', 'skip the gateway and call each service host directly (for local dev)')
33
+ .option('--org <org_id>', 'organization to act on')
34
+ .showHelpAfterError()
35
+ .exitOverride()
36
+ .configureOutput({ writeOut: (s) => out.log(s.trimEnd()), writeErr: (s) => out.err(s.trimEnd()) });
37
+ const g = () => program.opts();
38
+ const client = async () => {
39
+ const o = g();
40
+ const store = await getStore().catch(() => undefined);
41
+ const opts = {
42
+ apiKey: o.apiKey ?? env.GENESIS_API_KEY, token: env.GENESIS_TOKEN,
43
+ baseUrl: o.baseUrl ?? env.GENESIS_BASE_URL, routing: o.direct || env.GENESIS_ROUTING === 'direct' ? 'direct' : 'gateway',
44
+ fetch: f, userAgent: 'genesis-cli/0.1.0',
45
+ credentials: store ? savedLoginProvider(auth, store) : undefined,
46
+ };
47
+ return new Genesis(opts);
48
+ };
49
+ const emit = (data, pretty) => out.log(g().json ? JSON.stringify(data, null, 2) : pretty());
50
+ registerRunner(program, client, emit);
51
+ const orgId = async (gen) => {
52
+ if (g().org)
53
+ return g().org;
54
+ const s = await (await getStore().catch(() => undefined))?.get();
55
+ if (s?.organization_id && !g().apiKey && !env.GENESIS_API_KEY)
56
+ return s.organization_id;
57
+ return (await gen.whoami()).org;
58
+ };
59
+ registerDials(program, { client, orgId, emit });
60
+ // ---- auth ------------------------------------------------------------
61
+ program.command('login').description('Sign in with LifeID in your browser').option('--no-browser', 'just print the link')
62
+ .action(async (o) => {
63
+ const store = await getStore();
64
+ const d = await startDevice(auth);
65
+ out.err(`\n Your code: ${c.bold(d.user_code)}\n Opening ${d.verification_uri_complete}\n ${c.dim('Waiting for you to approve in the browser…')}\n`);
66
+ if (o.browser)
67
+ (deps.openBrowser ?? defaultOpen)(d.verification_uri_complete);
68
+ const s = await pollDevice(auth, d);
69
+ await store.set(s);
70
+ emit({ user: s.user, organization_id: s.organization_id, store: store.kind }, () => `${c.green('✓')} Signed in as ${c.bold(s.user?.email ?? s.user?.id ?? 'you')}${s.organization_id ? ` (${s.organization_id})` : ''}. Token saved to your ${store.kind === 'os-keychain' ? 'OS keychain' : store.kind}.`);
71
+ });
72
+ program.command('logout').description('Forget your saved login').action(async () => {
73
+ const had = await (await getStore()).clear();
74
+ emit({ logged_out: had }, () => had ? `${c.green('✓')} Signed out.` : 'You were not signed in.');
75
+ });
76
+ program.command('whoami').description('Show who you are signed in as').action(async () => {
77
+ const gen = await client();
78
+ const saved = await (await getStore().catch(() => undefined))?.get();
79
+ try {
80
+ const p = await gen.whoami();
81
+ emit({ ...p, user: saved?.user }, () => [
82
+ `${c.bold('Org')} ${p.org}`, `${c.bold('Principal')} ${p.principal ?? '-'}`,
83
+ `${c.bold('Via')} ${p.kind}`, `${c.bold('Scopes')} ${p.scopes.join(', ') || '-'}`,
84
+ ...(saved?.user?.email ? [`${c.bold('User')} ${saved.user.email}`] : [])
85
+ ].join('\n'));
86
+ }
87
+ catch (e) {
88
+ if (!saved || g().apiKey || env.GENESIS_API_KEY)
89
+ throw e;
90
+ // Saved login exists but LifeID verify isn't reachable yet (auth-v4-backend PR #30 not deployed).
91
+ emit({ user: saved.user, organization_id: saved.organization_id, verified: false, reason: e.message }, () => `${c.bold('User')} ${saved.user?.email ?? saved.user?.id}\n${c.bold('Org')} ${saved.organization_id ?? '-'}\n${c.yellow('!')} ${c.dim('From your saved login; LifeID verify unavailable: ' + e.message)}`);
92
+ }
93
+ });
94
+ const orgs = program.command('orgs').description('Your organizations');
95
+ orgs.command('list', { isDefault: true }).description('Show your current organization').action(async () => {
96
+ const s = await (await getStore()).get();
97
+ if (!s)
98
+ throw new Error('Not signed in. Run `genesis login`.');
99
+ const rows = s.organization_id ? [{ id: s.organization_id, current: '*' }] : [];
100
+ emit({ current: s.organization_id ?? null, organizations: rows }, () => `${table(rows, ['id', 'current'])}\n${c.dim('Listing every org you belong to needs a LifeID memberships endpoint (not built yet). Switch with `genesis orgs switch <org_id>`.')}`);
101
+ });
102
+ orgs.command('switch <org_id>').description('Switch your login to another organization').action(async (id) => {
103
+ const store = await getStore();
104
+ const s = await store.get();
105
+ if (!s)
106
+ throw new Error('Not signed in. Run `genesis login`.');
107
+ const n = await refresh(auth, s, id);
108
+ await store.set(n);
109
+ emit({ organization_id: n.organization_id }, () => `${c.green('✓')} Now using ${c.bold(n.organization_id ?? id)}.`);
110
+ });
111
+ // ---- keys ------------------------------------------------------------
112
+ const keys = program.command('keys').description('Org API keys (LifeID / WorkOS)');
113
+ keys.command('list').action(async () => {
114
+ const gen = await client();
115
+ const org = await orgId(gen);
116
+ const r = await gen.keys.list(org);
117
+ emit(r, () => table(r.data, ['id', 'name', 'obfuscated_value', 'scopes', 'last_used_at', 'expires_at']));
118
+ });
119
+ keys.command('create <name>').description('Create a key (shown once), or with --blinds hand it straight to Blinds so nobody ever sees it')
120
+ .option('--scopes <list>', 'comma-separated scopes, e.g. hives:read,relays:write')
121
+ .option('--blinds', 'store the key in Blinds; the raw key is never shown to anyone')
122
+ .option('--expires <iso>', 'expiry, e.g. 2027-01-01T00:00:00Z')
123
+ .action(async (name, o) => {
124
+ const gen = await client();
125
+ const org = await orgId(gen);
126
+ 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 } : {}) });
127
+ emit(k, () => o.blinds
128
+ ? `${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.')}`
129
+ : `${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`);
130
+ });
131
+ keys.command('revoke <key_id>').action(async (id) => {
132
+ const gen = await client();
133
+ await gen.keys.revoke(id);
134
+ emit({ revoked: id }, () => `${c.green('✓')} Revoked ${id} ${c.dim('(takes effect within 60s)')}`);
135
+ });
136
+ // ---- foundry ---------------------------------------------------------
137
+ const foundry = program.command('foundry').description('Foundry: models and inference');
138
+ foundry.command('models').action(async () => {
139
+ const r = await (await client()).foundry.models.list();
140
+ const rows = listOf(r).map((m) => ({ id: m.id, owned_by: m.owned_by ?? m.provider ?? '' }));
141
+ emit(r, () => table(rows, ['id', 'owned_by']));
142
+ });
143
+ foundry.command('chat <prompt...>').description('Chat with a model').option('-m, --model <model>', 'model id', env.GENESIS_MODEL)
144
+ .option('--no-stream', 'wait for the full answer').option('-s, --system <text>', 'system prompt')
145
+ .action(async (prompt, o) => {
146
+ if (!o.model)
147
+ throw new Error('Pick a model with --model (see `genesis foundry models`) or set GENESIS_MODEL.');
148
+ const gen = await client();
149
+ const messages = [...(o.system ? [{ role: 'system', content: o.system }] : []), { role: 'user', content: prompt.join(' ') }];
150
+ if (o.stream && !g().json) {
151
+ for await (const d of gen.foundry.chatStream({ model: o.model, messages }))
152
+ process.stdout.write(d);
153
+ process.stdout.write('\n');
154
+ return;
155
+ }
156
+ const r = await gen.foundry.chat({ model: o.model, messages });
157
+ emit(r, () => r.choices?.[0]?.message?.content ?? JSON.stringify(r));
158
+ });
159
+ // ---- hives -----------------------------------------------------------
160
+ const hives = program.command('hives').description('Hives: projects, deployments, functions');
161
+ const projects = hives.command('projects').description('Hives projects');
162
+ projects.command('list', { isDefault: true }).action(async () => {
163
+ const r = await (await client()).hives.projects.list();
164
+ emit(r, () => table(listOf(r), ['id', 'name', 'slug', 'region', 'total_deployments', 'active_instances']));
165
+ });
166
+ projects.command('get <project_id>').action(async (id) => {
167
+ const r = await (await client()).hives.projects.get(id);
168
+ emit(r, () => JSON.stringify(r, null, 2));
169
+ });
170
+ // ---- relays ----------------------------------------------------------
171
+ const relays = program.command('relays').description('Relays: DNS zones and records');
172
+ relays.command('zones').description('List DNS zones').option('--search <q>').action(async (o) => {
173
+ const r = await (await client()).relays.zones.list(o.search ? { search: o.search } : {});
174
+ emit(r, () => table((r.zones ?? []), ['name', 'type', 'record_count', 'dnssec_enabled', 'status']));
175
+ });
176
+ const records = relays.command('records').description('DNS records in a zone');
177
+ records.command('list <zone>', { isDefault: true }).option('--type <type>').action(async (zone, o) => {
178
+ const r = await (await client()).relays.records.list(zone, o.type ? { type: o.type } : {});
179
+ emit(r, () => table((r.records ?? []), ['id', 'name', 'type', 'content', 'ttl']));
180
+ });
181
+ records.command('add <zone> <name> <type> <content>').option('--ttl <seconds>', 'TTL', '3600').action(async (zone, name, type, content, o) => {
182
+ const r = await (await client()).relays.records.create(zone, { name, type: type.toUpperCase(), content, ttl: Number(o.ttl) });
183
+ emit(r, () => `${c.green('✓')} Added ${type.toUpperCase()} ${name} → ${content} in ${zone}${r.id ? ` (id ${r.id})` : ''}`);
184
+ });
185
+ records.command('rm <zone> <record_id>').action(async (zone, id) => {
186
+ await (await client()).relays.records.delete(zone, Number(id));
187
+ emit({ deleted: Number(id) }, () => `${c.green('✓')} Deleted record ${id} from ${zone}`);
188
+ });
189
+ addHenryCommands(program, client, value => out.log(JSON.stringify(value, null, 2)));
190
+ registerFleets(program, client, (data) => emit(data, () => JSON.stringify(data, null, 2)));
191
+ registerBeacons(program, client, emit);
192
+ // ---- status ----------------------------------------------------------
193
+ program.command('status').description('Which Genesis services are wired up').action(async () => {
194
+ emit(services, () => table(services.map((s) => ({ service: s.name, status: s.status, ops: s.operations, upstream: s.upstream, note: s.statusNote })), ['service', 'status', 'ops', 'upstream', 'note']));
195
+ });
196
+ return program;
197
+ }
198
+ export async function main(argv, deps = {}) {
199
+ const out = deps.out ?? stdio;
200
+ const program = buildProgram(deps);
201
+ try {
202
+ await program.parseAsync(argv);
203
+ return 0;
204
+ }
205
+ catch (e) {
206
+ if (e instanceof CommanderError)
207
+ return e.exitCode;
208
+ const json = program.opts().json;
209
+ if (e instanceof GenesisError) {
210
+ if (json)
211
+ out.err(JSON.stringify({ error: { type: e.type, status: e.status, message: e.message, request_id: e.requestId } }));
212
+ else
213
+ out.err(`${c.red('✗')} ${e.message}${e.status ? c.dim(` (${e.status} ${e.type}${e.requestId ? `, request ${e.requestId}` : ''})`) : ''}`);
214
+ return 1;
215
+ }
216
+ const msg = e.message ?? String(e);
217
+ const net = /fetch failed|ENOTFOUND|ECONNREFUSED/.test(msg + (e.cause?.code ?? ''));
218
+ out.err(json ? JSON.stringify({ error: { type: net ? 'network' : 'error', message: msg } })
219
+ : `${c.red('✗')} ${net ? `Could not reach the Genesis API gateway (${msg}). Try --direct to call service hosts directly.` : msg}`);
220
+ return 1;
221
+ }
222
+ }
@@ -0,0 +1,16 @@
1
+ export interface Out {
2
+ log(s: string): void;
3
+ err(s: string): void;
4
+ }
5
+ export declare const stdio: Out;
6
+ export declare const c: {
7
+ bold: (s: string) => string;
8
+ dim: (s: string) => string;
9
+ green: (s: string) => string;
10
+ yellow: (s: string) => string;
11
+ red: (s: string) => string;
12
+ };
13
+ /** Render rows as an aligned table. */
14
+ export declare function table(rows: Record<string, unknown>[], cols: string[]): string;
15
+ /** Pull the array out of the various list envelopes ({data}, {zones}, {records}, bare array). */
16
+ export declare function listOf(x: any, ...keys: string[]): any[];
package/dist/output.js ADDED
@@ -0,0 +1,27 @@
1
+ export const stdio = { log: (s) => process.stdout.write(s + '\n'), err: (s) => process.stderr.write(s + '\n') };
2
+ const tty = () => !!process.stdout.isTTY && !process.env.NO_COLOR;
3
+ export const c = {
4
+ bold: (s) => (tty() ? `\x1b[1m${s}\x1b[0m` : s),
5
+ dim: (s) => (tty() ? `\x1b[2m${s}\x1b[0m` : s),
6
+ green: (s) => (tty() ? `\x1b[32m${s}\x1b[0m` : s),
7
+ yellow: (s) => (tty() ? `\x1b[33m${s}\x1b[0m` : s),
8
+ red: (s) => (tty() ? `\x1b[31m${s}\x1b[0m` : s),
9
+ };
10
+ /** Render rows as an aligned table. */
11
+ export function table(rows, cols) {
12
+ if (!rows.length)
13
+ return c.dim('(none)');
14
+ const cell = (v) => (v === undefined || v === null ? '' : Array.isArray(v) ? v.join(',') : String(v));
15
+ const w = cols.map((k) => Math.max(k.length, ...rows.map((r) => cell(r[k]).length)));
16
+ const line = (vals) => vals.map((v, i) => v.padEnd(w[i])).join(' ').trimEnd();
17
+ return [c.bold(line(cols.map((k) => k.toUpperCase()))), ...rows.map((r) => line(cols.map((k) => cell(r[k]))))].join('\n');
18
+ }
19
+ /** Pull the array out of the various list envelopes ({data}, {zones}, {records}, bare array). */
20
+ export function listOf(x, ...keys) {
21
+ if (Array.isArray(x))
22
+ return x;
23
+ for (const k of [...keys, 'data', 'items', 'results'])
24
+ if (Array.isArray(x?.[k]))
25
+ return x[k];
26
+ return [];
27
+ }
@@ -0,0 +1,4 @@
1
+ import type { Command } from 'commander';
2
+ import type { Genesis } from '@lifesi/genesis';
3
+ /** Register only Runner commands. The authenticated credential determines org and subject. */
4
+ export declare function registerRunner(program: Command, client: () => Promise<Genesis>, emit: (data: unknown, pretty: () => string) => void): void;
package/dist/runner.js ADDED
@@ -0,0 +1,40 @@
1
+ /** Register only Runner commands. The authenticated credential determines org and subject. */
2
+ export function registerRunner(program, client, emit) {
3
+ const runner = program.command('runner').description('Runner: agent task execution');
4
+ const output = (value) => emit(value, () => JSON.stringify(value, null, 2));
5
+ runner.command('catalog').description('Authorized workspaces and execution routes')
6
+ .action(async () => output(await (await client()).runner.catalog()));
7
+ const jobs = runner.command('jobs').description('Jobs belonging to your verified identity');
8
+ jobs.command('list', { isDefault: true }).description('Newest 100 jobs')
9
+ .action(async () => output(await (await client()).runner.jobs.list()));
10
+ jobs.command('start').description('Start a bounded task; refs must come from catalog')
11
+ .requiredOption('--task-id <id>', 'opaque task ID')
12
+ .requiredOption('--text <text>', 'task instructions (at most 32768 characters)')
13
+ .requiredOption('--workspace <ref>', 'authorized workspace key')
14
+ .requiredOption('--kind <kind>', 'external-harness or internal-model')
15
+ .requiredOption('--route <id>', 'authorized execution route')
16
+ .requiredOption('--idempotency-key <key>', 'stable key for retries of the same task')
17
+ .option('--duration-ms <ms>', 'maximum execution duration', '600000')
18
+ .option('--output-bytes <bytes>', 'maximum output size', '1048576')
19
+ .option('--max-events <count>', 'retained event limit', '200')
20
+ .action(async (o) => {
21
+ if (!['external-harness', 'internal-model'].includes(o.kind))
22
+ throw new TypeError('Runner kind must be external-harness or internal-model');
23
+ const body = {
24
+ task: { id: o.taskId, text: o.text }, workspaceRef: o.workspace,
25
+ executionRef: { kind: o.kind, routeId: o.route },
26
+ limits: { maxDurationMs: Number(o.durationMs), maxOutputBytes: Number(o.outputBytes), maxEvents: Number(o.maxEvents) },
27
+ idempotencyKey: o.idempotencyKey,
28
+ };
29
+ output(await (await client()).runner.jobs.start(body));
30
+ });
31
+ jobs.command('get <job_id>').description('Read the authoritative snapshot')
32
+ .action(async (id) => output(await (await client()).runner.jobs.get(id)));
33
+ jobs.command('events <job_id>').description('Read one event page; continue with nextAfterSeq while hasMore')
34
+ .option('--after-seq <seq>', 'exclusive event cursor', '0')
35
+ .action(async (id, o) => output(await (await client()).runner.jobs.events(id, Number(o.afterSeq))));
36
+ jobs.command('result <job_id>').description('Read the verified completed provider deliverable')
37
+ .action(async (id) => output(await (await client()).runner.jobs.result(id)));
38
+ jobs.command('cancel <job_id>').description('Request cancellation; then watch status and cleanupPending')
39
+ .action(async (id) => output(await (await client()).runner.jobs.cancel(id)));
40
+ }
@@ -0,0 +1,23 @@
1
+ /** Saved `genesis login`: kept in the OS keychain (macOS Keychain, Windows Credential Manager, libsecret). */
2
+ export interface SavedLogin {
3
+ access_token: string;
4
+ refresh_token?: string;
5
+ user?: {
6
+ id: string;
7
+ email?: string;
8
+ first_name?: string | null;
9
+ last_name?: string | null;
10
+ };
11
+ organization_id?: string;
12
+ /** epoch seconds, from the access token's exp claim */
13
+ expires_at?: number;
14
+ client_id: string;
15
+ }
16
+ export interface CredentialStore {
17
+ get(): Promise<SavedLogin | undefined>;
18
+ set(v: SavedLogin): Promise<void>;
19
+ clear(): Promise<boolean>;
20
+ readonly kind: string;
21
+ }
22
+ export declare function keychainStore(): Promise<CredentialStore>;
23
+ export declare function memoryStore(initial?: SavedLogin): CredentialStore;
package/dist/store.js ADDED
@@ -0,0 +1,33 @@
1
+ const SERVICE = 'one.life.genesis';
2
+ const ACCOUNT = 'default';
3
+ export async function keychainStore() {
4
+ let Entry;
5
+ try {
6
+ ({ Entry } = await import('@napi-rs/keyring'));
7
+ }
8
+ catch {
9
+ throw new Error('OS keychain is unavailable (@napi-rs/keyring not installed). Use GENESIS_API_KEY instead.');
10
+ }
11
+ const entry = new Entry(SERVICE, ACCOUNT);
12
+ return {
13
+ kind: 'os-keychain',
14
+ async get() { try {
15
+ const s = entry.getPassword();
16
+ return s ? JSON.parse(s) : undefined;
17
+ }
18
+ catch {
19
+ return undefined;
20
+ } },
21
+ async set(v) { entry.setPassword(JSON.stringify(v)); },
22
+ async clear() { try {
23
+ return entry.deletePassword();
24
+ }
25
+ catch {
26
+ return false;
27
+ } },
28
+ };
29
+ }
30
+ export function memoryStore(initial) {
31
+ let v = initial;
32
+ return { kind: 'memory', async get() { return v; }, async set(x) { v = x; }, async clear() { const had = !!v; v = undefined; return had; } };
33
+ }
package/package.json CHANGED
@@ -1,6 +1,23 @@
1
1
  {
2
2
  "name": "@lifesi/genesis-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "genesis: one CLI for every Genesis service",
5
+ "type": "module",
6
+ "bin": {
7
+ "genesis": "bin/genesis.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "dist"
12
+ ],
13
+ "dependencies": {
14
+ "commander": "^14.0.0",
15
+ "@lifesi/genesis": "0.1.0"
16
+ },
17
+ "optionalDependencies": {
18
+ "@napi-rs/keyring": "^1.2.0"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json"
22
+ }
6
23
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.