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