|
|
|
@@ -1,32 +1,41 @@
|
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# Scribe plugin — SessionStart push channel.
|
|
|
|
|
# Scribe plugin — SessionStart push channel (two tiers).
|
|
|
|
|
#
|
|
|
|
|
# Curls the operator's Scribe instance for always-on rules + active-project
|
|
|
|
|
# context and emits it as SessionStart `additionalContext`. Config comes from
|
|
|
|
|
# the plugin's userConfig, which Claude Code exports to hook subprocesses as
|
|
|
|
|
# CLAUDE_PLUGIN_OPTION_<key> env vars:
|
|
|
|
|
# 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 the API token never reached this hook. The latter is a known
|
|
|
|
|
# Claude Code gap: sensitive userConfig values aren't always exported to the
|
|
|
|
|
# hook subprocess, so the dynamic tier can silently get nothing. 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)
|
|
|
|
|
#
|
|
|
|
|
# The active project is NOT configured here — it's resolved server-side from
|
|
|
|
|
# the working repo's git remote (see services/repo_bindings). This keeps a
|
|
|
|
|
# single install working across many repos/projects: bind each repo once with
|
|
|
|
|
# the `bind_repo` MCP tool. An unbound repo just yields standing rules + a hint.
|
|
|
|
|
# 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.
|
|
|
|
|
#
|
|
|
|
|
# IMPORTANT: do NOT pass config via `${user_config.*}` substitution in
|
|
|
|
|
# hooks.json — sensitive userConfig values (api_token) are kept in the keychain
|
|
|
|
|
# and are never spliced into a hook command line, so the placeholder arrives
|
|
|
|
|
# unexpanded. The harness env vars above are the supported channel. SCRIBE_URL
|
|
|
|
|
# / SCRIBE_TOKEN still override, for the settings.json dogfooding path where the
|
|
|
|
|
# hook is wired up by hand.
|
|
|
|
|
# hooks.json — sensitive values are kept in the keychain and never spliced into
|
|
|
|
|
# a hook command line, so the placeholder arrives unexpanded. The harness env
|
|
|
|
|
# vars above are the supported channel; SCRIBE_URL / SCRIBE_TOKEN override for
|
|
|
|
|
# the settings.json dogfooding path.
|
|
|
|
|
#
|
|
|
|
|
# FAIL-OPEN: any missing tool/config, network error, or unreachable instance
|
|
|
|
|
# injects nothing and exits 0. A session must never be blocked by Scribe.
|
|
|
|
|
# FAIL-OPEN: the dynamic tier injects nothing on any error; the static tier
|
|
|
|
|
# still fires. A session is never blocked by Scribe.
|
|
|
|
|
set -uo pipefail
|
|
|
|
|
|
|
|
|
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
|
|
|
command -v curl >/dev/null 2>&1 || exit 0
|
|
|
|
|
command -v jq >/dev/null 2>&1 || exit 0 # needed to emit the JSON envelope safely
|
|
|
|
|
|
|
|
|
|
here=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) || exit 0
|
|
|
|
|
|
|
|
|
|
# --- Tier 1: static behavioral mandate (always, keyless, networkless) ---
|
|
|
|
|
out=""
|
|
|
|
|
[ -f "$here/scribe_static_context.md" ] && out=$(cat "$here/scribe_static_context.md")
|
|
|
|
|
|
|
|
|
|
# --- 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:-}}
|
|
|
|
|
|
|
|
|
@@ -35,27 +44,33 @@ token=${SCRIBE_TOKEN:-${CLAUDE_PLUGIN_OPTION_api_token:-}}
|
|
|
|
|
case "$url" in *'${'*) url="" ;; esac
|
|
|
|
|
case "$token" in *'${'*) token="" ;; esac
|
|
|
|
|
|
|
|
|
|
[ -n "$url" ] && [ -n "$token" ] || exit 0
|
|
|
|
|
|
|
|
|
|
# Resolve the working repo's remote so the server can map it to a project. Use
|
|
|
|
|
# the session's project dir when provided, else the current dir. No remote (or
|
|
|
|
|
# not a git repo) → omit; the server returns standing rules only.
|
|
|
|
|
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 -rR '@uri' 2>/dev/null) || enc=""
|
|
|
|
|
[ -n "$enc" ] && q="?repo=${enc}"
|
|
|
|
|
if command -v curl >/dev/null 2>&1 && [ -n "$url" ] && [ -n "$token" ]; 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 -rR '@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=""
|
|
|
|
|
if [ -n "$body" ]; then
|
|
|
|
|
dyn=$(printf '%s' "$body" | jq -r '.context // empty' 2>/dev/null) || dyn=""
|
|
|
|
|
if [ -n "$dyn" ]; then
|
|
|
|
|
if [ -n "$out" ]; then
|
|
|
|
|
out="${out}"$'\n\n---\n\n'"${dyn}"
|
|
|
|
|
else
|
|
|
|
|
out="$dyn"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
body=$(curl -fsS --max-time 8 \
|
|
|
|
|
-H "Authorization: Bearer ${token}" \
|
|
|
|
|
"${url%/}/api/plugin/context${q}" 2>/dev/null) || exit 0
|
|
|
|
|
# Nothing to inject (no static file AND no dynamic context) → stay silent.
|
|
|
|
|
[ -n "$out" ] || exit 0
|
|
|
|
|
|
|
|
|
|
text=$(printf '%s' "$body" | jq -r '.context // empty' 2>/dev/null) || exit 0
|
|
|
|
|
[ -n "$text" ] || exit 0
|
|
|
|
|
|
|
|
|
|
jq -n --arg c "$text" \
|
|
|
|
|
jq -n --arg c "$out" \
|
|
|
|
|
'{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $c}}'
|
|
|
|
|
exit 0
|
|
|
|
|