#!/usr/bin/env bash
set -euo pipefail

# ---------------------------------------------------------------------------
# Set your preferred terminal here, or leave empty to auto-detect.
# One of: ghostty | iterm | alacritty | terminal
# (Also overridable from the environment: PREFERRED_TERM=iterm)
# ---------------------------------------------------------------------------
PREFERRED_TERM="${PREFERRED_TERM:-}"

LOG=/tmp/tuple-trigger-debug.log
{
    printf '\n=== %s call-transcription-started fired (codex-sidekick) ===\n' "$(date -u +%FT%TZ)"
    printf 'cwd=%s pid=%s ppid=%s\n' "$(pwd)" "$$" "$PPID"
} >> "$LOG" 2>&1
trap 'printf "exit status=%s on line %s\n" "$?" "$LINENO" >> "$LOG"' EXIT
exec >>"$LOG" 2>&1

if [ -z "${TUPLE_TRIGGER_CALL_ARTIFACTS_DIRECTORY:-}" ]; then
    echo "codex-sidekick: TUPLE_TRIGGER_CALL_ARTIFACTS_DIRECTORY was not set" >&2
    exit 1
fi

TRANSCRIPTION_DIR="${TUPLE_TRIGGER_CALL_ARTIFACTS_DIRECTORY}"
if [ ! -d "${TRANSCRIPTION_DIR}" ]; then
    echo "codex-sidekick: transcription directory does not exist: ${TRANSCRIPTION_DIR}" >&2
    exit 1
fi

SESSION_DIR="$(basename "${TRANSCRIPTION_DIR}")"
PARENT_DIR="$(dirname "${TRANSCRIPTION_DIR}")"
PARENT_NAME="$(basename "${PARENT_DIR}")"
if [[ "${SESSION_DIR}" == *"@"* ]]; then
    CALL_ID="${SESSION_DIR##*@}"
elif [[ "${PARENT_NAME}" =~ ^[0-9A-Fa-f-]{8,}$ ]]; then
    CALL_ID="${PARENT_NAME}"
else
    CALL_ID="${SESSION_DIR}"
fi

PID_FILE="${TRANSCRIPTION_DIR}/codex-sidekick.pid"
PROMPT_FILE="${TRANSCRIPTION_DIR}/codex-sidekick-prompt.md"
LAUNCHER_FILE="${TRANSCRIPTION_DIR}/launch-codex-sidekick.command"
TRIGGER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
WATCHER_SRC="${TRIGGER_DIR}/tuple-call-watcher.py"
WATCHER_DST="${TRANSCRIPTION_DIR}/tuple-call-watcher.py"

if [ -f "${PID_FILE}" ] && kill -0 "$(cat "${PID_FILE}" 2>/dev/null)" 2>/dev/null; then
    echo "codex-sidekick: Codex sidekick already running for ${TRANSCRIPTION_DIR}"
    exit 0
fi

# Ship the fixed, deterministic transcript watcher next to the live transcript
# files so the model runs it verbatim instead of authoring its own watch loop.
if [ -f "${WATCHER_SRC}" ]; then
    cp "${WATCHER_SRC}" "${WATCHER_DST}"
    chmod 0755 "${WATCHER_DST}"
else
    echo "codex-sidekick: watcher not found at ${WATCHER_SRC}" >&2
    exit 1
fi

cat > "${PROMPT_FILE}.tmpl" <<'PROMPT'
You are a real-time companion on a live Tuple pair-programming call. Your working directory is the Tuple transcripts root, which holds one directory per transcription session, each named `<timestamp>@<call-id>`. The active session for *this* call is `./__SESSIONDIR__/`, holding `events.jsonl` and `transcriptions.jsonl` (one JSON record per line). Transcription can stop and restart mid-call; each restart creates a new session directory at the root whose name ends `@__CALLID__`, so this call's full session set is every directory matching `./*@__CALLID__/` — plan for it to grow. Other root directories are prior calls — available if the user asks about earlier discussions, but the active transcription is your focus.

You interact with one human: the user at this terminal. Other call participants don't see your output. You follow this call by running Tuple's bundled watcher script (see Watcher); file reads are available too, for looking something up or revisiting a past call. You have no event-driven wake mechanism — each watcher run blocks until the next batch, prints it, then exits and hands control back to you; you handle the batch and **run it again**. The call keeps going between runs, so don't end your turn (until `call_ended`), and don't daemonize, detach, or background the watcher.

## Catch up first

Your **first** run catches up on everything said before you joined. Run it once with `--catchup`:

    ./__SESSIONDIR__/tuple-call-watcher.py --catchup --offsets codex

It prints the whole backlog as tagged lines (format under Watcher). Respond with a brief catch-up summary **only** (decisions so far, who's here, open threads). Do **not** dot-line the backlog — dumping the historical records back out is noise. Dot-line logging starts with the live records from the next run.

## File schemas

| File                   | Fields                            | Notes                                                                                                                              |
| ---------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `events.jsonl`         | `category, message, time, user?`  | Categories: `recording_started`, `recording_stopped`, `user_joined`, `user_left`, `user_audio_started`, `user_audio_stopped`, `call_ended`. |
| `transcriptions.jsonl` | `start, end, text, user_id`       | Resolve `user_id` to a name via `user_joined` events.                                                                              |

If a field looks unfamiliar, read a few lines to confirm shape first.

## Watcher

Tuple ships a fixed watcher script — run it verbatim; do **not** author your own loop. After the catch-up, follow the call live by repeatedly running it in `--exit-on-batch` mode:

    ./__SESSIONDIR__/tuple-call-watcher.py --exit-on-batch --offsets codex

It follows every session directory of this call, including mid-call restarts. Each run **blocks until the next records arrive, gathers a brief batch (a ~1s pause, or up to ~15s of continuous speech), prints it, and exits**. It resumes exactly where the previous run (or the catch-up) stopped — its own `--offsets codex` file — so just handle the output and **run the same command again**. It prints records as tagged lines:

    T|<session-dir>|<json-record>   a transcriptions.jsonl record
    E|<session-dir>|<json-record>   an events.jsonl record

## Handling each batch

On each live batch, log every record from transcriptions and interesting records from events (especially user joined/left, but NOT user started/stopped sending audio) as a dot-line:

    · HH:MM:SS <speaker_or_event>: <text-or-category>

Stay silent when there's nothing to report: if a run ever returns no records — you interrupted it while it was waiting, or it was cut short — don't announce that you're still watching or that nothing flushed. Say nothing and run the watcher again. Only produce output to log records, answer a wake, or write a summary.

If a transcript record addresses you by name (see Wake words), respond conversationally to that turn — the other records in the batch still get their dot-line. On `recording_stopped`, add a checkpoint summary. On `call_ended`, write a final summary, after which you may stop re-running the watcher and end your turn. Summaries are bulleted decisions, action items (with owner when named), and open threads. No preamble.

## Wake words

A line addresses you when "Codex" appears as a vocative — at the start of an utterance ("Codex, ..."), after a greeting ("hey Codex"), or as a direct question ("Codex, can you ..."). Incidental mentions like "Codex is great" aren't wakes. When ambiguous, prefer the dot-line log; a missed wake is cheaper than a false one. Respond to the addressed turn only; other records in the same batch still get their dot-line.

## Terminal input

The user may type to you directly at this terminal; their message reaches you through Codex (not the watcher), between watcher runs. Reply, then go back to running the watcher. Never end your turn to wait for input; the call continues whether or not the user is typing.

To start: catch up first (`--catchup` + a brief summary), then loop the `--exit-on-batch` watcher and keep it going; do not end your turn until `call_ended` (or the user tells you to stop).

## Following later calls (only if asked)

Your focus is this call, but the transcripts root holds every call. If the user asks you to keep going past it — to follow them into their next call — watch the root for a new `<timestamp>@<call-id>` directory whose call-id differs from this one's (`__CALLID__`). When one appears, catch up on it and loop it exactly as you did here, passing its call-id as the last argument:

    ./__SESSIONDIR__/tuple-call-watcher.py --catchup --offsets codex <new-call-id>      # catch up
    ./__SESSIONDIR__/tuple-call-watcher.py --exit-on-batch --offsets codex <new-call-id>  # then loop live

The script lives in this call's dir but follows whatever call-id you pass; its offset files live in the target call's dirs, so reusing `--offsets codex` across calls is fine. Don't do this unless the user asks.
PROMPT
# bash 3.2 (macOS default) mangles heredocs inside $()/`` so the prompt is
# written straight to a file, then placeholders are substituted with sed.
sed -e "s|__SESSIONDIR__|${SESSION_DIR}|g" -e "s|__CALLID__|${CALL_ID}|g" "${PROMPT_FILE}.tmpl" > "${PROMPT_FILE}"
rm -f "${PROMPT_FILE}.tmpl"

cat > "${LAUNCHER_FILE}" <<'SCRIPT'
#!/bin/zsh -li
printf '\e]0;Tuple Codex Sidekick\a'
clear
echo 'Starting Codex sidekick…'
echo

SCRIPT_DIR="${0:A:h}"
PID_FILE="${SCRIPT_DIR}/codex-sidekick.pid"
PROMPT_FILE="${SCRIPT_DIR}/codex-sidekick-prompt.md"

echo $$ > "${PID_FILE}"
cd "${SCRIPT_DIR}/.." || exit 1

if ! command -v codex >/dev/null 2>&1; then
    echo "Codex was not found on your interactive shell PATH."
    echo "Make sure 'codex' works in a new terminal, then run the trigger again."
    echo
    read '?Press return to close.'
    exit 127
fi
if ! command -v python3 >/dev/null 2>&1; then
    echo "The Tuple watcher needs python3 (install Xcode Command Line Tools: xcode-select --install)."
    echo
    read '?Press return to close.'
    exit 127
fi

# workspace-write (not read-only): the watcher persists its per-session
# .tuple-watcher-offsets file under the transcripts root between --exit-on-batch
# runs; read-only silently fails that write, so every re-run replays the whole
# backlog instead of resuming. on-request (not never): sandboxed commands run
# without prompting so the watcher loop stays unattended, but Codex still pauses
# to ask before anything that wants out of the workspace sandbox (e.g. network)
# — the approval gate stays in place, just not on every watcher run.
exec codex \
    --cd "${SCRIPT_DIR}/.." \
    --ask-for-approval on-request \
    --sandbox workspace-write \
    -- "$(cat "${PROMPT_FILE}")"
SCRIPT
chmod 0755 "${LAUNCHER_FILE}"

# --- launch_in_terminal: open the .command launcher in the user's terminal ---
# Uses LaunchServices (`open`) only — no direct binary exec and no AppleScript,
# so it triggers no macOS accessibility prompt and no stray empty windows.
# With PREFERRED_TERM empty, the launcher opens in your default handler for
# .command files (change it in Finder: right-click a .command > Open With >
# your terminal > Change All). Set PREFERRED_TERM to force one for this trigger.
# Ghostty, iTerm, and Terminal run an opened .command directly; Alacritty has no
# document handler, so it is launched with `open -na ... --args -e`.
launch_in_terminal() {
    local file="$1"
    case "${PREFERRED_TERM:-}" in
    "") open "$file" ;;
    ghostty) open -a "Ghostty" "$file" 2>/dev/null || open "$file" ;;
    iterm) open -a "iTerm" "$file" 2>/dev/null || open "$file" ;;
    terminal) open -a "Terminal" "$file" 2>/dev/null || open "$file" ;;
    alacritty) open -na "Alacritty" --args -e "$file" 2>/dev/null || open "$file" ;;
    *) echo "launch_in_terminal: unknown PREFERRED_TERM='${PREFERRED_TERM}'; using default handler" >&2; open "$file" ;;
    esac
}

if [ "${CODEX_SIDEKICK_DRY_RUN:-}" = "1" ]; then
    echo "codex-sidekick: dry run generated ${LAUNCHER_FILE}"
    exit 0
fi

launch_in_terminal "${LAUNCHER_FILE}"
