#!/bin/sh # # Cron wrapper for push-claude-usage.js: take the lock, load the secrets, check # the install, hand over to node. Every fifteen minutes, unattended. # # NEVER add `set -x`. This script is meant to be piped into `logger`, and the # shell would echo the `. "$ENV_FILE"` expansion — the ingest token — straight # into /var/log/syslog and journald, where it would stay. Same reason nothing # below ever echoes a variable that holds a secret: the checks print the NAME of # what is missing, never its value. # # Suggested crontab line (see agent/README.md): # */15 * * * * $HOME/.local/share/maximus-claude-usage/run-claude-usage.sh 2>&1 | logger -t claude-usage set -eu SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) AGENT="$SCRIPT_DIR/push-claude-usage.js" # Overridable for testing only; cron uses the default. ENV_FILE="${CLAUDE_USAGE_ENV_FILE:-$HOME/.config/maximus-claude-usage.env}" STATE_DIR="$HOME/.local/state/maximus-claude-usage" LOCK_FILE="$STATE_DIR/run.lock" if ! command -v flock >/dev/null 2>&1; then echo "claude-usage: flock not found (util-linux)" >&2 exit 1 fi mkdir -p "$STATE_DIR" chmod 700 "$STATE_DIR" # One run at a time. The first full scan of ~210 MB of transcripts can outlast # the 15-minute beat; a second run would only redo the same work and race the # state file. fd 9 stays open through `exec`, so node holds the lock until it # exits. A beat that finds the lock taken is not an error: exit 0. exec 9>"$LOCK_FILE" if ! flock -n 9; then echo "claude-usage: a run is already in progress, skipping" exit 0 fi if [ ! -f "$ENV_FILE" ]; then echo "claude-usage: env file not found: $ENV_FILE" >&2 exit 1 fi # A warning, not a refusal: a wrong mode bit is worth shouting about, but it is # not worth silencing the whole agent over. FILE_MODE=$(stat -c "%a" "$ENV_FILE" 2>/dev/null || echo "") if [ -n "$FILE_MODE" ] && [ "$FILE_MODE" != "600" ]; then echo "claude-usage: warning: $ENV_FILE is mode $FILE_MODE, expected 600" >&2 fi # shellcheck source=/dev/null . "$ENV_FILE" if [ -z "${CLAUDE_USAGE_API_URL:-}" ]; then echo "claude-usage: CLAUDE_USAGE_API_URL is not set in $ENV_FILE" >&2 exit 1 fi if [ -z "${CLAUDE_USAGE_INGEST_TOKEN:-}" ]; then echo "claude-usage: CLAUDE_USAGE_INGEST_TOKEN is not set in $ENV_FILE" >&2 exit 1 fi if [ ! -f "$AGENT" ]; then echo "claude-usage: agent not found: $AGENT" >&2 exit 1 fi # cron's PATH is famously short (/usr/bin:/bin), and a node installed through # nvm or /usr/local lives outside it. Set NODE_BIN in the env file when # `command -v node` comes up empty under cron but works in a login shell. NODE_BIN="${NODE_BIN:-node}" if ! command -v "$NODE_BIN" >/dev/null 2>&1; then echo "claude-usage: node not found (NODE_BIN=$NODE_BIN); set NODE_BIN in $ENV_FILE" >&2 exit 1 fi # CLAUDE_USAGE_LIMITS is optional (`off` skips the Anthropic call). Exported # too, otherwise setting it in the env file silently does nothing. export CLAUDE_USAGE_API_URL CLAUDE_USAGE_INGEST_TOKEN CLAUDE_USAGE_LIMITS exec "$NODE_BIN" "$AGENT" "$@"