#!/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 (claude-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 "claude-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 "claude-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}/claude-sidekick.pid"
PROMPT_FILE="${TRANSCRIPTION_DIR}/claude-sidekick-prompt.md"
LAUNCHER_FILE="${TRANSCRIPTION_DIR}/launch-claude-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 "claude-sidekick: Claude 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 a poll loop each
# session (which is non-deterministic and silently emits nothing when it reaches
# for bash-isms under the zsh-launched Monitor).
if [ -f "${WATCHER_SRC}" ]; then
    cp "${WATCHER_SRC}" "${WATCHER_DST}"
    chmod 0755 "${WATCHER_DST}"
else
    echo "claude-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 with Tuple's bundled watcher script: one `Bash` run to catch up, then a `Monitor`'d run for live updates (see Catch up first / Watcher). `Read` is available too, for looking something up or revisiting a past call.

## Catch up first

First, catch up on everything said before you joined. Run the watcher once via `Bash` in catch-up mode:

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

It prints the whole backlog as `T|`/`E|` tagged lines (format under Watcher); if the output is large enough that Claude Code saves it to a file, `Read` that file to get all of it. 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.

## 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

Once you've caught up, start live monitoring. Subscribe via `Monitor` to the same script — same `--offsets claude` file, so it resumes exactly where the catch-up stopped (no gap, no repeat):

    ./__SESSIONDIR__/tuple-call-watcher.py --offsets claude

It follows every session directory of this call (including mid-call restarts) and forwards new records as they arrive, at conversation speed. Each `Monitor` wake delivers one or more tagged lines:

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

`Monitor` truncates a large notification (and any over-long line), marking the cut with `...(truncated)`. Live batches are small and usually fit, but if a record arrives carrying that marker, `Read` it from the session's `transcriptions.jsonl` to recover the full text. No data is lost — the files always hold it all.

## Handling each batch

You're a real-time companion for the whole call — log as it happens. Each live wake delivers one or more `T|`/`E|` tagged lines (described above). Parse the `<json-record>` portion of each. 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>

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

## Wake words

A line addresses you when "Claude" appears as a vocative — at the start of an utterance ("Claude, ..."), after a greeting ("hey Claude"), or as a direct question ("Claude, can you ..."). Incidental mentions like "Claude Code 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 never stops the watcher

The user can type directly into this terminal — to address you or ask a question. Treat the message as one event among many. Reply, then immediately resume polling for new transcript records. Don't sit idle waiting for the next user line. The call is still happening even when the user is typing.

To start: catch up first (one `Bash` run + a brief summary), then start the live `Monitor` watcher and end your turn, waiting for records.

## Following later calls (only if asked)

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

    ./__SESSIONDIR__/tuple-call-watcher.py --catchup --offsets claude <new-call-id>   # catch up
    ./__SESSIONDIR__/tuple-call-watcher.py --offsets claude <new-call-id>             # then Monitor 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 claude` 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 Claude Sidekick\a'
clear
echo 'Starting Claude sidekick…'
echo

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

echo $$ > "${PID_FILE}"
# cwd = the transcripts root (parent of this call's session dir) so the watcher
# path `./<session-dir>/...` and the "follow later calls" glob both resolve.
cd "${SCRIPT_DIR}/.." || exit 1

if ! command -v claude >/dev/null 2>&1; then
    echo "Claude Code was not found on your interactive shell PATH."
    echo "Make sure 'claude' 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

exec claude --allowed-tools Read Bash Monitor --name "Tuple Claude Sidekick" -- "$(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 [ "${CLAUDE_SIDEKICK_DRY_RUN:-}" = "1" ]; then
    echo "claude-sidekick: dry run generated ${LAUNCHER_FILE}"
    exit 0
fi

launch_in_terminal "${LAUNCHER_FILE}"
