@kodelyth/acpx 2026.5.39 → 2026.5.42

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.
Files changed (47) hide show
  1. package/AGENTS.md +54 -0
  2. package/CLAUDE.md +54 -0
  3. package/dist/index.js +14 -0
  4. package/dist/process-reaper-DdVqzAA_.js +370 -0
  5. package/dist/register.runtime.js +53 -0
  6. package/dist/runtime-D9qhNKmy.js +741 -0
  7. package/dist/runtime-api.js +4 -0
  8. package/dist/service-CXeUME_-.js +1483 -0
  9. package/dist/setup-api.js +16 -0
  10. package/index.test.ts +119 -0
  11. package/index.ts +19 -0
  12. package/klaw.plugin.json +12 -27
  13. package/package.json +2 -2
  14. package/register.runtime.test.ts +104 -0
  15. package/register.runtime.ts +86 -0
  16. package/runtime-api.ts +49 -0
  17. package/setup-api.ts +18 -0
  18. package/src/acpx-runtime-compat.d.ts +65 -0
  19. package/src/claude-agent-acp-completion.test.ts +187 -0
  20. package/src/codex-auth-bridge.test.ts +688 -0
  21. package/src/codex-auth-bridge.ts +780 -0
  22. package/src/codex-trust-config.ts +297 -0
  23. package/src/config-schema.ts +118 -0
  24. package/src/config.test.ts +285 -0
  25. package/src/config.ts +281 -0
  26. package/src/manifest.test.ts +21 -0
  27. package/src/process-lease.test.ts +89 -0
  28. package/src/process-lease.ts +179 -0
  29. package/src/process-reaper.test.ts +330 -0
  30. package/src/process-reaper.ts +434 -0
  31. package/src/runtime-internals/error-format.mjs +6 -0
  32. package/src/runtime-internals/mcp-command-line.mjs +123 -0
  33. package/src/runtime-internals/mcp-command-line.test.ts +59 -0
  34. package/src/runtime-internals/mcp-proxy.mjs +121 -0
  35. package/src/runtime-internals/mcp-proxy.test.ts +130 -0
  36. package/src/runtime.test.ts +1817 -0
  37. package/src/runtime.ts +1261 -0
  38. package/src/service.test.ts +802 -0
  39. package/src/service.ts +630 -0
  40. package/tsconfig.json +16 -0
  41. package/index.js +0 -7
  42. package/register.runtime.js +0 -7
  43. package/runtime-api.js +0 -7
  44. package/setup-api.js +0 -7
  45. /package/{error-format.mjs → dist/error-format.mjs} +0 -0
  46. /package/{mcp-command-line.mjs → dist/mcp-command-line.mjs} +0 -0
  47. /package/{mcp-proxy.mjs → dist/mcp-proxy.mjs} +0 -0
@@ -0,0 +1,780 @@
1
+ import fsSync from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import { createRequire } from "node:module";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { readJsonFileWithFallback } from "klaw/plugin-sdk/json-store";
7
+ import {
8
+ extractTrustedCodexProjectPaths,
9
+ renderIsolatedCodexConfig,
10
+ } from "./codex-trust-config.js";
11
+ import { resolveAcpxPluginRoot } from "./config.js";
12
+ import type { ResolvedAcpxPluginConfig } from "./config.js";
13
+ import {
14
+ KLAW_ACPX_LEASE_ID_ARG,
15
+ KLAW_ACPX_LEASE_ID_ENV,
16
+ KLAW_GATEWAY_INSTANCE_ID_ARG,
17
+ } from "./process-lease.js";
18
+
19
+ const CODEX_ACP_PACKAGE = "@zed-industries/codex-acp";
20
+ const CODEX_ACP_BIN = "codex-acp";
21
+ const CLAUDE_ACP_PACKAGE = "@agentclientprotocol/claude-agent-acp";
22
+ const CLAUDE_ACP_BIN = "claude-agent-acp";
23
+ const RUN_CONFIGURED_COMMAND_SENTINEL = "--klaw-run-configured";
24
+ const requireFromHere = createRequire(import.meta.url);
25
+
26
+ type PackageManifest = {
27
+ name?: unknown;
28
+ bin?: unknown;
29
+ dependencies?: Record<string, unknown>;
30
+ };
31
+
32
+ function readSelfManifest(): PackageManifest {
33
+ const manifestPath = path.join(resolveAcpxPluginRoot(import.meta.url), "package.json");
34
+ return JSON.parse(fsSync.readFileSync(manifestPath, "utf8")) as PackageManifest;
35
+ }
36
+
37
+ function readManifestDependencyVersion(packageName: string): string {
38
+ const version = readSelfManifest().dependencies?.[packageName];
39
+ if (typeof version !== "string" || version.trim() === "") {
40
+ throw new Error(`Missing ${packageName} dependency version in @kodelyth/acpx manifest`);
41
+ }
42
+ return version;
43
+ }
44
+
45
+ const CODEX_ACP_PACKAGE_VERSION = readManifestDependencyVersion(CODEX_ACP_PACKAGE);
46
+ const CLAUDE_ACP_PACKAGE_VERSION = readManifestDependencyVersion(CLAUDE_ACP_PACKAGE);
47
+
48
+ function quoteCommandPart(value: string): string {
49
+ return JSON.stringify(value);
50
+ }
51
+
52
+ function splitCommandParts(value: string): string[] {
53
+ const parts: string[] = [];
54
+ let current = "";
55
+ let quote: "'" | '"' | null = null;
56
+ let escaping = false;
57
+
58
+ for (const ch of value) {
59
+ if (escaping) {
60
+ current += ch;
61
+ escaping = false;
62
+ continue;
63
+ }
64
+ if (ch === "\\" && quote !== "'") {
65
+ escaping = true;
66
+ continue;
67
+ }
68
+ if (quote) {
69
+ if (ch === quote) {
70
+ quote = null;
71
+ } else {
72
+ current += ch;
73
+ }
74
+ continue;
75
+ }
76
+ if (ch === "'" || ch === '"') {
77
+ quote = ch;
78
+ continue;
79
+ }
80
+ if (/\s/.test(ch)) {
81
+ if (current) {
82
+ parts.push(current);
83
+ current = "";
84
+ }
85
+ continue;
86
+ }
87
+ current += ch;
88
+ }
89
+
90
+ if (escaping) {
91
+ current += "\\";
92
+ }
93
+ if (current) {
94
+ parts.push(current);
95
+ }
96
+ return parts;
97
+ }
98
+
99
+ function basename(value: string): string {
100
+ return value.split(/[\\/]/).pop() ?? value;
101
+ }
102
+
103
+ function resolvePackageBinPath(
104
+ packageJsonPath: string,
105
+ manifest: PackageManifest,
106
+ binName: string,
107
+ ): string | undefined {
108
+ const { bin } = manifest;
109
+ const relativeBinPath =
110
+ typeof bin === "string"
111
+ ? bin
112
+ : bin && typeof bin === "object"
113
+ ? (bin as Record<string, unknown>)[binName]
114
+ : undefined;
115
+ if (typeof relativeBinPath !== "string" || relativeBinPath.trim() === "") {
116
+ return undefined;
117
+ }
118
+ return path.resolve(path.dirname(packageJsonPath), relativeBinPath);
119
+ }
120
+
121
+ async function resolveInstalledAcpPackageBinPath(
122
+ packageName: string,
123
+ binName: string,
124
+ ): Promise<string | undefined> {
125
+ try {
126
+ const packageJsonPath = requireFromHere.resolve(`${packageName}/package.json`);
127
+ const { value: manifest } = await readJsonFileWithFallback<PackageManifest>(
128
+ packageJsonPath,
129
+ {},
130
+ );
131
+ if (manifest.name !== packageName) {
132
+ return undefined;
133
+ }
134
+ const binPath = resolvePackageBinPath(packageJsonPath, manifest, binName);
135
+ if (!binPath) {
136
+ return undefined;
137
+ }
138
+ await fs.access(binPath);
139
+ return binPath;
140
+ } catch {
141
+ return undefined;
142
+ }
143
+ }
144
+
145
+ async function resolveInstalledCodexAcpBinPath(): Promise<string | undefined> {
146
+ // Keep Klaw's isolated CODEX_HOME wrapper, but launch the plugin-local
147
+ // Codex ACP adapter when the package dependency is available.
148
+ return await resolveInstalledAcpPackageBinPath(CODEX_ACP_PACKAGE, CODEX_ACP_BIN);
149
+ }
150
+
151
+ async function resolveInstalledClaudeAcpBinPath(): Promise<string | undefined> {
152
+ return await resolveInstalledAcpPackageBinPath(CLAUDE_ACP_PACKAGE, CLAUDE_ACP_BIN);
153
+ }
154
+
155
+ type DiagnosticRedactionRuleSpec = {
156
+ source: string;
157
+ flags: string;
158
+ replacement: string;
159
+ };
160
+
161
+ const DIAGNOSTIC_REDACTION_RULES: DiagnosticRedactionRuleSpec[] = [
162
+ {
163
+ source: String.raw`(authorization\s*[:=]\s*bearer\s+)[^\s'"<>]+`,
164
+ flags: "gi",
165
+ replacement: "$1[REDACTED]",
166
+ },
167
+ {
168
+ source: String.raw`((?:api[_-]?key|apiKey|access[_-]?token|refresh[_-]?token|client[_-]?secret|token|secret|password|passwd|credential)\s*[:=]\s*)[^\s'"<>]+`,
169
+ flags: "gi",
170
+ replacement: "$1[REDACTED]",
171
+ },
172
+ {
173
+ source: String.raw`("(?:apiKey|token|secret|password|passwd|accessToken|refreshToken)"\s*:\s*")[^"]+`,
174
+ flags: "g",
175
+ replacement: "$1[REDACTED]",
176
+ },
177
+ {
178
+ source: String.raw`(["']?(?:api[-_]?key|apiKey|access[-_]?token|accessToken|refresh[-_]?token|refreshToken|id[-_]?token|idToken|auth[-_]?token|authToken|client[-_]?secret|clientSecret|app[-_]?secret|appSecret|token|secret|password|passwd|credential)["']?\s*[:=]\s*["']?)[^"',}\s<>]+`,
179
+ flags: "gi",
180
+ replacement: "$1[REDACTED]",
181
+ },
182
+ {
183
+ source: String.raw`([?&](?:access[-_]?token|auth[-_]?token|refresh[-_]?token|api[-_]?key|client[-_]?secret|token|key|secret|password|pass|passwd|auth|signature)=)[^&\s'"<>]+`,
184
+ flags: "gi",
185
+ replacement: "$1[REDACTED]",
186
+ },
187
+ {
188
+ source: String.raw`(--(?:api[-_]?key|token|secret|password|passwd)\s+)[^\s'"]+`,
189
+ flags: "gi",
190
+ replacement: "$1[REDACTED]",
191
+ },
192
+ {
193
+ source:
194
+ String.raw`-----BEGIN [A-Z ]*PRI` +
195
+ String.raw`VATE KEY-----[\s\S]+?-----END [A-Z ]*PRI` +
196
+ String.raw`VATE KEY-----`,
197
+ flags: "g",
198
+ replacement: "[REDACTED_PRIVATE_KEY]",
199
+ },
200
+ {
201
+ source: String.raw`\b(sk-[A-Za-z0-9_-]{8,})\b`,
202
+ flags: "g",
203
+ replacement: "[REDACTED_OPENAI_KEY]",
204
+ },
205
+ {
206
+ source: String.raw`\b(gh[pousr]_[A-Za-z0-9_]{20,})\b`,
207
+ flags: "g",
208
+ replacement: "[REDACTED_GITHUB_TOKEN]",
209
+ },
210
+ {
211
+ source: String.raw`\b(github_pat_[A-Za-z0-9_]{20,})\b`,
212
+ flags: "g",
213
+ replacement: "[REDACTED_GITHUB_TOKEN]",
214
+ },
215
+ {
216
+ source: String.raw`\b(xox[baprs]-[A-Za-z0-9-]{10,})\b`,
217
+ flags: "g",
218
+ replacement: "[REDACTED_SLACK_TOKEN]",
219
+ },
220
+ {
221
+ source: String.raw`\b(gsk_[A-Za-z0-9_-]{10,})\b`,
222
+ flags: "g",
223
+ replacement: "[REDACTED_API_KEY]",
224
+ },
225
+ {
226
+ source: String.raw`\b(AIza[0-9A-Za-z\-_]{20,})\b`,
227
+ flags: "g",
228
+ replacement: "[REDACTED_GOOGLE_KEY]",
229
+ },
230
+ {
231
+ source: String.raw`\b(ya29\.[0-9A-Za-z_\-./+=]{10,})\b`,
232
+ flags: "g",
233
+ replacement: "[REDACTED_GOOGLE_TOKEN]",
234
+ },
235
+ {
236
+ source: String.raw`\b(eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b`,
237
+ flags: "g",
238
+ replacement: "[REDACTED_JWT]",
239
+ },
240
+ {
241
+ source: String.raw`\b(pplx-[A-Za-z0-9_-]{10,})\b`,
242
+ flags: "g",
243
+ replacement: "[REDACTED_API_KEY]",
244
+ },
245
+ {
246
+ source: String.raw`\b(npm_[A-Za-z0-9]{10,})\b`,
247
+ flags: "g",
248
+ replacement: "[REDACTED_NPM_TOKEN]",
249
+ },
250
+ {
251
+ source: String.raw`\b(LTAI[A-Za-z0-9]{10,})\b`,
252
+ flags: "g",
253
+ replacement: "[REDACTED_ACCESS_KEY]",
254
+ },
255
+ { source: String.raw`\b(hf_[A-Za-z0-9]{10,})\b`, flags: "g", replacement: "[REDACTED_API_KEY]" },
256
+ {
257
+ source: String.raw`\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b`,
258
+ flags: "g",
259
+ replacement: "bot[REDACTED_TELEGRAM_TOKEN]",
260
+ },
261
+ {
262
+ source: String.raw`\b(\d{6,}:[A-Za-z0-9_-]{20,})\b`,
263
+ flags: "g",
264
+ replacement: "[REDACTED_TELEGRAM_TOKEN]",
265
+ },
266
+ ];
267
+
268
+ function renderDiagnosticRedactionRuleSpecs(): string {
269
+ return JSON.stringify(DIAGNOSTIC_REDACTION_RULES);
270
+ }
271
+
272
+ function buildAdapterWrapperScript(params: {
273
+ displayName: string;
274
+ packageSpec: string;
275
+ binName: string;
276
+ installedBinPath?: string;
277
+ envSetup: string;
278
+ stderrLogFileNamePrefix?: string;
279
+ }): string {
280
+ return `#!/usr/bin/env node
281
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
282
+ import path from "node:path";
283
+ import { spawn } from "node:child_process";
284
+ import { fileURLToPath } from "node:url";
285
+
286
+ ${params.envSetup}
287
+ const stderrLogFileNamePrefix = ${params.stderrLogFileNamePrefix ? JSON.stringify(params.stderrLogFileNamePrefix) : "undefined"};
288
+ const stderrLogMaxChars = 256 * 1024;
289
+
290
+ const openClawWrapperArgs = new Set([
291
+ ${quoteCommandPart(KLAW_ACPX_LEASE_ID_ARG)},
292
+ ${quoteCommandPart(KLAW_GATEWAY_INSTANCE_ID_ARG)},
293
+ ]);
294
+
295
+ function readKlawWrapperArg(args, name) {
296
+ const index = args.indexOf(name);
297
+ if (index < 0) {
298
+ return undefined;
299
+ }
300
+ const value = args[index + 1];
301
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
302
+ }
303
+
304
+ function safeDiagnosticFilePart(value) {
305
+ const sanitized = String(value || "").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120);
306
+ return sanitized || "pid-" + process.pid;
307
+ }
308
+
309
+ function resolveStderrLogPath(args) {
310
+ if (!stderrLogFileNamePrefix) {
311
+ return undefined;
312
+ }
313
+ const leaseId =
314
+ process.env[${JSON.stringify(KLAW_ACPX_LEASE_ID_ENV)}] ||
315
+ readKlawWrapperArg(args, ${quoteCommandPart(KLAW_ACPX_LEASE_ID_ARG)}) ||
316
+ "pid-" + process.pid;
317
+ const fileName = stderrLogFileNamePrefix + "." + safeDiagnosticFilePart(leaseId) + ".log";
318
+ return fileURLToPath(new URL("./" + fileName, import.meta.url));
319
+ }
320
+
321
+ const diagnosticRedactionRules = ${renderDiagnosticRedactionRuleSpecs()}.map((rule) => [
322
+ new RegExp(rule.source, rule.flags),
323
+ rule.replacement,
324
+ ]);
325
+
326
+ function redactDiagnosticText(text) {
327
+ let redacted = text;
328
+ for (const [pattern, replacement] of diagnosticRedactionRules) {
329
+ redacted = redacted.replace(pattern, replacement);
330
+ }
331
+ return redacted;
332
+ }
333
+
334
+ let pendingStderrLogText = "";
335
+ const stderrPrivateKeyEndPattern = /-----END [A-Z ]*PRIVATE KEY-----/;
336
+
337
+ function hasUnclosedPrivateKeyBlock(text) {
338
+ let lastBeginIndex = -1;
339
+ for (const match of text.matchAll(/-----BEGIN [A-Z ]*PRIVATE KEY-----/g)) {
340
+ lastBeginIndex = match.index ?? lastBeginIndex;
341
+ }
342
+ if (lastBeginIndex === -1) {
343
+ return -1;
344
+ }
345
+ return stderrPrivateKeyEndPattern.test(text.slice(lastBeginIndex)) ? -1 : lastBeginIndex;
346
+ }
347
+
348
+ function writeRedactedStderrLog(text) {
349
+ if (!stderrLogPath) {
350
+ return;
351
+ }
352
+ if (!text) {
353
+ return;
354
+ }
355
+ try {
356
+ appendFileSync(stderrLogPath, redactDiagnosticText(text), "utf8");
357
+ const current = readFileSync(stderrLogPath, "utf8");
358
+ if (current.length > stderrLogMaxChars) {
359
+ writeFileSync(stderrLogPath, current.slice(-stderrLogMaxChars), "utf8");
360
+ }
361
+ } catch {
362
+ // Stderr capture is diagnostic-only; never break the ACP adapter.
363
+ }
364
+ }
365
+
366
+ function redactIncompletePrivateKeyTail(text) {
367
+ const unclosedPrivateKeyStart = hasUnclosedPrivateKeyBlock(text);
368
+ if (unclosedPrivateKeyStart === -1) {
369
+ return text;
370
+ }
371
+ return text.slice(0, unclosedPrivateKeyStart) + "[REDACTED_PRIVATE_KEY]";
372
+ }
373
+
374
+ function flushFinalizedStderrLogText() {
375
+ const lastLineBreak = pendingStderrLogText.lastIndexOf("\\n");
376
+ if (lastLineBreak === -1) {
377
+ if (pendingStderrLogText.length > stderrLogMaxChars) {
378
+ pendingStderrLogText = pendingStderrLogText.slice(-stderrLogMaxChars);
379
+ }
380
+ return;
381
+ }
382
+ let flushEnd = lastLineBreak + 1;
383
+ const unclosedPrivateKeyStart = hasUnclosedPrivateKeyBlock(
384
+ pendingStderrLogText.slice(0, flushEnd),
385
+ );
386
+ if (unclosedPrivateKeyStart !== -1) {
387
+ flushEnd = unclosedPrivateKeyStart;
388
+ }
389
+ if (flushEnd <= 0) {
390
+ if (pendingStderrLogText.length > stderrLogMaxChars) {
391
+ pendingStderrLogText = pendingStderrLogText.slice(-stderrLogMaxChars);
392
+ }
393
+ return;
394
+ }
395
+ const finalizedText = pendingStderrLogText.slice(0, flushEnd);
396
+ pendingStderrLogText = pendingStderrLogText.slice(flushEnd);
397
+ writeRedactedStderrLog(finalizedText);
398
+ }
399
+
400
+ function appendStderrLog(chunk) {
401
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
402
+ if (!text) {
403
+ return;
404
+ }
405
+ pendingStderrLogText += text;
406
+ flushFinalizedStderrLogText();
407
+ }
408
+
409
+ function finishStderrLog() {
410
+ const text = redactIncompletePrivateKeyTail(pendingStderrLogText);
411
+ pendingStderrLogText = "";
412
+ writeRedactedStderrLog(text);
413
+ }
414
+
415
+ function stripKlawWrapperArgs(args) {
416
+ const stripped = [];
417
+ for (let index = 0; index < args.length; index += 1) {
418
+ const value = args[index];
419
+ if (openClawWrapperArgs.has(value)) {
420
+ index += 1;
421
+ continue;
422
+ }
423
+ stripped.push(value);
424
+ }
425
+ return stripped;
426
+ }
427
+
428
+ const rawConfiguredArgs = process.argv.slice(2);
429
+ const stderrLogPath = resolveStderrLogPath(rawConfiguredArgs);
430
+
431
+ try {
432
+ if (stderrLogPath) {
433
+ writeFileSync(stderrLogPath, "", "utf8");
434
+ }
435
+ } catch {
436
+ // Stderr capture is diagnostic-only; never break the ACP adapter.
437
+ }
438
+
439
+ const configuredArgs = stripKlawWrapperArgs(rawConfiguredArgs);
440
+
441
+ function resolveNpmCliPath() {
442
+ const candidate = path.resolve(
443
+ path.dirname(process.execPath),
444
+ "..",
445
+ "lib",
446
+ "node_modules",
447
+ "npm",
448
+ "bin",
449
+ "npm-cli.js",
450
+ );
451
+ return existsSync(candidate) ? candidate : undefined;
452
+ }
453
+
454
+ const npmCliPath = resolveNpmCliPath();
455
+ const installedBinPath = ${params.installedBinPath ? quoteCommandPart(params.installedBinPath) : "undefined"};
456
+ let defaultCommand;
457
+ let defaultArgs;
458
+ if (installedBinPath) {
459
+ defaultCommand = process.execPath;
460
+ defaultArgs = [installedBinPath];
461
+ } else if (npmCliPath) {
462
+ defaultCommand = process.execPath;
463
+ defaultArgs = [npmCliPath, "exec", "--yes", "--package", "${params.packageSpec}", "--", "${params.binName}"];
464
+ } else {
465
+ defaultCommand = process.platform === "win32" ? "npx.cmd" : "npx";
466
+ defaultArgs = ["--yes", "--package", "${params.packageSpec}", "--", "${params.binName}"];
467
+ }
468
+ const command =
469
+ configuredArgs[0] === "${RUN_CONFIGURED_COMMAND_SENTINEL}" ? configuredArgs[1] : defaultCommand;
470
+ const args =
471
+ configuredArgs[0] === "${RUN_CONFIGURED_COMMAND_SENTINEL}"
472
+ ? configuredArgs.slice(2)
473
+ : [...defaultArgs, ...configuredArgs];
474
+
475
+ if (!command) {
476
+ console.error("[klaw] missing configured ${params.displayName} ACP command");
477
+ process.exit(1);
478
+ }
479
+
480
+ const child = spawn(command, args, {
481
+ detached: process.platform !== "win32",
482
+ env,
483
+ stdio: ["inherit", "inherit", "pipe"],
484
+ windowsHide: true,
485
+ });
486
+
487
+ child.stderr?.on("data", (chunk) => {
488
+ appendStderrLog(chunk);
489
+ process.stderr.write(chunk);
490
+ });
491
+
492
+ let forceKillTimer;
493
+ let orphanCleanupStarted = false;
494
+ let childExitCode = 1;
495
+
496
+ function killChildTree(signal, options = {}) {
497
+ if (!child.pid || (!options.force && child.killed)) {
498
+ return;
499
+ }
500
+ if (process.platform !== "win32") {
501
+ try {
502
+ // The adapter can spawn grandchildren; signaling the process group keeps
503
+ // the generated wrapper from leaving an ACP tree behind.
504
+ process.kill(-child.pid, signal);
505
+ return;
506
+ } catch {
507
+ // Fall back to direct child signaling below.
508
+ }
509
+ }
510
+ child.kill(signal);
511
+ }
512
+
513
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
514
+ process.once(signal, () => {
515
+ killChildTree(signal);
516
+ });
517
+ }
518
+
519
+ const originalParentPid = process.ppid;
520
+ const parentWatcher =
521
+ process.platform === "win32"
522
+ ? undefined
523
+ : setInterval(() => {
524
+ if (process.ppid === originalParentPid || process.ppid !== 1) {
525
+ return;
526
+ }
527
+ if (orphanCleanupStarted) {
528
+ return;
529
+ }
530
+ orphanCleanupStarted = true;
531
+ if (parentWatcher) {
532
+ clearInterval(parentWatcher);
533
+ }
534
+ killChildTree("SIGTERM");
535
+ // Keep the wrapper alive long enough for stubborn adapters to receive
536
+ // a forced fallback signal after SIGTERM.
537
+ forceKillTimer = setTimeout(() => {
538
+ killChildTree("SIGKILL", { force: true });
539
+ childExitCode = 1;
540
+ }, 1_500);
541
+ }, 1_000);
542
+ parentWatcher?.unref?.();
543
+
544
+ child.on("error", (error) => {
545
+ console.error(\`[klaw] failed to launch ${params.displayName} ACP wrapper: \${error.message}\`);
546
+ process.exit(1);
547
+ });
548
+
549
+ child.on("exit", (code, signal) => {
550
+ if (parentWatcher) {
551
+ clearInterval(parentWatcher);
552
+ }
553
+ if (orphanCleanupStarted) {
554
+ return;
555
+ }
556
+ if (forceKillTimer) {
557
+ clearTimeout(forceKillTimer);
558
+ }
559
+ if (code !== null) {
560
+ childExitCode = code;
561
+ return;
562
+ }
563
+ childExitCode = signal ? 1 : 0;
564
+ });
565
+
566
+ child.on("close", () => {
567
+ finishStderrLog();
568
+ process.exit(childExitCode);
569
+ });
570
+ `;
571
+ }
572
+
573
+ function buildCodexAcpWrapperScript(installedBinPath?: string): string {
574
+ return buildAdapterWrapperScript({
575
+ displayName: "Codex",
576
+ packageSpec: `${CODEX_ACP_PACKAGE}@${CODEX_ACP_PACKAGE_VERSION}`,
577
+ binName: CODEX_ACP_BIN,
578
+ installedBinPath,
579
+ stderrLogFileNamePrefix: "codex-acp-wrapper.stderr",
580
+ envSetup: `const codexHome = fileURLToPath(new URL("./codex-home/", import.meta.url));
581
+ const env = {
582
+ ...process.env,
583
+ CODEX_HOME: codexHome,
584
+ };`,
585
+ });
586
+ }
587
+
588
+ function buildClaudeAcpWrapperScript(installedBinPath?: string): string {
589
+ return buildAdapterWrapperScript({
590
+ displayName: "Claude",
591
+ // This package is patched in Klaw; fallback must not float to an unpatched newer release.
592
+ packageSpec: `${CLAUDE_ACP_PACKAGE}@${CLAUDE_ACP_PACKAGE_VERSION}`,
593
+ binName: CLAUDE_ACP_BIN,
594
+ installedBinPath,
595
+ envSetup: `const env = {
596
+ ...process.env,
597
+ };`,
598
+ });
599
+ }
600
+
601
+ async function readSourceCodexConfig(codexHome: string): Promise<string | undefined> {
602
+ try {
603
+ return await fs.readFile(path.join(codexHome, "config.toml"), "utf8");
604
+ } catch (error) {
605
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
606
+ return undefined;
607
+ }
608
+ throw error;
609
+ }
610
+ }
611
+
612
+ async function prepareIsolatedCodexHome(params: {
613
+ baseDir: string;
614
+ workspaceDir: string;
615
+ }): Promise<string> {
616
+ const sourceCodexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
617
+ const sourceConfig = await readSourceCodexConfig(sourceCodexHome);
618
+ const trustedProjectPaths = [
619
+ ...(sourceConfig ? extractTrustedCodexProjectPaths(sourceConfig) : []),
620
+ params.workspaceDir,
621
+ ];
622
+ const codexHome = path.join(params.baseDir, "codex-home");
623
+ await fs.mkdir(codexHome, { recursive: true });
624
+ await fs.writeFile(
625
+ path.join(codexHome, "config.toml"),
626
+ renderIsolatedCodexConfig({
627
+ sourceConfigToml: sourceConfig,
628
+ projectPaths: trustedProjectPaths,
629
+ }),
630
+ "utf8",
631
+ );
632
+ return codexHome;
633
+ }
634
+
635
+ async function makeGeneratedWrapperExecutableIfPossible(wrapperPath: string): Promise<void> {
636
+ try {
637
+ await fs.chmod(wrapperPath, 0o755);
638
+ } catch {
639
+ // The wrapper is invoked via `node wrapper.mjs`; executable mode is only a convenience.
640
+ }
641
+ }
642
+
643
+ async function writeCodexAcpWrapper(baseDir: string, installedBinPath?: string): Promise<string> {
644
+ await fs.mkdir(baseDir, { recursive: true });
645
+ const wrapperPath = path.join(baseDir, "codex-acp-wrapper.mjs");
646
+ await fs.writeFile(wrapperPath, buildCodexAcpWrapperScript(installedBinPath), {
647
+ encoding: "utf8",
648
+ });
649
+ await makeGeneratedWrapperExecutableIfPossible(wrapperPath);
650
+ return wrapperPath;
651
+ }
652
+
653
+ async function writeClaudeAcpWrapper(baseDir: string, installedBinPath?: string): Promise<string> {
654
+ await fs.mkdir(baseDir, { recursive: true });
655
+ const wrapperPath = path.join(baseDir, "claude-agent-acp-wrapper.mjs");
656
+ await fs.writeFile(wrapperPath, buildClaudeAcpWrapperScript(installedBinPath), {
657
+ encoding: "utf8",
658
+ });
659
+ await makeGeneratedWrapperExecutableIfPossible(wrapperPath);
660
+ return wrapperPath;
661
+ }
662
+
663
+ function buildWrapperCommand(wrapperPath: string, args: string[] = []): string {
664
+ return [process.execPath, wrapperPath, ...args].map(quoteCommandPart).join(" ");
665
+ }
666
+
667
+ function isAcpPackageSpec(value: string, packageName: string): boolean {
668
+ const escapedPackageName = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
669
+ return new RegExp(`^${escapedPackageName}(?:@.+)?$`, "i").test(value.trim());
670
+ }
671
+
672
+ function isAcpBinName(value: string, binName: string): boolean {
673
+ const commandName = basename(value);
674
+ const escapedBinName = binName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
675
+ return new RegExp(`^${escapedBinName}(?:\\.exe|\\.[cm]?js)?$`, "i").test(commandName);
676
+ }
677
+
678
+ function isPackageRunnerCommand(value: string): boolean {
679
+ return /^(?:npx|npm|pnpm|bunx)(?:\.cmd|\.exe)?$/i.test(basename(value));
680
+ }
681
+
682
+ function extractConfiguredAdapterArgs(params: {
683
+ configuredCommand?: string;
684
+ packageName: string;
685
+ binName: string;
686
+ }): string[] | undefined {
687
+ const trimmedConfiguredCommand = params.configuredCommand?.trim();
688
+ if (!trimmedConfiguredCommand) {
689
+ return [];
690
+ }
691
+ const parts = splitCommandParts(trimmedConfiguredCommand);
692
+ if (!parts.length) {
693
+ return [];
694
+ }
695
+
696
+ const packageIndex = parts.findIndex((part) => isAcpPackageSpec(part, params.packageName));
697
+ if (packageIndex >= 0) {
698
+ if (!isPackageRunnerCommand(parts[0] ?? "")) {
699
+ return undefined;
700
+ }
701
+ const afterPackage = parts.slice(packageIndex + 1);
702
+ if (afterPackage[0] === "--" && isAcpBinName(afterPackage[1] ?? "", params.binName)) {
703
+ return afterPackage.slice(2);
704
+ }
705
+ if (isAcpBinName(afterPackage[0] ?? "", params.binName)) {
706
+ return afterPackage.slice(1);
707
+ }
708
+ return afterPackage[0] === "--" ? afterPackage.slice(1) : afterPackage;
709
+ }
710
+
711
+ if (isAcpBinName(parts[0] ?? "", params.binName)) {
712
+ return parts.slice(1);
713
+ }
714
+ if (basename(parts[0] ?? "") === "node" && isAcpBinName(parts[1] ?? "", params.binName)) {
715
+ return parts.slice(2);
716
+ }
717
+
718
+ return undefined;
719
+ }
720
+
721
+ function buildCodexAcpWrapperCommand(wrapperPath: string, configuredCommand?: string): string {
722
+ const configuredAdapterArgs = extractConfiguredAdapterArgs({
723
+ configuredCommand,
724
+ packageName: CODEX_ACP_PACKAGE,
725
+ binName: CODEX_ACP_BIN,
726
+ });
727
+ if (configuredAdapterArgs) {
728
+ return buildWrapperCommand(wrapperPath, configuredAdapterArgs);
729
+ }
730
+ return buildWrapperCommand(wrapperPath, [
731
+ RUN_CONFIGURED_COMMAND_SENTINEL,
732
+ ...splitCommandParts(configuredCommand?.trim() ?? ""),
733
+ ]);
734
+ }
735
+
736
+ function buildClaudeAcpWrapperCommand(wrapperPath: string, configuredCommand?: string): string {
737
+ const configuredAdapterArgs = extractConfiguredAdapterArgs({
738
+ configuredCommand,
739
+ packageName: CLAUDE_ACP_PACKAGE,
740
+ binName: CLAUDE_ACP_BIN,
741
+ });
742
+ if (configuredAdapterArgs) {
743
+ return buildWrapperCommand(wrapperPath, configuredAdapterArgs);
744
+ }
745
+ return configuredCommand?.trim() || buildWrapperCommand(wrapperPath);
746
+ }
747
+
748
+ export async function prepareAcpxCodexAuthConfig(params: {
749
+ pluginConfig: ResolvedAcpxPluginConfig;
750
+ stateDir: string;
751
+ logger?: unknown;
752
+ resolveInstalledCodexAcpBinPath?: () => Promise<string | undefined>;
753
+ resolveInstalledClaudeAcpBinPath?: () => Promise<string | undefined>;
754
+ }): Promise<ResolvedAcpxPluginConfig> {
755
+ void params.logger;
756
+ const codexBaseDir = path.join(params.stateDir, "acpx");
757
+ await prepareIsolatedCodexHome({
758
+ baseDir: codexBaseDir,
759
+ workspaceDir: params.pluginConfig.cwd,
760
+ });
761
+ const installedCodexBinPath = await (
762
+ params.resolveInstalledCodexAcpBinPath ?? resolveInstalledCodexAcpBinPath
763
+ )();
764
+ const installedClaudeBinPath = await (
765
+ params.resolveInstalledClaudeAcpBinPath ?? resolveInstalledClaudeAcpBinPath
766
+ )();
767
+ const wrapperPath = await writeCodexAcpWrapper(codexBaseDir, installedCodexBinPath);
768
+ const claudeWrapperPath = await writeClaudeAcpWrapper(codexBaseDir, installedClaudeBinPath);
769
+ const configuredCodexCommand = params.pluginConfig.agents.codex;
770
+ const configuredClaudeCommand = params.pluginConfig.agents.claude;
771
+
772
+ return {
773
+ ...params.pluginConfig,
774
+ agents: {
775
+ ...params.pluginConfig.agents,
776
+ codex: buildCodexAcpWrapperCommand(wrapperPath, configuredCodexCommand),
777
+ claude: buildClaudeAcpWrapperCommand(claudeWrapperPath, configuredClaudeCommand),
778
+ },
779
+ };
780
+ }