#!/usr/bin/env bash # # opencode-builder.sh # # Launches an OpenCode server, creates a session with the auto-agents # agent, and continuously sends "continue" messages in an infinite loop. # # Signal handling: # Ctrl+C once — finishes the in-flight request, then stops the loop # Ctrl+C twice — (within 2 seconds) kills the request and exits immediately # set -uo pipefail # ── Configuration (override via environment) ────────────────────────────── PORT="${OPENCODE_PORT:-4096}" HOST="${OPENCODE_HOST:-127.0.0.1}" BASE="http://${HOST}:${PORT}" AGENT="auto-agents" PROMPT="Complete the current project's milestones up to and including v3.7.0 to a production ready state" HEALTH_TIMEOUT=60 # seconds to wait for the server to become healthy # ── Internal state ──────────────────────────────────────────────────────── SERVER_PID="" CURL_PID="" SESSION_ID="" STOP_LOOP=false LAST_SIGINT=0 OWN_SERVER=false # ── Helpers ─────────────────────────────────────────────────────────────── die() { printf "ERROR: %s\n" "$*" >&2; exit 1; } log() { printf "[%s] %s\n" "$(date +%H:%M:%S)" "$*"; } check_opencode_running() { # Check if port is open and responding as OpenCode server if curl -sf "${BASE}/global/health" >/dev/null 2>&1; then return 0 # OpenCode server is running else return 1 # No OpenCode server detected fi } cleanup() { if $OWN_SERVER && [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then log "Stopping opencode server (PID $SERVER_PID)..." kill "$SERVER_PID" 2>/dev/null wait "$SERVER_PID" 2>/dev/null || true elif ! $OWN_SERVER; then log "Leaving existing OpenCode server running (not managed by this script)" fi } handle_sigint() { local now now=$(date +%s) # Second Ctrl+C within 2 seconds → force-quit if $STOP_LOOP && (( now - LAST_SIGINT <= 2 )); then printf "\nForce quit!\n" [[ -n "$CURL_PID" ]] && kill "$CURL_PID" 2>/dev/null || true cleanup exit 130 fi # First Ctrl+C → graceful stop after current request STOP_LOOP=true LAST_SIGINT=$now printf "\nWill stop after the current request completes. Ctrl+C again within 2s to force-quit.\n" } trap handle_sigint INT trap cleanup EXIT # ── Preflight checks ───────────────────────────────────────────────────── for cmd in curl jq opencode; do command -v "$cmd" &>/dev/null || die "'$cmd' is required but not found in PATH" done # ── Start the opencode server ───────────────────────────────────────────── # Check if OpenCode server is already running if check_opencode_running; then echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ ⚠️ WARNING: OpenCode server already running on ${HOST}:${PORT}" echo "│ 🔗 Connecting to existing server instead of starting new one" echo "│ 💡 The script will NOT manage this server's lifecycle" echo "└─────────────────────────────────────────────────────────────────┘" echo "" log "Using existing OpenCode server at ${HOST}:${PORT}" OWN_SERVER=false else # Start our own server as before log "Starting opencode server on ${HOST}:${PORT}..." opencode serve --port "$PORT" --hostname "$HOST" & SERVER_PID=$! OWN_SERVER=true log "Waiting for server to become healthy (up to ${HEALTH_TIMEOUT}s)..." healthy=false for (( i = 1; i <= HEALTH_TIMEOUT; i++ )); do if curl -sf "${BASE}/global/health" >/dev/null 2>&1; then healthy=true break fi kill -0 "$SERVER_PID" 2>/dev/null || die "Server process exited unexpectedly" sleep 1 done $healthy || die "Server did not become healthy within ${HEALTH_TIMEOUT}s" log "Server is ready." fi # ── Create a session ────────────────────────────────────────────────────── log "Creating session..." SESSION_ID=$( curl -sf -X POST "${BASE}/session" \ -H "Content-Type: application/json" \ -d '{"title":"auto-agents"}' \ | jq -r '.id' ) || die "Failed to create session" [[ -n "$SESSION_ID" && "$SESSION_ID" != "null" ]] || die "Session ID is empty or null" log "Session ID: ${SESSION_ID}" # ── send_message ────────────────────────────────────────────────────────── # Sends a JSON body to POST /session/:id/message and blocks until the LLM # responds. curl is run in a subprocess that ignores SIGINT (SIG_IGN is # inherited across exec), so a single Ctrl+C cannot kill the in-flight # request. The parent re-waits whenever its own trap handler interrupts # the `wait` builtin. send_message() { local body=$1 ( trap '' INT exec curl -sf -X POST "${BASE}/session/${SESSION_ID}/message" \ -H "Content-Type: application/json" \ -d "$body" \ -o /dev/null ) & CURL_PID=$! # Keep waiting until curl actually exits — `wait` can return early when # our SIGINT handler fires, so we loop on kill -0 to re-wait. while kill -0 "$CURL_PID" 2>/dev/null; do wait "$CURL_PID" 2>/dev/null || true done CURL_PID="" } # ── Send the initial prompt ────────────────────────────────────────────── INIT_BODY=$(jq -nc --arg a "$AGENT" --arg t "$PROMPT" \ '{agent: $a, parts: [{type: "text", text: $t}]}') log "Sending initial prompt (agent: ${AGENT})..." log "Prompt: ${PROMPT}" echo "" send_message "$INIT_BODY" log "Initial prompt complete." # ── Continue loop ───────────────────────────────────────────────────────── CONT_BODY=$(jq -nc --arg a "$AGENT" '{agent: $a, parts: [{type: "text", text: "continue with your mainloop, monitor the supervisors, keep them alive, and sleep in an infinite loop."}]}') N=0 while ! $STOP_LOOP; do N=$((N + 1)) log "── continue #${N} ──" send_message "$CONT_BODY" log "continue #${N} complete." done echo "" log "Loop stopped. Shutting down." exit 0