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

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,189 @@ 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,
558
785
  tools: config.tools?.map((tool) => ({
559
786
  name: tool.name,
560
787
  description: tool.description,
@@ -562,40 +789,81 @@ class CopilotClient {
562
789
  overridesBuiltInTool: tool.overridesBuiltInTool,
563
790
  skipPermission: tool.skipPermission
564
791
  })),
792
+ canvases: config.canvases?.map((canvas) => canvas.declaration),
793
+ requestCanvasRenderer: config.requestCanvasRenderer,
794
+ requestExtensions: config.requestExtensions,
795
+ extensionInfo: config.extensionInfo,
565
796
  commands: config.commands?.map((cmd) => ({
566
797
  name: cmd.name,
567
798
  description: cmd.description
568
799
  })),
569
800
  systemMessage: wireSystemMessage,
570
- availableTools: config.availableTools,
571
- excludedTools: config.excludedTools,
801
+ availableTools: toolFilterOptions.availableTools,
802
+ excludedTools: toolFilterOptions.excludedTools,
803
+ toolFilterPrecedence: toolFilterOptions.toolFilterPrecedence,
572
804
  provider: config.provider,
805
+ enableSessionTelemetry: config.enableSessionTelemetry,
573
806
  modelCapabilities: config.modelCapabilities,
574
- requestPermission: true,
807
+ largeOutput: toWireLargeOutput(config.largeOutput),
808
+ requestPermission: !!config.onPermissionRequest,
575
809
  requestUserInput: !!config.onUserInputRequest,
576
810
  requestElicitation: !!config.onElicitationRequest,
811
+ ...config.enableMcpApps ? { requestMcpApps: true } : {},
812
+ requestExitPlanMode: !!config.onExitPlanModeRequest,
813
+ requestAutoModeSwitch: !!config.onAutoModeSwitchRequest,
577
814
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
578
815
  workingDirectory: config.workingDirectory,
579
816
  streaming: config.streaming,
580
817
  includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
581
- mcpServers: config.mcpServers,
818
+ mcpServers: toWireMcpServers(config.mcpServers),
819
+ mcpOAuthTokenStorage: config.mcpOAuthTokenStorage,
582
820
  envValueMode: "direct",
583
- customAgents: config.customAgents,
821
+ customAgents: toWireCustomAgents(config.customAgents),
584
822
  defaultAgent: config.defaultAgent,
585
823
  agent: config.agent,
586
- configDir: config.configDir,
824
+ configDir: config.configDirectory,
587
825
  enableConfigDiscovery: config.enableConfigDiscovery,
826
+ skipEmbeddingRetrieval: config.skipEmbeddingRetrieval,
827
+ embeddingCacheStorage: config.embeddingCacheStorage,
828
+ organizationCustomInstructions: config.organizationCustomInstructions,
829
+ enableOnDemandInstructionDiscovery: config.enableOnDemandInstructionDiscovery,
830
+ enableFileHooks: config.enableFileHooks,
831
+ enableHostGitOperations: config.enableHostGitOperations,
832
+ enableSessionStore: config.enableSessionStore,
833
+ enableSkills: config.enableSkills,
588
834
  skillDirectories: config.skillDirectories,
835
+ pluginDirectories: config.pluginDirectories,
589
836
  instructionDirectories: config.instructionDirectories,
590
837
  disabledSkills: config.disabledSkills,
591
838
  infiniteSessions: config.infiniteSessions,
592
- gitHubToken: config.gitHubToken
839
+ gitHubToken: config.gitHubToken,
840
+ remoteSession: config.remoteSession,
841
+ cloud: config.cloud
593
842
  });
594
- const { workspacePath, capabilities } = response;
843
+ const {
844
+ sessionId: returnedSessionId,
845
+ workspacePath,
846
+ capabilities
847
+ } = response;
848
+ if (!returnedSessionId) {
849
+ throw new Error("session.create response did not include a sessionId");
850
+ }
851
+ if (localSessionId !== void 0 && localSessionId !== returnedSessionId) {
852
+ throw new Error(
853
+ `session.create returned sessionId ${returnedSessionId} but the caller requested ${localSessionId}`
854
+ );
855
+ }
856
+ if (session === void 0) {
857
+ session = initializeSession(returnedSessionId);
858
+ registeredId = returnedSessionId;
859
+ }
595
860
  session["_workspacePath"] = workspacePath;
596
861
  session.setCapabilities(capabilities);
862
+ await this.updateSessionOptionsForMode(session, config);
597
863
  } catch (e) {
598
- this.sessions.delete(sessionId);
864
+ if (registeredId !== void 0) {
865
+ this.sessions.delete(registeredId);
866
+ }
599
867
  throw e;
600
868
  }
601
869
  return session;
@@ -625,17 +893,8 @@ class CopilotClient {
625
893
  * ```
626
894
  */
627
895
  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
896
  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
- }
897
+ await this.start();
639
898
  }
640
899
  const session = new import_session.CopilotSession(
641
900
  sessionId,
@@ -644,6 +903,7 @@ class CopilotClient {
644
903
  this.onGetTraceContext
645
904
  );
646
905
  session.registerTools(config.tools);
906
+ session.registerCanvases(config.canvases);
647
907
  session.registerCommands(config.commands);
648
908
  session.registerPermissionHandler(config.onPermissionRequest);
649
909
  if (config.onUserInputRequest) {
@@ -652,9 +912,17 @@ class CopilotClient {
652
912
  if (config.onElicitationRequest) {
653
913
  session.registerElicitationHandler(config.onElicitationRequest);
654
914
  }
915
+ if (config.onExitPlanModeRequest) {
916
+ session.registerExitPlanModeHandler(config.onExitPlanModeRequest);
917
+ }
918
+ if (config.onAutoModeSwitchRequest) {
919
+ session.registerAutoModeSwitchHandler(config.onAutoModeSwitchRequest);
920
+ }
655
921
  if (config.hooks) {
656
922
  session.registerHooks(config.hooks);
657
923
  }
924
+ config = { ...this.configDefaultsForMode(), ...config };
925
+ config.systemMessage = this.getSystemMessageConfigForMode(config.systemMessage);
658
926
  const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
659
927
  config.systemMessage
660
928
  );
@@ -665,17 +933,8 @@ class CopilotClient {
665
933
  session.on(config.onEvent);
666
934
  }
667
935
  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
- }
936
+ this.setupSessionFs(session, config);
937
+ const toolFilterOptions = this.resolveToolFilterOptions(config);
679
938
  try {
680
939
  const response = await this.connection.sendRequest("session.resume", {
681
940
  ...await (0, import_telemetry.getTraceContext)(this.onGetTraceContext),
@@ -683,9 +942,12 @@ class CopilotClient {
683
942
  clientName: config.clientName,
684
943
  model: config.model,
685
944
  reasoningEffort: config.reasoningEffort,
945
+ reasoningSummary: config.reasoningSummary,
686
946
  systemMessage: wireSystemMessage,
687
- availableTools: config.availableTools,
688
- excludedTools: config.excludedTools,
947
+ availableTools: toolFilterOptions.availableTools,
948
+ excludedTools: toolFilterOptions.excludedTools,
949
+ toolFilterPrecedence: toolFilterOptions.toolFilterPrecedence,
950
+ enableSessionTelemetry: config.enableSessionTelemetry,
689
951
  tools: config.tools?.map((tool) => ({
690
952
  name: tool.name,
691
953
  description: tool.description,
@@ -693,58 +955,65 @@ class CopilotClient {
693
955
  overridesBuiltInTool: tool.overridesBuiltInTool,
694
956
  skipPermission: tool.skipPermission
695
957
  })),
958
+ canvases: config.canvases?.map((canvas) => canvas.declaration),
959
+ requestCanvasRenderer: config.requestCanvasRenderer,
960
+ requestExtensions: config.requestExtensions,
961
+ extensionInfo: config.extensionInfo,
696
962
  commands: config.commands?.map((cmd) => ({
697
963
  name: cmd.name,
698
964
  description: cmd.description
699
965
  })),
700
966
  provider: config.provider,
701
967
  modelCapabilities: config.modelCapabilities,
968
+ largeOutput: toWireLargeOutput(config.largeOutput),
702
969
  requestPermission: config.onPermissionRequest !== import_types.defaultJoinSessionPermissionHandler,
703
970
  requestUserInput: !!config.onUserInputRequest,
704
971
  requestElicitation: !!config.onElicitationRequest,
972
+ ...config.enableMcpApps ? { requestMcpApps: true } : {},
973
+ requestExitPlanMode: !!config.onExitPlanModeRequest,
974
+ requestAutoModeSwitch: !!config.onAutoModeSwitchRequest,
705
975
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
706
976
  workingDirectory: config.workingDirectory,
707
- configDir: config.configDir,
977
+ configDir: config.configDirectory,
708
978
  enableConfigDiscovery: config.enableConfigDiscovery,
979
+ skipEmbeddingRetrieval: config.skipEmbeddingRetrieval,
980
+ embeddingCacheStorage: config.embeddingCacheStorage,
981
+ organizationCustomInstructions: config.organizationCustomInstructions,
982
+ enableOnDemandInstructionDiscovery: config.enableOnDemandInstructionDiscovery,
983
+ enableFileHooks: config.enableFileHooks,
984
+ enableHostGitOperations: config.enableHostGitOperations,
985
+ enableSessionStore: config.enableSessionStore,
986
+ enableSkills: config.enableSkills,
709
987
  streaming: config.streaming,
710
988
  includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
711
- mcpServers: config.mcpServers,
989
+ mcpServers: toWireMcpServers(config.mcpServers),
990
+ mcpOAuthTokenStorage: config.mcpOAuthTokenStorage,
712
991
  envValueMode: "direct",
713
- customAgents: config.customAgents,
992
+ customAgents: toWireCustomAgents(config.customAgents),
714
993
  defaultAgent: config.defaultAgent,
715
994
  agent: config.agent,
716
995
  skillDirectories: config.skillDirectories,
996
+ pluginDirectories: config.pluginDirectories,
717
997
  instructionDirectories: config.instructionDirectories,
718
998
  disabledSkills: config.disabledSkills,
719
999
  infiniteSessions: config.infiniteSessions,
720
- disableResume: config.disableResume,
1000
+ suppressResumeEvent: config.suppressResumeEvent,
721
1001
  continuePendingWork: config.continuePendingWork,
722
- gitHubToken: config.gitHubToken
1002
+ gitHubToken: config.gitHubToken,
1003
+ remoteSession: config.remoteSession,
1004
+ openCanvases: config.openCanvases
723
1005
  });
724
- const { workspacePath, capabilities } = response;
1006
+ const { workspacePath, capabilities, openCanvases } = response;
725
1007
  session["_workspacePath"] = workspacePath;
726
1008
  session.setCapabilities(capabilities);
1009
+ session.setOpenCanvases(openCanvases ?? []);
1010
+ await this.updateSessionOptionsForMode(session, config);
727
1011
  } catch (e) {
728
1012
  this.sessions.delete(sessionId);
729
1013
  throw e;
730
1014
  }
731
1015
  return session;
732
1016
  }
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
1017
  /**
749
1018
  * Sends a ping request to the server to verify connectivity.
750
1019
  *
@@ -857,7 +1126,7 @@ class CopilotClient {
857
1126
  );
858
1127
  serverVersion = result.protocolVersion;
859
1128
  } catch (err) {
860
- if (err instanceof import_node.ResponseError && err.code === import_node.ErrorCodes.MethodNotFound) {
1129
+ if (err instanceof import_node.ResponseError && (err.code === import_node.ErrorCodes.MethodNotFound || err.message === "Unhandled method connect")) {
861
1130
  serverVersion = (await raceAgainstExit(this.ping())).protocolVersion;
862
1131
  } else {
863
1132
  throw err;
@@ -946,8 +1215,13 @@ class CopilotClient {
946
1215
  if (!this.connection) {
947
1216
  throw new Error("Client not connected");
948
1217
  }
1218
+ let wireFilter;
1219
+ if (filter) {
1220
+ const { workingDirectory, ...rest } = filter;
1221
+ wireFilter = { ...rest, cwd: workingDirectory };
1222
+ }
949
1223
  const response = await this.connection.sendRequest("session.list", {
950
- filter
1224
+ filter: wireFilter
951
1225
  });
952
1226
  const { sessions } = response;
953
1227
  return sessions.map(CopilotClient.toSessionMetadata);
@@ -982,13 +1256,19 @@ class CopilotClient {
982
1256
  return CopilotClient.toSessionMetadata(session);
983
1257
  }
984
1258
  static toSessionMetadata(raw) {
1259
+ const { context } = raw;
985
1260
  return {
986
1261
  sessionId: raw.sessionId,
987
1262
  startTime: new Date(raw.startTime),
988
1263
  modifiedTime: new Date(raw.modifiedTime),
989
1264
  summary: raw.summary,
990
1265
  isRemote: raw.isRemote,
991
- context: raw.context
1266
+ context: context ? {
1267
+ workingDirectory: context.cwd,
1268
+ gitRoot: context.gitRoot,
1269
+ repository: context.repository,
1270
+ branch: context.branch
1271
+ } : void 0
992
1272
  };
993
1273
  }
994
1274
  /**
@@ -1041,7 +1321,7 @@ class CopilotClient {
1041
1321
  throw new Error(result.error || "Failed to set foreground session");
1042
1322
  }
1043
1323
  }
1044
- on(eventTypeOrHandler, handler) {
1324
+ onLifecycle(eventTypeOrHandler, handler) {
1045
1325
  if (typeof eventTypeOrHandler === "string" && handler) {
1046
1326
  const eventType = eventTypeOrHandler;
1047
1327
  if (!this.typedLifecycleHandlers.has(eventType)) {
@@ -1068,17 +1348,17 @@ class CopilotClient {
1068
1348
  async startCLIServer() {
1069
1349
  return new Promise((resolve, reject) => {
1070
1350
  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) {
1351
+ const args = [...this.connectionExtraArgs, "--headless", "--no-auto-update"];
1352
+ if (this.options.logLevel) {
1353
+ args.push("--log-level", this.options.logLevel);
1354
+ }
1355
+ if (this.connectionConfig.kind === "stdio") {
1079
1356
  args.push("--stdio");
1080
- } else if (this.options.port > 0) {
1081
- args.push("--port", this.options.port.toString());
1357
+ } else if (this.connectionConfig.kind === "tcp") {
1358
+ const requestedPort = this.connectionConfig.port ?? 0;
1359
+ if (requestedPort > 0) {
1360
+ args.push("--port", requestedPort.toString());
1361
+ }
1082
1362
  }
1083
1363
  if (this.options.gitHubToken) {
1084
1364
  args.push("--auth-token-env", "COPILOT_SDK_AUTH_TOKEN");
@@ -1092,7 +1372,10 @@ class CopilotClient {
1092
1372
  this.options.sessionIdleTimeoutSeconds.toString()
1093
1373
  );
1094
1374
  }
1095
- const envWithoutNodeDebug = { ...this.options.env };
1375
+ if (this.options.enableRemoteSessions) {
1376
+ args.push("--remote");
1377
+ }
1378
+ const envWithoutNodeDebug = { ...this.resolvedEnv };
1096
1379
  delete envWithoutNodeDebug.NODE_DEBUG;
1097
1380
  if (this.options.gitHubToken) {
1098
1381
  envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.gitHubToken;
@@ -1100,12 +1383,15 @@ class CopilotClient {
1100
1383
  if (this.effectiveConnectionToken) {
1101
1384
  envWithoutNodeDebug.COPILOT_CONNECTION_TOKEN = this.effectiveConnectionToken;
1102
1385
  }
1103
- if (this.options.copilotHome) {
1104
- envWithoutNodeDebug.COPILOT_HOME = this.options.copilotHome;
1386
+ if (this.options.baseDirectory) {
1387
+ envWithoutNodeDebug.COPILOT_HOME = this.options.baseDirectory;
1105
1388
  }
1106
- if (!this.options.cliPath) {
1389
+ if (this.options.mode === "empty") {
1390
+ envWithoutNodeDebug.COPILOT_DISABLE_KEYTAR = "1";
1391
+ }
1392
+ if (!this.resolvedCliPath) {
1107
1393
  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."
1394
+ "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
1395
  );
1110
1396
  }
1111
1397
  if (this.options.telemetry) {
@@ -1124,31 +1410,31 @@ class CopilotClient {
1124
1410
  t.captureContent
1125
1411
  );
1126
1412
  }
1127
- if (!(0, import_node_fs.existsSync)(this.options.cliPath)) {
1413
+ if (!(0, import_node_fs.existsSync)(this.resolvedCliPath)) {
1128
1414
  throw new Error(
1129
- `Copilot CLI not found at ${this.options.cliPath}. Ensure @github/copilot is installed.`
1415
+ `Copilot CLI not found at ${this.resolvedCliPath}. Ensure @github/copilot is installed.`
1130
1416
  );
1131
1417
  }
1132
- const stdioConfig = this.options.useStdio ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
1133
- const isJsFile = this.options.cliPath.endsWith(".js");
1418
+ const stdioConfig = this.connectionConfig.kind === "stdio" ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
1419
+ const isJsFile = this.resolvedCliPath.endsWith(".js");
1134
1420
  if (isJsFile) {
1135
- this.cliProcess = (0, import_node_child_process.spawn)(getNodeExecPath(), [this.options.cliPath, ...args], {
1421
+ this.cliProcess = (0, import_node_child_process.spawn)(getNodeExecPath(), [this.resolvedCliPath, ...args], {
1136
1422
  stdio: stdioConfig,
1137
- cwd: this.options.cwd,
1423
+ cwd: this.options.workingDirectory,
1138
1424
  env: envWithoutNodeDebug,
1139
1425
  windowsHide: true
1140
1426
  });
1141
1427
  } else {
1142
- this.cliProcess = (0, import_node_child_process.spawn)(this.options.cliPath, args, {
1428
+ this.cliProcess = (0, import_node_child_process.spawn)(this.resolvedCliPath, args, {
1143
1429
  stdio: stdioConfig,
1144
- cwd: this.options.cwd,
1430
+ cwd: this.options.workingDirectory,
1145
1431
  env: envWithoutNodeDebug,
1146
1432
  windowsHide: true
1147
1433
  });
1148
1434
  }
1149
1435
  let stdout = "";
1150
1436
  let resolved = false;
1151
- if (this.options.useStdio) {
1437
+ if (this.connectionConfig.kind === "stdio") {
1152
1438
  resolved = true;
1153
1439
  resolve();
1154
1440
  } else {
@@ -1156,7 +1442,7 @@ class CopilotClient {
1156
1442
  stdout += data.toString();
1157
1443
  const match = stdout.match(/listening on port (\d+)/i);
1158
1444
  if (match && !resolved) {
1159
- this.actualPort = parseInt(match[1], 10);
1445
+ this.runtimePort = parseInt(match[1], 10);
1160
1446
  resolved = true;
1161
1447
  resolve();
1162
1448
  }
@@ -1230,19 +1516,21 @@ stderr: ${stderrOutput}`
1230
1516
  resolved = true;
1231
1517
  reject(new Error("Timeout waiting for CLI server to start"));
1232
1518
  }
1233
- }, 1e4);
1519
+ }, 3e4);
1234
1520
  });
1235
1521
  }
1236
1522
  /**
1237
1523
  * Connect to the CLI server (via socket or stdio)
1238
1524
  */
1239
1525
  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();
1526
+ switch (this.connectionConfig.kind) {
1527
+ case "parent-process":
1528
+ return this.connectToParentProcessViaStdio();
1529
+ case "stdio":
1530
+ return this.connectToChildProcessViaStdio();
1531
+ case "tcp":
1532
+ case "uri":
1533
+ return this.connectViaTcp();
1246
1534
  }
1247
1535
  }
1248
1536
  /**
@@ -1282,12 +1570,17 @@ stderr: ${stderrOutput}`
1282
1570
  * Connect to the CLI server via TCP socket
1283
1571
  */
1284
1572
  async connectViaTcp() {
1285
- if (!this.actualPort) {
1573
+ if (!this.runtimePort) {
1286
1574
  throw new Error("Server port not available");
1287
1575
  }
1288
1576
  return new Promise((resolve, reject) => {
1289
1577
  this.socket = new import_node_net.Socket();
1290
- this.socket.connect(this.actualPort, this.actualHost, () => {
1578
+ const connectionTimeout = setTimeout(() => {
1579
+ this.socket?.destroy();
1580
+ reject(new Error("Timeout connecting to CLI server"));
1581
+ }, 1e4);
1582
+ this.socket.connect(this.runtimePort, this.actualHost, () => {
1583
+ clearTimeout(connectionTimeout);
1291
1584
  this.connection = (0, import_node.createMessageConnection)(
1292
1585
  new import_node.StreamMessageReader(this.socket),
1293
1586
  new import_node.StreamMessageWriter(this.socket)
@@ -1297,6 +1590,7 @@ stderr: ${stderrOutput}`
1297
1590
  resolve();
1298
1591
  });
1299
1592
  this.socket.on("error", (error) => {
1593
+ clearTimeout(connectionTimeout);
1300
1594
  reject(new Error(`Failed to connect to CLI server: ${error.message}`));
1301
1595
  });
1302
1596
  });
@@ -1312,16 +1606,16 @@ stderr: ${stderrOutput}`
1312
1606
  this.handleSessionLifecycleNotification(notification);
1313
1607
  });
1314
1608
  this.connection.onRequest(
1315
- "tool.call",
1316
- async (params) => await this.handleToolCallRequestV2(params)
1609
+ "userInput.request",
1610
+ async (params) => await this.handleUserInputRequest(params)
1317
1611
  );
1318
1612
  this.connection.onRequest(
1319
- "permission.request",
1320
- async (params) => await this.handlePermissionRequestV2(params)
1613
+ "exitPlanMode.request",
1614
+ async (params) => await this.handleExitPlanModeRequest(params)
1321
1615
  );
1322
1616
  this.connection.onRequest(
1323
- "userInput.request",
1324
- async (params) => await this.handleUserInputRequest(params)
1617
+ "autoModeSwitch.request",
1618
+ async (params) => await this.handleAutoModeSwitchRequest(params)
1325
1619
  );
1326
1620
  this.connection.onRequest(
1327
1621
  "hooks.invoke",
@@ -1357,7 +1651,20 @@ stderr: ${stderrOutput}`
1357
1651
  if (typeof notification !== "object" || !notification || !("type" in notification) || typeof notification.type !== "string" || !("sessionId" in notification) || typeof notification.sessionId !== "string") {
1358
1652
  return;
1359
1653
  }
1360
- const event = notification;
1654
+ const raw = notification;
1655
+ let metadata;
1656
+ if (raw.metadata && raw.metadata.startTime && raw.metadata.modifiedTime) {
1657
+ metadata = {
1658
+ startTime: new Date(raw.metadata.startTime),
1659
+ modifiedTime: new Date(raw.metadata.modifiedTime),
1660
+ summary: raw.metadata.summary
1661
+ };
1662
+ }
1663
+ const event = {
1664
+ type: raw.type,
1665
+ sessionId: raw.sessionId,
1666
+ metadata
1667
+ };
1361
1668
  const typedHandlers = this.typedLifecycleHandlers.get(event.type);
1362
1669
  if (typedHandlers) {
1363
1670
  for (const handler of typedHandlers) {
@@ -1389,125 +1696,55 @@ stderr: ${stderrOutput}`
1389
1696
  });
1390
1697
  return result;
1391
1698
  }
1392
- async handleHooksInvoke(params) {
1393
- if (!params || typeof params.sessionId !== "string" || typeof params.hookType !== "string") {
1394
- throw new Error("Invalid hooks invoke payload");
1699
+ async handleExitPlanModeRequest(params) {
1700
+ if (!params || typeof params.sessionId !== "string" || typeof params.summary !== "string" || !Array.isArray(params.actions) || typeof params.recommendedAction !== "string") {
1701
+ throw new Error("Invalid exit plan mode request payload");
1395
1702
  }
1396
1703
  const session = this.sessions.get(params.sessionId);
1397
1704
  if (!session) {
1398
1705
  throw new Error(`Session not found: ${params.sessionId}`);
1399
1706
  }
1400
- const output = await session._handleHooksInvoke(params.hookType, params.input);
1401
- return { output };
1707
+ return await session._handleExitPlanModeRequest({
1708
+ summary: params.summary,
1709
+ planContent: params.planContent,
1710
+ actions: params.actions,
1711
+ recommendedAction: params.recommendedAction
1712
+ });
1402
1713
  }
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");
1714
+ async handleAutoModeSwitchRequest(params) {
1715
+ if (!params || typeof params.sessionId !== "string") {
1716
+ throw new Error("Invalid auto mode switch request payload");
1406
1717
  }
1407
1718
  const session = this.sessions.get(params.sessionId);
1408
1719
  if (!session) {
1409
1720
  throw new Error(`Session not found: ${params.sessionId}`);
1410
1721
  }
1411
- return await session._handleSystemMessageTransform(params.sections);
1722
+ const response = await session._handleAutoModeSwitchRequest({
1723
+ errorCode: params.errorCode,
1724
+ retryAfterSeconds: params.retryAfterSeconds
1725
+ });
1726
+ return { response };
1412
1727
  }
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");
1728
+ async handleHooksInvoke(params) {
1729
+ if (!params || typeof params.sessionId !== "string" || typeof params.hookType !== "string") {
1730
+ throw new Error("Invalid hooks invoke payload");
1424
1731
  }
1425
1732
  const session = this.sessions.get(params.sessionId);
1426
1733
  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
- };
1734
+ throw new Error(`Session not found: ${params.sessionId}`);
1463
1735
  }
1736
+ const output = await session._handleHooksInvoke(params.hookType, params.input);
1737
+ return { output };
1464
1738
  }
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");
1739
+ async handleSystemMessageTransform(params) {
1740
+ if (!params || typeof params.sessionId !== "string" || !params.sections || typeof params.sections !== "object") {
1741
+ throw new Error("Invalid systemMessage.transform payload");
1471
1742
  }
1472
1743
  const session = this.sessions.get(params.sessionId);
1473
1744
  if (!session) {
1474
1745
  throw new Error(`Session not found: ${params.sessionId}`);
1475
1746
  }
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;
1747
+ return await session._handleSystemMessageTransform(params.sections);
1511
1748
  }
1512
1749
  }
1513
1750
  // Annotate the CommonJS export names for ESM import in node: