@kodelyth/acpx 2026.5.39 → 2026.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ import { createAcpxRuntimeService } from "./register.runtime.js";
2
+ import { tryDispatchAcpReplyHook } from "klaw/plugin-sdk/acp-runtime-backend";
3
+ //#region extensions/acpx/index.ts
4
+ const plugin = {
5
+ id: "acpx",
6
+ name: "ACPX Runtime",
7
+ description: "Embedded ACP runtime backend with plugin-owned session and transport management.",
8
+ register(api) {
9
+ api.registerService(createAcpxRuntimeService({ pluginConfig: api.pluginConfig }));
10
+ api.on("reply_dispatch", tryDispatchAcpReplyHook);
11
+ }
12
+ };
13
+ //#endregion
14
+ export { plugin as default };
@@ -0,0 +1,370 @@
1
+ import { createRequire } from "node:module";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { readJsonFileWithFallback, writeJsonFileAtomically } from "klaw/plugin-sdk/json-store";
6
+ import { execFile } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ //#region extensions/acpx/src/process-lease.ts
9
+ const KLAW_ACPX_LEASE_ID_ENV = "KLAW_ACPX_LEASE_ID";
10
+ const KLAW_GATEWAY_INSTANCE_ID_ENV = "KLAW_GATEWAY_INSTANCE_ID";
11
+ const KLAW_ACPX_LEASE_ID_ARG = "--klaw-acpx-lease-id";
12
+ const KLAW_GATEWAY_INSTANCE_ID_ARG = "--klaw-gateway-instance-id";
13
+ const LEASE_FILE = "process-leases.json";
14
+ function normalizeLease(value) {
15
+ if (typeof value !== "object" || value === null) return;
16
+ const record = value;
17
+ if (typeof record.leaseId !== "string" || typeof record.gatewayInstanceId !== "string" || typeof record.sessionKey !== "string" || typeof record.wrapperRoot !== "string" || typeof record.wrapperPath !== "string" || typeof record.rootPid !== "number" || typeof record.commandHash !== "string" || typeof record.startedAt !== "number" || ![
18
+ "open",
19
+ "closing",
20
+ "closed",
21
+ "lost"
22
+ ].includes(String(record.state))) return;
23
+ return {
24
+ leaseId: record.leaseId,
25
+ gatewayInstanceId: record.gatewayInstanceId,
26
+ sessionKey: record.sessionKey,
27
+ wrapperRoot: record.wrapperRoot,
28
+ wrapperPath: record.wrapperPath,
29
+ rootPid: record.rootPid,
30
+ ...typeof record.processGroupId === "number" ? { processGroupId: record.processGroupId } : {},
31
+ commandHash: record.commandHash,
32
+ startedAt: record.startedAt,
33
+ state: record.state
34
+ };
35
+ }
36
+ async function readLeaseFile(filePath) {
37
+ const { value } = await readJsonFileWithFallback(filePath, {
38
+ version: 1,
39
+ leases: []
40
+ });
41
+ return {
42
+ version: 1,
43
+ leases: Array.isArray(value.leases) ? value.leases.map(normalizeLease).filter((lease) => !!lease) : []
44
+ };
45
+ }
46
+ function writeLeaseFile(filePath, value) {
47
+ return writeJsonFileAtomically(filePath, value);
48
+ }
49
+ function createAcpxProcessLeaseStore(params) {
50
+ const filePath = path.join(params.stateDir, LEASE_FILE);
51
+ let updateQueue = Promise.resolve();
52
+ async function update(mutator) {
53
+ const run = updateQueue.then(async () => {
54
+ await fs.mkdir(params.stateDir, { recursive: true });
55
+ await writeLeaseFile(filePath, {
56
+ version: 1,
57
+ leases: mutator((await readLeaseFile(filePath)).leases)
58
+ });
59
+ });
60
+ updateQueue = run.catch(() => {});
61
+ await run;
62
+ }
63
+ async function readCurrent() {
64
+ await updateQueue;
65
+ return await readLeaseFile(filePath);
66
+ }
67
+ return {
68
+ async load(leaseId) {
69
+ return (await readCurrent()).leases.find((lease) => lease.leaseId === leaseId);
70
+ },
71
+ async listOpen(gatewayInstanceId) {
72
+ return (await readCurrent()).leases.filter((lease) => (lease.state === "open" || lease.state === "closing") && (!gatewayInstanceId || lease.gatewayInstanceId === gatewayInstanceId));
73
+ },
74
+ async save(lease) {
75
+ await update((leases) => [...leases.filter((entry) => entry.leaseId !== lease.leaseId), lease]);
76
+ },
77
+ async markState(leaseId, state) {
78
+ await update((leases) => leases.map((lease) => lease.leaseId === leaseId ? {
79
+ ...lease,
80
+ state
81
+ } : lease));
82
+ }
83
+ };
84
+ }
85
+ function createAcpxProcessLeaseId() {
86
+ return randomUUID();
87
+ }
88
+ function hashAcpxProcessCommand(command) {
89
+ return createHash("sha256").update(command).digest("hex");
90
+ }
91
+ function quoteEnvValue(value) {
92
+ return /^[A-Za-z0-9_./:=@+-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
93
+ }
94
+ function appendAcpxLeaseArgs(params) {
95
+ return [
96
+ params.command,
97
+ KLAW_ACPX_LEASE_ID_ARG,
98
+ quoteEnvValue(params.leaseId),
99
+ KLAW_GATEWAY_INSTANCE_ID_ARG,
100
+ quoteEnvValue(params.gatewayInstanceId)
101
+ ].join(" ");
102
+ }
103
+ function withAcpxLeaseEnvironment(params) {
104
+ if ((params.platform ?? process.platform) === "win32") return appendAcpxLeaseArgs(params);
105
+ return [
106
+ "env",
107
+ `${KLAW_ACPX_LEASE_ID_ENV}=${quoteEnvValue(params.leaseId)}`,
108
+ `${KLAW_GATEWAY_INSTANCE_ID_ENV}=${quoteEnvValue(params.gatewayInstanceId)}`,
109
+ appendAcpxLeaseArgs(params)
110
+ ].join(" ");
111
+ }
112
+ //#endregion
113
+ //#region extensions/acpx/src/process-reaper.ts
114
+ const execFileAsync = promisify(execFile);
115
+ const requireFromHere = createRequire(import.meta.url);
116
+ const GENERATED_WRAPPER_BASENAMES = new Set(["codex-acp-wrapper.mjs", "claude-agent-acp-wrapper.mjs"]);
117
+ const KLAW_PLUGIN_DEPS_MARKER = "/plugin-runtime-deps/";
118
+ const OWNED_ACP_PACKAGE_NAMES = [
119
+ "@zed-industries/codex-acp",
120
+ "@zed-industries/codex-acp-darwin-arm64",
121
+ "@zed-industries/codex-acp-darwin-x64",
122
+ "@zed-industries/codex-acp-linux-arm64",
123
+ "@zed-industries/codex-acp-linux-x64",
124
+ "@zed-industries/codex-acp-win32-arm64",
125
+ "@zed-industries/codex-acp-win32-x64",
126
+ "@agentclientprotocol/claude-agent-acp",
127
+ "acpx"
128
+ ];
129
+ const ACP_PACKAGE_MARKERS = [
130
+ "/@zed-industries/codex-acp/",
131
+ "/@agentclientprotocol/claude-agent-acp/",
132
+ "/acpx/dist/"
133
+ ];
134
+ function normalizePathLike(value) {
135
+ return value.replaceAll("\\", "/");
136
+ }
137
+ function resolvePackageRoot(packageName) {
138
+ try {
139
+ return normalizePathLike(path.dirname(requireFromHere.resolve(`${packageName}/package.json`)));
140
+ } catch {
141
+ return;
142
+ }
143
+ }
144
+ const OWNED_ACP_PACKAGE_ROOTS = OWNED_ACP_PACKAGE_NAMES.map(resolvePackageRoot).filter((root) => Boolean(root));
145
+ function commandBelongsToResolvedAcpPackage(command) {
146
+ return OWNED_ACP_PACKAGE_ROOTS.some((root) => command.includes(`${root}/`));
147
+ }
148
+ function commandMentionsGeneratedWrapper(command) {
149
+ return Array.from(GENERATED_WRAPPER_BASENAMES).some((basename) => command.includes(basename));
150
+ }
151
+ function commandWrapperBelongsToRoot(command, wrapperRoot) {
152
+ if (!wrapperRoot) return true;
153
+ const normalizedCommand = normalizePathLike(command);
154
+ const normalizedRoot = normalizePathLike(wrapperRoot).replace(/\/+$/, "");
155
+ return Array.from(GENERATED_WRAPPER_BASENAMES).some((basename) => normalizedCommand.includes(`${normalizedRoot}/${basename}`));
156
+ }
157
+ function isKlawLeaseAwareAcpxProcessCommand(params) {
158
+ const command = params.command?.trim();
159
+ if (!command) return false;
160
+ const normalized = normalizePathLike(command);
161
+ return commandMentionsGeneratedWrapper(normalized) && commandWrapperBelongsToRoot(normalized, params.wrapperRoot);
162
+ }
163
+ function commandsReferToSameRootCommand(liveCommand, storedCommand) {
164
+ if (!storedCommand?.trim()) return true;
165
+ return normalizePathLike(liveCommand).trim() === normalizePathLike(storedCommand).trim();
166
+ }
167
+ function splitCommandParts(value) {
168
+ const parts = [];
169
+ let current = "";
170
+ let quote = null;
171
+ let escaping = false;
172
+ for (const ch of value) {
173
+ if (escaping) {
174
+ current += ch;
175
+ escaping = false;
176
+ continue;
177
+ }
178
+ if (ch === "\\" && quote !== "'") {
179
+ escaping = true;
180
+ continue;
181
+ }
182
+ if (quote) {
183
+ if (ch === quote) quote = null;
184
+ else current += ch;
185
+ continue;
186
+ }
187
+ if (ch === "'" || ch === "\"") {
188
+ quote = ch;
189
+ continue;
190
+ }
191
+ if (/\s/.test(ch)) {
192
+ if (current) {
193
+ parts.push(current);
194
+ current = "";
195
+ }
196
+ continue;
197
+ }
198
+ current += ch;
199
+ }
200
+ if (escaping) current += "\\";
201
+ if (current) parts.push(current);
202
+ return parts;
203
+ }
204
+ function commandOptionEquals(parts, option, expected) {
205
+ if (!expected) return true;
206
+ const index = parts.indexOf(option);
207
+ return index >= 0 && parts[index + 1] === expected;
208
+ }
209
+ function liveCommandMatchesLeaseIdentity(params) {
210
+ if (!params.expectedLeaseId && !params.expectedGatewayInstanceId) return true;
211
+ const parts = splitCommandParts(params.command ?? "");
212
+ return commandOptionEquals(parts, "--klaw-acpx-lease-id", params.expectedLeaseId) && commandOptionEquals(parts, "--klaw-gateway-instance-id", params.expectedGatewayInstanceId);
213
+ }
214
+ function isKlawOwnedAcpxProcessCommand(params) {
215
+ const command = params.command?.trim();
216
+ if (!command) return false;
217
+ const normalized = normalizePathLike(command);
218
+ if (isKlawLeaseAwareAcpxProcessCommand({
219
+ command: normalized,
220
+ wrapperRoot: params.wrapperRoot
221
+ })) return true;
222
+ if (commandBelongsToResolvedAcpPackage(normalized)) return true;
223
+ if (!normalized.includes(KLAW_PLUGIN_DEPS_MARKER)) return false;
224
+ return ACP_PACKAGE_MARKERS.some((marker) => normalized.includes(marker));
225
+ }
226
+ function parseProcessList(stdout) {
227
+ const processes = [];
228
+ for (const line of stdout.split(/\r?\n/)) {
229
+ const match = /^\s*(?<pid>\d+)\s+(?<ppid>\d+)\s+(?<command>.+?)\s*$/.exec(line);
230
+ if (!match?.groups) continue;
231
+ processes.push({
232
+ pid: Number.parseInt(match.groups.pid, 10),
233
+ ppid: Number.parseInt(match.groups.ppid, 10),
234
+ command: match.groups.command
235
+ });
236
+ }
237
+ return processes;
238
+ }
239
+ async function listPlatformProcesses() {
240
+ if (process.platform === "win32") return [];
241
+ const { stdout } = await execFileAsync("ps", ["-axo", "pid=,ppid=,command="], { maxBuffer: 8 * 1024 * 1024 });
242
+ return parseProcessList(stdout);
243
+ }
244
+ function collectProcessTree(processes, rootPid) {
245
+ const childrenByParent = /* @__PURE__ */ new Map();
246
+ for (const processInfo of processes) {
247
+ const children = childrenByParent.get(processInfo.ppid) ?? [];
248
+ children.push(processInfo);
249
+ childrenByParent.set(processInfo.ppid, children);
250
+ }
251
+ const root = new Map(processes.map((processInfo) => [processInfo.pid, processInfo])).get(rootPid);
252
+ const collected = [];
253
+ if (root) collected.push(root);
254
+ const queue = [...childrenByParent.get(rootPid) ?? []];
255
+ while (queue.length > 0) {
256
+ const next = queue.shift();
257
+ if (!next || collected.some((processInfo) => processInfo.pid === next.pid)) continue;
258
+ collected.push(next);
259
+ queue.push(...childrenByParent.get(next.pid) ?? []);
260
+ }
261
+ return collected;
262
+ }
263
+ function uniquePids(processes) {
264
+ return Array.from(new Set(processes.map((processInfo) => processInfo.pid).filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid)));
265
+ }
266
+ function isProcessAlive(pid) {
267
+ try {
268
+ process.kill(pid, 0);
269
+ return true;
270
+ } catch {
271
+ return false;
272
+ }
273
+ }
274
+ async function terminatePids(pids, deps) {
275
+ const killProcess = deps?.killProcess ?? ((pid, signal) => process.kill(pid, signal));
276
+ const sleep = deps?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
277
+ const terminated = [];
278
+ for (const pid of pids) try {
279
+ killProcess(pid, "SIGTERM");
280
+ terminated.push(pid);
281
+ } catch {}
282
+ if (terminated.length === 0) return terminated;
283
+ await sleep(750);
284
+ for (const pid of terminated) if (deps?.killProcess || isProcessAlive(pid)) try {
285
+ killProcess(pid, "SIGKILL");
286
+ } catch {}
287
+ return terminated;
288
+ }
289
+ async function cleanupKlawOwnedAcpxProcessTree(params) {
290
+ const rootPid = params.rootPid;
291
+ if (!rootPid || rootPid <= 0 || rootPid === process.pid) return {
292
+ inspectedPids: [],
293
+ terminatedPids: [],
294
+ skippedReason: "missing-root"
295
+ };
296
+ let processes = [];
297
+ try {
298
+ processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
299
+ } catch {
300
+ processes = [];
301
+ }
302
+ const listedTree = collectProcessTree(processes, rootPid);
303
+ if (listedTree.length === 0) return {
304
+ inspectedPids: [],
305
+ terminatedPids: [],
306
+ skippedReason: "unverified-root"
307
+ };
308
+ const rootCommand = listedTree[0]?.command ?? params.rootCommand;
309
+ const liveCommandWasGeneratedWrapper = commandMentionsGeneratedWrapper(normalizePathLike(rootCommand ?? ""));
310
+ const storedCommandWasGeneratedWrapper = commandMentionsGeneratedWrapper(normalizePathLike(params.rootCommand ?? ""));
311
+ if (!liveCommandWasGeneratedWrapper && storedCommandWasGeneratedWrapper) return {
312
+ inspectedPids: listedTree.map((processInfo) => processInfo.pid),
313
+ terminatedPids: [],
314
+ skippedReason: "not-klaw-owned"
315
+ };
316
+ if (!liveCommandWasGeneratedWrapper && !commandsReferToSameRootCommand(rootCommand ?? "", params.rootCommand)) return {
317
+ inspectedPids: listedTree.map((processInfo) => processInfo.pid),
318
+ terminatedPids: [],
319
+ skippedReason: "not-klaw-owned"
320
+ };
321
+ if (!isKlawOwnedAcpxProcessCommand({
322
+ command: rootCommand,
323
+ wrapperRoot: params.wrapperRoot
324
+ })) return {
325
+ inspectedPids: listedTree.map((processInfo) => processInfo.pid),
326
+ terminatedPids: [],
327
+ skippedReason: "not-klaw-owned"
328
+ };
329
+ if (!liveCommandMatchesLeaseIdentity({
330
+ command: rootCommand,
331
+ expectedLeaseId: params.expectedLeaseId,
332
+ expectedGatewayInstanceId: params.expectedGatewayInstanceId
333
+ })) return {
334
+ inspectedPids: listedTree.map((processInfo) => processInfo.pid),
335
+ terminatedPids: [],
336
+ skippedReason: "not-klaw-owned"
337
+ };
338
+ const pids = uniquePids(listedTree.toReversed());
339
+ return {
340
+ inspectedPids: uniquePids(listedTree),
341
+ terminatedPids: await terminatePids(pids, params.deps)
342
+ };
343
+ }
344
+ async function reapStaleKlawOwnedAcpxOrphans(params) {
345
+ if (process.platform === "win32") return {
346
+ inspectedPids: [],
347
+ terminatedPids: [],
348
+ skippedReason: "unsupported-platform"
349
+ };
350
+ let processes;
351
+ try {
352
+ processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
353
+ } catch {
354
+ return {
355
+ inspectedPids: [],
356
+ terminatedPids: [],
357
+ skippedReason: "process-list-unavailable"
358
+ };
359
+ }
360
+ const orphanTrees = processes.filter((processInfo) => processInfo.ppid === 1 && isKlawOwnedAcpxProcessCommand({
361
+ command: processInfo.command,
362
+ wrapperRoot: params.wrapperRoot
363
+ })).map((orphan) => collectProcessTree(processes, orphan.pid));
364
+ return {
365
+ inspectedPids: uniquePids(orphanTrees.flat()),
366
+ terminatedPids: await terminatePids(uniquePids(orphanTrees.flatMap((tree) => tree.toReversed())), params.deps)
367
+ };
368
+ }
369
+ //#endregion
370
+ export { KLAW_ACPX_LEASE_ID_ENV as a, createAcpxProcessLeaseStore as c, KLAW_ACPX_LEASE_ID_ARG as i, hashAcpxProcessCommand as l, isKlawLeaseAwareAcpxProcessCommand as n, KLAW_GATEWAY_INSTANCE_ID_ARG as o, reapStaleKlawOwnedAcpxOrphans as r, createAcpxProcessLeaseId as s, cleanupKlawOwnedAcpxProcessTree as t, withAcpxLeaseEnvironment as u };
@@ -0,0 +1,53 @@
1
+ import { getAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "klaw/plugin-sdk/acp-runtime-backend";
2
+ //#region extensions/acpx/register.runtime.ts
3
+ const ACPX_BACKEND_ID = "acpx";
4
+ let serviceModulePromise = null;
5
+ function loadServiceModule() {
6
+ serviceModulePromise ??= import("./service-CXeUME_-.js");
7
+ return serviceModulePromise;
8
+ }
9
+ async function startRealService(state) {
10
+ if (state.realRuntime) return state.realRuntime;
11
+ if (!state.ctx) throw new Error("ACPX runtime service is not started");
12
+ state.startPromise ??= (async () => {
13
+ const { createAcpxRuntimeService } = await loadServiceModule();
14
+ const service = createAcpxRuntimeService(state.params);
15
+ state.realService = service;
16
+ await service.start(state.ctx);
17
+ const backend = getAcpRuntimeBackend(ACPX_BACKEND_ID);
18
+ if (!backend?.runtime) throw new Error("ACPX runtime service did not register an ACP backend");
19
+ state.realRuntime = backend.runtime;
20
+ return state.realRuntime;
21
+ })();
22
+ return await state.startPromise;
23
+ }
24
+ function createAcpxRuntimeService(params = {}) {
25
+ const state = {
26
+ ctx: null,
27
+ params,
28
+ realRuntime: null,
29
+ realService: null,
30
+ startPromise: null
31
+ };
32
+ return {
33
+ id: "acpx-runtime",
34
+ async start(ctx) {
35
+ if (process.env.KLAW_SKIP_ACPX_RUNTIME === "1") {
36
+ ctx.logger.info("skipping embedded acpx runtime backend (KLAW_SKIP_ACPX_RUNTIME=1)");
37
+ return;
38
+ }
39
+ state.ctx = ctx;
40
+ await startRealService(state);
41
+ },
42
+ async stop(ctx) {
43
+ if (state.realService) await state.realService.stop?.(ctx);
44
+ else unregisterAcpRuntimeBackend(ACPX_BACKEND_ID);
45
+ state.ctx = null;
46
+ state.realRuntime = null;
47
+ state.realService = null;
48
+ state.startPromise = null;
49
+ }
50
+ };
51
+ }
52
+ //#endregion
53
+ export { createAcpxRuntimeService };