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:
megaproxy 2026-07-08 19:33:55 +01:00
commit 71a60ab054
74 changed files with 15613 additions and 0 deletions

74
agent/hooks/toknmtr-capture.sh Executable file
View file

@ -0,0 +1,74 @@
#!/usr/bin/env bash
#
# toknmtr-capture.sh — Claude Code hook that triggers an incremental toknmtr agent sweep.
#
# Registered (by ops/install-hook.sh) under the Claude Code 'Stop' hook event, so it fires
# at the end of every assistant turn — see ops/install-hook.sh for the Stop-vs-SessionEnd
# tradeoff writeup.
#
# FAIL-OPEN CONTRACT — this script must NEVER block, slow down, or fail the Claude Code
# session it's attached to:
# - The actual agent run (`node agent/run.ts --once`) is launched with `nohup ... &` and
# disowned, with stdio redirected to a log file (never inherited from the hook's own
# stdio), so this script returns to Claude Code in a handful of milliseconds — it does
# NOT wait for the parse+push to finish.
# - `timeout` wraps the backgrounded agent run so a hung/unreachable server can never
# leave an orphaned process running forever.
# - Every prerequisite (project dir, config file, node, timeout) is individually checked;
# any miss just skips capture for this turn silently.
# - This script prints NOTHING to its own stdout/stderr (Claude Code parses Stop-hook
# stdout as potential JSON — e.g. {"decision":"block"} would force the session to keep
# going — so silence here is required, not just polite). All diagnostic output goes to
# $LOG_FILE instead.
# - Always exits 0, intentionally not using `set -e`: every step below is already
# individually guarded, so a failure anywhere means "skip capture this turn", never
# "fail the hook" / block the Stop event.
#
# Config: ~/.toknmtr/env (TOKNMTR_URL, TOKNMTR_TOKEN — see ops/README.md). Kept out of
# ~/.claude/settings.json so secrets aren't sitting in a file that's more likely to be
# shared, synced, or dumped for support/debugging.
#
# Override knobs (env, all optional):
# TOKNMTR_PROJECT_DIR path to the toknmtr repo checkout (default: ~/claude/projects/toknmtr)
# TOKNMTR_ENV_FILE path to the config file (default: ~/.toknmtr/env)
# TOKNMTR_LOG_FILE where backgrounded output is logged (default: ~/.toknmtr/capture.log)
# TOKNMTR_HOOK_TIMEOUT_S max seconds the backgrounded sweep may run (default: 25)
PROJECT_DIR="${TOKNMTR_PROJECT_DIR:-$HOME/claude/projects/toknmtr}"
CONFIG_FILE="${TOKNMTR_ENV_FILE:-$HOME/.toknmtr/env}"
LOG_FILE="${TOKNMTR_LOG_FILE:-$HOME/.toknmtr/capture.log}"
TIMEOUT_S="${TOKNMTR_HOOK_TIMEOUT_S:-25}"
# Claude Code hooks receive a JSON payload on stdin describing the event. We don't need its
# contents (the agent re-walks transcripts itself from disk), but drain it anyway so we
# never leave the pipe half-read.
cat >/dev/null 2>&1 || true
# Bail out quietly (still exit 0) if any prerequisite is missing — never surface a hook
# failure to the session over a merely-unconfigured machine.
[ -d "$PROJECT_DIR" ] || exit 0
[ -f "$PROJECT_DIR/agent/run.ts" ] || exit 0
[ -f "$CONFIG_FILE" ] || exit 0
command -v node >/dev/null 2>&1 || exit 0
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || exit 0
# Build the backgrounded command as a single string for `bash -c` so it can `cd`, source
# the config file (exporting TOKNMTR_URL/TOKNMTR_TOKEN into its own environment), and then
# exec node — all inside the detached child, never the foreground hook process.
read -r -d '' INNER_CMD <<EOF || true
cd "$PROJECT_DIR" || exit 0
set -a
. "$CONFIG_FILE"
set +a
exec node --experimental-strip-types agent/run.ts --once
EOF
if command -v timeout >/dev/null 2>&1; then
nohup timeout "${TIMEOUT_S}s" bash -c "$INNER_CMD" >>"$LOG_FILE" 2>&1 &
else
nohup bash -c "$INNER_CMD" >>"$LOG_FILE" 2>&1 &
fi
disown 2>/dev/null || true
exit 0