@wrongstack/acp 0.295.1 → 0.296.3
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/stdio-transport.d.ts.map +1 -1
- package/dist/agent/wrongstack-acp-agent.d.ts +4 -2
- package/dist/agent/wrongstack-acp-agent.d.ts.map +1 -1
- package/dist/agent.js +16 -2
- package/dist/agent.js.map +2 -2
- package/dist/client/acp-message-routing.d.ts +2 -0
- package/dist/client/acp-message-routing.d.ts.map +1 -0
- package/dist/client/acp-session-callbacks.d.ts +12 -0
- package/dist/client/acp-session-callbacks.d.ts.map +1 -0
- package/dist/client/acp-session-content.d.ts +24 -0
- package/dist/client/acp-session-content.d.ts.map +1 -0
- package/dist/client/acp-session-errors.d.ts +13 -0
- package/dist/client/acp-session-errors.d.ts.map +1 -0
- package/dist/client/acp-session-types.d.ts +81 -0
- package/dist/client/acp-session-types.d.ts.map +1 -0
- package/dist/client/acp-session-updates.d.ts +18 -0
- package/dist/client/acp-session-updates.d.ts.map +1 -0
- package/dist/client/acp-session.d.ts +6 -171
- package/dist/client/acp-session.d.ts.map +1 -1
- package/dist/client.js +318 -313
- package/dist/client.js.map +3 -3
- package/dist/index.js +334 -315
- package/dist/index.js.map +3 -3
- package/dist/wrongstack-acp-agent.js +22 -3
- package/dist/wrongstack-acp-agent.js.map +2 -2
- package/package.json +3 -3
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);
|
|
@@ -1203,8 +1206,9 @@ function toolToPriority(tool) {
|
|
|
1203
1206
|
|
|
1204
1207
|
// src/agent/wrongstack-acp-agent.ts
|
|
1205
1208
|
import { createServer } from "node:http";
|
|
1209
|
+
import { isIP } from "node:net";
|
|
1206
1210
|
import { fileURLToPath } from "node:url";
|
|
1207
|
-
import { writeErr as writeErr2 } from "@wrongstack/core/utils";
|
|
1211
|
+
import { expandIPv6, writeErr as writeErr2 } from "@wrongstack/core/utils";
|
|
1208
1212
|
var WrongStackACPServer = class {
|
|
1209
1213
|
transport;
|
|
1210
1214
|
handler;
|
|
@@ -1260,7 +1264,10 @@ var WrongStackACPServer = class {
|
|
|
1260
1264
|
async startHttp(port) {
|
|
1261
1265
|
const host = this.options.host ?? "127.0.0.1";
|
|
1262
1266
|
const handler = this.handler;
|
|
1263
|
-
const authToken = this.options.authToken;
|
|
1267
|
+
const authToken = this.options.authToken?.trim();
|
|
1268
|
+
if (!authToken && !isLoopbackHost(host)) {
|
|
1269
|
+
throw new Error("ACP HTTP transport requires authToken for non-loopback hosts");
|
|
1270
|
+
}
|
|
1264
1271
|
let httpChain = Promise.resolve();
|
|
1265
1272
|
this.httpServer = createServer(async (req, res) => {
|
|
1266
1273
|
if (authToken) {
|
|
@@ -1388,6 +1395,16 @@ function headerValue(value) {
|
|
|
1388
1395
|
function requestPath(value) {
|
|
1389
1396
|
return value ?? "/";
|
|
1390
1397
|
}
|
|
1398
|
+
function isLoopbackHost(host) {
|
|
1399
|
+
const normalized = host.trim().toLowerCase();
|
|
1400
|
+
if (normalized === "localhost") return true;
|
|
1401
|
+
const literal = normalized.startsWith("[") && normalized.endsWith("]") ? normalized.slice(1, -1) : normalized;
|
|
1402
|
+
const version = isIP(literal);
|
|
1403
|
+
if (version === 4) return literal.startsWith("127.");
|
|
1404
|
+
if (version !== 6) return false;
|
|
1405
|
+
const groups = expandIPv6(literal);
|
|
1406
|
+
return groups !== null && groups.slice(0, 7).every((group) => group === 0) && groups[7] === 1;
|
|
1407
|
+
}
|
|
1391
1408
|
async function main() {
|
|
1392
1409
|
const server = new WrongStackACPServer();
|
|
1393
1410
|
await server.start();
|
|
@@ -1401,6 +1418,39 @@ if (isEntrypoint) {
|
|
|
1401
1418
|
});
|
|
1402
1419
|
}
|
|
1403
1420
|
|
|
1421
|
+
// src/client/acp-session-content.ts
|
|
1422
|
+
function textContent(text) {
|
|
1423
|
+
return { type: "text", text };
|
|
1424
|
+
}
|
|
1425
|
+
function imageContent(mimeType, data) {
|
|
1426
|
+
return { type: "image", mimeType, data };
|
|
1427
|
+
}
|
|
1428
|
+
function audioContent(mimeType, data) {
|
|
1429
|
+
return { type: "audio", mimeType, data };
|
|
1430
|
+
}
|
|
1431
|
+
function extractText(block) {
|
|
1432
|
+
if (typeof block !== "object" || block === null) return "";
|
|
1433
|
+
const b = block;
|
|
1434
|
+
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
1435
|
+
if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
|
|
1436
|
+
return b.resource.text;
|
|
1437
|
+
}
|
|
1438
|
+
return "";
|
|
1439
|
+
}
|
|
1440
|
+
function isRecord(v) {
|
|
1441
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1442
|
+
}
|
|
1443
|
+
function emptyRunResult(stopReason) {
|
|
1444
|
+
return {
|
|
1445
|
+
text: "",
|
|
1446
|
+
stopReason,
|
|
1447
|
+
hasText: false,
|
|
1448
|
+
toolCalls: [],
|
|
1449
|
+
diffs: [],
|
|
1450
|
+
thoughts: ""
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1404
1454
|
// src/client/file-server.ts
|
|
1405
1455
|
import { randomBytes } from "node:crypto";
|
|
1406
1456
|
import { realpathSync } from "node:fs";
|
|
@@ -2076,7 +2126,7 @@ function finitePositiveLimit(value, fallback) {
|
|
|
2076
2126
|
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
2077
2127
|
}
|
|
2078
2128
|
|
|
2079
|
-
// src/client/acp-session.ts
|
|
2129
|
+
// src/client/acp-session-errors.ts
|
|
2080
2130
|
var ACPSessionError = class extends Error {
|
|
2081
2131
|
kind;
|
|
2082
2132
|
cause;
|
|
@@ -2090,6 +2140,264 @@ var ACPSessionError = class extends Error {
|
|
|
2090
2140
|
function isJsonRpcError(v) {
|
|
2091
2141
|
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
2092
2142
|
}
|
|
2143
|
+
|
|
2144
|
+
// src/client/acp-session-updates.ts
|
|
2145
|
+
function createSessionScratch() {
|
|
2146
|
+
return { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
|
|
2147
|
+
}
|
|
2148
|
+
function handleAcpSessionUpdate(msg, scratch, emitProgress) {
|
|
2149
|
+
const update = msg.params?.update;
|
|
2150
|
+
if (typeof update !== "object" || update === null) return;
|
|
2151
|
+
const u = update;
|
|
2152
|
+
emitProgress({ type: "raw", update: u });
|
|
2153
|
+
switch (u.sessionUpdate) {
|
|
2154
|
+
case "agent_message_chunk": {
|
|
2155
|
+
const text = extractText(u.content);
|
|
2156
|
+
if (text) {
|
|
2157
|
+
scratch.text += text;
|
|
2158
|
+
emitProgress({ type: "message", text });
|
|
2159
|
+
}
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
case "thought_chunk": {
|
|
2163
|
+
const text = extractText(u.content);
|
|
2164
|
+
if (text) {
|
|
2165
|
+
scratch.thoughts += text;
|
|
2166
|
+
emitProgress({ type: "thought", text });
|
|
2167
|
+
}
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
case "tool_call":
|
|
2171
|
+
case "tool_call_update":
|
|
2172
|
+
captureToolCall(u, u.sessionUpdate === "tool_call", scratch, emitProgress);
|
|
2173
|
+
return;
|
|
2174
|
+
case "plan":
|
|
2175
|
+
if (Array.isArray(u.entries)) {
|
|
2176
|
+
scratch.plan = u.entries;
|
|
2177
|
+
emitProgress({ type: "plan", entries: u.entries });
|
|
2178
|
+
}
|
|
2179
|
+
return;
|
|
2180
|
+
case "usage_update":
|
|
2181
|
+
if (typeof u.used === "number" && typeof u.size === "number") {
|
|
2182
|
+
const usage = {
|
|
2183
|
+
used: u.used,
|
|
2184
|
+
size: u.size,
|
|
2185
|
+
...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
|
|
2186
|
+
};
|
|
2187
|
+
scratch.usage = usage;
|
|
2188
|
+
emitProgress({ type: "usage", usage });
|
|
2189
|
+
}
|
|
2190
|
+
return;
|
|
2191
|
+
case "available_commands_update":
|
|
2192
|
+
case "current_mode_update":
|
|
2193
|
+
case "config_option_update":
|
|
2194
|
+
case "session_info_update":
|
|
2195
|
+
case "user_message_chunk":
|
|
2196
|
+
case "next_edit_suggestions":
|
|
2197
|
+
case "elicitation":
|
|
2198
|
+
return;
|
|
2199
|
+
default:
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
function captureToolCall(u, isNew, scratch, emitProgress) {
|
|
2204
|
+
const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
|
|
2205
|
+
if (!toolCallId) return;
|
|
2206
|
+
const prev = scratch.toolCalls.get(toolCallId);
|
|
2207
|
+
const record = {
|
|
2208
|
+
toolCallId,
|
|
2209
|
+
title: typeof u.title === "string" ? u.title : prev?.title ?? toolCallId,
|
|
2210
|
+
kind: typeof u.kind === "string" ? u.kind : prev?.kind,
|
|
2211
|
+
status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
|
|
2212
|
+
rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
|
|
2213
|
+
rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
|
|
2214
|
+
};
|
|
2215
|
+
scratch.toolCalls.set(toolCallId, record);
|
|
2216
|
+
if (Array.isArray(u.content)) {
|
|
2217
|
+
for (const c of u.content) {
|
|
2218
|
+
if (c && typeof c === "object" && c.type === "diff") {
|
|
2219
|
+
const diff = {
|
|
2220
|
+
path: c.path,
|
|
2221
|
+
oldText: c.oldText,
|
|
2222
|
+
newText: c.newText
|
|
2223
|
+
};
|
|
2224
|
+
scratch.diffs.push(diff);
|
|
2225
|
+
emitProgress({ type: "diff", diff });
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
emitProgress({
|
|
2230
|
+
type: isNew ? "tool_call" : "tool_call_update",
|
|
2231
|
+
toolCall: record
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// src/client/acp-session-callbacks.ts
|
|
2236
|
+
async function handleAcpPermissionRequest(msg, permissionPolicy, sender) {
|
|
2237
|
+
const id = msg.id;
|
|
2238
|
+
if (id === void 0) return;
|
|
2239
|
+
const params = msg.params;
|
|
2240
|
+
const toolCall = params?.toolCall;
|
|
2241
|
+
const options = Array.isArray(params?.options) ? params.options : [];
|
|
2242
|
+
if (!toolCall) {
|
|
2243
|
+
await sender.sendErrorResponse(id, -32602, "toolCall is required");
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
const policyAbort = new AbortController();
|
|
2247
|
+
try {
|
|
2248
|
+
const outcome = await permissionPolicy({
|
|
2249
|
+
toolCall,
|
|
2250
|
+
options,
|
|
2251
|
+
signal: policyAbort.signal
|
|
2252
|
+
});
|
|
2253
|
+
await sender.sendResult(id, { outcome });
|
|
2254
|
+
} catch (err) {
|
|
2255
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2256
|
+
await sender.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender) {
|
|
2260
|
+
const id = msg.id;
|
|
2261
|
+
if (id === void 0) return;
|
|
2262
|
+
const params = msg.params;
|
|
2263
|
+
if (!params?.path) {
|
|
2264
|
+
await sender.sendErrorResponse(id, -32602, "path is required");
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
if (msg.method === "fs/write_text_file") {
|
|
2268
|
+
const allowed = await authorizeAcpCallback(permissionPolicy, {
|
|
2269
|
+
toolCallId: `acp-fs-write-${id}`,
|
|
2270
|
+
title: `Write file: ${params.path}`,
|
|
2271
|
+
kind: "edit",
|
|
2272
|
+
rawInput: { path: params.path, sessionId: params.sessionId }
|
|
2273
|
+
});
|
|
2274
|
+
if (!allowed) {
|
|
2275
|
+
await sender.sendErrorResponse(id, -32602, "filesystem write denied by permission policy");
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
try {
|
|
2280
|
+
if (msg.method === "fs/read_text_file") {
|
|
2281
|
+
const result = await fileServer.readTextFile({
|
|
2282
|
+
sessionId: params.sessionId ?? "",
|
|
2283
|
+
path: params.path
|
|
2284
|
+
});
|
|
2285
|
+
await sender.sendResult(id, result);
|
|
2286
|
+
} else {
|
|
2287
|
+
await fileServer.writeTextFile({
|
|
2288
|
+
sessionId: params.sessionId ?? "",
|
|
2289
|
+
path: params.path,
|
|
2290
|
+
content: params.content ?? ""
|
|
2291
|
+
});
|
|
2292
|
+
await sender.sendResult(id, {});
|
|
2293
|
+
}
|
|
2294
|
+
} catch (err) {
|
|
2295
|
+
const code = err instanceof FsError ? -32602 : -32603;
|
|
2296
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2297
|
+
await sender.sendErrorResponse(id, code, message);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender) {
|
|
2301
|
+
const id = msg.id;
|
|
2302
|
+
if (id === void 0) return;
|
|
2303
|
+
const params = msg.params ?? {};
|
|
2304
|
+
try {
|
|
2305
|
+
switch (msg.method) {
|
|
2306
|
+
case "terminal/create": {
|
|
2307
|
+
const allowed = await authorizeAcpCallback(permissionPolicy, {
|
|
2308
|
+
toolCallId: `acp-terminal-create-${id}`,
|
|
2309
|
+
title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
|
|
2310
|
+
kind: "execute",
|
|
2311
|
+
rawInput: {
|
|
2312
|
+
command: params.command,
|
|
2313
|
+
args: params.args,
|
|
2314
|
+
cwd: params.cwd,
|
|
2315
|
+
sessionId: params.sessionId
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
2318
|
+
if (!allowed) {
|
|
2319
|
+
await sender.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
const createOpts = {
|
|
2323
|
+
sessionId: String(params.sessionId ?? ""),
|
|
2324
|
+
command: String(params.command ?? ""),
|
|
2325
|
+
args: Array.isArray(params.args) ? params.args : []
|
|
2326
|
+
};
|
|
2327
|
+
if (Array.isArray(params.env)) {
|
|
2328
|
+
createOpts.env = params.env;
|
|
2329
|
+
}
|
|
2330
|
+
if (typeof params.cwd === "string") {
|
|
2331
|
+
createOpts.cwd = params.cwd;
|
|
2332
|
+
}
|
|
2333
|
+
if (typeof params.outputByteLimit === "number") {
|
|
2334
|
+
createOpts.outputByteLimit = params.outputByteLimit;
|
|
2335
|
+
}
|
|
2336
|
+
const result = terminalServer.create(createOpts);
|
|
2337
|
+
await sender.sendResult(id, result);
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
case "terminal/output": {
|
|
2341
|
+
const terminalId = String(params.terminalId ?? "");
|
|
2342
|
+
const out = terminalServer.output(terminalId);
|
|
2343
|
+
await sender.sendResult(id, out);
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
case "terminal/wait_for_exit": {
|
|
2347
|
+
const terminalId = String(params.terminalId ?? "");
|
|
2348
|
+
const exit = await terminalServer.waitForExit(terminalId);
|
|
2349
|
+
await sender.sendResult(id, exit);
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
case "terminal/kill": {
|
|
2353
|
+
const terminalId = String(params.terminalId ?? "");
|
|
2354
|
+
terminalServer.kill(terminalId);
|
|
2355
|
+
await sender.sendResult(id, {});
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
case "terminal/release": {
|
|
2359
|
+
const terminalId = String(params.terminalId ?? "");
|
|
2360
|
+
terminalServer.release(terminalId);
|
|
2361
|
+
await sender.sendResult(id, {});
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
default:
|
|
2365
|
+
await sender.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
|
|
2366
|
+
}
|
|
2367
|
+
} catch (err) {
|
|
2368
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2369
|
+
await sender.sendErrorResponse(id, -32603, message);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
async function authorizeAcpCallback(permissionPolicy, partial) {
|
|
2373
|
+
try {
|
|
2374
|
+
const outcome = await permissionPolicy({
|
|
2375
|
+
toolCall: {
|
|
2376
|
+
sessionUpdate: "tool_call_update",
|
|
2377
|
+
toolCallId: partial.toolCallId,
|
|
2378
|
+
title: partial.title,
|
|
2379
|
+
kind: partial.kind,
|
|
2380
|
+
status: "pending",
|
|
2381
|
+
...partial.rawInput ? { rawInput: partial.rawInput } : {}
|
|
2382
|
+
},
|
|
2383
|
+
options: [
|
|
2384
|
+
{ optionId: "allow", name: "Allow", kind: "allow_once" },
|
|
2385
|
+
{ optionId: "reject", name: "Reject", kind: "reject_once" }
|
|
2386
|
+
],
|
|
2387
|
+
signal: new AbortController().signal
|
|
2388
|
+
});
|
|
2389
|
+
return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always";
|
|
2390
|
+
} catch {
|
|
2391
|
+
return false;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
// src/client/acp-message-routing.ts
|
|
2396
|
+
function isBestEffortAckMethod(method) {
|
|
2397
|
+
return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
// src/client/acp-session.ts
|
|
2093
2401
|
var ACPSession = class _ACPSession {
|
|
2094
2402
|
transport;
|
|
2095
2403
|
fileServer;
|
|
@@ -2783,6 +3091,12 @@ var ACPSession = class _ACPSession {
|
|
|
2783
3091
|
error: { code, message }
|
|
2784
3092
|
});
|
|
2785
3093
|
}
|
|
3094
|
+
responseSender() {
|
|
3095
|
+
return {
|
|
3096
|
+
sendResult: (id, result) => this.sendResult(id, result),
|
|
3097
|
+
sendErrorResponse: (id, code, message) => this.sendErrorResponse(id, code, message)
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
2786
3100
|
handleMessage(msg) {
|
|
2787
3101
|
if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
|
|
2788
3102
|
const pending = this.pending.get(msg.id);
|
|
@@ -2797,29 +3111,32 @@ var ACPSession = class _ACPSession {
|
|
|
2797
3111
|
return;
|
|
2798
3112
|
}
|
|
2799
3113
|
if (msg.method === "session/update") {
|
|
2800
|
-
this.
|
|
3114
|
+
handleAcpSessionUpdate(msg, this.scratch, (event) => this.emitProgress(event));
|
|
2801
3115
|
return;
|
|
2802
3116
|
}
|
|
2803
3117
|
if (msg.method === "session/request_permission") {
|
|
2804
|
-
void this.
|
|
3118
|
+
void handleAcpPermissionRequest(msg, this.permissionPolicy, this.responseSender());
|
|
2805
3119
|
return;
|
|
2806
3120
|
}
|
|
2807
3121
|
if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
|
|
2808
|
-
void
|
|
3122
|
+
void handleAcpFsRequest(
|
|
3123
|
+
msg,
|
|
3124
|
+
this.fileServer,
|
|
3125
|
+
this.permissionPolicy,
|
|
3126
|
+
this.responseSender()
|
|
3127
|
+
);
|
|
2809
3128
|
return;
|
|
2810
3129
|
}
|
|
2811
3130
|
if (msg.method?.startsWith("terminal/")) {
|
|
2812
|
-
void
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
});
|
|
2819
|
-
}
|
|
3131
|
+
void handleAcpTerminalRequest(
|
|
3132
|
+
msg,
|
|
3133
|
+
this.terminalServer,
|
|
3134
|
+
this.permissionPolicy,
|
|
3135
|
+
this.responseSender()
|
|
3136
|
+
);
|
|
2820
3137
|
return;
|
|
2821
3138
|
}
|
|
2822
|
-
if (msg.method
|
|
3139
|
+
if (isBestEffortAckMethod(msg.method)) {
|
|
2823
3140
|
if (msg.id !== void 0) {
|
|
2824
3141
|
this.sendResult(msg.id, {}).catch(() => {
|
|
2825
3142
|
});
|
|
@@ -2840,98 +3157,6 @@ var ACPSession = class _ACPSession {
|
|
|
2840
3157
|
);
|
|
2841
3158
|
}
|
|
2842
3159
|
}
|
|
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
3160
|
emitProgress(event) {
|
|
2936
3161
|
if (!this.progressHandler) return;
|
|
2937
3162
|
try {
|
|
@@ -2942,217 +3167,11 @@ var ACPSession = class _ACPSession {
|
|
|
2942
3167
|
/** Live progress handler installed for the duration of a `prompt()` turn. */
|
|
2943
3168
|
progressHandler = null;
|
|
2944
3169
|
// Per-prompt scratch state
|
|
2945
|
-
scratch =
|
|
3170
|
+
scratch = createSessionScratch();
|
|
2946
3171
|
resetScratch() {
|
|
2947
|
-
this.scratch =
|
|
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
|
-
}
|
|
3172
|
+
this.scratch = createSessionScratch();
|
|
3123
3173
|
}
|
|
3124
3174
|
};
|
|
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
3175
|
|
|
3157
3176
|
// src/client/tool-translator.ts
|
|
3158
3177
|
import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
|