@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.
Files changed (63) hide show
  1. package/README.md +29 -2
  2. package/dist/beacons.js +68 -0
  3. package/dist/dials.js +7 -4
  4. package/dist/fleets.js +30 -0
  5. package/dist/flocks.d.ts +7 -1
  6. package/dist/flocks.js +257 -3
  7. package/dist/henry.d.ts +5 -1
  8. package/dist/henry.js +60 -1
  9. package/dist/hives-coverage.d.ts +12 -0
  10. package/dist/hives-coverage.js +116 -0
  11. package/dist/hives-node-transport.js +3 -0
  12. package/dist/hives-operations.d.ts +1 -0
  13. package/dist/hives-operations.js +2 -0
  14. package/dist/hives-platform.d.ts +13 -0
  15. package/dist/hives-platform.js +133 -0
  16. package/dist/iam-directory.d.ts +15 -0
  17. package/dist/iam-directory.js +71 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.js +255 -23
  20. package/dist/minds.d.ts +1 -1
  21. package/dist/minds.js +37 -2
  22. package/dist/onboard/art.d.ts +50 -0
  23. package/dist/onboard/art.js +139 -0
  24. package/dist/onboard/brand.d.ts +56 -0
  25. package/dist/onboard/brand.js +134 -0
  26. package/dist/onboard/caps.d.ts +90 -0
  27. package/dist/onboard/caps.js +232 -0
  28. package/dist/onboard/encode.d.ts +23 -0
  29. package/dist/onboard/encode.js +235 -0
  30. package/dist/onboard/eye.d.ts +103 -0
  31. package/dist/onboard/eye.js +147 -0
  32. package/dist/onboard/index.d.ts +61 -0
  33. package/dist/onboard/index.js +562 -0
  34. package/dist/onboard/mcp.d.ts +35 -0
  35. package/dist/onboard/mcp.js +118 -0
  36. package/dist/onboard/raster.d.ts +43 -0
  37. package/dist/onboard/raster.js +253 -0
  38. package/dist/onboard/sound.d.ts +2 -0
  39. package/dist/onboard/sound.js +54 -0
  40. package/dist/onboard/state.d.ts +24 -0
  41. package/dist/onboard/state.js +48 -0
  42. package/dist/onboard/term.d.ts +69 -0
  43. package/dist/onboard/term.js +292 -0
  44. package/dist/onboard/ui.d.ts +97 -0
  45. package/dist/onboard/ui.js +615 -0
  46. package/dist/orgs.d.ts +16 -0
  47. package/dist/orgs.js +89 -0
  48. package/dist/products.d.ts +6 -2
  49. package/dist/products.js +238 -12
  50. package/dist/relays-commands/certificates.d.ts +3 -0
  51. package/dist/relays-commands/certificates.js +20 -0
  52. package/dist/relays-commands/domains.d.ts +3 -0
  53. package/dist/relays-commands/domains.js +23 -0
  54. package/dist/relays-commands/index.d.ts +4 -0
  55. package/dist/relays-commands/index.js +10 -0
  56. package/dist/relays-commands/monitoring.d.ts +3 -0
  57. package/dist/relays-commands/monitoring.js +41 -0
  58. package/dist/relays-commands/telemetry.d.ts +3 -0
  59. package/dist/relays-commands/telemetry.js +28 -0
  60. package/dist/relays-commands/zones.d.ts +5 -0
  61. package/dist/relays-commands/zones.js +9 -0
  62. package/dist/runner.js +2 -0
  63. package/package.json +2 -2
@@ -0,0 +1,562 @@
1
+ /**
2
+ * `genesis setup`: from a fresh install to a live call in about a minute. Six steps, each skippable and resumable:
3
+ * sign in with One, pick the organization, project and environment, optionally create a scoped API key, put the MCP
4
+ * server into your agents, make a first live call, and see what to do next.
5
+ *
6
+ * Loaded only by `genesis setup` and a bare `genesis` on a terminal (dynamic import in ../index.ts), so no other
7
+ * command pays for any of it. `--json`, pipes and CI never animate and never prompt.
8
+ */
9
+ import { hostname } from 'node:os';
10
+ import { spawn } from 'node:child_process';
11
+ import { safe } from '../output.js';
12
+ import { detect, probe, systemPreferences } from './caps.js';
13
+ import { Term, Interrupted } from './term.js';
14
+ import { Style, welcome, select, confirm, input, pick, waitBeside, delight, busy, typeLines } from './ui.js';
15
+ import { SetupStore, STEPS, nextStep } from './state.js';
16
+ import { defaultMcpEnv, detectClients, install, MCP_PACKAGE } from './mcp.js';
17
+ import { play } from './sound.js';
18
+ const TITLES = { signin: 'Sign in', context: 'Organization and project', key: 'API key', mcp: 'Your agents', try: 'First live call', next: 'Next' };
19
+ const COMMANDS = {
20
+ signin: 'genesis login', context: 'genesis context select --organization <o_…> --workspace <w_…> --project <pr_…>',
21
+ key: 'genesis keys create <name> --scopes hives:read', mcp: `claude mcp add genesis -- npx -y ${MCP_PACKAGE}`, try: 'genesis status', next: 'genesis --help',
22
+ };
23
+ export async function capsFor(d) {
24
+ const stdout = d.stdout ?? process.stdout, stdin = d.stdin ?? process.stdin;
25
+ let caps = detect({ env: d.env, stdoutTTY: !!stdout.isTTY && !d.flags.json, stdinTTY: !!stdin.isTTY, columns: stdout.columns, rows: stdout.rows,
26
+ flags: { animation: d.flags.animation, screenReader: d.flags.screenReader, sound: d.flags.sound, screencast: d.flags.screencast } });
27
+ if (!caps.interactive || d.flags.probe === false)
28
+ return caps;
29
+ const [probed, prefs] = await Promise.all([probe(caps, { stdin, stdout }), systemPreferences()]);
30
+ caps = probed;
31
+ // GENESIS_SETUP_TRACE=<file>: what was detected and why, for bug reports (never secrets: only terminal facts).
32
+ if (d.env.GENESIS_SETUP_TRACE)
33
+ await import('node:fs/promises').then((fs) => fs.writeFile(d.env.GENESIS_SETUP_TRACE, JSON.stringify({ caps, prefs }, null, 2))).catch(() => { });
34
+ if (prefs.screenReader)
35
+ caps = { ...caps, screenReader: true, motion: false, stillBecause: caps.stillBecause ?? 'screen reader' };
36
+ if (prefs.reducedMotion)
37
+ caps = { ...caps, motion: false, stillBecause: caps.stillBecause ?? 'reduced motion (system setting)' };
38
+ return caps;
39
+ }
40
+ export async function runSetup(d) {
41
+ const setup = SetupStore.for(d.env);
42
+ if (d.flags.restart)
43
+ await setup.write({ version: 1, steps: {} });
44
+ const state = await setup.read();
45
+ const only = d.flags.step;
46
+ if (only && !STEPS.includes(only))
47
+ throw new Error(`Unknown setup step "${safe(only)}". Steps: ${STEPS.join(', ')}.`);
48
+ // --json: where setup is up to, for scripts. Never prompts, never draws.
49
+ if (d.flags.json) {
50
+ d.log(JSON.stringify({ config: setup.path, next: nextStep(state), steps: STEPS.map((id) => ({ id, title: TITLES[id], ...(state.steps[id] ?? { state: 'pending' }), command: COMMANDS[id] })) }, null, 2));
51
+ return 0;
52
+ }
53
+ const caps = await capsFor(d);
54
+ if (!caps.interactive) {
55
+ // A pipe or CI: say what setup would do, as commands, and change nothing.
56
+ d.log('Genesis setup needs an interactive terminal. In scripts and CI, use an API key (GENESIS_API_KEY) and these commands:');
57
+ for (const id of STEPS)
58
+ d.log(` ${state.steps[id]?.state === 'done' ? 'done ' : 'pending'} ${TITLES[id].padEnd(25)} ${COMMANDS[id]}`);
59
+ return 0;
60
+ }
61
+ const term = new Term(caps, d.stdout ?? process.stdout, d.stdin ?? process.stdin);
62
+ const style = new Style(term);
63
+ term.start('Genesis · setup');
64
+ term.onInterrupt = () => {
65
+ void setup.read().then((s) => {
66
+ term.say();
67
+ term.margin = ' ';
68
+ term.say(style.muted(`Paused. Run ${style.bold('genesis setup')} to pick up at ${TITLES[nextStep(s) ?? 'signin'].toLowerCase()}.`));
69
+ }).finally(() => { term.restore(); process.exit(130); });
70
+ };
71
+ try {
72
+ if (!only) {
73
+ await welcome(term, style, { short: !!state.welcomed, seed: d.seed ?? (d.env.GENESIS_DEMO_SEED ? Number(d.env.GENESIS_DEMO_SEED) : undefined), tagline: [
74
+ `${style.bold('Genesis')} ${style.muted('the developer tools for OneOS')}`,
75
+ style.muted(`Six steps, about a minute. Enter takes the default, s skips, Ctrl-C pauses.`),
76
+ ] });
77
+ await setup.write({ ...(await setup.read()), welcomed: state.welcomed ?? new Date().toISOString() });
78
+ }
79
+ const order = only ? [only] : STEPS.filter((id) => state.steps[id]?.state !== 'done');
80
+ if (!order.length) {
81
+ term.say(`${style.check} ${style.bold('Setup is complete.')} ${style.muted('Redo a step with genesis setup <step>: ' + STEPS.join(', ') + '.')}`);
82
+ return 0;
83
+ }
84
+ const ctx = new Steps(d, term, style, setup);
85
+ for (const id of order) {
86
+ const n = STEPS.indexOf(id) + 1;
87
+ term.progress(1, ((n - 1) / STEPS.length) * 100);
88
+ term.title(`Genesis · ${TITLES[id]}`);
89
+ term.say();
90
+ term.margin = ' ';
91
+ term.say(`${style.muted(`${n}/${STEPS.length}`)} ${style.bold(TITLES[id])}`);
92
+ term.margin = ' ';
93
+ let result;
94
+ try {
95
+ result = await ctx[id]();
96
+ }
97
+ catch (e) {
98
+ if (e instanceof Interrupted)
99
+ throw e;
100
+ // One step failing never ends setup: say why, keep it for next time, and carry on.
101
+ term.say(`${style.error(caps.unicode ? '✗' : 'x')} ${safe(e.message)}`);
102
+ term.say(style.muted(`This step stays open; run genesis setup ${id} to try it again.`));
103
+ result = { state: 'skipped', note: 'failed' };
104
+ }
105
+ await setup.mark(id, result.state, result.note);
106
+ term.margin = ' ';
107
+ }
108
+ term.progress(1, 100);
109
+ if (!only) {
110
+ term.say();
111
+ await delight(term, style, `${style.bold("You're set.")} ${style.muted('Build something.')}`);
112
+ if (caps.sound)
113
+ await play();
114
+ }
115
+ return 0;
116
+ }
117
+ catch (e) {
118
+ if (e instanceof Interrupted) {
119
+ const s = await setup.read();
120
+ term.say();
121
+ term.margin = ' ';
122
+ term.say(style.muted(`Paused. Run ${style.bold('genesis setup')} to pick up at ${TITLES[nextStep(s) ?? 'signin'].toLowerCase()}.`));
123
+ return 130;
124
+ }
125
+ throw e;
126
+ }
127
+ finally {
128
+ term.restore();
129
+ }
130
+ }
131
+ class Steps {
132
+ d;
133
+ term;
134
+ style;
135
+ setup;
136
+ store;
137
+ lastKey;
138
+ constructor(d, term, style, setup) {
139
+ this.d = d;
140
+ this.term = term;
141
+ this.style = style;
142
+ this.setup = setup;
143
+ }
144
+ async getStore() { return (this.store ??= await this.d.getStore()); }
145
+ /** A name from the account (an organization, project or workspace), masked to its length in screencast mode. */
146
+ nameOf(v) {
147
+ // A fixed width, so the mask says nothing about the name's length.
148
+ return this.term.caps.screencast ? '••••••' : safe(v.name ?? v.id);
149
+ }
150
+ ok(line) { this.term.say(`${this.style.check} ${line}`); }
151
+ note(line) { this.term.say(this.style.muted(line)); }
152
+ // ── 1. sign in ──
153
+ async signin() {
154
+ const { term, style, d } = this;
155
+ let store;
156
+ try {
157
+ store = await this.getStore();
158
+ }
159
+ catch (e) {
160
+ this.note(`${safe(e.message)}`);
161
+ return { state: 'skipped', note: 'keychain unavailable' };
162
+ }
163
+ const saved = await store.get();
164
+ if (saved) {
165
+ const who = await busy(term, style, 'Checking your saved sign-in', (async () => (await d.client()).whoami())().catch(() => null));
166
+ if (who) {
167
+ const name = this.term.mask(safe(saved.user?.email ?? saved.user?.id ?? who.principal ?? 'you'));
168
+ const keep = await select(term, style, { title: `Signed in as ${name}.`, options: [
169
+ { label: 'Keep this sign-in', value: true }, { label: 'Sign in as someone else', value: false }
170
+ ] });
171
+ if (keep !== false)
172
+ return { state: 'done' };
173
+ }
174
+ else
175
+ this.note('Your saved sign-in has expired. Sign in again:');
176
+ }
177
+ const remote = !!(d.env.SSH_CONNECTION || d.env.SSH_TTY) || (process.platform === 'linux' && !d.env.DISPLAY && !d.env.WAYLAND_DISPLAY);
178
+ const how = await select(term, style, { title: 'How do you want to sign in with One?', skippable: true, initial: remote ? 1 : 0, options: [
179
+ { label: 'In this browser', value: 'browser', hint: remote ? 'this looks like a remote machine' : undefined },
180
+ { label: 'With a code, on any device', value: 'device' },
181
+ ] });
182
+ if (how === 'skip')
183
+ return { state: 'skipped' };
184
+ const s = how === 'browser' ? await this.browserLogin() : await this.deviceLogin();
185
+ if (!s)
186
+ return { state: 'skipped' };
187
+ await store.set(s);
188
+ const who = safe(s.user?.email ?? s.user?.id ?? 'you');
189
+ term.notify('Genesis', `Signed in as ${term.mask(who)}`);
190
+ await delight(term, style, `Signed in as ${style.bold(who)}`);
191
+ if (term.caps.sound)
192
+ void play();
193
+ return { state: 'done' };
194
+ }
195
+ async browserLogin() {
196
+ const { term, style, d } = this;
197
+ const attempt = await d.login.startLogin(d.auth, {});
198
+ try {
199
+ d.openBrowser(attempt.browserUrl);
200
+ const done = d.login.completeLogin(d.auth, attempt);
201
+ done.catch(() => { });
202
+ let copied = false;
203
+ for (;;) {
204
+ const r = await waitBeside(term, style, () => [
205
+ style.bold('Finish signing in with One in your browser.'),
206
+ `${term.link(attempt.browserUrl, 'Open the sign-in page')} ${style.muted('if it did not open')}`,
207
+ style.muted(copied ? 'Link copied. d use a code instead' : 'o open again c copy the link d use a code instead'),
208
+ ], done, ['o', 'c', 'd']);
209
+ if (r.value)
210
+ return r.value;
211
+ if (r.key?.name === 'o')
212
+ d.openBrowser(attempt.browserUrl);
213
+ if (r.key?.name === 'c')
214
+ copied = await copy(attempt.browserUrl);
215
+ if (r.key?.name === 'd') {
216
+ attempt.close();
217
+ return this.deviceLogin();
218
+ }
219
+ }
220
+ }
221
+ finally {
222
+ attempt.close();
223
+ }
224
+ }
225
+ async deviceLogin() {
226
+ const { term, style, d } = this;
227
+ // One signal for the whole device sign-in: leaving this step for any reason (Ctrl-C, an error) stops the polling,
228
+ // so a paused setup exits at once instead of waiting out the code.
229
+ const ac = new AbortController();
230
+ try {
231
+ const start = await busy(term, style, 'Asking One for a code', d.login.startDeviceLogin(d.auth, ac.signal));
232
+ const code = safe(start.userCode), uri = safe(start.verificationUri), full = safe(start.verificationUriComplete);
233
+ // A screencast shows the shape of the code, never the code itself.
234
+ const shownCode = term.caps.screencast ? code.replace(/[A-Za-z0-9]/g, '•') : code;
235
+ const done = d.login.completeDeviceLogin(d.auth, start, ac.signal);
236
+ done.catch(() => { });
237
+ let copied = false;
238
+ for (;;) {
239
+ const r = await waitBeside(term, style, () => [
240
+ `On any device, open ${term.link(full, style.bold(uri))}`,
241
+ `and enter ${style.bold(shownCode.split('').join(term.caps.screenReader ? ' ' : ''))}`,
242
+ style.muted(copied ? 'Code copied. o open it here' : `o open it here c copy the code ${Math.round(start.expiresIn / 60)} min to approve`),
243
+ ], done, ['o', 'c']);
244
+ if (r.value)
245
+ return r.value;
246
+ if (r.key?.name === 'o')
247
+ d.openBrowser(full);
248
+ if (r.key?.name === 'c')
249
+ copied = await copy(code);
250
+ }
251
+ }
252
+ finally {
253
+ ac.abort();
254
+ }
255
+ }
256
+ // ── 2. organization, project, environment ──
257
+ async context() {
258
+ const { term, style, d } = this;
259
+ const store = await this.getStore().catch(() => undefined);
260
+ const saved = await store?.get();
261
+ if (!store || !saved) {
262
+ this.note('Sign in first; this step comes back next time.');
263
+ return { state: 'skipped' };
264
+ }
265
+ const token = () => d.login.savedLoginProvider(d.auth, store)().then((t) => t ?? '');
266
+ const iam = new Iam(secureUrl(d.env.GENESIS_IAM_URL ?? 'https://iam.l1fe.ai', 'GENESIS_IAM_URL'), d.fetch, token);
267
+ const orgs = await busy(term, style, 'Finding your organizations', iam.list('/v4/iam/orgs').catch((e) => {
268
+ this.note(`Your organizations could not be listed (${safe(e.message)}); using the one you signed in to.`);
269
+ return [];
270
+ }));
271
+ const known = orgs.length ? orgs : saved.organization_id ? [{ id: saved.organization_id, name: saved.organization_id }] : [];
272
+ if (!known.length) {
273
+ this.note('No organization on this sign-in yet. Create one at ' + term.link('https://os.one.ml', 'os.one.ml') + '.');
274
+ return { state: 'skipped' };
275
+ }
276
+ let org = known.find((o) => o.id === saved.organization_id) ?? known[0];
277
+ if (known.length > 1) {
278
+ const chosen = await select(term, style, { title: 'Organization', skippable: true, initial: Math.max(0, known.indexOf(org)),
279
+ options: known.map((o) => ({ label: this.nameOf(o), value: o, hint: safe(o.id) })) });
280
+ if (chosen === 'skip')
281
+ return { state: 'skipped' };
282
+ org = chosen;
283
+ }
284
+ else
285
+ this.ok(`Organization ${style.bold(this.nameOf(org))} ${style.muted(safe(org.id))}`);
286
+ if (org.id !== saved.organization_id)
287
+ await busy(term, style, 'Switching organization', d.login.selectSavedContext(d.auth, store, { organizationId: org.id }));
288
+ const lists = async () => Promise.all([iam.list(`/v4/iam/orgs/${encodeURIComponent(org.id)}/projects`), iam.list(`/v4/iam/orgs/${encodeURIComponent(org.id)}/workspaces`)]);
289
+ let projects = [], workspaces = [];
290
+ try {
291
+ [projects, workspaces] = await busy(term, style, 'Reading projects', lists().catch(async (e) => {
292
+ // A sign-in with no tenant bound yet: bind the organization, then read again.
293
+ if (!(e instanceof IamError) || e.status >= 500)
294
+ throw e;
295
+ await d.login.selectSavedContext(d.auth, store, { organizationId: org.id });
296
+ return lists();
297
+ }));
298
+ }
299
+ catch (e) {
300
+ this.note(`Projects could not be read (${safe(e.message)}). Choose one later with:`);
301
+ this.note(COMMANDS.context);
302
+ return { state: 'skipped', note: 'iam unavailable' };
303
+ }
304
+ projects = projects.filter((p) => /^pr_[A-Za-z0-9_-]+$/.test(p.id));
305
+ if (!projects.length) {
306
+ this.note(`No projects in ${this.nameOf(org)} yet. Create one in the ${term.link('https://os.one.ml', 'Console')}, then run genesis setup context.`);
307
+ return { state: 'skipped', note: 'no projects' };
308
+ }
309
+ let project = projects[0];
310
+ if (projects.length > 1) {
311
+ const chosen = await select(term, style, { title: 'Project', skippable: true, options: projects.map((p) => ({ label: this.nameOf(p), value: p, hint: safe(p.id) })) });
312
+ if (chosen === 'skip')
313
+ return { state: 'skipped' };
314
+ project = chosen;
315
+ }
316
+ else
317
+ this.ok(`Project ${style.bold(this.nameOf(project))} ${style.muted(safe(project.id))}`);
318
+ const valid = workspaces.filter((w) => /^w_[A-Za-z0-9_-]+$/.test(w.id));
319
+ const mine = valid.filter((w) => (w.project_id ?? w.projectId ?? project.id) === project.id);
320
+ const pool = mine.length ? mine : valid;
321
+ if (!pool.length) {
322
+ this.note('This project has no workspace yet. Create one in the Console, then run genesis setup context.');
323
+ return { state: 'skipped', note: 'no workspace' };
324
+ }
325
+ let ws = pool[0];
326
+ if (pool.length > 1) {
327
+ const chosen = await select(term, style, { title: 'Workspace', skippable: true, options: pool.map((w) => ({ label: this.nameOf(w), value: w, hint: safe(w.id) })) });
328
+ if (chosen === 'skip')
329
+ return { state: 'skipped' };
330
+ ws = chosen;
331
+ }
332
+ this.ok(`Environment ${style.bold('Sandbox')}`);
333
+ await busy(term, style, 'Binding your sign-in to this project', d.login.selectSavedContext(d.auth, store, {
334
+ organizationId: org.id, workspaceId: ws.id, projectId: project.id, platformId: 'pf_console', fleetId: null, environmentId: 'sandbox'
335
+ }));
336
+ this.ok(`Using ${style.bold(this.nameOf(project))} ${style.muted('/')} ${this.nameOf(ws)} ${style.muted('/ Sandbox')}`);
337
+ return { state: 'done', note: `${org.id} ${ws.id} ${project.id}` };
338
+ }
339
+ // ── 3. API key ──
340
+ async key() {
341
+ const { term, style, d } = this;
342
+ this.note('Your sign-in already works here. A key is for CI, servers and other machines.');
343
+ if (!(await confirm(term, style, 'Create an API key now?', false)))
344
+ return { state: 'done', note: 'declined' };
345
+ const name = await input(term, style, { label: 'Name', initial: `${hostname().split('.')[0].toLowerCase().replace(/[^a-z0-9-]/g, '-') || 'my'}-genesis`,
346
+ validate: (v) => /^[\w .-]{1,64}$/.test(v) ? null : 'Use up to 64 letters, numbers, spaces, dots, dashes or underscores.' });
347
+ const access = await select(term, style, { title: 'Access', options: [
348
+ { label: 'Read only', value: 'read', hint: 'hives, traces, beacons, flocks, fleets, …' },
349
+ { label: 'Same as you', value: 'all', hint: 'every scope your sign-in has' },
350
+ { label: 'Choose scopes', value: 'custom' },
351
+ ] });
352
+ let scopes;
353
+ if (access === 'read')
354
+ scopes = READ_SCOPES;
355
+ if (access === 'custom')
356
+ scopes = (await input(term, style, { label: 'Scopes', initial: 'hives:read',
357
+ validate: (v) => v.split(',').every((x) => /^[a-z]+:(read|write|admin)$/.test(x.trim())) ? null : 'Comma-separated, like hives:read,relays:write.' })).split(',').map((x) => x.trim());
358
+ const custody = await select(term, style, { title: 'Where should the key live?', options: [
359
+ { label: 'Show it once', value: 'reveal', hint: 'copy it into your CI secrets' },
360
+ { label: 'Keep it in Blinds', value: 'blinds', hint: 'nobody, human or agent, ever sees it' },
361
+ ] });
362
+ if (access === 'skip' || custody === 'skip')
363
+ return { state: 'skipped' };
364
+ const gen = await d.client();
365
+ const who = await gen.whoami();
366
+ const k = await busy(term, style, 'Creating the key', gen.keys.create(who.org, { name, custody, ...(scopes ? { scopes } : {}) }));
367
+ if (custody === 'blinds')
368
+ this.ok(`Key ${style.bold(safe(k.id))} is in Blinds as ${style.bold(safe(String(k.blinds_credential_ref)))}.`);
369
+ else if (typeof k.value !== 'string' || !k.value) {
370
+ this.note(`Key ${safe(k.id)} was created, but its value did not come back. Revoke it with genesis keys revoke ${safe(k.id)} and create another.`);
371
+ return { state: 'skipped', note: 'no value returned' };
372
+ }
373
+ else {
374
+ this.ok(`Key ${style.bold(safe(k.id))} created. Copy it now; it will not be shown again:`);
375
+ // Recordings outlive "shown once": a screencast never shows the value; the clipboard still gets it.
376
+ term.say();
377
+ term.say(` ${style.bold(term.caps.screencast ? '•'.repeat(24) : safe(String(k.value)))}`);
378
+ term.say();
379
+ if (await confirm(term, style, 'Copy it to the clipboard?', true)) {
380
+ if (await copy(String(k.value)))
381
+ this.note('Copied.');
382
+ }
383
+ }
384
+ this.lastKey = { id: k.id };
385
+ return { state: 'done', note: k.id };
386
+ }
387
+ // ── 4. MCP ──
388
+ async mcp() {
389
+ const { term, style, d } = this;
390
+ const env = d.mcpEnv ?? defaultMcpEnv(d.version);
391
+ const clients = await busy(term, style, 'Looking for your agents', detectClients(env));
392
+ const found = clients.filter((c) => c.present);
393
+ if (!found.length) {
394
+ this.note('No MCP clients found here. Add Genesis to any client as a stdio server:');
395
+ this.note(`npx -y ${MCP_PACKAGE}@${d.version}`);
396
+ return { state: 'done', note: 'no clients' };
397
+ }
398
+ const chosen = await pick(term, style, { title: 'Add Genesis to', options: found.map((c) => ({
399
+ label: c.name, value: c.id, checked: !c.installed, disabled: c.installed, hint: c.installed ? 'already added' : undefined
400
+ })) });
401
+ if (!chosen.length)
402
+ return { state: found.every((c) => c.installed) ? 'done' : 'skipped' };
403
+ term.say(style.muted('These change:'));
404
+ for (const id of chosen)
405
+ term.say(` ${style.muted(safe(clients.find((c) => c.id === id).target))}`);
406
+ term.say(style.muted('Each JSON file keeps its other servers and a .genesis-backup copy. It uses your genesis login; no key is written.'));
407
+ if (!(await confirm(term, style, 'Make these changes?', true)))
408
+ return { state: 'skipped' };
409
+ let okAll = true;
410
+ for (const id of chosen) {
411
+ const c = clients.find((x) => x.id === id);
412
+ try {
413
+ const msg = await busy(term, style, `Adding to ${c.name}`, install(env, id));
414
+ this.ok(`${style.bold(c.name)} ${style.muted(safe(msg))}`);
415
+ }
416
+ catch (e) {
417
+ okAll = false;
418
+ term.say(`${style.error('✗')} ${c.name} ${style.muted(safe(e.message))}`);
419
+ }
420
+ }
421
+ if (chosen.includes('claude-desktop'))
422
+ this.note('Restart Claude Desktop to load it.');
423
+ return { state: okAll ? 'done' : 'skipped' };
424
+ }
425
+ // ── 5. first live call ──
426
+ async try() {
427
+ const { term, style, d } = this;
428
+ term.say(style.muted(`$ genesis status`));
429
+ const url = `${secureUrl(d.baseUrl, 'base URL').replace(/\/+$/, '')}/v1/status`;
430
+ const report = await busy(term, style, 'Asking every service how it is', (async () => {
431
+ const r = await d.fetch(url, { headers: { accept: 'application/json', 'user-agent': `genesis-cli/${d.version} (setup)` }, signal: AbortSignal.timeout(15_000) });
432
+ if (!r.ok)
433
+ throw new Error(`HTTP ${r.status}`);
434
+ return await r.json();
435
+ })()).catch((e) => { this.note(`The gateway did not answer (${safe(e.message)}). Try genesis status later.`); return null; });
436
+ if (!report?.services?.length)
437
+ return { state: 'skipped' };
438
+ // Only what the gateway is documented to send reaches the terminal: names and states through safe(), latency
439
+ // only when it is a finite number.
440
+ const rows = report.services.map((r) => ({ service: String(r.service), state: String(r.state),
441
+ latency_ms: typeof r.latency_ms === 'number' && Number.isFinite(r.latency_ms) ? Math.round(r.latency_ms) : undefined }));
442
+ const healthy = (r) => r.state === 'ok' || r.state === 'up';
443
+ const nameW = Math.max(...rows.map((r) => safe(r.service).length));
444
+ const msW = Math.max(...rows.map((r) => (r.latency_ms !== undefined ? `${r.latency_ms} ms` : '').length));
445
+ const stateW = Math.max(0, ...rows.filter((r) => !healthy(r)).map((r) => safe(r.state).length));
446
+ // Ink for services that answered, muted for the rest with their state; latencies right-aligned.
447
+ const cell = (r) => {
448
+ const name = safe(r.service).padEnd(nameW), ms = (r.latency_ms !== undefined ? `${r.latency_ms} ms` : '').padStart(msW);
449
+ const state = (healthy(r) ? '' : safe(r.state)).padEnd(stateW);
450
+ return { text: healthy(r) ? `${name} ${style.muted(ms)}${' '.repeat(stateW ? stateW + 2 : 0)}` : style.muted(`${name} ${ms}${stateW ? ' ' + state : ''}`),
451
+ width: nameW + 2 + msW + (stateW ? stateW + 2 : 0) };
452
+ };
453
+ // A grid of columns, filled down then across, as wide as the window allows.
454
+ const cells = rows.map(cell), colW = cells[0].width + 4;
455
+ const perRow = Math.max(1, Math.min(4, Math.floor((term.columns - 6) / colW)));
456
+ const height = Math.ceil(cells.length / perRow);
457
+ const lines = Array.from({ length: height }, (_, y) => Array.from({ length: perRow }, (_, x) => cells[x * height + y]).filter(Boolean)
458
+ .map((c, i, all) => c.text + (i < all.length - 1 ? ' '.repeat(colW - c.width) : '')).join(''));
459
+ if (term.caps.motion) {
460
+ // The rows arrive one after another, 40 ms apart: a live answer, not a printout.
461
+ for (let i = 0; i < lines.length; i++) {
462
+ term.frame(lines.slice(0, i + 1));
463
+ await new Promise((r) => setTimeout(r, 40));
464
+ }
465
+ term.commit();
466
+ }
467
+ else
468
+ for (const l of lines)
469
+ term.say(l);
470
+ const up = rows.filter(healthy).length;
471
+ const who = await (async () => (await d.client()).whoami())().catch(() => null);
472
+ this.ok(`${up} of ${rows.length} services answering${who ? ` ${style.muted(`for ${safe(who.org)}`)}` : ''}`);
473
+ return { state: 'done' };
474
+ }
475
+ // ── 6. next ──
476
+ async next() {
477
+ const { term, style } = this;
478
+ const cmd = (c, what) => `${style.bold(c.padEnd(46))} ${style.muted(what)}`;
479
+ await typeLines(term, style, [
480
+ cmd('genesis hives projects create my-app', 'start a project'),
481
+ cmd('genesis foundry models', 'the models you can call'),
482
+ cmd('genesis traces search "…" --idempotency-key k1', 'research with evidence'),
483
+ cmd('genesis --help', 'everything else'),
484
+ ]);
485
+ term.say();
486
+ term.say(`${term.link('https://os.one.ml', 'Console')} ${style.dot} ${term.link('https://os.one.ml/docs', 'Docs')} ${style.dot} ${style.muted('genesis setup <step> redoes a step')}`);
487
+ return { state: 'done' };
488
+ }
489
+ }
490
+ const READ_SCOPES = ['hives:read', 'traces:read', 'beacons:read', 'lanes:read', 'flocks:read', 'flares:read', 'fleets:read', 'gears:read', 'rigs:read', 'runner:read', 'blinds:read'];
491
+ class IamError extends Error {
492
+ status;
493
+ constructor(status, message) {
494
+ super(message);
495
+ this.status = status;
496
+ }
497
+ }
498
+ /** IAM v4's directory, the one the Console's picker reads: paged lists, bearer = your own sign-in. */
499
+ class Iam {
500
+ base;
501
+ f;
502
+ token;
503
+ constructor(base, f, token) {
504
+ this.base = base;
505
+ this.f = f;
506
+ this.token = token;
507
+ }
508
+ async list(path) {
509
+ const out = [];
510
+ let cursor;
511
+ for (let page = 0; page < 20; page++) {
512
+ const u = new URL(path, this.base);
513
+ u.searchParams.set('limit', '100');
514
+ if (cursor)
515
+ u.searchParams.set('cursor', cursor);
516
+ const r = await this.f(u, { headers: { authorization: `Bearer ${await this.token()}`, accept: 'application/json' }, redirect: 'error', signal: AbortSignal.timeout(15_000) });
517
+ if (!r.ok) {
518
+ void r.body?.cancel().catch(() => { });
519
+ throw new IamError(r.status, `IAM ${r.status}`);
520
+ }
521
+ const body = await r.json();
522
+ const items = body.items ?? body.data ?? [];
523
+ out.push(...items.filter((x) => x && typeof x.id === 'string'));
524
+ cursor = body.next_cursor ?? body.nextCursor;
525
+ if (!cursor)
526
+ break;
527
+ }
528
+ return out;
529
+ }
530
+ }
531
+ /** https, or http only on an exact loopback host (the SDK's transport rule), before any request or bearer leaves. */
532
+ function secureUrl(value, what) {
533
+ let u;
534
+ try {
535
+ u = new URL(value);
536
+ }
537
+ catch {
538
+ throw new Error(`${what} is not a URL: ${safe(value)}`);
539
+ }
540
+ const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(u.hostname);
541
+ if (u.protocol !== 'https:' && !(u.protocol === 'http:' && loopback))
542
+ throw new Error(`Refusing ${what} ${safe(value)}: use https.`);
543
+ if (u.username || u.password)
544
+ throw new Error(`Refusing ${what} with credentials in it.`);
545
+ return u.href;
546
+ }
547
+ /** Copy with the platform's clipboard tool. Returns false when there is none. */
548
+ function copy(text) {
549
+ const tool = process.platform === 'darwin' ? ['pbcopy', []] : process.platform === 'win32' ? ['clip', []]
550
+ : process.env.WAYLAND_DISPLAY ? ['wl-copy', []] : ['xclip', ['-selection', 'clipboard']];
551
+ return new Promise((resolve) => {
552
+ try {
553
+ const p = spawn(tool[0], tool[1], { stdio: ['pipe', 'ignore', 'ignore'] });
554
+ p.on('error', () => resolve(false));
555
+ p.on('close', (c) => resolve(c === 0));
556
+ p.stdin.end(text);
557
+ }
558
+ catch {
559
+ resolve(false);
560
+ }
561
+ });
562
+ }
@@ -0,0 +1,35 @@
1
+ export type ClientId = 'claude-code' | 'claude-desktop' | 'cursor' | 'vscode' | 'windsurf' | 'codex';
2
+ export interface Client {
3
+ id: ClientId;
4
+ name: string;
5
+ /** What changes, in words the confirmation shows: a file path or a command line. */
6
+ target: string;
7
+ installed: boolean;
8
+ present: boolean;
9
+ }
10
+ export interface McpEnv {
11
+ home: string;
12
+ platform: NodeJS.Platform;
13
+ appData?: string;
14
+ execPath: string;
15
+ version: string;
16
+ run(cmd: string, args: string[]): Promise<{
17
+ code: number;
18
+ stdout: string;
19
+ }>;
20
+ }
21
+ export declare function defaultMcpEnv(version: string): McpEnv;
22
+ /** The npm package of the MCP server: the one place setup names the package scope. */
23
+ export declare const MCP_PACKAGE = "@onesi/genesis-mcp";
24
+ export declare function paths(e: McpEnv): {
25
+ readonly 'claude-desktop': string;
26
+ readonly cursor: string;
27
+ readonly vscode: string;
28
+ readonly windsurf: string;
29
+ readonly codex: string;
30
+ };
31
+ /** An absolute npx for GUI clients, when one sits next to this Node; otherwise plain npx. */
32
+ export declare function npxFor(e: McpEnv, gui: boolean): Promise<string>;
33
+ export declare function detectClients(e: McpEnv): Promise<Client[]>;
34
+ /** Install into one client. Returns a sentence for the summary, or throws with the reason. */
35
+ export declare function install(e: McpEnv, id: ClientId): Promise<string>;