grepmax 0.16.5 → 0.16.9

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/README.md CHANGED
@@ -60,8 +60,7 @@ gmax symbols auth # List indexed symbols
60
60
  ### Analysis Commands
61
61
 
62
62
  ```bash
63
- gmax diff main # Changed files vs main
64
- gmax diff main --query "auth changes" # Semantic search within changes
63
+ gmax log src/lib/auth.ts # Git commit history for a path or symbol
65
64
  gmax test handleAuth # Find tests via reverse call graph
66
65
  gmax impact handleAuth # Dependents + affected tests
67
66
  gmax similar handleAuth # Find similar code patterns
@@ -73,7 +72,6 @@ gmax context "auth system" --budget 4000 # Token-budgeted topic summary
73
72
  ```bash
74
73
  gmax project # Languages, structure, key symbols
75
74
  gmax related src/lib/auth.ts # Dependencies + dependents
76
- gmax recent # Recently modified files
77
75
  gmax status # All indexed projects + chunk counts
78
76
  ```
79
77
 
@@ -309,14 +309,28 @@ exports.watch
309
309
  var _a;
310
310
  const { isDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
311
311
  let stoppedDaemon = false;
312
+ let daemonPid;
312
313
  // Try shutting down daemon first
313
314
  if (yield isDaemonRunning()) {
315
+ // Capture PID before IPC shutdown so the --all loop below can skip the
316
+ // daemon's own watcher-store entries (it registers itself under every
317
+ // watched project's PID — racing killProcess against the in-flight
318
+ // graceful shutdown is what produced "did not exit after SIGKILL"
319
+ // storms and the orphaned pid/sock/lock files this code path used to
320
+ // leave behind).
321
+ try {
322
+ const pidRaw = yield fs.promises.readFile(config_1.PATHS.daemonPidFile, "utf-8");
323
+ const parsed = parseInt(pidRaw.trim(), 10);
324
+ if (Number.isFinite(parsed) && parsed > 0)
325
+ daemonPid = parsed;
326
+ }
327
+ catch (_b) { }
314
328
  let parentCmd = "?";
315
329
  try {
316
330
  const { execSync } = yield Promise.resolve().then(() => __importStar(require("node:child_process")));
317
331
  parentCmd = execSync(`ps -o command= -p ${process.ppid}`, { encoding: "utf8" }).trim();
318
332
  }
319
- catch (_b) { }
333
+ catch (_c) { }
320
334
  yield sendDaemonCommand({
321
335
  cmd: "shutdown",
322
336
  reason: "gmax-watch-stop",
@@ -325,20 +339,38 @@ exports.watch
325
339
  from_argv: process.argv.slice(0, 4),
326
340
  from_parent_cmd: parentCmd,
327
341
  });
342
+ // Wait for the daemon to actually exit before we start killing watcher
343
+ // entries — many of those entries share the daemon's PID. Poll for up
344
+ // to 10s; shutdown work can take a few seconds on large indexes.
345
+ if (daemonPid) {
346
+ for (let i = 0; i < 100; i++) {
347
+ if (!(0, watcher_store_1.isProcessRunning)(daemonPid))
348
+ break;
349
+ yield new Promise((r) => setTimeout(r, 100));
350
+ }
351
+ }
328
352
  console.log("Daemon stopped.");
329
353
  stoppedDaemon = true;
330
354
  }
331
355
  if (options.all) {
332
356
  const watchers = (0, watcher_store_1.listWatchers)();
357
+ const seenPids = new Set();
358
+ let projectStops = 0;
333
359
  for (const w of watchers) {
360
+ if (daemonPid && w.pid === daemonPid)
361
+ continue;
362
+ if (seenPids.has(w.pid))
363
+ continue;
364
+ seenPids.add(w.pid);
334
365
  const killed = yield (0, process_1.killProcess)(w.pid);
335
366
  (0, watcher_store_1.unregisterWatcher)(w.pid);
367
+ projectStops++;
336
368
  if (!killed) {
337
369
  console.warn(`Warning: PID ${w.pid} did not exit after SIGKILL`);
338
370
  }
339
371
  }
340
- if (watchers.length > 0) {
341
- console.log(`Stopped ${watchers.length} per-project watcher(s).`);
372
+ if (projectStops > 0) {
373
+ console.log(`Stopped ${projectStops} per-project watcher(s).`);
342
374
  }
343
375
  else if (!stoppedDaemon) {
344
376
  console.log("No running watchers.");
@@ -1310,6 +1310,24 @@ class Daemon {
1310
1310
  return;
1311
1311
  this.shuttingDown = true;
1312
1312
  console.log("[daemon] Shutting down...");
1313
+ // Drop external liveness markers FIRST so the next daemon start isn't
1314
+ // fooled by leftover state if the long cleanup below is interrupted
1315
+ // (uncaught exception, second SIGTERM, OOM kill mid-shutdown). The
1316
+ // fresh-lock check in isDaemonHeartbeatFresh keyed on these — orphans
1317
+ // here used to cause silent no-op spawns for up to 150s.
1318
+ try {
1319
+ fs.unlinkSync(config_1.PATHS.daemonSocket);
1320
+ }
1321
+ catch (_e) { }
1322
+ try {
1323
+ fs.unlinkSync(config_1.PATHS.daemonPidFile);
1324
+ }
1325
+ catch (_f) { }
1326
+ if (this.releaseLock) {
1327
+ const release = this.releaseLock;
1328
+ this.releaseLock = null;
1329
+ release().catch(() => { });
1330
+ }
1313
1331
  if (this.heartbeatInterval)
1314
1332
  clearInterval(this.heartbeatInterval);
1315
1333
  if (this.idleInterval)
@@ -1332,7 +1350,7 @@ class Daemon {
1332
1350
  try {
1333
1351
  yield ((_a = this.llmServer) === null || _a === void 0 ? void 0 : _a.stop());
1334
1352
  }
1335
- catch (_e) { }
1353
+ catch (_g) { }
1336
1354
  // Stop MLX embed server if we started it
1337
1355
  this.stopMlxServer();
1338
1356
  // Destroy worker pool to prevent orphaned child processes
@@ -1340,7 +1358,7 @@ class Daemon {
1340
1358
  try {
1341
1359
  yield (0, pool_1.destroyWorkerPool)();
1342
1360
  }
1343
- catch (_f) { }
1361
+ catch (_h) { }
1344
1362
  }
1345
1363
  // Stop poll intervals + their FSEvents recovery probes
1346
1364
  for (const interval of this.pollIntervals.values()) {
@@ -1356,26 +1374,11 @@ class Daemon {
1356
1374
  try {
1357
1375
  yield sub.unsubscribe();
1358
1376
  }
1359
- catch (_g) { }
1377
+ catch (_j) { }
1360
1378
  }
1361
1379
  this.subscriptions.clear();
1362
- // Close server + socket + PID file + lock
1380
+ // Close server (socket/pid/lock already dropped at the top of shutdown)
1363
1381
  (_b = this.server) === null || _b === void 0 ? void 0 : _b.close();
1364
- try {
1365
- fs.unlinkSync(config_1.PATHS.daemonSocket);
1366
- }
1367
- catch (_h) { }
1368
- try {
1369
- fs.unlinkSync(config_1.PATHS.daemonPidFile);
1370
- }
1371
- catch (_j) { }
1372
- if (this.releaseLock) {
1373
- try {
1374
- yield this.releaseLock();
1375
- }
1376
- catch (_k) { }
1377
- this.releaseLock = null;
1378
- }
1379
1382
  // Unregister all
1380
1383
  for (const root of this.processors.keys()) {
1381
1384
  (0, watcher_store_1.unregisterWatcherByRoot)(root);
@@ -1386,11 +1389,11 @@ class Daemon {
1386
1389
  try {
1387
1390
  yield ((_c = this.metaCache) === null || _c === void 0 ? void 0 : _c.close());
1388
1391
  }
1389
- catch (_l) { }
1392
+ catch (_k) { }
1390
1393
  try {
1391
1394
  yield ((_d = this.vectorDb) === null || _d === void 0 ? void 0 : _d.close());
1392
1395
  }
1393
- catch (_m) { }
1396
+ catch (_l) { }
1394
1397
  console.log("[daemon] Shutdown complete");
1395
1398
  });
1396
1399
  }
@@ -122,13 +122,30 @@ function isDaemonRunning(opts) {
122
122
  * Lock-file-based liveness probe. A running daemon refreshes daemon.lock's
123
123
  * mtime every 60s via its heartbeat loop; a fresh mtime means the daemon is
124
124
  * alive even if its socket ping times out under load.
125
+ *
126
+ * A fresh mtime alone is not proof of life — a SIGKILL'd (or OOM-killed,
127
+ * panicked, power-lost) daemon leaves the lock file behind with its last
128
+ * heartbeat mtime, fooling this check for up to HEARTBEAT_FRESH_THRESHOLD_MS.
129
+ * Cross-check that the PID file points at an actually-running process.
125
130
  */
126
131
  function isDaemonHeartbeatFresh() {
127
132
  try {
128
133
  const stats = fs.statSync(config_1.PATHS.daemonLockFile);
129
- return Date.now() - stats.mtimeMs < HEARTBEAT_FRESH_THRESHOLD_MS;
134
+ if (Date.now() - stats.mtimeMs >= HEARTBEAT_FRESH_THRESHOLD_MS)
135
+ return false;
136
+ const pidRaw = fs.readFileSync(config_1.PATHS.daemonPidFile, "utf-8").trim();
137
+ const pid = parseInt(pidRaw, 10);
138
+ if (!Number.isFinite(pid) || pid <= 0)
139
+ return false;
140
+ try {
141
+ process.kill(pid, 0);
142
+ return true;
143
+ }
144
+ catch (_a) {
145
+ return false;
146
+ }
130
147
  }
131
- catch (_a) {
148
+ catch (_b) {
132
149
  return false;
133
150
  }
134
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.16.5",
3
+ "version": "0.16.9",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -14,6 +14,27 @@
14
14
  "bin": {
15
15
  "gmax": "dist/index.js"
16
16
  },
17
+ "scripts": {
18
+ "postinstall": "node scripts/postinstall.js",
19
+ "prebuild": "mkdir -p dist",
20
+ "build": "tsc",
21
+ "postbuild": "chmod +x dist/index.js",
22
+ "dev": "npx tsc && node dist/index.js",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "benchmark": "./run-benchmark.sh",
26
+ "benchmark:index": "./run-benchmark.sh $HOME/gmax-benchmarks --index",
27
+ "benchmark:agent": "npx tsx src/bench/benchmark-agent.ts",
28
+ "benchmark:chart": "npx tsx src/bench/generate-benchmark-chart.ts",
29
+ "format": "biome check --write .",
30
+ "format:check": "biome check .",
31
+ "lint": "biome lint .",
32
+ "typecheck": "tsc --noEmit",
33
+ "prepublishOnly": "pnpm build",
34
+ "preversion": "pnpm test && pnpm typecheck",
35
+ "version": "bash scripts/sync-versions.sh && git add -A",
36
+ "postversion": "git push origin main && git push origin v$npm_package_version && gh release create v$npm_package_version --generate-notes --title v$npm_package_version && sleep 5 && gh run watch $(gh run list --workflow=release.yml --branch v$npm_package_version --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status && sleep 30 && npm cache clean --force && npm install -g grepmax@$npm_package_version"
37
+ },
17
38
  "keywords": [
18
39
  "grepmax",
19
40
  "grep",
@@ -65,25 +86,5 @@
65
86
  "typescript": "^6.0.2",
66
87
  "vite": "^8.0.3",
67
88
  "vitest": "^4.1.2"
68
- },
69
- "scripts": {
70
- "postinstall": "node scripts/postinstall.js",
71
- "prebuild": "mkdir -p dist",
72
- "build": "tsc",
73
- "postbuild": "chmod +x dist/index.js",
74
- "dev": "npx tsc && node dist/index.js",
75
- "test": "vitest run",
76
- "test:watch": "vitest",
77
- "benchmark": "./run-benchmark.sh",
78
- "benchmark:index": "./run-benchmark.sh $HOME/gmax-benchmarks --index",
79
- "benchmark:agent": "npx tsx src/bench/benchmark-agent.ts",
80
- "benchmark:chart": "npx tsx src/bench/generate-benchmark-chart.ts",
81
- "format": "biome check --write .",
82
- "format:check": "biome check .",
83
- "lint": "biome lint .",
84
- "typecheck": "tsc --noEmit",
85
- "preversion": "pnpm test && pnpm typecheck",
86
- "version": "bash scripts/sync-versions.sh && git add -A",
87
- "postversion": "git push origin main && git push origin v$npm_package_version && gh release create v$npm_package_version --generate-notes --title v$npm_package_version && sleep 5 && gh run watch $(gh run list --workflow=release.yml --branch v$npm_package_version --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status && sleep 30 && npm cache clean --force && npm install -g grepmax@$npm_package_version"
88
89
  }
89
- }
90
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.16.5",
3
+ "version": "0.16.9",
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",
@@ -187,8 +187,10 @@ Understand:
187
187
  gmax trace <symbol> call graph (--inbound = callers + snippets)
188
188
  gmax test <symbol> tests for symbol
189
189
  gmax impact <symbol> blast radius
190
+ gmax related <file> file deps + dependents
190
191
 
191
192
  Survey:
193
+ gmax project codebase overview (langs, structure, key symbols)
192
194
  gmax skeleton <file> file structure (file path, NOT a directory)
193
195
  gmax context "topic" --budget 4000 multi-file topic summary
194
196
  gmax log <path-or-symbol> git commits (replaces recent/diff)
@@ -196,7 +198,7 @@ Survey:
196
198
 
197
199
  Scope flags: --root <name|path>, --in <subpath>, --exclude <subpath>.
198
200
  Roles in results: [DEFI] [ORCH] [IMPL] [DOCS].
199
- Recovery: "not added yet" → gmax add; stale results → gmax index.`,
201
+ Recovery: "not added yet" → gmax add; stale gmax index; broken → gmax doctor --fix.`,
200
202
  },
201
203
  };
202
204
  process.stdout.write(JSON.stringify(response));
@@ -40,7 +40,7 @@ If search returns "This project hasn't been added to gmax yet", run `Bash(gmax a
40
40
 
41
41
  ### Search — `gmax "query" --agent`
42
42
 
43
- The `--agent` flag produces compact, token-efficient output for AI agents. It is supported on: `search`, `trace`, `symbols`, `related`, `recent`, `status`, and `project`.
43
+ The `--agent` flag produces compact, token-efficient output for AI agents. It works on most commands — `search`, `peek`, `extract`, `trace`, `test`, `impact`, `similar`, `log`, `related`, `symbols`, `status`, `project`, `context`, `skeleton`, and `doctor`.
44
44
 
45
45
  ```
46
46
  gmax "where do we handle authentication" --agent
@@ -196,12 +196,12 @@ Agentic Q&A: a local LLM autonomously uses gmax tools (search, trace, peek, impa
196
196
  ```
197
197
  gmax status # show all indexed projects
198
198
  gmax status --agent # compact: name\tchunks\tage\tstatus
199
- gmax recent --agent # compact: path\tage
200
199
  gmax related src/file.ts --agent # compact: dep:/rev: path\tcount
201
200
  gmax project --agent # compact: key\tvalue pairs
202
201
  gmax index # reindex current directory
203
202
  gmax config # view/change settings
204
203
  gmax doctor # health check
204
+ gmax doctor --fix # auto-repair (compact, prune, clear stale locks)
205
205
  gmax llm on/off/start/stop/status # manage local LLM server
206
206
  ```
207
207
 
@@ -215,7 +215,7 @@ gmax llm on/off/start/stop/status # manage local LLM server
215
215
  6. **Skeleton** — `Bash(gmax skeleton <path>)` before reading large files, or use `--skeleton` on search
216
216
  7. **Read** — `Read file:line` for specific ranges identified by search/skeleton
217
217
  8. **Trace** — `Bash(gmax trace <symbol>)` for deep call flow (multi-hop)
218
- 9. **Diff** — `Bash(gmax diff [ref])` to see what changed and search within changes
218
+ 9. **Log** — `Bash(gmax log <path-or-symbol>)` for git commit history on a path or symbol
219
219
  10. **Test** — `Bash(gmax test <symbol>)` to find tests covering a symbol before editing
220
220
  11. **Impact** — `Bash(gmax impact <symbol>)` for blast radius before significant changes
221
221
  12. **Similar** — `Bash(gmax similar <symbol>)` to find similar patterns for DRY analysis
@@ -225,7 +225,7 @@ gmax llm on/off/start/stop/status # manage local LLM server
225
225
 
226
226
  ## Tips
227
227
 
228
- - **Use `--agent` for compact output** — supported on search, trace, symbols, related, recent, status, project.
228
+ - **Use `--agent` for compact output** — works on most commands: search, peek, extract, trace, log, test, impact, similar, related, status, project, doctor.
229
229
  - **Be specific.** 5+ words. "auth" returns noise. "where does the server validate JWT tokens" is specific.
230
230
  - **Use `--role ORCHESTRATION`** to skip type definitions and find the actual logic.
231
231
  - **Use `--symbol`** when the query is a function/class name — gets search + trace in one call.