borgmcp 2.4.1 → 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 +6 -4
- 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/server-handshake.d.ts +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 +69 -5
- 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 +8 -8
- package/docs/LOCAL_SERVER.md +14 -5
- package/docs/RELEASING.md +16 -6
- package/package.json +2 -2
- 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 +6 -6
- package/src/repository-cube-init.ts +16 -3
- package/src/server-facade.ts +34 -3
- package/src/server-handshake.ts +1 -1
- package/src/setup.ts +11 -0
- package/src/stream-status.ts +85 -21
- package/src/update-cmd.ts +89 -6
- package/src/wake-path-health.ts +85 -0
package/src/assimilate-cmd.ts
CHANGED
|
@@ -189,6 +189,9 @@ export interface AssimilateDeps {
|
|
|
189
189
|
prompt: (message: string) => Promise<string>;
|
|
190
190
|
promptSecret: (message: string) => Promise<string>;
|
|
191
191
|
isTTY: () => boolean;
|
|
192
|
+
ensureLocalServerInstalled: (connectCommand: string) => Promise<
|
|
193
|
+
'present' | 'installed' | 'declined' | 'non-interactive' | 'failed'
|
|
194
|
+
>;
|
|
192
195
|
/** Selected-harness approval inspection/consent (client#20). */
|
|
193
196
|
resolveCliApprovals?: (cli: BorgCli, cwd: string) => Promise<LaunchApprovalDecision>;
|
|
194
197
|
|
|
@@ -360,6 +363,7 @@ function affirmative(answer: string): boolean {
|
|
|
360
363
|
async function selectAssimilationAuthority(
|
|
361
364
|
flags: AssimilateFlags,
|
|
362
365
|
deps: AssimilateDeps,
|
|
366
|
+
mode: 'assimilate' | 'cube-init',
|
|
363
367
|
): Promise<AssimilationAuthority | null> {
|
|
364
368
|
if (flags.server !== undefined) {
|
|
365
369
|
try {
|
|
@@ -374,7 +378,10 @@ async function selectAssimilationAuthority(
|
|
|
374
378
|
// NOT infer an authority — fail closed with actionable guidance.
|
|
375
379
|
if (!deps.isTTY() || flags.yes) {
|
|
376
380
|
if (deps.defaultAuthority) return deps.defaultAuthority;
|
|
377
|
-
|
|
381
|
+
const command = mode === 'cube-init'
|
|
382
|
+
? '`borg server cube init --host <host>`'
|
|
383
|
+
: '`borg assimilate --host <host> --here`';
|
|
384
|
+
deps.stderr(`No local server selected. Use ${command} to select a local server.\n`);
|
|
378
385
|
return null;
|
|
379
386
|
}
|
|
380
387
|
|
|
@@ -403,8 +410,13 @@ async function selectAssimilationAuthority(
|
|
|
403
410
|
}
|
|
404
411
|
}
|
|
405
412
|
|
|
406
|
-
function localAssimilateCommand(
|
|
407
|
-
|
|
413
|
+
function localAssimilateCommand(
|
|
414
|
+
apiUrl: string,
|
|
415
|
+
enroll = false,
|
|
416
|
+
mode: 'assimilate' | 'cube-init' = 'assimilate',
|
|
417
|
+
): string {
|
|
418
|
+
const command = mode === 'cube-init' ? 'borg server cube init' : 'borg assimilate';
|
|
419
|
+
return `\`${command} --host ${apiUrl}${enroll ? ' --enroll' : ''}\``;
|
|
408
420
|
}
|
|
409
421
|
|
|
410
422
|
function localAssimilateRoleCommand(apiUrl: string): string {
|
|
@@ -420,36 +432,44 @@ function reportServerFailure(
|
|
|
420
432
|
apiUrl: string,
|
|
421
433
|
error: unknown,
|
|
422
434
|
enroll = false,
|
|
435
|
+
mode: 'assimilate' | 'cube-init' = 'assimilate',
|
|
423
436
|
): number {
|
|
424
437
|
const message = error instanceof Error ? error.message : String(error);
|
|
425
|
-
const retryCommand = localAssimilateCommand(apiUrl, enroll);
|
|
438
|
+
const retryCommand = localAssimilateCommand(apiUrl, enroll, mode);
|
|
426
439
|
if (error instanceof BorgServerError && error.code === 'CREATE_CUBE_DENIED') {
|
|
427
440
|
deps.stderr(
|
|
428
441
|
`This enrolled client cannot create a cube on ${apiUrl}. ` +
|
|
429
442
|
'Ask the server operator to grant access to a cube, then rerun ' +
|
|
430
|
-
`${localAssimilateCommand(apiUrl)}.\n`,
|
|
443
|
+
`${localAssimilateCommand(apiUrl, false, mode)}.\n`,
|
|
431
444
|
);
|
|
432
445
|
return 1;
|
|
433
446
|
}
|
|
434
447
|
if (error instanceof BorgServerError && error.code === 'NOT_ENROLLED') {
|
|
435
448
|
deps.stderr(
|
|
436
|
-
`
|
|
437
|
-
|
|
449
|
+
`Borg could not find a saved enrollment for ${apiUrl}. ` +
|
|
450
|
+
`This can mean that this client has not enrolled with the server, or that its enrollment ` +
|
|
451
|
+
`is saved for a different endpoint. Confirm that the host, port, and IPv4 or IPv6 ` +
|
|
452
|
+
`loopback form in ${apiUrl} match the endpoint used during enrollment. ` +
|
|
453
|
+
`If this client has never enrolled with that server, run ` +
|
|
454
|
+
`${localAssimilateCommand(apiUrl, true, mode)} from the operator’s terminal.\n`,
|
|
438
455
|
);
|
|
439
456
|
return 1;
|
|
440
457
|
}
|
|
441
458
|
if (error instanceof BorgServerError && error.code === 'CREDENTIAL_REJECTED') {
|
|
442
459
|
deps.stderr(
|
|
443
460
|
`The saved enrollment for ${apiUrl} was rejected. Re-run ` +
|
|
444
|
-
`${localAssimilateCommand(apiUrl, true)} from the operator’s terminal.\n`,
|
|
461
|
+
`${localAssimilateCommand(apiUrl, true, mode)} from the operator’s terminal.\n`,
|
|
445
462
|
);
|
|
446
463
|
return 1;
|
|
447
464
|
}
|
|
448
465
|
if (error instanceof LegacySessionCredentialCollisionError) {
|
|
466
|
+
const recovery = mode === 'cube-init'
|
|
467
|
+
? localAssimilateCommand(error.origin, true, mode)
|
|
468
|
+
: `borg assimilate --host ${error.origin} --enroll`;
|
|
449
469
|
deps.stderr(
|
|
450
470
|
`Local session credential collision detected.\n` +
|
|
451
471
|
`No local credentials were changed.\n` +
|
|
452
|
-
`Next: run
|
|
472
|
+
`Next: run ${recovery}.\n`,
|
|
453
473
|
);
|
|
454
474
|
return 1;
|
|
455
475
|
}
|
|
@@ -460,10 +480,10 @@ function reportServerFailure(
|
|
|
460
480
|
// this worktree — no server/trust-anchor/cube/other-worktree reset, no restart
|
|
461
481
|
// or version-alignment advice (#1082).
|
|
462
482
|
if (error instanceof BorgServerError && error.code === 'SESSION_REVOKED') {
|
|
463
|
-
return diagnoseSessionTermination(deps, apiUrl, 'revoked');
|
|
483
|
+
return diagnoseSessionTermination(deps, apiUrl, 'revoked', mode);
|
|
464
484
|
}
|
|
465
485
|
if (error instanceof BorgServerError && error.code === 'SESSION_REJECTED') {
|
|
466
|
-
return diagnoseSessionTermination(deps, apiUrl, 'superseded');
|
|
486
|
+
return diagnoseSessionTermination(deps, apiUrl, 'superseded', mode);
|
|
467
487
|
}
|
|
468
488
|
if (error instanceof BorgServerError && error.code === 'INVITATION_REJECTED') {
|
|
469
489
|
deps.stderr(
|
|
@@ -471,14 +491,14 @@ function reportServerFailure(
|
|
|
471
491
|
'Ask the server operator for a replacement invitation — the server can stay running: ' +
|
|
472
492
|
'for an unclaimed owner client run `borg-mcp-server owner-invite`; for an ordinary ' +
|
|
473
493
|
'client run `borg-mcp-server client-invite`. Then rerun ' +
|
|
474
|
-
`${localAssimilateCommand(apiUrl, true)}.\n`,
|
|
494
|
+
`${localAssimilateCommand(apiUrl, true, mode)}.\n`,
|
|
475
495
|
);
|
|
476
496
|
return 1;
|
|
477
497
|
}
|
|
478
498
|
if (/HTTP 40[13]|auth(?:entication|orization)|credential.*(?:invalid|rejected)/i.test(message)) {
|
|
479
499
|
deps.stderr(
|
|
480
500
|
`The saved enrollment for ${apiUrl} was rejected. Re-run ` +
|
|
481
|
-
`${localAssimilateCommand(apiUrl, true)} from the operator’s terminal.\n`,
|
|
501
|
+
`${localAssimilateCommand(apiUrl, true, mode)} from the operator’s terminal.\n`,
|
|
482
502
|
);
|
|
483
503
|
return 1;
|
|
484
504
|
}
|
|
@@ -545,13 +565,17 @@ function diagnoseSessionTermination(
|
|
|
545
565
|
deps: AssimilateDeps,
|
|
546
566
|
apiUrl: string,
|
|
547
567
|
outcome: 'revoked' | 'superseded',
|
|
568
|
+
mode: 'assimilate' | 'cube-init' = 'assimilate',
|
|
548
569
|
): number {
|
|
549
570
|
const message = outcome === 'revoked'
|
|
550
571
|
? 'Local session was revoked.'
|
|
551
572
|
: 'Local session was superseded by a newer enrollment.';
|
|
573
|
+
const recovery = mode === 'cube-init'
|
|
574
|
+
? localAssimilateCommand(apiUrl, true, mode)
|
|
575
|
+
: `borg assimilate --host ${apiUrl} --enroll`;
|
|
552
576
|
deps.stderr(
|
|
553
577
|
`${message}\n` +
|
|
554
|
-
`Next: run borg reset-local-seat, then
|
|
578
|
+
`Next: run borg reset-local-seat, then ${recovery}.\n`,
|
|
555
579
|
);
|
|
556
580
|
return 1;
|
|
557
581
|
}
|
|
@@ -586,7 +610,8 @@ export async function runAssimilate(
|
|
|
586
610
|
repositoryContext = await deps.resolveRepositoryContext(deps.cwd());
|
|
587
611
|
} catch (error) {
|
|
588
612
|
if (error instanceof Error && error.message === 'BARE_REPOSITORY') {
|
|
589
|
-
|
|
613
|
+
const command = mode === 'cube-init' ? 'borg server cube init' : 'borg assimilate';
|
|
614
|
+
deps.stderr(`${command} requires a non-bare repository worktree. Clone or check out the repository, then retry.\n`);
|
|
590
615
|
return 1;
|
|
591
616
|
}
|
|
592
617
|
repositoryContext = null;
|
|
@@ -600,6 +625,19 @@ export async function runAssimilate(
|
|
|
600
625
|
return 1;
|
|
601
626
|
}
|
|
602
627
|
|
|
628
|
+
if (args.flags.server === undefined && deps.defaultAuthority === undefined) {
|
|
629
|
+
const connectCommand = mode === 'cube-init'
|
|
630
|
+
? 'borg server cube init --host <host>'
|
|
631
|
+
: 'borg assimilate --host <host>';
|
|
632
|
+
const serverInstall = await deps.ensureLocalServerInstalled(connectCommand);
|
|
633
|
+
if (serverInstall !== 'present') {
|
|
634
|
+
// A newly installed server still needs its explicit setup/start journey.
|
|
635
|
+
// Decline, non-interactive, and failure paths have already printed exact
|
|
636
|
+
// recovery commands. None may continue into private-state mutation.
|
|
637
|
+
return serverInstall === 'installed' ? 0 : 1;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
603
641
|
try {
|
|
604
642
|
await deps.preparePrivateRoot();
|
|
605
643
|
} catch {
|
|
@@ -618,16 +656,16 @@ export async function runAssimilate(
|
|
|
618
656
|
hasPersistedIdentity = existing !== null || await deps.hasPersistedActiveCube();
|
|
619
657
|
} catch (error) {
|
|
620
658
|
if (error instanceof LegacySessionCredentialCollisionError) {
|
|
621
|
-
return reportServerFailure(deps, error.origin, error);
|
|
659
|
+
return reportServerFailure(deps, error.origin, error, false, mode);
|
|
622
660
|
}
|
|
623
661
|
localSeatReadError = error;
|
|
624
662
|
}
|
|
625
663
|
|
|
626
664
|
// ----- Step 1: Select and authenticate the local server -----
|
|
627
|
-
const authority = await selectAssimilationAuthority(args.flags, deps);
|
|
665
|
+
const authority = await selectAssimilationAuthority(args.flags, deps, mode);
|
|
628
666
|
if (!authority) return 1;
|
|
629
667
|
if (localSeatReadError !== undefined) {
|
|
630
|
-
return reportServerFailure(deps, authority.apiUrl, localSeatReadError);
|
|
668
|
+
return reportServerFailure(deps, authority.apiUrl, localSeatReadError, false, mode);
|
|
631
669
|
}
|
|
632
670
|
|
|
633
671
|
const projectRoot = repositoryContext.root;
|
|
@@ -644,7 +682,7 @@ export async function runAssimilate(
|
|
|
644
682
|
if (!deps.isTTY()) {
|
|
645
683
|
deps.stderr(
|
|
646
684
|
'Local enrollment requires an interactive operator terminal. ' +
|
|
647
|
-
`Re-run ${localAssimilateCommand(authority.apiUrl, true)} from the operator’s terminal.\n`,
|
|
685
|
+
`Re-run ${localAssimilateCommand(authority.apiUrl, true, mode)} from the operator’s terminal.\n`,
|
|
648
686
|
);
|
|
649
687
|
return 1;
|
|
650
688
|
}
|
|
@@ -663,7 +701,7 @@ export async function runAssimilate(
|
|
|
663
701
|
if (!invitation) {
|
|
664
702
|
deps.stderr(
|
|
665
703
|
`No enrollment invitation was entered for ${authority.apiUrl}. ` +
|
|
666
|
-
`Ask the server operator for one, then rerun ${localAssimilateCommand(authority.apiUrl, true)}.\n`,
|
|
704
|
+
`Ask the server operator for one, then rerun ${localAssimilateCommand(authority.apiUrl, true, mode)}.\n`,
|
|
667
705
|
);
|
|
668
706
|
return 1;
|
|
669
707
|
}
|
|
@@ -697,7 +735,7 @@ export async function runAssimilate(
|
|
|
697
735
|
serverCapabilities: serverAuth.serverCapabilities ?? [],
|
|
698
736
|
};
|
|
699
737
|
} catch (error) {
|
|
700
|
-
return reportServerFailure(deps, authority.apiUrl, error, args.flags.enroll === true);
|
|
738
|
+
return reportServerFailure(deps, authority.apiUrl, error, args.flags.enroll === true, mode);
|
|
701
739
|
}
|
|
702
740
|
}
|
|
703
741
|
|
|
@@ -728,6 +766,8 @@ export async function runAssimilate(
|
|
|
728
766
|
isTTY: deps.isTTY,
|
|
729
767
|
prompt: deps.prompt,
|
|
730
768
|
write: deps.stderr,
|
|
769
|
+
...(mode === 'cube-init' ? { writeResult: deps.stdout } : {}),
|
|
770
|
+
useColor: () => deps.isTTY() && !process.env.NO_COLOR && !process.env.CI,
|
|
731
771
|
getIdentity: deps.getRepositoryIdentity,
|
|
732
772
|
getAssociation: (repository) => deps.getRepositoryAssociation(auth.serverTrustIdentity, repository),
|
|
733
773
|
saveAssociation: (repository, association) =>
|
|
@@ -815,12 +855,12 @@ export async function runAssimilate(
|
|
|
815
855
|
if (error instanceof BorgServerError && error.code === 'CREATE_CUBE_DENIED') {
|
|
816
856
|
deps.stderr(
|
|
817
857
|
`This enrolled client cannot create a cube on ${auth.apiUrl}. ` +
|
|
818
|
-
`Ask the server operator to grant access to a cube, then rerun ${localAssimilateCommand(auth.apiUrl)}.\n`,
|
|
858
|
+
`Ask the server operator to grant access to a cube, then rerun ${localAssimilateCommand(auth.apiUrl, false, mode)}.\n`,
|
|
819
859
|
);
|
|
820
860
|
return 1;
|
|
821
861
|
}
|
|
822
862
|
if (error instanceof BorgServerError) {
|
|
823
|
-
return reportServerFailure(deps, auth.apiUrl, error);
|
|
863
|
+
return reportServerFailure(deps, auth.apiUrl, error, false, mode);
|
|
824
864
|
}
|
|
825
865
|
deps.stderr(
|
|
826
866
|
'Repository cube initialization failed.\n' +
|
|
@@ -1800,7 +1840,7 @@ export async function runAssimilate(
|
|
|
1800
1840
|
// OpenCode assimilate launch: start TUI with the kickoff passed via
|
|
1801
1841
|
// --prompt (auto-submits it as the first message). BORG_SESSION is
|
|
1802
1842
|
// pinned in opencode.json. A unique port is assigned so the MCP child
|
|
1803
|
-
// can connect
|
|
1843
|
+
// can connect to OpenCode's local HTTP API for durable entry injection.
|
|
1804
1844
|
dronePort = computeOpenCodePort(result.drone_id);
|
|
1805
1845
|
installBorgPlugin();
|
|
1806
1846
|
const cwd = agentCwd;
|
|
@@ -1824,9 +1864,8 @@ export async function runAssimilate(
|
|
|
1824
1864
|
launchedAtSeconds: Math.floor(Date.now() / 1000),
|
|
1825
1865
|
});
|
|
1826
1866
|
}
|
|
1827
|
-
// gh#opencode:
|
|
1828
|
-
//
|
|
1829
|
-
// programmatically once the HTTP server is ready. Best-effort.
|
|
1867
|
+
// gh#opencode: bind to the kickoff-bearing session through OpenCode's local
|
|
1868
|
+
// HTTP API after the TUI auto-submits --prompt. Best-effort.
|
|
1830
1869
|
if (cli === 'opencode' && openCodeKickoff) {
|
|
1831
1870
|
const launchKickoff = openCodeKickoff;
|
|
1832
1871
|
const serverUrl = `http://127.0.0.1:${dronePort}`;
|
package/src/assimilate-deps.ts
CHANGED
|
@@ -18,6 +18,10 @@ import { readinessProbeEnv } from './readiness-probe.js';
|
|
|
18
18
|
import { resolveMcpBinaryPath } from './self-path.js';
|
|
19
19
|
import { buildRuntimeMetadataReport } from './runtime-metadata.js';
|
|
20
20
|
import { inspectLiveInboxMonitor } from './seat-reattach-guard.js';
|
|
21
|
+
import {
|
|
22
|
+
buildDefaultFirstRunServerInstallDeps,
|
|
23
|
+
offerFirstRunServerInstall,
|
|
24
|
+
} from './first-run-server.js';
|
|
21
25
|
|
|
22
26
|
import type { AssimilateDeps } from './assimilate-cmd.js';
|
|
23
27
|
import {
|
|
@@ -149,6 +153,11 @@ export function buildDefaultAssimilateDeps(
|
|
|
149
153
|
return typeof result.invitation === 'string' ? result.invitation : '';
|
|
150
154
|
},
|
|
151
155
|
isTTY: () => process.stdin.isTTY === true,
|
|
156
|
+
ensureLocalServerInstalled: async (connectCommand) =>
|
|
157
|
+
(await offerFirstRunServerInstall(
|
|
158
|
+
buildDefaultFirstRunServerInstallDeps(),
|
|
159
|
+
connectCommand,
|
|
160
|
+
)).kind,
|
|
152
161
|
resolveCliApprovals: (cli, cwd) => resolveLaunchBorgApprovals(
|
|
153
162
|
cli,
|
|
154
163
|
defaultApprovalIo(
|
package/src/claude.ts
CHANGED
|
@@ -82,7 +82,7 @@ import { ensureCliMcpConfigured } from './ensure-mcp-config.js';
|
|
|
82
82
|
import { installBorgPlugin } from './opencode-plugin.js';
|
|
83
83
|
import { connectOpenCodeDrone, computeOpenCodePort, createOpenCodeLaunchKickoff, injectInitialKickoff } from './opencode-drone.js';
|
|
84
84
|
import { buildOpenCodeLaunchArgs, defaultApprovalIo, resolveLaunchBorgApprovals } from './cli-tool-approval.js';
|
|
85
|
-
import { runEarlyServerFacade } from './server-facade.js';
|
|
85
|
+
import { isClientOwnedCubeInitArgv, runEarlyServerFacade } from './server-facade.js';
|
|
86
86
|
import { runEarlyUpdate } from './update-cmd.js';
|
|
87
87
|
|
|
88
88
|
export type AssimilateDepsBuilder = typeof buildDefaultAssimilateDeps;
|
|
@@ -106,6 +106,13 @@ async function main() {
|
|
|
106
106
|
const updateExitCode = await runEarlyUpdate(process.argv);
|
|
107
107
|
if (updateExitCode !== null) process.exit(updateExitCode);
|
|
108
108
|
|
|
109
|
+
// Cube initialization is client-owned, so enable debug before its early
|
|
110
|
+
// facade dispatch. Server lifecycle commands keep their argv verbatim:
|
|
111
|
+
// their separate executable owns and parses `--debug`.
|
|
112
|
+
if (isClientOwnedCubeInitArgv(process.argv)) {
|
|
113
|
+
initDebugFromArgv(process.argv);
|
|
114
|
+
}
|
|
115
|
+
|
|
109
116
|
const serverExitCode = await runEarlyServerFacade(process.argv);
|
|
110
117
|
if (serverExitCode !== null) process.exit(serverExitCode);
|
|
111
118
|
|
|
@@ -462,7 +469,7 @@ async function main() {
|
|
|
462
469
|
// OpenCode launch: start TUI with the kickoff passed via --prompt
|
|
463
470
|
// (auto-submits it as the first message). BORG_SESSION is pinned in
|
|
464
471
|
// opencode.json. A unique port is assigned so the MCP child can connect
|
|
465
|
-
//
|
|
472
|
+
// to OpenCode's local HTTP API for durable entry injection.
|
|
466
473
|
const dronePort = active
|
|
467
474
|
? computeOpenCodePort(active.droneId)
|
|
468
475
|
: 14096;
|
|
@@ -500,7 +507,7 @@ async function main() {
|
|
|
500
507
|
}
|
|
501
508
|
|
|
502
509
|
// gh#857 WI-2: wake-target recording is codex-only (app-server bridge).
|
|
503
|
-
// OpenCode
|
|
510
|
+
// OpenCode uses HTTP entry injection; Claude uses the inbox Monitor.
|
|
504
511
|
if (cli === 'codex' && active && codexSocketPath) {
|
|
505
512
|
void recordCodexWakeTarget({
|
|
506
513
|
deps: { setCodexWakeTarget, findLoadedCodexThread },
|
package/src/cli-help.ts
CHANGED
|
@@ -31,7 +31,7 @@ export function topLevelHelpText(version: string): string {
|
|
|
31
31
|
` borg assimilate [role] Join or create a cube\n` +
|
|
32
32
|
` borg assimilate --host <host> Join or create on an explicit server\n` +
|
|
33
33
|
` borg assimilate --worktree <name> Spawn a worktree drone (in ~/.borg/worktrees/<repo>/<name>)\n` +
|
|
34
|
-
` borg server cube init
|
|
34
|
+
` borg server cube init Initialize this repository's cube without creating a drone\n` +
|
|
35
35
|
` borg reset-local-seat Clear ONLY this worktree's saved local seat (offline; after a rejection)\n` +
|
|
36
36
|
` borg sync [--prune] Sync this worktree's branch to origin/main\n` +
|
|
37
37
|
` borg cleanup [--prune] Report (or --prune) worktrees orphaned by evicted drones\n` +
|
|
@@ -82,9 +82,9 @@ export function serverHelpText(): string {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/** Client-owned help for repository cube initialization without a drone. */
|
|
85
|
-
export function cubeInitHelpText(): string {
|
|
85
|
+
export function cubeInitHelpText(version: string): string {
|
|
86
86
|
return (
|
|
87
|
-
`borg server cube init — initialize this Git repository's cube without creating a drone\n\n` +
|
|
87
|
+
`borg server cube init (borgmcp ${version}) — initialize this Git repository's cube without creating a drone\n\n` +
|
|
88
88
|
`Usage:\n` +
|
|
89
89
|
` borg server cube init [options]\n\n` +
|
|
90
90
|
`Options:\n` +
|
|
@@ -93,7 +93,10 @@ export function cubeInitHelpText(): string {
|
|
|
93
93
|
` --cube-name <name> Repository cube name (otherwise edit the proposed name)\n` +
|
|
94
94
|
` --template software-dev|starter New-cube template (default: software-dev)\n` +
|
|
95
95
|
` --yes, -y Accept new-cube defaults; never adopt by name\n` +
|
|
96
|
-
` --help, -h Show this help\n`
|
|
96
|
+
` --help, -h Show this help\n\n` +
|
|
97
|
+
`An existing repository association skips all prompts. One accessible exact-name legacy\n` +
|
|
98
|
+
`cube requires explicit interactive adoption; ambiguous matches fail closed. An enrolled\n` +
|
|
99
|
+
`owner client may create a repository cube; ordinary clients require an explicit cube grant.\n`
|
|
97
100
|
);
|
|
98
101
|
}
|
|
99
102
|
|
package/src/codex-launch.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BorgCli } from './cubes.js';
|
|
2
2
|
import { wakePathArming, type AgentKind } from './regen-format.js';
|
|
3
|
+
import { OPENCODE_WAKE_PATH_GUIDANCE } from './opencode-wake-copy.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* The claude kickoff prompt's wake-path section (gh#929) — the SAME shared
|
|
@@ -60,7 +61,7 @@ export function buildAgentKickoffPrompt(options: {
|
|
|
60
61
|
const codexWakePathClause =
|
|
61
62
|
options.codexWakePathClause ??
|
|
62
63
|
`Codex Borg wakeups use remote-control when available; if no wake arrives, run borg_regen manually when returning to the session.`;
|
|
63
|
-
const opencodeWakePathClause =
|
|
64
|
+
const opencodeWakePathClause = OPENCODE_WAKE_PATH_GUIDANCE;
|
|
64
65
|
const loopOrEmpty = options.cli === 'claude' ? '/loop ' : '';
|
|
65
66
|
const wakeClause = options.cli === 'claude'
|
|
66
67
|
? options.monitorClause
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import {
|
|
5
|
+
buildDefaultUpdateDeps,
|
|
6
|
+
isExactSemver,
|
|
7
|
+
resolveCompatibleServerTarget,
|
|
8
|
+
type InstalledPackage,
|
|
9
|
+
type PublishedPackage,
|
|
10
|
+
type UpdateDeps,
|
|
11
|
+
} from './update-cmd.js';
|
|
12
|
+
|
|
13
|
+
const CLIENT_PACKAGE = 'borgmcp';
|
|
14
|
+
const SERVER_PACKAGE = 'borgmcp-server';
|
|
15
|
+
const SHARED_PACKAGE = 'borgmcp-shared';
|
|
16
|
+
const DEFAULT_CONNECT_COMMAND = 'borg assimilate --host <host>';
|
|
17
|
+
|
|
18
|
+
export type FirstRunServerInstallResult =
|
|
19
|
+
| { kind: 'present'; server: InstalledPackage }
|
|
20
|
+
| { kind: 'installed'; server: InstalledPackage }
|
|
21
|
+
| { kind: 'declined' | 'non-interactive' | 'failed' };
|
|
22
|
+
|
|
23
|
+
export interface FirstRunServerInstallDeps {
|
|
24
|
+
currentServer(): Promise<InstalledPackage | null>;
|
|
25
|
+
publishedPackage(
|
|
26
|
+
name: 'borgmcp' | 'borgmcp-server',
|
|
27
|
+
version: string,
|
|
28
|
+
): Promise<PublishedPackage>;
|
|
29
|
+
publishedVersions(name: 'borgmcp' | 'borgmcp-server'): Promise<string[]>;
|
|
30
|
+
installGlobal(
|
|
31
|
+
name: 'borgmcp' | 'borgmcp-server',
|
|
32
|
+
version: string,
|
|
33
|
+
options?: { ignoreScripts?: boolean },
|
|
34
|
+
): Promise<void>;
|
|
35
|
+
confirm(message: string): Promise<'yes' | 'no' | 'eof' | 'interrupted'>;
|
|
36
|
+
isTTY(): boolean;
|
|
37
|
+
stdout(text: string): void;
|
|
38
|
+
stderr(text: string): void;
|
|
39
|
+
clientSharedVersion(): string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readClientSharedVersion(): string {
|
|
43
|
+
const here = fileURLToPath(import.meta.url);
|
|
44
|
+
const manifestPath = join(dirname(here), '..', 'package.json');
|
|
45
|
+
const parsed: unknown = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
46
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
47
|
+
throw new Error('installed borgmcp package manifest is invalid');
|
|
48
|
+
}
|
|
49
|
+
const dependencies = (parsed as Record<string, unknown>).dependencies;
|
|
50
|
+
if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) {
|
|
51
|
+
throw new Error('installed borgmcp dependency manifest is invalid');
|
|
52
|
+
}
|
|
53
|
+
const version = (dependencies as Record<string, unknown>)[SHARED_PACKAGE];
|
|
54
|
+
if (!isExactSemver(version)) {
|
|
55
|
+
throw new Error(`installed ${CLIENT_PACKAGE} does not declare an exact ${SHARED_PACKAGE} pin`);
|
|
56
|
+
}
|
|
57
|
+
return version;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function exactInstallCommand(version: string): string {
|
|
61
|
+
return `npm install --global --ignore-scripts ${SERVER_PACKAGE}@${version}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function assertInstalledServer(
|
|
65
|
+
installed: InstalledPackage | null,
|
|
66
|
+
published: PublishedPackage,
|
|
67
|
+
): asserts installed is InstalledPackage {
|
|
68
|
+
if (
|
|
69
|
+
installed?.name !== SERVER_PACKAGE ||
|
|
70
|
+
installed.version !== published.version ||
|
|
71
|
+
installed.sharedVersion !== published.sharedVersion
|
|
72
|
+
) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`installation verification expected ${SERVER_PACKAGE}@${published.version} with ` +
|
|
75
|
+
`${SHARED_PACKAGE}@${published.sharedVersion}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function buildDefaultFirstRunServerInstallDeps(): FirstRunServerInstallDeps {
|
|
81
|
+
const update: UpdateDeps = buildDefaultUpdateDeps();
|
|
82
|
+
return {
|
|
83
|
+
currentServer: update.currentServer,
|
|
84
|
+
publishedPackage: update.publishedPackage,
|
|
85
|
+
publishedVersions: update.publishedVersions,
|
|
86
|
+
installGlobal: update.installGlobal,
|
|
87
|
+
confirm: update.confirm,
|
|
88
|
+
isTTY: update.isTTY,
|
|
89
|
+
stdout: update.stdout,
|
|
90
|
+
stderr: update.stderr,
|
|
91
|
+
clientSharedVersion: readClientSharedVersion,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Offer the exact compatible server before first-run setup mutates state.
|
|
97
|
+
* Returning anything except `present`/`installed` means no caller-owned setup
|
|
98
|
+
* or assimilation work should continue.
|
|
99
|
+
*/
|
|
100
|
+
export async function offerFirstRunServerInstall(
|
|
101
|
+
deps: FirstRunServerInstallDeps = buildDefaultFirstRunServerInstallDeps(),
|
|
102
|
+
connectCommand = DEFAULT_CONNECT_COMMAND,
|
|
103
|
+
): Promise<FirstRunServerInstallResult> {
|
|
104
|
+
let installed: InstalledPackage | null;
|
|
105
|
+
try {
|
|
106
|
+
installed = await deps.currentServer();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
deps.stderr(
|
|
109
|
+
`The local ${SERVER_PACKAGE} installation could not be verified. No installation was attempted.\n` +
|
|
110
|
+
`Run \`borg server status\` to inspect it, or repair the npm-global installation and run this command again.\n` +
|
|
111
|
+
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
112
|
+
);
|
|
113
|
+
return { kind: 'failed' };
|
|
114
|
+
}
|
|
115
|
+
if (installed) return { kind: 'present', server: installed };
|
|
116
|
+
|
|
117
|
+
if (!deps.isTTY()) {
|
|
118
|
+
deps.stderr(
|
|
119
|
+
`No local ${SERVER_PACKAGE} installation was found. No installation was attempted because this terminal is non-interactive.\n` +
|
|
120
|
+
`Run \`borg setup\` in an interactive terminal, or connect to an existing server with ` +
|
|
121
|
+
`\`${connectCommand}\`.\n`,
|
|
122
|
+
);
|
|
123
|
+
return { kind: 'non-interactive' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let target: PublishedPackage;
|
|
127
|
+
try {
|
|
128
|
+
const sharedVersion = deps.clientSharedVersion();
|
|
129
|
+
target = await resolveCompatibleServerTarget(sharedVersion, deps);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
deps.stderr(
|
|
132
|
+
`Borg could not resolve a compatible local server. No installation was attempted.\n` +
|
|
133
|
+
`${error instanceof Error ? error.message : String(error)}\n` +
|
|
134
|
+
`Run \`borg update\`, then run \`borg setup\` again. Or connect to an existing server with ` +
|
|
135
|
+
`\`${connectCommand}\`.\n`,
|
|
136
|
+
);
|
|
137
|
+
return { kind: 'failed' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const command = exactInstallCommand(target.version);
|
|
141
|
+
const decision = await deps.confirm(
|
|
142
|
+
`No local ${SERVER_PACKAGE} installation was found. ` +
|
|
143
|
+
`Install ${SERVER_PACKAGE}@${target.version}, which uses ${SHARED_PACKAGE}@${target.sharedVersion}, now?\n` +
|
|
144
|
+
`Command: ${command}\n` +
|
|
145
|
+
`Continue? [y/N] `,
|
|
146
|
+
);
|
|
147
|
+
if (decision !== 'yes') {
|
|
148
|
+
const reason = decision === 'interrupted'
|
|
149
|
+
? 'Installation was cancelled by SIGINT.'
|
|
150
|
+
: decision === 'eof'
|
|
151
|
+
? 'Installation was cancelled because confirmation input ended.'
|
|
152
|
+
: 'Installation was declined.';
|
|
153
|
+
deps.stderr(
|
|
154
|
+
`${reason} No server package or server state was changed.\n` +
|
|
155
|
+
`To install it later, run \`${command}\`. Then run \`borg server setup\` and \`borg server start\`.\n`,
|
|
156
|
+
);
|
|
157
|
+
return { kind: 'declined' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
await deps.installGlobal(SERVER_PACKAGE, target.version, { ignoreScripts: true });
|
|
162
|
+
installed = await deps.currentServer();
|
|
163
|
+
assertInstalledServer(installed, target);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
deps.stderr(
|
|
166
|
+
`The ${SERVER_PACKAGE}@${target.version} installation could not be completed and verified.\n` +
|
|
167
|
+
`${error instanceof Error ? error.message : String(error)}\n` +
|
|
168
|
+
`Run \`${command}\` to retry. Then run \`borg server setup\` and \`borg server start\`.\n`,
|
|
169
|
+
);
|
|
170
|
+
return { kind: 'failed' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
deps.stdout(
|
|
174
|
+
`Installed ${SERVER_PACKAGE}@${target.version} with ${SHARED_PACKAGE}@${target.sharedVersion}.\n` +
|
|
175
|
+
`Next, run \`borg server setup\`, then run \`borg server start\` in a terminal you keep open.\n`,
|
|
176
|
+
);
|
|
177
|
+
return { kind: 'installed', server: installed };
|
|
178
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -90,10 +90,10 @@ import { filterToolsForRole } from './tool-scope.js';
|
|
|
90
90
|
import { getPackageVersion, getOnDiskVersion, handleVersionFlag } from './version.js';
|
|
91
91
|
import {
|
|
92
92
|
renderStreamStatus,
|
|
93
|
-
checkInboxMonitorHealthy,
|
|
94
93
|
formatWakePathPrefix,
|
|
95
94
|
shouldShowWakePathWarning,
|
|
96
95
|
} from './stream-status.js';
|
|
96
|
+
import { inspectWakePath } from './wake-path-health.js';
|
|
97
97
|
import {
|
|
98
98
|
RUNTIME_METADATA_ADVISORY,
|
|
99
99
|
renderRoster,
|
|
@@ -128,7 +128,6 @@ import {
|
|
|
128
128
|
connectOpenCodeDrone,
|
|
129
129
|
injectOpenCodeEntry,
|
|
130
130
|
computeOpenCodePort,
|
|
131
|
-
getOpenCodeConnectionState,
|
|
132
131
|
} from './opencode-drone.js';
|
|
133
132
|
import { installBorgPlugin } from './opencode-plugin.js';
|
|
134
133
|
import { setModuleInjectOpenCode } from './log-stream.js';
|
|
@@ -401,31 +400,28 @@ export async function main() {
|
|
|
401
400
|
await refreshActiveCubeMetadata(freshActive);
|
|
402
401
|
}
|
|
403
402
|
|
|
404
|
-
// Wake-path self-heal (gh#43): SSE delivery to the inbox
|
|
405
|
-
// is
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
// boundary), the drone misses every incoming entry until the
|
|
409
|
-
// /loop fallback heartbeat. Because regen runs on every /loop
|
|
410
|
-
// iteration, surfacing the breakage here gives self-healing at
|
|
411
|
-
// worst-case latency = the heartbeat interval. Mirrors the
|
|
412
|
-
// State-5 self-arm instruction in stream-status.ts.
|
|
403
|
+
// Wake-path self-heal (gh#43): SSE delivery to the durable inbox
|
|
404
|
+
// is distinct from the runtime-specific mechanism that wakes the
|
|
405
|
+
// agent. Inspect that real mechanism for Claude, Codex, or OpenCode
|
|
406
|
+
// and surface a runtime-specific recovery prefix when it is broken.
|
|
413
407
|
const streamStatus = getStreamStatus();
|
|
414
408
|
const inboxPath = inboxPathForDrone(freshActive.cubeId, freshActive.droneId);
|
|
415
409
|
const monitorStateRoot = monitorStateRootForWorktree(findProjectRoot());
|
|
416
|
-
// Non-Claude CLIs do not use the Claude inbox Monitor. Keep the
|
|
417
|
-
// agent CLI distinction independent from whether Codex's optional
|
|
418
|
-
// remote-wake transport is currently armed.
|
|
419
410
|
const agentKind = resolveSessionAgentKind();
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
:
|
|
411
|
+
const wakePath = await inspectWakePath({
|
|
412
|
+
agentKind,
|
|
413
|
+
active: freshActive,
|
|
414
|
+
inboxPath,
|
|
415
|
+
monitorStateRoot,
|
|
416
|
+
});
|
|
417
|
+
const inboxMonitorHealthy = wakePath.healthy;
|
|
423
418
|
const prefix = shouldShowWakePathWarning(streamStatus, inboxMonitorHealthy)
|
|
424
419
|
? formatWakePathPrefix({
|
|
425
420
|
inboxPath,
|
|
426
421
|
monitorStateRoot,
|
|
427
422
|
droneLabel: regenWakePathDroneLabel(displayedResult, displayIdentity.droneLabel),
|
|
428
423
|
cubeName: displayIdentity.cubeName,
|
|
424
|
+
wakePath,
|
|
429
425
|
})
|
|
430
426
|
: '';
|
|
431
427
|
|
|
@@ -686,9 +682,9 @@ export async function main() {
|
|
|
686
682
|
// Probe the in-process SSE consumer state. Does NOT require
|
|
687
683
|
// an active cube — if the consumer hasn't started or is
|
|
688
684
|
// between cubes, the snapshot still reports current values.
|
|
689
|
-
// Also probes wake-path completeness
|
|
690
|
-
//
|
|
691
|
-
//
|
|
685
|
+
// Also probes runtime-specific wake-path completeness: Claude's
|
|
686
|
+
// inbox Monitor, Codex's remote-control bridge, or OpenCode's
|
|
687
|
+
// durable injection delivery state.
|
|
692
688
|
const status = getStreamStatus();
|
|
693
689
|
const active = await getActiveCube();
|
|
694
690
|
const inboxPath = active
|
|
@@ -697,14 +693,13 @@ export async function main() {
|
|
|
697
693
|
const monitorStateRoot = active
|
|
698
694
|
? monitorStateRootForWorktree(findProjectRoot())
|
|
699
695
|
: null;
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
: null;
|
|
696
|
+
const wakePath = await inspectWakePath({
|
|
697
|
+
agentKind: resolveSessionAgentKind(),
|
|
698
|
+
active,
|
|
699
|
+
inboxPath,
|
|
700
|
+
monitorStateRoot,
|
|
701
|
+
});
|
|
702
|
+
const inboxMonitorHealthy = wakePath.healthy;
|
|
708
703
|
let silentInertWarning = '';
|
|
709
704
|
if (status.runLoopHealth === 'silent-inert') {
|
|
710
705
|
silentInertWarning = '## ⚠ SSE stream loop silent-inert — run /mcp and reconnect to restart\n\n' +
|
|
@@ -713,6 +708,7 @@ export async function main() {
|
|
|
713
708
|
const text = renderStreamStatus({
|
|
714
709
|
status,
|
|
715
710
|
inboxMonitorHealthy,
|
|
711
|
+
wakePath,
|
|
716
712
|
inboxPath,
|
|
717
713
|
monitorStateRoot,
|
|
718
714
|
droneLabel: active?.droneLabel ?? null,
|