@wrongstack/acp 0.293.0 → 0.295.1
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/README.md +22 -2
- package/dist/agent/protocol-contract.d.ts +210 -0
- package/dist/agent/protocol-contract.d.ts.map +1 -0
- package/dist/agent/protocol-handler.d.ts +15 -189
- package/dist/agent/protocol-handler.d.ts.map +1 -1
- package/dist/agent/server-agent-turn.d.ts +89 -1
- package/dist/agent/server-agent-turn.d.ts.map +1 -1
- package/dist/agent/session-store.d.ts.map +1 -1
- package/dist/agent/stdio-transport.d.ts +30 -1
- package/dist/agent/stdio-transport.d.ts.map +1 -1
- package/dist/agent/tools-registry.d.ts +1 -1
- package/dist/agent/tools-registry.d.ts.map +1 -1
- package/dist/agent/wrongstack-acp-agent.d.ts +37 -0
- package/dist/agent/wrongstack-acp-agent.d.ts.map +1 -1
- package/dist/agent.js +189 -32
- package/dist/agent.js.map +4 -4
- package/dist/client/acp-session.d.ts +13 -1
- package/dist/client/acp-session.d.ts.map +1 -1
- package/dist/client/file-server.d.ts +19 -0
- package/dist/client/file-server.d.ts.map +1 -1
- package/dist/client/index.d.ts +11 -9
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/terminal-server.d.ts +5 -0
- package/dist/client/terminal-server.d.ts.map +1 -1
- package/dist/client/tool-translator.d.ts +1 -1
- package/dist/client/tool-translator.d.ts.map +1 -1
- package/dist/client/trust-boundary-permission.d.ts +31 -0
- package/dist/client/trust-boundary-permission.d.ts.map +1 -0
- package/dist/client/websocket-transport.d.ts +6 -0
- package/dist/client/websocket-transport.d.ts.map +1 -1
- package/dist/client.js +459 -243
- package/dist/client.js.map +4 -4
- package/dist/index.d.ts +29 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2116 -1802
- package/dist/index.js.map +4 -4
- package/dist/integration/acp-bench.d.ts +32 -0
- package/dist/integration/acp-bench.d.ts.map +1 -1
- package/dist/integration/acp-subagent-runner.d.ts +3 -3
- package/dist/integration/acp-subagent-runner.d.ts.map +1 -1
- package/dist/integration/ensemble-runner.d.ts +22 -1
- package/dist/integration/ensemble-runner.d.ts.map +1 -1
- package/dist/legacy.d.ts +8 -0
- package/dist/legacy.d.ts.map +1 -0
- package/dist/legacy.js +6 -0
- package/dist/legacy.js.map +7 -0
- package/dist/registry/acp-registry-fetch.d.ts +1 -1
- package/dist/registry/acp-registry-fetch.d.ts.map +1 -1
- package/dist/registry/ensemble-registry.d.ts +22 -0
- package/dist/registry/ensemble-registry.d.ts.map +1 -1
- package/dist/sdk.d.ts +10 -8
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +22 -0
- package/dist/sdk.js.map +3 -3
- package/dist/v1.d.ts +3 -0
- package/dist/v1.d.ts.map +1 -0
- package/dist/v1.js +12 -0
- package/dist/v1.js.map +7 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/wrongstack-acp-agent.js +121 -24
- package/dist/wrongstack-acp-agent.js.map +4 -4
- package/package.json +10 -2
package/dist/index.js
CHANGED
|
@@ -1,628 +1,524 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
assertSafeWin32CmdArgs([command, ...args]);
|
|
8
|
-
const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
|
|
9
|
-
return {
|
|
10
|
-
command: process.env["COMSPEC"] ?? "cmd.exe",
|
|
11
|
-
args: ["/d", "/c", line],
|
|
12
|
-
windowsVerbatimArguments: true
|
|
13
|
-
};
|
|
1
|
+
// src/types/acp-v1.ts
|
|
2
|
+
var ACP_PROTOCOL_VERSION = 1;
|
|
3
|
+
function assertNeverSessionUpdate(x) {
|
|
4
|
+
throw new Error(
|
|
5
|
+
`Unhandled sessionUpdate: ${JSON.stringify(x)}`
|
|
6
|
+
);
|
|
14
7
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
|
|
9
|
+
// src/version.ts
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
var require2 = createRequire(import.meta.url);
|
|
12
|
+
function readPackageVersion(load = () => require2("../package.json")) {
|
|
13
|
+
try {
|
|
14
|
+
const packageJson = load();
|
|
15
|
+
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
16
|
+
return packageJson.version;
|
|
21
17
|
}
|
|
18
|
+
} catch {
|
|
22
19
|
}
|
|
20
|
+
return "dev";
|
|
23
21
|
}
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
var ACP_PACKAGE_VERSION = readPackageVersion();
|
|
23
|
+
|
|
24
|
+
// src/agent/protocol-contract.ts
|
|
25
|
+
function toWire(msg) {
|
|
26
|
+
return msg;
|
|
26
27
|
}
|
|
28
|
+
var WRONGSTACK_VERSION = ACP_PACKAGE_VERSION;
|
|
27
29
|
|
|
28
|
-
// src/agent/
|
|
29
|
-
var
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
resolveRead = null;
|
|
37
|
-
messageQueue = [];
|
|
38
|
-
constructor() {
|
|
39
|
-
this.stdin.resume();
|
|
40
|
-
this.stdin.setEncoding("utf8");
|
|
41
|
-
this.stdin.on("data", (chunk) => this.onData(chunk));
|
|
42
|
-
this.stdin.on("end", () => this.handleClose());
|
|
43
|
-
this.stdin.on("error", (err) => this.failAll(err));
|
|
30
|
+
// src/agent/protocol-handler.ts
|
|
31
|
+
var WRONGSTACK_AUTH_METHODS = [
|
|
32
|
+
{
|
|
33
|
+
id: "wrongstack-auth",
|
|
34
|
+
name: "Run wstack auth",
|
|
35
|
+
description: "Configure a WrongStack model provider in an interactive terminal.",
|
|
36
|
+
type: "terminal",
|
|
37
|
+
args: ["auth"]
|
|
44
38
|
}
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
];
|
|
40
|
+
var DEFAULT_MODE_ID = "code";
|
|
41
|
+
var DEFAULT_MAX_SESSIONS = 64;
|
|
42
|
+
var DEFAULT_MODES = [
|
|
43
|
+
{
|
|
44
|
+
id: DEFAULT_MODE_ID,
|
|
45
|
+
name: "Code",
|
|
46
|
+
description: "Default agent mode for code-generation tasks."
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
];
|
|
49
|
+
var ACPProtocolHandler = class {
|
|
50
|
+
transport;
|
|
51
|
+
defaultCwd;
|
|
52
|
+
runTurn;
|
|
53
|
+
onSessionNew;
|
|
54
|
+
modes;
|
|
55
|
+
configOptions;
|
|
56
|
+
agentName;
|
|
57
|
+
replayFor;
|
|
58
|
+
seedFor;
|
|
59
|
+
disposeFor;
|
|
60
|
+
maxSessions;
|
|
61
|
+
store;
|
|
62
|
+
initialized = false;
|
|
63
|
+
clientCapabilities = {};
|
|
64
|
+
sessions = /* @__PURE__ */ new Map();
|
|
65
|
+
nextId = 1;
|
|
66
|
+
// Outbound request correlation (server → client requests, e.g.
|
|
67
|
+
// session/request_permission). Keyed by our own `srv_N` ids.
|
|
68
|
+
pendingOut = /* @__PURE__ */ new Map();
|
|
69
|
+
nextOutId = 1;
|
|
70
|
+
constructor(opts) {
|
|
71
|
+
this.transport = opts.transport;
|
|
72
|
+
this.defaultCwd = opts.defaultCwd;
|
|
73
|
+
this.runTurn = opts.runTurn;
|
|
74
|
+
this.onSessionNew = opts.onSessionNew ?? (() => {
|
|
53
75
|
});
|
|
76
|
+
this.modes = opts.modes ?? DEFAULT_MODES;
|
|
77
|
+
this.configOptions = opts.configOptions ?? [];
|
|
78
|
+
this.agentName = opts.agentName ?? "wrongstack";
|
|
79
|
+
this.replayFor = opts.replayFor;
|
|
80
|
+
this.seedFor = opts.seedFor;
|
|
81
|
+
this.disposeFor = opts.disposeFor;
|
|
82
|
+
this.maxSessions = Number.isFinite(opts.maxSessions) && opts.maxSessions > 0 ? Math.floor(opts.maxSessions) : DEFAULT_MAX_SESSIONS;
|
|
83
|
+
this.store = opts.store;
|
|
84
|
+
if (typeof this.transport.onMessage === "function") {
|
|
85
|
+
this.transport.onMessage((m) => this.maybeResolvePending(m));
|
|
86
|
+
}
|
|
54
87
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Send a request to the client and await its response. Used for
|
|
90
|
+
* server-initiated calls like `session/request_permission`. Rejects on
|
|
91
|
+
* timeout or transport error so the caller can pick a safe fallback.
|
|
92
|
+
*/
|
|
93
|
+
request(method, params, timeoutMs = 6e4) {
|
|
94
|
+
const id = `srv_${this.nextOutId++}`;
|
|
95
|
+
return new Promise((resolve3, reject) => {
|
|
96
|
+
const timer = setTimeout(() => {
|
|
97
|
+
this.pendingOut.delete(id);
|
|
98
|
+
reject(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
99
|
+
}, timeoutMs);
|
|
100
|
+
this.pendingOut.set(id, { resolve: resolve3, reject, timer });
|
|
101
|
+
this.transport.send(toWire({ jsonrpc: "2.0", id, method, params })).catch((e) => {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
this.pendingOut.delete(id);
|
|
104
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
105
|
+
});
|
|
63
106
|
});
|
|
64
107
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
108
|
+
maybeResolvePending(m) {
|
|
109
|
+
const id = m.id;
|
|
110
|
+
if (typeof id !== "string") return;
|
|
111
|
+
const pending = this.pendingOut.get(id);
|
|
112
|
+
if (!pending) return;
|
|
113
|
+
this.pendingOut.delete(id);
|
|
114
|
+
clearTimeout(pending.timer);
|
|
115
|
+
const err = m.error;
|
|
116
|
+
if (err) pending.reject(new Error(err.message ?? "client request failed"));
|
|
117
|
+
else pending.resolve(m.result);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Process one inbound message. Returns true if this was a terminal
|
|
121
|
+
* message (rare; reserved for future use by the server's own
|
|
122
|
+
* shutdown signal).
|
|
123
|
+
*/
|
|
124
|
+
async handleMessage(msg) {
|
|
125
|
+
if (typeof msg !== "object" || msg === null) return false;
|
|
126
|
+
const m = msg;
|
|
127
|
+
if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (m.id !== void 0 && typeof m.method === "string") {
|
|
131
|
+
return this.handleRequest(m.id, m.method, m.params);
|
|
132
|
+
}
|
|
133
|
+
if (typeof m.method === "string") {
|
|
134
|
+
return this.handleNotification(m.method, m.params);
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
68
137
|
}
|
|
138
|
+
/** Abort all active turns and drop session state. */
|
|
69
139
|
close() {
|
|
70
|
-
this.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
140
|
+
for (const [sessionId, session] of this.sessions) {
|
|
141
|
+
session.abort.abort();
|
|
142
|
+
this.disposeSession(sessionId);
|
|
143
|
+
}
|
|
144
|
+
this.sessions.clear();
|
|
145
|
+
for (const [, p] of this.pendingOut) {
|
|
146
|
+
clearTimeout(p.timer);
|
|
147
|
+
p.reject(new Error("protocol handler closed"));
|
|
148
|
+
}
|
|
149
|
+
this.pendingOut.clear();
|
|
74
150
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
for (const raw of lines) {
|
|
80
|
-
if (!raw.trim()) continue;
|
|
81
|
-
try {
|
|
82
|
-
this.dispatch(JSON.parse(raw));
|
|
83
|
-
} catch (err) {
|
|
84
|
-
this.stderr.write(`[wstack-acp parse error] ${err}
|
|
85
|
-
`, "utf8");
|
|
86
|
-
}
|
|
151
|
+
disposeSession(sessionId) {
|
|
152
|
+
try {
|
|
153
|
+
this.disposeFor?.(sessionId);
|
|
154
|
+
} catch {
|
|
87
155
|
}
|
|
88
156
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
157
|
+
// ────────────────────────────────────────────────────────────────────
|
|
158
|
+
// Requests
|
|
159
|
+
// ────────────────────────────────────────────────────────────────────
|
|
160
|
+
async handleRequest(id, method, params) {
|
|
161
|
+
if (method !== "initialize" && !this.initialized) {
|
|
162
|
+
await this.sendError(id, -32e3, "Not initialized");
|
|
163
|
+
return false;
|
|
96
164
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
165
|
+
try {
|
|
166
|
+
switch (method) {
|
|
167
|
+
case "initialize":
|
|
168
|
+
return await this.handleInitialize(id, params);
|
|
169
|
+
case "authenticate":
|
|
170
|
+
return await this.handleAuthenticate(id, params);
|
|
171
|
+
case "logout":
|
|
172
|
+
return await this.handleLogout(id, params);
|
|
173
|
+
case "session/new":
|
|
174
|
+
return await this.handleSessionNew(id, params);
|
|
175
|
+
case "session/load":
|
|
176
|
+
return await this.handleSessionLoad(id, params);
|
|
177
|
+
case "session/resume":
|
|
178
|
+
return await this.handleSessionResume(id, params);
|
|
179
|
+
case "session/close":
|
|
180
|
+
return await this.handleSessionClose(id, params);
|
|
181
|
+
case "session/delete":
|
|
182
|
+
return await this.handleSessionDelete(id, params);
|
|
183
|
+
case "session/prompt":
|
|
184
|
+
return await this.handleSessionPrompt(id, params);
|
|
185
|
+
case "session/set_mode":
|
|
186
|
+
return await this.handleSetMode(id, params);
|
|
187
|
+
case "session/set_config_option":
|
|
188
|
+
return await this.handleSetConfigOption(id, params);
|
|
189
|
+
case "session/list":
|
|
190
|
+
return await this.handleSessionList(id);
|
|
191
|
+
case "session/fork":
|
|
192
|
+
return await this.handleSessionFork(id, params);
|
|
193
|
+
case "providers/list":
|
|
194
|
+
return await this.handleProvidersList(id, params);
|
|
195
|
+
case "providers/set":
|
|
196
|
+
return await this.handleProvidersSet(id, params);
|
|
197
|
+
case "providers/disable":
|
|
198
|
+
return await this.handleProvidersDisable(id, params);
|
|
199
|
+
case "mcp/message":
|
|
200
|
+
return await this.handleMcpMessage(id, params);
|
|
201
|
+
default:
|
|
202
|
+
await this.sendError(id, -32601, `Unknown method: ${method}`);
|
|
203
|
+
return false;
|
|
103
204
|
}
|
|
205
|
+
} catch (err) {
|
|
206
|
+
const { code, message, data } = errorToJsonRpc(err);
|
|
207
|
+
await this.sendError(id, code, message, data);
|
|
208
|
+
return false;
|
|
104
209
|
}
|
|
105
210
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
failAll(err) {
|
|
112
|
-
this.stderr.write(`[wstack-acp stdin error] ${err.message}
|
|
113
|
-
`, "utf8");
|
|
114
|
-
this.close();
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
var ClientTransport = class {
|
|
118
|
-
child = null;
|
|
119
|
-
buffer = "";
|
|
120
|
-
handlers = /* @__PURE__ */ new Set();
|
|
121
|
-
closed = false;
|
|
122
|
-
resolveRead = null;
|
|
123
|
-
messageQueue = [];
|
|
124
|
-
opts;
|
|
125
|
-
constructor(options) {
|
|
126
|
-
this.opts = {
|
|
127
|
-
handshakeTimeoutMs: 3e4,
|
|
128
|
-
...options
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
async start() {
|
|
132
|
-
if (this.child) return;
|
|
133
|
-
const [{ spawn: spawn3 }, { buildChildEnv: buildChildEnv2 }, os] = await Promise.all([
|
|
134
|
-
import("node:child_process"),
|
|
135
|
-
import("@wrongstack/core"),
|
|
136
|
-
import("node:os")
|
|
137
|
-
]);
|
|
138
|
-
return new Promise((resolve3, reject) => {
|
|
139
|
-
const timeout = setTimeout(() => {
|
|
140
|
-
reject(
|
|
141
|
-
new Error(`ACP child process failed to start within ${this.opts.handshakeTimeoutMs}ms`)
|
|
142
|
-
);
|
|
143
|
-
}, this.opts.handshakeTimeoutMs);
|
|
144
|
-
const isPkgLauncher = this.opts.command === "npx" || this.opts.command === "uvx";
|
|
145
|
-
const spawnCwd = isPkgLauncher ? os.homedir() : this.opts.cwd;
|
|
146
|
-
try {
|
|
147
|
-
const childArgs = this.opts.args ?? [];
|
|
148
|
-
const shim = process.platform === "win32" ? buildWin32CmdShimInvocation(this.opts.command, childArgs) : null;
|
|
149
|
-
this.child = spawn3(shim?.command ?? this.opts.command, shim?.args ?? childArgs, {
|
|
150
|
-
env: { ...buildChildEnv2(), ...this.opts.env },
|
|
151
|
-
cwd: spawnCwd,
|
|
152
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
153
|
-
windowsHide: true,
|
|
154
|
-
...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
|
|
155
|
-
});
|
|
156
|
-
} catch (err) {
|
|
157
|
-
clearTimeout(timeout);
|
|
158
|
-
reject(err);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
const child = this.child;
|
|
162
|
-
child.stdout.setEncoding("utf8");
|
|
163
|
-
let settled = false;
|
|
164
|
-
const onSpawnFailure = (err) => {
|
|
165
|
-
if (settled) {
|
|
166
|
-
this.closed = true;
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
settled = true;
|
|
170
|
-
clearTimeout(timeout);
|
|
171
|
-
reject(err);
|
|
172
|
-
};
|
|
173
|
-
child.on("error", onSpawnFailure);
|
|
174
|
-
child.stdout.on("error", onSpawnFailure);
|
|
175
|
-
if (this.opts.skipHandshakeMarker) {
|
|
176
|
-
child.stdout.on("data", (c) => this.onChildData(c));
|
|
177
|
-
child.stderr.on("data", (c) => this.onChildError(c));
|
|
178
|
-
child.on("close", (code) => this.onChildClose(code));
|
|
179
|
-
child.once("spawn", () => {
|
|
180
|
-
if (settled) return;
|
|
181
|
-
settled = true;
|
|
182
|
-
clearTimeout(timeout);
|
|
183
|
-
resolve3();
|
|
184
|
-
});
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
const onReady = () => {
|
|
188
|
-
if (settled) return;
|
|
189
|
-
settled = true;
|
|
190
|
-
child.stdout.on("data", (c) => this.onChildData(c));
|
|
191
|
-
child.stderr.on("data", (c) => this.onChildError(c));
|
|
192
|
-
child.on("close", (code) => this.onChildClose(code));
|
|
193
|
-
clearTimeout(timeout);
|
|
194
|
-
resolve3();
|
|
195
|
-
};
|
|
196
|
-
const waitForMarker = (chunk) => {
|
|
197
|
-
this.buffer += chunk;
|
|
198
|
-
const idx = this.buffer.indexOf("[wstack-acp]\n");
|
|
199
|
-
if (idx !== -1) {
|
|
200
|
-
this.buffer = this.buffer.slice(idx + "[wstack-acp]\n".length);
|
|
201
|
-
child.stdout.removeListener("data", waitForMarker);
|
|
202
|
-
onReady();
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
child.stdout.on("data", waitForMarker);
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
send(msg) {
|
|
209
|
-
if (!this.child) return Promise.reject(new Error("ClientTransport not started"));
|
|
210
|
-
return new Promise((resolve3, reject) => {
|
|
211
|
-
const line = JSON.stringify(msg) + "\n";
|
|
212
|
-
this.child?.stdin.write(line, "utf8", (err) => {
|
|
213
|
-
if (err) reject(err);
|
|
214
|
-
else resolve3();
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
read() {
|
|
219
|
-
if (this.messageQueue.length > 0) return Promise.resolve(expectDefined(this.messageQueue.shift()));
|
|
220
|
-
if (this.closed) return Promise.resolve(null);
|
|
221
|
-
return new Promise((resolve3) => {
|
|
222
|
-
this.resolveRead = resolve3;
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
onMessage(handler) {
|
|
226
|
-
this.handlers.add(handler);
|
|
227
|
-
return () => this.handlers.delete(handler);
|
|
228
|
-
}
|
|
229
|
-
stop() {
|
|
230
|
-
if (!this.child) return;
|
|
231
|
-
this.closed = true;
|
|
232
|
-
try {
|
|
233
|
-
this.child.kill();
|
|
234
|
-
} catch {
|
|
211
|
+
async handleInitialize(id, params) {
|
|
212
|
+
const p = params ?? {};
|
|
213
|
+
if (p.clientCapabilities && typeof p.clientCapabilities === "object") {
|
|
214
|
+
this.clientCapabilities = p.clientCapabilities;
|
|
235
215
|
}
|
|
236
|
-
this.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
216
|
+
this.initialized = true;
|
|
217
|
+
await this.transport.send(toWire({
|
|
218
|
+
jsonrpc: "2.0",
|
|
219
|
+
id,
|
|
220
|
+
result: {
|
|
221
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
222
|
+
agentCapabilities: {
|
|
223
|
+
loadSession: true,
|
|
224
|
+
promptCapabilities: {
|
|
225
|
+
// We route ACP image blocks into the core agent's multimodal
|
|
226
|
+
// input (server-agent-turn.promptToAgentInput); whether the
|
|
227
|
+
// model can see them is the configured provider's concern.
|
|
228
|
+
image: true,
|
|
229
|
+
audio: false,
|
|
230
|
+
embeddedContext: true
|
|
231
|
+
},
|
|
232
|
+
mcpCapabilities: {
|
|
233
|
+
http: false,
|
|
234
|
+
sse: false
|
|
235
|
+
},
|
|
236
|
+
sessionCapabilities: {
|
|
237
|
+
close: {},
|
|
238
|
+
list: {},
|
|
239
|
+
delete: {},
|
|
240
|
+
resume: {},
|
|
241
|
+
fork: {}
|
|
242
|
+
},
|
|
243
|
+
auth: {
|
|
244
|
+
logout: {}
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
agentInfo: {
|
|
248
|
+
name: this.agentName,
|
|
249
|
+
title: "WrongStack",
|
|
250
|
+
version: WRONGSTACK_VERSION
|
|
251
|
+
},
|
|
252
|
+
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
253
|
+
modes: this.modes,
|
|
254
|
+
configOptions: this.configOptions
|
|
247
255
|
}
|
|
248
|
-
}
|
|
256
|
+
}));
|
|
257
|
+
return false;
|
|
249
258
|
}
|
|
250
|
-
|
|
251
|
-
|
|
259
|
+
async handleAuthenticate(id, _params) {
|
|
260
|
+
await this.transport.send(toWire({
|
|
261
|
+
jsonrpc: "2.0",
|
|
262
|
+
id,
|
|
263
|
+
result: { outcome: "unauthenticated" }
|
|
264
|
+
}));
|
|
265
|
+
return false;
|
|
252
266
|
}
|
|
253
|
-
|
|
254
|
-
this.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
267
|
+
async handleLogout(id, _params) {
|
|
268
|
+
await this.transport.send(toWire({
|
|
269
|
+
jsonrpc: "2.0",
|
|
270
|
+
id,
|
|
271
|
+
result: {}
|
|
272
|
+
}));
|
|
273
|
+
return false;
|
|
261
274
|
}
|
|
262
|
-
|
|
263
|
-
if (this.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
resolve3(msg);
|
|
267
|
-
} else {
|
|
268
|
-
this.messageQueue.push(msg);
|
|
275
|
+
async handleSessionNew(id, params) {
|
|
276
|
+
if (this.sessions.size >= this.maxSessions) {
|
|
277
|
+
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
278
|
+
return false;
|
|
269
279
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
280
|
+
const p = params ?? {};
|
|
281
|
+
const cwd = typeof p.cwd === "string" ? p.cwd : this.defaultCwd;
|
|
282
|
+
const sessionId = `sess_${this.allocId()}`;
|
|
283
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
284
|
+
const state = {
|
|
285
|
+
id: sessionId,
|
|
286
|
+
cwd,
|
|
287
|
+
abort: new AbortController(),
|
|
288
|
+
modeId: DEFAULT_MODE_ID,
|
|
289
|
+
createdAt: now,
|
|
290
|
+
updatedAt: now
|
|
291
|
+
};
|
|
292
|
+
this.sessions.set(sessionId, state);
|
|
293
|
+
this.onSessionNew(state);
|
|
294
|
+
await this.persist(state);
|
|
295
|
+
await this.sendNotification({
|
|
296
|
+
sessionId,
|
|
297
|
+
update: {
|
|
298
|
+
sessionUpdate: "current_mode_update",
|
|
299
|
+
modeId: this.modes[0]?.id ?? DEFAULT_MODE_ID
|
|
274
300
|
}
|
|
301
|
+
});
|
|
302
|
+
if (this.configOptions.length > 0) {
|
|
303
|
+
await this.sendNotification({
|
|
304
|
+
sessionId,
|
|
305
|
+
update: {
|
|
306
|
+
sessionUpdate: "config_option_update",
|
|
307
|
+
configOptions: [...this.configOptions]
|
|
308
|
+
}
|
|
309
|
+
});
|
|
275
310
|
}
|
|
311
|
+
await this.transport.send(toWire({
|
|
312
|
+
jsonrpc: "2.0",
|
|
313
|
+
id,
|
|
314
|
+
result: {
|
|
315
|
+
sessionId,
|
|
316
|
+
modes: this.modes,
|
|
317
|
+
configOptions: this.configOptions
|
|
318
|
+
}
|
|
319
|
+
}));
|
|
320
|
+
return false;
|
|
276
321
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
322
|
+
async handleSessionLoad(id, params) {
|
|
323
|
+
const p = params ?? {};
|
|
324
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
325
|
+
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
326
|
+
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
327
|
+
if (!existing && sessionId && this.store) {
|
|
328
|
+
const persisted = await this.store.load(sessionId);
|
|
329
|
+
if (persisted) {
|
|
330
|
+
if (this.sessions.size >= this.maxSessions) {
|
|
331
|
+
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
const restored = {
|
|
335
|
+
id: sessionId,
|
|
336
|
+
cwd: persisted.cwd ?? loadCwd ?? this.defaultCwd,
|
|
337
|
+
abort: new AbortController(),
|
|
338
|
+
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
339
|
+
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
340
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
341
|
+
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
342
|
+
};
|
|
343
|
+
this.sessions.set(sessionId, restored);
|
|
344
|
+
this.seedFor?.(sessionId, persisted.history ?? []);
|
|
345
|
+
for (const update of persisted.history ?? []) {
|
|
346
|
+
await this.sendNotification({ sessionId, update });
|
|
347
|
+
}
|
|
348
|
+
await this.sendNotification({
|
|
349
|
+
sessionId,
|
|
350
|
+
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
351
|
+
});
|
|
352
|
+
await this.transport.send(toWire({
|
|
353
|
+
jsonrpc: "2.0",
|
|
354
|
+
id,
|
|
355
|
+
result: {
|
|
356
|
+
initialMode: { currentModeId: restored.modeId, availableModes: this.modes }
|
|
357
|
+
}
|
|
358
|
+
}));
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
293
361
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
list() {
|
|
309
|
-
return Array.from(this.tools.values());
|
|
310
|
-
}
|
|
311
|
-
/** Build the ACP tools/list payload from registered tools. */
|
|
312
|
-
buildToolList() {
|
|
313
|
-
return {
|
|
314
|
-
tools: Array.from(this.tools.values()).map(
|
|
315
|
-
(t) => toACPToolDefinition(t, this.owner)
|
|
316
|
-
)
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Execute a tool by name and return ACP-formatted result.
|
|
321
|
-
* Returns null if the tool is not found.
|
|
322
|
-
*/
|
|
323
|
-
async execute(name, args, ctx, signal) {
|
|
324
|
-
const tool = this.tools.get(name);
|
|
325
|
-
if (!tool) return null;
|
|
326
|
-
try {
|
|
327
|
-
const result = await tool.execute(args, ctx, {
|
|
328
|
-
signal
|
|
362
|
+
if (existing) {
|
|
363
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
364
|
+
const replay = this.replayFor?.(sessionId);
|
|
365
|
+
if (replay) {
|
|
366
|
+
for (const update of replay) {
|
|
367
|
+
await this.sendNotification({ sessionId, update });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
await this.sendNotification({
|
|
371
|
+
sessionId,
|
|
372
|
+
update: {
|
|
373
|
+
sessionUpdate: "session_info_update",
|
|
374
|
+
updatedAt: existing.updatedAt
|
|
375
|
+
}
|
|
329
376
|
});
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
377
|
+
await this.sendNotification({
|
|
378
|
+
sessionId,
|
|
379
|
+
update: {
|
|
380
|
+
sessionUpdate: "current_mode_update",
|
|
381
|
+
modeId: existing.modeId
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
await this.transport.send(toWire({
|
|
385
|
+
jsonrpc: "2.0",
|
|
386
|
+
id,
|
|
387
|
+
result: {
|
|
388
|
+
initialMode: {
|
|
389
|
+
currentModeId: existing.modeId,
|
|
390
|
+
availableModes: this.modes
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}));
|
|
394
|
+
return false;
|
|
334
395
|
}
|
|
396
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
397
|
+
return false;
|
|
335
398
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
399
|
+
async handleSessionResume(id, params) {
|
|
400
|
+
const p = params ?? {};
|
|
401
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
402
|
+
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
403
|
+
if (existing) {
|
|
404
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
405
|
+
await this.transport.send(toWire({
|
|
406
|
+
jsonrpc: "2.0",
|
|
407
|
+
id,
|
|
408
|
+
result: {
|
|
409
|
+
initialMode: {
|
|
410
|
+
currentModeId: existing.modeId,
|
|
411
|
+
availableModes: this.modes
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}));
|
|
415
|
+
return false;
|
|
347
416
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
function toACPInputSchema(src) {
|
|
351
|
-
if (!src || typeof src !== "object") {
|
|
352
|
-
return {};
|
|
417
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
418
|
+
return false;
|
|
353
419
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (typeof s.maximum === "number") out.maximum = s.maximum;
|
|
362
|
-
if (s.items) out.items = toACPInputSchema(s.items);
|
|
363
|
-
if (s.properties && typeof s.properties === "object") {
|
|
364
|
-
const props = {};
|
|
365
|
-
for (const [k, v] of Object.entries(s.properties)) {
|
|
366
|
-
props[k] = toACPInputSchema(v);
|
|
420
|
+
async handleSessionClose(id, params) {
|
|
421
|
+
const p = params ?? {};
|
|
422
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
423
|
+
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
424
|
+
if (!session) {
|
|
425
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
426
|
+
return false;
|
|
367
427
|
}
|
|
368
|
-
|
|
369
|
-
|
|
428
|
+
session.abort.abort();
|
|
429
|
+
this.sessions.delete(sessionId);
|
|
430
|
+
this.disposeSession(sessionId);
|
|
431
|
+
await this.transport.send(toWire({
|
|
432
|
+
jsonrpc: "2.0",
|
|
433
|
+
id,
|
|
434
|
+
result: {}
|
|
435
|
+
}));
|
|
436
|
+
return false;
|
|
370
437
|
}
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
438
|
+
async handleSessionDelete(id, params) {
|
|
439
|
+
const p = params ?? {};
|
|
440
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
441
|
+
if (!sessionId) {
|
|
442
|
+
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
if (!this.sessions.has(sessionId)) {
|
|
446
|
+
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } }));
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
const session = this.sessions.get(sessionId);
|
|
450
|
+
session.abort.abort();
|
|
451
|
+
this.sessions.delete(sessionId);
|
|
452
|
+
this.disposeSession(sessionId);
|
|
453
|
+
await this.transport.send(toWire({
|
|
454
|
+
jsonrpc: "2.0",
|
|
455
|
+
id,
|
|
456
|
+
result: {}
|
|
457
|
+
}));
|
|
458
|
+
return false;
|
|
377
459
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
460
|
+
async handleSessionFork(id, params) {
|
|
461
|
+
const p = params ?? {};
|
|
462
|
+
const sourceId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
463
|
+
const source = sourceId ? this.sessions.get(sourceId) : void 0;
|
|
464
|
+
if (!sourceId || !source) {
|
|
465
|
+
await this.sendError(id, -32e3, `session not found: ${sourceId}`);
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
if (this.sessions.size >= this.maxSessions) {
|
|
469
|
+
await this.sendError(id, -32e3, `active session limit reached (${this.maxSessions})`);
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
473
|
+
const sessionId = `sess_${this.allocId()}`;
|
|
474
|
+
const forked = {
|
|
475
|
+
id: sessionId,
|
|
476
|
+
cwd: typeof p.cwd === "string" ? p.cwd : source.cwd,
|
|
477
|
+
abort: new AbortController(),
|
|
478
|
+
modeId: source.modeId,
|
|
479
|
+
createdAt: now,
|
|
480
|
+
updatedAt: now,
|
|
481
|
+
...source.title !== void 0 ? { title: source.title } : {}
|
|
482
|
+
};
|
|
483
|
+
const history = (this.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
484
|
+
sessionUpdate: update.sessionUpdate,
|
|
485
|
+
content: structuredClone(update.content)
|
|
486
|
+
}));
|
|
487
|
+
this.sessions.set(sessionId, forked);
|
|
488
|
+
this.seedFor?.(sessionId, history);
|
|
489
|
+
this.onSessionNew(forked);
|
|
490
|
+
await this.persist(forked, history);
|
|
491
|
+
await this.sendNotification({
|
|
492
|
+
sessionId,
|
|
493
|
+
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
494
|
+
});
|
|
495
|
+
await this.transport.send(toWire({
|
|
496
|
+
jsonrpc: "2.0",
|
|
497
|
+
id,
|
|
498
|
+
result: {
|
|
499
|
+
sessionId,
|
|
500
|
+
modes: this.modes,
|
|
501
|
+
configOptions: this.configOptions
|
|
502
|
+
}
|
|
503
|
+
}));
|
|
504
|
+
return false;
|
|
384
505
|
}
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
function toolToPriority(tool) {
|
|
388
|
-
if (tool.riskTier === "destructive") return "high";
|
|
389
|
-
if (tool.riskTier === "standard" || tool.permission === "confirm") return "medium";
|
|
390
|
-
return "low";
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// src/types/acp-v1.ts
|
|
394
|
-
var ACP_PROTOCOL_VERSION = 1;
|
|
395
|
-
|
|
396
|
-
// src/agent/protocol-handler.ts
|
|
397
|
-
function toWire(msg) {
|
|
398
|
-
return msg;
|
|
399
|
-
}
|
|
400
|
-
var WRONGSTACK_VERSION = "0.274.1";
|
|
401
|
-
var WRONGSTACK_AUTH_METHODS = [
|
|
402
|
-
{
|
|
403
|
-
id: "wrongstack-auth",
|
|
404
|
-
name: "Run wstack auth",
|
|
405
|
-
description: "Configure a WrongStack model provider in an interactive terminal.",
|
|
406
|
-
type: "terminal",
|
|
407
|
-
args: ["auth"]
|
|
408
|
-
}
|
|
409
|
-
];
|
|
410
|
-
var DEFAULT_MODE_ID = "code";
|
|
411
|
-
var DEFAULT_MODES = [
|
|
412
|
-
{
|
|
413
|
-
id: DEFAULT_MODE_ID,
|
|
414
|
-
name: "Code",
|
|
415
|
-
description: "Default agent mode for code-generation tasks."
|
|
416
|
-
}
|
|
417
|
-
];
|
|
418
|
-
var ACPProtocolHandler = class {
|
|
419
|
-
transport;
|
|
420
|
-
defaultCwd;
|
|
421
|
-
runTurn;
|
|
422
|
-
onSessionNew;
|
|
423
|
-
modes;
|
|
424
|
-
configOptions;
|
|
425
|
-
agentName;
|
|
426
|
-
replayFor;
|
|
427
|
-
seedFor;
|
|
428
|
-
store;
|
|
429
|
-
initialized = false;
|
|
430
|
-
clientCapabilities = {};
|
|
431
|
-
sessions = /* @__PURE__ */ new Map();
|
|
432
|
-
nextId = 1;
|
|
433
|
-
// Outbound request correlation (server → client requests, e.g.
|
|
434
|
-
// session/request_permission). Keyed by our own `srv_N` ids.
|
|
435
|
-
pendingOut = /* @__PURE__ */ new Map();
|
|
436
|
-
nextOutId = 1;
|
|
437
|
-
constructor(opts) {
|
|
438
|
-
this.transport = opts.transport;
|
|
439
|
-
this.defaultCwd = opts.defaultCwd;
|
|
440
|
-
this.runTurn = opts.runTurn;
|
|
441
|
-
this.onSessionNew = opts.onSessionNew ?? (() => {
|
|
442
|
-
});
|
|
443
|
-
this.modes = opts.modes ?? DEFAULT_MODES;
|
|
444
|
-
this.configOptions = opts.configOptions ?? [];
|
|
445
|
-
this.agentName = opts.agentName ?? "wrongstack";
|
|
446
|
-
this.replayFor = opts.replayFor;
|
|
447
|
-
this.seedFor = opts.seedFor;
|
|
448
|
-
this.store = opts.store;
|
|
449
|
-
if (typeof this.transport.onMessage === "function") {
|
|
450
|
-
this.transport.onMessage((m) => this.maybeResolvePending(m));
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
/**
|
|
454
|
-
* Send a request to the client and await its response. Used for
|
|
455
|
-
* server-initiated calls like `session/request_permission`. Rejects on
|
|
456
|
-
* timeout or transport error so the caller can pick a safe fallback.
|
|
457
|
-
*/
|
|
458
|
-
request(method, params, timeoutMs = 6e4) {
|
|
459
|
-
const id = `srv_${this.nextOutId++}`;
|
|
460
|
-
return new Promise((resolve3, reject) => {
|
|
461
|
-
const timer = setTimeout(() => {
|
|
462
|
-
this.pendingOut.delete(id);
|
|
463
|
-
reject(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
464
|
-
}, timeoutMs);
|
|
465
|
-
this.pendingOut.set(id, { resolve: resolve3, reject, timer });
|
|
466
|
-
this.transport.send(toWire({ jsonrpc: "2.0", id, method, params })).catch((e) => {
|
|
467
|
-
clearTimeout(timer);
|
|
468
|
-
this.pendingOut.delete(id);
|
|
469
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
470
|
-
});
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
maybeResolvePending(m) {
|
|
474
|
-
const id = m.id;
|
|
475
|
-
if (typeof id !== "string") return;
|
|
476
|
-
const pending = this.pendingOut.get(id);
|
|
477
|
-
if (!pending) return;
|
|
478
|
-
this.pendingOut.delete(id);
|
|
479
|
-
clearTimeout(pending.timer);
|
|
480
|
-
const err = m.error;
|
|
481
|
-
if (err) pending.reject(new Error(err.message ?? "client request failed"));
|
|
482
|
-
else pending.resolve(m.result);
|
|
483
|
-
}
|
|
484
|
-
/**
|
|
485
|
-
* Process one inbound message. Returns true if this was a terminal
|
|
486
|
-
* message (rare; reserved for future use by the server's own
|
|
487
|
-
* shutdown signal).
|
|
488
|
-
*/
|
|
489
|
-
async handleMessage(msg) {
|
|
490
|
-
if (typeof msg !== "object" || msg === null) return false;
|
|
491
|
-
const m = msg;
|
|
492
|
-
if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
|
|
493
|
-
return false;
|
|
494
|
-
}
|
|
495
|
-
if (m.id !== void 0 && typeof m.method === "string") {
|
|
496
|
-
return this.handleRequest(m.id, m.method, m.params);
|
|
497
|
-
}
|
|
498
|
-
if (typeof m.method === "string") {
|
|
499
|
-
return this.handleNotification(m.method, m.params);
|
|
500
|
-
}
|
|
501
|
-
return false;
|
|
502
|
-
}
|
|
503
|
-
/** Abort all active turns and drop session state. */
|
|
504
|
-
close() {
|
|
505
|
-
for (const [, session] of this.sessions) {
|
|
506
|
-
session.abort.abort();
|
|
507
|
-
}
|
|
508
|
-
this.sessions.clear();
|
|
509
|
-
for (const [, p] of this.pendingOut) {
|
|
510
|
-
clearTimeout(p.timer);
|
|
511
|
-
p.reject(new Error("protocol handler closed"));
|
|
512
|
-
}
|
|
513
|
-
this.pendingOut.clear();
|
|
514
|
-
}
|
|
515
|
-
// ────────────────────────────────────────────────────────────────────
|
|
516
|
-
// Requests
|
|
517
|
-
// ────────────────────────────────────────────────────────────────────
|
|
518
|
-
async handleRequest(id, method, params) {
|
|
519
|
-
if (method !== "initialize" && !this.initialized) {
|
|
520
|
-
await this.sendError(id, -32e3, "Not initialized");
|
|
521
|
-
return false;
|
|
522
|
-
}
|
|
523
|
-
try {
|
|
524
|
-
switch (method) {
|
|
525
|
-
case "initialize":
|
|
526
|
-
return await this.handleInitialize(id, params);
|
|
527
|
-
case "authenticate":
|
|
528
|
-
return await this.handleAuthenticate(id, params);
|
|
529
|
-
case "logout":
|
|
530
|
-
return await this.handleLogout(id, params);
|
|
531
|
-
case "session/new":
|
|
532
|
-
return await this.handleSessionNew(id, params);
|
|
533
|
-
case "session/load":
|
|
534
|
-
return await this.handleSessionLoad(id, params);
|
|
535
|
-
case "session/resume":
|
|
536
|
-
return await this.handleSessionResume(id, params);
|
|
537
|
-
case "session/close":
|
|
538
|
-
return await this.handleSessionClose(id, params);
|
|
539
|
-
case "session/delete":
|
|
540
|
-
return await this.handleSessionDelete(id, params);
|
|
541
|
-
case "session/prompt":
|
|
542
|
-
return await this.handleSessionPrompt(id, params);
|
|
543
|
-
case "session/set_mode":
|
|
544
|
-
return await this.handleSetMode(id, params);
|
|
545
|
-
case "session/set_config_option":
|
|
546
|
-
return await this.handleSetConfigOption(id, params);
|
|
547
|
-
case "session/list":
|
|
548
|
-
return await this.handleSessionList(id);
|
|
549
|
-
case "session/fork":
|
|
550
|
-
return await this.handleSessionFork(id, params);
|
|
551
|
-
case "providers/list":
|
|
552
|
-
return await this.handleProvidersList(id, params);
|
|
553
|
-
case "providers/set":
|
|
554
|
-
return await this.handleProvidersSet(id, params);
|
|
555
|
-
case "providers/disable":
|
|
556
|
-
return await this.handleProvidersDisable(id, params);
|
|
557
|
-
case "mcp/message":
|
|
558
|
-
return await this.handleMcpMessage(id, params);
|
|
559
|
-
default:
|
|
560
|
-
await this.sendError(id, -32601, `Unknown method: ${method}`);
|
|
561
|
-
return false;
|
|
562
|
-
}
|
|
563
|
-
} catch (err) {
|
|
564
|
-
const { code, message, data } = errorToJsonRpc(err);
|
|
565
|
-
await this.sendError(id, code, message, data);
|
|
566
|
-
return false;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
async handleInitialize(id, params) {
|
|
570
|
-
const p = params ?? {};
|
|
571
|
-
if (p.clientCapabilities && typeof p.clientCapabilities === "object") {
|
|
572
|
-
this.clientCapabilities = p.clientCapabilities;
|
|
573
|
-
}
|
|
574
|
-
this.initialized = true;
|
|
506
|
+
async handleProvidersList(id, _params) {
|
|
575
507
|
await this.transport.send(toWire({
|
|
576
508
|
jsonrpc: "2.0",
|
|
577
509
|
id,
|
|
578
510
|
result: {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
loadSession: true,
|
|
582
|
-
promptCapabilities: {
|
|
583
|
-
// We route ACP image blocks into the core agent's multimodal
|
|
584
|
-
// input (server-agent-turn.promptToAgentInput); whether the
|
|
585
|
-
// model can see them is the configured provider's concern.
|
|
586
|
-
image: true,
|
|
587
|
-
audio: false,
|
|
588
|
-
embeddedContext: true
|
|
589
|
-
},
|
|
590
|
-
mcpCapabilities: {
|
|
591
|
-
http: false,
|
|
592
|
-
sse: false
|
|
593
|
-
},
|
|
594
|
-
sessionCapabilities: {
|
|
595
|
-
close: {},
|
|
596
|
-
list: {},
|
|
597
|
-
delete: {},
|
|
598
|
-
resume: {},
|
|
599
|
-
fork: {}
|
|
600
|
-
},
|
|
601
|
-
auth: {
|
|
602
|
-
logout: {}
|
|
603
|
-
}
|
|
604
|
-
},
|
|
605
|
-
agentInfo: {
|
|
606
|
-
name: this.agentName,
|
|
607
|
-
title: "WrongStack",
|
|
608
|
-
version: WRONGSTACK_VERSION
|
|
609
|
-
},
|
|
610
|
-
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
611
|
-
modes: this.modes,
|
|
612
|
-
configOptions: this.configOptions
|
|
511
|
+
providers: [],
|
|
512
|
+
currentProviderId: null
|
|
613
513
|
}
|
|
614
514
|
}));
|
|
615
515
|
return false;
|
|
616
516
|
}
|
|
617
|
-
async
|
|
618
|
-
await this.
|
|
619
|
-
jsonrpc: "2.0",
|
|
620
|
-
id,
|
|
621
|
-
result: { outcome: "unauthenticated" }
|
|
622
|
-
}));
|
|
517
|
+
async handleProvidersSet(id, _params) {
|
|
518
|
+
await this.sendError(id, -32e3, "provider configuration not available through ACP; use wstack auth");
|
|
623
519
|
return false;
|
|
624
520
|
}
|
|
625
|
-
async
|
|
521
|
+
async handleProvidersDisable(id, _params) {
|
|
626
522
|
await this.transport.send(toWire({
|
|
627
523
|
jsonrpc: "2.0",
|
|
628
524
|
id,
|
|
@@ -630,251 +526,11 @@ var ACPProtocolHandler = class {
|
|
|
630
526
|
}));
|
|
631
527
|
return false;
|
|
632
528
|
}
|
|
633
|
-
async
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const state = {
|
|
639
|
-
id: sessionId,
|
|
640
|
-
cwd,
|
|
641
|
-
abort: new AbortController(),
|
|
642
|
-
modeId: DEFAULT_MODE_ID,
|
|
643
|
-
createdAt: now,
|
|
644
|
-
updatedAt: now
|
|
645
|
-
};
|
|
646
|
-
this.sessions.set(sessionId, state);
|
|
647
|
-
this.onSessionNew(state);
|
|
648
|
-
await this.persist(state);
|
|
649
|
-
await this.sendNotification({
|
|
650
|
-
sessionId,
|
|
651
|
-
update: {
|
|
652
|
-
sessionUpdate: "current_mode_update",
|
|
653
|
-
modeId: this.modes[0]?.id ?? DEFAULT_MODE_ID
|
|
654
|
-
}
|
|
655
|
-
});
|
|
656
|
-
if (this.configOptions.length > 0) {
|
|
657
|
-
await this.sendNotification({
|
|
658
|
-
sessionId,
|
|
659
|
-
update: {
|
|
660
|
-
sessionUpdate: "config_option_update",
|
|
661
|
-
configOptions: [...this.configOptions]
|
|
662
|
-
}
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
await this.transport.send(toWire({
|
|
666
|
-
jsonrpc: "2.0",
|
|
667
|
-
id,
|
|
668
|
-
result: {
|
|
669
|
-
sessionId,
|
|
670
|
-
modes: this.modes,
|
|
671
|
-
configOptions: this.configOptions
|
|
672
|
-
}
|
|
673
|
-
}));
|
|
674
|
-
return false;
|
|
675
|
-
}
|
|
676
|
-
async handleSessionLoad(id, params) {
|
|
677
|
-
const p = params ?? {};
|
|
678
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
679
|
-
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
680
|
-
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
681
|
-
if (!existing && sessionId && this.store) {
|
|
682
|
-
const persisted = await this.store.load(sessionId);
|
|
683
|
-
if (persisted) {
|
|
684
|
-
const restored = {
|
|
685
|
-
id: sessionId,
|
|
686
|
-
cwd: persisted.cwd ?? loadCwd ?? this.defaultCwd,
|
|
687
|
-
abort: new AbortController(),
|
|
688
|
-
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
689
|
-
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
690
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
691
|
-
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
692
|
-
};
|
|
693
|
-
this.sessions.set(sessionId, restored);
|
|
694
|
-
this.seedFor?.(sessionId, persisted.history ?? []);
|
|
695
|
-
for (const update of persisted.history ?? []) {
|
|
696
|
-
await this.sendNotification({ sessionId, update });
|
|
697
|
-
}
|
|
698
|
-
await this.sendNotification({
|
|
699
|
-
sessionId,
|
|
700
|
-
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
701
|
-
});
|
|
702
|
-
await this.transport.send(toWire({
|
|
703
|
-
jsonrpc: "2.0",
|
|
704
|
-
id,
|
|
705
|
-
result: {
|
|
706
|
-
initialMode: { currentModeId: restored.modeId, availableModes: this.modes }
|
|
707
|
-
}
|
|
708
|
-
}));
|
|
709
|
-
return false;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
if (existing) {
|
|
713
|
-
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
714
|
-
const replay = sessionId ? this.replayFor?.(sessionId) : void 0;
|
|
715
|
-
if (replay) {
|
|
716
|
-
for (const update of replay) {
|
|
717
|
-
await this.sendNotification({ sessionId, update });
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
await this.sendNotification({
|
|
721
|
-
sessionId,
|
|
722
|
-
update: {
|
|
723
|
-
sessionUpdate: "session_info_update",
|
|
724
|
-
updatedAt: existing.updatedAt
|
|
725
|
-
}
|
|
726
|
-
});
|
|
727
|
-
await this.sendNotification({
|
|
728
|
-
sessionId,
|
|
729
|
-
update: {
|
|
730
|
-
sessionUpdate: "current_mode_update",
|
|
731
|
-
modeId: existing.modeId
|
|
732
|
-
}
|
|
733
|
-
});
|
|
734
|
-
await this.transport.send(toWire({
|
|
735
|
-
jsonrpc: "2.0",
|
|
736
|
-
id,
|
|
737
|
-
result: {
|
|
738
|
-
initialMode: {
|
|
739
|
-
currentModeId: existing.modeId,
|
|
740
|
-
availableModes: this.modes
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}));
|
|
744
|
-
return false;
|
|
745
|
-
}
|
|
746
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
747
|
-
return false;
|
|
748
|
-
}
|
|
749
|
-
async handleSessionResume(id, params) {
|
|
750
|
-
const p = params ?? {};
|
|
751
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
752
|
-
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
753
|
-
if (existing) {
|
|
754
|
-
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
755
|
-
await this.transport.send(toWire({
|
|
756
|
-
jsonrpc: "2.0",
|
|
757
|
-
id,
|
|
758
|
-
result: {
|
|
759
|
-
initialMode: {
|
|
760
|
-
currentModeId: existing.modeId,
|
|
761
|
-
availableModes: this.modes
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}));
|
|
765
|
-
return false;
|
|
766
|
-
}
|
|
767
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
768
|
-
return false;
|
|
769
|
-
}
|
|
770
|
-
async handleSessionClose(id, params) {
|
|
771
|
-
const p = params ?? {};
|
|
772
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
773
|
-
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
774
|
-
if (!session) {
|
|
775
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
776
|
-
return false;
|
|
777
|
-
}
|
|
778
|
-
session.abort.abort();
|
|
779
|
-
if (sessionId) this.sessions.delete(sessionId);
|
|
780
|
-
await this.transport.send(toWire({
|
|
781
|
-
jsonrpc: "2.0",
|
|
782
|
-
id,
|
|
783
|
-
result: {}
|
|
784
|
-
}));
|
|
785
|
-
return false;
|
|
786
|
-
}
|
|
787
|
-
async handleSessionDelete(id, params) {
|
|
788
|
-
const p = params ?? {};
|
|
789
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
790
|
-
if (!sessionId) {
|
|
791
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
792
|
-
return false;
|
|
793
|
-
}
|
|
794
|
-
if (!this.sessions.has(sessionId)) {
|
|
795
|
-
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } }));
|
|
796
|
-
return false;
|
|
797
|
-
}
|
|
798
|
-
const session = this.sessions.get(sessionId);
|
|
799
|
-
session.abort.abort();
|
|
800
|
-
this.sessions.delete(sessionId);
|
|
801
|
-
await this.transport.send(toWire({
|
|
802
|
-
jsonrpc: "2.0",
|
|
803
|
-
id,
|
|
804
|
-
result: {}
|
|
805
|
-
}));
|
|
806
|
-
return false;
|
|
807
|
-
}
|
|
808
|
-
async handleSessionFork(id, params) {
|
|
809
|
-
const p = params ?? {};
|
|
810
|
-
const sourceId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
811
|
-
const source = sourceId ? this.sessions.get(sourceId) : void 0;
|
|
812
|
-
if (!sourceId || !source) {
|
|
813
|
-
await this.sendError(id, -32e3, `session not found: ${sourceId}`);
|
|
814
|
-
return false;
|
|
815
|
-
}
|
|
816
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
817
|
-
const sessionId = `sess_${this.allocId()}`;
|
|
818
|
-
const forked = {
|
|
819
|
-
id: sessionId,
|
|
820
|
-
cwd: typeof p.cwd === "string" ? p.cwd : source.cwd,
|
|
821
|
-
abort: new AbortController(),
|
|
822
|
-
modeId: source.modeId,
|
|
823
|
-
createdAt: now,
|
|
824
|
-
updatedAt: now,
|
|
825
|
-
...source.title !== void 0 ? { title: source.title } : {}
|
|
826
|
-
};
|
|
827
|
-
const history = (this.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
828
|
-
sessionUpdate: update.sessionUpdate,
|
|
829
|
-
content: structuredClone(update.content)
|
|
830
|
-
}));
|
|
831
|
-
this.sessions.set(sessionId, forked);
|
|
832
|
-
this.seedFor?.(sessionId, history);
|
|
833
|
-
this.onSessionNew(forked);
|
|
834
|
-
await this.persist(forked, history);
|
|
835
|
-
await this.sendNotification({
|
|
836
|
-
sessionId,
|
|
837
|
-
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
838
|
-
});
|
|
839
|
-
await this.transport.send(toWire({
|
|
840
|
-
jsonrpc: "2.0",
|
|
841
|
-
id,
|
|
842
|
-
result: {
|
|
843
|
-
sessionId,
|
|
844
|
-
modes: this.modes,
|
|
845
|
-
configOptions: this.configOptions
|
|
846
|
-
}
|
|
847
|
-
}));
|
|
848
|
-
return false;
|
|
849
|
-
}
|
|
850
|
-
async handleProvidersList(id, _params) {
|
|
851
|
-
await this.transport.send(toWire({
|
|
852
|
-
jsonrpc: "2.0",
|
|
853
|
-
id,
|
|
854
|
-
result: {
|
|
855
|
-
providers: [],
|
|
856
|
-
currentProviderId: null
|
|
857
|
-
}
|
|
858
|
-
}));
|
|
859
|
-
return false;
|
|
860
|
-
}
|
|
861
|
-
async handleProvidersSet(id, _params) {
|
|
862
|
-
await this.sendError(id, -32e3, "provider configuration not available through ACP; use wstack auth");
|
|
863
|
-
return false;
|
|
864
|
-
}
|
|
865
|
-
async handleProvidersDisable(id, _params) {
|
|
866
|
-
await this.transport.send(toWire({
|
|
867
|
-
jsonrpc: "2.0",
|
|
868
|
-
id,
|
|
869
|
-
result: {}
|
|
870
|
-
}));
|
|
871
|
-
return false;
|
|
872
|
-
}
|
|
873
|
-
async handleMcpMessage(id, _params) {
|
|
874
|
-
await this.sendError(id, -32e3, "MCP message routing not available through ACP");
|
|
875
|
-
return false;
|
|
876
|
-
}
|
|
877
|
-
async handleSessionPrompt(id, params) {
|
|
529
|
+
async handleMcpMessage(id, _params) {
|
|
530
|
+
await this.sendError(id, -32e3, "MCP message routing not available through ACP");
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
async handleSessionPrompt(id, params) {
|
|
878
534
|
const p = params ?? {};
|
|
879
535
|
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
880
536
|
if (!sessionId || !this.sessions.has(sessionId)) {
|
|
@@ -1078,259 +734,273 @@ function errorToJsonRpc(err) {
|
|
|
1078
734
|
return { code: -32603, message };
|
|
1079
735
|
}
|
|
1080
736
|
|
|
1081
|
-
// src/agent/
|
|
1082
|
-
import {
|
|
1083
|
-
import {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
...opts.seedFor ? { seedFor: opts.seedFor } : {},
|
|
1103
|
-
...opts.store ? { store: opts.store } : {}
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
/**
|
|
1107
|
-
* Start the server. Mode depends on `options.transport`:
|
|
1108
|
-
* - 'stdio' (default): reads JSON-RPC from stdin, writes to stdout.
|
|
1109
|
-
* - number: listens as HTTP on the given port.
|
|
1110
|
-
*/
|
|
1111
|
-
async start() {
|
|
1112
|
-
const transportMode = this.options.transport;
|
|
1113
|
-
if (typeof transportMode === "number") {
|
|
1114
|
-
await this.startHttp(transportMode);
|
|
1115
|
-
} else {
|
|
1116
|
-
await this.startStdio();
|
|
737
|
+
// src/agent/stdio-transport.ts
|
|
738
|
+
import { expectDefined, writeErr } from "@wrongstack/core/utils";
|
|
739
|
+
import { treeKill } from "@wrongstack/core/utils/tree-kill";
|
|
740
|
+
|
|
741
|
+
// src/win32-cmd.ts
|
|
742
|
+
var WIN32_CMD_META = /[&|<>"\r\n\0]/;
|
|
743
|
+
function buildWin32CmdShimInvocation(command, args = []) {
|
|
744
|
+
assertSafeWin32CmdArgs([command, ...args]);
|
|
745
|
+
const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
|
|
746
|
+
return {
|
|
747
|
+
command: process.env["COMSPEC"] ?? "cmd.exe",
|
|
748
|
+
args: ["/d", "/c", line],
|
|
749
|
+
windowsVerbatimArguments: true
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function assertSafeWin32CmdArgs(args) {
|
|
753
|
+
for (const arg of args) {
|
|
754
|
+
if (typeof arg === "string" && WIN32_CMD_META.test(arg)) {
|
|
755
|
+
throw new Error(
|
|
756
|
+
'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > ", or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
|
|
757
|
+
);
|
|
1117
758
|
}
|
|
1118
759
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
760
|
+
}
|
|
761
|
+
function quoteWin32CmdArg(arg) {
|
|
762
|
+
return `"${arg}"`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/agent/stdio-transport.ts
|
|
766
|
+
var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
|
|
767
|
+
var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
|
|
768
|
+
function positiveLimit(value, fallback) {
|
|
769
|
+
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
770
|
+
}
|
|
771
|
+
var StdioTransport = class {
|
|
772
|
+
stdin = process.stdin;
|
|
773
|
+
stdout = process.stdout;
|
|
774
|
+
stderr = process.stderr;
|
|
775
|
+
buffer = "";
|
|
776
|
+
handlers = /* @__PURE__ */ new Set();
|
|
777
|
+
closed = false;
|
|
778
|
+
resolveRead = null;
|
|
779
|
+
messageQueue = [];
|
|
780
|
+
maxFrameChars;
|
|
781
|
+
maxQueuedMessages;
|
|
782
|
+
constructor(opts = {}) {
|
|
783
|
+
this.maxFrameChars = positiveLimit(opts.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
|
|
784
|
+
this.maxQueuedMessages = positiveLimit(opts.maxQueuedMessages, DEFAULT_MAX_QUEUED_MESSAGES);
|
|
785
|
+
this.stdin.resume();
|
|
786
|
+
this.stdin.setEncoding("utf8");
|
|
787
|
+
this.stdin.on("data", (chunk) => this.onData(chunk));
|
|
788
|
+
this.stdin.on("end", () => this.handleClose());
|
|
789
|
+
this.stdin.on("error", (err) => this.failAll(err));
|
|
1131
790
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
791
|
+
sendStartupMarker() {
|
|
792
|
+
this.stdout.write("[wstack-acp]\n", "utf8");
|
|
793
|
+
}
|
|
794
|
+
send(msg) {
|
|
795
|
+
if (this.closed) return Promise.resolve();
|
|
796
|
+
return new Promise((resolve3) => {
|
|
797
|
+
const line = JSON.stringify(msg) + "\n";
|
|
798
|
+
this.stdout.write(line, "utf8", () => resolve3());
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
sendRaw(chunk) {
|
|
802
|
+
this.stdout.write(chunk, "utf8");
|
|
803
|
+
}
|
|
804
|
+
read() {
|
|
805
|
+
if (this.messageQueue.length > 0)
|
|
806
|
+
return Promise.resolve(expectDefined(this.messageQueue.shift()));
|
|
807
|
+
if (this.closed) return Promise.resolve(null);
|
|
808
|
+
return new Promise((resolve3) => {
|
|
809
|
+
this.resolveRead = resolve3;
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
onMessage(handler) {
|
|
813
|
+
this.handlers.add(handler);
|
|
814
|
+
return () => this.handlers.delete(handler);
|
|
815
|
+
}
|
|
816
|
+
close() {
|
|
817
|
+
this.closed = true;
|
|
818
|
+
this.stdin.pause();
|
|
819
|
+
this.resolveRead?.(null);
|
|
820
|
+
this.resolveRead = null;
|
|
821
|
+
this.buffer = "";
|
|
822
|
+
this.messageQueue.length = 0;
|
|
823
|
+
this.handlers.clear();
|
|
824
|
+
}
|
|
825
|
+
onData(chunk) {
|
|
826
|
+
this.buffer += chunk;
|
|
827
|
+
const lines = this.buffer.split("\n");
|
|
828
|
+
this.buffer = lines.pop() ?? "";
|
|
829
|
+
if (this.buffer.length > this.maxFrameChars) {
|
|
830
|
+
this.stderr.write(
|
|
831
|
+
`[wstack-acp frame error] pending frame exceeds ${this.maxFrameChars} characters
|
|
832
|
+
`,
|
|
833
|
+
"utf8"
|
|
834
|
+
);
|
|
835
|
+
this.close();
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
for (const raw of lines) {
|
|
839
|
+
if (!raw.trim()) continue;
|
|
840
|
+
if (raw.length > this.maxFrameChars) {
|
|
841
|
+
this.stderr.write(
|
|
842
|
+
`[wstack-acp frame error] frame exceeds ${this.maxFrameChars} characters
|
|
843
|
+
`,
|
|
844
|
+
"utf8"
|
|
845
|
+
);
|
|
846
|
+
this.close();
|
|
1185
847
|
return;
|
|
1186
848
|
}
|
|
1187
|
-
let msg;
|
|
1188
849
|
try {
|
|
1189
|
-
|
|
1190
|
-
} catch {
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
return;
|
|
850
|
+
this.dispatch(JSON.parse(raw));
|
|
851
|
+
} catch (err) {
|
|
852
|
+
this.stderr.write(`[wstack-acp parse error] ${err}
|
|
853
|
+
`, "utf8");
|
|
1194
854
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
dispatch(msg) {
|
|
858
|
+
if (this.resolveRead) {
|
|
859
|
+
const resolve3 = this.resolveRead;
|
|
860
|
+
this.resolveRead = null;
|
|
861
|
+
resolve3(msg);
|
|
862
|
+
} else {
|
|
863
|
+
if (this.messageQueue.length >= this.maxQueuedMessages) {
|
|
864
|
+
this.stderr.write(
|
|
865
|
+
`[wstack-acp queue error] pending message queue exceeds ${this.maxQueuedMessages} entries
|
|
866
|
+
`,
|
|
867
|
+
"utf8"
|
|
868
|
+
);
|
|
869
|
+
this.close();
|
|
1203
870
|
return;
|
|
1204
871
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
const originalSend = this.transport.send.bind(this.transport);
|
|
1209
|
-
this.transport.send = async (m) => {
|
|
1210
|
-
if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
|
|
1211
|
-
response = m;
|
|
1212
|
-
} else if (m.method === "session/update") {
|
|
1213
|
-
notifications.push(m.params);
|
|
1214
|
-
} else {
|
|
1215
|
-
notifications.push(m);
|
|
1216
|
-
}
|
|
1217
|
-
};
|
|
1218
|
-
try {
|
|
1219
|
-
await handler.handleMessage(msg);
|
|
1220
|
-
} finally {
|
|
1221
|
-
this.transport.send = originalSend;
|
|
1222
|
-
}
|
|
1223
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1224
|
-
const responseBody = response !== null ? { ...response, notifications } : { notifications };
|
|
1225
|
-
res.end(JSON.stringify(responseBody));
|
|
1226
|
-
});
|
|
1227
|
-
httpChain = requestPromise.catch(() => void 0);
|
|
872
|
+
this.messageQueue.push(msg);
|
|
873
|
+
}
|
|
874
|
+
for (const handler of this.handlers) {
|
|
1228
875
|
try {
|
|
1229
|
-
|
|
1230
|
-
} catch {
|
|
876
|
+
handler(msg);
|
|
877
|
+
} catch (err) {
|
|
878
|
+
this.stderr.write(`[wstack-acp handler error] ${err}
|
|
879
|
+
`, "utf8");
|
|
1231
880
|
}
|
|
1232
|
-
});
|
|
1233
|
-
return new Promise((resolve3) => {
|
|
1234
|
-
this.httpServer.listen(port, host, () => {
|
|
1235
|
-
writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${port}
|
|
1236
|
-
`);
|
|
1237
|
-
this.running = true;
|
|
1238
|
-
resolve3();
|
|
1239
|
-
});
|
|
1240
|
-
});
|
|
1241
|
-
}
|
|
1242
|
-
/** Stop the server. */
|
|
1243
|
-
stop() {
|
|
1244
|
-
this.running = false;
|
|
1245
|
-
this.transport.close();
|
|
1246
|
-
if (this.httpServer) {
|
|
1247
|
-
this.httpServer.close();
|
|
1248
|
-
this.httpServer = null;
|
|
1249
881
|
}
|
|
1250
882
|
}
|
|
883
|
+
handleClose() {
|
|
884
|
+
this.close();
|
|
885
|
+
}
|
|
886
|
+
failAll(err) {
|
|
887
|
+
this.stderr.write(`[wstack-acp stdin error] ${err.message}
|
|
888
|
+
`, "utf8");
|
|
889
|
+
this.close();
|
|
890
|
+
}
|
|
1251
891
|
};
|
|
1252
|
-
var
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
async function main() {
|
|
1256
|
-
const server = new WrongStackACPServer();
|
|
1257
|
-
await server.start();
|
|
1258
|
-
}
|
|
1259
|
-
var isEntrypoint = process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === process.argv[1];
|
|
1260
|
-
if (isEntrypoint) {
|
|
1261
|
-
main().catch((err) => {
|
|
1262
|
-
writeErr2(`[wstack-acp fatal] ${err}
|
|
1263
|
-
`);
|
|
1264
|
-
process.exit(1);
|
|
1265
|
-
});
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
// src/client/websocket-transport.ts
|
|
1269
|
-
var WebSocketClientTransport = class {
|
|
1270
|
-
ws = null;
|
|
892
|
+
var ClientTransport = class {
|
|
893
|
+
child = null;
|
|
894
|
+
buffer = "";
|
|
1271
895
|
handlers = /* @__PURE__ */ new Set();
|
|
1272
896
|
closed = false;
|
|
897
|
+
resolveRead = null;
|
|
898
|
+
messageQueue = [];
|
|
1273
899
|
opts;
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
const
|
|
900
|
+
maxFrameChars;
|
|
901
|
+
maxQueuedMessages;
|
|
902
|
+
constructor(options) {
|
|
903
|
+
this.opts = {
|
|
904
|
+
handshakeTimeoutMs: 3e4,
|
|
905
|
+
...options
|
|
906
|
+
};
|
|
907
|
+
this.maxFrameChars = positiveLimit(options.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
|
|
908
|
+
this.maxQueuedMessages = positiveLimit(options.maxQueuedMessages, DEFAULT_MAX_QUEUED_MESSAGES);
|
|
909
|
+
}
|
|
910
|
+
async start() {
|
|
911
|
+
if (this.child) return;
|
|
912
|
+
const [{ spawn: spawn3 }, { buildChildEnv: buildChildEnv2 }, os] = await Promise.all([
|
|
913
|
+
import("node:child_process"),
|
|
914
|
+
import("@wrongstack/core/utils"),
|
|
915
|
+
import("node:os")
|
|
916
|
+
]);
|
|
1287
917
|
return new Promise((resolve3, reject) => {
|
|
918
|
+
const timeout = setTimeout(() => {
|
|
919
|
+
reject(
|
|
920
|
+
new Error(`ACP child process failed to start within ${this.opts.handshakeTimeoutMs}ms`)
|
|
921
|
+
);
|
|
922
|
+
}, this.opts.handshakeTimeoutMs);
|
|
923
|
+
const isPkgLauncher = this.opts.command === "npx" || this.opts.command === "uvx";
|
|
924
|
+
const spawnCwd = isPkgLauncher ? os.homedir() : this.opts.cwd;
|
|
925
|
+
try {
|
|
926
|
+
const childArgs = this.opts.args ?? [];
|
|
927
|
+
const invocation = spawnInvocation(this.opts.command, childArgs, process.platform);
|
|
928
|
+
this.child = spawn3(invocation.command, invocation.args, {
|
|
929
|
+
env: { ...buildChildEnv2(), ...this.opts.env },
|
|
930
|
+
cwd: spawnCwd,
|
|
931
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
932
|
+
windowsHide: true,
|
|
933
|
+
...verbatimOptions(invocation)
|
|
934
|
+
});
|
|
935
|
+
} catch (err) {
|
|
936
|
+
clearTimeout(timeout);
|
|
937
|
+
reject(err);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const child = this.child;
|
|
941
|
+
child.stdout.setEncoding("utf8");
|
|
1288
942
|
let settled = false;
|
|
1289
|
-
const
|
|
1290
|
-
this.ws = ws;
|
|
1291
|
-
const timer = setTimeout(() => {
|
|
1292
|
-
if (settled) return;
|
|
1293
|
-
settled = true;
|
|
1294
|
-
try {
|
|
1295
|
-
ws.close();
|
|
1296
|
-
} catch {
|
|
1297
|
-
}
|
|
1298
|
-
reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
1299
|
-
}, timeoutMs);
|
|
1300
|
-
ws.addEventListener("open", () => {
|
|
1301
|
-
if (settled) return;
|
|
1302
|
-
settled = true;
|
|
1303
|
-
clearTimeout(timer);
|
|
1304
|
-
resolve3();
|
|
1305
|
-
});
|
|
1306
|
-
ws.addEventListener("error", (ev) => {
|
|
943
|
+
const onSpawnFailure = (err) => {
|
|
1307
944
|
if (settled) {
|
|
1308
945
|
this.closed = true;
|
|
1309
946
|
return;
|
|
1310
947
|
}
|
|
1311
948
|
settled = true;
|
|
1312
|
-
clearTimeout(
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
this.
|
|
1321
|
-
|
|
949
|
+
clearTimeout(timeout);
|
|
950
|
+
reject(err);
|
|
951
|
+
};
|
|
952
|
+
child.on("error", onSpawnFailure);
|
|
953
|
+
child.stdout.on("error", onSpawnFailure);
|
|
954
|
+
if (this.opts.skipHandshakeMarker) {
|
|
955
|
+
child.stdout.on("data", (c) => this.onChildData(c));
|
|
956
|
+
child.stderr.on("data", (c) => this.onChildError(c));
|
|
957
|
+
child.on("close", (code) => this.onChildClose(code));
|
|
958
|
+
child.once("spawn", () => {
|
|
959
|
+
if (settled) return;
|
|
960
|
+
settled = true;
|
|
961
|
+
clearTimeout(timeout);
|
|
962
|
+
resolve3();
|
|
963
|
+
});
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const onReady = () => {
|
|
967
|
+
if (settled) return;
|
|
968
|
+
settled = true;
|
|
969
|
+
child.stdout.on("data", (c) => this.onChildData(c));
|
|
970
|
+
child.stderr.on("data", (c) => this.onChildError(c));
|
|
971
|
+
child.on("close", (code) => this.onChildClose(code));
|
|
972
|
+
clearTimeout(timeout);
|
|
973
|
+
resolve3();
|
|
974
|
+
};
|
|
975
|
+
const waitForMarker = (chunk) => {
|
|
976
|
+
this.buffer += chunk;
|
|
977
|
+
const idx = this.buffer.indexOf("[wstack-acp]\n");
|
|
978
|
+
if (idx !== -1) {
|
|
979
|
+
this.buffer = this.buffer.slice(idx + "[wstack-acp]\n".length);
|
|
980
|
+
child.stdout.removeListener("data", waitForMarker);
|
|
981
|
+
onReady();
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
child.stdout.on("data", waitForMarker);
|
|
1322
985
|
});
|
|
1323
986
|
}
|
|
1324
987
|
send(msg) {
|
|
1325
|
-
if (this.
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
988
|
+
if (!this.child) return Promise.reject(new Error("ClientTransport not started"));
|
|
989
|
+
return new Promise((resolve3, reject) => {
|
|
990
|
+
const line = JSON.stringify(msg) + "\n";
|
|
991
|
+
this.child?.stdin.write(line, "utf8", (err) => {
|
|
992
|
+
if (err) reject(err);
|
|
993
|
+
else resolve3();
|
|
994
|
+
});
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
read() {
|
|
998
|
+
if (this.messageQueue.length > 0)
|
|
999
|
+
return Promise.resolve(expectDefined(this.messageQueue.shift()));
|
|
1000
|
+
if (this.closed) return Promise.resolve(null);
|
|
1001
|
+
return new Promise((resolve3) => {
|
|
1002
|
+
this.resolveRead = resolve3;
|
|
1003
|
+
});
|
|
1334
1004
|
}
|
|
1335
1005
|
onMessage(handler) {
|
|
1336
1006
|
this.handlers.add(handler);
|
|
@@ -1338,34 +1008,70 @@ var WebSocketClientTransport = class {
|
|
|
1338
1008
|
}
|
|
1339
1009
|
stop() {
|
|
1340
1010
|
this.closed = true;
|
|
1341
|
-
|
|
1011
|
+
this.resolveRead?.(null);
|
|
1012
|
+
this.resolveRead = null;
|
|
1013
|
+
this.buffer = "";
|
|
1014
|
+
this.messageQueue.length = 0;
|
|
1015
|
+
this.handlers.clear();
|
|
1016
|
+
const child = this.child;
|
|
1017
|
+
if (!child) return;
|
|
1018
|
+
treeKill(child);
|
|
1019
|
+
this.child = null;
|
|
1020
|
+
}
|
|
1021
|
+
onChildData(chunk) {
|
|
1022
|
+
this.buffer += chunk;
|
|
1023
|
+
const lines = this.buffer.split("\n");
|
|
1024
|
+
this.buffer = lines.pop() ?? "";
|
|
1025
|
+
if (this.buffer.length > this.maxFrameChars) {
|
|
1026
|
+
writeErr(`[acp-child pending frame exceeds ${this.maxFrameChars} characters]
|
|
1027
|
+
`);
|
|
1028
|
+
this.stop();
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
for (const raw of lines) {
|
|
1032
|
+
if (!raw.trim()) continue;
|
|
1033
|
+
if (raw.length > this.maxFrameChars) {
|
|
1034
|
+
writeErr(`[acp-child frame exceeds ${this.maxFrameChars} characters]
|
|
1035
|
+
`);
|
|
1036
|
+
this.stop();
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1342
1039
|
try {
|
|
1343
|
-
this.
|
|
1040
|
+
this.dispatch(JSON.parse(raw));
|
|
1344
1041
|
} catch {
|
|
1345
1042
|
}
|
|
1346
|
-
this.ws = null;
|
|
1347
1043
|
}
|
|
1348
1044
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
}
|
|
1363
|
-
return;
|
|
1045
|
+
onChildError(chunk) {
|
|
1046
|
+
writeErr(`[acp-child stderr] ${chunk}`);
|
|
1047
|
+
}
|
|
1048
|
+
onChildClose(code) {
|
|
1049
|
+
this.closed = true;
|
|
1050
|
+
this.resolveRead?.(null);
|
|
1051
|
+
this.resolveRead = null;
|
|
1052
|
+
this.buffer = "";
|
|
1053
|
+
this.messageQueue.length = 0;
|
|
1054
|
+
this.handlers.clear();
|
|
1055
|
+
if (code !== 0 && code !== null) {
|
|
1056
|
+
writeErr(`[acp-child exited with code ${code}]
|
|
1057
|
+
`);
|
|
1364
1058
|
}
|
|
1365
|
-
this.dispatch(msg);
|
|
1366
1059
|
}
|
|
1367
1060
|
dispatch(msg) {
|
|
1368
|
-
|
|
1061
|
+
if (this.resolveRead) {
|
|
1062
|
+
const resolve3 = this.resolveRead;
|
|
1063
|
+
this.resolveRead = null;
|
|
1064
|
+
resolve3(msg);
|
|
1065
|
+
} else if (this.handlers.size === 0) {
|
|
1066
|
+
if (this.messageQueue.length >= this.maxQueuedMessages) {
|
|
1067
|
+
writeErr(`[acp-child message queue exceeds ${this.maxQueuedMessages} entries]
|
|
1068
|
+
`);
|
|
1069
|
+
this.stop();
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
this.messageQueue.push(msg);
|
|
1073
|
+
}
|
|
1074
|
+
for (const handler of this.handlers) {
|
|
1369
1075
|
try {
|
|
1370
1076
|
handler(msg);
|
|
1371
1077
|
} catch {
|
|
@@ -1373,321 +1079,604 @@ var WebSocketClientTransport = class {
|
|
|
1373
1079
|
}
|
|
1374
1080
|
}
|
|
1375
1081
|
};
|
|
1082
|
+
function spawnInvocation(command, args, platform) {
|
|
1083
|
+
if (platform !== "win32") return { command, args };
|
|
1084
|
+
return buildWin32CmdShimInvocation(command, args);
|
|
1085
|
+
}
|
|
1086
|
+
function verbatimOptions(invocation) {
|
|
1087
|
+
return invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: invocation.windowsVerbatimArguments } : {};
|
|
1088
|
+
}
|
|
1376
1089
|
|
|
1377
|
-
// src/
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
};
|
|
1384
|
-
var ToolTranslator = class {
|
|
1385
|
-
opts;
|
|
1386
|
-
pending = /* @__PURE__ */ new Map();
|
|
1387
|
-
constructor(opts = {}) {
|
|
1388
|
-
this.opts = { ...DEFAULT_OPTIONS, ...opts };
|
|
1090
|
+
// src/agent/tools-registry.ts
|
|
1091
|
+
var ACPToolsRegistry = class {
|
|
1092
|
+
tools = /* @__PURE__ */ new Map();
|
|
1093
|
+
owner;
|
|
1094
|
+
constructor(owner = "wrongstack") {
|
|
1095
|
+
this.owner = owner;
|
|
1389
1096
|
}
|
|
1390
1097
|
/**
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1098
|
+
* Register one or more tools.
|
|
1099
|
+
* Throws on duplicate name unless force=true.
|
|
1393
1100
|
*/
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
if (pending) {
|
|
1399
|
-
clearTimeout(pending.timeout);
|
|
1400
|
-
this.pending.delete(expectDefined2(msg.id));
|
|
1401
|
-
pending.resolve(msg);
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
if (msg.method === "cancel" && msg.id !== void 0) {
|
|
1405
|
-
const pending = this.pending.get(msg.id);
|
|
1406
|
-
if (pending) {
|
|
1407
|
-
clearTimeout(pending.timeout);
|
|
1408
|
-
this.pending.delete(expectDefined2(msg.id));
|
|
1409
|
-
pending.reject(new Error("Call cancelled by client"));
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
});
|
|
1101
|
+
register(tools) {
|
|
1102
|
+
for (const tool of tools) {
|
|
1103
|
+
this.tools.set(tool.name, tool);
|
|
1104
|
+
}
|
|
1413
1105
|
}
|
|
1414
1106
|
/**
|
|
1415
|
-
*
|
|
1416
|
-
* If asyncTools is true, polls for progress and resolves when the final
|
|
1417
|
-
* response arrives.
|
|
1107
|
+
* Replace the current tool set.
|
|
1418
1108
|
*/
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
method: "tools/call",
|
|
1423
|
-
id: callId,
|
|
1424
|
-
params: { name, arguments: args }
|
|
1425
|
-
});
|
|
1426
|
-
return new Promise((resolve3, reject) => {
|
|
1427
|
-
const timeout = setTimeout(() => {
|
|
1428
|
-
this.pending.delete(callId);
|
|
1429
|
-
reject(new Error(`Tool call ${name} timed out after ${this.opts.totalTimeoutMs}ms`));
|
|
1430
|
-
}, this.opts.totalTimeoutMs);
|
|
1431
|
-
this.pending.set(callId, { resolve: resolve3, reject, timeout });
|
|
1432
|
-
});
|
|
1109
|
+
setTools(tools) {
|
|
1110
|
+
this.tools.clear();
|
|
1111
|
+
for (const tool of tools) this.tools.set(tool.name, tool);
|
|
1433
1112
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
clearTimeout(p.timeout);
|
|
1437
|
-
}
|
|
1438
|
-
this.pending.clear();
|
|
1113
|
+
get(name) {
|
|
1114
|
+
return this.tools.get(name);
|
|
1439
1115
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
// src/client/file-server.ts
|
|
1443
|
-
import { randomBytes } from "node:crypto";
|
|
1444
|
-
import { realpathSync } from "node:fs";
|
|
1445
|
-
import * as fsp from "node:fs/promises";
|
|
1446
|
-
import * as path from "node:path";
|
|
1447
|
-
var DEFAULT_MAX_READ_BYTES = 5 * 1024 * 1024;
|
|
1448
|
-
var DEFAULT_MAX_WRITE_BYTES = 5 * 1024 * 1024;
|
|
1449
|
-
var FsError = class extends Error {
|
|
1450
|
-
code;
|
|
1451
|
-
path;
|
|
1452
|
-
constructor(code, path4, message) {
|
|
1453
|
-
super(message);
|
|
1454
|
-
this.name = "FsError";
|
|
1455
|
-
this.code = code;
|
|
1456
|
-
this.path = path4;
|
|
1116
|
+
has(name) {
|
|
1117
|
+
return this.tools.has(name);
|
|
1457
1118
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
root;
|
|
1461
|
-
realRoot;
|
|
1462
|
-
timeoutMs;
|
|
1463
|
-
maxReadBytes;
|
|
1464
|
-
maxWriteBytes;
|
|
1465
|
-
constructor(opts) {
|
|
1466
|
-
this.root = path.resolve(opts.projectRoot);
|
|
1467
|
-
this.realRoot = safeRealpathSync(this.root);
|
|
1468
|
-
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
1469
|
-
this.maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
|
|
1470
|
-
this.maxWriteBytes = opts.maxWriteBytes ?? DEFAULT_MAX_WRITE_BYTES;
|
|
1119
|
+
list() {
|
|
1120
|
+
return Array.from(this.tools.values());
|
|
1471
1121
|
}
|
|
1472
|
-
/**
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1122
|
+
/** Build the ACP tools/list payload from registered tools. */
|
|
1123
|
+
buildToolList() {
|
|
1124
|
+
return {
|
|
1125
|
+
tools: Array.from(this.tools.values()).map(
|
|
1126
|
+
(t) => toACPToolDefinition(t, this.owner)
|
|
1127
|
+
)
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Execute a tool by name and return ACP-formatted result.
|
|
1132
|
+
* Returns null if the tool is not found.
|
|
1133
|
+
*/
|
|
1134
|
+
async execute(name, args, ctx, signal) {
|
|
1135
|
+
const tool = this.tools.get(name);
|
|
1136
|
+
if (!tool) return null;
|
|
1477
1137
|
try {
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1480
|
-
});
|
|
1481
|
-
if (stat2.size > this.maxReadBytes) {
|
|
1482
|
-
throw new FsError(
|
|
1483
|
-
"TOO_LARGE",
|
|
1484
|
-
safe,
|
|
1485
|
-
`file is ${stat2.size} bytes, max read is ${this.maxReadBytes} bytes`
|
|
1486
|
-
);
|
|
1487
|
-
}
|
|
1488
|
-
const content = await fsp.readFile(safe, {
|
|
1489
|
-
encoding: "utf8",
|
|
1490
|
-
signal: controller.signal
|
|
1138
|
+
const result = await tool.execute(args, ctx, {
|
|
1139
|
+
signal
|
|
1491
1140
|
});
|
|
1492
|
-
return
|
|
1141
|
+
return toACPToolResult(result);
|
|
1493
1142
|
} catch (err) {
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
|
|
1497
|
-
}
|
|
1498
|
-
throw mapFsError(err, safe);
|
|
1499
|
-
} finally {
|
|
1500
|
-
clearTimeout(timer);
|
|
1143
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1144
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
1501
1145
|
}
|
|
1502
1146
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1147
|
+
};
|
|
1148
|
+
function toACPToolDefinition(tool, _owner) {
|
|
1149
|
+
return {
|
|
1150
|
+
name: tool.name,
|
|
1151
|
+
description: tool.description,
|
|
1152
|
+
inputSchema: toACPInputSchema(tool.inputSchema),
|
|
1153
|
+
annotations: {
|
|
1154
|
+
title: tool.name,
|
|
1155
|
+
description: tool.usageHint ?? tool.description,
|
|
1156
|
+
priority: toolToPriority(tool),
|
|
1157
|
+
alwaysAccept: tool.permission === "auto"
|
|
1512
1158
|
}
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
} catch {
|
|
1533
|
-
}
|
|
1534
|
-
if (controller.signal.aborted) {
|
|
1535
|
-
throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
|
|
1536
|
-
}
|
|
1537
|
-
throw mapFsError(err, safe);
|
|
1538
|
-
} finally {
|
|
1539
|
-
clearTimeout(timer);
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
function toACPInputSchema(src) {
|
|
1162
|
+
if (!src || typeof src !== "object") {
|
|
1163
|
+
return {};
|
|
1164
|
+
}
|
|
1165
|
+
const s = src;
|
|
1166
|
+
const out = {};
|
|
1167
|
+
if (typeof s.type === "string") out.type = s.type;
|
|
1168
|
+
if (Array.isArray(s.enum)) out.enum = s.enum;
|
|
1169
|
+
if (typeof s.description === "string") out.description = s.description;
|
|
1170
|
+
if ("default" in s) out.default = s.default;
|
|
1171
|
+
if (typeof s.minimum === "number") out.minimum = s.minimum;
|
|
1172
|
+
if (typeof s.maximum === "number") out.maximum = s.maximum;
|
|
1173
|
+
if (s.items) out.items = toACPInputSchema(s.items);
|
|
1174
|
+
if (s.properties && typeof s.properties === "object") {
|
|
1175
|
+
const props = {};
|
|
1176
|
+
for (const [k, v] of Object.entries(s.properties)) {
|
|
1177
|
+
props[k] = toACPInputSchema(v);
|
|
1540
1178
|
}
|
|
1179
|
+
out.properties = props;
|
|
1180
|
+
if (Array.isArray(s.required)) out.required = s.required;
|
|
1181
|
+
}
|
|
1182
|
+
return out;
|
|
1183
|
+
}
|
|
1184
|
+
function toACPToolResult(result) {
|
|
1185
|
+
const blocks = [];
|
|
1186
|
+
if (result === void 0 || result === null) {
|
|
1187
|
+
return { content: [{ type: "text", text: "ok" }] };
|
|
1188
|
+
}
|
|
1189
|
+
if (typeof result === "string") {
|
|
1190
|
+
blocks.push({ type: "text", text: result });
|
|
1191
|
+
} else if (typeof result === "object") {
|
|
1192
|
+
blocks.push({ type: "text", text: JSON.stringify(result, null, 2) });
|
|
1193
|
+
} else {
|
|
1194
|
+
blocks.push({ type: "text", text: String(result) });
|
|
1195
|
+
}
|
|
1196
|
+
return { content: blocks };
|
|
1197
|
+
}
|
|
1198
|
+
function toolToPriority(tool) {
|
|
1199
|
+
if (tool.riskTier === "destructive") return "high";
|
|
1200
|
+
if (tool.riskTier === "standard" || tool.permission === "confirm") return "medium";
|
|
1201
|
+
return "low";
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// src/agent/wrongstack-acp-agent.ts
|
|
1205
|
+
import { createServer } from "node:http";
|
|
1206
|
+
import { fileURLToPath } from "node:url";
|
|
1207
|
+
import { writeErr as writeErr2 } from "@wrongstack/core/utils";
|
|
1208
|
+
var WrongStackACPServer = class {
|
|
1209
|
+
transport;
|
|
1210
|
+
handler;
|
|
1211
|
+
options;
|
|
1212
|
+
/** HTTP server when transport mode is HTTP. */
|
|
1213
|
+
httpServer = null;
|
|
1214
|
+
running = false;
|
|
1215
|
+
constructor(opts = {}) {
|
|
1216
|
+
this.options = opts;
|
|
1217
|
+
this.transport = new StdioTransport();
|
|
1218
|
+
const runTurn = opts.runTurn ?? defaultEchoRunTurn;
|
|
1219
|
+
this.handler = new ACPProtocolHandler({
|
|
1220
|
+
transport: this.transport,
|
|
1221
|
+
defaultCwd: opts.defaultCwd ?? process.cwd(),
|
|
1222
|
+
runTurn,
|
|
1223
|
+
agentName: opts.agentName,
|
|
1224
|
+
...opts.replayFor ? { replayFor: opts.replayFor } : {},
|
|
1225
|
+
...opts.seedFor ? { seedFor: opts.seedFor } : {},
|
|
1226
|
+
...opts.disposeFor ? { disposeFor: opts.disposeFor } : {},
|
|
1227
|
+
...opts.store ? { store: opts.store } : {}
|
|
1228
|
+
});
|
|
1541
1229
|
}
|
|
1542
1230
|
/**
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1546
|
-
*
|
|
1547
|
-
* For files that don't exist yet (e.g. a write to a new file), the
|
|
1548
|
-
* nearest existing ancestor directory is realpath-checked instead.
|
|
1231
|
+
* Start the server. Mode depends on `options.transport`:
|
|
1232
|
+
* - 'stdio' (default): reads JSON-RPC from stdin, writes to stdout.
|
|
1233
|
+
* - number: listens as HTTP on the given port.
|
|
1549
1234
|
*/
|
|
1550
|
-
async
|
|
1551
|
-
|
|
1552
|
-
|
|
1235
|
+
async start() {
|
|
1236
|
+
const transportMode = this.options.transport;
|
|
1237
|
+
if (typeof transportMode === "number") {
|
|
1238
|
+
await this.startHttp(transportMode);
|
|
1239
|
+
} else {
|
|
1240
|
+
await this.startStdio();
|
|
1553
1241
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1242
|
+
}
|
|
1243
|
+
async startStdio() {
|
|
1244
|
+
if (this.options.legacyStartupMarker) {
|
|
1245
|
+
this.transport.sendStartupMarker();
|
|
1556
1246
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1247
|
+
this.running = true;
|
|
1248
|
+
try {
|
|
1249
|
+
while (this.running) {
|
|
1250
|
+
const msg = await this.transport.read();
|
|
1251
|
+
if (!msg) break;
|
|
1252
|
+
const terminal = await this.handler.handleMessage(msg);
|
|
1253
|
+
if (terminal) break;
|
|
1254
|
+
}
|
|
1255
|
+
} finally {
|
|
1256
|
+
this.handler.close();
|
|
1257
|
+
this.transport.close();
|
|
1561
1258
|
}
|
|
1562
|
-
await this.assertRealInside(resolved);
|
|
1563
|
-
return resolved;
|
|
1564
1259
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
if (parent === probe) return;
|
|
1581
|
-
probe = parent;
|
|
1582
|
-
continue;
|
|
1260
|
+
async startHttp(port) {
|
|
1261
|
+
const host = this.options.host ?? "127.0.0.1";
|
|
1262
|
+
const handler = this.handler;
|
|
1263
|
+
const authToken = this.options.authToken;
|
|
1264
|
+
let httpChain = Promise.resolve();
|
|
1265
|
+
this.httpServer = createServer(async (req, res) => {
|
|
1266
|
+
if (authToken) {
|
|
1267
|
+
const url = new URL(requestPath(req.url), `http://${host}:${port}`);
|
|
1268
|
+
const queryToken = url.searchParams.get("token");
|
|
1269
|
+
const bearerToken = headerValue(req.headers.authorization)?.replace(/^Bearer\s+/i, "");
|
|
1270
|
+
const supplied = queryToken ?? bearerToken ?? "";
|
|
1271
|
+
if (supplied !== authToken) {
|
|
1272
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1273
|
+
res.end(JSON.stringify({ error: { code: -32001, message: "Unauthorized" } }));
|
|
1274
|
+
return;
|
|
1583
1275
|
}
|
|
1584
|
-
throw mapFsError(err, resolvedPath);
|
|
1585
1276
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1277
|
+
const selfOrigin = `http://${host}:${port}`;
|
|
1278
|
+
const reqOrigin = headerValue(req.headers.origin);
|
|
1279
|
+
if (reqOrigin && reqOrigin !== selfOrigin) {
|
|
1280
|
+
res.writeHead(403);
|
|
1281
|
+
res.end(JSON.stringify({ error: "cross-origin request forbidden" }));
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (reqOrigin) res.setHeader("Access-Control-Allow-Origin", reqOrigin);
|
|
1285
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
1286
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Authorization");
|
|
1287
|
+
if (req.method === "OPTIONS") {
|
|
1288
|
+
res.writeHead(204);
|
|
1289
|
+
res.end();
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
if (req.method !== "POST") {
|
|
1293
|
+
res.writeHead(405);
|
|
1294
|
+
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
const MAX_HTTP_BODY = 10 * 1024 * 1024;
|
|
1298
|
+
let body = "";
|
|
1299
|
+
let bodyBytes = 0;
|
|
1300
|
+
let tooLarge = false;
|
|
1301
|
+
for await (const chunk of req) {
|
|
1302
|
+
bodyBytes += chunk.length;
|
|
1303
|
+
if (bodyBytes > MAX_HTTP_BODY) {
|
|
1304
|
+
tooLarge = true;
|
|
1305
|
+
break;
|
|
1306
|
+
}
|
|
1307
|
+
body += chunk;
|
|
1308
|
+
}
|
|
1309
|
+
if (tooLarge) {
|
|
1310
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
1311
|
+
res.end(JSON.stringify({ error: { code: -32700, message: "Request body too large" } }));
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
let msg;
|
|
1315
|
+
try {
|
|
1316
|
+
msg = JSON.parse(body);
|
|
1317
|
+
} catch {
|
|
1318
|
+
res.writeHead(400);
|
|
1319
|
+
res.end(JSON.stringify({ error: { code: -32700, message: "Parse error" } }));
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const isNotification = typeof msg === "object" && msg !== null && msg.id === void 0 && typeof msg.method === "string";
|
|
1323
|
+
if (isNotification) {
|
|
1324
|
+
try {
|
|
1325
|
+
await handler.handleMessage(msg);
|
|
1326
|
+
} catch {
|
|
1327
|
+
}
|
|
1328
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1329
|
+
res.end(JSON.stringify({ notifications: [] }));
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const requestPromise = httpChain.then(async () => {
|
|
1333
|
+
const notifications = [];
|
|
1334
|
+
let response = null;
|
|
1335
|
+
const originalSend = this.transport.send.bind(this.transport);
|
|
1336
|
+
this.transport.send = async (m) => {
|
|
1337
|
+
if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
|
|
1338
|
+
response = m;
|
|
1339
|
+
} else if (m.method === "session/update") {
|
|
1340
|
+
notifications.push(m.params);
|
|
1341
|
+
} else {
|
|
1342
|
+
notifications.push(m);
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
try {
|
|
1346
|
+
await handler.handleMessage(msg);
|
|
1347
|
+
} finally {
|
|
1348
|
+
this.transport.send = originalSend;
|
|
1349
|
+
}
|
|
1350
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1351
|
+
const responseBody = response !== null ? { ...response, notifications } : { notifications };
|
|
1352
|
+
res.end(JSON.stringify(responseBody));
|
|
1353
|
+
});
|
|
1354
|
+
httpChain = requestPromise.catch(() => void 0);
|
|
1355
|
+
try {
|
|
1356
|
+
await requestPromise;
|
|
1357
|
+
} catch {
|
|
1358
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1359
|
+
res.end(JSON.stringify({ error: { code: -32603, message: "Internal error" } }));
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
return new Promise((resolve3) => {
|
|
1363
|
+
this.httpServer.listen(port, host, () => {
|
|
1364
|
+
writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${port}
|
|
1365
|
+
`);
|
|
1366
|
+
this.running = true;
|
|
1367
|
+
resolve3();
|
|
1368
|
+
});
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
/** Stop the server. */
|
|
1372
|
+
stop() {
|
|
1373
|
+
this.running = false;
|
|
1374
|
+
this.handler.close();
|
|
1375
|
+
this.transport.close();
|
|
1376
|
+
if (this.httpServer) {
|
|
1377
|
+
this.httpServer.close();
|
|
1378
|
+
this.httpServer = null;
|
|
1592
1379
|
}
|
|
1593
1380
|
}
|
|
1594
1381
|
};
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
}
|
|
1601
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1602
|
-
return new FsError("INVALID_PATH", p, msg);
|
|
1382
|
+
var defaultEchoRunTurn = async (_input, _emit) => {
|
|
1383
|
+
return { stopReason: "end_turn" };
|
|
1384
|
+
};
|
|
1385
|
+
function headerValue(value) {
|
|
1386
|
+
return Array.isArray(value) ? value[0] : value;
|
|
1603
1387
|
}
|
|
1604
|
-
function
|
|
1605
|
-
|
|
1606
|
-
return realpathSync(p);
|
|
1607
|
-
} catch {
|
|
1608
|
-
return p;
|
|
1609
|
-
}
|
|
1388
|
+
function requestPath(value) {
|
|
1389
|
+
return value ?? "/";
|
|
1610
1390
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
const ranked = [...options].sort((a, b) => {
|
|
1615
|
-
const score = (k) => {
|
|
1616
|
-
if (k === "allow_once") return 0;
|
|
1617
|
-
if (k === "allow_always") return 1;
|
|
1618
|
-
if (k === "reject_once") return 2;
|
|
1619
|
-
return 3;
|
|
1620
|
-
};
|
|
1621
|
-
return score(a.kind) - score(b.kind);
|
|
1622
|
-
});
|
|
1623
|
-
const chosen = ranked[0];
|
|
1624
|
-
if (!chosen || chosen.kind === "reject_once" || chosen.kind === "reject_always") {
|
|
1625
|
-
return { outcome: "cancelled" };
|
|
1626
|
-
}
|
|
1627
|
-
return { outcome: "selected", optionId: chosen.optionId };
|
|
1391
|
+
async function main() {
|
|
1392
|
+
const server = new WrongStackACPServer();
|
|
1393
|
+
await server.start();
|
|
1628
1394
|
}
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1395
|
+
var isEntrypoint = process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === process.argv[1];
|
|
1396
|
+
if (isEntrypoint) {
|
|
1397
|
+
main().catch((err) => {
|
|
1398
|
+
writeErr2(`[wstack-acp fatal] ${err}
|
|
1399
|
+
`);
|
|
1400
|
+
process.exit(1);
|
|
1401
|
+
});
|
|
1634
1402
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1403
|
+
|
|
1404
|
+
// src/client/file-server.ts
|
|
1405
|
+
import { randomBytes } from "node:crypto";
|
|
1406
|
+
import { realpathSync } from "node:fs";
|
|
1407
|
+
import * as fsp from "node:fs/promises";
|
|
1408
|
+
import * as path from "node:path";
|
|
1409
|
+
var DEFAULT_FILE_OPERATIONS = {
|
|
1410
|
+
stat: fsp.stat,
|
|
1411
|
+
readFile: fsp.readFile,
|
|
1412
|
+
writeFile: fsp.writeFile,
|
|
1413
|
+
realpath: fsp.realpath,
|
|
1414
|
+
rename: fsp.rename,
|
|
1415
|
+
unlink: fsp.unlink
|
|
1639
1416
|
};
|
|
1640
|
-
var
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1417
|
+
var DEFAULT_MAX_READ_BYTES = 5 * 1024 * 1024;
|
|
1418
|
+
var DEFAULT_MAX_WRITE_BYTES = 5 * 1024 * 1024;
|
|
1419
|
+
var FsError = class extends Error {
|
|
1420
|
+
code;
|
|
1421
|
+
path;
|
|
1422
|
+
constructor(code, path4, message) {
|
|
1423
|
+
super(message);
|
|
1424
|
+
this.name = "FsError";
|
|
1425
|
+
this.code = code;
|
|
1426
|
+
this.path = path4;
|
|
1645
1427
|
}
|
|
1646
|
-
return pickReject(req.options);
|
|
1647
1428
|
};
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
// src/client/terminal-server.ts
|
|
1657
|
-
import { spawn } from "node:child_process";
|
|
1658
|
-
import { realpathSync as realpathSync2 } from "node:fs";
|
|
1659
|
-
import * as path2 from "node:path";
|
|
1660
|
-
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
1661
|
-
var TerminalServer = class {
|
|
1662
|
-
terminals = /* @__PURE__ */ new Map();
|
|
1663
|
-
projectRoot;
|
|
1664
|
-
commandTimeoutMs;
|
|
1665
|
-
outputByteLimit;
|
|
1666
|
-
maxOutputByteLimit;
|
|
1667
|
-
nextId = 1;
|
|
1429
|
+
var FileServer = class {
|
|
1430
|
+
root;
|
|
1431
|
+
realRoot;
|
|
1432
|
+
timeoutMs;
|
|
1433
|
+
maxReadBytes;
|
|
1434
|
+
maxWriteBytes;
|
|
1435
|
+
operations;
|
|
1668
1436
|
constructor(opts) {
|
|
1669
|
-
this.
|
|
1670
|
-
this.
|
|
1671
|
-
this.
|
|
1672
|
-
this.
|
|
1673
|
-
|
|
1674
|
-
|
|
1437
|
+
this.root = path.resolve(opts.projectRoot);
|
|
1438
|
+
this.realRoot = safeRealpathSync(this.root);
|
|
1439
|
+
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
1440
|
+
this.maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
|
|
1441
|
+
this.maxWriteBytes = opts.maxWriteBytes ?? DEFAULT_MAX_WRITE_BYTES;
|
|
1442
|
+
this.operations = opts.operations ?? DEFAULT_FILE_OPERATIONS;
|
|
1443
|
+
}
|
|
1444
|
+
/** Read a text file. Returns the content as a string. */
|
|
1445
|
+
async readTextFile(params) {
|
|
1446
|
+
const safe = await this.resolveInside(params.path);
|
|
1447
|
+
const controller = new AbortController();
|
|
1448
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
1449
|
+
try {
|
|
1450
|
+
const stat2 = await this.operations.stat(safe).catch((err) => {
|
|
1451
|
+
throw mapFsError(err, safe);
|
|
1452
|
+
});
|
|
1453
|
+
if (stat2.size > this.maxReadBytes) {
|
|
1454
|
+
throw new FsError(
|
|
1455
|
+
"TOO_LARGE",
|
|
1456
|
+
safe,
|
|
1457
|
+
`file is ${stat2.size} bytes, max read is ${this.maxReadBytes} bytes`
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
const content = await this.operations.readFile(safe, {
|
|
1461
|
+
encoding: "utf8",
|
|
1462
|
+
signal: controller.signal
|
|
1463
|
+
});
|
|
1464
|
+
return { content };
|
|
1465
|
+
} catch (err) {
|
|
1466
|
+
if (err instanceof FsError) throw err;
|
|
1467
|
+
if (controller.signal.aborted) {
|
|
1468
|
+
throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
|
|
1469
|
+
}
|
|
1470
|
+
throw mapFsError(err, safe);
|
|
1471
|
+
} finally {
|
|
1472
|
+
clearTimeout(timer);
|
|
1675
1473
|
}
|
|
1676
1474
|
}
|
|
1677
|
-
/**
|
|
1678
|
-
|
|
1679
|
-
const
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1475
|
+
/** Write a text file. Atomic via write-then-rename. */
|
|
1476
|
+
async writeTextFile(params) {
|
|
1477
|
+
const byteLength = Buffer.byteLength(params.content, "utf8");
|
|
1478
|
+
if (byteLength > this.maxWriteBytes) {
|
|
1479
|
+
throw new FsError(
|
|
1480
|
+
"TOO_LARGE",
|
|
1481
|
+
params.path,
|
|
1482
|
+
`content is ${byteLength} bytes, max write is ${this.maxWriteBytes} bytes`
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
const safe = await this.resolveInside(params.path);
|
|
1486
|
+
const controller = new AbortController();
|
|
1487
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
1488
|
+
const tmp = `${safe}.${randomBytes(6).toString("hex")}.tmp`;
|
|
1489
|
+
try {
|
|
1490
|
+
await this.operations.writeFile(tmp, params.content, {
|
|
1491
|
+
encoding: "utf8",
|
|
1492
|
+
signal: controller.signal
|
|
1493
|
+
});
|
|
1494
|
+
await this.assertRealInside(tmp);
|
|
1495
|
+
await this.assertRealInside(path.dirname(safe));
|
|
1496
|
+
await this.operations.rename(tmp, safe);
|
|
1497
|
+
} catch (err) {
|
|
1498
|
+
if (err instanceof FsError) {
|
|
1499
|
+
await this.operations.unlink(tmp).catch(() => void 0);
|
|
1500
|
+
throw err;
|
|
1501
|
+
}
|
|
1502
|
+
try {
|
|
1503
|
+
await this.operations.unlink(tmp);
|
|
1504
|
+
} catch {
|
|
1505
|
+
}
|
|
1506
|
+
if (controller.signal.aborted) {
|
|
1507
|
+
throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
|
|
1508
|
+
}
|
|
1509
|
+
throw mapFsError(err, safe);
|
|
1510
|
+
} finally {
|
|
1511
|
+
clearTimeout(timer);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Resolve a path and verify it is inside the project root by realpath.
|
|
1516
|
+
* Rejects with `FsError` if the textual path, the resolved path, or the
|
|
1517
|
+
* real (symlink-resolved) path escapes the project root.
|
|
1518
|
+
*
|
|
1519
|
+
* For files that don't exist yet (e.g. a write to a new file), the
|
|
1520
|
+
* nearest existing ancestor directory is realpath-checked instead.
|
|
1521
|
+
*/
|
|
1522
|
+
async resolveInside(p) {
|
|
1523
|
+
if (typeof p !== "string" || p.length === 0) {
|
|
1524
|
+
throw new FsError("INVALID_PATH", p, "path is empty or not a string");
|
|
1525
|
+
}
|
|
1526
|
+
if (!path.isAbsolute(p)) {
|
|
1527
|
+
throw new FsError("INVALID_PATH", p, "path must be absolute (ACP requirement)");
|
|
1528
|
+
}
|
|
1529
|
+
const resolved = path.resolve(p);
|
|
1530
|
+
const rootWithSep = this.root.endsWith(path.sep) ? this.root : this.root + path.sep;
|
|
1531
|
+
if (resolved !== this.root && !resolved.startsWith(rootWithSep)) {
|
|
1532
|
+
throw new FsError("OUTSIDE_ROOT", resolved, "path is outside the project root");
|
|
1533
|
+
}
|
|
1534
|
+
await this.assertRealInside(resolved);
|
|
1535
|
+
return resolved;
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Resolve `resolvedPath` through `fs.realpath` and verify the result is
|
|
1539
|
+
* inside `realRoot`. For non-existent paths (new files), walk up to the
|
|
1540
|
+
* nearest existing ancestor and check that instead.
|
|
1541
|
+
*/
|
|
1542
|
+
async assertRealInside(resolvedPath) {
|
|
1543
|
+
let probe = resolvedPath;
|
|
1544
|
+
for (; ; ) {
|
|
1545
|
+
let real;
|
|
1546
|
+
try {
|
|
1547
|
+
real = await this.operations.realpath(probe);
|
|
1548
|
+
} catch (err) {
|
|
1549
|
+
const code = err.code;
|
|
1550
|
+
if (code === "ENOENT") {
|
|
1551
|
+
const parent = path.dirname(probe);
|
|
1552
|
+
if (parent === probe) {
|
|
1553
|
+
throw new FsError("ENOENT", resolvedPath, `no existing ancestor: ${resolvedPath}`);
|
|
1554
|
+
}
|
|
1555
|
+
probe = parent;
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
throw mapFsError(err, resolvedPath);
|
|
1559
|
+
}
|
|
1560
|
+
if (real === this.realRoot || real.startsWith(this.realRoot + path.sep)) return;
|
|
1561
|
+
throw new FsError(
|
|
1562
|
+
"OUTSIDE_ROOT",
|
|
1563
|
+
resolvedPath,
|
|
1564
|
+
"path resolves through a symlink outside the project root"
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
};
|
|
1569
|
+
function mapFsError(err, p) {
|
|
1570
|
+
const code = err?.code;
|
|
1571
|
+
if (code === "ENOENT") return new FsError("ENOENT", p, `no such file: ${p}`);
|
|
1572
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
1573
|
+
return new FsError("EACCES", p, `permission denied: ${p}`);
|
|
1574
|
+
}
|
|
1575
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1576
|
+
return new FsError("INVALID_PATH", p, msg);
|
|
1577
|
+
}
|
|
1578
|
+
function safeRealpathSync(p) {
|
|
1579
|
+
try {
|
|
1580
|
+
return realpathSync(p);
|
|
1581
|
+
} catch {
|
|
1582
|
+
return p;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/client/permission.ts
|
|
1587
|
+
function pickAllow(options) {
|
|
1588
|
+
const ranked = [...options].sort((a, b) => {
|
|
1589
|
+
const score = (k) => {
|
|
1590
|
+
if (k === "allow_once") return 0;
|
|
1591
|
+
if (k === "allow_always") return 1;
|
|
1592
|
+
if (k === "reject_once") return 2;
|
|
1593
|
+
return 3;
|
|
1594
|
+
};
|
|
1595
|
+
return score(a.kind) - score(b.kind);
|
|
1596
|
+
});
|
|
1597
|
+
const chosen = ranked[0];
|
|
1598
|
+
if (!chosen || chosen.kind === "reject_once" || chosen.kind === "reject_always") {
|
|
1599
|
+
return { outcome: "cancelled" };
|
|
1600
|
+
}
|
|
1601
|
+
return { outcome: "selected", optionId: chosen.optionId };
|
|
1602
|
+
}
|
|
1603
|
+
function pickReject(options) {
|
|
1604
|
+
const reject = options.find(
|
|
1605
|
+
(o) => o.kind === "reject_once" || o.kind === "reject_always"
|
|
1606
|
+
);
|
|
1607
|
+
return reject ? { outcome: "selected", optionId: reject.optionId } : { outcome: "cancelled" };
|
|
1608
|
+
}
|
|
1609
|
+
var READ_ONLY_KINDS = /* @__PURE__ */ new Set(["read", "search", "fetch", "think"]);
|
|
1610
|
+
var defaultPermissionPolicy = async (req) => {
|
|
1611
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1612
|
+
return pickAllow(req.options);
|
|
1613
|
+
};
|
|
1614
|
+
var readOnlyPermissionPolicy = async (req) => {
|
|
1615
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1616
|
+
const kind = req.toolCall.kind;
|
|
1617
|
+
if (kind && READ_ONLY_KINDS.has(kind)) {
|
|
1618
|
+
return pickAllow(req.options);
|
|
1619
|
+
}
|
|
1620
|
+
return pickReject(req.options);
|
|
1621
|
+
};
|
|
1622
|
+
function makePermissionPolicy(decide) {
|
|
1623
|
+
return async (req) => {
|
|
1624
|
+
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
1625
|
+
const allow = await decide(req);
|
|
1626
|
+
return allow ? pickAllow(req.options) : pickReject(req.options);
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/client/terminal-server.ts
|
|
1631
|
+
import { spawn } from "node:child_process";
|
|
1632
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
1633
|
+
import * as path2 from "node:path";
|
|
1634
|
+
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
1635
|
+
var EMPTY_BUFFER = Buffer.alloc(0);
|
|
1636
|
+
var TerminalServer = class {
|
|
1637
|
+
terminals = /* @__PURE__ */ new Map();
|
|
1638
|
+
projectRoot;
|
|
1639
|
+
commandTimeoutMs;
|
|
1640
|
+
outputByteLimit;
|
|
1641
|
+
maxOutputByteLimit;
|
|
1642
|
+
maxTerminals;
|
|
1643
|
+
abortSignal;
|
|
1644
|
+
abortHandler = () => this.releaseAll();
|
|
1645
|
+
nextId = 1;
|
|
1646
|
+
constructor(opts) {
|
|
1647
|
+
this.projectRoot = path2.resolve(opts.projectRoot);
|
|
1648
|
+
this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
|
|
1649
|
+
this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
|
|
1650
|
+
this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
|
|
1651
|
+
this.maxTerminals = this.clampFiniteInt(opts.maxTerminals, 32);
|
|
1652
|
+
this.abortSignal = opts.signal;
|
|
1653
|
+
if (opts.signal) {
|
|
1654
|
+
opts.signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
/** Spawn a new terminal. Returns the agent-facing id. */
|
|
1658
|
+
create(params) {
|
|
1659
|
+
if (this.terminals.size >= this.maxTerminals) {
|
|
1660
|
+
throw new Error(
|
|
1661
|
+
`terminal limit reached (${this.maxTerminals}); release an existing terminal before creating another`
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
const id = `term_${this.nextId++}`;
|
|
1665
|
+
const cwd = this.resolveCwd(params.cwd);
|
|
1666
|
+
const perCallByteLimit = Math.min(
|
|
1667
|
+
Math.max(1, this.clampFiniteInt(params.outputByteLimit, this.outputByteLimit)),
|
|
1668
|
+
this.maxOutputByteLimit
|
|
1669
|
+
);
|
|
1670
|
+
const proc = spawn(params.command, params.args ?? [], {
|
|
1671
|
+
cwd,
|
|
1672
|
+
env: this.buildEnv(params.env),
|
|
1673
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1674
|
+
windowsHide: true
|
|
1675
|
+
// shell: false on purpose. The terminal server is invoked with
|
|
1676
|
+
// the agent's explicit argv; turning on shell-mode would make
|
|
1677
|
+
// the command a single shell-parsed string, which breaks
|
|
1678
|
+
// Windows cmd quoting for the common case of running node with
|
|
1679
|
+
// `-e "<script>"`. If a future feature needs shell features
|
|
1691
1680
|
// (pipes, redirects), it should be opt-in per-call, not the
|
|
1692
1681
|
// default.
|
|
1693
1682
|
});
|
|
@@ -1696,7 +1685,8 @@ var TerminalServer = class {
|
|
|
1696
1685
|
cwd,
|
|
1697
1686
|
command: params.command,
|
|
1698
1687
|
args: params.args ?? [],
|
|
1699
|
-
|
|
1688
|
+
outputChunks: [],
|
|
1689
|
+
outputHead: 0,
|
|
1700
1690
|
retainedBytes: 0,
|
|
1701
1691
|
truncated: false,
|
|
1702
1692
|
exitStatus: void 0,
|
|
@@ -1721,31 +1711,44 @@ var TerminalServer = class {
|
|
|
1721
1711
|
}
|
|
1722
1712
|
const exitStatus = { exitCode: 127, signal: null };
|
|
1723
1713
|
state.exitStatus = exitStatus;
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1714
|
+
let errorOutput = Buffer.from(`[spawn error] ${err.message}
|
|
1715
|
+
`, "utf8");
|
|
1716
|
+
if (errorOutput.length > perCallByteLimit) {
|
|
1717
|
+
let start = errorOutput.length - perCallByteLimit;
|
|
1718
|
+
while (start < errorOutput.length && (errorOutput[start] & 192) === 128) start++;
|
|
1719
|
+
errorOutput = errorOutput.subarray(start);
|
|
1720
|
+
state.truncated = true;
|
|
1721
|
+
}
|
|
1722
|
+
state.outputChunks.push(errorOutput);
|
|
1723
|
+
state.retainedBytes = errorOutput.length;
|
|
1727
1724
|
resolve3(exitStatus);
|
|
1728
1725
|
});
|
|
1729
1726
|
})
|
|
1730
1727
|
};
|
|
1731
|
-
const perCallByteLimit = Math.min(
|
|
1732
|
-
Math.max(1, this.clampFiniteInt(params.outputByteLimit, this.outputByteLimit)),
|
|
1733
|
-
this.maxOutputByteLimit
|
|
1734
|
-
);
|
|
1735
1728
|
proc.stdout?.setEncoding("utf8");
|
|
1736
1729
|
proc.stderr?.setEncoding("utf8");
|
|
1737
1730
|
const onData = (chunk) => {
|
|
1738
|
-
|
|
1739
|
-
state.
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
const
|
|
1744
|
-
|
|
1745
|
-
|
|
1731
|
+
const outputChunk = Buffer.from(chunk, "utf8");
|
|
1732
|
+
state.outputChunks.push(outputChunk);
|
|
1733
|
+
state.retainedBytes += outputChunk.length;
|
|
1734
|
+
if (state.retainedBytes > perCallByteLimit) state.truncated = true;
|
|
1735
|
+
while (state.retainedBytes > perCallByteLimit && state.outputHead < state.outputChunks.length) {
|
|
1736
|
+
const first = state.outputChunks[state.outputHead];
|
|
1737
|
+
const overflow = state.retainedBytes - perCallByteLimit;
|
|
1738
|
+
if (first.length <= overflow) {
|
|
1739
|
+
state.outputChunks[state.outputHead] = EMPTY_BUFFER;
|
|
1740
|
+
state.outputHead++;
|
|
1741
|
+
state.retainedBytes -= first.length;
|
|
1742
|
+
continue;
|
|
1746
1743
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1744
|
+
let start = overflow;
|
|
1745
|
+
while (start < first.length && (first[start] & 192) === 128) start++;
|
|
1746
|
+
state.outputChunks[state.outputHead] = first.subarray(start);
|
|
1747
|
+
state.retainedBytes -= start;
|
|
1748
|
+
}
|
|
1749
|
+
if (state.outputHead >= 256 && state.outputHead * 2 >= state.outputChunks.length) {
|
|
1750
|
+
state.outputChunks = state.outputChunks.slice(state.outputHead);
|
|
1751
|
+
state.outputHead = 0;
|
|
1749
1752
|
}
|
|
1750
1753
|
};
|
|
1751
1754
|
proc.stdout?.on("data", onData);
|
|
@@ -1764,7 +1767,10 @@ var TerminalServer = class {
|
|
|
1764
1767
|
const state = this.terminals.get(terminalId);
|
|
1765
1768
|
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
1766
1769
|
return {
|
|
1767
|
-
output:
|
|
1770
|
+
output: Buffer.concat(
|
|
1771
|
+
state.outputChunks.slice(state.outputHead),
|
|
1772
|
+
state.retainedBytes
|
|
1773
|
+
).toString("utf8"),
|
|
1768
1774
|
truncated: state.truncated,
|
|
1769
1775
|
...state.exitStatus ? { exitStatus: state.exitStatus } : {}
|
|
1770
1776
|
};
|
|
@@ -1800,6 +1806,7 @@ var TerminalServer = class {
|
|
|
1800
1806
|
}
|
|
1801
1807
|
/** Kill all active terminals. Used on session close. */
|
|
1802
1808
|
releaseAll() {
|
|
1809
|
+
this.abortSignal?.removeEventListener("abort", this.abortHandler);
|
|
1803
1810
|
for (const id of [...this.terminals.keys()]) {
|
|
1804
1811
|
this.release(id);
|
|
1805
1812
|
}
|
|
@@ -1863,44 +1870,251 @@ var DENIED_AGENT_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
|
1863
1870
|
"RUBYLIB"
|
|
1864
1871
|
]);
|
|
1865
1872
|
|
|
1866
|
-
// src/client/
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1873
|
+
// src/client/trust-boundary-permission.ts
|
|
1874
|
+
function pickOption(options, allowed) {
|
|
1875
|
+
const kinds = allowed ? ["allow_once", "allow_always"] : ["reject_once", "reject_always"];
|
|
1876
|
+
for (const kind of kinds) {
|
|
1877
|
+
const option = options.find((candidate) => candidate.kind === kind);
|
|
1878
|
+
if (option) return { outcome: "selected", optionId: option.optionId };
|
|
1879
|
+
}
|
|
1880
|
+
return { outcome: "cancelled" };
|
|
1881
|
+
}
|
|
1882
|
+
function riskFor(kind) {
|
|
1883
|
+
if (kind === "read" || kind === "search" || kind === "fetch" || kind === "think") return "low";
|
|
1884
|
+
if (kind === "edit" || kind === "move") return "elevated";
|
|
1885
|
+
if (kind === "delete" || kind === "execute") return "high";
|
|
1886
|
+
return "elevated";
|
|
1887
|
+
}
|
|
1888
|
+
function capabilityFor(request) {
|
|
1889
|
+
const raw = request.toolCall.rawInput;
|
|
1890
|
+
if (typeof raw?.path === "string") {
|
|
1891
|
+
return request.toolCall.kind === "read" || request.toolCall.kind === "search" ? "filesystem.read" : "filesystem.write";
|
|
1892
|
+
}
|
|
1893
|
+
if (typeof raw?.command === "string" || request.toolCall.kind === "execute")
|
|
1894
|
+
return "process.spawn";
|
|
1895
|
+
if (request.toolCall.kind === "fetch") return "network.fetch";
|
|
1896
|
+
return `tool.${request.toolCall.kind ?? "unknown"}`;
|
|
1897
|
+
}
|
|
1898
|
+
function subjectFor(request) {
|
|
1899
|
+
const raw = request.toolCall.rawInput;
|
|
1900
|
+
const title = request.toolCall.title ?? `ACP tool call ${String(request.toolCall.toolCallId)}`;
|
|
1901
|
+
if (typeof raw?.path === "string") {
|
|
1902
|
+
return { kind: "path", id: raw.path, attributes: { toolKind: request.toolCall.kind ?? null } };
|
|
1875
1903
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1904
|
+
if (typeof raw?.command === "string") {
|
|
1905
|
+
return {
|
|
1906
|
+
kind: "command",
|
|
1907
|
+
id: raw.command,
|
|
1908
|
+
attributes: { toolKind: request.toolCall.kind ?? null }
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
return {
|
|
1912
|
+
kind: "resource",
|
|
1913
|
+
id: title,
|
|
1914
|
+
attributes: { toolKind: request.toolCall.kind ?? null }
|
|
1915
|
+
};
|
|
1879
1916
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1917
|
+
function isAllowed(decision) {
|
|
1918
|
+
return decision.kind === "allow" || decision.kind === "scoped-token";
|
|
1919
|
+
}
|
|
1920
|
+
function toTrustBoundaryRequest(request, options) {
|
|
1921
|
+
const rawSessionId = request.toolCall.rawInput?.sessionId;
|
|
1922
|
+
const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : options.actor?.sessionId;
|
|
1923
|
+
return {
|
|
1924
|
+
version: 1,
|
|
1925
|
+
requestId: String(request.toolCall.toolCallId),
|
|
1926
|
+
actor: {
|
|
1927
|
+
...options.actor ?? { kind: "agent" },
|
|
1928
|
+
...sessionId ? { sessionId } : {}
|
|
1929
|
+
},
|
|
1930
|
+
surface: "acp",
|
|
1931
|
+
capability: capabilityFor(request),
|
|
1932
|
+
subject: subjectFor(request),
|
|
1933
|
+
risk: riskFor(request.toolCall.kind),
|
|
1934
|
+
scope: {
|
|
1935
|
+
...options.scope ?? {},
|
|
1936
|
+
...sessionId ? { sessionId } : {}
|
|
1937
|
+
},
|
|
1938
|
+
...options.authContext ? { authContext: options.authContext } : {},
|
|
1939
|
+
metadata: {
|
|
1940
|
+
...request.toolCall.title ? { title: request.toolCall.title } : {},
|
|
1941
|
+
toolKind: request.toolCall.kind ?? null
|
|
1942
|
+
}
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
function makeTrustBoundaryPermissionPolicy(options) {
|
|
1946
|
+
return async (request) => {
|
|
1947
|
+
if (request.signal.aborted) return { outcome: "cancelled" };
|
|
1948
|
+
const decision = await options.boundary.evaluate(toTrustBoundaryRequest(request, options));
|
|
1949
|
+
if (request.signal.aborted) return { outcome: "cancelled" };
|
|
1950
|
+
return pickOption(request.options, isAllowed(decision));
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// src/client/websocket-transport.ts
|
|
1955
|
+
var WebSocketClientTransport = class {
|
|
1956
|
+
ws = null;
|
|
1957
|
+
handlers = /* @__PURE__ */ new Set();
|
|
1893
1958
|
closed = false;
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
/** Protocol version negotiated with the agent during initialize. */
|
|
1899
|
-
negotiatedVersion = ACP_PROTOCOL_VERSION;
|
|
1900
|
-
constructor(opts, transport) {
|
|
1959
|
+
opts;
|
|
1960
|
+
maxBufferedBytes;
|
|
1961
|
+
maxMessageChars;
|
|
1962
|
+
constructor(opts) {
|
|
1901
1963
|
this.opts = opts;
|
|
1902
|
-
this.
|
|
1903
|
-
this.
|
|
1964
|
+
this.maxBufferedBytes = finitePositiveLimit(opts.maxBufferedBytes, 32 * 1024 * 1024);
|
|
1965
|
+
this.maxMessageChars = finitePositiveLimit(opts.maxMessageChars, 20 * 1024 * 1024);
|
|
1966
|
+
}
|
|
1967
|
+
start() {
|
|
1968
|
+
const WS = globalThis.WebSocket;
|
|
1969
|
+
if (!WS) {
|
|
1970
|
+
return Promise.reject(
|
|
1971
|
+
new Error(
|
|
1972
|
+
"global WebSocket is not available \u2014 Node \u2265 22 is required for the remote ACP transport"
|
|
1973
|
+
)
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
1977
|
+
return new Promise((resolve3, reject) => {
|
|
1978
|
+
let settled = false;
|
|
1979
|
+
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
1980
|
+
this.ws = ws;
|
|
1981
|
+
const timer = setTimeout(() => {
|
|
1982
|
+
settled = true;
|
|
1983
|
+
try {
|
|
1984
|
+
ws.close();
|
|
1985
|
+
} catch {
|
|
1986
|
+
}
|
|
1987
|
+
reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
1988
|
+
}, timeoutMs);
|
|
1989
|
+
ws.addEventListener("open", () => {
|
|
1990
|
+
if (settled) return;
|
|
1991
|
+
settled = true;
|
|
1992
|
+
clearTimeout(timer);
|
|
1993
|
+
resolve3();
|
|
1994
|
+
});
|
|
1995
|
+
ws.addEventListener("error", (ev) => {
|
|
1996
|
+
if (settled) {
|
|
1997
|
+
this.closed = true;
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
settled = true;
|
|
2001
|
+
clearTimeout(timer);
|
|
2002
|
+
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
|
|
2003
|
+
reject(new Error(message));
|
|
2004
|
+
});
|
|
2005
|
+
ws.addEventListener("close", () => {
|
|
2006
|
+
this.closed = true;
|
|
2007
|
+
});
|
|
2008
|
+
ws.addEventListener("message", (ev) => {
|
|
2009
|
+
this.onData(ev.data);
|
|
2010
|
+
});
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
send(msg) {
|
|
2014
|
+
if (this.closed || !this.ws) {
|
|
2015
|
+
return Promise.reject(new Error("WebSocket transport is not open"));
|
|
2016
|
+
}
|
|
2017
|
+
try {
|
|
2018
|
+
const serialized = JSON.stringify(msg);
|
|
2019
|
+
const buffered = Number.isFinite(this.ws.bufferedAmount) ? this.ws.bufferedAmount : 0;
|
|
2020
|
+
if (buffered + Buffer.byteLength(serialized, "utf8") > this.maxBufferedBytes) {
|
|
2021
|
+
this.stop();
|
|
2022
|
+
return Promise.reject(new Error("WebSocket transport send buffer limit exceeded"));
|
|
2023
|
+
}
|
|
2024
|
+
this.ws.send(serialized);
|
|
2025
|
+
return Promise.resolve();
|
|
2026
|
+
} catch (err) {
|
|
2027
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
onMessage(handler) {
|
|
2031
|
+
this.handlers.add(handler);
|
|
2032
|
+
return () => this.handlers.delete(handler);
|
|
2033
|
+
}
|
|
2034
|
+
stop() {
|
|
2035
|
+
this.closed = true;
|
|
2036
|
+
if (this.ws) {
|
|
2037
|
+
try {
|
|
2038
|
+
this.ws.close();
|
|
2039
|
+
} catch {
|
|
2040
|
+
}
|
|
2041
|
+
this.ws = null;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
onData(data) {
|
|
2045
|
+
const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
2046
|
+
if (text.length > this.maxMessageChars) {
|
|
2047
|
+
this.stop();
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
if (!text.trim()) return;
|
|
2051
|
+
let msg;
|
|
2052
|
+
try {
|
|
2053
|
+
msg = JSON.parse(text);
|
|
2054
|
+
} catch {
|
|
2055
|
+
for (const line of text.split("\n")) {
|
|
2056
|
+
if (!line.trim()) continue;
|
|
2057
|
+
try {
|
|
2058
|
+
this.dispatch(JSON.parse(line));
|
|
2059
|
+
} catch {
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
this.dispatch(msg);
|
|
2065
|
+
}
|
|
2066
|
+
dispatch(msg) {
|
|
2067
|
+
for (const handler of [...this.handlers]) {
|
|
2068
|
+
try {
|
|
2069
|
+
handler(msg);
|
|
2070
|
+
} catch {
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
function finitePositiveLimit(value, fallback) {
|
|
2076
|
+
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
// src/client/acp-session.ts
|
|
2080
|
+
var ACPSessionError = class extends Error {
|
|
2081
|
+
kind;
|
|
2082
|
+
cause;
|
|
2083
|
+
constructor(kind, message, cause) {
|
|
2084
|
+
super(message);
|
|
2085
|
+
this.name = "ACPSessionError";
|
|
2086
|
+
this.kind = kind;
|
|
2087
|
+
this.cause = cause;
|
|
2088
|
+
}
|
|
2089
|
+
};
|
|
2090
|
+
function isJsonRpcError(v) {
|
|
2091
|
+
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
2092
|
+
}
|
|
2093
|
+
var ACPSession = class _ACPSession {
|
|
2094
|
+
transport;
|
|
2095
|
+
fileServer;
|
|
2096
|
+
terminalServer;
|
|
2097
|
+
permissionPolicy;
|
|
2098
|
+
timeoutMs;
|
|
2099
|
+
opts;
|
|
2100
|
+
transportOff = null;
|
|
2101
|
+
state = "init";
|
|
2102
|
+
sessionId = null;
|
|
2103
|
+
/** Pending outbound requests (initialize, session/new, session/prompt, etc). */
|
|
2104
|
+
pending = /* @__PURE__ */ new Map();
|
|
2105
|
+
nextId = 1;
|
|
2106
|
+
/** True after close() has been called. */
|
|
2107
|
+
closed = false;
|
|
2108
|
+
// Agent-provided info from the initialize handshake
|
|
2109
|
+
agentCapabilities = {};
|
|
2110
|
+
agentInfo = null;
|
|
2111
|
+
authMethods = [];
|
|
2112
|
+
/** Protocol version negotiated with the agent during initialize. */
|
|
2113
|
+
negotiatedVersion = ACP_PROTOCOL_VERSION;
|
|
2114
|
+
constructor(opts, transport) {
|
|
2115
|
+
this.opts = opts;
|
|
2116
|
+
this.transport = transport;
|
|
2117
|
+
this.timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
1904
2118
|
const fsOpts = {
|
|
1905
2119
|
projectRoot: opts.projectRoot
|
|
1906
2120
|
};
|
|
@@ -1915,8 +2129,19 @@ var ACPSession = class _ACPSession {
|
|
|
1915
2129
|
if (opts.terminalOutputByteLimit !== void 0) {
|
|
1916
2130
|
termOpts.outputByteLimit = opts.terminalOutputByteLimit;
|
|
1917
2131
|
}
|
|
2132
|
+
if (opts.terminalMaxCount !== void 0) {
|
|
2133
|
+
termOpts.maxTerminals = opts.terminalMaxCount;
|
|
2134
|
+
}
|
|
1918
2135
|
this.terminalServer = new TerminalServer(termOpts);
|
|
1919
|
-
|
|
2136
|
+
if (opts.permissionPolicy && opts.trustBoundary) {
|
|
2137
|
+
throw new TypeError("permissionPolicy and trustBoundary are mutually exclusive");
|
|
2138
|
+
}
|
|
2139
|
+
this.permissionPolicy = opts.trustBoundary ? makeTrustBoundaryPermissionPolicy({
|
|
2140
|
+
boundary: opts.trustBoundary,
|
|
2141
|
+
...opts.trustActor ? { actor: opts.trustActor } : {},
|
|
2142
|
+
scope: opts.trustScope ?? { cwd: opts.projectRoot },
|
|
2143
|
+
...opts.trustAuthContext ? { authContext: opts.trustAuthContext } : {}
|
|
2144
|
+
}) : opts.permissionPolicy ?? readOnlyPermissionPolicy;
|
|
1920
2145
|
}
|
|
1921
2146
|
// ──────────────────────────────────────────────────────────────────────
|
|
1922
2147
|
// Public accessors
|
|
@@ -1990,10 +2215,12 @@ var ACPSession = class _ACPSession {
|
|
|
1990
2215
|
throw new ACPSessionError("spawn_failed", `${spawnErrLabel}: ${msg}`, err);
|
|
1991
2216
|
}
|
|
1992
2217
|
const session = new _ACPSession(opts, transport);
|
|
1993
|
-
transport.onMessage((msg) => session.handleMessage(msg));
|
|
2218
|
+
session.transportOff = transport.onMessage((msg) => session.handleMessage(msg));
|
|
1994
2219
|
try {
|
|
1995
2220
|
await session.initialize();
|
|
1996
2221
|
} catch (err) {
|
|
2222
|
+
session.transportOff?.();
|
|
2223
|
+
session.transportOff = null;
|
|
1997
2224
|
try {
|
|
1998
2225
|
transport.stop();
|
|
1999
2226
|
} catch {
|
|
@@ -2157,7 +2384,11 @@ var ACPSession = class _ACPSession {
|
|
|
2157
2384
|
mcpServers: servers
|
|
2158
2385
|
});
|
|
2159
2386
|
if (isJsonRpcError(result)) {
|
|
2160
|
-
throw new ACPSessionError(
|
|
2387
|
+
throw new ACPSessionError(
|
|
2388
|
+
"prompt_failed",
|
|
2389
|
+
`session/resume failed: ${result.message}`,
|
|
2390
|
+
result
|
|
2391
|
+
);
|
|
2161
2392
|
}
|
|
2162
2393
|
this.sessionId = sessionId;
|
|
2163
2394
|
}
|
|
@@ -2208,7 +2439,11 @@ var ACPSession = class _ACPSession {
|
|
|
2208
2439
|
const id = this.allocId();
|
|
2209
2440
|
const result = await this.sendRequest(id, "session/delete", { sessionId });
|
|
2210
2441
|
if (isJsonRpcError(result)) {
|
|
2211
|
-
throw new ACPSessionError(
|
|
2442
|
+
throw new ACPSessionError(
|
|
2443
|
+
"prompt_failed",
|
|
2444
|
+
`session/delete failed: ${result.message}`,
|
|
2445
|
+
result
|
|
2446
|
+
);
|
|
2212
2447
|
}
|
|
2213
2448
|
if (this.sessionId === sessionId) {
|
|
2214
2449
|
this.sessionId = null;
|
|
@@ -2243,7 +2478,11 @@ var ACPSession = class _ACPSession {
|
|
|
2243
2478
|
const id = this.allocId();
|
|
2244
2479
|
const result = await this.sendRequest(id, "session/set_mode", { sessionId, modeId });
|
|
2245
2480
|
if (isJsonRpcError(result)) {
|
|
2246
|
-
throw new ACPSessionError(
|
|
2481
|
+
throw new ACPSessionError(
|
|
2482
|
+
"prompt_failed",
|
|
2483
|
+
`session/set_mode failed: ${result.message}`,
|
|
2484
|
+
result
|
|
2485
|
+
);
|
|
2247
2486
|
}
|
|
2248
2487
|
}
|
|
2249
2488
|
/**
|
|
@@ -2258,7 +2497,11 @@ var ACPSession = class _ACPSession {
|
|
|
2258
2497
|
value
|
|
2259
2498
|
});
|
|
2260
2499
|
if (isJsonRpcError(result)) {
|
|
2261
|
-
throw new ACPSessionError(
|
|
2500
|
+
throw new ACPSessionError(
|
|
2501
|
+
"prompt_failed",
|
|
2502
|
+
`session/set_config_option failed: ${result.message}`,
|
|
2503
|
+
result
|
|
2504
|
+
);
|
|
2262
2505
|
}
|
|
2263
2506
|
}
|
|
2264
2507
|
/**
|
|
@@ -2269,7 +2512,11 @@ var ACPSession = class _ACPSession {
|
|
|
2269
2512
|
const id = this.allocId();
|
|
2270
2513
|
const result = await this.sendRequest(id, "providers/list", {});
|
|
2271
2514
|
if (isJsonRpcError(result)) {
|
|
2272
|
-
throw new ACPSessionError(
|
|
2515
|
+
throw new ACPSessionError(
|
|
2516
|
+
"prompt_failed",
|
|
2517
|
+
`providers/list failed: ${result.message}`,
|
|
2518
|
+
result
|
|
2519
|
+
);
|
|
2273
2520
|
}
|
|
2274
2521
|
const r = result;
|
|
2275
2522
|
return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
|
|
@@ -2305,7 +2552,11 @@ var ACPSession = class _ACPSession {
|
|
|
2305
2552
|
const id = this.allocId();
|
|
2306
2553
|
const result = await this.sendRequest(id, "providers/disable", {});
|
|
2307
2554
|
if (isJsonRpcError(result)) {
|
|
2308
|
-
throw new ACPSessionError(
|
|
2555
|
+
throw new ACPSessionError(
|
|
2556
|
+
"prompt_failed",
|
|
2557
|
+
`providers/disable failed: ${result.message}`,
|
|
2558
|
+
result
|
|
2559
|
+
);
|
|
2309
2560
|
}
|
|
2310
2561
|
}
|
|
2311
2562
|
// ──────────────────────────────────────────────────────────────────────
|
|
@@ -2412,11 +2663,7 @@ var ACPSession = class _ACPSession {
|
|
|
2412
2663
|
}
|
|
2413
2664
|
const sessionId = result.sessionId;
|
|
2414
2665
|
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
2415
|
-
throw new ACPSessionError(
|
|
2416
|
-
"protocol_error",
|
|
2417
|
-
"session/new returned no sessionId",
|
|
2418
|
-
result
|
|
2419
|
-
);
|
|
2666
|
+
throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
|
|
2420
2667
|
}
|
|
2421
2668
|
this.sessionId = sessionId;
|
|
2422
2669
|
}
|
|
@@ -2459,6 +2706,8 @@ var ACPSession = class _ACPSession {
|
|
|
2459
2706
|
p.reject(new ACPSessionError("closed", "session was closed"));
|
|
2460
2707
|
}
|
|
2461
2708
|
this.pending.clear();
|
|
2709
|
+
this.transportOff?.();
|
|
2710
|
+
this.transportOff = null;
|
|
2462
2711
|
try {
|
|
2463
2712
|
this.transport.stop();
|
|
2464
2713
|
} catch {
|
|
@@ -2494,10 +2743,7 @@ var ACPSession = class _ACPSession {
|
|
|
2494
2743
|
const handle = setTimeout(() => {
|
|
2495
2744
|
this.pending.delete(id);
|
|
2496
2745
|
reject(
|
|
2497
|
-
new ACPSessionError(
|
|
2498
|
-
"protocol_error",
|
|
2499
|
-
`${method} timed out after ${effectiveTimeout}ms`
|
|
2500
|
-
)
|
|
2746
|
+
new ACPSessionError("protocol_error", `${method} timed out after ${effectiveTimeout}ms`)
|
|
2501
2747
|
);
|
|
2502
2748
|
}, effectiveTimeout);
|
|
2503
2749
|
this.pending.set(id, {
|
|
@@ -2814,7 +3060,12 @@ var ACPSession = class _ACPSession {
|
|
|
2814
3060
|
toolCallId: `acp-terminal-create-${id}`,
|
|
2815
3061
|
title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
|
|
2816
3062
|
kind: "execute",
|
|
2817
|
-
rawInput: {
|
|
3063
|
+
rawInput: {
|
|
3064
|
+
command: params.command,
|
|
3065
|
+
args: params.args,
|
|
3066
|
+
cwd: params.cwd,
|
|
3067
|
+
sessionId: params.sessionId
|
|
3068
|
+
}
|
|
2818
3069
|
});
|
|
2819
3070
|
if (!allowed) {
|
|
2820
3071
|
await this.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
|
|
@@ -2862,45 +3113,323 @@ var ACPSession = class _ACPSession {
|
|
|
2862
3113
|
await this.sendResult(id, {});
|
|
2863
3114
|
return;
|
|
2864
3115
|
}
|
|
2865
|
-
default:
|
|
2866
|
-
await this.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
|
|
2867
|
-
}
|
|
2868
|
-
} catch (err) {
|
|
2869
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2870
|
-
await this.sendErrorResponse(id, -32603, message);
|
|
2871
|
-
}
|
|
3116
|
+
default:
|
|
3117
|
+
await this.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
|
|
3118
|
+
}
|
|
3119
|
+
} catch (err) {
|
|
3120
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3121
|
+
await this.sendErrorResponse(id, -32603, message);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
};
|
|
3125
|
+
function textContent(text) {
|
|
3126
|
+
return { type: "text", text };
|
|
3127
|
+
}
|
|
3128
|
+
function imageContent(mimeType, data) {
|
|
3129
|
+
return { type: "image", mimeType, data };
|
|
3130
|
+
}
|
|
3131
|
+
function audioContent(mimeType, data) {
|
|
3132
|
+
return { type: "audio", mimeType, data };
|
|
3133
|
+
}
|
|
3134
|
+
function extractText(block) {
|
|
3135
|
+
if (typeof block !== "object" || block === null) return "";
|
|
3136
|
+
const b = block;
|
|
3137
|
+
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
3138
|
+
if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
|
|
3139
|
+
return b.resource.text;
|
|
3140
|
+
}
|
|
3141
|
+
return "";
|
|
3142
|
+
}
|
|
3143
|
+
function isRecord(v) {
|
|
3144
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3145
|
+
}
|
|
3146
|
+
function emptyRunResult(stopReason) {
|
|
3147
|
+
return {
|
|
3148
|
+
text: "",
|
|
3149
|
+
stopReason,
|
|
3150
|
+
hasText: false,
|
|
3151
|
+
toolCalls: [],
|
|
3152
|
+
diffs: [],
|
|
3153
|
+
thoughts: ""
|
|
3154
|
+
};
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
// src/client/tool-translator.ts
|
|
3158
|
+
import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
|
|
3159
|
+
var DEFAULT_OPTIONS = {
|
|
3160
|
+
asyncTools: true,
|
|
3161
|
+
pollIntervalMs: 500,
|
|
3162
|
+
totalTimeoutMs: 12e4
|
|
3163
|
+
};
|
|
3164
|
+
var ToolTranslator = class {
|
|
3165
|
+
opts;
|
|
3166
|
+
pending = /* @__PURE__ */ new Map();
|
|
3167
|
+
constructor(opts = {}) {
|
|
3168
|
+
this.opts = { ...DEFAULT_OPTIONS, ...opts };
|
|
3169
|
+
}
|
|
3170
|
+
/**
|
|
3171
|
+
* Start listening to a transport for tool responses and cancellations.
|
|
3172
|
+
* Call this once after constructing the translator and before sending tasks.
|
|
3173
|
+
*/
|
|
3174
|
+
attachToTransport(transport) {
|
|
3175
|
+
transport.onMessage((msg) => {
|
|
3176
|
+
if (msg.method === "tools/call" && msg.id !== void 0) {
|
|
3177
|
+
const pending = this.pending.get(msg.id);
|
|
3178
|
+
if (pending) {
|
|
3179
|
+
clearTimeout(pending.timeout);
|
|
3180
|
+
this.pending.delete(expectDefined2(msg.id));
|
|
3181
|
+
pending.resolve(msg);
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
if (msg.method === "cancel" && msg.id !== void 0) {
|
|
3185
|
+
const pending = this.pending.get(msg.id);
|
|
3186
|
+
if (pending) {
|
|
3187
|
+
clearTimeout(pending.timeout);
|
|
3188
|
+
this.pending.delete(expectDefined2(msg.id));
|
|
3189
|
+
pending.reject(new Error("Call cancelled by client"));
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
});
|
|
3193
|
+
}
|
|
3194
|
+
/**
|
|
3195
|
+
* Send a tool call over the transport and wait for a response.
|
|
3196
|
+
* If asyncTools is true, polls for progress and resolves when the final
|
|
3197
|
+
* response arrives.
|
|
3198
|
+
*/
|
|
3199
|
+
async callTool(transport, name, args, callId = crypto.randomUUID()) {
|
|
3200
|
+
await transport.send({
|
|
3201
|
+
jsonrpc: "2.0",
|
|
3202
|
+
method: "tools/call",
|
|
3203
|
+
id: callId,
|
|
3204
|
+
params: { name, arguments: args }
|
|
3205
|
+
});
|
|
3206
|
+
return new Promise((resolve3, reject) => {
|
|
3207
|
+
const timeout = setTimeout(() => {
|
|
3208
|
+
this.pending.delete(callId);
|
|
3209
|
+
reject(new Error(`Tool call ${name} timed out after ${this.opts.totalTimeoutMs}ms`));
|
|
3210
|
+
}, this.opts.totalTimeoutMs);
|
|
3211
|
+
this.pending.set(callId, { resolve: resolve3, reject, timeout });
|
|
3212
|
+
});
|
|
3213
|
+
}
|
|
3214
|
+
cancelAll() {
|
|
3215
|
+
for (const [, p] of this.pending) {
|
|
3216
|
+
clearTimeout(p.timeout);
|
|
3217
|
+
}
|
|
3218
|
+
this.pending.clear();
|
|
3219
|
+
}
|
|
3220
|
+
};
|
|
3221
|
+
|
|
3222
|
+
// src/integration/acp-bench.ts
|
|
3223
|
+
import * as fsp2 from "node:fs/promises";
|
|
3224
|
+
import * as path3 from "node:path";
|
|
3225
|
+
function firstLine(s) {
|
|
3226
|
+
const line = s.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "";
|
|
3227
|
+
return line.length > 120 ? `${line.slice(0, 117)}\u2026` : line;
|
|
3228
|
+
}
|
|
3229
|
+
function randomMarker() {
|
|
3230
|
+
return `ACP_OK_${Math.random().toString(36).slice(2, 8).toUpperCase()}`;
|
|
3231
|
+
}
|
|
3232
|
+
async function benchOne(agentId, cmd, opts) {
|
|
3233
|
+
const checks = [];
|
|
3234
|
+
const startedAt = opts.now();
|
|
3235
|
+
let session = null;
|
|
3236
|
+
const signal = opts.signal ?? new AbortController().signal;
|
|
3237
|
+
const hsStart = opts.now();
|
|
3238
|
+
try {
|
|
3239
|
+
session = await ACPSession.start({
|
|
3240
|
+
command: cmd.command,
|
|
3241
|
+
...cmd.args !== void 0 ? { args: [...cmd.args] } : {},
|
|
3242
|
+
...cmd.env !== void 0 ? { env: cmd.env } : {},
|
|
3243
|
+
projectRoot: opts.projectRoot,
|
|
3244
|
+
timeoutMs: opts.timeoutMs
|
|
3245
|
+
});
|
|
3246
|
+
} catch (err) {
|
|
3247
|
+
const reason2 = err instanceof Error ? err.message : String(err);
|
|
3248
|
+
checks.push({ name: "handshake", ok: false, detail: reason2 });
|
|
3249
|
+
return {
|
|
3250
|
+
agentId,
|
|
3251
|
+
status: "fail",
|
|
3252
|
+
checks,
|
|
3253
|
+
reason: reason2,
|
|
3254
|
+
handshakeMs: opts.now() - hsStart,
|
|
3255
|
+
durationMs: opts.now() - startedAt
|
|
3256
|
+
};
|
|
3257
|
+
}
|
|
3258
|
+
const handshakeMs = opts.now() - hsStart;
|
|
3259
|
+
const agentInfo = session.getAgentInfo() ?? void 0;
|
|
3260
|
+
checks.push({
|
|
3261
|
+
name: "handshake",
|
|
3262
|
+
ok: true,
|
|
3263
|
+
detail: agentInfo ? `${agentInfo.name} ${agentInfo.version}` : void 0
|
|
3264
|
+
});
|
|
3265
|
+
let promptMs;
|
|
3266
|
+
let sample;
|
|
3267
|
+
let reason;
|
|
3268
|
+
try {
|
|
3269
|
+
const pStart = opts.now();
|
|
3270
|
+
const res = await session.prompt(
|
|
3271
|
+
[textContent(`Reply with exactly this token and nothing else: ${opts.marker}`)],
|
|
3272
|
+
signal
|
|
3273
|
+
);
|
|
3274
|
+
promptMs = opts.now() - pStart;
|
|
3275
|
+
sample = res.text ? firstLine(res.text) : void 0;
|
|
3276
|
+
const promptOk = res.hasText && res.stopReason !== "refusal";
|
|
3277
|
+
checks.push({
|
|
3278
|
+
name: "prompt",
|
|
3279
|
+
ok: promptOk,
|
|
3280
|
+
detail: `stopReason=${res.stopReason}${res.hasText ? "" : ", no text"}`
|
|
3281
|
+
});
|
|
3282
|
+
const markerOk = res.text.includes(opts.marker);
|
|
3283
|
+
checks.push({
|
|
3284
|
+
name: "marker",
|
|
3285
|
+
ok: markerOk,
|
|
3286
|
+
detail: markerOk ? void 0 : "reply did not contain the token"
|
|
3287
|
+
});
|
|
3288
|
+
if (opts.checkFs) {
|
|
3289
|
+
const fileToken = `FILE_${opts.marker}`;
|
|
3290
|
+
const fileName = `acp-bench-${opts.marker}.txt`;
|
|
3291
|
+
const filePath = path3.join(opts.projectRoot, fileName);
|
|
3292
|
+
let fsOk = false;
|
|
3293
|
+
let fsDetail;
|
|
3294
|
+
try {
|
|
3295
|
+
await fsp2.writeFile(filePath, fileToken, "utf8");
|
|
3296
|
+
const fsRes = await session.prompt(
|
|
3297
|
+
[
|
|
3298
|
+
textContent(
|
|
3299
|
+
`Read the file "${fileName}" in the current directory and reply with its exact contents.`
|
|
3300
|
+
)
|
|
3301
|
+
],
|
|
3302
|
+
signal
|
|
3303
|
+
);
|
|
3304
|
+
fsOk = fsRes.text.includes(fileToken);
|
|
3305
|
+
if (!fsOk)
|
|
3306
|
+
fsDetail = "agent did not return the file contents (may not have used a read tool)";
|
|
3307
|
+
} catch (err) {
|
|
3308
|
+
fsDetail = err instanceof Error ? err.message : String(err);
|
|
3309
|
+
} finally {
|
|
3310
|
+
await removeBenchFile(filePath);
|
|
3311
|
+
}
|
|
3312
|
+
checks.push({ name: "fs", ok: fsOk, detail: fsDetail });
|
|
3313
|
+
}
|
|
3314
|
+
} catch (err) {
|
|
3315
|
+
reason = err instanceof Error ? err.message : String(err);
|
|
3316
|
+
checks.push({ name: "prompt", ok: false, detail: reason });
|
|
3317
|
+
} finally {
|
|
3318
|
+
try {
|
|
3319
|
+
await session.close();
|
|
3320
|
+
} catch {
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
const required = checks.filter((c) => c.name !== "fs" || opts.checkFs);
|
|
3324
|
+
const allReq = required.every((c) => c.ok);
|
|
3325
|
+
const status = allReq ? "pass" : "partial";
|
|
3326
|
+
return {
|
|
3327
|
+
agentId,
|
|
3328
|
+
status,
|
|
3329
|
+
checks,
|
|
3330
|
+
...agentInfo ? { agentInfo } : {},
|
|
3331
|
+
handshakeMs,
|
|
3332
|
+
...promptMs !== void 0 ? { promptMs } : {},
|
|
3333
|
+
...sample ? { sample } : {},
|
|
3334
|
+
...reason ? { reason } : {},
|
|
3335
|
+
durationMs: opts.now() - startedAt
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
async function runAcpBench(opts) {
|
|
3339
|
+
const now = opts.now ?? Date.now;
|
|
3340
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
3341
|
+
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
3342
|
+
const checkFs = opts.checkFs ?? false;
|
|
3343
|
+
const marker = opts.marker ?? randomMarker();
|
|
3344
|
+
const concurrency = Math.max(1, opts.concurrency ?? 2);
|
|
3345
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3346
|
+
const ids = [];
|
|
3347
|
+
for (const raw of opts.agentIds) {
|
|
3348
|
+
const id = raw.trim();
|
|
3349
|
+
if (id && !seen.has(id)) {
|
|
3350
|
+
seen.add(id);
|
|
3351
|
+
ids.push(id);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
const results = ids.map((agentId) => ({
|
|
3355
|
+
agentId,
|
|
3356
|
+
status: "skipped",
|
|
3357
|
+
checks: [],
|
|
3358
|
+
durationMs: 0,
|
|
3359
|
+
reason: "unknown agent"
|
|
3360
|
+
}));
|
|
3361
|
+
const startMs = now();
|
|
3362
|
+
const runnable = [];
|
|
3363
|
+
ids.forEach((id, index) => {
|
|
3364
|
+
const cmd = opts.resolveCmd(id);
|
|
3365
|
+
if (cmd) runnable.push({ id, cmd, index });
|
|
3366
|
+
});
|
|
3367
|
+
let next = 0;
|
|
3368
|
+
const workers = [];
|
|
3369
|
+
const workerCount = Math.min(concurrency, runnable.length);
|
|
3370
|
+
for (let w = 0; w < workerCount; w++) {
|
|
3371
|
+
workers.push(
|
|
3372
|
+
(async () => {
|
|
3373
|
+
while (true) {
|
|
3374
|
+
const current = next++;
|
|
3375
|
+
if (current >= runnable.length) return;
|
|
3376
|
+
const { id, cmd, index } = runnable[current];
|
|
3377
|
+
if (opts.signal?.aborted) {
|
|
3378
|
+
results[index] = {
|
|
3379
|
+
agentId: id,
|
|
3380
|
+
status: "skipped",
|
|
3381
|
+
checks: [],
|
|
3382
|
+
durationMs: 0,
|
|
3383
|
+
reason: "aborted"
|
|
3384
|
+
};
|
|
3385
|
+
continue;
|
|
3386
|
+
}
|
|
3387
|
+
opts.onProgress?.(id, "start");
|
|
3388
|
+
const r = await benchOne(id, cmd, {
|
|
3389
|
+
projectRoot,
|
|
3390
|
+
timeoutMs,
|
|
3391
|
+
checkFs,
|
|
3392
|
+
marker,
|
|
3393
|
+
now,
|
|
3394
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
3395
|
+
});
|
|
3396
|
+
results[index] = r;
|
|
3397
|
+
opts.onProgress?.(id, "done", r);
|
|
3398
|
+
}
|
|
3399
|
+
})()
|
|
3400
|
+
);
|
|
2872
3401
|
}
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
}
|
|
2877
|
-
function imageContent(mimeType, data) {
|
|
2878
|
-
return { type: "image", mimeType, data };
|
|
2879
|
-
}
|
|
2880
|
-
function audioContent(mimeType, data) {
|
|
2881
|
-
return { type: "audio", mimeType, data };
|
|
3402
|
+
await Promise.all(workers);
|
|
3403
|
+
const summary = { pass: 0, partial: 0, fail: 0, skipped: 0 };
|
|
3404
|
+
for (const r of results) summary[r.status]++;
|
|
3405
|
+
return { results, summary, totalDurationMs: now() - startMs };
|
|
2882
3406
|
}
|
|
2883
|
-
function
|
|
2884
|
-
|
|
2885
|
-
const
|
|
2886
|
-
if (
|
|
2887
|
-
|
|
2888
|
-
return
|
|
3407
|
+
function renderAcpBenchText(result) {
|
|
3408
|
+
const icon = (s) => s === "pass" ? "\u2713" : s === "partial" ? "\u25D0" : s === "skipped" ? "\u2013" : "\u2717";
|
|
3409
|
+
const lines = ["ACP client bench:", ""];
|
|
3410
|
+
if (result.results.length === 0) {
|
|
3411
|
+
lines.push("No agents to bench.");
|
|
3412
|
+
return lines.join("\n");
|
|
2889
3413
|
}
|
|
2890
|
-
|
|
2891
|
-
}
|
|
2892
|
-
|
|
2893
|
-
|
|
3414
|
+
for (const r of result.results) {
|
|
3415
|
+
const checks = r.checks.map((c) => `${c.ok ? "\u2713" : "\u2717"}${c.name}`).join(" ");
|
|
3416
|
+
const timing = r.handshakeMs !== void 0 ? ` hs=${r.handshakeMs}ms${r.promptMs !== void 0 ? ` prompt=${r.promptMs}ms` : ""}` : "";
|
|
3417
|
+
lines.push(
|
|
3418
|
+
` ${icon(r.status)} ${r.agentId.padEnd(16)} ${r.status.toUpperCase().padEnd(7)} ${checks}${timing}`
|
|
3419
|
+
);
|
|
3420
|
+
if (r.agentInfo) lines.push(` agent: ${r.agentInfo.name} ${r.agentInfo.version}`);
|
|
3421
|
+
if (r.sample) lines.push(` reply: ${r.sample}`);
|
|
3422
|
+
if (r.reason) lines.push(` reason: ${r.reason}`);
|
|
3423
|
+
}
|
|
3424
|
+
const { pass, partial, fail, skipped } = result.summary;
|
|
3425
|
+
lines.push("");
|
|
3426
|
+
lines.push(
|
|
3427
|
+
`Bench summary: ${pass} pass, ${partial} partial, ${fail} fail, ${skipped} skipped. (${result.totalDurationMs}ms total)`
|
|
3428
|
+
);
|
|
3429
|
+
return lines.join("\n");
|
|
2894
3430
|
}
|
|
2895
|
-
function
|
|
2896
|
-
|
|
2897
|
-
text: "",
|
|
2898
|
-
stopReason,
|
|
2899
|
-
hasText: false,
|
|
2900
|
-
toolCalls: [],
|
|
2901
|
-
diffs: [],
|
|
2902
|
-
thoughts: ""
|
|
2903
|
-
};
|
|
3431
|
+
async function removeBenchFile(filePath, remove = fsp2.rm) {
|
|
3432
|
+
await remove(filePath, { force: true }).catch(() => void 0);
|
|
2904
3433
|
}
|
|
2905
3434
|
|
|
2906
3435
|
// src/registry/agents.catalog.ts
|
|
@@ -3139,50 +3668,6 @@ function findAgentDescriptor(id) {
|
|
|
3139
3668
|
return AGENTS_CATALOG.find((a) => a.id === id);
|
|
3140
3669
|
}
|
|
3141
3670
|
|
|
3142
|
-
// src/integration/run-one-acp-task.ts
|
|
3143
|
-
import { SubagentBudget } from "@wrongstack/core/coordination";
|
|
3144
|
-
async function runOneAcpTask(opts) {
|
|
3145
|
-
const role = opts.role ?? "acp";
|
|
3146
|
-
const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
3147
|
-
const { runner, stop } = await makeACPSubagentRunnerWithStop({
|
|
3148
|
-
command: opts.command,
|
|
3149
|
-
...opts.args !== void 0 ? { args: opts.args } : {},
|
|
3150
|
-
...opts.env !== void 0 ? { env: opts.env } : {},
|
|
3151
|
-
...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
|
|
3152
|
-
...opts.projectRoot !== void 0 ? { projectRoot: opts.projectRoot } : {},
|
|
3153
|
-
role,
|
|
3154
|
-
timeoutMs,
|
|
3155
|
-
...opts.onProgress !== void 0 ? { onProgress: opts.onProgress } : {},
|
|
3156
|
-
...opts.permissionPolicy !== void 0 ? { permissionPolicy: opts.permissionPolicy } : {}
|
|
3157
|
-
});
|
|
3158
|
-
try {
|
|
3159
|
-
const budget = new SubagentBudget({
|
|
3160
|
-
timeoutMs,
|
|
3161
|
-
maxIterations: 2e3,
|
|
3162
|
-
maxToolCalls: 5e3
|
|
3163
|
-
});
|
|
3164
|
-
budget.start();
|
|
3165
|
-
const ctx = {
|
|
3166
|
-
subagentId: role,
|
|
3167
|
-
config: { id: role, name: role, role, provider: "acp", prompt: "" },
|
|
3168
|
-
budget,
|
|
3169
|
-
signal: opts.signal ?? new AbortController().signal,
|
|
3170
|
-
bridge: null
|
|
3171
|
-
};
|
|
3172
|
-
const result = await runner({ id: `acp-${role}`, description: opts.task }, ctx);
|
|
3173
|
-
return {
|
|
3174
|
-
result: result.result == null ? "" : String(result.result),
|
|
3175
|
-
iterations: result.iterations,
|
|
3176
|
-
toolCalls: result.toolCalls
|
|
3177
|
-
};
|
|
3178
|
-
} finally {
|
|
3179
|
-
try {
|
|
3180
|
-
await stop();
|
|
3181
|
-
} catch {
|
|
3182
|
-
}
|
|
3183
|
-
}
|
|
3184
|
-
}
|
|
3185
|
-
|
|
3186
3671
|
// src/integration/acp-subagent-runner.ts
|
|
3187
3672
|
var ACP_AGENT_COMMANDS = {
|
|
3188
3673
|
cline: {
|
|
@@ -3334,16 +3819,8 @@ function mapACPKind(acpKind) {
|
|
|
3334
3819
|
}
|
|
3335
3820
|
}
|
|
3336
3821
|
function isRetryable(kind) {
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
case "provider_rate_limit":
|
|
3340
|
-
case "provider_timeout":
|
|
3341
|
-
case "tool_threw":
|
|
3342
|
-
case "budget_timeout":
|
|
3343
|
-
return true;
|
|
3344
|
-
default:
|
|
3345
|
-
return false;
|
|
3346
|
-
}
|
|
3822
|
+
void kind;
|
|
3823
|
+
return false;
|
|
3347
3824
|
}
|
|
3348
3825
|
var REGISTRY_ID_ALIASES = {
|
|
3349
3826
|
"claude-code": "claude-acp",
|
|
@@ -3483,11 +3960,15 @@ async function probeAcpAgents(opts) {
|
|
|
3483
3960
|
}
|
|
3484
3961
|
await runPhase(local, opts.concurrency ?? 4, localTimeout);
|
|
3485
3962
|
await runPhase(pkg, 2, pkgTimeout);
|
|
3486
|
-
return ids.map((id) => byId.get(id)
|
|
3963
|
+
return ids.map((id) => byId.get(id));
|
|
3487
3964
|
}
|
|
3488
3965
|
|
|
3966
|
+
// src/integration/ensemble-runner.ts
|
|
3967
|
+
import { SubagentBudget } from "@wrongstack/core/coordination";
|
|
3968
|
+
|
|
3489
3969
|
// src/registry/ensemble-registry.ts
|
|
3490
3970
|
import { spawn as spawn2 } from "node:child_process";
|
|
3971
|
+
import { treeKill as treeKill2 } from "@wrongstack/core/utils/tree-kill";
|
|
3491
3972
|
var PROBE_TIMEOUT_MS = 5e3;
|
|
3492
3973
|
var PROBE_CACHE_MS = 5e3;
|
|
3493
3974
|
var MAX_PARALLEL_PROBES = 4;
|
|
@@ -3511,7 +3992,7 @@ async function probeWithBound(items, worker, limit) {
|
|
|
3511
3992
|
await Promise.all(runners);
|
|
3512
3993
|
return results;
|
|
3513
3994
|
}
|
|
3514
|
-
async function defaultProbe(desc, timeoutMs) {
|
|
3995
|
+
async function defaultProbe(desc, timeoutMs, spawnProcess = spawn2, platform = process.platform) {
|
|
3515
3996
|
const start = Date.now();
|
|
3516
3997
|
return new Promise((resolve3) => {
|
|
3517
3998
|
let settled = false;
|
|
@@ -3521,7 +4002,9 @@ async function defaultProbe(desc, timeoutMs) {
|
|
|
3521
4002
|
if (settled) return;
|
|
3522
4003
|
settled = true;
|
|
3523
4004
|
try {
|
|
3524
|
-
child.
|
|
4005
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
4006
|
+
treeKill2(child);
|
|
4007
|
+
}
|
|
3525
4008
|
} catch {
|
|
3526
4009
|
}
|
|
3527
4010
|
resolve3(result);
|
|
@@ -3529,8 +4012,8 @@ async function defaultProbe(desc, timeoutMs) {
|
|
|
3529
4012
|
let child;
|
|
3530
4013
|
try {
|
|
3531
4014
|
const probeArgs = [...desc.probe.args ?? []];
|
|
3532
|
-
const shim =
|
|
3533
|
-
child =
|
|
4015
|
+
const shim = platform === "win32" ? buildWin32CmdShimInvocation(desc.probe.command, probeArgs) : null;
|
|
4016
|
+
child = spawnProcess(shim?.command ?? desc.probe.command, shim?.args ?? probeArgs, {
|
|
3534
4017
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3535
4018
|
windowsHide: true,
|
|
3536
4019
|
...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
|
|
@@ -3563,7 +4046,7 @@ async function defaultProbe(desc, timeoutMs) {
|
|
|
3563
4046
|
clearTimeout(timer);
|
|
3564
4047
|
const durationMs = Date.now() - start;
|
|
3565
4048
|
const out = (stdout + stderr).trim();
|
|
3566
|
-
const isWindowsShellMiss =
|
|
4049
|
+
const isWindowsShellMiss = platform === "win32" && out.toLowerCase().includes("is not recognized");
|
|
3567
4050
|
if (isWindowsShellMiss) {
|
|
3568
4051
|
finish({
|
|
3569
4052
|
ok: false,
|
|
@@ -3575,7 +4058,7 @@ async function defaultProbe(desc, timeoutMs) {
|
|
|
3575
4058
|
if (out.length > 0) {
|
|
3576
4059
|
finish({
|
|
3577
4060
|
ok: true,
|
|
3578
|
-
version: out.split("\n")[0]
|
|
4061
|
+
version: out.split("\n")[0].trim(),
|
|
3579
4062
|
path: desc.probe.command,
|
|
3580
4063
|
durationMs
|
|
3581
4064
|
});
|
|
@@ -3650,93 +4133,7 @@ var EnsembleRegistry = class {
|
|
|
3650
4133
|
}
|
|
3651
4134
|
};
|
|
3652
4135
|
|
|
3653
|
-
// src/registry/acp-registry-fetch.ts
|
|
3654
|
-
var ACP_REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
|
|
3655
|
-
function currentPlatformKey() {
|
|
3656
|
-
const os = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "darwin" : "linux";
|
|
3657
|
-
const arch = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : process.arch;
|
|
3658
|
-
return `${os}-${arch}`;
|
|
3659
|
-
}
|
|
3660
|
-
function basename(cmd) {
|
|
3661
|
-
const cleaned = cmd.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
3662
|
-
const parts = cleaned.split("/");
|
|
3663
|
-
return parts[parts.length - 1] || cleaned;
|
|
3664
|
-
}
|
|
3665
|
-
function mapRegistryEntry(entry, platformKey = currentPlatformKey()) {
|
|
3666
|
-
if (!entry || typeof entry.id !== "string" || entry.id.length === 0) return null;
|
|
3667
|
-
const dist = entry.distribution;
|
|
3668
|
-
let acp = null;
|
|
3669
|
-
if (dist?.npx?.package) {
|
|
3670
|
-
acp = { command: "npx", args: ["-y", dist.npx.package, ...dist.npx.args ?? []] };
|
|
3671
|
-
} else if (dist?.uvx?.package) {
|
|
3672
|
-
acp = { command: "uvx", args: [dist.uvx.package, ...dist.uvx.args ?? []] };
|
|
3673
|
-
} else if (dist?.binary) {
|
|
3674
|
-
const target = dist.binary[platformKey];
|
|
3675
|
-
if (target?.cmd) {
|
|
3676
|
-
acp = {
|
|
3677
|
-
command: basename(target.cmd),
|
|
3678
|
-
args: [...target.args ?? []],
|
|
3679
|
-
...target.env ? { env: target.env } : {}
|
|
3680
|
-
};
|
|
3681
|
-
}
|
|
3682
|
-
}
|
|
3683
|
-
if (!acp) return null;
|
|
3684
|
-
const probeCmd = acp.command === "npx" ? "npx" : acp.command === "uvx" ? "uvx" : acp.command;
|
|
3685
|
-
return {
|
|
3686
|
-
id: entry.id,
|
|
3687
|
-
displayName: entry.name ?? entry.id,
|
|
3688
|
-
vendor: inferVendor(entry),
|
|
3689
|
-
probe: { command: probeCmd, args: ["--version"] },
|
|
3690
|
-
acp,
|
|
3691
|
-
supports: { loadSession: true, promptImages: true, terminal: true, fs: true },
|
|
3692
|
-
integration: "native",
|
|
3693
|
-
docs: entry.repository ?? entry.website ?? ""
|
|
3694
|
-
};
|
|
3695
|
-
}
|
|
3696
|
-
function inferVendor(entry) {
|
|
3697
|
-
const hay = `${entry.id} ${entry.name ?? ""} ${(entry.authors ?? []).join(" ")}`.toLowerCase();
|
|
3698
|
-
if (hay.includes("anthropic") || hay.includes("claude")) return "anthropic";
|
|
3699
|
-
if (hay.includes("google") || hay.includes("gemini")) return "google";
|
|
3700
|
-
if (hay.includes("openai") || hay.includes("codex")) return "openai";
|
|
3701
|
-
if (hay.includes("github") || hay.includes("copilot")) return "github";
|
|
3702
|
-
if (hay.includes("moonshot") || hay.includes("kimi")) return "moonshot";
|
|
3703
|
-
return "community";
|
|
3704
|
-
}
|
|
3705
|
-
async function fetchAcpRegistry(opts = {}) {
|
|
3706
|
-
const url = opts.url ?? ACP_REGISTRY_URL;
|
|
3707
|
-
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
3708
|
-
const controller = new AbortController();
|
|
3709
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
3710
|
-
const onParentAbort = () => controller.abort();
|
|
3711
|
-
if (opts.signal) {
|
|
3712
|
-
if (opts.signal.aborted) controller.abort();
|
|
3713
|
-
else opts.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
3714
|
-
}
|
|
3715
|
-
try {
|
|
3716
|
-
const res = await fetch(url, { signal: controller.signal });
|
|
3717
|
-
if (!res.ok) {
|
|
3718
|
-
throw new Error(`ACP registry fetch failed: HTTP ${res.status}`);
|
|
3719
|
-
}
|
|
3720
|
-
const body = await res.json();
|
|
3721
|
-
const rawAgents = Array.isArray(body) ? body : Array.isArray(body?.agents) ? body.agents : null;
|
|
3722
|
-
if (!rawAgents) {
|
|
3723
|
-
throw new Error("ACP registry response had no agents array");
|
|
3724
|
-
}
|
|
3725
|
-
const platformKey = opts.platformKey ?? currentPlatformKey();
|
|
3726
|
-
const agents = [];
|
|
3727
|
-
for (const raw of rawAgents) {
|
|
3728
|
-
const mapped = mapRegistryEntry(raw, platformKey);
|
|
3729
|
-
if (mapped) agents.push(mapped);
|
|
3730
|
-
}
|
|
3731
|
-
return { fetchedAt: opts.now ?? (/* @__PURE__ */ new Date()).toISOString(), agents };
|
|
3732
|
-
} finally {
|
|
3733
|
-
clearTimeout(timer);
|
|
3734
|
-
opts.signal?.removeEventListener("abort", onParentAbort);
|
|
3735
|
-
}
|
|
3736
|
-
}
|
|
3737
|
-
|
|
3738
4136
|
// src/integration/ensemble-runner.ts
|
|
3739
|
-
import { SubagentBudget as SubagentBudget2 } from "@wrongstack/core/coordination";
|
|
3740
4137
|
var DEFAULT_MAX_CONCURRENCY = 4;
|
|
3741
4138
|
async function mapBound(items, worker, limit) {
|
|
3742
4139
|
const results = new Array(items.length);
|
|
@@ -3778,7 +4175,7 @@ async function runOne(agentId, cmd, task, timeoutMs, signal, onProgress) {
|
|
|
3778
4175
|
...onProgress ? { onProgress: (event) => onProgress(agentId, event) } : {}
|
|
3779
4176
|
});
|
|
3780
4177
|
try {
|
|
3781
|
-
const budget = new
|
|
4178
|
+
const budget = new SubagentBudget({
|
|
3782
4179
|
timeoutMs,
|
|
3783
4180
|
maxIterations: 2e3,
|
|
3784
4181
|
maxToolCalls: 5e3
|
|
@@ -3879,10 +4276,7 @@ async function runEnsemble(opts) {
|
|
|
3879
4276
|
}
|
|
3880
4277
|
runnable.push({ id, cmd });
|
|
3881
4278
|
}
|
|
3882
|
-
const concurrency = Math.max(
|
|
3883
|
-
1,
|
|
3884
|
-
opts.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY
|
|
3885
|
-
);
|
|
4279
|
+
const concurrency = Math.max(1, opts.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY);
|
|
3886
4280
|
await mapBound(
|
|
3887
4281
|
runnable,
|
|
3888
4282
|
async ({ id, cmd }) => {
|
|
@@ -3931,15 +4325,11 @@ function renderEnsembleText(result) {
|
|
|
3931
4325
|
);
|
|
3932
4326
|
break;
|
|
3933
4327
|
case "failed":
|
|
3934
|
-
lines.push(
|
|
3935
|
-
`[${r.error?.kind ?? "unknown"}] ${r.error?.message ?? "failed"}`
|
|
3936
|
-
);
|
|
4328
|
+
lines.push(`[${r.error?.kind ?? "unknown"}] ${r.error?.message ?? "failed"}`);
|
|
3937
4329
|
lines.push(`[${r.agentId}] failed ${r.durationMs}ms`);
|
|
3938
4330
|
break;
|
|
3939
4331
|
case "cancelled":
|
|
3940
|
-
lines.push(
|
|
3941
|
-
`[${r.error?.kind ?? "aborted"}] ${r.error?.message ?? "cancelled"}`
|
|
3942
|
-
);
|
|
4332
|
+
lines.push(`[${r.error?.kind ?? "aborted"}] ${r.error?.message ?? "cancelled"}`);
|
|
3943
4333
|
lines.push(`[${r.agentId}] cancelled ${r.durationMs}ms`);
|
|
3944
4334
|
break;
|
|
3945
4335
|
case "skipped":
|
|
@@ -3952,216 +4342,135 @@ function renderEnsembleText(result) {
|
|
|
3952
4342
|
`
|
|
3953
4343
|
Ensemble summary: ${succeeded} succeeded, ${failed} failed, ${cancelled} cancelled, ${skipped} skipped. (${result.totalDurationMs}ms total)`
|
|
3954
4344
|
);
|
|
3955
|
-
return lines.join("\n");
|
|
3956
|
-
}
|
|
3957
|
-
|
|
3958
|
-
// src/integration/acp-
|
|
3959
|
-
import
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
const
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
}
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
const hsStart = opts.now();
|
|
3974
|
-
try {
|
|
3975
|
-
session = await ACPSession.start({
|
|
3976
|
-
command: cmd.command,
|
|
3977
|
-
...cmd.args !== void 0 ? { args: [...cmd.args] } : {},
|
|
3978
|
-
...cmd.env !== void 0 ? { env: cmd.env } : {},
|
|
3979
|
-
projectRoot: opts.projectRoot,
|
|
3980
|
-
timeoutMs: opts.timeoutMs
|
|
3981
|
-
});
|
|
3982
|
-
} catch (err) {
|
|
3983
|
-
const reason2 = err instanceof Error ? err.message : String(err);
|
|
3984
|
-
checks.push({ name: "handshake", ok: false, detail: reason2 });
|
|
3985
|
-
return {
|
|
3986
|
-
agentId,
|
|
3987
|
-
status: "fail",
|
|
3988
|
-
checks,
|
|
3989
|
-
reason: reason2,
|
|
3990
|
-
handshakeMs: opts.now() - hsStart,
|
|
3991
|
-
durationMs: opts.now() - startedAt
|
|
3992
|
-
};
|
|
3993
|
-
}
|
|
3994
|
-
const handshakeMs = opts.now() - hsStart;
|
|
3995
|
-
const agentInfo = session.getAgentInfo() ?? void 0;
|
|
3996
|
-
checks.push({
|
|
3997
|
-
name: "handshake",
|
|
3998
|
-
ok: true,
|
|
3999
|
-
detail: agentInfo ? `${agentInfo.name} ${agentInfo.version}` : void 0
|
|
4345
|
+
return lines.join("\n");
|
|
4346
|
+
}
|
|
4347
|
+
|
|
4348
|
+
// src/integration/run-one-acp-task.ts
|
|
4349
|
+
import { SubagentBudget as SubagentBudget2 } from "@wrongstack/core/coordination";
|
|
4350
|
+
async function runOneAcpTask(opts) {
|
|
4351
|
+
const role = opts.role ?? "acp";
|
|
4352
|
+
const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
4353
|
+
const { runner, stop } = await makeACPSubagentRunnerWithStop({
|
|
4354
|
+
command: opts.command,
|
|
4355
|
+
...opts.args !== void 0 ? { args: opts.args } : {},
|
|
4356
|
+
...opts.env !== void 0 ? { env: opts.env } : {},
|
|
4357
|
+
...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
|
|
4358
|
+
...opts.projectRoot !== void 0 ? { projectRoot: opts.projectRoot } : {},
|
|
4359
|
+
role,
|
|
4360
|
+
timeoutMs,
|
|
4361
|
+
...opts.onProgress !== void 0 ? { onProgress: opts.onProgress } : {},
|
|
4362
|
+
...opts.permissionPolicy !== void 0 ? { permissionPolicy: opts.permissionPolicy } : {}
|
|
4000
4363
|
});
|
|
4001
|
-
let promptMs;
|
|
4002
|
-
let sample;
|
|
4003
|
-
let reason;
|
|
4004
4364
|
try {
|
|
4005
|
-
const
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
);
|
|
4010
|
-
promptMs = opts.now() - pStart;
|
|
4011
|
-
sample = res.text ? firstLine(res.text) : void 0;
|
|
4012
|
-
const promptOk = res.hasText && res.stopReason !== "refusal";
|
|
4013
|
-
checks.push({
|
|
4014
|
-
name: "prompt",
|
|
4015
|
-
ok: promptOk,
|
|
4016
|
-
detail: `stopReason=${res.stopReason}${res.hasText ? "" : ", no text"}`
|
|
4017
|
-
});
|
|
4018
|
-
const markerOk = res.text.includes(opts.marker);
|
|
4019
|
-
checks.push({
|
|
4020
|
-
name: "marker",
|
|
4021
|
-
ok: markerOk,
|
|
4022
|
-
detail: markerOk ? void 0 : "reply did not contain the token"
|
|
4365
|
+
const budget = new SubagentBudget2({
|
|
4366
|
+
timeoutMs,
|
|
4367
|
+
maxIterations: 2e3,
|
|
4368
|
+
maxToolCalls: 5e3
|
|
4023
4369
|
});
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
signal
|
|
4039
|
-
);
|
|
4040
|
-
fsOk = fsRes.text.includes(fileToken);
|
|
4041
|
-
if (!fsOk) fsDetail = "agent did not return the file contents (may not have used a read tool)";
|
|
4042
|
-
} catch (err) {
|
|
4043
|
-
fsDetail = err instanceof Error ? err.message : String(err);
|
|
4044
|
-
} finally {
|
|
4045
|
-
await fsp2.rm(filePath, { force: true }).catch(() => {
|
|
4046
|
-
});
|
|
4047
|
-
}
|
|
4048
|
-
checks.push({ name: "fs", ok: fsOk, detail: fsDetail });
|
|
4049
|
-
}
|
|
4050
|
-
} catch (err) {
|
|
4051
|
-
reason = err instanceof Error ? err.message : String(err);
|
|
4052
|
-
checks.push({ name: "prompt", ok: false, detail: reason });
|
|
4370
|
+
budget.start();
|
|
4371
|
+
const ctx = {
|
|
4372
|
+
subagentId: role,
|
|
4373
|
+
config: { id: role, name: role, role, provider: "acp", prompt: "" },
|
|
4374
|
+
budget,
|
|
4375
|
+
signal: opts.signal ?? new AbortController().signal,
|
|
4376
|
+
bridge: null
|
|
4377
|
+
};
|
|
4378
|
+
const result = await runner({ id: `acp-${role}`, description: opts.task }, ctx);
|
|
4379
|
+
return {
|
|
4380
|
+
result: result.result == null ? "" : String(result.result),
|
|
4381
|
+
iterations: result.iterations,
|
|
4382
|
+
toolCalls: result.toolCalls
|
|
4383
|
+
};
|
|
4053
4384
|
} finally {
|
|
4054
4385
|
try {
|
|
4055
|
-
await
|
|
4386
|
+
await stop();
|
|
4056
4387
|
} catch {
|
|
4057
4388
|
}
|
|
4058
4389
|
}
|
|
4059
|
-
const required = checks.filter((c) => c.name !== "fs" || opts.checkFs);
|
|
4060
|
-
const allReq = required.every((c) => c.ok);
|
|
4061
|
-
const handshakeOk = checks.find((c) => c.name === "handshake")?.ok === true;
|
|
4062
|
-
const status = allReq ? "pass" : handshakeOk ? "partial" : "fail";
|
|
4063
|
-
return {
|
|
4064
|
-
agentId,
|
|
4065
|
-
status,
|
|
4066
|
-
checks,
|
|
4067
|
-
...agentInfo ? { agentInfo } : {},
|
|
4068
|
-
handshakeMs,
|
|
4069
|
-
...promptMs !== void 0 ? { promptMs } : {},
|
|
4070
|
-
...sample ? { sample } : {},
|
|
4071
|
-
...reason ? { reason } : {},
|
|
4072
|
-
durationMs: opts.now() - startedAt
|
|
4073
|
-
};
|
|
4074
4390
|
}
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
const
|
|
4080
|
-
const
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4391
|
+
|
|
4392
|
+
// src/registry/acp-registry-fetch.ts
|
|
4393
|
+
var ACP_REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
|
|
4394
|
+
function currentPlatformKey(platform = process.platform, architecture = process.arch) {
|
|
4395
|
+
const os = platform === "win32" ? "windows" : platform === "darwin" ? "darwin" : "linux";
|
|
4396
|
+
const arch = architecture === "arm64" ? "aarch64" : architecture === "x64" ? "x86_64" : architecture;
|
|
4397
|
+
return `${os}-${arch}`;
|
|
4398
|
+
}
|
|
4399
|
+
function basename(cmd) {
|
|
4400
|
+
const cleaned = cmd.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
4401
|
+
return cleaned.slice(cleaned.lastIndexOf("/") + 1);
|
|
4402
|
+
}
|
|
4403
|
+
function mapRegistryEntry(entry, platformKey = currentPlatformKey()) {
|
|
4404
|
+
if (!entry || typeof entry.id !== "string" || entry.id.length === 0) return null;
|
|
4405
|
+
const dist = entry.distribution;
|
|
4406
|
+
let acp = null;
|
|
4407
|
+
if (dist?.npx?.package) {
|
|
4408
|
+
acp = { command: "npx", args: ["-y", dist.npx.package, ...dist.npx.args ?? []] };
|
|
4409
|
+
} else if (dist?.uvx?.package) {
|
|
4410
|
+
acp = { command: "uvx", args: [dist.uvx.package, ...dist.uvx.args ?? []] };
|
|
4411
|
+
} else if (dist?.binary) {
|
|
4412
|
+
const target = dist.binary[platformKey];
|
|
4413
|
+
if (target?.cmd) {
|
|
4414
|
+
acp = {
|
|
4415
|
+
command: basename(target.cmd),
|
|
4416
|
+
args: [...target.args ?? []],
|
|
4417
|
+
...target.env ? { env: target.env } : {}
|
|
4418
|
+
};
|
|
4089
4419
|
}
|
|
4090
4420
|
}
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
});
|
|
4104
|
-
let next = 0;
|
|
4105
|
-
const workers = [];
|
|
4106
|
-
const workerCount = Math.min(concurrency, runnable.length);
|
|
4107
|
-
for (let w = 0; w < workerCount; w++) {
|
|
4108
|
-
workers.push(
|
|
4109
|
-
(async () => {
|
|
4110
|
-
while (true) {
|
|
4111
|
-
const current = next++;
|
|
4112
|
-
if (current >= runnable.length) return;
|
|
4113
|
-
const { id, cmd, index } = runnable[current];
|
|
4114
|
-
if (opts.signal?.aborted) {
|
|
4115
|
-
results[index] = {
|
|
4116
|
-
agentId: id,
|
|
4117
|
-
status: "skipped",
|
|
4118
|
-
checks: [],
|
|
4119
|
-
durationMs: 0,
|
|
4120
|
-
reason: "aborted"
|
|
4121
|
-
};
|
|
4122
|
-
continue;
|
|
4123
|
-
}
|
|
4124
|
-
opts.onProgress?.(id, "start");
|
|
4125
|
-
const r = await benchOne(id, cmd, {
|
|
4126
|
-
projectRoot,
|
|
4127
|
-
timeoutMs,
|
|
4128
|
-
checkFs,
|
|
4129
|
-
marker,
|
|
4130
|
-
now,
|
|
4131
|
-
...opts.signal ? { signal: opts.signal } : {}
|
|
4132
|
-
});
|
|
4133
|
-
results[index] = r;
|
|
4134
|
-
opts.onProgress?.(id, "done", r);
|
|
4135
|
-
}
|
|
4136
|
-
})()
|
|
4137
|
-
);
|
|
4138
|
-
}
|
|
4139
|
-
await Promise.all(workers);
|
|
4140
|
-
const summary = { pass: 0, partial: 0, fail: 0, skipped: 0 };
|
|
4141
|
-
for (const r of results) summary[r.status]++;
|
|
4142
|
-
return { results, summary, totalDurationMs: now() - startMs };
|
|
4421
|
+
if (!acp) return null;
|
|
4422
|
+
const probeCmd = acp.command === "npx" ? "npx" : acp.command === "uvx" ? "uvx" : acp.command;
|
|
4423
|
+
return {
|
|
4424
|
+
id: entry.id,
|
|
4425
|
+
displayName: entry.name ?? entry.id,
|
|
4426
|
+
vendor: inferVendor(entry),
|
|
4427
|
+
probe: { command: probeCmd, args: ["--version"] },
|
|
4428
|
+
acp,
|
|
4429
|
+
supports: { loadSession: true, promptImages: true, terminal: true, fs: true },
|
|
4430
|
+
integration: "native",
|
|
4431
|
+
docs: entry.repository ?? entry.website ?? ""
|
|
4432
|
+
};
|
|
4143
4433
|
}
|
|
4144
|
-
function
|
|
4145
|
-
const
|
|
4146
|
-
|
|
4147
|
-
if (
|
|
4148
|
-
|
|
4149
|
-
|
|
4434
|
+
function inferVendor(entry) {
|
|
4435
|
+
const hay = `${entry.id} ${entry.name ?? ""} ${(entry.authors ?? []).join(" ")}`.toLowerCase();
|
|
4436
|
+
if (hay.includes("anthropic") || hay.includes("claude")) return "anthropic";
|
|
4437
|
+
if (hay.includes("google") || hay.includes("gemini")) return "google";
|
|
4438
|
+
if (hay.includes("openai") || hay.includes("codex")) return "openai";
|
|
4439
|
+
if (hay.includes("github") || hay.includes("copilot")) return "github";
|
|
4440
|
+
if (hay.includes("moonshot") || hay.includes("kimi")) return "moonshot";
|
|
4441
|
+
return "community";
|
|
4442
|
+
}
|
|
4443
|
+
async function fetchAcpRegistry(opts = {}) {
|
|
4444
|
+
const url = opts.url ?? ACP_REGISTRY_URL;
|
|
4445
|
+
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
4446
|
+
const controller = new AbortController();
|
|
4447
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4448
|
+
const onParentAbort = () => controller.abort();
|
|
4449
|
+
if (opts.signal) {
|
|
4450
|
+
if (opts.signal.aborted) controller.abort();
|
|
4451
|
+
else opts.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
4150
4452
|
}
|
|
4151
|
-
|
|
4152
|
-
const
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4453
|
+
try {
|
|
4454
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
4455
|
+
if (!res.ok) {
|
|
4456
|
+
throw new Error(`ACP registry fetch failed: HTTP ${res.status}`);
|
|
4457
|
+
}
|
|
4458
|
+
const body = await res.json();
|
|
4459
|
+
const rawAgents = Array.isArray(body) ? body : Array.isArray(body?.agents) ? body.agents : null;
|
|
4460
|
+
if (!rawAgents) {
|
|
4461
|
+
throw new Error("ACP registry response had no agents array");
|
|
4462
|
+
}
|
|
4463
|
+
const platformKey = opts.platformKey ?? currentPlatformKey();
|
|
4464
|
+
const agents = [];
|
|
4465
|
+
for (const raw of rawAgents) {
|
|
4466
|
+
const mapped = mapRegistryEntry(raw, platformKey);
|
|
4467
|
+
if (mapped) agents.push(mapped);
|
|
4468
|
+
}
|
|
4469
|
+
return { fetchedAt: opts.now ?? (/* @__PURE__ */ new Date()).toISOString(), agents };
|
|
4470
|
+
} finally {
|
|
4471
|
+
clearTimeout(timer);
|
|
4472
|
+
opts.signal?.removeEventListener("abort", onParentAbort);
|
|
4158
4473
|
}
|
|
4159
|
-
const { pass, partial, fail, skipped } = result.summary;
|
|
4160
|
-
lines.push("");
|
|
4161
|
-
lines.push(
|
|
4162
|
-
`Bench summary: ${pass} pass, ${partial} partial, ${fail} fail, ${skipped} skipped. (${result.totalDurationMs}ms total)`
|
|
4163
|
-
);
|
|
4164
|
-
return lines.join("\n");
|
|
4165
4474
|
}
|
|
4166
4475
|
export {
|
|
4167
4476
|
ACPProtocolHandler,
|
|
@@ -4169,6 +4478,8 @@ export {
|
|
|
4169
4478
|
ACPSessionError,
|
|
4170
4479
|
ACPToolsRegistry,
|
|
4171
4480
|
ACP_AGENT_COMMANDS,
|
|
4481
|
+
ACP_PACKAGE_VERSION,
|
|
4482
|
+
ACP_PROTOCOL_VERSION,
|
|
4172
4483
|
ACP_REGISTRY_URL,
|
|
4173
4484
|
AGENTS_CATALOG,
|
|
4174
4485
|
ClientTransport,
|
|
@@ -4181,6 +4492,7 @@ export {
|
|
|
4181
4492
|
ToolTranslator,
|
|
4182
4493
|
WebSocketClientTransport,
|
|
4183
4494
|
WrongStackACPServer,
|
|
4495
|
+
assertNeverSessionUpdate,
|
|
4184
4496
|
audioContent,
|
|
4185
4497
|
defaultEnsembleCmdResolver,
|
|
4186
4498
|
defaultPermissionPolicy,
|
|
@@ -4190,6 +4502,7 @@ export {
|
|
|
4190
4502
|
makeACPSubagentRunner,
|
|
4191
4503
|
makeACPSubagentRunnerWithStop,
|
|
4192
4504
|
makePermissionPolicy,
|
|
4505
|
+
makeTrustBoundaryPermissionPolicy,
|
|
4193
4506
|
mapRegistryEntry,
|
|
4194
4507
|
probeAcpAgent,
|
|
4195
4508
|
probeAcpAgents,
|
|
@@ -4200,6 +4513,7 @@ export {
|
|
|
4200
4513
|
runAcpBench,
|
|
4201
4514
|
runEnsemble,
|
|
4202
4515
|
runOneAcpTask,
|
|
4203
|
-
textContent
|
|
4516
|
+
textContent,
|
|
4517
|
+
toTrustBoundaryRequest
|
|
4204
4518
|
};
|
|
4205
4519
|
//# sourceMappingURL=index.js.map
|