vps-health-api/agent/claude-usage/run-claude-usage.sh
le king fu e274ca7ed5 feat(agent): add the claude-usage agent: plan limits, local cache, push
limits.js reads the Claude Code OAuth credentials read-only and calls the
pinned Anthropic usage endpoint once (redirect refused, 10 s timeout, no
retry, never a refresh); detail codes come from a fixed enum only.
state.js freezes every Toronto day before yesterday, empty ones included,
prunes past 35 days, keeps the last good limits, and writes atomically in
mode 600. push-claude-usage.js writes the cache before pushing, refuses a
non-https URL (exit 1) and a body over 64 KiB (exit 2, nothing sent), and
prints no token. run-claude-usage.sh takes a non-blocking flock so a beat
that overlaps the previous one exits 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:26:15 -04:00

85 lines
3 KiB
Bash
Executable file

#!/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" "$@"