@github/copilot-sdk-darwin-arm64 0.0.1 → 1.0.13-preview.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/animations/app-install-nudge.json.gz +0 -0
  2. package/animations/banner.json.gz +0 -0
  3. package/builtin/customize-cloud-agent/SKILL.md +254 -0
  4. package/builtin/discover-resources/SKILL.md +35 -0
  5. package/builtin/github-pr-media/SKILL.md +108 -0
  6. package/builtin-skills/customize-cloud-agent/SKILL.md +254 -0
  7. package/builtin-skills/discover-resources/SKILL.md +35 -0
  8. package/builtin-skills/github-pr-media/SKILL.md +108 -0
  9. package/copilot-sdk/canvas.d.ts +126 -0
  10. package/copilot-sdk/client.d.ts +478 -0
  11. package/copilot-sdk/copilotRequestHandler.d.ts +85 -0
  12. package/copilot-sdk/docs/agent-author.md +295 -0
  13. package/copilot-sdk/docs/examples.md +682 -0
  14. package/copilot-sdk/docs/extensions.md +81 -0
  15. package/copilot-sdk/docs/factories.md +279 -0
  16. package/copilot-sdk/docs/factory-patterns.md +194 -0
  17. package/copilot-sdk/extension.d.ts +59 -0
  18. package/copilot-sdk/extension.js +11399 -0
  19. package/copilot-sdk/factory.d.ts +301 -0
  20. package/copilot-sdk/ffiRuntimeHost.d.ts +35 -0
  21. package/copilot-sdk/generated/rpc.d.ts +24623 -0
  22. package/copilot-sdk/generated/session-events.d.ts +11117 -0
  23. package/copilot-sdk/index.d.ts +15 -0
  24. package/copilot-sdk/index.js +11540 -0
  25. package/copilot-sdk/runtimeArtifacts.d.ts +6 -0
  26. package/copilot-sdk/sdkProtocolVersion.d.ts +10 -0
  27. package/copilot-sdk/session.d.ts +331 -0
  28. package/copilot-sdk/sessionFsProvider.d.ts +107 -0
  29. package/copilot-sdk/telemetry.d.ts +14 -0
  30. package/copilot-sdk/toolSet.d.ts +75 -0
  31. package/copilot-sdk/types.d.ts +2974 -0
  32. package/definitions/code-review.agent.yaml +94 -0
  33. package/definitions/explore.agent.yaml +75 -0
  34. package/definitions/rem-agent.agent.yaml +22 -0
  35. package/definitions/research.agent.yaml +111 -0
  36. package/definitions/rubber-duck.agent.yaml +67 -0
  37. package/definitions/security-review.agent.yaml +261 -0
  38. package/definitions/sidekick/cloud-session-search.yaml +37 -0
  39. package/definitions/sidekick/github-context-memory.yaml +46 -0
  40. package/definitions/sidekick/github-context.yaml +44 -0
  41. package/definitions/sidekick/session-search.yaml +37 -0
  42. package/definitions/sidekick/subconscious-agent.yaml +60 -0
  43. package/definitions/sidekick/test-sidekick-context-changed.yaml +24 -0
  44. package/definitions/sidekick/test-sidekick-persistent.yaml +23 -0
  45. package/definitions/sidekick/test-sidekick-restart.yaml +23 -0
  46. package/definitions/sidekick/test-sidekick-trigger-once.yaml +22 -0
  47. package/definitions/task.agent.yaml +44 -0
  48. package/package.json +14 -11
  49. package/plugins/computer-use/.mcp.json +10 -0
  50. package/plugins/computer-use/.plugin/plugin.json +6 -0
  51. package/plugins/computer-use/.release-target +1 -0
  52. package/plugins/computer-use/Copilot Computer Use.app/Contents/CodeResources +0 -0
  53. package/plugins/computer-use/Copilot Computer Use.app/Contents/Info.plist +38 -0
  54. package/plugins/computer-use/Copilot Computer Use.app/Contents/MacOS/Copilot Computer Use +0 -0
  55. package/plugins/computer-use/Copilot Computer Use.app/Contents/PkgInfo +1 -0
  56. package/plugins/computer-use/Copilot Computer Use.app/Contents/Resources/Assets.car +0 -0
  57. package/plugins/computer-use/Copilot Computer Use.app/Contents/Resources/icon.icns +0 -0
  58. package/plugins/computer-use/Copilot Computer Use.app/Contents/_CodeSignature/CodeResources +139 -0
  59. package/plugins/computer-use/computer-use-mcp +0 -0
  60. package/prebuilds/darwin-arm64/copilot-runtime +0 -0
  61. package/prebuilds/darwin-arm64/runtime.node +0 -0
  62. package/preloads/extension_bootstrap.mjs +68 -0
  63. package/preloads/extension_sdk_resolver.mjs +34 -0
  64. package/ripgrep/bin/darwin-arm64/rg +0 -0
  65. package/schemas/api.schema.json +41931 -0
  66. package/schemas/session-events.schema.json +20937 -0
  67. package/sdk/index.js +1489 -0
  68. package/tgrep/bin/darwin-arm64/tgrep +0 -0
  69. package/README.md +0 -3
@@ -0,0 +1,682 @@
1
+ # Copilot CLI Extension Examples
2
+
3
+ A practical guide to writing extensions using the `@github/copilot-sdk` extension API.
4
+
5
+ ## Extension Skeleton
6
+
7
+ Every extension starts with the same boilerplate:
8
+
9
+ ```js
10
+ import { joinSession } from "@github/copilot-sdk/extension";
11
+
12
+ const session = await joinSession({
13
+ hooks: {
14
+ /* ... */
15
+ },
16
+ tools: [
17
+ /* ... */
18
+ ],
19
+ });
20
+ ```
21
+
22
+ `joinSession` returns a `CopilotSession` object you can use to send messages and subscribe to events.
23
+
24
+ > **Platform notes (Windows vs macOS/Linux):**
25
+ >
26
+ > - Use `process.platform === "win32"` to detect Windows at runtime.
27
+ > - Clipboard: `pbcopy` on macOS, `clip` on Windows.
28
+ > - Use `exec()` instead of `execFile()` for `.cmd` scripts like `code`, `npx`, `npm` on Windows.
29
+ > - PowerShell stderr redirection uses `*>&1` instead of `2>&1`.
30
+
31
+ ---
32
+
33
+ ## Logging to the Timeline
34
+
35
+ Use `session.log()` to surface messages to the user in the CLI timeline:
36
+
37
+ ```js
38
+ const session = await joinSession({
39
+ hooks: {
40
+ onSessionStart: async () => {
41
+ await session.log("My extension loaded");
42
+ },
43
+ onPreToolUse: async (input) => {
44
+ if (input.toolName === "bash") {
45
+ await session.log(`Running: ${input.toolArgs?.command}`, { ephemeral: true });
46
+ }
47
+ },
48
+ },
49
+ tools: [],
50
+ });
51
+ ```
52
+
53
+ Levels: `"info"` (default), `"warning"`, `"error"`. Set `ephemeral: true` for transient messages that aren't persisted.
54
+
55
+ ---
56
+
57
+ ## Registering Custom Tools
58
+
59
+ Tools are functions the agent can call. Define them with a name, description, JSON Schema parameters, and a handler.
60
+
61
+ ### Basic tool
62
+
63
+ ```js
64
+ tools: [
65
+ {
66
+ name: "my_tool",
67
+ description: "Does something useful",
68
+ parameters: {
69
+ type: "object",
70
+ properties: {
71
+ input: { type: "string", description: "The input value" },
72
+ },
73
+ required: ["input"],
74
+ },
75
+ handler: async (args) => {
76
+ return `Processed: ${args.input}`;
77
+ },
78
+ },
79
+ ];
80
+ ```
81
+
82
+ ### Tool that invokes an external shell command
83
+
84
+ ```js
85
+ import { execFile } from "node:child_process";
86
+
87
+ {
88
+ name: "run_command",
89
+ description: "Runs a shell command and returns its output",
90
+ parameters: {
91
+ type: "object",
92
+ properties: {
93
+ command: { type: "string", description: "The command to run" },
94
+ },
95
+ required: ["command"],
96
+ },
97
+ handler: async (args) => {
98
+ const isWindows = process.platform === "win32";
99
+ const shell = isWindows ? "powershell" : "bash";
100
+ const shellArgs = isWindows
101
+ ? ["-NoProfile", "-Command", args.command]
102
+ : ["-c", args.command];
103
+ return new Promise((resolve) => {
104
+ execFile(shell, shellArgs, (err, stdout, stderr) => {
105
+ if (err) resolve(`Error: ${stderr || err.message}`);
106
+ else resolve(stdout);
107
+ });
108
+ });
109
+ },
110
+ }
111
+ ```
112
+
113
+ ### Tool that calls an external API
114
+
115
+ ```js
116
+ {
117
+ name: "fetch_data",
118
+ description: "Fetches data from an API endpoint",
119
+ parameters: {
120
+ type: "object",
121
+ properties: {
122
+ url: { type: "string", description: "The URL to fetch" },
123
+ },
124
+ required: ["url"],
125
+ },
126
+ handler: async (args) => {
127
+ const res = await fetch(args.url);
128
+ if (!res.ok) return `Error: HTTP ${res.status}`;
129
+ return await res.text();
130
+ },
131
+ }
132
+ ```
133
+
134
+ ### Tool handler invocation context
135
+
136
+ The handler receives a second argument with invocation metadata:
137
+
138
+ ```js
139
+ handler: async (args, invocation) => {
140
+ // invocation.sessionId — current session ID
141
+ // invocation.toolCallId — unique ID for this tool call
142
+ // invocation.toolName — name of the tool being called
143
+ return "done";
144
+ };
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Hooks
150
+
151
+ Hooks intercept and modify behavior at key lifecycle points. Register them in the `hooks` option.
152
+
153
+ ### Available Hooks
154
+
155
+ | Hook | Fires When | Can Modify |
156
+ | ----------------------- | ---------------------------------------- | ------------------------------------------- |
157
+ | `onUserPromptSubmitted` | User sends a message | The prompt text, add context |
158
+ | `onPreToolUse` | Before a tool executes | Tool args, permission decision, add context |
159
+ | `onPostToolUse` | After a tool executes successfully | Tool result, add context |
160
+ | `onPostToolUseFailure` | After a tool execution returns a failure | Add hidden guidance to the model |
161
+ | `onSessionStart` | Session starts or resumes | Add context |
162
+ | `onSessionEnd` | Session ends | Cleanup actions, summary |
163
+ | `onErrorOccurred` | An error occurs | Error handling strategy (retry/skip/abort) |
164
+
165
+ All hook inputs include `timestamp` (`Date`) and `workingDirectory`.
166
+
167
+ ### Modifying the user's message
168
+
169
+ Use `onUserPromptSubmitted` to rewrite or augment what the user typed before the agent sees it.
170
+
171
+ ```js
172
+ hooks: {
173
+ onUserPromptSubmitted: async (input) => {
174
+ // Rewrite the prompt
175
+ return { modifiedPrompt: input.prompt.toUpperCase() };
176
+ },
177
+ }
178
+ ```
179
+
180
+ ### Injecting additional context into every message
181
+
182
+ Return `additionalContext` to silently append instructions the agent will follow.
183
+
184
+ ```js
185
+ hooks: {
186
+ onUserPromptSubmitted: async (input) => {
187
+ return {
188
+ additionalContext: "Always respond in bullet points. Follow our team coding standards.",
189
+ };
190
+ },
191
+ }
192
+ ```
193
+
194
+ ### Sending a follow-up message based on a keyword
195
+
196
+ Use `session.send()` to programmatically inject a new user message.
197
+
198
+ ```js
199
+ hooks: {
200
+ onUserPromptSubmitted: async (input) => {
201
+ if (/\\burgent\\b/i.test(input.prompt)) {
202
+ // Fire-and-forget a follow-up message
203
+ setTimeout(() => session.send({ prompt: "Please prioritize this." }), 0);
204
+ }
205
+ },
206
+ }
207
+ ```
208
+
209
+ > **Tip:** Guard against infinite loops if your follow-up message could re-trigger the same hook.
210
+
211
+ ### Blocking dangerous tool calls
212
+
213
+ Use `onPreToolUse` to inspect and optionally deny tool execution.
214
+
215
+ ```js
216
+ hooks: {
217
+ onPreToolUse: async (input) => {
218
+ if (input.toolName === "bash") {
219
+ const cmd = String(input.toolArgs?.command || "");
220
+ if (/rm\\s+-rf/i.test(cmd) || /Remove-Item\\s+.*-Recurse/i.test(cmd)) {
221
+ return {
222
+ permissionDecision: "deny",
223
+ permissionDecisionReason: "Destructive commands are not allowed.",
224
+ };
225
+ }
226
+ }
227
+ // Allow everything else
228
+ return { permissionDecision: "allow" };
229
+ },
230
+ }
231
+ ```
232
+
233
+ ### Modifying tool arguments before execution
234
+
235
+ ```js
236
+ hooks: {
237
+ onPreToolUse: async (input) => {
238
+ if (input.toolName === "bash") {
239
+ const redirect = process.platform === "win32" ? "*>&1" : "2>&1";
240
+ return {
241
+ modifiedArgs: {
242
+ ...input.toolArgs,
243
+ command: `${input.toolArgs.command} ${redirect}`,
244
+ },
245
+ };
246
+ }
247
+ },
248
+ }
249
+ ```
250
+
251
+ ### Reacting when the agent creates or edits a file
252
+
253
+ Use `onPostToolUse` to run side effects after a tool completes.
254
+
255
+ ```js
256
+ import { exec } from "node:child_process";
257
+
258
+ hooks: {
259
+ onPostToolUse: async (input) => {
260
+ if (input.toolName === "create" || input.toolName === "edit") {
261
+ const filePath = input.toolArgs?.path;
262
+ if (filePath) {
263
+ // Open the file in VS Code
264
+ exec(`code "${filePath}"`, () => {});
265
+ }
266
+ }
267
+ },
268
+ }
269
+ ```
270
+
271
+ ### Reacting when a tool fails
272
+
273
+ `onPostToolUse` only fires for successful tool executions. To observe or react
274
+ to failures, register `onPostToolUseFailure`. The input includes
275
+ `input.error` (the stringified failure message); only `additionalContext` on
276
+ the return value is consumed by the runtime, and it is appended as hidden
277
+ guidance alongside the failed tool result.
278
+
279
+ ```js
280
+ hooks: {
281
+ onPostToolUseFailure: async (input) => {
282
+ if (input.toolName === "bash") {
283
+ return {
284
+ additionalContext: "The command failed. Try a different approach.",
285
+ };
286
+ }
287
+ },
288
+ }
289
+ ```
290
+
291
+ ### Running a linter after every file edit
292
+
293
+ ```js
294
+ import { exec } from "node:child_process";
295
+
296
+ hooks: {
297
+ onPostToolUse: async (input) => {
298
+ if (input.toolName === "edit") {
299
+ const filePath = input.toolArgs?.path;
300
+ if (filePath?.endsWith(".ts")) {
301
+ const result = await new Promise((resolve) => {
302
+ exec(`npx eslint "${filePath}"`, (err, stdout) => {
303
+ resolve(err ? stdout : "No lint errors.");
304
+ });
305
+ });
306
+ return { additionalContext: `Lint result: ${result}` };
307
+ }
308
+ }
309
+ },
310
+ }
311
+ ```
312
+
313
+ ### Handling errors with retry logic
314
+
315
+ ```js
316
+ hooks: {
317
+ onErrorOccurred: async (input) => {
318
+ if (input.recoverable && input.errorContext === "model_call") {
319
+ return { errorHandling: "retry", retryCount: 2 };
320
+ }
321
+ return {
322
+ errorHandling: "abort",
323
+ userNotification: `An error occurred: ${input.error}`,
324
+ };
325
+ },
326
+ }
327
+ ```
328
+
329
+ ### Session lifecycle hooks
330
+
331
+ ```js
332
+ hooks: {
333
+ onSessionStart: async (input) => {
334
+ // input.source is "startup", "resume", or "new"
335
+ return { additionalContext: "Remember to write tests for all changes." };
336
+ },
337
+ onSessionEnd: async (input) => {
338
+ // input.reason is "complete", "error", "abort", "timeout", or "user_exit"
339
+ },
340
+ }
341
+ ```
342
+
343
+ ---
344
+
345
+ ## Session Events
346
+
347
+ After calling `joinSession`, use `session.on()` to react to events in real time.
348
+
349
+ ### Listening to a specific event type
350
+
351
+ ```js
352
+ session.on("assistant.message", (event) => {
353
+ // event.data.content has the agent's response text
354
+ });
355
+ ```
356
+
357
+ ### Listening to all events
358
+
359
+ ```js
360
+ session.on((event) => {
361
+ // event.type and event.data are available for all events
362
+ });
363
+ ```
364
+
365
+ ### Unsubscribing from events
366
+
367
+ `session.on()` returns an unsubscribe function:
368
+
369
+ ```js
370
+ const unsubscribe = session.on("tool.execution_complete", (event) => {
371
+ // event.data.success, event.data.result, event.data.error
372
+ });
373
+
374
+ // Later, stop listening
375
+ unsubscribe();
376
+ ```
377
+
378
+ ### Example: Auto-copy agent responses to clipboard
379
+
380
+ Combine a hook (to detect a keyword) with a session event (to capture the response):
381
+
382
+ ```js
383
+ import { execFile } from "node:child_process";
384
+
385
+ let copyNextResponse = false;
386
+
387
+ function copyToClipboard(text) {
388
+ const cmd = process.platform === "win32" ? "clip" : "pbcopy";
389
+ const proc = execFile(cmd, [], () => {});
390
+ proc.stdin.write(text);
391
+ proc.stdin.end();
392
+ }
393
+
394
+ const session = await joinSession({
395
+ hooks: {
396
+ onUserPromptSubmitted: async (input) => {
397
+ if (/\\bcopy\\b/i.test(input.prompt)) {
398
+ copyNextResponse = true;
399
+ }
400
+ },
401
+ },
402
+ tools: [],
403
+ });
404
+
405
+ session.on("assistant.message", (event) => {
406
+ if (copyNextResponse) {
407
+ copyNextResponse = false;
408
+ copyToClipboard(event.data.content);
409
+ }
410
+ });
411
+ ```
412
+
413
+ ### Top 10 Most Useful Event Types
414
+
415
+ | Event Type | Description | Key Data Fields |
416
+ | --------------------------- | ------------------------------------------------ | ------------------------------------------------------ |
417
+ | `assistant.message` | Agent's final response | `content`, `messageId`, `toolRequests` |
418
+ | `assistant.message_delta` | Message content chunks (ephemeral) | `deltaContent` |
419
+ | `tool.execution_start` | A tool is about to run | `toolCallId`, `toolName`, `arguments` |
420
+ | `tool.execution_complete` | A tool finished running | `toolCallId`, `success`, `result`, `error` |
421
+ | `user.message` | User sent a message | `content`, `attachments`, `source` |
422
+ | `session.idle` | Session finished processing a turn | `aborted` |
423
+ | `session.error` | An error occurred | `errorType`, `message`, `stack` |
424
+ | `permission.requested` | Agent needs permission (shell, file write, etc.) | `requestId`, `permissionRequest.kind` |
425
+ | `session.shutdown` | Session is ending | `shutdownType`, `totalPremiumRequests`, `codeChanges` |
426
+ | `assistant.turn_start` | Agent begins a new thinking/response cycle | `turnId` |
427
+
428
+ ### Example: Detecting when the plan file is created or edited
429
+
430
+ Use `session.workspacePath` to locate the session's `plan.md`, then `fs.watchFile` to detect changes.
431
+ Correlate `tool.execution_start` / `tool.execution_complete` events by `toolCallId` to distinguish agent edits from user edits.
432
+
433
+ ```js
434
+ import { existsSync, watchFile, readFileSync } from "node:fs";
435
+ import { join } from "node:path";
436
+ import { joinSession } from "@github/copilot-sdk/extension";
437
+
438
+ const agentEdits = new Set(); // toolCallIds for in-flight agent edits
439
+ const recentAgentPaths = new Set(); // paths recently written by the agent
440
+
441
+ const session = await joinSession();
442
+
443
+ const workspace = session.workspacePath; // e.g. ~/.copilot/session-state/<id>
444
+ if (workspace) {
445
+ const planPath = join(workspace, "plan.md");
446
+ let lastContent = existsSync(planPath) ? readFileSync(planPath, "utf-8") : null;
447
+
448
+ // Track agent edits to suppress false triggers
449
+ session.on("tool.execution_start", (event) => {
450
+ if (
451
+ (event.data.toolName === "edit" || event.data.toolName === "create") &&
452
+ String(event.data.arguments?.path || "").endsWith("plan.md")
453
+ ) {
454
+ agentEdits.add(event.data.toolCallId);
455
+ recentAgentPaths.add(planPath);
456
+ }
457
+ });
458
+ session.on("tool.execution_complete", (event) => {
459
+ if (agentEdits.delete(event.data.toolCallId)) {
460
+ setTimeout(() => {
461
+ recentAgentPaths.delete(planPath);
462
+ lastContent = existsSync(planPath) ? readFileSync(planPath, "utf-8") : null;
463
+ }, 2000);
464
+ }
465
+ });
466
+
467
+ watchFile(planPath, { interval: 1000 }, () => {
468
+ if (recentAgentPaths.has(planPath) || agentEdits.size > 0) return;
469
+ const content = existsSync(planPath) ? readFileSync(planPath, "utf-8") : null;
470
+ if (content === lastContent) return;
471
+ const wasCreated = lastContent === null && content !== null;
472
+ lastContent = content;
473
+ if (content !== null) {
474
+ session.send({
475
+ prompt: `The plan was ${wasCreated ? "created" : "edited"} by the user.`,
476
+ });
477
+ }
478
+ });
479
+ }
480
+ ```
481
+
482
+ ### Example: Reacting when the user manually edits any file in the repo
483
+
484
+ Use `fs.watch` with `recursive: true` on `process.cwd()` to detect file changes.
485
+ Filter out agent edits by tracking `tool.execution_start` / `tool.execution_complete` events.
486
+
487
+ ```js
488
+ import { watch, readFileSync, statSync } from "node:fs";
489
+ import { join, relative, resolve } from "node:path";
490
+ import { joinSession } from "@github/copilot-sdk/extension";
491
+
492
+ const agentEditPaths = new Set();
493
+
494
+ const session = await joinSession();
495
+
496
+ const cwd = process.cwd();
497
+ const IGNORE = new Set(["node_modules", ".git", "dist"]);
498
+
499
+ // Track agent file edits
500
+ session.on("tool.execution_start", (event) => {
501
+ if (event.data.toolName === "edit" || event.data.toolName === "create") {
502
+ const p = String(event.data.arguments?.path || "");
503
+ if (p) agentEditPaths.add(resolve(p));
504
+ }
505
+ });
506
+ session.on("tool.execution_complete", (event) => {
507
+ // Clear after a delay to avoid race with fs.watch
508
+ const p = [...agentEditPaths].find((x) => x); // any tracked path
509
+ setTimeout(() => agentEditPaths.clear(), 3000);
510
+ });
511
+
512
+ const debounce = new Map();
513
+
514
+ watch(cwd, { recursive: true }, (eventType, filename) => {
515
+ if (!filename || eventType !== "change") return;
516
+ if (filename.split(/[\\\\\\/]/).some((p) => IGNORE.has(p))) return;
517
+
518
+ if (debounce.has(filename)) clearTimeout(debounce.get(filename));
519
+ debounce.set(filename, setTimeout(() => {
520
+ debounce.delete(filename);
521
+ const fullPath = join(cwd, filename);
522
+ if (agentEditPaths.has(resolve(fullPath))) return;
523
+
524
+ try { if (!statSync(fullPath).isFile()) return; } catch { return; }
525
+ const relPath = relative(cwd, fullPath);
526
+ session.send({
527
+ prompt: `The user edited \\`${relPath}\\`.`,
528
+ attachments: [{ type: "file", path: fullPath }],
529
+ });
530
+ }, 500));
531
+ });
532
+ ```
533
+
534
+ ---
535
+
536
+ ## Sending Messages Programmatically
537
+
538
+ ### Fire-and-forget
539
+
540
+ ```js
541
+ await session.send({ prompt: "Analyze the test results." });
542
+ ```
543
+
544
+ ### Send and wait for the response
545
+
546
+ ```js
547
+ const response = await session.sendAndWait({ prompt: "What is 2 + 2?" });
548
+ // response?.data.content contains the agent's reply
549
+ ```
550
+
551
+ ### Send with file attachments
552
+
553
+ ```js
554
+ await session.send({
555
+ prompt: "Review this file",
556
+ attachments: [{ type: "file", path: "./src/index.ts" }],
557
+ });
558
+ ```
559
+
560
+ ---
561
+
562
+ ## Permission and User Input Handlers
563
+
564
+ ### Custom permission logic
565
+
566
+ ```js
567
+ const session = await joinSession({
568
+ onPermissionRequest: async (request) => {
569
+ if (request.kind === "shell") {
570
+ // request.fullCommandText has the shell command
571
+ return { kind: "approve-once" };
572
+ }
573
+ if (request.kind === "write") {
574
+ return { kind: "approve-once" };
575
+ }
576
+ return { kind: "reject" };
577
+ },
578
+ });
579
+ ```
580
+
581
+ ### Handling agent questions (ask_user)
582
+
583
+ Register `onUserInputRequest` to enable the agent's `ask_user` tool:
584
+
585
+ ```js
586
+ const session = await joinSession({
587
+ onUserInputRequest: async (request) => {
588
+ // request.question has the agent's question
589
+ // request.choices has the options (if multiple choice)
590
+ return { answer: "yes", wasFreeform: false };
591
+ },
592
+ });
593
+ ```
594
+
595
+ ---
596
+
597
+ ## Complete Example: Multi-Feature Extension
598
+
599
+ An extension that combines tools, hooks, and events.
600
+
601
+ ```js
602
+ import { execFile, exec } from "node:child_process";
603
+ import { joinSession } from "@github/copilot-sdk/extension";
604
+
605
+ const isWindows = process.platform === "win32";
606
+ let copyNextResponse = false;
607
+
608
+ function copyToClipboard(text) {
609
+ const proc = execFile(isWindows ? "clip" : "pbcopy", [], () => {});
610
+ proc.stdin.write(text);
611
+ proc.stdin.end();
612
+ }
613
+
614
+ function openInEditor(filePath) {
615
+ if (isWindows) exec(`code "${filePath}"`, () => {});
616
+ else execFile("code", [filePath], () => {});
617
+ }
618
+
619
+ const session = await joinSession({
620
+ hooks: {
621
+ onUserPromptSubmitted: async (input) => {
622
+ if (/\\bcopy this\\b/i.test(input.prompt)) {
623
+ copyNextResponse = true;
624
+ }
625
+ return {
626
+ additionalContext: "Follow our team style guide. Use 4-space indentation.",
627
+ };
628
+ },
629
+ onPreToolUse: async (input) => {
630
+ if (input.toolName === "bash") {
631
+ const cmd = String(input.toolArgs?.command || "");
632
+ if (/rm\\s+-rf\\s+\//i.test(cmd) || /Remove-Item\\s+.*-Recurse/i.test(cmd)) {
633
+ return {
634
+ permissionDecision: "deny",
635
+ permissionDecisionReason: "Destructive commands are not allowed.",
636
+ };
637
+ }
638
+ }
639
+ },
640
+ onPostToolUse: async (input) => {
641
+ if (input.toolName === "create" || input.toolName === "edit") {
642
+ const filePath = input.toolArgs?.path;
643
+ if (filePath) openInEditor(filePath);
644
+ }
645
+ },
646
+ },
647
+ tools: [
648
+ {
649
+ name: "copy_to_clipboard",
650
+ description: "Copies text to the system clipboard.",
651
+ parameters: {
652
+ type: "object",
653
+ properties: {
654
+ text: { type: "string", description: "Text to copy" },
655
+ },
656
+ required: ["text"],
657
+ },
658
+ handler: async (args) => {
659
+ return new Promise((resolve) => {
660
+ const proc = execFile(isWindows ? "clip" : "pbcopy", [], (err) => {
661
+ if (err) resolve(`Error: ${err.message}`);
662
+ else resolve("Copied to clipboard.");
663
+ });
664
+ proc.stdin.write(args.text);
665
+ proc.stdin.end();
666
+ });
667
+ },
668
+ },
669
+ ],
670
+ });
671
+
672
+ session.on("assistant.message", (event) => {
673
+ if (copyNextResponse) {
674
+ copyNextResponse = false;
675
+ copyToClipboard(event.data.content);
676
+ }
677
+ });
678
+
679
+ session.on("tool.execution_complete", (event) => {
680
+ // event.data.success, event.data.result
681
+ });
682
+ ```