grepmax 0.26.50 → 0.26.52

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.50",
3
+ "version": "0.26.52",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -41,10 +41,12 @@
41
41
  "typecheck": "tsc --noEmit",
42
42
  "test:typecheck": "tsc -p tsconfig.tests.json",
43
43
  "prepublishOnly": "pnpm run test:typecheck && pnpm build",
44
- "preversion": "pnpm audit --prod && pnpm test && pnpm typecheck && pnpm run test:typecheck && pnpm run format:check && pnpm build && pnpm run audit:consumer",
44
+ "preversion": "pnpm audit && pnpm run audit:python && pnpm test && pnpm run test:python && pnpm typecheck && pnpm run test:typecheck && pnpm run format:check && pnpm build && pnpm run audit:consumer",
45
45
  "version": "bash scripts/sync-versions.sh && git add -A",
46
46
  "postversion": "bash scripts/postrelease.sh",
47
- "audit:consumer": "node scripts/audit-consumer.cjs"
47
+ "audit:consumer": "node scripts/audit-consumer.cjs",
48
+ "audit:python": "python3 scripts/audit-python.py",
49
+ "test:python": "python3 -m unittest discover -s tests/python"
48
50
  },
49
51
  "keywords": [
50
52
  "grepmax",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.50",
3
+ "version": "0.26.52",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",
@@ -0,0 +1,12 @@
1
+ const { ensureSessionLease, readHookInput } = require("./watch-lease");
2
+
3
+ // Bounded renewal on turns and Bash activity keeps long CLI sessions watched.
4
+ // No detached timer, global watcher or additional MCP process is needed.
5
+ async function main() {
6
+ const input = await readHookInput();
7
+ await ensureSessionLease(input, input.cwd || process.cwd(), {
8
+ allowStart: true,
9
+ });
10
+ }
11
+
12
+ main();
@@ -1,6 +1,5 @@
1
- const { execFileSync } = require("node:child_process");
2
1
  const {
3
- acquireSessionLease,
2
+ ensureSessionLease,
4
3
  readHookInput,
5
4
  registeredRootFor,
6
5
  releaseSessionLeases,
@@ -20,19 +19,7 @@ async function main() {
20
19
  }
21
20
  }
22
21
 
23
- if (!registeredRootFor(newCwd)) return;
24
-
25
- try {
26
- execFileSync("gmax", ["watch", "--daemon", "-b"], {
27
- timeout: 5000,
28
- stdio: "ignore",
29
- });
30
- } catch {
31
- try {
32
- execFileSync("gmax", ["watch", "-b"], { timeout: 5000, stdio: "ignore" });
33
- } catch {}
34
- }
35
- await acquireSessionLease(input, newCwd);
22
+ await ensureSessionLease(input, newCwd, { allowStart: true });
36
23
  }
37
24
 
38
25
  main();
@@ -2,7 +2,7 @@ const fs = require("node:fs");
2
2
  const _path = require("node:path");
3
3
  const { execFileSync } = require("node:child_process");
4
4
  const {
5
- acquireSessionLease,
5
+ ensureSessionLease,
6
6
  readHookInput,
7
7
  registeredRootFor,
8
8
  } = require("./watch-lease");
@@ -70,43 +70,10 @@ function isProjectRegistered(dir) {
70
70
  return registeredRootFor(dir) !== null;
71
71
  }
72
72
 
73
- // A kill switch for the auto-start. Create ~/.gmax/autostart-disabled (or export
74
- // GMAX_NO_AUTOSTART=1) to keep sessions from reviving the daemon — needed when the
75
- // host is quarantined after a kernel-zone incident and the daemon's write volume is
76
- // the thing under investigation. Delete the file to restore normal behavior.
77
- //
78
- // The CLI honors the same switch via src/lib/utils/autostart.ts (PATHS.autostartDisabledFile).
79
- // This copy exists because a hook cannot import from dist — keep the two in sync.
80
- function isAutostartDisabled() {
81
- if (process.env.GMAX_NO_AUTOSTART === "1") return true;
82
- try {
83
- return require("node:fs").existsSync(
84
- _path.join(require("node:os").homedir(), ".gmax", "autostart-disabled"),
85
- );
86
- } catch {
87
- return false;
88
- }
89
- }
90
-
91
73
  async function startWatcher(input) {
92
- if (isAutostartDisabled()) return;
93
74
  const dir = input.cwd || process.cwd();
94
75
  if (!isProjectRegistered(dir)) return;
95
- try {
96
- execFileSync("gmax", ["watch", "--daemon", "-b"], {
97
- timeout: 5000,
98
- stdio: "ignore",
99
- });
100
- } catch {
101
- // Fallback to per-project mode (older gmax without --daemon)
102
- try {
103
- execFileSync("gmax", ["watch", "-b"], { timeout: 5000, stdio: "ignore" });
104
- } catch {
105
- // Watcher may already be running or gmax not in PATH — ignore
106
- }
107
- }
108
- // The daemon only watches projects a session holds a lease on.
109
- await acquireSessionLease(input, dir);
76
+ await ensureSessionLease(input, dir, { allowStart: true });
110
77
  }
111
78
 
112
79
  async function main() {
@@ -1,10 +1,9 @@
1
1
  // Session watch leases for the hooks.
2
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.
3
+ // Claude's CLI integration takes a lease at startup, renews it during activity,
4
+ // and releases it at SessionEnd. Other MCP clients own separate leases. A hook
5
+ // cannot import from dist, so this speaks the one-line Unix socket protocol.
6
+ const { spawn } = require("node:child_process");
8
7
  const fs = require("node:fs");
9
8
  const net = require("node:net");
10
9
  const os = require("node:os");
@@ -13,10 +12,18 @@ const path = require("node:path");
13
12
  const GMAX_DIR = path.join(os.homedir(), ".gmax");
14
13
  const SOCKET = path.join(GMAX_DIR, "daemon.sock");
15
14
 
16
- // Long enough to cover a session without an MCP server; SessionEnd releases it
17
- // early, and the daemon caps any lease at 12h.
15
+ // Activity refreshes this TTL; idle sessions do not watch forever.
18
16
  const SESSION_LEASE_TTL_MS = 4 * 60 * 60 * 1000;
19
17
 
18
+ function isAutostartDisabled() {
19
+ if (process.env.GMAX_NO_AUTOSTART === "1") return true;
20
+ try {
21
+ return fs.existsSync(path.join(GMAX_DIR, "autostart-disabled"));
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
20
27
  function readHookInput(timeoutMs = 1000) {
21
28
  return new Promise((resolve) => {
22
29
  let data = "";
@@ -102,6 +109,7 @@ function sendOnce(cmd, timeoutMs) {
102
109
  socket.on("error", (err) =>
103
110
  finish({
104
111
  ok: false,
112
+ absent: err.code === "ENOENT" || err.code === "ECONNREFUSED",
105
113
  retry: err.code === "ENOENT" || err.code === "ECONNREFUSED",
106
114
  }),
107
115
  );
@@ -143,6 +151,42 @@ async function acquireSessionLease(input, dir) {
143
151
  return resp !== null;
144
152
  }
145
153
 
154
+ /** Reuse a lease-capable daemon; start one only after a definite absent socket. */
155
+ async function ensureSessionLease(input, dir, { allowStart = false } = {}) {
156
+ if (isAutostartDisabled() || !sessionHolder(input) || !registeredRootFor(dir))
157
+ return false;
158
+ const ping = await sendOnce({ cmd: "ping" }, 500);
159
+ if (ping.ok) {
160
+ if (!ping.resp.capabilities?.watchLeases) return false;
161
+ return acquireSessionLease(input, dir);
162
+ }
163
+ // Busy, initializing, denied and timed-out sockets belong to an existing
164
+ // daemon. Never start a per-project writer or replace that process here.
165
+ if (!allowStart || !ping.absent) return false;
166
+ try {
167
+ const child = spawn("gmax", ["watch", "--daemon", "-b"], {
168
+ detached: true,
169
+ stdio: "ignore",
170
+ cwd: dir,
171
+ // Another session can win the race after the absent-socket probe. The
172
+ // launcher must reuse that peer, even if its package version differs.
173
+ env: { ...process.env, GMAX_DAEMON_START_ONLY: "1" },
174
+ });
175
+ await new Promise((resolve, reject) => {
176
+ child.once("spawn", resolve);
177
+ child.once("error", reject);
178
+ });
179
+ child.unref();
180
+ } catch {
181
+ return false;
182
+ }
183
+ // A cold daemon may take longer than a hook's budget to become ready. Later
184
+ // activity retries ownership instead of opening another watcher.
185
+ const ready = await sendDaemonLine({ cmd: "ping" }, 1500);
186
+ if (!ready?.capabilities?.watchLeases) return false;
187
+ return acquireSessionLease(input, dir);
188
+ }
189
+
146
190
  async function releaseSessionLeases(input, dir) {
147
191
  const holder = sessionHolder(input);
148
192
  const root = registeredRootFor(dir);
@@ -157,6 +201,8 @@ async function releaseSessionLeases(input, dir) {
157
201
 
158
202
  module.exports = {
159
203
  acquireSessionLease,
204
+ ensureSessionLease,
205
+ isAutostartDisabled,
160
206
  readHookInput,
161
207
  registeredRootFor,
162
208
  releaseSessionLeases,
@@ -5,7 +5,7 @@
5
5
  "hooks": [
6
6
  {
7
7
  "type": "command",
8
- "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/start.js",
8
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/start.js\"",
9
9
  "timeout": 10
10
10
  }
11
11
  ]
@@ -16,7 +16,7 @@
16
16
  "hooks": [
17
17
  {
18
18
  "type": "command",
19
- "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/stop.js",
19
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/stop.js\"",
20
20
  "timeout": 10
21
21
  }
22
22
  ]
@@ -27,8 +27,8 @@
27
27
  "hooks": [
28
28
  {
29
29
  "type": "command",
30
- "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/cwd-changed.js",
31
- "timeout": 5
30
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/cwd-changed.js\"",
31
+ "timeout": 10
32
32
  }
33
33
  ]
34
34
  }
@@ -38,7 +38,7 @@
38
38
  "hooks": [
39
39
  {
40
40
  "type": "command",
41
- "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/subagent-start.js",
41
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/subagent-start.js\"",
42
42
  "timeout": 3
43
43
  }
44
44
  ]
@@ -50,11 +50,34 @@
50
50
  "hooks": [
51
51
  {
52
52
  "type": "command",
53
- "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pre-grep.js",
53
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/pre-grep.js\"",
54
54
  "timeout": 2
55
55
  }
56
56
  ]
57
57
  }
58
+ ],
59
+ "UserPromptSubmit": [
60
+ {
61
+ "hooks": [
62
+ {
63
+ "type": "command",
64
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/activity.js\"",
65
+ "timeout": 10
66
+ }
67
+ ]
68
+ }
69
+ ],
70
+ "PostToolUse": [
71
+ {
72
+ "matcher": "Bash",
73
+ "hooks": [
74
+ {
75
+ "type": "command",
76
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/activity.js\"",
77
+ "timeout": 10
78
+ }
79
+ ]
80
+ }
58
81
  ]
59
82
  }
60
83
  }