@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.
- package/README.md +212 -0
- package/adapters/pi/codelens.extension.ts +280 -0
- package/adapters/pi/extension.json +10 -0
- package/build/src/cli.js +265 -0
- package/build/src/cli.js.map +1 -0
- package/build/src/context/schema.sql +23 -0
- package/build/src/context/store.js +91 -0
- package/build/src/context/store.js.map +1 -0
- package/build/src/db/db.js +65 -0
- package/build/src/db/db.js.map +1 -0
- package/build/src/db/migrations.js +88 -0
- package/build/src/db/migrations.js.map +1 -0
- package/build/src/db/schema.js +11 -0
- package/build/src/db/schema.js.map +1 -0
- package/build/src/db/schema.sql +111 -0
- package/build/src/git/scope.js +68 -0
- package/build/src/git/scope.js.map +1 -0
- package/build/src/graph/edges.js +76 -0
- package/build/src/graph/edges.js.map +1 -0
- package/build/src/graph/grammars.js +57 -0
- package/build/src/graph/grammars.js.map +1 -0
- package/build/src/graph/query.js +52 -0
- package/build/src/graph/query.js.map +1 -0
- package/build/src/graph/resolve.js +68 -0
- package/build/src/graph/resolve.js.map +1 -0
- package/build/src/graph/symbols.js +84 -0
- package/build/src/graph/symbols.js.map +1 -0
- package/build/src/graph/tests.js +73 -0
- package/build/src/graph/tests.js.map +1 -0
- package/build/src/index/autoprune.js +29 -0
- package/build/src/index/autoprune.js.map +1 -0
- package/build/src/index/deny.js +40 -0
- package/build/src/index/deny.js.map +1 -0
- package/build/src/index/freshness.js +60 -0
- package/build/src/index/freshness.js.map +1 -0
- package/build/src/index/fts.js +125 -0
- package/build/src/index/fts.js.map +1 -0
- package/build/src/index/identity.js +21 -0
- package/build/src/index/identity.js.map +1 -0
- package/build/src/index/indexer.js +32 -0
- package/build/src/index/indexer.js.map +1 -0
- package/build/src/index/manager.js +48 -0
- package/build/src/index/manager.js.map +1 -0
- package/build/src/index/queue.js +47 -0
- package/build/src/index/queue.js.map +1 -0
- package/build/src/index/recovery.js +51 -0
- package/build/src/index/recovery.js.map +1 -0
- package/build/src/index/reindex.js +70 -0
- package/build/src/index/reindex.js.map +1 -0
- package/build/src/index/scanner.js +147 -0
- package/build/src/index/scanner.js.map +1 -0
- package/build/src/index/ttl.js +87 -0
- package/build/src/index/ttl.js.map +1 -0
- package/build/src/index/watcher.js +74 -0
- package/build/src/index/watcher.js.map +1 -0
- package/build/src/installer/agents.js +440 -0
- package/build/src/installer/agents.js.map +1 -0
- package/build/src/obs/doctor.js +53 -0
- package/build/src/obs/doctor.js.map +1 -0
- package/build/src/obs/stats.js +28 -0
- package/build/src/obs/stats.js.map +1 -0
- package/build/src/obs/usage.js +136 -0
- package/build/src/obs/usage.js.map +1 -0
- package/build/src/search/rank.js +70 -0
- package/build/src/search/rank.js.map +1 -0
- package/build/src/search/snippet.js +66 -0
- package/build/src/search/snippet.js.map +1 -0
- package/build/src/server.js +126 -0
- package/build/src/server.js.map +1 -0
- package/build/src/tools/current.js +32 -0
- package/build/src/tools/current.js.map +1 -0
- package/build/src/tools/expand.js +54 -0
- package/build/src/tools/expand.js.map +1 -0
- package/build/src/tools/prune.js +46 -0
- package/build/src/tools/prune.js.map +1 -0
- package/build/src/tools/refresh.js +14 -0
- package/build/src/tools/refresh.js.map +1 -0
- package/build/src/tools/registry.js +176 -0
- package/build/src/tools/registry.js.map +1 -0
- package/build/src/tools/related.js +28 -0
- package/build/src/tools/related.js.map +1 -0
- package/build/src/tools/save.js +15 -0
- package/build/src/tools/save.js.map +1 -0
- package/build/src/tools/search.js +124 -0
- package/build/src/tools/search.js.map +1 -0
- package/build/src/upgrade.js +74 -0
- package/build/src/upgrade.js.map +1 -0
- package/build/src/util/hash.js +15 -0
- package/build/src/util/hash.js.map +1 -0
- package/build/src/util/paths.js +67 -0
- package/build/src/util/paths.js.map +1 -0
- package/build/src/version.js +27 -0
- package/build/src/version.js.map +1 -0
- package/docs/agent-guide.md +47 -0
- package/docs/codelens-preview.png +0 -0
- package/docs/routing.md +59 -0
- package/docs/tools.md +53 -0
- package/package.json +103 -0
package/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# CodeLens
|
|
2
|
+
|
|
3
|
+

|
|
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
|
+
}
|