@parlor/agent-host 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +16 -2
- package/dist/acp.d.ts +165 -0
- package/dist/acp.d.ts.map +1 -0
- package/dist/acp.js +626 -0
- package/dist/acp.js.map +1 -0
- package/dist/adapter-launch.d.ts +7 -0
- package/dist/adapter-launch.d.ts.map +1 -0
- package/dist/adapter-launch.js +35 -0
- package/dist/adapter-launch.js.map +1 -0
- package/dist/agents.d.ts +32 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +132 -0
- package/dist/agents.js.map +1 -0
- package/dist/assertion.d.ts +9 -0
- package/dist/assertion.d.ts.map +1 -0
- package/dist/assertion.js +22 -0
- package/dist/assertion.js.map +1 -0
- package/dist/control.d.ts +106 -0
- package/dist/control.d.ts.map +1 -0
- package/dist/control.js +32 -0
- package/dist/control.js.map +1 -0
- package/dist/daemon.d.ts +50 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +543 -0
- package/dist/daemon.js.map +1 -0
- package/dist/dev-host.d.ts +5 -0
- package/dist/dev-host.d.ts.map +1 -0
- package/dist/dev-host.js +29 -0
- package/dist/dev-host.js.map +1 -0
- package/dist/device-list.d.ts +6 -0
- package/dist/device-list.d.ts.map +1 -0
- package/dist/device-list.js +30 -0
- package/dist/device-list.js.map +1 -0
- package/dist/embedded.d.ts +6 -0
- package/dist/embedded.d.ts.map +1 -0
- package/dist/embedded.js +3 -0
- package/dist/embedded.js.map +1 -0
- package/dist/fold.d.ts +38 -0
- package/dist/fold.d.ts.map +1 -0
- package/dist/fold.js +165 -0
- package/dist/fold.js.map +1 -0
- package/dist/grants.d.ts +54 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +157 -0
- package/dist/grants.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/json-file.d.ts +6 -0
- package/dist/json-file.d.ts.map +1 -0
- package/dist/json-file.js +35 -0
- package/dist/json-file.js.map +1 -0
- package/dist/lines.d.ts +5 -0
- package/dist/lines.d.ts.map +1 -0
- package/dist/lines.js +9 -0
- package/dist/lines.js.map +1 -0
- package/dist/link-device.d.ts +28 -0
- package/dist/link-device.d.ts.map +1 -0
- package/dist/link-device.js +40 -0
- package/dist/link-device.js.map +1 -0
- package/dist/link.d.ts +52 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +277 -0
- package/dist/link.js.map +1 -0
- package/dist/lock.d.ts +7 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +105 -0
- package/dist/lock.js.map +1 -0
- package/dist/log.d.ts +3 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +18 -0
- package/dist/log.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +520 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp-proxy.d.ts +33 -0
- package/dist/mcp-proxy.d.ts.map +1 -0
- package/dist/mcp-proxy.js +317 -0
- package/dist/mcp-proxy.js.map +1 -0
- package/dist/naming.d.ts +7 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/naming.js +42 -0
- package/dist/naming.js.map +1 -0
- package/dist/pairing.d.ts +68 -0
- package/dist/pairing.d.ts.map +1 -0
- package/dist/pairing.js +312 -0
- package/dist/pairing.js.map +1 -0
- package/dist/paths.d.ts +19 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +23 -0
- package/dist/paths.js.map +1 -0
- package/dist/policy.d.ts +37 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +60 -0
- package/dist/policy.js.map +1 -0
- package/dist/pool.d.ts +34 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +112 -0
- package/dist/pool.js.map +1 -0
- package/dist/prompter.d.ts +33 -0
- package/dist/prompter.d.ts.map +1 -0
- package/dist/prompter.js +98 -0
- package/dist/prompter.js.map +1 -0
- package/dist/release-main.d.ts +2 -0
- package/dist/release-main.d.ts.map +1 -0
- package/dist/release-main.js +50 -0
- package/dist/release-main.js.map +1 -0
- package/dist/release.d.ts +22 -0
- package/dist/release.d.ts.map +1 -0
- package/dist/release.js +81 -0
- package/dist/release.js.map +1 -0
- package/dist/revoke.d.ts +10 -0
- package/dist/revoke.d.ts.map +1 -0
- package/dist/revoke.js +23 -0
- package/dist/revoke.js.map +1 -0
- package/dist/self.d.ts +7 -0
- package/dist/self.d.ts.map +1 -0
- package/dist/self.js +10 -0
- package/dist/self.js.map +1 -0
- package/dist/service.d.ts +97 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +205 -0
- package/dist/service.js.map +1 -0
- package/dist/shell-path.d.ts +10 -0
- package/dist/shell-path.d.ts.map +1 -0
- package/dist/shell-path.js +64 -0
- package/dist/shell-path.js.map +1 -0
- package/dist/skeleton-kit.d.ts +8 -0
- package/dist/skeleton-kit.d.ts.map +1 -0
- package/dist/skeleton-kit.js +143 -0
- package/dist/skeleton-kit.js.map +1 -0
- package/dist/spec-kit.d.ts +102 -0
- package/dist/spec-kit.d.ts.map +1 -0
- package/dist/spec-kit.js +372 -0
- package/dist/spec-kit.js.map +1 -0
- package/dist/testing/fake-agent.d.ts +65 -0
- package/dist/testing/fake-agent.d.ts.map +1 -0
- package/dist/testing/fake-agent.js +238 -0
- package/dist/testing/fake-agent.js.map +1 -0
- package/dist/trust.d.ts +20 -0
- package/dist/trust.d.ts.map +1 -0
- package/dist/trust.js +27 -0
- package/dist/trust.js.map +1 -0
- package/dist/turn.d.ts +32 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +650 -0
- package/dist/turn.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/package.json +57 -3
package/dist/acp.js
ADDED
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { Readable, Writable } from "node:stream";
|
|
3
|
+
import { agent as acpAgent, client as acpClient, methods, ndJsonStream, PROTOCOL_VERSION, RequestError, } from "@agentclientprotocol/sdk";
|
|
4
|
+
import { commandOf, launchEnvironment } from "./adapter-launch.js";
|
|
5
|
+
import { logEvent } from "./log.js";
|
|
6
|
+
/** `openSession` would not run the session; `leftOpen` when the agent still holds it. */
|
|
7
|
+
export class SessionRefused extends Error {
|
|
8
|
+
leftOpen;
|
|
9
|
+
name = "SessionRefused";
|
|
10
|
+
constructor(message, leftOpen, options) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.leftOpen = leftOpen;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const STDERR_TAIL_BYTES = 4_096;
|
|
16
|
+
const KILL_GRACE_MS = 2_000;
|
|
17
|
+
const MAX_DETAIL_CHARS = 4_000;
|
|
18
|
+
const AUTH_REQUIRED = -32000;
|
|
19
|
+
const MCP_PREFIX = "mcp__";
|
|
20
|
+
const ASK_MODES_BY_PREFERENCE = ["default", "workspace-write", "read-only"];
|
|
21
|
+
const STRICT_ASK_MODES_BY_PREFERENCE = ["default", "read-only", "workspace-write"];
|
|
22
|
+
const isFields = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
const TOOL_KINDS = new Set([
|
|
24
|
+
"read",
|
|
25
|
+
"edit",
|
|
26
|
+
"delete",
|
|
27
|
+
"move",
|
|
28
|
+
"search",
|
|
29
|
+
"execute",
|
|
30
|
+
"think",
|
|
31
|
+
"fetch",
|
|
32
|
+
"switch_mode",
|
|
33
|
+
"other",
|
|
34
|
+
]);
|
|
35
|
+
function toolKindOf(kind) {
|
|
36
|
+
return kind && TOOL_KINDS.has(kind) ? kind : "other";
|
|
37
|
+
}
|
|
38
|
+
function stopReasonOf(reason) {
|
|
39
|
+
return reason;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Which MCP server serves a tool call, by the agents' own conventions: Claude Code's
|
|
43
|
+
* `mcp__<server>__<tool>` name, Codex's `mcp.<server>.<tool>` title and raw input.
|
|
44
|
+
*/
|
|
45
|
+
function mcpRefOf(call) {
|
|
46
|
+
const claude = isFields(call._meta?.claudeCode) ? call._meta.claudeCode : undefined;
|
|
47
|
+
const name = typeof claude?.toolName === "string" ? claude.toolName : call.name;
|
|
48
|
+
if (name?.startsWith(MCP_PREFIX)) {
|
|
49
|
+
const rest = name.slice(MCP_PREFIX.length);
|
|
50
|
+
const separator = rest.indexOf("__");
|
|
51
|
+
if (separator > 0 && rest.length > separator + 2) {
|
|
52
|
+
return { server: rest.slice(0, separator), tool: rest.slice(separator + 2) };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const raw = call.rawInput;
|
|
56
|
+
if (call.title?.startsWith("mcp.") &&
|
|
57
|
+
isFields(raw) &&
|
|
58
|
+
typeof raw.server === "string" &&
|
|
59
|
+
typeof raw.tool === "string") {
|
|
60
|
+
return { server: raw.server, tool: raw.tool };
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
function readableTitle(title, mcp) {
|
|
65
|
+
const keys = [
|
|
66
|
+
`${MCP_PREFIX}${mcp.server}__${mcp.tool}`,
|
|
67
|
+
`mcp.${mcp.server}.${mcp.tool}`,
|
|
68
|
+
];
|
|
69
|
+
return keys.includes(title) ? mcp.tool : title;
|
|
70
|
+
}
|
|
71
|
+
function detailOf(update) {
|
|
72
|
+
const texts = (update.content ?? []).flatMap((content) => content.type === "content" && content.content.type === "text"
|
|
73
|
+
? [content.content.text]
|
|
74
|
+
: []);
|
|
75
|
+
const text = texts.length > 0 ? texts.join("\n") : update.rawOutput;
|
|
76
|
+
return typeof text === "string" && text.trim() !== ""
|
|
77
|
+
? text.slice(0, MAX_DETAIL_CHARS)
|
|
78
|
+
: undefined;
|
|
79
|
+
}
|
|
80
|
+
function textOf(content) {
|
|
81
|
+
return content.type === "text" ? content.text : undefined;
|
|
82
|
+
}
|
|
83
|
+
function updateOf(update) {
|
|
84
|
+
switch (update.sessionUpdate) {
|
|
85
|
+
case "agent_message_chunk": {
|
|
86
|
+
const text = textOf(update.content);
|
|
87
|
+
return text === undefined ? undefined : { kind: "text", text };
|
|
88
|
+
}
|
|
89
|
+
case "agent_thought_chunk": {
|
|
90
|
+
const text = textOf(update.content);
|
|
91
|
+
return text === undefined ? undefined : { kind: "thought", text };
|
|
92
|
+
}
|
|
93
|
+
case "tool_call": {
|
|
94
|
+
const mcp = mcpRefOf(update);
|
|
95
|
+
return {
|
|
96
|
+
kind: "tool_start",
|
|
97
|
+
toolCallId: update.toolCallId,
|
|
98
|
+
title: mcp ? readableTitle(update.title, mcp) : update.title,
|
|
99
|
+
toolKind: toolKindOf(update.kind),
|
|
100
|
+
mcp,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
case "tool_call_update":
|
|
104
|
+
return {
|
|
105
|
+
kind: "tool_update",
|
|
106
|
+
toolCallId: update.toolCallId,
|
|
107
|
+
title: update.title ?? undefined,
|
|
108
|
+
status: update.status ?? undefined,
|
|
109
|
+
detail: detailOf(update),
|
|
110
|
+
};
|
|
111
|
+
case "plan":
|
|
112
|
+
return {
|
|
113
|
+
kind: "plan",
|
|
114
|
+
entries: update.entries.map((entry) => ({
|
|
115
|
+
content: entry.content,
|
|
116
|
+
status: entry.status,
|
|
117
|
+
})),
|
|
118
|
+
};
|
|
119
|
+
case "current_mode_update":
|
|
120
|
+
return { kind: "mode", modeId: update.currentModeId };
|
|
121
|
+
default:
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function sessionUpdateOf(update) {
|
|
126
|
+
switch (update.kind) {
|
|
127
|
+
case "text":
|
|
128
|
+
return {
|
|
129
|
+
sessionUpdate: "agent_message_chunk",
|
|
130
|
+
content: { type: "text", text: update.text },
|
|
131
|
+
};
|
|
132
|
+
case "thought":
|
|
133
|
+
return {
|
|
134
|
+
sessionUpdate: "agent_thought_chunk",
|
|
135
|
+
content: { type: "text", text: update.text },
|
|
136
|
+
};
|
|
137
|
+
case "tool_start":
|
|
138
|
+
return {
|
|
139
|
+
sessionUpdate: "tool_call",
|
|
140
|
+
toolCallId: update.toolCallId,
|
|
141
|
+
title: update.title,
|
|
142
|
+
kind: update.toolKind,
|
|
143
|
+
status: "pending",
|
|
144
|
+
...(update.mcp ? { _meta: claudeToolName(update.mcp) } : {}),
|
|
145
|
+
};
|
|
146
|
+
case "tool_update":
|
|
147
|
+
return {
|
|
148
|
+
sessionUpdate: "tool_call_update",
|
|
149
|
+
toolCallId: update.toolCallId,
|
|
150
|
+
...(update.title !== undefined ? { title: update.title } : {}),
|
|
151
|
+
...(update.status !== undefined ? { status: update.status } : {}),
|
|
152
|
+
...(update.detail !== undefined
|
|
153
|
+
? {
|
|
154
|
+
content: [
|
|
155
|
+
{ type: "content", content: { type: "text", text: update.detail } },
|
|
156
|
+
],
|
|
157
|
+
}
|
|
158
|
+
: {}),
|
|
159
|
+
};
|
|
160
|
+
case "plan":
|
|
161
|
+
return {
|
|
162
|
+
sessionUpdate: "plan",
|
|
163
|
+
entries: update.entries.map((entry) => ({ ...entry, priority: "medium" })),
|
|
164
|
+
};
|
|
165
|
+
case "mode":
|
|
166
|
+
return { sessionUpdate: "current_mode_update", currentModeId: update.modeId };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const claudeToolName = (mcp) => ({
|
|
170
|
+
claudeCode: { toolName: `${MCP_PREFIX}${mcp.server}__${mcp.tool}` },
|
|
171
|
+
});
|
|
172
|
+
function commandIn(rawInput) {
|
|
173
|
+
return isFields(rawInput) && typeof rawInput.command === "string"
|
|
174
|
+
? rawInput.command
|
|
175
|
+
: undefined;
|
|
176
|
+
}
|
|
177
|
+
function permissionQuestion(askedTitle, callDetail) {
|
|
178
|
+
const detail = callDetail?.trim() ?? "";
|
|
179
|
+
if (detail === "" || askedTitle.includes(detail))
|
|
180
|
+
return askedTitle;
|
|
181
|
+
if (askedTitle.trim() === "" || detail.includes(askedTitle))
|
|
182
|
+
return detail;
|
|
183
|
+
return `${askedTitle}: ${detail}`;
|
|
184
|
+
}
|
|
185
|
+
function permissionAskOf(request, known) {
|
|
186
|
+
const call = request.toolCall;
|
|
187
|
+
const mcp = mcpRefOf(call) ?? known?.mcp;
|
|
188
|
+
const title = call.title ?? "";
|
|
189
|
+
return {
|
|
190
|
+
kind: "permission",
|
|
191
|
+
toolCallId: call.toolCallId,
|
|
192
|
+
title: permissionQuestion(mcp ? readableTitle(title, mcp) : title, known?.title ?? commandIn(call.rawInput)),
|
|
193
|
+
toolKind: toolKindOf(call.kind),
|
|
194
|
+
mcp,
|
|
195
|
+
options: request.options.map((option) => ({
|
|
196
|
+
id: option.optionId,
|
|
197
|
+
label: option.name,
|
|
198
|
+
effect: option.kind,
|
|
199
|
+
})),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function permissionAnswerOf(request, answer) {
|
|
203
|
+
const selected = (optionId) => ({
|
|
204
|
+
outcome: { outcome: "selected", optionId },
|
|
205
|
+
});
|
|
206
|
+
if (answer.kind === "selected")
|
|
207
|
+
return selected(answer.optionId);
|
|
208
|
+
if (answer.kind === "declined") {
|
|
209
|
+
const reject = request.options.find((option) => option.kind === "reject_once") ??
|
|
210
|
+
request.options.find((option) => option.kind === "reject_always");
|
|
211
|
+
if (reject)
|
|
212
|
+
return selected(reject.optionId);
|
|
213
|
+
}
|
|
214
|
+
return { outcome: { outcome: "cancelled" } };
|
|
215
|
+
}
|
|
216
|
+
function elicitationAnswerOf(answer) {
|
|
217
|
+
switch (answer.kind) {
|
|
218
|
+
case "values":
|
|
219
|
+
return { action: "accept", content: { ...answer.values } };
|
|
220
|
+
case "selected":
|
|
221
|
+
return { action: "accept", content: {} };
|
|
222
|
+
case "declined":
|
|
223
|
+
return { action: "decline" };
|
|
224
|
+
case "cancelled":
|
|
225
|
+
return { action: "cancel" };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function stdioServerOf(server) {
|
|
229
|
+
return {
|
|
230
|
+
name: server.name,
|
|
231
|
+
command: server.command,
|
|
232
|
+
args: [...server.args],
|
|
233
|
+
env: Object.entries(server.env).map(([name, value]) => ({ name, value })),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function stdioServersOf(servers) {
|
|
237
|
+
return (servers ?? []).flatMap((server) => "command" in server
|
|
238
|
+
? [
|
|
239
|
+
{
|
|
240
|
+
name: server.name,
|
|
241
|
+
command: server.command,
|
|
242
|
+
args: server.args,
|
|
243
|
+
env: Object.fromEntries(server.env.map((entry) => [entry.name, entry.value])),
|
|
244
|
+
},
|
|
245
|
+
]
|
|
246
|
+
: []);
|
|
247
|
+
}
|
|
248
|
+
/** Takes `session/update` off the wire in order, before the SDK sees the reply after it. */
|
|
249
|
+
function tapUpdates(stream, deliver) {
|
|
250
|
+
return {
|
|
251
|
+
writable: stream.writable,
|
|
252
|
+
readable: stream.readable.pipeThrough(new TransformStream({
|
|
253
|
+
transform(message, controller) {
|
|
254
|
+
const fields = message;
|
|
255
|
+
if (isFields(fields) &&
|
|
256
|
+
fields.method === methods.client.session.update &&
|
|
257
|
+
!("id" in fields)) {
|
|
258
|
+
deliver(fields.params);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
controller.enqueue(message);
|
|
262
|
+
},
|
|
263
|
+
})),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function tapSends(stream) {
|
|
267
|
+
const writer = stream.writable.getWriter();
|
|
268
|
+
return {
|
|
269
|
+
readable: stream.readable,
|
|
270
|
+
writable: new WritableStream({
|
|
271
|
+
write(message) {
|
|
272
|
+
const fields = message;
|
|
273
|
+
if (isFields(fields) && typeof fields.method === "string") {
|
|
274
|
+
logEvent("acp_send", { method: fields.method });
|
|
275
|
+
}
|
|
276
|
+
return writer.write(message);
|
|
277
|
+
},
|
|
278
|
+
close: () => writer.close(),
|
|
279
|
+
abort: (reason) => writer.abort(reason),
|
|
280
|
+
}),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function askingModeOf(modes, strict) {
|
|
284
|
+
const available = new Set(modes.availableModes.map((mode) => mode.id));
|
|
285
|
+
if (strict) {
|
|
286
|
+
available.add(modes.currentModeId);
|
|
287
|
+
return STRICT_ASK_MODES_BY_PREFERENCE.find((mode) => available.has(mode));
|
|
288
|
+
}
|
|
289
|
+
if (ASK_MODES_BY_PREFERENCE.includes(modes.currentModeId))
|
|
290
|
+
return modes.currentModeId;
|
|
291
|
+
return ASK_MODES_BY_PREFERENCE.find((mode) => available.has(mode));
|
|
292
|
+
}
|
|
293
|
+
function sessionModesOf(modes) {
|
|
294
|
+
return modes
|
|
295
|
+
? {
|
|
296
|
+
currentModeId: modes.current,
|
|
297
|
+
availableModes: modes.available.map((id) => ({ id, name: id })),
|
|
298
|
+
}
|
|
299
|
+
: undefined;
|
|
300
|
+
}
|
|
301
|
+
function signalExit(pid, signal) {
|
|
302
|
+
if (pid === undefined)
|
|
303
|
+
return;
|
|
304
|
+
try {
|
|
305
|
+
process.kill(-pid, signal);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
if (error.code !== "ESRCH")
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/** ACP v1 over stdio. */
|
|
313
|
+
export function acpLauncher() {
|
|
314
|
+
return {
|
|
315
|
+
launch: async (profile, options) => {
|
|
316
|
+
const { command, args } = commandOf(profile);
|
|
317
|
+
const child = spawn(command, [...args], {
|
|
318
|
+
cwd: options.cwd,
|
|
319
|
+
env: await launchEnvironment(profile, options.env),
|
|
320
|
+
detached: true,
|
|
321
|
+
shell: false,
|
|
322
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
323
|
+
});
|
|
324
|
+
let stderr = "";
|
|
325
|
+
child.stderr.setEncoding("utf8");
|
|
326
|
+
child.stderr.on("data", (chunk) => {
|
|
327
|
+
stderr = (stderr + chunk).slice(-STDERR_TAIL_BYTES);
|
|
328
|
+
});
|
|
329
|
+
let killing = false;
|
|
330
|
+
const exited = new Promise((resolve) => {
|
|
331
|
+
child.once("error", (error) => resolve({
|
|
332
|
+
kind: "exited",
|
|
333
|
+
code: null,
|
|
334
|
+
stderrTail: `${stderr}${error.message}`,
|
|
335
|
+
}));
|
|
336
|
+
child.once("exit", (code) => resolve(killing ? { kind: "killed" } : { kind: "exited", code, stderrTail: stderr }));
|
|
337
|
+
});
|
|
338
|
+
const kill = async () => {
|
|
339
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
340
|
+
return;
|
|
341
|
+
killing = true;
|
|
342
|
+
signalExit(child.pid, "SIGTERM");
|
|
343
|
+
const force = setTimeout(() => signalExit(child.pid, "SIGKILL"), KILL_GRACE_MS);
|
|
344
|
+
await exited;
|
|
345
|
+
clearTimeout(force);
|
|
346
|
+
};
|
|
347
|
+
if (options.signal.aborted)
|
|
348
|
+
void kill();
|
|
349
|
+
options.signal.addEventListener("abort", () => void kill(), { once: true });
|
|
350
|
+
const sessions = new Map();
|
|
351
|
+
const deliver = (notification) => {
|
|
352
|
+
const state = sessions.get(notification.sessionId);
|
|
353
|
+
if (!state)
|
|
354
|
+
return;
|
|
355
|
+
const { update } = notification;
|
|
356
|
+
const mapped = updateOf(update);
|
|
357
|
+
if (!mapped)
|
|
358
|
+
return;
|
|
359
|
+
if (mapped.kind === "tool_start") {
|
|
360
|
+
state.calls.set(mapped.toolCallId, { title: mapped.title, mcp: mapped.mcp });
|
|
361
|
+
}
|
|
362
|
+
else if (mapped.kind === "tool_update" && mapped.title) {
|
|
363
|
+
const known = state.calls.get(mapped.toolCallId);
|
|
364
|
+
state.calls.set(mapped.toolCallId, { ...known, title: mapped.title });
|
|
365
|
+
}
|
|
366
|
+
if (mapped.kind === "mode" && state.pinned && mapped.modeId !== state.pinned) {
|
|
367
|
+
void context
|
|
368
|
+
.request(methods.agent.session.setMode, {
|
|
369
|
+
sessionId: notification.sessionId,
|
|
370
|
+
modeId: state.pinned,
|
|
371
|
+
})
|
|
372
|
+
.catch(() => undefined);
|
|
373
|
+
}
|
|
374
|
+
state.handlers?.update(mapped);
|
|
375
|
+
};
|
|
376
|
+
const stream = tapUpdates(tapSends(ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout))), deliver);
|
|
377
|
+
const ask = async (sessionId, question) => {
|
|
378
|
+
const handlers = sessionId === undefined ? undefined : sessions.get(sessionId)?.handlers;
|
|
379
|
+
return handlers ? handlers.ask(question) : { kind: "cancelled" };
|
|
380
|
+
};
|
|
381
|
+
const connection = acpClient({ name: "parlor-host" })
|
|
382
|
+
.onRequest(methods.client.session.requestPermission, async ({ params }) => {
|
|
383
|
+
const known = sessions
|
|
384
|
+
.get(params.sessionId)
|
|
385
|
+
?.calls.get(params.toolCall.toolCallId);
|
|
386
|
+
const answer = await ask(params.sessionId, permissionAskOf(params, known));
|
|
387
|
+
return permissionAnswerOf(params, answer);
|
|
388
|
+
})
|
|
389
|
+
.onRequest(methods.client.elicitation.create, async ({ params }) => {
|
|
390
|
+
if (params.mode !== "form")
|
|
391
|
+
return { action: "cancel" };
|
|
392
|
+
const sessionId = "sessionId" in params && typeof params.sessionId === "string"
|
|
393
|
+
? params.sessionId
|
|
394
|
+
: undefined;
|
|
395
|
+
const schema = isFields(params.requestedSchema) ? params.requestedSchema : {};
|
|
396
|
+
const answer = await ask(sessionId, {
|
|
397
|
+
kind: "question",
|
|
398
|
+
message: params.message,
|
|
399
|
+
schema,
|
|
400
|
+
});
|
|
401
|
+
return elicitationAnswerOf(answer);
|
|
402
|
+
})
|
|
403
|
+
.connect(stream);
|
|
404
|
+
const context = connection.agent;
|
|
405
|
+
exited.then(() => connection.close(), () => undefined);
|
|
406
|
+
let initialized;
|
|
407
|
+
try {
|
|
408
|
+
initialized = await context.request(methods.agent.initialize, {
|
|
409
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
410
|
+
clientCapabilities: { elicitation: { form: {} } },
|
|
411
|
+
clientInfo: { name: "parlor-host", version: "0.0.0" },
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
await kill();
|
|
416
|
+
const exit = await exited;
|
|
417
|
+
const why = exit.kind === "exited" && exit.stderrTail ? `: ${exit.stderrTail.trim()}` : "";
|
|
418
|
+
throw new Error(`${profile.name} did not start${why}`, { cause: error });
|
|
419
|
+
}
|
|
420
|
+
const agentCapabilities = initialized.agentCapabilities ?? {};
|
|
421
|
+
const capabilities = {
|
|
422
|
+
resume: Boolean(agentCapabilities.sessionCapabilities?.resume),
|
|
423
|
+
close: Boolean(agentCapabilities.sessionCapabilities?.close),
|
|
424
|
+
mcpHttp: agentCapabilities.mcpCapabilities?.http === true,
|
|
425
|
+
ready: true,
|
|
426
|
+
};
|
|
427
|
+
const loginProblem = (error) => error instanceof RequestError && error.code === AUTH_REQUIRED
|
|
428
|
+
? new Error(`${profile.name} is not logged in on this computer: run \`${profile.binary}\` in a terminal to log in.`, { cause: error })
|
|
429
|
+
: error;
|
|
430
|
+
const open = async (input) => {
|
|
431
|
+
const mcpServers = input.mcpServers.map(stdioServerOf);
|
|
432
|
+
if (input.resume && capabilities.resume) {
|
|
433
|
+
try {
|
|
434
|
+
const resumed = await context.request(methods.agent.session.resume, {
|
|
435
|
+
sessionId: input.resume,
|
|
436
|
+
cwd: input.cwd,
|
|
437
|
+
mcpServers,
|
|
438
|
+
});
|
|
439
|
+
return { id: input.resume, modes: resumed.modes, resumed: true };
|
|
440
|
+
}
|
|
441
|
+
catch { }
|
|
442
|
+
}
|
|
443
|
+
const created = await context.request(methods.agent.session.new, {
|
|
444
|
+
cwd: input.cwd,
|
|
445
|
+
mcpServers,
|
|
446
|
+
});
|
|
447
|
+
return { id: created.sessionId, modes: created.modes, resumed: false };
|
|
448
|
+
};
|
|
449
|
+
return {
|
|
450
|
+
capabilities,
|
|
451
|
+
exited,
|
|
452
|
+
kill,
|
|
453
|
+
openSession: async (input) => {
|
|
454
|
+
let opened;
|
|
455
|
+
try {
|
|
456
|
+
opened = await open(input);
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
throw loginProblem(error);
|
|
460
|
+
}
|
|
461
|
+
const sessionId = opened.id;
|
|
462
|
+
const close = async () => {
|
|
463
|
+
sessions.delete(sessionId);
|
|
464
|
+
if (!capabilities.close)
|
|
465
|
+
return false;
|
|
466
|
+
return context.request(methods.agent.session.close, { sessionId }).then(() => true, () => false);
|
|
467
|
+
};
|
|
468
|
+
const modes = input.pinAskMode ? opened.modes : undefined;
|
|
469
|
+
const pinned = modes
|
|
470
|
+
? askingModeOf(modes, input.strictAsk === true)
|
|
471
|
+
: undefined;
|
|
472
|
+
if (modes && pinned === undefined) {
|
|
473
|
+
throw new SessionRefused(`${profile.name} offers no mode that asks before it acts, so it cannot take a turn from an app.`, !(await close()));
|
|
474
|
+
}
|
|
475
|
+
const state = { handlers: undefined, calls: new Map(), pinned };
|
|
476
|
+
sessions.set(sessionId, state);
|
|
477
|
+
if (pinned !== undefined && pinned !== modes?.currentModeId) {
|
|
478
|
+
try {
|
|
479
|
+
await context.request(methods.agent.session.setMode, {
|
|
480
|
+
sessionId,
|
|
481
|
+
modeId: pinned,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
throw new SessionRefused(`${profile.name} did not switch to a mode that asks before it acts.`, !(await close()), { cause: error });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const session = {
|
|
489
|
+
id: sessionId,
|
|
490
|
+
resumed: opened.resumed,
|
|
491
|
+
prompt: async (blocks, handlers) => {
|
|
492
|
+
if (state.handlers)
|
|
493
|
+
throw new Error("This session is already answering a prompt.");
|
|
494
|
+
state.handlers = handlers;
|
|
495
|
+
try {
|
|
496
|
+
const request = {
|
|
497
|
+
sessionId,
|
|
498
|
+
prompt: blocks.map((block) => ({ type: "text", text: block.text })),
|
|
499
|
+
};
|
|
500
|
+
const response = await context.request(methods.agent.session.prompt, request);
|
|
501
|
+
return stopReasonOf(response.stopReason);
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
state.handlers = undefined;
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
cancel: () => context
|
|
508
|
+
.notify(methods.agent.session.cancel, { sessionId })
|
|
509
|
+
.catch(() => undefined),
|
|
510
|
+
close: async () => {
|
|
511
|
+
await close();
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
return session;
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/** Serves `handlers` as an ACP agent on `io`, until the other side hangs up. */
|
|
521
|
+
export function serveAcpAgent(handlers, io) {
|
|
522
|
+
const running = new Map();
|
|
523
|
+
const aliases = new Map();
|
|
524
|
+
const idOf = (sessionId) => aliases.get(sessionId) ?? sessionId;
|
|
525
|
+
const app = acpAgent({ name: "parlor-fake-agent" })
|
|
526
|
+
.onRequest(methods.agent.initialize, () => ({
|
|
527
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
528
|
+
agentCapabilities: {
|
|
529
|
+
loadSession: false,
|
|
530
|
+
mcpCapabilities: { http: handlers.capabilities.mcpHttp, sse: false },
|
|
531
|
+
sessionCapabilities: {
|
|
532
|
+
...(handlers.capabilities.resume ? { resume: {} } : {}),
|
|
533
|
+
...(handlers.capabilities.close ? { close: {} } : {}),
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
authMethods: [],
|
|
537
|
+
}))
|
|
538
|
+
.onRequest(methods.agent.session.new, async ({ params }) => {
|
|
539
|
+
const { id, modes } = await handlers.newSession({
|
|
540
|
+
cwd: params.cwd,
|
|
541
|
+
mcpServers: stdioServersOf(params.mcpServers),
|
|
542
|
+
pinAskMode: false,
|
|
543
|
+
});
|
|
544
|
+
const state = sessionModesOf(modes);
|
|
545
|
+
return { sessionId: id, ...(state ? { modes: state } : {}) };
|
|
546
|
+
})
|
|
547
|
+
.onRequest(methods.agent.session.resume, async ({ params }) => {
|
|
548
|
+
if (!handlers.capabilities.resume) {
|
|
549
|
+
throw RequestError.methodNotFound(methods.agent.session.resume);
|
|
550
|
+
}
|
|
551
|
+
const { id } = await handlers.newSession({
|
|
552
|
+
cwd: params.cwd,
|
|
553
|
+
mcpServers: stdioServersOf(params.mcpServers),
|
|
554
|
+
pinAskMode: false,
|
|
555
|
+
});
|
|
556
|
+
aliases.set(params.sessionId, id);
|
|
557
|
+
return {};
|
|
558
|
+
})
|
|
559
|
+
.onRequest(methods.agent.session.setMode, () => ({}))
|
|
560
|
+
.onRequest(methods.agent.session.close, () => {
|
|
561
|
+
if (!handlers.capabilities.close) {
|
|
562
|
+
throw RequestError.methodNotFound(methods.agent.session.close);
|
|
563
|
+
}
|
|
564
|
+
return {};
|
|
565
|
+
})
|
|
566
|
+
.onRequest(methods.agent.session.prompt, async ({ params, client }) => {
|
|
567
|
+
const sessionId = params.sessionId;
|
|
568
|
+
const cancelled = new AbortController();
|
|
569
|
+
running.set(sessionId, cancelled);
|
|
570
|
+
try {
|
|
571
|
+
const blocks = params.prompt.flatMap((block) => block.type === "text" ? [{ kind: "text", text: block.text }] : []);
|
|
572
|
+
const stopReason = await handlers.prompt(idOf(sessionId), blocks, {
|
|
573
|
+
update: (update) => client.notify(methods.client.session.update, {
|
|
574
|
+
sessionId,
|
|
575
|
+
update: sessionUpdateOf(update),
|
|
576
|
+
}),
|
|
577
|
+
ask: async (question) => {
|
|
578
|
+
if (question.kind === "question") {
|
|
579
|
+
const answer = await client.request(methods.client.elicitation.create, {
|
|
580
|
+
sessionId,
|
|
581
|
+
mode: "form",
|
|
582
|
+
message: question.message,
|
|
583
|
+
requestedSchema: question.schema,
|
|
584
|
+
});
|
|
585
|
+
if (answer.action === "accept") {
|
|
586
|
+
const content = isFields(answer.content) ? answer.content : {};
|
|
587
|
+
return { kind: "values", values: content };
|
|
588
|
+
}
|
|
589
|
+
return answer.action === "decline"
|
|
590
|
+
? { kind: "declined" }
|
|
591
|
+
: { kind: "cancelled" };
|
|
592
|
+
}
|
|
593
|
+
const request = {
|
|
594
|
+
sessionId,
|
|
595
|
+
toolCall: {
|
|
596
|
+
toolCallId: question.toolCallId,
|
|
597
|
+
title: question.title,
|
|
598
|
+
kind: question.toolKind,
|
|
599
|
+
status: "pending",
|
|
600
|
+
...(question.mcp ? { _meta: claudeToolName(question.mcp) } : {}),
|
|
601
|
+
},
|
|
602
|
+
options: question.options.map((option) => ({
|
|
603
|
+
optionId: option.id,
|
|
604
|
+
name: option.label,
|
|
605
|
+
kind: option.effect,
|
|
606
|
+
})),
|
|
607
|
+
};
|
|
608
|
+
const answer = await client.request(methods.client.session.requestPermission, request);
|
|
609
|
+
return answer.outcome.outcome === "selected"
|
|
610
|
+
? { kind: "selected", optionId: answer.outcome.optionId }
|
|
611
|
+
: { kind: "cancelled" };
|
|
612
|
+
},
|
|
613
|
+
cancelled: cancelled.signal,
|
|
614
|
+
});
|
|
615
|
+
return { stopReason };
|
|
616
|
+
}
|
|
617
|
+
finally {
|
|
618
|
+
running.delete(sessionId);
|
|
619
|
+
}
|
|
620
|
+
})
|
|
621
|
+
.onNotification(methods.agent.session.cancel, ({ params }) => {
|
|
622
|
+
running.get(params.sessionId)?.abort();
|
|
623
|
+
});
|
|
624
|
+
return app.connect(ndJsonStream(io.output, io.input)).closed;
|
|
625
|
+
}
|
|
626
|
+
//# sourceMappingURL=acp.js.map
|