grepmax 0.6.0 → 0.6.3

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.
@@ -64,6 +64,16 @@ exports.index = new commander_1.Command("index")
64
64
  var _a;
65
65
  const options = cmd.optsWithGlobals();
66
66
  let vectorDb = null;
67
+ const ac = new AbortController();
68
+ let aborted = false;
69
+ const onSignal = () => {
70
+ if (aborted)
71
+ return;
72
+ aborted = true;
73
+ ac.abort();
74
+ };
75
+ process.on("SIGINT", onSignal);
76
+ process.on("SIGTERM", onSignal);
67
77
  try {
68
78
  yield (0, setup_helpers_1.ensureSetup)();
69
79
  const indexRoot = options.path
@@ -107,7 +117,12 @@ exports.index = new commander_1.Command("index")
107
117
  dryRun: options.dryRun,
108
118
  reset: options.reset,
109
119
  onProgress,
120
+ signal: ac.signal,
110
121
  });
122
+ if (aborted) {
123
+ spinner.warn(`Indexing interrupted — partial progress saved (${result.indexed} indexed)`);
124
+ return;
125
+ }
111
126
  if (options.dryRun) {
112
127
  spinner.succeed(`Dry run complete(${result.processed} / ${result.total}) • would have indexed ${result.indexed} `);
113
128
  console.log((0, sync_helpers_1.formatDryRunSummary)(result, {
@@ -143,6 +158,8 @@ exports.index = new commander_1.Command("index")
143
158
  process.exitCode = 1;
144
159
  }
145
160
  finally {
161
+ process.removeListener("SIGINT", onSignal);
162
+ process.removeListener("SIGTERM", onSignal);
146
163
  if (vectorDb) {
147
164
  try {
148
165
  yield vectorDb.close();
@@ -338,6 +338,7 @@ exports.mcp = new commander_1.Command("mcp")
338
338
  // --- Tool handlers ---
339
339
  function handleSemanticSearch(args_1) {
340
340
  return __awaiter(this, arguments, void 0, function* (args, searchAll = false) {
341
+ var _a;
341
342
  const query = String(args.query || "");
342
343
  if (!query)
343
344
  return err("Missing required parameter: query");
@@ -434,7 +435,11 @@ exports.mcp = new commander_1.Command("mcp")
434
435
  return true;
435
436
  });
436
437
  }
437
- return ok(results.map((r) => r.text).join("\n\n"));
438
+ const output = results.map((r) => r.text).join("\n\n");
439
+ if ((_a = result.warnings) === null || _a === void 0 ? void 0 : _a.length) {
440
+ return ok(`${result.warnings.join("\n")}\n\n${output}`);
441
+ }
442
+ return ok(output);
438
443
  }
439
444
  catch (e) {
440
445
  const msg = e instanceof Error ? e.message : String(e);
@@ -339,7 +339,7 @@ exports.search = new commander_1.Command("search")
339
339
  .argument("<pattern>", "The pattern to search for")
340
340
  .argument("[path]", "The path to search in")
341
341
  .action((pattern, exec_path, _options, cmd) => __awaiter(void 0, void 0, void 0, function* () {
342
- var _a, _b;
342
+ var _a, _b, _c;
343
343
  const options = cmd.optsWithGlobals();
344
344
  const root = process.cwd();
345
345
  const minScore = Number.isFinite(Number.parseFloat(options.minScore))
@@ -488,6 +488,11 @@ exports.search = new commander_1.Command("search")
488
488
  ? searchPathPrefix
489
489
  : `${searchPathPrefix}/`;
490
490
  const searchResult = yield searcher.search(pattern, parseInt(options.m, 10), { rerank: true }, undefined, pathFilter);
491
+ if ((_c = searchResult.warnings) === null || _c === void 0 ? void 0 : _c.length) {
492
+ for (const w of searchResult.warnings) {
493
+ console.warn(`Warning: ${w}`);
494
+ }
495
+ }
491
496
  const filteredData = searchResult.data.filter((r) => typeof r.score !== "number" || r.score >= minScore);
492
497
  if (options.skeleton) {
493
498
  yield outputSkeletons(filteredData, projectRoot, parseInt(options.m, 10), vectorDb);
@@ -426,7 +426,7 @@ exports.serve = new commander_1.Command("serve")
426
426
  });
427
427
  // Clean close of owned resources
428
428
  try {
429
- metaCache.close();
429
+ yield metaCache.close();
430
430
  }
431
431
  catch (e) {
432
432
  console.error("Error closing meta cache:", e);
@@ -154,7 +154,7 @@ exports.watch = new commander_1.Command("watch")
154
154
  yield watcher.close();
155
155
  }
156
156
  catch (_a) { }
157
- metaCache.close();
157
+ yield metaCache.close();
158
158
  yield vectorDb.close();
159
159
  (0, watcher_registry_1.unregisterWatcher)(process.pid);
160
160
  yield (0, exit_1.gracefulExit)();
@@ -14,25 +14,27 @@ exports.DEFAULT_IGNORE_PATTERNS = [
14
14
  "*.log",
15
15
  "*.csv",
16
16
  // Safety nets for nested non-git folders
17
- "**/node_modules/**",
18
- "**/dist/**",
19
- "**/build/**",
20
- "**/out/**",
21
- "**/target/**",
22
- "**/__pycache__/**",
23
- "**/coverage/**",
24
- "**/venv/**",
25
- "**/.venv/**",
26
- "**/.tox/**",
27
- "**/.mypy_cache/**",
28
- "**/.pytest_cache/**",
29
- "**/.next/**",
30
- "**/.nuxt/**",
31
- "**/.gradle/**",
32
- "**/.m2/**",
33
- "**/vendor/**",
34
- "**/.osgrep/**",
35
- "**/.gmax/**",
17
+ // Use bare names so the `ignore` library matches the directory itself
18
+ // (prevents descending into it), not just files inside it.
19
+ "node_modules",
20
+ "dist",
21
+ "build",
22
+ "out",
23
+ "target",
24
+ "__pycache__",
25
+ "coverage",
26
+ "venv",
27
+ ".venv",
28
+ ".tox",
29
+ ".mypy_cache",
30
+ ".pytest_cache",
31
+ ".next",
32
+ ".nuxt",
33
+ ".gradle",
34
+ ".m2",
35
+ "vendor",
36
+ ".osgrep",
37
+ ".gmax",
36
38
  // Minified/generated assets
37
39
  "*.min.js",
38
40
  "*.min.css",
@@ -174,7 +174,7 @@ function createNoopMetaCache() {
174
174
  delete: (filePath) => {
175
175
  store.delete(filePath);
176
176
  },
177
- close: () => { },
177
+ close: () => __awaiter(this, void 0, void 0, function* () { }),
178
178
  };
179
179
  }
180
180
  function initialSync(options) {
@@ -457,7 +457,17 @@ function initialSync(options) {
457
457
  ? flushError
458
458
  : new Error(String(flushError));
459
459
  }
460
- if (!dryRun) {
460
+ // Stale cleanup: only remove paths scoped to this project's root
461
+ const stale = Array.from(cachedPaths).filter((p) => !seenPaths.has(p));
462
+ if (!dryRun && stale.length > 0 && !shouldSkipCleanup) {
463
+ (0, logger_1.log)("index", `Stale cleanup: ${stale.length} paths`);
464
+ yield vectorDb.deletePaths(stale);
465
+ stale.forEach((p) => {
466
+ metaCache.delete(p);
467
+ });
468
+ }
469
+ // Only rebuild FTS index if data actually changed
470
+ if (!dryRun && (indexed > 0 || stale.length > 0)) {
461
471
  const ftsTimer = (0, logger_1.timer)("index", "FTS");
462
472
  onProgress === null || onProgress === void 0 ? void 0 : onProgress({
463
473
  processed,
@@ -468,15 +478,6 @@ function initialSync(options) {
468
478
  yield vectorDb.createFTSIndex();
469
479
  ftsTimer();
470
480
  }
471
- // Stale cleanup: only remove paths scoped to this project's root
472
- const stale = Array.from(cachedPaths).filter((p) => !seenPaths.has(p));
473
- if (!dryRun && stale.length > 0 && !shouldSkipCleanup) {
474
- (0, logger_1.log)("index", `Stale cleanup: ${stale.length} paths`);
475
- yield vectorDb.deletePaths(stale);
476
- stale.forEach((p) => {
477
- metaCache.delete(p);
478
- });
479
- }
480
481
  syncTimer();
481
482
  // Write model config so future runs can detect model changes
482
483
  if (!dryRun) {
@@ -501,7 +502,7 @@ function initialSync(options) {
501
502
  if (lock) {
502
503
  yield lock.release();
503
504
  }
504
- metaCache === null || metaCache === void 0 ? void 0 : metaCache.close();
505
+ yield (metaCache === null || metaCache === void 0 ? void 0 : metaCache.close());
505
506
  yield vectorDb.close();
506
507
  }
507
508
  });
@@ -75,9 +75,12 @@ const FTS_REBUILD_INTERVAL_MS = 5 * 60 * 1000;
75
75
  function startWatcher(opts) {
76
76
  const { projectRoot, vectorDb, metaCache, dataDir, onReindex } = opts;
77
77
  const pending = new Map();
78
+ const retryCount = new Map();
78
79
  let debounceTimer = null;
79
80
  let processing = false;
80
81
  let closed = false;
82
+ let consecutiveLockFailures = 0;
83
+ const MAX_RETRIES = 5;
81
84
  const watcher = (0, chokidar_1.watch)(projectRoot, {
82
85
  ignored: exports.WATCHER_IGNORE_PATTERNS,
83
86
  ignoreInitial: true,
@@ -98,6 +101,7 @@ function startWatcher(opts) {
98
101
  debounceTimer = setTimeout(() => processBatch(), DEBOUNCE_MS);
99
102
  };
100
103
  const processBatch = () => __awaiter(this, void 0, void 0, function* () {
104
+ var _a;
101
105
  if (closed || processing || pending.size === 0)
102
106
  return;
103
107
  processing = true;
@@ -175,13 +179,19 @@ function startWatcher(opts) {
175
179
  }
176
180
  }
177
181
  }
178
- // Flush to VectorDB
179
- if (deletes.length > 0) {
180
- yield vectorDb.deletePaths(deletes);
181
- }
182
+ // Flush to VectorDB: insert first, then delete old (preserving new)
183
+ const newIds = vectors.map((v) => v.id);
182
184
  if (vectors.length > 0) {
183
185
  yield vectorDb.insertBatch(vectors);
184
186
  }
187
+ if (deletes.length > 0) {
188
+ if (newIds.length > 0) {
189
+ yield vectorDb.deletePathsExcludingIds(deletes, newIds);
190
+ }
191
+ else {
192
+ yield vectorDb.deletePaths(deletes);
193
+ }
194
+ }
185
195
  // Update MetaCache
186
196
  for (const [p, entry] of metaUpdates) {
187
197
  metaCache.put(p, entry);
@@ -222,7 +232,7 @@ function startWatcher(opts) {
222
232
  }
223
233
  }
224
234
  }
225
- catch (_a) {
235
+ catch (_b) {
226
236
  // Summarizer unavailable — skip silently
227
237
  }
228
238
  }
@@ -230,16 +240,38 @@ function startWatcher(opts) {
230
240
  const duration = Date.now() - start;
231
241
  onReindex === null || onReindex === void 0 ? void 0 : onReindex(reindexed, duration);
232
242
  }
243
+ consecutiveLockFailures = 0;
244
+ for (const absPath of batch.keys()) {
245
+ retryCount.delete(absPath);
246
+ }
233
247
  }
234
248
  catch (err) {
249
+ const isLockError = err instanceof Error && err.message.includes("lock already held");
250
+ if (isLockError) {
251
+ consecutiveLockFailures++;
252
+ }
235
253
  console.error("[watch] Batch processing failed:", err);
236
- // Re-queue failed items for retry
254
+ let dropped = 0;
237
255
  for (const [absPath, event] of batch) {
238
- if (!pending.has(absPath)) {
256
+ const count = ((_a = retryCount.get(absPath)) !== null && _a !== void 0 ? _a : 0) + 1;
257
+ if (count >= MAX_RETRIES) {
258
+ retryCount.delete(absPath);
259
+ dropped++;
260
+ }
261
+ else if (!pending.has(absPath)) {
239
262
  pending.set(absPath, event);
263
+ retryCount.set(absPath, count);
240
264
  }
241
265
  }
242
- scheduleBatch();
266
+ if (dropped > 0) {
267
+ console.warn(`[watch] Dropped ${dropped} file(s) after ${MAX_RETRIES} failed retries`);
268
+ }
269
+ if (pending.size > 0) {
270
+ const backoffMs = Math.min(DEBOUNCE_MS * Math.pow(2, consecutiveLockFailures), 30000);
271
+ if (debounceTimer)
272
+ clearTimeout(debounceTimer);
273
+ debounceTimer = setTimeout(() => processBatch(), backoffMs);
274
+ }
243
275
  }
244
276
  finally {
245
277
  processing = false;
@@ -18,6 +18,8 @@ class Searcher {
18
18
  constructor(db) {
19
19
  this.db = db;
20
20
  this.ftsIndexChecked = false;
21
+ this.ftsAvailable = false;
22
+ this.ftsLastCheckedAt = 0;
21
23
  }
22
24
  mapRecordToChunk(record, score) {
23
25
  var _a;
@@ -307,13 +309,19 @@ class Searcher {
307
309
  catch (_k) {
308
310
  return { data: [] };
309
311
  }
310
- // Ensure FTS index exists (lazy init on first search)
311
- if (!this.ftsIndexChecked) {
312
- this.ftsIndexChecked = true; // Set immediately to prevent retry spam
312
+ // Ensure FTS index exists (lazy init, retry periodically on failure)
313
+ const now = Date.now();
314
+ if (!this.ftsIndexChecked ||
315
+ (!this.ftsAvailable &&
316
+ now - this.ftsLastCheckedAt > Searcher.FTS_RETRY_INTERVAL_MS)) {
317
+ this.ftsIndexChecked = true;
318
+ this.ftsLastCheckedAt = now;
313
319
  try {
314
320
  yield this.db.createFTSIndex();
321
+ this.ftsAvailable = true;
315
322
  }
316
323
  catch (e) {
324
+ this.ftsAvailable = false;
317
325
  console.warn("[Searcher] Failed to ensure FTS index:", e);
318
326
  }
319
327
  }
@@ -323,16 +331,21 @@ class Searcher {
323
331
  }
324
332
  const vectorResults = (yield vectorQuery.toArray());
325
333
  let ftsResults = [];
326
- try {
327
- let ftsQuery = table.search(query).limit(PRE_RERANK_K);
328
- if (whereClause) {
329
- ftsQuery = ftsQuery.where(whereClause);
334
+ let ftsSearchFailed = false;
335
+ if (this.ftsAvailable) {
336
+ try {
337
+ let ftsQuery = table.search(query).limit(PRE_RERANK_K);
338
+ if (whereClause) {
339
+ ftsQuery = ftsQuery.where(whereClause);
340
+ }
341
+ ftsResults = (yield ftsQuery.toArray());
342
+ }
343
+ catch (e) {
344
+ ftsSearchFailed = true;
345
+ this.ftsAvailable = false;
346
+ const msg = e instanceof Error ? e.message : String(e);
347
+ console.warn(`[Searcher] FTS search failed (will retry later): ${msg}`);
330
348
  }
331
- ftsResults = (yield ftsQuery.toArray());
332
- }
333
- catch (e) {
334
- const msg = e instanceof Error ? e.message : String(e);
335
- console.warn(`[Searcher] FTS search failed: ${msg}`);
336
349
  }
337
350
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
338
351
  const err = new Error("Aborted");
@@ -453,8 +466,7 @@ class Searcher {
453
466
  const finalResults = diversified.map((item) => (Object.assign(Object.assign({}, item.record), { _score: item.score, vector: undefined, colbert: undefined })));
454
467
  // Item 12: Score Calibration
455
468
  const maxScore = finalResults.length > 0 ? finalResults[0]._score : 1.0;
456
- return {
457
- data: finalResults.map((r) => {
469
+ return Object.assign({ data: finalResults.map((r) => {
458
470
  const chunk = this.mapRecordToChunk(r, r._score || 0);
459
471
  // Normalize score relative to top result
460
472
  const normalized = maxScore > 0 ? r._score / maxScore : 0;
@@ -466,9 +478,15 @@ class Searcher {
466
478
  chunk.score = normalized;
467
479
  chunk.confidence = confidence;
468
480
  return chunk;
469
- }),
470
- };
481
+ }) }, (!this.ftsAvailable || ftsSearchFailed
482
+ ? {
483
+ warnings: [
484
+ "Full-text search unavailable — results may be less precise",
485
+ ],
486
+ }
487
+ : {}));
471
488
  });
472
489
  }
473
490
  }
474
491
  exports.Searcher = Searcher;
492
+ Searcher.FTS_RETRY_INTERVAL_MS = 5 * 60 * 1000;
@@ -156,13 +156,16 @@ class MetaCache {
156
156
  });
157
157
  }
158
158
  close() {
159
- var _a;
160
- if (this.closed)
161
- return;
162
- this.closed = true;
163
- (_a = this.unregisterCleanup) === null || _a === void 0 ? void 0 : _a.call(this);
164
- this.unregisterCleanup = undefined;
165
- this.db.close();
159
+ return __awaiter(this, void 0, void 0, function* () {
160
+ var _a;
161
+ if (this.closed)
162
+ return;
163
+ this.closed = true;
164
+ (_a = this.unregisterCleanup) === null || _a === void 0 ? void 0 : _a.call(this);
165
+ this.unregisterCleanup = undefined;
166
+ yield this.db.flushed;
167
+ this.db.close();
168
+ });
166
169
  }
167
170
  }
168
171
  exports.MetaCache = MetaCache;
@@ -344,6 +344,25 @@ class VectorDB {
344
344
  }
345
345
  });
346
346
  }
347
+ deletePathsExcludingIds(paths, excludeIds) {
348
+ return __awaiter(this, void 0, void 0, function* () {
349
+ if (!paths.length)
350
+ return;
351
+ const table = yield this.ensureTable();
352
+ const unique = Array.from(new Set(paths));
353
+ const batchSize = 500;
354
+ const idExclusion = excludeIds.length > 0
355
+ ? ` AND id NOT IN (${excludeIds.map((id) => `'${id.replace(/'/g, "''")}'`).join(",")})`
356
+ : "";
357
+ for (let i = 0; i < unique.length; i += batchSize) {
358
+ const slice = unique.slice(i, i + batchSize);
359
+ const values = slice
360
+ .map((p) => `'${p.replace(/'/g, "''")}'`)
361
+ .join(",");
362
+ yield table.delete(`path IN (${values})${idExclusion}`);
363
+ }
364
+ });
365
+ }
347
366
  deletePathsWithPrefix(prefix) {
348
367
  return __awaiter(this, void 0, void 0, function* () {
349
368
  const table = yield this.ensureTable();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.6.0",
3
+ "version": "0.6.3",
4
4
  "author": "Robert Owens <robowens@me.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -75,6 +75,9 @@
75
75
  "format": "biome check --write .",
76
76
  "format:check": "biome check .",
77
77
  "lint": "biome lint .",
78
- "typecheck": "tsc --noEmit"
78
+ "typecheck": "tsc --noEmit",
79
+ "preversion": "pnpm test && pnpm typecheck",
80
+ "version": "bash scripts/sync-versions.sh && git add -A",
81
+ "postversion": "git push origin main --tags && gh release create v$npm_package_version --generate-notes --title v$npm_package_version && sleep 5 && gh run watch $(gh run list --workflow=release.yml --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status && sleep 30 && npm cache clean --force && npm install -g grepmax@$npm_package_version"
79
82
  }
80
83
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.6.0",
3
+ "version": "0.6.3",
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",