#!/usr/bin/env bash
# Scribe plugin — SessionStart push channel (two tiers + compaction re-grounding).
#
# Tier 1 (STATIC, always fires, no auth, no network): injects a bundled
# behavioral mandate (scribe_static_context.md) so a fresh session knows to
# reach for Scribe — record work, recall before acting — even when the instance
# is unreachable or unconfigured. The static tier is the load-bearing floor that
# does not depend on the key or the network.
#
# Tier 2 (DYNAMIC, best-effort enrichment): curls the operator's Scribe instance
# for always-on rules + active-project context and appends it. Config comes from
# the plugin's userConfig, exported to hooks as:
#   CLAUDE_PLUGIN_OPTION_API_ENDPOINT  base URL, no trailing slash
#   CLAUDE_PLUGIN_OPTION_API_TOKEN     fmcp_ API key (sensitive)
# NOTE THE CASE: Claude Code uppercases the userConfig key when exporting it, so
# the `api_token` option arrives as CLAUDE_PLUGIN_OPTION_API_TOKEN. Reading the
# lowercase spelling silently yields nothing — that was issue #2198, and it
# disabled the dynamic tier, auto-inject, and the write-path trigger at once.
# The active project is resolved server-side from the working repo's git remote
# (see services/repo_bindings); bind each repo once with the bind_repo MCP tool.
#
# COMPACTION RE-GROUNDING: this hook is registered matcher-less, so it ALSO
# fires after a compaction (SessionStart input `source` == "compact"), when
# earlier turns have just been summarized and in-flight state is most at risk.
# On that source we lead with a banner telling the model to reload project +
# in-flight tasks from Scribe. (PreCompact is the wrong tool here — a host hook
# can't make the model flush, and can't know the in-flight task ids; the durable
# path is record-as-you-go + this post-compaction reload.)
#
# IMPORTANT: do NOT pass config via `${user_config.*}` substitution in a
# shell-form hooks.json command — Claude Code rejects that outright (splicing a
# configured value into a shell command line would let the shell run whatever it
# contains). The env vars above are the supported channel; SCRIBE_URL /
# SCRIBE_TOKEN override for the settings.json dogfooding path.
#
# FAIL-OPEN, BUT NOT SILENT: the dynamic tier never blocks a session, and every
# way it can come up empty produces a short status line — failed fetch, missing
# token, and missing-everything alike. Nothing about the credential path is
# allowed to fail quietly; see the #2198 comment at the status block below.
set -uo pipefail

command -v jq >/dev/null 2>&1 || exit 0   # needed to emit the JSON envelope safely

# `CDPATH= cd` is deliberate, not a typo'd assignment: it runs this one `cd`
# with CDPATH empty, so an operator whose CDPATH happens to contain a matching
# directory name can't send us somewhere else — and `cd` won't echo the resolved
# path into our output. shellcheck can't tell that idiom from `CDPATH=cd`.
# shellcheck disable=SC1007
here=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) || exit 0

# SessionStart delivers a JSON event on stdin; `source` is startup|resume|compact|clear.
event=$(cat 2>/dev/null || true)
source=$(printf '%s' "$event" | jq -r '.source // empty' 2>/dev/null) || source=""

out=""
# Append $1 to $out, separated by a horizontal rule when $out already has content.
append() { if [ -n "$out" ]; then out="${out}"$'\n\n---\n\n'"$1"; else out="$1"; fi; }
# Prepend $1 above $out (used for the compaction banner so it's seen first).
prepend() { if [ -n "$out" ]; then out="$1"$'\n\n---\n\n'"${out}"; else out="$1"; fi; }

# --- Tier 1: static behavioral mandate (always, keyless, networkless) ---
[ -f "$here/scribe_static_context.md" ] && out=$(cat "$here/scribe_static_context.md")

# --- Which version is actually RUNNING (keyless, networkless) ---
#
# An install has two halves and only one self-updates:
#
#   marketplaces/…/scribe-plugin/    git clone — pulls on its own
#   cache/…/scribe/<version>/        what EXECUTES — refreshed only when the
#                                    manifest version changes
#
# So inspecting the clone shows a fix present while the broken copy keeps
# running, and the obvious debugging move actively misleads (#2209). Twice, the
# only detector was the operator saying "I don't think it updated".
#
# Naming the running version in every session makes that answerable from the
# transcript instead of by archaeology in the cache directory. Deliberately NOT
# a server round-trip or a stored per-user record: the state most needing
# diagnosis is the one where credentials never arrive, and this line still
# appears there.
manifest="$here/../.claude-plugin/plugin.json"
if [ -f "$manifest" ]; then
  plugin_version=$(jq -r '.version // empty' "$manifest" 2>/dev/null) || plugin_version=""
  if [ -n "$plugin_version" ]; then
    append "> Scribe plugin **v${plugin_version}** is executing in this session. A fix merged after this version has not reached it — the marketplace clone updates on its own, but the cache that runs only refreshes when the manifest version changes."
  fi
fi

# --- Tier 2: dynamic rules + active-project context (best-effort) ---
url=${SCRIBE_URL:-${CLAUDE_PLUGIN_OPTION_API_ENDPOINT:-}}
token=${SCRIBE_TOKEN:-${CLAUDE_PLUGIN_OPTION_API_TOKEN:-}}

# Guard against an unexpanded `${...}` placeholder reaching us as a literal — it
# would otherwise be sent as a garbage Bearer token and 401. Treat as unset.
case "$url"   in *'${'*) url="" ;;  esac
case "$token" in *'${'*) token="" ;; esac

dyn=""
status=""
if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then
  # Resolve the working repo's remote so the server can map it to a project.
  repo_dir=${CLAUDE_PROJECT_DIR:-$PWD}
  repo=$(git -C "$repo_dir" remote get-url origin 2>/dev/null || true)
  q=""
  if [ -n "$repo" ]; then
    enc=$(printf '%s' "$repo" | jq -sRr '@uri' 2>/dev/null) || enc=""
    [ -n "$enc" ] && q="?repo=${enc}"
  fi
  body=$(curl -fsS --max-time 8 \
    -H "Authorization: Bearer ${token}" \
    "${url%/}/api/plugin/context${q}" 2>/dev/null) || body=""
  [ -n "$body" ] && dyn=$(printf '%s' "$body" | jq -r '.context // empty' 2>/dev/null)
  [ -z "$dyn" ] && status="> ⚠️ Scribe: live rules/project context could not be loaded this session (instance unreachable or request failed). The standing guidance above still applies — pull rules with \`list_always_on_rules()\` and project context with \`enter_project()\` as needed."
elif [ -n "$url" ] && [ -z "$token" ]; then
  status="> ⚠️ Scribe: live context disabled this session — the API key is not configured (Scribe base URL is). Set it with \`/plugin\` → Scribe → configure, or export SCRIBE_TOKEN. Tools still work; pull rules with \`list_always_on_rules()\` and project context with \`enter_project()\`."
elif [ -z "$url" ] && [ -z "$token" ]; then
  # NEITHER value arrived. Previously this case stayed silent as "an unconfigured
  # install", which made issue #2198 invisible for weeks: a *casing* bug here
  # (reading CLAUDE_PLUGIN_OPTION_api_token when Claude Code exports the key
  # UPPERCASED) looks identical to never having configured the plugin, and
  # silently disabled auto-inject and the write-path trigger too. It is not a
  # benign state — the plugin prompts for both values at enable time, so if
  # neither reached the hook, something is wrong. Say so.
  status="> ⚠️ Scribe: live context disabled this session — neither the Scribe base URL nor the API key reached this hook. Configure the plugin (\`/plugin\` → Scribe), or export SCRIBE_URL + SCRIBE_TOKEN. Note this also disables prompt auto-inject and the write-path prior-art trigger. Tools still work; pull rules with \`list_always_on_rules()\` and project context with \`enter_project()\`."
fi

[ -n "$dyn" ]    && append "$dyn"
[ -n "$status" ] && append "$status"

# Compaction re-grounding: lead with a reload banner when this fire is a compact.
if [ "$source" = "compact" ]; then
  prepend "> ⟳ This session was just COMPACTED — earlier turns are now a summary, so in-flight detail may be lost. Before continuing, reload your bearings from Scribe: re-pull the operator's binding rules with \`list_always_on_rules()\` (a compaction can summarize them out of context, leaving only generic harness defaults in their place), re-run \`enter_project()\` for the active project, check its open tasks and recent notes, and reconcile what you're mid-way through against what Scribe records. Don't trust half-remembered state — Scribe is the record."
fi

# Nothing at all to inject → stay silent.
[ -n "$out" ] || exit 0

jq -n --arg c "$out" \
  '{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $c}}'
exit 0
