@github/copilot-sdk 1.0.0-beta.1 → 1.0.0-beta.11

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
@@ -18,11 +18,12 @@ import {
18
18
  registerClientSessionApiHandlers
19
19
  } from "./generated/rpc.js";
20
20
  import { getSdkProtocolVersion } from "./sdkProtocolVersion.js";
21
- import { CopilotSession, NO_RESULT_PERMISSION_V2_ERROR } from "./session.js";
21
+ import { CopilotSession } from "./session.js";
22
22
  import { createSessionFsAdapter } from "./sessionFsProvider.js";
23
23
  import { getTraceContext } from "./telemetry.js";
24
+ import { ToolSet } from "./toolSet.js";
24
25
  import { defaultJoinSessionPermissionHandler } from "./types.js";
25
- const MIN_PROTOCOL_VERSION = 2;
26
+ const MIN_PROTOCOL_VERSION = 3;
26
27
  function isZodSchema(value) {
27
28
  return value != null && typeof value === "object" && "toJSONSchema" in value && typeof value.toJSONSchema === "function";
28
29
  }
@@ -33,6 +34,53 @@ function toJsonSchema(parameters) {
33
34
  }
34
35
  return parameters;
35
36
  }
37
+ function toWireMcpServers(mcpServers) {
38
+ if (!mcpServers) return void 0;
39
+ return Object.fromEntries(
40
+ Object.entries(mcpServers).map(([name, server]) => {
41
+ if ("workingDirectory" in server) {
42
+ const { workingDirectory, ...rest } = server;
43
+ return [name, { ...rest, cwd: workingDirectory }];
44
+ }
45
+ return [name, server];
46
+ })
47
+ );
48
+ }
49
+ function toWireCustomAgents(agents) {
50
+ if (!agents) return void 0;
51
+ return agents.map((agent) => {
52
+ if (!agent.mcpServers) return agent;
53
+ const { mcpServers, ...rest } = agent;
54
+ return { ...rest, mcpServers: toWireMcpServers(mcpServers) };
55
+ });
56
+ }
57
+ function toWireLargeOutput(config) {
58
+ if (!config) return void 0;
59
+ const { outputDirectory, ...rest } = config;
60
+ const wire = { ...rest };
61
+ if (outputDirectory !== void 0) {
62
+ wire.outputDir = outputDirectory;
63
+ }
64
+ return wire;
65
+ }
66
+ function toolFilterListToArray(value) {
67
+ if (value === void 0) {
68
+ return void 0;
69
+ }
70
+ return value instanceof ToolSet ? value.toArray() : value;
71
+ }
72
+ function validateToolFilterList(field, list) {
73
+ if (!list) {
74
+ return;
75
+ }
76
+ for (const entry of list) {
77
+ if (entry === "*") {
78
+ throw new Error(
79
+ `Invalid ${field} entry '*': there is no bare wildcard. Use one or more of \`new ToolSet().addBuiltIn('*')\`, \`.addMcp('*')\`, or \`.addCustom('*')\` to target a specific source.`
80
+ );
81
+ }
82
+ }
83
+ }
36
84
  function extractTransformCallbacks(systemMessage) {
37
85
  if (!systemMessage || systemMessage.mode !== "customize" || !systemMessage.sections) {
38
86
  return { wirePayload: systemMessage, transformCallbacks: void 0 };
@@ -86,12 +134,18 @@ class CopilotClient {
86
134
  cliProcess = null;
87
135
  connection = null;
88
136
  socket = null;
89
- actualPort = null;
137
+ runtimePort = null;
90
138
  actualHost = "localhost";
91
139
  state = "disconnected";
92
140
  sessions = /* @__PURE__ */ new Map();
93
141
  stderrBuffer = "";
94
142
  // Captures CLI stderr for error messages
143
+ /** Resolved connection mode chosen in the constructor. */
144
+ connectionConfig;
145
+ /** Resolved path to the runtime executable (only used for child-process kinds). */
146
+ resolvedCliPath;
147
+ /** Resolved environment passed to the spawned runtime. */
148
+ resolvedEnv;
95
149
  options;
96
150
  isExternalServer = false;
97
151
  forceStopping = false;
@@ -140,85 +194,90 @@ class CopilotClient {
140
194
  * Creates a new CopilotClient instance.
141
195
  *
142
196
  * @param options - Configuration options for the client
143
- * @throws Error if mutually exclusive options are provided (e.g., cliUrl with useStdio or cliPath)
144
197
  *
145
198
  * @example
146
199
  * ```typescript
147
- * // Default options - spawns CLI server using stdio
200
+ * // Default: spawns the bundled runtime over stdio
148
201
  * const client = new CopilotClient();
149
202
  *
150
- * // Connect to an existing server
151
- * const client = new CopilotClient({ cliUrl: "localhost:3000" });
203
+ * // Connect to an existing runtime
204
+ * const client = new CopilotClient({
205
+ * connection: RuntimeConnection.forUri("localhost:3000"),
206
+ * });
207
+ *
208
+ * // Spawn the runtime over TCP on a chosen port
209
+ * const client = new CopilotClient({
210
+ * connection: RuntimeConnection.forTcp({ port: 9001 }),
211
+ * });
152
212
  *
153
- * // Custom CLI path with specific log level
213
+ * // Use a custom runtime binary
154
214
  * const client = new CopilotClient({
155
- * cliPath: "/usr/local/bin/copilot",
156
- * logLevel: "debug"
215
+ * connection: RuntimeConnection.forStdio({ path: "/usr/local/bin/copilot" }),
216
+ * logLevel: "debug",
157
217
  * });
158
218
  * ```
159
219
  */
160
220
  constructor(options = {}) {
161
- if (options.cliUrl && (options.useStdio === true || options.cliPath)) {
162
- throw new Error("cliUrl is mutually exclusive with useStdio and cliPath");
163
- }
164
- if (options.isChildProcess && (options.cliUrl || options.useStdio === false)) {
165
- throw new Error(
166
- "isChildProcess must be used in conjunction with useStdio and not with cliUrl"
167
- );
168
- }
169
- if (options.cliUrl && (options.gitHubToken || options.useLoggedInUser !== void 0)) {
221
+ const conn = options._internalConnection ?? options.connection ?? { kind: "stdio" };
222
+ if (conn.kind === "uri" && (options.gitHubToken !== void 0 || options.useLoggedInUser !== void 0)) {
170
223
  throw new Error(
171
- "gitHubToken and useLoggedInUser cannot be used with cliUrl (external server manages its own auth)"
224
+ "gitHubToken and useLoggedInUser cannot be used with RuntimeConnection.forUri (external server manages its own auth)"
172
225
  );
173
226
  }
174
- if (options.tcpConnectionToken !== void 0) {
175
- if (typeof options.tcpConnectionToken !== "string" || options.tcpConnectionToken.length === 0) {
176
- throw new Error("tcpConnectionToken must be a non-empty string");
177
- }
178
- if (options.useStdio === true) {
179
- throw new Error("tcpConnectionToken cannot be used with useStdio: true");
227
+ if (conn.kind === "tcp" && conn.connectionToken !== void 0) {
228
+ if (typeof conn.connectionToken !== "string" || conn.connectionToken.length === 0) {
229
+ throw new Error("connectionToken must be a non-empty string");
180
230
  }
181
231
  }
182
- const willUseStdio = options.cliUrl ? false : options.useStdio ?? true;
183
- const sdkSpawnsCli = !willUseStdio && !options.cliUrl && !options.isChildProcess;
184
- this.effectiveConnectionToken = options.tcpConnectionToken ?? (sdkSpawnsCli ? randomUUID() : void 0);
232
+ this.connectionConfig = conn;
185
233
  if (options.sessionFs) {
186
234
  this.validateSessionFsConfig(options.sessionFs);
187
235
  }
188
- if (options.cliUrl) {
189
- const { host, port } = this.parseCliUrl(options.cliUrl);
236
+ if (conn.kind === "uri") {
237
+ const { host, port } = this.parseCliUrl(conn.url);
190
238
  this.actualHost = host;
191
- this.actualPort = port;
239
+ this.runtimePort = port;
192
240
  this.isExternalServer = true;
193
- }
194
- if (options.isChildProcess) {
241
+ } else if (conn.kind === "parent-process") {
195
242
  this.isExternalServer = true;
196
243
  }
244
+ if (conn.kind === "tcp") {
245
+ this.effectiveConnectionToken = conn.connectionToken ?? randomUUID();
246
+ } else if (conn.kind === "uri") {
247
+ this.effectiveConnectionToken = conn.connectionToken;
248
+ }
197
249
  this.onListModels = options.onListModels;
198
250
  this.onGetTraceContext = options.onGetTraceContext;
199
251
  this.sessionFsConfig = options.sessionFs ?? null;
200
252
  const effectiveEnv = options.env ?? process.env;
253
+ this.resolvedEnv = effectiveEnv;
254
+ this.resolvedCliPath = conn.kind === "stdio" || conn.kind === "tcp" ? conn.path ?? effectiveEnv.COPILOT_CLI_PATH ?? getBundledCliPath() : void 0;
255
+ const connArgs = conn.kind === "stdio" || conn.kind === "tcp" ? conn.args ?? [] : [];
256
+ this.connectionExtraArgs = [...connArgs];
201
257
  this.options = {
202
- cliPath: options.cliUrl ? void 0 : options.cliPath || effectiveEnv.COPILOT_CLI_PATH || getBundledCliPath(),
203
- cliArgs: options.cliArgs ?? [],
204
- cwd: options.cwd ?? process.cwd(),
205
- port: options.port || 0,
206
- useStdio: options.cliUrl ? false : options.useStdio ?? true,
207
- // Default to stdio unless cliUrl is provided
208
- isChildProcess: options.isChildProcess ?? false,
209
- cliUrl: options.cliUrl,
210
- logLevel: options.logLevel || "debug",
211
- autoStart: options.autoStart ?? true,
212
- autoRestart: false,
213
- env: effectiveEnv,
258
+ workingDirectory: options.workingDirectory ?? process.cwd(),
259
+ logLevel: options.logLevel,
214
260
  gitHubToken: options.gitHubToken,
215
- // Default useLoggedInUser to false when gitHubToken is provided, otherwise true
261
+ // Default useLoggedInUser to false when gitHubToken is provided, otherwise true.
216
262
  useLoggedInUser: options.useLoggedInUser ?? (options.gitHubToken ? false : true),
217
263
  telemetry: options.telemetry,
218
- copilotHome: options.copilotHome,
219
- sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0
264
+ baseDirectory: options.baseDirectory,
265
+ sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0,
266
+ enableRemoteSessions: options.enableRemoteSessions ?? false,
267
+ mode: options.mode ?? "copilot-cli"
220
268
  };
269
+ if (this.options.mode === "empty") {
270
+ const hasPersistence = this.options.baseDirectory !== void 0 || this.sessionFsConfig !== null || // External runtimes manage their own persistence layer; the SDK
271
+ // can't enforce it from here.
272
+ conn.kind === "uri" || conn.kind === "parent-process";
273
+ if (!hasPersistence) {
274
+ throw new Error(
275
+ "CopilotClient was created with mode: 'empty' but neither 'baseDirectory' nor 'sessionFs' was set. Empty mode requires an explicit per-session persistence location; pick one."
276
+ );
277
+ }
278
+ }
221
279
  }
280
+ connectionExtraArgs = [];
222
281
  /**
223
282
  * Parse CLI URL into host and port
224
283
  * Supports formats: "host:port", "http://host:port", "https://host:port", or just "port"
@@ -252,20 +311,37 @@ class CopilotClient {
252
311
  throw new Error("sessionFs.conventions must be either 'windows' or 'posix'");
253
312
  }
254
313
  }
314
+ setupSessionFs(session, config) {
315
+ if (!this.sessionFsConfig) {
316
+ return;
317
+ }
318
+ if (!config.createSessionFsProvider) {
319
+ throw new Error(
320
+ "createSessionFsProvider is required in session config when sessionFs is enabled in client options."
321
+ );
322
+ }
323
+ const provider = config.createSessionFsProvider(session);
324
+ if (this.sessionFsConfig.capabilities?.sqlite && !provider.sqlite) {
325
+ throw new Error(
326
+ "SessionFsConfig declares capabilities.sqlite but the provider does not implement sqlite."
327
+ );
328
+ }
329
+ session.clientSessionApis.sessionFs = createSessionFsAdapter(provider);
330
+ }
255
331
  /**
256
332
  * Starts the CLI server and establishes a connection.
257
333
  *
258
334
  * If connecting to an external server (via cliUrl), only establishes the connection.
259
335
  * Otherwise, spawns the CLI server process and then connects.
260
336
  *
261
- * This method is called automatically when creating a session if `autoStart` is true (default).
337
+ * This method is called automatically the first time you create or resume a session.
262
338
  *
263
339
  * @returns A promise that resolves when the connection is established
264
340
  * @throws Error if the server fails to start or the connection fails
265
341
  *
266
342
  * @example
267
343
  * ```typescript
268
- * const client = new CopilotClient({ autoStart: false });
344
+ * const client = new CopilotClient();
269
345
  * await client.start();
270
346
  * // Now ready to create sessions
271
347
  * ```
@@ -285,7 +361,8 @@ class CopilotClient {
285
361
  await this.connection.sendRequest("sessionFs.setProvider", {
286
362
  initialCwd: this.sessionFsConfig.initialCwd,
287
363
  sessionStatePath: this.sessionFsConfig.sessionStatePath,
288
- conventions: this.sessionFsConfig.conventions
364
+ conventions: this.sessionFsConfig.conventions,
365
+ capabilities: this.sessionFsConfig.capabilities
289
366
  });
290
367
  }
291
368
  this.state = "connected";
@@ -359,8 +436,15 @@ class CopilotClient {
359
436
  }
360
437
  this.modelsCache = null;
361
438
  if (this.socket) {
439
+ const socket = this.socket;
440
+ this.socket = null;
362
441
  try {
363
- this.socket.end();
442
+ if (!socket.destroyed) {
443
+ await new Promise((resolve) => {
444
+ socket.once("close", () => resolve());
445
+ socket.end();
446
+ });
447
+ }
364
448
  } catch (error) {
365
449
  errors.push(
366
450
  new Error(
@@ -368,11 +452,18 @@ class CopilotClient {
368
452
  )
369
453
  );
370
454
  }
371
- this.socket = null;
372
455
  }
373
456
  if (this.cliProcess && !this.isExternalServer) {
457
+ const child = this.cliProcess;
458
+ this.cliProcess = null;
374
459
  try {
375
- this.cliProcess.kill();
460
+ if (child.exitCode === null && child.signalCode === null) {
461
+ const exited = new Promise((resolve) => {
462
+ child.once("exit", () => resolve());
463
+ });
464
+ child.kill();
465
+ await exited;
466
+ }
376
467
  } catch (error) {
377
468
  errors.push(
378
469
  new Error(
@@ -380,18 +471,32 @@ class CopilotClient {
380
471
  )
381
472
  );
382
473
  }
383
- this.cliProcess = null;
384
474
  }
385
475
  if (this.cliStartTimeout) {
386
476
  clearTimeout(this.cliStartTimeout);
387
477
  this.cliStartTimeout = null;
388
478
  }
389
479
  this.state = "disconnected";
390
- this.actualPort = null;
480
+ this.runtimePort = null;
391
481
  this.stderrBuffer = "";
392
482
  this.processExitPromise = null;
393
483
  return errors;
394
484
  }
485
+ /**
486
+ * Alias for {@link stop} that lets `CopilotClient` participate in `await using`
487
+ * blocks for automatic cleanup.
488
+ *
489
+ * @example
490
+ * ```typescript
491
+ * await using client = new CopilotClient();
492
+ * const session = await client.createSession({ onPermissionRequest: approveAll });
493
+ * await session.sendAndWait("Hello");
494
+ * // client.stop() is called automatically when the block exits.
495
+ * ```
496
+ */
497
+ async [Symbol.asyncDispose]() {
498
+ await this.stop();
499
+ }
395
500
  /**
396
501
  * Forcefully stops the CLI server without graceful cleanup.
397
502
  *
@@ -448,7 +553,7 @@ class CopilotClient {
448
553
  this.cliStartTimeout = null;
449
554
  }
450
555
  this.state = "disconnected";
451
- this.actualPort = null;
556
+ this.runtimePort = null;
452
557
  this.stderrBuffer = "";
453
558
  this.processExitPromise = null;
454
559
  }
@@ -456,12 +561,11 @@ class CopilotClient {
456
561
  * Creates a new conversation session with the Copilot CLI.
457
562
  *
458
563
  * Sessions maintain conversation state, handle events, and manage tool execution.
459
- * If the client is not connected and `autoStart` is enabled, this will automatically
460
- * start the connection.
564
+ * If the client is not connected, this method automatically starts the connection.
461
565
  *
462
566
  * @param config - Optional configuration for the session
463
567
  * @returns A promise that resolves with the created session
464
- * @throws Error if the client is not connected and autoStart is disabled
568
+ * @throws Error if the client fails to start
465
569
  *
466
570
  * @example
467
571
  * ```typescript
@@ -481,66 +585,190 @@ class CopilotClient {
481
585
  * });
482
586
  * ```
483
587
  */
484
- async createSession(config) {
485
- if (!config?.onPermissionRequest) {
486
- throw new Error(
487
- "An onPermissionRequest handler is required when creating a session. For example, to allow all permissions, use { onPermissionRequest: approveAll }."
488
- );
489
- }
490
- if (!this.connection) {
491
- if (this.options.autoStart) {
492
- await this.start();
493
- } else {
494
- throw new Error("Client not connected. Call start() first.");
588
+ /**
589
+ * Normalizes session-level tool filter options. Converts {@link ToolSet}
590
+ * instances to plain string arrays, rejects misuse (bare `"*"`) and the
591
+ * missing-availableTools case in `mode = "empty"`.
592
+ *
593
+ * The SDK always sends `toolFilterPrecedence: "excluded"` so callers can
594
+ * compose include + exclude lists naturally (e.g. "everything matching X
595
+ * except Y") regardless of mode. Allowlist-precedence is intentionally not
596
+ * exposed — it's available on the runtime side as a CLI-only concession to
597
+ * legacy behavior, but SDK consumers always get the composable semantics.
598
+ *
599
+ * @internal
600
+ */
601
+ resolveToolFilterOptions(config) {
602
+ const availableTools = toolFilterListToArray(config.availableTools);
603
+ const excludedTools = toolFilterListToArray(config.excludedTools);
604
+ validateToolFilterList("availableTools", availableTools);
605
+ validateToolFilterList("excludedTools", excludedTools);
606
+ if (this.options.mode === "empty") {
607
+ if (availableTools === void 0) {
608
+ throw new Error(
609
+ "CopilotClient is in mode: 'empty' but the session config did not specify 'availableTools'. Empty mode requires every session to explicitly opt into the tools it wants \u2014 e.g. `new ToolSet().addBuiltIn(BuiltInTools.Isolated)`."
610
+ );
495
611
  }
496
612
  }
497
- const sessionId = config.sessionId ?? randomUUID();
498
- const session = new CopilotSession(
499
- sessionId,
500
- this.connection,
501
- void 0,
502
- this.onGetTraceContext
503
- );
504
- session.registerTools(config.tools);
505
- session.registerCommands(config.commands);
506
- session.registerPermissionHandler(config.onPermissionRequest);
507
- if (config.onUserInputRequest) {
508
- session.registerUserInputHandler(config.onUserInputRequest);
613
+ return { availableTools, excludedTools, toolFilterPrecedence: "excluded" };
614
+ }
615
+ /** Mode-specific defaults spread under the caller's config (app values win). */
616
+ configDefaultsForMode() {
617
+ if (this.options.mode === "empty") {
618
+ return {
619
+ enableSessionTelemetry: false,
620
+ mcpOAuthTokenStorage: "in-memory",
621
+ skipEmbeddingRetrieval: true,
622
+ embeddingCacheStorage: "in-memory",
623
+ enableOnDemandInstructionDiscovery: false,
624
+ enableFileHooks: false,
625
+ enableHostGitOperations: false,
626
+ enableSessionStore: false,
627
+ enableSkills: false
628
+ };
509
629
  }
510
- if (config.onElicitationRequest) {
511
- session.registerElicitationHandler(config.onElicitationRequest);
630
+ return {};
631
+ }
632
+ /**
633
+ * Returns the systemMessage config to use, adjusted for the current mode.
634
+ * In empty mode we ensure the environment_context section is removed
635
+ * unless the app has already taken control of it. `append` (and
636
+ * unspecified) mode is promoted to `customize` so we can also strip
637
+ * environment_context; the caller's `content` is preserved verbatim
638
+ * because the runtime appends it as additional instructions in both
639
+ * customize and append modes.
640
+ */
641
+ getSystemMessageConfigForMode(supplied) {
642
+ if (this.options.mode !== "empty") return supplied;
643
+ if (!supplied) {
644
+ return {
645
+ mode: "customize",
646
+ sections: { environment_context: { action: "remove" } }
647
+ };
512
648
  }
513
- if (config.hooks) {
514
- session.registerHooks(config.hooks);
649
+ switch (supplied.mode) {
650
+ case "replace":
651
+ return supplied;
652
+ case "customize":
653
+ if (supplied.sections?.environment_context) return supplied;
654
+ return {
655
+ ...supplied,
656
+ sections: {
657
+ ...supplied.sections,
658
+ environment_context: { action: "remove" }
659
+ }
660
+ };
661
+ case "append":
662
+ case void 0:
663
+ return {
664
+ mode: "customize",
665
+ content: supplied.content,
666
+ sections: { environment_context: { action: "remove" } }
667
+ };
668
+ }
669
+ }
670
+ /**
671
+ * Mode-specific options applied via session.options.update after create/resume.
672
+ *
673
+ * In empty mode, defaults the four overridable feature flags to safe values
674
+ * (caller values from `config` win). `installedPlugins=[]` is unconditional
675
+ * in empty mode — apps that need custom plugins should switch modes.
676
+ */
677
+ async updateSessionOptionsForMode(session, config) {
678
+ const patch = {};
679
+ if (this.options.mode === "empty") {
680
+ patch.skipCustomInstructions = config.skipCustomInstructions ?? true;
681
+ patch.customAgentsLocalOnly = config.customAgentsLocalOnly ?? true;
682
+ patch.coauthorEnabled = config.coauthorEnabled ?? false;
683
+ patch.manageScheduleEnabled = config.manageScheduleEnabled ?? false;
684
+ patch.installedPlugins = [];
685
+ } else {
686
+ if (config.skipCustomInstructions !== void 0)
687
+ patch.skipCustomInstructions = config.skipCustomInstructions;
688
+ if (config.customAgentsLocalOnly !== void 0)
689
+ patch.customAgentsLocalOnly = config.customAgentsLocalOnly;
690
+ if (config.coauthorEnabled !== void 0)
691
+ patch.coauthorEnabled = config.coauthorEnabled;
692
+ if (config.manageScheduleEnabled !== void 0)
693
+ patch.manageScheduleEnabled = config.manageScheduleEnabled;
694
+ }
695
+ if (Object.keys(patch).length === 0) {
696
+ return;
697
+ }
698
+ try {
699
+ await session.rpc.options.update(patch);
700
+ } catch (e) {
701
+ try {
702
+ await session.disconnect();
703
+ } catch {
704
+ }
705
+ throw e;
706
+ }
707
+ }
708
+ async createSession(config) {
709
+ if (!this.connection) {
710
+ await this.start();
515
711
  }
712
+ config = { ...this.configDefaultsForMode(), ...config };
713
+ config.systemMessage = this.getSystemMessageConfigForMode(config.systemMessage);
714
+ const callerSessionId = config.sessionId;
715
+ const useServerGeneratedId = config.cloud != null && callerSessionId == null;
716
+ const localSessionId = useServerGeneratedId ? void 0 : callerSessionId ?? randomUUID();
516
717
  const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
517
718
  config.systemMessage
518
719
  );
519
- if (transformCallbacks) {
520
- session.registerTransformCallbacks(transformCallbacks);
521
- }
522
- if (config.onEvent) {
523
- session.on(config.onEvent);
524
- }
525
- this.sessions.set(sessionId, session);
526
- if (this.sessionFsConfig) {
527
- if (config.createSessionFsHandler) {
528
- session.clientSessionApis.sessionFs = createSessionFsAdapter(
529
- config.createSessionFsHandler(session)
530
- );
531
- } else {
532
- throw new Error(
533
- "createSessionFsHandler is required in session config when sessionFs is enabled in client options."
534
- );
720
+ const initializeSession = (sessionId) => {
721
+ const s = new CopilotSession(
722
+ sessionId,
723
+ this.connection,
724
+ void 0,
725
+ this.onGetTraceContext
726
+ );
727
+ s.registerTools(config.tools);
728
+ s.registerCanvases(config.canvases);
729
+ s.registerCommands(config.commands);
730
+ s.registerPermissionHandler(config.onPermissionRequest);
731
+ if (config.onUserInputRequest) {
732
+ s.registerUserInputHandler(config.onUserInputRequest);
733
+ }
734
+ if (config.onElicitationRequest) {
735
+ s.registerElicitationHandler(config.onElicitationRequest);
736
+ }
737
+ if (config.onExitPlanModeRequest) {
738
+ s.registerExitPlanModeHandler(config.onExitPlanModeRequest);
739
+ }
740
+ if (config.onAutoModeSwitchRequest) {
741
+ s.registerAutoModeSwitchHandler(config.onAutoModeSwitchRequest);
742
+ }
743
+ if (config.hooks) {
744
+ s.registerHooks(config.hooks);
745
+ }
746
+ if (transformCallbacks) {
747
+ s.registerTransformCallbacks(transformCallbacks);
748
+ }
749
+ if (config.onEvent) {
750
+ s.on(config.onEvent);
535
751
  }
752
+ this.sessions.set(sessionId, s);
753
+ this.setupSessionFs(s, config);
754
+ return s;
755
+ };
756
+ let session;
757
+ let registeredId;
758
+ if (localSessionId !== void 0) {
759
+ session = initializeSession(localSessionId);
760
+ registeredId = localSessionId;
536
761
  }
762
+ const toolFilterOptions = this.resolveToolFilterOptions(config);
537
763
  try {
538
764
  const response = await this.connection.sendRequest("session.create", {
539
765
  ...await getTraceContext(this.onGetTraceContext),
540
766
  model: config.model,
541
- sessionId,
767
+ sessionId: localSessionId,
542
768
  clientName: config.clientName,
543
769
  reasoningEffort: config.reasoningEffort,
770
+ reasoningSummary: config.reasoningSummary,
771
+ contextTier: config.contextTier,
544
772
  tools: config.tools?.map((tool) => ({
545
773
  name: tool.name,
546
774
  description: tool.description,
@@ -548,40 +776,82 @@ class CopilotClient {
548
776
  overridesBuiltInTool: tool.overridesBuiltInTool,
549
777
  skipPermission: tool.skipPermission
550
778
  })),
779
+ canvases: config.canvases?.map((canvas) => canvas.declaration),
780
+ requestCanvasRenderer: config.requestCanvasRenderer,
781
+ requestExtensions: config.requestExtensions,
782
+ extensionSdkPath: config.extensionSdkPath,
783
+ extensionInfo: config.extensionInfo,
551
784
  commands: config.commands?.map((cmd) => ({
552
785
  name: cmd.name,
553
786
  description: cmd.description
554
787
  })),
555
788
  systemMessage: wireSystemMessage,
556
- availableTools: config.availableTools,
557
- excludedTools: config.excludedTools,
789
+ availableTools: toolFilterOptions.availableTools,
790
+ excludedTools: toolFilterOptions.excludedTools,
791
+ toolFilterPrecedence: toolFilterOptions.toolFilterPrecedence,
558
792
  provider: config.provider,
793
+ enableSessionTelemetry: config.enableSessionTelemetry,
559
794
  modelCapabilities: config.modelCapabilities,
560
- requestPermission: true,
795
+ largeOutput: toWireLargeOutput(config.largeOutput),
796
+ requestPermission: !!config.onPermissionRequest,
561
797
  requestUserInput: !!config.onUserInputRequest,
562
798
  requestElicitation: !!config.onElicitationRequest,
799
+ ...config.enableMcpApps ? { requestMcpApps: true } : {},
800
+ requestExitPlanMode: !!config.onExitPlanModeRequest,
801
+ requestAutoModeSwitch: !!config.onAutoModeSwitchRequest,
563
802
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
564
803
  workingDirectory: config.workingDirectory,
565
804
  streaming: config.streaming,
566
805
  includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
567
- mcpServers: config.mcpServers,
806
+ mcpServers: toWireMcpServers(config.mcpServers),
807
+ mcpOAuthTokenStorage: config.mcpOAuthTokenStorage,
568
808
  envValueMode: "direct",
569
- customAgents: config.customAgents,
809
+ customAgents: toWireCustomAgents(config.customAgents),
570
810
  defaultAgent: config.defaultAgent,
571
811
  agent: config.agent,
572
- configDir: config.configDir,
812
+ configDir: config.configDirectory,
573
813
  enableConfigDiscovery: config.enableConfigDiscovery,
814
+ skipEmbeddingRetrieval: config.skipEmbeddingRetrieval,
815
+ embeddingCacheStorage: config.embeddingCacheStorage,
816
+ organizationCustomInstructions: config.organizationCustomInstructions,
817
+ enableOnDemandInstructionDiscovery: config.enableOnDemandInstructionDiscovery,
818
+ enableFileHooks: config.enableFileHooks,
819
+ enableHostGitOperations: config.enableHostGitOperations,
820
+ enableSessionStore: config.enableSessionStore,
821
+ enableSkills: config.enableSkills,
574
822
  skillDirectories: config.skillDirectories,
823
+ pluginDirectories: config.pluginDirectories,
575
824
  instructionDirectories: config.instructionDirectories,
576
825
  disabledSkills: config.disabledSkills,
577
826
  infiniteSessions: config.infiniteSessions,
578
- gitHubToken: config.gitHubToken
827
+ gitHubToken: config.gitHubToken,
828
+ remoteSession: config.remoteSession,
829
+ cloud: config.cloud
579
830
  });
580
- const { workspacePath, capabilities } = response;
831
+ const {
832
+ sessionId: returnedSessionId,
833
+ workspacePath,
834
+ capabilities
835
+ } = response;
836
+ if (!returnedSessionId) {
837
+ throw new Error("session.create response did not include a sessionId");
838
+ }
839
+ if (localSessionId !== void 0 && localSessionId !== returnedSessionId) {
840
+ throw new Error(
841
+ `session.create returned sessionId ${returnedSessionId} but the caller requested ${localSessionId}`
842
+ );
843
+ }
844
+ if (session === void 0) {
845
+ session = initializeSession(returnedSessionId);
846
+ registeredId = returnedSessionId;
847
+ }
581
848
  session["_workspacePath"] = workspacePath;
582
849
  session.setCapabilities(capabilities);
850
+ await this.updateSessionOptionsForMode(session, config);
583
851
  } catch (e) {
584
- this.sessions.delete(sessionId);
852
+ if (registeredId !== void 0) {
853
+ this.sessions.delete(registeredId);
854
+ }
585
855
  throw e;
586
856
  }
587
857
  return session;
@@ -611,17 +881,8 @@ class CopilotClient {
611
881
  * ```
612
882
  */
613
883
  async resumeSession(sessionId, config) {
614
- if (!config?.onPermissionRequest) {
615
- throw new Error(
616
- "An onPermissionRequest handler is required when resuming a session. For example, to allow all permissions, use { onPermissionRequest: approveAll }."
617
- );
618
- }
619
884
  if (!this.connection) {
620
- if (this.options.autoStart) {
621
- await this.start();
622
- } else {
623
- throw new Error("Client not connected. Call start() first.");
624
- }
885
+ await this.start();
625
886
  }
626
887
  const session = new CopilotSession(
627
888
  sessionId,
@@ -630,6 +891,7 @@ class CopilotClient {
630
891
  this.onGetTraceContext
631
892
  );
632
893
  session.registerTools(config.tools);
894
+ session.registerCanvases(config.canvases);
633
895
  session.registerCommands(config.commands);
634
896
  session.registerPermissionHandler(config.onPermissionRequest);
635
897
  if (config.onUserInputRequest) {
@@ -638,9 +900,17 @@ class CopilotClient {
638
900
  if (config.onElicitationRequest) {
639
901
  session.registerElicitationHandler(config.onElicitationRequest);
640
902
  }
903
+ if (config.onExitPlanModeRequest) {
904
+ session.registerExitPlanModeHandler(config.onExitPlanModeRequest);
905
+ }
906
+ if (config.onAutoModeSwitchRequest) {
907
+ session.registerAutoModeSwitchHandler(config.onAutoModeSwitchRequest);
908
+ }
641
909
  if (config.hooks) {
642
910
  session.registerHooks(config.hooks);
643
911
  }
912
+ config = { ...this.configDefaultsForMode(), ...config };
913
+ config.systemMessage = this.getSystemMessageConfigForMode(config.systemMessage);
644
914
  const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
645
915
  config.systemMessage
646
916
  );
@@ -651,17 +921,8 @@ class CopilotClient {
651
921
  session.on(config.onEvent);
652
922
  }
653
923
  this.sessions.set(sessionId, session);
654
- if (this.sessionFsConfig) {
655
- if (config.createSessionFsHandler) {
656
- session.clientSessionApis.sessionFs = createSessionFsAdapter(
657
- config.createSessionFsHandler(session)
658
- );
659
- } else {
660
- throw new Error(
661
- "createSessionFsHandler is required in session config when sessionFs is enabled in client options."
662
- );
663
- }
664
- }
924
+ this.setupSessionFs(session, config);
925
+ const toolFilterOptions = this.resolveToolFilterOptions(config);
665
926
  try {
666
927
  const response = await this.connection.sendRequest("session.resume", {
667
928
  ...await getTraceContext(this.onGetTraceContext),
@@ -669,9 +930,13 @@ class CopilotClient {
669
930
  clientName: config.clientName,
670
931
  model: config.model,
671
932
  reasoningEffort: config.reasoningEffort,
933
+ reasoningSummary: config.reasoningSummary,
934
+ contextTier: config.contextTier,
672
935
  systemMessage: wireSystemMessage,
673
- availableTools: config.availableTools,
674
- excludedTools: config.excludedTools,
936
+ availableTools: toolFilterOptions.availableTools,
937
+ excludedTools: toolFilterOptions.excludedTools,
938
+ toolFilterPrecedence: toolFilterOptions.toolFilterPrecedence,
939
+ enableSessionTelemetry: config.enableSessionTelemetry,
675
940
  tools: config.tools?.map((tool) => ({
676
941
  name: tool.name,
677
942
  description: tool.description,
@@ -679,58 +944,66 @@ class CopilotClient {
679
944
  overridesBuiltInTool: tool.overridesBuiltInTool,
680
945
  skipPermission: tool.skipPermission
681
946
  })),
947
+ canvases: config.canvases?.map((canvas) => canvas.declaration),
948
+ requestCanvasRenderer: config.requestCanvasRenderer,
949
+ requestExtensions: config.requestExtensions,
950
+ extensionSdkPath: config.extensionSdkPath,
951
+ extensionInfo: config.extensionInfo,
682
952
  commands: config.commands?.map((cmd) => ({
683
953
  name: cmd.name,
684
954
  description: cmd.description
685
955
  })),
686
956
  provider: config.provider,
687
957
  modelCapabilities: config.modelCapabilities,
958
+ largeOutput: toWireLargeOutput(config.largeOutput),
688
959
  requestPermission: config.onPermissionRequest !== defaultJoinSessionPermissionHandler,
689
960
  requestUserInput: !!config.onUserInputRequest,
690
961
  requestElicitation: !!config.onElicitationRequest,
962
+ ...config.enableMcpApps ? { requestMcpApps: true } : {},
963
+ requestExitPlanMode: !!config.onExitPlanModeRequest,
964
+ requestAutoModeSwitch: !!config.onAutoModeSwitchRequest,
691
965
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
692
966
  workingDirectory: config.workingDirectory,
693
- configDir: config.configDir,
967
+ configDir: config.configDirectory,
694
968
  enableConfigDiscovery: config.enableConfigDiscovery,
969
+ skipEmbeddingRetrieval: config.skipEmbeddingRetrieval,
970
+ embeddingCacheStorage: config.embeddingCacheStorage,
971
+ organizationCustomInstructions: config.organizationCustomInstructions,
972
+ enableOnDemandInstructionDiscovery: config.enableOnDemandInstructionDiscovery,
973
+ enableFileHooks: config.enableFileHooks,
974
+ enableHostGitOperations: config.enableHostGitOperations,
975
+ enableSessionStore: config.enableSessionStore,
976
+ enableSkills: config.enableSkills,
695
977
  streaming: config.streaming,
696
978
  includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
697
- mcpServers: config.mcpServers,
979
+ mcpServers: toWireMcpServers(config.mcpServers),
980
+ mcpOAuthTokenStorage: config.mcpOAuthTokenStorage,
698
981
  envValueMode: "direct",
699
- customAgents: config.customAgents,
982
+ customAgents: toWireCustomAgents(config.customAgents),
700
983
  defaultAgent: config.defaultAgent,
701
984
  agent: config.agent,
702
985
  skillDirectories: config.skillDirectories,
986
+ pluginDirectories: config.pluginDirectories,
703
987
  instructionDirectories: config.instructionDirectories,
704
988
  disabledSkills: config.disabledSkills,
705
989
  infiniteSessions: config.infiniteSessions,
706
- disableResume: config.disableResume,
990
+ disableResume: config.suppressResumeEvent,
707
991
  continuePendingWork: config.continuePendingWork,
708
- gitHubToken: config.gitHubToken
992
+ gitHubToken: config.gitHubToken,
993
+ remoteSession: config.remoteSession,
994
+ openCanvases: config.openCanvases
709
995
  });
710
- const { workspacePath, capabilities } = response;
996
+ const { workspacePath, capabilities, openCanvases } = response;
711
997
  session["_workspacePath"] = workspacePath;
712
998
  session.setCapabilities(capabilities);
999
+ session.setOpenCanvases(openCanvases ?? []);
1000
+ await this.updateSessionOptionsForMode(session, config);
713
1001
  } catch (e) {
714
1002
  this.sessions.delete(sessionId);
715
1003
  throw e;
716
1004
  }
717
1005
  return session;
718
1006
  }
719
- /**
720
- * Gets the current connection state of the client.
721
- *
722
- * @returns The current connection state: "disconnected", "connecting", "connected", or "error"
723
- *
724
- * @example
725
- * ```typescript
726
- * if (client.getState() === "connected") {
727
- * const session = await client.createSession({ onPermissionRequest: approveAll });
728
- * }
729
- * ```
730
- */
731
- getState() {
732
- return this.state;
733
- }
734
1007
  /**
735
1008
  * Sends a ping request to the server to verify connectivity.
736
1009
  *
@@ -843,7 +1116,7 @@ class CopilotClient {
843
1116
  );
844
1117
  serverVersion = result.protocolVersion;
845
1118
  } catch (err) {
846
- if (err instanceof ResponseError && err.code === ErrorCodes.MethodNotFound) {
1119
+ if (err instanceof ResponseError && (err.code === ErrorCodes.MethodNotFound || err.message === "Unhandled method connect")) {
847
1120
  serverVersion = (await raceAgainstExit(this.ping())).protocolVersion;
848
1121
  } else {
849
1122
  throw err;
@@ -932,8 +1205,13 @@ class CopilotClient {
932
1205
  if (!this.connection) {
933
1206
  throw new Error("Client not connected");
934
1207
  }
1208
+ let wireFilter;
1209
+ if (filter) {
1210
+ const { workingDirectory, ...rest } = filter;
1211
+ wireFilter = { ...rest, cwd: workingDirectory };
1212
+ }
935
1213
  const response = await this.connection.sendRequest("session.list", {
936
- filter
1214
+ filter: wireFilter
937
1215
  });
938
1216
  const { sessions } = response;
939
1217
  return sessions.map(CopilotClient.toSessionMetadata);
@@ -968,13 +1246,19 @@ class CopilotClient {
968
1246
  return CopilotClient.toSessionMetadata(session);
969
1247
  }
970
1248
  static toSessionMetadata(raw) {
1249
+ const { context } = raw;
971
1250
  return {
972
1251
  sessionId: raw.sessionId,
973
1252
  startTime: new Date(raw.startTime),
974
1253
  modifiedTime: new Date(raw.modifiedTime),
975
1254
  summary: raw.summary,
976
1255
  isRemote: raw.isRemote,
977
- context: raw.context
1256
+ context: context ? {
1257
+ workingDirectory: context.cwd,
1258
+ gitRoot: context.gitRoot,
1259
+ repository: context.repository,
1260
+ branch: context.branch
1261
+ } : void 0
978
1262
  };
979
1263
  }
980
1264
  /**
@@ -1027,7 +1311,7 @@ class CopilotClient {
1027
1311
  throw new Error(result.error || "Failed to set foreground session");
1028
1312
  }
1029
1313
  }
1030
- on(eventTypeOrHandler, handler) {
1314
+ onLifecycle(eventTypeOrHandler, handler) {
1031
1315
  if (typeof eventTypeOrHandler === "string" && handler) {
1032
1316
  const eventType = eventTypeOrHandler;
1033
1317
  if (!this.typedLifecycleHandlers.has(eventType)) {
@@ -1054,17 +1338,17 @@ class CopilotClient {
1054
1338
  async startCLIServer() {
1055
1339
  return new Promise((resolve, reject) => {
1056
1340
  this.stderrBuffer = "";
1057
- const args = [
1058
- ...this.options.cliArgs,
1059
- "--headless",
1060
- "--no-auto-update",
1061
- "--log-level",
1062
- this.options.logLevel
1063
- ];
1064
- if (this.options.useStdio) {
1341
+ const args = [...this.connectionExtraArgs, "--headless", "--no-auto-update"];
1342
+ if (this.options.logLevel) {
1343
+ args.push("--log-level", this.options.logLevel);
1344
+ }
1345
+ if (this.connectionConfig.kind === "stdio") {
1065
1346
  args.push("--stdio");
1066
- } else if (this.options.port > 0) {
1067
- args.push("--port", this.options.port.toString());
1347
+ } else if (this.connectionConfig.kind === "tcp") {
1348
+ const requestedPort = this.connectionConfig.port ?? 0;
1349
+ if (requestedPort > 0) {
1350
+ args.push("--port", requestedPort.toString());
1351
+ }
1068
1352
  }
1069
1353
  if (this.options.gitHubToken) {
1070
1354
  args.push("--auth-token-env", "COPILOT_SDK_AUTH_TOKEN");
@@ -1078,7 +1362,10 @@ class CopilotClient {
1078
1362
  this.options.sessionIdleTimeoutSeconds.toString()
1079
1363
  );
1080
1364
  }
1081
- const envWithoutNodeDebug = { ...this.options.env };
1365
+ if (this.options.enableRemoteSessions) {
1366
+ args.push("--remote");
1367
+ }
1368
+ const envWithoutNodeDebug = { ...this.resolvedEnv };
1082
1369
  delete envWithoutNodeDebug.NODE_DEBUG;
1083
1370
  if (this.options.gitHubToken) {
1084
1371
  envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.gitHubToken;
@@ -1086,12 +1373,15 @@ class CopilotClient {
1086
1373
  if (this.effectiveConnectionToken) {
1087
1374
  envWithoutNodeDebug.COPILOT_CONNECTION_TOKEN = this.effectiveConnectionToken;
1088
1375
  }
1089
- if (this.options.copilotHome) {
1090
- envWithoutNodeDebug.COPILOT_HOME = this.options.copilotHome;
1376
+ if (this.options.baseDirectory) {
1377
+ envWithoutNodeDebug.COPILOT_HOME = this.options.baseDirectory;
1091
1378
  }
1092
- if (!this.options.cliPath) {
1379
+ if (this.options.mode === "empty") {
1380
+ envWithoutNodeDebug.COPILOT_DISABLE_KEYTAR = "1";
1381
+ }
1382
+ if (!this.resolvedCliPath) {
1093
1383
  throw new Error(
1094
- "Path to Copilot CLI is required. Please provide it via the cliPath option, or use cliUrl to rely on a remote CLI."
1384
+ "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."
1095
1385
  );
1096
1386
  }
1097
1387
  if (this.options.telemetry) {
@@ -1110,31 +1400,31 @@ class CopilotClient {
1110
1400
  t.captureContent
1111
1401
  );
1112
1402
  }
1113
- if (!existsSync(this.options.cliPath)) {
1403
+ if (!existsSync(this.resolvedCliPath)) {
1114
1404
  throw new Error(
1115
- `Copilot CLI not found at ${this.options.cliPath}. Ensure @github/copilot is installed.`
1405
+ `Copilot CLI not found at ${this.resolvedCliPath}. Ensure @github/copilot is installed.`
1116
1406
  );
1117
1407
  }
1118
- const stdioConfig = this.options.useStdio ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
1119
- const isJsFile = this.options.cliPath.endsWith(".js");
1408
+ const stdioConfig = this.connectionConfig.kind === "stdio" ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
1409
+ const isJsFile = this.resolvedCliPath.endsWith(".js");
1120
1410
  if (isJsFile) {
1121
- this.cliProcess = spawn(getNodeExecPath(), [this.options.cliPath, ...args], {
1411
+ this.cliProcess = spawn(getNodeExecPath(), [this.resolvedCliPath, ...args], {
1122
1412
  stdio: stdioConfig,
1123
- cwd: this.options.cwd,
1413
+ cwd: this.options.workingDirectory,
1124
1414
  env: envWithoutNodeDebug,
1125
1415
  windowsHide: true
1126
1416
  });
1127
1417
  } else {
1128
- this.cliProcess = spawn(this.options.cliPath, args, {
1418
+ this.cliProcess = spawn(this.resolvedCliPath, args, {
1129
1419
  stdio: stdioConfig,
1130
- cwd: this.options.cwd,
1420
+ cwd: this.options.workingDirectory,
1131
1421
  env: envWithoutNodeDebug,
1132
1422
  windowsHide: true
1133
1423
  });
1134
1424
  }
1135
1425
  let stdout = "";
1136
1426
  let resolved = false;
1137
- if (this.options.useStdio) {
1427
+ if (this.connectionConfig.kind === "stdio") {
1138
1428
  resolved = true;
1139
1429
  resolve();
1140
1430
  } else {
@@ -1142,7 +1432,7 @@ class CopilotClient {
1142
1432
  stdout += data.toString();
1143
1433
  const match = stdout.match(/listening on port (\d+)/i);
1144
1434
  if (match && !resolved) {
1145
- this.actualPort = parseInt(match[1], 10);
1435
+ this.runtimePort = parseInt(match[1], 10);
1146
1436
  resolved = true;
1147
1437
  resolve();
1148
1438
  }
@@ -1216,19 +1506,21 @@ stderr: ${stderrOutput}`
1216
1506
  resolved = true;
1217
1507
  reject(new Error("Timeout waiting for CLI server to start"));
1218
1508
  }
1219
- }, 1e4);
1509
+ }, 3e4);
1220
1510
  });
1221
1511
  }
1222
1512
  /**
1223
1513
  * Connect to the CLI server (via socket or stdio)
1224
1514
  */
1225
1515
  async connectToServer() {
1226
- if (this.options.isChildProcess) {
1227
- return this.connectToParentProcessViaStdio();
1228
- } else if (this.options.useStdio) {
1229
- return this.connectToChildProcessViaStdio();
1230
- } else {
1231
- return this.connectViaTcp();
1516
+ switch (this.connectionConfig.kind) {
1517
+ case "parent-process":
1518
+ return this.connectToParentProcessViaStdio();
1519
+ case "stdio":
1520
+ return this.connectToChildProcessViaStdio();
1521
+ case "tcp":
1522
+ case "uri":
1523
+ return this.connectViaTcp();
1232
1524
  }
1233
1525
  }
1234
1526
  /**
@@ -1268,12 +1560,17 @@ stderr: ${stderrOutput}`
1268
1560
  * Connect to the CLI server via TCP socket
1269
1561
  */
1270
1562
  async connectViaTcp() {
1271
- if (!this.actualPort) {
1563
+ if (!this.runtimePort) {
1272
1564
  throw new Error("Server port not available");
1273
1565
  }
1274
1566
  return new Promise((resolve, reject) => {
1275
1567
  this.socket = new Socket();
1276
- this.socket.connect(this.actualPort, this.actualHost, () => {
1568
+ const connectionTimeout = setTimeout(() => {
1569
+ this.socket?.destroy();
1570
+ reject(new Error("Timeout connecting to CLI server"));
1571
+ }, 1e4);
1572
+ this.socket.connect(this.runtimePort, this.actualHost, () => {
1573
+ clearTimeout(connectionTimeout);
1277
1574
  this.connection = createMessageConnection(
1278
1575
  new StreamMessageReader(this.socket),
1279
1576
  new StreamMessageWriter(this.socket)
@@ -1283,6 +1580,7 @@ stderr: ${stderrOutput}`
1283
1580
  resolve();
1284
1581
  });
1285
1582
  this.socket.on("error", (error) => {
1583
+ clearTimeout(connectionTimeout);
1286
1584
  reject(new Error(`Failed to connect to CLI server: ${error.message}`));
1287
1585
  });
1288
1586
  });
@@ -1298,16 +1596,16 @@ stderr: ${stderrOutput}`
1298
1596
  this.handleSessionLifecycleNotification(notification);
1299
1597
  });
1300
1598
  this.connection.onRequest(
1301
- "tool.call",
1302
- async (params) => await this.handleToolCallRequestV2(params)
1599
+ "userInput.request",
1600
+ async (params) => await this.handleUserInputRequest(params)
1303
1601
  );
1304
1602
  this.connection.onRequest(
1305
- "permission.request",
1306
- async (params) => await this.handlePermissionRequestV2(params)
1603
+ "exitPlanMode.request",
1604
+ async (params) => await this.handleExitPlanModeRequest(params)
1307
1605
  );
1308
1606
  this.connection.onRequest(
1309
- "userInput.request",
1310
- async (params) => await this.handleUserInputRequest(params)
1607
+ "autoModeSwitch.request",
1608
+ async (params) => await this.handleAutoModeSwitchRequest(params)
1311
1609
  );
1312
1610
  this.connection.onRequest(
1313
1611
  "hooks.invoke",
@@ -1343,7 +1641,20 @@ stderr: ${stderrOutput}`
1343
1641
  if (typeof notification !== "object" || !notification || !("type" in notification) || typeof notification.type !== "string" || !("sessionId" in notification) || typeof notification.sessionId !== "string") {
1344
1642
  return;
1345
1643
  }
1346
- const event = notification;
1644
+ const raw = notification;
1645
+ let metadata;
1646
+ if (raw.metadata && raw.metadata.startTime && raw.metadata.modifiedTime) {
1647
+ metadata = {
1648
+ startTime: new Date(raw.metadata.startTime),
1649
+ modifiedTime: new Date(raw.metadata.modifiedTime),
1650
+ summary: raw.metadata.summary
1651
+ };
1652
+ }
1653
+ const event = {
1654
+ type: raw.type,
1655
+ sessionId: raw.sessionId,
1656
+ metadata
1657
+ };
1347
1658
  const typedHandlers = this.typedLifecycleHandlers.get(event.type);
1348
1659
  if (typedHandlers) {
1349
1660
  for (const handler of typedHandlers) {
@@ -1375,125 +1686,55 @@ stderr: ${stderrOutput}`
1375
1686
  });
1376
1687
  return result;
1377
1688
  }
1378
- async handleHooksInvoke(params) {
1379
- if (!params || typeof params.sessionId !== "string" || typeof params.hookType !== "string") {
1380
- throw new Error("Invalid hooks invoke payload");
1689
+ async handleExitPlanModeRequest(params) {
1690
+ if (!params || typeof params.sessionId !== "string" || typeof params.summary !== "string" || !Array.isArray(params.actions) || typeof params.recommendedAction !== "string") {
1691
+ throw new Error("Invalid exit plan mode request payload");
1381
1692
  }
1382
1693
  const session = this.sessions.get(params.sessionId);
1383
1694
  if (!session) {
1384
1695
  throw new Error(`Session not found: ${params.sessionId}`);
1385
1696
  }
1386
- const output = await session._handleHooksInvoke(params.hookType, params.input);
1387
- return { output };
1697
+ return await session._handleExitPlanModeRequest({
1698
+ summary: params.summary,
1699
+ planContent: params.planContent,
1700
+ actions: params.actions,
1701
+ recommendedAction: params.recommendedAction
1702
+ });
1388
1703
  }
1389
- async handleSystemMessageTransform(params) {
1390
- if (!params || typeof params.sessionId !== "string" || !params.sections || typeof params.sections !== "object") {
1391
- throw new Error("Invalid systemMessage.transform payload");
1704
+ async handleAutoModeSwitchRequest(params) {
1705
+ if (!params || typeof params.sessionId !== "string") {
1706
+ throw new Error("Invalid auto mode switch request payload");
1392
1707
  }
1393
1708
  const session = this.sessions.get(params.sessionId);
1394
1709
  if (!session) {
1395
1710
  throw new Error(`Session not found: ${params.sessionId}`);
1396
1711
  }
1397
- return await session._handleSystemMessageTransform(params.sections);
1712
+ const response = await session._handleAutoModeSwitchRequest({
1713
+ errorCode: params.errorCode,
1714
+ retryAfterSeconds: params.retryAfterSeconds
1715
+ });
1716
+ return { response };
1398
1717
  }
1399
- // ========================================================================
1400
- // Protocol v2 backward-compatibility adapters
1401
- // ========================================================================
1402
- /**
1403
- * Handles a v2-style tool.call RPC request from the server.
1404
- * Looks up the session and tool handler, executes it, and returns the result
1405
- * in the v2 response format.
1406
- */
1407
- async handleToolCallRequestV2(params) {
1408
- if (!params || typeof params.sessionId !== "string" || typeof params.toolCallId !== "string" || typeof params.toolName !== "string") {
1409
- throw new Error("Invalid tool call payload");
1718
+ async handleHooksInvoke(params) {
1719
+ if (!params || typeof params.sessionId !== "string" || typeof params.hookType !== "string") {
1720
+ throw new Error("Invalid hooks invoke payload");
1410
1721
  }
1411
1722
  const session = this.sessions.get(params.sessionId);
1412
1723
  if (!session) {
1413
- throw new Error(`Unknown session ${params.sessionId}`);
1414
- }
1415
- const handler = session.getToolHandler(params.toolName);
1416
- if (!handler) {
1417
- return {
1418
- result: {
1419
- textResultForLlm: `Tool '${params.toolName}' is not supported by this client instance.`,
1420
- resultType: "failure",
1421
- error: `tool '${params.toolName}' not supported`,
1422
- toolTelemetry: {}
1423
- }
1424
- };
1425
- }
1426
- try {
1427
- const traceparent = params.traceparent;
1428
- const tracestate = params.tracestate;
1429
- const invocation = {
1430
- sessionId: params.sessionId,
1431
- toolCallId: params.toolCallId,
1432
- toolName: params.toolName,
1433
- arguments: params.arguments,
1434
- traceparent,
1435
- tracestate
1436
- };
1437
- const result = await handler(params.arguments, invocation);
1438
- return { result: this.normalizeToolResultV2(result) };
1439
- } catch (error) {
1440
- const message = error instanceof Error ? error.message : String(error);
1441
- return {
1442
- result: {
1443
- textResultForLlm: "Invoking this tool produced an error. Detailed information is not available.",
1444
- resultType: "failure",
1445
- error: message,
1446
- toolTelemetry: {}
1447
- }
1448
- };
1724
+ throw new Error(`Session not found: ${params.sessionId}`);
1449
1725
  }
1726
+ const output = await session._handleHooksInvoke(params.hookType, params.input);
1727
+ return { output };
1450
1728
  }
1451
- /**
1452
- * Handles a v2-style permission.request RPC request from the server.
1453
- */
1454
- async handlePermissionRequestV2(params) {
1455
- if (!params || typeof params.sessionId !== "string" || !params.permissionRequest) {
1456
- throw new Error("Invalid permission request payload");
1729
+ async handleSystemMessageTransform(params) {
1730
+ if (!params || typeof params.sessionId !== "string" || !params.sections || typeof params.sections !== "object") {
1731
+ throw new Error("Invalid systemMessage.transform payload");
1457
1732
  }
1458
1733
  const session = this.sessions.get(params.sessionId);
1459
1734
  if (!session) {
1460
1735
  throw new Error(`Session not found: ${params.sessionId}`);
1461
1736
  }
1462
- try {
1463
- const result = await session._handlePermissionRequestV2(params.permissionRequest);
1464
- return { result };
1465
- } catch (error) {
1466
- if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) {
1467
- throw error;
1468
- }
1469
- return {
1470
- result: {
1471
- kind: "user-not-available"
1472
- }
1473
- };
1474
- }
1475
- }
1476
- normalizeToolResultV2(result) {
1477
- if (result === void 0 || result === null) {
1478
- return {
1479
- textResultForLlm: "Tool returned no result",
1480
- resultType: "failure",
1481
- error: "tool returned no result",
1482
- toolTelemetry: {}
1483
- };
1484
- }
1485
- if (this.isToolResultObject(result)) {
1486
- return result;
1487
- }
1488
- const textResult = typeof result === "string" ? result : JSON.stringify(result);
1489
- return {
1490
- textResultForLlm: textResult,
1491
- resultType: "success",
1492
- toolTelemetry: {}
1493
- };
1494
- }
1495
- isToolResultObject(value) {
1496
- return typeof value === "object" && value !== null && "textResultForLlm" in value && typeof value.textResultForLlm === "string" && "resultType" in value;
1737
+ return await session._handleSystemMessageTransform(params.sections);
1497
1738
  }
1498
1739
  }
1499
1740
  export {