#!/usr/bin/env bash set -euo pipefail # # ops/install-cron.sh — idempotently install a crontab entry that runs the toknmtr agent in # reconcile mode (`--once`) every ~10 minutes. # # Why this exists alongside the Stop hook (ops/install-hook.sh): # The hook is near-live but best-effort — it's deliberately fail-open (see # agent/hooks/toknmtr-capture.sh), so it silently no-ops if the project dir/config/node # binary is momentarily missing, and it never retries a failed push itself (a failed push # just leaves the file's cursor unmoved, see agent/run.ts). It also only fires on Claude # Code turns, so a transcript that's touched outside a tracked Stop event (or a session # whose final hook fire was the one that raced/failed) won't get its tail captured until # *something* sweeps again. This cron line is that backstop: a periodic, host-crontab- # driven sweep that's independent of any particular Claude Code session being open, so the # data self-heals even if every hook fire that session ever missed/failed. # Push is idempotent (PK = host+session_id+uuid, see CLAUDE.md), so overlapping # hook-triggered and cron-triggered sweeps are always safe to interleave. # # What this script does: # - Adds (or updates, if already present) a single crontab line running # `node --experimental-strip-types agent/run.ts --once` every 10 minutes, sourcing # ~/.toknmtr/env first for TOKNMTR_URL/TOKNMTR_TOKEN, with stdout/stderr appended to a # log file. # - Idempotent: matches by a unique marker comment, so re-running replaces (rather than # duplicating) the line — safe to re-run after changing TOKNMTR_PROJECT_DIR etc. # - Reversible: `ops/install-cron.sh --remove` deletes just the marked line; everything # else in the user's crontab is left untouched. # # Usage: # ops/install-cron.sh # install/update the cron line # ops/install-cron.sh --remove # remove just this cron line # # 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 cron sweep log path (default: ~/.toknmtr/cron.log) # TOKNMTR_CRON_SCHEDULE cron schedule expression (default: "*/10 * * * *") PROJECT_DIR="${TOKNMTR_PROJECT_DIR:-$HOME/claude/projects/toknmtr}" CONFIG_FILE="${TOKNMTR_ENV_FILE:-$HOME/.toknmtr/env}" LOG_FILE="${TOKNMTR_LOG_FILE:-$HOME/.toknmtr/cron.log}" SCHEDULE="${TOKNMTR_CRON_SCHEDULE:-*/10 * * * *}" MARKER="# toknmtr-cron-reconcile" CRON_CMD="cd \"$PROJECT_DIR\" && . \"$CONFIG_FILE\" && node --experimental-strip-types agent/run.ts --once >> \"$LOG_FILE\" 2>&1" CRON_LINE="$SCHEDULE bash -lc '$CRON_CMD' $MARKER" current_crontab() { crontab -l 2>/dev/null || true } if [ "${1:-}" = "--remove" ]; then existing="$(current_crontab)" if ! printf '%s\n' "$existing" | grep -qF "$MARKER"; then echo "no toknmtr cron line found, nothing to remove" exit 0 fi printf '%s\n' "$existing" | grep -vF "$MARKER" | crontab - echo "removed toknmtr reconcile cron line" exit 0 fi command -v crontab >/dev/null 2>&1 || { echo "error: crontab is required (e.g. 'sudo apt install cron') and the cron daemon must be running" >&2 exit 1 } [ -d "$PROJECT_DIR" ] || { echo "error: $PROJECT_DIR does not exist (set TOKNMTR_PROJECT_DIR?)" >&2 exit 1 } [ -f "$PROJECT_DIR/agent/run.ts" ] || { echo "error: $PROJECT_DIR/agent/run.ts not found — is TOKNMTR_PROJECT_DIR correct?" >&2 exit 1 } mkdir -p "$(dirname "$LOG_FILE")" # Replace any prior toknmtr line (matched by marker) with the new one; append if absent. existing="$(current_crontab)" filtered="$(printf '%s\n' "$existing" | grep -vF "$MARKER" || true)" { [ -n "$filtered" ] && printf '%s\n' "$filtered" printf '%s\n' "$CRON_LINE" } | crontab - echo "installed toknmtr reconcile cron line (schedule: $SCHEDULE):" echo " $CRON_LINE" echo "" echo "config read from: $CONFIG_FILE (must exist with TOKNMTR_URL / TOKNMTR_TOKEN, see ops/README.md)" echo "sweep log: $LOG_FILE" echo "" echo "to remove later:" echo " ops/install-cron.sh --remove" echo " (or run 'crontab -e' and delete the line ending in '$MARKER')"