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
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,53 @@ 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": {
104
+ "action": "installed|unchanged|skipped|failed|not-applicable",
105
+ "path": "...",
106
+ "cohortHooks": [
107
+ {
108
+ "vendorKey": "cursor|gemini|codex|copilot",
109
+ "displayName": "...",
110
+ "path": "~/.cursor/hooks.json",
111
+ "action": "installed|updated|unchanged|failed",
112
+ "reason": "..."
113
+ }
114
+ ]
115
+ },
116
+ "sync": {
117
+ "added": 0,
118
+ "updated": 0,
119
+ "removed": 0,
120
+ "notModified": false,
121
+ "fullSync": true,
122
+ "syncedAt": "2026-05-01T00:00:00.000Z"
123
+ }
124
+ }
125
+ ```
126
+
127
+ Field notes:
128
+ - `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.
129
+ - `sessionSync.cohortHooks[]` reports per-vendor outcomes for the auto-refresh hooks installed alongside the Claude Code SessionStart hook (one entry per cohort vendor with a non-null `agentHook` registry spec — Cursor, Gemini CLI, Codex CLI, VS Code + Copilot). `reason` is present only when `action: "failed"`. Empty array when `--no-session-sync` was passed or no cohort vendor was selected.
130
+ - `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`).
131
+ - `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
132
 
70
133
  `init` is idempotent: re-running with a valid existing config re-runs
71
134
  detection + MCP merge + first sync without re-prompting for a key. If the
@@ -77,14 +140,17 @@ fresh key without leaving the terminal.
77
140
  When stdin is not a TTY (CI, piped input), `init` skips the browser launch
78
141
  and just prints the URL — paste the key via the upstream pipe.
79
142
 
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).
143
+ **Headless / non-interactive:** under `--yes` the picker is skipped and
144
+ the pre-checked rows become the selection. With no detection signals,
145
+ both default rows are pre-checked — running `init --yes` on a fresh
146
+ clone configures both targets so CI bootstrap scripts always produce
147
+ a working library. Pass `--agent <target>` (`claude`, `agents`, `none`,
148
+ or a vendor name like `cursor`) to bypass the picker entirely.
83
149
 
84
150
  ### `update` — sync your library
85
151
 
86
152
  ```sh
87
- skillrepo update [--global] [--ide <list>] [--json]
153
+ skillrepo update [--global] [--agent <list>] [--json] [--silent]
88
154
  ```
89
155
 
90
156
  Pulls the latest state of your library from the server using a delta
@@ -93,10 +159,20 @@ from the library, and skips skills that are unchanged. Uses ETag
93
159
  caching so repeat runs return `304 Not Modified` when nothing has
94
160
  changed.
95
161
 
162
+ `--silent` suppresses normal output and writes a single `{}` line to
163
+ stdout on success. Designed for SessionStart hooks that pipe stdout
164
+ to their agent's session log — Gemini CLI specifically requires hook
165
+ stdout to be valid JSON, and the empty object satisfies that without
166
+ injecting model context. Sync progress and warnings still go to
167
+ stderr. On failure, the command exits with the appropriate non-zero
168
+ code and the error message goes to stderr (distinct from
169
+ `--session-hook`, which is Claude-Code-specific and exits 0 on every
170
+ error so a sync failure can't block a session start).
171
+
96
172
  ### `get` — fetch a single skill
97
173
 
98
174
  ```sh
99
- skillrepo get <@owner/name> [--global] [--ide <list>] [--json]
175
+ skillrepo get <@owner/name> [--global] [--agent <list>] [--json]
100
176
  ```
101
177
 
102
178
  One-shot fetch. Does NOT mutate your library or the server — just reads
@@ -125,7 +201,7 @@ but currently a no-op — semantic search is a planned backend feature.
125
201
  ### `add` — add a skill to your library
126
202
 
127
203
  ```sh
128
- skillrepo add <@owner/name> [--global] [--ide <list>] [--json]
204
+ skillrepo add <@owner/name> [--global] [--agent <list>] [--json]
129
205
  ```
130
206
 
131
207
  POSTs to `/api/v1/library`, then fetches the single skill directly and
@@ -136,7 +212,7 @@ consistent.
136
212
  ### `remove` — remove a skill from your library
137
213
 
138
214
  ```sh
139
- skillrepo remove <@owner/name> [--global] [--ide <list>] [--json]
215
+ skillrepo remove <@owner/name> [--global] [--agent <list>] [--json]
140
216
  ```
141
217
 
142
218
  DELETEs from `/api/v1/library` and deletes the local directory. Requires
@@ -154,6 +230,23 @@ Installs (or removes) a Claude Code [SessionStart hook](https://docs.claude.com/
154
230
 
155
231
  By default `skillrepo init` prompts you to install this hook. If you said no (or passed `--no-session-sync`), run `session-sync enable` later to turn it on.
156
232
 
233
+ #### Auto-refresh hooks for other agents
234
+
235
+ For Cursor, Gemini CLI, Codex CLI, and VS Code + Copilot, `skillrepo init` writes a SessionStart hook to each agent's user-scope hook config so your library refreshes on every session start without a separate command. Each hook invokes `npx --yes skillrepo update --silent`, so it works without a global `skillrepo` install.
236
+
237
+ | Agent | Hook config path | Notes |
238
+ |-------|------------------|-------|
239
+ | Cursor | `~/.cursor/hooks.json` (`sessionStart` event) | `timeout: 60` (seconds) |
240
+ | Gemini CLI | `~/.gemini/settings.json` (`SessionStart` event) | `matcher: "*"` group filter, `timeout: 60000` (milliseconds), entry named `skillrepo-update` |
241
+ | Codex CLI | `~/.codex/hooks.json` (`SessionStart` event) | `timeout: 60` (seconds). Codex also reads `[hooks]` from `~/.codex/config.toml`; both sources coexist — Codex merges them at runtime, so a hand-written TOML entry won't conflict with our JSON. |
242
+ | VS Code + Copilot | `~/.copilot/hooks/skillrepo-update.json` (`sessionStart` event) | `timeout: 60` (seconds). **Preview status**: GitHub currently labels Copilot's hook system as Preview; the schema may shift before GA. |
243
+
244
+ `skillrepo init` writes these alongside the Claude Code hook for every selected vendor that publishes a SessionStart-equivalent event. `--no-session-sync` skips ALL of them. `skillrepo uninstall --global` removes them surgically — other tools' entries (1Password, Snyk, your own scripts) in those config files are preserved.
245
+
246
+ **Cloud-agent runners** (e.g. GitHub Codespaces, Copilot's cloud agent) read only the committed default-branch content. Because skills sync to a per-developer, gitignored cache, those runners do not see the local skill library — same documented limitation as `.agents/skills/` placement. The auto-refresh hooks above are per-developer; they don't run in cloud runners.
247
+
248
+ Auto-refresh hooks for Windsurf and Cline are not yet supported — those agents lack a documented SessionStart-equivalent event today. Run `skillrepo update` manually in those environments.
249
+
157
250
  **Under `npx skillrepo init`, the CLI offers to install itself globally.** Session sync needs the binary at a stable absolute path (the `npx` cache path is transient and would break on the next cache eviction). Rather than skipping with a warning the way v3.1.1 did, v3.1.2 prompts during init: *"SkillRepo needs a global install to enable session sync. Install `skillrepo` globally now? (Y/n)"* — saying yes runs `npm install -g skillrepo@<version>` (pinned to the version you just invoked) and then installs the hook with the resulting absolute path. Under `--yes` the install runs without prompting; under `--no-session-sync` it's skipped entirely. If the install fails (permissions, network, registry), init prints actionable next-steps and continues; the rest of init still completes successfully.
158
251
 
159
252
  `skillrepo session-sync enable` does **not** auto-install — it's an explicit, deliberate command and assumes you already have a global install. If invoked under `npx` without a global install present, it returns a clear "install globally first" message rather than mutating your global package set.
@@ -194,6 +287,13 @@ With `--global`, also removes:
194
287
  - `mcpServers.skillrepo` from `~/.codeium/windsurf/mcp_config.json`
195
288
  - The `~/.claude/skills/` global skill cache
196
289
  - The `~/.claude/skillrepo/` directory (stored credentials + sync cache)
290
+ - The SkillRepo SessionStart entry from each cohort vendor's hook config —
291
+ `~/.cursor/hooks.json`, `~/.gemini/settings.json`, `~/.codex/hooks.json`,
292
+ and `~/.copilot/hooks/skillrepo-update.json`. Other tools' entries
293
+ (1Password, Snyk, Apiiro, the user's own hooks) and unrelated top-level
294
+ keys (theme, mcpServers, etc.) are preserved — only our entry, identified
295
+ by the canonical command `npx --yes skillrepo update --silent`, is
296
+ filtered out.
197
297
 
198
298
  Flags:
199
299
 
@@ -275,11 +375,20 @@ Two scenarios worth calling out:
275
375
 
276
376
  ### Skill placement
277
377
 
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.
378
+ Skills land at one of two project paths, depending on the agent:
379
+
380
+ | Agent | Project path | Personal (`--global`) path |
381
+ |---|---|---|
382
+ | Claude Code | `.claude/skills/<name>/` | `~/.claude/skills/<name>/` |
383
+ | Cursor / Windsurf / Gemini CLI / Codex CLI / Cline / GitHub Copilot | `.agents/skills/<name>/` | `~/.agents/skills/<name>/` |
384
+ | Windsurf (personal-scope override) | — | `~/.codeium/windsurf/skills/<name>/` |
385
+
386
+ The CLI auto-adds `.claude/skills/` and `.agents/skills/` to your project
387
+ `.gitignore` on first write — skills are a per-developer cache, not
388
+ committed content. The `--agent` flag overrides detection (e.g.
389
+ `--agent claude,agents` writes both paths). See
390
+ [`docs/vendor-paths.md`](docs/vendor-paths.md) for primary-source
391
+ citations on each agent's read paths.
283
392
 
284
393
  ## Exit codes
285
394
 
@@ -342,11 +451,12 @@ path — both the interactive prompt in `init` and the Bearer header
342
451
  used by every subsequent command — so pasting from email or a
343
452
  browser with a trailing newline is safe.
344
453
 
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.
454
+ **Picker selected nothing under `--yes`**Phase 3 (#1236) replaced
455
+ the old "no agents detected → refuse" branch with a friendlier
456
+ default: when no detection signals fire, both default rows are
457
+ pre-checked, so `--yes` configures both targets. If you want to skip
458
+ placement entirely under `--yes`, pass `--agent none` (config and
459
+ gitignore still happen; only the skill files are skipped).
350
460
 
351
461
  **"Rate limit exceeded — retried automatically and still getting
352
462
  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.1.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
  );
@@ -0,0 +1,127 @@
1
+ /**
2
+ * `skillrepo init` step-6 sibling for cohort SessionStart hooks (#1240).
3
+ *
4
+ * Runs ALONGSIDE `installSessionSyncHook` (the legacy Claude-Code-only
5
+ * installer). Where the Claude installer has to resolve a stable
6
+ * absolute `binaryPath` for the hook command — three branches
7
+ * (non-npx, existing global, auto-install) — the cohort installer has
8
+ * no such concern: every cohort hook is `npx --yes skillrepo update
9
+ * --silent`, which works on any host without a global install.
10
+ *
11
+ * That's why this is a sibling step, not an extension of
12
+ * `installSessionSyncHook`. The two flows share almost no logic. The
13
+ * architect review on #1240 specifically called out conflating them
14
+ * as a structural mistake — `installSessionSyncHook` is named for
15
+ * Claude Code's hook mechanism, and folding cohort logic into it
16
+ * would create a function with four irreducible branches by Phase 4
17
+ * (#1241-#1244).
18
+ *
19
+ * ## Decision tree
20
+ *
21
+ * noSessionSync → skip all cohort hooks (mirrors
22
+ * Claude path's --no-session-sync
23
+ * semantics)
24
+ * no cohort vendors selected → no-op (nothing to install)
25
+ * cohort vendors selected → install hooks for each one;
26
+ * per-vendor failures don't abort
27
+ * siblings
28
+ *
29
+ * ## Return shape
30
+ *
31
+ * { hooks: AgentHookInstallResult[] }
32
+ *
33
+ * Each entry: `{ vendorKey, displayName, path, action, reason? }`. The
34
+ * caller (init.mjs) merges these into the `sessionSync.cohortHooks`
35
+ * field of the JSON summary.
36
+ */
37
+
38
+ import { installAgentHooksForVendors } from "../lib/agent-hook-merge.mjs";
39
+ import { getAgentByKey } from "../lib/agent-registry.mjs";
40
+
41
+ /**
42
+ * @typedef {Object} CohortHookOptions
43
+ * @property {boolean} noSessionSync - True if `--no-session-sync` was
44
+ * passed. The flag covers BOTH the Claude session hook and
45
+ * the cohort hooks — semantically "skip all auto-refresh
46
+ * hooks." Pre-#1240 this was a Claude-only flag; the
47
+ * framing widened with the cohort feature.
48
+ * @property {string[]} vendors - Canonical vendor keys the user
49
+ * selected (via `--agent` or the picker). Vendors without
50
+ * an `agentHook` spec are silently skipped by the
51
+ * dispatcher; truly unknown keys surface as failures.
52
+ * @property {object} p - Init's printer (from `init.mjs:makePrinter`).
53
+ * Silenced under `--json`; otherwise renders one line per
54
+ * cohort hook installed/updated/failed.
55
+ */
56
+
57
+ /**
58
+ * Run the cohort sibling step. Always returns a result; never throws
59
+ * on user-recoverable failures (per-vendor errors are captured in
60
+ * each result entry).
61
+ *
62
+ * @param {CohortHookOptions} options
63
+ * @returns {{ hooks: import("../lib/agent-hook-merge.mjs").AgentHookInstallResult[] }}
64
+ */
65
+ export function installCohortHooks({ noSessionSync, vendors, p }) {
66
+ if (noSessionSync) {
67
+ // Match the Claude path's --no-session-sync semantics: silent
68
+ // skip. The Claude path already prints the warning, so we don't
69
+ // double-warn here. If somehow the user passes --no-session-sync
70
+ // AND no Claude target (so the Claude path doesn't print), the
71
+ // user got exactly what they asked for — explicit opt-out.
72
+ return { hooks: [] };
73
+ }
74
+
75
+ // Filter vendors to those with an agentHook spec. The dispatcher
76
+ // does this filtering internally too (silently skipping unknown
77
+ // entries), but doing it here lets us avoid the noise of "Skipped
78
+ // claudeCode (no agentHook)" lines for every vendor in the cohort
79
+ // list that isn't ours to install. Claude Code, Windsurf, and
80
+ // Cline all hit this filter; only the four cohort vendors with
81
+ // `agentHook != null` reach the dispatcher.
82
+ const eligible = vendors.filter((v) => {
83
+ const entry = getAgentByKey(v);
84
+ return entry && entry.agentHook !== null;
85
+ });
86
+
87
+ if (eligible.length === 0) {
88
+ return { hooks: [] };
89
+ }
90
+
91
+ const results = installAgentHooksForVendors({ vendors: eligible });
92
+
93
+ // Surface each result on the printer so the user sees what was
94
+ // written / refreshed / failed. Mirrors `installSessionSyncHook`'s
95
+ // per-action printing for Claude.
96
+ for (const r of results) {
97
+ if (r.action === "installed") {
98
+ p.success(`Cohort SessionStart hook installed for ${r.displayName} (${r.path})`);
99
+ } else if (r.action === "updated") {
100
+ p.success(`Cohort SessionStart hook updated for ${r.displayName} (${r.path})`);
101
+ } else if (r.action === "unchanged") {
102
+ p.success(`Cohort SessionStart hook already installed for ${r.displayName} (${r.path})`);
103
+ } else if (r.action === "failed") {
104
+ p.warning(
105
+ `Cohort SessionStart hook for ${r.displayName} failed: ${r.reason}. ` +
106
+ `Run \`skillrepo init\` again after fixing the issue.`,
107
+ );
108
+ }
109
+ }
110
+
111
+ // VS Code + Copilot's hook system is currently labelled Preview by
112
+ // GitHub (#1244). Surface that caveat once if Copilot was among the
113
+ // installed vendors so users know the hook schema may shift before
114
+ // GA. Skip the warning if Copilot's install actually failed — we
115
+ // already printed the failure line above and a Preview note would
116
+ // muddle the actionable signal.
117
+ const copilotResult = results.find((r) => r.vendorKey === "copilot");
118
+ if (copilotResult && copilotResult.action !== "failed") {
119
+ p.warning(
120
+ "Copilot's SessionStart hook system is currently labelled Preview by GitHub. " +
121
+ "The schema may shift before GA; re-run `skillrepo init` after upgrading the CLI " +
122
+ "if Copilot's hook config format changes.",
123
+ );
124
+ }
125
+
126
+ return { hooks: results };
127
+ }
@@ -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