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
@@ -48,9 +48,12 @@ const os = __importStar(require("node:os"));
48
48
  const path = __importStar(require("node:path"));
49
49
  const commander_1 = require("commander");
50
50
  const config_1 = require("../config");
51
+ const embedding_status_1 = require("../lib/index/embedding-status");
51
52
  const index_config_1 = require("../lib/index/index-config");
53
+ const doctor_status_1 = require("../lib/utils/doctor-status");
52
54
  const exit_1 = require("../lib/utils/exit");
53
55
  const lock_1 = require("../lib/utils/lock");
56
+ const mlx_hf_cache_1 = require("../lib/utils/mlx-hf-cache");
54
57
  const project_registry_1 = require("../lib/utils/project-registry");
55
58
  const project_root_1 = require("../lib/utils/project-root");
56
59
  function formatSize(bytes) {
@@ -85,7 +88,7 @@ exports.doctor = new commander_1.Command("doctor")
85
88
  .option("--fix", "Auto-fix detected issues (compact, prune, remove stale locks)", false)
86
89
  .option("--agent", "Compact output for AI agents", false)
87
90
  .action((opts) => __awaiter(void 0, void 0, void 0, function* () {
88
- var _a, _b, _c, _d, _e, _f;
91
+ var _a, _b, _c, _d, _e, _f, _g, _h;
89
92
  if (!opts.agent)
90
93
  console.log("gmax Doctor\n");
91
94
  const root = config_1.PATHS.globalRoot;
@@ -106,33 +109,29 @@ exports.doctor = new commander_1.Command("doctor")
106
109
  if (!config_1.MODEL_TIERS[globalConfig.modelTier]) {
107
110
  console.log(`WARN Unknown model tier '${globalConfig.modelTier}', falling back to 'small'`);
108
111
  }
109
- const embedModel = globalConfig.embedMode === "gpu" ? tier.mlxModel : tier.onnxModel;
112
+ const embedModel = globalConfig.embedMode === "gpu"
113
+ ? ((_b = globalConfig.mlxModel) !== null && _b !== void 0 ? _b : tier.mlxModel)
114
+ : tier.onnxModel;
110
115
  if (!opts.agent) {
111
116
  console.log(`\nEmbed mode: ${globalConfig.embedMode} | Model tier: ${globalConfig.modelTier} (${tier.vectorDim}d)`);
112
117
  console.log(`Embed model: ${embedModel}`);
113
118
  console.log(`ColBERT model: ${config_1.MODEL_IDS.colbert}`);
114
- const modelStatuses = [embedModel, config_1.MODEL_IDS.colbert].map((id) => {
115
- const modelPath = path.join(models, ...id.split("/"));
116
- return { id, path: modelPath, exists: fs.existsSync(modelPath) };
117
- });
118
- modelStatuses.forEach(({ id, exists }) => {
119
- console.log(`${exists ? "ok" : "WARN"} ${id}: ${exists ? "downloaded" : "will download on first use"}`);
120
- });
121
- console.log(`\nLocal Project: ${process.cwd()}`);
122
- const projectRoot = (0, project_root_1.findProjectRoot)(process.cwd());
123
- if (projectRoot) {
124
- console.log(`ok Project root: ${projectRoot}`);
125
- console.log(` Centralized index at: ~/.gmax/lancedb/`);
126
- }
127
- else {
128
- console.log(`INFO No index found in current directory (run 'gmax index' to create one)`);
129
- }
130
- // Check MLX embed server
119
+ // Probe the MLX embed server once, up front: the gpu-mode embed model
120
+ // status is derived from it (the server, not the ONNX models dir, is the
121
+ // source of truth in gpu mode). Reused below for the "MLX Embed" line.
131
122
  let embedUp = false;
132
123
  let embedError = "";
133
124
  try {
134
125
  const res = yield fetch("http://127.0.0.1:8100/health");
135
- embedUp = res.ok;
126
+ const health = (yield res.json());
127
+ embedUp =
128
+ res.ok && health.status === "ok" && health.model === embedModel;
129
+ if (res.ok && health.model !== embedModel) {
130
+ embedError = `wrong model ${String(health.model)}; expected ${embedModel}`;
131
+ }
132
+ else if (res.ok && health.status !== "ok") {
133
+ embedError = "invalid health response";
134
+ }
136
135
  }
137
136
  catch (err) {
138
137
  embedError =
@@ -140,6 +139,28 @@ exports.doctor = new commander_1.Command("doctor")
140
139
  ? "connection refused"
141
140
  : err.message || String(err);
142
141
  }
142
+ // Embed model availability. In gpu mode the model is served by MLX from
143
+ // the pinned HF cache (~/.gmax/hf), never the ONNX models dir — report
144
+ // from the live server / HF cache. In cpu mode it's an ONNX model in
145
+ // ~/.gmax/models. ColBERT is always ONNX in-worker regardless of mode.
146
+ const onnxExists = (id) => fs.existsSync(path.join(models, ...id.split("/")));
147
+ const embedStatus = globalConfig.embedMode === "gpu"
148
+ ? (0, doctor_status_1.gpuEmbedModelStatus)(embedModel, { up: embedUp }, (0, mlx_hf_cache_1.isMlxModelCached)(embedModel))
149
+ : (0, doctor_status_1.onnxModelStatus)(embedModel, onnxExists(embedModel));
150
+ const colbertStatus = (0, doctor_status_1.onnxModelStatus)(config_1.MODEL_IDS.colbert, onnxExists(config_1.MODEL_IDS.colbert));
151
+ for (const s of [embedStatus, colbertStatus]) {
152
+ console.log(`${s.symbol} ${s.message}`);
153
+ }
154
+ console.log(`\nLocal Project: ${process.cwd()}`);
155
+ const projectRoot = (0, project_root_1.findProjectRoot)(process.cwd());
156
+ if (projectRoot) {
157
+ console.log(`ok Project root: ${projectRoot}`);
158
+ console.log(` Centralized index at: ~/.gmax/lancedb/`);
159
+ }
160
+ else {
161
+ console.log(`INFO No index found in current directory (run 'gmax index' to create one)`);
162
+ }
163
+ // MLX embed server (probed once above).
143
164
  console.log(`${embedUp ? "ok" : "WARN"} MLX Embed: ${embedUp ? "running (port 8100)" : `not running${embedError ? ` (${embedError})` : ""}`}`);
144
165
  if (embedUp) {
145
166
  try {
@@ -150,7 +171,7 @@ exports.doctor = new commander_1.Command("doctor")
150
171
  body: JSON.stringify({ texts: ["gmax health check"] }),
151
172
  });
152
173
  const embedData = yield embedRes.json();
153
- const dim = (_d = (_c = (_b = embedData === null || embedData === void 0 ? void 0 : embedData.vectors) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0;
174
+ const dim = (_e = (_d = (_c = embedData === null || embedData === void 0 ? void 0 : embedData.vectors) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0;
154
175
  const ms = Date.now() - start;
155
176
  const expectedDim = tier.vectorDim || 384;
156
177
  if (dim === expectedDim) {
@@ -164,11 +185,13 @@ exports.doctor = new commander_1.Command("doctor")
164
185
  console.log(`FAIL Embedding: test failed (${err.message || err})`);
165
186
  }
166
187
  }
167
- // Check summarizer server
188
+ // Check summarizer server. Down is INFO, not WARN — opt-in, respawns on
189
+ // demand, idles out after 10min (see summarizerServerStatus).
168
190
  const summarizerUp = yield fetch("http://127.0.0.1:8101/health")
169
191
  .then((r) => r.ok)
170
192
  .catch(() => false);
171
- console.log(`${summarizerUp ? "ok" : "WARN"} Summarizer: ${summarizerUp ? "running (port 8101)" : "not running"}`);
193
+ const summarizerStatus = (0, doctor_status_1.summarizerServerStatus)(summarizerUp);
194
+ console.log(`${summarizerStatus.symbol} ${summarizerStatus.message}`);
172
195
  }
173
196
  // --- Index Health ---
174
197
  let needsOptimize = false;
@@ -193,9 +216,8 @@ exports.doctor = new commander_1.Command("doctor")
193
216
  .where("length(summary) > 5")
194
217
  .select(["id"])
195
218
  .toArray()).length;
196
- const pct = Math.round((withSummary / totalChunks) * 100);
197
- const symbol = pct >= 90 ? "ok" : pct > 0 ? "WARN" : "FAIL";
198
- console.log(`${symbol} Summary coverage: ${withSummary}/${totalChunks} (${pct}%)`);
219
+ const coverage = (0, doctor_status_1.summaryCoverageStatus)(withSummary, totalChunks);
220
+ console.log(`${coverage.symbol} ${coverage.message}`);
199
221
  }
200
222
  else if (!opts.agent && totalChunks === 0) {
201
223
  console.log("INFO No indexed chunks yet");
@@ -251,21 +273,14 @@ exports.doctor = new commander_1.Command("doctor")
251
273
  ? "LOW"
252
274
  : "ok";
253
275
  }
254
- catch (_g) { }
276
+ catch (_j) { }
255
277
  const staleChunkerProjects = projects.filter((p) => {
256
278
  var _a;
257
279
  return p.status === "indexed" &&
258
280
  ((_a = p.chunkerVersion) !== null && _a !== void 0 ? _a : 1) < config_1.CONFIG.CHUNKER_VERSION;
259
281
  });
260
- // Projects whose stored embedding model/dim no longer matches the global
261
- // config. Visibility only — recovery is a manual `gmax index --reset`
262
- // (a dim change can't be auto-fixed in the shared fixed-dim table; that's
263
- // the deferred Phase 1B re-embed work).
264
282
  const staleEmbeddingProjects = projects.filter((p) => p.status === "indexed" &&
265
- (0, config_1.describeEmbeddingGap)({ modelTier: p.modelTier, vectorDim: p.vectorDim }, {
266
- modelTier: globalConfig.modelTier,
267
- vectorDim: globalConfig.vectorDim,
268
- }) !== null);
283
+ (0, embedding_status_1.projectEmbeddingStatus)(p, globalConfig).state === "stale");
269
284
  if (opts.agent) {
270
285
  const fields = [
271
286
  "index_health",
@@ -304,35 +319,30 @@ exports.doctor = new commander_1.Command("doctor")
304
319
  ].join("\t"));
305
320
  }
306
321
  for (const p of staleEmbeddingProjects) {
307
- const gap = (0, config_1.describeEmbeddingGap)({ modelTier: p.modelTier, vectorDim: p.vectorDim }, {
308
- modelTier: globalConfig.modelTier,
309
- vectorDim: globalConfig.vectorDim,
310
- });
311
- if (!gap)
322
+ const identity = (0, embedding_status_1.projectEmbeddingStatus)(p, globalConfig);
323
+ if (!identity.built)
312
324
  continue;
313
325
  console.log([
314
326
  "stale_embedding_project",
315
327
  `name=${p.name || path.basename(p.root)}`,
316
- `indexed_model=${gap.fromModel}`,
317
- `current_model=${gap.toModel}`,
318
- `indexed_dim=${gap.fromDim}`,
319
- `current_dim=${gap.toDim}`,
320
- `dim_changed=${gap.dimChanged}`,
321
- `severity=${gap.severity}`,
322
- // A dim change can't be fixed by a per-project reset (the shared
323
- // table is fixed-width) — point at the global rebuild instead.
324
- `fix=${gap.dimChanged ? config_1.REBUILD_COMMAND : `gmax index --reset (in ${p.root})`}`,
328
+ `indexed_model=${identity.built.tier}`,
329
+ `current_model=${identity.configured.tier}`,
330
+ `indexed_dim=${identity.built.vectorDim}`,
331
+ `current_dim=${identity.configured.vectorDim}`,
332
+ `indexed_fingerprint=${identity.built.fingerprint}`,
333
+ `current_fingerprint=${identity.configured.fingerprint}`,
334
+ "severity=breaking",
335
+ "fix=gmax repair --rebuild",
325
336
  ].join("\t"));
326
337
  }
327
338
  }
328
339
  else {
329
340
  console.log("\nIndex Health\n");
330
- // Physical schema width — a mismatch means every write throws until a
331
- // global rebuild (the shared fixed-width table can't be reshaped by a
332
- // per-project reset). Surfaced first because it's the most severe.
341
+ // Physical schema width mismatch remains fail-closed until the user
342
+ // explicitly runs the guarded whole-corpus rebuild.
333
343
  if (schemaGap) {
334
344
  console.log(`FAIL Schema: vector table is ${schemaGap.tableDim}d, config expects ${schemaGap.configDim}d`);
335
- console.log(` run '${config_1.REBUILD_COMMAND}' (drops + reindexes all projects at the new width)`);
345
+ console.log(" run 'gmax repair --rebuild' to rebuild the whole corpus");
336
346
  }
337
347
  else if (physicalDim) {
338
348
  console.log(`ok Schema: vector table is ${physicalDim}d`);
@@ -392,34 +402,13 @@ exports.doctor = new commander_1.Command("doctor")
392
402
  console.log(` run 'gmax index --reset' in ${p.root}`);
393
403
  }
394
404
  }
395
- // Index built with a different embedding model/dim than the current
396
- // config. A dim change is breaking (search scores are invalid until a
397
- // re-embed); a same-dim model swap is additive. Recovery differs by kind:
398
- // a same-dim model swap is fixed by a per-project `gmax index --reset`,
399
- // but a dim change can't be — the shared table is fixed-width, so it
400
- // needs the global rebuild (see the Schema check above).
401
405
  if (staleEmbeddingProjects.length > 0) {
402
- const gaps = staleEmbeddingProjects.map((p) => (0, config_1.describeEmbeddingGap)({ modelTier: p.modelTier, vectorDim: p.vectorDim }, {
403
- modelTier: globalConfig.modelTier,
404
- vectorDim: globalConfig.vectorDim,
405
- }));
406
- const anyBreaking = gaps.some((g) => (g === null || g === void 0 ? void 0 : g.severity) === "breaking");
407
- const anyDimChange = gaps.some((g) => g === null || g === void 0 ? void 0 : g.dimChanged);
408
- const headerFix = anyDimChange
409
- ? `run '${config_1.REBUILD_COMMAND}' (dim change needs a full rebuild)`
410
- : "run 'gmax index --reset' per project";
411
- console.log(`${anyBreaking ? "WARN" : "INFO"} Stale embedding: ${staleEmbeddingProjects.length} project(s) indexed with a different embedding model/dim — ${headerFix}`);
412
- staleEmbeddingProjects.forEach((p, i) => {
413
- const gap = gaps[i];
414
- if (!gap)
406
+ console.log(`WARN Stale embedding: ${staleEmbeddingProjects.length} project(s) use a different embedding generation run 'gmax repair --rebuild'`);
407
+ staleEmbeddingProjects.forEach((p) => {
408
+ const identity = (0, embedding_status_1.projectEmbeddingStatus)(p, globalConfig);
409
+ if (!identity.built)
415
410
  return;
416
- const change = gap.dimChanged
417
- ? `${gap.fromDim}d→${gap.toDim}d`
418
- : `model ${gap.fromModel}→${gap.toModel}`;
419
- console.log(` - ${p.name || path.basename(p.root)} (${change}, ${gap.severity})`);
420
- console.log(gap.dimChanged
421
- ? ` run '${config_1.REBUILD_COMMAND}'`
422
- : ` run 'gmax index --reset' in ${p.root}`);
411
+ console.log(` - ${p.name || path.basename(p.root)} (${identity.built.tier} ${identity.built.vectorDim}d → ${identity.configured.tier} ${identity.configured.vectorDim}d, breaking)`);
423
412
  });
424
413
  }
425
414
  // Projects
@@ -452,7 +441,7 @@ exports.doctor = new commander_1.Command("doctor")
452
441
  }
453
442
  yield mc.close();
454
443
  }
455
- catch (_h) { }
444
+ catch (_k) { }
456
445
  }
457
446
  }
458
447
  // --fix auto-remediation
@@ -497,8 +486,8 @@ exports.doctor = new commander_1.Command("doctor")
497
486
  if (!opts.agent)
498
487
  console.log(`... Reindexing ${name} (--reset)...`);
499
488
  const done = yield sendStreamingCommand({ cmd: "index", root: p.root, reset: true }, () => { }).catch((e) => ({ ok: false, error: String(e) }));
500
- if (done.ok) {
501
- (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, p), { status: "indexed", chunkerVersion: config_1.CONFIG.CHUNKER_VERSION, lastIndexed: new Date().toISOString(), chunkCount: (_e = done.indexed) !== null && _e !== void 0 ? _e : p.chunkCount }));
489
+ if (done.ok &&
490
+ (!("degraded" in done) || done.degraded !== true)) {
502
491
  const chunks = (_f = done.indexed) !== null && _f !== void 0 ? _f : 0;
503
492
  if (opts.agent) {
504
493
  console.log(`stale_chunker_reindexed\tname=${name}\tchunks=${chunks}`);
@@ -509,7 +498,11 @@ exports.doctor = new commander_1.Command("doctor")
509
498
  fixed++;
510
499
  }
511
500
  else if (!opts.agent) {
512
- console.log(`FAIL ${name}: reindex failed (${done.error})`);
501
+ const degraded = "degraded" in done && done.degraded === true;
502
+ const reason = degraded
503
+ ? `${(_g = ("scanErrors" in done ? done.scanErrors : 0)) !== null && _g !== void 0 ? _g : 0} scan errors, ${(_h = ("failedFiles" in done ? done.failedFiles : 0)) !== null && _h !== void 0 ? _h : 0} file failures`
504
+ : done.error;
505
+ console.log(`FAIL ${name}: reindex failed (${reason})`);
513
506
  }
514
507
  }
515
508
  }
@@ -51,6 +51,7 @@ const exit_1 = require("../lib/utils/exit");
51
51
  const filter_builder_1 = require("../lib/utils/filter-builder");
52
52
  const import_extractor_1 = require("../lib/utils/import-extractor");
53
53
  const language_1 = require("../lib/utils/language");
54
+ const path_containment_1 = require("../lib/utils/path-containment");
54
55
  const project_registry_1 = require("../lib/utils/project-registry");
55
56
  const project_root_1 = require("../lib/utils/project-root");
56
57
  const useColors = process.stdout.isTTY && !process.env.NO_COLOR;
@@ -129,7 +130,17 @@ exports.extract = new commander_1.Command("extract")
129
130
  exclude: opts.exclude,
130
131
  });
131
132
  const where = buildScopeWhere(scope, `array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}')`);
132
- const chunks = yield findSymbolChunks(vectorDb, where);
133
+ const indexedChunks = yield findSymbolChunks(vectorDb, where);
134
+ const chunks = indexedChunks.flatMap((chunk) => {
135
+ try {
136
+ return [
137
+ Object.assign(Object.assign({}, chunk), { path: (0, path_containment_1.resolveContainedFile)(projectRoot, chunk.path) }),
138
+ ];
139
+ }
140
+ catch (_a) {
141
+ return [];
142
+ }
143
+ });
133
144
  if (chunks.length === 0) {
134
145
  console.log((0, agent_errors_1.symbolNotFoundLines)(symbol, {
135
146
  agent: opts.agent,
@@ -47,7 +47,6 @@ const path = __importStar(require("node:path"));
47
47
  const commander_1 = require("commander");
48
48
  const config_1 = require("../config");
49
49
  const grammar_loader_1 = require("../lib/index/grammar-loader");
50
- const index_config_1 = require("../lib/index/index-config");
51
50
  const sync_helpers_1 = require("../lib/index/sync-helpers");
52
51
  const syncer_1 = require("../lib/index/syncer");
53
52
  const setup_helpers_1 = require("../lib/setup/setup-helpers");
@@ -71,7 +70,7 @@ Examples:
71
70
  gmax index --reset Full re-index from scratch
72
71
  `)
73
72
  .action((_args, cmd) => __awaiter(void 0, void 0, void 0, function* () {
74
- var _a, _b, _c, _d, _e;
73
+ var _a, _b, _c, _d, _e, _f;
75
74
  const options = cmd.optsWithGlobals();
76
75
  let vectorDb = null;
77
76
  const ac = new AbortController();
@@ -134,19 +133,12 @@ Examples:
134
133
  spinner.succeed(`Dry run complete(${done.processed} / ${done.total}) • would have indexed ${done.indexed} `);
135
134
  return;
136
135
  }
137
- const globalConfig = (0, index_config_1.readGlobalConfig)();
138
- (0, project_registry_1.registerProject)({
139
- root: projectRoot,
140
- name: path.basename(projectRoot),
141
- vectorDim: globalConfig.vectorDim,
142
- modelTier: globalConfig.modelTier,
143
- embedMode: globalConfig.embedMode,
144
- lastIndexed: new Date().toISOString(),
145
- chunkCount: (_d = done.indexed) !== null && _d !== void 0 ? _d : 0,
146
- status: "indexed",
147
- chunkerVersion: stampedChunkerVersion,
148
- });
149
- const failedFiles = (_e = done.failedFiles) !== null && _e !== void 0 ? _e : 0;
136
+ if (done.degraded === true) {
137
+ spinner.warn(`Indexing incomplete • ${(_d = done.scanErrors) !== null && _d !== void 0 ? _d : 0} scan errors • ${(_e = done.failedFiles) !== null && _e !== void 0 ? _e : 0} failed`);
138
+ process.exitCode = 1;
139
+ return;
140
+ }
141
+ const failedFiles = (_f = done.failedFiles) !== null && _f !== void 0 ? _f : 0;
150
142
  const failedSuffix = failedFiles > 0 ? ` • ${failedFiles} failed` : "";
151
143
  spinner.succeed(`Indexing complete(${done.processed} / ${done.total}) • indexed ${done.indexed}${failedSuffix} `);
152
144
  }
@@ -166,7 +158,7 @@ Examples:
166
158
  try {
167
159
  process.kill(watcher.pid, "SIGTERM");
168
160
  }
169
- catch (_f) { }
161
+ catch (_g) { }
170
162
  for (let i = 0; i < 50; i++) {
171
163
  if (!(0, watcher_store_1.isProcessRunning)(watcher.pid))
172
164
  break;
@@ -196,17 +188,24 @@ Examples:
196
188
  }));
197
189
  return;
198
190
  }
199
- const globalConfig = (0, index_config_1.readGlobalConfig)();
200
- (0, project_registry_1.registerProject)({
191
+ if (result.degraded) {
192
+ spinner.warn(`Indexing incomplete • ${result.scanErrors} scan errors • ${result.failedFiles} failed`);
193
+ process.exitCode = 1;
194
+ return;
195
+ }
196
+ const prefix = projectRoot.endsWith("/")
197
+ ? projectRoot
198
+ : `${projectRoot}/`;
199
+ const chunkCount = yield vectorDb.countRowsForPath(prefix);
200
+ (0, project_registry_1.stampProjectFullSync)({
201
201
  root: projectRoot,
202
202
  name: path.basename(projectRoot),
203
- vectorDim: globalConfig.vectorDim,
204
- modelTier: globalConfig.modelTier,
205
- embedMode: globalConfig.embedMode,
206
- lastIndexed: new Date().toISOString(),
207
- chunkCount: result.indexed,
208
- status: "indexed",
203
+ generation: result.generation,
204
+ embedMode: result.embedMode,
205
+ chunkCount,
209
206
  chunkerVersion: stampedChunkerVersion,
207
+ expectedFingerprint: result.registryExpectation.embeddingFingerprint,
208
+ expectedRebuildId: result.registryExpectation.rebuildId,
210
209
  });
211
210
  const failedSuffix = result.failedFiles > 0 ? ` • ${result.failedFiles} failed` : "";
212
211
  spinner.succeed(`Indexing complete(${result.processed} / ${result.total}) • indexed ${result.indexed}${failedSuffix} `);
@@ -47,6 +47,7 @@ const fs = __importStar(require("node:fs"));
47
47
  const os = __importStar(require("node:os"));
48
48
  const path = __importStar(require("node:path"));
49
49
  const commander_1 = require("commander");
50
+ const embedding_status_1 = require("../lib/index/embedding-status");
50
51
  const index_config_1 = require("../lib/index/index-config");
51
52
  const exit_1 = require("../lib/utils/exit");
52
53
  const project_registry_1 = require("../lib/utils/project-registry");
@@ -123,7 +124,7 @@ exports.list = new commander_1.Command("list")
123
124
  }));
124
125
  function showCurrentProject() {
125
126
  return __awaiter(this, void 0, void 0, function* () {
126
- var _a, _b, _c, _d;
127
+ var _a;
127
128
  const projectRoot = (_a = (0, project_root_1.findProjectRoot)(process.cwd())) !== null && _a !== void 0 ? _a : process.cwd();
128
129
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
129
130
  const entries = [
@@ -132,13 +133,17 @@ function showCurrentProject() {
132
133
  ];
133
134
  console.log(`\n${style.bold("Project")}: ${style.green(projectRoot)}`);
134
135
  console.log(`${style.dim("Data directory")}: ${paths.dataDir}\n`);
135
- const config = (0, index_config_1.readIndexConfig)(paths.configPath);
136
- if (config) {
137
- console.log(`${style.dim("Model")}: ${(_b = config.modelTier) !== null && _b !== void 0 ? _b : "small"} (${(_c = config.vectorDim) !== null && _c !== void 0 ? _c : 384}d)`);
138
- console.log(`${style.dim("Mode")}: ${(_d = config.embedMode) !== null && _d !== void 0 ? _d : "cpu"}`);
139
- if (config.indexedAt) {
140
- console.log(`${style.dim("Indexed")}: ${formatDate(new Date(config.indexedAt))}`);
136
+ const globalConfig = (0, index_config_1.readGlobalConfig)();
137
+ const project = (0, project_registry_1.getProject)(projectRoot);
138
+ if (project) {
139
+ const identity = (0, embedding_status_1.projectEmbeddingStatus)(project, globalConfig);
140
+ console.log(`${style.dim("Configured embedding")}: ${identity.configured.tier} (${identity.configured.vectorDim}d)`);
141
+ if (identity.built) {
142
+ console.log(`${style.dim("Built embedding")}: ${identity.built.tier} (${identity.built.vectorDim}d, ${identity.state})`);
141
143
  }
144
+ console.log(`${style.dim("Mode")}: ${globalConfig.embedMode}`);
145
+ if (project.lastIndexed)
146
+ console.log(`${style.dim("Indexed")}: ${formatDate(new Date(project.lastIndexed))}`);
142
147
  console.log();
143
148
  }
144
149
  for (const entry of entries) {
@@ -167,17 +172,21 @@ function showAllProjects() {
167
172
  // Header
168
173
  console.log(`${style.bold(pad("Project", nameWidth))} ${style.bold(pad("Path", pathWidth))} ${style.bold("Dims")} ${style.bold("Status")}`);
169
174
  for (const project of projects) {
170
- const dimMatch = project.vectorDim === globalConfig.vectorDim;
175
+ const identity = (0, embedding_status_1.projectEmbeddingStatus)(project, globalConfig);
171
176
  const dimsStr = `${project.vectorDim}d`;
172
- const status = dimMatch
173
- ? style.green("ok")
174
- : style.yellow("reindex needed");
177
+ const status = identity.state === "current"
178
+ ? style.green("current")
179
+ : identity.state === "legacy"
180
+ ? style.yellow("legacy")
181
+ : identity.state === "stale"
182
+ ? style.red("stale")
183
+ : style.dim("unbuilt");
175
184
  console.log(`${pad(project.name, nameWidth)} ${style.dim(pad(shortenPath(project.root), pathWidth))} ${pad(dimsStr, 4)} ${status}`);
176
185
  }
177
186
  console.log(`\n${style.dim("Global config")}: ${globalConfig.modelTier} (${globalConfig.vectorDim}d), ${globalConfig.embedMode}`);
178
- const needsReindex = projects.filter((p) => p.vectorDim !== globalConfig.vectorDim);
179
- if (needsReindex.length > 0) {
180
- console.log(style.yellow(`\n${needsReindex.length} project(s) need reindexing. Search will auto-reindex on first use.`));
187
+ const stale = projects.filter((project) => (0, embedding_status_1.projectEmbeddingStatus)(project, globalConfig).state === "stale");
188
+ if (stale.length > 0) {
189
+ console.log(style.yellow(`\n${stale.length} project(s) use a stale embedding generation. Existing indexes are preserved; run 'gmax repair --rebuild' to rebuild the whole corpus.`));
181
190
  }
182
191
  });
183
192
  }