@github/copilot-sdk
Advanced tools
| # Agent Extension Authoring Guide | ||
| A precise, step-by-step reference for agents writing Copilot CLI extensions programmatically. | ||
| ## Workflow | ||
| ### Step 1: Scaffold the extension | ||
| Use the `extensions_manage` tool with `operation: "scaffold"`: | ||
| ``` | ||
| extensions_manage({ operation: "scaffold", name: "my-extension" }) | ||
| ``` | ||
| This creates `.github/extensions/my-extension/extension.mjs` with a working skeleton. | ||
| For user-scoped extensions (persist across all repos), add `location: "user"`. | ||
| ### Step 2: Edit the extension file | ||
| Modify the generated `extension.mjs` using `edit` or `create` tools. The file must: | ||
| - Be named `extension.mjs` (only `.mjs` is supported) | ||
| - Use ES module syntax (`import`/`export`) | ||
| - Call `joinSession({ ... })` | ||
| ### Step 3: Reload extensions | ||
| ``` | ||
| extensions_reload({}) | ||
| ``` | ||
| This stops all running extensions and re-discovers/re-launches them. New tools are available immediately in the same turn (mid-turn refresh). | ||
| ### Step 4: Verify | ||
| ``` | ||
| extensions_manage({ operation: "list" }) | ||
| extensions_manage({ operation: "inspect", name: "my-extension" }) | ||
| ``` | ||
| Check that the extension loaded successfully and isn't marked as "failed". | ||
| --- | ||
| ## File Structure | ||
| ``` | ||
| .github/extensions/<name>/extension.mjs | ||
| ``` | ||
| Discovery rules: | ||
| - The CLI scans `.github/extensions/` relative to the git root | ||
| - It also scans the user's copilot config extensions directory | ||
| - Only immediate subdirectories are checked (not recursive) | ||
| - Each subdirectory must contain a file named `extension.mjs` | ||
| - Project extensions shadow user extensions on name collision | ||
| --- | ||
| ## Minimal Skeleton | ||
| ```js | ||
| import { joinSession } from "@github/copilot-sdk/extension"; | ||
| await joinSession({ | ||
| tools: [], // Optional — custom tools | ||
| hooks: {}, // Optional — lifecycle hooks | ||
| }); | ||
| ``` | ||
| --- | ||
| ## Registering Tools | ||
| ```js | ||
| tools: [ | ||
| { | ||
| name: "tool_name", // Required. Must be globally unique across all extensions. | ||
| description: "What it does", // Required. Shown to the agent in tool descriptions. | ||
| parameters: { // Optional. JSON Schema for the arguments. | ||
| type: "object", | ||
| properties: { | ||
| arg1: { type: "string", description: "..." }, | ||
| }, | ||
| required: ["arg1"], | ||
| }, | ||
| handler: async (args, invocation) => { | ||
| // args: parsed arguments matching the schema | ||
| // invocation.sessionId: current session ID | ||
| // invocation.toolCallId: unique call ID | ||
| // invocation.toolName: this tool's name | ||
| // | ||
| // Return value: string or ToolResultObject | ||
| // string → treated as success | ||
| // { textResultForLlm, resultType } → structured result | ||
| // resultType: "success" | "failure" | "rejected" | "denied" | ||
| return `Result: ${args.arg1}`; | ||
| }, | ||
| }, | ||
| ] | ||
| ``` | ||
| **Constraints:** | ||
| - Tool names must be unique across ALL loaded extensions. Collisions cause the second extension to fail to load. | ||
| - Handler must return a string or `{ textResultForLlm: string, resultType?: string }`. | ||
| - Handler receives `(args, invocation)` — the second argument has `sessionId`, `toolCallId`, `toolName`. | ||
| - Use `session.log()` to surface messages to the user. Don't use `console.log()` (stdout is reserved for JSON-RPC). | ||
| --- | ||
| ## Registering Hooks | ||
| ```js | ||
| hooks: { | ||
| onUserPromptSubmitted: async (input, invocation) => { ... }, | ||
| onPreToolUse: async (input, invocation) => { ... }, | ||
| onPostToolUse: async (input, invocation) => { ... }, | ||
| onSessionStart: async (input, invocation) => { ... }, | ||
| onSessionEnd: async (input, invocation) => { ... }, | ||
| onErrorOccurred: async (input, invocation) => { ... }, | ||
| } | ||
| ``` | ||
| All hook inputs include `timestamp` (unix ms) and `cwd` (working directory). | ||
| All handlers receive `invocation: { sessionId: string }` as the second argument. | ||
| All handlers may return `void`/`undefined` (no-op) or an output object. | ||
| ### onUserPromptSubmitted | ||
| **Input:** `{ prompt: string, timestamp, cwd }` | ||
| **Output (all fields optional):** | ||
| | Field | Type | Effect | | ||
| |-------|------|--------| | ||
| | `modifiedPrompt` | `string` | Replaces the user's prompt | | ||
| | `additionalContext` | `string` | Appended as hidden context the agent sees | | ||
| ### onPreToolUse | ||
| **Input:** `{ toolName: string, toolArgs: unknown, timestamp, cwd }` | ||
| **Output (all fields optional):** | ||
| | Field | Type | Effect | | ||
| |-------|------|--------| | ||
| | `permissionDecision` | `"allow" \| "deny" \| "ask"` | Override the permission check | | ||
| | `permissionDecisionReason` | `string` | Shown to user if denied | | ||
| | `modifiedArgs` | `unknown` | Replaces the tool arguments | | ||
| | `additionalContext` | `string` | Injected into the conversation | | ||
| ### onPostToolUse | ||
| **Input:** `{ toolName: string, toolArgs: unknown, toolResult: ToolResultObject, timestamp, cwd }` | ||
| **Output (all fields optional):** | ||
| | Field | Type | Effect | | ||
| |-------|------|--------| | ||
| | `modifiedResult` | `ToolResultObject` | Replaces the tool result | | ||
| | `additionalContext` | `string` | Injected into the conversation | | ||
| ### onSessionStart | ||
| **Input:** `{ source: "startup" \| "resume" \| "new", initialPrompt?: string, timestamp, cwd }` | ||
| **Output (all fields optional):** | ||
| | Field | Type | Effect | | ||
| |-------|------|--------| | ||
| | `additionalContext` | `string` | Injected as initial context | | ||
| ### onSessionEnd | ||
| **Input:** `{ reason: "complete" \| "error" \| "abort" \| "timeout" \| "user_exit", finalMessage?: string, error?: string, timestamp, cwd }` | ||
| **Output (all fields optional):** | ||
| | Field | Type | Effect | | ||
| |-------|------|--------| | ||
| | `sessionSummary` | `string` | Summary for session persistence | | ||
| | `cleanupActions` | `string[]` | Cleanup descriptions | | ||
| ### onErrorOccurred | ||
| **Input:** `{ error: string, errorContext: "model_call" \| "tool_execution" \| "system" \| "user_input", recoverable: boolean, timestamp, cwd }` | ||
| **Output (all fields optional):** | ||
| | Field | Type | Effect | | ||
| |-------|------|--------| | ||
| | `errorHandling` | `"retry" \| "skip" \| "abort"` | How to handle the error | | ||
| | `retryCount` | `number` | Max retries (when errorHandling is "retry") | | ||
| | `userNotification` | `string` | Message shown to the user | | ||
| --- | ||
| ## Session Object | ||
| After `joinSession()`, the returned `session` provides: | ||
| ### session.send(options) | ||
| Send a message programmatically: | ||
| ```js | ||
| await session.send({ prompt: "Analyze the test results." }); | ||
| await session.send({ | ||
| prompt: "Review this file", | ||
| attachments: [{ type: "file", path: "./src/index.ts" }], | ||
| }); | ||
| ``` | ||
| ### session.sendAndWait(options, timeout?) | ||
| Send and block until the agent finishes (resolves on `session.idle`): | ||
| ```js | ||
| const response = await session.sendAndWait({ prompt: "What is 2+2?" }); | ||
| // response?.data.content contains the agent's reply | ||
| ``` | ||
| ### session.log(message, options?) | ||
| Log to the CLI timeline: | ||
| ```js | ||
| await session.log("Extension ready"); | ||
| await session.log("Rate limit approaching", { level: "warning" }); | ||
| await session.log("Connection failed", { level: "error" }); | ||
| await session.log("Processing...", { ephemeral: true }); // transient, not persisted | ||
| ``` | ||
| ### session.on(eventType, handler) | ||
| Subscribe to session events. Returns an unsubscribe function. | ||
| ```js | ||
| const unsub = session.on("tool.execution_complete", (event) => { | ||
| // event.data.toolName, event.data.success, event.data.result | ||
| }); | ||
| ``` | ||
| ### Key Event Types | ||
| | Event | Key Data Fields | | ||
| |-------|----------------| | ||
| | `assistant.message` | `content`, `messageId` | | ||
| | `tool.execution_start` | `toolCallId`, `toolName`, `arguments` | | ||
| | `tool.execution_complete` | `toolCallId`, `toolName`, `success`, `result`, `error` | | ||
| | `user.message` | `content`, `attachments`, `source` | | ||
| | `session.idle` | `backgroundTasks` | | ||
| | `session.error` | `errorType`, `message`, `stack` | | ||
| | `permission.requested` | `requestId`, `permissionRequest.kind` | | ||
| | `session.shutdown` | `shutdownType`, `totalPremiumRequests` | | ||
| ### session.workspacePath | ||
| Path to the session workspace directory (checkpoints, plan.md, files/). `undefined` if infinite sessions disabled. | ||
| ### session.rpc | ||
| Low-level typed RPC access to all session APIs (model, mode, plan, workspace, etc.). | ||
| --- | ||
| ## Gotchas | ||
| - **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. | ||
| - **Tool name collisions are fatal.** If two extensions register the same tool name, the second extension fails to initialize. | ||
| - **Don't call `session.send()` synchronously from `onUserPromptSubmitted`.** Use `setTimeout(() => session.send(...), 0)` to avoid infinite loops. | ||
| - **Extensions are reloaded on `/clear`.** Any in-memory state is lost between sessions. | ||
| - **Only `.mjs` is supported.** TypeScript (`.ts`) is not yet supported. | ||
| - **The handler's return value is the tool result.** Returning `undefined` sends an empty success. Throwing sends a failure with the error message. |
+668
| # Copilot CLI Extension Examples | ||
| A practical guide to writing extensions using the `@github/copilot-sdk` extension API. | ||
| ## Extension Skeleton | ||
| Every extension starts with the same boilerplate: | ||
| ```js | ||
| import { joinSession } from "@github/copilot-sdk/extension"; | ||
| const session = await joinSession({ | ||
| hooks: { /* ... */ }, | ||
| tools: [ /* ... */ ], | ||
| }); | ||
| ``` | ||
| `joinSession` returns a `CopilotSession` object you can use to send messages and subscribe to events. | ||
| > **Platform notes (Windows vs macOS/Linux):** | ||
| > - Use `process.platform === "win32"` to detect Windows at runtime. | ||
| > - Clipboard: `pbcopy` on macOS, `clip` on Windows. | ||
| > - Use `exec()` instead of `execFile()` for `.cmd` scripts like `code`, `npx`, `npm` on Windows. | ||
| > - PowerShell stderr redirection uses `*>&1` instead of `2>&1`. | ||
| --- | ||
| ## Logging to the Timeline | ||
| Use `session.log()` to surface messages to the user in the CLI timeline: | ||
| ```js | ||
| const session = await joinSession({ | ||
| hooks: { | ||
| onSessionStart: async () => { | ||
| await session.log("My extension loaded"); | ||
| }, | ||
| onPreToolUse: async (input) => { | ||
| if (input.toolName === "bash") { | ||
| await session.log(`Running: ${input.toolArgs?.command}`, { ephemeral: true }); | ||
| } | ||
| }, | ||
| }, | ||
| tools: [], | ||
| }); | ||
| ``` | ||
| Levels: `"info"` (default), `"warning"`, `"error"`. Set `ephemeral: true` for transient messages that aren't persisted. | ||
| --- | ||
| ## Registering Custom Tools | ||
| Tools are functions the agent can call. Define them with a name, description, JSON Schema parameters, and a handler. | ||
| ### Basic tool | ||
| ```js | ||
| tools: [ | ||
| { | ||
| name: "my_tool", | ||
| description: "Does something useful", | ||
| parameters: { | ||
| type: "object", | ||
| properties: { | ||
| input: { type: "string", description: "The input value" }, | ||
| }, | ||
| required: ["input"], | ||
| }, | ||
| handler: async (args) => { | ||
| return `Processed: ${args.input}`; | ||
| }, | ||
| }, | ||
| ] | ||
| ``` | ||
| ### Tool that invokes an external shell command | ||
| ```js | ||
| import { execFile } from "node:child_process"; | ||
| { | ||
| name: "run_command", | ||
| description: "Runs a shell command and returns its output", | ||
| parameters: { | ||
| type: "object", | ||
| properties: { | ||
| command: { type: "string", description: "The command to run" }, | ||
| }, | ||
| required: ["command"], | ||
| }, | ||
| handler: async (args) => { | ||
| const isWindows = process.platform === "win32"; | ||
| const shell = isWindows ? "powershell" : "bash"; | ||
| const shellArgs = isWindows | ||
| ? ["-NoProfile", "-Command", args.command] | ||
| : ["-c", args.command]; | ||
| return new Promise((resolve) => { | ||
| execFile(shell, shellArgs, (err, stdout, stderr) => { | ||
| if (err) resolve(`Error: ${stderr || err.message}`); | ||
| else resolve(stdout); | ||
| }); | ||
| }); | ||
| }, | ||
| } | ||
| ``` | ||
| ### Tool that calls an external API | ||
| ```js | ||
| { | ||
| name: "fetch_data", | ||
| description: "Fetches data from an API endpoint", | ||
| parameters: { | ||
| type: "object", | ||
| properties: { | ||
| url: { type: "string", description: "The URL to fetch" }, | ||
| }, | ||
| required: ["url"], | ||
| }, | ||
| handler: async (args) => { | ||
| const res = await fetch(args.url); | ||
| if (!res.ok) return `Error: HTTP ${res.status}`; | ||
| return await res.text(); | ||
| }, | ||
| } | ||
| ``` | ||
| ### Tool handler invocation context | ||
| The handler receives a second argument with invocation metadata: | ||
| ```js | ||
| handler: async (args, invocation) => { | ||
| // invocation.sessionId — current session ID | ||
| // invocation.toolCallId — unique ID for this tool call | ||
| // invocation.toolName — name of the tool being called | ||
| return "done"; | ||
| } | ||
| ``` | ||
| --- | ||
| ## Hooks | ||
| Hooks intercept and modify behavior at key lifecycle points. Register them in the `hooks` option. | ||
| ### Available Hooks | ||
| | Hook | Fires When | Can Modify | | ||
| |------|-----------|------------| | ||
| | `onUserPromptSubmitted` | User sends a message | The prompt text, add context | | ||
| | `onPreToolUse` | Before a tool executes | Tool args, permission decision, add context | | ||
| | `onPostToolUse` | After a tool executes | Tool result, add context | | ||
| | `onSessionStart` | Session starts or resumes | Add context, modify config | | ||
| | `onSessionEnd` | Session ends | Cleanup actions, summary | | ||
| | `onErrorOccurred` | An error occurs | Error handling strategy (retry/skip/abort) | | ||
| All hook inputs include `timestamp` (unix ms) and `cwd` (working directory). | ||
| ### Modifying the user's message | ||
| Use `onUserPromptSubmitted` to rewrite or augment what the user typed before the agent sees it. | ||
| ```js | ||
| hooks: { | ||
| onUserPromptSubmitted: async (input) => { | ||
| // Rewrite the prompt | ||
| return { modifiedPrompt: input.prompt.toUpperCase() }; | ||
| }, | ||
| } | ||
| ``` | ||
| ### Injecting additional context into every message | ||
| Return `additionalContext` to silently append instructions the agent will follow. | ||
| ```js | ||
| hooks: { | ||
| onUserPromptSubmitted: async (input) => { | ||
| return { | ||
| additionalContext: "Always respond in bullet points. Follow our team coding standards.", | ||
| }; | ||
| }, | ||
| } | ||
| ``` | ||
| ### Sending a follow-up message based on a keyword | ||
| Use `session.send()` to programmatically inject a new user message. | ||
| ```js | ||
| hooks: { | ||
| onUserPromptSubmitted: async (input) => { | ||
| if (/\\burgent\\b/i.test(input.prompt)) { | ||
| // Fire-and-forget a follow-up message | ||
| setTimeout(() => session.send({ prompt: "Please prioritize this." }), 0); | ||
| } | ||
| }, | ||
| } | ||
| ``` | ||
| > **Tip:** Guard against infinite loops if your follow-up message could re-trigger the same hook. | ||
| ### Blocking dangerous tool calls | ||
| Use `onPreToolUse` to inspect and optionally deny tool execution. | ||
| ```js | ||
| hooks: { | ||
| onPreToolUse: async (input) => { | ||
| if (input.toolName === "bash") { | ||
| const cmd = String(input.toolArgs?.command || ""); | ||
| if (/rm\\s+-rf/i.test(cmd) || /Remove-Item\\s+.*-Recurse/i.test(cmd)) { | ||
| return { | ||
| permissionDecision: "deny", | ||
| permissionDecisionReason: "Destructive commands are not allowed.", | ||
| }; | ||
| } | ||
| } | ||
| // Allow everything else | ||
| return { permissionDecision: "allow" }; | ||
| }, | ||
| } | ||
| ``` | ||
| ### Modifying tool arguments before execution | ||
| ```js | ||
| hooks: { | ||
| onPreToolUse: async (input) => { | ||
| if (input.toolName === "bash") { | ||
| const redirect = process.platform === "win32" ? "*>&1" : "2>&1"; | ||
| return { | ||
| modifiedArgs: { | ||
| ...input.toolArgs, | ||
| command: `${input.toolArgs.command} ${redirect}`, | ||
| }, | ||
| }; | ||
| } | ||
| }, | ||
| } | ||
| ``` | ||
| ### Reacting when the agent creates or edits a file | ||
| Use `onPostToolUse` to run side effects after a tool completes. | ||
| ```js | ||
| import { exec } from "node:child_process"; | ||
| hooks: { | ||
| onPostToolUse: async (input) => { | ||
| if (input.toolName === "create" || input.toolName === "edit") { | ||
| const filePath = input.toolArgs?.path; | ||
| if (filePath) { | ||
| // Open the file in VS Code | ||
| exec(`code "${filePath}"`, () => {}); | ||
| } | ||
| } | ||
| }, | ||
| } | ||
| ``` | ||
| ### Augmenting tool results with extra context | ||
| ```js | ||
| hooks: { | ||
| onPostToolUse: async (input) => { | ||
| if (input.toolName === "bash" && input.toolResult?.resultType === "failure") { | ||
| return { | ||
| additionalContext: "The command failed. Try a different approach.", | ||
| }; | ||
| } | ||
| }, | ||
| } | ||
| ``` | ||
| ### Running a linter after every file edit | ||
| ```js | ||
| import { exec } from "node:child_process"; | ||
| hooks: { | ||
| onPostToolUse: async (input) => { | ||
| if (input.toolName === "edit") { | ||
| const filePath = input.toolArgs?.path; | ||
| if (filePath?.endsWith(".ts")) { | ||
| const result = await new Promise((resolve) => { | ||
| exec(`npx eslint "${filePath}"`, (err, stdout) => { | ||
| resolve(err ? stdout : "No lint errors."); | ||
| }); | ||
| }); | ||
| return { additionalContext: `Lint result: ${result}` }; | ||
| } | ||
| } | ||
| }, | ||
| } | ||
| ``` | ||
| ### Handling errors with retry logic | ||
| ```js | ||
| hooks: { | ||
| onErrorOccurred: async (input) => { | ||
| if (input.recoverable && input.errorContext === "model_call") { | ||
| return { errorHandling: "retry", retryCount: 2 }; | ||
| } | ||
| return { | ||
| errorHandling: "abort", | ||
| userNotification: `An error occurred: ${input.error}`, | ||
| }; | ||
| }, | ||
| } | ||
| ``` | ||
| ### Session lifecycle hooks | ||
| ```js | ||
| hooks: { | ||
| onSessionStart: async (input) => { | ||
| // input.source is "startup", "resume", or "new" | ||
| return { additionalContext: "Remember to write tests for all changes." }; | ||
| }, | ||
| onSessionEnd: async (input) => { | ||
| // input.reason is "complete", "error", "abort", "timeout", or "user_exit" | ||
| }, | ||
| } | ||
| ``` | ||
| --- | ||
| ## Session Events | ||
| After calling `joinSession`, use `session.on()` to react to events in real time. | ||
| ### Listening to a specific event type | ||
| ```js | ||
| session.on("assistant.message", (event) => { | ||
| // event.data.content has the agent's response text | ||
| }); | ||
| ``` | ||
| ### Listening to all events | ||
| ```js | ||
| session.on((event) => { | ||
| // event.type and event.data are available for all events | ||
| }); | ||
| ``` | ||
| ### Unsubscribing from events | ||
| `session.on()` returns an unsubscribe function: | ||
| ```js | ||
| const unsubscribe = session.on("tool.execution_complete", (event) => { | ||
| // event.data.toolName, event.data.success, event.data.result, event.data.error | ||
| }); | ||
| // Later, stop listening | ||
| unsubscribe(); | ||
| ``` | ||
| ### Example: Auto-copy agent responses to clipboard | ||
| Combine a hook (to detect a keyword) with a session event (to capture the response): | ||
| ```js | ||
| import { execFile } from "node:child_process"; | ||
| let copyNextResponse = false; | ||
| function copyToClipboard(text) { | ||
| const cmd = process.platform === "win32" ? "clip" : "pbcopy"; | ||
| const proc = execFile(cmd, [], () => {}); | ||
| proc.stdin.write(text); | ||
| proc.stdin.end(); | ||
| } | ||
| const session = await joinSession({ | ||
| hooks: { | ||
| onUserPromptSubmitted: async (input) => { | ||
| if (/\\bcopy\\b/i.test(input.prompt)) { | ||
| copyNextResponse = true; | ||
| } | ||
| }, | ||
| }, | ||
| tools: [], | ||
| }); | ||
| session.on("assistant.message", (event) => { | ||
| if (copyNextResponse) { | ||
| copyNextResponse = false; | ||
| copyToClipboard(event.data.content); | ||
| } | ||
| }); | ||
| ``` | ||
| ### Top 10 Most Useful Event Types | ||
| | Event Type | Description | Key Data Fields | | ||
| |-----------|-------------|-----------------| | ||
| | `assistant.message` | Agent's final response | `content`, `messageId`, `toolRequests` | | ||
| | `assistant.streaming_delta` | Token-by-token streaming (ephemeral) | `totalResponseSizeBytes` | | ||
| | `tool.execution_start` | A tool is about to run | `toolCallId`, `toolName`, `arguments` | | ||
| | `tool.execution_complete` | A tool finished running | `toolCallId`, `toolName`, `success`, `result`, `error` | | ||
| | `user.message` | User sent a message | `content`, `attachments`, `source` | | ||
| | `session.idle` | Session finished processing a turn | `backgroundTasks` | | ||
| | `session.error` | An error occurred | `errorType`, `message`, `stack` | | ||
| | `permission.requested` | Agent needs permission (shell, file write, etc.) | `requestId`, `permissionRequest.kind` | | ||
| | `session.shutdown` | Session is ending | `shutdownType`, `totalPremiumRequests`, `codeChanges` | | ||
| | `assistant.turn_start` | Agent begins a new thinking/response cycle | `turnId` | | ||
| ### Example: Detecting when the plan file is created or edited | ||
| Use `session.workspacePath` to locate the session's `plan.md`, then `fs.watchFile` to detect changes. | ||
| Correlate `tool.execution_start` / `tool.execution_complete` events by `toolCallId` to distinguish agent edits from user edits. | ||
| ```js | ||
| import { existsSync, watchFile, readFileSync } from "node:fs"; | ||
| import { join } from "node:path"; | ||
| import { joinSession } from "@github/copilot-sdk/extension"; | ||
| const agentEdits = new Set(); // toolCallIds for in-flight agent edits | ||
| const recentAgentPaths = new Set(); // paths recently written by the agent | ||
| const session = await joinSession(); | ||
| const workspace = session.workspacePath; // e.g. ~/.copilot/session-state/<id> | ||
| if (workspace) { | ||
| const planPath = join(workspace, "plan.md"); | ||
| let lastContent = existsSync(planPath) ? readFileSync(planPath, "utf-8") : null; | ||
| // Track agent edits to suppress false triggers | ||
| session.on("tool.execution_start", (event) => { | ||
| if ((event.data.toolName === "edit" || event.data.toolName === "create") | ||
| && String(event.data.arguments?.path || "").endsWith("plan.md")) { | ||
| agentEdits.add(event.data.toolCallId); | ||
| recentAgentPaths.add(planPath); | ||
| } | ||
| }); | ||
| session.on("tool.execution_complete", (event) => { | ||
| if (agentEdits.delete(event.data.toolCallId)) { | ||
| setTimeout(() => { | ||
| recentAgentPaths.delete(planPath); | ||
| lastContent = existsSync(planPath) ? readFileSync(planPath, "utf-8") : null; | ||
| }, 2000); | ||
| } | ||
| }); | ||
| watchFile(planPath, { interval: 1000 }, () => { | ||
| if (recentAgentPaths.has(planPath) || agentEdits.size > 0) return; | ||
| const content = existsSync(planPath) ? readFileSync(planPath, "utf-8") : null; | ||
| if (content === lastContent) return; | ||
| const wasCreated = lastContent === null && content !== null; | ||
| lastContent = content; | ||
| if (content !== null) { | ||
| session.send({ | ||
| prompt: `The plan was ${wasCreated ? "created" : "edited"} by the user.`, | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| ``` | ||
| ### Example: Reacting when the user manually edits any file in the repo | ||
| Use `fs.watch` with `recursive: true` on `process.cwd()` to detect file changes. | ||
| Filter out agent edits by tracking `tool.execution_start` / `tool.execution_complete` events. | ||
| ```js | ||
| import { watch, readFileSync, statSync } from "node:fs"; | ||
| import { join, relative, resolve } from "node:path"; | ||
| import { joinSession } from "@github/copilot-sdk/extension"; | ||
| const agentEditPaths = new Set(); | ||
| const session = await joinSession(); | ||
| const cwd = process.cwd(); | ||
| const IGNORE = new Set(["node_modules", ".git", "dist"]); | ||
| // Track agent file edits | ||
| session.on("tool.execution_start", (event) => { | ||
| if (event.data.toolName === "edit" || event.data.toolName === "create") { | ||
| const p = String(event.data.arguments?.path || ""); | ||
| if (p) agentEditPaths.add(resolve(p)); | ||
| } | ||
| }); | ||
| session.on("tool.execution_complete", (event) => { | ||
| // Clear after a delay to avoid race with fs.watch | ||
| const p = [...agentEditPaths].find((x) => x); // any tracked path | ||
| setTimeout(() => agentEditPaths.clear(), 3000); | ||
| }); | ||
| const debounce = new Map(); | ||
| watch(cwd, { recursive: true }, (eventType, filename) => { | ||
| if (!filename || eventType !== "change") return; | ||
| if (filename.split(/[\\\\\\/]/).some((p) => IGNORE.has(p))) return; | ||
| if (debounce.has(filename)) clearTimeout(debounce.get(filename)); | ||
| debounce.set(filename, setTimeout(() => { | ||
| debounce.delete(filename); | ||
| const fullPath = join(cwd, filename); | ||
| if (agentEditPaths.has(resolve(fullPath))) return; | ||
| try { if (!statSync(fullPath).isFile()) return; } catch { return; } | ||
| const relPath = relative(cwd, fullPath); | ||
| session.send({ | ||
| prompt: `The user edited \\`${relPath}\\`.`, | ||
| attachments: [{ type: "file", path: fullPath }], | ||
| }); | ||
| }, 500)); | ||
| }); | ||
| ``` | ||
| --- | ||
| ## Sending Messages Programmatically | ||
| ### Fire-and-forget | ||
| ```js | ||
| await session.send({ prompt: "Analyze the test results." }); | ||
| ``` | ||
| ### Send and wait for the response | ||
| ```js | ||
| const response = await session.sendAndWait({ prompt: "What is 2 + 2?" }); | ||
| // response?.data.content contains the agent's reply | ||
| ``` | ||
| ### Send with file attachments | ||
| ```js | ||
| await session.send({ | ||
| prompt: "Review this file", | ||
| attachments: [ | ||
| { type: "file", path: "./src/index.ts" }, | ||
| ], | ||
| }); | ||
| ``` | ||
| --- | ||
| ## Permission and User Input Handlers | ||
| ### Custom permission logic | ||
| ```js | ||
| const session = await joinSession({ | ||
| onPermissionRequest: async (request) => { | ||
| if (request.kind === "shell") { | ||
| // request.fullCommandText has the shell command | ||
| return { kind: "approved" }; | ||
| } | ||
| if (request.kind === "write") { | ||
| return { kind: "approved" }; | ||
| } | ||
| return { kind: "denied-by-rules" }; | ||
| }, | ||
| }); | ||
| ``` | ||
| ### Handling agent questions (ask_user) | ||
| Register `onUserInputRequest` to enable the agent's `ask_user` tool: | ||
| ```js | ||
| const session = await joinSession({ | ||
| onUserInputRequest: async (request) => { | ||
| // request.question has the agent's question | ||
| // request.choices has the options (if multiple choice) | ||
| return { answer: "yes", wasFreeform: false }; | ||
| }, | ||
| }); | ||
| ``` | ||
| --- | ||
| ## Complete Example: Multi-Feature Extension | ||
| An extension that combines tools, hooks, and events. | ||
| ```js | ||
| import { execFile, exec } from "node:child_process"; | ||
| import { joinSession } from "@github/copilot-sdk/extension"; | ||
| const isWindows = process.platform === "win32"; | ||
| let copyNextResponse = false; | ||
| function copyToClipboard(text) { | ||
| const proc = execFile(isWindows ? "clip" : "pbcopy", [], () => {}); | ||
| proc.stdin.write(text); | ||
| proc.stdin.end(); | ||
| } | ||
| function openInEditor(filePath) { | ||
| if (isWindows) exec(`code "${filePath}"`, () => {}); | ||
| else execFile("code", [filePath], () => {}); | ||
| } | ||
| const session = await joinSession({ | ||
| hooks: { | ||
| onUserPromptSubmitted: async (input) => { | ||
| if (/\\bcopy this\\b/i.test(input.prompt)) { | ||
| copyNextResponse = true; | ||
| } | ||
| return { | ||
| additionalContext: "Follow our team style guide. Use 4-space indentation.", | ||
| }; | ||
| }, | ||
| onPreToolUse: async (input) => { | ||
| if (input.toolName === "bash") { | ||
| const cmd = String(input.toolArgs?.command || ""); | ||
| if (/rm\\s+-rf\\s+\\//i.test(cmd) || /Remove-Item\\s+.*-Recurse/i.test(cmd)) { | ||
| return { permissionDecision: "deny" }; | ||
| } | ||
| } | ||
| }, | ||
| onPostToolUse: async (input) => { | ||
| if (input.toolName === "create" || input.toolName === "edit") { | ||
| const filePath = input.toolArgs?.path; | ||
| if (filePath) openInEditor(filePath); | ||
| } | ||
| }, | ||
| }, | ||
| tools: [ | ||
| { | ||
| name: "copy_to_clipboard", | ||
| description: "Copies text to the system clipboard.", | ||
| parameters: { | ||
| type: "object", | ||
| properties: { | ||
| text: { type: "string", description: "Text to copy" }, | ||
| }, | ||
| required: ["text"], | ||
| }, | ||
| handler: async (args) => { | ||
| return new Promise((resolve) => { | ||
| const proc = execFile(isWindows ? "clip" : "pbcopy", [], (err) => { | ||
| if (err) resolve(`Error: ${err.message}`); | ||
| else resolve("Copied to clipboard."); | ||
| }); | ||
| proc.stdin.write(args.text); | ||
| proc.stdin.end(); | ||
| }); | ||
| }, | ||
| }, | ||
| ], | ||
| }); | ||
| session.on("assistant.message", (event) => { | ||
| if (copyNextResponse) { | ||
| copyNextResponse = false; | ||
| copyToClipboard(event.data.content); | ||
| } | ||
| }); | ||
| session.on("tool.execution_complete", (event) => { | ||
| // event.data.success, event.data.toolName, event.data.result | ||
| }); | ||
| ``` | ||
| # Copilot CLI Extensions | ||
| Extensions add custom tools, hooks, and behaviors to the Copilot CLI. They run as separate Node.js processes that communicate with the CLI over JSON-RPC via stdio. | ||
| ## How Extensions Work | ||
| ``` | ||
| ┌─────────────────────┐ JSON-RPC / stdio ┌──────────────────────┐ | ||
| │ Copilot CLI │ ◄──────────────────────────────────► │ Extension Process │ | ||
| │ (parent process) │ tool calls, events, hooks │ (forked child) │ | ||
| │ │ │ │ | ||
| │ • Discovers exts │ │ • Registers tools │ | ||
| │ • Forks processes │ │ • Registers hooks │ | ||
| │ • Routes tool calls │ │ • Listens to events │ | ||
| │ • Manages lifecycle │ │ • Uses SDK APIs │ | ||
| └─────────────────────┘ └──────────────────────┘ | ||
| ``` | ||
| 1. **Discovery**: The CLI scans `.github/extensions/` (project) and the user's copilot config extensions directory for subdirectories containing `extension.mjs`. | ||
| 2. **Launch**: Each extension is forked as a child process with `@github/copilot-sdk` available via an automatic module resolver. | ||
| 3. **Connection**: The extension calls `joinSession()` which establishes a JSON-RPC connection over stdio to the CLI and attaches to the user's current foreground session. | ||
| 4. **Registration**: Tools and hooks declared in the session options are registered with the CLI and become available to the agent. | ||
| 5. **Lifecycle**: Extensions are reloaded on `/clear` (or if the foreground session is replaced) and stopped on CLI exit (SIGTERM, then SIGKILL after 5s). | ||
| ## File Structure | ||
| ``` | ||
| .github/extensions/ | ||
| my-extension/ | ||
| extension.mjs ← Entry point (required, must be .mjs) | ||
| ``` | ||
| - Only `.mjs` files are supported (ES modules). The file must be named `extension.mjs`. | ||
| - Each extension lives in its own subdirectory. | ||
| - The `@github/copilot-sdk` import is resolved automatically — you don't install it. | ||
| ## The SDK | ||
| Extensions use `@github/copilot-sdk` for all interactions with the CLI: | ||
| ```js | ||
| import { joinSession } from "@github/copilot-sdk/extension"; | ||
| const session = await joinSession({ | ||
| tools: [ | ||
| /* ... */ | ||
| ], | ||
| hooks: { | ||
| /* ... */ | ||
| }, | ||
| }); | ||
| ``` | ||
| The `session` object provides methods for sending messages, logging to the timeline, listening to events, and accessing the RPC API. See the `.d.ts` files in the SDK package for full type information. | ||
| ## Further Reading | ||
| - `examples.md` — Practical code examples for tools, hooks, events, and complete extensions | ||
| - `agent-author.md` — Step-by-step workflow for agents authoring extensions programmatically |
+38
-1
| import { createServerRpc } from "./generated/rpc.js"; | ||
| import { CopilotSession } from "./session.js"; | ||
| import type { ConnectionState, CopilotClientOptions, GetAuthStatusResponse, GetStatusResponse, ModelInfo, ResumeSessionConfig, SessionConfig, SessionLifecycleEventType, SessionLifecycleHandler, SessionListFilter, SessionMetadata, TypedSessionLifecycleHandler } from "./types.js"; | ||
| /** | ||
| * Main client for interacting with the Copilot CLI. | ||
| * | ||
| * The CopilotClient manages the connection to the Copilot CLI server and provides | ||
| * methods to create and manage conversation sessions. It can either spawn a CLI | ||
| * server process or connect to an existing server. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * import { CopilotClient } from "@github/copilot-sdk"; | ||
| * | ||
| * // Create a client with default options (spawns CLI server) | ||
| * const client = new CopilotClient(); | ||
| * | ||
| * // Or connect to an existing server | ||
| * const client = new CopilotClient({ cliUrl: "localhost:3000" }); | ||
| * | ||
| * // Create a session | ||
| * const session = await client.createSession({ onPermissionRequest: approveAll, model: "gpt-4" }); | ||
| * | ||
| * // Send messages and handle responses | ||
| * session.on((event) => { | ||
| * if (event.type === "assistant.message") { | ||
| * console.log(event.data.content); | ||
| * } | ||
| * }); | ||
| * await session.send({ prompt: "Hello!" }); | ||
| * | ||
| * // Clean up | ||
| * await session.disconnect(); | ||
| * await client.stop(); | ||
| * ``` | ||
| */ | ||
| export declare class CopilotClient { | ||
@@ -16,2 +49,3 @@ private cliProcess; | ||
| private forceStopping; | ||
| private onListModels?; | ||
| private modelsCache; | ||
@@ -222,6 +256,9 @@ private modelsCacheLock; | ||
| * | ||
| * If an `onListModels` handler was provided in the client options, | ||
| * it is called instead of querying the CLI server. | ||
| * | ||
| * Results are cached after the first successful call to avoid rate limiting. | ||
| * The cache is cleared when the client disconnects. | ||
| * | ||
| * @throws Error if not authenticated | ||
| * @throws Error if not connected (when no custom handler is set) | ||
| */ | ||
@@ -228,0 +265,0 @@ listModels(): Promise<ModelInfo[]>; |
+113
-73
| import { spawn } from "node:child_process"; | ||
| import { randomUUID } from "node:crypto"; | ||
| import { existsSync } from "node:fs"; | ||
@@ -13,3 +14,3 @@ import { Socket } from "node:net"; | ||
| import { getSdkProtocolVersion } from "./sdkProtocolVersion.js"; | ||
| import { CopilotSession } from "./session.js"; | ||
| import { CopilotSession, NO_RESULT_PERMISSION_V2_ERROR } from "./session.js"; | ||
| const MIN_PROTOCOL_VERSION = 2; | ||
@@ -50,2 +51,3 @@ function isZodSchema(value) { | ||
| forceStopping = false; | ||
| onListModels; | ||
| modelsCache = null; | ||
@@ -116,4 +118,5 @@ modelsCacheLock = Promise.resolve(); | ||
| } | ||
| this.onListModels = options.onListModels; | ||
| this.options = { | ||
| cliPath: options.cliPath || getBundledCliPath(), | ||
| cliPath: options.cliUrl ? void 0 : options.cliPath || getBundledCliPath(), | ||
| cliArgs: options.cliArgs ?? [], | ||
@@ -384,32 +387,4 @@ cwd: options.cwd ?? process.cwd(), | ||
| } | ||
| const response = await this.connection.sendRequest("session.create", { | ||
| model: config.model, | ||
| sessionId: config.sessionId, | ||
| clientName: config.clientName, | ||
| reasoningEffort: config.reasoningEffort, | ||
| tools: config.tools?.map((tool) => ({ | ||
| name: tool.name, | ||
| description: tool.description, | ||
| parameters: toJsonSchema(tool.parameters), | ||
| overridesBuiltInTool: tool.overridesBuiltInTool | ||
| })), | ||
| systemMessage: config.systemMessage, | ||
| availableTools: config.availableTools, | ||
| excludedTools: config.excludedTools, | ||
| provider: config.provider, | ||
| requestPermission: true, | ||
| requestUserInput: !!config.onUserInputRequest, | ||
| hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), | ||
| workingDirectory: config.workingDirectory, | ||
| streaming: config.streaming, | ||
| mcpServers: config.mcpServers, | ||
| envValueMode: "direct", | ||
| customAgents: config.customAgents, | ||
| configDir: config.configDir, | ||
| skillDirectories: config.skillDirectories, | ||
| disabledSkills: config.disabledSkills, | ||
| infiniteSessions: config.infiniteSessions | ||
| }); | ||
| const { sessionId, workspacePath } = response; | ||
| const session = new CopilotSession(sessionId, this.connection, workspacePath); | ||
| const sessionId = config.sessionId ?? randomUUID(); | ||
| const session = new CopilotSession(sessionId, this.connection); | ||
| session.registerTools(config.tools); | ||
@@ -423,3 +398,42 @@ session.registerPermissionHandler(config.onPermissionRequest); | ||
| } | ||
| if (config.onEvent) { | ||
| session.on(config.onEvent); | ||
| } | ||
| this.sessions.set(sessionId, session); | ||
| try { | ||
| const response = await this.connection.sendRequest("session.create", { | ||
| model: config.model, | ||
| sessionId, | ||
| clientName: config.clientName, | ||
| reasoningEffort: config.reasoningEffort, | ||
| tools: config.tools?.map((tool) => ({ | ||
| name: tool.name, | ||
| description: tool.description, | ||
| parameters: toJsonSchema(tool.parameters), | ||
| overridesBuiltInTool: tool.overridesBuiltInTool | ||
| })), | ||
| systemMessage: config.systemMessage, | ||
| availableTools: config.availableTools, | ||
| excludedTools: config.excludedTools, | ||
| provider: config.provider, | ||
| requestPermission: true, | ||
| requestUserInput: !!config.onUserInputRequest, | ||
| hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), | ||
| workingDirectory: config.workingDirectory, | ||
| streaming: config.streaming, | ||
| mcpServers: config.mcpServers, | ||
| envValueMode: "direct", | ||
| customAgents: config.customAgents, | ||
| agent: config.agent, | ||
| configDir: config.configDir, | ||
| skillDirectories: config.skillDirectories, | ||
| disabledSkills: config.disabledSkills, | ||
| infiniteSessions: config.infiniteSessions | ||
| }); | ||
| const { workspacePath } = response; | ||
| session["_workspacePath"] = workspacePath; | ||
| } catch (e) { | ||
| this.sessions.delete(sessionId); | ||
| throw e; | ||
| } | ||
| return session; | ||
@@ -464,33 +478,3 @@ } | ||
| } | ||
| const response = await this.connection.sendRequest("session.resume", { | ||
| sessionId, | ||
| clientName: config.clientName, | ||
| model: config.model, | ||
| reasoningEffort: config.reasoningEffort, | ||
| systemMessage: config.systemMessage, | ||
| availableTools: config.availableTools, | ||
| excludedTools: config.excludedTools, | ||
| tools: config.tools?.map((tool) => ({ | ||
| name: tool.name, | ||
| description: tool.description, | ||
| parameters: toJsonSchema(tool.parameters), | ||
| overridesBuiltInTool: tool.overridesBuiltInTool | ||
| })), | ||
| provider: config.provider, | ||
| requestPermission: true, | ||
| requestUserInput: !!config.onUserInputRequest, | ||
| hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), | ||
| workingDirectory: config.workingDirectory, | ||
| configDir: config.configDir, | ||
| streaming: config.streaming, | ||
| mcpServers: config.mcpServers, | ||
| envValueMode: "direct", | ||
| customAgents: config.customAgents, | ||
| skillDirectories: config.skillDirectories, | ||
| disabledSkills: config.disabledSkills, | ||
| infiniteSessions: config.infiniteSessions, | ||
| disableResume: config.disableResume | ||
| }); | ||
| const { sessionId: resumedSessionId, workspacePath } = response; | ||
| const session = new CopilotSession(resumedSessionId, this.connection, workspacePath); | ||
| const session = new CopilotSession(sessionId, this.connection); | ||
| session.registerTools(config.tools); | ||
@@ -504,3 +488,43 @@ session.registerPermissionHandler(config.onPermissionRequest); | ||
| } | ||
| this.sessions.set(resumedSessionId, session); | ||
| if (config.onEvent) { | ||
| session.on(config.onEvent); | ||
| } | ||
| this.sessions.set(sessionId, session); | ||
| try { | ||
| const response = await this.connection.sendRequest("session.resume", { | ||
| sessionId, | ||
| clientName: config.clientName, | ||
| model: config.model, | ||
| reasoningEffort: config.reasoningEffort, | ||
| systemMessage: config.systemMessage, | ||
| availableTools: config.availableTools, | ||
| excludedTools: config.excludedTools, | ||
| tools: config.tools?.map((tool) => ({ | ||
| name: tool.name, | ||
| description: tool.description, | ||
| parameters: toJsonSchema(tool.parameters), | ||
| overridesBuiltInTool: tool.overridesBuiltInTool | ||
| })), | ||
| provider: config.provider, | ||
| requestPermission: true, | ||
| requestUserInput: !!config.onUserInputRequest, | ||
| hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), | ||
| workingDirectory: config.workingDirectory, | ||
| configDir: config.configDir, | ||
| streaming: config.streaming, | ||
| mcpServers: config.mcpServers, | ||
| envValueMode: "direct", | ||
| customAgents: config.customAgents, | ||
| agent: config.agent, | ||
| skillDirectories: config.skillDirectories, | ||
| disabledSkills: config.disabledSkills, | ||
| infiniteSessions: config.infiniteSessions, | ||
| disableResume: config.disableResume | ||
| }); | ||
| const { workspacePath } = response; | ||
| session["_workspacePath"] = workspacePath; | ||
| } catch (e) { | ||
| this.sessions.delete(sessionId); | ||
| throw e; | ||
| } | ||
| return session; | ||
@@ -566,11 +590,11 @@ } | ||
| * | ||
| * If an `onListModels` handler was provided in the client options, | ||
| * it is called instead of querying the CLI server. | ||
| * | ||
| * Results are cached after the first successful call to avoid rate limiting. | ||
| * The cache is cleared when the client disconnects. | ||
| * | ||
| * @throws Error if not authenticated | ||
| * @throws Error if not connected (when no custom handler is set) | ||
| */ | ||
| async listModels() { | ||
| if (!this.connection) { | ||
| throw new Error("Client not connected"); | ||
| } | ||
| await this.modelsCacheLock; | ||
@@ -585,6 +609,14 @@ let resolveLock; | ||
| } | ||
| const result = await this.connection.sendRequest("models.list", {}); | ||
| const response = result; | ||
| const models = response.models; | ||
| this.modelsCache = models; | ||
| let models; | ||
| if (this.onListModels) { | ||
| models = await this.onListModels(); | ||
| } else { | ||
| if (!this.connection) { | ||
| throw new Error("Client not connected"); | ||
| } | ||
| const result = await this.connection.sendRequest("models.list", {}); | ||
| const response = result; | ||
| models = response.models; | ||
| } | ||
| this.modelsCache = [...models]; | ||
| return [...models]; | ||
@@ -802,2 +834,7 @@ } finally { | ||
| } | ||
| if (!this.options.cliPath) { | ||
| throw new Error( | ||
| "Path to Copilot CLI is required. Please provide it via the cliPath option, or use cliUrl to rely on a remote CLI." | ||
| ); | ||
| } | ||
| if (!existsSync(this.options.cliPath)) { | ||
@@ -1134,3 +1171,6 @@ throw new Error( | ||
| return { result }; | ||
| } catch (_error) { | ||
| } catch (error) { | ||
| if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) { | ||
| throw error; | ||
| } | ||
| return { | ||
@@ -1137,0 +1177,0 @@ result: { |
+19
-2
@@ -1,2 +0,19 @@ | ||
| import { CopilotClient } from "./client.js"; | ||
| export declare const extension: CopilotClient; | ||
| import type { CopilotSession } from "./session.js"; | ||
| import type { PermissionHandler, ResumeSessionConfig } from "./types.js"; | ||
| export type JoinSessionConfig = Omit<ResumeSessionConfig, "onPermissionRequest"> & { | ||
| onPermissionRequest?: PermissionHandler; | ||
| }; | ||
| /** | ||
| * Joins the current foreground session. | ||
| * | ||
| * @param config - Configuration to add to the session | ||
| * @returns A promise that resolves with the joined session | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * import { joinSession } from "@github/copilot-sdk/extension"; | ||
| * | ||
| * const session = await joinSession({ tools: [myTool] }); | ||
| * ``` | ||
| */ | ||
| export declare function joinSession(config?: JoinSessionConfig): Promise<CopilotSession>; |
+18
-2
| import { CopilotClient } from "./client.js"; | ||
| const extension = new CopilotClient({ isChildProcess: true }); | ||
| const defaultJoinSessionPermissionHandler = () => ({ | ||
| kind: "no-result" | ||
| }); | ||
| async function joinSession(config = {}) { | ||
| const sessionId = process.env.SESSION_ID; | ||
| if (!sessionId) { | ||
| throw new Error( | ||
| "joinSession() is intended for extensions running as child processes of the Copilot CLI." | ||
| ); | ||
| } | ||
| const client = new CopilotClient({ isChildProcess: true }); | ||
| return client.resumeSession(sessionId, { | ||
| ...config, | ||
| onPermissionRequest: config.onPermissionRequest ?? defaultJoinSessionPermissionHandler, | ||
| disableResume: config.disableResume ?? true | ||
| }); | ||
| } | ||
| export { | ||
| extension | ||
| joinSession | ||
| }; |
+119
-0
@@ -43,3 +43,9 @@ /** | ||
| capabilities: { | ||
| /** | ||
| * Feature flags indicating what the model supports | ||
| */ | ||
| supports: { | ||
| /** | ||
| * Whether this model supports vision/image input | ||
| */ | ||
| vision?: boolean; | ||
@@ -51,5 +57,17 @@ /** | ||
| }; | ||
| /** | ||
| * Token limits for prompts, outputs, and context window | ||
| */ | ||
| limits: { | ||
| /** | ||
| * Maximum number of prompt/input tokens | ||
| */ | ||
| max_prompt_tokens?: number; | ||
| /** | ||
| * Maximum number of output/completion tokens | ||
| */ | ||
| max_output_tokens?: number; | ||
| /** | ||
| * Maximum total context window size in tokens | ||
| */ | ||
| max_context_window_tokens: number; | ||
@@ -62,3 +80,9 @@ }; | ||
| policy?: { | ||
| /** | ||
| * Current policy state for this model | ||
| */ | ||
| state: string; | ||
| /** | ||
| * Usage terms or conditions for this model | ||
| */ | ||
| terms: string; | ||
@@ -70,2 +94,5 @@ }; | ||
| billing?: { | ||
| /** | ||
| * Billing cost multiplier relative to the base rate | ||
| */ | ||
| multiplier: number; | ||
@@ -152,2 +179,5 @@ }; | ||
| export interface SessionModelGetCurrentResult { | ||
| /** | ||
| * Currently active model identifier | ||
| */ | ||
| modelId?: string; | ||
@@ -162,2 +192,5 @@ } | ||
| export interface SessionModelSwitchToResult { | ||
| /** | ||
| * Currently active model identifier after the switch | ||
| */ | ||
| modelId?: string; | ||
@@ -170,3 +203,10 @@ } | ||
| sessionId: string; | ||
| /** | ||
| * Model identifier to switch to | ||
| */ | ||
| modelId: string; | ||
| /** | ||
| * Reasoning effort level to use for the model | ||
| */ | ||
| reasoningEffort?: string; | ||
| } | ||
@@ -409,2 +449,5 @@ export interface SessionModeGetResult { | ||
| export interface SessionToolsHandlePendingToolCallResult { | ||
| /** | ||
| * Whether the tool call result was handled successfully | ||
| */ | ||
| success: boolean; | ||
@@ -429,2 +472,5 @@ } | ||
| export interface SessionPermissionsHandlePendingPermissionRequestResult { | ||
| /** | ||
| * Whether the permission request was handled successfully | ||
| */ | ||
| success: boolean; | ||
@@ -454,2 +500,70 @@ } | ||
| } | ||
| export interface SessionLogResult { | ||
| /** | ||
| * The unique identifier of the emitted session event | ||
| */ | ||
| eventId: string; | ||
| } | ||
| export interface SessionLogParams { | ||
| /** | ||
| * Target session identifier | ||
| */ | ||
| sessionId: string; | ||
| /** | ||
| * Human-readable message | ||
| */ | ||
| message: string; | ||
| /** | ||
| * Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". | ||
| */ | ||
| level?: "info" | "warning" | "error"; | ||
| /** | ||
| * When true, the message is transient and not persisted to the session event log on disk | ||
| */ | ||
| ephemeral?: boolean; | ||
| } | ||
| export interface SessionShellExecResult { | ||
| /** | ||
| * Unique identifier for tracking streamed output | ||
| */ | ||
| processId: string; | ||
| } | ||
| export interface SessionShellExecParams { | ||
| /** | ||
| * Target session identifier | ||
| */ | ||
| sessionId: string; | ||
| /** | ||
| * Shell command to execute | ||
| */ | ||
| command: string; | ||
| /** | ||
| * Working directory (defaults to session working directory) | ||
| */ | ||
| cwd?: string; | ||
| /** | ||
| * Timeout in milliseconds (default: 30000) | ||
| */ | ||
| timeout?: number; | ||
| } | ||
| export interface SessionShellKillResult { | ||
| /** | ||
| * Whether the signal was sent successfully | ||
| */ | ||
| killed: boolean; | ||
| } | ||
| export interface SessionShellKillParams { | ||
| /** | ||
| * Target session identifier | ||
| */ | ||
| sessionId: string; | ||
| /** | ||
| * Process identifier returned by shell.exec | ||
| */ | ||
| processId: string; | ||
| /** | ||
| * Signal to send (default: SIGTERM) | ||
| */ | ||
| signal?: "SIGTERM" | "SIGKILL" | "SIGINT"; | ||
| } | ||
| /** Create typed server-scoped RPC methods (no session required). */ | ||
@@ -506,2 +620,7 @@ export declare function createServerRpc(connection: MessageConnection): { | ||
| }; | ||
| log: (params: Omit<SessionLogParams, "sessionId">) => Promise<SessionLogResult>; | ||
| shell: { | ||
| exec: (params: Omit<SessionShellExecParams, "sessionId">) => Promise<SessionShellExecResult>; | ||
| kill: (params: Omit<SessionShellKillParams, "sessionId">) => Promise<SessionShellKillResult>; | ||
| }; | ||
| }; |
@@ -52,2 +52,7 @@ function createServerRpc(connection) { | ||
| handlePendingPermissionRequest: async (params) => connection.sendRequest("session.permissions.handlePendingPermissionRequest", { sessionId, ...params }) | ||
| }, | ||
| log: async (params) => connection.sendRequest("session.log", { sessionId, ...params }), | ||
| shell: { | ||
| exec: async (params) => connection.sendRequest("session.shell.exec", { sessionId, ...params }), | ||
| kill: async (params) => connection.sendRequest("session.shell.kill", { sessionId, ...params }) | ||
| } | ||
@@ -54,0 +59,0 @@ }; |
+23
-2
@@ -5,5 +5,6 @@ /** | ||
| */ | ||
| import type { MessageConnection } from "vscode-jsonrpc/node"; | ||
| import type { MessageConnection } from "vscode-jsonrpc/node.js"; | ||
| import { createSessionRpc } from "./generated/rpc.js"; | ||
| import type { MessageOptions, PermissionHandler, PermissionRequestResult, SessionEvent, SessionEventHandler, SessionEventType, SessionHooks, Tool, ToolHandler, TypedSessionEventHandler, UserInputHandler, UserInputResponse } from "./types.js"; | ||
| export declare const NO_RESULT_PERMISSION_V2_ERROR = "Permission handlers cannot return 'no-result' when connected to a protocol v2 server."; | ||
| /** Assistant message event - the final response from the assistant. */ | ||
@@ -41,3 +42,3 @@ export type AssistantMessageEvent = Extract<SessionEvent, { | ||
| private connection; | ||
| private readonly _workspacePath?; | ||
| private _workspacePath?; | ||
| private eventHandlers; | ||
@@ -344,2 +345,22 @@ private typedEventHandlers; | ||
| setModel(model: string): Promise<void>; | ||
| /** | ||
| * Log a message to the session timeline. | ||
| * The message appears in the session event stream and is visible to SDK consumers | ||
| * and (for non-ephemeral messages) persisted to the session event log on disk. | ||
| * | ||
| * @param message - Human-readable message text | ||
| * @param options - Optional log level and ephemeral flag | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * await session.log("Processing started"); | ||
| * await session.log("Disk usage high", { level: "warning" }); | ||
| * await session.log("Connection failed", { level: "error" }); | ||
| * await session.log("Debug info", { ephemeral: true }); | ||
| * ``` | ||
| */ | ||
| log(message: string, options?: { | ||
| level?: "info" | "warning" | "error"; | ||
| ephemeral?: boolean; | ||
| }): Promise<void>; | ||
| } |
+33
-3
@@ -1,3 +0,4 @@ | ||
| import { ConnectionError, ResponseError } from "vscode-jsonrpc/node"; | ||
| import { ConnectionError, ResponseError } from "vscode-jsonrpc/node.js"; | ||
| import { createSessionRpc } from "./generated/rpc.js"; | ||
| const NO_RESULT_PERMISSION_V2_ERROR = "Permission handlers cannot return 'no-result' when connected to a protocol v2 server."; | ||
| class CopilotSession { | ||
@@ -242,2 +243,5 @@ /** | ||
| }); | ||
| if (result.kind === "no-result") { | ||
| return; | ||
| } | ||
| await this.rpc.permissions.handlePendingPermissionRequest({ requestId, result }); | ||
@@ -339,4 +343,10 @@ } catch (_error) { | ||
| }); | ||
| if (result.kind === "no-result") { | ||
| throw new Error(NO_RESULT_PERMISSION_V2_ERROR); | ||
| } | ||
| return result; | ||
| } catch (_error) { | ||
| } catch (error) { | ||
| if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) { | ||
| throw error; | ||
| } | ||
| return { kind: "denied-no-approval-rule-and-could-not-request-from-user" }; | ||
@@ -506,5 +516,25 @@ } | ||
| } | ||
| /** | ||
| * Log a message to the session timeline. | ||
| * The message appears in the session event stream and is visible to SDK consumers | ||
| * and (for non-ephemeral messages) persisted to the session event log on disk. | ||
| * | ||
| * @param message - Human-readable message text | ||
| * @param options - Optional log level and ephemeral flag | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * await session.log("Processing started"); | ||
| * await session.log("Disk usage high", { level: "warning" }); | ||
| * await session.log("Connection failed", { level: "error" }); | ||
| * await session.log("Debug info", { ephemeral: true }); | ||
| * ``` | ||
| */ | ||
| async log(message, options) { | ||
| await this.rpc.log({ message, ...options }); | ||
| } | ||
| } | ||
| export { | ||
| CopilotSession | ||
| CopilotSession, | ||
| NO_RESULT_PERMISSION_V2_ERROR | ||
| }; |
+27
-2
@@ -80,2 +80,9 @@ /** | ||
| useLoggedInUser?: boolean; | ||
| /** | ||
| * Custom handler for listing available models. | ||
| * When provided, client.listModels() calls this handler instead of | ||
| * querying the CLI server. Useful in BYOK mode to return models | ||
| * available from your custom provider. | ||
| */ | ||
| onListModels?: () => Promise<ModelInfo[]> | ModelInfo[]; | ||
| } | ||
@@ -190,3 +197,5 @@ /** | ||
| import type { SessionPermissionsHandlePendingPermissionRequestParams } from "./generated/rpc.js"; | ||
| export type PermissionRequestResult = SessionPermissionsHandlePendingPermissionRequestParams["result"]; | ||
| export type PermissionRequestResult = SessionPermissionsHandlePendingPermissionRequestParams["result"] | { | ||
| kind: "no-result"; | ||
| }; | ||
| export type PermissionHandler = (request: PermissionRequest, invocation: { | ||
@@ -592,2 +601,8 @@ sessionId: string; | ||
| /** | ||
| * Name of the custom agent to activate when the session starts. | ||
| * Must match the `name` of one of the agents in `customAgents`. | ||
| * Equivalent to calling `session.rpc.agent.select({ name })` after creation. | ||
| */ | ||
| agent?: string; | ||
| /** | ||
| * Directories to load skills from. | ||
@@ -606,2 +621,12 @@ */ | ||
| infiniteSessions?: InfiniteSessionConfig; | ||
| /** | ||
| * Optional event handler that is registered on the session before the | ||
| * session.create RPC is issued. This guarantees that early events emitted | ||
| * by the CLI during session creation (e.g. session.start) are delivered to | ||
| * the handler. | ||
| * | ||
| * Equivalent to calling `session.on(handler)` immediately after creation, | ||
| * but executes earlier in the lifecycle so no events are missed. | ||
| */ | ||
| onEvent?: SessionEventHandler; | ||
| } | ||
@@ -611,3 +636,3 @@ /** | ||
| */ | ||
| export type ResumeSessionConfig = Pick<SessionConfig, "clientName" | "model" | "tools" | "systemMessage" | "availableTools" | "excludedTools" | "provider" | "streaming" | "reasoningEffort" | "onPermissionRequest" | "onUserInputRequest" | "hooks" | "workingDirectory" | "configDir" | "mcpServers" | "customAgents" | "skillDirectories" | "disabledSkills" | "infiniteSessions"> & { | ||
| 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" | "onEvent"> & { | ||
| /** | ||
@@ -614,0 +639,0 @@ * When true, skips emitting the session.resume event. |
+3
-2
@@ -7,3 +7,3 @@ { | ||
| }, | ||
| "version": "0.1.32", | ||
| "version": "0.1.33-preview.0", | ||
| "description": "TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC", | ||
@@ -48,3 +48,3 @@ "main": "./dist/index.js", | ||
| "dependencies": { | ||
| "@github/copilot": "^1.0.2", | ||
| "@github/copilot": "^1.0.4", | ||
| "vscode-jsonrpc": "^8.2.1", | ||
@@ -75,4 +75,5 @@ "zod": "^4.3.6" | ||
| "dist/**/*", | ||
| "docs/**/*", | ||
| "README.md" | ||
| ] | ||
| } |
Sorry, the diff of this file is too big to display
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
283113
23.74%21
16.67%7228
10.88%3
50%Updated