borgmcp 4.3.0 → 4.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/dist/agent-integration-health.d.ts +2 -0
- package/dist/agent-integration-health.d.ts.map +1 -1
- package/dist/agent-integration-health.js +18 -1
- package/dist/agent-integration-health.js.map +1 -1
- package/dist/assimilate-cmd.d.ts +3 -0
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +12 -0
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +4 -2
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/backends/launch-all-terminals.d.ts.map +1 -1
- package/dist/backends/launch-all-terminals.js +6 -3
- package/dist/backends/launch-all-terminals.js.map +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/index.d.ts.map +1 -1
- package/dist/index.js +27 -4
- package/dist/index.js.map +1 -1
- package/dist/opencode-drone.d.ts +19 -0
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +374 -67
- package/dist/opencode-drone.js.map +1 -1
- package/dist/opencode-seat-identity.d.ts +1 -1
- package/dist/opencode-seat-identity.d.ts.map +1 -1
- package/dist/opencode-seat-identity.js.map +1 -1
- package/dist/private-root.d.ts +2 -0
- package/dist/private-root.d.ts.map +1 -1
- package/dist/private-root.js +35 -1
- package/dist/private-root.js.map +1 -1
- package/dist/roster-render.d.ts.map +1 -1
- package/dist/roster-render.js +2 -3
- package/dist/roster-render.js.map +1 -1
- package/dist/seats.d.ts +5 -0
- package/dist/seats.d.ts.map +1 -1
- package/dist/seats.js +10 -0
- package/dist/seats.js.map +1 -1
- package/dist/server-errors.d.ts +22 -0
- package/dist/server-errors.d.ts.map +1 -1
- package/dist/server-errors.js +32 -0
- package/dist/server-errors.js.map +1 -1
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +1 -0
- package/dist/server-handshake.js.map +1 -1
- package/dist/stream-status.d.ts.map +1 -1
- package/dist/stream-status.js +7 -2
- package/dist/stream-status.js.map +1 -1
- package/dist/terminal-title.d.ts.map +1 -1
- package/dist/terminal-title.js +3 -2
- package/dist/terminal-title.js.map +1 -1
- package/dist/update-cmd.d.ts +2 -1
- package/dist/update-cmd.d.ts.map +1 -1
- package/dist/update-cmd.js +110 -41
- package/dist/update-cmd.js.map +1 -1
- package/docs/LOCAL_SERVER.md +21 -10
- package/package.json +1 -1
- package/src/agent-integration-health.ts +20 -1
- package/src/assimilate-cmd.ts +17 -0
- package/src/assimilate-deps.ts +4 -1
- package/src/backends/launch-all-terminals.ts +6 -3
- package/src/cli-help.ts +7 -4
- package/src/index.ts +30 -3
- package/src/opencode-drone.ts +461 -73
- package/src/opencode-seat-identity.ts +1 -0
- package/src/private-root.ts +38 -1
- package/src/roster-render.ts +2 -3
- package/src/seats.ts +15 -0
- package/src/server-errors.ts +50 -0
- package/src/server-handshake.ts +1 -0
- package/src/stream-status.ts +7 -2
- package/src/terminal-title.ts +4 -2
- package/src/update-cmd.ts +118 -41
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
installBorgPlugin,
|
|
16
16
|
openCodePluginPath,
|
|
17
17
|
} from './opencode-plugin.js';
|
|
18
|
+
import { openCodeStartupDiagnosticLogPath } from './opencode-drone.js';
|
|
18
19
|
|
|
19
20
|
export const AGENT_HOOK_BINS = [
|
|
20
21
|
'borg-regen',
|
|
@@ -292,12 +293,30 @@ export function assertAgentIntegrationHealthy(report: AgentIntegrationHealth): v
|
|
|
292
293
|
|
|
293
294
|
export function runDoctor(options: InspectAgentIntegrationHealthOptions & {
|
|
294
295
|
stdout?: (text: string) => void;
|
|
296
|
+
openCodeStartupLogPath?: string;
|
|
295
297
|
} = {}): number {
|
|
296
298
|
const report = inspectAgentIntegrationHealth(options);
|
|
297
|
-
|
|
299
|
+
const output = renderAgentIntegrationHealth(report) + renderOpenCodeStartupDiagnostics(
|
|
300
|
+
options.openCodeStartupLogPath ?? openCodeStartupDiagnosticLogPath(),
|
|
301
|
+
);
|
|
302
|
+
(options.stdout ?? ((text) => process.stdout.write(text)))(output);
|
|
298
303
|
return report.issues.length === 0 ? 0 : 1;
|
|
299
304
|
}
|
|
300
305
|
|
|
306
|
+
export function renderOpenCodeStartupDiagnostics(logPath: string): string {
|
|
307
|
+
try {
|
|
308
|
+
const metadata = fs.lstatSync(logPath);
|
|
309
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
310
|
+
return `OpenCode startup diagnostics: refused at ${logPath} (path is not a regular file)\n`;
|
|
311
|
+
}
|
|
312
|
+
const contents = fs.readFileSync(logPath, 'utf8');
|
|
313
|
+
return `OpenCode startup diagnostics (${logPath}):\n${contents.trimEnd()}\n`;
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return '';
|
|
316
|
+
return `OpenCode startup diagnostics: unreadable at ${logPath} (${error instanceof Error ? error.message : String(error)})\n`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
301
320
|
export function warnIfAgentIntegrationUnhealthy(
|
|
302
321
|
options: InspectAgentIntegrationHealthOptions & { stderr?: (text: string) => void } = {},
|
|
303
322
|
): boolean {
|
package/src/assimilate-cmd.ts
CHANGED
|
@@ -299,10 +299,13 @@ export interface AssimilateDeps {
|
|
|
299
299
|
* production; absent from unit stubs that fully mock `assimilate`. */
|
|
300
300
|
finalizeServerSeat?: (input: {
|
|
301
301
|
active: ActiveCube;
|
|
302
|
+
commonDir: string;
|
|
302
303
|
expected: ExpectedBinding;
|
|
303
304
|
activate: (binding: SeatBinding) => Promise<unknown>;
|
|
304
305
|
scrubPending: () => Promise<unknown>;
|
|
305
306
|
}) => Promise<FinalizeServerSeatOutcome>;
|
|
307
|
+
/** Advisory clone-family comparison for public repositories. */
|
|
308
|
+
hasActiveSeatInDifferentCloneFamily?: (cubeId: string, commonDir: string) => Promise<boolean>;
|
|
306
309
|
findProjectRoot: (cwd: string) => string;
|
|
307
310
|
resolveRepositoryContext: (cwd: string) => Promise<GitRepositoryContext | null>;
|
|
308
311
|
getRepositoryIdentity: (context: GitRepositoryContext) => Promise<CreateCubeRepository>;
|
|
@@ -1938,6 +1941,7 @@ export async function runAssimilate(
|
|
|
1938
1941
|
try {
|
|
1939
1942
|
outcome = await deps.finalizeServerSeat({
|
|
1940
1943
|
active: activeCube,
|
|
1944
|
+
commonDir: repositoryContext.commonDir,
|
|
1941
1945
|
expected: sessionExpected,
|
|
1942
1946
|
activate: result.finalize.activate,
|
|
1943
1947
|
scrubPending: result.finalize.scrubPending,
|
|
@@ -2030,6 +2034,19 @@ export async function runAssimilate(
|
|
|
2030
2034
|
}
|
|
2031
2035
|
}
|
|
2032
2036
|
|
|
2037
|
+
if (repositoryContext.publicRepository && deps.hasActiveSeatInDifferentCloneFamily) {
|
|
2038
|
+
try {
|
|
2039
|
+
if (await deps.hasActiveSeatInDifferentCloneFamily(result.cube_id, repositoryContext.commonDir)) {
|
|
2040
|
+
deps.stderr(
|
|
2041
|
+
'warning: this cube already has a seat from a different clone family. ' +
|
|
2042
|
+
'All seats for a repository use worktrees from the same clone family, sharing its object database and refs.\n',
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
} catch {
|
|
2046
|
+
// This comparison is advisory and must never block assimilation.
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2033
2050
|
// gh#793: best-effort GC of orphaned inbox files (evicted/dead drones) in the
|
|
2034
2051
|
// cube just joined — lazy-on-assimilate, no cron/new command. NEVER blocks or
|
|
2035
2052
|
// fails the assimilate (whole call swallowed). Local-only signal (CubeDetail
|
package/src/assimilate-deps.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
import { advanceLocalServerCursor } from './local-server-cursor.js';
|
|
44
44
|
import {
|
|
45
45
|
findIncompleteSiblingAttempt,
|
|
46
|
+
hasActiveSeatInDifferentCloneFamily,
|
|
46
47
|
observeSeat,
|
|
47
48
|
prepareSeat,
|
|
48
49
|
seatRef,
|
|
@@ -214,9 +215,10 @@ export function buildDefaultAssimilateDeps(
|
|
|
214
215
|
// missing/replaced/throw → the pending record is the rerunnable locator; the
|
|
215
216
|
// caller PRESERVES the worktree). expectation-mismatch is produced upstream at
|
|
216
217
|
// PREPARE (result.prepareAborted), never here.
|
|
217
|
-
finalizeServerSeat: async ({ active, activate }) => {
|
|
218
|
+
finalizeServerSeat: async ({ active, commonDir, activate }) => {
|
|
218
219
|
const binding = {
|
|
219
220
|
worktree: cubesFindProjectRoot(process.cwd()),
|
|
221
|
+
commonDir,
|
|
220
222
|
name: active.name,
|
|
221
223
|
droneLabel: active.droneLabel,
|
|
222
224
|
...(active.roleName !== undefined ? { roleName: active.roleName } : {}),
|
|
@@ -233,6 +235,7 @@ export function buildDefaultAssimilateDeps(
|
|
|
233
235
|
? { committed: true }
|
|
234
236
|
: { committed: false, reason: 'activation-failed' };
|
|
235
237
|
},
|
|
238
|
+
hasActiveSeatInDifferentCloneFamily,
|
|
236
239
|
findProjectRoot: (cwd) => cubesFindProjectRoot(cwd),
|
|
237
240
|
resolveRepositoryContext: resolveGitRepositoryContext,
|
|
238
241
|
getRepositoryIdentity: getOrCreateRepositoryIdentity,
|
|
@@ -96,14 +96,17 @@ async function launchLinux(candidates: DroneCandidate[], opts: TerminalsOpts, de
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
* Control char (incl. newline) in a worktree path.
|
|
99
|
+
* Control char (incl. newline) in a worktree path. A path cannot be escaped
|
|
100
|
+
* without changing the launch target, so it remains a fail-loud rejection.
|
|
101
|
+
* Server-derived title fields are escaped separately by composeTerminalTitle.
|
|
102
|
+
* SR f94bb3fe + Coordinator
|
|
100
103
|
* c6370c41 fold-in: a newline in the path breaks the macOS osascript AppleScript
|
|
101
104
|
* string literal (parse error). Reject such paths up front (the user's own path;
|
|
102
105
|
* pathological) — fail-loud + skip, never silently mangle. Applies to both
|
|
103
106
|
* platforms defensively (the tmux primary handles control chars fine via
|
|
104
107
|
* shellEscape + array-args, so this guard is terminals-backend-specific).
|
|
105
108
|
*/
|
|
106
|
-
const
|
|
109
|
+
const WORKTREE_CONTROL_CHAR_RE = /[\x00-\x1f\x7f]/;
|
|
107
110
|
|
|
108
111
|
export async function runTerminalsBackend(
|
|
109
112
|
candidates: DroneCandidate[],
|
|
@@ -111,7 +114,7 @@ export async function runTerminalsBackend(
|
|
|
111
114
|
deps: LaunchAllDeps
|
|
112
115
|
): Promise<void> {
|
|
113
116
|
const safe = candidates.filter((c) => {
|
|
114
|
-
if (
|
|
117
|
+
if (WORKTREE_CONTROL_CHAR_RE.test(c.worktreeDir)) {
|
|
115
118
|
deps.stderr(
|
|
116
119
|
`skipping ${c.droneLabel} (${JSON.stringify(c.worktreeDir)}): worktree path contains a control ` +
|
|
117
120
|
`character — unsafe for --mode terminals; use --mode tmux instead.\n`
|
package/src/cli-help.ts
CHANGED
|
@@ -117,7 +117,7 @@ export function doctorHelpText(version: string): string {
|
|
|
117
117
|
return (
|
|
118
118
|
`borg doctor (borgmcp ${version}) — inspect Borg agent integrations without changing them\n\n` +
|
|
119
119
|
`Usage:\n` +
|
|
120
|
-
` borg doctor Run read-only checks for hook commands,
|
|
120
|
+
` borg doctor Run read-only checks for hook commands, OpenCode integration, and startup diagnostics\n` +
|
|
121
121
|
` borg doctor --help Show this help\n`
|
|
122
122
|
);
|
|
123
123
|
}
|
|
@@ -172,7 +172,7 @@ export function topLevelHelpText(version: string): string {
|
|
|
172
172
|
` borg Show the launch menu in a repository root; resume directly in a linked worktree\n` +
|
|
173
173
|
` borg setup Set up borg MCP server + agent CLI integration\n` +
|
|
174
174
|
` borg update Update the client and installed local server together\n` +
|
|
175
|
-
` borg doctor Check agent hook commands,
|
|
175
|
+
` borg doctor Check agent hook commands, OpenCode integration, and startup diagnostics\n` +
|
|
176
176
|
` borg clone <url> [dir] Clone a repository, then create and launch its cube\n` +
|
|
177
177
|
` borg quickstart Create a cube and a drone for every role, then launch them\n` +
|
|
178
178
|
` borg assimilate [role] Join or create a cube with one drone under one role\n` +
|
|
@@ -210,12 +210,15 @@ export function updateHelpText(version: string): string {
|
|
|
210
210
|
`Usage:\n` +
|
|
211
211
|
` borg update Confirm interactively, then update\n` +
|
|
212
212
|
` borg update --yes Update without prompting (required outside a TTY)\n` +
|
|
213
|
+
` borg update --registry <url>\n` +
|
|
214
|
+
` Acknowledge this exact configured npm registry for one update\n` +
|
|
213
215
|
` borg update --help Show this help\n\n` +
|
|
214
216
|
`Before changing anything, Borg reads the exact published client and server manifests,\n` +
|
|
215
217
|
`requires matching exact borgmcp-shared pins, and verifies canonical npm ownership. The client is\n` +
|
|
216
218
|
`installed first and the update continues under that new client before the server controller\n` +
|
|
217
|
-
`and runtime are updated.
|
|
218
|
-
|
|
219
|
+
`and runtime are updated. An alternate configured registry requires an exact per-invocation\n` +
|
|
220
|
+
`--registry acknowledgement; registry changes refuse before any subsequent package mutation,\n` +
|
|
221
|
+
`and unsupported or ambiguous package-manager provenance fails closed.\n\n` +
|
|
219
222
|
`If no local server is installed, the server phase is skipped. A failure after the client\n` +
|
|
220
223
|
`succeeds is reported as partial completion with status-specific recovery. Borg never starts\n` +
|
|
221
224
|
`a stopped server. After package verification, Borg replaces stale borgmcp package launch paths\n` +
|
package/src/index.ts
CHANGED
|
@@ -150,6 +150,7 @@ import {
|
|
|
150
150
|
configuredOpenCodePort,
|
|
151
151
|
OPEN_CODE_PORT_MISSING_DIAGNOSTIC,
|
|
152
152
|
openCodeLaunchBinding,
|
|
153
|
+
writeOpenCodeStartupDiagnostic,
|
|
153
154
|
} from './opencode-drone.js';
|
|
154
155
|
import { installBorgPlugin } from './opencode-plugin.js';
|
|
155
156
|
import { openCodeApiPasswordFromEnv } from './opencode-launch-trust.js';
|
|
@@ -172,6 +173,9 @@ import {
|
|
|
172
173
|
runEvictDroneTool,
|
|
173
174
|
runReassignDroneTool,
|
|
174
175
|
} from './drone-management.js';
|
|
176
|
+
|
|
177
|
+
const OPEN_CODE_IDENTITY_HANDSHAKE_TIMEOUT_MS = 5_000;
|
|
178
|
+
|
|
175
179
|
/**
|
|
176
180
|
* Apply a template's roles + message_taxonomy to a cube.
|
|
177
181
|
*
|
|
@@ -392,8 +396,25 @@ export async function main() {
|
|
|
392
396
|
|
|
393
397
|
let openCodeIdentityFailure: OpenCodeSeatIdentityError | null = null;
|
|
394
398
|
let finishOpenCodeIdentity: (() => void) | null = null;
|
|
399
|
+
let openCodeIdentityTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
400
|
+
const settleOpenCodeIdentity = () => {
|
|
401
|
+
if (openCodeIdentityTimeout) clearTimeout(openCodeIdentityTimeout);
|
|
402
|
+
openCodeIdentityTimeout = null;
|
|
403
|
+
const finish = finishOpenCodeIdentity;
|
|
404
|
+
finishOpenCodeIdentity = null;
|
|
405
|
+
finish?.();
|
|
406
|
+
};
|
|
395
407
|
const openCodeIdentityReady = openCodeRuntime && !readinessProbe
|
|
396
|
-
? new Promise<void>((resolveIdentity) => {
|
|
408
|
+
? new Promise<void>((resolveIdentity) => {
|
|
409
|
+
finishOpenCodeIdentity = resolveIdentity;
|
|
410
|
+
openCodeIdentityTimeout = setTimeout(() => {
|
|
411
|
+
openCodeIdentityFailure = new OpenCodeSeatIdentityError(
|
|
412
|
+
'IDENTITY_HANDSHAKE_TIMEOUT',
|
|
413
|
+
'OpenCode identity handshake did not complete within 5 seconds.',
|
|
414
|
+
);
|
|
415
|
+
settleOpenCodeIdentity();
|
|
416
|
+
}, OPEN_CODE_IDENTITY_HANDSHAKE_TIMEOUT_MS);
|
|
417
|
+
})
|
|
397
418
|
: null;
|
|
398
419
|
const waitForOpenCodeIdentity = async (): Promise<OpenCodeSeatIdentityError | null> => {
|
|
399
420
|
if (openCodeIdentityReady) await openCodeIdentityReady;
|
|
@@ -1663,7 +1684,7 @@ export async function main() {
|
|
|
1663
1684
|
? error
|
|
1664
1685
|
: new OpenCodeSeatIdentityError('ROOTS_UNAVAILABLE', String(error));
|
|
1665
1686
|
}).finally(() => {
|
|
1666
|
-
|
|
1687
|
+
settleOpenCodeIdentity();
|
|
1667
1688
|
});
|
|
1668
1689
|
};
|
|
1669
1690
|
}
|
|
@@ -1674,7 +1695,13 @@ export async function main() {
|
|
|
1674
1695
|
if (openCodeIdentityReady) {
|
|
1675
1696
|
await openCodeIdentityReady;
|
|
1676
1697
|
if (openCodeIdentityFailure) {
|
|
1677
|
-
|
|
1698
|
+
const diagnostic = formatOpenCodeSeatIdentityError(openCodeIdentityFailure, process.cwd());
|
|
1699
|
+
try {
|
|
1700
|
+
writeOpenCodeStartupDiagnostic(diagnostic);
|
|
1701
|
+
} catch {
|
|
1702
|
+
// Diagnostic persistence is best-effort; the original identity failure must terminate startup.
|
|
1703
|
+
}
|
|
1704
|
+
throw new Error(diagnostic);
|
|
1678
1705
|
} else {
|
|
1679
1706
|
await runMcpStartupServices(false, startupServices, { openCodeFirst: true });
|
|
1680
1707
|
}
|