@github/copilot-sdk 0.0.1 → 0.1.10-preview.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/client.d.ts +95 -0
- package/dist/client.js +551 -0
- package/dist/generated/session-events.d.ts +384 -0
- package/dist/generated/session-events.js +0 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/session.d.ts +39 -0
- package/dist/session.js +83 -0
- package/dist/types.d.ts +277 -0
- package/dist/types.js +6 -0
- package/package.json +67 -2
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Coming soon
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { CopilotSession } from "./session.js";
|
|
2
|
+
import type { ConnectionState, CopilotClientOptions, ResumeSessionConfig, SessionConfig, SessionMetadata } from "./types.js";
|
|
3
|
+
export declare class CopilotClient {
|
|
4
|
+
private cliProcess;
|
|
5
|
+
private connection;
|
|
6
|
+
private socket;
|
|
7
|
+
private actualPort;
|
|
8
|
+
private actualHost;
|
|
9
|
+
private state;
|
|
10
|
+
private sessions;
|
|
11
|
+
private options;
|
|
12
|
+
private isExternalServer;
|
|
13
|
+
private forceStopping;
|
|
14
|
+
constructor(options?: CopilotClientOptions);
|
|
15
|
+
/**
|
|
16
|
+
* Parse CLI URL into host and port
|
|
17
|
+
* Supports formats: "host:port", "http://host:port", "https://host:port", or just "port"
|
|
18
|
+
*/
|
|
19
|
+
private parseCliUrl;
|
|
20
|
+
/**
|
|
21
|
+
* Start the CLI server and establish connection
|
|
22
|
+
*/
|
|
23
|
+
start(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Stop the CLI server and close all sessions
|
|
26
|
+
* Returns array of errors encountered during cleanup (empty if all succeeded)
|
|
27
|
+
*/
|
|
28
|
+
stop(): Promise<Error[]>;
|
|
29
|
+
/**
|
|
30
|
+
* Force stop the CLI server without graceful cleanup
|
|
31
|
+
* Use when normal stop() fails or takes too long
|
|
32
|
+
*/
|
|
33
|
+
forceStop(): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Create a new session
|
|
36
|
+
*/
|
|
37
|
+
createSession(config?: SessionConfig): Promise<CopilotSession>;
|
|
38
|
+
/**
|
|
39
|
+
* Resume an existing session
|
|
40
|
+
*/
|
|
41
|
+
resumeSession(sessionId: string, config?: ResumeSessionConfig): Promise<CopilotSession>;
|
|
42
|
+
/**
|
|
43
|
+
* Get connection state
|
|
44
|
+
*/
|
|
45
|
+
getState(): ConnectionState;
|
|
46
|
+
/**
|
|
47
|
+
* Ping the server
|
|
48
|
+
*/
|
|
49
|
+
ping(message?: string): Promise<{
|
|
50
|
+
message: string;
|
|
51
|
+
timestamp: number;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Get the ID of the most recently updated session
|
|
55
|
+
* @returns The session ID, or undefined if no sessions exist
|
|
56
|
+
*/
|
|
57
|
+
getLastSessionId(): Promise<string | undefined>;
|
|
58
|
+
/**
|
|
59
|
+
* Delete a session and its data from disk
|
|
60
|
+
* @param sessionId The ID of the session to delete
|
|
61
|
+
*/
|
|
62
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* List all available sessions
|
|
65
|
+
* @returns Array of session metadata
|
|
66
|
+
*/
|
|
67
|
+
listSessions(): Promise<SessionMetadata[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Start the CLI server process
|
|
70
|
+
*/
|
|
71
|
+
private startCLIServer;
|
|
72
|
+
/**
|
|
73
|
+
* Connect to the CLI server (via socket or stdio)
|
|
74
|
+
*/
|
|
75
|
+
private connectToServer;
|
|
76
|
+
/**
|
|
77
|
+
* Connect via stdio pipes
|
|
78
|
+
*/
|
|
79
|
+
private connectViaStdio;
|
|
80
|
+
/**
|
|
81
|
+
* Connect to the CLI server via TCP socket
|
|
82
|
+
*/
|
|
83
|
+
private connectViaTcp;
|
|
84
|
+
private attachConnectionHandlers;
|
|
85
|
+
private handleSessionEventNotification;
|
|
86
|
+
private handleToolCallRequest;
|
|
87
|
+
private executeToolCall;
|
|
88
|
+
private normalizeToolResult;
|
|
89
|
+
private isToolResultObject;
|
|
90
|
+
private buildUnsupportedToolResult;
|
|
91
|
+
/**
|
|
92
|
+
* Attempt to reconnect to the server
|
|
93
|
+
*/
|
|
94
|
+
private reconnect;
|
|
95
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { Socket } from "node:net";
|
|
3
|
+
import {
|
|
4
|
+
createMessageConnection,
|
|
5
|
+
StreamMessageReader,
|
|
6
|
+
StreamMessageWriter
|
|
7
|
+
} from "vscode-jsonrpc/node.js";
|
|
8
|
+
import { CopilotSession } from "./session.js";
|
|
9
|
+
function isZodSchema(value) {
|
|
10
|
+
return value != null && typeof value === "object" && "toJSONSchema" in value && typeof value.toJSONSchema === "function";
|
|
11
|
+
}
|
|
12
|
+
function toJsonSchema(parameters) {
|
|
13
|
+
if (!parameters) return void 0;
|
|
14
|
+
if (isZodSchema(parameters)) {
|
|
15
|
+
return parameters.toJSONSchema();
|
|
16
|
+
}
|
|
17
|
+
return parameters;
|
|
18
|
+
}
|
|
19
|
+
class CopilotClient {
|
|
20
|
+
cliProcess = null;
|
|
21
|
+
connection = null;
|
|
22
|
+
socket = null;
|
|
23
|
+
actualPort = null;
|
|
24
|
+
actualHost = "localhost";
|
|
25
|
+
state = "disconnected";
|
|
26
|
+
sessions = /* @__PURE__ */ new Map();
|
|
27
|
+
options;
|
|
28
|
+
isExternalServer = false;
|
|
29
|
+
forceStopping = false;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
if (options.cliUrl && (options.useStdio === true || options.cliPath)) {
|
|
32
|
+
throw new Error("cliUrl is mutually exclusive with useStdio and cliPath");
|
|
33
|
+
}
|
|
34
|
+
if (options.cliUrl) {
|
|
35
|
+
const { host, port } = this.parseCliUrl(options.cliUrl);
|
|
36
|
+
this.actualHost = host;
|
|
37
|
+
this.actualPort = port;
|
|
38
|
+
this.isExternalServer = true;
|
|
39
|
+
}
|
|
40
|
+
this.options = {
|
|
41
|
+
cliPath: options.cliPath || "copilot",
|
|
42
|
+
cliArgs: options.cliArgs ?? [],
|
|
43
|
+
cwd: options.cwd ?? process.cwd(),
|
|
44
|
+
port: options.port || 0,
|
|
45
|
+
useStdio: options.cliUrl ? false : options.useStdio ?? true,
|
|
46
|
+
// Default to stdio unless cliUrl is provided
|
|
47
|
+
cliUrl: options.cliUrl,
|
|
48
|
+
logLevel: options.logLevel || "info",
|
|
49
|
+
autoStart: options.autoStart ?? true,
|
|
50
|
+
autoRestart: options.autoRestart ?? true,
|
|
51
|
+
env: options.env ?? process.env
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Parse CLI URL into host and port
|
|
56
|
+
* Supports formats: "host:port", "http://host:port", "https://host:port", or just "port"
|
|
57
|
+
*/
|
|
58
|
+
parseCliUrl(url) {
|
|
59
|
+
let cleanUrl = url.replace(/^https?:\/\//, "");
|
|
60
|
+
if (/^\d+$/.test(cleanUrl)) {
|
|
61
|
+
return { host: "localhost", port: parseInt(cleanUrl, 10) };
|
|
62
|
+
}
|
|
63
|
+
const parts = cleanUrl.split(":");
|
|
64
|
+
if (parts.length !== 2) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Invalid cliUrl format: ${url}. Expected "host:port", "http://host:port", or "port"`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const host = parts[0] || "localhost";
|
|
70
|
+
const port = parseInt(parts[1], 10);
|
|
71
|
+
if (isNaN(port) || port <= 0 || port > 65535) {
|
|
72
|
+
throw new Error(`Invalid port in cliUrl: ${url}`);
|
|
73
|
+
}
|
|
74
|
+
return { host, port };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Start the CLI server and establish connection
|
|
78
|
+
*/
|
|
79
|
+
async start() {
|
|
80
|
+
if (this.state === "connected") {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.state = "connecting";
|
|
84
|
+
try {
|
|
85
|
+
if (!this.isExternalServer) {
|
|
86
|
+
await this.startCLIServer();
|
|
87
|
+
}
|
|
88
|
+
await this.connectToServer();
|
|
89
|
+
this.state = "connected";
|
|
90
|
+
} catch (error) {
|
|
91
|
+
this.state = "error";
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Stop the CLI server and close all sessions
|
|
97
|
+
* Returns array of errors encountered during cleanup (empty if all succeeded)
|
|
98
|
+
*/
|
|
99
|
+
async stop() {
|
|
100
|
+
const errors = [];
|
|
101
|
+
for (const session of this.sessions.values()) {
|
|
102
|
+
const sessionId = session.sessionId;
|
|
103
|
+
let lastError = null;
|
|
104
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
105
|
+
try {
|
|
106
|
+
await session.destroy();
|
|
107
|
+
lastError = null;
|
|
108
|
+
break;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
111
|
+
if (attempt < 3) {
|
|
112
|
+
const delay = 100 * Math.pow(2, attempt - 1);
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (lastError) {
|
|
118
|
+
errors.push(
|
|
119
|
+
new Error(
|
|
120
|
+
`Failed to destroy session ${sessionId} after 3 attempts: ${lastError.message}`
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
this.sessions.clear();
|
|
126
|
+
if (this.connection) {
|
|
127
|
+
try {
|
|
128
|
+
this.connection.dispose();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
errors.push(
|
|
131
|
+
new Error(
|
|
132
|
+
`Failed to dispose connection: ${error instanceof Error ? error.message : String(error)}`
|
|
133
|
+
)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
this.connection = null;
|
|
137
|
+
}
|
|
138
|
+
if (this.socket) {
|
|
139
|
+
try {
|
|
140
|
+
this.socket.end();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
errors.push(
|
|
143
|
+
new Error(
|
|
144
|
+
`Failed to close socket: ${error instanceof Error ? error.message : String(error)}`
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
this.socket = null;
|
|
149
|
+
}
|
|
150
|
+
if (this.cliProcess && !this.isExternalServer) {
|
|
151
|
+
try {
|
|
152
|
+
this.cliProcess.kill();
|
|
153
|
+
} catch (error) {
|
|
154
|
+
errors.push(
|
|
155
|
+
new Error(
|
|
156
|
+
`Failed to kill CLI process: ${error instanceof Error ? error.message : String(error)}`
|
|
157
|
+
)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
this.cliProcess = null;
|
|
161
|
+
}
|
|
162
|
+
this.state = "disconnected";
|
|
163
|
+
this.actualPort = null;
|
|
164
|
+
return errors;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Force stop the CLI server without graceful cleanup
|
|
168
|
+
* Use when normal stop() fails or takes too long
|
|
169
|
+
*/
|
|
170
|
+
async forceStop() {
|
|
171
|
+
this.forceStopping = true;
|
|
172
|
+
this.sessions.clear();
|
|
173
|
+
if (this.connection) {
|
|
174
|
+
try {
|
|
175
|
+
this.connection.dispose();
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
this.connection = null;
|
|
179
|
+
}
|
|
180
|
+
if (this.socket) {
|
|
181
|
+
try {
|
|
182
|
+
this.socket.destroy();
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
this.socket = null;
|
|
186
|
+
}
|
|
187
|
+
if (this.cliProcess && !this.isExternalServer) {
|
|
188
|
+
try {
|
|
189
|
+
this.cliProcess.kill("SIGKILL");
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
this.cliProcess = null;
|
|
193
|
+
}
|
|
194
|
+
this.state = "disconnected";
|
|
195
|
+
this.actualPort = null;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Create a new session
|
|
199
|
+
*/
|
|
200
|
+
async createSession(config = {}) {
|
|
201
|
+
if (!this.connection) {
|
|
202
|
+
if (this.options.autoStart) {
|
|
203
|
+
await this.start();
|
|
204
|
+
} else {
|
|
205
|
+
throw new Error("Client not connected. Call start() first.");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const response = await this.connection.sendRequest("session.create", {
|
|
209
|
+
model: config.model,
|
|
210
|
+
sessionId: config.sessionId,
|
|
211
|
+
tools: config.tools?.map((tool) => ({
|
|
212
|
+
name: tool.name,
|
|
213
|
+
description: tool.description,
|
|
214
|
+
parameters: toJsonSchema(tool.parameters)
|
|
215
|
+
})),
|
|
216
|
+
systemMessage: config.systemMessage,
|
|
217
|
+
availableTools: config.availableTools,
|
|
218
|
+
excludedTools: config.excludedTools,
|
|
219
|
+
provider: config.provider,
|
|
220
|
+
streaming: config.streaming
|
|
221
|
+
});
|
|
222
|
+
const sessionId = response.sessionId;
|
|
223
|
+
const session = new CopilotSession(sessionId, this.connection);
|
|
224
|
+
session.registerTools(config.tools);
|
|
225
|
+
this.sessions.set(sessionId, session);
|
|
226
|
+
return session;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Resume an existing session
|
|
230
|
+
*/
|
|
231
|
+
async resumeSession(sessionId, config = {}) {
|
|
232
|
+
if (!this.connection) {
|
|
233
|
+
if (this.options.autoStart) {
|
|
234
|
+
await this.start();
|
|
235
|
+
} else {
|
|
236
|
+
throw new Error("Client not connected. Call start() first.");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const response = await this.connection.sendRequest("session.resume", {
|
|
240
|
+
sessionId,
|
|
241
|
+
tools: config.tools?.map((tool) => ({
|
|
242
|
+
name: tool.name,
|
|
243
|
+
description: tool.description,
|
|
244
|
+
parameters: toJsonSchema(tool.parameters)
|
|
245
|
+
})),
|
|
246
|
+
provider: config.provider,
|
|
247
|
+
streaming: config.streaming
|
|
248
|
+
});
|
|
249
|
+
const resumedSessionId = response.sessionId;
|
|
250
|
+
const session = new CopilotSession(resumedSessionId, this.connection);
|
|
251
|
+
session.registerTools(config.tools);
|
|
252
|
+
this.sessions.set(resumedSessionId, session);
|
|
253
|
+
return session;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Get connection state
|
|
257
|
+
*/
|
|
258
|
+
getState() {
|
|
259
|
+
return this.state;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Ping the server
|
|
263
|
+
*/
|
|
264
|
+
async ping(message) {
|
|
265
|
+
if (!this.connection) {
|
|
266
|
+
throw new Error("Client not connected");
|
|
267
|
+
}
|
|
268
|
+
const result = await this.connection.sendRequest("ping", { message });
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Get the ID of the most recently updated session
|
|
273
|
+
* @returns The session ID, or undefined if no sessions exist
|
|
274
|
+
*/
|
|
275
|
+
async getLastSessionId() {
|
|
276
|
+
if (!this.connection) {
|
|
277
|
+
throw new Error("Client not connected");
|
|
278
|
+
}
|
|
279
|
+
const response = await this.connection.sendRequest("session.getLastId", {});
|
|
280
|
+
return response.sessionId;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Delete a session and its data from disk
|
|
284
|
+
* @param sessionId The ID of the session to delete
|
|
285
|
+
*/
|
|
286
|
+
async deleteSession(sessionId) {
|
|
287
|
+
if (!this.connection) {
|
|
288
|
+
throw new Error("Client not connected");
|
|
289
|
+
}
|
|
290
|
+
const response = await this.connection.sendRequest("session.delete", {
|
|
291
|
+
sessionId
|
|
292
|
+
});
|
|
293
|
+
const { success, error } = response;
|
|
294
|
+
if (!success) {
|
|
295
|
+
throw new Error(`Failed to delete session ${sessionId}: ${error || "Unknown error"}`);
|
|
296
|
+
}
|
|
297
|
+
this.sessions.delete(sessionId);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* List all available sessions
|
|
301
|
+
* @returns Array of session metadata
|
|
302
|
+
*/
|
|
303
|
+
async listSessions() {
|
|
304
|
+
if (!this.connection) {
|
|
305
|
+
throw new Error("Client not connected");
|
|
306
|
+
}
|
|
307
|
+
const response = await this.connection.sendRequest("session.list", {});
|
|
308
|
+
const { sessions } = response;
|
|
309
|
+
return sessions.map((s) => ({
|
|
310
|
+
sessionId: s.sessionId,
|
|
311
|
+
startTime: new Date(s.startTime),
|
|
312
|
+
modifiedTime: new Date(s.modifiedTime),
|
|
313
|
+
summary: s.summary,
|
|
314
|
+
isRemote: s.isRemote
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Start the CLI server process
|
|
319
|
+
*/
|
|
320
|
+
async startCLIServer() {
|
|
321
|
+
return new Promise((resolve, reject) => {
|
|
322
|
+
const args = [
|
|
323
|
+
...this.options.cliArgs,
|
|
324
|
+
"--server",
|
|
325
|
+
"--log-level",
|
|
326
|
+
this.options.logLevel
|
|
327
|
+
];
|
|
328
|
+
if (this.options.useStdio) {
|
|
329
|
+
args.push("--stdio");
|
|
330
|
+
} else if (this.options.port > 0) {
|
|
331
|
+
args.push("--port", this.options.port.toString());
|
|
332
|
+
}
|
|
333
|
+
const envWithoutNodeDebug = { ...this.options.env };
|
|
334
|
+
delete envWithoutNodeDebug.NODE_DEBUG;
|
|
335
|
+
const isJsFile = this.options.cliPath.endsWith(".js");
|
|
336
|
+
const command = isJsFile ? "node" : this.options.cliPath;
|
|
337
|
+
const spawnArgs = isJsFile ? [this.options.cliPath, ...args] : args;
|
|
338
|
+
this.cliProcess = spawn(command, spawnArgs, {
|
|
339
|
+
stdio: this.options.useStdio ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
|
|
340
|
+
cwd: this.options.cwd,
|
|
341
|
+
env: envWithoutNodeDebug
|
|
342
|
+
});
|
|
343
|
+
let stdout = "";
|
|
344
|
+
let resolved = false;
|
|
345
|
+
if (this.options.useStdio) {
|
|
346
|
+
resolved = true;
|
|
347
|
+
resolve();
|
|
348
|
+
} else {
|
|
349
|
+
this.cliProcess.stdout?.on("data", (data) => {
|
|
350
|
+
stdout += data.toString();
|
|
351
|
+
const match = stdout.match(/listening on port (\d+)/i);
|
|
352
|
+
if (match && !resolved) {
|
|
353
|
+
this.actualPort = parseInt(match[1], 10);
|
|
354
|
+
resolved = true;
|
|
355
|
+
resolve();
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
this.cliProcess.stderr?.on("data", (data) => {
|
|
360
|
+
const lines = data.toString().split("\n");
|
|
361
|
+
for (const line of lines) {
|
|
362
|
+
if (line.trim()) {
|
|
363
|
+
process.stderr.write(`[CLI subprocess] ${line}
|
|
364
|
+
`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
this.cliProcess.on("error", (error) => {
|
|
369
|
+
if (!resolved) {
|
|
370
|
+
resolved = true;
|
|
371
|
+
reject(new Error(`Failed to start CLI server: ${error.message}`));
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
this.cliProcess.on("exit", (code) => {
|
|
375
|
+
if (!resolved) {
|
|
376
|
+
resolved = true;
|
|
377
|
+
reject(new Error(`CLI server exited with code ${code}`));
|
|
378
|
+
} else if (this.options.autoRestart && this.state === "connected") {
|
|
379
|
+
void this.reconnect();
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
setTimeout(() => {
|
|
383
|
+
if (!resolved) {
|
|
384
|
+
resolved = true;
|
|
385
|
+
reject(new Error("Timeout waiting for CLI server to start"));
|
|
386
|
+
}
|
|
387
|
+
}, 1e4);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Connect to the CLI server (via socket or stdio)
|
|
392
|
+
*/
|
|
393
|
+
async connectToServer() {
|
|
394
|
+
if (this.options.useStdio) {
|
|
395
|
+
return this.connectViaStdio();
|
|
396
|
+
} else {
|
|
397
|
+
return this.connectViaTcp();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Connect via stdio pipes
|
|
402
|
+
*/
|
|
403
|
+
async connectViaStdio() {
|
|
404
|
+
if (!this.cliProcess) {
|
|
405
|
+
throw new Error("CLI process not started");
|
|
406
|
+
}
|
|
407
|
+
this.cliProcess.stdin?.on("error", (err) => {
|
|
408
|
+
if (!this.forceStopping) {
|
|
409
|
+
throw err;
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
this.connection = createMessageConnection(
|
|
413
|
+
new StreamMessageReader(this.cliProcess.stdout),
|
|
414
|
+
new StreamMessageWriter(this.cliProcess.stdin)
|
|
415
|
+
);
|
|
416
|
+
this.attachConnectionHandlers();
|
|
417
|
+
this.connection.listen();
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Connect to the CLI server via TCP socket
|
|
421
|
+
*/
|
|
422
|
+
async connectViaTcp() {
|
|
423
|
+
if (!this.actualPort) {
|
|
424
|
+
throw new Error("Server port not available");
|
|
425
|
+
}
|
|
426
|
+
return new Promise((resolve, reject) => {
|
|
427
|
+
this.socket = new Socket();
|
|
428
|
+
this.socket.connect(this.actualPort, this.actualHost, () => {
|
|
429
|
+
this.connection = createMessageConnection(
|
|
430
|
+
new StreamMessageReader(this.socket),
|
|
431
|
+
new StreamMessageWriter(this.socket)
|
|
432
|
+
);
|
|
433
|
+
this.attachConnectionHandlers();
|
|
434
|
+
this.connection.listen();
|
|
435
|
+
resolve();
|
|
436
|
+
});
|
|
437
|
+
this.socket.on("error", (error) => {
|
|
438
|
+
reject(new Error(`Failed to connect to CLI server: ${error.message}`));
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
attachConnectionHandlers() {
|
|
443
|
+
if (!this.connection) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
this.connection.onNotification("session.event", (notification) => {
|
|
447
|
+
this.handleSessionEventNotification(notification);
|
|
448
|
+
});
|
|
449
|
+
this.connection.onRequest(
|
|
450
|
+
"tool.call",
|
|
451
|
+
async (params) => await this.handleToolCallRequest(params)
|
|
452
|
+
);
|
|
453
|
+
this.connection.onClose(() => {
|
|
454
|
+
if (this.state === "connected" && this.options.autoRestart) {
|
|
455
|
+
void this.reconnect();
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
this.connection.onError((_error) => {
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
handleSessionEventNotification(notification) {
|
|
462
|
+
if (typeof notification !== "object" || !notification || !("sessionId" in notification) || typeof notification.sessionId !== "string" || !("event" in notification)) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const session = this.sessions.get(notification.sessionId);
|
|
466
|
+
if (session) {
|
|
467
|
+
session._dispatchEvent(notification.event);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
async handleToolCallRequest(params) {
|
|
471
|
+
if (!params || typeof params.sessionId !== "string" || typeof params.toolCallId !== "string" || typeof params.toolName !== "string") {
|
|
472
|
+
throw new Error("Invalid tool call payload");
|
|
473
|
+
}
|
|
474
|
+
const session = this.sessions.get(params.sessionId);
|
|
475
|
+
if (!session) {
|
|
476
|
+
throw new Error(`Unknown session ${params.sessionId}`);
|
|
477
|
+
}
|
|
478
|
+
const handler = session.getToolHandler(params.toolName);
|
|
479
|
+
if (!handler) {
|
|
480
|
+
return { result: this.buildUnsupportedToolResult(params.toolName) };
|
|
481
|
+
}
|
|
482
|
+
return await this.executeToolCall(handler, params);
|
|
483
|
+
}
|
|
484
|
+
async executeToolCall(handler, request) {
|
|
485
|
+
try {
|
|
486
|
+
const invocation = {
|
|
487
|
+
sessionId: request.sessionId,
|
|
488
|
+
toolCallId: request.toolCallId,
|
|
489
|
+
toolName: request.toolName,
|
|
490
|
+
arguments: request.arguments
|
|
491
|
+
};
|
|
492
|
+
const result = await handler(request.arguments, invocation);
|
|
493
|
+
return { result: this.normalizeToolResult(result) };
|
|
494
|
+
} catch (error) {
|
|
495
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
496
|
+
return {
|
|
497
|
+
result: {
|
|
498
|
+
// Don't expose detailed error information to the LLM for security reasons
|
|
499
|
+
textResultForLlm: "Invoking this tool produced an error. Detailed information is not available.",
|
|
500
|
+
resultType: "failure",
|
|
501
|
+
error: message,
|
|
502
|
+
toolTelemetry: {}
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
normalizeToolResult(result) {
|
|
508
|
+
if (result === void 0 || result === null) {
|
|
509
|
+
return {
|
|
510
|
+
textResultForLlm: "Tool returned no result",
|
|
511
|
+
resultType: "failure",
|
|
512
|
+
error: "tool returned no result",
|
|
513
|
+
toolTelemetry: {}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
if (this.isToolResultObject(result)) {
|
|
517
|
+
return result;
|
|
518
|
+
}
|
|
519
|
+
const textResult = typeof result === "string" ? result : JSON.stringify(result);
|
|
520
|
+
return {
|
|
521
|
+
textResultForLlm: textResult,
|
|
522
|
+
resultType: "success",
|
|
523
|
+
toolTelemetry: {}
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
isToolResultObject(value) {
|
|
527
|
+
return typeof value === "object" && value !== null && "textResultForLlm" in value && typeof value.textResultForLlm === "string" && "resultType" in value;
|
|
528
|
+
}
|
|
529
|
+
buildUnsupportedToolResult(toolName) {
|
|
530
|
+
return {
|
|
531
|
+
textResultForLlm: `Tool '${toolName}' is not supported by this client instance.`,
|
|
532
|
+
resultType: "failure",
|
|
533
|
+
error: `tool '${toolName}' not supported`,
|
|
534
|
+
toolTelemetry: {}
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Attempt to reconnect to the server
|
|
539
|
+
*/
|
|
540
|
+
async reconnect() {
|
|
541
|
+
this.state = "disconnected";
|
|
542
|
+
try {
|
|
543
|
+
await this.stop();
|
|
544
|
+
await this.start();
|
|
545
|
+
} catch (_error) {
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
export {
|
|
550
|
+
CopilotClient
|
|
551
|
+
};
|