@agentplugins/adapter-copilot 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,618 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CopilotAdapter: () => CopilotAdapter,
24
+ copilotAdapter: () => copilotAdapter,
25
+ createCopilotAdapter: () => createCopilotAdapter
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ var import_core = require("@agentplugins/core");
29
+ var PLATFORM_NAME = "copilot";
30
+ var DISPLAY_NAME = "GitHub Copilot CLI";
31
+ var MANIFEST_PATH = "plugin.json";
32
+ var MANIFEST_FORMAT = "json";
33
+ var SUPPORTED_HOOKS = [
34
+ "sessionStart",
35
+ "sessionEnd",
36
+ "userPromptSubmit",
37
+ "preToolUse",
38
+ "postToolUse",
39
+ "postToolUseFailure",
40
+ "permissionRequest",
41
+ "subagentStart",
42
+ "subagentStop",
43
+ "preCompact",
44
+ "notification"
45
+ ];
46
+ var SUPPORTED_HANDLERS = ["command", "http", "prompt"];
47
+ var HOOK_MAP = {
48
+ sessionStart: "sessionStart",
49
+ sessionEnd: "sessionEnd",
50
+ userPromptSubmit: "userPromptSubmitted",
51
+ preToolUse: "preToolUse",
52
+ postToolUse: "postToolUse",
53
+ postToolUseFailure: "postToolUseFailure",
54
+ permissionRequest: "permissionRequest",
55
+ subagentStart: "subagentStart",
56
+ subagentStop: "agentStop",
57
+ // Copilot has no subagentStop; agentStop is closest
58
+ preCompact: "preCompact",
59
+ notification: "notification",
60
+ // Explicitly unsupported hooks
61
+ userPromptExpansion: void 0,
62
+ permissionDenied: void 0,
63
+ postCompact: void 0,
64
+ stop: void 0,
65
+ stopFailure: void 0,
66
+ fileChanged: void 0,
67
+ cwdChanged: void 0,
68
+ setup: void 0
69
+ };
70
+ var FIRE_AND_FORGET_HOOKS = ["notification"];
71
+ var FAIL_CLOSED_HOOKS = ["preToolUse"];
72
+ var MAX_HOOK_TIMEOUT_SECONDS = 30;
73
+ var MAX_ADDITIONAL_CONTEXT_BYTES = 10 * 1024;
74
+ var PROMPT_ALLOWED_HOOK = "sessionStart";
75
+ function byteSize(value) {
76
+ try {
77
+ return new globalThis.TextEncoder().encode(JSON.stringify(value)).length;
78
+ } catch {
79
+ return 0;
80
+ }
81
+ }
82
+ function issue(severity, message, field) {
83
+ return { severity, message, field };
84
+ }
85
+ function checkUnsupportedHook(hook, path) {
86
+ const mapped = HOOK_MAP[hook];
87
+ if (mapped === void 0) {
88
+ return issue(
89
+ import_core.Severity.ERROR,
90
+ `Hook "${hook}" is not supported by the Copilot CLI platform. Supported hooks: ${SUPPORTED_HOOKS.map((h) => `"${h}"`).join(", ")}.`,
91
+ path
92
+ );
93
+ }
94
+ return void 0;
95
+ }
96
+ function validateHandler(handler, hookName, path) {
97
+ const issues = [];
98
+ const handlerPath = `${path}.handler`;
99
+ if (!SUPPORTED_HANDLERS.includes(handler.type)) {
100
+ issues.push(
101
+ issue(
102
+ import_core.Severity.ERROR,
103
+ `Handler type "${handler.type}" is not supported by Copilot CLI. Supported types: ${SUPPORTED_HANDLERS.map((t) => `"${t}"`).join(", ")}.`,
104
+ handlerPath
105
+ )
106
+ );
107
+ return issues;
108
+ }
109
+ switch (handler.type) {
110
+ case "command": {
111
+ const cmd = handler.config;
112
+ if (!cmd.shell) {
113
+ issues.push(
114
+ issue(
115
+ import_core.Severity.ERROR,
116
+ `Command handler must specify "shell" (e.g. "bash" or "powershell").`,
117
+ `${handlerPath}.config.shell`
118
+ )
119
+ );
120
+ } else if (!["bash", "powershell"].includes(cmd.shell)) {
121
+ issues.push(
122
+ issue(
123
+ import_core.Severity.WARNING,
124
+ `Shell "${cmd.shell}" may not be supported by all Copilot CLI environments. Recommended: "bash" or "powershell".`,
125
+ `${handlerPath}.config.shell`
126
+ )
127
+ );
128
+ }
129
+ if (!cmd.script && !cmd.command) {
130
+ issues.push(
131
+ issue(
132
+ import_core.Severity.ERROR,
133
+ `Command handler must specify "script" or "command".`,
134
+ `${handlerPath}.config`
135
+ )
136
+ );
137
+ }
138
+ break;
139
+ }
140
+ case "http": {
141
+ const httpCfg = handler.config;
142
+ if (!httpCfg.url) {
143
+ issues.push(
144
+ issue(
145
+ import_core.Severity.ERROR,
146
+ `HTTP handler must specify "url".`,
147
+ `${handlerPath}.config.url`
148
+ )
149
+ );
150
+ }
151
+ if (httpCfg.method && httpCfg.method.toUpperCase() !== "POST") {
152
+ issues.push(
153
+ issue(
154
+ import_core.Severity.ERROR,
155
+ `Copilot CLI only supports POST for HTTP handlers. Found method: "${httpCfg.method}".`,
156
+ `${handlerPath}.config.method`
157
+ )
158
+ );
159
+ }
160
+ break;
161
+ }
162
+ case "prompt": {
163
+ if (hookName !== PROMPT_ALLOWED_HOOK) {
164
+ issues.push(
165
+ issue(
166
+ import_core.Severity.ERROR,
167
+ `Prompt handler type is only allowed for the "${PROMPT_ALLOWED_HOOK}" hook. Hook "${hookName}" cannot use a prompt handler.`,
168
+ handlerPath
169
+ )
170
+ );
171
+ }
172
+ const promptCfg = handler.config;
173
+ if (!promptCfg.template) {
174
+ issues.push(
175
+ issue(
176
+ import_core.Severity.ERROR,
177
+ `Prompt handler must specify a "template".`,
178
+ `${handlerPath}.config.template`
179
+ )
180
+ );
181
+ }
182
+ break;
183
+ }
184
+ }
185
+ return issues;
186
+ }
187
+ function validateHookConstraints(hook, hookName, path) {
188
+ const issues = [];
189
+ const copilotHook = hook;
190
+ if (copilotHook.additionalContext) {
191
+ const size = byteSize(copilotHook.additionalContext);
192
+ if (size > MAX_ADDITIONAL_CONTEXT_BYTES) {
193
+ issues.push(
194
+ issue(
195
+ import_core.Severity.ERROR,
196
+ `additionalContext exceeds ${MAX_ADDITIONAL_CONTEXT_BYTES} bytes (${size} bytes found). Reduce the size of data passed to hooks.`,
197
+ `${path}.additionalContext`
198
+ )
199
+ );
200
+ }
201
+ }
202
+ if (copilotHook.timeout !== void 0 && copilotHook.timeout > MAX_HOOK_TIMEOUT_SECONDS) {
203
+ issues.push(
204
+ issue(
205
+ import_core.Severity.ERROR,
206
+ `Hook timeout (${copilotHook.timeout}s) exceeds the Copilot CLI maximum of ${MAX_HOOK_TIMEOUT_SECONDS}s.`,
207
+ `${path}.timeout`
208
+ )
209
+ );
210
+ }
211
+ if (hookName === "preToolUse") {
212
+ const cfg = hook.handler?.config;
213
+ if (!cfg?.matcher?.field || !cfg?.matcher?.value) {
214
+ issues.push(
215
+ issue(
216
+ import_core.Severity.WARNING,
217
+ `preToolUse hooks should specify a matcher (e.g. toolName: "Bash") to avoid intercepting every tool call. Without a matcher, the hook runs for ALL tools and errors will deny them (fail-closed behavior).`,
218
+ `${path}.handler.config.matcher`
219
+ )
220
+ );
221
+ }
222
+ }
223
+ return issues;
224
+ }
225
+ function compileSkill(skill) {
226
+ const copilotSkill = skill;
227
+ const descriptor = {
228
+ name: skill.name,
229
+ description: skill.description,
230
+ parameters: copilotSkill.parameters?.map((p) => ({
231
+ name: p.name,
232
+ type: p.type,
233
+ description: p.description,
234
+ required: p.required
235
+ })),
236
+ examples: copilotSkill.examples
237
+ };
238
+ const skillDir = `skills/${skill.name}`;
239
+ let md = `# ${skill.name}
240
+
241
+ `;
242
+ md += `${skill.description}
243
+
244
+ `;
245
+ if (descriptor.parameters && descriptor.parameters.length > 0) {
246
+ md += `## Parameters
247
+
248
+ `;
249
+ md += `| Name | Type | Required | Description |
250
+ `;
251
+ md += `|------|------|----------|-------------|
252
+ `;
253
+ for (const p of descriptor.parameters) {
254
+ md += `| ${p.name} | ${p.type} | ${p.required ? "Yes" : "No"} | ${p.description ?? ""} |
255
+ `;
256
+ }
257
+ md += `
258
+ `;
259
+ }
260
+ if (descriptor.examples && descriptor.examples.length > 0) {
261
+ md += `## Examples
262
+
263
+ `;
264
+ for (const ex of descriptor.examples) {
265
+ md += `\`\`\`
266
+ ${ex}
267
+ \`\`\`
268
+
269
+ `;
270
+ }
271
+ }
272
+ md += `## Metadata
273
+
274
+ `;
275
+ md += `\`\`\`json
276
+ ${JSON.stringify(descriptor, null, 2)}
277
+ \`\`\`
278
+ `;
279
+ return { descriptor, skillDir, skillMdContent: md };
280
+ }
281
+ function compileHookEntry(hookName, hook) {
282
+ const handler = hook.handler;
283
+ const copilotEvent = HOOK_MAP[hookName];
284
+ const entry = {
285
+ event: copilotEvent,
286
+ type: handler.type,
287
+ handler: ""
288
+ };
289
+ switch (handler.type) {
290
+ case "command": {
291
+ const cfg = handler.config;
292
+ entry.handler = cfg.script ?? cfg.command ?? "";
293
+ break;
294
+ }
295
+ case "http": {
296
+ const cfg = handler.config;
297
+ entry.handler = cfg.url;
298
+ break;
299
+ }
300
+ case "prompt": {
301
+ const cfg = handler.config;
302
+ entry.handler = cfg.template;
303
+ break;
304
+ }
305
+ }
306
+ const matcher = handler.config?.matcher;
307
+ if (matcher?.field && matcher?.value) {
308
+ entry.matcher = {
309
+ field: matcher.field,
310
+ value: matcher.value
311
+ };
312
+ }
313
+ if (FIRE_AND_FORGET_HOOKS.includes(copilotEvent)) {
314
+ entry.awaitResponse = false;
315
+ }
316
+ const copilotHook = hook;
317
+ const timeout = copilotHook.timeout ?? MAX_HOOK_TIMEOUT_SECONDS;
318
+ entry.timeout = Math.min(timeout, MAX_HOOK_TIMEOUT_SECONDS);
319
+ if (FAIL_CLOSED_HOOKS.includes(copilotEvent)) {
320
+ entry.failClosed = true;
321
+ }
322
+ return entry;
323
+ }
324
+ var CopilotAdapter = class {
325
+ /** Platform identifier. */
326
+ name = PLATFORM_NAME;
327
+ /** Human-readable display name. */
328
+ displayName = DISPLAY_NAME;
329
+ /** Universal hooks supported by this adapter. */
330
+ supportedHooks = SUPPORTED_HOOKS;
331
+ /** Handler types understood by Copilot CLI. */
332
+ supportedHandlers = SUPPORTED_HANDLERS;
333
+ /** Path to the generated manifest file. */
334
+ manifestPath = MANIFEST_PATH;
335
+ /** Manifest format (JSON). */
336
+ manifestFormat = MANIFEST_FORMAT;
337
+ /**
338
+ * Validate a {@link PluginManifest} for Copilot CLI compatibility.
339
+ *
340
+ * Checks performed:
341
+ * 1. Only supported hooks are referenced.
342
+ * 2. Handler types are within the supported set.
343
+ * 3. Command handlers specify shell (bash/powershell) and script/command.
344
+ * 4. HTTP handlers use POST (the only method Copilot sends).
345
+ * 5. Prompt handlers are only used on `sessionStart`.
346
+ * 6. additionalContext does not exceed 10 KB.
347
+ * 7. Hook timeouts do not exceed 30 seconds.
348
+ * 8. preToolUse hooks are encouraged to specify a matcher.
349
+ * 9. All referenced skills have required fields (name, description).
350
+ *
351
+ * @param plugin – the plugin manifest to validate
352
+ * @returns array of validation issues (empty if fully valid)
353
+ */
354
+ validate(plugin) {
355
+ const issues = [];
356
+ if (plugin.hooks) {
357
+ for (const [hookName, hookDef] of Object.entries(plugin.hooks)) {
358
+ const hookPath = `hooks.${hookName}`;
359
+ const uHook = hookName;
360
+ const unsupported = checkUnsupportedHook(uHook, hookPath);
361
+ if (unsupported) {
362
+ issues.push(unsupported);
363
+ continue;
364
+ }
365
+ if (hookDef.handler) {
366
+ issues.push(
367
+ ...validateHandler(
368
+ hookDef.handler,
369
+ uHook,
370
+ hookPath
371
+ )
372
+ );
373
+ } else {
374
+ issues.push(
375
+ issue(
376
+ import_core.Severity.ERROR,
377
+ `Hook "${hookName}" is missing a handler definition.`,
378
+ hookPath
379
+ )
380
+ );
381
+ }
382
+ issues.push(...validateHookConstraints(hookDef, uHook, hookPath));
383
+ }
384
+ }
385
+ if (plugin.skills) {
386
+ for (let i = 0; i < plugin.skills.length; i++) {
387
+ const skill = plugin.skills[i];
388
+ const skillPath = `skills[${i}]`;
389
+ if (!skill.name || skill.name.trim() === "") {
390
+ issues.push(
391
+ issue(
392
+ import_core.Severity.ERROR,
393
+ `Skill at index ${i} is missing a "name".`,
394
+ `${skillPath}.name`
395
+ )
396
+ );
397
+ }
398
+ if (!skill.description || skill.description.trim() === "") {
399
+ issues.push(
400
+ issue(
401
+ import_core.Severity.ERROR,
402
+ `Skill "${skill.name ?? `#${i}`}" is missing a "description".`,
403
+ `${skillPath}.description`
404
+ )
405
+ );
406
+ }
407
+ const copilotSkill = skill;
408
+ if (copilotSkill.parameters) {
409
+ const validTypes = ["string", "number", "boolean", "object", "array"];
410
+ for (let j = 0; j < copilotSkill.parameters.length; j++) {
411
+ const p = copilotSkill.parameters[j];
412
+ if (!validTypes.includes(p.type)) {
413
+ issues.push(
414
+ issue(
415
+ import_core.Severity.WARNING,
416
+ `Parameter "${p.name}" has type "${p.type}" which may not be recognised by Copilot CLI. Valid types: ${validTypes.join(", ")}.`,
417
+ `${skillPath}.parameters[${j}].type`
418
+ )
419
+ );
420
+ }
421
+ }
422
+ }
423
+ }
424
+ }
425
+ if (plugin.hooks?.preToolUse) {
426
+ const preToolHandler = plugin.hooks.preToolUse.handler;
427
+ const matcher = preToolHandler?.config?.matcher;
428
+ if (!matcher) {
429
+ issues.push(
430
+ issue(
431
+ import_core.Severity.WARNING,
432
+ `Plugin defines a "preToolUse" hook without a matcher. This will intercept ALL tool calls and any error will deny them (fail-closed). Consider adding a matcher like { field: "toolName", value: "Bash" }.`,
433
+ `hooks.preToolUse.handler.config.matcher`
434
+ )
435
+ );
436
+ }
437
+ }
438
+ return issues;
439
+ }
440
+ /**
441
+ * Compile a {@link PluginManifest} into the Copilot CLI file layout.
442
+ *
443
+ * Produces:
444
+ * - `plugin.json` – top-level manifest with metadata and file references
445
+ * - `hooks.json` – hook bindings mapped to Copilot event names
446
+ * - `skills/<name>/SKILL.md` – one file per skill
447
+ * - `.mcp.json` – MCP server configuration (if MCP servers are defined)
448
+ *
449
+ * @param plugin – the validated plugin manifest
450
+ * @returns {@link AdapterOutput} containing all generated files
451
+ */
452
+ compile(plugin) {
453
+ const files = [];
454
+ const warnings = [];
455
+ const hookEntries = [];
456
+ if (plugin.hooks) {
457
+ for (const [hookName, hookDef] of Object.entries(plugin.hooks)) {
458
+ const uHook = hookName;
459
+ if (!SUPPORTED_HOOKS.includes(uHook)) {
460
+ warnings.push(
461
+ `Skipping unsupported hook "${hookName}" during compilation.`
462
+ );
463
+ continue;
464
+ }
465
+ const handler = hookDef.handler;
466
+ if (handler.type === "prompt") {
467
+ const entry = compileHookEntry(uHook, hookDef);
468
+ hookEntries.push(entry);
469
+ } else {
470
+ const entry = compileHookEntry(uHook, hookDef);
471
+ if (handler.type === "command") {
472
+ const cfg = handler.config;
473
+ const scriptName = `hook-${hookName}.${cfg.shell === "powershell" ? "ps1" : "sh"}`;
474
+ const scriptContent = generateWrapperScript(cfg);
475
+ files.push({
476
+ path: scriptName,
477
+ content: scriptContent
478
+ });
479
+ entry.handler = `./${scriptName}`;
480
+ }
481
+ hookEntries.push(entry);
482
+ }
483
+ }
484
+ }
485
+ if (hookEntries.length > 0) {
486
+ files.push({
487
+ path: "hooks.json",
488
+ content: JSON.stringify(hookEntries, null, 2)
489
+ });
490
+ }
491
+ const skillDirs = [];
492
+ if (plugin.skills && plugin.skills.length > 0) {
493
+ for (const skill of plugin.skills) {
494
+ const { skillDir, skillMdContent } = compileSkill(skill);
495
+ skillDirs.push(skillDir);
496
+ files.push({
497
+ path: `${skillDir}/SKILL.md`,
498
+ content: skillMdContent
499
+ });
500
+ }
501
+ }
502
+ const mcpConfig = this.buildMcpConfig(plugin);
503
+ if (mcpConfig) {
504
+ files.push({
505
+ path: ".mcp.json",
506
+ content: JSON.stringify(mcpConfig, null, 2)
507
+ });
508
+ }
509
+ const copilotPlugin = plugin;
510
+ const copilotManifest = {
511
+ schemaVersion: "1.0",
512
+ id: copilotPlugin.id ?? copilotPlugin.name,
513
+ name: copilotPlugin.name,
514
+ description: copilotPlugin.description,
515
+ version: copilotPlugin.version,
516
+ strict: copilotPlugin.strict ?? true,
517
+ maxAdditionalContextBytes: MAX_ADDITIONAL_CONTEXT_BYTES,
518
+ hookTimeoutSeconds: MAX_HOOK_TIMEOUT_SECONDS,
519
+ author: typeof copilotPlugin.author === "string" ? copilotPlugin.author : copilotPlugin.author?.name,
520
+ homepage: copilotPlugin.homepage,
521
+ license: copilotPlugin.license,
522
+ tags: copilotPlugin.tags ?? copilotPlugin.keywords ?? void 0,
523
+ ...hookEntries.length > 0 && { hooks: "hooks.json" },
524
+ ...skillDirs.length > 0 && { skills: skillDirs },
525
+ ...mcpConfig && { mcp: ".mcp.json" }
526
+ };
527
+ files.unshift({
528
+ path: MANIFEST_PATH,
529
+ content: JSON.stringify(copilotManifest, null, 2)
530
+ });
531
+ return {
532
+ files,
533
+ manifest: copilotManifest,
534
+ warnings: warnings.length > 0 ? warnings : [],
535
+ issues: []
536
+ };
537
+ }
538
+ /**
539
+ * Build an MCP server configuration object from the plugin manifest.
540
+ *
541
+ * If the plugin defines MCP servers (in `plugin.mcpServers`), they are
542
+ * converted into Copilot's `.mcp.json` format.
543
+ *
544
+ * @param plugin – the plugin manifest
545
+ * @returns MCP config object, or `null` if no MCP servers are defined
546
+ */
547
+ buildMcpConfig(plugin) {
548
+ const servers = plugin.mcpServers;
549
+ if (!servers || servers.length === 0) {
550
+ return null;
551
+ }
552
+ return {
553
+ schemaVersion: "1.0",
554
+ servers: servers.map((s) => ({
555
+ id: s.id,
556
+ name: s.name,
557
+ transport: s.transport,
558
+ ...s.command && { command: s.command },
559
+ ...s.args && { args: s.args },
560
+ ...s.url && { url: s.url },
561
+ ...s.env && { env: s.env }
562
+ }))
563
+ };
564
+ }
565
+ };
566
+ function generateWrapperScript(cfg) {
567
+ const scriptBody = cfg.script ?? cfg.command ?? "";
568
+ if (cfg.shell === "powershell") {
569
+ return `
570
+ # Auto-generated Copilot CLI hook wrapper
571
+ param(
572
+ [Parameter(ValueFromPipeline = $true)]
573
+ [string]$Payload
574
+ )
575
+
576
+ # Read payload from stdin if not provided as argument
577
+ if (-not $Payload) {
578
+ $Payload = $input | Out-String
579
+ }
580
+
581
+ # Parse JSON payload
582
+ $HookData = $Payload | ConvertFrom-Json -ErrorAction SilentlyContinue
583
+
584
+ # TODO: export relevant fields as environment variables if needed
585
+ # $env:COPILOT_TOOL_NAME = $HookData.toolName
586
+
587
+ # Execute user script
588
+ ${scriptBody}
589
+ `.trim();
590
+ }
591
+ return `#!/usr/bin/env bash
592
+ # Auto-generated Copilot CLI hook wrapper
593
+
594
+ set -euo pipefail
595
+
596
+ # Read JSON payload from stdin
597
+ PAYLOAD="$(cat)"
598
+
599
+ # Parse payload with jq if available; otherwise pass through raw
600
+ if command -v jq &> /dev/null; then
601
+ TOOL_NAME="$(echo "$PAYLOAD" | jq -r '.toolName // empty')"
602
+ export COPILOT_TOOL_NAME="\${TOOL_NAME:-}"
603
+ fi
604
+
605
+ # Execute user script
606
+ ${scriptBody}
607
+ `.trim();
608
+ }
609
+ var copilotAdapter = new CopilotAdapter();
610
+ function createCopilotAdapter() {
611
+ return new CopilotAdapter();
612
+ }
613
+ // Annotate the CommonJS export names for ESM import in node:
614
+ 0 && (module.exports = {
615
+ CopilotAdapter,
616
+ copilotAdapter,
617
+ createCopilotAdapter
618
+ });