#!/bin/bash # slab-tracked Codex launcher. # # Registers this Codex session as a "prompt rock" so it shows up in the slab # menubar + prox exactly like a Claude session. Codex fires no per-event hooks # the way Claude Code does, so instead we: # 1. write an active-prompts marker (agent_type=codex) up front, # 2. run a background watcher that tails Codex's rollout JSONL and flips the # shared state files (active-prompts / awaiting-prompts / running-tools) # on task_started / task_complete — the same files the Claude hooks touch, # so the menubar's state→color reducer needs zero Codex-specific logic, # 3. clean everything up when Codex exits (or gets reaped if the tab dies, # since the marker carries agent_pid). # # Invoked by the `co` / `cor` fish shortcuts. Run `codex` directly to skip # tracking. set -u SLAB_HOME=${SLAB_HOME:-$HOME/.local/share/slab} SLAB_BIN=${SLAB_BIN:-$HOME/.local/bin} ACTIVE_DIR="$SLAB_HOME/state/active-prompts" AWAITING_DIR="$SLAB_HOME/state/awaiting-prompts" RUNNING_DIR="$SLAB_HOME/state/running-tools" mkdir -p "$ACTIVE_DIR" "$AWAITING_DIR" "$RUNNING_DIR" # No jq or codex? Fall back to a plain, untracked codex so the shortcut never # breaks the user's ability to launch it. if ! command -v codex >/dev/null 2>&1; then echo "codex not found on PATH" >&2 exit 127 fi if ! command -v jq >/dev/null 2>&1; then exec codex "$@" fi sid=$(uuidgen) export SLAB_PROMPT_SESSION_ID="$sid" begin=$(date +%s) # The wrapper (this bash process) is the parent of codex and holds the tab's # controlling tty — use it for liveness + terminal-close, matching claude_pid. tty=${SLAB_TERMINAL_TTY:-$(ps -o tty= -p $$ 2>/dev/null | tr -d ' ')} cwd=$PWD ts=$(date -u +%Y-%m-%dT%H:%M:%SZ) # Codex probes its terminal colors once. Set Slab's page BEFORE launching it, # then retain that appearance while its status hues change during the session. terminal_dark=null if command -v node >/dev/null 2>&1 && [[ -f "$SLAB_BIN/slab-prepare-terminal.mjs" ]]; then terminal_dark=$(node "$SLAB_BIN/slab-prepare-terminal.mjs" "$tty" 2>/dev/null) || terminal_dark=null fi case "$terminal_dark" in true|false|null) ;; *) terminal_dark=null ;; esac # Fresh window reads "blank" (like Claude's SessionStart) until the first turn. jq -n \ --arg sid "$sid" --arg cwd "$cwd" --arg tty "$tty" \ --arg pid "$$" --arg ts "$ts" \ --argjson terminal_dark "$terminal_dark" \ --arg nudge "${SLAB_NUDGE_SCREEN:-}" --arg contact "${SLAB_LOOPBOY_CONTACT:-}" \ '{session_id:$sid, cwd:$cwd, subject:"codex session", summary:"codex", tty:$tty, agent_pid:($pid|tonumber), agent_type:"codex", updated:$ts, state:"blank", terminal_dark:$terminal_dark, nudge_screen:$nudge, loopboy_contact:$contact}' \ > "$ACTIVE_DIR/$sid" 2>/dev/null # Background watcher: derives per-turn state from the rollout JSONL. watcher="" if command -v node >/dev/null 2>&1 && [[ -f "$SLAB_BIN/codex-session-watch.mjs" ]]; then node "$SLAB_BIN/codex-session-watch.mjs" "$sid" "$begin" "$$" "$tty" "$cwd" \ >/dev/null 2>&1 & watcher=$! fi cleanup() { [[ -n "$watcher" ]] && kill "$watcher" 2>/dev/null rm -f "$ACTIVE_DIR/$sid" "$AWAITING_DIR/$sid" "$RUNNING_DIR/$sid" } agent_pid="" stop_agent() { trap - INT TERM if [[ -n "$agent_pid" ]] && kill -0 "$agent_pid" 2>/dev/null; then kill -TERM "$agent_pid" 2>/dev/null || true for _ in {1..10}; do kill -0 "$agent_pid" 2>/dev/null || break sleep 0.05 done kill -KILL "$agent_pid" 2>/dev/null || true wait "$agent_pid" 2>/dev/null || true fi exit 143 } trap cleanup EXIT trap stop_agent INT TERM # Loopboys are background client loops, never fleet/UI operators. Remove the # browser surfaces at process startup; the proxy separately scopes its private # inbox to the named contact and session identity. codex_guard_args=() if [[ -n "${SLAB_LOOPBOY_CONTACT:-}" ]]; then codex_guard_args+=( -c 'mcp_servers.prox={url="http://127.0.0.1:7773/mcp", env_http_headers={"x-slab-loopboy-contact"="SLAB_LOOPBOY_CONTACT", "x-slab-prompt-session-id"="SLAB_PROMPT_SESSION_ID"}}' -c 'mcp_servers.puppet.enabled=false' -c 'mcp_servers.frame.enabled=false' -c 'mcp_servers.fleet.enabled=false' --disable browser_use --disable browser_use_external --disable browser_use_full_cdp_access --disable in_app_browser ) fi # Keep Codex on this wrapper's foreground process group and explicitly attach # its streams to the controlling TTY. Running it as a managed child lets a ZZZ # SIGTERM reach the actual agent before the wrapper exits, without closing the # terminal that will carry the resume receipt. # macOS still ships Bash 3.2, where expanding an empty array under `set -u` # raises "unbound variable". Keep the guarded and ordinary launch forms # explicit so the everyday `co` path never touches an empty array expansion. if (( ${#codex_guard_args[@]} )); then codex "${codex_guard_args[@]}" "$@" < "/dev/$tty" > "/dev/$tty" 2>&1 & else codex "$@" < "/dev/$tty" > "/dev/$tty" 2>&1 & fi agent_pid=$! # From this point on, lifecycle managers must signal the actual Codex process, # not this Bash wrapper. Bash defers TERM traps while it waits for a child; # tracking the wrapper here could therefore orphan Codex when ZZZ parks it. marker_tmp="$ACTIVE_DIR/.$sid.$$.tmp" if jq --arg pid "$agent_pid" --arg wrapper "$$" \ '.agent_pid=($pid|tonumber) | .wrapper_pid=($wrapper|tonumber)' \ "$ACTIVE_DIR/$sid" > "$marker_tmp" 2>/dev/null; then mv "$marker_tmp" "$ACTIVE_DIR/$sid" else rm -f "$marker_tmp" fi wait "$agent_pid" status=$? agent_pid="" exit "$status"