forked from bchanot/claude
lib/graphify-gate.sh counts tracked code files (graphify's AST extension set, vendored trees excluded) and, from 200 with no graphify-out/graph.json, prints one banner-sized line; session-start shows it with the /graphify hint. Nothing is built, installed or updated: the rule is the user's (BDR-097), grounded in the LRN-162 measurements (AST build 2.3 s, 0 tokens, a query 2 to 3k tokens). Doctrine section and plugin-advisor thresholds follow the same rule; graphify claude install stays rejected. Test: 11 checks. GRAPHIFY_MIN_CODE_FILES overrides the threshold.
257 lines
9.8 KiB
Bash
257 lines
9.8 KiB
Bash
#!/usr/bin/env bash
|
|
# ============================================================
|
|
# Claude Code — Session start plugin status
|
|
# Runs once per session. Filesystem only, except one quiet
|
|
# git fetch for the version/update check near the end.
|
|
# ============================================================
|
|
|
|
# ── Quick health check (filesystem only, no subprocesses) ──
|
|
BROKEN=()
|
|
for f in CLAUDE.md settings.json agents skills; do
|
|
[ ! -e "$HOME/.claude/$f" ] && BROKEN+=("$f")
|
|
done
|
|
|
|
if [ ${#BROKEN[@]} -gt 0 ]; then
|
|
# Try to find the repo path from an existing symlink
|
|
_repo_hint=""
|
|
for _probe in CLAUDE.md settings.json; do
|
|
if [ -L "$HOME/.claude/$_probe" ]; then
|
|
_repo_hint="$(cd "$(dirname "$(readlink "$HOME/.claude/$_probe")")" 2>/dev/null && pwd)"
|
|
break
|
|
fi
|
|
done
|
|
_fix_cmd="${_repo_hint:+cd $_repo_hint && }bash link.sh"
|
|
|
|
echo ""
|
|
echo "┌─ ⚠️ CONFIG ISSUES ────────────────────────────────┐"
|
|
for b in "${BROKEN[@]}"; do
|
|
printf "│ MISSING: ~/.claude/%-30s│\n" "$b"
|
|
done
|
|
printf "│ → %-47s│\n" "$_fix_cmd"
|
|
echo "│ → make doctor for full diagnostic │"
|
|
echo "└───────────────────────────────────────────────────┘"
|
|
unset _repo_hint _fix_cmd
|
|
fi
|
|
|
|
# ── Load shared detection library ──
|
|
_lib="$(dirname "${BASH_SOURCE[0]}")/../lib/detect-plugins.sh"
|
|
if [ -f "$_lib" ]; then
|
|
# shellcheck source=../lib/detect-plugins.sh disable=SC1091
|
|
source "$_lib"
|
|
else
|
|
echo "⚠️ lib/detect-plugins.sh not found — config broken, run: bash link.sh"
|
|
exit 0
|
|
fi
|
|
unset _lib
|
|
|
|
# ── gitflow hooks reconcile (BDR-095) ──
|
|
# A repo's .githooks/ lags lib/gitflow.sh until someone re-runs install-hook
|
|
# (LRN-114). Do it here, once per session, silently when current; the lib
|
|
# prints the refreshed names, shown in the banner with a commit reminder.
|
|
GF_REFRESHED=""
|
|
_gf_lib="$(dirname "${BASH_SOURCE[0]}")/../lib/gitflow.sh"
|
|
if [ -f "$_gf_lib" ] && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
GF_REFRESHED=$(bash "$_gf_lib" reconcile-hooks 2>/dev/null | sed -n 's/^gitflow hooks refreshed: *//p')
|
|
fi
|
|
unset _gf_lib
|
|
|
|
# ── graphify threshold signal (BDR-097) ──
|
|
# Informs, never acts: one banner line when the repo holds ≥ 200 tracked code
|
|
# files and no graph. The user decides whether to build one.
|
|
GRAPHIFY_HINT=""
|
|
_gg_lib="$(dirname "${BASH_SOURCE[0]}")/../lib/graphify-gate.sh"
|
|
if [ -f "$_gg_lib" ]; then GRAPHIFY_HINT=$(bash "$_gg_lib" "$PWD" 2>/dev/null); fi
|
|
unset _gg_lib
|
|
|
|
# ── Toggle plugin detection ──
|
|
|
|
TOGGLE_ACTIVE=()
|
|
TOGGLE_INACTIVE=()
|
|
|
|
for plugin in gstack uiux_pro_max plugin_dev context7 graphifyy; do
|
|
# Map function name to display name. graphifyy = the pipx PACKAGE name
|
|
# (pypi:graphifyy); the CLI and skill are 'graphify' — display that.
|
|
case "$plugin" in
|
|
uiux_pro_max) display="ui-ux-pro-max" ;;
|
|
plugin_dev) display="plugin-dev" ;;
|
|
graphifyy) display="graphify" ;;
|
|
*) display="$plugin" ;;
|
|
esac
|
|
|
|
if "detect_$plugin" 2>/dev/null; then
|
|
TOGGLE_ACTIVE+=("$display")
|
|
else
|
|
TOGGLE_INACTIVE+=("$display")
|
|
fi
|
|
done
|
|
|
|
# --- Format output ---
|
|
ACTIVE_STR="${TOGGLE_ACTIVE[*]:-none}"
|
|
INACTIVE_STR="${TOGGLE_INACTIVE[*]:-none}"
|
|
|
|
# GSD v2 — standalone CLI (not a Claude Code plugin — shown separately)
|
|
if detect_gsd 2>/dev/null; then
|
|
GSD_STATUS="gsd v2 ✓"
|
|
else
|
|
GSD_STATUS="gsd v2 ✗ (npm install -g gsd-pi)"
|
|
fi
|
|
|
|
# Version detection: follow CLAUDE.md symlink back to repo, then read version.txt
|
|
_claude_real="$(readlink "$HOME/.claude/CLAUDE.md" 2>/dev/null || true)"
|
|
if [ -n "$_claude_real" ]; then
|
|
_repo_dir="$(cd "$(dirname "$_claude_real")" 2>/dev/null && pwd)"
|
|
CONFIG_VERSION=$(cat "$_repo_dir/version.txt" 2>/dev/null || echo "?")
|
|
else
|
|
CONFIG_VERSION="?"
|
|
fi
|
|
REPO_DIR="${_repo_dir:-}"
|
|
unset _claude_real _repo_dir
|
|
|
|
# Detect plan and set passive token budget
|
|
PLAN=$(detect_plan 2>/dev/null || echo "pro")
|
|
case "$PLAN" in
|
|
max) _budget=20000; PLAN_LABEL="Max" ;;
|
|
pro) _budget=11000; PLAN_LABEL="Pro" ;;
|
|
free) _budget=5000; PLAN_LABEL="Free" ;;
|
|
*) _budget=11000; PLAN_LABEL="Pro" ;;
|
|
esac
|
|
|
|
# Quick passive token cost estimate
|
|
# Only count plugins that are ACTIVE (detected as ON), not just installed
|
|
_passive_t=0
|
|
detect_superpowers 2>/dev/null && _passive_t=$((_passive_t + 800))
|
|
|
|
# Token costs for toggle plugins — map display name to cost
|
|
declare -A _plugin_costs=(
|
|
[gstack]=2750
|
|
[ui-ux-pro-max]=400
|
|
[plugin-dev]=100
|
|
[context7]=200
|
|
[graphify]=300
|
|
)
|
|
for _p in "${TOGGLE_ACTIVE[@]}"; do
|
|
_cost="${_plugin_costs[$_p]:-0}"
|
|
_passive_t=$((_passive_t + _cost))
|
|
done
|
|
_budget_pct=$((_passive_t * 100 / _budget))
|
|
if [ "$_budget_pct" -gt 50 ]; then
|
|
TOKEN_WARN="⚠️ ~${_passive_t}t passif (${_budget_pct}% budget $PLAN_LABEL)"
|
|
elif [ "$_budget_pct" -gt 25 ]; then
|
|
TOKEN_WARN="~${_passive_t}t passif (${_budget_pct}% budget $PLAN_LABEL)"
|
|
else
|
|
TOKEN_WARN=""
|
|
fi
|
|
unset _passive_t _budget_pct _budget
|
|
|
|
echo ""
|
|
echo "┌─ Claude Code config ──────────────────────────────────┐"
|
|
# "ALWAYS ON" row — actual state, not hardcoded. RTK is a binary on PATH;
|
|
# the others are marketplace plugins whose state lives in
|
|
# settings.json:enabledPlugins. Anything missing/disabled is omitted so
|
|
# the user sees the real picture instead of a misleading literal.
|
|
ALWAYS_ON=()
|
|
detect_rtk &>/dev/null && ALWAYS_ON+=("rtk")
|
|
# Derive the plugin list from settings.json:enabledPlugins (true entries)
|
|
# instead of a hardcoded name pair — a hardcoded SET under-reports newly
|
|
# enabled plugins (pr-review-toolkit was enabled yet invisible). LRN-005
|
|
# class. Plugins owned by the toggle row below are excluded (dual display).
|
|
_toggle_owned=" gstack ui-ux-pro-max plugin-dev context7 graphify "
|
|
while IFS= read -r _pl; do
|
|
case "$_toggle_owned" in
|
|
*" $_pl "*) : ;;
|
|
*) ALWAYS_ON+=("$_pl") ;;
|
|
esac
|
|
done < <(grep -oE '"[A-Za-z0-9_-]+@[A-Za-z0-9_-]+"[[:space:]]*:[[:space:]]*true' "$HOME/.claude/settings.json" 2>/dev/null \
|
|
| sed -E 's/^"([^@]+)@.*$/\1/')
|
|
unset _toggle_owned _pl
|
|
ALWAYS_ON_STR="${ALWAYS_ON[*]:-none}"
|
|
# Same 40-char-width split policy as the toggle row below — keeps the
|
|
# right border aligned on overflow. Greedy width-fill (not a fixed 3-name
|
|
# cut: 3 long names overflowed line 1 and left line 2 empty).
|
|
if [ "${#ALWAYS_ON_STR}" -le 40 ]; then
|
|
printf "│ ✅ ON : %-40s│\n" "$ALWAYS_ON_STR"
|
|
else
|
|
_ao_l1=""; _ao_l2=""
|
|
for _ao_e in "${ALWAYS_ON[@]}"; do
|
|
if [ -z "$_ao_l2" ] && [ $(( ${#_ao_l1} + ${#_ao_e} + 1 )) -le 40 ]; then
|
|
_ao_l1="${_ao_l1:+$_ao_l1 }$_ao_e"
|
|
else
|
|
_ao_l2="${_ao_l2:+$_ao_l2 }$_ao_e"
|
|
fi
|
|
done
|
|
printf "│ ✅ ON : %-40s│\n" "$_ao_l1"
|
|
printf "│ %-40s│\n" "$_ao_l2"
|
|
unset _ao_l1 _ao_l2 _ao_e
|
|
fi
|
|
unset ALWAYS_ON ALWAYS_ON_STR
|
|
# Plugin display — all plugins shown, split across 2 lines if >4
|
|
_active_count=${#TOGGLE_ACTIVE[@]}
|
|
_inactive_count=${#TOGGLE_INACTIVE[@]}
|
|
|
|
if [ "$_active_count" -eq 0 ]; then
|
|
printf "│ 🟢 ON : %-40s│\n" "none"
|
|
elif [ "$_active_count" -le 4 ]; then
|
|
printf "│ 🟢 ON : %-40s│\n" "$ACTIVE_STR"
|
|
else
|
|
# Split: first 4 on line 1, rest on continuation line
|
|
_line1="${TOGGLE_ACTIVE[0]} ${TOGGLE_ACTIVE[1]} ${TOGGLE_ACTIVE[2]} ${TOGGLE_ACTIVE[3]}"
|
|
_rest=("${TOGGLE_ACTIVE[@]:4}")
|
|
_line2="${_rest[*]}"
|
|
printf "│ 🟢 ON : %-40s│\n" "$_line1"
|
|
printf "│ %-40s│\n" "$_line2"
|
|
unset _line1 _line2 _rest
|
|
fi
|
|
|
|
if [ "$_inactive_count" -eq 0 ]; then
|
|
printf "│ ⚫ OFF : %-40s│\n" "none"
|
|
elif [ "$_inactive_count" -le 4 ]; then
|
|
printf "│ ⚫ OFF : %-40s│\n" "$INACTIVE_STR"
|
|
else
|
|
_line1="${TOGGLE_INACTIVE[0]} ${TOGGLE_INACTIVE[1]} ${TOGGLE_INACTIVE[2]} ${TOGGLE_INACTIVE[3]}"
|
|
_rest=("${TOGGLE_INACTIVE[@]:4}")
|
|
_line2="${_rest[*]}"
|
|
printf "│ ⚫ OFF : %-40s│\n" "$_line1"
|
|
printf "│ %-40s│\n" "$_line2"
|
|
unset _line1 _line2 _rest
|
|
fi
|
|
unset _active_count _inactive_count
|
|
printf "│ 🖥️ CLI : %-40s│\n" "$GSD_STATUS"
|
|
[ -n "$TOKEN_WARN" ] && printf "│ 💰 %-44s│\n" "${TOKEN_WARN:0:44}"
|
|
printf "│ 📦 v%-45s│\n" "$CONFIG_VERSION"
|
|
if [ -n "$GF_REFRESHED" ]; then
|
|
_gf_line="hooks refreshed: $GF_REFRESHED → commit .githooks/"
|
|
printf "│ 🪝 %-44s│\n" "${_gf_line:0:44}"
|
|
unset _gf_line
|
|
fi
|
|
if [ -n "$GRAPHIFY_HINT" ]; then
|
|
printf "│ 🕸️ %-44s│\n" "${GRAPHIFY_HINT:0:44}"
|
|
printf "│ %-40s│\n" "→ /graphify (AST, seconds) — you decide"
|
|
fi
|
|
# CLAUDE.global.md line-count guard (anti-regression). BDR-062 supersedes
|
|
# BDR-031's 275 target: 305 is the assumed reality (extraction done at
|
|
# job1; further compression costs clarity > token gain) — warn past 320.
|
|
if [ -n "$REPO_DIR" ] && [ -f "$REPO_DIR/CLAUDE.global.md" ]; then
|
|
_claude_lines=$(wc -l < "$REPO_DIR/CLAUDE.global.md")
|
|
if [ "$_claude_lines" -gt 320 ]; then
|
|
_cmd_warn="CLAUDE.global.md ${_claude_lines}L (>320) — density pass"
|
|
printf "│ ⚠️ %-44s│\n" "${_cmd_warn:0:44}"
|
|
unset _cmd_warn
|
|
fi
|
|
unset _claude_lines
|
|
fi
|
|
# Version check: compare local vs remote (non-blocking)
|
|
_remote_ver=""
|
|
if [ -n "$REPO_DIR" ] && [ -d "$REPO_DIR/.git" ] && [ -z "${SESSION_START_OFFLINE:-}" ]; then
|
|
_remote_ver=$(cd "$REPO_DIR" 2>/dev/null && git fetch origin --quiet 2>/dev/null && git show origin/main:version.txt 2>/dev/null) || _remote_ver=""
|
|
fi
|
|
if [ -n "$_remote_ver" ] && [ "$_remote_ver" != "$CONFIG_VERSION" ]; then
|
|
printf "│ 🔄 update available: v%-27s│\n" "$_remote_ver"
|
|
fi
|
|
unset _remote_ver REPO_DIR
|
|
|
|
echo "│ 💡 /plugin-check before starting a new project │"
|
|
echo "│ 🩺 make doctor full diagnostic │"
|
|
echo "└───────────────────────────────────────────────────┘"
|
|
echo ""
|
|
unset TOKEN_WARN
|