grepmax 0.26.28 → 0.26.32
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +4 -2
- package/dist/commands/mcp.js +17 -1
- package/dist/commands/watch.js +1 -1
- package/dist/config.js +8 -2
- package/dist/lib/analysis/surprising-connections.js +135 -15
- package/dist/lib/daemon/daemon.js +131 -13
- package/dist/lib/daemon/graph-handler.js +12 -6
- package/dist/lib/daemon/ipc-handler.js +18 -3
- package/dist/lib/daemon/read-verbs.js +52 -1
- package/dist/lib/daemon/search-handler.js +7 -4
- package/dist/lib/daemon/watch-leases.js +179 -0
- package/dist/lib/index/batch-processor.js +7 -1
- package/dist/lib/index/watcher.js +4 -0
- package/dist/lib/search/pagerank.js +80 -19
- package/dist/lib/store/vector-db.js +35 -1
- package/dist/lib/utils/async-semaphore.js +141 -0
- package/dist/lib/utils/process-footprint.js +51 -0
- package/dist/lib/utils/watcher-launcher.js +36 -19
- package/dist/lib/workers/pool.js +72 -2
- package/package.json +12 -13
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
- package/plugins/grepmax/hooks/cwd-changed.js +19 -36
- package/plugins/grepmax/hooks/start.js +15 -19
- package/plugins/grepmax/hooks/stop.js +13 -4
- package/plugins/grepmax/hooks/watch-lease.js +163 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Session watch leases for the hooks.
|
|
2
|
+
//
|
|
3
|
+
// The daemon only watches projects that some session holds a lease on (see
|
|
4
|
+
// src/lib/daemon/watch-leases.ts). The MCP server holds the long-lived one; the
|
|
5
|
+
// hooks take a session lease so a project is watched from the first moment of a
|
|
6
|
+
// session, and drop it at SessionEnd. A hook cannot import from dist, so this
|
|
7
|
+
// speaks the daemon's one-line JSON protocol directly.
|
|
8
|
+
const fs = require("node:fs");
|
|
9
|
+
const net = require("node:net");
|
|
10
|
+
const os = require("node:os");
|
|
11
|
+
const path = require("node:path");
|
|
12
|
+
|
|
13
|
+
const GMAX_DIR = path.join(os.homedir(), ".gmax");
|
|
14
|
+
const SOCKET = path.join(GMAX_DIR, "daemon.sock");
|
|
15
|
+
|
|
16
|
+
// Long enough to cover a session without an MCP server; SessionEnd releases it
|
|
17
|
+
// early, and the daemon caps any lease at 12h.
|
|
18
|
+
const SESSION_LEASE_TTL_MS = 4 * 60 * 60 * 1000;
|
|
19
|
+
|
|
20
|
+
function readHookInput(timeoutMs = 1000) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
let data = "";
|
|
23
|
+
const done = () => {
|
|
24
|
+
// An open stdin would otherwise keep the hook alive until its timeout.
|
|
25
|
+
process.stdin.destroy();
|
|
26
|
+
try {
|
|
27
|
+
resolve(data ? JSON.parse(data) : {});
|
|
28
|
+
} catch {
|
|
29
|
+
resolve({});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const timer = setTimeout(done, timeoutMs);
|
|
33
|
+
process.stdin.setEncoding("utf-8");
|
|
34
|
+
process.stdin.on("data", (chunk) => {
|
|
35
|
+
data += chunk;
|
|
36
|
+
});
|
|
37
|
+
process.stdin.on("end", () => {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
done();
|
|
40
|
+
});
|
|
41
|
+
process.stdin.on("error", () => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
done();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isWithin(root, dir) {
|
|
49
|
+
const rel = path.relative(root, dir);
|
|
50
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The most specific registered project containing `dir`, or null. */
|
|
54
|
+
function registeredRootFor(dir) {
|
|
55
|
+
try {
|
|
56
|
+
const projects = JSON.parse(
|
|
57
|
+
fs.readFileSync(path.join(GMAX_DIR, "projects.json"), "utf-8"),
|
|
58
|
+
);
|
|
59
|
+
const resolved = path.resolve(dir);
|
|
60
|
+
const matches = projects
|
|
61
|
+
.filter((p) => p && typeof p.root === "string")
|
|
62
|
+
.filter((p) => isWithin(path.resolve(p.root), resolved))
|
|
63
|
+
.sort((a, b) => b.root.length - a.root.length);
|
|
64
|
+
return matches.length > 0 ? matches[0].root : null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sendOnce(cmd, timeoutMs) {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
const socket = net.connect(SOCKET);
|
|
73
|
+
let settled = false;
|
|
74
|
+
const finish = (result) => {
|
|
75
|
+
if (settled) return;
|
|
76
|
+
settled = true;
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
socket.destroy();
|
|
79
|
+
resolve(result);
|
|
80
|
+
};
|
|
81
|
+
const timer = setTimeout(
|
|
82
|
+
() => finish({ ok: false, retry: false }),
|
|
83
|
+
timeoutMs,
|
|
84
|
+
);
|
|
85
|
+
let buf = "";
|
|
86
|
+
socket.on("connect", () => socket.write(`${JSON.stringify(cmd)}\n`));
|
|
87
|
+
socket.on("data", (chunk) => {
|
|
88
|
+
buf += chunk;
|
|
89
|
+
const newline = buf.indexOf("\n");
|
|
90
|
+
if (newline === -1) return;
|
|
91
|
+
let resp = {};
|
|
92
|
+
try {
|
|
93
|
+
resp = JSON.parse(buf.slice(0, newline));
|
|
94
|
+
} catch {}
|
|
95
|
+
finish({
|
|
96
|
+
ok: resp.ok === true,
|
|
97
|
+
resp,
|
|
98
|
+
// A daemon the hook just spawned answers before its stores are open.
|
|
99
|
+
retry: resp.error === "daemon initializing",
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
socket.on("error", (err) =>
|
|
103
|
+
finish({
|
|
104
|
+
ok: false,
|
|
105
|
+
retry: err.code === "ENOENT" || err.code === "ECONNREFUSED",
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Send one command, retrying while the daemon is still coming up (the hook may
|
|
113
|
+
* have just spawned it). Never throws — a hook must not fail the session.
|
|
114
|
+
*/
|
|
115
|
+
async function sendDaemonLine(cmd, deadlineMs = 3000) {
|
|
116
|
+
const deadline = Date.now() + deadlineMs;
|
|
117
|
+
for (;;) {
|
|
118
|
+
const remaining = deadline - Date.now();
|
|
119
|
+
if (remaining <= 0) return null;
|
|
120
|
+
const result = await sendOnce(cmd, Math.min(remaining, 2000));
|
|
121
|
+
if (result.ok) return result.resp;
|
|
122
|
+
if (!result.retry) return null;
|
|
123
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function sessionHolder(input) {
|
|
128
|
+
const id =
|
|
129
|
+
input && typeof input.session_id === "string" ? input.session_id : "";
|
|
130
|
+
return id ? `session:${id}` : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function acquireSessionLease(input, dir) {
|
|
134
|
+
const holder = sessionHolder(input);
|
|
135
|
+
const root = registeredRootFor(dir);
|
|
136
|
+
if (!holder || !root) return false;
|
|
137
|
+
const resp = await sendDaemonLine({
|
|
138
|
+
cmd: "watch",
|
|
139
|
+
root,
|
|
140
|
+
holder,
|
|
141
|
+
ttlMs: SESSION_LEASE_TTL_MS,
|
|
142
|
+
});
|
|
143
|
+
return resp !== null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function releaseSessionLeases(input, dir) {
|
|
147
|
+
const holder = sessionHolder(input);
|
|
148
|
+
const root = registeredRootFor(dir);
|
|
149
|
+
if (!holder || !root) return false;
|
|
150
|
+
// Only release against a live daemon that understands leases: an older one
|
|
151
|
+
// reads `unwatch` as "stop watching for everyone".
|
|
152
|
+
const ping = await sendDaemonLine({ cmd: "ping" }, 1000);
|
|
153
|
+
if (!ping?.capabilities?.watchLeases) return false;
|
|
154
|
+
const resp = await sendDaemonLine({ cmd: "unwatch", root, holder }, 1000);
|
|
155
|
+
return resp !== null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
acquireSessionLease,
|
|
160
|
+
readHookInput,
|
|
161
|
+
registeredRootFor,
|
|
162
|
+
releaseSessionLeases,
|
|
163
|
+
};
|