ruvnet-brain 3.9.85-dev → 3.9.129-dev

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.
@@ -0,0 +1,236 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ export const MANAGED_EXECUTABLES = Object.freeze([
7
+ 'ruflo',
8
+ 'claude-flow',
9
+ 'agentic-flow',
10
+ 'agentic-qe',
11
+ 'ruvector',
12
+ 'agent-browser',
13
+ 'ruv-swarm',
14
+ ]);
15
+
16
+ const MANAGED = new Set(MANAGED_EXECUTABLES);
17
+ const SUBCOMMAND = /^[a-z][a-z0-9-]*$/;
18
+ const MAX_ARGS = 256;
19
+ const MAX_ARG_BYTES = 8192;
20
+ const DEFAULT_FRESH_MS = 24 * 60 * 60 * 1000;
21
+ const DEFAULT_TIMEOUT_MS = 30_000;
22
+ const MAX_OUTPUT_BYTES = 1024 * 1024;
23
+
24
+ const executableSchema = {
25
+ type: 'string',
26
+ enum: [...MANAGED_EXECUTABLES],
27
+ description: 'One of the finite RuvNet ecosystem executables managed by this boundary.',
28
+ };
29
+
30
+ export const MANAGED_CLI_TOOLS = Object.freeze([
31
+ {
32
+ name: 'ruvnet_cli_help',
33
+ description: 'Read a managed CLI interface from the executable itself. Runs only the supplied subcommand path plus --help and records a fresh stamp only after exit 0.',
34
+ inputSchema: {
35
+ type: 'object',
36
+ additionalProperties: false,
37
+ properties: {
38
+ executable: executableSchema,
39
+ argv: {
40
+ type: 'array',
41
+ description: 'Zero, one, or two literal lowercase subcommand tokens. --help is appended by the server.',
42
+ items: { type: 'string', pattern: '^[a-z][a-z0-9-]*$' },
43
+ maxItems: 2,
44
+ default: [],
45
+ },
46
+ },
47
+ required: ['executable', 'argv'],
48
+ },
49
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
50
+ },
51
+ {
52
+ name: 'ruvnet_cli_run',
53
+ description: 'Run a managed CLI with a literal argv array and no shell. Refuses unless the matching interface help was read successfully in the last 24 hours.',
54
+ inputSchema: {
55
+ type: 'object',
56
+ additionalProperties: false,
57
+ properties: {
58
+ executable: executableSchema,
59
+ argv: {
60
+ type: 'array',
61
+ description: 'Literal argv tokens. Shell metacharacters are ordinary argument bytes and are never evaluated.',
62
+ items: { type: 'string', maxLength: MAX_ARG_BYTES },
63
+ maxItems: MAX_ARGS,
64
+ },
65
+ },
66
+ required: ['executable', 'argv'],
67
+ },
68
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
69
+ },
70
+ ]);
71
+
72
+ function assertExecutable(executable) {
73
+ if (typeof executable !== 'string' || !MANAGED.has(executable)) {
74
+ throw new Error(`unknown managed executable: ${String(executable || '')}`);
75
+ }
76
+ return executable;
77
+ }
78
+
79
+ function literalArgv(argv) {
80
+ if (!Array.isArray(argv) || argv.length > MAX_ARGS) throw new Error('argv must be an array of at most 256 strings');
81
+ return argv.map((arg) => {
82
+ if (typeof arg !== 'string' || arg.includes('\0') || Buffer.byteLength(arg) > MAX_ARG_BYTES) {
83
+ throw new Error('every argv entry must be a literal string of at most 8192 bytes without NUL');
84
+ }
85
+ return arg;
86
+ });
87
+ }
88
+
89
+ export function helpKey(executable, argv) {
90
+ const tool = assertExecutable(executable);
91
+ const args = literalArgv(argv);
92
+ const subcommands = [];
93
+ for (const arg of args) {
94
+ if (!SUBCOMMAND.test(arg)) break;
95
+ subcommands.push(arg);
96
+ if (subcommands.length === 2) break;
97
+ }
98
+ return [tool, ...subcommands].join('.');
99
+ }
100
+
101
+ export function stampKeysForHelp(executable, argv) {
102
+ const tool = assertExecutable(executable);
103
+ const args = literalArgv(argv);
104
+ if (args.length > 2 || args.some((arg) => !SUBCOMMAND.test(arg))) {
105
+ throw new Error('invalid subcommand path: help accepts zero, one, or two lowercase tokens');
106
+ }
107
+ const exact = [tool, ...args].join('.');
108
+ if (args.length < 2) return [exact];
109
+ return [exact, [tool, args[0]].join('.')];
110
+ }
111
+
112
+ function stateDir(env) {
113
+ const brainHome = env.RUVNET_BRAIN_HOME || path.join(env.HOME || os.homedir(), '.cache', 'ruvnet-brain');
114
+ return env.RUVNET_BRAIN_HELP_READ_DIR || path.join(brainHome, 'help-read');
115
+ }
116
+
117
+ function freshStamp(executable, argv, env, now = Date.now()) {
118
+ const stamp = path.join(stateDir(env), helpKey(executable, argv));
119
+ let stat;
120
+ try {
121
+ stat = fs.statSync(stamp);
122
+ } catch {
123
+ return false;
124
+ }
125
+ const configured = Number(env.RUVNET_BRAIN_HELP_MAX_AGE_MS);
126
+ const maxAge = Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_FRESH_MS;
127
+ return now - stat.mtimeMs < maxAge;
128
+ }
129
+
130
+ function writeStamps(executable, argv, env) {
131
+ const dir = stateDir(env);
132
+ fs.mkdirSync(dir, { recursive: true });
133
+ for (const key of stampKeysForHelp(executable, argv)) {
134
+ const stamp = path.join(dir, key);
135
+ const temporary = path.join(dir, `.${key}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
136
+ try {
137
+ fs.writeFileSync(temporary, '', { flag: 'wx', mode: 0o600 });
138
+ fs.renameSync(temporary, stamp);
139
+ } finally {
140
+ try { fs.unlinkSync(temporary); } catch { /* renamed or never created */ }
141
+ }
142
+ }
143
+ }
144
+
145
+ function execute(executable, argv, env) {
146
+ return new Promise((resolve) => {
147
+ const child = spawn(executable, argv, {
148
+ env,
149
+ shell: false,
150
+ stdio: ['ignore', 'pipe', 'pipe'],
151
+ });
152
+ const stdout = [];
153
+ const stderr = [];
154
+ let stdoutBytes = 0;
155
+ let stderrBytes = 0;
156
+ let settled = false;
157
+ const append = (chunks, chunk, used) => {
158
+ const remaining = MAX_OUTPUT_BYTES - used;
159
+ if (remaining > 0) chunks.push(chunk.subarray(0, remaining));
160
+ return used + Math.min(chunk.length, Math.max(remaining, 0));
161
+ };
162
+ child.stdout.on('data', (chunk) => { stdoutBytes = append(stdout, chunk, stdoutBytes); });
163
+ child.stderr.on('data', (chunk) => { stderrBytes = append(stderr, chunk, stderrBytes); });
164
+ const configured = Number(env.RUVNET_BRAIN_MANAGED_CLI_TIMEOUT_MS);
165
+ const timeoutMs = Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_TIMEOUT_MS;
166
+ const timer = setTimeout(() => {
167
+ if (settled) return;
168
+ settled = true;
169
+ child.kill('SIGTERM');
170
+ resolve({ code: null, stdout: Buffer.concat(stdout).toString(), stderr: Buffer.concat(stderr).toString(), error: `timed out after ${timeoutMs}ms` });
171
+ }, timeoutMs);
172
+ child.once('error', (error) => {
173
+ if (settled) return;
174
+ settled = true;
175
+ clearTimeout(timer);
176
+ resolve({ code: null, stdout: '', stderr: '', error: error.message });
177
+ });
178
+ child.once('close', (code) => {
179
+ if (settled) return;
180
+ settled = true;
181
+ clearTimeout(timer);
182
+ resolve({ code, stdout: Buffer.concat(stdout).toString(), stderr: Buffer.concat(stderr).toString(), error: null });
183
+ });
184
+ });
185
+ }
186
+
187
+ function resultOf(executable, argv, result) {
188
+ const output = [result.stdout, result.stderr].filter(Boolean).join(result.stdout && result.stderr ? '\n' : '');
189
+ if (result.error || result.code !== 0) {
190
+ const reason = result.error || `exit ${result.code}`;
191
+ return {
192
+ content: [{ type: 'text', text: output || `${executable} ${argv.join(' ')} failed: ${reason}` }],
193
+ isError: true,
194
+ };
195
+ }
196
+ return {
197
+ content: [{ type: 'text', text: output || `${executable} ${argv.join(' ')} completed successfully` }],
198
+ isError: false,
199
+ };
200
+ }
201
+
202
+ export async function callManagedCli(toolName, args, env = process.env) {
203
+ try {
204
+ const executable = assertExecutable(args?.executable);
205
+ const argv = literalArgv(args?.argv);
206
+
207
+ if (toolName === 'ruvnet_cli_help') {
208
+ stampKeysForHelp(executable, argv);
209
+ const commandArgv = [...argv, '--help'];
210
+ const execution = await execute(executable, commandArgv, env);
211
+ if (execution.code === 0 && !execution.error) writeStamps(executable, argv, env);
212
+ return resultOf(executable, commandArgv, execution);
213
+ }
214
+
215
+ if (toolName === 'ruvnet_cli_run') {
216
+ if (!freshStamp(executable, argv, env)) {
217
+ const key = helpKey(executable, argv).replaceAll('.', ' ');
218
+ return {
219
+ content: [{ type: 'text', text: `Read the interface first with ruvnet_cli_help for: ${key}` }],
220
+ isError: true,
221
+ };
222
+ }
223
+ return resultOf(executable, argv, await execute(executable, argv, env));
224
+ }
225
+
226
+ return {
227
+ content: [{ type: 'text', text: `unknown managed CLI tool: ${String(toolName || '')}` }],
228
+ isError: true,
229
+ };
230
+ } catch (error) {
231
+ return {
232
+ content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
233
+ isError: true,
234
+ };
235
+ }
236
+ }
@@ -33,6 +33,7 @@ import fs from 'node:fs';
33
33
  import path from 'node:path';
34
34
  import os from 'node:os';
35
35
  import readline from 'node:readline';
36
+ import { callManagedCli, MANAGED_CLI_TOOLS } from './managed-cli-interface.mjs';
36
37
 
37
38
  const BRAIN_HOME = process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain');
38
39
  const KB = process.env.RUVNET_BRAIN_KB || path.join(BRAIN_HOME, 'kb');
@@ -43,9 +44,9 @@ const LEASE = path.join(LEASES, `mcp-${process.pid}.json`);
43
44
 
44
45
  const PROTOCOL_VERSION = '2024-11-05';
45
46
  const SERVER_INFO = { name: 'ruvnet-brain', version: '2.0.0' };
46
- // Static fallback tool declaration — same name + inputSchema the brain declares (the SCHEMA is the
47
- // frozen contract; the description is data and is refreshed from the live child when one is up).
48
- const FALLBACK_TOOLS = [{
47
+ // Local frozen tool declarations. search_ruvnet is only a fallback: a live child's declaration wins
48
+ // so its description can refresh. The two managed-CLI schemas are owned by this protocol shell.
49
+ const SEARCH_TOOL = {
49
50
  name: 'search_ruvnet',
50
51
  description: 'Source-grounded knowledge base for the RuvNet ecosystem. (Brain bundle not installed on this machine — calls will return install guidance.)',
51
52
  inputSchema: {
@@ -56,7 +57,19 @@ const FALLBACK_TOOLS = [{
56
57
  },
57
58
  required: ['query'],
58
59
  },
59
- }];
60
+ };
61
+ const FALLBACK_TOOLS = [SEARCH_TOOL, ...MANAGED_CLI_TOOLS];
62
+
63
+ function withLocalTools(tools) {
64
+ const merged = new Map();
65
+ for (const tool of Array.isArray(tools) ? tools : []) {
66
+ if (tool?.name) merged.set(tool.name, tool);
67
+ }
68
+ for (const tool of FALLBACK_TOOLS) {
69
+ if (!merged.has(tool.name)) merged.set(tool.name, tool);
70
+ }
71
+ return [...merged.values()];
72
+ }
60
73
 
61
74
  const out = (obj) => process.stdout.write(JSON.stringify(obj) + '\n');
62
75
  const clientOk = (id, result) => out({ jsonrpc: '2.0', id, result });
@@ -80,45 +93,117 @@ process.on('exit', () => { try { fs.rmSync(LEASE, { force: true }); } catch { /*
80
93
  // ── the warm child + private protocol ───────────────────────────────────────────────────────────
81
94
  let child = null; // { proc, generation, nextId, pending: Map<childId, {resolve, reject}> }
82
95
  let pendingCount = 0; // client tools/call requests currently in flight (drain gate for swaps)
96
+ let childRetirement = Promise.resolve();
97
+ let childStartup = null;
98
+ const CHILD_TERM_GRACE_MS = 3_000;
99
+ const CHILD_INIT_TIMEOUT_MS = Number(process.env.RUVNET_BRAIN_INIT_TIMEOUT_MS) || 60_000;
83
100
 
84
101
  function killChild(reason) {
85
- if (!child) return;
102
+ if (!child) return childRetirement;
86
103
  const c = child; child = null;
87
104
  for (const [, p] of c.pending) p.reject(new Error(`brain worker ${reason}`));
88
105
  c.pending.clear();
106
+ childRetirement = new Promise((resolve) => {
107
+ let settled = false;
108
+ const finish = () => {
109
+ if (settled) return;
110
+ settled = true;
111
+ clearTimeout(force);
112
+ resolve();
113
+ };
114
+ c.proc.once('exit', finish);
115
+ const force = setTimeout(() => {
116
+ try { c.proc.kill('SIGKILL'); } catch { /* already gone */ }
117
+ }, CHILD_TERM_GRACE_MS);
118
+ force.unref?.();
119
+ });
89
120
  try { c.proc.kill('SIGTERM'); } catch { /* already dead */ }
121
+ return childRetirement;
90
122
  }
91
123
 
92
124
  async function ensureChild() {
93
125
  const gen = currentGeneration();
94
- if (child && child.generation !== gen && pendingCount === 0) killChild('superseded by a newer generation');
126
+ if (child && child.generation !== gen && pendingCount === 0) await killChild('superseded by a newer generation');
95
127
  if (child) return child;
128
+ await childRetirement;
96
129
  if (!fs.existsSync(CHILD_MCP)) return null;
97
130
 
98
- const env = { ...process.env, KB_DIR: KB };
99
- if (!env.KB_MODEL_CACHE) env.KB_MODEL_CACHE = path.join(BRAIN_HOME, 'models');
100
- const proc = spawn(process.execPath, [CHILD_MCP], { stdio: ['pipe', 'pipe', 'inherit'], env });
101
- const c = { proc, generation: gen, nextId: 1, pending: new Map() };
102
- const rl = readline.createInterface({ input: proc.stdout });
103
- rl.on('line', (line) => {
104
- let msg; try { msg = JSON.parse(line); } catch { return; }
105
- const waiter = c.pending.get(msg.id);
106
- if (waiter) { c.pending.delete(msg.id); waiter.resolve(msg); }
107
- });
108
- proc.on('exit', () => { if (child === c) child = null; for (const [, p] of c.pending) p.reject(new Error('brain worker exited')); c.pending.clear(); });
109
- proc.on('error', () => { if (child === c) child = null; });
110
- child = c;
111
-
112
- // PRIVATE handshake — parent-owned id, never the client's (finding 8).
113
- try { await childRequest(c, 'initialize', { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: 'ruvnet-brain-shell', version: SERVER_INFO.version } }, 10_000); }
114
- catch { killChild('failed initialize'); return null; }
115
- return c;
131
+ if (!childStartup) {
132
+ childStartup = (async () => {
133
+ const env = { ...process.env, KB_DIR: KB };
134
+ if (!env.KB_MODEL_CACHE) env.KB_MODEL_CACHE = path.join(BRAIN_HOME, 'models');
135
+ const proc = spawn(process.execPath, [CHILD_MCP], { stdio: ['pipe', 'pipe', 'inherit'], env });
136
+ const c = { proc, generation: currentGeneration(), nextId: 1, pending: new Map() };
137
+ const rl = readline.createInterface({ input: proc.stdout });
138
+ rl.on('line', (line) => {
139
+ let msg; try { msg = JSON.parse(line); } catch { return; }
140
+ const waiter = c.pending.get(msg.id);
141
+ if (waiter) { c.pending.delete(msg.id); waiter.resolve(msg); }
142
+ });
143
+ proc.on('exit', () => {
144
+ if (child === c) child = null;
145
+ for (const [, p] of c.pending) p.reject(new Error('brain worker exited'));
146
+ c.pending.clear();
147
+ });
148
+ proc.on('error', () => { if (child === c) child = null; });
149
+ child = c;
150
+
151
+ try {
152
+ await childRequest(c, 'initialize', {
153
+ protocolVersion: PROTOCOL_VERSION,
154
+ capabilities: {},
155
+ clientInfo: { name: 'ruvnet-brain-shell', version: SERVER_INFO.version },
156
+ }, CHILD_INIT_TIMEOUT_MS);
157
+ } catch (e) {
158
+ await killChild('failed initialize');
159
+ throw new Error(`brain worker failed to initialize: ${e.message}`);
160
+ }
161
+ return c;
162
+ })();
163
+ }
164
+ const attempt = childStartup;
165
+ try { return await attempt; }
166
+ finally { if (childStartup === attempt) childStartup = null; }
116
167
  }
117
168
 
118
- function childRequest(c, method, params, timeoutMs = 120_000) {
169
+ // A TIMEOUT IS AN OUTAGE, and it must both STOP and be RECORDED. Measured 2026-07-27: the timeout
170
+ // below deleted its pending entry and rejected, and did nothing else — so
171
+ // (a) the child kept computing an answer nobody would ever read, at ~95% CPU, and on the
172
+ // all-repos path (605 cross-encoder pairs, a fan-out that alone exceeds this deadline) that
173
+ // is minutes of burn per abandoned query, which then slows the RETRY, which times out too; and
174
+ // (b) nothing wrote health.json, so a total retrieval failure read as healthy. The live file
175
+ // said "status":"ok" dated four days earlier while every query was timing out.
176
+ // (b) is the worse half: the product reporting green while it is down is the one thing it may
177
+ // never do. The child's own alarm cannot cover this — it only rings when a search RETURNS failure,
178
+ // and a timeout is precisely the case where it never returns at all. Only the parent can see it.
179
+ async function onChildTimeout(method, timeoutMs) {
180
+ killChild(`timed out after ${timeoutMs / 1000}s on ${method}`); // stop the burn; ensureChild() respawns
181
+ try {
182
+ const alarm = await import(new URL('../../kb/brain-alarm.mjs', import.meta.url).href);
183
+ await alarm.reportBrainDown({
184
+ error: `brain worker timed out after ${timeoutMs / 1000}s on ${method} (no answer returned)`,
185
+ source: 'mcp-parent-timeout',
186
+ });
187
+ } catch (e) {
188
+ // REPORTED, never swallowed: brain-alarm.mjs lives in the KB, which can legitimately be absent
189
+ // or half-installed — but a health reporter that fails silently is the same lie one layer down.
190
+ console.error(`[ruvnet-brain] timeout on ${method} could not be recorded to health.json: ${e.message}`);
191
+ }
192
+ }
193
+
194
+ // Overridable so the outage path above can actually be TESTED — a 120s default is untestable, and
195
+ // an untestable failure path is how this one went four days reporting "ok" while it was down. Also
196
+ // genuinely useful in the field: a slow machine can raise it, and a CI runner can lower it.
197
+ const CALL_TIMEOUT_MS = Number(process.env.RUVNET_BRAIN_CALL_TIMEOUT_MS) || 120_000;
198
+
199
+ function childRequest(c, method, params, timeoutMs = CALL_TIMEOUT_MS) {
119
200
  return new Promise((resolve, reject) => {
120
201
  const id = c.nextId++;
121
- const timer = setTimeout(() => { c.pending.delete(id); reject(new Error(`brain worker timeout on ${method}`)); }, timeoutMs);
202
+ const timer = setTimeout(() => {
203
+ c.pending.delete(id);
204
+ reject(new Error(`brain worker timeout on ${method}`));
205
+ void onChildTimeout(method, timeoutMs); // after the reject — the caller must not wait on the alarm
206
+ }, timeoutMs);
122
207
  c.pending.set(id, { resolve: (m) => { clearTimeout(timer); resolve(m); }, reject: (e) => { clearTimeout(timer); reject(e); } });
123
208
  try { c.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n'); }
124
209
  catch (e) { clearTimeout(timer); c.pending.delete(id); reject(e); }
@@ -135,17 +220,33 @@ async function handleClient(msg) {
135
220
  case 'ping':
136
221
  return clientOk(id, {});
137
222
  case 'tools/list': {
138
- const c = await ensureChild();
139
- if (c) {
140
- try { const r = await childRequest(c, 'tools/list', {}, 15_000); if (r.result?.tools?.length) return clientOk(id, r.result); }
141
- catch { /* fall through to the static declaration */ }
142
- }
223
+ try {
224
+ const c = await ensureChild();
225
+ if (c) {
226
+ try {
227
+ const r = await childRequest(c, 'tools/list', {}, 15_000);
228
+ if (r.result?.tools?.length) return clientOk(id, { ...r.result, tools: withLocalTools(r.result.tools) });
229
+ }
230
+ catch { /* fall through to the static declaration */ }
231
+ }
232
+ } catch { /* fall through to the static declaration during a transient startup outage */ }
143
233
  return clientOk(id, { tools: FALLBACK_TOOLS });
144
234
  }
145
235
  case 'tools/call': {
236
+ if (params?.name === 'ruvnet_cli_help' || params?.name === 'ruvnet_cli_run') {
237
+ return clientOk(id, await callManagedCli(params.name, params.arguments || {}));
238
+ }
146
239
  if (params?.name !== 'search_ruvnet') return clientErr(id, -32602, `unknown tool: ${params?.name}`);
147
240
  refreshLease();
148
- const c = await ensureChild();
241
+ let c;
242
+ try {
243
+ c = await ensureChild();
244
+ } catch (e) {
245
+ return clientOk(id, {
246
+ content: [{ type: 'text', text: `search_ruvnet error: brain worker temporarily unavailable (${e.message})` }],
247
+ isError: true,
248
+ });
249
+ }
149
250
  if (!c) {
150
251
  return clientOk(id, { content: [{ type: 'text', text: `search_ruvnet error: the brain bundle is not installed at ${KB}. Install it with: npx github:stuinfla/ruvnet-brain (or set RUVNET_BRAIN_KB to your brain's kb dir).` }], isError: true });
151
252
  }
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+
7
+ const brainHome = process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain');
8
+ const versions = path.join(brainHome, 'versions');
9
+
10
+ function activeRoot() {
11
+ try {
12
+ const active = JSON.parse(fs.readFileSync(path.join(brainHome, 'active.json'), 'utf8'));
13
+ if (!active || typeof active.codeRoot !== 'string') return null;
14
+ const candidate = path.isAbsolute(active.codeRoot)
15
+ ? active.codeRoot
16
+ : path.join(brainHome, active.codeRoot);
17
+ const real = fs.realpathSync(candidate);
18
+ const versionsReal = fs.realpathSync(versions);
19
+ return real.startsWith(`${versionsReal}${path.sep}`) ? real : null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ const input = fs.readFileSync(0);
26
+ const root = activeRoot();
27
+ const adapter = root && path.join(root, 'scripts', 'codex-hook-adapter.mjs');
28
+ if (!adapter || !fs.existsSync(adapter)) process.exit(0);
29
+
30
+ const result = spawnSync(process.execPath, [adapter, ...process.argv.slice(2)], {
31
+ input,
32
+ encoding: 'utf8',
33
+ env: process.env,
34
+ });
35
+ if (result.stdout) process.stdout.write(result.stdout);
36
+ if (result.stderr) process.stderr.write(result.stderr);
37
+ process.exit(result.status ?? 0);