@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 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
- let s = await store.get();
186
- if (!s)
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, s);
189
- const exp = jwtExp(s.access_token);
190
- const deadline = Math.min(s.expires_at ?? 0, exp ?? 0);
191
- if (!Number.isFinite(deadline) || deadline - 60 <= now()) {
192
- let pending = rotations.get(store);
193
- if (!pending) {
194
- pending = (async () => { const next = await refresh(c, s); await store.set(next); return next; })();
195
- rotations.set(store, pending);
196
- }
197
- try {
198
- s = await pending;
199
- }
200
- finally {
201
- if (rotations.get(store) === pending)
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, refresh, savedLoginProvider, startLogin } from './auth.js';
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.7')
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.7',
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 refresh(auth, s, id);
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');
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ import type { Genesis } from '@lifesi/genesis';
3
+ export declare function registerMinds(program: Command, client: () => Promise<Genesis>, emit: (data: unknown) => void): void;
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.7",
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.7"
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
  }