grepmax 0.23.0 → 0.25.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 (85) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +25 -6
  3. package/dist/commands/add.js +38 -31
  4. package/dist/commands/config.js +16 -15
  5. package/dist/commands/context.js +38 -13
  6. package/dist/commands/doctor.js +76 -83
  7. package/dist/commands/extract.js +12 -1
  8. package/dist/commands/impact.js +33 -1
  9. package/dist/commands/index.js +23 -24
  10. package/dist/commands/list.js +23 -14
  11. package/dist/commands/mcp.js +494 -162
  12. package/dist/commands/peek.js +16 -5
  13. package/dist/commands/repair.js +54 -120
  14. package/dist/commands/search-output.js +30 -9
  15. package/dist/commands/search-run.js +75 -47
  16. package/dist/commands/search-skeletons.js +28 -18
  17. package/dist/commands/search.js +45 -49
  18. package/dist/commands/serve.js +415 -373
  19. package/dist/commands/setup.js +2 -2
  20. package/dist/commands/similar.js +5 -5
  21. package/dist/commands/skeleton.js +67 -41
  22. package/dist/commands/status.js +5 -2
  23. package/dist/commands/summarize.js +6 -0
  24. package/dist/commands/surprises.js +150 -0
  25. package/dist/commands/watch.js +102 -38
  26. package/dist/config.js +3 -0
  27. package/dist/eval-surprising-connections.js +191 -0
  28. package/dist/index.js +2 -0
  29. package/dist/lib/analysis/surprising-connections.js +600 -0
  30. package/dist/lib/daemon/daemon.js +1101 -379
  31. package/dist/lib/daemon/ipc-handler.js +122 -13
  32. package/dist/lib/daemon/mlx-server-manager.js +268 -125
  33. package/dist/lib/daemon/process-manager.js +7 -4
  34. package/dist/lib/daemon/search-handler.js +23 -9
  35. package/dist/lib/daemon/watcher-manager.js +353 -110
  36. package/dist/lib/graph/impact-rollup.js +202 -0
  37. package/dist/lib/graph/impact.js +15 -1
  38. package/dist/lib/help/agent-cheatsheet.js +1 -0
  39. package/dist/lib/index/batch-processor.js +231 -67
  40. package/dist/lib/index/embedding-generation.js +109 -0
  41. package/dist/lib/index/embedding-status.js +23 -0
  42. package/dist/lib/index/file-policy.js +273 -0
  43. package/dist/lib/index/ignore-patterns.js +4 -0
  44. package/dist/lib/index/index-config.js +18 -4
  45. package/dist/lib/index/syncer.js +256 -79
  46. package/dist/lib/index/walker.js +66 -86
  47. package/dist/lib/index/watcher-batch.js +9 -0
  48. package/dist/lib/index/watcher.js +129 -3
  49. package/dist/lib/llm/server.js +6 -0
  50. package/dist/lib/search/searcher.js +30 -19
  51. package/dist/lib/skeleton/skeletonizer.js +118 -0
  52. package/dist/lib/skeleton/summary-formatter.js +8 -1
  53. package/dist/lib/store/store-lease.js +473 -0
  54. package/dist/lib/store/vector-db.js +302 -57
  55. package/dist/lib/utils/blocked-roots.js +63 -0
  56. package/dist/lib/utils/cross-project.js +7 -3
  57. package/dist/lib/utils/daemon-client.js +24 -1
  58. package/dist/lib/utils/daemon-launcher.js +38 -13
  59. package/dist/lib/utils/doctor-status.js +76 -0
  60. package/dist/lib/utils/file-utils.js +74 -4
  61. package/dist/lib/utils/keyed-mutex.js +101 -0
  62. package/dist/lib/utils/logger.js +57 -1
  63. package/dist/lib/utils/mlx-hf-cache.js +114 -0
  64. package/dist/lib/utils/operation-coordinator.js +146 -0
  65. package/dist/lib/utils/path-containment.js +106 -0
  66. package/dist/lib/utils/process.js +44 -0
  67. package/dist/lib/utils/project-registry.js +351 -3
  68. package/dist/lib/utils/scope-filter.js +3 -9
  69. package/dist/lib/utils/stale-hint.js +12 -19
  70. package/dist/lib/utils/watcher-launcher.js +5 -1
  71. package/dist/lib/utils/watcher-store.js +2 -2
  72. package/dist/lib/workers/colbert-math.js +15 -12
  73. package/dist/lib/workers/embeddings/colbert.js +3 -2
  74. package/dist/lib/workers/embeddings/granite.js +4 -3
  75. package/dist/lib/workers/embeddings/mlx-client.js +59 -16
  76. package/dist/lib/workers/orchestrator.js +39 -8
  77. package/dist/lib/workers/pool.js +150 -83
  78. package/dist/lib/workers/process-child.js +11 -2
  79. package/dist/lib/workers/serialized-handler.js +10 -0
  80. package/dist/lib/workers/worker.js +13 -4
  81. package/mlx-embed-server/server.py +21 -3
  82. package/mlx-embed-server/summarizer.py +8 -0
  83. package/package.json +6 -3
  84. package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
  85. package/plugins/grepmax/hooks/start.js +3 -170
@@ -41,77 +41,345 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
41
41
  step((generator = generator.apply(thisArg, _arguments || [])).next());
42
42
  });
43
43
  };
44
+ var __rest = (this && this.__rest) || function (s, e) {
45
+ var t = {};
46
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
47
+ t[p] = s[p];
48
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
49
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
50
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
51
+ t[p[i]] = s[p[i]];
52
+ }
53
+ return t;
54
+ };
44
55
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.serve = void 0;
56
+ exports.serve = exports.MAX_HTTP_SEARCH_LIMIT = void 0;
57
+ exports.waitForChildSpawn = waitForChildSpawn;
58
+ exports.serveRootError = serveRootError;
59
+ exports.createServeHttpServer = createServeHttpServer;
60
+ exports.listenOnLoopback = listenOnLoopback;
46
61
  const node_child_process_1 = require("node:child_process");
47
62
  const fs = __importStar(require("node:fs"));
48
63
  const http = __importStar(require("node:http"));
49
64
  const path = __importStar(require("node:path"));
50
65
  const commander_1 = require("commander");
51
66
  const config_1 = require("../config");
52
- const grammar_loader_1 = require("../lib/index/grammar-loader");
53
- const index_config_1 = require("../lib/index/index-config");
54
- const sync_helpers_1 = require("../lib/index/sync-helpers");
55
- const syncer_1 = require("../lib/index/syncer");
56
- const watcher_1 = require("../lib/index/watcher");
57
- const searcher_1 = require("../lib/search/searcher");
58
- const setup_helpers_1 = require("../lib/setup/setup-helpers");
59
- const meta_cache_1 = require("../lib/store/meta-cache");
60
- const vector_db_1 = require("../lib/store/vector-db");
67
+ const blocked_roots_1 = require("../lib/utils/blocked-roots");
68
+ const daemon_client_1 = require("../lib/utils/daemon-client");
61
69
  const exit_1 = require("../lib/utils/exit");
62
70
  const log_rotate_1 = require("../lib/utils/log-rotate");
71
+ const path_containment_1 = require("../lib/utils/path-containment");
63
72
  const project_root_1 = require("../lib/utils/project-root");
73
+ const scope_filter_1 = require("../lib/utils/scope-filter");
64
74
  const server_registry_1 = require("../lib/utils/server-registry");
65
- function isMlxServerUp() {
66
- const port = parseInt(process.env.MLX_EMBED_PORT || "8100", 10);
67
- return new Promise((resolve) => {
68
- const req = http.get({ hostname: "127.0.0.1", port, path: "/health", timeout: 2000 }, (res) => {
69
- res.resume();
70
- resolve(res.statusCode === 200);
75
+ const LOOPBACK_HOST = "127.0.0.1";
76
+ const MAX_BODY_BYTES = 1000000;
77
+ exports.MAX_HTTP_SEARCH_LIMIT = 50;
78
+ class HttpRequestError extends Error {
79
+ constructor(status, code) {
80
+ super(code);
81
+ this.status = status;
82
+ this.code = code;
83
+ }
84
+ }
85
+ function waitForChildSpawn(child) {
86
+ return new Promise((resolve, reject) => {
87
+ const onSpawn = () => {
88
+ child.off("error", onError);
89
+ child.on("error", (error) => {
90
+ console.error(`Background server process error: ${error.message}`);
91
+ });
92
+ if (child.pid === undefined) {
93
+ reject(new Error("Background process started without a PID"));
94
+ return;
95
+ }
96
+ resolve(child.pid);
97
+ };
98
+ const onError = (error) => {
99
+ child.off("spawn", onSpawn);
100
+ reject(error);
101
+ };
102
+ child.once("spawn", onSpawn);
103
+ child.once("error", onError);
104
+ });
105
+ }
106
+ function writeJson(res, status, body) {
107
+ if (res.writableEnded)
108
+ return;
109
+ res.statusCode = status;
110
+ res.setHeader("Content-Type", "application/json");
111
+ res.end(JSON.stringify(body));
112
+ }
113
+ function readJsonBody(req) {
114
+ return new Promise((resolve, reject) => {
115
+ const chunks = [];
116
+ let size = 0;
117
+ let settled = false;
118
+ const fail = (err) => {
119
+ if (settled)
120
+ return;
121
+ settled = true;
122
+ reject(err);
123
+ };
124
+ req.on("data", (chunk) => {
125
+ if (settled)
126
+ return;
127
+ size += chunk.length;
128
+ if (size > MAX_BODY_BYTES) {
129
+ fail(new HttpRequestError(413, "payload_too_large"));
130
+ return;
131
+ }
132
+ chunks.push(chunk);
71
133
  });
72
- req.on("error", () => resolve(false));
73
- req.on("timeout", () => {
74
- req.destroy();
75
- resolve(false);
134
+ req.on("error", fail);
135
+ req.on("aborted", () => fail(new HttpRequestError(499, "aborted")));
136
+ req.on("end", () => {
137
+ if (settled)
138
+ return;
139
+ settled = true;
140
+ try {
141
+ const value = chunks.length
142
+ ? JSON.parse(Buffer.concat(chunks).toString("utf8"))
143
+ : {};
144
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
145
+ reject(new HttpRequestError(400, "invalid_json_object"));
146
+ return;
147
+ }
148
+ resolve(value);
149
+ }
150
+ catch (_a) {
151
+ reject(new HttpRequestError(400, "invalid_json"));
152
+ }
76
153
  });
77
154
  });
78
155
  }
79
- function startMlxServer(mlxModel) {
80
- // Look for mlx-embed-server relative to the grepmax package
81
- const candidates = [
82
- path.resolve(__dirname, "../../mlx-embed-server"),
83
- path.resolve(__dirname, "../mlx-embed-server"),
84
- ];
85
- const serverDir = candidates.find((d) => fs.existsSync(path.join(d, "server.py")));
86
- if (!serverDir)
87
- return null;
88
- const out = (0, log_rotate_1.openRotatedLog)(path.join(config_1.PATHS.logsDir, "mlx-embed-server.log"));
89
- const env = Object.assign({}, process.env);
90
- if (mlxModel) {
91
- env.MLX_EMBED_MODEL = mlxModel;
156
+ function daemonErrorStatus(error) {
157
+ if (error === "busy" || error === "rebuilding")
158
+ return 503;
159
+ if (error === "project not registered" || error === "project not watched") {
160
+ return 404;
161
+ }
162
+ if (error === "invalid limit" || error === "invalid path")
163
+ return 400;
164
+ return 500;
165
+ }
166
+ function serveRootError(root) {
167
+ if (!(0, blocked_roots_1.isBlockedProjectRoot)(root))
168
+ return undefined;
169
+ return (`Refusing to serve ${root}: this path is blocked from indexing.\n` +
170
+ `(Blocked: ${blocked_roots_1.BLOCKED_ROOTS_DESCRIPTION}.)\n` +
171
+ "Pick a specific project subdirectory instead.");
172
+ }
173
+ function optionalString(body, key) {
174
+ const value = body[key];
175
+ if (value === undefined)
176
+ return undefined;
177
+ if (typeof value !== "string")
178
+ throw new HttpRequestError(400, `invalid_${key}`);
179
+ return value;
180
+ }
181
+ function optionalStringArray(body, key) {
182
+ const value = body[key];
183
+ if (value === undefined)
184
+ return undefined;
185
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
186
+ throw new HttpRequestError(400, `invalid_${key}`);
92
187
  }
93
- const child = (0, node_child_process_1.spawn)("uv", ["run", "python", "server.py"], {
94
- cwd: serverDir,
95
- detached: true,
96
- stdio: ["ignore", out, out],
97
- env,
188
+ return value;
189
+ }
190
+ function createServeHttpServer(projectRoot, deps) {
191
+ const active = new Set();
192
+ const server = http.createServer((req, res) => __awaiter(this, void 0, void 0, function* () {
193
+ var _a, _b, _c, _d;
194
+ (_a = deps.onActivity) === null || _a === void 0 ? void 0 : _a.call(deps);
195
+ if (req.method === "GET" && req.url === "/health") {
196
+ writeJson(res, 200, { status: "ok" });
197
+ return;
198
+ }
199
+ const ac = new AbortController();
200
+ active.add(ac);
201
+ const abort = () => ac.abort();
202
+ req.once("aborted", abort);
203
+ res.once("close", () => {
204
+ if (!res.writableFinished)
205
+ abort();
206
+ });
207
+ try {
208
+ if (req.method === "GET" && req.url === "/stats") {
209
+ const response = yield deps.stats(ac.signal);
210
+ if (!response.ok) {
211
+ writeJson(res, daemonErrorStatus(response.error), {
212
+ error: (_b = response.error) !== null && _b !== void 0 ? _b : "stats_failed",
213
+ });
214
+ return;
215
+ }
216
+ const { ok: _ok } = response, stats = __rest(response, ["ok"]);
217
+ writeJson(res, 200, stats);
218
+ return;
219
+ }
220
+ if (req.method === "POST" && req.url === "/search") {
221
+ const body = yield readJsonBody(req);
222
+ const query = typeof body.query === "string" ? body.query.trim() : "";
223
+ if (!query)
224
+ throw new HttpRequestError(400, "invalid_query");
225
+ const limit = body.limit === undefined ? 10 : body.limit;
226
+ if (typeof limit !== "number" ||
227
+ !Number.isInteger(limit) ||
228
+ limit < 1 ||
229
+ limit > exports.MAX_HTTP_SEARCH_LIMIT) {
230
+ throw new HttpRequestError(400, "invalid_limit");
231
+ }
232
+ const requestedPath = optionalString(body, "path");
233
+ const inValues = optionalStringArray(body, "in");
234
+ const excludeValues = optionalStringArray(body, "exclude");
235
+ const scope = (0, scope_filter_1.resolveScope)({
236
+ projectRoot,
237
+ in: (inValues === null || inValues === void 0 ? void 0 : inValues.length)
238
+ ? inValues
239
+ : requestedPath
240
+ ? [requestedPath]
241
+ : undefined,
242
+ exclude: excludeValues,
243
+ });
244
+ if (requestedPath) {
245
+ (0, path_containment_1.resolveContainedPath)(projectRoot, requestedPath, {
246
+ verifyExistingTarget: true,
247
+ });
248
+ }
249
+ const filters = {};
250
+ const file = optionalString(body, "file");
251
+ const language = optionalString(body, "lang");
252
+ const role = optionalString(body, "role");
253
+ if (file)
254
+ filters.file = file;
255
+ if (language)
256
+ filters.language = language;
257
+ if (role)
258
+ filters.role = role;
259
+ if (scope.inPrefixes.length > 0)
260
+ filters.inPrefixes = scope.inPrefixes;
261
+ if (scope.excludePrefixes.length > 0) {
262
+ filters.excludePrefixes = scope.excludePrefixes;
263
+ }
264
+ const response = yield deps.search({
265
+ query,
266
+ limit,
267
+ pathPrefix: scope.pathPrefix,
268
+ filters: Object.keys(filters).length ? filters : undefined,
269
+ }, ac.signal);
270
+ if (!response.ok) {
271
+ writeJson(res, daemonErrorStatus(response.error), {
272
+ error: (_c = response.error) !== null && _c !== void 0 ? _c : "search_failed",
273
+ });
274
+ return;
275
+ }
276
+ writeJson(res, 200, { results: (_d = response.data) !== null && _d !== void 0 ? _d : [] });
277
+ return;
278
+ }
279
+ writeJson(res, 404, { error: "not_found" });
280
+ }
281
+ catch (err) {
282
+ if (ac.signal.aborted)
283
+ return;
284
+ if (err instanceof path_containment_1.PathContainmentError) {
285
+ writeJson(res, 400, { error: "path_outside_project" });
286
+ }
287
+ else if (err instanceof HttpRequestError) {
288
+ writeJson(res, err.status, { error: err.code });
289
+ }
290
+ else {
291
+ writeJson(res, 500, {
292
+ error: err instanceof Error ? err.message : "internal_error",
293
+ });
294
+ }
295
+ }
296
+ finally {
297
+ active.delete(ac);
298
+ }
299
+ }));
300
+ server.setTimeout(60000);
301
+ return {
302
+ server,
303
+ abortActive: () => {
304
+ for (const ac of active)
305
+ ac.abort();
306
+ active.clear();
307
+ },
308
+ };
309
+ }
310
+ function listenOnLoopback(server_1, startPort_1) {
311
+ return __awaiter(this, arguments, void 0, function* (server, startPort, attempts = 10) {
312
+ let port = startPort;
313
+ for (let attempt = 0; attempt < attempts; attempt++, port++) {
314
+ try {
315
+ yield new Promise((resolve, reject) => {
316
+ const onError = (err) => {
317
+ server.off("listening", onListening);
318
+ reject(err);
319
+ };
320
+ const onListening = () => {
321
+ server.off("error", onError);
322
+ resolve();
323
+ };
324
+ server.once("error", onError);
325
+ server.once("listening", onListening);
326
+ server.listen(port, LOOPBACK_HOST);
327
+ });
328
+ const address = server.address();
329
+ return typeof address === "object" && address ? address.port : port;
330
+ }
331
+ catch (err) {
332
+ if (err.code !== "EADDRINUSE")
333
+ throw err;
334
+ }
335
+ }
336
+ throw new Error(`Could not find an open port between ${startPort} and ${startPort + attempts - 1}`);
337
+ });
338
+ }
339
+ function closeServer(server) {
340
+ return __awaiter(this, void 0, void 0, function* () {
341
+ if (!server.listening)
342
+ return;
343
+ yield new Promise((resolve) => {
344
+ const timeout = setTimeout(() => {
345
+ var _a;
346
+ (_a = server.closeAllConnections) === null || _a === void 0 ? void 0 : _a.call(server);
347
+ resolve();
348
+ }, 5000);
349
+ server.close(() => {
350
+ clearTimeout(timeout);
351
+ resolve();
352
+ });
353
+ });
98
354
  });
99
- child.unref();
100
- return child;
101
355
  }
102
356
  exports.serve = new commander_1.Command("serve")
103
- .description("HTTP search server with live file watching")
357
+ .description("Loopback HTTP search adapter backed by the gmax daemon")
104
358
  .option("-p, --port <port>", "Port to listen on", process.env.GMAX_PORT || "4444")
105
359
  .option("-b, --background", "Run in background", false)
106
- .option("--cpu", "Use CPU-only embeddings (skip MLX GPU server)", false)
360
+ .option("--cpu", "Deprecated: configure daemon CPU mode with gmax config", false)
107
361
  .option("--no-idle-timeout", "Disable the 30-minute idle shutdown", false)
108
362
  .action((_args, cmd) => __awaiter(void 0, void 0, void 0, function* () {
109
363
  var _a, _b;
110
364
  const options = cmd.optsWithGlobals();
111
- let port = parseInt(options.port, 10);
112
- const startPort = port;
365
+ const port = Number.parseInt(options.port, 10);
113
366
  const projectRoot = (_a = (0, project_root_1.findProjectRoot)(process.cwd())) !== null && _a !== void 0 ? _a : process.cwd();
114
- // Check if already running
367
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
368
+ console.error(`Invalid port: ${options.port}`);
369
+ process.exitCode = 1;
370
+ return;
371
+ }
372
+ const blockedRootError = serveRootError(projectRoot);
373
+ if (blockedRootError) {
374
+ console.error(blockedRootError);
375
+ process.exitCode = 1;
376
+ return;
377
+ }
378
+ if (options.cpu) {
379
+ console.error("--cpu is now configured on the daemon. Run `gmax config --embed-mode cpu`, then start serve again.");
380
+ process.exitCode = 1;
381
+ return;
382
+ }
115
383
  const existing = (0, server_registry_1.getServerForProject)(projectRoot);
116
384
  if (existing && (0, server_registry_1.isProcessRunning)(existing.pid)) {
117
385
  console.log(`Server already running for ${projectRoot} (PID: ${existing.pid}, Port: ${existing.port})`);
@@ -127,325 +395,101 @@ exports.serve = new commander_1.Command("serve")
127
395
  const logFile = path.join(config_1.PATHS.logsDir, `server-${safeName}.log`);
128
396
  const out = (0, log_rotate_1.openRotatedLog)(logFile);
129
397
  const err = (0, log_rotate_1.openRotatedLog)(logFile);
130
- const child = (0, node_child_process_1.spawn)(process.argv[0], [process.argv[1], ...args], {
131
- detached: true,
132
- stdio: ["ignore", out, err],
133
- cwd: process.cwd(),
134
- env: Object.assign(Object.assign({}, process.env), { GMAX_BACKGROUND: "true" }),
135
- });
136
- child.unref();
137
- console.log(`Started background server (PID: ${child.pid})`);
138
- return;
139
- }
140
- const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
141
- const projectName = path.basename(projectRoot);
142
- // Propagate project root to worker processes
143
- process.env.GMAX_PROJECT_ROOT = projectRoot;
144
- // Determine embed mode: --cpu flag overrides, then config, then default
145
- // Default to GPU on Apple Silicon, CPU everywhere else
146
- const isAppleSilicon = process.arch === "arm64" && process.platform === "darwin";
147
- const indexConfig = (0, index_config_1.readIndexConfig)(paths.configPath);
148
- const useGpu = options.cpu
149
- ? false
150
- : ((_b = indexConfig === null || indexConfig === void 0 ? void 0 : indexConfig.embedMode) !== null && _b !== void 0 ? _b : (isAppleSilicon ? "gpu" : "cpu")) === "gpu";
151
- const mlxModel = indexConfig === null || indexConfig === void 0 ? void 0 : indexConfig.mlxModel;
152
- // MLX GPU embed server — started when GPU mode is active.
153
- let mlxChild = null;
154
- if (!useGpu) {
155
- console.log(`[serve:${projectName}] CPU-only mode`);
156
- }
157
- else {
158
- const mlxUp = yield isMlxServerUp();
159
- if (mlxUp) {
160
- console.log(`[serve:${projectName}] MLX GPU server already running`);
398
+ try {
399
+ const child = (0, node_child_process_1.spawn)(process.argv[0], [process.argv[1], ...args], {
400
+ detached: true,
401
+ stdio: ["ignore", out, err],
402
+ cwd: process.cwd(),
403
+ env: Object.assign(Object.assign({}, process.env), { GMAX_BACKGROUND: "true" }),
404
+ });
405
+ const childPid = yield waitForChildSpawn(child);
406
+ child.unref();
407
+ console.log(`Started background server (PID: ${childPid})`);
161
408
  }
162
- else {
163
- mlxChild = startMlxServer(mlxModel);
164
- if (mlxChild) {
165
- console.log(`[serve] Starting MLX GPU embed server (PID: ${mlxChild.pid})${mlxModel ? ` [${mlxModel}]` : ""}`);
166
- let ready = false;
167
- for (let i = 0; i < 30; i++) {
168
- yield new Promise((r) => setTimeout(r, 1000));
169
- if (yield isMlxServerUp()) {
170
- console.log(`[serve:${projectName}] MLX GPU server ready`);
171
- ready = true;
172
- break;
173
- }
174
- }
175
- if (!ready) {
176
- console.error(`[serve:${projectName}] MLX GPU server failed to start. Run with --cpu to use CPU embeddings.`);
177
- process.exitCode = 1;
178
- return;
179
- }
180
- }
181
- else {
182
- console.error(`[serve:${projectName}] MLX server not found. Run with --cpu to use CPU embeddings.`);
183
- process.exitCode = 1;
184
- return;
185
- }
409
+ catch (err) {
410
+ console.error(`Failed to start background server: ${err instanceof Error ? err.message : String(err)}`);
411
+ process.exitCode = 1;
186
412
  }
413
+ finally {
414
+ fs.closeSync(out);
415
+ fs.closeSync(err);
416
+ }
417
+ return;
187
418
  }
419
+ const startupAc = new AbortController();
420
+ let runtime = null;
421
+ let idleTimer = null;
422
+ let registered = false;
423
+ let shutdownPromise = null;
424
+ const shutdown = (code = 0) => {
425
+ if (shutdownPromise)
426
+ return shutdownPromise;
427
+ shutdownPromise = (() => __awaiter(void 0, void 0, void 0, function* () {
428
+ startupAc.abort();
429
+ if (idleTimer)
430
+ clearInterval(idleTimer);
431
+ runtime === null || runtime === void 0 ? void 0 : runtime.abortActive();
432
+ if (runtime)
433
+ yield closeServer(runtime.server);
434
+ if (registered)
435
+ (0, server_registry_1.unregisterServer)(process.pid);
436
+ yield (0, exit_1.gracefulExit)(code);
437
+ }))();
438
+ return shutdownPromise;
439
+ };
440
+ process.once("SIGINT", () => void shutdown());
441
+ process.once("SIGTERM", () => void shutdown());
188
442
  try {
189
- yield (0, setup_helpers_1.ensureSetup)();
190
- yield (0, grammar_loader_1.ensureGrammars)(console.log, { silent: true });
191
- // Initial sync is self-contained (creates+closes its own VectorDB+MetaCache).
192
- if (!process.env.GMAX_BACKGROUND) {
193
- const { spinner, onProgress } = (0, sync_helpers_1.createIndexingSpinner)(projectRoot, "Indexing before starting server...");
194
- try {
195
- yield (0, syncer_1.initialSync)({ projectRoot, onProgress });
196
- spinner.succeed("Initial index ready. Starting server...");
197
- }
198
- catch (e) {
199
- spinner.fail("Indexing failed");
200
- throw e;
201
- }
443
+ if (!(yield (0, daemon_client_1.ensureDaemonRunning)())) {
444
+ throw new Error("Could not start the gmax daemon");
202
445
  }
203
- else {
204
- yield (0, syncer_1.initialSync)({ projectRoot });
446
+ const ensured = yield (0, daemon_client_1.sendStreamingCommand)({ cmd: "ensure-project", root: projectRoot }, () => { }, { signal: startupAc.signal });
447
+ if (!ensured.ok) {
448
+ throw new Error(String((_b = ensured.error) !== null && _b !== void 0 ? _b : "project setup failed"));
205
449
  }
206
- // Open long-lived resources for serving + watching.
207
- const vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
208
- const metaCache = new meta_cache_1.MetaCache(paths.lmdbPath);
209
- const searcher = new searcher_1.Searcher(vectorDb);
210
- // Start live file watcher
211
- let fileWatcher = yield (0, watcher_1.startWatcher)({
212
- projectRoot,
213
- vectorDb,
214
- metaCache,
215
- dataDir: paths.dataDir,
216
- onReindex: (files, durationMs) => {
217
- console.log(`[watch:${projectName}] Reindexed ${files} file${files !== 1 ? "s" : ""} (${(durationMs / 1000).toFixed(1)}s)`);
450
+ let lastActivity = Date.now();
451
+ runtime = createServeHttpServer(projectRoot, {
452
+ onActivity: () => {
453
+ lastActivity = Date.now();
218
454
  },
455
+ search: (input, signal) => (0, daemon_client_1.sendDaemonCommand)({
456
+ cmd: "search",
457
+ projectRoot,
458
+ query: input.query,
459
+ limit: input.limit,
460
+ pathPrefix: input.pathPrefix,
461
+ filters: input.filters,
462
+ rerank: true,
463
+ }, { timeoutMs: 65000, signal }),
464
+ stats: (signal) => (0, daemon_client_1.sendDaemonCommand)({ cmd: "project-stats", root: projectRoot }, { timeoutMs: 10000, signal }),
219
465
  });
220
- console.log(`[serve:${projectName}] File watcher active`);
221
- // Idle timeout: shut down if no searches for 30 minutes
222
- // Disabled when started by MCP server (--no-idle-timeout)
223
- let lastActivity = Date.now();
466
+ const actualPort = yield listenOnLoopback(runtime.server, port);
467
+ (0, server_registry_1.registerServer)({
468
+ pid: process.pid,
469
+ port: actualPort,
470
+ projectRoot,
471
+ startTime: Date.now(),
472
+ });
473
+ registered = true;
224
474
  if (options.idleTimeout) {
225
- const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
226
- const idleCheck = setInterval(() => {
227
- if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) {
228
- console.log(`[serve:${projectName}] Idle timeout reached, shutting down.`);
229
- clearInterval(idleCheck);
230
- process.kill(process.pid, "SIGTERM");
475
+ idleTimer = setInterval(() => {
476
+ if (Date.now() - lastActivity > 30 * 60 * 1000) {
477
+ void shutdown();
231
478
  }
232
479
  }, 60000);
233
- idleCheck.unref();
480
+ idleTimer.unref();
234
481
  }
235
- const server = http.createServer((req, res) => __awaiter(void 0, void 0, void 0, function* () {
236
- var _a, _b, _c, _d, _e;
237
- try {
238
- if (req.method === "GET" && req.url === "/health") {
239
- lastActivity = Date.now();
240
- res.statusCode = 200;
241
- res.setHeader("Content-Type", "application/json");
242
- res.end(JSON.stringify({ status: "ok" }));
243
- return;
244
- }
245
- if (req.method === "GET" && req.url === "/stats") {
246
- try {
247
- const dbStats = yield vectorDb.getStats();
248
- const cfg = (0, index_config_1.readIndexConfig)(paths.configPath);
249
- const stats = {
250
- files: dbStats.chunks > 0
251
- ? yield vectorDb.getDistinctFileCount()
252
- : 0,
253
- chunks: dbStats.chunks,
254
- totalBytes: dbStats.totalBytes,
255
- vectorDim: (_a = cfg === null || cfg === void 0 ? void 0 : cfg.vectorDim) !== null && _a !== void 0 ? _a : null,
256
- embedMode: (_b = cfg === null || cfg === void 0 ? void 0 : cfg.embedMode) !== null && _b !== void 0 ? _b : (isAppleSilicon ? "gpu" : "cpu"),
257
- model: (_c = cfg === null || cfg === void 0 ? void 0 : cfg.embedModel) !== null && _c !== void 0 ? _c : null,
258
- mlxModel: (_d = cfg === null || cfg === void 0 ? void 0 : cfg.mlxModel) !== null && _d !== void 0 ? _d : null,
259
- indexedAt: (_e = cfg === null || cfg === void 0 ? void 0 : cfg.indexedAt) !== null && _e !== void 0 ? _e : null,
260
- watching: fileWatcher !== null,
261
- };
262
- res.statusCode = 200;
263
- res.setHeader("Content-Type", "application/json");
264
- res.end(JSON.stringify(stats));
265
- }
266
- catch (err) {
267
- res.statusCode = 500;
268
- res.setHeader("Content-Type", "application/json");
269
- res.end(JSON.stringify({
270
- error: (err === null || err === void 0 ? void 0 : err.message) || "stats_failed",
271
- }));
272
- }
273
- return;
274
- }
275
- if (req.method === "POST" && req.url === "/search") {
276
- lastActivity = Date.now();
277
- const chunks = [];
278
- let totalSize = 0;
279
- let aborted = false;
280
- req.on("data", (chunk) => {
281
- if (aborted)
282
- return;
283
- totalSize += chunk.length;
284
- if (totalSize > 1000000) {
285
- aborted = true;
286
- res.statusCode = 413;
287
- res.setHeader("Content-Type", "application/json");
288
- res.end(JSON.stringify({ error: "payload_too_large" }));
289
- req.destroy();
290
- return;
291
- }
292
- chunks.push(chunk);
293
- });
294
- req.on("end", () => __awaiter(void 0, void 0, void 0, function* () {
295
- if (aborted)
296
- return;
297
- try {
298
- const body = chunks.length
299
- ? JSON.parse(Buffer.concat(chunks).toString("utf-8"))
300
- : {};
301
- const query = typeof body.query === "string" ? body.query : "";
302
- const limit = typeof body.limit === "number" ? body.limit : 10;
303
- // Use absolute path prefix for search filtering
304
- let searchPath = `${projectRoot}/`;
305
- if (typeof body.path === "string") {
306
- const resolvedPath = path.resolve(projectRoot, body.path);
307
- searchPath = resolvedPath.endsWith("/")
308
- ? resolvedPath
309
- : `${resolvedPath}/`;
310
- }
311
- // Add AbortController for cancellation
312
- const ac = new AbortController();
313
- req.on("close", () => {
314
- if (req.complete)
315
- return;
316
- ac.abort();
317
- });
318
- res.on("close", () => {
319
- if (res.writableFinished)
320
- return;
321
- ac.abort();
322
- });
323
- const result = yield searcher.search(query, limit, { rerank: true }, undefined, searchPath, undefined, // intent
324
- ac.signal);
325
- if (ac.signal.aborted) {
326
- // Request was cancelled, don't write response if possible
327
- // (Though usually 'close' means the socket is gone anyway)
328
- return;
329
- }
330
- res.statusCode = 200;
331
- res.setHeader("Content-Type", "application/json");
332
- res.end(JSON.stringify({ results: result.data }));
333
- }
334
- catch (err) {
335
- if (err instanceof Error && err.name === "AbortError") {
336
- // Request cancelled
337
- return;
338
- }
339
- res.statusCode = 500;
340
- res.setHeader("Content-Type", "application/json");
341
- res.end(JSON.stringify({
342
- error: (err === null || err === void 0 ? void 0 : err.message) || "search_failed",
343
- }));
344
- }
345
- }));
346
- req.on("error", (err) => {
347
- console.error(`[serve:${projectName}] request error:`, err);
348
- aborted = true;
349
- });
350
- return;
351
- }
352
- res.statusCode = 404;
353
- res.end();
354
- }
355
- catch (err) {
356
- console.error(`[serve:${projectName}] request handler error:`, err);
357
- if (!res.headersSent) {
358
- res.statusCode = 500;
359
- res.setHeader("Content-Type", "application/json");
360
- res.end(JSON.stringify({ error: "internal_error" }));
361
- }
362
- }
363
- }));
364
- server.on("error", (e) => {
365
- if (e.code === "EADDRINUSE") {
366
- const nextPort = port + 1;
367
- if (nextPort < startPort + 10) {
368
- console.log(`Port ${port} in use, retrying with ${nextPort}...`);
369
- port = nextPort;
370
- server.close(() => {
371
- server.listen(port);
372
- });
373
- return;
374
- }
375
- console.error(`Could not find an open port between ${startPort} and ${startPort + 9}`);
376
- }
377
- console.error(`[serve:${projectName}] server error:`, e);
378
- // Ensure we exit if server fails to start
379
- process.exit(1);
482
+ runtime.server.on("error", (err) => {
483
+ console.error(`[serve:${path.basename(projectRoot)}] server error:`, err);
484
+ void shutdown(1);
380
485
  });
381
- server.setTimeout(60000); // 60s request timeout
382
- server.listen(port, () => {
383
- const address = server.address();
384
- const actualPort = typeof address === "object" && address ? address.port : port;
385
- if (!process.env.GMAX_BACKGROUND) {
386
- console.log(`gmax server listening on http://localhost:${actualPort} (${projectRoot})`);
387
- }
388
- (0, server_registry_1.registerServer)({
389
- pid: process.pid,
390
- port: actualPort,
391
- projectRoot,
392
- startTime: Date.now(),
393
- });
394
- });
395
- const shutdown = () => __awaiter(void 0, void 0, void 0, function* () {
396
- (0, server_registry_1.unregisterServer)(process.pid);
397
- // Stop file watcher first
398
- if (fileWatcher) {
399
- try {
400
- yield fileWatcher.close();
401
- }
402
- catch (_a) { }
403
- fileWatcher = null;
404
- }
405
- // Stop MLX server if we started it
406
- if (mlxChild === null || mlxChild === void 0 ? void 0 : mlxChild.pid) {
407
- try {
408
- process.kill(mlxChild.pid, "SIGTERM");
409
- }
410
- catch (_b) { }
411
- }
412
- // Properly await server close
413
- yield new Promise((resolve, reject) => {
414
- server.close((err) => {
415
- if (err) {
416
- console.error("Error closing server:", err);
417
- reject(err);
418
- }
419
- else {
420
- resolve();
421
- }
422
- });
423
- // Timeout fallback in case close hangs
424
- setTimeout(resolve, 5000);
425
- });
426
- // Clean close of owned resources
427
- try {
428
- yield metaCache.close();
429
- }
430
- catch (e) {
431
- console.error("Error closing meta cache:", e);
432
- }
433
- try {
434
- yield vectorDb.close();
435
- }
436
- catch (e) {
437
- console.error("Error closing vector DB:", e);
438
- }
439
- yield (0, exit_1.gracefulExit)();
440
- });
441
- process.on("SIGINT", shutdown);
442
- process.on("SIGTERM", shutdown);
486
+ if (!process.env.GMAX_BACKGROUND) {
487
+ console.log(`gmax server listening on http://${LOOPBACK_HOST}:${actualPort} (${projectRoot})`);
488
+ }
443
489
  }
444
- catch (error) {
445
- const message = error instanceof Error ? error.message : "Unknown error";
446
- console.error("Serve failed:", message);
447
- process.exitCode = 1;
448
- yield (0, exit_1.gracefulExit)(1);
490
+ catch (err) {
491
+ console.error(`Serve failed: ${err instanceof Error ? err.message : String(err)}`);
492
+ yield shutdown(1);
449
493
  }
450
494
  }));
451
495
  exports.serve
@@ -458,9 +502,9 @@ exports.serve
458
502
  return;
459
503
  }
460
504
  console.log("Running servers:");
461
- servers.forEach((s) => {
462
- console.log(`- PID: ${s.pid} | Port: ${s.port} | Root: ${s.projectRoot}`);
463
- });
505
+ for (const server of servers) {
506
+ console.log(`- PID: ${server.pid} | Port: ${server.port} | Root: ${server.projectRoot}`);
507
+ }
464
508
  });
465
509
  exports.serve
466
510
  .command("stop")
@@ -471,31 +515,29 @@ exports.serve
471
515
  if (options.all) {
472
516
  const servers = (0, server_registry_1.listServers)();
473
517
  let count = 0;
474
- servers.forEach((s) => {
518
+ for (const server of servers) {
475
519
  try {
476
- process.kill(s.pid, "SIGTERM");
520
+ process.kill(server.pid, "SIGTERM");
477
521
  count++;
478
522
  }
479
- catch (e) {
480
- console.error(`Failed to stop PID ${s.pid}:`, e);
523
+ catch (err) {
524
+ console.error(`Failed to stop PID ${server.pid}:`, err);
481
525
  }
482
- });
526
+ }
483
527
  console.log(`Stopped ${count} servers.`);
528
+ return;
484
529
  }
485
- else {
486
- const projectRoot = (_a = (0, project_root_1.findProjectRoot)(process.cwd())) !== null && _a !== void 0 ? _a : process.cwd();
487
- const server = (0, server_registry_1.getServerForProject)(projectRoot);
488
- if (server) {
489
- try {
490
- process.kill(server.pid, "SIGTERM");
491
- console.log(`Stopped server for ${projectRoot} (PID: ${server.pid})`);
492
- }
493
- catch (e) {
494
- console.error(`Failed to stop PID ${server.pid}:`, e);
495
- }
496
- }
497
- else {
498
- console.log(`No server found for ${projectRoot}`);
499
- }
530
+ const projectRoot = (_a = (0, project_root_1.findProjectRoot)(process.cwd())) !== null && _a !== void 0 ? _a : process.cwd();
531
+ const server = (0, server_registry_1.getServerForProject)(projectRoot);
532
+ if (!server) {
533
+ console.log(`No server found for ${projectRoot}`);
534
+ return;
535
+ }
536
+ try {
537
+ process.kill(server.pid, "SIGTERM");
538
+ console.log(`Stopped server for ${projectRoot} (PID: ${server.pid})`);
539
+ }
540
+ catch (err) {
541
+ console.error(`Failed to stop PID ${server.pid}:`, err);
500
542
  }
501
543
  });