grepmax 0.10.2 → 0.10.4

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.
@@ -50,81 +50,257 @@ const commander_1 = require("commander");
50
50
  const config_1 = require("../config");
51
51
  const index_config_1 = require("../lib/index/index-config");
52
52
  const exit_1 = require("../lib/utils/exit");
53
+ const lock_1 = require("../lib/utils/lock");
54
+ const project_registry_1 = require("../lib/utils/project-registry");
53
55
  const project_root_1 = require("../lib/utils/project-root");
56
+ function formatSize(bytes) {
57
+ if (bytes < 1024)
58
+ return `${bytes} B`;
59
+ if (bytes < 1024 * 1024)
60
+ return `${(bytes / 1024).toFixed(1)} KB`;
61
+ if (bytes < 1024 * 1024 * 1024)
62
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
63
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
64
+ }
65
+ function getDirectorySize(dirPath) {
66
+ let totalSize = 0;
67
+ try {
68
+ const items = fs.readdirSync(dirPath);
69
+ for (const item of items) {
70
+ const itemPath = path.join(dirPath, item);
71
+ const stats = fs.statSync(itemPath);
72
+ if (stats.isDirectory()) {
73
+ totalSize += getDirectorySize(itemPath);
74
+ }
75
+ else {
76
+ totalSize += stats.size;
77
+ }
78
+ }
79
+ }
80
+ catch (_a) { }
81
+ return totalSize;
82
+ }
54
83
  exports.doctor = new commander_1.Command("doctor")
55
84
  .description("Check installation health, models, and index status")
56
- .action(() => __awaiter(void 0, void 0, void 0, function* () {
85
+ .option("--fix", "Auto-fix detected issues (compact, prune, remove stale locks)", false)
86
+ .option("--agent", "Compact output for AI agents", false)
87
+ .action((opts) => __awaiter(void 0, void 0, void 0, function* () {
57
88
  var _a;
58
- console.log("đŸĨ gmax Doctor\n");
89
+ if (!opts.agent)
90
+ console.log("gmax Doctor\n");
59
91
  const root = config_1.PATHS.globalRoot;
60
92
  const models = config_1.PATHS.models;
61
93
  const grammars = config_1.PATHS.grammars;
62
- const checkDir = (name, p) => {
63
- const exists = fs.existsSync(p);
64
- const symbol = exists ? "✅" : "❌";
65
- console.log(`${symbol} ${name}: ${p}`);
66
- };
67
- checkDir("Root", root);
68
- checkDir("Models", models);
69
- checkDir("Grammars", grammars);
94
+ if (!opts.agent) {
95
+ const checkDir = (name, p) => {
96
+ const exists = fs.existsSync(p);
97
+ const symbol = exists ? "ok" : "MISSING";
98
+ console.log(`${symbol} ${name}: ${p}`);
99
+ };
100
+ checkDir("Root", root);
101
+ checkDir("Models", models);
102
+ checkDir("Grammars", grammars);
103
+ }
70
104
  const globalConfig = (0, index_config_1.readGlobalConfig)();
71
105
  const tier = (_a = config_1.MODEL_TIERS[globalConfig.modelTier]) !== null && _a !== void 0 ? _a : config_1.MODEL_TIERS.small;
72
106
  const embedModel = globalConfig.embedMode === "gpu" ? tier.mlxModel : tier.onnxModel;
73
- console.log(`\nEmbed mode: ${globalConfig.embedMode} | Model tier: ${globalConfig.modelTier} (${tier.vectorDim}d)`);
74
- console.log(`Embed model: ${embedModel}`);
75
- console.log(`ColBERT model: ${config_1.MODEL_IDS.colbert}`);
76
- const modelStatuses = [embedModel, config_1.MODEL_IDS.colbert].map((id) => {
77
- const modelPath = path.join(models, ...id.split("/"));
78
- return { id, path: modelPath, exists: fs.existsSync(modelPath) };
79
- });
80
- modelStatuses.forEach(({ id, exists }) => {
81
- const symbol = exists ? "✅" : "âš ī¸ ";
82
- console.log(`${symbol} ${id}: ${exists ? "downloaded" : "will download on first use"}`);
83
- });
84
- console.log(`\nLocal Project: ${process.cwd()}`);
85
- const projectRoot = (0, project_root_1.findProjectRoot)(process.cwd());
86
- if (projectRoot) {
87
- console.log(`✅ Project root: ${projectRoot}`);
88
- console.log(` Centralized index at: ~/.gmax/lancedb/`);
89
- }
90
- else {
91
- console.log(`â„šī¸ No index found in current directory (run 'gmax index' to create one)`);
107
+ if (!opts.agent) {
108
+ console.log(`\nEmbed mode: ${globalConfig.embedMode} | Model tier: ${globalConfig.modelTier} (${tier.vectorDim}d)`);
109
+ console.log(`Embed model: ${embedModel}`);
110
+ console.log(`ColBERT model: ${config_1.MODEL_IDS.colbert}`);
111
+ const modelStatuses = [embedModel, config_1.MODEL_IDS.colbert].map((id) => {
112
+ const modelPath = path.join(models, ...id.split("/"));
113
+ return { id, path: modelPath, exists: fs.existsSync(modelPath) };
114
+ });
115
+ modelStatuses.forEach(({ id, exists }) => {
116
+ console.log(`${exists ? "ok" : "WARN"} ${id}: ${exists ? "downloaded" : "will download on first use"}`);
117
+ });
118
+ console.log(`\nLocal Project: ${process.cwd()}`);
119
+ const projectRoot = (0, project_root_1.findProjectRoot)(process.cwd());
120
+ if (projectRoot) {
121
+ console.log(`ok Project root: ${projectRoot}`);
122
+ console.log(` Centralized index at: ~/.gmax/lancedb/`);
123
+ }
124
+ else {
125
+ console.log(`INFO No index found in current directory (run 'gmax index' to create one)`);
126
+ }
127
+ // Check MLX embed server
128
+ const embedUp = yield fetch("http://127.0.0.1:8100/health")
129
+ .then((r) => r.ok)
130
+ .catch(() => false);
131
+ console.log(`${embedUp ? "ok" : "WARN"} MLX Embed: ${embedUp ? "running (port 8100)" : "not running"}`);
132
+ // Check summarizer server
133
+ const summarizerUp = yield fetch("http://127.0.0.1:8101/health")
134
+ .then((r) => r.ok)
135
+ .catch(() => false);
136
+ console.log(`${summarizerUp ? "ok" : "WARN"} Summarizer: ${summarizerUp ? "running (port 8101)" : "not running"}`);
92
137
  }
93
- // Check MLX embed server
94
- const embedUp = yield fetch("http://127.0.0.1:8100/health")
95
- .then((r) => r.ok)
96
- .catch(() => false);
97
- console.log(`${embedUp ? "✅" : "âš ī¸ "} MLX Embed: ${embedUp ? "running (port 8100)" : "not running"}`);
98
- // Check summarizer server
99
- const summarizerUp = yield fetch("http://127.0.0.1:8101/health")
100
- .then((r) => r.ok)
101
- .catch(() => false);
102
- console.log(`${summarizerUp ? "✅" : "âš ī¸ "} Summarizer: ${summarizerUp ? "running (port 8101)" : "not running"}`);
103
- // Check summary coverage
138
+ // --- Index Health ---
139
+ let needsOptimize = false;
140
+ let staleLock = false;
141
+ const orphanedProjects = [];
104
142
  try {
105
143
  const { VectorDB } = yield Promise.resolve().then(() => __importStar(require("../lib/store/vector-db")));
106
144
  const db = new VectorDB(config_1.PATHS.lancedbDir);
107
145
  const table = yield db.ensureTable();
108
146
  const totalChunks = yield table.countRows();
109
- if (totalChunks > 0) {
147
+ // Summary coverage (existing check)
148
+ if (!opts.agent && totalChunks > 0) {
110
149
  const withSummary = (yield table
111
150
  .query()
112
151
  .where("length(summary) > 5")
113
152
  .select(["id"])
114
153
  .toArray()).length;
115
154
  const pct = Math.round((withSummary / totalChunks) * 100);
116
- const symbol = pct >= 90 ? "✅" : pct > 0 ? "âš ī¸ " : "❌";
117
- console.log(`${symbol} Summary coverage: ${withSummary}/${totalChunks} (${pct}%)`);
155
+ const symbol = pct >= 90 ? "ok" : pct > 0 ? "WARN" : "FAIL";
156
+ console.log(`${symbol} Summary coverage: ${withSummary}/${totalChunks} (${pct}%)`);
157
+ }
158
+ else if (!opts.agent && totalChunks === 0) {
159
+ console.log("INFO No indexed chunks yet");
160
+ }
161
+ // Index health checks
162
+ const tableStats = yield table.stats();
163
+ const diskSize = getDirectorySize(config_1.PATHS.lancedbDir);
164
+ const logicalSize = tableStats.totalBytes;
165
+ const { numFragments, numSmallFragments } = tableStats.fragmentStats;
166
+ const versions = yield table.listVersions();
167
+ // Lock status
168
+ const lockPath = path.join(config_1.PATHS.globalRoot, "LOCK");
169
+ let lockStatus = "none";
170
+ if (fs.existsSync(lockPath)) {
171
+ const { pid, startedAt } = (0, lock_1.parseLock)(lockPath);
172
+ const alive = (0, lock_1.isProcessAlive)(pid);
173
+ if (alive) {
174
+ lockStatus = `active (PID ${pid})`;
175
+ }
176
+ else {
177
+ lockStatus = `stale (PID ${pid}${startedAt ? ` @ ${startedAt}` : ""})`;
178
+ staleLock = true;
179
+ }
180
+ }
181
+ // Daemon status
182
+ const { isDaemonRunning } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
183
+ const daemonUp = yield isDaemonRunning();
184
+ // Project registry health
185
+ const projects = (0, project_registry_1.listProjects)();
186
+ for (const p of projects) {
187
+ if (!fs.existsSync(p.root)) {
188
+ orphanedProjects.push(p.root);
189
+ }
190
+ }
191
+ // Compute warning flags
192
+ const bloatRatio = logicalSize > 0 ? diskSize / logicalSize : 0;
193
+ if (bloatRatio > 2.0)
194
+ needsOptimize = true;
195
+ if (numSmallFragments > 10)
196
+ needsOptimize = true;
197
+ if (versions.length > 50)
198
+ needsOptimize = true;
199
+ if (opts.agent) {
200
+ const fields = [
201
+ "index_health",
202
+ `rows=${totalChunks}`,
203
+ `logical=${formatSize(logicalSize)}`,
204
+ `disk=${formatSize(diskSize)}`,
205
+ `fragments=${numFragments}`,
206
+ `small=${numSmallFragments}`,
207
+ `versions=${versions.length}`,
208
+ `lock=${lockStatus.split(" ")[0]}`,
209
+ `daemon=${daemonUp ? "running" : "stopped"}`,
210
+ `orphaned=${orphanedProjects.length}`,
211
+ ];
212
+ console.log(fields.join("\t"));
118
213
  }
119
214
  else {
120
- console.log("â„šī¸ No indexed chunks yet");
215
+ console.log("\nIndex Health\n");
216
+ // Storage
217
+ if (bloatRatio > 2.0) {
218
+ console.log(`WARN Storage: ${totalChunks.toLocaleString()} rows, ${formatSize(logicalSize)} logical, ${formatSize(diskSize)} disk (${bloatRatio.toFixed(1)}x — orphaned files)`);
219
+ }
220
+ else {
221
+ console.log(`ok Storage: ${totalChunks.toLocaleString()} rows, ${formatSize(logicalSize)} logical, ${formatSize(diskSize)} disk`);
222
+ }
223
+ // Fragments
224
+ if (numSmallFragments > 10) {
225
+ console.log(`WARN Fragments: ${numFragments} total, ${numSmallFragments} small — needs compaction`);
226
+ }
227
+ else {
228
+ console.log(`ok Fragments: ${numFragments} total, ${numSmallFragments} small`);
229
+ }
230
+ // Versions
231
+ if (versions.length > 50) {
232
+ console.log(`WARN Versions: ${versions.length} — pruning recommended`);
233
+ }
234
+ else {
235
+ console.log(`ok Versions: ${versions.length}`);
236
+ }
237
+ // Lock
238
+ if (staleLock) {
239
+ console.log(`WARN Lock: ${lockStatus}`);
240
+ }
241
+ else if (lockStatus === "none") {
242
+ console.log("ok Lock: none");
243
+ }
244
+ else {
245
+ console.log(`ok Lock: ${lockStatus}`);
246
+ }
247
+ // Daemon
248
+ console.log(`${daemonUp ? "ok" : "INFO"} Daemon: ${daemonUp ? "running" : "not running"}`);
249
+ // Projects
250
+ if (orphanedProjects.length > 0) {
251
+ console.log(`WARN Orphaned projects: ${orphanedProjects.length} (directories no longer exist)`);
252
+ for (const op of orphanedProjects) {
253
+ console.log(` - ${op}`);
254
+ }
255
+ }
256
+ else if (projects.length > 0) {
257
+ console.log(`ok Projects: ${projects.length} registered, all directories exist`);
258
+ }
259
+ }
260
+ // --fix auto-remediation
261
+ if (opts.fix) {
262
+ if (!opts.agent)
263
+ console.log("\nAuto-fix\n");
264
+ let fixed = 0;
265
+ if (staleLock) {
266
+ yield (0, lock_1.removeLock)(lockPath);
267
+ if (!opts.agent)
268
+ console.log("ok Removed stale lock");
269
+ fixed++;
270
+ }
271
+ if (needsOptimize) {
272
+ if (!opts.agent)
273
+ console.log("... Running optimize (compact + prune)...");
274
+ yield db.optimize(3, 0);
275
+ if (!opts.agent)
276
+ console.log("ok Optimize complete");
277
+ fixed++;
278
+ }
279
+ if (orphanedProjects.length > 0) {
280
+ for (const op of orphanedProjects) {
281
+ (0, project_registry_1.removeProject)(op);
282
+ }
283
+ if (!opts.agent)
284
+ console.log(`ok Removed ${orphanedProjects.length} orphaned project(s) from registry`);
285
+ fixed++;
286
+ }
287
+ if (fixed === 0) {
288
+ if (!opts.agent)
289
+ console.log("ok Nothing to fix");
290
+ }
121
291
  }
122
292
  yield db.close();
123
293
  }
124
294
  catch (_b) {
125
- console.log("âš ī¸ Could not check summary coverage");
295
+ if (opts.agent) {
296
+ console.log("index_health\terror=could_not_check");
297
+ }
298
+ else {
299
+ console.log("\nWARN Could not check index health");
300
+ }
301
+ }
302
+ if (!opts.agent) {
303
+ console.log(`\nSystem: ${os.platform()} ${os.arch()} | Node: ${process.version}`);
126
304
  }
127
- console.log(`\nSystem: ${os.platform()} ${os.arch()} | Node: ${process.version}`);
128
- console.log("\nIf you see ✅ everywhere, you are ready to search!");
129
305
  yield (0, exit_1.gracefulExit)();
130
306
  }));
@@ -81,11 +81,10 @@ class ProjectBatchProcessor {
81
81
  if (this.closed || this.processing)
82
82
  return;
83
83
  try {
84
- yield this.vectorDb.createFTSIndex();
85
- yield this.vectorDb.optimize();
84
+ yield this.vectorDb.runMaintenance();
86
85
  }
87
86
  catch (err) {
88
- console.error(`[${this.wtag}] FTS rebuild / compaction failed:`, err);
87
+ console.error(`[${this.wtag}] Maintenance failed:`, err);
89
88
  }
90
89
  }), FTS_REBUILD_INTERVAL_MS);
91
90
  this.ftsInterval.unref();
@@ -509,7 +509,7 @@ function initialSync(options) {
509
509
  total,
510
510
  filePath: "Creating FTS index...",
511
511
  });
512
- yield vectorDb.createFTSIndex();
512
+ yield vectorDb.runMaintenance();
513
513
  ftsTimer();
514
514
  }
515
515
  syncTimer();
@@ -56,6 +56,7 @@ class VectorDB {
56
56
  this.lancedbDir = lancedbDir;
57
57
  this.db = null;
58
58
  this.closed = false;
59
+ this.maintenanceRunning = false;
59
60
  this.vectorDim = vectorDim !== null && vectorDim !== void 0 ? vectorDim : config_1.CONFIG.VECTOR_DIM;
60
61
  this.unregisterCleanup = (0, cleanup_1.registerCleanup)(() => this.close());
61
62
  }
@@ -275,21 +276,69 @@ class VectorDB {
275
276
  });
276
277
  }
277
278
  optimize() {
278
- return __awaiter(this, void 0, void 0, function* () {
279
+ return __awaiter(this, arguments, void 0, function* (retries = 3, retentionMs = 0) {
279
280
  const table = yield this.ensureTable();
280
- try {
281
- yield table.optimize({
282
- cleanupOlderThan: new Date(),
283
- });
284
- }
285
- catch (e) {
286
- const msg = e instanceof Error ? e.message : String(e);
287
- if (!msg.includes("Nothing to do")) {
281
+ const cutoff = new Date(Date.now() - retentionMs);
282
+ for (let attempt = 1; attempt <= retries; attempt++) {
283
+ try {
284
+ const done = (0, logger_1.timer)("vectordb", "optimize");
285
+ const stats = yield table.optimize({
286
+ cleanupOlderThan: cutoff,
287
+ deleteUnverified: true,
288
+ });
289
+ done();
290
+ const { compaction, prune } = stats;
291
+ if (compaction.fragmentsRemoved > 0 ||
292
+ prune.oldVersionsRemoved > 0 ||
293
+ prune.bytesRemoved > 0) {
294
+ (0, logger_1.log)("vectordb", `Compacted: ${compaction.fragmentsRemoved} frags → ${compaction.fragmentsAdded}, ` +
295
+ `pruned ${prune.oldVersionsRemoved} versions, ` +
296
+ `freed ${(prune.bytesRemoved / 1024 / 1024).toFixed(1)}MB`);
297
+ }
298
+ else {
299
+ (0, logger_1.debug)("vectordb", "Optimize: nothing to compact or prune");
300
+ }
301
+ return;
302
+ }
303
+ catch (e) {
304
+ const msg = e instanceof Error ? e.message : String(e);
305
+ if (msg.includes("Nothing to do")) {
306
+ (0, logger_1.debug)("vectordb", "Optimize: nothing to do");
307
+ return;
308
+ }
309
+ if (attempt < retries &&
310
+ (msg.includes("conflict") || msg.includes("Retryable"))) {
311
+ const delay = 1000 * Math.pow(2, (attempt - 1));
312
+ (0, logger_1.log)("vectordb", `Optimize conflict (attempt ${attempt}/${retries}), retrying in ${delay}ms`);
313
+ yield new Promise((r) => setTimeout(r, delay));
314
+ continue;
315
+ }
288
316
  (0, logger_1.log)("vectordb", `Optimize failed: ${msg}`);
317
+ return;
289
318
  }
290
319
  }
291
320
  });
292
321
  }
322
+ /**
323
+ * Run FTS rebuild + optimize as a single serialized operation.
324
+ * Safe to call from multiple project processors — only one runs at a time.
325
+ */
326
+ runMaintenance() {
327
+ return __awaiter(this, void 0, void 0, function* () {
328
+ if (this.maintenanceRunning) {
329
+ (0, logger_1.debug)("vectordb", "Maintenance already running, skipping");
330
+ return;
331
+ }
332
+ this.maintenanceRunning = true;
333
+ try {
334
+ yield this.createFTSIndex();
335
+ yield this.optimize();
336
+ }
337
+ finally {
338
+ this.maintenanceRunning = false;
339
+ }
340
+ });
341
+ }
293
342
  hasAnyRows() {
294
343
  return __awaiter(this, void 0, void 0, function* () {
295
344
  const table = yield this.ensureTable();
@@ -42,6 +42,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
42
42
  });
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.parseLock = parseLock;
46
+ exports.isProcessAlive = isProcessAlive;
47
+ exports.removeLock = removeLock;
45
48
  exports.acquireWriterLock = acquireWriterLock;
46
49
  exports.acquireWriterLockWithRetry = acquireWriterLockWithRetry;
47
50
  exports.isLocked = isLocked;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.10.2",
3
+ "version": "0.10.4",
4
4
  "author": "Robert Owens <robowens@me.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.10.2",
3
+ "version": "0.10.4",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",