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
@@ -44,16 +44,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.similar = void 0;
46
46
  const commander_1 = require("commander");
47
- const ann_config_1 = require("../lib/store/ann-config");
48
- const vector_db_1 = require("../lib/store/vector-db");
47
+ const rows_handler_1 = require("../lib/daemon/rows-handler");
48
+ const vector_handler_1 = require("../lib/daemon/vector-handler");
49
49
  const agent_errors_1 = require("../lib/utils/agent-errors");
50
- const arrow_1 = require("../lib/utils/arrow");
50
+ const daemon_client_1 = require("../lib/utils/daemon-client");
51
51
  const exit_1 = require("../lib/utils/exit");
52
- const filter_builder_1 = require("../lib/utils/filter-builder");
53
52
  const path_containment_1 = require("../lib/utils/path-containment");
54
53
  const project_registry_1 = require("../lib/utils/project-registry");
55
54
  const project_root_1 = require("../lib/utils/project-root");
56
55
  const stale_hint_1 = require("../lib/utils/stale-hint");
56
+ const store_access_1 = require("../lib/utils/store-access");
57
57
  exports.similar = new commander_1.Command("similar")
58
58
  .description("Find semantically similar code to a symbol or file")
59
59
  .argument("<target>", "Symbol name or file path")
@@ -67,7 +67,6 @@ exports.similar = new commander_1.Command("similar")
67
67
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
68
68
  const limit = Math.min(Math.max(Number.parseInt(opts.maxCount || "5", 10), 1), 25);
69
69
  const threshold = Number.parseFloat(opts.threshold || "0") || 0;
70
- let vectorDb = null;
71
70
  try {
72
71
  const root = (0, project_registry_1.resolveRootOrExit)(opts.root);
73
72
  if (root === null)
@@ -76,78 +75,57 @@ exports.similar = new commander_1.Command("similar")
76
75
  (0, stale_hint_1.maybeWarnStaleChunker)(projectRoot, { agent: opts.agent });
77
76
  (0, stale_hint_1.maybeWarnStaleEmbedding)(projectRoot, { agent: opts.agent });
78
77
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
79
- vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
80
- const table = yield vectorDb.ensureTable();
81
78
  const isFile = target.includes("/") || (target.includes(".") && !target.includes(" "));
82
- // Look up the source chunk's vector
83
- let sourceRows;
84
- if (isFile) {
85
- const absPath = (0, path_containment_1.resolveContainedPath)(projectRoot, target, {
79
+ // Resolved here, not daemon-side: verifyExistingTarget looks at this
80
+ // process's filesystem, and a path outside the project must fail with the
81
+ // containment error the user expects before anything reaches the store.
82
+ const absPath = isFile
83
+ ? (0, path_containment_1.resolveContainedPath)(projectRoot, target, {
86
84
  verifyExistingTarget: true,
87
- });
88
- sourceRows = yield table
89
- .query()
90
- .select(["vector", "path", "defined_symbols", "start_line"])
91
- .where(`path = '${(0, filter_builder_1.escapeSqlString)(absPath)}'`)
92
- .limit(1)
93
- .toArray();
94
- }
95
- else {
96
- sourceRows = yield table
97
- .query()
98
- .select(["vector", "path", "defined_symbols", "start_line"])
99
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(target)}') AND ${(0, filter_builder_1.pathStartsWith)(`${projectRoot}/`)}`)
100
- .limit(1)
101
- .toArray();
102
- }
103
- if (sourceRows.length === 0) {
85
+ })
86
+ : undefined;
87
+ const { resolveScope } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/scope-filter")));
88
+ const scope = resolveScope({
89
+ projectRoot,
90
+ in: opts.in,
91
+ exclude: opts.exclude,
92
+ });
93
+ // The source chunk's 384-float vector never leaves the store: the daemon
94
+ // looks it up, runs the vector search, and returns the ranked chunks.
95
+ const result = yield (0, store_access_1.withStoreRead)("similar", {
96
+ daemon: () => (0, daemon_client_1.sendDaemonCommand)({
97
+ cmd: "vector.similar",
98
+ projectRoot,
99
+ absPath,
100
+ symbol: isFile ? undefined : target,
101
+ scope: (0, rows_handler_1.scopeToWire)(scope),
102
+ limit,
103
+ threshold,
104
+ }, { timeoutMs: 60000 }),
105
+ render: (resp) => resp,
106
+ inProcess: () => (0, rows_handler_1.withLocalStore)(paths.lancedbDir, (deps) => (0, vector_handler_1.runSimilar)(deps, {
107
+ projectRoot,
108
+ absPath,
109
+ symbol: isFile ? undefined : target,
110
+ scope,
111
+ limit,
112
+ threshold,
113
+ })),
114
+ fallbackOnUnknownVerb: true,
115
+ });
116
+ if (result.status === "not-found") {
104
117
  console.log((isFile
105
118
  ? (0, agent_errors_1.fileNotFoundLines)(target, { agent: opts.agent })
106
119
  : (0, agent_errors_1.symbolNotFoundLines)(target, { agent: opts.agent })).join("\n"));
107
120
  process.exitCode = 1;
108
121
  return;
109
122
  }
110
- const source = sourceRows[0];
111
- const sourceVector = source.vector;
112
- const sourcePath = String(source.path || "");
113
- if (!sourceVector || sourceVector.length === 0) {
123
+ if (result.status === "no-vector") {
114
124
  console.log("Source chunk has no embedding vector.");
115
125
  process.exitCode = 1;
116
126
  return;
117
127
  }
118
- // Vector search using the source chunk's embedding
119
- const { resolveScope, buildScopeWhere } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/scope-filter")));
120
- const scope = resolveScope({
121
- projectRoot,
122
- in: opts.in,
123
- exclude: opts.exclude,
124
- });
125
- const pathScope = buildScopeWhere(scope);
126
- const results = yield (0, ann_config_1.configureAnnVectorQuery)(table.vectorSearch(sourceVector))
127
- .select([
128
- "path",
129
- "start_line",
130
- "end_line",
131
- "defined_symbols",
132
- "role",
133
- "content",
134
- "_distance",
135
- ])
136
- .where(pathScope)
137
- .limit(limit + 5) // fetch extra to account for self-filtering
138
- .toArray();
139
- // Filter out self and apply threshold
140
- const filtered = results.filter((r) => {
141
- if (r.path === sourcePath && r.start_line === source.start_line)
142
- return false;
143
- if (threshold > 0) {
144
- // LanceDB returns L2 distance; convert to similarity
145
- const sim = 1 / (1 + (r._distance || 0));
146
- if (sim < threshold)
147
- return false;
148
- }
149
- return true;
150
- });
128
+ const filtered = result.results;
151
129
  if (filtered.length === 0) {
152
130
  console.log(`No similar code found for ${target}.`);
153
131
  return;
@@ -155,7 +133,7 @@ exports.similar = new commander_1.Command("similar")
155
133
  const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
156
134
  if (opts.agent) {
157
135
  for (const r of filtered.slice(0, limit)) {
158
- const sym = (_c = (_b = (0, arrow_1.toArr)(r.defined_symbols)) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : "";
136
+ const sym = (_c = (_b = r.defined_symbols) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : "";
159
137
  const line = ((_d = r.start_line) !== null && _d !== void 0 ? _d : 0) + 1;
160
138
  const role = (r.role || "IMPL").slice(0, 4);
161
139
  const dist = ((_e = r._distance) !== null && _e !== void 0 ? _e : 0).toFixed(3);
@@ -165,7 +143,7 @@ exports.similar = new commander_1.Command("similar")
165
143
  else {
166
144
  console.log(`Code similar to ${target}:\n`);
167
145
  for (const r of filtered.slice(0, limit)) {
168
- const sym = (_g = (_f = (0, arrow_1.toArr)(r.defined_symbols)) === null || _f === void 0 ? void 0 : _f[0]) !== null && _g !== void 0 ? _g : "";
146
+ const sym = (_g = (_f = r.defined_symbols) === null || _f === void 0 ? void 0 : _f[0]) !== null && _g !== void 0 ? _g : "";
169
147
  const line = ((_h = r.start_line) !== null && _h !== void 0 ? _h : 0) + 1;
170
148
  const role = r.role || "IMPLEMENTATION";
171
149
  const dist = ((_j = r._distance) !== null && _j !== void 0 ? _j : 0).toFixed(3);
@@ -174,17 +152,13 @@ exports.similar = new commander_1.Command("similar")
174
152
  }
175
153
  }
176
154
  catch (error) {
177
- const msg = error instanceof Error ? error.message : "Unknown error";
178
- console.error("Similar search failed:", msg);
179
- process.exitCode = 1;
155
+ if (!(0, store_access_1.reportStoreAccessRefusal)(error)) {
156
+ const msg = error instanceof Error ? error.message : "Unknown error";
157
+ console.error("Similar search failed:", msg);
158
+ process.exitCode = 1;
159
+ }
180
160
  }
181
161
  finally {
182
- if (vectorDb) {
183
- try {
184
- yield vectorDb.close();
185
- }
186
- catch (_k) { }
187
- }
188
162
  yield (0, exit_1.gracefulExit)();
189
163
  }
190
164
  }));
@@ -54,19 +54,18 @@ exports.skeleton = void 0;
54
54
  const fs = __importStar(require("node:fs"));
55
55
  const commander_1 = require("commander");
56
56
  const config_1 = require("../config");
57
+ const rows_handler_1 = require("../lib/daemon/rows-handler");
57
58
  const sync_helpers_1 = require("../lib/index/sync-helpers");
58
- const syncer_1 = require("../lib/index/syncer");
59
59
  const searcher_1 = require("../lib/search/searcher");
60
60
  const setup_helpers_1 = require("../lib/setup/setup-helpers");
61
- const retriever_1 = require("../lib/skeleton/retriever");
62
61
  const skeletonizer_1 = require("../lib/skeleton/skeletonizer");
63
- const vector_db_1 = require("../lib/store/vector-db");
62
+ const daemon_client_1 = require("../lib/utils/daemon-client");
64
63
  const exit_1 = require("../lib/utils/exit");
65
64
  const file_utils_1 = require("../lib/utils/file-utils");
66
- const filter_builder_1 = require("../lib/utils/filter-builder");
67
65
  const path_containment_1 = require("../lib/utils/path-containment");
68
66
  const project_registry_1 = require("../lib/utils/project-registry");
69
67
  const project_root_1 = require("../lib/utils/project-root");
68
+ const store_access_1 = require("../lib/utils/store-access");
70
69
  /**
71
70
  * Check if target looks like a file path.
72
71
  */
@@ -83,34 +82,115 @@ function isSymbolLike(target) {
83
82
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(target) && !target.includes(" ");
84
83
  }
85
84
  /**
86
- * Find a file by symbol name in the index.
85
+ * `--sync`. With a daemon up this is the existing `ensure-project` streaming
86
+ * verb — the daemon already owns first-run indexing and is the only process
87
+ * that should be writing to the store. `initialSync` runs here only when no
88
+ * daemon is listening.
87
89
  */
88
- function findFileBySymbol(symbol, db, projectRoot) {
90
+ function syncIndex(projectRoot, lancedbDir) {
89
91
  return __awaiter(this, void 0, void 0, function* () {
90
- try {
91
- const table = yield db.ensureTable();
92
- // Search for files that define this symbol
93
- const results = yield table
94
- .search(symbol)
95
- .where((0, filter_builder_1.pathStartsWith)(`${projectRoot}/`))
96
- .limit(10)
97
- .toArray();
98
- // Find a result where this symbol is defined
99
- for (const result of results) {
100
- const defined = result.defined_symbols;
101
- if (defined === null || defined === void 0 ? void 0 : defined.includes(symbol)) {
102
- return result.path;
92
+ const { spinner, onProgress } = (0, sync_helpers_1.createIndexingSpinner)(projectRoot, "Syncing...", { verbose: false });
93
+ yield (0, store_access_1.withStoreRead)("skeleton sync", {
94
+ daemon: () => __awaiter(this, void 0, void 0, function* () {
95
+ var _a;
96
+ try {
97
+ const done = yield (0, daemon_client_1.sendStreamingCommand)({ cmd: "ensure-project", root: projectRoot }, (msg) => {
98
+ var _a, _b, _c;
99
+ return onProgress({
100
+ processed: Number((_a = msg.processed) !== null && _a !== void 0 ? _a : 0),
101
+ total: Number((_b = msg.total) !== null && _b !== void 0 ? _b : 0),
102
+ indexed: Number((_c = msg.indexed) !== null && _c !== void 0 ? _c : 0),
103
+ filePath: typeof msg.filePath === "string" ? msg.filePath : "",
104
+ });
105
+ });
106
+ return Object.assign(Object.assign({}, done), { ok: done.ok !== false });
103
107
  }
104
- }
105
- // Fallback: just return the first match's file
106
- if (results.length > 0) {
107
- return results[0].path;
108
- }
109
- return null;
110
- }
111
- catch (_a) {
112
- return null;
113
- }
108
+ catch (err) {
109
+ // sendStreamingCommand rejects with the socket errno as the message,
110
+ // which is exactly what the access policy classifies on.
111
+ return {
112
+ ok: false,
113
+ error: (_a = err === null || err === void 0 ? void 0 : err.code) !== null && _a !== void 0 ? _a : err === null || err === void 0 ? void 0 : err.message,
114
+ };
115
+ }
116
+ }),
117
+ render: () => {
118
+ spinner.succeed("Sync complete");
119
+ },
120
+ inProcess: () => __awaiter(this, void 0, void 0, function* () {
121
+ const { initialSync } = yield Promise.resolve().then(() => __importStar(require("../lib/index/syncer")));
122
+ const result = yield initialSync({ projectRoot, onProgress });
123
+ if (result.degraded) {
124
+ spinner.warn(`Sync incomplete: ${result.scanErrors} scan error(s), ${result.failedFiles} file failure(s)`);
125
+ return;
126
+ }
127
+ const prefix = projectRoot.endsWith("/")
128
+ ? projectRoot
129
+ : `${projectRoot}/`;
130
+ const chunkCount = yield (0, rows_handler_1.withLocalStore)(lancedbDir, (deps) => deps.vectorDb.countRowsForPath(prefix));
131
+ (0, project_registry_1.stampProjectFullSync)({
132
+ root: projectRoot,
133
+ generation: result.generation,
134
+ embedMode: result.embedMode,
135
+ chunkCount,
136
+ chunkerVersion: config_1.CONFIG.CHUNKER_VERSION,
137
+ expectedFingerprint: result.registryExpectation.embeddingFingerprint,
138
+ expectedRebuildId: result.registryExpectation.rebuildId,
139
+ });
140
+ spinner.succeed("Sync complete");
141
+ }),
142
+ });
143
+ });
144
+ }
145
+ /**
146
+ * The stored skeleton for a file, and/or the file that defines a symbol. One
147
+ * verb covers both because symbol mode always follows the lookup with the
148
+ * skeleton fetch.
149
+ */
150
+ function lookupSkeleton(projectRoot, lancedbDir, req) {
151
+ return __awaiter(this, void 0, void 0, function* () {
152
+ return (0, store_access_1.withStoreRead)("skeleton", {
153
+ daemon: () => (0, daemon_client_1.sendDaemonCommand)({
154
+ cmd: "rows.skeleton",
155
+ projectRoot,
156
+ path: req.path,
157
+ symbol: req.symbol,
158
+ }, { timeoutMs: 30000 }),
159
+ render: (resp) => {
160
+ var _a, _b;
161
+ return ({
162
+ path: ((_a = resp.path) !== null && _a !== void 0 ? _a : null),
163
+ skeleton: ((_b = resp.skeleton) !== null && _b !== void 0 ? _b : null),
164
+ });
165
+ },
166
+ inProcess: () => (0, rows_handler_1.withLocalStore)(lancedbDir, (deps) => (0, rows_handler_1.runSkeleton)(deps, Object.assign({ projectRoot }, req))),
167
+ fallbackOnUnknownVerb: true,
168
+ });
169
+ });
170
+ }
171
+ /**
172
+ * Query mode's search. Routed through the daemon's `search` verb so the
173
+ * Searcher — and with it the PageRank cache under ~/.gmax/pagerank/ — stays in
174
+ * one process.
175
+ */
176
+ function searchFiles(projectRoot, lancedbDir, query, limit) {
177
+ return __awaiter(this, void 0, void 0, function* () {
178
+ return (0, store_access_1.withStoreRead)("skeleton search", {
179
+ daemon: () => (0, daemon_client_1.sendDaemonCommand)({
180
+ cmd: "search",
181
+ projectRoot,
182
+ query,
183
+ limit,
184
+ pathPrefix: projectRoot,
185
+ }, { timeoutMs: 60000 }),
186
+ render: (resp) => { var _a; return ((_a = resp.data) !== null && _a !== void 0 ? _a : []); },
187
+ inProcess: () => (0, rows_handler_1.withLocalStore)(lancedbDir, (deps) => __awaiter(this, void 0, void 0, function* () {
188
+ var _a;
189
+ const searcher = new searcher_1.Searcher(deps.vectorDb);
190
+ const results = yield searcher.search(query, limit, {}, {}, `${projectRoot}/`);
191
+ return (_a = results.data) !== null && _a !== void 0 ? _a : [];
192
+ })),
193
+ });
114
194
  });
115
195
  }
116
196
  exports.skeleton = new commander_1.Command("skeleton")
@@ -129,36 +209,15 @@ Examples:
129
209
  `)
130
210
  .action((target, options, _cmd) => __awaiter(void 0, void 0, void 0, function* () {
131
211
  var _a, _b, _c, _d, _e;
132
- let vectorDb = null;
133
212
  try {
134
213
  // Initialize
135
214
  yield (0, setup_helpers_1.ensureSetup)();
136
215
  const projectRoot = (_a = (0, project_root_1.findProjectRoot)(process.cwd())) !== null && _a !== void 0 ? _a : process.cwd();
137
216
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
138
- vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
217
+ const lancedbDir = paths.lancedbDir;
139
218
  // Sync if requested
140
219
  if (options.sync) {
141
- const { spinner, onProgress } = (0, sync_helpers_1.createIndexingSpinner)(projectRoot, "Syncing...", { verbose: false });
142
- const result = yield (0, syncer_1.initialSync)({ projectRoot, onProgress });
143
- if (result.degraded) {
144
- spinner.warn(`Sync incomplete: ${result.scanErrors} scan error(s), ${result.failedFiles} file failure(s)`);
145
- }
146
- else {
147
- const prefix = projectRoot.endsWith("/")
148
- ? projectRoot
149
- : `${projectRoot}/`;
150
- const chunkCount = yield vectorDb.countRowsForPath(prefix);
151
- (0, project_registry_1.stampProjectFullSync)({
152
- root: projectRoot,
153
- generation: result.generation,
154
- embedMode: result.embedMode,
155
- chunkCount,
156
- chunkerVersion: config_1.CONFIG.CHUNKER_VERSION,
157
- expectedFingerprint: result.registryExpectation.embeddingFingerprint,
158
- expectedRebuildId: result.registryExpectation.rebuildId,
159
- });
160
- spinner.succeed("Sync complete");
161
- }
220
+ yield syncIndex(projectRoot, lancedbDir);
162
221
  }
163
222
  // Initialize skeletonizer
164
223
  const skeletonizer = new skeletonizer_1.Skeletonizer();
@@ -216,17 +275,15 @@ Examples:
216
275
  process.exitCode = 1;
217
276
  return;
218
277
  }
219
- if (vectorDb) {
220
- // Use absolute path for DB lookup (centralized index stores absolute paths)
221
- const cached = yield (0, retriever_1.getStoredSkeleton)(vectorDb, filePath);
222
- if (cached) {
223
- outputResult({
224
- success: true,
225
- skeleton: cached,
226
- tokenEstimate: Math.ceil(cached.length / 4),
227
- }, options);
228
- return;
229
- }
278
+ // Use absolute path for DB lookup (centralized index stores absolute paths)
279
+ const cached = (yield lookupSkeleton(projectRoot, lancedbDir, { path: filePath })).skeleton;
280
+ if (cached) {
281
+ outputResult({
282
+ success: true,
283
+ skeleton: cached,
284
+ tokenEstimate: Math.ceil(cached.length / 4),
285
+ }, options);
286
+ return;
230
287
  }
231
288
  const content = (0, file_utils_1.readContainedTextFileSync)(projectRoot, filePath);
232
289
  const result = yield skeletonizer.skeletonizeFile(filePath, content, skeletonOpts);
@@ -234,15 +291,17 @@ Examples:
234
291
  }
235
292
  else if (isSymbolLike(target) && !target.includes(" ")) {
236
293
  // === SYMBOL MODE ===
237
- const filePath = yield findFileBySymbol(target, vectorDb, projectRoot);
238
- if (!filePath) {
294
+ const found = yield lookupSkeleton(projectRoot, lancedbDir, {
295
+ symbol: target,
296
+ });
297
+ if (!found.path) {
239
298
  console.error(`Symbol not found in index: ${target}`);
240
299
  console.error("Try running 'gmax index' first or use a search query.");
241
300
  process.exitCode = 1;
242
301
  return;
243
302
  }
244
303
  // filePath from DB is absolute (centralized index)
245
- const absolutePath = (0, path_containment_1.resolveContainedPath)(projectRoot, filePath, {
304
+ const absolutePath = (0, path_containment_1.resolveContainedPath)(projectRoot, found.path, {
246
305
  verifyExistingTarget: true,
247
306
  });
248
307
  if (!fs.existsSync(absolutePath)) {
@@ -250,7 +309,7 @@ Examples:
250
309
  process.exitCode = 1;
251
310
  return;
252
311
  }
253
- const cached = yield (0, retriever_1.getStoredSkeleton)(vectorDb, absolutePath);
312
+ const cached = found.skeleton;
254
313
  if (cached) {
255
314
  outputResult({
256
315
  success: true,
@@ -265,10 +324,9 @@ Examples:
265
324
  }
266
325
  else {
267
326
  // === QUERY MODE ===
268
- const searcher = new searcher_1.Searcher(vectorDb);
269
327
  const limit = Math.min(Number.parseInt(options.limit, 10) || 3, 10);
270
- const searchResults = yield searcher.search(target, limit, {}, {}, `${projectRoot}/`);
271
- if (!searchResults.data || searchResults.data.length === 0) {
328
+ const searchData = yield searchFiles(projectRoot, lancedbDir, target, limit);
329
+ if (searchData.length === 0) {
272
330
  console.error(`No results found for: ${target}`);
273
331
  process.exitCode = 1;
274
332
  return;
@@ -276,7 +334,7 @@ Examples:
276
334
  // Get unique file paths from results
277
335
  const seenPaths = new Set();
278
336
  const filePaths = [];
279
- for (const result of searchResults.data) {
337
+ for (const result of searchData) {
280
338
  const resultPath = (_e = result.metadata) === null || _e === void 0 ? void 0 : _e.path;
281
339
  if (resultPath && !seenPaths.has(resultPath)) {
282
340
  seenPaths.add(resultPath);
@@ -314,7 +372,9 @@ Examples:
314
372
  continue;
315
373
  }
316
374
  // Try cache first
317
- const cached = yield (0, retriever_1.getStoredSkeleton)(vectorDb, absolutePath);
375
+ const cached = (yield lookupSkeleton(projectRoot, lancedbDir, {
376
+ path: absolutePath,
377
+ })).skeleton;
318
378
  if (cached) {
319
379
  results.push({
320
380
  file: filePath,
@@ -345,19 +405,13 @@ Examples:
345
405
  }
346
406
  }
347
407
  catch (error) {
348
- const message = error instanceof Error ? error.message : String(error);
349
- console.error("Error:", message);
350
- process.exitCode = 1;
408
+ if (!(0, store_access_1.reportStoreAccessRefusal)(error)) {
409
+ const message = error instanceof Error ? error.message : String(error);
410
+ console.error("Error:", message);
411
+ process.exitCode = 1;
412
+ }
351
413
  }
352
414
  finally {
353
- if (vectorDb) {
354
- try {
355
- yield vectorDb.close();
356
- }
357
- catch (_g) {
358
- // Ignore close errors
359
- }
360
- }
361
415
  const code = typeof process.exitCode === "number" ? process.exitCode : 0;
362
416
  yield (0, exit_1.gracefulExit)(code);
363
417
  }
@@ -48,12 +48,87 @@ const commander_1 = require("commander");
48
48
  const config_1 = require("../config");
49
49
  const embedding_status_1 = require("../lib/index/embedding-status");
50
50
  const index_config_1 = require("../lib/index/index-config");
51
+ const daemon_client_1 = require("../lib/utils/daemon-client");
51
52
  const exit_1 = require("../lib/utils/exit");
52
53
  const filter_builder_1 = require("../lib/utils/filter-builder");
53
54
  const lock_1 = require("../lib/utils/lock");
54
55
  const project_registry_1 = require("../lib/utils/project-registry");
55
56
  const project_root_1 = require("../lib/utils/project-root");
57
+ const store_access_1 = require("../lib/utils/store-access");
56
58
  const watcher_store_1 = require("../lib/utils/watcher-store");
59
+ /**
60
+ * Ask the daemon first. Both verbs already exist, so this works against an
61
+ * unrestarted daemon, and it is what makes `status` survive a sandbox that
62
+ * denies writes to ~/.gmax: `listWatchers()` opens LMDB, which needs its lock
63
+ * file even to read, and died with "Attempting to setup locks" before the
64
+ * command ever reached the store.
65
+ *
66
+ * The in-process branch is entered only when nothing is listening on the
67
+ * socket. A lease/LMDB denial there is converted by withStoreRead into the
68
+ * one-line filesystem hint.
69
+ */
70
+ function loadStatusView(projects) {
71
+ return __awaiter(this, void 0, void 0, function* () {
72
+ return (0, store_access_1.withStoreRead)("status", {
73
+ daemon: () => (0, daemon_client_1.sendDaemonCommand)({ cmd: "status" }),
74
+ render: (resp) => __awaiter(this, void 0, void 0, function* () {
75
+ const watchers = new Map();
76
+ const entries = Array.isArray(resp.projects) ? resp.projects : [];
77
+ for (const entry of entries) {
78
+ if (entry && typeof entry.root === "string") {
79
+ watchers.set(entry.root, { status: "watching" });
80
+ }
81
+ }
82
+ // One project-stats call per project. A failure here is not fatal: the
83
+ // renderer falls back to the registry's cached chunkCount, exactly as
84
+ // the in-process path does when the LanceDB query fails.
85
+ const chunkCounts = new Map();
86
+ for (const project of projects) {
87
+ const stats = yield (0, daemon_client_1.sendDaemonCommand)({ cmd: "project-stats", root: project.root }, { timeoutMs: 30000 });
88
+ if (stats.ok && typeof stats.chunks === "number") {
89
+ chunkCounts.set(project.root, stats.chunks);
90
+ }
91
+ }
92
+ return { watchers, chunkCounts };
93
+ }),
94
+ inProcess: () => __awaiter(this, void 0, void 0, function* () {
95
+ (0, watcher_store_1.listWatchers)(); // cleans stale entries as side effect
96
+ const watchers = new Map();
97
+ for (const project of projects) {
98
+ const watcher = (0, watcher_store_1.getWatcherForProject)(project.root);
99
+ if (watcher)
100
+ watchers.set(project.root, { status: watcher.status });
101
+ }
102
+ const chunkCounts = new Map();
103
+ try {
104
+ const { VectorDB } = yield Promise.resolve().then(() => __importStar(require("../lib/store/vector-db")));
105
+ const db = new VectorDB(config_1.PATHS.lancedbDir);
106
+ const table = yield db.ensureTable();
107
+ for (const project of projects) {
108
+ const prefix = project.root.endsWith("/")
109
+ ? project.root
110
+ : `${project.root}/`;
111
+ const rows = yield table
112
+ .query()
113
+ .select(["id"])
114
+ .where((0, filter_builder_1.pathStartsWith)(prefix))
115
+ .toArray();
116
+ chunkCounts.set(project.root, rows.length);
117
+ }
118
+ yield db.close();
119
+ }
120
+ catch (err) {
121
+ // A sandbox denial is not "the query failed" — let it surface as the
122
+ // one-line refusal instead of silently degrading to cached counts.
123
+ if ((0, store_access_1.classifyLeaseError)(err) === "sandboxed")
124
+ throw err;
125
+ console.warn(`[status] Failed to query LanceDB for live chunk counts, using cached counts`);
126
+ }
127
+ return { watchers, chunkCounts };
128
+ }),
129
+ });
130
+ });
131
+ }
57
132
  const style = {
58
133
  bold: (s) => `\x1b[1m${s}\x1b[22m`,
59
134
  dim: (s) => `\x1b[2m${s}\x1b[22m`,
@@ -102,34 +177,24 @@ Examples:
102
177
  var _a, _b, _c, _d;
103
178
  const globalConfig = (0, index_config_1.readGlobalConfig)();
104
179
  const projects = (0, project_registry_1.listProjects)();
105
- (0, watcher_store_1.listWatchers)(); // cleans stale entries as side effect
106
180
  const indexing = (0, lock_1.isLocked)(config_1.PATHS.globalRoot);
107
181
  const currentRoot = (0, project_root_1.findProjectRoot)(process.cwd());
108
- if (!opts.agent) {
109
- // Header
110
- console.log(`\n${style.bold("gmax")} · ${globalConfig.modelTier} (${globalConfig.vectorDim}d, ${globalConfig.embedMode})${indexing ? style.yellow(" · indexing...") : ""}`);
111
- }
112
- // Query live chunk counts from LanceDB
113
- const chunkCounts = new Map();
182
+ // Resolved before the header so a sandbox refusal prints one clean line.
183
+ let view;
114
184
  try {
115
- const { VectorDB } = yield Promise.resolve().then(() => __importStar(require("../lib/store/vector-db")));
116
- const db = new VectorDB(config_1.PATHS.lancedbDir);
117
- const table = yield db.ensureTable();
118
- for (const project of projects) {
119
- const prefix = project.root.endsWith("/")
120
- ? project.root
121
- : `${project.root}/`;
122
- const rows = yield table
123
- .query()
124
- .select(["id"])
125
- .where((0, filter_builder_1.pathStartsWith)(prefix))
126
- .toArray();
127
- chunkCounts.set(project.root, rows.length);
185
+ view = yield loadStatusView(projects);
186
+ }
187
+ catch (err) {
188
+ if ((0, store_access_1.reportStoreAccessRefusal)(err)) {
189
+ yield (0, exit_1.gracefulExit)(2);
190
+ return;
128
191
  }
129
- yield db.close();
192
+ throw err;
130
193
  }
131
- catch (_e) {
132
- console.warn(`[status] Failed to query LanceDB for live chunk counts, using cached counts`);
194
+ const { watchers, chunkCounts } = view;
195
+ if (!opts.agent) {
196
+ // Header
197
+ console.log(`\n${style.bold("gmax")} · ${globalConfig.modelTier} (${globalConfig.vectorDim}d, ${globalConfig.embedMode})${indexing ? style.yellow(" · indexing...") : ""}`);
133
198
  }
134
199
  if (projects.length === 0) {
135
200
  if (opts.agent) {
@@ -143,7 +208,7 @@ Examples:
143
208
  }
144
209
  if (opts.agent) {
145
210
  for (const project of projects) {
146
- const watcher = (0, watcher_store_1.getWatcherForProject)(project.root);
211
+ const watcher = watchers.get(project.root);
147
212
  const projectStatus = (_a = project.status) !== null && _a !== void 0 ? _a : "indexed";
148
213
  let st;
149
214
  if (projectStatus === "pending")
@@ -172,7 +237,7 @@ Examples:
172
237
  console.log();
173
238
  for (const project of projects) {
174
239
  const isCurrent = project.root === currentRoot;
175
- const watcher = (0, watcher_store_1.getWatcherForProject)(project.root);
240
+ const watcher = watchers.get(project.root);
176
241
  // Status column
177
242
  let statusStr;
178
243
  const projectStatus = (_c = project.status) !== null && _c !== void 0 ? _c : "indexed";