this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add caveman compression and shadcn skills

Introduce caveman mode and related skills: caveman, caveman-compress,
caveman-commit, caveman-review, caveman-help, caveman-stats.
Implement caveman-compress CLI and Python scripts (detect, compress,
validate, benchmark) with backup and safety checks.
Add shadcn skill with docs, CLI reference, MCP metadata, rules, and icon
assets

+4134 -8
-1
skills/cavecrew
··· 1 - ../.agents/skills/cavecrew
+82
skills/cavecrew/SKILL.md
··· 1 + --- 2 + name: cavecrew 3 + description: > 4 + Decision guide for delegating to caveman-style subagents. Tells the main 5 + thread WHEN to spawn `cavecrew-investigator` (locate code), `cavecrew-builder` 6 + (1-2 file edit), or `cavecrew-reviewer` (diff review) instead of doing the 7 + work inline or using vanilla `Explore`. Subagent output is caveman-compressed 8 + so the tool-result injected back into main context is ~60% smaller — main 9 + context lasts longer across long sessions. 10 + Trigger: "delegate to subagent", "use cavecrew", "spawn investigator/builder/reviewer", 11 + "save context", "compressed agent output". 12 + --- 13 + 14 + Cavecrew = three subagent presets that emit caveman output. Same job as Anthropic defaults (`Explore`, edit-style agents, reviewer); difference is the tool-result they return is compressed, so main context shrinks per delegation. 15 + 16 + ## When to use cavecrew vs alternatives 17 + 18 + | Task | Use | 19 + |---|---| 20 + | "Where is X defined / what calls Y / list uses of Z" | `cavecrew-investigator` | 21 + | Same but you also want suggestions/architecture commentary | `Explore` (vanilla) | 22 + | Surgical edit, ≤2 files, scope obvious | `cavecrew-builder` | 23 + | New feature / 3+ files / cross-cutting refactor | Main thread or `feature-dev:code-architect` | 24 + | Review diff, branch, or file for bugs | `cavecrew-reviewer` | 25 + | Deep code review with rationale + alternatives | `Code Reviewer` (vanilla) | 26 + | One-line answer you already know | Main thread, no subagent | 27 + 28 + Rule of thumb: **if you'd want the subagent's output in 1/3 the tokens, pick cavecrew. If you'd want prose, pick vanilla.** 29 + 30 + ## Why this exists (the real win) 31 + 32 + Subagent tool results get injected into main context verbatim. A vanilla `Explore` that returns 2k tokens of prose costs 2k tokens of main-context budget every time. The same finding from `cavecrew-investigator` returns ~700 tokens. Across 20 delegations in one session that's the difference between context exhaustion and finishing the task. 33 + 34 + ## Output contracts 35 + 36 + What main thread can rely on per agent: 37 + 38 + **`cavecrew-investigator`** 39 + ``` 40 + <Header>: 41 + - path:line — `symbol` — short note 42 + totals: <counts>. 43 + ``` 44 + Or `No match.` Always file-path-first, line-number-attached, backticked symbols. Safe to grep with `path:\d+`. 45 + 46 + **`cavecrew-builder`** 47 + ``` 48 + <path:line-range> — <change ≤10 words>. 49 + verified: <re-read OK | mismatch @ path:line>. 50 + ``` 51 + Or one of: `too-big.` / `needs-confirm.` / `ambiguous.` / `regressed.` (terminal first token). 52 + 53 + **`cavecrew-reviewer`** 54 + ``` 55 + path:line: <emoji> <severity>: <problem>. <fix>. 56 + totals: N🔴 N🟡 N🔵 N❓ 57 + ``` 58 + Or `No issues.` Findings sorted file → line ascending. 59 + 60 + ## Chaining patterns 61 + 62 + **Locate → fix → verify** (most common): 63 + 1. `cavecrew-investigator` returns site list. 64 + 2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`. 65 + 3. `cavecrew-reviewer` audits the diff. 66 + 67 + **Parallel scout** (when investigation is broad): 68 + Spawn 2-3 `cavecrew-investigator` calls in one message (different angles: defs vs callers vs tests). Aggregate in main thread. 69 + 70 + **Single-shot edit** (when site is already known): 71 + Skip investigator. Hand exact path:line to `cavecrew-builder` directly. 72 + 73 + ## What NOT to do 74 + 75 + - Don't use `cavecrew-builder` when you don't already know the file. Spawn investigator first or main thread will eat tokens passing context. 76 + - Don't chain `cavecrew-investigator → cavecrew-builder` for a 5-file refactor. Builder will return `too-big.` and you'll have wasted a turn. 77 + - Don't ask `cavecrew-reviewer` for "general feedback" — it returns findings only, no architecture opinions. Use `Code Reviewer` for that. 78 + - Don't expect prose. Cavecrew output is structured, sometimes terse to the point of cryptic. If a human will read it directly, paraphrase. 79 + 80 + ## Auto-clarity (inherited) 81 + 82 + Subagents drop caveman → normal English for security warnings, irreversible-action confirmations, and any output where fragment ambiguity could be misread. Resume caveman after.
-1
skills/caveman
··· 1 - ../.agents/skills/caveman
-1
skills/caveman-commit
··· 1 - ../.agents/skills/caveman-commit
+65
skills/caveman-commit/SKILL.md
··· 1 + --- 2 + name: caveman-commit 3 + description: > 4 + Ultra-compressed commit message generator. Cuts noise from commit messages while preserving 5 + intent and reasoning. Conventional Commits format. Subject ≤50 chars, body only when "why" 6 + isn't obvious. Use when user says "write a commit", "commit message", "generate commit", 7 + "/commit", or invokes /caveman-commit. Auto-triggers when staging changes. 8 + --- 9 + 10 + Write commit messages terse and exact. Conventional Commits format. No fluff. Why over what. 11 + 12 + ## Rules 13 + 14 + **Subject line:** 15 + - `<type>(<scope>): <imperative summary>` — `<scope>` optional 16 + - Types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `chore`, `build`, `ci`, `style`, `revert` 17 + - Imperative mood: "add", "fix", "remove" — not "added", "adds", "adding" 18 + - ≤50 chars when possible, hard cap 72 19 + - No trailing period 20 + - Match project convention for capitalization after the colon 21 + 22 + **Body (only if needed):** 23 + - Skip entirely when subject is self-explanatory 24 + - Add body only for: non-obvious *why*, breaking changes, migration notes, linked issues 25 + - Wrap at 72 chars 26 + - Bullets `-` not `*` 27 + - Reference issues/PRs at end: `Closes #42`, `Refs #17` 28 + 29 + **What NEVER goes in:** 30 + - "This commit does X", "I", "we", "now", "currently" — the diff says what 31 + - "As requested by..." — use Co-authored-by trailer 32 + - "Generated with Claude Code" or any AI attribution 33 + - Emoji (unless project convention requires) 34 + - Restating the file name when scope already says it 35 + 36 + ## Examples 37 + 38 + Diff: new endpoint for user profile with body explaining the why 39 + - ❌ "feat: add a new endpoint to get user profile information from the database" 40 + - ✅ 41 + ``` 42 + feat(api): add GET /users/:id/profile 43 + 44 + Mobile client needs profile data without the full user payload 45 + to reduce LTE bandwidth on cold-launch screens. 46 + 47 + Closes #128 48 + ``` 49 + 50 + Diff: breaking API change 51 + - ✅ 52 + ``` 53 + feat(api)!: rename /v1/orders to /v1/checkout 54 + 55 + BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout 56 + before 2026-06-01. Old route returns 410 after that date. 57 + ``` 58 + 59 + ## Auto-Clarity 60 + 61 + Always include body for: breaking changes, security fixes, data migrations, anything reverting a prior commit. Never compress these into subject-only — future debuggers need the context. 62 + 63 + ## Boundaries 64 + 65 + Only generates the commit message. Does not run `git commit`, does not stage files, does not amend. Output the message as a code block ready to paste. "stop caveman-commit" or "normal mode": revert to verbose commit style.
-1
skills/caveman-compress
··· 1 - ../.agents/skills/caveman-compress
+163
skills/caveman-compress/README.md
··· 1 + <p align="center"> 2 + <img src="https://em-content.zobj.net/source/apple/391/rock_1faa8.png" width="80" /> 3 + </p> 4 + 5 + <h1 align="center">caveman-compress</h1> 6 + 7 + <p align="center"> 8 + <strong>shrink memory file. save token every session.</strong> 9 + </p> 10 + 11 + --- 12 + 13 + A Claude Code skill that compresses your project memory files (`CLAUDE.md`, todos, preferences) into caveman format — so every session loads fewer tokens automatically. 14 + 15 + Claude read `CLAUDE.md` on every session start. If file big, cost big. Caveman make file small. Cost go down forever. 16 + 17 + ## What It Do 18 + 19 + ``` 20 + /caveman:compress CLAUDE.md 21 + ``` 22 + 23 + ``` 24 + CLAUDE.md ← compressed (Claude reads this — fewer tokens every session) 25 + CLAUDE.original.md ← human-readable backup (you edit this) 26 + ``` 27 + 28 + Original never lost. You can read and edit `.original.md`. Run skill again to re-compress after edits. 29 + 30 + ## Benchmarks 31 + 32 + Real results on real project files: 33 + 34 + | File | Original | Compressed | Saved | 35 + |------|----------:|----------:|------:| 36 + | `claude-md-preferences.md` | 706 | 285 | **59.6%** | 37 + | `project-notes.md` | 1145 | 535 | **53.3%** | 38 + | `claude-md-project.md` | 1122 | 636 | **43.3%** | 39 + | `todo-list.md` | 627 | 388 | **38.1%** | 40 + | `mixed-with-code.md` | 888 | 560 | **36.9%** | 41 + | **Average** | **898** | **481** | **46%** | 42 + 43 + All validations passed ✅ — headings, code blocks, URLs, file paths preserved exactly. 44 + 45 + ## Before / After 46 + 47 + <table> 48 + <tr> 49 + <td width="50%"> 50 + 51 + ### 📄 Original (706 tokens) 52 + 53 + > "I strongly prefer TypeScript with strict mode enabled for all new code. Please don't use `any` type unless there's genuinely no way around it, and if you do, leave a comment explaining the reasoning. I find that taking the time to properly type things catches a lot of bugs before they ever make it to runtime." 54 + 55 + </td> 56 + <td width="50%"> 57 + 58 + ### 🪨 Caveman (285 tokens) 59 + 60 + > "Prefer TypeScript strict mode always. No `any` unless unavoidable — comment why if used. Proper types catch bugs early." 61 + 62 + </td> 63 + </tr> 64 + </table> 65 + 66 + **Same instructions. 60% fewer tokens. Every. Single. Session.** 67 + 68 + ## Security 69 + 70 + `caveman-compress` is flagged as Snyk High Risk due to subprocess and file I/O patterns detected by static analysis. This is a false positive — see [SECURITY.md](./SECURITY.md) for a full explanation of what the skill does and does not do. 71 + 72 + ## Install 73 + 74 + Compress is built in with the `caveman` plugin. Install `caveman` once, then use `/caveman:compress`. 75 + 76 + If you need local files, the compress skill lives at: 77 + 78 + ```bash 79 + caveman-compress/ 80 + ``` 81 + 82 + **Requires:** Python 3.10+ 83 + 84 + ## Usage 85 + 86 + ``` 87 + /caveman:compress <filepath> 88 + ``` 89 + 90 + Examples: 91 + ``` 92 + /caveman:compress CLAUDE.md 93 + /caveman:compress docs/preferences.md 94 + /caveman:compress todos.md 95 + ``` 96 + 97 + ### What files work 98 + 99 + | Type | Compress? | 100 + |------|-----------| 101 + | `.md`, `.txt`, `.rst`, `.typ`, `.typst`, `.tex` | ✅ Yes | 102 + | Extensionless natural language | ✅ Yes | 103 + | `.py`, `.js`, `.ts`, `.json`, `.yaml` | ❌ Skip (code/config) | 104 + | `*.original.md` | ❌ Skip (backup files) | 105 + 106 + ## How It Work 107 + 108 + ``` 109 + /caveman:compress CLAUDE.md 110 + 111 + detect file type (no tokens) 112 + 113 + Claude compresses (tokens — one call) 114 + 115 + validate output (no tokens) 116 + checks: headings, code blocks, URLs, file paths, bullets 117 + 118 + if errors: Claude fixes cherry-picked issues only (tokens — targeted fix) 119 + does NOT recompress — only patches broken parts 120 + 121 + retry up to 2 times 122 + 123 + write compressed → CLAUDE.md 124 + write original → CLAUDE.original.md 125 + ``` 126 + 127 + Only two things use tokens: initial compression + targeted fix if validation fails. Everything else is local Python. 128 + 129 + ## What Is Preserved 130 + 131 + Caveman compress natural language. It never touch: 132 + 133 + - Code blocks (` ``` ` fenced or indented) 134 + - Inline code (`` `backtick content` ``) 135 + - URLs and links 136 + - File paths (`/src/components/...`) 137 + - Commands (`npm install`, `git commit`) 138 + - Technical terms, library names, API names 139 + - Headings (exact text preserved) 140 + - Tables (structure preserved, cell text compressed) 141 + - Dates, version numbers, numeric values 142 + 143 + ## Why This Matter 144 + 145 + `CLAUDE.md` loads on **every session start**. A 1000-token project memory file costs tokens every single time you open a project. Over 100 sessions that's 100,000 tokens of overhead — just for context you already wrote. 146 + 147 + Caveman cut that by ~46% on average. Same instructions. Same accuracy. Less waste. 148 + 149 + ``` 150 + ┌────────────────────────────────────────────┐ 151 + │ TOKEN SAVINGS PER FILE █████ 46% │ 152 + │ SESSIONS THAT BENEFIT ██████████ 100% │ 153 + │ INFORMATION PRESERVED ██████████ 100% │ 154 + │ SETUP TIME █ 1x │ 155 + └────────────────────────────────────────────┘ 156 + ``` 157 + 158 + ## Part of Caveman 159 + 160 + This skill is part of the [caveman](https://github.com/JuliusBrussee/caveman) toolkit — making Claude use fewer tokens without losing accuracy. 161 + 162 + - **caveman** — make Claude *speak* like caveman (cuts response tokens ~65%) 163 + - **caveman-compress** — make Claude *read* less (cuts context tokens ~46%)
+31
skills/caveman-compress/SECURITY.md
··· 1 + # Security 2 + 3 + ## Snyk High Risk Rating 4 + 5 + `caveman-compress` receives a Snyk High Risk rating due to static analysis heuristics. This document explains what the skill does and does not do. 6 + 7 + ### What triggers the rating 8 + 9 + 1. **subprocess usage**: The skill calls the `claude` CLI via `subprocess.run()` as a fallback when `ANTHROPIC_API_KEY` is not set. The subprocess call uses a fixed argument list — no shell interpolation occurs. User file content is passed via stdin, not as a shell argument. 10 + 11 + 2. **File read/write**: The skill reads the file the user explicitly points it at, compresses it, and writes the result back to the same path. A `.original.md` backup is saved alongside it. No files outside the user-specified path are read or written. 12 + 13 + ### What the skill does NOT do 14 + 15 + - Does not execute user file content as code 16 + - Does not make network requests except to Anthropic's API (via SDK or CLI) 17 + - Does not access files outside the path the user provides 18 + - Does not use shell=True or string interpolation in subprocess calls 19 + - Does not collect or transmit any data beyond the file being compressed 20 + 21 + ### Auth behavior 22 + 23 + If `ANTHROPIC_API_KEY` is set, the skill uses the Anthropic Python SDK directly (no subprocess). If not set, it falls back to the `claude` CLI, which uses the user's existing Claude desktop authentication. 24 + 25 + ### File size limit 26 + 27 + Files larger than 500KB are rejected before any API call is made. 28 + 29 + ### Reporting a vulnerability 30 + 31 + If you believe you've found a genuine security issue, please open a GitHub issue with the label `security`.
+111
skills/caveman-compress/SKILL.md
··· 1 + --- 2 + name: caveman-compress 3 + description: > 4 + Compress natural language memory files (CLAUDE.md, todos, preferences) into caveman format 5 + to save input tokens. Preserves all technical substance, code, URLs, and structure. 6 + Compressed version overwrites the original file. Human-readable backup saved as FILE.original.md. 7 + Trigger: /caveman:compress FILEPATH or "compress memory file" 8 + --- 9 + 10 + # Caveman Compress 11 + 12 + ## Purpose 13 + 14 + Compress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`. 15 + 16 + ## Trigger 17 + 18 + `/caveman:compress <filepath>` or when user asks to compress a memory file. 19 + 20 + ## Process 21 + 22 + 1. The compression scripts live in `caveman-compress/scripts/` (adjacent to this SKILL.md). If the path is not immediately available, search for `caveman-compress/scripts/__main__.py`. 23 + 24 + 2. Run: 25 + 26 + cd caveman-compress && python3 -m scripts <absolute_filepath> 27 + 28 + 3. The CLI will: 29 + - detect file type (no tokens) 30 + - call Claude to compress 31 + - validate output (no tokens) 32 + - if errors: cherry-pick fix with Claude (targeted fixes only, no recompression) 33 + - retry up to 2 times 34 + - if still failing after 2 retries: report error to user, leave original file untouched 35 + 36 + 4. Return result to user 37 + 38 + ## Compression Rules 39 + 40 + ### Remove 41 + - Articles: a, an, the 42 + - Filler: just, really, basically, actually, simply, essentially, generally 43 + - Pleasantries: "sure", "certainly", "of course", "happy to", "I'd recommend" 44 + - Hedging: "it might be worth", "you could consider", "it would be good to" 45 + - Redundant phrasing: "in order to" → "to", "make sure to" → "ensure", "the reason is because" → "because" 46 + - Connective fluff: "however", "furthermore", "additionally", "in addition" 47 + 48 + ### Preserve EXACTLY (never modify) 49 + - Code blocks (fenced ``` and indented) 50 + - Inline code (`backtick content`) 51 + - URLs and links (full URLs, markdown links) 52 + - File paths (`/src/components/...`, `./config.yaml`) 53 + - Commands (`npm install`, `git commit`, `docker build`) 54 + - Technical terms (library names, API names, protocols, algorithms) 55 + - Proper nouns (project names, people, companies) 56 + - Dates, version numbers, numeric values 57 + - Environment variables (`$HOME`, `NODE_ENV`) 58 + 59 + ### Preserve Structure 60 + - All markdown headings (keep exact heading text, compress body below) 61 + - Bullet point hierarchy (keep nesting level) 62 + - Numbered lists (keep numbering) 63 + - Tables (compress cell text, keep structure) 64 + - Frontmatter/YAML headers in markdown files 65 + 66 + ### Compress 67 + - Use short synonyms: "big" not "extensive", "fix" not "implement a solution for", "use" not "utilize" 68 + - Fragments OK: "Run tests before commit" not "You should always run tests before committing" 69 + - Drop "you should", "make sure to", "remember to" — just state the action 70 + - Merge redundant bullets that say the same thing differently 71 + - Keep one example where multiple examples show the same pattern 72 + 73 + CRITICAL RULE: 74 + Anything inside ``` ... ``` must be copied EXACTLY. 75 + Do not: 76 + - remove comments 77 + - remove spacing 78 + - reorder lines 79 + - shorten commands 80 + - simplify anything 81 + 82 + Inline code (`...`) must be preserved EXACTLY. 83 + Do not modify anything inside backticks. 84 + 85 + If file contains code blocks: 86 + - Treat code blocks as read-only regions 87 + - Only compress text outside them 88 + - Do not merge sections around code 89 + 90 + ## Pattern 91 + 92 + Original: 93 + > You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production. 94 + 95 + Compressed: 96 + > Run tests before push to main. Catch bugs early, prevent broken prod deploys. 97 + 98 + Original: 99 + > The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens. 100 + 101 + Compressed: 102 + > Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens. 103 + 104 + ## Boundaries 105 + 106 + - ONLY compress natural language files (.md, .txt, .typ, .typst, .tex, extensionless) 107 + - NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh 108 + - If file has mixed content (prose + code), compress ONLY the prose sections 109 + - If unsure whether something is code or prose, leave it unchanged 110 + - Original file is backed up as FILE.original.md before overwriting 111 + - Never compress FILE.original.md (skip it)
+9
skills/caveman-compress/scripts/__init__.py
··· 1 + """Caveman compress scripts. 2 + 3 + This package provides tools to compress natural language markdown files 4 + into caveman format to save input tokens. 5 + """ 6 + 7 + __all__ = ["cli", "compress", "detect", "validate"] 8 + 9 + __version__ = "1.0.0"
+3
skills/caveman-compress/scripts/__main__.py
··· 1 + from .cli import main 2 + 3 + main()
+78
skills/caveman-compress/scripts/benchmark.py
··· 1 + #!/usr/bin/env python3 2 + from pathlib import Path 3 + import sys 4 + 5 + # Support both direct execution and module import 6 + try: 7 + from .validate import validate 8 + except ImportError: 9 + sys.path.insert(0, str(Path(__file__).parent)) 10 + from validate import validate 11 + 12 + try: 13 + import tiktoken 14 + _enc = tiktoken.get_encoding("o200k_base") 15 + except ImportError: 16 + _enc = None 17 + 18 + 19 + def count_tokens(text): 20 + if _enc is None: 21 + return len(text.split()) # fallback: word count 22 + return len(_enc.encode(text)) 23 + 24 + 25 + def benchmark_pair(orig_path: Path, comp_path: Path): 26 + orig_text = orig_path.read_text() 27 + comp_text = comp_path.read_text() 28 + 29 + orig_tokens = count_tokens(orig_text) 30 + comp_tokens = count_tokens(comp_text) 31 + saved = 100 * (orig_tokens - comp_tokens) / orig_tokens if orig_tokens > 0 else 0.0 32 + result = validate(orig_path, comp_path) 33 + 34 + return (comp_path.name, orig_tokens, comp_tokens, saved, result.is_valid) 35 + 36 + 37 + def print_table(rows): 38 + print("\n| File | Original | Compressed | Saved % | Valid |") 39 + print("|------|----------|------------|---------|-------|") 40 + for r in rows: 41 + print(f"| {r[0]} | {r[1]} | {r[2]} | {r[3]:.1f}% | {'✅' if r[4] else '❌'} |") 42 + 43 + 44 + def main(): 45 + # Direct file pair: python3 benchmark.py original.md compressed.md 46 + if len(sys.argv) == 3: 47 + orig = Path(sys.argv[1]).resolve() 48 + comp = Path(sys.argv[2]).resolve() 49 + if not orig.exists(): 50 + print(f"❌ Not found: {orig}") 51 + sys.exit(1) 52 + if not comp.exists(): 53 + print(f"❌ Not found: {comp}") 54 + sys.exit(1) 55 + print_table([benchmark_pair(orig, comp)]) 56 + return 57 + 58 + # Glob mode: repo_root/tests/caveman-compress/ 59 + tests_dir = Path(__file__).parent.parent.parent / "tests" / "caveman-compress" 60 + if not tests_dir.exists(): 61 + print(f"❌ Tests dir not found: {tests_dir}") 62 + sys.exit(1) 63 + 64 + rows = [] 65 + for orig in sorted(tests_dir.glob("*.original.md")): 66 + comp = orig.with_name(orig.stem.removesuffix(".original") + ".md") 67 + if comp.exists(): 68 + rows.append(benchmark_pair(orig, comp)) 69 + 70 + if not rows: 71 + print("No compressed file pairs found.") 72 + return 73 + 74 + print_table(rows) 75 + 76 + 77 + if __name__ == "__main__": 78 + main()
+85
skills/caveman-compress/scripts/cli.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Caveman Compress CLI 4 + 5 + Usage: 6 + caveman <filepath> 7 + """ 8 + 9 + import sys 10 + 11 + # Force UTF-8 on stdout/stderr before any code can print. Windows consoles 12 + # default to cp1252 and crash on the ❌ glyphs in error/validation branches, 13 + # masking the real error and leaving the user with a half-compressed file. 14 + for _stream in (sys.stdout, sys.stderr): 15 + reconfigure = getattr(_stream, "reconfigure", None) 16 + if callable(reconfigure): 17 + try: 18 + reconfigure(encoding="utf-8", errors="replace") 19 + except Exception: 20 + pass 21 + 22 + from pathlib import Path 23 + 24 + from .compress import compress_file 25 + from .detect import detect_file_type, should_compress 26 + 27 + 28 + def print_usage(): 29 + print("Usage: caveman <filepath>") 30 + 31 + 32 + def main(): 33 + if len(sys.argv) != 2: 34 + print_usage() 35 + sys.exit(1) 36 + 37 + filepath = Path(sys.argv[1]) 38 + 39 + # Check file exists 40 + if not filepath.exists(): 41 + print(f"❌ File not found: {filepath}") 42 + sys.exit(1) 43 + 44 + if not filepath.is_file(): 45 + print(f"❌ Not a file: {filepath}") 46 + sys.exit(1) 47 + 48 + filepath = filepath.resolve() 49 + 50 + # Detect file type 51 + file_type = detect_file_type(filepath) 52 + 53 + print(f"Detected: {file_type}") 54 + 55 + # Check if compressible 56 + if not should_compress(filepath): 57 + print("Skipping: file is not natural language (code/config)") 58 + sys.exit(0) 59 + 60 + print("Starting caveman compression...\n") 61 + 62 + try: 63 + success = compress_file(filepath) 64 + 65 + if success: 66 + print("\nCompression completed successfully") 67 + backup_path = filepath.with_name(filepath.stem + ".original.md") 68 + print(f"Compressed: {filepath}") 69 + print(f"Original: {backup_path}") 70 + sys.exit(0) 71 + else: 72 + print("\n❌ Compression failed after retries") 73 + sys.exit(2) 74 + 75 + except KeyboardInterrupt: 76 + print("\nInterrupted by user") 77 + sys.exit(130) 78 + 79 + except Exception as e: 80 + print(f"\n❌ Error: {e}") 81 + sys.exit(1) 82 + 83 + 84 + if __name__ == "__main__": 85 + main()
+254
skills/caveman-compress/scripts/compress.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Caveman Memory Compression Orchestrator 4 + 5 + Usage: 6 + python scripts/compress.py <filepath> 7 + """ 8 + 9 + import os 10 + import re 11 + import subprocess 12 + from pathlib import Path 13 + from typing import List 14 + 15 + OUTER_FENCE_REGEX = re.compile( 16 + r"\A\s*(`{3,}|~{3,})[^\n]*\n(.*)\n\1\s*\Z", re.DOTALL 17 + ) 18 + 19 + # Filenames and paths that almost certainly hold secrets or PII. Compressing 20 + # them ships raw bytes to the Anthropic API — a third-party data boundary that 21 + # developers on sensitive codebases cannot cross. detect.py already skips .env 22 + # by extension, but credentials.md / secrets.txt / ~/.aws/credentials would 23 + # slip through the natural-language filter. This is a hard refuse before read. 24 + SENSITIVE_BASENAME_REGEX = re.compile( 25 + r"(?ix)^(" 26 + r"\.env(\..+)?" 27 + r"|\.netrc" 28 + r"|credentials(\..+)?" 29 + r"|secrets?(\..+)?" 30 + r"|passwords?(\..+)?" 31 + r"|id_(rsa|dsa|ecdsa|ed25519)(\.pub)?" 32 + r"|authorized_keys" 33 + r"|known_hosts" 34 + r"|.*\.(pem|key|p12|pfx|crt|cer|jks|keystore|asc|gpg)" 35 + r")$" 36 + ) 37 + 38 + SENSITIVE_PATH_COMPONENTS = frozenset({".ssh", ".aws", ".gnupg", ".kube", ".docker"}) 39 + 40 + SENSITIVE_NAME_TOKENS = ( 41 + "secret", "credential", "password", "passwd", 42 + "apikey", "accesskey", "token", "privatekey", 43 + ) 44 + 45 + 46 + def is_sensitive_path(filepath: Path) -> bool: 47 + """Heuristic denylist for files that must never be shipped to a third-party API.""" 48 + name = filepath.name 49 + if SENSITIVE_BASENAME_REGEX.match(name): 50 + return True 51 + lowered_parts = {p.lower() for p in filepath.parts} 52 + if lowered_parts & SENSITIVE_PATH_COMPONENTS: 53 + return True 54 + # Normalize separators so "api-key" and "api_key" both match "apikey". 55 + lower = re.sub(r"[_\-\s.]", "", name.lower()) 56 + return any(tok in lower for tok in SENSITIVE_NAME_TOKENS) 57 + 58 + 59 + def strip_llm_wrapper(text: str) -> str: 60 + """Strip outer ```markdown ... ``` fence when it wraps the entire output.""" 61 + m = OUTER_FENCE_REGEX.match(text) 62 + if m: 63 + return m.group(2) 64 + return text 65 + 66 + from .detect import should_compress 67 + from .validate import validate 68 + 69 + MAX_RETRIES = 2 70 + 71 + 72 + # ---------- Claude Calls ---------- 73 + 74 + 75 + def call_claude(prompt: str) -> str: 76 + api_key = os.environ.get("ANTHROPIC_API_KEY") 77 + if api_key: 78 + try: 79 + import anthropic 80 + 81 + client = anthropic.Anthropic(api_key=api_key) 82 + msg = client.messages.create( 83 + model=os.environ.get("CAVEMAN_MODEL", "claude-sonnet-4-5"), 84 + max_tokens=8192, 85 + messages=[{"role": "user", "content": prompt}], 86 + ) 87 + return strip_llm_wrapper(msg.content[0].text.strip()) 88 + except ImportError: 89 + pass # anthropic not installed, fall back to CLI 90 + # Fallback: use claude CLI (handles desktop auth) 91 + try: 92 + result = subprocess.run( 93 + ["claude", "--print"], 94 + input=prompt, 95 + text=True, 96 + capture_output=True, 97 + check=True, 98 + ) 99 + return strip_llm_wrapper(result.stdout.strip()) 100 + except subprocess.CalledProcessError as e: 101 + raise RuntimeError(f"Claude call failed:\n{e.stderr}") 102 + 103 + 104 + def build_compress_prompt(original: str) -> str: 105 + return f""" 106 + Compress this markdown into caveman format. 107 + 108 + STRICT RULES: 109 + - Do NOT modify anything inside ``` code blocks 110 + - Do NOT modify anything inside inline backticks 111 + - Preserve ALL URLs exactly 112 + - Preserve ALL headings exactly 113 + - Preserve file paths and commands 114 + - Return ONLY the compressed markdown body — do NOT wrap the entire output in a ```markdown fence or any other fence. Inner code blocks from the original stay as-is; do not add a new outer fence around the whole file. 115 + 116 + Only compress natural language. 117 + 118 + TEXT: 119 + {original} 120 + """ 121 + 122 + 123 + def build_fix_prompt(original: str, compressed: str, errors: List[str]) -> str: 124 + errors_str = "\n".join(f"- {e}" for e in errors) 125 + return f"""You are fixing a caveman-compressed markdown file. Specific validation errors were found. 126 + 127 + CRITICAL RULES: 128 + - DO NOT recompress or rephrase the file 129 + - ONLY fix the listed errors — leave everything else exactly as-is 130 + - The ORIGINAL is provided as reference only (to restore missing content) 131 + - Preserve caveman style in all untouched sections 132 + 133 + ERRORS TO FIX: 134 + {errors_str} 135 + 136 + HOW TO FIX: 137 + - Missing URL: find it in ORIGINAL, restore it exactly where it belongs in COMPRESSED 138 + - Code block mismatch: find the exact code block in ORIGINAL, restore it in COMPRESSED 139 + - Heading mismatch: restore the exact heading text from ORIGINAL into COMPRESSED 140 + - Do not touch any section not mentioned in the errors 141 + 142 + ORIGINAL (reference only): 143 + {original} 144 + 145 + COMPRESSED (fix this): 146 + {compressed} 147 + 148 + Return ONLY the fixed compressed file. No explanation. 149 + """ 150 + 151 + 152 + # ---------- Core Logic ---------- 153 + 154 + 155 + def compress_file(filepath: Path) -> bool: 156 + # Resolve and validate path 157 + filepath = filepath.resolve() 158 + MAX_FILE_SIZE = 500_000 # 500KB 159 + if not filepath.exists(): 160 + raise FileNotFoundError(f"File not found: {filepath}") 161 + if filepath.stat().st_size > MAX_FILE_SIZE: 162 + raise ValueError(f"File too large to compress safely (max 500KB): {filepath}") 163 + 164 + # Refuse files that look like they contain secrets or PII. Compressing ships 165 + # the raw bytes to the Anthropic API — a third-party boundary — so we fail 166 + # loudly rather than silently exfiltrate credentials or keys. Override is 167 + # intentional: the user must rename the file if the heuristic is wrong. 168 + if is_sensitive_path(filepath): 169 + raise ValueError( 170 + f"Refusing to compress {filepath}: filename looks sensitive " 171 + "(credentials, keys, secrets, or known private paths). " 172 + "Compression sends file contents to the Anthropic API. " 173 + "Rename the file if this is a false positive." 174 + ) 175 + 176 + print(f"Processing: {filepath}") 177 + 178 + if not should_compress(filepath): 179 + print("Skipping (not natural language)") 180 + return False 181 + 182 + original_text = filepath.read_text(errors="ignore") 183 + backup_path = filepath.with_name(filepath.stem + ".original.md") 184 + 185 + if not original_text.strip(): 186 + print("❌ Refusing to compress: file is empty or whitespace-only.") 187 + return False 188 + 189 + # Check if backup already exists to prevent accidental overwriting 190 + if backup_path.exists(): 191 + print(f"⚠️ Backup file already exists: {backup_path}") 192 + print("The original backup may contain important content.") 193 + print("Aborting to prevent data loss. Please remove or rename the backup file if you want to proceed.") 194 + return False 195 + 196 + # Step 1: Compress 197 + print("Compressing with Claude...") 198 + compressed = call_claude(build_compress_prompt(original_text)) 199 + 200 + if compressed is None or not compressed.strip(): 201 + print("❌ Compression aborted: Claude returned an empty response.") 202 + print(" Original file is untouched (no backup created).") 203 + return False 204 + 205 + if compressed.strip() == original_text.strip(): 206 + print("❌ Compression aborted: output is identical to input.") 207 + print(" Likely causes: Claude refused, returned the prompt verbatim, or the file is") 208 + print(" already in caveman form. Original file is untouched (no backup created).") 209 + return False 210 + 211 + # Save original as backup, then verify the backup readback before 212 + # touching the input file. If the filesystem dropped bytes (encoding, 213 + # antivirus, disk full), unlink the bad backup and abort instead of 214 + # leaving the user with a corrupt backup + compressed primary. 215 + backup_path.write_text(original_text) 216 + backup_readback = backup_path.read_text(errors="ignore") 217 + if backup_readback != original_text: 218 + print(f"❌ Backup write verification failed: {backup_path}") 219 + print(" In-memory original differs from on-disk backup. Aborting before touching the input file.") 220 + try: 221 + backup_path.unlink() 222 + except OSError: 223 + pass 224 + return False 225 + filepath.write_text(compressed) 226 + 227 + # Step 2: Validate + Retry 228 + for attempt in range(MAX_RETRIES): 229 + print(f"\nValidation attempt {attempt + 1}") 230 + 231 + result = validate(backup_path, filepath) 232 + 233 + if result.is_valid: 234 + print("Validation passed") 235 + break 236 + 237 + print("❌ Validation failed:") 238 + for err in result.errors: 239 + print(f" - {err}") 240 + 241 + if attempt == MAX_RETRIES - 1: 242 + # Restore original on failure 243 + filepath.write_text(original_text) 244 + backup_path.unlink(missing_ok=True) 245 + print("❌ Failed after retries — original restored") 246 + return False 247 + 248 + print("Fixing with Claude...") 249 + compressed = call_claude( 250 + build_fix_prompt(original_text, compressed, result.errors) 251 + ) 252 + filepath.write_text(compressed) 253 + 254 + return True
+121
skills/caveman-compress/scripts/detect.py
··· 1 + #!/usr/bin/env python3 2 + """Detect whether a file is natural language (compressible) or code/config (skip).""" 3 + 4 + import json 5 + import re 6 + from pathlib import Path 7 + 8 + # Extensions that are natural language and compressible 9 + COMPRESSIBLE_EXTENSIONS = {".md", ".txt", ".markdown", ".rst", ".typ", ".typst", ".tex"} 10 + 11 + # Extensions that are code/config and should be skipped 12 + SKIP_EXTENSIONS = { 13 + ".py", ".js", ".ts", ".tsx", ".jsx", ".json", ".yaml", ".yml", 14 + ".toml", ".env", ".lock", ".css", ".scss", ".html", ".xml", 15 + ".sql", ".sh", ".bash", ".zsh", ".go", ".rs", ".java", ".c", 16 + ".cpp", ".h", ".hpp", ".rb", ".php", ".swift", ".kt", ".lua", 17 + ".dockerfile", ".makefile", ".csv", ".ini", ".cfg", 18 + } 19 + 20 + # Patterns that indicate a line is code 21 + CODE_PATTERNS = [ 22 + re.compile(r"^\s*(import |from .+ import |require\(|const |let |var )"), 23 + re.compile(r"^\s*(def |class |function |async function |export )"), 24 + re.compile(r"^\s*(if\s*\(|for\s*\(|while\s*\(|switch\s*\(|try\s*\{)"), 25 + re.compile(r"^\s*[\}\]\);]+\s*$"), # closing braces/brackets 26 + re.compile(r"^\s*@\w+"), # decorators/annotations 27 + re.compile(r'^\s*"[^"]+"\s*:\s*'), # JSON-like key-value 28 + re.compile(r"^\s*\w+\s*=\s*[{\[\(\"']"), # assignment with literal 29 + ] 30 + 31 + 32 + def _is_code_line(line: str) -> bool: 33 + """Check if a line looks like code.""" 34 + return any(p.match(line) for p in CODE_PATTERNS) 35 + 36 + 37 + def _is_json_content(text: str) -> bool: 38 + """Check if content is valid JSON.""" 39 + try: 40 + json.loads(text) 41 + return True 42 + except (json.JSONDecodeError, ValueError): 43 + return False 44 + 45 + 46 + def _is_yaml_content(lines: list[str]) -> bool: 47 + """Heuristic: check if content looks like YAML.""" 48 + yaml_indicators = 0 49 + for line in lines[:30]: 50 + stripped = line.strip() 51 + if stripped.startswith("---"): 52 + yaml_indicators += 1 53 + elif re.match(r"^\w[\w\s]*:\s", stripped): 54 + yaml_indicators += 1 55 + elif stripped.startswith("- ") and ":" in stripped: 56 + yaml_indicators += 1 57 + # If most non-empty lines look like YAML 58 + non_empty = sum(1 for l in lines[:30] if l.strip()) 59 + return non_empty > 0 and yaml_indicators / non_empty > 0.6 60 + 61 + 62 + def detect_file_type(filepath: Path) -> str: 63 + """Classify a file as 'natural_language', 'code', 'config', or 'unknown'. 64 + 65 + Returns: 66 + One of: 'natural_language', 'code', 'config', 'unknown' 67 + """ 68 + ext = filepath.suffix.lower() 69 + 70 + # Extension-based classification 71 + if ext in COMPRESSIBLE_EXTENSIONS: 72 + return "natural_language" 73 + if ext in SKIP_EXTENSIONS: 74 + return "code" if ext not in {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".env"} else "config" 75 + 76 + # Extensionless files (like CLAUDE.md, TODO) — check content 77 + if not ext: 78 + try: 79 + text = filepath.read_text(errors="ignore") 80 + except (OSError, PermissionError): 81 + return "unknown" 82 + 83 + lines = text.splitlines()[:50] 84 + 85 + if _is_json_content(text[:10000]): 86 + return "config" 87 + if _is_yaml_content(lines): 88 + return "config" 89 + 90 + code_lines = sum(1 for l in lines if l.strip() and _is_code_line(l)) 91 + non_empty = sum(1 for l in lines if l.strip()) 92 + if non_empty > 0 and code_lines / non_empty > 0.4: 93 + return "code" 94 + 95 + return "natural_language" 96 + 97 + return "unknown" 98 + 99 + 100 + def should_compress(filepath: Path) -> bool: 101 + """Return True if the file is natural language and should be compressed.""" 102 + if not filepath.is_file(): 103 + return False 104 + # Skip backup files 105 + if filepath.name.endswith(".original.md"): 106 + return False 107 + return detect_file_type(filepath) == "natural_language" 108 + 109 + 110 + if __name__ == "__main__": 111 + import sys 112 + 113 + if len(sys.argv) < 2: 114 + print("Usage: python detect.py <file1> [file2] ...") 115 + sys.exit(1) 116 + 117 + for path_str in sys.argv[1:]: 118 + p = Path(path_str).resolve() 119 + file_type = detect_file_type(p) 120 + compress = should_compress(p) 121 + print(f" {p.name:30s} type={file_type:20s} compress={compress}")
+213
skills/caveman-compress/scripts/validate.py
··· 1 + #!/usr/bin/env python3 2 + import re 3 + from collections import Counter 4 + from pathlib import Path 5 + 6 + URL_REGEX = re.compile(r"https?://[^\s)]+") 7 + FENCE_OPEN_REGEX = re.compile(r"^(\s{0,3})(`{3,}|~{3,})(.*)$") 8 + HEADING_REGEX = re.compile(r"^(#{1,6})\s+(.*)", re.MULTILINE) 9 + BULLET_REGEX = re.compile(r"^\s*[-*+]\s+", re.MULTILINE) 10 + 11 + # crude but effective path detection 12 + # Requires either a path prefix (./ ../ / or drive letter) or a slash/backslash within the match 13 + PATH_REGEX = re.compile(r"(?:\./|\.\./|/|[A-Za-z]:\\)[\w\-/\\\.]+|[\w\-\.]+[/\\][\w\-/\\\.]+") 14 + 15 + 16 + class ValidationResult: 17 + def __init__(self): 18 + self.is_valid = True 19 + self.errors = [] 20 + self.warnings = [] 21 + 22 + def add_error(self, msg): 23 + self.is_valid = False 24 + self.errors.append(msg) 25 + 26 + def add_warning(self, msg): 27 + self.warnings.append(msg) 28 + 29 + 30 + def read_file(path: Path) -> str: 31 + return path.read_text(errors="ignore") 32 + 33 + 34 + # ---------- Extractors ---------- 35 + 36 + 37 + def extract_headings(text): 38 + return [(level, title.strip()) for level, title in HEADING_REGEX.findall(text)] 39 + 40 + 41 + def extract_code_blocks(text): 42 + """Line-based fenced code block extractor. 43 + 44 + Handles ``` and ~~~ fences with variable length (CommonMark: closing 45 + fence must use same char and be at least as long as opening). Supports 46 + nested fences (e.g. an outer 4-backtick block wrapping inner 3-backtick 47 + content). 48 + """ 49 + blocks = [] 50 + lines = text.split("\n") 51 + i = 0 52 + n = len(lines) 53 + while i < n: 54 + m = FENCE_OPEN_REGEX.match(lines[i]) 55 + if not m: 56 + i += 1 57 + continue 58 + fence_char = m.group(2)[0] 59 + fence_len = len(m.group(2)) 60 + open_line = lines[i] 61 + block_lines = [open_line] 62 + i += 1 63 + closed = False 64 + while i < n: 65 + close_m = FENCE_OPEN_REGEX.match(lines[i]) 66 + if ( 67 + close_m 68 + and close_m.group(2)[0] == fence_char 69 + and len(close_m.group(2)) >= fence_len 70 + and close_m.group(3).strip() == "" 71 + ): 72 + block_lines.append(lines[i]) 73 + closed = True 74 + i += 1 75 + break 76 + block_lines.append(lines[i]) 77 + i += 1 78 + if closed: 79 + blocks.append("\n".join(block_lines)) 80 + # Unclosed fences are silently skipped — they indicate malformed markdown 81 + # and including them would cause false-positive validation failures. 82 + return blocks 83 + 84 + 85 + def extract_urls(text): 86 + return set(URL_REGEX.findall(text)) 87 + 88 + 89 + def extract_paths(text): 90 + return set(PATH_REGEX.findall(text)) 91 + 92 + 93 + def count_bullets(text): 94 + return len(BULLET_REGEX.findall(text)) 95 + 96 + 97 + def extract_inline_codes(text): 98 + text_without_fences = re.sub(r"^```[\s\S]*?^```", "", text, flags=re.MULTILINE) 99 + text_without_fences = re.sub(r"^~~~[\s\S]*?^~~~", "", text_without_fences, flags=re.MULTILINE) 100 + return re.findall(r"`([^`]+)`", text_without_fences) 101 + 102 + 103 + # ---------- Validators ---------- 104 + 105 + 106 + def validate_headings(orig, comp, result): 107 + h1 = extract_headings(orig) 108 + h2 = extract_headings(comp) 109 + 110 + if len(h1) != len(h2): 111 + result.add_error(f"Heading count mismatch: {len(h1)} vs {len(h2)}") 112 + 113 + if h1 != h2: 114 + result.add_warning("Heading text/order changed") 115 + 116 + 117 + def validate_code_blocks(orig, comp, result): 118 + c1 = extract_code_blocks(orig) 119 + c2 = extract_code_blocks(comp) 120 + 121 + if c1 != c2: 122 + result.add_error("Code blocks not preserved exactly") 123 + 124 + 125 + def validate_urls(orig, comp, result): 126 + u1 = extract_urls(orig) 127 + u2 = extract_urls(comp) 128 + 129 + if u1 != u2: 130 + result.add_error(f"URL mismatch: lost={u1 - u2}, added={u2 - u1}") 131 + 132 + 133 + def validate_paths(orig, comp, result): 134 + p1 = extract_paths(orig) 135 + p2 = extract_paths(comp) 136 + 137 + if p1 != p2: 138 + result.add_warning(f"Path mismatch: lost={p1 - p2}, added={p2 - p1}") 139 + 140 + 141 + def validate_bullets(orig, comp, result): 142 + b1 = count_bullets(orig) 143 + b2 = count_bullets(comp) 144 + 145 + if b1 == 0: 146 + return 147 + 148 + diff = abs(b1 - b2) / b1 149 + 150 + if diff > 0.15: 151 + result.add_warning(f"Bullet count changed too much: {b1} -> {b2}") 152 + 153 + 154 + def validate_inline_codes(orig, comp, result): 155 + c1 = Counter(extract_inline_codes(orig)) 156 + c2 = Counter(extract_inline_codes(comp)) 157 + 158 + if c1 != c2: 159 + lost = set(c1.keys()) - set(c2.keys()) 160 + added = set(c2.keys()) - set(c1.keys()) 161 + for code, count in c1.items(): 162 + if code in c2 and c2[code] < count: 163 + lost.add(f"{code} (lost {count - c2[code]} of {count} occurrences)") 164 + if lost: 165 + result.add_error(f"Inline code lost: {lost}") 166 + if added: 167 + result.add_warning(f"Inline code added: {added}") 168 + 169 + 170 + # ---------- Main ---------- 171 + 172 + 173 + def validate(original_path: Path, compressed_path: Path) -> ValidationResult: 174 + result = ValidationResult() 175 + 176 + orig = read_file(original_path) 177 + comp = read_file(compressed_path) 178 + 179 + validate_headings(orig, comp, result) 180 + validate_code_blocks(orig, comp, result) 181 + validate_urls(orig, comp, result) 182 + validate_paths(orig, comp, result) 183 + validate_bullets(orig, comp, result) 184 + validate_inline_codes(orig, comp, result) 185 + 186 + return result 187 + 188 + 189 + # ---------- CLI ---------- 190 + 191 + if __name__ == "__main__": 192 + import sys 193 + 194 + if len(sys.argv) != 3: 195 + print("Usage: python validate.py <original> <compressed>") 196 + sys.exit(1) 197 + 198 + orig = Path(sys.argv[1]).resolve() 199 + comp = Path(sys.argv[2]).resolve() 200 + 201 + res = validate(orig, comp) 202 + 203 + print(f"\nValid: {res.is_valid}") 204 + 205 + if res.errors: 206 + print("\nErrors:") 207 + for e in res.errors: 208 + print(f" - {e}") 209 + 210 + if res.warnings: 211 + print("\nWarnings:") 212 + for w in res.warnings: 213 + print(f" - {w}")
-1
skills/caveman-help
··· 1 - ../.agents/skills/caveman-help
+59
skills/caveman-help/SKILL.md
··· 1 + --- 2 + name: caveman-help 3 + description: > 4 + Quick-reference card for all caveman modes, skills, and commands. 5 + One-shot display, not a persistent mode. Trigger: /caveman-help, 6 + "caveman help", "what caveman commands", "how do I use caveman". 7 + --- 8 + 9 + # Caveman Help 10 + 11 + Display this reference card when invoked. One-shot — do NOT change mode, write flag files, or persist anything. Output in caveman style. 12 + 13 + ## Modes 14 + 15 + | Mode | Trigger | What change | 16 + |------|---------|-------------| 17 + | **Lite** | `/caveman lite` | Drop filler. Keep sentence structure. | 18 + | **Full** | `/caveman` | Drop articles, filler, pleasantries, hedging. Fragments OK. Default. | 19 + | **Ultra** | `/caveman ultra` | Extreme compression. Bare fragments. Tables over prose. | 20 + | **Wenyan-Lite** | `/caveman wenyan-lite` | Classical Chinese style, light compression. | 21 + | **Wenyan-Full** | `/caveman wenyan` | Full 文言文. Maximum classical terseness. | 22 + | **Wenyan-Ultra** | `/caveman wenyan-ultra` | Extreme. Ancient scholar on a budget. | 23 + 24 + Mode stick until changed or session end. 25 + 26 + ## Skills 27 + 28 + | Skill | Trigger | What it do | 29 + |-------|---------|-----------| 30 + | **caveman-commit** | `/caveman-commit` | Terse commit messages. Conventional Commits. ≤50 char subject. | 31 + | **caveman-review** | `/caveman-review` | One-line PR comments: `L42: bug: user null. Add guard.` | 32 + | **caveman-compress** | `/caveman:compress <file>` | Compress .md files to caveman prose. Saves ~46% input tokens. | 33 + | **caveman-help** | `/caveman-help` | This card. | 34 + 35 + ## Deactivate 36 + 37 + Say "stop caveman" or "normal mode". Resume anytime with `/caveman`. 38 + 39 + ## Configure Default Mode 40 + 41 + Default mode = `full`. Change it: 42 + 43 + **Environment variable** (highest priority): 44 + ```bash 45 + export CAVEMAN_DEFAULT_MODE=ultra 46 + ``` 47 + 48 + **Config file** (`~/.config/caveman/config.json`): 49 + ```json 50 + { "defaultMode": "lite" } 51 + ``` 52 + 53 + Set `"off"` to disable auto-activation on session start. User can still activate manually with `/caveman`. 54 + 55 + Resolution: env var > config file > `full`. 56 + 57 + ## More 58 + 59 + Full docs: https://github.com/JuliusBrussee/caveman
-1
skills/caveman-review
··· 1 - ../.agents/skills/caveman-review
+55
skills/caveman-review/SKILL.md
··· 1 + --- 2 + name: caveman-review 3 + description: > 4 + Ultra-compressed code review comments. Cuts noise from PR feedback while preserving 5 + the actionable signal. Each comment is one line: location, problem, fix. Use when user 6 + says "review this PR", "code review", "review the diff", "/review", or invokes 7 + /caveman-review. Auto-triggers when reviewing pull requests. 8 + --- 9 + 10 + Write code review comments terse and actionable. One line per finding. Location, problem, fix. No throat-clearing. 11 + 12 + ## Rules 13 + 14 + **Format:** `L<line>: <problem>. <fix>.` — or `<file>:L<line>: ...` when reviewing multi-file diffs. 15 + 16 + **Severity prefix (optional, when mixed):** 17 + - `🔴 bug:` — broken behavior, will cause incident 18 + - `🟡 risk:` — works but fragile (race, missing null check, swallowed error) 19 + - `🔵 nit:` — style, naming, micro-optim. Author can ignore 20 + - `❓ q:` — genuine question, not a suggestion 21 + 22 + **Drop:** 23 + - "I noticed that...", "It seems like...", "You might want to consider..." 24 + - "This is just a suggestion but..." — use `nit:` instead 25 + - "Great work!", "Looks good overall but..." — say it once at the top, not per comment 26 + - Restating what the line does — the reviewer can read the diff 27 + - Hedging ("perhaps", "maybe", "I think") — if unsure use `q:` 28 + 29 + **Keep:** 30 + - Exact line numbers 31 + - Exact symbol/function/variable names in backticks 32 + - Concrete fix, not "consider refactoring this" 33 + - The *why* if the fix isn't obvious from the problem statement 34 + 35 + ## Examples 36 + 37 + ❌ "I noticed that on line 42 you're not checking if the user object is null before accessing the email property. This could potentially cause a crash if the user is not found in the database. You might want to add a null check here." 38 + 39 + ✅ `L42: 🔴 bug: user can be null after .find(). Add guard before .email.` 40 + 41 + ❌ "It looks like this function is doing a lot of things and might benefit from being broken up into smaller functions for readability." 42 + 43 + ✅ `L88-140: 🔵 nit: 50-line fn does 4 things. Extract validate/normalize/persist.` 44 + 45 + ❌ "Have you considered what happens if the API returns a 429? I think we should probably handle that case." 46 + 47 + ✅ `L23: 🟡 risk: no retry on 429. Wrap in withBackoff(3).` 48 + 49 + ## Auto-Clarity 50 + 51 + Drop terse mode for: security findings (CVE-class bugs need full explanation + reference), architectural disagreements (need rationale, not just a one-liner), and onboarding contexts where the author is new and needs the "why". In those cases write a normal paragraph, then resume terse for the rest. 52 + 53 + ## Boundaries 54 + 55 + Reviews only — does not write the code fix, does not approve/request-changes, does not run linters. Output the comment(s) ready to paste into the PR. "stop caveman-review" or "normal mode": revert to verbose review style.
-1
skills/caveman-stats
··· 1 - ../.agents/skills/caveman-stats
+10
skills/caveman-stats/SKILL.md
··· 1 + --- 2 + name: caveman-stats 3 + description: > 4 + Show real token usage and estimated savings for the current session. 5 + Reads directly from the Claude Code session log — no AI estimation. 6 + Triggers on /caveman-stats. Output is injected by the mode-tracker hook; 7 + the model itself does not compute the numbers. 8 + --- 9 + 10 + This skill is delivered by `hooks/caveman-stats.js` (read by `hooks/caveman-mode-tracker.js` on `/caveman-stats`). The model does not need to do anything when this skill fires — the hook returns `decision: "block"` with the formatted stats as the reason. The user sees the numbers immediately.
+74
skills/caveman/SKILL.md
··· 1 + --- 2 + name: caveman 3 + description: > 4 + Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman 5 + while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra, 6 + wenyan-lite, wenyan-full, wenyan-ultra. 7 + Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens", 8 + "be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested. 9 + --- 10 + 11 + Respond terse like smart caveman. All technical substance stay. Only fluff die. 12 + 13 + ## Persistence 14 + 15 + ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode". 16 + 17 + Default: **full**. Switch: `/caveman lite|full|ultra`. 18 + 19 + ## Rules 20 + 21 + Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact. 22 + 23 + Pattern: `[thing] [action] [reason]. [next step].` 24 + 25 + Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..." 26 + Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:" 27 + 28 + ## Intensity 29 + 30 + | Level | What change | 31 + |-------|------------| 32 + | **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight | 33 + | **full** | Drop articles, fragments OK, short synonyms. Classic caveman | 34 + | **ultra** | Abbreviate prose words (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough. Code symbols, function names, API names, error strings: never abbreviate | 35 + | **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register | 36 + | **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) | 37 + | **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse | 38 + 39 + Example — "Why React component re-render?" 40 + - lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`." 41 + - full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`." 42 + - ultra: "Inline obj prop → new ref → re-render. `useMemo`." 43 + - wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。" 44 + - wenyan-full: "物出新參照,致重繪。useMemo .Wrap之。" 45 + - wenyan-ultra: "新參照→重繪。useMemo Wrap。" 46 + 47 + Example — "Explain database connection pooling." 48 + - lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead." 49 + - full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead." 50 + - ultra: "Pool = reuse DB conn. Skip handshake → fast under load." 51 + - wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。" 52 + - wenyan-ultra: "池reuse conn。skip handshake → fast。" 53 + 54 + ## Auto-Clarity 55 + 56 + Drop caveman when: 57 + - Security warnings 58 + - Irreversible action confirmations 59 + - Multi-step sequences where fragment order or omitted conjunctions risk misread 60 + - Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` — order unclear without articles/conjunctions) 61 + - User asks to clarify or repeats question 62 + 63 + Resume caveman after clear part done. 64 + 65 + Example — destructive op: 66 + > **Warning:** This will permanently delete all rows in the `users` table and cannot be undone. 67 + > ```sql 68 + > DROP TABLE users; 69 + > ``` 70 + > Caveman resume. Verify backup exist first. 71 + 72 + ## Boundaries 73 + 74 + Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
-1
skills/compress
··· 1 - ../.agents/skills/compress
+111
skills/compress/SKILL.md
··· 1 + --- 2 + name: compress 3 + description: > 4 + Compress natural language memory files (CLAUDE.md, todos, preferences) into caveman format 5 + to save input tokens. Preserves all technical substance, code, URLs, and structure. 6 + Compressed version overwrites the original file. Human-readable backup saved as FILE.original.md. 7 + Trigger: /caveman:compress FILEPATH or "compress memory file" 8 + --- 9 + 10 + # Caveman Compress 11 + 12 + ## Purpose 13 + 14 + Compress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`. 15 + 16 + ## Trigger 17 + 18 + `/caveman:compress <filepath>` or when user asks to compress a memory file. 19 + 20 + ## Process 21 + 22 + 1. This SKILL.md lives alongside `scripts/` in the same directory. Find that directory. 23 + 24 + 2. Run: 25 + 26 + cd <directory_containing_this_SKILL.md> && python3 -m scripts <absolute_filepath> 27 + 28 + 3. The CLI will: 29 + - detect file type (no tokens) 30 + - call Claude to compress 31 + - validate output (no tokens) 32 + - if errors: cherry-pick fix with Claude (targeted fixes only, no recompression) 33 + - retry up to 2 times 34 + - if still failing after 2 retries: report error to user, leave original file untouched 35 + 36 + 4. Return result to user 37 + 38 + ## Compression Rules 39 + 40 + ### Remove 41 + - Articles: a, an, the 42 + - Filler: just, really, basically, actually, simply, essentially, generally 43 + - Pleasantries: "sure", "certainly", "of course", "happy to", "I'd recommend" 44 + - Hedging: "it might be worth", "you could consider", "it would be good to" 45 + - Redundant phrasing: "in order to" → "to", "make sure to" → "ensure", "the reason is because" → "because" 46 + - Connective fluff: "however", "furthermore", "additionally", "in addition" 47 + 48 + ### Preserve EXACTLY (never modify) 49 + - Code blocks (fenced ``` and indented) 50 + - Inline code (`backtick content`) 51 + - URLs and links (full URLs, markdown links) 52 + - File paths (`/src/components/...`, `./config.yaml`) 53 + - Commands (`npm install`, `git commit`, `docker build`) 54 + - Technical terms (library names, API names, protocols, algorithms) 55 + - Proper nouns (project names, people, companies) 56 + - Dates, version numbers, numeric values 57 + - Environment variables (`$HOME`, `NODE_ENV`) 58 + 59 + ### Preserve Structure 60 + - All markdown headings (keep exact heading text, compress body below) 61 + - Bullet point hierarchy (keep nesting level) 62 + - Numbered lists (keep numbering) 63 + - Tables (compress cell text, keep structure) 64 + - Frontmatter/YAML headers in markdown files 65 + 66 + ### Compress 67 + - Use short synonyms: "big" not "extensive", "fix" not "implement a solution for", "use" not "utilize" 68 + - Fragments OK: "Run tests before commit" not "You should always run tests before committing" 69 + - Drop "you should", "make sure to", "remember to" — just state the action 70 + - Merge redundant bullets that say the same thing differently 71 + - Keep one example where multiple examples show the same pattern 72 + 73 + CRITICAL RULE: 74 + Anything inside ``` ... ``` must be copied EXACTLY. 75 + Do not: 76 + - remove comments 77 + - remove spacing 78 + - reorder lines 79 + - shorten commands 80 + - simplify anything 81 + 82 + Inline code (`...`) must be preserved EXACTLY. 83 + Do not modify anything inside backticks. 84 + 85 + If file contains code blocks: 86 + - Treat code blocks as read-only regions 87 + - Only compress text outside them 88 + - Do not merge sections around code 89 + 90 + ## Pattern 91 + 92 + Original: 93 + > You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production. 94 + 95 + Compressed: 96 + > Run tests before push to main. Catch bugs early, prevent broken prod deploys. 97 + 98 + Original: 99 + > The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens. 100 + 101 + Compressed: 102 + > Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens. 103 + 104 + ## Boundaries 105 + 106 + - ONLY compress natural language files (.md, .txt, .typ, .typst, .tex, extensionless) 107 + - NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh 108 + - If file has mixed content (prose + code), compress ONLY the prose sections 109 + - If unsure whether something is code or prose, leave it unchanged 110 + - Original file is backed up as FILE.original.md before overwriting 111 + - Never compress FILE.original.md (skip it)
+9
skills/compress/scripts/__init__.py
··· 1 + """Caveman compress scripts. 2 + 3 + This package provides tools to compress natural language markdown files 4 + into caveman format to save input tokens. 5 + """ 6 + 7 + __all__ = ["cli", "compress", "detect", "validate"] 8 + 9 + __version__ = "1.0.0"
+3
skills/compress/scripts/__main__.py
··· 1 + from .cli import main 2 + 3 + main()
+78
skills/compress/scripts/benchmark.py
··· 1 + #!/usr/bin/env python3 2 + from pathlib import Path 3 + import sys 4 + 5 + # Support both direct execution and module import 6 + try: 7 + from .validate import validate 8 + except ImportError: 9 + sys.path.insert(0, str(Path(__file__).parent)) 10 + from validate import validate 11 + 12 + try: 13 + import tiktoken 14 + _enc = tiktoken.get_encoding("o200k_base") 15 + except ImportError: 16 + _enc = None 17 + 18 + 19 + def count_tokens(text): 20 + if _enc is None: 21 + return len(text.split()) # fallback: word count 22 + return len(_enc.encode(text)) 23 + 24 + 25 + def benchmark_pair(orig_path: Path, comp_path: Path): 26 + orig_text = orig_path.read_text() 27 + comp_text = comp_path.read_text() 28 + 29 + orig_tokens = count_tokens(orig_text) 30 + comp_tokens = count_tokens(comp_text) 31 + saved = 100 * (orig_tokens - comp_tokens) / orig_tokens if orig_tokens > 0 else 0.0 32 + result = validate(orig_path, comp_path) 33 + 34 + return (comp_path.name, orig_tokens, comp_tokens, saved, result.is_valid) 35 + 36 + 37 + def print_table(rows): 38 + print("\n| File | Original | Compressed | Saved % | Valid |") 39 + print("|------|----------|------------|---------|-------|") 40 + for r in rows: 41 + print(f"| {r[0]} | {r[1]} | {r[2]} | {r[3]:.1f}% | {'✅' if r[4] else '❌'} |") 42 + 43 + 44 + def main(): 45 + # Direct file pair: python3 benchmark.py original.md compressed.md 46 + if len(sys.argv) == 3: 47 + orig = Path(sys.argv[1]).resolve() 48 + comp = Path(sys.argv[2]).resolve() 49 + if not orig.exists(): 50 + print(f"❌ Not found: {orig}") 51 + sys.exit(1) 52 + if not comp.exists(): 53 + print(f"❌ Not found: {comp}") 54 + sys.exit(1) 55 + print_table([benchmark_pair(orig, comp)]) 56 + return 57 + 58 + # Glob mode: repo_root/tests/caveman-compress/ 59 + tests_dir = Path(__file__).parent.parent.parent / "tests" / "caveman-compress" 60 + if not tests_dir.exists(): 61 + print(f"❌ Tests dir not found: {tests_dir}") 62 + sys.exit(1) 63 + 64 + rows = [] 65 + for orig in sorted(tests_dir.glob("*.original.md")): 66 + comp = orig.with_name(orig.stem.removesuffix(".original") + ".md") 67 + if comp.exists(): 68 + rows.append(benchmark_pair(orig, comp)) 69 + 70 + if not rows: 71 + print("No compressed file pairs found.") 72 + return 73 + 74 + print_table(rows) 75 + 76 + 77 + if __name__ == "__main__": 78 + main()
+85
skills/compress/scripts/cli.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Caveman Compress CLI 4 + 5 + Usage: 6 + caveman <filepath> 7 + """ 8 + 9 + import sys 10 + 11 + # Force UTF-8 on stdout/stderr before any code can print. Windows consoles 12 + # default to cp1252 and crash on the ❌ glyphs in error/validation branches, 13 + # masking the real error and leaving the user with a half-compressed file. 14 + for _stream in (sys.stdout, sys.stderr): 15 + reconfigure = getattr(_stream, "reconfigure", None) 16 + if callable(reconfigure): 17 + try: 18 + reconfigure(encoding="utf-8", errors="replace") 19 + except Exception: 20 + pass 21 + 22 + from pathlib import Path 23 + 24 + from .compress import compress_file 25 + from .detect import detect_file_type, should_compress 26 + 27 + 28 + def print_usage(): 29 + print("Usage: caveman <filepath>") 30 + 31 + 32 + def main(): 33 + if len(sys.argv) != 2: 34 + print_usage() 35 + sys.exit(1) 36 + 37 + filepath = Path(sys.argv[1]) 38 + 39 + # Check file exists 40 + if not filepath.exists(): 41 + print(f"❌ File not found: {filepath}") 42 + sys.exit(1) 43 + 44 + if not filepath.is_file(): 45 + print(f"❌ Not a file: {filepath}") 46 + sys.exit(1) 47 + 48 + filepath = filepath.resolve() 49 + 50 + # Detect file type 51 + file_type = detect_file_type(filepath) 52 + 53 + print(f"Detected: {file_type}") 54 + 55 + # Check if compressible 56 + if not should_compress(filepath): 57 + print("Skipping: file is not natural language (code/config)") 58 + sys.exit(0) 59 + 60 + print("Starting caveman compression...\n") 61 + 62 + try: 63 + success = compress_file(filepath) 64 + 65 + if success: 66 + print("\nCompression completed successfully") 67 + backup_path = filepath.with_name(filepath.stem + ".original.md") 68 + print(f"Compressed: {filepath}") 69 + print(f"Original: {backup_path}") 70 + sys.exit(0) 71 + else: 72 + print("\n❌ Compression failed after retries") 73 + sys.exit(2) 74 + 75 + except KeyboardInterrupt: 76 + print("\nInterrupted by user") 77 + sys.exit(130) 78 + 79 + except Exception as e: 80 + print(f"\n❌ Error: {e}") 81 + sys.exit(1) 82 + 83 + 84 + if __name__ == "__main__": 85 + main()
+254
skills/compress/scripts/compress.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Caveman Memory Compression Orchestrator 4 + 5 + Usage: 6 + python scripts/compress.py <filepath> 7 + """ 8 + 9 + import os 10 + import re 11 + import subprocess 12 + from pathlib import Path 13 + from typing import List 14 + 15 + OUTER_FENCE_REGEX = re.compile( 16 + r"\A\s*(`{3,}|~{3,})[^\n]*\n(.*)\n\1\s*\Z", re.DOTALL 17 + ) 18 + 19 + # Filenames and paths that almost certainly hold secrets or PII. Compressing 20 + # them ships raw bytes to the Anthropic API — a third-party data boundary that 21 + # developers on sensitive codebases cannot cross. detect.py already skips .env 22 + # by extension, but credentials.md / secrets.txt / ~/.aws/credentials would 23 + # slip through the natural-language filter. This is a hard refuse before read. 24 + SENSITIVE_BASENAME_REGEX = re.compile( 25 + r"(?ix)^(" 26 + r"\.env(\..+)?" 27 + r"|\.netrc" 28 + r"|credentials(\..+)?" 29 + r"|secrets?(\..+)?" 30 + r"|passwords?(\..+)?" 31 + r"|id_(rsa|dsa|ecdsa|ed25519)(\.pub)?" 32 + r"|authorized_keys" 33 + r"|known_hosts" 34 + r"|.*\.(pem|key|p12|pfx|crt|cer|jks|keystore|asc|gpg)" 35 + r")$" 36 + ) 37 + 38 + SENSITIVE_PATH_COMPONENTS = frozenset({".ssh", ".aws", ".gnupg", ".kube", ".docker"}) 39 + 40 + SENSITIVE_NAME_TOKENS = ( 41 + "secret", "credential", "password", "passwd", 42 + "apikey", "accesskey", "token", "privatekey", 43 + ) 44 + 45 + 46 + def is_sensitive_path(filepath: Path) -> bool: 47 + """Heuristic denylist for files that must never be shipped to a third-party API.""" 48 + name = filepath.name 49 + if SENSITIVE_BASENAME_REGEX.match(name): 50 + return True 51 + lowered_parts = {p.lower() for p in filepath.parts} 52 + if lowered_parts & SENSITIVE_PATH_COMPONENTS: 53 + return True 54 + # Normalize separators so "api-key" and "api_key" both match "apikey". 55 + lower = re.sub(r"[_\-\s.]", "", name.lower()) 56 + return any(tok in lower for tok in SENSITIVE_NAME_TOKENS) 57 + 58 + 59 + def strip_llm_wrapper(text: str) -> str: 60 + """Strip outer ```markdown ... ``` fence when it wraps the entire output.""" 61 + m = OUTER_FENCE_REGEX.match(text) 62 + if m: 63 + return m.group(2) 64 + return text 65 + 66 + from .detect import should_compress 67 + from .validate import validate 68 + 69 + MAX_RETRIES = 2 70 + 71 + 72 + # ---------- Claude Calls ---------- 73 + 74 + 75 + def call_claude(prompt: str) -> str: 76 + api_key = os.environ.get("ANTHROPIC_API_KEY") 77 + if api_key: 78 + try: 79 + import anthropic 80 + 81 + client = anthropic.Anthropic(api_key=api_key) 82 + msg = client.messages.create( 83 + model=os.environ.get("CAVEMAN_MODEL", "claude-sonnet-4-5"), 84 + max_tokens=8192, 85 + messages=[{"role": "user", "content": prompt}], 86 + ) 87 + return strip_llm_wrapper(msg.content[0].text.strip()) 88 + except ImportError: 89 + pass # anthropic not installed, fall back to CLI 90 + # Fallback: use claude CLI (handles desktop auth) 91 + try: 92 + result = subprocess.run( 93 + ["claude", "--print"], 94 + input=prompt, 95 + text=True, 96 + capture_output=True, 97 + check=True, 98 + ) 99 + return strip_llm_wrapper(result.stdout.strip()) 100 + except subprocess.CalledProcessError as e: 101 + raise RuntimeError(f"Claude call failed:\n{e.stderr}") 102 + 103 + 104 + def build_compress_prompt(original: str) -> str: 105 + return f""" 106 + Compress this markdown into caveman format. 107 + 108 + STRICT RULES: 109 + - Do NOT modify anything inside ``` code blocks 110 + - Do NOT modify anything inside inline backticks 111 + - Preserve ALL URLs exactly 112 + - Preserve ALL headings exactly 113 + - Preserve file paths and commands 114 + - Return ONLY the compressed markdown body — do NOT wrap the entire output in a ```markdown fence or any other fence. Inner code blocks from the original stay as-is; do not add a new outer fence around the whole file. 115 + 116 + Only compress natural language. 117 + 118 + TEXT: 119 + {original} 120 + """ 121 + 122 + 123 + def build_fix_prompt(original: str, compressed: str, errors: List[str]) -> str: 124 + errors_str = "\n".join(f"- {e}" for e in errors) 125 + return f"""You are fixing a caveman-compressed markdown file. Specific validation errors were found. 126 + 127 + CRITICAL RULES: 128 + - DO NOT recompress or rephrase the file 129 + - ONLY fix the listed errors — leave everything else exactly as-is 130 + - The ORIGINAL is provided as reference only (to restore missing content) 131 + - Preserve caveman style in all untouched sections 132 + 133 + ERRORS TO FIX: 134 + {errors_str} 135 + 136 + HOW TO FIX: 137 + - Missing URL: find it in ORIGINAL, restore it exactly where it belongs in COMPRESSED 138 + - Code block mismatch: find the exact code block in ORIGINAL, restore it in COMPRESSED 139 + - Heading mismatch: restore the exact heading text from ORIGINAL into COMPRESSED 140 + - Do not touch any section not mentioned in the errors 141 + 142 + ORIGINAL (reference only): 143 + {original} 144 + 145 + COMPRESSED (fix this): 146 + {compressed} 147 + 148 + Return ONLY the fixed compressed file. No explanation. 149 + """ 150 + 151 + 152 + # ---------- Core Logic ---------- 153 + 154 + 155 + def compress_file(filepath: Path) -> bool: 156 + # Resolve and validate path 157 + filepath = filepath.resolve() 158 + MAX_FILE_SIZE = 500_000 # 500KB 159 + if not filepath.exists(): 160 + raise FileNotFoundError(f"File not found: {filepath}") 161 + if filepath.stat().st_size > MAX_FILE_SIZE: 162 + raise ValueError(f"File too large to compress safely (max 500KB): {filepath}") 163 + 164 + # Refuse files that look like they contain secrets or PII. Compressing ships 165 + # the raw bytes to the Anthropic API — a third-party boundary — so we fail 166 + # loudly rather than silently exfiltrate credentials or keys. Override is 167 + # intentional: the user must rename the file if the heuristic is wrong. 168 + if is_sensitive_path(filepath): 169 + raise ValueError( 170 + f"Refusing to compress {filepath}: filename looks sensitive " 171 + "(credentials, keys, secrets, or known private paths). " 172 + "Compression sends file contents to the Anthropic API. " 173 + "Rename the file if this is a false positive." 174 + ) 175 + 176 + print(f"Processing: {filepath}") 177 + 178 + if not should_compress(filepath): 179 + print("Skipping (not natural language)") 180 + return False 181 + 182 + original_text = filepath.read_text(errors="ignore") 183 + backup_path = filepath.with_name(filepath.stem + ".original.md") 184 + 185 + if not original_text.strip(): 186 + print("❌ Refusing to compress: file is empty or whitespace-only.") 187 + return False 188 + 189 + # Check if backup already exists to prevent accidental overwriting 190 + if backup_path.exists(): 191 + print(f"⚠️ Backup file already exists: {backup_path}") 192 + print("The original backup may contain important content.") 193 + print("Aborting to prevent data loss. Please remove or rename the backup file if you want to proceed.") 194 + return False 195 + 196 + # Step 1: Compress 197 + print("Compressing with Claude...") 198 + compressed = call_claude(build_compress_prompt(original_text)) 199 + 200 + if compressed is None or not compressed.strip(): 201 + print("❌ Compression aborted: Claude returned an empty response.") 202 + print(" Original file is untouched (no backup created).") 203 + return False 204 + 205 + if compressed.strip() == original_text.strip(): 206 + print("❌ Compression aborted: output is identical to input.") 207 + print(" Likely causes: Claude refused, returned the prompt verbatim, or the file is") 208 + print(" already in caveman form. Original file is untouched (no backup created).") 209 + return False 210 + 211 + # Save original as backup, then verify the backup readback before 212 + # touching the input file. If the filesystem dropped bytes (encoding, 213 + # antivirus, disk full), unlink the bad backup and abort instead of 214 + # leaving the user with a corrupt backup + compressed primary. 215 + backup_path.write_text(original_text) 216 + backup_readback = backup_path.read_text(errors="ignore") 217 + if backup_readback != original_text: 218 + print(f"❌ Backup write verification failed: {backup_path}") 219 + print(" In-memory original differs from on-disk backup. Aborting before touching the input file.") 220 + try: 221 + backup_path.unlink() 222 + except OSError: 223 + pass 224 + return False 225 + filepath.write_text(compressed) 226 + 227 + # Step 2: Validate + Retry 228 + for attempt in range(MAX_RETRIES): 229 + print(f"\nValidation attempt {attempt + 1}") 230 + 231 + result = validate(backup_path, filepath) 232 + 233 + if result.is_valid: 234 + print("Validation passed") 235 + break 236 + 237 + print("❌ Validation failed:") 238 + for err in result.errors: 239 + print(f" - {err}") 240 + 241 + if attempt == MAX_RETRIES - 1: 242 + # Restore original on failure 243 + filepath.write_text(original_text) 244 + backup_path.unlink(missing_ok=True) 245 + print("❌ Failed after retries — original restored") 246 + return False 247 + 248 + print("Fixing with Claude...") 249 + compressed = call_claude( 250 + build_fix_prompt(original_text, compressed, result.errors) 251 + ) 252 + filepath.write_text(compressed) 253 + 254 + return True
+121
skills/compress/scripts/detect.py
··· 1 + #!/usr/bin/env python3 2 + """Detect whether a file is natural language (compressible) or code/config (skip).""" 3 + 4 + import json 5 + import re 6 + from pathlib import Path 7 + 8 + # Extensions that are natural language and compressible 9 + COMPRESSIBLE_EXTENSIONS = {".md", ".txt", ".markdown", ".rst", ".typ", ".typst", ".tex"} 10 + 11 + # Extensions that are code/config and should be skipped 12 + SKIP_EXTENSIONS = { 13 + ".py", ".js", ".ts", ".tsx", ".jsx", ".json", ".yaml", ".yml", 14 + ".toml", ".env", ".lock", ".css", ".scss", ".html", ".xml", 15 + ".sql", ".sh", ".bash", ".zsh", ".go", ".rs", ".java", ".c", 16 + ".cpp", ".h", ".hpp", ".rb", ".php", ".swift", ".kt", ".lua", 17 + ".dockerfile", ".makefile", ".csv", ".ini", ".cfg", 18 + } 19 + 20 + # Patterns that indicate a line is code 21 + CODE_PATTERNS = [ 22 + re.compile(r"^\s*(import |from .+ import |require\(|const |let |var )"), 23 + re.compile(r"^\s*(def |class |function |async function |export )"), 24 + re.compile(r"^\s*(if\s*\(|for\s*\(|while\s*\(|switch\s*\(|try\s*\{)"), 25 + re.compile(r"^\s*[\}\]\);]+\s*$"), # closing braces/brackets 26 + re.compile(r"^\s*@\w+"), # decorators/annotations 27 + re.compile(r'^\s*"[^"]+"\s*:\s*'), # JSON-like key-value 28 + re.compile(r"^\s*\w+\s*=\s*[{\[\(\"']"), # assignment with literal 29 + ] 30 + 31 + 32 + def _is_code_line(line: str) -> bool: 33 + """Check if a line looks like code.""" 34 + return any(p.match(line) for p in CODE_PATTERNS) 35 + 36 + 37 + def _is_json_content(text: str) -> bool: 38 + """Check if content is valid JSON.""" 39 + try: 40 + json.loads(text) 41 + return True 42 + except (json.JSONDecodeError, ValueError): 43 + return False 44 + 45 + 46 + def _is_yaml_content(lines: list[str]) -> bool: 47 + """Heuristic: check if content looks like YAML.""" 48 + yaml_indicators = 0 49 + for line in lines[:30]: 50 + stripped = line.strip() 51 + if stripped.startswith("---"): 52 + yaml_indicators += 1 53 + elif re.match(r"^\w[\w\s]*:\s", stripped): 54 + yaml_indicators += 1 55 + elif stripped.startswith("- ") and ":" in stripped: 56 + yaml_indicators += 1 57 + # If most non-empty lines look like YAML 58 + non_empty = sum(1 for l in lines[:30] if l.strip()) 59 + return non_empty > 0 and yaml_indicators / non_empty > 0.6 60 + 61 + 62 + def detect_file_type(filepath: Path) -> str: 63 + """Classify a file as 'natural_language', 'code', 'config', or 'unknown'. 64 + 65 + Returns: 66 + One of: 'natural_language', 'code', 'config', 'unknown' 67 + """ 68 + ext = filepath.suffix.lower() 69 + 70 + # Extension-based classification 71 + if ext in COMPRESSIBLE_EXTENSIONS: 72 + return "natural_language" 73 + if ext in SKIP_EXTENSIONS: 74 + return "code" if ext not in {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".env"} else "config" 75 + 76 + # Extensionless files (like CLAUDE.md, TODO) — check content 77 + if not ext: 78 + try: 79 + text = filepath.read_text(errors="ignore") 80 + except (OSError, PermissionError): 81 + return "unknown" 82 + 83 + lines = text.splitlines()[:50] 84 + 85 + if _is_json_content(text[:10000]): 86 + return "config" 87 + if _is_yaml_content(lines): 88 + return "config" 89 + 90 + code_lines = sum(1 for l in lines if l.strip() and _is_code_line(l)) 91 + non_empty = sum(1 for l in lines if l.strip()) 92 + if non_empty > 0 and code_lines / non_empty > 0.4: 93 + return "code" 94 + 95 + return "natural_language" 96 + 97 + return "unknown" 98 + 99 + 100 + def should_compress(filepath: Path) -> bool: 101 + """Return True if the file is natural language and should be compressed.""" 102 + if not filepath.is_file(): 103 + return False 104 + # Skip backup files 105 + if filepath.name.endswith(".original.md"): 106 + return False 107 + return detect_file_type(filepath) == "natural_language" 108 + 109 + 110 + if __name__ == "__main__": 111 + import sys 112 + 113 + if len(sys.argv) < 2: 114 + print("Usage: python detect.py <file1> [file2] ...") 115 + sys.exit(1) 116 + 117 + for path_str in sys.argv[1:]: 118 + p = Path(path_str).resolve() 119 + file_type = detect_file_type(p) 120 + compress = should_compress(p) 121 + print(f" {p.name:30s} type={file_type:20s} compress={compress}")
+213
skills/compress/scripts/validate.py
··· 1 + #!/usr/bin/env python3 2 + import re 3 + from collections import Counter 4 + from pathlib import Path 5 + 6 + URL_REGEX = re.compile(r"https?://[^\s)]+") 7 + FENCE_OPEN_REGEX = re.compile(r"^(\s{0,3})(`{3,}|~{3,})(.*)$") 8 + HEADING_REGEX = re.compile(r"^(#{1,6})\s+(.*)", re.MULTILINE) 9 + BULLET_REGEX = re.compile(r"^\s*[-*+]\s+", re.MULTILINE) 10 + 11 + # crude but effective path detection 12 + # Requires either a path prefix (./ ../ / or drive letter) or a slash/backslash within the match 13 + PATH_REGEX = re.compile(r"(?:\./|\.\./|/|[A-Za-z]:\\)[\w\-/\\\.]+|[\w\-\.]+[/\\][\w\-/\\\.]+") 14 + 15 + 16 + class ValidationResult: 17 + def __init__(self): 18 + self.is_valid = True 19 + self.errors = [] 20 + self.warnings = [] 21 + 22 + def add_error(self, msg): 23 + self.is_valid = False 24 + self.errors.append(msg) 25 + 26 + def add_warning(self, msg): 27 + self.warnings.append(msg) 28 + 29 + 30 + def read_file(path: Path) -> str: 31 + return path.read_text(errors="ignore") 32 + 33 + 34 + # ---------- Extractors ---------- 35 + 36 + 37 + def extract_headings(text): 38 + return [(level, title.strip()) for level, title in HEADING_REGEX.findall(text)] 39 + 40 + 41 + def extract_code_blocks(text): 42 + """Line-based fenced code block extractor. 43 + 44 + Handles ``` and ~~~ fences with variable length (CommonMark: closing 45 + fence must use same char and be at least as long as opening). Supports 46 + nested fences (e.g. an outer 4-backtick block wrapping inner 3-backtick 47 + content). 48 + """ 49 + blocks = [] 50 + lines = text.split("\n") 51 + i = 0 52 + n = len(lines) 53 + while i < n: 54 + m = FENCE_OPEN_REGEX.match(lines[i]) 55 + if not m: 56 + i += 1 57 + continue 58 + fence_char = m.group(2)[0] 59 + fence_len = len(m.group(2)) 60 + open_line = lines[i] 61 + block_lines = [open_line] 62 + i += 1 63 + closed = False 64 + while i < n: 65 + close_m = FENCE_OPEN_REGEX.match(lines[i]) 66 + if ( 67 + close_m 68 + and close_m.group(2)[0] == fence_char 69 + and len(close_m.group(2)) >= fence_len 70 + and close_m.group(3).strip() == "" 71 + ): 72 + block_lines.append(lines[i]) 73 + closed = True 74 + i += 1 75 + break 76 + block_lines.append(lines[i]) 77 + i += 1 78 + if closed: 79 + blocks.append("\n".join(block_lines)) 80 + # Unclosed fences are silently skipped — they indicate malformed markdown 81 + # and including them would cause false-positive validation failures. 82 + return blocks 83 + 84 + 85 + def extract_urls(text): 86 + return set(URL_REGEX.findall(text)) 87 + 88 + 89 + def extract_paths(text): 90 + return set(PATH_REGEX.findall(text)) 91 + 92 + 93 + def count_bullets(text): 94 + return len(BULLET_REGEX.findall(text)) 95 + 96 + 97 + def extract_inline_codes(text): 98 + text_without_fences = re.sub(r"^```[\s\S]*?^```", "", text, flags=re.MULTILINE) 99 + text_without_fences = re.sub(r"^~~~[\s\S]*?^~~~", "", text_without_fences, flags=re.MULTILINE) 100 + return re.findall(r"`([^`]+)`", text_without_fences) 101 + 102 + 103 + # ---------- Validators ---------- 104 + 105 + 106 + def validate_headings(orig, comp, result): 107 + h1 = extract_headings(orig) 108 + h2 = extract_headings(comp) 109 + 110 + if len(h1) != len(h2): 111 + result.add_error(f"Heading count mismatch: {len(h1)} vs {len(h2)}") 112 + 113 + if h1 != h2: 114 + result.add_warning("Heading text/order changed") 115 + 116 + 117 + def validate_code_blocks(orig, comp, result): 118 + c1 = extract_code_blocks(orig) 119 + c2 = extract_code_blocks(comp) 120 + 121 + if c1 != c2: 122 + result.add_error("Code blocks not preserved exactly") 123 + 124 + 125 + def validate_urls(orig, comp, result): 126 + u1 = extract_urls(orig) 127 + u2 = extract_urls(comp) 128 + 129 + if u1 != u2: 130 + result.add_error(f"URL mismatch: lost={u1 - u2}, added={u2 - u1}") 131 + 132 + 133 + def validate_paths(orig, comp, result): 134 + p1 = extract_paths(orig) 135 + p2 = extract_paths(comp) 136 + 137 + if p1 != p2: 138 + result.add_warning(f"Path mismatch: lost={p1 - p2}, added={p2 - p1}") 139 + 140 + 141 + def validate_bullets(orig, comp, result): 142 + b1 = count_bullets(orig) 143 + b2 = count_bullets(comp) 144 + 145 + if b1 == 0: 146 + return 147 + 148 + diff = abs(b1 - b2) / b1 149 + 150 + if diff > 0.15: 151 + result.add_warning(f"Bullet count changed too much: {b1} -> {b2}") 152 + 153 + 154 + def validate_inline_codes(orig, comp, result): 155 + c1 = Counter(extract_inline_codes(orig)) 156 + c2 = Counter(extract_inline_codes(comp)) 157 + 158 + if c1 != c2: 159 + lost = set(c1.keys()) - set(c2.keys()) 160 + added = set(c2.keys()) - set(c1.keys()) 161 + for code, count in c1.items(): 162 + if code in c2 and c2[code] < count: 163 + lost.add(f"{code} (lost {count - c2[code]} of {count} occurrences)") 164 + if lost: 165 + result.add_error(f"Inline code lost: {lost}") 166 + if added: 167 + result.add_warning(f"Inline code added: {added}") 168 + 169 + 170 + # ---------- Main ---------- 171 + 172 + 173 + def validate(original_path: Path, compressed_path: Path) -> ValidationResult: 174 + result = ValidationResult() 175 + 176 + orig = read_file(original_path) 177 + comp = read_file(compressed_path) 178 + 179 + validate_headings(orig, comp, result) 180 + validate_code_blocks(orig, comp, result) 181 + validate_urls(orig, comp, result) 182 + validate_paths(orig, comp, result) 183 + validate_bullets(orig, comp, result) 184 + validate_inline_codes(orig, comp, result) 185 + 186 + return result 187 + 188 + 189 + # ---------- CLI ---------- 190 + 191 + if __name__ == "__main__": 192 + import sys 193 + 194 + if len(sys.argv) != 3: 195 + print("Usage: python validate.py <original> <compressed>") 196 + sys.exit(1) 197 + 198 + orig = Path(sys.argv[1]).resolve() 199 + comp = Path(sys.argv[2]).resolve() 200 + 201 + res = validate(orig, comp) 202 + 203 + print(f"\nValid: {res.is_valid}") 204 + 205 + if res.errors: 206 + print("\nErrors:") 207 + for e in res.errors: 208 + print(f" - {e}") 209 + 210 + if res.warnings: 211 + print("\nWarnings:") 212 + for w in res.warnings: 213 + print(f" - {w}")
+260
skills/shadcn/SKILL.md
··· 1 + --- 2 + name: shadcn 3 + description: Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI. Provides project context, component docs, and usage examples. Applies when working with shadcn/ui, component registries, presets, --preset codes, or any project with a components.json file. Also triggers for "shadcn init", "create an app with --preset", or "switch to --preset". 4 + user-invocable: false 5 + allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *) 6 + --- 7 + 8 + # shadcn/ui 9 + 10 + A framework for building ui, components and design systems. Components are added as source code to the user's project via the CLI. 11 + 12 + > **IMPORTANT:** Run all CLI commands using the project's package runner: `npx shadcn@latest`, `pnpm dlx shadcn@latest`, or `bunx --bun shadcn@latest` — based on the project's `packageManager`. Examples below use `npx shadcn@latest` but substitute the correct runner for the project. 13 + 14 + ## Current Project Context 15 + 16 + ```json 17 + !`npx shadcn@latest info --json` 18 + ``` 19 + 20 + The JSON above contains the project config and installed components. Use `npx shadcn@latest docs <component>` to get documentation and example URLs for any component. 21 + 22 + ## Principles 23 + 24 + 1. **Use existing components first.** Use `npx shadcn@latest search` to check registries before writing custom UI. Check community registries too. 25 + 2. **Compose, don't reinvent.** Settings page = Tabs + Card + form controls. Dashboard = Sidebar + Card + Chart + Table. 26 + 3. **Use built-in variants before custom styles.** `variant="outline"`, `size="sm"`, etc. 27 + 4. **Use semantic colors.** `bg-primary`, `text-muted-foreground` — never raw values like `bg-blue-500`. 28 + 29 + ## Critical Rules 30 + 31 + These rules are **always enforced**. Each links to a file with Incorrect/Correct code pairs. 32 + 33 + ### Styling & Tailwind → [styling.md](./rules/styling.md) 34 + 35 + - **`className` for layout, not styling.** Never override component colors or typography. 36 + - **No `space-x-*` or `space-y-*`.** Use `flex` with `gap-*`. For vertical stacks, `flex flex-col gap-*`. 37 + - **Use `size-*` when width and height are equal.** `size-10` not `w-10 h-10`. 38 + - **Use `truncate` shorthand.** Not `overflow-hidden text-ellipsis whitespace-nowrap`. 39 + - **No manual `dark:` color overrides.** Use semantic tokens (`bg-background`, `text-muted-foreground`). 40 + - **Use `cn()` for conditional classes.** Don't write manual template literal ternaries. 41 + - **No manual `z-index` on overlay components.** Dialog, Sheet, Popover, etc. handle their own stacking. 42 + 43 + ### Forms & Inputs → [forms.md](./rules/forms.md) 44 + 45 + - **Forms use `FieldGroup` + `Field`.** Never use raw `div` with `space-y-*` or `grid gap-*` for form layout. 46 + - **`InputGroup` uses `InputGroupInput`/`InputGroupTextarea`.** Never raw `Input`/`Textarea` inside `InputGroup`. 47 + - **Buttons inside inputs use `InputGroup` + `InputGroupAddon`.** 48 + - **Option sets (2–7 choices) use `ToggleGroup`.** Don't loop `Button` with manual active state. 49 + - **`FieldSet` + `FieldLegend` for grouping related checkboxes/radios.** Don't use a `div` with a heading. 50 + - **Field validation uses `data-invalid` + `aria-invalid`.** `data-invalid` on `Field`, `aria-invalid` on the control. For disabled: `data-disabled` on `Field`, `disabled` on the control. 51 + 52 + ### Component Structure → [composition.md](./rules/composition.md) 53 + 54 + - **Items always inside their Group.** `SelectItem` → `SelectGroup`. `DropdownMenuItem` → `DropdownMenuGroup`. `CommandItem` → `CommandGroup`. 55 + - **Use `asChild` (radix) or `render` (base) for custom triggers.** Check `base` field from `npx shadcn@latest info`. → [base-vs-radix.md](./rules/base-vs-radix.md) 56 + - **Dialog, Sheet, and Drawer always need a Title.** `DialogTitle`, `SheetTitle`, `DrawerTitle` required for accessibility. Use `className="sr-only"` if visually hidden. 57 + - **Use full Card composition.** `CardHeader`/`CardTitle`/`CardDescription`/`CardContent`/`CardFooter`. Don't dump everything in `CardContent`. 58 + - **Button has no `isPending`/`isLoading`.** Compose with `Spinner` + `data-icon` + `disabled`. 59 + - **`TabsTrigger` must be inside `TabsList`.** Never render triggers directly in `Tabs`. 60 + - **`Avatar` always needs `AvatarFallback`.** For when the image fails to load. 61 + 62 + ### Use Components, Not Custom Markup → [composition.md](./rules/composition.md) 63 + 64 + - **Use existing components before custom markup.** Check if a component exists before writing a styled `div`. 65 + - **Callouts use `Alert`.** Don't build custom styled divs. 66 + - **Empty states use `Empty`.** Don't build custom empty state markup. 67 + - **Toast via `sonner`.** Use `toast()` from `sonner`. 68 + - **Use `Separator`** instead of `<hr>` or `<div className="border-t">`. 69 + - **Use `Skeleton`** for loading placeholders. No custom `animate-pulse` divs. 70 + - **Use `Badge`** instead of custom styled spans. 71 + 72 + ### Icons → [icons.md](./rules/icons.md) 73 + 74 + - **Icons in `Button` use `data-icon`.** `data-icon="inline-start"` or `data-icon="inline-end"` on the icon. 75 + - **No sizing classes on icons inside components.** Components handle icon sizing via CSS. No `size-4` or `w-4 h-4`. 76 + - **Pass icons as objects, not string keys.** `icon={CheckIcon}`, not a string lookup. 77 + 78 + ### CLI 79 + 80 + - **Never decode preset codes or build preset URLs manually.** Use `npx shadcn@latest preset decode <code>`, `preset url <code>`, or `preset open <code>`. For project-aware preset detection, use `npx shadcn@latest preset resolve`. 81 + - **Apply preset codes directly with the CLI.** Use `npx shadcn@latest apply <code>` for existing projects, or `npx shadcn@latest init --preset <code>` when initializing. 82 + 83 + ## Key Patterns 84 + 85 + These are the most common patterns that differentiate correct shadcn/ui code. For edge cases, see the linked rule files above. 86 + 87 + ```tsx 88 + // Form layout: FieldGroup + Field, not div + Label. 89 + <FieldGroup> 90 + <Field> 91 + <FieldLabel htmlFor="email">Email</FieldLabel> 92 + <Input id="email" /> 93 + </Field> 94 + </FieldGroup> 95 + 96 + // Validation: data-invalid on Field, aria-invalid on the control. 97 + <Field data-invalid> 98 + <FieldLabel>Email</FieldLabel> 99 + <Input aria-invalid /> 100 + <FieldDescription>Invalid email.</FieldDescription> 101 + </Field> 102 + 103 + // Icons in buttons: data-icon, no sizing classes. 104 + <Button> 105 + <SearchIcon data-icon="inline-start" /> 106 + Search 107 + </Button> 108 + 109 + // Spacing: gap-*, not space-y-*. 110 + <div className="flex flex-col gap-4"> // correct 111 + <div className="space-y-4"> // wrong 112 + 113 + // Equal dimensions: size-*, not w-* h-*. 114 + <Avatar className="size-10"> // correct 115 + <Avatar className="w-10 h-10"> // wrong 116 + 117 + // Status colors: Badge variants or semantic tokens, not raw colors. 118 + <Badge variant="secondary">+20.1%</Badge> // correct 119 + <span className="text-emerald-600">+20.1%</span> // wrong 120 + ``` 121 + 122 + ## Component Selection 123 + 124 + | Need | Use | 125 + | -------------------------- | --------------------------------------------------------------------------------------------------- | 126 + | Button/action | `Button` with appropriate variant | 127 + | Form inputs | `Input`, `Select`, `Combobox`, `Switch`, `Checkbox`, `RadioGroup`, `Textarea`, `InputOTP`, `Slider` | 128 + | Toggle between 2–5 options | `ToggleGroup` + `ToggleGroupItem` | 129 + | Data display | `Table`, `Card`, `Badge`, `Avatar` | 130 + | Navigation | `Sidebar`, `NavigationMenu`, `Breadcrumb`, `Tabs`, `Pagination` | 131 + | Overlays | `Dialog` (modal), `Sheet` (side panel), `Drawer` (bottom sheet), `AlertDialog` (confirmation) | 132 + | Feedback | `sonner` (toast), `Alert`, `Progress`, `Skeleton`, `Spinner` | 133 + | Command palette | `Command` inside `Dialog` | 134 + | Charts | `Chart` (wraps Recharts) | 135 + | Layout | `Card`, `Separator`, `Resizable`, `ScrollArea`, `Accordion`, `Collapsible` | 136 + | Empty states | `Empty` | 137 + | Menus | `DropdownMenu`, `ContextMenu`, `Menubar` | 138 + | Tooltips/info | `Tooltip`, `HoverCard`, `Popover` | 139 + 140 + ## Key Fields 141 + 142 + The injected project context contains these key fields: 143 + 144 + - **`aliases`** → use the actual alias prefix for imports (e.g. `@/`, `~/`), never hardcode. 145 + - **`isRSC`** → when `true`, components using `useState`, `useEffect`, event handlers, or browser APIs need `"use client"` at the top of the file. Always reference this field when advising on the directive. 146 + - **`tailwindVersion`** → `"v4"` uses `@theme inline` blocks; `"v3"` uses `tailwind.config.js`. 147 + - **`tailwindCssFile`** → the global CSS file where custom CSS variables are defined. Always edit this file, never create a new one. 148 + - **`style`** → component visual treatment (e.g. `nova`, `vega`). 149 + - **`base`** → primitive library (`radix` or `base`). Affects component APIs and available props. 150 + - **`iconLibrary`** → determines icon imports. Use `lucide-react` for `lucide`, `@tabler/icons-react` for `tabler`, etc. Never assume `lucide-react`. 151 + - **`resolvedPaths`** → exact file-system destinations for components, utils, hooks, etc. 152 + - **`framework`** → routing and file conventions (e.g. Next.js App Router vs Vite SPA). 153 + - **`packageManager`** → use this for any non-shadcn dependency installs (e.g. `pnpm add date-fns` vs `npm install date-fns`). 154 + - **`preset`** → resolved preset code and values for the current project. Use `npx shadcn@latest preset resolve --json` when you only need preset information. 155 + 156 + See [cli.md — `info` command](./cli.md) for the full field reference. 157 + 158 + ## Component Docs, Examples, and Usage 159 + 160 + Run `npx shadcn@latest docs <component>` to get the URLs for a component's documentation, examples, and API reference. Fetch these URLs to get the actual content. 161 + 162 + ```bash 163 + npx shadcn@latest docs button dialog select 164 + ``` 165 + 166 + **When creating, fixing, debugging, or using a component, always run `npx shadcn@latest docs` and fetch the URLs first.** This ensures you're working with the correct API and usage patterns rather than guessing. 167 + 168 + ## Workflow 169 + 170 + 1. **Get project context** — already injected above. Run `npx shadcn@latest info` again if you need to refresh. 171 + 2. **Check installed components first** — before running `add`, always check the `components` list from project context or list the `resolvedPaths.ui` directory. Don't import components that haven't been added, and don't re-add ones already installed. 172 + 3. **Find components** — `npx shadcn@latest search`. 173 + 4. **Get docs and examples** — run `npx shadcn@latest docs <component>` to get URLs, then fetch them. Use `npx shadcn@latest view` to browse registry items you haven't installed. To preview changes to installed components, use `npx shadcn@latest add --diff`. 174 + 5. **Install or update** — `npx shadcn@latest add`. When updating existing components, use `--dry-run` and `--diff` to preview changes first (see [Updating Components](#updating-components) below). 175 + 6. **Fix imports in third-party components** — After adding components from community registries (e.g. `@bundui`, `@magicui`), check the added non-UI files for hardcoded import paths like `@/components/ui/...`. These won't match the project's actual aliases. Use `npx shadcn@latest info` to get the correct `ui` alias (e.g. `@workspace/ui/components`) and rewrite the imports accordingly. The CLI rewrites imports for its own UI files, but third-party registry components may use default paths that don't match the project. 176 + 7. **Review added components** — After adding a component or block from any registry, **always read the added files and verify they are correct**. Check for missing sub-components (e.g. `SelectItem` without `SelectGroup`), missing imports, incorrect composition, or violations of the [Critical Rules](#critical-rules). Also replace any icon imports with the project's `iconLibrary` from the project context (e.g. if the registry item uses `lucide-react` but the project uses `hugeicons`, swap the imports and icon names accordingly). Fix all issues before moving on. 177 + 8. **Registry must be explicit** — When the user asks to add a block or component, **do not guess the registry**. If no registry is specified (e.g. user says "add a login block" without specifying `@shadcn`, `@tailark`, etc.), ask which registry to use. Never default to a registry on behalf of the user. 178 + 9. **Switching presets** — Ask the user first: **overwrite**, **partial**, **merge**, or **skip**? 179 + - **Inspect current preset**: `npx shadcn@latest preset resolve`. Use `--json` when you need structured values. 180 + - **Inspect incoming preset**: `npx shadcn@latest preset decode <code>`. Use `preset url <code>` or `preset open <code>` to share or open the preset builder. 181 + - **Overwrite**: `npx shadcn@latest apply <code>`. Overwrites detected components, fonts, and CSS variables. 182 + - **Partial**: `npx shadcn@latest apply <code> --only theme,font`. Updates only the selected preset parts without reinstalling UI components. Supported values are `theme` and `font`; comma-separated combinations are allowed. `icon` is intentionally not supported, because icon changes may require full component reinstall and transforms. 183 + - **Merge**: `npx shadcn@latest init --preset <code> --force --no-reinstall`, then run `npx shadcn@latest info` to list installed components, then for each installed component use `--dry-run` and `--diff` to [smart merge](#updating-components) it individually. 184 + - **Skip**: `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS, leaves components as-is. 185 + - **Important**: Always run preset commands inside the user's project directory. `apply` only works in an existing project with a `components.json` file. The CLI automatically preserves the current base (`base` vs `radix`) from `components.json`. If you must use a scratch/temp directory (e.g. for `--dry-run` comparisons), pass `--base <current-base>` explicitly — preset codes do not encode the base. 186 + 187 + ## Updating Components 188 + 189 + When the user asks to update a component from upstream while keeping their local changes, use `--dry-run` and `--diff` to intelligently merge. **NEVER fetch raw files from GitHub manually — always use the CLI.** 190 + 191 + 1. Run `npx shadcn@latest add <component> --dry-run` to see all files that would be affected. 192 + 2. For each file, run `npx shadcn@latest add <component> --diff <file>` to see what changed upstream vs local. 193 + 3. Decide per file based on the diff: 194 + - No local changes → safe to overwrite. 195 + - Has local changes → read the local file, analyze the diff, and apply upstream updates while preserving local modifications. 196 + - User says "just update everything" → use `--overwrite`, but confirm first. 197 + 4. **Never use `--overwrite` without the user's explicit approval.** 198 + 199 + ## Quick Reference 200 + 201 + ```bash 202 + # Create a new project. 203 + npx shadcn@latest init --name my-app --preset base-nova 204 + npx shadcn@latest init --name my-app --preset a2r6bw --template vite 205 + 206 + # Create a monorepo project. 207 + npx shadcn@latest init --name my-app --preset base-nova --monorepo 208 + npx shadcn@latest init --name my-app --preset base-nova --template next --monorepo 209 + 210 + # Initialize existing project. 211 + npx shadcn@latest init --preset base-nova 212 + npx shadcn@latest init --defaults # shortcut: --template=next --preset=nova (base style implied) 213 + 214 + # Apply a preset to an existing project. 215 + npx shadcn@latest apply a2r6bw 216 + npx shadcn@latest apply a2r6bw --only theme 217 + npx shadcn@latest apply a2r6bw --only font 218 + npx shadcn@latest apply a2r6bw --only theme,font 219 + 220 + # Inspect preset codes and project preset state. 221 + npx shadcn@latest preset decode a2r6bw 222 + npx shadcn@latest preset url a2r6bw 223 + npx shadcn@latest preset open a2r6bw 224 + npx shadcn@latest preset resolve 225 + npx shadcn@latest preset resolve --json 226 + 227 + # Add components. 228 + npx shadcn@latest add button card dialog 229 + npx shadcn@latest add @magicui/shimmer-button 230 + npx shadcn@latest add --all 231 + 232 + # Preview changes before adding/updating. 233 + npx shadcn@latest add button --dry-run 234 + npx shadcn@latest add button --diff button.tsx 235 + npx shadcn@latest add @acme/form --view button.tsx 236 + 237 + # Search registries. 238 + npx shadcn@latest search @shadcn -q "sidebar" 239 + npx shadcn@latest search @tailark -q "stats" 240 + 241 + # Get component docs and example URLs. 242 + npx shadcn@latest docs button dialog select 243 + 244 + # View registry item details (for items not yet installed). 245 + npx shadcn@latest view @shadcn/button 246 + ``` 247 + 248 + **Named presets:** `nova`, `vega`, `maia`, `lyra`, `mira`, `luma` 249 + **Templates:** `next`, `vite`, `start`, `react-router`, `astro` (all support `--monorepo`) and `laravel` (not supported for monorepo) 250 + **Preset codes:** Version-prefixed base62 strings (e.g. `a2r6bw` or `b0`), from [ui.shadcn.com](https://ui.shadcn.com). 251 + 252 + ## Detailed References 253 + 254 + - [rules/forms.md](./rules/forms.md) — FieldGroup, Field, InputGroup, ToggleGroup, FieldSet, validation states 255 + - [rules/composition.md](./rules/composition.md) — Groups, overlays, Card, Tabs, Avatar, Alert, Empty, Toast, Separator, Skeleton, Badge, Button loading 256 + - [rules/icons.md](./rules/icons.md) — data-icon, icon sizing, passing icons as objects 257 + - [rules/styling.md](./rules/styling.md) — Semantic colors, variants, className, spacing, size, truncate, dark mode, cn(), z-index 258 + - [rules/base-vs-radix.md](./rules/base-vs-radix.md) — asChild vs render, Select, ToggleGroup, Slider, Accordion 259 + - [cli.md](./cli.md) — Commands, flags, presets, templates 260 + - [customization.md](./customization.md) — Theming, CSS variables, extending components
+5
skills/shadcn/agents/openai.yml
··· 1 + interface: 2 + display_name: "shadcn/ui" 3 + short_description: "Manages shadcn/ui components — adding, searching, fixing, debugging, styling, and composing UI." 4 + icon_small: "./assets/shadcn-small.png" 5 + icon_large: "./assets/shadcn.png"
skills/shadcn/assets/shadcn-small.png

This is a binary file and will not be displayed.

skills/shadcn/assets/shadcn.png

This is a binary file and will not be displayed.

+276
skills/shadcn/cli.md
··· 1 + # shadcn CLI Reference 2 + 3 + Configuration is read from `components.json`. 4 + 5 + > **IMPORTANT:** Always run commands using the project's package runner: `npx shadcn@latest`, `pnpm dlx shadcn@latest`, or `bunx --bun shadcn@latest`. Check `packageManager` from project context to choose the right one. Examples below use `npx shadcn@latest` but substitute the correct runner for the project. 6 + 7 + > **IMPORTANT:** Only use the flags documented below. Do not invent or guess flags — if a flag isn't listed here, it doesn't exist. The CLI auto-detects the package manager from the project's lockfile; there is no `--package-manager` flag. 8 + 9 + ## Contents 10 + 11 + - Commands: init, apply, add (dry-run, smart merge), search, view, docs, info, build 12 + - Templates: next, vite, start, react-router, astro 13 + - Presets: named, code, URL formats and fields 14 + - Switching presets 15 + 16 + --- 17 + 18 + ## Commands 19 + 20 + ### `init` — Initialize or create a project 21 + 22 + ```bash 23 + npx shadcn@latest init [components...] [options] 24 + ``` 25 + 26 + Initializes shadcn/ui in an existing project or creates a new project (when `--name` is provided). Optionally installs components in the same step. 27 + 28 + | Flag | Short | Description | Default | 29 + | ----------------------- | ----- | --------------------------------------------------------- | ------- | 30 + | `--template <template>` | `-t` | Template (next, start, vite, next-monorepo, react-router) | — | 31 + | `--preset [name]` | `-p` | Preset configuration (named, code, or URL) | — | 32 + | `--yes` | `-y` | Skip confirmation prompt | `true` | 33 + | `--defaults` | `-d` | Use defaults (`--template=next --preset=base-nova`) | `false` | 34 + | `--force` | `-f` | Force overwrite existing configuration | `false` | 35 + | `--cwd <cwd>` | `-c` | Working directory | current | 36 + | `--name <name>` | `-n` | Name for new project | — | 37 + | `--silent` | `-s` | Mute output | `false` | 38 + | `--rtl` | | Enable RTL support | — | 39 + | `--reinstall` | | Re-install existing UI components | `false` | 40 + | `--monorepo` | | Scaffold a monorepo project | — | 41 + | `--no-monorepo` | | Skip the monorepo prompt | — | 42 + 43 + `npx shadcn@latest create` is an alias for `npx shadcn@latest init`. 44 + 45 + ### `apply` — Apply a preset to an existing project 46 + 47 + ```bash 48 + npx shadcn@latest apply [preset] [options] 49 + ``` 50 + 51 + Applies a preset to an existing project, overwriting preset-driven config, fonts, CSS variables, and detected UI components. 52 + 53 + | Flag | Short | Description | Default | 54 + | ------------------- | ----- | ------------------------------------------ | ------- | 55 + | `--preset <preset>` | — | Preset configuration (named, code, or URL) | — | 56 + | `--yes` | `-y` | Skip confirmation prompt | `false` | 57 + | `--cwd <cwd>` | `-c` | Working directory | current | 58 + | `--silent` | `-s` | Mute output | `false` | 59 + 60 + `[preset]` is a shorthand for `--preset <preset>`. If both are provided, they must match. 61 + If no preset is provided, the CLI offers to open the custom preset builder on `ui.shadcn.com/create`. 62 + 63 + ### `add` — Add components 64 + 65 + > **IMPORTANT:** To compare local components against upstream or to preview changes, ALWAYS use `npx shadcn@latest add <component> --dry-run`, `--diff`, or `--view`. NEVER fetch raw files from GitHub or other sources manually. The CLI handles registry resolution, file paths, and CSS diffing automatically. 66 + 67 + ```bash 68 + npx shadcn@latest add [components...] [options] 69 + ``` 70 + 71 + Accepts component names, registry-prefixed names (`@magicui/shimmer-button`), URLs, or local paths. 72 + 73 + | Flag | Short | Description | Default | 74 + | --------------- | ----- | -------------------------------------------------------------------------------------------------------------------- | ------- | 75 + | `--yes` | `-y` | Skip confirmation prompt | `false` | 76 + | `--overwrite` | `-o` | Overwrite existing files | `false` | 77 + | `--cwd <cwd>` | `-c` | Working directory | current | 78 + | `--all` | `-a` | Add all available components | `false` | 79 + | `--path <path>` | `-p` | Target path for the component | — | 80 + | `--silent` | `-s` | Mute output | `false` | 81 + | `--dry-run` | | Preview all changes without writing files | `false` | 82 + | `--diff [path]` | | Show diffs. Without a path, shows the first 5 files. With a path, shows that file only (implies `--dry-run`) | — | 83 + | `--view [path]` | | Show file contents. Without a path, shows the first 5 files. With a path, shows that file only (implies `--dry-run`) | — | 84 + 85 + #### Dry-Run Mode 86 + 87 + Use `--dry-run` to preview what `add` would do without writing any files. `--diff` and `--view` both imply `--dry-run`. 88 + 89 + ```bash 90 + # Preview all changes. 91 + npx shadcn@latest add button --dry-run 92 + 93 + # Show diffs for all files (top 5). 94 + npx shadcn@latest add button --diff 95 + 96 + # Show the diff for a specific file. 97 + npx shadcn@latest add button --diff button.tsx 98 + 99 + # Show contents for all files (top 5). 100 + npx shadcn@latest add button --view 101 + 102 + # Show the full content of a specific file. 103 + npx shadcn@latest add button --view button.tsx 104 + 105 + # Works with URLs too. 106 + npx shadcn@latest add https://api.npoint.io/abc123 --dry-run 107 + 108 + # CSS diffs. 109 + npx shadcn@latest add button --diff globals.css 110 + ``` 111 + 112 + **When to use dry-run:** 113 + 114 + - When the user asks "what files will this add?" or "what will this change?" — use `--dry-run`. 115 + - Before overwriting existing components — use `--diff` to preview the changes first. 116 + - When the user wants to inspect component source code without installing — use `--view`. 117 + - When checking what CSS changes would be made to `globals.css` — use `--diff globals.css`. 118 + - When the user asks to review or audit third-party registry code before installing — use `--view` to inspect the source. 119 + 120 + > **`npx shadcn@latest add --dry-run` vs `npx shadcn@latest view`:** Prefer `npx shadcn@latest add --dry-run/--diff/--view` over `npx shadcn@latest view` when the user wants to preview changes to their project. `npx shadcn@latest view` only shows raw registry metadata. `npx shadcn@latest add --dry-run` shows exactly what would happen in the user's project: resolved file paths, diffs against existing files, and CSS updates. Use `npx shadcn@latest view` only when the user wants to browse registry info without a project context. 121 + 122 + #### Smart Merge from Upstream 123 + 124 + See [Updating Components in SKILL.md](./SKILL.md#updating-components) for the full workflow. 125 + 126 + ### `search` — Search registries 127 + 128 + ```bash 129 + npx shadcn@latest search <registries...> [options] 130 + ``` 131 + 132 + Fuzzy search across registries. Also aliased as `npx shadcn@latest list`. Without `-q`, lists all items. 133 + 134 + | Flag | Short | Description | Default | 135 + | ------------------- | ----- | ---------------------- | ------- | 136 + | `--query <query>` | `-q` | Search query | — | 137 + | `--limit <number>` | `-l` | Max items per registry | `100` | 138 + | `--offset <number>` | `-o` | Items to skip | `0` | 139 + | `--cwd <cwd>` | `-c` | Working directory | current | 140 + 141 + ### `view` — View item details 142 + 143 + ```bash 144 + npx shadcn@latest view <items...> [options] 145 + ``` 146 + 147 + Displays item info including file contents. Example: `npx shadcn@latest view @shadcn/button`. 148 + 149 + ### `docs` — Get component documentation URLs 150 + 151 + ```bash 152 + npx shadcn@latest docs <components...> [options] 153 + ``` 154 + 155 + Outputs resolved URLs for component documentation, examples, and API references. Accepts one or more component names. Fetch the URLs to get the actual content. 156 + 157 + Example output for `npx shadcn@latest docs input button`: 158 + 159 + ``` 160 + base radix 161 + 162 + input 163 + docs https://ui.shadcn.com/docs/components/radix/input 164 + examples https://raw.githubusercontent.com/.../examples/input-example.tsx 165 + 166 + button 167 + docs https://ui.shadcn.com/docs/components/radix/button 168 + examples https://raw.githubusercontent.com/.../examples/button-example.tsx 169 + ``` 170 + 171 + Some components include an `api` link to the underlying library (e.g. `cmdk` for the command component). 172 + 173 + ### `diff` — Check for updates 174 + 175 + Do not use this command. Use `npx shadcn@latest add --diff` instead. 176 + 177 + ### `info` — Project information 178 + 179 + ```bash 180 + npx shadcn@latest info [options] 181 + ``` 182 + 183 + Displays project info and `components.json` configuration. Run this first to discover the project's framework, aliases, Tailwind version, and resolved paths. 184 + 185 + | Flag | Short | Description | Default | 186 + | ------------- | ----- | ----------------- | ------- | 187 + | `--cwd <cwd>` | `-c` | Working directory | current | 188 + 189 + **Project Info fields:** 190 + 191 + | Field | Type | Meaning | 192 + | -------------------- | --------- | ------------------------------------------------------------------ | 193 + | `framework` | `string` | Detected framework (`next`, `vite`, `react-router`, `start`, etc.) | 194 + | `frameworkVersion` | `string` | Framework version (e.g. `15.2.4`) | 195 + | `isSrcDir` | `boolean` | Whether the project uses a `src/` directory | 196 + | `isRSC` | `boolean` | Whether React Server Components are enabled | 197 + | `isTsx` | `boolean` | Whether the project uses TypeScript | 198 + | `tailwindVersion` | `string` | `"v3"` or `"v4"` | 199 + | `tailwindConfigFile` | `string` | Path to the Tailwind config file | 200 + | `tailwindCssFile` | `string` | Path to the global CSS file | 201 + | `aliasPrefix` | `string` | Import alias prefix (e.g. `@`, `~`, `@/`) | 202 + | `packageManager` | `string` | Detected package manager (`npm`, `pnpm`, `yarn`, `bun`) | 203 + 204 + **Components.json fields:** 205 + 206 + | Field | Type | Meaning | 207 + | -------------------- | --------- | ------------------------------------------------------------------------------------------ | 208 + | `base` | `string` | Primitive library (`radix` or `base`) — determines component APIs and available props | 209 + | `style` | `string` | Visual style (e.g. `nova`, `vega`) | 210 + | `rsc` | `boolean` | RSC flag from config | 211 + | `tsx` | `boolean` | TypeScript flag | 212 + | `tailwind.config` | `string` | Tailwind config path | 213 + | `tailwind.css` | `string` | Global CSS path — this is where custom CSS variables go | 214 + | `iconLibrary` | `string` | Icon library — determines icon import package (e.g. `lucide-react`, `@tabler/icons-react`) | 215 + | `aliases.components` | `string` | Component import alias (e.g. `@/components`) | 216 + | `aliases.utils` | `string` | Utils import alias (e.g. `@/lib/utils`) | 217 + | `aliases.ui` | `string` | UI component alias (e.g. `@/components/ui`) | 218 + | `aliases.lib` | `string` | Lib alias (e.g. `@/lib`) | 219 + | `aliases.hooks` | `string` | Hooks alias (e.g. `@/hooks`) | 220 + | `resolvedPaths` | `object` | Absolute file-system paths for each alias | 221 + | `registries` | `object` | Configured custom registries | 222 + 223 + **Links fields:** 224 + 225 + The `info` output includes a **Links** section with templated URLs for component docs, source, and examples. For resolved URLs, use `npx shadcn@latest docs <component>` instead. 226 + 227 + ### `build` — Build a custom registry 228 + 229 + ```bash 230 + npx shadcn@latest build [registry] [options] 231 + ``` 232 + 233 + Builds `registry.json` into individual JSON files for distribution. Default input: `./registry.json`, default output: `./public/r`. 234 + 235 + | Flag | Short | Description | Default | 236 + | ----------------- | ----- | ----------------- | ------------ | 237 + | `--output <path>` | `-o` | Output directory | `./public/r` | 238 + | `--cwd <cwd>` | `-c` | Working directory | current | 239 + 240 + --- 241 + 242 + ## Templates 243 + 244 + | Value | Framework | Monorepo support | 245 + | -------------- | -------------- | ---------------- | 246 + | `next` | Next.js | Yes | 247 + | `vite` | Vite | Yes | 248 + | `start` | TanStack Start | Yes | 249 + | `react-router` | React Router | Yes | 250 + | `astro` | Astro | Yes | 251 + | `laravel` | Laravel | No | 252 + 253 + All templates support monorepo scaffolding via the `--monorepo` flag. When passed, the CLI uses a monorepo-specific template directory (e.g. `next-monorepo`, `vite-monorepo`). When neither `--monorepo` nor `--no-monorepo` is passed, the CLI prompts interactively. Laravel does not support monorepo scaffolding. 254 + 255 + --- 256 + 257 + ## Presets 258 + 259 + Three ways to specify a preset via `--preset`: 260 + 261 + 1. **Named:** `--preset nova` or `--preset lyra` 262 + 2. **Code:** `--preset a2r6bw` (version-prefixed base62 string, e.g. `a2r6bw` or `b0`) 263 + 3. **URL:** `--preset "https://ui.shadcn.com/init?base=radix&style=nova&..."` 264 + 265 + > **IMPORTANT:** Never try to decode, fetch, or resolve preset codes manually. Preset codes are opaque — pass them directly to `npx shadcn@latest init --preset <code>` and let the CLI handle resolution. 266 + > Use `npx shadcn@latest apply --preset <code>` when overwriting an existing project's preset. 267 + 268 + ## Switching Presets 269 + 270 + Ask the user first: **overwrite**, **merge**, or **skip** existing components? 271 + 272 + - **Overwrite / Re-install** → `npx shadcn@latest apply --preset <code>`. Overwrites all detected component files with the new preset styles. Use when the user hasn't customized components. 273 + - **Merge** → `npx shadcn@latest init --preset <code> --force --no-reinstall`, then run `npx shadcn@latest info` to get the list of installed components and use the [smart merge workflow](./SKILL.md#updating-components) to update them one by one, preserving local changes. Use when the user has customized components. 274 + - **Skip** → `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS variables, leaves existing components as-is. 275 + 276 + Always run preset commands inside the user's project directory. `apply` only works in an existing project with a `components.json` file. The CLI automatically preserves the current base (`base` vs `radix`) from `components.json`. If you must use a scratch/temp directory (e.g. for `--dry-run` comparisons), pass `--base <current-base>` explicitly — preset codes do not encode the base.
+209
skills/shadcn/customization.md
··· 1 + # Customization & Theming 2 + 3 + Components reference semantic CSS variable tokens. Change the variables to change every component. 4 + 5 + ## Contents 6 + 7 + - How it works (CSS variables → Tailwind utilities → components) 8 + - Color variables and OKLCH format 9 + - Dark mode setup 10 + - Changing the theme (presets, CSS variables) 11 + - Adding custom colors (Tailwind v3 and v4) 12 + - Border radius 13 + - Customizing components (variants, className, wrappers) 14 + - Checking for updates 15 + 16 + --- 17 + 18 + ## How It Works 19 + 20 + 1. CSS variables defined in `:root` (light) and `.dark` (dark mode). 21 + 2. Tailwind maps them to utilities: `bg-primary`, `text-muted-foreground`, etc. 22 + 3. Components use these utilities — changing a variable changes all components that reference it. 23 + 24 + --- 25 + 26 + ## Color Variables 27 + 28 + Every color follows the `name` / `name-foreground` convention. The base variable is for backgrounds, `-foreground` is for text/icons on that background. 29 + 30 + | Variable | Purpose | 31 + | -------------------------------------------- | -------------------------------- | 32 + | `--background` / `--foreground` | Page background and default text | 33 + | `--card` / `--card-foreground` | Card surfaces | 34 + | `--primary` / `--primary-foreground` | Primary buttons and actions | 35 + | `--secondary` / `--secondary-foreground` | Secondary actions | 36 + | `--muted` / `--muted-foreground` | Muted/disabled states | 37 + | `--accent` / `--accent-foreground` | Hover and accent states | 38 + | `--destructive` / `--destructive-foreground` | Error and destructive actions | 39 + | `--border` | Default border color | 40 + | `--input` | Form input borders | 41 + | `--ring` | Focus ring color | 42 + | `--chart-1` through `--chart-5` | Chart/data visualization | 43 + | `--sidebar-*` | Sidebar-specific colors | 44 + | `--surface` / `--surface-foreground` | Secondary surface | 45 + 46 + Colors use OKLCH: `--primary: oklch(0.205 0 0)` where values are lightness (0–1), chroma (0 = gray), and hue (0–360). 47 + 48 + --- 49 + 50 + ## Dark Mode 51 + 52 + Class-based toggle via `.dark` on the root element. In Next.js, use `next-themes`: 53 + 54 + ```tsx 55 + import { ThemeProvider } from "next-themes" 56 + 57 + <ThemeProvider attribute="class" defaultTheme="system" enableSystem> 58 + {children} 59 + </ThemeProvider> 60 + ``` 61 + 62 + --- 63 + 64 + ## Changing the Theme 65 + 66 + ```bash 67 + # Apply a preset code from ui.shadcn.com. 68 + npx shadcn@latest apply --preset a2r6bw 69 + 70 + # Positional shorthand also works. 71 + npx shadcn@latest apply a2r6bw 72 + 73 + # Switch to a named preset and overwrite existing components. 74 + npx shadcn@latest apply --preset nova 75 + 76 + # Preserve existing components instead. 77 + npx shadcn@latest init --preset nova --force --no-reinstall 78 + 79 + # Use a custom theme URL. 80 + npx shadcn@latest apply --preset "https://ui.shadcn.com/init?base=radix&style=nova&theme=blue&..." 81 + ``` 82 + 83 + Or edit CSS variables directly in `globals.css`. 84 + 85 + --- 86 + 87 + ## Adding Custom Colors 88 + 89 + Add variables to the file at `tailwindCssFile` from `npx shadcn@latest info` (typically `globals.css`). Never create a new CSS file for this. 90 + 91 + ```css 92 + /* 1. Define in the global CSS file. */ 93 + :root { 94 + --warning: oklch(0.84 0.16 84); 95 + --warning-foreground: oklch(0.28 0.07 46); 96 + } 97 + .dark { 98 + --warning: oklch(0.41 0.11 46); 99 + --warning-foreground: oklch(0.99 0.02 95); 100 + } 101 + ``` 102 + 103 + ```css 104 + /* 2a. Register with Tailwind v4 (@theme inline). */ 105 + @theme inline { 106 + --color-warning: var(--warning); 107 + --color-warning-foreground: var(--warning-foreground); 108 + } 109 + ``` 110 + 111 + When `tailwindVersion` is `"v3"` (check via `npx shadcn@latest info`), register in `tailwind.config.js` instead: 112 + 113 + ```js 114 + // 2b. Register with Tailwind v3 (tailwind.config.js). 115 + module.exports = { 116 + theme: { 117 + extend: { 118 + colors: { 119 + warning: "oklch(var(--warning) / <alpha-value>)", 120 + "warning-foreground": 121 + "oklch(var(--warning-foreground) / <alpha-value>)", 122 + }, 123 + }, 124 + }, 125 + } 126 + ``` 127 + 128 + ```tsx 129 + // 3. Use in components. 130 + <div className="bg-warning text-warning-foreground">Warning</div> 131 + ``` 132 + 133 + --- 134 + 135 + ## Border Radius 136 + 137 + `--radius` controls border radius globally. Components derive values from it (`rounded-lg` = `var(--radius)`, `rounded-md` = `calc(var(--radius) - 2px)`). 138 + 139 + --- 140 + 141 + ## Customizing Components 142 + 143 + See also: [rules/styling.md](./rules/styling.md) for Incorrect/Correct examples. 144 + 145 + Prefer these approaches in order: 146 + 147 + ### 1. Built-in variants 148 + 149 + ```tsx 150 + <Button variant="outline" size="sm"> 151 + Click 152 + </Button> 153 + ``` 154 + 155 + ### 2. Tailwind classes via `className` 156 + 157 + ```tsx 158 + <Card className="mx-auto max-w-md">...</Card> 159 + ``` 160 + 161 + ### 3. Add a new variant 162 + 163 + Edit the component source to add a variant via `cva`: 164 + 165 + ```tsx 166 + // components/ui/button.tsx 167 + warning: "bg-warning text-warning-foreground hover:bg-warning/90", 168 + ``` 169 + 170 + ### 4. Wrapper components 171 + 172 + Compose shadcn/ui primitives into higher-level components: 173 + 174 + ```tsx 175 + export function ConfirmDialog({ title, description, onConfirm, children }) { 176 + return ( 177 + <AlertDialog> 178 + <AlertDialogTrigger asChild>{children}</AlertDialogTrigger> 179 + <AlertDialogContent> 180 + <AlertDialogHeader> 181 + <AlertDialogTitle>{title}</AlertDialogTitle> 182 + <AlertDialogDescription>{description}</AlertDialogDescription> 183 + </AlertDialogHeader> 184 + <AlertDialogFooter> 185 + <AlertDialogCancel>Cancel</AlertDialogCancel> 186 + <AlertDialogAction onClick={onConfirm}>Confirm</AlertDialogAction> 187 + </AlertDialogFooter> 188 + </AlertDialogContent> 189 + </AlertDialog> 190 + ) 191 + } 192 + ``` 193 + 194 + --- 195 + 196 + ## Checking for Updates 197 + 198 + ```bash 199 + npx shadcn@latest add button --diff 200 + ``` 201 + 202 + To preview exactly what would change before updating, use `--dry-run` and `--diff`: 203 + 204 + ```bash 205 + npx shadcn@latest add button --dry-run # see all affected files 206 + npx shadcn@latest add button --diff button.tsx # see the diff for a specific file 207 + ``` 208 + 209 + See [Updating Components in SKILL.md](./SKILL.md#updating-components) for the full smart merge workflow.
+47
skills/shadcn/evals/evals.json
··· 1 + { 2 + "skill_name": "shadcn", 3 + "evals": [ 4 + { 5 + "id": 1, 6 + "prompt": "I'm building a Next.js app with shadcn/ui (base-nova preset, lucide icons). Create a settings form component with fields for: full name, email address, and notification preferences (email, SMS, push notifications as toggle options). Add validation states for required fields.", 7 + "expected_output": "A React component using FieldGroup, Field, ToggleGroup, data-invalid/aria-invalid validation, gap-* spacing, and semantic colors.", 8 + "files": [], 9 + "expectations": [ 10 + "Uses FieldGroup and Field components for form layout instead of raw div with space-y", 11 + "Uses Switch for independent on/off notification toggles (not looping Button with manual active state)", 12 + "Uses data-invalid on Field and aria-invalid on the input control for validation states", 13 + "Uses gap-* (e.g. gap-4, gap-6) instead of space-y-* or space-x-* for spacing", 14 + "Uses semantic color tokens (e.g. bg-background, text-muted-foreground, text-destructive) instead of raw colors like bg-red-500", 15 + "No manual dark: color overrides" 16 + ] 17 + }, 18 + { 19 + "id": 2, 20 + "prompt": "Create a dialog component for editing a user profile. It should have the user's avatar at the top, input fields for name and bio, and Save/Cancel buttons with appropriate icons. Using shadcn/ui with radix-nova preset and tabler icons.", 21 + "expected_output": "A React component with DialogTitle, Avatar+AvatarFallback, data-icon on icon buttons, no icon sizing classes, tabler icon imports.", 22 + "files": [], 23 + "expectations": [ 24 + "Includes DialogTitle for accessibility (visible or with sr-only class)", 25 + "Avatar component includes AvatarFallback", 26 + "Icons on buttons use the data-icon attribute (data-icon=\"inline-start\" or data-icon=\"inline-end\")", 27 + "No sizing classes on icons inside components (no size-4, w-4, h-4, etc.)", 28 + "Uses tabler icons (@tabler/icons-react) instead of lucide-react", 29 + "Uses asChild for custom triggers (radix preset)" 30 + ] 31 + }, 32 + { 33 + "id": 3, 34 + "prompt": "Create a dashboard component that shows 4 stat cards in a grid. Each card has a title, large number, percentage change badge, and a loading skeleton state. Using shadcn/ui with base-nova preset and lucide icons.", 35 + "expected_output": "A React component with full Card composition, Skeleton for loading, Badge for changes, semantic colors, gap-* spacing.", 36 + "files": [], 37 + "expectations": [ 38 + "Uses full Card composition with CardHeader, CardTitle, CardContent (not dumping everything into CardContent)", 39 + "Uses Skeleton component for loading placeholders instead of custom animate-pulse divs", 40 + "Uses Badge component for percentage change instead of custom styled spans", 41 + "Uses semantic color tokens instead of raw color values like bg-green-500 or text-red-600", 42 + "Uses gap-* instead of space-y-* or space-x-* for spacing", 43 + "Uses size-* when width and height are equal instead of separate w-* h-*" 44 + ] 45 + } 46 + ] 47 + }
+94
skills/shadcn/mcp.md
··· 1 + # shadcn MCP Server 2 + 3 + The CLI includes an MCP server that lets AI assistants search, browse, view, and install components from registries. 4 + 5 + --- 6 + 7 + ## Setup 8 + 9 + ```bash 10 + shadcn mcp # start the MCP server (stdio) 11 + shadcn mcp init # write config for your editor 12 + ``` 13 + 14 + Editor config files: 15 + 16 + | Editor | Config file | 17 + |--------|------------| 18 + | Claude Code | `.mcp.json` | 19 + | Cursor | `.cursor/mcp.json` | 20 + | VS Code | `.vscode/mcp.json` | 21 + | OpenCode | `opencode.json` | 22 + | Codex | `~/.codex/config.toml` (manual) | 23 + 24 + --- 25 + 26 + ## Tools 27 + 28 + > **Tip:** MCP tools handle registry operations (search, view, install). For project configuration (aliases, framework, Tailwind version), use `npx shadcn@latest info` — there is no MCP equivalent. 29 + 30 + ### `shadcn:get_project_registries` 31 + 32 + Returns registry names from `components.json`. Errors if no `components.json` exists. 33 + 34 + **Input:** none 35 + 36 + ### `shadcn:list_items_in_registries` 37 + 38 + Lists all items from one or more registries. 39 + 40 + **Input:** `registries` (string[]), `limit` (number, optional), `offset` (number, optional) 41 + 42 + ### `shadcn:search_items_in_registries` 43 + 44 + Fuzzy search across registries. 45 + 46 + **Input:** `registries` (string[]), `query` (string), `limit` (number, optional), `offset` (number, optional) 47 + 48 + ### `shadcn:view_items_in_registries` 49 + 50 + View item details including full file contents. 51 + 52 + **Input:** `items` (string[]) — e.g. `["@shadcn/button", "@shadcn/card"]` 53 + 54 + ### `shadcn:get_item_examples_from_registries` 55 + 56 + Find usage examples and demos with source code. 57 + 58 + **Input:** `registries` (string[]), `query` (string) — e.g. `"accordion-demo"`, `"button example"` 59 + 60 + ### `shadcn:get_add_command_for_items` 61 + 62 + Returns the CLI install command. 63 + 64 + **Input:** `items` (string[]) — e.g. `["@shadcn/button"]` 65 + 66 + ### `shadcn:get_audit_checklist` 67 + 68 + Returns a checklist for verifying components (imports, deps, lint, TypeScript). 69 + 70 + **Input:** none 71 + 72 + --- 73 + 74 + ## Configuring Registries 75 + 76 + Registries are set in `components.json`. The `@shadcn` registry is always built-in. 77 + 78 + ```json 79 + { 80 + "registries": { 81 + "@acme": "https://acme.com/r/{name}.json", 82 + "@private": { 83 + "url": "https://private.com/r/{name}.json", 84 + "headers": { "Authorization": "Bearer ${MY_TOKEN}" } 85 + } 86 + } 87 + } 88 + ``` 89 + 90 + - Names must start with `@`. 91 + - URLs must contain `{name}`. 92 + - `${VAR}` references are resolved from environment variables. 93 + 94 + Community registry index: `https://ui.shadcn.com/r/registries.json`
+306
skills/shadcn/rules/base-vs-radix.md
··· 1 + # Base vs Radix 2 + 3 + API differences between `base` and `radix`. Check the `base` field from `npx shadcn@latest info`. 4 + 5 + ## Contents 6 + 7 + - Composition: asChild vs render 8 + - Button / trigger as non-button element 9 + - Select (items prop, placeholder, positioning, multiple, object values) 10 + - ToggleGroup (type vs multiple) 11 + - Slider (scalar vs array) 12 + - Accordion (type and defaultValue) 13 + 14 + --- 15 + 16 + ## Composition: asChild (radix) vs render (base) 17 + 18 + Radix uses `asChild` to replace the default element. Base uses `render`. Don't wrap triggers in extra elements. 19 + 20 + **Incorrect:** 21 + 22 + ```tsx 23 + <DialogTrigger> 24 + <div> 25 + <Button>Open</Button> 26 + </div> 27 + </DialogTrigger> 28 + ``` 29 + 30 + **Correct (radix):** 31 + 32 + ```tsx 33 + <DialogTrigger asChild> 34 + <Button>Open</Button> 35 + </DialogTrigger> 36 + ``` 37 + 38 + **Correct (base):** 39 + 40 + ```tsx 41 + <DialogTrigger render={<Button />}>Open</DialogTrigger> 42 + ``` 43 + 44 + This applies to all trigger and close components: `DialogTrigger`, `SheetTrigger`, `AlertDialogTrigger`, `DropdownMenuTrigger`, `PopoverTrigger`, `TooltipTrigger`, `CollapsibleTrigger`, `DialogClose`, `SheetClose`, `NavigationMenuLink`, `BreadcrumbLink`, `SidebarMenuButton`, `Badge`, `Item`. 45 + 46 + --- 47 + 48 + ## Button / trigger as non-button element (base only) 49 + 50 + When `render` changes an element to a non-button (`<a>`, `<span>`), add `nativeButton={false}`. 51 + 52 + **Incorrect (base):** missing `nativeButton={false}`. 53 + 54 + ```tsx 55 + <Button render={<a href="/docs" />}>Read the docs</Button> 56 + ``` 57 + 58 + **Correct (base):** 59 + 60 + ```tsx 61 + <Button render={<a href="/docs" />} nativeButton={false}> 62 + Read the docs 63 + </Button> 64 + ``` 65 + 66 + **Correct (radix):** 67 + 68 + ```tsx 69 + <Button asChild> 70 + <a href="/docs">Read the docs</a> 71 + </Button> 72 + ``` 73 + 74 + Same for triggers whose `render` is not a `Button`: 75 + 76 + ```tsx 77 + // base. 78 + <PopoverTrigger render={<InputGroupAddon />} nativeButton={false}> 79 + Pick date 80 + </PopoverTrigger> 81 + ``` 82 + 83 + --- 84 + 85 + ## Select 86 + 87 + **items prop (base only).** Base requires an `items` prop on the root. Radix uses inline JSX only. 88 + 89 + **Incorrect (base):** 90 + 91 + ```tsx 92 + <Select> 93 + <SelectTrigger><SelectValue placeholder="Select a fruit" /></SelectTrigger> 94 + </Select> 95 + ``` 96 + 97 + **Correct (base):** 98 + 99 + ```tsx 100 + const items = [ 101 + { label: "Select a fruit", value: null }, 102 + { label: "Apple", value: "apple" }, 103 + { label: "Banana", value: "banana" }, 104 + ] 105 + 106 + <Select items={items}> 107 + <SelectTrigger> 108 + <SelectValue /> 109 + </SelectTrigger> 110 + <SelectContent> 111 + <SelectGroup> 112 + {items.map((item) => ( 113 + <SelectItem key={item.value} value={item.value}>{item.label}</SelectItem> 114 + ))} 115 + </SelectGroup> 116 + </SelectContent> 117 + </Select> 118 + ``` 119 + 120 + **Correct (radix):** 121 + 122 + ```tsx 123 + <Select> 124 + <SelectTrigger> 125 + <SelectValue placeholder="Select a fruit" /> 126 + </SelectTrigger> 127 + <SelectContent> 128 + <SelectGroup> 129 + <SelectItem value="apple">Apple</SelectItem> 130 + <SelectItem value="banana">Banana</SelectItem> 131 + </SelectGroup> 132 + </SelectContent> 133 + </Select> 134 + ``` 135 + 136 + **Placeholder.** Base uses a `{ value: null }` item in the items array. Radix uses `<SelectValue placeholder="...">`. 137 + 138 + **Content positioning.** Base uses `alignItemWithTrigger`. Radix uses `position`. 139 + 140 + ```tsx 141 + // base. 142 + <SelectContent alignItemWithTrigger={false} side="bottom"> 143 + 144 + // radix. 145 + <SelectContent position="popper"> 146 + ``` 147 + 148 + --- 149 + 150 + ## Select — multiple selection and object values (base only) 151 + 152 + Base supports `multiple`, render-function children on `SelectValue`, and object values with `itemToStringValue`. Radix is single-select with string values only. 153 + 154 + **Correct (base — multiple selection):** 155 + 156 + ```tsx 157 + <Select items={items} multiple defaultValue={[]}> 158 + <SelectTrigger> 159 + <SelectValue> 160 + {(value: string[]) => value.length === 0 ? "Select fruits" : `${value.length} selected`} 161 + </SelectValue> 162 + </SelectTrigger> 163 + ... 164 + </Select> 165 + ``` 166 + 167 + **Correct (base — object values):** 168 + 169 + ```tsx 170 + <Select defaultValue={plans[0]} itemToStringValue={(plan) => plan.name}> 171 + <SelectTrigger> 172 + <SelectValue>{(value) => value.name}</SelectValue> 173 + </SelectTrigger> 174 + ... 175 + </Select> 176 + ``` 177 + 178 + --- 179 + 180 + ## ToggleGroup 181 + 182 + Base uses a `multiple` boolean prop. Radix uses `type="single"` or `type="multiple"`. 183 + 184 + **Incorrect (base):** 185 + 186 + ```tsx 187 + <ToggleGroup type="single" defaultValue="daily"> 188 + <ToggleGroupItem value="daily">Daily</ToggleGroupItem> 189 + </ToggleGroup> 190 + ``` 191 + 192 + **Correct (base):** 193 + 194 + ```tsx 195 + // Single (no prop needed), defaultValue is always an array. 196 + <ToggleGroup defaultValue={["daily"]} spacing={2}> 197 + <ToggleGroupItem value="daily">Daily</ToggleGroupItem> 198 + <ToggleGroupItem value="weekly">Weekly</ToggleGroupItem> 199 + </ToggleGroup> 200 + 201 + // Multi-selection. 202 + <ToggleGroup multiple> 203 + <ToggleGroupItem value="bold">Bold</ToggleGroupItem> 204 + <ToggleGroupItem value="italic">Italic</ToggleGroupItem> 205 + </ToggleGroup> 206 + ``` 207 + 208 + **Correct (radix):** 209 + 210 + ```tsx 211 + // Single, defaultValue is a string. 212 + <ToggleGroup type="single" defaultValue="daily" spacing={2}> 213 + <ToggleGroupItem value="daily">Daily</ToggleGroupItem> 214 + <ToggleGroupItem value="weekly">Weekly</ToggleGroupItem> 215 + </ToggleGroup> 216 + 217 + // Multi-selection. 218 + <ToggleGroup type="multiple"> 219 + <ToggleGroupItem value="bold">Bold</ToggleGroupItem> 220 + <ToggleGroupItem value="italic">Italic</ToggleGroupItem> 221 + </ToggleGroup> 222 + ``` 223 + 224 + **Controlled single value:** 225 + 226 + ```tsx 227 + // base — wrap/unwrap arrays. 228 + const [value, setValue] = React.useState("normal") 229 + <ToggleGroup value={[value]} onValueChange={(v) => setValue(v[0])}> 230 + 231 + // radix — plain string. 232 + const [value, setValue] = React.useState("normal") 233 + <ToggleGroup type="single" value={value} onValueChange={setValue}> 234 + ``` 235 + 236 + --- 237 + 238 + ## Slider 239 + 240 + Base accepts a plain number for a single thumb. Radix always requires an array. 241 + 242 + **Incorrect (base):** 243 + 244 + ```tsx 245 + <Slider defaultValue={[50]} max={100} step={1} /> 246 + ``` 247 + 248 + **Correct (base):** 249 + 250 + ```tsx 251 + <Slider defaultValue={50} max={100} step={1} /> 252 + ``` 253 + 254 + **Correct (radix):** 255 + 256 + ```tsx 257 + <Slider defaultValue={[50]} max={100} step={1} /> 258 + ``` 259 + 260 + Both use arrays for range sliders. Controlled `onValueChange` in base may need a cast: 261 + 262 + ```tsx 263 + // base. 264 + const [value, setValue] = React.useState([0.3, 0.7]) 265 + <Slider value={value} onValueChange={(v) => setValue(v as number[])} /> 266 + 267 + // radix. 268 + const [value, setValue] = React.useState([0.3, 0.7]) 269 + <Slider value={value} onValueChange={setValue} /> 270 + ``` 271 + 272 + --- 273 + 274 + ## Accordion 275 + 276 + Radix requires `type="single"` or `type="multiple"` and supports `collapsible`. `defaultValue` is a string. Base uses no `type` prop, uses `multiple` boolean, and `defaultValue` is always an array. 277 + 278 + **Incorrect (base):** 279 + 280 + ```tsx 281 + <Accordion type="single" collapsible defaultValue="item-1"> 282 + <AccordionItem value="item-1">...</AccordionItem> 283 + </Accordion> 284 + ``` 285 + 286 + **Correct (base):** 287 + 288 + ```tsx 289 + <Accordion defaultValue={["item-1"]}> 290 + <AccordionItem value="item-1">...</AccordionItem> 291 + </Accordion> 292 + 293 + // Multi-select. 294 + <Accordion multiple defaultValue={["item-1", "item-2"]}> 295 + <AccordionItem value="item-1">...</AccordionItem> 296 + <AccordionItem value="item-2">...</AccordionItem> 297 + </Accordion> 298 + ``` 299 + 300 + **Correct (radix):** 301 + 302 + ```tsx 303 + <Accordion type="single" collapsible defaultValue="item-1"> 304 + <AccordionItem value="item-1">...</AccordionItem> 305 + </Accordion> 306 + ```
+195
skills/shadcn/rules/composition.md
··· 1 + # Component Composition 2 + 3 + ## Contents 4 + 5 + - Items always inside their Group component 6 + - Callouts use Alert 7 + - Empty states use Empty component 8 + - Toast notifications use sonner 9 + - Choosing between overlay components 10 + - Dialog, Sheet, and Drawer always need a Title 11 + - Card structure 12 + - Button has no isPending or isLoading prop 13 + - TabsTrigger must be inside TabsList 14 + - Avatar always needs AvatarFallback 15 + - Use Separator instead of raw hr or border divs 16 + - Use Skeleton for loading placeholders 17 + - Use Badge instead of custom styled spans 18 + 19 + --- 20 + 21 + ## Items always inside their Group component 22 + 23 + Never render items directly inside the content container. 24 + 25 + **Incorrect:** 26 + 27 + ```tsx 28 + <SelectContent> 29 + <SelectItem value="apple">Apple</SelectItem> 30 + <SelectItem value="banana">Banana</SelectItem> 31 + </SelectContent> 32 + ``` 33 + 34 + **Correct:** 35 + 36 + ```tsx 37 + <SelectContent> 38 + <SelectGroup> 39 + <SelectItem value="apple">Apple</SelectItem> 40 + <SelectItem value="banana">Banana</SelectItem> 41 + </SelectGroup> 42 + </SelectContent> 43 + ``` 44 + 45 + This applies to all group-based components: 46 + 47 + | Item | Group | 48 + |------|-------| 49 + | `SelectItem`, `SelectLabel` | `SelectGroup` | 50 + | `DropdownMenuItem`, `DropdownMenuLabel`, `DropdownMenuSub` | `DropdownMenuGroup` | 51 + | `MenubarItem` | `MenubarGroup` | 52 + | `ContextMenuItem` | `ContextMenuGroup` | 53 + | `CommandItem` | `CommandGroup` | 54 + 55 + --- 56 + 57 + ## Callouts use Alert 58 + 59 + ```tsx 60 + <Alert> 61 + <AlertTitle>Warning</AlertTitle> 62 + <AlertDescription>Something needs attention.</AlertDescription> 63 + </Alert> 64 + ``` 65 + 66 + --- 67 + 68 + ## Empty states use Empty component 69 + 70 + ```tsx 71 + <Empty> 72 + <EmptyHeader> 73 + <EmptyMedia variant="icon"><FolderIcon /></EmptyMedia> 74 + <EmptyTitle>No projects yet</EmptyTitle> 75 + <EmptyDescription>Get started by creating a new project.</EmptyDescription> 76 + </EmptyHeader> 77 + <EmptyContent> 78 + <Button>Create Project</Button> 79 + </EmptyContent> 80 + </Empty> 81 + ``` 82 + 83 + --- 84 + 85 + ## Toast notifications use sonner 86 + 87 + ```tsx 88 + import { toast } from "sonner" 89 + 90 + toast.success("Changes saved.") 91 + toast.error("Something went wrong.") 92 + toast("File deleted.", { 93 + action: { label: "Undo", onClick: () => undoDelete() }, 94 + }) 95 + ``` 96 + 97 + --- 98 + 99 + ## Choosing between overlay components 100 + 101 + | Use case | Component | 102 + |----------|-----------| 103 + | Focused task that requires input | `Dialog` | 104 + | Destructive action confirmation | `AlertDialog` | 105 + | Side panel with details or filters | `Sheet` | 106 + | Mobile-first bottom panel | `Drawer` | 107 + | Quick info on hover | `HoverCard` | 108 + | Small contextual content on click | `Popover` | 109 + 110 + --- 111 + 112 + ## Dialog, Sheet, and Drawer always need a Title 113 + 114 + `DialogTitle`, `SheetTitle`, `DrawerTitle` are required for accessibility. Use `className="sr-only"` if visually hidden. 115 + 116 + ```tsx 117 + <DialogContent> 118 + <DialogHeader> 119 + <DialogTitle>Edit Profile</DialogTitle> 120 + <DialogDescription>Update your profile.</DialogDescription> 121 + </DialogHeader> 122 + ... 123 + </DialogContent> 124 + ``` 125 + 126 + --- 127 + 128 + ## Card structure 129 + 130 + Use full composition — don't dump everything into `CardContent`: 131 + 132 + ```tsx 133 + <Card> 134 + <CardHeader> 135 + <CardTitle>Team Members</CardTitle> 136 + <CardDescription>Manage your team.</CardDescription> 137 + </CardHeader> 138 + <CardContent>...</CardContent> 139 + <CardFooter> 140 + <Button>Invite</Button> 141 + </CardFooter> 142 + </Card> 143 + ``` 144 + 145 + --- 146 + 147 + ## Button has no isPending or isLoading prop 148 + 149 + Compose with `Spinner` + `data-icon` + `disabled`: 150 + 151 + ```tsx 152 + <Button disabled> 153 + <Spinner data-icon="inline-start" /> 154 + Saving... 155 + </Button> 156 + ``` 157 + 158 + --- 159 + 160 + ## TabsTrigger must be inside TabsList 161 + 162 + Never render `TabsTrigger` directly inside `Tabs` — always wrap in `TabsList`: 163 + 164 + ```tsx 165 + <Tabs defaultValue="account"> 166 + <TabsList> 167 + <TabsTrigger value="account">Account</TabsTrigger> 168 + <TabsTrigger value="password">Password</TabsTrigger> 169 + </TabsList> 170 + <TabsContent value="account">...</TabsContent> 171 + </Tabs> 172 + ``` 173 + 174 + --- 175 + 176 + ## Avatar always needs AvatarFallback 177 + 178 + Always include `AvatarFallback` for when the image fails to load: 179 + 180 + ```tsx 181 + <Avatar> 182 + <AvatarImage src="/avatar.png" alt="User" /> 183 + <AvatarFallback>JD</AvatarFallback> 184 + </Avatar> 185 + ``` 186 + 187 + --- 188 + 189 + ## Use existing components instead of custom markup 190 + 191 + | Instead of | Use | 192 + |---|---| 193 + | `<hr>` or `<div className="border-t">` | `<Separator />` | 194 + | `<div className="animate-pulse">` with styled divs | `<Skeleton className="h-4 w-3/4" />` | 195 + | `<span className="rounded-full bg-green-100 ...">` | `<Badge variant="secondary">` |
+192
skills/shadcn/rules/forms.md
··· 1 + # Forms & Inputs 2 + 3 + ## Contents 4 + 5 + - Forms use FieldGroup + Field 6 + - InputGroup requires InputGroupInput/InputGroupTextarea 7 + - Buttons inside inputs use InputGroup + InputGroupAddon 8 + - Option sets (2–7 choices) use ToggleGroup 9 + - FieldSet + FieldLegend for grouping related fields 10 + - Field validation and disabled states 11 + 12 + --- 13 + 14 + ## Forms use FieldGroup + Field 15 + 16 + Always use `FieldGroup` + `Field` — never raw `div` with `space-y-*`: 17 + 18 + ```tsx 19 + <FieldGroup> 20 + <Field> 21 + <FieldLabel htmlFor="email">Email</FieldLabel> 22 + <Input id="email" type="email" /> 23 + </Field> 24 + <Field> 25 + <FieldLabel htmlFor="password">Password</FieldLabel> 26 + <Input id="password" type="password" /> 27 + </Field> 28 + </FieldGroup> 29 + ``` 30 + 31 + Use `Field orientation="horizontal"` for settings pages. Use `FieldLabel className="sr-only"` for visually hidden labels. 32 + 33 + **Choosing form controls:** 34 + 35 + - Simple text input → `Input` 36 + - Dropdown with predefined options → `Select` 37 + - Searchable dropdown → `Combobox` 38 + - Native HTML select (no JS) → `native-select` 39 + - Boolean toggle → `Switch` (for settings) or `Checkbox` (for forms) 40 + - Single choice from few options → `RadioGroup` 41 + - Toggle between 2–5 options → `ToggleGroup` + `ToggleGroupItem` 42 + - OTP/verification code → `InputOTP` 43 + - Multi-line text → `Textarea` 44 + 45 + --- 46 + 47 + ## InputGroup requires InputGroupInput/InputGroupTextarea 48 + 49 + Never use raw `Input` or `Textarea` inside an `InputGroup`. 50 + 51 + **Incorrect:** 52 + 53 + ```tsx 54 + <InputGroup> 55 + <Input placeholder="Search..." /> 56 + </InputGroup> 57 + ``` 58 + 59 + **Correct:** 60 + 61 + ```tsx 62 + import { InputGroup, InputGroupInput } from "@/components/ui/input-group" 63 + 64 + <InputGroup> 65 + <InputGroupInput placeholder="Search..." /> 66 + </InputGroup> 67 + ``` 68 + 69 + --- 70 + 71 + ## Buttons inside inputs use InputGroup + InputGroupAddon 72 + 73 + Never place a `Button` directly inside or adjacent to an `Input` with custom positioning. 74 + 75 + **Incorrect:** 76 + 77 + ```tsx 78 + <div className="relative"> 79 + <Input placeholder="Search..." className="pr-10" /> 80 + <Button className="absolute right-0 top-0" size="icon"> 81 + <SearchIcon /> 82 + </Button> 83 + </div> 84 + ``` 85 + 86 + **Correct:** 87 + 88 + ```tsx 89 + import { InputGroup, InputGroupInput, InputGroupAddon } from "@/components/ui/input-group" 90 + 91 + <InputGroup> 92 + <InputGroupInput placeholder="Search..." /> 93 + <InputGroupAddon> 94 + <Button size="icon"> 95 + <SearchIcon data-icon="inline-start" /> 96 + </Button> 97 + </InputGroupAddon> 98 + </InputGroup> 99 + ``` 100 + 101 + --- 102 + 103 + ## Option sets (2–7 choices) use ToggleGroup 104 + 105 + Don't manually loop `Button` components with active state. 106 + 107 + **Incorrect:** 108 + 109 + ```tsx 110 + const [selected, setSelected] = useState("daily") 111 + 112 + <div className="flex gap-2"> 113 + {["daily", "weekly", "monthly"].map((option) => ( 114 + <Button 115 + key={option} 116 + variant={selected === option ? "default" : "outline"} 117 + onClick={() => setSelected(option)} 118 + > 119 + {option} 120 + </Button> 121 + ))} 122 + </div> 123 + ``` 124 + 125 + **Correct:** 126 + 127 + ```tsx 128 + import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" 129 + 130 + <ToggleGroup spacing={2}> 131 + <ToggleGroupItem value="daily">Daily</ToggleGroupItem> 132 + <ToggleGroupItem value="weekly">Weekly</ToggleGroupItem> 133 + <ToggleGroupItem value="monthly">Monthly</ToggleGroupItem> 134 + </ToggleGroup> 135 + ``` 136 + 137 + Combine with `Field` for labelled toggle groups: 138 + 139 + ```tsx 140 + <Field orientation="horizontal"> 141 + <FieldTitle id="theme-label">Theme</FieldTitle> 142 + <ToggleGroup aria-labelledby="theme-label" spacing={2}> 143 + <ToggleGroupItem value="light">Light</ToggleGroupItem> 144 + <ToggleGroupItem value="dark">Dark</ToggleGroupItem> 145 + <ToggleGroupItem value="system">System</ToggleGroupItem> 146 + </ToggleGroup> 147 + </Field> 148 + ``` 149 + 150 + > **Note:** `defaultValue` and `type`/`multiple` props differ between base and radix. See [base-vs-radix.md](./base-vs-radix.md#togglegroup). 151 + 152 + --- 153 + 154 + ## FieldSet + FieldLegend for grouping related fields 155 + 156 + Use `FieldSet` + `FieldLegend` for related checkboxes, radios, or switches — not `div` with a heading: 157 + 158 + ```tsx 159 + <FieldSet> 160 + <FieldLegend variant="label">Preferences</FieldLegend> 161 + <FieldDescription>Select all that apply.</FieldDescription> 162 + <FieldGroup className="gap-3"> 163 + <Field orientation="horizontal"> 164 + <Checkbox id="dark" /> 165 + <FieldLabel htmlFor="dark" className="font-normal">Dark mode</FieldLabel> 166 + </Field> 167 + </FieldGroup> 168 + </FieldSet> 169 + ``` 170 + 171 + --- 172 + 173 + ## Field validation and disabled states 174 + 175 + Both attributes are needed — `data-invalid`/`data-disabled` styles the field (label, description), while `aria-invalid`/`disabled` styles the control. 176 + 177 + ```tsx 178 + // Invalid. 179 + <Field data-invalid> 180 + <FieldLabel htmlFor="email">Email</FieldLabel> 181 + <Input id="email" aria-invalid /> 182 + <FieldDescription>Invalid email address.</FieldDescription> 183 + </Field> 184 + 185 + // Disabled. 186 + <Field data-disabled> 187 + <FieldLabel htmlFor="email">Email</FieldLabel> 188 + <Input id="email" disabled /> 189 + </Field> 190 + ``` 191 + 192 + Works for all controls: `Input`, `Textarea`, `Select`, `Checkbox`, `RadioGroupItem`, `Switch`, `Slider`, `NativeSelect`, `InputOTP`.
+101
skills/shadcn/rules/icons.md
··· 1 + # Icons 2 + 3 + **Always use the project's configured `iconLibrary` for imports.** Check the `iconLibrary` field from project context: `lucide` → `lucide-react`, `tabler` → `@tabler/icons-react`, etc. Never assume `lucide-react`. 4 + 5 + --- 6 + 7 + ## Icons in Button use data-icon attribute 8 + 9 + Add `data-icon="inline-start"` (prefix) or `data-icon="inline-end"` (suffix) to the icon. No sizing classes on the icon. 10 + 11 + **Incorrect:** 12 + 13 + ```tsx 14 + <Button> 15 + <SearchIcon className="mr-2 size-4" /> 16 + Search 17 + </Button> 18 + ``` 19 + 20 + **Correct:** 21 + 22 + ```tsx 23 + <Button> 24 + <SearchIcon data-icon="inline-start"/> 25 + Search 26 + </Button> 27 + 28 + <Button> 29 + Next 30 + <ArrowRightIcon data-icon="inline-end"/> 31 + </Button> 32 + ``` 33 + 34 + --- 35 + 36 + ## No sizing classes on icons inside components 37 + 38 + Components handle icon sizing via CSS. Don't add `size-4`, `w-4 h-4`, or other sizing classes to icons inside `Button`, `DropdownMenuItem`, `Alert`, `Sidebar*`, or other shadcn components. Unless the user explicitly asks for custom icon sizes. 39 + 40 + **Incorrect:** 41 + 42 + ```tsx 43 + <Button> 44 + <SearchIcon className="size-4" data-icon="inline-start" /> 45 + Search 46 + </Button> 47 + 48 + <DropdownMenuItem> 49 + <SettingsIcon className="mr-2 size-4" /> 50 + Settings 51 + </DropdownMenuItem> 52 + ``` 53 + 54 + **Correct:** 55 + 56 + ```tsx 57 + <Button> 58 + <SearchIcon data-icon="inline-start" /> 59 + Search 60 + </Button> 61 + 62 + <DropdownMenuItem> 63 + <SettingsIcon /> 64 + Settings 65 + </DropdownMenuItem> 66 + ``` 67 + 68 + --- 69 + 70 + ## Pass icons as component objects, not string keys 71 + 72 + Use `icon={CheckIcon}`, not a string key to a lookup map. 73 + 74 + **Incorrect:** 75 + 76 + ```tsx 77 + const iconMap = { 78 + check: CheckIcon, 79 + alert: AlertIcon, 80 + } 81 + 82 + function StatusBadge({ icon }: { icon: string }) { 83 + const Icon = iconMap[icon] 84 + return <Icon /> 85 + } 86 + 87 + <StatusBadge icon="check" /> 88 + ``` 89 + 90 + **Correct:** 91 + 92 + ```tsx 93 + // Import from the project's configured iconLibrary (e.g. lucide-react, @tabler/icons-react). 94 + import { CheckIcon } from "lucide-react" 95 + 96 + function StatusBadge({ icon: Icon }: { icon: React.ComponentType }) { 97 + return <Icon /> 98 + } 99 + 100 + <StatusBadge icon={CheckIcon} /> 101 + ```
+162
skills/shadcn/rules/styling.md
··· 1 + # Styling & Customization 2 + 3 + See [customization.md](../customization.md) for theming, CSS variables, and adding custom colors. 4 + 5 + ## Contents 6 + 7 + - Semantic colors 8 + - Built-in variants first 9 + - className for layout only 10 + - No space-x-* / space-y-* 11 + - Prefer size-* over w-* h-* when equal 12 + - Prefer truncate shorthand 13 + - No manual dark: color overrides 14 + - Use cn() for conditional classes 15 + - No manual z-index on overlay components 16 + 17 + --- 18 + 19 + ## Semantic colors 20 + 21 + **Incorrect:** 22 + 23 + ```tsx 24 + <div className="bg-blue-500 text-white"> 25 + <p className="text-gray-600">Secondary text</p> 26 + </div> 27 + ``` 28 + 29 + **Correct:** 30 + 31 + ```tsx 32 + <div className="bg-primary text-primary-foreground"> 33 + <p className="text-muted-foreground">Secondary text</p> 34 + </div> 35 + ``` 36 + 37 + --- 38 + 39 + ## No raw color values for status/state indicators 40 + 41 + For positive, negative, or status indicators, use Badge variants, semantic tokens like `text-destructive`, or define custom CSS variables — don't reach for raw Tailwind colors. 42 + 43 + **Incorrect:** 44 + 45 + ```tsx 46 + <span className="text-emerald-600">+20.1%</span> 47 + <span className="text-green-500">Active</span> 48 + <span className="text-red-600">-3.2%</span> 49 + ``` 50 + 51 + **Correct:** 52 + 53 + ```tsx 54 + <Badge variant="secondary">+20.1%</Badge> 55 + <Badge>Active</Badge> 56 + <span className="text-destructive">-3.2%</span> 57 + ``` 58 + 59 + If you need a success/positive color that doesn't exist as a semantic token, use a Badge variant or ask the user about adding a custom CSS variable to the theme (see [customization.md](../customization.md)). 60 + 61 + --- 62 + 63 + ## Built-in variants first 64 + 65 + **Incorrect:** 66 + 67 + ```tsx 68 + <Button className="border border-input bg-transparent hover:bg-accent"> 69 + Click me 70 + </Button> 71 + ``` 72 + 73 + **Correct:** 74 + 75 + ```tsx 76 + <Button variant="outline">Click me</Button> 77 + ``` 78 + 79 + --- 80 + 81 + ## className for layout only 82 + 83 + Use `className` for layout (e.g. `max-w-md`, `mx-auto`, `mt-4`), **not** for overriding component colors or typography. To change colors, use semantic tokens, built-in variants, or CSS variables. 84 + 85 + **Incorrect:** 86 + 87 + ```tsx 88 + <Card className="bg-blue-100 text-blue-900 font-bold"> 89 + <CardContent>Dashboard</CardContent> 90 + </Card> 91 + ``` 92 + 93 + **Correct:** 94 + 95 + ```tsx 96 + <Card className="max-w-md mx-auto"> 97 + <CardContent>Dashboard</CardContent> 98 + </Card> 99 + ``` 100 + 101 + To customize a component's appearance, prefer these approaches in order: 102 + 1. **Built-in variants** — `variant="outline"`, `variant="destructive"`, etc. 103 + 2. **Semantic color tokens** — `bg-primary`, `text-muted-foreground`. 104 + 3. **CSS variables** — define custom colors in the global CSS file (see [customization.md](../customization.md)). 105 + 106 + --- 107 + 108 + ## No space-x-* / space-y-* 109 + 110 + Use `gap-*` instead. `space-y-4` → `flex flex-col gap-4`. `space-x-2` → `flex gap-2`. 111 + 112 + ```tsx 113 + <div className="flex flex-col gap-4"> 114 + <Input /> 115 + <Input /> 116 + <Button>Submit</Button> 117 + </div> 118 + ``` 119 + 120 + --- 121 + 122 + ## Prefer size-* over w-* h-* when equal 123 + 124 + `size-10` not `w-10 h-10`. Applies to icons, avatars, skeletons, etc. 125 + 126 + --- 127 + 128 + ## Prefer truncate shorthand 129 + 130 + `truncate` not `overflow-hidden text-ellipsis whitespace-nowrap`. 131 + 132 + --- 133 + 134 + ## No manual dark: color overrides 135 + 136 + Use semantic tokens — they handle light/dark via CSS variables. `bg-background text-foreground` not `bg-white dark:bg-gray-950`. 137 + 138 + --- 139 + 140 + ## Use cn() for conditional classes 141 + 142 + Use the `cn()` utility from the project for conditional or merged class names. Don't write manual ternaries in className strings. 143 + 144 + **Incorrect:** 145 + 146 + ```tsx 147 + <div className={`flex items-center ${isActive ? "bg-primary text-primary-foreground" : "bg-muted"}`}> 148 + ``` 149 + 150 + **Correct:** 151 + 152 + ```tsx 153 + import { cn } from "@/lib/utils" 154 + 155 + <div className={cn("flex items-center", isActive ? "bg-primary text-primary-foreground" : "bg-muted")}> 156 + ``` 157 + 158 + --- 159 + 160 + ## No manual z-index on overlay components 161 + 162 + `Dialog`, `Sheet`, `Drawer`, `AlertDialog`, `DropdownMenu`, `Popover`, `Tooltip`, `HoverCard` handle their own stacking. Never add `z-50` or `z-[999]`.