toknmtr — self-hostable Claude Code usage & analytics dashboard
Server (SvelteKit + SQLite Docker container) + per-machine agent that parses Claude Code JSONL transcripts. docker-compose one-command deploy; raw transcript view + search gated behind SHOW_TRANSCRIPTS (hidden by default). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
71a60ab054
74 changed files with 15613 additions and 0 deletions
113
ops/install-hook.sh
Executable file
113
ops/install-hook.sh
Executable file
|
|
@ -0,0 +1,113 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
# ops/install-hook.sh — idempotently register the toknmtr capture hook
|
||||
# (agent/hooks/toknmtr-capture.sh) in ~/.claude/settings.json under the Claude Code 'Stop'
|
||||
# hook event.
|
||||
#
|
||||
# WHY 'Stop' AND NOT 'SessionEnd':
|
||||
# Stop fires at the end of EVERY assistant turn (each time Claude finishes responding and
|
||||
# yields back to the user) — not just once when the whole CLI process exits. That makes
|
||||
# capture close to live: a dashboard refresh a few seconds after a turn already reflects
|
||||
# it. The cost is the hook running more often, but agent/hooks/toknmtr-capture.sh is
|
||||
# designed to be near-zero-cost on the foreground session (it backgrounds the real work
|
||||
# and returns in milliseconds — see that script's header), so the extra frequency is
|
||||
# nearly free.
|
||||
# SessionEnd only fires once, when the CLI process actually exits — on a long interactive
|
||||
# session that can be hours after the data was generated, and Claude Code does not
|
||||
# guarantee SessionEnd fires on every exit path (e.g. a killed/crashed terminal can skip
|
||||
# it entirely), so relying on it alone risks losing the tail of a session permanently
|
||||
# until the next cron sweep (ops/install-cron.sh) catches it.
|
||||
# Net: Stop is the better default given the hook is cheap. The cron sweep is an
|
||||
# independent safety net either way (server-down resilience, missed hook fires, etc.) —
|
||||
# it doesn't change this tradeoff, it just makes the choice lower-stakes. Revisit by
|
||||
# swapping HOOK_EVENT below to "SessionEnd" if Stop's per-turn frequency ever proves to be
|
||||
# a real problem in practice (e.g. very chatty multi-turn sessions on a slow machine).
|
||||
#
|
||||
# What this script does:
|
||||
# - Merges (via jq) a Stop-hook entry pointing at agent/hooks/toknmtr-capture.sh into
|
||||
# ~/.claude/settings.json, WITHOUT touching any other keys/hooks already in that file.
|
||||
# - Idempotent: matches on the exact command string, so re-running is a safe no-op.
|
||||
# - Writes a timestamped backup of settings.json before every modification.
|
||||
# - Reversible: `ops/install-hook.sh --remove` undoes just this hook's entry (or restore
|
||||
# a backup manually — see the "to remove later" line this script prints on install).
|
||||
#
|
||||
# Usage:
|
||||
# ops/install-hook.sh # install/update the hook
|
||||
# ops/install-hook.sh --remove # remove this hook's entry from settings.json
|
||||
#
|
||||
# Override knobs (env, all optional):
|
||||
# TOKNMTR_PROJECT_DIR path to the toknmtr repo checkout (default: ~/claude/projects/toknmtr)
|
||||
# CLAUDE_SETTINGS_FILE path to settings.json (default: ~/.claude/settings.json)
|
||||
|
||||
PROJECT_DIR="${TOKNMTR_PROJECT_DIR:-$HOME/claude/projects/toknmtr}"
|
||||
SETTINGS_FILE="${CLAUDE_SETTINGS_FILE:-$HOME/.claude/settings.json}"
|
||||
HOOK_SCRIPT="$PROJECT_DIR/agent/hooks/toknmtr-capture.sh"
|
||||
HOOK_EVENT="Stop"
|
||||
HOOK_CMD="bash \"$HOOK_SCRIPT\""
|
||||
HOOK_TIMEOUT_S=10
|
||||
|
||||
command -v jq >/dev/null 2>&1 || {
|
||||
echo "error: jq is required (sudo apt install jq / brew install jq)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
backup_settings() {
|
||||
[ -f "$SETTINGS_FILE" ] || return 0
|
||||
local stamp
|
||||
stamp="$(date +%Y%m%dT%H%M%S)"
|
||||
cp "$SETTINGS_FILE" "$SETTINGS_FILE.bak.$stamp"
|
||||
echo "backed up $SETTINGS_FILE -> $SETTINGS_FILE.bak.$stamp"
|
||||
}
|
||||
|
||||
if [ "${1:-}" = "--remove" ]; then
|
||||
if [ ! -f "$SETTINGS_FILE" ]; then
|
||||
echo "no settings file at $SETTINGS_FILE, nothing to remove"
|
||||
exit 0
|
||||
fi
|
||||
backup_settings
|
||||
tmp="$(mktemp)"
|
||||
jq --arg cmd "$HOOK_CMD" --arg event "$HOOK_EVENT" '
|
||||
if (.hooks // {} | has($event)) then
|
||||
.hooks[$event] = [
|
||||
.hooks[$event][] |
|
||||
.hooks = [(.hooks // [])[] | select(.command != $cmd)] |
|
||||
select((.hooks | length) > 0)
|
||||
]
|
||||
else . end
|
||||
' "$SETTINGS_FILE" >"$tmp" && mv "$tmp" "$SETTINGS_FILE"
|
||||
echo "removed toknmtr capture hook from $SETTINGS_FILE.$HOOK_EVENT (backup written above; other $HOOK_EVENT hooks, if any, were left untouched)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$HOOK_SCRIPT" ]; then
|
||||
echo "error: $HOOK_SCRIPT not found (is TOKNMTR_PROJECT_DIR=$PROJECT_DIR correct?)" >&2
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$HOOK_SCRIPT" 2>/dev/null || true
|
||||
|
||||
mkdir -p "$(dirname "$SETTINGS_FILE")"
|
||||
[ -f "$SETTINGS_FILE" ] || echo '{}' >"$SETTINGS_FILE"
|
||||
jq empty "$SETTINGS_FILE" || {
|
||||
echo "error: $SETTINGS_FILE is not valid JSON — fix it manually before running this script" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
backup_settings
|
||||
|
||||
tmp="$(mktemp)"
|
||||
jq --arg cmd "$HOOK_CMD" --arg event "$HOOK_EVENT" --argjson timeout "$HOOK_TIMEOUT_S" '
|
||||
.hooks //= {} |
|
||||
.hooks[$event] //= [] |
|
||||
([.hooks[$event][]? | (.hooks // [])[]? | .command] | index($cmd)) as $already |
|
||||
if $already == null then
|
||||
.hooks[$event] += [{"hooks": [{"type": "command", "command": $cmd, "timeout": $timeout}]}]
|
||||
else . end
|
||||
' "$SETTINGS_FILE" >"$tmp" && mv "$tmp" "$SETTINGS_FILE"
|
||||
|
||||
echo "installed toknmtr capture hook into $SETTINGS_FILE under hooks.$HOOK_EVENT"
|
||||
echo " command: $HOOK_CMD"
|
||||
echo ""
|
||||
echo "to remove later:"
|
||||
echo " ops/install-hook.sh --remove"
|
||||
echo " (or restore a $SETTINGS_FILE.bak.<timestamp> file written next to it)"
|
||||
Loading…
Add table
Add a link
Reference in a new issue