diff --git a/docs/auth/byok.md b/docs/auth/byok.md index 13ad8b055..6a2804f4d 100644 --- a/docs/auth/byok.md +++ b/docs/auth/byok.md @@ -79,6 +79,7 @@ const session = await client.createSession({ wireApi: "responses", // Use "completions" for older models apiKey: process.env.FOUNDRY_API_KEY, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); session.on("assistant.message", (event) => { @@ -350,12 +351,14 @@ When using BYOK, the `model` parameter is **required**: // ❌ Error: Model required with custom provider const session = await client.createSession({ provider: { type: "openai", baseUrl: "..." }, + onPermissionRequest: async () => ({ kind: "approved" }), }); // ✅ Correct: Model specified const session = await client.createSession({ model: "gpt-4", // Required! provider: { type: "openai", baseUrl: "..." }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` diff --git a/docs/compatibility.md b/docs/compatibility.md index 268c077a3..4c3bc5a30 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -158,6 +158,7 @@ const session = await client.createSession({ backgroundCompactionThreshold: 0.80, // Start background compaction at 80% context utilization bufferExhaustionThreshold: 0.95, // Block and compact at 95% context utilization }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` diff --git a/docs/debugging.md b/docs/debugging.md index 6183cccdf..c18762f95 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -376,8 +376,9 @@ const client = new CopilotClient({ ```typescript const session = await client.createSession({ tools: [myTool], + onPermissionRequest: async () => ({ kind: "approved" }), }); - + // Check registered tools console.log("Registered tools:", session.getTools?.()); ``` diff --git a/docs/getting-started.md b/docs/getting-started.md index 05bbde8dc..19cab363d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -20,7 +20,7 @@ Before you begin, make sure you have: - **GitHub Copilot CLI** installed and authenticated ([Installation guide](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)) - Your preferred language runtime: - - **Node.js** 18+ or **Python** 3.8+ or **Go** 1.21+ or **.NET** 8.0+ + - **Node.js** 22.13.0+ / 23.5.0+ / 24+ or **Python** 3.8+ or **Go** 1.21+ or **.NET** 8.0+ Verify the CLI is working: @@ -105,7 +105,7 @@ Create `index.ts`: import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient(); -const session = await client.createSession({ model: "gpt-4.1" }); +const session = await client.createSession({ model: "gpt-4.1", onPermissionRequest: async () => ({ kind: "approved" }) }); const response = await session.sendAndWait({ prompt: "What is 2 + 2?" }); console.log(response?.data.content); @@ -248,6 +248,7 @@ const client = new CopilotClient(); const session = await client.createSession({ model: "gpt-4.1", streaming: true, + onPermissionRequest: async () => ({ kind: "approved" }), }); // Listen for response chunks @@ -624,6 +625,7 @@ const session = await client.createSession({ model: "gpt-4.1", streaming: true, tools: [getWeather], + onPermissionRequest: async () => ({ kind: "approved" }), }); session.on("assistant.message_delta", (event) => { @@ -876,6 +878,7 @@ const session = await client.createSession({ model: "gpt-4.1", streaming: true, tools: [getWeather], + onPermissionRequest: async () => ({ kind: "approved" }), }); session.on("assistant.message_delta", (event) => { @@ -1221,6 +1224,7 @@ const session = await client.createSession({ url: "https://api.githubcopilot.com/mcp/", }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -1238,6 +1242,7 @@ const session = await client.createSession({ description: "Reviews pull requests for best practices", prompt: "You are an expert code reviewer. Focus on security, performance, and maintainability.", }], + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -1250,6 +1255,7 @@ const session = await client.createSession({ systemMessage: { content: "You are a helpful assistant for our engineering team. Always be concise.", }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` diff --git a/docs/guides/session-persistence.md b/docs/guides/session-persistence.md index 527f5ecc7..8df9ed29e 100644 --- a/docs/guides/session-persistence.md +++ b/docs/guides/session-persistence.md @@ -34,6 +34,7 @@ const client = new CopilotClient(); const session = await client.createSession({ sessionId: "user-123-task-456", model: "gpt-5.2-codex", + onPermissionRequest: async () => ({ kind: "approved" }), }); // Do some work... @@ -111,10 +112,10 @@ flowchart LR subgraph Day1["Day 1"] A1[Client A:
createSession] --> A2[Work...] end - + A2 --> S[(💾 Storage:
~/.copilot/session-state/)] S --> B1 - + subgraph Day2["Day 2"] B1[Client B:
resumeSession] --> B2[Continue] end @@ -124,7 +125,7 @@ flowchart LR ```typescript // Resume from a different client instance (or after restart) -const session = await client.resumeSession("user-123-task-456"); +const session = await client.resumeSession("user-123-task-456", { onPermissionRequest: async () => ({ kind: "approved" }) }); // Continue where you left off await session.sendAndWait({ prompt: "What did we discuss earlier?" }); @@ -192,6 +193,7 @@ When resuming a session, you can optionally reconfigure many settings. This is u const session = await client.resumeSession("user-123-task-456", { model: "claude-sonnet-4", // Switch to a different model reasoningEffort: "high", // Increase reasoning effort + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -210,6 +212,7 @@ const session = await client.createSession({ apiKey: process.env.AZURE_OPENAI_KEY, deploymentId: "my-gpt-deployment", }, + onPermissionRequest: async () => ({ kind: "approved" }), }); // When resuming, you MUST re-provide the provider config @@ -220,6 +223,7 @@ const resumed = await client.resumeSession("user-123-task-456", { apiKey: process.env.AZURE_OPENAI_KEY, // Required again deploymentId: "my-gpt-deployment", }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -311,7 +315,7 @@ const repoSessions = await client.listSessions({ repository: "owner/repo" }); async function cleanupExpiredSessions(maxAgeMs: number) { const sessions = await client.listSessions(); const now = Date.now(); - + for (const session of sessions) { const age = now - new Date(session.createdAt).getTime(); if (age > maxAgeMs) { @@ -333,7 +337,7 @@ When a task completes, destroy the session explicitly rather than waiting for ti try { // Do work... await session.sendAndWait({ prompt: "Complete the task" }); - + // Task complete - clean up await session.destroy(); } catch (error) { @@ -408,12 +412,12 @@ async function resumeSessionWithAuth( ): Promise { // Parse user from session ID const [sessionUserId] = sessionId.split("-"); - + if (sessionUserId !== currentUserId) { throw new Error("Access denied: session belongs to another user"); } - - return client.resumeSession(sessionId); + + return client.resumeSession(sessionId, { onPermissionRequest: async () => ({ kind: "approved" }) }); } ``` @@ -446,10 +450,10 @@ flowchart LR subgraph Before["Container A"] CLI1[CLI + Session X] end - + CLI1 --> |persist| Azure[(☁️ Azure File Share)] Azure --> |restore| CLI2 - + subgraph After["Container B (restart)"] CLI2[CLI + Session X] end @@ -469,6 +473,7 @@ const session = await client.createSession({ backgroundCompactionThreshold: 0.80, // Start compaction at 80% context bufferExhaustionThreshold: 0.95, // Block at 95% if needed }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -499,11 +504,11 @@ async function withSessionLock( ): Promise { const lockKey = `session-lock:${sessionId}`; const acquired = await redis.set(lockKey, "locked", "NX", "EX", 300); - + if (!acquired) { throw new Error("Session is in use by another client"); } - + try { return await fn(); } finally { @@ -513,7 +518,7 @@ async function withSessionLock( // Usage await withSessionLock("user-123-task-456", async () => { - const session = await client.resumeSession("user-123-task-456"); + const session = await client.resumeSession("user-123-task-456", { onPermissionRequest: async () => ({ kind: "approved" }) }); await session.sendAndWait({ prompt: "Continue the task" }); }); ``` diff --git a/docs/guides/setup/azure-managed-identity.md b/docs/guides/setup/azure-managed-identity.md index bfafc6f91..118a6d836 100644 --- a/docs/guides/setup/azure-managed-identity.md +++ b/docs/guides/setup/azure-managed-identity.md @@ -145,6 +145,7 @@ const session = await client.createSession({ bearerToken: tokenResponse.token, wireApi: "responses", }, + onPermissionRequest: async () => ({ kind: "approved" }), }); const response = await session.sendAndWait({ prompt: "Hello!" }); diff --git a/docs/guides/setup/backend-services.md b/docs/guides/setup/backend-services.md index c9bc13f8d..e7a433345 100644 --- a/docs/guides/setup/backend-services.md +++ b/docs/guides/setup/backend-services.md @@ -99,6 +99,7 @@ const client = new CopilotClient({ const session = await client.createSession({ sessionId: `user-${userId}-${Date.now()}`, model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), }); const response = await session.sendAndWait({ prompt: req.body.message }); @@ -213,6 +214,7 @@ app.post("/chat", authMiddleware, async (req, res) => { const session = await client.createSession({ sessionId: `user-${req.user.id}-chat`, model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), }); const response = await session.sendAndWait({ @@ -239,6 +241,7 @@ const session = await client.createSession({ baseUrl: "https://api.openai.com/v1", apiKey: process.env.OPENAI_API_KEY, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -280,11 +283,12 @@ app.post("/api/chat", async (req, res) => { // Create or resume session let session; try { - session = await client.resumeSession(sessionId); + session = await client.resumeSession(sessionId, { onPermissionRequest: async () => ({ kind: "approved" }) }); } catch { session = await client.createSession({ sessionId, model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), }); } @@ -312,6 +316,7 @@ async function processJob(job: Job) { const session = await client.createSession({ sessionId: `job-${job.id}`, model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), }); const response = await session.sendAndWait({ diff --git a/docs/guides/setup/bundled-cli.md b/docs/guides/setup/bundled-cli.md index 6daf57b56..75afdfc9b 100644 --- a/docs/guides/setup/bundled-cli.md +++ b/docs/guides/setup/bundled-cli.md @@ -72,7 +72,7 @@ const client = new CopilotClient({ cliPath: path.join(__dirname, "vendor", "copilot"), }); -const session = await client.createSession({ model: "gpt-4.1" }); +const session = await client.createSession({ model: "gpt-4.1", onPermissionRequest: async () => ({ kind: "approved" }) }); const response = await session.sendAndWait({ prompt: "Hello!" }); console.log(response?.data.content); @@ -200,6 +200,7 @@ const session = await client.createSession({ baseUrl: "https://api.openai.com/v1", apiKey: process.env.OPENAI_API_KEY, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -219,11 +220,12 @@ const sessionId = `project-${projectName}`; const session = await client.createSession({ sessionId, model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), }); // User closes app... // Later, resume where they left off -const resumed = await client.resumeSession(sessionId); +const resumed = await client.resumeSession(sessionId, { onPermissionRequest: async () => ({ kind: "approved" }) }); ``` Session state persists at `~/.copilot/session-state/{sessionId}/`. diff --git a/docs/guides/setup/byok.md b/docs/guides/setup/byok.md index 5b8b8a460..8ba1d4019 100644 --- a/docs/guides/setup/byok.md +++ b/docs/guides/setup/byok.md @@ -78,6 +78,7 @@ const session = await client.createSession({ baseUrl: "https://api.openai.com/v1", apiKey: process.env.OPENAI_API_KEY, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); const response = await session.sendAndWait({ prompt: "Hello!" }); @@ -255,6 +256,7 @@ app.post("/chat", authMiddleware, async (req, res) => { baseUrl: "https://api.openai.com/v1", apiKey: process.env.OPENAI_API_KEY, // Your key, your billing }, + onPermissionRequest: async () => ({ kind: "approved" }), }); const response = await session.sendAndWait({ prompt: req.body.message }); @@ -303,6 +305,7 @@ async function createSessionForCustomer(customerId: string) { baseUrl: config.baseUrl, apiKey: config.apiKey, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); } ``` @@ -321,6 +324,7 @@ const session = await client.createSession({ baseUrl: "https://api.openai.com/v1", apiKey: process.env.OPENAI_API_KEY, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); // Resume later — must re-provide provider config @@ -330,6 +334,7 @@ const resumed = await client.resumeSession("task-123", { baseUrl: "https://api.openai.com/v1", apiKey: process.env.OPENAI_API_KEY, // Required again }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` diff --git a/docs/guides/setup/github-oauth.md b/docs/guides/setup/github-oauth.md index 07251c8fb..9f1152ede 100644 --- a/docs/guides/setup/github-oauth.md +++ b/docs/guides/setup/github-oauth.md @@ -134,6 +134,7 @@ const client = createClientForUser("gho_user_access_token"); const session = await client.createSession({ sessionId: `user-${userId}-session`, model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), }); const response = await session.sendAndWait({ prompt: "Hello!" }); diff --git a/docs/guides/setup/local-cli.md b/docs/guides/setup/local-cli.md index a5fa906b8..c267f79af 100644 --- a/docs/guides/setup/local-cli.md +++ b/docs/guides/setup/local-cli.md @@ -37,7 +37,7 @@ The default configuration requires no options at all: import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient(); -const session = await client.createSession({ model: "gpt-4.1" }); +const session = await client.createSession({ model: "gpt-4.1", onPermissionRequest: async () => ({ kind: "approved" }) }); const response = await session.sendAndWait({ prompt: "Hello!" }); console.log(response?.data.content); @@ -173,10 +173,11 @@ With the local CLI, sessions default to ephemeral. To create resumable sessions, const session = await client.createSession({ sessionId: "my-project-analysis", model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), }); // Later, resume it -const resumed = await client.resumeSession("my-project-analysis"); +const resumed = await client.resumeSession("my-project-analysis", { onPermissionRequest: async () => ({ kind: "approved" }) }); ``` Session state is stored locally at `~/.copilot/session-state/{sessionId}/`. diff --git a/docs/guides/setup/scaling.md b/docs/guides/setup/scaling.md index fcdb716da..872a6b034 100644 --- a/docs/guides/setup/scaling.md +++ b/docs/guides/setup/scaling.md @@ -153,7 +153,7 @@ async function resumeSessionWithAuth( if (sessionUserId !== currentUserId) { throw new Error("Access denied: session belongs to another user"); } - return sharedClient.resumeSession(sessionId); + return sharedClient.resumeSession(sessionId, { onPermissionRequest: async () => ({ kind: "approved" }) }); } ``` @@ -227,7 +227,7 @@ async function withSessionLock( // Usage: serialize access to shared session app.post("/team-chat", authMiddleware, async (req, res) => { const result = await withSessionLock("team-project-review", async () => { - const session = await client.resumeSession("team-project-review"); + const session = await client.resumeSession("team-project-review", { onPermissionRequest: async () => ({ kind: "approved" }) }); return session.sendAndWait({ prompt: req.body.message }); }); @@ -323,6 +323,7 @@ app.post("/chat", async (req, res) => { const session = await client.createSession({ sessionId: `user-${req.user.id}-chat`, model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), }); const response = await session.sendAndWait({ prompt: req.body.message }); @@ -403,6 +404,7 @@ class SessionManager { const session = await client.createSession({ sessionId, model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), }); this.activeSessions.set(sessionId, session); @@ -449,6 +451,7 @@ For stateless API endpoints where each request is independent: app.post("/api/analyze", async (req, res) => { const session = await client.createSession({ model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), }); try { @@ -478,6 +481,7 @@ app.post("/api/chat/start", async (req, res) => { enabled: true, backgroundCompactionThreshold: 0.80, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); res.json({ sessionId }); @@ -485,7 +489,7 @@ app.post("/api/chat/start", async (req, res) => { // Continue the conversation app.post("/api/chat/message", async (req, res) => { - const session = await client.resumeSession(req.body.sessionId); + const session = await client.resumeSession(req.body.sessionId, { onPermissionRequest: async () => ({ kind: "approved" }) }); const response = await session.sendAndWait({ prompt: req.body.message }); res.json({ content: response?.data.content }); diff --git a/docs/guides/skills.md b/docs/guides/skills.md index b2ea3ae7a..d049ac177 100644 --- a/docs/guides/skills.md +++ b/docs/guides/skills.md @@ -150,6 +150,7 @@ Disable specific skills while keeping others active: const session = await client.createSession({ skillDirectories: ["./skills"], disabledSkills: ["experimental-feature", "deprecated-tool"], + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` diff --git a/docs/hooks/error-handling.md b/docs/hooks/error-handling.md index 0f705868d..6abd2b0a7 100644 --- a/docs/hooks/error-handling.md +++ b/docs/hooks/error-handling.md @@ -98,6 +98,7 @@ const session = await client.createSession({ return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -180,10 +181,11 @@ const session = await client.createSession({ cwd: input.cwd, }, }); - + return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -201,16 +203,17 @@ const session = await client.createSession({ hooks: { onErrorOccurred: async (input) => { const friendlyMessage = ERROR_MESSAGES[input.errorContext]; - + if (friendlyMessage) { return { userNotification: friendlyMessage, }; } - + return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -228,6 +231,7 @@ const session = await client.createSession({ return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -247,7 +251,7 @@ The tool failed. Here are some recovery suggestions: `.trim(), }; } - + if (input.errorContext === "model_call" && input.error.includes("rate")) { return { errorHandling: "retry", @@ -255,10 +259,11 @@ The tool failed. Here are some recovery suggestions: userNotification: "Rate limit hit. Retrying...", }; } - + return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -277,27 +282,28 @@ const session = await client.createSession({ hooks: { onErrorOccurred: async (input, invocation) => { const key = `${input.errorContext}:${input.error.substring(0, 50)}`; - + const existing = errorStats.get(key) || { count: 0, lastOccurred: 0, contexts: [], }; - + existing.count++; existing.lastOccurred = input.timestamp; existing.contexts.push(invocation.sessionId); - + errorStats.set(key, existing); - + // Alert if error is recurring if (existing.count >= 5) { console.warn(`Recurring error detected: ${key} (${existing.count} times)`); } - + return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -318,10 +324,11 @@ const session = await client.createSession({ timestamp: new Date(input.timestamp).toISOString(), }); } - + return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -338,17 +345,17 @@ const session = await client.createSession({ sessionContext.set(invocation.sessionId, ctx); return { permissionDecision: "allow" }; }, - + onUserPromptSubmitted: async (input, invocation) => { const ctx = sessionContext.get(invocation.sessionId) || {}; ctx.lastPrompt = input.prompt.substring(0, 100); sessionContext.set(invocation.sessionId, ctx); return null; }, - + onErrorOccurred: async (input, invocation) => { const ctx = sessionContext.get(invocation.sessionId); - + console.error(`Error in session ${invocation.sessionId}:`); console.error(` Error: ${input.error}`); console.error(` Context: ${input.errorContext}`); @@ -358,10 +365,11 @@ const session = await client.createSession({ if (ctx?.lastPrompt) { console.error(` Last prompt: ${ctx.lastPrompt}...`); } - + return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` diff --git a/docs/hooks/overview.md b/docs/hooks/overview.md index a51ef0464..ab2a3c2ea 100644 --- a/docs/hooks/overview.md +++ b/docs/hooks/overview.md @@ -44,6 +44,7 @@ const session = await client.createSession({ return { additionalContext: "User prefers concise answers." }; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -183,6 +184,7 @@ const session = await client.createSession({ return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -203,6 +205,7 @@ const session = await client.createSession({ return { permissionDecision: "allow" }; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -218,6 +221,7 @@ const session = await client.createSession({ }; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` diff --git a/docs/hooks/post-tool-use.md b/docs/hooks/post-tool-use.md index 0021e20a0..4578a5485 100644 --- a/docs/hooks/post-tool-use.md +++ b/docs/hooks/post-tool-use.md @@ -97,6 +97,7 @@ const session = await client.createSession({ return null; // Pass through unchanged }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -177,7 +178,7 @@ const session = await client.createSession({ for (const pattern of SENSITIVE_PATTERNS) { redacted = redacted.replace(pattern, "[REDACTED]"); } - + if (redacted !== input.toolResult) { return { modifiedResult: redacted }; } @@ -185,6 +186,7 @@ const session = await client.createSession({ return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -197,7 +199,7 @@ const session = await client.createSession({ hooks: { onPostToolUse: async (input) => { const resultStr = JSON.stringify(input.toolResult); - + if (resultStr.length > MAX_RESULT_LENGTH) { return { modifiedResult: { @@ -211,6 +213,7 @@ const session = await client.createSession({ return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -226,17 +229,18 @@ const session = await client.createSession({ additionalContext: "Tip: If the file doesn't exist, consider creating it or checking the path.", }; } - + // If shell command failed, add debugging hint if (input.toolName === "shell" && input.toolResult?.exitCode !== 0) { return { additionalContext: "The command failed. Check if required dependencies are installed.", }; } - + return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -259,6 +263,7 @@ const session = await client.createSession({ return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -287,13 +292,14 @@ const session = await client.createSession({ result: input.toolResult, success: !input.toolResult?.error, }); - + // Optionally persist to database/file await saveAuditLog(auditLog); - + return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -307,10 +313,10 @@ const session = await client.createSession({ onPostToolUse: async (input) => { if (NOISY_TOOLS.includes(input.toolName)) { // Summarize instead of showing full result - const items = Array.isArray(input.toolResult) - ? input.toolResult + const items = Array.isArray(input.toolResult) + ? input.toolResult : input.toolResult?.items || []; - + return { modifiedResult: { summary: `Found ${items.length} items`, @@ -321,6 +327,7 @@ const session = await client.createSession({ return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` diff --git a/docs/hooks/pre-tool-use.md b/docs/hooks/pre-tool-use.md index ac12df4fa..87f785d74 100644 --- a/docs/hooks/pre-tool-use.md +++ b/docs/hooks/pre-tool-use.md @@ -105,6 +105,7 @@ const session = await client.createSession({ return { permissionDecision: "allow" }; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -186,6 +187,7 @@ const session = await client.createSession({ return { permissionDecision: "allow" }; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -209,6 +211,7 @@ const session = await client.createSession({ return { permissionDecision: "allow" }; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -222,10 +225,10 @@ const session = await client.createSession({ onPreToolUse: async (input) => { if (input.toolName === "read_file" || input.toolName === "write_file") { const args = input.toolArgs as { path: string }; - const isAllowed = ALLOWED_DIRECTORIES.some(dir => + const isAllowed = ALLOWED_DIRECTORIES.some(dir => args.path.startsWith(dir) ); - + if (!isAllowed) { return { permissionDecision: "deny", @@ -236,6 +239,7 @@ const session = await client.createSession({ return { permissionDecision: "allow" }; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -253,6 +257,7 @@ const session = await client.createSession({ }; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -271,6 +276,7 @@ const session = await client.createSession({ return { permissionDecision: "allow" }; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` diff --git a/docs/hooks/session-lifecycle.md b/docs/hooks/session-lifecycle.md index 74f4666f4..5b03cab74 100644 --- a/docs/hooks/session-lifecycle.md +++ b/docs/hooks/session-lifecycle.md @@ -92,9 +92,9 @@ const session = await client.createSession({ hooks: { onSessionStart: async (input, invocation) => { console.log(`Session ${invocation.sessionId} started (${input.source})`); - + const projectInfo = await detectProjectType(input.cwd); - + return { additionalContext: ` This is a ${projectInfo.type} project. @@ -104,6 +104,7 @@ Package manager: ${projectInfo.packageManager} }; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -115,9 +116,9 @@ Package manager: ${projectInfo.packageManager} ```python async def on_session_start(input_data, invocation): print(f"Session {invocation['session_id']} started ({input_data['source']})") - + project_info = await detect_project_type(input_data["cwd"]) - + return { "additionalContext": f""" This is a {project_info['type']} project. @@ -142,7 +143,7 @@ const session = await client.createSession({ if (input.source === "resume") { // Load previous session state const previousState = await loadSessionState(invocation.sessionId); - + return { additionalContext: ` Session resumed. Previous context: @@ -154,6 +155,7 @@ Session resumed. Previous context: return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -164,9 +166,9 @@ const session = await client.createSession({ hooks: { onSessionStart: async () => { const preferences = await loadUserPreferences(); - + const contextParts = []; - + if (preferences.language) { contextParts.push(`Preferred language: ${preferences.language}`); } @@ -176,12 +178,13 @@ const session = await client.createSession({ if (preferences.verbosity === "concise") { contextParts.push("Keep responses brief and to the point."); } - + return { additionalContext: contextParts.join("\n"), }; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -289,17 +292,18 @@ const session = await client.createSession({ onSessionEnd: async (input, invocation) => { const startTime = sessionStartTimes.get(invocation.sessionId); const duration = startTime ? input.timestamp - startTime : 0; - + await recordMetrics({ sessionId: invocation.sessionId, duration, endReason: input.reason, }); - + sessionStartTimes.delete(invocation.sessionId); return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -318,13 +322,13 @@ async def on_session_start(input_data, invocation): async def on_session_end(input_data, invocation): start_time = session_start_times.get(invocation["session_id"]) duration = input_data["timestamp"] - start_time if start_time else 0 - + await record_metrics({ "session_id": invocation["session_id"], "duration": duration, "end_reason": input_data["reason"], }) - + session_start_times.pop(invocation["session_id"], None) return None @@ -351,7 +355,7 @@ const session = await client.createSession({ }, onSessionEnd: async (input, invocation) => { const resources = sessionResources.get(invocation.sessionId); - + if (resources) { // Clean up temp files for (const file of resources.tempFiles) { @@ -359,11 +363,12 @@ const session = await client.createSession({ } sessionResources.delete(invocation.sessionId); } - + console.log(`Session ${invocation.sessionId} ended: ${input.reason}`); return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -384,6 +389,7 @@ const session = await client.createSession({ return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -395,10 +401,10 @@ const sessionData: Record { - sessionData[invocation.sessionId] = { - prompts: 0, - tools: 0, - startTime: input.timestamp + sessionData[invocation.sessionId] = { + prompts: 0, + tools: 0, + startTime: input.timestamp }; return null; }, @@ -420,11 +426,12 @@ Session Summary: Tool calls: ${data.tools} End reason: ${input.reason} `.trim()); - + delete sessionData[invocation.sessionId]; return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` diff --git a/docs/hooks/user-prompt-submitted.md b/docs/hooks/user-prompt-submitted.md index 3205b95cd..a4f74190e 100644 --- a/docs/hooks/user-prompt-submitted.md +++ b/docs/hooks/user-prompt-submitted.md @@ -93,6 +93,7 @@ const session = await client.createSession({ return null; // Pass through unchanged }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -157,7 +158,7 @@ const session = await client.createSession({ hooks: { onUserPromptSubmitted: async (input) => { const projectInfo = await getProjectInfo(); - + return { additionalContext: ` Project: ${projectInfo.name} @@ -167,6 +168,7 @@ Framework: ${projectInfo.framework} }; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -194,6 +196,7 @@ const session = await client.createSession({ return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -221,6 +224,7 @@ const session = await client.createSession({ return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -242,6 +246,7 @@ const session = await client.createSession({ return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -258,24 +263,25 @@ const session = await client.createSession({ hooks: { onUserPromptSubmitted: async (input) => { const prefs: UserPreferences = await loadUserPreferences(); - + const contextParts = []; - + if (prefs.codeStyle === "concise") { contextParts.push("User prefers concise code with minimal comments."); } else { contextParts.push("User prefers verbose code with detailed comments."); } - + if (prefs.experienceLevel === "beginner") { contextParts.push("Explain concepts in simple terms."); } - + return { additionalContext: contextParts.join(" "), }; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -290,23 +296,24 @@ const session = await client.createSession({ hooks: { onUserPromptSubmitted: async (input) => { const now = Date.now(); - + // Remove timestamps outside the window while (promptTimestamps.length > 0 && promptTimestamps[0] < now - RATE_WINDOW) { promptTimestamps.shift(); } - + if (promptTimestamps.length >= RATE_LIMIT) { return { reject: true, rejectReason: `Rate limit exceeded. Please wait before sending more prompts.`, }; } - + promptTimestamps.push(now); return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -343,6 +350,7 @@ const session = await client.createSession({ return null; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` diff --git a/docs/mcp/overview.md b/docs/mcp/overview.md index aa2fba668..9ffba8cf4 100644 --- a/docs/mcp/overview.md +++ b/docs/mcp/overview.md @@ -52,6 +52,7 @@ const session = await client.createSession({ tools: ["*"], }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -180,6 +181,7 @@ async function main() { tools: ["*"], }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); console.log("Session created:", session.sessionId); diff --git a/nodejs/README.md b/nodejs/README.md index 31558b8ab..acc1dde58 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -35,6 +35,7 @@ await client.start(); // Create a session const session = await client.createSession({ model: "gpt-5", + onPermissionRequest: async () => ({ kind: "approved" }), }); // Wait for response using typed event handlers @@ -93,7 +94,7 @@ Stop the server and close all sessions. Returns a list of any errors encountered Force stop the CLI server without graceful cleanup. Use when `stop()` takes too long. -##### `createSession(config?: SessionConfig): Promise` +##### `createSession(config: SessionConfig): Promise` Create a new conversation session. @@ -106,10 +107,11 @@ Create a new conversation session. - `systemMessage?: SystemMessageConfig` - System message customization (see below) - `infiniteSessions?: InfiniteSessionConfig` - Configure automatic context compaction (see below) - `provider?: ProviderConfig` - Custom API provider configuration (BYOK - Bring Your Own Key). See [Custom Providers](#custom-providers) section. +- `onPermissionRequest: PermissionHandler` - Handler for permission requests from the server. See [Permission Control](../docs/compatibility.md#permission-control) section. - `onUserInputRequest?: UserInputHandler` - Handler for user input requests from the agent. Enables the `ask_user` tool. See [User Input Requests](#user-input-requests) section. - `hooks?: SessionHooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. -##### `resumeSession(sessionId: string, config?: ResumeSessionConfig): Promise` +##### `resumeSession(sessionId: string, config: ResumeSessionConfig): Promise` Resume an existing session. Returns the session with `workspacePath` populated if infinite sessions were enabled. @@ -314,6 +316,7 @@ Enable streaming to receive assistant response chunks as they're generated: const session = await client.createSession({ model: "gpt-5", streaming: true, + onPermissionRequest: async () => ({ kind: "approved" }), }); // Wait for completion using typed event handlers @@ -397,6 +400,7 @@ const session = await client.createSession({ }, }), ], + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -417,6 +421,7 @@ const session = await client.createSession({ `, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -431,6 +436,7 @@ const session = await client.createSession({ mode: "replace", content: "You are a helpful assistant.", }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -440,7 +446,7 @@ By default, sessions use **infinite sessions** which automatically manage contex ```typescript // Default: infinite sessions enabled with default thresholds -const session = await client.createSession({ model: "gpt-5" }); +const session = await client.createSession({ model: "gpt-5", onPermissionRequest: async () => ({ kind: "approved" }) }); // Access the workspace path for checkpoints and files console.log(session.workspacePath); @@ -454,12 +460,14 @@ const session = await client.createSession({ backgroundCompactionThreshold: 0.80, // Start compacting at 80% context usage bufferExhaustionThreshold: 0.95, // Block at 95% until compaction completes }, + onPermissionRequest: async () => ({ kind: "approved" }), }); // Disable infinite sessions const session = await client.createSession({ model: "gpt-5", infiniteSessions: { enabled: false }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -471,8 +479,8 @@ When enabled, sessions emit compaction events: ### Multiple Sessions ```typescript -const session1 = await client.createSession({ model: "gpt-5" }); -const session2 = await client.createSession({ model: "claude-sonnet-4.5" }); +const session1 = await client.createSession({ model: "gpt-5", onPermissionRequest: async () => ({ kind: "approved" }) }); +const session2 = await client.createSession({ model: "claude-sonnet-4.5", onPermissionRequest: async () => ({ kind: "approved" }) }); // Both sessions are independent await session1.sendAndWait({ prompt: "Hello from session 1" }); @@ -485,6 +493,7 @@ await session2.sendAndWait({ prompt: "Hello from session 2" }); const session = await client.createSession({ sessionId: "my-custom-session-id", model: "gpt-5", + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -526,6 +535,7 @@ const session = await client.createSession({ baseUrl: "http://localhost:11434/v1", // Ollama endpoint // apiKey not required for Ollama }, + onPermissionRequest: async () => ({ kind: "approved" }), }); await session.sendAndWait({ prompt: "Hello!" }); @@ -541,6 +551,7 @@ const session = await client.createSession({ baseUrl: "https://my-api.example.com/v1", apiKey: process.env.MY_API_KEY, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -557,6 +568,7 @@ const session = await client.createSession({ apiVersion: "2024-10-21", }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -588,6 +600,7 @@ const session = await client.createSession({ wasFreeform: true, // Whether the answer was freeform (not from choices) }; }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -648,6 +661,7 @@ const session = await client.createSession({ }; }, }, + onPermissionRequest: async () => ({ kind: "approved" }), }); ``` @@ -664,7 +678,7 @@ const session = await client.createSession({ ```typescript try { - const session = await client.createSession(); + const session = await client.createSession({ onPermissionRequest: async () => ({ kind: "approved" }) }); await session.send({ prompt: "Hello" }); } catch (error) { console.error("Error:", error.message); diff --git a/nodejs/examples/basic-example.ts b/nodejs/examples/basic-example.ts index b0b993138..2556cd27e 100644 --- a/nodejs/examples/basic-example.ts +++ b/nodejs/examples/basic-example.ts @@ -22,7 +22,7 @@ const lookupFactTool = defineTool("lookup_fact", { // Create client - will auto-start CLI server (searches PATH for "copilot") const client = new CopilotClient({ logLevel: "info" }); -const session = await client.createSession({ tools: [lookupFactTool] }); +const session = await client.createSession({ tools: [lookupFactTool], onPermissionRequest: async () => ({ kind: "approved" }) }); console.log(`✅ Session created: ${session.sessionId}\n`); // Listen to events