borgmcp 3.4.0 → 3.5.1
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 +20 -2
- 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 +83 -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 +30 -7
- 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-stream.d.ts.map +1 -1
- package/dist/log-stream.js +5 -8
- package/dist/log-stream.js.map +1 -1
- package/dist/opencode-drone.d.ts +3 -4
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +15 -10
- package/dist/opencode-drone.js.map +1 -1
- package/dist/opencode-plugin.d.ts +1 -0
- package/dist/opencode-plugin.d.ts.map +1 -1
- package/dist/opencode-plugin.js +2 -1
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/opencode-wake-copy.js +1 -1
- package/dist/opencode-wake-copy.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/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/package.json +1 -1
- 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 +127 -7
- package/src/cli-help.ts +37 -2
- package/src/config-utils.ts +32 -7
- 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-stream.ts +5 -8
- package/src/opencode-drone.ts +24 -10
- package/src/opencode-plugin.ts +2 -1
- package/src/opencode-wake-copy.ts +1 -1
- package/src/parse-launch-all-args.ts +4 -4
- 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/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
|
}
|
package/src/log-stream.ts
CHANGED
|
@@ -142,11 +142,8 @@ export function setModuleInjectOpenCode(
|
|
|
142
142
|
*/
|
|
143
143
|
const RECENT_IDS_CAP = 50;
|
|
144
144
|
|
|
145
|
-
function formatOpenCodeWakePrompt(line: string
|
|
146
|
-
|
|
147
|
-
return wakeNonce === undefined
|
|
148
|
-
? prompt
|
|
149
|
-
: `${prompt}\n\n<!-- borg-wake-nonce:${wakeNonce} -->`;
|
|
145
|
+
function formatOpenCodeWakePrompt(line: string): string {
|
|
146
|
+
return formatCubeActivityWakeMessage(line);
|
|
150
147
|
}
|
|
151
148
|
|
|
152
149
|
export const INBOX_TAIL_LINES_CAP = 512;
|
|
@@ -875,7 +872,7 @@ export async function streamOnce(
|
|
|
875
872
|
// exception: each wake nonce is a new delivery identity even when the
|
|
876
873
|
// underlying inbox line is already durable.
|
|
877
874
|
if (isReping) {
|
|
878
|
-
await injectOpenCode(formatOpenCodeWakePrompt(line
|
|
875
|
+
await injectOpenCode(formatOpenCodeWakePrompt(line), deliveryId, true);
|
|
879
876
|
}
|
|
880
877
|
markEventPersisted(ev.id, ev.data?.created_at ?? '');
|
|
881
878
|
return 'persisted-skip';
|
|
@@ -883,7 +880,7 @@ export async function streamOnce(
|
|
|
883
880
|
// The inbox append is the durable record. OpenCode injection is only the
|
|
884
881
|
// wake attempt and may return before the agent finishes processing.
|
|
885
882
|
await appendLine(active.cubeId, active.droneId, line);
|
|
886
|
-
if (!(await injectOpenCode(formatOpenCodeWakePrompt(line
|
|
883
|
+
if (!(await injectOpenCode(formatOpenCodeWakePrompt(line), deliveryId, true))) {
|
|
887
884
|
if (ev.wake_nonce === undefined) wakeCodex(formatCodexWakePrompt(line));
|
|
888
885
|
else wakeCodex(formatCodexWakePrompt(line), ev.wake_nonce);
|
|
889
886
|
}
|
|
@@ -1201,7 +1198,7 @@ export async function streamOnce(
|
|
|
1201
1198
|
if (isRecentWakeReplay) {
|
|
1202
1199
|
const line = formatInboxLine(withSseEventId(event.data, event.id));
|
|
1203
1200
|
const wakeNonce = event.wake_nonce;
|
|
1204
|
-
if (wakeNonce !== undefined && !(await injectOpenCode(formatOpenCodeWakePrompt(line
|
|
1201
|
+
if (wakeNonce !== undefined && !(await injectOpenCode(formatOpenCodeWakePrompt(line), wakeNonce, true))) {
|
|
1205
1202
|
wakeCodex(formatCodexWakePrompt(line), wakeNonce);
|
|
1206
1203
|
}
|
|
1207
1204
|
continue;
|
package/src/opencode-drone.ts
CHANGED
|
@@ -3,7 +3,10 @@ import { createHash, randomUUID } from 'crypto';
|
|
|
3
3
|
import { createServer } from 'node:net';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { tmpdir } from 'os';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
OPENCODE_INJECTED_ENTRY_METADATA_KEY,
|
|
8
|
+
OPENCODE_WAKE_IDENTITY_METADATA_KEY,
|
|
9
|
+
} from './opencode-plugin.js';
|
|
7
10
|
|
|
8
11
|
const LOG_FILE = join(tmpdir(), 'borg-opencode-drone.log');
|
|
9
12
|
function log(msg: string) {
|
|
@@ -57,6 +60,7 @@ interface OCMessage {
|
|
|
57
60
|
parts?: Array<{
|
|
58
61
|
type?: string;
|
|
59
62
|
text?: string;
|
|
63
|
+
metadata?: Record<string, unknown>;
|
|
60
64
|
}>;
|
|
61
65
|
}
|
|
62
66
|
|
|
@@ -213,12 +217,20 @@ async function listSessionMessages(id: string): Promise<OCMessage[]> {
|
|
|
213
217
|
return JSON.parse(body);
|
|
214
218
|
}
|
|
215
219
|
|
|
216
|
-
async function findInjectedMessage(
|
|
220
|
+
async function findInjectedMessage(
|
|
221
|
+
sessionId: string,
|
|
222
|
+
expectedWakeIdentity: string,
|
|
223
|
+
): Promise<string | null> {
|
|
217
224
|
const messages = await listSessionMessages(sessionId);
|
|
218
225
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
219
226
|
const message = messages[index];
|
|
220
227
|
if (message?.info?.role !== 'user' || typeof message.info.id !== 'string') continue;
|
|
221
|
-
|
|
228
|
+
const part = message.parts?.[0];
|
|
229
|
+
if (
|
|
230
|
+
part?.type === 'text' &&
|
|
231
|
+
part.metadata?.[OPENCODE_INJECTED_ENTRY_METADATA_KEY] === true &&
|
|
232
|
+
part.metadata?.[OPENCODE_WAKE_IDENTITY_METADATA_KEY] === expectedWakeIdentity
|
|
233
|
+
) {
|
|
222
234
|
return message.info.id;
|
|
223
235
|
}
|
|
224
236
|
}
|
|
@@ -493,7 +505,7 @@ async function deliverOpenCodeEntry(
|
|
|
493
505
|
}
|
|
494
506
|
|
|
495
507
|
try {
|
|
496
|
-
if (await findInjectedMessage(target.id, delivery.
|
|
508
|
+
if (await findInjectedMessage(target.id, delivery.entryId)) {
|
|
497
509
|
log(`entry ${delivery.entryId} already present in session ${target.id}`);
|
|
498
510
|
return 'delivered';
|
|
499
511
|
}
|
|
@@ -515,7 +527,10 @@ async function deliverOpenCodeEntry(
|
|
|
515
527
|
parts: [{
|
|
516
528
|
type: 'text',
|
|
517
529
|
text: delivery.text,
|
|
518
|
-
metadata: {
|
|
530
|
+
metadata: {
|
|
531
|
+
[OPENCODE_INJECTED_ENTRY_METADATA_KEY]: true,
|
|
532
|
+
[OPENCODE_WAKE_IDENTITY_METADATA_KEY]: delivery.entryId,
|
|
533
|
+
},
|
|
519
534
|
}],
|
|
520
535
|
});
|
|
521
536
|
} catch (err) {
|
|
@@ -541,7 +556,7 @@ async function deliverOpenCodeEntry(
|
|
|
541
556
|
delivery.state = 'delivered-unconfirmed';
|
|
542
557
|
}
|
|
543
558
|
try {
|
|
544
|
-
if (await findInjectedMessage(target.id, delivery.
|
|
559
|
+
if (await findInjectedMessage(target.id, delivery.entryId)) {
|
|
545
560
|
return 'delivered';
|
|
546
561
|
}
|
|
547
562
|
} catch (err) {
|
|
@@ -640,10 +655,9 @@ export async function injectInitialKickoff(launch: OpenCodeLaunchKickoff): Promi
|
|
|
640
655
|
|
|
641
656
|
/**
|
|
642
657
|
* Queue one durable inbox entry for delivery into the bound OpenCode session.
|
|
643
|
-
* The
|
|
644
|
-
*
|
|
645
|
-
* ordering-breaking caller message ID or
|
|
646
|
-
* canonical inbox text; wake re-pings include their stable nonce marker.
|
|
658
|
+
* The delivery identity is stored in TextPart metadata, so retries and replay
|
|
659
|
+
* can confirm an earlier ambiguous submission without supplying an
|
|
660
|
+
* ordering-breaking caller message ID or exposing the identity in delivered text.
|
|
647
661
|
*/
|
|
648
662
|
export function injectOpenCodeEntry(
|
|
649
663
|
text: string,
|
package/src/opencode-plugin.ts
CHANGED
|
@@ -18,6 +18,7 @@ const COMPACT_FALLBACK =
|
|
|
18
18
|
'## Borg Cube\nYou are in a Borg MCP multi-agent coordination cube. ' +
|
|
19
19
|
'Use MCP tool borg_regen to get full context and recent activity.';
|
|
20
20
|
export const OPENCODE_INJECTED_ENTRY_METADATA_KEY = 'borgOpenCodeInjectedEntry';
|
|
21
|
+
export const OPENCODE_WAKE_IDENTITY_METADATA_KEY = 'borgOpenCodeWakeIdentity';
|
|
21
22
|
export const OPENCODE_RECOVERY_METADATA_KEY = 'borgOpenCodeSessionOrientation';
|
|
22
23
|
const PLUGIN_REL_PATH = path.join('.config', 'opencode', 'plugins', 'borg-orient.js');
|
|
23
24
|
|
|
@@ -146,7 +147,7 @@ export function createOpenCodePluginCore(
|
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
export function buildBorgPluginSource(version: string): string {
|
|
149
|
-
const marker = `borgmcp-opencode-plugin:${version};opencode=${OPENCODE_COMPATIBILITY.opencode};sdk=${OPENCODE_COMPATIBILITY.sdk};textpart-metadata=exists+persisted+tui-hidden+model-hidden`;
|
|
150
|
+
const marker = `borgmcp-opencode-plugin:${version};opencode=${OPENCODE_COMPATIBILITY.opencode};sdk=${OPENCODE_COMPATIBILITY.sdk};textpart-metadata=exists+persisted+tui-hidden+model-hidden;wake-identity-key=${OPENCODE_WAKE_IDENTITY_METADATA_KEY}`;
|
|
150
151
|
return `// ${marker}
|
|
151
152
|
// Generated by borgmcp. Self-contained; do not edit.
|
|
152
153
|
const createCore = ${createOpenCodePluginCore.toString()};
|
|
@@ -3,7 +3,7 @@ import { CUBE_ACTIVITY_RESUME_WAKE_MESSAGE } from './cube-activity-wake-copy.js'
|
|
|
3
3
|
export const OPENCODE_WAKE_PATH_GUIDANCE =
|
|
4
4
|
'OpenCode wakes through HTTP entry injection into its session through the local HTTP API. ' +
|
|
5
5
|
'Borg writes each entry to the durable inbox before one injection attempt and ' +
|
|
6
|
-
'confirms delivery by
|
|
6
|
+
'confirms delivery by its unique persisted metadata identity; no inbox Monitor or secondary wake loop is used. ' +
|
|
7
7
|
'If injection is rejected or cannot be ' +
|
|
8
8
|
'confirmed, the durable entry remains available and `borg_stream-status` reports ' +
|
|
9
9
|
'it as failed or delivered-unconfirmed; run `borg_read-log unread_only=true` and ' +
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Pure: rawArgs → validated LaunchAllArgs | error. Mirror of parse-assimilate-args.
|
|
3
3
|
|
|
4
4
|
export interface LaunchAllFlags {
|
|
5
|
-
mode?: 'tmux' | '
|
|
5
|
+
mode?: 'tmux' | 'terminals' | 'pastelist';
|
|
6
6
|
only?: string;
|
|
7
7
|
dryRun?: boolean;
|
|
8
8
|
cli?: 'claude' | 'codex' | 'opencode';
|
|
@@ -28,7 +28,7 @@ export type ParseLaunchAllResult =
|
|
|
28
28
|
| { ok: false; error: string };
|
|
29
29
|
|
|
30
30
|
const SUPPORTED =
|
|
31
|
-
'--mode <tmux|
|
|
31
|
+
'--mode <tmux|terminals|pastelist>, --only <name>, --dry-run, --cli <claude|codex|opencode>, ' +
|
|
32
32
|
'--no-attach, --yes/-y, --force, --launch-delay <ms>';
|
|
33
33
|
|
|
34
34
|
export function parseLaunchAllArgs(rawArgs: string[]): ParseLaunchAllResult {
|
|
@@ -40,8 +40,8 @@ export function parseLaunchAllArgs(rawArgs: string[]): ParseLaunchAllResult {
|
|
|
40
40
|
switch (arg) {
|
|
41
41
|
case '--mode': {
|
|
42
42
|
const v = rawArgs[++i];
|
|
43
|
-
if (v !== 'tmux' && v !== '
|
|
44
|
-
return { ok: false, error: `--mode must be one of tmux|
|
|
43
|
+
if (v !== 'tmux' && v !== 'terminals' && v !== 'pastelist') {
|
|
44
|
+
return { ok: false, error: `--mode must be one of tmux|terminals|pastelist (got: ${v ?? '<missing>'})` };
|
|
45
45
|
}
|
|
46
46
|
flags.mode = v;
|
|
47
47
|
break;
|