@wrongstack/acp 0.295.1 → 0.296.2

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 CHANGED
@@ -974,6 +974,9 @@ var ClientTransport = class {
974
974
  };
975
975
  const waitForMarker = (chunk) => {
976
976
  this.buffer += chunk;
977
+ if (this.buffer.length > this.maxFrameChars) {
978
+ this.buffer = this.buffer.slice(-this.maxFrameChars);
979
+ }
977
980
  const idx = this.buffer.indexOf("[wstack-acp]\n");
978
981
  if (idx !== -1) {
979
982
  this.buffer = this.buffer.slice(idx + "[wstack-acp]\n".length);
@@ -1401,6 +1404,39 @@ if (isEntrypoint) {
1401
1404
  });
1402
1405
  }
1403
1406
 
1407
+ // src/client/acp-session-content.ts
1408
+ function textContent(text) {
1409
+ return { type: "text", text };
1410
+ }
1411
+ function imageContent(mimeType, data) {
1412
+ return { type: "image", mimeType, data };
1413
+ }
1414
+ function audioContent(mimeType, data) {
1415
+ return { type: "audio", mimeType, data };
1416
+ }
1417
+ function extractText(block) {
1418
+ if (typeof block !== "object" || block === null) return "";
1419
+ const b = block;
1420
+ if (b.type === "text" && typeof b.text === "string") return b.text;
1421
+ if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
1422
+ return b.resource.text;
1423
+ }
1424
+ return "";
1425
+ }
1426
+ function isRecord(v) {
1427
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1428
+ }
1429
+ function emptyRunResult(stopReason) {
1430
+ return {
1431
+ text: "",
1432
+ stopReason,
1433
+ hasText: false,
1434
+ toolCalls: [],
1435
+ diffs: [],
1436
+ thoughts: ""
1437
+ };
1438
+ }
1439
+
1404
1440
  // src/client/file-server.ts
1405
1441
  import { randomBytes } from "node:crypto";
1406
1442
  import { realpathSync } from "node:fs";
@@ -2076,7 +2112,7 @@ function finitePositiveLimit(value, fallback) {
2076
2112
  return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
2077
2113
  }
2078
2114
 
2079
- // src/client/acp-session.ts
2115
+ // src/client/acp-session-errors.ts
2080
2116
  var ACPSessionError = class extends Error {
2081
2117
  kind;
2082
2118
  cause;
@@ -2090,6 +2126,264 @@ var ACPSessionError = class extends Error {
2090
2126
  function isJsonRpcError(v) {
2091
2127
  return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
2092
2128
  }
2129
+
2130
+ // src/client/acp-session-updates.ts
2131
+ function createSessionScratch() {
2132
+ return { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
2133
+ }
2134
+ function handleAcpSessionUpdate(msg, scratch, emitProgress) {
2135
+ const update = msg.params?.update;
2136
+ if (typeof update !== "object" || update === null) return;
2137
+ const u = update;
2138
+ emitProgress({ type: "raw", update: u });
2139
+ switch (u.sessionUpdate) {
2140
+ case "agent_message_chunk": {
2141
+ const text = extractText(u.content);
2142
+ if (text) {
2143
+ scratch.text += text;
2144
+ emitProgress({ type: "message", text });
2145
+ }
2146
+ return;
2147
+ }
2148
+ case "thought_chunk": {
2149
+ const text = extractText(u.content);
2150
+ if (text) {
2151
+ scratch.thoughts += text;
2152
+ emitProgress({ type: "thought", text });
2153
+ }
2154
+ return;
2155
+ }
2156
+ case "tool_call":
2157
+ case "tool_call_update":
2158
+ captureToolCall(u, u.sessionUpdate === "tool_call", scratch, emitProgress);
2159
+ return;
2160
+ case "plan":
2161
+ if (Array.isArray(u.entries)) {
2162
+ scratch.plan = u.entries;
2163
+ emitProgress({ type: "plan", entries: u.entries });
2164
+ }
2165
+ return;
2166
+ case "usage_update":
2167
+ if (typeof u.used === "number" && typeof u.size === "number") {
2168
+ const usage = {
2169
+ used: u.used,
2170
+ size: u.size,
2171
+ ...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
2172
+ };
2173
+ scratch.usage = usage;
2174
+ emitProgress({ type: "usage", usage });
2175
+ }
2176
+ return;
2177
+ case "available_commands_update":
2178
+ case "current_mode_update":
2179
+ case "config_option_update":
2180
+ case "session_info_update":
2181
+ case "user_message_chunk":
2182
+ case "next_edit_suggestions":
2183
+ case "elicitation":
2184
+ return;
2185
+ default:
2186
+ return;
2187
+ }
2188
+ }
2189
+ function captureToolCall(u, isNew, scratch, emitProgress) {
2190
+ const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
2191
+ if (!toolCallId) return;
2192
+ const prev = scratch.toolCalls.get(toolCallId);
2193
+ const record = {
2194
+ toolCallId,
2195
+ title: typeof u.title === "string" ? u.title : prev?.title ?? toolCallId,
2196
+ kind: typeof u.kind === "string" ? u.kind : prev?.kind,
2197
+ status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
2198
+ rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
2199
+ rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
2200
+ };
2201
+ scratch.toolCalls.set(toolCallId, record);
2202
+ if (Array.isArray(u.content)) {
2203
+ for (const c of u.content) {
2204
+ if (c && typeof c === "object" && c.type === "diff") {
2205
+ const diff = {
2206
+ path: c.path,
2207
+ oldText: c.oldText,
2208
+ newText: c.newText
2209
+ };
2210
+ scratch.diffs.push(diff);
2211
+ emitProgress({ type: "diff", diff });
2212
+ }
2213
+ }
2214
+ }
2215
+ emitProgress({
2216
+ type: isNew ? "tool_call" : "tool_call_update",
2217
+ toolCall: record
2218
+ });
2219
+ }
2220
+
2221
+ // src/client/acp-session-callbacks.ts
2222
+ async function handleAcpPermissionRequest(msg, permissionPolicy, sender) {
2223
+ const id = msg.id;
2224
+ if (id === void 0) return;
2225
+ const params = msg.params;
2226
+ const toolCall = params?.toolCall;
2227
+ const options = Array.isArray(params?.options) ? params.options : [];
2228
+ if (!toolCall) {
2229
+ await sender.sendErrorResponse(id, -32602, "toolCall is required");
2230
+ return;
2231
+ }
2232
+ const policyAbort = new AbortController();
2233
+ try {
2234
+ const outcome = await permissionPolicy({
2235
+ toolCall,
2236
+ options,
2237
+ signal: policyAbort.signal
2238
+ });
2239
+ await sender.sendResult(id, { outcome });
2240
+ } catch (err) {
2241
+ const message = err instanceof Error ? err.message : String(err);
2242
+ await sender.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
2243
+ }
2244
+ }
2245
+ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender) {
2246
+ const id = msg.id;
2247
+ if (id === void 0) return;
2248
+ const params = msg.params;
2249
+ if (!params?.path) {
2250
+ await sender.sendErrorResponse(id, -32602, "path is required");
2251
+ return;
2252
+ }
2253
+ if (msg.method === "fs/write_text_file") {
2254
+ const allowed = await authorizeAcpCallback(permissionPolicy, {
2255
+ toolCallId: `acp-fs-write-${id}`,
2256
+ title: `Write file: ${params.path}`,
2257
+ kind: "edit",
2258
+ rawInput: { path: params.path, sessionId: params.sessionId }
2259
+ });
2260
+ if (!allowed) {
2261
+ await sender.sendErrorResponse(id, -32602, "filesystem write denied by permission policy");
2262
+ return;
2263
+ }
2264
+ }
2265
+ try {
2266
+ if (msg.method === "fs/read_text_file") {
2267
+ const result = await fileServer.readTextFile({
2268
+ sessionId: params.sessionId ?? "",
2269
+ path: params.path
2270
+ });
2271
+ await sender.sendResult(id, result);
2272
+ } else {
2273
+ await fileServer.writeTextFile({
2274
+ sessionId: params.sessionId ?? "",
2275
+ path: params.path,
2276
+ content: params.content ?? ""
2277
+ });
2278
+ await sender.sendResult(id, {});
2279
+ }
2280
+ } catch (err) {
2281
+ const code = err instanceof FsError ? -32602 : -32603;
2282
+ const message = err instanceof Error ? err.message : String(err);
2283
+ await sender.sendErrorResponse(id, code, message);
2284
+ }
2285
+ }
2286
+ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender) {
2287
+ const id = msg.id;
2288
+ if (id === void 0) return;
2289
+ const params = msg.params ?? {};
2290
+ try {
2291
+ switch (msg.method) {
2292
+ case "terminal/create": {
2293
+ const allowed = await authorizeAcpCallback(permissionPolicy, {
2294
+ toolCallId: `acp-terminal-create-${id}`,
2295
+ title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
2296
+ kind: "execute",
2297
+ rawInput: {
2298
+ command: params.command,
2299
+ args: params.args,
2300
+ cwd: params.cwd,
2301
+ sessionId: params.sessionId
2302
+ }
2303
+ });
2304
+ if (!allowed) {
2305
+ await sender.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
2306
+ return;
2307
+ }
2308
+ const createOpts = {
2309
+ sessionId: String(params.sessionId ?? ""),
2310
+ command: String(params.command ?? ""),
2311
+ args: Array.isArray(params.args) ? params.args : []
2312
+ };
2313
+ if (Array.isArray(params.env)) {
2314
+ createOpts.env = params.env;
2315
+ }
2316
+ if (typeof params.cwd === "string") {
2317
+ createOpts.cwd = params.cwd;
2318
+ }
2319
+ if (typeof params.outputByteLimit === "number") {
2320
+ createOpts.outputByteLimit = params.outputByteLimit;
2321
+ }
2322
+ const result = terminalServer.create(createOpts);
2323
+ await sender.sendResult(id, result);
2324
+ return;
2325
+ }
2326
+ case "terminal/output": {
2327
+ const terminalId = String(params.terminalId ?? "");
2328
+ const out = terminalServer.output(terminalId);
2329
+ await sender.sendResult(id, out);
2330
+ return;
2331
+ }
2332
+ case "terminal/wait_for_exit": {
2333
+ const terminalId = String(params.terminalId ?? "");
2334
+ const exit = await terminalServer.waitForExit(terminalId);
2335
+ await sender.sendResult(id, exit);
2336
+ return;
2337
+ }
2338
+ case "terminal/kill": {
2339
+ const terminalId = String(params.terminalId ?? "");
2340
+ terminalServer.kill(terminalId);
2341
+ await sender.sendResult(id, {});
2342
+ return;
2343
+ }
2344
+ case "terminal/release": {
2345
+ const terminalId = String(params.terminalId ?? "");
2346
+ terminalServer.release(terminalId);
2347
+ await sender.sendResult(id, {});
2348
+ return;
2349
+ }
2350
+ default:
2351
+ await sender.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
2352
+ }
2353
+ } catch (err) {
2354
+ const message = err instanceof Error ? err.message : String(err);
2355
+ await sender.sendErrorResponse(id, -32603, message);
2356
+ }
2357
+ }
2358
+ async function authorizeAcpCallback(permissionPolicy, partial) {
2359
+ try {
2360
+ const outcome = await permissionPolicy({
2361
+ toolCall: {
2362
+ sessionUpdate: "tool_call_update",
2363
+ toolCallId: partial.toolCallId,
2364
+ title: partial.title,
2365
+ kind: partial.kind,
2366
+ status: "pending",
2367
+ ...partial.rawInput ? { rawInput: partial.rawInput } : {}
2368
+ },
2369
+ options: [
2370
+ { optionId: "allow", name: "Allow", kind: "allow_once" },
2371
+ { optionId: "reject", name: "Reject", kind: "reject_once" }
2372
+ ],
2373
+ signal: new AbortController().signal
2374
+ });
2375
+ return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always";
2376
+ } catch {
2377
+ return false;
2378
+ }
2379
+ }
2380
+
2381
+ // src/client/acp-message-routing.ts
2382
+ function isBestEffortAckMethod(method) {
2383
+ return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
2384
+ }
2385
+
2386
+ // src/client/acp-session.ts
2093
2387
  var ACPSession = class _ACPSession {
2094
2388
  transport;
2095
2389
  fileServer;
@@ -2783,6 +3077,12 @@ var ACPSession = class _ACPSession {
2783
3077
  error: { code, message }
2784
3078
  });
2785
3079
  }
3080
+ responseSender() {
3081
+ return {
3082
+ sendResult: (id, result) => this.sendResult(id, result),
3083
+ sendErrorResponse: (id, code, message) => this.sendErrorResponse(id, code, message)
3084
+ };
3085
+ }
2786
3086
  handleMessage(msg) {
2787
3087
  if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
2788
3088
  const pending = this.pending.get(msg.id);
@@ -2797,29 +3097,32 @@ var ACPSession = class _ACPSession {
2797
3097
  return;
2798
3098
  }
2799
3099
  if (msg.method === "session/update") {
2800
- this.handleUpdate(msg);
3100
+ handleAcpSessionUpdate(msg, this.scratch, (event) => this.emitProgress(event));
2801
3101
  return;
2802
3102
  }
2803
3103
  if (msg.method === "session/request_permission") {
2804
- void this.handlePermissionRequest(msg);
3104
+ void handleAcpPermissionRequest(msg, this.permissionPolicy, this.responseSender());
2805
3105
  return;
2806
3106
  }
2807
3107
  if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
2808
- void this.handleFsRequest(msg);
3108
+ void handleAcpFsRequest(
3109
+ msg,
3110
+ this.fileServer,
3111
+ this.permissionPolicy,
3112
+ this.responseSender()
3113
+ );
2809
3114
  return;
2810
3115
  }
2811
3116
  if (msg.method?.startsWith("terminal/")) {
2812
- void this.handleTerminalRequest(msg);
2813
- return;
2814
- }
2815
- if (msg.method === "mcp/connect" || msg.method === "mcp/message" || msg.method === "mcp/disconnect") {
2816
- if (msg.id !== void 0) {
2817
- this.sendResult(msg.id, {}).catch(() => {
2818
- });
2819
- }
3117
+ void handleAcpTerminalRequest(
3118
+ msg,
3119
+ this.terminalServer,
3120
+ this.permissionPolicy,
3121
+ this.responseSender()
3122
+ );
2820
3123
  return;
2821
3124
  }
2822
- if (msg.method === "elicitation/create" || msg.method === "elicitation/complete") {
3125
+ if (isBestEffortAckMethod(msg.method)) {
2823
3126
  if (msg.id !== void 0) {
2824
3127
  this.sendResult(msg.id, {}).catch(() => {
2825
3128
  });
@@ -2840,98 +3143,6 @@ var ACPSession = class _ACPSession {
2840
3143
  );
2841
3144
  }
2842
3145
  }
2843
- handleUpdate(msg) {
2844
- const update = msg.params?.update;
2845
- if (typeof update !== "object" || update === null) return;
2846
- const u = update;
2847
- this.emitProgress({ type: "raw", update: u });
2848
- switch (u.sessionUpdate) {
2849
- case "agent_message_chunk": {
2850
- const text = extractText(u.content);
2851
- if (text) {
2852
- this.scratch.text += text;
2853
- this.emitProgress({ type: "message", text });
2854
- }
2855
- return;
2856
- }
2857
- case "thought_chunk": {
2858
- const text = extractText(u.content);
2859
- if (text) {
2860
- this.scratch.thoughts += text;
2861
- this.emitProgress({ type: "thought", text });
2862
- }
2863
- return;
2864
- }
2865
- case "tool_call":
2866
- case "tool_call_update": {
2867
- this.captureToolCall(u, u.sessionUpdate === "tool_call");
2868
- return;
2869
- }
2870
- case "plan":
2871
- if (Array.isArray(u.entries)) {
2872
- this.scratch.plan = u.entries;
2873
- this.emitProgress({ type: "plan", entries: u.entries });
2874
- }
2875
- return;
2876
- case "usage_update":
2877
- if (typeof u.used === "number" && typeof u.size === "number") {
2878
- const usage = {
2879
- used: u.used,
2880
- size: u.size,
2881
- ...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
2882
- };
2883
- this.scratch.usage = usage;
2884
- this.emitProgress({ type: "usage", usage });
2885
- }
2886
- return;
2887
- case "available_commands_update":
2888
- case "current_mode_update":
2889
- case "config_option_update":
2890
- case "session_info_update":
2891
- case "user_message_chunk":
2892
- case "next_edit_suggestions":
2893
- case "elicitation":
2894
- return;
2895
- default:
2896
- return;
2897
- }
2898
- }
2899
- /**
2900
- * Fold a `tool_call` / `tool_call_update` notification into the scratch
2901
- * tool-call map (deduped by toolCallId), extract any `diff` content into
2902
- * the diffs list, and emit live progress.
2903
- */
2904
- captureToolCall(u, isNew) {
2905
- const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
2906
- if (!toolCallId) return;
2907
- const prev = this.scratch.toolCalls.get(toolCallId);
2908
- const record = {
2909
- toolCallId,
2910
- title: typeof u.title === "string" ? u.title : prev?.title ?? toolCallId,
2911
- kind: typeof u.kind === "string" ? u.kind : prev?.kind,
2912
- status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
2913
- rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
2914
- rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
2915
- };
2916
- this.scratch.toolCalls.set(toolCallId, record);
2917
- if (Array.isArray(u.content)) {
2918
- for (const c of u.content) {
2919
- if (c && typeof c === "object" && c.type === "diff") {
2920
- const diff = {
2921
- path: c.path,
2922
- oldText: c.oldText,
2923
- newText: c.newText
2924
- };
2925
- this.scratch.diffs.push(diff);
2926
- this.emitProgress({ type: "diff", diff });
2927
- }
2928
- }
2929
- }
2930
- this.emitProgress({
2931
- type: isNew ? "tool_call" : "tool_call_update",
2932
- toolCall: record
2933
- });
2934
- }
2935
3146
  emitProgress(event) {
2936
3147
  if (!this.progressHandler) return;
2937
3148
  try {
@@ -2942,217 +3153,11 @@ var ACPSession = class _ACPSession {
2942
3153
  /** Live progress handler installed for the duration of a `prompt()` turn. */
2943
3154
  progressHandler = null;
2944
3155
  // Per-prompt scratch state
2945
- scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
3156
+ scratch = createSessionScratch();
2946
3157
  resetScratch() {
2947
- this.scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
2948
- }
2949
- async handlePermissionRequest(msg) {
2950
- const id = msg.id;
2951
- if (id === void 0) return;
2952
- const params = msg.params;
2953
- const toolCall = params?.toolCall;
2954
- const options = Array.isArray(params?.options) ? params.options : [];
2955
- if (!toolCall) {
2956
- await this.sendErrorResponse(id, -32602, "toolCall is required");
2957
- return;
2958
- }
2959
- const policyAbort = new AbortController();
2960
- try {
2961
- const outcome = await this.permissionPolicy({
2962
- toolCall,
2963
- options,
2964
- signal: policyAbort.signal
2965
- });
2966
- await this.sendResult(id, { outcome });
2967
- } catch (err) {
2968
- const message = err instanceof Error ? err.message : String(err);
2969
- await this.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
2970
- }
2971
- }
2972
- /**
2973
- * Enforce authorization at privileged callback sinks (fs/write,
2974
- * terminal/create). Unlike `handlePermissionRequest` which responds to
2975
- * agent-initiated `session/request_permission` messages, this method is
2976
- * called by the handler BEFORE dispatching to FileServer/TerminalServer,
2977
- * closing the gap where the agent simply skips the voluntary permission
2978
- * request and sends the privileged callback directly.
2979
- *
2980
- * Uses the session's permission policy. The default
2981
- * (`readOnlyPermissionPolicy`) auto-approves only side-effect-free tool
2982
- * calls (read/search/fetch/think) and rejects everything else — this is
2983
- * the safe-by-default posture. For trusted local agents (CLI `acp spawn`,
2984
- * Director fan-out), inject `defaultPermissionPolicy` to grant
2985
- * write/execute access.
2986
- *
2987
- * Returns true if the callback is authorized, false if denied.
2988
- */
2989
- async authorizeCallback(partial) {
2990
- try {
2991
- const outcome = await this.permissionPolicy({
2992
- toolCall: {
2993
- sessionUpdate: "tool_call_update",
2994
- toolCallId: partial.toolCallId,
2995
- title: partial.title,
2996
- kind: partial.kind,
2997
- status: "pending",
2998
- ...partial.rawInput ? { rawInput: partial.rawInput } : {}
2999
- },
3000
- options: [
3001
- { optionId: "allow", name: "Allow", kind: "allow_once" },
3002
- { optionId: "reject", name: "Reject", kind: "reject_once" }
3003
- ],
3004
- signal: new AbortController().signal
3005
- });
3006
- return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always";
3007
- } catch {
3008
- return false;
3009
- }
3010
- }
3011
- async handleFsRequest(msg) {
3012
- const id = msg.id;
3013
- if (id === void 0) return;
3014
- const params = msg.params;
3015
- if (!params?.path) {
3016
- await this.sendErrorResponse(id, -32602, "path is required");
3017
- return;
3018
- }
3019
- if (msg.method === "fs/write_text_file") {
3020
- const allowed = await this.authorizeCallback({
3021
- toolCallId: `acp-fs-write-${id}`,
3022
- title: `Write file: ${params.path}`,
3023
- kind: "edit",
3024
- rawInput: { path: params.path, sessionId: params.sessionId }
3025
- });
3026
- if (!allowed) {
3027
- await this.sendErrorResponse(id, -32602, "filesystem write denied by permission policy");
3028
- return;
3029
- }
3030
- }
3031
- try {
3032
- if (msg.method === "fs/read_text_file") {
3033
- const result = await this.fileServer.readTextFile({
3034
- sessionId: params.sessionId ?? "",
3035
- path: params.path
3036
- });
3037
- await this.sendResult(id, result);
3038
- } else {
3039
- await this.fileServer.writeTextFile({
3040
- sessionId: params.sessionId ?? "",
3041
- path: params.path,
3042
- content: params.content ?? ""
3043
- });
3044
- await this.sendResult(id, {});
3045
- }
3046
- } catch (err) {
3047
- const code = err instanceof FsError ? -32602 : -32603;
3048
- const message = err instanceof Error ? err.message : String(err);
3049
- await this.sendErrorResponse(id, code, message);
3050
- }
3051
- }
3052
- async handleTerminalRequest(msg) {
3053
- const id = msg.id;
3054
- if (id === void 0) return;
3055
- const params = msg.params ?? {};
3056
- try {
3057
- switch (msg.method) {
3058
- case "terminal/create": {
3059
- const allowed = await this.authorizeCallback({
3060
- toolCallId: `acp-terminal-create-${id}`,
3061
- title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
3062
- kind: "execute",
3063
- rawInput: {
3064
- command: params.command,
3065
- args: params.args,
3066
- cwd: params.cwd,
3067
- sessionId: params.sessionId
3068
- }
3069
- });
3070
- if (!allowed) {
3071
- await this.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
3072
- return;
3073
- }
3074
- const createOpts = {
3075
- sessionId: String(params.sessionId ?? ""),
3076
- command: String(params.command ?? ""),
3077
- args: Array.isArray(params.args) ? params.args : []
3078
- };
3079
- if (Array.isArray(params.env)) {
3080
- createOpts.env = params.env;
3081
- }
3082
- if (typeof params.cwd === "string") {
3083
- createOpts.cwd = params.cwd;
3084
- }
3085
- if (typeof params.outputByteLimit === "number") {
3086
- createOpts.outputByteLimit = params.outputByteLimit;
3087
- }
3088
- const result = this.terminalServer.create(createOpts);
3089
- await this.sendResult(id, result);
3090
- return;
3091
- }
3092
- case "terminal/output": {
3093
- const terminalId = String(params.terminalId ?? "");
3094
- const out = this.terminalServer.output(terminalId);
3095
- await this.sendResult(id, out);
3096
- return;
3097
- }
3098
- case "terminal/wait_for_exit": {
3099
- const terminalId = String(params.terminalId ?? "");
3100
- const exit = await this.terminalServer.waitForExit(terminalId);
3101
- await this.sendResult(id, exit);
3102
- return;
3103
- }
3104
- case "terminal/kill": {
3105
- const terminalId = String(params.terminalId ?? "");
3106
- this.terminalServer.kill(terminalId);
3107
- await this.sendResult(id, {});
3108
- return;
3109
- }
3110
- case "terminal/release": {
3111
- const terminalId = String(params.terminalId ?? "");
3112
- this.terminalServer.release(terminalId);
3113
- await this.sendResult(id, {});
3114
- return;
3115
- }
3116
- default:
3117
- await this.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
3118
- }
3119
- } catch (err) {
3120
- const message = err instanceof Error ? err.message : String(err);
3121
- await this.sendErrorResponse(id, -32603, message);
3122
- }
3158
+ this.scratch = createSessionScratch();
3123
3159
  }
3124
3160
  };
3125
- function textContent(text) {
3126
- return { type: "text", text };
3127
- }
3128
- function imageContent(mimeType, data) {
3129
- return { type: "image", mimeType, data };
3130
- }
3131
- function audioContent(mimeType, data) {
3132
- return { type: "audio", mimeType, data };
3133
- }
3134
- function extractText(block) {
3135
- if (typeof block !== "object" || block === null) return "";
3136
- const b = block;
3137
- if (b.type === "text" && typeof b.text === "string") return b.text;
3138
- if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
3139
- return b.resource.text;
3140
- }
3141
- return "";
3142
- }
3143
- function isRecord(v) {
3144
- return typeof v === "object" && v !== null && !Array.isArray(v);
3145
- }
3146
- function emptyRunResult(stopReason) {
3147
- return {
3148
- text: "",
3149
- stopReason,
3150
- hasText: false,
3151
- toolCalls: [],
3152
- diffs: [],
3153
- thoughts: ""
3154
- };
3155
- }
3156
3161
 
3157
3162
  // src/client/tool-translator.ts
3158
3163
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";