@github/copilot-sdk 0.3.0 → 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,15 +148,23 @@ 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;
166
+ /** Token sent in `connect`; auto-generated when the SDK spawns its own CLI in TCP mode. */
167
+ effectiveConnectionToken;
112
168
  onListModels;
113
169
  onGetTraceContext;
114
170
  modelsCache = null;
@@ -116,6 +172,7 @@ class CopilotClient {
116
172
  sessionLifecycleHandlers = /* @__PURE__ */ new Set();
117
173
  typedLifecycleHandlers = /* @__PURE__ */ new Map();
118
174
  _rpc = null;
175
+ _internalRpc = null;
119
176
  processExitPromise = null;
120
177
  // Rejects when CLI process exits
121
178
  negotiatedProtocolVersion = null;
@@ -134,77 +191,107 @@ class CopilotClient {
134
191
  }
135
192
  return this._rpc;
136
193
  }
194
+ /**
195
+ * Internal RPC surface (e.g. handshake helpers). Not part of the public API.
196
+ * @internal
197
+ */
198
+ get internalRpc() {
199
+ if (!this.connection) {
200
+ throw new Error("Client is not connected. Call start() first.");
201
+ }
202
+ if (!this._internalRpc) {
203
+ this._internalRpc = (0, import_rpc.createInternalServerRpc)(this.connection);
204
+ }
205
+ return this._internalRpc;
206
+ }
137
207
  /**
138
208
  * Creates a new CopilotClient instance.
139
209
  *
140
210
  * @param options - Configuration options for the client
141
- * @throws Error if mutually exclusive options are provided (e.g., cliUrl with useStdio or cliPath)
142
211
  *
143
212
  * @example
144
213
  * ```typescript
145
- * // Default options - spawns CLI server using stdio
214
+ * // Default: spawns the bundled runtime over stdio
146
215
  * const client = new CopilotClient();
147
216
  *
148
- * // Connect to an existing server
149
- * 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
+ * });
150
221
  *
151
- * // Custom CLI path with specific log level
222
+ * // Spawn the runtime over TCP on a chosen port
152
223
  * const client = new CopilotClient({
153
- * cliPath: "/usr/local/bin/copilot",
154
- * logLevel: "debug"
224
+ * connection: RuntimeConnection.forTcp({ port: 9001 }),
225
+ * });
226
+ *
227
+ * // Use a custom runtime binary
228
+ * const client = new CopilotClient({
229
+ * connection: RuntimeConnection.forStdio({ path: "/usr/local/bin/copilot" }),
230
+ * logLevel: "debug",
155
231
  * });
156
232
  * ```
157
233
  */
158
234
  constructor(options = {}) {
159
- if (options.cliUrl && (options.useStdio === true || options.cliPath)) {
160
- throw new Error("cliUrl is mutually exclusive with useStdio and cliPath");
161
- }
162
- if (options.isChildProcess && (options.cliUrl || options.useStdio === false)) {
235
+ const conn = options._internalConnection ?? options.connection ?? { kind: "stdio" };
236
+ if (conn.kind === "uri" && (options.gitHubToken !== void 0 || options.useLoggedInUser !== void 0)) {
163
237
  throw new Error(
164
- "isChildProcess must be used in conjunction with useStdio and not with cliUrl"
238
+ "gitHubToken and useLoggedInUser cannot be used with RuntimeConnection.forUri (external server manages its own auth)"
165
239
  );
166
240
  }
167
- if (options.cliUrl && (options.gitHubToken || options.useLoggedInUser !== void 0)) {
168
- throw new Error(
169
- "gitHubToken and useLoggedInUser cannot be used with cliUrl (external server manages its own auth)"
170
- );
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");
244
+ }
171
245
  }
246
+ this.connectionConfig = conn;
172
247
  if (options.sessionFs) {
173
248
  this.validateSessionFsConfig(options.sessionFs);
174
249
  }
175
- if (options.cliUrl) {
176
- const { host, port } = this.parseCliUrl(options.cliUrl);
250
+ if (conn.kind === "uri") {
251
+ const { host, port } = this.parseCliUrl(conn.url);
177
252
  this.actualHost = host;
178
- this.actualPort = port;
253
+ this.runtimePort = port;
179
254
  this.isExternalServer = true;
180
- }
181
- if (options.isChildProcess) {
255
+ } else if (conn.kind === "parent-process") {
182
256
  this.isExternalServer = true;
183
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
+ }
184
263
  this.onListModels = options.onListModels;
185
264
  this.onGetTraceContext = options.onGetTraceContext;
186
265
  this.sessionFsConfig = options.sessionFs ?? null;
187
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];
188
271
  this.options = {
189
- cliPath: options.cliUrl ? void 0 : options.cliPath || effectiveEnv.COPILOT_CLI_PATH || getBundledCliPath(),
190
- cliArgs: options.cliArgs ?? [],
191
- cwd: options.cwd ?? process.cwd(),
192
- port: options.port || 0,
193
- useStdio: options.cliUrl ? false : options.useStdio ?? true,
194
- // Default to stdio unless cliUrl is provided
195
- isChildProcess: options.isChildProcess ?? false,
196
- cliUrl: options.cliUrl,
197
- logLevel: options.logLevel || "debug",
198
- autoStart: options.autoStart ?? true,
199
- autoRestart: false,
200
- env: effectiveEnv,
272
+ workingDirectory: options.workingDirectory ?? process.cwd(),
273
+ logLevel: options.logLevel,
201
274
  gitHubToken: options.gitHubToken,
202
- // Default useLoggedInUser to false when gitHubToken is provided, otherwise true
275
+ // Default useLoggedInUser to false when gitHubToken is provided, otherwise true.
203
276
  useLoggedInUser: options.useLoggedInUser ?? (options.gitHubToken ? false : true),
204
277
  telemetry: options.telemetry,
205
- sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0
278
+ baseDirectory: options.baseDirectory,
279
+ sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0,
280
+ enableRemoteSessions: options.enableRemoteSessions ?? false,
281
+ mode: options.mode ?? "copilot-cli"
206
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
+ }
207
293
  }
294
+ connectionExtraArgs = [];
208
295
  /**
209
296
  * Parse CLI URL into host and port
210
297
  * Supports formats: "host:port", "http://host:port", "https://host:port", or just "port"
@@ -238,20 +325,37 @@ class CopilotClient {
238
325
  throw new Error("sessionFs.conventions must be either 'windows' or 'posix'");
239
326
  }
240
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
+ }
241
345
  /**
242
346
  * Starts the CLI server and establishes a connection.
243
347
  *
244
348
  * If connecting to an external server (via cliUrl), only establishes the connection.
245
349
  * Otherwise, spawns the CLI server process and then connects.
246
350
  *
247
- * 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.
248
352
  *
249
353
  * @returns A promise that resolves when the connection is established
250
354
  * @throws Error if the server fails to start or the connection fails
251
355
  *
252
356
  * @example
253
357
  * ```typescript
254
- * const client = new CopilotClient({ autoStart: false });
358
+ * const client = new CopilotClient();
255
359
  * await client.start();
256
360
  * // Now ready to create sessions
257
361
  * ```
@@ -271,7 +375,8 @@ class CopilotClient {
271
375
  await this.connection.sendRequest("sessionFs.setProvider", {
272
376
  initialCwd: this.sessionFsConfig.initialCwd,
273
377
  sessionStatePath: this.sessionFsConfig.sessionStatePath,
274
- conventions: this.sessionFsConfig.conventions
378
+ conventions: this.sessionFsConfig.conventions,
379
+ capabilities: this.sessionFsConfig.capabilities
275
380
  });
276
381
  }
277
382
  this.state = "connected";
@@ -345,8 +450,15 @@ class CopilotClient {
345
450
  }
346
451
  this.modelsCache = null;
347
452
  if (this.socket) {
453
+ const socket = this.socket;
454
+ this.socket = null;
348
455
  try {
349
- this.socket.end();
456
+ if (!socket.destroyed) {
457
+ await new Promise((resolve) => {
458
+ socket.once("close", () => resolve());
459
+ socket.end();
460
+ });
461
+ }
350
462
  } catch (error) {
351
463
  errors.push(
352
464
  new Error(
@@ -354,11 +466,18 @@ class CopilotClient {
354
466
  )
355
467
  );
356
468
  }
357
- this.socket = null;
358
469
  }
359
470
  if (this.cliProcess && !this.isExternalServer) {
471
+ const child = this.cliProcess;
472
+ this.cliProcess = null;
360
473
  try {
361
- 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
+ }
362
481
  } catch (error) {
363
482
  errors.push(
364
483
  new Error(
@@ -366,18 +485,32 @@ class CopilotClient {
366
485
  )
367
486
  );
368
487
  }
369
- this.cliProcess = null;
370
488
  }
371
489
  if (this.cliStartTimeout) {
372
490
  clearTimeout(this.cliStartTimeout);
373
491
  this.cliStartTimeout = null;
374
492
  }
375
493
  this.state = "disconnected";
376
- this.actualPort = null;
494
+ this.runtimePort = null;
377
495
  this.stderrBuffer = "";
378
496
  this.processExitPromise = null;
379
497
  return errors;
380
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
+ }
381
514
  /**
382
515
  * Forcefully stops the CLI server without graceful cleanup.
383
516
  *
@@ -434,7 +567,7 @@ class CopilotClient {
434
567
  this.cliStartTimeout = null;
435
568
  }
436
569
  this.state = "disconnected";
437
- this.actualPort = null;
570
+ this.runtimePort = null;
438
571
  this.stderrBuffer = "";
439
572
  this.processExitPromise = null;
440
573
  }
@@ -442,12 +575,11 @@ class CopilotClient {
442
575
  * Creates a new conversation session with the Copilot CLI.
443
576
  *
444
577
  * Sessions maintain conversation state, handle events, and manage tool execution.
445
- * If the client is not connected and `autoStart` is enabled, this will automatically
446
- * start the connection.
578
+ * If the client is not connected, this method automatically starts the connection.
447
579
  *
448
580
  * @param config - Optional configuration for the session
449
581
  * @returns A promise that resolves with the created session
450
- * @throws Error if the client is not connected and autoStart is disabled
582
+ * @throws Error if the client fails to start
451
583
  *
452
584
  * @example
453
585
  * ```typescript
@@ -467,66 +599,189 @@ class CopilotClient {
467
599
  * });
468
600
  * ```
469
601
  */
470
- async createSession(config) {
471
- if (!config?.onPermissionRequest) {
472
- throw new Error(
473
- "An onPermissionRequest handler is required when creating a session. For example, to allow all permissions, use { onPermissionRequest: approveAll }."
474
- );
475
- }
476
- if (!this.connection) {
477
- if (this.options.autoStart) {
478
- await this.start();
479
- } else {
480
- 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
+ );
481
625
  }
482
626
  }
483
- const sessionId = config.sessionId ?? (0, import_node_crypto.randomUUID)();
484
- const session = new import_session.CopilotSession(
485
- sessionId,
486
- this.connection,
487
- void 0,
488
- this.onGetTraceContext
489
- );
490
- session.registerTools(config.tools);
491
- session.registerCommands(config.commands);
492
- session.registerPermissionHandler(config.onPermissionRequest);
493
- if (config.onUserInputRequest) {
494
- 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
+ };
495
643
  }
496
- if (config.onElicitationRequest) {
497
- 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
+ };
498
662
  }
499
- if (config.hooks) {
500
- 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();
501
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)();
502
731
  const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
503
732
  config.systemMessage
504
733
  );
505
- if (transformCallbacks) {
506
- session.registerTransformCallbacks(transformCallbacks);
507
- }
508
- if (config.onEvent) {
509
- session.on(config.onEvent);
510
- }
511
- this.sessions.set(sessionId, session);
512
- if (this.sessionFsConfig) {
513
- if (config.createSessionFsHandler) {
514
- session.clientSessionApis.sessionFs = (0, import_sessionFsProvider.createSessionFsAdapter)(
515
- config.createSessionFsHandler(session)
516
- );
517
- } else {
518
- throw new Error(
519
- "createSessionFsHandler is required in session config when sessionFs is enabled in client options."
520
- );
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);
521
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);
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;
522
775
  }
776
+ const toolFilterOptions = this.resolveToolFilterOptions(config);
523
777
  try {
524
778
  const response = await this.connection.sendRequest("session.create", {
525
779
  ...await (0, import_telemetry.getTraceContext)(this.onGetTraceContext),
526
780
  model: config.model,
527
- sessionId,
781
+ sessionId: localSessionId,
528
782
  clientName: config.clientName,
529
783
  reasoningEffort: config.reasoningEffort,
784
+ reasoningSummary: config.reasoningSummary,
530
785
  tools: config.tools?.map((tool) => ({
531
786
  name: tool.name,
532
787
  description: tool.description,
@@ -534,39 +789,81 @@ class CopilotClient {
534
789
  overridesBuiltInTool: tool.overridesBuiltInTool,
535
790
  skipPermission: tool.skipPermission
536
791
  })),
792
+ canvases: config.canvases?.map((canvas) => canvas.declaration),
793
+ requestCanvasRenderer: config.requestCanvasRenderer,
794
+ requestExtensions: config.requestExtensions,
795
+ extensionInfo: config.extensionInfo,
537
796
  commands: config.commands?.map((cmd) => ({
538
797
  name: cmd.name,
539
798
  description: cmd.description
540
799
  })),
541
800
  systemMessage: wireSystemMessage,
542
- availableTools: config.availableTools,
543
- excludedTools: config.excludedTools,
801
+ availableTools: toolFilterOptions.availableTools,
802
+ excludedTools: toolFilterOptions.excludedTools,
803
+ toolFilterPrecedence: toolFilterOptions.toolFilterPrecedence,
544
804
  provider: config.provider,
805
+ enableSessionTelemetry: config.enableSessionTelemetry,
545
806
  modelCapabilities: config.modelCapabilities,
546
- requestPermission: true,
807
+ largeOutput: toWireLargeOutput(config.largeOutput),
808
+ requestPermission: !!config.onPermissionRequest,
547
809
  requestUserInput: !!config.onUserInputRequest,
548
810
  requestElicitation: !!config.onElicitationRequest,
811
+ ...config.enableMcpApps ? { requestMcpApps: true } : {},
812
+ requestExitPlanMode: !!config.onExitPlanModeRequest,
813
+ requestAutoModeSwitch: !!config.onAutoModeSwitchRequest,
549
814
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
550
815
  workingDirectory: config.workingDirectory,
551
816
  streaming: config.streaming,
552
817
  includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
553
- mcpServers: config.mcpServers,
818
+ mcpServers: toWireMcpServers(config.mcpServers),
819
+ mcpOAuthTokenStorage: config.mcpOAuthTokenStorage,
554
820
  envValueMode: "direct",
555
- customAgents: config.customAgents,
821
+ customAgents: toWireCustomAgents(config.customAgents),
556
822
  defaultAgent: config.defaultAgent,
557
823
  agent: config.agent,
558
- configDir: config.configDir,
824
+ configDir: config.configDirectory,
559
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,
560
834
  skillDirectories: config.skillDirectories,
835
+ pluginDirectories: config.pluginDirectories,
836
+ instructionDirectories: config.instructionDirectories,
561
837
  disabledSkills: config.disabledSkills,
562
838
  infiniteSessions: config.infiniteSessions,
563
- gitHubToken: config.gitHubToken
839
+ gitHubToken: config.gitHubToken,
840
+ remoteSession: config.remoteSession,
841
+ cloud: config.cloud
564
842
  });
565
- 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
+ }
566
860
  session["_workspacePath"] = workspacePath;
567
861
  session.setCapabilities(capabilities);
862
+ await this.updateSessionOptionsForMode(session, config);
568
863
  } catch (e) {
569
- this.sessions.delete(sessionId);
864
+ if (registeredId !== void 0) {
865
+ this.sessions.delete(registeredId);
866
+ }
570
867
  throw e;
571
868
  }
572
869
  return session;
@@ -596,17 +893,8 @@ class CopilotClient {
596
893
  * ```
597
894
  */
598
895
  async resumeSession(sessionId, config) {
599
- if (!config?.onPermissionRequest) {
600
- throw new Error(
601
- "An onPermissionRequest handler is required when resuming a session. For example, to allow all permissions, use { onPermissionRequest: approveAll }."
602
- );
603
- }
604
896
  if (!this.connection) {
605
- if (this.options.autoStart) {
606
- await this.start();
607
- } else {
608
- throw new Error("Client not connected. Call start() first.");
609
- }
897
+ await this.start();
610
898
  }
611
899
  const session = new import_session.CopilotSession(
612
900
  sessionId,
@@ -615,6 +903,7 @@ class CopilotClient {
615
903
  this.onGetTraceContext
616
904
  );
617
905
  session.registerTools(config.tools);
906
+ session.registerCanvases(config.canvases);
618
907
  session.registerCommands(config.commands);
619
908
  session.registerPermissionHandler(config.onPermissionRequest);
620
909
  if (config.onUserInputRequest) {
@@ -623,9 +912,17 @@ class CopilotClient {
623
912
  if (config.onElicitationRequest) {
624
913
  session.registerElicitationHandler(config.onElicitationRequest);
625
914
  }
915
+ if (config.onExitPlanModeRequest) {
916
+ session.registerExitPlanModeHandler(config.onExitPlanModeRequest);
917
+ }
918
+ if (config.onAutoModeSwitchRequest) {
919
+ session.registerAutoModeSwitchHandler(config.onAutoModeSwitchRequest);
920
+ }
626
921
  if (config.hooks) {
627
922
  session.registerHooks(config.hooks);
628
923
  }
924
+ config = { ...this.configDefaultsForMode(), ...config };
925
+ config.systemMessage = this.getSystemMessageConfigForMode(config.systemMessage);
629
926
  const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
630
927
  config.systemMessage
631
928
  );
@@ -636,17 +933,8 @@ class CopilotClient {
636
933
  session.on(config.onEvent);
637
934
  }
638
935
  this.sessions.set(sessionId, session);
639
- if (this.sessionFsConfig) {
640
- if (config.createSessionFsHandler) {
641
- session.clientSessionApis.sessionFs = (0, import_sessionFsProvider.createSessionFsAdapter)(
642
- config.createSessionFsHandler(session)
643
- );
644
- } else {
645
- throw new Error(
646
- "createSessionFsHandler is required in session config when sessionFs is enabled in client options."
647
- );
648
- }
649
- }
936
+ this.setupSessionFs(session, config);
937
+ const toolFilterOptions = this.resolveToolFilterOptions(config);
650
938
  try {
651
939
  const response = await this.connection.sendRequest("session.resume", {
652
940
  ...await (0, import_telemetry.getTraceContext)(this.onGetTraceContext),
@@ -654,9 +942,12 @@ class CopilotClient {
654
942
  clientName: config.clientName,
655
943
  model: config.model,
656
944
  reasoningEffort: config.reasoningEffort,
945
+ reasoningSummary: config.reasoningSummary,
657
946
  systemMessage: wireSystemMessage,
658
- availableTools: config.availableTools,
659
- excludedTools: config.excludedTools,
947
+ availableTools: toolFilterOptions.availableTools,
948
+ excludedTools: toolFilterOptions.excludedTools,
949
+ toolFilterPrecedence: toolFilterOptions.toolFilterPrecedence,
950
+ enableSessionTelemetry: config.enableSessionTelemetry,
660
951
  tools: config.tools?.map((tool) => ({
661
952
  name: tool.name,
662
953
  description: tool.description,
@@ -664,56 +955,65 @@ class CopilotClient {
664
955
  overridesBuiltInTool: tool.overridesBuiltInTool,
665
956
  skipPermission: tool.skipPermission
666
957
  })),
958
+ canvases: config.canvases?.map((canvas) => canvas.declaration),
959
+ requestCanvasRenderer: config.requestCanvasRenderer,
960
+ requestExtensions: config.requestExtensions,
961
+ extensionInfo: config.extensionInfo,
667
962
  commands: config.commands?.map((cmd) => ({
668
963
  name: cmd.name,
669
964
  description: cmd.description
670
965
  })),
671
966
  provider: config.provider,
672
967
  modelCapabilities: config.modelCapabilities,
968
+ largeOutput: toWireLargeOutput(config.largeOutput),
673
969
  requestPermission: config.onPermissionRequest !== import_types.defaultJoinSessionPermissionHandler,
674
970
  requestUserInput: !!config.onUserInputRequest,
675
971
  requestElicitation: !!config.onElicitationRequest,
972
+ ...config.enableMcpApps ? { requestMcpApps: true } : {},
973
+ requestExitPlanMode: !!config.onExitPlanModeRequest,
974
+ requestAutoModeSwitch: !!config.onAutoModeSwitchRequest,
676
975
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
677
976
  workingDirectory: config.workingDirectory,
678
- configDir: config.configDir,
977
+ configDir: config.configDirectory,
679
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,
680
987
  streaming: config.streaming,
681
988
  includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
682
- mcpServers: config.mcpServers,
989
+ mcpServers: toWireMcpServers(config.mcpServers),
990
+ mcpOAuthTokenStorage: config.mcpOAuthTokenStorage,
683
991
  envValueMode: "direct",
684
- customAgents: config.customAgents,
992
+ customAgents: toWireCustomAgents(config.customAgents),
685
993
  defaultAgent: config.defaultAgent,
686
994
  agent: config.agent,
687
995
  skillDirectories: config.skillDirectories,
996
+ pluginDirectories: config.pluginDirectories,
997
+ instructionDirectories: config.instructionDirectories,
688
998
  disabledSkills: config.disabledSkills,
689
999
  infiniteSessions: config.infiniteSessions,
690
- disableResume: config.disableResume,
691
- gitHubToken: config.gitHubToken
1000
+ suppressResumeEvent: config.suppressResumeEvent,
1001
+ continuePendingWork: config.continuePendingWork,
1002
+ gitHubToken: config.gitHubToken,
1003
+ remoteSession: config.remoteSession,
1004
+ openCanvases: config.openCanvases
692
1005
  });
693
- const { workspacePath, capabilities } = response;
1006
+ const { workspacePath, capabilities, openCanvases } = response;
694
1007
  session["_workspacePath"] = workspacePath;
695
1008
  session.setCapabilities(capabilities);
1009
+ session.setOpenCanvases(openCanvases ?? []);
1010
+ await this.updateSessionOptionsForMode(session, config);
696
1011
  } catch (e) {
697
1012
  this.sessions.delete(sessionId);
698
1013
  throw e;
699
1014
  }
700
1015
  return session;
701
1016
  }
702
- /**
703
- * Gets the current connection state of the client.
704
- *
705
- * @returns The current connection state: "disconnected", "connecting", "connected", or "error"
706
- *
707
- * @example
708
- * ```typescript
709
- * if (client.getState() === "connected") {
710
- * const session = await client.createSession({ onPermissionRequest: approveAll });
711
- * }
712
- * ```
713
- */
714
- getState() {
715
- return this.state;
716
- }
717
1017
  /**
718
1018
  * Sends a ping request to the server to verify connectivity.
719
1019
  *
@@ -809,18 +1109,29 @@ class CopilotClient {
809
1109
  }
810
1110
  }
811
1111
  /**
812
- * Verify that the server's protocol version is within the supported range
813
- * and store the negotiated version.
1112
+ * Send the `connect` handshake (carrying the optional token) and verify the
1113
+ * server's protocol version. Falls back to `ping` against legacy servers
1114
+ * that don't implement `connect`.
814
1115
  */
815
1116
  async verifyProtocolVersion() {
1117
+ if (!this.connection) {
1118
+ throw new Error("Client not connected");
1119
+ }
816
1120
  const maxVersion = (0, import_sdkProtocolVersion.getSdkProtocolVersion)();
817
- let pingResult;
818
- if (this.processExitPromise) {
819
- pingResult = await Promise.race([this.ping(), this.processExitPromise]);
820
- } else {
821
- pingResult = await this.ping();
1121
+ const raceAgainstExit = (p) => this.processExitPromise ? Promise.race([p, this.processExitPromise]) : p;
1122
+ let serverVersion;
1123
+ try {
1124
+ const result = await raceAgainstExit(
1125
+ this.internalRpc.connect({ token: this.effectiveConnectionToken })
1126
+ );
1127
+ serverVersion = result.protocolVersion;
1128
+ } catch (err) {
1129
+ if (err instanceof import_node.ResponseError && (err.code === import_node.ErrorCodes.MethodNotFound || err.message === "Unhandled method connect")) {
1130
+ serverVersion = (await raceAgainstExit(this.ping())).protocolVersion;
1131
+ } else {
1132
+ throw err;
1133
+ }
822
1134
  }
823
- const serverVersion = pingResult.protocolVersion;
824
1135
  if (serverVersion === void 0) {
825
1136
  throw new Error(
826
1137
  `SDK protocol version mismatch: SDK supports versions ${MIN_PROTOCOL_VERSION}-${maxVersion}, but server does not report a protocol version. Please update your server to ensure compatibility.`
@@ -904,8 +1215,13 @@ class CopilotClient {
904
1215
  if (!this.connection) {
905
1216
  throw new Error("Client not connected");
906
1217
  }
1218
+ let wireFilter;
1219
+ if (filter) {
1220
+ const { workingDirectory, ...rest } = filter;
1221
+ wireFilter = { ...rest, cwd: workingDirectory };
1222
+ }
907
1223
  const response = await this.connection.sendRequest("session.list", {
908
- filter
1224
+ filter: wireFilter
909
1225
  });
910
1226
  const { sessions } = response;
911
1227
  return sessions.map(CopilotClient.toSessionMetadata);
@@ -940,13 +1256,19 @@ class CopilotClient {
940
1256
  return CopilotClient.toSessionMetadata(session);
941
1257
  }
942
1258
  static toSessionMetadata(raw) {
1259
+ const { context } = raw;
943
1260
  return {
944
1261
  sessionId: raw.sessionId,
945
1262
  startTime: new Date(raw.startTime),
946
1263
  modifiedTime: new Date(raw.modifiedTime),
947
1264
  summary: raw.summary,
948
1265
  isRemote: raw.isRemote,
949
- 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
950
1272
  };
951
1273
  }
952
1274
  /**
@@ -999,7 +1321,7 @@ class CopilotClient {
999
1321
  throw new Error(result.error || "Failed to set foreground session");
1000
1322
  }
1001
1323
  }
1002
- on(eventTypeOrHandler, handler) {
1324
+ onLifecycle(eventTypeOrHandler, handler) {
1003
1325
  if (typeof eventTypeOrHandler === "string" && handler) {
1004
1326
  const eventType = eventTypeOrHandler;
1005
1327
  if (!this.typedLifecycleHandlers.has(eventType)) {
@@ -1026,17 +1348,17 @@ class CopilotClient {
1026
1348
  async startCLIServer() {
1027
1349
  return new Promise((resolve, reject) => {
1028
1350
  this.stderrBuffer = "";
1029
- const args = [
1030
- ...this.options.cliArgs,
1031
- "--headless",
1032
- "--no-auto-update",
1033
- "--log-level",
1034
- this.options.logLevel
1035
- ];
1036
- 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") {
1037
1356
  args.push("--stdio");
1038
- } else if (this.options.port > 0) {
1039
- 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
+ }
1040
1362
  }
1041
1363
  if (this.options.gitHubToken) {
1042
1364
  args.push("--auth-token-env", "COPILOT_SDK_AUTH_TOKEN");
@@ -1050,14 +1372,26 @@ class CopilotClient {
1050
1372
  this.options.sessionIdleTimeoutSeconds.toString()
1051
1373
  );
1052
1374
  }
1053
- const envWithoutNodeDebug = { ...this.options.env };
1375
+ if (this.options.enableRemoteSessions) {
1376
+ args.push("--remote");
1377
+ }
1378
+ const envWithoutNodeDebug = { ...this.resolvedEnv };
1054
1379
  delete envWithoutNodeDebug.NODE_DEBUG;
1055
1380
  if (this.options.gitHubToken) {
1056
1381
  envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.gitHubToken;
1057
1382
  }
1058
- if (!this.options.cliPath) {
1383
+ if (this.effectiveConnectionToken) {
1384
+ envWithoutNodeDebug.COPILOT_CONNECTION_TOKEN = this.effectiveConnectionToken;
1385
+ }
1386
+ if (this.options.baseDirectory) {
1387
+ envWithoutNodeDebug.COPILOT_HOME = this.options.baseDirectory;
1388
+ }
1389
+ if (this.options.mode === "empty") {
1390
+ envWithoutNodeDebug.COPILOT_DISABLE_KEYTAR = "1";
1391
+ }
1392
+ if (!this.resolvedCliPath) {
1059
1393
  throw new Error(
1060
- "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."
1061
1395
  );
1062
1396
  }
1063
1397
  if (this.options.telemetry) {
@@ -1076,31 +1410,31 @@ class CopilotClient {
1076
1410
  t.captureContent
1077
1411
  );
1078
1412
  }
1079
- if (!(0, import_node_fs.existsSync)(this.options.cliPath)) {
1413
+ if (!(0, import_node_fs.existsSync)(this.resolvedCliPath)) {
1080
1414
  throw new Error(
1081
- `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.`
1082
1416
  );
1083
1417
  }
1084
- const stdioConfig = this.options.useStdio ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
1085
- 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");
1086
1420
  if (isJsFile) {
1087
- 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], {
1088
1422
  stdio: stdioConfig,
1089
- cwd: this.options.cwd,
1423
+ cwd: this.options.workingDirectory,
1090
1424
  env: envWithoutNodeDebug,
1091
1425
  windowsHide: true
1092
1426
  });
1093
1427
  } else {
1094
- this.cliProcess = (0, import_node_child_process.spawn)(this.options.cliPath, args, {
1428
+ this.cliProcess = (0, import_node_child_process.spawn)(this.resolvedCliPath, args, {
1095
1429
  stdio: stdioConfig,
1096
- cwd: this.options.cwd,
1430
+ cwd: this.options.workingDirectory,
1097
1431
  env: envWithoutNodeDebug,
1098
1432
  windowsHide: true
1099
1433
  });
1100
1434
  }
1101
1435
  let stdout = "";
1102
1436
  let resolved = false;
1103
- if (this.options.useStdio) {
1437
+ if (this.connectionConfig.kind === "stdio") {
1104
1438
  resolved = true;
1105
1439
  resolve();
1106
1440
  } else {
@@ -1108,7 +1442,7 @@ class CopilotClient {
1108
1442
  stdout += data.toString();
1109
1443
  const match = stdout.match(/listening on port (\d+)/i);
1110
1444
  if (match && !resolved) {
1111
- this.actualPort = parseInt(match[1], 10);
1445
+ this.runtimePort = parseInt(match[1], 10);
1112
1446
  resolved = true;
1113
1447
  resolve();
1114
1448
  }
@@ -1182,19 +1516,21 @@ stderr: ${stderrOutput}`
1182
1516
  resolved = true;
1183
1517
  reject(new Error("Timeout waiting for CLI server to start"));
1184
1518
  }
1185
- }, 1e4);
1519
+ }, 3e4);
1186
1520
  });
1187
1521
  }
1188
1522
  /**
1189
1523
  * Connect to the CLI server (via socket or stdio)
1190
1524
  */
1191
1525
  async connectToServer() {
1192
- if (this.options.isChildProcess) {
1193
- return this.connectToParentProcessViaStdio();
1194
- } else if (this.options.useStdio) {
1195
- return this.connectToChildProcessViaStdio();
1196
- } else {
1197
- 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();
1198
1534
  }
1199
1535
  }
1200
1536
  /**
@@ -1234,12 +1570,17 @@ stderr: ${stderrOutput}`
1234
1570
  * Connect to the CLI server via TCP socket
1235
1571
  */
1236
1572
  async connectViaTcp() {
1237
- if (!this.actualPort) {
1573
+ if (!this.runtimePort) {
1238
1574
  throw new Error("Server port not available");
1239
1575
  }
1240
1576
  return new Promise((resolve, reject) => {
1241
1577
  this.socket = new import_node_net.Socket();
1242
- 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);
1243
1584
  this.connection = (0, import_node.createMessageConnection)(
1244
1585
  new import_node.StreamMessageReader(this.socket),
1245
1586
  new import_node.StreamMessageWriter(this.socket)
@@ -1249,6 +1590,7 @@ stderr: ${stderrOutput}`
1249
1590
  resolve();
1250
1591
  });
1251
1592
  this.socket.on("error", (error) => {
1593
+ clearTimeout(connectionTimeout);
1252
1594
  reject(new Error(`Failed to connect to CLI server: ${error.message}`));
1253
1595
  });
1254
1596
  });
@@ -1264,16 +1606,16 @@ stderr: ${stderrOutput}`
1264
1606
  this.handleSessionLifecycleNotification(notification);
1265
1607
  });
1266
1608
  this.connection.onRequest(
1267
- "tool.call",
1268
- async (params) => await this.handleToolCallRequestV2(params)
1609
+ "userInput.request",
1610
+ async (params) => await this.handleUserInputRequest(params)
1269
1611
  );
1270
1612
  this.connection.onRequest(
1271
- "permission.request",
1272
- async (params) => await this.handlePermissionRequestV2(params)
1613
+ "exitPlanMode.request",
1614
+ async (params) => await this.handleExitPlanModeRequest(params)
1273
1615
  );
1274
1616
  this.connection.onRequest(
1275
- "userInput.request",
1276
- async (params) => await this.handleUserInputRequest(params)
1617
+ "autoModeSwitch.request",
1618
+ async (params) => await this.handleAutoModeSwitchRequest(params)
1277
1619
  );
1278
1620
  this.connection.onRequest(
1279
1621
  "hooks.invoke",
@@ -1309,7 +1651,20 @@ stderr: ${stderrOutput}`
1309
1651
  if (typeof notification !== "object" || !notification || !("type" in notification) || typeof notification.type !== "string" || !("sessionId" in notification) || typeof notification.sessionId !== "string") {
1310
1652
  return;
1311
1653
  }
1312
- 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
+ };
1313
1668
  const typedHandlers = this.typedLifecycleHandlers.get(event.type);
1314
1669
  if (typedHandlers) {
1315
1670
  for (const handler of typedHandlers) {
@@ -1341,125 +1696,55 @@ stderr: ${stderrOutput}`
1341
1696
  });
1342
1697
  return result;
1343
1698
  }
1344
- async handleHooksInvoke(params) {
1345
- if (!params || typeof params.sessionId !== "string" || typeof params.hookType !== "string") {
1346
- 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");
1347
1702
  }
1348
1703
  const session = this.sessions.get(params.sessionId);
1349
1704
  if (!session) {
1350
1705
  throw new Error(`Session not found: ${params.sessionId}`);
1351
1706
  }
1352
- const output = await session._handleHooksInvoke(params.hookType, params.input);
1353
- return { output };
1707
+ return await session._handleExitPlanModeRequest({
1708
+ summary: params.summary,
1709
+ planContent: params.planContent,
1710
+ actions: params.actions,
1711
+ recommendedAction: params.recommendedAction
1712
+ });
1354
1713
  }
1355
- async handleSystemMessageTransform(params) {
1356
- if (!params || typeof params.sessionId !== "string" || !params.sections || typeof params.sections !== "object") {
1357
- 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");
1358
1717
  }
1359
1718
  const session = this.sessions.get(params.sessionId);
1360
1719
  if (!session) {
1361
1720
  throw new Error(`Session not found: ${params.sessionId}`);
1362
1721
  }
1363
- return await session._handleSystemMessageTransform(params.sections);
1722
+ const response = await session._handleAutoModeSwitchRequest({
1723
+ errorCode: params.errorCode,
1724
+ retryAfterSeconds: params.retryAfterSeconds
1725
+ });
1726
+ return { response };
1364
1727
  }
1365
- // ========================================================================
1366
- // Protocol v2 backward-compatibility adapters
1367
- // ========================================================================
1368
- /**
1369
- * Handles a v2-style tool.call RPC request from the server.
1370
- * Looks up the session and tool handler, executes it, and returns the result
1371
- * in the v2 response format.
1372
- */
1373
- async handleToolCallRequestV2(params) {
1374
- if (!params || typeof params.sessionId !== "string" || typeof params.toolCallId !== "string" || typeof params.toolName !== "string") {
1375
- 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");
1376
1731
  }
1377
1732
  const session = this.sessions.get(params.sessionId);
1378
1733
  if (!session) {
1379
- throw new Error(`Unknown session ${params.sessionId}`);
1380
- }
1381
- const handler = session.getToolHandler(params.toolName);
1382
- if (!handler) {
1383
- return {
1384
- result: {
1385
- textResultForLlm: `Tool '${params.toolName}' is not supported by this client instance.`,
1386
- resultType: "failure",
1387
- error: `tool '${params.toolName}' not supported`,
1388
- toolTelemetry: {}
1389
- }
1390
- };
1391
- }
1392
- try {
1393
- const traceparent = params.traceparent;
1394
- const tracestate = params.tracestate;
1395
- const invocation = {
1396
- sessionId: params.sessionId,
1397
- toolCallId: params.toolCallId,
1398
- toolName: params.toolName,
1399
- arguments: params.arguments,
1400
- traceparent,
1401
- tracestate
1402
- };
1403
- const result = await handler(params.arguments, invocation);
1404
- return { result: this.normalizeToolResultV2(result) };
1405
- } catch (error) {
1406
- const message = error instanceof Error ? error.message : String(error);
1407
- return {
1408
- result: {
1409
- textResultForLlm: "Invoking this tool produced an error. Detailed information is not available.",
1410
- resultType: "failure",
1411
- error: message,
1412
- toolTelemetry: {}
1413
- }
1414
- };
1734
+ throw new Error(`Session not found: ${params.sessionId}`);
1415
1735
  }
1736
+ const output = await session._handleHooksInvoke(params.hookType, params.input);
1737
+ return { output };
1416
1738
  }
1417
- /**
1418
- * Handles a v2-style permission.request RPC request from the server.
1419
- */
1420
- async handlePermissionRequestV2(params) {
1421
- if (!params || typeof params.sessionId !== "string" || !params.permissionRequest) {
1422
- 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");
1423
1742
  }
1424
1743
  const session = this.sessions.get(params.sessionId);
1425
1744
  if (!session) {
1426
1745
  throw new Error(`Session not found: ${params.sessionId}`);
1427
1746
  }
1428
- try {
1429
- const result = await session._handlePermissionRequestV2(params.permissionRequest);
1430
- return { result };
1431
- } catch (error) {
1432
- if (error instanceof Error && error.message === import_session.NO_RESULT_PERMISSION_V2_ERROR) {
1433
- throw error;
1434
- }
1435
- return {
1436
- result: {
1437
- kind: "user-not-available"
1438
- }
1439
- };
1440
- }
1441
- }
1442
- normalizeToolResultV2(result) {
1443
- if (result === void 0 || result === null) {
1444
- return {
1445
- textResultForLlm: "Tool returned no result",
1446
- resultType: "failure",
1447
- error: "tool returned no result",
1448
- toolTelemetry: {}
1449
- };
1450
- }
1451
- if (this.isToolResultObject(result)) {
1452
- return result;
1453
- }
1454
- const textResult = typeof result === "string" ? result : JSON.stringify(result);
1455
- return {
1456
- textResultForLlm: textResult,
1457
- resultType: "success",
1458
- toolTelemetry: {}
1459
- };
1460
- }
1461
- isToolResultObject(value) {
1462
- return typeof value === "object" && value !== null && "textResultForLlm" in value && typeof value.textResultForLlm === "string" && "resultType" in value;
1747
+ return await session._handleSystemMessageTransform(params.sections);
1463
1748
  }
1464
1749
  }
1465
1750
  // Annotate the CommonJS export names for ESM import in node: