mirror of
https://github.com/JuliusBrussee/caveman.git
synced 2026-08-11 13:21:09 +02:00
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>
200 lines
6.7 KiB
Bash
Executable File
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."
|