@github/copilot-sdk 0.1.33-preview.1 → 0.1.33-preview.3
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 +142 -4
- package/dist/cjs/client.js +1267 -0
- package/dist/cjs/extension.js +45 -0
- package/dist/cjs/generated/rpc.js +91 -0
- package/dist/cjs/generated/session-events.js +16 -0
- package/dist/cjs/index.js +36 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/sdkProtocolVersion.js +33 -0
- package/dist/cjs/session.js +581 -0
- package/dist/cjs/telemetry.js +35 -0
- package/dist/cjs/types.js +33 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +57 -7
- package/dist/generated/rpc.d.ts +15 -0
- package/dist/generated/rpc.js +3 -0
- package/dist/index.d.ts +1 -1
- package/dist/session.d.ts +9 -3
- package/dist/session.js +25 -6
- package/dist/telemetry.d.ts +14 -0
- package/dist/telemetry.js +11 -0
- package/dist/types.d.ts +73 -1
- package/package.json +18 -6
|
@@ -0,0 +1,1267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var client_exports = {};
|
|
20
|
+
__export(client_exports, {
|
|
21
|
+
CopilotClient: () => CopilotClient
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(client_exports);
|
|
24
|
+
var import_node_child_process = require("node:child_process");
|
|
25
|
+
var import_node_crypto = require("node:crypto");
|
|
26
|
+
var import_node_fs = require("node:fs");
|
|
27
|
+
var import_node_module = require("node:module");
|
|
28
|
+
var import_node_net = require("node:net");
|
|
29
|
+
var import_node_path = require("node:path");
|
|
30
|
+
var import_node_url = require("node:url");
|
|
31
|
+
var import_node = require("vscode-jsonrpc/node.js");
|
|
32
|
+
var import_rpc = require("./generated/rpc.js");
|
|
33
|
+
var import_sdkProtocolVersion = require("./sdkProtocolVersion.js");
|
|
34
|
+
var import_session = require("./session.js");
|
|
35
|
+
var import_telemetry = require("./telemetry.js");
|
|
36
|
+
const import_meta = {};
|
|
37
|
+
const MIN_PROTOCOL_VERSION = 2;
|
|
38
|
+
function isZodSchema(value) {
|
|
39
|
+
return value != null && typeof value === "object" && "toJSONSchema" in value && typeof value.toJSONSchema === "function";
|
|
40
|
+
}
|
|
41
|
+
function toJsonSchema(parameters) {
|
|
42
|
+
if (!parameters) return void 0;
|
|
43
|
+
if (isZodSchema(parameters)) {
|
|
44
|
+
return parameters.toJSONSchema();
|
|
45
|
+
}
|
|
46
|
+
return parameters;
|
|
47
|
+
}
|
|
48
|
+
function getNodeExecPath() {
|
|
49
|
+
if (process.versions.bun) {
|
|
50
|
+
return "node";
|
|
51
|
+
}
|
|
52
|
+
return process.execPath;
|
|
53
|
+
}
|
|
54
|
+
function getBundledCliPath() {
|
|
55
|
+
if (typeof import_meta.resolve === "function") {
|
|
56
|
+
const sdkUrl = import_meta.resolve("@github/copilot/sdk");
|
|
57
|
+
const sdkPath = (0, import_node_url.fileURLToPath)(sdkUrl);
|
|
58
|
+
return (0, import_node_path.join)((0, import_node_path.dirname)((0, import_node_path.dirname)(sdkPath)), "index.js");
|
|
59
|
+
}
|
|
60
|
+
const req = (0, import_node_module.createRequire)(__filename);
|
|
61
|
+
const searchPaths = req.resolve.paths("@github/copilot") ?? [];
|
|
62
|
+
for (const base of searchPaths) {
|
|
63
|
+
const candidate = (0, import_node_path.join)(base, "@github", "copilot", "index.js");
|
|
64
|
+
if ((0, import_node_fs.existsSync)(candidate)) {
|
|
65
|
+
return candidate;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Could not find @github/copilot package. Searched ${searchPaths.length} paths. Ensure it is installed, or pass cliPath/cliUrl to CopilotClient.`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
class CopilotClient {
|
|
73
|
+
cliProcess = null;
|
|
74
|
+
connection = null;
|
|
75
|
+
socket = null;
|
|
76
|
+
actualPort = null;
|
|
77
|
+
actualHost = "localhost";
|
|
78
|
+
state = "disconnected";
|
|
79
|
+
sessions = /* @__PURE__ */ new Map();
|
|
80
|
+
stderrBuffer = "";
|
|
81
|
+
// Captures CLI stderr for error messages
|
|
82
|
+
options;
|
|
83
|
+
isExternalServer = false;
|
|
84
|
+
forceStopping = false;
|
|
85
|
+
onListModels;
|
|
86
|
+
onGetTraceContext;
|
|
87
|
+
modelsCache = null;
|
|
88
|
+
modelsCacheLock = Promise.resolve();
|
|
89
|
+
sessionLifecycleHandlers = /* @__PURE__ */ new Set();
|
|
90
|
+
typedLifecycleHandlers = /* @__PURE__ */ new Map();
|
|
91
|
+
_rpc = null;
|
|
92
|
+
processExitPromise = null;
|
|
93
|
+
// Rejects when CLI process exits
|
|
94
|
+
negotiatedProtocolVersion = null;
|
|
95
|
+
/**
|
|
96
|
+
* Typed server-scoped RPC methods.
|
|
97
|
+
* @throws Error if the client is not connected
|
|
98
|
+
*/
|
|
99
|
+
get rpc() {
|
|
100
|
+
if (!this.connection) {
|
|
101
|
+
throw new Error("Client is not connected. Call start() first.");
|
|
102
|
+
}
|
|
103
|
+
if (!this._rpc) {
|
|
104
|
+
this._rpc = (0, import_rpc.createServerRpc)(this.connection);
|
|
105
|
+
}
|
|
106
|
+
return this._rpc;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Creates a new CopilotClient instance.
|
|
110
|
+
*
|
|
111
|
+
* @param options - Configuration options for the client
|
|
112
|
+
* @throws Error if mutually exclusive options are provided (e.g., cliUrl with useStdio or cliPath)
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* // Default options - spawns CLI server using stdio
|
|
117
|
+
* const client = new CopilotClient();
|
|
118
|
+
*
|
|
119
|
+
* // Connect to an existing server
|
|
120
|
+
* const client = new CopilotClient({ cliUrl: "localhost:3000" });
|
|
121
|
+
*
|
|
122
|
+
* // Custom CLI path with specific log level
|
|
123
|
+
* const client = new CopilotClient({
|
|
124
|
+
* cliPath: "/usr/local/bin/copilot",
|
|
125
|
+
* logLevel: "debug"
|
|
126
|
+
* });
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
constructor(options = {}) {
|
|
130
|
+
if (options.cliUrl && (options.useStdio === true || options.cliPath)) {
|
|
131
|
+
throw new Error("cliUrl is mutually exclusive with useStdio and cliPath");
|
|
132
|
+
}
|
|
133
|
+
if (options.isChildProcess && (options.cliUrl || options.useStdio === false)) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"isChildProcess must be used in conjunction with useStdio and not with cliUrl"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (options.cliUrl && (options.githubToken || options.useLoggedInUser !== void 0)) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
"githubToken and useLoggedInUser cannot be used with cliUrl (external server manages its own auth)"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (options.cliUrl) {
|
|
144
|
+
const { host, port } = this.parseCliUrl(options.cliUrl);
|
|
145
|
+
this.actualHost = host;
|
|
146
|
+
this.actualPort = port;
|
|
147
|
+
this.isExternalServer = true;
|
|
148
|
+
}
|
|
149
|
+
if (options.isChildProcess) {
|
|
150
|
+
this.isExternalServer = true;
|
|
151
|
+
}
|
|
152
|
+
this.onListModels = options.onListModels;
|
|
153
|
+
this.onGetTraceContext = options.onGetTraceContext;
|
|
154
|
+
this.options = {
|
|
155
|
+
cliPath: options.cliUrl ? void 0 : options.cliPath || getBundledCliPath(),
|
|
156
|
+
cliArgs: options.cliArgs ?? [],
|
|
157
|
+
cwd: options.cwd ?? process.cwd(),
|
|
158
|
+
port: options.port || 0,
|
|
159
|
+
useStdio: options.cliUrl ? false : options.useStdio ?? true,
|
|
160
|
+
// Default to stdio unless cliUrl is provided
|
|
161
|
+
isChildProcess: options.isChildProcess ?? false,
|
|
162
|
+
cliUrl: options.cliUrl,
|
|
163
|
+
logLevel: options.logLevel || "debug",
|
|
164
|
+
autoStart: options.autoStart ?? true,
|
|
165
|
+
autoRestart: false,
|
|
166
|
+
env: options.env ?? process.env,
|
|
167
|
+
githubToken: options.githubToken,
|
|
168
|
+
// Default useLoggedInUser to false when githubToken is provided, otherwise true
|
|
169
|
+
useLoggedInUser: options.useLoggedInUser ?? (options.githubToken ? false : true),
|
|
170
|
+
telemetry: options.telemetry
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Parse CLI URL into host and port
|
|
175
|
+
* Supports formats: "host:port", "http://host:port", "https://host:port", or just "port"
|
|
176
|
+
*/
|
|
177
|
+
parseCliUrl(url) {
|
|
178
|
+
let cleanUrl = url.replace(/^https?:\/\//, "");
|
|
179
|
+
if (/^\d+$/.test(cleanUrl)) {
|
|
180
|
+
return { host: "localhost", port: parseInt(cleanUrl, 10) };
|
|
181
|
+
}
|
|
182
|
+
const parts = cleanUrl.split(":");
|
|
183
|
+
if (parts.length !== 2) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Invalid cliUrl format: ${url}. Expected "host:port", "http://host:port", or "port"`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const host = parts[0] || "localhost";
|
|
189
|
+
const port = parseInt(parts[1], 10);
|
|
190
|
+
if (isNaN(port) || port <= 0 || port > 65535) {
|
|
191
|
+
throw new Error(`Invalid port in cliUrl: ${url}`);
|
|
192
|
+
}
|
|
193
|
+
return { host, port };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Starts the CLI server and establishes a connection.
|
|
197
|
+
*
|
|
198
|
+
* If connecting to an external server (via cliUrl), only establishes the connection.
|
|
199
|
+
* Otherwise, spawns the CLI server process and then connects.
|
|
200
|
+
*
|
|
201
|
+
* This method is called automatically when creating a session if `autoStart` is true (default).
|
|
202
|
+
*
|
|
203
|
+
* @returns A promise that resolves when the connection is established
|
|
204
|
+
* @throws Error if the server fails to start or the connection fails
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```typescript
|
|
208
|
+
* const client = new CopilotClient({ autoStart: false });
|
|
209
|
+
* await client.start();
|
|
210
|
+
* // Now ready to create sessions
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
async start() {
|
|
214
|
+
if (this.state === "connected") {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
this.state = "connecting";
|
|
218
|
+
try {
|
|
219
|
+
if (!this.isExternalServer) {
|
|
220
|
+
await this.startCLIServer();
|
|
221
|
+
}
|
|
222
|
+
await this.connectToServer();
|
|
223
|
+
await this.verifyProtocolVersion();
|
|
224
|
+
this.state = "connected";
|
|
225
|
+
} catch (error) {
|
|
226
|
+
this.state = "error";
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Stops the CLI server and closes all active sessions.
|
|
232
|
+
*
|
|
233
|
+
* This method performs graceful cleanup:
|
|
234
|
+
* 1. Closes all active sessions (releases in-memory resources)
|
|
235
|
+
* 2. Closes the JSON-RPC connection
|
|
236
|
+
* 3. Terminates the CLI server process (if spawned by this client)
|
|
237
|
+
*
|
|
238
|
+
* Note: session data on disk is preserved, so sessions can be resumed later.
|
|
239
|
+
* To permanently remove session data before stopping, call
|
|
240
|
+
* {@link deleteSession} for each session first.
|
|
241
|
+
*
|
|
242
|
+
* @returns A promise that resolves with an array of errors encountered during cleanup.
|
|
243
|
+
* An empty array indicates all cleanup succeeded.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```typescript
|
|
247
|
+
* const errors = await client.stop();
|
|
248
|
+
* if (errors.length > 0) {
|
|
249
|
+
* console.error("Cleanup errors:", errors);
|
|
250
|
+
* }
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
async stop() {
|
|
254
|
+
const errors = [];
|
|
255
|
+
for (const session of this.sessions.values()) {
|
|
256
|
+
const sessionId = session.sessionId;
|
|
257
|
+
let lastError = null;
|
|
258
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
259
|
+
try {
|
|
260
|
+
await session.disconnect();
|
|
261
|
+
lastError = null;
|
|
262
|
+
break;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
265
|
+
if (attempt < 3) {
|
|
266
|
+
const delay = 100 * Math.pow(2, attempt - 1);
|
|
267
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (lastError) {
|
|
272
|
+
errors.push(
|
|
273
|
+
new Error(
|
|
274
|
+
`Failed to disconnect session ${sessionId} after 3 attempts: ${lastError.message}`
|
|
275
|
+
)
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
this.sessions.clear();
|
|
280
|
+
if (this.connection) {
|
|
281
|
+
try {
|
|
282
|
+
this.connection.dispose();
|
|
283
|
+
} catch (error) {
|
|
284
|
+
errors.push(
|
|
285
|
+
new Error(
|
|
286
|
+
`Failed to dispose connection: ${error instanceof Error ? error.message : String(error)}`
|
|
287
|
+
)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
this.connection = null;
|
|
291
|
+
this._rpc = null;
|
|
292
|
+
}
|
|
293
|
+
this.modelsCache = null;
|
|
294
|
+
if (this.socket) {
|
|
295
|
+
try {
|
|
296
|
+
this.socket.end();
|
|
297
|
+
} catch (error) {
|
|
298
|
+
errors.push(
|
|
299
|
+
new Error(
|
|
300
|
+
`Failed to close socket: ${error instanceof Error ? error.message : String(error)}`
|
|
301
|
+
)
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
this.socket = null;
|
|
305
|
+
}
|
|
306
|
+
if (this.cliProcess && !this.isExternalServer) {
|
|
307
|
+
try {
|
|
308
|
+
this.cliProcess.kill();
|
|
309
|
+
} catch (error) {
|
|
310
|
+
errors.push(
|
|
311
|
+
new Error(
|
|
312
|
+
`Failed to kill CLI process: ${error instanceof Error ? error.message : String(error)}`
|
|
313
|
+
)
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
this.cliProcess = null;
|
|
317
|
+
}
|
|
318
|
+
this.state = "disconnected";
|
|
319
|
+
this.actualPort = null;
|
|
320
|
+
this.stderrBuffer = "";
|
|
321
|
+
this.processExitPromise = null;
|
|
322
|
+
return errors;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Forcefully stops the CLI server without graceful cleanup.
|
|
326
|
+
*
|
|
327
|
+
* Use this when {@link stop} fails or takes too long. This method:
|
|
328
|
+
* - Clears all sessions immediately without destroying them
|
|
329
|
+
* - Force closes the connection
|
|
330
|
+
* - Sends SIGKILL to the CLI process (if spawned by this client)
|
|
331
|
+
*
|
|
332
|
+
* @returns A promise that resolves when the force stop is complete
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```typescript
|
|
336
|
+
* // If normal stop hangs, force stop
|
|
337
|
+
* const stopPromise = client.stop();
|
|
338
|
+
* const timeout = new Promise((_, reject) =>
|
|
339
|
+
* setTimeout(() => reject(new Error("Timeout")), 5000)
|
|
340
|
+
* );
|
|
341
|
+
*
|
|
342
|
+
* try {
|
|
343
|
+
* await Promise.race([stopPromise, timeout]);
|
|
344
|
+
* } catch {
|
|
345
|
+
* await client.forceStop();
|
|
346
|
+
* }
|
|
347
|
+
* ```
|
|
348
|
+
*/
|
|
349
|
+
async forceStop() {
|
|
350
|
+
this.forceStopping = true;
|
|
351
|
+
this.sessions.clear();
|
|
352
|
+
if (this.connection) {
|
|
353
|
+
try {
|
|
354
|
+
this.connection.dispose();
|
|
355
|
+
} catch {
|
|
356
|
+
}
|
|
357
|
+
this.connection = null;
|
|
358
|
+
this._rpc = null;
|
|
359
|
+
}
|
|
360
|
+
this.modelsCache = null;
|
|
361
|
+
if (this.socket) {
|
|
362
|
+
try {
|
|
363
|
+
this.socket.destroy();
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
366
|
+
this.socket = null;
|
|
367
|
+
}
|
|
368
|
+
if (this.cliProcess && !this.isExternalServer) {
|
|
369
|
+
try {
|
|
370
|
+
this.cliProcess.kill("SIGKILL");
|
|
371
|
+
} catch {
|
|
372
|
+
}
|
|
373
|
+
this.cliProcess = null;
|
|
374
|
+
}
|
|
375
|
+
this.state = "disconnected";
|
|
376
|
+
this.actualPort = null;
|
|
377
|
+
this.stderrBuffer = "";
|
|
378
|
+
this.processExitPromise = null;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Creates a new conversation session with the Copilot CLI.
|
|
382
|
+
*
|
|
383
|
+
* Sessions maintain conversation state, handle events, and manage tool execution.
|
|
384
|
+
* If the client is not connected and `autoStart` is enabled, this will automatically
|
|
385
|
+
* start the connection.
|
|
386
|
+
*
|
|
387
|
+
* @param config - Optional configuration for the session
|
|
388
|
+
* @returns A promise that resolves with the created session
|
|
389
|
+
* @throws Error if the client is not connected and autoStart is disabled
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* ```typescript
|
|
393
|
+
* // Basic session
|
|
394
|
+
* const session = await client.createSession({ onPermissionRequest: approveAll });
|
|
395
|
+
*
|
|
396
|
+
* // Session with model and tools
|
|
397
|
+
* const session = await client.createSession({
|
|
398
|
+
* onPermissionRequest: approveAll,
|
|
399
|
+
* model: "gpt-4",
|
|
400
|
+
* tools: [{
|
|
401
|
+
* name: "get_weather",
|
|
402
|
+
* description: "Get weather for a location",
|
|
403
|
+
* parameters: { type: "object", properties: { location: { type: "string" } } },
|
|
404
|
+
* handler: async (args) => ({ temperature: 72 })
|
|
405
|
+
* }]
|
|
406
|
+
* });
|
|
407
|
+
* ```
|
|
408
|
+
*/
|
|
409
|
+
async createSession(config) {
|
|
410
|
+
if (!config?.onPermissionRequest) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
"An onPermissionRequest handler is required when creating a session. For example, to allow all permissions, use { onPermissionRequest: approveAll }."
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
if (!this.connection) {
|
|
416
|
+
if (this.options.autoStart) {
|
|
417
|
+
await this.start();
|
|
418
|
+
} else {
|
|
419
|
+
throw new Error("Client not connected. Call start() first.");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const sessionId = config.sessionId ?? (0, import_node_crypto.randomUUID)();
|
|
423
|
+
const session = new import_session.CopilotSession(
|
|
424
|
+
sessionId,
|
|
425
|
+
this.connection,
|
|
426
|
+
void 0,
|
|
427
|
+
this.onGetTraceContext
|
|
428
|
+
);
|
|
429
|
+
session.registerTools(config.tools);
|
|
430
|
+
session.registerPermissionHandler(config.onPermissionRequest);
|
|
431
|
+
if (config.onUserInputRequest) {
|
|
432
|
+
session.registerUserInputHandler(config.onUserInputRequest);
|
|
433
|
+
}
|
|
434
|
+
if (config.hooks) {
|
|
435
|
+
session.registerHooks(config.hooks);
|
|
436
|
+
}
|
|
437
|
+
if (config.onEvent) {
|
|
438
|
+
session.on(config.onEvent);
|
|
439
|
+
}
|
|
440
|
+
this.sessions.set(sessionId, session);
|
|
441
|
+
try {
|
|
442
|
+
const response = await this.connection.sendRequest("session.create", {
|
|
443
|
+
...await (0, import_telemetry.getTraceContext)(this.onGetTraceContext),
|
|
444
|
+
model: config.model,
|
|
445
|
+
sessionId,
|
|
446
|
+
clientName: config.clientName,
|
|
447
|
+
reasoningEffort: config.reasoningEffort,
|
|
448
|
+
tools: config.tools?.map((tool) => ({
|
|
449
|
+
name: tool.name,
|
|
450
|
+
description: tool.description,
|
|
451
|
+
parameters: toJsonSchema(tool.parameters),
|
|
452
|
+
overridesBuiltInTool: tool.overridesBuiltInTool,
|
|
453
|
+
skipPermission: tool.skipPermission
|
|
454
|
+
})),
|
|
455
|
+
systemMessage: config.systemMessage,
|
|
456
|
+
availableTools: config.availableTools,
|
|
457
|
+
excludedTools: config.excludedTools,
|
|
458
|
+
provider: config.provider,
|
|
459
|
+
requestPermission: true,
|
|
460
|
+
requestUserInput: !!config.onUserInputRequest,
|
|
461
|
+
hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
|
|
462
|
+
workingDirectory: config.workingDirectory,
|
|
463
|
+
streaming: config.streaming,
|
|
464
|
+
mcpServers: config.mcpServers,
|
|
465
|
+
envValueMode: "direct",
|
|
466
|
+
customAgents: config.customAgents,
|
|
467
|
+
agent: config.agent,
|
|
468
|
+
configDir: config.configDir,
|
|
469
|
+
skillDirectories: config.skillDirectories,
|
|
470
|
+
disabledSkills: config.disabledSkills,
|
|
471
|
+
infiniteSessions: config.infiniteSessions
|
|
472
|
+
});
|
|
473
|
+
const { workspacePath } = response;
|
|
474
|
+
session["_workspacePath"] = workspacePath;
|
|
475
|
+
} catch (e) {
|
|
476
|
+
this.sessions.delete(sessionId);
|
|
477
|
+
throw e;
|
|
478
|
+
}
|
|
479
|
+
return session;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Resumes an existing conversation session by its ID.
|
|
483
|
+
*
|
|
484
|
+
* This allows you to continue a previous conversation, maintaining all
|
|
485
|
+
* conversation history. The session must have been previously created
|
|
486
|
+
* and not deleted.
|
|
487
|
+
*
|
|
488
|
+
* @param sessionId - The ID of the session to resume
|
|
489
|
+
* @param config - Optional configuration for the resumed session
|
|
490
|
+
* @returns A promise that resolves with the resumed session
|
|
491
|
+
* @throws Error if the session does not exist or the client is not connected
|
|
492
|
+
*
|
|
493
|
+
* @example
|
|
494
|
+
* ```typescript
|
|
495
|
+
* // Resume a previous session
|
|
496
|
+
* const session = await client.resumeSession("session-123", { onPermissionRequest: approveAll });
|
|
497
|
+
*
|
|
498
|
+
* // Resume with new tools
|
|
499
|
+
* const session = await client.resumeSession("session-123", {
|
|
500
|
+
* onPermissionRequest: approveAll,
|
|
501
|
+
* tools: [myNewTool]
|
|
502
|
+
* });
|
|
503
|
+
* ```
|
|
504
|
+
*/
|
|
505
|
+
async resumeSession(sessionId, config) {
|
|
506
|
+
if (!config?.onPermissionRequest) {
|
|
507
|
+
throw new Error(
|
|
508
|
+
"An onPermissionRequest handler is required when resuming a session. For example, to allow all permissions, use { onPermissionRequest: approveAll }."
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
if (!this.connection) {
|
|
512
|
+
if (this.options.autoStart) {
|
|
513
|
+
await this.start();
|
|
514
|
+
} else {
|
|
515
|
+
throw new Error("Client not connected. Call start() first.");
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const session = new import_session.CopilotSession(
|
|
519
|
+
sessionId,
|
|
520
|
+
this.connection,
|
|
521
|
+
void 0,
|
|
522
|
+
this.onGetTraceContext
|
|
523
|
+
);
|
|
524
|
+
session.registerTools(config.tools);
|
|
525
|
+
session.registerPermissionHandler(config.onPermissionRequest);
|
|
526
|
+
if (config.onUserInputRequest) {
|
|
527
|
+
session.registerUserInputHandler(config.onUserInputRequest);
|
|
528
|
+
}
|
|
529
|
+
if (config.hooks) {
|
|
530
|
+
session.registerHooks(config.hooks);
|
|
531
|
+
}
|
|
532
|
+
if (config.onEvent) {
|
|
533
|
+
session.on(config.onEvent);
|
|
534
|
+
}
|
|
535
|
+
this.sessions.set(sessionId, session);
|
|
536
|
+
try {
|
|
537
|
+
const response = await this.connection.sendRequest("session.resume", {
|
|
538
|
+
...await (0, import_telemetry.getTraceContext)(this.onGetTraceContext),
|
|
539
|
+
sessionId,
|
|
540
|
+
clientName: config.clientName,
|
|
541
|
+
model: config.model,
|
|
542
|
+
reasoningEffort: config.reasoningEffort,
|
|
543
|
+
systemMessage: config.systemMessage,
|
|
544
|
+
availableTools: config.availableTools,
|
|
545
|
+
excludedTools: config.excludedTools,
|
|
546
|
+
tools: config.tools?.map((tool) => ({
|
|
547
|
+
name: tool.name,
|
|
548
|
+
description: tool.description,
|
|
549
|
+
parameters: toJsonSchema(tool.parameters),
|
|
550
|
+
overridesBuiltInTool: tool.overridesBuiltInTool,
|
|
551
|
+
skipPermission: tool.skipPermission
|
|
552
|
+
})),
|
|
553
|
+
provider: config.provider,
|
|
554
|
+
requestPermission: true,
|
|
555
|
+
requestUserInput: !!config.onUserInputRequest,
|
|
556
|
+
hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
|
|
557
|
+
workingDirectory: config.workingDirectory,
|
|
558
|
+
configDir: config.configDir,
|
|
559
|
+
streaming: config.streaming,
|
|
560
|
+
mcpServers: config.mcpServers,
|
|
561
|
+
envValueMode: "direct",
|
|
562
|
+
customAgents: config.customAgents,
|
|
563
|
+
agent: config.agent,
|
|
564
|
+
skillDirectories: config.skillDirectories,
|
|
565
|
+
disabledSkills: config.disabledSkills,
|
|
566
|
+
infiniteSessions: config.infiniteSessions,
|
|
567
|
+
disableResume: config.disableResume
|
|
568
|
+
});
|
|
569
|
+
const { workspacePath } = response;
|
|
570
|
+
session["_workspacePath"] = workspacePath;
|
|
571
|
+
} catch (e) {
|
|
572
|
+
this.sessions.delete(sessionId);
|
|
573
|
+
throw e;
|
|
574
|
+
}
|
|
575
|
+
return session;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Gets the current connection state of the client.
|
|
579
|
+
*
|
|
580
|
+
* @returns The current connection state: "disconnected", "connecting", "connected", or "error"
|
|
581
|
+
*
|
|
582
|
+
* @example
|
|
583
|
+
* ```typescript
|
|
584
|
+
* if (client.getState() === "connected") {
|
|
585
|
+
* const session = await client.createSession({ onPermissionRequest: approveAll });
|
|
586
|
+
* }
|
|
587
|
+
* ```
|
|
588
|
+
*/
|
|
589
|
+
getState() {
|
|
590
|
+
return this.state;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Sends a ping request to the server to verify connectivity.
|
|
594
|
+
*
|
|
595
|
+
* @param message - Optional message to include in the ping
|
|
596
|
+
* @returns A promise that resolves with the ping response containing the message and timestamp
|
|
597
|
+
* @throws Error if the client is not connected
|
|
598
|
+
*
|
|
599
|
+
* @example
|
|
600
|
+
* ```typescript
|
|
601
|
+
* const response = await client.ping("health check");
|
|
602
|
+
* console.log(`Server responded at ${new Date(response.timestamp)}`);
|
|
603
|
+
* ```
|
|
604
|
+
*/
|
|
605
|
+
async ping(message) {
|
|
606
|
+
if (!this.connection) {
|
|
607
|
+
throw new Error("Client not connected");
|
|
608
|
+
}
|
|
609
|
+
const result = await this.connection.sendRequest("ping", { message });
|
|
610
|
+
return result;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Get CLI status including version and protocol information
|
|
614
|
+
*/
|
|
615
|
+
async getStatus() {
|
|
616
|
+
if (!this.connection) {
|
|
617
|
+
throw new Error("Client not connected");
|
|
618
|
+
}
|
|
619
|
+
const result = await this.connection.sendRequest("status.get", {});
|
|
620
|
+
return result;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Get current authentication status
|
|
624
|
+
*/
|
|
625
|
+
async getAuthStatus() {
|
|
626
|
+
if (!this.connection) {
|
|
627
|
+
throw new Error("Client not connected");
|
|
628
|
+
}
|
|
629
|
+
const result = await this.connection.sendRequest("auth.getStatus", {});
|
|
630
|
+
return result;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* List available models with their metadata.
|
|
634
|
+
*
|
|
635
|
+
* If an `onListModels` handler was provided in the client options,
|
|
636
|
+
* it is called instead of querying the CLI server.
|
|
637
|
+
*
|
|
638
|
+
* Results are cached after the first successful call to avoid rate limiting.
|
|
639
|
+
* The cache is cleared when the client disconnects.
|
|
640
|
+
*
|
|
641
|
+
* @throws Error if not connected (when no custom handler is set)
|
|
642
|
+
*/
|
|
643
|
+
async listModels() {
|
|
644
|
+
await this.modelsCacheLock;
|
|
645
|
+
let resolveLock;
|
|
646
|
+
this.modelsCacheLock = new Promise((resolve) => {
|
|
647
|
+
resolveLock = resolve;
|
|
648
|
+
});
|
|
649
|
+
try {
|
|
650
|
+
if (this.modelsCache !== null) {
|
|
651
|
+
return [...this.modelsCache];
|
|
652
|
+
}
|
|
653
|
+
let models;
|
|
654
|
+
if (this.onListModels) {
|
|
655
|
+
models = await this.onListModels();
|
|
656
|
+
} else {
|
|
657
|
+
if (!this.connection) {
|
|
658
|
+
throw new Error("Client not connected");
|
|
659
|
+
}
|
|
660
|
+
const result = await this.connection.sendRequest("models.list", {});
|
|
661
|
+
const response = result;
|
|
662
|
+
models = response.models;
|
|
663
|
+
}
|
|
664
|
+
this.modelsCache = [...models];
|
|
665
|
+
return [...models];
|
|
666
|
+
} finally {
|
|
667
|
+
resolveLock();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Verify that the server's protocol version is within the supported range
|
|
672
|
+
* and store the negotiated version.
|
|
673
|
+
*/
|
|
674
|
+
async verifyProtocolVersion() {
|
|
675
|
+
const maxVersion = (0, import_sdkProtocolVersion.getSdkProtocolVersion)();
|
|
676
|
+
let pingResult;
|
|
677
|
+
if (this.processExitPromise) {
|
|
678
|
+
pingResult = await Promise.race([this.ping(), this.processExitPromise]);
|
|
679
|
+
} else {
|
|
680
|
+
pingResult = await this.ping();
|
|
681
|
+
}
|
|
682
|
+
const serverVersion = pingResult.protocolVersion;
|
|
683
|
+
if (serverVersion === void 0) {
|
|
684
|
+
throw new Error(
|
|
685
|
+
`SDK protocol version mismatch: SDK supports versions ${MIN_PROTOCOL_VERSION}-${maxVersion}, but server does not report a protocol version. Please update your server to ensure compatibility.`
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
if (serverVersion < MIN_PROTOCOL_VERSION || serverVersion > maxVersion) {
|
|
689
|
+
throw new Error(
|
|
690
|
+
`SDK protocol version mismatch: SDK supports versions ${MIN_PROTOCOL_VERSION}-${maxVersion}, but server reports version ${serverVersion}. Please update your SDK or server to ensure compatibility.`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
this.negotiatedProtocolVersion = serverVersion;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Gets the ID of the most recently updated session.
|
|
697
|
+
*
|
|
698
|
+
* This is useful for resuming the last conversation when the session ID
|
|
699
|
+
* was not stored.
|
|
700
|
+
*
|
|
701
|
+
* @returns A promise that resolves with the session ID, or undefined if no sessions exist
|
|
702
|
+
* @throws Error if the client is not connected
|
|
703
|
+
*
|
|
704
|
+
* @example
|
|
705
|
+
* ```typescript
|
|
706
|
+
* const lastId = await client.getLastSessionId();
|
|
707
|
+
* if (lastId) {
|
|
708
|
+
* const session = await client.resumeSession(lastId, { onPermissionRequest: approveAll });
|
|
709
|
+
* }
|
|
710
|
+
* ```
|
|
711
|
+
*/
|
|
712
|
+
async getLastSessionId() {
|
|
713
|
+
if (!this.connection) {
|
|
714
|
+
throw new Error("Client not connected");
|
|
715
|
+
}
|
|
716
|
+
const response = await this.connection.sendRequest("session.getLastId", {});
|
|
717
|
+
return response.sessionId;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Permanently deletes a session and all its data from disk, including
|
|
721
|
+
* conversation history, planning state, and artifacts.
|
|
722
|
+
*
|
|
723
|
+
* Unlike {@link CopilotSession.disconnect}, which only releases in-memory
|
|
724
|
+
* resources and preserves session data for later resumption, this method
|
|
725
|
+
* is irreversible. The session cannot be resumed after deletion.
|
|
726
|
+
*
|
|
727
|
+
* @param sessionId - The ID of the session to delete
|
|
728
|
+
* @returns A promise that resolves when the session is deleted
|
|
729
|
+
* @throws Error if the session does not exist or deletion fails
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* ```typescript
|
|
733
|
+
* await client.deleteSession("session-123");
|
|
734
|
+
* ```
|
|
735
|
+
*/
|
|
736
|
+
async deleteSession(sessionId) {
|
|
737
|
+
if (!this.connection) {
|
|
738
|
+
throw new Error("Client not connected");
|
|
739
|
+
}
|
|
740
|
+
const response = await this.connection.sendRequest("session.delete", {
|
|
741
|
+
sessionId
|
|
742
|
+
});
|
|
743
|
+
const { success, error } = response;
|
|
744
|
+
if (!success) {
|
|
745
|
+
throw new Error(`Failed to delete session ${sessionId}: ${error || "Unknown error"}`);
|
|
746
|
+
}
|
|
747
|
+
this.sessions.delete(sessionId);
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* List all available sessions.
|
|
751
|
+
*
|
|
752
|
+
* @param filter - Optional filter to limit returned sessions by context fields
|
|
753
|
+
*
|
|
754
|
+
* @example
|
|
755
|
+
* // List all sessions
|
|
756
|
+
* const sessions = await client.listSessions();
|
|
757
|
+
*
|
|
758
|
+
* @example
|
|
759
|
+
* // List sessions for a specific repository
|
|
760
|
+
* const sessions = await client.listSessions({ repository: "owner/repo" });
|
|
761
|
+
*/
|
|
762
|
+
async listSessions(filter) {
|
|
763
|
+
if (!this.connection) {
|
|
764
|
+
throw new Error("Client not connected");
|
|
765
|
+
}
|
|
766
|
+
const response = await this.connection.sendRequest("session.list", { filter });
|
|
767
|
+
const { sessions } = response;
|
|
768
|
+
return sessions.map((s) => ({
|
|
769
|
+
sessionId: s.sessionId,
|
|
770
|
+
startTime: new Date(s.startTime),
|
|
771
|
+
modifiedTime: new Date(s.modifiedTime),
|
|
772
|
+
summary: s.summary,
|
|
773
|
+
isRemote: s.isRemote,
|
|
774
|
+
context: s.context
|
|
775
|
+
}));
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Gets the foreground session ID in TUI+server mode.
|
|
779
|
+
*
|
|
780
|
+
* This returns the ID of the session currently displayed in the TUI.
|
|
781
|
+
* Only available when connecting to a server running in TUI+server mode (--ui-server).
|
|
782
|
+
*
|
|
783
|
+
* @returns A promise that resolves with the foreground session ID, or undefined if none
|
|
784
|
+
* @throws Error if the client is not connected
|
|
785
|
+
*
|
|
786
|
+
* @example
|
|
787
|
+
* ```typescript
|
|
788
|
+
* const sessionId = await client.getForegroundSessionId();
|
|
789
|
+
* if (sessionId) {
|
|
790
|
+
* console.log(`TUI is displaying session: ${sessionId}`);
|
|
791
|
+
* }
|
|
792
|
+
* ```
|
|
793
|
+
*/
|
|
794
|
+
async getForegroundSessionId() {
|
|
795
|
+
if (!this.connection) {
|
|
796
|
+
throw new Error("Client not connected");
|
|
797
|
+
}
|
|
798
|
+
const response = await this.connection.sendRequest("session.getForeground", {});
|
|
799
|
+
return response.sessionId;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Sets the foreground session in TUI+server mode.
|
|
803
|
+
*
|
|
804
|
+
* This requests the TUI to switch to displaying the specified session.
|
|
805
|
+
* Only available when connecting to a server running in TUI+server mode (--ui-server).
|
|
806
|
+
*
|
|
807
|
+
* @param sessionId - The ID of the session to display in the TUI
|
|
808
|
+
* @returns A promise that resolves when the session is switched
|
|
809
|
+
* @throws Error if the client is not connected or if the operation fails
|
|
810
|
+
*
|
|
811
|
+
* @example
|
|
812
|
+
* ```typescript
|
|
813
|
+
* // Switch the TUI to display a specific session
|
|
814
|
+
* await client.setForegroundSessionId("session-123");
|
|
815
|
+
* ```
|
|
816
|
+
*/
|
|
817
|
+
async setForegroundSessionId(sessionId) {
|
|
818
|
+
if (!this.connection) {
|
|
819
|
+
throw new Error("Client not connected");
|
|
820
|
+
}
|
|
821
|
+
const response = await this.connection.sendRequest("session.setForeground", { sessionId });
|
|
822
|
+
const result = response;
|
|
823
|
+
if (!result.success) {
|
|
824
|
+
throw new Error(result.error || "Failed to set foreground session");
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
on(eventTypeOrHandler, handler) {
|
|
828
|
+
if (typeof eventTypeOrHandler === "string" && handler) {
|
|
829
|
+
const eventType = eventTypeOrHandler;
|
|
830
|
+
if (!this.typedLifecycleHandlers.has(eventType)) {
|
|
831
|
+
this.typedLifecycleHandlers.set(eventType, /* @__PURE__ */ new Set());
|
|
832
|
+
}
|
|
833
|
+
const storedHandler = handler;
|
|
834
|
+
this.typedLifecycleHandlers.get(eventType).add(storedHandler);
|
|
835
|
+
return () => {
|
|
836
|
+
const handlers = this.typedLifecycleHandlers.get(eventType);
|
|
837
|
+
if (handlers) {
|
|
838
|
+
handlers.delete(storedHandler);
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
const wildcardHandler = eventTypeOrHandler;
|
|
843
|
+
this.sessionLifecycleHandlers.add(wildcardHandler);
|
|
844
|
+
return () => {
|
|
845
|
+
this.sessionLifecycleHandlers.delete(wildcardHandler);
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Start the CLI server process
|
|
850
|
+
*/
|
|
851
|
+
async startCLIServer() {
|
|
852
|
+
return new Promise((resolve, reject) => {
|
|
853
|
+
this.stderrBuffer = "";
|
|
854
|
+
const args = [
|
|
855
|
+
...this.options.cliArgs,
|
|
856
|
+
"--headless",
|
|
857
|
+
"--no-auto-update",
|
|
858
|
+
"--log-level",
|
|
859
|
+
this.options.logLevel
|
|
860
|
+
];
|
|
861
|
+
if (this.options.useStdio) {
|
|
862
|
+
args.push("--stdio");
|
|
863
|
+
} else if (this.options.port > 0) {
|
|
864
|
+
args.push("--port", this.options.port.toString());
|
|
865
|
+
}
|
|
866
|
+
if (this.options.githubToken) {
|
|
867
|
+
args.push("--auth-token-env", "COPILOT_SDK_AUTH_TOKEN");
|
|
868
|
+
}
|
|
869
|
+
if (!this.options.useLoggedInUser) {
|
|
870
|
+
args.push("--no-auto-login");
|
|
871
|
+
}
|
|
872
|
+
const envWithoutNodeDebug = { ...this.options.env };
|
|
873
|
+
delete envWithoutNodeDebug.NODE_DEBUG;
|
|
874
|
+
if (this.options.githubToken) {
|
|
875
|
+
envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.githubToken;
|
|
876
|
+
}
|
|
877
|
+
if (!this.options.cliPath) {
|
|
878
|
+
throw new Error(
|
|
879
|
+
"Path to Copilot CLI is required. Please provide it via the cliPath option, or use cliUrl to rely on a remote CLI."
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
if (this.options.telemetry) {
|
|
883
|
+
const t = this.options.telemetry;
|
|
884
|
+
envWithoutNodeDebug.COPILOT_OTEL_ENABLED = "true";
|
|
885
|
+
if (t.otlpEndpoint !== void 0)
|
|
886
|
+
envWithoutNodeDebug.OTEL_EXPORTER_OTLP_ENDPOINT = t.otlpEndpoint;
|
|
887
|
+
if (t.filePath !== void 0)
|
|
888
|
+
envWithoutNodeDebug.COPILOT_OTEL_FILE_EXPORTER_PATH = t.filePath;
|
|
889
|
+
if (t.exporterType !== void 0)
|
|
890
|
+
envWithoutNodeDebug.COPILOT_OTEL_EXPORTER_TYPE = t.exporterType;
|
|
891
|
+
if (t.sourceName !== void 0)
|
|
892
|
+
envWithoutNodeDebug.COPILOT_OTEL_SOURCE_NAME = t.sourceName;
|
|
893
|
+
if (t.captureContent !== void 0)
|
|
894
|
+
envWithoutNodeDebug.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = String(
|
|
895
|
+
t.captureContent
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
if (!(0, import_node_fs.existsSync)(this.options.cliPath)) {
|
|
899
|
+
throw new Error(
|
|
900
|
+
`Copilot CLI not found at ${this.options.cliPath}. Ensure @github/copilot is installed.`
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
const stdioConfig = this.options.useStdio ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
|
|
904
|
+
const isJsFile = this.options.cliPath.endsWith(".js");
|
|
905
|
+
if (isJsFile) {
|
|
906
|
+
this.cliProcess = (0, import_node_child_process.spawn)(getNodeExecPath(), [this.options.cliPath, ...args], {
|
|
907
|
+
stdio: stdioConfig,
|
|
908
|
+
cwd: this.options.cwd,
|
|
909
|
+
env: envWithoutNodeDebug,
|
|
910
|
+
windowsHide: true
|
|
911
|
+
});
|
|
912
|
+
} else {
|
|
913
|
+
this.cliProcess = (0, import_node_child_process.spawn)(this.options.cliPath, args, {
|
|
914
|
+
stdio: stdioConfig,
|
|
915
|
+
cwd: this.options.cwd,
|
|
916
|
+
env: envWithoutNodeDebug,
|
|
917
|
+
windowsHide: true
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
let stdout = "";
|
|
921
|
+
let resolved = false;
|
|
922
|
+
if (this.options.useStdio) {
|
|
923
|
+
resolved = true;
|
|
924
|
+
resolve();
|
|
925
|
+
} else {
|
|
926
|
+
this.cliProcess.stdout?.on("data", (data) => {
|
|
927
|
+
stdout += data.toString();
|
|
928
|
+
const match = stdout.match(/listening on port (\d+)/i);
|
|
929
|
+
if (match && !resolved) {
|
|
930
|
+
this.actualPort = parseInt(match[1], 10);
|
|
931
|
+
resolved = true;
|
|
932
|
+
resolve();
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
this.cliProcess.stderr?.on("data", (data) => {
|
|
937
|
+
this.stderrBuffer += data.toString();
|
|
938
|
+
const lines = data.toString().split("\n");
|
|
939
|
+
for (const line of lines) {
|
|
940
|
+
if (line.trim()) {
|
|
941
|
+
process.stderr.write(`[CLI subprocess] ${line}
|
|
942
|
+
`);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
this.cliProcess.on("error", (error) => {
|
|
947
|
+
if (!resolved) {
|
|
948
|
+
resolved = true;
|
|
949
|
+
const stderrOutput = this.stderrBuffer.trim();
|
|
950
|
+
if (stderrOutput) {
|
|
951
|
+
reject(
|
|
952
|
+
new Error(
|
|
953
|
+
`Failed to start CLI server: ${error.message}
|
|
954
|
+
stderr: ${stderrOutput}`
|
|
955
|
+
)
|
|
956
|
+
);
|
|
957
|
+
} else {
|
|
958
|
+
reject(new Error(`Failed to start CLI server: ${error.message}`));
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
this.processExitPromise = new Promise((_, rejectProcessExit) => {
|
|
963
|
+
this.cliProcess.on("exit", (code) => {
|
|
964
|
+
setTimeout(() => {
|
|
965
|
+
const stderrOutput = this.stderrBuffer.trim();
|
|
966
|
+
if (stderrOutput) {
|
|
967
|
+
rejectProcessExit(
|
|
968
|
+
new Error(
|
|
969
|
+
`CLI server exited with code ${code}
|
|
970
|
+
stderr: ${stderrOutput}`
|
|
971
|
+
)
|
|
972
|
+
);
|
|
973
|
+
} else {
|
|
974
|
+
rejectProcessExit(
|
|
975
|
+
new Error(`CLI server exited unexpectedly with code ${code}`)
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
}, 50);
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
this.processExitPromise.catch(() => {
|
|
982
|
+
});
|
|
983
|
+
this.cliProcess.on("exit", (code) => {
|
|
984
|
+
if (!resolved) {
|
|
985
|
+
resolved = true;
|
|
986
|
+
const stderrOutput = this.stderrBuffer.trim();
|
|
987
|
+
if (stderrOutput) {
|
|
988
|
+
reject(
|
|
989
|
+
new Error(
|
|
990
|
+
`CLI server exited with code ${code}
|
|
991
|
+
stderr: ${stderrOutput}`
|
|
992
|
+
)
|
|
993
|
+
);
|
|
994
|
+
} else {
|
|
995
|
+
reject(new Error(`CLI server exited with code ${code}`));
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
setTimeout(() => {
|
|
1000
|
+
if (!resolved) {
|
|
1001
|
+
resolved = true;
|
|
1002
|
+
reject(new Error("Timeout waiting for CLI server to start"));
|
|
1003
|
+
}
|
|
1004
|
+
}, 1e4);
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Connect to the CLI server (via socket or stdio)
|
|
1009
|
+
*/
|
|
1010
|
+
async connectToServer() {
|
|
1011
|
+
if (this.options.isChildProcess) {
|
|
1012
|
+
return this.connectToParentProcessViaStdio();
|
|
1013
|
+
} else if (this.options.useStdio) {
|
|
1014
|
+
return this.connectToChildProcessViaStdio();
|
|
1015
|
+
} else {
|
|
1016
|
+
return this.connectViaTcp();
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Connect to child via stdio pipes
|
|
1021
|
+
*/
|
|
1022
|
+
async connectToChildProcessViaStdio() {
|
|
1023
|
+
if (!this.cliProcess) {
|
|
1024
|
+
throw new Error("CLI process not started");
|
|
1025
|
+
}
|
|
1026
|
+
this.cliProcess.stdin?.on("error", (err) => {
|
|
1027
|
+
if (!this.forceStopping) {
|
|
1028
|
+
throw err;
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
this.connection = (0, import_node.createMessageConnection)(
|
|
1032
|
+
new import_node.StreamMessageReader(this.cliProcess.stdout),
|
|
1033
|
+
new import_node.StreamMessageWriter(this.cliProcess.stdin)
|
|
1034
|
+
);
|
|
1035
|
+
this.attachConnectionHandlers();
|
|
1036
|
+
this.connection.listen();
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Connect to parent via stdio pipes
|
|
1040
|
+
*/
|
|
1041
|
+
async connectToParentProcessViaStdio() {
|
|
1042
|
+
if (this.cliProcess) {
|
|
1043
|
+
throw new Error("CLI child process was unexpectedly started in parent process mode");
|
|
1044
|
+
}
|
|
1045
|
+
this.connection = (0, import_node.createMessageConnection)(
|
|
1046
|
+
new import_node.StreamMessageReader(process.stdin),
|
|
1047
|
+
new import_node.StreamMessageWriter(process.stdout)
|
|
1048
|
+
);
|
|
1049
|
+
this.attachConnectionHandlers();
|
|
1050
|
+
this.connection.listen();
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Connect to the CLI server via TCP socket
|
|
1054
|
+
*/
|
|
1055
|
+
async connectViaTcp() {
|
|
1056
|
+
if (!this.actualPort) {
|
|
1057
|
+
throw new Error("Server port not available");
|
|
1058
|
+
}
|
|
1059
|
+
return new Promise((resolve, reject) => {
|
|
1060
|
+
this.socket = new import_node_net.Socket();
|
|
1061
|
+
this.socket.connect(this.actualPort, this.actualHost, () => {
|
|
1062
|
+
this.connection = (0, import_node.createMessageConnection)(
|
|
1063
|
+
new import_node.StreamMessageReader(this.socket),
|
|
1064
|
+
new import_node.StreamMessageWriter(this.socket)
|
|
1065
|
+
);
|
|
1066
|
+
this.attachConnectionHandlers();
|
|
1067
|
+
this.connection.listen();
|
|
1068
|
+
resolve();
|
|
1069
|
+
});
|
|
1070
|
+
this.socket.on("error", (error) => {
|
|
1071
|
+
reject(new Error(`Failed to connect to CLI server: ${error.message}`));
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
attachConnectionHandlers() {
|
|
1076
|
+
if (!this.connection) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
this.connection.onNotification("session.event", (notification) => {
|
|
1080
|
+
this.handleSessionEventNotification(notification);
|
|
1081
|
+
});
|
|
1082
|
+
this.connection.onNotification("session.lifecycle", (notification) => {
|
|
1083
|
+
this.handleSessionLifecycleNotification(notification);
|
|
1084
|
+
});
|
|
1085
|
+
this.connection.onRequest(
|
|
1086
|
+
"tool.call",
|
|
1087
|
+
async (params) => await this.handleToolCallRequestV2(params)
|
|
1088
|
+
);
|
|
1089
|
+
this.connection.onRequest(
|
|
1090
|
+
"permission.request",
|
|
1091
|
+
async (params) => await this.handlePermissionRequestV2(params)
|
|
1092
|
+
);
|
|
1093
|
+
this.connection.onRequest(
|
|
1094
|
+
"userInput.request",
|
|
1095
|
+
async (params) => await this.handleUserInputRequest(params)
|
|
1096
|
+
);
|
|
1097
|
+
this.connection.onRequest(
|
|
1098
|
+
"hooks.invoke",
|
|
1099
|
+
async (params) => await this.handleHooksInvoke(params)
|
|
1100
|
+
);
|
|
1101
|
+
this.connection.onClose(() => {
|
|
1102
|
+
this.state = "disconnected";
|
|
1103
|
+
});
|
|
1104
|
+
this.connection.onError((_error) => {
|
|
1105
|
+
this.state = "disconnected";
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
handleSessionEventNotification(notification) {
|
|
1109
|
+
if (typeof notification !== "object" || !notification || !("sessionId" in notification) || typeof notification.sessionId !== "string" || !("event" in notification)) {
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const session = this.sessions.get(notification.sessionId);
|
|
1113
|
+
if (session) {
|
|
1114
|
+
session._dispatchEvent(notification.event);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
handleSessionLifecycleNotification(notification) {
|
|
1118
|
+
if (typeof notification !== "object" || !notification || !("type" in notification) || typeof notification.type !== "string" || !("sessionId" in notification) || typeof notification.sessionId !== "string") {
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
const event = notification;
|
|
1122
|
+
const typedHandlers = this.typedLifecycleHandlers.get(event.type);
|
|
1123
|
+
if (typedHandlers) {
|
|
1124
|
+
for (const handler of typedHandlers) {
|
|
1125
|
+
try {
|
|
1126
|
+
handler(event);
|
|
1127
|
+
} catch {
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
for (const handler of this.sessionLifecycleHandlers) {
|
|
1132
|
+
try {
|
|
1133
|
+
handler(event);
|
|
1134
|
+
} catch {
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
async handleUserInputRequest(params) {
|
|
1139
|
+
if (!params || typeof params.sessionId !== "string" || typeof params.question !== "string") {
|
|
1140
|
+
throw new Error("Invalid user input request payload");
|
|
1141
|
+
}
|
|
1142
|
+
const session = this.sessions.get(params.sessionId);
|
|
1143
|
+
if (!session) {
|
|
1144
|
+
throw new Error(`Session not found: ${params.sessionId}`);
|
|
1145
|
+
}
|
|
1146
|
+
const result = await session._handleUserInputRequest({
|
|
1147
|
+
question: params.question,
|
|
1148
|
+
choices: params.choices,
|
|
1149
|
+
allowFreeform: params.allowFreeform
|
|
1150
|
+
});
|
|
1151
|
+
return result;
|
|
1152
|
+
}
|
|
1153
|
+
async handleHooksInvoke(params) {
|
|
1154
|
+
if (!params || typeof params.sessionId !== "string" || typeof params.hookType !== "string") {
|
|
1155
|
+
throw new Error("Invalid hooks invoke payload");
|
|
1156
|
+
}
|
|
1157
|
+
const session = this.sessions.get(params.sessionId);
|
|
1158
|
+
if (!session) {
|
|
1159
|
+
throw new Error(`Session not found: ${params.sessionId}`);
|
|
1160
|
+
}
|
|
1161
|
+
const output = await session._handleHooksInvoke(params.hookType, params.input);
|
|
1162
|
+
return { output };
|
|
1163
|
+
}
|
|
1164
|
+
// ========================================================================
|
|
1165
|
+
// Protocol v2 backward-compatibility adapters
|
|
1166
|
+
// ========================================================================
|
|
1167
|
+
/**
|
|
1168
|
+
* Handles a v2-style tool.call RPC request from the server.
|
|
1169
|
+
* Looks up the session and tool handler, executes it, and returns the result
|
|
1170
|
+
* in the v2 response format.
|
|
1171
|
+
*/
|
|
1172
|
+
async handleToolCallRequestV2(params) {
|
|
1173
|
+
if (!params || typeof params.sessionId !== "string" || typeof params.toolCallId !== "string" || typeof params.toolName !== "string") {
|
|
1174
|
+
throw new Error("Invalid tool call payload");
|
|
1175
|
+
}
|
|
1176
|
+
const session = this.sessions.get(params.sessionId);
|
|
1177
|
+
if (!session) {
|
|
1178
|
+
throw new Error(`Unknown session ${params.sessionId}`);
|
|
1179
|
+
}
|
|
1180
|
+
const handler = session.getToolHandler(params.toolName);
|
|
1181
|
+
if (!handler) {
|
|
1182
|
+
return {
|
|
1183
|
+
result: {
|
|
1184
|
+
textResultForLlm: `Tool '${params.toolName}' is not supported by this client instance.`,
|
|
1185
|
+
resultType: "failure",
|
|
1186
|
+
error: `tool '${params.toolName}' not supported`,
|
|
1187
|
+
toolTelemetry: {}
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
try {
|
|
1192
|
+
const traceparent = params.traceparent;
|
|
1193
|
+
const tracestate = params.tracestate;
|
|
1194
|
+
const invocation = {
|
|
1195
|
+
sessionId: params.sessionId,
|
|
1196
|
+
toolCallId: params.toolCallId,
|
|
1197
|
+
toolName: params.toolName,
|
|
1198
|
+
arguments: params.arguments,
|
|
1199
|
+
traceparent,
|
|
1200
|
+
tracestate
|
|
1201
|
+
};
|
|
1202
|
+
const result = await handler(params.arguments, invocation);
|
|
1203
|
+
return { result: this.normalizeToolResultV2(result) };
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1206
|
+
return {
|
|
1207
|
+
result: {
|
|
1208
|
+
textResultForLlm: "Invoking this tool produced an error. Detailed information is not available.",
|
|
1209
|
+
resultType: "failure",
|
|
1210
|
+
error: message,
|
|
1211
|
+
toolTelemetry: {}
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Handles a v2-style permission.request RPC request from the server.
|
|
1218
|
+
*/
|
|
1219
|
+
async handlePermissionRequestV2(params) {
|
|
1220
|
+
if (!params || typeof params.sessionId !== "string" || !params.permissionRequest) {
|
|
1221
|
+
throw new Error("Invalid permission request payload");
|
|
1222
|
+
}
|
|
1223
|
+
const session = this.sessions.get(params.sessionId);
|
|
1224
|
+
if (!session) {
|
|
1225
|
+
throw new Error(`Session not found: ${params.sessionId}`);
|
|
1226
|
+
}
|
|
1227
|
+
try {
|
|
1228
|
+
const result = await session._handlePermissionRequestV2(params.permissionRequest);
|
|
1229
|
+
return { result };
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
if (error instanceof Error && error.message === import_session.NO_RESULT_PERMISSION_V2_ERROR) {
|
|
1232
|
+
throw error;
|
|
1233
|
+
}
|
|
1234
|
+
return {
|
|
1235
|
+
result: {
|
|
1236
|
+
kind: "denied-no-approval-rule-and-could-not-request-from-user"
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
normalizeToolResultV2(result) {
|
|
1242
|
+
if (result === void 0 || result === null) {
|
|
1243
|
+
return {
|
|
1244
|
+
textResultForLlm: "Tool returned no result",
|
|
1245
|
+
resultType: "failure",
|
|
1246
|
+
error: "tool returned no result",
|
|
1247
|
+
toolTelemetry: {}
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
if (this.isToolResultObject(result)) {
|
|
1251
|
+
return result;
|
|
1252
|
+
}
|
|
1253
|
+
const textResult = typeof result === "string" ? result : JSON.stringify(result);
|
|
1254
|
+
return {
|
|
1255
|
+
textResultForLlm: textResult,
|
|
1256
|
+
resultType: "success",
|
|
1257
|
+
toolTelemetry: {}
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
isToolResultObject(value) {
|
|
1261
|
+
return typeof value === "object" && value !== null && "textResultForLlm" in value && typeof value.textResultForLlm === "string" && "resultType" in value;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1265
|
+
0 && (module.exports = {
|
|
1266
|
+
CopilotClient
|
|
1267
|
+
});
|