#!/bin/bash # Tempo one-line installer — curl -fsSL https://trytempo.net/install.sh | bash # Local-only product: this script downloads the bundle from trytempo.net, # installs to ~/.tempo, and wires Claude Code hooks. Backs up your settings. set -e echo "" echo "tempo — installing…" TMPD=$(mktemp -d) trap 'rm -rf "$TMPD"' EXIT curl -fsSL https://trytempo.net/tempo-beta.zip -o "$TMPD/tempo.zip" unzip -q "$TMPD/tempo.zip" -d "$TMPD" SRC="$TMPD/tempo" mkdir -p "$HOME/.tempo" cp "$SRC/phase0/observe.py" "$SRC/phase0/analyze.py" "$HOME/.tempo/" cp "$SRC/phase1/score.py" "$SRC/phase1/reset.py" "$SRC/phase1/statusline.py" \ "$SRC/phase1/library.json" "$SRC/phase1/demo.py" "$SRC/phase1/week.py" \ "$SRC/phase1/doctor.py" "$HOME/.tempo/" chmod +x "$HOME/.tempo/observe.py" "$HOME/.tempo/doctor.py" python3 - <<'PY' import json, os, time, shutil sp = os.path.expanduser("~/.claude/settings.json") os.makedirs(os.path.dirname(sp), exist_ok=True) settings = {} if os.path.exists(sp): shutil.copy2(sp, sp + ".tempo-backup-%d" % int(time.time())) try: with open(sp) as f: settings = json.load(f) except Exception: print("WARNING: existing settings.json is not valid JSON; leaving it untouched.") raise SystemExit(1) OBS = 'python3 "$HOME/.tempo/observe.py"' SCO = 'python3 "$HOME/.tempo/score.py"' # The offer. Claude Code discards plain stdout from a Stop hook, so reset.py # --hook speaks through a JSON systemMessage instead - and only at a seam, # only when depleted, at most twice a day. OFFER = 'python3 "$HOME/.tempo/reset.py" --hook' EVENTS = ["SessionStart", "UserPromptSubmit", "PostToolUse", "SubagentStop", "Stop", "SessionEnd"] hooks = settings.setdefault("hooks", {}) for ev in EVENTS: entries = hooks.setdefault(ev, []) existing = json.dumps(entries) block = {"hooks": []} if ev == "PostToolUse": block["matcher"] = "*" cmds = (OBS, SCO, OFFER) if ev == "Stop" else (OBS, SCO) for cmd in cmds: if cmd not in existing: block["hooks"].append({"type": "command", "command": cmd, "timeout": 5}) if block["hooks"]: entries.append(block) TEMPO_SL = {"type": "command", "command": 'python3 "$HOME/.tempo/statusline.py"'} wrap_path = os.path.expanduser("~/.tempo/wrapped.json") existing_sl = settings.get("statusLine") or {} existing_cmd = existing_sl.get("command", "") if not existing_cmd: settings["statusLine"] = TEMPO_SL sl = "installed" elif "tempo" in existing_cmd: sl = "already tempo's" elif existing_sl.get("type") == "command": # Do not silently skip. A statusline that never appears is the single # most common reason people conclude Tempo does nothing, so wrap the # one you have: yours runs first and unmodified, the glyph goes after. with open(wrap_path, "w") as f: json.dump({"command": existing_cmd, "wrapped_at": int(time.time())}, f, indent=2) settings["statusLine"] = TEMPO_SL sl = "wrapping yours (restore: delete ~/.tempo/wrapped.json)" else: sl = "SKIPPED (yours is type=%r, which tempo cannot wrap)" % existing_sl.get("type") with open(sp, "w") as f: json.dump(settings, f, indent=2) print("hooks merged into ~/.claude/settings.json (backup saved)") print("statusline: " + sl) PY printf '{\n "cold_start_days": 0\n}\n' > "$HOME/.tempo/config.json" touch "$HOME/.tempo/enabled" python3 "$HOME/.tempo/score.py" --force >/dev/null 2>&1 || true echo "" echo "✅ tempo installed and ON." echo " open a Claude Code session — look for the tempo ● glyph in the statusline." echo " is it working? python3 ~/.tempo/doctor.py" echo " your data story: python3 ~/.tempo/week.py" echo " pause: rm ~/.tempo/enabled · erase everything: rm -rf ~/.tempo" echo ""