diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..52bb857 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,4 @@ +@./skills/caveman/SKILL.md +@./skills/caveman-commit/SKILL.md +@./skills/caveman-review/SKILL.md +@./skills/compress/SKILL.md diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..52bb857 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,4 @@ +@./skills/caveman/SKILL.md +@./skills/caveman-commit/SKILL.md +@./skills/caveman-review/SKILL.md +@./skills/compress/SKILL.md diff --git a/README.md b/README.md index 97110d0..ad91460 100644 --- a/README.md +++ b/README.md @@ -124,14 +124,14 @@ Based on the viral observation that caveman-speak dramatically reduces LLM token ### Claude Code (recommended) -Install as a plugin — includes skills + auto-loading hooks (caveman activates every session, mode badge tracks `/caveman ultra` etc.): +Install as a plugin — includes skills + auto-loading hooks + statusline badge. Caveman activates every session, `[CAVEMAN:ULTRA]` badge tracks mode, Claude sets up the statusline on first session: ```bash claude plugin marketplace add JuliusBrussee/caveman claude plugin install caveman@caveman ``` -### Any agent (Claude Code, Cursor, Copilot, Windsurf, Cline, Codex) +### Any agent (Claude Code, Cursor, Copilot, Windsurf, Cline, Codex, Gemini CLI, Antigravity) ```bash npx skills add JuliusBrussee/caveman @@ -141,6 +141,7 @@ For a specific agent: `npx skills add JuliusBrussee/caveman -a cursor` > [!NOTE] > `npx skills` installs skills only (no hooks). For Claude Code auto-loading hooks, use the plugin install above or run `bash hooks/install.sh`. +> Gemini CLI users can also install directly: `gemini extensions install https://github.com/JuliusBrussee/caveman` ### Codex @@ -151,9 +152,13 @@ For a specific agent: `npx skills add JuliusBrussee/caveman -a cursor` Install once. Use in all sessions after that. One rock. That it. -### Optional: Statusline Badge +### Statusline Badge -Add a `[CAVEMAN:ULTRA]` badge to your statusline showing which mode is active. See [`hooks/README.md`](hooks/README.md) for the snippet. +The plugin ships a statusline script that shows `[CAVEMAN]`, `[CAVEMAN:ULTRA]`, etc. in your Claude Code status bar. + +- **Plugin install:** Claude offers to configure it on first session (auto-detected) +- **Standalone install (`install.sh`):** Configured automatically +- **Custom statusline:** See [`hooks/README.md`](hooks/README.md) for the snippet to add to your existing script ## Usage diff --git a/caveman-compress/SKILL.md b/caveman-compress/SKILL.md index 8d299a6..7b3e3aa 100644 --- a/caveman-compress/SKILL.md +++ b/caveman-compress/SKILL.md @@ -1,5 +1,5 @@ --- -name: compress +name: caveman-compress description: > Compress natural language memory files (CLAUDE.md, todos, preferences) into caveman format to save input tokens. Preserves all technical substance, code, URLs, and structure. @@ -19,11 +19,11 @@ Compress natural language files (CLAUDE.md, todos, preferences) into caveman-spe ## Process -1. This SKILL.md lives alongside `scripts/` in the same directory. Find that directory. +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`. 2. Run: -cd && python3 -m scripts +cd caveman-compress && python3 -m scripts 3. The CLI will: - detect file type (no tokens) @@ -31,6 +31,7 @@ cd && python3 -m scripts str: + """Strip outer ```markdown ... ``` fence when it wraps the entire output.""" + m = OUTER_FENCE_REGEX.match(text) + if m: + return m.group(2) + return text + from .detect import should_compress from .validate import validate @@ -29,10 +42,10 @@ def call_claude(prompt: str) -> str: client = anthropic.Anthropic(api_key=api_key) msg = client.messages.create( model=os.environ.get("CAVEMAN_MODEL", "claude-sonnet-4-5"), - max_tokens=8096, + max_tokens=8192, messages=[{"role": "user", "content": prompt}], ) - return msg.content[0].text.strip() + return strip_llm_wrapper(msg.content[0].text.strip()) except ImportError: pass # anthropic not installed, fall back to CLI # Fallback: use claude CLI (handles desktop auth) @@ -44,7 +57,7 @@ def call_claude(prompt: str) -> str: capture_output=True, check=True, ) - return result.stdout.strip() + return strip_llm_wrapper(result.stdout.strip()) except subprocess.CalledProcessError as e: raise RuntimeError(f"Claude call failed:\n{e.stderr}") @@ -59,6 +72,7 @@ STRICT RULES: - Preserve ALL URLs exactly - Preserve ALL headings exactly - Preserve file paths and commands +- 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. Only compress natural language. diff --git a/caveman-compress/scripts/validate.py b/caveman-compress/scripts/validate.py index d28f1f8..a8b3490 100644 --- a/caveman-compress/scripts/validate.py +++ b/caveman-compress/scripts/validate.py @@ -3,7 +3,7 @@ import re from pathlib import Path URL_REGEX = re.compile(r"https?://[^\s)]+") -CODE_BLOCK_REGEX = re.compile(r"```.*?```", re.DOTALL) +FENCE_OPEN_REGEX = re.compile(r"^(\s{0,3})(`{3,}|~{3,})(.*)$") HEADING_REGEX = re.compile(r"^(#{1,6})\s+(.*)", re.MULTILINE) BULLET_REGEX = re.compile(r"^\s*[-*+]\s+", re.MULTILINE) @@ -38,7 +38,44 @@ def extract_headings(text): def extract_code_blocks(text): - return CODE_BLOCK_REGEX.findall(text) + """Line-based fenced code block extractor. + + Handles ``` and ~~~ fences with variable length (CommonMark: closing + fence must use same char and be at least as long as opening). Supports + nested fences (e.g. an outer 4-backtick block wrapping inner 3-backtick + content). + """ + blocks = [] + lines = text.split("\n") + i = 0 + n = len(lines) + while i < n: + m = FENCE_OPEN_REGEX.match(lines[i]) + if not m: + i += 1 + continue + fence_char = m.group(2)[0] + fence_len = len(m.group(2)) + open_line = lines[i] + block_lines = [open_line] + i += 1 + closed = False + while i < n: + close_m = FENCE_OPEN_REGEX.match(lines[i]) + if ( + close_m + and close_m.group(2)[0] == fence_char + and len(close_m.group(2)) >= fence_len + and close_m.group(3).strip() == "" + ): + block_lines.append(lines[i]) + closed = True + i += 1 + break + block_lines.append(lines[i]) + i += 1 + blocks.append("\n".join(block_lines)) + return blocks def extract_urls(text): diff --git a/gemini-extension.json b/gemini-extension.json new file mode 100644 index 0000000..771c481 --- /dev/null +++ b/gemini-extension.json @@ -0,0 +1,6 @@ +{ + "name": "caveman", + "description": "Ultra-compressed communication mode. Cuts ~75% of tokens while keeping full technical accuracy by speaking like a caveman.", + "version": "1.0.0", + "contextFileName": "GEMINI.md" +} diff --git a/hooks/README.md b/hooks/README.md index 412d38f..eb8abee 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -11,6 +11,7 @@ If you installed caveman standalone (without the plugin), you can use `bash hook - Runs once when Claude Code starts - Writes `full` to `~/.claude/.caveman-active` (flag file) - Emits caveman rules as hidden SessionStart context +- Detects missing statusline config and emits setup nudge (Claude will offer to help) ### `caveman-mode-tracker.js` — UserPromptSubmit hook @@ -18,9 +19,33 @@ If you installed caveman standalone (without the plugin), you can use `bash hook - Writes the active mode to the flag file when a caveman command is detected - Supports: `full`, `lite`, `ultra`, `wenyan`, `wenyan-lite`, `wenyan-ultra`, `commit`, `review`, `compress` -## Optional: Statusline Badge +### `caveman-statusline.sh` — Statusline badge script -The flag file bridges the gap between hooks (which Claude sees) and your statusline (which you see). Add this to your statusline script to show which mode is active: +- Reads `~/.claude/.caveman-active` and outputs a colored badge +- Shows `[CAVEMAN]`, `[CAVEMAN:ULTRA]`, `[CAVEMAN:WENYAN]`, etc. + +## Statusline Badge + +The statusline badge shows which caveman mode is active directly in your Claude Code status bar. + +**Plugin users:** On your first session after install, Claude will detect the missing statusline config and offer to set it up for you. Accept and you're done. + +**Standalone users:** `install.sh` wires the statusline automatically — no manual step needed. + +**Manual setup:** If you need to configure it yourself, add this to `~/.claude/settings.json`: + +```json +{ + "statusLine": { + "type": "command", + "command": "bash /path/to/caveman-statusline.sh" + } +} +``` + +Replace `/path/to/` with the actual path to the script (e.g. `~/.claude/hooks/` for standalone installs, or the plugin install directory for plugin installs). + +**Custom statusline:** If you already have a statusline script, add this snippet to it: ```bash caveman_text="" @@ -28,10 +53,10 @@ caveman_flag="$HOME/.claude/.caveman-active" if [ -f "$caveman_flag" ]; then caveman_mode=$(cat "$caveman_flag" 2>/dev/null) if [ "$caveman_mode" = "full" ] || [ -z "$caveman_mode" ]; then - caveman_text="\033[38;5;172m[CAVEMAN]\033[0m" + caveman_text=$'\033[38;5;172m[CAVEMAN]\033[0m' else caveman_suffix=$(echo "$caveman_mode" | tr '[:lower:]' '[:upper:]') - caveman_text="\033[38;5;172m[CAVEMAN:${caveman_suffix}]\033[0m" + caveman_text=$'\033[38;5;172m[CAVEMAN:'"${caveman_suffix}"$']\033[0m' fi fi ``` @@ -61,6 +86,11 @@ SessionStart stdout is injected as hidden system context — Claude sees it, use If installed via plugin: disable the plugin — hooks deactivate automatically. If installed via `install.sh`: -1. Remove `~/.claude/hooks/caveman-activate.js` and `~/.claude/hooks/caveman-mode-tracker.js` -2. Remove the SessionStart and UserPromptSubmit entries from `~/.claude/settings.json` +```bash +bash hooks/uninstall.sh +``` + +Or manually: +1. Remove `~/.claude/hooks/caveman-activate.js`, `~/.claude/hooks/caveman-mode-tracker.js`, and `~/.claude/hooks/caveman-statusline.sh` +2. Remove the SessionStart, UserPromptSubmit, and statusLine entries from `~/.claude/settings.json` 3. Delete `~/.claude/.caveman-active` diff --git a/hooks/caveman-activate.js b/hooks/caveman-activate.js index 3155e20..04f02f7 100644 --- a/hooks/caveman-activate.js +++ b/hooks/caveman-activate.js @@ -1,22 +1,20 @@ #!/usr/bin/env node -// caveman — optional Claude Code SessionStart activation hook +// caveman — Claude Code SessionStart activation hook // -// When wired into ~/.claude/settings.json as a SessionStart hook: -// - Writes a flag file at ~/.claude/.caveman-active so a statusline -// script can prove caveman mode is loaded (see README for the badge -// snippet — SessionStart stdout is otherwise invisible to users) -// - Emits a short ruleset reminder as SessionStart context -// -// This is a pure addition — if you don't wire it up, nothing changes. -// Install instructions: see the "Optional: SessionStart Hook" section -// in README.md. +// Runs on every session start: +// 1. Writes flag file at ~/.claude/.caveman-active (statusline reads this) +// 2. Emits caveman ruleset as hidden SessionStart context +// 3. Detects missing statusline config and emits setup nudge const fs = require('fs'); const path = require('path'); const os = require('os'); -const flagPath = path.join(os.homedir(), '.claude', '.caveman-active'); +const claudeDir = path.join(os.homedir(), '.claude'); +const flagPath = path.join(claudeDir, '.caveman-active'); +const settingsPath = path.join(claudeDir, 'settings.json'); +// 1. Write flag file try { fs.mkdirSync(path.dirname(flagPath), { recursive: true }); fs.writeFileSync(flagPath, 'full'); @@ -24,11 +22,40 @@ try { // Silent fail -- flag is best-effort, don't block the hook } -process.stdout.write( +// 2. Emit caveman rules (always) +let output = "CAVEMAN MODE ACTIVE. Rules: Drop articles/filler/pleasantries/hedging. " + "Fragments OK. Short synonyms. Pattern: [thing] [action] [reason]. [next step]. " + "Not: 'Sure! I'd be happy to help you with that.' " + "Yes: 'Bug in auth middleware. Fix:' " + "Code/commits/security: write normal. " + - "User says 'normal' or 'stop caveman' to deactivate." -); + "User says 'normal' or 'stop caveman' to deactivate."; + +// 3. Detect missing statusline config — nudge Claude to help set it up +try { + let hasStatusline = false; + if (fs.existsSync(settingsPath)) { + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + if (settings.statusLine && typeof settings.statusLine === 'object' && + settings.statusLine.command && settings.statusLine.command.includes('caveman')) { + hasStatusline = true; + } else if (typeof settings.statusLine === 'string' && settings.statusLine.includes('caveman')) { + hasStatusline = true; + } + } + + if (!hasStatusline) { + // Resolve the statusline script path — works for both plugin and standalone installs + const scriptPath = path.join(__dirname, 'caveman-statusline.sh'); + output += "\n\n" + + "STATUSLINE SETUP NEEDED: The caveman plugin includes a statusline badge showing active mode " + + "(e.g. [CAVEMAN], [CAVEMAN:ULTRA]). It is not configured yet. " + + "To enable, add this to ~/.claude/settings.json: " + + '"statusLine": { "type": "command", "command": "bash ' + scriptPath + '" } ' + + "Proactively offer to set this up for the user on first interaction."; + } +} catch (e) { + // Silent fail — don't block session start over statusline detection +} + +process.stdout.write(output); diff --git a/hooks/caveman-statusline.sh b/hooks/caveman-statusline.sh new file mode 100755 index 0000000..f849ef2 --- /dev/null +++ b/hooks/caveman-statusline.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# caveman — statusline badge script for Claude Code +# Reads the caveman mode flag file and outputs a colored badge. +# +# Usage in ~/.claude/settings.json: +# "statusLine": { "type": "command", "command": "bash /path/to/caveman-statusline.sh" } +# +# Plugin users: Claude will offer to set this up on first session. +# Standalone users: install.sh wires this automatically. + +FLAG="$HOME/.claude/.caveman-active" +[ ! -f "$FLAG" ] && exit 0 + +MODE=$(cat "$FLAG" 2>/dev/null) +if [ "$MODE" = "full" ] || [ -z "$MODE" ]; then + printf '\033[38;5;172m[CAVEMAN]\033[0m' +else + SUFFIX=$(echo "$MODE" | tr '[:lower:]' '[:upper:]') + printf '\033[38;5;172m[CAVEMAN:%s]\033[0m' "$SUFFIX" +fi diff --git a/hooks/install.sh b/hooks/install.sh index 994095d..db193c6 100755 --- a/hooks/install.sh +++ b/hooks/install.sh @@ -5,12 +5,20 @@ # or: bash <(curl -s https://raw.githubusercontent.com/JuliusBrussee/caveman/main/hooks/install.sh) set -e +# Require node — we use it to merge the hook config into settings.json +if ! command -v node >/dev/null 2>&1; then + echo "ERROR: 'node' is required to install the caveman hooks (used to merge" + echo " the hook config into ~/.claude/settings.json safely)." + echo " Install Node.js from https://nodejs.org and re-run this script." + exit 1 +fi + CLAUDE_DIR="$HOME/.claude" HOOKS_DIR="$CLAUDE_DIR/hooks" SETTINGS="$CLAUDE_DIR/settings.json" REPO_URL="https://raw.githubusercontent.com/JuliusBrussee/caveman/main/hooks" -HOOK_FILES=("caveman-activate.js" "caveman-mode-tracker.js") +HOOK_FILES=("caveman-activate.js" "caveman-mode-tracker.js" "caveman-statusline.sh") # Resolve source — works from repo clone or curl pipe SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}" 2>/dev/null)" 2>/dev/null && pwd)" @@ -30,11 +38,17 @@ for hook in "${HOOK_FILES[@]}"; do echo " Installed: $HOOKS_DIR/$hook" done -# 3. Wire hooks into settings.json (idempotent) +# Make statusline script executable +chmod +x "$HOOKS_DIR/caveman-statusline.sh" + +# 3. Wire hooks + statusline into settings.json (idempotent) if [ ! -f "$SETTINGS" ]; then echo '{}' > "$SETTINGS" fi +# Back up existing settings.json before touching it +cp "$SETTINGS" "$SETTINGS.bak" + node -e " const fs = require('fs'); const settings = JSON.parse(fs.readFileSync('$SETTINGS', 'utf8')); @@ -72,9 +86,17 @@ node -e " }); } + // Statusline — wire caveman badge (only if no statusLine is already configured) + if (!settings.statusLine) { + settings.statusLine = { + type: 'command', + command: 'bash $HOOKS_DIR/caveman-statusline.sh' + }; + } + fs.writeFileSync('$SETTINGS', JSON.stringify(settings, null, 2) + '\n'); " -echo " Hooks wired in settings.json" +echo " Hooks + statusline wired in settings.json" echo "" echo "Done! Restart Claude Code to activate." @@ -83,6 +105,4 @@ echo "What's installed:" echo " - SessionStart hook: auto-loads caveman rules every session" echo " - Mode tracker hook: updates statusline badge when you switch modes" echo " (/caveman lite, /caveman ultra, /caveman-commit, etc.)" -echo "" -echo "Optional: Add a [CAVEMAN] badge to your statusline." -echo "See: https://github.com/JuliusBrussee/caveman/blob/main/hooks/README.md" +echo " - Statusline badge: shows [CAVEMAN] or [CAVEMAN:ULTRA] etc." diff --git a/hooks/uninstall.sh b/hooks/uninstall.sh new file mode 100755 index 0000000..6ecf3f9 --- /dev/null +++ b/hooks/uninstall.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# caveman — uninstaller for the SessionStart + UserPromptSubmit hooks +# Removes: hook files in ~/.claude/hooks, settings.json entries, and the flag file +# Usage: bash hooks/uninstall.sh +# or: bash <(curl -s https://raw.githubusercontent.com/JuliusBrussee/caveman/main/hooks/uninstall.sh) +set -e + +CLAUDE_DIR="$HOME/.claude" +HOOKS_DIR="$CLAUDE_DIR/hooks" +SETTINGS="$CLAUDE_DIR/settings.json" +FLAG_FILE="$CLAUDE_DIR/.caveman-active" + +HOOK_FILES=("caveman-activate.js" "caveman-mode-tracker.js" "caveman-statusline.sh") + +echo "Uninstalling caveman hooks..." + +# 1. Remove hook files +for hook in "${HOOK_FILES[@]}"; do + if [ -f "$HOOKS_DIR/$hook" ]; then + rm "$HOOKS_DIR/$hook" + echo " Removed: $HOOKS_DIR/$hook" + fi +done + +# 2. Remove caveman entries from settings.json (idempotent) +if [ -f "$SETTINGS" ]; then + # Require node for the same reason install.sh does — safe JSON editing + if ! command -v node >/dev/null 2>&1; then + echo "WARNING: 'node' not found — cannot safely edit settings.json." + echo " Remove the caveman SessionStart and UserPromptSubmit" + echo " entries from $SETTINGS manually." + else + # Back up before editing, same policy as install.sh + cp "$SETTINGS" "$SETTINGS.bak" + + node -e " + const fs = require('fs'); + const settings = JSON.parse(fs.readFileSync('$SETTINGS', 'utf8')); + + const isCavemanEntry = (entry) => + entry && entry.hooks && entry.hooks.some(h => + h.command && h.command.includes('caveman') + ); + + let removed = 0; + if (settings.hooks) { + for (const event of ['SessionStart', 'UserPromptSubmit']) { + if (Array.isArray(settings.hooks[event])) { + const before = settings.hooks[event].length; + settings.hooks[event] = settings.hooks[event].filter(e => !isCavemanEntry(e)); + removed += before - settings.hooks[event].length; + // Drop the event key if it's now empty (keeps settings.json tidy) + if (settings.hooks[event].length === 0) { + delete settings.hooks[event]; + } + } + } + // Drop settings.hooks if it's now empty + if (Object.keys(settings.hooks).length === 0) { + delete settings.hooks; + } + } + + // Remove statusLine if it references caveman + if (settings.statusLine) { + const cmd = typeof settings.statusLine === 'string' + ? settings.statusLine + : (settings.statusLine.command || ''); + if (cmd.includes('caveman')) { + delete settings.statusLine; + console.log(' Removed caveman statusLine from settings.json'); + } + } + + fs.writeFileSync('$SETTINGS', JSON.stringify(settings, null, 2) + '\n'); + console.log(' Removed ' + removed + ' caveman hook entries from settings.json'); + " + fi +fi + +# 3. Remove flag file +if [ -f "$FLAG_FILE" ]; then + rm "$FLAG_FILE" + echo " Removed: $FLAG_FILE" +fi + +echo "" +echo "Done! Restart Claude Code to complete the uninstall." +echo "" +echo "Note: If you installed caveman as a plugin, disabling the plugin is" +echo " the recommended way to deactivate hooks — this script is only" +echo " needed if you installed manually via install.sh." diff --git a/skills/caveman/SKILL.md b/skills/caveman/SKILL.md index 29b154f..1a9d451 100644 --- a/skills/caveman/SKILL.md +++ b/skills/caveman/SKILL.md @@ -49,7 +49,7 @@ Example — "Explain database connection pooling." ## Auto-Clarity -Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user confused. Resume caveman after clear part done. +Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user asks to clarify or repeats question. Resume caveman after clear part done. Example — destructive op: > **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.