borgmcp 1.0.6 → 1.0.7
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.js +39 -511
- package/dist/assimilate-deps.js +3 -177
- package/dist/assimilate-welcome.js +2 -24
- package/dist/auth-env.js +1 -107
- package/dist/auth.js +23 -612
- package/dist/claude.js +11 -281
- package/dist/cli-help.js +29 -50
- package/dist/cli-platform.js +4 -94
- package/dist/codex-app-server.js +4 -228
- package/dist/codex-app-wake.js +2 -122
- package/dist/codex-launch.js +1 -81
- package/dist/codex-remote.js +1 -250
- package/dist/config-utils.js +3 -385
- package/dist/config.js +1 -190
- package/dist/console-prefix.js +1 -86
- package/dist/cube-name.js +1 -65
- package/dist/cubes.js +4 -269
- package/dist/debug.js +1 -71
- package/dist/device-auth.js +1 -167
- package/dist/direct-log.js +1 -11
- package/dist/health-beat.js +1 -168
- package/dist/inbox-monitor.js +1 -129
- package/dist/index.js +26 -1378
- package/dist/lifecycle-log-guard.js +2 -93
- package/dist/list-roles-render.js +6 -39
- package/dist/log-audit.js +3 -186
- package/dist/log-stream.js +9 -848
- package/dist/name-validator.js +1 -22
- package/dist/parse-assimilate-args.js +1 -82
- package/dist/postinstall.js +8 -22
- package/dist/regen-format.js +11 -337
- package/dist/regen.js +5 -83
- package/dist/remote-client.js +1 -695
- package/dist/role-resolver.js +1 -36
- package/dist/role-section.js +8 -208
- package/dist/roster-render.js +3 -96
- package/dist/setup.js +36 -251
- package/dist/shell-escape.js +1 -22
- package/dist/spawn.js +10 -29
- package/dist/stale-version-check.js +1 -102
- package/dist/stream-owner.js +2 -202
- package/dist/stream-status.js +3 -211
- package/dist/subscription-retry.js +1 -23
- package/dist/sync-roles-render.js +3 -118
- package/dist/sync.js +22 -286
- package/dist/templates.js +120 -626
- package/dist/terminal-title.js +1 -68
- package/dist/token-crypto.js +1 -91
- package/dist/token-store.js +1 -222
- package/dist/types.js +0 -5
- package/dist/version.js +2 -78
- package/dist/worktree-lifecycle.js +2 -173
- package/package.json +11 -2
- package/dist/assimilate-cmd.d.ts.map +0 -1
- package/dist/assimilate-cmd.js.map +0 -1
- package/dist/assimilate-deps.d.ts.map +0 -1
- package/dist/assimilate-deps.js.map +0 -1
- package/dist/assimilate-welcome.d.ts.map +0 -1
- package/dist/assimilate-welcome.js.map +0 -1
- package/dist/auth-env.d.ts.map +0 -1
- package/dist/auth-env.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/claude.d.ts.map +0 -1
- package/dist/claude.js.map +0 -1
- package/dist/cli-help.d.ts.map +0 -1
- package/dist/cli-help.js.map +0 -1
- package/dist/cli-platform.d.ts.map +0 -1
- package/dist/cli-platform.js.map +0 -1
- package/dist/codex-app-server.d.ts.map +0 -1
- package/dist/codex-app-server.js.map +0 -1
- package/dist/codex-app-wake.d.ts.map +0 -1
- package/dist/codex-app-wake.js.map +0 -1
- package/dist/codex-launch.d.ts.map +0 -1
- package/dist/codex-launch.js.map +0 -1
- package/dist/codex-remote.d.ts.map +0 -1
- package/dist/codex-remote.js.map +0 -1
- package/dist/config-utils.d.ts.map +0 -1
- package/dist/config-utils.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/console-prefix.d.ts.map +0 -1
- package/dist/console-prefix.js.map +0 -1
- package/dist/cube-name.d.ts.map +0 -1
- package/dist/cube-name.js.map +0 -1
- package/dist/cubes.d.ts.map +0 -1
- package/dist/cubes.js.map +0 -1
- package/dist/debug.d.ts.map +0 -1
- package/dist/debug.js.map +0 -1
- package/dist/device-auth.d.ts.map +0 -1
- package/dist/device-auth.js.map +0 -1
- package/dist/direct-log.d.ts.map +0 -1
- package/dist/direct-log.js.map +0 -1
- package/dist/health-beat.d.ts.map +0 -1
- package/dist/health-beat.js.map +0 -1
- package/dist/inbox-monitor.d.ts.map +0 -1
- package/dist/inbox-monitor.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lifecycle-log-guard.d.ts.map +0 -1
- package/dist/lifecycle-log-guard.js.map +0 -1
- package/dist/list-roles-render.d.ts.map +0 -1
- package/dist/list-roles-render.js.map +0 -1
- package/dist/log-audit.d.ts.map +0 -1
- package/dist/log-audit.js.map +0 -1
- package/dist/log-stream.d.ts.map +0 -1
- package/dist/log-stream.js.map +0 -1
- package/dist/name-validator.d.ts.map +0 -1
- package/dist/name-validator.js.map +0 -1
- package/dist/parse-assimilate-args.d.ts.map +0 -1
- package/dist/parse-assimilate-args.js.map +0 -1
- package/dist/postinstall.d.ts.map +0 -1
- package/dist/postinstall.js.map +0 -1
- package/dist/regen-format.d.ts.map +0 -1
- package/dist/regen-format.js.map +0 -1
- package/dist/regen.d.ts.map +0 -1
- package/dist/regen.js.map +0 -1
- package/dist/remote-client.d.ts.map +0 -1
- package/dist/remote-client.js.map +0 -1
- package/dist/role-resolver.d.ts.map +0 -1
- package/dist/role-resolver.js.map +0 -1
- package/dist/role-section.d.ts.map +0 -1
- package/dist/role-section.js.map +0 -1
- package/dist/roster-render.d.ts.map +0 -1
- package/dist/roster-render.js.map +0 -1
- package/dist/setup.d.ts.map +0 -1
- package/dist/setup.js.map +0 -1
- package/dist/shell-escape.d.ts.map +0 -1
- package/dist/shell-escape.js.map +0 -1
- package/dist/spawn.d.ts.map +0 -1
- package/dist/spawn.js.map +0 -1
- package/dist/stale-version-check.d.ts.map +0 -1
- package/dist/stale-version-check.js.map +0 -1
- package/dist/stream-owner.d.ts.map +0 -1
- package/dist/stream-owner.js.map +0 -1
- package/dist/stream-status.d.ts.map +0 -1
- package/dist/stream-status.js.map +0 -1
- package/dist/subscription-retry.d.ts.map +0 -1
- package/dist/subscription-retry.js.map +0 -1
- package/dist/sync-roles-render.d.ts.map +0 -1
- package/dist/sync-roles-render.js.map +0 -1
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js.map +0 -1
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js.map +0 -1
- package/dist/terminal-title.d.ts.map +0 -1
- package/dist/terminal-title.js.map +0 -1
- package/dist/token-crypto.d.ts.map +0 -1
- package/dist/token-crypto.js.map +0 -1
- package/dist/token-store.d.ts.map +0 -1
- package/dist/token-store.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
- package/dist/worktree-lifecycle.d.ts.map +0 -1
- package/dist/worktree-lifecycle.js.map +0 -1
package/dist/codex-remote.js
CHANGED
|
@@ -1,250 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { randomBytes } from 'node:crypto';
|
|
5
|
-
import { spawn } from 'node:child_process';
|
|
6
|
-
import { CodexAppServerClient } from './codex-app-server.js';
|
|
7
|
-
export const DEFAULT_CODEX_REMOTE_DIR = join(homedir(), '.config', 'borgmcp', 'codex-remote');
|
|
8
|
-
export function withCodexCwdArg(args, cwd) {
|
|
9
|
-
if (hasCodexCwdArg(args))
|
|
10
|
-
return args;
|
|
11
|
-
return ['--cd', cwd, ...args];
|
|
12
|
-
}
|
|
13
|
-
function hasCodexCwdArg(args) {
|
|
14
|
-
return args.some((arg) => arg === '--cd' || arg.startsWith('--cd=') || arg === '-C');
|
|
15
|
-
}
|
|
16
|
-
export function defaultIsAlive(pid) {
|
|
17
|
-
try {
|
|
18
|
-
process.kill(pid, 0);
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
|
-
catch (err) {
|
|
22
|
-
// EPERM ⇒ the process exists but we can't signal it (still alive).
|
|
23
|
-
return err?.code === 'EPERM';
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* gh#633: process-liveness probe for the borg-owned codex app-server — the
|
|
28
|
-
* transport-agnostic analogue of the claude tail-F Monitor's pgrep check
|
|
29
|
-
* (checkInboxMonitorHealthy / stream-status.ts:48). Codex drones wake via this
|
|
30
|
-
* app-server bridge, NOT a tail-F Monitor, so wake_path_client_monitor_armed is
|
|
31
|
-
* false-by-design for them; the HOP-2 wake-path-deaf classifier mis-read that
|
|
32
|
-
* as deaf (gh#633). This gives HOP-2 the codex wake path's ACTUAL health.
|
|
33
|
-
*
|
|
34
|
-
* Uses the app-server PIDFILE (written at spawn, beside the socket) +
|
|
35
|
-
* process.kill(pid, 0) — NOT pgrep. The codex TUI is also launched
|
|
36
|
-
* `codex --remote unix://<socketPath>`, so a `pgrep -f <socketPath>` would ALSO
|
|
37
|
-
* match the live TUI and FALSE-ARM when the app-server has crashed but the TUI
|
|
38
|
-
* still runs (the deaf-but-alive case). The pidfile holds the EXACT app-server
|
|
39
|
-
* pid, so kill(0) reflects the app-server's liveness specifically — and it's
|
|
40
|
-
* cheaper than pgrep (no subprocess). Mirrors pruneStaleSockets' pid check.
|
|
41
|
-
*
|
|
42
|
-
* Tri-state (mirrors checkInboxMonitorHealthy's boolean|null contract):
|
|
43
|
-
* - true: pidfile resolves to a LIVE pid → app-server (bridge) is up → armed.
|
|
44
|
-
* - false: pidfile resolves to a DEAD pid → an unclean exit (crash/kill -9)
|
|
45
|
-
* left a stale pidfile (cleanup never ran) → bridge down → HOP-2
|
|
46
|
-
* correctly flags a genuinely-deaf codex drone (no SLI-lie).
|
|
47
|
-
* - null: pidfile missing / unreadable / unparseable → cannot determine →
|
|
48
|
-
* caller maps null→armed (false-deaf-avoidance, same as the claude
|
|
49
|
-
* monitor branch). A CLEAN app-server exit removes the pidfile, but
|
|
50
|
-
* then the drone is shutting down → the silent-stall watchdog
|
|
51
|
-
* backstops it via a separate layer.
|
|
52
|
-
*
|
|
53
|
-
* Residual (negligible, gh#633 / Coordinator 6f28fe3f): PID reuse — if the
|
|
54
|
-
* crashed app-server's pid is recycled by an unrelated process before the next
|
|
55
|
-
* launch's pruneStaleSockets removes the stale pidfile, kill(0) reports alive →
|
|
56
|
-
* a brief false-arm. The window is tiny (exact-pid reuse during the crash gap)
|
|
57
|
-
* and self-heals on the next launch's prune; far smaller than existsSync's
|
|
58
|
-
* always-on stale-file masking.
|
|
59
|
-
*/
|
|
60
|
-
export function checkCodexBridgeHealthy(socketPath, deps = {}) {
|
|
61
|
-
if (!socketPath)
|
|
62
|
-
return null;
|
|
63
|
-
const isAlive = deps.isAlive ?? defaultIsAlive;
|
|
64
|
-
const readPidFile = deps.readPidFile ?? ((pidPath) => readFileSync(pidPath, 'utf-8'));
|
|
65
|
-
const pidPath = socketPath.replace(/\.sock$/, '.pid');
|
|
66
|
-
try {
|
|
67
|
-
const pid = Number.parseInt(readPidFile(pidPath).trim(), 10);
|
|
68
|
-
if (Number.isNaN(pid))
|
|
69
|
-
return null;
|
|
70
|
-
return isAlive(pid);
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
function safeRm(p) {
|
|
77
|
-
try {
|
|
78
|
-
rmSync(p, { force: true });
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
// best-effort
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Remove sockets in the owned dir whose owning app-server pid is no longer
|
|
86
|
-
* alive (crashed prior launches), leaving live concurrent sessions' sockets
|
|
87
|
-
* untouched. Operates ONLY inside the borg-owned runtime dir.
|
|
88
|
-
*/
|
|
89
|
-
function pruneStaleSockets(runtimeDir, isAlive) {
|
|
90
|
-
let entries;
|
|
91
|
-
try {
|
|
92
|
-
entries = readdirSync(runtimeDir);
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
for (const name of entries) {
|
|
98
|
-
if (!name.endsWith('.pid'))
|
|
99
|
-
continue;
|
|
100
|
-
const pidPath = join(runtimeDir, name);
|
|
101
|
-
const sockPath = join(runtimeDir, name.replace(/\.pid$/, '.sock'));
|
|
102
|
-
let pid;
|
|
103
|
-
try {
|
|
104
|
-
pid = Number.parseInt(readFileSync(pidPath, 'utf-8').trim(), 10);
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
safeRm(pidPath);
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if (Number.isNaN(pid) || !isAlive(pid)) {
|
|
111
|
-
safeRm(sockPath);
|
|
112
|
-
safeRm(pidPath);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
function failLoud(reason) {
|
|
117
|
-
return { args: [], env: {}, warning: reason };
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Start a borg-owned per-launch Codex app-server, probe it for readiness, and
|
|
121
|
-
* return the `--remote` launch args + an owned handle (or a fail-loud warning).
|
|
122
|
-
* Async + lifecycle-owning: the caller MUST call `result.server?.cleanup()` on
|
|
123
|
-
* TUI exit.
|
|
124
|
-
*/
|
|
125
|
-
export async function prepareCodexRemoteLaunch(deps) {
|
|
126
|
-
const runtimeDir = deps.runtimeDir ?? DEFAULT_CODEX_REMOTE_DIR;
|
|
127
|
-
const isAlive = deps.isAlive ?? defaultIsAlive;
|
|
128
|
-
const readyTimeoutMs = deps.readyTimeoutMs ?? 8000;
|
|
129
|
-
const pollIntervalMs = deps.pollIntervalMs ?? 250;
|
|
130
|
-
// 1. 0700 owned dir + prune crashed prior sockets (concurrent-safe via pid liveness).
|
|
131
|
-
try {
|
|
132
|
-
mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
|
|
133
|
-
chmodSync(runtimeDir, 0o700); // enforce 0700 even if it pre-existed with looser perms
|
|
134
|
-
pruneStaleSockets(runtimeDir, isAlive);
|
|
135
|
-
}
|
|
136
|
-
catch (err) {
|
|
137
|
-
return failLoud(`Codex remote-wake disabled: could not prepare ${runtimeDir} (${err?.message ?? err}); run borg:regen manually.`);
|
|
138
|
-
}
|
|
139
|
-
// 2. unique, non-predictable socket path inside the owned dir.
|
|
140
|
-
const id = (deps.socketId ?? (() => randomBytes(16).toString('hex')))();
|
|
141
|
-
const socketPath = join(runtimeDir, `${id}.sock`);
|
|
142
|
-
const pidPath = join(runtimeDir, `${id}.pid`);
|
|
143
|
-
// 3. spawn the long-lived app-server.
|
|
144
|
-
let child;
|
|
145
|
-
try {
|
|
146
|
-
child = deps.spawnAppServer(socketPath);
|
|
147
|
-
}
|
|
148
|
-
catch (err) {
|
|
149
|
-
safeRm(socketPath);
|
|
150
|
-
return failLoud(`Codex remote-wake disabled: could not start \`codex app-server\` (${err?.message ?? err}) — ` +
|
|
151
|
-
`is Codex installed + up to date? This session only wakes on the ~30min /loop fallback; ` +
|
|
152
|
-
`run borg:regen manually.`);
|
|
153
|
-
}
|
|
154
|
-
if (child.pid != null) {
|
|
155
|
-
try {
|
|
156
|
-
writeFileSync(pidPath, String(child.pid));
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
// pidfile is only for stale-prune; launch still proceeds.
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
const cleanup = () => {
|
|
163
|
-
try {
|
|
164
|
-
child.kill();
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
// best-effort
|
|
168
|
-
}
|
|
169
|
-
safeRm(socketPath);
|
|
170
|
-
safeRm(pidPath);
|
|
171
|
-
};
|
|
172
|
-
// 4. readiness via a real protocol round-trip — bounded attempts (no clock dep).
|
|
173
|
-
const attempts = Math.max(1, Math.ceil(readyTimeoutMs / pollIntervalMs));
|
|
174
|
-
let ready = false;
|
|
175
|
-
for (let i = 0; i < attempts && !ready; i++) {
|
|
176
|
-
try {
|
|
177
|
-
ready = await deps.probeReady(socketPath);
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
ready = false;
|
|
181
|
-
}
|
|
182
|
-
if (!ready && i < attempts - 1)
|
|
183
|
-
await deps.sleep(pollIntervalMs);
|
|
184
|
-
}
|
|
185
|
-
if (!ready) {
|
|
186
|
-
cleanup();
|
|
187
|
-
return failLoud(`Codex remote-wake disabled: could not reach a Codex app-server at ${socketPath} within ` +
|
|
188
|
-
`${readyTimeoutMs}ms (is Codex up to date? \`codex app-server --listen\` is required). ` +
|
|
189
|
-
`This session only wakes on the ~30min /loop fallback — run borg:regen manually when you return.`);
|
|
190
|
-
}
|
|
191
|
-
// 5. ready → owned remote launch.
|
|
192
|
-
return {
|
|
193
|
-
args: ['--remote', `unix://${socketPath}`],
|
|
194
|
-
env: { BORG_CODEX_REMOTE_WAKE: '1' },
|
|
195
|
-
server: { pid: child.pid, socketPath, cleanup },
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Production deps for prepareCodexRemoteLaunch — spawn the real `codex
|
|
200
|
-
* app-server` child + probe it with the real CodexAppServerClient. Shared by
|
|
201
|
-
* claude.ts and assimilate-deps.ts so there's ONE wiring.
|
|
202
|
-
*
|
|
203
|
-
* The readiness probe uses Codex app-server RPCs ONLY (connect + thread/loaded/
|
|
204
|
-
* list) — it never calls a borg /api/drone/* endpoint — so it can never advance
|
|
205
|
-
* last_seen/last_regen_at and mask a deaf Codex (the gh#46/gh#406 signal-truth
|
|
206
|
-
* invariant; the app-server socket is the wake-DELIVERY wire, not a liveness
|
|
207
|
-
* signal).
|
|
208
|
-
*/
|
|
209
|
-
export function defaultCodexRemoteDeps() {
|
|
210
|
-
return {
|
|
211
|
-
spawnAppServer: (socketPath) => {
|
|
212
|
-
const child = spawn('codex', ['app-server', '--listen', `unix://${socketPath}`], {
|
|
213
|
-
stdio: 'ignore',
|
|
214
|
-
shell: false,
|
|
215
|
-
});
|
|
216
|
-
return {
|
|
217
|
-
pid: child.pid,
|
|
218
|
-
kill: () => {
|
|
219
|
-
try {
|
|
220
|
-
child.kill();
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
// best-effort
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
};
|
|
227
|
-
},
|
|
228
|
-
probeReady: async (socketPath) => {
|
|
229
|
-
const probe = new CodexAppServerClient(socketPath);
|
|
230
|
-
try {
|
|
231
|
-
await probe.connect();
|
|
232
|
-
await probe.loadedThreadIds();
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
catch {
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
finally {
|
|
239
|
-
try {
|
|
240
|
-
probe.close();
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
// best-effort
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
//# sourceMappingURL=codex-remote.js.map
|
|
1
|
+
import{mkdirSync as w,chmodSync as k,readdirSync as v,rmSync as C,writeFileSync as b,readFileSync as y}from"node:fs";import{homedir as g}from"node:os";import{join as u}from"node:path";import{randomBytes as S}from"node:crypto";import{spawn as A}from"node:child_process";import{CodexAppServerClient as $}from"./codex-app-server.js";const E=u(g(),".config","borgmcp","codex-remote");function F(e,t){return R(e)?e:["--cd",t,...e]}function R(e){return e.some(t=>t==="--cd"||t.startsWith("--cd=")||t==="-C")}function x(e){try{return process.kill(e,0),!0}catch(t){return t?.code==="EPERM"}}function O(e,t={}){if(!e)return null;const c=t.isAlive??x,i=t.readPidFile??(o=>y(o,"utf-8")),a=e.replace(/\.sock$/,".pid");try{const o=Number.parseInt(i(a).trim(),10);return Number.isNaN(o)?null:c(o)}catch{return null}}function s(e){try{C(e,{force:!0})}catch{}}function M(e,t){let c;try{c=v(e)}catch{return}for(const i of c){if(!i.endsWith(".pid"))continue;const a=u(e,i),o=u(e,i.replace(/\.pid$/,".sock"));let r;try{r=Number.parseInt(y(a,"utf-8").trim(),10)}catch{s(a);continue}(Number.isNaN(r)||!t(r))&&(s(o),s(a))}}function p(e){return{args:[],env:{},warning:e}}async function B(e){const t=e.runtimeDir??E,c=e.isAlive??x,i=e.readyTimeoutMs??8e3,a=e.pollIntervalMs??250;try{w(t,{recursive:!0,mode:448}),k(t,448),M(t,c)}catch(n){return p(`Codex remote-wake disabled: could not prepare ${t} (${n?.message??n}); run borg:regen manually.`)}const o=(e.socketId??(()=>S(16).toString("hex")))(),r=u(t,`${o}.sock`),m=u(t,`${o}.pid`);let l;try{l=e.spawnAppServer(r)}catch(n){return s(r),p(`Codex remote-wake disabled: could not start \`codex app-server\` (${n?.message??n}) \u2014 is Codex installed + up to date? This session only wakes on the ~30min /loop fallback; run borg:regen manually.`)}if(l.pid!=null)try{b(m,String(l.pid))}catch{}const f=()=>{try{l.kill()}catch{}s(r),s(m)},h=Math.max(1,Math.ceil(i/a));let d=!1;for(let n=0;n<h&&!d;n++){try{d=await e.probeReady(r)}catch{d=!1}!d&&n<h-1&&await e.sleep(a)}return d?{args:["--remote",`unix://${r}`],env:{BORG_CODEX_REMOTE_WAKE:"1"},server:{pid:l.pid,socketPath:r,cleanup:f}}:(f(),p(`Codex remote-wake disabled: could not reach a Codex app-server at ${r} within ${i}ms (is Codex up to date? \`codex app-server --listen\` is required). This session only wakes on the ~30min /loop fallback \u2014 run borg:regen manually when you return.`))}function L(){return{spawnAppServer:e=>{const t=A("codex",["app-server","--listen",`unix://${e}`],{stdio:"ignore",shell:!1});return{pid:t.pid,kill:()=>{try{t.kill()}catch{}}}},probeReady:async e=>{const t=new $(e);try{return await t.connect(),await t.loadedThreadIds(),!0}catch{return!1}finally{try{t.close()}catch{}}},sleep:e=>new Promise(t=>setTimeout(t,e))}}export{E as DEFAULT_CODEX_REMOTE_DIR,O as checkCodexBridgeHealthy,L as defaultCodexRemoteDeps,x as defaultIsAlive,B as prepareCodexRemoteLaunch,F as withCodexCwdArg};
|
package/dist/config-utils.js
CHANGED
|
@@ -1,385 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Handles adding borg-mcp to Claude Code via the claude CLI
|
|
5
|
-
*/
|
|
6
|
-
import { execSync } from 'child_process';
|
|
7
|
-
import fs from 'fs';
|
|
8
|
-
import os from 'os';
|
|
9
|
-
import path from 'path';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
|
-
import { dirname } from 'path';
|
|
12
|
-
// Get __dirname equivalent in ESM
|
|
13
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
-
const __dirname = dirname(__filename);
|
|
15
|
-
const HOOK_COMMAND = 'borg-regen';
|
|
16
|
-
const AUDIT_HOOK_COMMAND = 'borg-log-audit';
|
|
17
|
-
/**
|
|
18
|
-
* Claude Code CLI config path. The CLI reads `mcpServers.<name>` from
|
|
19
|
-
* this file to discover registered MCP servers; `addMcpServer()` (below)
|
|
20
|
-
* writes to it via the `claude mcp add --scope user borg borg-mcp` shell
|
|
21
|
-
* command. Server name is `borg` (not `borgmcp` — `borg-mcp` is the
|
|
22
|
-
* binary that backs it). NOTE: distinct from
|
|
23
|
-
* `~/Library/Application Support/Claude/claude_desktop_config.json`,
|
|
24
|
-
* which is the Claude Desktop app's config (different product).
|
|
25
|
-
*/
|
|
26
|
-
const CLAUDE_CONFIG_PATH = path.join(os.homedir(), '.claude.json');
|
|
27
|
-
const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
|
|
28
|
-
const CODEX_HOOKS_PATH = path.join(os.homedir(), '.codex', 'hooks.json');
|
|
29
|
-
const MCP_SERVER_NAME = 'borg';
|
|
30
|
-
function settingsPath() {
|
|
31
|
-
return path.join(os.homedir(), '.claude', 'settings.json');
|
|
32
|
-
}
|
|
33
|
-
function readSettings() {
|
|
34
|
-
const p = settingsPath();
|
|
35
|
-
if (!fs.existsSync(p))
|
|
36
|
-
return {};
|
|
37
|
-
const text = fs.readFileSync(p, 'utf-8');
|
|
38
|
-
if (!text.trim())
|
|
39
|
-
return {};
|
|
40
|
-
return JSON.parse(text);
|
|
41
|
-
}
|
|
42
|
-
function writeSettings(settings) {
|
|
43
|
-
const p = settingsPath();
|
|
44
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
45
|
-
fs.writeFileSync(p, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
46
|
-
}
|
|
47
|
-
function readJsonFile(p) {
|
|
48
|
-
if (!fs.existsSync(p))
|
|
49
|
-
return {};
|
|
50
|
-
const text = fs.readFileSync(p, 'utf-8');
|
|
51
|
-
if (!text.trim())
|
|
52
|
-
return {};
|
|
53
|
-
return JSON.parse(text);
|
|
54
|
-
}
|
|
55
|
-
function writeJsonFile(p, data) {
|
|
56
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
57
|
-
fs.writeFileSync(p, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Register a Claude Code SessionStart hook that runs `borg-regen` at the
|
|
61
|
-
* start of every session. Idempotent: re-running won't add duplicates.
|
|
62
|
-
*
|
|
63
|
-
* Returns true if a change was made, false otherwise (already present, or
|
|
64
|
-
* settings.json could not be parsed).
|
|
65
|
-
*/
|
|
66
|
-
export function addSessionStartHook() {
|
|
67
|
-
let settings;
|
|
68
|
-
try {
|
|
69
|
-
settings = readSettings();
|
|
70
|
-
}
|
|
71
|
-
catch (err) {
|
|
72
|
-
console.error(`⚠ Could not parse ${settingsPath()}: ${err.message}. Skipping hook registration; you can add it manually.`);
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
settings.hooks ??= {};
|
|
76
|
-
settings.hooks.SessionStart ??= [];
|
|
77
|
-
const alreadyPresent = settings.hooks.SessionStart.some((entry) => Array.isArray(entry?.hooks) &&
|
|
78
|
-
entry.hooks.some((h) => h?.type === 'command' && h?.command === HOOK_COMMAND));
|
|
79
|
-
if (alreadyPresent)
|
|
80
|
-
return false;
|
|
81
|
-
settings.hooks.SessionStart.push({
|
|
82
|
-
matcher: '*',
|
|
83
|
-
hooks: [{ type: 'command', command: HOOK_COMMAND }],
|
|
84
|
-
});
|
|
85
|
-
writeSettings(settings);
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Peek whether the borg-regen SessionStart hook is already registered, without
|
|
90
|
-
* mutating settings. Returns false on any read error (safe-default).
|
|
91
|
-
*/
|
|
92
|
-
export function isSessionStartHookRegistered() {
|
|
93
|
-
let settings;
|
|
94
|
-
try {
|
|
95
|
-
settings = readSettings();
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
const arr = settings?.hooks?.SessionStart;
|
|
101
|
-
if (!Array.isArray(arr))
|
|
102
|
-
return false;
|
|
103
|
-
return arr.some((entry) => Array.isArray(entry?.hooks) &&
|
|
104
|
-
entry.hooks.some((h) => h?.type === 'command' && h?.command === HOOK_COMMAND));
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Inverse of addSessionStartHook: remove any SessionStart hook entry whose
|
|
108
|
-
* inner hooks array contains a `borg-regen` command. If multiple commands
|
|
109
|
-
* share an entry, only the borg-regen command is removed; otherwise the
|
|
110
|
-
* entire entry is dropped. Empty containers are cleaned up.
|
|
111
|
-
*
|
|
112
|
-
* Returns true if a change was made, false otherwise.
|
|
113
|
-
*/
|
|
114
|
-
export function removeSessionStartHook() {
|
|
115
|
-
let settings;
|
|
116
|
-
try {
|
|
117
|
-
settings = readSettings();
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
if (!settings?.hooks?.SessionStart)
|
|
123
|
-
return false;
|
|
124
|
-
let changed = false;
|
|
125
|
-
settings.hooks.SessionStart = settings.hooks.SessionStart
|
|
126
|
-
.map((entry) => {
|
|
127
|
-
if (!Array.isArray(entry?.hooks))
|
|
128
|
-
return entry;
|
|
129
|
-
const filtered = entry.hooks.filter((h) => !(h?.type === 'command' && h?.command === HOOK_COMMAND));
|
|
130
|
-
if (filtered.length !== entry.hooks.length) {
|
|
131
|
-
changed = true;
|
|
132
|
-
return { ...entry, hooks: filtered };
|
|
133
|
-
}
|
|
134
|
-
return entry;
|
|
135
|
-
})
|
|
136
|
-
.filter((entry) => Array.isArray(entry?.hooks) && entry.hooks.length > 0);
|
|
137
|
-
if (settings.hooks.SessionStart.length === 0) {
|
|
138
|
-
delete settings.hooks.SessionStart;
|
|
139
|
-
}
|
|
140
|
-
if (Object.keys(settings.hooks).length === 0) {
|
|
141
|
-
delete settings.hooks;
|
|
142
|
-
}
|
|
143
|
-
if (changed)
|
|
144
|
-
writeSettings(settings);
|
|
145
|
-
return changed;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Register a Claude Code UserPromptSubmit hook that runs `borg-log-audit`
|
|
149
|
-
* before each user prompt. The audit script nudges the drone if the
|
|
150
|
-
* previous assistant span used state-changing tools without calling
|
|
151
|
-
* `borg:log`. Idempotent: re-running won't add duplicates.
|
|
152
|
-
*
|
|
153
|
-
* Returns true if a change was made, false otherwise.
|
|
154
|
-
*/
|
|
155
|
-
export function addUserPromptSubmitHook() {
|
|
156
|
-
let settings;
|
|
157
|
-
try {
|
|
158
|
-
settings = readSettings();
|
|
159
|
-
}
|
|
160
|
-
catch (err) {
|
|
161
|
-
console.error(`⚠ Could not parse ${settingsPath()}: ${err.message}. Skipping audit hook registration.`);
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
settings.hooks ??= {};
|
|
165
|
-
settings.hooks.UserPromptSubmit ??= [];
|
|
166
|
-
const alreadyPresent = settings.hooks.UserPromptSubmit.some((entry) => Array.isArray(entry?.hooks) &&
|
|
167
|
-
entry.hooks.some((h) => h?.type === 'command' && h?.command === AUDIT_HOOK_COMMAND));
|
|
168
|
-
if (alreadyPresent)
|
|
169
|
-
return false;
|
|
170
|
-
settings.hooks.UserPromptSubmit.push({
|
|
171
|
-
matcher: '*',
|
|
172
|
-
hooks: [{ type: 'command', command: AUDIT_HOOK_COMMAND }],
|
|
173
|
-
});
|
|
174
|
-
writeSettings(settings);
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Inverse of addUserPromptSubmitHook: remove any UserPromptSubmit hook
|
|
179
|
-
* entry that runs `borg-log-audit`. Symmetric cleanup to
|
|
180
|
-
* removeSessionStartHook.
|
|
181
|
-
*/
|
|
182
|
-
export function removeUserPromptSubmitHook() {
|
|
183
|
-
let settings;
|
|
184
|
-
try {
|
|
185
|
-
settings = readSettings();
|
|
186
|
-
}
|
|
187
|
-
catch {
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
if (!settings?.hooks?.UserPromptSubmit)
|
|
191
|
-
return false;
|
|
192
|
-
let changed = false;
|
|
193
|
-
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit
|
|
194
|
-
.map((entry) => {
|
|
195
|
-
if (!Array.isArray(entry?.hooks))
|
|
196
|
-
return entry;
|
|
197
|
-
const filtered = entry.hooks.filter((h) => !(h?.type === 'command' && h?.command === AUDIT_HOOK_COMMAND));
|
|
198
|
-
if (filtered.length !== entry.hooks.length) {
|
|
199
|
-
changed = true;
|
|
200
|
-
return { ...entry, hooks: filtered };
|
|
201
|
-
}
|
|
202
|
-
return entry;
|
|
203
|
-
})
|
|
204
|
-
.filter((entry) => Array.isArray(entry?.hooks) && entry.hooks.length > 0);
|
|
205
|
-
if (settings.hooks.UserPromptSubmit.length === 0) {
|
|
206
|
-
delete settings.hooks.UserPromptSubmit;
|
|
207
|
-
}
|
|
208
|
-
if (Object.keys(settings.hooks).length === 0) {
|
|
209
|
-
delete settings.hooks;
|
|
210
|
-
}
|
|
211
|
-
if (changed)
|
|
212
|
-
writeSettings(settings);
|
|
213
|
-
return changed;
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Detect whether the borg MCP server is already registered in the Claude
|
|
217
|
-
* Code CLI config (`~/.claude.json` `mcpServers.borg`).
|
|
218
|
-
*
|
|
219
|
-
* Per gh#79: when a user re-runs `borg setup` to refresh OAuth (the
|
|
220
|
-
* canonical re-run reason), the setup wizard's "Add borg to Claude Code?"
|
|
221
|
-
* prompt is redundant — the answer is deterministic ("already
|
|
222
|
-
* configured"). This detect lets the wizard silently skip Step 1 entirely
|
|
223
|
-
* when borg is present. Per the dispatch's Queen-implicit anti-scope,
|
|
224
|
-
* "silent means silent" — callers must not log an "already configured"
|
|
225
|
-
* notice when this returns true.
|
|
226
|
-
*
|
|
227
|
-
* Safe-default contract: any read error (file missing, malformed JSON,
|
|
228
|
-
* permission denied, empty file, unexpected shape) returns `false` so
|
|
229
|
-
* the caller still prompts. The dispatch's edge-case framing is "if
|
|
230
|
-
* indeterminate → prompt fires" — never silent-skip when state is
|
|
231
|
-
* ambiguous. The prompt is the safe path; silent-skip is the
|
|
232
|
-
* optimization layered on top of a verified-present signal.
|
|
233
|
-
*
|
|
234
|
-
* @param configPath Override the config-file path; primarily for tests.
|
|
235
|
-
*/
|
|
236
|
-
export function isMcpServerConfigured(configPath = CLAUDE_CONFIG_PATH) {
|
|
237
|
-
try {
|
|
238
|
-
if (!fs.existsSync(configPath))
|
|
239
|
-
return false;
|
|
240
|
-
const text = fs.readFileSync(configPath, 'utf-8');
|
|
241
|
-
if (!text.trim())
|
|
242
|
-
return false;
|
|
243
|
-
const parsed = JSON.parse(text);
|
|
244
|
-
if (!parsed || typeof parsed !== 'object')
|
|
245
|
-
return false;
|
|
246
|
-
const servers = parsed.mcpServers;
|
|
247
|
-
if (!servers || typeof servers !== 'object' || Array.isArray(servers))
|
|
248
|
-
return false;
|
|
249
|
-
return MCP_SERVER_NAME in servers;
|
|
250
|
-
}
|
|
251
|
-
catch {
|
|
252
|
-
return false;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
export function isCodexMcpServerConfigured(configPath = CODEX_CONFIG_PATH) {
|
|
256
|
-
try {
|
|
257
|
-
if (!fs.existsSync(configPath))
|
|
258
|
-
return false;
|
|
259
|
-
const text = fs.readFileSync(configPath, 'utf-8');
|
|
260
|
-
return (/^\s*\[mcp_servers\.borg\]\s*$/m.test(text) &&
|
|
261
|
-
/^\s*BORG_CODEX_REMOTE_WAKE\s*=\s*"1"\s*$/m.test(text));
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Get absolute path to borg index.js
|
|
269
|
-
* Returns the actual index.js file, not the npm symlink
|
|
270
|
-
*/
|
|
271
|
-
export function getBinaryPath() {
|
|
272
|
-
// In production: dist/index.js is in the same directory as this file
|
|
273
|
-
// In development: same
|
|
274
|
-
return path.join(__dirname, 'index.js');
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Add borg MCP server to Claude Code using claude CLI
|
|
278
|
-
* First removes any existing borg configuration, then adds fresh one
|
|
279
|
-
* Runs: claude mcp remove --scope user borg && claude mcp add --scope user borg borg-mcp
|
|
280
|
-
*/
|
|
281
|
-
export function addMcpServer() {
|
|
282
|
-
try {
|
|
283
|
-
// First, remove any existing borg configuration (ignore errors if not found)
|
|
284
|
-
try {
|
|
285
|
-
execSync('claude mcp remove --scope user borg', { stdio: 'ignore' });
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
// Ignore - server might not exist yet
|
|
289
|
-
}
|
|
290
|
-
// Run claude mcp add command (uses borg-mcp binary which points to index.js)
|
|
291
|
-
const command = `claude mcp add --scope user borg borg-mcp`;
|
|
292
|
-
execSync(command, {
|
|
293
|
-
stdio: 'inherit', // Show output to user
|
|
294
|
-
env: {
|
|
295
|
-
...process.env,
|
|
296
|
-
BORG_API_URL: process.env.BORG_API_URL || 'https://api.borgmcp.ai'
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
catch (error) {
|
|
301
|
-
if (error.message?.includes('command not found')) {
|
|
302
|
-
throw new Error('Claude CLI not found. Please install Claude Code first.');
|
|
303
|
-
}
|
|
304
|
-
throw new Error(`Failed to add MCP server: ${error.message}`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
export function addCodexMcpServer() {
|
|
308
|
-
try {
|
|
309
|
-
try {
|
|
310
|
-
execSync('codex mcp remove borg', { stdio: 'ignore' });
|
|
311
|
-
}
|
|
312
|
-
catch {
|
|
313
|
-
// Ignore - server might not exist yet.
|
|
314
|
-
}
|
|
315
|
-
execSync('codex mcp add borg --env BORG_API_URL=' +
|
|
316
|
-
shellQuote(process.env.BORG_API_URL || 'https://api.borgmcp.ai') +
|
|
317
|
-
' --env BORG_CODEX_REMOTE_WAKE=1' +
|
|
318
|
-
' -- borg-mcp', {
|
|
319
|
-
stdio: 'inherit',
|
|
320
|
-
env: {
|
|
321
|
-
...process.env,
|
|
322
|
-
BORG_API_URL: process.env.BORG_API_URL || 'https://api.borgmcp.ai',
|
|
323
|
-
BORG_CODEX_REMOTE_WAKE: '1',
|
|
324
|
-
},
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
catch (error) {
|
|
328
|
-
if (error.message?.includes('command not found')) {
|
|
329
|
-
throw new Error('Codex CLI not found. Please install Codex first.');
|
|
330
|
-
}
|
|
331
|
-
throw new Error(`Failed to add MCP server to Codex: ${error.message}`);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
function shellQuote(value) {
|
|
335
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
336
|
-
}
|
|
337
|
-
function addCodexHook(eventName, command, options = {}) {
|
|
338
|
-
let hooksFile;
|
|
339
|
-
try {
|
|
340
|
-
hooksFile = readJsonFile(CODEX_HOOKS_PATH);
|
|
341
|
-
}
|
|
342
|
-
catch (err) {
|
|
343
|
-
console.error(`⚠ Could not parse ${CODEX_HOOKS_PATH}: ${err.message}. Skipping Codex hook registration.`);
|
|
344
|
-
return false;
|
|
345
|
-
}
|
|
346
|
-
hooksFile.hooks ??= {};
|
|
347
|
-
hooksFile.hooks[eventName] ??= [];
|
|
348
|
-
const entries = hooksFile.hooks[eventName];
|
|
349
|
-
if (!Array.isArray(entries))
|
|
350
|
-
return false;
|
|
351
|
-
const alreadyPresent = entries.some((entry) => Array.isArray(entry?.hooks) &&
|
|
352
|
-
entry.hooks.some((h) => h?.type === 'command' && h?.command === command));
|
|
353
|
-
if (alreadyPresent)
|
|
354
|
-
return false;
|
|
355
|
-
const entry = {
|
|
356
|
-
hooks: [{ type: 'command', command }],
|
|
357
|
-
};
|
|
358
|
-
if (options.matcher)
|
|
359
|
-
entry.matcher = options.matcher;
|
|
360
|
-
if (typeof options.timeout === 'number')
|
|
361
|
-
entry.hooks[0].timeout = options.timeout;
|
|
362
|
-
entries.push(entry);
|
|
363
|
-
writeJsonFile(CODEX_HOOKS_PATH, hooksFile);
|
|
364
|
-
return true;
|
|
365
|
-
}
|
|
366
|
-
export function addCodexSessionStartHook() {
|
|
367
|
-
return addCodexHook('SessionStart', HOOK_COMMAND, { matcher: 'startup|resume', timeout: 30 });
|
|
368
|
-
}
|
|
369
|
-
export function addCodexUserPromptSubmitHook() {
|
|
370
|
-
return addCodexHook('UserPromptSubmit', AUDIT_HOOK_COMMAND, { timeout: 10 });
|
|
371
|
-
}
|
|
372
|
-
export function isCodexHookRegistered(eventName, command, hooksPath = CODEX_HOOKS_PATH) {
|
|
373
|
-
try {
|
|
374
|
-
const parsed = readJsonFile(hooksPath);
|
|
375
|
-
const arr = parsed?.hooks?.[eventName];
|
|
376
|
-
if (!Array.isArray(arr))
|
|
377
|
-
return false;
|
|
378
|
-
return arr.some((entry) => Array.isArray(entry?.hooks) &&
|
|
379
|
-
entry.hooks.some((h) => h?.type === 'command' && h?.command === command));
|
|
380
|
-
}
|
|
381
|
-
catch {
|
|
382
|
-
return false;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
//# sourceMappingURL=config-utils.js.map
|
|
1
|
+
import{execSync as d}from"child_process";import n from"fs";import f from"os";import i from"path";import{fileURLToPath as x}from"url";import{dirname as _}from"path";const O=x(import.meta.url),C=_(O),c="borg-regen",l="borg-log-audit",P=i.join(f.homedir(),".claude.json"),b=i.join(f.homedir(),".codex","config.toml"),h=i.join(f.homedir(),".codex","hooks.json"),v="borg";function p(){return i.join(f.homedir(),".claude","settings.json")}function m(){const e=p();if(!n.existsSync(e))return{};const o=n.readFileSync(e,"utf-8");return o.trim()?JSON.parse(o):{}}function k(e){const o=p();n.mkdirSync(i.dirname(o),{recursive:!0}),n.writeFileSync(o,JSON.stringify(e,null,2)+`
|
|
2
|
+
`,"utf-8")}function g(e){if(!n.existsSync(e))return{};const o=n.readFileSync(e,"utf-8");return o.trim()?JSON.parse(o):{}}function R(e,o){n.mkdirSync(i.dirname(e),{recursive:!0}),n.writeFileSync(e,JSON.stringify(o,null,2)+`
|
|
3
|
+
`,"utf-8")}function G(){let e;try{e=m()}catch(r){return console.error(`\u26A0 Could not parse ${p()}: ${r.message}. Skipping hook registration; you can add it manually.`),!1}return e.hooks??={},e.hooks.SessionStart??=[],e.hooks.SessionStart.some(r=>Array.isArray(r?.hooks)&&r.hooks.some(t=>t?.type==="command"&&t?.command===c))?!1:(e.hooks.SessionStart.push({matcher:"*",hooks:[{type:"command",command:c}]}),k(e),!0)}function I(){let e;try{e=m()}catch{return!1}const o=e?.hooks?.SessionStart;return Array.isArray(o)?o.some(r=>Array.isArray(r?.hooks)&&r.hooks.some(t=>t?.type==="command"&&t?.command===c)):!1}function $(){let e;try{e=m()}catch{return!1}if(!e?.hooks?.SessionStart)return!1;let o=!1;return e.hooks.SessionStart=e.hooks.SessionStart.map(r=>{if(!Array.isArray(r?.hooks))return r;const t=r.hooks.filter(s=>!(s?.type==="command"&&s?.command===c));return t.length!==r.hooks.length?(o=!0,{...r,hooks:t}):r}).filter(r=>Array.isArray(r?.hooks)&&r.hooks.length>0),e.hooks.SessionStart.length===0&&delete e.hooks.SessionStart,Object.keys(e.hooks).length===0&&delete e.hooks,o&&k(e),o}function B(){let e;try{e=m()}catch(r){return console.error(`\u26A0 Could not parse ${p()}: ${r.message}. Skipping audit hook registration.`),!1}return e.hooks??={},e.hooks.UserPromptSubmit??=[],e.hooks.UserPromptSubmit.some(r=>Array.isArray(r?.hooks)&&r.hooks.some(t=>t?.type==="command"&&t?.command===l))?!1:(e.hooks.UserPromptSubmit.push({matcher:"*",hooks:[{type:"command",command:l}]}),k(e),!0)}function L(){let e;try{e=m()}catch{return!1}if(!e?.hooks?.UserPromptSubmit)return!1;let o=!1;return e.hooks.UserPromptSubmit=e.hooks.UserPromptSubmit.map(r=>{if(!Array.isArray(r?.hooks))return r;const t=r.hooks.filter(s=>!(s?.type==="command"&&s?.command===l));return t.length!==r.hooks.length?(o=!0,{...r,hooks:t}):r}).filter(r=>Array.isArray(r?.hooks)&&r.hooks.length>0),e.hooks.UserPromptSubmit.length===0&&delete e.hooks.UserPromptSubmit,Object.keys(e.hooks).length===0&&delete e.hooks,o&&k(e),o}function D(e=P){try{if(!n.existsSync(e))return!1;const o=n.readFileSync(e,"utf-8");if(!o.trim())return!1;const r=JSON.parse(o);if(!r||typeof r!="object")return!1;const t=r.mcpServers;return!t||typeof t!="object"||Array.isArray(t)?!1:v in t}catch{return!1}}function N(e=b){try{if(!n.existsSync(e))return!1;const o=n.readFileSync(e,"utf-8");return/^\s*\[mcp_servers\.borg\]\s*$/m.test(o)&&/^\s*BORG_CODEX_REMOTE_WAKE\s*=\s*"1"\s*$/m.test(o)}catch{return!1}}function T(){return i.join(C,"index.js")}function J(){try{try{d("claude mcp remove --scope user borg",{stdio:"ignore"})}catch{}d("claude mcp add --scope user borg borg-mcp",{stdio:"inherit",env:{...process.env,BORG_API_URL:process.env.BORG_API_URL||"https://api.borgmcp.ai"}})}catch(e){throw e.message?.includes("command not found")?new Error("Claude CLI not found. Please install Claude Code first."):new Error(`Failed to add MCP server: ${e.message}`)}}function K(){try{try{d("codex mcp remove borg",{stdio:"ignore"})}catch{}d("codex mcp add borg --env BORG_API_URL="+E(process.env.BORG_API_URL||"https://api.borgmcp.ai")+" --env BORG_CODEX_REMOTE_WAKE=1 -- borg-mcp",{stdio:"inherit",env:{...process.env,BORG_API_URL:process.env.BORG_API_URL||"https://api.borgmcp.ai",BORG_CODEX_REMOTE_WAKE:"1"}})}catch(e){throw e.message?.includes("command not found")?new Error("Codex CLI not found. Please install Codex first."):new Error(`Failed to add MCP server to Codex: ${e.message}`)}}function E(e){return`'${e.replace(/'/g,"'\\''")}'`}function A(e,o,r={}){let t;try{t=g(h)}catch(u){return console.error(`\u26A0 Could not parse ${h}: ${u.message}. Skipping Codex hook registration.`),!1}t.hooks??={},t.hooks[e]??=[];const s=t.hooks[e];if(!Array.isArray(s)||s.some(u=>Array.isArray(u?.hooks)&&u.hooks.some(y=>y?.type==="command"&&y?.command===o)))return!1;const a={hooks:[{type:"command",command:o}]};return r.matcher&&(a.matcher=r.matcher),typeof r.timeout=="number"&&(a.hooks[0].timeout=r.timeout),s.push(a),R(h,t),!0}function X(){return A("SessionStart",c,{matcher:"startup|resume",timeout:30})}function W(){return A("UserPromptSubmit",l,{timeout:10})}function Q(e,o,r=h){try{const s=g(r)?.hooks?.[e];return Array.isArray(s)?s.some(S=>Array.isArray(S?.hooks)&&S.hooks.some(a=>a?.type==="command"&&a?.command===o)):!1}catch{return!1}}export{K as addCodexMcpServer,X as addCodexSessionStartHook,W as addCodexUserPromptSubmitHook,J as addMcpServer,G as addSessionStartHook,B as addUserPromptSubmitHook,T as getBinaryPath,Q as isCodexHookRegistered,N as isCodexMcpServerConfigured,D as isMcpServerConfigured,I as isSessionStartHookRegistered,$ as removeSessionStartHook,L as removeUserPromptSubmitHook};
|