@wrongstack/acp 1.0.19 → 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/agent/protocol-contract.d.ts +13 -0
- package/dist/agent/protocol-handler.d.ts +2 -0
- package/dist/agent/protocol-session-management.d.ts +2 -1
- package/dist/agent/protocol-session-ops.d.ts +1 -4
- package/dist/agent.js +223 -101
- package/dist/client/acp-late-session.d.ts +4 -0
- package/dist/client/acp-request-state.d.ts +9 -0
- package/dist/client/acp-session-auth.d.ts +20 -0
- package/dist/client/acp-session.d.ts +1 -0
- package/dist/client/file-server.d.ts +2 -0
- package/dist/client.js +578 -476
- package/dist/index.js +860 -644
- package/dist/types/acp-v1.d.ts +4 -3
- package/dist/wrongstack-acp-agent.js +213 -99
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/agent/protocol-handler.ts
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
|
|
3
4
|
|
|
4
5
|
// src/types/acp-v1.ts
|
|
5
6
|
var ACP_PROTOCOL_VERSION = 1;
|
|
@@ -180,7 +181,7 @@ function createRunTurnApi(sessionId, clientCapabilities, request, sendSessionUpd
|
|
|
180
181
|
...sendSessionUpdate ? { sendSessionUpdate } : {}
|
|
181
182
|
};
|
|
182
183
|
}
|
|
183
|
-
function buildInitializeResult(agentName, modes, configOptions) {
|
|
184
|
+
function buildInitializeResult(agentName, modes, configOptions, clientCapabilities = {}) {
|
|
184
185
|
return {
|
|
185
186
|
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
186
187
|
agentCapabilities: {
|
|
@@ -207,9 +208,6 @@ function buildInitializeResult(agentName, modes, configOptions) {
|
|
|
207
208
|
delete: {},
|
|
208
209
|
resume: {},
|
|
209
210
|
fork: {}
|
|
210
|
-
},
|
|
211
|
-
auth: {
|
|
212
|
-
logout: {}
|
|
213
211
|
}
|
|
214
212
|
},
|
|
215
213
|
agentInfo: {
|
|
@@ -217,7 +215,7 @@ function buildInitializeResult(agentName, modes, configOptions) {
|
|
|
217
215
|
title: "WrongStack",
|
|
218
216
|
version: WRONGSTACK_VERSION
|
|
219
217
|
},
|
|
220
|
-
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
218
|
+
authMethods: clientCapabilities.auth?.terminal === true ? WRONGSTACK_AUTH_METHODS : [],
|
|
221
219
|
modes,
|
|
222
220
|
configOptions
|
|
223
221
|
};
|
|
@@ -247,7 +245,8 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
247
245
|
id: sessionId,
|
|
248
246
|
cwd,
|
|
249
247
|
abort: new AbortController(),
|
|
250
|
-
modeId: DEFAULT_MODE_ID,
|
|
248
|
+
modeId: ctx.modes[0]?.id ?? DEFAULT_MODE_ID,
|
|
249
|
+
configOptions: structuredClone([...ctx.configOptions]),
|
|
251
250
|
createdAt: now,
|
|
252
251
|
updatedAt: now,
|
|
253
252
|
...mcpServers.length > 0 ? { mcpServers } : {}
|
|
@@ -259,6 +258,7 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
259
258
|
sessionId,
|
|
260
259
|
update: {
|
|
261
260
|
sessionUpdate: "current_mode_update",
|
|
261
|
+
currentModeId: ctx.modes[0]?.id ?? DEFAULT_MODE_ID,
|
|
262
262
|
modeId: ctx.modes[0]?.id ?? DEFAULT_MODE_ID
|
|
263
263
|
}
|
|
264
264
|
});
|
|
@@ -274,12 +274,12 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
274
274
|
await reportSkippedMcpServers(ctx, sessionId, skipped);
|
|
275
275
|
await ctx.sendResult(id, {
|
|
276
276
|
sessionId,
|
|
277
|
-
modes: ctx.modes,
|
|
278
|
-
configOptions:
|
|
277
|
+
modes: { currentModeId: state.modeId, availableModes: ctx.modes },
|
|
278
|
+
configOptions: state.configOptions
|
|
279
279
|
});
|
|
280
280
|
return false;
|
|
281
281
|
}
|
|
282
|
-
async function handleSessionLoadOp(ctx, id, params) {
|
|
282
|
+
async function handleSessionLoadOp(ctx, id, params, replayHistory = true) {
|
|
283
283
|
const p = params ?? {};
|
|
284
284
|
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
285
285
|
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
@@ -304,6 +304,7 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
304
304
|
cwd: restoredCwd,
|
|
305
305
|
abort: new AbortController(),
|
|
306
306
|
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
307
|
+
configOptions: structuredClone(persisted.configOptions ?? [...ctx.configOptions]),
|
|
307
308
|
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
308
309
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
309
310
|
...persisted.title !== void 0 ? { title: persisted.title } : {},
|
|
@@ -311,51 +312,69 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
311
312
|
};
|
|
312
313
|
ctx.sessions.set(sessionId, restored);
|
|
313
314
|
ctx.seedFor?.(sessionId, persisted.history ?? []);
|
|
314
|
-
for (const update of persisted.history ?? []) {
|
|
315
|
+
for (const update of replayHistory ? persisted.history ?? [] : []) {
|
|
315
316
|
await ctx.sendNotification({ sessionId, update });
|
|
316
317
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
318
|
+
if (replayHistory)
|
|
319
|
+
await ctx.sendNotification({
|
|
320
|
+
sessionId,
|
|
321
|
+
update: {
|
|
322
|
+
sessionUpdate: "current_mode_update",
|
|
323
|
+
currentModeId: restored.modeId,
|
|
324
|
+
modeId: restored.modeId
|
|
325
|
+
}
|
|
326
|
+
});
|
|
321
327
|
await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
|
|
322
328
|
await ctx.sendResult(id, {
|
|
323
|
-
|
|
329
|
+
modes: { currentModeId: restored.modeId, availableModes: ctx.modes },
|
|
330
|
+
configOptions: restored.configOptions
|
|
324
331
|
});
|
|
325
332
|
return false;
|
|
326
333
|
}
|
|
327
334
|
}
|
|
328
335
|
if (existing) {
|
|
336
|
+
if (existing.prompting) {
|
|
337
|
+
await ctx.sendError(id, -32e3, "cannot reload a session while a prompt is running");
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
329
340
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
330
|
-
|
|
341
|
+
const replay = ctx.replayFor?.(sessionId);
|
|
342
|
+
if (Array.isArray(p.mcpServers)) {
|
|
343
|
+
if (JSON.stringify(existing.mcpServers ?? []) !== JSON.stringify(loadMcpServers)) {
|
|
344
|
+
ctx.disposeFor?.(sessionId);
|
|
345
|
+
ctx.seedFor?.(sessionId, replay ?? []);
|
|
346
|
+
}
|
|
331
347
|
existing.mcpServers = loadMcpServers;
|
|
332
348
|
}
|
|
333
|
-
|
|
334
|
-
if (replay) {
|
|
349
|
+
if (replayHistory && replay) {
|
|
335
350
|
for (const update of replay) {
|
|
336
351
|
await ctx.sendNotification({ sessionId, update });
|
|
337
352
|
}
|
|
338
353
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
354
|
+
if (replayHistory)
|
|
355
|
+
await ctx.sendNotification({
|
|
356
|
+
sessionId,
|
|
357
|
+
update: {
|
|
358
|
+
sessionUpdate: "session_info_update",
|
|
359
|
+
updatedAt: existing.updatedAt
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
if (replayHistory)
|
|
363
|
+
await ctx.sendNotification({
|
|
364
|
+
sessionId,
|
|
365
|
+
update: {
|
|
366
|
+
sessionUpdate: "current_mode_update",
|
|
367
|
+
currentModeId: existing.modeId,
|
|
368
|
+
modeId: existing.modeId
|
|
369
|
+
}
|
|
370
|
+
});
|
|
353
371
|
await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
|
|
354
372
|
await ctx.sendResult(id, {
|
|
355
|
-
|
|
373
|
+
modes: {
|
|
356
374
|
currentModeId: existing.modeId,
|
|
357
375
|
availableModes: ctx.modes
|
|
358
|
-
}
|
|
376
|
+
},
|
|
377
|
+
configOptions: existing.configOptions ?? [...ctx.configOptions]
|
|
359
378
|
});
|
|
360
379
|
return false;
|
|
361
380
|
}
|
|
@@ -393,6 +412,7 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
393
412
|
cwd: forkCwd,
|
|
394
413
|
abort: new AbortController(),
|
|
395
414
|
modeId: source.modeId,
|
|
415
|
+
configOptions: structuredClone(source.configOptions ?? [...ctx.configOptions]),
|
|
396
416
|
createdAt: now,
|
|
397
417
|
updatedAt: now,
|
|
398
418
|
...source.title !== void 0 ? { title: source.title } : {},
|
|
@@ -408,13 +428,17 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
408
428
|
await ctx.persist(forked, history);
|
|
409
429
|
await ctx.sendNotification({
|
|
410
430
|
sessionId,
|
|
411
|
-
update: {
|
|
431
|
+
update: {
|
|
432
|
+
sessionUpdate: "current_mode_update",
|
|
433
|
+
currentModeId: forked.modeId,
|
|
434
|
+
modeId: forked.modeId
|
|
435
|
+
}
|
|
412
436
|
});
|
|
413
437
|
await reportSkippedMcpServers(ctx, sessionId, forkSkipped);
|
|
414
438
|
await ctx.sendResult(id, {
|
|
415
439
|
sessionId,
|
|
416
|
-
modes: ctx.modes,
|
|
417
|
-
configOptions:
|
|
440
|
+
modes: { currentModeId: forked.modeId, availableModes: ctx.modes },
|
|
441
|
+
configOptions: forked.configOptions
|
|
418
442
|
});
|
|
419
443
|
return false;
|
|
420
444
|
}
|
|
@@ -430,6 +454,11 @@ async function handleSessionPromptOp(ctx, id, params) {
|
|
|
430
454
|
return false;
|
|
431
455
|
}
|
|
432
456
|
const session = ctx.sessions.get(sessionId);
|
|
457
|
+
if (session.prompting) {
|
|
458
|
+
await ctx.sendError(id, -32e3, "a prompt is already running for this session");
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
session.prompting = true;
|
|
433
462
|
if (session.abort.signal.aborted) {
|
|
434
463
|
session.abort = new AbortController();
|
|
435
464
|
}
|
|
@@ -458,6 +487,8 @@ async function handleSessionPromptOp(ctx, id, params) {
|
|
|
458
487
|
{
|
|
459
488
|
sessionId,
|
|
460
489
|
prompt: p.prompt,
|
|
490
|
+
modeId: session.modeId,
|
|
491
|
+
configOptions: session.configOptions,
|
|
461
492
|
signal: turnSignal.signal,
|
|
462
493
|
cwd: session.cwd,
|
|
463
494
|
...session.mcpServers ? { mcpServers: session.mcpServers } : {}
|
|
@@ -466,16 +497,25 @@ async function handleSessionPromptOp(ctx, id, params) {
|
|
|
466
497
|
api
|
|
467
498
|
);
|
|
468
499
|
} catch (err) {
|
|
500
|
+
session.prompting = false;
|
|
469
501
|
session.abort.signal.removeEventListener("abort", onCancel);
|
|
502
|
+
await Promise.all(pendingNotifications);
|
|
503
|
+
if (turnSignal.signal.aborted) {
|
|
504
|
+
await ctx.sendResult(id, { stopReason: "cancelled" });
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
470
507
|
const { code, message, data } = errorToJsonRpc(err);
|
|
471
508
|
await ctx.sendError(id, code, message, data);
|
|
472
509
|
return false;
|
|
473
510
|
}
|
|
474
511
|
await Promise.all(pendingNotifications);
|
|
512
|
+
session.prompting = false;
|
|
475
513
|
session.abort.signal.removeEventListener("abort", onCancel);
|
|
476
514
|
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
477
|
-
await ctx.persist(session);
|
|
478
|
-
await ctx.sendResult(id, {
|
|
515
|
+
if (ctx.sessions.get(sessionId) === session) await ctx.persist(session);
|
|
516
|
+
await ctx.sendResult(id, {
|
|
517
|
+
stopReason: turnSignal.signal.aborted ? "cancelled" : result.stopReason
|
|
518
|
+
});
|
|
479
519
|
return false;
|
|
480
520
|
}
|
|
481
521
|
async function handleSetModeOp(ctx, id, params) {
|
|
@@ -491,7 +531,11 @@ async function handleSetModeOp(ctx, id, params) {
|
|
|
491
531
|
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
492
532
|
await ctx.sendNotification({
|
|
493
533
|
sessionId,
|
|
494
|
-
update: {
|
|
534
|
+
update: {
|
|
535
|
+
sessionUpdate: "current_mode_update",
|
|
536
|
+
currentModeId: modeId,
|
|
537
|
+
modeId
|
|
538
|
+
}
|
|
495
539
|
});
|
|
496
540
|
await ctx.sendResult(id, {});
|
|
497
541
|
return false;
|
|
@@ -502,21 +546,24 @@ async function handleSetConfigOptionOp(ctx, id, params) {
|
|
|
502
546
|
const optionId = typeof p.configId === "string" ? p.configId : null;
|
|
503
547
|
const value = typeof p.value === "string" ? p.value : null;
|
|
504
548
|
const session = sessionId ? ctx.sessions.get(sessionId) : void 0;
|
|
505
|
-
const
|
|
549
|
+
const options = session?.configOptions ?? structuredClone([...ctx.configOptions]);
|
|
550
|
+
const option = optionId ? options.find((o) => o.id === optionId) : void 0;
|
|
506
551
|
if (!session || !option || value === null || !option.options.some((o) => o.value === value)) {
|
|
507
552
|
await ctx.sendError(id, -32602, "invalid sessionId, configId, or value");
|
|
508
553
|
return false;
|
|
509
554
|
}
|
|
510
555
|
option.currentValue = value;
|
|
556
|
+
session.configOptions = options;
|
|
511
557
|
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
512
558
|
await ctx.sendNotification({
|
|
513
559
|
sessionId,
|
|
514
560
|
update: {
|
|
515
561
|
sessionUpdate: "config_option_update",
|
|
516
|
-
configOptions:
|
|
562
|
+
configOptions: options
|
|
517
563
|
}
|
|
518
564
|
});
|
|
519
|
-
await ctx.
|
|
565
|
+
await ctx.persist(session);
|
|
566
|
+
await ctx.sendResult(id, { configOptions: options });
|
|
520
567
|
return false;
|
|
521
568
|
}
|
|
522
569
|
async function reportSkippedMcpServers(ctx, sessionId, skipped) {
|
|
@@ -558,6 +605,8 @@ var ACPProtocolHandler = class {
|
|
|
558
605
|
// session/request_permission). Keyed by our own `srv_N` ids.
|
|
559
606
|
pendingOut = /* @__PURE__ */ new Map();
|
|
560
607
|
nextOutId = 1;
|
|
608
|
+
promptRequests = /* @__PURE__ */ new Map();
|
|
609
|
+
persistenceWrites = /* @__PURE__ */ new Map();
|
|
561
610
|
constructor(opts) {
|
|
562
611
|
this.transport = opts.transport;
|
|
563
612
|
this.defaultCwd = opts.defaultCwd;
|
|
@@ -583,20 +632,46 @@ var ACPProtocolHandler = class {
|
|
|
583
632
|
*/
|
|
584
633
|
request(method, params, timeoutMs = 6e4) {
|
|
585
634
|
const id = `srv_${this.nextOutId++}`;
|
|
586
|
-
|
|
587
|
-
|
|
635
|
+
const sessionId = params?.sessionId;
|
|
636
|
+
const signal = sessionId ? this.sessions.get(sessionId)?.abort.signal : void 0;
|
|
637
|
+
return new Promise((resolve5, reject) => {
|
|
638
|
+
const cleanup = () => {
|
|
639
|
+
clearTimeout(timer);
|
|
640
|
+
signal?.removeEventListener("abort", abort);
|
|
588
641
|
this.pendingOut.delete(id);
|
|
589
|
-
|
|
642
|
+
};
|
|
643
|
+
const fail = (error) => {
|
|
644
|
+
cleanup();
|
|
645
|
+
reject(error);
|
|
646
|
+
};
|
|
647
|
+
const abort = () => {
|
|
648
|
+
fail(new Error("client request cancelled"));
|
|
649
|
+
void this.transport.send(toWire({ jsonrpc: "2.0", method: "$/cancel_request", params: { requestId: id } })).catch(() => {
|
|
650
|
+
});
|
|
651
|
+
};
|
|
652
|
+
const timer = setTimeout(() => {
|
|
653
|
+
fail(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
590
654
|
}, timeoutMs);
|
|
591
|
-
|
|
655
|
+
if (signal?.aborted) {
|
|
656
|
+
fail(new Error("client request cancelled"));
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
660
|
+
this.pendingOut.set(id, {
|
|
661
|
+
resolve: (value) => {
|
|
662
|
+
cleanup();
|
|
663
|
+
resolve5(value);
|
|
664
|
+
},
|
|
665
|
+
reject: fail,
|
|
666
|
+
timer
|
|
667
|
+
});
|
|
592
668
|
this.transport.send(toWire({ jsonrpc: "2.0", id, method, params })).catch((e) => {
|
|
593
|
-
|
|
594
|
-
this.pendingOut.delete(id);
|
|
595
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
669
|
+
fail(e instanceof Error ? e : new Error(String(e)));
|
|
596
670
|
});
|
|
597
671
|
});
|
|
598
672
|
}
|
|
599
673
|
maybeResolvePending(m) {
|
|
674
|
+
if (m.result === void 0 && m.error === void 0) return;
|
|
600
675
|
const id = m.id;
|
|
601
676
|
if (typeof id !== "string") return;
|
|
602
677
|
const pending = this.pendingOut.get(id);
|
|
@@ -616,6 +691,7 @@ var ACPProtocolHandler = class {
|
|
|
616
691
|
if (typeof msg !== "object" || msg === null) return false;
|
|
617
692
|
const m = msg;
|
|
618
693
|
if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
|
|
694
|
+
this.maybeResolvePending(msg);
|
|
619
695
|
return false;
|
|
620
696
|
}
|
|
621
697
|
if (m.id !== void 0 && typeof m.method === "string") {
|
|
@@ -655,6 +731,7 @@ var ACPProtocolHandler = class {
|
|
|
655
731
|
store: this.store,
|
|
656
732
|
replayFor: this.replayFor,
|
|
657
733
|
seedFor: this.seedFor,
|
|
734
|
+
disposeFor: this.disposeFor,
|
|
658
735
|
onSessionNew: this.onSessionNew,
|
|
659
736
|
allocId: () => this.allocId(),
|
|
660
737
|
persist: (state, history) => this.persist(state, history),
|
|
@@ -692,14 +769,23 @@ var ACPProtocolHandler = class {
|
|
|
692
769
|
return await this.handleSessionClose(id, params);
|
|
693
770
|
case "session/delete":
|
|
694
771
|
return await this.handleSessionDelete(id, params);
|
|
695
|
-
case "session/prompt":
|
|
696
|
-
|
|
772
|
+
case "session/prompt": {
|
|
773
|
+
const sessionId = params?.sessionId;
|
|
774
|
+
if (typeof sessionId === "string" && !this.sessions.get(sessionId)?.prompting) {
|
|
775
|
+
this.promptRequests.set(id, sessionId);
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
return await handleSessionPromptOp(this.sessionContext(), id, params);
|
|
779
|
+
} finally {
|
|
780
|
+
this.promptRequests.delete(id);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
697
783
|
case "session/set_mode":
|
|
698
784
|
return await handleSetModeOp(this.sessionContext(), id, params);
|
|
699
785
|
case "session/set_config_option":
|
|
700
786
|
return await handleSetConfigOptionOp(this.sessionContext(), id, params);
|
|
701
787
|
case "session/list":
|
|
702
|
-
return await this.handleSessionList(id);
|
|
788
|
+
return await this.handleSessionList(id, params);
|
|
703
789
|
case "session/fork":
|
|
704
790
|
return await handleSessionForkOp(this.sessionContext(), id, params);
|
|
705
791
|
case "providers/list":
|
|
@@ -728,7 +814,12 @@ var ACPProtocolHandler = class {
|
|
|
728
814
|
this.initialized = true;
|
|
729
815
|
await this.sendResult(
|
|
730
816
|
id,
|
|
731
|
-
buildInitializeResult(
|
|
817
|
+
buildInitializeResult(
|
|
818
|
+
this.agentName,
|
|
819
|
+
this.modes,
|
|
820
|
+
this.configOptions,
|
|
821
|
+
this.clientCapabilities
|
|
822
|
+
)
|
|
732
823
|
);
|
|
733
824
|
return false;
|
|
734
825
|
}
|
|
@@ -737,29 +828,15 @@ var ACPProtocolHandler = class {
|
|
|
737
828
|
return false;
|
|
738
829
|
}
|
|
739
830
|
async handleLogout(id, _params) {
|
|
740
|
-
await this.
|
|
831
|
+
await this.sendError(
|
|
832
|
+
id,
|
|
833
|
+
-32601,
|
|
834
|
+
"logout is not supported; manage provider credentials with wstack auth"
|
|
835
|
+
);
|
|
741
836
|
return false;
|
|
742
837
|
}
|
|
743
838
|
async handleSessionResume(id, params) {
|
|
744
|
-
|
|
745
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
746
|
-
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
747
|
-
if (existing) {
|
|
748
|
-
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
749
|
-
const resumeServers = parseMcpServers(p.mcpServers);
|
|
750
|
-
if (resumeServers.length > 0) {
|
|
751
|
-
existing.mcpServers = resumeServers;
|
|
752
|
-
}
|
|
753
|
-
await this.sendResult(id, {
|
|
754
|
-
initialMode: {
|
|
755
|
-
currentModeId: existing.modeId,
|
|
756
|
-
availableModes: this.modes
|
|
757
|
-
}
|
|
758
|
-
});
|
|
759
|
-
return false;
|
|
760
|
-
}
|
|
761
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
762
|
-
return false;
|
|
839
|
+
return handleSessionLoadOp(this.sessionContext(), id, params, false);
|
|
763
840
|
}
|
|
764
841
|
async handleSessionClose(id, params) {
|
|
765
842
|
const p = params ?? {};
|
|
@@ -782,14 +859,12 @@ var ACPProtocolHandler = class {
|
|
|
782
859
|
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
783
860
|
return false;
|
|
784
861
|
}
|
|
785
|
-
if (!this.sessions.has(sessionId)) {
|
|
786
|
-
await this.sendResult(id, { configOptions: [...this.configOptions] });
|
|
787
|
-
return false;
|
|
788
|
-
}
|
|
789
862
|
const session = this.sessions.get(sessionId);
|
|
790
|
-
session
|
|
863
|
+
session?.abort.abort();
|
|
791
864
|
this.sessions.delete(sessionId);
|
|
792
865
|
this.disposeSession(sessionId);
|
|
866
|
+
await this.persistenceWrites.get(sessionId);
|
|
867
|
+
await this.store?.delete?.(sessionId);
|
|
793
868
|
await this.sendResult(id, {});
|
|
794
869
|
return false;
|
|
795
870
|
}
|
|
@@ -816,12 +891,23 @@ var ACPProtocolHandler = class {
|
|
|
816
891
|
await this.sendError(id, -32e3, "MCP message routing not available through ACP");
|
|
817
892
|
return false;
|
|
818
893
|
}
|
|
819
|
-
async handleSessionList(id) {
|
|
820
|
-
const
|
|
894
|
+
async handleSessionList(id, params) {
|
|
895
|
+
const p = params ?? {};
|
|
896
|
+
if (p.cwd != null && (typeof p.cwd !== "string" || !isAbsolute2(p.cwd)) || p.cursor != null) {
|
|
897
|
+
await this.sendError(id, -32602, "invalid cwd or cursor");
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
const known = new Map(this.sessions);
|
|
901
|
+
for (const entry of await this.store?.list?.() ?? []) {
|
|
902
|
+
if (known.has(entry.id)) continue;
|
|
903
|
+
const saved = await this.store?.load(entry.id);
|
|
904
|
+
if (saved?.cwd) known.set(entry.id, { ...saved, id: entry.id });
|
|
905
|
+
}
|
|
906
|
+
const sessions = Array.from(known.values()).filter((s) => typeof p.cwd !== "string" || s.cwd && resolve2(s.cwd) === resolve2(p.cwd)).map((s) => {
|
|
821
907
|
const out = {
|
|
822
908
|
sessionId: s.id,
|
|
823
909
|
cwd: s.cwd,
|
|
824
|
-
updatedAt: s.updatedAt
|
|
910
|
+
updatedAt: s.updatedAt ?? ""
|
|
825
911
|
};
|
|
826
912
|
if (s.title !== void 0) out.title = s.title;
|
|
827
913
|
return out;
|
|
@@ -844,6 +930,11 @@ var ACPProtocolHandler = class {
|
|
|
844
930
|
return false;
|
|
845
931
|
}
|
|
846
932
|
case "$/cancel_request": {
|
|
933
|
+
const requestId = params?.requestId;
|
|
934
|
+
if (typeof requestId === "string" || typeof requestId === "number") {
|
|
935
|
+
const sessionId = this.promptRequests.get(requestId);
|
|
936
|
+
if (sessionId) this.sessions.get(sessionId)?.abort.abort();
|
|
937
|
+
}
|
|
847
938
|
return false;
|
|
848
939
|
}
|
|
849
940
|
case "exit":
|
|
@@ -864,10 +955,18 @@ var ACPProtocolHandler = class {
|
|
|
864
955
|
}
|
|
865
956
|
async persist(state, history = void 0) {
|
|
866
957
|
if (!this.store) return;
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
958
|
+
const store = this.store;
|
|
959
|
+
const replay = history ?? this.replayFor?.(state.id);
|
|
960
|
+
const previous = this.persistenceWrites.get(state.id) ?? Promise.resolve();
|
|
961
|
+
const write = previous.then(() => store.save(state, replay)).then(
|
|
962
|
+
() => {
|
|
963
|
+
},
|
|
964
|
+
() => {
|
|
965
|
+
}
|
|
966
|
+
);
|
|
967
|
+
this.persistenceWrites.set(state.id, write);
|
|
968
|
+
await write;
|
|
969
|
+
if (this.persistenceWrites.get(state.id) === write) this.persistenceWrites.delete(state.id);
|
|
871
970
|
}
|
|
872
971
|
async sendError(id, code, message, data) {
|
|
873
972
|
const error = { code, message };
|
|
@@ -927,9 +1026,9 @@ var StdioTransport = class {
|
|
|
927
1026
|
}
|
|
928
1027
|
send(msg) {
|
|
929
1028
|
if (this.closed) return Promise.resolve();
|
|
930
|
-
return new Promise((
|
|
1029
|
+
return new Promise((resolve5) => {
|
|
931
1030
|
const line = JSON.stringify(msg) + "\n";
|
|
932
|
-
this.stdout.write(line, "utf8", () =>
|
|
1031
|
+
this.stdout.write(line, "utf8", () => resolve5());
|
|
933
1032
|
});
|
|
934
1033
|
}
|
|
935
1034
|
sendRaw(chunk) {
|
|
@@ -942,8 +1041,8 @@ var StdioTransport = class {
|
|
|
942
1041
|
return Promise.resolve(queued.message);
|
|
943
1042
|
}
|
|
944
1043
|
if (this.closed) return Promise.resolve(null);
|
|
945
|
-
return new Promise((
|
|
946
|
-
this.resolveRead =
|
|
1044
|
+
return new Promise((resolve5) => {
|
|
1045
|
+
this.resolveRead = resolve5;
|
|
947
1046
|
});
|
|
948
1047
|
}
|
|
949
1048
|
onMessage(handler) {
|
|
@@ -1012,9 +1111,9 @@ var StdioTransport = class {
|
|
|
1012
1111
|
}
|
|
1013
1112
|
dispatch(msg, chars = JSON.stringify(msg).length) {
|
|
1014
1113
|
if (this.resolveRead) {
|
|
1015
|
-
const
|
|
1114
|
+
const resolve5 = this.resolveRead;
|
|
1016
1115
|
this.resolveRead = null;
|
|
1017
|
-
|
|
1116
|
+
resolve5(msg);
|
|
1018
1117
|
} else {
|
|
1019
1118
|
let claimed = false;
|
|
1020
1119
|
for (const handler of this.claimHandlers) {
|
|
@@ -1086,7 +1185,7 @@ var ClientTransport = class {
|
|
|
1086
1185
|
import("@wrongstack/core/utils"),
|
|
1087
1186
|
import("node:os")
|
|
1088
1187
|
]);
|
|
1089
|
-
return new Promise((
|
|
1188
|
+
return new Promise((resolve5, reject) => {
|
|
1090
1189
|
const timeout = setTimeout(() => {
|
|
1091
1190
|
const child2 = this.child;
|
|
1092
1191
|
this.child = null;
|
|
@@ -1139,7 +1238,7 @@ var ClientTransport = class {
|
|
|
1139
1238
|
if (settled) return;
|
|
1140
1239
|
settled = true;
|
|
1141
1240
|
clearTimeout(timeout);
|
|
1142
|
-
|
|
1241
|
+
resolve5();
|
|
1143
1242
|
});
|
|
1144
1243
|
return;
|
|
1145
1244
|
}
|
|
@@ -1150,7 +1249,7 @@ var ClientTransport = class {
|
|
|
1150
1249
|
child.stderr.on("data", (c) => this.onChildError(c));
|
|
1151
1250
|
child.on("close", (code) => this.onChildClose(code));
|
|
1152
1251
|
clearTimeout(timeout);
|
|
1153
|
-
|
|
1252
|
+
resolve5();
|
|
1154
1253
|
};
|
|
1155
1254
|
const waitForMarker = (chunk) => {
|
|
1156
1255
|
this.buffer += chunk;
|
|
@@ -1169,11 +1268,11 @@ var ClientTransport = class {
|
|
|
1169
1268
|
}
|
|
1170
1269
|
send(msg) {
|
|
1171
1270
|
if (!this.child) return Promise.reject(new Error("ClientTransport not started"));
|
|
1172
|
-
return new Promise((
|
|
1271
|
+
return new Promise((resolve5, reject) => {
|
|
1173
1272
|
const line = JSON.stringify(msg) + "\n";
|
|
1174
1273
|
this.child?.stdin.write(line, "utf8", (err) => {
|
|
1175
1274
|
if (err) reject(err);
|
|
1176
|
-
else
|
|
1275
|
+
else resolve5();
|
|
1177
1276
|
});
|
|
1178
1277
|
});
|
|
1179
1278
|
}
|
|
@@ -1184,8 +1283,8 @@ var ClientTransport = class {
|
|
|
1184
1283
|
return Promise.resolve(queued.message);
|
|
1185
1284
|
}
|
|
1186
1285
|
if (this.closed) return Promise.resolve(null);
|
|
1187
|
-
return new Promise((
|
|
1188
|
-
this.resolveRead =
|
|
1286
|
+
return new Promise((resolve5) => {
|
|
1287
|
+
this.resolveRead = resolve5;
|
|
1189
1288
|
});
|
|
1190
1289
|
}
|
|
1191
1290
|
onMessage(handler) {
|
|
@@ -1256,6 +1355,7 @@ var ClientTransport = class {
|
|
|
1256
1355
|
this.messageQueue.length = 0;
|
|
1257
1356
|
this.queuedChars = 0;
|
|
1258
1357
|
this.handlers.clear();
|
|
1358
|
+
this.claimHandlers.clear();
|
|
1259
1359
|
if (code !== 0 && code !== null) {
|
|
1260
1360
|
writeErr(`[acp-child exited with code ${code}]
|
|
1261
1361
|
`);
|
|
@@ -1263,20 +1363,31 @@ var ClientTransport = class {
|
|
|
1263
1363
|
}
|
|
1264
1364
|
dispatch(msg, chars = JSON.stringify(msg).length) {
|
|
1265
1365
|
if (this.resolveRead) {
|
|
1266
|
-
const
|
|
1366
|
+
const resolve5 = this.resolveRead;
|
|
1267
1367
|
this.resolveRead = null;
|
|
1268
|
-
|
|
1269
|
-
} else
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1368
|
+
resolve5(msg);
|
|
1369
|
+
} else {
|
|
1370
|
+
let claimed = false;
|
|
1371
|
+
for (const handler of this.claimHandlers) {
|
|
1372
|
+
try {
|
|
1373
|
+
if (handler(msg)) claimed = true;
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
writeErr(`[acp-child handler error] ${err}
|
|
1376
|
+
`);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
if (!claimed && this.handlers.size === 0) {
|
|
1380
|
+
if (this.messageQueue.length >= this.maxQueuedMessages || this.queuedChars + chars > this.maxQueuedChars) {
|
|
1381
|
+
writeErr(
|
|
1382
|
+
`[acp-child message queue exceeds ${this.maxQueuedMessages} entries or ${this.maxQueuedChars} characters]
|
|
1273
1383
|
`
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1384
|
+
);
|
|
1385
|
+
this.stop();
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
this.messageQueue.push({ message: msg, chars });
|
|
1389
|
+
this.queuedChars += chars;
|
|
1277
1390
|
}
|
|
1278
|
-
this.messageQueue.push({ message: msg, chars });
|
|
1279
|
-
this.queuedChars += chars;
|
|
1280
1391
|
}
|
|
1281
1392
|
for (const handler of this.handlers) {
|
|
1282
1393
|
try {
|
|
@@ -1410,7 +1521,7 @@ function toolToPriority(tool) {
|
|
|
1410
1521
|
import { createServer } from "node:http";
|
|
1411
1522
|
import { isIP } from "node:net";
|
|
1412
1523
|
import { fileURLToPath } from "node:url";
|
|
1413
|
-
import { expandIPv6, writeErr as writeErr2 } from "@wrongstack/core/utils";
|
|
1524
|
+
import { expandIPv6, isStandaloneBinary, writeErr as writeErr2 } from "@wrongstack/core/utils";
|
|
1414
1525
|
import { timingSafeTokenEqual } from "@wrongstack/primitives";
|
|
1415
1526
|
var LISTEN_RETRY_LIMIT = 5;
|
|
1416
1527
|
var LISTEN_RETRY_BASE_MS = 25;
|
|
@@ -1456,15 +1567,30 @@ var WrongStackACPServer = class {
|
|
|
1456
1567
|
this.transport.sendStartupMarker();
|
|
1457
1568
|
}
|
|
1458
1569
|
this.running = true;
|
|
1570
|
+
const prompts = /* @__PURE__ */ new Set();
|
|
1459
1571
|
try {
|
|
1460
1572
|
while (this.running) {
|
|
1461
1573
|
const msg = await this.transport.read();
|
|
1462
1574
|
if (!msg) break;
|
|
1575
|
+
if (msg.method === "session/prompt") {
|
|
1576
|
+
const task = this.handler.handleMessage(msg).then(
|
|
1577
|
+
() => {
|
|
1578
|
+
},
|
|
1579
|
+
(err) => {
|
|
1580
|
+
writeErr2(`[wstack-acp] prompt dispatch failed: ${String(err)}
|
|
1581
|
+
`);
|
|
1582
|
+
}
|
|
1583
|
+
);
|
|
1584
|
+
prompts.add(task);
|
|
1585
|
+
void task.finally(() => prompts.delete(task));
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1463
1588
|
const terminal = await this.handler.handleMessage(msg);
|
|
1464
1589
|
if (terminal) break;
|
|
1465
1590
|
}
|
|
1466
1591
|
} finally {
|
|
1467
1592
|
this.handler.close();
|
|
1593
|
+
await Promise.allSettled(prompts);
|
|
1468
1594
|
this.transport.close();
|
|
1469
1595
|
}
|
|
1470
1596
|
}
|
|
@@ -1598,10 +1724,10 @@ var WrongStackACPServer = class {
|
|
|
1598
1724
|
listenWithRetry(port, host, attempt = 0) {
|
|
1599
1725
|
const server = this.httpServer;
|
|
1600
1726
|
if (!server) return Promise.resolve();
|
|
1601
|
-
return new Promise((
|
|
1727
|
+
return new Promise((resolve5, reject) => {
|
|
1602
1728
|
const onListening = () => {
|
|
1603
1729
|
server.removeListener("error", onError);
|
|
1604
|
-
|
|
1730
|
+
resolve5();
|
|
1605
1731
|
};
|
|
1606
1732
|
const onError = (err) => {
|
|
1607
1733
|
server.removeListener("listening", onListening);
|
|
@@ -1609,7 +1735,7 @@ var WrongStackACPServer = class {
|
|
|
1609
1735
|
if (port === 0 && transient && attempt < LISTEN_RETRY_LIMIT) {
|
|
1610
1736
|
const timer = setTimeout(
|
|
1611
1737
|
() => {
|
|
1612
|
-
this.listenWithRetry(port, host, attempt + 1).then(
|
|
1738
|
+
this.listenWithRetry(port, host, attempt + 1).then(resolve5, reject);
|
|
1613
1739
|
},
|
|
1614
1740
|
LISTEN_RETRY_BASE_MS * (attempt + 1)
|
|
1615
1741
|
);
|
|
@@ -1637,8 +1763,8 @@ var WrongStackACPServer = class {
|
|
|
1637
1763
|
if (!server) return;
|
|
1638
1764
|
for (const socket of this.httpSockets) socket.destroy();
|
|
1639
1765
|
this.httpSockets.clear();
|
|
1640
|
-
await new Promise((
|
|
1641
|
-
server.close(() =>
|
|
1766
|
+
await new Promise((resolve5) => {
|
|
1767
|
+
server.close(() => resolve5());
|
|
1642
1768
|
});
|
|
1643
1769
|
}
|
|
1644
1770
|
};
|
|
@@ -1669,7 +1795,7 @@ async function main() {
|
|
|
1669
1795
|
const server = new WrongStackACPServer();
|
|
1670
1796
|
await server.start();
|
|
1671
1797
|
}
|
|
1672
|
-
var isEntrypoint = process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === process.argv[1];
|
|
1798
|
+
var isEntrypoint = process.argv[1] !== void 0 && !isStandaloneBinary() && fileURLToPath(import.meta.url) === process.argv[1];
|
|
1673
1799
|
if (isEntrypoint) {
|
|
1674
1800
|
main().catch((err) => {
|
|
1675
1801
|
writeErr2(`[wstack-acp fatal] ${err}
|
|
@@ -1678,93 +1804,472 @@ if (isEntrypoint) {
|
|
|
1678
1804
|
});
|
|
1679
1805
|
}
|
|
1680
1806
|
|
|
1807
|
+
// src/client/acp-late-session.ts
|
|
1808
|
+
var LATE_CANCEL_SEND_TIMEOUT_MS = 1e4;
|
|
1809
|
+
function cancelLateAcpSession(transport, createPromise) {
|
|
1810
|
+
createPromise.then(async (lateId) => {
|
|
1811
|
+
try {
|
|
1812
|
+
await new Promise((resolve5, reject) => {
|
|
1813
|
+
const timer = setTimeout(() => {
|
|
1814
|
+
reject(new Error("late session/cancel send timed out"));
|
|
1815
|
+
}, LATE_CANCEL_SEND_TIMEOUT_MS);
|
|
1816
|
+
Promise.resolve(
|
|
1817
|
+
transport.send({
|
|
1818
|
+
jsonrpc: "2.0",
|
|
1819
|
+
method: "session/cancel",
|
|
1820
|
+
params: { sessionId: lateId }
|
|
1821
|
+
})
|
|
1822
|
+
).then(
|
|
1823
|
+
() => {
|
|
1824
|
+
clearTimeout(timer);
|
|
1825
|
+
resolve5();
|
|
1826
|
+
},
|
|
1827
|
+
(sendErr) => {
|
|
1828
|
+
clearTimeout(timer);
|
|
1829
|
+
reject(sendErr instanceof Error ? sendErr : new Error(String(sendErr)));
|
|
1830
|
+
}
|
|
1831
|
+
);
|
|
1832
|
+
});
|
|
1833
|
+
} catch (err) {
|
|
1834
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1835
|
+
console.warn(
|
|
1836
|
+
JSON.stringify({
|
|
1837
|
+
level: "warn",
|
|
1838
|
+
event: "acp_session.late_cancel_failed",
|
|
1839
|
+
sessionId: lateId,
|
|
1840
|
+
message
|
|
1841
|
+
})
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1844
|
+
}).catch((reason) => {
|
|
1845
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
1846
|
+
console.warn(
|
|
1847
|
+
JSON.stringify({
|
|
1848
|
+
level: "warn",
|
|
1849
|
+
event: "acp_session.late_cancel_failed",
|
|
1850
|
+
reason: message
|
|
1851
|
+
})
|
|
1852
|
+
);
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1681
1856
|
// src/client/acp-message-routing.ts
|
|
1682
1857
|
function isBestEffortAckMethod(method) {
|
|
1683
1858
|
return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
|
|
1684
1859
|
}
|
|
1685
1860
|
|
|
1686
|
-
// src/client/
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
var DEFAULT_FILE_OPERATIONS = {
|
|
1692
|
-
stat: fsp2.stat,
|
|
1693
|
-
readFile: fsp2.readFile,
|
|
1694
|
-
writeFile: fsp2.writeFile,
|
|
1695
|
-
realpath: fsp2.realpath,
|
|
1696
|
-
rename: fsp2.rename,
|
|
1697
|
-
unlink: fsp2.unlink
|
|
1698
|
-
};
|
|
1699
|
-
var DEFAULT_MAX_READ_BYTES = 5 * 1024 * 1024;
|
|
1700
|
-
var DEFAULT_MAX_WRITE_BYTES = 5 * 1024 * 1024;
|
|
1701
|
-
var FsError = class extends Error {
|
|
1702
|
-
code;
|
|
1703
|
-
path;
|
|
1704
|
-
constructor(code, path5, message) {
|
|
1861
|
+
// src/client/acp-session-errors.ts
|
|
1862
|
+
var ACPSessionError = class extends Error {
|
|
1863
|
+
kind;
|
|
1864
|
+
cause;
|
|
1865
|
+
constructor(kind, message, cause) {
|
|
1705
1866
|
super(message);
|
|
1706
|
-
this.name = "
|
|
1707
|
-
this.
|
|
1708
|
-
this.
|
|
1867
|
+
this.name = "ACPSessionError";
|
|
1868
|
+
this.kind = kind;
|
|
1869
|
+
this.cause = cause;
|
|
1709
1870
|
}
|
|
1710
1871
|
};
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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;
|
|
1725
1886
|
}
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
return { content };
|
|
1747
|
-
} catch (err) {
|
|
1748
|
-
if (err instanceof FsError) throw err;
|
|
1749
|
-
if (controller.signal.aborted) {
|
|
1750
|
-
throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
|
|
1751
|
-
}
|
|
1752
|
-
throw mapFsError(err, safe);
|
|
1753
|
-
} finally {
|
|
1754
|
-
clearTimeout(timer);
|
|
1755
|
-
}
|
|
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
|
+
);
|
|
1756
1907
|
}
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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
|
+
|
|
2179
|
+
// src/client/file-server.ts
|
|
2180
|
+
import { randomBytes } from "node:crypto";
|
|
2181
|
+
import { realpathSync } from "node:fs";
|
|
2182
|
+
import * as fsp2 from "node:fs/promises";
|
|
2183
|
+
import * as path2 from "node:path";
|
|
2184
|
+
var DEFAULT_FILE_OPERATIONS = {
|
|
2185
|
+
stat: fsp2.stat,
|
|
2186
|
+
readFile: fsp2.readFile,
|
|
2187
|
+
writeFile: fsp2.writeFile,
|
|
2188
|
+
realpath: fsp2.realpath,
|
|
2189
|
+
rename: fsp2.rename,
|
|
2190
|
+
unlink: fsp2.unlink
|
|
2191
|
+
};
|
|
2192
|
+
var DEFAULT_MAX_READ_BYTES = 5 * 1024 * 1024;
|
|
2193
|
+
var DEFAULT_MAX_WRITE_BYTES = 5 * 1024 * 1024;
|
|
2194
|
+
var FsError = class extends Error {
|
|
2195
|
+
code;
|
|
2196
|
+
path;
|
|
2197
|
+
constructor(code, path5, message) {
|
|
2198
|
+
super(message);
|
|
2199
|
+
this.name = "FsError";
|
|
2200
|
+
this.code = code;
|
|
2201
|
+
this.path = path5;
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
var FileServer = class {
|
|
2205
|
+
root;
|
|
2206
|
+
realRoot;
|
|
2207
|
+
timeoutMs;
|
|
2208
|
+
maxReadBytes;
|
|
2209
|
+
maxWriteBytes;
|
|
2210
|
+
operations;
|
|
2211
|
+
constructor(opts) {
|
|
2212
|
+
this.root = path2.resolve(opts.projectRoot);
|
|
2213
|
+
this.realRoot = safeRealpathSync(this.root);
|
|
2214
|
+
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
2215
|
+
this.maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
|
|
2216
|
+
this.maxWriteBytes = opts.maxWriteBytes ?? DEFAULT_MAX_WRITE_BYTES;
|
|
2217
|
+
this.operations = opts.operations ?? DEFAULT_FILE_OPERATIONS;
|
|
2218
|
+
}
|
|
2219
|
+
/** Read a text file. Returns the content as a string. */
|
|
2220
|
+
async readTextFile(params) {
|
|
2221
|
+
if (params.line != null && (!Number.isSafeInteger(params.line) || params.line < 1) || params.limit != null && (!Number.isSafeInteger(params.limit) || params.limit < 0)) {
|
|
2222
|
+
throw new FsError(
|
|
2223
|
+
"INVALID_PATH",
|
|
2224
|
+
params.path,
|
|
2225
|
+
"line must be a positive integer and limit a non-negative integer"
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2228
|
+
const safe = await this.resolveInside(params.path);
|
|
2229
|
+
const controller = new AbortController();
|
|
2230
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2231
|
+
try {
|
|
2232
|
+
const stat3 = await this.operations.stat(safe).catch((err) => {
|
|
2233
|
+
throw mapFsError(err, safe);
|
|
2234
|
+
});
|
|
2235
|
+
if (stat3.size > this.maxReadBytes) {
|
|
2236
|
+
throw new FsError(
|
|
2237
|
+
"TOO_LARGE",
|
|
2238
|
+
safe,
|
|
2239
|
+
`file is ${stat3.size} bytes, max read is ${this.maxReadBytes} bytes`
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
const content = await this.operations.readFile(safe, {
|
|
2243
|
+
encoding: "utf8",
|
|
2244
|
+
signal: controller.signal
|
|
2245
|
+
});
|
|
2246
|
+
if (params.line == null && params.limit == null) return { content };
|
|
2247
|
+
const lines = content.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
2248
|
+
const start = (params.line ?? 1) - 1;
|
|
2249
|
+
return {
|
|
2250
|
+
content: lines.slice(start, params.limit == null ? void 0 : start + params.limit).join("")
|
|
2251
|
+
};
|
|
2252
|
+
} catch (err) {
|
|
2253
|
+
if (err instanceof FsError) throw err;
|
|
2254
|
+
if (controller.signal.aborted) {
|
|
2255
|
+
throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
|
|
2256
|
+
}
|
|
2257
|
+
throw mapFsError(err, safe);
|
|
2258
|
+
} finally {
|
|
2259
|
+
clearTimeout(timer);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
/** Write a text file. Atomic via write-then-rename. */
|
|
2263
|
+
async writeTextFile(params) {
|
|
2264
|
+
if (typeof params.content !== "string") {
|
|
2265
|
+
throw new FsError("INVALID_PATH", params.path, "content must be a string");
|
|
2266
|
+
}
|
|
2267
|
+
const byteLength = Buffer.byteLength(params.content, "utf8");
|
|
2268
|
+
if (byteLength > this.maxWriteBytes) {
|
|
2269
|
+
throw new FsError(
|
|
2270
|
+
"TOO_LARGE",
|
|
2271
|
+
params.path,
|
|
2272
|
+
`content is ${byteLength} bytes, max write is ${this.maxWriteBytes} bytes`
|
|
1768
2273
|
);
|
|
1769
2274
|
}
|
|
1770
2275
|
const safe = await this.resolveInside(params.path);
|
|
@@ -1848,7 +2353,8 @@ var FileServer = class {
|
|
|
1848
2353
|
throw mapFsError(err, resolvedPath);
|
|
1849
2354
|
}
|
|
1850
2355
|
const normReal = isWindows ? real.toLowerCase() : real;
|
|
1851
|
-
|
|
2356
|
+
const realRootWithSep = normRealRoot.endsWith(path2.sep) ? normRealRoot : normRealRoot + path2.sep;
|
|
2357
|
+
if (normReal === normRealRoot || normReal.startsWith(realRootWithSep)) return;
|
|
1852
2358
|
throw new FsError(
|
|
1853
2359
|
"OUTSIDE_ROOT",
|
|
1854
2360
|
resolvedPath,
|
|
@@ -1897,9 +2403,12 @@ async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbac
|
|
|
1897
2403
|
);
|
|
1898
2404
|
await sender.sendResult(id, { outcome });
|
|
1899
2405
|
} catch (err) {
|
|
2406
|
+
if (isAbortLikeError(err)) {
|
|
2407
|
+
await sender.sendResult(id, { outcome: { outcome: "cancelled" } });
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
1900
2410
|
const message = err instanceof Error ? err.message : String(err);
|
|
1901
|
-
|
|
1902
|
-
await sender.sendErrorResponse(id, code, `permission policy failed: ${message}`);
|
|
2411
|
+
await sender.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
|
|
1903
2412
|
}
|
|
1904
2413
|
}
|
|
1905
2414
|
async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, callbackOptions = {}) {
|
|
@@ -1935,7 +2444,9 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, cal
|
|
|
1935
2444
|
if (msg.method === "fs/read_text_file") {
|
|
1936
2445
|
const result = await fileServer.readTextFile({
|
|
1937
2446
|
sessionId: params.sessionId ?? "",
|
|
1938
|
-
path: params.path
|
|
2447
|
+
path: params.path,
|
|
2448
|
+
line: params.line,
|
|
2449
|
+
limit: params.limit
|
|
1939
2450
|
});
|
|
1940
2451
|
await sender.sendResult(id, result);
|
|
1941
2452
|
} else {
|
|
@@ -2037,336 +2548,105 @@ async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions)
|
|
|
2037
2548
|
try {
|
|
2038
2549
|
const outcome = await runPermissionWithDeadline(
|
|
2039
2550
|
permissionPolicy,
|
|
2040
|
-
{
|
|
2041
|
-
toolCall: {
|
|
2042
|
-
sessionUpdate: "tool_call_update",
|
|
2043
|
-
toolCallId: partial.toolCallId,
|
|
2044
|
-
title: partial.title,
|
|
2045
|
-
kind: partial.kind,
|
|
2046
|
-
status: "pending",
|
|
2047
|
-
rawInput: partial.rawInput
|
|
2048
|
-
},
|
|
2049
|
-
options: [
|
|
2050
|
-
{ optionId: "allow", name: "Allow", kind: "allow_once" },
|
|
2051
|
-
{ optionId: "reject", name: "Reject", kind: "reject_once" }
|
|
2052
|
-
]
|
|
2053
|
-
},
|
|
2054
|
-
callbackOptions
|
|
2055
|
-
);
|
|
2056
|
-
return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always" ? "allowed" : "denied";
|
|
2057
|
-
} catch (err) {
|
|
2058
|
-
return isAbortLikeError(err) ? "cancelled" : "denied";
|
|
2059
|
-
}
|
|
2060
|
-
}
|
|
2061
|
-
async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions) {
|
|
2062
|
-
const timeoutMs = resolvePermissionDeadline(callbackOptions.permissionTimeoutMs);
|
|
2063
|
-
const controller = new AbortController();
|
|
2064
|
-
const abort = () => controller.abort();
|
|
2065
|
-
const timer = timeoutMs === null ? null : setTimeout(abort, timeoutMs);
|
|
2066
|
-
let removeAbort;
|
|
2067
|
-
if (callbackOptions.signal) {
|
|
2068
|
-
if (callbackOptions.signal.aborted) {
|
|
2069
|
-
abort();
|
|
2070
|
-
} else {
|
|
2071
|
-
callbackOptions.signal.addEventListener("abort", abort, { once: true });
|
|
2072
|
-
removeAbort = () => callbackOptions.signal?.removeEventListener("abort", abort);
|
|
2073
|
-
}
|
|
2074
|
-
}
|
|
2075
|
-
try {
|
|
2076
|
-
return await Promise.race([
|
|
2077
|
-
permissionPolicy({
|
|
2078
|
-
toolCall: call.toolCall,
|
|
2079
|
-
options: call.options,
|
|
2080
|
-
signal: controller.signal
|
|
2081
|
-
}),
|
|
2082
|
-
rejectOnAbort(controller.signal)
|
|
2083
|
-
]);
|
|
2084
|
-
} finally {
|
|
2085
|
-
if (timer !== null) clearTimeout(timer);
|
|
2086
|
-
removeAbort?.();
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
function rejectOnAbort(signal) {
|
|
2090
|
-
return new Promise((_, reject) => {
|
|
2091
|
-
const rejectAbort = () => reject(new Error("permission request cancelled or timed out"));
|
|
2092
|
-
if (signal.aborted) {
|
|
2093
|
-
rejectAbort();
|
|
2094
|
-
return;
|
|
2095
|
-
}
|
|
2096
|
-
signal.addEventListener("abort", rejectAbort, { once: true });
|
|
2097
|
-
});
|
|
2098
|
-
}
|
|
2099
|
-
function isAbortLikeError(err) {
|
|
2100
|
-
return err instanceof Error && /cancelled|canceled|timed out|aborted/i.test(err.message);
|
|
2101
|
-
}
|
|
2102
|
-
function resolvePermissionDeadline(value) {
|
|
2103
|
-
if (value === Number.POSITIVE_INFINITY) return null;
|
|
2104
|
-
if (value !== void 0 && Number.isFinite(value) && value > 0) return Math.trunc(value);
|
|
2105
|
-
return DEFAULT_PERMISSION_TIMEOUT_MS;
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
// src/client/acp-session-content.ts
|
|
2109
|
-
function textContent(text) {
|
|
2110
|
-
return { type: "text", text };
|
|
2111
|
-
}
|
|
2112
|
-
function imageContent(mimeType, data) {
|
|
2113
|
-
return { type: "image", mimeType, data };
|
|
2114
|
-
}
|
|
2115
|
-
function audioContent(mimeType, data) {
|
|
2116
|
-
return { type: "audio", mimeType, data };
|
|
2117
|
-
}
|
|
2118
|
-
function extractText(block) {
|
|
2119
|
-
if (typeof block !== "object" || block === null) return "";
|
|
2120
|
-
const b = block;
|
|
2121
|
-
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
2122
|
-
if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
|
|
2123
|
-
return b.resource.text;
|
|
2124
|
-
}
|
|
2125
|
-
return "";
|
|
2126
|
-
}
|
|
2127
|
-
function isRecord(v) {
|
|
2128
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2129
|
-
}
|
|
2130
|
-
function emptyRunResult(stopReason) {
|
|
2131
|
-
return {
|
|
2132
|
-
text: "",
|
|
2133
|
-
stopReason,
|
|
2134
|
-
hasText: false,
|
|
2135
|
-
toolCalls: [],
|
|
2136
|
-
diffs: [],
|
|
2137
|
-
thoughts: ""
|
|
2138
|
-
};
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
// src/client/acp-session-errors.ts
|
|
2142
|
-
var ACPSessionError = class extends Error {
|
|
2143
|
-
kind;
|
|
2144
|
-
cause;
|
|
2145
|
-
constructor(kind, message, cause) {
|
|
2146
|
-
super(message);
|
|
2147
|
-
this.name = "ACPSessionError";
|
|
2148
|
-
this.kind = kind;
|
|
2149
|
-
this.cause = cause;
|
|
2150
|
-
}
|
|
2151
|
-
};
|
|
2152
|
-
function isJsonRpcError(v) {
|
|
2153
|
-
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
2154
|
-
}
|
|
2155
|
-
function isAuthRequiredError(err) {
|
|
2156
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2157
|
-
if (/auth(_|-)?required|authentication required/i.test(message)) return true;
|
|
2158
|
-
const cause = err instanceof ACPSessionError ? err.cause : err && typeof err === "object" && "cause" in err ? err.cause : err;
|
|
2159
|
-
if (!cause || typeof cause !== "object") return false;
|
|
2160
|
-
const data = cause.data;
|
|
2161
|
-
if (data === "auth_required" || data === "AUTH_REQUIRED") return true;
|
|
2162
|
-
if (data && typeof data === "object") {
|
|
2163
|
-
const d = data;
|
|
2164
|
-
if (d.authRequired === true) return true;
|
|
2165
|
-
if (d.code === "auth_required" || d.code === "AUTH_REQUIRED") return true;
|
|
2166
|
-
}
|
|
2167
|
-
return false;
|
|
2168
|
-
}
|
|
2169
|
-
|
|
2170
|
-
// src/client/acp-session-ops.ts
|
|
2171
|
-
function filterMcpServers(agentCapabilities, servers) {
|
|
2172
|
-
if (!servers || servers.length === 0) return [];
|
|
2173
|
-
const mcpCaps = agentCapabilities.mcpCapabilities ?? {};
|
|
2174
|
-
return servers.filter((s) => {
|
|
2175
|
-
if ("type" in s && s.type === "http") return mcpCaps.http === true;
|
|
2176
|
-
if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
|
|
2177
|
-
return true;
|
|
2178
|
-
});
|
|
2179
|
-
}
|
|
2180
|
-
async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
|
|
2181
|
-
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2182
|
-
if (!ctx.agentCapabilities.loadSession) {
|
|
2183
|
-
throw new ACPSessionError(
|
|
2184
|
-
"unsupported_capability",
|
|
2185
|
-
"agent does not support session/load (loadSession capability not advertised)"
|
|
2186
|
-
);
|
|
2187
|
-
}
|
|
2188
|
-
if (ctx.sessionId) {
|
|
2189
|
-
await ctx.closeSession();
|
|
2190
|
-
}
|
|
2191
|
-
ctx.resetScratch();
|
|
2192
|
-
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
2193
|
-
const id = ctx.allocId();
|
|
2194
|
-
const result = await ctx.sendRequest(id, "session/load", {
|
|
2195
|
-
sessionId,
|
|
2196
|
-
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
2197
|
-
mcpServers: servers
|
|
2198
|
-
});
|
|
2199
|
-
if (isJsonRpcError(result)) {
|
|
2200
|
-
throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
|
|
2201
|
-
}
|
|
2202
|
-
ctx.setSessionId(sessionId);
|
|
2203
|
-
}
|
|
2204
|
-
async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
|
|
2205
|
-
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2206
|
-
if (!ctx.agentCapabilities.sessionCapabilities?.resume) {
|
|
2207
|
-
throw new ACPSessionError(
|
|
2208
|
-
"unsupported_capability",
|
|
2209
|
-
"agent does not support session/resume (sessionCapabilities.resume not advertised)"
|
|
2210
|
-
);
|
|
2211
|
-
}
|
|
2212
|
-
if (ctx.sessionId) {
|
|
2213
|
-
await ctx.closeSession();
|
|
2214
|
-
}
|
|
2215
|
-
const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
|
|
2216
|
-
const id = ctx.allocId();
|
|
2217
|
-
const result = await ctx.sendRequest(id, "session/resume", {
|
|
2218
|
-
sessionId,
|
|
2219
|
-
cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
|
|
2220
|
-
mcpServers: servers
|
|
2221
|
-
});
|
|
2222
|
-
if (isJsonRpcError(result)) {
|
|
2223
|
-
throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
|
|
2224
|
-
}
|
|
2225
|
-
ctx.setSessionId(sessionId);
|
|
2226
|
-
}
|
|
2227
|
-
async function executeListSessions(ctx, cursor, cwd) {
|
|
2228
|
-
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2229
|
-
if (!ctx.agentCapabilities.sessionCapabilities?.list) {
|
|
2230
|
-
throw new ACPSessionError(
|
|
2231
|
-
"unsupported_capability",
|
|
2232
|
-
"agent does not support session/list (sessionCapabilities.list not advertised)"
|
|
2233
|
-
);
|
|
2234
|
-
}
|
|
2235
|
-
const id = ctx.allocId();
|
|
2236
|
-
const params = {};
|
|
2237
|
-
if (cursor !== void 0) params.cursor = cursor;
|
|
2238
|
-
if (cwd !== void 0) params.cwd = cwd;
|
|
2239
|
-
const result = await ctx.sendRequest(id, "session/list", params);
|
|
2240
|
-
if (isJsonRpcError(result)) {
|
|
2241
|
-
throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
|
|
2242
|
-
}
|
|
2243
|
-
const r = result;
|
|
2244
|
-
return {
|
|
2245
|
-
sessions: r.sessions ?? [],
|
|
2246
|
-
nextCursor: r.nextCursor
|
|
2247
|
-
};
|
|
2248
|
-
}
|
|
2249
|
-
async function executeDeleteSession(ctx, sessionId) {
|
|
2250
|
-
if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
|
|
2251
|
-
if (!ctx.agentCapabilities.sessionCapabilities?.delete) {
|
|
2252
|
-
throw new ACPSessionError(
|
|
2253
|
-
"unsupported_capability",
|
|
2254
|
-
"agent does not support session/delete (sessionCapabilities.delete not advertised)"
|
|
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
|
|
2255
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";
|
|
2256
2570
|
}
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
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
|
+
}
|
|
2261
2585
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
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?.();
|
|
2264
2598
|
}
|
|
2265
2599
|
}
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
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 });
|
|
2274
2608
|
});
|
|
2275
|
-
if (isJsonRpcError(result)) {
|
|
2276
|
-
throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
|
|
2277
|
-
}
|
|
2278
|
-
const newId = result.sessionId;
|
|
2279
|
-
if (typeof newId !== "string" || !newId) {
|
|
2280
|
-
throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
|
|
2281
|
-
}
|
|
2282
|
-
return newId;
|
|
2283
2609
|
}
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
const id = ctx.allocId();
|
|
2287
|
-
const result = await ctx.sendRequest(id, "session/set_mode", { sessionId, modeId });
|
|
2288
|
-
if (isJsonRpcError(result)) {
|
|
2289
|
-
throw new ACPSessionError(
|
|
2290
|
-
"prompt_failed",
|
|
2291
|
-
`session/set_mode failed: ${result.message}`,
|
|
2292
|
-
result
|
|
2293
|
-
);
|
|
2294
|
-
}
|
|
2610
|
+
function isAbortLikeError(err) {
|
|
2611
|
+
return err instanceof Error && /cancelled|canceled|timed out|aborted/i.test(err.message);
|
|
2295
2612
|
}
|
|
2296
|
-
|
|
2297
|
-
if (
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
sessionId,
|
|
2301
|
-
configId,
|
|
2302
|
-
value
|
|
2303
|
-
});
|
|
2304
|
-
if (isJsonRpcError(result)) {
|
|
2305
|
-
throw new ACPSessionError(
|
|
2306
|
-
"prompt_failed",
|
|
2307
|
-
`session/set_config_option failed: ${result.message}`,
|
|
2308
|
-
result
|
|
2309
|
-
);
|
|
2310
|
-
}
|
|
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;
|
|
2311
2617
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
if (isJsonRpcError(result)) {
|
|
2317
|
-
throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
|
|
2318
|
-
}
|
|
2319
|
-
const r = result;
|
|
2320
|
-
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 };
|
|
2321
2622
|
}
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
const id = ctx.allocId();
|
|
2325
|
-
const result = await ctx.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
|
|
2326
|
-
if (isJsonRpcError(result)) {
|
|
2327
|
-
throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
|
|
2328
|
-
}
|
|
2623
|
+
function imageContent(mimeType, data) {
|
|
2624
|
+
return { type: "image", mimeType, data };
|
|
2329
2625
|
}
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
const id = ctx.allocId();
|
|
2333
|
-
const result = await ctx.sendRequest(id, "providers/disable", {});
|
|
2334
|
-
if (isJsonRpcError(result)) {
|
|
2335
|
-
throw new ACPSessionError(
|
|
2336
|
-
"prompt_failed",
|
|
2337
|
-
`providers/disable failed: ${result.message}`,
|
|
2338
|
-
result
|
|
2339
|
-
);
|
|
2340
|
-
}
|
|
2626
|
+
function audioContent(mimeType, data) {
|
|
2627
|
+
return { type: "audio", mimeType, data };
|
|
2341
2628
|
}
|
|
2342
|
-
|
|
2343
|
-
if (
|
|
2344
|
-
const
|
|
2345
|
-
|
|
2346
|
-
if (
|
|
2347
|
-
|
|
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;
|
|
2348
2635
|
}
|
|
2349
|
-
return
|
|
2636
|
+
return "";
|
|
2350
2637
|
}
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
);
|
|
2364
|
-
}
|
|
2365
|
-
const sessionId = result.sessionId;
|
|
2366
|
-
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
2367
|
-
throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
|
|
2368
|
-
}
|
|
2369
|
-
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
|
+
};
|
|
2370
2650
|
}
|
|
2371
2651
|
|
|
2372
2652
|
// src/client/acp-session-updates.ts
|
|
@@ -2387,6 +2667,7 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress) {
|
|
|
2387
2667
|
}
|
|
2388
2668
|
return;
|
|
2389
2669
|
}
|
|
2670
|
+
case "agent_thought_chunk":
|
|
2390
2671
|
case "thought_chunk": {
|
|
2391
2672
|
const text = extractText(u.content);
|
|
2392
2673
|
if (text) {
|
|
@@ -2584,7 +2865,7 @@ var TerminalServer = class {
|
|
|
2584
2865
|
truncated: false,
|
|
2585
2866
|
exitStatus: void 0,
|
|
2586
2867
|
timeoutHandle: null,
|
|
2587
|
-
exitPromise: new Promise((
|
|
2868
|
+
exitPromise: new Promise((resolve5) => {
|
|
2588
2869
|
proc.on("close", (code, signalName) => {
|
|
2589
2870
|
if (state.timeoutHandle) {
|
|
2590
2871
|
clearTimeout(state.timeoutHandle);
|
|
@@ -2595,7 +2876,7 @@ var TerminalServer = class {
|
|
|
2595
2876
|
signal: typeof signalName === "string" ? signalName : null
|
|
2596
2877
|
};
|
|
2597
2878
|
state.exitStatus = exitStatus;
|
|
2598
|
-
|
|
2879
|
+
resolve5(exitStatus);
|
|
2599
2880
|
});
|
|
2600
2881
|
proc.on("error", (err) => {
|
|
2601
2882
|
if (state.timeoutHandle) {
|
|
@@ -2614,7 +2895,7 @@ var TerminalServer = class {
|
|
|
2614
2895
|
}
|
|
2615
2896
|
state.outputChunks.push(errorOutput);
|
|
2616
2897
|
state.retainedBytes = errorOutput.length;
|
|
2617
|
-
|
|
2898
|
+
resolve5(exitStatus);
|
|
2618
2899
|
});
|
|
2619
2900
|
})
|
|
2620
2901
|
};
|
|
@@ -2921,7 +3202,7 @@ var WebSocketClientTransport = class {
|
|
|
2921
3202
|
);
|
|
2922
3203
|
}
|
|
2923
3204
|
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
2924
|
-
return new Promise((
|
|
3205
|
+
return new Promise((resolve5, reject) => {
|
|
2925
3206
|
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
2926
3207
|
this.ws = ws;
|
|
2927
3208
|
const timer = setTimeout(() => {
|
|
@@ -2937,7 +3218,7 @@ var WebSocketClientTransport = class {
|
|
|
2937
3218
|
}
|
|
2938
3219
|
pending.reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
2939
3220
|
}, timeoutMs);
|
|
2940
|
-
this.pendingStart = { resolve:
|
|
3221
|
+
this.pendingStart = { resolve: resolve5, reject, timer };
|
|
2941
3222
|
ws.addEventListener("open", () => {
|
|
2942
3223
|
const pending = this.pendingStart;
|
|
2943
3224
|
if (pending === null) return;
|
|
@@ -3052,7 +3333,6 @@ function finitePositiveLimit(value, fallback) {
|
|
|
3052
3333
|
}
|
|
3053
3334
|
|
|
3054
3335
|
// src/client/acp-session.ts
|
|
3055
|
-
var LATE_CANCEL_SEND_TIMEOUT_MS = 1e4;
|
|
3056
3336
|
var ACPSession = class _ACPSession {
|
|
3057
3337
|
transport;
|
|
3058
3338
|
fileServer;
|
|
@@ -3222,7 +3502,7 @@ var ACPSession = class _ACPSession {
|
|
|
3222
3502
|
throw new ACPSessionError("protocol_error", "initialize returned no protocolVersion");
|
|
3223
3503
|
}
|
|
3224
3504
|
const r = result;
|
|
3225
|
-
if (r.protocolVersion
|
|
3505
|
+
if (r.protocolVersion !== ACP_PROTOCOL_VERSION) {
|
|
3226
3506
|
throw new ACPSessionError(
|
|
3227
3507
|
"unsupported_capability",
|
|
3228
3508
|
`agent requires protocolVersion=${r.protocolVersion}, client supports up to ${ACP_PROTOCOL_VERSION}`
|
|
@@ -3242,49 +3522,14 @@ var ACPSession = class _ACPSession {
|
|
|
3242
3522
|
* Call this AFTER start() and BEFORE any session/new call.
|
|
3243
3523
|
*/
|
|
3244
3524
|
async authenticate(methodId) {
|
|
3245
|
-
|
|
3246
|
-
throw new ACPSessionError("closed", "session is closed");
|
|
3247
|
-
}
|
|
3248
|
-
if (this.state !== "ready" && this.state !== "authenticated") {
|
|
3249
|
-
throw new ACPSessionError(
|
|
3250
|
-
"protocol_error",
|
|
3251
|
-
`authenticate called in state=${this.state} (expected 'ready')`
|
|
3252
|
-
);
|
|
3253
|
-
}
|
|
3254
|
-
if (this.state === "authenticated") return;
|
|
3255
|
-
if (!this.authMethods.some((m) => m.id === methodId)) {
|
|
3256
|
-
throw new ACPSessionError(
|
|
3257
|
-
"auth_failed",
|
|
3258
|
-
`auth method "${methodId}" not in advertised methods: ${this.authMethods.map((m) => m.id).join(", ")}`
|
|
3259
|
-
);
|
|
3260
|
-
}
|
|
3261
|
-
const id = this.allocId();
|
|
3262
|
-
const result = await this.sendRequest(id, "authenticate", { methodId });
|
|
3263
|
-
if (isJsonRpcError(result)) {
|
|
3264
|
-
throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
|
|
3265
|
-
}
|
|
3266
|
-
this.state = "authenticated";
|
|
3525
|
+
return authenticate(this.acpSessionAuthHost(), methodId);
|
|
3267
3526
|
}
|
|
3268
3527
|
/**
|
|
3269
3528
|
* Log out from the current authenticated session.
|
|
3270
3529
|
* Only callable if the agent advertises `auth.logout` capability.
|
|
3271
3530
|
*/
|
|
3272
3531
|
async logout() {
|
|
3273
|
-
|
|
3274
|
-
throw new ACPSessionError("closed", "session is closed");
|
|
3275
|
-
}
|
|
3276
|
-
if (!this.agentCapabilities.auth?.logout) {
|
|
3277
|
-
throw new ACPSessionError(
|
|
3278
|
-
"unsupported_capability",
|
|
3279
|
-
"agent does not support logout (auth.logout capability not advertised)"
|
|
3280
|
-
);
|
|
3281
|
-
}
|
|
3282
|
-
const id = this.allocId();
|
|
3283
|
-
const result = await this.sendRequest(id, "logout", {});
|
|
3284
|
-
if (isJsonRpcError(result)) {
|
|
3285
|
-
throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
|
|
3286
|
-
}
|
|
3287
|
-
this.state = "ready";
|
|
3532
|
+
return logout(this.acpSessionAuthHost());
|
|
3288
3533
|
}
|
|
3289
3534
|
// ──────────────────────────────────────────────────────────────────────
|
|
3290
3535
|
// Session management delegation
|
|
@@ -3353,6 +3598,16 @@ var ACPSession = class _ACPSession {
|
|
|
3353
3598
|
if (signal.aborted) {
|
|
3354
3599
|
return emptyRunResult("cancelled");
|
|
3355
3600
|
}
|
|
3601
|
+
const caps = this.agentCapabilities.promptCapabilities;
|
|
3602
|
+
for (const block of blocks) {
|
|
3603
|
+
const supported = block.type === "image" ? caps?.image : block.type === "audio" ? caps?.audio : block.type === "resource" ? caps?.embeddedContext : true;
|
|
3604
|
+
if (supported !== true) {
|
|
3605
|
+
throw new ACPSessionError(
|
|
3606
|
+
"unsupported_capability",
|
|
3607
|
+
`agent does not support ${block.type} prompt content`
|
|
3608
|
+
);
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3356
3611
|
let cancelled = false;
|
|
3357
3612
|
this.promptCallbackAbort = new AbortController();
|
|
3358
3613
|
const onAbort = () => {
|
|
@@ -3487,50 +3742,7 @@ var ACPSession = class _ACPSession {
|
|
|
3487
3742
|
* being silently swallowed.
|
|
3488
3743
|
*/
|
|
3489
3744
|
cancelLateSession(createPromise) {
|
|
3490
|
-
|
|
3491
|
-
try {
|
|
3492
|
-
await new Promise((resolve4, reject) => {
|
|
3493
|
-
const timer = setTimeout(() => {
|
|
3494
|
-
reject(new Error("late session/cancel send timed out"));
|
|
3495
|
-
}, LATE_CANCEL_SEND_TIMEOUT_MS);
|
|
3496
|
-
Promise.resolve(
|
|
3497
|
-
this.transport.send({
|
|
3498
|
-
jsonrpc: "2.0",
|
|
3499
|
-
method: "session/cancel",
|
|
3500
|
-
params: { sessionId: lateId }
|
|
3501
|
-
})
|
|
3502
|
-
).then(
|
|
3503
|
-
() => {
|
|
3504
|
-
clearTimeout(timer);
|
|
3505
|
-
resolve4();
|
|
3506
|
-
},
|
|
3507
|
-
(sendErr) => {
|
|
3508
|
-
clearTimeout(timer);
|
|
3509
|
-
reject(sendErr instanceof Error ? sendErr : new Error(String(sendErr)));
|
|
3510
|
-
}
|
|
3511
|
-
);
|
|
3512
|
-
});
|
|
3513
|
-
} catch (err) {
|
|
3514
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
3515
|
-
console.warn(
|
|
3516
|
-
JSON.stringify({
|
|
3517
|
-
level: "warn",
|
|
3518
|
-
event: "acp_session.late_cancel_failed",
|
|
3519
|
-
sessionId: lateId,
|
|
3520
|
-
message
|
|
3521
|
-
})
|
|
3522
|
-
);
|
|
3523
|
-
}
|
|
3524
|
-
}).catch((reason) => {
|
|
3525
|
-
const message = reason instanceof Error ? reason.message : String(reason);
|
|
3526
|
-
console.warn(
|
|
3527
|
-
JSON.stringify({
|
|
3528
|
-
level: "warn",
|
|
3529
|
-
event: "acp_session.late_cancel_failed",
|
|
3530
|
-
reason: message
|
|
3531
|
-
})
|
|
3532
|
-
);
|
|
3533
|
-
});
|
|
3745
|
+
cancelLateAcpSession(this.transport, createPromise);
|
|
3534
3746
|
}
|
|
3535
3747
|
async closeSession() {
|
|
3536
3748
|
if (!this.sessionId) return;
|
|
@@ -3577,10 +3789,16 @@ var ACPSession = class _ACPSession {
|
|
|
3577
3789
|
// Wire layer
|
|
3578
3790
|
// ────────────────────────────────────────────────────────────────────
|
|
3579
3791
|
allocId() {
|
|
3580
|
-
|
|
3792
|
+
let candidate = Number.isSafeInteger(this.nextId) && this.nextId >= 1 ? this.nextId : 1;
|
|
3793
|
+
for (let probe = 0; probe <= this.pending.size; probe++) {
|
|
3794
|
+
this.nextId = candidate >= Number.MAX_SAFE_INTEGER ? 1 : candidate + 1;
|
|
3795
|
+
if (!this.pending.has(candidate)) return candidate;
|
|
3796
|
+
candidate = this.nextId;
|
|
3797
|
+
}
|
|
3798
|
+
throw new ACPSessionError("protocol_error", "no JSON-RPC request id is available");
|
|
3581
3799
|
}
|
|
3582
3800
|
async sendRequest(id, method, params, timeoutMs) {
|
|
3583
|
-
return new Promise((
|
|
3801
|
+
return new Promise((resolve5, reject) => {
|
|
3584
3802
|
const effectiveTimeout = timeoutMs ?? this.timeoutMs;
|
|
3585
3803
|
const handle = setTimeout(() => {
|
|
3586
3804
|
this.pending.delete(id);
|
|
@@ -3590,7 +3808,7 @@ var ACPSession = class _ACPSession {
|
|
|
3590
3808
|
}, effectiveTimeout);
|
|
3591
3809
|
this.pending.set(id, {
|
|
3592
3810
|
method,
|
|
3593
|
-
resolve:
|
|
3811
|
+
resolve: resolve5,
|
|
3594
3812
|
reject,
|
|
3595
3813
|
timeoutMs: effectiveTimeout,
|
|
3596
3814
|
timeoutHandle: handle
|
|
@@ -3609,19 +3827,7 @@ var ACPSession = class _ACPSession {
|
|
|
3609
3827
|
* `authMethods`; we do not pop OAuth on every spawn.
|
|
3610
3828
|
*/
|
|
3611
3829
|
async createSessionWithAuth() {
|
|
3612
|
-
|
|
3613
|
-
return await executeCreateSession(this.opContext());
|
|
3614
|
-
} catch (err) {
|
|
3615
|
-
if (this.state === "authenticated" || !isAuthRequiredError(err)) {
|
|
3616
|
-
throw err instanceof ACPSessionError ? err : new ACPSessionError(
|
|
3617
|
-
"session_create_failed",
|
|
3618
|
-
err instanceof Error ? err.message : String(err),
|
|
3619
|
-
err
|
|
3620
|
-
);
|
|
3621
|
-
}
|
|
3622
|
-
await this.ensureAuthenticated();
|
|
3623
|
-
return executeCreateSession(this.opContext());
|
|
3624
|
-
}
|
|
3830
|
+
return createSessionWithAuth(this.acpSessionAuthHost());
|
|
3625
3831
|
}
|
|
3626
3832
|
/**
|
|
3627
3833
|
* Pick a non-terminal auth method and run `authenticate`. Terminal-only
|
|
@@ -3629,27 +3835,7 @@ var ACPSession = class _ACPSession {
|
|
|
3629
3835
|
* we refuse rather than hang a TUI inside the JSON-RPC child.
|
|
3630
3836
|
*/
|
|
3631
3837
|
async ensureAuthenticated() {
|
|
3632
|
-
|
|
3633
|
-
if (this.authMethods.length === 0) {
|
|
3634
|
-
throw new ACPSessionError(
|
|
3635
|
-
"auth_failed",
|
|
3636
|
-
"This agent requires authentication before a session can start, but advertised no authMethods. Log into the CLI, then retry."
|
|
3637
|
-
);
|
|
3638
|
-
}
|
|
3639
|
-
const inProcess = this.authMethods.find(
|
|
3640
|
-
(m) => m.type === void 0 || m.type === "agent" || m.type === "oauth" || m.type === "http"
|
|
3641
|
-
);
|
|
3642
|
-
if (inProcess) {
|
|
3643
|
-
await this.authenticate(inProcess.id);
|
|
3644
|
-
return;
|
|
3645
|
-
}
|
|
3646
|
-
const terminal = this.authMethods.find((m) => m.type === "terminal");
|
|
3647
|
-
const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
|
|
3648
|
-
const setup = setupArgs !== void 0 ? `${this.opts.command} ${setupArgs}` : `${this.opts.command}${this.opts.args?.length ? ` ${this.opts.args.join(" ")}` : ""}`;
|
|
3649
|
-
throw new ACPSessionError(
|
|
3650
|
-
"auth_failed",
|
|
3651
|
-
`This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
|
|
3652
|
-
);
|
|
3838
|
+
return ensureAuthenticated(this.acpSessionAuthHost());
|
|
3653
3839
|
}
|
|
3654
3840
|
sendResult(id, result) {
|
|
3655
3841
|
return this.transport.send({ jsonrpc: "2.0", id, result });
|
|
@@ -3727,7 +3913,7 @@ var ACPSession = class _ACPSession {
|
|
|
3727
3913
|
}
|
|
3728
3914
|
if (isBestEffortAckMethod(msg.method)) {
|
|
3729
3915
|
if (msg.id !== void 0) {
|
|
3730
|
-
this.
|
|
3916
|
+
this.sendErrorResponse(msg.id, -32601, `Unsupported method: ${msg.method}`).catch(() => {
|
|
3731
3917
|
});
|
|
3732
3918
|
}
|
|
3733
3919
|
return;
|
|
@@ -3736,6 +3922,13 @@ var ACPSession = class _ACPSession {
|
|
|
3736
3922
|
return;
|
|
3737
3923
|
}
|
|
3738
3924
|
if (msg.method) {
|
|
3925
|
+
if (msg.id !== void 0) {
|
|
3926
|
+
void this.sendErrorResponse(msg.id, -32601, `Unsupported method: ${msg.method}`).catch(
|
|
3927
|
+
() => {
|
|
3928
|
+
}
|
|
3929
|
+
);
|
|
3930
|
+
return;
|
|
3931
|
+
}
|
|
3739
3932
|
console.warn(
|
|
3740
3933
|
JSON.stringify({
|
|
3741
3934
|
level: "warn",
|
|
@@ -3758,6 +3951,29 @@ var ACPSession = class _ACPSession {
|
|
|
3758
3951
|
resetScratch() {
|
|
3759
3952
|
this.scratch = createSessionScratch();
|
|
3760
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
|
+
}
|
|
3761
3977
|
};
|
|
3762
3978
|
|
|
3763
3979
|
// src/client/tool-translator.ts
|
|
@@ -3809,12 +4025,12 @@ var ToolTranslator = class {
|
|
|
3809
4025
|
id: callId,
|
|
3810
4026
|
params: { name, arguments: args }
|
|
3811
4027
|
});
|
|
3812
|
-
return new Promise((
|
|
4028
|
+
return new Promise((resolve5, reject) => {
|
|
3813
4029
|
const timeout = setTimeout(() => {
|
|
3814
4030
|
this.pending.delete(callId);
|
|
3815
4031
|
reject(new Error(`Tool call ${name} timed out after ${this.opts.totalTimeoutMs}ms`));
|
|
3816
4032
|
}, this.opts.totalTimeoutMs);
|
|
3817
|
-
this.pending.set(callId, { resolve:
|
|
4033
|
+
this.pending.set(callId, { resolve: resolve5, reject, timeout });
|
|
3818
4034
|
});
|
|
3819
4035
|
}
|
|
3820
4036
|
cancelAll() {
|
|
@@ -4637,7 +4853,7 @@ async function probeWithBound(items, worker, limit) {
|
|
|
4637
4853
|
}
|
|
4638
4854
|
async function defaultProbe(desc, timeoutMs, spawnProcess = spawn2, platform = process.platform) {
|
|
4639
4855
|
const start = Date.now();
|
|
4640
|
-
return new Promise((
|
|
4856
|
+
return new Promise((resolve5) => {
|
|
4641
4857
|
let settled = false;
|
|
4642
4858
|
let stdout = "";
|
|
4643
4859
|
let stderr = "";
|
|
@@ -4650,7 +4866,7 @@ async function defaultProbe(desc, timeoutMs, spawnProcess = spawn2, platform = p
|
|
|
4650
4866
|
}
|
|
4651
4867
|
} catch {
|
|
4652
4868
|
}
|
|
4653
|
-
|
|
4869
|
+
resolve5(result);
|
|
4654
4870
|
};
|
|
4655
4871
|
let child;
|
|
4656
4872
|
try {
|