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