borgmcp 3.3.0 → 3.5.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 +30 -5
- package/dist/agent-integration-health.d.ts +9 -5
- package/dist/agent-integration-health.d.ts.map +1 -1
- package/dist/agent-integration-health.js +104 -5
- package/dist/agent-integration-health.js.map +1 -1
- package/dist/backends/{launch-all-windows.d.ts → launch-all-terminals.d.ts} +5 -3
- package/dist/backends/launch-all-terminals.d.ts.map +1 -0
- package/dist/backends/{launch-all-windows.js → launch-all-terminals.js} +26 -12
- package/dist/backends/launch-all-terminals.js.map +1 -0
- package/dist/bare-launch-menu.d.ts +53 -16
- package/dist/bare-launch-menu.d.ts.map +1 -1
- package/dist/bare-launch-menu.js +95 -4
- package/dist/bare-launch-menu.js.map +1 -1
- package/dist/claude.d.ts +6 -0
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +84 -8
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +2 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +31 -2
- package/dist/cli-help.js.map +1 -1
- package/dist/config-utils.d.ts +12 -6
- package/dist/config-utils.d.ts.map +1 -1
- package/dist/config-utils.js +31 -8
- package/dist/config-utils.js.map +1 -1
- package/dist/cubes.d.ts +17 -0
- package/dist/cubes.d.ts.map +1 -1
- package/dist/cubes.js +87 -1
- package/dist/cubes.js.map +1 -1
- package/dist/ensure-mcp-config.d.ts.map +1 -1
- package/dist/ensure-mcp-config.js +5 -2
- package/dist/ensure-mcp-config.js.map +1 -1
- package/dist/launch-all-cmd.d.ts.map +1 -1
- package/dist/launch-all-cmd.js +20 -10
- package/dist/launch-all-cmd.js.map +1 -1
- package/dist/log-audit-core.d.ts +7 -0
- package/dist/log-audit-core.d.ts.map +1 -0
- package/dist/log-audit-core.js +80 -0
- package/dist/log-audit-core.js.map +1 -0
- package/dist/log-audit.d.ts +3 -3
- package/dist/log-audit.js +7 -110
- package/dist/log-audit.js.map +1 -1
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +6 -1
- package/dist/opencode-drone.js.map +1 -1
- package/dist/opencode-plugin.d.ts +47 -7
- package/dist/opencode-plugin.d.ts.map +1 -1
- package/dist/opencode-plugin.js +187 -23
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/parse-launch-all-args.d.ts +1 -1
- package/dist/parse-launch-all-args.d.ts.map +1 -1
- package/dist/parse-launch-all-args.js +3 -3
- package/dist/parse-launch-all-args.js.map +1 -1
- package/dist/regen-format.js +1 -1
- package/dist/regen-format.js.map +1 -1
- package/dist/resolved-cli-config.d.ts +1 -0
- package/dist/resolved-cli-config.d.ts.map +1 -1
- package/dist/resolved-cli-config.js +1 -0
- package/dist/resolved-cli-config.js.map +1 -1
- package/dist/seat-commands.d.ts +58 -0
- package/dist/seat-commands.d.ts.map +1 -0
- package/dist/seat-commands.js +240 -0
- package/dist/seat-commands.js.map +1 -0
- package/dist/seats.d.ts +8 -0
- package/dist/seats.d.ts.map +1 -1
- package/dist/seats.js +14 -0
- package/dist/seats.js.map +1 -1
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -1
- package/dist/unknown-subcommand.js +2 -0
- package/dist/unknown-subcommand.js.map +1 -1
- package/docs/LOCAL_SERVER.md +17 -4
- package/package.json +1 -1
- package/src/agent-integration-health.ts +121 -6
- package/src/backends/{launch-all-windows.ts → launch-all-terminals.ts} +33 -15
- package/src/bare-launch-menu.ts +141 -18
- package/src/claude.ts +128 -7
- package/src/cli-help.ts +37 -2
- package/src/config-utils.ts +33 -8
- package/src/cubes.ts +118 -1
- package/src/ensure-mcp-config.ts +5 -2
- package/src/launch-all-cmd.ts +24 -11
- package/src/log-audit-core.ts +81 -0
- package/src/log-audit.ts +6 -117
- package/src/opencode-drone.ts +6 -1
- package/src/opencode-plugin.ts +216 -22
- package/src/parse-launch-all-args.ts +4 -4
- package/src/regen-format.ts +1 -1
- package/src/resolved-cli-config.ts +2 -0
- package/src/seat-commands.ts +322 -0
- package/src/seats.ts +15 -0
- package/src/unknown-subcommand.ts +2 -0
- package/dist/backends/launch-all-windows.d.ts.map +0 -1
- package/dist/backends/launch-all-windows.js.map +0 -1
package/src/claude.ts
CHANGED
|
@@ -22,7 +22,18 @@ import { basename } from 'node:path';
|
|
|
22
22
|
import { createInterface } from 'node:readline/promises';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
24
|
import chalk from 'chalk';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
BORG_LAUNCH_EXPECTED_SEAT_ENV,
|
|
27
|
+
codexLaunchSeatExpectationConfigArgs,
|
|
28
|
+
findProjectRoot,
|
|
29
|
+
getActiveCube,
|
|
30
|
+
inboxPathForDrone,
|
|
31
|
+
LaunchSeatIdentityChangedError,
|
|
32
|
+
setCodexWakeTarget,
|
|
33
|
+
pruneDeadCodexWakeTargets,
|
|
34
|
+
type ActiveCube,
|
|
35
|
+
type BorgCli,
|
|
36
|
+
} from './cubes.js';
|
|
26
37
|
import { monitorStateRootForWorktree } from './inbox-monitor.js';
|
|
27
38
|
import { formatSeatReattachRefusal, inspectLiveInboxMonitor } from './seat-reattach-guard.js';
|
|
28
39
|
import { handleVersionFlag, getPackageVersion } from './version.js';
|
|
@@ -43,9 +54,17 @@ import { unknownSubcommand } from './unknown-subcommand.js';
|
|
|
43
54
|
import { parseRecoverEnrollmentArgs, runRecoverEnrollment } from './recover-enrollment-cmd.js';
|
|
44
55
|
import { runLaunchAll } from './launch-all-cmd.js';
|
|
45
56
|
import { buildDefaultLaunchAllDeps } from './launch-all-deps.js';
|
|
57
|
+
import {
|
|
58
|
+
buildDefaultSeatCommandDeps,
|
|
59
|
+
parseLaunchSeatArgs,
|
|
60
|
+
parseSeatsArgs,
|
|
61
|
+
runLaunchSeat,
|
|
62
|
+
runSeats,
|
|
63
|
+
} from './seat-commands.js';
|
|
46
64
|
import { discoverDroneCandidates } from './launch-all-discovery.js';
|
|
47
65
|
import {
|
|
48
66
|
configureSelectedLaunchCli,
|
|
67
|
+
discoverLiveLaunchMenuCandidates,
|
|
49
68
|
explicitCliLaunchHint,
|
|
50
69
|
runBareLaunchMenu,
|
|
51
70
|
shouldResolveExplicitCliLaunchHintTargets,
|
|
@@ -90,6 +109,19 @@ import { runDoctor, warnIfAgentIntegrationUnhealthy } from './agent-integration-
|
|
|
90
109
|
|
|
91
110
|
export type AssimilateDepsBuilder = typeof buildDefaultAssimilateDeps;
|
|
92
111
|
|
|
112
|
+
export class OpenCodeTargetedLaunchConfigError extends Error {
|
|
113
|
+
readonly code = 'OPENCODE_TARGETED_LAUNCH_CONFIG';
|
|
114
|
+
|
|
115
|
+
constructor(droneLabel: string, worktree: string) {
|
|
116
|
+
super(
|
|
117
|
+
`borg launch: did not launch '${droneLabel}' — borg could not update or verify the OpenCode configuration ` +
|
|
118
|
+
'that a targeted launch needs to open the correct drone. Check that the OpenCode configuration file is ' +
|
|
119
|
+
`writable, then try again. As a fallback, run \`borg\` in ${worktree} to resume that worktree's drone directly.`,
|
|
120
|
+
);
|
|
121
|
+
this.name = 'OpenCodeTargetedLaunchConfigError';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
93
125
|
export function createOpenCodeLaunchPlan(
|
|
94
126
|
cwd: string,
|
|
95
127
|
port: number,
|
|
@@ -267,6 +299,27 @@ async function main() {
|
|
|
267
299
|
const code = await runCleanup({}, parsed.options);
|
|
268
300
|
process.exit(code);
|
|
269
301
|
}
|
|
302
|
+
if (process.argv[2] === 'seats') {
|
|
303
|
+
const parsed = parseSeatsArgs(process.argv.slice(3));
|
|
304
|
+
if (!parsed.ok) {
|
|
305
|
+
process.stderr.write(chalk.red(`${consolePrefix()}◼ borg seats: ${parsed.error}\n`));
|
|
306
|
+
process.stderr.write(`Run \`borg seats --help\` for usage.\n`);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
process.exit(await runSeats(buildDefaultSeatCommandDeps()));
|
|
310
|
+
}
|
|
311
|
+
if (process.argv[2] === 'launch') {
|
|
312
|
+
const parsed = parseLaunchSeatArgs(process.argv.slice(3));
|
|
313
|
+
if (!parsed.ok) {
|
|
314
|
+
process.stderr.write(chalk.red(`${consolePrefix()}◼ borg launch: ${parsed.error}\n`));
|
|
315
|
+
process.stderr.write(`Run \`borg launch --help\` for usage.\n`);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
process.exit(await runLaunchSeat(
|
|
319
|
+
{ target: parsed.target, ...(parsed.cube ? { cube: parsed.cube } : {}) },
|
|
320
|
+
buildDefaultSeatCommandDeps(),
|
|
321
|
+
));
|
|
322
|
+
}
|
|
270
323
|
if (process.argv[2] === 'launch-all') {
|
|
271
324
|
const parsed = parseLaunchAllArgs(process.argv.slice(3));
|
|
272
325
|
if (!parsed.ok) {
|
|
@@ -359,19 +412,59 @@ async function main() {
|
|
|
359
412
|
extraArgs: process.argv.slice(2),
|
|
360
413
|
stdinIsTTY,
|
|
361
414
|
stdoutIsTTY,
|
|
415
|
+
hasActiveSeat: active !== null,
|
|
362
416
|
})
|
|
363
417
|
) {
|
|
418
|
+
const launchAllDeps = buildDefaultLaunchAllDeps();
|
|
419
|
+
const seatCommandDeps = buildDefaultSeatCommandDeps();
|
|
420
|
+
const siblingContext = await discoverLiveLaunchMenuCandidates({
|
|
421
|
+
readAllProjectIdentities: launchAllDeps.readAllProjectIdentities,
|
|
422
|
+
discoverDroneCandidates: (cubeId) => discoverDroneCandidates(
|
|
423
|
+
{ targetCubeId: cubeId },
|
|
424
|
+
{ ...launchAllDeps, stderr: () => {} },
|
|
425
|
+
),
|
|
426
|
+
getActiveSeatForWorktree: seatCommandDeps.getActiveSeatForWorktree,
|
|
427
|
+
pathExists: launchAllDeps.pathExists,
|
|
428
|
+
probeSeat: (candidate) => launchAllDeps.probeSeat(
|
|
429
|
+
candidate.sessionToken,
|
|
430
|
+
candidate.apiUrl,
|
|
431
|
+
candidate.serverTrustIdentity,
|
|
432
|
+
),
|
|
433
|
+
});
|
|
364
434
|
const otherConfiguredClis = configuredCliNames(
|
|
365
435
|
detectCliAvailability(),
|
|
366
436
|
detectCliConfiguration(),
|
|
367
437
|
).filter((c) => c !== cli);
|
|
368
438
|
const action = await runBareLaunchMenu(
|
|
369
|
-
{
|
|
439
|
+
{
|
|
440
|
+
defaultCli: cli,
|
|
441
|
+
otherConfiguredClis,
|
|
442
|
+
hasLaunchAllTargets: siblingContext.launchAllCubeId !== undefined,
|
|
443
|
+
droneCandidates: siblingContext.candidates,
|
|
444
|
+
...(siblingContext.launchAllCubeId
|
|
445
|
+
? { launchAllCubeId: siblingContext.launchAllCubeId }
|
|
446
|
+
: {}),
|
|
447
|
+
},
|
|
370
448
|
prompt
|
|
371
449
|
);
|
|
450
|
+
if (action.kind === 'launch-seat') {
|
|
451
|
+
process.exit(await runLaunchSeat(
|
|
452
|
+
{ target: action.target },
|
|
453
|
+
buildDefaultSeatCommandDeps(),
|
|
454
|
+
));
|
|
455
|
+
}
|
|
372
456
|
if (action.kind === 'launch-all') {
|
|
373
|
-
const
|
|
374
|
-
const
|
|
457
|
+
const deps = buildDefaultLaunchAllDeps();
|
|
458
|
+
const selectedIdentity = action.cubeId
|
|
459
|
+
? (await deps.readAllProjectIdentities()).find(({ cube }) => cube.cubeId === action.cubeId)
|
|
460
|
+
: undefined;
|
|
461
|
+
const parsed = parseLaunchAllArgs([]);
|
|
462
|
+
const code = parsed.ok
|
|
463
|
+
? await runLaunchAll(
|
|
464
|
+
parsed.args,
|
|
465
|
+
selectedIdentity ? { ...deps, getActiveCube: async () => selectedIdentity.cube } : deps,
|
|
466
|
+
)
|
|
467
|
+
: 1;
|
|
375
468
|
process.exit(code);
|
|
376
469
|
}
|
|
377
470
|
// option 1 → configured default; option 2 → the other agent, ONE-SHOT
|
|
@@ -382,7 +475,11 @@ async function main() {
|
|
|
382
475
|
|
|
383
476
|
// Configure only the CLI that will actually launch. This must follow the
|
|
384
477
|
// one-shot menu: the resolved default can differ from the menu selection.
|
|
385
|
-
cli = configureSelectedLaunchCli(
|
|
478
|
+
cli = configureSelectedLaunchCli(
|
|
479
|
+
cli,
|
|
480
|
+
launchAction,
|
|
481
|
+
(selectedCli) => ensureResolvedCliConfigured(selectedCli, active),
|
|
482
|
+
);
|
|
386
483
|
|
|
387
484
|
if (active && !parsedCli.force) {
|
|
388
485
|
const inboxPath = inboxPathForDrone(active.cubeId, active.droneId);
|
|
@@ -531,6 +628,7 @@ async function main() {
|
|
|
531
628
|
...codexAgentKindConfigArgs(),
|
|
532
629
|
...codexRemoteWakeConfigArgs(codexSocketPath !== null),
|
|
533
630
|
...codexStateRootConfigArgs(),
|
|
631
|
+
...codexLaunchSeatExpectationConfigArgs(),
|
|
534
632
|
...remoteArgs,
|
|
535
633
|
...withCodexCwdArg([...passthroughArgs, kickoff], process.cwd()),
|
|
536
634
|
];
|
|
@@ -614,11 +712,25 @@ async function main() {
|
|
|
614
712
|
});
|
|
615
713
|
}
|
|
616
714
|
|
|
617
|
-
function ensureResolvedCliConfigured(cli: BorgCli): void {
|
|
715
|
+
export function ensureResolvedCliConfigured(cli: BorgCli, active: ActiveCube | null = null): void {
|
|
618
716
|
const label = cli === 'claude' ? 'Claude Code' : cli === 'codex' ? 'Codex' : 'OpenCode';
|
|
717
|
+
const targetedOpenCodeLaunch = cli === 'opencode' &&
|
|
718
|
+
Boolean(process.env[BORG_LAUNCH_EXPECTED_SEAT_ENV]);
|
|
619
719
|
try {
|
|
620
720
|
configureResolvedCli(cli, {
|
|
621
|
-
ensureMcp:
|
|
721
|
+
ensureMcp: (selectedCli) => {
|
|
722
|
+
try {
|
|
723
|
+
ensureCliMcpConfigured(selectedCli);
|
|
724
|
+
} catch (error) {
|
|
725
|
+
if (targetedOpenCodeLaunch && selectedCli === 'opencode') {
|
|
726
|
+
throw new OpenCodeTargetedLaunchConfigError(
|
|
727
|
+
active?.droneLabel ?? '<unknown>',
|
|
728
|
+
active?.worktree ?? process.cwd(),
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
throw error;
|
|
732
|
+
}
|
|
733
|
+
},
|
|
622
734
|
addClaudeProjectSessionStartHook: () => {
|
|
623
735
|
// gh#673 P2 (WI-1): the orientation hook lives PROJECT-LOCAL in
|
|
624
736
|
// <root>/.claude/settings.local.json — ensured on every bare
|
|
@@ -630,8 +742,10 @@ function ensureResolvedCliConfigured(cli: BorgCli): void {
|
|
|
630
742
|
addClaudeUserPromptSubmitHook: addUserPromptSubmitHook,
|
|
631
743
|
addCodexSessionStartHook,
|
|
632
744
|
addCodexUserPromptSubmitHook,
|
|
745
|
+
installOpenCodePlugin: installBorgPlugin,
|
|
633
746
|
});
|
|
634
747
|
} catch (err: any) {
|
|
748
|
+
if (err instanceof OpenCodeTargetedLaunchConfigError) throw err;
|
|
635
749
|
console.error(`${consolePrefix()}${chalk.yellow(`warning: ${label} integration check failed: ${err?.message ?? err}`)}`);
|
|
636
750
|
}
|
|
637
751
|
try {
|
|
@@ -655,6 +769,13 @@ function isEntryInvocation(): boolean {
|
|
|
655
769
|
|
|
656
770
|
if (isEntryInvocation()) {
|
|
657
771
|
main().catch((error) => {
|
|
772
|
+
if (
|
|
773
|
+
error instanceof LaunchSeatIdentityChangedError ||
|
|
774
|
+
error instanceof OpenCodeTargetedLaunchConfigError
|
|
775
|
+
) {
|
|
776
|
+
process.stderr.write(`${error.message}\n`);
|
|
777
|
+
process.exit(1);
|
|
778
|
+
}
|
|
658
779
|
console.error(`${consolePrefix()}${chalk.red(`\n◼ Error: ${error.message}\n`)}`);
|
|
659
780
|
process.exit(1);
|
|
660
781
|
});
|
package/src/cli-help.ts
CHANGED
|
@@ -32,7 +32,7 @@ export function launchAllHelpText(version: string): string {
|
|
|
32
32
|
`Usage:\n` +
|
|
33
33
|
` borg launch-all [cube] [options]\n\n` +
|
|
34
34
|
`Options:\n` +
|
|
35
|
-
` --mode <tmux|
|
|
35
|
+
` --mode <tmux|terminals|pastelist> Select the launch backend\n` +
|
|
36
36
|
` --only <name> Launch one role or drone label\n` +
|
|
37
37
|
` --dry-run Show what would launch\n` +
|
|
38
38
|
` --cli <claude|codex|opencode> Select the agent CLI\n` +
|
|
@@ -40,7 +40,38 @@ export function launchAllHelpText(version: string): string {
|
|
|
40
40
|
` --yes, -y Skip the large-fleet confirmation\n` +
|
|
41
41
|
` --force Override live-session skips\n` +
|
|
42
42
|
` --launch-delay <ms> Wait between launches\n` +
|
|
43
|
-
` --help, -h Show this help\n`
|
|
43
|
+
` --help, -h Show this help\n\n` +
|
|
44
|
+
`Modes:\n` +
|
|
45
|
+
` terminals Open each drone in its own terminal tab, with the drone's name as\n` +
|
|
46
|
+
` the tab title. Default on macOS when iTerm2 or Terminal.app is\n` +
|
|
47
|
+
` installed. Terminal.app opens one named window for each drone\n` +
|
|
48
|
+
` instead; tabs would need macOS automation permissions, which borg\n` +
|
|
49
|
+
` does not request.\n` +
|
|
50
|
+
` tmux Open all drones in one shared tmux session. Default on Linux.\n` +
|
|
51
|
+
` pastelist Print the launch commands so you can run them yourself.\n`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function seatsHelpText(version: string): string {
|
|
56
|
+
return (
|
|
57
|
+
`borg seats (borgmcp ${version}) — list this machine's registered drones\n\n` +
|
|
58
|
+
`Usage:\n` +
|
|
59
|
+
` borg seats Show drone, cube, worktree, agent CLI, and local state\n` +
|
|
60
|
+
` borg seats --help Show this help\n\n` +
|
|
61
|
+
`The local registry belongs to this machine only.\n`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function launchSeatHelpText(version: string): string {
|
|
66
|
+
return (
|
|
67
|
+
`borg launch (borgmcp ${version}) — reopen one registered drone from its worktree\n\n` +
|
|
68
|
+
`Usage:\n` +
|
|
69
|
+
` borg launch <drone-label-or-id-prefix>\n` +
|
|
70
|
+
` borg launch <drone-label-or-id-prefix> --cube <name>\n` +
|
|
71
|
+
` borg launch --help\n\n` +
|
|
72
|
+
`Options:\n` +
|
|
73
|
+
` --cube <name> Disambiguate the same drone label across cubes\n` +
|
|
74
|
+
` --help, -h Show this help\n`
|
|
44
75
|
);
|
|
45
76
|
}
|
|
46
77
|
|
|
@@ -65,6 +96,8 @@ export function clientSubcommandHelpText(
|
|
|
65
96
|
case 'reset-local-connection': return resetLocalSeatHelpText(version);
|
|
66
97
|
case 'recover-enrollment': return recoverEnrollmentHelpText(version);
|
|
67
98
|
case 'cleanup': return cleanupHelpText(version);
|
|
99
|
+
case 'seats': return seatsHelpText(version);
|
|
100
|
+
case 'launch': return launchSeatHelpText(version);
|
|
68
101
|
case 'launch-all': return launchAllHelpText(version);
|
|
69
102
|
case 'doctor': return doctorHelpText(version);
|
|
70
103
|
default: return null;
|
|
@@ -104,6 +137,8 @@ export function topLevelHelpText(version: string): string {
|
|
|
104
137
|
` borg reset-local-connection Clear ONLY this worktree's saved connection to its cube (offline; after a rejection)\n` +
|
|
105
138
|
` borg recover-enrollment Restore or clear ONLY one failed server enrollment transaction\n` +
|
|
106
139
|
` borg cleanup [--prune] Report (or --prune) worktrees orphaned by evicted drones\n` +
|
|
140
|
+
` borg seats List this machine's registered drones and worktrees\n` +
|
|
141
|
+
` borg launch <drone-label-or-id-prefix> Reopen one registered drone from its worktree\n` +
|
|
107
142
|
` borg launch-all [cube] Launch all drone worktrees of a cube (default: active cube)\n` +
|
|
108
143
|
` borg launch-all [cube] --cli claude|codex|opencode\n` +
|
|
109
144
|
` Launch all drone worktrees with that agent CLI\n` +
|
package/src/config-utils.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { shellEscape } from './shell-escape.js';
|
|
24
24
|
import { BORG_STATE_ROOT_ENV, borgAgentConfigEnv, borgHomeRoot } from './private-root.js';
|
|
25
25
|
import type { LaunchAccessPaths } from './launch-access.js';
|
|
26
|
+
import { BORG_LAUNCH_EXPECTED_SEAT_ENV } from './cubes.js';
|
|
26
27
|
|
|
27
28
|
// Get __dirname equivalent in ESM
|
|
28
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -36,6 +37,7 @@ const CLEAR_REWAKE_HOOK_COMMAND = 'borg-clear-rewake';
|
|
|
36
37
|
const AUDIT_HOOK_COMMAND = 'borg-log-audit';
|
|
37
38
|
const FOREIGN_PATH_REMINDER_HOOK_COMMAND = 'borg-foreign-path-reminder';
|
|
38
39
|
const MCP_COMMAND = 'borg-mcp';
|
|
40
|
+
const OPENCODE_LAUNCH_EXPECTED_SEAT_REFERENCE = `{env:${BORG_LAUNCH_EXPECTED_SEAT_ENV}}`;
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* Claude Code CLI config path. The CLI reads `mcpServers.<name>` from
|
|
@@ -1167,7 +1169,7 @@ export function isCodexUserPromptSubmitHookRegistered(hooksPath: string = CODEX_
|
|
|
1167
1169
|
* `type: "local"`. Safe-default: any read error returns `false`.
|
|
1168
1170
|
*/
|
|
1169
1171
|
export function isOpenCodeMcpServerConfigured(
|
|
1170
|
-
configPath: string =
|
|
1172
|
+
configPath: string = path.join(borgHomeRoot(), '.config', 'opencode', 'opencode.json')
|
|
1171
1173
|
): boolean {
|
|
1172
1174
|
try {
|
|
1173
1175
|
if (!fs.existsSync(configPath)) return false;
|
|
@@ -1183,6 +1185,28 @@ export function isOpenCodeMcpServerConfigured(
|
|
|
1183
1185
|
}
|
|
1184
1186
|
}
|
|
1185
1187
|
|
|
1188
|
+
/**
|
|
1189
|
+
* Launch-time OpenCode registration check. Ordinary launches retain the
|
|
1190
|
+
* existing configured/not-configured behavior. A targeted `borg launch`,
|
|
1191
|
+
* identified by its non-empty expected-seat marker, additionally requires the
|
|
1192
|
+
* config substitution that carries that marker into OpenCode's MCP child.
|
|
1193
|
+
*/
|
|
1194
|
+
export function isOpenCodeMcpServerConfiguredForLaunch(
|
|
1195
|
+
configPath: string = path.join(borgHomeRoot(), '.config', 'opencode', 'opencode.json'),
|
|
1196
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
1197
|
+
): boolean {
|
|
1198
|
+
if (!isOpenCodeMcpServerConfigured(configPath)) return false;
|
|
1199
|
+
if (!env[BORG_LAUNCH_EXPECTED_SEAT_ENV]) return true;
|
|
1200
|
+
try {
|
|
1201
|
+
const borgServer = readJsonFile(configPath)?.mcp?.borg;
|
|
1202
|
+
const environment = borgServer?.environment ?? borgServer?.env;
|
|
1203
|
+
return environment?.[BORG_LAUNCH_EXPECTED_SEAT_ENV] ===
|
|
1204
|
+
OPENCODE_LAUNCH_EXPECTED_SEAT_REFERENCE;
|
|
1205
|
+
} catch {
|
|
1206
|
+
return false;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1186
1210
|
/**
|
|
1187
1211
|
* Pre-authorize the exact worktree + scratch paths in the launch-root
|
|
1188
1212
|
* OpenCode config. This intentionally writes only the project-local
|
|
@@ -1254,12 +1278,11 @@ export function addOpenCodeLaunchAccess(
|
|
|
1254
1278
|
|
|
1255
1279
|
/**
|
|
1256
1280
|
* Add borg MCP server to OpenCode using `opencode mcp add` CLI.
|
|
1257
|
-
* Pins
|
|
1258
|
-
*
|
|
1259
|
-
*
|
|
1260
|
-
*
|
|
1261
|
-
*
|
|
1262
|
-
* fallback.
|
|
1281
|
+
* Pins activation and agent-kind signals plus an OpenCode config substitution
|
|
1282
|
+
* for the launch-scoped expected seat. OpenCode resolves `{env:NAME}` from its
|
|
1283
|
+
* own launch environment before starting the MCP child, so `borg launch`
|
|
1284
|
+
* reaches the identity check without persisting one launch's value. Existing
|
|
1285
|
+
* configs with BORG_OPENCODE remain supported by the runtime fallback.
|
|
1263
1286
|
*/
|
|
1264
1287
|
export function addOpenCodeMcpServer(): void {
|
|
1265
1288
|
try {
|
|
@@ -1270,8 +1293,10 @@ export function addOpenCodeMcpServer(): void {
|
|
|
1270
1293
|
const stateRootEnvArg = stateRoot
|
|
1271
1294
|
? ` --env ${BORG_STATE_ROOT_ENV}=${shellQuote(stateRoot)}`
|
|
1272
1295
|
: '';
|
|
1296
|
+
const launchExpectedSeatEnvArg =
|
|
1297
|
+
` --env ${BORG_LAUNCH_EXPECTED_SEAT_ENV}=${shellQuote(OPENCODE_LAUNCH_EXPECTED_SEAT_REFERENCE)}`;
|
|
1273
1298
|
execSync(
|
|
1274
|
-
`opencode mcp add borg --env BORG_SESSION=1 --env BORG_AGENT_KIND=opencode --env BORG_OPENCODE=1${apiUrlEnvArg}${stateRootEnvArg} -- ${shellQuote(MCP_COMMAND)}`,
|
|
1299
|
+
`opencode mcp add borg --env BORG_SESSION=1 --env BORG_AGENT_KIND=opencode --env BORG_OPENCODE=1${apiUrlEnvArg}${stateRootEnvArg}${launchExpectedSeatEnvArg} -- ${shellQuote(MCP_COMMAND)}`,
|
|
1275
1300
|
{ stdio: 'inherit', env: borgAgentConfigEnv(process.env) }
|
|
1276
1301
|
);
|
|
1277
1302
|
} catch (error: any) {
|
package/src/cubes.ts
CHANGED
|
@@ -76,6 +76,113 @@ export interface ActiveCube {
|
|
|
76
76
|
worktree?: string;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
export const BORG_LAUNCH_EXPECTED_SEAT_ENV = 'BORG_LAUNCH_EXPECTED_SEAT';
|
|
80
|
+
|
|
81
|
+
export interface LaunchSeatExpectation {
|
|
82
|
+
credentialRef: string;
|
|
83
|
+
cubeId: string;
|
|
84
|
+
droneId: string;
|
|
85
|
+
worktree: string;
|
|
86
|
+
droneLabel: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class LaunchSeatIdentityChangedError extends Error {
|
|
90
|
+
readonly code = 'LAUNCH_SEAT_IDENTITY_CHANGED';
|
|
91
|
+
|
|
92
|
+
constructor(droneLabel: string) {
|
|
93
|
+
super(
|
|
94
|
+
`borg launch: did not launch '${droneLabel}' — its seat registration changed before the launch could start. ` +
|
|
95
|
+
'Run `borg seats` to see the current state, then try again.',
|
|
96
|
+
);
|
|
97
|
+
this.name = 'LaunchSeatIdentityChangedError';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function withLaunchSeatExpectationEnv(
|
|
102
|
+
env: NodeJS.ProcessEnv,
|
|
103
|
+
expectation: LaunchSeatExpectation,
|
|
104
|
+
): NodeJS.ProcessEnv {
|
|
105
|
+
// The deterministic ref and public identity are sufficient; never copy the
|
|
106
|
+
// stored bearer into a process environment.
|
|
107
|
+
return {
|
|
108
|
+
...env,
|
|
109
|
+
[BORG_LAUNCH_EXPECTED_SEAT_ENV]: JSON.stringify(expectation),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Codex MCP children do not inherit the wrapper environment, so carry the
|
|
114
|
+
* launch-scoped expected seat through the same per-invocation config channel as
|
|
115
|
+
* the Borg-session and state-root markers. */
|
|
116
|
+
export function codexLaunchSeatExpectationConfigArgs(
|
|
117
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
118
|
+
): string[] {
|
|
119
|
+
const expectation = env[BORG_LAUNCH_EXPECTED_SEAT_ENV];
|
|
120
|
+
if (expectation === undefined) return [];
|
|
121
|
+
return [
|
|
122
|
+
'-c',
|
|
123
|
+
`mcp_servers.borg.env.${BORG_LAUNCH_EXPECTED_SEAT_ENV}=${JSON.stringify(expectation)}`,
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readLaunchSeatExpectation(
|
|
128
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
129
|
+
): LaunchSeatExpectation | null {
|
|
130
|
+
const raw = env[BORG_LAUNCH_EXPECTED_SEAT_ENV];
|
|
131
|
+
// OpenCode substitutes an unset `{env:NAME}` reference with an empty string.
|
|
132
|
+
// That is an ordinary bare launch, not a malformed launch-seat expectation.
|
|
133
|
+
if (raw === undefined || raw === '') return null;
|
|
134
|
+
let parsed: unknown;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(raw);
|
|
137
|
+
} catch {
|
|
138
|
+
throw new LaunchSeatIdentityChangedError('<unknown>');
|
|
139
|
+
}
|
|
140
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
141
|
+
throw new LaunchSeatIdentityChangedError('<unknown>');
|
|
142
|
+
}
|
|
143
|
+
const value = parsed as Record<string, unknown>;
|
|
144
|
+
const droneLabel = typeof value.droneLabel === 'string' ? value.droneLabel : '<unknown>';
|
|
145
|
+
if (
|
|
146
|
+
typeof value.credentialRef !== 'string' ||
|
|
147
|
+
typeof value.cubeId !== 'string' ||
|
|
148
|
+
typeof value.droneId !== 'string' ||
|
|
149
|
+
typeof value.worktree !== 'string' ||
|
|
150
|
+
typeof value.droneLabel !== 'string'
|
|
151
|
+
) {
|
|
152
|
+
throw new LaunchSeatIdentityChangedError(droneLabel);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
credentialRef: value.credentialRef,
|
|
156
|
+
cubeId: value.cubeId,
|
|
157
|
+
droneId: value.droneId,
|
|
158
|
+
worktree: value.worktree,
|
|
159
|
+
droneLabel: value.droneLabel,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function assertLaunchSeatExpectation(
|
|
164
|
+
expectation: LaunchSeatExpectation,
|
|
165
|
+
active: ActiveCube | null | undefined,
|
|
166
|
+
worktree: string,
|
|
167
|
+
currentRecord: SeatRecord | null,
|
|
168
|
+
): void {
|
|
169
|
+
if (
|
|
170
|
+
currentRecord === null ||
|
|
171
|
+
seatRef(currentRecord) !== expectation.credentialRef ||
|
|
172
|
+
currentRecord.cubeId !== expectation.cubeId ||
|
|
173
|
+
currentRecord.droneId !== expectation.droneId ||
|
|
174
|
+
resolve(worktree) !== resolve(expectation.worktree) ||
|
|
175
|
+
(active !== undefined && (
|
|
176
|
+
active === null ||
|
|
177
|
+
active.localSessionCredentialRef !== expectation.credentialRef ||
|
|
178
|
+
active.cubeId !== expectation.cubeId ||
|
|
179
|
+
active.droneId !== expectation.droneId
|
|
180
|
+
))
|
|
181
|
+
) {
|
|
182
|
+
throw new LaunchSeatIdentityChangedError(expectation.droneLabel);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
79
186
|
export type ActiveCubeInput = Omit<ActiveCube, 'sessionToken'> & {
|
|
80
187
|
sessionToken?: string;
|
|
81
188
|
};
|
|
@@ -303,9 +410,19 @@ export async function getActiveCube(): Promise<ActiveCube | null> {
|
|
|
303
410
|
}
|
|
304
411
|
|
|
305
412
|
export async function getActiveCubeForWorktree(worktree: string): Promise<ActiveCube | null> {
|
|
306
|
-
const
|
|
413
|
+
const projectRoot = findProjectRoot(worktree);
|
|
414
|
+
const expectation = readLaunchSeatExpectation();
|
|
415
|
+
const record = await getActiveSeatForWorktree(projectRoot);
|
|
416
|
+
// A launch-by-label child must hydrate the exact record selected by its
|
|
417
|
+
// parent. Check on both sides of bearer hydration so a concurrent preferred-
|
|
418
|
+
// seat replacement fails closed instead of launching a different drone.
|
|
419
|
+
if (expectation) assertLaunchSeatExpectation(expectation, undefined, projectRoot, record);
|
|
307
420
|
if (!record || !record.cubeId || !record.droneId) return null;
|
|
308
421
|
const active = await hydrateActiveCube(record);
|
|
422
|
+
if (expectation) {
|
|
423
|
+
const currentRecord = await getActiveSeatForWorktree(projectRoot);
|
|
424
|
+
assertLaunchSeatExpectation(expectation, active, projectRoot, currentRecord);
|
|
425
|
+
}
|
|
309
426
|
if (active && pinnedMcpSeatIdentity && (
|
|
310
427
|
resolve(active.worktree ?? '') !== pinnedMcpSeatIdentity.worktree ||
|
|
311
428
|
active.cubeId !== pinnedMcpSeatIdentity.cubeId ||
|
package/src/ensure-mcp-config.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
addOpenCodeMcpServer,
|
|
6
6
|
isCodexMcpServerConfigured,
|
|
7
7
|
isMcpServerConfigured,
|
|
8
|
-
|
|
8
|
+
isOpenCodeMcpServerConfiguredForLaunch,
|
|
9
9
|
} from './config-utils.js';
|
|
10
10
|
|
|
11
11
|
export interface EnsureMcpConfigDeps {
|
|
@@ -22,7 +22,7 @@ const defaultDeps: EnsureMcpConfigDeps = {
|
|
|
22
22
|
addClaude: addMcpServer,
|
|
23
23
|
isCodexConfigured: isCodexMcpServerConfigured,
|
|
24
24
|
addCodex: addCodexMcpServer,
|
|
25
|
-
isOpenCodeConfigured:
|
|
25
|
+
isOpenCodeConfigured: isOpenCodeMcpServerConfiguredForLaunch,
|
|
26
26
|
addOpenCode: addOpenCodeMcpServer,
|
|
27
27
|
};
|
|
28
28
|
|
|
@@ -49,6 +49,9 @@ export function ensureCliMcpConfigured(
|
|
|
49
49
|
case 'opencode':
|
|
50
50
|
if (deps.isOpenCodeConfigured()) return false;
|
|
51
51
|
deps.addOpenCode();
|
|
52
|
+
if (!deps.isOpenCodeConfigured()) {
|
|
53
|
+
throw new Error('OpenCode MCP registration could not be verified after setup');
|
|
54
|
+
}
|
|
52
55
|
return true;
|
|
53
56
|
}
|
|
54
57
|
}
|
package/src/launch-all-cmd.ts
CHANGED
|
@@ -11,10 +11,10 @@ import type { SeatStatus } from './seat-probe.js';
|
|
|
11
11
|
import { resolveBorgPath } from './launch-all-command.js';
|
|
12
12
|
import { sweepStaleLocks, isLockLive } from './launch-all-locks.js';
|
|
13
13
|
import { runTmuxBackend } from './backends/launch-all-tmux.js';
|
|
14
|
-
import {
|
|
14
|
+
import { hasMacOSTerminalApp, runTerminalsBackend } from './backends/launch-all-terminals.js';
|
|
15
15
|
import { runPastelistBackend } from './backends/launch-all-pastelist.js';
|
|
16
16
|
|
|
17
|
-
type Backend = 'tmux' | '
|
|
17
|
+
type Backend = 'tmux' | 'terminals' | 'pastelist';
|
|
18
18
|
|
|
19
19
|
const TMUX_INSTALL_HINT =
|
|
20
20
|
'borg launch-all: tmux not found.\n' +
|
|
@@ -82,10 +82,11 @@ async function resolveTargetCube(
|
|
|
82
82
|
return { cubeId: active.cubeId, name: active.name };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
/** Backend selection
|
|
85
|
+
/** Backend selection: native-Windows / explicit / platform-specific auto. */
|
|
86
86
|
function selectBackend(args: LaunchAllArgs, deps: LaunchAllDeps): { backend: Backend } | { hardFail: string } {
|
|
87
87
|
const explicit = args.flags.mode;
|
|
88
|
-
const
|
|
88
|
+
const platform = deps.platform();
|
|
89
|
+
const nativeWindows = platform === 'win32' && !isWSL(deps);
|
|
89
90
|
if (nativeWindows) {
|
|
90
91
|
deps.stderr(
|
|
91
92
|
'native Windows is not supported for interactive launch; using pastelist mode instead ' +
|
|
@@ -93,13 +94,23 @@ function selectBackend(args: LaunchAllArgs, deps: LaunchAllDeps): { backend: Bac
|
|
|
93
94
|
);
|
|
94
95
|
return { backend: 'pastelist' };
|
|
95
96
|
}
|
|
96
|
-
if (explicit === '
|
|
97
|
+
if (explicit === 'terminals') return { backend: 'terminals' };
|
|
97
98
|
if (explicit === 'pastelist') return { backend: 'pastelist' };
|
|
98
|
-
|
|
99
|
-
const tmuxAvail = checkTmuxAvailable(deps);
|
|
100
99
|
if (explicit === 'tmux') {
|
|
101
|
-
return
|
|
100
|
+
return checkTmuxAvailable(deps) ? { backend: 'tmux' } : { hardFail: TMUX_INSTALL_HINT };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// macOS defaults to native terminal tabs/windows when either supported app
|
|
104
|
+
// is installed. Explicit modes above always win.
|
|
105
|
+
if (
|
|
106
|
+
platform === 'darwin' &&
|
|
107
|
+
deps.isTTY() &&
|
|
108
|
+
hasMacOSTerminalApp(deps)
|
|
109
|
+
) {
|
|
110
|
+
return { backend: 'terminals' };
|
|
102
111
|
}
|
|
112
|
+
|
|
113
|
+
const tmuxAvail = checkTmuxAvailable(deps);
|
|
103
114
|
// auto (no explicit mode)
|
|
104
115
|
if (tmuxAvail) return { backend: 'tmux' };
|
|
105
116
|
deps.stderr(TMUX_INSTALL_HINT + 'Falling back to pastelist mode (paste the commands below).\n');
|
|
@@ -416,8 +427,8 @@ export async function runLaunchAll(
|
|
|
416
427
|
}
|
|
417
428
|
printCheatSheet(sessionName, deps);
|
|
418
429
|
}
|
|
419
|
-
} else if (sel.backend === '
|
|
420
|
-
await
|
|
430
|
+
} else if (sel.backend === 'terminals') {
|
|
431
|
+
await runTerminalsBackend(launchable, { borgPath, platform: deps.platform(), cubeName, launchedAtISO: launchStartISO, launchDelayMs, sleep }, deps);
|
|
421
432
|
} else {
|
|
422
433
|
runPastelistBackend(launchable, borgPath, deps);
|
|
423
434
|
return 0; // pastelist: nothing to reconcile (operator pastes manually)
|
|
@@ -453,6 +464,8 @@ export async function runLaunchAll(
|
|
|
453
464
|
const status = statuses ? (statuses.get(c.droneId) === 'verified' ? 'VERIFIED' : 'unconfirmed (may still be joining)') : 'launched';
|
|
454
465
|
deps.stdout(` ${c.droneLabel} ${c.worktreeDir} ${status}\n`);
|
|
455
466
|
}
|
|
456
|
-
|
|
467
|
+
if (sel.backend === 'tmux') {
|
|
468
|
+
deps.stdout(`\nAttach: tmux attach -t ${sessionName}\n`);
|
|
469
|
+
}
|
|
457
470
|
return 0;
|
|
458
471
|
}
|