@wrongstack/acp 0.306.4 → 0.307.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/protocol-handler.d.ts +3 -49
- package/dist/agent/protocol-session-management.d.ts +36 -0
- package/dist/agent/protocol-session-ops.d.ts +59 -0
- package/dist/agent.js +430 -517
- package/dist/client/acp-session-ops.d.ts +33 -0
- package/dist/client/acp-session.d.ts +1 -98
- package/dist/client.js +242 -300
- package/dist/index.js +677 -822
- package/dist/wrongstack-acp-agent.js +430 -517
- package/package.json +3 -3
|
@@ -7,8 +7,6 @@ import { expandIPv6, writeErr as writeErr2 } from "@wrongstack/core/utils";
|
|
|
7
7
|
|
|
8
8
|
// src/agent/protocol-handler.ts
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
|
-
import * as fsp from "node:fs/promises";
|
|
11
|
-
import * as path from "node:path";
|
|
12
10
|
|
|
13
11
|
// src/types/acp-v1.ts
|
|
14
12
|
var ACP_PROTOCOL_VERSION = 1;
|
|
@@ -34,7 +32,9 @@ function toWire(msg) {
|
|
|
34
32
|
}
|
|
35
33
|
var WRONGSTACK_VERSION = ACP_PACKAGE_VERSION;
|
|
36
34
|
|
|
37
|
-
// src/agent/protocol-
|
|
35
|
+
// src/agent/protocol-session-ops.ts
|
|
36
|
+
import * as fsp from "node:fs/promises";
|
|
37
|
+
import * as path from "node:path";
|
|
38
38
|
var WRONGSTACK_AUTH_METHODS = [
|
|
39
39
|
{
|
|
40
40
|
id: "wrongstack-auth",
|
|
@@ -53,6 +53,384 @@ var DEFAULT_MODES = [
|
|
|
53
53
|
description: "Default agent mode for code-generation tasks."
|
|
54
54
|
}
|
|
55
55
|
];
|
|
56
|
+
async function resolveSessionCwd(requested) {
|
|
57
|
+
if (!path.isAbsolute(requested)) return null;
|
|
58
|
+
const resolved = path.resolve(requested);
|
|
59
|
+
try {
|
|
60
|
+
const stat2 = await fsp.stat(resolved);
|
|
61
|
+
return stat2.isDirectory() ? resolved : null;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function errorToJsonRpc(err) {
|
|
67
|
+
if (err && typeof err === "object") {
|
|
68
|
+
const e = err;
|
|
69
|
+
if (typeof e.code === "number" && typeof e.message === "string") {
|
|
70
|
+
const result = {
|
|
71
|
+
code: e.code,
|
|
72
|
+
message: e.message
|
|
73
|
+
};
|
|
74
|
+
if (e.data !== void 0) result.data = e.data;
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
79
|
+
return { code: -32603, message };
|
|
80
|
+
}
|
|
81
|
+
function createRunTurnApi(sessionId, clientCapabilities, request) {
|
|
82
|
+
return {
|
|
83
|
+
clientCapabilities,
|
|
84
|
+
requestPermission: async (req) => {
|
|
85
|
+
const res = await request("session/request_permission", {
|
|
86
|
+
sessionId,
|
|
87
|
+
toolCall: req.toolCall,
|
|
88
|
+
options: req.options
|
|
89
|
+
});
|
|
90
|
+
const outcome = res?.outcome;
|
|
91
|
+
return outcome ?? { outcome: "cancelled" };
|
|
92
|
+
},
|
|
93
|
+
readTextFile: async (params) => {
|
|
94
|
+
const res = await request("fs/read_text_file", { sessionId, ...params });
|
|
95
|
+
return String(res?.content ?? "");
|
|
96
|
+
},
|
|
97
|
+
writeTextFile: async (params) => {
|
|
98
|
+
await request("fs/write_text_file", { sessionId, ...params });
|
|
99
|
+
},
|
|
100
|
+
runTerminal: async ({ command, args, cwd }) => {
|
|
101
|
+
const created = await request("terminal/create", {
|
|
102
|
+
sessionId,
|
|
103
|
+
command,
|
|
104
|
+
...args ? { args } : {},
|
|
105
|
+
...cwd ? { cwd } : {}
|
|
106
|
+
});
|
|
107
|
+
const terminalId = created?.terminalId;
|
|
108
|
+
if (!terminalId) return { output: "", exitCode: null };
|
|
109
|
+
try {
|
|
110
|
+
const exit = await request("terminal/wait_for_exit", {
|
|
111
|
+
sessionId,
|
|
112
|
+
terminalId
|
|
113
|
+
});
|
|
114
|
+
const out = await request("terminal/output", { sessionId, terminalId });
|
|
115
|
+
return {
|
|
116
|
+
output: String(out?.output ?? ""),
|
|
117
|
+
exitCode: typeof exit?.exitCode === "number" ? exit.exitCode : null
|
|
118
|
+
};
|
|
119
|
+
} finally {
|
|
120
|
+
try {
|
|
121
|
+
await request("terminal/release", { sessionId, terminalId });
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function buildInitializeResult(agentName, modes, configOptions) {
|
|
129
|
+
return {
|
|
130
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
131
|
+
agentCapabilities: {
|
|
132
|
+
loadSession: true,
|
|
133
|
+
promptCapabilities: {
|
|
134
|
+
image: true,
|
|
135
|
+
audio: false,
|
|
136
|
+
embeddedContext: true
|
|
137
|
+
},
|
|
138
|
+
mcpCapabilities: {
|
|
139
|
+
http: false,
|
|
140
|
+
sse: false
|
|
141
|
+
},
|
|
142
|
+
sessionCapabilities: {
|
|
143
|
+
close: {},
|
|
144
|
+
list: {},
|
|
145
|
+
delete: {},
|
|
146
|
+
resume: {},
|
|
147
|
+
fork: {}
|
|
148
|
+
},
|
|
149
|
+
auth: {
|
|
150
|
+
logout: {}
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
agentInfo: {
|
|
154
|
+
name: agentName,
|
|
155
|
+
title: "WrongStack",
|
|
156
|
+
version: WRONGSTACK_VERSION
|
|
157
|
+
},
|
|
158
|
+
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
159
|
+
modes,
|
|
160
|
+
configOptions
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/agent/protocol-session-management.ts
|
|
165
|
+
async function handleSessionNewOp(ctx, id, params) {
|
|
166
|
+
if (ctx.sessions.size >= ctx.maxSessions) {
|
|
167
|
+
await ctx.sendError(id, -32e3, `active session limit reached (${ctx.maxSessions})`);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
const p = params ?? {};
|
|
171
|
+
let cwd = ctx.defaultCwd;
|
|
172
|
+
if (typeof p.cwd === "string") {
|
|
173
|
+
const resolved = await resolveSessionCwd(p.cwd);
|
|
174
|
+
if (resolved === null) {
|
|
175
|
+
await ctx.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
cwd = resolved;
|
|
179
|
+
}
|
|
180
|
+
const sessionId = `sess_${ctx.allocId()}`;
|
|
181
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
182
|
+
const state = {
|
|
183
|
+
id: sessionId,
|
|
184
|
+
cwd,
|
|
185
|
+
abort: new AbortController(),
|
|
186
|
+
modeId: DEFAULT_MODE_ID,
|
|
187
|
+
createdAt: now,
|
|
188
|
+
updatedAt: now
|
|
189
|
+
};
|
|
190
|
+
ctx.sessions.set(sessionId, state);
|
|
191
|
+
ctx.onSessionNew(state);
|
|
192
|
+
await ctx.persist(state);
|
|
193
|
+
await ctx.sendNotification({
|
|
194
|
+
sessionId,
|
|
195
|
+
update: {
|
|
196
|
+
sessionUpdate: "current_mode_update",
|
|
197
|
+
modeId: ctx.modes[0]?.id ?? DEFAULT_MODE_ID
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
if (ctx.configOptions.length > 0) {
|
|
201
|
+
await ctx.sendNotification({
|
|
202
|
+
sessionId,
|
|
203
|
+
update: {
|
|
204
|
+
sessionUpdate: "config_option_update",
|
|
205
|
+
configOptions: [...ctx.configOptions]
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
await ctx.sendResult(id, {
|
|
210
|
+
sessionId,
|
|
211
|
+
modes: ctx.modes,
|
|
212
|
+
configOptions: ctx.configOptions
|
|
213
|
+
});
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
async function handleSessionLoadOp(ctx, id, params) {
|
|
217
|
+
const p = params ?? {};
|
|
218
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
219
|
+
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
220
|
+
const existing = sessionId ? ctx.sessions.get(sessionId) : void 0;
|
|
221
|
+
if (!existing && sessionId && ctx.store) {
|
|
222
|
+
const persisted = await ctx.store.load(sessionId);
|
|
223
|
+
if (persisted) {
|
|
224
|
+
if (ctx.sessions.size >= ctx.maxSessions) {
|
|
225
|
+
await ctx.sendError(id, -32e3, `active session limit reached (${ctx.maxSessions})`);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
if (loadCwd !== void 0 && await resolveSessionCwd(loadCwd) === null) {
|
|
229
|
+
await ctx.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
const candidateCwd = persisted.cwd ?? loadCwd ?? ctx.defaultCwd;
|
|
233
|
+
const restoredCwd = await resolveSessionCwd(candidateCwd) ?? ctx.defaultCwd;
|
|
234
|
+
const restored = {
|
|
235
|
+
id: sessionId,
|
|
236
|
+
cwd: restoredCwd,
|
|
237
|
+
abort: new AbortController(),
|
|
238
|
+
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
239
|
+
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
240
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
241
|
+
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
242
|
+
};
|
|
243
|
+
ctx.sessions.set(sessionId, restored);
|
|
244
|
+
ctx.seedFor?.(sessionId, persisted.history ?? []);
|
|
245
|
+
for (const update of persisted.history ?? []) {
|
|
246
|
+
await ctx.sendNotification({ sessionId, update });
|
|
247
|
+
}
|
|
248
|
+
await ctx.sendNotification({
|
|
249
|
+
sessionId,
|
|
250
|
+
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
251
|
+
});
|
|
252
|
+
await ctx.sendResult(id, {
|
|
253
|
+
initialMode: { currentModeId: restored.modeId, availableModes: ctx.modes }
|
|
254
|
+
});
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (existing) {
|
|
259
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
260
|
+
const replay = ctx.replayFor?.(sessionId);
|
|
261
|
+
if (replay) {
|
|
262
|
+
for (const update of replay) {
|
|
263
|
+
await ctx.sendNotification({ sessionId, update });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
await ctx.sendNotification({
|
|
267
|
+
sessionId,
|
|
268
|
+
update: {
|
|
269
|
+
sessionUpdate: "session_info_update",
|
|
270
|
+
updatedAt: existing.updatedAt
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
await ctx.sendNotification({
|
|
274
|
+
sessionId,
|
|
275
|
+
update: {
|
|
276
|
+
sessionUpdate: "current_mode_update",
|
|
277
|
+
modeId: existing.modeId
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
await ctx.sendResult(id, {
|
|
281
|
+
initialMode: {
|
|
282
|
+
currentModeId: existing.modeId,
|
|
283
|
+
availableModes: ctx.modes
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
await ctx.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
async function handleSessionForkOp(ctx, id, params) {
|
|
292
|
+
const p = params ?? {};
|
|
293
|
+
const sourceId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
294
|
+
const source = sourceId ? ctx.sessions.get(sourceId) : void 0;
|
|
295
|
+
if (!sourceId || !source) {
|
|
296
|
+
await ctx.sendError(id, -32e3, `session not found: ${sourceId}`);
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
if (ctx.sessions.size >= ctx.maxSessions) {
|
|
300
|
+
await ctx.sendError(id, -32e3, `active session limit reached (${ctx.maxSessions})`);
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
let forkCwd = source.cwd;
|
|
304
|
+
if (typeof p.cwd === "string") {
|
|
305
|
+
const resolved = await resolveSessionCwd(p.cwd);
|
|
306
|
+
if (resolved === null) {
|
|
307
|
+
await ctx.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
forkCwd = resolved;
|
|
311
|
+
}
|
|
312
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
313
|
+
const sessionId = `sess_${ctx.allocId()}`;
|
|
314
|
+
const forked = {
|
|
315
|
+
id: sessionId,
|
|
316
|
+
cwd: forkCwd,
|
|
317
|
+
abort: new AbortController(),
|
|
318
|
+
modeId: source.modeId,
|
|
319
|
+
createdAt: now,
|
|
320
|
+
updatedAt: now,
|
|
321
|
+
...source.title !== void 0 ? { title: source.title } : {}
|
|
322
|
+
};
|
|
323
|
+
const history = (ctx.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
324
|
+
sessionUpdate: update.sessionUpdate,
|
|
325
|
+
content: structuredClone(update.content)
|
|
326
|
+
}));
|
|
327
|
+
ctx.sessions.set(sessionId, forked);
|
|
328
|
+
ctx.seedFor?.(sessionId, history);
|
|
329
|
+
ctx.onSessionNew(forked);
|
|
330
|
+
await ctx.persist(forked, history);
|
|
331
|
+
await ctx.sendNotification({
|
|
332
|
+
sessionId,
|
|
333
|
+
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
334
|
+
});
|
|
335
|
+
await ctx.sendResult(id, {
|
|
336
|
+
sessionId,
|
|
337
|
+
modes: ctx.modes,
|
|
338
|
+
configOptions: ctx.configOptions
|
|
339
|
+
});
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
async function handleSessionPromptOp(ctx, id, params) {
|
|
343
|
+
const p = params ?? {};
|
|
344
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
345
|
+
if (!sessionId || !ctx.sessions.has(sessionId)) {
|
|
346
|
+
await ctx.sendError(id, -32e3, "unknown or missing sessionId");
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
if (!Array.isArray(p.prompt)) {
|
|
350
|
+
await ctx.sendError(id, -32602, "prompt must be an array of content blocks");
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
const session = ctx.sessions.get(sessionId);
|
|
354
|
+
if (session.abort.signal.aborted) {
|
|
355
|
+
session.abort = new AbortController();
|
|
356
|
+
}
|
|
357
|
+
const turnSignal = new AbortController();
|
|
358
|
+
const onCancel = () => turnSignal.abort();
|
|
359
|
+
session.abort.signal.addEventListener("abort", onCancel, { once: true });
|
|
360
|
+
const api = createRunTurnApi(
|
|
361
|
+
sessionId,
|
|
362
|
+
ctx.clientCapabilities ?? {},
|
|
363
|
+
(method, req) => ctx.request(method, req)
|
|
364
|
+
);
|
|
365
|
+
let result;
|
|
366
|
+
const pendingNotifications = [];
|
|
367
|
+
const emit = (update) => {
|
|
368
|
+
const notifPromise = ctx.sendNotification({ sessionId, update });
|
|
369
|
+
pendingNotifications.push(notifPromise.catch(() => {
|
|
370
|
+
}));
|
|
371
|
+
};
|
|
372
|
+
try {
|
|
373
|
+
result = await ctx.runTurn(
|
|
374
|
+
{ sessionId, prompt: p.prompt, signal: turnSignal.signal },
|
|
375
|
+
emit,
|
|
376
|
+
api
|
|
377
|
+
);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
session.abort.signal.removeEventListener("abort", onCancel);
|
|
380
|
+
const { code, message, data } = errorToJsonRpc(err);
|
|
381
|
+
await ctx.sendError(id, code, message, data);
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
await Promise.all(pendingNotifications);
|
|
385
|
+
session.abort.signal.removeEventListener("abort", onCancel);
|
|
386
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
387
|
+
await ctx.persist(session);
|
|
388
|
+
await ctx.sendResult(id, { stopReason: result.stopReason });
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
async function handleSetModeOp(ctx, id, params) {
|
|
392
|
+
const p = params ?? {};
|
|
393
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
394
|
+
const modeId = typeof p.modeId === "string" ? p.modeId : null;
|
|
395
|
+
const session = sessionId ? ctx.sessions.get(sessionId) : void 0;
|
|
396
|
+
if (!session || !modeId || !ctx.modes.some((m) => m.id === modeId)) {
|
|
397
|
+
await ctx.sendError(id, -32602, "invalid sessionId or modeId");
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
session.modeId = modeId;
|
|
401
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
402
|
+
await ctx.sendNotification({
|
|
403
|
+
sessionId,
|
|
404
|
+
update: { sessionUpdate: "current_mode_update", modeId }
|
|
405
|
+
});
|
|
406
|
+
await ctx.sendResult(id, {});
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
async function handleSetConfigOptionOp(ctx, id, params) {
|
|
410
|
+
const p = params ?? {};
|
|
411
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
412
|
+
const optionId = typeof p.configId === "string" ? p.configId : null;
|
|
413
|
+
const value = typeof p.value === "string" ? p.value : null;
|
|
414
|
+
const session = sessionId ? ctx.sessions.get(sessionId) : void 0;
|
|
415
|
+
const option = optionId ? ctx.configOptions.find((o) => o.id === optionId) : void 0;
|
|
416
|
+
if (!session || !option || value === null || !option.options.some((o) => o.value === value)) {
|
|
417
|
+
await ctx.sendError(id, -32602, "invalid sessionId, configId, or value");
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
option.currentValue = value;
|
|
421
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
422
|
+
await ctx.sendNotification({
|
|
423
|
+
sessionId,
|
|
424
|
+
update: {
|
|
425
|
+
sessionUpdate: "config_option_update",
|
|
426
|
+
configOptions: [...ctx.configOptions]
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
await ctx.sendResult(id, { configOptions: [...ctx.configOptions] });
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/agent/protocol-handler.ts
|
|
56
434
|
var ACPProtocolHandler = class {
|
|
57
435
|
transport;
|
|
58
436
|
defaultCwd;
|
|
@@ -161,6 +539,27 @@ var ACPProtocolHandler = class {
|
|
|
161
539
|
} catch {
|
|
162
540
|
}
|
|
163
541
|
}
|
|
542
|
+
sessionContext() {
|
|
543
|
+
return {
|
|
544
|
+
sessions: this.sessions,
|
|
545
|
+
maxSessions: this.maxSessions,
|
|
546
|
+
defaultCwd: this.defaultCwd,
|
|
547
|
+
modes: this.modes,
|
|
548
|
+
configOptions: this.configOptions,
|
|
549
|
+
store: this.store,
|
|
550
|
+
replayFor: this.replayFor,
|
|
551
|
+
seedFor: this.seedFor,
|
|
552
|
+
onSessionNew: this.onSessionNew,
|
|
553
|
+
allocId: () => this.allocId(),
|
|
554
|
+
persist: (state, history) => this.persist(state, history),
|
|
555
|
+
sendNotification: (params) => this.sendNotification(params),
|
|
556
|
+
sendError: (id, code, message, data) => this.sendError(id, code, message, data),
|
|
557
|
+
sendResult: (id, result) => this.sendResult(id, result),
|
|
558
|
+
request: (method, params, timeoutMs) => this.request(method, params, timeoutMs),
|
|
559
|
+
runTurn: this.runTurn,
|
|
560
|
+
clientCapabilities: this.clientCapabilities
|
|
561
|
+
};
|
|
562
|
+
}
|
|
164
563
|
// ────────────────────────────────────────────────────────────────────
|
|
165
564
|
// Requests
|
|
166
565
|
// ────────────────────────────────────────────────────────────────────
|
|
@@ -178,9 +577,9 @@ var ACPProtocolHandler = class {
|
|
|
178
577
|
case "logout":
|
|
179
578
|
return await this.handleLogout(id, params);
|
|
180
579
|
case "session/new":
|
|
181
|
-
return await this.
|
|
580
|
+
return await handleSessionNewOp(this.sessionContext(), id, params);
|
|
182
581
|
case "session/load":
|
|
183
|
-
return await this.
|
|
582
|
+
return await handleSessionLoadOp(this.sessionContext(), id, params);
|
|
184
583
|
case "session/resume":
|
|
185
584
|
return await this.handleSessionResume(id, params);
|
|
186
585
|
case "session/close":
|
|
@@ -188,15 +587,15 @@ var ACPProtocolHandler = class {
|
|
|
188
587
|
case "session/delete":
|
|
189
588
|
return await this.handleSessionDelete(id, params);
|
|
190
589
|
case "session/prompt":
|
|
191
|
-
return await this.
|
|
590
|
+
return await handleSessionPromptOp(this.sessionContext(), id, params);
|
|
192
591
|
case "session/set_mode":
|
|
193
|
-
return await this.
|
|
592
|
+
return await handleSetModeOp(this.sessionContext(), id, params);
|
|
194
593
|
case "session/set_config_option":
|
|
195
|
-
return await this.
|
|
594
|
+
return await handleSetConfigOptionOp(this.sessionContext(), id, params);
|
|
196
595
|
case "session/list":
|
|
197
596
|
return await this.handleSessionList(id);
|
|
198
597
|
case "session/fork":
|
|
199
|
-
return await this.
|
|
598
|
+
return await handleSessionForkOp(this.sessionContext(), id, params);
|
|
200
599
|
case "providers/list":
|
|
201
600
|
return await this.handleProvidersList(id, params);
|
|
202
601
|
case "providers/set":
|
|
@@ -221,232 +620,32 @@ var ACPProtocolHandler = class {
|
|
|
221
620
|
this.clientCapabilities = p.clientCapabilities;
|
|
222
621
|
}
|
|
223
622
|
this.initialized = true;
|
|
224
|
-
await this.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
id,
|
|
228
|
-
result: {
|
|
229
|
-
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
230
|
-
agentCapabilities: {
|
|
231
|
-
loadSession: true,
|
|
232
|
-
promptCapabilities: {
|
|
233
|
-
// We route ACP image blocks into the core agent's multimodal
|
|
234
|
-
// input (server-agent-turn.promptToAgentInput); whether the
|
|
235
|
-
// model can see them is the configured provider's concern.
|
|
236
|
-
image: true,
|
|
237
|
-
audio: false,
|
|
238
|
-
embeddedContext: true
|
|
239
|
-
},
|
|
240
|
-
mcpCapabilities: {
|
|
241
|
-
http: false,
|
|
242
|
-
sse: false
|
|
243
|
-
},
|
|
244
|
-
sessionCapabilities: {
|
|
245
|
-
close: {},
|
|
246
|
-
list: {},
|
|
247
|
-
delete: {},
|
|
248
|
-
resume: {},
|
|
249
|
-
fork: {}
|
|
250
|
-
},
|
|
251
|
-
auth: {
|
|
252
|
-
logout: {}
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
agentInfo: {
|
|
256
|
-
name: this.agentName,
|
|
257
|
-
title: "WrongStack",
|
|
258
|
-
version: WRONGSTACK_VERSION
|
|
259
|
-
},
|
|
260
|
-
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
261
|
-
modes: this.modes,
|
|
262
|
-
configOptions: this.configOptions
|
|
263
|
-
}
|
|
264
|
-
})
|
|
623
|
+
await this.sendResult(
|
|
624
|
+
id,
|
|
625
|
+
buildInitializeResult(this.agentName, this.modes, this.configOptions)
|
|
265
626
|
);
|
|
266
627
|
return false;
|
|
267
628
|
}
|
|
268
629
|
async handleAuthenticate(id, _params) {
|
|
269
|
-
await this.
|
|
270
|
-
toWire({
|
|
271
|
-
jsonrpc: "2.0",
|
|
272
|
-
id,
|
|
273
|
-
result: { outcome: "unauthenticated" }
|
|
274
|
-
})
|
|
275
|
-
);
|
|
630
|
+
await this.sendResult(id, { outcome: "unauthenticated" });
|
|
276
631
|
return false;
|
|
277
632
|
}
|
|
278
633
|
async handleLogout(id, _params) {
|
|
279
|
-
await this.
|
|
280
|
-
toWire({
|
|
281
|
-
jsonrpc: "2.0",
|
|
282
|
-
id,
|
|
283
|
-
result: {}
|
|
284
|
-
})
|
|
285
|
-
);
|
|
634
|
+
await this.sendResult(id, {});
|
|
286
635
|
return false;
|
|
287
636
|
}
|
|
288
|
-
async
|
|
289
|
-
if (this.sessions.size >= this.maxSessions) {
|
|
290
|
-
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
291
|
-
return false;
|
|
292
|
-
}
|
|
293
|
-
const p = params ?? {};
|
|
294
|
-
let cwd = this.defaultCwd;
|
|
295
|
-
if (typeof p.cwd === "string") {
|
|
296
|
-
const resolved = await this.resolveSessionCwd(p.cwd);
|
|
297
|
-
if (resolved === null) {
|
|
298
|
-
await this.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
299
|
-
return false;
|
|
300
|
-
}
|
|
301
|
-
cwd = resolved;
|
|
302
|
-
}
|
|
303
|
-
const sessionId = `sess_${this.allocId()}`;
|
|
304
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
305
|
-
const state = {
|
|
306
|
-
id: sessionId,
|
|
307
|
-
cwd,
|
|
308
|
-
abort: new AbortController(),
|
|
309
|
-
modeId: DEFAULT_MODE_ID,
|
|
310
|
-
createdAt: now,
|
|
311
|
-
updatedAt: now
|
|
312
|
-
};
|
|
313
|
-
this.sessions.set(sessionId, state);
|
|
314
|
-
this.onSessionNew(state);
|
|
315
|
-
await this.persist(state);
|
|
316
|
-
await this.sendNotification({
|
|
317
|
-
sessionId,
|
|
318
|
-
update: {
|
|
319
|
-
sessionUpdate: "current_mode_update",
|
|
320
|
-
modeId: this.modes[0]?.id ?? DEFAULT_MODE_ID
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
if (this.configOptions.length > 0) {
|
|
324
|
-
await this.sendNotification({
|
|
325
|
-
sessionId,
|
|
326
|
-
update: {
|
|
327
|
-
sessionUpdate: "config_option_update",
|
|
328
|
-
configOptions: [...this.configOptions]
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
await this.transport.send(
|
|
333
|
-
toWire({
|
|
334
|
-
jsonrpc: "2.0",
|
|
335
|
-
id,
|
|
336
|
-
result: {
|
|
337
|
-
sessionId,
|
|
338
|
-
modes: this.modes,
|
|
339
|
-
configOptions: this.configOptions
|
|
340
|
-
}
|
|
341
|
-
})
|
|
342
|
-
);
|
|
343
|
-
return false;
|
|
344
|
-
}
|
|
345
|
-
async handleSessionLoad(id, params) {
|
|
637
|
+
async handleSessionResume(id, params) {
|
|
346
638
|
const p = params ?? {};
|
|
347
639
|
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
348
|
-
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
349
640
|
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
350
|
-
if (!existing && sessionId && this.store) {
|
|
351
|
-
const persisted = await this.store.load(sessionId);
|
|
352
|
-
if (persisted) {
|
|
353
|
-
if (this.sessions.size >= this.maxSessions) {
|
|
354
|
-
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
355
|
-
return false;
|
|
356
|
-
}
|
|
357
|
-
if (loadCwd !== void 0 && await this.resolveSessionCwd(loadCwd) === null) {
|
|
358
|
-
await this.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
359
|
-
return false;
|
|
360
|
-
}
|
|
361
|
-
const candidateCwd = persisted.cwd ?? loadCwd ?? this.defaultCwd;
|
|
362
|
-
const restoredCwd = await this.resolveSessionCwd(candidateCwd) ?? this.defaultCwd;
|
|
363
|
-
const restored = {
|
|
364
|
-
id: sessionId,
|
|
365
|
-
cwd: restoredCwd,
|
|
366
|
-
abort: new AbortController(),
|
|
367
|
-
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
368
|
-
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
369
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
370
|
-
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
371
|
-
};
|
|
372
|
-
this.sessions.set(sessionId, restored);
|
|
373
|
-
this.seedFor?.(sessionId, persisted.history ?? []);
|
|
374
|
-
for (const update of persisted.history ?? []) {
|
|
375
|
-
await this.sendNotification({ sessionId, update });
|
|
376
|
-
}
|
|
377
|
-
await this.sendNotification({
|
|
378
|
-
sessionId,
|
|
379
|
-
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
380
|
-
});
|
|
381
|
-
await this.transport.send(
|
|
382
|
-
toWire({
|
|
383
|
-
jsonrpc: "2.0",
|
|
384
|
-
id,
|
|
385
|
-
result: {
|
|
386
|
-
initialMode: { currentModeId: restored.modeId, availableModes: this.modes }
|
|
387
|
-
}
|
|
388
|
-
})
|
|
389
|
-
);
|
|
390
|
-
return false;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
641
|
if (existing) {
|
|
394
642
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
await this.sendNotification({
|
|
402
|
-
sessionId,
|
|
403
|
-
update: {
|
|
404
|
-
sessionUpdate: "session_info_update",
|
|
405
|
-
updatedAt: existing.updatedAt
|
|
406
|
-
}
|
|
407
|
-
});
|
|
408
|
-
await this.sendNotification({
|
|
409
|
-
sessionId,
|
|
410
|
-
update: {
|
|
411
|
-
sessionUpdate: "current_mode_update",
|
|
412
|
-
modeId: existing.modeId
|
|
643
|
+
await this.sendResult(id, {
|
|
644
|
+
initialMode: {
|
|
645
|
+
currentModeId: existing.modeId,
|
|
646
|
+
availableModes: this.modes
|
|
413
647
|
}
|
|
414
648
|
});
|
|
415
|
-
await this.transport.send(
|
|
416
|
-
toWire({
|
|
417
|
-
jsonrpc: "2.0",
|
|
418
|
-
id,
|
|
419
|
-
result: {
|
|
420
|
-
initialMode: {
|
|
421
|
-
currentModeId: existing.modeId,
|
|
422
|
-
availableModes: this.modes
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
})
|
|
426
|
-
);
|
|
427
|
-
return false;
|
|
428
|
-
}
|
|
429
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
async handleSessionResume(id, params) {
|
|
433
|
-
const p = params ?? {};
|
|
434
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
435
|
-
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
436
|
-
if (existing) {
|
|
437
|
-
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
438
|
-
await this.transport.send(
|
|
439
|
-
toWire({
|
|
440
|
-
jsonrpc: "2.0",
|
|
441
|
-
id,
|
|
442
|
-
result: {
|
|
443
|
-
initialMode: {
|
|
444
|
-
currentModeId: existing.modeId,
|
|
445
|
-
availableModes: this.modes
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
})
|
|
449
|
-
);
|
|
450
649
|
return false;
|
|
451
650
|
}
|
|
452
651
|
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
@@ -463,13 +662,7 @@ var ACPProtocolHandler = class {
|
|
|
463
662
|
session.abort.abort();
|
|
464
663
|
this.sessions.delete(sessionId);
|
|
465
664
|
this.disposeSession(sessionId);
|
|
466
|
-
await this.
|
|
467
|
-
toWire({
|
|
468
|
-
jsonrpc: "2.0",
|
|
469
|
-
id,
|
|
470
|
-
result: {}
|
|
471
|
-
})
|
|
472
|
-
);
|
|
665
|
+
await this.sendResult(id, {});
|
|
473
666
|
return false;
|
|
474
667
|
}
|
|
475
668
|
async handleSessionDelete(id, params) {
|
|
@@ -480,92 +673,21 @@ var ACPProtocolHandler = class {
|
|
|
480
673
|
return false;
|
|
481
674
|
}
|
|
482
675
|
if (!this.sessions.has(sessionId)) {
|
|
483
|
-
await this.
|
|
484
|
-
toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } })
|
|
485
|
-
);
|
|
676
|
+
await this.sendResult(id, { configOptions: [...this.configOptions] });
|
|
486
677
|
return false;
|
|
487
678
|
}
|
|
488
679
|
const session = this.sessions.get(sessionId);
|
|
489
680
|
session.abort.abort();
|
|
490
681
|
this.sessions.delete(sessionId);
|
|
491
682
|
this.disposeSession(sessionId);
|
|
492
|
-
await this.
|
|
493
|
-
toWire({
|
|
494
|
-
jsonrpc: "2.0",
|
|
495
|
-
id,
|
|
496
|
-
result: {}
|
|
497
|
-
})
|
|
498
|
-
);
|
|
499
|
-
return false;
|
|
500
|
-
}
|
|
501
|
-
async handleSessionFork(id, params) {
|
|
502
|
-
const p = params ?? {};
|
|
503
|
-
const sourceId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
504
|
-
const source = sourceId ? this.sessions.get(sourceId) : void 0;
|
|
505
|
-
if (!sourceId || !source) {
|
|
506
|
-
await this.sendError(id, -32e3, `session not found: ${sourceId}`);
|
|
507
|
-
return false;
|
|
508
|
-
}
|
|
509
|
-
if (this.sessions.size >= this.maxSessions) {
|
|
510
|
-
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
511
|
-
return false;
|
|
512
|
-
}
|
|
513
|
-
let forkCwd = source.cwd;
|
|
514
|
-
if (typeof p.cwd === "string") {
|
|
515
|
-
const resolved = await this.resolveSessionCwd(p.cwd);
|
|
516
|
-
if (resolved === null) {
|
|
517
|
-
await this.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
518
|
-
return false;
|
|
519
|
-
}
|
|
520
|
-
forkCwd = resolved;
|
|
521
|
-
}
|
|
522
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
523
|
-
const sessionId = `sess_${this.allocId()}`;
|
|
524
|
-
const forked = {
|
|
525
|
-
id: sessionId,
|
|
526
|
-
cwd: forkCwd,
|
|
527
|
-
abort: new AbortController(),
|
|
528
|
-
modeId: source.modeId,
|
|
529
|
-
createdAt: now,
|
|
530
|
-
updatedAt: now,
|
|
531
|
-
...source.title !== void 0 ? { title: source.title } : {}
|
|
532
|
-
};
|
|
533
|
-
const history = (this.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
534
|
-
sessionUpdate: update.sessionUpdate,
|
|
535
|
-
content: structuredClone(update.content)
|
|
536
|
-
}));
|
|
537
|
-
this.sessions.set(sessionId, forked);
|
|
538
|
-
this.seedFor?.(sessionId, history);
|
|
539
|
-
this.onSessionNew(forked);
|
|
540
|
-
await this.persist(forked, history);
|
|
541
|
-
await this.sendNotification({
|
|
542
|
-
sessionId,
|
|
543
|
-
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
544
|
-
});
|
|
545
|
-
await this.transport.send(
|
|
546
|
-
toWire({
|
|
547
|
-
jsonrpc: "2.0",
|
|
548
|
-
id,
|
|
549
|
-
result: {
|
|
550
|
-
sessionId,
|
|
551
|
-
modes: this.modes,
|
|
552
|
-
configOptions: this.configOptions
|
|
553
|
-
}
|
|
554
|
-
})
|
|
555
|
-
);
|
|
683
|
+
await this.sendResult(id, {});
|
|
556
684
|
return false;
|
|
557
685
|
}
|
|
558
686
|
async handleProvidersList(id, _params) {
|
|
559
|
-
await this.
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
result: {
|
|
564
|
-
providers: [],
|
|
565
|
-
currentProviderId: null
|
|
566
|
-
}
|
|
567
|
-
})
|
|
568
|
-
);
|
|
687
|
+
await this.sendResult(id, {
|
|
688
|
+
providers: [],
|
|
689
|
+
currentProviderId: null
|
|
690
|
+
});
|
|
569
691
|
return false;
|
|
570
692
|
}
|
|
571
693
|
async handleProvidersSet(id, _params) {
|
|
@@ -577,157 +699,13 @@ var ACPProtocolHandler = class {
|
|
|
577
699
|
return false;
|
|
578
700
|
}
|
|
579
701
|
async handleProvidersDisable(id, _params) {
|
|
580
|
-
await this.
|
|
581
|
-
toWire({
|
|
582
|
-
jsonrpc: "2.0",
|
|
583
|
-
id,
|
|
584
|
-
result: {}
|
|
585
|
-
})
|
|
586
|
-
);
|
|
702
|
+
await this.sendResult(id, {});
|
|
587
703
|
return false;
|
|
588
704
|
}
|
|
589
705
|
async handleMcpMessage(id, _params) {
|
|
590
706
|
await this.sendError(id, -32e3, "MCP message routing not available through ACP");
|
|
591
707
|
return false;
|
|
592
708
|
}
|
|
593
|
-
async handleSessionPrompt(id, params) {
|
|
594
|
-
const p = params ?? {};
|
|
595
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
596
|
-
if (!sessionId || !this.sessions.has(sessionId)) {
|
|
597
|
-
await this.sendError(id, -32e3, "unknown or missing sessionId");
|
|
598
|
-
return false;
|
|
599
|
-
}
|
|
600
|
-
if (!Array.isArray(p.prompt)) {
|
|
601
|
-
await this.sendError(id, -32602, "prompt must be an array of content blocks");
|
|
602
|
-
return false;
|
|
603
|
-
}
|
|
604
|
-
const session = this.sessions.get(sessionId);
|
|
605
|
-
if (session.abort.signal.aborted) {
|
|
606
|
-
session.abort = new AbortController();
|
|
607
|
-
}
|
|
608
|
-
const turnSignal = new AbortController();
|
|
609
|
-
const onCancel = () => turnSignal.abort();
|
|
610
|
-
session.abort.signal.addEventListener("abort", onCancel, { once: true });
|
|
611
|
-
const api = {
|
|
612
|
-
clientCapabilities: this.clientCapabilities,
|
|
613
|
-
requestPermission: async (req) => {
|
|
614
|
-
const res = await this.request("session/request_permission", {
|
|
615
|
-
sessionId,
|
|
616
|
-
toolCall: req.toolCall,
|
|
617
|
-
options: req.options
|
|
618
|
-
});
|
|
619
|
-
const outcome = res?.outcome;
|
|
620
|
-
return outcome ?? { outcome: "cancelled" };
|
|
621
|
-
},
|
|
622
|
-
readTextFile: async (params2) => {
|
|
623
|
-
const res = await this.request("fs/read_text_file", { sessionId, ...params2 });
|
|
624
|
-
return String(res?.content ?? "");
|
|
625
|
-
},
|
|
626
|
-
writeTextFile: async (params2) => {
|
|
627
|
-
await this.request("fs/write_text_file", { sessionId, ...params2 });
|
|
628
|
-
},
|
|
629
|
-
runTerminal: async ({ command, args, cwd }) => {
|
|
630
|
-
const created = await this.request("terminal/create", {
|
|
631
|
-
sessionId,
|
|
632
|
-
command,
|
|
633
|
-
...args ? { args } : {},
|
|
634
|
-
...cwd ? { cwd } : {}
|
|
635
|
-
});
|
|
636
|
-
const terminalId = created?.terminalId;
|
|
637
|
-
if (!terminalId) return { output: "", exitCode: null };
|
|
638
|
-
try {
|
|
639
|
-
const exit = await this.request("terminal/wait_for_exit", {
|
|
640
|
-
sessionId,
|
|
641
|
-
terminalId
|
|
642
|
-
});
|
|
643
|
-
const out = await this.request("terminal/output", { sessionId, terminalId });
|
|
644
|
-
return {
|
|
645
|
-
output: String(out?.output ?? ""),
|
|
646
|
-
exitCode: typeof exit?.exitCode === "number" ? exit.exitCode : null
|
|
647
|
-
};
|
|
648
|
-
} finally {
|
|
649
|
-
try {
|
|
650
|
-
await this.request("terminal/release", { sessionId, terminalId });
|
|
651
|
-
} catch {
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
};
|
|
656
|
-
let result;
|
|
657
|
-
const pendingNotifications = [];
|
|
658
|
-
const emit = (update) => {
|
|
659
|
-
const p2 = this.sendNotification({ sessionId, update });
|
|
660
|
-
pendingNotifications.push(p2.catch(() => {
|
|
661
|
-
}));
|
|
662
|
-
};
|
|
663
|
-
try {
|
|
664
|
-
result = await this.runTurn(
|
|
665
|
-
{ sessionId, prompt: p.prompt, signal: turnSignal.signal },
|
|
666
|
-
emit,
|
|
667
|
-
api
|
|
668
|
-
);
|
|
669
|
-
} catch (err) {
|
|
670
|
-
session.abort.signal.removeEventListener("abort", onCancel);
|
|
671
|
-
const { code, message, data } = errorToJsonRpc(err);
|
|
672
|
-
await this.sendError(id, code, message, data);
|
|
673
|
-
return false;
|
|
674
|
-
}
|
|
675
|
-
await Promise.all(pendingNotifications);
|
|
676
|
-
session.abort.signal.removeEventListener("abort", onCancel);
|
|
677
|
-
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
678
|
-
await this.persist(session);
|
|
679
|
-
await this.transport.send(
|
|
680
|
-
toWire({
|
|
681
|
-
jsonrpc: "2.0",
|
|
682
|
-
id,
|
|
683
|
-
result: { stopReason: result.stopReason }
|
|
684
|
-
})
|
|
685
|
-
);
|
|
686
|
-
return false;
|
|
687
|
-
}
|
|
688
|
-
async handleSetMode(id, params) {
|
|
689
|
-
const p = params ?? {};
|
|
690
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
691
|
-
const modeId = typeof p.modeId === "string" ? p.modeId : null;
|
|
692
|
-
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
693
|
-
if (!session || !modeId || !this.modes.some((m) => m.id === modeId)) {
|
|
694
|
-
await this.sendError(id, -32602, "invalid sessionId or modeId");
|
|
695
|
-
return false;
|
|
696
|
-
}
|
|
697
|
-
session.modeId = modeId;
|
|
698
|
-
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
699
|
-
await this.sendNotification({
|
|
700
|
-
sessionId,
|
|
701
|
-
update: { sessionUpdate: "current_mode_update", modeId }
|
|
702
|
-
});
|
|
703
|
-
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: {} }));
|
|
704
|
-
return false;
|
|
705
|
-
}
|
|
706
|
-
async handleSetConfigOption(id, params) {
|
|
707
|
-
const p = params ?? {};
|
|
708
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
709
|
-
const optionId = typeof p.configId === "string" ? p.configId : null;
|
|
710
|
-
const value = typeof p.value === "string" ? p.value : null;
|
|
711
|
-
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
712
|
-
const option = optionId ? this.configOptions.find((o) => o.id === optionId) : void 0;
|
|
713
|
-
if (!session || !option || value === null || !option.options.some((o) => o.value === value)) {
|
|
714
|
-
await this.sendError(id, -32602, "invalid sessionId, configId, or value");
|
|
715
|
-
return false;
|
|
716
|
-
}
|
|
717
|
-
option.currentValue = value;
|
|
718
|
-
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
719
|
-
await this.sendNotification({
|
|
720
|
-
sessionId,
|
|
721
|
-
update: {
|
|
722
|
-
sessionUpdate: "config_option_update",
|
|
723
|
-
configOptions: [...this.configOptions]
|
|
724
|
-
}
|
|
725
|
-
});
|
|
726
|
-
await this.transport.send(
|
|
727
|
-
toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } })
|
|
728
|
-
);
|
|
729
|
-
return false;
|
|
730
|
-
}
|
|
731
709
|
async handleSessionList(id) {
|
|
732
710
|
const sessions = Array.from(this.sessions.values()).map((s) => {
|
|
733
711
|
const out = {
|
|
@@ -738,13 +716,7 @@ var ACPProtocolHandler = class {
|
|
|
738
716
|
if (s.title !== void 0) out.title = s.title;
|
|
739
717
|
return out;
|
|
740
718
|
});
|
|
741
|
-
await this.
|
|
742
|
-
toWire({
|
|
743
|
-
jsonrpc: "2.0",
|
|
744
|
-
id,
|
|
745
|
-
result: { sessions }
|
|
746
|
-
})
|
|
747
|
-
);
|
|
719
|
+
await this.sendResult(id, { sessions });
|
|
748
720
|
return false;
|
|
749
721
|
}
|
|
750
722
|
// ────────────────────────────────────────────────────────────────────
|
|
@@ -777,7 +749,9 @@ var ACPProtocolHandler = class {
|
|
|
777
749
|
async sendNotification(params) {
|
|
778
750
|
await this.transport.send(toWire({ jsonrpc: "2.0", method: "session/update", params }));
|
|
779
751
|
}
|
|
780
|
-
|
|
752
|
+
async sendResult(id, result) {
|
|
753
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", id, result }));
|
|
754
|
+
}
|
|
781
755
|
async persist(state, history = void 0) {
|
|
782
756
|
if (!this.store) return;
|
|
783
757
|
try {
|
|
@@ -790,71 +764,10 @@ var ACPProtocolHandler = class {
|
|
|
790
764
|
if (data !== void 0) error.data = data;
|
|
791
765
|
await this.transport.send(toWire({ jsonrpc: "2.0", id, error }));
|
|
792
766
|
}
|
|
793
|
-
/**
|
|
794
|
-
* Allocate a session id (WS-015).
|
|
795
|
-
*
|
|
796
|
-
* This was `this.nextId++`, so ids were `sess_1`, `sess_2`, … — and the
|
|
797
|
-
* handler has no per-connection ownership: any caller that names a session
|
|
798
|
-
* id can `session/load`, `session/prompt`, `session/cancel` or
|
|
799
|
-
* `session/delete` it. Over stdio that is academic (one client per process),
|
|
800
|
-
* but the agent also serves over HTTP, where a guessable id is the whole
|
|
801
|
-
* authorization story for any local process or page that reaches the port.
|
|
802
|
-
*
|
|
803
|
-
* Random ids do not create ownership — they remove the trivial enumeration
|
|
804
|
-
* that made its absence exploitable. Real per-connection ownership is the
|
|
805
|
-
* larger fix and is noted in the WS-015 test file.
|
|
806
|
-
*
|
|
807
|
-
* The counter is retained: it keeps ids ordered for debugging and guarantees
|
|
808
|
-
* uniqueness within a process even in the (impossible) event of a UUID
|
|
809
|
-
* collision. The random half is what makes the id unguessable.
|
|
810
|
-
*/
|
|
811
|
-
/**
|
|
812
|
-
* Resolve a client-supplied `cwd` for a session, or `null` when it is not
|
|
813
|
-
* usable (WS-015).
|
|
814
|
-
*
|
|
815
|
-
* `session/new`, `session/load` and `session/fork` all took `params.cwd`
|
|
816
|
-
* with a single `typeof === 'string'` check and nothing else. That value is
|
|
817
|
-
* the working directory the agent then reads, writes and executes in.
|
|
818
|
-
*
|
|
819
|
-
* SCOPE, deliberately stated: this does NOT confine the session to a root.
|
|
820
|
-
* In ACP the client IS the editor and legitimately names its own workspace —
|
|
821
|
-
* Zed and JetBrains pass the project root — so a fixed boundary here would
|
|
822
|
-
* break the integration this package exists for. What it enforces is that
|
|
823
|
-
* the directory is absolute and actually exists as a directory: a relative
|
|
824
|
-
* or missing `cwd` is a bug or an attack under either reading, and silently
|
|
825
|
-
* running the agent somewhere other than where the client asked is worse
|
|
826
|
-
* than refusing. Confinement, if wanted, belongs in an operator-set option
|
|
827
|
-
* on top of this, not in place of it.
|
|
828
|
-
*/
|
|
829
|
-
async resolveSessionCwd(requested) {
|
|
830
|
-
if (!path.isAbsolute(requested)) return null;
|
|
831
|
-
const resolved = path.resolve(requested);
|
|
832
|
-
try {
|
|
833
|
-
const stat2 = await fsp.stat(resolved);
|
|
834
|
-
return stat2.isDirectory() ? resolved : null;
|
|
835
|
-
} catch {
|
|
836
|
-
return null;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
767
|
allocId() {
|
|
840
768
|
return `${this.nextId++}_${randomUUID().replaceAll("-", "")}`;
|
|
841
769
|
}
|
|
842
770
|
};
|
|
843
|
-
function errorToJsonRpc(err) {
|
|
844
|
-
if (err && typeof err === "object") {
|
|
845
|
-
const e = err;
|
|
846
|
-
if (typeof e.code === "number" && typeof e.message === "string") {
|
|
847
|
-
const result = {
|
|
848
|
-
code: e.code,
|
|
849
|
-
message: e.message
|
|
850
|
-
};
|
|
851
|
-
if (e.data !== void 0) result.data = e.data;
|
|
852
|
-
return result;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
856
|
-
return { code: -32603, message };
|
|
857
|
-
}
|
|
858
771
|
|
|
859
772
|
// src/agent/stdio-transport.ts
|
|
860
773
|
import { expectDefined, writeErr } from "@wrongstack/core/utils";
|