borgmcp 4.6.1 → 4.7.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/assimilate-cmd.d.ts +8 -23
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +9 -5
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +3 -16
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/claude.d.ts +5 -3
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +82 -30
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +1 -0
- package/dist/cli-help.js.map +1 -1
- package/dist/codex-launch.d.ts +1 -1
- package/dist/codex-launch.d.ts.map +1 -1
- package/dist/codex-launch.js.map +1 -1
- package/dist/config-utils.d.ts +3 -0
- package/dist/config-utils.d.ts.map +1 -1
- package/dist/config-utils.js +141 -4
- package/dist/config-utils.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +55 -18
- package/dist/config.js.map +1 -1
- package/dist/cubes.d.ts +0 -7
- package/dist/cubes.d.ts.map +1 -1
- package/dist/cubes.js +0 -9
- package/dist/cubes.js.map +1 -1
- package/dist/gc-orphan-inboxes.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -47
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts +6 -16
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js.map +1 -1
- package/dist/opencode-drone.d.ts +2 -0
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +101 -28
- package/dist/opencode-drone.js.map +1 -1
- package/dist/opencode-launch-trust.d.ts +1 -0
- package/dist/opencode-launch-trust.d.ts.map +1 -1
- package/dist/opencode-launch-trust.js +4 -0
- package/dist/opencode-launch-trust.js.map +1 -1
- package/dist/quickstart-cmd.d.ts.map +1 -1
- package/dist/quickstart-cmd.js +9 -2
- package/dist/quickstart-cmd.js.map +1 -1
- package/dist/regen-format.d.ts +1 -2
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js.map +1 -1
- package/dist/regen.js +1 -1
- package/dist/regen.js.map +1 -1
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +8 -8
- package/dist/remote-client.js.map +1 -1
- package/dist/repository-cube-init.d.ts +1 -1
- package/dist/repository-cube-init.d.ts.map +1 -1
- package/dist/repository-cube-init.js +2 -5
- package/dist/repository-cube-init.js.map +1 -1
- package/dist/repository-identity.d.ts +14 -4
- package/dist/repository-identity.d.ts.map +1 -1
- package/dist/repository-identity.js +86 -18
- package/dist/repository-identity.js.map +1 -1
- package/dist/self-path.d.ts.map +1 -1
- package/dist/self-path.js.map +1 -1
- package/dist/server-handshake.d.ts +1 -1
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +0 -1
- package/dist/server-handshake.js.map +1 -1
- package/dist/tool-manifest.js +3 -3
- package/dist/tool-manifest.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 +1 -0
- package/dist/unknown-subcommand.js.map +1 -1
- package/dist/update-cmd.js +1 -1
- package/dist/update-cmd.js.map +1 -1
- package/docs/RELEASING.md +5 -4
- package/package.json +2 -2
- package/src/assimilate-cmd.ts +29 -37
- package/src/assimilate-deps.ts +2 -18
- package/src/claude.ts +91 -31
- package/src/cli-help.ts +1 -0
- package/src/codex-launch.ts +2 -1
- package/src/config-utils.ts +156 -4
- package/src/config.ts +58 -22
- package/src/cubes.ts +0 -10
- package/src/gc-orphan-inboxes.ts +1 -1
- package/src/index.ts +24 -51
- package/src/log-stream.ts +12 -17
- package/src/opencode-drone.ts +112 -30
- package/src/opencode-launch-trust.ts +5 -0
- package/src/quickstart-cmd.ts +10 -2
- package/src/regen-format.ts +1 -3
- package/src/regen.ts +2 -3
- package/src/remote-client.ts +12 -9
- package/src/repository-cube-init.ts +3 -6
- package/src/repository-identity.ts +125 -20
- package/src/self-path.ts +0 -1
- package/src/server-handshake.ts +1 -2
- package/src/tool-manifest.ts +3 -3
- package/src/unknown-subcommand.ts +1 -0
- package/src/update-cmd.ts +1 -1
- package/dist/postinstall-banner.d.ts +0 -2
- package/dist/postinstall-banner.d.ts.map +0 -1
- package/dist/postinstall-banner.js +0 -27
- package/dist/postinstall-banner.js.map +0 -1
- package/dist/postinstall.d.ts +0 -11
- package/dist/postinstall.d.ts.map +0 -1
- package/dist/postinstall.js +0 -33
- package/dist/postinstall.js.map +0 -1
- package/dist/types.d.ts +0 -5
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/src/postinstall-banner.ts +0 -37
- package/src/postinstall.ts +0 -36
- package/src/types.ts +0 -5
package/src/claude.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { spawn } from 'child_process';
|
|
19
19
|
import { randomUUID } from 'node:crypto';
|
|
20
20
|
import { realpathSync } from 'node:fs';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
21
22
|
import { basename } from 'node:path';
|
|
22
23
|
import { createInterface } from 'node:readline/promises';
|
|
23
24
|
import { fileURLToPath } from 'node:url';
|
|
@@ -102,6 +103,7 @@ import {
|
|
|
102
103
|
addCodexUserPromptSubmitHook,
|
|
103
104
|
addProjectSessionStartHook,
|
|
104
105
|
addUserPromptSubmitHook,
|
|
106
|
+
provisionLaunchAccess,
|
|
105
107
|
removeSessionStartHook,
|
|
106
108
|
} from './config-utils.js';
|
|
107
109
|
import { ensureCliMcpConfigured } from './ensure-mcp-config.js';
|
|
@@ -118,6 +120,7 @@ import { buildOpenCodeLaunchArgs, defaultApprovalIo, resolveLaunchBorgApprovals
|
|
|
118
120
|
import { isClientOwnedCubeInitArgv, runEarlyServerFacade } from './server-facade.js';
|
|
119
121
|
import { runEarlyUpdate } from './update-cmd.js';
|
|
120
122
|
import { runDoctor, warnIfAgentIntegrationUnhealthy } from './agent-integration-health.js';
|
|
123
|
+
import { scratchRootForSeat } from './launch-access.js';
|
|
121
124
|
|
|
122
125
|
export type AssimilateDepsBuilder = typeof buildDefaultAssimilateDeps;
|
|
123
126
|
|
|
@@ -148,7 +151,7 @@ export function createOpenCodeLaunchPlan(
|
|
|
148
151
|
};
|
|
149
152
|
}
|
|
150
153
|
|
|
151
|
-
export function launchOpenCodeProcess(options: {
|
|
154
|
+
export async function launchOpenCodeProcess(options: {
|
|
152
155
|
cwd: string;
|
|
153
156
|
port: number;
|
|
154
157
|
prompt: string;
|
|
@@ -159,36 +162,76 @@ export function launchOpenCodeProcess(options: {
|
|
|
159
162
|
kickoff: ReturnType<typeof createOpenCodeLaunchKickoff>;
|
|
160
163
|
spawnProcess?: typeof spawn;
|
|
161
164
|
connect?: typeof connectOpenCodeDrone;
|
|
162
|
-
|
|
165
|
+
injectKickoff?: typeof injectInitialKickoff;
|
|
166
|
+
allocatePort?: typeof allocateOpenCodePort;
|
|
167
|
+
}): Promise<{
|
|
163
168
|
launchArgs: string[];
|
|
164
169
|
launchEnv: NodeJS.ProcessEnv;
|
|
165
170
|
process: ReturnType<typeof spawn>;
|
|
166
|
-
} {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
171
|
+
}> {
|
|
172
|
+
let port = options.port;
|
|
173
|
+
let lastExit: { code: number | null; signal: NodeJS.Signals | null } | null = null;
|
|
174
|
+
// Each child gets independent trust so a retry cannot bind a session or
|
|
175
|
+
// durable binding left by an earlier attempt.
|
|
176
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
177
|
+
const attemptKickoff = attempt === 0
|
|
178
|
+
? options.kickoff
|
|
179
|
+
: createOpenCodeLaunchKickoff(options.prompt);
|
|
180
|
+
const plan = createOpenCodeLaunchPlan(options.cwd, port, options.prompt, options.passthroughArgs);
|
|
181
|
+
const launchEnv = {
|
|
182
|
+
...options.env,
|
|
183
|
+
BORG_OPENCODE_PORT: plan.envPort,
|
|
184
|
+
[OPENCODE_SERVER_USERNAME_ENV]: OPENCODE_SERVER_USERNAME,
|
|
185
|
+
[OPENCODE_SERVER_PASSWORD_ENV]: attemptKickoff.apiPassword,
|
|
186
|
+
[BORG_OPENCODE_LAUNCH_CORRELATION_ENV]: attemptKickoff.correlationIdentity,
|
|
187
|
+
};
|
|
188
|
+
const child = (options.spawnProcess ?? spawn)('opencode', plan.launchArgs, {
|
|
189
|
+
stdio: 'inherit',
|
|
190
|
+
shell: false,
|
|
191
|
+
env: launchEnv,
|
|
192
|
+
});
|
|
193
|
+
type Stopped =
|
|
194
|
+
| { kind: 'error'; error: NodeJS.ErrnoException }
|
|
195
|
+
| { kind: 'exit'; code: number | null; signal: NodeJS.Signals | null };
|
|
196
|
+
let markStopped: (outcome: Stopped) => void;
|
|
197
|
+
const stopped = new Promise<Stopped>((resolve) => { markStopped = resolve; });
|
|
198
|
+
const onError = (error: NodeJS.ErrnoException) => markStopped({ kind: 'error', error });
|
|
199
|
+
const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
200
|
+
markStopped({ kind: 'exit', code, signal });
|
|
201
|
+
};
|
|
202
|
+
child.once('error', onError);
|
|
203
|
+
child.once('exit', onExit);
|
|
204
|
+
await (options.connect ?? connectOpenCodeDrone)({
|
|
205
|
+
serverUrl: plan.serverUrl,
|
|
206
|
+
apiPassword: attemptKickoff.apiPassword,
|
|
207
|
+
directory: options.cwd,
|
|
208
|
+
droneLabel: options.droneLabel,
|
|
209
|
+
cubeName: options.cubeName,
|
|
210
|
+
launchIdentity: attemptKickoff.correlationIdentity,
|
|
211
|
+
});
|
|
212
|
+
const outcome = await Promise.race([
|
|
213
|
+
(options.injectKickoff ?? injectInitialKickoff)(attemptKickoff).then(
|
|
214
|
+
(bound) => bound ? { kind: 'ready' as const } : { kind: 'unbound' as const },
|
|
215
|
+
),
|
|
216
|
+
stopped,
|
|
217
|
+
]);
|
|
218
|
+
child.off('error', onError);
|
|
219
|
+
child.off('exit', onExit);
|
|
220
|
+
if (outcome.kind === 'ready') {
|
|
221
|
+
return { launchArgs: plan.launchArgs, launchEnv, process: child };
|
|
222
|
+
}
|
|
223
|
+
if (outcome.kind === 'unbound') {
|
|
224
|
+
child.kill();
|
|
225
|
+
throw new Error('OpenCode started but its launch session could not be identified');
|
|
226
|
+
}
|
|
227
|
+
if (outcome.kind === 'error') throw outcome.error;
|
|
228
|
+
lastExit = outcome;
|
|
229
|
+
if (attempt < 2) port = await (options.allocatePort ?? allocateOpenCodePort)();
|
|
230
|
+
}
|
|
231
|
+
throw new Error(
|
|
232
|
+
`OpenCode exited before launch readiness after 3 attempts `
|
|
233
|
+
+ `(code=${lastExit?.code ?? 'none'}, signal=${lastExit?.signal ?? 'none'})`,
|
|
234
|
+
);
|
|
192
235
|
}
|
|
193
236
|
|
|
194
237
|
export async function runAssimilateEntry(
|
|
@@ -510,7 +553,19 @@ async function main() {
|
|
|
510
553
|
cli = configureSelectedLaunchCli(
|
|
511
554
|
cli,
|
|
512
555
|
launchAction,
|
|
513
|
-
(selectedCli) =>
|
|
556
|
+
(selectedCli) => {
|
|
557
|
+
ensureResolvedCliConfigured(selectedCli, active);
|
|
558
|
+
if (selectedCli === 'opencode' && active) {
|
|
559
|
+
const worktree = findProjectRoot(process.cwd());
|
|
560
|
+
provisionLaunchAccess(selectedCli, worktree, {
|
|
561
|
+
worktree,
|
|
562
|
+
scratch: scratchRootForSeat(homedir(), active.droneLabel, active.droneId),
|
|
563
|
+
// OpenCode does not grant Git internals; this field is used only by
|
|
564
|
+
// the Codex launch-argument path.
|
|
565
|
+
commonDir: worktree,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
},
|
|
514
569
|
);
|
|
515
570
|
|
|
516
571
|
if (active && !parsedCli.force) {
|
|
@@ -682,7 +737,7 @@ async function main() {
|
|
|
682
737
|
console.error(`${consolePrefix()}${chalk.blue(`◼ Launching ${cliDisplayName}…`)}`);
|
|
683
738
|
|
|
684
739
|
const agentProcess = cli === 'opencode' && openCodeKickoff && openCodePort !== undefined
|
|
685
|
-
? launchOpenCodeProcess({
|
|
740
|
+
? (await launchOpenCodeProcess({
|
|
686
741
|
cwd: process.cwd(),
|
|
687
742
|
port: openCodePort,
|
|
688
743
|
prompt: openCodeKickoff.prompt,
|
|
@@ -691,7 +746,7 @@ async function main() {
|
|
|
691
746
|
droneLabel: active?.droneLabel ?? 'opencode',
|
|
692
747
|
cubeName: active?.name ?? 'borg',
|
|
693
748
|
kickoff: openCodeKickoff,
|
|
694
|
-
}).process
|
|
749
|
+
})).process
|
|
695
750
|
: spawn(cli, launchArgs, { stdio: 'inherit', shell: false, env: launchEnv });
|
|
696
751
|
|
|
697
752
|
// gh#857 WI-2: wake-target recording is codex-only (app-server bridge).
|
|
@@ -807,6 +862,11 @@ if (isEntryInvocation()) {
|
|
|
807
862
|
process.stderr.write(`${error.message}\n`);
|
|
808
863
|
process.exit(1);
|
|
809
864
|
}
|
|
865
|
+
if (error?.code === 'ENOENT' && error?.path === 'opencode') {
|
|
866
|
+
console.error(`${consolePrefix()}${chalk.red('\n◼ Failed to launch opencode')}`);
|
|
867
|
+
console.error(`${consolePrefix()}${chalk.gray('Make sure opencode is installed.\n')}`);
|
|
868
|
+
process.exit(1);
|
|
869
|
+
}
|
|
810
870
|
console.error(`${consolePrefix()}${chalk.red(`\n◼ Error: ${error.message}\n`)}`);
|
|
811
871
|
process.exit(1);
|
|
812
872
|
});
|
package/src/cli-help.ts
CHANGED
|
@@ -172,6 +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 upgrade Alias for borg update\n` +
|
|
175
176
|
` borg doctor Check agent hook commands, OpenCode integration, and startup diagnostics\n` +
|
|
176
177
|
` borg clone <url> [dir] Clone a repository, then create and launch its cube\n` +
|
|
177
178
|
` borg quickstart Create a cube and a drone for every role, then launch them\n` +
|
package/src/codex-launch.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BorgCli } from './cubes.js';
|
|
2
|
-
import {
|
|
2
|
+
import type { AgentKind } from './agent-runtime.js';
|
|
3
|
+
import { wakePathArming } from './regen-format.js';
|
|
3
4
|
import { OPENCODE_WAKE_PATH_GUIDANCE } from './opencode-wake-copy.js';
|
|
4
5
|
|
|
5
6
|
/**
|
package/src/config-utils.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
|
+
import { randomBytes } from 'node:crypto';
|
|
8
9
|
import fs from 'fs';
|
|
9
10
|
import path from 'path';
|
|
10
11
|
import { fileURLToPath } from 'url';
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
import { shellEscape } from './shell-escape.js';
|
|
24
25
|
import { BORG_STATE_ROOT_ENV, borgAgentConfigEnv, borgHomeRoot } from './private-root.js';
|
|
25
26
|
import type { LaunchAccessPaths } from './launch-access.js';
|
|
26
|
-
import { BORG_LAUNCH_EXPECTED_SEAT_ENV } from './cubes.js';
|
|
27
|
+
import { BORG_LAUNCH_EXPECTED_SEAT_ENV, type BorgCli } from './cubes.js';
|
|
27
28
|
import {
|
|
28
29
|
OPENCODE_SERVER_PASSWORD_ENV,
|
|
29
30
|
OPENCODE_SERVER_PASSWORD_REFERENCE,
|
|
@@ -209,6 +210,132 @@ function writeJsonFile(p: string, data: any): void {
|
|
|
209
210
|
fs.writeFileSync(p, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
210
211
|
}
|
|
211
212
|
|
|
213
|
+
interface OpenCodeProjectConfigSnapshot {
|
|
214
|
+
config: unknown;
|
|
215
|
+
identity: { dev: number; ino: number } | null;
|
|
216
|
+
mode: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function unsafeOpenCodeConfigPath(configPath: string, detail: string): Error {
|
|
220
|
+
return new Error(`OpenCode config path ${configPath} is unsafe: ${detail}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function assertOpenCodeProjectConfigDirectory(projectRoot: string, configPath: string): string {
|
|
224
|
+
const root = path.resolve(projectRoot);
|
|
225
|
+
let rootStat: fs.Stats;
|
|
226
|
+
try {
|
|
227
|
+
rootStat = fs.lstatSync(root);
|
|
228
|
+
} catch {
|
|
229
|
+
throw unsafeOpenCodeConfigPath(configPath, 'project root is missing or unreadable');
|
|
230
|
+
}
|
|
231
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory() || fs.realpathSync(root) !== root) {
|
|
232
|
+
throw unsafeOpenCodeConfigPath(configPath, 'project root must be a canonical real directory');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const configDir = path.join(root, '.opencode');
|
|
236
|
+
try {
|
|
237
|
+
fs.mkdirSync(configDir, { mode: 0o700 });
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
240
|
+
}
|
|
241
|
+
let directoryStat: fs.Stats;
|
|
242
|
+
try {
|
|
243
|
+
directoryStat = fs.lstatSync(configDir);
|
|
244
|
+
} catch {
|
|
245
|
+
throw unsafeOpenCodeConfigPath(configPath, '.opencode is missing or unreadable');
|
|
246
|
+
}
|
|
247
|
+
if (
|
|
248
|
+
directoryStat.isSymbolicLink()
|
|
249
|
+
|| !directoryStat.isDirectory()
|
|
250
|
+
|| fs.realpathSync(configDir) !== configDir
|
|
251
|
+
) {
|
|
252
|
+
throw unsafeOpenCodeConfigPath(configPath, '.opencode must be a canonical real directory, not a symlink');
|
|
253
|
+
}
|
|
254
|
+
return configDir;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function inspectOpenCodeProjectConfigFile(configPath: string): fs.Stats | null {
|
|
258
|
+
let metadata: fs.Stats;
|
|
259
|
+
try {
|
|
260
|
+
metadata = fs.lstatSync(configPath);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
if (metadata.isSymbolicLink() || !metadata.isFile() || metadata.nlink !== 1) {
|
|
266
|
+
throw unsafeOpenCodeConfigPath(configPath, 'final path must be one regular file, not a symlink');
|
|
267
|
+
}
|
|
268
|
+
return metadata;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function readOpenCodeProjectConfig(projectRoot: string, configPath: string): OpenCodeProjectConfigSnapshot {
|
|
272
|
+
assertOpenCodeProjectConfigDirectory(projectRoot, configPath);
|
|
273
|
+
const metadata = inspectOpenCodeProjectConfigFile(configPath);
|
|
274
|
+
if (!metadata) return { config: {}, identity: null, mode: 0o600 };
|
|
275
|
+
|
|
276
|
+
let descriptor: number | null = null;
|
|
277
|
+
try {
|
|
278
|
+
descriptor = fs.openSync(configPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
279
|
+
const opened = fs.fstatSync(descriptor);
|
|
280
|
+
if (!opened.isFile() || opened.nlink !== 1 || opened.dev !== metadata.dev || opened.ino !== metadata.ino) {
|
|
281
|
+
throw unsafeOpenCodeConfigPath(configPath, 'final file changed while opening');
|
|
282
|
+
}
|
|
283
|
+
const text = fs.readFileSync(descriptor, 'utf8');
|
|
284
|
+
return {
|
|
285
|
+
config: text.trim() ? JSON.parse(text) : {},
|
|
286
|
+
identity: { dev: opened.dev, ino: opened.ino },
|
|
287
|
+
mode: opened.mode & 0o777,
|
|
288
|
+
};
|
|
289
|
+
} finally {
|
|
290
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function writeOpenCodeProjectConfig(
|
|
295
|
+
projectRoot: string,
|
|
296
|
+
configPath: string,
|
|
297
|
+
data: unknown,
|
|
298
|
+
snapshot: OpenCodeProjectConfigSnapshot,
|
|
299
|
+
): void {
|
|
300
|
+
const configDir = assertOpenCodeProjectConfigDirectory(projectRoot, configPath);
|
|
301
|
+
const temporary = path.join(
|
|
302
|
+
configDir,
|
|
303
|
+
`.opencode.json.${process.pid}.${randomBytes(12).toString('hex')}.tmp`,
|
|
304
|
+
);
|
|
305
|
+
let descriptor: number | null = null;
|
|
306
|
+
try {
|
|
307
|
+
descriptor = fs.openSync(
|
|
308
|
+
temporary,
|
|
309
|
+
fs.constants.O_WRONLY
|
|
310
|
+
| fs.constants.O_CREAT
|
|
311
|
+
| fs.constants.O_EXCL
|
|
312
|
+
| (fs.constants.O_NOFOLLOW ?? 0),
|
|
313
|
+
snapshot.mode,
|
|
314
|
+
);
|
|
315
|
+
fs.writeFileSync(descriptor, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
316
|
+
fs.fsyncSync(descriptor);
|
|
317
|
+
fs.closeSync(descriptor);
|
|
318
|
+
descriptor = null;
|
|
319
|
+
|
|
320
|
+
assertOpenCodeProjectConfigDirectory(projectRoot, configPath);
|
|
321
|
+
const current = inspectOpenCodeProjectConfigFile(configPath);
|
|
322
|
+
const unchanged = snapshot.identity === null
|
|
323
|
+
? current === null
|
|
324
|
+
: current !== null
|
|
325
|
+
&& current.dev === snapshot.identity.dev
|
|
326
|
+
&& current.ino === snapshot.identity.ino;
|
|
327
|
+
if (!unchanged) {
|
|
328
|
+
throw unsafeOpenCodeConfigPath(configPath, 'final file changed before replacement');
|
|
329
|
+
}
|
|
330
|
+
fs.renameSync(temporary, configPath);
|
|
331
|
+
} finally {
|
|
332
|
+
if (descriptor !== null) {
|
|
333
|
+
try { fs.closeSync(descriptor); } catch { /* Preserve the primary failure. */ }
|
|
334
|
+
}
|
|
335
|
+
try { fs.unlinkSync(temporary); } catch { /* Renamed or never created. */ }
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
212
339
|
/**
|
|
213
340
|
* Register a Claude Code SessionStart hook that runs `borg-regen` at the
|
|
214
341
|
* start of every session. Idempotent: re-running won't add duplicates.
|
|
@@ -1339,11 +1466,13 @@ export function addOpenCodeLaunchAccess(
|
|
|
1339
1466
|
paths: LaunchAccessPaths,
|
|
1340
1467
|
): boolean {
|
|
1341
1468
|
const configPath = path.join(projectRoot, '.opencode', 'opencode.json');
|
|
1469
|
+
let snapshot: OpenCodeProjectConfigSnapshot;
|
|
1342
1470
|
let config: any;
|
|
1343
1471
|
try {
|
|
1344
|
-
|
|
1472
|
+
snapshot = readOpenCodeProjectConfig(projectRoot, configPath);
|
|
1473
|
+
config = snapshot.config;
|
|
1345
1474
|
} catch (err: any) {
|
|
1346
|
-
throw new Error(`Could not
|
|
1475
|
+
throw new Error(`Could not safely read OpenCode config ${configPath}: ${err.message}`);
|
|
1347
1476
|
}
|
|
1348
1477
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
1349
1478
|
throw new Error(`OpenCode config ${configPath} is not an object`);
|
|
@@ -1394,10 +1523,33 @@ export function addOpenCodeLaunchAccess(
|
|
|
1394
1523
|
config.permission = permissionObject;
|
|
1395
1524
|
|
|
1396
1525
|
const changed = JSON.stringify(config) !== before;
|
|
1397
|
-
if (changed)
|
|
1526
|
+
if (changed) {
|
|
1527
|
+
try {
|
|
1528
|
+
writeOpenCodeProjectConfig(projectRoot, configPath, config, snapshot);
|
|
1529
|
+
} catch (err: any) {
|
|
1530
|
+
throw new Error(`Could not safely update OpenCode config ${configPath}: ${err.message}`);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1398
1533
|
return changed;
|
|
1399
1534
|
}
|
|
1400
1535
|
|
|
1536
|
+
/** Provision the project-local path access required by the selected CLI. */
|
|
1537
|
+
export function provisionLaunchAccess(
|
|
1538
|
+
cli: BorgCli,
|
|
1539
|
+
projectRoot: string,
|
|
1540
|
+
paths: LaunchAccessPaths,
|
|
1541
|
+
): void {
|
|
1542
|
+
if (cli === 'claude') {
|
|
1543
|
+
addClaudeLaunchAccess(projectRoot, paths);
|
|
1544
|
+
} else if (cli === 'codex') {
|
|
1545
|
+
// Codex receives path grants on its launch command. Its global hook reads
|
|
1546
|
+
// the scoped launch environment and only supplies the reminder.
|
|
1547
|
+
addCodexForeignPathReminderHook();
|
|
1548
|
+
} else {
|
|
1549
|
+
addOpenCodeLaunchAccess(projectRoot, paths);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1401
1553
|
/**
|
|
1402
1554
|
* Add borg MCP server to OpenCode using `opencode mcp add` CLI.
|
|
1403
1555
|
* Pins activation and agent-kind signals plus OpenCode config substitutions
|
package/src/config.ts
CHANGED
|
@@ -31,7 +31,8 @@ const SERVER_CREDENTIAL_RECORD_VERSION = 2 as const;
|
|
|
31
31
|
const SERVER_PENDING_ENROLLMENT_RECORD_VERSION = 3 as const;
|
|
32
32
|
const LEGACY_SERVER_PENDING_ENROLLMENT_RECORD_VERSION = 1 as const;
|
|
33
33
|
const SERVER_ACCEPTED_ENROLLMENT_MARKER_VERSION = 1 as const;
|
|
34
|
-
const SERVER_CUBE_RETRY_RECORD_VERSION =
|
|
34
|
+
const SERVER_CUBE_RETRY_RECORD_VERSION = 3 as const;
|
|
35
|
+
const LEGACY_SERVER_CUBE_RETRY_RECORD_VERSION = 2 as const;
|
|
35
36
|
// The 0600 credential store (Queen rescope: replaces the OS keychain). A single
|
|
36
37
|
// file holds every parent credential/enrollment record; a single flock
|
|
37
38
|
// serializes every mutator + observer that must (SR-seven #4).
|
|
@@ -326,7 +327,7 @@ async function writeServerCredentialRecord(
|
|
|
326
327
|
}
|
|
327
328
|
if (backend.entries) {
|
|
328
329
|
const accounts = await backend.entries();
|
|
329
|
-
const conflicts = Object.entries(accounts).filter(([
|
|
330
|
+
const conflicts = Object.entries(accounts).filter(([, value]) => {
|
|
330
331
|
try {
|
|
331
332
|
const parsed = JSON.parse(value) as { version?: unknown; origin?: unknown };
|
|
332
333
|
return parsed.version === SERVER_CREDENTIAL_RECORD_VERSION && parsed.origin === record.origin;
|
|
@@ -1106,39 +1107,74 @@ export async function getOrCreatePendingServerCubeCreation(
|
|
|
1106
1107
|
return withCredentialStoreLock(async () => {
|
|
1107
1108
|
const stored = await backend.get(account);
|
|
1108
1109
|
if (stored) {
|
|
1110
|
+
let record: Omit<Partial<PendingServerCubeCreationRecord>, 'template'> & {
|
|
1111
|
+
version?: unknown;
|
|
1112
|
+
state?: unknown;
|
|
1113
|
+
template?: unknown;
|
|
1114
|
+
};
|
|
1109
1115
|
try {
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1116
|
+
const parsed: unknown = JSON.parse(stored);
|
|
1117
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('invalid record');
|
|
1118
|
+
record = parsed as typeof record;
|
|
1119
|
+
} catch {
|
|
1120
|
+
throw new Error('pending Borg server cube creation does not match this repository');
|
|
1121
|
+
}
|
|
1122
|
+
const retryKey = typeof record.retryKey === 'string' && UUID_RE.test(record.retryKey)
|
|
1123
|
+
? record.retryKey
|
|
1124
|
+
: null;
|
|
1125
|
+
const sameOperation =
|
|
1126
|
+
record.state === 'pending' &&
|
|
1127
|
+
record.origin === input.origin &&
|
|
1128
|
+
record.trustIdentity === input.trustIdentity &&
|
|
1129
|
+
record.clientId === input.clientId &&
|
|
1130
|
+
record.repositoryBinding === repositoryBinding &&
|
|
1131
|
+
record.name === input.name &&
|
|
1132
|
+
record.workingRepoName === input.workingRepoName &&
|
|
1133
|
+
retryKey !== null;
|
|
1134
|
+
if (sameOperation && record.version === SERVER_CUBE_RETRY_RECORD_VERSION && record.template === input.template) {
|
|
1135
|
+
return {
|
|
1136
|
+
origin: input.origin,
|
|
1137
|
+
trustIdentity: input.trustIdentity,
|
|
1138
|
+
clientId: input.clientId,
|
|
1139
|
+
repositoryBinding,
|
|
1140
|
+
retryKey,
|
|
1141
|
+
name: input.name,
|
|
1142
|
+
workingRepoName: input.workingRepoName,
|
|
1143
|
+
repository: input.repository,
|
|
1144
|
+
template: input.template,
|
|
1113
1145
|
};
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
record.clientId !== input.clientId ||
|
|
1120
|
-
record.repositoryBinding !== repositoryBinding ||
|
|
1121
|
-
record.name !== input.name ||
|
|
1122
|
-
record.workingRepoName !== input.workingRepoName ||
|
|
1123
|
-
record.template !== input.template ||
|
|
1124
|
-
typeof record.retryKey !== 'string' || !UUID_RE.test(record.retryKey)
|
|
1125
|
-
) {
|
|
1126
|
-
throw new Error('mismatch');
|
|
1146
|
+
}
|
|
1147
|
+
if (sameOperation && record.version === LEGACY_SERVER_CUBE_RETRY_RECORD_VERSION) {
|
|
1148
|
+
const preserveRetryKey = record.template === input.template;
|
|
1149
|
+
if (!preserveRetryKey && record.template !== 'default') {
|
|
1150
|
+
throw new Error('pending Borg server cube creation does not match this repository');
|
|
1127
1151
|
}
|
|
1128
|
-
|
|
1152
|
+
const replacement: PendingServerCubeCreationRecord = {
|
|
1129
1153
|
origin: input.origin,
|
|
1130
1154
|
trustIdentity: input.trustIdentity,
|
|
1131
1155
|
clientId: input.clientId,
|
|
1132
1156
|
repositoryBinding,
|
|
1133
|
-
retryKey:
|
|
1157
|
+
retryKey: preserveRetryKey ? retryKey : randomUUID(),
|
|
1134
1158
|
name: input.name,
|
|
1135
1159
|
workingRepoName: input.workingRepoName,
|
|
1136
1160
|
repository: input.repository,
|
|
1137
1161
|
template: input.template,
|
|
1138
1162
|
};
|
|
1139
|
-
|
|
1140
|
-
|
|
1163
|
+
await backend.set(account, JSON.stringify({
|
|
1164
|
+
version: SERVER_CUBE_RETRY_RECORD_VERSION,
|
|
1165
|
+
state: 'pending',
|
|
1166
|
+
origin: replacement.origin,
|
|
1167
|
+
trustIdentity: replacement.trustIdentity,
|
|
1168
|
+
clientId: replacement.clientId,
|
|
1169
|
+
repositoryBinding: replacement.repositoryBinding,
|
|
1170
|
+
retryKey: replacement.retryKey,
|
|
1171
|
+
name: replacement.name,
|
|
1172
|
+
workingRepoName: replacement.workingRepoName,
|
|
1173
|
+
template: replacement.template,
|
|
1174
|
+
}));
|
|
1175
|
+
return replacement;
|
|
1141
1176
|
}
|
|
1177
|
+
throw new Error('pending Borg server cube creation does not match this repository');
|
|
1142
1178
|
}
|
|
1143
1179
|
const record: PendingServerCubeCreationRecord = {
|
|
1144
1180
|
origin: input.origin,
|
package/src/cubes.ts
CHANGED
|
@@ -498,16 +498,6 @@ export async function findRemainingActiveSeatForWorktree(worktree: string): Prom
|
|
|
498
498
|
return record ? { apiUrl: record.origin, operation: record.operation } : null;
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
-
/**
|
|
502
|
-
* Legacy binding-only writer. In the collapsed single-store model an ACTIVE seat is
|
|
503
|
-
* created ONLY by the atomic mint→activate+bind path in seats.ts (driven by the
|
|
504
|
-
* attach FINALIZE); there is no standalone binding write. Retained solely as a
|
|
505
|
-
* fail-closed guard for incomplete session metadata.
|
|
506
|
-
*/
|
|
507
|
-
export async function setActiveCube(_active: ActiveCubeInput): Promise<void> {
|
|
508
|
-
throw new Error('local Borg server session metadata is incomplete');
|
|
509
|
-
}
|
|
510
|
-
|
|
511
501
|
export function activeCubeWithFreshRegenIdentity(
|
|
512
502
|
active: ActiveCube,
|
|
513
503
|
result: {
|
package/src/gc-orphan-inboxes.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* always err toward KEEPING.
|
|
15
15
|
*/
|
|
16
16
|
import { spawnSync } from 'node:child_process';
|
|
17
|
-
import { readFileSync, readdirSync, statSync
|
|
17
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import {
|
|
20
20
|
pidfilePathFor,
|