@wrongstack/acp 1.0.24 → 1.0.26

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
@@ -1858,6 +1858,324 @@ function isBestEffortAckMethod(method) {
1858
1858
  return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
1859
1859
  }
1860
1860
 
1861
+ // src/client/acp-session-errors.ts
1862
+ var ACPSessionError = class extends Error {
1863
+ kind;
1864
+ cause;
1865
+ constructor(kind, message, cause) {
1866
+ super(message);
1867
+ this.name = "ACPSessionError";
1868
+ this.kind = kind;
1869
+ this.cause = cause;
1870
+ }
1871
+ };
1872
+ function isJsonRpcError(v) {
1873
+ return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
1874
+ }
1875
+ function isAuthRequiredError(err) {
1876
+ const message = err instanceof Error ? err.message : String(err);
1877
+ if (/auth(_|-)?required|authentication required/i.test(message)) return true;
1878
+ const cause = err instanceof ACPSessionError ? err.cause : err && typeof err === "object" && "cause" in err ? err.cause : err;
1879
+ if (!cause || typeof cause !== "object") return false;
1880
+ const data = cause.data;
1881
+ if (data === "auth_required" || data === "AUTH_REQUIRED") return true;
1882
+ if (data && typeof data === "object") {
1883
+ const d = data;
1884
+ if (d.authRequired === true) return true;
1885
+ if (d.code === "auth_required" || d.code === "AUTH_REQUIRED") return true;
1886
+ }
1887
+ return false;
1888
+ }
1889
+
1890
+ // src/client/acp-session-ops.ts
1891
+ function filterMcpServers(agentCapabilities, servers) {
1892
+ if (!servers || servers.length === 0) return [];
1893
+ const mcpCaps = agentCapabilities.mcpCapabilities ?? {};
1894
+ return servers.filter((s) => {
1895
+ if ("type" in s && s.type === "http") return mcpCaps.http === true;
1896
+ if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
1897
+ return true;
1898
+ });
1899
+ }
1900
+ async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
1901
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1902
+ if (!ctx.agentCapabilities.loadSession) {
1903
+ throw new ACPSessionError(
1904
+ "unsupported_capability",
1905
+ "agent does not support session/load (loadSession capability not advertised)"
1906
+ );
1907
+ }
1908
+ if (ctx.sessionId && ctx.sessionId !== sessionId) {
1909
+ await ctx.closeSession();
1910
+ }
1911
+ ctx.resetScratch();
1912
+ const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
1913
+ const id = ctx.allocId();
1914
+ const result = await ctx.sendRequest(id, "session/load", {
1915
+ sessionId,
1916
+ cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
1917
+ mcpServers: servers
1918
+ });
1919
+ if (isJsonRpcError(result)) {
1920
+ throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
1921
+ }
1922
+ ctx.setSessionId(sessionId);
1923
+ }
1924
+ async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
1925
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1926
+ if (!ctx.agentCapabilities.sessionCapabilities?.resume) {
1927
+ throw new ACPSessionError(
1928
+ "unsupported_capability",
1929
+ "agent does not support session/resume (sessionCapabilities.resume not advertised)"
1930
+ );
1931
+ }
1932
+ if (ctx.sessionId && ctx.sessionId !== sessionId) {
1933
+ await ctx.closeSession();
1934
+ }
1935
+ const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
1936
+ const id = ctx.allocId();
1937
+ const result = await ctx.sendRequest(id, "session/resume", {
1938
+ sessionId,
1939
+ cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
1940
+ mcpServers: servers
1941
+ });
1942
+ if (isJsonRpcError(result)) {
1943
+ throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
1944
+ }
1945
+ ctx.setSessionId(sessionId);
1946
+ }
1947
+ async function executeListSessions(ctx, cursor, cwd) {
1948
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1949
+ if (!ctx.agentCapabilities.sessionCapabilities?.list) {
1950
+ throw new ACPSessionError(
1951
+ "unsupported_capability",
1952
+ "agent does not support session/list (sessionCapabilities.list not advertised)"
1953
+ );
1954
+ }
1955
+ const id = ctx.allocId();
1956
+ const params = {};
1957
+ if (cursor !== void 0) params.cursor = cursor;
1958
+ if (cwd !== void 0) params.cwd = cwd;
1959
+ const result = await ctx.sendRequest(id, "session/list", params);
1960
+ if (isJsonRpcError(result)) {
1961
+ throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
1962
+ }
1963
+ const r = result;
1964
+ return {
1965
+ sessions: r.sessions ?? [],
1966
+ nextCursor: r.nextCursor
1967
+ };
1968
+ }
1969
+ async function executeDeleteSession(ctx, sessionId) {
1970
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1971
+ if (!ctx.agentCapabilities.sessionCapabilities?.delete) {
1972
+ throw new ACPSessionError(
1973
+ "unsupported_capability",
1974
+ "agent does not support session/delete (sessionCapabilities.delete not advertised)"
1975
+ );
1976
+ }
1977
+ const id = ctx.allocId();
1978
+ const result = await ctx.sendRequest(id, "session/delete", { sessionId });
1979
+ if (isJsonRpcError(result)) {
1980
+ throw new ACPSessionError("prompt_failed", `session/delete failed: ${result.message}`, result);
1981
+ }
1982
+ if (ctx.sessionId === sessionId) {
1983
+ ctx.setSessionId(null);
1984
+ }
1985
+ }
1986
+ async function executeForkSession(ctx, sourceSessionId, cwd, mcpServers) {
1987
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1988
+ const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
1989
+ const id = ctx.allocId();
1990
+ const result = await ctx.sendRequest(id, "session/fork", {
1991
+ sessionId: sourceSessionId,
1992
+ cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
1993
+ ...servers.length > 0 ? { mcpServers: servers } : {}
1994
+ });
1995
+ if (isJsonRpcError(result)) {
1996
+ throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
1997
+ }
1998
+ const newId = result.sessionId;
1999
+ if (typeof newId !== "string" || !newId) {
2000
+ throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
2001
+ }
2002
+ return newId;
2003
+ }
2004
+ async function executeSetMode(ctx, sessionId, modeId) {
2005
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2006
+ const id = ctx.allocId();
2007
+ const result = await ctx.sendRequest(id, "session/set_mode", { sessionId, modeId });
2008
+ if (isJsonRpcError(result)) {
2009
+ throw new ACPSessionError(
2010
+ "prompt_failed",
2011
+ `session/set_mode failed: ${result.message}`,
2012
+ result
2013
+ );
2014
+ }
2015
+ }
2016
+ async function executeSetConfigOption(ctx, sessionId, configId, value) {
2017
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2018
+ const id = ctx.allocId();
2019
+ const result = await ctx.sendRequest(id, "session/set_config_option", {
2020
+ sessionId,
2021
+ configId,
2022
+ value
2023
+ });
2024
+ if (isJsonRpcError(result)) {
2025
+ throw new ACPSessionError(
2026
+ "prompt_failed",
2027
+ `session/set_config_option failed: ${result.message}`,
2028
+ result
2029
+ );
2030
+ }
2031
+ }
2032
+ async function executeListProviders(ctx) {
2033
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2034
+ const id = ctx.allocId();
2035
+ const result = await ctx.sendRequest(id, "providers/list", {});
2036
+ if (isJsonRpcError(result)) {
2037
+ throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
2038
+ }
2039
+ const r = result;
2040
+ return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
2041
+ }
2042
+ async function executeSetProvider(ctx, providerId, config) {
2043
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2044
+ const id = ctx.allocId();
2045
+ const result = await ctx.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
2046
+ if (isJsonRpcError(result)) {
2047
+ throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
2048
+ }
2049
+ }
2050
+ async function executeDisableProvider(ctx) {
2051
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2052
+ const id = ctx.allocId();
2053
+ const result = await ctx.sendRequest(id, "providers/disable", {});
2054
+ if (isJsonRpcError(result)) {
2055
+ throw new ACPSessionError(
2056
+ "prompt_failed",
2057
+ `providers/disable failed: ${result.message}`,
2058
+ result
2059
+ );
2060
+ }
2061
+ }
2062
+ async function executeMcpMessage(ctx, connectionId, message) {
2063
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2064
+ const id = ctx.allocId();
2065
+ const result = await ctx.sendRequest(id, "mcp/message", { connectionId, message });
2066
+ if (isJsonRpcError(result)) {
2067
+ throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
2068
+ }
2069
+ return result;
2070
+ }
2071
+ async function executeCreateSession(ctx) {
2072
+ const servers = filterMcpServers(ctx.agentCapabilities, ctx.opts.mcpServers);
2073
+ const id = ctx.allocId();
2074
+ const result = await ctx.sendRequest(id, "session/new", {
2075
+ cwd: ctx.opts.cwd ?? ctx.opts.projectRoot,
2076
+ mcpServers: servers
2077
+ });
2078
+ if (isJsonRpcError(result)) {
2079
+ throw new ACPSessionError(
2080
+ "session_create_failed",
2081
+ `session/new failed: ${result.message}`,
2082
+ result
2083
+ );
2084
+ }
2085
+ const sessionId = result.sessionId;
2086
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
2087
+ throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
2088
+ }
2089
+ return sessionId;
2090
+ }
2091
+
2092
+ // src/client/acp-session-auth.ts
2093
+ async function authenticate(host, methodId) {
2094
+ if (host.state === "closed") {
2095
+ throw new ACPSessionError("closed", "session is closed");
2096
+ }
2097
+ if (host.state !== "ready" && host.state !== "authenticated") {
2098
+ throw new ACPSessionError(
2099
+ "protocol_error",
2100
+ `authenticate called in state=${host.state} (expected 'ready')`
2101
+ );
2102
+ }
2103
+ if (host.state === "authenticated") return;
2104
+ if (!host.authMethods.some((m) => m.id === methodId)) {
2105
+ throw new ACPSessionError(
2106
+ "auth_failed",
2107
+ `auth method "${methodId}" not in advertised methods: ${host.authMethods.map((m) => m.id).join(", ")}`
2108
+ );
2109
+ }
2110
+ if (host.authMethods.find((m) => m.id === methodId)?.type === "terminal") {
2111
+ throw new ACPSessionError(
2112
+ "auth_failed",
2113
+ "Terminal authentication requires interactive login followed by reconnect; it cannot use authenticate"
2114
+ );
2115
+ }
2116
+ const id = host.allocId();
2117
+ const result = await host.sendRequest(id, "authenticate", { methodId });
2118
+ if (isJsonRpcError(result)) {
2119
+ throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
2120
+ }
2121
+ host.state = "authenticated";
2122
+ }
2123
+ async function logout(host) {
2124
+ if (host.state === "closed") {
2125
+ throw new ACPSessionError("closed", "session is closed");
2126
+ }
2127
+ if (!host.agentCapabilities.auth?.logout) {
2128
+ throw new ACPSessionError(
2129
+ "unsupported_capability",
2130
+ "agent does not support logout (auth.logout capability not advertised)"
2131
+ );
2132
+ }
2133
+ const id = host.allocId();
2134
+ const result = await host.sendRequest(id, "logout", {});
2135
+ if (isJsonRpcError(result)) {
2136
+ throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
2137
+ }
2138
+ host.state = "ready";
2139
+ }
2140
+ async function createSessionWithAuth(host) {
2141
+ try {
2142
+ return await executeCreateSession(host.opContext());
2143
+ } catch (err) {
2144
+ if (host.state === "authenticated" || !isAuthRequiredError(err)) {
2145
+ throw err instanceof ACPSessionError ? err : new ACPSessionError(
2146
+ "session_create_failed",
2147
+ err instanceof Error ? err.message : String(err),
2148
+ err
2149
+ );
2150
+ }
2151
+ await host.ensureAuthenticated();
2152
+ return executeCreateSession(host.opContext());
2153
+ }
2154
+ }
2155
+ async function ensureAuthenticated(host) {
2156
+ if (host.state === "authenticated") return;
2157
+ if (host.authMethods.length === 0) {
2158
+ throw new ACPSessionError(
2159
+ "auth_failed",
2160
+ "This agent requires authentication before a session can start, but advertised no authMethods. Log into the CLI, then retry."
2161
+ );
2162
+ }
2163
+ const inProcess = host.authMethods.find(
2164
+ (m) => m.type === void 0 || m.type === "agent" || m.type === "oauth" || m.type === "http"
2165
+ );
2166
+ if (inProcess) {
2167
+ await host.authenticate(inProcess.id);
2168
+ return;
2169
+ }
2170
+ const terminal = host.authMethods.find((m) => m.type === "terminal");
2171
+ const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
2172
+ const setup = setupArgs !== void 0 ? `${host.opts.command}${host.opts.args?.length ? ` ${host.opts.args.join(" ")}` : ""} ${setupArgs}` : `${host.opts.command}${host.opts.args?.length ? ` ${host.opts.args.join(" ")}` : ""}`;
2173
+ throw new ACPSessionError(
2174
+ "auth_failed",
2175
+ `This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
2176
+ );
2177
+ }
2178
+
1861
2179
  // src/client/file-server.ts
1862
2180
  import { randomBytes } from "node:crypto";
1863
2181
  import { realpathSync } from "node:fs";
@@ -2215,351 +2533,120 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
2215
2533
  case "terminal/release": {
2216
2534
  const terminalId = String(params.terminalId ?? "");
2217
2535
  terminalServer.release(terminalId);
2218
- await sender.sendResult(id, {});
2219
- return;
2220
- }
2221
- default:
2222
- await sender.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
2223
- }
2224
- } catch (err) {
2225
- const message = err instanceof Error ? err.message : String(err);
2226
- await sender.sendErrorResponse(id, -32603, message);
2227
- }
2228
- }
2229
- async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions) {
2230
- try {
2231
- const outcome = await runPermissionWithDeadline(
2232
- permissionPolicy,
2233
- {
2234
- toolCall: {
2235
- sessionUpdate: "tool_call_update",
2236
- toolCallId: partial.toolCallId,
2237
- title: partial.title,
2238
- kind: partial.kind,
2239
- status: "pending",
2240
- rawInput: partial.rawInput
2241
- },
2242
- options: [
2243
- { optionId: "allow", name: "Allow", kind: "allow_once" },
2244
- { optionId: "reject", name: "Reject", kind: "reject_once" }
2245
- ]
2246
- },
2247
- callbackOptions
2248
- );
2249
- return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always" ? "allowed" : "denied";
2250
- } catch (err) {
2251
- return isAbortLikeError(err) ? "cancelled" : "denied";
2252
- }
2253
- }
2254
- async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions) {
2255
- const timeoutMs = resolvePermissionDeadline(callbackOptions.permissionTimeoutMs);
2256
- const controller = new AbortController();
2257
- const abort = () => controller.abort();
2258
- const timer = timeoutMs === null ? null : setTimeout(abort, timeoutMs);
2259
- let removeAbort;
2260
- if (callbackOptions.signal) {
2261
- if (callbackOptions.signal.aborted) {
2262
- abort();
2263
- } else {
2264
- callbackOptions.signal.addEventListener("abort", abort, { once: true });
2265
- removeAbort = () => callbackOptions.signal?.removeEventListener("abort", abort);
2266
- }
2267
- }
2268
- try {
2269
- return await Promise.race([
2270
- permissionPolicy({
2271
- toolCall: call.toolCall,
2272
- options: call.options,
2273
- signal: controller.signal
2274
- }),
2275
- rejectOnAbort(controller.signal)
2276
- ]);
2277
- } finally {
2278
- if (timer !== null) clearTimeout(timer);
2279
- removeAbort?.();
2280
- }
2281
- }
2282
- function rejectOnAbort(signal) {
2283
- return new Promise((_, reject) => {
2284
- const rejectAbort = () => reject(new Error("permission request cancelled or timed out"));
2285
- if (signal.aborted) {
2286
- rejectAbort();
2287
- return;
2288
- }
2289
- signal.addEventListener("abort", rejectAbort, { once: true });
2290
- });
2291
- }
2292
- function isAbortLikeError(err) {
2293
- return err instanceof Error && /cancelled|canceled|timed out|aborted/i.test(err.message);
2294
- }
2295
- function resolvePermissionDeadline(value) {
2296
- if (value === Number.POSITIVE_INFINITY) return null;
2297
- if (value !== void 0 && Number.isFinite(value) && value > 0) return Math.trunc(value);
2298
- return DEFAULT_PERMISSION_TIMEOUT_MS;
2299
- }
2300
-
2301
- // src/client/acp-session-content.ts
2302
- function textContent(text) {
2303
- return { type: "text", text };
2304
- }
2305
- function imageContent(mimeType, data) {
2306
- return { type: "image", mimeType, data };
2307
- }
2308
- function audioContent(mimeType, data) {
2309
- return { type: "audio", mimeType, data };
2310
- }
2311
- function extractText(block) {
2312
- if (typeof block !== "object" || block === null) return "";
2313
- const b = block;
2314
- if (b.type === "text" && typeof b.text === "string") return b.text;
2315
- if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
2316
- return b.resource.text;
2317
- }
2318
- return "";
2319
- }
2320
- function isRecord(v) {
2321
- return typeof v === "object" && v !== null && !Array.isArray(v);
2322
- }
2323
- function emptyRunResult(stopReason) {
2324
- return {
2325
- text: "",
2326
- stopReason,
2327
- hasText: false,
2328
- toolCalls: [],
2329
- diffs: [],
2330
- thoughts: ""
2331
- };
2332
- }
2333
-
2334
- // src/client/acp-session-errors.ts
2335
- var ACPSessionError = class extends Error {
2336
- kind;
2337
- cause;
2338
- constructor(kind, message, cause) {
2339
- super(message);
2340
- this.name = "ACPSessionError";
2341
- this.kind = kind;
2342
- this.cause = cause;
2343
- }
2344
- };
2345
- function isJsonRpcError(v) {
2346
- return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
2347
- }
2348
- function isAuthRequiredError(err) {
2349
- const message = err instanceof Error ? err.message : String(err);
2350
- if (/auth(_|-)?required|authentication required/i.test(message)) return true;
2351
- const cause = err instanceof ACPSessionError ? err.cause : err && typeof err === "object" && "cause" in err ? err.cause : err;
2352
- if (!cause || typeof cause !== "object") return false;
2353
- const data = cause.data;
2354
- if (data === "auth_required" || data === "AUTH_REQUIRED") return true;
2355
- if (data && typeof data === "object") {
2356
- const d = data;
2357
- if (d.authRequired === true) return true;
2358
- if (d.code === "auth_required" || d.code === "AUTH_REQUIRED") return true;
2359
- }
2360
- return false;
2361
- }
2362
-
2363
- // src/client/acp-session-ops.ts
2364
- function filterMcpServers(agentCapabilities, servers) {
2365
- if (!servers || servers.length === 0) return [];
2366
- const mcpCaps = agentCapabilities.mcpCapabilities ?? {};
2367
- return servers.filter((s) => {
2368
- if ("type" in s && s.type === "http") return mcpCaps.http === true;
2369
- if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
2370
- return true;
2371
- });
2372
- }
2373
- async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
2374
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2375
- if (!ctx.agentCapabilities.loadSession) {
2376
- throw new ACPSessionError(
2377
- "unsupported_capability",
2378
- "agent does not support session/load (loadSession capability not advertised)"
2379
- );
2380
- }
2381
- if (ctx.sessionId && ctx.sessionId !== sessionId) {
2382
- await ctx.closeSession();
2383
- }
2384
- ctx.resetScratch();
2385
- const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
2386
- const id = ctx.allocId();
2387
- const result = await ctx.sendRequest(id, "session/load", {
2388
- sessionId,
2389
- cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
2390
- mcpServers: servers
2391
- });
2392
- if (isJsonRpcError(result)) {
2393
- throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
2394
- }
2395
- ctx.setSessionId(sessionId);
2396
- }
2397
- async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
2398
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2399
- if (!ctx.agentCapabilities.sessionCapabilities?.resume) {
2400
- throw new ACPSessionError(
2401
- "unsupported_capability",
2402
- "agent does not support session/resume (sessionCapabilities.resume not advertised)"
2403
- );
2404
- }
2405
- if (ctx.sessionId && ctx.sessionId !== sessionId) {
2406
- await ctx.closeSession();
2407
- }
2408
- const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
2409
- const id = ctx.allocId();
2410
- const result = await ctx.sendRequest(id, "session/resume", {
2411
- sessionId,
2412
- cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
2413
- mcpServers: servers
2414
- });
2415
- if (isJsonRpcError(result)) {
2416
- throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
2417
- }
2418
- ctx.setSessionId(sessionId);
2419
- }
2420
- async function executeListSessions(ctx, cursor, cwd) {
2421
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2422
- if (!ctx.agentCapabilities.sessionCapabilities?.list) {
2423
- throw new ACPSessionError(
2424
- "unsupported_capability",
2425
- "agent does not support session/list (sessionCapabilities.list not advertised)"
2426
- );
2427
- }
2428
- const id = ctx.allocId();
2429
- const params = {};
2430
- if (cursor !== void 0) params.cursor = cursor;
2431
- if (cwd !== void 0) params.cwd = cwd;
2432
- const result = await ctx.sendRequest(id, "session/list", params);
2433
- if (isJsonRpcError(result)) {
2434
- throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
2536
+ await sender.sendResult(id, {});
2537
+ return;
2538
+ }
2539
+ default:
2540
+ await sender.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
2541
+ }
2542
+ } catch (err) {
2543
+ const message = err instanceof Error ? err.message : String(err);
2544
+ await sender.sendErrorResponse(id, -32603, message);
2435
2545
  }
2436
- const r = result;
2437
- return {
2438
- sessions: r.sessions ?? [],
2439
- nextCursor: r.nextCursor
2440
- };
2441
2546
  }
2442
- async function executeDeleteSession(ctx, sessionId) {
2443
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2444
- if (!ctx.agentCapabilities.sessionCapabilities?.delete) {
2445
- throw new ACPSessionError(
2446
- "unsupported_capability",
2447
- "agent does not support session/delete (sessionCapabilities.delete not advertised)"
2547
+ async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions) {
2548
+ try {
2549
+ const outcome = await runPermissionWithDeadline(
2550
+ permissionPolicy,
2551
+ {
2552
+ toolCall: {
2553
+ sessionUpdate: "tool_call_update",
2554
+ toolCallId: partial.toolCallId,
2555
+ title: partial.title,
2556
+ kind: partial.kind,
2557
+ status: "pending",
2558
+ rawInput: partial.rawInput
2559
+ },
2560
+ options: [
2561
+ { optionId: "allow", name: "Allow", kind: "allow_once" },
2562
+ { optionId: "reject", name: "Reject", kind: "reject_once" }
2563
+ ]
2564
+ },
2565
+ callbackOptions
2448
2566
  );
2567
+ return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always" ? "allowed" : "denied";
2568
+ } catch (err) {
2569
+ return isAbortLikeError(err) ? "cancelled" : "denied";
2449
2570
  }
2450
- const id = ctx.allocId();
2451
- const result = await ctx.sendRequest(id, "session/delete", { sessionId });
2452
- if (isJsonRpcError(result)) {
2453
- throw new ACPSessionError("prompt_failed", `session/delete failed: ${result.message}`, result);
2571
+ }
2572
+ async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions) {
2573
+ const timeoutMs = resolvePermissionDeadline(callbackOptions.permissionTimeoutMs);
2574
+ const controller = new AbortController();
2575
+ const abort = () => controller.abort();
2576
+ const timer = timeoutMs === null ? null : setTimeout(abort, timeoutMs);
2577
+ let removeAbort;
2578
+ if (callbackOptions.signal) {
2579
+ if (callbackOptions.signal.aborted) {
2580
+ abort();
2581
+ } else {
2582
+ callbackOptions.signal.addEventListener("abort", abort, { once: true });
2583
+ removeAbort = () => callbackOptions.signal?.removeEventListener("abort", abort);
2584
+ }
2454
2585
  }
2455
- if (ctx.sessionId === sessionId) {
2456
- ctx.setSessionId(null);
2586
+ try {
2587
+ return await Promise.race([
2588
+ permissionPolicy({
2589
+ toolCall: call.toolCall,
2590
+ options: call.options,
2591
+ signal: controller.signal
2592
+ }),
2593
+ rejectOnAbort(controller.signal)
2594
+ ]);
2595
+ } finally {
2596
+ if (timer !== null) clearTimeout(timer);
2597
+ removeAbort?.();
2457
2598
  }
2458
2599
  }
2459
- async function executeForkSession(ctx, sourceSessionId, cwd, mcpServers) {
2460
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2461
- const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
2462
- const id = ctx.allocId();
2463
- const result = await ctx.sendRequest(id, "session/fork", {
2464
- sessionId: sourceSessionId,
2465
- cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
2466
- ...servers.length > 0 ? { mcpServers: servers } : {}
2600
+ function rejectOnAbort(signal) {
2601
+ return new Promise((_, reject) => {
2602
+ const rejectAbort = () => reject(new Error("permission request cancelled or timed out"));
2603
+ if (signal.aborted) {
2604
+ rejectAbort();
2605
+ return;
2606
+ }
2607
+ signal.addEventListener("abort", rejectAbort, { once: true });
2467
2608
  });
2468
- if (isJsonRpcError(result)) {
2469
- throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
2470
- }
2471
- const newId = result.sessionId;
2472
- if (typeof newId !== "string" || !newId) {
2473
- throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
2474
- }
2475
- return newId;
2476
2609
  }
2477
- async function executeSetMode(ctx, sessionId, modeId) {
2478
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2479
- const id = ctx.allocId();
2480
- const result = await ctx.sendRequest(id, "session/set_mode", { sessionId, modeId });
2481
- if (isJsonRpcError(result)) {
2482
- throw new ACPSessionError(
2483
- "prompt_failed",
2484
- `session/set_mode failed: ${result.message}`,
2485
- result
2486
- );
2487
- }
2610
+ function isAbortLikeError(err) {
2611
+ return err instanceof Error && /cancelled|canceled|timed out|aborted/i.test(err.message);
2488
2612
  }
2489
- async function executeSetConfigOption(ctx, sessionId, configId, value) {
2490
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2491
- const id = ctx.allocId();
2492
- const result = await ctx.sendRequest(id, "session/set_config_option", {
2493
- sessionId,
2494
- configId,
2495
- value
2496
- });
2497
- if (isJsonRpcError(result)) {
2498
- throw new ACPSessionError(
2499
- "prompt_failed",
2500
- `session/set_config_option failed: ${result.message}`,
2501
- result
2502
- );
2503
- }
2613
+ function resolvePermissionDeadline(value) {
2614
+ if (value === Number.POSITIVE_INFINITY) return null;
2615
+ if (value !== void 0 && Number.isFinite(value) && value > 0) return Math.trunc(value);
2616
+ return DEFAULT_PERMISSION_TIMEOUT_MS;
2504
2617
  }
2505
- async function executeListProviders(ctx) {
2506
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2507
- const id = ctx.allocId();
2508
- const result = await ctx.sendRequest(id, "providers/list", {});
2509
- if (isJsonRpcError(result)) {
2510
- throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
2511
- }
2512
- const r = result;
2513
- return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
2618
+
2619
+ // src/client/acp-session-content.ts
2620
+ function textContent(text) {
2621
+ return { type: "text", text };
2514
2622
  }
2515
- async function executeSetProvider(ctx, providerId, config) {
2516
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2517
- const id = ctx.allocId();
2518
- const result = await ctx.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
2519
- if (isJsonRpcError(result)) {
2520
- throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
2521
- }
2623
+ function imageContent(mimeType, data) {
2624
+ return { type: "image", mimeType, data };
2522
2625
  }
2523
- async function executeDisableProvider(ctx) {
2524
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2525
- const id = ctx.allocId();
2526
- const result = await ctx.sendRequest(id, "providers/disable", {});
2527
- if (isJsonRpcError(result)) {
2528
- throw new ACPSessionError(
2529
- "prompt_failed",
2530
- `providers/disable failed: ${result.message}`,
2531
- result
2532
- );
2533
- }
2626
+ function audioContent(mimeType, data) {
2627
+ return { type: "audio", mimeType, data };
2534
2628
  }
2535
- async function executeMcpMessage(ctx, connectionId, message) {
2536
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2537
- const id = ctx.allocId();
2538
- const result = await ctx.sendRequest(id, "mcp/message", { connectionId, message });
2539
- if (isJsonRpcError(result)) {
2540
- throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
2629
+ function extractText(block) {
2630
+ if (typeof block !== "object" || block === null) return "";
2631
+ const b = block;
2632
+ if (b.type === "text" && typeof b.text === "string") return b.text;
2633
+ if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
2634
+ return b.resource.text;
2541
2635
  }
2542
- return result;
2636
+ return "";
2543
2637
  }
2544
- async function executeCreateSession(ctx) {
2545
- const servers = filterMcpServers(ctx.agentCapabilities, ctx.opts.mcpServers);
2546
- const id = ctx.allocId();
2547
- const result = await ctx.sendRequest(id, "session/new", {
2548
- cwd: ctx.opts.cwd ?? ctx.opts.projectRoot,
2549
- mcpServers: servers
2550
- });
2551
- if (isJsonRpcError(result)) {
2552
- throw new ACPSessionError(
2553
- "session_create_failed",
2554
- `session/new failed: ${result.message}`,
2555
- result
2556
- );
2557
- }
2558
- const sessionId = result.sessionId;
2559
- if (typeof sessionId !== "string" || sessionId.length === 0) {
2560
- throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
2561
- }
2562
- return sessionId;
2638
+ function isRecord(v) {
2639
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2640
+ }
2641
+ function emptyRunResult(stopReason) {
2642
+ return {
2643
+ text: "",
2644
+ stopReason,
2645
+ hasText: false,
2646
+ toolCalls: [],
2647
+ diffs: [],
2648
+ thoughts: ""
2649
+ };
2563
2650
  }
2564
2651
 
2565
2652
  // src/client/acp-session-updates.ts
@@ -3435,55 +3522,14 @@ var ACPSession = class _ACPSession {
3435
3522
  * Call this AFTER start() and BEFORE any session/new call.
3436
3523
  */
3437
3524
  async authenticate(methodId) {
3438
- if (this.state === "closed") {
3439
- throw new ACPSessionError("closed", "session is closed");
3440
- }
3441
- if (this.state !== "ready" && this.state !== "authenticated") {
3442
- throw new ACPSessionError(
3443
- "protocol_error",
3444
- `authenticate called in state=${this.state} (expected 'ready')`
3445
- );
3446
- }
3447
- if (this.state === "authenticated") return;
3448
- if (!this.authMethods.some((m) => m.id === methodId)) {
3449
- throw new ACPSessionError(
3450
- "auth_failed",
3451
- `auth method "${methodId}" not in advertised methods: ${this.authMethods.map((m) => m.id).join(", ")}`
3452
- );
3453
- }
3454
- if (this.authMethods.find((m) => m.id === methodId)?.type === "terminal") {
3455
- throw new ACPSessionError(
3456
- "auth_failed",
3457
- "Terminal authentication requires interactive login followed by reconnect; it cannot use authenticate"
3458
- );
3459
- }
3460
- const id = this.allocId();
3461
- const result = await this.sendRequest(id, "authenticate", { methodId });
3462
- if (isJsonRpcError(result)) {
3463
- throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
3464
- }
3465
- this.state = "authenticated";
3525
+ return authenticate(this.acpSessionAuthHost(), methodId);
3466
3526
  }
3467
3527
  /**
3468
3528
  * Log out from the current authenticated session.
3469
3529
  * Only callable if the agent advertises `auth.logout` capability.
3470
3530
  */
3471
3531
  async logout() {
3472
- if (this.state === "closed") {
3473
- throw new ACPSessionError("closed", "session is closed");
3474
- }
3475
- if (!this.agentCapabilities.auth?.logout) {
3476
- throw new ACPSessionError(
3477
- "unsupported_capability",
3478
- "agent does not support logout (auth.logout capability not advertised)"
3479
- );
3480
- }
3481
- const id = this.allocId();
3482
- const result = await this.sendRequest(id, "logout", {});
3483
- if (isJsonRpcError(result)) {
3484
- throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
3485
- }
3486
- this.state = "ready";
3532
+ return logout(this.acpSessionAuthHost());
3487
3533
  }
3488
3534
  // ──────────────────────────────────────────────────────────────────────
3489
3535
  // Session management delegation
@@ -3781,19 +3827,7 @@ var ACPSession = class _ACPSession {
3781
3827
  * `authMethods`; we do not pop OAuth on every spawn.
3782
3828
  */
3783
3829
  async createSessionWithAuth() {
3784
- try {
3785
- return await executeCreateSession(this.opContext());
3786
- } catch (err) {
3787
- if (this.state === "authenticated" || !isAuthRequiredError(err)) {
3788
- throw err instanceof ACPSessionError ? err : new ACPSessionError(
3789
- "session_create_failed",
3790
- err instanceof Error ? err.message : String(err),
3791
- err
3792
- );
3793
- }
3794
- await this.ensureAuthenticated();
3795
- return executeCreateSession(this.opContext());
3796
- }
3830
+ return createSessionWithAuth(this.acpSessionAuthHost());
3797
3831
  }
3798
3832
  /**
3799
3833
  * Pick a non-terminal auth method and run `authenticate`. Terminal-only
@@ -3801,27 +3835,7 @@ var ACPSession = class _ACPSession {
3801
3835
  * we refuse rather than hang a TUI inside the JSON-RPC child.
3802
3836
  */
3803
3837
  async ensureAuthenticated() {
3804
- if (this.state === "authenticated") return;
3805
- if (this.authMethods.length === 0) {
3806
- throw new ACPSessionError(
3807
- "auth_failed",
3808
- "This agent requires authentication before a session can start, but advertised no authMethods. Log into the CLI, then retry."
3809
- );
3810
- }
3811
- const inProcess = this.authMethods.find(
3812
- (m) => m.type === void 0 || m.type === "agent" || m.type === "oauth" || m.type === "http"
3813
- );
3814
- if (inProcess) {
3815
- await this.authenticate(inProcess.id);
3816
- return;
3817
- }
3818
- const terminal = this.authMethods.find((m) => m.type === "terminal");
3819
- const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
3820
- 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(" ")}` : ""}`;
3821
- throw new ACPSessionError(
3822
- "auth_failed",
3823
- `This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
3824
- );
3838
+ return ensureAuthenticated(this.acpSessionAuthHost());
3825
3839
  }
3826
3840
  sendResult(id, result) {
3827
3841
  return this.transport.send({ jsonrpc: "2.0", id, result });
@@ -3937,6 +3951,29 @@ var ACPSession = class _ACPSession {
3937
3951
  resetScratch() {
3938
3952
  this.scratch = createSessionScratch();
3939
3953
  }
3954
+ acpSessionAuthHost() {
3955
+ const self = this;
3956
+ return {
3957
+ get state() {
3958
+ return self.state;
3959
+ },
3960
+ set state(value) {
3961
+ self.state = value;
3962
+ },
3963
+ get authMethods() {
3964
+ return self.authMethods;
3965
+ },
3966
+ allocId: (...args) => this.allocId(...args),
3967
+ sendRequest: (...args) => this.sendRequest(...args),
3968
+ get agentCapabilities() {
3969
+ return self.agentCapabilities;
3970
+ },
3971
+ opContext: (...args) => this.opContext(...args),
3972
+ ensureAuthenticated: (...args) => this.ensureAuthenticated(...args),
3973
+ authenticate: (...args) => this.authenticate(...args),
3974
+ opts: this.opts
3975
+ };
3976
+ }
3940
3977
  };
3941
3978
 
3942
3979
  // src/client/tool-translator.ts