@ours.network/install 0.12.0-nightly.2 → 0.12.0-nightly.20

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/install.mjs CHANGED
@@ -3,7 +3,8 @@
3
3
  // bootstrap, and the `ours-install` command once the stack is on the machine).
4
4
  //
5
5
  // ONE installer for the WHOLE stack — ours core (the daemon) + the harness plugins (Claude Code /
6
- // Codex) + ours-fleet + the Telegram connector — for someone who ALREADY has Claude and/or Codex.
6
+ // Codex / Hermes) + ours-fleet + the Telegram connector — for someone who ALREADY has Claude,
7
+ // Codex, and/or Hermes.
7
8
  // Its whole job: install the stack cleanly, then hand back ONE copy-paste prompt the user drops
8
9
  // into their agent to finish all real configuration conversationally. No tokens, no port editing,
9
10
  // no config files. See packages/installer/README.md and the UX spec for the full contract.
@@ -16,7 +17,7 @@
16
17
  // installed/started/restarted — it prints exactly what it WOULD do. That is the safe way to walk
17
18
  // the whole flow on a machine you don't want to touch (and how the tests drive it).
18
19
  import { spawn, spawnSync } from 'node:child_process';
19
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
20
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
20
21
  import { homedir, userInfo, platform as osPlatform, release as osRelease } from 'node:os';
21
22
  import { join, dirname } from 'node:path';
22
23
  import { banner, heading, ok, info, warn, c, box, withSpinner, openTty, makeWriter, closeSync } from './lib/ui.mjs';
@@ -24,10 +25,14 @@ import { askLine, askYesNo, isCancel } from './lib/prompt.mjs';
24
25
  import {
25
26
  suggestPort, parsePort, validateBroker, mergeConfig, parseVersion, parseStatus,
26
27
  detectPlatform, classifyHarnessProbe, buildHandoffPrompt,
27
- DEFAULT_PORT,
28
+ DEFAULT_PORT, resolveChannel, pkgSpec,
28
29
  } from './lib/logic.mjs';
29
30
 
30
31
  const NPM = process.env.OURS_NPM || 'npm';
32
+ // Release channel: OURS_CHANNEL=nightly installs @nightly for mcp/tg-connector/plugin
33
+ // launchers but keeps @ours.network/fleet at @latest (fleet has no nightly). Default: latest.
34
+ const CHANNEL = resolveChannel(process.env.OURS_CHANNEL || process.env.OURS_INSTALL_CHANNEL);
35
+ const spec = (pkgKey) => pkgSpec(pkgKey, CHANNEL); // → "@ours.network/<key>@<tag>"
31
36
  let DRY = !!process.env.OURS_INSTALL_DRY_RUN;
32
37
  const SELFHOST_URL = 'ours.network';
33
38
  const CLAUDE_MARKET = 'adapt-toolkit/ours-claude-marketplace';
@@ -131,6 +136,21 @@ function detectHarness(name) {
131
136
  return { name, ...verdict };
132
137
  }
133
138
 
139
+ // Hermes detection is DIFFERENT from Claude Code / Codex: Hermes has no driven CLI. Its ours plugin
140
+ // (`ours-hermes-install`) never calls a `hermes` binary — it just writes ~/.hermes/config.yaml + the
141
+ // skills — so "is it drivable?" is the wrong question. Per the Hermes plugin's own prerequisites,
142
+ // presence == the config dir (~/.hermes, override with HERMES_DIR) exists. We still run the alias-safe
143
+ // CLI probe in case a real `hermes` command is on PATH, purely to enrich detection; either signal
144
+ // makes it installable. No config dir and no CLI → absent (skipped, like an uninstalled harness).
145
+ function detectHermes() {
146
+ const dir = process.env.HERMES_DIR || join(homedir(), '.hermes');
147
+ const dirPresent = existsSync(dir);
148
+ const cli = detectHarness('hermes'); // best-effort — Hermes usually has no `--version` CLI
149
+ const status = dirPresent || cli.status === 'ok' ? 'ok' : 'absent';
150
+ const detail = dirPresent ? `config dir ${dir} present` : cli.detail;
151
+ return { name: 'hermes', label: 'Hermes', status, detail };
152
+ }
153
+
134
154
  // Set by main() so the top-level catch can route a Ctrl+C (InstallCancelled) through the same
135
155
  // clean-exit path as the SIGINT handler.
136
156
  let cancelHandler = null;
@@ -148,7 +168,7 @@ const USAGE = `ours-install — the unified ours.network stack installer.
148
168
  ours-install [--dry-run] [--help] [--version]
149
169
 
150
170
  Guided ~3-minute setup for the whole stack: ours core (the daemon), the harness
151
- plugins (Claude Code + Codex), ours-fleet, and the Telegram connector — then one
171
+ plugins (Claude Code + Codex + Hermes), ours-fleet, and the Telegram connector — then one
152
172
  copy-paste hand-off prompt. You approve each step; re-run any time to add a piece
153
173
  or update.
154
174
 
@@ -221,7 +241,10 @@ async function main() {
221
241
  { name: 'claude', label: 'Claude Code' },
222
242
  { name: 'codex', label: 'Codex' },
223
243
  ];
244
+ // Claude Code + Codex are driven CLIs (alias-safe --version probe); Hermes is config-dir based
245
+ // (see detectHermes) so it gets its own detector, appended after them.
224
246
  const harnesses = harnessSpecs.map((h) => ({ ...h, ...detectHarness(h.name) }));
247
+ harnesses.push(detectHermes());
225
248
  for (const h of harnesses) {
226
249
  if (h.status === 'ok') line(ok(`'${h.name}' → real program (its plugin can be installed)`));
227
250
  else if (h.status === 'alias') line(warn(`'${h.name}' → ${h.detail} (I won't call it — see the note below; you can still install it by hand)`));
@@ -232,7 +255,7 @@ async function main() {
232
255
  const anyHarness = harnesses.some((h) => h.status !== 'absent');
233
256
  if (!anyHarness) {
234
257
  line('');
235
- line(warn('No Claude Code or Codex found on this machine.'));
258
+ line(warn('No Claude Code, Codex, or Hermes found on this machine.'));
236
259
  line(info('Install one of them first, then re-run ours-install to wire it up.'));
237
260
  finish(ttyFd); return;
238
261
  }
@@ -317,7 +340,7 @@ async function main() {
317
340
  // Without a daemon the rest is moot; go straight to the summary.
318
341
  return endScreen({ ttyFd, summary, chosenPort, chosenBroker });
319
342
  }
320
- await actSpin('ensuring @ours.network/mcp@latest…', 'npm i -g @ours.network/mcp@latest', () => runAsync(NPM, ['i', '-g', '@ours.network/mcp@latest']));
343
+ await actSpin(`ensuring ${spec('mcp')}…`, `npm i -g ${spec('mcp')}`, () => runAsync(NPM, ['i', '-g', spec('mcp')]));
321
344
  const patch = { port: chosenPort };
322
345
  if (chosenBroker) patch.brokerUrl = chosenBroker;
323
346
  await act(`write config (${configPath()}) with port ${chosenPort}${chosenBroker ? ' + custom broker' : ''}`, async () => { writeConfigPatch(patch); return { ok: true }; });
@@ -332,7 +355,7 @@ async function main() {
332
355
  const running = daemonRunning();
333
356
  const upd = yes(` ours core is installed (${before || '?'}) — check for an update now?`, false);
334
357
  if (upd) {
335
- await actSpin('updating @ours.network/mcp@latest…', 'npm i -g @ours.network/mcp@latest', () => runAsync(NPM, ['i', '-g', '@ours.network/mcp@latest']));
358
+ await actSpin(`updating ${spec('mcp')}…`, `npm i -g ${spec('mcp')}`, () => runAsync(NPM, ['i', '-g', spec('mcp')]));
336
359
  const after = parseVersion(daemonVersionLine());
337
360
  if (before && after && before !== after) {
338
361
  await act(`ours-mcp restart (now v${after})`, async () => { if (!run('ours-mcp', ['restart']).ok) run('ours-mcp', ['start']); return { ok: true }; });
@@ -413,7 +436,7 @@ async function main() {
413
436
  const add = await act(`codex plugin marketplace add ${CODEX_MARKET}`, async () => run('codex', ['plugin', 'marketplace', 'add', CODEX_MARKET], { capture: true }));
414
437
  const inst = add.ok ? await act('codex plugin add ours@ours-codex-marketplace', async () => run('codex', ['plugin', 'add', 'ours@ours-codex-marketplace'], { capture: true })) : add;
415
438
  // Owner-mandated: choosing the Codex plugin ALSO installs the ours-codex live launcher, same step.
416
- const wrap = inst.ok ? await actSpin('installing the ours-codex live launcher…', 'npm i -g @ours.network/codex@latest (provides ours-codex)', () => runAsync(NPM, ['i', '-g', '@ours.network/codex@latest'])) : inst;
439
+ const wrap = inst.ok ? await actSpin('installing the ours-codex live launcher…', `npm i -g ${spec('codex')} (provides ours-codex)`, () => runAsync(NPM, ['i', '-g', spec('codex')])) : inst;
417
440
  if (inst.ok && wrap.ok) {
418
441
  line(ok(`Codex plugin + ours-codex live launcher installed — pointed at port ${chosenPort}. No problems.`));
419
442
  // Plain-language: what ours-codex is and why you'd use it (background wake vs blocking).
@@ -427,17 +450,39 @@ async function main() {
427
450
  } else { failCodex(); record({ key: 'codex', label: 'Codex plugin + ours-codex', state: 'failed', note: 'marketplace/install step failed' }); }
428
451
  return true;
429
452
  }
453
+ // Hermes: NOT a driven CLI. Its plugin install is `npm i -g @ours.network/hermes@<channel>` then
454
+ // `ours-hermes-install`, which writes ~/.hermes/config.yaml (the ours MCP server) + the skills. No
455
+ // marketplace/plugin-add, no alias-safety gate — nothing here calls the `hermes` binary. We pass
456
+ // --skip-daemon because the unified installer already owns the daemon (Step 1, chosen port);
457
+ // ours-hermes-install would otherwise re-ensure/restart it. Same never-dead-end contract as above.
458
+ async function installHermes() {
459
+ const go = yes(' Install the ours plugin into Hermes?', true);
460
+ if (!go) { line(info('skipped — re-run ours-install to add it.')); record({ key: 'hermes', label: 'Hermes plugin', state: 'skipped' }); return false; }
461
+ const npmOk = await actSpin(`installing ${spec('hermes')}…`, `npm i -g ${spec('hermes')} (provides ours-hermes-install)`, () => runAsync(NPM, ['i', '-g', spec('hermes')]));
462
+ const inst = npmOk.ok
463
+ ? await act('ours-hermes-install --skip-daemon (writes ~/.hermes: ours MCP server + skills)', async () => run('ours-hermes-install', ['--skip-daemon'], { capture: true }))
464
+ : npmOk;
465
+ if (inst.ok) {
466
+ line(ok('Hermes plugin installed — the ours MCP server + skills are registered in ~/.hermes. No problems.'));
467
+ line(info("(run '/reload-mcp' in Hermes to load the ours tools.)"));
468
+ record({ key: 'hermes', label: 'Hermes plugin', state: 'installed', note: 'run /reload-mcp' });
469
+ } else { failHermes(); record({ key: 'hermes', label: 'Hermes plugin', state: 'failed', note: 'npm/ours-hermes-install step failed' }); }
470
+ return true;
471
+ }
430
472
 
431
473
  // ============================================================================================
432
- // STEP 2 / 4 — harness plugins (Claude Code + Codex). The installer drives the plugin CLIs.
474
+ // STEP 2 / 4 — harness plugins (Claude Code + Codex + Hermes). The installer drives the plugin
475
+ // CLIs for Claude/Codex; Hermes installs via npm + ours-hermes-install (no CLI driving).
433
476
  // ============================================================================================
434
477
  line(heading('2/4 — harness plugins'));
435
- line(info('These teach Claude Code and Codex the ours skills, so you can just talk to your agent'));
436
- line(info("to message people and set things up. I'll install them for you — no commands to type."));
478
+ line(info('These teach Claude Code, Codex, and Hermes the ours skills, so you can just talk to your'));
479
+ line(info("agent to message people and set things up. I'll install them for you — no commands to type."));
437
480
  for (const h of harnesses) {
438
481
  if (h.status === 'absent') continue; // nothing to offer; pre-flight already noted it
439
482
  line('');
440
- const acted = h.name === 'claude' ? await installClaude(h) : await installCodex(h);
483
+ const acted = h.name === 'claude' ? await installClaude(h)
484
+ : h.name === 'codex' ? await installCodex(h)
485
+ : await installHermes(h);
441
486
  cont(acted);
442
487
  }
443
488
 
@@ -451,7 +496,8 @@ async function main() {
451
496
  line(info('over Telegram so they talk to each other — and it all configures maximally easily.'));
452
497
  const goFleet = yes(' Install it?', true);
453
498
  if (goFleet) {
454
- await actSpin('installing @ours.network/fleet@latest…', 'npm i -g @ours.network/fleet@latest', () => runAsync(NPM, ['i', '-g', '@ours.network/fleet@latest']));
499
+ // ours-fleet is ALWAYS @latest — it has no nightly tag (pkgSpec pins it even under OURS_CHANNEL=nightly).
500
+ await actSpin(`installing ${spec('fleet')}…`, `npm i -g ${spec('fleet')}`, () => runAsync(NPM, ['i', '-g', spec('fleet')]));
455
501
  const init = await act('ours-fleet init (one-time host setup: units, dirs, linger)', async () => run('ours-fleet', ['init']));
456
502
  if (!init.ok) line(warn(`ours-fleet host setup didn't finish — retry '${c.cyan('ours-fleet init')}'.`));
457
503
 
@@ -467,7 +513,7 @@ async function main() {
467
513
  const r = await act(`claude plugin install ${CLAUDE_FLEET_PLUGIN}`, async () => run('claude', ['plugin', 'install', CLAUDE_FLEET_PLUGIN], { capture: true }));
468
514
  if (r.ok) { line(ok('Claude Code fleet plugin installed — you can spawn agents from Claude Code. No problems.')); fleetIn.push('Claude Code'); }
469
515
  else failClaudeFleet();
470
- } else { // codex
516
+ } else if (h.name === 'codex') {
471
517
  if (h.status !== 'ok') { manualCodexFleet(h); continue; }
472
518
  await act(`codex plugin marketplace add ${CODEX_MARKET}`, async () => run('codex', ['plugin', 'marketplace', 'add', CODEX_MARKET], { capture: true }));
473
519
  const r = await act(`codex plugin add ${CODEX_FLEET_PLUGIN}`, async () => run('codex', ['plugin', 'add', CODEX_FLEET_PLUGIN], { capture: true }));
@@ -478,7 +524,10 @@ async function main() {
478
524
  // Only claim the skill is present where the fleet plugin actually installed.
479
525
  if (init.ok && fleetIn.length) line(ok(`ours-fleet ready — ${fleetIn.join(' + ')} now know the fleet skill. No problems.`));
480
526
  else if (init.ok) line(info('ours-fleet CLI is installed; add the fleet plugin to your harness with the commands above.'));
481
- const fleetOk = init.ok && (fleetIn.length > 0 || harnesses.every((h) => h.status === 'absent'));
527
+ // Fleet plugins target Claude Code + Codex only (Hermes has none) — so "no fleet-capable harness
528
+ // to install into" means every claude/codex is absent, NOT every harness (Hermes doesn't count).
529
+ const fleetCapableAbsent = harnesses.filter((h) => h.name === 'claude' || h.name === 'codex').every((h) => h.status === 'absent');
530
+ const fleetOk = init.ok && (fleetIn.length > 0 || fleetCapableAbsent);
482
531
  record({ key: 'fleet', label: 'ours-fleet', state: fleetOk ? 'installed' : 'failed', version: globalVersion('@ours.network/fleet'), note: fleetIn.length ? fleetIn.join(' + ') : (init.ok ? 'CLI only — add plugin manually' : 'ours-fleet init failed') });
483
532
  } else {
484
533
  line(info('skipped cleanly — re-run ours-install any time to add it.'));
@@ -494,7 +543,7 @@ async function main() {
494
543
  line(info("Telegram. (You'll set up the actual bot later, with your agent — not here.)"));
495
544
  const goTg = yes(' Install it?', false);
496
545
  if (goTg) {
497
- await actSpin('installing @ours.network/tg-connector@latest…', 'npm i -g @ours.network/tg-connector@latest', () => runAsync(NPM, ['i', '-g', '@ours.network/tg-connector@latest']));
546
+ await actSpin(`installing ${spec('tg-connector')}…`, `npm i -g ${spec('tg-connector')}`, () => runAsync(NPM, ['i', '-g', spec('tg-connector')]));
498
547
  const asService = yes(' Keep it running in the background so it starts automatically on boot?', true);
499
548
  if (asService) {
500
549
  const svc = await act('ours-tg-connector install-service (starts on boot)', async () => run('ours-tg-connector', ['install-service']));
@@ -555,6 +604,13 @@ function failCodex() {
555
604
  line(' ' + c.cyan('npm i -g @ours.network/codex'));
556
605
  line(info('Your daemon and other steps are intact. Continuing.'));
557
606
  }
607
+ function failHermes() {
608
+ line(warn('Couldn\'t install the Hermes plugin automatically (network or npm).'));
609
+ line(info('Install it by hand — run these two, then run /reload-mcp in Hermes:'));
610
+ line(' ' + c.cyan('npm i -g @ours.network/hermes'));
611
+ line(' ' + c.cyan('ours-hermes-install'));
612
+ line(info('Your daemon and other steps are intact. Continuing.'));
613
+ }
558
614
  // --- fleet HARNESS PLUGIN never-dead-end messaging ---------------------------------------------
559
615
  function manualClaudeFleet(h) {
560
616
  line(warn(h.status === 'alias'
package/lib/logic.mjs CHANGED
@@ -26,6 +26,45 @@ export function canonHarnesses(raw) {
26
26
  return { names, unknown };
27
27
  }
28
28
 
29
+ // ── Release CHANNEL / npm dist-tag selection (owner 2026-07-17) ─────────────────
30
+ // The installer normally installs everything at @latest (stable). Setting
31
+ // OURS_CHANNEL=nightly (or OURS_INSTALL_CHANNEL) makes it install the NIGHTLY tag
32
+ // for the packages that HAVE a nightly (mcp, tg-connector, and the harness-plugin
33
+ // launchers claude-code/codex/hermes — all lockstep-published to the `nightly` tag),
34
+ // but keep @ours.network/fleet at @latest ALWAYS: ours-fleet lives in its own repo
35
+ // and publishes NO nightly tag, so `@nightly` there would 404 the whole install.
36
+ export const DEFAULT_CHANNEL = 'latest';
37
+
38
+ // Packages that follow the selected channel (nightly ⇒ @nightly). Short keys map to
39
+ // the @ours.network/<key> npm name. NOTE fleet is deliberately ABSENT — it is pinned.
40
+ const CHANNEL_TRACKING_PKGS = new Set(['mcp', 'tg-connector', 'claude-code', 'codex', 'hermes']);
41
+ // Packages ALWAYS pinned to @latest regardless of channel (no nightly tag exists).
42
+ const STABLE_ONLY_PKGS = new Set(['fleet']);
43
+
44
+ // Normalize a raw channel selection to 'latest' | 'nightly'. Anything unrecognized
45
+ // (incl. undefined/'') falls back to the safe default 'latest' — never guesses a tag.
46
+ export function resolveChannel(raw) {
47
+ const v = String(raw || '').trim().toLowerCase();
48
+ if (v === 'nightly' || v === 'prerelease' || v === 'next') return 'nightly';
49
+ return DEFAULT_CHANNEL; // 'latest' and everything else
50
+ }
51
+
52
+ // The npm dist-tag to install for one package key under a channel. fleet is ALWAYS
53
+ // 'latest'; channel-tracking packages take the channel; anything else defaults to 'latest'.
54
+ export function pkgTag(pkgKey, channel = DEFAULT_CHANNEL) {
55
+ const key = String(pkgKey || '').replace(/^@ours\.network\//, '');
56
+ if (STABLE_ONLY_PKGS.has(key)) return 'latest';
57
+ const ch = resolveChannel(channel);
58
+ if (ch === 'nightly' && CHANNEL_TRACKING_PKGS.has(key)) return 'nightly';
59
+ return 'latest';
60
+ }
61
+
62
+ // Full `@ours.network/<key>@<tag>` spec for `npm i -g`, honoring the channel.
63
+ export function pkgSpec(pkgKey, channel = DEFAULT_CHANNEL) {
64
+ const key = String(pkgKey || '').replace(/^@ours\.network\//, '');
65
+ return `@ours.network/${key}@${pkgTag(key, channel)}`;
66
+ }
67
+
29
68
  // The Telegram connector owns 3051 — the installer must never hand a daemon that port.
30
69
  export const RESERVED_PORTS = [3051];
31
70
  export const DEFAULT_PORT = 3050;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/install",
3
- "version": "0.12.0-nightly.2",
3
+ "version": "0.12.0-nightly.20",
4
4
  "private": false,
5
5
  "description": "The unified ours.network stack installer (ours-install): one guided ~3-minute flow for ours core (the daemon) + the harness plugins (Claude Code / Codex) + ours-fleet + the Telegram connector, then a single copy-paste hand-off prompt. Self-contained (Node built-ins only); run as `ours-install` or via curl|bash (install.sh).",
6
6
  "type": "module",