grepmax 0.14.4 → 0.14.6
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.
- package/dist/commands/add.js +20 -0
- package/dist/commands/skeleton.js +1 -0
- package/dist/commands/watch.js +7 -0
- package/dist/index.js +13 -0
- package/dist/lib/daemon/daemon.js +253 -21
- package/dist/lib/daemon/ipc-handler.js +2 -0
- package/dist/lib/graph/impact.js +44 -2
- package/dist/lib/index/batch-processor.js +7 -0
- package/dist/lib/index/chunker.js +62 -3
- package/dist/lib/index/syncer.js +26 -84
- package/dist/lib/search/searcher.js +2 -4
- package/dist/lib/skeleton/skeletonizer.js +17 -1
- package/dist/lib/store/vector-db.js +7 -2
- package/dist/lib/utils/logger.js +33 -2
- package/dist/lib/utils/watcher-store.js +0 -1
- package/dist/lib/workers/embeddings/mlx-client.js +24 -6
- package/dist/lib/workers/orchestrator.js +18 -1
- package/dist/lib/workers/pool.js +82 -16
- package/dist/lib/workers/process-child.js +7 -0
- package/mlx-embed-server/server.py +25 -0
- package/package.json +1 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
package/dist/commands/add.js
CHANGED
|
@@ -92,7 +92,27 @@ Examples:
|
|
|
92
92
|
if (children.length > 0) {
|
|
93
93
|
const names = children.map((c) => c.name).join(", ");
|
|
94
94
|
console.log(`Absorbing ${children.length} sub-project(s): ${names}`);
|
|
95
|
+
const { ensureDaemonRunning: checkDaemon, sendStreamingCommand: sendCmd } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
|
|
96
|
+
const daemonUp = yield checkDaemon();
|
|
95
97
|
for (const child of children) {
|
|
98
|
+
if (daemonUp) {
|
|
99
|
+
// Daemon handles unwatch + vector delete + MetaCache cleanup
|
|
100
|
+
yield sendCmd({ cmd: "remove", root: child.root }, () => { });
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Direct mode: delete vectors and MetaCache entries
|
|
104
|
+
const childPaths = (0, project_root_1.ensureProjectPaths)(child.root);
|
|
105
|
+
const db = new vector_db_1.VectorDB(childPaths.lancedbDir);
|
|
106
|
+
const childPrefix = child.root.endsWith("/") ? child.root : `${child.root}/`;
|
|
107
|
+
yield db.deletePathsWithPrefix(childPrefix);
|
|
108
|
+
const { MetaCache } = yield Promise.resolve().then(() => __importStar(require("../lib/store/meta-cache")));
|
|
109
|
+
const mc = new MetaCache(childPaths.lmdbPath);
|
|
110
|
+
const keys = yield mc.getKeysWithPrefix(childPrefix);
|
|
111
|
+
for (const key of keys)
|
|
112
|
+
mc.delete(key);
|
|
113
|
+
mc.close();
|
|
114
|
+
yield db.close();
|
|
115
|
+
}
|
|
96
116
|
(0, project_registry_1.removeProject)(child.root);
|
|
97
117
|
}
|
|
98
118
|
}
|
|
@@ -126,6 +126,7 @@ exports.skeleton = new commander_1.Command("skeleton")
|
|
|
126
126
|
.option("--json", "Output as JSON", false)
|
|
127
127
|
.option("--no-summary", "Omit call/complexity summary in bodies", false)
|
|
128
128
|
.option("-s, --sync", "Sync index before searching", false)
|
|
129
|
+
.option("--agent", "Compact output for AI agents", false)
|
|
129
130
|
.addHelpText("after", `
|
|
130
131
|
Examples:
|
|
131
132
|
gmax skeleton src/lib/auth.ts Show file structure
|
package/dist/commands/watch.js
CHANGED
|
@@ -114,6 +114,13 @@ exports.watch = new commander_1.Command("watch")
|
|
|
114
114
|
}
|
|
115
115
|
process.on("SIGINT", () => daemon.shutdown().then(() => (0, exit_1.gracefulExit)()));
|
|
116
116
|
process.on("SIGTERM", () => daemon.shutdown().then(() => (0, exit_1.gracefulExit)()));
|
|
117
|
+
process.on("uncaughtException", (err) => {
|
|
118
|
+
console.error("[daemon] uncaughtException:", err);
|
|
119
|
+
daemon.shutdown().then(() => process.exit(1));
|
|
120
|
+
});
|
|
121
|
+
process.on("unhandledRejection", (reason) => {
|
|
122
|
+
console.error("[daemon] unhandledRejection:", reason);
|
|
123
|
+
});
|
|
117
124
|
return;
|
|
118
125
|
}
|
|
119
126
|
// --- Per-project mode ---
|
package/dist/index.js
CHANGED
|
@@ -88,6 +88,19 @@ if (legacyProjectData) {
|
|
|
88
88
|
console.log(" gmax now uses a centralized index at ~/.gmax/lancedb/.");
|
|
89
89
|
console.log(" Run 'gmax index' to re-index into the centralized store.");
|
|
90
90
|
}
|
|
91
|
+
// Wire global --store to per-command --root so cross-project queries work
|
|
92
|
+
commander_1.program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
93
|
+
var _a, _b, _c;
|
|
94
|
+
const globals = (_b = (_a = actionCommand.optsWithGlobals) === null || _a === void 0 ? void 0 : _a.call(actionCommand)) !== null && _b !== void 0 ? _b : {};
|
|
95
|
+
if (globals.store && !((_c = actionCommand.getOptionValue) === null || _c === void 0 ? void 0 : _c.call(actionCommand, "root"))) {
|
|
96
|
+
try {
|
|
97
|
+
actionCommand.setOptionValue("root", globals.store);
|
|
98
|
+
}
|
|
99
|
+
catch (_d) {
|
|
100
|
+
// Command may not have --root; that's fine
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
91
104
|
// Core commands
|
|
92
105
|
commander_1.program.addCommand(search_1.search, { isDefault: true });
|
|
93
106
|
commander_1.program.addCommand(add_1.add);
|
|
@@ -69,6 +69,13 @@ const project_registry_1 = require("../utils/project-registry");
|
|
|
69
69
|
const watcher_store_1 = require("../utils/watcher-store");
|
|
70
70
|
const server_1 = require("../llm/server");
|
|
71
71
|
const ipc_handler_1 = require("./ipc-handler");
|
|
72
|
+
const logger_1 = require("../utils/logger");
|
|
73
|
+
const daemon_client_1 = require("../utils/daemon-client");
|
|
74
|
+
const watcher_store_2 = require("../utils/watcher-store");
|
|
75
|
+
const index_config_1 = require("../index/index-config");
|
|
76
|
+
const log_rotate_1 = require("../utils/log-rotate");
|
|
77
|
+
const node_child_process_1 = require("node:child_process");
|
|
78
|
+
const http = __importStar(require("node:http"));
|
|
72
79
|
const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
73
80
|
const HEARTBEAT_INTERVAL_MS = 60 * 1000;
|
|
74
81
|
class Daemon {
|
|
@@ -85,20 +92,49 @@ class Daemon {
|
|
|
85
92
|
this.idleInterval = null;
|
|
86
93
|
this.shuttingDown = false;
|
|
87
94
|
this.pendingOps = new Set();
|
|
95
|
+
this.watcherFailCount = new Map();
|
|
96
|
+
this.pollIntervals = new Map();
|
|
88
97
|
this.projectLocks = new Map();
|
|
89
98
|
this.llmServer = null;
|
|
99
|
+
this.mlxChild = null;
|
|
90
100
|
}
|
|
91
101
|
start() {
|
|
92
102
|
return __awaiter(this, void 0, void 0, function* () {
|
|
93
103
|
process.title = "gmax-daemon";
|
|
104
|
+
// 0. Singleton enforcement: check PID file for existing daemon
|
|
105
|
+
try {
|
|
106
|
+
const pidStr = fs.readFileSync(config_1.PATHS.daemonPidFile, "utf-8").trim();
|
|
107
|
+
const existingPid = parseInt(pidStr, 10);
|
|
108
|
+
if (existingPid && existingPid !== process.pid && (0, watcher_store_2.isProcessRunning)(existingPid)) {
|
|
109
|
+
(0, logger_1.log)("daemon", `found existing daemon PID:${existingPid}, checking socket...`);
|
|
110
|
+
const responsive = yield (0, daemon_client_1.isDaemonRunning)();
|
|
111
|
+
if (responsive) {
|
|
112
|
+
(0, logger_1.log)("daemon", "existing daemon is responsive — exiting");
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
// Unresponsive but alive — kill it
|
|
116
|
+
(0, logger_1.log)("daemon", `existing daemon PID:${existingPid} unresponsive — killing`);
|
|
117
|
+
yield (0, process_1.killProcess)(existingPid);
|
|
118
|
+
(0, logger_1.log)("daemon", `killed stale daemon PID:${existingPid}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (_a) {
|
|
122
|
+
// No PID file or unreadable — proceed normally
|
|
123
|
+
}
|
|
94
124
|
// 1. Acquire exclusive lock — kernel-enforced, atomic, auto-released on death
|
|
95
125
|
fs.mkdirSync(path.dirname(config_1.PATHS.daemonLockFile), { recursive: true });
|
|
96
126
|
fs.writeFileSync(config_1.PATHS.daemonLockFile, "", { flag: "a" }); // ensure file exists
|
|
127
|
+
(0, logger_1.debug)("daemon", "acquiring lock...");
|
|
97
128
|
try {
|
|
98
129
|
this.releaseLock = yield proper_lockfile_1.default.lock(config_1.PATHS.daemonLockFile, {
|
|
99
130
|
retries: 0,
|
|
100
|
-
stale:
|
|
131
|
+
stale: 120000,
|
|
132
|
+
onCompromised: () => {
|
|
133
|
+
console.error("[daemon] Lock compromised — another daemon took over. Shutting down.");
|
|
134
|
+
this.shutdown();
|
|
135
|
+
},
|
|
101
136
|
});
|
|
137
|
+
(0, logger_1.debug)("daemon", "lock acquired");
|
|
102
138
|
}
|
|
103
139
|
catch (err) {
|
|
104
140
|
if (err.code === "ELOCKED") {
|
|
@@ -120,7 +156,7 @@ class Daemon {
|
|
|
120
156
|
try {
|
|
121
157
|
fs.unlinkSync(config_1.PATHS.daemonSocket);
|
|
122
158
|
}
|
|
123
|
-
catch (
|
|
159
|
+
catch (_b) { }
|
|
124
160
|
// 5. Open shared resources
|
|
125
161
|
try {
|
|
126
162
|
fs.mkdirSync(config_1.PATHS.cacheDir, { recursive: true });
|
|
@@ -137,9 +173,15 @@ class Daemon {
|
|
|
137
173
|
}
|
|
138
174
|
// 6. LLM server manager (constructed, not started — starts on first request)
|
|
139
175
|
this.llmServer = new server_1.LlmServer();
|
|
176
|
+
// 6b. MLX embed server — start if GPU mode is active
|
|
177
|
+
const globalConfig = (0, index_config_1.readGlobalConfig)();
|
|
178
|
+
const isAppleSilicon = process.arch === "arm64" && process.platform === "darwin";
|
|
179
|
+
if (isAppleSilicon && globalConfig.embedMode === "gpu") {
|
|
180
|
+
yield this.ensureMlxServer(globalConfig.mlxModel);
|
|
181
|
+
}
|
|
140
182
|
// 7. Register daemon (only after resources are open)
|
|
141
183
|
(0, watcher_store_1.registerDaemon)(process.pid);
|
|
142
|
-
//
|
|
184
|
+
// 8. Subscribe to all registered projects (skip missing directories)
|
|
143
185
|
const allProjects = (0, project_registry_1.listProjects)();
|
|
144
186
|
const indexed = allProjects.filter((p) => p.status === "indexed");
|
|
145
187
|
for (const p of indexed) {
|
|
@@ -154,26 +196,32 @@ class Daemon {
|
|
|
154
196
|
console.error(`[daemon] Failed to watch ${path.basename(p.root)}:`, err);
|
|
155
197
|
}
|
|
156
198
|
}
|
|
157
|
-
//
|
|
199
|
+
// 8b. Index pending projects in the background
|
|
158
200
|
const pending = allProjects.filter((p) => p.status === "pending" && fs.existsSync(p.root));
|
|
159
201
|
for (const p of pending) {
|
|
160
202
|
this.indexPendingProject(p.root).catch((err) => {
|
|
161
203
|
console.error(`[daemon] Failed to index pending ${path.basename(p.root)}:`, err);
|
|
162
204
|
});
|
|
163
205
|
}
|
|
164
|
-
//
|
|
206
|
+
// 9. Heartbeat + refresh lockfile mtime to prevent stale detection
|
|
165
207
|
this.heartbeatInterval = setInterval(() => {
|
|
166
208
|
(0, watcher_store_1.heartbeat)(process.pid);
|
|
209
|
+
try {
|
|
210
|
+
const now = new Date();
|
|
211
|
+
fs.utimesSync(config_1.PATHS.daemonLockFile, now, now);
|
|
212
|
+
}
|
|
213
|
+
catch (_a) { }
|
|
167
214
|
}, HEARTBEAT_INTERVAL_MS);
|
|
168
|
-
//
|
|
215
|
+
// 10. Idle timeout
|
|
169
216
|
this.idleInterval = setInterval(() => {
|
|
170
217
|
if (Date.now() - this.lastActivity > IDLE_TIMEOUT_MS) {
|
|
171
218
|
console.log("[daemon] Idle for 30 minutes, shutting down");
|
|
172
219
|
this.shutdown();
|
|
173
220
|
}
|
|
174
221
|
}, HEARTBEAT_INTERVAL_MS);
|
|
175
|
-
//
|
|
222
|
+
// 11. Socket server
|
|
176
223
|
this.server = net.createServer((conn) => {
|
|
224
|
+
(0, logger_1.debug)("daemon", "client connected");
|
|
177
225
|
let buf = "";
|
|
178
226
|
conn.on("data", (chunk) => {
|
|
179
227
|
buf += chunk.toString();
|
|
@@ -275,17 +323,7 @@ class Daemon {
|
|
|
275
323
|
});
|
|
276
324
|
this.processors.set(root, processor);
|
|
277
325
|
// Subscribe with @parcel/watcher — native backend, no polling
|
|
278
|
-
|
|
279
|
-
if (err) {
|
|
280
|
-
console.error(`[daemon:${path.basename(root)}] Watcher error:`, err);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
for (const event of events) {
|
|
284
|
-
processor.handleFileEvent(event.type === "delete" ? "unlink" : "change", event.path);
|
|
285
|
-
}
|
|
286
|
-
this.lastActivity = Date.now();
|
|
287
|
-
}, { ignore: watcher_1.WATCHER_IGNORE_GLOBS });
|
|
288
|
-
this.subscriptions.set(root, sub);
|
|
326
|
+
yield this.subscribeWatcher(root, processor);
|
|
289
327
|
(0, watcher_store_1.registerWatcher)({
|
|
290
328
|
pid: process.pid,
|
|
291
329
|
projectRoot: root,
|
|
@@ -301,6 +339,105 @@ class Daemon {
|
|
|
301
339
|
console.log(`[daemon] Watching ${root}`);
|
|
302
340
|
});
|
|
303
341
|
}
|
|
342
|
+
subscribeWatcher(root, processor) {
|
|
343
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
344
|
+
const name = path.basename(root);
|
|
345
|
+
// Unsubscribe existing watcher if any (e.g. during recovery)
|
|
346
|
+
const existingSub = this.subscriptions.get(root);
|
|
347
|
+
if (existingSub) {
|
|
348
|
+
try {
|
|
349
|
+
yield existingSub.unsubscribe();
|
|
350
|
+
}
|
|
351
|
+
catch (_a) { }
|
|
352
|
+
this.subscriptions.delete(root);
|
|
353
|
+
}
|
|
354
|
+
const sub = yield watcher.subscribe(root, (err, events) => {
|
|
355
|
+
if (err) {
|
|
356
|
+
console.error(`[daemon:${name}] Watcher error:`, err);
|
|
357
|
+
this.recoverWatcher(root, processor);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// Watcher is healthy — reset fail counter
|
|
361
|
+
this.watcherFailCount.delete(root);
|
|
362
|
+
for (const event of events) {
|
|
363
|
+
processor.handleFileEvent(event.type === "delete" ? "unlink" : "change", event.path);
|
|
364
|
+
}
|
|
365
|
+
this.lastActivity = Date.now();
|
|
366
|
+
}, { ignore: watcher_1.WATCHER_IGNORE_GLOBS });
|
|
367
|
+
this.subscriptions.set(root, sub);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
recoverWatcher(root, processor) {
|
|
371
|
+
var _a;
|
|
372
|
+
const name = path.basename(root);
|
|
373
|
+
if (this.shuttingDown)
|
|
374
|
+
return;
|
|
375
|
+
// Debounce: avoid multiple overlapping recovery attempts
|
|
376
|
+
const recoveryKey = `recover:${root}`;
|
|
377
|
+
if (this.pendingOps.has(recoveryKey))
|
|
378
|
+
return;
|
|
379
|
+
this.pendingOps.add(recoveryKey);
|
|
380
|
+
const fails = ((_a = this.watcherFailCount.get(root)) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
381
|
+
this.watcherFailCount.set(root, fails);
|
|
382
|
+
const MAX_WATCHER_RETRIES = 3;
|
|
383
|
+
const POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
384
|
+
if (fails > MAX_WATCHER_RETRIES) {
|
|
385
|
+
// FSEvents can't handle this project — degrade to periodic catchup scans
|
|
386
|
+
if (!this.pollIntervals.has(root)) {
|
|
387
|
+
console.error(`[daemon:${name}] FSEvents unreliable after ${fails} failures — switching to poll mode (${POLL_INTERVAL_MS / 60000}min interval)`);
|
|
388
|
+
// Unsubscribe the broken watcher
|
|
389
|
+
const sub = this.subscriptions.get(root);
|
|
390
|
+
if (sub) {
|
|
391
|
+
sub.unsubscribe().catch(() => { });
|
|
392
|
+
this.subscriptions.delete(root);
|
|
393
|
+
}
|
|
394
|
+
// Run an immediate catchup, then schedule periodic ones
|
|
395
|
+
this.catchupScan(root, processor).catch((err) => {
|
|
396
|
+
console.error(`[daemon:${name}] Poll catchup failed:`, err);
|
|
397
|
+
});
|
|
398
|
+
const interval = setInterval(() => {
|
|
399
|
+
if (this.shuttingDown)
|
|
400
|
+
return;
|
|
401
|
+
this.lastActivity = Date.now();
|
|
402
|
+
this.catchupScan(root, processor).catch((err) => {
|
|
403
|
+
console.error(`[daemon:${name}] Poll catchup failed:`, err);
|
|
404
|
+
});
|
|
405
|
+
}, POLL_INTERVAL_MS);
|
|
406
|
+
this.pollIntervals.set(root, interval);
|
|
407
|
+
(0, watcher_store_1.registerWatcher)({
|
|
408
|
+
pid: process.pid,
|
|
409
|
+
projectRoot: root,
|
|
410
|
+
startTime: Date.now(),
|
|
411
|
+
status: "watching",
|
|
412
|
+
lastHeartbeat: Date.now(),
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
this.pendingOps.delete(recoveryKey);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
// Backoff: wait before re-subscribing (3s, 6s, 12s)
|
|
419
|
+
const delayMs = 3000 * Math.pow(2, fails - 1);
|
|
420
|
+
console.error(`[daemon:${name}] Recovering watcher (attempt ${fails}/${MAX_WATCHER_RETRIES}, backoff ${delayMs}ms)...`);
|
|
421
|
+
setTimeout(() => {
|
|
422
|
+
if (this.shuttingDown) {
|
|
423
|
+
this.pendingOps.delete(recoveryKey);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
(() => __awaiter(this, void 0, void 0, function* () {
|
|
427
|
+
try {
|
|
428
|
+
yield this.subscribeWatcher(root, processor);
|
|
429
|
+
yield this.catchupScan(root, processor);
|
|
430
|
+
console.log(`[daemon:${name}] Watcher recovered`);
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
console.error(`[daemon:${name}] Watcher recovery failed:`, err);
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
this.pendingOps.delete(recoveryKey);
|
|
437
|
+
}
|
|
438
|
+
}))();
|
|
439
|
+
}, delayMs);
|
|
440
|
+
}
|
|
304
441
|
catchupScan(root, processor) {
|
|
305
442
|
return __awaiter(this, void 0, void 0, function* () {
|
|
306
443
|
var _a, e_1, _b, _c;
|
|
@@ -311,6 +448,8 @@ class Daemon {
|
|
|
311
448
|
const cachedPaths = yield this.metaCache.getKeysWithPrefix(rootPrefix);
|
|
312
449
|
const seenPaths = new Set();
|
|
313
450
|
let queued = 0;
|
|
451
|
+
let skipped = 0;
|
|
452
|
+
let debugSamples = 0;
|
|
314
453
|
try {
|
|
315
454
|
for (var _d = true, _e = __asyncValues(walk(root, {
|
|
316
455
|
additionalPatterns: ["**/.git/**", "**/.gmax/**"],
|
|
@@ -331,9 +470,30 @@ class Daemon {
|
|
|
331
470
|
continue;
|
|
332
471
|
const cached = this.metaCache.get(absPath);
|
|
333
472
|
if (!isFileCached(cached, stats)) {
|
|
473
|
+
// Fast path: if only mtime changed but size is identical and we have a hash,
|
|
474
|
+
// just verify the hash in-process instead of sending to a worker.
|
|
475
|
+
if (cached && cached.hash && cached.size === stats.size) {
|
|
476
|
+
const { computeBufferHash } = yield Promise.resolve().then(() => __importStar(require("../utils/file-utils")));
|
|
477
|
+
const buf = yield fs.promises.readFile(absPath);
|
|
478
|
+
const hash = computeBufferHash(buf);
|
|
479
|
+
if (hash === cached.hash) {
|
|
480
|
+
// Content unchanged — update mtime in cache and skip worker
|
|
481
|
+
this.metaCache.put(absPath, Object.assign(Object.assign({}, cached), { mtimeMs: stats.mtimeMs }));
|
|
482
|
+
skipped++;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Debug: log first few misses to diagnose re-queue loops
|
|
487
|
+
if (debugSamples < 5) {
|
|
488
|
+
(0, logger_1.debug)("catchup", `miss ${relPath}: cached=${cached ? `mtime=${Math.trunc(cached.mtimeMs)} size=${cached.size}` : "null"} stat=mtime=${Math.trunc(stats.mtimeMs)} size=${stats.size}`);
|
|
489
|
+
debugSamples++;
|
|
490
|
+
}
|
|
334
491
|
processor.handleFileEvent("change", absPath);
|
|
335
492
|
queued++;
|
|
336
493
|
}
|
|
494
|
+
else {
|
|
495
|
+
skipped++;
|
|
496
|
+
}
|
|
337
497
|
}
|
|
338
498
|
catch (_g) { }
|
|
339
499
|
}
|
|
@@ -345,6 +505,7 @@ class Daemon {
|
|
|
345
505
|
}
|
|
346
506
|
finally { if (e_1) throw e_1.error; }
|
|
347
507
|
}
|
|
508
|
+
(0, logger_1.debug)("catchup", `${path.basename(root)}: ${queued} queued, ${skipped} skipped (cached ok), ${seenPaths.size} total`);
|
|
348
509
|
// Purge files deleted while daemon was offline
|
|
349
510
|
let purged = 0;
|
|
350
511
|
for (const cachedPath of cachedPaths) {
|
|
@@ -369,7 +530,9 @@ class Daemon {
|
|
|
369
530
|
var _a;
|
|
370
531
|
if (!this.vectorDb || !this.metaCache)
|
|
371
532
|
return;
|
|
372
|
-
|
|
533
|
+
const name = path.basename(root);
|
|
534
|
+
const start = Date.now();
|
|
535
|
+
(0, logger_1.log)("daemon", `indexPendingProject start: ${name} (${root})`);
|
|
373
536
|
this.vectorDb.pauseMaintenanceLoop();
|
|
374
537
|
try {
|
|
375
538
|
const result = yield (0, syncer_1.initialSync)({
|
|
@@ -383,11 +546,11 @@ class Daemon {
|
|
|
383
546
|
(0, project_registry_1.registerProject)(Object.assign(Object.assign({}, proj), { lastIndexed: new Date().toISOString(), chunkCount: result.indexed, status: "indexed" }));
|
|
384
547
|
}
|
|
385
548
|
yield this.watchProject(root);
|
|
386
|
-
|
|
549
|
+
(0, logger_1.log)("daemon", `indexPendingProject done: ${name} — ${result.total} files, ${result.indexed} chunks, ${Date.now() - start}ms`);
|
|
387
550
|
}
|
|
388
551
|
catch (err) {
|
|
389
552
|
const msg = err instanceof Error ? err.message : String(err);
|
|
390
|
-
console.error(`[daemon] indexPendingProject failed for ${
|
|
553
|
+
console.error(`[daemon] indexPendingProject failed for ${name} after ${Date.now() - start}ms: ${msg}`);
|
|
391
554
|
}
|
|
392
555
|
finally {
|
|
393
556
|
(_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.resumeMaintenanceLoop();
|
|
@@ -683,6 +846,68 @@ class Daemon {
|
|
|
683
846
|
}
|
|
684
847
|
});
|
|
685
848
|
}
|
|
849
|
+
// --- MLX embed server management ---
|
|
850
|
+
isMlxServerUp() {
|
|
851
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
852
|
+
const port = parseInt(process.env.MLX_EMBED_PORT || "8100", 10);
|
|
853
|
+
return new Promise((resolve) => {
|
|
854
|
+
const req = http.get({ hostname: "127.0.0.1", port, path: "/health", timeout: 2000 }, (res) => { res.resume(); resolve(res.statusCode === 200); });
|
|
855
|
+
req.on("error", () => resolve(false));
|
|
856
|
+
req.on("timeout", () => { req.destroy(); resolve(false); });
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
ensureMlxServer(mlxModel) {
|
|
861
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
862
|
+
if (yield this.isMlxServerUp()) {
|
|
863
|
+
console.log("[daemon] MLX embed server already running");
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
// Find mlx-embed-server/server.py relative to the grepmax package
|
|
867
|
+
const candidates = [
|
|
868
|
+
path.resolve(__dirname, "../../../mlx-embed-server"),
|
|
869
|
+
path.resolve(__dirname, "../../mlx-embed-server"),
|
|
870
|
+
];
|
|
871
|
+
const serverDir = candidates.find((d) => fs.existsSync(path.join(d, "server.py")));
|
|
872
|
+
if (!serverDir) {
|
|
873
|
+
console.warn("[daemon] MLX embed server not found — falling back to CPU embeddings");
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const logFd = (0, log_rotate_1.openRotatedLog)(path.join(config_1.PATHS.logsDir, "mlx-embed-server.log"));
|
|
877
|
+
const env = Object.assign({}, process.env);
|
|
878
|
+
if (mlxModel)
|
|
879
|
+
env.MLX_EMBED_MODEL = mlxModel;
|
|
880
|
+
this.mlxChild = (0, node_child_process_1.spawn)("uv", ["run", "python", "server.py"], {
|
|
881
|
+
cwd: serverDir,
|
|
882
|
+
detached: true,
|
|
883
|
+
stdio: ["ignore", logFd, logFd],
|
|
884
|
+
env,
|
|
885
|
+
});
|
|
886
|
+
this.mlxChild.unref();
|
|
887
|
+
console.log(`[daemon] Starting MLX embed server (PID: ${this.mlxChild.pid})`);
|
|
888
|
+
// Poll for readiness (up to 30s)
|
|
889
|
+
for (let i = 0; i < 30; i++) {
|
|
890
|
+
yield new Promise((r) => setTimeout(r, 1000));
|
|
891
|
+
if (yield this.isMlxServerUp()) {
|
|
892
|
+
console.log("[daemon] MLX embed server ready");
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
console.error("[daemon] MLX embed server failed to start within 30s — falling back to CPU embeddings");
|
|
897
|
+
this.mlxChild = null;
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
stopMlxServer() {
|
|
901
|
+
var _a;
|
|
902
|
+
if (!((_a = this.mlxChild) === null || _a === void 0 ? void 0 : _a.pid))
|
|
903
|
+
return;
|
|
904
|
+
try {
|
|
905
|
+
process.kill(this.mlxChild.pid, "SIGTERM");
|
|
906
|
+
console.log(`[daemon] Stopped MLX embed server (PID: ${this.mlxChild.pid})`);
|
|
907
|
+
}
|
|
908
|
+
catch (_b) { }
|
|
909
|
+
this.mlxChild = null;
|
|
910
|
+
}
|
|
686
911
|
shutdown() {
|
|
687
912
|
return __awaiter(this, void 0, void 0, function* () {
|
|
688
913
|
var _a, _b, _c, _d;
|
|
@@ -703,6 +928,13 @@ class Daemon {
|
|
|
703
928
|
yield ((_a = this.llmServer) === null || _a === void 0 ? void 0 : _a.stop());
|
|
704
929
|
}
|
|
705
930
|
catch (_e) { }
|
|
931
|
+
// Stop MLX embed server if we started it
|
|
932
|
+
this.stopMlxServer();
|
|
933
|
+
// Stop poll intervals
|
|
934
|
+
for (const interval of this.pollIntervals.values()) {
|
|
935
|
+
clearInterval(interval);
|
|
936
|
+
}
|
|
937
|
+
this.pollIntervals.clear();
|
|
706
938
|
// Unsubscribe all watchers
|
|
707
939
|
for (const sub of this.subscriptions.values()) {
|
|
708
940
|
try {
|
|
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.writeProgress = writeProgress;
|
|
13
13
|
exports.writeDone = writeDone;
|
|
14
14
|
exports.handleCommand = handleCommand;
|
|
15
|
+
const logger_1 = require("../utils/logger");
|
|
15
16
|
/**
|
|
16
17
|
* Write a streaming progress line to the IPC connection.
|
|
17
18
|
*/
|
|
@@ -38,6 +39,7 @@ function writeDone(conn, data) {
|
|
|
38
39
|
function handleCommand(daemon, cmd, conn) {
|
|
39
40
|
return __awaiter(this, void 0, void 0, function* () {
|
|
40
41
|
try {
|
|
42
|
+
(0, logger_1.debug)("daemon", `ipc cmd=${cmd.cmd}${cmd.root ? ` root=${cmd.root}` : ""}`);
|
|
41
43
|
switch (cmd.cmd) {
|
|
42
44
|
case "ping":
|
|
43
45
|
return { ok: true, pid: process.pid, uptime: daemon.uptime() };
|
package/dist/lib/graph/impact.js
CHANGED
|
@@ -17,8 +17,12 @@ const filter_builder_1 = require("../utils/filter-builder");
|
|
|
17
17
|
const graph_builder_1 = require("./graph-builder");
|
|
18
18
|
const TEST_DIR_RE = /(^|\/)(__tests__|tests?|specs?|benchmark)(\/|$)/i;
|
|
19
19
|
const TEST_FILE_RE = /\.(test|spec)\.[cm]?[jt]sx?$/i;
|
|
20
|
+
// Swift/Kotlin/Java: FooTests.swift, FooTest.kt, FooTest.java, or dirs like AppTests/
|
|
21
|
+
const NATIVE_TEST_DIR_RE = /(^|\/)\w+Tests?(\/|$)/;
|
|
22
|
+
const NATIVE_TEST_FILE_RE = /Tests?\.(swift|kt|java)$/;
|
|
20
23
|
function isTestPath(filePath) {
|
|
21
|
-
return TEST_DIR_RE.test(filePath) || TEST_FILE_RE.test(filePath)
|
|
24
|
+
return TEST_DIR_RE.test(filePath) || TEST_FILE_RE.test(filePath)
|
|
25
|
+
|| NATIVE_TEST_DIR_RE.test(filePath) || NATIVE_TEST_FILE_RE.test(filePath);
|
|
22
26
|
}
|
|
23
27
|
const arrow_1 = require("../utils/arrow");
|
|
24
28
|
/**
|
|
@@ -48,6 +52,42 @@ function resolveTargetSymbols(target, vectorDb, projectRoot) {
|
|
|
48
52
|
return { symbols: [target], resolvedAsFile: false };
|
|
49
53
|
});
|
|
50
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* For a single symbol, expand to include all symbols defined in the same file.
|
|
57
|
+
* This catches cases where tests call methods of a class rather than the class name itself
|
|
58
|
+
* (e.g., Swift tests call `handleNotification()` rather than referencing `DeepLinkRouter`).
|
|
59
|
+
*/
|
|
60
|
+
function expandFileSymbols(symbols, vectorDb, projectRoot) {
|
|
61
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
if (symbols.length !== 1)
|
|
63
|
+
return symbols;
|
|
64
|
+
const table = yield vectorDb.ensureTable();
|
|
65
|
+
const prefix = projectRoot.endsWith("/") ? projectRoot : `${projectRoot}/`;
|
|
66
|
+
// Find the file that defines this symbol
|
|
67
|
+
const defRows = yield table
|
|
68
|
+
.query()
|
|
69
|
+
.select(["path"])
|
|
70
|
+
.where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbols[0])}') AND path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
|
|
71
|
+
.limit(1)
|
|
72
|
+
.toArray();
|
|
73
|
+
if (defRows.length === 0)
|
|
74
|
+
return symbols;
|
|
75
|
+
const filePath = String(defRows[0].path);
|
|
76
|
+
// Get ALL symbols defined in that file
|
|
77
|
+
const fileRows = yield table
|
|
78
|
+
.query()
|
|
79
|
+
.select(["defined_symbols"])
|
|
80
|
+
.where(`path = '${(0, filter_builder_1.escapeSqlString)(filePath)}'`)
|
|
81
|
+
.toArray();
|
|
82
|
+
const expanded = new Set(symbols);
|
|
83
|
+
for (const row of fileRows) {
|
|
84
|
+
for (const s of (0, arrow_1.toArr)(row.defined_symbols)) {
|
|
85
|
+
expanded.add(s);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [...expanded];
|
|
89
|
+
});
|
|
90
|
+
}
|
|
51
91
|
/**
|
|
52
92
|
* Find test files that exercise a set of symbols, using reverse call graph traversal.
|
|
53
93
|
*/
|
|
@@ -55,7 +95,9 @@ function findTests(symbols_1, vectorDb_1, projectRoot_1) {
|
|
|
55
95
|
return __awaiter(this, arguments, void 0, function* (symbols, vectorDb, projectRoot, depth = 1) {
|
|
56
96
|
const graphBuilder = new graph_builder_1.GraphBuilder(vectorDb, projectRoot);
|
|
57
97
|
const testHits = new Map(); // key: file+symbol
|
|
58
|
-
|
|
98
|
+
// Expand single-symbol targets to include all symbols from the same file
|
|
99
|
+
const expanded = yield expandFileSymbols(symbols, vectorDb, projectRoot);
|
|
100
|
+
for (const symbol of expanded) {
|
|
59
101
|
yield walkCallers(symbol, graphBuilder, testHits, 0, depth, new Set());
|
|
60
102
|
}
|
|
61
103
|
return [...testHits.values()].sort((a, b) => a.hops - b.hops || a.file.localeCompare(b.file));
|
|
@@ -51,6 +51,9 @@ const file_utils_1 = require("../utils/file-utils");
|
|
|
51
51
|
const logger_1 = require("../utils/logger");
|
|
52
52
|
const pool_1 = require("../workers/pool");
|
|
53
53
|
const watcher_batch_1 = require("./watcher-batch");
|
|
54
|
+
// Fast path-segment check to reject events that leak through FSEvents overflow.
|
|
55
|
+
// Matches /node_modules/, /.git/, /dist/, /build/, /.next/, etc. anywhere in path.
|
|
56
|
+
const IGNORED_PATH_SEGMENTS_RE = /\/(?:node_modules|\.git|\.next|\.nuxt|__pycache__|coverage|\.gmax)\//;
|
|
54
57
|
const DEBOUNCE_MS = 2000;
|
|
55
58
|
const MAX_RETRIES = 5;
|
|
56
59
|
const MAX_BATCH_SIZE = 50;
|
|
@@ -83,6 +86,10 @@ class ProjectBatchProcessor {
|
|
|
83
86
|
const bn = path.basename(absPath).toLowerCase();
|
|
84
87
|
if (!config_1.INDEXABLE_EXTENSIONS.has(ext) && !config_1.INDEXABLE_EXTENSIONS.has(bn))
|
|
85
88
|
return;
|
|
89
|
+
// Safety net: reject paths with ignored directory segments.
|
|
90
|
+
// FSEvents can leak events during overflow before the watcher drops them.
|
|
91
|
+
if (IGNORED_PATH_SEGMENTS_RE.test(absPath))
|
|
92
|
+
return;
|
|
86
93
|
this.pending.set(absPath, event);
|
|
87
94
|
(_a = this.onActivity) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
88
95
|
this.scheduleBatch();
|