diff --git a/pyproject.toml b/pyproject.toml index d86f6d2..4eb3e39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,5 +66,5 @@ packages = [ [tool.setuptools.package-data] "*" = ["*.json", "*.yaml"] -"ctx_retriever.hooks" = ["*.py", "ctx-telemetry"] +"ctx_retriever.hooks" = ["*.py", "*.sh", "ctx-telemetry"] "ctx_retriever.dashboard" = ["*.py", "static/*.html", "static/*.js", "static/*.css"] diff --git a/src/cli/install.py b/src/cli/install.py index ec61d0d..8afac86 100644 --- a/src/cli/install.py +++ b/src/cli/install.py @@ -48,20 +48,32 @@ # Daemon scripts shipped in wheel → deployed to ~/.local/share/claude-vault/ CTX_DAEMONS = ["vec-daemon.py", "bge-daemon.py"] -# The 5 production hooks CTX ships. Each entry: (filename, event, async). +# The CTX hooks ctx-install ships. Each entry: (filename, event, async[, extra_args]). # Matched against current ~/.claude/settings.json structure. +# +# SessionStart hook (ensure-claude-vault-daemons.sh) handles vec-daemon / +# bge-daemon autostart so a fresh `pip install ctx-retriever && ctx-install` +# matches the daemon-autostart behaviour the plugin Setup path provides. +# Async so session start isn't blocked by daemon model load. CTX_HOOKS = [ - ("chat-memory.py", "UserPromptSubmit", False), - ("bm25-memory.py", "UserPromptSubmit", False, ["--rich"]), - ("memory-keyword-trigger.py", "UserPromptSubmit", False), - ("g2-fallback.py", "PostToolUse", False), - ("utility-rate.py", "Stop", True), # telemetry — async + ("chat-memory.py", "UserPromptSubmit", False), + ("bm25-memory.py", "UserPromptSubmit", False, ["--rich"]), + ("memory-keyword-trigger.py", "UserPromptSubmit", False), + ("g2-fallback.py", "PostToolUse", False), + ("utility-rate.py", "Stop", True), # telemetry — async + ("ensure-claude-vault-daemons.sh", "SessionStart", True), # daemon autostart — async ] def _hook_entry(filename: str, extra_args: list[str] | None = None) -> dict: - """Build the JSON hook entry for settings.json.""" - cmd = f"python3 $HOME/.claude/hooks/{filename}" + """Build the JSON hook entry for settings.json. + + Shell scripts (`.sh`) are invoked via `bash`; Python hooks via `python3`. + """ + if filename.endswith(".sh"): + cmd = f"bash $HOME/.claude/hooks/{filename}" + else: + cmd = f"python3 $HOME/.claude/hooks/{filename}" if extra_args: cmd = cmd + " " + " ".join(extra_args) return {"type": "command", "command": cmd} diff --git a/src/hooks/ensure-claude-vault-daemons.sh b/src/hooks/ensure-claude-vault-daemons.sh new file mode 100755 index 0000000..b10342e --- /dev/null +++ b/src/hooks/ensure-claude-vault-daemons.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Idempotent autostart for vec-daemon + bge-daemon (SessionStart hook). +# Wired into ~/.claude/settings.json by ctx-install. +# Spawns missing daemons in background; exits immediately so session start isn't blocked. +# +# Python interpreter detection (priority order): +# 1. pipx env: ~/.local/pipx/venvs/ctx-retriever/bin/python (pip install path) +# 2. venv: ~/.local/share/claude-vault/venv/bin/python (plugin Setup path) +# 3. (none) graceful exit — hook stays BM25-only +# Daemon spawn requires `sentence_transformers` importable in the chosen interpreter. +# +# Liveness check: pgrep is the source of truth. A stale socket file alone (e.g. +# from a daemon SIGKILL'd, system reboot, or terminal close) does not count as +# "alive" — we unlink it and respawn. This avoids the "dead daemon, stale sock, +# script silently skips spawn forever" failure mode. + +set -u +DAEMON_DIR="$HOME/.local/share/claude-vault" +LOG_DIR="$HOME/.cache/claude-vault-daemons" +mkdir -p "$LOG_DIR" + +PIPX_PY="$HOME/.local/pipx/venvs/ctx-retriever/bin/python" +VENV_PY="$DAEMON_DIR/venv/bin/python" + +PY="" +if [ -x "$PIPX_PY" ] && "$PIPX_PY" -c "import sentence_transformers" 2>/dev/null; then + PY="$PIPX_PY" +elif [ -x "$VENV_PY" ] && "$VENV_PY" -c "import sentence_transformers" 2>/dev/null; then + PY="$VENV_PY" +fi + +# Graceful degradation: no usable interpreter → BM25-only, no spawn +if [ -z "$PY" ]; then + exit 0 +fi + +# vec-daemon — spawn if process not alive (pgrep authoritative, socket can be stale) +if ! pgrep -f "vec-daemon.py" >/dev/null 2>&1; then + rm -f "$DAEMON_DIR/vec-daemon.sock" "$DAEMON_DIR/vec-daemon.pid" + if [ -f "$DAEMON_DIR/vec-daemon.py" ]; then + nohup "$PY" "$DAEMON_DIR/vec-daemon.py" \ + >"$LOG_DIR/vec-daemon.log" 2>&1 & + disown + fi +fi + +# bge-daemon — opt-in via CTX_BGE_ENABLE=1 (semantic rerank, ~58s first-run model load) +if [ "${CTX_BGE_ENABLE:-0}" = "1" ]; then + if ! pgrep -f "bge-daemon.py" >/dev/null 2>&1; then + rm -f "$DAEMON_DIR/bge-daemon.sock" "$DAEMON_DIR/bge-daemon.pid" + if [ -f "$DAEMON_DIR/bge-daemon.py" ]; then + nohup "$PY" "$DAEMON_DIR/bge-daemon.py" \ + >"$LOG_DIR/bge-daemon.log" 2>&1 & + disown + fi + fi +fi + +exit 0