Awesome-omni-skill copilot-sdk-nodejs
This file provides guidance on building Node.js/TypeScript applications using GitHub Copilot SDK. Triggers on: **.ts, **.js, package.json
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/backend/copilot-sdk-nodejs" ~/.claude/skills/diegosouzapw-awesome-omni-skill-copilot-sdk-nodejs && rm -rf "$T"
skills/backend/copilot-sdk-nodejs/SKILL.md- references .env files
- references API keys
Core Principles
- The SDK is in technical preview and may have breaking changes
- Requires Node.js 18.0 or later
- Requires GitHub Copilot CLI installed and in PATH
- Built with TypeScript for type safety
- Uses async/await patterns throughout
- Provides full TypeScript type definitions
Installation
Always install via npm/pnpm/yarn:
npm install @github/copilot-sdk # or pnpm add @github/copilot-sdk # or yarn add @github/copilot-sdk
Client Initialization
Basic Client Setup
import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient(); await client.start(); // Use client... await client.stop();
Client Configuration Options
When creating a CopilotClient, use
CopilotClientOptions:
- Path to CLI executable (default: "copilot" from PATH)cliPath
- Extra arguments prepended before SDK-managed flags (string[])cliArgs
- URL of existing CLI server (e.g., "localhost:8080"). When provided, client won't spawn a processcliUrl
- Server port (default: 0 for random)port
- Use stdio transport instead of TCP (default: true)useStdio
- Log level (default: "debug")logLevel
- Auto-start server (default: true)autoStart
- Auto-restart on crash (default: true)autoRestart
- Working directory for the CLI process (default: process.cwd())cwd
- Environment variables for the CLI process (default: process.env)env
Manual Server Control
For explicit control:
const client = new CopilotClient({ autoStart: false }); await client.start(); // Use client... await client.stop();
Use
forceStop() when stop() takes too long.
Session Management
Creating Sessions
Use
SessionConfig for configuration:
const session = await client.createSession({ model: "gpt-5", streaming: true, tools: [...], systemMessage: { ... }, availableTools: ["tool1", "tool2"], excludedTools: ["tool3"], provider: { ... } });
Session Config Options
- Custom session ID (string)sessionId
- Model name ("gpt-5", "claude-sonnet-4.5", etc.)model
- Custom tools exposed to the CLI (Tool[])tools
- System message customization (SystemMessageConfig)systemMessage
- Allowlist of tool names (string[])availableTools
- Blocklist of tool names (string[])excludedTools
- Custom API provider configuration (BYOK) (ProviderConfig)provider
- Enable streaming response chunks (boolean)streaming
- MCP server configurations (MCPServerConfig[])mcpServers
- Custom agent configurations (CustomAgentConfig[])customAgents
- Config directory override (string)configDir
- Skill directories (string[])skillDirectories
- Disabled skills (string[])disabledSkills
- Permission request handler (PermissionHandler)onPermissionRequest
Resuming Sessions
const session = await client.resumeSession("session-id", { tools: [myNewTool], });
Session Operations
- Get session identifier (string)session.sessionId
- Send message, returns Promise<string>await session.send({ prompt: "...", attachments: [...] })
- Send and wait for idle, returns Promise<AssistantMessageEvent | null>await session.sendAndWait({ prompt: "..." }, timeout)
- Abort current processingawait session.abort()
- Get all events/messages, returns Promise<SessionEvent[]>await session.getMessages()
- Clean up sessionawait session.destroy()
Event Handling
Event Subscription Pattern
ALWAYS use async/await or Promises for waiting on session events:
await new Promise<void>((resolve) => { session.on((event) => { if (event.type === "assistant.message") { console.log(event.data.content); } else if (event.type === "session.idle") { resolve(); } }); session.send({ prompt: "..." }); });
Unsubscribing from Events
The
on() method returns a function that unsubscribes:
const unsubscribe = session.on((event) => { // handler }); // Later... unsubscribe();
Event Types
Use discriminated unions with type guards for event handling:
session.on((event) => { switch (event.type) { case "user.message": // Handle user message break; case "assistant.message": console.log(event.data.content); break; case "tool.executionStart": // Tool execution started break; case "tool.executionComplete": // Tool execution completed break; case "session.start": // Session started break; case "session.idle": // Session is idle (processing complete) break; case "session.error": console.error(`Error: ${event.data.message}`); break; } });
Streaming Responses
Enabling Streaming
Set
streaming: true in SessionConfig:
const session = await client.createSession({ model: "gpt-5", streaming: true, });
Handling Streaming Events
Handle both delta events (incremental) and final events:
await new Promise<void>((resolve) => { session.on((event) => { switch (event.type) { case "assistant.message.delta": // Incremental text chunk process.stdout.write(event.data.deltaContent); break; case "assistant.reasoning.delta": // Incremental reasoning chunk (model-dependent) process.stdout.write(event.data.deltaContent); break; case "assistant.message": // Final complete message console.log("\n--- Final ---"); console.log(event.data.content); break; case "assistant.reasoning": // Final reasoning content console.log("--- Reasoning ---"); console.log(event.data.content); break; case "session.idle": resolve(); break; } }); session.send({ prompt: "Tell me a story" }); });
Note: Final events (
assistant.message, assistant.reasoning) are ALWAYS sent regardless of streaming setting.
Custom Tools
Defining Tools with defineTool
Use
defineTool for type-safe tool definitions:
import { defineTool } from "@github/copilot-sdk"; const session = await client.createSession({ model: "gpt-5", tools: [ defineTool({ name: "lookup_issue", description: "Fetch issue details from tracker", parameters: { type: "object", properties: { id: { type: "string", description: "Issue ID" }, }, required: ["id"], }, handler: async (args) => { const issue = await fetchIssue(args.id); return issue; }, }), ], });
Using Zod for Parameters
The SDK supports Zod schemas for parameters:
import { z } from "zod"; const session = await client.createSession({ tools: [ defineTool({ name: "get_weather", description: "Get weather for a location", parameters: z.object({ location: z.string().describe("City name"), units: z.enum(["celsius", "fahrenheit"]).optional(), }), handler: async (args) => { return { temperature: 72, units: args.units || "fahrenheit" }; }, }), ], });
Tool Return Types
- Return any JSON-serializable value (automatically wrapped)
- Or return
for full control over metadata:ToolResultObject
{ textResultForLlm: string; // Result shown to LLM resultType: "success" | "failure"; error?: string; // Internal error (not shown to LLM) toolTelemetry?: Record<string, unknown>; }
Tool Execution Flow
When Copilot invokes a tool, the client automatically:
- Runs your handler function
- Serializes the return value
- Responds to the CLI
System Message Customization
Append Mode (Default - Preserves Guardrails)
const session = await client.createSession({ model: "gpt-5", systemMessage: { mode: "append", content: ` <workflow_rules> - Always check for security vulnerabilities - Suggest performance improvements when applicable </workflow_rules> `, }, });
Replace Mode (Full Control - Removes Guardrails)
const session = await client.createSession({ model: "gpt-5", systemMessage: { mode: "replace", content: "You are a helpful assistant.", }, });
File Attachments
Attach files to messages:
await session.send({ prompt: "Analyze this file", attachments: [ { type: "file", path: "/path/to/file.ts", displayName: "My File", }, ], });
Message Delivery Modes
Use the
mode property in message options:
- Queue message for processing"enqueue"
- Process message immediately"immediate"
await session.send({ prompt: "...", mode: "enqueue", });
Multiple Sessions
Sessions are independent and can run concurrently:
const session1 = await client.createSession({ model: "gpt-5" }); const session2 = await client.createSession({ model: "claude-sonnet-4.5" }); await Promise.all([ session1.send({ prompt: "Hello from session 1" }), session2.send({ prompt: "Hello from session 2" }), ]);
Bring Your Own Key (BYOK)
Use custom API providers via
provider:
const session = await client.createSession({ provider: { type: "openai", baseUrl: "https://api.openai.com/v1", apiKey: "your-api-key", }, });
Session Lifecycle Management
Listing Sessions
const sessions = await client.listSessions(); for (const metadata of sessions) { console.log(`${metadata.sessionId}: ${metadata.summary}`); }
Deleting Sessions
await client.deleteSession(sessionId);
Getting Last Session ID
const lastId = await client.getLastSessionId(); if (lastId) { const session = await client.resumeSession(lastId); }
Checking Connection State
const state = client.getState(); // Returns: "disconnected" | "connecting" | "connected" | "error"
Error Handling
Standard Exception Handling
try { const session = await client.createSession(); await session.send({ prompt: "Hello" }); } catch (error) { console.error(`Error: ${error.message}`); }
Session Error Events
Monitor
session.error event type for runtime errors:
session.on((event) => { if (event.type === "session.error") { console.error(`Session Error: ${event.data.message}`); } });
Connectivity Testing
Use ping to verify server connectivity:
const response = await client.ping("health check"); console.log(`Server responded at ${new Date(response.timestamp)}`);
Resource Cleanup
Automatic Cleanup with Try-Finally
ALWAYS use try-finally or cleanup in a finally block:
const client = new CopilotClient(); try { await client.start(); const session = await client.createSession(); try { // Use session... } finally { await session.destroy(); } } finally { await client.stop(); }
Cleanup Function Pattern
async function withClient<T>( fn: (client: CopilotClient) => Promise<T>, ): Promise<T> { const client = new CopilotClient(); try { await client.start(); return await fn(client); } finally { await client.stop(); } } async function withSession<T>( client: CopilotClient, fn: (session: CopilotSession) => Promise<T>, ): Promise<T> { const session = await client.createSession(); try { return await fn(session); } finally { await session.destroy(); } } // Usage await withClient(async (client) => { await withSession(client, async (session) => { await session.send({ prompt: "Hello!" }); }); });
Best Practices
- Always use try-finally for resource cleanup
- Use Promises to wait for session.idle event
- Handle session.error events for robust error handling
- Use type guards or switch statements for event handling
- Enable streaming for better UX in interactive scenarios
- Use defineTool for type-safe tool definitions
- Use Zod schemas for runtime parameter validation
- Dispose event subscriptions when no longer needed
- Use systemMessage with mode: "append" to preserve safety guardrails
- Handle both delta and final events when streaming is enabled
- Leverage TypeScript types for compile-time safety
Common Patterns
Simple Query-Response
import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient(); try { await client.start(); const session = await client.createSession({ model: "gpt-5" }); try { await new Promise<void>((resolve) => { session.on((event) => { if (event.type === "assistant.message") { console.log(event.data.content); } else if (event.type === "session.idle") { resolve(); } }); session.send({ prompt: "What is 2+2?" }); }); } finally { await session.destroy(); } } finally { await client.stop(); }
Multi-Turn Conversation
const session = await client.createSession(); async function sendAndWait(prompt: string): Promise<void> { await new Promise<void>((resolve, reject) => { const unsubscribe = session.on((event) => { if (event.type === "assistant.message") { console.log(event.data.content); } else if (event.type === "session.idle") { unsubscribe(); resolve(); } else if (event.type === "session.error") { unsubscribe(); reject(new Error(event.data.message)); } }); session.send({ prompt }); }); } await sendAndWait("What is the capital of France?"); await sendAndWait("What is its population?");
SendAndWait Helper
// Use built-in sendAndWait for simpler synchronous interaction const response = await session.sendAndWait({ prompt: "What is 2+2?" }, 60000); if (response) { console.log(response.data.content); }
Tool with Type-Safe Parameters
import { z } from "zod"; import { defineTool } from "@github/copilot-sdk"; interface UserInfo { id: string; name: string; email: string; role: string; } const session = await client.createSession({ tools: [ defineTool({ name: "get_user", description: "Retrieve user information", parameters: z.object({ userId: z.string().describe("User ID"), }), handler: async (args): Promise<UserInfo> => { return { id: args.userId, name: "John Doe", email: "john@example.com", role: "Developer", }; }, }), ], });
Streaming with Progress
let currentMessage = ""; const unsubscribe = session.on((event) => { if (event.type === "assistant.message.delta") { currentMessage += event.data.deltaContent; process.stdout.write(event.data.deltaContent); } else if (event.type === "assistant.message") { console.log("\n\n=== Complete ==="); console.log(`Total length: ${event.data.content.length} chars`); } else if (event.type === "session.idle") { unsubscribe(); } }); await session.send({ prompt: "Write a long story" });
Error Recovery
session.on((event) => { if (event.type === "session.error") { console.error("Session error:", event.data.message); // Optionally retry or handle error } }); try { await session.send({ prompt: "risky operation" }); } catch (error) { // Handle send errors console.error("Failed to send:", error); }
TypeScript-Specific Features
Type Inference
import type { SessionEvent, AssistantMessageEvent } from "@github/copilot-sdk"; session.on((event: SessionEvent) => { if (event.type === "assistant.message") { // TypeScript knows event is AssistantMessageEvent here const content: string = event.data.content; } });
Generic Helper
async function waitForEvent<T extends SessionEvent["type"]>( session: CopilotSession, eventType: T, ): Promise<Extract<SessionEvent, { type: T }>> { return new Promise((resolve) => { const unsubscribe = session.on((event) => { if (event.type === eventType) { unsubscribe(); resolve(event as Extract<SessionEvent, { type: T }>); } }); }); } // Usage const message = await waitForEvent(session, "assistant.message"); console.log(message.data.content);