@github/copilot-sdk 0.2.1-preview.1 → 0.2.1-preview.2

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/README.md CHANGED
@@ -120,6 +120,7 @@ Create a new conversation session.
120
120
  - `provider?: ProviderConfig` - Custom API provider configuration (BYOK - Bring Your Own Key). See [Custom Providers](#custom-providers) section.
121
121
  - `onPermissionRequest: PermissionHandler` - **Required.** Handler called before each tool execution to approve or deny it. Use `approveAll` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section.
122
122
  - `onUserInputRequest?: UserInputHandler` - Handler for user input requests from the agent. Enables the `ask_user` tool. See [User Input Requests](#user-input-requests) section.
123
+ - `onElicitationRequest?: ElicitationHandler` - Handler for elicitation requests dispatched by the server. Enables this client to present form-based UI dialogs on behalf of the agent or other session participants. See [Elicitation Requests](#elicitation-requests) section.
123
124
  - `hooks?: SessionHooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
124
125
 
125
126
  ##### `resumeSession(sessionId: string, config?: ResumeSessionConfig): Promise<CopilotSession>`
@@ -293,6 +294,8 @@ if (session.capabilities.ui?.elicitation) {
293
294
  }
294
295
  ```
295
296
 
297
+ Capabilities may update during the session. For example, when another client joins or disconnects with an elicitation handler. The SDK automatically applies `capabilities.changed` events, so this property always reflects the current state.
298
+
296
299
  ##### `ui: SessionUiApi`
297
300
 
298
301
  Interactive UI methods for showing dialogs to the user. Only available when the CLI host supports elicitation (`session.capabilities.ui?.elicitation === true`). See [UI Elicitation](#ui-elicitation) for full details.
@@ -505,9 +508,9 @@ Commands are sent to the CLI on both `createSession` and `resumeSession`, so you
505
508
 
506
509
  ### UI Elicitation
507
510
 
508
- When the CLI is running with a TUI (not in headless mode), the SDK can request interactive form dialogs from the user. The `session.ui` object provides convenience methods built on a single generic `elicitation` RPC.
511
+ When the session has elicitation support either from the CLI's TUI or from another client that registered an `onElicitationRequest` handler (see [Elicitation Requests](#elicitation-requests)) the SDK can request interactive form dialogs from the user. The `session.ui` object provides convenience methods built on a single generic `elicitation` RPC.
509
512
 
510
- > **Capability check:** Elicitation is only available when the host advertises support. Always check `session.capabilities.ui?.elicitation` before calling UI methods.
513
+ > **Capability check:** Elicitation is only available when at least one connected participant advertises support. Always check `session.capabilities.ui?.elicitation` before calling UI methods — this property updates automatically as participants join and leave.
511
514
 
512
515
  ```ts
513
516
  const session = await client.createSession({ onPermissionRequest: approveAll });
@@ -899,6 +902,42 @@ const session = await client.createSession({
899
902
  });
900
903
  ```
901
904
 
905
+ ## Elicitation Requests
906
+
907
+ Register an `onElicitationRequest` handler to let your client act as an elicitation provider — presenting form-based UI dialogs on behalf of the agent. When provided, the server notifies your client whenever a tool or MCP server needs structured user input.
908
+
909
+ ```typescript
910
+ const session = await client.createSession({
911
+ model: "gpt-5",
912
+ onPermissionRequest: approveAll,
913
+ onElicitationRequest: async (request, invocation) => {
914
+ // request.message - Description of what information is needed
915
+ // request.requestedSchema - JSON Schema describing the form fields
916
+ // request.mode - "form" (structured input) or "url" (browser redirect)
917
+ // request.elicitationSource - Origin of the request (e.g. MCP server name)
918
+
919
+ console.log(`Elicitation from ${request.elicitationSource}: ${request.message}`);
920
+
921
+ // Present UI to the user and collect their response...
922
+ return {
923
+ action: "accept", // "accept", "decline", or "cancel"
924
+ content: { region: "us-east", dryRun: true },
925
+ };
926
+ },
927
+ });
928
+
929
+ // The session now reports elicitation capability
930
+ console.log(session.capabilities.ui?.elicitation); // true
931
+ ```
932
+
933
+ When `onElicitationRequest` is provided, the SDK sends `requestElicitation: true` during session create/resume, which enables `session.capabilities.ui.elicitation` on the session.
934
+
935
+ In multi-client scenarios:
936
+
937
+ - If no connected client was previously providing an elicitation capability, but a new client joins that can, all clients will receive a `capabilities.changed` event to notify them that elicitation is now possible. The SDK automatically updates `session.capabilities` when these events arrive.
938
+ - Similarly, if the last elicitation provider disconnects, all clients receive a `capabilities.changed` event indicating elicitation is no longer available.
939
+ - The server fans out elicitation requests to **all** connected clients that registered a handler — the first response wins.
940
+
902
941
  ## Session Hooks
903
942
 
904
943
  Hook into session lifecycle events by providing handlers in the `hooks` configuration:
@@ -116,6 +116,8 @@ class CopilotClient {
116
116
  processExitPromise = null;
117
117
  // Rejects when CLI process exits
118
118
  negotiatedProtocolVersion = null;
119
+ /** Connection-level session filesystem config, set via constructor option. */
120
+ sessionFsConfig = null;
119
121
  /**
120
122
  * Typed server-scoped RPC methods.
121
123
  * @throws Error if the client is not connected
@@ -175,6 +177,7 @@ class CopilotClient {
175
177
  }
176
178
  this.onListModels = options.onListModels;
177
179
  this.onGetTraceContext = options.onGetTraceContext;
180
+ this.sessionFsConfig = options.sessionFs ?? null;
178
181
  const effectiveEnv = options.env ?? process.env;
179
182
  this.options = {
180
183
  cliPath: options.cliUrl ? void 0 : options.cliPath || effectiveEnv.COPILOT_CLI_PATH || getBundledCliPath(),
@@ -246,6 +249,13 @@ class CopilotClient {
246
249
  }
247
250
  await this.connectToServer();
248
251
  await this.verifyProtocolVersion();
252
+ if (this.sessionFsConfig) {
253
+ await this.connection.sendRequest("sessionFs.setProvider", {
254
+ initialCwd: this.sessionFsConfig.initialCwd,
255
+ sessionStatePath: this.sessionFsConfig.sessionStatePath,
256
+ conventions: this.sessionFsConfig.conventions
257
+ });
258
+ }
249
259
  this.state = "connected";
250
260
  } catch (error) {
251
261
  this.state = "error";
@@ -457,6 +467,9 @@ class CopilotClient {
457
467
  if (config.onUserInputRequest) {
458
468
  session.registerUserInputHandler(config.onUserInputRequest);
459
469
  }
470
+ if (config.onElicitationRequest) {
471
+ session.registerElicitationHandler(config.onElicitationRequest);
472
+ }
460
473
  if (config.hooks) {
461
474
  session.registerHooks(config.hooks);
462
475
  }
@@ -470,6 +483,15 @@ class CopilotClient {
470
483
  session.on(config.onEvent);
471
484
  }
472
485
  this.sessions.set(sessionId, session);
486
+ if (this.sessionFsConfig) {
487
+ if (config.createSessionFsHandler) {
488
+ session.clientSessionApis.sessionFs = config.createSessionFsHandler(session);
489
+ } else {
490
+ throw new Error(
491
+ "createSessionFsHandler is required in session config when sessionFs is enabled in client options."
492
+ );
493
+ }
494
+ }
473
495
  try {
474
496
  const response = await this.connection.sendRequest("session.create", {
475
497
  ...await (0, import_telemetry.getTraceContext)(this.onGetTraceContext),
@@ -494,6 +516,7 @@ class CopilotClient {
494
516
  provider: config.provider,
495
517
  requestPermission: true,
496
518
  requestUserInput: !!config.onUserInputRequest,
519
+ requestElicitation: !!config.onElicitationRequest,
497
520
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
498
521
  workingDirectory: config.workingDirectory,
499
522
  streaming: config.streaming,
@@ -564,6 +587,9 @@ class CopilotClient {
564
587
  if (config.onUserInputRequest) {
565
588
  session.registerUserInputHandler(config.onUserInputRequest);
566
589
  }
590
+ if (config.onElicitationRequest) {
591
+ session.registerElicitationHandler(config.onElicitationRequest);
592
+ }
567
593
  if (config.hooks) {
568
594
  session.registerHooks(config.hooks);
569
595
  }
@@ -577,6 +603,15 @@ class CopilotClient {
577
603
  session.on(config.onEvent);
578
604
  }
579
605
  this.sessions.set(sessionId, session);
606
+ if (this.sessionFsConfig) {
607
+ if (config.createSessionFsHandler) {
608
+ session.clientSessionApis.sessionFs = config.createSessionFsHandler(session);
609
+ } else {
610
+ throw new Error(
611
+ "createSessionFsHandler is required in session config when sessionFs is enabled in client options."
612
+ );
613
+ }
614
+ }
580
615
  try {
581
616
  const response = await this.connection.sendRequest("session.resume", {
582
617
  ...await (0, import_telemetry.getTraceContext)(this.onGetTraceContext),
@@ -601,6 +636,7 @@ class CopilotClient {
601
636
  provider: config.provider,
602
637
  requestPermission: true,
603
638
  requestUserInput: !!config.onUserInputRequest,
639
+ requestElicitation: !!config.onElicitationRequest,
604
640
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
605
641
  workingDirectory: config.workingDirectory,
606
642
  configDir: config.configDir,
@@ -812,16 +848,50 @@ class CopilotClient {
812
848
  if (!this.connection) {
813
849
  throw new Error("Client not connected");
814
850
  }
815
- const response = await this.connection.sendRequest("session.list", { filter });
851
+ const response = await this.connection.sendRequest("session.list", {
852
+ filter
853
+ });
816
854
  const { sessions } = response;
817
- return sessions.map((s) => ({
818
- sessionId: s.sessionId,
819
- startTime: new Date(s.startTime),
820
- modifiedTime: new Date(s.modifiedTime),
821
- summary: s.summary,
822
- isRemote: s.isRemote,
823
- context: s.context
824
- }));
855
+ return sessions.map(CopilotClient.toSessionMetadata);
856
+ }
857
+ /**
858
+ * Gets metadata for a specific session by ID.
859
+ *
860
+ * This provides an efficient O(1) lookup of a single session's metadata
861
+ * instead of listing all sessions. Returns undefined if the session is not found.
862
+ *
863
+ * @param sessionId - The ID of the session to look up
864
+ * @returns A promise that resolves with the session metadata, or undefined if not found
865
+ * @throws Error if the client is not connected
866
+ *
867
+ * @example
868
+ * ```typescript
869
+ * const metadata = await client.getSessionMetadata("session-123");
870
+ * if (metadata) {
871
+ * console.log(`Session started at: ${metadata.startTime}`);
872
+ * }
873
+ * ```
874
+ */
875
+ async getSessionMetadata(sessionId) {
876
+ if (!this.connection) {
877
+ throw new Error("Client not connected");
878
+ }
879
+ const response = await this.connection.sendRequest("session.getMetadata", { sessionId });
880
+ const { session } = response;
881
+ if (!session) {
882
+ return void 0;
883
+ }
884
+ return CopilotClient.toSessionMetadata(session);
885
+ }
886
+ static toSessionMetadata(raw) {
887
+ return {
888
+ sessionId: raw.sessionId,
889
+ startTime: new Date(raw.startTime),
890
+ modifiedTime: new Date(raw.modifiedTime),
891
+ summary: raw.summary,
892
+ isRemote: raw.isRemote,
893
+ context: raw.context
894
+ };
825
895
  }
826
896
  /**
827
897
  * Gets the foreground session ID in TUI+server mode.
@@ -1151,6 +1221,12 @@ stderr: ${stderrOutput}`
1151
1221
  "systemMessage.transform",
1152
1222
  async (params) => await this.handleSystemMessageTransform(params)
1153
1223
  );
1224
+ const sessions = this.sessions;
1225
+ (0, import_rpc.registerClientSessionApiHandlers)(this.connection, (sessionId) => {
1226
+ const session = sessions.get(sessionId);
1227
+ if (!session) throw new Error(`No session found for sessionId: ${sessionId}`);
1228
+ return session.clientSessionApis;
1229
+ });
1154
1230
  this.connection.onClose(() => {
1155
1231
  this.state = "disconnected";
1156
1232
  });
@@ -19,7 +19,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  var rpc_exports = {};
20
20
  __export(rpc_exports, {
21
21
  createServerRpc: () => createServerRpc,
22
- createSessionRpc: () => createSessionRpc
22
+ createSessionRpc: () => createSessionRpc,
23
+ registerClientSessionApiHandlers: () => registerClientSessionApiHandlers
23
24
  });
24
25
  module.exports = __toCommonJS(rpc_exports);
25
26
  function createServerRpc(connection) {
@@ -33,6 +34,17 @@ function createServerRpc(connection) {
33
34
  },
34
35
  account: {
35
36
  getQuota: async () => connection.sendRequest("account.getQuota", {})
37
+ },
38
+ mcp: {
39
+ config: {
40
+ list: async () => connection.sendRequest("mcp.config.list", {}),
41
+ add: async (params) => connection.sendRequest("mcp.config.add", params),
42
+ update: async (params) => connection.sendRequest("mcp.config.update", params),
43
+ remove: async (params) => connection.sendRequest("mcp.config.remove", params)
44
+ }
45
+ },
46
+ sessionFs: {
47
+ setProvider: async (params) => connection.sendRequest("sessionFs.setProvider", params)
36
48
  }
37
49
  };
38
50
  }
@@ -104,7 +116,8 @@ function createSessionRpc(connection, sessionId) {
104
116
  handlePendingCommand: async (params) => connection.sendRequest("session.commands.handlePendingCommand", { sessionId, ...params })
105
117
  },
106
118
  ui: {
107
- elicitation: async (params) => connection.sendRequest("session.ui.elicitation", { sessionId, ...params })
119
+ elicitation: async (params) => connection.sendRequest("session.ui.elicitation", { sessionId, ...params }),
120
+ handlePendingElicitation: async (params) => connection.sendRequest("session.ui.handlePendingElicitation", { sessionId, ...params })
108
121
  },
109
122
  permissions: {
110
123
  handlePendingPermissionRequest: async (params) => connection.sendRequest("session.permissions.handlePendingPermissionRequest", { sessionId, ...params })
@@ -116,8 +129,61 @@ function createSessionRpc(connection, sessionId) {
116
129
  }
117
130
  };
118
131
  }
132
+ function registerClientSessionApiHandlers(connection, getHandlers) {
133
+ connection.onRequest("sessionFs.readFile", async (params) => {
134
+ const handler = getHandlers(params.sessionId).sessionFs;
135
+ if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`);
136
+ return handler.readFile(params);
137
+ });
138
+ connection.onRequest("sessionFs.writeFile", async (params) => {
139
+ const handler = getHandlers(params.sessionId).sessionFs;
140
+ if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`);
141
+ return handler.writeFile(params);
142
+ });
143
+ connection.onRequest("sessionFs.appendFile", async (params) => {
144
+ const handler = getHandlers(params.sessionId).sessionFs;
145
+ if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`);
146
+ return handler.appendFile(params);
147
+ });
148
+ connection.onRequest("sessionFs.exists", async (params) => {
149
+ const handler = getHandlers(params.sessionId).sessionFs;
150
+ if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`);
151
+ return handler.exists(params);
152
+ });
153
+ connection.onRequest("sessionFs.stat", async (params) => {
154
+ const handler = getHandlers(params.sessionId).sessionFs;
155
+ if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`);
156
+ return handler.stat(params);
157
+ });
158
+ connection.onRequest("sessionFs.mkdir", async (params) => {
159
+ const handler = getHandlers(params.sessionId).sessionFs;
160
+ if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`);
161
+ return handler.mkdir(params);
162
+ });
163
+ connection.onRequest("sessionFs.readdir", async (params) => {
164
+ const handler = getHandlers(params.sessionId).sessionFs;
165
+ if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`);
166
+ return handler.readdir(params);
167
+ });
168
+ connection.onRequest("sessionFs.readdirWithTypes", async (params) => {
169
+ const handler = getHandlers(params.sessionId).sessionFs;
170
+ if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`);
171
+ return handler.readdirWithTypes(params);
172
+ });
173
+ connection.onRequest("sessionFs.rm", async (params) => {
174
+ const handler = getHandlers(params.sessionId).sessionFs;
175
+ if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`);
176
+ return handler.rm(params);
177
+ });
178
+ connection.onRequest("sessionFs.rename", async (params) => {
179
+ const handler = getHandlers(params.sessionId).sessionFs;
180
+ if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`);
181
+ return handler.rename(params);
182
+ });
183
+ }
119
184
  // Annotate the CommonJS export names for ESM import in node:
120
185
  0 && (module.exports = {
121
186
  createServerRpc,
122
- createSessionRpc
187
+ createSessionRpc,
188
+ registerClientSessionApiHandlers
123
189
  });
@@ -48,11 +48,14 @@ class CopilotSession {
48
48
  commandHandlers = /* @__PURE__ */ new Map();
49
49
  permissionHandler;
50
50
  userInputHandler;
51
+ elicitationHandler;
51
52
  hooks;
52
53
  transformCallbacks;
53
54
  _rpc = null;
54
55
  traceContextProvider;
55
56
  _capabilities = {};
57
+ /** @internal Client session API handlers, populated by CopilotClient during create/resume. */
58
+ clientSessionApis = {};
56
59
  /**
57
60
  * Typed session-scoped RPC methods.
58
61
  */
@@ -269,6 +272,22 @@ class CopilotSession {
269
272
  } else if (event.type === "command.execute") {
270
273
  const { requestId, commandName, command, args } = event.data;
271
274
  void this._executeCommandAndRespond(requestId, commandName, command, args);
275
+ } else if (event.type === "elicitation.requested") {
276
+ if (this.elicitationHandler) {
277
+ const { message, requestedSchema, mode, elicitationSource, url, requestId } = event.data;
278
+ void this._handleElicitationRequest(
279
+ {
280
+ message,
281
+ requestedSchema,
282
+ mode,
283
+ elicitationSource,
284
+ url
285
+ },
286
+ requestId
287
+ );
288
+ }
289
+ } else if (event.type === "capabilities.changed") {
290
+ this._capabilities = { ...this._capabilities, ...event.data };
272
291
  }
273
292
  }
274
293
  /**
@@ -290,6 +309,8 @@ class CopilotSession {
290
309
  result = "";
291
310
  } else if (typeof rawResult === "string") {
292
311
  result = rawResult;
312
+ } else if (isToolResultObject(rawResult)) {
313
+ result = rawResult;
293
314
  } else {
294
315
  result = JSON.stringify(rawResult);
295
316
  }
@@ -409,6 +430,40 @@ class CopilotSession {
409
430
  this.commandHandlers.set(cmd.name, cmd.handler);
410
431
  }
411
432
  }
433
+ /**
434
+ * Registers the elicitation handler for this session.
435
+ *
436
+ * @param handler - The handler to invoke when the server dispatches an elicitation request
437
+ * @internal This method is typically called internally when creating/resuming a session.
438
+ */
439
+ registerElicitationHandler(handler) {
440
+ this.elicitationHandler = handler;
441
+ }
442
+ /**
443
+ * Handles an elicitation.requested broadcast event.
444
+ * Invokes the registered handler and responds via handlePendingElicitation RPC.
445
+ * @internal
446
+ */
447
+ async _handleElicitationRequest(request, requestId) {
448
+ if (!this.elicitationHandler) {
449
+ return;
450
+ }
451
+ try {
452
+ const result = await this.elicitationHandler(request, { sessionId: this.sessionId });
453
+ await this.rpc.ui.handlePendingElicitation({ requestId, result });
454
+ } catch {
455
+ try {
456
+ await this.rpc.ui.handlePendingElicitation({
457
+ requestId,
458
+ result: { action: "cancel" }
459
+ });
460
+ } catch (rpcError) {
461
+ if (!(rpcError instanceof import_node.ConnectionError || rpcError instanceof import_node.ResponseError)) {
462
+ throw rpcError;
463
+ }
464
+ }
465
+ }
466
+ }
412
467
  /**
413
468
  * Sets the host capabilities for this session.
414
469
  *
@@ -767,6 +822,25 @@ class CopilotSession {
767
822
  await this.rpc.log({ message, ...options });
768
823
  }
769
824
  }
825
+ function isToolResultObject(value) {
826
+ if (typeof value !== "object" || value === null) {
827
+ return false;
828
+ }
829
+ if (!("textResultForLlm" in value) || typeof value.textResultForLlm !== "string") {
830
+ return false;
831
+ }
832
+ if (!("resultType" in value) || typeof value.resultType !== "string") {
833
+ return false;
834
+ }
835
+ const allowedResultTypes = [
836
+ "success",
837
+ "failure",
838
+ "rejected",
839
+ "denied",
840
+ "timeout"
841
+ ];
842
+ return allowedResultTypes.includes(value.resultType);
843
+ }
770
844
  // Annotate the CommonJS export names for ESM import in node:
771
845
  0 && (module.exports = {
772
846
  CopilotSession,
package/dist/client.d.ts CHANGED
@@ -55,6 +55,8 @@ export declare class CopilotClient {
55
55
  private _rpc;
56
56
  private processExitPromise;
57
57
  private negotiatedProtocolVersion;
58
+ /** Connection-level session filesystem config, set via constructor option. */
59
+ private sessionFsConfig;
58
60
  /**
59
61
  * Typed server-scoped RPC methods.
60
62
  * @throws Error if the client is not connected
@@ -317,6 +319,26 @@ export declare class CopilotClient {
317
319
  * const sessions = await client.listSessions({ repository: "owner/repo" });
318
320
  */
319
321
  listSessions(filter?: SessionListFilter): Promise<SessionMetadata[]>;
322
+ /**
323
+ * Gets metadata for a specific session by ID.
324
+ *
325
+ * This provides an efficient O(1) lookup of a single session's metadata
326
+ * instead of listing all sessions. Returns undefined if the session is not found.
327
+ *
328
+ * @param sessionId - The ID of the session to look up
329
+ * @returns A promise that resolves with the session metadata, or undefined if not found
330
+ * @throws Error if the client is not connected
331
+ *
332
+ * @example
333
+ * ```typescript
334
+ * const metadata = await client.getSessionMetadata("session-123");
335
+ * if (metadata) {
336
+ * console.log(`Session started at: ${metadata.startTime}`);
337
+ * }
338
+ * ```
339
+ */
340
+ getSessionMetadata(sessionId: string): Promise<SessionMetadata | undefined>;
341
+ private static toSessionMetadata;
320
342
  /**
321
343
  * Gets the foreground session ID in TUI+server mode.
322
344
  *
package/dist/client.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  StreamMessageReader,
11
11
  StreamMessageWriter
12
12
  } from "vscode-jsonrpc/node.js";
13
- import { createServerRpc } from "./generated/rpc.js";
13
+ import { createServerRpc, registerClientSessionApiHandlers } from "./generated/rpc.js";
14
14
  import { getSdkProtocolVersion } from "./sdkProtocolVersion.js";
15
15
  import { CopilotSession, NO_RESULT_PERMISSION_V2_ERROR } from "./session.js";
16
16
  import { getTraceContext } from "./telemetry.js";
@@ -96,6 +96,8 @@ class CopilotClient {
96
96
  processExitPromise = null;
97
97
  // Rejects when CLI process exits
98
98
  negotiatedProtocolVersion = null;
99
+ /** Connection-level session filesystem config, set via constructor option. */
100
+ sessionFsConfig = null;
99
101
  /**
100
102
  * Typed server-scoped RPC methods.
101
103
  * @throws Error if the client is not connected
@@ -155,6 +157,7 @@ class CopilotClient {
155
157
  }
156
158
  this.onListModels = options.onListModels;
157
159
  this.onGetTraceContext = options.onGetTraceContext;
160
+ this.sessionFsConfig = options.sessionFs ?? null;
158
161
  const effectiveEnv = options.env ?? process.env;
159
162
  this.options = {
160
163
  cliPath: options.cliUrl ? void 0 : options.cliPath || effectiveEnv.COPILOT_CLI_PATH || getBundledCliPath(),
@@ -226,6 +229,13 @@ class CopilotClient {
226
229
  }
227
230
  await this.connectToServer();
228
231
  await this.verifyProtocolVersion();
232
+ if (this.sessionFsConfig) {
233
+ await this.connection.sendRequest("sessionFs.setProvider", {
234
+ initialCwd: this.sessionFsConfig.initialCwd,
235
+ sessionStatePath: this.sessionFsConfig.sessionStatePath,
236
+ conventions: this.sessionFsConfig.conventions
237
+ });
238
+ }
229
239
  this.state = "connected";
230
240
  } catch (error) {
231
241
  this.state = "error";
@@ -437,6 +447,9 @@ class CopilotClient {
437
447
  if (config.onUserInputRequest) {
438
448
  session.registerUserInputHandler(config.onUserInputRequest);
439
449
  }
450
+ if (config.onElicitationRequest) {
451
+ session.registerElicitationHandler(config.onElicitationRequest);
452
+ }
440
453
  if (config.hooks) {
441
454
  session.registerHooks(config.hooks);
442
455
  }
@@ -450,6 +463,15 @@ class CopilotClient {
450
463
  session.on(config.onEvent);
451
464
  }
452
465
  this.sessions.set(sessionId, session);
466
+ if (this.sessionFsConfig) {
467
+ if (config.createSessionFsHandler) {
468
+ session.clientSessionApis.sessionFs = config.createSessionFsHandler(session);
469
+ } else {
470
+ throw new Error(
471
+ "createSessionFsHandler is required in session config when sessionFs is enabled in client options."
472
+ );
473
+ }
474
+ }
453
475
  try {
454
476
  const response = await this.connection.sendRequest("session.create", {
455
477
  ...await getTraceContext(this.onGetTraceContext),
@@ -474,6 +496,7 @@ class CopilotClient {
474
496
  provider: config.provider,
475
497
  requestPermission: true,
476
498
  requestUserInput: !!config.onUserInputRequest,
499
+ requestElicitation: !!config.onElicitationRequest,
477
500
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
478
501
  workingDirectory: config.workingDirectory,
479
502
  streaming: config.streaming,
@@ -544,6 +567,9 @@ class CopilotClient {
544
567
  if (config.onUserInputRequest) {
545
568
  session.registerUserInputHandler(config.onUserInputRequest);
546
569
  }
570
+ if (config.onElicitationRequest) {
571
+ session.registerElicitationHandler(config.onElicitationRequest);
572
+ }
547
573
  if (config.hooks) {
548
574
  session.registerHooks(config.hooks);
549
575
  }
@@ -557,6 +583,15 @@ class CopilotClient {
557
583
  session.on(config.onEvent);
558
584
  }
559
585
  this.sessions.set(sessionId, session);
586
+ if (this.sessionFsConfig) {
587
+ if (config.createSessionFsHandler) {
588
+ session.clientSessionApis.sessionFs = config.createSessionFsHandler(session);
589
+ } else {
590
+ throw new Error(
591
+ "createSessionFsHandler is required in session config when sessionFs is enabled in client options."
592
+ );
593
+ }
594
+ }
560
595
  try {
561
596
  const response = await this.connection.sendRequest("session.resume", {
562
597
  ...await getTraceContext(this.onGetTraceContext),
@@ -581,6 +616,7 @@ class CopilotClient {
581
616
  provider: config.provider,
582
617
  requestPermission: true,
583
618
  requestUserInput: !!config.onUserInputRequest,
619
+ requestElicitation: !!config.onElicitationRequest,
584
620
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
585
621
  workingDirectory: config.workingDirectory,
586
622
  configDir: config.configDir,
@@ -792,16 +828,50 @@ class CopilotClient {
792
828
  if (!this.connection) {
793
829
  throw new Error("Client not connected");
794
830
  }
795
- const response = await this.connection.sendRequest("session.list", { filter });
831
+ const response = await this.connection.sendRequest("session.list", {
832
+ filter
833
+ });
796
834
  const { sessions } = response;
797
- return sessions.map((s) => ({
798
- sessionId: s.sessionId,
799
- startTime: new Date(s.startTime),
800
- modifiedTime: new Date(s.modifiedTime),
801
- summary: s.summary,
802
- isRemote: s.isRemote,
803
- context: s.context
804
- }));
835
+ return sessions.map(CopilotClient.toSessionMetadata);
836
+ }
837
+ /**
838
+ * Gets metadata for a specific session by ID.
839
+ *
840
+ * This provides an efficient O(1) lookup of a single session's metadata
841
+ * instead of listing all sessions. Returns undefined if the session is not found.
842
+ *
843
+ * @param sessionId - The ID of the session to look up
844
+ * @returns A promise that resolves with the session metadata, or undefined if not found
845
+ * @throws Error if the client is not connected
846
+ *
847
+ * @example
848
+ * ```typescript
849
+ * const metadata = await client.getSessionMetadata("session-123");
850
+ * if (metadata) {
851
+ * console.log(`Session started at: ${metadata.startTime}`);
852
+ * }
853
+ * ```
854
+ */
855
+ async getSessionMetadata(sessionId) {
856
+ if (!this.connection) {
857
+ throw new Error("Client not connected");
858
+ }
859
+ const response = await this.connection.sendRequest("session.getMetadata", { sessionId });
860
+ const { session } = response;
861
+ if (!session) {
862
+ return void 0;
863
+ }
864
+ return CopilotClient.toSessionMetadata(session);
865
+ }
866
+ static toSessionMetadata(raw) {
867
+ return {
868
+ sessionId: raw.sessionId,
869
+ startTime: new Date(raw.startTime),
870
+ modifiedTime: new Date(raw.modifiedTime),
871
+ summary: raw.summary,
872
+ isRemote: raw.isRemote,
873
+ context: raw.context
874
+ };
805
875
  }
806
876
  /**
807
877
  * Gets the foreground session ID in TUI+server mode.
@@ -1131,6 +1201,12 @@ stderr: ${stderrOutput}`
1131
1201
  "systemMessage.transform",
1132
1202
  async (params) => await this.handleSystemMessageTransform(params)
1133
1203
  );
1204
+ const sessions = this.sessions;
1205
+ registerClientSessionApiHandlers(this.connection, (sessionId) => {
1206
+ const session = sessions.get(sessionId);
1207
+ if (!session) throw new Error(`No session found for sessionId: ${sessionId}`);
1208
+ return session.clientSessionApis;
1209
+ });
1134
1210
  this.connection.onClose(() => {
1135
1211
  this.state = "disconnected";
1136
1212
  });