@wrongstack/acp 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/protocol-contract.d.ts +22 -2
- package/dist/agent/protocol-handler.d.ts +1 -1
- package/dist/agent/protocol-session-ops.d.ts +12 -1
- package/dist/agent/server-agent-turn.d.ts +6 -2
- package/dist/agent.js +121 -8
- package/dist/client/acp-session-errors.d.ts +6 -0
- package/dist/client/acp-session.d.ts +12 -0
- package/dist/client.js +90 -4
- package/dist/index.js +220 -27
- package/dist/integration/acp-subagent-runner.d.ts +8 -2
- package/dist/registry/acp-registry-fetch.d.ts +2 -0
- package/dist/types/acp-v1.d.ts +9 -1
- package/dist/wrongstack-acp-agent.js +115 -7
- package/package.json +2 -2
package/dist/agent/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { ClientCapabilities, RunTurn, RunTurnApi, RunTurnInput, RunTurnPermissionRequest, RunTurnResult, SessionPersistence, } from './protocol-handler.js';
|
|
1
|
+
export type { ClientCapabilities, McpServer, RunTurn, RunTurnApi, RunTurnInput, RunTurnPermissionRequest, RunTurnResult, SessionPersistence, } from './protocol-handler.js';
|
|
2
2
|
export { ACPProtocolHandler } from './protocol-handler.js';
|
|
3
3
|
export type { ACPServerAgentTurnOptions } from './server-agent-turn.js';
|
|
4
4
|
export { makeACPServerAgentTurn } from './server-agent-turn.js';
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Keeping these declarations separate leaves the handler focused on request dispatch.
|
|
4
4
|
*/
|
|
5
5
|
import type { ACPMessage } from '../types/acp-messages.js';
|
|
6
|
-
import { ACP_PROTOCOL_VERSION, type ContentBlock, type PermissionOption, type PlanEntry, type RequestPermissionOutcome, type StopReason, type ToolKind, type UsageCost } from '../types/acp-v1.js';
|
|
6
|
+
import { ACP_PROTOCOL_VERSION, type ContentBlock, type McpServer, type PermissionOption, type PlanEntry, type RequestPermissionOutcome, type StopReason, type ToolKind, type UsageCost } from '../types/acp-v1.js';
|
|
7
7
|
import type { AgentServerTransport } from './stdio-transport.js';
|
|
8
|
-
export type { ACPMessage, ContentBlock, RequestPermissionOutcome };
|
|
8
|
+
export type { ACPMessage, ContentBlock, McpServer, RequestPermissionOutcome };
|
|
9
9
|
export { ACP_PROTOCOL_VERSION };
|
|
10
10
|
type WireMessage = {
|
|
11
11
|
jsonrpc?: '2.0';
|
|
@@ -33,6 +33,19 @@ export interface RunTurnInput {
|
|
|
33
33
|
prompt: readonly ContentBlock[];
|
|
34
34
|
/** Cancelled when the client sends `session/cancel` for this session. */
|
|
35
35
|
signal: AbortSignal;
|
|
36
|
+
/** Session working directory from `session/new` (absolute). */
|
|
37
|
+
cwd?: string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* MCP servers the CLIENT asked this session to connect to, as supplied in
|
|
40
|
+
* `session/new` / `session/load` / `session/fork`. Validated at the wire
|
|
41
|
+
* boundary (see `parseMcpServers`), so entries here are well-formed.
|
|
42
|
+
*
|
|
43
|
+
* The runTurn implementation owns the connection: it is what holds the tool
|
|
44
|
+
* registry these servers' tools must land in. Nothing here is optional
|
|
45
|
+
* decoration — an editor that passes a server and gets no tools has been
|
|
46
|
+
* told a successful `session/new` about work that never happened.
|
|
47
|
+
*/
|
|
48
|
+
mcpServers?: readonly McpServer[] | undefined;
|
|
36
49
|
}
|
|
37
50
|
export interface RunTurnResult {
|
|
38
51
|
stopReason: StopReason;
|
|
@@ -127,6 +140,13 @@ export interface SessionState {
|
|
|
127
140
|
updatedAt: string;
|
|
128
141
|
/** Optional human title. */
|
|
129
142
|
title?: string;
|
|
143
|
+
/**
|
|
144
|
+
* The client's MCP servers for this session, captured at `session/new`
|
|
145
|
+
* (or `load`/`fork`) and replayed into every turn. Held on the session
|
|
146
|
+
* because the connection is session-scoped: two ACP sessions may ask for
|
|
147
|
+
* different servers, and closing one must not tear down the other's.
|
|
148
|
+
*/
|
|
149
|
+
mcpServers?: readonly McpServer[] | undefined;
|
|
130
150
|
}
|
|
131
151
|
/** MCP-style session mode advertised in current_mode_update. */
|
|
132
152
|
export interface SessionMode {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ProtocolHandlerOptions, WRONGSTACK_VERSION } from './protocol-contract.js';
|
|
2
2
|
import { errorToJsonRpc } from './protocol-session-ops.js';
|
|
3
|
-
export type { AgentCapabilities, ClientCapabilities, PromptCapabilities, ProtocolHandlerOptions, RunTurn, RunTurnApi, RunTurnInput, RunTurnPermissionRequest, RunTurnResult, SessionConfigOption, SessionMode, SessionPersistence, SessionState, } from './protocol-contract.js';
|
|
3
|
+
export type { AgentCapabilities, ClientCapabilities, McpServer, PromptCapabilities, ProtocolHandlerOptions, RunTurn, RunTurnApi, RunTurnInput, RunTurnPermissionRequest, RunTurnResult, SessionConfigOption, SessionMode, SessionPersistence, SessionState, } from './protocol-contract.js';
|
|
4
4
|
export { WRONGSTACK_VERSION };
|
|
5
5
|
export declare class ACPProtocolHandler {
|
|
6
6
|
private readonly transport;
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import { type ClientCapabilities, type RunTurnApi, type SessionConfigOption, type SessionMode } from './protocol-contract.js';
|
|
1
|
+
import { type ClientCapabilities, type McpServer, type RunTurnApi, type SessionConfigOption, type SessionMode } from './protocol-contract.js';
|
|
2
2
|
/** Single global mode id, sufficient for v1. */
|
|
3
3
|
export declare const DEFAULT_MODE_ID = "code";
|
|
4
4
|
export declare const DEFAULT_MAX_SESSIONS = 64;
|
|
5
5
|
export declare const DEFAULT_MODES: readonly SessionMode[];
|
|
6
|
+
/**
|
|
7
|
+
* Validate the `mcpServers` array a client sends with `session/new`,
|
|
8
|
+
* `session/load` or `session/fork`.
|
|
9
|
+
*
|
|
10
|
+
* Malformed entries are dropped rather than rejecting the whole request: an
|
|
11
|
+
* editor that ships one bad server config should still get a working session.
|
|
12
|
+
* A dropped entry is reported through `onSkipped` so the caller can tell the
|
|
13
|
+
* client instead of silently discarding what it asked for — silent discard is
|
|
14
|
+
* exactly how this array came to be ignored in the first place.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseMcpServers(raw: unknown, onSkipped?: (reason: string) => void): McpServer[];
|
|
6
17
|
export declare function resolveSessionCwd(requested: string): Promise<string | null>;
|
|
7
18
|
export declare function errorToJsonRpc(err: unknown): {
|
|
8
19
|
code: number;
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* `{stopReason: 'cancelled'}`.
|
|
40
40
|
*/
|
|
41
41
|
import type { Agent, AgentInput } from '@wrongstack/core/agent';
|
|
42
|
-
import type { ContentBlock, PlanEntry, StopReason, ToolKind, UsageCost } from '../types/acp-v1.js';
|
|
42
|
+
import type { ContentBlock, McpServer, PlanEntry, StopReason, ToolKind, UsageCost } from '../types/acp-v1.js';
|
|
43
43
|
import type { RunTurn, RunTurnApi, RunTurnResult } from './protocol-handler.js';
|
|
44
44
|
export interface ACPServerAgentTurnOptions {
|
|
45
45
|
/**
|
|
@@ -53,8 +53,12 @@ export interface ACPServerAgentTurnOptions {
|
|
|
53
53
|
* advertises those capabilities. A factory that wires it builds a
|
|
54
54
|
* client-backed permission policy and ACP-backed fs/terminal tools
|
|
55
55
|
* instead of silently auto-approving against the local disk.
|
|
56
|
+
*
|
|
57
|
+
* `mcpServers` is the client's per-session MCP server list from
|
|
58
|
+
* `session/new`. The factory owns the session's tool registry, so it is the
|
|
59
|
+
* only place those servers can be connected and their tools registered.
|
|
56
60
|
*/
|
|
57
|
-
agentFor: (sessionId: string, cwd: string, api?: RunTurnApi) => Promise<Agent> | Agent;
|
|
61
|
+
agentFor: (sessionId: string, cwd: string, api?: RunTurnApi, mcpServers?: readonly McpServer[]) => Promise<Agent> | Agent;
|
|
58
62
|
/**
|
|
59
63
|
* Hard wall-clock cap for one turn. The agent's own provider
|
|
60
64
|
* timeout is layered under this; this cap is a safety belt.
|
package/dist/agent.js
CHANGED
|
@@ -46,6 +46,64 @@ var DEFAULT_MODES = [
|
|
|
46
46
|
description: "Default agent mode for code-generation tasks."
|
|
47
47
|
}
|
|
48
48
|
];
|
|
49
|
+
function parseMcpServers(raw, onSkipped) {
|
|
50
|
+
if (!Array.isArray(raw)) return [];
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const entry of raw) {
|
|
53
|
+
if (typeof entry !== "object" || entry === null) {
|
|
54
|
+
onSkipped?.("entry is not an object");
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const e = entry;
|
|
58
|
+
const name = typeof e.name === "string" ? e.name.trim() : "";
|
|
59
|
+
if (name === "") {
|
|
60
|
+
onSkipped?.("entry has no name");
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const type = typeof e.type === "string" ? e.type : "stdio";
|
|
64
|
+
if (type === "http" || type === "sse") {
|
|
65
|
+
if (typeof e.url !== "string" || e.url === "") {
|
|
66
|
+
onSkipped?.(`"${name}": ${type} server has no url`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const headers = parseNameValuePairs(e.headers);
|
|
70
|
+
const url = e.url;
|
|
71
|
+
out.push(
|
|
72
|
+
type === "http" ? { type: "http", name, url, ...headers ? { headers } : {} } : { type: "sse", name, url, ...headers ? { headers } : {} }
|
|
73
|
+
);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (type !== "stdio") {
|
|
77
|
+
onSkipped?.(`"${name}": unknown transport "${type}"`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (typeof e.command !== "string" || e.command === "") {
|
|
81
|
+
onSkipped?.(`"${name}": stdio server has no command`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const args = Array.isArray(e.args) ? e.args.filter((a) => typeof a === "string") : void 0;
|
|
85
|
+
const env = parseNameValuePairs(e.env);
|
|
86
|
+
out.push({
|
|
87
|
+
name,
|
|
88
|
+
command: e.command,
|
|
89
|
+
...args && args.length > 0 ? { args } : {},
|
|
90
|
+
...env ? { env } : {}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
function parseNameValuePairs(raw) {
|
|
96
|
+
if (!Array.isArray(raw)) return void 0;
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const pair of raw) {
|
|
99
|
+
if (typeof pair !== "object" || pair === null) continue;
|
|
100
|
+
const p = pair;
|
|
101
|
+
if (typeof p.name === "string" && p.name !== "" && typeof p.value === "string") {
|
|
102
|
+
out.push({ name: p.name, value: p.value });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out.length > 0 ? out : void 0;
|
|
106
|
+
}
|
|
49
107
|
async function resolveSessionCwd(requested) {
|
|
50
108
|
if (!path.isAbsolute(requested)) return null;
|
|
51
109
|
const resolved = path.resolve(requested);
|
|
@@ -128,9 +186,16 @@ function buildInitializeResult(agentName, modes, configOptions) {
|
|
|
128
186
|
audio: false,
|
|
129
187
|
embeddedContext: true
|
|
130
188
|
},
|
|
189
|
+
// All three ACP transports are supported. stdio is mandatory per spec
|
|
190
|
+
// and cannot be declined; http and sse are declared here because the
|
|
191
|
+
// agent now actually connects them (see `parseMcpServers` above and the
|
|
192
|
+
// per-session MCP registry in `buildAcpServerAgentFactory`). Before that
|
|
193
|
+
// wiring existed the array was destructured and thrown away at every
|
|
194
|
+
// entry point, so a client got a successful `session/new` and no tools —
|
|
195
|
+
// flip these back to false if that connection path is ever removed.
|
|
131
196
|
mcpCapabilities: {
|
|
132
|
-
http:
|
|
133
|
-
sse:
|
|
197
|
+
http: true,
|
|
198
|
+
sse: true
|
|
134
199
|
},
|
|
135
200
|
sessionCapabilities: {
|
|
136
201
|
close: {},
|
|
@@ -170,6 +235,8 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
170
235
|
}
|
|
171
236
|
cwd = resolved;
|
|
172
237
|
}
|
|
238
|
+
const skipped = [];
|
|
239
|
+
const mcpServers = parseMcpServers(p.mcpServers, (reason) => skipped.push(reason));
|
|
173
240
|
const sessionId = `sess_${ctx.allocId()}`;
|
|
174
241
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
175
242
|
const state = {
|
|
@@ -178,7 +245,8 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
178
245
|
abort: new AbortController(),
|
|
179
246
|
modeId: DEFAULT_MODE_ID,
|
|
180
247
|
createdAt: now,
|
|
181
|
-
updatedAt: now
|
|
248
|
+
updatedAt: now,
|
|
249
|
+
...mcpServers.length > 0 ? { mcpServers } : {}
|
|
182
250
|
};
|
|
183
251
|
ctx.sessions.set(sessionId, state);
|
|
184
252
|
ctx.onSessionNew(state);
|
|
@@ -199,6 +267,7 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
199
267
|
}
|
|
200
268
|
});
|
|
201
269
|
}
|
|
270
|
+
await reportSkippedMcpServers(ctx, sessionId, skipped);
|
|
202
271
|
await ctx.sendResult(id, {
|
|
203
272
|
sessionId,
|
|
204
273
|
modes: ctx.modes,
|
|
@@ -210,6 +279,8 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
210
279
|
const p = params ?? {};
|
|
211
280
|
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
212
281
|
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
282
|
+
const loadSkipped = [];
|
|
283
|
+
const loadMcpServers = parseMcpServers(p.mcpServers, (reason) => loadSkipped.push(reason));
|
|
213
284
|
const existing = sessionId ? ctx.sessions.get(sessionId) : void 0;
|
|
214
285
|
if (!existing && sessionId && ctx.store) {
|
|
215
286
|
const persisted = await ctx.store.load(sessionId);
|
|
@@ -231,7 +302,8 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
231
302
|
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
232
303
|
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
233
304
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
234
|
-
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
305
|
+
...persisted.title !== void 0 ? { title: persisted.title } : {},
|
|
306
|
+
...loadMcpServers.length > 0 ? { mcpServers: loadMcpServers } : {}
|
|
235
307
|
};
|
|
236
308
|
ctx.sessions.set(sessionId, restored);
|
|
237
309
|
ctx.seedFor?.(sessionId, persisted.history ?? []);
|
|
@@ -242,6 +314,7 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
242
314
|
sessionId,
|
|
243
315
|
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
244
316
|
});
|
|
317
|
+
await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
|
|
245
318
|
await ctx.sendResult(id, {
|
|
246
319
|
initialMode: { currentModeId: restored.modeId, availableModes: ctx.modes }
|
|
247
320
|
});
|
|
@@ -250,6 +323,9 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
250
323
|
}
|
|
251
324
|
if (existing) {
|
|
252
325
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
326
|
+
if (loadMcpServers.length > 0) {
|
|
327
|
+
existing.mcpServers = loadMcpServers;
|
|
328
|
+
}
|
|
253
329
|
const replay = ctx.replayFor?.(sessionId);
|
|
254
330
|
if (replay) {
|
|
255
331
|
for (const update of replay) {
|
|
@@ -270,6 +346,7 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
270
346
|
modeId: existing.modeId
|
|
271
347
|
}
|
|
272
348
|
});
|
|
349
|
+
await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
|
|
273
350
|
await ctx.sendResult(id, {
|
|
274
351
|
initialMode: {
|
|
275
352
|
currentModeId: existing.modeId,
|
|
@@ -302,6 +379,9 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
302
379
|
}
|
|
303
380
|
forkCwd = resolved;
|
|
304
381
|
}
|
|
382
|
+
const forkSkipped = [];
|
|
383
|
+
const forkRequested = parseMcpServers(p.mcpServers, (reason) => forkSkipped.push(reason));
|
|
384
|
+
const forkMcpServers = forkRequested.length > 0 ? forkRequested : source.mcpServers;
|
|
305
385
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
306
386
|
const sessionId = `sess_${ctx.allocId()}`;
|
|
307
387
|
const forked = {
|
|
@@ -311,7 +391,8 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
311
391
|
modeId: source.modeId,
|
|
312
392
|
createdAt: now,
|
|
313
393
|
updatedAt: now,
|
|
314
|
-
...source.title !== void 0 ? { title: source.title } : {}
|
|
394
|
+
...source.title !== void 0 ? { title: source.title } : {},
|
|
395
|
+
...forkMcpServers && forkMcpServers.length > 0 ? { mcpServers: forkMcpServers } : {}
|
|
315
396
|
};
|
|
316
397
|
const history = (ctx.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
317
398
|
sessionUpdate: update.sessionUpdate,
|
|
@@ -325,6 +406,7 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
325
406
|
sessionId,
|
|
326
407
|
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
327
408
|
});
|
|
409
|
+
await reportSkippedMcpServers(ctx, sessionId, forkSkipped);
|
|
328
410
|
await ctx.sendResult(id, {
|
|
329
411
|
sessionId,
|
|
330
412
|
modes: ctx.modes,
|
|
@@ -364,7 +446,13 @@ async function handleSessionPromptOp(ctx, id, params) {
|
|
|
364
446
|
};
|
|
365
447
|
try {
|
|
366
448
|
result = await ctx.runTurn(
|
|
367
|
-
{
|
|
449
|
+
{
|
|
450
|
+
sessionId,
|
|
451
|
+
prompt: p.prompt,
|
|
452
|
+
signal: turnSignal.signal,
|
|
453
|
+
cwd: session.cwd,
|
|
454
|
+
...session.mcpServers ? { mcpServers: session.mcpServers } : {}
|
|
455
|
+
},
|
|
368
456
|
emit,
|
|
369
457
|
api
|
|
370
458
|
);
|
|
@@ -422,6 +510,22 @@ async function handleSetConfigOptionOp(ctx, id, params) {
|
|
|
422
510
|
await ctx.sendResult(id, { configOptions: [...ctx.configOptions] });
|
|
423
511
|
return false;
|
|
424
512
|
}
|
|
513
|
+
async function reportSkippedMcpServers(ctx, sessionId, skipped) {
|
|
514
|
+
if (skipped.length === 0) return;
|
|
515
|
+
try {
|
|
516
|
+
await ctx.sendNotification({
|
|
517
|
+
sessionId,
|
|
518
|
+
update: {
|
|
519
|
+
sessionUpdate: "agent_message_chunk",
|
|
520
|
+
content: {
|
|
521
|
+
type: "text",
|
|
522
|
+
text: `Ignored ${skipped.length} malformed mcpServers entr${skipped.length === 1 ? "y" : "ies"}: ${skipped.join("; ")}`
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
} catch {
|
|
527
|
+
}
|
|
528
|
+
}
|
|
425
529
|
|
|
426
530
|
// src/agent/protocol-handler.ts
|
|
427
531
|
var ACPProtocolHandler = class {
|
|
@@ -620,7 +724,7 @@ var ACPProtocolHandler = class {
|
|
|
620
724
|
return false;
|
|
621
725
|
}
|
|
622
726
|
async handleAuthenticate(id, _params) {
|
|
623
|
-
await this.sendResult(id, {
|
|
727
|
+
await this.sendResult(id, {});
|
|
624
728
|
return false;
|
|
625
729
|
}
|
|
626
730
|
async handleLogout(id, _params) {
|
|
@@ -633,6 +737,10 @@ var ACPProtocolHandler = class {
|
|
|
633
737
|
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
634
738
|
if (existing) {
|
|
635
739
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
740
|
+
const resumeServers = parseMcpServers(p.mcpServers);
|
|
741
|
+
if (resumeServers.length > 0) {
|
|
742
|
+
existing.mcpServers = resumeServers;
|
|
743
|
+
}
|
|
636
744
|
await this.sendResult(id, {
|
|
637
745
|
initialMode: {
|
|
638
746
|
currentModeId: existing.modeId,
|
|
@@ -775,7 +883,12 @@ function makeACPServerAgentTurn(opts) {
|
|
|
775
883
|
const turn = async (input, emit, api) => {
|
|
776
884
|
let agent = agents.get(input.sessionId);
|
|
777
885
|
if (!agent) {
|
|
778
|
-
agent = await opts.agentFor(
|
|
886
|
+
agent = await opts.agentFor(
|
|
887
|
+
input.sessionId,
|
|
888
|
+
input.cwd ?? process.cwd(),
|
|
889
|
+
api,
|
|
890
|
+
input.mcpServers
|
|
891
|
+
);
|
|
779
892
|
agents.set(input.sessionId, agent);
|
|
780
893
|
if (pendingSeed.has(input.sessionId)) {
|
|
781
894
|
pendingSeed.delete(input.sessionId);
|
|
@@ -10,5 +10,11 @@ interface JsonRpcError {
|
|
|
10
10
|
data?: unknown;
|
|
11
11
|
}
|
|
12
12
|
export declare function isJsonRpcError(v: unknown): v is JsonRpcError;
|
|
13
|
+
/**
|
|
14
|
+
* True when an agent refused `session/new` (or similar) because the user
|
|
15
|
+
* must authenticate first. Official registry agents often return this
|
|
16
|
+
* (~19/31 in the 2026-09 protocol matrix) instead of creating a session.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isAuthRequiredError(err: unknown): boolean;
|
|
13
19
|
export {};
|
|
14
20
|
//# sourceMappingURL=acp-session-errors.d.ts.map
|
|
@@ -107,6 +107,18 @@ export declare class ACPSession {
|
|
|
107
107
|
close(): Promise<void>;
|
|
108
108
|
private allocId;
|
|
109
109
|
private sendRequest;
|
|
110
|
+
/**
|
|
111
|
+
* `session/new`, then one authenticate+retry if the agent demands login.
|
|
112
|
+
* Logged-in CLIs succeed on the first call even when they advertise
|
|
113
|
+
* `authMethods`; we do not pop OAuth on every spawn.
|
|
114
|
+
*/
|
|
115
|
+
private createSessionWithAuth;
|
|
116
|
+
/**
|
|
117
|
+
* Pick a non-terminal auth method and run `authenticate`. Terminal-only
|
|
118
|
+
* agents need an out-of-band login CLI (registry AUTHENTICATION.md) —
|
|
119
|
+
* we refuse rather than hang a TUI inside the JSON-RPC child.
|
|
120
|
+
*/
|
|
121
|
+
private ensureAuthenticated;
|
|
110
122
|
private sendResult;
|
|
111
123
|
private sendErrorResponse;
|
|
112
124
|
private responseSender;
|
package/dist/client.js
CHANGED
|
@@ -254,6 +254,21 @@ function verbatimOptions(invocation) {
|
|
|
254
254
|
// src/types/acp-v1.ts
|
|
255
255
|
var ACP_PROTOCOL_VERSION = 1;
|
|
256
256
|
|
|
257
|
+
// src/version.ts
|
|
258
|
+
import { createRequire } from "node:module";
|
|
259
|
+
var require2 = createRequire(import.meta.url);
|
|
260
|
+
function readPackageVersion(load = () => require2("../package.json")) {
|
|
261
|
+
try {
|
|
262
|
+
const packageJson = load();
|
|
263
|
+
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
264
|
+
return packageJson.version;
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
return "dev";
|
|
269
|
+
}
|
|
270
|
+
var ACP_PACKAGE_VERSION = readPackageVersion();
|
|
271
|
+
|
|
257
272
|
// src/client/acp-message-routing.ts
|
|
258
273
|
function isBestEffortAckMethod(method) {
|
|
259
274
|
return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
|
|
@@ -728,6 +743,20 @@ var ACPSessionError = class extends Error {
|
|
|
728
743
|
function isJsonRpcError(v) {
|
|
729
744
|
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
730
745
|
}
|
|
746
|
+
function isAuthRequiredError(err) {
|
|
747
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
748
|
+
if (/auth(_|-)?required|authentication required/i.test(message)) return true;
|
|
749
|
+
const cause = err instanceof ACPSessionError ? err.cause : err && typeof err === "object" && "cause" in err ? err.cause : err;
|
|
750
|
+
if (!cause || typeof cause !== "object") return false;
|
|
751
|
+
const data = cause.data;
|
|
752
|
+
if (data === "auth_required" || data === "AUTH_REQUIRED") return true;
|
|
753
|
+
if (data && typeof data === "object") {
|
|
754
|
+
const d = data;
|
|
755
|
+
if (d.authRequired === true) return true;
|
|
756
|
+
if (d.code === "auth_required" || d.code === "AUTH_REQUIRED") return true;
|
|
757
|
+
}
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
731
760
|
|
|
732
761
|
// src/client/acp-session-ops.ts
|
|
733
762
|
function filterMcpServers(agentCapabilities, servers) {
|
|
@@ -1772,7 +1801,7 @@ var ACPSession = class _ACPSession {
|
|
|
1772
1801
|
fs: { readTextFile: true, writeTextFile: true },
|
|
1773
1802
|
terminal: true
|
|
1774
1803
|
},
|
|
1775
|
-
clientInfo: { name: "wrongstack", title: "WrongStack", version:
|
|
1804
|
+
clientInfo: { name: "wrongstack", title: "WrongStack", version: ACP_PACKAGE_VERSION }
|
|
1776
1805
|
});
|
|
1777
1806
|
if (isJsonRpcError(result)) {
|
|
1778
1807
|
throw new ACPSessionError("init_failed", `initialize failed: ${result.message}`, result);
|
|
@@ -1804,12 +1833,13 @@ var ACPSession = class _ACPSession {
|
|
|
1804
1833
|
if (this.state === "closed") {
|
|
1805
1834
|
throw new ACPSessionError("closed", "session is closed");
|
|
1806
1835
|
}
|
|
1807
|
-
if (this.state !== "ready") {
|
|
1836
|
+
if (this.state !== "ready" && this.state !== "authenticated") {
|
|
1808
1837
|
throw new ACPSessionError(
|
|
1809
1838
|
"protocol_error",
|
|
1810
1839
|
`authenticate called in state=${this.state} (expected 'ready')`
|
|
1811
1840
|
);
|
|
1812
1841
|
}
|
|
1842
|
+
if (this.state === "authenticated") return;
|
|
1813
1843
|
if (!this.authMethods.some((m) => m.id === methodId)) {
|
|
1814
1844
|
throw new ACPSessionError(
|
|
1815
1845
|
"auth_failed",
|
|
@@ -1912,7 +1942,7 @@ var ACPSession = class _ACPSession {
|
|
|
1912
1942
|
return emptyRunResult("cancelled");
|
|
1913
1943
|
}
|
|
1914
1944
|
if (!this.sessionId) {
|
|
1915
|
-
this.sessionId = await
|
|
1945
|
+
this.sessionId = await this.createSessionWithAuth();
|
|
1916
1946
|
}
|
|
1917
1947
|
if (signal.aborted) {
|
|
1918
1948
|
return emptyRunResult("cancelled");
|
|
@@ -2048,6 +2078,54 @@ var ACPSession = class _ACPSession {
|
|
|
2048
2078
|
});
|
|
2049
2079
|
});
|
|
2050
2080
|
}
|
|
2081
|
+
/**
|
|
2082
|
+
* `session/new`, then one authenticate+retry if the agent demands login.
|
|
2083
|
+
* Logged-in CLIs succeed on the first call even when they advertise
|
|
2084
|
+
* `authMethods`; we do not pop OAuth on every spawn.
|
|
2085
|
+
*/
|
|
2086
|
+
async createSessionWithAuth() {
|
|
2087
|
+
try {
|
|
2088
|
+
return await executeCreateSession(this.opContext());
|
|
2089
|
+
} catch (err) {
|
|
2090
|
+
if (this.state === "authenticated" || !isAuthRequiredError(err)) {
|
|
2091
|
+
throw err instanceof ACPSessionError ? err : new ACPSessionError(
|
|
2092
|
+
"session_create_failed",
|
|
2093
|
+
err instanceof Error ? err.message : String(err),
|
|
2094
|
+
err
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
await this.ensureAuthenticated();
|
|
2098
|
+
return executeCreateSession(this.opContext());
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* Pick a non-terminal auth method and run `authenticate`. Terminal-only
|
|
2103
|
+
* agents need an out-of-band login CLI (registry AUTHENTICATION.md) —
|
|
2104
|
+
* we refuse rather than hang a TUI inside the JSON-RPC child.
|
|
2105
|
+
*/
|
|
2106
|
+
async ensureAuthenticated() {
|
|
2107
|
+
if (this.state === "authenticated") return;
|
|
2108
|
+
if (this.authMethods.length === 0) {
|
|
2109
|
+
throw new ACPSessionError(
|
|
2110
|
+
"auth_failed",
|
|
2111
|
+
"This agent requires authentication before a session can start, but advertised no authMethods. Log into the CLI, then retry."
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
const inProcess = this.authMethods.find(
|
|
2115
|
+
(m) => m.type === void 0 || m.type === "agent" || m.type === "oauth" || m.type === "http"
|
|
2116
|
+
);
|
|
2117
|
+
if (inProcess) {
|
|
2118
|
+
await this.authenticate(inProcess.id);
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
const terminal = this.authMethods.find((m) => m.type === "terminal");
|
|
2122
|
+
const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
|
|
2123
|
+
const setup = setupArgs !== void 0 ? `${this.opts.command} ${setupArgs}` : `${this.opts.command}${this.opts.args?.length ? ` ${this.opts.args.join(" ")}` : ""}`;
|
|
2124
|
+
throw new ACPSessionError(
|
|
2125
|
+
"auth_failed",
|
|
2126
|
+
`This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2051
2129
|
sendResult(id, result) {
|
|
2052
2130
|
return this.transport.send({ jsonrpc: "2.0", id, result });
|
|
2053
2131
|
}
|
|
@@ -2076,7 +2154,11 @@ var ACPSession = class _ACPSession {
|
|
|
2076
2154
|
clearTimeout(pending.timeoutHandle);
|
|
2077
2155
|
this.pending.delete(msg.id);
|
|
2078
2156
|
if (msg.error !== void 0) {
|
|
2079
|
-
|
|
2157
|
+
const method = pending.method;
|
|
2158
|
+
const kind = method === "session/new" ? "session_create_failed" : method === "authenticate" ? "auth_failed" : "protocol_error";
|
|
2159
|
+
pending.reject(
|
|
2160
|
+
new ACPSessionError(kind, msg.error.message ?? "unknown JSON-RPC error", msg.error)
|
|
2161
|
+
);
|
|
2080
2162
|
} else {
|
|
2081
2163
|
pending.resolve(msg.result);
|
|
2082
2164
|
}
|
|
@@ -2198,6 +2280,10 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2198
2280
|
} catch {
|
|
2199
2281
|
}
|
|
2200
2282
|
options.onProgress?.(event);
|
|
2283
|
+
try {
|
|
2284
|
+
options.publishLive?.(ctx, task, event);
|
|
2285
|
+
} catch {
|
|
2286
|
+
}
|
|
2201
2287
|
};
|
|
2202
2288
|
try {
|
|
2203
2289
|
const result = await session.prompt([textContent(task.description)], ctx.signal, onProgress);
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,64 @@ var DEFAULT_MODES = [
|
|
|
49
49
|
description: "Default agent mode for code-generation tasks."
|
|
50
50
|
}
|
|
51
51
|
];
|
|
52
|
+
function parseMcpServers(raw, onSkipped) {
|
|
53
|
+
if (!Array.isArray(raw)) return [];
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const entry of raw) {
|
|
56
|
+
if (typeof entry !== "object" || entry === null) {
|
|
57
|
+
onSkipped?.("entry is not an object");
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const e = entry;
|
|
61
|
+
const name = typeof e.name === "string" ? e.name.trim() : "";
|
|
62
|
+
if (name === "") {
|
|
63
|
+
onSkipped?.("entry has no name");
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const type = typeof e.type === "string" ? e.type : "stdio";
|
|
67
|
+
if (type === "http" || type === "sse") {
|
|
68
|
+
if (typeof e.url !== "string" || e.url === "") {
|
|
69
|
+
onSkipped?.(`"${name}": ${type} server has no url`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const headers = parseNameValuePairs(e.headers);
|
|
73
|
+
const url = e.url;
|
|
74
|
+
out.push(
|
|
75
|
+
type === "http" ? { type: "http", name, url, ...headers ? { headers } : {} } : { type: "sse", name, url, ...headers ? { headers } : {} }
|
|
76
|
+
);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (type !== "stdio") {
|
|
80
|
+
onSkipped?.(`"${name}": unknown transport "${type}"`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (typeof e.command !== "string" || e.command === "") {
|
|
84
|
+
onSkipped?.(`"${name}": stdio server has no command`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const args = Array.isArray(e.args) ? e.args.filter((a) => typeof a === "string") : void 0;
|
|
88
|
+
const env = parseNameValuePairs(e.env);
|
|
89
|
+
out.push({
|
|
90
|
+
name,
|
|
91
|
+
command: e.command,
|
|
92
|
+
...args && args.length > 0 ? { args } : {},
|
|
93
|
+
...env ? { env } : {}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function parseNameValuePairs(raw) {
|
|
99
|
+
if (!Array.isArray(raw)) return void 0;
|
|
100
|
+
const out = [];
|
|
101
|
+
for (const pair of raw) {
|
|
102
|
+
if (typeof pair !== "object" || pair === null) continue;
|
|
103
|
+
const p = pair;
|
|
104
|
+
if (typeof p.name === "string" && p.name !== "" && typeof p.value === "string") {
|
|
105
|
+
out.push({ name: p.name, value: p.value });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out.length > 0 ? out : void 0;
|
|
109
|
+
}
|
|
52
110
|
async function resolveSessionCwd(requested) {
|
|
53
111
|
if (!path.isAbsolute(requested)) return null;
|
|
54
112
|
const resolved = path.resolve(requested);
|
|
@@ -131,9 +189,16 @@ function buildInitializeResult(agentName, modes, configOptions) {
|
|
|
131
189
|
audio: false,
|
|
132
190
|
embeddedContext: true
|
|
133
191
|
},
|
|
192
|
+
// All three ACP transports are supported. stdio is mandatory per spec
|
|
193
|
+
// and cannot be declined; http and sse are declared here because the
|
|
194
|
+
// agent now actually connects them (see `parseMcpServers` above and the
|
|
195
|
+
// per-session MCP registry in `buildAcpServerAgentFactory`). Before that
|
|
196
|
+
// wiring existed the array was destructured and thrown away at every
|
|
197
|
+
// entry point, so a client got a successful `session/new` and no tools —
|
|
198
|
+
// flip these back to false if that connection path is ever removed.
|
|
134
199
|
mcpCapabilities: {
|
|
135
|
-
http:
|
|
136
|
-
sse:
|
|
200
|
+
http: true,
|
|
201
|
+
sse: true
|
|
137
202
|
},
|
|
138
203
|
sessionCapabilities: {
|
|
139
204
|
close: {},
|
|
@@ -173,6 +238,8 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
173
238
|
}
|
|
174
239
|
cwd = resolved;
|
|
175
240
|
}
|
|
241
|
+
const skipped = [];
|
|
242
|
+
const mcpServers = parseMcpServers(p.mcpServers, (reason) => skipped.push(reason));
|
|
176
243
|
const sessionId = `sess_${ctx.allocId()}`;
|
|
177
244
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
178
245
|
const state = {
|
|
@@ -181,7 +248,8 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
181
248
|
abort: new AbortController(),
|
|
182
249
|
modeId: DEFAULT_MODE_ID,
|
|
183
250
|
createdAt: now,
|
|
184
|
-
updatedAt: now
|
|
251
|
+
updatedAt: now,
|
|
252
|
+
...mcpServers.length > 0 ? { mcpServers } : {}
|
|
185
253
|
};
|
|
186
254
|
ctx.sessions.set(sessionId, state);
|
|
187
255
|
ctx.onSessionNew(state);
|
|
@@ -202,6 +270,7 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
202
270
|
}
|
|
203
271
|
});
|
|
204
272
|
}
|
|
273
|
+
await reportSkippedMcpServers(ctx, sessionId, skipped);
|
|
205
274
|
await ctx.sendResult(id, {
|
|
206
275
|
sessionId,
|
|
207
276
|
modes: ctx.modes,
|
|
@@ -213,6 +282,8 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
213
282
|
const p = params ?? {};
|
|
214
283
|
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
215
284
|
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
285
|
+
const loadSkipped = [];
|
|
286
|
+
const loadMcpServers = parseMcpServers(p.mcpServers, (reason) => loadSkipped.push(reason));
|
|
216
287
|
const existing = sessionId ? ctx.sessions.get(sessionId) : void 0;
|
|
217
288
|
if (!existing && sessionId && ctx.store) {
|
|
218
289
|
const persisted = await ctx.store.load(sessionId);
|
|
@@ -234,7 +305,8 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
234
305
|
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
235
306
|
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
236
307
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
237
|
-
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
308
|
+
...persisted.title !== void 0 ? { title: persisted.title } : {},
|
|
309
|
+
...loadMcpServers.length > 0 ? { mcpServers: loadMcpServers } : {}
|
|
238
310
|
};
|
|
239
311
|
ctx.sessions.set(sessionId, restored);
|
|
240
312
|
ctx.seedFor?.(sessionId, persisted.history ?? []);
|
|
@@ -245,6 +317,7 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
245
317
|
sessionId,
|
|
246
318
|
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
247
319
|
});
|
|
320
|
+
await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
|
|
248
321
|
await ctx.sendResult(id, {
|
|
249
322
|
initialMode: { currentModeId: restored.modeId, availableModes: ctx.modes }
|
|
250
323
|
});
|
|
@@ -253,6 +326,9 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
253
326
|
}
|
|
254
327
|
if (existing) {
|
|
255
328
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
329
|
+
if (loadMcpServers.length > 0) {
|
|
330
|
+
existing.mcpServers = loadMcpServers;
|
|
331
|
+
}
|
|
256
332
|
const replay = ctx.replayFor?.(sessionId);
|
|
257
333
|
if (replay) {
|
|
258
334
|
for (const update of replay) {
|
|
@@ -273,6 +349,7 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
273
349
|
modeId: existing.modeId
|
|
274
350
|
}
|
|
275
351
|
});
|
|
352
|
+
await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
|
|
276
353
|
await ctx.sendResult(id, {
|
|
277
354
|
initialMode: {
|
|
278
355
|
currentModeId: existing.modeId,
|
|
@@ -305,6 +382,9 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
305
382
|
}
|
|
306
383
|
forkCwd = resolved;
|
|
307
384
|
}
|
|
385
|
+
const forkSkipped = [];
|
|
386
|
+
const forkRequested = parseMcpServers(p.mcpServers, (reason) => forkSkipped.push(reason));
|
|
387
|
+
const forkMcpServers = forkRequested.length > 0 ? forkRequested : source.mcpServers;
|
|
308
388
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
309
389
|
const sessionId = `sess_${ctx.allocId()}`;
|
|
310
390
|
const forked = {
|
|
@@ -314,7 +394,8 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
314
394
|
modeId: source.modeId,
|
|
315
395
|
createdAt: now,
|
|
316
396
|
updatedAt: now,
|
|
317
|
-
...source.title !== void 0 ? { title: source.title } : {}
|
|
397
|
+
...source.title !== void 0 ? { title: source.title } : {},
|
|
398
|
+
...forkMcpServers && forkMcpServers.length > 0 ? { mcpServers: forkMcpServers } : {}
|
|
318
399
|
};
|
|
319
400
|
const history = (ctx.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
320
401
|
sessionUpdate: update.sessionUpdate,
|
|
@@ -328,6 +409,7 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
328
409
|
sessionId,
|
|
329
410
|
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
330
411
|
});
|
|
412
|
+
await reportSkippedMcpServers(ctx, sessionId, forkSkipped);
|
|
331
413
|
await ctx.sendResult(id, {
|
|
332
414
|
sessionId,
|
|
333
415
|
modes: ctx.modes,
|
|
@@ -367,7 +449,13 @@ async function handleSessionPromptOp(ctx, id, params) {
|
|
|
367
449
|
};
|
|
368
450
|
try {
|
|
369
451
|
result = await ctx.runTurn(
|
|
370
|
-
{
|
|
452
|
+
{
|
|
453
|
+
sessionId,
|
|
454
|
+
prompt: p.prompt,
|
|
455
|
+
signal: turnSignal.signal,
|
|
456
|
+
cwd: session.cwd,
|
|
457
|
+
...session.mcpServers ? { mcpServers: session.mcpServers } : {}
|
|
458
|
+
},
|
|
371
459
|
emit,
|
|
372
460
|
api
|
|
373
461
|
);
|
|
@@ -425,6 +513,22 @@ async function handleSetConfigOptionOp(ctx, id, params) {
|
|
|
425
513
|
await ctx.sendResult(id, { configOptions: [...ctx.configOptions] });
|
|
426
514
|
return false;
|
|
427
515
|
}
|
|
516
|
+
async function reportSkippedMcpServers(ctx, sessionId, skipped) {
|
|
517
|
+
if (skipped.length === 0) return;
|
|
518
|
+
try {
|
|
519
|
+
await ctx.sendNotification({
|
|
520
|
+
sessionId,
|
|
521
|
+
update: {
|
|
522
|
+
sessionUpdate: "agent_message_chunk",
|
|
523
|
+
content: {
|
|
524
|
+
type: "text",
|
|
525
|
+
text: `Ignored ${skipped.length} malformed mcpServers entr${skipped.length === 1 ? "y" : "ies"}: ${skipped.join("; ")}`
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
} catch {
|
|
530
|
+
}
|
|
531
|
+
}
|
|
428
532
|
|
|
429
533
|
// src/agent/protocol-handler.ts
|
|
430
534
|
var ACPProtocolHandler = class {
|
|
@@ -623,7 +727,7 @@ var ACPProtocolHandler = class {
|
|
|
623
727
|
return false;
|
|
624
728
|
}
|
|
625
729
|
async handleAuthenticate(id, _params) {
|
|
626
|
-
await this.sendResult(id, {
|
|
730
|
+
await this.sendResult(id, {});
|
|
627
731
|
return false;
|
|
628
732
|
}
|
|
629
733
|
async handleLogout(id, _params) {
|
|
@@ -636,6 +740,10 @@ var ACPProtocolHandler = class {
|
|
|
636
740
|
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
637
741
|
if (existing) {
|
|
638
742
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
743
|
+
const resumeServers = parseMcpServers(p.mcpServers);
|
|
744
|
+
if (resumeServers.length > 0) {
|
|
745
|
+
existing.mcpServers = resumeServers;
|
|
746
|
+
}
|
|
639
747
|
await this.sendResult(id, {
|
|
640
748
|
initialMode: {
|
|
641
749
|
currentModeId: existing.modeId,
|
|
@@ -2045,6 +2153,20 @@ var ACPSessionError = class extends Error {
|
|
|
2045
2153
|
function isJsonRpcError(v) {
|
|
2046
2154
|
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
2047
2155
|
}
|
|
2156
|
+
function isAuthRequiredError(err) {
|
|
2157
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2158
|
+
if (/auth(_|-)?required|authentication required/i.test(message)) return true;
|
|
2159
|
+
const cause = err instanceof ACPSessionError ? err.cause : err && typeof err === "object" && "cause" in err ? err.cause : err;
|
|
2160
|
+
if (!cause || typeof cause !== "object") return false;
|
|
2161
|
+
const data = cause.data;
|
|
2162
|
+
if (data === "auth_required" || data === "AUTH_REQUIRED") return true;
|
|
2163
|
+
if (data && typeof data === "object") {
|
|
2164
|
+
const d = data;
|
|
2165
|
+
if (d.authRequired === true) return true;
|
|
2166
|
+
if (d.code === "auth_required" || d.code === "AUTH_REQUIRED") return true;
|
|
2167
|
+
}
|
|
2168
|
+
return false;
|
|
2169
|
+
}
|
|
2048
2170
|
|
|
2049
2171
|
// src/client/acp-session-ops.ts
|
|
2050
2172
|
function filterMcpServers(agentCapabilities, servers) {
|
|
@@ -3089,7 +3211,7 @@ var ACPSession = class _ACPSession {
|
|
|
3089
3211
|
fs: { readTextFile: true, writeTextFile: true },
|
|
3090
3212
|
terminal: true
|
|
3091
3213
|
},
|
|
3092
|
-
clientInfo: { name: "wrongstack", title: "WrongStack", version:
|
|
3214
|
+
clientInfo: { name: "wrongstack", title: "WrongStack", version: ACP_PACKAGE_VERSION }
|
|
3093
3215
|
});
|
|
3094
3216
|
if (isJsonRpcError(result)) {
|
|
3095
3217
|
throw new ACPSessionError("init_failed", `initialize failed: ${result.message}`, result);
|
|
@@ -3121,12 +3243,13 @@ var ACPSession = class _ACPSession {
|
|
|
3121
3243
|
if (this.state === "closed") {
|
|
3122
3244
|
throw new ACPSessionError("closed", "session is closed");
|
|
3123
3245
|
}
|
|
3124
|
-
if (this.state !== "ready") {
|
|
3246
|
+
if (this.state !== "ready" && this.state !== "authenticated") {
|
|
3125
3247
|
throw new ACPSessionError(
|
|
3126
3248
|
"protocol_error",
|
|
3127
3249
|
`authenticate called in state=${this.state} (expected 'ready')`
|
|
3128
3250
|
);
|
|
3129
3251
|
}
|
|
3252
|
+
if (this.state === "authenticated") return;
|
|
3130
3253
|
if (!this.authMethods.some((m) => m.id === methodId)) {
|
|
3131
3254
|
throw new ACPSessionError(
|
|
3132
3255
|
"auth_failed",
|
|
@@ -3229,7 +3352,7 @@ var ACPSession = class _ACPSession {
|
|
|
3229
3352
|
return emptyRunResult("cancelled");
|
|
3230
3353
|
}
|
|
3231
3354
|
if (!this.sessionId) {
|
|
3232
|
-
this.sessionId = await
|
|
3355
|
+
this.sessionId = await this.createSessionWithAuth();
|
|
3233
3356
|
}
|
|
3234
3357
|
if (signal.aborted) {
|
|
3235
3358
|
return emptyRunResult("cancelled");
|
|
@@ -3365,6 +3488,54 @@ var ACPSession = class _ACPSession {
|
|
|
3365
3488
|
});
|
|
3366
3489
|
});
|
|
3367
3490
|
}
|
|
3491
|
+
/**
|
|
3492
|
+
* `session/new`, then one authenticate+retry if the agent demands login.
|
|
3493
|
+
* Logged-in CLIs succeed on the first call even when they advertise
|
|
3494
|
+
* `authMethods`; we do not pop OAuth on every spawn.
|
|
3495
|
+
*/
|
|
3496
|
+
async createSessionWithAuth() {
|
|
3497
|
+
try {
|
|
3498
|
+
return await executeCreateSession(this.opContext());
|
|
3499
|
+
} catch (err) {
|
|
3500
|
+
if (this.state === "authenticated" || !isAuthRequiredError(err)) {
|
|
3501
|
+
throw err instanceof ACPSessionError ? err : new ACPSessionError(
|
|
3502
|
+
"session_create_failed",
|
|
3503
|
+
err instanceof Error ? err.message : String(err),
|
|
3504
|
+
err
|
|
3505
|
+
);
|
|
3506
|
+
}
|
|
3507
|
+
await this.ensureAuthenticated();
|
|
3508
|
+
return executeCreateSession(this.opContext());
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
/**
|
|
3512
|
+
* Pick a non-terminal auth method and run `authenticate`. Terminal-only
|
|
3513
|
+
* agents need an out-of-band login CLI (registry AUTHENTICATION.md) —
|
|
3514
|
+
* we refuse rather than hang a TUI inside the JSON-RPC child.
|
|
3515
|
+
*/
|
|
3516
|
+
async ensureAuthenticated() {
|
|
3517
|
+
if (this.state === "authenticated") return;
|
|
3518
|
+
if (this.authMethods.length === 0) {
|
|
3519
|
+
throw new ACPSessionError(
|
|
3520
|
+
"auth_failed",
|
|
3521
|
+
"This agent requires authentication before a session can start, but advertised no authMethods. Log into the CLI, then retry."
|
|
3522
|
+
);
|
|
3523
|
+
}
|
|
3524
|
+
const inProcess = this.authMethods.find(
|
|
3525
|
+
(m) => m.type === void 0 || m.type === "agent" || m.type === "oauth" || m.type === "http"
|
|
3526
|
+
);
|
|
3527
|
+
if (inProcess) {
|
|
3528
|
+
await this.authenticate(inProcess.id);
|
|
3529
|
+
return;
|
|
3530
|
+
}
|
|
3531
|
+
const terminal = this.authMethods.find((m) => m.type === "terminal");
|
|
3532
|
+
const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
|
|
3533
|
+
const setup = setupArgs !== void 0 ? `${this.opts.command} ${setupArgs}` : `${this.opts.command}${this.opts.args?.length ? ` ${this.opts.args.join(" ")}` : ""}`;
|
|
3534
|
+
throw new ACPSessionError(
|
|
3535
|
+
"auth_failed",
|
|
3536
|
+
`This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
|
|
3537
|
+
);
|
|
3538
|
+
}
|
|
3368
3539
|
sendResult(id, result) {
|
|
3369
3540
|
return this.transport.send({ jsonrpc: "2.0", id, result });
|
|
3370
3541
|
}
|
|
@@ -3393,7 +3564,11 @@ var ACPSession = class _ACPSession {
|
|
|
3393
3564
|
clearTimeout(pending.timeoutHandle);
|
|
3394
3565
|
this.pending.delete(msg.id);
|
|
3395
3566
|
if (msg.error !== void 0) {
|
|
3396
|
-
|
|
3567
|
+
const method = pending.method;
|
|
3568
|
+
const kind = method === "session/new" ? "session_create_failed" : method === "authenticate" ? "auth_failed" : "protocol_error";
|
|
3569
|
+
pending.reject(
|
|
3570
|
+
new ACPSessionError(kind, msg.error.message ?? "unknown JSON-RPC error", msg.error)
|
|
3571
|
+
);
|
|
3397
3572
|
} else {
|
|
3398
3573
|
pending.resolve(msg.result);
|
|
3399
3574
|
}
|
|
@@ -3833,7 +4008,7 @@ var AGENTS_CATALOG = [
|
|
|
3833
4008
|
id: "cline",
|
|
3834
4009
|
displayName: "Cline",
|
|
3835
4010
|
vendor: "community",
|
|
3836
|
-
probe: { command: "
|
|
4011
|
+
probe: { command: "cline", args: ["--version"] },
|
|
3837
4012
|
// Registry id `cline`: the `cline` npm package speaks ACP behind `--acp`.
|
|
3838
4013
|
acp: {
|
|
3839
4014
|
command: "npx",
|
|
@@ -3876,7 +4051,8 @@ var AGENTS_CATALOG = [
|
|
|
3876
4051
|
fs: true
|
|
3877
4052
|
},
|
|
3878
4053
|
integration: "experimental",
|
|
3879
|
-
//
|
|
4054
|
+
// Not in the official agentclientprotocol/registry (2026-09). Keep as a
|
|
4055
|
+
// local-PATH fallback; probe/spawn may hang if the binary has no ACP entry.
|
|
3880
4056
|
docs: "https://github.com/OpenHands/OpenHands"
|
|
3881
4057
|
},
|
|
3882
4058
|
// ── Vendor CLIs (native binaries) ───────────────────────────────────
|
|
@@ -3909,6 +4085,7 @@ var AGENTS_CATALOG = [
|
|
|
3909
4085
|
fs: true
|
|
3910
4086
|
},
|
|
3911
4087
|
integration: "experimental",
|
|
4088
|
+
// Not in the official agentclientprotocol/registry (2026-09).
|
|
3912
4089
|
docs: "https://kiro.dev"
|
|
3913
4090
|
},
|
|
3914
4091
|
{
|
|
@@ -3931,8 +4108,9 @@ var AGENTS_CATALOG = [
|
|
|
3931
4108
|
id: "mistral-vibe",
|
|
3932
4109
|
displayName: "Mistral Vibe",
|
|
3933
4110
|
vendor: "community",
|
|
3934
|
-
probe: { command: "vibe", args: ["--version"] },
|
|
3935
|
-
|
|
4111
|
+
probe: { command: "vibe-acp", args: ["--version"] },
|
|
4112
|
+
// Official registry ships a dedicated `vibe-acp` binary, not bare `vibe`.
|
|
4113
|
+
acp: { command: "vibe-acp", args: [] },
|
|
3936
4114
|
supports: {
|
|
3937
4115
|
loadSession: false,
|
|
3938
4116
|
promptImages: false,
|
|
@@ -4054,6 +4232,10 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
4054
4232
|
} catch {
|
|
4055
4233
|
}
|
|
4056
4234
|
options.onProgress?.(event);
|
|
4235
|
+
try {
|
|
4236
|
+
options.publishLive?.(ctx, task, event);
|
|
4237
|
+
} catch {
|
|
4238
|
+
}
|
|
4057
4239
|
};
|
|
4058
4240
|
try {
|
|
4059
4241
|
const result = await session.prompt([textContent(task.description)], ctx.signal, onProgress);
|
|
@@ -4140,10 +4322,13 @@ var REGISTRY_ID_ALIASES = {
|
|
|
4140
4322
|
"gemini-cli": "gemini",
|
|
4141
4323
|
"codex-cli": "codex-acp",
|
|
4142
4324
|
copilot: "github-copilot-cli",
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4325
|
+
kimi: "kimi",
|
|
4326
|
+
cline: "cline",
|
|
4327
|
+
goose: "goose",
|
|
4328
|
+
opencode: "opencode",
|
|
4329
|
+
cursor: "cursor",
|
|
4330
|
+
"qwen-code": "qwen-code",
|
|
4331
|
+
"mistral-vibe": "mistral-vibe"
|
|
4147
4332
|
};
|
|
4148
4333
|
function resolveAcpAgentCommand(id, overrides, live) {
|
|
4149
4334
|
const ov = overrides?.[id];
|
|
@@ -4566,20 +4751,20 @@ async function runEnsemble(opts) {
|
|
|
4566
4751
|
const detectedById = new Map(detected.map((a) => [a.id, a]));
|
|
4567
4752
|
const runnable = [];
|
|
4568
4753
|
for (const id of requested) {
|
|
4754
|
+
const cmd = resolveCmd(id);
|
|
4569
4755
|
const det = detectedById.get(id);
|
|
4570
|
-
|
|
4756
|
+
const pkgLauncher = cmd?.command === "npx" || cmd?.command === "uvx";
|
|
4757
|
+
if (det && !det.installed && !pkgLauncher) {
|
|
4571
4758
|
setResult(results, id, {
|
|
4572
4759
|
status: "skipped",
|
|
4573
|
-
reason: det
|
|
4760
|
+
reason: det.reason ?? "binary not found"
|
|
4574
4761
|
});
|
|
4575
4762
|
continue;
|
|
4576
4763
|
}
|
|
4577
|
-
const cmd = resolveCmd(id);
|
|
4578
4764
|
if (!cmd) {
|
|
4579
4765
|
setResult(results, id, {
|
|
4580
|
-
status: "
|
|
4581
|
-
|
|
4582
|
-
durationMs: 0
|
|
4766
|
+
status: "skipped",
|
|
4767
|
+
reason: det?.reason ?? "not in catalog"
|
|
4583
4768
|
});
|
|
4584
4769
|
continue;
|
|
4585
4770
|
}
|
|
@@ -4715,9 +4900,17 @@ function mapRegistryEntry(entry, platformKey = currentPlatformKey()) {
|
|
|
4715
4900
|
const dist = entry.distribution;
|
|
4716
4901
|
let acp = null;
|
|
4717
4902
|
if (dist?.npx?.package) {
|
|
4718
|
-
acp = {
|
|
4903
|
+
acp = {
|
|
4904
|
+
command: "npx",
|
|
4905
|
+
args: ["-y", dist.npx.package, ...dist.npx.args ?? []],
|
|
4906
|
+
...dist.npx.env ? { env: dist.npx.env } : {}
|
|
4907
|
+
};
|
|
4719
4908
|
} else if (dist?.uvx?.package) {
|
|
4720
|
-
acp = {
|
|
4909
|
+
acp = {
|
|
4910
|
+
command: "uvx",
|
|
4911
|
+
args: [dist.uvx.package, ...dist.uvx.args ?? []],
|
|
4912
|
+
...dist.uvx.env ? { env: dist.uvx.env } : {}
|
|
4913
|
+
};
|
|
4721
4914
|
} else if (dist?.binary) {
|
|
4722
4915
|
const target = dist.binary[platformKey];
|
|
4723
4916
|
if (target?.cmd) {
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* Connected to the Director / MultiAgentCoordinator via the
|
|
12
12
|
* `SubagentRunner` interface (same shape as `AgentSubagentRunner`).
|
|
13
13
|
*/
|
|
14
|
-
import type { SubagentRunner } from '@wrongstack/core/types';
|
|
15
|
-
import { type ACPProgressHandler, ACPSession } from '../client/acp-session.js';
|
|
14
|
+
import type { SubagentRunContext, SubagentRunner, TaskSpec } from '@wrongstack/core/types';
|
|
15
|
+
import { type ACPProgressEvent, type ACPProgressHandler, ACPSession } from '../client/acp-session.js';
|
|
16
16
|
import type { PermissionPolicy } from '../client/permission.js';
|
|
17
17
|
import type { McpServer } from '../types/acp-v1.js';
|
|
18
18
|
export interface ACPSubagentRunnerOptions {
|
|
@@ -40,6 +40,12 @@ export interface ACPSubagentRunnerOptions {
|
|
|
40
40
|
* stream, instead of waiting for the buffered final result.
|
|
41
41
|
*/
|
|
42
42
|
onProgress?: ACPProgressHandler | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Host live-view hook. Called with the run context so a fleet/TUI/WebUI
|
|
45
|
+
* publisher can attribute tool calls and text to the right subagent.
|
|
46
|
+
* Invoked in addition to `onProgress`.
|
|
47
|
+
*/
|
|
48
|
+
publishLive?: ((ctx: SubagentRunContext, task: TaskSpec, event: ACPProgressEvent) => void) | undefined;
|
|
43
49
|
/**
|
|
44
50
|
* Permission policy for the external agent's `session/request_permission`
|
|
45
51
|
* calls. Defaults to the session's own default. Inject the host's
|
|
@@ -39,10 +39,12 @@ export interface RegistryAgentEntry {
|
|
|
39
39
|
npx?: {
|
|
40
40
|
package: string;
|
|
41
41
|
args?: string[];
|
|
42
|
+
env?: Record<string, string>;
|
|
42
43
|
};
|
|
43
44
|
uvx?: {
|
|
44
45
|
package: string;
|
|
45
46
|
args?: string[];
|
|
47
|
+
env?: Record<string, string>;
|
|
46
48
|
};
|
|
47
49
|
binary?: Record<string, {
|
|
48
50
|
archive?: string;
|
package/dist/types/acp-v1.d.ts
CHANGED
|
@@ -95,7 +95,15 @@ export interface AuthMethod {
|
|
|
95
95
|
id: string;
|
|
96
96
|
name: string;
|
|
97
97
|
description?: string | undefined;
|
|
98
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Registry agents advertise `agent` (OAuth in-process) or `terminal`
|
|
100
|
+
* (separate login CLI). `oauth`/`http` are spec aliases; `env_var` is
|
|
101
|
+
* used by some agents but is not a registry-supported setup path.
|
|
102
|
+
*/
|
|
103
|
+
type?: 'agent' | 'oauth' | 'http' | 'terminal' | 'env_var' | undefined;
|
|
104
|
+
/** Extra argv for `type: 'terminal'` setup (replaces the ACP entry args). */
|
|
105
|
+
args?: string[] | undefined;
|
|
106
|
+
env?: Record<string, string> | undefined;
|
|
99
107
|
}
|
|
100
108
|
export interface AuthenticateRequest {
|
|
101
109
|
methodId: string;
|
|
@@ -53,6 +53,64 @@ var DEFAULT_MODES = [
|
|
|
53
53
|
description: "Default agent mode for code-generation tasks."
|
|
54
54
|
}
|
|
55
55
|
];
|
|
56
|
+
function parseMcpServers(raw, onSkipped) {
|
|
57
|
+
if (!Array.isArray(raw)) return [];
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const entry of raw) {
|
|
60
|
+
if (typeof entry !== "object" || entry === null) {
|
|
61
|
+
onSkipped?.("entry is not an object");
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const e = entry;
|
|
65
|
+
const name = typeof e.name === "string" ? e.name.trim() : "";
|
|
66
|
+
if (name === "") {
|
|
67
|
+
onSkipped?.("entry has no name");
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const type = typeof e.type === "string" ? e.type : "stdio";
|
|
71
|
+
if (type === "http" || type === "sse") {
|
|
72
|
+
if (typeof e.url !== "string" || e.url === "") {
|
|
73
|
+
onSkipped?.(`"${name}": ${type} server has no url`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const headers = parseNameValuePairs(e.headers);
|
|
77
|
+
const url = e.url;
|
|
78
|
+
out.push(
|
|
79
|
+
type === "http" ? { type: "http", name, url, ...headers ? { headers } : {} } : { type: "sse", name, url, ...headers ? { headers } : {} }
|
|
80
|
+
);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (type !== "stdio") {
|
|
84
|
+
onSkipped?.(`"${name}": unknown transport "${type}"`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (typeof e.command !== "string" || e.command === "") {
|
|
88
|
+
onSkipped?.(`"${name}": stdio server has no command`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const args = Array.isArray(e.args) ? e.args.filter((a) => typeof a === "string") : void 0;
|
|
92
|
+
const env = parseNameValuePairs(e.env);
|
|
93
|
+
out.push({
|
|
94
|
+
name,
|
|
95
|
+
command: e.command,
|
|
96
|
+
...args && args.length > 0 ? { args } : {},
|
|
97
|
+
...env ? { env } : {}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
function parseNameValuePairs(raw) {
|
|
103
|
+
if (!Array.isArray(raw)) return void 0;
|
|
104
|
+
const out = [];
|
|
105
|
+
for (const pair of raw) {
|
|
106
|
+
if (typeof pair !== "object" || pair === null) continue;
|
|
107
|
+
const p = pair;
|
|
108
|
+
if (typeof p.name === "string" && p.name !== "" && typeof p.value === "string") {
|
|
109
|
+
out.push({ name: p.name, value: p.value });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out.length > 0 ? out : void 0;
|
|
113
|
+
}
|
|
56
114
|
async function resolveSessionCwd(requested) {
|
|
57
115
|
if (!path.isAbsolute(requested)) return null;
|
|
58
116
|
const resolved = path.resolve(requested);
|
|
@@ -135,9 +193,16 @@ function buildInitializeResult(agentName, modes, configOptions) {
|
|
|
135
193
|
audio: false,
|
|
136
194
|
embeddedContext: true
|
|
137
195
|
},
|
|
196
|
+
// All three ACP transports are supported. stdio is mandatory per spec
|
|
197
|
+
// and cannot be declined; http and sse are declared here because the
|
|
198
|
+
// agent now actually connects them (see `parseMcpServers` above and the
|
|
199
|
+
// per-session MCP registry in `buildAcpServerAgentFactory`). Before that
|
|
200
|
+
// wiring existed the array was destructured and thrown away at every
|
|
201
|
+
// entry point, so a client got a successful `session/new` and no tools —
|
|
202
|
+
// flip these back to false if that connection path is ever removed.
|
|
138
203
|
mcpCapabilities: {
|
|
139
|
-
http:
|
|
140
|
-
sse:
|
|
204
|
+
http: true,
|
|
205
|
+
sse: true
|
|
141
206
|
},
|
|
142
207
|
sessionCapabilities: {
|
|
143
208
|
close: {},
|
|
@@ -177,6 +242,8 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
177
242
|
}
|
|
178
243
|
cwd = resolved;
|
|
179
244
|
}
|
|
245
|
+
const skipped = [];
|
|
246
|
+
const mcpServers = parseMcpServers(p.mcpServers, (reason) => skipped.push(reason));
|
|
180
247
|
const sessionId = `sess_${ctx.allocId()}`;
|
|
181
248
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
182
249
|
const state = {
|
|
@@ -185,7 +252,8 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
185
252
|
abort: new AbortController(),
|
|
186
253
|
modeId: DEFAULT_MODE_ID,
|
|
187
254
|
createdAt: now,
|
|
188
|
-
updatedAt: now
|
|
255
|
+
updatedAt: now,
|
|
256
|
+
...mcpServers.length > 0 ? { mcpServers } : {}
|
|
189
257
|
};
|
|
190
258
|
ctx.sessions.set(sessionId, state);
|
|
191
259
|
ctx.onSessionNew(state);
|
|
@@ -206,6 +274,7 @@ async function handleSessionNewOp(ctx, id, params) {
|
|
|
206
274
|
}
|
|
207
275
|
});
|
|
208
276
|
}
|
|
277
|
+
await reportSkippedMcpServers(ctx, sessionId, skipped);
|
|
209
278
|
await ctx.sendResult(id, {
|
|
210
279
|
sessionId,
|
|
211
280
|
modes: ctx.modes,
|
|
@@ -217,6 +286,8 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
217
286
|
const p = params ?? {};
|
|
218
287
|
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
219
288
|
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
289
|
+
const loadSkipped = [];
|
|
290
|
+
const loadMcpServers = parseMcpServers(p.mcpServers, (reason) => loadSkipped.push(reason));
|
|
220
291
|
const existing = sessionId ? ctx.sessions.get(sessionId) : void 0;
|
|
221
292
|
if (!existing && sessionId && ctx.store) {
|
|
222
293
|
const persisted = await ctx.store.load(sessionId);
|
|
@@ -238,7 +309,8 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
238
309
|
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
239
310
|
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
240
311
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
241
|
-
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
312
|
+
...persisted.title !== void 0 ? { title: persisted.title } : {},
|
|
313
|
+
...loadMcpServers.length > 0 ? { mcpServers: loadMcpServers } : {}
|
|
242
314
|
};
|
|
243
315
|
ctx.sessions.set(sessionId, restored);
|
|
244
316
|
ctx.seedFor?.(sessionId, persisted.history ?? []);
|
|
@@ -249,6 +321,7 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
249
321
|
sessionId,
|
|
250
322
|
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
251
323
|
});
|
|
324
|
+
await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
|
|
252
325
|
await ctx.sendResult(id, {
|
|
253
326
|
initialMode: { currentModeId: restored.modeId, availableModes: ctx.modes }
|
|
254
327
|
});
|
|
@@ -257,6 +330,9 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
257
330
|
}
|
|
258
331
|
if (existing) {
|
|
259
332
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
333
|
+
if (loadMcpServers.length > 0) {
|
|
334
|
+
existing.mcpServers = loadMcpServers;
|
|
335
|
+
}
|
|
260
336
|
const replay = ctx.replayFor?.(sessionId);
|
|
261
337
|
if (replay) {
|
|
262
338
|
for (const update of replay) {
|
|
@@ -277,6 +353,7 @@ async function handleSessionLoadOp(ctx, id, params) {
|
|
|
277
353
|
modeId: existing.modeId
|
|
278
354
|
}
|
|
279
355
|
});
|
|
356
|
+
await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
|
|
280
357
|
await ctx.sendResult(id, {
|
|
281
358
|
initialMode: {
|
|
282
359
|
currentModeId: existing.modeId,
|
|
@@ -309,6 +386,9 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
309
386
|
}
|
|
310
387
|
forkCwd = resolved;
|
|
311
388
|
}
|
|
389
|
+
const forkSkipped = [];
|
|
390
|
+
const forkRequested = parseMcpServers(p.mcpServers, (reason) => forkSkipped.push(reason));
|
|
391
|
+
const forkMcpServers = forkRequested.length > 0 ? forkRequested : source.mcpServers;
|
|
312
392
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
313
393
|
const sessionId = `sess_${ctx.allocId()}`;
|
|
314
394
|
const forked = {
|
|
@@ -318,7 +398,8 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
318
398
|
modeId: source.modeId,
|
|
319
399
|
createdAt: now,
|
|
320
400
|
updatedAt: now,
|
|
321
|
-
...source.title !== void 0 ? { title: source.title } : {}
|
|
401
|
+
...source.title !== void 0 ? { title: source.title } : {},
|
|
402
|
+
...forkMcpServers && forkMcpServers.length > 0 ? { mcpServers: forkMcpServers } : {}
|
|
322
403
|
};
|
|
323
404
|
const history = (ctx.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
324
405
|
sessionUpdate: update.sessionUpdate,
|
|
@@ -332,6 +413,7 @@ async function handleSessionForkOp(ctx, id, params) {
|
|
|
332
413
|
sessionId,
|
|
333
414
|
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
334
415
|
});
|
|
416
|
+
await reportSkippedMcpServers(ctx, sessionId, forkSkipped);
|
|
335
417
|
await ctx.sendResult(id, {
|
|
336
418
|
sessionId,
|
|
337
419
|
modes: ctx.modes,
|
|
@@ -371,7 +453,13 @@ async function handleSessionPromptOp(ctx, id, params) {
|
|
|
371
453
|
};
|
|
372
454
|
try {
|
|
373
455
|
result = await ctx.runTurn(
|
|
374
|
-
{
|
|
456
|
+
{
|
|
457
|
+
sessionId,
|
|
458
|
+
prompt: p.prompt,
|
|
459
|
+
signal: turnSignal.signal,
|
|
460
|
+
cwd: session.cwd,
|
|
461
|
+
...session.mcpServers ? { mcpServers: session.mcpServers } : {}
|
|
462
|
+
},
|
|
375
463
|
emit,
|
|
376
464
|
api
|
|
377
465
|
);
|
|
@@ -429,6 +517,22 @@ async function handleSetConfigOptionOp(ctx, id, params) {
|
|
|
429
517
|
await ctx.sendResult(id, { configOptions: [...ctx.configOptions] });
|
|
430
518
|
return false;
|
|
431
519
|
}
|
|
520
|
+
async function reportSkippedMcpServers(ctx, sessionId, skipped) {
|
|
521
|
+
if (skipped.length === 0) return;
|
|
522
|
+
try {
|
|
523
|
+
await ctx.sendNotification({
|
|
524
|
+
sessionId,
|
|
525
|
+
update: {
|
|
526
|
+
sessionUpdate: "agent_message_chunk",
|
|
527
|
+
content: {
|
|
528
|
+
type: "text",
|
|
529
|
+
text: `Ignored ${skipped.length} malformed mcpServers entr${skipped.length === 1 ? "y" : "ies"}: ${skipped.join("; ")}`
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
} catch {
|
|
534
|
+
}
|
|
535
|
+
}
|
|
432
536
|
|
|
433
537
|
// src/agent/protocol-handler.ts
|
|
434
538
|
var ACPProtocolHandler = class {
|
|
@@ -627,7 +731,7 @@ var ACPProtocolHandler = class {
|
|
|
627
731
|
return false;
|
|
628
732
|
}
|
|
629
733
|
async handleAuthenticate(id, _params) {
|
|
630
|
-
await this.sendResult(id, {
|
|
734
|
+
await this.sendResult(id, {});
|
|
631
735
|
return false;
|
|
632
736
|
}
|
|
633
737
|
async handleLogout(id, _params) {
|
|
@@ -640,6 +744,10 @@ var ACPProtocolHandler = class {
|
|
|
640
744
|
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
641
745
|
if (existing) {
|
|
642
746
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
747
|
+
const resumeServers = parseMcpServers(p.mcpServers);
|
|
748
|
+
if (resumeServers.length > 0) {
|
|
749
|
+
existing.mcpServers = resumeServers;
|
|
750
|
+
}
|
|
643
751
|
await this.sendResult(id, {
|
|
644
752
|
initialMode: {
|
|
645
753
|
currentModeId: existing.modeId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/acp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "ACP (Agent Client Protocol) integration for WrongStack — client + agent support",
|
|
6
6
|
"keywords": [
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
],
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@agentclientprotocol/sdk": "^1.4.0",
|
|
55
|
-
"@wrongstack/core": "1.0.
|
|
55
|
+
"@wrongstack/core": "1.0.4"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/node": "^26.2.0",
|