@lifesi/genesis-cli 0.0.0-stage → 0.1.1
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 +3 -0
- package/dist/auth.d.ts +30 -0
- package/dist/auth.js +79 -0
- package/dist/beacons.d.ts +3 -0
- package/dist/beacons.js +20 -0
- package/dist/dials.d.ts +7 -0
- package/dist/dials.js +49 -0
- package/dist/fleets.d.ts +4 -0
- package/dist/fleets.js +44 -0
- package/dist/function-input.d.ts +3 -0
- package/dist/function-input.js +49 -0
- package/dist/henry.d.ts +3 -0
- package/dist/henry.js +31 -0
- package/dist/hives-node-transport.d.ts +6 -0
- package/dist/hives-node-transport.js +145 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +430 -0
- package/dist/output.d.ts +16 -0
- package/dist/output.js +27 -0
- package/dist/runner.d.ts +4 -0
- package/dist/runner.js +40 -0
- package/dist/secret-input.d.ts +14 -0
- package/dist/secret-input.js +85 -0
- package/dist/store.d.ts +23 -0
- package/dist/store.js +33 -0
- package/dist/wasm-input.d.ts +2 -0
- package/dist/wasm-input.js +31 -0
- package/package.json +21 -3
- package/README.md +0 -3
package/bin/genesis.js
ADDED
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
|
+
}
|
package/dist/beacons.js
ADDED
|
@@ -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
|
+
}
|
package/dist/dials.d.ts
ADDED
|
@@ -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,49 @@
|
|
|
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').option('--filters <json-or-@file>', 'carrier search filters, including capabilities and rate center').action(async (options) => {
|
|
17
|
+
const { client, scope } = await context();
|
|
18
|
+
const text = options.filters?.startsWith('@') ? await readFile(options.filters.slice(1), 'utf8') : options.filters;
|
|
19
|
+
const filters = text ? JSON.parse(text) : {};
|
|
20
|
+
if (!filters || typeof filters !== 'object' || Array.isArray(filters))
|
|
21
|
+
throw new Error('Search filters must be a JSON object.');
|
|
22
|
+
const allowed = new Set(['areaCode', 'exchangeCode', 'localCallingArea', 'contains', 'startsWith', 'endsWith', 'repeatingDigit', 'repeatingDigitCount', 'region', 'locality', 'postalCode', 'rateCenter', 'lata', 'nearNumber', 'nearLatLong', 'distance', 'excludeAddressRequired', 'excludeLocalAddressRequired', 'excludeForeignAddressRequired', 'beta', 'voice', 'sms', 'mms', 'fax', 'numberType', 'limit']);
|
|
23
|
+
if (Object.keys(filters).some(key => !allowed.has(key)))
|
|
24
|
+
throw new Error('Unsupported Dials search filter. Scope is selected by the command context.');
|
|
25
|
+
emit(await client.dials.numbers.search(scope, { ...filters, ...(options.areaCode ? { areaCode: options.areaCode } : {}) }));
|
|
26
|
+
});
|
|
27
|
+
numbers.command('reconciliations').action(async () => { const { client, scope } = await context(); emit(await client.dials.numbers.reconciliations(scope)); });
|
|
28
|
+
numbers.command('drift').action(async () => { const { client, scope } = await context(); emit(await client.dials.numbers.drift(scope)); });
|
|
29
|
+
dials.command('overview').action(async () => { const { client, scope } = await context(); emit(await client.dials.overview.summary(scope)); });
|
|
30
|
+
dials.command('operating-surface').action(async () => { const { client, scope } = await context(); emit(await client.dials.overview.operatingSurface(scope)); });
|
|
31
|
+
const calls = dials.command('calls').description('Call activity and approved outbound calls');
|
|
32
|
+
calls.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.dials.calls.list(scope)); });
|
|
33
|
+
// @file keeps structured approval and idempotency fields out of shell history.
|
|
34
|
+
for (const [parent, name, operation] of [
|
|
35
|
+
[numbers, 'caller-name', 'dials_updateNumberCallerName'], [numbers, 'reconcile', 'dials_reconcileNumberInventory'],
|
|
36
|
+
[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'],
|
|
38
|
+
]) {
|
|
39
|
+
parent.command(name).requiredOption('--body <json-or-@file>', 'JSON input including actorId and required operation fields')
|
|
40
|
+
.action(async (options) => {
|
|
41
|
+
const { client, scope } = await context();
|
|
42
|
+
const text = options.body.startsWith('@') ? await readFile(options.body.slice(1), 'utf8') : options.body;
|
|
43
|
+
const body = JSON.parse(text);
|
|
44
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
45
|
+
throw new Error('Dials body must be a JSON object.');
|
|
46
|
+
emit(await client.call(operation, { body: { ...body, scope } }));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/fleets.d.ts
ADDED
|
@@ -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
|
+
export declare function readFunctionFile(path: string, limit: number): Promise<Uint8Array>;
|
|
2
|
+
export declare function readFunctionJson(path: string, limit: number): Promise<Record<string, unknown>>;
|
|
3
|
+
export declare function writeFunctionOutput(path: string, bytes: Uint8Array): Promise<void>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { open, writeFile } from 'node:fs/promises';
|
|
2
|
+
export async function readFunctionFile(path, limit) {
|
|
3
|
+
let file;
|
|
4
|
+
try {
|
|
5
|
+
file = await open(path, 'r');
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
throw new Error('Could not open the function input file. Check its path and permissions.');
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const stat = await file.stat();
|
|
12
|
+
if (!stat.isFile() || stat.size > limit)
|
|
13
|
+
throw new Error('Choose a regular function input file within the documented size limit.');
|
|
14
|
+
const bytes = new Uint8Array(limit + 1);
|
|
15
|
+
let size = 0;
|
|
16
|
+
for (;;) {
|
|
17
|
+
const { bytesRead } = await file.read(bytes, size, bytes.length - size, null);
|
|
18
|
+
if (!bytesRead)
|
|
19
|
+
break;
|
|
20
|
+
size += bytesRead;
|
|
21
|
+
if (size > limit)
|
|
22
|
+
throw new Error('Function input file exceeds the documented size limit.');
|
|
23
|
+
}
|
|
24
|
+
return bytes.slice(0, size);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
await file.close();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function readFunctionJson(path, limit) {
|
|
31
|
+
const bytes = await readFunctionFile(path, limit);
|
|
32
|
+
try {
|
|
33
|
+
const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
34
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
35
|
+
throw new Error();
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new Error('Function request file must contain a JSON object. No input value is echoed.');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function writeFunctionOutput(path, bytes) {
|
|
43
|
+
try {
|
|
44
|
+
await writeFile(path, bytes, { flag: 'wx', mode: 0o600 });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new Error('Could not create the output file. Choose a new path with write permission. Existing files are preserved.');
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/henry.d.ts
ADDED
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,6 @@
|
|
|
1
|
+
import { type GenesisOptions } from '@lifesi/genesis';
|
|
2
|
+
export declare function closeHivesNodeTransport(): Promise<void>;
|
|
3
|
+
export declare function hivesOperationBudget(id: string): number;
|
|
4
|
+
/** Hives leaf transport. Caller fetch overrides stay authoritative; only native
|
|
5
|
+
* global fetch gets our Agent. Every operation retains an overall body deadline. */
|
|
6
|
+
export declare function createHivesNodeTransport(opts: GenesisOptions, callerSignal?: AbortSignal): typeof fetch;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Agent } from 'undici';
|
|
2
|
+
import { Genesis, operationTable } from '@lifesi/genesis';
|
|
3
|
+
// Node's native fetch accepts a compatible per-request dispatcher. Never change
|
|
4
|
+
// the global dispatcher or the portable SDK: other products retain their transport.
|
|
5
|
+
const agentOptions = { headersTimeout: 340_000, bodyTimeout: 340_000, connect: { timeout: 5_000 }, connections: 4, pipelining: 1 };
|
|
6
|
+
let agent;
|
|
7
|
+
const nativeAgent = () => agent ??= new Agent(agentOptions);
|
|
8
|
+
export async function closeHivesNodeTransport() {
|
|
9
|
+
const current = agent;
|
|
10
|
+
agent = undefined;
|
|
11
|
+
await current?.destroy();
|
|
12
|
+
}
|
|
13
|
+
export function hivesOperationBudget(id) {
|
|
14
|
+
if (id === 'hives_cancel_deployment' || id === 'hives_delete_deployment')
|
|
15
|
+
return 330_000;
|
|
16
|
+
if (['hives_create_deployment', 'hives_upload_deployment', 'hives_invoke_deployment', 'hives_invoke_function', 'hives_invoke_function_async'].includes(id))
|
|
17
|
+
return 40_000;
|
|
18
|
+
if (['hives_create_function', 'hives_update_function', 'hives_delete_function'].includes(id))
|
|
19
|
+
return 100_000;
|
|
20
|
+
return 20_000;
|
|
21
|
+
}
|
|
22
|
+
const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
23
|
+
const templatePattern = (value) => new RegExp('^' + value.split(/(\{[^}]+\})/).map((part) => part.startsWith('{') ? '[^/]+' : escape(part)).join('') + '$');
|
|
24
|
+
/** Hives leaf transport. Caller fetch overrides stay authoritative; only native
|
|
25
|
+
* global fetch gets our Agent. Every operation retains an overall body deadline. */
|
|
26
|
+
export function createHivesNodeTransport(opts, callerSignal) {
|
|
27
|
+
const supplied = opts.fetch ?? globalThis.fetch;
|
|
28
|
+
const underlying = supplied.bind(globalThis);
|
|
29
|
+
const useNative = supplied === globalThis.fetch;
|
|
30
|
+
const resolver = new Genesis(opts);
|
|
31
|
+
const routes = Object.entries(operationTable)
|
|
32
|
+
.filter(([, op]) => op.service === 'hives' && !op.internal)
|
|
33
|
+
.flatMap(([id, op]) => {
|
|
34
|
+
const direct = resolver.routing === 'direct';
|
|
35
|
+
let base;
|
|
36
|
+
try {
|
|
37
|
+
base = new URL(direct ? (opts.upstreams?.hives ?? op.upstream.base) : resolver.baseUrl);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new Error('Invalid Hives service address.');
|
|
41
|
+
}
|
|
42
|
+
const path = direct ? op.upstream.path : op.path;
|
|
43
|
+
const prefix = base.pathname.replace(/\/+$/, '');
|
|
44
|
+
const paths = [path];
|
|
45
|
+
if (!direct && path.startsWith('/v1/hives/'))
|
|
46
|
+
paths.push(path.replace('/v1/hives/', '/hives/v1/'));
|
|
47
|
+
return paths.map((p) => ({ id, origin: base.origin, method: direct ? op.upstream.method : op.method, pattern: templatePattern(prefix + p) }));
|
|
48
|
+
});
|
|
49
|
+
return (async (input, init = {}) => {
|
|
50
|
+
let url;
|
|
51
|
+
try {
|
|
52
|
+
url = new URL(input instanceof Request ? input.url : String(input));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new Error('Invalid Hives request address.');
|
|
56
|
+
}
|
|
57
|
+
const method = (init.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();
|
|
58
|
+
const route = routes.find((entry) => entry.origin === url.origin && entry.method === method && entry.pattern.test(url.pathname));
|
|
59
|
+
if (!route)
|
|
60
|
+
return underlying(input, init);
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
let message = 'Hives transport failed. Refresh the resource and retry.';
|
|
63
|
+
const safeError = () => new Error(message);
|
|
64
|
+
const signals = [...new Set([callerSignal, init.signal, input instanceof Request ? input.signal : undefined].filter((signal) => !!signal))];
|
|
65
|
+
const canceled = () => {
|
|
66
|
+
message = 'Hives request canceled before completion. Refresh the resource before retrying.';
|
|
67
|
+
controller.abort();
|
|
68
|
+
};
|
|
69
|
+
for (const signal of signals)
|
|
70
|
+
signal.addEventListener('abort', canceled, { once: true });
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
message = 'Hives request deadline exceeded. Refresh the resource before retrying.';
|
|
73
|
+
controller.abort();
|
|
74
|
+
}, hivesOperationBudget(route.id));
|
|
75
|
+
timer.unref();
|
|
76
|
+
let abortReject = () => { };
|
|
77
|
+
let reader;
|
|
78
|
+
let bodyController;
|
|
79
|
+
const aborted = () => {
|
|
80
|
+
abortReject(safeError());
|
|
81
|
+
bodyController?.error(safeError());
|
|
82
|
+
void reader?.cancel().catch(() => { });
|
|
83
|
+
cleanup();
|
|
84
|
+
};
|
|
85
|
+
controller.signal.addEventListener('abort', aborted, { once: true });
|
|
86
|
+
const abortPromise = new Promise((_, reject) => { abortReject = reject; });
|
|
87
|
+
void abortPromise.catch(() => { });
|
|
88
|
+
const cleanup = () => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
for (const signal of signals)
|
|
91
|
+
signal.removeEventListener('abort', canceled);
|
|
92
|
+
controller.signal.removeEventListener('abort', aborted);
|
|
93
|
+
};
|
|
94
|
+
if (signals.some((signal) => signal.aborted))
|
|
95
|
+
canceled();
|
|
96
|
+
try {
|
|
97
|
+
if (controller.signal.aborted)
|
|
98
|
+
throw safeError();
|
|
99
|
+
const requestInit = {
|
|
100
|
+
...init, signal: controller.signal, cache: 'no-store',
|
|
101
|
+
redirect: init.redirect === 'manual' ? 'manual' : 'error',
|
|
102
|
+
...(useNative ? { dispatcher: nativeAgent() } : {}),
|
|
103
|
+
};
|
|
104
|
+
const pending = underlying(input, requestInit);
|
|
105
|
+
// A caller transport may ignore AbortSignal. Reject on our budget anyway,
|
|
106
|
+
// and discard any late response rather than presenting an acknowledgement.
|
|
107
|
+
void pending.then((response) => { if (controller.signal.aborted)
|
|
108
|
+
void response.body?.cancel().catch(() => { }); }, () => { });
|
|
109
|
+
const response = await Promise.race([pending, abortPromise]);
|
|
110
|
+
if (controller.signal.aborted)
|
|
111
|
+
throw safeError();
|
|
112
|
+
if (!response.body) {
|
|
113
|
+
cleanup();
|
|
114
|
+
return response;
|
|
115
|
+
}
|
|
116
|
+
reader = response.body.getReader();
|
|
117
|
+
const body = new ReadableStream({
|
|
118
|
+
start(value) { bodyController = value; },
|
|
119
|
+
async pull(value) {
|
|
120
|
+
try {
|
|
121
|
+
const next = await reader.read();
|
|
122
|
+
if (controller.signal.aborted)
|
|
123
|
+
return;
|
|
124
|
+
if (next.done) {
|
|
125
|
+
cleanup();
|
|
126
|
+
value.close();
|
|
127
|
+
}
|
|
128
|
+
else
|
|
129
|
+
value.enqueue(next.value);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
cleanup();
|
|
133
|
+
value.error(safeError());
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
async cancel() { cleanup(); await reader.cancel().catch(() => { }); },
|
|
137
|
+
});
|
|
138
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
cleanup();
|
|
142
|
+
throw safeError();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { type CredentialStore } from './store.js';
|
|
3
|
+
import { type Out } from './output.js';
|
|
4
|
+
import { type SecretInputOptions } from './secret-input.js';
|
|
5
|
+
export interface Deps {
|
|
6
|
+
out?: Out;
|
|
7
|
+
store?: () => Promise<CredentialStore>;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
/** Cancel Hives calls through response-body completion; other products retain their transport. */
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
env?: Record<string, string | undefined>;
|
|
12
|
+
openBrowser?: (url: string) => void;
|
|
13
|
+
sleep?: (ms: number) => Promise<void>;
|
|
14
|
+
readSecretInput?: (options: SecretInputOptions, limit: number) => Promise<string>;
|
|
15
|
+
readWasmFile?: (path: string) => Promise<Blob>;
|
|
16
|
+
writeBytes?: (bytes: Uint8Array) => void;
|
|
17
|
+
writeOutputFile?: (path: string, bytes: Uint8Array) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare function buildProgram(deps?: Deps): Command;
|
|
20
|
+
export declare function main(argv: string[], deps?: Deps): Promise<number>;
|