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/CLAUDE-CODE-PLUGIN.md b/CLAUDE-CODE-PLUGIN.md new file mode 100644 index 0000000..908d6cc --- /dev/null +++ b/CLAUDE-CODE-PLUGIN.md @@ -0,0 +1,491 @@ +# Claude Code Plugin (`cix`) + +Official Claude Code plugin for `cix` — semantic code search and navigation. +Installs in two commands, bundles the `cix` CLI, ships slash commands, a +skill, and behavioral hooks that nudge Claude to prefer `cix` over `Grep` +for semantic queries. + +> [!IMPORTANT] +> **The plugin does NOT include the `cix` server.** The plugin only ships +> the CLI client and Claude Code integration glue. You must run a `cix` +> server separately and point the CLI at it (see [Prerequisites](#prerequisites) +> below). Without a reachable server the plugin can install, but `cix` +> commands will fail at runtime. + +--- + +## What you get + +When the plugin is enabled, every Claude Code session in a `cix`-indexed +project automatically gets: + +- **Slash commands** — `/cix:search`, `/cix:def`, `/cix:refs`, `/cix:init`, + `/cix:status`, `/cix:summary`. Invocable manually or by Claude. +- **Bundled `cix` CLI** — the plugin auto-installs `cix` to + `~/.local/bin/` on first use if it isn't already in your `PATH` (no + sudo). If you already installed `cix` system-wide via `install.sh`, + the plugin reuses that binary — no second copy. +- **`cix` skill (`SKILL.md`)** — full usage reference (when to reach for + cix vs Grep, query patterns, scoring landscape, CLI flags). **Lazy-loaded** + by Claude Code — enters context only when invoked, stays once per + session. +- **Behavioral hooks (5 total):** + - **`SessionStart`** — at session start, runs `cix status` (2-second + timeout) to ask the cix-server whether the current project is + registered. The verdict (`1` or `0`) is cached in + `$CLAUDE_PLUGIN_DATA/cix-aware-$SESSION_ID-$DIR_HASH`. On `1`, + injects a one-line reminder. + - **`CwdChanged`** — when Claude changes working directory mid-session + (e.g. via `cd ../other-project`), evaluates cix-awareness for the + NEW directory and caches the verdict. Silent (no reminder) — the + next Grep/Glob call will fire the standard backoff nudge if + appropriate. No-op if the new directory was already evaluated in + this session (Claude bouncing back to a known project). + - **`PreToolUse(Grep|Glob)`** — reads the cache for the current + `(session, project_dir)` pair (~1 ms, no cix call). If the cache + says `1`, occasionally suggests `cix search` instead of Grep, + throttled with **exponential backoff** (fires on call #1, 2, 4, 8, + 16, 32, 64, … *per project*). Each project visited in a session + starts a fresh backoff counter, so the first Grep in a new + cix-aware project always gets a nudge. **Strict policy:** missing + cache or `0` → silent for the entire session in that project. + - **`PostCompact`** — after Claude Code compacts the conversation + (long-running sessions), re-injects the SessionStart reminder if + the current project is cix-aware. Skill bodies survive compaction + natively, but the SessionStart `additionalContext` does not — this + hook keeps cix-awareness alive after compaction without relying on + the skill being invoked yet. + - **`SessionEnd`** — when the session terminates, deletes every + cache file belonging to this session (glob: all `(session, *)` + pairs across every project visited). + +**Cache key includes a project-dir hash** (`shasum -a 256` first 8 +chars), so a single Claude Code session that traverses multiple +projects keeps a separate verdict per project — fresh backoff per +project, correct cix-aware state per directory. + +The strict cache contract means: a session that started while the +cix-server was unreachable will stay in "silent" mode for that project +even if the server comes back online. Restart the Claude Code session +or `cd` away and back to re-evaluate. Better to miss a few nudges than +to pester a developer whose server is down. + +State location: `$CLAUDE_PLUGIN_DATA` is plugin-persistent storage +(`~/.claude/plugins/data/cix-code-index/`) — it survives plugin updates +and is **not** cleaned by the OS, unlike `/tmp` (macOS purges 3-day-old +files daily; Linux clears on reboot). Cleanup is two-tiered: SessionEnd +removes per-session markers on normal exit; SessionStart opportunistically +deletes markers older than 30 days as a safety net for forced kills +(kill -9, OOM, panic). + +--- + +## How it works + +The plugin uses a **4-layer adoption design** 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 +([skill content lifecycle docs](https://code.claude.com/docs/en/skills#skill-content-lifecycle)). + +--- + +## Prerequisites + +The plugin is the **client side** of the cix stack. Before installing, +make sure you have: + +### 1. A reachable `cix` server + +The CLI talks to a `cix-server` over HTTP. The server runs separately +(in Docker, natively on macOS, or remotely). See the [main README](README.md) +for the three deployment modes: + +- Docker CPU — `docker compose up -d` +- Docker CUDA (NVIDIA GPU) — `docker compose -f docker-compose.cuda.yml up -d` +- Native macOS (Apple Silicon Metal) — `cd server && make bundle && make run` + +Verify it's up: + +```bash +curl http://localhost:21847/health # → {"status":"ok"} +``` + +If you have a fresh database, the server requires `CIX_BOOTSTRAP_ADMIN_EMAIL` +and `CIX_BOOTSTRAP_ADMIN_PASSWORD` on first boot — see the main README's +Quick Start. + +### 2. The `cix` CLI configured to point at that server + +The CLI configuration is **independent of the plugin**. The plugin uses +whatever config the CLI reads from `~/.cix/config.yaml`. Configure it +once: + +```bash +cix config set api.url http://localhost:21847 +cix config set api.key cix_ +``` + +Get an API key from one of: + +- The dashboard: open `http://localhost:21847/dashboard` → **API Keys** → + mint a new key. +- Your `.env` file: `grep CIX_API_KEY /path/to/code-index/.env | cut -d= -f2`. + +Verify the CLI can reach the server: + +```bash +cix list # should show projects (or "no projects yet") +``` + +> [!IMPORTANT] +> If the CLI is not configured (or the server is unreachable), Claude +> will see error output from `cix` commands. The plugin can't paper +> over a missing server — it's a thin wrapper, not a replacement. + +### 3. Claude Code v2.1.0 or later + +The plugin uses `hookSpecificOutput.additionalContext` for hook-driven +nudges, which requires Claude Code 2.1.0+. Check with `claude --version`. + +--- + +## Installation + +There are three install paths depending on your scenario. + +### Option A — From GitHub (recommended for end users) + +Once the plugin is merged to `main`: + +```bash +claude plugin marketplace add dvcdsys/code-index --sparse .claude-plugin plugins +claude plugin install cix@code-index --scope user +``` + +The `--sparse` flag limits checkout to the plugin directories +(`.claude-plugin/` + `plugins/`), avoiding a full clone of the server, +CLI source, and dashboard build. + +### Option B — From a specific branch or tag (testing / pinned versions) + +```bash +# From a branch (e.g. testing a feature branch) +claude plugin marketplace add dvcdsys/code-index@feat/claude-code-plugin \ + --sparse .claude-plugin plugins + +# From a tag (pinned release) +claude plugin marketplace add dvcdsys/code-index@plugin/v0.1.0 \ + --sparse .claude-plugin plugins + +claude plugin install cix@code-index --scope user +``` + +### Option C — From a local clone (plugin development) + +```bash +git clone https://github.com/dvcdsys/code-index +claude plugin marketplace add /absolute/path/to/code-index +claude plugin install cix@code-index --scope user +``` + +### Choosing the scope + +`--scope user` (default in our examples) — plugin available in every +project. Stored in `~/.claude/settings.json`. **Recommended for personal +use.** + +`--scope project` — committed to `.claude/settings.json`, shared with +teammates via git. Good for team-wide enable. + +`--scope local` — stored in `.claude/settings.local.json`, gitignored. +Only the current project. Useful when testing. + +After install, restart Claude Code (or run `/reload-plugins` in an +existing session) so hooks register. + +--- + +## Verification + +```bash +# Plugin is installed and enabled +claude plugin list +# Expected output (excerpt): +# ❯ cix@code-index +# Version: 0.1.0 +# Scope: user +# Status: ✔ enabled + +# Both manifests pass official validation +claude plugin validate $(claude plugin list --json | jq -r '.[] | select(.id=="cix@code-index").installPath') + +# The bundled CLI wrapper works +$(claude plugin list --json | jq -r '.[] | select(.id=="cix@code-index").installPath')/bin/cix --version +# → cix v0.X.Y +``` + +Then in a Claude Code session inside a cix-indexed project: + +1. Type `/cix` and check autocomplete shows 6 commands (`search`, `def`, + `refs`, `init`, `status`, `summary`). +2. Run `/cix:status` — should print `cix status` output. +3. Ask Claude something semantic ("find the auth middleware") and watch + whether it reaches for `cix search` or falls back to Grep. + +--- + +## Uninstall + +### Plugin only (keep marketplace registered) + +```bash +claude plugin uninstall cix@code-index --scope user +``` + +### Plugin + marketplace + cache (full cleanup) + +```bash +claude plugin uninstall cix@code-index --scope user +claude plugin marketplace remove code-index +rm -rf ~/.claude/plugins/cache/code-index # belt-and-suspenders +``` + +This does **not** uninstall the `cix` CLI itself or stop the cix server +— those are independent. Remove them separately if needed: + +```bash +# Remove the CLI binary if you used the plugin's bootstrap install +rm ~/.local/bin/cix + +# Stop the server +docker compose down # CPU mode +docker compose -f docker-compose.cuda.yml down # CUDA mode +launchctl unload ~/Library/LaunchAgents/com.cix.server.plist # native macOS +``` + +### Troubleshooting "wrong scope" errors + +If `claude plugin uninstall` complains the plugin is in a different +scope, check the master state file: + +```bash +jq '.plugins["cix@code-index"]' ~/.claude/plugins/installed_plugins.json +``` + +This shows every install (with `scope` and, for local installs, +`projectPath`). For local-scope uninstall, `cd` to the registered +`projectPath` first, then re-run the uninstall. + +--- + +## Configuration + +Most plugin behavior is automatic. The few env vars you can set: + +| Variable | Default | Effect | +|---|---|---| +| `CIX_PLUGIN_BIN_DIR` | `$HOME/.local/bin` | Where the wrapper installs `cix` if it isn't on PATH yet. | + +The CLI config (`~/.cix/config.yaml`) is **separate** from the plugin — +the plugin doesn't write to it. Configure the CLI once (see +[Prerequisites](#prerequisites)) and the plugin will use that config. + +### Per-project trigger threshold + +The plugin nudges Claude in projects that `cix status` reports as +**indexed**. The check runs once per session at SessionStart (against +the cix-server) and is cached for the remainder of the session. +PreToolUse(Grep|Glob) only ever reads the cache — it never makes its +own `cix` calls. + +If the cix-server is unreachable at session start, the project is +locked into "silent" mode for the rest of the session. Restart Claude +Code (Cmd+Q + reopen, or `/reload` in CLI) once the server is back to +re-evaluate. + +--- + +## What the plugin does NOT do + +To keep the v0.1 surface focused, the plugin intentionally excludes: + +- **MCP server** — cix isn't exposed as an MCP tool yet (planned for v0.2). + This means the plugin works in Claude Code (CLI + Code mode in Claude + Desktop) but **not** in pure Claude Desktop chat sessions, which only + consume MCP servers. +- **Server lifecycle management** — the plugin will not start, stop, or + configure your `cix-server`. That's intentional: the server is shared + infrastructure (one server can index many projects, used by many + agents and IDEs), not a per-plugin concern. +- **CLI configuration UI** — `cix config set` is the source of truth. + The plugin reads it but doesn't replace it. + +--- + +## Troubleshooting + +### "cix: command not found" inside Claude Code + +The plugin's `bin/` should be on `PATH` while the plugin is enabled. +Check: + +```bash +claude plugin list # plugin enabled? +ls -la ~/.claude/plugins/cache/code-index/cix/*/bin/cix # symlink exists? +``` + +If the symlink is missing, reinstall: +`claude plugin uninstall cix@code-index --scope user && claude plugin install cix@code-index --scope user`. + +### Hooks silent in indexed project + +The hooks rely on `cix status` succeeding at SessionStart. Verify: + +```bash +cix status -p $(pwd) # must exit 0 +echo "exit=$?" +``` + +If `cix status` fails: +- Server unreachable: `curl http://localhost:21847/health` +- API key not set: `cix config show` +- Project not registered: `cix init` + +Once `cix status` exits 0, **restart the Claude Code session** — +SessionStart cached the previous "not indexed" verdict and the +PreToolUse hook reads only that cache. There's no inline retry by +design (a flaky server shouldn't cause intermittent nudges). + +To inspect the current verdict from outside Claude Code: + +```bash +ls -la ~/.claude/plugins/data/cix-code-index/cix-aware-* +cat ~/.claude/plugins/data/cix-code-index/cix-aware- +# "1" → nudges allowed; "0" → silent +``` + +### Hooks too loud / too quiet + +Edit `scripts/grep-nudge.sh` in your local clone of the plugin, change +the power-of-2 check to your taste, and reinstall. Default schedule +(1, 2, 4, 8, 16, …) was chosen to balance "loud at start" with +"fade away". + +### `cix` commands return errors at runtime + +The CLI runs but the server is unreachable, or the API key is invalid. +Verify each step from [Prerequisites](#prerequisites): + +```bash +curl http://localhost:21847/health +cix config show +cix list +``` + +### Two duplicate entries in `claude plugin list` + +This usually means you installed the plugin in two scopes (e.g. `local` +plus `user`). Check the master state: + +```bash +jq '.plugins["cix@code-index"]' ~/.claude/plugins/installed_plugins.json +``` + +Uninstall from the unwanted scope (you may need to `cd` to the +registered `projectPath` for local-scope uninstall). + +### Plugin installs but slash commands don't appear + +Slash commands are loaded at session start. After install, run +`/reload-plugins` in an existing Claude Code session, or quit and +re-open Claude Code. + +--- + +## Security & testing + +The plugin runs bash scripts on every Claude Code session, with calls +that include `find -delete` against `$CLAUDE_PLUGIN_DATA`. Safety comes +from **what we delete**, not **where the directory lives** — every +deletion is gated by file-level filters that practically cannot match +anything except our own marker files: + +1. **Restrictive find filters.** Every `find -delete` uses: + - `-maxdepth 1` — never recurses into subdirectories + - `-type f` — files only (skips dirs and symlinks) + - `-name 'cix-aware-*'` / `-name 'cix-grep-count-*'` — exact prefix + match on our marker names; for `session-end.sh` the pattern also + embeds the current `$SESSION_ID` (a Claude-Code-assigned UUID), + so it cannot match other sessions' files + + `rm -rf` is not used anywhere in the plugin. There is no path on + which a hook script could touch a file that doesn't already match + the strict name pattern, regardless of how `$CLAUDE_PLUGIN_DATA` is + configured. This means **custom data dirs work fine** — corporate + setups, XDG-style layouts, or alternative paths are all supported. + +2. **Automated test suite.** `plugins/cix/tests/` contains 41 + [bats-core](https://bats-core.readthedocs.io/) tests covering all 6 + hook scripts. Adversarial cases include: + - `session_id` containing shell metacharacters — must not inject + commands (canary file survives) + - Other sessions' cache files — must not be touched + - Files with similar-but-different names (`cix-other-pattern`, + `X-cix-aware-fake-...`, `cix` alone) — must not be touched + - Subdirectories in the cache dir — must not be touched (no recursion) + - 30-day GC — must spare files outside the cix-prefixed patterns + - Custom non-standard `$CLAUDE_PLUGIN_DATA` — must work without + deleting unrelated files in that dir + - Path-with-spaces project dirs — must hash correctly + + GitHub Actions runs the suite on Ubuntu and macOS for every PR + that touches `plugins/cix/` or `.claude-plugin/`. ShellCheck runs + alongside, gating warnings. + +To run tests locally: + +```bash +brew install bats-core jq shellcheck # macOS +sudo apt-get install bats jq shellcheck # Debian / Ubuntu + +bats plugins/cix/tests/*.bats +shellcheck --severity=warning plugins/cix/scripts/*.sh +``` + +See [`plugins/cix/tests/README.md`](plugins/cix/tests/README.md) for +the full test matrix and instructions for adding new cases. + +## Roadmap + +**v0.2** (after v0.1 feedback): + +- **MCP server** exposing `cix_search`, `cix_definitions`, `cix_references` + as native Claude tools, so cix becomes available in Claude Desktop chat + and any other MCP-compatible client. +- **`PreToolUse(Bash)` hook** that catches inline `grep` calls (not just + the `Grep` tool) and routes them through cix where appropriate. +- **`cix-explorer` subagent** preconfigured for codebase exploration tasks + (`Skill: cix` + read-only tool whitelist + `context: fork`). + +**v0.3+:** auto-`cix init` on first use, hot-reload of the skill from +git after a `cix reindex`, distribution as an officially-listed plugin +in `claude-plugins-official`. + +--- + +## License + +MIT — same as the parent project. diff --git a/README.md b/README.md index 36640a2..02c6754 100644 --- a/README.md +++ b/README.md @@ -350,7 +350,30 @@ Config file: `~/.cix/config.yaml` ### Claude Code -Install the bundled skill so Claude knows to use `cix` automatically: +Two integration paths — pick whichever fits your setup. + +#### Option A — Plugin (recommended) + +The official `cix` Claude Code plugin bundles the skill, slash commands +(`/cix:search`, `/cix:def`, `/cix:refs`, `/cix:init`, `/cix:status`, +`/cix:summary`), the CLI auto-bootstrap, and behavioral hooks that nudge +Claude to prefer `cix` over Grep for semantic queries. + +```bash +claude plugin marketplace add dvcdsys/code-index --sparse .claude-plugin plugins +claude plugin install cix@code-index --scope user +``` + +> The server still runs separately, and the CLI must be configured to +> point at it (steps above in [Quick Start](#quick-start)). +> +> See **[CLAUDE-CODE-PLUGIN.md](CLAUDE-CODE-PLUGIN.md)** for full details: +> prerequisites, install paths (branch / tag / local clone), +> verification, configuration, uninstall, and troubleshooting. + +#### Option B — Skill (manual, legacy) + +For environments without plugin support, copy the bundled skill manually: ```bash cp -r skills/cix ~/.claude/skills/cix diff --git a/doc/TODO.md b/doc/TODO.md new file mode 100644 index 0000000..01782ef --- /dev/null +++ b/doc/TODO.md @@ -0,0 +1,109 @@ +# TODO / Roadmap + +Tracked deferred work for the cix project. Items here are deliberately +postponed — typically because they require data from real-world usage, +need a separate design pass, or sit outside the current release scope. + +When you start an item, link the PR/branch in the relevant section. + +--- + +## Plugin v0.2 + +### `PostToolUseFailure` hook for `Bash(cix *)` — graceful degradation when cix-server is unreachable + +**Status:** designed, not implemented. + +**Problem.** During a Claude Code session the cix-server can become +unreachable mid-flight (Docker restart, OOM, OS sleep, network blip). +The plugin's `SessionStart` hook ran successfully at session start and +cached `cix-aware = "1"` for the project. PreToolUse(Grep|Glob) keeps +nudging the model to use `cix search`. The model dutifully runs `cix +search …`, the CLI exits non-zero, and the model sees an error. Plugin +keeps nudging on the next Grep — model retries cix — fails again. Loop. + +**Why we're deferring.** The edge case is rare on a stable local server, +the loop is annoying but not destructive (model picks up after a couple +of failures and switches back to Grep), and the manual workaround +(restart Claude Code session, or set the cache file to `0`) is trivial. +We'd rather collect data on real failure rates from v0.1 before adding +more state machinery. + +**The interactive-prompt question.** Initial intent was an actual UI +dialog: "cix-server unreachable. Disable cix nudges for this session? +[Yes] [No]". After investigating Claude Code's hook API, this isn't +available — `permissionDecision: "ask"` only works for `PreToolUse` +events, and `PostToolUseFailure` does not accept it. There's no +mechanism for a hook to trigger an arbitrary user prompt. + +**Functional equivalent that IS available:** `PostToolUseFailure` can +return `hookSpecificOutput.additionalContext`. We can use that to +1. Overwrite `$CLAUDE_PLUGIN_DATA/cix-aware-$SESSION_ID-$DIR_HASH` + with `"0"` — silencing all subsequent PreToolUse(Grep|Glob) and + PostCompact hooks for the rest of the session in this project. +2. Inject a one-line message via `additionalContext`: + > 💡 cix command failed (server unreachable). Disabled cix nudges + > for this session. Run `cix status` and restart Claude Code if + > you've fixed the server. + +The model relays this to the user in its next response. Effect is +identical to "user clicked Yes on a Disable dialog": plugin goes silent, +user is informed and decides what to do. No actual interactive UI, but +the developer experience is the same. + +**Implementation sketch:** + +1. New script `plugins/cix/scripts/cix-failed.sh` — reads `session_id`, + computes `DIR_HASH`, overwrites the cache file with `"0"`, emits + the JSON message. +2. Register it in `plugins/cix/hooks/hooks.json`: + ```json + "PostToolUseFailure": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/cix-failed.sh" + } + ] + } + ] + ``` +3. Inside the script, parse `tool_input.command` from stdin and exit + silently if it doesn't start with `cix ` — so unrelated Bash + failures don't trigger the disable path. +4. Idempotent: if cache is already `"0"`, no-op (avoid re-injecting the + message on every subsequent failure). + +**Ship criteria.** Wait for at least one user report (or one self-observed +incident) where v0.1 plugin loops on cix failures before implementing. +Otherwise we're solving a phantom problem. + +**Estimate:** ~1 day. ~50 lines of bash, hooks.json registration, doc +updates in `CLAUDE-CODE-PLUGIN.md`, manual test scenario covering +`docker compose stop` mid-session. + +--- + +### Other deferred plugin work + +- **MCP server** exposing `cix_search` / `cix_definitions` / `cix_references` + as native Claude tools, so cix becomes available in pure Claude + Desktop chat (where plugins don't run). +- **`PreToolUse(Bash)` matcher** that catches inline `grep` calls + (`Bash(grep ...)`) — currently the plugin only nudges on the dedicated + `Grep`/`Glob` tools, not on `grep` invoked through `Bash`. +- **`cix-explorer` subagent** preconfigured for codebase exploration — + `Skill: cix` preloaded + `context: fork` + `agent: Explore` + read-only + tool whitelist. +- **Plugin tag stream + `release-plugin.yml` workflow** so the plugin + has its own version tags (`plugin/v0.1.0`, `plugin/v0.2.0`, …) + alongside `cli/v*` and `server/v*`. + +--- + +## Server / CLI + +(none currently tracked here; server and CLI roadmap is in their +respective changelogs and release-server.yml / release-cli.yml workflows) 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..052cf02 --- /dev/null +++ b/plugins/cix/README.md @@ -0,0 +1,164 @@ +# 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) 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. + +## 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..81435cd --- /dev/null +++ b/plugins/cix/hooks/hooks.json @@ -0,0 +1,55 @@ +{ + "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" + } + ] + } + ], + "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..73fa12b --- /dev/null +++ b/plugins/cix/scripts/grep-nudge.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# PreToolUse(Grep|Glob) 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/Glob. Otherwise stay silent. +# +# 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 + +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 use Grep/Glob (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