wave-code 0.19.2 → 0.19.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/index.d.ts.map +1 -1
- package/dist/index.js +11 -10
- package/dist/stdio/agentBridge.d.ts +80 -0
- package/dist/stdio/agentBridge.d.ts.map +1 -0
- package/dist/stdio/agentBridge.js +562 -0
- package/dist/stdio/index.d.ts +4 -0
- package/dist/stdio/index.d.ts.map +1 -0
- package/dist/stdio/index.js +3 -0
- package/dist/stdio/protocol.d.ts +42 -0
- package/dist/stdio/protocol.d.ts.map +1 -0
- package/dist/stdio/protocol.js +26 -0
- package/dist/stdio/stdioServer.d.ts +34 -0
- package/dist/stdio/stdioServer.d.ts.map +1 -0
- package/dist/stdio/stdioServer.js +127 -0
- package/dist/stdio-cli.d.ts +9 -0
- package/dist/stdio-cli.d.ts.map +1 -0
- package/dist/stdio-cli.js +15 -0
- package/package.json +3 -4
- package/src/index.ts +12 -11
- package/src/stdio/agentBridge.ts +965 -0
- package/src/stdio/index.ts +7 -0
- package/src/stdio/protocol.ts +134 -0
- package/src/stdio/stdioServer.ts +169 -0
- package/src/stdio-cli.ts +18 -0
- package/dist/acp/agent.d.ts +0 -32
- package/dist/acp/agent.d.ts.map +0 -1
- package/dist/acp/agent.js +0 -1163
- package/dist/acp/index.d.ts +0 -2
- package/dist/acp/index.d.ts.map +0 -1
- package/dist/acp/index.js +0 -22
- package/dist/acp-cli.d.ts +0 -2
- package/dist/acp-cli.d.ts.map +0 -1
- package/dist/acp-cli.js +0 -4
- package/src/acp/agent.ts +0 -1501
- package/src/acp/index.ts +0 -28
- package/src/acp-cli.ts +0 -5
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentBridge — wraps the SDK Agent and translates between the JSON-RPC-like
|
|
3
|
+
* stdio protocol and Agent method calls / callbacks.
|
|
4
|
+
*
|
|
5
|
+
* Multi-tenant: maintains a Map<sessionId, {agent, storedConfig}> so a single
|
|
6
|
+
* `wave --stdio` process can host multiple sessions. Session-scoped requests
|
|
7
|
+
* carry `sessionId` on the JSON-RPC envelope for routing; global requests
|
|
8
|
+
* (listSessions/searchFiles/auth/plugins) don't require it but may use it
|
|
9
|
+
* for workdir fallback.
|
|
10
|
+
*
|
|
11
|
+
* Responsibilities:
|
|
12
|
+
* - Route incoming requests to the appropriate Agent method (by sessionId)
|
|
13
|
+
* - Translate AgentCallbacks into outgoing notifications (with sessionId)
|
|
14
|
+
* - Implement the canUseTool permission flow over the stdio protocol
|
|
15
|
+
* - Handle config updates by destroying and recreating the Agent
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
Agent,
|
|
20
|
+
type AgentCallbacks,
|
|
21
|
+
type AgentOptions,
|
|
22
|
+
type Message,
|
|
23
|
+
type PermissionDecision,
|
|
24
|
+
type PermissionMode,
|
|
25
|
+
type ToolPermissionContext,
|
|
26
|
+
type McpServerStatus,
|
|
27
|
+
type Task,
|
|
28
|
+
type QueuedMessage,
|
|
29
|
+
type SessionMetadata,
|
|
30
|
+
type McpServerConfig,
|
|
31
|
+
type Scope,
|
|
32
|
+
listSessions,
|
|
33
|
+
searchFiles,
|
|
34
|
+
PromptHistoryManager,
|
|
35
|
+
AuthService,
|
|
36
|
+
PluginCore,
|
|
37
|
+
type SlashCommand,
|
|
38
|
+
} from "wave-agent-sdk";
|
|
39
|
+
import {
|
|
40
|
+
type JsonRpcError,
|
|
41
|
+
INTERNAL_ERROR as PROTOCOL_INTERNAL_ERROR,
|
|
42
|
+
METHOD_NOT_FOUND as PROTOCOL_METHOD_NOT_FOUND,
|
|
43
|
+
} from "./protocol.js";
|
|
44
|
+
import { readFileSync } from "fs";
|
|
45
|
+
import path from "path";
|
|
46
|
+
import { fileURLToPath } from "url";
|
|
47
|
+
|
|
48
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
49
|
+
const CLI_VERSION: string = (() => {
|
|
50
|
+
try {
|
|
51
|
+
const pkg = JSON.parse(
|
|
52
|
+
readFileSync(path.resolve(__dirname, "../../package.json"), "utf-8"),
|
|
53
|
+
);
|
|
54
|
+
return pkg.version ?? "";
|
|
55
|
+
} catch {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
59
|
+
|
|
60
|
+
export type NotificationEmitter = (
|
|
61
|
+
method: string,
|
|
62
|
+
params: unknown,
|
|
63
|
+
sessionId?: string,
|
|
64
|
+
) => void;
|
|
65
|
+
|
|
66
|
+
export interface AgentBridgeOptions {
|
|
67
|
+
emit: NotificationEmitter;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface InitializeParams {
|
|
71
|
+
workdir?: string;
|
|
72
|
+
restoreSessionId?: string;
|
|
73
|
+
apiKey?: string;
|
|
74
|
+
baseURL?: string;
|
|
75
|
+
serverUrl?: string;
|
|
76
|
+
defaultHeaders?: Record<string, string>;
|
|
77
|
+
model?: string;
|
|
78
|
+
fastModel?: string;
|
|
79
|
+
language?: string;
|
|
80
|
+
permissionMode?: PermissionMode;
|
|
81
|
+
tools?: string[];
|
|
82
|
+
allowedTools?: string[];
|
|
83
|
+
disallowedTools?: string[];
|
|
84
|
+
pluginDirs?: string[];
|
|
85
|
+
mcpServers?: Record<string, McpServerConfig>;
|
|
86
|
+
clientVersion?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface UpdateConfigParams {
|
|
90
|
+
apiKey?: string;
|
|
91
|
+
baseURL?: string;
|
|
92
|
+
serverUrl?: string;
|
|
93
|
+
defaultHeaders?: Record<string, string>;
|
|
94
|
+
model?: string;
|
|
95
|
+
fastModel?: string;
|
|
96
|
+
language?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface SearchFilesParams {
|
|
100
|
+
query: string;
|
|
101
|
+
maxResults?: number;
|
|
102
|
+
workdir?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface SessionEntry {
|
|
106
|
+
agent: Agent;
|
|
107
|
+
storedConfig: Partial<InitializeParams>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Mutable holder so callbacks/canUseTool (created before Agent.create resolves)
|
|
112
|
+
* can reference the agent and its registered sessionId after creation.
|
|
113
|
+
* `registeredSessionId` is the sessionId the client knows about; it's used as
|
|
114
|
+
* the envelope sessionId for outgoing notifications so the client's router can
|
|
115
|
+
* demultiplex correctly. It's updated atomically when onSessionIdChange fires.
|
|
116
|
+
*/
|
|
117
|
+
interface SessionContext {
|
|
118
|
+
agent?: Agent;
|
|
119
|
+
registeredSessionId?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class AgentBridge {
|
|
123
|
+
private sessions = new Map<string, SessionEntry>();
|
|
124
|
+
private pendingPermissions = new Map<
|
|
125
|
+
string,
|
|
126
|
+
(decision: PermissionDecision) => void
|
|
127
|
+
>();
|
|
128
|
+
private permissionCounter = 0;
|
|
129
|
+
private emit: NotificationEmitter;
|
|
130
|
+
private pluginCore: PluginCore | undefined;
|
|
131
|
+
private pluginCoreWorkdir: string | undefined;
|
|
132
|
+
|
|
133
|
+
constructor(options: AgentBridgeOptions) {
|
|
134
|
+
this.emit = options.emit;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Public API ────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
async handleRequest(
|
|
140
|
+
method: string,
|
|
141
|
+
params: unknown,
|
|
142
|
+
sessionId?: string,
|
|
143
|
+
): Promise<unknown> {
|
|
144
|
+
const p = (params ?? {}) as Record<string, unknown>;
|
|
145
|
+
switch (method) {
|
|
146
|
+
// ── Lifecycle ──
|
|
147
|
+
case "initialize":
|
|
148
|
+
return this.initialize(p as unknown as InitializeParams);
|
|
149
|
+
case "destroy":
|
|
150
|
+
return this.destroy(sessionId);
|
|
151
|
+
case "restoreSession":
|
|
152
|
+
return this.restoreSession(p.sessionId as string, sessionId);
|
|
153
|
+
case "listSessions":
|
|
154
|
+
return this.listSessions(p.workdir as string | undefined, sessionId);
|
|
155
|
+
case "getSessionInfo":
|
|
156
|
+
return this.getSessionInfo(sessionId);
|
|
157
|
+
case "updateConfig":
|
|
158
|
+
return this.updateConfig(p as unknown as UpdateConfigParams, sessionId);
|
|
159
|
+
|
|
160
|
+
// ── Messages ──
|
|
161
|
+
case "sendMessage":
|
|
162
|
+
return this.sendMessage(
|
|
163
|
+
p as unknown as {
|
|
164
|
+
text: string;
|
|
165
|
+
images?: Array<{ path: string; mimeType: string }>;
|
|
166
|
+
force?: boolean;
|
|
167
|
+
},
|
|
168
|
+
sessionId,
|
|
169
|
+
);
|
|
170
|
+
case "bang":
|
|
171
|
+
return this.bang(p.command as string, sessionId);
|
|
172
|
+
case "abortMessage":
|
|
173
|
+
return this.abortMessage(sessionId);
|
|
174
|
+
case "clearMessages":
|
|
175
|
+
return this.clearMessages(sessionId);
|
|
176
|
+
case "rewindToMessage":
|
|
177
|
+
return this.rewindToMessage(p.messageId as string, sessionId);
|
|
178
|
+
case "deleteQueuedMessage":
|
|
179
|
+
return this.deleteQueuedMessage(p.index as number, sessionId);
|
|
180
|
+
case "getMessages":
|
|
181
|
+
return this.getMessages(sessionId);
|
|
182
|
+
case "getFullMessageThread":
|
|
183
|
+
return this.getFullMessageThread(sessionId);
|
|
184
|
+
|
|
185
|
+
// ── Permissions ──
|
|
186
|
+
case "setPermissionMode":
|
|
187
|
+
return this.setPermissionMode(p.mode as PermissionMode, sessionId);
|
|
188
|
+
case "getPermissionMode":
|
|
189
|
+
return this.getPermissionMode(sessionId);
|
|
190
|
+
|
|
191
|
+
// ── MCP ──
|
|
192
|
+
case "getMcpServers":
|
|
193
|
+
return this.getMcpServers(sessionId);
|
|
194
|
+
case "connectMcpServer":
|
|
195
|
+
return this.connectMcpServer(p.serverName as string, sessionId);
|
|
196
|
+
case "disconnectMcpServer":
|
|
197
|
+
return this.disconnectMcpServer(p.serverName as string, sessionId);
|
|
198
|
+
|
|
199
|
+
// ── Commands ──
|
|
200
|
+
case "getSlashCommands":
|
|
201
|
+
return this.getSlashCommands(sessionId);
|
|
202
|
+
|
|
203
|
+
// ── File / History (global — no session required) ──
|
|
204
|
+
case "searchFiles":
|
|
205
|
+
return this.searchFiles(p as unknown as SearchFilesParams, sessionId);
|
|
206
|
+
case "getPromptHistory":
|
|
207
|
+
return this.getPromptHistory(
|
|
208
|
+
p.workdir as string | undefined,
|
|
209
|
+
sessionId,
|
|
210
|
+
);
|
|
211
|
+
case "searchPromptHistory":
|
|
212
|
+
return this.searchPromptHistory(
|
|
213
|
+
p.query as string,
|
|
214
|
+
p.workdir as string | undefined,
|
|
215
|
+
sessionId,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// ── Auth (global — no session required) ──
|
|
219
|
+
case "getAuthStatus":
|
|
220
|
+
return this.getAuthStatus();
|
|
221
|
+
case "login":
|
|
222
|
+
return this.login(p.serverUrl as string | undefined);
|
|
223
|
+
case "logout":
|
|
224
|
+
return this.logout();
|
|
225
|
+
|
|
226
|
+
// ── Plugins (global — no session required) ──
|
|
227
|
+
case "listPlugins":
|
|
228
|
+
return this.listPlugins(p.workdir as string | undefined, sessionId);
|
|
229
|
+
case "installPlugin":
|
|
230
|
+
return this.installPlugin(
|
|
231
|
+
p.pluginId as string,
|
|
232
|
+
p.scope as Scope | undefined,
|
|
233
|
+
p.workdir as string | undefined,
|
|
234
|
+
sessionId,
|
|
235
|
+
);
|
|
236
|
+
case "uninstallPlugin":
|
|
237
|
+
return this.uninstallPlugin(
|
|
238
|
+
p.pluginId as string,
|
|
239
|
+
p.workdir as string | undefined,
|
|
240
|
+
sessionId,
|
|
241
|
+
);
|
|
242
|
+
case "enablePlugin":
|
|
243
|
+
return this.enablePlugin(
|
|
244
|
+
p.pluginId as string,
|
|
245
|
+
p.scope as Scope | undefined,
|
|
246
|
+
p.workdir as string | undefined,
|
|
247
|
+
sessionId,
|
|
248
|
+
);
|
|
249
|
+
case "disablePlugin":
|
|
250
|
+
return this.disablePlugin(
|
|
251
|
+
p.pluginId as string,
|
|
252
|
+
p.scope as Scope | undefined,
|
|
253
|
+
p.workdir as string | undefined,
|
|
254
|
+
sessionId,
|
|
255
|
+
);
|
|
256
|
+
case "updatePlugin":
|
|
257
|
+
return this.updatePlugin(
|
|
258
|
+
p.pluginId as string,
|
|
259
|
+
p.workdir as string | undefined,
|
|
260
|
+
sessionId,
|
|
261
|
+
);
|
|
262
|
+
case "listMarketplaces":
|
|
263
|
+
return this.listMarketplaces(
|
|
264
|
+
p.workdir as string | undefined,
|
|
265
|
+
sessionId,
|
|
266
|
+
);
|
|
267
|
+
case "addMarketplace":
|
|
268
|
+
return this.addMarketplace(
|
|
269
|
+
p.input as string,
|
|
270
|
+
p.scope as Scope | undefined,
|
|
271
|
+
p.workdir as string | undefined,
|
|
272
|
+
sessionId,
|
|
273
|
+
);
|
|
274
|
+
case "removeMarketplace":
|
|
275
|
+
return this.removeMarketplace(
|
|
276
|
+
p.name as string,
|
|
277
|
+
p.scope as Scope | undefined,
|
|
278
|
+
p.workdir as string | undefined,
|
|
279
|
+
sessionId,
|
|
280
|
+
);
|
|
281
|
+
case "updateMarketplace":
|
|
282
|
+
return this.updateMarketplace(
|
|
283
|
+
p.name as string | undefined,
|
|
284
|
+
p.workdir as string | undefined,
|
|
285
|
+
sessionId,
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
default:
|
|
289
|
+
throw new RpcError(
|
|
290
|
+
PROTOCOL_METHOD_NOT_FOUND,
|
|
291
|
+
`Method not found: ${method}`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
handleNotification(method: string, params: unknown): void {
|
|
297
|
+
if (method === "permissionResponse") {
|
|
298
|
+
const p = params as {
|
|
299
|
+
requestId: string;
|
|
300
|
+
decision: PermissionDecision;
|
|
301
|
+
};
|
|
302
|
+
// requestId is process-level unique; lookup doesn't need sessionId
|
|
303
|
+
const resolve = this.pendingPermissions.get(p.requestId);
|
|
304
|
+
if (resolve) {
|
|
305
|
+
this.pendingPermissions.delete(p.requestId);
|
|
306
|
+
resolve(p.decision);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── Lifecycle ─────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
private async initialize(params: InitializeParams): Promise<{
|
|
314
|
+
sessionId: string;
|
|
315
|
+
workingDirectory: string;
|
|
316
|
+
permissionMode: PermissionMode;
|
|
317
|
+
latestTotalTokens: number;
|
|
318
|
+
serverVersion: string;
|
|
319
|
+
}> {
|
|
320
|
+
const ctx: SessionContext = {};
|
|
321
|
+
const callbacks = this.createCallbacks(ctx);
|
|
322
|
+
|
|
323
|
+
const options: AgentOptions = {
|
|
324
|
+
callbacks,
|
|
325
|
+
workdir: params.workdir,
|
|
326
|
+
restoreSessionId: params.restoreSessionId,
|
|
327
|
+
apiKey: params.apiKey,
|
|
328
|
+
baseURL: params.baseURL,
|
|
329
|
+
defaultHeaders: params.defaultHeaders,
|
|
330
|
+
model: params.model,
|
|
331
|
+
fastModel: params.fastModel,
|
|
332
|
+
language: params.language,
|
|
333
|
+
permissionMode: params.permissionMode,
|
|
334
|
+
tools: params.tools,
|
|
335
|
+
allowedTools: params.allowedTools,
|
|
336
|
+
disallowedTools: params.disallowedTools,
|
|
337
|
+
plugins: params.pluginDirs?.map((path) => ({ type: "local", path })),
|
|
338
|
+
mcpServers: params.mcpServers,
|
|
339
|
+
canUseTool: (context: ToolPermissionContext) =>
|
|
340
|
+
this.canUseTool(context, ctx),
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const agent = await Agent.create(options);
|
|
344
|
+
ctx.agent = agent;
|
|
345
|
+
ctx.registeredSessionId = agent.sessionId;
|
|
346
|
+
|
|
347
|
+
this.sessions.set(agent.sessionId, {
|
|
348
|
+
agent,
|
|
349
|
+
storedConfig: { ...params },
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
if (params.clientVersion) {
|
|
353
|
+
console.debug(
|
|
354
|
+
`[agentBridge] clientVersion=${params.clientVersion} serverVersion=${CLI_VERSION}`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
sessionId: agent.sessionId,
|
|
360
|
+
workingDirectory: agent.workingDirectory,
|
|
361
|
+
permissionMode: agent.getPermissionMode(),
|
|
362
|
+
latestTotalTokens: agent.latestTotalTokens,
|
|
363
|
+
serverVersion: CLI_VERSION,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private async destroy(sessionId?: string): Promise<null> {
|
|
368
|
+
if (sessionId) {
|
|
369
|
+
const entry = this.sessions.get(sessionId);
|
|
370
|
+
if (entry) {
|
|
371
|
+
await entry.agent.destroy();
|
|
372
|
+
this.sessions.delete(sessionId);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private async restoreSession(
|
|
379
|
+
restoreId: string,
|
|
380
|
+
sessionId?: string,
|
|
381
|
+
): Promise<null> {
|
|
382
|
+
const entry = this.requireSession(sessionId);
|
|
383
|
+
await entry.agent.restoreSession(restoreId);
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private async listSessions(
|
|
388
|
+
workdir?: string,
|
|
389
|
+
sessionId?: string,
|
|
390
|
+
): Promise<{ sessions: SessionMetadata[] }> {
|
|
391
|
+
const sessions = await listSessions(
|
|
392
|
+
workdir || this.getSessionWorkdir(sessionId) || process.cwd(),
|
|
393
|
+
);
|
|
394
|
+
return { sessions };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private getSessionInfo(sessionId?: string): {
|
|
398
|
+
sessionId: string;
|
|
399
|
+
workingDirectory: string;
|
|
400
|
+
latestTotalTokens: number;
|
|
401
|
+
permissionMode: PermissionMode;
|
|
402
|
+
availableTools: string[];
|
|
403
|
+
} {
|
|
404
|
+
const entry = this.requireSession(sessionId);
|
|
405
|
+
return {
|
|
406
|
+
sessionId: entry.agent.sessionId,
|
|
407
|
+
workingDirectory: entry.agent.workingDirectory,
|
|
408
|
+
latestTotalTokens: entry.agent.latestTotalTokens,
|
|
409
|
+
permissionMode: entry.agent.getPermissionMode(),
|
|
410
|
+
availableTools: entry.agent.getAvailableToolNames(),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private async updateConfig(
|
|
415
|
+
params: UpdateConfigParams,
|
|
416
|
+
sessionId?: string,
|
|
417
|
+
): Promise<{ sessionId: string }> {
|
|
418
|
+
const entry = this.requireSession(sessionId);
|
|
419
|
+
const currentSessionId = entry.agent.sessionId;
|
|
420
|
+
// Merge new config into stored config
|
|
421
|
+
entry.storedConfig = { ...entry.storedConfig, ...params };
|
|
422
|
+
// Destroy and recreate within the same session slot
|
|
423
|
+
await entry.agent.destroy();
|
|
424
|
+
this.sessions.delete(currentSessionId);
|
|
425
|
+
|
|
426
|
+
const ctx: SessionContext = {};
|
|
427
|
+
const callbacks = this.createCallbacks(ctx);
|
|
428
|
+
const options: AgentOptions = {
|
|
429
|
+
callbacks,
|
|
430
|
+
workdir: entry.storedConfig.workdir,
|
|
431
|
+
restoreSessionId: currentSessionId,
|
|
432
|
+
apiKey: entry.storedConfig.apiKey,
|
|
433
|
+
baseURL: entry.storedConfig.baseURL,
|
|
434
|
+
defaultHeaders: entry.storedConfig.defaultHeaders,
|
|
435
|
+
model: entry.storedConfig.model,
|
|
436
|
+
fastModel: entry.storedConfig.fastModel,
|
|
437
|
+
language: entry.storedConfig.language,
|
|
438
|
+
permissionMode: entry.storedConfig.permissionMode,
|
|
439
|
+
tools: entry.storedConfig.tools,
|
|
440
|
+
allowedTools: entry.storedConfig.allowedTools,
|
|
441
|
+
disallowedTools: entry.storedConfig.disallowedTools,
|
|
442
|
+
plugins: entry.storedConfig.pluginDirs?.map((p) => ({
|
|
443
|
+
type: "local",
|
|
444
|
+
path: p,
|
|
445
|
+
})),
|
|
446
|
+
mcpServers: entry.storedConfig.mcpServers,
|
|
447
|
+
canUseTool: (context: ToolPermissionContext) =>
|
|
448
|
+
this.canUseTool(context, ctx),
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const agent = await Agent.create(options);
|
|
452
|
+
ctx.agent = agent;
|
|
453
|
+
ctx.registeredSessionId = agent.sessionId;
|
|
454
|
+
this.sessions.set(agent.sessionId, {
|
|
455
|
+
agent,
|
|
456
|
+
storedConfig: { ...entry.storedConfig },
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
return { sessionId: agent.sessionId };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ── Messages ──────────────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
private async sendMessage(
|
|
465
|
+
params: {
|
|
466
|
+
text: string;
|
|
467
|
+
images?: Array<{ path: string; mimeType: string }>;
|
|
468
|
+
force?: boolean;
|
|
469
|
+
},
|
|
470
|
+
sessionId?: string,
|
|
471
|
+
): Promise<null> {
|
|
472
|
+
const entry = this.requireSession(sessionId);
|
|
473
|
+
if (params.force) {
|
|
474
|
+
entry.agent.abortMessage();
|
|
475
|
+
}
|
|
476
|
+
// Save prompt to history (mirrors VSCE chatSession.ts:236-242)
|
|
477
|
+
try {
|
|
478
|
+
await PromptHistoryManager.addEntry(
|
|
479
|
+
params.text,
|
|
480
|
+
entry.agent.sessionId,
|
|
481
|
+
{},
|
|
482
|
+
entry.agent.workingDirectory,
|
|
483
|
+
);
|
|
484
|
+
} catch {
|
|
485
|
+
// Best-effort; don't block message sending on history save failure
|
|
486
|
+
}
|
|
487
|
+
await entry.agent.sendMessage(params.text, params.images);
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private async bang(command: string, sessionId?: string): Promise<null> {
|
|
492
|
+
const entry = this.requireSession(sessionId);
|
|
493
|
+
await entry.agent.bang(command);
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private async abortMessage(sessionId?: string): Promise<null> {
|
|
498
|
+
const entry = this.requireSession(sessionId);
|
|
499
|
+
entry.agent.abortMessage();
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private async clearMessages(sessionId?: string): Promise<null> {
|
|
504
|
+
const entry = this.requireSession(sessionId);
|
|
505
|
+
entry.agent.clearMessages();
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private async rewindToMessage(
|
|
510
|
+
messageId: string,
|
|
511
|
+
sessionId?: string,
|
|
512
|
+
): Promise<{
|
|
513
|
+
inputContent: string;
|
|
514
|
+
}> {
|
|
515
|
+
const entry = this.requireSession(sessionId);
|
|
516
|
+
const { messages } = await entry.agent.getFullMessageThread();
|
|
517
|
+
const index = messages.findIndex((m) => m.id === messageId);
|
|
518
|
+
if (index === -1) {
|
|
519
|
+
throw new RpcError(
|
|
520
|
+
PROTOCOL_INTERNAL_ERROR,
|
|
521
|
+
`Message not found: ${messageId}`,
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
const message = messages[index];
|
|
525
|
+
const textBlock = message.blocks.find((b) => b.type === "text") as
|
|
526
|
+
| { content?: string }
|
|
527
|
+
| undefined;
|
|
528
|
+
await entry.agent.truncateHistory(index);
|
|
529
|
+
return { inputContent: textBlock?.content || "" };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private deleteQueuedMessage(index: number, sessionId?: string): null {
|
|
533
|
+
const entry = this.requireSession(sessionId);
|
|
534
|
+
entry.agent.removeQueuedMessage(index);
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
private getMessages(sessionId?: string): { messages: Message[] } {
|
|
539
|
+
const entry = this.requireSession(sessionId);
|
|
540
|
+
return { messages: entry.agent.messages };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private async getFullMessageThread(sessionId?: string): Promise<{
|
|
544
|
+
messages: Message[];
|
|
545
|
+
sessionIds: string[];
|
|
546
|
+
}> {
|
|
547
|
+
const entry = this.requireSession(sessionId);
|
|
548
|
+
return entry.agent.getFullMessageThread();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ── Permissions ───────────────────────────────────────────────
|
|
552
|
+
|
|
553
|
+
private async setPermissionMode(
|
|
554
|
+
mode: PermissionMode,
|
|
555
|
+
sessionId?: string,
|
|
556
|
+
): Promise<null> {
|
|
557
|
+
const entry = this.requireSession(sessionId);
|
|
558
|
+
await entry.agent.setPermissionMode(mode);
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private getPermissionMode(sessionId?: string): { mode: PermissionMode } {
|
|
563
|
+
const entry = this.requireSession(sessionId);
|
|
564
|
+
return { mode: entry.agent.getPermissionMode() };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ── MCP ───────────────────────────────────────────────────────
|
|
568
|
+
|
|
569
|
+
private getMcpServers(sessionId?: string): { servers: McpServerStatus[] } {
|
|
570
|
+
const entry = this.requireSession(sessionId);
|
|
571
|
+
return { servers: entry.agent.getMcpServers() };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private async connectMcpServer(
|
|
575
|
+
serverName: string,
|
|
576
|
+
sessionId?: string,
|
|
577
|
+
): Promise<{ success: boolean }> {
|
|
578
|
+
const entry = this.requireSession(sessionId);
|
|
579
|
+
const success = await entry.agent.connectMcpServer(serverName);
|
|
580
|
+
return { success };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private async disconnectMcpServer(
|
|
584
|
+
serverName: string,
|
|
585
|
+
sessionId?: string,
|
|
586
|
+
): Promise<{ success: boolean }> {
|
|
587
|
+
const entry = this.requireSession(sessionId);
|
|
588
|
+
const success = await entry.agent.disconnectMcpServer(serverName);
|
|
589
|
+
return { success };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ── Commands ──────────────────────────────────────────────────
|
|
593
|
+
|
|
594
|
+
private getSlashCommands(sessionId?: string): { commands: SlashCommand[] } {
|
|
595
|
+
const entry = this.requireSession(sessionId);
|
|
596
|
+
return { commands: entry.agent.getSlashCommands() };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ── File / History (global) ───────────────────────────────────
|
|
600
|
+
|
|
601
|
+
private async searchFiles(
|
|
602
|
+
params: SearchFilesParams,
|
|
603
|
+
sessionId?: string,
|
|
604
|
+
): Promise<{ files: Awaited<ReturnType<typeof searchFiles>> }> {
|
|
605
|
+
const files = await searchFiles(params.query, {
|
|
606
|
+
maxResults: params.maxResults,
|
|
607
|
+
workingDirectory:
|
|
608
|
+
params.workdir || this.getSessionWorkdir(sessionId) || process.cwd(),
|
|
609
|
+
});
|
|
610
|
+
return { files };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private async getPromptHistory(
|
|
614
|
+
workdir?: string,
|
|
615
|
+
sessionId?: string,
|
|
616
|
+
): Promise<{
|
|
617
|
+
history: Awaited<ReturnType<typeof PromptHistoryManager.getHistory>>;
|
|
618
|
+
}> {
|
|
619
|
+
const history = await PromptHistoryManager.getHistory({
|
|
620
|
+
workdir: workdir || this.getSessionWorkdir(sessionId),
|
|
621
|
+
});
|
|
622
|
+
return { history };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
private async searchPromptHistory(
|
|
626
|
+
query: string,
|
|
627
|
+
workdir?: string,
|
|
628
|
+
sessionId?: string,
|
|
629
|
+
): Promise<{
|
|
630
|
+
history: Awaited<ReturnType<typeof PromptHistoryManager.searchHistory>>;
|
|
631
|
+
}> {
|
|
632
|
+
const history = await PromptHistoryManager.searchHistory(query, {
|
|
633
|
+
workdir: workdir || this.getSessionWorkdir(sessionId),
|
|
634
|
+
});
|
|
635
|
+
return { history };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ── canUseTool flow ───────────────────────────────────────────
|
|
639
|
+
|
|
640
|
+
private canUseTool(
|
|
641
|
+
context: ToolPermissionContext,
|
|
642
|
+
ctx: SessionContext,
|
|
643
|
+
): Promise<PermissionDecision> {
|
|
644
|
+
const requestId = `perm_${++this.permissionCounter}`;
|
|
645
|
+
return new Promise<PermissionDecision>((resolve) => {
|
|
646
|
+
this.pendingPermissions.set(requestId, resolve);
|
|
647
|
+
this.emit(
|
|
648
|
+
"permissionRequest",
|
|
649
|
+
{ requestId, context },
|
|
650
|
+
ctx.registeredSessionId,
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
// 5-minute timeout → auto-deny
|
|
654
|
+
setTimeout(
|
|
655
|
+
() => {
|
|
656
|
+
if (this.pendingPermissions.has(requestId)) {
|
|
657
|
+
this.pendingPermissions.delete(requestId);
|
|
658
|
+
resolve({
|
|
659
|
+
behavior: "deny",
|
|
660
|
+
message: "Permission request timed out",
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
5 * 60 * 1000,
|
|
665
|
+
);
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// ── Auth (global) ────────────────────────────────────────────
|
|
670
|
+
|
|
671
|
+
private async getAuthStatus(): Promise<{
|
|
672
|
+
isAuthenticated: boolean;
|
|
673
|
+
user: { id: string; email?: string } | undefined;
|
|
674
|
+
}> {
|
|
675
|
+
const authService = AuthService.getInstance();
|
|
676
|
+
return {
|
|
677
|
+
isAuthenticated: authService.isSSOAuthenticated(),
|
|
678
|
+
user: authService.getAuthUser(),
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private async login(
|
|
683
|
+
serverUrl?: string,
|
|
684
|
+
): Promise<{ user: { id: string; email?: string } | undefined }> {
|
|
685
|
+
const authService = AuthService.getInstance();
|
|
686
|
+
await authService.login({
|
|
687
|
+
onAuthUrl: (url: string) => {
|
|
688
|
+
this.emit("authUrl", { url });
|
|
689
|
+
},
|
|
690
|
+
serverUrl,
|
|
691
|
+
});
|
|
692
|
+
return { user: authService.getAuthUser() };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private async logout(): Promise<null> {
|
|
696
|
+
const authService = AuthService.getInstance();
|
|
697
|
+
await authService.clearAuth();
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ── Plugins (global) ─────────────────────────────────────────
|
|
702
|
+
|
|
703
|
+
private getPluginCore(workdir?: string, sessionId?: string): PluginCore {
|
|
704
|
+
const resolvedWorkdir =
|
|
705
|
+
workdir || this.getSessionWorkdir(sessionId) || process.cwd();
|
|
706
|
+
if (!this.pluginCore || this.pluginCoreWorkdir !== resolvedWorkdir) {
|
|
707
|
+
this.pluginCore = new PluginCore(resolvedWorkdir);
|
|
708
|
+
this.pluginCoreWorkdir = resolvedWorkdir;
|
|
709
|
+
}
|
|
710
|
+
return this.pluginCore;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
private async listPlugins(workdir?: string, sessionId?: string) {
|
|
714
|
+
const core = this.getPluginCore(workdir, sessionId);
|
|
715
|
+
const { plugins, mergedEnabled } = await core.listPlugins();
|
|
716
|
+
return {
|
|
717
|
+
plugins: plugins.map((p) => {
|
|
718
|
+
const pluginId = `${p.name}@${p.marketplace}`;
|
|
719
|
+
return {
|
|
720
|
+
id: pluginId,
|
|
721
|
+
name: p.name,
|
|
722
|
+
description: p.description,
|
|
723
|
+
marketplace: p.marketplace,
|
|
724
|
+
installed: p.installed,
|
|
725
|
+
version: p.version,
|
|
726
|
+
enabled: mergedEnabled[pluginId] !== false,
|
|
727
|
+
scope: p.scope,
|
|
728
|
+
};
|
|
729
|
+
}),
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private async installPlugin(
|
|
734
|
+
pluginId: string,
|
|
735
|
+
scope?: Scope,
|
|
736
|
+
workdir?: string,
|
|
737
|
+
sessionId?: string,
|
|
738
|
+
) {
|
|
739
|
+
return this.getPluginCore(workdir, sessionId).installPlugin(
|
|
740
|
+
pluginId,
|
|
741
|
+
scope,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private async uninstallPlugin(
|
|
746
|
+
pluginId: string,
|
|
747
|
+
workdir?: string,
|
|
748
|
+
sessionId?: string,
|
|
749
|
+
) {
|
|
750
|
+
await this.getPluginCore(workdir, sessionId).uninstallPlugin(pluginId);
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
private async enablePlugin(
|
|
755
|
+
pluginId: string,
|
|
756
|
+
scope?: Scope,
|
|
757
|
+
workdir?: string,
|
|
758
|
+
sessionId?: string,
|
|
759
|
+
) {
|
|
760
|
+
return this.getPluginCore(workdir, sessionId).enablePlugin(pluginId, scope);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
private async disablePlugin(
|
|
764
|
+
pluginId: string,
|
|
765
|
+
scope?: Scope,
|
|
766
|
+
workdir?: string,
|
|
767
|
+
sessionId?: string,
|
|
768
|
+
) {
|
|
769
|
+
return this.getPluginCore(workdir, sessionId).disablePlugin(
|
|
770
|
+
pluginId,
|
|
771
|
+
scope,
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
private async updatePlugin(
|
|
776
|
+
pluginId: string,
|
|
777
|
+
workdir?: string,
|
|
778
|
+
sessionId?: string,
|
|
779
|
+
) {
|
|
780
|
+
return this.getPluginCore(workdir, sessionId).updatePlugin(pluginId);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
private async listMarketplaces(workdir?: string, sessionId?: string) {
|
|
784
|
+
return this.getPluginCore(workdir, sessionId).listMarketplaces();
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private async addMarketplace(
|
|
788
|
+
input: string,
|
|
789
|
+
scope?: Scope,
|
|
790
|
+
workdir?: string,
|
|
791
|
+
sessionId?: string,
|
|
792
|
+
) {
|
|
793
|
+
return this.getPluginCore(workdir, sessionId).addMarketplace(input, scope);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private async removeMarketplace(
|
|
797
|
+
name: string,
|
|
798
|
+
scope?: Scope,
|
|
799
|
+
workdir?: string,
|
|
800
|
+
sessionId?: string,
|
|
801
|
+
) {
|
|
802
|
+
await this.getPluginCore(workdir, sessionId).removeMarketplace(name, scope);
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
private async updateMarketplace(
|
|
807
|
+
name?: string,
|
|
808
|
+
workdir?: string,
|
|
809
|
+
sessionId?: string,
|
|
810
|
+
) {
|
|
811
|
+
await this.getPluginCore(workdir, sessionId).updateMarketplace(name);
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ── Callbacks → Notifications ─────────────────────────────────
|
|
816
|
+
|
|
817
|
+
private createCallbacks(ctx: SessionContext): AgentCallbacks {
|
|
818
|
+
return {
|
|
819
|
+
onMessagesChange: (messages: Message[]) => {
|
|
820
|
+
this.emit("messagesChange", { messages }, ctx.registeredSessionId);
|
|
821
|
+
},
|
|
822
|
+
onUserMessageAdded: () => {
|
|
823
|
+
const msg = this.findLastUserMessage(ctx.agent);
|
|
824
|
+
if (msg)
|
|
825
|
+
this.emit(
|
|
826
|
+
"userMessageAdded",
|
|
827
|
+
{ message: msg },
|
|
828
|
+
ctx.registeredSessionId,
|
|
829
|
+
);
|
|
830
|
+
},
|
|
831
|
+
onAssistantMessageAdded: (messageId: string) => {
|
|
832
|
+
const msg = ctx.agent?.messages.find((m) => m.id === messageId);
|
|
833
|
+
if (msg)
|
|
834
|
+
this.emit(
|
|
835
|
+
"assistantMessageAdded",
|
|
836
|
+
{ message: msg },
|
|
837
|
+
ctx.registeredSessionId,
|
|
838
|
+
);
|
|
839
|
+
},
|
|
840
|
+
onAssistantContentUpdated: (params) => {
|
|
841
|
+
this.emit("assistantContentUpdated", params, ctx.registeredSessionId);
|
|
842
|
+
},
|
|
843
|
+
onAssistantReasoningUpdated: (params) => {
|
|
844
|
+
this.emit("assistantReasoningUpdated", params, ctx.registeredSessionId);
|
|
845
|
+
},
|
|
846
|
+
onToolBlockUpdated: (params) => {
|
|
847
|
+
this.emit("toolBlockUpdated", params, ctx.registeredSessionId);
|
|
848
|
+
},
|
|
849
|
+
onErrorBlockAdded: (error: string) => {
|
|
850
|
+
this.emit("errorBlockAdded", { error }, ctx.registeredSessionId);
|
|
851
|
+
},
|
|
852
|
+
onLoadingChange: (loading: boolean) => {
|
|
853
|
+
this.emit(
|
|
854
|
+
"loadingChange",
|
|
855
|
+
{
|
|
856
|
+
loading,
|
|
857
|
+
latestTotalTokens: ctx.agent?.latestTotalTokens,
|
|
858
|
+
},
|
|
859
|
+
ctx.registeredSessionId,
|
|
860
|
+
);
|
|
861
|
+
},
|
|
862
|
+
onCommandRunningChange: (running: boolean) => {
|
|
863
|
+
this.emit("commandRunningChange", { running }, ctx.registeredSessionId);
|
|
864
|
+
},
|
|
865
|
+
onQueuedMessagesChange: (messages: QueuedMessage[]) => {
|
|
866
|
+
this.emit(
|
|
867
|
+
"queuedMessagesChange",
|
|
868
|
+
{ messages },
|
|
869
|
+
ctx.registeredSessionId,
|
|
870
|
+
);
|
|
871
|
+
},
|
|
872
|
+
onTasksChange: (tasks: Task[]) => {
|
|
873
|
+
this.emit("tasksChange", { tasks }, ctx.registeredSessionId);
|
|
874
|
+
},
|
|
875
|
+
onSessionIdChange: (newSessionId: string) => {
|
|
876
|
+
const oldSessionId = ctx.registeredSessionId;
|
|
877
|
+
// Emit with the OLD sessionId so the client's router can deliver it
|
|
878
|
+
this.emit("sessionIdChange", { sessionId: newSessionId }, oldSessionId);
|
|
879
|
+
// Update the sessions Map key atomically (single-threaded, no await)
|
|
880
|
+
if (oldSessionId && oldSessionId !== newSessionId) {
|
|
881
|
+
const entry = this.sessions.get(oldSessionId);
|
|
882
|
+
if (entry) {
|
|
883
|
+
this.sessions.delete(oldSessionId);
|
|
884
|
+
this.sessions.set(newSessionId, entry);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
ctx.registeredSessionId = newSessionId;
|
|
888
|
+
},
|
|
889
|
+
onPermissionModeChange: (mode: PermissionMode) => {
|
|
890
|
+
this.emit("permissionModeChange", { mode }, ctx.registeredSessionId);
|
|
891
|
+
},
|
|
892
|
+
onMcpServersChange: (servers: McpServerStatus[]) => {
|
|
893
|
+
this.emit("mcpServersChange", { servers }, ctx.registeredSessionId);
|
|
894
|
+
},
|
|
895
|
+
onAddBangMessage: () => {
|
|
896
|
+
this.emit("bangMessageAdded", {}, ctx.registeredSessionId);
|
|
897
|
+
},
|
|
898
|
+
onUpdateBangMessage: () => {
|
|
899
|
+
this.emit("bangMessageUpdated", {}, ctx.registeredSessionId);
|
|
900
|
+
},
|
|
901
|
+
onCompleteBangMessage: () => {
|
|
902
|
+
this.emit("bangMessageCompleted", {}, ctx.registeredSessionId);
|
|
903
|
+
},
|
|
904
|
+
onNotificationMessageAdded: (params) => {
|
|
905
|
+
const msg = ctx.agent?.messages.find(
|
|
906
|
+
(m) =>
|
|
907
|
+
m.role === "user" &&
|
|
908
|
+
m.blocks.some(
|
|
909
|
+
(b) =>
|
|
910
|
+
b.type === "task_notification" &&
|
|
911
|
+
(b as { taskId: string }).taskId === params.taskId,
|
|
912
|
+
),
|
|
913
|
+
);
|
|
914
|
+
this.emit(
|
|
915
|
+
"notificationMessageAdded",
|
|
916
|
+
{ ...params, message: msg },
|
|
917
|
+
ctx.registeredSessionId,
|
|
918
|
+
);
|
|
919
|
+
},
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
private findLastUserMessage(agent?: Agent): Message | undefined {
|
|
924
|
+
const userMessages = agent?.messages.filter((m) => m.role === "user") ?? [];
|
|
925
|
+
return userMessages[userMessages.length - 1];
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// ── Utils ─────────────────────────────────────────────────────
|
|
929
|
+
|
|
930
|
+
private requireSession(sessionId?: string): SessionEntry {
|
|
931
|
+
if (!sessionId) {
|
|
932
|
+
throw new RpcError(
|
|
933
|
+
PROTOCOL_INTERNAL_ERROR,
|
|
934
|
+
"sessionId is required for this request",
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
const entry = this.sessions.get(sessionId);
|
|
938
|
+
if (!entry) {
|
|
939
|
+
throw new RpcError(
|
|
940
|
+
PROTOCOL_INTERNAL_ERROR,
|
|
941
|
+
`Session not found: ${sessionId}`,
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
return entry;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
private getSessionWorkdir(sessionId?: string): string | undefined {
|
|
948
|
+
if (!sessionId) return undefined;
|
|
949
|
+
return this.sessions.get(sessionId)?.agent.workingDirectory;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// ── Error class for protocol errors ─────────────────────────────
|
|
954
|
+
|
|
955
|
+
export class RpcError extends Error {
|
|
956
|
+
code: number;
|
|
957
|
+
constructor(code: number, message: string) {
|
|
958
|
+
super(message);
|
|
959
|
+
this.code = code;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
toJsonRpcError(): JsonRpcError {
|
|
963
|
+
return { code: this.code, message: this.message };
|
|
964
|
+
}
|
|
965
|
+
}
|