grepmax 0.17.23 → 0.18.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.
- package/dist/commands/audit.js +10 -2
- package/dist/commands/dead.js +3 -0
- package/dist/commands/doctor.js +103 -10
- package/dist/commands/impact.js +3 -0
- package/dist/commands/peek.js +3 -0
- package/dist/commands/related.js +3 -0
- package/dist/commands/search-output.js +531 -0
- package/dist/commands/search.js +46 -462
- package/dist/commands/similar.js +3 -0
- package/dist/commands/test-find.js +3 -0
- package/dist/commands/trace.js +3 -0
- package/dist/config.js +80 -5
- package/dist/eval-graph-nav.js +374 -0
- package/dist/eval-graph-sanity.js +138 -19
- package/dist/lib/daemon/daemon.js +29 -643
- package/dist/lib/daemon/mlx-server-manager.js +191 -0
- package/dist/lib/daemon/process-manager.js +139 -0
- package/dist/lib/daemon/watcher-manager.js +484 -0
- package/dist/lib/graph/graph-builder.js +30 -8
- package/dist/lib/graph/impact.js +5 -3
- package/dist/lib/index/chunker.js +149 -4
- package/dist/lib/llm/tools.js +58 -16
- package/dist/lib/store/vector-db.js +43 -13
- package/dist/lib/utils/stale-hint.js +164 -0
- package/dist/lib/workers/orchestrator.js +2 -1
- package/package.json +17 -3
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.MlxServerManager = void 0;
|
|
46
|
+
const node_child_process_1 = require("node:child_process");
|
|
47
|
+
const fs = __importStar(require("node:fs"));
|
|
48
|
+
const http = __importStar(require("node:http"));
|
|
49
|
+
const path = __importStar(require("node:path"));
|
|
50
|
+
const config_1 = require("../../config");
|
|
51
|
+
const log_rotate_1 = require("../utils/log-rotate");
|
|
52
|
+
const process_1 = require("../utils/process");
|
|
53
|
+
/**
|
|
54
|
+
* Owns the MLX embed server (port 8100) process lifecycle: spawn, health probe,
|
|
55
|
+
* zombie recovery, and teardown. Fully isolated — touches only the port and the
|
|
56
|
+
* child process, no shared daemon state beyond a shutting-down getter.
|
|
57
|
+
*
|
|
58
|
+
* Extracted from daemon.ts (Phase 2). `__dirname` resolves the bundled
|
|
59
|
+
* mlx-embed-server/server.py relative to this file; it sits in the same
|
|
60
|
+
* dist/lib/daemon directory as daemon.ts did, so resolution is unchanged.
|
|
61
|
+
*/
|
|
62
|
+
class MlxServerManager {
|
|
63
|
+
constructor(deps) {
|
|
64
|
+
this.deps = deps;
|
|
65
|
+
this.mlxChild = null;
|
|
66
|
+
this.mlxRecoveryInFlight = false;
|
|
67
|
+
}
|
|
68
|
+
isMlxServerUp() {
|
|
69
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
70
|
+
const port = parseInt(process.env.MLX_EMBED_PORT || "8100", 10);
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
const req = http.get({ hostname: "127.0.0.1", port, path: "/health", timeout: 2000 }, (res) => { res.resume(); resolve(res.statusCode === 200); });
|
|
73
|
+
req.on("error", () => resolve(false));
|
|
74
|
+
req.on("timeout", () => { req.destroy(); resolve(false); });
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
getPortPid(port) {
|
|
79
|
+
try {
|
|
80
|
+
const out = (0, node_child_process_1.execSync)(`lsof -ti :${port}`, { timeout: 5000 }).toString().trim();
|
|
81
|
+
const pid = parseInt(out.split("\n")[0], 10);
|
|
82
|
+
return Number.isFinite(pid) ? pid : null;
|
|
83
|
+
}
|
|
84
|
+
catch (_a) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
checkMlxHealth() {
|
|
89
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
90
|
+
if (this.deps.getShuttingDown() || this.mlxRecoveryInFlight)
|
|
91
|
+
return;
|
|
92
|
+
if (yield this.isMlxServerUp())
|
|
93
|
+
return;
|
|
94
|
+
const port = parseInt(process.env.MLX_EMBED_PORT || "8100", 10);
|
|
95
|
+
const stalePid = this.getPortPid(port);
|
|
96
|
+
if (!stalePid)
|
|
97
|
+
return; // No process — let the next user-facing path spawn it.
|
|
98
|
+
this.mlxRecoveryInFlight = true;
|
|
99
|
+
try {
|
|
100
|
+
console.log(`[daemon] MLX zombie detected on port ${port} (PID ${stalePid}) — killing and respawning`);
|
|
101
|
+
yield (0, process_1.killProcess)(stalePid);
|
|
102
|
+
yield new Promise((r) => setTimeout(r, 500));
|
|
103
|
+
yield this.ensureMlxServer();
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error(`[daemon] MLX recovery failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
this.mlxRecoveryInFlight = false;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
ensureMlxServer(mlxModel) {
|
|
114
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
115
|
+
if (yield this.isMlxServerUp()) {
|
|
116
|
+
console.log("[daemon] MLX embed server already running");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Kill stale process holding the port (orphaned from a previous daemon)
|
|
120
|
+
const port = parseInt(process.env.MLX_EMBED_PORT || "8100", 10);
|
|
121
|
+
const stalePid = this.getPortPid(port);
|
|
122
|
+
if (stalePid) {
|
|
123
|
+
console.log(`[daemon] Killing stale MLX process on port ${port} (PID: ${stalePid})`);
|
|
124
|
+
yield (0, process_1.killProcess)(stalePid);
|
|
125
|
+
// Brief pause for OS to release the port
|
|
126
|
+
yield new Promise((r) => setTimeout(r, 500));
|
|
127
|
+
}
|
|
128
|
+
// Find mlx-embed-server/server.py relative to the grepmax package
|
|
129
|
+
const candidates = [
|
|
130
|
+
path.resolve(__dirname, "../../../mlx-embed-server"),
|
|
131
|
+
path.resolve(__dirname, "../../mlx-embed-server"),
|
|
132
|
+
];
|
|
133
|
+
const serverDir = candidates.find((d) => fs.existsSync(path.join(d, "server.py")));
|
|
134
|
+
if (!serverDir) {
|
|
135
|
+
console.warn("[daemon] MLX embed server not found — falling back to CPU embeddings");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const logFd = (0, log_rotate_1.openRotatedLog)(path.join(config_1.PATHS.logsDir, "mlx-embed-server.log"));
|
|
139
|
+
const env = Object.assign({}, process.env);
|
|
140
|
+
if (mlxModel)
|
|
141
|
+
env.MLX_EMBED_MODEL = mlxModel;
|
|
142
|
+
this.mlxChild = (0, node_child_process_1.spawn)("uv", ["run", "python", "server.py"], {
|
|
143
|
+
cwd: serverDir,
|
|
144
|
+
detached: true,
|
|
145
|
+
stdio: ["ignore", logFd, logFd],
|
|
146
|
+
env,
|
|
147
|
+
});
|
|
148
|
+
this.mlxChild.unref();
|
|
149
|
+
console.log(`[daemon] Starting MLX embed server (PID: ${this.mlxChild.pid})`);
|
|
150
|
+
// Poll for readiness (up to 30s)
|
|
151
|
+
for (let i = 0; i < 30; i++) {
|
|
152
|
+
yield new Promise((r) => setTimeout(r, 1000));
|
|
153
|
+
if (yield this.isMlxServerUp()) {
|
|
154
|
+
console.log("[daemon] MLX embed server ready");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
console.error("[daemon] MLX embed server failed to start within 30s — falling back to CPU embeddings");
|
|
159
|
+
this.mlxChild = null;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
stopMlxServer() {
|
|
163
|
+
var _a;
|
|
164
|
+
// The spawned process is `uv`, which forks `python` then exits. Killing the
|
|
165
|
+
// recorded PID alone leaves python orphaned (the orphan source for port 8100
|
|
166
|
+
// collisions across daemon restarts). Always also kill whoever owns the port.
|
|
167
|
+
if ((_a = this.mlxChild) === null || _a === void 0 ? void 0 : _a.pid) {
|
|
168
|
+
try {
|
|
169
|
+
process.kill(-this.mlxChild.pid, "SIGTERM");
|
|
170
|
+
}
|
|
171
|
+
catch (_b) {
|
|
172
|
+
try {
|
|
173
|
+
process.kill(this.mlxChild.pid, "SIGTERM");
|
|
174
|
+
}
|
|
175
|
+
catch (_c) { }
|
|
176
|
+
}
|
|
177
|
+
console.log(`[daemon] Stopped MLX embed server (PID: ${this.mlxChild.pid})`);
|
|
178
|
+
this.mlxChild = null;
|
|
179
|
+
}
|
|
180
|
+
const port = parseInt(process.env.MLX_EMBED_PORT || "8100", 10);
|
|
181
|
+
const portOwner = this.getPortPid(port);
|
|
182
|
+
if (portOwner) {
|
|
183
|
+
try {
|
|
184
|
+
process.kill(portOwner, "SIGTERM");
|
|
185
|
+
console.log(`[daemon] Killed orphan MLX on port ${port} (PID: ${portOwner})`);
|
|
186
|
+
}
|
|
187
|
+
catch (_d) { }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
exports.MlxServerManager = MlxServerManager;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.ProcessManager = void 0;
|
|
13
|
+
const node_child_process_1 = require("node:child_process");
|
|
14
|
+
const daemon_client_1 = require("../utils/daemon-client");
|
|
15
|
+
const logger_1 = require("../utils/logger");
|
|
16
|
+
const process_1 = require("../utils/process");
|
|
17
|
+
const pool_1 = require("../workers/pool");
|
|
18
|
+
/**
|
|
19
|
+
* OS-level process hygiene for the daemon: discovering processes by title,
|
|
20
|
+
* sweeping orphaned workers, and killing stale daemons/workers at startup.
|
|
21
|
+
*
|
|
22
|
+
* Extracted from daemon.ts (Phase 2). Holds no daemon state beyond its own
|
|
23
|
+
* orphan-suspicion set and a shutting-down getter — the lowest-coupling of the
|
|
24
|
+
* three managers.
|
|
25
|
+
*/
|
|
26
|
+
class ProcessManager {
|
|
27
|
+
constructor(deps) {
|
|
28
|
+
this.deps = deps;
|
|
29
|
+
// PIDs flagged as orphan workers on the previous sweep. A worker must look
|
|
30
|
+
// orphaned twice in a row before we kill it, so a worker the pool forked
|
|
31
|
+
// between our process snapshot and its array update is never killed by a race.
|
|
32
|
+
this.suspectedOrphanWorkers = new Set();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Kill gmax-worker processes that are children of THIS daemon but the worker
|
|
36
|
+
* pool no longer tracks — strays left behind if a kill ever failed silently.
|
|
37
|
+
* Filters by parent PID so a per-project `gmax watch`'s own workers are never
|
|
38
|
+
* touched. Requires a worker to look orphaned on two consecutive sweeps so a
|
|
39
|
+
* just-forked worker can't be killed by a snapshot race.
|
|
40
|
+
*/
|
|
41
|
+
sweepOrphanWorkers() {
|
|
42
|
+
if (this.deps.getShuttingDown() || !(0, pool_1.isWorkerPoolInitialized)())
|
|
43
|
+
return;
|
|
44
|
+
const tracked = new Set((0, pool_1.getWorkerPool)().getWorkerPids());
|
|
45
|
+
const workerPids = new Set(this.findProcessesByTitle("gmax-worker"));
|
|
46
|
+
const ourChildren = this.findChildPids();
|
|
47
|
+
const orphans = ourChildren.filter((pid) => workerPids.has(pid) && !tracked.has(pid));
|
|
48
|
+
const confirmed = orphans.filter((pid) => this.suspectedOrphanWorkers.has(pid));
|
|
49
|
+
this.suspectedOrphanWorkers = new Set(orphans);
|
|
50
|
+
for (const pid of confirmed) {
|
|
51
|
+
console.log(`[daemon] Killing orphan worker PID:${pid} (untracked by pool)`);
|
|
52
|
+
try {
|
|
53
|
+
process.kill(pid, "SIGKILL");
|
|
54
|
+
}
|
|
55
|
+
catch (_a) { }
|
|
56
|
+
this.suspectedOrphanWorkers.delete(pid);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Child PIDs of this process (workers, MLX, llama-server). */
|
|
60
|
+
findChildPids() {
|
|
61
|
+
try {
|
|
62
|
+
const out = (0, node_child_process_1.execSync)(`pgrep -P ${process.pid}`, {
|
|
63
|
+
timeout: 5000,
|
|
64
|
+
encoding: "utf-8",
|
|
65
|
+
}).trim();
|
|
66
|
+
if (!out)
|
|
67
|
+
return [];
|
|
68
|
+
return out
|
|
69
|
+
.split("\n")
|
|
70
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
71
|
+
.filter((n) => Number.isFinite(n) && n > 0);
|
|
72
|
+
}
|
|
73
|
+
catch (_a) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Find and kill all stale gmax-daemon and gmax-worker processes.
|
|
79
|
+
* Uses pgrep to scan by process title rather than relying solely on
|
|
80
|
+
* the PID file, which becomes stale when a daemon is orphaned through
|
|
81
|
+
* the lock-compromise path.
|
|
82
|
+
*/
|
|
83
|
+
killStaleProcesses() {
|
|
84
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
85
|
+
// 1. Check for other daemon processes
|
|
86
|
+
const daemonPids = this.findProcessesByTitle("gmax-daemon")
|
|
87
|
+
.filter((pid) => pid !== process.pid);
|
|
88
|
+
const workerPids = this.findProcessesByTitle("gmax-worker");
|
|
89
|
+
if (daemonPids.length === 0 && workerPids.length === 0) {
|
|
90
|
+
(0, logger_1.log)("daemon", "No stale processes found");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const pid of daemonPids) {
|
|
94
|
+
(0, logger_1.log)("daemon", `found daemon PID:${pid}, checking liveness...`);
|
|
95
|
+
// A busy daemon (mid-index, compaction, big LMDB write) can block the
|
|
96
|
+
// event loop long enough to miss a ping. Two independent liveness
|
|
97
|
+
// probes — if either says "alive", defer to the running peer instead
|
|
98
|
+
// of killing its workers mid-flight.
|
|
99
|
+
// 1. daemon.lock mtime (refreshed by heartbeat every 60s)
|
|
100
|
+
// 2. socket ping with a generous 10s timeout
|
|
101
|
+
const heartbeatFresh = (0, daemon_client_1.isDaemonHeartbeatFresh)();
|
|
102
|
+
const responsive = yield (0, daemon_client_1.isDaemonRunning)({ timeoutMs: 10000 });
|
|
103
|
+
if (heartbeatFresh || responsive) {
|
|
104
|
+
(0, logger_1.log)("daemon", `existing daemon PID:${pid} is alive (heartbeat=${heartbeatFresh} ping=${responsive}) — exiting`);
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
(0, logger_1.log)("daemon", `stale daemon PID:${pid} unresponsive and heartbeat stale — killing`);
|
|
108
|
+
yield (0, process_1.killProcess)(pid);
|
|
109
|
+
(0, logger_1.log)("daemon", `killed stale daemon PID:${pid}`);
|
|
110
|
+
}
|
|
111
|
+
// 2. Kill orphaned workers from previous daemon instances.
|
|
112
|
+
// Safe because this runs before the new daemon's worker pool is initialized.
|
|
113
|
+
for (const pid of workerPids) {
|
|
114
|
+
(0, logger_1.log)("daemon", `killing orphaned worker PID:${pid}`);
|
|
115
|
+
yield (0, process_1.killProcess)(pid);
|
|
116
|
+
}
|
|
117
|
+
(0, logger_1.log)("daemon", `Cleaned up ${daemonPids.length} stale daemon(s), ${workerPids.length} orphaned worker(s)`);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
findProcessesByTitle(title) {
|
|
121
|
+
try {
|
|
122
|
+
const out = (0, node_child_process_1.execSync)(`pgrep -x "${title}"`, {
|
|
123
|
+
timeout: 5000,
|
|
124
|
+
encoding: "utf-8",
|
|
125
|
+
}).trim();
|
|
126
|
+
if (!out)
|
|
127
|
+
return [];
|
|
128
|
+
return out
|
|
129
|
+
.split("\n")
|
|
130
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
131
|
+
.filter((n) => Number.isFinite(n) && n > 0);
|
|
132
|
+
}
|
|
133
|
+
catch (_a) {
|
|
134
|
+
// pgrep exits 1 when no processes match — not an error
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
exports.ProcessManager = ProcessManager;
|