grepmax 0.26.13 → 0.26.15

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.
@@ -10,7 +10,7 @@
10
10
  "name": "grepmax",
11
11
  "source": "./plugins/grepmax",
12
12
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
13
- "version": "0.26.13",
13
+ "version": "0.26.15",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
@@ -56,6 +56,7 @@ const lock_1 = require("../lib/utils/lock");
56
56
  const mlx_hf_cache_1 = require("../lib/utils/mlx-hf-cache");
57
57
  const project_registry_1 = require("../lib/utils/project-registry");
58
58
  const project_root_1 = require("../lib/utils/project-root");
59
+ const root_availability_1 = require("../lib/utils/root-availability");
59
60
  function formatSize(bytes) {
60
61
  if (bytes < 1024)
61
62
  return `${bytes} B`;
@@ -197,6 +198,7 @@ exports.doctor = new commander_1.Command("doctor")
197
198
  let needsOptimize = false;
198
199
  let staleLock = false;
199
200
  const orphanedProjects = [];
201
+ const unavailableProjects = [];
200
202
  try {
201
203
  const { VectorDB, findStaleLanceTempFiles, sweepStaleLanceTempFiles } = yield Promise.resolve().then(() => __importStar(require("../lib/store/vector-db")));
202
204
  const db = new VectorDB(config_1.PATHS.lancedbDir);
@@ -250,12 +252,21 @@ exports.doctor = new commander_1.Command("doctor")
250
252
  // Daemon status
251
253
  const { isDaemonRunning } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
252
254
  const daemonUp = yield isDaemonRunning();
253
- // Project registry health
255
+ // Project registry health.
256
+ //
257
+ // A root can be unreadable for two very different reasons, and only one of
258
+ // them is the project's fault. An unplugged external drive or a downed
259
+ // network share leaves the project perfectly intact — deregistering it
260
+ // would strand its vectors in the shared table with no owning project and
261
+ // would not come back when the volume returns. Only a root whose volume is
262
+ // mounted, and whose directory is still gone, is actually orphaned.
254
263
  const projects = (0, project_registry_1.listProjects)();
255
264
  for (const p of projects) {
256
- if (!fs.existsSync(p.root)) {
265
+ const availability = (0, root_availability_1.classifyRoot)(p.root);
266
+ if (availability === "missing")
257
267
  orphanedProjects.push(p.root);
258
- }
268
+ else if (availability === "unavailable")
269
+ unavailableProjects.push(p.root);
259
270
  }
260
271
  // Compute warning flags
261
272
  //
@@ -313,6 +324,7 @@ exports.doctor = new commander_1.Command("doctor")
313
324
  `lock=${lockStatus.split(" ")[0]}`,
314
325
  `daemon=${daemonUp ? "running" : "stopped"}`,
315
326
  `orphaned=${orphanedProjects.length}`,
327
+ `offline=${unavailableProjects.length}`,
316
328
  `stale_temp=${staleTempFiles.length}`,
317
329
  `stale_temp_bytes=${staleTempBytes}`,
318
330
  `stale_chunker=${staleChunkerProjects.length}`,
@@ -455,13 +467,19 @@ exports.doctor = new commander_1.Command("doctor")
455
467
  if (legacyNotice)
456
468
  console.log(legacyNotice);
457
469
  // Projects
470
+ if (unavailableProjects.length > 0) {
471
+ console.log(`INFO Offline projects: ${unavailableProjects.length} (volume not mounted — kept, reconnect to resume)`);
472
+ for (const up of unavailableProjects) {
473
+ console.log(` - ${up}`);
474
+ }
475
+ }
458
476
  if (orphanedProjects.length > 0) {
459
477
  console.log(`WARN Orphaned projects: ${orphanedProjects.length} (directories no longer exist)`);
460
478
  for (const op of orphanedProjects) {
461
479
  console.log(` - ${op}`);
462
480
  }
463
481
  }
464
- else if (projects.length > 0) {
482
+ else if (projects.length > 0 && unavailableProjects.length === 0) {
465
483
  console.log(`ok Projects: ${projects.length} registered, all directories exist`);
466
484
  }
467
485
  // Cache Coherence
@@ -563,6 +581,9 @@ exports.doctor = new commander_1.Command("doctor")
563
581
  console.log(`ok Optimize complete (${via})`);
564
582
  fixed++;
565
583
  }
584
+ // Only roots whose volume is mounted and whose directory is still gone
585
+ // reach this list — projects on an unplugged drive are classified
586
+ // `unavailable` and never enter it, so --fix cannot deregister them.
566
587
  if (orphanedProjects.length > 0) {
567
588
  for (const op of orphanedProjects) {
568
589
  (0, project_registry_1.removeProject)(op);
@@ -67,6 +67,7 @@ const logger_1 = require("../utils/logger");
67
67
  const operation_coordinator_1 = require("../utils/operation-coordinator");
68
68
  const process_1 = require("../utils/process");
69
69
  const project_registry_1 = require("../utils/project-registry");
70
+ const root_availability_1 = require("../utils/root-availability");
70
71
  const watcher_store_1 = require("../utils/watcher-store");
71
72
  const pool_1 = require("../workers/pool");
72
73
  const ipc_handler_1 = require("./ipc-handler");
@@ -352,7 +353,10 @@ class Daemon {
352
353
  const indexed = allProjects.filter((p) => p.status === "indexed");
353
354
  for (const p of indexed) {
354
355
  if (!fs.existsSync(p.root)) {
355
- console.log(`[daemon] Skipping ${path.basename(p.root)} — directory not found`);
356
+ // Skipping is already the safe behavior — the project keeps its registry
357
+ // entry and its vectors either way. Naming the reason keeps an unplugged
358
+ // drive from reading like data loss in the log.
359
+ console.log(`[daemon] Skipping ${path.basename(p.root)} — ${(0, root_availability_1.describeRoot)(p.root)}`);
356
360
  continue;
357
361
  }
358
362
  try {
@@ -324,9 +324,13 @@ function initialSync(options) {
324
324
  yield currentFlush;
325
325
  (0, logger_1.debug)("index", `flush done: ${Date.now() - flushStart}ms`);
326
326
  flushCount++;
327
- // Periodically compact during sync to prevent fragment accumulation
327
+ // Periodically compact during sync to prevent fragment accumulation.
328
+ // Uses the shared threshold deliberately: a hardcoded 30 here meant
329
+ // the full-index path kept compacting at the old rate long after the
330
+ // threshold was raised to 400, re-triggering the whole-table rewrite
331
+ // that raising it exists to avoid.
328
332
  if (flushCount % 10 === 0) {
329
- yield vectorDb.compactIfNeeded(30);
333
+ yield vectorDb.compactIfNeeded(config_1.FRAGMENT_COMPACT_THRESHOLD);
330
334
  }
331
335
  }
332
336
  catch (err) {
@@ -187,22 +187,115 @@ function isIndexableFile(filePath, size) {
187
187
  function isIndexablePath(filePath) {
188
188
  return isIndexableFile(filePath);
189
189
  }
190
- // Well-known machine-generated markers emitted at the top of codegen output.
191
- // Tools (protoc, graphql-codegen, Apollo, Relay, Go `stringer`, OpenAPI, etc.)
192
- // stamp one of these in a banner comment. Matched against only the first few KB
193
- // because the banner is always at the very top — this keeps a hand-written
194
- // "do not edit this section" note deep in a real file from being misclassified.
195
- const GENERATED_MARKER = /@generated\b|\bDO NOT EDIT\b|Code generated by |auto-?generated by |This (?:file|code) (?:is|was) (?:\w+[ -])?generated|machine generated|@autogenerated\b/i;
190
+ // Markers that only ever appear in a codegen banner. Any one of these, on its
191
+ // own, is enough. Tools (protoc, graphql-codegen, Apollo, Relay, Go `stringer`,
192
+ // OpenAPI) stamp one of these; doc generators fence their output with the
193
+ // `generated-body` / `AUTO-GENERATED-CONTENT` style directives.
194
+ const STRONG_GENERATED_MARKER = /@generated\b|@autogenerated\b|\bCode generated by |\bauto-?generated by |\bThis (?:file|code) (?:is|was) (?:\w+[ -])?generated|\bmachine[ -]generated\b|\bauto-?generated[-_ ]?content\b|\bgenerated[-_](?:body|content|section|block)\b|\bBEGIN GENERATED\b/i;
195
+ // "DO NOT EDIT" is not proof of codegen on its own — humans write it on prose
196
+ // they want left alone (archived vendor snapshots, verbatim captures). Real
197
+ // codegen banners pair it with a generation word, so require that pairing.
198
+ const DO_NOT_EDIT_MARKER = /\bDO NOT EDIT\b/i;
199
+ const GENERATION_CONTEXT = /generat|codegen|autogen/i;
196
200
  const GENERATED_SNIFF_BYTES = 2048;
201
+ const DOC_LIKE_EXTENSIONS = /\.(?:md|mdx|markdown|txt|rst|adoc|asciidoc)$/i;
202
+ /**
203
+ * Extract the leading banner: the run of shebang, frontmatter, blank, and
204
+ * comment lines at the very top, stopping at the first line of actual code.
205
+ *
206
+ * Scanning raw head bytes instead was the bug this replaces. Two ways it
207
+ * misfired, both silently erasing hand-written files from the index:
208
+ *
209
+ * - A *generator* was classified as its own output, because the banner it
210
+ * emits appears as a string literal in its body
211
+ * (`lines.push('# Auto-generated by ...')`).
212
+ * - A comment quoting another file's rules ("its README says do not edit")
213
+ * counted as a banner.
214
+ *
215
+ * A real codegen banner precedes all code, so anchoring here removes both
216
+ * without weakening detection.
217
+ *
218
+ * In doc-like files `#` starts a heading, not a comment, so only frontmatter
219
+ * and HTML comments count — otherwise the whole document body would be read as
220
+ * banner.
221
+ */
222
+ function leadingBanner(head, filePath) {
223
+ var _a, _b;
224
+ const docLike = filePath ? DOC_LIKE_EXTENSIONS.test(filePath) : false;
225
+ const lines = head.split("\n");
226
+ const banner = [];
227
+ let i = 0;
228
+ if (!docLike && ((_a = lines[0]) === null || _a === void 0 ? void 0 : _a.startsWith("#!"))) {
229
+ banner.push(lines[0]);
230
+ i = 1;
231
+ }
232
+ // YAML frontmatter is metadata, not code — a generated-doc marker often sits
233
+ // directly after it.
234
+ if (((_b = lines[i]) === null || _b === void 0 ? void 0 : _b.trim()) === "---") {
235
+ banner.push(lines[i]);
236
+ i++;
237
+ while (i < lines.length && lines[i].trim() !== "---")
238
+ banner.push(lines[i++]);
239
+ if (i < lines.length)
240
+ banner.push(lines[i++]);
241
+ }
242
+ let blockEnd;
243
+ for (; i < lines.length; i++) {
244
+ const line = lines[i];
245
+ const trimmed = line.trim();
246
+ if (blockEnd) {
247
+ banner.push(line);
248
+ if (trimmed.includes(blockEnd))
249
+ blockEnd = undefined;
250
+ continue;
251
+ }
252
+ if (trimmed === "") {
253
+ banner.push(line);
254
+ continue;
255
+ }
256
+ if (trimmed.startsWith("<!--")) {
257
+ banner.push(line);
258
+ if (!trimmed.includes("-->"))
259
+ blockEnd = "-->";
260
+ continue;
261
+ }
262
+ if (!docLike) {
263
+ if (trimmed.startsWith("/*")) {
264
+ banner.push(line);
265
+ if (!trimmed.includes("*/"))
266
+ blockEnd = "*/";
267
+ continue;
268
+ }
269
+ if (trimmed.startsWith("//") ||
270
+ trimmed.startsWith("#") ||
271
+ trimmed.startsWith("--") ||
272
+ trimmed.startsWith(";") ||
273
+ trimmed.startsWith("*")) {
274
+ banner.push(line);
275
+ continue;
276
+ }
277
+ }
278
+ break; // first real line of content — the banner is over
279
+ }
280
+ return banner.join("\n");
281
+ }
197
282
  /**
198
283
  * Detect machine-generated source by its banner. Generated files flood both the
199
284
  * vector index and the symbol graph (codegen types rank as god nodes), so they
200
285
  * are skipped at index time. Path globs in `ignore-patterns.ts` catch the
201
286
  * obvious filenames; this catches the rest by content.
287
+ *
288
+ * `filePath` is optional but worth passing: without it, doc-like files are
289
+ * scanned with code comment rules, so a markdown `# Heading` reads as a comment.
202
290
  */
203
- function isGeneratedContent(buffer) {
291
+ function isGeneratedContent(buffer, filePath) {
204
292
  const head = buffer.subarray(0, GENERATED_SNIFF_BYTES).toString("utf-8");
205
- return GENERATED_MARKER.test(head);
293
+ const banner = leadingBanner(head, filePath);
294
+ if (!banner.trim())
295
+ return false;
296
+ if (STRONG_GENERATED_MARKER.test(banner))
297
+ return true;
298
+ return DO_NOT_EDIT_MARKER.test(banner) && GENERATION_CONTEXT.test(banner);
206
299
  }
207
300
  function formatDenseSnippet(text, maxLength = 1500) {
208
301
  const clean = text !== null && text !== void 0 ? text : "";
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.classifyRoot = classifyRoot;
37
+ exports.isRootUnavailable = isRootUnavailable;
38
+ exports.describeRoot = describeRoot;
39
+ const fs = __importStar(require("node:fs"));
40
+ /**
41
+ * Directories that hold mount points rather than user data, with how many path
42
+ * segments below them the mount point itself sits.
43
+ *
44
+ * `mountDepth` errs high where a platform is ambiguous (Linux uses both
45
+ * `/media/<volume>` and `/media/<user>/<volume>`). Overshooting biases the
46
+ * classifier toward `unavailable`, which is the safe direction: a wrong
47
+ * `unavailable` only means we decline to auto-remove a genuinely dead project,
48
+ * while a wrong `missing` silently deregisters a live one.
49
+ */
50
+ const MOUNT_CONTAINERS = [
51
+ { prefix: "/Volumes", mountDepth: 1 }, // macOS
52
+ { prefix: "/run/media", mountDepth: 2 }, // Linux, udisks2
53
+ { prefix: "/media", mountDepth: 2 },
54
+ { prefix: "/mnt", mountDepth: 1 },
55
+ { prefix: "/net", mountDepth: 1 }, // autofs network mounts
56
+ ];
57
+ function exists(p) {
58
+ try {
59
+ fs.statSync(p);
60
+ return true;
61
+ }
62
+ catch (_a) {
63
+ // A disconnected network mount can throw EIO/ETIMEDOUT rather than ENOENT.
64
+ // Either way the path is not readable, and the caller's container check
65
+ // decides whether that means unavailable or missing.
66
+ return false;
67
+ }
68
+ }
69
+ /**
70
+ * Classify a project root that may live on removable or network storage.
71
+ *
72
+ * Longest matching container wins, so `/run/media` is not shadowed by `/media`.
73
+ */
74
+ function classifyRoot(root) {
75
+ if (exists(root))
76
+ return "present";
77
+ const container = MOUNT_CONTAINERS.filter((c) => root === c.prefix || root.startsWith(`${c.prefix}/`)).sort((a, b) => b.prefix.length - a.prefix.length)[0];
78
+ if (!container)
79
+ return "missing";
80
+ // The mount point is the container plus `mountDepth` segments. If it is
81
+ // absent, nothing is mounted there and the project is merely out of reach.
82
+ const rest = root.slice(container.prefix.length).split("/").filter(Boolean);
83
+ if (rest.length < container.mountDepth)
84
+ return "missing";
85
+ const mountPoint = `${container.prefix}/${rest.slice(0, container.mountDepth).join("/")}`;
86
+ return exists(mountPoint) ? "missing" : "unavailable";
87
+ }
88
+ /**
89
+ * True when the root is absent only because its volume is not mounted.
90
+ * Callers that delete or rewrite persistent state must check this first.
91
+ */
92
+ function isRootUnavailable(root) {
93
+ return classifyRoot(root) === "unavailable";
94
+ }
95
+ /** Human-readable reason for a non-present root, for logs and doctor output. */
96
+ function describeRoot(root) {
97
+ switch (classifyRoot(root)) {
98
+ case "present":
99
+ return "available";
100
+ case "unavailable":
101
+ return "volume not mounted";
102
+ case "missing":
103
+ return "directory not found";
104
+ }
105
+ }
@@ -256,7 +256,7 @@ class WorkerOrchestrator {
256
256
  (0, logger_1.debug)("orch", `skip ${input.path} (empty or binary)`);
257
257
  return { vectors: [], hash, mtimeMs, size, shouldDelete: true };
258
258
  }
259
- if ((0, file_utils_1.isGeneratedContent)(buffer)) {
259
+ if ((0, file_utils_1.isGeneratedContent)(buffer, absolutePath)) {
260
260
  (0, logger_1.debug)("orch", `skip ${input.path} (machine-generated header)`);
261
261
  return { vectors: [], hash, mtimeMs, size, shouldDelete: true };
262
262
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.13",
3
+ "version": "0.26.15",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.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.26.13",
3
+ "version": "0.26.15",
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",