@fodx/codelens 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +212 -0
  2. package/adapters/pi/codelens.extension.ts +280 -0
  3. package/adapters/pi/extension.json +10 -0
  4. package/build/src/cli.js +265 -0
  5. package/build/src/cli.js.map +1 -0
  6. package/build/src/context/schema.sql +23 -0
  7. package/build/src/context/store.js +91 -0
  8. package/build/src/context/store.js.map +1 -0
  9. package/build/src/db/db.js +65 -0
  10. package/build/src/db/db.js.map +1 -0
  11. package/build/src/db/migrations.js +88 -0
  12. package/build/src/db/migrations.js.map +1 -0
  13. package/build/src/db/schema.js +11 -0
  14. package/build/src/db/schema.js.map +1 -0
  15. package/build/src/db/schema.sql +111 -0
  16. package/build/src/git/scope.js +68 -0
  17. package/build/src/git/scope.js.map +1 -0
  18. package/build/src/graph/edges.js +76 -0
  19. package/build/src/graph/edges.js.map +1 -0
  20. package/build/src/graph/grammars.js +57 -0
  21. package/build/src/graph/grammars.js.map +1 -0
  22. package/build/src/graph/query.js +52 -0
  23. package/build/src/graph/query.js.map +1 -0
  24. package/build/src/graph/resolve.js +68 -0
  25. package/build/src/graph/resolve.js.map +1 -0
  26. package/build/src/graph/symbols.js +84 -0
  27. package/build/src/graph/symbols.js.map +1 -0
  28. package/build/src/graph/tests.js +73 -0
  29. package/build/src/graph/tests.js.map +1 -0
  30. package/build/src/index/autoprune.js +29 -0
  31. package/build/src/index/autoprune.js.map +1 -0
  32. package/build/src/index/deny.js +40 -0
  33. package/build/src/index/deny.js.map +1 -0
  34. package/build/src/index/freshness.js +60 -0
  35. package/build/src/index/freshness.js.map +1 -0
  36. package/build/src/index/fts.js +125 -0
  37. package/build/src/index/fts.js.map +1 -0
  38. package/build/src/index/identity.js +21 -0
  39. package/build/src/index/identity.js.map +1 -0
  40. package/build/src/index/indexer.js +32 -0
  41. package/build/src/index/indexer.js.map +1 -0
  42. package/build/src/index/manager.js +48 -0
  43. package/build/src/index/manager.js.map +1 -0
  44. package/build/src/index/queue.js +47 -0
  45. package/build/src/index/queue.js.map +1 -0
  46. package/build/src/index/recovery.js +51 -0
  47. package/build/src/index/recovery.js.map +1 -0
  48. package/build/src/index/reindex.js +70 -0
  49. package/build/src/index/reindex.js.map +1 -0
  50. package/build/src/index/scanner.js +147 -0
  51. package/build/src/index/scanner.js.map +1 -0
  52. package/build/src/index/ttl.js +87 -0
  53. package/build/src/index/ttl.js.map +1 -0
  54. package/build/src/index/watcher.js +74 -0
  55. package/build/src/index/watcher.js.map +1 -0
  56. package/build/src/installer/agents.js +440 -0
  57. package/build/src/installer/agents.js.map +1 -0
  58. package/build/src/obs/doctor.js +53 -0
  59. package/build/src/obs/doctor.js.map +1 -0
  60. package/build/src/obs/stats.js +28 -0
  61. package/build/src/obs/stats.js.map +1 -0
  62. package/build/src/obs/usage.js +136 -0
  63. package/build/src/obs/usage.js.map +1 -0
  64. package/build/src/search/rank.js +70 -0
  65. package/build/src/search/rank.js.map +1 -0
  66. package/build/src/search/snippet.js +66 -0
  67. package/build/src/search/snippet.js.map +1 -0
  68. package/build/src/server.js +126 -0
  69. package/build/src/server.js.map +1 -0
  70. package/build/src/tools/current.js +32 -0
  71. package/build/src/tools/current.js.map +1 -0
  72. package/build/src/tools/expand.js +54 -0
  73. package/build/src/tools/expand.js.map +1 -0
  74. package/build/src/tools/prune.js +46 -0
  75. package/build/src/tools/prune.js.map +1 -0
  76. package/build/src/tools/refresh.js +14 -0
  77. package/build/src/tools/refresh.js.map +1 -0
  78. package/build/src/tools/registry.js +176 -0
  79. package/build/src/tools/registry.js.map +1 -0
  80. package/build/src/tools/related.js +28 -0
  81. package/build/src/tools/related.js.map +1 -0
  82. package/build/src/tools/save.js +15 -0
  83. package/build/src/tools/save.js.map +1 -0
  84. package/build/src/tools/search.js +124 -0
  85. package/build/src/tools/search.js.map +1 -0
  86. package/build/src/upgrade.js +74 -0
  87. package/build/src/upgrade.js.map +1 -0
  88. package/build/src/util/hash.js +15 -0
  89. package/build/src/util/hash.js.map +1 -0
  90. package/build/src/util/paths.js +67 -0
  91. package/build/src/util/paths.js.map +1 -0
  92. package/build/src/version.js +27 -0
  93. package/build/src/version.js.map +1 -0
  94. package/docs/agent-guide.md +47 -0
  95. package/docs/codelens-preview.png +0 -0
  96. package/docs/routing.md +59 -0
  97. package/docs/tools.md +53 -0
  98. package/package.json +103 -0
package/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # CodeLens
2
+
3
+ ![CodeLens](docs/codelens-preview.png)
4
+
5
+ A local, **branch-aware code search & relation graph** — a lens into how
6
+ the code in a repo connects. Indexes the current branch into SQLite (FTS5
7
+ lexical + tree-sitter symbols + source-graph edges: imports / tests / calls /
8
+ defines / belongs_to) and returns compact, ranked, re-queryable handles so you
9
+ can find relevant files/symbols and walk their relationships without flooding
10
+ your context with raw grep/read output.
11
+
12
+ No chat LLM is involved — retrieval is deterministic. Surfaced as an **MCP
13
+ server** and a **CLI**; works with any MCP-compatible client (Claude Code,
14
+ Cursor, Gemini CLI, opencode, Codex CLI) or directly from the terminal.
15
+
16
+ - **Branch isolation** — each branch/worktree has its own index; results never
17
+ leak across branches.
18
+ - **Relations, not just text** — graph edges (imports / tests / callers / …)
19
+ ranked alongside FTS5 + symbol-name matches.
20
+ - **Fresh** — auto-refreshes changed files before query; `cl_expand` always
21
+ reads from disk.
22
+ - **Compact** — returns ranked handles; expand only what you need.
23
+ - **Durable** — saved contexts live in a separate DB and survive index rebuilds.
24
+ - **Self-cleaning** — automatic TTL prunes inactive indexes.
25
+
26
+ ## Install (one command)
27
+
28
+ The installer builds the tool, puts `codelens` on your PATH, and wires the
29
+ MCP server into your agents/IDEs automatically (Claude Code, Cursor, Gemini CLI,
30
+ opencode, Codex CLI). Requires Node.js ≥ 22.5.
31
+
32
+ **macOS / Linux:**
33
+ ```bash
34
+ curl -fsSL https://raw.githubusercontent.com/ex-git/codeLens/main/install.sh | sh
35
+ ```
36
+
37
+ **Windows (PowerShell):**
38
+ ```powershell
39
+ irm https://raw.githubusercontent.com/ex-git/codeLens/main/install.ps1 | iex
40
+ ```
41
+
42
+ Then open a new terminal (so PATH picks up) and the agents you use are configured.
43
+ The installer wires the MCP server into every detected agent; to choose
44
+ explicitly or re-run later:
45
+
46
+ ```bash
47
+ codelens install --target all --yes # wire all agents
48
+ codelens install --target claude,cursor # wire specific agents
49
+ codelens install --target auto --location=local # project-local config
50
+ codelens --print-config codex # print a snippet, no writes
51
+ codelens uninstall # remove from all agents
52
+ ```
53
+
54
+ **Upgrade:**
55
+ ```bash
56
+ codelens upgrade --check # is an update available?
57
+ codelens upgrade # git pull + rebuild
58
+ ```
59
+
60
+ **Uninstall everything:**
61
+ ```bash
62
+ curl -fsSL https://raw.githubusercontent.com/ex-git/codeLens/main/install.sh | sh -s -- --uninstall
63
+ ```
64
+
65
+ > Note on clients/LLMs: MCP clients (Claude Code, Cursor, …) bring their own
66
+ > model — CodeLens has no LLM and configures none. The installer only wires the
67
+ > MCP server + routing instructions into each host's config.
68
+
69
+ ## Supported agents / IDEs
70
+
71
+ The installer (`codelens install --target <id>`) writes the MCP server config
72
+ and routing instructions into each host's real config file. `--location=global`
73
+ (user-wide) is the default; `--location=local` writes project-local config
74
+ instead. All writes are idempotent and removable with `codelens uninstall`.
75
+
76
+ | Host | target id | Config file (global → local) | Entry shape |
77
+ |---|---|---|---|
78
+ | Claude Code | `claude` | `~/.claude.json` → `.mcp.json` | `mcpServers.codelens = { command, args: [] }` |
79
+ | Cursor | `cursor` | `~/.cursor/mcp.json` → `.cursor/mcp.json` | `mcpServers.codelens = { command, args: [] }` |
80
+ | Gemini CLI | `gemini` | `~/.gemini/settings.json` → `.gemini/settings.json` | `mcpServers.codelens = { command, args: [] }` |
81
+ | opencode | `opencode` | `~/.config/opencode/opencode.json` → `./opencode.json` | `mcp.codelens = { type: "local", command: [cmd], enabled: true }` |
82
+ | Codex CLI | `codex` | `~/.codex/config.toml` → `.codex/config.toml` | TOML `[mcp_servers.codelens]` block (`command`, `args = []`) |
83
+ | Pi Coding Agent | `pi` | `pi install npm:@fodx/codelens` (loads `adapters/pi/codelens.extension.ts`) | Pi extension that bridges the MCP server via `pi.registerTool` |
84
+
85
+ `command` is the absolute path to the installed `codelens` launcher (written by
86
+ the installer); for a manual snippet use `npx -y @fodx/codelens`.
87
+
88
+ **Routing instructions** are also written so the host prefers codelens tools for
89
+ discovery over raw grep/read:
90
+
91
+ | Host | Instructions file (global → local) |
92
+ |---|---|
93
+ | Claude Code | `~/.claude/CLAUDE.md` → `./CLAUDE.md` (+ slash commands in `~/.claude/commands/codelens-*.md`) |
94
+ | Cursor | `~/.cursor/rules/codelens.mdc` → `.cursor/rules/codelens.mdc` (dedicated, `alwaysApply`) |
95
+ | Gemini CLI | `~/.gemini/GEMINI.md` → `./GEMINI.md` |
96
+ | opencode | `~/.config/opencode/AGENTS.md` → `./AGENTS.md` |
97
+ | Codex CLI | `~/.codex/AGENTS.md` → `./AGENTS.md` |
98
+
99
+ **Print a snippet without writing anything:**
100
+
101
+ ```bash
102
+ codelens --print-config claude # JSON mcpServers snippet + target path
103
+ codelens --print-config codex # TOML [mcp_servers.codelens] block
104
+ codelens --print-config pi # Pi extension manifest pointer
105
+ ```
106
+
107
+ **Pi Coding Agent** — install as a Pi package (the npm tarball ships the
108
+ extension under `adapters/pi/` and is tagged `pi-package`, so it appears in the
109
+ [Pi package gallery](https://pi.dev/packages) once published):
110
+
111
+ ```bash
112
+ pi install npm:@fodx/codelens # user-wide
113
+ pi install -l npm:@fodx/codelens # project-local (.pi/settings.json)
114
+ pi -e npm:@fodx/codelens # try it for one run without installing
115
+ ```
116
+
117
+ > **npm discoverability:** `package.json` is tagged `pi-package` (required for
118
+ > the Pi gallery) plus descriptive keywords (`mcp-server`,
119
+ > `modelcontextprotocol`, `claude-code`, `cursor`, `gemini-cli`, `opencode`,
120
+ > `codex`) so the package is findable via npm search. Only `pi-package` is
121
+ > confirmed to trigger a host gallery listing; the others are for search
122
+ > discoverability and do not imply auto-listing in those hosts' marketplaces.
123
+
124
+ ## Install (MCP) — manual alternative
125
+
126
+ Add to your MCP client config (Claude Code, Cursor, OpenCode, Gemini CLI,
127
+ Codex CLI):
128
+
129
+ ```json
130
+ {
131
+ "mcpServers": {
132
+ "codelens": {
133
+ "command": "npx",
134
+ "args": ["-y", "@fodx/codelens"]
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
140
+ Or run locally from source:
141
+
142
+ ```bash
143
+ npm install --legacy-peer-deps
144
+ npm run build
145
+ node build/src/server.js
146
+ ```
147
+
148
+ ## Quickstart
149
+
150
+ 1. In your repo, the agent calls `cl_current` (builds index if missing) or
151
+ `cl_refresh` explicitly.
152
+ 2. `cl_search(query: "session validation")` → ranked handles.
153
+ 3. `cl_related(path: "src/auth/session.ts", types: ["tests"])` → tests.
154
+ 4. `cl_expand(path: "src/auth/session.ts", startLine: 12, endLine: 58)` → exact code.
155
+
156
+ See [`docs/agent-guide.md`](docs/agent-guide.md) for a full walkthrough,
157
+ [`docs/tools.md`](docs/tools.md) for the tool reference, and
158
+ [`docs/routing.md`](docs/routing.md) for agent routing instructions.
159
+
160
+
161
+ ## Native adapters
162
+
163
+ See `adapters/` for host-specific hook skeletons that nudge agents toward
164
+ codelens tools instead of raw grep/read.
165
+
166
+ ## CLI (non-MCP usage)
167
+
168
+ The `codelens` binary also works directly from the terminal:
169
+
170
+ ```bash
171
+ codelens doctor # health check
172
+ codelens index # build/update the current branch index
173
+ codelens search "session validation" # ranked search
174
+ codelens related src/auth/session.ts # graph neighbors
175
+ codelens stats # index counts
176
+ codelens current # repo/branch status
177
+ ```
178
+
179
+ ## Development
180
+
181
+ ```bash
182
+ npm run typecheck # tsc --noEmit
183
+ npm run lint # eslint
184
+ npm test # vitest run
185
+ npm run build # tsc + copy schema assets
186
+ npm run benchmark # performance gate (search <50ms, cold <3s)
187
+ ```
188
+
189
+ ## Docs
190
+
191
+ - [How CodeLens works](docs/how-it-works.md) — architecture, index layers, branch isolation, freshness, ranking, TTL, saved contexts, usage.
192
+ - [Usage metrics & how "saved" is calculated](docs/usage-metrics.md) — the formula, assumptions, and limits.
193
+ - [CodeLens vs CodeGraph](docs/vs-codegraph.md) — feature comparison.
194
+
195
+ ## Limitations
196
+
197
+ - **No semantic/vector search** — the ONNX subsystem was removed; ranking is
198
+ FTS5 + symbol-name + graph proximity. (A real tokenizer can be re-added later.)
199
+ - **Lazy on-demand indexing for very large repos** is future work — `cl_refresh`
200
+ is eager. The recommended pattern for large repos is one `cl_refresh` then
201
+ incremental + the file watcher thereafter (cold index ~1.6s for 2000 files).
202
+ - **Routing hooks are advisory** (soft nudges, not hard blocks) per the no-
203
+ throttling design decision — raw reads remain allowed for editing/verification.
204
+ - **npm package** — published automatically from `v*` git tags via the
205
+ `publish` workflow; `npx -y @fodx/codelens` works once a tag is pushed. See
206
+ `.github/workflows/publish.yml`.
207
+ - **Windows not tested** — path normalization handles backslashes, but `fs.watch`
208
+ recursive behavior and native builds are unverified on Windows.
209
+
210
+ ## License
211
+
212
+ MIT
@@ -0,0 +1,280 @@
1
+ /**
2
+ * CodeLens — Pi Coding Agent extension.
3
+ *
4
+ * Bridges the CodeLens MCP server (stdio) into Pi by spawning it as a
5
+ * long-lived child, performing the MCP handshake (initialize → tools/list),
6
+ * and registering each tool with `pi.registerTool`. Each tool's `execute()`
7
+ * forwards to `tools/call`. No external deps — pure child_process + JSON-RPC
8
+ * over stdio line frames.
9
+ *
10
+ * Install: copy/symlink this file to ~/.pi/agent/extensions/codelens.ts
11
+ * (Pi auto-discovers global extensions there; `/reload` hot-reloads it).
12
+ *
13
+ * The server script path resolves from (in order): CODELENS_SERVER env,
14
+ * the launcher on PATH (~/.local/bin/codelens → handled below by exec'ing
15
+ * node directly), or the build next to this repo. We exec `node <server.js>`
16
+ * to avoid depending on the launcher being on PATH inside Pi's spawn env.
17
+ */
18
+ import { spawn, type ChildProcess } from "node:child_process";
19
+ import { existsSync, readFileSync } from "node:fs";
20
+ import { join, dirname } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import { homedir } from "node:os";
23
+
24
+ const SERVER_PATH_FILE = join(homedir(), ".codelens", "server-path");
25
+
26
+ /** A resolved server spec: either `node <server.js>` or a direct launcher. */
27
+ interface ServerSpec { command: string; args: string[] }
28
+
29
+ /** Wrap a .js path with the current node binary; otherwise treat as a launcher. */
30
+ function specFor(path: string): ServerSpec {
31
+ return path.endsWith(".js") || path.endsWith(".mjs")
32
+ ? { command: process.execPath, args: [path] }
33
+ : { command: path, args: [] };
34
+ }
35
+
36
+ function resolveServer(): ServerSpec | null {
37
+ // 1. Installer-written absolute path (~/.codelens/server-path).
38
+ try {
39
+ if (existsSync(SERVER_PATH_FILE)) {
40
+ const p = readFileSync(SERVER_PATH_FILE, "utf-8").trim();
41
+ if (p && existsSync(p)) return specFor(p);
42
+ }
43
+ } catch { /* ignore */ }
44
+ // 2. Env override.
45
+ if (process.env.CODELENS_SERVER) {
46
+ const p = process.env.CODELENS_SERVER;
47
+ if (existsSync(p)) return specFor(p);
48
+ }
49
+ // 3. install.sh default location.
50
+ const appBuild = join(homedir(), ".codelens", "app", "build", "src", "server.js");
51
+ if (existsSync(appBuild)) return specFor(appBuild);
52
+ // 4. Dev: extension lives at <repo>/adapters/pi/ → <repo>/build/src/server.js.
53
+ try {
54
+ const here = dirname(fileURLToPath(import.meta.url));
55
+ const dev = join(here, "..", "..", "..", "build", "src", "server.js");
56
+ if (existsSync(dev)) return specFor(dev);
57
+ } catch { /* ignore */ }
58
+ return null;
59
+ }
60
+
61
+ // ── Minimal MCP stdio JSON-RPC client ────────────────────────
62
+
63
+ interface JsonRpcResponse { id?: number; result?: unknown; error?: { message: string }; }
64
+ interface ToolDef { name: string; description?: string; inputSchema?: Record<string, unknown>; }
65
+
66
+ class McpStdioClient {
67
+ private proc: ChildProcess | null = null;
68
+ private nextId = 1;
69
+ private pending = new Map<number, (r: JsonRpcResponse) => void>();
70
+ private buffer = "";
71
+
72
+ start(spec: ServerSpec, cwd?: string): void {
73
+ this.proc = spawn(spec.command, spec.args, {
74
+ stdio: ["pipe", "pipe", "inherit"],
75
+ env: process.env,
76
+ cwd,
77
+ });
78
+ this.proc.stdout!.setEncoding("utf-8");
79
+ this.proc.stdout!.on("data", (chunk: string) => {
80
+ this.buffer += chunk;
81
+ let nl: number;
82
+ while ((nl = this.buffer.indexOf("\n")) >= 0) {
83
+ const line = this.buffer.slice(0, nl).trim();
84
+ this.buffer = this.buffer.slice(nl + 1);
85
+ if (!line) continue;
86
+ try {
87
+ const msg = JSON.parse(line) as JsonRpcResponse;
88
+ if (msg.id !== undefined && this.pending.has(msg.id)) {
89
+ this.pending.get(msg.id)!(msg);
90
+ this.pending.delete(msg.id);
91
+ }
92
+ } catch { /* not a JSON-RPC response line */ }
93
+ }
94
+ });
95
+ }
96
+
97
+ private request(method: string, params: unknown, timeoutMs = 30000): Promise<unknown> {
98
+ const id = this.nextId++;
99
+ const frame = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
100
+ return new Promise((resolve, reject) => {
101
+ const timer = setTimeout(() => reject(new Error(`${method} timed out`)), timeoutMs);
102
+ this.pending.set(id, (msg) => { clearTimeout(timer); if (msg.error) reject(new Error(msg.error.message)); else resolve(msg.result); });
103
+ this.proc!.stdin!.write(frame);
104
+ });
105
+ }
106
+
107
+ async initialize(): Promise<void> {
108
+ await this.request("initialize", {
109
+ protocolVersion: "2025-06-18",
110
+ capabilities: {},
111
+ clientInfo: { name: "pi-codelens-bridge", version: "1.0.0" },
112
+ });
113
+ this.proc!.stdin!.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n");
114
+ }
115
+
116
+ async listTools(): Promise<ToolDef[]> {
117
+ const r = (await this.request("tools/list", {})) as { tools?: ToolDef[] };
118
+ return r.tools ?? [];
119
+ }
120
+
121
+ async callTool(name: string, args: Record<string, unknown>): Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }> {
122
+ const r = (await this.request("tools/call", { name, arguments: args }, 120000)) as { content?: Array<{ type: string; text: string }>; isError?: boolean };
123
+ return { content: r.content ?? [], isError: r.isError };
124
+ }
125
+
126
+ shutdown(): void {
127
+ try { this.proc?.stdin?.end(); } catch { /* ignore */ }
128
+ try { this.proc?.kill("SIGTERM"); } catch { /* ignore */ }
129
+ this.proc = null;
130
+ }
131
+ }
132
+
133
+ // ── Routing instructions injected into Pi's system prompt ─────
134
+ // Tells the LLM to prefer the cl_* tools over raw grep/find/read for code
135
+ // discovery. Only injected once the bridge has registered the tools.
136
+ const ROUTING = `
137
+ ## CodeLens (code context index) — routing
138
+
139
+ A local branch-scoped code index is available via these tools. For codebase
140
+ DISCOVERY, prefer them over grep/find/read:
141
+ - cl_current — repo/branch/index status (call if unsure whether index is ready)
142
+ - cl_search — ranked semantic+lexical search over the current branch index;
143
+ returns compact handles (path + line range + score). Pass
144
+ contentType:"code" for source only. Auto-builds the index on
145
+ first use, so you can call it directly.
146
+ - cl_related — graph neighbors of a file/symbol (tests, importers, callers)
147
+ - cl_expand — exact current file content by path/range (reads disk, never stale)
148
+
149
+ Rules:
150
+ - Use cl_search / cl_related BEFORE grep, find, or reading multiple files to
151
+ locate code. It keeps your context lean and is branch-aware.
152
+ - Use raw read ONLY for the exact file you are about to edit or verify, not for
153
+ discovery.
154
+ - After \`git checkout\`, results auto-scope to the new branch; call cl_current
155
+ if you want to confirm index status.
156
+ - If cl_search returns nothing relevant, fall back to grep/read as usual.
157
+
158
+ You do NOT need the user's permission to use these tools — they are the
159
+ preferred discovery path.`;
160
+
161
+ // ── Extension entry ──────────────────────────────────────────
162
+
163
+ export default function (pi: import("@earendil-works/pi-coding-agent").ExtensionAPI) {
164
+ const spec = resolveServer();
165
+ if (!spec) {
166
+ process.stderr.write(`[codelens] server not found. Run \`codelens install\` (writes ~/.codelens/server-path), or set CODELENS_SERVER=<path>/build/src/server.js, or run install.sh.\n`);
167
+ return;
168
+ }
169
+
170
+ // Mutable bridge holder. Tools' execute() closures reference `client` and
171
+ // `bridged` directly, so reassigning them on a project switch re-points every
172
+ // tool at the new server child (no re-registration needed).
173
+ let client: McpStdioClient | null = null;
174
+ let bridged = false;
175
+ let currentCwd = process.cwd();
176
+ let toolsRegistered = false;
177
+
178
+ async function boot(cwd: string): Promise<void> {
179
+ try { client?.shutdown(); } catch { /* ignore */ }
180
+ const c = new McpStdioClient();
181
+ c.start(spec, cwd); // spawn the server with cwd = the current project
182
+ await c.initialize();
183
+ const tools = await c.listTools();
184
+ if (!toolsRegistered) {
185
+ for (const tool of tools) {
186
+ pi.registerTool({
187
+ name: tool.name,
188
+ label: tool.name,
189
+ description: tool.description ?? "",
190
+ parameters: tool.inputSchema ?? { type: "object", properties: {} },
191
+ async execute(_id, params) {
192
+ if (!client) throw new Error("codelens bridge not ready");
193
+ const result = await client.callTool(tool.name, (params ?? {}) as Record<string, unknown>);
194
+ const text = (result.content ?? []).filter((x) => x?.type === "text").map((x) => x.text).join("\n");
195
+ if (result.isError) throw new Error(text || `${tool.name} returned an error`);
196
+ return { content: [{ type: "text", text }], details: {} };
197
+ },
198
+ });
199
+ }
200
+ toolsRegistered = true;
201
+ }
202
+ client = c;
203
+ currentCwd = cwd;
204
+ bridged = true;
205
+ }
206
+
207
+ boot(currentCwd).catch((err: unknown) => {
208
+ process.stderr.write(`[codelens] MCP bridge failed: ${err instanceof Error ? err.message : String(err)}\n`);
209
+ bridged = false;
210
+ });
211
+
212
+ // Each turn: if Pi's cwd changed (project switch), respawn the server into
213
+ // the new project so cl_search queries the right repo. Then inject routing.
214
+ pi.on("before_agent_start", async (event: { systemPrompt?: string; systemPromptOptions?: { cwd?: string } }) => {
215
+ const cwd = event.systemPromptOptions?.cwd ?? process.cwd();
216
+ if (bridged && cwd && cwd !== currentCwd) {
217
+ try { await boot(cwd); } catch { /* keep old bridge on respawn failure */ }
218
+ }
219
+ if (!bridged) return {};
220
+ const base = event.systemPrompt ?? "";
221
+ if (base.includes("CodeLens (code context index)")) return {};
222
+ return { systemPrompt: base + "\n" + ROUTING };
223
+ });
224
+
225
+ // ── Slash commands (discoverable via `/`) ────────────────────
226
+ const call = async (name: string, args: Record<string, unknown> = {}): Promise<string> => {
227
+ if (!client || !bridged) return "codelens bridge not ready yet — wait a moment or /reload.";
228
+ const r = await client.callTool(name, args);
229
+ return (r.content ?? []).filter((c) => c?.type === "text").map((c) => c.text).join("\n");
230
+ };
231
+ const fmtBytes = (n: number) => (n < 1024 ? `${n}B` : n < 1048576 ? `${(n/1024).toFixed(1)}KB` : `${(n/1048576).toFixed(2)}MB`);
232
+
233
+ pi.registerCommand("codelens-usage", {
234
+ description: "Show codelens tool usage stats (global, across repos)",
235
+ handler: async (_args, ctx) => {
236
+ const text = await call("cl_usage");
237
+ try {
238
+ const u = JSON.parse(text);
239
+ const t = u.totals ?? { calls: 0, bytes_served: 0, bytes_saved: 0 };
240
+ const lines: string[] = [];
241
+ lines.push(`cl_* usage (global) — calls: ${t.calls} | served: ${fmtBytes(t.bytes_served)} | saved(est): ${fmtBytes(t.bytes_saved)}`);
242
+ lines.push("Per tool:");
243
+ for (const row of (u.perTool ?? [])) lines.push(` ${row.tool.padEnd(14)} calls=${row.calls} served=${fmtBytes(row.bytes_served)} saved=${fmtBytes(row.bytes_saved)}`);
244
+ if (!(u.perTool ?? []).length) lines.push(" (no calls recorded yet)");
245
+ lines.push("Per repo:");
246
+ for (const r of (u.perRepo ?? [])) lines.push(` ${r.repo_id.slice(0,8)} calls=${r.calls} saved=${fmtBytes(r.bytes_saved)}`);
247
+ if (!(u.perRepo ?? []).length) lines.push(" (none)");
248
+ ctx.ui.notify(lines.join("\n"), "info");
249
+ } catch { ctx.ui.notify(text.slice(0, 800), "info"); }
250
+ },
251
+ });
252
+
253
+ pi.registerCommand("codelens-stats", {
254
+ description: "Show current index statistics (file/symbol/chunk/edge counts)",
255
+ handler: async (_args, ctx) => { ctx.ui.notify((await call("cl_stats")).slice(0, 800), "info"); },
256
+ });
257
+
258
+ pi.registerCommand("codelens-doctor", {
259
+ description: "Run a codelens health check",
260
+ handler: async (_args, ctx) => { ctx.ui.notify((await call("cl_doctor")).slice(0, 800), "info"); },
261
+ });
262
+
263
+ pi.registerCommand("codelens-search", {
264
+ description: "Search the current branch index: /codelens-search <query>",
265
+ handler: async (args, ctx) => {
266
+ const query = (args ?? "").trim();
267
+ if (!query) { ctx.ui.notify("Usage: /codelens-search <query>", "warn"); return; }
268
+ const text = await call("cl_search", { query, limit: 5 });
269
+ try {
270
+ const r = JSON.parse(text);
271
+ const lines = (r.results ?? []).map((h: { score: number; path: string; startLine: number; endLine: number; why?: string[] }) =>
272
+ `${h.score.toFixed(3)} ${h.path}:${h.startLine}-${h.endLine} [${(h.why ?? []).join(",")}]`);
273
+ ctx.ui.notify(lines.length ? lines.join("\n") : "no results", "info");
274
+ } catch { ctx.ui.notify(text.slice(0, 800), "info"); }
275
+ },
276
+ });
277
+
278
+ // Clean up the child on Pi shutdown.
279
+ pi.on("shutdown", () => { try { client?.shutdown(); } catch { /* ignore */ } });
280
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "codelens",
3
+ "version": "1.0.0",
4
+ "type": "mcp",
5
+ "mcp": {
6
+ "command": "npx",
7
+ "args": ["-y", "@fodx/codelens"]
8
+ },
9
+ "description": "Branch-aware code search & relation graph (MCP server + CLI). Routes agents to cl_search/cl_related/cl_expand instead of raw grep/read."
10
+ }