@onesi/genesis-cli 0.1.13 → 0.2.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/README.md +29 -2
- package/dist/beacons.js +68 -0
- package/dist/dials.js +7 -4
- package/dist/fleets.js +30 -0
- package/dist/flocks.d.ts +7 -1
- package/dist/flocks.js +257 -3
- package/dist/henry.d.ts +5 -1
- package/dist/henry.js +60 -1
- package/dist/hives-coverage.d.ts +12 -0
- package/dist/hives-coverage.js +116 -0
- package/dist/hives-node-transport.js +3 -0
- package/dist/hives-operations.d.ts +1 -0
- package/dist/hives-operations.js +2 -0
- package/dist/hives-platform.d.ts +13 -0
- package/dist/hives-platform.js +133 -0
- package/dist/iam-directory.d.ts +15 -0
- package/dist/iam-directory.js +71 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +255 -23
- package/dist/minds.d.ts +1 -1
- package/dist/minds.js +37 -2
- package/dist/onboard/art.d.ts +50 -0
- package/dist/onboard/art.js +139 -0
- package/dist/onboard/brand.d.ts +56 -0
- package/dist/onboard/brand.js +134 -0
- package/dist/onboard/caps.d.ts +90 -0
- package/dist/onboard/caps.js +232 -0
- package/dist/onboard/encode.d.ts +23 -0
- package/dist/onboard/encode.js +235 -0
- package/dist/onboard/eye.d.ts +103 -0
- package/dist/onboard/eye.js +147 -0
- package/dist/onboard/index.d.ts +61 -0
- package/dist/onboard/index.js +562 -0
- package/dist/onboard/mcp.d.ts +35 -0
- package/dist/onboard/mcp.js +118 -0
- package/dist/onboard/raster.d.ts +43 -0
- package/dist/onboard/raster.js +253 -0
- package/dist/onboard/sound.d.ts +2 -0
- package/dist/onboard/sound.js +54 -0
- package/dist/onboard/state.d.ts +24 -0
- package/dist/onboard/state.js +48 -0
- package/dist/onboard/term.d.ts +69 -0
- package/dist/onboard/term.js +292 -0
- package/dist/onboard/ui.d.ts +97 -0
- package/dist/onboard/ui.js +615 -0
- package/dist/orgs.d.ts +16 -0
- package/dist/orgs.js +89 -0
- package/dist/products.d.ts +6 -2
- package/dist/products.js +238 -12
- package/dist/relays-commands/certificates.d.ts +3 -0
- package/dist/relays-commands/certificates.js +20 -0
- package/dist/relays-commands/domains.d.ts +3 -0
- package/dist/relays-commands/domains.js +23 -0
- package/dist/relays-commands/index.d.ts +4 -0
- package/dist/relays-commands/index.js +10 -0
- package/dist/relays-commands/monitoring.d.ts +3 -0
- package/dist/relays-commands/monitoring.js +41 -0
- package/dist/relays-commands/telemetry.d.ts +3 -0
- package/dist/relays-commands/telemetry.js +28 -0
- package/dist/relays-commands/zones.d.ts +5 -0
- package/dist/relays-commands/zones.js +9 -0
- package/dist/runner.js +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,9 +3,36 @@
|
|
|
3
3
|
`genesis`, the OneOS command line. It works against the Genesis API at `https://os.one.ml/api`.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
npx -y @onesi/genesis-cli@0.
|
|
6
|
+
npx -y @onesi/genesis-cli@0.2.0 --help
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
## First run
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npx -y @onesi/genesis-cli # or: npm i -g @onesi/genesis-cli, then genesis
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
On a terminal, the first `genesis` opens `genesis setup`: six steps, about a minute, each skippable and resumable.
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
1. **Sign in with One**, in this browser or with a code on any device.
|
|
20
|
+
2. **Organization, project and environment**, picked from your own (Sandbox).
|
|
21
|
+
3. **An API key** for CI and other machines, only if you want one: read-only, your access, or chosen scopes; shown once or kept in Blinds.
|
|
22
|
+
4. **Your agents**: the Genesis MCP server added to Claude Code, Claude Desktop, Cursor, VS Code, Windsurf or Codex. You see every file or command first; JSON files keep their other servers and a `.genesis-backup` copy.
|
|
23
|
+
5. **A first live call**: every service's state and latency from the gateway.
|
|
24
|
+
6. **What to do next.**
|
|
25
|
+
|
|
26
|
+
Enter takes the default, `s` skips, Ctrl-C pauses. `genesis setup` picks up at the first open step, `genesis setup <step>` redoes one (`signin`, `context`, `key`, `mcp`, `try`, `next`), and `genesis setup --restart` starts over. `genesis setup --json` reports progress for scripts. Progress lives in `~/.config/genesis/setup.json` (no credentials; the login stays in the OS keychain).
|
|
27
|
+
|
|
28
|
+
Setup draws with what your terminal can do, and asks it first: truecolor with edges blended against your real background (or your own ink when the terminal does not say), synchronized output, OSC 8 links, tab progress, and a desktop notification when sign-in finishes while you are in the browser. It is still with `NO_COLOR`, `TERM=dumb`, in CI or a pipe, with `--no-animation` or `GENESIS_REDUCED_MOTION=1`, and when your system asks for reduced motion. `--screen-reader` (or `GENESIS_SCREEN_READER=1`, or VoiceOver running) gives linear output with numbered choices.
|
|
29
|
+
|
|
30
|
+
| Signing in with a code | Still, with `NO_COLOR` and `--no-animation` |
|
|
31
|
+
|---|---|
|
|
32
|
+
|  |  |
|
|
33
|
+
|
|
34
|
+
The recordings were made with `--screencast` against the live services; `docs/media/genesis-setup/*.tape` are the [VHS](https://github.com/charmbracelet/vhs) scripts. `--sound` adds a soft chime. `--screencast` (or `GENESIS_SCREENCAST=1`) masks account names, emails and IDs, for recording demos. Only `genesis setup` and a bare `genesis` load any of this; other commands start exactly as fast as before.
|
|
35
|
+
|
|
9
36
|
## Sign in
|
|
10
37
|
|
|
11
38
|
```sh
|
|
@@ -27,7 +54,7 @@ genesis status # live state of each service
|
|
|
27
54
|
genesis hives projects list
|
|
28
55
|
genesis hives projects create my-app --kind worker
|
|
29
56
|
genesis hives projects delete prj_...
|
|
30
|
-
genesis --json silos
|
|
57
|
+
genesis --json silos instances list # machine-readable output; errors include code, required_scope and request_id
|
|
31
58
|
```
|
|
32
59
|
|
|
33
60
|
`genesis <service> --help` lists every command for a service. `--base-url` or `GENESIS_BASE_URL` overrides the gateway, and `https://console.life.one/api` is an alias. Requires Node.js 20.19 or later.
|
package/dist/beacons.js
CHANGED
|
@@ -15,6 +15,26 @@ export function registerBeacons(program, client, emit) {
|
|
|
15
15
|
const result = await (await client()).beacons.fleets.peers(id);
|
|
16
16
|
emit(result, () => JSON.stringify(result, null, 2));
|
|
17
17
|
});
|
|
18
|
+
fleets.command('get <fleet-id>').description('Read one fleet in the current authorized scope').action(async (id) => {
|
|
19
|
+
const result = await (await client()).beacons.fleets.get(id);
|
|
20
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
21
|
+
});
|
|
22
|
+
fleets.command('dns <fleet-id>').description('Mesh DNS map: one A record per live peer').action(async (id) => {
|
|
23
|
+
const result = await (await client()).beacons.fleets.dns(id);
|
|
24
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
25
|
+
});
|
|
26
|
+
fleets.command('discovery <fleet-id>').description("Mesh discovery map: live peers' public keys, DNS and relay map").action(async (id) => {
|
|
27
|
+
const result = await (await client()).beacons.fleets.discovery(id);
|
|
28
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
29
|
+
});
|
|
30
|
+
fleets.command('client-profile <fleet-id> <platform>').description('beacons-agent profile: linux, macos, windows, ios or android').action(async (id, platform) => {
|
|
31
|
+
const result = await (await client()).beacons.fleets.clientProfile(id, platform);
|
|
32
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
33
|
+
});
|
|
34
|
+
fleets.command('wireguard-config <fleet-id> <peer-id>').description("One peer's WireGuard config: other peers' public keys, never a private key").action(async (id, peer) => {
|
|
35
|
+
const result = await (await client()).beacons.fleets.wireguardConfig(id, peer);
|
|
36
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
37
|
+
});
|
|
18
38
|
const policy = fleets.command('policy').description('Future enrollment admission; does not grant permissions or revoke devices');
|
|
19
39
|
policy.command('get <fleet-id>').action(async (id) => {
|
|
20
40
|
const result = await (await client()).beacons.fleets.policy.get(id);
|
|
@@ -33,6 +53,54 @@ export function registerBeacons(program, client, emit) {
|
|
|
33
53
|
});
|
|
34
54
|
emit(result, () => JSON.stringify(result, null, 2));
|
|
35
55
|
});
|
|
56
|
+
const resources = fleets.command('resources').description('Fleet configuration records: gateways, private-gateways, sims (configuration only)');
|
|
57
|
+
const family = (value) => {
|
|
58
|
+
if (!['gateways', 'private-gateways', 'sims'].includes(value))
|
|
59
|
+
throw new Error('Family must be gateways, private-gateways or sims.');
|
|
60
|
+
return value;
|
|
61
|
+
};
|
|
62
|
+
const config = (value) => {
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(value);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new Error('--config must be a JSON object.');
|
|
69
|
+
}
|
|
70
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
71
|
+
throw new Error('--config must be a JSON object.');
|
|
72
|
+
return parsed;
|
|
73
|
+
};
|
|
74
|
+
resources.command('list <fleet-id> <family>').description('List the fleet\'s records of one family').action(async (id, kind) => {
|
|
75
|
+
const result = await (await client()).beacons.fleets.resources.list(id, family(kind));
|
|
76
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
77
|
+
});
|
|
78
|
+
resources.command('create <fleet-id> <family>').description('Create one record; not idempotent')
|
|
79
|
+
.requiredOption('--name <name>', 'Record name, at most 128 UTF-8 bytes')
|
|
80
|
+
.requiredOption('--status <status>', 'active, inactive, disabled or pending')
|
|
81
|
+
.option('--config <json>', 'Family config object, e.g. \'{"type":"relay","routes":["10.0.0.0/24"]}\'', '{}')
|
|
82
|
+
.action(async (id, kind, opts) => {
|
|
83
|
+
const result = await (await client()).beacons.fleets.resources.create(id, family(kind), { name: opts.name, status: opts.status, config: config(opts.config) });
|
|
84
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
85
|
+
});
|
|
86
|
+
resources.command('update <fleet-id> <family> <resource-id>').description('Replace the given fields of one record')
|
|
87
|
+
.option('--name <name>', 'New name').option('--status <status>', 'active, inactive, disabled or pending')
|
|
88
|
+
.option('--config <json>', 'Replacement family config object')
|
|
89
|
+
.action(async (id, kind, resourceId, opts) => {
|
|
90
|
+
if (opts.name === undefined && opts.status === undefined && opts.config === undefined)
|
|
91
|
+
throw new Error('Supply --name, --status or --config.');
|
|
92
|
+
const result = await (await client()).beacons.fleets.resources.update(id, family(kind), resourceId, {
|
|
93
|
+
...(opts.name === undefined ? {} : { name: opts.name }), ...(opts.status === undefined ? {} : { status: opts.status }),
|
|
94
|
+
...(opts.config === undefined ? {} : { config: config(opts.config) })
|
|
95
|
+
});
|
|
96
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
97
|
+
});
|
|
98
|
+
resources.command('delete <fleet-id> <family> <resource-id>').description('Delete one configuration record')
|
|
99
|
+
.action(async (id, kind, resourceId) => {
|
|
100
|
+
await (await client()).beacons.fleets.resources.delete(id, family(kind), resourceId);
|
|
101
|
+
const result = { deleted: resourceId };
|
|
102
|
+
emit(result, () => `Deleted ${resourceId}`);
|
|
103
|
+
});
|
|
36
104
|
fleets.command('provision').requiredOption('--request-id <uuid>', 'Stable UUID; reuse with the same name on retry')
|
|
37
105
|
.option('--name <name>', 'Optional fleet name')
|
|
38
106
|
.action(async (opts) => {
|
package/dist/dials.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
export function registerDials(program, dependencies) {
|
|
3
3
|
const dials = program.command('dials').description('Dials: phone numbers and calls for agents')
|
|
4
|
-
.
|
|
5
|
-
.
|
|
6
|
-
.
|
|
4
|
+
.option('--workspace <id>', 'workspace (default: an API key\'s bound workspace)')
|
|
5
|
+
.option('--project <id>', 'project (default: an API key\'s bound project)')
|
|
6
|
+
.option('--environment <id>', 'environment (default: an API key\'s bound environment)');
|
|
7
7
|
const context = async () => {
|
|
8
8
|
const client = await dependencies.client();
|
|
9
9
|
const options = dials.opts();
|
|
10
|
-
|
|
10
|
+
// An org API key carries a signed project/workspace/environment binding that Dials fills in; a flag that differs from it is refused.
|
|
11
|
+
const scope = { organizationId: await dependencies.orgId(client),
|
|
12
|
+
...(options.workspace ? { workspaceId: options.workspace } : {}), ...(options.project ? { projectId: options.project } : {}),
|
|
13
|
+
...(options.environment ? { environmentId: options.environment } : {}) };
|
|
11
14
|
return { client, scope };
|
|
12
15
|
};
|
|
13
16
|
const emit = (data) => dependencies.emit(data, () => JSON.stringify(data, null, 2));
|
package/dist/fleets.js
CHANGED
|
@@ -39,6 +39,34 @@ export function registerFleets(program, client, emit, keyInput = readFoundryKey)
|
|
|
39
39
|
});
|
|
40
40
|
agents.command('stop <agent_id>').action(async (id) => emit(await (await client()).fleets.agents.stop(id)));
|
|
41
41
|
agents.command('recover <agent_id>').action(async (id) => emit(await (await client()).fleets.agents.recover(id)));
|
|
42
|
+
agents.command('update <agent_id>').description('Change name, model, instructions or memory (memory needs a stopped agent)')
|
|
43
|
+
.option('--name <name>').option('--model <model>').option('--clear-model', 'use the Foundry default model')
|
|
44
|
+
.option('--instructions <text>').option('--clear-instructions', 'remove the instructions')
|
|
45
|
+
.option('--memory <provider>', 'memory provider (minds, files or custom)').option('--namespace <namespace>').option('--endpoint <url>')
|
|
46
|
+
.action(async (id, options) => {
|
|
47
|
+
if (options.model !== undefined && options.clearModel)
|
|
48
|
+
throw new Error('Use --model or --clear-model, not both.');
|
|
49
|
+
if (options.instructions !== undefined && options.clearInstructions)
|
|
50
|
+
throw new Error('Use --instructions or --clear-instructions, not both.');
|
|
51
|
+
if ((options.namespace !== undefined || options.endpoint !== undefined) && options.memory === undefined)
|
|
52
|
+
throw new Error('--namespace and --endpoint need --memory.');
|
|
53
|
+
if (options.memory !== undefined && !['minds', 'files', 'custom'].includes(options.memory))
|
|
54
|
+
throw new Error('Memory provider must be minds, files, or custom.');
|
|
55
|
+
if (options.memory === 'custom' && !options.endpoint)
|
|
56
|
+
throw new Error('Custom memory requires --endpoint.');
|
|
57
|
+
const body = {
|
|
58
|
+
...(options.name !== undefined ? { name: options.name } : {}),
|
|
59
|
+
...(options.clearModel ? { model: null } : options.model !== undefined ? { model: options.model } : {}),
|
|
60
|
+
...(options.clearInstructions ? { instructions: null } : options.instructions !== undefined ? { instructions: options.instructions } : {}),
|
|
61
|
+
...(options.memory !== undefined ? { memory: { provider: options.memory, ...(options.namespace ? { namespace: options.namespace } : {}), ...(options.endpoint ? { endpoint: options.endpoint } : {}) } } : {}),
|
|
62
|
+
};
|
|
63
|
+
if (!Object.keys(body).length)
|
|
64
|
+
throw new Error('Provide at least one of --name, --model, --clear-model, --instructions, --clear-instructions or --memory.');
|
|
65
|
+
emit(await (await client()).fleets.agents.update(id, body));
|
|
66
|
+
});
|
|
67
|
+
agents.command('delete <agent_id>').description('Delete a stopped agent and its log').action(async (id) => emit(await (await client()).fleets.agents.delete(id)));
|
|
68
|
+
agents.command('logs <agent_id>').description('Read the agent operation log, oldest first').option('--after <seq>', 'next_after from the previous page').option('--limit <n>', '1–200 entries')
|
|
69
|
+
.action(async (id, options) => emit(await (await client()).fleets.agents.logs(id, { ...(options.after !== undefined ? { after: Number(options.after) } : {}), ...(options.limit !== undefined ? { limit: Number(options.limit) } : {}) })));
|
|
42
70
|
const instances = fleets.command('instances').description('Actual admitted Henry Host status');
|
|
43
71
|
instances.command('list').option('--limit <number>').option('--after <instance_id>').action(async (options) => {
|
|
44
72
|
emit(await (await client()).fleets.instances.list({ ...(options.limit ? { limit: Number(options.limit) } : {}), ...(options.after ? { after: options.after } : {}) }));
|
|
@@ -69,4 +97,6 @@ export function registerFleets(program, client, emit, keyInput = readFoundryKey)
|
|
|
69
97
|
tasks.command('get <task_id>').action(async (id) => emit(await (await client()).fleets.tasks.get(id)));
|
|
70
98
|
tasks.command('cancel <task_id>').action(async (id) => emit(await (await client()).fleets.tasks.cancel(id)));
|
|
71
99
|
tasks.command('receipt <task_id>').action(async (id) => emit(await (await client()).fleets.tasks.receipt(id)));
|
|
100
|
+
tasks.command('logs <task_id>').description('Read the task lifecycle and output log').option('--after <seq>', 'next_after from the previous read')
|
|
101
|
+
.action(async (id, options) => emit(await (await client()).fleets.tasks.logs(id, options.after !== undefined ? { after: Number(options.after) } : {})));
|
|
72
102
|
}
|
package/dist/flocks.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import type { Genesis } from '@onesi/genesis';
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function scrub(value: unknown): unknown;
|
|
4
|
+
/** A reference to a key the org already holds in Blinds or Secret Manager (never the key itself). */
|
|
5
|
+
export declare function checkSecretRef(ref: string): string;
|
|
6
|
+
/** One line from stdin. On a terminal the keystrokes are read raw and never echoed; piped input is read to its end. */
|
|
7
|
+
export declare function readPastedCode(): Promise<string>;
|
|
8
|
+
export declare function checkCode(raw: string): string;
|
|
9
|
+
export declare function registerFlocks(program: Command, client: () => Promise<Genesis>, emit: (data: unknown, pretty: () => string) => void, env?: Record<string, string | undefined>, readCode?: () => Promise<string>, isJson?: () => boolean): void;
|
package/dist/flocks.js
CHANGED
|
@@ -1,5 +1,82 @@
|
|
|
1
|
+
import { Option } from 'commander';
|
|
1
2
|
import { readFunctionJson } from './function-input.js';
|
|
2
|
-
|
|
3
|
+
import { safe, table } from './output.js';
|
|
4
|
+
/** Keys that must never reach a terminal or log, even if a server one day returned them. */
|
|
5
|
+
const SECRET_KEY = /^(?:credential_ref|sealed|envelope|ciphertext|enc|ct|token|access_token|refresh_token|secret|password|api_key|api_key_secret_ref|key_material)$/i;
|
|
6
|
+
export function scrub(value) {
|
|
7
|
+
if (Array.isArray(value))
|
|
8
|
+
return value.map(scrub);
|
|
9
|
+
if (value && typeof value === 'object')
|
|
10
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, SECRET_KEY.test(k) ? '[redacted]' : scrub(v)]));
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
/** A reference to a key the org already holds in Blinds or Secret Manager (never the key itself). */
|
|
14
|
+
export function checkSecretRef(ref) {
|
|
15
|
+
// A Blinds variable name in your org, written NAME or blinds:NAME. Secret Manager names are not supported (Blinds answers 422).
|
|
16
|
+
if (!/^(?:blinds:)?[A-Za-z][A-Za-z0-9_]{0,127}$/.test(ref) || /^(?:sk_|AIza|ghp_|xox[a-z]_)/i.test(ref) || ref.length >= 40)
|
|
17
|
+
throw new Error('--api-key-secret-ref takes the name of a Blinds variable your org already holds (NAME or blinds:NAME), never the key itself.');
|
|
18
|
+
return ref;
|
|
19
|
+
}
|
|
20
|
+
/** One line from stdin. On a terminal the keystrokes are read raw and never echoed; piped input is read to its end. */
|
|
21
|
+
export async function readPastedCode() {
|
|
22
|
+
const stdin = process.stdin;
|
|
23
|
+
let text = '';
|
|
24
|
+
if (stdin.isTTY) {
|
|
25
|
+
process.stderr.write('Paste the authorization code (input hidden), then press Enter: ');
|
|
26
|
+
stdin.setRawMode(true);
|
|
27
|
+
stdin.resume();
|
|
28
|
+
stdin.setEncoding('utf8');
|
|
29
|
+
await new Promise((resolve, reject) => {
|
|
30
|
+
const onData = (chunk) => {
|
|
31
|
+
for (const ch of chunk) {
|
|
32
|
+
if (ch === '\r' || ch === '\n') {
|
|
33
|
+
done();
|
|
34
|
+
return resolve();
|
|
35
|
+
}
|
|
36
|
+
if (ch === '\u0003') {
|
|
37
|
+
done();
|
|
38
|
+
return reject(new Error('Cancelled.'));
|
|
39
|
+
}
|
|
40
|
+
if (ch === '\u007f' || ch === '\b')
|
|
41
|
+
text = text.slice(0, -1);
|
|
42
|
+
else if (ch >= ' ')
|
|
43
|
+
text += ch;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const done = () => { stdin.off('data', onData); stdin.setRawMode(false); stdin.pause(); process.stderr.write('\n'); };
|
|
47
|
+
stdin.on('data', onData);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
for await (const chunk of stdin) {
|
|
52
|
+
text += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
|
|
53
|
+
if (text.length > 8192)
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return checkCode(text);
|
|
58
|
+
}
|
|
59
|
+
export function checkCode(raw) {
|
|
60
|
+
const code = raw.split(/\r?\n/)[0].trim();
|
|
61
|
+
if (!code || code.length > 512 || /[\u0000-\u001f\u007f]/.test(code))
|
|
62
|
+
throw new Error('Expected the authorization code on one line (at most 512 characters).');
|
|
63
|
+
return code;
|
|
64
|
+
}
|
|
65
|
+
const JOB_WAIT_DEFAULT_MS = 600_000;
|
|
66
|
+
const parseLabel = (value, previous = {}) => {
|
|
67
|
+
const i = value.indexOf('=');
|
|
68
|
+
if (i < 1)
|
|
69
|
+
throw new Error('--label takes key=value.');
|
|
70
|
+
return { ...previous, [value.slice(0, i)]: value.slice(i + 1) };
|
|
71
|
+
};
|
|
72
|
+
const bounded = (value, name, min, max) => {
|
|
73
|
+
if (value === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
if (!/^\d+$/.test(value) || Number(value) < min || Number(value) > max)
|
|
76
|
+
throw new Error(`${name} must be an integer from ${min} to ${max}.`);
|
|
77
|
+
return Number(value);
|
|
78
|
+
};
|
|
79
|
+
export function registerFlocks(program, client, emit, env = process.env, readCode = readPastedCode, isJson = () => false) {
|
|
3
80
|
const flocks = program.command('flocks').description('Flocks: your machines and cloud instances with a harness installed (nodes, harnesses, agents and runs)');
|
|
4
81
|
const secret = (name) => {
|
|
5
82
|
const value = env[name];
|
|
@@ -7,10 +84,12 @@ export function registerFlocks(program, client, emit, env = process.env) {
|
|
|
7
84
|
throw new Error(`Set ${name} in the environment; do not pass Flocks credentials as command-line arguments.`);
|
|
8
85
|
return value;
|
|
9
86
|
};
|
|
10
|
-
const show = async (run) => {
|
|
87
|
+
const show = async (run, pretty) => {
|
|
11
88
|
const data = await run(await client());
|
|
12
|
-
emit(data, () => JSON.stringify(data, null, 2));
|
|
89
|
+
emit(data, () => (pretty ? pretty(data) : JSON.stringify(data, null, 2)));
|
|
13
90
|
};
|
|
91
|
+
// Account, grant, instance and job commands redact secret-looking fields; enrollment commands (join-token) must print theirs.
|
|
92
|
+
const showSafe = (run, pretty) => show(async (g) => scrub(await run(g)), pretty);
|
|
14
93
|
flocks.command('readiness').description('Check whether node enrollment is currently available').action(() => show(g => g.flocks.readiness.get()));
|
|
15
94
|
flocks.command('nodes').description('List your org nodes').action(() => show(g => g.flocks.nodes.list()));
|
|
16
95
|
const paging = (command) => command.option('--limit <n>', 'page size, 1–100').option('--cursor <cursor>', 'next_cursor from the previous page');
|
|
@@ -34,6 +113,7 @@ export function registerFlocks(program, client, emit, env = process.env) {
|
|
|
34
113
|
agents.command('cancel <agent_id>').description('Cancel the canonical execution admitted under an idempotency key')
|
|
35
114
|
.requiredOption('--idempotency-key <key>', 'the key the execution was admitted with')
|
|
36
115
|
.action((id, opts) => show(g => g.flocks.agents.cancel(id, opts.idempotencyKey)));
|
|
116
|
+
flocks.command('activity').description('Your recent agent sessions and events (needs a signed-in session, not an API key)').action(() => show(g => g.flocks.activity.get()));
|
|
37
117
|
const runs = flocks.command('runs').description('Recorded agent runs');
|
|
38
118
|
paging(runs.command('list').description('List runs').option('--agent-id <id>', 'only runs of this agent'))
|
|
39
119
|
.action((opts) => show(g => g.flocks.runs.list(query(opts))));
|
|
@@ -69,4 +149,178 @@ export function registerFlocks(program, client, emit, env = process.env) {
|
|
|
69
149
|
throw new Error('Action must be prepare, commit or cancel');
|
|
70
150
|
return show(g => g.flocks.nodes.rotateCredential(id, { action: action, node_nonce: secret('FLOCKS_NODE_NONCE'), operation_nonce: secret('FLOCKS_ROTATION_NONCE') }));
|
|
71
151
|
});
|
|
152
|
+
registerAccounts(flocks, client, showSafe, emit, env, readCode, isJson);
|
|
153
|
+
}
|
|
154
|
+
const ago = (t) => (typeof t === 'number' ? new Date(t * (t < 1e12 ? 1000 : 1)).toISOString().slice(0, 19) + 'Z' : '');
|
|
155
|
+
const items = (data) => (Array.isArray(data) ? data : Array.isArray(data?.items) ? data.items : []);
|
|
156
|
+
const accountRows = (data) => table(items(data).map(a => ({ ...a, owner: a.granted ? `${a.owner_org} (granted)` : 'you' })), ['id', 'provider', 'label', 'auth_kind', 'identity', 'status', 'owner']);
|
|
157
|
+
const jobText = (job) => {
|
|
158
|
+
const t = job.totals ?? {};
|
|
159
|
+
const head = `${safe(job.id)} ${safe(job.kind)} ${safe(job.state)}` + (job.account_id ? ` account ${safe(job.account_id)}` : '') + ` succeeded ${t.succeeded ?? 0}/${(job.items ?? []).length}`
|
|
160
|
+
+ (t.failed ? `, failed ${t.failed}` : '') + (t.timed_out ? `, timed out ${t.timed_out}` : '') + (t.rolled_back ? `, rolled back ${t.rolled_back}` : '') + (t.unsupported ? `, unsupported ${t.unsupported}` : '');
|
|
161
|
+
const rows = (job.items ?? []).map((i) => ({ ...i, login: undefined }));
|
|
162
|
+
const logins = (job.items ?? []).filter((i) => i.login).map((i) => i.login.url && i.login.code
|
|
163
|
+
? `Open ${safe(i.login.url)} and enter code ${safe(i.login.code)} (instance ${safe(i.instance_id)})`
|
|
164
|
+
: i.login.url
|
|
165
|
+
? `Open ${safe(i.login.url)}, sign in, then send the code it shows with: genesis flocks jobs login-input ${safe(job.id)} --instance ${safe(i.instance_id)}`
|
|
166
|
+
: `Waiting for the sign-in code from instance ${safe(i.instance_id)}...`);
|
|
167
|
+
return [head, rows.length ? table(rows, ['instance_id', 'state', 'previous_account_id', 'identity', 'error_code']) : '', ...logins].filter(Boolean).join('\n');
|
|
168
|
+
};
|
|
169
|
+
const nodeRows = (data) => table(items(data).map(n => ({
|
|
170
|
+
id: n.id, nickname: n.nickname, state: n.state, flock: n.labels?.flock ?? '',
|
|
171
|
+
labels: Object.entries(n.labels ?? {}).filter(([k]) => k !== 'flock').map(([k, v]) => `${k}=${v}`),
|
|
172
|
+
accounts: (n.accounts ?? []).map((a) => `${a.provider}:${a.identity ?? a.account_id ?? 'none'}`),
|
|
173
|
+
})), ['id', 'nickname', 'state', 'flock', 'labels', 'accounts']);
|
|
174
|
+
function registerAccounts(flocks, client, show, emit, env, readCode, isJson) {
|
|
175
|
+
const interval = Number(env.FLOCKS_POLL_INTERVAL_MS) > 0 ? Number(env.FLOCKS_POLL_INTERVAL_MS) : 2000;
|
|
176
|
+
const finish = (job) => {
|
|
177
|
+
if (job.state === 'partial' || job.state === 'failed')
|
|
178
|
+
throw new Error(`Job ${safe(job.id)} finished ${safe(job.state)}: ${job.totals?.failed ?? 0} failed, ${job.totals?.timed_out ?? 0} timed out, ${job.totals?.rolled_back ?? 0} rolled back. Exit status 1.`);
|
|
179
|
+
};
|
|
180
|
+
/** Show the job the service accepted; with --wait, follow it to the end and fail the command when any instance failed. */
|
|
181
|
+
const showJob = async (start, wait) => {
|
|
182
|
+
const g = await client();
|
|
183
|
+
let job = scrub(await start(g));
|
|
184
|
+
if (wait && job.state === 'running')
|
|
185
|
+
job = scrub(await g.flocks.jobs.wait(job.id, { intervalMs: interval, timeoutMs: JOB_WAIT_DEFAULT_MS }));
|
|
186
|
+
emit(job, () => jobText(job));
|
|
187
|
+
if (wait)
|
|
188
|
+
finish(job);
|
|
189
|
+
};
|
|
190
|
+
const withJobId = async (g, r, wait) => {
|
|
191
|
+
// delete and revoke answer { account|grant, job_id }
|
|
192
|
+
if (!wait || !r?.job_id)
|
|
193
|
+
return r;
|
|
194
|
+
const job = await g.flocks.jobs.wait(r.job_id, { intervalMs: interval, timeoutMs: JOB_WAIT_DEFAULT_MS });
|
|
195
|
+
return { ...r, job };
|
|
196
|
+
};
|
|
197
|
+
flocks.command('providers').description('Inference providers, the login kinds each supports, and which plans fit sharing one login').action(() => show(g => g.flocks.providers.list(), data => items(data).map(p => `${safe(p.id)} ${safe(p.display_name)} (${(p.auth_kinds ?? []).map(safe).join(', ')})\n ${safe(p.plan_guidance)}`).join('\n')));
|
|
198
|
+
const accounts = flocks.command('accounts').description('Inference accounts: one login (or API key) many instances can run under');
|
|
199
|
+
accounts.command('list').description('Accounts your org owns plus accounts granted to it')
|
|
200
|
+
.option('--provider <id>', 'only this provider').option('--limit <n>', 'page size, 1-100').option('--cursor <cursor>', 'next_cursor from the previous page')
|
|
201
|
+
.action((o) => show(g => g.flocks.accounts.list({
|
|
202
|
+
...(o.provider ? { provider: o.provider } : {}), ...(o.limit ? { limit: bounded(o.limit, '--limit', 1, 100) } : {}), ...(o.cursor ? { cursor: o.cursor } : {}),
|
|
203
|
+
}), accountRows));
|
|
204
|
+
accounts.command('add <provider> <label>').description('Add an account. subscription_oauth accounts are signed in afterwards with `accounts login`; api_key accounts take a secret reference only')
|
|
205
|
+
.option('--auth-kind <kind>', 'subscription_oauth or api_key', 'subscription_oauth')
|
|
206
|
+
.option('--api-key-secret-ref <ref>', 'api_key accounts: the name of a key your org already holds in Blinds (NAME or blinds:NAME)')
|
|
207
|
+
.addOption(new Option('--api-key <value>').hideHelp())
|
|
208
|
+
.action((provider, label, o) => {
|
|
209
|
+
if (o.apiKey !== undefined)
|
|
210
|
+
throw new Error('Raw API keys are never accepted on the command line. Store the key in Blinds or Secret Manager and pass --api-key-secret-ref.');
|
|
211
|
+
if (!['subscription_oauth', 'api_key'].includes(o.authKind))
|
|
212
|
+
throw new Error('--auth-kind must be subscription_oauth or api_key.');
|
|
213
|
+
if (o.authKind === 'api_key' && !o.apiKeySecretRef)
|
|
214
|
+
throw new Error('api_key accounts need --api-key-secret-ref (the name of a Blinds variable your org already holds).');
|
|
215
|
+
if (o.authKind !== 'api_key' && o.apiKeySecretRef)
|
|
216
|
+
throw new Error('--api-key-secret-ref is only for --auth-kind api_key.');
|
|
217
|
+
const ref = o.apiKeySecretRef === undefined ? undefined : checkSecretRef(o.apiKeySecretRef);
|
|
218
|
+
return show(g => g.flocks.accounts.create({ provider, label, auth_kind: o.authKind, ...(ref ? { api_key_secret_ref: ref } : {}) }), a => accountRows([a]));
|
|
219
|
+
});
|
|
220
|
+
accounts.command('get <account_id>').description('One account, with the number of instances bound to it')
|
|
221
|
+
.action((id) => show(g => g.flocks.accounts.get(id), a => accountRows([a]) + `\nbound instances: ${a.bound_instances ?? 0}`));
|
|
222
|
+
accounts.command('login <account_id>').description('Sign an account in from one of your instances; prints the device url and code (only you receive them)')
|
|
223
|
+
.requiredOption('--instance <id>', 'the instance that runs the provider login')
|
|
224
|
+
.option('--timeout <seconds>', 'how long to wait for the code', '60')
|
|
225
|
+
.action(async (id, o) => {
|
|
226
|
+
const g = await client();
|
|
227
|
+
const seconds = bounded(o.timeout, '--timeout', 0, 600);
|
|
228
|
+
let job = scrub(await g.flocks.accounts.login(id, o.instance));
|
|
229
|
+
const deadline = Date.now() + seconds * 1000;
|
|
230
|
+
const awaiting = (j) => (j.items ?? []).find((i) => i.login?.url && !i.login.code && i.state !== 'succeeded' && i.state !== 'failed');
|
|
231
|
+
while (job.state === 'running' && !(job.items ?? []).some((i) => i.login?.url) && Date.now() < deadline) {
|
|
232
|
+
await new Promise(r => setTimeout(r, interval));
|
|
233
|
+
job = scrub(await g.flocks.jobs.get(job.id));
|
|
234
|
+
}
|
|
235
|
+
const waiting = job.state === 'running' ? awaiting(job) : undefined;
|
|
236
|
+
if (waiting) {
|
|
237
|
+
// The provider printed a URL and waits for the pasted code: show the URL, then read the code without echoing it.
|
|
238
|
+
if (!isJson())
|
|
239
|
+
emit(job, () => jobText(job)); // --json prints one document, after the code is sent
|
|
240
|
+
const code = await readCode();
|
|
241
|
+
await g.flocks.jobs.submitLoginInput(job.id, waiting.instance_id, code);
|
|
242
|
+
job = scrub(await g.flocks.jobs.get(job.id));
|
|
243
|
+
emit(job, () => `Code sent.\n` + jobText(job) + (job.state === 'running' ? `\nFinish with: genesis flocks jobs get ${safe(job.id)} --wait` : ''));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
emit(job, () => jobText(job) + (job.state === 'running' ? `\nFinish with: genesis flocks jobs get ${safe(job.id)} --wait` : ''));
|
|
247
|
+
});
|
|
248
|
+
accounts.command('remove <account_id>').description('Sign the account out of every bound instance, then revoke its credential')
|
|
249
|
+
.option('--wait', 'follow the logout job to the end')
|
|
250
|
+
.action((id, o) => show(async (g) => withJobId(g, await g.flocks.accounts.delete(id), o.wait), r => `account ${safe(r.account?.id)} ${safe(r.account?.status)}; logout job ${safe(r.job_id)}` + (r.job ? '\n' + jobText(r.job) : '')));
|
|
251
|
+
const grants = flocks.command('grants').description('Let another org bind its instances to one of your accounts');
|
|
252
|
+
grants.command('list').description('Grants you issued or received')
|
|
253
|
+
.option('--direction <dir>', 'issued or received', 'issued').option('--account <id>', 'only this account')
|
|
254
|
+
.action((o) => {
|
|
255
|
+
if (!['issued', 'received'].includes(o.direction))
|
|
256
|
+
throw new Error('--direction must be issued or received.');
|
|
257
|
+
return show(g => g.flocks.grants.list({ direction: o.direction, ...(o.account ? { account_id: o.account } : {}) }), d => table(items(d).map(x => ({ ...x, created: ago(x.created_at), revoked: ago(x.revoked_at) })), ['id', 'account_id', 'owner_org', 'grantee_org', 'status', 'note', 'created', 'revoked']));
|
|
258
|
+
});
|
|
259
|
+
grants.command('add <account_id> <grantee_org>').description('Grant an account you own to another org')
|
|
260
|
+
.option('--note <text>')
|
|
261
|
+
.action((id, org, o) => show(g => g.flocks.grants.create(id, { grantee_org_id: org, ...(o.note ? { note: o.note } : {}) })));
|
|
262
|
+
grants.command('revoke <grant_id>').description('Revoke a grant: new runs stop at once and grantee instances are signed out and wiped')
|
|
263
|
+
.option('--wait', 'follow the logout job to the end')
|
|
264
|
+
.action((id, o) => show(async (g) => withJobId(g, await g.flocks.grants.revoke(id), o.wait), r => `grant ${safe(r.grant?.id)} ${safe(r.grant?.status)}; logout job ${safe(r.job_id)}` + (r.job ? '\n' + jobText(r.job) : '')));
|
|
265
|
+
const instances = flocks.command('instances').description('Your instances and the inference account each runs under');
|
|
266
|
+
instances.command('list').description('Instances with their flock, labels and active accounts').action(() => show(g => g.flocks.nodes.list(), nodeRows));
|
|
267
|
+
instances.command('label <instance_id> [labels...]').description('Set labels (key=value) on an instance; the key flock groups instances. --remove drops a key')
|
|
268
|
+
.option('--remove <key...>', 'label keys to remove')
|
|
269
|
+
.action((id, pairs, o) => show(async (g) => {
|
|
270
|
+
const nodes = (await g.flocks.nodes.list());
|
|
271
|
+
const current = nodes.find(n => n.id === id);
|
|
272
|
+
if (!current)
|
|
273
|
+
throw new Error(`No instance ${id} in your org.`);
|
|
274
|
+
let labels = { ...(current.labels ?? {}) };
|
|
275
|
+
for (const p of pairs)
|
|
276
|
+
labels = parseLabel(p, labels);
|
|
277
|
+
for (const k of o.remove ?? [])
|
|
278
|
+
delete labels[k];
|
|
279
|
+
return g.flocks.nodes.update(id, { labels });
|
|
280
|
+
}, n => nodeRows([n])));
|
|
281
|
+
instances.command('account <instance_id>').description('Which account is bound to an instance, per provider')
|
|
282
|
+
.action((id) => show(g => g.flocks.instances.getAccount(id), d => table((d.accounts ?? []).map((a) => ({ ...a, updated: ago(a.updated_at) })), ['provider', 'account_id', 'identity', 'auth_state', 'binding_state', 'desired_account_id', 'updated'])));
|
|
283
|
+
const selecting = (command) => command
|
|
284
|
+
.option('--flock <name>', 'every instance in this flock (label flock)').option('--label <k=v>', 'instances carrying this label (repeatable)', parseLabel)
|
|
285
|
+
.option('--from-account <id>', 'instances currently bound to this account')
|
|
286
|
+
.option('--concurrency <n>', 'instances processed at once, 1-20 (bulk)')
|
|
287
|
+
.option('--wait', 'follow the job to the end; exits non-zero if any instance failed');
|
|
288
|
+
const selector = (ids, o) => {
|
|
289
|
+
const sel = { ...(ids.length ? { instance_ids: ids } : {}), ...(o.flock ? { flock: o.flock } : {}), ...(o.label ? { labels: o.label } : {}), ...(o.fromAccount ? { from_account_id: o.fromAccount } : {}) };
|
|
290
|
+
if (!Object.keys(sel).length)
|
|
291
|
+
throw new Error('Name instances, or select with --flock, --label or --from-account.');
|
|
292
|
+
return sel;
|
|
293
|
+
};
|
|
294
|
+
selecting(instances.command('switch-account [instance_ids...]').description('Switch instances to an account: drain, install, verify identity, swap (a failed verify rolls back)')
|
|
295
|
+
.requiredOption('--account <id>', 'the account to switch to').option('--provider <id>', 'provider when the account spans several')
|
|
296
|
+
.option('--force', 'cancel in-flight runs instead of waiting for them').option('--drain-timeout <seconds>', 'wait for in-flight runs, default 120'))
|
|
297
|
+
.action((ids, o) => {
|
|
298
|
+
const drain = bounded(o.drainTimeout, '--drain-timeout', 0, 3600);
|
|
299
|
+
const concurrency = bounded(o.concurrency, '--concurrency', 1, 20);
|
|
300
|
+
const extra = { ...(o.force ? { force: true } : {}), ...(drain === undefined ? {} : { drain_timeout_secs: drain }) };
|
|
301
|
+
const single = ids.length === 1 && !o.flock && !o.label && !o.fromAccount && concurrency === undefined;
|
|
302
|
+
return showJob(g => single
|
|
303
|
+
? g.flocks.instances.switchAccount(ids[0], { account_id: o.account, ...(o.provider ? { provider: o.provider } : {}), ...extra })
|
|
304
|
+
: g.flocks.instances.bulkSwitchAccount({ selector: selector(ids, o), account_id: o.account, ...(concurrency === undefined ? {} : { concurrency }), ...extra }), o.wait);
|
|
305
|
+
});
|
|
306
|
+
selecting(instances.command('logout [instance_ids...]').description('Sign instances out of a provider account (files deleted; the provider login itself is untouched)')
|
|
307
|
+
.requiredOption('--provider <id>', 'the provider to sign out of')
|
|
308
|
+
.option('--force', 'cancel in-flight runs instead of waiting for them'))
|
|
309
|
+
.action((ids, o) => {
|
|
310
|
+
const concurrency = bounded(o.concurrency, '--concurrency', 1, 20);
|
|
311
|
+
const single = ids.length === 1 && !o.flock && !o.label && !o.fromAccount && concurrency === undefined;
|
|
312
|
+
return showJob(g => single
|
|
313
|
+
? g.flocks.instances.logout(ids[0], o.provider, o.force)
|
|
314
|
+
: g.flocks.instances.bulkLogout({ selector: selector(ids, o), provider: o.provider, ...(o.force ? { force: true } : {}), ...(concurrency === undefined ? {} : { concurrency }) }), o.wait);
|
|
315
|
+
});
|
|
316
|
+
const jobs = flocks.command('jobs').description('Asynchronous account jobs (login, switch, sign-out)');
|
|
317
|
+
jobs.command('get <job_id>').description('A job and its per-instance results')
|
|
318
|
+
.option('--wait', 'poll until the job is no longer running; exits non-zero if any instance failed')
|
|
319
|
+
.action((id, o) => showJob(g => g.flocks.jobs.get(id), o.wait));
|
|
320
|
+
jobs.command('login-input <job_id>').description('Send the authorization code a headless sign-in is waiting for; the code is read from stdin, never from arguments')
|
|
321
|
+
.requiredOption('--instance <id>', 'the instance running the sign-in')
|
|
322
|
+
.action(async (id, o) => {
|
|
323
|
+
const code = await readCode();
|
|
324
|
+
await show(g => g.flocks.jobs.submitLoginInput(id, o.instance, code), () => 'Code sent. Follow the job with: genesis flocks jobs get ' + safe(id) + ' --wait');
|
|
325
|
+
});
|
|
72
326
|
}
|
package/dist/henry.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import type { Genesis } from '@onesi/genesis';
|
|
3
|
-
|
|
3
|
+
/** `line` prints one NDJSON line (henry events --follow); `signal` stops a follow. */
|
|
4
|
+
export declare function addHenryCommands(program: Command, client: () => Promise<Genesis>, output: (value: unknown) => void, stream?: {
|
|
5
|
+
line: (text: string) => void;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
}): void;
|
package/dist/henry.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { readFoundryKey } from './fleets.js';
|
|
2
|
-
|
|
2
|
+
const RUN_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
3
|
+
const DELIVERIES = ['steer', 'queue', 'interrupt'];
|
|
4
|
+
/** `line` prints one NDJSON line (henry events --follow); `signal` stops a follow. */
|
|
5
|
+
export function addHenryCommands(program, client, output, stream = { line: text => process.stdout.write(`${text}\n`) }) {
|
|
6
|
+
const number = (value, name) => {
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
10
|
+
throw new Error(`${name} must be a non-negative integer`);
|
|
11
|
+
return Number(value);
|
|
12
|
+
};
|
|
3
13
|
const henry = program.command('henry').description('Henry: agent runtime, live activity and approvals');
|
|
4
14
|
henry.command('config').action(async () => output(await (await client()).henry.config()));
|
|
5
15
|
henry.command('credential').description('Read a Foundry API key from stdin and encrypt it in Genesis KMS')
|
|
@@ -12,6 +22,55 @@ export function addHenryCommands(program, client, output) {
|
|
|
12
22
|
henry.command('run').requiredOption('--model <model>').requiredOption('--task <text>').option('--run-id <uuid>', 'UUIDv4 for concurrent activity polling')
|
|
13
23
|
.action(async (options) => output(await (await client()).henry.runs.create({ model: options.model, task: options.task, ...(options.runId ? { run_id: options.runId } : {}) })));
|
|
14
24
|
henry.command('get <run_id>').action(async (runId) => output(await (await client()).henry.runs.get(runId)));
|
|
25
|
+
henry.command('events <run_id>').description('Page a run\'s activity events, or follow them live with --follow')
|
|
26
|
+
.option('--after-seq <n>', 'return events after this sequence number').option('--limit <n>', 'page size, 1-500')
|
|
27
|
+
.option('--follow', 'stream events as NDJSON lines until the run ends (reconnects every 120 s stream)')
|
|
28
|
+
.action(async (runId, options) => {
|
|
29
|
+
const afterSeq = number(options.afterSeq, '--after-seq'), limit = number(options.limit, '--limit');
|
|
30
|
+
if (options.follow) {
|
|
31
|
+
if (limit !== undefined)
|
|
32
|
+
throw new Error('--limit pages events; --follow streams them');
|
|
33
|
+
const events = (await client()).henry.runs.streamEvents(runId, { ...(afterSeq === undefined ? {} : { afterSeq }), ...(stream.signal ? { signal: stream.signal } : {}) });
|
|
34
|
+
for await (const { event, data } of events)
|
|
35
|
+
stream.line(JSON.stringify({ event, ...data }));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
output(await (await client()).henry.runs.events(runId, { ...(afterSeq === undefined ? {} : { after_seq: afterSeq }), ...(limit === undefined ? {} : { limit }) }));
|
|
39
|
+
});
|
|
40
|
+
henry.command('message <run_id>').description('Send a message into a run: steer, queue (default) or interrupt')
|
|
41
|
+
.requiredOption('--text <text>').option('--delivery <delivery>', 'steer, queue or interrupt')
|
|
42
|
+
.action(async (runId, options) => {
|
|
43
|
+
if (options.delivery !== undefined && !DELIVERIES.includes(options.delivery))
|
|
44
|
+
throw new Error('--delivery must be steer, queue or interrupt');
|
|
45
|
+
output(await (await client()).henry.runs.message(runId, { text: options.text, ...(options.delivery ? { delivery: options.delivery } : {}) }));
|
|
46
|
+
});
|
|
47
|
+
henry.command('fork <run_id>').description('Start a new run from a past turn (0 = the original task; default the latest, which needs --text)')
|
|
48
|
+
.option('--turn <n>', 'turn to fork from').option('--text <text>', 'user message to append').option('--model <model>').option('--run-id <uuid>', 'UUIDv4 for the new run (idempotency key)')
|
|
49
|
+
.action(async (runId, options) => {
|
|
50
|
+
const turn = number(options.turn, '--turn');
|
|
51
|
+
if (options.runId !== undefined && !RUN_ID.test(options.runId))
|
|
52
|
+
throw new Error('--run-id must be a lowercase UUIDv4');
|
|
53
|
+
output(await (await client()).henry.runs.fork(runId, { ...(turn === undefined ? {} : { turn }), ...(options.text === undefined ? {} : { text: options.text }),
|
|
54
|
+
...(options.model === undefined ? {} : { model: options.model }), ...(options.runId === undefined ? {} : { run_id: options.runId }) }));
|
|
55
|
+
});
|
|
56
|
+
henry.command('replay <run_id>').description('Read every event of a run and fold them into turns (each turn\'s inputs and answer)')
|
|
57
|
+
.action(async (runId) => output(await (await client()).henry.runs.replay(runId, stream.signal ? { signal: stream.signal } : {})));
|
|
58
|
+
henry.command('delete <run_id>').description('Delete a finished run (cancel active runs first)').action(async (runId) => output(await (await client()).henry.runs.delete(runId)));
|
|
59
|
+
henry.command('receipt-key').description('Print the current Henry receipt verification key').action(async () => output(await (await client()).henry.receipts.verificationKey()));
|
|
60
|
+
henry.command('blinds-proxy').description('Send an HTTPS request through the organization\'s Blinds machine')
|
|
61
|
+
.requiredOption('--method <method>', 'GET, POST, PUT, PATCH or DELETE').requiredOption('--url <https-url>')
|
|
62
|
+
.option('--header <name:value...>', 'request header, repeatable').option('--body <text>').option('--request-id <id>')
|
|
63
|
+
.action(async (options) => {
|
|
64
|
+
const headers = {};
|
|
65
|
+
for (const entry of options.header ?? []) {
|
|
66
|
+
const at = String(entry).indexOf(':');
|
|
67
|
+
if (at < 1)
|
|
68
|
+
throw new Error('--header must be name:value');
|
|
69
|
+
headers[entry.slice(0, at).trim()] = entry.slice(at + 1).trim();
|
|
70
|
+
}
|
|
71
|
+
output(await (await client()).henry.blinds.proxy({ method: String(options.method).toUpperCase(), url: options.url,
|
|
72
|
+
...(Object.keys(headers).length ? { headers } : {}), ...(options.body === undefined ? {} : { body: options.body }), ...(options.requestId ? { request_id: options.requestId } : {}) }));
|
|
73
|
+
});
|
|
15
74
|
henry.command('command <run_id> <action>').option('--text <text>')
|
|
16
75
|
.action(async (runId, action, options) => {
|
|
17
76
|
if (!['pause', 'resume', 'cancel', 'input'].includes(action))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { type Genesis, type OperationId } from '@onesi/genesis';
|
|
3
|
+
/** `genesis hives <words>` -> Hives operation. Positional arguments are the path parameters, query
|
|
4
|
+
* parameters become options, a JSON body comes from --body-file. Groups that already exist
|
|
5
|
+
* (functions, deployments, environment, domains, logs) are extended, never replaced. */
|
|
6
|
+
export declare const HIVES_COVERAGE_COMMANDS: Record<string, OperationId>;
|
|
7
|
+
/** Register after every other `hives` group so existing groups are found and extended. */
|
|
8
|
+
export declare function registerHivesCoverage(hives: Command, deps: {
|
|
9
|
+
client: () => Promise<Genesis>;
|
|
10
|
+
emit: (data: unknown, pretty: () => string) => void;
|
|
11
|
+
readRequest?: (path: string, limit: number) => Promise<unknown>;
|
|
12
|
+
}): void;
|