grepmax 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +11 -6
  3. package/dist/commands/add.js +38 -31
  4. package/dist/commands/config.js +16 -15
  5. package/dist/commands/context.js +38 -13
  6. package/dist/commands/doctor.js +76 -83
  7. package/dist/commands/extract.js +12 -1
  8. package/dist/commands/index.js +23 -24
  9. package/dist/commands/list.js +23 -14
  10. package/dist/commands/mcp.js +278 -143
  11. package/dist/commands/peek.js +16 -5
  12. package/dist/commands/repair.js +54 -120
  13. package/dist/commands/search-output.js +30 -9
  14. package/dist/commands/search-run.js +75 -47
  15. package/dist/commands/search-skeletons.js +28 -18
  16. package/dist/commands/search.js +45 -49
  17. package/dist/commands/serve.js +415 -373
  18. package/dist/commands/setup.js +2 -2
  19. package/dist/commands/similar.js +5 -5
  20. package/dist/commands/skeleton.js +67 -41
  21. package/dist/commands/status.js +5 -2
  22. package/dist/commands/summarize.js +6 -0
  23. package/dist/commands/watch.js +102 -38
  24. package/dist/config.js +3 -0
  25. package/dist/lib/daemon/daemon.js +1101 -379
  26. package/dist/lib/daemon/ipc-handler.js +122 -13
  27. package/dist/lib/daemon/mlx-server-manager.js +268 -125
  28. package/dist/lib/daemon/process-manager.js +7 -4
  29. package/dist/lib/daemon/search-handler.js +23 -9
  30. package/dist/lib/daemon/watcher-manager.js +353 -110
  31. package/dist/lib/index/batch-processor.js +231 -67
  32. package/dist/lib/index/embedding-generation.js +109 -0
  33. package/dist/lib/index/embedding-status.js +23 -0
  34. package/dist/lib/index/file-policy.js +273 -0
  35. package/dist/lib/index/ignore-patterns.js +4 -0
  36. package/dist/lib/index/index-config.js +18 -4
  37. package/dist/lib/index/syncer.js +256 -79
  38. package/dist/lib/index/walker.js +66 -86
  39. package/dist/lib/index/watcher-batch.js +9 -0
  40. package/dist/lib/index/watcher.js +129 -3
  41. package/dist/lib/llm/server.js +6 -0
  42. package/dist/lib/search/searcher.js +30 -19
  43. package/dist/lib/store/store-lease.js +473 -0
  44. package/dist/lib/store/vector-db.js +302 -57
  45. package/dist/lib/utils/blocked-roots.js +63 -0
  46. package/dist/lib/utils/cross-project.js +7 -3
  47. package/dist/lib/utils/daemon-client.js +24 -1
  48. package/dist/lib/utils/daemon-launcher.js +38 -13
  49. package/dist/lib/utils/doctor-status.js +76 -0
  50. package/dist/lib/utils/file-utils.js +74 -4
  51. package/dist/lib/utils/keyed-mutex.js +101 -0
  52. package/dist/lib/utils/logger.js +57 -1
  53. package/dist/lib/utils/mlx-hf-cache.js +114 -0
  54. package/dist/lib/utils/operation-coordinator.js +146 -0
  55. package/dist/lib/utils/path-containment.js +106 -0
  56. package/dist/lib/utils/process.js +44 -0
  57. package/dist/lib/utils/project-registry.js +351 -3
  58. package/dist/lib/utils/scope-filter.js +3 -9
  59. package/dist/lib/utils/stale-hint.js +12 -19
  60. package/dist/lib/utils/watcher-launcher.js +5 -1
  61. package/dist/lib/utils/watcher-store.js +2 -2
  62. package/dist/lib/workers/colbert-math.js +15 -12
  63. package/dist/lib/workers/embeddings/colbert.js +3 -2
  64. package/dist/lib/workers/embeddings/granite.js +4 -3
  65. package/dist/lib/workers/embeddings/mlx-client.js +59 -16
  66. package/dist/lib/workers/orchestrator.js +39 -8
  67. package/dist/lib/workers/pool.js +146 -82
  68. package/dist/lib/workers/process-child.js +11 -2
  69. package/dist/lib/workers/serialized-handler.js +10 -0
  70. package/dist/lib/workers/worker.js +13 -4
  71. package/mlx-embed-server/server.py +21 -3
  72. package/mlx-embed-server/summarizer.py +8 -0
  73. package/package.json +4 -3
  74. package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
  75. package/plugins/grepmax/hooks/start.js +3 -170
@@ -52,6 +52,7 @@ const agent_errors_1 = require("../lib/utils/agent-errors");
52
52
  const exit_1 = require("../lib/utils/exit");
53
53
  const filter_builder_1 = require("../lib/utils/filter-builder");
54
54
  const language_1 = require("../lib/utils/language");
55
+ const path_containment_1 = require("../lib/utils/path-containment");
55
56
  const project_registry_1 = require("../lib/utils/project-registry");
56
57
  const project_root_1 = require("../lib/utils/project-root");
57
58
  const stale_hint_1 = require("../lib/utils/stale-hint");
@@ -65,9 +66,9 @@ const style = {
65
66
  };
66
67
  const MAX_CALLERS = 5;
67
68
  const MAX_CALLEES = 8;
68
- function extractSignature(filePath, startLine, endLine) {
69
+ function extractSignature(projectRoot, filePath, startLine, endLine) {
69
70
  try {
70
- const content = fs.readFileSync(filePath, "utf-8");
71
+ const content = fs.readFileSync((0, path_containment_1.resolveContainedFile)(projectRoot, filePath), "utf-8");
71
72
  const lines = content.split("\n");
72
73
  const chunk = lines.slice(startLine, endLine + 1);
73
74
  const bodyLines = chunk.length;
@@ -249,7 +250,17 @@ exports.peek = new commander_1.Command("peek")
249
250
  // Re-anchor chunk-level caller rows to actual call sites and dedupe —
250
251
  // getCallers() returns one row per chunk, which multiplies callers for
251
252
  // classes split across many chunks (verified: 3 real call sites → 66).
252
- const resolvedCallers = (0, callsites_1.resolveCallSites)(callerList, symbol).map((c) => {
253
+ const readableCallers = callerList.flatMap((caller) => {
254
+ try {
255
+ return [
256
+ Object.assign(Object.assign({}, caller), { file: (0, path_containment_1.resolveContainedFile)(projectRoot, caller.file) }),
257
+ ];
258
+ }
259
+ catch (_a) {
260
+ return [];
261
+ }
262
+ });
263
+ const resolvedCallers = (0, callsites_1.resolveCallSites)(readableCallers, symbol).map((c) => {
253
264
  var _a;
254
265
  return ({
255
266
  symbol: c.symbol,
@@ -294,7 +305,7 @@ exports.peek = new commander_1.Command("peek")
294
305
  }
295
306
  // Signature — all lines up to the opening brace, collapsed to one
296
307
  // line so parameters survive (first-line-only loses them).
297
- const { signatureOnly } = extractSignature(center.file, startLine, endLine);
308
+ const { signatureOnly } = extractSignature(projectRoot, center.file, startLine, endLine);
298
309
  const sigOnly = signatureOnly
299
310
  .split("\n")
300
311
  .map((l) => l.trim())
@@ -340,7 +351,7 @@ exports.peek = new commander_1.Command("peek")
340
351
  }
341
352
  console.log();
342
353
  // Signature with collapsed body
343
- const { signature } = extractSignature(center.file, startLine, endLine);
354
+ const { signature } = extractSignature(projectRoot, center.file, startLine, endLine);
344
355
  for (const line of signature.split("\n")) {
345
356
  console.log(` ${line}`);
346
357
  }
@@ -1,37 +1,4 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
3
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
4
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -41,50 +8,78 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
41
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
42
9
  });
43
10
  };
44
- var __importDefault = (this && this.__importDefault) || function (mod) {
45
- return (mod && mod.__esModule) ? mod : { "default": mod };
46
- };
47
11
  Object.defineProperty(exports, "__esModule", { value: true });
48
12
  exports.repair = void 0;
49
- const readline = __importStar(require("node:readline"));
50
13
  const commander_1 = require("commander");
51
- const ora_1 = __importDefault(require("ora"));
52
14
  const config_1 = require("../config");
53
15
  const index_config_1 = require("../lib/index/index-config");
54
16
  const vector_db_1 = require("../lib/store/vector-db");
17
+ const daemon_client_1 = require("../lib/utils/daemon-client");
55
18
  const exit_1 = require("../lib/utils/exit");
56
19
  const project_registry_1 = require("../lib/utils/project-registry");
57
- function confirm(message) {
58
- const rl = readline.createInterface({
59
- input: process.stdin,
60
- output: process.stdout,
61
- });
62
- return new Promise((resolve) => {
63
- rl.question(`${message} [y/N] `, (answer) => {
64
- rl.close();
65
- resolve(answer.toLowerCase() === "y");
66
- });
67
- });
68
- }
20
+ const REBUILD_PROTOCOL = 1;
69
21
  exports.repair = new commander_1.Command("repair")
70
22
  .description("Repair the centralized index (recover from a schema mismatch)")
71
- .option("--rebuild", "Drop the shared vector table and re-index every project at the configured embedding dim", false)
72
- .option("-y, --yes", "Skip the confirmation prompt", false)
23
+ .option("--rebuild", "Run the guarded exclusive whole-corpus generation rebuild", false)
73
24
  .addHelpText("after", `
74
25
  The shared LanceDB \`chunks\` table is fixed-width at creation. Switching model
75
26
  tiers (e.g. small 384d -> standard 768d) strands the table at the old width, so
76
27
  every write then fails. A per-project \`gmax index --reset\` only deletes rows —
77
- it can't change the table width. \`${config_1.REBUILD_COMMAND}\` drops the table and
78
- re-embeds all projects at the current dim.
28
+ it can't change the table width. The guarded rebuild runs only through a daemon
29
+ that advertises the exclusive-generation protocol; it never falls back to local
30
+ store access.
79
31
 
80
32
  Examples:
81
- gmax repair Show schema status and what a rebuild would do
82
- gmax repair --rebuild Drop the table and re-index every project
83
- gmax repair --rebuild -y Rebuild without the confirmation prompt
33
+ gmax repair Show schema status
34
+ gmax repair --rebuild Rebuild every registered project at configured width
84
35
  `)
85
36
  .action((opts) => __awaiter(void 0, void 0, void 0, function* () {
86
37
  var _a, _b, _c;
87
38
  try {
39
+ if (opts.rebuild) {
40
+ const running = yield (0, daemon_client_1.ensureDaemonRunning)();
41
+ if (!running)
42
+ throw new Error("failed to start the gmax daemon");
43
+ const ping = yield (0, daemon_client_1.sendDaemonCommand)({ cmd: "ping" }, { timeoutMs: 10000 });
44
+ const capabilities = ping.capabilities && typeof ping.capabilities === "object"
45
+ ? ping.capabilities
46
+ : null;
47
+ if (!ping.ok ||
48
+ (capabilities === null || capabilities === void 0 ? void 0 : capabilities.exclusiveGenerationRebuild) !== REBUILD_PROTOCOL) {
49
+ throw new Error("running daemon does not support guarded rebuild protocol v1; restart the gmax daemon and retry");
50
+ }
51
+ const done = yield (0, daemon_client_1.sendStreamingCommand)({ cmd: "repair-v2", protocol: REBUILD_PROTOCOL }, (progress) => {
52
+ var _a;
53
+ const phase = String((_a = progress.phase) !== null && _a !== void 0 ? _a : "rebuild");
54
+ const project = typeof progress.project === "string"
55
+ ? ` ${progress.project}`
56
+ : "";
57
+ const counts = typeof progress.processed === "number" &&
58
+ typeof progress.total === "number"
59
+ ? ` ${progress.processed}/${progress.total}`
60
+ : "";
61
+ const message = typeof progress.message === "string"
62
+ ? `: ${progress.message}`
63
+ : "";
64
+ console.log(`[${phase}]${project}${counts}${message}`);
65
+ }, { timeoutMs: 24 * 60 * 60 * 1000 });
66
+ if (!done.ok) {
67
+ const blockers = Array.isArray(done.blockers)
68
+ ? ` Blockers: ${done.blockers
69
+ .map((owner) => {
70
+ var _a, _b;
71
+ const value = owner;
72
+ return `${(_a = value.role) !== null && _a !== void 0 ? _a : "unknown"} pid=${(_b = value.pid) !== null && _b !== void 0 ? _b : "?"}`;
73
+ })
74
+ .join(", ")}.`
75
+ : "";
76
+ throw new Error(`${String((_a = done.error) !== null && _a !== void 0 ? _a : "rebuild failed")}.${blockers}`);
77
+ }
78
+ console.log(`Rebuild complete: ${String((_b = done.completed) !== null && _b !== void 0 ? _b : 0)}/${String((_c = done.total) !== null && _c !== void 0 ? _c : 0)} projects indexed.`);
79
+ if (typeof done.warning === "string")
80
+ console.warn(done.warning);
81
+ return;
82
+ }
88
83
  const globalConfig = (0, index_config_1.readGlobalConfig)();
89
84
  const configDim = globalConfig.vectorDim;
90
85
  // Physical width of the on-disk table (null if no table yet). Opened
@@ -106,74 +101,13 @@ Examples:
106
101
  console.log(dimLine);
107
102
  if (!opts.rebuild) {
108
103
  if (physicalDim != null && physicalDim !== configDim) {
109
- console.log(`\nRun '${config_1.REBUILD_COMMAND}' to drop the table and re-index ` +
110
- `${projects.length} project(s) at ${configDim}d.`);
104
+ console.log(`\nRun ${config_1.REBUILD_COMMAND} to rebuild ${projects.length} project(s) at ${configDim}d.`);
111
105
  }
112
106
  else {
113
- console.log(`\nNothing to repair. Pass --rebuild to force a full drop + re-index anyway.`);
114
- }
115
- return;
116
- }
117
- if (projects.length === 0) {
118
- console.log("\nNo indexed projects to rebuild.");
119
- return;
120
- }
121
- const totalChunks = projects.reduce((sum, p) => { var _a; return sum + ((_a = p.chunkCount) !== null && _a !== void 0 ? _a : 0); }, 0);
122
- console.log(`\nThis will DROP the shared vector table and re-embed ${projects.length} project(s)` +
123
- (totalChunks > 0
124
- ? ` (~${totalChunks.toLocaleString()} chunks).`
125
- : "."));
126
- for (const p of projects) {
127
- console.log(` - ${p.name}`);
128
- }
129
- if (!opts.yes) {
130
- const ok = yield confirm("\nContinue?");
131
- if (!ok) {
132
- console.log("Cancelled.");
133
- return;
107
+ console.log("\nNo schema mismatch detected.");
134
108
  }
135
- }
136
- // The rebuild must run through the daemon — it is the single writer for
137
- // the shared table. Refuse rather than risk a torn drop alongside a
138
- // daemon mid-flush.
139
- const { ensureDaemonRunning, sendStreamingCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
140
- if (!(yield ensureDaemonRunning())) {
141
- console.error("Could not start the gmax daemon. Start it with 'gmax watch --daemon -b' and retry.");
142
- process.exitCode = 1;
143
109
  return;
144
110
  }
145
- const spinner = (0, ora_1.default)({
146
- text: "Dropping vector table...",
147
- isSilent: !process.stdout.isTTY,
148
- }).start();
149
- try {
150
- const done = yield sendStreamingCommand({ cmd: "repair" }, (msg) => {
151
- var _a, _b, _c, _d;
152
- if (msg.phase === "drop") {
153
- spinner.text = "Dropping vector table...";
154
- }
155
- else if (msg.phase === "reindex") {
156
- const doneN = (_a = msg.projectsDone) !== null && _a !== void 0 ? _a : 0;
157
- const totalN = (_b = msg.projectsTotal) !== null && _b !== void 0 ? _b : projects.length;
158
- const processed = (_c = msg.processed) !== null && _c !== void 0 ? _c : 0;
159
- const total = (_d = msg.total) !== null && _d !== void 0 ? _d : 0;
160
- const counts = total > 0 ? ` ${processed}/${total}` : "";
161
- spinner.text = `Re-indexing ${msg.project} (${doneN + 1}/${totalN})${counts}`;
162
- }
163
- });
164
- if (!done.ok) {
165
- spinner.fail("Rebuild failed");
166
- throw new Error((_a = done.error) !== null && _a !== void 0 ? _a : "daemon repair failed");
167
- }
168
- const rebuilt = (_b = done.projects) !== null && _b !== void 0 ? _b : 0;
169
- const indexed = (_c = done.indexed) !== null && _c !== void 0 ? _c : 0;
170
- spinner.succeed(`Rebuilt ${rebuilt} project(s) at ${configDim}d • ${indexed.toLocaleString()} chunks`);
171
- }
172
- catch (e) {
173
- if (spinner.isSpinning)
174
- spinner.fail("Rebuild failed");
175
- throw e;
176
- }
177
111
  }
178
112
  catch (error) {
179
113
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -51,6 +51,7 @@ const index_state_footer_1 = require("../lib/output/index-state-footer");
51
51
  const cross_project_1 = require("../lib/utils/cross-project");
52
52
  const formatter_1 = require("../lib/utils/formatter");
53
53
  const import_extractor_1 = require("../lib/utils/import-extractor");
54
+ const path_containment_1 = require("../lib/utils/path-containment");
54
55
  const search_skeletons_1 = require("./search-skeletons");
55
56
  /**
56
57
  * Standalone HTTP-server search path. The per-project server answers the query
@@ -70,7 +71,7 @@ function executeServerSearch(params) {
70
71
  return __awaiter(this, void 0, void 0, function* () {
71
72
  const { server, pattern, exec_path, projectRootForServer, options, minScore, } = params;
72
73
  try {
73
- const response = yield fetch(`http://localhost:${server.port}/search`, {
74
+ const response = yield fetch(`http://127.0.0.1:${server.port}/search`, {
74
75
  method: "POST",
75
76
  headers: { "Content-Type": "application/json" },
76
77
  body: JSON.stringify({
@@ -79,6 +80,11 @@ function executeServerSearch(params) {
79
80
  path: exec_path
80
81
  ? path.relative(projectRootForServer, path.resolve(exec_path))
81
82
  : undefined,
83
+ in: options.in,
84
+ exclude: options.exclude,
85
+ file: options.file,
86
+ lang: options.lang,
87
+ role: options.role,
82
88
  }),
83
89
  });
84
90
  if (response.ok) {
@@ -119,8 +125,15 @@ function executeServerSearch(params) {
119
125
  }
120
126
  if (options.agent) {
121
127
  const importCache = new Map();
122
- const getImportsForFile = (absPath) => {
128
+ const getImportsForFile = (indexedPath) => {
123
129
  var _a;
130
+ let absPath;
131
+ try {
132
+ absPath = (0, path_containment_1.resolveContainedFile)(projectRootForServer, indexedPath);
133
+ }
134
+ catch (_b) {
135
+ return "";
136
+ }
124
137
  if (!options.imports || !absPath)
125
138
  return "";
126
139
  if (!importCache.has(absPath)) {
@@ -212,15 +225,23 @@ function renderSearchOutput(params) {
212
225
  }
213
226
  // Build import cache when --imports is requested
214
227
  const importCache = new Map();
215
- const getImportsForFile = (absPath) => {
228
+ const getImportsForRoot = (root) => (indexedPath) => {
216
229
  var _a;
217
- if (!options.imports || !absPath)
230
+ if (!options.imports || !indexedPath)
231
+ return "";
232
+ let absPath;
233
+ try {
234
+ absPath = (0, path_containment_1.resolveContainedFile)(root, indexedPath);
235
+ }
236
+ catch (_b) {
218
237
  return "";
238
+ }
219
239
  if (!importCache.has(absPath)) {
220
240
  importCache.set(absPath, (0, import_extractor_1.extractImports)(absPath));
221
241
  }
222
242
  return (_a = importCache.get(absPath)) !== null && _a !== void 0 ? _a : "";
223
243
  };
244
+ const getImportsForFile = getImportsForRoot(effectiveRoot);
224
245
  // Agent mode: ultra-compact one-line-per-result output
225
246
  const resultCount = filteredData.length;
226
247
  // Cross-project (Phase 6): render grouped by owning project so idioms
@@ -259,7 +280,7 @@ function renderSearchOutput(params) {
259
280
  body = (0, agent_search_formatter_1.formatAgentSearchResults)(g.items, g.root, {
260
281
  includeImports: options.imports,
261
282
  query: pattern,
262
- getImportsForFile,
283
+ getImportsForFile: getImportsForRoot(g.root),
263
284
  explain: options.explain,
264
285
  });
265
286
  }
@@ -377,12 +398,10 @@ function renderSearchOutput(params) {
377
398
  // aborting the loop — recovering budget the old greedy `break` wasted.
378
399
  const candidates = filteredData.map((r, idx) => {
379
400
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
380
- const absP = (_c = (_a = r.path) !== null && _a !== void 0 ? _a : (_b = r.metadata) === null || _b === void 0 ? void 0 : _b.path) !== null && _c !== void 0 ? _c : "";
401
+ const indexedPath = (_c = (_a = r.path) !== null && _a !== void 0 ? _a : (_b = r.metadata) === null || _b === void 0 ? void 0 : _b.path) !== null && _c !== void 0 ? _c : "";
381
402
  const startLine = (_g = (_e = (_d = r.startLine) !== null && _d !== void 0 ? _d : r.start_line) !== null && _e !== void 0 ? _e : (_f = r.generated_metadata) === null || _f === void 0 ? void 0 : _f.start_line) !== null && _g !== void 0 ? _g : 0;
382
403
  const endLine = (_l = (_j = (_h = r.endLine) !== null && _h !== void 0 ? _h : r.end_line) !== null && _j !== void 0 ? _j : (_k = r.generated_metadata) === null || _k === void 0 ? void 0 : _k.end_line) !== null && _l !== void 0 ? _l : startLine;
383
- const relPath = absP.startsWith(projectRoot)
384
- ? absP.slice(projectRoot.length + 1)
385
- : absP;
404
+ let relPath = "(invalid indexed path)";
386
405
  const role = r.role || "IMPLEMENTATION";
387
406
  const symbol = Array.isArray(r.defined_symbols) &&
388
407
  r.defined_symbols.length > 0
@@ -390,6 +409,8 @@ function renderSearchOutput(params) {
390
409
  : "";
391
410
  let blobText;
392
411
  try {
412
+ const absP = (0, path_containment_1.resolveContainedFile)(projectRoot, indexedPath);
413
+ relPath = path.relative(fs.realpathSync(projectRoot), absP);
393
414
  const content = fs.readFileSync(absP, "utf-8");
394
415
  const allLines = content.split("\n");
395
416
  const body = allLines
@@ -42,15 +42,23 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
42
42
  });
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.shouldFallbackFromDaemonError = shouldFallbackFromDaemonError;
45
46
  exports.runSearch = runSearch;
46
47
  const path = __importStar(require("node:path"));
47
48
  const config_1 = require("../config");
49
+ const embedding_status_1 = require("../lib/index/embedding-status");
48
50
  const grammar_loader_1 = require("../lib/index/grammar-loader");
51
+ const index_config_1 = require("../lib/index/index-config");
49
52
  const sync_helpers_1 = require("../lib/index/sync-helpers");
50
53
  const syncer_1 = require("../lib/index/syncer");
51
54
  const searcher_1 = require("../lib/search/searcher");
52
55
  const vector_db_1 = require("../lib/store/vector-db");
53
56
  const lock_1 = require("../lib/utils/lock");
57
+ const project_registry_1 = require("../lib/utils/project-registry");
58
+ const DAEMON_FALLBACK_ERRORS = new Set(["ENOENT", "ECONNREFUSED"]);
59
+ function shouldFallbackFromDaemonError(error) {
60
+ return typeof error === "string" && DAEMON_FALLBACK_ERRORS.has(error);
61
+ }
54
62
  /**
55
63
  * Acquire search results, picking the path: daemon-mediated first (ships the
56
64
  * query over IPC to the already-warm daemon), in-process fallback otherwise
@@ -68,48 +76,65 @@ function runSearch(params) {
68
76
  // caller for closing after render, or (b) closed here if indexing throws.
69
77
  let openedDb = null;
70
78
  try {
79
+ const selectedRoots = Array.isArray(searchFilters.projectRoots)
80
+ ? new Set(searchFilters.projectRoots)
81
+ : new Set([projectRoot]);
82
+ (0, embedding_status_1.assertEmbeddingSearchCompatible)((0, project_registry_1.listProjects)().filter((project) => selectedRoots.has(project.root)), (0, index_config_1.readGlobalConfig)());
71
83
  // Daemon-mediated search: ships query+args over IPC, daemon runs the
72
84
  // hybrid+rerank against its already-warm VectorDB and worker pool.
73
85
  // Drops cold-start cost (~17s wall, 6GB RAM in the CLI) to <1s. Falls
74
- // back to in-process on any failure.
86
+ // back to in-process only when no daemon socket is available. A live daemon
87
+ // error (busy, rebuilding, timeout, protocol, scope, store failure) must not
88
+ // open the shared store concurrently.
75
89
  let searchResult = null;
76
90
  let precomputedSkeletons;
77
91
  let precomputedGraph;
78
92
  let indexState;
79
93
  if (!options.sync && !options.dryRun) {
80
94
  try {
81
- const { isDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
82
- if (yield isDaemonRunning()) {
83
- const resp = yield sendDaemonCommand({
84
- cmd: "search",
85
- projectRoot: effectiveRoot,
86
- query: pattern,
87
- limit: parseInt(options.m, 10),
88
- filters: Object.keys(searchFilters).length > 0
89
- ? searchFilters
90
- : undefined,
91
- pathPrefix: pathFilter,
92
- rerank: process.env.GMAX_RERANK === "1",
93
- explain: options.explain,
94
- seeds,
95
- includeSkeletons: options.skeleton,
96
- includeGraph: options.symbol,
97
- }, { timeoutMs: 60000 });
98
- if (resp.ok) {
99
- searchResult = {
100
- data: resp.data,
101
- warnings: resp.warnings,
102
- };
103
- precomputedSkeletons = resp.skeletons;
104
- precomputedGraph = resp.graph;
105
- indexState = resp.indexState;
106
- }
107
- else if (process.env.GMAX_DEBUG === "1") {
108
- console.error(`[search] daemon path unavailable: ${(_a = resp.error) !== null && _a !== void 0 ? _a : "unknown"}`);
109
- }
95
+ const { sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
96
+ const crossProject = Array.isArray(searchFilters.projectRoots);
97
+ const resp = yield sendDaemonCommand({
98
+ cmd: crossProject ? "search-v2" : "search",
99
+ projectRoot: effectiveRoot,
100
+ query: pattern,
101
+ limit: parseInt(options.m, 10),
102
+ filters: Object.keys(searchFilters).length > 0 ? searchFilters : undefined,
103
+ pathPrefix: pathFilter,
104
+ rerank: process.env.GMAX_RERANK === "1",
105
+ explain: options.explain,
106
+ seeds,
107
+ includeSkeletons: options.skeleton,
108
+ includeGraph: options.symbol,
109
+ }, { timeoutMs: 60000 });
110
+ if (resp.ok) {
111
+ searchResult = {
112
+ data: resp.data,
113
+ warnings: resp.warnings,
114
+ };
115
+ precomputedSkeletons = resp.skeletons;
116
+ precomputedGraph = resp.graph;
117
+ indexState = resp.indexState;
118
+ }
119
+ else if (!shouldFallbackFromDaemonError(resp.error)) {
120
+ const detail = typeof resp.hint === "string"
121
+ ? `: ${resp.hint}`
122
+ : typeof resp.error === "string"
123
+ ? `: ${resp.error}`
124
+ : "";
125
+ throw new Error(`Daemon search failed${detail}`);
126
+ }
127
+ else if (process.env.GMAX_DEBUG === "1") {
128
+ console.error(`[search] daemon path unavailable: ${(_a = resp.error) !== null && _a !== void 0 ? _a : "unknown"}`);
110
129
  }
111
130
  }
112
131
  catch (err) {
132
+ const code = err === null || err === void 0 ? void 0 : err.code;
133
+ if ((err instanceof Error &&
134
+ err.message.startsWith("Daemon search failed")) ||
135
+ !shouldFallbackFromDaemonError(code)) {
136
+ throw err;
137
+ }
113
138
  if (process.env.GMAX_DEBUG === "1") {
114
139
  console.error("[search] daemon attempt threw:", err);
115
140
  }
@@ -176,23 +201,26 @@ function runSearch(params) {
176
201
  return { kind: "dry-run", vectorDb: openedDb };
177
202
  }
178
203
  yield vectorDb.createFTSIndex();
179
- // Update registry after sync
180
- const { readGlobalConfig } = yield Promise.resolve().then(() => __importStar(require("../lib/index/index-config")));
181
- const { registerProject } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/project-registry")));
182
- const gc = readGlobalConfig();
183
- registerProject({
184
- root: projectRoot,
185
- name: path.basename(projectRoot),
186
- vectorDim: gc.vectorDim,
187
- modelTier: gc.modelTier,
188
- embedMode: gc.embedMode,
189
- lastIndexed: new Date().toISOString(),
190
- chunkCount: result.indexed,
191
- status: "indexed",
192
- chunkerVersion: config_1.CONFIG.CHUNKER_VERSION,
193
- });
194
- const failedSuffix = result.failedFiles > 0 ? ` • ${result.failedFiles} failed` : "";
195
- spinner.succeed(`${options.sync ? "Indexing" : "Initial indexing"} complete (${result.processed}/${result.total}) • indexed ${result.indexed}${failedSuffix}`);
204
+ if (result.degraded) {
205
+ spinner.warn(`Indexing incomplete (${result.processed}/${result.total}) ${result.scanErrors} scan errors • ${result.failedFiles} failed. Results may be partial.`);
206
+ }
207
+ else {
208
+ const prefix = projectRoot.endsWith("/")
209
+ ? projectRoot
210
+ : `${projectRoot}/`;
211
+ const chunkCount = yield vectorDb.countRowsForPath(prefix);
212
+ (0, project_registry_1.stampProjectFullSync)({
213
+ root: projectRoot,
214
+ name: path.basename(projectRoot),
215
+ generation: result.generation,
216
+ embedMode: result.embedMode,
217
+ chunkCount,
218
+ chunkerVersion: config_1.CONFIG.CHUNKER_VERSION,
219
+ expectedFingerprint: result.registryExpectation.embeddingFingerprint,
220
+ expectedRebuildId: result.registryExpectation.rebuildId,
221
+ });
222
+ spinner.succeed(`${options.sync ? "Indexing" : "Initial indexing"} complete (${result.processed}/${result.total}) • indexed ${result.indexed}`);
223
+ }
196
224
  }
197
225
  catch (e) {
198
226
  spinner.fail("Indexing failed");
@@ -44,9 +44,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.outputSkeletons = outputSkeletons;
46
46
  const fs = __importStar(require("node:fs"));
47
- const path = __importStar(require("node:path"));
48
47
  const skeleton_1 = require("../lib/skeleton");
49
48
  const retriever_1 = require("../lib/skeleton/retriever");
49
+ const path_containment_1 = require("../lib/utils/path-containment");
50
50
  // Reuse Skeletonizer instance
51
51
  let globalSkeletonizer = null;
52
52
  function outputSkeletons(results, projectRoot, limit, db, precomputed) {
@@ -77,10 +77,19 @@ function outputSkeletons(results, projectRoot, limit, db, precomputed) {
77
77
  const skeletonOpts = { includeSummary: true };
78
78
  const skeletonResults = [];
79
79
  for (const filePath of filesToProcess) {
80
- // Paths from search results are now absolute (centralized index)
81
- const absPath = path.isAbsolute(filePath)
82
- ? filePath
83
- : path.resolve(projectRoot, filePath);
80
+ let absPath;
81
+ try {
82
+ absPath = (0, path_containment_1.resolveContainedFile)(projectRoot, filePath);
83
+ }
84
+ catch (_c) {
85
+ skeletonResults.push({
86
+ file: filePath,
87
+ skeleton: `// File not readable: ${filePath}`,
88
+ tokens: 0,
89
+ error: "File not readable",
90
+ });
91
+ continue;
92
+ }
84
93
  // 0. Daemon-supplied (preferred — already-warm DB lookup, no cold open)
85
94
  const fromDaemon = (_b = precomputed === null || precomputed === void 0 ? void 0 : precomputed[absPath]) !== null && _b !== void 0 ? _b : precomputed === null || precomputed === void 0 ? void 0 : precomputed[filePath];
86
95
  if (fromDaemon) {
@@ -104,24 +113,25 @@ function outputSkeletons(results, projectRoot, limit, db, precomputed) {
104
113
  }
105
114
  }
106
115
  // 2. Fallback to fresh generation
107
- yield globalSkeletonizer.init();
108
- if (!fs.existsSync(absPath)) {
116
+ try {
117
+ yield globalSkeletonizer.init();
118
+ const content = fs.readFileSync(absPath, "utf-8");
119
+ const res = yield globalSkeletonizer.skeletonizeFile(absPath, content, skeletonOpts);
120
+ skeletonResults.push({
121
+ file: filePath,
122
+ skeleton: res.skeleton,
123
+ tokens: res.tokenEstimate,
124
+ error: res.error,
125
+ });
126
+ }
127
+ catch (_d) {
109
128
  skeletonResults.push({
110
129
  file: filePath,
111
- skeleton: `// File not found: ${filePath}`,
130
+ skeleton: `// File not readable: ${filePath}`,
112
131
  tokens: 0,
113
- error: "File not found",
132
+ error: "File not readable",
114
133
  });
115
- continue;
116
134
  }
117
- const content = fs.readFileSync(absPath, "utf-8");
118
- const res = yield globalSkeletonizer.skeletonizeFile(absPath, content, skeletonOpts);
119
- skeletonResults.push({
120
- file: filePath,
121
- skeleton: res.skeleton,
122
- tokens: res.tokenEstimate,
123
- error: res.error,
124
- });
125
135
  }
126
136
  // Since search doesn't support --json explicitly yet, we just print text.
127
137
  // But if we ever add it, we have the structure.