@github/copilot-sdk 0.2.1-preview.0 → 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
@@ -60,7 +60,10 @@ await client.stop();
60
60
  Sessions also support `Symbol.asyncDispose` for use with [`await using`](https://github.com/tc39/proposal-explicit-resource-management) (TypeScript 5.2+/Node.js 18.0+):
61
61
 
62
62
  ```typescript
63
- await using session = await client.createSession({ model: "gpt-5", onPermissionRequest: approveAll });
63
+ await using session = await client.createSession({
64
+ model: "gpt-5",
65
+ onPermissionRequest: approveAll,
66
+ });
64
67
  // session is automatically disconnected when leaving scope
65
68
  ```
66
69
 
@@ -76,7 +79,7 @@ new CopilotClient(options?: CopilotClientOptions)
76
79
 
77
80
  **Options:**
78
81
 
79
- - `cliPath?: string` - Path to CLI executable (default: "copilot" from PATH)
82
+ - `cliPath?: string` - Path to CLI executable (default: uses COPILOT_CLI_PATH env var or bundled instance)
80
83
  - `cliArgs?: string[]` - Extra arguments prepended before SDK-managed flags (e.g. `["./dist-cli/index.js"]` when using `node`)
81
84
  - `cliUrl?: string` - URL of existing CLI server to connect to (e.g., `"localhost:8080"`, `"http://127.0.0.1:9000"`, or just `"8080"`). When provided, the client will not spawn a CLI process.
82
85
  - `port?: number` - Server port (default: 0 for random)
@@ -117,6 +120,7 @@ Create a new conversation session.
117
120
  - `provider?: ProviderConfig` - Custom API provider configuration (BYOK - Bring Your Own Key). See [Custom Providers](#custom-providers) section.
118
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.
119
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.
120
124
  - `hooks?: SessionHooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
121
125
 
122
126
  ##### `resumeSession(sessionId: string, config?: ResumeSessionConfig): Promise<CopilotSession>`
@@ -184,6 +188,7 @@ const unsubscribe = client.on((event) => {
184
188
  ```
185
189
 
186
190
  **Lifecycle Event Types:**
191
+
187
192
  - `session.created` - A new session was created
188
193
  - `session.deleted` - A session was deleted
189
194
  - `session.updated` - A session was updated (e.g., new messages)
@@ -289,11 +294,13 @@ if (session.capabilities.ui?.elicitation) {
289
294
  }
290
295
  ```
291
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
+
292
299
  ##### `ui: SessionUiApi`
293
300
 
294
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.
295
302
 
296
- ##### `destroy(): Promise<void>` *(deprecated)*
303
+ ##### `destroy(): Promise<void>` _(deprecated)_
297
304
 
298
305
  Deprecated — use `disconnect()` instead.
299
306
 
@@ -454,8 +461,10 @@ defineTool("edit_file", {
454
461
  description: "Custom file editor with project-specific validation",
455
462
  parameters: z.object({ path: z.string(), content: z.string() }),
456
463
  overridesBuiltInTool: true,
457
- handler: async ({ path, content }) => { /* your logic */ },
458
- })
464
+ handler: async ({ path, content }) => {
465
+ /* your logic */
466
+ },
467
+ });
459
468
  ```
460
469
 
461
470
  #### Skipping Permission Prompts
@@ -467,8 +476,10 @@ defineTool("safe_lookup", {
467
476
  description: "A read-only lookup that needs no confirmation",
468
477
  parameters: z.object({ id: z.string() }),
469
478
  skipPermission: true,
470
- handler: async ({ id }) => { /* your logic */ },
471
- })
479
+ handler: async ({ id }) => {
480
+ /* your logic */
481
+ },
482
+ });
472
483
  ```
473
484
 
474
485
  ### Commands
@@ -497,9 +508,9 @@ Commands are sent to the CLI on both `createSession` and `resumeSession`, so you
497
508
 
498
509
  ### UI Elicitation
499
510
 
500
- 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.
501
512
 
502
- > **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.
503
514
 
504
515
  ```ts
505
516
  const session = await client.createSession({ onPermissionRequest: approveAll });
@@ -571,7 +582,10 @@ const session = await client.createSession({
571
582
  mode: "customize",
572
583
  sections: {
573
584
  // Replace the tone/style section
574
- tone: { action: "replace", content: "Respond in a warm, professional tone. Be thorough in explanations." },
585
+ tone: {
586
+ action: "replace",
587
+ content: "Respond in a warm, professional tone. Be thorough in explanations.",
588
+ },
575
589
  // Remove coding-specific rules
576
590
  code_change_rules: { action: "remove" },
577
591
  // Append to existing guidelines
@@ -586,6 +600,7 @@ const session = await client.createSession({
586
600
  Available section IDs: `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `last_instructions`. Use the `SYSTEM_PROMPT_SECTIONS` constant for descriptions of each section.
587
601
 
588
602
  Each section override supports four actions:
603
+
589
604
  - **`replace`** — Replace the section content entirely
590
605
  - **`remove`** — Remove the section from the prompt
591
606
  - **`append`** — Add content after the existing section
@@ -624,7 +639,7 @@ const session = await client.createSession({
624
639
  model: "gpt-5",
625
640
  infiniteSessions: {
626
641
  enabled: true,
627
- backgroundCompactionThreshold: 0.80, // Start compacting at 80% context usage
642
+ backgroundCompactionThreshold: 0.8, // Start compacting at 80% context usage
628
643
  bufferExhaustionThreshold: 0.95, // Block at 95% until compaction completes
629
644
  },
630
645
  });
@@ -723,8 +738,8 @@ const session = await client.createSession({
723
738
  const session = await client.createSession({
724
739
  model: "gpt-4",
725
740
  provider: {
726
- type: "azure", // Must be "azure" for Azure endpoints, NOT "openai"
727
- baseUrl: "https://my-resource.openai.azure.com", // Just the host, no path
741
+ type: "azure", // Must be "azure" for Azure endpoints, NOT "openai"
742
+ baseUrl: "https://my-resource.openai.azure.com", // Just the host, no path
728
743
  apiKey: process.env.AZURE_OPENAI_KEY,
729
744
  azure: {
730
745
  apiVersion: "2024-10-21",
@@ -734,6 +749,7 @@ const session = await client.createSession({
734
749
  ```
735
750
 
736
751
  > **Important notes:**
752
+ >
737
753
  > - When using a custom provider, the `model` parameter is **required**. The SDK will throw an error if no model is specified.
738
754
  > - For Azure OpenAI endpoints (`*.openai.azure.com`), you **must** use `type: "azure"`, not `type: "openai"`.
739
755
  > - The `baseUrl` should be just the host (e.g., `https://my-resource.openai.azure.com`). Do **not** include `/openai/v1` in the URL - the SDK handles path construction automatically.
@@ -744,9 +760,9 @@ The SDK supports OpenTelemetry for distributed tracing. Provide a `telemetry` co
744
760
 
745
761
  ```typescript
746
762
  const client = new CopilotClient({
747
- telemetry: {
748
- otlpEndpoint: "http://localhost:4318",
749
- },
763
+ telemetry: {
764
+ otlpEndpoint: "http://localhost:4318",
765
+ },
750
766
  });
751
767
  ```
752
768
 
@@ -772,12 +788,12 @@ If you're already using `@opentelemetry/api` in your app and want this linkage,
772
788
  import { propagation, context } from "@opentelemetry/api";
773
789
 
774
790
  const client = new CopilotClient({
775
- telemetry: { otlpEndpoint: "http://localhost:4318" },
776
- onGetTraceContext: () => {
777
- const carrier: Record<string, string> = {};
778
- propagation.inject(context.active(), carrier);
779
- return carrier;
780
- },
791
+ telemetry: { otlpEndpoint: "http://localhost:4318" },
792
+ onGetTraceContext: () => {
793
+ const carrier: Record<string, string> = {};
794
+ propagation.inject(context.active(), carrier);
795
+ return carrier;
796
+ },
781
797
  });
782
798
  ```
783
799
 
@@ -837,14 +853,15 @@ const session = await client.createSession({
837
853
 
838
854
  ### Permission Result Kinds
839
855
 
840
- | Kind | Meaning |
841
- |------|---------|
842
- | `"approved"` | Allow the tool to run |
843
- | `"denied-interactively-by-user"` | User explicitly denied the request |
844
- | `"denied-no-approval-rule-and-could-not-request-from-user"` | No approval rule matched and user could not be asked |
845
- | `"denied-by-rules"` | Denied by a policy rule |
846
- | `"denied-by-content-exclusion-policy"` | Denied due to a content exclusion policy |
847
- | `"no-result"` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) |
856
+ | Kind | Meaning |
857
+ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
858
+ | `"approved"` | Allow the tool to run |
859
+ | `"denied-interactively-by-user"` | User explicitly denied the request |
860
+ | `"denied-no-approval-rule-and-could-not-request-from-user"` | No approval rule matched and user could not be asked |
861
+ | `"denied-by-rules"` | Denied by a policy rule |
862
+ | `"denied-by-content-exclusion-policy"` | Denied due to a content exclusion policy |
863
+ | `"no-result"` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) |
864
+
848
865
  ### Resuming Sessions
849
866
 
850
867
  Pass `onPermissionRequest` when resuming a session too — it is required:
@@ -885,6 +902,42 @@ const session = await client.createSession({
885
902
  });
886
903
  ```
887
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
+
888
941
  ## Session Hooks
889
942
 
890
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,8 +177,10 @@ class CopilotClient {
175
177
  }
176
178
  this.onListModels = options.onListModels;
177
179
  this.onGetTraceContext = options.onGetTraceContext;
180
+ this.sessionFsConfig = options.sessionFs ?? null;
181
+ const effectiveEnv = options.env ?? process.env;
178
182
  this.options = {
179
- cliPath: options.cliUrl ? void 0 : options.cliPath || getBundledCliPath(),
183
+ cliPath: options.cliUrl ? void 0 : options.cliPath || effectiveEnv.COPILOT_CLI_PATH || getBundledCliPath(),
180
184
  cliArgs: options.cliArgs ?? [],
181
185
  cwd: options.cwd ?? process.cwd(),
182
186
  port: options.port || 0,
@@ -187,7 +191,7 @@ class CopilotClient {
187
191
  logLevel: options.logLevel || "debug",
188
192
  autoStart: options.autoStart ?? true,
189
193
  autoRestart: false,
190
- env: options.env ?? process.env,
194
+ env: effectiveEnv,
191
195
  githubToken: options.githubToken,
192
196
  // Default useLoggedInUser to false when githubToken is provided, otherwise true
193
197
  useLoggedInUser: options.useLoggedInUser ?? (options.githubToken ? false : true),
@@ -245,6 +249,13 @@ class CopilotClient {
245
249
  }
246
250
  await this.connectToServer();
247
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
+ }
248
259
  this.state = "connected";
249
260
  } catch (error) {
250
261
  this.state = "error";
@@ -456,6 +467,9 @@ class CopilotClient {
456
467
  if (config.onUserInputRequest) {
457
468
  session.registerUserInputHandler(config.onUserInputRequest);
458
469
  }
470
+ if (config.onElicitationRequest) {
471
+ session.registerElicitationHandler(config.onElicitationRequest);
472
+ }
459
473
  if (config.hooks) {
460
474
  session.registerHooks(config.hooks);
461
475
  }
@@ -469,6 +483,15 @@ class CopilotClient {
469
483
  session.on(config.onEvent);
470
484
  }
471
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
+ }
472
495
  try {
473
496
  const response = await this.connection.sendRequest("session.create", {
474
497
  ...await (0, import_telemetry.getTraceContext)(this.onGetTraceContext),
@@ -493,6 +516,7 @@ class CopilotClient {
493
516
  provider: config.provider,
494
517
  requestPermission: true,
495
518
  requestUserInput: !!config.onUserInputRequest,
519
+ requestElicitation: !!config.onElicitationRequest,
496
520
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
497
521
  workingDirectory: config.workingDirectory,
498
522
  streaming: config.streaming,
@@ -563,6 +587,9 @@ class CopilotClient {
563
587
  if (config.onUserInputRequest) {
564
588
  session.registerUserInputHandler(config.onUserInputRequest);
565
589
  }
590
+ if (config.onElicitationRequest) {
591
+ session.registerElicitationHandler(config.onElicitationRequest);
592
+ }
566
593
  if (config.hooks) {
567
594
  session.registerHooks(config.hooks);
568
595
  }
@@ -576,6 +603,15 @@ class CopilotClient {
576
603
  session.on(config.onEvent);
577
604
  }
578
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
+ }
579
615
  try {
580
616
  const response = await this.connection.sendRequest("session.resume", {
581
617
  ...await (0, import_telemetry.getTraceContext)(this.onGetTraceContext),
@@ -600,6 +636,7 @@ class CopilotClient {
600
636
  provider: config.provider,
601
637
  requestPermission: true,
602
638
  requestUserInput: !!config.onUserInputRequest,
639
+ requestElicitation: !!config.onElicitationRequest,
603
640
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
604
641
  workingDirectory: config.workingDirectory,
605
642
  configDir: config.configDir,
@@ -811,16 +848,50 @@ class CopilotClient {
811
848
  if (!this.connection) {
812
849
  throw new Error("Client not connected");
813
850
  }
814
- const response = await this.connection.sendRequest("session.list", { filter });
851
+ const response = await this.connection.sendRequest("session.list", {
852
+ filter
853
+ });
815
854
  const { sessions } = response;
816
- return sessions.map((s) => ({
817
- sessionId: s.sessionId,
818
- startTime: new Date(s.startTime),
819
- modifiedTime: new Date(s.modifiedTime),
820
- summary: s.summary,
821
- isRemote: s.isRemote,
822
- context: s.context
823
- }));
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
+ };
824
895
  }
825
896
  /**
826
897
  * Gets the foreground session ID in TUI+server mode.
@@ -1150,6 +1221,12 @@ stderr: ${stderrOutput}`
1150
1221
  "systemMessage.transform",
1151
1222
  async (params) => await this.handleSystemMessageTransform(params)
1152
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
+ });
1153
1230
  this.connection.onClose(() => {
1154
1231
  this.state = "disconnected";
1155
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
  *