grepmax 0.12.9 → 0.12.12

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
@@ -20,59 +20,61 @@
20
20
  Natural-language search that works like `grep`. Fast, local, and built for coding agents.
21
21
 
22
22
  - **Semantic:** Finds concepts ("where do transactions get created?"), not just strings.
23
- - **Call Graph Tracing:** Map dependencies with `trace` to see who calls what.
23
+ - **Call Graph Tracing:** Map dependencies with `trace`, find tests with `test`, measure blast radius with `impact`.
24
24
  - **Role Detection:** Distinguishes `ORCHESTRATION` (high-level logic) from `DEFINITION` (types/classes).
25
25
  - **Local & Private:** 100% local embeddings via ONNX (CPU) or MLX (Apple Silicon GPU).
26
26
  - **Centralized Index:** One database at `~/.gmax/` — index once, search from anywhere.
27
- - **LLM Summaries:** Optional Qwen3-Coder generates one-line descriptions per code chunk on demand.
28
- - **Agent-Ready:** Pointer mode returns metadata (symbol, role, calls, summary) — no code snippets, ~80% fewer tokens.
27
+ - **Agent-Ready:** `--agent` flag returns compact one-line output ~90% fewer tokens than default.
29
28
 
30
29
  ## Quick Start
31
30
 
32
31
  ```bash
33
32
  npm install -g grepmax # 1. Install
34
- cd my-repo && gmax index # 2. Index (models download automatically)
35
- gmax "where do we handle auth?" # 3. Search
33
+ cd my-repo && gmax add # 2. Add + index
34
+ gmax "where do we handle auth?" --agent # 3. Search
36
35
  ```
37
36
 
38
- That's it. No setup required — gmax auto-detects your platform (GPU on Apple Silicon, CPU elsewhere) and downloads models on first use.
37
+ No setup required — gmax auto-detects your platform (GPU on Apple Silicon, CPU elsewhere) and downloads models on first use.
39
38
 
40
- ### Optional: Interactive Setup
39
+ ### Setup & Config
41
40
 
42
41
  ```bash
43
- gmax setup # Choose model tier + embedding mode interactively
42
+ gmax setup # Interactive wizard (models, embedding mode, plugins)
43
+ gmax config # View current settings
44
+ gmax config --embed-mode gpu # Switch to GPU (Apple Silicon)
45
+ gmax doctor # Health check
46
+ gmax doctor --fix # Auto-repair (compact, prune, remove stale locks)
44
47
  ```
45
48
 
46
- Run this if you want to:
47
- - Switch between **CPU** (ONNX, works everywhere) and **GPU** (MLX, Apple Silicon only, ~3x faster)
48
- - Choose between **small** model (384d, 47M params, fast) and **standard** model (768d, 149M params, better quality)
49
-
50
- ### Quick Config (Non-Interactive)
49
+ ### Core Commands
51
50
 
52
51
  ```bash
53
- gmax config # View current settings
54
- gmax config --embed-mode cpu # Switch to CPU
55
- gmax config --embed-mode gpu # Switch to GPU (Apple Silicon only)
56
- gmax config --model-tier standard # Switch to larger model
52
+ gmax "where do we handle auth?" --agent # Semantic search (compact output)
53
+ gmax extract handleAuth # Full function body with line numbers
54
+ gmax peek handleAuth # Signature + callers + callees
55
+ gmax trace handleAuth -d 2 # Call graph (2-hop)
56
+ gmax skeleton src/lib/search/ # File structure (bodies collapsed)
57
+ gmax symbols auth # List indexed symbols
57
58
  ```
58
59
 
59
- ### Verify Installation
60
+ ### Analysis Commands
60
61
 
61
62
  ```bash
62
- gmax doctor # Check models, index, servers
63
+ gmax diff main # Changed files vs main
64
+ gmax diff main --query "auth changes" # Semantic search within changes
65
+ gmax test handleAuth # Find tests via reverse call graph
66
+ gmax impact handleAuth # Dependents + affected tests
67
+ gmax similar handleAuth # Find similar code patterns
68
+ gmax context "auth system" --budget 4000 # Token-budgeted topic summary
63
69
  ```
64
70
 
65
- ### Core Commands
71
+ ### Project Commands
66
72
 
67
73
  ```bash
68
- gmax "where do we handle auth?" # Semantic search
69
- gmax "VectorDB" --symbol --agent # Search + call graph (compact output)
70
- gmax trace handleAuth -d 2 # Call graph (2-hop)
71
- gmax skeleton src/lib/search/ # File structure (directory)
72
- gmax project # Project overview
73
- gmax related src/lib/syncer.ts # Dependencies + dependents
74
- gmax recent # Recently modified files
75
- gmax symbols auth # List indexed symbols
74
+ gmax project # Languages, structure, key symbols
75
+ gmax related src/lib/auth.ts # Dependencies + dependents
76
+ gmax recent # Recently modified files
77
+ gmax status # All indexed projects + chunk counts
76
78
  ```
77
79
 
78
80
  In our public benchmarks, `grepmax` can save about 20% of your LLM tokens and deliver a 30% speedup.
@@ -83,334 +85,162 @@ In our public benchmarks, `grepmax` can save about 20% of your LLM tokens and de
83
85
 
84
86
  ## Agent Plugins
85
87
 
86
- ### Claude Code
87
-
88
- 1. Run `gmax install-claude-code`
89
- 2. Open Claude Code — the plugin auto-starts the MLX GPU server and a background file watcher.
90
- 3. Claude uses `gmax` for semantic searches automatically via MCP tools.
91
-
92
- Plugin files (skill instructions, hooks) auto-update when you run `npm update -g grepmax` — no need to re-run `install-claude-code`.
93
-
94
- ### Opencode
95
- 1. Run `gmax install-opencode`
96
- 2. OC uses `gmax` for semantic searches via MCP.
97
-
98
- ### Codex
99
- 1. Run `gmax install-codex`
100
- 2. Codex uses `gmax` for semantic searches.
101
-
102
- ### Factory Droid
103
- 1. Run `gmax install-droid`
104
- 2. To remove: `gmax uninstall-droid`
105
-
106
- ### MCP Server
107
-
108
- `gmax mcp` starts a stdio-based MCP server that searches the centralized index directly — no HTTP daemon needed.
109
-
110
- | Tool | Description |
111
- | --- | --- |
112
- | `semantic_search` | Code search by meaning. 16 composable params: query, limit, root, path, detail (pointer/code/full), context_lines, min_score, max_per_file, file, exclude, language, role, mode (symbol), include_imports, name_pattern. |
113
- | `search_all` | Search ALL indexed code. Same params + `projects`/`exclude_projects` to scope by project name. |
114
- | `code_skeleton` | Collapsed file structure (~4x fewer tokens). Accepts files, directories, or comma-separated paths. `format: "json"` for structured output. |
115
- | `trace_calls` | Call graph with importers, callers (multi-hop via `depth`), and callees with file:line locations. |
116
- | `list_symbols` | List indexed symbols with role (ORCH/DEF/IMPL) and export status. |
117
- | `summarize_project` | High-level project overview — languages, directory structure, roles, key symbols, entry points. |
118
- | `related_files` | Find dependencies and dependents of a file by shared symbol references. |
119
- | `recent_changes` | Recently modified indexed files with relative timestamps. |
120
- | `index_status` | Check index health: per-project chunk counts, model info, watcher status. |
121
- | `summarize_directory` | Generate LLM summaries for indexed chunks. Summaries appear in search results. |
122
-
123
- ## Commands
124
-
125
- ### `gmax search`
126
-
127
- The default command. Searches indexed code using semantic meaning.
128
-
129
- ```bash
130
- gmax "how is the database connection pooled?"
131
- ```
132
-
133
- **Options:**
134
-
135
- | Flag | Description | Default |
136
- | --- | --- | --- |
137
- | `--agent` | Ultra-compact output for AI agents (one line per result). | `false` |
138
- | `-m <n>` | Max total results to return. | `5` |
139
- | `--per-file <n>` | Max matches to show per file. | `3` |
140
- | `-c`, `--content` | Show full chunk content instead of snippets. | `false` |
141
- | `-C <n>`, `--context <n>` | Include N lines before/after each result. | `0` |
142
- | `--scores` | Show relevance scores (0-1) for each result. | `false` |
143
- | `--min-score <n>` | Filter out results below this score threshold. | `0` |
144
- | `--root <dir>` | Search a different project directory. | cwd |
145
- | `--file <name>` | Filter to files matching this name (e.g. `syncer.ts`). | — |
146
- | `--exclude <prefix>` | Exclude files under this path prefix (e.g. `tests/`). | — |
147
- | `--lang <ext>` | Filter by file extension (e.g. `ts`, `py`). | — |
148
- | `--role <role>` | Filter by role: `ORCHESTRATION`, `DEFINITION`, `IMPLEMENTATION`. | — |
149
- | `--symbol` | Append call graph (importers, callers, callees) after results. | `false` |
150
- | `--imports` | Prepend file imports to each result. | `false` |
151
- | `--name <regex>` | Filter results by symbol name regex. | — |
152
- | `--compact` | Compact hits view (paths + line ranges + role/preview). | `false` |
153
- | `--skeleton` | Show code skeleton for matching files instead of snippets. | `false` |
154
- | `--plain` | Disable ANSI colors and use simpler formatting. | `false` |
155
- | `-s`, `--sync` | Force re-index changed files before searching. | `false` |
156
-
157
- **Examples:**
158
-
159
- ```bash
160
- gmax "API rate limiting logic"
161
- gmax "auth handler" --role ORCHESTRATION --lang ts --agent
162
- gmax "database" --file syncer.ts --agent
163
- gmax "VectorDB" --symbol --agent
164
- gmax "error handling" -C 5 --imports --plain
165
- gmax "handler" --name "handle.*" --exclude tests/ --agent
166
- ```
167
-
168
- > **For AI agents:** Use `--agent` for the most token-efficient output (~90% fewer tokens than default). Output format: `file:line symbol [role] — summary`
169
-
170
- ### `gmax index`
171
-
172
- Index a directory into the centralized store.
173
-
174
- - Respects `.gitignore` and `.gmaxignore`.
175
- - Only embeds code and config files. Skips binaries, lockfiles, and minified assets.
176
- - Uses TreeSitter for semantic chunking (TypeScript, JavaScript, Python, Go, Rust, C/C++, Java, C#, Ruby, PHP, Swift, Kotlin, JSON).
177
- - Files already indexed with matching content are skipped automatically.
88
+ gmax integrates with Claude Code, OpenCode, Codex, and Factory Droid. Install all detected clients at once:
178
89
 
179
90
  ```bash
180
- gmax index # Index current dir
181
- gmax index --path ~/workspace # Index a specific directory
182
- gmax index --dry-run # See what would be indexed
183
- gmax index --verbose # Watch detailed progress
184
- gmax index --reset # Full re-index from scratch
91
+ gmax plugin add # Install all detected clients
92
+ gmax plugin # Show plugin status
93
+ gmax plugin remove # Remove all plugins
185
94
  ```
186
95
 
187
- ### `gmax watch`
188
-
189
- Background file watcher for live reindexing. A single daemon process watches all registered projects through native OS file system events (`@parcel/watcher` — FSEvents on macOS, inotify on Linux). File changes are detected in sub-second and incrementally reindexed.
190
-
191
- ```bash
192
- gmax watch --daemon -b # Start daemon (watches all projects)
193
- gmax watch -b # Per-project mode (fallback)
194
- gmax watch status # Show daemon + watcher status
195
- gmax watch stop # Stop daemon
196
- gmax watch stop --all # Stop everything
197
- ```
198
-
199
- The daemon auto-starts when you run `gmax search` or use MCP tools. It shuts down after 30 minutes of inactivity. CLI commands communicate with the daemon over a Unix domain socket at `~/.gmax/daemon.sock`.
200
-
201
- ### `gmax summarize`
202
-
203
- Generate one-line LLM summaries for indexed chunks. Requires the summarizer server (Qwen3-Coder via MLX on Apple Silicon). Summaries are stored in LanceDB and appear in search results.
96
+ Or manage individually:
204
97
 
205
98
  ```bash
206
- gmax summarize # Summarize all unsummarized chunks
207
- gmax summarize --path src/lib/ # Only summarize chunks under a directory
99
+ gmax plugin add claude # Claude Code only
100
+ gmax plugin add opencode # OpenCode only
101
+ gmax plugin add codex # Codex only
102
+ gmax plugin add droid # Factory Droid only
103
+ gmax plugin remove claude # Remove specific plugin
208
104
  ```
209
105
 
210
- Summarization is **on-demand only** it does not run automatically during indexing or file watching. The `summarize_directory` MCP tool provides the same functionality for AI agents.
106
+ Plugins auto-update when you run `npm install -g grepmax@latest` no need to re-run `gmax plugin add`.
211
107
 
212
- ### `gmax serve`
108
+ ### How it works per client
213
109
 
214
- HTTP server with live file watching. Useful for non-MCP integrations.
110
+ - **Claude Code:** Plugin with hooks (SessionStart, CwdChanged, SubagentStart, PreToolUse). Model uses CLI via `Bash(gmax ... --agent)`.
111
+ - **OpenCode:** Tool shim with dynamic SKILL + session plugin for daemon startup. Model calls gmax tool directly.
112
+ - **Codex:** MCP server registration + AGENTS.md skill instructions.
113
+ - **Factory Droid:** Skills + SessionStart/SessionEnd hooks for daemon lifecycle.
215
114
 
216
- ```bash
217
- gmax serve # Foreground, port 4444
218
- gmax serve --background # Background mode
219
- gmax serve --cpu # Force CPU-only embeddings
220
- ```
221
-
222
- ### `gmax trace`
223
-
224
- Call graph — who imports a symbol, who calls it, and what it calls.
225
-
226
- ```bash
227
- gmax trace handleAuth # 1-hop trace
228
- gmax trace handleAuth -d 2 # 2-hop: callers-of-callers
229
- ```
230
-
231
- ### `gmax skeleton`
232
-
233
- Compressed view of a file — signatures with bodies collapsed. Supports files, directories, and batch.
234
-
235
- ```bash
236
- gmax skeleton src/lib/auth.ts # Single file
237
- gmax skeleton src/lib/search/ # All files in directory
238
- gmax skeleton src/a.ts,src/b.ts # Batch
239
- gmax skeleton src/lib/auth.ts --json # Structured JSON output
240
- gmax skeleton AuthService # Find symbol, skeletonize its file
241
- ```
242
-
243
- **Supported Languages:** TypeScript, JavaScript, Python, Go, Rust, Java, C#, C++, C, Ruby, PHP, Swift, Kotlin.
244
-
245
- ### `gmax project`
246
-
247
- High-level project overview — languages, directory structure, role distribution, key symbols, entry points.
248
-
249
- ```bash
250
- gmax project # Current project
251
- gmax project --root ~/workspace # Different project
252
- ```
253
-
254
- ### `gmax related`
115
+ ### MCP Server
255
116
 
256
- Find files related by shared symbol references dependencies and dependents.
117
+ `gmax mcp` starts a stdio-based MCP server for clients that support MCP but can't run shell commands (Cursor, Windsurf, custom agents).
257
118
 
258
- ```bash
259
- gmax related src/lib/index/syncer.ts
260
- gmax related src/commands/mcp.ts -l 5
119
+ | Tool | Description |
120
+ | --- | --- |
121
+ | `semantic_search` | Search by meaning. 16+ params: query, limit, role, language, scope (project/all), etc. |
122
+ | `search_all` | Cross-project search. Same params + project filtering. |
123
+ | `code_skeleton` | File structure with bodies collapsed (~4x fewer tokens). |
124
+ | `trace_calls` | Call graph: importers, callers (multi-hop), callees with file:line. |
125
+ | `extract_symbol` | Complete function/class body by symbol name. |
126
+ | `peek_symbol` | Compact overview: signature + callers + callees. |
127
+ | `list_symbols` | Indexed symbols with role and export status. |
128
+ | `index_status` | Index health: chunks, files, projects, watcher status. |
129
+ | `summarize_project` | Project overview: languages, structure, key symbols, entry points. |
130
+ | `summarize_directory` | Generate LLM summaries for indexed chunks. |
131
+ | `related_files` | Dependencies and dependents by shared symbols. |
132
+ | `recent_changes` | Recently modified indexed files. |
133
+ | `diff_changes` | Search scoped to git changes. |
134
+ | `find_tests` | Find tests via reverse call graph. |
135
+ | `impact_analysis` | Dependents + affected tests for a symbol or file. |
136
+ | `find_similar` | Vector similarity search. |
137
+ | `build_context` | Token-budgeted topic summary. |
138
+
139
+ ## Search Options
140
+
141
+ ```bash
142
+ gmax "query" [options]
261
143
  ```
262
144
 
263
- ### `gmax recent`
264
-
265
- Show recently modified indexed files with relative timestamps.
266
-
267
- ```bash
268
- gmax recent # Last 20 modified files
269
- gmax recent -l 10 # Last 10
270
- gmax recent --root ~/workspace # Different project
271
- ```
145
+ | Flag | Description | Default |
146
+ | --- | --- | --- |
147
+ | `--agent` | Compact one-line output for AI agents. | `false` |
148
+ | `-m <n>` | Max results. | `5` |
149
+ | `--per-file <n>` | Max matches per file. | `3` |
150
+ | `--role <role>` | Filter: `ORCHESTRATION`, `DEFINITION`, `IMPLEMENTATION`. | — |
151
+ | `--lang <ext>` | Filter by extension (e.g. `ts`, `py`). | — |
152
+ | `--file <name>` | Filter by filename. | — |
153
+ | `--exclude <prefix>` | Exclude path prefix. | — |
154
+ | `--symbol` | Append call graph after results. | `false` |
155
+ | `--imports` | Prepend file imports per result. | `false` |
156
+ | `--name <regex>` | Filter by symbol name. | — |
157
+ | `--skeleton` | Show file skeletons for top matches. | `false` |
158
+ | `--context-for-llm` | Full function bodies + imports per result. | `false` |
159
+ | `--budget <tokens>` | Cap output tokens (for `--context-for-llm`). | `8000` |
160
+ | `--explain` | Show scoring breakdown per result. | `false` |
161
+ | `-C <n>` | Context lines before/after. | `0` |
162
+ | `--root <dir>` | Search a different project. | cwd |
163
+ | `--min-score <n>` | Minimum relevance score. | `0` |
272
164
 
273
- ### `gmax config`
165
+ ## Background Daemon
274
166
 
275
- View or update configuration without the full interactive setup.
167
+ A single daemon watches all registered projects via native OS file events (FSEvents/inotify). Changes are detected in sub-second and incrementally reindexed.
276
168
 
277
169
  ```bash
278
- gmax config # Show current settings
279
- gmax config --embed-mode cpu # Switch to CPU embeddings
280
- gmax config --embed-mode gpu # Switch to GPU (MLX)
281
- gmax config --model-tier standard # Switch to standard model (768d)
170
+ gmax watch --daemon -b # Start daemon
171
+ gmax watch stop # Stop daemon
172
+ gmax status # See all projects + watcher status
282
173
  ```
283
174
 
284
- ### `gmax doctor`
285
-
286
- Checks installation health, model paths, and database integrity.
287
-
288
- ```bash
289
- gmax doctor
290
- ```
175
+ The daemon auto-starts via agent plugins and shuts down after 30 minutes of inactivity.
291
176
 
292
177
  ## Architecture
293
178
 
294
- ### Centralized Index
295
-
296
179
  All data lives in `~/.gmax/`:
297
- - `~/.gmax/lancedb/` — LanceDB vector store (one database for all indexed directories)
298
- - `~/.gmax/cache/meta.lmdb` — file metadata cache (content hashes, mtimes)
299
- - `~/.gmax/cache/watchers.lmdb` — watcher/daemon registry (LMDB, crash-safe)
300
- - `~/.gmax/daemon.sock` — Unix domain socket for daemon IPC
301
- - `~/.gmax/logs/`daemon and watcher logs (5MB rotation)
302
- - `~/.gmax/config.json` — global config (model tier, embed mode)
303
- - `~/.gmax/models/`embedding models
304
- - `~/.gmax/grammars/` — Tree-sitter grammars
305
- - `~/.gmax/projects.json` — registry of indexed directories
306
-
307
- All chunks store **absolute file paths**. Search scoping is done via path prefix filtering. There are no per-project index directories.
308
-
309
- ### Performance
310
-
311
- - **Single Daemon:** One process watches all projects via native OS events — no polling, sub-second file change detection. Shared VectorDB, MetaCache, and worker pool across projects.
312
- - **Native File Watching:** `@parcel/watcher` uses FSEvents (macOS), inotify (Linux), ReadDirectoryChangesW (Windows) — zero CPU overhead, no file descriptor exhaustion.
313
- - **Automatic Compaction:** LanceDB table fragments from incremental inserts are compacted every 5 minutes, preventing performance degradation over time.
314
- - **LMDB Caching:** File metadata reads use LRU/LFU caching for the watcher's hot path.
315
- - **Bounded Concurrency:** Worker threads scale to 50% of CPU cores (min 4). Override with `GMAX_WORKER_THREADS`.
316
- - **Smart Chunking:** `tree-sitter` splits code by function/class boundaries for complete logical blocks.
317
- - **Deduplication:** Identical code blocks are embedded once and cached.
318
- - **Multi-stage Search:** Vector search + FTS + RRF fusion + ColBERT reranking + structural boosting.
319
- - **Role Classification:** Detects `ORCHESTRATION` (high complexity, many calls) vs `DEFINITION` (types/classes).
180
+ - `lancedb/` — LanceDB vector store (centralized, all projects)
181
+ - `cache/meta.lmdb` — file metadata cache (hashes, mtimes)
182
+ - `cache/watchers.lmdb` — watcher/daemon registry (LMDB, crash-safe)
183
+ - `daemon.sock` — Unix domain socket for daemon IPC
184
+ - `daemon.pid`PID file for daemon dedup
185
+ - `logs/`daemon and server logs (5MB rotation)
186
+ - `config.json`global config (model tier, embed mode)
187
+ - `models/` — embedding models
188
+ - `grammars/`Tree-sitter grammars
189
+ - `projects.json` — registry of indexed directories
320
190
 
321
- ### GPU Embeddings (Apple Silicon)
191
+ **Pipeline:** Walk (gitignore-aware) → Chunk (Tree-sitter) → Embed (384-dim Granite via ONNX/MLX) → Store (LanceDB + LMDB) → Search (vector + FTS + RRF fusion + ColBERT rerank)
322
192
 
323
- On Macs with Apple Silicon, gmax defaults to MLX for GPU-accelerated embeddings. The MLX embed server runs on port `8100` and is managed automatically by the Claude Code plugin hook.
324
-
325
- To force CPU mode: `GMAX_EMBED_MODE=cpu gmax index`
326
-
327
- ### LLM Summaries
328
-
329
- gmax can generate one-line natural language descriptions for every code chunk using a local LLM (Qwen3-Coder-30B-A3B via MLX). Summaries are stored in LanceDB — zero latency at search time.
330
-
331
- Summarization is **on-demand**, not automatic. Run `gmax summarize` or use the `summarize_directory` MCP tool after indexing. The summarizer server runs on port `8101` and must be started separately. If unavailable, `gmax summarize` will report the server is not running.
332
-
333
- ```bash
334
- gmax summarize # Generate summaries for all unsummarized chunks
335
- gmax summarize --path src/lib/ # Scope to a directory
336
- gmax doctor # Check summarizer status + coverage
337
- ```
338
-
339
- Example search output with summaries:
340
- ```
341
- handleAuth [exported ORCH C:8] src/auth/handler.ts:45-90
342
- Validates JWT from Authorization header, checks RBAC permissions, returns 401 on failure
343
- parent:AuthController calls:validateToken,checkRole,respond
344
- ```
193
+ **Supported Languages:** TypeScript, JavaScript, Python, Go, Rust, Java, C#, C++, C, Ruby, PHP, Swift, Kotlin, JSON, YAML, Markdown, SQL, Shell.
345
194
 
346
195
  ## Configuration
347
196
 
348
- ### Config File
349
-
350
- Settings are stored in `~/.gmax/config.json`:
351
-
352
197
  ```json
198
+ // ~/.gmax/config.json
353
199
  {
354
200
  "modelTier": "small",
355
201
  "vectorDim": 384,
356
- "embedMode": "gpu",
357
- "mlxModel": "ibm-granite/granite-embedding-small-english-r2"
202
+ "embedMode": "gpu"
358
203
  }
359
204
  ```
360
205
 
361
- View and change settings with `gmax config` or run `gmax setup` for interactive configuration.
362
-
363
206
  ### Ignoring Files
364
207
 
365
- gmax respects `.gitignore` and `.gmaxignore` files. Create a `.gmaxignore` in your directory root to exclude additional patterns:
208
+ gmax respects `.gitignore` and `.gmaxignore`:
366
209
 
367
210
  ```gitignore
368
- # .gmaxignore — same syntax as .gitignore
211
+ # .gmaxignore
369
212
  docs/generated/
370
213
  *.test.ts
371
214
  fixtures/
372
215
  ```
373
216
 
374
- ### Index Management
375
-
376
- - **View indexed directories:** `gmax list --all`
377
- - **Index location:** `~/.gmax/lancedb/` (centralized)
378
- - **Clean up:** `gmax index --reset` re-indexes the current directory from scratch
379
- - **Full reset:** `rm -rf ~/.gmax/lancedb ~/.gmax/cache` to start completely fresh
380
-
381
217
  ### Environment Variables
382
218
 
383
219
  | Variable | Description | Default |
384
220
  | --- | --- | --- |
385
- | `GMAX_WORKER_THREADS` | Number of worker threads for embedding | 50% of CPU cores |
386
- | `GMAX_EMBED_MODE` | Force `cpu` or `gpu` embedding mode | Auto-detect |
387
- | `GMAX_DEBUG` | Enable debug logging (`1` to enable) | Off |
388
- | `GMAX_VERBOSE` | Enable verbose output (`1` to enable) | Off |
389
- | `GMAX_WORKER_TASK_TIMEOUT_MS` | Worker task timeout in ms | `120000` |
390
- | `GMAX_MAX_WORKER_MEMORY_MB` | Max worker memory in MB | 50% of system RAM |
391
- | `GMAX_MAX_PER_FILE` | Default max results per file in search | `3` |
392
- | `GMAX_WATCH_POLL` | Force polling mode for file watcher (`1` to enable) | Off (FSEvents on macOS) |
221
+ | `GMAX_EMBED_MODE` | Force `cpu` or `gpu` | Auto-detect |
222
+ | `GMAX_WORKER_THREADS` | Worker threads for embedding | 50% of cores |
223
+ | `GMAX_DEBUG` | Debug logging | Off |
224
+ | `GMAX_SUMMARIZER` | Enable summarizer auto-start (`1`) | Off |
393
225
 
394
- ## Contributing
226
+ ## Troubleshooting
395
227
 
396
- See [CLAUDE.md](CLAUDE.md) for development setup, commands, and architecture details.
228
+ ```bash
229
+ gmax doctor # Check health
230
+ gmax doctor --fix # Auto-repair (compact, prune, fix locks)
231
+ gmax doctor --agent # Machine-readable health output
232
+ gmax index --reset # Full reindex from scratch
233
+ gmax watch stop && gmax watch --daemon -b # Restart daemon
234
+ ```
397
235
 
398
- ## Troubleshooting
236
+ ## Contributing
399
237
 
400
- - **Index feels stale?** Run `gmax index` to refresh. The daemon auto-reindexes on file changes.
401
- - **Weird results?** Run `gmax doctor` to verify models.
402
- - **Index getting stuck?** Run `gmax index --verbose` to see which file is being processed.
403
- - **Need a fresh start?** `rm -rf ~/.gmax/lancedb ~/.gmax/cache` then `gmax index`.
404
- - **Daemon issues?** Check `~/.gmax/logs/daemon.log`. Run `gmax watch stop` then `gmax watch --daemon -b` to restart.
405
- - **MLX server won't start?** Check `~/.gmax/logs/mlx-embed-server.log` for errors. Use `GMAX_EMBED_MODE=cpu` to fall back to CPU.
238
+ See [CLAUDE.md](CLAUDE.md) for development setup, commands, and architecture details.
406
239
 
407
240
  ## Attribution
408
241
 
409
- grepmax is built upon the foundation of [mgrep](https://github.com/mixedbread-ai/mgrep) by MixedBread. We acknowledge and appreciate the original architectural concepts and design decisions that informed this work.
410
-
411
- See the [NOTICE](NOTICE) file for detailed attribution information.
242
+ grepmax is built upon the foundation of [mgrep](https://github.com/mixedbread-ai/mgrep) by MixedBread. See the [NOTICE](NOTICE) file for details.
412
243
 
413
244
  ## License
414
245
 
415
- Licensed under the Apache License, Version 2.0.
416
- See [LICENSE](LICENSE) and [Apache-2.0](https://opensource.org/licenses/Apache-2.0) for details.
246
+ Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE).
@@ -44,7 +44,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.watch = void 0;
46
46
  const node_child_process_1 = require("node:child_process");
47
- const fs = __importStar(require("node:fs"));
48
47
  const path = __importStar(require("node:path"));
49
48
  const commander_1 = require("commander");
50
49
  const config_1 = require("../config");
@@ -80,16 +79,6 @@ exports.watch = new commander_1.Command("watch")
80
79
  if (options.background) {
81
80
  // Skip spawn if daemon already running — prevents process accumulation
82
81
  // when SessionStart hook fires on every session/clear/resume
83
- const pidFile = config_1.PATHS.daemonPidFile;
84
- try {
85
- const existingPid = Number.parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
86
- if (existingPid) {
87
- process.kill(existingPid, 0); // throws if dead
88
- process.exit(0); // alive — skip
89
- }
90
- }
91
- catch (_c) { }
92
- // Also check socket as fallback
93
82
  const { isDaemonRunning } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
94
83
  if (yield isDaemonRunning()) {
95
84
  process.exit(0);
package/dist/config.js CHANGED
@@ -96,6 +96,7 @@ exports.PATHS = {
96
96
  logsDir: path.join(GLOBAL_ROOT, "logs"),
97
97
  daemonSocket: path.join(GLOBAL_ROOT, "daemon.sock"),
98
98
  daemonPidFile: path.join(GLOBAL_ROOT, "daemon.pid"),
99
+ daemonLockFile: path.join(GLOBAL_ROOT, "daemon.lock"),
99
100
  // Centralized index storage — one database for all indexed directories
100
101
  lancedbDir: path.join(GLOBAL_ROOT, "lancedb"),
101
102
  cacheDir: path.join(GLOBAL_ROOT, "cache"),
@@ -41,12 +41,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
41
41
  step((generator = generator.apply(thisArg, _arguments || [])).next());
42
42
  });
43
43
  };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
44
47
  Object.defineProperty(exports, "__esModule", { value: true });
45
48
  exports.Daemon = void 0;
46
49
  const fs = __importStar(require("node:fs"));
47
50
  const net = __importStar(require("node:net"));
48
51
  const path = __importStar(require("node:path"));
49
52
  const watcher = __importStar(require("@parcel/watcher"));
53
+ const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
50
54
  const config_1 = require("../../config");
51
55
  const batch_processor_1 = require("../index/batch-processor");
52
56
  const watcher_1 = require("../index/watcher");
@@ -65,6 +69,7 @@ class Daemon {
65
69
  this.vectorDb = null;
66
70
  this.metaCache = null;
67
71
  this.server = null;
72
+ this.releaseLock = null;
68
73
  this.lastActivity = Date.now();
69
74
  this.startTime = Date.now();
70
75
  this.heartbeatInterval = null;
@@ -75,35 +80,37 @@ class Daemon {
75
80
  start() {
76
81
  return __awaiter(this, void 0, void 0, function* () {
77
82
  process.title = "gmax-daemon";
78
- // 1. Kill existing per-project watchers
83
+ // 1. Acquire exclusive lock — kernel-enforced, atomic, auto-released on death
84
+ fs.mkdirSync(path.dirname(config_1.PATHS.daemonLockFile), { recursive: true });
85
+ fs.writeFileSync(config_1.PATHS.daemonLockFile, "", { flag: "a" }); // ensure file exists
86
+ try {
87
+ this.releaseLock = yield proper_lockfile_1.default.lock(config_1.PATHS.daemonLockFile, {
88
+ retries: 0,
89
+ stale: 30000,
90
+ });
91
+ }
92
+ catch (err) {
93
+ if (err.code === "ELOCKED") {
94
+ console.error("[daemon] Another daemon is already running");
95
+ process.exit(0);
96
+ }
97
+ throw err;
98
+ }
99
+ // 2. Kill existing per-project watchers
79
100
  const existing = (0, watcher_store_1.listWatchers)();
80
101
  for (const w of existing) {
81
102
  console.log(`[daemon] Taking over from per-project watcher (PID: ${w.pid}, ${path.basename(w.projectRoot)})`);
82
103
  yield (0, process_1.killProcess)(w.pid);
83
104
  (0, watcher_store_1.unregisterWatcher)(w.pid);
84
105
  }
85
- // 2. PID file — atomic dedup guard
86
- const pidFile = config_1.PATHS.daemonPidFile;
87
- try {
88
- // Check if another daemon is alive
89
- const existingPid = Number.parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
90
- if (existingPid && existingPid !== process.pid) {
91
- try {
92
- process.kill(existingPid, 0); // throws if dead
93
- console.error("[daemon] Another daemon is already running (PID:", existingPid + ")");
94
- process.exit(0);
95
- }
96
- catch (_a) { }
97
- }
98
- }
99
- catch (_b) { }
100
- fs.writeFileSync(pidFile, String(process.pid));
101
- // 3. Stale socket cleanup
106
+ // 3. Write PID file (informational only lock is the real guard)
107
+ fs.writeFileSync(config_1.PATHS.daemonPidFile, String(process.pid));
108
+ // 4. Stale socket cleanup
102
109
  try {
103
110
  fs.unlinkSync(config_1.PATHS.daemonSocket);
104
111
  }
105
- catch (_c) { }
106
- // 3. Open shared resources
112
+ catch (_a) { }
113
+ // 5. Open shared resources
107
114
  try {
108
115
  fs.mkdirSync(config_1.PATHS.cacheDir, { recursive: true });
109
116
  fs.mkdirSync(config_1.PATHS.lancedbDir, { recursive: true });
@@ -114,9 +121,9 @@ class Daemon {
114
121
  console.error("[daemon] Failed to open shared resources:", err);
115
122
  throw err;
116
123
  }
117
- // 4. Register daemon (only after resources are open)
124
+ // 6. Register daemon (only after resources are open)
118
125
  (0, watcher_store_1.registerDaemon)(process.pid);
119
- // 5. Subscribe to all registered projects (skip missing directories)
126
+ // 7. Subscribe to all registered projects (skip missing directories)
120
127
  const projects = (0, project_registry_1.listProjects)().filter((p) => p.status === "indexed");
121
128
  for (const p of projects) {
122
129
  if (!fs.existsSync(p.root)) {
@@ -130,18 +137,18 @@ class Daemon {
130
137
  console.error(`[daemon] Failed to watch ${path.basename(p.root)}:`, err);
131
138
  }
132
139
  }
133
- // 6. Heartbeat
140
+ // 8. Heartbeat
134
141
  this.heartbeatInterval = setInterval(() => {
135
142
  (0, watcher_store_1.heartbeat)(process.pid);
136
143
  }, HEARTBEAT_INTERVAL_MS);
137
- // 7. Idle timeout
144
+ // 9. Idle timeout
138
145
  this.idleInterval = setInterval(() => {
139
146
  if (Date.now() - this.lastActivity > IDLE_TIMEOUT_MS) {
140
147
  console.log("[daemon] Idle for 30 minutes, shutting down");
141
148
  this.shutdown();
142
149
  }
143
150
  }, HEARTBEAT_INTERVAL_MS);
144
- // 8. Socket server
151
+ // 10. Socket server
145
152
  this.server = net.createServer((conn) => {
146
153
  let buf = "";
147
154
  conn.on("data", (chunk) => {
@@ -171,7 +178,7 @@ class Daemon {
171
178
  this.server.on("error", (err) => {
172
179
  const code = err.code;
173
180
  if (code === "EADDRINUSE") {
174
- console.error("[daemon] Another daemon is already running");
181
+ console.error("[daemon] Socket already in use");
175
182
  reject(err);
176
183
  }
177
184
  else if (code === "EOPNOTSUPP") {
@@ -301,7 +308,7 @@ class Daemon {
301
308
  catch (_d) { }
302
309
  }
303
310
  this.subscriptions.clear();
304
- // Close server + socket + PID file
311
+ // Close server + socket + PID file + lock
305
312
  (_a = this.server) === null || _a === void 0 ? void 0 : _a.close();
306
313
  try {
307
314
  fs.unlinkSync(config_1.PATHS.daemonSocket);
@@ -311,6 +318,13 @@ class Daemon {
311
318
  fs.unlinkSync(config_1.PATHS.daemonPidFile);
312
319
  }
313
320
  catch (_f) { }
321
+ if (this.releaseLock) {
322
+ try {
323
+ yield this.releaseLock();
324
+ }
325
+ catch (_g) { }
326
+ this.releaseLock = null;
327
+ }
314
328
  // Unregister all
315
329
  for (const root of this.processors.keys()) {
316
330
  (0, watcher_store_1.unregisterWatcherByRoot)(root);
@@ -321,11 +335,11 @@ class Daemon {
321
335
  try {
322
336
  yield ((_b = this.metaCache) === null || _b === void 0 ? void 0 : _b.close());
323
337
  }
324
- catch (_g) { }
338
+ catch (_h) { }
325
339
  try {
326
340
  yield ((_c = this.vectorDb) === null || _c === void 0 ? void 0 : _c.close());
327
341
  }
328
- catch (_h) { }
342
+ catch (_j) { }
329
343
  console.log("[daemon] Shutdown complete");
330
344
  });
331
345
  }
@@ -488,8 +488,12 @@ class VectorDB {
488
488
  (_a = this.unregisterCleanup) === null || _a === void 0 ? void 0 : _a.call(this);
489
489
  this.unregisterCleanup = undefined;
490
490
  if (this.db) {
491
- if (this.db.close)
492
- yield this.db.close();
491
+ if (this.db.close) {
492
+ yield Promise.race([
493
+ this.db.close(),
494
+ new Promise((resolve) => setTimeout(resolve, 5000)),
495
+ ]);
496
+ }
493
497
  }
494
498
  this.db = null;
495
499
  });
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.gracefulExit = gracefulExit;
13
13
  const pool_1 = require("../workers/pool");
14
14
  const cleanup_1 = require("./cleanup");
15
+ const EXIT_TIMEOUT_MS = 8000;
15
16
  function gracefulExit(code) {
16
17
  return __awaiter(this, void 0, void 0, function* () {
17
18
  const finalCode = typeof code === "number"
@@ -19,6 +20,12 @@ function gracefulExit(code) {
19
20
  : typeof process.exitCode === "number"
20
21
  ? process.exitCode
21
22
  : 0;
23
+ // Safety net: force-exit if cleanup hangs
24
+ const forceTimer = !process.env.VITEST && process.env.NODE_ENV !== "test"
25
+ ? setTimeout(() => process.exit(finalCode), EXIT_TIMEOUT_MS)
26
+ : undefined;
27
+ if (forceTimer)
28
+ forceTimer.unref();
22
29
  try {
23
30
  if ((0, pool_1.isWorkerPoolInitialized)()) {
24
31
  yield (0, pool_1.destroyWorkerPool)();
@@ -28,6 +35,8 @@ function gracefulExit(code) {
28
35
  console.error("[exit] Failed to destroy worker pool:", err);
29
36
  }
30
37
  yield (0, cleanup_1.runCleanup)();
38
+ if (forceTimer)
39
+ clearTimeout(forceTimer);
31
40
  // Avoid exiting the process during test runs so Vitest can report results.
32
41
  if (process.env.VITEST || process.env.NODE_ENV === "test") {
33
42
  process.exitCode = finalCode;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.12.9",
3
+ "version": "0.12.12",
4
4
  "author": "Robert Owens <robowens@me.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -48,6 +48,7 @@
48
48
  "onnxruntime-node": "1.24.3",
49
49
  "ora": "^9.3.0",
50
50
  "piscina": "^5.1.4",
51
+ "proper-lockfile": "^4.1.2",
51
52
  "simsimd": "^6.5.5",
52
53
  "uuid": "^13.0.0",
53
54
  "web-tree-sitter": "^0.26.7",
@@ -57,6 +58,7 @@
57
58
  "@anthropic-ai/claude-agent-sdk": "^0.2.87",
58
59
  "@biomejs/biome": "2.4.10",
59
60
  "@types/node": "^25.5.0",
61
+ "@types/proper-lockfile": "^4.1.4",
60
62
  "node-gyp": "^12.1.0",
61
63
  "ts-node": "^10.9.2",
62
64
  "typescript": "^6.0.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.12.9",
3
+ "version": "0.12.12",
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",