@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
package/dist/agent.js
CHANGED
|
@@ -286,8 +286,6 @@ function toolToPriority(tool) {
|
|
|
286
286
|
|
|
287
287
|
// src/agent/protocol-handler.ts
|
|
288
288
|
import { randomUUID } from "node:crypto";
|
|
289
|
-
import * as fsp from "node:fs/promises";
|
|
290
|
-
import * as path from "node:path";
|
|
291
289
|
|
|
292
290
|
// src/types/acp-v1.ts
|
|
293
291
|
var ACP_PROTOCOL_VERSION = 1;
|
|
@@ -313,7 +311,9 @@ function toWire(msg) {
|
|
|
313
311
|
}
|
|
314
312
|
var WRONGSTACK_VERSION = ACP_PACKAGE_VERSION;
|
|
315
313
|
|
|
316
|
-
// src/agent/protocol-
|
|
314
|
+
// src/agent/protocol-session-ops.ts
|
|
315
|
+
import * as fsp from "node:fs/promises";
|
|
316
|
+
import * as path from "node:path";
|
|
317
317
|
var WRONGSTACK_AUTH_METHODS = [
|
|
318
318
|
{
|
|
319
319
|
id: "wrongstack-auth",
|
|
@@ -332,6 +332,384 @@ var DEFAULT_MODES = [
|
|
|
332
332
|
description: "Default agent mode for code-generation tasks."
|
|
333
333
|
}
|
|
334
334
|
];
|
|
335
|
+
async function resolveSessionCwd(requested) {
|
|
336
|
+
if (!path.isAbsolute(requested)) return null;
|
|
337
|
+
const resolved = path.resolve(requested);
|
|
338
|
+
try {
|
|
339
|
+
const stat2 = await fsp.stat(resolved);
|
|
340
|
+
return stat2.isDirectory() ? resolved : null;
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function errorToJsonRpc(err) {
|
|
346
|
+
if (err && typeof err === "object") {
|
|
347
|
+
const e = err;
|
|
348
|
+
if (typeof e.code === "number" && typeof e.message === "string") {
|
|
349
|
+
const result = {
|
|
350
|
+
code: e.code,
|
|
351
|
+
message: e.message
|
|
352
|
+
};
|
|
353
|
+
if (e.data !== void 0) result.data = e.data;
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
358
|
+
return { code: -32603, message };
|
|
359
|
+
}
|
|
360
|
+
function createRunTurnApi(sessionId, clientCapabilities, request) {
|
|
361
|
+
return {
|
|
362
|
+
clientCapabilities,
|
|
363
|
+
requestPermission: async (req) => {
|
|
364
|
+
const res = await request("session/request_permission", {
|
|
365
|
+
sessionId,
|
|
366
|
+
toolCall: req.toolCall,
|
|
367
|
+
options: req.options
|
|
368
|
+
});
|
|
369
|
+
const outcome = res?.outcome;
|
|
370
|
+
return outcome ?? { outcome: "cancelled" };
|
|
371
|
+
},
|
|
372
|
+
readTextFile: async (params) => {
|
|
373
|
+
const res = await request("fs/read_text_file", { sessionId, ...params });
|
|
374
|
+
return String(res?.content ?? "");
|
|
375
|
+
},
|
|
376
|
+
writeTextFile: async (params) => {
|
|
377
|
+
await request("fs/write_text_file", { sessionId, ...params });
|
|
378
|
+
},
|
|
379
|
+
runTerminal: async ({ command, args, cwd }) => {
|
|
380
|
+
const created = await request("terminal/create", {
|
|
381
|
+
sessionId,
|
|
382
|
+
command,
|
|
383
|
+
...args ? { args } : {},
|
|
384
|
+
...cwd ? { cwd } : {}
|
|
385
|
+
});
|
|
386
|
+
const terminalId = created?.terminalId;
|
|
387
|
+
if (!terminalId) return { output: "", exitCode: null };
|
|
388
|
+
try {
|
|
389
|
+
const exit = await request("terminal/wait_for_exit", {
|
|
390
|
+
sessionId,
|
|
391
|
+
terminalId
|
|
392
|
+
});
|
|
393
|
+
const out = await request("terminal/output", { sessionId, terminalId });
|
|
394
|
+
return {
|
|
395
|
+
output: String(out?.output ?? ""),
|
|
396
|
+
exitCode: typeof exit?.exitCode === "number" ? exit.exitCode : null
|
|
397
|
+
};
|
|
398
|
+
} finally {
|
|
399
|
+
try {
|
|
400
|
+
await request("terminal/release", { sessionId, terminalId });
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function buildInitializeResult(agentName, modes, configOptions) {
|
|
408
|
+
return {
|
|
409
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
410
|
+
agentCapabilities: {
|
|
411
|
+
loadSession: true,
|
|
412
|
+
promptCapabilities: {
|
|
413
|
+
image: true,
|
|
414
|
+
audio: false,
|
|
415
|
+
embeddedContext: true
|
|
416
|
+
},
|
|
417
|
+
mcpCapabilities: {
|
|
418
|
+
http: false,
|
|
419
|
+
sse: false
|
|
420
|
+
},
|
|
421
|
+
sessionCapabilities: {
|
|
422
|
+
close: {},
|
|
423
|
+
list: {},
|
|
424
|
+
delete: {},
|
|
425
|
+
resume: {},
|
|
426
|
+
fork: {}
|
|
427
|
+
},
|
|
428
|
+
auth: {
|
|
429
|
+
logout: {}
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
agentInfo: {
|
|
433
|
+
name: agentName,
|
|
434
|
+
title: "WrongStack",
|
|
435
|
+
version: WRONGSTACK_VERSION
|
|
436
|
+
},
|
|
437
|
+
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
438
|
+
modes,
|
|
439
|
+
configOptions
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/agent/protocol-session-management.ts
|
|
444
|
+
async function handleSessionNewOp(ctx, id, params) {
|
|
445
|
+
if (ctx.sessions.size >= ctx.maxSessions) {
|
|
446
|
+
await ctx.sendError(id, -32e3, `active session limit reached (${ctx.maxSessions})`);
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
const p = params ?? {};
|
|
450
|
+
let cwd = ctx.defaultCwd;
|
|
451
|
+
if (typeof p.cwd === "string") {
|
|
452
|
+
const resolved = await resolveSessionCwd(p.cwd);
|
|
453
|
+
if (resolved === null) {
|
|
454
|
+
await ctx.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
cwd = resolved;
|
|
458
|
+
}
|
|
459
|
+
const sessionId = `sess_${ctx.allocId()}`;
|
|
460
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
461
|
+
const state = {
|
|
462
|
+
id: sessionId,
|
|
463
|
+
cwd,
|
|
464
|
+
abort: new AbortController(),
|
|
465
|
+
modeId: DEFAULT_MODE_ID,
|
|
466
|
+
createdAt: now,
|
|
467
|
+
updatedAt: now
|
|
468
|
+
};
|
|
469
|
+
ctx.sessions.set(sessionId, state);
|
|
470
|
+
ctx.onSessionNew(state);
|
|
471
|
+
await ctx.persist(state);
|
|
472
|
+
await ctx.sendNotification({
|
|
473
|
+
sessionId,
|
|
474
|
+
update: {
|
|
475
|
+
sessionUpdate: "current_mode_update",
|
|
476
|
+
modeId: ctx.modes[0]?.id ?? DEFAULT_MODE_ID
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
if (ctx.configOptions.length > 0) {
|
|
480
|
+
await ctx.sendNotification({
|
|
481
|
+
sessionId,
|
|
482
|
+
update: {
|
|
483
|
+
sessionUpdate: "config_option_update",
|
|
484
|
+
configOptions: [...ctx.configOptions]
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
await ctx.sendResult(id, {
|
|
489
|
+
sessionId,
|
|
490
|
+
modes: ctx.modes,
|
|
491
|
+
configOptions: ctx.configOptions
|
|
492
|
+
});
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
async function handleSessionLoadOp(ctx, id, params) {
|
|
496
|
+
const p = params ?? {};
|
|
497
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
498
|
+
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
499
|
+
const existing = sessionId ? ctx.sessions.get(sessionId) : void 0;
|
|
500
|
+
if (!existing && sessionId && ctx.store) {
|
|
501
|
+
const persisted = await ctx.store.load(sessionId);
|
|
502
|
+
if (persisted) {
|
|
503
|
+
if (ctx.sessions.size >= ctx.maxSessions) {
|
|
504
|
+
await ctx.sendError(id, -32e3, `active session limit reached (${ctx.maxSessions})`);
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
if (loadCwd !== void 0 && await resolveSessionCwd(loadCwd) === null) {
|
|
508
|
+
await ctx.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
const candidateCwd = persisted.cwd ?? loadCwd ?? ctx.defaultCwd;
|
|
512
|
+
const restoredCwd = await resolveSessionCwd(candidateCwd) ?? ctx.defaultCwd;
|
|
513
|
+
const restored = {
|
|
514
|
+
id: sessionId,
|
|
515
|
+
cwd: restoredCwd,
|
|
516
|
+
abort: new AbortController(),
|
|
517
|
+
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
518
|
+
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
519
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
520
|
+
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
521
|
+
};
|
|
522
|
+
ctx.sessions.set(sessionId, restored);
|
|
523
|
+
ctx.seedFor?.(sessionId, persisted.history ?? []);
|
|
524
|
+
for (const update of persisted.history ?? []) {
|
|
525
|
+
await ctx.sendNotification({ sessionId, update });
|
|
526
|
+
}
|
|
527
|
+
await ctx.sendNotification({
|
|
528
|
+
sessionId,
|
|
529
|
+
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
530
|
+
});
|
|
531
|
+
await ctx.sendResult(id, {
|
|
532
|
+
initialMode: { currentModeId: restored.modeId, availableModes: ctx.modes }
|
|
533
|
+
});
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (existing) {
|
|
538
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
539
|
+
const replay = ctx.replayFor?.(sessionId);
|
|
540
|
+
if (replay) {
|
|
541
|
+
for (const update of replay) {
|
|
542
|
+
await ctx.sendNotification({ sessionId, update });
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
await ctx.sendNotification({
|
|
546
|
+
sessionId,
|
|
547
|
+
update: {
|
|
548
|
+
sessionUpdate: "session_info_update",
|
|
549
|
+
updatedAt: existing.updatedAt
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
await ctx.sendNotification({
|
|
553
|
+
sessionId,
|
|
554
|
+
update: {
|
|
555
|
+
sessionUpdate: "current_mode_update",
|
|
556
|
+
modeId: existing.modeId
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
await ctx.sendResult(id, {
|
|
560
|
+
initialMode: {
|
|
561
|
+
currentModeId: existing.modeId,
|
|
562
|
+
availableModes: ctx.modes
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
await ctx.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
async function handleSessionForkOp(ctx, id, params) {
|
|
571
|
+
const p = params ?? {};
|
|
572
|
+
const sourceId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
573
|
+
const source = sourceId ? ctx.sessions.get(sourceId) : void 0;
|
|
574
|
+
if (!sourceId || !source) {
|
|
575
|
+
await ctx.sendError(id, -32e3, `session not found: ${sourceId}`);
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
if (ctx.sessions.size >= ctx.maxSessions) {
|
|
579
|
+
await ctx.sendError(id, -32e3, `active session limit reached (${ctx.maxSessions})`);
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
let forkCwd = source.cwd;
|
|
583
|
+
if (typeof p.cwd === "string") {
|
|
584
|
+
const resolved = await resolveSessionCwd(p.cwd);
|
|
585
|
+
if (resolved === null) {
|
|
586
|
+
await ctx.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
forkCwd = resolved;
|
|
590
|
+
}
|
|
591
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
592
|
+
const sessionId = `sess_${ctx.allocId()}`;
|
|
593
|
+
const forked = {
|
|
594
|
+
id: sessionId,
|
|
595
|
+
cwd: forkCwd,
|
|
596
|
+
abort: new AbortController(),
|
|
597
|
+
modeId: source.modeId,
|
|
598
|
+
createdAt: now,
|
|
599
|
+
updatedAt: now,
|
|
600
|
+
...source.title !== void 0 ? { title: source.title } : {}
|
|
601
|
+
};
|
|
602
|
+
const history = (ctx.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
603
|
+
sessionUpdate: update.sessionUpdate,
|
|
604
|
+
content: structuredClone(update.content)
|
|
605
|
+
}));
|
|
606
|
+
ctx.sessions.set(sessionId, forked);
|
|
607
|
+
ctx.seedFor?.(sessionId, history);
|
|
608
|
+
ctx.onSessionNew(forked);
|
|
609
|
+
await ctx.persist(forked, history);
|
|
610
|
+
await ctx.sendNotification({
|
|
611
|
+
sessionId,
|
|
612
|
+
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
613
|
+
});
|
|
614
|
+
await ctx.sendResult(id, {
|
|
615
|
+
sessionId,
|
|
616
|
+
modes: ctx.modes,
|
|
617
|
+
configOptions: ctx.configOptions
|
|
618
|
+
});
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
async function handleSessionPromptOp(ctx, id, params) {
|
|
622
|
+
const p = params ?? {};
|
|
623
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
624
|
+
if (!sessionId || !ctx.sessions.has(sessionId)) {
|
|
625
|
+
await ctx.sendError(id, -32e3, "unknown or missing sessionId");
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
if (!Array.isArray(p.prompt)) {
|
|
629
|
+
await ctx.sendError(id, -32602, "prompt must be an array of content blocks");
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
const session = ctx.sessions.get(sessionId);
|
|
633
|
+
if (session.abort.signal.aborted) {
|
|
634
|
+
session.abort = new AbortController();
|
|
635
|
+
}
|
|
636
|
+
const turnSignal = new AbortController();
|
|
637
|
+
const onCancel = () => turnSignal.abort();
|
|
638
|
+
session.abort.signal.addEventListener("abort", onCancel, { once: true });
|
|
639
|
+
const api = createRunTurnApi(
|
|
640
|
+
sessionId,
|
|
641
|
+
ctx.clientCapabilities ?? {},
|
|
642
|
+
(method, req) => ctx.request(method, req)
|
|
643
|
+
);
|
|
644
|
+
let result;
|
|
645
|
+
const pendingNotifications = [];
|
|
646
|
+
const emit = (update) => {
|
|
647
|
+
const notifPromise = ctx.sendNotification({ sessionId, update });
|
|
648
|
+
pendingNotifications.push(notifPromise.catch(() => {
|
|
649
|
+
}));
|
|
650
|
+
};
|
|
651
|
+
try {
|
|
652
|
+
result = await ctx.runTurn(
|
|
653
|
+
{ sessionId, prompt: p.prompt, signal: turnSignal.signal },
|
|
654
|
+
emit,
|
|
655
|
+
api
|
|
656
|
+
);
|
|
657
|
+
} catch (err) {
|
|
658
|
+
session.abort.signal.removeEventListener("abort", onCancel);
|
|
659
|
+
const { code, message, data } = errorToJsonRpc(err);
|
|
660
|
+
await ctx.sendError(id, code, message, data);
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
await Promise.all(pendingNotifications);
|
|
664
|
+
session.abort.signal.removeEventListener("abort", onCancel);
|
|
665
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
666
|
+
await ctx.persist(session);
|
|
667
|
+
await ctx.sendResult(id, { stopReason: result.stopReason });
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
async function handleSetModeOp(ctx, id, params) {
|
|
671
|
+
const p = params ?? {};
|
|
672
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
673
|
+
const modeId = typeof p.modeId === "string" ? p.modeId : null;
|
|
674
|
+
const session = sessionId ? ctx.sessions.get(sessionId) : void 0;
|
|
675
|
+
if (!session || !modeId || !ctx.modes.some((m) => m.id === modeId)) {
|
|
676
|
+
await ctx.sendError(id, -32602, "invalid sessionId or modeId");
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
session.modeId = modeId;
|
|
680
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
681
|
+
await ctx.sendNotification({
|
|
682
|
+
sessionId,
|
|
683
|
+
update: { sessionUpdate: "current_mode_update", modeId }
|
|
684
|
+
});
|
|
685
|
+
await ctx.sendResult(id, {});
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
async function handleSetConfigOptionOp(ctx, id, params) {
|
|
689
|
+
const p = params ?? {};
|
|
690
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
691
|
+
const optionId = typeof p.configId === "string" ? p.configId : null;
|
|
692
|
+
const value = typeof p.value === "string" ? p.value : null;
|
|
693
|
+
const session = sessionId ? ctx.sessions.get(sessionId) : void 0;
|
|
694
|
+
const option = optionId ? ctx.configOptions.find((o) => o.id === optionId) : void 0;
|
|
695
|
+
if (!session || !option || value === null || !option.options.some((o) => o.value === value)) {
|
|
696
|
+
await ctx.sendError(id, -32602, "invalid sessionId, configId, or value");
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
option.currentValue = value;
|
|
700
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
701
|
+
await ctx.sendNotification({
|
|
702
|
+
sessionId,
|
|
703
|
+
update: {
|
|
704
|
+
sessionUpdate: "config_option_update",
|
|
705
|
+
configOptions: [...ctx.configOptions]
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
await ctx.sendResult(id, { configOptions: [...ctx.configOptions] });
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/agent/protocol-handler.ts
|
|
335
713
|
var ACPProtocolHandler = class {
|
|
336
714
|
transport;
|
|
337
715
|
defaultCwd;
|
|
@@ -440,6 +818,27 @@ var ACPProtocolHandler = class {
|
|
|
440
818
|
} catch {
|
|
441
819
|
}
|
|
442
820
|
}
|
|
821
|
+
sessionContext() {
|
|
822
|
+
return {
|
|
823
|
+
sessions: this.sessions,
|
|
824
|
+
maxSessions: this.maxSessions,
|
|
825
|
+
defaultCwd: this.defaultCwd,
|
|
826
|
+
modes: this.modes,
|
|
827
|
+
configOptions: this.configOptions,
|
|
828
|
+
store: this.store,
|
|
829
|
+
replayFor: this.replayFor,
|
|
830
|
+
seedFor: this.seedFor,
|
|
831
|
+
onSessionNew: this.onSessionNew,
|
|
832
|
+
allocId: () => this.allocId(),
|
|
833
|
+
persist: (state, history) => this.persist(state, history),
|
|
834
|
+
sendNotification: (params) => this.sendNotification(params),
|
|
835
|
+
sendError: (id, code, message, data) => this.sendError(id, code, message, data),
|
|
836
|
+
sendResult: (id, result) => this.sendResult(id, result),
|
|
837
|
+
request: (method, params, timeoutMs) => this.request(method, params, timeoutMs),
|
|
838
|
+
runTurn: this.runTurn,
|
|
839
|
+
clientCapabilities: this.clientCapabilities
|
|
840
|
+
};
|
|
841
|
+
}
|
|
443
842
|
// ────────────────────────────────────────────────────────────────────
|
|
444
843
|
// Requests
|
|
445
844
|
// ────────────────────────────────────────────────────────────────────
|
|
@@ -457,9 +856,9 @@ var ACPProtocolHandler = class {
|
|
|
457
856
|
case "logout":
|
|
458
857
|
return await this.handleLogout(id, params);
|
|
459
858
|
case "session/new":
|
|
460
|
-
return await this.
|
|
859
|
+
return await handleSessionNewOp(this.sessionContext(), id, params);
|
|
461
860
|
case "session/load":
|
|
462
|
-
return await this.
|
|
861
|
+
return await handleSessionLoadOp(this.sessionContext(), id, params);
|
|
463
862
|
case "session/resume":
|
|
464
863
|
return await this.handleSessionResume(id, params);
|
|
465
864
|
case "session/close":
|
|
@@ -467,15 +866,15 @@ var ACPProtocolHandler = class {
|
|
|
467
866
|
case "session/delete":
|
|
468
867
|
return await this.handleSessionDelete(id, params);
|
|
469
868
|
case "session/prompt":
|
|
470
|
-
return await this.
|
|
869
|
+
return await handleSessionPromptOp(this.sessionContext(), id, params);
|
|
471
870
|
case "session/set_mode":
|
|
472
|
-
return await this.
|
|
871
|
+
return await handleSetModeOp(this.sessionContext(), id, params);
|
|
473
872
|
case "session/set_config_option":
|
|
474
|
-
return await this.
|
|
873
|
+
return await handleSetConfigOptionOp(this.sessionContext(), id, params);
|
|
475
874
|
case "session/list":
|
|
476
875
|
return await this.handleSessionList(id);
|
|
477
876
|
case "session/fork":
|
|
478
|
-
return await this.
|
|
877
|
+
return await handleSessionForkOp(this.sessionContext(), id, params);
|
|
479
878
|
case "providers/list":
|
|
480
879
|
return await this.handleProvidersList(id, params);
|
|
481
880
|
case "providers/set":
|
|
@@ -500,232 +899,32 @@ var ACPProtocolHandler = class {
|
|
|
500
899
|
this.clientCapabilities = p.clientCapabilities;
|
|
501
900
|
}
|
|
502
901
|
this.initialized = true;
|
|
503
|
-
await this.
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
id,
|
|
507
|
-
result: {
|
|
508
|
-
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
509
|
-
agentCapabilities: {
|
|
510
|
-
loadSession: true,
|
|
511
|
-
promptCapabilities: {
|
|
512
|
-
// We route ACP image blocks into the core agent's multimodal
|
|
513
|
-
// input (server-agent-turn.promptToAgentInput); whether the
|
|
514
|
-
// model can see them is the configured provider's concern.
|
|
515
|
-
image: true,
|
|
516
|
-
audio: false,
|
|
517
|
-
embeddedContext: true
|
|
518
|
-
},
|
|
519
|
-
mcpCapabilities: {
|
|
520
|
-
http: false,
|
|
521
|
-
sse: false
|
|
522
|
-
},
|
|
523
|
-
sessionCapabilities: {
|
|
524
|
-
close: {},
|
|
525
|
-
list: {},
|
|
526
|
-
delete: {},
|
|
527
|
-
resume: {},
|
|
528
|
-
fork: {}
|
|
529
|
-
},
|
|
530
|
-
auth: {
|
|
531
|
-
logout: {}
|
|
532
|
-
}
|
|
533
|
-
},
|
|
534
|
-
agentInfo: {
|
|
535
|
-
name: this.agentName,
|
|
536
|
-
title: "WrongStack",
|
|
537
|
-
version: WRONGSTACK_VERSION
|
|
538
|
-
},
|
|
539
|
-
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
540
|
-
modes: this.modes,
|
|
541
|
-
configOptions: this.configOptions
|
|
542
|
-
}
|
|
543
|
-
})
|
|
902
|
+
await this.sendResult(
|
|
903
|
+
id,
|
|
904
|
+
buildInitializeResult(this.agentName, this.modes, this.configOptions)
|
|
544
905
|
);
|
|
545
906
|
return false;
|
|
546
907
|
}
|
|
547
908
|
async handleAuthenticate(id, _params) {
|
|
548
|
-
await this.
|
|
549
|
-
toWire({
|
|
550
|
-
jsonrpc: "2.0",
|
|
551
|
-
id,
|
|
552
|
-
result: { outcome: "unauthenticated" }
|
|
553
|
-
})
|
|
554
|
-
);
|
|
909
|
+
await this.sendResult(id, { outcome: "unauthenticated" });
|
|
555
910
|
return false;
|
|
556
911
|
}
|
|
557
912
|
async handleLogout(id, _params) {
|
|
558
|
-
await this.
|
|
559
|
-
toWire({
|
|
560
|
-
jsonrpc: "2.0",
|
|
561
|
-
id,
|
|
562
|
-
result: {}
|
|
563
|
-
})
|
|
564
|
-
);
|
|
913
|
+
await this.sendResult(id, {});
|
|
565
914
|
return false;
|
|
566
915
|
}
|
|
567
|
-
async
|
|
568
|
-
if (this.sessions.size >= this.maxSessions) {
|
|
569
|
-
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
570
|
-
return false;
|
|
571
|
-
}
|
|
572
|
-
const p = params ?? {};
|
|
573
|
-
let cwd = this.defaultCwd;
|
|
574
|
-
if (typeof p.cwd === "string") {
|
|
575
|
-
const resolved = await this.resolveSessionCwd(p.cwd);
|
|
576
|
-
if (resolved === null) {
|
|
577
|
-
await this.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
578
|
-
return false;
|
|
579
|
-
}
|
|
580
|
-
cwd = resolved;
|
|
581
|
-
}
|
|
582
|
-
const sessionId = `sess_${this.allocId()}`;
|
|
583
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
584
|
-
const state = {
|
|
585
|
-
id: sessionId,
|
|
586
|
-
cwd,
|
|
587
|
-
abort: new AbortController(),
|
|
588
|
-
modeId: DEFAULT_MODE_ID,
|
|
589
|
-
createdAt: now,
|
|
590
|
-
updatedAt: now
|
|
591
|
-
};
|
|
592
|
-
this.sessions.set(sessionId, state);
|
|
593
|
-
this.onSessionNew(state);
|
|
594
|
-
await this.persist(state);
|
|
595
|
-
await this.sendNotification({
|
|
596
|
-
sessionId,
|
|
597
|
-
update: {
|
|
598
|
-
sessionUpdate: "current_mode_update",
|
|
599
|
-
modeId: this.modes[0]?.id ?? DEFAULT_MODE_ID
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
if (this.configOptions.length > 0) {
|
|
603
|
-
await this.sendNotification({
|
|
604
|
-
sessionId,
|
|
605
|
-
update: {
|
|
606
|
-
sessionUpdate: "config_option_update",
|
|
607
|
-
configOptions: [...this.configOptions]
|
|
608
|
-
}
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
await this.transport.send(
|
|
612
|
-
toWire({
|
|
613
|
-
jsonrpc: "2.0",
|
|
614
|
-
id,
|
|
615
|
-
result: {
|
|
616
|
-
sessionId,
|
|
617
|
-
modes: this.modes,
|
|
618
|
-
configOptions: this.configOptions
|
|
619
|
-
}
|
|
620
|
-
})
|
|
621
|
-
);
|
|
622
|
-
return false;
|
|
623
|
-
}
|
|
624
|
-
async handleSessionLoad(id, params) {
|
|
916
|
+
async handleSessionResume(id, params) {
|
|
625
917
|
const p = params ?? {};
|
|
626
918
|
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
627
|
-
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
628
919
|
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
629
|
-
if (!existing && sessionId && this.store) {
|
|
630
|
-
const persisted = await this.store.load(sessionId);
|
|
631
|
-
if (persisted) {
|
|
632
|
-
if (this.sessions.size >= this.maxSessions) {
|
|
633
|
-
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
634
|
-
return false;
|
|
635
|
-
}
|
|
636
|
-
if (loadCwd !== void 0 && await this.resolveSessionCwd(loadCwd) === null) {
|
|
637
|
-
await this.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
638
|
-
return false;
|
|
639
|
-
}
|
|
640
|
-
const candidateCwd = persisted.cwd ?? loadCwd ?? this.defaultCwd;
|
|
641
|
-
const restoredCwd = await this.resolveSessionCwd(candidateCwd) ?? this.defaultCwd;
|
|
642
|
-
const restored = {
|
|
643
|
-
id: sessionId,
|
|
644
|
-
cwd: restoredCwd,
|
|
645
|
-
abort: new AbortController(),
|
|
646
|
-
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
647
|
-
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
648
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
649
|
-
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
650
|
-
};
|
|
651
|
-
this.sessions.set(sessionId, restored);
|
|
652
|
-
this.seedFor?.(sessionId, persisted.history ?? []);
|
|
653
|
-
for (const update of persisted.history ?? []) {
|
|
654
|
-
await this.sendNotification({ sessionId, update });
|
|
655
|
-
}
|
|
656
|
-
await this.sendNotification({
|
|
657
|
-
sessionId,
|
|
658
|
-
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
659
|
-
});
|
|
660
|
-
await this.transport.send(
|
|
661
|
-
toWire({
|
|
662
|
-
jsonrpc: "2.0",
|
|
663
|
-
id,
|
|
664
|
-
result: {
|
|
665
|
-
initialMode: { currentModeId: restored.modeId, availableModes: this.modes }
|
|
666
|
-
}
|
|
667
|
-
})
|
|
668
|
-
);
|
|
669
|
-
return false;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
920
|
if (existing) {
|
|
673
921
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
await this.sendNotification({
|
|
681
|
-
sessionId,
|
|
682
|
-
update: {
|
|
683
|
-
sessionUpdate: "session_info_update",
|
|
684
|
-
updatedAt: existing.updatedAt
|
|
685
|
-
}
|
|
686
|
-
});
|
|
687
|
-
await this.sendNotification({
|
|
688
|
-
sessionId,
|
|
689
|
-
update: {
|
|
690
|
-
sessionUpdate: "current_mode_update",
|
|
691
|
-
modeId: existing.modeId
|
|
922
|
+
await this.sendResult(id, {
|
|
923
|
+
initialMode: {
|
|
924
|
+
currentModeId: existing.modeId,
|
|
925
|
+
availableModes: this.modes
|
|
692
926
|
}
|
|
693
927
|
});
|
|
694
|
-
await this.transport.send(
|
|
695
|
-
toWire({
|
|
696
|
-
jsonrpc: "2.0",
|
|
697
|
-
id,
|
|
698
|
-
result: {
|
|
699
|
-
initialMode: {
|
|
700
|
-
currentModeId: existing.modeId,
|
|
701
|
-
availableModes: this.modes
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
})
|
|
705
|
-
);
|
|
706
|
-
return false;
|
|
707
|
-
}
|
|
708
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
709
|
-
return false;
|
|
710
|
-
}
|
|
711
|
-
async handleSessionResume(id, params) {
|
|
712
|
-
const p = params ?? {};
|
|
713
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
714
|
-
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
715
|
-
if (existing) {
|
|
716
|
-
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
717
|
-
await this.transport.send(
|
|
718
|
-
toWire({
|
|
719
|
-
jsonrpc: "2.0",
|
|
720
|
-
id,
|
|
721
|
-
result: {
|
|
722
|
-
initialMode: {
|
|
723
|
-
currentModeId: existing.modeId,
|
|
724
|
-
availableModes: this.modes
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
})
|
|
728
|
-
);
|
|
729
928
|
return false;
|
|
730
929
|
}
|
|
731
930
|
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
@@ -742,13 +941,7 @@ var ACPProtocolHandler = class {
|
|
|
742
941
|
session.abort.abort();
|
|
743
942
|
this.sessions.delete(sessionId);
|
|
744
943
|
this.disposeSession(sessionId);
|
|
745
|
-
await this.
|
|
746
|
-
toWire({
|
|
747
|
-
jsonrpc: "2.0",
|
|
748
|
-
id,
|
|
749
|
-
result: {}
|
|
750
|
-
})
|
|
751
|
-
);
|
|
944
|
+
await this.sendResult(id, {});
|
|
752
945
|
return false;
|
|
753
946
|
}
|
|
754
947
|
async handleSessionDelete(id, params) {
|
|
@@ -759,92 +952,21 @@ var ACPProtocolHandler = class {
|
|
|
759
952
|
return false;
|
|
760
953
|
}
|
|
761
954
|
if (!this.sessions.has(sessionId)) {
|
|
762
|
-
await this.
|
|
763
|
-
toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } })
|
|
764
|
-
);
|
|
955
|
+
await this.sendResult(id, { configOptions: [...this.configOptions] });
|
|
765
956
|
return false;
|
|
766
957
|
}
|
|
767
958
|
const session = this.sessions.get(sessionId);
|
|
768
959
|
session.abort.abort();
|
|
769
960
|
this.sessions.delete(sessionId);
|
|
770
961
|
this.disposeSession(sessionId);
|
|
771
|
-
await this.
|
|
772
|
-
toWire({
|
|
773
|
-
jsonrpc: "2.0",
|
|
774
|
-
id,
|
|
775
|
-
result: {}
|
|
776
|
-
})
|
|
777
|
-
);
|
|
778
|
-
return false;
|
|
779
|
-
}
|
|
780
|
-
async handleSessionFork(id, params) {
|
|
781
|
-
const p = params ?? {};
|
|
782
|
-
const sourceId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
783
|
-
const source = sourceId ? this.sessions.get(sourceId) : void 0;
|
|
784
|
-
if (!sourceId || !source) {
|
|
785
|
-
await this.sendError(id, -32e3, `session not found: ${sourceId}`);
|
|
786
|
-
return false;
|
|
787
|
-
}
|
|
788
|
-
if (this.sessions.size >= this.maxSessions) {
|
|
789
|
-
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
790
|
-
return false;
|
|
791
|
-
}
|
|
792
|
-
let forkCwd = source.cwd;
|
|
793
|
-
if (typeof p.cwd === "string") {
|
|
794
|
-
const resolved = await this.resolveSessionCwd(p.cwd);
|
|
795
|
-
if (resolved === null) {
|
|
796
|
-
await this.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
797
|
-
return false;
|
|
798
|
-
}
|
|
799
|
-
forkCwd = resolved;
|
|
800
|
-
}
|
|
801
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
802
|
-
const sessionId = `sess_${this.allocId()}`;
|
|
803
|
-
const forked = {
|
|
804
|
-
id: sessionId,
|
|
805
|
-
cwd: forkCwd,
|
|
806
|
-
abort: new AbortController(),
|
|
807
|
-
modeId: source.modeId,
|
|
808
|
-
createdAt: now,
|
|
809
|
-
updatedAt: now,
|
|
810
|
-
...source.title !== void 0 ? { title: source.title } : {}
|
|
811
|
-
};
|
|
812
|
-
const history = (this.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
813
|
-
sessionUpdate: update.sessionUpdate,
|
|
814
|
-
content: structuredClone(update.content)
|
|
815
|
-
}));
|
|
816
|
-
this.sessions.set(sessionId, forked);
|
|
817
|
-
this.seedFor?.(sessionId, history);
|
|
818
|
-
this.onSessionNew(forked);
|
|
819
|
-
await this.persist(forked, history);
|
|
820
|
-
await this.sendNotification({
|
|
821
|
-
sessionId,
|
|
822
|
-
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
823
|
-
});
|
|
824
|
-
await this.transport.send(
|
|
825
|
-
toWire({
|
|
826
|
-
jsonrpc: "2.0",
|
|
827
|
-
id,
|
|
828
|
-
result: {
|
|
829
|
-
sessionId,
|
|
830
|
-
modes: this.modes,
|
|
831
|
-
configOptions: this.configOptions
|
|
832
|
-
}
|
|
833
|
-
})
|
|
834
|
-
);
|
|
962
|
+
await this.sendResult(id, {});
|
|
835
963
|
return false;
|
|
836
964
|
}
|
|
837
965
|
async handleProvidersList(id, _params) {
|
|
838
|
-
await this.
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
result: {
|
|
843
|
-
providers: [],
|
|
844
|
-
currentProviderId: null
|
|
845
|
-
}
|
|
846
|
-
})
|
|
847
|
-
);
|
|
966
|
+
await this.sendResult(id, {
|
|
967
|
+
providers: [],
|
|
968
|
+
currentProviderId: null
|
|
969
|
+
});
|
|
848
970
|
return false;
|
|
849
971
|
}
|
|
850
972
|
async handleProvidersSet(id, _params) {
|
|
@@ -856,157 +978,13 @@ var ACPProtocolHandler = class {
|
|
|
856
978
|
return false;
|
|
857
979
|
}
|
|
858
980
|
async handleProvidersDisable(id, _params) {
|
|
859
|
-
await this.
|
|
860
|
-
toWire({
|
|
861
|
-
jsonrpc: "2.0",
|
|
862
|
-
id,
|
|
863
|
-
result: {}
|
|
864
|
-
})
|
|
865
|
-
);
|
|
981
|
+
await this.sendResult(id, {});
|
|
866
982
|
return false;
|
|
867
983
|
}
|
|
868
984
|
async handleMcpMessage(id, _params) {
|
|
869
985
|
await this.sendError(id, -32e3, "MCP message routing not available through ACP");
|
|
870
986
|
return false;
|
|
871
987
|
}
|
|
872
|
-
async handleSessionPrompt(id, params) {
|
|
873
|
-
const p = params ?? {};
|
|
874
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
875
|
-
if (!sessionId || !this.sessions.has(sessionId)) {
|
|
876
|
-
await this.sendError(id, -32e3, "unknown or missing sessionId");
|
|
877
|
-
return false;
|
|
878
|
-
}
|
|
879
|
-
if (!Array.isArray(p.prompt)) {
|
|
880
|
-
await this.sendError(id, -32602, "prompt must be an array of content blocks");
|
|
881
|
-
return false;
|
|
882
|
-
}
|
|
883
|
-
const session = this.sessions.get(sessionId);
|
|
884
|
-
if (session.abort.signal.aborted) {
|
|
885
|
-
session.abort = new AbortController();
|
|
886
|
-
}
|
|
887
|
-
const turnSignal = new AbortController();
|
|
888
|
-
const onCancel = () => turnSignal.abort();
|
|
889
|
-
session.abort.signal.addEventListener("abort", onCancel, { once: true });
|
|
890
|
-
const api = {
|
|
891
|
-
clientCapabilities: this.clientCapabilities,
|
|
892
|
-
requestPermission: async (req) => {
|
|
893
|
-
const res = await this.request("session/request_permission", {
|
|
894
|
-
sessionId,
|
|
895
|
-
toolCall: req.toolCall,
|
|
896
|
-
options: req.options
|
|
897
|
-
});
|
|
898
|
-
const outcome = res?.outcome;
|
|
899
|
-
return outcome ?? { outcome: "cancelled" };
|
|
900
|
-
},
|
|
901
|
-
readTextFile: async (params2) => {
|
|
902
|
-
const res = await this.request("fs/read_text_file", { sessionId, ...params2 });
|
|
903
|
-
return String(res?.content ?? "");
|
|
904
|
-
},
|
|
905
|
-
writeTextFile: async (params2) => {
|
|
906
|
-
await this.request("fs/write_text_file", { sessionId, ...params2 });
|
|
907
|
-
},
|
|
908
|
-
runTerminal: async ({ command, args, cwd }) => {
|
|
909
|
-
const created = await this.request("terminal/create", {
|
|
910
|
-
sessionId,
|
|
911
|
-
command,
|
|
912
|
-
...args ? { args } : {},
|
|
913
|
-
...cwd ? { cwd } : {}
|
|
914
|
-
});
|
|
915
|
-
const terminalId = created?.terminalId;
|
|
916
|
-
if (!terminalId) return { output: "", exitCode: null };
|
|
917
|
-
try {
|
|
918
|
-
const exit = await this.request("terminal/wait_for_exit", {
|
|
919
|
-
sessionId,
|
|
920
|
-
terminalId
|
|
921
|
-
});
|
|
922
|
-
const out = await this.request("terminal/output", { sessionId, terminalId });
|
|
923
|
-
return {
|
|
924
|
-
output: String(out?.output ?? ""),
|
|
925
|
-
exitCode: typeof exit?.exitCode === "number" ? exit.exitCode : null
|
|
926
|
-
};
|
|
927
|
-
} finally {
|
|
928
|
-
try {
|
|
929
|
-
await this.request("terminal/release", { sessionId, terminalId });
|
|
930
|
-
} catch {
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
};
|
|
935
|
-
let result;
|
|
936
|
-
const pendingNotifications = [];
|
|
937
|
-
const emit = (update) => {
|
|
938
|
-
const p2 = this.sendNotification({ sessionId, update });
|
|
939
|
-
pendingNotifications.push(p2.catch(() => {
|
|
940
|
-
}));
|
|
941
|
-
};
|
|
942
|
-
try {
|
|
943
|
-
result = await this.runTurn(
|
|
944
|
-
{ sessionId, prompt: p.prompt, signal: turnSignal.signal },
|
|
945
|
-
emit,
|
|
946
|
-
api
|
|
947
|
-
);
|
|
948
|
-
} catch (err) {
|
|
949
|
-
session.abort.signal.removeEventListener("abort", onCancel);
|
|
950
|
-
const { code, message, data } = errorToJsonRpc(err);
|
|
951
|
-
await this.sendError(id, code, message, data);
|
|
952
|
-
return false;
|
|
953
|
-
}
|
|
954
|
-
await Promise.all(pendingNotifications);
|
|
955
|
-
session.abort.signal.removeEventListener("abort", onCancel);
|
|
956
|
-
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
957
|
-
await this.persist(session);
|
|
958
|
-
await this.transport.send(
|
|
959
|
-
toWire({
|
|
960
|
-
jsonrpc: "2.0",
|
|
961
|
-
id,
|
|
962
|
-
result: { stopReason: result.stopReason }
|
|
963
|
-
})
|
|
964
|
-
);
|
|
965
|
-
return false;
|
|
966
|
-
}
|
|
967
|
-
async handleSetMode(id, params) {
|
|
968
|
-
const p = params ?? {};
|
|
969
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
970
|
-
const modeId = typeof p.modeId === "string" ? p.modeId : null;
|
|
971
|
-
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
972
|
-
if (!session || !modeId || !this.modes.some((m) => m.id === modeId)) {
|
|
973
|
-
await this.sendError(id, -32602, "invalid sessionId or modeId");
|
|
974
|
-
return false;
|
|
975
|
-
}
|
|
976
|
-
session.modeId = modeId;
|
|
977
|
-
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
978
|
-
await this.sendNotification({
|
|
979
|
-
sessionId,
|
|
980
|
-
update: { sessionUpdate: "current_mode_update", modeId }
|
|
981
|
-
});
|
|
982
|
-
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: {} }));
|
|
983
|
-
return false;
|
|
984
|
-
}
|
|
985
|
-
async handleSetConfigOption(id, params) {
|
|
986
|
-
const p = params ?? {};
|
|
987
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
988
|
-
const optionId = typeof p.configId === "string" ? p.configId : null;
|
|
989
|
-
const value = typeof p.value === "string" ? p.value : null;
|
|
990
|
-
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
991
|
-
const option = optionId ? this.configOptions.find((o) => o.id === optionId) : void 0;
|
|
992
|
-
if (!session || !option || value === null || !option.options.some((o) => o.value === value)) {
|
|
993
|
-
await this.sendError(id, -32602, "invalid sessionId, configId, or value");
|
|
994
|
-
return false;
|
|
995
|
-
}
|
|
996
|
-
option.currentValue = value;
|
|
997
|
-
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
998
|
-
await this.sendNotification({
|
|
999
|
-
sessionId,
|
|
1000
|
-
update: {
|
|
1001
|
-
sessionUpdate: "config_option_update",
|
|
1002
|
-
configOptions: [...this.configOptions]
|
|
1003
|
-
}
|
|
1004
|
-
});
|
|
1005
|
-
await this.transport.send(
|
|
1006
|
-
toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } })
|
|
1007
|
-
);
|
|
1008
|
-
return false;
|
|
1009
|
-
}
|
|
1010
988
|
async handleSessionList(id) {
|
|
1011
989
|
const sessions = Array.from(this.sessions.values()).map((s) => {
|
|
1012
990
|
const out = {
|
|
@@ -1017,13 +995,7 @@ var ACPProtocolHandler = class {
|
|
|
1017
995
|
if (s.title !== void 0) out.title = s.title;
|
|
1018
996
|
return out;
|
|
1019
997
|
});
|
|
1020
|
-
await this.
|
|
1021
|
-
toWire({
|
|
1022
|
-
jsonrpc: "2.0",
|
|
1023
|
-
id,
|
|
1024
|
-
result: { sessions }
|
|
1025
|
-
})
|
|
1026
|
-
);
|
|
998
|
+
await this.sendResult(id, { sessions });
|
|
1027
999
|
return false;
|
|
1028
1000
|
}
|
|
1029
1001
|
// ────────────────────────────────────────────────────────────────────
|
|
@@ -1056,7 +1028,9 @@ var ACPProtocolHandler = class {
|
|
|
1056
1028
|
async sendNotification(params) {
|
|
1057
1029
|
await this.transport.send(toWire({ jsonrpc: "2.0", method: "session/update", params }));
|
|
1058
1030
|
}
|
|
1059
|
-
|
|
1031
|
+
async sendResult(id, result) {
|
|
1032
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", id, result }));
|
|
1033
|
+
}
|
|
1060
1034
|
async persist(state, history = void 0) {
|
|
1061
1035
|
if (!this.store) return;
|
|
1062
1036
|
try {
|
|
@@ -1069,71 +1043,10 @@ var ACPProtocolHandler = class {
|
|
|
1069
1043
|
if (data !== void 0) error.data = data;
|
|
1070
1044
|
await this.transport.send(toWire({ jsonrpc: "2.0", id, error }));
|
|
1071
1045
|
}
|
|
1072
|
-
/**
|
|
1073
|
-
* Allocate a session id (WS-015).
|
|
1074
|
-
*
|
|
1075
|
-
* This was `this.nextId++`, so ids were `sess_1`, `sess_2`, … — and the
|
|
1076
|
-
* handler has no per-connection ownership: any caller that names a session
|
|
1077
|
-
* id can `session/load`, `session/prompt`, `session/cancel` or
|
|
1078
|
-
* `session/delete` it. Over stdio that is academic (one client per process),
|
|
1079
|
-
* but the agent also serves over HTTP, where a guessable id is the whole
|
|
1080
|
-
* authorization story for any local process or page that reaches the port.
|
|
1081
|
-
*
|
|
1082
|
-
* Random ids do not create ownership — they remove the trivial enumeration
|
|
1083
|
-
* that made its absence exploitable. Real per-connection ownership is the
|
|
1084
|
-
* larger fix and is noted in the WS-015 test file.
|
|
1085
|
-
*
|
|
1086
|
-
* The counter is retained: it keeps ids ordered for debugging and guarantees
|
|
1087
|
-
* uniqueness within a process even in the (impossible) event of a UUID
|
|
1088
|
-
* collision. The random half is what makes the id unguessable.
|
|
1089
|
-
*/
|
|
1090
|
-
/**
|
|
1091
|
-
* Resolve a client-supplied `cwd` for a session, or `null` when it is not
|
|
1092
|
-
* usable (WS-015).
|
|
1093
|
-
*
|
|
1094
|
-
* `session/new`, `session/load` and `session/fork` all took `params.cwd`
|
|
1095
|
-
* with a single `typeof === 'string'` check and nothing else. That value is
|
|
1096
|
-
* the working directory the agent then reads, writes and executes in.
|
|
1097
|
-
*
|
|
1098
|
-
* SCOPE, deliberately stated: this does NOT confine the session to a root.
|
|
1099
|
-
* In ACP the client IS the editor and legitimately names its own workspace —
|
|
1100
|
-
* Zed and JetBrains pass the project root — so a fixed boundary here would
|
|
1101
|
-
* break the integration this package exists for. What it enforces is that
|
|
1102
|
-
* the directory is absolute and actually exists as a directory: a relative
|
|
1103
|
-
* or missing `cwd` is a bug or an attack under either reading, and silently
|
|
1104
|
-
* running the agent somewhere other than where the client asked is worse
|
|
1105
|
-
* than refusing. Confinement, if wanted, belongs in an operator-set option
|
|
1106
|
-
* on top of this, not in place of it.
|
|
1107
|
-
*/
|
|
1108
|
-
async resolveSessionCwd(requested) {
|
|
1109
|
-
if (!path.isAbsolute(requested)) return null;
|
|
1110
|
-
const resolved = path.resolve(requested);
|
|
1111
|
-
try {
|
|
1112
|
-
const stat2 = await fsp.stat(resolved);
|
|
1113
|
-
return stat2.isDirectory() ? resolved : null;
|
|
1114
|
-
} catch {
|
|
1115
|
-
return null;
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
1046
|
allocId() {
|
|
1119
1047
|
return `${this.nextId++}_${randomUUID().replaceAll("-", "")}`;
|
|
1120
1048
|
}
|
|
1121
1049
|
};
|
|
1122
|
-
function errorToJsonRpc(err) {
|
|
1123
|
-
if (err && typeof err === "object") {
|
|
1124
|
-
const e = err;
|
|
1125
|
-
if (typeof e.code === "number" && typeof e.message === "string") {
|
|
1126
|
-
const result = {
|
|
1127
|
-
code: e.code,
|
|
1128
|
-
message: e.message
|
|
1129
|
-
};
|
|
1130
|
-
if (e.data !== void 0) result.data = e.data;
|
|
1131
|
-
return result;
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1135
|
-
return { code: -32603, message };
|
|
1136
|
-
}
|
|
1137
1050
|
|
|
1138
1051
|
// src/agent/session-store.ts
|
|
1139
1052
|
import * as fsp2 from "node:fs/promises";
|