@wrongstack/acp 1.0.17 → 1.0.24
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/agent/protocol-contract.d.ts +13 -0
- package/dist/agent/protocol-handler.d.ts +2 -0
- package/dist/agent/protocol-session-management.d.ts +2 -1
- package/dist/agent/protocol-session-ops.d.ts +1 -4
- package/dist/agent.js +223 -101
- package/dist/client/acp-late-session.d.ts +4 -0
- package/dist/client/acp-request-state.d.ts +9 -0
- package/dist/client/file-server.d.ts +2 -0
- package/dist/client.js +133 -66
- package/dist/index.js +366 -185
- package/dist/types/acp-v1.d.ts +4 -3
- package/dist/wrongstack-acp-agent.js +213 -99
- package/package.json +3 -3
package/dist/client.js
CHANGED
|
@@ -213,6 +213,7 @@ var ClientTransport = class {
|
|
|
213
213
|
this.messageQueue.length = 0;
|
|
214
214
|
this.queuedChars = 0;
|
|
215
215
|
this.handlers.clear();
|
|
216
|
+
this.claimHandlers.clear();
|
|
216
217
|
if (code !== 0 && code !== null) {
|
|
217
218
|
writeErr(`[acp-child exited with code ${code}]
|
|
218
219
|
`);
|
|
@@ -223,17 +224,28 @@ var ClientTransport = class {
|
|
|
223
224
|
const resolve3 = this.resolveRead;
|
|
224
225
|
this.resolveRead = null;
|
|
225
226
|
resolve3(msg);
|
|
226
|
-
} else
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
} else {
|
|
228
|
+
let claimed = false;
|
|
229
|
+
for (const handler of this.claimHandlers) {
|
|
230
|
+
try {
|
|
231
|
+
if (handler(msg)) claimed = true;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
writeErr(`[acp-child handler error] ${err}
|
|
234
|
+
`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (!claimed && this.handlers.size === 0) {
|
|
238
|
+
if (this.messageQueue.length >= this.maxQueuedMessages || this.queuedChars + chars > this.maxQueuedChars) {
|
|
239
|
+
writeErr(
|
|
240
|
+
`[acp-child message queue exceeds ${this.maxQueuedMessages} entries or ${this.maxQueuedChars} characters]
|
|
230
241
|
`
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
242
|
+
);
|
|
243
|
+
this.stop();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
this.messageQueue.push({ message: msg, chars });
|
|
247
|
+
this.queuedChars += chars;
|
|
234
248
|
}
|
|
235
|
-
this.messageQueue.push({ message: msg, chars });
|
|
236
|
-
this.queuedChars += chars;
|
|
237
249
|
}
|
|
238
250
|
for (const handler of this.handlers) {
|
|
239
251
|
try {
|
|
@@ -269,6 +281,55 @@ function readPackageVersion(load = () => require2("../package.json")) {
|
|
|
269
281
|
}
|
|
270
282
|
var ACP_PACKAGE_VERSION = readPackageVersion();
|
|
271
283
|
|
|
284
|
+
// src/client/acp-late-session.ts
|
|
285
|
+
var LATE_CANCEL_SEND_TIMEOUT_MS = 1e4;
|
|
286
|
+
function cancelLateAcpSession(transport, createPromise) {
|
|
287
|
+
createPromise.then(async (lateId) => {
|
|
288
|
+
try {
|
|
289
|
+
await new Promise((resolve3, reject) => {
|
|
290
|
+
const timer = setTimeout(() => {
|
|
291
|
+
reject(new Error("late session/cancel send timed out"));
|
|
292
|
+
}, LATE_CANCEL_SEND_TIMEOUT_MS);
|
|
293
|
+
Promise.resolve(
|
|
294
|
+
transport.send({
|
|
295
|
+
jsonrpc: "2.0",
|
|
296
|
+
method: "session/cancel",
|
|
297
|
+
params: { sessionId: lateId }
|
|
298
|
+
})
|
|
299
|
+
).then(
|
|
300
|
+
() => {
|
|
301
|
+
clearTimeout(timer);
|
|
302
|
+
resolve3();
|
|
303
|
+
},
|
|
304
|
+
(sendErr) => {
|
|
305
|
+
clearTimeout(timer);
|
|
306
|
+
reject(sendErr instanceof Error ? sendErr : new Error(String(sendErr)));
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
} catch (err) {
|
|
311
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
312
|
+
console.warn(
|
|
313
|
+
JSON.stringify({
|
|
314
|
+
level: "warn",
|
|
315
|
+
event: "acp_session.late_cancel_failed",
|
|
316
|
+
sessionId: lateId,
|
|
317
|
+
message
|
|
318
|
+
})
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}).catch((reason) => {
|
|
322
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
323
|
+
console.warn(
|
|
324
|
+
JSON.stringify({
|
|
325
|
+
level: "warn",
|
|
326
|
+
event: "acp_session.late_cancel_failed",
|
|
327
|
+
reason: message
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
272
333
|
// src/client/acp-message-routing.ts
|
|
273
334
|
function isBestEffortAckMethod(method) {
|
|
274
335
|
return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
|
|
@@ -316,6 +377,13 @@ var FileServer = class {
|
|
|
316
377
|
}
|
|
317
378
|
/** Read a text file. Returns the content as a string. */
|
|
318
379
|
async readTextFile(params) {
|
|
380
|
+
if (params.line != null && (!Number.isSafeInteger(params.line) || params.line < 1) || params.limit != null && (!Number.isSafeInteger(params.limit) || params.limit < 0)) {
|
|
381
|
+
throw new FsError(
|
|
382
|
+
"INVALID_PATH",
|
|
383
|
+
params.path,
|
|
384
|
+
"line must be a positive integer and limit a non-negative integer"
|
|
385
|
+
);
|
|
386
|
+
}
|
|
319
387
|
const safe = await this.resolveInside(params.path);
|
|
320
388
|
const controller = new AbortController();
|
|
321
389
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
@@ -334,7 +402,12 @@ var FileServer = class {
|
|
|
334
402
|
encoding: "utf8",
|
|
335
403
|
signal: controller.signal
|
|
336
404
|
});
|
|
337
|
-
return { content };
|
|
405
|
+
if (params.line == null && params.limit == null) return { content };
|
|
406
|
+
const lines = content.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
407
|
+
const start = (params.line ?? 1) - 1;
|
|
408
|
+
return {
|
|
409
|
+
content: lines.slice(start, params.limit == null ? void 0 : start + params.limit).join("")
|
|
410
|
+
};
|
|
338
411
|
} catch (err) {
|
|
339
412
|
if (err instanceof FsError) throw err;
|
|
340
413
|
if (controller.signal.aborted) {
|
|
@@ -439,7 +512,8 @@ var FileServer = class {
|
|
|
439
512
|
throw mapFsError(err, resolvedPath);
|
|
440
513
|
}
|
|
441
514
|
const normReal = isWindows ? real.toLowerCase() : real;
|
|
442
|
-
|
|
515
|
+
const realRootWithSep = normRealRoot.endsWith(path.sep) ? normRealRoot : normRealRoot + path.sep;
|
|
516
|
+
if (normReal === normRealRoot || normReal.startsWith(realRootWithSep)) return;
|
|
443
517
|
throw new FsError(
|
|
444
518
|
"OUTSIDE_ROOT",
|
|
445
519
|
resolvedPath,
|
|
@@ -488,9 +562,12 @@ async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbac
|
|
|
488
562
|
);
|
|
489
563
|
await sender.sendResult(id, { outcome });
|
|
490
564
|
} catch (err) {
|
|
565
|
+
if (isAbortLikeError(err)) {
|
|
566
|
+
await sender.sendResult(id, { outcome: { outcome: "cancelled" } });
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
491
569
|
const message = err instanceof Error ? err.message : String(err);
|
|
492
|
-
|
|
493
|
-
await sender.sendErrorResponse(id, code, `permission policy failed: ${message}`);
|
|
570
|
+
await sender.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
|
|
494
571
|
}
|
|
495
572
|
}
|
|
496
573
|
async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, callbackOptions = {}) {
|
|
@@ -526,7 +603,9 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, cal
|
|
|
526
603
|
if (msg.method === "fs/read_text_file") {
|
|
527
604
|
const result = await fileServer.readTextFile({
|
|
528
605
|
sessionId: params.sessionId ?? "",
|
|
529
|
-
path: params.path
|
|
606
|
+
path: params.path,
|
|
607
|
+
line: params.line,
|
|
608
|
+
limit: params.limit
|
|
530
609
|
});
|
|
531
610
|
await sender.sendResult(id, result);
|
|
532
611
|
} else {
|
|
@@ -776,7 +855,7 @@ async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
|
|
|
776
855
|
"agent does not support session/load (loadSession capability not advertised)"
|
|
777
856
|
);
|
|
778
857
|
}
|
|
779
|
-
if (ctx.sessionId) {
|
|
858
|
+
if (ctx.sessionId && ctx.sessionId !== sessionId) {
|
|
780
859
|
await ctx.closeSession();
|
|
781
860
|
}
|
|
782
861
|
ctx.resetScratch();
|
|
@@ -800,7 +879,7 @@ async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
|
|
|
800
879
|
"agent does not support session/resume (sessionCapabilities.resume not advertised)"
|
|
801
880
|
);
|
|
802
881
|
}
|
|
803
|
-
if (ctx.sessionId) {
|
|
882
|
+
if (ctx.sessionId && ctx.sessionId !== sessionId) {
|
|
804
883
|
await ctx.closeSession();
|
|
805
884
|
}
|
|
806
885
|
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
@@ -978,6 +1057,7 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress) {
|
|
|
978
1057
|
}
|
|
979
1058
|
return;
|
|
980
1059
|
}
|
|
1060
|
+
case "agent_thought_chunk":
|
|
981
1061
|
case "thought_chunk": {
|
|
982
1062
|
const text = extractText(u.content);
|
|
983
1063
|
if (text) {
|
|
@@ -1127,7 +1207,9 @@ var TerminalServer = class {
|
|
|
1127
1207
|
this.instanceId = `term_srv_${randomBytes2(4).toString("hex")}`;
|
|
1128
1208
|
this.maxTerminals = this.clampFiniteInt(opts.maxTerminals, 32);
|
|
1129
1209
|
this.abortSignal = opts.signal;
|
|
1130
|
-
if (opts.signal) {
|
|
1210
|
+
if (opts.signal?.aborted) {
|
|
1211
|
+
this.dispose();
|
|
1212
|
+
} else if (opts.signal) {
|
|
1131
1213
|
opts.signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
1132
1214
|
}
|
|
1133
1215
|
}
|
|
@@ -1641,7 +1723,6 @@ function finitePositiveLimit(value, fallback) {
|
|
|
1641
1723
|
}
|
|
1642
1724
|
|
|
1643
1725
|
// src/client/acp-session.ts
|
|
1644
|
-
var LATE_CANCEL_SEND_TIMEOUT_MS = 1e4;
|
|
1645
1726
|
var ACPSession = class _ACPSession {
|
|
1646
1727
|
transport;
|
|
1647
1728
|
fileServer;
|
|
@@ -1811,7 +1892,7 @@ var ACPSession = class _ACPSession {
|
|
|
1811
1892
|
throw new ACPSessionError("protocol_error", "initialize returned no protocolVersion");
|
|
1812
1893
|
}
|
|
1813
1894
|
const r = result;
|
|
1814
|
-
if (r.protocolVersion
|
|
1895
|
+
if (r.protocolVersion !== ACP_PROTOCOL_VERSION) {
|
|
1815
1896
|
throw new ACPSessionError(
|
|
1816
1897
|
"unsupported_capability",
|
|
1817
1898
|
`agent requires protocolVersion=${r.protocolVersion}, client supports up to ${ACP_PROTOCOL_VERSION}`
|
|
@@ -1847,6 +1928,12 @@ var ACPSession = class _ACPSession {
|
|
|
1847
1928
|
`auth method "${methodId}" not in advertised methods: ${this.authMethods.map((m) => m.id).join(", ")}`
|
|
1848
1929
|
);
|
|
1849
1930
|
}
|
|
1931
|
+
if (this.authMethods.find((m) => m.id === methodId)?.type === "terminal") {
|
|
1932
|
+
throw new ACPSessionError(
|
|
1933
|
+
"auth_failed",
|
|
1934
|
+
"Terminal authentication requires interactive login followed by reconnect; it cannot use authenticate"
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1850
1937
|
const id = this.allocId();
|
|
1851
1938
|
const result = await this.sendRequest(id, "authenticate", { methodId });
|
|
1852
1939
|
if (isJsonRpcError(result)) {
|
|
@@ -1942,6 +2029,16 @@ var ACPSession = class _ACPSession {
|
|
|
1942
2029
|
if (signal.aborted) {
|
|
1943
2030
|
return emptyRunResult("cancelled");
|
|
1944
2031
|
}
|
|
2032
|
+
const caps = this.agentCapabilities.promptCapabilities;
|
|
2033
|
+
for (const block of blocks) {
|
|
2034
|
+
const supported = block.type === "image" ? caps?.image : block.type === "audio" ? caps?.audio : block.type === "resource" ? caps?.embeddedContext : true;
|
|
2035
|
+
if (supported !== true) {
|
|
2036
|
+
throw new ACPSessionError(
|
|
2037
|
+
"unsupported_capability",
|
|
2038
|
+
`agent does not support ${block.type} prompt content`
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
1945
2042
|
let cancelled = false;
|
|
1946
2043
|
this.promptCallbackAbort = new AbortController();
|
|
1947
2044
|
const onAbort = () => {
|
|
@@ -2076,50 +2173,7 @@ var ACPSession = class _ACPSession {
|
|
|
2076
2173
|
* being silently swallowed.
|
|
2077
2174
|
*/
|
|
2078
2175
|
cancelLateSession(createPromise) {
|
|
2079
|
-
|
|
2080
|
-
try {
|
|
2081
|
-
await new Promise((resolve3, reject) => {
|
|
2082
|
-
const timer = setTimeout(() => {
|
|
2083
|
-
reject(new Error("late session/cancel send timed out"));
|
|
2084
|
-
}, LATE_CANCEL_SEND_TIMEOUT_MS);
|
|
2085
|
-
Promise.resolve(
|
|
2086
|
-
this.transport.send({
|
|
2087
|
-
jsonrpc: "2.0",
|
|
2088
|
-
method: "session/cancel",
|
|
2089
|
-
params: { sessionId: lateId }
|
|
2090
|
-
})
|
|
2091
|
-
).then(
|
|
2092
|
-
() => {
|
|
2093
|
-
clearTimeout(timer);
|
|
2094
|
-
resolve3();
|
|
2095
|
-
},
|
|
2096
|
-
(sendErr) => {
|
|
2097
|
-
clearTimeout(timer);
|
|
2098
|
-
reject(sendErr instanceof Error ? sendErr : new Error(String(sendErr)));
|
|
2099
|
-
}
|
|
2100
|
-
);
|
|
2101
|
-
});
|
|
2102
|
-
} catch (err) {
|
|
2103
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2104
|
-
console.warn(
|
|
2105
|
-
JSON.stringify({
|
|
2106
|
-
level: "warn",
|
|
2107
|
-
event: "acp_session.late_cancel_failed",
|
|
2108
|
-
sessionId: lateId,
|
|
2109
|
-
message
|
|
2110
|
-
})
|
|
2111
|
-
);
|
|
2112
|
-
}
|
|
2113
|
-
}).catch((reason) => {
|
|
2114
|
-
const message = reason instanceof Error ? reason.message : String(reason);
|
|
2115
|
-
console.warn(
|
|
2116
|
-
JSON.stringify({
|
|
2117
|
-
level: "warn",
|
|
2118
|
-
event: "acp_session.late_cancel_failed",
|
|
2119
|
-
reason: message
|
|
2120
|
-
})
|
|
2121
|
-
);
|
|
2122
|
-
});
|
|
2176
|
+
cancelLateAcpSession(this.transport, createPromise);
|
|
2123
2177
|
}
|
|
2124
2178
|
async closeSession() {
|
|
2125
2179
|
if (!this.sessionId) return;
|
|
@@ -2166,7 +2220,13 @@ var ACPSession = class _ACPSession {
|
|
|
2166
2220
|
// Wire layer
|
|
2167
2221
|
// ────────────────────────────────────────────────────────────────────
|
|
2168
2222
|
allocId() {
|
|
2169
|
-
|
|
2223
|
+
let candidate = Number.isSafeInteger(this.nextId) && this.nextId >= 1 ? this.nextId : 1;
|
|
2224
|
+
for (let probe = 0; probe <= this.pending.size; probe++) {
|
|
2225
|
+
this.nextId = candidate >= Number.MAX_SAFE_INTEGER ? 1 : candidate + 1;
|
|
2226
|
+
if (!this.pending.has(candidate)) return candidate;
|
|
2227
|
+
candidate = this.nextId;
|
|
2228
|
+
}
|
|
2229
|
+
throw new ACPSessionError("protocol_error", "no JSON-RPC request id is available");
|
|
2170
2230
|
}
|
|
2171
2231
|
async sendRequest(id, method, params, timeoutMs) {
|
|
2172
2232
|
return new Promise((resolve3, reject) => {
|
|
@@ -2234,7 +2294,7 @@ var ACPSession = class _ACPSession {
|
|
|
2234
2294
|
}
|
|
2235
2295
|
const terminal = this.authMethods.find((m) => m.type === "terminal");
|
|
2236
2296
|
const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
|
|
2237
|
-
const setup = setupArgs !== void 0 ? `${this.opts.command} ${setupArgs}` : `${this.opts.command}${this.opts.args?.length ? ` ${this.opts.args.join(" ")}` : ""}`;
|
|
2297
|
+
const setup = setupArgs !== void 0 ? `${this.opts.command}${this.opts.args?.length ? ` ${this.opts.args.join(" ")}` : ""} ${setupArgs}` : `${this.opts.command}${this.opts.args?.length ? ` ${this.opts.args.join(" ")}` : ""}`;
|
|
2238
2298
|
throw new ACPSessionError(
|
|
2239
2299
|
"auth_failed",
|
|
2240
2300
|
`This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
|
|
@@ -2316,7 +2376,7 @@ var ACPSession = class _ACPSession {
|
|
|
2316
2376
|
}
|
|
2317
2377
|
if (isBestEffortAckMethod(msg.method)) {
|
|
2318
2378
|
if (msg.id !== void 0) {
|
|
2319
|
-
this.
|
|
2379
|
+
this.sendErrorResponse(msg.id, -32601, `Unsupported method: ${msg.method}`).catch(() => {
|
|
2320
2380
|
});
|
|
2321
2381
|
}
|
|
2322
2382
|
return;
|
|
@@ -2325,6 +2385,13 @@ var ACPSession = class _ACPSession {
|
|
|
2325
2385
|
return;
|
|
2326
2386
|
}
|
|
2327
2387
|
if (msg.method) {
|
|
2388
|
+
if (msg.id !== void 0) {
|
|
2389
|
+
void this.sendErrorResponse(msg.id, -32601, `Unsupported method: ${msg.method}`).catch(
|
|
2390
|
+
() => {
|
|
2391
|
+
}
|
|
2392
|
+
);
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2328
2395
|
console.warn(
|
|
2329
2396
|
JSON.stringify({
|
|
2330
2397
|
level: "warn",
|