claude-skill-lint 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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +383 -0
  3. package/bin/cli.js +10 -0
  4. package/dist/changed-files.d.ts +20 -0
  5. package/dist/changed-files.d.ts.map +1 -0
  6. package/dist/changed-files.js +49 -0
  7. package/dist/changed-files.js.map +1 -0
  8. package/dist/classify.d.ts +13 -0
  9. package/dist/classify.d.ts.map +1 -0
  10. package/dist/classify.js +72 -0
  11. package/dist/classify.js.map +1 -0
  12. package/dist/cli.d.ts +2 -0
  13. package/dist/cli.d.ts.map +1 -0
  14. package/dist/cli.js +124 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/config.d.ts +19 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +103 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/detect-format.d.ts +18 -0
  21. package/dist/detect-format.d.ts.map +1 -0
  22. package/dist/detect-format.js +137 -0
  23. package/dist/detect-format.js.map +1 -0
  24. package/dist/extract.d.ts +19 -0
  25. package/dist/extract.d.ts.map +1 -0
  26. package/dist/extract.js +197 -0
  27. package/dist/extract.js.map +1 -0
  28. package/dist/graph.d.ts +21 -0
  29. package/dist/graph.d.ts.map +1 -0
  30. package/dist/graph.js +94 -0
  31. package/dist/graph.js.map +1 -0
  32. package/dist/index.d.ts +26 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +30 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/init.d.ts +17 -0
  37. package/dist/init.d.ts.map +1 -0
  38. package/dist/init.js +45 -0
  39. package/dist/init.js.map +1 -0
  40. package/dist/lint.d.ts +16 -0
  41. package/dist/lint.d.ts.map +1 -0
  42. package/dist/lint.js +152 -0
  43. package/dist/lint.js.map +1 -0
  44. package/dist/profiles.d.ts +37 -0
  45. package/dist/profiles.d.ts.map +1 -0
  46. package/dist/profiles.js +161 -0
  47. package/dist/profiles.js.map +1 -0
  48. package/dist/reporter.d.ts +21 -0
  49. package/dist/reporter.d.ts.map +1 -0
  50. package/dist/reporter.js +110 -0
  51. package/dist/reporter.js.map +1 -0
  52. package/dist/types.d.ts +61 -0
  53. package/dist/types.d.ts.map +1 -0
  54. package/dist/types.js +6 -0
  55. package/dist/types.js.map +1 -0
  56. package/dist/validate-frontmatter.d.ts +41 -0
  57. package/dist/validate-frontmatter.d.ts.map +1 -0
  58. package/dist/validate-frontmatter.js +409 -0
  59. package/dist/validate-frontmatter.js.map +1 -0
  60. package/dist/validate-graph.d.ts +46 -0
  61. package/dist/validate-graph.d.ts.map +1 -0
  62. package/dist/validate-graph.js +651 -0
  63. package/dist/validate-graph.js.map +1 -0
  64. package/dist/validate-manifest.d.ts +22 -0
  65. package/dist/validate-manifest.d.ts.map +1 -0
  66. package/dist/validate-manifest.js +365 -0
  67. package/dist/validate-manifest.js.map +1 -0
  68. package/package.json +64 -0
  69. package/schemas/agent.schema.json +58 -0
  70. package/schemas/command.schema.json +60 -0
  71. package/schemas/skill.schema.json +74 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Robin Cannon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,383 @@
1
+ # claude-skill-lint
2
+
3
+ Structural validation and token-efficiency tooling for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skills. Catches the bugs that Claude tolerates but your users pay for.
4
+
5
+ ## The Problem No One Sees
6
+
7
+ Claude Code skills load into the context window on every invocation. When a skill references a context file that doesn't exist, Claude doesn't throw an error. It hallucinates the missing context and keeps going. When two skills resolve to the same install path, one silently overwrites the other. When a context file is loaded but nothing references it, you're burning tokens for nothing.
8
+
9
+ These aren't hypothetical failure modes. They're what we found running claude-skill-lint against a real 139-file production suite:
10
+
11
+ - **32 broken references.** 23 skills referenced a context file that's generated at setup time — in the shareable repo, before setup runs, those skills have no context at all. 9 more referenced a file that had been renamed months earlier. Every one of those skills was silently degraded.
12
+ - **11 orphaned files.** Context and agent files loaded into the window but referenced by zero skills. Dead weight on every invocation.
13
+ - **1 dependency cycle.** Two context files referencing each other, pulling both into the window when only one was needed.
14
+
15
+ Caught in under two seconds. No LLM calls. Deterministic.
16
+
17
+ "Tolerate" is not "work correctly."
18
+
19
+ ## Two Layers
20
+
21
+ | | `claude-skill-lint` | `/te-review` |
22
+ |-|-------------|-------------|
23
+ | **What** | Structural validation | Token efficiency audit |
24
+ | **Speed** | <2s, every PR | ~60s, on demand |
25
+ | **Approach** | Deterministic CI — no LLM | LLM-powered deep analysis |
26
+ | **Catches** | Broken refs, orphans, cycles, collisions, parse errors, size violations, quality regressions | Redundant content, output format waste, instruction bloat, model routing, architecture-level optimization |
27
+
28
+ claude-skill-lint enforces the structural foundation. `/te-review` (included in [`skills/te-review.md`](skills/te-review.md)) goes deeper — four-pass analysis across architecture, efficiency, and instruction quality, producing a scored assessment (0-24) with a prioritized optimization plan and estimated token savings per fix.
29
+
30
+ We ran te-review against itself. It scored 20/24. The main finding: the skill didn't constrain its own output the way it tells others to. After adding "top 5 findings per category" and "each subagent returns top 10 findings only," estimated output token savings on large suite reviews dropped by ~50%. The tool practices what it preaches.
31
+
32
+ Install the deep audit skill:
33
+
34
+ ```bash
35
+ cp node_modules/claude-skill-lint/skills/te-review.md ~/.claude/commands/te-review.md
36
+ ```
37
+
38
+ Then in any Claude Code session:
39
+
40
+ ```
41
+ /te-review suite # Full suite audit (scored 0-24)
42
+ /te-review audit my-skill # Single skill deep dive
43
+ /te-review compare old.md new.md # Before/after token impact
44
+ ```
45
+
46
+ ### What te-review checks
47
+
48
+ Four sequential passes, each producing up to 5 findings per severity level:
49
+
50
+ 1. **Structural Audit** — CLAUDE.md bloat, unused tool declarations, reference depth, subagent model routing, file size
51
+ 2. **Redundancy Detection** — skill-context overlap, cross-skill duplication, CLAUDE.md-skill overlap. High-fanout context files are flagged as highest-ROI optimization targets.
52
+ 3. **Output Efficiency** — missing format constraints, missing conciseness directives, unbounded output sections, prose where structured would suffice. Output tokens cost 5x input — this pass often finds the biggest savings.
53
+ 4. **Instruction Quality** — motivational fluff, politeness tokens, filler phrases, default-behavior instructions, conflicting constraints, emphasis overuse
54
+
55
+ ## What It Actually Found
56
+
57
+ ### Against a 139-file shared skill suite
58
+
59
+ ```
60
+ $ claude-skill-lint graph .
61
+ ✖ 43 errors and 11 warnings in 54 files (139 files checked)
62
+ ```
63
+
64
+ The headline: `inv-output-conventions.md` was renamed to `inv-output-patterns.md` at some point. Nine invention skills still referenced the old name. They silently got no output formatting guidance. A rename bug, invisible at authoring time, caught in seconds.
65
+
66
+ ### Against Anthropic's official skills repo (plugin format)
67
+
68
+ ```
69
+ $ claude-skill-lint graph ~/Development/anthropic-skills/
70
+ ✖ 19 errors and 10 warnings in 26 files (63 files checked)
71
+ ```
72
+
73
+ Name-collision findings in the `claude-api` skill — five language-specific `claude-api.md` files (PHP, Java, Ruby, Go, C#) all resolve to the same canonical name. Broken references to `shared/tool-use-concepts.md` and `shared/live-sources.md` — files that don't exist. Orphaned theme files in `theme-factory` (loaded dynamically, not via static references). Legitimate structural observations, not false positives.
74
+
75
+ ### Against a multi-plugin production repo
76
+
77
+ ```
78
+ $ claude-skill-lint lint ~/Development/work/ai-plugins/
79
+ ✖ 3 warnings in 3 files (45 files checked)
80
+ ```
81
+
82
+ Three unlisted plugins (valid `plugin.json` but not declared in the root `marketplace.json`). Graph validation: clean — zero errors across 45 files. The multi-plugin format's relative path resolution (`../../context/foo.md`) works correctly.
83
+
84
+ ### Frontmatter lint: honest assessment
85
+
86
+ The lint pass found 34 type errors across the same 139-file suite — `argument-hint` values parsed as YAML arrays instead of strings, `tools` fields as comma-separated strings instead of arrays.
87
+
88
+ Claude Code tolerates all of them. Skills work fine. The linter is being opinionated about structure, enforcing that frontmatter conforms to a schema. That matters when you're sharing skills, publishing to a marketplace, or building tooling that expects consistent types. For personal skills that just work, graph validation is where the real value lives.
89
+
90
+ The 4 YAML parse errors, on the other hand, are genuinely broken. The frontmatter can't be read at all.
91
+
92
+ ## Installation
93
+
94
+ ```bash
95
+ npm install -g claude-skill-lint
96
+ ```
97
+
98
+ Or directly:
99
+
100
+ ```bash
101
+ npx claude-skill-lint lint .
102
+ ```
103
+
104
+ Requires Node.js 20+.
105
+
106
+ ## Quick Start
107
+
108
+ ```bash
109
+ claude-skill-lint init # Auto-detect format, generate config
110
+ claude-skill-lint graph . # Cross-file references — the high-value bugs
111
+ claude-skill-lint lint . # Frontmatter structure
112
+ ```
113
+
114
+ ## What It Checks
115
+
116
+ ### Graph Validation
117
+
118
+ Builds a dependency graph from cross-file references. Finds:
119
+
120
+ - **Broken references** — skill says "read context/foo.md," file doesn't exist. Claude hallucinates the gap.
121
+ - **Orphaned files** — context or agent files that nothing references. Tokens loaded for nothing.
122
+ - **Name collisions** — two files resolve to the same canonical name. One overwrites the other on install.
123
+ - **Dependency cycles** — circular references between files. Context pollution.
124
+
125
+ Resolves both installed paths (`~/.claude/commands/context/foo.md`) and relative paths (`../../context/foo.md`, `./reference/guide.md`, `agents/scanner.md`), with automatic fallback between resolution strategies.
126
+
127
+ ### Frontmatter Validation
128
+
129
+ Validates YAML frontmatter against file-type schemas:
130
+
131
+ | File Type | Required Fields | Optional Fields |
132
+ |-----------|----------------|-----------------|
133
+ | Command | `description` | `model`, `allowed-tools`, `argument-hint`, `context`, `agent`, `effort`, `hooks`, `compatibility`, `metadata` |
134
+ | Agent | `name`, `description` | `model`, `tools`, `context`, `agent`, `effort`, `hooks`, `compatibility`, `metadata` |
135
+ | Skill (plugin) | `name`, `description` | `invocable`, `argument-hint`, `user-invocable`, `allowed-tools`, `context`, `agent`, `effort`, `hooks`, `compatibility`, `metadata` |
136
+ | Context | *(none)* | — |
137
+
138
+ #### Modern Frontmatter Fields
139
+
140
+ These fields are supported across all file types (command, agent, skill):
141
+
142
+ | Field | Type | Description |
143
+ |-------|------|-------------|
144
+ | `context` | `string` | Execution context for the skill (e.g. `fork` to run in a separate process) |
145
+ | `agent` | `string` | Agent mode or name to delegate execution to |
146
+ | `effort` | `string` | Reasoning effort level — controls how much thinking the model applies |
147
+ | `hooks` | `object` | Lifecycle hooks triggered before/after skill execution |
148
+ | `compatibility` | `string` | Compatibility requirements or version constraints |
149
+ | `metadata` | `object` | Arbitrary key-value metadata for tooling and marketplace use |
150
+ | `allowed-tools` | `array` or `string` | Tools the skill can use. Supports glob patterns like `mcp__*` and `Bash(*)` for broad matching, or specific tool names for fine-grained control |
151
+
152
+ At Level 1: model enum validation, known tool verification (including `Bash(python*)` pattern syntax), tool-to-body consistency, file size limits, `effort` value validation, skill name format.
153
+
154
+ ### Manifest Validation (plugin format)
155
+
156
+ Validates `marketplace.json` and `plugin.json` structure, source path resolution, name consistency, and missing skill files.
157
+
158
+ ## Progressive Quality Levels
159
+
160
+ Skills mature. The quality bar should mature with them.
161
+
162
+ | Level | What It Adds | When |
163
+ |-------|-------------|------|
164
+ | **0** | Valid YAML, required fields, non-empty body | New skills, prototyping |
165
+ | **1** | Model enum, known tools, tool-in-body check, file size limits | Established skills, shared suites |
166
+
167
+ Declare per file:
168
+
169
+ ```yaml
170
+ ---
171
+ name: my-skill
172
+ description: Does something useful
173
+ quality_level: 1
174
+ ---
175
+ ```
176
+
177
+ Or set directory defaults in `.skill-lint.yaml`:
178
+
179
+ ```yaml
180
+ default_level: 0
181
+ levels:
182
+ commands/: 1
183
+ agents/: 1
184
+ ```
185
+
186
+ Effective level: `max(file declaration, directory default, --level flag)`. The highest value wins. You can raise the floor but never lower a file's declared level.
187
+
188
+ ### Ratchet
189
+
190
+ ```bash
191
+ claude-skill-lint lint . --ratchet --base origin/main
192
+ ```
193
+
194
+ Compares each file's `quality_level` against the base branch. If any level decreased, the build fails. Quality improvements become permanent. That's the point.
195
+
196
+ ## Repository Formats
197
+
198
+ claude-skill-lint auto-detects your repository structure. Four formats are supported:
199
+
200
+ | Format | Structure | Detection Signal |
201
+ |--------|-----------|-----------------|
202
+ | **legacy-commands** | `commands/`, `agents/`, `context/` at repo root | No `.claude-plugin/` directory |
203
+ | **project-skills** | `.claude/skills/{name}/SKILL.md` | `.claude/skills/` with `SKILL.md` files |
204
+ | **plugin** | `skills/{name}/SKILL.md` with marketplace manifest | `.claude-plugin/marketplace.json` at root |
205
+ | **multi-plugin** | `plugins/{name}/skills/{skill}/SKILL.md` | Plugin subdirectories with `.claude-plugin/plugin.json` |
206
+
207
+ Detection priority: config override > multi-plugin > plugin > project-skills > legacy-commands. The first match wins.
208
+
209
+ ### project-skills: The `.claude/skills/` Format
210
+
211
+ The `project-skills` format uses `.claude/skills/{name}/SKILL.md` — the same structure Claude Code uses for project-scoped skills. Each skill lives in its own directory under `.claude/skills/`.
212
+
213
+ claude-skill-lint discovers skills in nested `.claude/skills/` directories automatically. In monorepo setups where multiple packages each have their own `.claude/skills/` directory, point the linter at the repo root and it finds them all.
214
+
215
+ Hybrid repos work too. A repo with both `.claude/skills/` and legacy `commands/` directories — or a published plugin that also has project-level skills — gets everything linted in a single run. No configuration needed.
216
+
217
+ ### Migration Note
218
+
219
+ For repos transitioning from legacy commands to modern skills, claude-skill-lint validates both locations in a single run. Set the format explicitly in `.skill-lint.yaml` if auto-detection picks the wrong one, or omit it and let detection handle the transition — legacy-commands is the fallback when no modern format signals are found.
220
+
221
+ ## Custom Structures
222
+
223
+ Not every repo follows a standard layout. claude-skill-lint provides three configuration levers for non-standard structures:
224
+
225
+ ### `skills_root`
226
+
227
+ If your skill files live in a subdirectory rather than the repo root:
228
+
229
+ ```yaml
230
+ skills_root: "packages/my-plugin"
231
+ ```
232
+
233
+ All path resolution starts from this root. Useful for monorepos where skills are nested deep.
234
+
235
+ ### `format` Override
236
+
237
+ Auto-detection works for standard layouts. When it doesn't — or when your repo is mid-migration between formats — set the format explicitly:
238
+
239
+ ```yaml
240
+ format: plugin # Force plugin format detection
241
+ # format: legacy-commands | plugin | multi-plugin | project-skills
242
+ ```
243
+
244
+ ### `ignore` Patterns
245
+
246
+ Exclude paths that look like skills but aren't:
247
+
248
+ ```yaml
249
+ ignore:
250
+ - "**/README.md"
251
+ - "**/CLAUDE.md"
252
+ - "node_modules/**"
253
+ - "docs/**/*.md"
254
+ - "archive/**"
255
+ ```
256
+
257
+ Glob patterns, matched against file paths relative to `skills_root`. `node_modules/` is always excluded.
258
+
259
+ ## Commands
260
+
261
+ ### `claude-skill-lint graph [paths...]`
262
+
263
+ ```bash
264
+ claude-skill-lint graph . # Full graph analysis
265
+ claude-skill-lint graph . --format json # JSON output
266
+ claude-skill-lint graph . --format github # GitHub annotations
267
+ claude-skill-lint graph . --strict # Orphan warnings become errors
268
+ ```
269
+
270
+ ### `claude-skill-lint lint [paths...]`
271
+
272
+ ```bash
273
+ claude-skill-lint lint . # Lint everything
274
+ claude-skill-lint lint . --level 1 # Enforce Level 1
275
+ claude-skill-lint lint . --strict # Warnings become errors
276
+ claude-skill-lint lint . --ratchet # Prevent quality regression
277
+ claude-skill-lint lint . --changed-only --base origin/main # Only changed files
278
+ claude-skill-lint lint . --format json # JSON for tooling
279
+ claude-skill-lint lint . --format github # GitHub Actions annotations
280
+ ```
281
+
282
+ ### `claude-skill-lint init`
283
+
284
+ ```bash
285
+ claude-skill-lint init # Auto-detect format, generate config
286
+ claude-skill-lint init --force # Overwrite existing
287
+ ```
288
+
289
+ Exit codes: `0` clean, `1` errors found, `2` config error.
290
+
291
+ ## Options
292
+
293
+ | Option | Default | Description |
294
+ |--------|---------|-------------|
295
+ | `--level` / `-l` | `0` | Minimum quality level (0-3) |
296
+ | `--changed-only` | `false` | Only check files changed since base ref |
297
+ | `--base` | `origin/main` | Git ref for `--changed-only` and `--ratchet` |
298
+ | `--format` / `-f` | `terminal` | Output: `terminal`, `json`, `github` |
299
+ | `--strict` | `false` | Treat warnings as errors |
300
+ | `--ratchet` | `false` | Fail if any quality_level decreased vs base |
301
+
302
+ ## Configuration
303
+
304
+ `claude-skill-lint init` generates this. Or create `.skill-lint.yaml` manually:
305
+
306
+ ```yaml
307
+ skills_root: "."
308
+ default_level: 0
309
+
310
+ levels:
311
+ commands/: 1
312
+ agents/: 1
313
+
314
+ # Auto-detected if omitted
315
+ # format: legacy-commands | plugin | multi-plugin | project-skills
316
+
317
+ models: [opus, sonnet, haiku]
318
+
319
+ tools:
320
+ mcp_pattern: "mcp__*"
321
+ custom: []
322
+
323
+ limits:
324
+ max_file_size: 15360
325
+
326
+ ignore:
327
+ - "**/README.md"
328
+ - "**/CLAUDE.md"
329
+ - "node_modules/**"
330
+
331
+ graph:
332
+ warn_orphans: true
333
+ detect_cycles: true
334
+ detect_duplicates: true
335
+ ```
336
+
337
+ ## CI Integration
338
+
339
+ ### GitHub Actions
340
+
341
+ ```yaml
342
+ name: Skill Lint
343
+ on:
344
+ pull_request:
345
+ paths: ['**/*.md', '.skill-lint.yaml']
346
+
347
+ jobs:
348
+ lint:
349
+ runs-on: ubuntu-latest
350
+ steps:
351
+ - uses: actions/checkout@v4
352
+ with:
353
+ fetch-depth: 0
354
+ - uses: actions/setup-node@v4
355
+ with:
356
+ node-version: '20'
357
+ - run: npm install -g claude-skill-lint
358
+
359
+ - name: Graph validation
360
+ run: claude-skill-lint graph . --format github
361
+
362
+ - name: Lint changed skills
363
+ run: claude-skill-lint lint . --format github --changed-only --base origin/${{ github.base_ref }}
364
+
365
+ - name: Quality ratchet
366
+ run: claude-skill-lint lint . --ratchet --base origin/${{ github.base_ref }} --format github
367
+ ```
368
+
369
+ `--format github` produces annotations that appear inline on PR diffs.
370
+
371
+ ### Pre-commit Hook
372
+
373
+ ```bash
374
+ #!/bin/sh
375
+ STAGED=$(git diff --cached --name-only --diff-filter=ACM -- '*.md')
376
+ if [ -n "$STAGED" ]; then
377
+ npx claude-skill-lint lint $STAGED --level 1
378
+ fi
379
+ ```
380
+
381
+ ## License
382
+
383
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { basename } from 'node:path';
3
+
4
+ // Deprecation notice when invoked as the old binary name
5
+ const binName = basename(process.argv[1] || '');
6
+ if (binName === 'skill-lint') {
7
+ process.stderr.write('Note: skill-lint is deprecated. Use claude-skill-lint instead.\n');
8
+ }
9
+
10
+ import '../dist/cli.js';
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Git-aware file filtering for --changed-only mode.
3
+ * Returns absolute paths to .md files changed since a given base ref.
4
+ */
5
+ /** Error thrown when git operations fail (non-git dir, bad ref, etc.). */
6
+ export declare class ChangedFilesError extends Error {
7
+ constructor(message: string);
8
+ }
9
+ /**
10
+ * Get the list of changed .md files between `base` and HEAD.
11
+ *
12
+ * Runs `git diff --name-only --diff-filter=ACM {base}...HEAD -- '*.md'`
13
+ * and resolves repo-relative paths to absolute paths.
14
+ *
15
+ * @param base - Git ref to diff against (e.g. "origin/main", "main")
16
+ * @returns Array of absolute file paths to changed .md files
17
+ * @throws ChangedFilesError if git commands fail
18
+ */
19
+ export declare function getChangedFiles(base: string): string[];
20
+ //# sourceMappingURL=changed-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"changed-files.d.ts","sourceRoot":"","sources":["../src/changed-files.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,0EAA0E;AAC1E,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAiCtD"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Git-aware file filtering for --changed-only mode.
3
+ * Returns absolute paths to .md files changed since a given base ref.
4
+ */
5
+ import { execFileSync } from 'node:child_process';
6
+ import { resolve } from 'node:path';
7
+ /** Error thrown when git operations fail (non-git dir, bad ref, etc.). */
8
+ export class ChangedFilesError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = 'ChangedFilesError';
12
+ }
13
+ }
14
+ /**
15
+ * Get the list of changed .md files between `base` and HEAD.
16
+ *
17
+ * Runs `git diff --name-only --diff-filter=ACM {base}...HEAD -- '*.md'`
18
+ * and resolves repo-relative paths to absolute paths.
19
+ *
20
+ * @param base - Git ref to diff against (e.g. "origin/main", "main")
21
+ * @returns Array of absolute file paths to changed .md files
22
+ * @throws ChangedFilesError if git commands fail
23
+ */
24
+ export function getChangedFiles(base) {
25
+ let repoRoot;
26
+ try {
27
+ repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
28
+ encoding: 'utf-8',
29
+ }).trim();
30
+ }
31
+ catch {
32
+ throw new ChangedFilesError('Not a git repository (or git is not installed)');
33
+ }
34
+ let diffOutput;
35
+ try {
36
+ diffOutput = execFileSync('git', ['diff', '--name-only', '--diff-filter=ACM', `${base}...HEAD`, '--', '*.md'], { encoding: 'utf-8', cwd: repoRoot }).trim();
37
+ }
38
+ catch (err) {
39
+ const msg = err instanceof Error ? err.message : String(err);
40
+ throw new ChangedFilesError(`Failed to get changed files (base: ${base}): ${msg}`);
41
+ }
42
+ if (diffOutput === '') {
43
+ return [];
44
+ }
45
+ return diffOutput
46
+ .split('\n')
47
+ .map((relativePath) => resolve(repoRoot, relativePath));
48
+ }
49
+ //# sourceMappingURL=changed-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"changed-files.js","sourceRoot":"","sources":["../src/changed-files.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,0EAA0E;AAC1E,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAC1C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,IAAI,QAAgB,CAAC;IACrB,IAAI,CAAC;QACH,QAAQ,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,iBAAiB,CAAC,EAAE;YAC/D,QAAQ,EAAE,OAAO;SAClB,CAAC,CAAC,IAAI,EAAE,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,iBAAiB,CACzB,gDAAgD,CACjD,CAAC;IACJ,CAAC;IAED,IAAI,UAAkB,CAAC;IACvB,IAAI,CAAC;QACH,UAAU,GAAG,YAAY,CACvB,KAAK,EACL,CAAC,MAAM,EAAE,aAAa,EAAE,mBAAmB,EAAE,GAAG,IAAI,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,EAC5E,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,CACrC,CAAC,IAAI,EAAE,CAAC;IACX,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,MAAM,IAAI,iBAAiB,CACzB,sCAAsC,IAAI,MAAM,GAAG,EAAE,CACtD,CAAC;IACJ,CAAC;IAED,IAAI,UAAU,KAAK,EAAE,EAAE,CAAC;QACtB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO,UAAU;SACd,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC;AAC5D,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { type FileType } from './types.js';
2
+ /**
3
+ * Classify a skill file by its path and frontmatter presence.
4
+ *
5
+ * Classification rules (in priority order):
6
+ * 1. Basename `SKILL.md` (case-sensitive) → skill
7
+ * 2. Basename `README.md` or `CLAUDE.md` (case-insensitive) → readme
8
+ * 3. Rightmost known directory segment → command | agent | context | skill
9
+ * 4. Agent path + hasFrontmatter false → legacy-agent
10
+ * 5. No match → unknown
11
+ */
12
+ export declare function classifyFile(filePath: string, hasFrontmatter: boolean): FileType;
13
+ //# sourceMappingURL=classify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../src/classify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAe3C;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,OAAO,GACtB,QAAQ,CAyDV"}
@@ -0,0 +1,72 @@
1
+ /** Known directory segments that map to file types. */
2
+ const SEGMENT_TYPE_MAP = new Map([
3
+ ['commands', 'command'],
4
+ ['agents', 'agent'],
5
+ ['context', 'context'],
6
+ ['skills', 'skill'],
7
+ ['reference', 'context'],
8
+ ['shared', 'context'],
9
+ ['examples', 'context'],
10
+ ['templates', 'context'],
11
+ ['themes', 'context'],
12
+ ]);
13
+ /**
14
+ * Classify a skill file by its path and frontmatter presence.
15
+ *
16
+ * Classification rules (in priority order):
17
+ * 1. Basename `SKILL.md` (case-sensitive) → skill
18
+ * 2. Basename `README.md` or `CLAUDE.md` (case-insensitive) → readme
19
+ * 3. Rightmost known directory segment → command | agent | context | skill
20
+ * 4. Agent path + hasFrontmatter false → legacy-agent
21
+ * 5. No match → unknown
22
+ */
23
+ export function classifyFile(filePath, hasFrontmatter) {
24
+ const basename = filePath.split('/').pop() ?? '';
25
+ // AC-3 (story-016): SKILL.md basename (case-sensitive) → skill
26
+ if (basename === 'SKILL.md') {
27
+ return 'skill';
28
+ }
29
+ // AC-9 (story-016): CLAUDE.md (case-insensitive) → readme
30
+ if (basename.toLowerCase() === 'claude.md') {
31
+ return 'readme';
32
+ }
33
+ // AC-4: README.md basename check (case-insensitive)
34
+ if (basename.toLowerCase() === 'readme.md') {
35
+ return 'readme';
36
+ }
37
+ // Split into segments and find the rightmost known directory segment (AC-7).
38
+ const segments = filePath.split('/');
39
+ let matchedType;
40
+ for (const segment of segments) {
41
+ const type = SEGMENT_TYPE_MAP.get(segment);
42
+ if (type !== undefined) {
43
+ matchedType = type;
44
+ // Don't break — keep scanning so the rightmost wins.
45
+ }
46
+ }
47
+ if (matchedType === undefined) {
48
+ return 'unknown'; // AC-9
49
+ }
50
+ // AC-5 / AC-6: legacy-agent reclassification
51
+ if (matchedType === 'agent') {
52
+ return hasFrontmatter ? 'agent' : 'legacy-agent';
53
+ }
54
+ // Non-SKILL.md markdown in skills/ dirs:
55
+ // - Directly in skills/name/ → readme
56
+ // - In skills/name/subdir/ where subdir is not in SEGMENT_TYPE_MAP → unknown
57
+ // (If the subdir were recognized, it would have overridden matchedType.)
58
+ if (matchedType === 'skill') {
59
+ const skillsIdx = segments.lastIndexOf('skills');
60
+ // afterSkills = [skillName, ...subdirs, basename]
61
+ const afterSkills = segments.slice(skillsIdx + 1);
62
+ if (afterSkills.length > 2) {
63
+ // File is nested in a subdirectory of the skill folder, and that
64
+ // subdirectory is unrecognized (otherwise matchedType would not
65
+ // still be 'skill'). Classify as unknown.
66
+ return 'unknown';
67
+ }
68
+ return 'readme';
69
+ }
70
+ return matchedType;
71
+ }
72
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../src/classify.ts"],"names":[],"mappings":"AAEA,uDAAuD;AACvD,MAAM,gBAAgB,GAAkC,IAAI,GAAG,CAAC;IAC9D,CAAC,UAAU,EAAE,SAAS,CAAC;IACvB,CAAC,QAAQ,EAAE,OAAO,CAAC;IACnB,CAAC,SAAS,EAAE,SAAS,CAAC;IACtB,CAAC,QAAQ,EAAE,OAAO,CAAC;IACnB,CAAC,WAAW,EAAE,SAAS,CAAC;IACxB,CAAC,QAAQ,EAAE,SAAS,CAAC;IACrB,CAAC,UAAU,EAAE,SAAS,CAAC;IACvB,CAAC,WAAW,EAAE,SAAS,CAAC;IACxB,CAAC,QAAQ,EAAE,SAAS,CAAC;CACtB,CAAC,CAAC;AAEH;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAC1B,QAAgB,EAChB,cAAuB;IAEvB,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;IAEjD,+DAA+D;IAC/D,IAAI,QAAQ,KAAK,UAAU,EAAE,CAAC;QAC5B,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,0DAA0D;IAC1D,IAAI,QAAQ,CAAC,WAAW,EAAE,KAAK,WAAW,EAAE,CAAC;QAC3C,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,oDAAoD;IACpD,IAAI,QAAQ,CAAC,WAAW,EAAE,KAAK,WAAW,EAAE,CAAC;QAC3C,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,6EAA6E;IAC7E,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,WAAiC,CAAC;IAEtC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,gBAAgB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC3C,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,WAAW,GAAG,IAAI,CAAC;YACnB,qDAAqD;QACvD,CAAC;IACH,CAAC;IAED,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,SAAS,CAAC,CAAC,OAAO;IAC3B,CAAC;IAED,6CAA6C;IAC7C,IAAI,WAAW,KAAK,OAAO,EAAE,CAAC;QAC5B,OAAO,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC;IACnD,CAAC;IAED,yCAAyC;IACzC,sCAAsC;IACtC,6EAA6E;IAC7E,2EAA2E;IAC3E,IAAI,WAAW,KAAK,OAAO,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,QAAQ,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QACjD,kDAAkD;QAClD,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;QAClD,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,iEAAiE;YACjE,gEAAgE;YAChE,0CAA0C;YAC1C,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,OAAO,WAAW,CAAC;AACrB,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}