@onesi/genesis-cli 0.0.1-stage → 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 +64 -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 +162 -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 +99 -0
- package/dist/fleets.d.ts +4 -0
- package/dist/fleets.js +102 -0
- package/dist/flocks.d.ts +9 -0
- package/dist/flocks.js +326 -0
- package/dist/function-input.d.ts +3 -0
- package/dist/function-input.js +49 -0
- package/dist/henry.d.ts +7 -0
- package/dist/henry.js +85 -0
- package/dist/hives-coverage.d.ts +12 -0
- package/dist/hives-coverage.js +116 -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 +150 -0
- package/dist/hives-operations.d.ts +10 -0
- package/dist/hives-operations.js +63 -0
- package/dist/hives-platform.d.ts +13 -0
- package/dist/hives-platform.js +133 -0
- package/dist/hives-resources.d.ts +9 -0
- package/dist/hives-resources.js +94 -0
- package/dist/iam-directory.d.ts +15 -0
- package/dist/iam-directory.js +71 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +872 -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 +125 -0
- 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/output.d.ts +28 -0
- package/dist/output.js +46 -0
- package/dist/products.d.ts +18 -0
- package/dist/products.js +311 -0
- 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/relays-enrollment.d.ts +3 -0
- package/dist/relays-enrollment.js +7 -0
- package/dist/runner.d.ts +4 -0
- package/dist/runner.js +57 -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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { LIFEID_ISSUER } from './lifeid-native.js';
|
|
2
|
+
import { errorToken } from '@onesi/genesis';
|
|
3
|
+
/** RFC 8628 device grant against One account, for agents and machines without a local browser. */
|
|
4
|
+
export const DEVICE_CLIENT_ID = 'genesis-cli-device';
|
|
5
|
+
export const DEVICE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
6
|
+
const DEVICE_SCOPE = 'openid offline_access email profile';
|
|
7
|
+
const MAX_BODY = 32_768;
|
|
8
|
+
class DeviceLoginError extends Error {
|
|
9
|
+
}
|
|
10
|
+
function object(v) {
|
|
11
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
|
|
12
|
+
}
|
|
13
|
+
function secret(v) {
|
|
14
|
+
return typeof v === 'string' && v.length > 0 && v.length <= 16_384 && !/[\s\x00-\x1f\x7f]/.test(v);
|
|
15
|
+
}
|
|
16
|
+
function claimsOf(token) {
|
|
17
|
+
try {
|
|
18
|
+
const parts = token.split('.');
|
|
19
|
+
if (parts.length !== 3 || !parts.every(p => /^[A-Za-z0-9_-]+$/.test(p)))
|
|
20
|
+
return undefined;
|
|
21
|
+
return object(JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const ISSUER_HOST = new URL(LIFEID_ISSUER).host;
|
|
28
|
+
/** https on exactly the issuer's host (and port), with no userinfo or control characters. */
|
|
29
|
+
function onIssuer(u) {
|
|
30
|
+
if (typeof u !== 'string' || u.length > 2048 || /[\s\x00-\x1f\x7f-\x9f]/.test(u))
|
|
31
|
+
return false;
|
|
32
|
+
try {
|
|
33
|
+
const url = new URL(u);
|
|
34
|
+
return url.protocol === 'https:' && url.host === ISSUER_HOST && url.username === '' && url.password === '' && !/@/.test(u.split(/[/?#]/, 3)[2] ?? '');
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function invalid() { throw new DeviceLoginError('One account returned an invalid device login response. Run `genesis login --device` again.'); }
|
|
41
|
+
/** Form POST to One account: bounded body, no redirects, 15s deadline, and no response echo. */
|
|
42
|
+
async function postForm(c, path, form, signal) {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const abort = () => controller.abort();
|
|
45
|
+
const timer = setTimeout(abort, 15_000);
|
|
46
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
47
|
+
try {
|
|
48
|
+
const response = await c.fetch(`${LIFEID_ISSUER}${path}`, {
|
|
49
|
+
method: 'POST', redirect: 'error', cache: 'no-store', signal: controller.signal,
|
|
50
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
|
|
51
|
+
body: new URLSearchParams(form).toString(),
|
|
52
|
+
});
|
|
53
|
+
if (!/^application\/json(?:;|$)/i.test(response.headers.get('content-type') ?? '')) {
|
|
54
|
+
void response.body?.cancel().catch(() => { });
|
|
55
|
+
throw new DeviceLoginError(`One account request failed (${response.status}).`);
|
|
56
|
+
}
|
|
57
|
+
const reader = response.body?.getReader();
|
|
58
|
+
if (!reader)
|
|
59
|
+
invalid();
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let size = 0;
|
|
62
|
+
for (;;) {
|
|
63
|
+
const part = await reader.read();
|
|
64
|
+
if (part.done)
|
|
65
|
+
break;
|
|
66
|
+
size += part.value.byteLength;
|
|
67
|
+
if (size > MAX_BODY) {
|
|
68
|
+
void reader.cancel().catch(() => { });
|
|
69
|
+
invalid();
|
|
70
|
+
}
|
|
71
|
+
chunks.push(part.value);
|
|
72
|
+
}
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
invalid();
|
|
79
|
+
}
|
|
80
|
+
return { status: response.status, body: object(parsed) ?? invalid() };
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof DeviceLoginError)
|
|
84
|
+
throw error;
|
|
85
|
+
if (signal?.aborted)
|
|
86
|
+
throw new DeviceLoginError('One account device login cancelled.');
|
|
87
|
+
throw new DeviceLoginError('One account transport failed. No automatic retry was made.');
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
signal?.removeEventListener('abort', abort);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export async function startDeviceLogin(c, signal) {
|
|
95
|
+
const { status, body } = await postForm(c, '/v4/oauth/device_authorization', { client_id: DEVICE_CLIENT_ID, scope: DEVICE_SCOPE }, signal);
|
|
96
|
+
if (status !== 200)
|
|
97
|
+
throw new DeviceLoginError(`One account refused the device login (${status}).`);
|
|
98
|
+
const { device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval } = body;
|
|
99
|
+
if (!secret(device_code) || typeof user_code !== 'string' || !/^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$/.test(user_code)
|
|
100
|
+
|| typeof expires_in !== 'number' || !Number.isInteger(expires_in) || expires_in < 1 || expires_in > 3600
|
|
101
|
+
|| typeof interval !== 'number' || !Number.isInteger(interval) || interval < 1 || interval > 60)
|
|
102
|
+
invalid();
|
|
103
|
+
if (!onIssuer(verification_uri) || !onIssuer(verification_uri_complete)) {
|
|
104
|
+
throw new DeviceLoginError(`One account returned a verification URL outside ${ISSUER_HOST}. Refusing to show it; run \`genesis login --device\` again.`);
|
|
105
|
+
}
|
|
106
|
+
return { deviceCode: device_code, userCode: user_code, verificationUri: verification_uri,
|
|
107
|
+
verificationUriComplete: verification_uri_complete, expiresIn: expires_in, interval };
|
|
108
|
+
}
|
|
109
|
+
/** Saved session from an OAuth token response. Authority stays with the One account verifier. */
|
|
110
|
+
export function parseDeviceTokens(body, now = Date.now() / 1000) {
|
|
111
|
+
const access = body.access_token, refresh = body.refresh_token;
|
|
112
|
+
if (body.token_type !== 'Bearer' || !secret(access) || !secret(refresh))
|
|
113
|
+
invalid();
|
|
114
|
+
const claims = claimsOf(access);
|
|
115
|
+
const exp = claims?.exp;
|
|
116
|
+
if (!claims || claims.iss !== LIFEID_ISSUER || claims.kind !== 'human' || typeof claims.sub !== 'string'
|
|
117
|
+
|| typeof claims.sid !== 'string' || typeof exp !== 'number' || exp <= now
|
|
118
|
+
|| claims.cnf !== undefined || claims.cap !== undefined || claims.obo !== undefined)
|
|
119
|
+
invalid();
|
|
120
|
+
const org = typeof claims.org === 'string' ? claims.org : undefined;
|
|
121
|
+
const email = typeof claims.email === 'string' ? claims.email : undefined;
|
|
122
|
+
return { access_token: access, refresh_token: refresh, user: { id: claims.sub, ...(email ? { email } : {}) },
|
|
123
|
+
...(org ? { organization_id: org } : {}), expires_at: exp, client_id: DEVICE_CLIENT_ID,
|
|
124
|
+
auth: { kind: 'lifeid-device', issuer: LIFEID_ISSUER, client_id: DEVICE_CLIENT_ID, principal_id: claims.sub, session_id: claims.sid } };
|
|
125
|
+
}
|
|
126
|
+
/** Poll until approved, denied or expired, honouring `interval` and `slow_down`. */
|
|
127
|
+
export async function completeDeviceLogin(c, start, signal) {
|
|
128
|
+
const sleep = c.sleep ?? ((ms) => new Promise(r => setTimeout(r, ms)));
|
|
129
|
+
const deadline = Date.now() + start.expiresIn * 1000;
|
|
130
|
+
let interval = start.interval;
|
|
131
|
+
for (;;) {
|
|
132
|
+
await sleep(interval * 1000);
|
|
133
|
+
if (signal?.aborted)
|
|
134
|
+
throw new DeviceLoginError('One account device login cancelled.');
|
|
135
|
+
if (Date.now() > deadline)
|
|
136
|
+
throw new DeviceLoginError('The device code expired. Run `genesis login --device` again.');
|
|
137
|
+
const { status, body } = await postForm(c, '/v4/oauth/token', { grant_type: DEVICE_GRANT, client_id: DEVICE_CLIENT_ID, device_code: start.deviceCode }, signal);
|
|
138
|
+
if (status === 200)
|
|
139
|
+
return parseDeviceTokens(body);
|
|
140
|
+
// RFC 8628 string errors from Auth; the envelope form (error.code) is accepted too.
|
|
141
|
+
switch (errorToken(body)) {
|
|
142
|
+
case 'authorization_pending': continue;
|
|
143
|
+
case 'slow_down':
|
|
144
|
+
interval = Math.min(interval + 5, 60);
|
|
145
|
+
continue;
|
|
146
|
+
case 'access_denied': throw new DeviceLoginError('Sign-in was denied on the verification page.');
|
|
147
|
+
case 'expired_token': throw new DeviceLoginError('The device code expired. Run `genesis login --device` again.');
|
|
148
|
+
default: throw new DeviceLoginError(`One account refused the device login (${status}). Run \`genesis login --device\` again.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Rotate a device session through the OAuth refresh grant of the same public client. */
|
|
153
|
+
export async function refreshDeviceLogin(c, saved) {
|
|
154
|
+
if (!secret(saved.refresh_token))
|
|
155
|
+
throw new DeviceLoginError('Session expired. Run `genesis login --device`.');
|
|
156
|
+
const { status, body } = await postForm(c, '/v4/oauth/token', { grant_type: 'refresh_token', client_id: DEVICE_CLIENT_ID, refresh_token: saved.refresh_token });
|
|
157
|
+
if (status !== 200)
|
|
158
|
+
throw new DeviceLoginError(`One account refresh failed (${status}). Run \`genesis login --device\` again.`);
|
|
159
|
+
const next = parseDeviceTokens(body);
|
|
160
|
+
if (next.auth?.principal_id !== saved.auth?.principal_id)
|
|
161
|
+
invalid();
|
|
162
|
+
return next;
|
|
163
|
+
}
|
|
164
|
+
export function requireDeviceSaved(s) {
|
|
165
|
+
const a = s.auth;
|
|
166
|
+
const claims = claimsOf(s.access_token);
|
|
167
|
+
if (!a || a.kind !== 'lifeid-device' || a.issuer !== LIFEID_ISSUER || a.client_id !== DEVICE_CLIENT_ID || s.client_id !== DEVICE_CLIENT_ID
|
|
168
|
+
|| !claims || claims.iss !== a.issuer || claims.kind !== 'human' || claims.sub !== a.principal_id || claims.sid !== a.session_id
|
|
169
|
+
|| (s.organization_id !== undefined && claims.org !== s.organization_id)
|
|
170
|
+
|| claims.cnf !== undefined || claims.cap !== undefined || claims.obo !== undefined) {
|
|
171
|
+
throw new Error('Saved One account device session is inconsistent. Run `genesis login --device` again.');
|
|
172
|
+
}
|
|
173
|
+
}
|
package/dist/dials.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { Genesis } from '@onesi/genesis';
|
|
3
|
+
export declare function registerDials(program: Command, dependencies: {
|
|
4
|
+
client: () => Promise<Genesis>;
|
|
5
|
+
orgId: (client: Genesis) => Promise<string>;
|
|
6
|
+
emit: (data: unknown, pretty: () => string) => void;
|
|
7
|
+
}): void;
|
package/dist/dials.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
.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
|
+
const context = async () => {
|
|
8
|
+
const client = await dependencies.client();
|
|
9
|
+
const options = dials.opts();
|
|
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 } : {}) };
|
|
14
|
+
return { client, scope };
|
|
15
|
+
};
|
|
16
|
+
const emit = (data) => dependencies.emit(data, () => JSON.stringify(data, null, 2));
|
|
17
|
+
const numbers = dials.command('numbers').description('Carrier number inventory');
|
|
18
|
+
numbers.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.dials.numbers.list(scope)); });
|
|
19
|
+
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) => {
|
|
20
|
+
const { client, scope } = await context();
|
|
21
|
+
const text = options.filters?.startsWith('@') ? await readFile(options.filters.slice(1), 'utf8') : options.filters;
|
|
22
|
+
const filters = text ? JSON.parse(text) : {};
|
|
23
|
+
if (!filters || typeof filters !== 'object' || Array.isArray(filters))
|
|
24
|
+
throw new Error('Search filters must be a JSON object.');
|
|
25
|
+
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']);
|
|
26
|
+
if (Object.keys(filters).some(key => !allowed.has(key)))
|
|
27
|
+
throw new Error('Unsupported Dials search filter. Scope is selected by the command context.');
|
|
28
|
+
emit(await client.dials.numbers.search(scope, { ...filters, ...(options.areaCode ? { areaCode: options.areaCode } : {}) }));
|
|
29
|
+
});
|
|
30
|
+
numbers.command('reconciliations').action(async () => { const { client, scope } = await context(); emit(await client.dials.numbers.reconciliations(scope)); });
|
|
31
|
+
numbers.command('drift').action(async () => { const { client, scope } = await context(); emit(await client.dials.numbers.drift(scope)); });
|
|
32
|
+
dials.command('overview').action(async () => { const { client, scope } = await context(); emit(await client.dials.overview.summary(scope)); });
|
|
33
|
+
dials.command('operating-surface').action(async () => { const { client, scope } = await context(); emit(await client.dials.overview.operatingSurface(scope)); });
|
|
34
|
+
for (const [name, list, create] of [['people', 'dials_listHumanProfiles', 'dials_createHumanProfile'], ['agents', 'dials_listAgents', 'dials_createAgent'], ['voice', 'dials_listVoiceProfiles', 'dials_createVoiceProfile']]) {
|
|
35
|
+
const profiles = dials.command(name).description('Saved profile configuration in the selected scope');
|
|
36
|
+
profiles.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.call(list, scope)); });
|
|
37
|
+
profiles.command('create').requiredOption('--body <json-or-@file>', 'Unassigned profile fields and actorId; same-name POST updates the stored profile').action(async (options) => {
|
|
38
|
+
const { client, scope } = await context();
|
|
39
|
+
const text = options.body.startsWith('@') ? await readFile(options.body.slice(1), 'utf8') : options.body;
|
|
40
|
+
const body = JSON.parse(text);
|
|
41
|
+
if (!body || typeof body !== 'object' || Array.isArray(body) || 'phoneNumberId' in body)
|
|
42
|
+
throw new Error('Supply unassigned profile fields; use numbers assign after saving.');
|
|
43
|
+
emit(await client.call(create, { body: { ...body, scope } }));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const readBody = async (value) => {
|
|
47
|
+
const body = JSON.parse(value.startsWith('@') ? await readFile(value.slice(1), 'utf8') : value);
|
|
48
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
49
|
+
throw new Error('Dials body must be a JSON object.');
|
|
50
|
+
return body;
|
|
51
|
+
};
|
|
52
|
+
const messages = dials.command('messages').description('Saved messages, templates and pending registration configuration');
|
|
53
|
+
messages.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.dials.messages.list(scope)); });
|
|
54
|
+
messages.command('send').requiredOption('--body <json-or-@file>', 'Explicit approvals, actor, sender, destination, body and idempotency key')
|
|
55
|
+
.action(async (options) => { const { client, scope } = await context(); const body = await readBody(options.body); emit(await client.call('dials_createOutboundMessage', { body: { ...body, scope } })); });
|
|
56
|
+
for (const [name, list, create] of [
|
|
57
|
+
['templates', 'dials_listMessageTemplates', 'dials_createMessageTemplate'],
|
|
58
|
+
['brands', 'dials_listMessagingBrands', 'dials_createMessagingBrand'],
|
|
59
|
+
['campaigns', 'dials_listMessagingCampaigns', 'dials_createMessagingCampaign'],
|
|
60
|
+
]) {
|
|
61
|
+
const group = messages.command(name).description('Scoped saved configuration; creation is not provider approval');
|
|
62
|
+
group.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.call(list, scope)); });
|
|
63
|
+
group.command('create').requiredOption('--body <json-or-@file>', 'Configuration fields and actorId')
|
|
64
|
+
.action(async (options) => { const { client, scope } = await context(); const body = await readBody(options.body); emit(await client.call(create, { body: { ...body, scope } })); });
|
|
65
|
+
if (name === 'campaigns')
|
|
66
|
+
group.command('assign').requiredOption('--campaign <id>', 'Owned campaign ID').requiredOption('--body <json-or-@file>', 'Owned phoneNumberId and actorId')
|
|
67
|
+
.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 } })); });
|
|
68
|
+
}
|
|
69
|
+
const routing = dials.command('routing').description('Managed owned-number inbound policies; workflow documents are separate');
|
|
70
|
+
routing.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.dials.routing.list(scope)); });
|
|
71
|
+
routing.command('create').requiredOption('--body <json-or-@file>', 'Owned source, supported action, priority and persisted original UUID; creates an enabled policy')
|
|
72
|
+
.action(async (options) => { const { client, scope } = await context(); const body = await readBody(options.body); emit(await client.call('dials_createRoutingRule', { body: { ...body, scope } })); });
|
|
73
|
+
routing.command('disable').requiredOption('--rule <id>', 'Owned managed policy ID; record is retained')
|
|
74
|
+
.action(async (options) => { const { client, scope } = await context(); emit(await client.dials.routing.disable(options.rule, { scope })); });
|
|
75
|
+
const workflows = dials.command('workflows').description('Persisted routing descriptors; execution is verified separately');
|
|
76
|
+
workflows.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.dials.workflows.list(scope)); });
|
|
77
|
+
workflows.command('deploy').requiredOption('--body <json-or-@file>', 'Workflow graph and actorId')
|
|
78
|
+
.action(async (options) => { const { client, scope } = await context(); const body = await readBody(options.body); emit(await client.call('dials_deployWorkflow', { body: { ...body, scope } })); });
|
|
79
|
+
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')
|
|
80
|
+
.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 } : {}) })); });
|
|
81
|
+
const calls = dials.command('calls').description('Call activity and approved outbound calls');
|
|
82
|
+
calls.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.dials.calls.list(scope)); });
|
|
83
|
+
// @file keeps structured approval and idempotency fields out of shell history.
|
|
84
|
+
for (const [parent, name, operation] of [
|
|
85
|
+
[numbers, 'caller-name', 'dials_updateNumberCallerName'], [numbers, 'reconcile', 'dials_reconcileNumberInventory'],
|
|
86
|
+
[numbers, 'provision', 'dials_provisionNumber'], [numbers, 'assign', 'dials_assignNumber'], [numbers, 'release', 'dials_releaseNumber'], [numbers, 'e911', 'dials_registerEmergencyAddress'],
|
|
87
|
+
[calls, 'validate', 'dials_validateOutboundCall'], [calls, 'create', 'dials_createOutboundCall'], [calls, 'recover', 'dials_recoverOutboundCall'],
|
|
88
|
+
]) {
|
|
89
|
+
parent.command(name).requiredOption('--body <json-or-@file>', 'JSON input including actorId and required operation fields')
|
|
90
|
+
.action(async (options) => {
|
|
91
|
+
const { client, scope } = await context();
|
|
92
|
+
const text = options.body.startsWith('@') ? await readFile(options.body.slice(1), 'utf8') : options.body;
|
|
93
|
+
const body = JSON.parse(text);
|
|
94
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
95
|
+
throw new Error('Dials body must be a JSON object.');
|
|
96
|
+
emit(await client.call(operation, { body: { ...body, scope } }));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
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,102 @@
|
|
|
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
|
+
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) } : {}) })));
|
|
70
|
+
const instances = fleets.command('instances').description('Actual admitted Henry Host status');
|
|
71
|
+
instances.command('list').option('--limit <number>').option('--after <instance_id>').action(async (options) => {
|
|
72
|
+
emit(await (await client()).fleets.instances.list({ ...(options.limit ? { limit: Number(options.limit) } : {}), ...(options.after ? { after: options.after } : {}) }));
|
|
73
|
+
});
|
|
74
|
+
instances.command('get <instance_id>').action(async (id) => emit(await (await client()).fleets.instances.get(id)));
|
|
75
|
+
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 })));
|
|
76
|
+
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 })));
|
|
77
|
+
const connection = fleets.command('connection').description('Organization-owned Foundry connection');
|
|
78
|
+
connection.command('get').action(async () => emit(await (await client()).fleets.connection.get()));
|
|
79
|
+
connection.command('set').description('Read a Foundry key from stdin and encrypt it with Genesis KMS').action(async () => {
|
|
80
|
+
const apiKey = await keyInput();
|
|
81
|
+
emit(await (await client()).fleets.connection.set(apiKey));
|
|
82
|
+
});
|
|
83
|
+
connection.command('delete').action(async () => emit(await (await client()).fleets.connection.delete()));
|
|
84
|
+
const usage = fleets.command('usage').description('Usage observations derived from your tasks');
|
|
85
|
+
usage.command('events').description('List task lifecycle and execution-time usage events').option('--limit <n>').option('--before <task_id>')
|
|
86
|
+
.action(async (options) => emit(await (await client()).fleets.usage.events({ ...(options.limit ? { limit: Number(options.limit) } : {}), ...(options.before ? { before: options.before } : {}) })));
|
|
87
|
+
const tasks = fleets.command('tasks');
|
|
88
|
+
tasks.command('list').option('--limit <number>').option('--before <task_id>').action(async (options) => {
|
|
89
|
+
emit(await (await client()).fleets.tasks.list({ ...(options.limit ? { limit: Number(options.limit) } : {}), ...(options.before ? { before: options.before } : {}) }));
|
|
90
|
+
});
|
|
91
|
+
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) => {
|
|
92
|
+
const timeout = Number(options.timeout);
|
|
93
|
+
if (!Number.isInteger(timeout) || timeout < 1 || timeout > 3600)
|
|
94
|
+
throw new Error('Timeout must be 1–3600 seconds.');
|
|
95
|
+
emit(await (await client()).fleets.tasks.submit({ input, timeout_seconds: timeout, idempotency_key: options.idempotencyKey }));
|
|
96
|
+
});
|
|
97
|
+
tasks.command('get <task_id>').action(async (id) => emit(await (await client()).fleets.tasks.get(id)));
|
|
98
|
+
tasks.command('cancel <task_id>').action(async (id) => emit(await (await client()).fleets.tasks.cancel(id)));
|
|
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) } : {})));
|
|
102
|
+
}
|
package/dist/flocks.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { Genesis } from '@onesi/genesis';
|
|
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;
|