#!/usr/bin/env bash # ============================================================================ # Claude Code Overnight Automation — Setup Script # ============================================================================ # Creates the loop directory, hook scripts, and configures settings.json. # Run this from your project root: # bash path/to/setup.sh # ============================================================================ set -euo pipefail # ── Colors ─────────────────────────────────────────────────────────────────── BOLD='\033[1m' DIM='\033[2m' GREEN='\033[0;32m' YELLOW='\033[0;33m' CYAN='\033[0;36m' RED='\033[0;31m' NC='\033[0m' ok() { echo -e " ${GREEN}+${NC} $1"; } warn() { echo -e " ${YELLOW}!${NC} $1"; } err() { echo -e " ${RED}x${NC} $1"; } info() { echo -e " ${DIM}$1${NC}"; } # ── Header ─────────────────────────────────────────────────────────────────── echo "" echo -e "${BOLD} Claude Code Overnight Automation${NC}" echo -e " ${DIM}────────────────────────────────────${NC}" echo "" # ── Find template directory ────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" TEMPLATE_DIR="$SCRIPT_DIR/templates" if [[ ! -d "$TEMPLATE_DIR" ]]; then err "Templates directory not found at: $TEMPLATE_DIR" err "Make sure setup.sh is in the same folder as the templates/ directory." exit 1 fi # ── Check prerequisites ───────────────────────────────────────────────────── echo -e " ${BOLD}Checking prerequisites...${NC}" echo "" MISSING=0 if command -v claude &>/dev/null; then ok "Claude CLI found: $(which claude)" else err "Claude CLI not found. Install from: https://docs.anthropic.com/en/docs/claude-code" MISSING=1 fi if command -v git &>/dev/null; then ok "Git found: $(which git)" else err "Git not found. Install git first." MISSING=1 fi if [[ "$(uname)" == "Darwin" ]]; then ok "macOS detected (caffeinate + launchd available)" else warn "Not macOS — caffeinate/launchd hooks will need manual Linux equivalents" fi if git rev-parse --is-inside-work-tree &>/dev/null; then PROJECT_DIR="$(git rev-parse --show-toplevel)" ok "Git repo found: $PROJECT_DIR" else PROJECT_DIR="$(pwd)" warn "Not in a git repo. Using current directory: $PROJECT_DIR" fi if [[ "$MISSING" -eq 1 ]]; then echo "" err "Missing prerequisites. Fix the above and re-run." exit 1 fi echo "" # ── Create loop directory ──────────────────────────────────────────────────── echo -e " ${BOLD}Setting up loop directory...${NC}" echo "" LOOP_DIR="$PROJECT_DIR/loop" mkdir -p "$LOOP_DIR" # Copy loop.sh if [[ -f "$LOOP_DIR/loop.sh" ]]; then warn "loop/loop.sh already exists — skipping (won't overwrite)" else cp "$TEMPLATE_DIR/loop.sh" "$LOOP_DIR/loop.sh" chmod +x "$LOOP_DIR/loop.sh" ok "Created loop/loop.sh" fi # Copy prompt.md if [[ -f "$LOOP_DIR/prompt.md" ]]; then warn "loop/prompt.md already exists — skipping" else cp "$TEMPLATE_DIR/prompt.md" "$LOOP_DIR/prompt.md" ok "Created loop/prompt.md (edit this with your project rules)" fi # Copy plan.md if [[ -f "$LOOP_DIR/plan.md" ]]; then warn "loop/plan.md already exists — skipping" else cp "$TEMPLATE_DIR/plan.md" "$LOOP_DIR/plan.md" ok "Created loop/plan.md (edit this with your tasks)" fi echo "" # ── Install hooks ──────────────────────────────────────────────────────────── echo -e " ${BOLD}Installing hooks...${NC}" echo "" HOOKS_DIR="$HOME/.claude/hooks" mkdir -p "$HOOKS_DIR" for hook in prevent-sleep.sh stop-hook-autonomous.sh allow-sleep.sh; do if [[ -f "$HOOKS_DIR/$hook" ]]; then warn "$hook already exists — skipping (won't overwrite)" else cp "$TEMPLATE_DIR/hooks/$hook" "$HOOKS_DIR/$hook" chmod +x "$HOOKS_DIR/$hook" ok "Installed ~/.claude/hooks/$hook" fi done echo "" # ── Update settings.json ──────────────────────────────────────────────────── echo -e " ${BOLD}Configuring Claude settings...${NC}" echo "" SETTINGS_FILE="$HOME/.claude/settings.json" # Backup existing settings if [[ -f "$SETTINGS_FILE" ]]; then cp "$SETTINGS_FILE" "${SETTINGS_FILE}.backup.$(date +%s)" info "Backed up existing settings.json" fi # Check if hooks are already registered if [[ -f "$SETTINGS_FILE" ]] && grep -q "stop-hook-autonomous" "$SETTINGS_FILE" 2>/dev/null; then ok "Hooks already registered in settings.json" else # Create or update settings.json with hook registrations if command -v python3 &>/dev/null; then python3 << 'PYEOF' import json, os settings_path = os.path.expanduser("~/.claude/settings.json") hooks_dir = os.path.expanduser("~/.claude/hooks") # Load existing or start fresh if os.path.exists(settings_path): with open(settings_path) as f: settings = json.load(f) else: settings = {} # Ensure hooks section exists if "hooks" not in settings: settings["hooks"] = {} hooks = settings["hooks"] # Add UserPromptSubmit hook (prevent sleep) if "UserPromptSubmit" not in hooks: hooks["UserPromptSubmit"] = [] prevent_sleep = {"matcher": "", "hooks": [{"type": "command", "command": f"{hooks_dir}/prevent-sleep.sh"}]} if not any("prevent-sleep" in json.dumps(h) for h in hooks["UserPromptSubmit"]): hooks["UserPromptSubmit"].append(prevent_sleep) # Add Stop hook (autonomous block) if "Stop" not in hooks: hooks["Stop"] = [] stop_hook = {"matcher": "", "hooks": [{"type": "command", "command": f"{hooks_dir}/stop-hook-autonomous.sh"}]} if not any("stop-hook-autonomous" in json.dumps(h) for h in hooks["Stop"]): hooks["Stop"].append(stop_hook) # Add SessionEnd hook (allow sleep) if "SessionEnd" not in hooks: hooks["SessionEnd"] = [] session_end = {"matcher": "", "hooks": [{"type": "command", "command": f"{hooks_dir}/allow-sleep.sh"}]} if not any("allow-sleep" in json.dumps(h) for h in hooks["SessionEnd"]): hooks["SessionEnd"].append(session_end) with open(settings_path, "w") as f: json.dump(settings, f, indent=2) f.write("\n") print("done") PYEOF ok "Registered hooks in ~/.claude/settings.json" else warn "python3 not found — you'll need to manually add hooks to ~/.claude/settings.json" info "See the README for the required settings.json format" fi fi echo "" # ── Summary ────────────────────────────────────────────────────────────────── echo -e " ${BOLD}${GREEN}Setup complete!${NC}" echo "" echo -e " ${DIM}────────────────────────────────────────────────────────${NC}" echo "" echo -e " ${BOLD}How to use:${NC}" echo "" echo -e " ${CYAN}1.${NC} Edit your task list:" echo -e " ${DIM}vim loop/plan.md${NC}" echo "" echo -e " ${CYAN}2.${NC} Edit your prompt (add your project's rules and specs):" echo -e " ${DIM}vim loop/prompt.md${NC}" echo "" echo -e " Replace the {{PLACEHOLDERS}} with your project's:" echo -e " - Spec file path (detailed task descriptions)" echo -e " - Project rules file path (coding conventions)" echo -e " - Inline project rules (style, frameworks, constraints)" echo "" echo -e " ${CYAN}3.${NC} Start the overnight run:" echo -e " ${DIM}./loop/loop.sh${NC}" echo "" echo -e " Or with caffeinate (belt-and-suspenders sleep prevention):" echo -e " ${DIM}caffeinate -i ./loop/loop.sh${NC}" echo "" echo -e " ${CYAN}4.${NC} Monitor progress:" echo -e " ${DIM}tail -f loop/loop.log${NC}" echo -e " ${DIM}grep -c '\\[x\\]' loop/plan.md${NC} # completed tasks" echo -e " ${DIM}grep -c '\\[ \\]' loop/plan.md${NC} # remaining tasks" echo "" echo -e " ${BOLD}Configuration:${NC}" echo "" echo -e " ${DIM}CLAUDE_AUTONOMOUS=1${NC} Stop hook active (default)" echo -e " ${DIM}CLAUDE_AUTONOMOUS=0${NC} Claude can stop freely" echo -e " ${DIM}ITERATION_COUNT=20${NC} Run 20 iterations" echo -e " ${DIM}ITERATION_DELAY=60${NC} 60s between iterations" echo "" echo -e " ${BOLD}Quick test:${NC}" echo -e " ${DIM}CLAUDE_AUTONOMOUS=0 ITERATION_COUNT=1 ./loop/loop.sh${NC}" echo "" echo -e " ${DIM}────────────────────────────────────────────────────────${NC}" echo -e " ${DIM}Full documentation: README.md${NC}" echo ""