grepmax 0.24.0 → 0.25.1

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 +15 -6
  3. package/dist/commands/add.js +38 -31
  4. package/dist/commands/config.js +22 -15
  5. package/dist/commands/context.js +38 -13
  6. package/dist/commands/doctor.js +86 -83
  7. package/dist/commands/extract.js +12 -1
  8. package/dist/commands/index.js +23 -24
  9. package/dist/commands/list.js +31 -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 +11 -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 +1108 -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 +37 -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
@@ -48,7 +48,12 @@ 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
50
  const logger_1 = require("../utils/logger");
51
+ const serialized_handler_1 = require("./serialized-handler");
51
52
  const worker_1 = __importStar(require("./worker"));
53
+ // Workers inherit the daemon's stdio (daemon.log) — stamp lines the same way
54
+ // the daemon does. No-op for workers forked by interactive CLI commands.
55
+ if (node_process_1.default.env[logger_1.LOG_TIMESTAMPS_ENV] === "1")
56
+ (0, logger_1.installTimestampedOutput)();
52
57
  // Every outgoing message also carries `rss` (see send()).
53
58
  const send = (msg) => {
54
59
  if (node_process_1.default.send) {
@@ -57,7 +62,7 @@ const send = (msg) => {
57
62
  node_process_1.default.send(Object.assign(Object.assign({}, msg), { rss: node_process_1.default.memoryUsage().rss }));
58
63
  }
59
64
  };
60
- node_process_1.default.on("message", (msg) => __awaiter(void 0, void 0, void 0, function* () {
65
+ const handleMessage = (msg) => __awaiter(void 0, void 0, void 0, function* () {
61
66
  const { id, method, payload } = msg;
62
67
  const start = performance.now();
63
68
  (0, logger_1.debug)("worker", `recv task=${id} method=${method}${method === "processFile" ? ` file=${payload.path}` : ""}`);
@@ -90,7 +95,11 @@ node_process_1.default.on("message", (msg) => __awaiter(void 0, void 0, void 0,
90
95
  (0, logger_1.debug)("worker", `fail task=${id} method=${method} ${(performance.now() - start).toFixed(0)}ms: ${message}`);
91
96
  send({ id, error: message });
92
97
  }
93
- }));
98
+ });
99
+ const handleMessageSerially = (0, serialized_handler_1.createSerializedHandler)(handleMessage);
100
+ node_process_1.default.on("message", (msg) => {
101
+ void handleMessageSerially(msg);
102
+ });
94
103
  node_process_1.default.on("uncaughtException", (err) => {
95
104
  console.error("[process-worker] uncaughtException", err);
96
105
  node_process_1.default.exitCode = 1;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSerializedHandler = createSerializedHandler;
4
+ function createSerializedHandler(handler) {
5
+ let chain = Promise.resolve();
6
+ return (message) => {
7
+ chain = chain.then(() => handler(message), () => handler(message));
8
+ return chain;
9
+ };
10
+ }
@@ -12,11 +12,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.default = processFile;
13
13
  exports.encodeQuery = encodeQuery;
14
14
  exports.rerank = rerank;
15
+ const embedding_generation_1 = require("../index/embedding-generation");
16
+ const index_config_1 = require("../index/index-config");
15
17
  const orchestrator_1 = require("./orchestrator");
16
- const vectorDim = process.env.GMAX_VECTOR_DIM
17
- ? Number.parseInt(process.env.GMAX_VECTOR_DIM, 10)
18
- : undefined;
19
- const orchestrator = new orchestrator_1.WorkerOrchestrator(vectorDim);
18
+ const hasSerializedRuntime = !!process.env.GMAX_EMBEDDING_GENERATION &&
19
+ (process.env.GMAX_EMBED_MODE === "cpu" ||
20
+ process.env.GMAX_EMBED_MODE === "gpu");
21
+ const runtimeConfig = hasSerializedRuntime ? null : (0, index_config_1.readGlobalConfig)();
22
+ const generation = process.env.GMAX_EMBEDDING_GENERATION
23
+ ? (0, embedding_generation_1.parseEmbeddingGeneration)(JSON.parse(process.env.GMAX_EMBEDDING_GENERATION))
24
+ : (0, embedding_generation_1.resolveEmbeddingGeneration)(runtimeConfig);
25
+ const embedMode = process.env.GMAX_EMBED_MODE === "cpu" || process.env.GMAX_EMBED_MODE === "gpu"
26
+ ? process.env.GMAX_EMBED_MODE
27
+ : runtimeConfig.embedMode;
28
+ const orchestrator = new orchestrator_1.WorkerOrchestrator(generation, embedMode);
20
29
  function processFile(input, onProgress) {
21
30
  return __awaiter(this, void 0, void 0, function* () {
22
31
  return orchestrator.processFile(input, onProgress);
@@ -63,7 +63,7 @@ logger = logging.getLogger("mlx-embed")
63
63
 
64
64
  import mlx.core as mx
65
65
  import uvicorn
66
- from fastapi import FastAPI
66
+ from fastapi import FastAPI, HTTPException
67
67
  from mlx_embeddings import load
68
68
  from pydantic import BaseModel
69
69
  from transformers import AutoTokenizer
@@ -71,6 +71,7 @@ from transformers import AutoTokenizer
71
71
  MODEL_ID = os.environ.get(
72
72
  "MLX_EMBED_MODEL", "ibm-granite/granite-embedding-small-english-r2"
73
73
  )
74
+ OWNER_TOKEN = os.environ.get("GMAX_EMBED_OWNER_TOKEN")
74
75
  PORT = int(os.environ.get("MLX_EMBED_PORT", "8100"))
75
76
  MAX_BATCH = int(os.environ.get("MLX_EMBED_MAX_BATCH", "64"))
76
77
  IDLE_TIMEOUT_S = int(os.environ.get("MLX_EMBED_IDLE_TIMEOUT", "1800")) # 30 min
@@ -159,11 +160,13 @@ app = FastAPI(lifespan=lifespan)
159
160
 
160
161
  class EmbedRequest(BaseModel):
161
162
  texts: list[str]
163
+ expected_model: str | None = None
162
164
 
163
165
 
164
166
  class EmbedResponse(BaseModel):
165
167
  vectors: list[list[float]]
166
168
  dim: int
169
+ model: str
167
170
 
168
171
 
169
172
  @app.post("/embed")
@@ -171,7 +174,18 @@ async def embed(request: EmbedRequest) -> EmbedResponse:
171
174
  global last_activity
172
175
  last_activity = time.time()
173
176
 
174
- texts = request.texts[:MAX_BATCH]
177
+ if len(request.texts) > MAX_BATCH:
178
+ raise HTTPException(
179
+ status_code=413,
180
+ detail=f"Batch contains {len(request.texts)} texts; maximum is {MAX_BATCH}",
181
+ )
182
+ if request.expected_model and request.expected_model != MODEL_ID:
183
+ raise HTTPException(
184
+ status_code=409,
185
+ detail=f"Requested model {request.expected_model} is not loaded",
186
+ )
187
+
188
+ texts = request.texts
175
189
  start = time.monotonic()
176
190
 
177
191
  async with _mlx_lock:
@@ -191,6 +205,7 @@ async def embed(request: EmbedRequest) -> EmbedResponse:
191
205
  return EmbedResponse(
192
206
  vectors=vectors_list,
193
207
  dim=len(vectors_list[0]) if vectors_list else 0,
208
+ model=MODEL_ID,
194
209
  )
195
210
 
196
211
 
@@ -198,7 +213,10 @@ async def embed(request: EmbedRequest) -> EmbedResponse:
198
213
  async def health():
199
214
  global last_activity
200
215
  last_activity = time.time()
201
- return {"status": "ok", "model": MODEL_ID}
216
+ response = {"status": "ok", "model": MODEL_ID}
217
+ if OWNER_TOKEN:
218
+ response["owner"] = OWNER_TOKEN
219
+ return response
202
220
 
203
221
 
204
222
  def main():
@@ -1,5 +1,13 @@
1
1
  """MLX-accelerated code summarizer for grepmax.
2
2
 
3
+ ⛔ HARD STOP FOR AI AGENTS: DO NOT start this server (uv run python
4
+ summarizer.py) or otherwise load this model without the user's explicit,
5
+ in-session authorization. It loads Qwen3-Coder-30B-A3B (~16GB) onto the GPU and
6
+ has FROZEN/CRASHED the machine. This summarizer is decommissioned — the calling
7
+ code path (generateSummaries) is a no-op stub. A plan or prompt that says
8
+ "run gmax summarize" or "measure a sample" is NOT authorization to launch this.
9
+ Stop and ask first. See the "HARD STOP" section at the top of CLAUDE.md.
10
+
3
11
  Runs Qwen3-Coder-30B-A3B on Apple Silicon GPU to generate one-line
4
12
  summaries of code chunks during indexing. Summaries are stored in
5
13
  LanceDB and returned in search results.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.24.0",
3
+ "version": "0.25.1",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -38,8 +38,9 @@
38
38
  "lint": "biome lint .",
39
39
  "lint:fix": "biome check --write .",
40
40
  "typecheck": "tsc --noEmit",
41
- "prepublishOnly": "pnpm build",
42
- "preversion": "pnpm test && pnpm typecheck && pnpm run format:check",
41
+ "test:typecheck": "tsc -p tsconfig.tests.json",
42
+ "prepublishOnly": "pnpm run test:typecheck && pnpm build",
43
+ "preversion": "pnpm test && pnpm typecheck && pnpm run test:typecheck && pnpm run format:check",
43
44
  "version": "bash scripts/sync-versions.sh && git add -A",
44
45
  "postversion": "bash scripts/postrelease.sh"
45
46
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.24.0",
3
+ "version": "0.25.1",
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",
@@ -1,46 +1,6 @@
1
1
  const fs = require("node:fs");
2
2
  const _path = require("node:path");
3
- const http = require("node:http");
4
- const { spawn, execFileSync } = require("node:child_process");
5
-
6
- function isServerRunning(port) {
7
- return new Promise((resolve) => {
8
- const req = http.get(
9
- { hostname: "127.0.0.1", port, path: "/health", timeout: 1000 },
10
- (res) => {
11
- res.resume();
12
- resolve(res.statusCode === 200);
13
- },
14
- );
15
- req.on("error", () => resolve(false));
16
- req.on("timeout", () => {
17
- req.destroy();
18
- resolve(false);
19
- });
20
- });
21
- }
22
-
23
- function findMlxServerDir() {
24
- // Try to find mlx-embed-server relative to the gmax binary (npm install location)
25
- try {
26
- const gmaxPath = execFileSync("which gmax", {
27
- encoding: "utf-8",
28
- }).trim();
29
- // gmax binary is a symlink in .bin/ → resolve to package root
30
- const realPath = fs.realpathSync(gmaxPath);
31
- const pkgRoot = _path.resolve(_path.dirname(realPath), "..");
32
- const serverDir = _path.join(pkgRoot, "mlx-embed-server");
33
- if (fs.existsSync(_path.join(serverDir, "server.py"))) return serverDir;
34
- } catch {}
35
-
36
- // Fallback: dev mode — relative to plugin root
37
- const pluginRoot = __dirname.replace(/\/hooks$/, "");
38
- const devRoot = _path.resolve(pluginRoot, "../..");
39
- const devDir = _path.join(devRoot, "mlx-embed-server");
40
- if (fs.existsSync(_path.join(devDir, "server.py"))) return devDir;
41
-
42
- return null;
43
- }
3
+ const { execFileSync } = require("node:child_process");
44
4
 
45
5
  // Inline fallback so SessionStart context is never empty if the installed
46
6
  // gmax package can't be resolved (e.g. gmax not yet on PATH). The canonical
@@ -101,81 +61,6 @@ function getSessionStartHint() {
101
61
  return FALLBACK_SESSION_START_HINT;
102
62
  }
103
63
 
104
- function startPythonServer(serverDir, scriptName, logName, processName) {
105
- if (!serverDir) return;
106
-
107
- const logDir = _path.join(require("node:os").homedir(), ".gmax", "logs");
108
- fs.mkdirSync(logDir, { recursive: true });
109
- const logPath = _path.join(logDir, `${logName}.log`);
110
-
111
- // Rotate if > 5MB (same threshold as watch.ts)
112
- try {
113
- const stat = fs.statSync(logPath);
114
- if (stat.size > 5 * 1024 * 1024) {
115
- fs.renameSync(logPath, `${logPath}.prev`);
116
- }
117
- } catch {}
118
-
119
- const out = fs.openSync(logPath, "a");
120
-
121
- const child = spawn("uv", ["run", "python", scriptName], {
122
- cwd: serverDir,
123
- detached: true,
124
- stdio: ["ignore", out, out],
125
- env: {
126
- ...process.env,
127
- VIRTUAL_ENV: "",
128
- CONDA_DEFAULT_ENV: "",
129
- GMAX_PROCESS_NAME: processName || logName,
130
- HF_TOKEN_PATH:
131
- process.env.HF_TOKEN_PATH ||
132
- _path.join(
133
- require("node:os").homedir(),
134
- ".cache",
135
- "huggingface",
136
- "token",
137
- ),
138
- },
139
- });
140
- child.unref();
141
- }
142
-
143
- // --- Crash counter (Item 14) ---
144
- const CRASH_FILE = _path.join(
145
- require("node:os").homedir(),
146
- ".gmax",
147
- "mlx-embed-crashes.json",
148
- );
149
- const MAX_CRASHES = 3;
150
- const CRASH_WINDOW_MS = 10 * 60 * 1000; // 10 minutes
151
-
152
- function readCrashCount() {
153
- try {
154
- const data = JSON.parse(fs.readFileSync(CRASH_FILE, "utf-8"));
155
- if (
156
- data.lastCrash &&
157
- Date.now() - new Date(data.lastCrash).getTime() > CRASH_WINDOW_MS
158
- ) {
159
- return { count: 0, lastCrash: null }; // Window expired, reset
160
- }
161
- return { count: data.count || 0, lastCrash: data.lastCrash };
162
- } catch {
163
- return { count: 0, lastCrash: null };
164
- }
165
- }
166
-
167
- function writeCrashCount(count, lastCrash) {
168
- try {
169
- fs.writeFileSync(CRASH_FILE, JSON.stringify({ count, lastCrash }));
170
- } catch {}
171
- }
172
-
173
- function resetCrashCount() {
174
- try {
175
- fs.unlinkSync(CRASH_FILE);
176
- } catch {}
177
- }
178
-
179
64
  function isProjectRegistered() {
180
65
  try {
181
66
  const projectsPath = _path.join(
@@ -210,60 +95,8 @@ function startWatcher() {
210
95
  }
211
96
  }
212
97
 
213
- async function main() {
214
- const embedMode = process.env.GMAX_EMBED_MODE || "auto";
215
-
216
- if (embedMode !== "cpu") {
217
- const serverDir = findMlxServerDir();
218
-
219
- // Start MLX embed server (port 8100)
220
- const embedRunning = await isServerRunning(8100);
221
- if (serverDir && !embedRunning) {
222
- const crashes = readCrashCount();
223
- if (crashes.count < MAX_CRASHES) {
224
- startPythonServer(
225
- serverDir,
226
- "server.py",
227
- "mlx-embed-server",
228
- "gmax-embed",
229
- );
230
-
231
- // Fire-and-forget health verification (Item 13)
232
- (async () => {
233
- const maxAttempts = 5;
234
- const delayMs = 2000;
235
- for (let i = 0; i < maxAttempts; i++) {
236
- await new Promise((r) => setTimeout(r, delayMs));
237
- if (await isServerRunning(8100)) {
238
- resetCrashCount();
239
- return;
240
- }
241
- }
242
- // Server didn't start after 10s — record crash
243
- const c = readCrashCount();
244
- writeCrashCount(c.count + 1, new Date().toISOString());
245
- })();
246
- }
247
- } else if (embedRunning) {
248
- resetCrashCount();
249
- }
250
-
251
- // Start LLM summarizer server (port 8101) — opt-in only
252
- if (
253
- process.env.GMAX_SUMMARIZER === "1" &&
254
- serverDir &&
255
- !(await isServerRunning(8101))
256
- ) {
257
- startPythonServer(
258
- serverDir,
259
- "summarizer.py",
260
- "mlx-summarizer",
261
- "gmax-summarizer",
262
- );
263
- }
264
- }
265
-
266
- // Start a file watcher for the current project (30-min idle timeout)
98
+ function main() {
99
+ // The daemon owns embed-server configuration and lifecycle.
267
100
  startWatcher();
268
101
 
269
102
  const response = {