@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.
@@ -36,6 +36,8 @@ export interface FileServerOperations {
36
36
  export interface ReadFileParams {
37
37
  sessionId: string;
38
38
  path: string;
39
+ line?: number | undefined;
40
+ limit?: number | undefined;
39
41
  }
40
42
  export interface WriteFileParams {
41
43
  sessionId: string;
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 if (this.handlers.size === 0) {
227
- if (this.messageQueue.length >= this.maxQueuedMessages || this.queuedChars + chars > this.maxQueuedChars) {
228
- writeErr(
229
- `[acp-child message queue exceeds ${this.maxQueuedMessages} entries or ${this.maxQueuedChars} characters]
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
- this.stop();
233
- return;
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
- if (normReal === normRealRoot || normReal.startsWith(normRealRoot + path.sep)) return;
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
- const code = isAbortLikeError(err) ? -32800 : -32603;
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 > ACP_PROTOCOL_VERSION) {
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
- createPromise.then(async (lateId) => {
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
- return this.nextId++;
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.sendResult(msg.id, {}).catch(() => {
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",