@wrongstack/acp 0.297.0 → 0.298.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 +39 -46
- package/dist/agent/session-store.d.ts +55 -0
- package/dist/agent/stdio-transport.d.ts +74 -1
- package/dist/agent.js +472 -210
- package/dist/client/terminal-server.d.ts +5 -1
- package/dist/client.js +70 -44
- package/dist/index.js +433 -253
- package/dist/win32-cmd.d.ts +9 -6
- package/dist/wrongstack-acp-agent.js +312 -157
- package/package.json +4 -3
- package/dist/agent/index.d.ts.map +0 -1
- package/dist/agent/protocol-contract.d.ts.map +0 -1
- package/dist/agent/protocol-handler.d.ts.map +0 -1
- package/dist/agent/server-agent-turn.d.ts.map +0 -1
- package/dist/agent/session-store.d.ts.map +0 -1
- package/dist/agent/stdio-transport.d.ts.map +0 -1
- package/dist/agent/tools-registry.d.ts.map +0 -1
- package/dist/agent/wrongstack-acp-agent.d.ts.map +0 -1
- package/dist/agent/ws-bridge-transport.d.ts.map +0 -1
- package/dist/agent.js.map +0 -7
- package/dist/client/acp-message-routing.d.ts.map +0 -1
- package/dist/client/acp-session-callbacks.d.ts.map +0 -1
- package/dist/client/acp-session-content.d.ts.map +0 -1
- package/dist/client/acp-session-errors.d.ts.map +0 -1
- package/dist/client/acp-session-types.d.ts.map +0 -1
- package/dist/client/acp-session-updates.d.ts.map +0 -1
- package/dist/client/acp-session.d.ts.map +0 -1
- package/dist/client/file-server.d.ts.map +0 -1
- package/dist/client/index.d.ts.map +0 -1
- package/dist/client/permission.d.ts.map +0 -1
- package/dist/client/terminal-server.d.ts.map +0 -1
- package/dist/client/tool-translator.d.ts.map +0 -1
- package/dist/client/trust-boundary-permission.d.ts.map +0 -1
- package/dist/client/websocket-transport.d.ts.map +0 -1
- package/dist/client.js.map +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -7
- package/dist/integration/acp-bench.d.ts.map +0 -1
- package/dist/integration/acp-subagent-runner.d.ts.map +0 -1
- package/dist/integration/ensemble-runner.d.ts.map +0 -1
- package/dist/integration/run-one-acp-task.d.ts.map +0 -1
- package/dist/legacy.d.ts.map +0 -1
- package/dist/legacy.js.map +0 -7
- package/dist/registry/acp-registry-fetch.d.ts.map +0 -1
- package/dist/registry/agents.catalog.d.ts.map +0 -1
- package/dist/registry/ensemble-registry.d.ts.map +0 -1
- package/dist/sdk.d.ts.map +0 -1
- package/dist/sdk.js.map +0 -7
- package/dist/types/acp-messages.d.ts.map +0 -1
- package/dist/types/acp-v1.d.ts.map +0 -1
- package/dist/v1.d.ts.map +0 -1
- package/dist/v1.js.map +0 -7
- package/dist/version.d.ts.map +0 -1
- package/dist/win32-cmd.d.ts.map +0 -1
- package/dist/wrongstack-acp-agent.js.map +0 -7
package/dist/agent.js
CHANGED
|
@@ -3,6 +3,7 @@ import { expectDefined, writeErr } from "@wrongstack/core/utils";
|
|
|
3
3
|
import { treeKill } from "@wrongstack/core/utils/tree-kill";
|
|
4
4
|
var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
|
|
5
5
|
var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
|
|
6
|
+
var DEFAULT_MAX_QUEUED_CHARS = 32 * 1024 * 1024;
|
|
6
7
|
function positiveLimit(value, fallback) {
|
|
7
8
|
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
8
9
|
}
|
|
@@ -12,53 +13,82 @@ var StdioTransport = class {
|
|
|
12
13
|
stderr = process.stderr;
|
|
13
14
|
buffer = "";
|
|
14
15
|
handlers = /* @__PURE__ */ new Set();
|
|
16
|
+
claimHandlers = /* @__PURE__ */ new Set();
|
|
15
17
|
closed = false;
|
|
16
18
|
resolveRead = null;
|
|
17
19
|
messageQueue = [];
|
|
20
|
+
queuedChars = 0;
|
|
18
21
|
maxFrameChars;
|
|
19
22
|
maxQueuedMessages;
|
|
23
|
+
maxQueuedChars;
|
|
24
|
+
onStdinData = (chunk) => this.onData(chunk);
|
|
25
|
+
onStdinEnd = () => this.handleClose();
|
|
26
|
+
onStdinError = (err) => this.failAll(err);
|
|
20
27
|
constructor(opts = {}) {
|
|
21
28
|
this.maxFrameChars = positiveLimit(opts.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
|
|
22
29
|
this.maxQueuedMessages = positiveLimit(opts.maxQueuedMessages, DEFAULT_MAX_QUEUED_MESSAGES);
|
|
30
|
+
this.maxQueuedChars = positiveLimit(opts.maxQueuedChars, DEFAULT_MAX_QUEUED_CHARS);
|
|
23
31
|
this.stdin.resume();
|
|
24
32
|
this.stdin.setEncoding("utf8");
|
|
25
|
-
this.stdin.on("data",
|
|
26
|
-
this.stdin.on("end",
|
|
27
|
-
this.stdin.on("error",
|
|
33
|
+
this.stdin.on("data", this.onStdinData);
|
|
34
|
+
this.stdin.on("end", this.onStdinEnd);
|
|
35
|
+
this.stdin.on("error", this.onStdinError);
|
|
28
36
|
}
|
|
29
37
|
sendStartupMarker() {
|
|
30
38
|
this.stdout.write("[wstack-acp]\n", "utf8");
|
|
31
39
|
}
|
|
32
40
|
send(msg) {
|
|
33
41
|
if (this.closed) return Promise.resolve();
|
|
34
|
-
return new Promise((
|
|
42
|
+
return new Promise((resolve2) => {
|
|
35
43
|
const line = JSON.stringify(msg) + "\n";
|
|
36
|
-
this.stdout.write(line, "utf8", () =>
|
|
44
|
+
this.stdout.write(line, "utf8", () => resolve2());
|
|
37
45
|
});
|
|
38
46
|
}
|
|
39
47
|
sendRaw(chunk) {
|
|
40
48
|
this.stdout.write(chunk, "utf8");
|
|
41
49
|
}
|
|
42
50
|
read() {
|
|
43
|
-
if (this.messageQueue.length > 0)
|
|
44
|
-
|
|
51
|
+
if (this.messageQueue.length > 0) {
|
|
52
|
+
const queued = expectDefined(this.messageQueue.shift());
|
|
53
|
+
this.queuedChars = Math.max(0, this.queuedChars - queued.chars);
|
|
54
|
+
return Promise.resolve(queued.message);
|
|
55
|
+
}
|
|
45
56
|
if (this.closed) return Promise.resolve(null);
|
|
46
|
-
return new Promise((
|
|
47
|
-
this.resolveRead =
|
|
57
|
+
return new Promise((resolve2) => {
|
|
58
|
+
this.resolveRead = resolve2;
|
|
48
59
|
});
|
|
49
60
|
}
|
|
50
61
|
onMessage(handler) {
|
|
51
62
|
this.handlers.add(handler);
|
|
52
63
|
return () => this.handlers.delete(handler);
|
|
53
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Register a handler that MAY claim a message by returning `true`.
|
|
67
|
+
* Claimed messages are NOT enqueued for the read() loop — they are
|
|
68
|
+
* considered fully consumed by the handler. This is how the ACP
|
|
69
|
+
* server transport prevents the correlation handler (which always
|
|
70
|
+
* fires on every message but only actually correlates responses)
|
|
71
|
+
* from starving the read() loop of pipelined requests, notifications,
|
|
72
|
+
* and any message the handler chose to ignore. See dispatch() for
|
|
73
|
+
* the gating logic.
|
|
74
|
+
*/
|
|
75
|
+
onMessageClaim(handler) {
|
|
76
|
+
this.claimHandlers.add(handler);
|
|
77
|
+
return () => this.claimHandlers.delete(handler);
|
|
78
|
+
}
|
|
54
79
|
close() {
|
|
55
80
|
this.closed = true;
|
|
81
|
+
this.stdin.off("data", this.onStdinData);
|
|
82
|
+
this.stdin.off("end", this.onStdinEnd);
|
|
83
|
+
this.stdin.off("error", this.onStdinError);
|
|
56
84
|
this.stdin.pause();
|
|
57
85
|
this.resolveRead?.(null);
|
|
58
86
|
this.resolveRead = null;
|
|
59
87
|
this.buffer = "";
|
|
60
88
|
this.messageQueue.length = 0;
|
|
89
|
+
this.queuedChars = 0;
|
|
61
90
|
this.handlers.clear();
|
|
91
|
+
this.claimHandlers.clear();
|
|
62
92
|
}
|
|
63
93
|
onData(chunk) {
|
|
64
94
|
this.buffer += chunk;
|
|
@@ -85,29 +115,41 @@ var StdioTransport = class {
|
|
|
85
115
|
return;
|
|
86
116
|
}
|
|
87
117
|
try {
|
|
88
|
-
this.dispatch(JSON.parse(raw));
|
|
118
|
+
this.dispatch(JSON.parse(raw), raw.length);
|
|
89
119
|
} catch (err) {
|
|
90
120
|
this.stderr.write(`[wstack-acp parse error] ${err}
|
|
91
121
|
`, "utf8");
|
|
92
122
|
}
|
|
93
123
|
}
|
|
94
124
|
}
|
|
95
|
-
dispatch(msg) {
|
|
125
|
+
dispatch(msg, chars = JSON.stringify(msg).length) {
|
|
96
126
|
if (this.resolveRead) {
|
|
97
|
-
const
|
|
127
|
+
const resolve2 = this.resolveRead;
|
|
98
128
|
this.resolveRead = null;
|
|
99
|
-
|
|
129
|
+
resolve2(msg);
|
|
100
130
|
} else {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
131
|
+
let claimed = false;
|
|
132
|
+
for (const handler of this.claimHandlers) {
|
|
133
|
+
try {
|
|
134
|
+
if (handler(msg)) claimed = true;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
this.stderr.write(`[wstack-acp handler error] ${err}
|
|
137
|
+
`, "utf8");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!claimed) {
|
|
141
|
+
if (this.messageQueue.length >= this.maxQueuedMessages || this.queuedChars + chars > this.maxQueuedChars) {
|
|
142
|
+
this.stderr.write(
|
|
143
|
+
`[wstack-acp queue error] pending message queue exceeds ${this.maxQueuedMessages} entries or ${this.maxQueuedChars} characters
|
|
104
144
|
`,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
145
|
+
"utf8"
|
|
146
|
+
);
|
|
147
|
+
this.close();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
this.messageQueue.push({ message: msg, chars });
|
|
151
|
+
this.queuedChars += chars;
|
|
109
152
|
}
|
|
110
|
-
this.messageQueue.push(msg);
|
|
111
153
|
}
|
|
112
154
|
for (const handler of this.handlers) {
|
|
113
155
|
try {
|
|
@@ -242,6 +284,11 @@ function toolToPriority(tool) {
|
|
|
242
284
|
return "low";
|
|
243
285
|
}
|
|
244
286
|
|
|
287
|
+
// src/agent/protocol-handler.ts
|
|
288
|
+
import { randomUUID } from "node:crypto";
|
|
289
|
+
import * as fsp from "node:fs/promises";
|
|
290
|
+
import * as path from "node:path";
|
|
291
|
+
|
|
245
292
|
// src/types/acp-v1.ts
|
|
246
293
|
var ACP_PROTOCOL_VERSION = 1;
|
|
247
294
|
|
|
@@ -331,12 +378,12 @@ var ACPProtocolHandler = class {
|
|
|
331
378
|
*/
|
|
332
379
|
request(method, params, timeoutMs = 6e4) {
|
|
333
380
|
const id = `srv_${this.nextOutId++}`;
|
|
334
|
-
return new Promise((
|
|
381
|
+
return new Promise((resolve2, reject) => {
|
|
335
382
|
const timer = setTimeout(() => {
|
|
336
383
|
this.pendingOut.delete(id);
|
|
337
384
|
reject(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
338
385
|
}, timeoutMs);
|
|
339
|
-
this.pendingOut.set(id, { resolve, reject, timer });
|
|
386
|
+
this.pendingOut.set(id, { resolve: resolve2, reject, timer });
|
|
340
387
|
this.transport.send(toWire({ jsonrpc: "2.0", id, method, params })).catch((e) => {
|
|
341
388
|
clearTimeout(timer);
|
|
342
389
|
this.pendingOut.delete(id);
|
|
@@ -453,62 +500,68 @@ var ACPProtocolHandler = class {
|
|
|
453
500
|
this.clientCapabilities = p.clientCapabilities;
|
|
454
501
|
}
|
|
455
502
|
this.initialized = true;
|
|
456
|
-
await this.transport.send(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
503
|
+
await this.transport.send(
|
|
504
|
+
toWire({
|
|
505
|
+
jsonrpc: "2.0",
|
|
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
|
+
}
|
|
474
533
|
},
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
resume: {},
|
|
480
|
-
fork: {}
|
|
534
|
+
agentInfo: {
|
|
535
|
+
name: this.agentName,
|
|
536
|
+
title: "WrongStack",
|
|
537
|
+
version: WRONGSTACK_VERSION
|
|
481
538
|
},
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
title: "WrongStack",
|
|
489
|
-
version: WRONGSTACK_VERSION
|
|
490
|
-
},
|
|
491
|
-
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
492
|
-
modes: this.modes,
|
|
493
|
-
configOptions: this.configOptions
|
|
494
|
-
}
|
|
495
|
-
}));
|
|
539
|
+
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
540
|
+
modes: this.modes,
|
|
541
|
+
configOptions: this.configOptions
|
|
542
|
+
}
|
|
543
|
+
})
|
|
544
|
+
);
|
|
496
545
|
return false;
|
|
497
546
|
}
|
|
498
547
|
async handleAuthenticate(id, _params) {
|
|
499
|
-
await this.transport.send(
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
548
|
+
await this.transport.send(
|
|
549
|
+
toWire({
|
|
550
|
+
jsonrpc: "2.0",
|
|
551
|
+
id,
|
|
552
|
+
result: { outcome: "unauthenticated" }
|
|
553
|
+
})
|
|
554
|
+
);
|
|
504
555
|
return false;
|
|
505
556
|
}
|
|
506
557
|
async handleLogout(id, _params) {
|
|
507
|
-
await this.transport.send(
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
558
|
+
await this.transport.send(
|
|
559
|
+
toWire({
|
|
560
|
+
jsonrpc: "2.0",
|
|
561
|
+
id,
|
|
562
|
+
result: {}
|
|
563
|
+
})
|
|
564
|
+
);
|
|
512
565
|
return false;
|
|
513
566
|
}
|
|
514
567
|
async handleSessionNew(id, params) {
|
|
@@ -517,7 +570,15 @@ var ACPProtocolHandler = class {
|
|
|
517
570
|
return false;
|
|
518
571
|
}
|
|
519
572
|
const p = params ?? {};
|
|
520
|
-
|
|
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
|
+
}
|
|
521
582
|
const sessionId = `sess_${this.allocId()}`;
|
|
522
583
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
523
584
|
const state = {
|
|
@@ -547,15 +608,17 @@ var ACPProtocolHandler = class {
|
|
|
547
608
|
}
|
|
548
609
|
});
|
|
549
610
|
}
|
|
550
|
-
await this.transport.send(
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
+
);
|
|
559
622
|
return false;
|
|
560
623
|
}
|
|
561
624
|
async handleSessionLoad(id, params) {
|
|
@@ -570,9 +633,15 @@ var ACPProtocolHandler = class {
|
|
|
570
633
|
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
571
634
|
return false;
|
|
572
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;
|
|
573
642
|
const restored = {
|
|
574
643
|
id: sessionId,
|
|
575
|
-
cwd:
|
|
644
|
+
cwd: restoredCwd,
|
|
576
645
|
abort: new AbortController(),
|
|
577
646
|
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
578
647
|
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -588,13 +657,15 @@ var ACPProtocolHandler = class {
|
|
|
588
657
|
sessionId,
|
|
589
658
|
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
590
659
|
});
|
|
591
|
-
await this.transport.send(
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
+
);
|
|
598
669
|
return false;
|
|
599
670
|
}
|
|
600
671
|
}
|
|
@@ -620,16 +691,18 @@ var ACPProtocolHandler = class {
|
|
|
620
691
|
modeId: existing.modeId
|
|
621
692
|
}
|
|
622
693
|
});
|
|
623
|
-
await this.transport.send(
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
+
}
|
|
630
703
|
}
|
|
631
|
-
}
|
|
632
|
-
|
|
704
|
+
})
|
|
705
|
+
);
|
|
633
706
|
return false;
|
|
634
707
|
}
|
|
635
708
|
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
@@ -641,16 +714,18 @@ var ACPProtocolHandler = class {
|
|
|
641
714
|
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
642
715
|
if (existing) {
|
|
643
716
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
644
|
-
await this.transport.send(
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
+
}
|
|
651
726
|
}
|
|
652
|
-
}
|
|
653
|
-
|
|
727
|
+
})
|
|
728
|
+
);
|
|
654
729
|
return false;
|
|
655
730
|
}
|
|
656
731
|
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
@@ -667,11 +742,13 @@ var ACPProtocolHandler = class {
|
|
|
667
742
|
session.abort.abort();
|
|
668
743
|
this.sessions.delete(sessionId);
|
|
669
744
|
this.disposeSession(sessionId);
|
|
670
|
-
await this.transport.send(
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
745
|
+
await this.transport.send(
|
|
746
|
+
toWire({
|
|
747
|
+
jsonrpc: "2.0",
|
|
748
|
+
id,
|
|
749
|
+
result: {}
|
|
750
|
+
})
|
|
751
|
+
);
|
|
675
752
|
return false;
|
|
676
753
|
}
|
|
677
754
|
async handleSessionDelete(id, params) {
|
|
@@ -682,18 +759,22 @@ var ACPProtocolHandler = class {
|
|
|
682
759
|
return false;
|
|
683
760
|
}
|
|
684
761
|
if (!this.sessions.has(sessionId)) {
|
|
685
|
-
await this.transport.send(
|
|
762
|
+
await this.transport.send(
|
|
763
|
+
toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } })
|
|
764
|
+
);
|
|
686
765
|
return false;
|
|
687
766
|
}
|
|
688
767
|
const session = this.sessions.get(sessionId);
|
|
689
768
|
session.abort.abort();
|
|
690
769
|
this.sessions.delete(sessionId);
|
|
691
770
|
this.disposeSession(sessionId);
|
|
692
|
-
await this.transport.send(
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
771
|
+
await this.transport.send(
|
|
772
|
+
toWire({
|
|
773
|
+
jsonrpc: "2.0",
|
|
774
|
+
id,
|
|
775
|
+
result: {}
|
|
776
|
+
})
|
|
777
|
+
);
|
|
697
778
|
return false;
|
|
698
779
|
}
|
|
699
780
|
async handleSessionFork(id, params) {
|
|
@@ -708,11 +789,20 @@ var ACPProtocolHandler = class {
|
|
|
708
789
|
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
709
790
|
return false;
|
|
710
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
|
+
}
|
|
711
801
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
712
802
|
const sessionId = `sess_${this.allocId()}`;
|
|
713
803
|
const forked = {
|
|
714
804
|
id: sessionId,
|
|
715
|
-
cwd:
|
|
805
|
+
cwd: forkCwd,
|
|
716
806
|
abort: new AbortController(),
|
|
717
807
|
modeId: source.modeId,
|
|
718
808
|
createdAt: now,
|
|
@@ -731,38 +821,48 @@ var ACPProtocolHandler = class {
|
|
|
731
821
|
sessionId,
|
|
732
822
|
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
733
823
|
});
|
|
734
|
-
await this.transport.send(
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
+
);
|
|
743
835
|
return false;
|
|
744
836
|
}
|
|
745
837
|
async handleProvidersList(id, _params) {
|
|
746
|
-
await this.transport.send(
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
838
|
+
await this.transport.send(
|
|
839
|
+
toWire({
|
|
840
|
+
jsonrpc: "2.0",
|
|
841
|
+
id,
|
|
842
|
+
result: {
|
|
843
|
+
providers: [],
|
|
844
|
+
currentProviderId: null
|
|
845
|
+
}
|
|
846
|
+
})
|
|
847
|
+
);
|
|
754
848
|
return false;
|
|
755
849
|
}
|
|
756
850
|
async handleProvidersSet(id, _params) {
|
|
757
|
-
await this.sendError(
|
|
851
|
+
await this.sendError(
|
|
852
|
+
id,
|
|
853
|
+
-32e3,
|
|
854
|
+
"provider configuration not available through ACP; use wstack auth"
|
|
855
|
+
);
|
|
758
856
|
return false;
|
|
759
857
|
}
|
|
760
858
|
async handleProvidersDisable(id, _params) {
|
|
761
|
-
await this.transport.send(
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
859
|
+
await this.transport.send(
|
|
860
|
+
toWire({
|
|
861
|
+
jsonrpc: "2.0",
|
|
862
|
+
id,
|
|
863
|
+
result: {}
|
|
864
|
+
})
|
|
865
|
+
);
|
|
766
866
|
return false;
|
|
767
867
|
}
|
|
768
868
|
async handleMcpMessage(id, _params) {
|
|
@@ -815,7 +915,10 @@ var ACPProtocolHandler = class {
|
|
|
815
915
|
const terminalId = created?.terminalId;
|
|
816
916
|
if (!terminalId) return { output: "", exitCode: null };
|
|
817
917
|
try {
|
|
818
|
-
const exit = await this.request("terminal/wait_for_exit", {
|
|
918
|
+
const exit = await this.request("terminal/wait_for_exit", {
|
|
919
|
+
sessionId,
|
|
920
|
+
terminalId
|
|
921
|
+
});
|
|
819
922
|
const out = await this.request("terminal/output", { sessionId, terminalId });
|
|
820
923
|
return {
|
|
821
924
|
output: String(out?.output ?? ""),
|
|
@@ -845,11 +948,13 @@ var ACPProtocolHandler = class {
|
|
|
845
948
|
session.abort.signal.removeEventListener("abort", onCancel);
|
|
846
949
|
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
847
950
|
await this.persist(session);
|
|
848
|
-
await this.transport.send(
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
951
|
+
await this.transport.send(
|
|
952
|
+
toWire({
|
|
953
|
+
jsonrpc: "2.0",
|
|
954
|
+
id,
|
|
955
|
+
result: { stopReason: result.stopReason }
|
|
956
|
+
})
|
|
957
|
+
);
|
|
853
958
|
return false;
|
|
854
959
|
}
|
|
855
960
|
async handleSetMode(id, params) {
|
|
@@ -890,7 +995,9 @@ var ACPProtocolHandler = class {
|
|
|
890
995
|
configOptions: [...this.configOptions]
|
|
891
996
|
}
|
|
892
997
|
});
|
|
893
|
-
await this.transport.send(
|
|
998
|
+
await this.transport.send(
|
|
999
|
+
toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } })
|
|
1000
|
+
);
|
|
894
1001
|
return false;
|
|
895
1002
|
}
|
|
896
1003
|
async handleSessionList(id) {
|
|
@@ -903,11 +1010,13 @@ var ACPProtocolHandler = class {
|
|
|
903
1010
|
if (s.title !== void 0) out.title = s.title;
|
|
904
1011
|
return out;
|
|
905
1012
|
});
|
|
906
|
-
await this.transport.send(
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1013
|
+
await this.transport.send(
|
|
1014
|
+
toWire({
|
|
1015
|
+
jsonrpc: "2.0",
|
|
1016
|
+
id,
|
|
1017
|
+
result: { sessions }
|
|
1018
|
+
})
|
|
1019
|
+
);
|
|
911
1020
|
return false;
|
|
912
1021
|
}
|
|
913
1022
|
// ────────────────────────────────────────────────────────────────────
|
|
@@ -953,8 +1062,54 @@ var ACPProtocolHandler = class {
|
|
|
953
1062
|
if (data !== void 0) error.data = data;
|
|
954
1063
|
await this.transport.send(toWire({ jsonrpc: "2.0", id, error }));
|
|
955
1064
|
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Allocate a session id (WS-015).
|
|
1067
|
+
*
|
|
1068
|
+
* This was `this.nextId++`, so ids were `sess_1`, `sess_2`, … — and the
|
|
1069
|
+
* handler has no per-connection ownership: any caller that names a session
|
|
1070
|
+
* id can `session/load`, `session/prompt`, `session/cancel` or
|
|
1071
|
+
* `session/delete` it. Over stdio that is academic (one client per process),
|
|
1072
|
+
* but the agent also serves over HTTP, where a guessable id is the whole
|
|
1073
|
+
* authorization story for any local process or page that reaches the port.
|
|
1074
|
+
*
|
|
1075
|
+
* Random ids do not create ownership — they remove the trivial enumeration
|
|
1076
|
+
* that made its absence exploitable. Real per-connection ownership is the
|
|
1077
|
+
* larger fix and is noted in the WS-015 test file.
|
|
1078
|
+
*
|
|
1079
|
+
* The counter is retained: it keeps ids ordered for debugging and guarantees
|
|
1080
|
+
* uniqueness within a process even in the (impossible) event of a UUID
|
|
1081
|
+
* collision. The random half is what makes the id unguessable.
|
|
1082
|
+
*/
|
|
1083
|
+
/**
|
|
1084
|
+
* Resolve a client-supplied `cwd` for a session, or `null` when it is not
|
|
1085
|
+
* usable (WS-015).
|
|
1086
|
+
*
|
|
1087
|
+
* `session/new`, `session/load` and `session/fork` all took `params.cwd`
|
|
1088
|
+
* with a single `typeof === 'string'` check and nothing else. That value is
|
|
1089
|
+
* the working directory the agent then reads, writes and executes in.
|
|
1090
|
+
*
|
|
1091
|
+
* SCOPE, deliberately stated: this does NOT confine the session to a root.
|
|
1092
|
+
* In ACP the client IS the editor and legitimately names its own workspace —
|
|
1093
|
+
* Zed and JetBrains pass the project root — so a fixed boundary here would
|
|
1094
|
+
* break the integration this package exists for. What it enforces is that
|
|
1095
|
+
* the directory is absolute and actually exists as a directory: a relative
|
|
1096
|
+
* or missing `cwd` is a bug or an attack under either reading, and silently
|
|
1097
|
+
* running the agent somewhere other than where the client asked is worse
|
|
1098
|
+
* than refusing. Confinement, if wanted, belongs in an operator-set option
|
|
1099
|
+
* on top of this, not in place of it.
|
|
1100
|
+
*/
|
|
1101
|
+
async resolveSessionCwd(requested) {
|
|
1102
|
+
if (!path.isAbsolute(requested)) return null;
|
|
1103
|
+
const resolved = path.resolve(requested);
|
|
1104
|
+
try {
|
|
1105
|
+
const stat2 = await fsp.stat(resolved);
|
|
1106
|
+
return stat2.isDirectory() ? resolved : null;
|
|
1107
|
+
} catch {
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
956
1111
|
allocId() {
|
|
957
|
-
return this.nextId
|
|
1112
|
+
return `${this.nextId++}_${randomUUID().replaceAll("-", "")}`;
|
|
958
1113
|
}
|
|
959
1114
|
};
|
|
960
1115
|
function errorToJsonRpc(err) {
|
|
@@ -974,8 +1129,10 @@ function errorToJsonRpc(err) {
|
|
|
974
1129
|
}
|
|
975
1130
|
|
|
976
1131
|
// src/agent/session-store.ts
|
|
977
|
-
import * as
|
|
978
|
-
import * as
|
|
1132
|
+
import * as fsp2 from "node:fs/promises";
|
|
1133
|
+
import * as path2 from "node:path";
|
|
1134
|
+
import { isSafePathSegment, resolveContainedPath } from "@wrongstack/core/utils";
|
|
1135
|
+
var RESERVED_SESSION_IDS = /* @__PURE__ */ new Set(["index"]);
|
|
979
1136
|
var ACPSessionStore = class {
|
|
980
1137
|
dir;
|
|
981
1138
|
/**
|
|
@@ -985,13 +1142,53 @@ var ACPSessionStore = class {
|
|
|
985
1142
|
* Cleared automatically if the directory disappears between calls.
|
|
986
1143
|
*/
|
|
987
1144
|
initialized = false;
|
|
1145
|
+
/**
|
|
1146
|
+
* Tail of in-flight sidecar-index mutations. `readIndex`/`writeIndex`/
|
|
1147
|
+
* `updateIndex`/delete's index-touching branch all run through the
|
|
1148
|
+
* `withIndexLock` gate so a concurrent save/delete/save cannot read the
|
|
1149
|
+
* same baseline twice and clobber the other writer's edit (Chimera
|
|
1150
|
+
* HIGH — race that could permanently omit or resurrect sessions in
|
|
1151
|
+
* `list()`). Mirrors the `writeChains` pattern in
|
|
1152
|
+
* `packages/core/src/storage/tool-audit-log.ts`.
|
|
1153
|
+
*/
|
|
1154
|
+
indexChain = Promise.resolve();
|
|
1155
|
+
/**
|
|
1156
|
+
* Monotonic per-store counter. Used to make tmp filenames unique so two
|
|
1157
|
+
* concurrent saves started in the same millisecond cannot collide on
|
|
1158
|
+
* `<target>.<pid>.<ts>.tmp` and lose one's tmp mid-write (latent bug
|
|
1159
|
+
* discovered by the concurrent same-id save/delete stress test).
|
|
1160
|
+
*/
|
|
1161
|
+
writeSeq = 0;
|
|
988
1162
|
constructor(opts = {}) {
|
|
989
|
-
this.dir = opts.dir ??
|
|
1163
|
+
this.dir = opts.dir ?? path2.join(process.cwd(), ".acp-sessions");
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Path of `<sessionId>.json` inside the store, or `null` when `sessionId`
|
|
1167
|
+
* is not usable as a single path segment (WS-015).
|
|
1168
|
+
*
|
|
1169
|
+
* The session id arrives from the ACP client — `session/load`,
|
|
1170
|
+
* `session/close` and `session/delete` all take it verbatim from the wire.
|
|
1171
|
+
* It was joined straight onto the store directory, so `../../..` escaped it,
|
|
1172
|
+
* and the `.json` suffix was the only thing narrowing the blast radius:
|
|
1173
|
+
*
|
|
1174
|
+
* - `load` — read any `.json` on the machine, e.g. the provider config
|
|
1175
|
+
* holding API keys
|
|
1176
|
+
* - `delete` — unlink any `.json` on the machine
|
|
1177
|
+
* - `save` — the worst one: a loaded session keeps the traversing id as
|
|
1178
|
+
* its `state.id`, so the next persist WRITES to that path
|
|
1179
|
+
*
|
|
1180
|
+
* All three now route through here, so the escape is closed once rather than
|
|
1181
|
+
* three times.
|
|
1182
|
+
*/
|
|
1183
|
+
sessionFile(sessionId) {
|
|
1184
|
+
if (!isSafePathSegment(sessionId)) return null;
|
|
1185
|
+
if (RESERVED_SESSION_IDS.has(sessionId)) return null;
|
|
1186
|
+
return resolveContainedPath(this.dir, `${sessionId}.json`);
|
|
990
1187
|
}
|
|
991
1188
|
/** Ensure the store directory exists. Memoized — only mkdirs once. */
|
|
992
1189
|
async init() {
|
|
993
1190
|
if (this.initialized) return;
|
|
994
|
-
await
|
|
1191
|
+
await fsp2.mkdir(this.dir, { recursive: true });
|
|
995
1192
|
this.initialized = true;
|
|
996
1193
|
}
|
|
997
1194
|
/**
|
|
@@ -1000,30 +1197,43 @@ var ACPSessionStore = class {
|
|
|
1000
1197
|
* `session/load` replay.
|
|
1001
1198
|
*/
|
|
1002
1199
|
async save(state, history) {
|
|
1200
|
+
const target = this.sessionFile(state.id);
|
|
1201
|
+
if (target === null) {
|
|
1202
|
+
throw new Error(`ACPSessionStore: refusing to persist unsafe session id "${state.id}"`);
|
|
1203
|
+
}
|
|
1003
1204
|
await this.init();
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1205
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.${++this.writeSeq}.tmp`;
|
|
1206
|
+
let renamed = false;
|
|
1207
|
+
try {
|
|
1208
|
+
await fsp2.writeFile(
|
|
1209
|
+
tmp,
|
|
1210
|
+
JSON.stringify({
|
|
1211
|
+
id: state.id,
|
|
1212
|
+
cwd: state.cwd,
|
|
1213
|
+
modeId: state.modeId,
|
|
1214
|
+
createdAt: state.createdAt,
|
|
1215
|
+
updatedAt: state.updatedAt,
|
|
1216
|
+
title: state.title,
|
|
1217
|
+
...history && history.length > 0 ? { history } : {}
|
|
1218
|
+
}),
|
|
1219
|
+
"utf8"
|
|
1220
|
+
);
|
|
1221
|
+
await fsp2.rename(tmp, target);
|
|
1222
|
+
renamed = true;
|
|
1223
|
+
} finally {
|
|
1224
|
+
if (!renamed) {
|
|
1225
|
+
await fsp2.unlink(tmp).catch(() => void 0);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1020
1228
|
await this.updateIndex(state.id, state.updatedAt);
|
|
1021
1229
|
return state.id;
|
|
1022
1230
|
}
|
|
1023
1231
|
/** Load a persisted session (metadata + history) from disk, or null. */
|
|
1024
1232
|
async load(sessionId) {
|
|
1233
|
+
const target = this.sessionFile(sessionId);
|
|
1234
|
+
if (target === null) return null;
|
|
1025
1235
|
try {
|
|
1026
|
-
const data = await
|
|
1236
|
+
const data = await fsp2.readFile(target, "utf8");
|
|
1027
1237
|
return JSON.parse(data);
|
|
1028
1238
|
} catch {
|
|
1029
1239
|
return null;
|
|
@@ -1035,9 +1245,20 @@ var ACPSessionStore = class {
|
|
|
1035
1245
|
if (indexEntries !== null) {
|
|
1036
1246
|
return indexEntries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
1037
1247
|
}
|
|
1248
|
+
const sessions = await this.scanForSessions();
|
|
1249
|
+
void this.writeIndex(sessions).catch(() => void 0);
|
|
1250
|
+
return sessions;
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Walk `<dir>` and parse every session file into `{id, updatedAt}`
|
|
1254
|
+
* metadata. Used as the slow-path fallback by `list()` and by
|
|
1255
|
+
* `updateIndex()` when the sidecar index is missing. Pure read —
|
|
1256
|
+
* does NOT touch `index.json`.
|
|
1257
|
+
*/
|
|
1258
|
+
async scanForSessions() {
|
|
1038
1259
|
const files = [];
|
|
1039
1260
|
try {
|
|
1040
|
-
const entries = await
|
|
1261
|
+
const entries = await fsp2.readdir(this.dir);
|
|
1041
1262
|
for (const entry of entries) {
|
|
1042
1263
|
if (entry.endsWith(".json") && entry !== "index.json") {
|
|
1043
1264
|
files.push(entry);
|
|
@@ -1049,7 +1270,7 @@ var ACPSessionStore = class {
|
|
|
1049
1270
|
const sessions = [];
|
|
1050
1271
|
for (const file of files) {
|
|
1051
1272
|
try {
|
|
1052
|
-
const data = await
|
|
1273
|
+
const data = await fsp2.readFile(path2.join(this.dir, file), "utf8");
|
|
1053
1274
|
const parsed = JSON.parse(data);
|
|
1054
1275
|
if (parsed.id) {
|
|
1055
1276
|
sessions.push({ id: parsed.id, updatedAt: parsed.updatedAt ?? "" });
|
|
@@ -1058,17 +1279,40 @@ var ACPSessionStore = class {
|
|
|
1058
1279
|
}
|
|
1059
1280
|
}
|
|
1060
1281
|
sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
1061
|
-
void this.writeIndex(sessions).catch(() => void 0);
|
|
1062
1282
|
return sessions;
|
|
1063
1283
|
}
|
|
1064
1284
|
/** Sidecar path that stores `{id, updatedAt}` for every saved session. */
|
|
1065
1285
|
indexPath() {
|
|
1066
|
-
return
|
|
1286
|
+
return path2.join(this.dir, "index.json");
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Serialize sidecar-index mutations. Each call appends `fn` to the tail
|
|
1290
|
+
* of `indexChain` so reads and writes see a linearized order — a second
|
|
1291
|
+
* caller cannot start until the first one's index update has finished.
|
|
1292
|
+
*
|
|
1293
|
+
* The chain swallows errors (`prev.then(fn, fn)`) so one failing writer
|
|
1294
|
+
* does not poison every subsequent caller, and the catch keeps an
|
|
1295
|
+
* unhandled rejection from leaking out of the stored tail. The cleanup
|
|
1296
|
+
* removes the tail entry once settled, so a quiet store does not retain
|
|
1297
|
+
* stale promises forever.
|
|
1298
|
+
*/
|
|
1299
|
+
withIndexLock(fn) {
|
|
1300
|
+
const prev = this.indexChain;
|
|
1301
|
+
const next = prev.then(fn, fn);
|
|
1302
|
+
const settled = next.then(
|
|
1303
|
+
() => void 0,
|
|
1304
|
+
() => void 0
|
|
1305
|
+
);
|
|
1306
|
+
this.indexChain = settled;
|
|
1307
|
+
void settled.finally(() => {
|
|
1308
|
+
if (this.indexChain === settled) this.indexChain = Promise.resolve();
|
|
1309
|
+
});
|
|
1310
|
+
return next;
|
|
1067
1311
|
}
|
|
1068
1312
|
/** Read the sidecar index. Returns `null` when missing or unreadable. */
|
|
1069
1313
|
async readIndex() {
|
|
1070
1314
|
try {
|
|
1071
|
-
const data = await
|
|
1315
|
+
const data = await fsp2.readFile(this.indexPath(), "utf8");
|
|
1072
1316
|
const parsed = JSON.parse(data);
|
|
1073
1317
|
if (!Array.isArray(parsed)) return null;
|
|
1074
1318
|
const out = [];
|
|
@@ -1088,40 +1332,58 @@ var ACPSessionStore = class {
|
|
|
1088
1332
|
/** Atomically replace the sidecar index with the supplied entries. */
|
|
1089
1333
|
async writeIndex(entries) {
|
|
1090
1334
|
const target = this.indexPath();
|
|
1091
|
-
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
1092
|
-
|
|
1093
|
-
|
|
1335
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.${++this.writeSeq}.tmp`;
|
|
1336
|
+
let renamed = false;
|
|
1337
|
+
try {
|
|
1338
|
+
await fsp2.writeFile(tmp, JSON.stringify(entries), "utf8");
|
|
1339
|
+
await fsp2.rename(tmp, target);
|
|
1340
|
+
renamed = true;
|
|
1341
|
+
} finally {
|
|
1342
|
+
if (!renamed) {
|
|
1343
|
+
await fsp2.unlink(tmp).catch(() => void 0);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1094
1346
|
}
|
|
1095
1347
|
/** Update one entry in the index, adding it if missing. Best-effort. */
|
|
1096
1348
|
async updateIndex(id, updatedAt) {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1349
|
+
await this.withIndexLock(async () => {
|
|
1350
|
+
const entries = await this.readIndex();
|
|
1351
|
+
if (entries === null) {
|
|
1352
|
+
const sessions = await this.scanForSessions();
|
|
1353
|
+
try {
|
|
1354
|
+
await this.writeIndex(sessions);
|
|
1355
|
+
} catch {
|
|
1356
|
+
}
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
const i = entries.findIndex((e) => e.id === id);
|
|
1360
|
+
if (i >= 0) entries[i] = { id, updatedAt };
|
|
1361
|
+
else entries.push({ id, updatedAt });
|
|
1362
|
+
try {
|
|
1363
|
+
await this.writeIndex(entries);
|
|
1364
|
+
} catch {
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1109
1367
|
}
|
|
1110
1368
|
/** Delete a session file. */
|
|
1111
1369
|
async delete(sessionId) {
|
|
1370
|
+
const target = this.sessionFile(sessionId);
|
|
1371
|
+
if (target === null) return;
|
|
1112
1372
|
try {
|
|
1113
|
-
await
|
|
1373
|
+
await fsp2.unlink(target);
|
|
1114
1374
|
} catch {
|
|
1115
1375
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1376
|
+
await this.withIndexLock(async () => {
|
|
1377
|
+
const entries = await this.readIndex();
|
|
1378
|
+
if (entries === null) return;
|
|
1379
|
+
const next = entries.filter((e) => e.id !== sessionId);
|
|
1380
|
+
if (next.length !== entries.length) {
|
|
1381
|
+
try {
|
|
1382
|
+
await this.writeIndex(next);
|
|
1383
|
+
} catch {
|
|
1384
|
+
}
|
|
1123
1385
|
}
|
|
1124
|
-
}
|
|
1386
|
+
});
|
|
1125
1387
|
}
|
|
1126
1388
|
/** Get the store directory path. */
|
|
1127
1389
|
getDirectory() {
|
|
@@ -1338,12 +1600,12 @@ var WrongStackACPServer = class {
|
|
|
1338
1600
|
res.end(JSON.stringify({ error: { code: -32603, message: "Internal error" } }));
|
|
1339
1601
|
}
|
|
1340
1602
|
});
|
|
1341
|
-
return new Promise((
|
|
1603
|
+
return new Promise((resolve2) => {
|
|
1342
1604
|
this.httpServer.listen(port, host, () => {
|
|
1343
1605
|
writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${port}
|
|
1344
1606
|
`);
|
|
1345
1607
|
this.running = true;
|
|
1346
|
-
|
|
1608
|
+
resolve2();
|
|
1347
1609
|
});
|
|
1348
1610
|
});
|
|
1349
1611
|
}
|
|
@@ -1591,9 +1853,9 @@ function toolNameToKind(name) {
|
|
|
1591
1853
|
}
|
|
1592
1854
|
function toolTitle(name, input) {
|
|
1593
1855
|
if (isRecord(input)) {
|
|
1594
|
-
const
|
|
1595
|
-
if (typeof
|
|
1596
|
-
return `${name}: ${
|
|
1856
|
+
const path3 = input.path ?? input.file ?? input.filePath ?? input.pattern ?? input.command;
|
|
1857
|
+
if (typeof path3 === "string" && path3.length > 0) {
|
|
1858
|
+
return `${name}: ${path3.length > 80 ? `${path3.slice(0, 77)}\u2026` : path3}`;
|
|
1597
1859
|
}
|
|
1598
1860
|
}
|
|
1599
1861
|
return name;
|