@b-apm/integration-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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sdju
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # @b-apm/integration-copilot
2
+
3
+ > **UNSTABLE:** Early public release. APIs and on-disk layouts may change without a major bump. Not production-ready.
4
+
5
+ GitHub Copilot **runtime** host for bapm (project-scope file primitives + home MCP translate).
6
+
7
+ ## Opt-in load
8
+
9
+ ```bash
10
+ npm i -D @b-apm/integration-copilot
11
+ ```
12
+
13
+ ```yaml
14
+ targets:
15
+ copilot: "@b-apm/integration-copilot"
16
+ active:
17
+ - copilot
18
+ ```
19
+
20
+ ```bash
21
+ bapm install --target copilot
22
+ bapm compile --target copilot
23
+ ```
24
+
25
+ ## Detection vs forced target
26
+
27
+ | Mode | When active | Creates roots? |
28
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
29
+ | **Auto-detect** | Any whitelist signal under `.github/` (`copilot-instructions.md` or instructions/agents/prompts/hooks dirs) | Only when materialize / configure runs after detect |
30
+ | **Forced** (`--target copilot`) | Explicit forced target id, even if detect is false | MAY `mkdir` `.github/` / `.agents/` as needed |
31
+
32
+ Auto-detect without a whitelist signal MUST NOT create roots solely for detection.
33
+
34
+ ## Deploy roots
35
+
36
+ - `.github` — instructions, prompts, agents, hooks, compile output, ownership sidecar
37
+ - `.agents` — portable skills under `.agents/skills/<name>/`
38
+
39
+ Home MCP (`~/.copilot/mcp-config.json`, override with `COPILOT_HOME`) is outside project deploy roots.
40
+
41
+ ## Materialize routing
42
+
43
+ | Primitive type | Destination |
44
+ | ---------------- | --------------------------------------------------------------------------------------------------- |
45
+ | instruction | `.github/instructions/<name>.instructions.md` (preserve `applyTo` / Copilot FM) |
46
+ | command / prompt | `.github/prompts/<name>.prompt.md` (never `.github/commands/`) |
47
+ | agent | `.github/agents/<name>.agent.md` |
48
+ | skill | `.agents/skills/<name>/` (never `.github/skills/`) |
49
+ | hook | per-file `.github/hooks/<pkg>-<stem>.json` + scripts under `.github/hooks/scripts/<pkg>/` + sidecar |
50
+
51
+ MCP is **not** written by materialize — only via `configureMcp`.
52
+
53
+ ## MCP (translate)
54
+
55
+ - Config: `~/.copilot/mcp-config.json` (`mcpServers`)
56
+ - `mcpEnvMode: "translate"` — APM `${VAR}` / `${env:VAR}` / `<VAR>` stay as runtime `${VAR}` placeholders
57
+ - Install skips bake for this host; unrelated servers and top-level keys are preserved
58
+ - Never writes project `.vscode/mcp.json`
59
+
60
+ ## Compile
61
+
62
+ Thin emitter → `.github/copilot-instructions.md`. Instruction primitives already deployed under `.github/instructions/` are omitted from the compile body. Honors `write` vs validate/preview.
63
+
64
+ ## Exports
65
+
66
+ - `createCopilotIntegration` / `createIntegration` — runtime factory (`id: "copilot"`)
@@ -0,0 +1,17 @@
1
+ import { BapmIntegration } from "@b-apm/integration-api";
2
+ //#region src/createCopilotIntegration.d.ts
3
+ /**
4
+ * Create the GitHub Copilot runtime target.
5
+ * Detect: APM SIGNAL_WHITELIST under `.github/` (any one signal; no mkdir).
6
+ * Materialize: instructions/prompts/agents/hooks under `.github/`, skills under `.agents/skills/`.
7
+ * MCP: home `~/.copilot/mcp-config.json` (`COPILOT_HOME`) with translate placeholders.
8
+ * Compile: thin `.github/copilot-instructions.md`, omitting instruction primitives.
9
+ */
10
+ declare function createCopilotIntegration(options?: {
11
+ id?: string;
12
+ deployRoots?: string[];
13
+ }): BapmIntegration;
14
+ /** Normalize hook event names to camelCase (`session_start` / `SessionStart` → `sessionStart`). */
15
+ declare function toCamelCaseEvent(event: string): string;
16
+ //#endregion
17
+ export { createCopilotIntegration, createCopilotIntegration as createIntegration, toCamelCaseEvent };
package/dist/index.mjs ADDED
@@ -0,0 +1,368 @@
1
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, join, resolve } from "node:path";
4
+ import { assertUnderDeployRoots, compileMarkdownReport, copyHookScript, materializeSkill, primitivesList, primitivesMaterialize, readHookOwnershipSidecar, readPrimitiveContent, removeOwnedHookArtifacts, renderPrimitivesMarkdown, sanitizeName, writeDeployedFile, writeHookOwnershipSidecar } from "@b-apm/integration-api";
5
+ //#region src/createCopilotIntegration.ts
6
+ const DEFAULT_DEPLOY_ROOTS = [".github", ".agents"];
7
+ const HOOKS_OWNERSHIP_REL = ".github/bapm-hooks.json";
8
+ const COMPILE_DEFAULT = ".github/copilot-instructions.md";
9
+ const DETECT_DIRS = [
10
+ ".github/instructions",
11
+ ".github/agents",
12
+ ".github/prompts",
13
+ ".github/hooks"
14
+ ];
15
+ /**
16
+ * Create the GitHub Copilot runtime target.
17
+ * Detect: APM SIGNAL_WHITELIST under `.github/` (any one signal; no mkdir).
18
+ * Materialize: instructions/prompts/agents/hooks under `.github/`, skills under `.agents/skills/`.
19
+ * MCP: home `~/.copilot/mcp-config.json` (`COPILOT_HOME`) with translate placeholders.
20
+ * Compile: thin `.github/copilot-instructions.md`, omitting instruction primitives.
21
+ */
22
+ function createCopilotIntegration(options) {
23
+ const id = options?.id ?? "copilot";
24
+ const deployRoots = [...options?.deployRoots ?? DEFAULT_DEPLOY_ROOTS];
25
+ return {
26
+ id,
27
+ deployRoots,
28
+ mcpEnvMode: "translate",
29
+ detect: ({ cwd }) => detectCopilot(cwd),
30
+ getDeployRoots: () => [...deployRoots],
31
+ async compile(primitives, context) {
32
+ const content = renderPrimitivesMarkdown({
33
+ primitives: primitivesList(primitives),
34
+ title: "# GitHub Copilot instructions",
35
+ filter: (p) => !/instruction/i.test(String(p.type ?? ""))
36
+ });
37
+ return compileMarkdownReport({
38
+ cwd: context.cwd,
39
+ outputFile: context.outputFile ?? COMPILE_DEFAULT,
40
+ write: context.write,
41
+ content,
42
+ outsideCwdMessage: "Copilot compile output must be a cwd-relative file path"
43
+ });
44
+ },
45
+ async materialize(primitives, ctx) {
46
+ const cwd = resolve(ctx?.cwd ?? process.cwd());
47
+ const roots = ctx?.deployRoots?.length ? [...ctx.deployRoots] : deployRoots;
48
+ const deployedFiles = [];
49
+ const diagnostics = [];
50
+ const hookPrimitives = [];
51
+ await primitivesMaterialize(primitives, {
52
+ skill(p, { name }) {
53
+ deployedFiles.push(...materializeSkill({
54
+ primitive: p,
55
+ cwd,
56
+ deployRoots: roots,
57
+ destDir: join(".agents", "skills", name)
58
+ }));
59
+ },
60
+ instruction(p, { name }) {
61
+ deployedFiles.push(writeDeployedFile({
62
+ cwd,
63
+ deployRoots: roots,
64
+ destRel: join(".github", "instructions", `${name}.instructions.md`),
65
+ content: readPrimitiveContent(p),
66
+ primitive: {
67
+ name: String(p.name),
68
+ packageName: p.packageName
69
+ }
70
+ }));
71
+ },
72
+ agent(p, { name }) {
73
+ deployedFiles.push(writeDeployedFile({
74
+ cwd,
75
+ deployRoots: roots,
76
+ destRel: join(".github", "agents", `${name}.agent.md`),
77
+ content: readPrimitiveContent(p),
78
+ primitive: {
79
+ name: String(p.name),
80
+ packageName: p.packageName
81
+ }
82
+ }));
83
+ },
84
+ command(p, { name }) {
85
+ materializePromptLike(p, name, cwd, roots, deployedFiles);
86
+ },
87
+ hook(p) {
88
+ hookPrimitives.push(p);
89
+ },
90
+ unknown(p, { name, type }) {
91
+ if (/prompt/.test(type)) materializePromptLike(p, name, cwd, roots, deployedFiles);
92
+ }
93
+ });
94
+ if (hookPrimitives.length > 0) {
95
+ const hookDeployed = materializeCopilotHooks({
96
+ cwd,
97
+ roots,
98
+ hooks: hookPrimitives
99
+ });
100
+ deployedFiles.push(...hookDeployed.deployedFiles);
101
+ diagnostics.push(...hookDeployed.diagnostics);
102
+ }
103
+ return {
104
+ targetId: id,
105
+ deployedFiles,
106
+ ...diagnostics.length > 0 ? { diagnostics } : {}
107
+ };
108
+ },
109
+ async configureMcp(servers, ctx) {
110
+ return writeCopilotMcpConfig(servers, {
111
+ cwd: resolve(ctx?.cwd ?? process.cwd()),
112
+ deployRoots: ctx?.deployRoots?.length ? [...ctx.deployRoots] : deployRoots,
113
+ targetId: ctx?.targetId ?? id
114
+ });
115
+ }
116
+ };
117
+ }
118
+ function detectCopilot(cwd) {
119
+ const instructions = join(cwd, ".github", "copilot-instructions.md");
120
+ if (existsSync(instructions) && statSync(instructions).isFile()) return true;
121
+ for (const rel of DETECT_DIRS) {
122
+ const abs = join(cwd, rel);
123
+ if (existsSync(abs) && statSync(abs).isDirectory()) return true;
124
+ }
125
+ return false;
126
+ }
127
+ function materializePromptLike(p, name, cwd, roots, deployedFiles) {
128
+ deployedFiles.push(writeDeployedFile({
129
+ cwd,
130
+ deployRoots: roots,
131
+ destRel: join(".github", "prompts", `${name}.prompt.md`),
132
+ content: readPrimitiveContent(p),
133
+ primitive: {
134
+ name: String(p.name),
135
+ packageName: p.packageName
136
+ }
137
+ }));
138
+ }
139
+ function resolveCopilotHome() {
140
+ const override = process.env.COPILOT_HOME?.trim();
141
+ if (override) return resolve(override);
142
+ return join(homedir(), ".copilot");
143
+ }
144
+ function writeCopilotMcpConfig(servers, ctx) {
145
+ const home = resolveCopilotHome();
146
+ mkdirSync(home, { recursive: true });
147
+ const destFile = join(home, "mcp-config.json");
148
+ const existingDoc = readExistingMcpDoc(destFile);
149
+ const existingServers = existingDoc.mcpServers && typeof existingDoc.mcpServers === "object" ? { ...existingDoc.mcpServers } : {};
150
+ const { entries: incoming, diagnostics } = normalizeServerEntries(servers);
151
+ const nextServers = { ...existingServers };
152
+ const writtenNames = [];
153
+ for (const [name, entry] of Object.entries(incoming)) {
154
+ const prior = existingServers[name];
155
+ const merged = prior && typeof prior === "object" ? {
156
+ ...prior,
157
+ ...entry
158
+ } : { ...entry };
159
+ delete merged.format;
160
+ delete merged.packageName;
161
+ delete merged.registry;
162
+ nextServers[name] = merged;
163
+ writtenNames.push(name);
164
+ }
165
+ const doc = {
166
+ ...existingDoc,
167
+ mcpServers: nextServers
168
+ };
169
+ writeFileSync(destFile, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
170
+ return {
171
+ targetId: ctx.targetId,
172
+ configPath: destFile,
173
+ servers: writtenNames,
174
+ deployedFiles: [],
175
+ diagnostics
176
+ };
177
+ }
178
+ function readExistingMcpDoc(path) {
179
+ if (!existsSync(path)) return {};
180
+ try {
181
+ const raw = JSON.parse(readFileSync(path, "utf8"));
182
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? { ...raw } : {};
183
+ } catch {
184
+ return {};
185
+ }
186
+ }
187
+ function normalizeServerEntries(servers) {
188
+ const out = {};
189
+ const diagnostics = [];
190
+ const list = Array.isArray(servers) ? servers : Object.entries(servers).map(([name, value]) => ({
191
+ ...value,
192
+ name: value.name ?? name
193
+ }));
194
+ for (const server of list) {
195
+ const name = String(server.name ?? "").trim();
196
+ if (!name) continue;
197
+ out[name] = toCopilotServerEntry(server);
198
+ }
199
+ return {
200
+ entries: out,
201
+ diagnostics
202
+ };
203
+ }
204
+ function translateEnvPlaceholders(env) {
205
+ const out = {};
206
+ for (const [key, raw] of Object.entries(env)) {
207
+ if (typeof raw !== "string") {
208
+ out[key] = String(raw);
209
+ continue;
210
+ }
211
+ out[key] = raw.replace(/\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, "${$1}").replace(/<([A-Z_][A-Z0-9_]*)>/g, "${$1}");
212
+ }
213
+ return out;
214
+ }
215
+ function toCopilotServerEntry(server) {
216
+ const transport = String(server.transport ?? server.type ?? "").toLowerCase();
217
+ const entry = {};
218
+ if (transport === "http" || transport === "sse" || typeof server.url === "string") {
219
+ if (server.url) entry.url = server.url;
220
+ if (transport) entry.type = transport === "sse" ? "sse" : "http";
221
+ } else {
222
+ if (server.command) entry.command = String(server.command);
223
+ if (Array.isArray(server.args)) entry.args = server.args;
224
+ entry.type = "stdio";
225
+ }
226
+ if (server.env && typeof server.env === "object") entry.env = translateEnvPlaceholders(server.env);
227
+ for (const [key, value] of Object.entries(server)) {
228
+ if ([
229
+ "name",
230
+ "transport",
231
+ "type",
232
+ "command",
233
+ "args",
234
+ "url",
235
+ "env",
236
+ "packageName",
237
+ "registry",
238
+ "format",
239
+ "cwd"
240
+ ].includes(key)) continue;
241
+ if (value !== void 0) entry[key] = value;
242
+ }
243
+ return entry;
244
+ }
245
+ /** Normalize hook event names to camelCase (`session_start` / `SessionStart` → `sessionStart`). */
246
+ function toCamelCaseEvent(event) {
247
+ const trimmed = String(event ?? "").trim();
248
+ if (!trimmed) return trimmed;
249
+ if (trimmed.includes("_") || trimmed.includes("-")) return trimmed.split(/[_-]+/).filter(Boolean).map((part, index) => {
250
+ const lower = part.toLowerCase();
251
+ if (index === 0) return lower;
252
+ return lower.charAt(0).toUpperCase() + lower.slice(1);
253
+ }).join("");
254
+ return trimmed.charAt(0).toLowerCase() + trimmed.slice(1);
255
+ }
256
+ function packageKey(p) {
257
+ if (typeof p.packageName === "string" && p.packageName.trim()) return sanitizeName(p.packageName);
258
+ const source = String(p.source ?? "");
259
+ if (source.startsWith("dependency:")) return sanitizeName(source.slice(11));
260
+ return "local";
261
+ }
262
+ function materializeCopilotHooks(args) {
263
+ const { cwd, roots, hooks } = args;
264
+ const deployedFiles = [];
265
+ const diagnostics = [];
266
+ const ownershipPath = join(cwd, HOOKS_OWNERSHIP_REL);
267
+ assertUnderDeployRoots(cwd, ownershipPath, roots);
268
+ mkdirSync(join(cwd, ".github", "hooks"), { recursive: true });
269
+ const ownership = readHookOwnershipSidecar(ownershipPath);
270
+ removeOwnedHookArtifacts(cwd, ownership);
271
+ const nextOwned = {};
272
+ for (const p of hooks) {
273
+ const stem = sanitizeName(String(p.name));
274
+ const pkg = packageKey(p);
275
+ const hookRel = `.github/hooks/${pkg}-${stem}.json`;
276
+ const hookAbs = join(cwd, hookRel);
277
+ assertUnderDeployRoots(cwd, hookAbs, roots);
278
+ const srcPath = p.path ? resolve(p.path) : void 0;
279
+ if (!srcPath || !existsSync(srcPath) || !statSync(srcPath).isFile()) {
280
+ diagnostics.push({
281
+ code: "COPILOT_HOOK_SOURCE_MISSING",
282
+ message: `Hook primitive "${p.name}" has no readable source JSON`,
283
+ primitive: String(p.name)
284
+ });
285
+ continue;
286
+ }
287
+ let parsed;
288
+ try {
289
+ parsed = JSON.parse(readFileSync(srcPath, "utf8"));
290
+ } catch (cause) {
291
+ diagnostics.push({
292
+ code: "COPILOT_HOOK_JSON_INVALID",
293
+ message: `Hook "${p.name}" JSON is invalid: ${cause instanceof Error ? cause.message : String(cause)}`,
294
+ primitive: String(p.name)
295
+ });
296
+ continue;
297
+ }
298
+ const sourceHooks = parsed.hooks && typeof parsed.hooks === "object" ? parsed.hooks : {};
299
+ const outHooks = {};
300
+ const scripts = [];
301
+ for (const [event, entries] of Object.entries(sourceHooks)) {
302
+ if (!Array.isArray(entries)) continue;
303
+ const camel = toCamelCaseEvent(event);
304
+ const destList = [];
305
+ for (const entry of entries) {
306
+ if (!entry || typeof entry !== "object") continue;
307
+ const command = typeof entry.command === "string" ? entry.command.trim() : "";
308
+ const { _apm_source: _drop, ...clean } = entry;
309
+ if (!command) {
310
+ destList.push({ ...clean });
311
+ continue;
312
+ }
313
+ const rewritten = copyHookScript({
314
+ cwd,
315
+ deployRoots: roots,
316
+ hookFile: srcPath,
317
+ command,
318
+ alreadyDeployedNeedle: ".github/hooks/",
319
+ destRel: `.github/hooks/scripts/${pkg}/${basename(command.replace(/^\.\//, ""))}`,
320
+ commandAsDotSlash: true
321
+ });
322
+ destList.push({
323
+ ...clean,
324
+ command: rewritten.commandRel
325
+ });
326
+ if (rewritten.scriptRel) {
327
+ scripts.push(rewritten.scriptRel);
328
+ deployedFiles.push({
329
+ path: rewritten.scriptRel,
330
+ primitive: {
331
+ name: String(p.name),
332
+ packageName: p.packageName
333
+ }
334
+ });
335
+ }
336
+ }
337
+ outHooks[camel] = destList;
338
+ }
339
+ writeFileSync(hookAbs, `${JSON.stringify({ hooks: outHooks }, null, 2)}\n`, "utf8");
340
+ deployedFiles.push({
341
+ path: hookRel,
342
+ primitive: {
343
+ name: String(p.name),
344
+ packageName: p.packageName
345
+ }
346
+ });
347
+ const ownedKey = `${pkg}-${stem}`;
348
+ nextOwned[ownedKey] = {
349
+ ...p.packageName ? { packageName: p.packageName } : {},
350
+ hookFile: hookRel,
351
+ scripts
352
+ };
353
+ }
354
+ writeHookOwnershipSidecar(ownershipPath, { owned: nextOwned });
355
+ deployedFiles.push({
356
+ path: HOOKS_OWNERSHIP_REL,
357
+ ...hooks[0] ? { primitive: {
358
+ name: String(hooks[0].name),
359
+ packageName: hooks[0].packageName
360
+ } } : {}
361
+ });
362
+ return {
363
+ deployedFiles,
364
+ diagnostics
365
+ };
366
+ }
367
+ //#endregion
368
+ export { createCopilotIntegration, createCopilotIntegration as createIntegration, toCamelCaseEvent };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@b-apm/integration-copilot",
3
+ "version": "0.1.0",
4
+ "description": "GitHub Copilot runtime integration for bapm",
5
+ "license": "MIT",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "type": "module",
10
+ "exports": {
11
+ ".": "./dist/index.mjs",
12
+ "./package.json": "./package.json"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "dependencies": {
18
+ "@b-apm/integration-api": "0.1.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^26.1.2",
22
+ "typescript": "^7.0.2",
23
+ "vite": "npm:@voidzero-dev/vite-plus-core@latest",
24
+ "vite-plus": "latest"
25
+ },
26
+ "engines": {
27
+ "node": ">=22.12.0"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/Sdju/bapm.git",
32
+ "directory": "packages/integration-copilot"
33
+ },
34
+ "homepage": "https://github.com/Sdju/bapm#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/Sdju/bapm/issues"
37
+ },
38
+ "keywords": [
39
+ "unstable",
40
+ "bapm",
41
+ "b-apm"
42
+ ],
43
+ "scripts": {
44
+ "build": "vp pack",
45
+ "dev": "vp pack --watch",
46
+ "test": "vp test",
47
+ "check": "vp check"
48
+ }
49
+ }