grepmax 0.26.27 → 0.26.30
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +40 -0
- package/dist/commands/audit.js +28 -260
- package/dist/commands/context.js +42 -26
- package/dist/commands/dead.js +37 -35
- package/dist/commands/doctor.js +280 -10
- package/dist/commands/extract.js +79 -49
- package/dist/commands/impact.js +74 -20
- package/dist/commands/log.js +25 -25
- package/dist/commands/mcp.js +639 -475
- package/dist/commands/peek.js +49 -47
- package/dist/commands/project.js +29 -107
- package/dist/commands/related.js +43 -52
- package/dist/commands/search-run.js +23 -8
- package/dist/commands/search.js +7 -2
- package/dist/commands/similar.js +50 -76
- package/dist/commands/skeleton.js +137 -83
- package/dist/commands/status.js +90 -25
- package/dist/commands/surprises.js +73 -16
- package/dist/commands/symbols.js +58 -77
- package/dist/commands/test-find.js +51 -17
- package/dist/commands/trace.js +26 -11
- package/dist/commands/watch.js +1 -1
- package/dist/config.js +8 -2
- package/dist/lib/analysis/surprising-connections.js +135 -15
- package/dist/lib/daemon/daemon.js +157 -13
- package/dist/lib/daemon/graph-handler.js +859 -0
- package/dist/lib/daemon/ipc-handler.js +36 -4
- package/dist/lib/daemon/read-verbs.js +156 -0
- package/dist/lib/daemon/rows-handler.js +665 -0
- package/dist/lib/daemon/search-handler.js +7 -4
- package/dist/lib/daemon/vector-handler.js +199 -0
- package/dist/lib/daemon/watch-leases.js +179 -0
- package/dist/lib/index/batch-processor.js +7 -1
- package/dist/lib/index/watcher.js +4 -0
- package/dist/lib/search/pagerank.js +80 -19
- package/dist/lib/store/vector-db.js +35 -1
- package/dist/lib/utils/async-semaphore.js +141 -0
- package/dist/lib/utils/daemon-client.js +11 -0
- package/dist/lib/utils/process-footprint.js +51 -0
- package/dist/lib/utils/store-access.js +381 -0
- package/dist/lib/utils/tests-footer.js +19 -2
- package/dist/lib/utils/watcher-launcher.js +46 -19
- package/dist/lib/workers/pool.js +72 -2
- package/package.json +1 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
- package/plugins/grepmax/hooks/cwd-changed.js +19 -36
- package/plugins/grepmax/hooks/start.js +15 -19
- package/plugins/grepmax/hooks/stop.js +13 -4
- package/plugins/grepmax/hooks/watch-lease.js +163 -0
|
@@ -53,6 +53,7 @@ const logger_1 = require("../utils/logger");
|
|
|
53
53
|
const operation_coordinator_1 = require("../utils/operation-coordinator");
|
|
54
54
|
const path_containment_1 = require("../utils/path-containment");
|
|
55
55
|
const project_registry_1 = require("../utils/project-registry");
|
|
56
|
+
const read_verbs_1 = require("./read-verbs");
|
|
56
57
|
const DAEMON_VERSION = (() => {
|
|
57
58
|
try {
|
|
58
59
|
return JSON.parse(fs.readFileSync(path.join(__dirname, "../../../package.json"), "utf-8")).version;
|
|
@@ -148,20 +149,29 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
148
149
|
rebuildPending: daemon.hasUnfinishedRebuild(),
|
|
149
150
|
capabilities: {
|
|
150
151
|
exclusiveGenerationRebuild: exports.EXCLUSIVE_GENERATION_REBUILD_PROTOCOL,
|
|
152
|
+
readVerbs: read_verbs_1.READ_VERBS_PROTOCOL,
|
|
153
|
+
// `watch`/`unwatch` accept holder/pid/ttlMs. A daemon without this
|
|
154
|
+
// treats `unwatch` as unwatch-for-everyone, so clients releasing a
|
|
155
|
+
// single lease must check it first.
|
|
156
|
+
watchLeases: 1,
|
|
151
157
|
},
|
|
152
158
|
};
|
|
153
159
|
case "watch": {
|
|
154
160
|
const root = String(cmd.root || "");
|
|
155
161
|
if (!root)
|
|
156
162
|
return { ok: false, error: "missing root" };
|
|
157
|
-
yield daemon.
|
|
163
|
+
yield daemon.requestWatch(root, {
|
|
164
|
+
holder: typeof cmd.holder === "string" ? cmd.holder : undefined,
|
|
165
|
+
pid: typeof cmd.pid === "number" ? cmd.pid : undefined,
|
|
166
|
+
ttlMs: typeof cmd.ttlMs === "number" ? cmd.ttlMs : undefined,
|
|
167
|
+
});
|
|
158
168
|
return { ok: true, pid: process.pid };
|
|
159
169
|
}
|
|
160
170
|
case "unwatch": {
|
|
161
171
|
const root = String(cmd.root || "");
|
|
162
172
|
if (!root)
|
|
163
173
|
return { ok: false, error: "missing root" };
|
|
164
|
-
yield daemon.
|
|
174
|
+
yield daemon.releaseWatch(root, typeof cmd.holder === "string" ? cmd.holder : undefined);
|
|
165
175
|
return { ok: true };
|
|
166
176
|
}
|
|
167
177
|
case "status":
|
|
@@ -170,6 +180,7 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
170
180
|
pid: process.pid,
|
|
171
181
|
uptime: daemon.uptime(),
|
|
172
182
|
projects: daemon.listProjects(),
|
|
183
|
+
leases: daemon.listWatchLeases(),
|
|
173
184
|
diskPressure: daemon.getDiskPressure(),
|
|
174
185
|
mlx: daemon.getMlxStatus(),
|
|
175
186
|
};
|
|
@@ -302,6 +313,7 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
302
313
|
const root = String(cmd.root || "");
|
|
303
314
|
if (!root)
|
|
304
315
|
return { ok: false, error: "missing root" };
|
|
316
|
+
daemon.leaseForCli(root);
|
|
305
317
|
startStreaming(conn, () => daemon.addProject(root, conn));
|
|
306
318
|
return null;
|
|
307
319
|
}
|
|
@@ -309,6 +321,7 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
309
321
|
const root = String(cmd.root || "");
|
|
310
322
|
if (!root)
|
|
311
323
|
return { ok: false, error: "missing root" };
|
|
324
|
+
daemon.leaseForCli(root);
|
|
312
325
|
startStreaming(conn, () => daemon.ensureProject(root, conn));
|
|
313
326
|
return null;
|
|
314
327
|
}
|
|
@@ -316,6 +329,7 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
316
329
|
const root = String(cmd.root || "");
|
|
317
330
|
if (!root)
|
|
318
331
|
return { ok: false, error: "missing root" };
|
|
332
|
+
daemon.leaseForCli(root);
|
|
319
333
|
startStreaming(conn, () => daemon.indexProject(root, conn, {
|
|
320
334
|
reset: !!cmd.reset,
|
|
321
335
|
dryRun: !!cmd.dryRun,
|
|
@@ -373,8 +387,26 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
373
387
|
return yield daemon.llmStop();
|
|
374
388
|
case "llm-status":
|
|
375
389
|
return daemon.llmStatus();
|
|
376
|
-
|
|
377
|
-
|
|
390
|
+
// --- Read verbs (registry in read-verbs.ts) ---
|
|
391
|
+
default: {
|
|
392
|
+
const verb = (0, read_verbs_1.getReadVerb)(cmd.cmd);
|
|
393
|
+
if (!verb)
|
|
394
|
+
return { ok: false, error: `unknown command: ${cmd.cmd}` };
|
|
395
|
+
// Same abort wiring as `search`: a client ctrl-C closes the socket,
|
|
396
|
+
// which cancels the in-flight read instead of letting it run on.
|
|
397
|
+
const ac = new AbortController();
|
|
398
|
+
const onClose = () => ac.abort();
|
|
399
|
+
conn.on("close", onClose);
|
|
400
|
+
const name = String(cmd.cmd);
|
|
401
|
+
try {
|
|
402
|
+
// Heavy verbs queue on a global gate before admission; see
|
|
403
|
+
// HEAVY_READ_VERBS in read-verbs.ts.
|
|
404
|
+
return yield (0, read_verbs_1.runReadVerb)(name, ac.signal, () => daemon.runSharedOperation(name, ac.signal, (signal) => verb(cmd, { daemon, conn, signal })));
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
conn.off("close", onClose);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
378
410
|
}
|
|
379
411
|
}
|
|
380
412
|
catch (err) {
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Registry for daemon read verbs.
|
|
4
|
+
*
|
|
5
|
+
* The daemon is meant to be the only process that opens the store, so every
|
|
6
|
+
* read-only CLI command becomes a thin IPC client. Rather than growing one more
|
|
7
|
+
* `case` in `ipc-handler.ts` per verb (and making that file a merge magnet),
|
|
8
|
+
* verbs register here and `ipc-handler`'s `default:` looks them up before it
|
|
9
|
+
* reports `unknown command`.
|
|
10
|
+
*
|
|
11
|
+
* Each verb runs through `Daemon.runSharedOperation(verbName, ...)`, so an
|
|
12
|
+
* exclusive rebuild answers DAEMON_BUSY naming the verb instead of racing it,
|
|
13
|
+
* and its abort signal is bound to socket close exactly like `search`.
|
|
14
|
+
*
|
|
15
|
+
* Verbs are request/response only: one JSON line in, one JSON line out. Anything
|
|
16
|
+
* that needs progress belongs in the streaming command set instead.
|
|
17
|
+
*
|
|
18
|
+
* Handlers live beside `search-handler.ts` (`graph-handler.ts`,
|
|
19
|
+
* `rows-handler.ts`, `vector-handler.ts`), each taking a narrow deps interface
|
|
20
|
+
* so `ipc-handler` tests can drive them with a fake `Daemon`.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.heavyReadGate = exports.DEFAULT_HEAVY_READ_CONCURRENCY = exports.HEAVY_READ_VERBS = exports.READ_VERBS_PROTOCOL = void 0;
|
|
24
|
+
exports.registerReadVerbs = registerReadVerbs;
|
|
25
|
+
exports.getReadVerb = getReadVerb;
|
|
26
|
+
exports.readVerbNames = readVerbNames;
|
|
27
|
+
exports.resolveHeavyReadConcurrency = resolveHeavyReadConcurrency;
|
|
28
|
+
exports.isHeavyReadVerb = isHeavyReadVerb;
|
|
29
|
+
exports.runReadVerb = runReadVerb;
|
|
30
|
+
exports.clearReadVerbs = clearReadVerbs;
|
|
31
|
+
exports.registerGraphVerbs = registerGraphVerbs;
|
|
32
|
+
const async_semaphore_1 = require("../utils/async-semaphore");
|
|
33
|
+
const graph_handler_1 = require("./graph-handler");
|
|
34
|
+
/** Bumped when the verb wire shapes change incompatibly. */
|
|
35
|
+
exports.READ_VERBS_PROTOCOL = 1;
|
|
36
|
+
const registry = new Map();
|
|
37
|
+
/**
|
|
38
|
+
* Register one or more verbs. Re-registering a name replaces it, which keeps
|
|
39
|
+
* hot-reload and test setup simple; duplicate names across packages are a bug,
|
|
40
|
+
* so the collision is logged when it happens with a different handler.
|
|
41
|
+
*/
|
|
42
|
+
function registerReadVerbs(entries) {
|
|
43
|
+
const iterable = entries[Symbol.iterator];
|
|
44
|
+
const pairs = typeof iterable === "function"
|
|
45
|
+
? entries
|
|
46
|
+
: Object.entries(entries);
|
|
47
|
+
for (const [name, handler] of pairs) {
|
|
48
|
+
registry.set(name, handler);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function getReadVerb(name) {
|
|
52
|
+
return typeof name === "string" ? registry.get(name) : undefined;
|
|
53
|
+
}
|
|
54
|
+
function readVerbNames() {
|
|
55
|
+
return [...registry.keys()].sort();
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Heavy-verb admission.
|
|
59
|
+
//
|
|
60
|
+
// `runSharedOperation` only tracks a verb; it never limits how many run. These
|
|
61
|
+
// verbs each scan a large slice of the table (tens to hundreds of thousands of
|
|
62
|
+
// rows, or one vector search per sampled anchor), and their Arrow buffers sit
|
|
63
|
+
// in native memory until the answer is built. With one MCP session per Claude
|
|
64
|
+
// Code window, N sessions asking at once meant N such scans resident together.
|
|
65
|
+
// They now share a small global semaphore; everything else stays unbounded,
|
|
66
|
+
// because point lookups (`graph.resolve`, `rows.locate`, ...) are cheap and
|
|
67
|
+
// must not queue behind an audit.
|
|
68
|
+
//
|
|
69
|
+
// A queued request waits *before* `runSharedOperation` admits it (see
|
|
70
|
+
// `runReadVerb`), so an exclusive rebuild does not have to sit behind scans
|
|
71
|
+
// that have not started yet, and a client that disconnects while queued
|
|
72
|
+
// leaves without ever touching the store.
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
exports.HEAVY_READ_VERBS = new Set([
|
|
75
|
+
"graph.audit", // up to AUDIT_ROW_LIMIT rows with symbol arrays
|
|
76
|
+
"graph.dead", // whole caller set
|
|
77
|
+
"graph.risk", // up to MAX_RISK_SYMBOLS x three content scans
|
|
78
|
+
"graph.subgraph", // per-file symbol scans over up to MAX_SUBGRAPH_FILES
|
|
79
|
+
"graph.trace", // multi-hop caller/callee expansion
|
|
80
|
+
"rows.project", // up to 200k rows with two symbol-array columns
|
|
81
|
+
"vector.similar", // vector search plus content
|
|
82
|
+
"vector.surprises", // up to 100k-row scan plus one search per anchor
|
|
83
|
+
]);
|
|
84
|
+
exports.DEFAULT_HEAVY_READ_CONCURRENCY = 2;
|
|
85
|
+
/** `GMAX_HEAVY_READ_CONCURRENCY`, or the default when unset or not a positive integer. */
|
|
86
|
+
function resolveHeavyReadConcurrency(raw = process.env.GMAX_HEAVY_READ_CONCURRENCY) {
|
|
87
|
+
const n = Number(raw);
|
|
88
|
+
return raw !== undefined && Number.isInteger(n) && n >= 1
|
|
89
|
+
? n
|
|
90
|
+
: exports.DEFAULT_HEAVY_READ_CONCURRENCY;
|
|
91
|
+
}
|
|
92
|
+
exports.heavyReadGate = new async_semaphore_1.AsyncSemaphore(resolveHeavyReadConcurrency());
|
|
93
|
+
function isHeavyReadVerb(name) {
|
|
94
|
+
return typeof name === "string" && exports.HEAVY_READ_VERBS.has(name);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Admission wrapper `ipc-handler` puts around a verb's shared operation: heavy
|
|
98
|
+
* verbs wait for a gate slot first (abortable through `signal`), light verbs
|
|
99
|
+
* run straight through.
|
|
100
|
+
*/
|
|
101
|
+
function runReadVerb(name, signal, run, gate = exports.heavyReadGate) {
|
|
102
|
+
return isHeavyReadVerb(name) ? gate.run(signal, run) : run();
|
|
103
|
+
}
|
|
104
|
+
/** Test hook: drop every registration. */
|
|
105
|
+
function clearReadVerbs() {
|
|
106
|
+
registry.clear();
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// graph verbs (WP-B) — graph.resolve, graph.tests, graph.dependents,
|
|
110
|
+
// graph.trace, graph.peek, graph.dead, graph.audit; handlers in
|
|
111
|
+
// graph-handler.ts.
|
|
112
|
+
//
|
|
113
|
+
// `graph.peek` is not in the plan's table: `peek` needs its defining-chunk rows
|
|
114
|
+
// and its `is_exported`/`end_line` metadata alongside the graph, and those
|
|
115
|
+
// selects would otherwise land in WP-C's `rows.locate`. Keeping them in one
|
|
116
|
+
// composite verb costs one round trip instead of four and keeps peek's
|
|
117
|
+
// rendering (and its `fs.readFileSync` signature extraction) unchanged.
|
|
118
|
+
//
|
|
119
|
+
// WP-D added four more for the MCP tools that have no CLI twin:
|
|
120
|
+
// `graph.neighbors`, `graph.paths` and `graph.subgraph` are the GraphBuilder
|
|
121
|
+
// traversal primitives behind `get_neighbors` / `find_paths` /
|
|
122
|
+
// `subgraph_for_files` — distinct entry points with their own hop and size
|
|
123
|
+
// bounds, which is why they are separate verbs rather than options on
|
|
124
|
+
// `graph.trace`. `graph.risk` is the store half of `review_risk`: the git half
|
|
125
|
+
// (diff, churn) stays with the caller, exactly as `extract` keeps its body read.
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
/**
|
|
128
|
+
* Called once from the Daemon constructor rather than at module load, so that
|
|
129
|
+
* importing this registry (as `ipc-handler` and its tests do) does not populate
|
|
130
|
+
* it behind a test's back.
|
|
131
|
+
*/
|
|
132
|
+
function registerGraphVerbs() {
|
|
133
|
+
registerReadVerbs((0, graph_handler_1.createGraphVerbs)((ctx) => ctx.daemon.storeReadDeps()));
|
|
134
|
+
}
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// rows verbs (WP-C) — rows.symbols, rows.project, rows.locate, rows.skeleton.
|
|
137
|
+
// Handlers and their `registerRowsVerbs()` live in rows-handler.ts;
|
|
138
|
+
// `Daemon.start()` calls it, because registering at module scope would make the
|
|
139
|
+
// registry non-empty on import and break the assertions in
|
|
140
|
+
// tests/ipc-read-verbs.test.ts.
|
|
141
|
+
//
|
|
142
|
+
// `rows.project` is not in the plan's table: `symbols` and `project` were to
|
|
143
|
+
// share one row-proxy verb. `project.ts` selects up to 200 000 rows with two
|
|
144
|
+
// symbol-array columns, and the platform project holds 266 753 chunks, so the
|
|
145
|
+
// proxy shape would push hundreds of megabytes through the daemon's event loop
|
|
146
|
+
// for a summary that fits in a few kilobytes. Both verbs return the finished
|
|
147
|
+
// aggregate instead — see the rows-handler.ts header.
|
|
148
|
+
//
|
|
149
|
+
// WP-C also shipped `rows.tests` for `extract`'s footer; WP-D removed it. It
|
|
150
|
+
// wrapped the same `findTests` call `graph.tests` does, and `extract` now sends
|
|
151
|
+
// that verb, so the duplicate wire shape is gone before any daemon served it.
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// vector verbs (WP-C) — vector.similar, vector.surprises; handlers and
|
|
155
|
+
// `registerVectorVerbs()` in vector-handler.ts, called from `Daemon.start()`.
|
|
156
|
+
// ---------------------------------------------------------------------------
|