diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..e2e09a3 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-marketplace.json", + "name": "code-index", + "owner": { + "name": "dvcdsys", + "email": "dvcdsys@gmail.com" + }, + "description": "Marketplace for cix — semantic code search and navigation tooling for Claude Code", + "plugins": [ + { + "name": "cix", + "source": "./plugins/cix", + "description": "Semantic code search and navigation. Bundles the cix CLI and nudges Claude to prefer cix over Grep for semantic queries.", + "author": { + "name": "dvcdsys" + }, + "homepage": "https://github.com/dvcdsys/code-index", + "repository": "https://github.com/dvcdsys/code-index", + "license": "MIT", + "keywords": ["search", "code-search", "semantic", "navigation", "indexing", "embeddings"], + "category": "developer-tools", + "tags": ["search", "indexing", "ai", "embeddings"] + } + ] +} diff --git a/.github/workflows/ci-plugin.yml b/.github/workflows/ci-plugin.yml new file mode 100644 index 0000000..24b5c59 --- /dev/null +++ b/.github/workflows/ci-plugin.yml @@ -0,0 +1,75 @@ +name: Plugin Tests + +# Trigger only when plugin files change — server/CLI/dashboard work +# is unaffected and shouldn't run plugin tests. +on: + push: + branches: [main, 'feat/*', 'fix/*'] + paths: + - 'plugins/cix/**' + - '.claude-plugin/**' + - '.github/workflows/ci-plugin.yml' + pull_request: + paths: + - 'plugins/cix/**' + - '.claude-plugin/**' + - '.github/workflows/ci-plugin.yml' + +# Minimum permissions required by the workflow (CodeQL workflow-permissions advisory). +# Read-only on repo contents is enough — we don't push code, comments, or releases. +permissions: + contents: read + +jobs: + test: + name: bats + shellcheck on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Install bats, jq, shellcheck (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y bats jq shellcheck + + - name: Install bats, jq, shellcheck (macOS) + if: runner.os == 'macOS' + run: | + brew install bats-core jq shellcheck + + - name: Verify bats version + run: bats --version + + - name: Run bats test suites + run: bats --tap plugins/cix/tests/*.bats + + - name: ShellCheck on hook scripts + run: | + # `--severity=warning` filters out style nags; `-x` follows + # sourced files (we don't source any in v0.1, but defensive). + shellcheck --severity=warning plugins/cix/scripts/*.sh + + - name: Validate JSON manifests with jq + run: | + jq . .claude-plugin/marketplace.json + jq . plugins/cix/.claude-plugin/plugin.json + jq . plugins/cix/hooks/hooks.json + + - name: Verify symlink integrity + run: | + # The bin/cix symlink MUST point at scripts/cix-wrapper.sh. + if [[ ! -L plugins/cix/bin/cix ]]; then + echo "::error::plugins/cix/bin/cix is not a symlink" + exit 1 + fi + target=$(readlink plugins/cix/bin/cix) + if [[ "$target" != "../scripts/cix-wrapper.sh" ]]; then + echo "::error::bin/cix points to '$target' (expected '../scripts/cix-wrapper.sh')" + exit 1 + fi diff --git a/plugins/cix/.claude-plugin/plugin.json b/plugins/cix/.claude-plugin/plugin.json new file mode 100644 index 0000000..a193e5a --- /dev/null +++ b/plugins/cix/.claude-plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", + "name": "cix", + "version": "0.1.0", + "description": "Semantic code search and navigation for Claude Code via the cix index. Bundles the cix CLI (auto-installs if missing) and nudges Claude to prefer cix over Grep for semantic queries.", + "author": { + "name": "dvcdsys", + "email": "dvcdsys@gmail.com" + }, + "homepage": "https://github.com/dvcdsys/code-index", + "repository": "https://github.com/dvcdsys/code-index", + "license": "MIT", + "keywords": ["search", "code-search", "semantic", "navigation", "indexing", "embeddings", "ai"] +} diff --git a/plugins/cix/README.md b/plugins/cix/README.md new file mode 100644 index 0000000..f1cc9cb --- /dev/null +++ b/plugins/cix/README.md @@ -0,0 +1,171 @@ +# cix — Claude Code plugin + +Semantic code search and navigation for Claude Code, powered by the +[cix](https://github.com/dvcdsys/code-index) index. + +## What you get + +- **`/cix:search`, `/cix:def`, `/cix:refs`, `/cix:init`, `/cix:status`, + `/cix:summary`** — slash commands wrapping the most-used `cix` CLI + operations. +- **Bundled cix CLI** — the plugin auto-installs `cix` on first use if + it isn't already in your `PATH` (no sudo, installs to `~/.local/bin`). + If you already have `cix` installed via the official `install.sh`, the + plugin just uses it. +- **`cix` skill (SKILL.md)** — lazy-loaded full instruction sheet + covering when to use cix vs Grep, query patterns, scoring landscape, + and CLI flags. Loads into the conversation only when Claude or you + invoke it (`/cix:search`, `/cix-skill`, or auto-trigger on a relevant + prompt). Stays in context for the rest of the session — never + duplicated. +- **Behavioral nudges (5 hooks):** + - **SessionStart** — calls `cix status` (2 s timeout). Caches the + yes/no verdict in `$CLAUDE_PLUGIN_DATA/cix-aware-$SESSION_ID-$DIR_HASH`, + injects a one-line reminder on success. + - **CwdChanged** — when Claude `cd`s into another directory mid-session, + re-runs `cix status` for the new dir and caches the verdict. Silent + (no reminder); PreToolUse handles the first-Grep-in-new-project + nudge through its per-project backoff. + - **PreToolUse(Grep|Glob)** — reads the cache for the current + `(session, project_dir)` pair; no inline `cix` calls. If the + verdict is "yes" (`1`), suggests `cix search` with exponential + backoff per project (fires on call #1, 2, 4, 8, …). Missing cache + or "no" (`0`) → silent for the rest of the session in that project. + - **PostCompact** — after auto-compaction in long sessions, re-injects + the SessionStart reminder if the current project is cix-aware + (skill body itself survives compaction natively; the SessionStart + one-liner does not). + - **SessionEnd** — glob-deletes every per-(session, dir) cache file + when the session terminates. Best-effort; the 30-day GC inside + SessionStart catches markers left over from forced kills. + +The cache key includes a project-dir hash (`shasum -a 256` first 8 +chars), so per-session, per-project state is isolated — Claude can +move between projects mid-session and each one keeps its own verdict +and backoff counter. + +## Install + +From an existing Claude Code marketplace: + +``` +/plugin marketplace add dvcdsys/code-index +/plugin install cix@code-index +/reload-plugins # or restart Claude Code +``` + +Or for local development against this repo: + +``` +/plugin marketplace add /path/to/code-index +/plugin install cix@code-index --scope local +``` + +## Requirements + +- **Claude Code v2.1.0+** (uses `hookSpecificOutput.additionalContext` + for hook-driven nudges). +- **`curl`** — only needed the first time, for the auto-bootstrap of + the `cix` CLI. +- **A reachable `cix-server`** — the CLI is a thin client. If you don't + yet have a server, see the project README for Docker setup + instructions. + +## How adoption works (the design) + +The plugin uses a 4-layer approach so SKILL.md loads at most once and +nudges don't spam the context: + +| Layer | Mechanism | Cost over a 100-prompt session | +|---|---|---| +| 1. Skill description | Native Claude Code (always-in-context, ~200 B) | ~200 B once | +| 2. SessionStart hook | One-time reminder in indexed projects | ~200 B once | +| 3. PreToolUse(Grep\|Glob) hook | Exponential-backoff nudge | ~80 B × ~7 calls = ~560 B | +| 4. SKILL.md body | Native lazy-load (skill mechanism) | ~7 KB **once** if invoked | + +Total plugin context overhead in a session that uses cix heavily: +~8 KB. In a session that doesn't touch cix at all: ~400 B (skill +description + slash command metadata). + +The SKILL.md body is **never duplicated** — Claude Code's skill +mechanism guarantees a single insertion that stays in context for the +session. See the [skill content lifecycle](https://code.claude.com/docs/en/skills#skill-content-lifecycle) +docs. + +## Configuration + +### Where the bundled CLI is installed + +The wrapper installs `cix` to `~/.local/bin/cix` by default. To override +the install location, set `CIX_PLUGIN_BIN_DIR` in your environment: + +```bash +export CIX_PLUGIN_BIN_DIR=/usr/local/bin # if you want sudo-installed +``` + +If you've already installed `cix` system-wide (e.g. via the project's +`install.sh`), the wrapper detects it and uses that binary — no second +copy is downloaded. + +### Skipping the auto-install + +Set `CIX_PLUGIN_BIN_DIR` to a directory that already contains a working +`cix` binary, or simply make sure `cix` is in your `$PATH` before +enabling the plugin. + +### Hook state cleanup + +Two per-session marker files live in `$CLAUDE_PLUGIN_DATA` +(resolves to `~/.claude/plugins/data/cix-code-index/`): +- `cix-aware-$SESSION_ID` — written by SessionStart, read by + PreToolUse. Single-byte file (`0` or `1`). +- `cix-grep-count-$SESSION_ID` — counter for the exponential backoff. + +This directory is plugin-managed and **not** cleaned by the OS +(unlike `/tmp`, which macOS purges daily). The plugin manages cleanup +in two tiers: +1. **SessionEnd hook** — deletes both markers when the session + terminates normally. Covers the common case. +2. **30-day GC in SessionStart** — opportunistically deletes markers + older than 30 days at every session start. Catches markers left + over from sessions that exited forcibly (kill -9, OOM). + +## Files + +| Path | Purpose | +|---|---| +| `.claude-plugin/plugin.json` | Plugin manifest | +| `skills/cix/SKILL.md` | Lazy-loaded usage skill (~7 KB) | +| `commands/*.md` | Six slash commands | +| `hooks/hooks.json` | SessionStart + PreToolUse(Grep\|Glob\|Bash) registration | +| `scripts/cix-wrapper.sh` | "Use system or auto-install" CLI wrapper | +| `scripts/session-start.sh` | One-time session reminder | +| `scripts/grep-nudge.sh` | Exponential-backoff Grep nudge | +| `bin/cix` | Symlink to wrapper, exposed on `$PATH` while plugin enabled | + +## Troubleshooting + +- **"cix: command not found" inside Claude Code Bash tool** — the + plugin isn't enabled or `bin/cix` isn't on `$PATH`. Run + `/plugin list` and `which cix` from inside a Claude Code session. +- **Hooks not firing** — run Claude Code with `--debug` and look for + hook registration messages. Check `/Users/dvcdsys/.claude/...` (or + your local cache path) for the hook scripts and verify they're + executable: `ls -la $(claude plugin list ... | path)/scripts/`. +- **Nudges feel too frequent / too rare** — edit the power-of-2 check + in `scripts/grep-nudge.sh` to your taste. The current schedule + (1, 2, 4, 8, 16, …) was chosen to balance "loud at start" with + "fade away". +- **"This project has a cix semantic code index" never appears** — + the project must contain a `.cix/` directory. Run `/cix:init` first. +- **Nudge does not fire on `grep` invoked via Bash** — the `PreToolUse` + matcher works on `tool_name`, not on the command string. The plugin + matches `Bash` explicitly and filters grep/rg from + `tool_input.command` inside `grep-nudge.sh`. Confirm + `hooks/hooks.json` contains both `"matcher": "Grep|Glob"` and + `"matcher": "Bash"` entries; the regression in + `tests/manifest.bats` enforces this. + +## License + +MIT — same as the parent project. diff --git a/plugins/cix/bin/cix b/plugins/cix/bin/cix new file mode 120000 index 0000000..4263b5f --- /dev/null +++ b/plugins/cix/bin/cix @@ -0,0 +1 @@ +../scripts/cix-wrapper.sh \ No newline at end of file diff --git a/plugins/cix/commands/def.md b/plugins/cix/commands/def.md new file mode 100644 index 0000000..c53303e --- /dev/null +++ b/plugins/cix/commands/def.md @@ -0,0 +1,15 @@ +--- +description: Find symbol definition(s) via cix — go-to-definition across the indexed codebase +argument-hint: [--kind function|class|method|type] [--file ] +allowed-tools: Bash(cix *) +--- + +Look up the definition of the symbol **$ARGUMENTS** in the cix index: + +```! +cix definitions $ARGUMENTS +``` + +If multiple matches are returned, point out the most likely one based on +context. If nothing is found, suggest `cix symbols $ARGUMENTS` for a +broader name search. diff --git a/plugins/cix/commands/init.md b/plugins/cix/commands/init.md new file mode 100644 index 0000000..7fc49aa --- /dev/null +++ b/plugins/cix/commands/init.md @@ -0,0 +1,17 @@ +--- +description: Initialize the cix index for the current project (registers, indexes, starts file watcher) +allowed-tools: Bash(cix *) +--- + +Initialize the cix index for the current project. This registers the +project with the cix server, performs a full initial index, and starts +the file-watcher daemon for auto-reindex on changes. + +```! +cix init +``` + +If the indexing run is in-progress, you can monitor it with `/cix:status`. +If it fails, common causes are: cix-server not reachable, missing +`CIX_API_KEY` env var, or `~/.cix/data` permission issues. Check +`cix status` for details. diff --git a/plugins/cix/commands/refs.md b/plugins/cix/commands/refs.md new file mode 100644 index 0000000..a5e3adb --- /dev/null +++ b/plugins/cix/commands/refs.md @@ -0,0 +1,14 @@ +--- +description: Find symbol references via cix — locate every usage of a symbol across the codebase +argument-hint: [--file ] [--limit ] +allowed-tools: Bash(cix *) +--- + +Find references to the symbol **$ARGUMENTS** in the cix index: + +```! +cix references $ARGUMENTS +``` + +Group the references by file and call out any high-traffic call sites or +suspicious usage patterns. If you need fewer results, add `--limit 20`. diff --git a/plugins/cix/commands/search.md b/plugins/cix/commands/search.md new file mode 100644 index 0000000..7e3c1c7 --- /dev/null +++ b/plugins/cix/commands/search.md @@ -0,0 +1,18 @@ +--- +description: Semantic code search via cix — find code by meaning, not by exact strings +argument-hint: +allowed-tools: Bash(cix *) +--- + +Run a semantic search through the cix index for the query: **$ARGUMENTS** + +```! +cix search "$ARGUMENTS" +``` + +Summarize the most relevant matches above. If results look weak, try: +- A more specific phrasing that names the area or symbol +- `cix search "$ARGUMENTS" --min-score 0.2` to lower the relevance floor +- `cix search "$ARGUMENTS" --in ` to narrow scope + +If `cix` is not yet initialized in this project, run `/cix:init` first. diff --git a/plugins/cix/commands/status.md b/plugins/cix/commands/status.md new file mode 100644 index 0000000..3b9326e --- /dev/null +++ b/plugins/cix/commands/status.md @@ -0,0 +1,15 @@ +--- +description: Show cix indexing status and file-watcher state for the current project +allowed-tools: Bash(cix *) +--- + +Show the current cix indexing status — last sync, number of indexed +files, and whether the file watcher is active. + +```! +cix status +``` + +If `Watcher: ✗ not running`, search results may be stale. Run +`cix watch` to restart the auto-reindex daemon, or `cix reindex` for a +one-off refresh. diff --git a/plugins/cix/commands/summary.md b/plugins/cix/commands/summary.md new file mode 100644 index 0000000..4d1b8b8 --- /dev/null +++ b/plugins/cix/commands/summary.md @@ -0,0 +1,16 @@ +--- +description: Show project overview from the cix index — languages, top directories, key symbols +allowed-tools: Bash(cix *) +--- + +Print a project overview from the cix index — languages, file counts, +top directories, and most-referenced symbols. Useful when starting work +on an unfamiliar codebase. + +```! +cix summary +``` + +Use this output to orient yourself before diving into specific +subsystems. For deeper exploration, follow up with `cix search` on the +top-level concepts you see here. diff --git a/plugins/cix/hooks/hooks.json b/plugins/cix/hooks/hooks.json new file mode 100644 index 0000000..6ee5c38 --- /dev/null +++ b/plugins/cix/hooks/hooks.json @@ -0,0 +1,64 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/session-start.sh" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Grep|Glob", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/grep-nudge.sh" + } + ] + }, + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/grep-nudge.sh" + } + ] + } + ], + "CwdChanged": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/cwd-changed.sh" + } + ] + } + ], + "PostCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/post-compact.sh" + } + ] + } + ], + "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/session-end.sh" + } + ] + } + ] + } +} diff --git a/plugins/cix/scripts/cix-wrapper.sh b/plugins/cix/scripts/cix-wrapper.sh new file mode 100755 index 0000000..28c504c --- /dev/null +++ b/plugins/cix/scripts/cix-wrapper.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# cix CLI wrapper for the Claude Code plugin. +# +# Strategy: "use system cix if available, else bootstrap install via the +# official install.sh script". We do NOT bundle the binary in git or +# maintain a separate cache — install.sh is the single source of truth. +# +# Resolution order: +# 1. If `cix` is found anywhere in PATH (excluding our own dir), +# exec it directly. +# 2. Otherwise, run install.sh with --bin-dir=$HOME/.local/bin +# (no sudo required), then exec the freshly installed binary. + +set -euo pipefail + +# ── Resolve our own directory (real path, dereferencing symlinks) ───────────── +# bin/cix is a symlink to ../scripts/cix-wrapper.sh, so BASH_SOURCE points to +# the real script under scripts/, not the symlink under bin/. We need the +# directory of the symlink (which is what's actually on PATH) — derive it +# from $0 instead, which preserves the invocation path. + +if [ -n "${0:-}" ] && [ "${0:0:1}" = "/" ]; then + INVOKED_PATH="$0" +else + # When called as bare `cix` via PATH, $0 is just "cix" — fall back to + # which/command -v to find ourselves. + INVOKED_PATH="$(command -v "$0" 2>/dev/null || echo "$0")" +fi + +SELF_DIR="$(cd "$(dirname "$INVOKED_PATH")" 2>/dev/null && pwd 2>/dev/null || echo "")" + +# ── Look for a cix binary elsewhere in PATH ─────────────────────────────────── +# Build a "safe PATH" that excludes our own directory so command -v doesn't +# find us recursively. + +SYS_CIX="" +if [ -n "$SELF_DIR" ]; then + SAFE_PATH="" + OLD_IFS="$IFS" + IFS=':' + # shellcheck disable=SC2086 + for dir in $PATH; do + [ -z "$dir" ] && continue + DIR_REAL="$(cd "$dir" 2>/dev/null && pwd 2>/dev/null || echo "$dir")" + if [ "$DIR_REAL" != "$SELF_DIR" ]; then + SAFE_PATH="${SAFE_PATH:+$SAFE_PATH:}$dir" + fi + done + IFS="$OLD_IFS" + SYS_CIX="$(PATH="$SAFE_PATH" command -v cix 2>/dev/null || true)" +else + SYS_CIX="$(command -v cix 2>/dev/null || true)" +fi + +if [ -n "$SYS_CIX" ]; then + exec "$SYS_CIX" "$@" +fi + +# ── Bootstrap install via install.sh (one-time) ─────────────────────────────── +TARGET="${CIX_PLUGIN_BIN_DIR:-$HOME/.local/bin}" +CACHED_CIX="$TARGET/cix" + +if [ ! -x "$CACHED_CIX" ]; then + if ! command -v curl >/dev/null 2>&1; then + echo "Error: cix is not installed and curl is not available to bootstrap it." >&2 + echo "Install cix manually: https://github.com/dvcdsys/code-index" >&2 + exit 1 + fi + + mkdir -p "$TARGET" + echo "cix CLI not found — installing to $TARGET (one-time, no sudo)..." >&2 + + # Use the official install script. Pinned to main; future versions of the + # plugin can pin to a tag (e.g. cli/v0.4.0) for reproducibility. + INSTALL_URL="https://raw.githubusercontent.com/dvcdsys/code-index/main/install.sh" + + if ! curl -fsSL "$INSTALL_URL" | bash -s -- --bin-dir "$TARGET"; then + echo "Error: cix install failed. Check network connectivity and try again." >&2 + echo "You can install manually: curl -fsSL $INSTALL_URL | bash" >&2 + exit 1 + fi + + if [ ! -x "$CACHED_CIX" ]; then + echo "Error: install.sh ran but $CACHED_CIX was not created." >&2 + exit 1 + fi + + echo "cix installed successfully at $CACHED_CIX" >&2 +fi + +exec "$CACHED_CIX" "$@" diff --git a/plugins/cix/scripts/cwd-changed.sh b/plugins/cix/scripts/cwd-changed.sh new file mode 100755 index 0000000..6c49f8f --- /dev/null +++ b/plugins/cix/scripts/cwd-changed.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# CwdChanged hook for the cix plugin. +# +# Behavior: when Claude changes working directory mid-session (e.g. via +# `cd`), evaluate cix-awareness for the new directory and cache the +# verdict. If we already have a verdict for this (session, project_dir) +# pair, this is a no-op — Claude probably came back to a project we +# already evaluated. +# +# Why no reminder injection: PreToolUse(Grep|Glob) handles the +# "first nudge in a fresh project" case via its per-project backoff +# counter (call #1 in a new project always fires). Re-inject a SessionStart +# reminder on every `cd` would be noisy if Claude bounces between +# directories. +# +# Behavior matrix: +# Cache exists for (session, NEW_DIR) → no-op (we know already) +# Cache absent + cix status exit 0 → write "1" (cix-aware) +# Cache absent + cix status exit ≠ 0 → write "0" (silent for this dir) +# Cache absent + cix CLI not found → write "0" +# Cache absent + cix status timeout → write "0" + +set -euo pipefail + +INPUT=$(cat 2>/dev/null || echo "{}") +if command -v jq >/dev/null 2>&1; then + SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null || echo "") +else + SESSION_ID=$(printf '%s' "$INPUT" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) +fi + +[ -z "$SESSION_ID" ] && exit 0 + +CACHE_DIR="${CLAUDE_PLUGIN_DATA:-/tmp}" +mkdir -p "$CACHE_DIR" 2>/dev/null || CACHE_DIR="/tmp" + +PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}" + +DIR_HASH=$(printf '%s' "$PROJECT_DIR" | shasum -a 256 2>/dev/null | cut -c1-8) +if [ -z "$DIR_HASH" ]; then + DIR_HASH=$(printf '%s' "$PROJECT_DIR" | tr -c 'a-zA-Z0-9' '-' | tail -c 16) +fi + +CACHE_FILE="$CACHE_DIR/cix-aware-$SESSION_ID-$DIR_HASH" + +# ── Already evaluated this (session, project) — no-op ───────────────────────── +if [ -f "$CACHE_FILE" ]; then + exit 0 +fi + +# ── Resolve cix binary ──────────────────────────────────────────────────────── +CIX_BIN="" +if [ -x "${CLAUDE_PLUGIN_ROOT:-}/bin/cix" ]; then + CIX_BIN="${CLAUDE_PLUGIN_ROOT}/bin/cix" +elif command -v cix >/dev/null 2>&1; then + CIX_BIN="$(command -v cix)" +fi + +if [ -z "$CIX_BIN" ]; then + printf '0' > "$CACHE_FILE" + exit 0 +fi + +# ── Run cix status with 2s timeout (same pattern as session-start.sh) ───────── +EXIT_FILE="$CACHE_FILE.exit" +( + "$CIX_BIN" status -p "$PROJECT_DIR" >/dev/null 2>&1 + echo "$?" > "$EXIT_FILE" 2>/dev/null +) & +CIX_PID=$! + +SLEPT=0 +while kill -0 "$CIX_PID" 2>/dev/null && [ "$SLEPT" -lt 20 ]; do + sleep 0.1 + SLEPT=$((SLEPT + 1)) +done + +if kill -0 "$CIX_PID" 2>/dev/null; then + kill -9 "$CIX_PID" 2>/dev/null || true + wait "$CIX_PID" 2>/dev/null || true + printf '0' > "$CACHE_FILE" + rm -f "$EXIT_FILE" + exit 0 +fi +wait "$CIX_PID" 2>/dev/null || true + +EXIT_CODE=1 +if [ -f "$EXIT_FILE" ]; then + EXIT_CODE=$(cat "$EXIT_FILE" 2>/dev/null || echo 1) + rm -f "$EXIT_FILE" +fi + +if [ "$EXIT_CODE" = "0" ]; then + printf '1' > "$CACHE_FILE" +else + printf '0' > "$CACHE_FILE" +fi + +# Silent — no context injection. PreToolUse(Grep|Glob) will handle the +# first-Grep-in-new-project nudge through its own backoff counter. +exit 0 diff --git a/plugins/cix/scripts/grep-nudge.sh b/plugins/cix/scripts/grep-nudge.sh new file mode 100755 index 0000000..7a571fc --- /dev/null +++ b/plugins/cix/scripts/grep-nudge.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# PreToolUse(Grep|Glob|Bash) hook for the cix plugin. +# +# Behavior: if SessionStart (or CwdChanged) concluded the current +# project is cix-indexed (cache file for this (session, project_dir) +# pair contains "1"), occasionally inject a system reminder pointing +# toward `cix search` instead of grep. Otherwise stay silent. +# +# Bash is matched in addition to Grep/Glob because real-session usage +# of `grep`/`rg` happens through the Bash tool (pipelines, `| head`, +# `cd && grep …`). The Bash branch inspects tool_input.command and +# only proceeds when it looks like a grep-family call; non-grep Bash +# (ls, git status, make, go test) is fully silent and does not even +# increment the backoff counter. +# +# This hook does NOT call `cix status` itself — it relies entirely on +# the cache written by SessionStart and refreshed by CwdChanged. +# Trade-off: a session that started before the cix-server came up will +# stay in "silent" mode for the rest of its life in that project, even +# if the server later comes back online. Intentional: better to miss a +# few nudge opportunities than spam a developer whose server is down. +# +# Per-(session, project) backoff: each project Claude visits has its +# own exponential-backoff counter. A new `cd` into a fresh project +# starts the backoff from scratch (call #1 → nudge), so the first Grep +# in a new cix-aware project always gets a reminder. +# +# Throttling: exponential backoff. Reminders fire on the 1st, 2nd, 4th, +# 8th, 16th, 32nd, 64th, ... Grep/Glob invocation in the current +# project. ~7 reminders per 100-Grep span, loud at the start, fading +# as the model "learns" the workflow. + +set -euo pipefail + +INPUT=$(cat 2>/dev/null || echo "{}") +if command -v jq >/dev/null 2>&1; then + SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null || echo "") +else + SESSION_ID=$(printf '%s' "$INPUT" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) +fi + +# No session_id → can't read the SessionStart cache. Stay silent. +[ -z "$SESSION_ID" ] && exit 0 + +# ── Gate by tool_name + command shape ───────────────────────────────────────── +# For Grep/Glob the intent is unambiguous — always proceed (still subject to +# the cache check and exponential backoff below). For Bash we additionally +# inspect tool_input.command and only proceed when it looks like a grep-family +# command; non-grep Bash exits silently here WITHOUT bumping the backoff +# counter, so ls/git status/make/etc. stay invisible. +# +# Without jq the Bash branch falls through to "silent": parsing shell commands +# out of a JSON blob with sed invites false positives, and silent is safer than +# nudging on every Bash call. +TOOL_NAME="" +if command -v jq >/dev/null 2>&1; then + TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null || echo "") +fi + +case "$TOOL_NAME" in + Grep|Glob) + : # always proceed + ;; + Bash) + TOOL_CMD="" + if command -v jq >/dev/null 2>&1; then + TOOL_CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || echo "") + fi + # Match grep/egrep/fgrep/rg/ripgrep as a standalone token. Anchors: + # start-of-string, whitespace, `|`, `;`, `&`, backtick, `(`. The regex + # rejects `git grep` (subcommand after `git`, not a standalone shell + # `grep`) and substring hits like `grepl`, `egrep_helper`. + if ! [[ "$TOOL_CMD" =~ (^|[[:space:]\|\&\;\`\(])(grep|egrep|fgrep|rg|ripgrep)([[:space:]]|$) ]]; then + exit 0 + fi + ;; + *) + # Unknown tool_name, or no jq available → silent. + exit 0 + ;; +esac + +CACHE_DIR="${CLAUDE_PLUGIN_DATA:-/tmp}" +PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}" + +# Compute per-project hash — same algorithm as session-start.sh. +DIR_HASH=$(printf '%s' "$PROJECT_DIR" | shasum -a 256 2>/dev/null | cut -c1-8) +if [ -z "$DIR_HASH" ]; then + DIR_HASH=$(printf '%s' "$PROJECT_DIR" | tr -c 'a-zA-Z0-9' '-' | tail -c 16) +fi + +# ── Read SessionStart's verdict for THIS project ────────────────────────────── +# Strict policy: only "1" allows nudges. Missing file or "0" → silent. +CACHE_FILE="$CACHE_DIR/cix-aware-$SESSION_ID-$DIR_HASH" + +if [ ! -f "$CACHE_FILE" ]; then + exit 0 +fi +if [ "$(cat "$CACHE_FILE" 2>/dev/null)" != "1" ]; then + exit 0 +fi + +# ── Increment per-(session, project) counter ────────────────────────────────── +COUNTER_FILE="$CACHE_DIR/cix-grep-count-$SESSION_ID-$DIR_HASH" +COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo 0) +case "$COUNT" in + ''|*[!0-9]*) COUNT=0 ;; +esac +COUNT=$((COUNT + 1)) +printf '%d' "$COUNT" > "$COUNTER_FILE" + +# Power-of-2 check: COUNT & (COUNT - 1) == 0 means COUNT is 1, 2, 4, 8, ... +if [ "$((COUNT & (COUNT - 1)))" -ne 0 ]; then + exit 0 +fi + +# ── Emit nudge ──────────────────────────────────────────────────────────────── +MESSAGE="💡 You're about to grep this project (call #$COUNT this session). This project has a cix semantic index — for queries by meaning (find by concept, cross-file lookups, symbol navigation), \`cix search\` / \`cix def\` / \`cix refs\` outperform grep. Grep is best for exact strings (error messages, config keys, import paths). The \`/cix:search\` slash command is also available." + +if command -v jq >/dev/null 2>&1; then + jq -n --arg msg "$MESSAGE" \ + '{hookSpecificOutput: {hookEventName: "PreToolUse", additionalContext: $msg}}' +else + ESC=$(printf '%s' "$MESSAGE" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ') + printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"%s"}}\n' "$ESC" +fi + +exit 0 diff --git a/plugins/cix/scripts/post-compact.sh b/plugins/cix/scripts/post-compact.sh new file mode 100755 index 0000000..b7d982b --- /dev/null +++ b/plugins/cix/scripts/post-compact.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# PostCompact hook for the cix plugin. +# +# Behavior: after Claude Code compacts the conversation, re-inject the +# SessionStart reminder if this (session, project) is cix-aware. +# +# Why this matters: skill bodies survive auto-compaction (Claude Code +# re-attaches them with up to 5K tokens per skill, see +# https://code.claude.com/docs/en/skills#skill-content-lifecycle). +# But the SessionStart `additionalContext` reminder — and PreToolUse +# nudges — are NOT skills. They live as regular tool result messages +# and are dropped/summarised during compaction. +# +# In long sessions (8+ hours of work) where the cix skill hasn't been +# invoked yet, the model may "forget" cix exists after compaction. +# Re-injecting the same one-line reminder keeps cix-awareness alive. +# +# This is a no-op if SessionStart concluded the project is not indexed +# (cache=0) or if no verdict exists yet. + +set -euo pipefail + +INPUT=$(cat 2>/dev/null || echo "{}") +if command -v jq >/dev/null 2>&1; then + SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null || echo "") +else + SESSION_ID=$(printf '%s' "$INPUT" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) +fi + +[ -z "$SESSION_ID" ] && exit 0 + +CACHE_DIR="${CLAUDE_PLUGIN_DATA:-/tmp}" +PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}" + +DIR_HASH=$(printf '%s' "$PROJECT_DIR" | shasum -a 256 2>/dev/null | cut -c1-8) +if [ -z "$DIR_HASH" ]; then + DIR_HASH=$(printf '%s' "$PROJECT_DIR" | tr -c 'a-zA-Z0-9' '-' | tail -c 16) +fi + +CACHE_FILE="$CACHE_DIR/cix-aware-$SESSION_ID-$DIR_HASH" + +# ── Read verdict ────────────────────────────────────────────────────────────── +# Strict policy mirrors grep-nudge.sh: only "1" triggers re-injection. +if [ ! -f "$CACHE_FILE" ]; then + exit 0 +fi +if [ "$(cat "$CACHE_FILE" 2>/dev/null)" != "1" ]; then + exit 0 +fi + +# ── Re-inject the SessionStart reminder ─────────────────────────────────────── +MESSAGE='💡 (Post-compact reminder) This project has a cix semantic code index. For semantic queries — finding code by meaning, cross-file lookups, symbol navigation, "where is X used", "how does Y work" — prefer `cix search`, `cix def`, `cix refs`, or the slash commands `/cix:search`, `/cix:def`, `/cix:refs`. Use Grep only for exact strings (error messages, config keys, import paths).' + +if command -v jq >/dev/null 2>&1; then + jq -n --arg msg "$MESSAGE" \ + '{hookSpecificOutput: {hookEventName: "PostCompact", additionalContext: $msg}}' +else + ESC=$(printf '%s' "$MESSAGE" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ') + printf '{"hookSpecificOutput":{"hookEventName":"PostCompact","additionalContext":"%s"}}\n' "$ESC" +fi + +exit 0 diff --git a/plugins/cix/scripts/session-end.sh b/plugins/cix/scripts/session-end.sh new file mode 100755 index 0000000..d400a37 --- /dev/null +++ b/plugins/cix/scripts/session-end.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# SessionEnd hook for the cix plugin. +# +# Behavior: when the Claude Code session terminates, remove every +# cache file belonging to this session from $CLAUDE_PLUGIN_DATA. +# A single session may have visited multiple projects (via `cd`), so +# we glob-delete by session_id prefix. Cleanup is best-effort: +# SessionEnd may not fire if the process was killed forcibly (kill -9, +# OOM, panic) — session-start.sh also runs a 30-day GC sweep as a +# safety net. +# +# Files removed (per session_id, all directory hashes): +# $CLAUDE_PLUGIN_DATA/cix-aware-$SESSION_ID-* (verdict caches) +# $CLAUDE_PLUGIN_DATA/cix-grep-count-$SESSION_ID-* (backoff counters) +# +# Output: nothing. Failures are silently ignored. + +set -euo pipefail + +INPUT=$(cat 2>/dev/null || echo "{}") +if command -v jq >/dev/null 2>&1; then + SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null || echo "") +else + SESSION_ID=$(printf '%s' "$INPUT" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) +fi + +# Without a session_id we don't know what to clean. Exit cleanly. +[ -z "$SESSION_ID" ] && exit 0 + +CACHE_DIR="${CLAUDE_PLUGIN_DATA:-/tmp}" +[ -d "$CACHE_DIR" ] || exit 0 + +# Glob-delete every per-(session, dir) marker for this session. +# +# Safety is enforced by the find filters, not by where the cache dir is: +# -maxdepth 1 — never recurse into subdirectories +# -type f — files only (skips dirs and symlinks) +# -name 'cix-aware-$SESSION_ID-*' — exact prefix + this session_id +# -name 'cix-grep-count-$SESSION_ID-*' — exact prefix + this session_id +# +# $SESSION_ID is a UUID assigned by Claude Code, so the patterns +# practically cannot match anything but our own marker files even in +# unusual cache-dir locations. +# +# We never use `rm -rf` and never recurse — there's no path on which +# this script could touch a file that doesn't already match the strict +# name pattern. +find "$CACHE_DIR" -maxdepth 1 -type f \ + \( -name "cix-aware-$SESSION_ID-*" -o -name "cix-grep-count-$SESSION_ID-*" \) \ + -delete 2>/dev/null || true + +exit 0 diff --git a/plugins/cix/scripts/session-start.sh b/plugins/cix/scripts/session-start.sh new file mode 100755 index 0000000..a99c352 --- /dev/null +++ b/plugins/cix/scripts/session-start.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# SessionStart hook for the cix plugin. +# +# Behavior: at session start, ask `cix status` whether the current +# project is indexed. The result is cached for the (session, project) +# pair in $CLAUDE_PLUGIN_DATA/cix-aware-$SESSION_ID-$DIR_HASH so the +# PreToolUse hook can short-circuit without re-querying the server. +# +# Cache key includes a hash of the project directory, so a single +# session that traverses multiple projects (via `cd`, see CwdChanged +# hook) keeps a separate verdict per project — fresh backoff counter +# per project, correct cix-aware state per directory. +# +# State location: $CLAUDE_PLUGIN_DATA is plugin-persistent storage +# managed by Claude Code (resolves to ~/.claude/plugins/data//). +# It survives plugin updates and is NOT periodically cleaned by the OS, +# unlike /tmp (macOS daily cleanup of 3-day-old files; Linux on reboot). +# Falls back to /tmp only when run outside a plugin context (tests). +# +# Decision contract (read by grep-nudge.sh, post-compact.sh): +# File present with content "1" → project is indexed, nudge allowed +# File present with content "0" → not indexed, nudge MUST stay silent +# File absent → no verdict yet, nudge stays silent +# +# Why no fallback in grep-nudge: if SessionStart (or CwdChanged) concluded +# "not indexed" (server unreachable, project not registered, etc.), the +# user should NOT see Grep nudges suggesting `cix search` for the rest +# of the session. Sending nudges based on `.cixignore` presence anyway +# would create false positives. + +set -euo pipefail + +# ── Read session_id from stdin JSON ─────────────────────────────────────────── +INPUT=$(cat 2>/dev/null || echo "{}") +if command -v jq >/dev/null 2>&1; then + SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null || echo "") +else + SESSION_ID=$(printf '%s' "$INPUT" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) +fi + +# Without a session_id we can't write a session-scoped marker. Stay silent. +if [ -z "$SESSION_ID" ]; then + exit 0 +fi + +# ── Resolve cache directory ─────────────────────────────────────────────────── +# Prefer plugin-persistent storage; fall back to /tmp for ad-hoc/test invocations. +# We do NOT whitelist parent paths — users can have non-standard layouts +# (custom $CLAUDE_PLUGIN_DATA, XDG dirs, corporate setups). Safety comes +# from the file-level checks below: -maxdepth 1, -type f, exact -name +# patterns matching only our session-id-prefixed markers. +CACHE_DIR="${CLAUDE_PLUGIN_DATA:-/tmp}" +mkdir -p "$CACHE_DIR" 2>/dev/null || CACHE_DIR="/tmp" +[ -d "$CACHE_DIR" ] || exit 0 + +PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}" + +# Hash the project dir so the cache file name is short and stable. +# `shasum -a 256` exists on both macOS (Perl-based) and Linux (coreutils). +DIR_HASH=$(printf '%s' "$PROJECT_DIR" | shasum -a 256 2>/dev/null | cut -c1-8) +if [ -z "$DIR_HASH" ]; then + # shasum unavailable; fall back to a path-derived suffix. + DIR_HASH=$(printf '%s' "$PROJECT_DIR" | tr -c 'a-zA-Z0-9' '-' | tail -c 16) +fi + +CACHE_FILE="$CACHE_DIR/cix-aware-$SESSION_ID-$DIR_HASH" + +# ── Light maintenance: clear markers older than 30 days ─────────────────────── +# Long-running Claude Code installs would accumulate one-byte markers +# otherwise. Cheap, runs once per session. Failures ignored. +# +# Safety constraints on the find: +# -maxdepth 1 — never recurse into subdirectories +# -type f — files only (skips dirs, symlinks) +# -name 'cix-aware-*' OR +# -name 'cix-grep-count-*' — exact prefix match on our marker names +# -mtime +30 — older than 30 days +# +# A file outside this prefix is invisible to find — it's never even +# considered for deletion, regardless of how the cache dir is configured. +find "$CACHE_DIR" -maxdepth 1 -type f \ + \( -name 'cix-aware-*' -o -name 'cix-grep-count-*' \) \ + -mtime +30 -delete 2>/dev/null || true + +# ── Resolve a working `cix` binary ──────────────────────────────────────────── +CIX_BIN="" +if [ -x "${CLAUDE_PLUGIN_ROOT:-}/bin/cix" ]; then + CIX_BIN="${CLAUDE_PLUGIN_ROOT}/bin/cix" +elif command -v cix >/dev/null 2>&1; then + CIX_BIN="$(command -v cix)" +fi + +if [ -z "$CIX_BIN" ]; then + # CLI not yet installed (would auto-bootstrap on first call). Mark off. + printf '0' > "$CACHE_FILE" + exit 0 +fi + +# ── Run `cix status` with a 2-second timeout ────────────────────────────────── +# macOS lacks `timeout`/`gtimeout` by default — implement in pure bash. +EXIT_FILE="$CACHE_FILE.exit" +( + "$CIX_BIN" status -p "$PROJECT_DIR" >/dev/null 2>&1 + echo "$?" > "$EXIT_FILE" 2>/dev/null +) & +CIX_PID=$! + +SLEPT=0 +while kill -0 "$CIX_PID" 2>/dev/null && [ "$SLEPT" -lt 20 ]; do + sleep 0.1 + SLEPT=$((SLEPT + 1)) +done + +if kill -0 "$CIX_PID" 2>/dev/null; then + kill -9 "$CIX_PID" 2>/dev/null || true + wait "$CIX_PID" 2>/dev/null || true + printf '0' > "$CACHE_FILE" + rm -f "$EXIT_FILE" + exit 0 +fi +wait "$CIX_PID" 2>/dev/null || true + +EXIT_CODE=1 +if [ -f "$EXIT_FILE" ]; then + EXIT_CODE=$(cat "$EXIT_FILE" 2>/dev/null || echo 1) + rm -f "$EXIT_FILE" +fi + +if [ "$EXIT_CODE" != "0" ]; then + # Not indexed (or server unreachable). Lock the session into "off" mode. + printf '0' > "$CACHE_FILE" + exit 0 +fi + +# ── Project IS indexed — cache + inject reminder ────────────────────────────── +printf '1' > "$CACHE_FILE" + +MESSAGE='💡 This project has a cix semantic code index. For semantic queries — finding code by meaning, cross-file lookups, symbol navigation, "where is X used", "how does Y work" — prefer `cix search`, `cix def`, `cix refs`, or the slash commands `/cix:search`, `/cix:def`, `/cix:refs`. Use Grep only for exact strings (error messages, config keys, import paths). Run `cix status` if results seem stale.' + +if command -v jq >/dev/null 2>&1; then + jq -n --arg msg "$MESSAGE" \ + '{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $msg}}' +else + ESC=$(printf '%s' "$MESSAGE" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ') + printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\n' "$ESC" +fi + +exit 0 diff --git a/plugins/cix/skills/cix/SKILL.md b/plugins/cix/skills/cix/SKILL.md new file mode 100644 index 0000000..13ea488 --- /dev/null +++ b/plugins/cix/skills/cix/SKILL.md @@ -0,0 +1,217 @@ +--- +name: cix +description: Semantic code search and navigation via the cix index. Use this when finding code by meaning rather than exact strings — cross-file lookups, symbol navigation, "where is X used", "how does Y work", "find authentication middleware", or exploring an unfamiliar codebase. Covers search, definitions, references, symbol search, file lookup, and indexing. +when_to_use: | + Trigger this skill when the user asks anything that requires semantic understanding of the codebase: + - "find authentication middleware" / "find the auth code" + - "where is X defined?" / "show me the definition of Y" + - "how does Z work in this codebase?" + - "what calls this function?" / "find references to ..." + - "search the codebase for ..." / "find by meaning" + - "explore this repo" / "give me an overview" + - Any time you would otherwise reach for Grep on a non-literal query + + Skip this skill (use Grep / Read instead) when: + - A stack trace or error already names file:line — just Read it + - Searching for an exact literal (specific error string, config key name, import path) + - Inside dependencies (node_modules, vendor, .venv) — they aren't indexed + - Editing a non-code file (Dockerfile, yaml, lockfile) +user-invocable: true +allowed-tools: Bash(cix *) +--- + +# Code Index (`cix`) — Semantic Code Search & Navigation + +You have access to `cix`, a semantic code index that understands the +codebase via embeddings + AST parsing. The right reflex is **"cix when +you don't have a pointer; grep when you do."** + +This plugin also exposes shortcuts: `/cix:search`, `/cix:def`, `/cix:refs`, +`/cix:init`, `/cix:status`, `/cix:summary`. The `cix` CLI is bundled — +the plugin auto-installs it on first use if your system doesn't have it. + +## When to use which + +**Reach for `cix` first when:** +- The starting point is open-ended ("how does indexing work?", "find the + authentication middleware", "where is the main entry point?") +- You need cross-file navigation (definitions / references / callers) +- You're searching by *meaning*, not by an exact string + (`"JWT validation"` should find `verifyToken` even without that phrase) +- You're exploring an unfamiliar package or codebase + +**Skip `cix`, use Read / Grep / Glob directly when:** +- A failing test or stack trace already names the file and function — + just `Read` it +- You're chasing an exact literal: a specific error message, a config + key, a commit-message phrase, an import path +- You're inside dependencies (`node_modules`, `vendor`, `.venv`) — they + aren't indexed +- You're editing a non-code file (Dockerfile, yaml, lockfile) + +If `cix` returns nothing relevant after one well-formed query, fall +back to grep — don't loop on cix. + +--- + +## Commands Reference + +### Semantic Search — find code by meaning +```bash +cix search "authentication middleware" +cix search "database connection retry logic" +cix search "error handling in payment flow" --limit 20 +cix search "config parsing" --in ./internal/config/ +cix search "API routes" --lang go +cix search "main entry point" --exclude bench/fixtures --exclude legacy +``` + +**Flags:** +- `--in ` — restrict to file or directory (can repeat) +- `--exclude ` — drop a directory or substring from results (can repeat) +- `--lang ` — filter by language (can repeat) +- `--limit ` — max **files** returned (default: 10) — output is + grouped per file with all matches inside, so 10 files ≈ many snippets +- `--min-score ` — minimum relevance 0.0–1.0 (default: **0.4**) + +### Go to Definition — find where a symbol is defined +```bash +cix definitions HandleRequest +cix def AuthMiddleware --kind function +cix def Config --file ./internal/config.go +``` +Aliases: `definitions`, `def`, `goto`. Flags: `--kind`, `--file`, `--limit`. + +### Find References — find where a symbol is used +```bash +cix references HandleRequest +cix refs AuthMiddleware --limit 50 +cix usages UserService --file ./internal/api/ +``` +Aliases: `references`, `refs`, `usages`. Flags: `--file`, `--limit`. + +### Symbol Search — find symbols by name +```bash +cix symbols handleRequest +cix symbols User --kind class +cix symbols Auth --kind function --kind method +``` +Flags: `--kind` (function/class/method/type, repeatable), `--limit`. + +### File Search — find files by path pattern +```bash +cix files "config" +cix files "middleware" --limit 20 +``` + +### Project Overview +```bash +cix summary # languages, top dirs, key symbols +cix status # indexing status + file watcher status +cix list # all indexed projects +``` + +### Indexing +```bash +cix init [path] # register + index + start watcher +cix reindex # incremental +cix reindex --full # full reindex +cix cancel # cancel an in-flight indexing run +cix watch # start file-change auto-reindex daemon +cix watch stop # stop daemon +``` + +The watcher auto-reindexes on file change — manual `reindex` is rarely +needed. `cix status` shows whether the watcher is running and the +last-sync timestamp. + +--- + +## Search quality — what scores mean + +Default `--min-score 0.4` is calibrated for the production embedding +model (CodeRankEmbed-Q8 with path-aware preamble). Rough landscape: + +| Score | Meaning | +|----------|---------------------------------------------------------| +| 0.65+ | Exact / very strong match — almost certainly relevant | +| 0.50–0.65| Strong match — usually relevant | +| 0.40–0.50| Weaker match — sometimes useful, sometimes not | +| <0.40 | Noise — filtered out by default | + +**If a query returns nothing**, lower the floor explicitly: +`--min-score 0.2` for very specific or long-tail queries. Don't drop +below 0.2 — results below that are noise. + +--- + +## Writing better queries — leverage path-aware embedding + +Each chunk is embedded with its file path, language, and symbol name in +the preamble. This means **mentioning a file/dir/symbol you already +know about boosts ranking**: + +```bash +# Generic +cix search "validation" +# Better — pins the search to the auth area +cix search "validation in auth middleware" +# Even better when you know the symbol +cix search "ValidateToken" --kind function +``` + +Natural-language queries that name the *kind of thing* and *where it +lives* outperform single-word queries. + +--- + +## Usage Patterns + +### Exploring unfamiliar code (`cix`'s strongest case) +```bash +cix summary # project structure, top dirs +cix search "main entry point server" # find where it starts +cix search "database connection setup" # find DB wiring +cix search "request handler" --in ./api # narrow to API +``` + +### Tracing a symbol end-to-end +```bash +cix def HandleRequest # where is it defined? +cix refs HandleRequest # who calls it? +cix search "HandleRequest error handling" # how are errors handled? +``` + +### Chasing a known target (often grep is enough) +```bash +# Stack trace says "internal/auth/middleware.go:42 — invalid token" +# → just Read that file. No cix needed. + +# Config key "max_concurrent_requests" used somewhere? +# → grep is more precise. +``` + +### Narrowing scope +```bash +cix search "middleware" --in ./api/ +cix search "config" --in ./cmd/ --exclude legacy +cix refs Config --file ./internal/server.go +``` + +--- + +## Tips + +- Search queries are natural language, not regex. Write what you'd ask + a colleague. +- Output groups by file: each result line is a file with all relevant + matches inside, ordered top-to-bottom by line number. The + `[best 0.NN]` is the score of the top hit in that file. +- `cix def` is a faster path than `cix symbols` when you already know + the exact name. +- `--exclude` complements `--in` — use it to drop noisy dirs (`bench/`, + `legacy/`, vendored code) inline without touching `.cixignore`. +- The watcher keeps the index fresh. If results feel stale, check + `cix status` first — `Watcher: ✗ not running` is the usual cause. +- Don't loop. If a query returns nothing useful after one well-phrased + attempt + one `--min-score 0.2` retry, drop to grep. diff --git a/plugins/cix/tests/README.md b/plugins/cix/tests/README.md new file mode 100644 index 0000000..51274dc --- /dev/null +++ b/plugins/cix/tests/README.md @@ -0,0 +1,94 @@ +# Plugin tests + +Hook script tests for the cix Claude Code plugin. Uses +[bats-core](https://bats-core.readthedocs.io/) with mocked `cix` binary, +isolated `$CLAUDE_PLUGIN_DATA`, and a per-test scratch project directory. + +## Run locally + +```bash +# Install bats + jq + shellcheck +brew install bats-core jq shellcheck # macOS +sudo apt-get install bats jq shellcheck # Debian / Ubuntu + +# From repo root: +bats plugins/cix/tests/*.bats + +# Or pick one suite: +bats plugins/cix/tests/session-end.bats + +# TAP-formatted output (what CI uses): +bats --tap plugins/cix/tests/*.bats +``` + +Each test runs in an isolated `$BATS_TMPDIR` scratch directory and +cleans up after itself — no state leaks between tests. + +## What's covered + +| Suite | Focus | +|---|---| +| `session-start.bats` | cix-status flow, cache write, 30-day GC, **non-matching files preserved** | +| `cwd-changed.bats` | First-cd evaluation, no-op on cached dir, multi-dir state | +| `grep-nudge.bats` | Exponential backoff (1, 2, 4, 8, 16), per-(session, dir) counters | +| `post-compact.bats` | Re-injection only when cache="1" | +| `session-end.bats` | **Security:** deletion never leaks to other sessions, non-cix files, or subdirs — even with custom `$CLAUDE_PLUGIN_DATA` | +| `cix-wrapper.bats` | System-cix passthrough, exit code propagation, self-recursion guard | + +## Security tests (the most important ones) + +Bash scripts that call `find -delete` get extra scrutiny. Safety comes +from **what** we delete (strict `-name` patterns + `-type f` + +`-maxdepth 1`), not **where** the cache dir lives. The plugin +deliberately does not whitelist parent paths, so users with custom +`$CLAUDE_PLUGIN_DATA` (corporate setups, XDG-style layouts) are +supported. + +`session-end.bats` and `session-start.bats` suites contain explicit +adversarial cases: + +- Other sessions' cache files → must NOT be touched +- Files with confusable names (`cix-other-pattern`, + `X-cix-aware-fake-...`, `cix` alone) → must NOT be touched +- Random files (`config.yaml`, `.env`, `secrets.json`) in cache dir + → must NOT be touched +- Subdirectories in cache dir + nested files → must NOT be touched + (only `-maxdepth 1`) +- 30-day GC → must spare files outside the `cix-aware-*` / + `cix-grep-count-*` prefixes, even if they're old +- `session_id` containing shell metacharacters → must NOT trigger + command injection (canary file survives) +- Custom non-standard `$CLAUDE_PLUGIN_DATA` → script proceeds without + refusing, deletes only matching files + +If any of these fail in CI, the offending change cannot land. + +## Mocks + +`tests/mocks/bin/cix` is a fake `cix` CLI controlled via env vars: + +- `MOCK_CIX_EXIT` — exit code (default 0) +- `MOCK_CIX_DELAY` — sleep before exit (for timeout tests) +- `MOCK_CIX_LOG_FILE` — append every invocation here so tests can + assert "was the script called with the right args?" + +`helpers.bash` puts the mock first on `$PATH` for every hook invocation, +so unqualified `cix` calls inside the hook scripts hit the mock. + +## Adding a new test + +1. Pick (or create) the right `.bats` file. +2. Use `setup() { setup_test_env; }` and `teardown() { teardown_test_env; }`. +3. Use `run_hook