grepmax 0.18.1 → 0.20.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 (78) hide show
  1. package/.claude-plugin/marketplace.json +23 -0
  2. package/README.md +28 -3
  3. package/dist/commands/add.js +4 -2
  4. package/dist/commands/claude-code.js +1 -1
  5. package/dist/commands/codex.js +3 -1
  6. package/dist/commands/context.js +1 -1
  7. package/dist/commands/dead.js +2 -6
  8. package/dist/commands/doctor.js +26 -7
  9. package/dist/commands/extract.js +3 -5
  10. package/dist/commands/impact.js +6 -4
  11. package/dist/commands/index.js +7 -2
  12. package/dist/commands/log.js +4 -2
  13. package/dist/commands/mcp.js +594 -722
  14. package/dist/commands/peek.js +15 -9
  15. package/dist/commands/plugin.js +36 -22
  16. package/dist/commands/project.js +16 -6
  17. package/dist/commands/related.js +8 -9
  18. package/dist/commands/remove.js +9 -2
  19. package/dist/commands/review.js +1 -1
  20. package/dist/commands/setup.js +4 -2
  21. package/dist/commands/similar.js +4 -4
  22. package/dist/commands/status.js +6 -4
  23. package/dist/commands/summarize.js +6 -4
  24. package/dist/commands/symbols.js +1 -1
  25. package/dist/commands/test-find.js +2 -2
  26. package/dist/commands/trace.js +12 -5
  27. package/dist/commands/watch.js +33 -9
  28. package/dist/eval-graph-nav.js +4 -1
  29. package/dist/eval-graph-recovery-probe.js +56 -14
  30. package/dist/eval-graph-spotcheck.js +10 -2
  31. package/dist/eval-graph-totals.js +5 -2
  32. package/dist/eval-oss.js +212 -37
  33. package/dist/eval-seed.js +13 -4
  34. package/dist/index.js +9 -9
  35. package/dist/lib/daemon/daemon.js +32 -13
  36. package/dist/lib/daemon/ipc-handler.js +24 -5
  37. package/dist/lib/daemon/mlx-server-manager.js +11 -3
  38. package/dist/lib/daemon/process-manager.js +1 -2
  39. package/dist/lib/daemon/watcher-manager.js +22 -6
  40. package/dist/lib/graph/callsites.js +151 -25
  41. package/dist/lib/graph/graph-builder.js +2 -2
  42. package/dist/lib/graph/impact.js +6 -6
  43. package/dist/lib/index/batch-processor.js +16 -6
  44. package/dist/lib/index/chunker.js +2 -0
  45. package/dist/lib/index/syncer.js +22 -10
  46. package/dist/lib/index/watcher-batch.js +10 -1
  47. package/dist/lib/llm/config.js +2 -1
  48. package/dist/lib/llm/diff.js +56 -11
  49. package/dist/lib/llm/investigate.js +52 -11
  50. package/dist/lib/llm/review.js +21 -8
  51. package/dist/lib/llm/server.js +25 -9
  52. package/dist/lib/llm/tools.js +1 -1
  53. package/dist/lib/output/agent-search-formatter.js +25 -3
  54. package/dist/lib/output/index-state-footer.js +1 -1
  55. package/dist/lib/review/risk.js +2 -4
  56. package/dist/lib/search/pagerank.js +1 -1
  57. package/dist/lib/search/searcher.js +43 -17
  58. package/dist/lib/search/seed-weight.js +4 -1
  59. package/dist/lib/skeleton/symbol-extractor.js +2 -1
  60. package/dist/lib/store/vector-db.js +21 -10
  61. package/dist/lib/utils/cross-project.js +5 -1
  62. package/dist/lib/utils/daemon-client.js +39 -1
  63. package/dist/lib/utils/filter-builder.js +22 -0
  64. package/dist/lib/utils/git.js +10 -2
  65. package/dist/lib/utils/project-registry.js +3 -2
  66. package/dist/lib/utils/scope-filter.js +6 -6
  67. package/dist/lib/utils/watcher-launcher.js +4 -1
  68. package/dist/lib/utils/watcher-store.js +2 -1
  69. package/dist/lib/workers/embeddings/granite.js +4 -1
  70. package/dist/lib/workers/embeddings/mlx-client.js +7 -2
  71. package/dist/lib/workers/pool.js +30 -7
  72. package/dist/lib/workers/process-child.js +1 -1
  73. package/package.json +23 -19
  74. package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
  75. package/plugins/grepmax/hooks/cwd-changed.js +4 -1
  76. package/plugins/grepmax/hooks/pre-grep.js +2 -1
  77. package/plugins/grepmax/hooks/start.js +45 -10
  78. package/plugins/grepmax/hooks/subagent-start.js +1 -4
@@ -48,6 +48,7 @@ const fs = __importStar(require("node:fs"));
48
48
  const lancedb = __importStar(require("@lancedb/lancedb"));
49
49
  const apache_arrow_1 = require("apache-arrow");
50
50
  const config_1 = require("../../config");
51
+ const index_config_1 = require("../index/index-config");
51
52
  const cleanup_1 = require("../utils/cleanup");
52
53
  const filter_builder_1 = require("../utils/filter-builder");
53
54
  const logger_1 = require("../utils/logger");
@@ -86,7 +87,10 @@ class VectorDB {
86
87
  this.activeWrites = 0;
87
88
  this.writeDrainResolve = null;
88
89
  this.compactingPromise = null;
89
- this.vectorDim = vectorDim !== null && vectorDim !== void 0 ? vectorDim : config_1.CONFIG.VECTOR_DIM;
90
+ // Default to the configured tier's dim (not the hard-wired small-tier 384)
91
+ // so a `standard`-tier index actually stores 768d vectors. An explicit
92
+ // arg still wins (eval scripts, tests).
93
+ this.vectorDim = vectorDim !== null && vectorDim !== void 0 ? vectorDim : (0, index_config_1.readGlobalConfig)().vectorDim;
90
94
  this.unregisterCleanup = (0, cleanup_1.registerCleanup)(() => this.close());
91
95
  }
92
96
  /**
@@ -411,11 +415,13 @@ class VectorDB {
411
415
  // Callers (syncer flushBatch) splice records before passing — they're never reused.
412
416
  for (const rec of records) {
413
417
  const vec = toNumberArray(rec.vector);
414
- if (vec.length < this.vectorDim) {
415
- vec.push(...Array(this.vectorDim - vec.length).fill(0));
416
- }
417
- else if (vec.length > this.vectorDim) {
418
- vec.length = this.vectorDim;
418
+ // Never silently pad/truncate: a width mismatch means the embedding tier
419
+ // and this table disagree (wrong model wired, or a stale index after a
420
+ // tier change). Reshaping would store garbage that scores meaninglessly.
421
+ // Fail loudly and point at the fix instead.
422
+ if (vec.length !== this.vectorDim) {
423
+ throw new Error(`Vector dimension mismatch: got ${vec.length}d, expected ${this.vectorDim}d. ` +
424
+ "The embedding model tier likely changed without a rebuild — run `gmax index --reset`.");
419
425
  }
420
426
  rec.vector = vec;
421
427
  rec.colbert = toBuffer(rec.colbert);
@@ -676,7 +682,7 @@ class VectorDB {
676
682
  const rows = yield table
677
683
  .query()
678
684
  .select(["id"])
679
- .where(`path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
685
+ .where((0, filter_builder_1.pathStartsWith)(prefix))
680
686
  .limit(1)
681
687
  .toArray();
682
688
  return rows.length > 0;
@@ -686,7 +692,7 @@ class VectorDB {
686
692
  return __awaiter(this, void 0, void 0, function* () {
687
693
  const table = yield this.ensureTable();
688
694
  const prefix = pathPrefix.endsWith("/") ? pathPrefix : `${pathPrefix}/`;
689
- return table.countRows(`path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`);
695
+ return table.countRows((0, filter_builder_1.pathStartsWith)(prefix));
690
696
  });
691
697
  }
692
698
  countDistinctFilesForPath(pathPrefix) {
@@ -701,7 +707,7 @@ class VectorDB {
701
707
  const rows = yield table
702
708
  .query()
703
709
  .select(["path"])
704
- .where(`path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
710
+ .where((0, filter_builder_1.pathStartsWith)(prefix))
705
711
  .toArray();
706
712
  const unique = new Set();
707
713
  for (const r of rows) {
@@ -804,7 +810,12 @@ class VectorDB {
804
810
  return __awaiter(this, void 0, void 0, function* () {
805
811
  this.ensureDiskOk();
806
812
  const table = yield this.ensureTable();
807
- yield this.withWriteGate(() => table.delete(`path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`));
813
+ // Slash-terminate so a project root can't bleed into a sibling
814
+ // (`/repo/app` must not delete `/repo/app2`), and use starts_with so `_`/`%`
815
+ // in the path are literal, not LIKE wildcards. Destructive path — keep this
816
+ // self-protective even if a caller forgets to normalize.
817
+ const dirPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
818
+ yield this.withWriteGate(() => table.delete((0, filter_builder_1.pathStartsWith)(dirPrefix)));
808
819
  });
809
820
  }
810
821
  drop() {
@@ -105,7 +105,11 @@ function groupResultsByProject(results, roots, getPath) {
105
105
  const key = (_a = owner === null || owner === void 0 ? void 0 : owner.root) !== null && _a !== void 0 ? _a : "(unknown)";
106
106
  let bucket = buckets.get(key);
107
107
  if (!bucket) {
108
- bucket = { name: (_b = owner === null || owner === void 0 ? void 0 : owner.name) !== null && _b !== void 0 ? _b : "(unknown)", root: (_c = owner === null || owner === void 0 ? void 0 : owner.root) !== null && _c !== void 0 ? _c : "", items: [] };
108
+ bucket = {
109
+ name: (_b = owner === null || owner === void 0 ? void 0 : owner.name) !== null && _b !== void 0 ? _b : "(unknown)",
110
+ root: (_c = owner === null || owner === void 0 ? void 0 : owner.root) !== null && _c !== void 0 ? _c : "",
111
+ items: [],
112
+ };
109
113
  buckets.set(key, bucket);
110
114
  order.push(key);
111
115
  }
@@ -45,6 +45,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.sendDaemonCommand = sendDaemonCommand;
46
46
  exports.isDaemonRunning = isDaemonRunning;
47
47
  exports.isDaemonHeartbeatFresh = isDaemonHeartbeatFresh;
48
+ exports.readDaemonPid = readDaemonPid;
49
+ exports.waitForProcessExit = waitForProcessExit;
48
50
  exports.ensureDaemonRunning = ensureDaemonRunning;
49
51
  exports.sendStreamingCommand = sendStreamingCommand;
50
52
  const fs = __importStar(require("node:fs"));
@@ -95,7 +97,10 @@ function sendDaemonCommand(cmd, opts) {
95
97
  socket.on("error", (err) => {
96
98
  var _a;
97
99
  clearTimeout(timer);
98
- finish({ ok: false, error: (_a = err.code) !== null && _a !== void 0 ? _a : err.message });
100
+ finish({
101
+ ok: false,
102
+ error: (_a = err.code) !== null && _a !== void 0 ? _a : err.message,
103
+ });
99
104
  });
100
105
  socket.on("close", () => {
101
106
  clearTimeout(timer);
@@ -149,6 +154,38 @@ function isDaemonHeartbeatFresh() {
149
154
  return false;
150
155
  }
151
156
  }
157
+ /** Read the daemon's PID from the PID file, or null if absent/invalid. */
158
+ function readDaemonPid() {
159
+ try {
160
+ const pid = parseInt(fs.readFileSync(config_1.PATHS.daemonPidFile, "utf-8").trim(), 10);
161
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
162
+ }
163
+ catch (_a) {
164
+ return null;
165
+ }
166
+ }
167
+ /**
168
+ * Poll until a process has fully exited, or the timeout elapses. Used before
169
+ * spawning a successor daemon: shutdown() drops the socket/lock immediately but
170
+ * keeps draining in-flight work, and a successor that starts in that window
171
+ * classifies the still-draining predecessor as stale and SIGKILLs it. Waiting
172
+ * for the actual process exit closes that race. Returns true if it exited.
173
+ */
174
+ function waitForProcessExit(pid, timeoutMs) {
175
+ return __awaiter(this, void 0, void 0, function* () {
176
+ const deadline = Date.now() + timeoutMs;
177
+ while (Date.now() < deadline) {
178
+ try {
179
+ process.kill(pid, 0);
180
+ }
181
+ catch (_a) {
182
+ return true; // ESRCH — process is gone
183
+ }
184
+ yield new Promise((r) => setTimeout(r, 100));
185
+ }
186
+ return false;
187
+ });
188
+ }
152
189
  /**
153
190
  * Ensure the daemon is running — start it if needed, poll up to 5s.
154
191
  * Returns true if daemon is ready, false if it couldn't be started.
@@ -207,6 +244,7 @@ function sendStreamingCommand(cmd, onProgress, opts) {
207
244
  socket.on("data", (chunk) => {
208
245
  buf += chunk.toString();
209
246
  let nl;
247
+ // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic buffer-split loop
210
248
  while ((nl = buf.indexOf("\n")) !== -1) {
211
249
  const line = buf.slice(0, nl);
212
250
  buf = buf.slice(nl + 1);
@@ -1,12 +1,34 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.escapeSqlString = escapeSqlString;
4
+ exports.pathStartsWith = pathStartsWith;
5
+ exports.pathNotStartsWith = pathNotStartsWith;
4
6
  exports.normalizePath = normalizePath;
5
7
  function escapeSqlString(str) {
6
8
  // LanceDB (via DataFusion) treats backslashes literally in standard strings.
7
9
  // We only need to escape single quotes by doubling them.
8
10
  return str.replace(/'/g, "''");
9
11
  }
12
+ /**
13
+ * SQL predicate matching rows whose `path` begins with `prefix`.
14
+ *
15
+ * Use this instead of `path LIKE '<prefix>%'` for any path-prefix scope.
16
+ * `escapeSqlString` only neutralizes quotes, not LIKE metacharacters, so a
17
+ * prefix containing `_` (matches any single char) or `%` (matches anything)
18
+ * would silently match — and delete — across sibling projects
19
+ * (`/repo/my_app/` would match `/repo/myXapp/`). `starts_with()` has no
20
+ * wildcard semantics, so the prefix is matched literally.
21
+ *
22
+ * Callers are responsible for trailing-slash boundary correctness: pass
23
+ * `/repo/app/` (not `/repo/app`) so the scope can't bleed into `/repo/app2/`.
24
+ */
25
+ function pathStartsWith(prefix) {
26
+ return `starts_with(path, '${escapeSqlString(prefix)}')`;
27
+ }
28
+ /** Negation of {@link pathStartsWith} — excludes paths under `prefix`. */
29
+ function pathNotStartsWith(prefix) {
30
+ return `NOT ${pathStartsWith(prefix)}`;
31
+ }
10
32
  /**
11
33
  * Normalizes a path to use forward slashes, ensuring consistency across platforms.
12
34
  * @param p The path to normalize
@@ -90,7 +90,11 @@ function getMainRepoRoot(worktreeRoot) {
90
90
  * Returns absolute paths. Includes both staged and unstaged changes.
91
91
  */
92
92
  function getChangedFiles(ref, cwd) {
93
- const opts = { cwd: cwd !== null && cwd !== void 0 ? cwd : process.cwd(), encoding: "utf-8", timeout: 10000 };
93
+ const opts = {
94
+ cwd: cwd !== null && cwd !== void 0 ? cwd : process.cwd(),
95
+ encoding: "utf-8",
96
+ timeout: 10000,
97
+ };
94
98
  try {
95
99
  let output;
96
100
  if (ref) {
@@ -210,7 +214,11 @@ function getCommitHistory(opts) {
210
214
  * Returns absolute paths.
211
215
  */
212
216
  function getUntrackedFiles(cwd) {
213
- const opts = { cwd: cwd !== null && cwd !== void 0 ? cwd : process.cwd(), encoding: "utf-8", timeout: 10000 };
217
+ const opts = {
218
+ cwd: cwd !== null && cwd !== void 0 ? cwd : process.cwd(),
219
+ encoding: "utf-8",
220
+ timeout: 10000,
221
+ };
214
222
  try {
215
223
  const output = (0, node_child_process_1.execFileSync)("git", ["ls-files", "--others", "--exclude-standard"], opts);
216
224
  const root = (0, node_child_process_1.execFileSync)("git", ["rev-parse", "--show-toplevel"], opts).trim();
@@ -66,7 +66,7 @@ function loadRegistry() {
66
66
  }
67
67
  function saveRegistry(entries) {
68
68
  fs.mkdirSync(path.dirname(REGISTRY_PATH), { recursive: true });
69
- const tmp = REGISTRY_PATH + ".tmp";
69
+ const tmp = `${REGISTRY_PATH}.tmp`;
70
70
  fs.writeFileSync(tmp, `${JSON.stringify(entries, null, 2)}\n`);
71
71
  fs.renameSync(tmp, REGISTRY_PATH);
72
72
  }
@@ -119,7 +119,8 @@ function removeProject(root) {
119
119
  */
120
120
  function getParentProject(root) {
121
121
  const resolved = root.endsWith("/") ? root : `${root}/`;
122
- return loadRegistry().find((e) => e.root !== root && resolved.startsWith(e.root.endsWith("/") ? e.root : `${e.root}/`));
122
+ return loadRegistry().find((e) => e.root !== root &&
123
+ resolved.startsWith(e.root.endsWith("/") ? e.root : `${e.root}/`));
123
124
  }
124
125
  /**
125
126
  * Find registered projects that are children of this path.
@@ -49,7 +49,9 @@ function toArray(value) {
49
49
  .filter(Boolean);
50
50
  }
51
51
  function joinSubpath(projectRoot, sub) {
52
- const rootWithSlash = projectRoot.endsWith("/") ? projectRoot : `${projectRoot}/`;
52
+ const rootWithSlash = projectRoot.endsWith("/")
53
+ ? projectRoot
54
+ : `${projectRoot}/`;
53
55
  if (path.isAbsolute(sub))
54
56
  return sub.endsWith("/") ? sub : `${sub}/`;
55
57
  if (sub.startsWith(rootWithSlash))
@@ -90,14 +92,12 @@ function buildScopeWhere(scope, condition) {
90
92
  const parts = [];
91
93
  if (condition)
92
94
  parts.push(condition);
93
- parts.push(`path LIKE '${(0, filter_builder_1.escapeSqlString)(scope.pathPrefix)}%'`);
95
+ parts.push((0, filter_builder_1.pathStartsWith)(scope.pathPrefix));
94
96
  for (const ex of scope.excludePrefixes) {
95
- parts.push(`path NOT LIKE '${(0, filter_builder_1.escapeSqlString)(ex)}%'`);
97
+ parts.push((0, filter_builder_1.pathNotStartsWith)(ex));
96
98
  }
97
99
  if (scope.inPrefixes.length > 0) {
98
- const ors = scope.inPrefixes
99
- .map((p) => `path LIKE '${(0, filter_builder_1.escapeSqlString)(p)}%'`)
100
- .join(" OR ");
100
+ const ors = scope.inPrefixes.map((p) => (0, filter_builder_1.pathStartsWith)(p)).join(" OR ");
101
101
  parts.push(`(${ors})`);
102
102
  }
103
103
  return parts.join(" AND ");
@@ -56,7 +56,10 @@ function launchWatcher(projectRoot) {
56
56
  for (let i = 0; i < 25; i++) {
57
57
  yield new Promise((r) => setTimeout(r, 200));
58
58
  try {
59
- const retry = yield (0, daemon_client_1.sendDaemonCommand)({ cmd: "watch", root: projectRoot });
59
+ const retry = yield (0, daemon_client_1.sendDaemonCommand)({
60
+ cmd: "watch",
61
+ root: projectRoot,
62
+ });
60
63
  if (retry.ok && typeof retry.pid === "number") {
61
64
  return { ok: true, pid: retry.pid, reused: false };
62
65
  }
@@ -84,7 +84,8 @@ function isAlive(info) {
84
84
  if (!isProcessRunning(info.pid))
85
85
  return false;
86
86
  // If heartbeat exists and is stale, treat as dead (possibly deadlocked)
87
- if (info.lastHeartbeat && Date.now() - info.lastHeartbeat > HEARTBEAT_STALE_MS) {
87
+ if (info.lastHeartbeat &&
88
+ Date.now() - info.lastHeartbeat > HEARTBEAT_STALE_MS) {
88
89
  return false;
89
90
  }
90
91
  return true;
@@ -63,7 +63,10 @@ class GraniteModel {
63
63
  this.vectorDimensions = vectorDim !== null && vectorDim !== void 0 ? vectorDim : config_1.CONFIG.VECTOR_DIM;
64
64
  }
65
65
  resolvePaths() {
66
- const basePath = path.join(CACHE_DIR, config_1.MODEL_IDS.embed);
66
+ // Honor the active tier's ONNX model (propagated from the pool); fall back
67
+ // to the small-tier default when unset (standalone/test invocations).
68
+ const modelId = process.env.GMAX_EMBED_ONNX_MODEL || config_1.MODEL_IDS.embed;
69
+ const basePath = path.join(CACHE_DIR, modelId);
67
70
  const onnxDir = path.join(basePath, "onnx");
68
71
  const candidates = ["model_q4.onnx", "model.onnx"];
69
72
  for (const candidate of candidates) {
@@ -182,8 +182,13 @@ function mlxEmbed(texts) {
182
182
  const wasPreviouslyAvailable = mlxAvailable !== false;
183
183
  mlxAvailable = false;
184
184
  const now = Date.now();
185
- if (wasPreviouslyAvailable || now - lastMlxWarning >= MLX_WARNING_INTERVAL_MS) {
186
- console.error("[mlx] Embed server failed: bad response (ok=" + ok + ", hasVectors=" + !!(data === null || data === void 0 ? void 0 : data.vectors) + ")");
185
+ if (wasPreviouslyAvailable ||
186
+ now - lastMlxWarning >= MLX_WARNING_INTERVAL_MS) {
187
+ console.error("[mlx] Embed server failed: bad response (ok=" +
188
+ ok +
189
+ ", hasVectors=" +
190
+ !!(data === null || data === void 0 ? void 0 : data.vectors) +
191
+ ")");
187
192
  lastMlxWarning = now;
188
193
  }
189
194
  return null;
@@ -43,6 +43,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.WorkerPool = void 0;
46
+ exports.embeddingEnv = embeddingEnv;
46
47
  exports.getWorkerPool = getWorkerPool;
47
48
  exports.destroyWorkerPool = destroyWorkerPool;
48
49
  exports.isWorkerPoolInitialized = isWorkerPoolInitialized;
@@ -51,10 +52,11 @@ exports.isWorkerPoolInitialized = isWorkerPoolInitialized;
51
52
  * to ensure the ONNX Runtime segfaults do not crash the main process.
52
53
  */
53
54
  const childProcess = __importStar(require("node:child_process"));
54
- const logger_1 = require("../utils/logger");
55
55
  const fs = __importStar(require("node:fs"));
56
56
  const path = __importStar(require("node:path"));
57
57
  const config_1 = require("../../config");
58
+ const index_config_1 = require("../index/index-config");
59
+ const logger_1 = require("../utils/logger");
58
60
  function reviveBufferLike(input) {
59
61
  if (input &&
60
62
  typeof input === "object" &&
@@ -110,6 +112,22 @@ const FORCE_KILL_GRACE_MS = 200;
110
112
  // escalate to SIGKILL. ~5s is well above ONNX teardown time but short
111
113
  // enough that the reap loop self-heals within a minute.
112
114
  const REAP_FORCE_KILL_GRACE_MS = 5000;
115
+ /**
116
+ * Embedding env derived from the active model tier, injected into every spawned
117
+ * worker so the worker's VectorDB dim (GMAX_VECTOR_DIM) and CPU ONNX model
118
+ * (GMAX_EMBED_ONNX_MODEL) match the configured tier. Without this, workers fall
119
+ * back to the hard-wired small tier (384d) regardless of config. One spot covers
120
+ * every worker user (index, add, search, daemon, mcp). Merged so process.env can
121
+ * override — see the spread order at fork().
122
+ */
123
+ function embeddingEnv() {
124
+ const { modelTier } = (0, index_config_1.readGlobalConfig)();
125
+ const ids = (0, index_config_1.getModelIdsForTier)(modelTier);
126
+ return {
127
+ GMAX_VECTOR_DIM: String(ids.vectorDim),
128
+ GMAX_EMBED_ONNX_MODEL: ids.embed,
129
+ };
130
+ }
113
131
  class ProcessWorker {
114
132
  constructor(modulePath, execArgv, maxMemoryMb) {
115
133
  this.modulePath = modulePath;
@@ -126,12 +144,12 @@ class ProcessWorker {
126
144
  // event). Guards against handleWorkerExit running twice when both events
127
145
  // fire for the same crash.
128
146
  this.cleanedUp = false;
129
- const memArgs = maxMemoryMb
130
- ? [`--max-old-space-size=${maxMemoryMb}`]
131
- : [];
147
+ const memArgs = maxMemoryMb ? [`--max-old-space-size=${maxMemoryMb}`] : [];
148
+ // embeddingEnv() first so a manually-exported GMAX_VECTOR_DIM /
149
+ // GMAX_EMBED_ONNX_MODEL in process.env still wins (spread last).
132
150
  this.child = childProcess.fork(modulePath, {
133
151
  execArgv: [...memArgs, ...execArgv],
134
- env: Object.assign({}, process.env),
152
+ env: Object.assign(Object.assign({}, embeddingEnv()), process.env),
135
153
  });
136
154
  }
137
155
  }
@@ -167,7 +185,10 @@ const WORKER_RSS_RECYCLE_MB = (() => {
167
185
  // Methods that must skip the indexing backlog. encodeQuery is the search hot
168
186
  // path: a single query is ~17ms but waits behind every queued processFile.
169
187
  // rerank is similarly small and latency-sensitive.
170
- const PRIORITY_METHODS = new Set(["encodeQuery", "rerank"]);
188
+ const PRIORITY_METHODS = new Set([
189
+ "encodeQuery",
190
+ "rerank",
191
+ ]);
171
192
  class WorkerPool {
172
193
  constructor() {
173
194
  this.workers = [];
@@ -320,7 +341,9 @@ class WorkerPool {
320
341
  if (task.method === "processFile") {
321
342
  result = reviveProcessFileResult(result);
322
343
  }
323
- const elapsed = task.startTime ? `${Date.now() - task.startTime}ms` : "?ms";
344
+ const elapsed = task.startTime
345
+ ? `${Date.now() - task.startTime}ms`
346
+ : "?ms";
324
347
  const filePath = (_h = (_f = (_e = task.payload) === null || _e === void 0 ? void 0 : _e.path) !== null && _f !== void 0 ? _f : (_g = task.payload) === null || _g === void 0 ? void 0 : _g.absolutePath) !== null && _h !== void 0 ? _h : "";
325
348
  (0, logger_1.debug)("pool", `complete task=${task.id} method=${task.method} ${elapsed}${filePath ? ` file=${filePath}` : ""}`);
326
349
  task.resolve(result);
@@ -47,8 +47,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
48
  const node_process_1 = __importDefault(require("node:process"));
49
49
  node_process_1.default.title = "gmax-worker";
50
- const worker_1 = __importStar(require("./worker"));
51
50
  const logger_1 = require("../utils/logger");
51
+ const worker_1 = __importStar(require("./worker"));
52
52
  // Every outgoing message also carries `rss` (see send()).
53
53
  const send = (msg) => {
54
54
  if (node_process_1.default.send) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.18.1",
3
+ "version": "0.20.0",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -14,6 +14,9 @@
14
14
  "bin": {
15
15
  "gmax": "dist/index.js"
16
16
  },
17
+ "engines": {
18
+ "node": ">=22.12.0"
19
+ },
17
20
  "scripts": {
18
21
  "postinstall": "node scripts/postinstall.js",
19
22
  "prebuild": "mkdir -p dist",
@@ -30,9 +33,10 @@
30
33
  "bench:recall:json": "GMAX_EVAL_JSON=1 npx tsx src/eval.ts",
31
34
  "bench:oss": "npx tsx src/eval-oss.ts all",
32
35
  "bench:oss:json": "GMAX_EVAL_JSON=1 npx tsx src/eval-oss.ts all",
33
- "format": "biome check --write .",
36
+ "format": "biome format --write .",
34
37
  "format:check": "biome check .",
35
38
  "lint": "biome lint .",
39
+ "lint:fix": "biome check --write .",
36
40
  "typecheck": "tsc --noEmit",
37
41
  "prepublishOnly": "pnpm build",
38
42
  "preversion": "pnpm test && pnpm typecheck",
@@ -61,6 +65,7 @@
61
65
  ],
62
66
  "files": [
63
67
  "dist",
68
+ ".claude-plugin",
64
69
  "plugins",
65
70
  "scripts/postinstall.js",
66
71
  "mlx-embed-server",
@@ -71,38 +76,37 @@
71
76
  "license": "Apache-2.0",
72
77
  "description": "Semantic code search for coding agents. Local embeddings, LLM summaries, call graph tracing.",
73
78
  "dependencies": {
74
- "@clack/prompts": "^1.1.0",
75
- "@huggingface/transformers": "^4.0.0",
79
+ "@clack/prompts": "^1.6.0",
80
+ "@huggingface/transformers": "^4.2.0",
76
81
  "@lancedb/lancedb": "^0.30.0",
77
82
  "@modelcontextprotocol/sdk": "^1.29.0",
78
83
  "@parcel/watcher": "^2.5.6",
79
84
  "apache-arrow": "^18.1.0",
80
85
  "chalk": "^5.6.2",
81
86
  "cli-highlight": "^2.1.11",
82
- "commander": "^14.0.2",
83
- "dotenv": "^17.2.3",
87
+ "commander": "^15.0.0",
88
+ "dotenv": "^17.4.2",
84
89
  "fast-glob": "^3.3.3",
85
90
  "ignore": "^7.0.5",
86
- "lmdb": "^3.5.2",
91
+ "lmdb": "^3.5.6",
87
92
  "onnxruntime-node": "1.24.3",
88
- "openai": "^6.33.0",
89
- "ora": "^9.3.0",
90
- "piscina": "^5.1.4",
93
+ "openai": "^6.45.0",
94
+ "ora": "^9.4.1",
91
95
  "proper-lockfile": "^4.1.2",
92
96
  "simsimd": "^6.5.5",
93
- "uuid": "^13.0.0",
94
- "web-tree-sitter": "^0.26.7",
95
- "zod": "^4.1.12"
97
+ "uuid": "^14.0.1",
98
+ "web-tree-sitter": "^0.26.9",
99
+ "zod": "^4.4.3"
96
100
  },
97
101
  "devDependencies": {
98
- "@anthropic-ai/claude-agent-sdk": "^0.2.87",
99
102
  "@biomejs/biome": "2.4.10",
100
- "@types/node": "^25.5.0",
103
+ "@types/node": "^26.0.1",
101
104
  "@types/proper-lockfile": "^4.1.4",
102
- "node-gyp": "^12.1.0",
105
+ "node-gyp": "^13.0.0",
103
106
  "ts-node": "^10.9.2",
104
- "typescript": "^6.0.2",
105
- "vite": "^8.0.3",
106
- "vitest": "^4.1.2"
107
+ "tsx": "^4.22.4",
108
+ "typescript": "^6.0.3",
109
+ "vite": "^8.1.0",
110
+ "vitest": "^4.1.9"
107
111
  }
108
112
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.18.1",
3
+ "version": "0.20.0",
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",
@@ -41,7 +41,10 @@ async function main() {
41
41
  if (!isProjectRegistered(newCwd)) return;
42
42
 
43
43
  try {
44
- execFileSync("gmax", ["watch", "--daemon", "-b"], { timeout: 5000, stdio: "ignore" });
44
+ execFileSync("gmax", ["watch", "--daemon", "-b"], {
45
+ timeout: 5000,
46
+ stdio: "ignore",
47
+ });
45
48
  } catch {
46
49
  try {
47
50
  execFileSync("gmax", ["watch", "-b"], { timeout: 5000, stdio: "ignore" });
@@ -28,7 +28,8 @@ const SNAKE_CASE = /_[a-z]/;
28
28
  const ALL_CAPS = /^[A-Z_]+$/;
29
29
  const HAS_DOT = /\./;
30
30
  const HAS_QUOTE = /['"]/;
31
- const CODE_KEYWORDS = /\b(class|function|const|let|var|import|export|return|extends|implements|interface|type|enum|struct|def|async|await)\b/;
31
+ const CODE_KEYWORDS =
32
+ /\b(class|function|const|let|var|import|export|return|extends|implements|interface|type|enum|struct|def|async|await)\b/;
32
33
 
33
34
  /**
34
35
  * Conservative heuristic: returns true only when the pattern is very