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.
Files changed (50) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +40 -0
  3. package/dist/commands/audit.js +28 -260
  4. package/dist/commands/context.js +42 -26
  5. package/dist/commands/dead.js +37 -35
  6. package/dist/commands/doctor.js +280 -10
  7. package/dist/commands/extract.js +79 -49
  8. package/dist/commands/impact.js +74 -20
  9. package/dist/commands/log.js +25 -25
  10. package/dist/commands/mcp.js +639 -475
  11. package/dist/commands/peek.js +49 -47
  12. package/dist/commands/project.js +29 -107
  13. package/dist/commands/related.js +43 -52
  14. package/dist/commands/search-run.js +23 -8
  15. package/dist/commands/search.js +7 -2
  16. package/dist/commands/similar.js +50 -76
  17. package/dist/commands/skeleton.js +137 -83
  18. package/dist/commands/status.js +90 -25
  19. package/dist/commands/surprises.js +73 -16
  20. package/dist/commands/symbols.js +58 -77
  21. package/dist/commands/test-find.js +51 -17
  22. package/dist/commands/trace.js +26 -11
  23. package/dist/commands/watch.js +1 -1
  24. package/dist/config.js +8 -2
  25. package/dist/lib/analysis/surprising-connections.js +135 -15
  26. package/dist/lib/daemon/daemon.js +157 -13
  27. package/dist/lib/daemon/graph-handler.js +859 -0
  28. package/dist/lib/daemon/ipc-handler.js +36 -4
  29. package/dist/lib/daemon/read-verbs.js +156 -0
  30. package/dist/lib/daemon/rows-handler.js +665 -0
  31. package/dist/lib/daemon/search-handler.js +7 -4
  32. package/dist/lib/daemon/vector-handler.js +199 -0
  33. package/dist/lib/daemon/watch-leases.js +179 -0
  34. package/dist/lib/index/batch-processor.js +7 -1
  35. package/dist/lib/index/watcher.js +4 -0
  36. package/dist/lib/search/pagerank.js +80 -19
  37. package/dist/lib/store/vector-db.js +35 -1
  38. package/dist/lib/utils/async-semaphore.js +141 -0
  39. package/dist/lib/utils/daemon-client.js +11 -0
  40. package/dist/lib/utils/process-footprint.js +51 -0
  41. package/dist/lib/utils/store-access.js +381 -0
  42. package/dist/lib/utils/tests-footer.js +19 -2
  43. package/dist/lib/utils/watcher-launcher.js +46 -19
  44. package/dist/lib/workers/pool.js +72 -2
  45. package/package.json +1 -1
  46. package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
  47. package/plugins/grepmax/hooks/cwd-changed.js +19 -36
  48. package/plugins/grepmax/hooks/start.js +15 -19
  49. package/plugins/grepmax/hooks/stop.js +13 -4
  50. 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.watchProject(root);
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.unwatchProject(root);
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
- default:
377
- return { ok: false, error: `unknown command: ${cmd.cmd}` };
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
+ // ---------------------------------------------------------------------------