@github/copilot-sdk 0.1.32 → 0.1.33-unstable.0

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.d.ts CHANGED
@@ -1,6 +1,39 @@
1
1
  import { createServerRpc } from "./generated/rpc.js";
2
2
  import { CopilotSession } from "./session.js";
3
3
  import type { ConnectionState, CopilotClientOptions, GetAuthStatusResponse, GetStatusResponse, ModelInfo, ResumeSessionConfig, SessionConfig, SessionLifecycleEventType, SessionLifecycleHandler, SessionListFilter, SessionMetadata, TypedSessionLifecycleHandler } from "./types.js";
4
+ /**
5
+ * Main client for interacting with the Copilot CLI.
6
+ *
7
+ * The CopilotClient manages the connection to the Copilot CLI server and provides
8
+ * methods to create and manage conversation sessions. It can either spawn a CLI
9
+ * server process or connect to an existing server.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { CopilotClient } from "@github/copilot-sdk";
14
+ *
15
+ * // Create a client with default options (spawns CLI server)
16
+ * const client = new CopilotClient();
17
+ *
18
+ * // Or connect to an existing server
19
+ * const client = new CopilotClient({ cliUrl: "localhost:3000" });
20
+ *
21
+ * // Create a session
22
+ * const session = await client.createSession({ onPermissionRequest: approveAll, model: "gpt-4" });
23
+ *
24
+ * // Send messages and handle responses
25
+ * session.on((event) => {
26
+ * if (event.type === "assistant.message") {
27
+ * console.log(event.data.content);
28
+ * }
29
+ * });
30
+ * await session.send({ prompt: "Hello!" });
31
+ *
32
+ * // Clean up
33
+ * await session.disconnect();
34
+ * await client.stop();
35
+ * ```
36
+ */
4
37
  export declare class CopilotClient {
5
38
  private cliProcess;
6
39
  private connection;
@@ -13,6 +46,7 @@ export declare class CopilotClient {
13
46
  private options;
14
47
  private isExternalServer;
15
48
  private forceStopping;
49
+ private onListModels?;
16
50
  private modelsCache;
17
51
  private modelsCacheLock;
18
52
  private sessionLifecycleHandlers;
@@ -218,10 +252,13 @@ export declare class CopilotClient {
218
252
  /**
219
253
  * List available models with their metadata.
220
254
  *
255
+ * If an `onListModels` handler was provided in the client options,
256
+ * it is called instead of querying the CLI server.
257
+ *
221
258
  * Results are cached after the first successful call to avoid rate limiting.
222
259
  * The cache is cleared when the client disconnects.
223
260
  *
224
- * @throws Error if not authenticated
261
+ * @throws Error if not connected (when no custom handler is set)
225
262
  */
226
263
  listModels(): Promise<ModelInfo[]>;
227
264
  /**
package/dist/client.js CHANGED
@@ -46,6 +46,7 @@ class CopilotClient {
46
46
  options;
47
47
  isExternalServer = false;
48
48
  forceStopping = false;
49
+ onListModels;
49
50
  modelsCache = null;
50
51
  modelsCacheLock = Promise.resolve();
51
52
  sessionLifecycleHandlers = /* @__PURE__ */ new Set();
@@ -111,6 +112,7 @@ class CopilotClient {
111
112
  if (options.isChildProcess) {
112
113
  this.isExternalServer = true;
113
114
  }
115
+ this.onListModels = options.onListModels;
114
116
  this.options = {
115
117
  cliPath: options.cliPath || getBundledCliPath(),
116
118
  cliArgs: options.cliArgs ?? [],
@@ -401,6 +403,7 @@ class CopilotClient {
401
403
  mcpServers: config.mcpServers,
402
404
  envValueMode: "direct",
403
405
  customAgents: config.customAgents,
406
+ agent: config.agent,
404
407
  configDir: config.configDir,
405
408
  skillDirectories: config.skillDirectories,
406
409
  disabledSkills: config.disabledSkills,
@@ -480,6 +483,7 @@ class CopilotClient {
480
483
  mcpServers: config.mcpServers,
481
484
  envValueMode: "direct",
482
485
  customAgents: config.customAgents,
486
+ agent: config.agent,
483
487
  skillDirectories: config.skillDirectories,
484
488
  disabledSkills: config.disabledSkills,
485
489
  infiniteSessions: config.infiniteSessions,
@@ -556,15 +560,15 @@ class CopilotClient {
556
560
  /**
557
561
  * List available models with their metadata.
558
562
  *
563
+ * If an `onListModels` handler was provided in the client options,
564
+ * it is called instead of querying the CLI server.
565
+ *
559
566
  * Results are cached after the first successful call to avoid rate limiting.
560
567
  * The cache is cleared when the client disconnects.
561
568
  *
562
- * @throws Error if not authenticated
569
+ * @throws Error if not connected (when no custom handler is set)
563
570
  */
564
571
  async listModels() {
565
- if (!this.connection) {
566
- throw new Error("Client not connected");
567
- }
568
572
  await this.modelsCacheLock;
569
573
  let resolveLock;
570
574
  this.modelsCacheLock = new Promise((resolve) => {
@@ -574,10 +578,18 @@ class CopilotClient {
574
578
  if (this.modelsCache !== null) {
575
579
  return [...this.modelsCache];
576
580
  }
577
- const result = await this.connection.sendRequest("models.list", {});
578
- const response = result;
579
- const models = response.models;
580
- this.modelsCache = models;
581
+ let models;
582
+ if (this.onListModels) {
583
+ models = await this.onListModels();
584
+ } else {
585
+ if (!this.connection) {
586
+ throw new Error("Client not connected");
587
+ }
588
+ const result = await this.connection.sendRequest("models.list", {});
589
+ const response = result;
590
+ models = response.models;
591
+ }
592
+ this.modelsCache = [...models];
581
593
  return [...models];
582
594
  } finally {
583
595
  resolveLock();
@@ -1,2 +1,20 @@
1
- import { CopilotClient } from "./client.js";
2
- export declare const extension: CopilotClient;
1
+ import type { CopilotSession } from "./session.js";
2
+ import type { ResumeSessionConfig } from "./types.js";
3
+ /**
4
+ * Joins the current foreground session.
5
+ *
6
+ * @param config - Configuration to add to the session
7
+ * @returns A promise that resolves with the joined session
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { approveAll } from "@github/copilot-sdk";
12
+ * import { joinSession } from "@github/copilot-sdk/extension";
13
+ *
14
+ * const session = await joinSession({
15
+ * onPermissionRequest: approveAll,
16
+ * tools: [myTool],
17
+ * });
18
+ * ```
19
+ */
20
+ export declare function joinSession(config: ResumeSessionConfig): Promise<CopilotSession>;
package/dist/extension.js CHANGED
@@ -1,5 +1,17 @@
1
1
  import { CopilotClient } from "./client.js";
2
- const extension = new CopilotClient({ isChildProcess: true });
2
+ async function joinSession(config) {
3
+ const sessionId = process.env.SESSION_ID;
4
+ if (!sessionId) {
5
+ throw new Error(
6
+ "joinSession() is intended for extensions running as child processes of the Copilot CLI."
7
+ );
8
+ }
9
+ const client = new CopilotClient({ isChildProcess: true });
10
+ return client.resumeSession(sessionId, {
11
+ ...config,
12
+ disableResume: config.disableResume ?? true
13
+ });
14
+ }
3
15
  export {
4
- extension
16
+ joinSession
5
17
  };
@@ -162,6 +162,7 @@ export interface SessionModelSwitchToParams {
162
162
  */
163
163
  sessionId: string;
164
164
  modelId: string;
165
+ reasoningEffort?: "low" | "medium" | "high" | "xhigh";
165
166
  }
166
167
  export interface SessionModeGetResult {
167
168
  /**
@@ -442,6 +443,30 @@ export interface SessionPermissionsHandlePendingPermissionRequestParams {
442
443
  message: string;
443
444
  };
444
445
  }
446
+ export interface SessionLogResult {
447
+ /**
448
+ * The unique identifier of the emitted session event
449
+ */
450
+ eventId: string;
451
+ }
452
+ export interface SessionLogParams {
453
+ /**
454
+ * Target session identifier
455
+ */
456
+ sessionId: string;
457
+ /**
458
+ * Human-readable message
459
+ */
460
+ message: string;
461
+ /**
462
+ * Log severity level. Determines how the message is displayed in the timeline. Defaults to "info".
463
+ */
464
+ level?: "info" | "warning" | "error";
465
+ /**
466
+ * When true, the message is transient and not persisted to the session event log on disk
467
+ */
468
+ ephemeral?: boolean;
469
+ }
445
470
  /** Create typed server-scoped RPC methods (no session required). */
446
471
  export declare function createServerRpc(connection: MessageConnection): {
447
472
  ping: (params: PingParams) => Promise<PingResult>;
@@ -493,4 +518,5 @@ export declare function createSessionRpc(connection: MessageConnection, sessionI
493
518
  permissions: {
494
519
  handlePendingPermissionRequest: (params: Omit<SessionPermissionsHandlePendingPermissionRequestParams, "sessionId">) => Promise<SessionPermissionsHandlePendingPermissionRequestResult>;
495
520
  };
521
+ log: (params: Omit<SessionLogParams, "sessionId">) => Promise<SessionLogResult>;
496
522
  };
@@ -49,7 +49,8 @@ function createSessionRpc(connection, sessionId) {
49
49
  },
50
50
  permissions: {
51
51
  handlePendingPermissionRequest: async (params) => connection.sendRequest("session.permissions.handlePendingPermissionRequest", { sessionId, ...params })
52
- }
52
+ },
53
+ log: async (params) => connection.sendRequest("session.log", { sessionId, ...params })
53
54
  };
54
55
  }
55
56
  export {
@@ -66,6 +66,7 @@ export type SessionEvent = {
66
66
  */
67
67
  branch?: string;
68
68
  };
69
+ alreadyInUse?: boolean;
69
70
  };
70
71
  } | {
71
72
  /**
@@ -115,6 +116,7 @@ export type SessionEvent = {
115
116
  */
116
117
  branch?: string;
117
118
  };
119
+ alreadyInUse?: boolean;
118
120
  };
119
121
  } | {
120
122
  /**
@@ -2090,6 +2092,80 @@ export type SessionEvent = {
2090
2092
  };
2091
2093
  };
2092
2094
  };
2095
+ } | {
2096
+ /**
2097
+ * Unique event identifier (UUID v4), generated when the event is emitted
2098
+ */
2099
+ id: string;
2100
+ /**
2101
+ * ISO 8601 timestamp when the event was created
2102
+ */
2103
+ timestamp: string;
2104
+ /**
2105
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
2106
+ */
2107
+ parentId: string | null;
2108
+ /**
2109
+ * When true, the event is transient and not persisted to the session event log on disk
2110
+ */
2111
+ ephemeral?: boolean;
2112
+ type: "system.notification";
2113
+ data: {
2114
+ /**
2115
+ * The notification text, typically wrapped in <system_notification> XML tags
2116
+ */
2117
+ content: string;
2118
+ /**
2119
+ * Structured metadata identifying what triggered this notification
2120
+ */
2121
+ kind: {
2122
+ type: "agent_completed";
2123
+ /**
2124
+ * Unique identifier of the background agent
2125
+ */
2126
+ agentId: string;
2127
+ /**
2128
+ * Type of the agent (e.g., explore, task, general-purpose)
2129
+ */
2130
+ agentType: string;
2131
+ /**
2132
+ * Whether the agent completed successfully or failed
2133
+ */
2134
+ status: "completed" | "failed";
2135
+ /**
2136
+ * Human-readable description of the agent task
2137
+ */
2138
+ description?: string;
2139
+ /**
2140
+ * The full prompt given to the background agent
2141
+ */
2142
+ prompt?: string;
2143
+ } | {
2144
+ type: "shell_completed";
2145
+ /**
2146
+ * Unique identifier of the shell session
2147
+ */
2148
+ shellId: string;
2149
+ /**
2150
+ * Exit code of the shell command, if available
2151
+ */
2152
+ exitCode?: number;
2153
+ /**
2154
+ * Human-readable description of the command
2155
+ */
2156
+ description?: string;
2157
+ } | {
2158
+ type: "shell_detached_completed";
2159
+ /**
2160
+ * Unique identifier of the detached shell session
2161
+ */
2162
+ shellId: string;
2163
+ /**
2164
+ * Human-readable description of the command
2165
+ */
2166
+ description?: string;
2167
+ };
2168
+ };
2093
2169
  } | {
2094
2170
  /**
2095
2171
  * Unique event identifier (UUID v4), generated when the event is emitted
package/dist/session.d.ts CHANGED
@@ -339,4 +339,24 @@ export declare class CopilotSession {
339
339
  * ```
340
340
  */
341
341
  setModel(model: string): Promise<void>;
342
+ /**
343
+ * Log a message to the session timeline.
344
+ * The message appears in the session event stream and is visible to SDK consumers
345
+ * and (for non-ephemeral messages) persisted to the session event log on disk.
346
+ *
347
+ * @param message - Human-readable message text
348
+ * @param options - Optional log level and ephemeral flag
349
+ *
350
+ * @example
351
+ * ```typescript
352
+ * await session.log("Processing started");
353
+ * await session.log("Disk usage high", { level: "warning" });
354
+ * await session.log("Connection failed", { level: "error" });
355
+ * await session.log("Debug info", { ephemeral: true });
356
+ * ```
357
+ */
358
+ log(message: string, options?: {
359
+ level?: "info" | "warning" | "error";
360
+ ephemeral?: boolean;
361
+ }): Promise<void>;
342
362
  }
package/dist/session.js CHANGED
@@ -501,6 +501,25 @@ class CopilotSession {
501
501
  async setModel(model) {
502
502
  await this.rpc.model.switchTo({ modelId: model });
503
503
  }
504
+ /**
505
+ * Log a message to the session timeline.
506
+ * The message appears in the session event stream and is visible to SDK consumers
507
+ * and (for non-ephemeral messages) persisted to the session event log on disk.
508
+ *
509
+ * @param message - Human-readable message text
510
+ * @param options - Optional log level and ephemeral flag
511
+ *
512
+ * @example
513
+ * ```typescript
514
+ * await session.log("Processing started");
515
+ * await session.log("Disk usage high", { level: "warning" });
516
+ * await session.log("Connection failed", { level: "error" });
517
+ * await session.log("Debug info", { ephemeral: true });
518
+ * ```
519
+ */
520
+ async log(message, options) {
521
+ await this.rpc.log({ message, ...options });
522
+ }
504
523
  }
505
524
  export {
506
525
  CopilotSession
package/dist/types.d.ts CHANGED
@@ -77,6 +77,13 @@ export interface CopilotClientOptions {
77
77
  * @default true (but defaults to false when githubToken is provided)
78
78
  */
79
79
  useLoggedInUser?: boolean;
80
+ /**
81
+ * Custom handler for listing available models.
82
+ * When provided, client.listModels() calls this handler instead of
83
+ * querying the CLI server. Useful in BYOK mode to return models
84
+ * available from your custom provider.
85
+ */
86
+ onListModels?: () => Promise<ModelInfo[]> | ModelInfo[];
80
87
  }
81
88
  /**
82
89
  * Configuration for creating a session
@@ -586,6 +593,12 @@ export interface SessionConfig {
586
593
  * Custom agent configurations for the session.
587
594
  */
588
595
  customAgents?: CustomAgentConfig[];
596
+ /**
597
+ * Name of the custom agent to activate when the session starts.
598
+ * Must match the `name` of one of the agents in `customAgents`.
599
+ * Equivalent to calling `session.rpc.agent.select({ name })` after creation.
600
+ */
601
+ agent?: string;
589
602
  /**
590
603
  * Directories to load skills from.
591
604
  */
@@ -604,7 +617,7 @@ export interface SessionConfig {
604
617
  /**
605
618
  * Configuration for resuming a session
606
619
  */
607
- export type ResumeSessionConfig = Pick<SessionConfig, "clientName" | "model" | "tools" | "systemMessage" | "availableTools" | "excludedTools" | "provider" | "streaming" | "reasoningEffort" | "onPermissionRequest" | "onUserInputRequest" | "hooks" | "workingDirectory" | "configDir" | "mcpServers" | "customAgents" | "skillDirectories" | "disabledSkills" | "infiniteSessions"> & {
620
+ export type ResumeSessionConfig = Pick<SessionConfig, "clientName" | "model" | "tools" | "systemMessage" | "availableTools" | "excludedTools" | "provider" | "streaming" | "reasoningEffort" | "onPermissionRequest" | "onUserInputRequest" | "hooks" | "workingDirectory" | "configDir" | "mcpServers" | "customAgents" | "agent" | "skillDirectories" | "disabledSkills" | "infiniteSessions"> & {
608
621
  /**
609
622
  * When true, skips emitting the session.resume event.
610
623
  * Useful for reconnecting to a session without triggering resume-related side effects.
@@ -0,0 +1,265 @@
1
+ # Agent Extension Authoring Guide
2
+
3
+ A precise, step-by-step reference for agents writing Copilot CLI extensions programmatically.
4
+
5
+ ## Workflow
6
+
7
+ ### Step 1: Scaffold the extension
8
+
9
+ Use the `extensions_manage` tool with `operation: "scaffold"`:
10
+
11
+ ```
12
+ extensions_manage({ operation: "scaffold", name: "my-extension" })
13
+ ```
14
+
15
+ This creates `.github/extensions/my-extension/extension.mjs` with a working skeleton.
16
+ For user-scoped extensions (persist across all repos), add `location: "user"`.
17
+
18
+ ### Step 2: Edit the extension file
19
+
20
+ Modify the generated `extension.mjs` using `edit` or `create` tools. The file must:
21
+ - Be named `extension.mjs` (only `.mjs` is supported)
22
+ - Use ES module syntax (`import`/`export`)
23
+ - Call `joinSession({ ... })`
24
+
25
+ ### Step 3: Reload extensions
26
+
27
+ ```
28
+ extensions_reload({})
29
+ ```
30
+
31
+ This stops all running extensions and re-discovers/re-launches them. New tools are available immediately in the same turn (mid-turn refresh).
32
+
33
+ ### Step 4: Verify
34
+
35
+ ```
36
+ extensions_manage({ operation: "list" })
37
+ extensions_manage({ operation: "inspect", name: "my-extension" })
38
+ ```
39
+
40
+ Check that the extension loaded successfully and isn't marked as "failed".
41
+
42
+ ---
43
+
44
+ ## File Structure
45
+
46
+ ```
47
+ .github/extensions/<name>/extension.mjs
48
+ ```
49
+
50
+ Discovery rules:
51
+ - The CLI scans `.github/extensions/` relative to the git root
52
+ - It also scans the user's copilot config extensions directory
53
+ - Only immediate subdirectories are checked (not recursive)
54
+ - Each subdirectory must contain a file named `extension.mjs`
55
+ - Project extensions shadow user extensions on name collision
56
+
57
+ ---
58
+
59
+ ## Minimal Skeleton
60
+
61
+ ```js
62
+ import { approveAll } from "@github/copilot-sdk";
63
+ import { joinSession } from "@github/copilot-sdk/extension";
64
+
65
+ await joinSession({
66
+ onPermissionRequest: approveAll, // Required — handle permission requests
67
+ tools: [], // Optional — custom tools
68
+ hooks: {}, // Optional — lifecycle hooks
69
+ });
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Registering Tools
75
+
76
+ ```js
77
+ tools: [
78
+ {
79
+ name: "tool_name", // Required. Must be globally unique across all extensions.
80
+ description: "What it does", // Required. Shown to the agent in tool descriptions.
81
+ parameters: { // Optional. JSON Schema for the arguments.
82
+ type: "object",
83
+ properties: {
84
+ arg1: { type: "string", description: "..." },
85
+ },
86
+ required: ["arg1"],
87
+ },
88
+ handler: async (args, invocation) => {
89
+ // args: parsed arguments matching the schema
90
+ // invocation.sessionId: current session ID
91
+ // invocation.toolCallId: unique call ID
92
+ // invocation.toolName: this tool's name
93
+ //
94
+ // Return value: string or ToolResultObject
95
+ // string → treated as success
96
+ // { textResultForLlm, resultType } → structured result
97
+ // resultType: "success" | "failure" | "rejected" | "denied"
98
+ return `Result: ${args.arg1}`;
99
+ },
100
+ },
101
+ ]
102
+ ```
103
+
104
+ **Constraints:**
105
+ - Tool names must be unique across ALL loaded extensions. Collisions cause the second extension to fail to load.
106
+ - Handler must return a string or `{ textResultForLlm: string, resultType?: string }`.
107
+ - Handler receives `(args, invocation)` — the second argument has `sessionId`, `toolCallId`, `toolName`.
108
+ - Use `session.log()` to surface messages to the user. Don't use `console.log()` (stdout is reserved for JSON-RPC).
109
+
110
+ ---
111
+
112
+ ## Registering Hooks
113
+
114
+ ```js
115
+ hooks: {
116
+ onUserPromptSubmitted: async (input, invocation) => { ... },
117
+ onPreToolUse: async (input, invocation) => { ... },
118
+ onPostToolUse: async (input, invocation) => { ... },
119
+ onSessionStart: async (input, invocation) => { ... },
120
+ onSessionEnd: async (input, invocation) => { ... },
121
+ onErrorOccurred: async (input, invocation) => { ... },
122
+ }
123
+ ```
124
+
125
+ All hook inputs include `timestamp` (unix ms) and `cwd` (working directory).
126
+ All handlers receive `invocation: { sessionId: string }` as the second argument.
127
+ All handlers may return `void`/`undefined` (no-op) or an output object.
128
+
129
+ ### onUserPromptSubmitted
130
+
131
+ **Input:** `{ prompt: string, timestamp, cwd }`
132
+
133
+ **Output (all fields optional):**
134
+ | Field | Type | Effect |
135
+ |-------|------|--------|
136
+ | `modifiedPrompt` | `string` | Replaces the user's prompt |
137
+ | `additionalContext` | `string` | Appended as hidden context the agent sees |
138
+
139
+ ### onPreToolUse
140
+
141
+ **Input:** `{ toolName: string, toolArgs: unknown, timestamp, cwd }`
142
+
143
+ **Output (all fields optional):**
144
+ | Field | Type | Effect |
145
+ |-------|------|--------|
146
+ | `permissionDecision` | `"allow" \| "deny" \| "ask"` | Override the permission check |
147
+ | `permissionDecisionReason` | `string` | Shown to user if denied |
148
+ | `modifiedArgs` | `unknown` | Replaces the tool arguments |
149
+ | `additionalContext` | `string` | Injected into the conversation |
150
+
151
+ ### onPostToolUse
152
+
153
+ **Input:** `{ toolName: string, toolArgs: unknown, toolResult: ToolResultObject, timestamp, cwd }`
154
+
155
+ **Output (all fields optional):**
156
+ | Field | Type | Effect |
157
+ |-------|------|--------|
158
+ | `modifiedResult` | `ToolResultObject` | Replaces the tool result |
159
+ | `additionalContext` | `string` | Injected into the conversation |
160
+
161
+ ### onSessionStart
162
+
163
+ **Input:** `{ source: "startup" \| "resume" \| "new", initialPrompt?: string, timestamp, cwd }`
164
+
165
+ **Output (all fields optional):**
166
+ | Field | Type | Effect |
167
+ |-------|------|--------|
168
+ | `additionalContext` | `string` | Injected as initial context |
169
+
170
+ ### onSessionEnd
171
+
172
+ **Input:** `{ reason: "complete" \| "error" \| "abort" \| "timeout" \| "user_exit", finalMessage?: string, error?: string, timestamp, cwd }`
173
+
174
+ **Output (all fields optional):**
175
+ | Field | Type | Effect |
176
+ |-------|------|--------|
177
+ | `sessionSummary` | `string` | Summary for session persistence |
178
+ | `cleanupActions` | `string[]` | Cleanup descriptions |
179
+
180
+ ### onErrorOccurred
181
+
182
+ **Input:** `{ error: string, errorContext: "model_call" \| "tool_execution" \| "system" \| "user_input", recoverable: boolean, timestamp, cwd }`
183
+
184
+ **Output (all fields optional):**
185
+ | Field | Type | Effect |
186
+ |-------|------|--------|
187
+ | `errorHandling` | `"retry" \| "skip" \| "abort"` | How to handle the error |
188
+ | `retryCount` | `number` | Max retries (when errorHandling is "retry") |
189
+ | `userNotification` | `string` | Message shown to the user |
190
+
191
+ ---
192
+
193
+ ## Session Object
194
+
195
+ After `joinSession()`, the returned `session` provides:
196
+
197
+ ### session.send(options)
198
+
199
+ Send a message programmatically:
200
+ ```js
201
+ await session.send({ prompt: "Analyze the test results." });
202
+ await session.send({
203
+ prompt: "Review this file",
204
+ attachments: [{ type: "file", path: "./src/index.ts" }],
205
+ });
206
+ ```
207
+
208
+ ### session.sendAndWait(options, timeout?)
209
+
210
+ Send and block until the agent finishes (resolves on `session.idle`):
211
+ ```js
212
+ const response = await session.sendAndWait({ prompt: "What is 2+2?" });
213
+ // response?.data.content contains the agent's reply
214
+ ```
215
+
216
+ ### session.log(message, options?)
217
+
218
+ Log to the CLI timeline:
219
+ ```js
220
+ await session.log("Extension ready");
221
+ await session.log("Rate limit approaching", { level: "warning" });
222
+ await session.log("Connection failed", { level: "error" });
223
+ await session.log("Processing...", { ephemeral: true }); // transient, not persisted
224
+ ```
225
+
226
+ ### session.on(eventType, handler)
227
+
228
+ Subscribe to session events. Returns an unsubscribe function.
229
+ ```js
230
+ const unsub = session.on("tool.execution_complete", (event) => {
231
+ // event.data.toolName, event.data.success, event.data.result
232
+ });
233
+ ```
234
+
235
+ ### Key Event Types
236
+
237
+ | Event | Key Data Fields |
238
+ |-------|----------------|
239
+ | `assistant.message` | `content`, `messageId` |
240
+ | `tool.execution_start` | `toolCallId`, `toolName`, `arguments` |
241
+ | `tool.execution_complete` | `toolCallId`, `toolName`, `success`, `result`, `error` |
242
+ | `user.message` | `content`, `attachments`, `source` |
243
+ | `session.idle` | `backgroundTasks` |
244
+ | `session.error` | `errorType`, `message`, `stack` |
245
+ | `permission.requested` | `requestId`, `permissionRequest.kind` |
246
+ | `session.shutdown` | `shutdownType`, `totalPremiumRequests` |
247
+
248
+ ### session.workspacePath
249
+
250
+ Path to the session workspace directory (checkpoints, plan.md, files/). `undefined` if infinite sessions disabled.
251
+
252
+ ### session.rpc
253
+
254
+ Low-level typed RPC access to all session APIs (model, mode, plan, workspace, etc.).
255
+
256
+ ---
257
+
258
+ ## Gotchas
259
+
260
+ - **stdout is reserved for JSON-RPC.** Don't use `console.log()` — it will corrupt the protocol. Use `session.log()` to surface messages to the user.
261
+ - **Tool name collisions are fatal.** If two extensions register the same tool name, the second extension fails to initialize.
262
+ - **Don't call `session.send()` synchronously from `onUserPromptSubmitted`.** Use `setTimeout(() => session.send(...), 0)` to avoid infinite loops.
263
+ - **Extensions are reloaded on `/clear`.** Any in-memory state is lost between sessions.
264
+ - **Only `.mjs` is supported.** TypeScript (`.ts`) is not yet supported.
265
+ - **The handler's return value is the tool result.** Returning `undefined` sends an empty success. Throwing sends a failure with the error message.