#!/usr/bin/env bash
set -euo pipefail
# POS: ai openrouter — Chat with OpenRouter models (ask, capture, chat, models, sessions)
# POS_SUBCMDS: ask capture chat models sessions
# POS_FLAGS: --model --session --system --full --last
# POS_CONFIG: ai-openrouter | ai-openrouter.env | OPENROUTER_API_KEY=secret:API key from openrouter.ai (https://openrouter.ai/settings/keys) | OPENROUTER_MODEL=:Model id (default openrouter/auto)

source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"

CONFIG_FILE="$HOME/.config/linux_post_install/ai-openrouter.env"
API="https://openrouter.ai/api/v1"
DEFAULT_MODEL="openrouter/auto"
SESSION_DIR="$HOME/.local/share/linux_post_install/ai-openrouter"
DISPATCH_LOG_DIR="$HOME/.local/share/linux_post_install/logs"  # bin/pos per-run logs
LAST_CMD_OUTPUT_FILE="$HOME/.local/share/linux_post_install/last_cmd_output"  # --last fallback for any command
OS_RELEASE_FILE="${OS_RELEASE_FILE:-/etc/os-release}"          # read-only test seam (DEV.md env-overridable paths)
SESSION="default"
SYSTEM_PROMPT=""
MAX_SESSION_TURNS=40
LAST_LOG_MAX_BYTES=4096
LAST_LOG_STALE_SECS=3600   # --last: warn when the attached log is older than this
# Built-in terse ask prompt. cmd_ask appends a machine-context clause
# (see machine_context) unless --system replaces it or --full drops everything.
DEFAULT_SYSTEM_PROMPT="You assist a user working in a Linux/Unix CLI terminal. Be extremely terse: lead with the exact command(s) to run; one-line explanations max; short bullets only when necessary; no greetings, no closing offers, no essays. The user's message may be an install/update/solve/edit question ('how do I …') and/or may paste a problem, error, or command output: diagnose it from that and lead with the fix command(s)."

usage() {
    cat <<EOF
Usage: pos ai openrouter <subcommand> [--model <id>] [--session <name>] [--system <text>] [--full] [--last]

Chat with OpenRouter models via the REST API (openrouter.ai).

Subcommands:
  ask "<prompt>"    Answer; prints the answer text to stdout. The prompt may
                    also be piped in via stdin when no argument is given.
                    Runs inside the persistent 'default' session (prior turns
                    are sent as context); --session <name> picks another.
  capture <cmd..>   Run a command, capture its output for --last, and show it.
                    Each capture overwrites the previous one (latest only).
  chat              Interactive multi-turn conversation (session 'default'
                    unless --session is given).
  models            List available models and flag the configured default.
  sessions          List persistent sessions / clear one:
                    'sessions' and 'sessions reset <name>'.

Options:
  --model <id>      Override the model for this invocation.
  --session <name>  Use a named persistent session instead of 'default':
                    ~/.local/share/linux_post_install/ai-openrouter/<name>.json
                    (capped at $MAX_SESSION_TURNS turns).
  --system <text>   System instruction sent with every turn (kept out of the
                    session file); replaces the built-in terse ask prompt
                    wholesale, e.g. "Reply like a friendly Telegram chat".
  --full            Skip the built-in terse prompt — long-form answers.
  --last            ask only: attach the most recent pos dispatcher log or
                    captured output (tail, max $LAST_LOG_MAX_BYTES chars) so
                    the model can diagnose a real failure. Sources in priority
                    order: (1) newest pos log, (2) captured output from
                    'capture'. Notes on stderr which source was attached and
                    its age; warns when stale (>60 min).
  -h|--help         This help.

Config:  $CONFIG_FILE  (edit with 'pos config ai-openrouter')
  OPENROUTER_API_KEY   API key from openrouter.ai (required)
  OPENROUTER_MODEL     Model id (default $DEFAULT_MODEL)

Notes:
  ask is terse by default: a built-in system instruction tells the model to
  lead with the exact commands and keep prose minimal — and to diagnose pasted
  errors/output with a fix first. That prompt ends with one machine-context
  line (hostname, distro, kernel, arch detected on this box) so answers fit
  the actual machine; --system replaces it wholesale, --full drops it all.
  Every ask/chat lands in a persistent session ('default' unless --session);
  clear it with 'pos ai openrouter sessions reset default'. On a terminal the
  answer is rendered as markdown (glow if installed, else a built-in
  renderer); when stdout is not a tty (pipes, scripts, Telegram bridges) the
  raw markdown is printed unchanged.

Examples:
  pos ai openrouter ask "check disk space on /"
  pos ai openrouter ask --full "Explain DNS in depth"
  echo "summarize this log" | pos ai openrouter ask
  failing-cmd 2>&1 | pos ai openrouter ask how do I fix this
  pos ai openrouter ask --last "why did that fail?"        # attach last output
  pos ai openrouter capture pip install xyz                # capture any command
  pos ai openrouter ask --last "what happened?"            # after capture
  pos ai openrouter chat
  pos ai openrouter ask --model anthropic/claude-sonnet-4 "hi"
  pos ai openrouter ask --system "Reply like a pirate" "explain chmod"
  pos ai openrouter ask --session work "my name is joe"
  pos ai openrouter ask --session work "what is my name?"   # remembers
  pos ai openrouter sessions
  pos ai openrouter sessions reset default                  # forget default memory
EOF
    exit 0
}

# ── ai-openrouter.env loader (same pattern as telegram.env) ────────
load_config() {
    [ -f "$CONFIG_FILE" ] || return 0
    local k v
    while IFS='=' read -r k v; do
        [ -n "$k" ] || continue
        case "$k" in
            \#*) continue ;;
        esac
        v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
        v="${v//$'\r'/}"
        if [ -z "${!k:-}" ]; then
            export "$k"="$v"
        fi
    done < <(grep -E '^[A-Z_]+=' "$CONFIG_FILE" || true)
}

require_key() {
    load_config
    [ -n "${OPENROUTER_API_KEY:-}" ] || err "No OpenRouter API key — run 'pos config ai-openrouter'"
}

resolve_model() {
    if [ -n "${MODEL_OVERRIDE:-}" ]; then
        printf '%s' "$MODEL_OVERRIDE"
    elif [ -n "${OPENROUTER_MODEL:-}" ]; then
        printf '%s' "$OPENROUTER_MODEL"
    else
        printf '%s' "$DEFAULT_MODEL"
    fi
}

# ── --last: attach the most recent pos command output ───────────
# bin/pos logs every non-interactive run to DISPATCH_LOG_DIR/<ts>_pos_<cmd>.log
# (ai-openrouter itself is interactive-logged, so it never creates its own output
# log). The <ts> prefix is zero-padded sortable, so name-descending = newest;
# mtime alone would tie-flake for same-second runs. pos.log is the invocation
# index, not command output — excluded.
newest_pos_log() {
    [ -d "$DISPATCH_LOG_DIR" ] || return 1
    local f
    while IFS= read -r f; do
        [ -s "$f" ] && { printf '%s' "$f"; return 0; }
    done < <(ls "$DISPATCH_LOG_DIR"/*.log 2>/dev/null | LC_ALL=C sort -r | grep -v '/pos\.log$')
    return 1
}

# stdout = the context block body for log $1: its END kept (errors live at
# the bottom), head-truncated to LAST_LOG_MAX_BYTES with a marker.
last_log_context() {
    local raw
    raw="$(tail -c "$LAST_LOG_MAX_BYTES" "$1")"
    # a byte cut can split a multibyte char — drop invalid sequences when possible
    if command -v iconv >/dev/null 2>&1; then
        raw="$(printf '%s' "$raw" | iconv -c -f utf-8 -t utf-8 2>/dev/null || printf '%s' "$raw")"
    fi
    if [ "$(wc -c <"$1")" -gt "$LAST_LOG_MAX_BYTES" ]; then
        printf '[…truncated…]\n%s' "$raw"
    else
        printf '%s' "$raw"
    fi
}

# ── --last transparency (stderr-only; stdout stays pure answer) ──
# Seconds → human age: just now / Nm / Nh / Nd.
human_age() {
    local s="$1"
    [ "$s" -lt 0 ] && s=0
    if   [ "$s" -lt 60 ];    then printf 'just now'
    elif [ "$s" -lt 3600 ];  then printf '%sm' "$((s / 60))"
    elif [ "$s" -lt 86400 ]; then printf '%sh' "$((s / 3600))"
    else                          printf '%sd' "$((s / 86400))"
    fi
}

# Tell the user WHICH pos log got attached and how fresh it is — on STDERR,
# so a misread of an ancient log as the current failure is visible before
# the model answers. $1 = log file (as recorded by last_log_context).
last_log_annotate() {
    local f="$1" age_s age line
    age_s=$(( $(date +%s) - $(stat -c %Y "$f") ))
    [ "$age_s" -lt 0 ] && age_s=0
    age="$(human_age "$age_s")"
    printf '[i] attaching last pos output — %s (%s)\n' "$(basename "$f")" "$age" >&2
    # preview: first meaningful line of the log (blank lines skipped)
    line="$(grep -m1 '[^[:space:]]' "$f" 2>/dev/null || true)"
    if [ -n "$line" ]; then
        printf '[i]   "%.100s"\n' "$line" >&2
    fi
    if [ "$age_s" -gt "$LAST_LOG_STALE_SECS" ]; then
        printf '[!] that log is %s old and may not match your current problem. For a FRESH failure of any command:  failing-cmd 2>&1 | pos ai openrouter ask "what happened"\n' "$age" >&2
    fi
}

# ── Persistent session memory ───────────────────────────────────
# History lives as an OpenAI-style "messages" JSON document per session name under
# SESSION_DIR. Names are sanitized to [A-Za-z0-9_-]; ask/chat always run in a
# session ('default' unless --session names another).
session_file() {
    local name="${1:-$SESSION}"
    name="${name//[^A-Za-z0-9_-]/_}"
    printf '%s/%s.json' "$SESSION_DIR" "$name"
}

session_load() {
    [ -n "$SESSION" ] || { printf '{"messages":[]}'; return 0; }
    local f
    f="$(session_file)"
    if [ -s "$f" ] && jq -e '.messages' "$f" >/dev/null 2>&1; then
        cat "$f"
    else
        printf '{"messages":[]}'
    fi
}

session_save() {
    [ -n "$SESSION" ] || return 0
    local f tmp
    f="$(session_file)"
    mkdir -p "$SESSION_DIR"
    tmp="$(mktemp)"
    printf '%s\n' "$1" >"$tmp"
    mv "$tmp" "$f"
    chmod 600 "$f"
}

# Append a turn and prune to the last MAX_SESSION_TURNS entries. stdout = JSON.
session_push() {
    local messages="$1" role="$2" text="$3"
    printf '%s' "$messages" | jq -c --arg r "$role" --arg t "$text" \
        '.messages += [{role:$r, content:$t}] | .messages |= .[-'"$MAX_SESSION_TURNS"':]'
}

# One OpenRouter chat completion call. $1 = model, $2 = messages JSON, $3 = optional
# system instruction (prepended as a system message, not stored in the session).
# stdout = the answer text on success; an error message on failure (exit 1).
openrouter_generate() {
    local model="$1" messages_json="$2" system="${3:-}" body resp code body_out errmsg
    if [ -n "$system" ]; then
        body="$(printf '%s' "$messages_json" | jq -c --arg s "$system" \
            '[{role:"system",content:$s}] + .messages')"
    else
        body="$(printf '%s' "$messages_json" | jq -c '.messages')"
    fi
    body="$(printf '%s' "$body" | jq -nc --arg m "$model" --argjson msgs "$body" \
        '{model:$m, messages:$msgs}')"
    resp="$(curl -sS -m 60 -X POST "${API}/chat/completions" \
        -H "Authorization: Bearer ${OPENROUTER_API_KEY}" \
        -H "Content-Type: application/json" \
        -H "HTTP-Referer: https://github.com/admin/Linux_post_install" \
        --write-out $'\n%{http_code}' \
        --data "$body")" || { echo "request failed (curl exit $?)" >&2; return 1; }
    code="${resp##*$'\n'}"
    body_out="${resp%$'\n'*}"
    if [ "$code" != "200" ]; then
        errmsg="$(printf '%s' "$body_out" | jq -r '.error.message // empty' 2>/dev/null || true)"
        echo "API error $code${errmsg:+: $errmsg}" >&2
        return 1
    fi
    printf '%s' "$body_out" | jq -r '.choices[0].message.content // ""'
}

# ── Terminal markdown rendering (tty-only; raw bytes otherwise) ──
# On a tty: one blank line separates the answer from the prompt line above,
# then fenced code indents+dims, `code`→yellow, **bold**/__bold__→bold,
# #-headers (1-4)→bold cyan with #'s stripped, --- rules→thin rule, list
# markers kept. glow(1) is used opportunistically when installed. The answer
# ends with exactly one trailing newline.
# stdout not a tty (pipes/scripts/Telegram bridges) → RAW markdown unchanged
# (nothing added: single trailing newline only).
render_markdown() {
    local text="$1"
    if [ ! -t 1 ]; then
        printf '%s\n' "$text"
        return 0
    fi
    local rendered prog
    # The awk program lives in a variable: its backtick regexes would be
    # parsed as command substitution inside $( … ). $() strips any
    # renderer-added trailing newlines; the final printf re-adds exactly
    # one, plus the leading blank separator line.
    prog='
        BEGIN {
            e = sprintf("%c", 27)
            R = e "[0m"; DIM = e "[2m"; B = e "[1m"
            YEL = e "[33m"; CYA = e "[1;36m"
            RULE = ""
            for (i = 0; i < 60; i++) RULE = RULE "─"
            RULE = DIM RULE R
        }
        /^```/ { fence = !fence; next }
        fence  { printf "%s    %s%s\n", DIM, $0, R; next }
        /^#{1,4} / {
            sub(/^#{1,4} +/, "")
            printf "%s%s%s\n", CYA, $0, R
            next
        }
        /^(-{3,}|\*{3,}|_{3,})$/ { print RULE; next }
        {
            line = $0
            out = ""; rest = line
            while (match(rest, /`[^`]*`/)) {   # inline code first: keeps ** literal in backticks
                out = out substr(rest, 1, RSTART - 1) YEL \
                      substr(rest, RSTART + 1, RLENGTH - 2) R
                rest = substr(rest, RSTART + RLENGTH)
            }
            line = out rest
            out = ""; rest = line
            while (match(rest, /\*\*[^*]+\*\*/)) {
                out = out substr(rest, 1, RSTART - 1) B \
                      substr(rest, RSTART + 2, RLENGTH - 4) R
                rest = substr(rest, RSTART + RLENGTH)
            }
            line = out rest
            out = ""; rest = line
            while (match(rest, /__[^_]+__/)) {
                out = out substr(rest, 1, RSTART - 1) B \
                      substr(rest, RSTART + 2, RLENGTH - 4) R
                rest = substr(rest, RSTART + RLENGTH)
            }
            print out rest
        }
    '
    if command -v glow >/dev/null 2>&1; then
        rendered="$(printf '%s\n' "$text" | glow -)"
    else
        rendered="$(printf '%s\n' "$text" | awk "$prog")"
    fi
    printf '\n%s\n' "$rendered"
}

# ── Machine context appended to the built-in default prompt ─────
# One compact clause so default answers fit the actual box. Collected
# best-effort: hostnamectl(1) first (single call), then /etc/os-release +
# uname(1) fill any gaps. Every source is optional and failures are
# ignored — with nothing resolvable the clause is omitted entirely.

mc_clean() {
    # stdin→stdout: strip ANSI color sequences, drop control chars (emoji,
    # CR…), collapse all whitespace runs to single spaces, trim both ends.
    # (tr handles the control ranges: sed lacks \xHH inside [brackets].)
    sed -e 's/\x1b\[[0-9;]*[A-Za-z]//g' \
        -e 's/[[:space:]][[:space:]]*/ /g' \
        | tr -d '\000-\010\013-\037\177' \
        | sed -e 's/^ //; s/ $//'
}

machine_context() {
    local raw line key val h="" o="" k="" a="" part out=""
    # Preferred single source: one hostnamectl status call.
    if command -v hostnamectl >/dev/null 2>&1; then
        raw="$(hostnamectl status 2>/dev/null || true)"
        while IFS= read -r line; do
            key="$(printf '%s' "${line%%:*}" | tr -d '[:space:]')"
            val="${line#*:}"
            case "$key" in
                Statichostname|Transienthostname|Hostname)
                    [ -z "$h" ] && h="$val" ;;
                OperatingSystem)
                    [ -z "$o" ] && o="$val" ;;
                Kernel)
                    [ -z "$k" ] && k="$val" ;;
                Architecture)
                    [ -z "$a" ] && a="$val" ;;
            esac
        done <<< "$raw"
    fi
    # Fallback/complement for the distro: os-release (PRETTY_NAME, else
    # NAME + VERSION_ID). Sourced in a subshell — its vars stay local.
    if [ -z "$o" ] && [ -r "$OS_RELEASE_FILE" ]; then
        o="$(
            . "$OS_RELEASE_FILE" 2>/dev/null || true
            if [ -n "${PRETTY_NAME:-}" ]; then
                printf '%s' "$PRETTY_NAME"
            elif [ -n "${NAME:-}" ]; then
                printf '%s' "${NAME}${VERSION_ID:+ (${VERSION_ID})}"
            fi
        )"
    fi
    # Fallback/complement for kernel + arch: uname(1).
    [ -n "$k" ] || k="$(uname -sr 2>/dev/null || true)"
    [ -n "$a" ] || a="$(uname -m 2>/dev/null || true)"
    h="$(printf '%s' "$h" | mc_clean)"
    o="$(printf '%s' "$o" | mc_clean)"
    k="$(printf '%s' "$k" | mc_clean)"
    a="$(printf '%s' "$a" | mc_clean)"
    # Normalize to the bare release: both hostnamectl and uname -sr report
    # "Linux <rel>".
    case "$k" in "Linux "*) k="${k#Linux }" ;; esac
    local out=""
    for part in "$h" "$o" "${k:+kernel $k}" "$a"; do
        [ -n "$part" ] || continue
        if [ -n "$out" ]; then out="$out, $part"; else out="$part"; fi
    done
    [ -n "$out" ] || return 0
    printf 'Machine context (answers must fit this box): %s.' "$out"
}

cmd_capture() {
    [ $# -gt 0 ] || err "usage: pos ai openrouter capture <command> [args...]"
    mkdir -p "$(dirname "$LAST_CMD_OUTPUT_FILE")"
    "$@" 2>&1 | tee "$LAST_CMD_OUTPUT_FILE"
    local rc=${PIPESTATUS[0]}
    printf '[captured → %s]\n' "$LAST_CMD_OUTPUT_FILE" >&2
    return $rc
}

cmd_ask() {
    local prompt="" messages out system ctx mc
    if [ $# -gt 0 ]; then
        prompt="$*"
    elif [ ! -t 0 ]; then
        prompt="$(cat)"
    fi
    [ -n "$prompt" ] || err "No prompt given — usage: pos ai openrouter ask \"<prompt>\""
    # --last: append the most recent logged pos command output AFTER the
    # question, so the model diagnoses the real failure.
    if [ "$LAST_MODE" -eq 1 ]; then
        local log_file=""
        # Priority 1: most recent pos dispatcher log
        log_file="$(newest_pos_log 2>/dev/null)" || true
        # Priority 2: captured output from any command
        if [ -z "$log_file" ] && [ -s "$LAST_CMD_OUTPUT_FILE" ]; then
            log_file="$LAST_CMD_OUTPUT_FILE"
        fi
        [ -n "$log_file" ] || err "No recent output found — run 'pos ai openrouter capture <cmd>' first, or pipe: cmd 2>&1 | pos ai openrouter ask \"what happened\""
        last_log_annotate "$log_file"
        ctx="$(last_log_context "$log_file")"
        prompt="$prompt"$'\n\n[last command output:]\n'"$ctx"
    fi
    require_key
    # Terse by default: user --system replaces the built-in prompt wholesale;
    # --full skips everything (built-in text AND machine context). The
    # default prompt carries a machine-context clause so answers fit this
    # box; with no detectable facts it is omitted.
    system="$SYSTEM_PROMPT"
    if [ -z "$system" ] && [ "$FULL_MODE" -eq 0 ]; then
        mc="$(machine_context)"
        [ -n "$mc" ] && mc=" $mc"
        system="$DEFAULT_SYSTEM_PROMPT$mc"
    fi
    # Persistent session memory ('default' unless --session).
    messages="$(session_load)"
    messages="$(session_push "$messages" user "$prompt")"
    if ! out="$(openrouter_generate "$(resolve_model)" "$messages" "$system" 2>&1)"; then
        err "$out"
    fi
    messages="$(session_push "$messages" assistant "$out")"
    session_save "$messages"
    render_markdown "$out"
}

cmd_chat() {
    [ $# -eq 0 ] || err "Unexpected argument for chat: $*"
    local model messages text answer
    model="$(resolve_model)"
    require_key
    messages="$(session_load)"
    printf 'session: %s (resumed %s prior turns)\n' "$SESSION" "$(printf '%s' "$messages" | jq -r '.messages | length')"
    trap 'echo; echo "bye"; exit 0' INT
    echo "OpenRouter · ${model} — type a message; q=quit, /reset=clear history"
    while true; do
        printf '> '
        IFS= read -r text || break
        case "$text" in
            "" ) continue ;;
            q|Q|quit|exit) echo; echo "bye"; return 0 ;;
            /reset)
                messages='{"messages":[]}'
                session_save "$messages"
                echo "[history cleared]"
                continue ;;
        esac
        messages="$(session_push "$messages" user "$text")"
        if ! answer="$(openrouter_generate "$model" "$messages" "$SYSTEM_PROMPT" 2>&1)"; then
            warn "AI error: $answer"
            continue
        fi
        messages="$(session_push "$messages" assistant "$answer")"
        session_save "$messages"
        # The bare '> ' prompt has no newline: this one closes the prompt
        # line, and render_markdown's tty-only blank line then shows as the
        # visible gap before the answer. On a pipe both bytes are preserved.
        printf '\n'
        render_markdown "$answer"
        printf '\n\n'
    done
    echo
    return 0
}

cmd_sessions() {
    local action="${1:-list}" name f n
    case "$action" in
        list|"")
            [ -d "$SESSION_DIR" ] || { echo "no sessions"; return 0; }
            local found=0
            for f in "$SESSION_DIR"/*.json; do
                [ -f "$f" ] || continue
                found=1
                n="$(jq -r '.messages | length' "$f" 2>/dev/null || echo 0)"
                printf '  %-32s %s turns\n' "$(basename "$f" .json)" "${n:-0}"
            done
            [ "$found" -eq 1 ] || echo "no sessions"
            ;;
        reset)
            [ $# -ge 2 ] || err "usage: pos ai openrouter sessions reset <name>"
            name="$2"
            if rm -f "$(session_file "$name")"; then
                ok "session '$name' cleared"
            fi
            ;;
        *) err "Unknown sessions subcommand '$action' (list | reset <name>)" ;;
    esac
}

cmd_models() {
    [ $# -eq 0 ] || err "Unexpected argument for models: $*"
    local model resp code body m
    model="$(resolve_model)"
    require_key
    resp="$(curl -sS -m 30 "${API}/models" \
        -H "Authorization: Bearer ${OPENROUTER_API_KEY}" \
        --write-out $'\n%{http_code}')" || err "request failed (curl exit $?)"
    code="${resp##*$'\n'}"
    body="${resp%$'\n'*}"
    if [ "$code" != "200" ]; then
        err "API error $code: $(printf '%s' "$body" | jq -r '.error.message // empty')"
    fi
    local list
    list="$(printf '%s' "$body" | jq -r '.data[]?.id' | sort)"
    echo "OpenRouter models:"
    while IFS= read -r m; do
        [ -n "$m" ] || continue
        if [ "$m" = "$model" ]; then
            printf '  %-48s <- default\n' "$m"
        else
            printf '  %-48s\n' "$m"
        fi
    done <<< "$list"
    if ! grep -qxF "$model" <<< "$list"; then
        warn "configured default '$model' is not in the list — set OPENROUTER_MODEL"
    fi
}

# ── Parse flags + subcommand ────────────────────────────────────
MODEL_OVERRIDE=""
FULL_MODE=0
LAST_MODE=0
cmd=""
args=()
while [ $# -gt 0 ]; do
    case "$1" in
        -h|--help) usage ;;
        --model)
            [ $# -ge 2 ] || err "--model needs a value"
            MODEL_OVERRIDE="$2"; shift 2 ;;
        --session)
            [ $# -ge 2 ] || err "--session needs a value"
            SESSION="$2"; shift 2 ;;
        --system)
            [ $# -ge 2 ] || err "--system needs a value"
            SYSTEM_PROMPT="$2"; shift 2 ;;
        --full)
            FULL_MODE=1; shift ;;
        --last)
            LAST_MODE=1; shift ;;
        -*) err "Unknown option '$1' (see --help)" ;;
        *)
            if [ -z "$cmd" ]; then
                cmd="$1"
            else
                args+=("$1")
            fi
            shift ;;
    esac
done

if [ "$LAST_MODE" -eq 1 ] && [ "${cmd:-}" != "ask" ]; then
    err "--last only applies to 'ask' — for capturing output use 'capture': pos ai openrouter capture <cmd>"
fi

case "${cmd:-}" in
    "")        usage ;;
    ask)       cmd_ask "${args[@]}" ;;
    capture)   cmd_capture "${args[@]}" ;;
    chat)      cmd_chat "${args[@]}" ;;
    models)    cmd_models "${args[@]}" ;;
    sessions)  cmd_sessions "${args[@]}" ;;
    *)         err "Unknown ai openrouter subcommand '$cmd' (see --help)" ;;
esac
