8126db3203
CI & Build / Python lint (push) Successful in 2s
CI & Build / integration (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 21s
CI & Build / Python tests (push) Successful in 43s
CI & Build / Build & push image (push) Successful in 55s
New UserPromptSubmit hook (scribe_autoinject.sh) + GET /api/plugin/retrieve that surface the TITLES (never bodies) of the few notes clearing four anti-bloat gates: a per-user confidence threshold (stricter than pull search), a margin gate, per-session dedup (exclude_ids), and a top-k ceiling. Each retrieval is logged to retrieval_logs as source=auto_inject so the threshold can be tuned from data. Per-user config (enable / threshold / top-k) is DB-backed via /api/settings with a Settings UI card; defaults enabled, threshold 0.55, top-k 3 (conservative — tune once auto_inject telemetry accrues). Scribe: project 2, milestone 93, task 1033. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xz4j1H7pjYSjKsEpgcNH5E
87 lines
3.7 KiB
Bash
Executable File
87 lines
3.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Scribe plugin — UserPromptSubmit push channel (knowledge auto-inject, Path A).
|
|
#
|
|
# On each user prompt, asks the operator's Scribe instance for a TITLE-FIRST
|
|
# awareness hint: the few notes that clear the per-user auto-inject gates
|
|
# (high-confidence threshold, margin gate, session dedup, top-k). Titles + ids
|
|
# only — never bodies; the agent calls get_note(id) to pull anything it judges
|
|
# relevant. Most turns inject nothing.
|
|
#
|
|
# Best-effort enrichment ONLY: unlike the SessionStart channel there is no
|
|
# static floor here. If the instance is unconfigured/unreachable, or anything
|
|
# fails, the hook stays SILENT and exits 0 — it must never block a prompt.
|
|
#
|
|
# Config (same as scribe_session_context.sh), exported to the hook by Claude Code:
|
|
# CLAUDE_PLUGIN_OPTION_api_endpoint base URL, no trailing slash
|
|
# CLAUDE_PLUGIN_OPTION_api_token fmcp_ API key (sensitive)
|
|
# SCRIBE_URL / SCRIBE_TOKEN override for the settings.json dogfooding path.
|
|
#
|
|
# Session dedup: each surfaced note id is remembered in a per-session file so a
|
|
# note is injected at most once per session. Passed back as exclude_ids.
|
|
set -uo pipefail
|
|
|
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
command -v curl >/dev/null 2>&1 || exit 0
|
|
|
|
# UserPromptSubmit delivers a JSON event on stdin: { prompt, session_id, cwd, ... }
|
|
event=$(cat 2>/dev/null || true)
|
|
prompt=$(printf '%s' "$event" | jq -r '.prompt // empty' 2>/dev/null) || prompt=""
|
|
session_id=$(printf '%s' "$event" | jq -r '.session_id // empty' 2>/dev/null) || session_id=""
|
|
event_cwd=$(printf '%s' "$event" | jq -r '.cwd // empty' 2>/dev/null) || event_cwd=""
|
|
|
|
# Nothing to retrieve against.
|
|
[ -n "$prompt" ] || exit 0
|
|
|
|
url=${SCRIBE_URL:-${CLAUDE_PLUGIN_OPTION_api_endpoint:-}}
|
|
token=${SCRIBE_TOKEN:-${CLAUDE_PLUGIN_OPTION_api_token:-}}
|
|
# Guard against an unexpanded ${...} placeholder arriving as a literal.
|
|
case "$url" in *'${'*) url="" ;; esac
|
|
case "$token" in *'${'*) token="" ;; esac
|
|
# Unconfigured install → silent (auto-inject is pure enrichment).
|
|
[ -n "$url" ] && [ -n "$token" ] || exit 0
|
|
|
|
# Cap the query length — a giant prompt makes a giant URL for no extra signal.
|
|
q=$(printf '%s' "$prompt" | cut -c1-2000)
|
|
q_enc=$(printf '%s' "$q" | jq -rR '@uri' 2>/dev/null) || exit 0
|
|
|
|
# Resolve the working repo's remote so the server can scope to the bound project.
|
|
repo_dir=${event_cwd:-${CLAUDE_PROJECT_DIR:-$PWD}}
|
|
repo=$(git -C "$repo_dir" remote get-url origin 2>/dev/null || true)
|
|
repo_q=""
|
|
if [ -n "$repo" ]; then
|
|
enc=$(printf '%s' "$repo" | jq -rR '@uri' 2>/dev/null) || enc=""
|
|
[ -n "$enc" ] && repo_q="&repo=${enc}"
|
|
fi
|
|
|
|
# Per-session dedup: ids already injected this session are skipped.
|
|
state_dir="${TMPDIR:-/tmp}/scribe-autoinject"
|
|
mkdir -p "$state_dir" 2>/dev/null || true
|
|
idfile=""
|
|
exclude_q=""
|
|
if [ -n "$session_id" ]; then
|
|
# session_id is an opaque token from Claude Code; keep only filename-safe chars.
|
|
safe_sid=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
|
idfile="$state_dir/${safe_sid}.ids"
|
|
if [ -f "$idfile" ]; then
|
|
seen=$(tr '\n' ',' < "$idfile" 2>/dev/null | sed 's/,$//')
|
|
[ -n "$seen" ] && exclude_q="&exclude_ids=${seen}"
|
|
fi
|
|
fi
|
|
|
|
body=$(curl -fsS --max-time 5 \
|
|
-H "Authorization: Bearer ${token}" \
|
|
"${url%/}/api/plugin/retrieve?q=${q_enc}${repo_q}${exclude_q}" 2>/dev/null) || exit 0
|
|
[ -n "$body" ] || exit 0
|
|
|
|
context=$(printf '%s' "$body" | jq -r '.context // empty' 2>/dev/null) || exit 0
|
|
[ -n "$context" ] || exit 0
|
|
|
|
# Remember the surfaced ids so they aren't injected again this session.
|
|
if [ -n "$idfile" ]; then
|
|
printf '%s' "$body" | jq -r '.note_ids[]? // empty' 2>/dev/null >> "$idfile" || true
|
|
fi
|
|
|
|
jq -n --arg c "$context" \
|
|
'{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: $c}}'
|
|
exit 0
|