skillshelf 0.1.0 → 0.3.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 (58) hide show
  1. package/README.md +98 -23
  2. package/package.json +8 -2
  3. package/src/adapters/inference/agent.ts +23 -16
  4. package/src/cli.ts +35 -0
  5. package/src/commands/add.ts +624 -128
  6. package/src/commands/agents.ts +120 -0
  7. package/src/commands/drop.ts +21 -13
  8. package/src/commands/import.ts +300 -0
  9. package/src/commands/infer.ts +6 -6
  10. package/src/commands/link.test.ts +160 -0
  11. package/src/commands/link.ts +317 -0
  12. package/src/commands/ls.ts +118 -18
  13. package/src/commands/mode-surfacing.test.ts +110 -0
  14. package/src/commands/new.ts +6 -6
  15. package/src/commands/outdated.test.ts +55 -0
  16. package/src/commands/outdated.ts +138 -18
  17. package/src/commands/refresh.ts +133 -0
  18. package/src/commands/remediation.test.ts +149 -0
  19. package/src/commands/rename.test.ts +121 -0
  20. package/src/commands/rename.ts +64 -0
  21. package/src/commands/retag.ts +58 -0
  22. package/src/commands/retire.ts +39 -0
  23. package/src/commands/rm.test.ts +133 -0
  24. package/src/commands/rm.ts +107 -0
  25. package/src/commands/roots.ts +41 -0
  26. package/src/commands/scan.ts +349 -0
  27. package/src/commands/show.ts +4 -1
  28. package/src/commands/status.ts +43 -8
  29. package/src/commands/tag.test.ts +109 -0
  30. package/src/commands/tag.ts +68 -0
  31. package/src/commands/unretire.ts +33 -0
  32. package/src/commands/untag.ts +73 -0
  33. package/src/commands/update.test.ts +71 -0
  34. package/src/commands/update.ts +65 -15
  35. package/src/commands/use.test.ts +92 -0
  36. package/src/commands/use.ts +46 -23
  37. package/src/commands/where.ts +232 -0
  38. package/src/config.test.ts +69 -0
  39. package/src/config.ts +126 -3
  40. package/src/core/agents.test.ts +232 -0
  41. package/src/core/agents.ts +363 -0
  42. package/src/core/bundle.ts +12 -15
  43. package/src/core/core.test.ts +14 -1
  44. package/src/core/crawl.ts +22 -5
  45. package/src/core/dedupe.ts +36 -0
  46. package/src/core/deployments.test.ts +147 -0
  47. package/src/core/deployments.ts +208 -0
  48. package/src/core/fetch.ts +344 -70
  49. package/src/core/indexgen.ts +2 -0
  50. package/src/core/library.test.ts +41 -0
  51. package/src/core/library.ts +62 -30
  52. package/src/core/lifecycle.ts +252 -0
  53. package/src/core/surfaces.ts +46 -0
  54. package/src/core/taxonomy.test.ts +159 -0
  55. package/src/core/taxonomy.ts +190 -0
  56. package/src/lib/fs.ts +2 -2
  57. package/src/types.ts +93 -13
  58. package/src/core/overlay.ts +0 -63
package/README.md CHANGED
@@ -7,11 +7,17 @@
7
7
  [![CI](https://img.shields.io/github/actions/workflow/status/Wang-Cankun/skillshelf/ci.yml?branch=main)](https://github.com/Wang-Cankun/skillshelf/actions)
8
8
  [![npm](https://img.shields.io/npm/v/skillshelf.svg)](https://www.npmjs.com/package/skillshelf)
9
9
 
10
- Your skills are scattered: some in `~/.claude/skills`, some buried in Obsidian or notes
11
- vaults, more copied into a dozen per-project `.claude` directories. You forget which ones
12
- exist, rewrite ones you already have, and copies drift out of sync. The naive fix — dump
13
- everything into `~/.claude/skills` makes every session pay the token cost of loading
14
- hundreds of skill descriptions at once.
10
+ Your skills are scattered across **every agent you use** — some in `~/.claude/skills`, some in
11
+ `~/.codex/skills` or `~/.cursor/skills`, some buried in Obsidian or notes vaults, more copied into
12
+ a dozen per-project `.claude` / `.codex` directories. Each tool scatters its own copies and
13
+ symlinks; you forget which ones exist, rewrite ones you already have, and copies drift out of sync.
14
+ The naive fix dump everything into one agent's dir — makes every session pay the token cost of
15
+ loading hundreds of skill descriptions at once.
16
+
17
+ skillshelf is **agent-agnostic** (Claude Code, Codex, Cursor, and compatible agents): the library
18
+ is a neutral source, and `skl where` maps where every skill is actually deployed across all of
19
+ them — surfacing untracked copies, drift, and dead links. It's the curation layer *over* your
20
+ agent dirs, complementary to installers like [`vercel-labs/skills`](https://github.com/vercel-labs/skills).
15
21
 
16
22
  skillshelf is the middle path: a single git-backed **library** that is a *passive shelf*
17
23
  (nothing auto-loads), plus a CLI to **search, tag, bundle, and load** exactly the skills a
@@ -22,6 +28,11 @@ actually use.
22
28
 
23
29
  skillshelf runs on [Bun](https://bun.sh) (>= 1.0). No other runtime dependencies.
24
30
 
31
+ > **Bun is required, not optional.** The `skl` bin is a TypeScript entrypoint with a
32
+ > `#!/usr/bin/env bun` shebang — there is no compiled Node build. `npm i -g skillshelf`
33
+ > will *not* give you a working `skl` (a `preinstall` guard aborts with a pointer to Bun);
34
+ > use `bunx` or `bun add -g` instead.
35
+
25
36
  ```bash
26
37
  # Run it without installing
27
38
  bunx skillshelf <command>
@@ -57,25 +68,72 @@ skl drop bioinfo # unlink when you're done
57
68
  Add `--json` to any command for machine-readable output (skillshelf is built to be driven
58
69
  by an agent as well as a human).
59
70
 
71
+ ## Migrating scattered skills
72
+
73
+ Already have skills strewn across `~/.claude/skills`, an Obsidian vault, and a dozen project
74
+ `.claude` dirs? Consolidate them with three deterministic primitives — `scan` discovers,
75
+ `import` adopts, `infer` tags. The judgment in between (which copies to keep, which drift wins)
76
+ is yours (or your agent's); the tool never guesses.
77
+
78
+ ```bash
79
+ # 1. Register the places your skills live, then take a read-only inventory.
80
+ # scan moves nothing — it just reports candidates, duplicates, and drift.
81
+ skl scan --add-root ~/.claude/skills
82
+ skl scan --add-root ~/notes/.agents/skills
83
+ skl scan # report every candidate + duplicate/drift group
84
+
85
+ # 2. Adopt the ones you want, one at a time. Each import moves the skill into the
86
+ # library and leaves a symlink behind so old paths keep resolving.
87
+ skl import rnaseq-qc --from ~/.claude/skills/rnaseq-qc
88
+ skl import xhs-title --from ~/notes/.agents/skills/xhs-title
89
+
90
+ # For a skill living inside a project repo, copy instead of move (no symlink left behind):
91
+ skl import deploy-check --from ~/projects/web/.claude/skills/deploy-check --copy
92
+
93
+ # When two copies drifted and you've picked the winner, overwrite the loser:
94
+ skl import rnaseq-qc --from ~/projects/lab/.claude/skills/rnaseq-qc --force
95
+
96
+ # For a skill you actively develop in its own git repo, shelve a LINK instead of a copy —
97
+ # the repo stays canonical and edits show up live, no drift, no re-sync (ADR-0004):
98
+ skl link --from ~/Documents/GitHub/cairn/skill/cairn
99
+
100
+ # 3. Tag the now-populated library in one pass. Domain is tags, not folders, so this
101
+ # runs AFTER import with no reorg — no skill ever has to move because a tag changed.
102
+ skl infer --emit # hand the payload to your agent, then `skl infer --apply`
103
+ ```
104
+
105
+ Domain lives entirely in tags ([ADR-0001](./docs/adr/0001-domain-is-tags-not-folders.md)): the
106
+ library layout is flat (`library/<name>/`) and `import` never decides a domain, so there is no
107
+ chicken-and-egg between adopting a skill and tagging it.
108
+
60
109
  ## How it works
61
110
 
62
111
  skillshelf separates *owning* a skill from *loading* it.
63
112
 
64
- - **Canonical library** — a dedicated git repo, one file per skill in its primary-domain
65
- folder. This is a passive shelf: nothing here auto-loads, which is exactly what kills the
66
- all-at-once token cost.
67
- - **Domain bundles** — bundles are *tag queries*, not folders. A skill tagged
68
- `domains: [coding, bioinfo]` shows up in both bundles from a single copy on disk.
69
- `skl use bioinfo` resolves every skill carrying that tag.
113
+ - **Canonical library** — a dedicated git repo, one copy per skill in a flat, non-semantic
114
+ layout (`library/<name>/`). This is a passive shelf: nothing here auto-loads, which is
115
+ exactly what kills the all-at-once token cost.
116
+ - **Domain bundles** — domain is *tags, not folders* ([ADR-0001](./docs/adr/0001-domain-is-tags-not-folders.md)).
117
+ A skill tagged `domains: [coding, bioinfo]` shows up in both bundles from a single copy on
118
+ disk; `skl use bioinfo` resolves every skill carrying that tag. `primaryDomain` is just
119
+ `domains[0]`, never inferred from a folder.
70
120
  - **Thin global core** — a handful of universal skills (commit, search, memory) are
71
121
  symlinked permanently into `~/.claude/skills` so they always auto-trigger. Small, bounded
72
122
  token cost — "some loaded is fine; all-at-once is the problem."
73
123
  - **On-demand `show`** — prints only the SKILL.md instruction body and lists the paths of
74
124
  any bundled reference files (without reading them). Progressive disclosure: cheap by
75
125
  default, deep when you ask. Works mid-task with no reload.
76
- - **Sidecar overlay** installed third-party skills keep a pristine `upstream/` body plus a
77
- `<skill>.shelf.json` overlay holding *your* tags, bundle membership, and notes. `skl update`
78
- swaps the upstream body cleanly while your taxonomy survives updates never clobber your tags.
126
+ - **Owned vs linked entries** ([ADR-0004](./docs/adr/0004-owned-vs-linked-entries.md)) the
127
+ library is a *bookshelf*: an entry either **owns** its bytes (a real copy; the library is
128
+ canonical for downloads and stabilized skills) or is **linked** (a symlink to an external dev
129
+ repo that stays canonical — for skills you actively develop in their own git, e.g. `cairn`).
130
+ `skl link --from <dev-repo>` registers a linked entry; `skl where` shows it as a clean
131
+ `✓ source`; `skl update` / `outdated` skip linked entries so they never pull upstream into your
132
+ dev repo. The mode is derived from the filesystem (a symlink resolving outside the library),
133
+ never stored, so it can't go stale.
134
+ - **Updates never clobber your tags** — domain tags live in the central `taxonomy.json`
135
+ ([ADR-0002](./docs/adr/0002-central-taxonomy-not-sidecars.md)), separate from the skill body, so
136
+ `skl update` can swap an owned skill's upstream `SKILL.md` cleanly while your taxonomy survives.
79
137
 
80
138
  ```
81
139
  skl search / ls / show skl use <bundle>
@@ -95,20 +153,37 @@ See [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) for the full design.
95
153
  | Command | Summary | Key flags |
96
154
  |---|---|---|
97
155
  | `skl init` | Set up `~/.skillshelf` config + library and link the global-core skills | `--force` |
156
+ | `skl scan [roots…]` | Read-only discovery of skill candidates across roots (counts, duplicates, drift) | `--add-root <path>`, `--remove-root <path>` |
157
+ | `skl roots` | List the persisted scan roots (read-only; no crawl) | — |
158
+ | `skl import <name> --from <path>` | Adopt your own skill into the library as an OWNED copy (move + symlink-back, or `--copy`) | `--copy`, `--as <slug>`, `--force` |
159
+ | `skl link [<name>] --from <dev-repo>` | Shelve a dev-repo skill as a LINKED entry (library symlinks to it; the repo stays canonical). `--at <path>` instead collapses a stray copy into the library | `--from`, `--at`, `--force` |
98
160
  | `skl new <name>` | Scaffold a new skill dir + SKILL.md into the library | `--domain <d>`, `--desc "..."`, `--force` |
99
- | `skl ls [bundle]` | One-line listing of the library, or one bundle | `--all` |
161
+ | `skl ls [bundle]` | One-line listing of the library, or one bundle (`--json` carries `mode`/`linkTarget`) | `--all` |
100
162
  | `skl search <kw...>` | Fuzzy match over name + description + domains across the library | — |
101
163
  | `skl show <name>` | Print a skill's SKILL.md body; list reference-file paths (not contents) | — |
102
- | `skl status` | Show which library skills are linked into `./.claude/skills` | — |
103
- | `skl use <bundle>` | Symlink a bundle's skills into `./.claude/skills/` (hot-loads) | — |
104
- | `skl drop <bundle>` | Remove a bundle's symlinks from `./.claude/skills/` | — |
105
- | `skl add <src>` | Install a third-party skill (`github:`/registry), record provenance, auto-tag | `--domain <d>`, `--name <slug>`, `--no-infer`, `--force` |
106
- | `skl outdated [name]` | Check upstream ref per tracked skill and mark stale ones | — |
107
- | `skl update [name]` | Re-pull upstream body, preserve overlay, diff if local body diverged | `--force`, `--dry-run` |
164
+ | `skl tag <name> <domain>…` | Add domain tag(s) to a skill in the central taxonomy (deterministic, no LLM) | — |
165
+ | `skl untag <name> <domain>` | Remove a domain tag from a skill | — |
166
+ | `skl retag <old> <new>` | Rename a domain across the whole library taxonomy (deterministic) | — |
167
+ | `skl rename <old> <new>` | Rename a skill slug atomically (dir + frontmatter + taxonomy + lock). Alias `skl mv` | — |
168
+ | `skl retire <name>` | Soft-delete a skill into `_retired/` (reversible; excluded from deploys) | — |
169
+ | `skl unretire <name>` | Restore a retired skill back to the active library | |
170
+ | `skl rm <name>` | Delete a skill (dir/symlink + taxonomy + lock), re-index. Refuses a live OWNED skill without `--force`; a LINKED entry `rm`s freely (safe unlink) | `--force`, `--dry-run` |
171
+ | `skl status` | Show which library skills are linked into `./.claude/skills`; flags unmanaged real copies (drift-prone) | — |
172
+ | `skl where [name]` | Map where each skill is deployed across all agents (Claude, Codex, Cursor…); flags copies, drift, 2nd-sources, dead links — a dev repo a library entry links to shows as a clean `✓ source` | `--problems`, `--prune`, `--fix`, `--dry-run` |
173
+ | `skl use <bundle\|skill>` | Symlink a bundle (or a single skill) into `./.claude/skills/` (hot-loads) | — |
174
+ | `skl drop <bundle\|skill>` | Remove a bundle's (or single skill's) symlinks from `./.claude/skills/` | — |
175
+ | `skl refresh` | Re-sync this project's `./.claude/skills` symlinks to current library reality (repoint stale, prune vanished) | `--dry-run` |
176
+ | `skl add <src>` | Install third-party skill(s) into the library (librarian only — no agent-dir writes). One repo = **one clone**: a bare repo with several skills needs `--all`/`--skill`/`--list`; single-skill `add <repo>/<path>` is unchanged. `--list` discovers + prints; `--dry-run` previews drift (new/identical/differs); a `differs` skill is skipped without `--force` | `--all`, `--skill <a,b>`, `--list`, `--dry-run`, `--domain <d>`, `--name <slug>`, `--no-infer`, `--force` |
177
+ | `skl outdated [name]` | Check upstream ref per tracked skill and mark stale ones (LINKED dev-repo entries are reported, never probed); `--check-local` diffs the local body against its baseline offline | `--check-local` |
178
+ | `skl update [name]` | Re-pull upstream body, preserve domain tags, diff if local body diverged (LINKED entries are skipped — their own git owns versioning) | `--force`, `--dry-run` |
108
179
  | `skl index` | Regenerate `INDEX.md` (catalog grouped by domain) | — |
109
180
  | `skl infer` | Re-run AI domain taxonomy over the library (emit/apply/provider modes) | see below |
110
181
 
111
- Every command also accepts `--json`.
182
+ Every command also accepts `--json`. Destructive/edit verbs (`rm`, `retire`/`unretire`,
183
+ `rename`, `tag`/`untag`/`retag`, `scan --remove-root`, `where --prune`/`--fix`,
184
+ `refresh`) are the inverse + fine-grained-edit family from
185
+ [ADR-0005](./docs/adr/0005-inverse-and-edit-verbs.md): reversible by default, transactional
186
+ across the skill dir + `taxonomy.json` + `shelf.lock.json` + `INDEX.md`.
112
187
 
113
188
  ## AI taxonomy & inference
114
189
 
@@ -126,7 +201,7 @@ skl infer [--emit | --apply <file.json> | --provider <name>] \
126
201
  - `--emit` — print a self-contained prompt + the library payload as JSON. Hand it to whatever
127
202
  agent or model you already have open; it does the reasoning.
128
203
  - `--apply <file.json>` — apply the taxonomy proposal the agent produced back into the library
129
- (for review/approval), updating tags via the overlay.
204
+ (for review/approval), writing tags into the central `taxonomy.json`.
130
205
 
131
206
  **API mode (skillshelf calls an OpenAI-compatible endpoint itself):**
132
207
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillshelf",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Agent-first skill registry + manager for Claude Code and compatible agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,6 +25,7 @@
25
25
  "skl": "./src/cli.ts"
26
26
  },
27
27
  "scripts": {
28
+ "preinstall": "bun --version >/dev/null 2>&1 || { echo '\\nskillshelf requires the Bun runtime (https://bun.sh) — its bin is a TypeScript entrypoint, not a compiled Node script.\\nInstall Bun, then: bun add -g skillshelf (or run without installing: bunx skillshelf <command>)\\n' >&2; exit 1; }",
28
29
  "skl": "bun run src/cli.ts",
29
30
  "test": "bun test"
30
31
  },
@@ -38,5 +39,10 @@
38
39
  "src",
39
40
  "README.md",
40
41
  "LICENSE"
41
- ]
42
+ ],
43
+ "devDependencies": {
44
+ "@types/bun": "^1.3.14",
45
+ "@types/node": "22",
46
+ "typescript": "5.6"
47
+ }
42
48
  }
@@ -4,15 +4,15 @@
4
4
  // - emit : assemble an InferenceCorpus + JSON schema + instruction and print it
5
5
  // to stdout so the HOST agent (Claude Code) can reason over it and
6
6
  // produce a proposal file.
7
- // - apply: read the agent's proposal JSON and write proposed domains/tags into
8
- // each skill's `<name>.shelf.json` overlay (never upstream SKILL.md).
7
+ // - apply: read the agent's proposal JSON and write proposed domains into the
8
+ // central taxonomy.json (never upstream SKILL.md).
9
9
  //
10
10
  // The api.ts adapter reuses buildCorpus() + applyProposal() to close the loop
11
11
  // automatically against any OpenAI-compatible LLM endpoint.
12
12
 
13
- import type { InferenceCorpus, Overlay, Skill } from "../../types.ts";
13
+ import type { InferenceCorpus, Skill } from "../../types.ts";
14
14
  import { listDomains } from "../../core/library.ts";
15
- import { readOverlay, writeOverlay } from "../../core/overlay.ts";
15
+ import { readTaxonomy, writeTaxonomy, domainsForName } from "../../core/taxonomy.ts";
16
16
  import { parseFrontmatter } from "../../lib/frontmatter.ts";
17
17
 
18
18
  /** Max characters of SKILL.md body included per skill in the corpus preview. */
@@ -20,8 +20,9 @@ const BODY_PREVIEW_CHARS = 1200;
20
20
 
21
21
  /**
22
22
  * The proposal shape the host agent (or the gateway) must return: a map of
23
- * skill name -> proposed domains, plus optional primary + notes. Authors apply
24
- * `domains` into each overlay (unioned with existing, never destructive).
23
+ * skill name -> proposed domains, plus optional primary + notes. Applying unions
24
+ * `domains` into the central taxonomy (per skill, never destructive). `notes` is
25
+ * accepted for backward compatibility but is no longer persisted (ADR-0002).
25
26
  */
26
27
  export interface InferenceProposalEntry {
27
28
  name: string;
@@ -82,7 +83,7 @@ export const INFER_INSTRUCTION = [
82
83
  "domain plus any honest secondary tags (a dual-use skill belongs to multiple).",
83
84
  "Keep domain tokens lowercase and hyphenated.",
84
85
  "Return ONE JSON object that validates against `schema` (no prose, no markdown",
85
- "fences). Then run `skl infer --apply <file.json>` to write it into the overlays.",
86
+ "fences). Then run `skl infer --apply <file.json>` to write it into taxonomy.json.",
86
87
  ].join(" ");
87
88
 
88
89
  /** Build the deterministic InferenceCorpus snapshot from loaded skills. */
@@ -185,7 +186,7 @@ export function normalizeProposal(raw: unknown): InferenceProposal {
185
186
  }
186
187
 
187
188
  export interface ApplyResult {
188
- /** skill name -> domains written into its overlay */
189
+ /** skill name -> domains written into the taxonomy for that skill */
189
190
  applied: Array<{ name: string; domains: string[]; added: string[] }>;
190
191
  /** assignment names with no matching skill in the library */
191
192
  unmatched: string[];
@@ -194,11 +195,13 @@ export interface ApplyResult {
194
195
  }
195
196
 
196
197
  /**
197
- * Apply a proposal into each skill's overlay. Domains are UNIONED with the
198
- * skill's existing effective domains (never destructive), written to
199
- * `<name>.shelf.json` only — upstream SKILL.md is never touched.
198
+ * Apply a proposal into the central taxonomy (`<library>/taxonomy.json`). For each
199
+ * assignment, domains are UNIONED with the skill's EXISTING taxonomy entry (never
200
+ * destructive); upstream SKILL.md is never touched. `notes` is dropped (ADR-0002).
201
+ * The taxonomy is read once and written once at the end.
200
202
  */
201
203
  export async function applyProposal(
204
+ libraryPath: string,
202
205
  skills: Skill[],
203
206
  proposal: InferenceProposal,
204
207
  ): Promise<ApplyResult> {
@@ -209,6 +212,8 @@ export async function applyProposal(
209
212
  if (!existing || (existing.mirrorOf && !s.mirrorOf)) byName.set(s.name, s);
210
213
  }
211
214
 
215
+ const tax = await readTaxonomy(libraryPath);
216
+
212
217
  const applied: ApplyResult["applied"] = [];
213
218
  const unmatched: string[] = [];
214
219
  const skipped: string[] = [];
@@ -232,8 +237,9 @@ export async function applyProposal(
232
237
  continue;
233
238
  }
234
239
 
235
- const prev = await readOverlay(skill);
236
- const existingDomains = Array.isArray(prev?.domains) ? prev!.domains : [];
240
+ // Union with the skill's EXISTING taxonomy domains (non-destructive). Resolve
241
+ // by the canonical skill name so mirror copies map to the same entry.
242
+ const existingDomains = domainsForName(tax, skill.name);
237
243
  const merged: string[] = [...existingDomains];
238
244
  const added: string[] = [];
239
245
  for (const d of ordered) {
@@ -243,11 +249,12 @@ export async function applyProposal(
243
249
  }
244
250
  }
245
251
 
246
- const next: Overlay = { ...(prev ?? {}), domains: merged };
247
- if (a.notes) next.notes = a.notes;
248
- await writeOverlay(skill, next);
252
+ tax.skills[skill.name] = merged;
249
253
  applied.push({ name: a.name, domains: merged, added });
250
254
  }
251
255
 
256
+ // Persist the whole taxonomy once.
257
+ await writeTaxonomy(libraryPath, tax);
258
+
252
259
  return { applied, unmatched, skipped };
253
260
  }
package/src/cli.ts CHANGED
@@ -27,16 +27,44 @@ import * as outdated from "./commands/outdated.ts";
27
27
  import * as update from "./commands/update.ts";
28
28
  import * as infer from "./commands/infer.ts";
29
29
  import * as newCmd from "./commands/new.ts";
30
+ import * as scan from "./commands/scan.ts";
31
+ import * as roots from "./commands/roots.ts";
32
+ import * as importCmd from "./commands/import.ts";
33
+ import * as link from "./commands/link.ts";
34
+ import * as where from "./commands/where.ts";
35
+ import * as agents from "./commands/agents.ts";
36
+ import * as tag from "./commands/tag.ts";
37
+ import * as untag from "./commands/untag.ts";
38
+ import * as retag from "./commands/retag.ts";
39
+ import * as rm from "./commands/rm.ts";
40
+ import * as retire from "./commands/retire.ts";
41
+ import * as unretire from "./commands/unretire.ts";
42
+ import * as rename from "./commands/rename.ts";
43
+ import * as refresh from "./commands/refresh.ts";
30
44
 
31
45
  // Registration order = display order in help.
32
46
  const MODULES: CommandModule[] = [
33
47
  search,
34
48
  ls,
35
49
  status,
50
+ where,
51
+ agents,
36
52
  show,
37
53
  use,
38
54
  drop,
55
+ refresh,
39
56
  add,
57
+ scan,
58
+ roots,
59
+ importCmd,
60
+ link,
61
+ tag,
62
+ untag,
63
+ retag,
64
+ retire,
65
+ unretire,
66
+ rename,
67
+ rm,
40
68
  outdated,
41
69
  update,
42
70
  init,
@@ -50,6 +78,13 @@ for (const mod of MODULES) {
50
78
  COMMANDS.set(mod.meta.name, mod);
51
79
  }
52
80
 
81
+ // Command aliases (not shown in the help listing; resolve to the canonical module).
82
+ const ALIASES: Record<string, string> = { mv: "rename" };
83
+ for (const [alias, target] of Object.entries(ALIASES)) {
84
+ const mod = COMMANDS.get(target);
85
+ if (mod) COMMANDS.set(alias, mod);
86
+ }
87
+
53
88
  function helpText(): string {
54
89
  const lines: string[] = [];
55
90
  lines.push("skl — skillshelf: agent-first skill registry + manager");