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>
85 lines
3 KiB
Bash
Executable file
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" "$@"
|