Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
28 changes: 20 additions & 8 deletions src/cli/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
59 changes: 59 additions & 0 deletions src/hooks/ensure-claude-vault-daemons.sh
Original file line number Diff line number Diff line change
@@ -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