borgmcp 3.10.0 → 3.11.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/dist/assimilate-cmd.d.ts +14 -1
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +27 -15
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +33 -4
- 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 +28 -1
- package/dist/cli-help.js.map +1 -1
- package/dist/clone-cmd.d.ts +28 -0
- package/dist/clone-cmd.d.ts.map +1 -0
- package/dist/clone-cmd.js +227 -0
- package/dist/clone-cmd.js.map +1 -0
- package/dist/clone-security.d.ts +9 -0
- package/dist/clone-security.d.ts.map +1 -0
- package/dist/clone-security.js +41 -0
- package/dist/clone-security.js.map +1 -0
- package/dist/config-utils.d.ts +13 -12
- package/dist/config-utils.d.ts.map +1 -1
- package/dist/config-utils.js +150 -27
- package/dist/config-utils.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/launch-all-cmd.d.ts +11 -0
- package/dist/launch-all-cmd.d.ts.map +1 -1
- package/dist/launch-all-cmd.js +24 -2
- package/dist/launch-all-cmd.js.map +1 -1
- package/dist/opencode-drone.d.ts +10 -8
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +56 -46
- package/dist/opencode-drone.js.map +1 -1
- package/dist/opencode-launch-trust.d.ts +13 -0
- package/dist/opencode-launch-trust.d.ts.map +1 -0
- package/dist/opencode-launch-trust.js +40 -0
- package/dist/opencode-launch-trust.js.map +1 -0
- package/dist/opencode-plugin.d.ts +3 -0
- package/dist/opencode-plugin.d.ts.map +1 -1
- package/dist/opencode-plugin.js +26 -1
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/parse-clone-args.d.ts +17 -0
- package/dist/parse-clone-args.d.ts.map +1 -0
- package/dist/parse-clone-args.js +38 -0
- package/dist/parse-clone-args.js.map +1 -0
- package/dist/parse-quickstart-args.d.ts +18 -0
- package/dist/parse-quickstart-args.d.ts.map +1 -0
- package/dist/parse-quickstart-args.js +43 -0
- package/dist/parse-quickstart-args.js.map +1 -0
- package/dist/quickstart-cmd.d.ts +23 -0
- package/dist/quickstart-cmd.d.ts.map +1 -0
- package/dist/quickstart-cmd.js +357 -0
- package/dist/quickstart-cmd.js.map +1 -0
- package/dist/setup-confirm.d.ts +1 -1
- package/dist/setup-confirm.js +3 -3
- package/dist/setup-confirm.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/assimilate-cmd.ts +48 -15
- package/src/claude.ts +38 -4
- package/src/cli-help.ts +34 -1
- package/src/clone-cmd.ts +243 -0
- package/src/clone-security.ts +40 -0
- package/src/config-utils.ts +147 -23
- package/src/index.ts +7 -0
- package/src/launch-all-cmd.ts +37 -2
- package/src/opencode-drone.ts +71 -54
- package/src/opencode-launch-trust.ts +46 -0
- package/src/opencode-plugin.ts +31 -1
- package/src/parse-clone-args.ts +44 -0
- package/src/parse-quickstart-args.ts +54 -0
- package/src/quickstart-cmd.ts +396 -0
- package/src/setup-confirm.ts +3 -3
- package/src/unknown-subcommand.ts +2 -0
package/src/claude.ts
CHANGED
|
@@ -42,8 +42,12 @@ import { runSpawn } from './spawn.js';
|
|
|
42
42
|
import { buildClaudeLaunchArgs } from './claude-launch-args.js';
|
|
43
43
|
import { parseCleanupArgs, runCleanup } from './cleanup-cmd.js';
|
|
44
44
|
import { parseAssimilateArgs } from './parse-assimilate-args.js';
|
|
45
|
+
import { parseQuickstartArgs } from './parse-quickstart-args.js';
|
|
46
|
+
import { parseCloneArgs, safeCloneParseError } from './parse-clone-args.js';
|
|
45
47
|
import { runAssimilate } from './assimilate-cmd.js';
|
|
46
48
|
import { buildDefaultAssimilateDeps } from './assimilate-deps.js';
|
|
49
|
+
import { buildDefaultQuickstartDeps, runQuickstart } from './quickstart-cmd.js';
|
|
50
|
+
import { buildDefaultCloneDeps, runClone } from './clone-cmd.js';
|
|
47
51
|
import {
|
|
48
52
|
parseResetLocalSeatArgs,
|
|
49
53
|
runResetLocalSeat,
|
|
@@ -104,6 +108,12 @@ import { ensureCliMcpConfigured } from './ensure-mcp-config.js';
|
|
|
104
108
|
import { configureResolvedCli } from './resolved-cli-config.js';
|
|
105
109
|
import { installBorgPlugin } from './opencode-plugin.js';
|
|
106
110
|
import { allocateOpenCodePort, connectOpenCodeDrone, createOpenCodeLaunchKickoff, injectInitialKickoff, openCodeLaunchBinding } from './opencode-drone.js';
|
|
111
|
+
import {
|
|
112
|
+
BORG_OPENCODE_LAUNCH_CORRELATION_ENV,
|
|
113
|
+
OPENCODE_SERVER_PASSWORD_ENV,
|
|
114
|
+
OPENCODE_SERVER_USERNAME,
|
|
115
|
+
OPENCODE_SERVER_USERNAME_ENV,
|
|
116
|
+
} from './opencode-launch-trust.js';
|
|
107
117
|
import { buildOpenCodeLaunchArgs, defaultApprovalIo, resolveLaunchBorgApprovals } from './cli-tool-approval.js';
|
|
108
118
|
import { isClientOwnedCubeInitArgv, runEarlyServerFacade } from './server-facade.js';
|
|
109
119
|
import { runEarlyUpdate } from './update-cmd.js';
|
|
@@ -155,7 +165,13 @@ export function launchOpenCodeProcess(options: {
|
|
|
155
165
|
process: ReturnType<typeof spawn>;
|
|
156
166
|
} {
|
|
157
167
|
const plan = createOpenCodeLaunchPlan(options.cwd, options.port, options.prompt, options.passthroughArgs);
|
|
158
|
-
const launchEnv = {
|
|
168
|
+
const launchEnv = {
|
|
169
|
+
...options.env,
|
|
170
|
+
BORG_OPENCODE_PORT: plan.envPort,
|
|
171
|
+
[OPENCODE_SERVER_USERNAME_ENV]: OPENCODE_SERVER_USERNAME,
|
|
172
|
+
[OPENCODE_SERVER_PASSWORD_ENV]: options.kickoff.apiPassword,
|
|
173
|
+
[BORG_OPENCODE_LAUNCH_CORRELATION_ENV]: options.kickoff.correlationIdentity,
|
|
174
|
+
};
|
|
159
175
|
// OpenCode's bind can still race this allocation; client#298 tracks the
|
|
160
176
|
// residual pre-bind window outside this slice.
|
|
161
177
|
const child = (options.spawnProcess ?? spawn)('opencode', plan.launchArgs, {
|
|
@@ -165,6 +181,7 @@ export function launchOpenCodeProcess(options: {
|
|
|
165
181
|
});
|
|
166
182
|
(options.connect ?? connectOpenCodeDrone)({
|
|
167
183
|
serverUrl: plan.serverUrl,
|
|
184
|
+
apiPassword: options.kickoff.apiPassword,
|
|
168
185
|
directory: options.cwd,
|
|
169
186
|
droneLabel: options.droneLabel,
|
|
170
187
|
cubeName: options.cubeName,
|
|
@@ -251,6 +268,24 @@ async function main() {
|
|
|
251
268
|
const code = await runAssimilateEntry(process.argv.slice(3));
|
|
252
269
|
process.exit(code);
|
|
253
270
|
}
|
|
271
|
+
if (process.argv[2] === 'clone') {
|
|
272
|
+
const parsed = parseCloneArgs(process.argv.slice(3));
|
|
273
|
+
if (!parsed.ok) {
|
|
274
|
+
process.stderr.write(chalk.red(`${consolePrefix()}◼ borg clone: ${safeCloneParseError(parsed)}\n`));
|
|
275
|
+
process.stderr.write(`Run \`borg clone --help\` for usage.\n`);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
process.exit(await runClone(parsed.args, buildDefaultCloneDeps()));
|
|
279
|
+
}
|
|
280
|
+
if (process.argv[2] === 'quickstart') {
|
|
281
|
+
const parsed = parseQuickstartArgs(process.argv.slice(3));
|
|
282
|
+
if (!parsed.ok) {
|
|
283
|
+
process.stderr.write(chalk.red(`${consolePrefix()}◼ borg quickstart: ${parsed.error}\n`));
|
|
284
|
+
process.stderr.write(`Run \`borg quickstart --help\` for usage.\n`);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
process.exit(await runQuickstart(parsed.args, buildDefaultQuickstartDeps()));
|
|
288
|
+
}
|
|
254
289
|
if (process.argv[2] === 'reset-local-connection') {
|
|
255
290
|
const parsed = parseResetLocalSeatArgs(process.argv.slice(3));
|
|
256
291
|
if (!parsed.ok) {
|
|
@@ -604,9 +639,8 @@ async function main() {
|
|
|
604
639
|
monitorClause,
|
|
605
640
|
codexWakePathClause,
|
|
606
641
|
});
|
|
607
|
-
// This stays separate from the shared kickoff so
|
|
608
|
-
//
|
|
609
|
-
// and later uses the nonce to bind its separately spawned MCP child.
|
|
642
|
+
// This stays separate from the shared kickoff so OpenCode can carry
|
|
643
|
+
// launch-only API and correlation identities outside argv and prompt text.
|
|
610
644
|
let openCodeKickoff: ReturnType<typeof createOpenCodeLaunchKickoff> | null = null;
|
|
611
645
|
let openCodePort: number | undefined;
|
|
612
646
|
let launchArgs: string[];
|
package/src/cli-help.ts
CHANGED
|
@@ -51,6 +51,35 @@ export function launchAllHelpText(version: string): string {
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
export function quickstartHelpText(version: string): string {
|
|
55
|
+
return (
|
|
56
|
+
`borg quickstart (borgmcp ${version}) — create, staff, and launch this repository's cube\n\n` +
|
|
57
|
+
`Usage:\n` +
|
|
58
|
+
` borg quickstart [options]\n\n` +
|
|
59
|
+
`Options:\n` +
|
|
60
|
+
` --template ${NEW_CUBE_TEMPLATE_OPTIONS} Choose the new-cube template without prompting\n` +
|
|
61
|
+
` --role <slug>[:<count>] Fully specify the roster (repeatable)\n` +
|
|
62
|
+
` --yes, -y Accept the displayed plan\n` +
|
|
63
|
+
` --help, -h Show this help\n\n` +
|
|
64
|
+
`Quickstart requires a running Borg server and never starts one. Rerun the same\n` +
|
|
65
|
+
`command after a partial failure; existing drones are kept and skipped.\n`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function cloneHelpText(version: string): string {
|
|
70
|
+
return (
|
|
71
|
+
`borg clone (borgmcp ${version}) — clone a repository and run quickstart\n\n` +
|
|
72
|
+
`Usage:\n` +
|
|
73
|
+
` borg clone <repository-url> [directory] [--no-launch]\n\n` +
|
|
74
|
+
`Options:\n` +
|
|
75
|
+
` --no-launch Stop after the checkout is ready; do not create or launch a cube\n` +
|
|
76
|
+
` --help, -h Show this help\n\n` +
|
|
77
|
+
`A repeated command reuses a checkout only when its origin matches. Default\n` +
|
|
78
|
+
`destination names avoid non-repository collisions. Credential-bearing URLs are\n` +
|
|
79
|
+
`refused; use a Git credential helper or SSH configuration instead.\n`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
54
83
|
export function seatsHelpText(version: string): string {
|
|
55
84
|
return (
|
|
56
85
|
`borg drones (borgmcp ${version}) — list this machine's registered drones\n\n` +
|
|
@@ -91,7 +120,9 @@ export function clientSubcommandHelpText(
|
|
|
91
120
|
if (!args.some(isHelpFlag)) return null;
|
|
92
121
|
switch (command) {
|
|
93
122
|
case 'setup': return setupHelpText(version);
|
|
123
|
+
case 'clone': return cloneHelpText(version);
|
|
94
124
|
case 'assimilate': return assimilateHelpText(version);
|
|
125
|
+
case 'quickstart': return quickstartHelpText(version);
|
|
95
126
|
case 'reset-local-connection': return resetLocalSeatHelpText(version);
|
|
96
127
|
case 'recover-enrollment': return recoverEnrollmentHelpText(version);
|
|
97
128
|
case 'cleanup': return cleanupHelpText(version);
|
|
@@ -107,7 +138,7 @@ export function setupNextStepsText(): string {
|
|
|
107
138
|
return (
|
|
108
139
|
`◼ Next steps:\n` +
|
|
109
140
|
`1. Run \`borg server start\` and leave that terminal open.\n` +
|
|
110
|
-
`2. In a second terminal, cd into your project's Git repository and run \`borg
|
|
141
|
+
`2. In a second terminal, cd into your project's Git repository and run \`borg quickstart\`.\n`
|
|
111
142
|
);
|
|
112
143
|
}
|
|
113
144
|
|
|
@@ -129,6 +160,8 @@ export function topLevelHelpText(version: string): string {
|
|
|
129
160
|
` borg setup Set up borg MCP server + agent CLI integration\n` +
|
|
130
161
|
` borg update Update the client and installed local server together\n` +
|
|
131
162
|
` borg doctor Check agent hook commands, versions, configs, and the OpenCode plugin\n` +
|
|
163
|
+
` borg clone <url> [dir] Clone a repository, then create and launch its cube\n` +
|
|
164
|
+
` borg quickstart Create a cube and a drone for every role, then launch them\n` +
|
|
132
165
|
` borg assimilate [role] Join or create a cube\n` +
|
|
133
166
|
` borg assimilate --host <host> Join or create on an explicit server\n` +
|
|
134
167
|
` borg assimilate --worktree <name> Spawn a worktree drone (in ~/.borg/worktrees/<repo>/<name>)\n` +
|
package/src/clone-cmd.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { hasCloneCredentials, redactCloneSecrets } from './clone-security.js';
|
|
6
|
+
import type { CloneArgs } from './parse-clone-args.js';
|
|
7
|
+
import { buildDefaultQuickstartDeps, runQuickstart } from './quickstart-cmd.js';
|
|
8
|
+
import { shellEscape } from './shell-escape.js';
|
|
9
|
+
|
|
10
|
+
export interface GitRunResult {
|
|
11
|
+
status: number | null;
|
|
12
|
+
stdout: string;
|
|
13
|
+
stderr: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CloneDeps {
|
|
17
|
+
cwd: () => string;
|
|
18
|
+
chdir: (path: string) => void;
|
|
19
|
+
runSync: (cmd: string, args: string[], cwd?: string) => GitRunResult;
|
|
20
|
+
pathExists: (path: string) => boolean;
|
|
21
|
+
isDirectory: (path: string) => boolean;
|
|
22
|
+
readDirectory: (path: string) => string[];
|
|
23
|
+
createDirectory: (path: string) => boolean;
|
|
24
|
+
removeTree: (path: string) => void;
|
|
25
|
+
quickstart: (cwd: string) => Promise<number>;
|
|
26
|
+
stdout: (text: string) => void;
|
|
27
|
+
stderr: (text: string) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f]/;
|
|
31
|
+
const ALLOWED_SCHEMES = new Set(['file:', 'git:', 'git+ssh:', 'http:', 'https:', 'ssh:']);
|
|
32
|
+
const SCP_REMOTE_RE = /^[^@\s/:]+@[^:\s]+:[^\s]+$/;
|
|
33
|
+
|
|
34
|
+
function defaultRunSync(cmd: string, args: string[], cwd?: string): GitRunResult {
|
|
35
|
+
const result = spawnSync(cmd, args, { cwd, encoding: 'utf8' });
|
|
36
|
+
return {
|
|
37
|
+
status: result.status,
|
|
38
|
+
stdout: result.stdout ?? '',
|
|
39
|
+
stderr: result.stderr ?? (result.error instanceof Error ? result.error.message : ''),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildDefaultCloneDeps(): CloneDeps {
|
|
44
|
+
return {
|
|
45
|
+
cwd: () => process.cwd(),
|
|
46
|
+
chdir: (path) => process.chdir(path),
|
|
47
|
+
runSync: defaultRunSync,
|
|
48
|
+
pathExists: existsSync,
|
|
49
|
+
isDirectory: (path) => {
|
|
50
|
+
try { return statSync(path).isDirectory(); } catch { return false; }
|
|
51
|
+
},
|
|
52
|
+
readDirectory: (path) => readdirSync(path),
|
|
53
|
+
createDirectory: (path) => {
|
|
54
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
55
|
+
try {
|
|
56
|
+
mkdirSync(path);
|
|
57
|
+
return true;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof Error && 'code' in error && error.code === 'EEXIST') return false;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
removeTree: (path) => rmSync(path, { recursive: true, force: true }),
|
|
64
|
+
quickstart: async (cwd) => {
|
|
65
|
+
process.chdir(cwd);
|
|
66
|
+
return runQuickstart({ roles: [], yes: false }, buildDefaultQuickstartDeps());
|
|
67
|
+
},
|
|
68
|
+
stdout: (text) => process.stdout.write(text),
|
|
69
|
+
stderr: (text) => process.stderr.write(text),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function validateCloneRepositoryUrl(value: string): { ok: true } | { ok: false; error: string } {
|
|
74
|
+
if (!value || value.trim() !== value || /\s/.test(value) || CONTROL_RE.test(value)) {
|
|
75
|
+
return { ok: false, error: 'repository URL contains whitespace or control characters' };
|
|
76
|
+
}
|
|
77
|
+
if (value.startsWith('-')) return { ok: false, error: 'repository URL must not start with a hyphen' };
|
|
78
|
+
if (hasCloneCredentials(value)) {
|
|
79
|
+
return { ok: false, error: 'credential-bearing repository URLs are not accepted; use a credential helper or SSH configuration instead' };
|
|
80
|
+
}
|
|
81
|
+
if (SCP_REMOTE_RE.test(value)) return { ok: true };
|
|
82
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1];
|
|
83
|
+
if (!scheme) return { ok: true }; // Local path.
|
|
84
|
+
if (!value.toLowerCase().startsWith(`${scheme.toLowerCase()}://`) && scheme.toLowerCase() !== 'file') {
|
|
85
|
+
return { ok: false, error: 'repository URL is not a supported hierarchical URL' };
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const parsed = new URL(value);
|
|
89
|
+
if (!ALLOWED_SCHEMES.has(parsed.protocol)) return { ok: false, error: `unsupported repository URL scheme ${parsed.protocol}` };
|
|
90
|
+
if (parsed.search || parsed.hash) return { ok: false, error: 'repository URLs with query strings or fragments are not accepted' };
|
|
91
|
+
if (parsed.protocol !== 'file:' && !parsed.hostname) return { ok: false, error: 'repository URL must include a host' };
|
|
92
|
+
if (!parsed.pathname || parsed.pathname === '/') return { ok: false, error: 'repository URL must include a repository path' };
|
|
93
|
+
return { ok: true };
|
|
94
|
+
} catch {
|
|
95
|
+
return { ok: false, error: 'repository URL is not valid' };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sourceName(value: string): string {
|
|
100
|
+
let leaf = value;
|
|
101
|
+
try {
|
|
102
|
+
const parsed = new URL(value);
|
|
103
|
+
leaf = parsed.protocol === 'file:' ? fileURLToPath(parsed) : parsed.pathname;
|
|
104
|
+
} catch {
|
|
105
|
+
if (SCP_REMOTE_RE.test(value)) leaf = value.slice(value.indexOf(':') + 1);
|
|
106
|
+
}
|
|
107
|
+
const name = basename(leaf.replace(/\/+$/, '')).replace(/\.git$/i, '')
|
|
108
|
+
.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
109
|
+
return name || 'repository';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function remoteKey(value: string, base: string): string {
|
|
113
|
+
if (SCP_REMOTE_RE.test(value)) {
|
|
114
|
+
const colon = value.indexOf(':');
|
|
115
|
+
const login = value.slice(0, colon);
|
|
116
|
+
const at = login.lastIndexOf('@');
|
|
117
|
+
const user = login.slice(0, at);
|
|
118
|
+
const host = login.slice(at + 1).toLowerCase();
|
|
119
|
+
return `ssh://${user}@${host}/${value.slice(colon + 1).replace(/\/+$/, '').replace(/\.git$/i, '')}`;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const parsed = new URL(value);
|
|
123
|
+
if (parsed.protocol === 'file:') return `file:${resolve(fileURLToPath(parsed))}`;
|
|
124
|
+
const user = parsed.username ? `${parsed.username}@` : '';
|
|
125
|
+
const port = parsed.port ? `:${parsed.port}` : '';
|
|
126
|
+
return `${parsed.protocol}//${user}${parsed.hostname.toLowerCase()}${port}${parsed.pathname.replace(/\/+$/, '').replace(/\.git$/i, '')}`;
|
|
127
|
+
} catch {
|
|
128
|
+
return `file:${resolve(base, value)}`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function readOrigin(deps: CloneDeps, directory: string): string | null {
|
|
133
|
+
const result = deps.runSync('git', ['remote', 'get-url', 'origin'], directory);
|
|
134
|
+
return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function repositoryAt(deps: CloneDeps, directory: string): boolean {
|
|
138
|
+
return deps.runSync('git', ['rev-parse', '--is-inside-work-tree'], directory).status === 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function validateDestination(value: string): string | null {
|
|
142
|
+
if (!value || value.trim() !== value || CONTROL_RE.test(value) || value.startsWith('-') || hasCloneCredentials(value)) {
|
|
143
|
+
return 'destination contains an unsafe path value';
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function chooseDestination(deps: CloneDeps, args: CloneArgs): { path: string } | { error: string } {
|
|
149
|
+
const base = deps.cwd();
|
|
150
|
+
if (args.destination !== undefined) {
|
|
151
|
+
const error = validateDestination(args.destination);
|
|
152
|
+
return error ? { error } : { path: resolve(base, args.destination) };
|
|
153
|
+
}
|
|
154
|
+
const name = sourceName(args.repositoryUrl);
|
|
155
|
+
for (let n = 1; n < 10_000; n += 1) {
|
|
156
|
+
const candidate = resolve(base, n === 1 ? name : `${name}-${n}`);
|
|
157
|
+
if (!deps.pathExists(candidate)) return { path: candidate };
|
|
158
|
+
if (deps.isDirectory(candidate) && repositoryAt(deps, candidate)) {
|
|
159
|
+
const origin = readOrigin(deps, candidate);
|
|
160
|
+
if (origin && !hasCloneCredentials(origin) && remoteKey(origin, candidate) === remoteKey(args.repositoryUrl, base)) {
|
|
161
|
+
return { path: candidate };
|
|
162
|
+
}
|
|
163
|
+
if (n === 1) return { error: `existing checkout at ${candidate} points at another remote; choose an explicit destination` };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { error: `could not find a collision-safe destination for ${name}` };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function runClone(args: CloneArgs, rawDeps: CloneDeps): Promise<number> {
|
|
170
|
+
const deps: CloneDeps = {
|
|
171
|
+
...rawDeps,
|
|
172
|
+
stdout: (text) => rawDeps.stdout(redactCloneSecrets(text)),
|
|
173
|
+
stderr: (text) => rawDeps.stderr(redactCloneSecrets(text)),
|
|
174
|
+
};
|
|
175
|
+
const valid = validateCloneRepositoryUrl(args.repositoryUrl);
|
|
176
|
+
if (!valid.ok) {
|
|
177
|
+
deps.stderr(`borg clone: ${valid.error}.\n`);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
const selected = chooseDestination(deps, args);
|
|
181
|
+
if ('error' in selected) {
|
|
182
|
+
deps.stderr(`borg clone: ${selected.error}.\n`);
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
const destination = selected.path;
|
|
186
|
+
let cloned = false;
|
|
187
|
+
const destinationExisted = deps.pathExists(destination);
|
|
188
|
+
const emptyDestination = destinationExisted && deps.isDirectory(destination) && deps.readDirectory(destination).length === 0;
|
|
189
|
+
if (destinationExisted && !emptyDestination) {
|
|
190
|
+
if (!deps.isDirectory(destination) || !repositoryAt(deps, destination)) {
|
|
191
|
+
deps.stderr(`borg clone: destination ${destination} exists and is not a Git checkout; it was left untouched.\n`);
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
const origin = readOrigin(deps, destination);
|
|
195
|
+
if (!origin || hasCloneCredentials(origin)) {
|
|
196
|
+
deps.stderr(`borg clone: the existing checkout has no safe, readable origin remote; it was left untouched.\n`);
|
|
197
|
+
return 1;
|
|
198
|
+
}
|
|
199
|
+
if (remoteKey(origin, destination) !== remoteKey(args.repositoryUrl, deps.cwd())) {
|
|
200
|
+
deps.stderr(`borg clone: remote mismatch at ${destination}; existing ${origin}, requested ${args.repositoryUrl}. The checkout was left untouched.\n`);
|
|
201
|
+
return 1;
|
|
202
|
+
}
|
|
203
|
+
deps.stdout(`Reusing existing checkout at ${destination}; its origin remote matches.\n`);
|
|
204
|
+
} else {
|
|
205
|
+
const createdDestination = destinationExisted ? false : deps.createDirectory(destination);
|
|
206
|
+
if (!destinationExisted && !createdDestination) {
|
|
207
|
+
deps.stderr(`borg clone: destination ${destination} appeared while preparing the clone; it was left untouched. Retry with another directory.\n`);
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
const result = deps.runSync('git', ['clone', '--', args.repositoryUrl, destination], deps.cwd());
|
|
211
|
+
if (result.status !== 0) {
|
|
212
|
+
if (createdDestination && deps.pathExists(destination)) {
|
|
213
|
+
deps.removeTree(destination);
|
|
214
|
+
}
|
|
215
|
+
deps.stderr(
|
|
216
|
+
`borg clone: clone failed${result.stderr.trim() ? `: ${result.stderr.trim()}` : ''}.\n` +
|
|
217
|
+
`Rollback: ${destinationExisted
|
|
218
|
+
? `the pre-existing empty destination ${destination} was preserved; inspect it for partial Git files`
|
|
219
|
+
: deps.pathExists(destination) ? `partial checkout remains at ${destination}` : `the directory borg created at ${destination} was removed`}.\n`,
|
|
220
|
+
);
|
|
221
|
+
return 1;
|
|
222
|
+
}
|
|
223
|
+
cloned = true;
|
|
224
|
+
deps.stdout(`Cloned ${sourceName(args.repositoryUrl)} into ${destination}.\n`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (args.noLaunch) {
|
|
228
|
+
deps.stdout(
|
|
229
|
+
`Checkout ready at ${destination}. No cube or drone was created.\n` +
|
|
230
|
+
`Next: cd ${shellEscape(destination)} && borg quickstart\n`,
|
|
231
|
+
);
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
deps.chdir(destination);
|
|
235
|
+
const code = await deps.quickstart(destination);
|
|
236
|
+
if (code !== 0) {
|
|
237
|
+
deps.stderr(
|
|
238
|
+
`The checkout${cloned ? '' : ' you already had'} is ready at ${destination}, but quickstart did not finish. ` +
|
|
239
|
+
`It was left untouched; cd there and run \`borg quickstart\` again.\n`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return code;
|
|
243
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Redaction and fail-closed credential detection for untrusted Git remotes. */
|
|
2
|
+
|
|
3
|
+
const URL_USERINFO_RE = /([a-z][a-z0-9+.-]*:\/\/)([^/\s@]+)@/gi;
|
|
4
|
+
const URL_SUFFIX_RE = /([a-z][a-z0-9+.-]*:\/\/[^\s?#)]+)([?#])[^\s)]*/gi;
|
|
5
|
+
const NAMED_SECRET_RE = /([?&](?:access[_-]?token|api[_-]?key|auth(?:entication)?|credential|password|passwd|private[_-]?key|secret|token)=)[^&#\s)]+/gi;
|
|
6
|
+
const NAMED_SECRET_TEST_RE = new RegExp(NAMED_SECRET_RE.source, 'i');
|
|
7
|
+
const OPTION_SECRET_RE = /((?:^|[\s("'`])--(?:access[_-]?token|api[_-]?key|auth(?:entication)?|credential|password|passwd|private[_-]?key|secret|token)=)[^\s)]*/gi;
|
|
8
|
+
const SCP_REMOTE_RE = /^[^@\s/:]+@[^:\s]+:[^\s]+$/;
|
|
9
|
+
|
|
10
|
+
export function redactCloneSecrets(value: string): string {
|
|
11
|
+
return value
|
|
12
|
+
.replace(URL_USERINFO_RE, '$1<credentials>@')
|
|
13
|
+
.replace(URL_SUFFIX_RE, '$1$2<redacted>')
|
|
14
|
+
.replace(NAMED_SECRET_RE, '$1<redacted>')
|
|
15
|
+
.replace(OPTION_SECRET_RE, '$1<redacted>')
|
|
16
|
+
.replace(/(^|[\s("'`])[^/\s:@]+:[^@\s/]+@(?=[^\s/])/g, '$1<credentials>@');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* True means the value must never reach subprocess argv, output, or Git config.
|
|
21
|
+
* Opaque `scheme:...@...` forms fail closed; URL() accepts those but exposes no
|
|
22
|
+
* username/password fields, which is precisely the class that escaped the old flow.
|
|
23
|
+
*/
|
|
24
|
+
export function hasCloneCredentials(value: string): boolean {
|
|
25
|
+
if (NAMED_SECRET_TEST_RE.test(value)) return true;
|
|
26
|
+
if (SCP_REMOTE_RE.test(value)) return false;
|
|
27
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1].toLowerCase();
|
|
28
|
+
if (scheme && !value.toLowerCase().startsWith(`${scheme}://`) && scheme !== 'file') {
|
|
29
|
+
return value.includes('@');
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const parsed = new URL(value);
|
|
33
|
+
if (parsed.protocol === 'ssh:' || parsed.protocol === 'git+ssh:') {
|
|
34
|
+
return parsed.password.length > 0;
|
|
35
|
+
}
|
|
36
|
+
return parsed.username.length > 0 || parsed.password.length > 0;
|
|
37
|
+
} catch {
|
|
38
|
+
return /[^/\s:@]+:[^@\s/]+@/.test(value);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/config-utils.ts
CHANGED
|
@@ -24,6 +24,10 @@ 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
26
|
import { BORG_LAUNCH_EXPECTED_SEAT_ENV } from './cubes.js';
|
|
27
|
+
import {
|
|
28
|
+
OPENCODE_SERVER_PASSWORD_ENV,
|
|
29
|
+
OPENCODE_SERVER_PASSWORD_REFERENCE,
|
|
30
|
+
} from './opencode-launch-trust.js';
|
|
27
31
|
|
|
28
32
|
// Get __dirname equivalent in ESM
|
|
29
33
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -53,6 +57,7 @@ const CLAUDE_CONFIG_PATH = path.join(CONFIG_HOME, '.claude.json');
|
|
|
53
57
|
const CODEX_CONFIG_PATH = path.join(CONFIG_HOME, '.codex', 'config.toml');
|
|
54
58
|
const CODEX_HOOKS_PATH = path.join(CONFIG_HOME, '.codex', 'hooks.json');
|
|
55
59
|
const OPENCODE_CONFIG_PATH = path.join(CONFIG_HOME, '.config', 'opencode', 'opencode.json');
|
|
60
|
+
const OPENCODE_GLOBAL_CONFIG_FILENAMES = ['config.json', 'opencode.json', 'opencode.jsonc'] as const;
|
|
56
61
|
const MCP_SERVER_NAME = 'borg';
|
|
57
62
|
|
|
58
63
|
function settingsPath(): string {
|
|
@@ -80,6 +85,125 @@ function readJsonFile(p: string): any {
|
|
|
80
85
|
return JSON.parse(text);
|
|
81
86
|
}
|
|
82
87
|
|
|
88
|
+
function parseJsonc(text: string): unknown {
|
|
89
|
+
let stripped = '';
|
|
90
|
+
let inString = false;
|
|
91
|
+
let escaped = false;
|
|
92
|
+
let lineComment = false;
|
|
93
|
+
let blockComment = false;
|
|
94
|
+
|
|
95
|
+
for (let index = 0; index < text.length; index++) {
|
|
96
|
+
const char = text[index];
|
|
97
|
+
const next = text[index + 1];
|
|
98
|
+
if (lineComment) {
|
|
99
|
+
if (char === '\n' || char === '\r') {
|
|
100
|
+
lineComment = false;
|
|
101
|
+
stripped += char;
|
|
102
|
+
} else {
|
|
103
|
+
stripped += ' ';
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (blockComment) {
|
|
108
|
+
if (char === '*' && next === '/') {
|
|
109
|
+
stripped += ' ';
|
|
110
|
+
index++;
|
|
111
|
+
blockComment = false;
|
|
112
|
+
} else {
|
|
113
|
+
stripped += char === '\n' || char === '\r' ? char : ' ';
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (inString) {
|
|
118
|
+
stripped += char;
|
|
119
|
+
if (escaped) escaped = false;
|
|
120
|
+
else if (char === '\\') escaped = true;
|
|
121
|
+
else if (char === '"') inString = false;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (char === '"') {
|
|
125
|
+
inString = true;
|
|
126
|
+
stripped += char;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (char === '/' && next === '/') {
|
|
130
|
+
stripped += ' ';
|
|
131
|
+
index++;
|
|
132
|
+
lineComment = true;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (char === '/' && next === '*') {
|
|
136
|
+
stripped += ' ';
|
|
137
|
+
index++;
|
|
138
|
+
blockComment = true;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
stripped += char;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (inString || blockComment) throw new SyntaxError('unterminated JSONC token');
|
|
145
|
+
|
|
146
|
+
let withoutTrailingCommas = '';
|
|
147
|
+
inString = false;
|
|
148
|
+
escaped = false;
|
|
149
|
+
for (let index = 0; index < stripped.length; index++) {
|
|
150
|
+
const char = stripped[index];
|
|
151
|
+
if (inString) {
|
|
152
|
+
withoutTrailingCommas += char;
|
|
153
|
+
if (escaped) escaped = false;
|
|
154
|
+
else if (char === '\\') escaped = true;
|
|
155
|
+
else if (char === '"') inString = false;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (char === '"') {
|
|
159
|
+
inString = true;
|
|
160
|
+
withoutTrailingCommas += char;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (char === ',') {
|
|
164
|
+
let lookahead = index + 1;
|
|
165
|
+
while (/\s/.test(stripped[lookahead] ?? '')) lookahead++;
|
|
166
|
+
if (stripped[lookahead] === '}' || stripped[lookahead] === ']') continue;
|
|
167
|
+
}
|
|
168
|
+
withoutTrailingCommas += char;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return JSON.parse(withoutTrailingCommas.replace(/^\uFEFF/, ''));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
175
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function mergeOpenCodeConfigValue(target: unknown, source: unknown): unknown {
|
|
179
|
+
if (!isPlainRecord(target) || !isPlainRecord(source)) return source;
|
|
180
|
+
const merged = Object.assign(Object.create(null) as Record<string, unknown>, target);
|
|
181
|
+
for (const [key, value] of Object.entries(source)) {
|
|
182
|
+
merged[key] = key in merged ? mergeOpenCodeConfigValue(merged[key], value) : value;
|
|
183
|
+
}
|
|
184
|
+
return merged;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function readOpenCodeGlobalConfig(configPath: string): Record<string, unknown> {
|
|
188
|
+
const basename = path.basename(configPath);
|
|
189
|
+
const paths = OPENCODE_GLOBAL_CONFIG_FILENAMES.includes(
|
|
190
|
+
basename as typeof OPENCODE_GLOBAL_CONFIG_FILENAMES[number],
|
|
191
|
+
)
|
|
192
|
+
? OPENCODE_GLOBAL_CONFIG_FILENAMES.map((filename) => path.join(path.dirname(configPath), filename))
|
|
193
|
+
: [configPath];
|
|
194
|
+
let merged: unknown = {};
|
|
195
|
+
for (const candidate of paths) {
|
|
196
|
+
if (!fs.existsSync(candidate)) continue;
|
|
197
|
+
const text = fs.readFileSync(candidate, 'utf-8');
|
|
198
|
+
if (text.length === 0) continue;
|
|
199
|
+
const parsed = parseJsonc(text);
|
|
200
|
+
if (!isPlainRecord(parsed)) throw new TypeError(`OpenCode config is not an object: ${candidate}`);
|
|
201
|
+
merged = mergeOpenCodeConfigValue(merged, parsed);
|
|
202
|
+
}
|
|
203
|
+
if (!isPlainRecord(merged)) throw new TypeError('OpenCode global config is not an object');
|
|
204
|
+
return merged;
|
|
205
|
+
}
|
|
206
|
+
|
|
83
207
|
function writeJsonFile(p: string, data: any): void {
|
|
84
208
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
85
209
|
fs.writeFileSync(p, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
@@ -1162,22 +1286,18 @@ export function isCodexUserPromptSubmitHookRegistered(hooksPath: string = CODEX_
|
|
|
1162
1286
|
// ─── OpenCode MCP integration ────────────────────────────────────────────
|
|
1163
1287
|
|
|
1164
1288
|
/**
|
|
1165
|
-
* Detect whether the borg MCP server is
|
|
1166
|
-
* config
|
|
1289
|
+
* Detect whether the borg MCP server is registered in OpenCode's effective
|
|
1290
|
+
* global config. OpenCode 1.18.15 loads `config.json`, `opencode.json`, then
|
|
1291
|
+
* `opencode.jsonc`, merging later files over earlier ones.
|
|
1167
1292
|
*
|
|
1168
|
-
* Reads
|
|
1293
|
+
* Reads JSON or JSONC and checks the effective `mcp.borg` entry for
|
|
1169
1294
|
* `type: "local"`. Safe-default: any read error returns `false`.
|
|
1170
1295
|
*/
|
|
1171
1296
|
export function isOpenCodeMcpServerConfigured(
|
|
1172
1297
|
configPath: string = path.join(borgHomeRoot(), '.config', 'opencode', 'opencode.json')
|
|
1173
1298
|
): boolean {
|
|
1174
1299
|
try {
|
|
1175
|
-
|
|
1176
|
-
const text = fs.readFileSync(configPath, 'utf-8');
|
|
1177
|
-
if (!text.trim()) return false;
|
|
1178
|
-
const parsed = JSON.parse(text);
|
|
1179
|
-
if (!parsed || typeof parsed !== 'object') return false;
|
|
1180
|
-
const borgServer = (parsed as any).mcp?.borg;
|
|
1300
|
+
const borgServer = (readOpenCodeGlobalConfig(configPath) as any).mcp?.borg;
|
|
1181
1301
|
if (!borgServer || typeof borgServer !== 'object') return false;
|
|
1182
1302
|
return borgServer.type === 'local';
|
|
1183
1303
|
} catch {
|
|
@@ -1186,22 +1306,24 @@ export function isOpenCodeMcpServerConfigured(
|
|
|
1186
1306
|
}
|
|
1187
1307
|
|
|
1188
1308
|
/**
|
|
1189
|
-
* Launch-time OpenCode registration check.
|
|
1190
|
-
*
|
|
1191
|
-
* identified by its non-empty expected-seat
|
|
1192
|
-
*
|
|
1309
|
+
* Launch-time OpenCode registration check. Every launch requires the exact
|
|
1310
|
+
* password substitution that carries its ephemeral credential into OpenCode's
|
|
1311
|
+
* MCP child. A targeted `borg launch`, identified by its non-empty expected-seat
|
|
1312
|
+
* marker, additionally requires the substitution that carries that marker.
|
|
1193
1313
|
*/
|
|
1194
1314
|
export function isOpenCodeMcpServerConfiguredForLaunch(
|
|
1195
1315
|
configPath: string = path.join(borgHomeRoot(), '.config', 'opencode', 'opencode.json'),
|
|
1196
1316
|
env: NodeJS.ProcessEnv = process.env,
|
|
1197
1317
|
): boolean {
|
|
1198
1318
|
if (!isOpenCodeMcpServerConfigured(configPath)) return false;
|
|
1199
|
-
if (!env[BORG_LAUNCH_EXPECTED_SEAT_ENV]) return true;
|
|
1200
1319
|
try {
|
|
1201
|
-
const borgServer =
|
|
1320
|
+
const borgServer = (readOpenCodeGlobalConfig(configPath) as any).mcp?.borg;
|
|
1202
1321
|
const environment = borgServer?.environment ?? borgServer?.env;
|
|
1203
|
-
|
|
1204
|
-
|
|
1322
|
+
if (environment?.[OPENCODE_SERVER_PASSWORD_ENV] !== OPENCODE_SERVER_PASSWORD_REFERENCE) {
|
|
1323
|
+
return false;
|
|
1324
|
+
}
|
|
1325
|
+
if (!env[BORG_LAUNCH_EXPECTED_SEAT_ENV]) return true;
|
|
1326
|
+
return environment?.[BORG_LAUNCH_EXPECTED_SEAT_ENV] === OPENCODE_LAUNCH_EXPECTED_SEAT_REFERENCE;
|
|
1205
1327
|
} catch {
|
|
1206
1328
|
return false;
|
|
1207
1329
|
}
|
|
@@ -1278,11 +1400,11 @@ export function addOpenCodeLaunchAccess(
|
|
|
1278
1400
|
|
|
1279
1401
|
/**
|
|
1280
1402
|
* Add borg MCP server to OpenCode using `opencode mcp add` CLI.
|
|
1281
|
-
* Pins activation and agent-kind signals plus
|
|
1282
|
-
* for the launch-scoped expected seat. OpenCode resolves
|
|
1283
|
-
* own launch environment before starting the MCP child,
|
|
1284
|
-
*
|
|
1285
|
-
*
|
|
1403
|
+
* Pins activation and agent-kind signals plus OpenCode config substitutions
|
|
1404
|
+
* for the launch-scoped password and expected seat. OpenCode resolves
|
|
1405
|
+
* `{env:NAME}` from its own launch environment before starting the MCP child,
|
|
1406
|
+
* so no launch credential is persisted. Existing configs with BORG_OPENCODE
|
|
1407
|
+
* remain supported by the runtime fallback after launch-time self-healing.
|
|
1286
1408
|
*/
|
|
1287
1409
|
export function addOpenCodeMcpServer(): void {
|
|
1288
1410
|
try {
|
|
@@ -1295,8 +1417,10 @@ export function addOpenCodeMcpServer(): void {
|
|
|
1295
1417
|
: '';
|
|
1296
1418
|
const launchExpectedSeatEnvArg =
|
|
1297
1419
|
` --env ${BORG_LAUNCH_EXPECTED_SEAT_ENV}=${shellQuote(OPENCODE_LAUNCH_EXPECTED_SEAT_REFERENCE)}`;
|
|
1420
|
+
const apiPasswordEnvArg =
|
|
1421
|
+
` --env ${OPENCODE_SERVER_PASSWORD_ENV}=${shellQuote(OPENCODE_SERVER_PASSWORD_REFERENCE)}`;
|
|
1298
1422
|
execSync(
|
|
1299
|
-
`opencode mcp add borg --env BORG_SESSION=1 --env BORG_AGENT_KIND=opencode --env BORG_OPENCODE=1${apiUrlEnvArg}${stateRootEnvArg}${launchExpectedSeatEnvArg} -- ${shellQuote(MCP_COMMAND)}`,
|
|
1423
|
+
`opencode mcp add borg --env BORG_SESSION=1 --env BORG_AGENT_KIND=opencode --env BORG_OPENCODE=1${apiUrlEnvArg}${stateRootEnvArg}${launchExpectedSeatEnvArg}${apiPasswordEnvArg} -- ${shellQuote(MCP_COMMAND)}`,
|
|
1300
1424
|
{ stdio: 'inherit', env: borgAgentConfigEnv(process.env) }
|
|
1301
1425
|
);
|
|
1302
1426
|
} catch (error: any) {
|