skillrepo 3.2.0 → 4.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.
Files changed (53) hide show
  1. package/README.md +137 -27
  2. package/bin/skillrepo.mjs +5 -5
  3. package/package.json +1 -1
  4. package/src/commands/add.mjs +21 -6
  5. package/src/commands/get.mjs +20 -4
  6. package/src/commands/init-cohort-hooks.mjs +127 -0
  7. package/src/commands/init-session-sync.mjs +1 -1
  8. package/src/commands/init.mjs +480 -117
  9. package/src/commands/list.mjs +1 -1
  10. package/src/commands/remove.mjs +10 -2
  11. package/src/commands/uninstall.mjs +13 -2
  12. package/src/commands/update.mjs +112 -19
  13. package/src/lib/agent-hook-merge.mjs +203 -0
  14. package/src/lib/agent-registry.mjs +399 -0
  15. package/src/lib/artifact-registry.mjs +111 -2
  16. package/src/lib/cli-config.mjs +146 -44
  17. package/src/lib/detect-agents.mjs +112 -0
  18. package/src/lib/file-write.mjs +162 -77
  19. package/src/lib/fs-utils.mjs +16 -1
  20. package/src/lib/mcp-merge.mjs +17 -36
  21. package/src/lib/mergers/agent-hook-claude-shape.mjs +519 -0
  22. package/src/lib/mergers/agent-hook-cursor-shape.mjs +318 -0
  23. package/src/lib/mergers/gitignore.mjs +55 -28
  24. package/src/lib/paths.mjs +27 -25
  25. package/src/lib/prompt-multiselect.mjs +324 -0
  26. package/src/lib/removers/agent-hooks.mjs +83 -0
  27. package/src/lib/sync.mjs +18 -19
  28. package/src/test/commands/add.test.mjs +18 -3
  29. package/src/test/commands/init-picker.test.mjs +144 -0
  30. package/src/test/commands/init.test.mjs +508 -41
  31. package/src/test/commands/remove.test.mjs +4 -1
  32. package/src/test/commands/update.test.mjs +148 -3
  33. package/src/test/e2e/cli-agent-permutations.test.mjs +631 -0
  34. package/src/test/e2e/cli-cohort-hooks.test.mjs +393 -0
  35. package/src/test/e2e/cli-commands.test.mjs +39 -13
  36. package/src/test/integration/agent-hooks.integration.test.mjs +340 -0
  37. package/src/test/integration/file-write.integration.test.mjs +31 -10
  38. package/src/test/lib/agent-hook-merge.test.mjs +172 -0
  39. package/src/test/lib/agent-registry.test.mjs +215 -0
  40. package/src/test/lib/artifact-registry.test.mjs +39 -0
  41. package/src/test/lib/cli-config.test.mjs +222 -38
  42. package/src/test/lib/detect-agents.test.mjs +336 -0
  43. package/src/test/lib/file-write-placement.test.mjs +264 -0
  44. package/src/test/lib/file-write.test.mjs +231 -30
  45. package/src/test/lib/mcp-merge.test.mjs +23 -15
  46. package/src/test/lib/paths.test.mjs +53 -17
  47. package/src/test/lib/prompt-multiselect.test.mjs +448 -0
  48. package/src/test/lib/sync.test.mjs +157 -0
  49. package/src/test/mergers/agent-hook-claude-shape.test.mjs +518 -0
  50. package/src/test/mergers/agent-hook-cursor-shape.test.mjs +306 -0
  51. package/src/test/removers/agent-hooks.test.mjs +206 -0
  52. package/src/lib/detect-ides.mjs +0 -44
  53. package/src/test/detect-ides.test.mjs +0 -65
@@ -33,11 +33,14 @@
33
33
  * - Blocked extensions — rejected (matches server BLOCKED_EXTENSIONS)
34
34
  * - Skill name vs frontmatter `name` — must match
35
35
  *
36
- * • Vendor placement: Claude Code uses .claude/skills/<name>/ (project)
37
- * or ~/.claude/skills/<name>/ (--global) per Anthropic's documented
38
- * convention. Other detected vendors without their own convention
39
- * fall back to <cwd>/skills/<name>/, with /skills/ added to .gitignore
40
- * on first creation. See follow-up #876.
36
+ * • Vendor placement: per-vendor decisions live in `agent-registry.mjs`.
37
+ * Claude Code writes to .claude/skills/<name>/ (Anthropic's documented
38
+ * convention); the cohort of cursor / windsurf / gemini / codex / cline
39
+ * / copilot shares .agents/skills/<name>/ for project scope (with
40
+ * `.agents/skills/` added to .gitignore on first cohort write). Personal
41
+ * scope honors per-vendor conventions where they exist (Windsurf has its
42
+ * own personal path under ~/.codeium/windsurf/skills/). See
43
+ * `packages/cli/docs/vendor-paths.md` for the full citation list.
41
44
  */
42
45
 
43
46
  import {
@@ -55,12 +58,17 @@ import { readFileSafe, writeFileSafe } from "./fs-utils.mjs";
55
58
  import {
56
59
  claudeSkillsProject,
57
60
  claudeSkillsGlobal,
58
- projectSkillsFallback,
59
- projectSkillsFallbackRoot,
60
61
  claudeSkillsProjectRoot,
61
62
  claudeSkillsGlobalRoot,
63
+ agentsSkillsProject,
64
+ agentsSkillsProjectRoot,
65
+ agentsSkillsGlobal,
66
+ agentsSkillsGlobalRoot,
67
+ windsurfSkillsGlobal,
68
+ windsurfSkillsGlobalRoot,
62
69
  gitignorePath,
63
70
  } from "./paths.mjs";
71
+ import { AGENT_REGISTRY, getAgentByKey } from "./agent-registry.mjs";
64
72
  import { CliError, validationError, diskError } from "./errors.mjs";
65
73
  import { platformConventions } from "./platform.mjs";
66
74
 
@@ -83,7 +91,7 @@ export const BLOCKED_EXTENSIONS = new Set([
83
91
  ".wasm",
84
92
  ]);
85
93
 
86
- const GITIGNORE_SKILLS_LINE = "/skills/";
94
+ const GITIGNORE_AGENTS_SKILLS_LINE = ".agents/skills/";
87
95
  const GITIGNORE_SKILLS_HEADER = "# SkillRepo (CLI-managed library skills)";
88
96
 
89
97
  // ── Types (JSDoc only — this is plain JS) ───────────────────────────────
@@ -102,9 +110,25 @@ const GITIGNORE_SKILLS_HEADER = "# SkillRepo (CLI-managed library skills)";
102
110
  */
103
111
 
104
112
  /**
105
- * @typedef {"claudeProject" | "claudeGlobal" | "projectFallback"} PlacementTarget
113
+ * @typedef {"claudeProject" | "claudeGlobal" | "agentsProject" | "agentsGlobal" | "windsurfGlobal"} PlacementTarget
106
114
  */
107
115
 
116
+ /** @type {readonly PlacementTarget[]} */
117
+ const ALL_TARGETS = Object.freeze([
118
+ "claudeProject",
119
+ "claudeGlobal",
120
+ "agentsProject",
121
+ "agentsGlobal",
122
+ "windsurfGlobal",
123
+ ]);
124
+
125
+ /** @type {readonly PlacementTarget[]} */
126
+ const GLOBAL_TARGETS = Object.freeze([
127
+ "claudeGlobal",
128
+ "agentsGlobal",
129
+ "windsurfGlobal",
130
+ ]);
131
+
108
132
  // ── Public API ──────────────────────────────────────────────────────────
109
133
 
110
134
  /**
@@ -165,8 +189,64 @@ export function resolvePlacementDir(target, skillName) {
165
189
  return claudeSkillsProject(skillName);
166
190
  case "claudeGlobal":
167
191
  return claudeSkillsGlobal(skillName);
168
- case "projectFallback":
169
- return projectSkillsFallback(skillName);
192
+ case "agentsProject":
193
+ return agentsSkillsProject(skillName);
194
+ case "agentsGlobal":
195
+ return agentsSkillsGlobal(skillName);
196
+ case "windsurfGlobal":
197
+ return windsurfSkillsGlobal(skillName);
198
+ default: {
199
+ throw validationError(`Unknown placement target: ${target}`);
200
+ }
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Human-readable path label for a placement target. Used in success
206
+ * messages so the user sees where files actually landed (`.agents/skills/`
207
+ * for cohort vendors, not the misleading `.claude/skills/` previously
208
+ * hardcoded in add/get).
209
+ *
210
+ * @param {PlacementTarget} target
211
+ * @returns {string}
212
+ */
213
+ export function describePlacementTarget(target) {
214
+ switch (target) {
215
+ case "claudeProject":
216
+ return ".claude/skills/";
217
+ case "claudeGlobal":
218
+ return "~/.claude/skills/";
219
+ case "agentsProject":
220
+ return ".agents/skills/";
221
+ case "agentsGlobal":
222
+ return "~/.agents/skills/";
223
+ case "windsurfGlobal":
224
+ return "~/.codeium/windsurf/skills/";
225
+ default:
226
+ return target;
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Map a `PlacementTarget` to its parent root resolver. Used by
232
+ * `cleanupOrphans` to know which directories to scan for `.tmp`/`.old`
233
+ * siblings.
234
+ *
235
+ * @param {PlacementTarget} target
236
+ * @returns {() => string}
237
+ */
238
+ function placementRootFn(target) {
239
+ switch (target) {
240
+ case "claudeProject":
241
+ return claudeSkillsProjectRoot;
242
+ case "claudeGlobal":
243
+ return claudeSkillsGlobalRoot;
244
+ case "agentsProject":
245
+ return agentsSkillsProjectRoot;
246
+ case "agentsGlobal":
247
+ return agentsSkillsGlobalRoot;
248
+ case "windsurfGlobal":
249
+ return windsurfSkillsGlobalRoot;
170
250
  default: {
171
251
  throw validationError(`Unknown placement target: ${target}`);
172
252
  }
@@ -174,46 +254,52 @@ export function resolvePlacementDir(target, skillName) {
174
254
  }
175
255
 
176
256
  /**
177
- * Compute the placement targets for a write based on detected/requested
178
- * vendors and the --global flag.
257
+ * Compute the placement targets for a write based on requested vendors
258
+ * and the --global flag.
179
259
  *
180
- * Rules:
181
- * - --global only ["claudeGlobal"] (other vendors don't have a
182
- * documented global convention; we don't synthesize one)
183
- * - claudeCode in vendors "claudeProject"
184
- * - any non-claudeCode vendor in vendors (cursor, windsurf, vscode)
185
- * → "projectFallback" (single fallback, deduped — multiple
186
- * non-claude vendors share the same fallback)
260
+ * Each vendor's `projectTarget` and `globalTarget` come from
261
+ * `agent-registry.mjs`. Multiple vendors mapping to the same target
262
+ * (e.g. cursor + windsurf both at `agentsProject`) collapse into one
263
+ * write. A vendor with `globalTarget === null` is rejected under
264
+ * `--global` because the vendor has no documented personal scope.
187
265
  *
188
266
  * @param {object} options
189
- * @param {string[]} options.vendors - List of vendor keys (claudeCode, cursor, windsurf, vscode).
190
- * @param {boolean} [options.global] - If true, write to personal/global Claude Code dir only.
267
+ * @param {string[]} options.vendors - List of canonical vendor keys.
268
+ * @param {boolean} [options.global] - If true, use each vendor's globalTarget.
191
269
  * @returns {PlacementTarget[]}
192
270
  */
193
271
  export function placementTargetsFor({ vendors, global = false }) {
194
- if (global) {
195
- return ["claudeGlobal"];
196
- }
197
-
198
272
  if (!Array.isArray(vendors) || vendors.length === 0) {
199
273
  throw validationError(
200
- "No vendors specified. Pass --ide claude (or another vendor) explicitly.",
274
+ "No vendors specified. Pass --agent claude (or another target) explicitly.",
201
275
  );
202
276
  }
203
277
 
204
278
  const targets = [];
205
- let needsFallback = false;
206
279
  for (const vendor of vendors) {
207
- if (vendor === "claudeCode") {
208
- targets.push("claudeProject");
209
- } else if (vendor === "cursor" || vendor === "windsurf" || vendor === "vscode") {
210
- needsFallback = true;
211
- } else {
212
- throw validationError(`Unknown vendor: ${vendor}`);
280
+ const entry = getAgentByKey(vendor);
281
+ if (!entry) {
282
+ throw validationError(`Unknown vendor: ${vendor}`, {
283
+ hint: `Valid keys: ${AGENT_REGISTRY.map((e) => e.key).join(", ")}.`,
284
+ });
285
+ }
286
+ const target = global ? entry.globalTarget : entry.projectTarget;
287
+ if (target === null) {
288
+ const supported = AGENT_REGISTRY
289
+ .filter((e) => e.globalTarget !== null)
290
+ .map((e) => e.key)
291
+ .join(", ");
292
+ throw validationError(
293
+ `${vendor} has no documented personal scope, so --global is not supported.`,
294
+ {
295
+ hint:
296
+ `Drop --global, or use --agent with a vendor that supports it (${supported}).`,
297
+ },
298
+ );
299
+ }
300
+ if (!targets.includes(target)) {
301
+ targets.push(target);
213
302
  }
214
- }
215
- if (needsFallback) {
216
- targets.push("projectFallback");
217
303
  }
218
304
  return targets;
219
305
  }
@@ -264,7 +350,7 @@ export function readFrontmatterName(files) {
264
350
  * skip-if-unchanged enhancement at this layer would duplicate that work
265
351
  * and require re-reading every file from disk.
266
352
  *
267
- * Pre-flight: `.gitignore` setup for the projectFallback target runs
353
+ * Pre-flight: `.gitignore` setup for the agentsProject target runs
268
354
  * BEFORE any per-target write. This is intentional — running it inline
269
355
  * inside the loop would let a read-only `.gitignore` failure abort the
270
356
  * loop AFTER an earlier vendor (e.g., claudeCode) had already
@@ -277,14 +363,15 @@ export function writeSkillDir(skill, options = {}) {
277
363
 
278
364
  const targets = placementTargetsFor(options);
279
365
 
280
- // Pre-flight: if any target requires the project /skills/ fallback,
281
- // ensure .gitignore is set up before starting any writes. A failure
282
- // here is recoverable (user fixes their .gitignore and re-runs)
283
- // because nothing has hit disk yet. A failure here AFTER a successful
284
- // claudeProject write would leave the user with a half-applied state
285
- // and an error message that doesn't reflect what's actually on disk.
286
- if (targets.includes("projectFallback")) {
287
- ensureFallbackGitignore();
366
+ // Pre-flight: if any target writes to .agents/skills/, ensure
367
+ // .gitignore is set up before starting any writes. A failure here
368
+ // is recoverable (user fixes their .gitignore and re-runs) because
369
+ // nothing has hit disk yet. A failure here AFTER a successful
370
+ // claudeProject write would leave the user with a half-applied
371
+ // state and an error message that doesn't reflect what's actually
372
+ // on disk.
373
+ if (targets.includes("agentsProject")) {
374
+ ensureAgentsGitignore();
288
375
  }
289
376
 
290
377
  const written = [];
@@ -377,26 +464,26 @@ export function removeSkillDir(skillName, options = {}) {
377
464
  * @returns {{ cleaned: string[] }}
378
465
  */
379
466
  export function cleanupOrphans(options = {}) {
380
- const roots = [];
381
- if (options.global) {
382
- roots.push(claudeSkillsGlobalRoot());
467
+ const roots = new Set();
468
+ if (Array.isArray(options.vendors) && options.vendors.length > 0) {
469
+ const targets = placementTargetsFor({
470
+ vendors: options.vendors,
471
+ global: !!options.global,
472
+ });
473
+ for (const target of targets) {
474
+ roots.add(placementRootFn(target)());
475
+ }
476
+ } else if (options.global) {
477
+ // No vendors under --global — sweep every known global root so a
478
+ // stale orphan from any prior --global write is cleaned.
479
+ for (const target of GLOBAL_TARGETS) {
480
+ roots.add(placementRootFn(target)());
481
+ }
383
482
  } else {
384
- if (Array.isArray(options.vendors) && options.vendors.length > 0) {
385
- if (options.vendors.includes("claudeCode")) {
386
- roots.push(claudeSkillsProjectRoot());
387
- }
388
- if (
389
- options.vendors.includes("cursor") ||
390
- options.vendors.includes("windsurf") ||
391
- options.vendors.includes("vscode")
392
- ) {
393
- roots.push(projectSkillsFallbackRoot());
394
- }
395
- } else {
396
- // No vendors specified — clean everything we know about.
397
- roots.push(claudeSkillsProjectRoot());
398
- roots.push(projectSkillsFallbackRoot());
399
- roots.push(claudeSkillsGlobalRoot());
483
+ // No vendors and no --global sweep every root we know about so
484
+ // any orphan from any prior run is cleaned.
485
+ for (const target of ALL_TARGETS) {
486
+ roots.add(placementRootFn(target)());
400
487
  }
401
488
  }
402
489
 
@@ -666,17 +753,14 @@ function writeSkillToDir(skill, targetDir) {
666
753
  }
667
754
 
668
755
  /**
669
- * Ensure the project /skills/ directory is gitignored. Idempotent —
670
- * skips if the entry is already present. Creates .gitignore if missing.
756
+ * Ensure `.agents/skills/` is gitignored. Idempotent — skips if the
757
+ * entry is already present. Creates .gitignore if missing.
671
758
  *
672
759
  * Throws diskError on any write failure so a read-only or symlinked
673
760
  * .gitignore surfaces as a user-visible error instead of silent data
674
- * loss. This is the explicit fix for the architect's PR1 review item:
675
- * `writeFileSafe` was previously called without a try/catch and a
676
- * failing write would leave the user with skills on disk but no
677
- * .gitignore protection — the next `git add` would commit them.
761
+ * loss.
678
762
  */
679
- function ensureFallbackGitignore() {
763
+ function ensureAgentsGitignore() {
680
764
  const filePath = gitignorePath();
681
765
  let existing = null;
682
766
  try {
@@ -684,18 +768,19 @@ function ensureFallbackGitignore() {
684
768
  } catch (err) {
685
769
  throw diskError(`Cannot read ${filePath}: ${err.message}`, {
686
770
  cause: err,
687
- hint: "Update your .gitignore manually to add /skills/ before re-running.",
771
+ hint:
772
+ `Update your .gitignore manually to add ${GITIGNORE_AGENTS_SKILLS_LINE} before re-running.`,
688
773
  });
689
774
  }
690
775
 
691
- if (existing !== null && existing.includes(GITIGNORE_SKILLS_LINE)) {
776
+ if (existing !== null && existing.includes(GITIGNORE_AGENTS_SKILLS_LINE)) {
692
777
  return; // Already present — no-op
693
778
  }
694
779
 
695
780
  const newContent =
696
781
  existing === null
697
- ? `${GITIGNORE_SKILLS_HEADER}\n${GITIGNORE_SKILLS_LINE}\n`
698
- : `${existing}${existing.endsWith("\n") ? "" : "\n"}\n${GITIGNORE_SKILLS_HEADER}\n${GITIGNORE_SKILLS_LINE}\n`;
782
+ ? `${GITIGNORE_SKILLS_HEADER}\n${GITIGNORE_AGENTS_SKILLS_LINE}\n`
783
+ : `${existing}${existing.endsWith("\n") ? "" : "\n"}\n${GITIGNORE_SKILLS_HEADER}\n${GITIGNORE_AGENTS_SKILLS_LINE}\n`;
699
784
 
700
785
  try {
701
786
  writeFileSafe(filePath, newContent);
@@ -703,7 +788,7 @@ function ensureFallbackGitignore() {
703
788
  throw diskError(`Cannot update ${filePath}: ${err.message}`, {
704
789
  cause: err,
705
790
  hint:
706
- "The CLI needs to add /skills/ to .gitignore so library skills don't get committed. " +
791
+ `The CLI needs to add ${GITIGNORE_AGENTS_SKILLS_LINE} to .gitignore so library skills don't get committed. ` +
707
792
  "Update .gitignore manually and re-run, or remove the read-only/symlinked constraint.",
708
793
  });
709
794
  }
@@ -87,7 +87,22 @@ export function writeFileAtomic(filePath, content, { mode } = {}) {
87
87
  throw new Error(`${filePath} is a directory, expected a file`);
88
88
  }
89
89
 
90
- const tmpPath = `${filePath}.tmp`;
90
+ // Per-process unique temp suffix prevents two concurrent writers
91
+ // (e.g. a SessionStart hook firing `skillrepo update --silent` while
92
+ // the user runs `skillrepo uninstall --global`) from writing to the
93
+ // same `.tmp` path and producing non-deterministic final content.
94
+ // The cohort SessionStart hooks (#1240) made this race observable
95
+ // in production: hook configs at user-scope can be touched by both
96
+ // the hook runner and the uninstaller simultaneously.
97
+ //
98
+ // `process.pid` plus a `Math.random()` token gives uniqueness across
99
+ // processes AND across rapid same-process re-entrant writes. The
100
+ // token is base-36 chars from a 64-bit float — collision probability
101
+ // between siblings within the rename window is astronomically low.
102
+ // No cryptographic strength required: this is a temp-file suffix,
103
+ // not a security boundary.
104
+ const uniqueSuffix = `${process.pid}.${Math.random().toString(36).slice(2, 10)}`;
105
+ const tmpPath = `${filePath}.${uniqueSuffix}.tmp`;
91
106
  try {
92
107
  writeFileSync(tmpPath, content, "utf-8");
93
108
  } catch (err) {
@@ -46,6 +46,7 @@ import { mergeWindsurfMcpConfig } from "./mergers/windsurf-mcp.mjs";
46
46
  import { mergeVscodeMcpConfig } from "./mergers/vscode-mcp.mjs";
47
47
  import { confirm as realConfirm } from "./prompt.mjs";
48
48
  import { validationError } from "./errors.mjs";
49
+ import { getAgentByKey } from "./agent-registry.mjs";
49
50
 
50
51
  /**
51
52
  * @typedef {Object} McpMergeResult
@@ -69,7 +70,7 @@ import { validationError } from "./errors.mjs";
69
70
  * user for each one unless `yes` is true.
70
71
  *
71
72
  * @param {object} options
72
- * @param {string[]} options.vendors - Vendor keys: claudeCode | cursor | windsurf | vscode
73
+ * @param {string[]} options.vendors - Canonical vendor keys (see agent-registry.mjs).
73
74
  * @param {string} options.mcpUrl - The SkillRepo MCP endpoint URL
74
75
  * @param {boolean} [options.yes] - Skip prompts (auto-accept everything)
75
76
  * @param {object} [options.io] - Injected streams for testability
@@ -111,18 +112,26 @@ export async function mergeMcpForVendors(options) {
111
112
  }
112
113
 
113
114
  // Deduplicate vendors so a caller passing `["claudeCode",
114
- // "claudeCode"]` doesn't run the merger twice. The round-1 review
115
- // flagged this as a latent caller trap — `cli-config.mjs`'s
116
- // parseVendorList doesn't dedupe either, so `--ide claude,claude`
117
- // would reach this loop twice for the same vendor. Idempotent on
118
- // a single vendor key (Set preserves first-seen insertion order).
115
+ // "claudeCode"]` doesn't run the merger twice. Defense-in-depth
116
+ // `cli-config.mjs`'s `parseAgentList` already dedupes, but a future
117
+ // caller building the vendor list manually could reintroduce the
118
+ // duplicate. Idempotent on a single vendor key (Set preserves
119
+ // first-seen insertion order).
119
120
  const uniqueVendors = Array.from(new Set(vendors));
120
121
 
121
122
  const results = [];
122
123
  for (const vendor of uniqueVendors) {
124
+ const entry = getAgentByKey(vendor);
125
+ // Vendors with `hasMcp: false` are file-only (no documented MCP
126
+ // config the CLI knows how to merge). Skipping silently is correct
127
+ // — they're a deliberate registry classification, not user error.
128
+ // A truly unknown vendor (no registry entry at all) is a caller
129
+ // bug; surface that as a "failed" result so it can't hide.
130
+ if (entry && entry.hasMcp === false) {
131
+ continue;
132
+ }
123
133
  const vendorInfo = VENDOR_INFO[vendor];
124
134
  if (!vendorInfo) {
125
- // Unknown vendor — skip with a recorded failure
126
135
  results.push({
127
136
  vendor,
128
137
  path: "(unknown)",
@@ -183,34 +192,6 @@ export async function mergeMcpForVendors(options) {
183
192
  return results;
184
193
  }
185
194
 
186
- /**
187
- * Print MCP config JSON for undetected IDEs so the user can paste
188
- * it into their own IDE's MCP settings manually. Called by init
189
- * when no IDEs were detected or when the user wants to see the
190
- * raw config shape.
191
- *
192
- * @param {string} mcpUrl
193
- * @param {object} [io]
194
- * @param {NodeJS.WritableStream} [io.stdout=process.stdout]
195
- */
196
- export function printManualMcpInstructions(mcpUrl, io = {}) {
197
- const stdout = io.stdout ?? process.stdout;
198
- const config = {
199
- mcpServers: {
200
- skillrepo: {
201
- type: "http",
202
- url: mcpUrl,
203
- headers: {
204
- Authorization: "Bearer ${SKILLREPO_ACCESS_KEY}",
205
- },
206
- },
207
- },
208
- };
209
- stdout.write("\n To configure MCP manually, add this to your IDE's MCP settings:\n\n");
210
- stdout.write(JSON.stringify(config, null, 2).replace(/^/gm, " ") + "\n\n");
211
- stdout.write(" The SKILLREPO_ACCESS_KEY env var is set in .env.local.\n\n");
212
- }
213
-
214
195
  // ── Internals ──────────────────────────────────────────────────────────
215
196
 
216
197
  /**
@@ -233,7 +214,7 @@ const VENDOR_INFO = {
233
214
  pathFn: windsurfMcpJson,
234
215
  displayPath: "~/.codeium/windsurf/mcp_config.json",
235
216
  },
236
- vscode: {
217
+ copilot: {
237
218
  merger: mergeVscodeMcpConfig,
238
219
  pathFn: vscodeMcpJson,
239
220
  displayPath: ".vscode/mcp.json",