@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.
package/dist/client.js CHANGED
@@ -7,16 +7,23 @@ import { dirname, join } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import {
9
9
  createMessageConnection,
10
+ ErrorCodes,
11
+ ResponseError,
10
12
  StreamMessageReader,
11
13
  StreamMessageWriter
12
14
  } from "vscode-jsonrpc/node.js";
13
- import { createServerRpc, registerClientSessionApiHandlers } from "./generated/rpc.js";
15
+ import {
16
+ createServerRpc,
17
+ createInternalServerRpc,
18
+ registerClientSessionApiHandlers
19
+ } from "./generated/rpc.js";
14
20
  import { getSdkProtocolVersion } from "./sdkProtocolVersion.js";
15
- import { CopilotSession, NO_RESULT_PERMISSION_V2_ERROR } from "./session.js";
21
+ import { CopilotSession } from "./session.js";
16
22
  import { createSessionFsAdapter } from "./sessionFsProvider.js";
17
23
  import { getTraceContext } from "./telemetry.js";
24
+ import { ToolSet } from "./toolSet.js";
18
25
  import { defaultJoinSessionPermissionHandler } from "./types.js";
19
- const MIN_PROTOCOL_VERSION = 2;
26
+ const MIN_PROTOCOL_VERSION = 3;
20
27
  function isZodSchema(value) {
21
28
  return value != null && typeof value === "object" && "toJSONSchema" in value && typeof value.toJSONSchema === "function";
22
29
  }
@@ -27,6 +34,53 @@ function toJsonSchema(parameters) {
27
34
  }
28
35
  return parameters;
29
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
+ }
30
84
  function extractTransformCallbacks(systemMessage) {
31
85
  if (!systemMessage || systemMessage.mode !== "customize" || !systemMessage.sections) {
32
86
  return { wirePayload: systemMessage, transformCallbacks: void 0 };
@@ -80,15 +134,23 @@ class CopilotClient {
80
134
  cliProcess = null;
81
135
  connection = null;
82
136
  socket = null;
83
- actualPort = null;
137
+ runtimePort = null;
84
138
  actualHost = "localhost";
85
139
  state = "disconnected";
86
140
  sessions = /* @__PURE__ */ new Map();
87
141
  stderrBuffer = "";
88
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;
89
149
  options;
90
150
  isExternalServer = false;
91
151
  forceStopping = false;
152
+ /** Token sent in `connect`; auto-generated when the SDK spawns its own CLI in TCP mode. */
153
+ effectiveConnectionToken;
92
154
  onListModels;
93
155
  onGetTraceContext;
94
156
  modelsCache = null;
@@ -96,6 +158,7 @@ class CopilotClient {
96
158
  sessionLifecycleHandlers = /* @__PURE__ */ new Set();
97
159
  typedLifecycleHandlers = /* @__PURE__ */ new Map();
98
160
  _rpc = null;
161
+ _internalRpc = null;
99
162
  processExitPromise = null;
100
163
  // Rejects when CLI process exits
101
164
  negotiatedProtocolVersion = null;
@@ -114,77 +177,107 @@ class CopilotClient {
114
177
  }
115
178
  return this._rpc;
116
179
  }
180
+ /**
181
+ * Internal RPC surface (e.g. handshake helpers). Not part of the public API.
182
+ * @internal
183
+ */
184
+ get internalRpc() {
185
+ if (!this.connection) {
186
+ throw new Error("Client is not connected. Call start() first.");
187
+ }
188
+ if (!this._internalRpc) {
189
+ this._internalRpc = createInternalServerRpc(this.connection);
190
+ }
191
+ return this._internalRpc;
192
+ }
117
193
  /**
118
194
  * Creates a new CopilotClient instance.
119
195
  *
120
196
  * @param options - Configuration options for the client
121
- * @throws Error if mutually exclusive options are provided (e.g., cliUrl with useStdio or cliPath)
122
197
  *
123
198
  * @example
124
199
  * ```typescript
125
- * // Default options - spawns CLI server using stdio
200
+ * // Default: spawns the bundled runtime over stdio
126
201
  * const client = new CopilotClient();
127
202
  *
128
- * // Connect to an existing server
129
- * 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
+ * });
130
207
  *
131
- * // Custom CLI path with specific log level
208
+ * // Spawn the runtime over TCP on a chosen port
132
209
  * const client = new CopilotClient({
133
- * cliPath: "/usr/local/bin/copilot",
134
- * logLevel: "debug"
210
+ * connection: RuntimeConnection.forTcp({ port: 9001 }),
211
+ * });
212
+ *
213
+ * // Use a custom runtime binary
214
+ * const client = new CopilotClient({
215
+ * connection: RuntimeConnection.forStdio({ path: "/usr/local/bin/copilot" }),
216
+ * logLevel: "debug",
135
217
  * });
136
218
  * ```
137
219
  */
138
220
  constructor(options = {}) {
139
- if (options.cliUrl && (options.useStdio === true || options.cliPath)) {
140
- throw new Error("cliUrl is mutually exclusive with useStdio and cliPath");
141
- }
142
- if (options.isChildProcess && (options.cliUrl || options.useStdio === false)) {
221
+ const conn = options._internalConnection ?? options.connection ?? { kind: "stdio" };
222
+ if (conn.kind === "uri" && (options.gitHubToken !== void 0 || options.useLoggedInUser !== void 0)) {
143
223
  throw new Error(
144
- "isChildProcess must be used in conjunction with useStdio and not with cliUrl"
224
+ "gitHubToken and useLoggedInUser cannot be used with RuntimeConnection.forUri (external server manages its own auth)"
145
225
  );
146
226
  }
147
- if (options.cliUrl && (options.gitHubToken || options.useLoggedInUser !== void 0)) {
148
- throw new Error(
149
- "gitHubToken and useLoggedInUser cannot be used with cliUrl (external server manages its own auth)"
150
- );
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");
230
+ }
151
231
  }
232
+ this.connectionConfig = conn;
152
233
  if (options.sessionFs) {
153
234
  this.validateSessionFsConfig(options.sessionFs);
154
235
  }
155
- if (options.cliUrl) {
156
- const { host, port } = this.parseCliUrl(options.cliUrl);
236
+ if (conn.kind === "uri") {
237
+ const { host, port } = this.parseCliUrl(conn.url);
157
238
  this.actualHost = host;
158
- this.actualPort = port;
239
+ this.runtimePort = port;
159
240
  this.isExternalServer = true;
160
- }
161
- if (options.isChildProcess) {
241
+ } else if (conn.kind === "parent-process") {
162
242
  this.isExternalServer = true;
163
243
  }
244
+ if (conn.kind === "tcp") {
245
+ this.effectiveConnectionToken = conn.connectionToken ?? randomUUID();
246
+ } else if (conn.kind === "uri") {
247
+ this.effectiveConnectionToken = conn.connectionToken;
248
+ }
164
249
  this.onListModels = options.onListModels;
165
250
  this.onGetTraceContext = options.onGetTraceContext;
166
251
  this.sessionFsConfig = options.sessionFs ?? null;
167
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];
168
257
  this.options = {
169
- cliPath: options.cliUrl ? void 0 : options.cliPath || effectiveEnv.COPILOT_CLI_PATH || getBundledCliPath(),
170
- cliArgs: options.cliArgs ?? [],
171
- cwd: options.cwd ?? process.cwd(),
172
- port: options.port || 0,
173
- useStdio: options.cliUrl ? false : options.useStdio ?? true,
174
- // Default to stdio unless cliUrl is provided
175
- isChildProcess: options.isChildProcess ?? false,
176
- cliUrl: options.cliUrl,
177
- logLevel: options.logLevel || "debug",
178
- autoStart: options.autoStart ?? true,
179
- autoRestart: false,
180
- env: effectiveEnv,
258
+ workingDirectory: options.workingDirectory ?? process.cwd(),
259
+ logLevel: options.logLevel,
181
260
  gitHubToken: options.gitHubToken,
182
- // Default useLoggedInUser to false when gitHubToken is provided, otherwise true
261
+ // Default useLoggedInUser to false when gitHubToken is provided, otherwise true.
183
262
  useLoggedInUser: options.useLoggedInUser ?? (options.gitHubToken ? false : true),
184
263
  telemetry: options.telemetry,
185
- sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0
264
+ baseDirectory: options.baseDirectory,
265
+ sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0,
266
+ enableRemoteSessions: options.enableRemoteSessions ?? false,
267
+ mode: options.mode ?? "copilot-cli"
186
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
+ }
187
279
  }
280
+ connectionExtraArgs = [];
188
281
  /**
189
282
  * Parse CLI URL into host and port
190
283
  * Supports formats: "host:port", "http://host:port", "https://host:port", or just "port"
@@ -218,20 +311,37 @@ class CopilotClient {
218
311
  throw new Error("sessionFs.conventions must be either 'windows' or 'posix'");
219
312
  }
220
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
+ }
221
331
  /**
222
332
  * Starts the CLI server and establishes a connection.
223
333
  *
224
334
  * If connecting to an external server (via cliUrl), only establishes the connection.
225
335
  * Otherwise, spawns the CLI server process and then connects.
226
336
  *
227
- * 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.
228
338
  *
229
339
  * @returns A promise that resolves when the connection is established
230
340
  * @throws Error if the server fails to start or the connection fails
231
341
  *
232
342
  * @example
233
343
  * ```typescript
234
- * const client = new CopilotClient({ autoStart: false });
344
+ * const client = new CopilotClient();
235
345
  * await client.start();
236
346
  * // Now ready to create sessions
237
347
  * ```
@@ -251,7 +361,8 @@ class CopilotClient {
251
361
  await this.connection.sendRequest("sessionFs.setProvider", {
252
362
  initialCwd: this.sessionFsConfig.initialCwd,
253
363
  sessionStatePath: this.sessionFsConfig.sessionStatePath,
254
- conventions: this.sessionFsConfig.conventions
364
+ conventions: this.sessionFsConfig.conventions,
365
+ capabilities: this.sessionFsConfig.capabilities
255
366
  });
256
367
  }
257
368
  this.state = "connected";
@@ -325,8 +436,15 @@ class CopilotClient {
325
436
  }
326
437
  this.modelsCache = null;
327
438
  if (this.socket) {
439
+ const socket = this.socket;
440
+ this.socket = null;
328
441
  try {
329
- this.socket.end();
442
+ if (!socket.destroyed) {
443
+ await new Promise((resolve) => {
444
+ socket.once("close", () => resolve());
445
+ socket.end();
446
+ });
447
+ }
330
448
  } catch (error) {
331
449
  errors.push(
332
450
  new Error(
@@ -334,11 +452,18 @@ class CopilotClient {
334
452
  )
335
453
  );
336
454
  }
337
- this.socket = null;
338
455
  }
339
456
  if (this.cliProcess && !this.isExternalServer) {
457
+ const child = this.cliProcess;
458
+ this.cliProcess = null;
340
459
  try {
341
- 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
+ }
342
467
  } catch (error) {
343
468
  errors.push(
344
469
  new Error(
@@ -346,18 +471,32 @@ class CopilotClient {
346
471
  )
347
472
  );
348
473
  }
349
- this.cliProcess = null;
350
474
  }
351
475
  if (this.cliStartTimeout) {
352
476
  clearTimeout(this.cliStartTimeout);
353
477
  this.cliStartTimeout = null;
354
478
  }
355
479
  this.state = "disconnected";
356
- this.actualPort = null;
480
+ this.runtimePort = null;
357
481
  this.stderrBuffer = "";
358
482
  this.processExitPromise = null;
359
483
  return errors;
360
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
+ }
361
500
  /**
362
501
  * Forcefully stops the CLI server without graceful cleanup.
363
502
  *
@@ -414,7 +553,7 @@ class CopilotClient {
414
553
  this.cliStartTimeout = null;
415
554
  }
416
555
  this.state = "disconnected";
417
- this.actualPort = null;
556
+ this.runtimePort = null;
418
557
  this.stderrBuffer = "";
419
558
  this.processExitPromise = null;
420
559
  }
@@ -422,12 +561,11 @@ class CopilotClient {
422
561
  * Creates a new conversation session with the Copilot CLI.
423
562
  *
424
563
  * Sessions maintain conversation state, handle events, and manage tool execution.
425
- * If the client is not connected and `autoStart` is enabled, this will automatically
426
- * start the connection.
564
+ * If the client is not connected, this method automatically starts the connection.
427
565
  *
428
566
  * @param config - Optional configuration for the session
429
567
  * @returns A promise that resolves with the created session
430
- * @throws Error if the client is not connected and autoStart is disabled
568
+ * @throws Error if the client fails to start
431
569
  *
432
570
  * @example
433
571
  * ```typescript
@@ -447,66 +585,189 @@ class CopilotClient {
447
585
  * });
448
586
  * ```
449
587
  */
450
- async createSession(config) {
451
- if (!config?.onPermissionRequest) {
452
- throw new Error(
453
- "An onPermissionRequest handler is required when creating a session. For example, to allow all permissions, use { onPermissionRequest: approveAll }."
454
- );
455
- }
456
- if (!this.connection) {
457
- if (this.options.autoStart) {
458
- await this.start();
459
- } else {
460
- 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
+ );
461
611
  }
462
612
  }
463
- const sessionId = config.sessionId ?? randomUUID();
464
- const session = new CopilotSession(
465
- sessionId,
466
- this.connection,
467
- void 0,
468
- this.onGetTraceContext
469
- );
470
- session.registerTools(config.tools);
471
- session.registerCommands(config.commands);
472
- session.registerPermissionHandler(config.onPermissionRequest);
473
- if (config.onUserInputRequest) {
474
- 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
+ };
475
629
  }
476
- if (config.onElicitationRequest) {
477
- 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
+ };
478
648
  }
479
- if (config.hooks) {
480
- 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();
481
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();
482
717
  const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
483
718
  config.systemMessage
484
719
  );
485
- if (transformCallbacks) {
486
- session.registerTransformCallbacks(transformCallbacks);
487
- }
488
- if (config.onEvent) {
489
- session.on(config.onEvent);
490
- }
491
- this.sessions.set(sessionId, session);
492
- if (this.sessionFsConfig) {
493
- if (config.createSessionFsHandler) {
494
- session.clientSessionApis.sessionFs = createSessionFsAdapter(
495
- config.createSessionFsHandler(session)
496
- );
497
- } else {
498
- throw new Error(
499
- "createSessionFsHandler is required in session config when sessionFs is enabled in client options."
500
- );
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);
501
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);
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;
502
761
  }
762
+ const toolFilterOptions = this.resolveToolFilterOptions(config);
503
763
  try {
504
764
  const response = await this.connection.sendRequest("session.create", {
505
765
  ...await getTraceContext(this.onGetTraceContext),
506
766
  model: config.model,
507
- sessionId,
767
+ sessionId: localSessionId,
508
768
  clientName: config.clientName,
509
769
  reasoningEffort: config.reasoningEffort,
770
+ reasoningSummary: config.reasoningSummary,
510
771
  tools: config.tools?.map((tool) => ({
511
772
  name: tool.name,
512
773
  description: tool.description,
@@ -514,39 +775,81 @@ class CopilotClient {
514
775
  overridesBuiltInTool: tool.overridesBuiltInTool,
515
776
  skipPermission: tool.skipPermission
516
777
  })),
778
+ canvases: config.canvases?.map((canvas) => canvas.declaration),
779
+ requestCanvasRenderer: config.requestCanvasRenderer,
780
+ requestExtensions: config.requestExtensions,
781
+ extensionInfo: config.extensionInfo,
517
782
  commands: config.commands?.map((cmd) => ({
518
783
  name: cmd.name,
519
784
  description: cmd.description
520
785
  })),
521
786
  systemMessage: wireSystemMessage,
522
- availableTools: config.availableTools,
523
- excludedTools: config.excludedTools,
787
+ availableTools: toolFilterOptions.availableTools,
788
+ excludedTools: toolFilterOptions.excludedTools,
789
+ toolFilterPrecedence: toolFilterOptions.toolFilterPrecedence,
524
790
  provider: config.provider,
791
+ enableSessionTelemetry: config.enableSessionTelemetry,
525
792
  modelCapabilities: config.modelCapabilities,
526
- requestPermission: true,
793
+ largeOutput: toWireLargeOutput(config.largeOutput),
794
+ requestPermission: !!config.onPermissionRequest,
527
795
  requestUserInput: !!config.onUserInputRequest,
528
796
  requestElicitation: !!config.onElicitationRequest,
797
+ ...config.enableMcpApps ? { requestMcpApps: true } : {},
798
+ requestExitPlanMode: !!config.onExitPlanModeRequest,
799
+ requestAutoModeSwitch: !!config.onAutoModeSwitchRequest,
529
800
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
530
801
  workingDirectory: config.workingDirectory,
531
802
  streaming: config.streaming,
532
803
  includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
533
- mcpServers: config.mcpServers,
804
+ mcpServers: toWireMcpServers(config.mcpServers),
805
+ mcpOAuthTokenStorage: config.mcpOAuthTokenStorage,
534
806
  envValueMode: "direct",
535
- customAgents: config.customAgents,
807
+ customAgents: toWireCustomAgents(config.customAgents),
536
808
  defaultAgent: config.defaultAgent,
537
809
  agent: config.agent,
538
- configDir: config.configDir,
810
+ configDir: config.configDirectory,
539
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,
540
820
  skillDirectories: config.skillDirectories,
821
+ pluginDirectories: config.pluginDirectories,
822
+ instructionDirectories: config.instructionDirectories,
541
823
  disabledSkills: config.disabledSkills,
542
824
  infiniteSessions: config.infiniteSessions,
543
- gitHubToken: config.gitHubToken
825
+ gitHubToken: config.gitHubToken,
826
+ remoteSession: config.remoteSession,
827
+ cloud: config.cloud
544
828
  });
545
- 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
+ }
546
846
  session["_workspacePath"] = workspacePath;
547
847
  session.setCapabilities(capabilities);
848
+ await this.updateSessionOptionsForMode(session, config);
548
849
  } catch (e) {
549
- this.sessions.delete(sessionId);
850
+ if (registeredId !== void 0) {
851
+ this.sessions.delete(registeredId);
852
+ }
550
853
  throw e;
551
854
  }
552
855
  return session;
@@ -576,17 +879,8 @@ class CopilotClient {
576
879
  * ```
577
880
  */
578
881
  async resumeSession(sessionId, config) {
579
- if (!config?.onPermissionRequest) {
580
- throw new Error(
581
- "An onPermissionRequest handler is required when resuming a session. For example, to allow all permissions, use { onPermissionRequest: approveAll }."
582
- );
583
- }
584
882
  if (!this.connection) {
585
- if (this.options.autoStart) {
586
- await this.start();
587
- } else {
588
- throw new Error("Client not connected. Call start() first.");
589
- }
883
+ await this.start();
590
884
  }
591
885
  const session = new CopilotSession(
592
886
  sessionId,
@@ -595,6 +889,7 @@ class CopilotClient {
595
889
  this.onGetTraceContext
596
890
  );
597
891
  session.registerTools(config.tools);
892
+ session.registerCanvases(config.canvases);
598
893
  session.registerCommands(config.commands);
599
894
  session.registerPermissionHandler(config.onPermissionRequest);
600
895
  if (config.onUserInputRequest) {
@@ -603,9 +898,17 @@ class CopilotClient {
603
898
  if (config.onElicitationRequest) {
604
899
  session.registerElicitationHandler(config.onElicitationRequest);
605
900
  }
901
+ if (config.onExitPlanModeRequest) {
902
+ session.registerExitPlanModeHandler(config.onExitPlanModeRequest);
903
+ }
904
+ if (config.onAutoModeSwitchRequest) {
905
+ session.registerAutoModeSwitchHandler(config.onAutoModeSwitchRequest);
906
+ }
606
907
  if (config.hooks) {
607
908
  session.registerHooks(config.hooks);
608
909
  }
910
+ config = { ...this.configDefaultsForMode(), ...config };
911
+ config.systemMessage = this.getSystemMessageConfigForMode(config.systemMessage);
609
912
  const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
610
913
  config.systemMessage
611
914
  );
@@ -616,17 +919,8 @@ class CopilotClient {
616
919
  session.on(config.onEvent);
617
920
  }
618
921
  this.sessions.set(sessionId, session);
619
- if (this.sessionFsConfig) {
620
- if (config.createSessionFsHandler) {
621
- session.clientSessionApis.sessionFs = createSessionFsAdapter(
622
- config.createSessionFsHandler(session)
623
- );
624
- } else {
625
- throw new Error(
626
- "createSessionFsHandler is required in session config when sessionFs is enabled in client options."
627
- );
628
- }
629
- }
922
+ this.setupSessionFs(session, config);
923
+ const toolFilterOptions = this.resolveToolFilterOptions(config);
630
924
  try {
631
925
  const response = await this.connection.sendRequest("session.resume", {
632
926
  ...await getTraceContext(this.onGetTraceContext),
@@ -634,9 +928,12 @@ class CopilotClient {
634
928
  clientName: config.clientName,
635
929
  model: config.model,
636
930
  reasoningEffort: config.reasoningEffort,
931
+ reasoningSummary: config.reasoningSummary,
637
932
  systemMessage: wireSystemMessage,
638
- availableTools: config.availableTools,
639
- excludedTools: config.excludedTools,
933
+ availableTools: toolFilterOptions.availableTools,
934
+ excludedTools: toolFilterOptions.excludedTools,
935
+ toolFilterPrecedence: toolFilterOptions.toolFilterPrecedence,
936
+ enableSessionTelemetry: config.enableSessionTelemetry,
640
937
  tools: config.tools?.map((tool) => ({
641
938
  name: tool.name,
642
939
  description: tool.description,
@@ -644,56 +941,65 @@ class CopilotClient {
644
941
  overridesBuiltInTool: tool.overridesBuiltInTool,
645
942
  skipPermission: tool.skipPermission
646
943
  })),
944
+ canvases: config.canvases?.map((canvas) => canvas.declaration),
945
+ requestCanvasRenderer: config.requestCanvasRenderer,
946
+ requestExtensions: config.requestExtensions,
947
+ extensionInfo: config.extensionInfo,
647
948
  commands: config.commands?.map((cmd) => ({
648
949
  name: cmd.name,
649
950
  description: cmd.description
650
951
  })),
651
952
  provider: config.provider,
652
953
  modelCapabilities: config.modelCapabilities,
954
+ largeOutput: toWireLargeOutput(config.largeOutput),
653
955
  requestPermission: config.onPermissionRequest !== defaultJoinSessionPermissionHandler,
654
956
  requestUserInput: !!config.onUserInputRequest,
655
957
  requestElicitation: !!config.onElicitationRequest,
958
+ ...config.enableMcpApps ? { requestMcpApps: true } : {},
959
+ requestExitPlanMode: !!config.onExitPlanModeRequest,
960
+ requestAutoModeSwitch: !!config.onAutoModeSwitchRequest,
656
961
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
657
962
  workingDirectory: config.workingDirectory,
658
- configDir: config.configDir,
963
+ configDir: config.configDirectory,
659
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,
660
973
  streaming: config.streaming,
661
974
  includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
662
- mcpServers: config.mcpServers,
975
+ mcpServers: toWireMcpServers(config.mcpServers),
976
+ mcpOAuthTokenStorage: config.mcpOAuthTokenStorage,
663
977
  envValueMode: "direct",
664
- customAgents: config.customAgents,
978
+ customAgents: toWireCustomAgents(config.customAgents),
665
979
  defaultAgent: config.defaultAgent,
666
980
  agent: config.agent,
667
981
  skillDirectories: config.skillDirectories,
982
+ pluginDirectories: config.pluginDirectories,
983
+ instructionDirectories: config.instructionDirectories,
668
984
  disabledSkills: config.disabledSkills,
669
985
  infiniteSessions: config.infiniteSessions,
670
- disableResume: config.disableResume,
671
- gitHubToken: config.gitHubToken
986
+ suppressResumeEvent: config.suppressResumeEvent,
987
+ continuePendingWork: config.continuePendingWork,
988
+ gitHubToken: config.gitHubToken,
989
+ remoteSession: config.remoteSession,
990
+ openCanvases: config.openCanvases
672
991
  });
673
- const { workspacePath, capabilities } = response;
992
+ const { workspacePath, capabilities, openCanvases } = response;
674
993
  session["_workspacePath"] = workspacePath;
675
994
  session.setCapabilities(capabilities);
995
+ session.setOpenCanvases(openCanvases ?? []);
996
+ await this.updateSessionOptionsForMode(session, config);
676
997
  } catch (e) {
677
998
  this.sessions.delete(sessionId);
678
999
  throw e;
679
1000
  }
680
1001
  return session;
681
1002
  }
682
- /**
683
- * Gets the current connection state of the client.
684
- *
685
- * @returns The current connection state: "disconnected", "connecting", "connected", or "error"
686
- *
687
- * @example
688
- * ```typescript
689
- * if (client.getState() === "connected") {
690
- * const session = await client.createSession({ onPermissionRequest: approveAll });
691
- * }
692
- * ```
693
- */
694
- getState() {
695
- return this.state;
696
- }
697
1003
  /**
698
1004
  * Sends a ping request to the server to verify connectivity.
699
1005
  *
@@ -789,18 +1095,29 @@ class CopilotClient {
789
1095
  }
790
1096
  }
791
1097
  /**
792
- * Verify that the server's protocol version is within the supported range
793
- * and store the negotiated version.
1098
+ * Send the `connect` handshake (carrying the optional token) and verify the
1099
+ * server's protocol version. Falls back to `ping` against legacy servers
1100
+ * that don't implement `connect`.
794
1101
  */
795
1102
  async verifyProtocolVersion() {
1103
+ if (!this.connection) {
1104
+ throw new Error("Client not connected");
1105
+ }
796
1106
  const maxVersion = getSdkProtocolVersion();
797
- let pingResult;
798
- if (this.processExitPromise) {
799
- pingResult = await Promise.race([this.ping(), this.processExitPromise]);
800
- } else {
801
- pingResult = await this.ping();
1107
+ const raceAgainstExit = (p) => this.processExitPromise ? Promise.race([p, this.processExitPromise]) : p;
1108
+ let serverVersion;
1109
+ try {
1110
+ const result = await raceAgainstExit(
1111
+ this.internalRpc.connect({ token: this.effectiveConnectionToken })
1112
+ );
1113
+ serverVersion = result.protocolVersion;
1114
+ } catch (err) {
1115
+ if (err instanceof ResponseError && (err.code === ErrorCodes.MethodNotFound || err.message === "Unhandled method connect")) {
1116
+ serverVersion = (await raceAgainstExit(this.ping())).protocolVersion;
1117
+ } else {
1118
+ throw err;
1119
+ }
802
1120
  }
803
- const serverVersion = pingResult.protocolVersion;
804
1121
  if (serverVersion === void 0) {
805
1122
  throw new Error(
806
1123
  `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.`
@@ -884,8 +1201,13 @@ class CopilotClient {
884
1201
  if (!this.connection) {
885
1202
  throw new Error("Client not connected");
886
1203
  }
1204
+ let wireFilter;
1205
+ if (filter) {
1206
+ const { workingDirectory, ...rest } = filter;
1207
+ wireFilter = { ...rest, cwd: workingDirectory };
1208
+ }
887
1209
  const response = await this.connection.sendRequest("session.list", {
888
- filter
1210
+ filter: wireFilter
889
1211
  });
890
1212
  const { sessions } = response;
891
1213
  return sessions.map(CopilotClient.toSessionMetadata);
@@ -920,13 +1242,19 @@ class CopilotClient {
920
1242
  return CopilotClient.toSessionMetadata(session);
921
1243
  }
922
1244
  static toSessionMetadata(raw) {
1245
+ const { context } = raw;
923
1246
  return {
924
1247
  sessionId: raw.sessionId,
925
1248
  startTime: new Date(raw.startTime),
926
1249
  modifiedTime: new Date(raw.modifiedTime),
927
1250
  summary: raw.summary,
928
1251
  isRemote: raw.isRemote,
929
- 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
930
1258
  };
931
1259
  }
932
1260
  /**
@@ -979,7 +1307,7 @@ class CopilotClient {
979
1307
  throw new Error(result.error || "Failed to set foreground session");
980
1308
  }
981
1309
  }
982
- on(eventTypeOrHandler, handler) {
1310
+ onLifecycle(eventTypeOrHandler, handler) {
983
1311
  if (typeof eventTypeOrHandler === "string" && handler) {
984
1312
  const eventType = eventTypeOrHandler;
985
1313
  if (!this.typedLifecycleHandlers.has(eventType)) {
@@ -1006,17 +1334,17 @@ class CopilotClient {
1006
1334
  async startCLIServer() {
1007
1335
  return new Promise((resolve, reject) => {
1008
1336
  this.stderrBuffer = "";
1009
- const args = [
1010
- ...this.options.cliArgs,
1011
- "--headless",
1012
- "--no-auto-update",
1013
- "--log-level",
1014
- this.options.logLevel
1015
- ];
1016
- 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") {
1017
1342
  args.push("--stdio");
1018
- } else if (this.options.port > 0) {
1019
- 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
+ }
1020
1348
  }
1021
1349
  if (this.options.gitHubToken) {
1022
1350
  args.push("--auth-token-env", "COPILOT_SDK_AUTH_TOKEN");
@@ -1030,14 +1358,26 @@ class CopilotClient {
1030
1358
  this.options.sessionIdleTimeoutSeconds.toString()
1031
1359
  );
1032
1360
  }
1033
- const envWithoutNodeDebug = { ...this.options.env };
1361
+ if (this.options.enableRemoteSessions) {
1362
+ args.push("--remote");
1363
+ }
1364
+ const envWithoutNodeDebug = { ...this.resolvedEnv };
1034
1365
  delete envWithoutNodeDebug.NODE_DEBUG;
1035
1366
  if (this.options.gitHubToken) {
1036
1367
  envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.gitHubToken;
1037
1368
  }
1038
- if (!this.options.cliPath) {
1369
+ if (this.effectiveConnectionToken) {
1370
+ envWithoutNodeDebug.COPILOT_CONNECTION_TOKEN = this.effectiveConnectionToken;
1371
+ }
1372
+ if (this.options.baseDirectory) {
1373
+ envWithoutNodeDebug.COPILOT_HOME = this.options.baseDirectory;
1374
+ }
1375
+ if (this.options.mode === "empty") {
1376
+ envWithoutNodeDebug.COPILOT_DISABLE_KEYTAR = "1";
1377
+ }
1378
+ if (!this.resolvedCliPath) {
1039
1379
  throw new Error(
1040
- "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."
1041
1381
  );
1042
1382
  }
1043
1383
  if (this.options.telemetry) {
@@ -1056,31 +1396,31 @@ class CopilotClient {
1056
1396
  t.captureContent
1057
1397
  );
1058
1398
  }
1059
- if (!existsSync(this.options.cliPath)) {
1399
+ if (!existsSync(this.resolvedCliPath)) {
1060
1400
  throw new Error(
1061
- `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.`
1062
1402
  );
1063
1403
  }
1064
- const stdioConfig = this.options.useStdio ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
1065
- 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");
1066
1406
  if (isJsFile) {
1067
- this.cliProcess = spawn(getNodeExecPath(), [this.options.cliPath, ...args], {
1407
+ this.cliProcess = spawn(getNodeExecPath(), [this.resolvedCliPath, ...args], {
1068
1408
  stdio: stdioConfig,
1069
- cwd: this.options.cwd,
1409
+ cwd: this.options.workingDirectory,
1070
1410
  env: envWithoutNodeDebug,
1071
1411
  windowsHide: true
1072
1412
  });
1073
1413
  } else {
1074
- this.cliProcess = spawn(this.options.cliPath, args, {
1414
+ this.cliProcess = spawn(this.resolvedCliPath, args, {
1075
1415
  stdio: stdioConfig,
1076
- cwd: this.options.cwd,
1416
+ cwd: this.options.workingDirectory,
1077
1417
  env: envWithoutNodeDebug,
1078
1418
  windowsHide: true
1079
1419
  });
1080
1420
  }
1081
1421
  let stdout = "";
1082
1422
  let resolved = false;
1083
- if (this.options.useStdio) {
1423
+ if (this.connectionConfig.kind === "stdio") {
1084
1424
  resolved = true;
1085
1425
  resolve();
1086
1426
  } else {
@@ -1088,7 +1428,7 @@ class CopilotClient {
1088
1428
  stdout += data.toString();
1089
1429
  const match = stdout.match(/listening on port (\d+)/i);
1090
1430
  if (match && !resolved) {
1091
- this.actualPort = parseInt(match[1], 10);
1431
+ this.runtimePort = parseInt(match[1], 10);
1092
1432
  resolved = true;
1093
1433
  resolve();
1094
1434
  }
@@ -1162,19 +1502,21 @@ stderr: ${stderrOutput}`
1162
1502
  resolved = true;
1163
1503
  reject(new Error("Timeout waiting for CLI server to start"));
1164
1504
  }
1165
- }, 1e4);
1505
+ }, 3e4);
1166
1506
  });
1167
1507
  }
1168
1508
  /**
1169
1509
  * Connect to the CLI server (via socket or stdio)
1170
1510
  */
1171
1511
  async connectToServer() {
1172
- if (this.options.isChildProcess) {
1173
- return this.connectToParentProcessViaStdio();
1174
- } else if (this.options.useStdio) {
1175
- return this.connectToChildProcessViaStdio();
1176
- } else {
1177
- 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();
1178
1520
  }
1179
1521
  }
1180
1522
  /**
@@ -1214,12 +1556,17 @@ stderr: ${stderrOutput}`
1214
1556
  * Connect to the CLI server via TCP socket
1215
1557
  */
1216
1558
  async connectViaTcp() {
1217
- if (!this.actualPort) {
1559
+ if (!this.runtimePort) {
1218
1560
  throw new Error("Server port not available");
1219
1561
  }
1220
1562
  return new Promise((resolve, reject) => {
1221
1563
  this.socket = new Socket();
1222
- 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);
1223
1570
  this.connection = createMessageConnection(
1224
1571
  new StreamMessageReader(this.socket),
1225
1572
  new StreamMessageWriter(this.socket)
@@ -1229,6 +1576,7 @@ stderr: ${stderrOutput}`
1229
1576
  resolve();
1230
1577
  });
1231
1578
  this.socket.on("error", (error) => {
1579
+ clearTimeout(connectionTimeout);
1232
1580
  reject(new Error(`Failed to connect to CLI server: ${error.message}`));
1233
1581
  });
1234
1582
  });
@@ -1244,16 +1592,16 @@ stderr: ${stderrOutput}`
1244
1592
  this.handleSessionLifecycleNotification(notification);
1245
1593
  });
1246
1594
  this.connection.onRequest(
1247
- "tool.call",
1248
- async (params) => await this.handleToolCallRequestV2(params)
1595
+ "userInput.request",
1596
+ async (params) => await this.handleUserInputRequest(params)
1249
1597
  );
1250
1598
  this.connection.onRequest(
1251
- "permission.request",
1252
- async (params) => await this.handlePermissionRequestV2(params)
1599
+ "exitPlanMode.request",
1600
+ async (params) => await this.handleExitPlanModeRequest(params)
1253
1601
  );
1254
1602
  this.connection.onRequest(
1255
- "userInput.request",
1256
- async (params) => await this.handleUserInputRequest(params)
1603
+ "autoModeSwitch.request",
1604
+ async (params) => await this.handleAutoModeSwitchRequest(params)
1257
1605
  );
1258
1606
  this.connection.onRequest(
1259
1607
  "hooks.invoke",
@@ -1289,7 +1637,20 @@ stderr: ${stderrOutput}`
1289
1637
  if (typeof notification !== "object" || !notification || !("type" in notification) || typeof notification.type !== "string" || !("sessionId" in notification) || typeof notification.sessionId !== "string") {
1290
1638
  return;
1291
1639
  }
1292
- 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
+ };
1293
1654
  const typedHandlers = this.typedLifecycleHandlers.get(event.type);
1294
1655
  if (typedHandlers) {
1295
1656
  for (const handler of typedHandlers) {
@@ -1321,125 +1682,55 @@ stderr: ${stderrOutput}`
1321
1682
  });
1322
1683
  return result;
1323
1684
  }
1324
- async handleHooksInvoke(params) {
1325
- if (!params || typeof params.sessionId !== "string" || typeof params.hookType !== "string") {
1326
- 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");
1327
1688
  }
1328
1689
  const session = this.sessions.get(params.sessionId);
1329
1690
  if (!session) {
1330
1691
  throw new Error(`Session not found: ${params.sessionId}`);
1331
1692
  }
1332
- const output = await session._handleHooksInvoke(params.hookType, params.input);
1333
- return { output };
1693
+ return await session._handleExitPlanModeRequest({
1694
+ summary: params.summary,
1695
+ planContent: params.planContent,
1696
+ actions: params.actions,
1697
+ recommendedAction: params.recommendedAction
1698
+ });
1334
1699
  }
1335
- async handleSystemMessageTransform(params) {
1336
- if (!params || typeof params.sessionId !== "string" || !params.sections || typeof params.sections !== "object") {
1337
- 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");
1338
1703
  }
1339
1704
  const session = this.sessions.get(params.sessionId);
1340
1705
  if (!session) {
1341
1706
  throw new Error(`Session not found: ${params.sessionId}`);
1342
1707
  }
1343
- return await session._handleSystemMessageTransform(params.sections);
1708
+ const response = await session._handleAutoModeSwitchRequest({
1709
+ errorCode: params.errorCode,
1710
+ retryAfterSeconds: params.retryAfterSeconds
1711
+ });
1712
+ return { response };
1344
1713
  }
1345
- // ========================================================================
1346
- // Protocol v2 backward-compatibility adapters
1347
- // ========================================================================
1348
- /**
1349
- * Handles a v2-style tool.call RPC request from the server.
1350
- * Looks up the session and tool handler, executes it, and returns the result
1351
- * in the v2 response format.
1352
- */
1353
- async handleToolCallRequestV2(params) {
1354
- if (!params || typeof params.sessionId !== "string" || typeof params.toolCallId !== "string" || typeof params.toolName !== "string") {
1355
- 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");
1356
1717
  }
1357
1718
  const session = this.sessions.get(params.sessionId);
1358
1719
  if (!session) {
1359
- throw new Error(`Unknown session ${params.sessionId}`);
1360
- }
1361
- const handler = session.getToolHandler(params.toolName);
1362
- if (!handler) {
1363
- return {
1364
- result: {
1365
- textResultForLlm: `Tool '${params.toolName}' is not supported by this client instance.`,
1366
- resultType: "failure",
1367
- error: `tool '${params.toolName}' not supported`,
1368
- toolTelemetry: {}
1369
- }
1370
- };
1371
- }
1372
- try {
1373
- const traceparent = params.traceparent;
1374
- const tracestate = params.tracestate;
1375
- const invocation = {
1376
- sessionId: params.sessionId,
1377
- toolCallId: params.toolCallId,
1378
- toolName: params.toolName,
1379
- arguments: params.arguments,
1380
- traceparent,
1381
- tracestate
1382
- };
1383
- const result = await handler(params.arguments, invocation);
1384
- return { result: this.normalizeToolResultV2(result) };
1385
- } catch (error) {
1386
- const message = error instanceof Error ? error.message : String(error);
1387
- return {
1388
- result: {
1389
- textResultForLlm: "Invoking this tool produced an error. Detailed information is not available.",
1390
- resultType: "failure",
1391
- error: message,
1392
- toolTelemetry: {}
1393
- }
1394
- };
1720
+ throw new Error(`Session not found: ${params.sessionId}`);
1395
1721
  }
1722
+ const output = await session._handleHooksInvoke(params.hookType, params.input);
1723
+ return { output };
1396
1724
  }
1397
- /**
1398
- * Handles a v2-style permission.request RPC request from the server.
1399
- */
1400
- async handlePermissionRequestV2(params) {
1401
- if (!params || typeof params.sessionId !== "string" || !params.permissionRequest) {
1402
- 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");
1403
1728
  }
1404
1729
  const session = this.sessions.get(params.sessionId);
1405
1730
  if (!session) {
1406
1731
  throw new Error(`Session not found: ${params.sessionId}`);
1407
1732
  }
1408
- try {
1409
- const result = await session._handlePermissionRequestV2(params.permissionRequest);
1410
- return { result };
1411
- } catch (error) {
1412
- if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) {
1413
- throw error;
1414
- }
1415
- return {
1416
- result: {
1417
- kind: "user-not-available"
1418
- }
1419
- };
1420
- }
1421
- }
1422
- normalizeToolResultV2(result) {
1423
- if (result === void 0 || result === null) {
1424
- return {
1425
- textResultForLlm: "Tool returned no result",
1426
- resultType: "failure",
1427
- error: "tool returned no result",
1428
- toolTelemetry: {}
1429
- };
1430
- }
1431
- if (this.isToolResultObject(result)) {
1432
- return result;
1433
- }
1434
- const textResult = typeof result === "string" ? result : JSON.stringify(result);
1435
- return {
1436
- textResultForLlm: textResult,
1437
- resultType: "success",
1438
- toolTelemetry: {}
1439
- };
1440
- }
1441
- isToolResultObject(value) {
1442
- return typeof value === "object" && value !== null && "textResultForLlm" in value && typeof value.textResultForLlm === "string" && "resultType" in value;
1733
+ return await session._handleSystemMessageTransform(params.sections);
1443
1734
  }
1444
1735
  }
1445
1736
  export {