skillrepo 3.2.0 → 4.0.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 (39) hide show
  1. package/README.md +90 -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-session-sync.mjs +1 -1
  7. package/src/commands/init.mjs +435 -111
  8. package/src/commands/list.mjs +1 -1
  9. package/src/commands/remove.mjs +10 -2
  10. package/src/commands/uninstall.mjs +1 -1
  11. package/src/commands/update.mjs +15 -3
  12. package/src/lib/agent-registry.mjs +215 -0
  13. package/src/lib/cli-config.mjs +146 -44
  14. package/src/lib/detect-agents.mjs +112 -0
  15. package/src/lib/file-write.mjs +162 -77
  16. package/src/lib/mcp-merge.mjs +17 -36
  17. package/src/lib/mergers/gitignore.mjs +55 -28
  18. package/src/lib/paths.mjs +27 -25
  19. package/src/lib/prompt-multiselect.mjs +324 -0
  20. package/src/lib/sync.mjs +18 -19
  21. package/src/test/commands/add.test.mjs +18 -3
  22. package/src/test/commands/init-picker.test.mjs +144 -0
  23. package/src/test/commands/init.test.mjs +228 -42
  24. package/src/test/commands/remove.test.mjs +4 -1
  25. package/src/test/commands/update.test.mjs +13 -3
  26. package/src/test/e2e/cli-agent-permutations.test.mjs +631 -0
  27. package/src/test/e2e/cli-commands.test.mjs +39 -13
  28. package/src/test/integration/file-write.integration.test.mjs +31 -10
  29. package/src/test/lib/agent-registry.test.mjs +215 -0
  30. package/src/test/lib/cli-config.test.mjs +222 -38
  31. package/src/test/lib/detect-agents.test.mjs +336 -0
  32. package/src/test/lib/file-write-placement.test.mjs +264 -0
  33. package/src/test/lib/file-write.test.mjs +231 -30
  34. package/src/test/lib/mcp-merge.test.mjs +23 -15
  35. package/src/test/lib/paths.test.mjs +53 -17
  36. package/src/test/lib/prompt-multiselect.test.mjs +448 -0
  37. package/src/test/lib/sync.test.mjs +157 -0
  38. package/src/lib/detect-ides.mjs +0 -44
  39. package/src/test/detect-ides.test.mjs +0 -65
package/README.md CHANGED
@@ -6,8 +6,8 @@ A pull-based CLI for managing your agent-skills library from the terminal.
6
6
  npx skillrepo init
7
7
  ```
8
8
 
9
- Validates your access key, auto-detects your IDEs, wires up the MCP config,
10
- and pulls your library. Safe to re-run at any time.
9
+ Validates your access key, picks which agents to configure, wires up the
10
+ MCP config, and pulls your library. Safe to re-run at any time.
11
11
 
12
12
  ## Install
13
13
 
@@ -34,8 +34,10 @@ Requires Node.js 18 or later.
34
34
  the terminal. The page works headless too — copy the key from any
35
35
  browser if your terminal can't auto-launch one.
36
36
  2. **Your library is now on disk.** The CLI wrote skills into
37
- `.claude/skills/` (project) and registered the MCP server with every IDE
38
- it detected (`.claude/`, `.cursor/`, `.vscode/`, `~/.codeium/windsurf/`).
37
+ `.claude/skills/` (for Claude Code) and `.agents/skills/` (the
38
+ shared path for Cursor, Windsurf, Gemini CLI, Codex CLI, Cline,
39
+ VS Code + Copilot, and others). Both paths are added to
40
+ `.gitignore` — skills are a per-developer cache, not committed.
39
41
  3. **From now on**, use `skillrepo update` to pull new versions,
40
42
  `skillrepo add` to add skills to your library, and `skillrepo list` to
41
43
  see what you have.
@@ -49,12 +51,30 @@ Requires Node.js 18 or later.
49
51
  ### `init` — first-run setup
50
52
 
51
53
  ```sh
52
- skillrepo init [--key <key>] [--url <url>] [--yes] [--force] [--ide <list>] [--global] [--json] [--no-session-sync]
54
+ skillrepo init [--key <key>] [--url <url>] [--yes] [--force] [--agent <list>] [--global] [--json] [--no-session-sync]
53
55
  ```
54
56
 
55
- Validates your access key, detects installed IDEs, writes the MCP config,
56
- installs the Claude Code SessionStart hook (opt-in — see `session-sync`
57
- below), and runs the first library sync.
57
+ Validates your access key, picks which targets to configure (interactive
58
+ two-row picker by default — Claude Code at `.claude/skills/` and the
59
+ shared `.agents/skills/` cohort path for everything else), writes the
60
+ MCP config, installs the Claude Code SessionStart hook (opt-in — see
61
+ `session-sync` below), and runs the first library sync.
62
+
63
+ Detection drives the picker's pre-checked rows. Three signal types per
64
+ agent are probed:
65
+ - **Active session** env vars (e.g., `CLAUDECODE`, `CURSOR_AGENT`,
66
+ `GEMINI_CLI`, `CLINE_ACTIVE`) — strongest signal, indicates the
67
+ agent is currently running.
68
+ - **HOME traces** (e.g., `~/.claude/`, `~/.cursor/`, `~/.codeium/windsurf/`)
69
+ — installed-on-this-machine signal.
70
+ - **Project dotfiles** (e.g., `.claude/`, `.cursor/`, `.github/skills/`)
71
+ — configured-for-this-repo signal.
72
+
73
+ When any signal fires for either target, that row pre-checks. Fresh
74
+ clone with no detection: both rows still pre-checked — the product's
75
+ job is to set up skills, defaulting to "do nothing" because env vars
76
+ didn't fire is wrong. Pick the third row ("None") to skip placement
77
+ and just write the config + gitignore.
58
78
 
59
79
  | Flag | Description |
60
80
  |------|-------------|
@@ -62,10 +82,40 @@ below), and runs the first library sync.
62
82
  | `--url, -u <url>` | Server URL. Defaults to `https://skillrepo.dev`. Use for self-hosted. |
63
83
  | `--yes, -y` | Non-interactive. Skip all confirmation prompts. Required for CI. Installs the session-sync hook by default — pass `--no-session-sync` to opt out. Under `npx`, this also auto-runs `npm install -g skillrepo@<this-version>` if no global install is present (so the session-sync hook can use the resulting absolute path). |
64
84
  | `--force` | Re-prompt for a new key even if `~/.claude/skillrepo/config.json` is valid. |
65
- | `--ide <list>` | Comma-separated vendor override. One or more of `claude`, `cursor`, `windsurf`, `vscode`, or `all`. |
85
+ | `--agent <list>` | Comma-separated agent target override. Canonical tokens: `claude` (Claude Code), `agents` (every other supported vendor — Cursor, Windsurf, Gemini CLI, Codex CLI, Cline, VS Code + Copilot — sharing the `.agents/skills/` cohort path), `none` (skip placement entirely; init still writes config + gitignore). Vendor names like `cursor` or `claude-code` are accepted as silent aliases. |
66
86
  | `--global` | Write skills to `~/.claude/skills/` (personal) instead of `.claude/skills/` (project). |
67
87
  | `--no-session-sync` | Skip step 6 (SessionStart hook install). Works in both interactive and `--yes` modes. Use for CI scripts that bootstrap a project without ever starting a Claude Code session. |
68
- | `--json` | Emit a structured JSON summary on success. Includes a `sessionSync: { action, path }` block describing whether the hook was installed. |
88
+ | `--json` | Emit a structured JSON summary on success. Requires `--yes`. See JSON output shape below. |
89
+
90
+ #### JSON output shape (`init --json --yes`)
91
+
92
+ ```json
93
+ {
94
+ "action": "initialized",
95
+ "account": { "slug": "...", "id": "...", "tier": "free|pro|enterprise" },
96
+ "config": { "action": "created|updated|unchanged" },
97
+ "vendors": ["claudeCode", "cursor", "..."],
98
+ "mcp": {
99
+ "merged": [".mcp.json", ".cursor/mcp.json"],
100
+ "skipped": ["..."],
101
+ "failed": [{ "path": "...", "reason": "..." }]
102
+ },
103
+ "sessionSync": { "action": "installed|unchanged|skipped|failed|not-applicable", "path": "..." },
104
+ "sync": {
105
+ "added": 0,
106
+ "updated": 0,
107
+ "removed": 0,
108
+ "notModified": false,
109
+ "fullSync": true,
110
+ "syncedAt": "2026-05-01T00:00:00.000Z"
111
+ }
112
+ }
113
+ ```
114
+
115
+ Field notes:
116
+ - `vendors` is the resolved canonical-key list, NOT the raw `--agent` input. `--agent agents` produces every cohort vendor (cursor, windsurf, gemini, codex, cline, copilot); `--agent none` produces an empty array.
117
+ - `sync.fullSync` is `true` for first-time syncs (no prior `.last-sync`), `false` for delta syncs, and `null` only on synthesized-failure summaries when the network call never completed (also signals via `sync.failureReason`).
118
+ - `sync.failureReason: string` is present only when the first sync failed but the rest of init succeeded — config is still saved; user should re-run `skillrepo update`.
69
119
 
70
120
  `init` is idempotent: re-running with a valid existing config re-runs
71
121
  detection + MCP merge + first sync without re-prompting for a key. If the
@@ -77,14 +127,17 @@ fresh key without leaving the terminal.
77
127
  When stdin is not a TTY (CI, piped input), `init` skips the browser launch
78
128
  and just prints the URL — paste the key via the upstream pipe.
79
129
 
80
- **Headless / CI:** if you run from a directory with no IDE markers, init
81
- will refuse and print a copy-pasteable MCP config for manual wiring. To
82
- proceed anyway, pass `--ide claude` (or the target vendor).
130
+ **Headless / non-interactive:** under `--yes` the picker is skipped and
131
+ the pre-checked rows become the selection. With no detection signals,
132
+ both default rows are pre-checked — running `init --yes` on a fresh
133
+ clone configures both targets so CI bootstrap scripts always produce
134
+ a working library. Pass `--agent <target>` (`claude`, `agents`, `none`,
135
+ or a vendor name like `cursor`) to bypass the picker entirely.
83
136
 
84
137
  ### `update` — sync your library
85
138
 
86
139
  ```sh
87
- skillrepo update [--global] [--ide <list>] [--json]
140
+ skillrepo update [--global] [--agent <list>] [--json]
88
141
  ```
89
142
 
90
143
  Pulls the latest state of your library from the server using a delta
@@ -96,7 +149,7 @@ changed.
96
149
  ### `get` — fetch a single skill
97
150
 
98
151
  ```sh
99
- skillrepo get <@owner/name> [--global] [--ide <list>] [--json]
152
+ skillrepo get <@owner/name> [--global] [--agent <list>] [--json]
100
153
  ```
101
154
 
102
155
  One-shot fetch. Does NOT mutate your library or the server — just reads
@@ -125,7 +178,7 @@ but currently a no-op — semantic search is a planned backend feature.
125
178
  ### `add` — add a skill to your library
126
179
 
127
180
  ```sh
128
- skillrepo add <@owner/name> [--global] [--ide <list>] [--json]
181
+ skillrepo add <@owner/name> [--global] [--agent <list>] [--json]
129
182
  ```
130
183
 
131
184
  POSTs to `/api/v1/library`, then fetches the single skill directly and
@@ -136,7 +189,7 @@ consistent.
136
189
  ### `remove` — remove a skill from your library
137
190
 
138
191
  ```sh
139
- skillrepo remove <@owner/name> [--global] [--ide <list>] [--json]
192
+ skillrepo remove <@owner/name> [--global] [--agent <list>] [--json]
140
193
  ```
141
194
 
142
195
  DELETEs from `/api/v1/library` and deletes the local directory. Requires
@@ -275,11 +328,20 @@ Two scenarios worth calling out:
275
328
 
276
329
  ### Skill placement
277
330
 
278
- By default, skills land at `.claude/skills/<name>/` (project-scoped). Pass
279
- `--global` to target `~/.claude/skills/<name>/` instead. When the CLI
280
- detects an IDE that has no documented skills convention (Cursor, Windsurf,
281
- VS Code), it falls back to project `/skills/<name>/` and adds `/skills/`
282
- to your `.gitignore` on first write.
331
+ Skills land at one of two project paths, depending on the agent:
332
+
333
+ | Agent | Project path | Personal (`--global`) path |
334
+ |---|---|---|
335
+ | Claude Code | `.claude/skills/<name>/` | `~/.claude/skills/<name>/` |
336
+ | Cursor / Windsurf / Gemini CLI / Codex CLI / Cline / GitHub Copilot | `.agents/skills/<name>/` | `~/.agents/skills/<name>/` |
337
+ | Windsurf (personal-scope override) | — | `~/.codeium/windsurf/skills/<name>/` |
338
+
339
+ The CLI auto-adds `.claude/skills/` and `.agents/skills/` to your project
340
+ `.gitignore` on first write — skills are a per-developer cache, not
341
+ committed content. The `--agent` flag overrides detection (e.g.
342
+ `--agent claude,agents` writes both paths). See
343
+ [`docs/vendor-paths.md`](docs/vendor-paths.md) for primary-source
344
+ citations on each agent's read paths.
283
345
 
284
346
  ## Exit codes
285
347
 
@@ -342,11 +404,12 @@ path — both the interactive prompt in `init` and the Bearer header
342
404
  used by every subsequent command — so pasting from email or a
343
405
  browser with a trailing newline is safe.
344
406
 
345
- **"No IDEs detected in this directory"** The CLI detected no
346
- `.claude/`, `.cursor/`, `.vscode/`, or `~/.codeium/windsurf/` markers.
347
- Either run from inside a project with one of those folders, pass
348
- `--ide <vendor>` to target a specific IDE, or copy the printed MCP
349
- config manually.
407
+ **Picker selected nothing under `--yes`**Phase 3 (#1236) replaced
408
+ the old "no agents detected → refuse" branch with a friendlier
409
+ default: when no detection signals fire, both default rows are
410
+ pre-checked, so `--yes` configures both targets. If you want to skip
411
+ placement entirely under `--yes`, pass `--agent none` (config and
412
+ gitignore still happen; only the skill files are skipped).
350
413
 
351
414
  **"Rate limit exceeded — retried automatically and still getting
352
415
  429"** — The CLI already retried with backoff. Wait a minute and
package/bin/skillrepo.mjs CHANGED
@@ -44,27 +44,27 @@ import { CliError, EXIT_OK, EXIT_VALIDATION } from "../src/lib/errors.mjs";
44
44
  const COMMANDS = {
45
45
  init: {
46
46
  description: "Validate access key, configure detected IDEs, and run first sync",
47
- usage: "skillrepo init [--key <key>] [--url <url>] [--yes]",
47
+ usage: "skillrepo init [--key <key>] [--url <url>] [--yes] [--agent <list>] [--global] [--force] [--no-session-sync] [--json]",
48
48
  run: async (argv) => runInit(argv),
49
49
  },
50
50
  update: {
51
51
  description: "Sync your library against the registry (delta + tombstones)",
52
- usage: "skillrepo update [--global] [--ide <list>] [--json]",
52
+ usage: "skillrepo update [--global] [--agent <list>] [--json]",
53
53
  run: async (argv) => runUpdate(argv),
54
54
  },
55
55
  get: {
56
56
  description: "Fetch a single skill and write it to disk (no library mutation)",
57
- usage: "skillrepo get <@owner/name> [--global] [--ide <list>] [--json]",
57
+ usage: "skillrepo get <@owner/name> [--global] [--agent <list>] [--json]",
58
58
  run: async (argv) => runGet(argv),
59
59
  },
60
60
  add: {
61
61
  description: "Add a skill to your library and pull it locally",
62
- usage: "skillrepo add <@owner/name> [--global] [--ide <list>] [--json]",
62
+ usage: "skillrepo add <@owner/name> [--global] [--agent <list>] [--json]",
63
63
  run: async (argv) => runAdd(argv),
64
64
  },
65
65
  remove: {
66
66
  description: "Remove a skill from your library and delete it locally",
67
- usage: "skillrepo remove <@owner/name> [--global] [--ide <list>] [--json]",
67
+ usage: "skillrepo remove <@owner/name> [--global] [--agent <list>] [--json]",
68
68
  run: async (argv) => runRemove(argv),
69
69
  },
70
70
  list: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillrepo",
3
- "version": "3.2.0",
3
+ "version": "4.0.0",
4
4
  "description": "Pull-based CLI for agent skills — init, sync, search, add, remove your library from any IDE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,13 +30,22 @@
30
30
  * - 403 plan-limit → validationError (via http.mjs) with billing hint
31
31
  * - 403 scope → scopeError (via http.mjs) with write-key hint
32
32
  *
33
- * Flags: --global / --ide / --json / --key / --url
33
+ * Flags: --global / --agent / --json / --key / --url
34
34
  * Positional: <@owner/name>
35
35
  */
36
36
 
37
37
  import { addSkillToLibrary, getSkill } from "../lib/http.mjs";
38
- import { writeSkillDir, cleanupOrphans } from "../lib/file-write.mjs";
39
- import { resolveFlags, effectiveVendors } from "../lib/cli-config.mjs";
38
+ import {
39
+ writeSkillDir,
40
+ cleanupOrphans,
41
+ placementTargetsFor,
42
+ describePlacementTarget,
43
+ } from "../lib/file-write.mjs";
44
+ import {
45
+ resolveFlags,
46
+ effectiveVendors,
47
+ requireVendorTargets,
48
+ } from "../lib/cli-config.mjs";
40
49
  import { parseIdentifier, formatIdentifier } from "../lib/identifier.mjs";
41
50
  import { validationError } from "../lib/errors.mjs";
42
51
 
@@ -73,6 +82,11 @@ export async function runAdd(argv, io = {}) {
73
82
 
74
83
  const { owner, name } = parseIdentifier(identifier);
75
84
  const vendors = effectiveVendors(flags);
85
+ // `add` writes specific files to disk — `--agent none` would make
86
+ // the command a documented no-op, which is never a useful intent.
87
+ // Reject early with an actionable hint pointing at the meaningful
88
+ // targets.
89
+ requireVendorTargets(vendors, "add");
76
90
 
77
91
  // Pre-flight: clean orphans from prior crashes (same pattern as get.mjs)
78
92
  cleanupOrphans({ vendors, global: flags.global });
@@ -161,9 +175,10 @@ export async function runAdd(argv, io = {}) {
161
175
  return;
162
176
  }
163
177
 
164
- const where = flags.global
165
- ? "personal (~/.claude/skills/)"
166
- : "project (.claude/skills/)";
178
+ const targets = placementTargetsFor({ vendors, global: flags.global });
179
+ const where = `${flags.global ? "personal" : "project"} (${targets
180
+ .map(describePlacementTarget)
181
+ .join(", ")})`;
167
182
  if (wasNewlyAdded) {
168
183
  stdout.write(
169
184
  `\n ✓ Added ${formatIdentifier({ owner, name })} to your library (${skill.files.length} files) → ${where}\n\n`,
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * Flags (via resolveFlags):
11
11
  * --global Write to ~/.claude/skills/ instead of project-local
12
- * --ide <list> Comma-separated vendor list
12
+ * --agent <list> Comma-separated agent target list
13
13
  * --key/--url Override credentials
14
14
  *
15
15
  * Positional:
@@ -25,8 +25,17 @@
25
25
  */
26
26
 
27
27
  import { getSkill } from "../lib/http.mjs";
28
- import { writeSkillDir, cleanupOrphans } from "../lib/file-write.mjs";
29
- import { resolveFlags, effectiveVendors } from "../lib/cli-config.mjs";
28
+ import {
29
+ writeSkillDir,
30
+ cleanupOrphans,
31
+ placementTargetsFor,
32
+ describePlacementTarget,
33
+ } from "../lib/file-write.mjs";
34
+ import {
35
+ resolveFlags,
36
+ effectiveVendors,
37
+ requireVendorTargets,
38
+ } from "../lib/cli-config.mjs";
30
39
  import { parseIdentifier, formatIdentifier } from "../lib/identifier.mjs";
31
40
  import { validationError } from "../lib/errors.mjs";
32
41
 
@@ -64,6 +73,10 @@ export async function runGet(argv, io = {}) {
64
73
 
65
74
  const { owner, name } = parseIdentifier(identifier);
66
75
  const vendors = effectiveVendors(flags);
76
+ // `get` writes a specific skill to disk — `--agent none` would
77
+ // make the command a no-op, which is never a useful intent. Reject
78
+ // early.
79
+ requireVendorTargets(vendors, "get");
67
80
 
68
81
  // Pre-flight: clean orphans from prior crashes before any new write
69
82
  cleanupOrphans({ vendors, global: flags.global });
@@ -109,7 +122,10 @@ export async function runGet(argv, io = {}) {
109
122
  return;
110
123
  }
111
124
 
112
- const where = flags.global ? "personal (~/.claude/skills/)" : "project (.claude/skills/)";
125
+ const targets = placementTargetsFor({ vendors, global: flags.global });
126
+ const where = `${flags.global ? "personal" : "project"} (${targets
127
+ .map(describePlacementTarget)
128
+ .join(", ")})`;
113
129
  stdout.write(
114
130
  `\n ✓ Fetched ${formatIdentifier({ owner, name })} (${skill.files.length} files) → ${where}\n\n`,
115
131
  );
@@ -73,7 +73,7 @@ import { SessionSyncAction, isHookActive } from "./session-sync-actions.mjs";
73
73
  * @typedef {Object} SessionSyncOptions
74
74
  * @property {boolean} noSessionSync - True if `--no-session-sync`.
75
75
  * @property {boolean} claudeTargeted - True if Claude Code is in the
76
- * vendor target list (via `--ide claude` or `--global` or
76
+ * vendor target list (via `--agent claude` or `--global` or
77
77
  * auto-detection).
78
78
  * @property {boolean} yes - True if `--yes`.
79
79
  * @property {boolean} json - True if `--json`. Affects spawn output