#!/usr/bin/env bash
# TaskPeace MCP installer — one-shot setup for Claude Code / Cursor.
#
# Usage:
#   curl -fsSL https://taskpeace.com/install.sh | bash        (prompts for your token)
#   PROMPTPRIO_API_TOKEN=pp_xxx bash <(curl -fsSL https://taskpeace.com/install.sh)
#
# What it does:
#   1. Verifies your token against /api/auth/me
#   2. Downloads the bundled MCP server to ~/.local/share/promptprio/mcp-server.js
#   3. Registers a `promptprio` server with Claude Code — via the `claude` CLI
#      (canonical, writes ~/.claude.json) or by merging ~/.claude.json directly.
#      Also configures Cursor (~/.cursor/mcp.json) when present.
#   4. Installs the one-key resume: a marked block in ~/.claude/CLAUDE.md so a bare `q`
#      resumes a stopped autopilot run in EVERY session (opt out: TASKPRIO_NO_RESUME_KEY=1).
#
# Re-run safely — idempotent. Updates the server binary + refreshes the config entry + resume block.

set -euo pipefail

API="${PROMPTPRIO_API_URL:-https://taskprio.com}"
SITE="https://taskpeace.com"          # human-facing site (API base stays taskprio.com for installed agents)
TOKEN="${PROMPTPRIO_API_TOKEN:-${1:-}}"
CACHE_DIR="$HOME/.local/share/promptprio"
MCP_BIN="$CACHE_DIR/mcp-server.js"
CLAUDE_JSON="$HOME/.claude.json"     # Claude Code reads MCP servers from HERE (not ~/.claude/mcp.json)
CURSOR_JSON="$HOME/.cursor/mcp.json"

red() { printf '\033[31m%s\033[0m\n' "$*"; }
grn() { printf '\033[32m%s\033[0m\n' "$*"; }
dim() { printf '\033[2m%s\033[0m\n' "$*"; }

printf '\033[1mTaskPeace MCP installer\033[0m\n'
dim "  $SITE"
echo

# No token in env/arg? Ask for it interactively (works under `curl | bash` via /dev/tty).
if [ -z "$TOKEN" ] && [ -e /dev/tty ] && [ -r /dev/tty ]; then
  printf 'Paste your token (%s → sidebar foot → Copy token): ' "$SITE" > /dev/tty
  IFS= read -rs TOKEN < /dev/tty || TOKEN=""
  printf '\n' > /dev/tty
fi

if [ -z "$TOKEN" ]; then
  red "No token provided."
  echo "Get yours at $SITE → sidebar foot → Copy token, then run:"
  echo "  PROMPTPRIO_API_TOKEN=pp_xxx bash <(curl -fsSL $SITE/install.sh)"
  echo
  dim "Tip: prefix the command with a space to keep the token out of shell history."
  exit 1
fi

# 1. Verify token + identity
printf 'Verifying token... '
ME_JSON=$(curl -fsS -H "Authorization: Bearer $TOKEN" "$API/api/auth/me" 2>/dev/null || true)
WHO=$(printf '%s' "$ME_JSON" | python3 -c "import json,sys
try:
  d=json.load(sys.stdin); u=d.get('user') or {}
  print((u.get('email') or '')+'|'+(u.get('plan') or ''))
except Exception:
  print('|')" 2>/dev/null || echo '|')
EMAIL="${WHO%|*}"
PLAN="${WHO#*|}"
if [ -z "$EMAIL" ]; then
  red "FAILED"
  echo "Token rejected by $API/api/auth/me. Check it at $SITE → sidebar foot → Copy token."
  exit 1
fi
grn "ok — $EMAIL · $PLAN"

# 2. Download bundled server (atomic via tmp + mv)
printf 'Downloading server... '
mkdir -p "$CACHE_DIR"
curl -fsSL "$API/mcp-server.js" -o "$MCP_BIN.tmp"
mv "$MCP_BIN.tmp" "$MCP_BIN"
chmod +x "$MCP_BIN"
SIZE=$(wc -c < "$MCP_BIN" | tr -d ' ')
grn "ok — $((SIZE/1024)) KB at $MCP_BIN"

# 3. Register the server with the agent(s) on this machine.
#    IMPORTANT: Claude Code reads MCP servers from ~/.claude.json (managed by the
#    `claude` CLI) — NOT ~/.claude/mcp.json. Prefer the CLI (version-proof); fall
#    back to merging ~/.claude.json directly when the CLI isn't on PATH.
SERVER_JSON=$(python3 -c "import json,sys;print(json.dumps({'command':'node','args':[sys.argv[1]],'env':{'PROMPTPRIO_API_TOKEN':sys.argv[2],'PROMPTPRIO_API_URL':sys.argv[3]}}))" "$MCP_BIN" "$TOKEN" "$API")

# helper: merge the promptprio entry into a Claude/Cursor mcp.json-shaped file
merge_cfg() {
  python3 - "$1" "$SERVER_JSON" <<'PY'
import json, os, sys
path, server = sys.argv[1], json.loads(sys.argv[2])
d = os.path.dirname(path)
if d:
    os.makedirs(d, exist_ok=True)
cfg = {}
if os.path.exists(path):
    try:
        with open(path) as f:
            txt = f.read().strip()
        cfg = json.loads(txt) if txt else {}
    except json.JSONDecodeError:
        os.rename(path, path + '.bak')  # back up rather than clobber
        cfg = {}
cfg.setdefault('mcpServers', {})['promptprio'] = server
with open(path, 'w') as f:
    json.dump(cfg, f, indent=2)
    f.write('\n')
PY
}

printf 'Registering with Claude Code... '
CC_OK=""
if command -v claude >/dev/null 2>&1; then
  claude mcp remove promptprio --scope user >/dev/null 2>&1 || true
  if claude mcp add-json promptprio "$SERVER_JSON" --scope user >/dev/null 2>&1; then
    grn "ok — via claude CLI (user scope)"; CC_OK=1
  fi
fi
if [ -z "$CC_OK" ]; then
  merge_cfg "$CLAUDE_JSON"
  grn "ok — merged into ~/.claude.json"
fi

# Cursor: only if it's installed on this machine.
if [ -d "$HOME/.cursor" ]; then
  printf 'Registering with Cursor... '
  merge_cfg "$CURSOR_JSON"
  grn "ok — $CURSOR_JSON"
fi

# 4. One-key autopilot — teach the agent what `p` (alias `q`), `d` and `O` mean in EVERY session
#    (not only when a prompt is pasted), so a bare `p` starts or resumes a run cold (fresh
#    session or after compaction). It REFUSES to downgrade or shrink an existing block. The block is marked, idempotent, and written ATOMICALLY with a one-time
#    .bak into ~/.claude/CLAUDE.md (Claude Code) + ./AGENTS.md when present (cross-tool: Cursor /
#    Codex / Gemini). Controls: TASKPRIO_NO_RESUME_KEY=1 skip · TASKPRIO_RESUME_KEY=1 install
#    without asking. On a terminal it asks first (default yes). Remove the block to undo.

# idempotently upsert the resume-key block into $1 — atomic, version-agnostic, .bak once.
write_resume_block() {
  RB_TARGET="$1" python3 <<'PY'
import os, re, tempfile
path = os.environ["RB_TARGET"]
VERSION = 5
body = (
  "## TaskPeace — the one-key autopilot (`p`; `q` = alias when idle, LAND when flying) + `d` + `O`\n"
  "When my whole message is exactly `p` (or `P` / `q` / `Q` / `continue`) — case-insensitive, on "
  "its own — run the TaskPeace AUTOPILOT: START the loop if none is flying in this session, "
  "CONTINUE it if one is. START = re-orient first (recover any in-flight task and VERIFY it against "
  "reality before redoing anything, make the board true with the `promptprio` MCP tools, "
  "collision-check before editing a shared repo), then loop (get_next_task → do it → verify on the "
  "real surface → complete_task with proof → repeat), auto-scoped to this session's working "
  "directory. CONTINUE = keep flying; a `p` that arrives mid-leg is a no-op nudge, never a restart. "
  "Report ONE line per leg and name the next candidate. Canonical spec: "
  "https://taskprio.com/autopilot-prompt.txt (resuming a stopped run: /continue-prompt.txt) — fetch "
  "+ follow it if the loop isn\'t already in context. Lead your reply with "
  "`▶ p · autopilot — starting…` or `▶ p · autopilot — continuing…` so I see it landed. "
  "`p <text>` scopes the run (e.g. `p ship readstacks`). It never dead-ends: an empty queue becomes "
  "a gap sweep, a blocker becomes a 👤 needs-human card it flies past, a rate limit becomes a "
  "cheaper tier rather than a halt.\n"
  "`q` is CONTEXT-DEPENDENT: with NO run flying it is an exact alias of `p` (start/resume the "
  "loop). Pressed WHILE a run is flying it means LAND CLEANLY, in order, then stop: (1) FINISH "
  "the work in flight, never mid-edit, and START NO NEW LEG; (2) SHIP it as one chain — commit → "
  "push → MR/PR → merge → deploy live; (3) QA the LIVE artifact, never the exit code — "
  "fingerprint the served page, check 375px if it is UI; (4) STOP — queue true, summary, handoff "
  "note. Lead with `▶ q · landing — finishing, shipping, QA…` and close `✓ landed`. NEVER deploy "
  "from a checkout you cannot certify (fetch first; if it is behind or dirty with someone else's "
  "work, PARK the deploy, say so, and land anyway — never ship a revert); never skip the QA to "
  "land faster; a step that needs real money or a public post under my name becomes a 👤 card and "
  "the run lands regardless. `q <text>` while flying scopes the LANDING. Once landed, the next "
  "`p` or `q` starts a FRESH run. `stop` / `pause` / `halt` still end a run at the current leg.\n"
  "`d` (or `Next` / `DTBNS`) on its own = ONE LEG instead of the loop: the same engine and the same "
  "gates, exactly one verified increment, then STOP — I chain by sending `d` again "
  "(https://taskprio.com/one-step-prompt.txt). Lead with `▶ d · one leg…`.\n"
  "`O` (or `o`) on its own = OPERATOR WALKTHROUGH: don\'t work the queue — walk ME, one step at a "
  "time, through only the items that genuinely need me (the 👤 / needs_human cards), verifying and "
  "completing each before the next. Lead with "
  "`▶ O · Operator walkthrough — clearing what only you can do…`.\n"
  "Bare `p` / `q` / `d` / `O` fire ONLY when essentially the whole message (\"add a q param\" does "
  "NOT). If this session has no `promptprio` MCP tools / no TaskPeace queue, treat them literally.\n"
)
block = "<!-- taskprio:resume-key v%d -->\n" % VERSION + body + "<!-- /taskprio:resume-key -->"
try:
    with open(path, encoding="utf-8") as f: txt = f.read()
except FileNotFoundError:
    txt = ""
# match ANY prior version of the block, so re-installs upgrade it in place (never duplicate)
pat = re.compile(r"<!--\s*taskprio:resume-key\b.*?-->.*?<!--\s*/taskprio:resume-key\s*-->", re.S)
m = pat.search(txt)
if m:
    # NEVER CLOBBER A NEWER OR RICHER BLOCK. Measured 2026-09-04: this replacement is wholesale, so
    # on a file whose block had been EXTENDED (a v3 block carrying the O key + a longer q contract)
    # a re-install silently deleted 3,669 bytes, the O key included. Two refusals close that path.
    prior = m.group(0)
    vm = re.search(r"taskprio:resume-key\s+v(\d+)", prior)
    prior_v = int(vm.group(1)) if vm else 0
    force = os.environ.get("TASKPRIO_FORCE_RESUME_KEY") == "1"
    if not force and prior_v > VERSION:
        raise SystemExit(0)                      # a newer block is already installed
    if not force and len(prior) > len(block):
        raise SystemExit(0)                      # an extended/customised block — never shrink it
    new = pat.sub(lambda _m: block, txt)
else:
    if txt.strip() and not os.path.exists(path + ".bak"):
        try:
            with open(path + ".bak", "w", encoding="utf-8") as b: b.write(txt)
        except OSError:
            pass
    new = (txt.rstrip() + "\n\n" if txt.strip() else "") + block + "\n"
if new == txt:
    raise SystemExit(0)  # already current — nothing to write
# atomic: write a temp file in the same dir, then replace — never truncate the live file
d = os.path.dirname(path) or "."
fd, tmp = tempfile.mkstemp(dir=d, prefix=".taskprio-rk-")
try:
    with os.fdopen(fd, "w", encoding="utf-8") as f: f.write(new)
    os.replace(tmp, path)
except BaseException:
    try: os.unlink(tmp)
    except OSError: pass
    raise
PY
}

# decide whether to install (env wins; else ask on a terminal; else default on + announce)
RK_DO=yes
if [ "${TASKPRIO_NO_RESUME_KEY:-}" = "1" ]; then
  RK_DO=no
# `-r/-w /dev/tty` test PERMISSION BITS, which pass with no controlling terminal — the
# printf then dies "Device not configured" under set -e (measured 2026-09-05 from Claude
# Code's Bash: MCP registered fine, exit 1 before the resume block and the DONE steps).
# Open the tty for real, both directions; on failure fall through to the default (on).
elif [ "${TASKPRIO_RESUME_KEY:-}" != "1" ] && { : </dev/tty >/dev/tty; } 2>/dev/null; then
  printf 'Add one-key resume — type \033[1mq\033[0m to continue a stopped run in any session? [Y/n] ' > /dev/tty 2>/dev/null || true
  RK_ANS=""; read -r RK_ANS < /dev/tty 2>/dev/null || true
  case "$RK_ANS" in [Nn]*) RK_DO=no ;; esac
fi

if [ "$RK_DO" = no ]; then
  dim "One-key resume (q): skipped — enable later by re-running with TASKPRIO_RESUME_KEY=1."
else
  RK_DONE=""
  # Claude Code global memory — only when Claude Code is actually in use here.
  if command -v claude >/dev/null 2>&1 || [ -d "$HOME/.claude" ] || [ "${CC_OK:-}" = "1" ]; then
    printf 'Installing one-key resume (q)... '
    mkdir -p "$HOME/.claude"
    if write_resume_block "$HOME/.claude/CLAUDE.md"; then
      grn "ok — ~/.claude/CLAUDE.md (every Claude Code session now knows q)"; RK_DONE=1
    else
      red "skipped — couldn't write ~/.claude/CLAUDE.md"
    fi
  fi
  # Cross-tool — only into an AGENTS.md that already exists where you ran this.
  if [ -f "$PWD/AGENTS.md" ] && write_resume_block "$PWD/AGENTS.md"; then
    grn "ok — $PWD/AGENTS.md (cross-tool resume for this project)"; RK_DONE=1
  fi
  if [ -n "$RK_DONE" ]; then
    dim "  Undo: delete the 'taskprio:resume-key' block (a one-time .bak was kept), or set TASKPRIO_NO_RESUME_KEY=1."
  else
    dim "One-key resume (q): nothing written (no Claude Code memory or ./AGENTS.md found)."
  fi
fi

echo
grn "DONE."
echo "Restart Claude Code (or Cursor), then verify:  claude mcp list   (you should see 'promptprio')"
echo
echo "First steps in your agent:"
echo "  - Look around:         ask \"list my promptprio tasks\""
echo "  - Start the autopilot: paste  https://taskpeace.com/autopilot-prompt.txt  into your agent,"
echo "                         inside a project's repo — it pulls the top-priority task, works it,"
echo "                         verifies, and clears the queue top to bottom, autonomously."
echo "  - Resume anytime:      type  q  on its own — it picks a stopped run back up (re-orient + continue)."
echo
dim "Remove: claude mcp remove promptprio --scope user  (or delete the 'promptprio' entry from the config)."
