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.
- package/README.md +12 -7
- package/dist/assimilate-cmd.d.ts +1 -0
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +58 -30
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +2 -0
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/claude.js +9 -3
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +7 -4
- package/dist/cli-help.js.map +1 -1
- package/dist/codex-launch.d.ts.map +1 -1
- package/dist/codex-launch.js +2 -1
- package/dist/codex-launch.js.map +1 -1
- package/dist/first-run-server.d.ts +31 -0
- package/dist/first-run-server.d.ts.map +1 -0
- package/dist/first-run-server.js +117 -0
- package/dist/first-run-server.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -27
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts +4 -8
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +22 -15
- package/dist/log-stream.js.map +1 -1
- package/dist/opencode-drone.d.ts +10 -5
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +241 -48
- package/dist/opencode-drone.js.map +1 -1
- package/dist/opencode-wake-copy.d.ts +2 -0
- package/dist/opencode-wake-copy.d.ts.map +1 -0
- package/dist/opencode-wake-copy.js +8 -0
- package/dist/opencode-wake-copy.js.map +1 -0
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js +2 -3
- package/dist/regen-format.js.map +1 -1
- package/dist/repository-cube-init.d.ts +2 -0
- package/dist/repository-cube-init.d.ts.map +1 -1
- package/dist/repository-cube-init.js +14 -3
- package/dist/repository-cube-init.js.map +1 -1
- package/dist/server-facade.d.ts +2 -0
- package/dist/server-facade.d.ts.map +1 -1
- package/dist/server-facade.js +25 -3
- package/dist/server-facade.js.map +1 -1
- package/dist/setup.js +10 -0
- package/dist/setup.js.map +1 -1
- package/dist/stream-status.d.ts +9 -8
- package/dist/stream-status.d.ts.map +1 -1
- package/dist/stream-status.js +67 -17
- package/dist/stream-status.js.map +1 -1
- package/dist/update-cmd.d.ts +11 -1
- package/dist/update-cmd.d.ts.map +1 -1
- package/dist/update-cmd.js +68 -4
- package/dist/update-cmd.js.map +1 -1
- package/dist/wake-path-health.d.ts +27 -0
- package/dist/wake-path-health.d.ts.map +1 -0
- package/dist/wake-path-health.js +51 -0
- package/dist/wake-path-health.js.map +1 -0
- package/docs/EXTRACTION_PROVENANCE.md +3 -3
- package/docs/LOCAL_SERVER.md +12 -3
- package/docs/RELEASING.md +6 -1
- package/package.json +1 -1
- package/src/assimilate-cmd.ts +66 -27
- package/src/assimilate-deps.ts +9 -0
- package/src/claude.ts +10 -3
- package/src/cli-help.ts +7 -4
- package/src/codex-launch.ts +2 -1
- package/src/first-run-server.ts +178 -0
- package/src/index.ts +24 -28
- package/src/log-stream.ts +36 -25
- package/src/opencode-drone.ts +295 -47
- package/src/opencode-wake-copy.ts +8 -0
- package/src/regen-format.ts +2 -5
- package/src/repository-cube-init.ts +16 -3
- package/src/server-facade.ts +34 -3
- package/src/setup.ts +11 -0
- package/src/stream-status.ts +85 -21
- package/src/update-cmd.ts +88 -5
- package/src/wake-path-health.ts +85 -0
package/src/server-facade.ts
CHANGED
|
@@ -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
|
-
|
|
96
|
-
|
|
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
|
|
package/src/stream-status.ts
CHANGED
|
@@ -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 +
|
|
153
|
-
// Precedence: disconnected >
|
|
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 (
|
|
165
|
-
summary =
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
282
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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):
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
+
}
|