@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.
@@ -34,9 +34,10 @@ var import_sdkProtocolVersion = require("./sdkProtocolVersion.js");
34
34
  var import_session = require("./session.js");
35
35
  var import_sessionFsProvider = require("./sessionFsProvider.js");
36
36
  var import_telemetry = require("./telemetry.js");
37
+ var import_toolSet = require("./toolSet.js");
37
38
  var import_types = require("./types.js");
38
39
  const import_meta = {};
39
- const MIN_PROTOCOL_VERSION = 2;
40
+ const MIN_PROTOCOL_VERSION = 3;
40
41
  function isZodSchema(value) {
41
42
  return value != null && typeof value === "object" && "toJSONSchema" in value && typeof value.toJSONSchema === "function";
42
43
  }
@@ -47,6 +48,53 @@ function toJsonSchema(parameters) {
47
48
  }
48
49
  return parameters;
49
50
  }
51
+ function toWireMcpServers(mcpServers) {
52
+ if (!mcpServers) return void 0;
53
+ return Object.fromEntries(
54
+ Object.entries(mcpServers).map(([name, server]) => {
55
+ if ("workingDirectory" in server) {
56
+ const { workingDirectory, ...rest } = server;
57
+ return [name, { ...rest, cwd: workingDirectory }];
58
+ }
59
+ return [name, server];
60
+ })
61
+ );
62
+ }
63
+ function toWireCustomAgents(agents) {
64
+ if (!agents) return void 0;
65
+ return agents.map((agent) => {
66
+ if (!agent.mcpServers) return agent;
67
+ const { mcpServers, ...rest } = agent;
68
+ return { ...rest, mcpServers: toWireMcpServers(mcpServers) };
69
+ });
70
+ }
71
+ function toWireLargeOutput(config) {
72
+ if (!config) return void 0;
73
+ const { outputDirectory, ...rest } = config;
74
+ const wire = { ...rest };
75
+ if (outputDirectory !== void 0) {
76
+ wire.outputDir = outputDirectory;
77
+ }
78
+ return wire;
79
+ }
80
+ function toolFilterListToArray(value) {
81
+ if (value === void 0) {
82
+ return void 0;
83
+ }
84
+ return value instanceof import_toolSet.ToolSet ? value.toArray() : value;
85
+ }
86
+ function validateToolFilterList(field, list) {
87
+ if (!list) {
88
+ return;
89
+ }
90
+ for (const entry of list) {
91
+ if (entry === "*") {
92
+ throw new Error(
93
+ `Invalid ${field} entry '*': there is no bare wildcard. Use one or more of \`new ToolSet().addBuiltIn('*')\`, \`.addMcp('*')\`, or \`.addCustom('*')\` to target a specific source.`
94
+ );
95
+ }
96
+ }
97
+ }
50
98
  function extractTransformCallbacks(systemMessage) {
51
99
  if (!systemMessage || systemMessage.mode !== "customize" || !systemMessage.sections) {
52
100
  return { wirePayload: systemMessage, transformCallbacks: void 0 };
@@ -100,12 +148,18 @@ class CopilotClient {
100
148
  cliProcess = null;
101
149
  connection = null;
102
150
  socket = null;
103
- actualPort = null;
151
+ runtimePort = null;
104
152
  actualHost = "localhost";
105
153
  state = "disconnected";
106
154
  sessions = /* @__PURE__ */ new Map();
107
155
  stderrBuffer = "";
108
156
  // Captures CLI stderr for error messages
157
+ /** Resolved connection mode chosen in the constructor. */
158
+ connectionConfig;
159
+ /** Resolved path to the runtime executable (only used for child-process kinds). */
160
+ resolvedCliPath;
161
+ /** Resolved environment passed to the spawned runtime. */
162
+ resolvedEnv;
109
163
  options;
110
164
  isExternalServer = false;
111
165
  forceStopping = false;
@@ -154,85 +208,90 @@ class CopilotClient {
154
208
  * Creates a new CopilotClient instance.
155
209
  *
156
210
  * @param options - Configuration options for the client
157
- * @throws Error if mutually exclusive options are provided (e.g., cliUrl with useStdio or cliPath)
158
211
  *
159
212
  * @example
160
213
  * ```typescript
161
- * // Default options - spawns CLI server using stdio
214
+ * // Default: spawns the bundled runtime over stdio
162
215
  * const client = new CopilotClient();
163
216
  *
164
- * // Connect to an existing server
165
- * const client = new CopilotClient({ cliUrl: "localhost:3000" });
217
+ * // Connect to an existing runtime
218
+ * const client = new CopilotClient({
219
+ * connection: RuntimeConnection.forUri("localhost:3000"),
220
+ * });
221
+ *
222
+ * // Spawn the runtime over TCP on a chosen port
223
+ * const client = new CopilotClient({
224
+ * connection: RuntimeConnection.forTcp({ port: 9001 }),
225
+ * });
166
226
  *
167
- * // Custom CLI path with specific log level
227
+ * // Use a custom runtime binary
168
228
  * const client = new CopilotClient({
169
- * cliPath: "/usr/local/bin/copilot",
170
- * logLevel: "debug"
229
+ * connection: RuntimeConnection.forStdio({ path: "/usr/local/bin/copilot" }),
230
+ * logLevel: "debug",
171
231
  * });
172
232
  * ```
173
233
  */
174
234
  constructor(options = {}) {
175
- if (options.cliUrl && (options.useStdio === true || options.cliPath)) {
176
- throw new Error("cliUrl is mutually exclusive with useStdio and cliPath");
177
- }
178
- if (options.isChildProcess && (options.cliUrl || options.useStdio === false)) {
179
- throw new Error(
180
- "isChildProcess must be used in conjunction with useStdio and not with cliUrl"
181
- );
182
- }
183
- if (options.cliUrl && (options.gitHubToken || options.useLoggedInUser !== void 0)) {
235
+ const conn = options._internalConnection ?? options.connection ?? { kind: "stdio" };
236
+ if (conn.kind === "uri" && (options.gitHubToken !== void 0 || options.useLoggedInUser !== void 0)) {
184
237
  throw new Error(
185
- "gitHubToken and useLoggedInUser cannot be used with cliUrl (external server manages its own auth)"
238
+ "gitHubToken and useLoggedInUser cannot be used with RuntimeConnection.forUri (external server manages its own auth)"
186
239
  );
187
240
  }
188
- if (options.tcpConnectionToken !== void 0) {
189
- if (typeof options.tcpConnectionToken !== "string" || options.tcpConnectionToken.length === 0) {
190
- throw new Error("tcpConnectionToken must be a non-empty string");
191
- }
192
- if (options.useStdio === true) {
193
- throw new Error("tcpConnectionToken cannot be used with useStdio: true");
241
+ if (conn.kind === "tcp" && conn.connectionToken !== void 0) {
242
+ if (typeof conn.connectionToken !== "string" || conn.connectionToken.length === 0) {
243
+ throw new Error("connectionToken must be a non-empty string");
194
244
  }
195
245
  }
196
- const willUseStdio = options.cliUrl ? false : options.useStdio ?? true;
197
- const sdkSpawnsCli = !willUseStdio && !options.cliUrl && !options.isChildProcess;
198
- this.effectiveConnectionToken = options.tcpConnectionToken ?? (sdkSpawnsCli ? (0, import_node_crypto.randomUUID)() : void 0);
246
+ this.connectionConfig = conn;
199
247
  if (options.sessionFs) {
200
248
  this.validateSessionFsConfig(options.sessionFs);
201
249
  }
202
- if (options.cliUrl) {
203
- const { host, port } = this.parseCliUrl(options.cliUrl);
250
+ if (conn.kind === "uri") {
251
+ const { host, port } = this.parseCliUrl(conn.url);
204
252
  this.actualHost = host;
205
- this.actualPort = port;
253
+ this.runtimePort = port;
206
254
  this.isExternalServer = true;
207
- }
208
- if (options.isChildProcess) {
255
+ } else if (conn.kind === "parent-process") {
209
256
  this.isExternalServer = true;
210
257
  }
258
+ if (conn.kind === "tcp") {
259
+ this.effectiveConnectionToken = conn.connectionToken ?? (0, import_node_crypto.randomUUID)();
260
+ } else if (conn.kind === "uri") {
261
+ this.effectiveConnectionToken = conn.connectionToken;
262
+ }
211
263
  this.onListModels = options.onListModels;
212
264
  this.onGetTraceContext = options.onGetTraceContext;
213
265
  this.sessionFsConfig = options.sessionFs ?? null;
214
266
  const effectiveEnv = options.env ?? process.env;
267
+ this.resolvedEnv = effectiveEnv;
268
+ this.resolvedCliPath = conn.kind === "stdio" || conn.kind === "tcp" ? conn.path ?? effectiveEnv.COPILOT_CLI_PATH ?? getBundledCliPath() : void 0;
269
+ const connArgs = conn.kind === "stdio" || conn.kind === "tcp" ? conn.args ?? [] : [];
270
+ this.connectionExtraArgs = [...connArgs];
215
271
  this.options = {
216
- cliPath: options.cliUrl ? void 0 : options.cliPath || effectiveEnv.COPILOT_CLI_PATH || getBundledCliPath(),
217
- cliArgs: options.cliArgs ?? [],
218
- cwd: options.cwd ?? process.cwd(),
219
- port: options.port || 0,
220
- useStdio: options.cliUrl ? false : options.useStdio ?? true,
221
- // Default to stdio unless cliUrl is provided
222
- isChildProcess: options.isChildProcess ?? false,
223
- cliUrl: options.cliUrl,
224
- logLevel: options.logLevel || "debug",
225
- autoStart: options.autoStart ?? true,
226
- autoRestart: false,
227
- env: effectiveEnv,
272
+ workingDirectory: options.workingDirectory ?? process.cwd(),
273
+ logLevel: options.logLevel,
228
274
  gitHubToken: options.gitHubToken,
229
- // Default useLoggedInUser to false when gitHubToken is provided, otherwise true
275
+ // Default useLoggedInUser to false when gitHubToken is provided, otherwise true.
230
276
  useLoggedInUser: options.useLoggedInUser ?? (options.gitHubToken ? false : true),
231
277
  telemetry: options.telemetry,
232
- copilotHome: options.copilotHome,
233
- sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0
278
+ baseDirectory: options.baseDirectory,
279
+ sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0,
280
+ enableRemoteSessions: options.enableRemoteSessions ?? false,
281
+ mode: options.mode ?? "copilot-cli"
234
282
  };
283
+ if (this.options.mode === "empty") {
284
+ const hasPersistence = this.options.baseDirectory !== void 0 || this.sessionFsConfig !== null || // External runtimes manage their own persistence layer; the SDK
285
+ // can't enforce it from here.
286
+ conn.kind === "uri" || conn.kind === "parent-process";
287
+ if (!hasPersistence) {
288
+ throw new Error(
289
+ "CopilotClient was created with mode: 'empty' but neither 'baseDirectory' nor 'sessionFs' was set. Empty mode requires an explicit per-session persistence location; pick one."
290
+ );
291
+ }
292
+ }
235
293
  }
294
+ connectionExtraArgs = [];
236
295
  /**
237
296
  * Parse CLI URL into host and port
238
297
  * Supports formats: "host:port", "http://host:port", "https://host:port", or just "port"
@@ -266,20 +325,37 @@ class CopilotClient {
266
325
  throw new Error("sessionFs.conventions must be either 'windows' or 'posix'");
267
326
  }
268
327
  }
328
+ setupSessionFs(session, config) {
329
+ if (!this.sessionFsConfig) {
330
+ return;
331
+ }
332
+ if (!config.createSessionFsProvider) {
333
+ throw new Error(
334
+ "createSessionFsProvider is required in session config when sessionFs is enabled in client options."
335
+ );
336
+ }
337
+ const provider = config.createSessionFsProvider(session);
338
+ if (this.sessionFsConfig.capabilities?.sqlite && !provider.sqlite) {
339
+ throw new Error(
340
+ "SessionFsConfig declares capabilities.sqlite but the provider does not implement sqlite."
341
+ );
342
+ }
343
+ session.clientSessionApis.sessionFs = (0, import_sessionFsProvider.createSessionFsAdapter)(provider);
344
+ }
269
345
  /**
270
346
  * Starts the CLI server and establishes a connection.
271
347
  *
272
348
  * If connecting to an external server (via cliUrl), only establishes the connection.
273
349
  * Otherwise, spawns the CLI server process and then connects.
274
350
  *
275
- * This method is called automatically when creating a session if `autoStart` is true (default).
351
+ * This method is called automatically the first time you create or resume a session.
276
352
  *
277
353
  * @returns A promise that resolves when the connection is established
278
354
  * @throws Error if the server fails to start or the connection fails
279
355
  *
280
356
  * @example
281
357
  * ```typescript
282
- * const client = new CopilotClient({ autoStart: false });
358
+ * const client = new CopilotClient();
283
359
  * await client.start();
284
360
  * // Now ready to create sessions
285
361
  * ```
@@ -299,7 +375,8 @@ class CopilotClient {
299
375
  await this.connection.sendRequest("sessionFs.setProvider", {
300
376
  initialCwd: this.sessionFsConfig.initialCwd,
301
377
  sessionStatePath: this.sessionFsConfig.sessionStatePath,
302
- conventions: this.sessionFsConfig.conventions
378
+ conventions: this.sessionFsConfig.conventions,
379
+ capabilities: this.sessionFsConfig.capabilities
303
380
  });
304
381
  }
305
382
  this.state = "connected";
@@ -373,8 +450,15 @@ class CopilotClient {
373
450
  }
374
451
  this.modelsCache = null;
375
452
  if (this.socket) {
453
+ const socket = this.socket;
454
+ this.socket = null;
376
455
  try {
377
- this.socket.end();
456
+ if (!socket.destroyed) {
457
+ await new Promise((resolve) => {
458
+ socket.once("close", () => resolve());
459
+ socket.end();
460
+ });
461
+ }
378
462
  } catch (error) {
379
463
  errors.push(
380
464
  new Error(
@@ -382,11 +466,18 @@ class CopilotClient {
382
466
  )
383
467
  );
384
468
  }
385
- this.socket = null;
386
469
  }
387
470
  if (this.cliProcess && !this.isExternalServer) {
471
+ const child = this.cliProcess;
472
+ this.cliProcess = null;
388
473
  try {
389
- this.cliProcess.kill();
474
+ if (child.exitCode === null && child.signalCode === null) {
475
+ const exited = new Promise((resolve) => {
476
+ child.once("exit", () => resolve());
477
+ });
478
+ child.kill();
479
+ await exited;
480
+ }
390
481
  } catch (error) {
391
482
  errors.push(
392
483
  new Error(
@@ -394,18 +485,32 @@ class CopilotClient {
394
485
  )
395
486
  );
396
487
  }
397
- this.cliProcess = null;
398
488
  }
399
489
  if (this.cliStartTimeout) {
400
490
  clearTimeout(this.cliStartTimeout);
401
491
  this.cliStartTimeout = null;
402
492
  }
403
493
  this.state = "disconnected";
404
- this.actualPort = null;
494
+ this.runtimePort = null;
405
495
  this.stderrBuffer = "";
406
496
  this.processExitPromise = null;
407
497
  return errors;
408
498
  }
499
+ /**
500
+ * Alias for {@link stop} that lets `CopilotClient` participate in `await using`
501
+ * blocks for automatic cleanup.
502
+ *
503
+ * @example
504
+ * ```typescript
505
+ * await using client = new CopilotClient();
506
+ * const session = await client.createSession({ onPermissionRequest: approveAll });
507
+ * await session.sendAndWait("Hello");
508
+ * // client.stop() is called automatically when the block exits.
509
+ * ```
510
+ */
511
+ async [Symbol.asyncDispose]() {
512
+ await this.stop();
513
+ }
409
514
  /**
410
515
  * Forcefully stops the CLI server without graceful cleanup.
411
516
  *
@@ -462,7 +567,7 @@ class CopilotClient {
462
567
  this.cliStartTimeout = null;
463
568
  }
464
569
  this.state = "disconnected";
465
- this.actualPort = null;
570
+ this.runtimePort = null;
466
571
  this.stderrBuffer = "";
467
572
  this.processExitPromise = null;
468
573
  }
@@ -470,12 +575,11 @@ class CopilotClient {
470
575
  * Creates a new conversation session with the Copilot CLI.
471
576
  *
472
577
  * Sessions maintain conversation state, handle events, and manage tool execution.
473
- * If the client is not connected and `autoStart` is enabled, this will automatically
474
- * start the connection.
578
+ * If the client is not connected, this method automatically starts the connection.
475
579
  *
476
580
  * @param config - Optional configuration for the session
477
581
  * @returns A promise that resolves with the created session
478
- * @throws Error if the client is not connected and autoStart is disabled
582
+ * @throws Error if the client fails to start
479
583
  *
480
584
  * @example
481
585
  * ```typescript
@@ -495,66 +599,190 @@ class CopilotClient {
495
599
  * });
496
600
  * ```
497
601
  */
498
- async createSession(config) {
499
- if (!config?.onPermissionRequest) {
500
- throw new Error(
501
- "An onPermissionRequest handler is required when creating a session. For example, to allow all permissions, use { onPermissionRequest: approveAll }."
502
- );
503
- }
504
- if (!this.connection) {
505
- if (this.options.autoStart) {
506
- await this.start();
507
- } else {
508
- throw new Error("Client not connected. Call start() first.");
602
+ /**
603
+ * Normalizes session-level tool filter options. Converts {@link ToolSet}
604
+ * instances to plain string arrays, rejects misuse (bare `"*"`) and the
605
+ * missing-availableTools case in `mode = "empty"`.
606
+ *
607
+ * The SDK always sends `toolFilterPrecedence: "excluded"` so callers can
608
+ * compose include + exclude lists naturally (e.g. "everything matching X
609
+ * except Y") regardless of mode. Allowlist-precedence is intentionally not
610
+ * exposed — it's available on the runtime side as a CLI-only concession to
611
+ * legacy behavior, but SDK consumers always get the composable semantics.
612
+ *
613
+ * @internal
614
+ */
615
+ resolveToolFilterOptions(config) {
616
+ const availableTools = toolFilterListToArray(config.availableTools);
617
+ const excludedTools = toolFilterListToArray(config.excludedTools);
618
+ validateToolFilterList("availableTools", availableTools);
619
+ validateToolFilterList("excludedTools", excludedTools);
620
+ if (this.options.mode === "empty") {
621
+ if (availableTools === void 0) {
622
+ throw new Error(
623
+ "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)`."
624
+ );
509
625
  }
510
626
  }
511
- const sessionId = config.sessionId ?? (0, import_node_crypto.randomUUID)();
512
- const session = new import_session.CopilotSession(
513
- sessionId,
514
- this.connection,
515
- void 0,
516
- this.onGetTraceContext
517
- );
518
- session.registerTools(config.tools);
519
- session.registerCommands(config.commands);
520
- session.registerPermissionHandler(config.onPermissionRequest);
521
- if (config.onUserInputRequest) {
522
- session.registerUserInputHandler(config.onUserInputRequest);
627
+ return { availableTools, excludedTools, toolFilterPrecedence: "excluded" };
628
+ }
629
+ /** Mode-specific defaults spread under the caller's config (app values win). */
630
+ configDefaultsForMode() {
631
+ if (this.options.mode === "empty") {
632
+ return {
633
+ enableSessionTelemetry: false,
634
+ mcpOAuthTokenStorage: "in-memory",
635
+ skipEmbeddingRetrieval: true,
636
+ embeddingCacheStorage: "in-memory",
637
+ enableOnDemandInstructionDiscovery: false,
638
+ enableFileHooks: false,
639
+ enableHostGitOperations: false,
640
+ enableSessionStore: false,
641
+ enableSkills: false
642
+ };
523
643
  }
524
- if (config.onElicitationRequest) {
525
- session.registerElicitationHandler(config.onElicitationRequest);
644
+ return {};
645
+ }
646
+ /**
647
+ * Returns the systemMessage config to use, adjusted for the current mode.
648
+ * In empty mode we ensure the environment_context section is removed
649
+ * unless the app has already taken control of it. `append` (and
650
+ * unspecified) mode is promoted to `customize` so we can also strip
651
+ * environment_context; the caller's `content` is preserved verbatim
652
+ * because the runtime appends it as additional instructions in both
653
+ * customize and append modes.
654
+ */
655
+ getSystemMessageConfigForMode(supplied) {
656
+ if (this.options.mode !== "empty") return supplied;
657
+ if (!supplied) {
658
+ return {
659
+ mode: "customize",
660
+ sections: { environment_context: { action: "remove" } }
661
+ };
526
662
  }
527
- if (config.hooks) {
528
- session.registerHooks(config.hooks);
663
+ switch (supplied.mode) {
664
+ case "replace":
665
+ return supplied;
666
+ case "customize":
667
+ if (supplied.sections?.environment_context) return supplied;
668
+ return {
669
+ ...supplied,
670
+ sections: {
671
+ ...supplied.sections,
672
+ environment_context: { action: "remove" }
673
+ }
674
+ };
675
+ case "append":
676
+ case void 0:
677
+ return {
678
+ mode: "customize",
679
+ content: supplied.content,
680
+ sections: { environment_context: { action: "remove" } }
681
+ };
682
+ }
683
+ }
684
+ /**
685
+ * Mode-specific options applied via session.options.update after create/resume.
686
+ *
687
+ * In empty mode, defaults the four overridable feature flags to safe values
688
+ * (caller values from `config` win). `installedPlugins=[]` is unconditional
689
+ * in empty mode — apps that need custom plugins should switch modes.
690
+ */
691
+ async updateSessionOptionsForMode(session, config) {
692
+ const patch = {};
693
+ if (this.options.mode === "empty") {
694
+ patch.skipCustomInstructions = config.skipCustomInstructions ?? true;
695
+ patch.customAgentsLocalOnly = config.customAgentsLocalOnly ?? true;
696
+ patch.coauthorEnabled = config.coauthorEnabled ?? false;
697
+ patch.manageScheduleEnabled = config.manageScheduleEnabled ?? false;
698
+ patch.installedPlugins = [];
699
+ } else {
700
+ if (config.skipCustomInstructions !== void 0)
701
+ patch.skipCustomInstructions = config.skipCustomInstructions;
702
+ if (config.customAgentsLocalOnly !== void 0)
703
+ patch.customAgentsLocalOnly = config.customAgentsLocalOnly;
704
+ if (config.coauthorEnabled !== void 0)
705
+ patch.coauthorEnabled = config.coauthorEnabled;
706
+ if (config.manageScheduleEnabled !== void 0)
707
+ patch.manageScheduleEnabled = config.manageScheduleEnabled;
708
+ }
709
+ if (Object.keys(patch).length === 0) {
710
+ return;
711
+ }
712
+ try {
713
+ await session.rpc.options.update(patch);
714
+ } catch (e) {
715
+ try {
716
+ await session.disconnect();
717
+ } catch {
718
+ }
719
+ throw e;
720
+ }
721
+ }
722
+ async createSession(config) {
723
+ if (!this.connection) {
724
+ await this.start();
529
725
  }
726
+ config = { ...this.configDefaultsForMode(), ...config };
727
+ config.systemMessage = this.getSystemMessageConfigForMode(config.systemMessage);
728
+ const callerSessionId = config.sessionId;
729
+ const useServerGeneratedId = config.cloud != null && callerSessionId == null;
730
+ const localSessionId = useServerGeneratedId ? void 0 : callerSessionId ?? (0, import_node_crypto.randomUUID)();
530
731
  const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
531
732
  config.systemMessage
532
733
  );
533
- if (transformCallbacks) {
534
- session.registerTransformCallbacks(transformCallbacks);
535
- }
536
- if (config.onEvent) {
537
- session.on(config.onEvent);
538
- }
539
- this.sessions.set(sessionId, session);
540
- if (this.sessionFsConfig) {
541
- if (config.createSessionFsHandler) {
542
- session.clientSessionApis.sessionFs = (0, import_sessionFsProvider.createSessionFsAdapter)(
543
- config.createSessionFsHandler(session)
544
- );
545
- } else {
546
- throw new Error(
547
- "createSessionFsHandler is required in session config when sessionFs is enabled in client options."
548
- );
734
+ const initializeSession = (sessionId) => {
735
+ const s = new import_session.CopilotSession(
736
+ sessionId,
737
+ this.connection,
738
+ void 0,
739
+ this.onGetTraceContext
740
+ );
741
+ s.registerTools(config.tools);
742
+ s.registerCanvases(config.canvases);
743
+ s.registerCommands(config.commands);
744
+ s.registerPermissionHandler(config.onPermissionRequest);
745
+ if (config.onUserInputRequest) {
746
+ s.registerUserInputHandler(config.onUserInputRequest);
747
+ }
748
+ if (config.onElicitationRequest) {
749
+ s.registerElicitationHandler(config.onElicitationRequest);
750
+ }
751
+ if (config.onExitPlanModeRequest) {
752
+ s.registerExitPlanModeHandler(config.onExitPlanModeRequest);
753
+ }
754
+ if (config.onAutoModeSwitchRequest) {
755
+ s.registerAutoModeSwitchHandler(config.onAutoModeSwitchRequest);
756
+ }
757
+ if (config.hooks) {
758
+ s.registerHooks(config.hooks);
759
+ }
760
+ if (transformCallbacks) {
761
+ s.registerTransformCallbacks(transformCallbacks);
762
+ }
763
+ if (config.onEvent) {
764
+ s.on(config.onEvent);
549
765
  }
766
+ this.sessions.set(sessionId, s);
767
+ this.setupSessionFs(s, config);
768
+ return s;
769
+ };
770
+ let session;
771
+ let registeredId;
772
+ if (localSessionId !== void 0) {
773
+ session = initializeSession(localSessionId);
774
+ registeredId = localSessionId;
550
775
  }
776
+ const toolFilterOptions = this.resolveToolFilterOptions(config);
551
777
  try {
552
778
  const response = await this.connection.sendRequest("session.create", {
553
779
  ...await (0, import_telemetry.getTraceContext)(this.onGetTraceContext),
554
780
  model: config.model,
555
- sessionId,
781
+ sessionId: localSessionId,
556
782
  clientName: config.clientName,
557
783
  reasoningEffort: config.reasoningEffort,
784
+ reasoningSummary: config.reasoningSummary,
785
+ contextTier: config.contextTier,
558
786
  tools: config.tools?.map((tool) => ({
559
787
  name: tool.name,
560
788
  description: tool.description,
@@ -562,40 +790,82 @@ class CopilotClient {
562
790
  overridesBuiltInTool: tool.overridesBuiltInTool,
563
791
  skipPermission: tool.skipPermission
564
792
  })),
793
+ canvases: config.canvases?.map((canvas) => canvas.declaration),
794
+ requestCanvasRenderer: config.requestCanvasRenderer,
795
+ requestExtensions: config.requestExtensions,
796
+ extensionSdkPath: config.extensionSdkPath,
797
+ extensionInfo: config.extensionInfo,
565
798
  commands: config.commands?.map((cmd) => ({
566
799
  name: cmd.name,
567
800
  description: cmd.description
568
801
  })),
569
802
  systemMessage: wireSystemMessage,
570
- availableTools: config.availableTools,
571
- excludedTools: config.excludedTools,
803
+ availableTools: toolFilterOptions.availableTools,
804
+ excludedTools: toolFilterOptions.excludedTools,
805
+ toolFilterPrecedence: toolFilterOptions.toolFilterPrecedence,
572
806
  provider: config.provider,
807
+ enableSessionTelemetry: config.enableSessionTelemetry,
573
808
  modelCapabilities: config.modelCapabilities,
574
- requestPermission: true,
809
+ largeOutput: toWireLargeOutput(config.largeOutput),
810
+ requestPermission: !!config.onPermissionRequest,
575
811
  requestUserInput: !!config.onUserInputRequest,
576
812
  requestElicitation: !!config.onElicitationRequest,
813
+ ...config.enableMcpApps ? { requestMcpApps: true } : {},
814
+ requestExitPlanMode: !!config.onExitPlanModeRequest,
815
+ requestAutoModeSwitch: !!config.onAutoModeSwitchRequest,
577
816
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
578
817
  workingDirectory: config.workingDirectory,
579
818
  streaming: config.streaming,
580
819
  includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
581
- mcpServers: config.mcpServers,
820
+ mcpServers: toWireMcpServers(config.mcpServers),
821
+ mcpOAuthTokenStorage: config.mcpOAuthTokenStorage,
582
822
  envValueMode: "direct",
583
- customAgents: config.customAgents,
823
+ customAgents: toWireCustomAgents(config.customAgents),
584
824
  defaultAgent: config.defaultAgent,
585
825
  agent: config.agent,
586
- configDir: config.configDir,
826
+ configDir: config.configDirectory,
587
827
  enableConfigDiscovery: config.enableConfigDiscovery,
828
+ skipEmbeddingRetrieval: config.skipEmbeddingRetrieval,
829
+ embeddingCacheStorage: config.embeddingCacheStorage,
830
+ organizationCustomInstructions: config.organizationCustomInstructions,
831
+ enableOnDemandInstructionDiscovery: config.enableOnDemandInstructionDiscovery,
832
+ enableFileHooks: config.enableFileHooks,
833
+ enableHostGitOperations: config.enableHostGitOperations,
834
+ enableSessionStore: config.enableSessionStore,
835
+ enableSkills: config.enableSkills,
588
836
  skillDirectories: config.skillDirectories,
837
+ pluginDirectories: config.pluginDirectories,
589
838
  instructionDirectories: config.instructionDirectories,
590
839
  disabledSkills: config.disabledSkills,
591
840
  infiniteSessions: config.infiniteSessions,
592
- gitHubToken: config.gitHubToken
841
+ gitHubToken: config.gitHubToken,
842
+ remoteSession: config.remoteSession,
843
+ cloud: config.cloud
593
844
  });
594
- const { workspacePath, capabilities } = response;
845
+ const {
846
+ sessionId: returnedSessionId,
847
+ workspacePath,
848
+ capabilities
849
+ } = response;
850
+ if (!returnedSessionId) {
851
+ throw new Error("session.create response did not include a sessionId");
852
+ }
853
+ if (localSessionId !== void 0 && localSessionId !== returnedSessionId) {
854
+ throw new Error(
855
+ `session.create returned sessionId ${returnedSessionId} but the caller requested ${localSessionId}`
856
+ );
857
+ }
858
+ if (session === void 0) {
859
+ session = initializeSession(returnedSessionId);
860
+ registeredId = returnedSessionId;
861
+ }
595
862
  session["_workspacePath"] = workspacePath;
596
863
  session.setCapabilities(capabilities);
864
+ await this.updateSessionOptionsForMode(session, config);
597
865
  } catch (e) {
598
- this.sessions.delete(sessionId);
866
+ if (registeredId !== void 0) {
867
+ this.sessions.delete(registeredId);
868
+ }
599
869
  throw e;
600
870
  }
601
871
  return session;
@@ -625,17 +895,8 @@ class CopilotClient {
625
895
  * ```
626
896
  */
627
897
  async resumeSession(sessionId, config) {
628
- if (!config?.onPermissionRequest) {
629
- throw new Error(
630
- "An onPermissionRequest handler is required when resuming a session. For example, to allow all permissions, use { onPermissionRequest: approveAll }."
631
- );
632
- }
633
898
  if (!this.connection) {
634
- if (this.options.autoStart) {
635
- await this.start();
636
- } else {
637
- throw new Error("Client not connected. Call start() first.");
638
- }
899
+ await this.start();
639
900
  }
640
901
  const session = new import_session.CopilotSession(
641
902
  sessionId,
@@ -644,6 +905,7 @@ class CopilotClient {
644
905
  this.onGetTraceContext
645
906
  );
646
907
  session.registerTools(config.tools);
908
+ session.registerCanvases(config.canvases);
647
909
  session.registerCommands(config.commands);
648
910
  session.registerPermissionHandler(config.onPermissionRequest);
649
911
  if (config.onUserInputRequest) {
@@ -652,9 +914,17 @@ class CopilotClient {
652
914
  if (config.onElicitationRequest) {
653
915
  session.registerElicitationHandler(config.onElicitationRequest);
654
916
  }
917
+ if (config.onExitPlanModeRequest) {
918
+ session.registerExitPlanModeHandler(config.onExitPlanModeRequest);
919
+ }
920
+ if (config.onAutoModeSwitchRequest) {
921
+ session.registerAutoModeSwitchHandler(config.onAutoModeSwitchRequest);
922
+ }
655
923
  if (config.hooks) {
656
924
  session.registerHooks(config.hooks);
657
925
  }
926
+ config = { ...this.configDefaultsForMode(), ...config };
927
+ config.systemMessage = this.getSystemMessageConfigForMode(config.systemMessage);
658
928
  const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
659
929
  config.systemMessage
660
930
  );
@@ -665,17 +935,8 @@ class CopilotClient {
665
935
  session.on(config.onEvent);
666
936
  }
667
937
  this.sessions.set(sessionId, session);
668
- if (this.sessionFsConfig) {
669
- if (config.createSessionFsHandler) {
670
- session.clientSessionApis.sessionFs = (0, import_sessionFsProvider.createSessionFsAdapter)(
671
- config.createSessionFsHandler(session)
672
- );
673
- } else {
674
- throw new Error(
675
- "createSessionFsHandler is required in session config when sessionFs is enabled in client options."
676
- );
677
- }
678
- }
938
+ this.setupSessionFs(session, config);
939
+ const toolFilterOptions = this.resolveToolFilterOptions(config);
679
940
  try {
680
941
  const response = await this.connection.sendRequest("session.resume", {
681
942
  ...await (0, import_telemetry.getTraceContext)(this.onGetTraceContext),
@@ -683,9 +944,13 @@ class CopilotClient {
683
944
  clientName: config.clientName,
684
945
  model: config.model,
685
946
  reasoningEffort: config.reasoningEffort,
947
+ reasoningSummary: config.reasoningSummary,
948
+ contextTier: config.contextTier,
686
949
  systemMessage: wireSystemMessage,
687
- availableTools: config.availableTools,
688
- excludedTools: config.excludedTools,
950
+ availableTools: toolFilterOptions.availableTools,
951
+ excludedTools: toolFilterOptions.excludedTools,
952
+ toolFilterPrecedence: toolFilterOptions.toolFilterPrecedence,
953
+ enableSessionTelemetry: config.enableSessionTelemetry,
689
954
  tools: config.tools?.map((tool) => ({
690
955
  name: tool.name,
691
956
  description: tool.description,
@@ -693,58 +958,66 @@ class CopilotClient {
693
958
  overridesBuiltInTool: tool.overridesBuiltInTool,
694
959
  skipPermission: tool.skipPermission
695
960
  })),
961
+ canvases: config.canvases?.map((canvas) => canvas.declaration),
962
+ requestCanvasRenderer: config.requestCanvasRenderer,
963
+ requestExtensions: config.requestExtensions,
964
+ extensionSdkPath: config.extensionSdkPath,
965
+ extensionInfo: config.extensionInfo,
696
966
  commands: config.commands?.map((cmd) => ({
697
967
  name: cmd.name,
698
968
  description: cmd.description
699
969
  })),
700
970
  provider: config.provider,
701
971
  modelCapabilities: config.modelCapabilities,
972
+ largeOutput: toWireLargeOutput(config.largeOutput),
702
973
  requestPermission: config.onPermissionRequest !== import_types.defaultJoinSessionPermissionHandler,
703
974
  requestUserInput: !!config.onUserInputRequest,
704
975
  requestElicitation: !!config.onElicitationRequest,
976
+ ...config.enableMcpApps ? { requestMcpApps: true } : {},
977
+ requestExitPlanMode: !!config.onExitPlanModeRequest,
978
+ requestAutoModeSwitch: !!config.onAutoModeSwitchRequest,
705
979
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
706
980
  workingDirectory: config.workingDirectory,
707
- configDir: config.configDir,
981
+ configDir: config.configDirectory,
708
982
  enableConfigDiscovery: config.enableConfigDiscovery,
983
+ skipEmbeddingRetrieval: config.skipEmbeddingRetrieval,
984
+ embeddingCacheStorage: config.embeddingCacheStorage,
985
+ organizationCustomInstructions: config.organizationCustomInstructions,
986
+ enableOnDemandInstructionDiscovery: config.enableOnDemandInstructionDiscovery,
987
+ enableFileHooks: config.enableFileHooks,
988
+ enableHostGitOperations: config.enableHostGitOperations,
989
+ enableSessionStore: config.enableSessionStore,
990
+ enableSkills: config.enableSkills,
709
991
  streaming: config.streaming,
710
992
  includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
711
- mcpServers: config.mcpServers,
993
+ mcpServers: toWireMcpServers(config.mcpServers),
994
+ mcpOAuthTokenStorage: config.mcpOAuthTokenStorage,
712
995
  envValueMode: "direct",
713
- customAgents: config.customAgents,
996
+ customAgents: toWireCustomAgents(config.customAgents),
714
997
  defaultAgent: config.defaultAgent,
715
998
  agent: config.agent,
716
999
  skillDirectories: config.skillDirectories,
1000
+ pluginDirectories: config.pluginDirectories,
717
1001
  instructionDirectories: config.instructionDirectories,
718
1002
  disabledSkills: config.disabledSkills,
719
1003
  infiniteSessions: config.infiniteSessions,
720
- disableResume: config.disableResume,
1004
+ disableResume: config.suppressResumeEvent,
721
1005
  continuePendingWork: config.continuePendingWork,
722
- gitHubToken: config.gitHubToken
1006
+ gitHubToken: config.gitHubToken,
1007
+ remoteSession: config.remoteSession,
1008
+ openCanvases: config.openCanvases
723
1009
  });
724
- const { workspacePath, capabilities } = response;
1010
+ const { workspacePath, capabilities, openCanvases } = response;
725
1011
  session["_workspacePath"] = workspacePath;
726
1012
  session.setCapabilities(capabilities);
1013
+ session.setOpenCanvases(openCanvases ?? []);
1014
+ await this.updateSessionOptionsForMode(session, config);
727
1015
  } catch (e) {
728
1016
  this.sessions.delete(sessionId);
729
1017
  throw e;
730
1018
  }
731
1019
  return session;
732
1020
  }
733
- /**
734
- * Gets the current connection state of the client.
735
- *
736
- * @returns The current connection state: "disconnected", "connecting", "connected", or "error"
737
- *
738
- * @example
739
- * ```typescript
740
- * if (client.getState() === "connected") {
741
- * const session = await client.createSession({ onPermissionRequest: approveAll });
742
- * }
743
- * ```
744
- */
745
- getState() {
746
- return this.state;
747
- }
748
1021
  /**
749
1022
  * Sends a ping request to the server to verify connectivity.
750
1023
  *
@@ -857,7 +1130,7 @@ class CopilotClient {
857
1130
  );
858
1131
  serverVersion = result.protocolVersion;
859
1132
  } catch (err) {
860
- if (err instanceof import_node.ResponseError && err.code === import_node.ErrorCodes.MethodNotFound) {
1133
+ if (err instanceof import_node.ResponseError && (err.code === import_node.ErrorCodes.MethodNotFound || err.message === "Unhandled method connect")) {
861
1134
  serverVersion = (await raceAgainstExit(this.ping())).protocolVersion;
862
1135
  } else {
863
1136
  throw err;
@@ -946,8 +1219,13 @@ class CopilotClient {
946
1219
  if (!this.connection) {
947
1220
  throw new Error("Client not connected");
948
1221
  }
1222
+ let wireFilter;
1223
+ if (filter) {
1224
+ const { workingDirectory, ...rest } = filter;
1225
+ wireFilter = { ...rest, cwd: workingDirectory };
1226
+ }
949
1227
  const response = await this.connection.sendRequest("session.list", {
950
- filter
1228
+ filter: wireFilter
951
1229
  });
952
1230
  const { sessions } = response;
953
1231
  return sessions.map(CopilotClient.toSessionMetadata);
@@ -982,13 +1260,19 @@ class CopilotClient {
982
1260
  return CopilotClient.toSessionMetadata(session);
983
1261
  }
984
1262
  static toSessionMetadata(raw) {
1263
+ const { context } = raw;
985
1264
  return {
986
1265
  sessionId: raw.sessionId,
987
1266
  startTime: new Date(raw.startTime),
988
1267
  modifiedTime: new Date(raw.modifiedTime),
989
1268
  summary: raw.summary,
990
1269
  isRemote: raw.isRemote,
991
- context: raw.context
1270
+ context: context ? {
1271
+ workingDirectory: context.cwd,
1272
+ gitRoot: context.gitRoot,
1273
+ repository: context.repository,
1274
+ branch: context.branch
1275
+ } : void 0
992
1276
  };
993
1277
  }
994
1278
  /**
@@ -1041,7 +1325,7 @@ class CopilotClient {
1041
1325
  throw new Error(result.error || "Failed to set foreground session");
1042
1326
  }
1043
1327
  }
1044
- on(eventTypeOrHandler, handler) {
1328
+ onLifecycle(eventTypeOrHandler, handler) {
1045
1329
  if (typeof eventTypeOrHandler === "string" && handler) {
1046
1330
  const eventType = eventTypeOrHandler;
1047
1331
  if (!this.typedLifecycleHandlers.has(eventType)) {
@@ -1068,17 +1352,17 @@ class CopilotClient {
1068
1352
  async startCLIServer() {
1069
1353
  return new Promise((resolve, reject) => {
1070
1354
  this.stderrBuffer = "";
1071
- const args = [
1072
- ...this.options.cliArgs,
1073
- "--headless",
1074
- "--no-auto-update",
1075
- "--log-level",
1076
- this.options.logLevel
1077
- ];
1078
- if (this.options.useStdio) {
1355
+ const args = [...this.connectionExtraArgs, "--headless", "--no-auto-update"];
1356
+ if (this.options.logLevel) {
1357
+ args.push("--log-level", this.options.logLevel);
1358
+ }
1359
+ if (this.connectionConfig.kind === "stdio") {
1079
1360
  args.push("--stdio");
1080
- } else if (this.options.port > 0) {
1081
- args.push("--port", this.options.port.toString());
1361
+ } else if (this.connectionConfig.kind === "tcp") {
1362
+ const requestedPort = this.connectionConfig.port ?? 0;
1363
+ if (requestedPort > 0) {
1364
+ args.push("--port", requestedPort.toString());
1365
+ }
1082
1366
  }
1083
1367
  if (this.options.gitHubToken) {
1084
1368
  args.push("--auth-token-env", "COPILOT_SDK_AUTH_TOKEN");
@@ -1092,7 +1376,10 @@ class CopilotClient {
1092
1376
  this.options.sessionIdleTimeoutSeconds.toString()
1093
1377
  );
1094
1378
  }
1095
- const envWithoutNodeDebug = { ...this.options.env };
1379
+ if (this.options.enableRemoteSessions) {
1380
+ args.push("--remote");
1381
+ }
1382
+ const envWithoutNodeDebug = { ...this.resolvedEnv };
1096
1383
  delete envWithoutNodeDebug.NODE_DEBUG;
1097
1384
  if (this.options.gitHubToken) {
1098
1385
  envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.gitHubToken;
@@ -1100,12 +1387,15 @@ class CopilotClient {
1100
1387
  if (this.effectiveConnectionToken) {
1101
1388
  envWithoutNodeDebug.COPILOT_CONNECTION_TOKEN = this.effectiveConnectionToken;
1102
1389
  }
1103
- if (this.options.copilotHome) {
1104
- envWithoutNodeDebug.COPILOT_HOME = this.options.copilotHome;
1390
+ if (this.options.baseDirectory) {
1391
+ envWithoutNodeDebug.COPILOT_HOME = this.options.baseDirectory;
1105
1392
  }
1106
- if (!this.options.cliPath) {
1393
+ if (this.options.mode === "empty") {
1394
+ envWithoutNodeDebug.COPILOT_DISABLE_KEYTAR = "1";
1395
+ }
1396
+ if (!this.resolvedCliPath) {
1107
1397
  throw new Error(
1108
- "Path to Copilot CLI is required. Please provide it via the cliPath option, or use cliUrl to rely on a remote CLI."
1398
+ "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."
1109
1399
  );
1110
1400
  }
1111
1401
  if (this.options.telemetry) {
@@ -1124,31 +1414,31 @@ class CopilotClient {
1124
1414
  t.captureContent
1125
1415
  );
1126
1416
  }
1127
- if (!(0, import_node_fs.existsSync)(this.options.cliPath)) {
1417
+ if (!(0, import_node_fs.existsSync)(this.resolvedCliPath)) {
1128
1418
  throw new Error(
1129
- `Copilot CLI not found at ${this.options.cliPath}. Ensure @github/copilot is installed.`
1419
+ `Copilot CLI not found at ${this.resolvedCliPath}. Ensure @github/copilot is installed.`
1130
1420
  );
1131
1421
  }
1132
- const stdioConfig = this.options.useStdio ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
1133
- const isJsFile = this.options.cliPath.endsWith(".js");
1422
+ const stdioConfig = this.connectionConfig.kind === "stdio" ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
1423
+ const isJsFile = this.resolvedCliPath.endsWith(".js");
1134
1424
  if (isJsFile) {
1135
- this.cliProcess = (0, import_node_child_process.spawn)(getNodeExecPath(), [this.options.cliPath, ...args], {
1425
+ this.cliProcess = (0, import_node_child_process.spawn)(getNodeExecPath(), [this.resolvedCliPath, ...args], {
1136
1426
  stdio: stdioConfig,
1137
- cwd: this.options.cwd,
1427
+ cwd: this.options.workingDirectory,
1138
1428
  env: envWithoutNodeDebug,
1139
1429
  windowsHide: true
1140
1430
  });
1141
1431
  } else {
1142
- this.cliProcess = (0, import_node_child_process.spawn)(this.options.cliPath, args, {
1432
+ this.cliProcess = (0, import_node_child_process.spawn)(this.resolvedCliPath, args, {
1143
1433
  stdio: stdioConfig,
1144
- cwd: this.options.cwd,
1434
+ cwd: this.options.workingDirectory,
1145
1435
  env: envWithoutNodeDebug,
1146
1436
  windowsHide: true
1147
1437
  });
1148
1438
  }
1149
1439
  let stdout = "";
1150
1440
  let resolved = false;
1151
- if (this.options.useStdio) {
1441
+ if (this.connectionConfig.kind === "stdio") {
1152
1442
  resolved = true;
1153
1443
  resolve();
1154
1444
  } else {
@@ -1156,7 +1446,7 @@ class CopilotClient {
1156
1446
  stdout += data.toString();
1157
1447
  const match = stdout.match(/listening on port (\d+)/i);
1158
1448
  if (match && !resolved) {
1159
- this.actualPort = parseInt(match[1], 10);
1449
+ this.runtimePort = parseInt(match[1], 10);
1160
1450
  resolved = true;
1161
1451
  resolve();
1162
1452
  }
@@ -1230,19 +1520,21 @@ stderr: ${stderrOutput}`
1230
1520
  resolved = true;
1231
1521
  reject(new Error("Timeout waiting for CLI server to start"));
1232
1522
  }
1233
- }, 1e4);
1523
+ }, 3e4);
1234
1524
  });
1235
1525
  }
1236
1526
  /**
1237
1527
  * Connect to the CLI server (via socket or stdio)
1238
1528
  */
1239
1529
  async connectToServer() {
1240
- if (this.options.isChildProcess) {
1241
- return this.connectToParentProcessViaStdio();
1242
- } else if (this.options.useStdio) {
1243
- return this.connectToChildProcessViaStdio();
1244
- } else {
1245
- return this.connectViaTcp();
1530
+ switch (this.connectionConfig.kind) {
1531
+ case "parent-process":
1532
+ return this.connectToParentProcessViaStdio();
1533
+ case "stdio":
1534
+ return this.connectToChildProcessViaStdio();
1535
+ case "tcp":
1536
+ case "uri":
1537
+ return this.connectViaTcp();
1246
1538
  }
1247
1539
  }
1248
1540
  /**
@@ -1282,12 +1574,17 @@ stderr: ${stderrOutput}`
1282
1574
  * Connect to the CLI server via TCP socket
1283
1575
  */
1284
1576
  async connectViaTcp() {
1285
- if (!this.actualPort) {
1577
+ if (!this.runtimePort) {
1286
1578
  throw new Error("Server port not available");
1287
1579
  }
1288
1580
  return new Promise((resolve, reject) => {
1289
1581
  this.socket = new import_node_net.Socket();
1290
- this.socket.connect(this.actualPort, this.actualHost, () => {
1582
+ const connectionTimeout = setTimeout(() => {
1583
+ this.socket?.destroy();
1584
+ reject(new Error("Timeout connecting to CLI server"));
1585
+ }, 1e4);
1586
+ this.socket.connect(this.runtimePort, this.actualHost, () => {
1587
+ clearTimeout(connectionTimeout);
1291
1588
  this.connection = (0, import_node.createMessageConnection)(
1292
1589
  new import_node.StreamMessageReader(this.socket),
1293
1590
  new import_node.StreamMessageWriter(this.socket)
@@ -1297,6 +1594,7 @@ stderr: ${stderrOutput}`
1297
1594
  resolve();
1298
1595
  });
1299
1596
  this.socket.on("error", (error) => {
1597
+ clearTimeout(connectionTimeout);
1300
1598
  reject(new Error(`Failed to connect to CLI server: ${error.message}`));
1301
1599
  });
1302
1600
  });
@@ -1312,16 +1610,16 @@ stderr: ${stderrOutput}`
1312
1610
  this.handleSessionLifecycleNotification(notification);
1313
1611
  });
1314
1612
  this.connection.onRequest(
1315
- "tool.call",
1316
- async (params) => await this.handleToolCallRequestV2(params)
1613
+ "userInput.request",
1614
+ async (params) => await this.handleUserInputRequest(params)
1317
1615
  );
1318
1616
  this.connection.onRequest(
1319
- "permission.request",
1320
- async (params) => await this.handlePermissionRequestV2(params)
1617
+ "exitPlanMode.request",
1618
+ async (params) => await this.handleExitPlanModeRequest(params)
1321
1619
  );
1322
1620
  this.connection.onRequest(
1323
- "userInput.request",
1324
- async (params) => await this.handleUserInputRequest(params)
1621
+ "autoModeSwitch.request",
1622
+ async (params) => await this.handleAutoModeSwitchRequest(params)
1325
1623
  );
1326
1624
  this.connection.onRequest(
1327
1625
  "hooks.invoke",
@@ -1357,7 +1655,20 @@ stderr: ${stderrOutput}`
1357
1655
  if (typeof notification !== "object" || !notification || !("type" in notification) || typeof notification.type !== "string" || !("sessionId" in notification) || typeof notification.sessionId !== "string") {
1358
1656
  return;
1359
1657
  }
1360
- const event = notification;
1658
+ const raw = notification;
1659
+ let metadata;
1660
+ if (raw.metadata && raw.metadata.startTime && raw.metadata.modifiedTime) {
1661
+ metadata = {
1662
+ startTime: new Date(raw.metadata.startTime),
1663
+ modifiedTime: new Date(raw.metadata.modifiedTime),
1664
+ summary: raw.metadata.summary
1665
+ };
1666
+ }
1667
+ const event = {
1668
+ type: raw.type,
1669
+ sessionId: raw.sessionId,
1670
+ metadata
1671
+ };
1361
1672
  const typedHandlers = this.typedLifecycleHandlers.get(event.type);
1362
1673
  if (typedHandlers) {
1363
1674
  for (const handler of typedHandlers) {
@@ -1389,125 +1700,55 @@ stderr: ${stderrOutput}`
1389
1700
  });
1390
1701
  return result;
1391
1702
  }
1392
- async handleHooksInvoke(params) {
1393
- if (!params || typeof params.sessionId !== "string" || typeof params.hookType !== "string") {
1394
- throw new Error("Invalid hooks invoke payload");
1703
+ async handleExitPlanModeRequest(params) {
1704
+ if (!params || typeof params.sessionId !== "string" || typeof params.summary !== "string" || !Array.isArray(params.actions) || typeof params.recommendedAction !== "string") {
1705
+ throw new Error("Invalid exit plan mode request payload");
1395
1706
  }
1396
1707
  const session = this.sessions.get(params.sessionId);
1397
1708
  if (!session) {
1398
1709
  throw new Error(`Session not found: ${params.sessionId}`);
1399
1710
  }
1400
- const output = await session._handleHooksInvoke(params.hookType, params.input);
1401
- return { output };
1711
+ return await session._handleExitPlanModeRequest({
1712
+ summary: params.summary,
1713
+ planContent: params.planContent,
1714
+ actions: params.actions,
1715
+ recommendedAction: params.recommendedAction
1716
+ });
1402
1717
  }
1403
- async handleSystemMessageTransform(params) {
1404
- if (!params || typeof params.sessionId !== "string" || !params.sections || typeof params.sections !== "object") {
1405
- throw new Error("Invalid systemMessage.transform payload");
1718
+ async handleAutoModeSwitchRequest(params) {
1719
+ if (!params || typeof params.sessionId !== "string") {
1720
+ throw new Error("Invalid auto mode switch request payload");
1406
1721
  }
1407
1722
  const session = this.sessions.get(params.sessionId);
1408
1723
  if (!session) {
1409
1724
  throw new Error(`Session not found: ${params.sessionId}`);
1410
1725
  }
1411
- return await session._handleSystemMessageTransform(params.sections);
1726
+ const response = await session._handleAutoModeSwitchRequest({
1727
+ errorCode: params.errorCode,
1728
+ retryAfterSeconds: params.retryAfterSeconds
1729
+ });
1730
+ return { response };
1412
1731
  }
1413
- // ========================================================================
1414
- // Protocol v2 backward-compatibility adapters
1415
- // ========================================================================
1416
- /**
1417
- * Handles a v2-style tool.call RPC request from the server.
1418
- * Looks up the session and tool handler, executes it, and returns the result
1419
- * in the v2 response format.
1420
- */
1421
- async handleToolCallRequestV2(params) {
1422
- if (!params || typeof params.sessionId !== "string" || typeof params.toolCallId !== "string" || typeof params.toolName !== "string") {
1423
- throw new Error("Invalid tool call payload");
1732
+ async handleHooksInvoke(params) {
1733
+ if (!params || typeof params.sessionId !== "string" || typeof params.hookType !== "string") {
1734
+ throw new Error("Invalid hooks invoke payload");
1424
1735
  }
1425
1736
  const session = this.sessions.get(params.sessionId);
1426
1737
  if (!session) {
1427
- throw new Error(`Unknown session ${params.sessionId}`);
1428
- }
1429
- const handler = session.getToolHandler(params.toolName);
1430
- if (!handler) {
1431
- return {
1432
- result: {
1433
- textResultForLlm: `Tool '${params.toolName}' is not supported by this client instance.`,
1434
- resultType: "failure",
1435
- error: `tool '${params.toolName}' not supported`,
1436
- toolTelemetry: {}
1437
- }
1438
- };
1439
- }
1440
- try {
1441
- const traceparent = params.traceparent;
1442
- const tracestate = params.tracestate;
1443
- const invocation = {
1444
- sessionId: params.sessionId,
1445
- toolCallId: params.toolCallId,
1446
- toolName: params.toolName,
1447
- arguments: params.arguments,
1448
- traceparent,
1449
- tracestate
1450
- };
1451
- const result = await handler(params.arguments, invocation);
1452
- return { result: this.normalizeToolResultV2(result) };
1453
- } catch (error) {
1454
- const message = error instanceof Error ? error.message : String(error);
1455
- return {
1456
- result: {
1457
- textResultForLlm: "Invoking this tool produced an error. Detailed information is not available.",
1458
- resultType: "failure",
1459
- error: message,
1460
- toolTelemetry: {}
1461
- }
1462
- };
1738
+ throw new Error(`Session not found: ${params.sessionId}`);
1463
1739
  }
1740
+ const output = await session._handleHooksInvoke(params.hookType, params.input);
1741
+ return { output };
1464
1742
  }
1465
- /**
1466
- * Handles a v2-style permission.request RPC request from the server.
1467
- */
1468
- async handlePermissionRequestV2(params) {
1469
- if (!params || typeof params.sessionId !== "string" || !params.permissionRequest) {
1470
- throw new Error("Invalid permission request payload");
1743
+ async handleSystemMessageTransform(params) {
1744
+ if (!params || typeof params.sessionId !== "string" || !params.sections || typeof params.sections !== "object") {
1745
+ throw new Error("Invalid systemMessage.transform payload");
1471
1746
  }
1472
1747
  const session = this.sessions.get(params.sessionId);
1473
1748
  if (!session) {
1474
1749
  throw new Error(`Session not found: ${params.sessionId}`);
1475
1750
  }
1476
- try {
1477
- const result = await session._handlePermissionRequestV2(params.permissionRequest);
1478
- return { result };
1479
- } catch (error) {
1480
- if (error instanceof Error && error.message === import_session.NO_RESULT_PERMISSION_V2_ERROR) {
1481
- throw error;
1482
- }
1483
- return {
1484
- result: {
1485
- kind: "user-not-available"
1486
- }
1487
- };
1488
- }
1489
- }
1490
- normalizeToolResultV2(result) {
1491
- if (result === void 0 || result === null) {
1492
- return {
1493
- textResultForLlm: "Tool returned no result",
1494
- resultType: "failure",
1495
- error: "tool returned no result",
1496
- toolTelemetry: {}
1497
- };
1498
- }
1499
- if (this.isToolResultObject(result)) {
1500
- return result;
1501
- }
1502
- const textResult = typeof result === "string" ? result : JSON.stringify(result);
1503
- return {
1504
- textResultForLlm: textResult,
1505
- resultType: "success",
1506
- toolTelemetry: {}
1507
- };
1508
- }
1509
- isToolResultObject(value) {
1510
- return typeof value === "object" && value !== null && "textResultForLlm" in value && typeof value.textResultForLlm === "string" && "resultType" in value;
1751
+ return await session._handleSystemMessageTransform(params.sections);
1511
1752
  }
1512
1753
  }
1513
1754
  // Annotate the CommonJS export names for ESM import in node: