@onesi/genesis-cli 0.0.1-stage → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/bin/genesis.js +3 -0
- package/dist/auth.d.ts +30 -0
- package/dist/auth.js +273 -0
- package/dist/beacons.d.ts +3 -0
- package/dist/beacons.js +94 -0
- package/dist/device-login.d.ts +25 -0
- package/dist/device-login.js +173 -0
- package/dist/dials.d.ts +7 -0
- package/dist/dials.js +96 -0
- package/dist/fleets.d.ts +4 -0
- package/dist/fleets.js +72 -0
- package/dist/flocks.d.ts +3 -0
- package/dist/flocks.js +72 -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 +26 -0
- package/dist/hives-marketplace.d.ts +10 -0
- package/dist/hives-marketplace.js +29 -0
- package/dist/hives-node-transport.d.ts +6 -0
- package/dist/hives-node-transport.js +147 -0
- package/dist/hives-operations.d.ts +9 -0
- package/dist/hives-operations.js +61 -0
- package/dist/hives-resources.d.ts +9 -0
- package/dist/hives-resources.js +94 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +640 -0
- package/dist/lifeid-native.d.ts +24 -0
- package/dist/lifeid-native.js +113 -0
- package/dist/minds-owner.d.ts +3 -0
- package/dist/minds-owner.js +42 -0
- package/dist/minds.d.ts +3 -0
- package/dist/minds.js +90 -0
- package/dist/output.d.ts +28 -0
- package/dist/output.js +46 -0
- package/dist/products.d.ts +14 -0
- package/dist/products.js +85 -0
- package/dist/relays-enrollment.d.ts +3 -0
- package/dist/relays-enrollment.js +7 -0
- package/dist/runner.d.ts +4 -0
- package/dist/runner.js +55 -0
- package/dist/secret-input.d.ts +14 -0
- package/dist/secret-input.js +85 -0
- package/dist/store.d.ts +39 -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 +54 -1
package/dist/dials.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
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 } : {}) })); });
|
|
78
|
+
const calls = dials.command('calls').description('Call activity and approved outbound calls');
|
|
79
|
+
calls.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.dials.calls.list(scope)); });
|
|
80
|
+
// @file keeps structured approval and idempotency fields out of shell history.
|
|
81
|
+
for (const [parent, name, operation] of [
|
|
82
|
+
[numbers, 'caller-name', 'dials_updateNumberCallerName'], [numbers, 'reconcile', 'dials_reconcileNumberInventory'],
|
|
83
|
+
[numbers, 'provision', 'dials_provisionNumber'], [numbers, 'assign', 'dials_assignNumber'], [numbers, 'release', 'dials_releaseNumber'], [numbers, 'e911', 'dials_registerEmergencyAddress'],
|
|
84
|
+
[calls, 'validate', 'dials_validateOutboundCall'], [calls, 'create', 'dials_createOutboundCall'], [calls, 'recover', 'dials_recoverOutboundCall'],
|
|
85
|
+
]) {
|
|
86
|
+
parent.command(name).requiredOption('--body <json-or-@file>', 'JSON input including actorId and required operation fields')
|
|
87
|
+
.action(async (options) => {
|
|
88
|
+
const { client, scope } = await context();
|
|
89
|
+
const text = options.body.startsWith('@') ? await readFile(options.body.slice(1), 'utf8') : options.body;
|
|
90
|
+
const body = JSON.parse(text);
|
|
91
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
92
|
+
throw new Error('Dials body must be a JSON object.');
|
|
93
|
+
emit(await client.call(operation, { body: { ...body, scope } }));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
package/dist/fleets.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { Genesis } from '@onesi/genesis';
|
|
3
|
+
export declare function readFoundryKey(input?: AsyncIterable<Uint8Array>): Promise<string>;
|
|
4
|
+
export declare function registerFleets(program: Command, client: () => Promise<Genesis>, emit: (data: unknown) => void, keyInput?: () => Promise<string>): void;
|
package/dist/fleets.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
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 Henry instances and tasks');
|
|
24
|
+
const agents = fleets.command('agents').description('Persistent Henry agents on Fleets-managed compute');
|
|
25
|
+
agents.command('list').action(async () => emit(await (await client()).fleets.agents.list()));
|
|
26
|
+
agents.command('launch <name>').option('--memory <provider>', 'memory provider (minds, files or custom)', 'minds').option('--namespace <namespace>').option('--endpoint <url>').option('--model <model>').option('--instructions <text>').requiredOption('--idempotency-key <key>', 'stable key to reuse on retry').action(async (name, options) => {
|
|
27
|
+
const provider = options.memory;
|
|
28
|
+
if (!['minds', 'files', 'custom'].includes(provider))
|
|
29
|
+
throw new Error('Memory provider must be minds, files, or custom.');
|
|
30
|
+
if (provider === 'custom' && !options.endpoint)
|
|
31
|
+
throw new Error('Custom memory requires --endpoint.');
|
|
32
|
+
emit(await (await client()).fleets.agents.launch({ name, memory: { provider, ...(options.namespace ? { namespace: options.namespace } : {}), ...(options.endpoint ? { endpoint: options.endpoint } : {}) }, ...(options.model ? { model: options.model } : {}), ...(options.instructions ? { instructions: options.instructions } : {}), idempotency_key: options.idempotencyKey }));
|
|
33
|
+
});
|
|
34
|
+
agents.command('get <agent_id>').action(async (id) => emit(await (await client()).fleets.agents.get(id)));
|
|
35
|
+
agents.command('command <agent_id> <input>').option('--delivery <mode>', 'queue, steer or interrupt', 'queue').action(async (id, input, options) => {
|
|
36
|
+
if (!['queue', 'steer', 'interrupt'].includes(options.delivery))
|
|
37
|
+
throw new Error('Delivery must be queue, steer, or interrupt.');
|
|
38
|
+
emit(await (await client()).fleets.agents.command(id, { input, delivery: options.delivery }));
|
|
39
|
+
});
|
|
40
|
+
agents.command('stop <agent_id>').action(async (id) => emit(await (await client()).fleets.agents.stop(id)));
|
|
41
|
+
agents.command('recover <agent_id>').action(async (id) => emit(await (await client()).fleets.agents.recover(id)));
|
|
42
|
+
const instances = fleets.command('instances').description('Actual admitted Henry Host status');
|
|
43
|
+
instances.command('list').option('--limit <number>').option('--after <instance_id>').action(async (options) => {
|
|
44
|
+
emit(await (await client()).fleets.instances.list({ ...(options.limit ? { limit: Number(options.limit) } : {}), ...(options.after ? { after: options.after } : {}) }));
|
|
45
|
+
});
|
|
46
|
+
instances.command('get <instance_id>').action(async (id) => emit(await (await client()).fleets.instances.get(id)));
|
|
47
|
+
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 })));
|
|
48
|
+
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 })));
|
|
49
|
+
const connection = fleets.command('connection').description('Organization-owned Foundry connection');
|
|
50
|
+
connection.command('get').action(async () => emit(await (await client()).fleets.connection.get()));
|
|
51
|
+
connection.command('set').description('Read a Foundry key from stdin and encrypt it with Genesis KMS').action(async () => {
|
|
52
|
+
const apiKey = await keyInput();
|
|
53
|
+
emit(await (await client()).fleets.connection.set(apiKey));
|
|
54
|
+
});
|
|
55
|
+
connection.command('delete').action(async () => emit(await (await client()).fleets.connection.delete()));
|
|
56
|
+
const usage = fleets.command('usage').description('Usage observations derived from your tasks');
|
|
57
|
+
usage.command('events').description('List task lifecycle and execution-time usage events').option('--limit <n>').option('--before <task_id>')
|
|
58
|
+
.action(async (options) => emit(await (await client()).fleets.usage.events({ ...(options.limit ? { limit: Number(options.limit) } : {}), ...(options.before ? { before: options.before } : {}) })));
|
|
59
|
+
const tasks = fleets.command('tasks');
|
|
60
|
+
tasks.command('list').option('--limit <number>').option('--before <task_id>').action(async (options) => {
|
|
61
|
+
emit(await (await client()).fleets.tasks.list({ ...(options.limit ? { limit: Number(options.limit) } : {}), ...(options.before ? { before: options.before } : {}) }));
|
|
62
|
+
});
|
|
63
|
+
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) => {
|
|
64
|
+
const timeout = Number(options.timeout);
|
|
65
|
+
if (!Number.isInteger(timeout) || timeout < 1 || timeout > 3600)
|
|
66
|
+
throw new Error('Timeout must be 1–3600 seconds.');
|
|
67
|
+
emit(await (await client()).fleets.tasks.submit({ input, timeout_seconds: timeout, idempotency_key: options.idempotencyKey }));
|
|
68
|
+
});
|
|
69
|
+
tasks.command('get <task_id>').action(async (id) => emit(await (await client()).fleets.tasks.get(id)));
|
|
70
|
+
tasks.command('cancel <task_id>').action(async (id) => emit(await (await client()).fleets.tasks.cancel(id)));
|
|
71
|
+
tasks.command('receipt <task_id>').action(async (id) => emit(await (await client()).fleets.tasks.receipt(id)));
|
|
72
|
+
}
|
package/dist/flocks.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { Genesis } from '@onesi/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,72 @@
|
|
|
1
|
+
import { readFunctionJson } from './function-input.js';
|
|
2
|
+
export function registerFlocks(program, client, emit, env = process.env) {
|
|
3
|
+
const flocks = program.command('flocks').description('Flocks: your machines and cloud instances with a harness installed (nodes, harnesses, agents and runs)');
|
|
4
|
+
const secret = (name) => {
|
|
5
|
+
const value = env[name];
|
|
6
|
+
if (!value)
|
|
7
|
+
throw new Error(`Set ${name} in the environment; do not pass Flocks credentials as command-line arguments.`);
|
|
8
|
+
return value;
|
|
9
|
+
};
|
|
10
|
+
const show = async (run) => {
|
|
11
|
+
const data = await run(await client());
|
|
12
|
+
emit(data, () => JSON.stringify(data, null, 2));
|
|
13
|
+
};
|
|
14
|
+
flocks.command('readiness').description('Check whether node enrollment is currently available').action(() => show(g => g.flocks.readiness.get()));
|
|
15
|
+
flocks.command('nodes').description('List your org nodes').action(() => show(g => g.flocks.nodes.list()));
|
|
16
|
+
const paging = (command) => command.option('--limit <n>', 'page size, 1–100').option('--cursor <cursor>', 'next_cursor from the previous page');
|
|
17
|
+
const query = (opts) => ({
|
|
18
|
+
...(opts.limit === undefined ? {} : { limit: Number(opts.limit) }),
|
|
19
|
+
...(opts.cursor === undefined ? {} : { cursor: opts.cursor }),
|
|
20
|
+
...(opts.agentId === undefined ? {} : { agent_id: opts.agentId }),
|
|
21
|
+
});
|
|
22
|
+
paging(flocks.command('harnesses').description('List the harnesses each node reports, with version and readiness'))
|
|
23
|
+
.action((opts) => show(g => g.flocks.harnesses.list(query(opts))));
|
|
24
|
+
const agents = flocks.command('agents').description('Harness agents, and canonical agent execution (a_<ULID>; needs a signed-in session, not an API key)');
|
|
25
|
+
paging(agents.command('list').description('List agents')).action((opts) => show(g => g.flocks.agents.list(query(opts))));
|
|
26
|
+
agents.command('create <name> <harness>').description('Create an agent on a harness')
|
|
27
|
+
.option('--node <id>', 'pin the agent to one of your online nodes')
|
|
28
|
+
.action((name, harness, opts) => show(g => g.flocks.agents.create({ name, harness, ...(opts.node === undefined ? {} : { node_id: opts.node }) })));
|
|
29
|
+
agents.command('get <agent_id>').action((id) => show(g => g.flocks.agents.get(id)));
|
|
30
|
+
agents.command('execute <agent_id>').description('Execute a task on a canonical agent; the JSON request needs idempotency_key, task, driver, provider_account_id, target_roost and external_correlation')
|
|
31
|
+
.requiredOption('--body-file <path>', 'JSON request body, up to 64 KiB')
|
|
32
|
+
.action(async (id, opts) => { const body = await readFunctionJson(opts.bodyFile, 64 * 1024); return show(g => g.flocks.agents.execute(id, body)); });
|
|
33
|
+
agents.command('status <agent_id>').description('Observe the latest canonical execution').action((id) => show(g => g.flocks.agents.status(id)));
|
|
34
|
+
agents.command('cancel <agent_id>').description('Cancel the canonical execution admitted under an idempotency key')
|
|
35
|
+
.requiredOption('--idempotency-key <key>', 'the key the execution was admitted with')
|
|
36
|
+
.action((id, opts) => show(g => g.flocks.agents.cancel(id, opts.idempotencyKey)));
|
|
37
|
+
const runs = flocks.command('runs').description('Recorded agent runs');
|
|
38
|
+
paging(runs.command('list').description('List runs').option('--agent-id <id>', 'only runs of this agent'))
|
|
39
|
+
.action((opts) => show(g => g.flocks.runs.list(query(opts))));
|
|
40
|
+
runs.command('get <run_id>').action((id) => show(g => g.flocks.runs.get(id)));
|
|
41
|
+
flocks.command('join-token').option('--ttl <seconds>', 'lifetime (60–600 seconds)', '600').action((opts) => {
|
|
42
|
+
const ttl = Number(opts.ttl);
|
|
43
|
+
if (!Number.isInteger(ttl) || ttl < 60 || ttl > 600)
|
|
44
|
+
throw new Error('TTL must be 60–600 seconds');
|
|
45
|
+
return show(g => g.flocks.joinTokens.create(ttl));
|
|
46
|
+
});
|
|
47
|
+
flocks.command('rename <id> <nickname>').action((id, nickname) => show(g => g.flocks.nodes.rename(id, nickname)));
|
|
48
|
+
flocks.command('revoke <id>').action((id) => show(g => g.flocks.nodes.revoke(id)));
|
|
49
|
+
flocks.command('enroll <name> <platform> <cpus>').description('Enroll this node with FLOCKS_JOIN_TOKEN from the environment').action((name, platform, cpus) => {
|
|
50
|
+
const cpu_count = Number(cpus);
|
|
51
|
+
if (!Number.isInteger(cpu_count) || cpu_count < 1 || cpu_count > 65536)
|
|
52
|
+
throw new Error('CPUs must be 1–65536');
|
|
53
|
+
const token = secret('FLOCKS_JOIN_TOKEN');
|
|
54
|
+
return show(g => g.flocks.nodes.enroll({ token, name, platform, cpu_count }));
|
|
55
|
+
});
|
|
56
|
+
flocks.command('heartbeat <id>').description('Send presence using FLOCKS_NODE_BINDING from the environment').action((id) => {
|
|
57
|
+
const binding = secret('FLOCKS_NODE_BINDING');
|
|
58
|
+
return show(g => g.flocks.nodes.heartbeat(id, binding));
|
|
59
|
+
});
|
|
60
|
+
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') })));
|
|
61
|
+
flocks.command('activate <id> <name> <platform> <cpus>').description('Activate the saved node key with FLOCKS_NODE_NONCE').action((id, name, platform, cpus) => {
|
|
62
|
+
const cpu_count = Number(cpus);
|
|
63
|
+
if (!Number.isInteger(cpu_count) || cpu_count < 1 || cpu_count > 65536)
|
|
64
|
+
throw new Error('CPUs must be 1–65536');
|
|
65
|
+
return show(g => g.flocks.nodes.activate(id, { name, platform, cpu_count, node_nonce: secret('FLOCKS_NODE_NONCE') }));
|
|
66
|
+
});
|
|
67
|
+
flocks.command('rotate <id> <action>').description('prepare→persist→commit, or cancel; use saved FLOCKS_NODE_NONCE/FLOCKS_ROTATION_NONCE').action((id, action) => {
|
|
68
|
+
if (!['prepare', 'commit', 'cancel'].includes(action))
|
|
69
|
+
throw new Error('Action must be prepare, commit or cancel');
|
|
70
|
+
return show(g => g.flocks.nodes.rotateCredential(id, { action: action, node_nonce: secret('FLOCKS_NODE_NONCE'), operation_nonce: secret('FLOCKS_ROTATION_NONCE') }));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -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,26 @@
|
|
|
1
|
+
import { readFoundryKey } from './fleets.js';
|
|
2
|
+
export function addHenryCommands(program, client, output) {
|
|
3
|
+
const henry = program.command('henry').description('Henry: agent runtime, live activity and approvals');
|
|
4
|
+
henry.command('config').action(async () => output(await (await client()).henry.config()));
|
|
5
|
+
henry.command('credential').description('Read a Foundry API key from stdin and encrypt it in Genesis KMS')
|
|
6
|
+
.action(async () => {
|
|
7
|
+
const apiKey = await readFoundryKey();
|
|
8
|
+
output(await (await client()).henry.credential(apiKey));
|
|
9
|
+
});
|
|
10
|
+
henry.command('models').description('List the Foundry models the stored organization key can run').action(async () => output(await (await client()).henry.models()));
|
|
11
|
+
henry.command('runs').option('--page-token <token>').action(async (options) => output(await (await client()).henry.runs.list(options.pageToken)));
|
|
12
|
+
henry.command('run').requiredOption('--model <model>').requiredOption('--task <text>').option('--run-id <uuid>', 'UUIDv4 for concurrent activity polling')
|
|
13
|
+
.action(async (options) => output(await (await client()).henry.runs.create({ model: options.model, task: options.task, ...(options.runId ? { run_id: options.runId } : {}) })));
|
|
14
|
+
henry.command('get <run_id>').action(async (runId) => output(await (await client()).henry.runs.get(runId)));
|
|
15
|
+
henry.command('command <run_id> <action>').option('--text <text>')
|
|
16
|
+
.action(async (runId, action, options) => {
|
|
17
|
+
if (!['pause', 'resume', 'cancel', 'input'].includes(action))
|
|
18
|
+
throw new Error('Action must be pause, resume, cancel or input');
|
|
19
|
+
output(await (await client()).henry.runs.command(runId, action, options.text));
|
|
20
|
+
});
|
|
21
|
+
henry.command('approval <run_id> <approval_id> <decision>').action(async (runId, approvalId, decision) => {
|
|
22
|
+
if (!['approve', 'reject'].includes(decision))
|
|
23
|
+
throw new Error('Decision must be approve or reject');
|
|
24
|
+
output(await (await client()).henry.runs.approval(runId, approvalId, decision));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { Genesis } from '@onesi/genesis';
|
|
3
|
+
type Deps = {
|
|
4
|
+
client: () => Promise<Genesis>;
|
|
5
|
+
emit: (data: unknown, pretty: () => string) => void;
|
|
6
|
+
readRequest?: (path: string, limit: number) => Promise<Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
/** Environment values belong in a bounded private JSON file, never command-line options. */
|
|
9
|
+
export declare function registerHivesMarketplace(hives: Command, deps: Deps): void;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Option } from 'commander';
|
|
2
|
+
import { readFunctionJson } from './function-input.js';
|
|
3
|
+
function number(value) { if (value === undefined)
|
|
4
|
+
return undefined; if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
5
|
+
throw new Error('Invalid page option.'); return Number(value); }
|
|
6
|
+
/** Environment values belong in a bounded private JSON file, never command-line options. */
|
|
7
|
+
export function registerHivesMarketplace(hives, deps) {
|
|
8
|
+
const m = hives.command('marketplace').description('Existing Hives packages, publication and persisted installations');
|
|
9
|
+
const output = (data) => deps.emit(data, () => JSON.stringify(data, null, 2));
|
|
10
|
+
const read = deps.readRequest ?? readFunctionJson;
|
|
11
|
+
const api = async () => (await deps.client()).hives.marketplace;
|
|
12
|
+
m.command('list <project_id>').option('--query <value>').option('--category <value>').option('--free-only').option('--verified-only')
|
|
13
|
+
.addOption(new Option('--sort <field>').choices(['name', 'installs', 'stars', 'created', 'updated']))
|
|
14
|
+
.addOption(new Option('--order <direction>').choices(['asc', 'desc'])).option('--page <number>').option('--page-size <number>')
|
|
15
|
+
.action(async (project, o) => output(await (await api()).list(project, { q: o.query, category: o.category, free_only: o.freeOnly, verified_only: o.verifiedOnly, sort: o.sort, order: o.order, page: number(o.page), page_size: number(o.pageSize) })));
|
|
16
|
+
m.command('get <project_id> <slug>').option('--version <semver>').action(async (project, slug, o) => output(await (await api()).get(project, slug, { version: o.version })));
|
|
17
|
+
m.command('versions <project_id> <slug>').option('--page <number>').option('--page-size <number>').action(async (project, slug, o) => output(await (await api()).versions(project, slug, { page: number(o.page), page_size: number(o.pageSize) })));
|
|
18
|
+
for (const action of ['categories', 'featured'])
|
|
19
|
+
m.command(`${action} <project_id>`).action(async (project) => output(await (await api())[action](project)));
|
|
20
|
+
m.command('publish <project_id>').requiredOption('--request-file <path>', 'JSON publication with an owned uploaded module_hash and immutable version')
|
|
21
|
+
.action(async (project, o) => output(await (await api()).publish(project, await read(o.requestFile, 512 * 1024))));
|
|
22
|
+
for (const action of ['install', 'update'])
|
|
23
|
+
m.command(`${action} <project_id> <slug>`).requiredOption('--request-file <path>', 'JSON version and env_overrides; use {} for defaults')
|
|
24
|
+
.action(async (project, slug, o) => output(await (await api())[action](project, slug, await read(o.requestFile, 512 * 1024))));
|
|
25
|
+
m.command('installed <project_id>').option('--page <number>').option('--page-size <number>').action(async (project, o) => output(await (await api()).installed(project, { page: number(o.page), page_size: number(o.pageSize) })));
|
|
26
|
+
m.command('installation <project_id> <installation_id>').action(async (project, id) => output(await (await api()).getInstallation(project, id)));
|
|
27
|
+
for (const action of ['reconcile', 'uninstall'])
|
|
28
|
+
m.command(`${action} <project_id> <installation_id>`).action(async (project, id) => output(await (await api())[action](project, id)));
|
|
29
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type GenesisOptions } from '@onesi/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,147 @@
|
|
|
1
|
+
import { Agent } from 'undici';
|
|
2
|
+
import { Genesis, operationTable } from '@onesi/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 (['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;
|
|
16
|
+
if (id === 'hives_cancel_deployment' || id === 'hives_delete_deployment' || id === 'hives_stop_deployment')
|
|
17
|
+
return 330_000;
|
|
18
|
+
if (['hives_create_deployment', 'hives_upload_deployment', 'hives_invoke_deployment', 'hives_invoke_function', 'hives_invoke_function_async'].includes(id))
|
|
19
|
+
return 40_000;
|
|
20
|
+
if (['hives_create_function', 'hives_update_function', 'hives_delete_function'].includes(id))
|
|
21
|
+
return 100_000;
|
|
22
|
+
return 20_000;
|
|
23
|
+
}
|
|
24
|
+
const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
25
|
+
const templatePattern = (value) => new RegExp('^' + value.split(/(\{[^}]+\})/).map((part) => part.startsWith('{') ? '[^/]+' : escape(part)).join('') + '$');
|
|
26
|
+
/** Hives leaf transport. Caller fetch overrides stay authoritative; only native
|
|
27
|
+
* global fetch gets our Agent. Every operation retains an overall body deadline. */
|
|
28
|
+
export function createHivesNodeTransport(opts, callerSignal) {
|
|
29
|
+
const supplied = opts.fetch ?? globalThis.fetch;
|
|
30
|
+
const underlying = supplied.bind(globalThis);
|
|
31
|
+
const useNative = supplied === globalThis.fetch;
|
|
32
|
+
const resolver = new Genesis(opts);
|
|
33
|
+
const routes = Object.entries(operationTable)
|
|
34
|
+
.filter(([, op]) => op.service === 'hives' && !op.internal)
|
|
35
|
+
.flatMap(([id, op]) => {
|
|
36
|
+
const direct = resolver.routing === 'direct';
|
|
37
|
+
let base;
|
|
38
|
+
try {
|
|
39
|
+
base = new URL(direct ? (opts.upstreams?.hives ?? op.upstream.base) : resolver.baseUrl);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new Error('Invalid Hives service address.');
|
|
43
|
+
}
|
|
44
|
+
const path = direct ? op.upstream.path : op.path;
|
|
45
|
+
const prefix = base.pathname.replace(/\/+$/, '');
|
|
46
|
+
const paths = [path];
|
|
47
|
+
if (!direct && path.startsWith('/v1/hives/'))
|
|
48
|
+
paths.push(path.replace('/v1/hives/', '/hives/v1/'));
|
|
49
|
+
return paths.map((p) => ({ id, origin: base.origin, method: direct ? op.upstream.method : op.method, pattern: templatePattern(prefix + p) }));
|
|
50
|
+
});
|
|
51
|
+
return (async (input, init = {}) => {
|
|
52
|
+
let url;
|
|
53
|
+
try {
|
|
54
|
+
url = new URL(input instanceof Request ? input.url : String(input));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new Error('Invalid Hives request address.');
|
|
58
|
+
}
|
|
59
|
+
const method = (init.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();
|
|
60
|
+
const route = routes.find((entry) => entry.origin === url.origin && entry.method === method && entry.pattern.test(url.pathname));
|
|
61
|
+
if (!route)
|
|
62
|
+
return underlying(input, init);
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
let message = 'Hives transport failed. Refresh the resource and retry.';
|
|
65
|
+
const safeError = () => new Error(message);
|
|
66
|
+
const signals = [...new Set([callerSignal, init.signal, input instanceof Request ? input.signal : undefined].filter((signal) => !!signal))];
|
|
67
|
+
const canceled = () => {
|
|
68
|
+
message = 'Hives request canceled before completion. Refresh the resource before retrying.';
|
|
69
|
+
controller.abort();
|
|
70
|
+
};
|
|
71
|
+
for (const signal of signals)
|
|
72
|
+
signal.addEventListener('abort', canceled, { once: true });
|
|
73
|
+
const timer = setTimeout(() => {
|
|
74
|
+
message = 'Hives request deadline exceeded. Refresh the resource before retrying.';
|
|
75
|
+
controller.abort();
|
|
76
|
+
}, hivesOperationBudget(route.id));
|
|
77
|
+
timer.unref();
|
|
78
|
+
let abortReject = () => { };
|
|
79
|
+
let reader;
|
|
80
|
+
let bodyController;
|
|
81
|
+
const aborted = () => {
|
|
82
|
+
abortReject(safeError());
|
|
83
|
+
bodyController?.error(safeError());
|
|
84
|
+
void reader?.cancel().catch(() => { });
|
|
85
|
+
cleanup();
|
|
86
|
+
};
|
|
87
|
+
controller.signal.addEventListener('abort', aborted, { once: true });
|
|
88
|
+
const abortPromise = new Promise((_, reject) => { abortReject = reject; });
|
|
89
|
+
void abortPromise.catch(() => { });
|
|
90
|
+
const cleanup = () => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
for (const signal of signals)
|
|
93
|
+
signal.removeEventListener('abort', canceled);
|
|
94
|
+
controller.signal.removeEventListener('abort', aborted);
|
|
95
|
+
};
|
|
96
|
+
if (signals.some((signal) => signal.aborted))
|
|
97
|
+
canceled();
|
|
98
|
+
try {
|
|
99
|
+
if (controller.signal.aborted)
|
|
100
|
+
throw safeError();
|
|
101
|
+
const requestInit = {
|
|
102
|
+
...init, signal: controller.signal, cache: 'no-store',
|
|
103
|
+
redirect: init.redirect === 'manual' ? 'manual' : 'error',
|
|
104
|
+
...(useNative ? { dispatcher: nativeAgent() } : {}),
|
|
105
|
+
};
|
|
106
|
+
const pending = underlying(input, requestInit);
|
|
107
|
+
// A caller transport may ignore AbortSignal. Reject on our budget anyway,
|
|
108
|
+
// and discard any late response rather than presenting an acknowledgement.
|
|
109
|
+
void pending.then((response) => { if (controller.signal.aborted)
|
|
110
|
+
void response.body?.cancel().catch(() => { }); }, () => { });
|
|
111
|
+
const response = await Promise.race([pending, abortPromise]);
|
|
112
|
+
if (controller.signal.aborted)
|
|
113
|
+
throw safeError();
|
|
114
|
+
if (!response.body) {
|
|
115
|
+
cleanup();
|
|
116
|
+
return response;
|
|
117
|
+
}
|
|
118
|
+
reader = response.body.getReader();
|
|
119
|
+
const body = new ReadableStream({
|
|
120
|
+
start(value) { bodyController = value; },
|
|
121
|
+
async pull(value) {
|
|
122
|
+
try {
|
|
123
|
+
const next = await reader.read();
|
|
124
|
+
if (controller.signal.aborted)
|
|
125
|
+
return;
|
|
126
|
+
if (next.done) {
|
|
127
|
+
cleanup();
|
|
128
|
+
value.close();
|
|
129
|
+
}
|
|
130
|
+
else
|
|
131
|
+
value.enqueue(next.value);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
cleanup();
|
|
135
|
+
value.error(safeError());
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
async cancel() { cleanup(); await reader.cancel().catch(() => { }); },
|
|
139
|
+
});
|
|
140
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
cleanup();
|
|
144
|
+
throw safeError();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { Genesis } from '@onesi/genesis';
|
|
3
|
+
type Deps = {
|
|
4
|
+
client: () => Promise<Genesis>;
|
|
5
|
+
emit: (data: unknown, pretty: () => string) => void;
|
|
6
|
+
readRequest?: (path: string, limit: number) => Promise<Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
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
|
+
}
|