Files
caveman/hooks/install.sh
T
31fa95478d feat: /caveman-stats — real session token usage + savings estimate
New slash command that reads the active Claude Code session JSONL
(.claude/projects/**/*.jsonl), sums output_tokens and
cache_read_input_tokens from assistant turns, and shows an estimated
savings figure when the active mode is 'full'.

Real numbers, not the model's guess:

  Caveman Stats
  ──────────────────────────────────
  Session:  ...projects/my-app/abc123.jsonl
  Turns:    47
  ──────────────────────────────────
  Output tokens:         3,210
  Cache-read tokens:     128,400
  ──────────────────────────────────
  Est. without caveman:  9,171
  Est. tokens saved:     5,961 (~65%)
  Savings est. from benchmarks/ (mean per-task). Actual varies by task.

Implementation:
* hooks/caveman-stats.js — script. Run directly with
  `node hooks/caveman-stats.js`, or via `--session-file <path>`.
  Falls back to most-recent JSONL under .claude/projects/ when no
  session file is passed.
* hooks/caveman-mode-tracker.js — `/caveman-stats` triggers an
  execFileSync call to caveman-stats.js with the hook's transcript_path,
  and the output is returned via `decision: "block"` so the user sees
  the stats inline without a model round-trip.
* install.sh / install.ps1 / uninstall.{sh,ps1} include
  caveman-stats.js in HOOK_FILES.
* skills/caveman-stats/SKILL.md (+ plugin mirror) for skill listing.
* README install matrix and Caveman Skills section updated.

Compression ratio (0.65) is the mean per-task figure from
benchmarks/results/*.json (avg_savings: 65 across 10 tasks). Only 'full'
mode has measured data — lite/ultra/wenyan show no estimate.

Tests: 6 passing in tests/test_caveman_stats.js covering direct
invocation, full-mode estimate math, non-full skip, no-session error,
mode-tracker block behavior, and flag preservation.

Closes #305 (re-implementation; takes the design from
@DeeptimaanB but rewritten against current main).

Co-Authored-By: Deeptimaan Banerjee <DeeptimaanB@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 01:14:53 +02:00

200 lines
6.7 KiB
Bash
Executable File

#!/bin/bash
# caveman — one-command hook installer for Claude Code
# Installs: SessionStart hook (auto-load rules) + UserPromptSubmit hook (mode tracking)
# Usage: bash hooks/install.sh
# or: bash <(curl -s https://raw.githubusercontent.com/JuliusBrussee/caveman/main/hooks/install.sh)
# or: bash hooks/install.sh --force (re-install over existing hooks)
set -e
FORCE=0
for arg in "$@"; do
case "$arg" in
--force|-f) FORCE=1 ;;
esac
done
# Detect Windows (Git Bash / MSYS / MINGW) — not WSL (WSL reports "linux-gnu")
case "$OSTYPE" in
msys*|cygwin*|mingw*)
echo "WARNING: Running on Windows ($OSTYPE)."
echo " This script works in Git Bash/MSYS but symlinks may require"
echo " Developer Mode or admin privileges."
echo " If you installed via 'claude plugin install', you don't need this script."
echo ""
;;
esac
# 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="${CLAUDE_CONFIG_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=("package.json" "caveman-config.js" "caveman-activate.js" "caveman-mode-tracker.js" "caveman-stats.js" "caveman-statusline.sh")
# Resolve source — works from repo clone or curl pipe
SCRIPT_DIR=""
if [ -n "${BASH_SOURCE[0]:-}" ] && [ -f "${BASH_SOURCE[0]}" ]; then
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
fi
# Check if already installed (unless --force). Older installs only had two hook
# files, so require the full current set plus the hook registrations before we
# short-circuit.
ALREADY_INSTALLED=0
if [ "$FORCE" -eq 0 ]; then
ALL_FILES_PRESENT=1
for hook in "${HOOK_FILES[@]}"; do
if [ ! -f "$HOOKS_DIR/$hook" ]; then
ALL_FILES_PRESENT=0
break
fi
done
HOOKS_WIRED=0
HAS_STATUSLINE=0
if [ "$ALL_FILES_PRESENT" -eq 1 ] && [ -f "$SETTINGS" ]; then
if CAVEMAN_SETTINGS="$SETTINGS" node -e "
const fs = require('fs');
const settings = JSON.parse(fs.readFileSync(process.env.CAVEMAN_SETTINGS, 'utf8'));
const hasCavemanHook = (event) =>
Array.isArray(settings.hooks?.[event]) &&
settings.hooks[event].some(e =>
e.hooks && e.hooks.some(h => h.command && h.command.includes('caveman'))
);
process.exit(
hasCavemanHook('SessionStart') &&
hasCavemanHook('UserPromptSubmit') &&
!!settings.statusLine
? 0
: 1
);
" >/dev/null 2>&1; then
HOOKS_WIRED=1
HAS_STATUSLINE=1
fi
fi
if [ "$ALL_FILES_PRESENT" -eq 1 ] && [ "$HOOKS_WIRED" -eq 1 ] && [ "$HAS_STATUSLINE" -eq 1 ]; then
ALREADY_INSTALLED=1
echo "Caveman hooks already installed in $HOOKS_DIR"
echo " Re-run with --force to overwrite: bash hooks/install.sh --force"
echo ""
fi
fi
if [ "$ALREADY_INSTALLED" -eq 1 ] && [ "$FORCE" -eq 0 ]; then
echo "Nothing to do. Hooks are already in place."
exit 0
fi
if [ "$FORCE" -eq 1 ] && [ -f "$HOOKS_DIR/caveman-activate.js" ]; then
echo "Reinstalling caveman hooks (--force)..."
else
echo "Installing caveman hooks..."
fi
# 1. Ensure hooks dir exists
mkdir -p "$HOOKS_DIR"
# 2. Copy or download hook files
for hook in "${HOOK_FILES[@]}"; do
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/$hook" ]; then
cp "$SCRIPT_DIR/$hook" "$HOOKS_DIR/$hook"
else
curl -fsSL "$REPO_URL/$hook" -o "$HOOKS_DIR/$hook"
fi
echo " Installed: $HOOKS_DIR/$hook"
done
# 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"
# Pass paths via env vars — avoids shell injection if $HOME contains single quotes
CAVEMAN_SETTINGS="$SETTINGS" CAVEMAN_HOOKS_DIR="$HOOKS_DIR" node -e "
const fs = require('fs');
const settingsPath = process.env.CAVEMAN_SETTINGS;
const hooksDir = process.env.CAVEMAN_HOOKS_DIR;
const managedStatusLinePath = hooksDir + '/caveman-statusline.sh';
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
if (!settings.hooks) settings.hooks = {};
// SessionStart — auto-load caveman rules
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
const hasStart = settings.hooks.SessionStart.some(e =>
e.hooks && e.hooks.some(h => h.command && h.command.includes('caveman'))
);
if (!hasStart) {
settings.hooks.SessionStart.push({
hooks: [{
type: 'command',
command: 'node \"' + hooksDir + '/caveman-activate.js\"',
timeout: 5,
statusMessage: 'Loading caveman mode...'
}]
});
}
// UserPromptSubmit — track mode changes when user types /caveman commands
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
const hasPrompt = settings.hooks.UserPromptSubmit.some(e =>
e.hooks && e.hooks.some(h => h.command && h.command.includes('caveman'))
);
if (!hasPrompt) {
settings.hooks.UserPromptSubmit.push({
hooks: [{
type: 'command',
command: 'node \"' + hooksDir + '/caveman-mode-tracker.js\"',
timeout: 5,
statusMessage: 'Tracking caveman mode...'
}]
});
}
// Statusline — wire caveman badge (report if skipped)
if (!settings.statusLine) {
settings.statusLine = {
type: 'command',
command: 'bash \"' + managedStatusLinePath + '\"'
};
console.log(' Statusline badge configured.');
} else {
const cmd = typeof settings.statusLine === 'string'
? settings.statusLine
: (settings.statusLine.command || '');
if (cmd.includes(managedStatusLinePath)) {
console.log(' Statusline badge already configured.');
} else {
console.log(' NOTE: Existing statusline detected — caveman badge NOT added.');
console.log(' See hooks/README.md to add the badge to your existing statusline.');
}
}
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
console.log(' Hooks wired in settings.json');
"
echo ""
echo "Done! Restart Claude Code to activate."
echo ""
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 " - Statusline badge: shows [CAVEMAN] or [CAVEMAN:ULTRA] etc."