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
@@ -142,10 +142,10 @@ exports.setup = new commander_1.Command("setup")
142
142
  const tierChanged = existingConfig.modelTier !== modelTier;
143
143
  const modeChanged = existingConfig.embedMode !== embedMode;
144
144
  if (tierChanged) {
145
- p.log.warn(`Model tier changed (${(_d = existingConfig.vectorDim) !== null && _d !== void 0 ? _d : 384}d → ${selectedTier.vectorDim}d). Existing indexes will be rebuilt on next use.`);
145
+ p.log.warn(`Model tier changed (${(_d = existingConfig.vectorDim) !== null && _d !== void 0 ? _d : 384}d → ${selectedTier.vectorDim}d). Existing indexes are preserved and search is blocked until you run 'gmax repair --rebuild'.`);
146
146
  }
147
147
  else if (modeChanged) {
148
- p.log.warn("Embedding mode changed. Run `gmax serve` to apply the new settings.");
148
+ p.log.warn("Embedding mode changed. Restart the gmax daemon to apply the new settings.");
149
149
  }
150
150
  }
151
151
  // Step 8: Install plugins for detected clients
@@ -43,13 +43,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.similar = void 0;
46
- const path = __importStar(require("node:path"));
47
46
  const commander_1 = require("commander");
48
47
  const vector_db_1 = require("../lib/store/vector-db");
49
48
  const agent_errors_1 = require("../lib/utils/agent-errors");
50
49
  const arrow_1 = require("../lib/utils/arrow");
51
50
  const exit_1 = require("../lib/utils/exit");
52
51
  const filter_builder_1 = require("../lib/utils/filter-builder");
52
+ const path_containment_1 = require("../lib/utils/path-containment");
53
53
  const project_registry_1 = require("../lib/utils/project-registry");
54
54
  const project_root_1 = require("../lib/utils/project-root");
55
55
  const stale_hint_1 = require("../lib/utils/stale-hint");
@@ -81,9 +81,9 @@ exports.similar = new commander_1.Command("similar")
81
81
  // Look up the source chunk's vector
82
82
  let sourceRows;
83
83
  if (isFile) {
84
- const absPath = target.startsWith("/")
85
- ? target
86
- : path.resolve(projectRoot, target);
84
+ const absPath = (0, path_containment_1.resolveContainedPath)(projectRoot, target, {
85
+ verifyExistingTarget: true,
86
+ });
87
87
  sourceRows = yield table
88
88
  .query()
89
89
  .select(["vector", "path", "defined_symbols", "start_line"])
@@ -95,7 +95,7 @@ exports.similar = new commander_1.Command("similar")
95
95
  sourceRows = yield table
96
96
  .query()
97
97
  .select(["vector", "path", "defined_symbols", "start_line"])
98
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(target)}')`)
98
+ .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(target)}') AND ${(0, filter_builder_1.pathStartsWith)(`${projectRoot}/`)}`)
99
99
  .limit(1)
100
100
  .toArray();
101
101
  }
@@ -52,8 +52,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
52
52
  Object.defineProperty(exports, "__esModule", { value: true });
53
53
  exports.skeleton = void 0;
54
54
  const fs = __importStar(require("node:fs"));
55
- const path = __importStar(require("node:path"));
56
55
  const commander_1 = require("commander");
56
+ const config_1 = require("../config");
57
57
  const sync_helpers_1 = require("../lib/index/sync-helpers");
58
58
  const syncer_1 = require("../lib/index/syncer");
59
59
  const searcher_1 = require("../lib/search/searcher");
@@ -62,20 +62,11 @@ const retriever_1 = require("../lib/skeleton/retriever");
62
62
  const skeletonizer_1 = require("../lib/skeleton/skeletonizer");
63
63
  const vector_db_1 = require("../lib/store/vector-db");
64
64
  const exit_1 = require("../lib/utils/exit");
65
+ const file_utils_1 = require("../lib/utils/file-utils");
66
+ const filter_builder_1 = require("../lib/utils/filter-builder");
67
+ const path_containment_1 = require("../lib/utils/path-containment");
65
68
  const project_registry_1 = require("../lib/utils/project-registry");
66
69
  const project_root_1 = require("../lib/utils/project-root");
67
- /**
68
- * Resolve a relative path across all indexed projects.
69
- * Returns the first match found, or null.
70
- */
71
- function resolveAcrossProjects(relativePath) {
72
- for (const project of (0, project_registry_1.listProjects)()) {
73
- const candidate = path.join(project.root, relativePath);
74
- if (fs.existsSync(candidate))
75
- return candidate;
76
- }
77
- return null;
78
- }
79
70
  /**
80
71
  * Check if target looks like a file path.
81
72
  */
@@ -94,12 +85,16 @@ function isSymbolLike(target) {
94
85
  /**
95
86
  * Find a file by symbol name in the index.
96
87
  */
97
- function findFileBySymbol(symbol, db) {
88
+ function findFileBySymbol(symbol, db, projectRoot) {
98
89
  return __awaiter(this, void 0, void 0, function* () {
99
90
  try {
100
91
  const table = yield db.ensureTable();
101
92
  // Search for files that define this symbol
102
- const results = yield table.search(symbol).limit(10).toArray();
93
+ const results = yield table
94
+ .search(symbol)
95
+ .where((0, filter_builder_1.pathStartsWith)(`${projectRoot}/`))
96
+ .limit(10)
97
+ .toArray();
103
98
  // Find a result where this symbol is defined
104
99
  for (const result of results) {
105
100
  const defined = result.defined_symbols;
@@ -144,8 +139,26 @@ Examples:
144
139
  // Sync if requested
145
140
  if (options.sync) {
146
141
  const { spinner, onProgress } = (0, sync_helpers_1.createIndexingSpinner)(projectRoot, "Syncing...", { verbose: false });
147
- yield (0, syncer_1.initialSync)({ projectRoot, onProgress });
148
- spinner.succeed("Sync complete");
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
+ }
149
162
  }
150
163
  // Initialize skeletonizer
151
164
  const skeletonizer = new skeletonizer_1.Skeletonizer();
@@ -154,7 +167,9 @@ Examples:
154
167
  includeSummary: !options.noSummary,
155
168
  };
156
169
  // Determine mode based on target
157
- const resolvedTarget = path.resolve(target);
170
+ const resolvedTarget = (0, path_containment_1.resolveContainedPath)(projectRoot, target, {
171
+ verifyExistingTarget: true,
172
+ });
158
173
  // Directory mode is unsupported. Auto-picking files from a directory
159
174
  // was confusingly magical (and on '.' it fell through to the resolver
160
175
  // path and skeletonized .gitignore). Refuse and point at the file form.
@@ -176,12 +191,14 @@ Examples:
176
191
  .map((t) => t.trim())
177
192
  .filter(Boolean);
178
193
  for (const t of targets) {
179
- const filePath = path.resolve(t);
194
+ const filePath = (0, path_containment_1.resolveContainedPath)(projectRoot, t, {
195
+ verifyExistingTarget: true,
196
+ });
180
197
  if (!fs.existsSync(filePath)) {
181
198
  console.error(`Not found: ${t}`);
182
199
  continue;
183
200
  }
184
- const content = fs.readFileSync(filePath, "utf-8");
201
+ const content = (0, file_utils_1.readContainedTextFileSync)(projectRoot, filePath);
185
202
  const result = yield skeletonizer.skeletonizeFile(filePath, content, skeletonOpts);
186
203
  outputResult(result, options);
187
204
  }
@@ -189,16 +206,13 @@ Examples:
189
206
  }
190
207
  if (isFilePath(target)) {
191
208
  // === FILE MODE ===
192
- let filePath = path.resolve(target);
209
+ const filePath = (0, path_containment_1.resolveContainedPath)(projectRoot, target, {
210
+ verifyExistingTarget: true,
211
+ });
193
212
  if (!fs.existsSync(filePath)) {
194
- // Try resolving across indexed projects
195
- const found = resolveAcrossProjects(target);
196
- if (!found) {
197
- console.error(`File not found: ${filePath}`);
198
- process.exitCode = 1;
199
- return;
200
- }
201
- filePath = found;
213
+ console.error(`File not found: ${filePath}`);
214
+ process.exitCode = 1;
215
+ return;
202
216
  }
203
217
  if (vectorDb) {
204
218
  // Use absolute path for DB lookup (centralized index stores absolute paths)
@@ -212,13 +226,13 @@ Examples:
212
226
  return;
213
227
  }
214
228
  }
215
- const content = fs.readFileSync(filePath, "utf-8");
229
+ const content = (0, file_utils_1.readContainedTextFileSync)(projectRoot, filePath);
216
230
  const result = yield skeletonizer.skeletonizeFile(filePath, content, skeletonOpts);
217
231
  outputResult(result, options);
218
232
  }
219
233
  else if (isSymbolLike(target) && !target.includes(" ")) {
220
234
  // === SYMBOL MODE ===
221
- const filePath = yield findFileBySymbol(target, vectorDb);
235
+ const filePath = yield findFileBySymbol(target, vectorDb, projectRoot);
222
236
  if (!filePath) {
223
237
  console.error(`Symbol not found in index: ${target}`);
224
238
  console.error("Try running 'gmax index' first or use a search query.");
@@ -226,9 +240,9 @@ Examples:
226
240
  return;
227
241
  }
228
242
  // filePath from DB is absolute (centralized index)
229
- const absolutePath = path.isAbsolute(filePath)
230
- ? filePath
231
- : path.resolve(projectRoot, filePath);
243
+ const absolutePath = (0, path_containment_1.resolveContainedPath)(projectRoot, filePath, {
244
+ verifyExistingTarget: true,
245
+ });
232
246
  if (!fs.existsSync(absolutePath)) {
233
247
  console.error(`File not found: ${absolutePath}`);
234
248
  process.exitCode = 1;
@@ -243,7 +257,7 @@ Examples:
243
257
  }, options);
244
258
  return;
245
259
  }
246
- const content = fs.readFileSync(absolutePath, "utf-8");
260
+ const content = (0, file_utils_1.readContainedTextFileSync)(projectRoot, absolutePath);
247
261
  const result = yield skeletonizer.skeletonizeFile(absolutePath, content, skeletonOpts);
248
262
  outputResult(result, options);
249
263
  }
@@ -251,7 +265,7 @@ Examples:
251
265
  // === QUERY MODE ===
252
266
  const searcher = new searcher_1.Searcher(vectorDb);
253
267
  const limit = Math.min(Number.parseInt(options.limit, 10) || 3, 10);
254
- const searchResults = yield searcher.search(target, limit);
268
+ const searchResults = yield searcher.search(target, limit, {}, {}, `${projectRoot}/`);
255
269
  if (!searchResults.data || searchResults.data.length === 0) {
256
270
  console.error(`No results found for: ${target}`);
257
271
  process.exitCode = 1;
@@ -273,9 +287,21 @@ Examples:
273
287
  const results = [];
274
288
  for (const filePath of filePaths) {
275
289
  // Paths from search results are absolute (centralized index)
276
- const absolutePath = path.isAbsolute(filePath)
277
- ? filePath
278
- : path.resolve(projectRoot, filePath);
290
+ let absolutePath;
291
+ try {
292
+ absolutePath = (0, path_containment_1.resolveContainedPath)(projectRoot, filePath, {
293
+ verifyExistingTarget: true,
294
+ });
295
+ }
296
+ catch (_c) {
297
+ results.push({
298
+ file: filePath,
299
+ skeleton: `// File outside selected project: ${filePath}`,
300
+ tokens: 0,
301
+ error: "File outside selected project",
302
+ });
303
+ continue;
304
+ }
279
305
  if (!fs.existsSync(absolutePath)) {
280
306
  results.push({
281
307
  file: filePath,
@@ -295,7 +321,7 @@ Examples:
295
321
  });
296
322
  continue;
297
323
  }
298
- const content = fs.readFileSync(absolutePath, "utf-8");
324
+ const content = (0, file_utils_1.readContainedTextFileSync)(projectRoot, absolutePath);
299
325
  const result = yield skeletonizer.skeletonizeFile(absolutePath, content, skeletonOpts);
300
326
  results.push({
301
327
  file: filePath,
@@ -326,7 +352,7 @@ Examples:
326
352
  try {
327
353
  yield vectorDb.close();
328
354
  }
329
- catch (_c) {
355
+ catch (_d) {
330
356
  // Ignore close errors
331
357
  }
332
358
  }
@@ -46,6 +46,7 @@ exports.status = void 0;
46
46
  const os = __importStar(require("node:os"));
47
47
  const commander_1 = require("commander");
48
48
  const config_1 = require("../config");
49
+ const embedding_status_1 = require("../lib/index/embedding-status");
49
50
  const index_config_1 = require("../lib/index/index-config");
50
51
  const exit_1 = require("../lib/utils/exit");
51
52
  const filter_builder_1 = require("../lib/utils/filter-builder");
@@ -157,7 +158,8 @@ Examples:
157
158
  st = "idle";
158
159
  const isCurrent = project.root === currentRoot;
159
160
  const count = (_b = chunkCounts.get(project.root)) !== null && _b !== void 0 ? _b : project.chunkCount;
160
- console.log(`${project.name}\t${formatChunks(count)}\t${formatAge(project.lastIndexed)}\t${st}${isCurrent ? "\tcurrent" : ""}`);
161
+ const identity = (0, embedding_status_1.projectEmbeddingStatus)(project, globalConfig);
162
+ console.log(`${project.name}\t${formatChunks(count)}\t${formatAge(project.lastIndexed)}\t${st}\tembedding=${identity.state}${isCurrent ? "\tcurrent" : ""}`);
161
163
  }
162
164
  yield (0, exit_1.gracefulExit)();
163
165
  return;
@@ -191,10 +193,11 @@ Examples:
191
193
  const chunks = `${formatChunks(count)} chunks`;
192
194
  // Age column
193
195
  const age = formatAge(project.lastIndexed);
196
+ const embedding = (0, embedding_status_1.projectEmbeddingStatus)(project, globalConfig).state;
194
197
  // Current marker
195
198
  const marker = isCurrent ? style.cyan(" ←") : "";
196
199
  const name = project.name.padEnd(nameWidth);
197
- console.log(` ${name} ${chunks.padEnd(12)} ${age.padEnd(10)} ${statusStr}${marker}`);
200
+ console.log(` ${name} ${chunks.padEnd(12)} ${age.padEnd(10)} ${statusStr} embedding:${embedding}${marker}`);
198
201
  }
199
202
  if (currentRoot) {
200
203
  console.log(`\n${style.dim("Current")}: ${shortenPath(currentRoot)}`);
@@ -50,6 +50,12 @@ const syncer_1 = require("../lib/index/syncer");
50
50
  const vector_db_1 = require("../lib/store/vector-db");
51
51
  const exit_1 = require("../lib/utils/exit");
52
52
  const project_root_1 = require("../lib/utils/project-root");
53
+ // ⛔ HARD STOP FOR AI AGENTS: The summarizer is decommissioned on purpose.
54
+ // `generateSummaries` (src/lib/index/syncer.ts) is a no-op stub, so this
55
+ // command intentionally does nothing. DO NOT re-enable it, and DO NOT start
56
+ // the summarizer server (mlx-embed-server/summarizer.py, port 8101) or load its
57
+ // ~16GB model to "measure"/"benchmark" — doing so has crashed the machine.
58
+ // Stop and get the user's explicit authorization first. See CLAUDE.md.
53
59
  exports.summarize = new commander_1.Command("summarize")
54
60
  .description("Generate LLM summaries for indexed chunks without re-indexing")
55
61
  .option("-p, --path <dir>", "Only summarize chunks under this directory")
@@ -48,7 +48,6 @@ const fs = __importStar(require("node:fs"));
48
48
  const path = __importStar(require("node:path"));
49
49
  const commander_1 = require("commander");
50
50
  const config_1 = require("../config");
51
- const index_config_1 = require("../lib/index/index-config");
52
51
  const syncer_1 = require("../lib/index/syncer");
53
52
  const watcher_1 = require("../lib/index/watcher");
54
53
  const meta_cache_1 = require("../lib/store/meta-cache");
@@ -122,19 +121,51 @@ exports.watch = new commander_1.Command("watch")
122
121
  }
123
122
  const logFile = path.join(config_1.PATHS.logsDir, "daemon.log");
124
123
  const out = (0, log_rotate_1.openRotatedLog)(logFile);
125
- const child = (0, node_child_process_1.spawn)(process.argv[0], [process.argv[1], "watch", "--daemon"], {
126
- detached: true,
127
- stdio: ["ignore", out, out],
128
- cwd: process.cwd(),
129
- env: Object.assign(Object.assign({}, process.env), { GMAX_BACKGROUND: "true" }),
130
- });
124
+ let child;
125
+ try {
126
+ child = (0, node_child_process_1.spawn)(process.argv[0], [process.argv[1], "watch", "--daemon"], {
127
+ detached: true,
128
+ stdio: ["ignore", out, out],
129
+ cwd: process.cwd(),
130
+ env: Object.assign(Object.assign({}, process.env), { GMAX_BACKGROUND: "true" }),
131
+ });
132
+ yield new Promise((resolve, reject) => {
133
+ child.once("spawn", resolve);
134
+ child.once("error", reject);
135
+ });
136
+ }
137
+ catch (error) {
138
+ const message = error instanceof Error ? error.message : String(error);
139
+ console.error(`Failed to start daemon: ${message}`);
140
+ process.exitCode = 1;
141
+ return;
142
+ }
143
+ finally {
144
+ try {
145
+ fs.closeSync(out);
146
+ }
147
+ catch (_c) { }
148
+ }
131
149
  child.unref();
132
150
  console.log(`Daemon started (PID: ${child.pid}, log: ${logFile})`);
133
151
  process.exit(0);
134
152
  }
135
153
  // Daemon foreground
154
+ // Stamp every daemon.log line so incidents can be correlated with the
155
+ // timestamped MLX/LLM server logs. Workers inherit via env flag.
156
+ const { installTimestampedOutput } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/logger")));
157
+ installTimestampedOutput();
136
158
  const { Daemon } = yield Promise.resolve().then(() => __importStar(require("../lib/daemon/daemon")));
137
159
  const daemon = new Daemon();
160
+ process.on("SIGINT", () => daemon.shutdown().then(() => (0, exit_1.gracefulExit)()));
161
+ process.on("SIGTERM", () => daemon.shutdown().then(() => (0, exit_1.gracefulExit)()));
162
+ process.on("uncaughtException", (err) => {
163
+ console.error("[daemon] uncaughtException:", err);
164
+ daemon.shutdown().then(() => (0, exit_1.gracefulExit)(1));
165
+ });
166
+ process.on("unhandledRejection", (reason) => {
167
+ console.error("[daemon] unhandledRejection:", reason);
168
+ });
138
169
  try {
139
170
  yield daemon.start();
140
171
  }
@@ -151,19 +182,10 @@ exports.watch = new commander_1.Command("watch")
151
182
  try {
152
183
  yield daemon.shutdown();
153
184
  }
154
- catch (_c) { }
185
+ catch (_d) { }
155
186
  process.exitCode = 1;
156
187
  return;
157
188
  }
158
- process.on("SIGINT", () => daemon.shutdown().then(() => (0, exit_1.gracefulExit)()));
159
- process.on("SIGTERM", () => daemon.shutdown().then(() => (0, exit_1.gracefulExit)()));
160
- process.on("uncaughtException", (err) => {
161
- console.error("[daemon] uncaughtException:", err);
162
- daemon.shutdown().then(() => (0, exit_1.gracefulExit)(1));
163
- });
164
- process.on("unhandledRejection", (reason) => {
165
- console.error("[daemon] unhandledRejection:", reason);
166
- });
167
189
  return;
168
190
  }
169
191
  // --- Per-project mode ---
@@ -185,12 +207,31 @@ exports.watch = new commander_1.Command("watch")
185
207
  const safeName = projectName.replace(/[^a-zA-Z0-9._-]/g, "_");
186
208
  const logFile = path.join(config_1.PATHS.logsDir, `watch-${safeName}.log`);
187
209
  const out = (0, log_rotate_1.openRotatedLog)(logFile);
188
- const child = (0, node_child_process_1.spawn)(process.argv[0], [process.argv[1], ...args], {
189
- detached: true,
190
- stdio: ["ignore", out, out],
191
- cwd: process.cwd(),
192
- env: Object.assign(Object.assign({}, process.env), { GMAX_BACKGROUND: "true" }),
193
- });
210
+ let child;
211
+ try {
212
+ child = (0, node_child_process_1.spawn)(process.argv[0], [process.argv[1], ...args], {
213
+ detached: true,
214
+ stdio: ["ignore", out, out],
215
+ cwd: process.cwd(),
216
+ env: Object.assign(Object.assign({}, process.env), { GMAX_BACKGROUND: "true" }),
217
+ });
218
+ yield new Promise((resolve, reject) => {
219
+ child.once("spawn", resolve);
220
+ child.once("error", reject);
221
+ });
222
+ }
223
+ catch (error) {
224
+ const message = error instanceof Error ? error.message : String(error);
225
+ console.error(`Failed to start watcher for ${projectName}: ${message}`);
226
+ process.exitCode = 1;
227
+ return;
228
+ }
229
+ finally {
230
+ try {
231
+ fs.closeSync(out);
232
+ }
233
+ catch (_e) { }
234
+ }
194
235
  child.unref();
195
236
  console.log(`Watcher started for ${projectName} (PID: ${child.pid}, log: ${logFile})`);
196
237
  process.exit(0);
@@ -225,24 +266,36 @@ exports.watch = new commander_1.Command("watch")
225
266
  .where((0, filter_builder_1.pathStartsWith)(prefix))
226
267
  .limit(1)
227
268
  .toArray();
269
+ let degraded = false;
270
+ let degradedErrors = 0;
271
+ let initialScanErrors = 0;
272
+ let initialFailedFiles = 0;
228
273
  if (indexed.length === 0) {
229
274
  console.log(`[watch:${projectName}] No index found for ${projectRoot}, running initial sync...`);
230
275
  const syncResult = yield (0, syncer_1.initialSync)({ projectRoot });
231
- // Update registry after sync
232
- const globalConfig = (0, index_config_1.readGlobalConfig)();
233
- (0, project_registry_1.registerProject)({
234
- root: projectRoot,
235
- name: projectName,
236
- vectorDim: globalConfig.vectorDim,
237
- modelTier: globalConfig.modelTier,
238
- embedMode: globalConfig.embedMode,
239
- lastIndexed: new Date().toISOString(),
240
- chunkCount: syncResult.indexed,
241
- status: "indexed",
242
- });
243
- console.log(`[watch:${projectName}] Initial sync complete.`);
276
+ if (syncResult.degraded) {
277
+ degraded = true;
278
+ degradedErrors = syncResult.scanErrors + syncResult.failedFiles;
279
+ initialScanErrors = syncResult.scanErrors;
280
+ initialFailedFiles = syncResult.failedFiles;
281
+ console.warn(`[watch:${projectName}] Initial sync incomplete: ${syncResult.scanErrors} scan error(s), ${syncResult.failedFiles} file failure(s). Preserving pending registry state.`);
282
+ }
283
+ else {
284
+ const chunkCount = yield vectorDb.countRowsForPath(prefix);
285
+ (0, project_registry_1.stampProjectFullSync)({
286
+ root: projectRoot,
287
+ name: projectName,
288
+ generation: syncResult.generation,
289
+ embedMode: syncResult.embedMode,
290
+ chunkCount,
291
+ chunkerVersion: config_1.CONFIG.CHUNKER_VERSION,
292
+ expectedFingerprint: syncResult.registryExpectation.embeddingFingerprint,
293
+ expectedRebuildId: syncResult.registryExpectation.rebuildId,
294
+ });
295
+ console.log(`[watch:${projectName}] Initial sync complete.`);
296
+ }
244
297
  }
245
- (0, watcher_store_1.updateWatcherStatus)(process.pid, "watching");
298
+ (0, watcher_store_1.updateWatcherStatus)(process.pid, degraded ? "degraded" : "watching", undefined, degraded ? `${degradedErrors} incomplete scan path(s)` : undefined);
246
299
  // Open resources for watcher
247
300
  const metaCache = new meta_cache_1.MetaCache(paths.lmdbPath);
248
301
  // Start watching
@@ -251,10 +304,17 @@ exports.watch = new commander_1.Command("watch")
251
304
  vectorDb,
252
305
  metaCache,
253
306
  dataDir: paths.dataDir,
307
+ initialScanErrors,
308
+ initialFailedFiles,
254
309
  onReindex: (files, ms) => {
255
310
  console.log(`[watch:${projectName}] Reindexed ${files} file${files !== 1 ? "s" : ""} (${(ms / 1000).toFixed(1)}s)`);
256
311
  lastActivity = Date.now();
257
- (0, watcher_store_1.updateWatcherStatus)(process.pid, "watching", Date.now());
312
+ (0, watcher_store_1.updateWatcherStatus)(process.pid, degraded ? "degraded" : "watching", Date.now(), degraded ? `${degradedErrors} incomplete scan path(s)` : undefined);
313
+ },
314
+ onHealthChange: (complete, errors) => {
315
+ degraded = !complete;
316
+ degradedErrors = errors;
317
+ (0, watcher_store_1.updateWatcherStatus)(process.pid, complete ? "watching" : "degraded", undefined, complete ? undefined : `${errors} incomplete scan path(s)`);
258
318
  },
259
319
  });
260
320
  console.log(`[watch:${projectName}] File watcher active`);
@@ -300,6 +360,10 @@ exports.watch
300
360
  const projects = resp.projects;
301
361
  const uptime = Math.floor(resp.uptime / 60);
302
362
  console.log(`Daemon (PID: ${resp.pid}, uptime: ${uptime}m):`);
363
+ const mlx = resp.mlx;
364
+ if (mlx) {
365
+ console.log(` MLX: ${mlx.state} (${mlx.model}${mlx.pid ? `, PID ${mlx.pid}` : ""})${mlx.error ? ` — ${mlx.error}` : ""}`);
366
+ }
303
367
  for (const p of projects) {
304
368
  console.log(` - ${p.root} [${p.status}]`);
305
369
  }
package/dist/config.js CHANGED
@@ -217,6 +217,9 @@ exports.PATHS = {
217
217
  globalRoot: GLOBAL_ROOT,
218
218
  models: path.join(GLOBAL_ROOT, "models"),
219
219
  grammars: path.join(GLOBAL_ROOT, "grammars"),
220
+ // HF cache for the MLX embed server, pinned to internal disk so the server
221
+ // survives an unmounted external volume behind the user's HF_HOME.
222
+ hfDir: path.join(GLOBAL_ROOT, "hf"),
220
223
  logsDir: path.join(GLOBAL_ROOT, "logs"),
221
224
  daemonSocket: path.join(GLOBAL_ROOT, "daemon.sock"),
222
225
  daemonPidFile: path.join(GLOBAL_ROOT, "daemon.pid"),