@github/copilot-sdk 1.0.0-beta.5 → 1.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -17,17 +17,15 @@ import {
17
17
  createInternalServerRpc,
18
18
  registerClientSessionApiHandlers
19
19
  } from "./generated/rpc.js";
20
+ import {
21
+ dispatchCanvasProviderRequest
22
+ } from "./canvas.js";
20
23
  import { getSdkProtocolVersion } from "./sdkProtocolVersion.js";
21
- import { CopilotSession, NO_RESULT_PERMISSION_V2_ERROR } from "./session.js";
24
+ import { CopilotSession } from "./session.js";
22
25
  import { createSessionFsAdapter } from "./sessionFsProvider.js";
23
26
  import { getTraceContext } from "./telemetry.js";
24
27
  import { defaultJoinSessionPermissionHandler } from "./types.js";
25
- function toWireProviderConfig(provider) {
26
- const { maxInputTokens, ...rest } = provider;
27
- if (maxInputTokens === void 0) return rest;
28
- return { ...rest, maxPromptTokens: maxInputTokens };
29
- }
30
- const MIN_PROTOCOL_VERSION = 2;
28
+ const MIN_PROTOCOL_VERSION = 3;
31
29
  function isZodSchema(value) {
32
30
  return value != null && typeof value === "object" && "toJSONSchema" in value && typeof value.toJSONSchema === "function";
33
31
  }
@@ -38,6 +36,36 @@ function toJsonSchema(parameters) {
38
36
  }
39
37
  return parameters;
40
38
  }
39
+ function toWireMcpServers(mcpServers) {
40
+ if (!mcpServers) return void 0;
41
+ return Object.fromEntries(
42
+ Object.entries(mcpServers).map(([name, server]) => {
43
+ if ("workingDirectory" in server) {
44
+ const { workingDirectory, ...rest } = server;
45
+ return [name, { ...rest, cwd: workingDirectory }];
46
+ }
47
+ return [name, server];
48
+ })
49
+ );
50
+ }
51
+ function toWireCustomAgents(agents) {
52
+ if (!agents) return void 0;
53
+ return agents.map((agent) => {
54
+ if (!agent.mcpServers) return agent;
55
+ const { mcpServers, ...rest } = agent;
56
+ return { ...rest, mcpServers: toWireMcpServers(mcpServers) };
57
+ });
58
+ }
59
+ function isCanvasProviderRequestParams(params) {
60
+ if (!params || typeof params !== "object") {
61
+ return false;
62
+ }
63
+ const request = params;
64
+ return typeof request.sessionId === "string" && typeof request.extensionId === "string" && typeof request.canvasId === "string" && typeof request.instanceId === "string";
65
+ }
66
+ function isCanvasActionInvokeParams(params) {
67
+ return isCanvasProviderRequestParams(params) && typeof params.actionName === "string";
68
+ }
41
69
  function extractTransformCallbacks(systemMessage) {
42
70
  if (!systemMessage || systemMessage.mode !== "customize" || !systemMessage.sections) {
43
71
  return { wirePayload: systemMessage, transformCallbacks: void 0 };
@@ -91,12 +119,18 @@ class CopilotClient {
91
119
  cliProcess = null;
92
120
  connection = null;
93
121
  socket = null;
94
- actualPort = null;
122
+ runtimePort = null;
95
123
  actualHost = "localhost";
96
124
  state = "disconnected";
97
125
  sessions = /* @__PURE__ */ new Map();
98
126
  stderrBuffer = "";
99
127
  // Captures CLI stderr for error messages
128
+ /** Resolved connection mode chosen in the constructor. */
129
+ connectionConfig;
130
+ /** Resolved path to the runtime executable (only used for child-process kinds). */
131
+ resolvedCliPath;
132
+ /** Resolved environment passed to the spawned runtime. */
133
+ resolvedEnv;
100
134
  options;
101
135
  isExternalServer = false;
102
136
  forceStopping = false;
@@ -145,86 +179,79 @@ class CopilotClient {
145
179
  * Creates a new CopilotClient instance.
146
180
  *
147
181
  * @param options - Configuration options for the client
148
- * @throws Error if mutually exclusive options are provided (e.g., cliUrl with useStdio or cliPath)
149
182
  *
150
183
  * @example
151
184
  * ```typescript
152
- * // Default options - spawns CLI server using stdio
185
+ * // Default: spawns the bundled runtime over stdio
153
186
  * const client = new CopilotClient();
154
187
  *
155
- * // Connect to an existing server
156
- * const client = new CopilotClient({ cliUrl: "localhost:3000" });
188
+ * // Connect to an existing runtime
189
+ * const client = new CopilotClient({
190
+ * connection: RuntimeConnection.forUri("localhost:3000"),
191
+ * });
157
192
  *
158
- * // Custom CLI path with specific log level
193
+ * // Spawn the runtime over TCP on a chosen port
159
194
  * const client = new CopilotClient({
160
- * cliPath: "/usr/local/bin/copilot",
161
- * logLevel: "debug"
195
+ * connection: RuntimeConnection.forTcp({ port: 9001 }),
196
+ * });
197
+ *
198
+ * // Use a custom runtime binary
199
+ * const client = new CopilotClient({
200
+ * connection: RuntimeConnection.forStdio({ path: "/usr/local/bin/copilot" }),
201
+ * logLevel: "debug",
162
202
  * });
163
203
  * ```
164
204
  */
165
205
  constructor(options = {}) {
166
- if (options.cliUrl && (options.useStdio === true || options.cliPath)) {
167
- throw new Error("cliUrl is mutually exclusive with useStdio and cliPath");
168
- }
169
- if (options.isChildProcess && (options.cliUrl || options.useStdio === false)) {
170
- throw new Error(
171
- "isChildProcess must be used in conjunction with useStdio and not with cliUrl"
172
- );
173
- }
174
- if (options.cliUrl && (options.gitHubToken || options.useLoggedInUser !== void 0)) {
206
+ const conn = options._internalConnection ?? options.connection ?? { kind: "stdio" };
207
+ if (conn.kind === "uri" && (options.gitHubToken !== void 0 || options.useLoggedInUser !== void 0)) {
175
208
  throw new Error(
176
- "gitHubToken and useLoggedInUser cannot be used with cliUrl (external server manages its own auth)"
209
+ "gitHubToken and useLoggedInUser cannot be used with RuntimeConnection.forUri (external server manages its own auth)"
177
210
  );
178
211
  }
179
- if (options.tcpConnectionToken !== void 0) {
180
- if (typeof options.tcpConnectionToken !== "string" || options.tcpConnectionToken.length === 0) {
181
- throw new Error("tcpConnectionToken must be a non-empty string");
182
- }
183
- if (options.useStdio === true) {
184
- throw new Error("tcpConnectionToken cannot be used with useStdio: true");
212
+ if (conn.kind === "tcp" && conn.connectionToken !== void 0) {
213
+ if (typeof conn.connectionToken !== "string" || conn.connectionToken.length === 0) {
214
+ throw new Error("connectionToken must be a non-empty string");
185
215
  }
186
216
  }
187
- const willUseStdio = options.cliUrl ? false : options.useStdio ?? true;
188
- const sdkSpawnsCli = !willUseStdio && !options.cliUrl && !options.isChildProcess;
189
- this.effectiveConnectionToken = options.tcpConnectionToken ?? (sdkSpawnsCli ? randomUUID() : void 0);
217
+ this.connectionConfig = conn;
190
218
  if (options.sessionFs) {
191
219
  this.validateSessionFsConfig(options.sessionFs);
192
220
  }
193
- if (options.cliUrl) {
194
- const { host, port } = this.parseCliUrl(options.cliUrl);
221
+ if (conn.kind === "uri") {
222
+ const { host, port } = this.parseCliUrl(conn.url);
195
223
  this.actualHost = host;
196
- this.actualPort = port;
224
+ this.runtimePort = port;
197
225
  this.isExternalServer = true;
198
- }
199
- if (options.isChildProcess) {
226
+ } else if (conn.kind === "parent-process") {
200
227
  this.isExternalServer = true;
201
228
  }
229
+ if (conn.kind === "tcp") {
230
+ this.effectiveConnectionToken = conn.connectionToken ?? randomUUID();
231
+ } else if (conn.kind === "uri") {
232
+ this.effectiveConnectionToken = conn.connectionToken;
233
+ }
202
234
  this.onListModels = options.onListModels;
203
235
  this.onGetTraceContext = options.onGetTraceContext;
204
236
  this.sessionFsConfig = options.sessionFs ?? null;
205
237
  const effectiveEnv = options.env ?? process.env;
238
+ this.resolvedEnv = effectiveEnv;
239
+ this.resolvedCliPath = conn.kind === "stdio" || conn.kind === "tcp" ? conn.path ?? effectiveEnv.COPILOT_CLI_PATH ?? getBundledCliPath() : void 0;
240
+ const connArgs = conn.kind === "stdio" || conn.kind === "tcp" ? conn.args ?? [] : [];
241
+ this.connectionExtraArgs = [...connArgs];
206
242
  this.options = {
207
- cliPath: options.cliUrl ? void 0 : options.cliPath || effectiveEnv.COPILOT_CLI_PATH || getBundledCliPath(),
208
- cliArgs: options.cliArgs ?? [],
209
- cwd: options.cwd ?? process.cwd(),
210
- port: options.port || 0,
211
- useStdio: options.cliUrl ? false : options.useStdio ?? true,
212
- // Default to stdio unless cliUrl is provided
213
- isChildProcess: options.isChildProcess ?? false,
214
- cliUrl: options.cliUrl,
215
- logLevel: options.logLevel || "debug",
216
- autoStart: options.autoStart ?? true,
217
- autoRestart: false,
218
- env: effectiveEnv,
243
+ workingDirectory: options.workingDirectory ?? process.cwd(),
244
+ logLevel: options.logLevel,
219
245
  gitHubToken: options.gitHubToken,
220
- // Default useLoggedInUser to false when gitHubToken is provided, otherwise true
246
+ // Default useLoggedInUser to false when gitHubToken is provided, otherwise true.
221
247
  useLoggedInUser: options.useLoggedInUser ?? (options.gitHubToken ? false : true),
222
248
  telemetry: options.telemetry,
223
- copilotHome: options.copilotHome,
249
+ baseDirectory: options.baseDirectory,
224
250
  sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0,
225
- remote: options.remote ?? false
251
+ enableRemoteSessions: options.enableRemoteSessions ?? false
226
252
  };
227
253
  }
254
+ connectionExtraArgs = [];
228
255
  /**
229
256
  * Parse CLI URL into host and port
230
257
  * Supports formats: "host:port", "http://host:port", "https://host:port", or just "port"
@@ -262,12 +289,12 @@ class CopilotClient {
262
289
  if (!this.sessionFsConfig) {
263
290
  return;
264
291
  }
265
- if (!config.createSessionFsHandler) {
292
+ if (!config.createSessionFsProvider) {
266
293
  throw new Error(
267
- "createSessionFsHandler is required in session config when sessionFs is enabled in client options."
294
+ "createSessionFsProvider is required in session config when sessionFs is enabled in client options."
268
295
  );
269
296
  }
270
- const provider = config.createSessionFsHandler(session);
297
+ const provider = config.createSessionFsProvider(session);
271
298
  if (this.sessionFsConfig.capabilities?.sqlite && !provider.sqlite) {
272
299
  throw new Error(
273
300
  "SessionFsConfig declares capabilities.sqlite but the provider does not implement sqlite."
@@ -281,14 +308,14 @@ class CopilotClient {
281
308
  * If connecting to an external server (via cliUrl), only establishes the connection.
282
309
  * Otherwise, spawns the CLI server process and then connects.
283
310
  *
284
- * This method is called automatically when creating a session if `autoStart` is true (default).
311
+ * This method is called automatically the first time you create or resume a session.
285
312
  *
286
313
  * @returns A promise that resolves when the connection is established
287
314
  * @throws Error if the server fails to start or the connection fails
288
315
  *
289
316
  * @example
290
317
  * ```typescript
291
- * const client = new CopilotClient({ autoStart: false });
318
+ * const client = new CopilotClient();
292
319
  * await client.start();
293
320
  * // Now ready to create sessions
294
321
  * ```
@@ -383,8 +410,15 @@ class CopilotClient {
383
410
  }
384
411
  this.modelsCache = null;
385
412
  if (this.socket) {
413
+ const socket = this.socket;
414
+ this.socket = null;
386
415
  try {
387
- this.socket.end();
416
+ if (!socket.destroyed) {
417
+ await new Promise((resolve) => {
418
+ socket.once("close", () => resolve());
419
+ socket.end();
420
+ });
421
+ }
388
422
  } catch (error) {
389
423
  errors.push(
390
424
  new Error(
@@ -392,11 +426,18 @@ class CopilotClient {
392
426
  )
393
427
  );
394
428
  }
395
- this.socket = null;
396
429
  }
397
430
  if (this.cliProcess && !this.isExternalServer) {
431
+ const child = this.cliProcess;
432
+ this.cliProcess = null;
398
433
  try {
399
- this.cliProcess.kill();
434
+ if (child.exitCode === null && child.signalCode === null) {
435
+ const exited = new Promise((resolve) => {
436
+ child.once("exit", () => resolve());
437
+ });
438
+ child.kill();
439
+ await exited;
440
+ }
400
441
  } catch (error) {
401
442
  errors.push(
402
443
  new Error(
@@ -404,18 +445,32 @@ class CopilotClient {
404
445
  )
405
446
  );
406
447
  }
407
- this.cliProcess = null;
408
448
  }
409
449
  if (this.cliStartTimeout) {
410
450
  clearTimeout(this.cliStartTimeout);
411
451
  this.cliStartTimeout = null;
412
452
  }
413
453
  this.state = "disconnected";
414
- this.actualPort = null;
454
+ this.runtimePort = null;
415
455
  this.stderrBuffer = "";
416
456
  this.processExitPromise = null;
417
457
  return errors;
418
458
  }
459
+ /**
460
+ * Alias for {@link stop} that lets `CopilotClient` participate in `await using`
461
+ * blocks for automatic cleanup.
462
+ *
463
+ * @example
464
+ * ```typescript
465
+ * await using client = new CopilotClient();
466
+ * const session = await client.createSession({ onPermissionRequest: approveAll });
467
+ * await session.sendAndWait("Hello");
468
+ * // client.stop() is called automatically when the block exits.
469
+ * ```
470
+ */
471
+ async [Symbol.asyncDispose]() {
472
+ await this.stop();
473
+ }
419
474
  /**
420
475
  * Forcefully stops the CLI server without graceful cleanup.
421
476
  *
@@ -472,7 +527,7 @@ class CopilotClient {
472
527
  this.cliStartTimeout = null;
473
528
  }
474
529
  this.state = "disconnected";
475
- this.actualPort = null;
530
+ this.runtimePort = null;
476
531
  this.stderrBuffer = "";
477
532
  this.processExitPromise = null;
478
533
  }
@@ -480,12 +535,11 @@ class CopilotClient {
480
535
  * Creates a new conversation session with the Copilot CLI.
481
536
  *
482
537
  * Sessions maintain conversation state, handle events, and manage tool execution.
483
- * If the client is not connected and `autoStart` is enabled, this will automatically
484
- * start the connection.
538
+ * If the client is not connected, this method automatically starts the connection.
485
539
  *
486
540
  * @param config - Optional configuration for the session
487
541
  * @returns A promise that resolves with the created session
488
- * @throws Error if the client is not connected and autoStart is disabled
542
+ * @throws Error if the client fails to start
489
543
  *
490
544
  * @example
491
545
  * ```typescript
@@ -507,11 +561,7 @@ class CopilotClient {
507
561
  */
508
562
  async createSession(config) {
509
563
  if (!this.connection) {
510
- if (this.options.autoStart) {
511
- await this.start();
512
- } else {
513
- throw new Error("Client not connected. Call start() first.");
514
- }
564
+ await this.start();
515
565
  }
516
566
  const sessionId = config.sessionId ?? randomUUID();
517
567
  const session = new CopilotSession(
@@ -521,6 +571,7 @@ class CopilotClient {
521
571
  this.onGetTraceContext
522
572
  );
523
573
  session.registerTools(config.tools);
574
+ session.registerCanvases(config.canvases);
524
575
  session.registerCommands(config.commands);
525
576
  session.registerPermissionHandler(config.onPermissionRequest);
526
577
  if (config.onUserInputRequest) {
@@ -529,11 +580,11 @@ class CopilotClient {
529
580
  if (config.onElicitationRequest) {
530
581
  session.registerElicitationHandler(config.onElicitationRequest);
531
582
  }
532
- if (config.onExitPlanMode) {
533
- session.registerExitPlanModeHandler(config.onExitPlanMode);
583
+ if (config.onExitPlanModeRequest) {
584
+ session.registerExitPlanModeHandler(config.onExitPlanModeRequest);
534
585
  }
535
- if (config.onAutoModeSwitch) {
536
- session.registerAutoModeSwitchHandler(config.onAutoModeSwitch);
586
+ if (config.onAutoModeSwitchRequest) {
587
+ session.registerAutoModeSwitchHandler(config.onAutoModeSwitchRequest);
537
588
  }
538
589
  if (config.hooks) {
539
590
  session.registerHooks(config.hooks);
@@ -563,6 +614,10 @@ class CopilotClient {
563
614
  overridesBuiltInTool: tool.overridesBuiltInTool,
564
615
  skipPermission: tool.skipPermission
565
616
  })),
617
+ canvases: config.canvases?.map((canvas) => canvas.declaration),
618
+ requestCanvasRenderer: config.requestCanvasRenderer,
619
+ requestExtensions: config.requestExtensions,
620
+ extensionInfo: config.extensionInfo,
566
621
  commands: config.commands?.map((cmd) => ({
567
622
  name: cmd.name,
568
623
  description: cmd.description
@@ -570,21 +625,21 @@ class CopilotClient {
570
625
  systemMessage: wireSystemMessage,
571
626
  availableTools: config.availableTools,
572
627
  excludedTools: config.excludedTools,
573
- provider: config.provider ? toWireProviderConfig(config.provider) : void 0,
628
+ provider: config.provider,
574
629
  enableSessionTelemetry: config.enableSessionTelemetry,
575
630
  modelCapabilities: config.modelCapabilities,
576
- requestPermission: true,
631
+ requestPermission: !!config.onPermissionRequest,
577
632
  requestUserInput: !!config.onUserInputRequest,
578
633
  requestElicitation: !!config.onElicitationRequest,
579
- requestExitPlanMode: !!config.onExitPlanMode,
580
- requestAutoModeSwitch: !!config.onAutoModeSwitch,
634
+ requestExitPlanMode: !!config.onExitPlanModeRequest,
635
+ requestAutoModeSwitch: !!config.onAutoModeSwitchRequest,
581
636
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
582
637
  workingDirectory: config.workingDirectory,
583
638
  streaming: config.streaming,
584
639
  includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
585
- mcpServers: config.mcpServers,
640
+ mcpServers: toWireMcpServers(config.mcpServers),
586
641
  envValueMode: "direct",
587
- customAgents: config.customAgents,
642
+ customAgents: toWireCustomAgents(config.customAgents),
588
643
  defaultAgent: config.defaultAgent,
589
644
  agent: config.agent,
590
645
  configDir: config.configDir,
@@ -632,11 +687,7 @@ class CopilotClient {
632
687
  */
633
688
  async resumeSession(sessionId, config) {
634
689
  if (!this.connection) {
635
- if (this.options.autoStart) {
636
- await this.start();
637
- } else {
638
- throw new Error("Client not connected. Call start() first.");
639
- }
690
+ await this.start();
640
691
  }
641
692
  const session = new CopilotSession(
642
693
  sessionId,
@@ -645,6 +696,7 @@ class CopilotClient {
645
696
  this.onGetTraceContext
646
697
  );
647
698
  session.registerTools(config.tools);
699
+ session.registerCanvases(config.canvases);
648
700
  session.registerCommands(config.commands);
649
701
  session.registerPermissionHandler(config.onPermissionRequest);
650
702
  if (config.onUserInputRequest) {
@@ -653,11 +705,11 @@ class CopilotClient {
653
705
  if (config.onElicitationRequest) {
654
706
  session.registerElicitationHandler(config.onElicitationRequest);
655
707
  }
656
- if (config.onExitPlanMode) {
657
- session.registerExitPlanModeHandler(config.onExitPlanMode);
708
+ if (config.onExitPlanModeRequest) {
709
+ session.registerExitPlanModeHandler(config.onExitPlanModeRequest);
658
710
  }
659
- if (config.onAutoModeSwitch) {
660
- session.registerAutoModeSwitchHandler(config.onAutoModeSwitch);
711
+ if (config.onAutoModeSwitchRequest) {
712
+ session.registerAutoModeSwitchHandler(config.onAutoModeSwitchRequest);
661
713
  }
662
714
  if (config.hooks) {
663
715
  session.registerHooks(config.hooks);
@@ -691,61 +743,52 @@ class CopilotClient {
691
743
  overridesBuiltInTool: tool.overridesBuiltInTool,
692
744
  skipPermission: tool.skipPermission
693
745
  })),
746
+ canvases: config.canvases?.map((canvas) => canvas.declaration),
747
+ requestCanvasRenderer: config.requestCanvasRenderer,
748
+ requestExtensions: config.requestExtensions,
749
+ extensionInfo: config.extensionInfo,
694
750
  commands: config.commands?.map((cmd) => ({
695
751
  name: cmd.name,
696
752
  description: cmd.description
697
753
  })),
698
- provider: config.provider ? toWireProviderConfig(config.provider) : void 0,
754
+ provider: config.provider,
699
755
  modelCapabilities: config.modelCapabilities,
700
756
  requestPermission: config.onPermissionRequest !== defaultJoinSessionPermissionHandler,
701
757
  requestUserInput: !!config.onUserInputRequest,
702
758
  requestElicitation: !!config.onElicitationRequest,
703
- requestExitPlanMode: !!config.onExitPlanMode,
704
- requestAutoModeSwitch: !!config.onAutoModeSwitch,
759
+ requestExitPlanMode: !!config.onExitPlanModeRequest,
760
+ requestAutoModeSwitch: !!config.onAutoModeSwitchRequest,
705
761
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
706
762
  workingDirectory: config.workingDirectory,
707
763
  configDir: config.configDir,
708
764
  enableConfigDiscovery: config.enableConfigDiscovery,
709
765
  streaming: config.streaming,
710
766
  includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
711
- mcpServers: config.mcpServers,
767
+ mcpServers: toWireMcpServers(config.mcpServers),
712
768
  envValueMode: "direct",
713
- customAgents: config.customAgents,
769
+ customAgents: toWireCustomAgents(config.customAgents),
714
770
  defaultAgent: config.defaultAgent,
715
771
  agent: config.agent,
716
772
  skillDirectories: config.skillDirectories,
717
773
  instructionDirectories: config.instructionDirectories,
718
774
  disabledSkills: config.disabledSkills,
719
775
  infiniteSessions: config.infiniteSessions,
720
- disableResume: config.disableResume,
776
+ suppressResumeEvent: config.suppressResumeEvent,
721
777
  continuePendingWork: config.continuePendingWork,
722
778
  gitHubToken: config.gitHubToken,
723
- remoteSession: config.remoteSession
779
+ remoteSession: config.remoteSession,
780
+ openCanvases: config.openCanvases
724
781
  });
725
- const { workspacePath, capabilities } = response;
782
+ const { workspacePath, capabilities, openCanvases } = response;
726
783
  session["_workspacePath"] = workspacePath;
727
784
  session.setCapabilities(capabilities);
785
+ session.setOpenCanvases(openCanvases ?? []);
728
786
  } catch (e) {
729
787
  this.sessions.delete(sessionId);
730
788
  throw e;
731
789
  }
732
790
  return session;
733
791
  }
734
- /**
735
- * Gets the current connection state of the client.
736
- *
737
- * @returns The current connection state: "disconnected", "connecting", "connected", or "error"
738
- *
739
- * @example
740
- * ```typescript
741
- * if (client.getState() === "connected") {
742
- * const session = await client.createSession({ onPermissionRequest: approveAll });
743
- * }
744
- * ```
745
- */
746
- getState() {
747
- return this.state;
748
- }
749
792
  /**
750
793
  * Sends a ping request to the server to verify connectivity.
751
794
  *
@@ -947,8 +990,13 @@ class CopilotClient {
947
990
  if (!this.connection) {
948
991
  throw new Error("Client not connected");
949
992
  }
993
+ let wireFilter;
994
+ if (filter) {
995
+ const { workingDirectory, ...rest } = filter;
996
+ wireFilter = { ...rest, cwd: workingDirectory };
997
+ }
950
998
  const response = await this.connection.sendRequest("session.list", {
951
- filter
999
+ filter: wireFilter
952
1000
  });
953
1001
  const { sessions } = response;
954
1002
  return sessions.map(CopilotClient.toSessionMetadata);
@@ -983,13 +1031,19 @@ class CopilotClient {
983
1031
  return CopilotClient.toSessionMetadata(session);
984
1032
  }
985
1033
  static toSessionMetadata(raw) {
1034
+ const { context } = raw;
986
1035
  return {
987
1036
  sessionId: raw.sessionId,
988
1037
  startTime: new Date(raw.startTime),
989
1038
  modifiedTime: new Date(raw.modifiedTime),
990
1039
  summary: raw.summary,
991
1040
  isRemote: raw.isRemote,
992
- context: raw.context
1041
+ context: context ? {
1042
+ workingDirectory: context.cwd,
1043
+ gitRoot: context.gitRoot,
1044
+ repository: context.repository,
1045
+ branch: context.branch
1046
+ } : void 0
993
1047
  };
994
1048
  }
995
1049
  /**
@@ -1042,7 +1096,7 @@ class CopilotClient {
1042
1096
  throw new Error(result.error || "Failed to set foreground session");
1043
1097
  }
1044
1098
  }
1045
- on(eventTypeOrHandler, handler) {
1099
+ onLifecycle(eventTypeOrHandler, handler) {
1046
1100
  if (typeof eventTypeOrHandler === "string" && handler) {
1047
1101
  const eventType = eventTypeOrHandler;
1048
1102
  if (!this.typedLifecycleHandlers.has(eventType)) {
@@ -1069,17 +1123,17 @@ class CopilotClient {
1069
1123
  async startCLIServer() {
1070
1124
  return new Promise((resolve, reject) => {
1071
1125
  this.stderrBuffer = "";
1072
- const args = [
1073
- ...this.options.cliArgs,
1074
- "--headless",
1075
- "--no-auto-update",
1076
- "--log-level",
1077
- this.options.logLevel
1078
- ];
1079
- if (this.options.useStdio) {
1126
+ const args = [...this.connectionExtraArgs, "--headless", "--no-auto-update"];
1127
+ if (this.options.logLevel) {
1128
+ args.push("--log-level", this.options.logLevel);
1129
+ }
1130
+ if (this.connectionConfig.kind === "stdio") {
1080
1131
  args.push("--stdio");
1081
- } else if (this.options.port > 0) {
1082
- args.push("--port", this.options.port.toString());
1132
+ } else if (this.connectionConfig.kind === "tcp") {
1133
+ const requestedPort = this.connectionConfig.port ?? 0;
1134
+ if (requestedPort > 0) {
1135
+ args.push("--port", requestedPort.toString());
1136
+ }
1083
1137
  }
1084
1138
  if (this.options.gitHubToken) {
1085
1139
  args.push("--auth-token-env", "COPILOT_SDK_AUTH_TOKEN");
@@ -1093,10 +1147,10 @@ class CopilotClient {
1093
1147
  this.options.sessionIdleTimeoutSeconds.toString()
1094
1148
  );
1095
1149
  }
1096
- if (this.options.remote) {
1150
+ if (this.options.enableRemoteSessions) {
1097
1151
  args.push("--remote");
1098
1152
  }
1099
- const envWithoutNodeDebug = { ...this.options.env };
1153
+ const envWithoutNodeDebug = { ...this.resolvedEnv };
1100
1154
  delete envWithoutNodeDebug.NODE_DEBUG;
1101
1155
  if (this.options.gitHubToken) {
1102
1156
  envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.gitHubToken;
@@ -1104,12 +1158,12 @@ class CopilotClient {
1104
1158
  if (this.effectiveConnectionToken) {
1105
1159
  envWithoutNodeDebug.COPILOT_CONNECTION_TOKEN = this.effectiveConnectionToken;
1106
1160
  }
1107
- if (this.options.copilotHome) {
1108
- envWithoutNodeDebug.COPILOT_HOME = this.options.copilotHome;
1161
+ if (this.options.baseDirectory) {
1162
+ envWithoutNodeDebug.COPILOT_HOME = this.options.baseDirectory;
1109
1163
  }
1110
- if (!this.options.cliPath) {
1164
+ if (!this.resolvedCliPath) {
1111
1165
  throw new Error(
1112
- "Path to Copilot CLI is required. Please provide it via the cliPath option, or use cliUrl to rely on a remote CLI."
1166
+ "Path to Copilot CLI is required. Please supply it via `RuntimeConnection.forStdio({ path })` or `RuntimeConnection.forTcp({ path })`, set the COPILOT_CLI_PATH environment variable, or use `RuntimeConnection.forUri(...)` to connect to an already-running runtime."
1113
1167
  );
1114
1168
  }
1115
1169
  if (this.options.telemetry) {
@@ -1128,31 +1182,31 @@ class CopilotClient {
1128
1182
  t.captureContent
1129
1183
  );
1130
1184
  }
1131
- if (!existsSync(this.options.cliPath)) {
1185
+ if (!existsSync(this.resolvedCliPath)) {
1132
1186
  throw new Error(
1133
- `Copilot CLI not found at ${this.options.cliPath}. Ensure @github/copilot is installed.`
1187
+ `Copilot CLI not found at ${this.resolvedCliPath}. Ensure @github/copilot is installed.`
1134
1188
  );
1135
1189
  }
1136
- const stdioConfig = this.options.useStdio ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
1137
- const isJsFile = this.options.cliPath.endsWith(".js");
1190
+ const stdioConfig = this.connectionConfig.kind === "stdio" ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
1191
+ const isJsFile = this.resolvedCliPath.endsWith(".js");
1138
1192
  if (isJsFile) {
1139
- this.cliProcess = spawn(getNodeExecPath(), [this.options.cliPath, ...args], {
1193
+ this.cliProcess = spawn(getNodeExecPath(), [this.resolvedCliPath, ...args], {
1140
1194
  stdio: stdioConfig,
1141
- cwd: this.options.cwd,
1195
+ cwd: this.options.workingDirectory,
1142
1196
  env: envWithoutNodeDebug,
1143
1197
  windowsHide: true
1144
1198
  });
1145
1199
  } else {
1146
- this.cliProcess = spawn(this.options.cliPath, args, {
1200
+ this.cliProcess = spawn(this.resolvedCliPath, args, {
1147
1201
  stdio: stdioConfig,
1148
- cwd: this.options.cwd,
1202
+ cwd: this.options.workingDirectory,
1149
1203
  env: envWithoutNodeDebug,
1150
1204
  windowsHide: true
1151
1205
  });
1152
1206
  }
1153
1207
  let stdout = "";
1154
1208
  let resolved = false;
1155
- if (this.options.useStdio) {
1209
+ if (this.connectionConfig.kind === "stdio") {
1156
1210
  resolved = true;
1157
1211
  resolve();
1158
1212
  } else {
@@ -1160,7 +1214,7 @@ class CopilotClient {
1160
1214
  stdout += data.toString();
1161
1215
  const match = stdout.match(/listening on port (\d+)/i);
1162
1216
  if (match && !resolved) {
1163
- this.actualPort = parseInt(match[1], 10);
1217
+ this.runtimePort = parseInt(match[1], 10);
1164
1218
  resolved = true;
1165
1219
  resolve();
1166
1220
  }
@@ -1234,19 +1288,21 @@ stderr: ${stderrOutput}`
1234
1288
  resolved = true;
1235
1289
  reject(new Error("Timeout waiting for CLI server to start"));
1236
1290
  }
1237
- }, 1e4);
1291
+ }, 3e4);
1238
1292
  });
1239
1293
  }
1240
1294
  /**
1241
1295
  * Connect to the CLI server (via socket or stdio)
1242
1296
  */
1243
1297
  async connectToServer() {
1244
- if (this.options.isChildProcess) {
1245
- return this.connectToParentProcessViaStdio();
1246
- } else if (this.options.useStdio) {
1247
- return this.connectToChildProcessViaStdio();
1248
- } else {
1249
- return this.connectViaTcp();
1298
+ switch (this.connectionConfig.kind) {
1299
+ case "parent-process":
1300
+ return this.connectToParentProcessViaStdio();
1301
+ case "stdio":
1302
+ return this.connectToChildProcessViaStdio();
1303
+ case "tcp":
1304
+ case "uri":
1305
+ return this.connectViaTcp();
1250
1306
  }
1251
1307
  }
1252
1308
  /**
@@ -1286,7 +1342,7 @@ stderr: ${stderrOutput}`
1286
1342
  * Connect to the CLI server via TCP socket
1287
1343
  */
1288
1344
  async connectViaTcp() {
1289
- if (!this.actualPort) {
1345
+ if (!this.runtimePort) {
1290
1346
  throw new Error("Server port not available");
1291
1347
  }
1292
1348
  return new Promise((resolve, reject) => {
@@ -1295,7 +1351,7 @@ stderr: ${stderrOutput}`
1295
1351
  this.socket?.destroy();
1296
1352
  reject(new Error("Timeout connecting to CLI server"));
1297
1353
  }, 1e4);
1298
- this.socket.connect(this.actualPort, this.actualHost, () => {
1354
+ this.socket.connect(this.runtimePort, this.actualHost, () => {
1299
1355
  clearTimeout(connectionTimeout);
1300
1356
  this.connection = createMessageConnection(
1301
1357
  new StreamMessageReader(this.socket),
@@ -1321,14 +1377,6 @@ stderr: ${stderrOutput}`
1321
1377
  this.connection.onNotification("session.lifecycle", (notification) => {
1322
1378
  this.handleSessionLifecycleNotification(notification);
1323
1379
  });
1324
- this.connection.onRequest(
1325
- "tool.call",
1326
- async (params) => await this.handleToolCallRequestV2(params)
1327
- );
1328
- this.connection.onRequest(
1329
- "permission.request",
1330
- async (params) => await this.handlePermissionRequestV2(params)
1331
- );
1332
1380
  this.connection.onRequest(
1333
1381
  "userInput.request",
1334
1382
  async (params) => await this.handleUserInputRequest(params)
@@ -1349,6 +1397,18 @@ stderr: ${stderrOutput}`
1349
1397
  "systemMessage.transform",
1350
1398
  async (params) => await this.handleSystemMessageTransform(params)
1351
1399
  );
1400
+ this.connection.onRequest(
1401
+ "canvas.open",
1402
+ async (params) => this.handleCanvasProviderRequest("canvas.open", params)
1403
+ );
1404
+ this.connection.onRequest(
1405
+ "canvas.close",
1406
+ async (params) => this.handleCanvasProviderRequest("canvas.close", params)
1407
+ );
1408
+ this.connection.onRequest(
1409
+ "canvas.action.invoke",
1410
+ async (params) => this.handleCanvasActionInvokeRequest(params)
1411
+ );
1352
1412
  const sessions = this.sessions;
1353
1413
  registerClientSessionApiHandlers(this.connection, (sessionId) => {
1354
1414
  const session = sessions.get(sessionId);
@@ -1375,7 +1435,20 @@ stderr: ${stderrOutput}`
1375
1435
  if (typeof notification !== "object" || !notification || !("type" in notification) || typeof notification.type !== "string" || !("sessionId" in notification) || typeof notification.sessionId !== "string") {
1376
1436
  return;
1377
1437
  }
1378
- const event = notification;
1438
+ const raw = notification;
1439
+ let metadata;
1440
+ if (raw.metadata && raw.metadata.startTime && raw.metadata.modifiedTime) {
1441
+ metadata = {
1442
+ startTime: new Date(raw.metadata.startTime),
1443
+ modifiedTime: new Date(raw.metadata.modifiedTime),
1444
+ summary: raw.metadata.summary
1445
+ };
1446
+ }
1447
+ const event = {
1448
+ type: raw.type,
1449
+ sessionId: raw.sessionId,
1450
+ metadata
1451
+ };
1379
1452
  const typedHandlers = this.typedLifecycleHandlers.get(event.type);
1380
1453
  if (typedHandlers) {
1381
1454
  for (const handler of typedHandlers) {
@@ -1457,104 +1530,25 @@ stderr: ${stderrOutput}`
1457
1530
  }
1458
1531
  return await session._handleSystemMessageTransform(params.sections);
1459
1532
  }
1460
- // ========================================================================
1461
- // Protocol v2 backward-compatibility adapters
1462
- // ========================================================================
1463
- /**
1464
- * Handles a v2-style tool.call RPC request from the server.
1465
- * Looks up the session and tool handler, executes it, and returns the result
1466
- * in the v2 response format.
1467
- */
1468
- async handleToolCallRequestV2(params) {
1469
- if (!params || typeof params.sessionId !== "string" || typeof params.toolCallId !== "string" || typeof params.toolName !== "string") {
1470
- throw new Error("Invalid tool call payload");
1471
- }
1472
- const session = this.sessions.get(params.sessionId);
1473
- if (!session) {
1474
- throw new Error(`Unknown session ${params.sessionId}`);
1475
- }
1476
- const handler = session.getToolHandler(params.toolName);
1477
- if (!handler) {
1478
- return {
1479
- result: {
1480
- textResultForLlm: `Tool '${params.toolName}' is not supported by this client instance.`,
1481
- resultType: "failure",
1482
- error: `tool '${params.toolName}' not supported`,
1483
- toolTelemetry: {}
1484
- }
1485
- };
1486
- }
1487
- try {
1488
- const traceparent = params.traceparent;
1489
- const tracestate = params.tracestate;
1490
- const invocation = {
1491
- sessionId: params.sessionId,
1492
- toolCallId: params.toolCallId,
1493
- toolName: params.toolName,
1494
- arguments: params.arguments,
1495
- traceparent,
1496
- tracestate
1497
- };
1498
- const result = await handler(params.arguments, invocation);
1499
- return { result: this.normalizeToolResultV2(result) };
1500
- } catch (error) {
1501
- const message = error instanceof Error ? error.message : String(error);
1502
- return {
1503
- result: {
1504
- textResultForLlm: "Invoking this tool produced an error. Detailed information is not available.",
1505
- resultType: "failure",
1506
- error: message,
1507
- toolTelemetry: {}
1508
- }
1509
- };
1510
- }
1511
- }
1512
- /**
1513
- * Handles a v2-style permission.request RPC request from the server.
1514
- */
1515
- async handlePermissionRequestV2(params) {
1516
- if (!params || typeof params.sessionId !== "string" || !params.permissionRequest) {
1517
- throw new Error("Invalid permission request payload");
1533
+ async handleCanvasProviderRequest(actionName, params) {
1534
+ if (!isCanvasProviderRequestParams(params)) {
1535
+ throw new Error("Invalid canvas provider request payload");
1518
1536
  }
1519
1537
  const session = this.sessions.get(params.sessionId);
1520
1538
  if (!session) {
1521
1539
  throw new Error(`Session not found: ${params.sessionId}`);
1522
1540
  }
1523
- try {
1524
- const result = await session._handlePermissionRequestV2(params.permissionRequest);
1525
- return { result };
1526
- } catch (error) {
1527
- if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) {
1528
- throw error;
1529
- }
1530
- return {
1531
- result: {
1532
- kind: "user-not-available"
1533
- }
1534
- };
1541
+ const canvas = session.getCanvas(params.canvasId);
1542
+ if (!canvas) {
1543
+ throw new Error(`No canvas registered with id "${params.canvasId}"`);
1535
1544
  }
1545
+ return dispatchCanvasProviderRequest(canvas, actionName, params);
1536
1546
  }
1537
- normalizeToolResultV2(result) {
1538
- if (result === void 0 || result === null) {
1539
- return {
1540
- textResultForLlm: "Tool returned no result",
1541
- resultType: "failure",
1542
- error: "tool returned no result",
1543
- toolTelemetry: {}
1544
- };
1545
- }
1546
- if (this.isToolResultObject(result)) {
1547
- return result;
1547
+ async handleCanvasActionInvokeRequest(params) {
1548
+ if (!isCanvasActionInvokeParams(params)) {
1549
+ throw new Error("Invalid canvas provider request payload");
1548
1550
  }
1549
- const textResult = typeof result === "string" ? result : JSON.stringify(result);
1550
- return {
1551
- textResultForLlm: textResult,
1552
- resultType: "success",
1553
- toolTelemetry: {}
1554
- };
1555
- }
1556
- isToolResultObject(value) {
1557
- return typeof value === "object" && value !== null && "textResultForLlm" in value && typeof value.textResultForLlm === "string" && "resultType" in value;
1551
+ return this.handleCanvasProviderRequest(params.actionName, params);
1558
1552
  }
1559
1553
  }
1560
1554
  export {