@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.
@@ -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
+ });