@lifesi/genesis-cli 0.1.7 → 0.1.8
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/dist/auth.d.ts +3 -0
- package/dist/auth.js +69 -20
- package/dist/index.js +15 -5
- package/dist/minds.d.ts +3 -0
- package/dist/minds.js +28 -0
- package/package.json +12 -2
package/dist/auth.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type GenesisContextSelection } from '@lifesi/genesis';
|
|
1
2
|
import type { CredentialStore, SavedLogin } from './store.js';
|
|
2
3
|
import { type NativeAttempt } from './lifeid-native.js';
|
|
3
4
|
/** LifeID's admitted native product bridge, never a direct AuthKit bearer. */
|
|
@@ -20,5 +21,7 @@ export declare function jwtExp(token: string): number | undefined;
|
|
|
20
21
|
export declare function parseLifeSession(c: AuthConfig, value: unknown, now?: number): SavedLogin;
|
|
21
22
|
export declare function completeLogin(c: AuthConfig, attempt: NativeAttempt): Promise<SavedLogin>;
|
|
22
23
|
export declare function refresh(c: AuthConfig, s: SavedLogin, organizationId?: string): Promise<SavedLogin>;
|
|
24
|
+
/** Commit both replacement credentials through the existing atomic keychain entry. */
|
|
25
|
+
export declare function selectSavedContext(c: AuthConfig, store: CredentialStore, selection: GenesisContextSelection, signal?: AbortSignal): Promise<SavedLogin>;
|
|
23
26
|
/** OS-keychain bridge session; one committed rotation per store, never raw AuthKit. */
|
|
24
27
|
export declare function savedLoginProvider(c: AuthConfig, store: CredentialStore, now?: () => number): () => Promise<string | undefined>;
|
package/dist/auth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { selectGenesisContext } from '@lifesi/genesis';
|
|
1
2
|
import { LIFEID_ISSUER, startNativeAttempt, validateRegistration } from './lifeid-native.js';
|
|
2
3
|
export function authConfig(env = process.env, f = fetch) {
|
|
3
4
|
return { productId: env.GENESIS_AUTH_PRODUCT, redirectUri: env.GENESIS_AUTH_REDIRECT_URI,
|
|
@@ -179,29 +180,77 @@ export async function refresh(c, s, organizationId) {
|
|
|
179
180
|
return n;
|
|
180
181
|
}
|
|
181
182
|
const rotations = new WeakMap();
|
|
183
|
+
/** Queue before the first await, so parallel selection and refresh cannot use
|
|
184
|
+
* the same spent credential. A failed predecessor does not trigger a retry. */
|
|
185
|
+
function serialize(store, run) {
|
|
186
|
+
const previous = rotations.get(store);
|
|
187
|
+
const pending = previous ? previous.then(run) : Promise.resolve().then(run);
|
|
188
|
+
rotations.set(store, pending);
|
|
189
|
+
return pending.finally(() => { if (rotations.get(store) === pending)
|
|
190
|
+
rotations.delete(store); });
|
|
191
|
+
}
|
|
192
|
+
async function commit(store, value) {
|
|
193
|
+
try {
|
|
194
|
+
await store.set(value);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
throw new Error('The returned LifeID session could not be saved. No automatic retry was made.');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function currentLogin(c, store) {
|
|
201
|
+
const saved = await store.get();
|
|
202
|
+
if (!saved)
|
|
203
|
+
throw new Error('Run `genesis login` before selecting a context.');
|
|
204
|
+
requireSaved(c, saved);
|
|
205
|
+
return saved;
|
|
206
|
+
}
|
|
207
|
+
function needsRefresh(saved, now) {
|
|
208
|
+
return Math.min(saved.expires_at ?? 0, jwtExp(saved.access_token) ?? 0) - 60 <= now;
|
|
209
|
+
}
|
|
210
|
+
/** Commit both replacement credentials through the existing atomic keychain entry. */
|
|
211
|
+
export async function selectSavedContext(c, store, selection, signal) {
|
|
212
|
+
const requested = { ...selection };
|
|
213
|
+
return serialize(store, async () => {
|
|
214
|
+
let saved;
|
|
215
|
+
const response = await selectGenesisContext(requested, { signal, fetch: c.fetch, credential: async () => {
|
|
216
|
+
signal?.throwIfAborted();
|
|
217
|
+
saved = await currentLogin(c, store);
|
|
218
|
+
if (needsRefresh(saved, Date.now() / 1000)) {
|
|
219
|
+
saved = await refresh(c, saved);
|
|
220
|
+
await commit(store, saved);
|
|
221
|
+
}
|
|
222
|
+
signal?.throwIfAborted();
|
|
223
|
+
return saved.access_token;
|
|
224
|
+
} });
|
|
225
|
+
const next = parseLifeSession(c, response);
|
|
226
|
+
if (next.auth?.principal_id !== saved?.auth?.principal_id || next.organization_id !== requested.organizationId)
|
|
227
|
+
invalidEnvelope();
|
|
228
|
+
await commit(store, next);
|
|
229
|
+
return next;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
182
232
|
/** OS-keychain bridge session; one committed rotation per store, never raw AuthKit. */
|
|
183
233
|
export function savedLoginProvider(c, store, now = () => Date.now() / 1000) {
|
|
184
234
|
return async () => {
|
|
185
|
-
|
|
186
|
-
if (
|
|
235
|
+
const mutation = rotations.get(store);
|
|
236
|
+
if (mutation)
|
|
237
|
+
return (await mutation).access_token;
|
|
238
|
+
const saved = await store.get();
|
|
239
|
+
if (!saved)
|
|
187
240
|
return undefined;
|
|
188
|
-
requireSaved(c,
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
rotations.delete(store);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return s.access_token;
|
|
241
|
+
requireSaved(c, saved);
|
|
242
|
+
const newer = rotations.get(store);
|
|
243
|
+
if (newer)
|
|
244
|
+
return (await newer).access_token;
|
|
245
|
+
if (!needsRefresh(saved, now()))
|
|
246
|
+
return saved.access_token;
|
|
247
|
+
return (await serialize(store, async () => {
|
|
248
|
+
const current = await currentLogin(c, store);
|
|
249
|
+
if (!needsRefresh(current, now()))
|
|
250
|
+
return current;
|
|
251
|
+
const next = await refresh(c, current);
|
|
252
|
+
await commit(store, next);
|
|
253
|
+
return next;
|
|
254
|
+
})).access_token;
|
|
206
255
|
};
|
|
207
256
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { registerMinds } from './minds.js';
|
|
1
2
|
import { registerRelaysEnrollment } from './relays-enrollment.js';
|
|
2
3
|
import { registerBeacons } from './beacons.js';
|
|
3
4
|
import { registerDials } from './dials.js';
|
|
4
5
|
import { Command, CommanderError } from 'commander';
|
|
5
6
|
import { Genesis, GenesisError, hivesResponseBytes, MAX_FUNCTION_BODY_BYTES, MAX_FUNCTION_JSON_BYTES, services } from '@lifesi/genesis';
|
|
6
|
-
import { authConfig, completeLogin,
|
|
7
|
+
import { authConfig, completeLogin, selectSavedContext, savedLoginProvider, startLogin } from './auth.js';
|
|
7
8
|
import { keychainStore } from './store.js';
|
|
8
9
|
import { c, listOf, stdio, table } from './output.js';
|
|
9
10
|
import { spawn } from 'node:child_process';
|
|
@@ -33,7 +34,7 @@ export function buildProgram(deps = {}) {
|
|
|
33
34
|
const auth = { ...authConfig(env, f), sleep: deps.sleep };
|
|
34
35
|
const program = new Command('genesis')
|
|
35
36
|
.description('One CLI for every Genesis service.')
|
|
36
|
-
.version('0.1.
|
|
37
|
+
.version('0.1.8')
|
|
37
38
|
.option('--json', 'machine-readable JSON output')
|
|
38
39
|
.option('--api-key <key>', 'use this API key (default: GENESIS_API_KEY, then your `genesis login`)')
|
|
39
40
|
.option('--base-url <url>', 'gateway URL (default: GENESIS_BASE_URL or https://console.life.one/api)')
|
|
@@ -49,7 +50,7 @@ export function buildProgram(deps = {}) {
|
|
|
49
50
|
const opts = {
|
|
50
51
|
apiKey: o.apiKey ?? env.GENESIS_API_KEY, token: env.GENESIS_TOKEN,
|
|
51
52
|
baseUrl: o.baseUrl ?? env.GENESIS_BASE_URL, routing: o.direct || env.GENESIS_ROUTING === 'direct' ? 'direct' : 'gateway',
|
|
52
|
-
fetch: f, userAgent: 'genesis-cli/0.1.
|
|
53
|
+
fetch: f, userAgent: 'genesis-cli/0.1.8',
|
|
53
54
|
credentials: store ? savedLoginProvider(auth, store) : undefined,
|
|
54
55
|
};
|
|
55
56
|
return new Genesis(hivesTransport ? { ...opts, fetch: createHivesNodeTransport(opts, deps.signal) } : opts);
|
|
@@ -148,10 +149,18 @@ export function buildProgram(deps = {}) {
|
|
|
148
149
|
const s = await store.get();
|
|
149
150
|
if (!s)
|
|
150
151
|
throw new Error('Not signed in. Run `genesis login`.');
|
|
151
|
-
const n = await
|
|
152
|
-
await store.set(n);
|
|
152
|
+
const n = await selectSavedContext(auth, store, { organizationId: id }, deps.signal);
|
|
153
153
|
emit({ organization_id: n.organization_id }, () => `${c.green('✓')} Now using ${c.bold(n.organization_id ?? id)}.`);
|
|
154
154
|
});
|
|
155
|
+
const selection = program.command('context').description('Select an authorized Genesis organization, workspace and project');
|
|
156
|
+
selection.command('select').requiredOption('--organization <id>').requiredOption('--workspace <id>').requiredOption('--project <id>')
|
|
157
|
+
.description('Bind the saved human login to a Console Sandbox context and atomically replace its credentials')
|
|
158
|
+
.action(async (options) => {
|
|
159
|
+
const target = { organizationId: options.organization, workspaceId: options.workspace, projectId: options.project,
|
|
160
|
+
platformId: 'pf_console', fleetId: null, environmentId: 'sandbox' };
|
|
161
|
+
const next = await selectSavedContext(auth, await getStore(), target, deps.signal);
|
|
162
|
+
emit({ principal_id: next.auth?.principal_id, ...target }, () => `Selected ${target.organizationId} / ${target.workspaceId} / ${target.projectId} (Sandbox).`);
|
|
163
|
+
});
|
|
155
164
|
// ---- keys ------------------------------------------------------------
|
|
156
165
|
const keys = program.command('keys').description('Org API keys (LifeID / WorkOS)');
|
|
157
166
|
keys.command('list').action(async () => {
|
|
@@ -205,6 +214,7 @@ export function buildProgram(deps = {}) {
|
|
|
205
214
|
const r = await gen.foundry.chat({ model: o.model, messages });
|
|
206
215
|
emit(r, () => r.choices?.[0]?.message?.content ?? JSON.stringify(r));
|
|
207
216
|
});
|
|
217
|
+
registerMinds(program, client, data => emit(data, () => JSON.stringify(data, null, 2)));
|
|
208
218
|
const hivesClient = () => client(true);
|
|
209
219
|
// ---- hives -----------------------------------------------------------
|
|
210
220
|
const hives = program.command('hives').description('Hives: projects, secrets, deployments, functions, schedules and activity');
|
package/dist/minds.d.ts
ADDED
package/dist/minds.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
export function registerMinds(program, client, emit) {
|
|
3
|
+
const minds = program.command('minds').description('Owned Minds and persisted memory; requires an admitted human Console Sandbox session');
|
|
4
|
+
minds.command('list').option('--after <uuid>').option('--limit <number>').description('List real Minds and the current creation context fingerprint').action(async (options) => {
|
|
5
|
+
emit(await (await client()).minds.list({ ...(options.after ? { after: options.after } : {}), ...(options.limit ? { limit: Number(options.limit) } : {}) }));
|
|
6
|
+
});
|
|
7
|
+
minds.command('get <mind_id>').action(async (id) => emit(await (await client()).minds.get(id)));
|
|
8
|
+
minds.command('create <name>').description('Create a capped Free/F1 Sandbox Mind; 202 means provisioning')
|
|
9
|
+
.requiredOption('--idempotency-key <uuid>', 'persist this UUID4 and retain it after an uncertain response')
|
|
10
|
+
.requiredOption('--context-sha256 <hash>', 'context_sha256 from the current minds list response')
|
|
11
|
+
.action(async (name, options) => emit(await (await client()).minds.create(name, { idempotencyKey: options.idempotencyKey, contextSha256: options.contextSha256 })));
|
|
12
|
+
const memory = minds.command('memory').description('Every operation selects one owned, ready Mind');
|
|
13
|
+
memory.command('insert <mind_id>').requiredOption('--event-file <path>', 'JSON event with id, content and timestamp; returned id is its storage UUID')
|
|
14
|
+
.action(async (id, options) => {
|
|
15
|
+
const metadata = await stat(options.eventFile);
|
|
16
|
+
if (!metadata.isFile() || metadata.size > 65_536)
|
|
17
|
+
throw new Error('Event file must be at most 64 KiB.');
|
|
18
|
+
const bytes = await readFile(options.eventFile);
|
|
19
|
+
if (bytes.length > 65_536)
|
|
20
|
+
throw new Error('Event file exceeds 64 KiB.');
|
|
21
|
+
const event = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
22
|
+
emit(await (await client()).minds.memory.insert(id, event));
|
|
23
|
+
});
|
|
24
|
+
memory.command('get <mind_id> <storage_id>').action(async (mind, storage) => emit(await (await client()).minds.memory.get(mind, storage)));
|
|
25
|
+
memory.command('search <mind_id> <text>').option('--limit <number>', '1–100 results', '20').action(async (mind, text, options) => {
|
|
26
|
+
emit(await (await client()).minds.memory.search(mind, text, Number(options.limit)));
|
|
27
|
+
});
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lifesi/genesis-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/l1felabs/genesis-api.git",
|
|
@@ -18,11 +18,21 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"commander": "^14.0.0",
|
|
20
20
|
"undici": "6.29.0",
|
|
21
|
-
"@lifesi/genesis": "0.1.
|
|
21
|
+
"@lifesi/genesis": "0.1.8"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
24
|
"@napi-rs/keyring": "^1.2.0"
|
|
25
25
|
},
|
|
26
|
+
"exports": {
|
|
27
|
+
"./auth": {
|
|
28
|
+
"types": "./dist/auth.d.ts",
|
|
29
|
+
"default": "./dist/auth.js"
|
|
30
|
+
},
|
|
31
|
+
"./store": {
|
|
32
|
+
"types": "./dist/store.d.ts",
|
|
33
|
+
"default": "./dist/store.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
26
36
|
"scripts": {
|
|
27
37
|
"build": "tsc -p tsconfig.json"
|
|
28
38
|
}
|