borgmcp 2.5.0 → 2.6.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 (82) hide show
  1. package/README.md +12 -7
  2. package/dist/assimilate-cmd.d.ts +1 -0
  3. package/dist/assimilate-cmd.d.ts.map +1 -1
  4. package/dist/assimilate-cmd.js +58 -30
  5. package/dist/assimilate-cmd.js.map +1 -1
  6. package/dist/assimilate-deps.d.ts.map +1 -1
  7. package/dist/assimilate-deps.js +2 -0
  8. package/dist/assimilate-deps.js.map +1 -1
  9. package/dist/claude.js +9 -3
  10. package/dist/claude.js.map +1 -1
  11. package/dist/cli-help.d.ts +1 -1
  12. package/dist/cli-help.d.ts.map +1 -1
  13. package/dist/cli-help.js +7 -4
  14. package/dist/cli-help.js.map +1 -1
  15. package/dist/codex-launch.d.ts.map +1 -1
  16. package/dist/codex-launch.js +2 -1
  17. package/dist/codex-launch.js.map +1 -1
  18. package/dist/first-run-server.d.ts +31 -0
  19. package/dist/first-run-server.d.ts.map +1 -0
  20. package/dist/first-run-server.js +117 -0
  21. package/dist/first-run-server.js.map +1 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +25 -27
  24. package/dist/index.js.map +1 -1
  25. package/dist/log-stream.d.ts +4 -8
  26. package/dist/log-stream.d.ts.map +1 -1
  27. package/dist/log-stream.js +22 -15
  28. package/dist/log-stream.js.map +1 -1
  29. package/dist/opencode-drone.d.ts +10 -5
  30. package/dist/opencode-drone.d.ts.map +1 -1
  31. package/dist/opencode-drone.js +241 -48
  32. package/dist/opencode-drone.js.map +1 -1
  33. package/dist/opencode-wake-copy.d.ts +2 -0
  34. package/dist/opencode-wake-copy.d.ts.map +1 -0
  35. package/dist/opencode-wake-copy.js +8 -0
  36. package/dist/opencode-wake-copy.js.map +1 -0
  37. package/dist/regen-format.d.ts.map +1 -1
  38. package/dist/regen-format.js +2 -3
  39. package/dist/regen-format.js.map +1 -1
  40. package/dist/repository-cube-init.d.ts +2 -0
  41. package/dist/repository-cube-init.d.ts.map +1 -1
  42. package/dist/repository-cube-init.js +14 -3
  43. package/dist/repository-cube-init.js.map +1 -1
  44. package/dist/server-facade.d.ts +2 -0
  45. package/dist/server-facade.d.ts.map +1 -1
  46. package/dist/server-facade.js +25 -3
  47. package/dist/server-facade.js.map +1 -1
  48. package/dist/setup.js +10 -0
  49. package/dist/setup.js.map +1 -1
  50. package/dist/stream-status.d.ts +9 -8
  51. package/dist/stream-status.d.ts.map +1 -1
  52. package/dist/stream-status.js +67 -17
  53. package/dist/stream-status.js.map +1 -1
  54. package/dist/update-cmd.d.ts +11 -1
  55. package/dist/update-cmd.d.ts.map +1 -1
  56. package/dist/update-cmd.js +68 -4
  57. package/dist/update-cmd.js.map +1 -1
  58. package/dist/wake-path-health.d.ts +27 -0
  59. package/dist/wake-path-health.d.ts.map +1 -0
  60. package/dist/wake-path-health.js +51 -0
  61. package/dist/wake-path-health.js.map +1 -0
  62. package/docs/EXTRACTION_PROVENANCE.md +3 -3
  63. package/docs/LOCAL_SERVER.md +12 -3
  64. package/docs/RELEASING.md +6 -1
  65. package/package.json +1 -1
  66. package/src/assimilate-cmd.ts +66 -27
  67. package/src/assimilate-deps.ts +9 -0
  68. package/src/claude.ts +10 -3
  69. package/src/cli-help.ts +7 -4
  70. package/src/codex-launch.ts +2 -1
  71. package/src/first-run-server.ts +178 -0
  72. package/src/index.ts +24 -28
  73. package/src/log-stream.ts +36 -25
  74. package/src/opencode-drone.ts +295 -47
  75. package/src/opencode-wake-copy.ts +8 -0
  76. package/src/regen-format.ts +2 -5
  77. package/src/repository-cube-init.ts +16 -3
  78. package/src/server-facade.ts +34 -3
  79. package/src/setup.ts +11 -0
  80. package/src/stream-status.ts +85 -21
  81. package/src/update-cmd.ts +88 -5
  82. package/src/wake-path-health.ts +85 -0
@@ -1,6 +1,9 @@
1
1
  import { spawn as spawnChild, type SpawnOptions } from 'node:child_process';
2
2
  import { constants } from 'node:os';
3
+ import chalk from 'chalk';
3
4
  import { cubeInitHelpText, isHelpFlag, serverHelpText } from './cli-help.js';
5
+ import { consolePrefix } from './console-prefix.js';
6
+ import { getPackageVersion } from './version.js';
4
7
 
5
8
  export const SERVER_LIFECYCLE_COMMANDS = ['setup', 'start', 'stop', 'status', 'update', 'invite', 'dashboard'] as const;
6
9
  export type ServerLifecycleCommand = typeof SERVER_LIFECYCLE_COMMANDS[number];
@@ -12,6 +15,10 @@ export type ParsedServerFacadeArgs =
12
15
  | { kind: 'command'; command: ServerLifecycleCommand; args: string[] }
13
16
  | { kind: 'error'; reason: 'unknown-command'; command: string };
14
17
 
18
+ export function isClientOwnedCubeInitArgv(argv: readonly string[]): boolean {
19
+ return argv[2] === 'server' && argv[3] === 'cube' && argv[4] === 'init';
20
+ }
21
+
15
22
  export function parseServerFacadeArgs(args: readonly string[]): ParsedServerFacadeArgs {
16
23
  const [command, ...rest] = args;
17
24
  if (command === undefined || command === '--help' || command === '-h') {
@@ -92,8 +99,9 @@ export function buildDefaultServerFacadeClientDeps(
92
99
  import('./assimilate-cmd.js'),
93
100
  ]);
94
101
  const parsed = parseAssimilateArgs([...args]);
95
- if (!parsed.ok || parsed.role !== undefined) {
96
- process.stderr.write(`${parsed.ok ? 'borg server cube init does not accept a role' : parsed.error}\n`);
102
+ const unsupported = parsed.ok ? unsupportedCubeInitInput(args, parsed) : undefined;
103
+ if (!parsed.ok || unsupported !== undefined) {
104
+ process.stderr.write(cubeInitUsageErrorText(parsed.ok ? unsupported! : parsed.error));
97
105
  return 1;
98
106
  }
99
107
  return runAssimilate(
@@ -104,6 +112,29 @@ export function buildDefaultServerFacadeClientDeps(
104
112
  };
105
113
  }
106
114
 
115
+ function unsupportedCubeInitInput(
116
+ args: readonly string[],
117
+ parsed: { role: string | undefined },
118
+ ): string | undefined {
119
+ if (parsed.role !== undefined) {
120
+ return 'A role is not accepted because this command does not create a drone.';
121
+ }
122
+ const unsupported = ['--worktree', '--here', '--force', '--cli', '--model', '--backend', '--no-template'];
123
+ const flag = args.find((arg) => unsupported.some((candidate) =>
124
+ arg === candidate || arg.startsWith(`${candidate}=`)));
125
+ return flag === undefined
126
+ ? undefined
127
+ : `${flag.split('=', 1)[0]} is not accepted because this command does not create a drone.`;
128
+ }
129
+
130
+ export function cubeInitUsageErrorText(reason: string): string {
131
+ const sentence = /[.!?]$/.test(reason) ? reason : `${reason}.`;
132
+ return (
133
+ chalk.red(`${consolePrefix()}◼ borg server cube init: ${sentence}\n`) +
134
+ 'Run `borg server cube init --help` for usage.\n'
135
+ );
136
+ }
137
+
107
138
  const defaultClientDeps = buildDefaultServerFacadeClientDeps();
108
139
 
109
140
  const MAX_RENDERED_COMMAND_CODE_POINTS = 80;
@@ -222,7 +253,7 @@ export async function runEarlyServerFacade(
222
253
  return 0;
223
254
  }
224
255
  if (parsed.kind === 'cube-init-help') {
225
- output.writeStdout(cubeInitHelpText());
256
+ output.writeStdout(cubeInitHelpText(getPackageVersion()));
226
257
  return 0;
227
258
  }
228
259
  if (parsed.kind === 'error') {
package/src/setup.ts CHANGED
@@ -37,6 +37,7 @@ import { ensureCliMcpConfigured } from './ensure-mcp-config.js';
37
37
  import { handleVersionFlag } from './version.js';
38
38
  import { initDebugFromArgv } from './debug.js';
39
39
  import { defaultApprovalIo, setupApprovalWarnings } from './cli-tool-approval.js';
40
+ import { offerFirstRunServerInstall } from './first-run-server.js';
40
41
 
41
42
  /**
42
43
  * Main setup wizard
@@ -67,6 +68,16 @@ async function main() {
67
68
  process.exit(1);
68
69
  }
69
70
 
71
+ // Resolve the separately published local server before setup writes agent
72
+ // configuration. Decline/non-interactive/failure paths therefore leave no
73
+ // partial setup state behind.
74
+ console.log(chalk.blue('◼ Local Server'));
75
+ const serverInstall = await offerFirstRunServerInstall();
76
+ if (serverInstall.kind !== 'present' && serverInstall.kind !== 'installed') {
77
+ process.exit(serverInstall.kind === 'declined' ? 0 : 1);
78
+ }
79
+ console.log('');
80
+
70
81
  // Step 1: Configure every detected agent CLI
71
82
  console.log(chalk.blue('◼ Agent CLI Integration'));
72
83
 
@@ -28,6 +28,7 @@ import {
28
28
  HEARTBEAT_STALE_MS,
29
29
  } from './inbox-monitor.js';
30
30
  import { shellEscape } from './shell-escape.js';
31
+ import type { WakePathSnapshot } from './wake-path-health.js';
31
32
 
32
33
  /**
33
34
  * Best-effort check: is a process tailing this inbox file?
@@ -111,11 +112,10 @@ export function isHeartbeatStale(inboxPath: string, monitorStateRoot?: string |
111
112
 
112
113
  export interface RenderInputs {
113
114
  status: StreamStatus;
114
- /**
115
- * Tri-state Monitor liveness: true = healthy, false = wake-path
116
- * broken, null = cannot determine.
117
- */
115
+ /** Legacy Claude Monitor health; runtime-aware callers also pass `wakePath`. */
118
116
  inboxMonitorHealthy: boolean | null;
117
+ /** Runtime-specific wake-path evidence. Omitted by legacy pure-render callers. */
118
+ wakePath?: WakePathSnapshot;
119
119
  /**
120
120
  * Inbox path for the State-5 self-arm instruction. Pass null when
121
121
  * unknown (no active cube); State 5 will then surface the failure
@@ -140,6 +140,12 @@ export interface RenderInputs {
140
140
  export function renderStreamStatus(inputs: RenderInputs): string {
141
141
  const { status, inboxMonitorHealthy, inboxPath, monitorStateRoot, droneLabel, cubeName, humanAgo } =
142
142
  inputs;
143
+ const wakePath = inputs.wakePath ?? {
144
+ agentKind: 'claude' as const,
145
+ healthy: inboxMonitorHealthy,
146
+ openCode: null,
147
+ };
148
+ const wakePathHealthy = wakePath.healthy;
143
149
 
144
150
  const isNotStarted =
145
151
  status.reconnectAttempts === 0 &&
@@ -149,9 +155,8 @@ export function renderStreamStatus(inputs: RenderInputs): string {
149
155
  const orphanedInitialization = status.ownership?.state === 'orphaned-initialization';
150
156
  const ownershipInitializing = status.ownership?.state === 'initializing';
151
157
 
152
- // Top-line verdict — 5 states + override per drone-4 contract.
153
- // Precedence: disconnected > no-inbox-Monitor (wire-down upstream
154
- // cause; State 5 only applies when wire is healthy).
158
+ // Top-line verdict — 5 states + runtime wake-path override.
159
+ // Precedence: disconnected > wake-path failure.
155
160
  let summary: string;
156
161
  if (orphanedInitialization) {
157
162
  summary = '**Stream blocked by an orphaned initialization lock.**';
@@ -161,8 +166,12 @@ export function renderStreamStatus(inputs: RenderInputs): string {
161
166
  summary = '**Stream not started.**';
162
167
  } else if (!status.connected) {
163
168
  summary = `**Stream disconnected (reconnect attempt ${status.reconnectAttempts}).**`;
164
- } else if (inboxMonitorHealthy === false) {
165
- summary = '**Stream connected (no inbox-Monitor — wake path broken).**';
169
+ } else if (wakePathHealthy === false) {
170
+ summary = wakePath.agentKind === 'opencode'
171
+ ? '**Stream connected (OpenCode delivery degraded).**'
172
+ : wakePath.agentKind === 'codex'
173
+ ? '**Stream connected (Codex wake path unavailable).**'
174
+ : '**Stream connected (no inbox-Monitor — wake path broken).**';
166
175
  } else if (status.lastContentEventAt === null) {
167
176
  // State 2: wire works, no content yet. Collapses two underlying
168
177
  // conditions per drone-4 contract — fresh connect pre-first-content
@@ -247,13 +256,42 @@ export function renderStreamStatus(inputs: RenderInputs): string {
247
256
  );
248
257
  }
249
258
 
250
- // State-5 body line + self-arm instruction. Only fires when we
251
- // POSITIVELY detected wake-path breakage AND the wire is up — the
252
- // disconnected case takes precedence above and would never reach
253
- // here. When `inboxMonitorHealthy === null` (couldn't determine) we
254
- // stay silent; surfacing an uncertain failure mode is worse UX than
255
- // omitting it.
256
- if (status.connected && inboxMonitorHealthy === false) {
259
+ if (wakePath.agentKind === 'opencode' && wakePath.openCode) {
260
+ const delivery = wakePath.openCode.deliveryStates;
261
+ lines.push(`- **OpenCode delivery connected**: ${wakePath.openCode.connected}`);
262
+ lines.push(`- **OpenCode queued**: ${delivery.queued}`);
263
+ lines.push(
264
+ `- **OpenCode delivered-unconfirmed**: ${delivery['delivered-unconfirmed']}`
265
+ );
266
+ lines.push(`- **OpenCode retried**: ${delivery.retried}`);
267
+ lines.push(`- **OpenCode failed**: ${delivery.failed}`);
268
+ }
269
+
270
+ // Runtime-specific wake-path warning. The wire-down case takes
271
+ // precedence above; an indeterminate signal remains honest and silent.
272
+ if (status.connected && wakePathHealthy === false) {
273
+ if (wakePath.agentKind === 'opencode') {
274
+ lines.push(
275
+ '- **OpenCode delivery**: _(degraded — inspect the durable unread log)_'
276
+ );
277
+ lines.push('');
278
+ lines.push('## OpenCode wake delivery is degraded');
279
+ lines.push(
280
+ 'One or more durable inbox entries were rejected or could not be confirmed. Run `borg_read-log unread_only=true` and drain the unread log now. Use `borg_stream-status` to check whether failed or delivered-unconfirmed entries remain.'
281
+ );
282
+ return lines.join('\n');
283
+ }
284
+ if (wakePath.agentKind === 'codex') {
285
+ lines.push(
286
+ '- **Codex wake path**: _(remote-control bridge unavailable)_'
287
+ );
288
+ lines.push('');
289
+ lines.push('## Codex wake delivery is unavailable');
290
+ lines.push(
291
+ 'The Codex remote-control bridge cannot currently deliver a wake. Run `borg_read-log unread_only=true` and drain the unread log now, then reconnect the Codex wake path.'
292
+ );
293
+ return lines.join('\n');
294
+ }
257
295
  lines.push(
258
296
  `- **inbox-monitor**: _(no watcher detected — wake path broken)_`
259
297
  );
@@ -278,13 +316,13 @@ export function renderStreamStatus(inputs: RenderInputs): string {
278
316
  * from the inline ternary in `src/index.ts` for direct unit-test
279
317
  * coverage of the (connected × healthy) cross-product).
280
318
  *
281
- * Returns true ONLY when the wire is up AND we positively detected a
282
- * dead inbox Monitor (`=== false` strict). The `null` branch
319
+ * Returns true ONLY when the wire is up AND the runtime-specific wake
320
+ * mechanism is positively unhealthy (`=== false` strict). The `null` branch
283
321
  * (couldn't determine) stays silent — surfacing an uncertain failure
284
322
  * mode is worse UX than omitting it (mirrors the State-5 precedence
285
323
  * rule in `renderStreamStatus`). When disconnected, the wire-down case
286
324
  * is the upstream cause and takes precedence; no point warning about
287
- * the wake path when the wake-path's input has no events to deliver.
325
+ * the wake path when its input has no events to deliver.
288
326
  */
289
327
  export function shouldShowWakePathWarning(
290
328
  streamStatus: StreamStatus,
@@ -294,7 +332,7 @@ export function shouldShowWakePathWarning(
294
332
  }
295
333
 
296
334
  /**
297
- * Wake-path-broken prefix for `borg_regen` output (gh#43).
335
+ * Runtime-specific wake-path-broken prefix for `borg_regen` output.
298
336
  *
299
337
  * Pure function — caller decides whether to call (gates on
300
338
  * `shouldShowWakePathWarning`). Returns an empty string when called
@@ -315,8 +353,34 @@ export function formatWakePathPrefix(inputs: {
315
353
  monitorStateRoot?: string | null;
316
354
  droneLabel: string | null;
317
355
  cubeName: string | null;
356
+ wakePath?: WakePathSnapshot;
318
357
  }): string {
319
- const { inboxPath, monitorStateRoot, droneLabel, cubeName } = inputs;
358
+ const { inboxPath, monitorStateRoot, droneLabel, cubeName, wakePath } = inputs;
359
+ if (wakePath?.agentKind === 'opencode') {
360
+ const delivery = wakePath.openCode?.deliveryStates;
361
+ return [
362
+ '## ⚠ OpenCode wake delivery degraded',
363
+ '',
364
+ `Durable delivery has ${
365
+ delivery
366
+ ? `${delivery['delivered-unconfirmed']} delivered-unconfirmed and ${delivery.failed} failed entries`
367
+ : 'an unhealthy runtime signal'
368
+ }. Run \`borg_stream-status\`, then run \`borg_read-log unread_only=true\` and drain the unread log.`,
369
+ '',
370
+ '---',
371
+ '',
372
+ ].join('\n');
373
+ }
374
+ if (wakePath?.agentKind === 'codex') {
375
+ return [
376
+ '## ⚠ Codex wake path unavailable',
377
+ '',
378
+ 'The remote-control bridge cannot currently deliver a wake. Run `borg_read-log unread_only=true` and drain the unread log, then reconnect the Codex wake path.',
379
+ '',
380
+ '---',
381
+ '',
382
+ ].join('\n');
383
+ }
320
384
  if (!inboxPath || !droneLabel || !cubeName) return '';
321
385
  return [
322
386
  `## ⚠ Wake path broken — arm Monitor NOW`,
package/src/update-cmd.ts CHANGED
@@ -54,7 +54,12 @@ export interface UpdateDeps {
54
54
  name: typeof CLIENT_PACKAGE | typeof SERVER_PACKAGE,
55
55
  version: string,
56
56
  ): Promise<PublishedPackage>;
57
- installGlobal(name: typeof CLIENT_PACKAGE | typeof SERVER_PACKAGE, version: string): Promise<void>;
57
+ publishedVersions(name: typeof CLIENT_PACKAGE | typeof SERVER_PACKAGE): Promise<string[]>;
58
+ installGlobal(
59
+ name: typeof CLIENT_PACKAGE | typeof SERVER_PACKAGE,
60
+ version: string,
61
+ options?: { ignoreScripts?: boolean },
62
+ ): Promise<void>;
58
63
  reenter(binPath: string, args: readonly string[]): Promise<number>;
59
64
  serverJson(binPath: string, command: 'update' | 'status'): Promise<unknown>;
60
65
  verifyRunningProtocol(origin: string): Promise<void>;
@@ -150,7 +155,7 @@ function renderServerState(
150
155
  );
151
156
  }
152
157
 
153
- function isExactSemver(value: unknown): boolean {
158
+ export function isExactSemver(value: unknown): value is string {
154
159
  return typeof value === 'string' && EXACT_SEMVER.test(value);
155
160
  }
156
161
 
@@ -275,6 +280,44 @@ async function publishedPair(
275
280
  return { client, server };
276
281
  }
277
282
 
283
+ /**
284
+ * Resolve the exact published server that can run with an already-installed
285
+ * client. First-run onboarding uses this instead of inventing a second
286
+ * registry path or installing an unpinned `latest` spec.
287
+ */
288
+ export async function resolveCompatibleServerTarget(
289
+ clientSharedVersion: string,
290
+ deps: Pick<UpdateDeps, 'publishedPackage' | 'publishedVersions'>,
291
+ ): Promise<PublishedPackage> {
292
+ if (!isExactSemver(clientSharedVersion)) {
293
+ throw new Error(`installed client has an invalid ${SHARED_PACKAGE} pin`);
294
+ }
295
+ const stableVersions = (await deps.publishedVersions(SERVER_PACKAGE))
296
+ .filter((version) => isExactSemver(version) && !version.includes('-'))
297
+ .sort(compareStableSemverDescending);
298
+ for (const version of stableVersions) {
299
+ const server = await deps.publishedPackage(SERVER_PACKAGE, version);
300
+ validatePublishedPackage(server, SERVER_PACKAGE);
301
+ if (server.version !== version) {
302
+ throw new Error(`registry returned the wrong ${SERVER_PACKAGE} manifest version`);
303
+ }
304
+ if (server.sharedVersion === clientSharedVersion) return server;
305
+ }
306
+ throw new Error(
307
+ `no published ${SERVER_PACKAGE} release uses ${SHARED_PACKAGE}@${clientSharedVersion}`,
308
+ );
309
+ }
310
+
311
+ function compareStableSemverDescending(left: string, right: string): number {
312
+ const leftParts = left.split('+', 1)[0].split('.').map((part) => BigInt(part));
313
+ const rightParts = right.split('+', 1)[0].split('.').map((part) => BigInt(part));
314
+ for (let index = 0; index < 3; index += 1) {
315
+ if (leftParts[index] > rightParts[index]) return -1;
316
+ if (leftParts[index] < rightParts[index]) return 1;
317
+ }
318
+ return right.localeCompare(left);
319
+ }
320
+
278
321
  function assertInstalled(
279
322
  installed: InstalledPackage,
280
323
  published: PublishedPackage,
@@ -533,7 +576,11 @@ export async function runUpdate(options: UpdateOptions, deps: UpdateDeps): Promi
533
576
  if (client.version !== pair.client.version || client.sharedVersion !== pair.client.sharedVersion) {
534
577
  let installedClient: InstalledPackage | null = null;
535
578
  try {
536
- await deps.installGlobal(CLIENT_PACKAGE, pair.client.version);
579
+ await deps.installGlobal(
580
+ CLIENT_PACKAGE,
581
+ pair.client.version,
582
+ { ignoreScripts: true },
583
+ );
537
584
  installedClient = await deps.currentClient();
538
585
  assertInstalled(installedClient, pair.client);
539
586
  const args = [
@@ -597,7 +644,11 @@ export async function runUpdate(options: UpdateOptions, deps: UpdateDeps): Promi
597
644
  discoveredServer.version !== pair.server.version ||
598
645
  discoveredServer.sharedVersion !== pair.server.sharedVersion
599
646
  ) {
600
- await deps.installGlobal(SERVER_PACKAGE, pair.server.version);
647
+ await deps.installGlobal(
648
+ SERVER_PACKAGE,
649
+ pair.server.version,
650
+ { ignoreScripts: true },
651
+ );
601
652
  }
602
653
  const verified = await deps.currentServer();
603
654
  if (!verified) throw new Error('server controller disappeared after installation');
@@ -920,6 +971,36 @@ async function defaultPublishedPackage(
920
971
  return published;
921
972
  }
922
973
 
974
+ async function defaultPublishedVersions(
975
+ name: typeof CLIENT_PACKAGE | typeof SERVER_PACKAGE,
976
+ context: NpmContext,
977
+ ): Promise<string[]> {
978
+ void context;
979
+ const endpoint = new URL(encodeURIComponent(name), CANONICAL_NPM_REGISTRY);
980
+ try {
981
+ const response = await fetch(endpoint, {
982
+ headers: { Accept: 'application/json' },
983
+ redirect: 'error',
984
+ });
985
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
986
+ const parsed: unknown = await response.json();
987
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
988
+ throw new Error('response was not a package object');
989
+ }
990
+ const versions = (parsed as Record<string, unknown>).versions;
991
+ if (!versions || typeof versions !== 'object' || Array.isArray(versions)) {
992
+ throw new Error('package versions were not an object');
993
+ }
994
+ const values = Object.keys(versions);
995
+ if (values.length === 0 || values.some((version) => !isExactSemver(version))) {
996
+ throw new Error('package versions were missing or invalid');
997
+ }
998
+ return values;
999
+ } catch {
1000
+ throw new Error(`registry version lookup failed for ${name}`);
1001
+ }
1002
+ }
1003
+
923
1004
  async function defaultConfirm(message: string): Promise<'yes' | 'no' | 'eof' | 'interrupted'> {
924
1005
  const rl = createInterface({ input: process.stdin, output: process.stdout });
925
1006
  let interrupted = false;
@@ -958,11 +1039,13 @@ export function buildDefaultUpdateDeps(): UpdateDeps {
958
1039
  await context(),
959
1040
  ),
960
1041
  publishedPackage: async (name, version) => defaultPublishedPackage(name, version, await context()),
961
- installGlobal: async (name, version) => {
1042
+ publishedVersions: async (name) => defaultPublishedVersions(name, await context()),
1043
+ installGlobal: async (name, version, options) => {
962
1044
  const npm = await context();
963
1045
  const result = await runCommand(npm.commandPath, [
964
1046
  'install',
965
1047
  '--global',
1048
+ ...(options?.ignoreScripts ? ['--ignore-scripts'] : []),
966
1049
  `--prefix=${npm.prefix}`,
967
1050
  `--registry=${CANONICAL_NPM_REGISTRY}`,
968
1051
  `${name}@${version}`,
@@ -0,0 +1,85 @@
1
+ import { probeCodexBridgeArmed } from './codex-app-wake.js';
2
+ import { checkInboxMonitorHealthy } from './stream-status.js';
3
+ import {
4
+ getOpenCodeConnectionState,
5
+ type OpenCodeConnectionState,
6
+ } from './opencode-drone.js';
7
+ import type { AgentKind } from './agent-runtime.js';
8
+
9
+ export interface WakePathSnapshot {
10
+ agentKind: AgentKind;
11
+ healthy: boolean | null;
12
+ openCode: OpenCodeConnectionState | null;
13
+ }
14
+
15
+ interface InspectWakePathInputs {
16
+ agentKind: AgentKind;
17
+ active: { cubeId: string; droneId: string } | null;
18
+ inboxPath: string | null;
19
+ monitorStateRoot: string | null;
20
+ }
21
+
22
+ interface InspectWakePathDeps {
23
+ checkClaudeMonitor?: typeof checkInboxMonitorHealthy;
24
+ probeCodex?: typeof probeCodexBridgeArmed;
25
+ getOpenCodeState?: typeof getOpenCodeConnectionState;
26
+ }
27
+
28
+ export function openCodeWakePathHealthy(
29
+ state: OpenCodeConnectionState,
30
+ ): boolean | null {
31
+ if (!state.connected) return false;
32
+ if (
33
+ state.deliveryStates.failed > 0 ||
34
+ state.deliveryStates['delivered-unconfirmed'] > 0
35
+ ) {
36
+ return false;
37
+ }
38
+ if (
39
+ state.deliveryStates.queued > 0 ||
40
+ state.deliveryStates.retried > 0
41
+ ) {
42
+ return null;
43
+ }
44
+ if (state.sessionId === null) return null;
45
+ return true;
46
+ }
47
+
48
+ export async function inspectWakePath(
49
+ inputs: InspectWakePathInputs,
50
+ deps: InspectWakePathDeps = {},
51
+ ): Promise<WakePathSnapshot> {
52
+ if (!inputs.active) {
53
+ return {
54
+ agentKind: inputs.agentKind,
55
+ healthy: null,
56
+ openCode: null,
57
+ };
58
+ }
59
+
60
+ if (inputs.agentKind === 'claude') {
61
+ const check = deps.checkClaudeMonitor ?? checkInboxMonitorHealthy;
62
+ return {
63
+ agentKind: inputs.agentKind,
64
+ healthy: check(inputs.inboxPath, inputs.monitorStateRoot),
65
+ openCode: null,
66
+ };
67
+ }
68
+
69
+ if (inputs.agentKind === 'codex') {
70
+ const probe = deps.probeCodex ?? probeCodexBridgeArmed;
71
+ return {
72
+ agentKind: inputs.agentKind,
73
+ healthy: await probe(inputs.active),
74
+ openCode: null,
75
+ };
76
+ }
77
+
78
+ const getState = deps.getOpenCodeState ?? getOpenCodeConnectionState;
79
+ const openCode = getState();
80
+ return {
81
+ agentKind: inputs.agentKind,
82
+ healthy: openCodeWakePathHealthy(openCode),
83
+ openCode,
84
+ };
85
+ }