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
@@ -10,7 +10,7 @@
10
10
  "name": "grepmax",
11
11
  "source": "./plugins/grepmax",
12
12
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
13
- "version": "0.26.27",
13
+ "version": "0.26.30",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
package/README.md CHANGED
@@ -217,6 +217,45 @@ gmax status # See all projects + watcher status
217
217
 
218
218
  The daemon auto-starts when you run `gmax add`, `gmax index`, `gmax remove`, `gmax summarize`, or `gmax mcp`. It shuts down after 30 minutes of inactivity. File-change batches are processed concurrently while reserving worker capacity for searches; LanceDB compaction runs after writes rather than on every maintenance tick.
219
219
 
220
+ ## Running under the Claude Code sandbox
221
+
222
+ Claude Code's Bash sandbox (the default on macOS) allows writes only under the working directory,
223
+ the added directories, and the session temp dir, and blocks every Unix socket unless it is listed.
224
+ Child processes inherit the profile, so a gmax command run from an agent shell inherits it too —
225
+ and gmax needs both kinds of access. A read command either asks the daemon over
226
+ `~/.gmax/daemon.sock`, or, with no daemon running, opens the shared store itself; opening the store
227
+ takes a lease, which means a `mkdir` under `~/.gmax`. A "read" is a writer as far as the filesystem
228
+ is concerned.
229
+
230
+ Two settings keys cover it (`~/.claude/settings.json`, or the project's `.claude/settings.json`):
231
+
232
+ ```json
233
+ "sandbox": {
234
+ "network": { "allowUnixSockets": ["~/.gmax/daemon.sock"] },
235
+ "filesystem": { "allowWrite": ["~/.gmax"] }
236
+ }
237
+ ```
238
+
239
+ | Key | What it unlocks |
240
+ | --- | --- |
241
+ | `sandbox.network.allowUnixSockets` | Every read command — search, `test`, `impact`, `trace`, `peek`, `status` — via the daemon. This is the one that matters day to day. |
242
+ | `sandbox.filesystem.allowWrite` | The in-process fallback, used only when no daemon is running (autostart disabled, CI). With a daemon up, nothing needs it. |
243
+
244
+ Without them gmax refuses rather than failing with a bare `EPERM`, exits `2`, and prints the key to add:
245
+
246
+ ```
247
+ gmax: cannot reach the daemon socket from this sandbox. Add to Claude Code settings: "sandbox": {"network": {"allowUnixSockets": ["~/.gmax/daemon.sock"]}}
248
+ gmax: cannot open the store from this sandbox. Add to Claude Code settings: "sandbox": {"filesystem": {"allowWrite": ["~/.gmax"]}}
249
+ ```
250
+
251
+ `gmax doctor` reports the same thing ahead of time as `WARN Claude Code sandbox` whenever your
252
+ settings enable the sandbox without allowing the socket or the store directory. It never edits
253
+ Claude Code settings, `--fix` included — they are not gmax's files to write.
254
+
255
+ On Linux the per-socket list does not exist; the only option is
256
+ `"network": { "allowAllUnixSockets": true }`. See
257
+ [`docs/known-limitations.md`](docs/known-limitations.md).
258
+
220
259
  ## Local LLM (optional)
221
260
 
222
261
  gmax can use a local LLM (via llama-server) for agentic codebase investigation. This is entirely opt-in and disabled by default — gmax works fine without it.
@@ -413,6 +452,7 @@ whether you need to act.
413
452
  | `Optimize panicked ... inverted/builder.rs` | Upstream Lance bug in incremental FTS merge ([lance#8310](https://github.com/lance-format/lance/issues/8310)). gmax rebuilds the index and retries automatically; no data loss, no action needed. |
414
453
  | `disabling auto-rebuild until an optimize succeeds` | Transient, not a wedge. The next successful optimize clears it. Search stays available. |
415
454
  | `ANN: vector index not built` | Normal — exact search is the default. |
455
+ | `cannot reach the daemon socket from this sandbox` (exit 2) | The shell is sandboxed. Add the two keys in [Running under the Claude Code sandbox](#running-under-the-claude-code-sandbox); `gmax doctor` warns about the same gap. |
416
456
  | `npm audit` reports advisories on install | Two transitive advisories (`sharp`, `adm-zip`) have no upstream fix yet; neither is on a code path gmax executes. |
417
457
 
418
458
  ## Contributing
@@ -43,14 +43,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.audit = void 0;
46
- exports.computeAudit = computeAudit;
47
46
  const commander_1 = require("commander");
48
- const callsites_1 = require("../lib/graph/callsites");
47
+ const graph_handler_1 = require("../lib/daemon/graph-handler");
49
48
  const vector_db_1 = require("../lib/store/vector-db");
50
- const arrow_1 = require("../lib/utils/arrow");
51
49
  const exit_1 = require("../lib/utils/exit");
52
50
  const project_registry_1 = require("../lib/utils/project-registry");
53
51
  const project_root_1 = require("../lib/utils/project-root");
52
+ const store_access_1 = require("../lib/utils/store-access");
54
53
  const useColors = process.stdout.isTTY && !process.env.NO_COLOR;
55
54
  const style = {
56
55
  bold: (s) => (useColors ? `\x1b[1m${s}\x1b[22m` : s),
@@ -59,222 +58,6 @@ const style = {
59
58
  yellow: (s) => (useColors ? `\x1b[33m${s}\x1b[39m` : s),
60
59
  cyan: (s) => (useColors ? `\x1b[36m${s}\x1b[39m` : s),
61
60
  };
62
- // Names too generic to be useful as god-node signal (`id`, `el`, `fn`, …).
63
- const MIN_GOD_NAME_LEN = 3;
64
- function rel(p, prefix) {
65
- return p.startsWith(prefix) ? p.slice(prefix.length) : p;
66
- }
67
- function findFileCycles(deps, prefix, top) {
68
- const indexByFile = new Map();
69
- const lowlink = new Map();
70
- const stack = [];
71
- const onStack = new Set();
72
- const components = [];
73
- let index = 0;
74
- const strongConnect = (file) => {
75
- var _a, _b, _c, _d, _e;
76
- indexByFile.set(file, index);
77
- lowlink.set(file, index);
78
- index++;
79
- stack.push(file);
80
- onStack.add(file);
81
- for (const dep of (_a = deps.get(file)) !== null && _a !== void 0 ? _a : []) {
82
- if (!indexByFile.has(dep)) {
83
- strongConnect(dep);
84
- lowlink.set(file, Math.min((_b = lowlink.get(file)) !== null && _b !== void 0 ? _b : 0, (_c = lowlink.get(dep)) !== null && _c !== void 0 ? _c : 0));
85
- }
86
- else if (onStack.has(dep)) {
87
- lowlink.set(file, Math.min((_d = lowlink.get(file)) !== null && _d !== void 0 ? _d : 0, (_e = indexByFile.get(dep)) !== null && _e !== void 0 ? _e : 0));
88
- }
89
- }
90
- if (lowlink.get(file) !== indexByFile.get(file))
91
- return;
92
- const component = [];
93
- while (stack.length > 0) {
94
- const dep = stack.pop();
95
- onStack.delete(dep);
96
- component.push(dep);
97
- if (dep === file)
98
- break;
99
- }
100
- if (component.length > 1)
101
- components.push(component);
102
- };
103
- const files = new Set();
104
- for (const [file, targets] of deps) {
105
- files.add(file);
106
- for (const target of targets)
107
- files.add(target);
108
- }
109
- for (const file of [...files].sort()) {
110
- if (!indexByFile.has(file))
111
- strongConnect(file);
112
- }
113
- return components
114
- .map((component) => {
115
- var _a;
116
- const set = new Set(component);
117
- let edgeCount = 0;
118
- for (const file of component) {
119
- for (const dep of (_a = deps.get(file)) !== null && _a !== void 0 ? _a : []) {
120
- if (set.has(dep))
121
- edgeCount++;
122
- }
123
- }
124
- return {
125
- files: component.map((f) => rel(f, prefix)).sort(),
126
- edgeCount,
127
- };
128
- })
129
- .sort((a, b) => b.files.length - a.files.length ||
130
- b.edgeCount - a.edgeCount ||
131
- a.files.join("\0").localeCompare(b.files.join("\0")))
132
- .slice(0, top);
133
- }
134
- /**
135
- * Pure aggregation over chunk rows — no DB, no I/O. Builds the symbol→def map,
136
- * cross-file inbound edges, and per-file fan-in/out, then derives god nodes
137
- * (most depended-upon symbols), hub files (most depended-upon files), and
138
- * dead-code candidates (non-exported symbols with zero inbound references).
139
- * `top` caps each list; `deadTotal` reports the full pre-cap dead count.
140
- */
141
- function computeAudit(rows, prefix, top) {
142
- var _a, _b;
143
- // First definition of a symbol wins (matches GraphBuilder semantics).
144
- const defs = new Map();
145
- // Distinct files defining each name — name-based edges can't tell same-name
146
- // symbols apart, so multi-file definitions get flagged in the output.
147
- const defFileCounts = new Map();
148
- // Distinct files that reference a symbol (cross-file inbound edges).
149
- const inboundFiles = new Map();
150
- const inboundTotal = new Map();
151
- // Per-file aggregates.
152
- const fileDefs = new Map();
153
- const fileOutRefs = new Map();
154
- const files = new Set();
155
- for (const row of rows) {
156
- const file = String(row.path || "");
157
- const line = Number(row.start_line || 0);
158
- const exported = Boolean(row.is_exported);
159
- const defSyms = (0, arrow_1.toArr)(row.defined_symbols);
160
- const refSyms = (0, arrow_1.toArr)(row.referenced_symbols);
161
- files.add(file);
162
- for (const s of defSyms) {
163
- if (!defs.has(s)) {
164
- defs.set(s, { file, line, exported, complexity: 0 });
165
- }
166
- if (!defFileCounts.has(s))
167
- defFileCounts.set(s, new Set());
168
- defFileCounts.get(s).add(file);
169
- if (!fileDefs.has(file))
170
- fileDefs.set(file, new Set());
171
- fileDefs.get(file).add(s);
172
- }
173
- for (const s of refSyms) {
174
- if (!inboundFiles.has(s))
175
- inboundFiles.set(s, new Set());
176
- inboundFiles.get(s).add(file);
177
- inboundTotal.set(s, (inboundTotal.get(s) || 0) + 1);
178
- if (!fileOutRefs.has(file))
179
- fileOutRefs.set(file, new Set());
180
- fileOutRefs.get(file).add(s);
181
- }
182
- }
183
- // God nodes — in-project symbols by distinct external inbound files.
184
- const godNodes = [];
185
- for (const [symbol, info] of defs) {
186
- if (symbol.length < MIN_GOD_NAME_LEN)
187
- continue;
188
- // Builtin method names (get, set, push, …) leak in via prototype/member
189
- // definitions and their inbound counts are meaningless name collisions.
190
- if ((0, callsites_1.isBuiltinCallee)(symbol))
191
- continue;
192
- const refFiles = inboundFiles.get(symbol);
193
- if (!refFiles)
194
- continue;
195
- let external = 0;
196
- for (const f of refFiles)
197
- if (f !== info.file)
198
- external++;
199
- if (external === 0)
200
- continue;
201
- godNodes.push({
202
- symbol,
203
- file: rel(info.file, prefix),
204
- line: info.line,
205
- inboundFiles: external,
206
- totalRefs: inboundTotal.get(symbol) || 0,
207
- defFiles: (_b = (_a = defFileCounts.get(symbol)) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 1,
208
- });
209
- }
210
- godNodes.sort((a, b) => b.inboundFiles - a.inboundFiles || b.totalRefs - a.totalRefs);
211
- // Hub files — distinct external files depending on each file (a file G
212
- // depends on F if G references any symbol F defines).
213
- const hubFiles = [];
214
- for (const [file, syms] of fileDefs) {
215
- const dependents = new Set();
216
- for (const s of syms) {
217
- const refFiles = inboundFiles.get(s);
218
- if (!refFiles)
219
- continue;
220
- for (const f of refFiles)
221
- if (f !== file)
222
- dependents.add(f);
223
- }
224
- // Fan-out: distinct referenced symbols that are defined somewhere
225
- // in-project (external-library calls don't count as coupling).
226
- let fanOut = 0;
227
- const out = fileOutRefs.get(file);
228
- if (out)
229
- for (const s of out)
230
- if (defs.has(s))
231
- fanOut++;
232
- hubFiles.push({
233
- file: rel(file, prefix),
234
- dependents: dependents.size,
235
- defines: syms.size,
236
- fanOut,
237
- });
238
- }
239
- hubFiles.sort((a, b) => b.dependents - a.dependents || b.defines - a.defines);
240
- const fileDeps = new Map();
241
- for (const [file, refs] of fileOutRefs) {
242
- for (const s of refs) {
243
- if ((0, callsites_1.isBuiltinCallee)(s))
244
- continue;
245
- const defFiles = defFileCounts.get(s);
246
- if (!defFiles || defFiles.size !== 1)
247
- continue;
248
- const [depFile] = defFiles;
249
- if (!depFile || depFile === file)
250
- continue;
251
- if (!fileDeps.has(file))
252
- fileDeps.set(file, new Set());
253
- fileDeps.get(file).add(depFile);
254
- }
255
- }
256
- const fileCycles = findFileCycles(fileDeps, prefix, top);
257
- // Dead candidates — non-exported in-project symbols with zero inbound
258
- // references anywhere (including their own file).
259
- const deadAll = [];
260
- for (const [symbol, info] of defs) {
261
- if (info.exported)
262
- continue;
263
- if ((inboundTotal.get(symbol) || 0) > 0)
264
- continue;
265
- deadAll.push({ symbol, file: rel(info.file, prefix), line: info.line });
266
- }
267
- deadAll.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
268
- return {
269
- scannedChunks: rows.length,
270
- scannedFiles: files.size,
271
- godNodes: godNodes.slice(0, top),
272
- hubFiles: hubFiles.filter((h) => h.dependents > 0).slice(0, top),
273
- fileCycles,
274
- deadCandidates: deadAll.slice(0, top),
275
- deadTotal: deadAll.length,
276
- };
277
- }
278
61
  function formatCycle(files) {
279
62
  const shown = files.length > 6
280
63
  ? [...files.slice(0, 6), `... ${files.length - 6} more`]
@@ -374,67 +157,52 @@ exports.audit = new commander_1.Command("audit")
374
157
  const root = (0, project_registry_1.resolveRootOrExit)(opts.root);
375
158
  if (root === null)
376
159
  return;
377
- let vectorDb = null;
160
+ // Opened only if the daemon is unreachable; withStoreRead decides.
161
+ const local = { db: null };
378
162
  try {
379
163
  const projectRoot = (_a = (0, project_root_1.findProjectRoot)(root)) !== null && _a !== void 0 ? _a : root;
380
- const prefix = projectRoot.endsWith("/")
381
- ? projectRoot
382
- : `${projectRoot}/`;
383
- const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
384
- vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
385
- const { resolveScope, buildScopeWhere } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/scope-filter")));
164
+ const { resolveScope } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/scope-filter")));
386
165
  const scope = resolveScope({
387
166
  projectRoot,
388
167
  in: opts.in,
389
168
  exclude: opts.exclude,
390
169
  });
391
170
  const top = Math.max(1, Number.parseInt(opts.top, 10) || 10);
392
- const table = yield vectorDb.ensureTable();
393
- const rows = yield table
394
- .query()
395
- .select([
396
- "path",
397
- "start_line",
398
- "defined_symbols",
399
- "referenced_symbols",
400
- "type_referenced_symbols",
401
- "is_exported",
402
- ])
403
- .where(buildScopeWhere(scope))
404
- .limit(500000)
405
- .toArray();
406
- if (rows.length === 0) {
171
+ // Up to 500k rows are read and aggregated wherever the store is open;
172
+ // what crosses the socket is the finished report.
173
+ const result = yield (0, graph_handler_1.callGraphVerb)("graph.audit", {
174
+ projectRoot,
175
+ scope,
176
+ payload: { top },
177
+ render: (resp) => { var _a; return (_a = resp.audit) !== null && _a !== void 0 ? _a : null; },
178
+ inProcess: () => {
179
+ var _a;
180
+ (_a = local.db) !== null && _a !== void 0 ? _a : (local.db = new vector_db_1.VectorDB((0, project_root_1.ensureProjectPaths)(projectRoot).lancedbDir));
181
+ return (0, graph_handler_1.runGraphAudit)(local.db, { projectRoot, scope, top });
182
+ },
183
+ });
184
+ if (result === null) {
407
185
  console.log(opts.agent
408
186
  ? "(no indexed data)"
409
187
  : `No indexed data for ${projectRoot}. Run: gmax index --path ${projectRoot}`);
410
188
  process.exitCode = 1;
411
189
  return;
412
190
  }
413
- const result = computeAudit(rows.map((r) => ({
414
- path: String(r.path || ""),
415
- start_line: Number(r.start_line || 0),
416
- is_exported: Boolean(r.is_exported),
417
- defined_symbols: (0, arrow_1.toArr)(r.defined_symbols),
418
- // Union call-position + type-position edges so god-node ranking and
419
- // dead-candidate detection see references made purely in type position.
420
- referenced_symbols: [
421
- ...new Set([
422
- ...(0, arrow_1.toArr)(r.referenced_symbols),
423
- ...(0, arrow_1.toArr)(r.type_referenced_symbols),
424
- ]),
425
- ],
426
- })), prefix, top);
427
191
  console.log(opts.agent ? formatAgent(result) : formatHuman(result));
428
192
  }
429
193
  catch (error) {
430
- const message = error instanceof Error ? error.message : "Unknown error";
431
- console.error("Audit failed:", message);
432
- process.exitCode = 1;
194
+ // A sandbox refusal is already one actionable line; an "Audit failed:"
195
+ // prefix would bury the settings key that fixes it.
196
+ if (!(0, store_access_1.reportStoreAccessRefusal)(error)) {
197
+ const message = error instanceof Error ? error.message : "Unknown error";
198
+ console.error("Audit failed:", message);
199
+ process.exitCode = 1;
200
+ }
433
201
  }
434
202
  finally {
435
- if (vectorDb) {
203
+ if (local.db) {
436
204
  try {
437
- yield vectorDb.close();
205
+ yield local.db.close();
438
206
  }
439
207
  catch (err) {
440
208
  console.error("Failed to close VectorDB:", err);
@@ -46,17 +46,18 @@ exports.context = void 0;
46
46
  exports.findEnclosingSignature = findEnclosingSignature;
47
47
  const fs = __importStar(require("node:fs"));
48
48
  const commander_1 = require("commander");
49
+ const rows_handler_1 = require("../lib/daemon/rows-handler");
49
50
  const searcher_1 = require("../lib/search/searcher");
50
51
  const skeleton_1 = require("../lib/skeleton");
51
- const vector_db_1 = require("../lib/store/vector-db");
52
52
  const arrow_1 = require("../lib/utils/arrow");
53
53
  const budget_pack_1 = require("../lib/utils/budget-pack");
54
+ const daemon_client_1 = require("../lib/utils/daemon-client");
54
55
  const exit_1 = require("../lib/utils/exit");
55
56
  const file_utils_1 = require("../lib/utils/file-utils");
56
- const filter_builder_1 = require("../lib/utils/filter-builder");
57
57
  const path_containment_1 = require("../lib/utils/path-containment");
58
58
  const project_registry_1 = require("../lib/utils/project-registry");
59
59
  const project_root_1 = require("../lib/utils/project-root");
60
+ const store_access_1 = require("../lib/utils/store-access");
60
61
  function estimateTokens(text) {
61
62
  return Math.ceil(text.length / 4);
62
63
  }
@@ -169,14 +170,12 @@ exports.context = new commander_1.Command("context")
169
170
  var _a, _b, _c;
170
171
  const budget = Number.parseInt(opts.budget || "4000", 10) || 4000;
171
172
  const maxResults = Number.parseInt(opts.maxResults || "10", 10) || 10;
172
- let vectorDb = null;
173
173
  try {
174
174
  const root = (0, project_registry_1.resolveRootOrExit)(opts.root);
175
175
  if (root === null)
176
176
  return;
177
177
  const projectRoot = (_a = (0, project_root_1.findProjectRoot)(root)) !== null && _a !== void 0 ? _a : root;
178
178
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
179
- vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
180
179
  const pathTarget = (0, path_containment_1.resolveContainedExistingPath)(projectRoot, topic, {
181
180
  cwd: root,
182
181
  onOutside: "throw",
@@ -187,10 +186,26 @@ exports.context = new commander_1.Command("context")
187
186
  console.log(rendered.sections.join("\n"));
188
187
  return;
189
188
  }
190
- const searcher = new searcher_1.Searcher(vectorDb);
191
- // Phase 1: Semantic search
192
- const response = yield searcher.search(topic, maxResults, { rerank: true }, {}, `${projectRoot}/`);
193
- const scopedData = response.data.filter((result) => (0, path_containment_1.isPathWithin)(projectRoot, chunkPath(result)));
189
+ // Phase 1: Semantic search. Routed through the daemon so the Searcher —
190
+ // and the PageRank cache under ~/.gmax/pagerank/ that it may write — lives
191
+ // in exactly one process.
192
+ const responseData = yield (0, store_access_1.withStoreRead)("context", {
193
+ daemon: () => (0, daemon_client_1.sendDaemonCommand)({
194
+ cmd: "search",
195
+ projectRoot,
196
+ query: topic,
197
+ limit: maxResults,
198
+ pathPrefix: projectRoot,
199
+ rerank: true,
200
+ }, { timeoutMs: 60000 }),
201
+ render: (resp) => { var _a; return ((_a = resp.data) !== null && _a !== void 0 ? _a : []); },
202
+ inProcess: () => (0, rows_handler_1.withLocalStore)(paths.lancedbDir, (deps) => __awaiter(void 0, void 0, void 0, function* () {
203
+ const searcher = new searcher_1.Searcher(deps.vectorDb);
204
+ const response = yield searcher.search(topic, maxResults, { rerank: true }, {}, `${projectRoot}/`);
205
+ return response.data;
206
+ })),
207
+ });
208
+ const scopedData = responseData.filter((result) => (0, path_containment_1.isPathWithin)(projectRoot, chunkPath(result)));
194
209
  if (scopedData.length === 0) {
195
210
  console.log(`No results found for "${topic}".`);
196
211
  return;
@@ -322,23 +337,28 @@ exports.context = new commander_1.Command("context")
322
337
  sections.push(skelSection.join(""));
323
338
  }
324
339
  // Phase 5: Related files summary
325
- const table = yield vectorDb.ensureTable();
326
340
  const allSymbols = new Set();
327
341
  for (const r of scopedData) {
328
342
  for (const s of (0, arrow_1.toArr)(r.defined_symbols))
329
343
  allSymbols.add(s);
330
344
  }
331
345
  if (allSymbols.size > 0) {
332
- const pathScope = (0, filter_builder_1.pathStartsWith)(`${projectRoot}/`);
333
346
  const relatedCounts = new Map();
334
347
  const searchedFiles = new Set(uniqueFiles);
335
- for (const sym of [...allSymbols].slice(0, 20)) {
336
- const rows = yield table
337
- .query()
338
- .select(["path"])
339
- .where(`array_contains(referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}') AND ${pathScope}`)
340
- .limit(5)
341
- .toArray();
348
+ // One batched round trip for the ≤20 symbol lookups.
349
+ const { resolveScope } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/scope-filter")));
350
+ const relatedRows = yield (0, rows_handler_1.readRows)({
351
+ name: "context",
352
+ projectRoot,
353
+ lancedbDir: paths.lancedbDir,
354
+ scope: resolveScope({ projectRoot }),
355
+ select: ["path"],
356
+ matches: [...allSymbols]
357
+ .slice(0, 20)
358
+ .map((symbol) => ({ kind: "referencedSymbol", symbol })),
359
+ limit: 5,
360
+ });
361
+ for (const rows of relatedRows) {
342
362
  for (const row of rows) {
343
363
  const p = String(row.path || "");
344
364
  if (searchedFiles.has(p))
@@ -366,17 +386,13 @@ exports.context = new commander_1.Command("context")
366
386
  console.log(sections.join("\n"));
367
387
  }
368
388
  catch (error) {
369
- const msg = error instanceof Error ? error.message : "Unknown error";
370
- console.error("Context generation failed:", msg);
371
- process.exitCode = 1;
389
+ if (!(0, store_access_1.reportStoreAccessRefusal)(error)) {
390
+ const msg = error instanceof Error ? error.message : "Unknown error";
391
+ console.error("Context generation failed:", msg);
392
+ process.exitCode = 1;
393
+ }
372
394
  }
373
395
  finally {
374
- if (vectorDb) {
375
- try {
376
- yield vectorDb.close();
377
- }
378
- catch (_e) { }
379
- }
380
396
  yield (0, exit_1.gracefulExit)();
381
397
  }
382
398
  }));
@@ -44,15 +44,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.dead = void 0;
46
46
  const commander_1 = require("commander");
47
- const languages_1 = require("../lib/core/languages");
48
- const graph_builder_1 = require("../lib/graph/graph-builder");
47
+ const graph_handler_1 = require("../lib/daemon/graph-handler");
49
48
  const vector_db_1 = require("../lib/store/vector-db");
50
49
  const agent_errors_1 = require("../lib/utils/agent-errors");
51
50
  const exit_1 = require("../lib/utils/exit");
52
- const filter_builder_1 = require("../lib/utils/filter-builder");
53
51
  const project_registry_1 = require("../lib/utils/project-registry");
54
52
  const project_root_1 = require("../lib/utils/project-root");
55
53
  const stale_hint_1 = require("../lib/utils/stale-hint");
54
+ const store_access_1 = require("../lib/utils/store-access");
56
55
  const useColors = process.stdout.isTTY && !process.env.NO_COLOR;
57
56
  const style = {
58
57
  bold: (s) => (useColors ? `\x1b[1m${s}\x1b[22m` : s),
@@ -117,63 +116,66 @@ exports.dead = new commander_1.Command("dead")
117
116
  const root = (0, project_registry_1.resolveRootOrExit)(opts.root);
118
117
  if (root === null)
119
118
  return;
120
- let vectorDb = null;
119
+ // Opened only if the daemon is unreachable; withStoreRead decides.
120
+ const local = { db: null };
121
121
  try {
122
122
  const projectRoot = (_a = (0, project_root_1.findProjectRoot)(root)) !== null && _a !== void 0 ? _a : root;
123
123
  (0, stale_hint_1.maybeWarnStaleChunker)(projectRoot, { agent: opts.agent });
124
124
  (0, stale_hint_1.maybeWarnStaleEmbedding)(projectRoot, { agent: opts.agent });
125
- const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
126
- vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
127
- const { resolveScope, buildScopeWhere } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/scope-filter")));
125
+ const { resolveScope } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/scope-filter")));
128
126
  const scope = resolveScope({
129
127
  projectRoot,
130
128
  in: opts.in,
131
129
  exclude: opts.exclude,
132
130
  });
133
- // Resolve the defining chunk to get path, line, and is_exported.
134
- const table = yield vectorDb.ensureTable();
135
- const defRows = yield table
136
- .query()
137
- .select(["path", "start_line", "is_exported"])
138
- .where(buildScopeWhere(scope, `array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}')`))
139
- .limit(1)
140
- .toArray();
141
- if (defRows.length === 0) {
131
+ // The defining chunk plus its inbound callers, counted store-side: the
132
+ // caller set can be large and only its size and top 3 are rendered.
133
+ const facts = yield (0, graph_handler_1.callGraphVerb)("graph.dead", {
134
+ projectRoot,
135
+ scope,
136
+ payload: { target: symbol },
137
+ render: (resp) => resp.dead,
138
+ inProcess: () => {
139
+ var _a;
140
+ (_a = local.db) !== null && _a !== void 0 ? _a : (local.db = new vector_db_1.VectorDB((0, project_root_1.ensureProjectPaths)(projectRoot).lancedbDir));
141
+ return (0, graph_handler_1.runGraphDead)(local.db, { symbol, scope });
142
+ },
143
+ });
144
+ if (!facts.found) {
142
145
  console.log((0, agent_errors_1.symbolNotFoundLines)(symbol, { agent: opts.agent }).join("\n"));
143
146
  process.exitCode = 1;
144
147
  return;
145
148
  }
146
- const defRow = defRows[0];
147
- const defPath = String(defRow.path || "");
148
- const defLine = Number(defRow.start_line || 0);
149
- const isExported = Boolean(defRow.is_exported);
150
- const builder = new graph_builder_1.GraphBuilder(vectorDb, scope.pathPrefix, scope.excludePrefixes);
151
- const callers = yield builder.getCallers(symbol, (0, languages_1.languageFamilyForPath)(defPath));
152
- const status = callers.length === 0 ? (isExported ? "PUBLIC_EXPORT" : "DEAD") : "LIVE";
153
- const topCallers = callers
154
- .slice(0, TOP_CALLERS)
155
- .map((c) => ({ file: c.file, line: c.line }));
149
+ const status = facts.callerCount === 0
150
+ ? facts.isExported
151
+ ? "PUBLIC_EXPORT"
152
+ : "DEAD"
153
+ : "LIVE";
156
154
  const result = {
157
155
  status,
158
156
  symbol,
159
- defPath,
160
- defLine,
161
- callerCount: callers.length,
162
- topCallers,
157
+ defPath: facts.defPath,
158
+ defLine: facts.defLine,
159
+ callerCount: facts.callerCount,
160
+ topCallers: facts.topCallers,
163
161
  };
164
162
  console.log(opts.agent
165
163
  ? formatAgent(result, projectRoot)
166
164
  : formatHuman(result, projectRoot));
167
165
  }
168
166
  catch (error) {
169
- const message = error instanceof Error ? error.message : "Unknown error";
170
- console.error("Dead check failed:", message);
171
- process.exitCode = 1;
167
+ // A sandbox refusal is already one actionable line; a "Dead check
168
+ // failed:" prefix would bury the settings key that fixes it.
169
+ if (!(0, store_access_1.reportStoreAccessRefusal)(error)) {
170
+ const message = error instanceof Error ? error.message : "Unknown error";
171
+ console.error("Dead check failed:", message);
172
+ process.exitCode = 1;
173
+ }
172
174
  }
173
175
  finally {
174
- if (vectorDb) {
176
+ if (local.db) {
175
177
  try {
176
- yield vectorDb.close();
178
+ yield local.db.close();
177
179
  }
178
180
  catch (err) {
179
181
  console.error("Failed to close VectorDB:", err);