grepmax 0.26.35 → 0.26.37

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.
@@ -10,7 +10,7 @@
10
10
  "name": "grepmax",
11
11
  "source": "./plugins/grepmax",
12
12
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
13
- "version": "0.26.35",
13
+ "version": "0.26.37",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
package/README.md CHANGED
@@ -50,6 +50,7 @@ No setup required — gmax auto-detects your platform (GPU on Apple Silicon, CPU
50
50
  gmax setup # Interactive wizard (models, embedding mode, plugins)
51
51
  gmax config # View current settings
52
52
  gmax config --embed-mode gpu # Switch to GPU (Apple Silicon)
53
+ gmax config --worker-threads 2 # Cap worker processes (auto = default); applies on daemon restart
53
54
  gmax doctor # Health check
54
55
  gmax doctor --fix # Auto-repair (compact, prune, remove stale locks)
55
56
  ```
@@ -85,6 +86,7 @@ gmax context src/lib/auth.ts --budget 4000 # Deterministic file/path context
85
86
  gmax project # Languages, structure, key symbols
86
87
  gmax related src/lib/auth.ts # Dependencies + dependents
87
88
  gmax status # All indexed projects + chunk counts
89
+ gmax status --json # The same plus daemon, settings and workers, as JSON
88
90
  ```
89
91
 
90
92
  In our public benchmarks, `grepmax` can save about 20% of your LLM tokens and deliver a 30% speedup.
@@ -214,6 +216,7 @@ The daemon watches only projects that are in use: each Claude Code session (its
214
216
  ```bash
215
217
  gmax watch --daemon -b # Start daemon manually
216
218
  gmax watch stop # Stop daemon
219
+ gmax watch restart # Stop it, wait for it to exit, start a fresh one (--json)
217
220
  gmax status # See all projects + watcher status
218
221
  ```
219
222
 
@@ -423,7 +426,7 @@ fixtures/
423
426
  | Variable | Description | Default |
424
427
  | --- | --- | --- |
425
428
  | `GMAX_EMBED_MODE` | Force `cpu` or `gpu` | Auto-detect |
426
- | `GMAX_WORKER_THREADS` | Worker threads for embedding | 50% of cores |
429
+ | `GMAX_WORKER_THREADS` | Worker processes for embedding; overrides `gmax config --worker-threads` | `min(4, max(2, cores/2))` |
427
430
  | `GMAX_WORKER_RSS_RECYCLE_MB` | Recycle workers that remain above this RSS; `0` disables the check | `1536` |
428
431
  | `GMAX_DEBUG` | Debug logging | Off |
429
432
  | `GMAX_SUMMARIZER` | Enable summarizer auto-start (`1`) | Off |
@@ -438,7 +441,7 @@ gmax doctor --fix # Auto-repair (compact, prune, fix locks)
438
441
  gmax doctor --agent # Machine-readable health output
439
442
  gmax index # Reindex (auto-detects and repairs cache/vector mismatches)
440
443
  gmax index --reset # Full reindex from scratch
441
- gmax watch stop && gmax watch --daemon -b # Restart daemon
444
+ gmax watch restart # Restart daemon
442
445
  ```
443
446
 
444
447
  `gmax doctor` reports ANN index state. `ANN: vector index not built` is normal with the default exact-search configuration.
@@ -1,4 +1,37 @@
1
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
+ })();
2
35
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
36
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
37
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -10,6 +43,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
43
  };
11
44
  Object.defineProperty(exports, "__esModule", { value: true });
12
45
  exports.config = void 0;
46
+ const os = __importStar(require("node:os"));
13
47
  const commander_1 = require("commander");
14
48
  const config_1 = require("../config");
15
49
  const embedding_status_1 = require("../lib/index/embedding-status");
@@ -22,12 +56,15 @@ exports.config = new commander_1.Command("config")
22
56
  .option("--embed-mode <mode>", "Set embedding mode: cpu or gpu")
23
57
  .option("--model-tier <tier>", "Set model tier: small (384d) or standard (768d)")
24
58
  .option("--query-log <on|off>", "Enable/disable query logging to ~/.gmax/logs/queries.jsonl")
59
+ .option("--worker-threads <n|auto>", "Set the number of embedding worker processes, or auto for the default")
25
60
  .addHelpText("after", `
26
61
  Examples:
27
62
  gmax config Show current configuration
28
63
  gmax config --embed-mode cpu Switch to CPU embeddings
29
64
  gmax config --model-tier standard Switch to standard model (768d)
30
65
  gmax config --query-log on Enable query logging
66
+ gmax config --worker-threads 2 Run at most 2 worker processes
67
+ gmax config --worker-threads auto Back to the computed default
31
68
  `)
32
69
  .action((_opts, cmd) => __awaiter(void 0, void 0, void 0, function* () {
33
70
  var _a, _b, _c, _d, _e;
@@ -37,7 +74,8 @@ Examples:
37
74
  const project = (0, project_registry_1.getProject)((_a = (0, project_root_1.findProjectRoot)(process.cwd())) !== null && _a !== void 0 ? _a : process.cwd());
38
75
  const hasUpdates = options.embedMode !== undefined ||
39
76
  options.modelTier !== undefined ||
40
- options.queryLog !== undefined;
77
+ options.queryLog !== undefined ||
78
+ options.workerThreads !== undefined;
41
79
  if (!hasUpdates) {
42
80
  // Show current config
43
81
  const tier = (_b = config_1.MODEL_TIERS[globalConfig.modelTier]) !== null && _b !== void 0 ? _b : config_1.MODEL_TIERS.small;
@@ -57,10 +95,15 @@ Examples:
57
95
  }
58
96
  }
59
97
  console.log(` Query log: ${globalConfig.queryLog ? "on" : "off"}`);
98
+ const threads = (0, config_1.resolveWorkerThreads)({
99
+ env: process.env.GMAX_WORKER_THREADS,
100
+ configValue: globalConfig.workerThreads,
101
+ });
102
+ console.log(` Worker threads: ${threads.value} (${threads.source})`);
60
103
  if (project === null || project === void 0 ? void 0 : project.lastIndexed) {
61
104
  console.log(` Last indexed: ${project.lastIndexed}`);
62
105
  }
63
- console.log(`\nTo change: gmax config --embed-mode <cpu|gpu> --model-tier <small|standard> --query-log <on|off>`);
106
+ console.log(`\nTo change: gmax config --embed-mode <cpu|gpu> --model-tier <small|standard> --query-log <on|off> --worker-threads <n|auto>`);
64
107
  yield (0, exit_1.gracefulExit)();
65
108
  return;
66
109
  }
@@ -75,6 +118,36 @@ Examples:
75
118
  yield (0, exit_1.gracefulExit)(1);
76
119
  return;
77
120
  }
121
+ if (options.workerThreads !== undefined) {
122
+ const cores = os.cpus().length || 1;
123
+ const raw = options.workerThreads.trim().toLowerCase();
124
+ const value = raw === "auto" ? undefined : (0, config_1.parseWorkerThreads)(raw, cores);
125
+ if (value === null) {
126
+ console.error(`Invalid worker threads: ${options.workerThreads} (use a whole number from 1 to ${cores}, or auto)`);
127
+ yield (0, exit_1.gracefulExit)(1);
128
+ return;
129
+ }
130
+ (0, index_config_1.writeGlobalConfig)(Object.assign(Object.assign({}, globalConfig), { workerThreads: value }));
131
+ globalConfig.workerThreads = value;
132
+ const effective = (0, config_1.resolveWorkerThreads)({
133
+ env: process.env.GMAX_WORKER_THREADS,
134
+ configValue: value,
135
+ cores,
136
+ });
137
+ console.log(value === undefined
138
+ ? `Worker threads set to auto (${(0, config_1.defaultWorkerThreads)(cores)} on this machine).`
139
+ : `Worker threads set to ${value}.`);
140
+ if (effective.source === "env") {
141
+ console.log(`GMAX_WORKER_THREADS=${process.env.GMAX_WORKER_THREADS} is set in this shell and overrides it here.`);
142
+ }
143
+ console.log("Takes effect when the gmax daemon restarts: gmax watch stop, then gmax watch --daemon -b.");
144
+ if (options.queryLog === undefined &&
145
+ !options.embedMode &&
146
+ !options.modelTier) {
147
+ yield (0, exit_1.gracefulExit)();
148
+ return;
149
+ }
150
+ }
78
151
  // Handle query-log toggle (independent of model/embed changes)
79
152
  if (options.queryLog !== undefined) {
80
153
  if (!["on", "off"].includes(options.queryLog)) {
@@ -103,6 +176,7 @@ Examples:
103
176
  embedMode: newMode,
104
177
  mlxModel: newMode === "gpu" ? tier.mlxModel : undefined,
105
178
  queryLog: globalConfig.queryLog,
179
+ workerThreads: globalConfig.workerThreads,
106
180
  });
107
181
  (0, index_config_1.writeSetupConfig)(paths.configPath, {
108
182
  embedMode: newMode,
@@ -43,6 +43,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.status = void 0;
46
+ exports.buildStatusJson = buildStatusJson;
47
+ const node_child_process_1 = require("node:child_process");
46
48
  const os = __importStar(require("node:os"));
47
49
  const commander_1 = require("commander");
48
50
  const config_1 = require("../config");
@@ -89,7 +91,17 @@ function loadStatusView(projects) {
89
91
  chunkCounts.set(project.root, stats.chunks);
90
92
  }
91
93
  }
92
- return { watchers, chunkCounts };
94
+ const daemon = {
95
+ pid: typeof resp.pid === "number" ? resp.pid : null,
96
+ uptimeSec: typeof resp.uptime === "number" ? resp.uptime : null,
97
+ // Older daemons do not report these.
98
+ workers: typeof resp.workers === "number" ? resp.workers : null,
99
+ workerThreads: resp.workerThreads &&
100
+ typeof resp.workerThreads.value === "number"
101
+ ? resp.workerThreads.value
102
+ : null,
103
+ };
104
+ return { watchers, chunkCounts, daemon };
93
105
  }),
94
106
  inProcess: () => __awaiter(this, void 0, void 0, function* () {
95
107
  (0, watcher_store_1.listWatchers)(); // cleans stale entries as side effect
@@ -129,6 +141,86 @@ function loadStatusView(projects) {
129
141
  });
130
142
  });
131
143
  }
144
+ /** State shown per project; shared by --agent and --json. */
145
+ function projectState(project, watcher) {
146
+ var _a;
147
+ const projectStatus = (_a = project.status) !== null && _a !== void 0 ? _a : "indexed";
148
+ if (projectStatus === "pending")
149
+ return "pending";
150
+ if (projectStatus === "error")
151
+ return "error";
152
+ if ((watcher === null || watcher === void 0 ? void 0 : watcher.status) === "syncing")
153
+ return "indexing";
154
+ if (watcher)
155
+ return "watching";
156
+ return "idle";
157
+ }
158
+ /**
159
+ * Worker processes under the daemon, from the process table. Used only when
160
+ * the daemon is too old to report its pool size.
161
+ */
162
+ function countWorkerProcesses(daemonPid) {
163
+ try {
164
+ const out = (0, node_child_process_1.execFileSync)("ps", ["-axo", "ppid=,command="], {
165
+ encoding: "utf8",
166
+ timeout: 2000,
167
+ });
168
+ let n = 0;
169
+ for (const line of out.split("\n")) {
170
+ const m = line.trim().match(/^(\d+)\s+(\S+)/);
171
+ if (m && Number(m[1]) === daemonPid && m[2] === "gmax-worker")
172
+ n++;
173
+ }
174
+ return n;
175
+ }
176
+ catch (_a) {
177
+ return null;
178
+ }
179
+ }
180
+ function toMs(iso) {
181
+ if (!iso)
182
+ return null;
183
+ const t = Date.parse(iso);
184
+ return Number.isFinite(t) ? t : null;
185
+ }
186
+ function buildStatusJson(input) {
187
+ var _a, _b, _c, _d, _e;
188
+ const now = (_a = input.now) !== null && _a !== void 0 ? _a : Date.now();
189
+ const { view, globalConfig } = input;
190
+ const d = view.daemon;
191
+ let workersRunning = null;
192
+ if (d) {
193
+ workersRunning = (_b = d.workers) !== null && _b !== void 0 ? _b : (d.pid !== null
194
+ ? ((_c = input.countWorkers) !== null && _c !== void 0 ? _c : countWorkerProcesses)(d.pid)
195
+ : null);
196
+ }
197
+ return {
198
+ daemon: {
199
+ running: d !== undefined,
200
+ pid: (_d = d === null || d === void 0 ? void 0 : d.pid) !== null && _d !== void 0 ? _d : null,
201
+ since: (d === null || d === void 0 ? void 0 : d.uptimeSec) != null ? now - d.uptimeSec * 1000 : null,
202
+ workerThreads: (_e = d === null || d === void 0 ? void 0 : d.workerThreads) !== null && _e !== void 0 ? _e : null,
203
+ },
204
+ settings: {
205
+ embedMode: globalConfig.embedMode,
206
+ modelTier: globalConfig.modelTier,
207
+ vectorDim: globalConfig.vectorDim,
208
+ queryLog: globalConfig.queryLog === true,
209
+ workerThreads: input.workerThreads,
210
+ },
211
+ workersRunning,
212
+ indexing: input.indexing,
213
+ projects: input.projects.map((project) => { var _a, _b; return ({
214
+ name: project.name,
215
+ root: project.root,
216
+ chunks: (_b = (_a = view.chunkCounts.get(project.root)) !== null && _a !== void 0 ? _a : project.chunkCount) !== null && _b !== void 0 ? _b : 0,
217
+ indexedAt: toMs(project.lastIndexed),
218
+ state: projectState(project, view.watchers.get(project.root)),
219
+ embedding: (0, embedding_status_1.projectEmbeddingStatus)(project, globalConfig).state,
220
+ }); }),
221
+ at: now,
222
+ };
223
+ }
132
224
  const style = {
133
225
  bold: (s) => `\x1b[1m${s}\x1b[22m`,
134
226
  dim: (s) => `\x1b[2m${s}\x1b[22m`,
@@ -169,12 +261,14 @@ function formatChunks(n) {
169
261
  exports.status = new commander_1.Command("status")
170
262
  .description("Show gmax index status for all projects")
171
263
  .option("--agent", "Compact output for AI agents", false)
264
+ .option("--json", "Machine-readable status: daemon, settings, projects", false)
172
265
  .addHelpText("after", `
173
266
  Examples:
174
267
  gmax status Show status of all indexed projects
268
+ gmax status --json The same, plus daemon and settings, as JSON
175
269
  `)
176
270
  .action((opts) => __awaiter(void 0, void 0, void 0, function* () {
177
- var _a, _b, _c, _d;
271
+ var _a, _b, _c;
178
272
  const globalConfig = (0, index_config_1.readGlobalConfig)();
179
273
  const projects = (0, project_registry_1.listProjects)();
180
274
  const indexing = (0, lock_1.isLocked)(config_1.PATHS.globalRoot);
@@ -192,6 +286,21 @@ Examples:
192
286
  throw err;
193
287
  }
194
288
  const { watchers, chunkCounts } = view;
289
+ if (opts.json) {
290
+ const json = buildStatusJson({
291
+ view,
292
+ projects,
293
+ globalConfig,
294
+ indexing,
295
+ workerThreads: (0, config_1.resolveWorkerThreads)({
296
+ env: process.env.GMAX_WORKER_THREADS,
297
+ configValue: globalConfig.workerThreads,
298
+ }),
299
+ });
300
+ console.log(JSON.stringify(json));
301
+ yield (0, exit_1.gracefulExit)();
302
+ return;
303
+ }
195
304
  if (!opts.agent) {
196
305
  // Header
197
306
  console.log(`\n${style.bold("gmax")} · ${globalConfig.modelTier} (${globalConfig.vectorDim}d, ${globalConfig.embedMode})${indexing ? style.yellow(" · indexing...") : ""}`);
@@ -208,21 +317,9 @@ Examples:
208
317
  }
209
318
  if (opts.agent) {
210
319
  for (const project of projects) {
211
- const watcher = watchers.get(project.root);
212
- const projectStatus = (_a = project.status) !== null && _a !== void 0 ? _a : "indexed";
213
- let st;
214
- if (projectStatus === "pending")
215
- st = "pending";
216
- else if (projectStatus === "error")
217
- st = "error";
218
- else if ((watcher === null || watcher === void 0 ? void 0 : watcher.status) === "syncing")
219
- st = "indexing";
220
- else if (watcher)
221
- st = "watching";
222
- else
223
- st = "idle";
320
+ const st = projectState(project, watchers.get(project.root));
224
321
  const isCurrent = project.root === currentRoot;
225
- const count = (_b = chunkCounts.get(project.root)) !== null && _b !== void 0 ? _b : project.chunkCount;
322
+ const count = (_a = chunkCounts.get(project.root)) !== null && _a !== void 0 ? _a : project.chunkCount;
226
323
  const identity = (0, embedding_status_1.projectEmbeddingStatus)(project, globalConfig);
227
324
  console.log(`${project.name}\t${formatChunks(count)}\t${formatAge(project.lastIndexed)}\t${st}\tembedding=${identity.state}${isCurrent ? "\tcurrent" : ""}`);
228
325
  }
@@ -240,7 +337,7 @@ Examples:
240
337
  const watcher = watchers.get(project.root);
241
338
  // Status column
242
339
  let statusStr;
243
- const projectStatus = (_c = project.status) !== null && _c !== void 0 ? _c : "indexed";
340
+ const projectStatus = (_b = project.status) !== null && _b !== void 0 ? _b : "indexed";
244
341
  if (projectStatus === "pending") {
245
342
  statusStr = style.yellow("pending");
246
343
  }
@@ -257,7 +354,7 @@ Examples:
257
354
  statusStr = style.dim("idle");
258
355
  }
259
356
  // Chunks column
260
- const count = (_d = chunkCounts.get(project.root)) !== null && _d !== void 0 ? _d : project.chunkCount;
357
+ const count = (_c = chunkCounts.get(project.root)) !== null && _c !== void 0 ? _c : project.chunkCount;
261
358
  const chunks = `${formatChunks(count)} chunks`;
262
359
  // Age column
263
360
  const age = formatAge(project.lastIndexed);
@@ -52,6 +52,7 @@ const syncer_1 = require("../lib/index/syncer");
52
52
  const watcher_1 = require("../lib/index/watcher");
53
53
  const meta_cache_1 = require("../lib/store/meta-cache");
54
54
  const vector_db_1 = require("../lib/store/vector-db");
55
+ const daemon_restart_1 = require("../lib/utils/daemon-restart");
55
56
  const exit_1 = require("../lib/utils/exit");
56
57
  const filter_builder_1 = require("../lib/utils/filter-builder");
57
58
  const log_rotate_1 = require("../lib/utils/log-rotate");
@@ -131,20 +132,10 @@ exports.watch = new commander_1.Command("watch")
131
132
  // competitor; the startup code would only kill the busy peer.
132
133
  process.exit(0);
133
134
  }
134
- const logFile = path.join(config_1.PATHS.logsDir, "daemon.log");
135
- const out = (0, log_rotate_1.openRotatedLog)(logFile);
136
- let child;
135
+ const { spawnDaemonProcess } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-launcher")));
136
+ let started;
137
137
  try {
138
- child = (0, node_child_process_1.spawn)(process.argv[0], [process.argv[1], "watch", "--daemon"], {
139
- detached: true,
140
- stdio: ["ignore", out, out],
141
- cwd: process.cwd(),
142
- env: Object.assign(Object.assign({}, process.env), { GMAX_BACKGROUND: "true" }),
143
- });
144
- yield new Promise((resolve, reject) => {
145
- child.once("spawn", resolve);
146
- child.once("error", reject);
147
- });
138
+ started = yield spawnDaemonProcess();
148
139
  }
149
140
  catch (error) {
150
141
  const message = error instanceof Error ? error.message : String(error);
@@ -152,14 +143,7 @@ exports.watch = new commander_1.Command("watch")
152
143
  process.exitCode = 1;
153
144
  return;
154
145
  }
155
- finally {
156
- try {
157
- fs.closeSync(out);
158
- }
159
- catch (_c) { }
160
- }
161
- child.unref();
162
- console.log(`Daemon started (PID: ${child.pid}, log: ${logFile})`);
146
+ console.log(`Daemon started (PID: ${started.pid}, log: ${started.logFile})`);
163
147
  process.exit(0);
164
148
  }
165
149
  // Daemon foreground
@@ -210,7 +194,7 @@ exports.watch = new commander_1.Command("watch")
210
194
  try {
211
195
  yield daemon.shutdown();
212
196
  }
213
- catch (_d) { }
197
+ catch (_c) { }
214
198
  process.exitCode = 1;
215
199
  return;
216
200
  }
@@ -258,7 +242,7 @@ exports.watch = new commander_1.Command("watch")
258
242
  try {
259
243
  fs.closeSync(out);
260
244
  }
261
- catch (_e) { }
245
+ catch (_d) { }
262
246
  }
263
247
  child.unref();
264
248
  console.log(`Watcher started for ${projectName} (PID: ${child.pid}, log: ${logFile})`);
@@ -413,6 +397,85 @@ exports.watch
413
397
  }
414
398
  yield (0, exit_1.gracefulExit)();
415
399
  }));
400
+ exports.watch
401
+ .command("restart")
402
+ .description("Stop the daemon and start a fresh one; waits for the old one to exit")
403
+ .option("--json", "Machine-readable result", false)
404
+ .action((options) => __awaiter(void 0, void 0, void 0, function* () {
405
+ const emit = (result) => {
406
+ if (options.json)
407
+ console.log(JSON.stringify(result));
408
+ else if (result.ok)
409
+ console.log((0, daemon_restart_1.formatRestartResult)(result));
410
+ else
411
+ console.error((0, daemon_restart_1.formatRestartResult)(result));
412
+ };
413
+ const refuse = (error) => {
414
+ emit({
415
+ ok: false,
416
+ previousPid: null,
417
+ stoppedVia: null,
418
+ pid: null,
419
+ version: null,
420
+ workers: null,
421
+ workerThreads: null,
422
+ adopted: false,
423
+ readyMs: null,
424
+ error,
425
+ });
426
+ };
427
+ // Refused before anything is stopped: a daemon that cannot be started
428
+ // again from here must not be taken down.
429
+ if (process.env.GMAX_SECONDARY_STORE === "1") {
430
+ refuse("this project's index lives on an external drive and never runs a daemon");
431
+ yield (0, exit_1.gracefulExit)(2);
432
+ return;
433
+ }
434
+ const { storeWriteDeniedNotice } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/store-access")));
435
+ const denied = storeWriteDeniedNotice();
436
+ if (denied) {
437
+ refuse(denied);
438
+ yield (0, exit_1.gracefulExit)(2);
439
+ return;
440
+ }
441
+ const client = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
442
+ const { spawnDaemonProcess } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-launcher")));
443
+ let parentCmd = "?";
444
+ try {
445
+ const { execSync } = yield Promise.resolve().then(() => __importStar(require("node:child_process")));
446
+ parentCmd = execSync(`ps -o command= -p ${process.ppid}`, {
447
+ encoding: "utf8",
448
+ }).trim();
449
+ }
450
+ catch (_a) { }
451
+ const result = yield (0, daemon_restart_1.restartDaemon)({
452
+ ping: (timeoutMs) => client.sendDaemonCommand({ cmd: "ping" }, { timeoutMs }),
453
+ status: () => client.sendDaemonCommand({ cmd: "status" }),
454
+ requestShutdown: () => client.sendDaemonCommand({
455
+ cmd: "shutdown",
456
+ reason: "gmax-watch-restart",
457
+ from_pid: process.pid,
458
+ from_ppid: process.ppid,
459
+ from_argv: process.argv.slice(0, 4),
460
+ from_parent_cmd: parentCmd,
461
+ }, { timeoutMs: 10000 }),
462
+ liveDaemonPidFromFiles: () => client.isDaemonHeartbeatFresh() ? client.readDaemonPid() : null,
463
+ isAlive: watcher_store_1.isProcessRunning,
464
+ terminate: (pid) => process.kill(pid, "SIGTERM"),
465
+ spawn: () => __awaiter(void 0, void 0, void 0, function* () {
466
+ try {
467
+ return (yield spawnDaemonProcess()).pid;
468
+ }
469
+ catch (_a) {
470
+ return null;
471
+ }
472
+ }),
473
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
474
+ now: () => Date.now(),
475
+ });
476
+ emit(result);
477
+ yield (0, exit_1.gracefulExit)(result.ok ? 0 : 1);
478
+ }));
416
479
  exports.watch
417
480
  .command("stop")
418
481
  .description("Stop watcher for current project")
package/dist/config.js CHANGED
@@ -33,11 +33,15 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.INDEXABLE_EXTENSIONS = exports.STALE_TEMP_FILE_AGE_MS = exports.VERSION_PRUNE_THRESHOLD = exports.FRAGMENT_COMPACT_THRESHOLD = exports.DISK_LOW_BYTES = exports.DISK_CRITICAL_BYTES = exports.MAX_FILE_SIZE_BYTES = exports.PATHS = exports.MAX_WORKER_MEMORY_MB = exports.WORKER_BOOT_TIMEOUT_MS = exports.WORKER_TIMEOUT_MS = exports.REBUILD_COMMAND = exports.CHUNKER_VERSION_HISTORY = exports.CONFIG = exports.MODEL_IDS = exports.DEFAULT_MODEL_TIER = exports.MODEL_TIERS = void 0;
36
+ exports.INDEXABLE_EXTENSIONS = exports.STALE_TEMP_FILE_AGE_MS = exports.VERSION_PRUNE_THRESHOLD = exports.FRAGMENT_COMPACT_THRESHOLD = exports.DISK_LOW_BYTES = exports.DISK_CRITICAL_BYTES = exports.MAX_FILE_SIZE_BYTES = exports.PATHS = exports.MAX_WORKER_MEMORY_MB = exports.WORKER_BOOT_TIMEOUT_MS = exports.WORKER_TIMEOUT_MS = exports.REBUILD_COMMAND = exports.CHUNKER_VERSION_HISTORY = exports.CONFIG = exports.WORKER_THREADS_SETTING = exports.MODEL_IDS = exports.DEFAULT_MODEL_TIER = exports.MODEL_TIERS = void 0;
37
+ exports.defaultWorkerThreads = defaultWorkerThreads;
38
+ exports.parseWorkerThreads = parseWorkerThreads;
39
+ exports.resolveWorkerThreads = resolveWorkerThreads;
37
40
  exports.describeChunkerGap = describeChunkerGap;
38
41
  exports.describeEmbeddingGap = describeEmbeddingGap;
39
42
  exports.describeSchemaDimGap = describeSchemaDimGap;
40
43
  exports.schemaDimAgentRow = schemaDimAgentRow;
44
+ const fs = __importStar(require("node:fs"));
41
45
  const os = __importStar(require("node:os"));
42
46
  const path = __importStar(require("node:path"));
43
47
  exports.MODEL_TIERS = {
@@ -63,27 +67,79 @@ exports.MODEL_IDS = {
63
67
  embed: exports.MODEL_TIERS[exports.DEFAULT_MODEL_TIER].onnxModel,
64
68
  colbert: "ryandono/mxbai-edge-colbert-v0-17m-onnx-int8",
65
69
  };
66
- const DEFAULT_WORKER_THREADS = (() => {
67
- var _a;
68
- const fromEnv = Number.parseInt((_a = process.env.GMAX_WORKER_THREADS) !== null && _a !== void 0 ? _a : "", 10);
69
- if (Number.isFinite(fromEnv) && fromEnv > 0)
70
- return fromEnv;
71
- // Each worker is a separate process that loads the ONNX models, peaking at
72
- // ~800 MB-1 GB. The old max(4, cores/2) let a 14-core host run 7 of them;
73
- // together with the daemon that froze a 48 GB machine. Four is the ceiling;
74
- // two is the floor where the cores exist, so one worker can stay free for
75
- // search. GMAX_WORKER_THREADS still overrides.
76
- const cores = os.cpus().length || 1;
70
+ const HOME = os.homedir();
71
+ // Models, grammars and the pinned HF cache are shared by every store and always
72
+ // live on the internal disk. GMAX_HOME moves only the data root: a secondary
73
+ // store on an external drive (see src/lib/utils/stores.ts) sets it before this
74
+ // module loads, so a project's index can live on the drive with its files.
75
+ const SHARED_ROOT = path.join(HOME, ".gmax");
76
+ const GLOBAL_ROOT = process.env.GMAX_HOME
77
+ ? path.resolve(process.env.GMAX_HOME)
78
+ : SHARED_ROOT;
79
+ function coreCount() {
80
+ return os.cpus().length || 1;
81
+ }
82
+ /**
83
+ * Each worker is a separate process that loads the ONNX models, peaking at
84
+ * ~800 MB-1 GB. The old max(4, cores/2) let a 14-core host run 7 of them;
85
+ * together with the daemon that froze a 48 GB machine. Four is the ceiling;
86
+ * two is the floor where the cores exist, so one worker can stay free for
87
+ * search.
88
+ */
89
+ function defaultWorkerThreads(cores = coreCount()) {
77
90
  const target = Math.min(4, Math.max(2, Math.floor(cores * 0.5)));
78
91
  return Math.max(1, Math.min(target, cores));
79
- })();
92
+ }
93
+ /** A stored or typed worker count is a whole number from 1 to the core count. */
94
+ function parseWorkerThreads(value, cores = coreCount()) {
95
+ const n = typeof value === "number"
96
+ ? value
97
+ : typeof value === "string" && /^\d+$/.test(value.trim())
98
+ ? Number(value.trim())
99
+ : Number.NaN;
100
+ if (!Number.isInteger(n) || n < 1 || n > cores)
101
+ return null;
102
+ return n;
103
+ }
104
+ /**
105
+ * GMAX_WORKER_THREADS wins, then `workerThreads` in ~/.gmax/config.json
106
+ * (`gmax config --worker-threads`), then the computed default. The env var
107
+ * keeps its old meaning (any positive whole number); a stored value outside
108
+ * 1..cores is ignored.
109
+ */
110
+ function resolveWorkerThreads(opts) {
111
+ var _a, _b;
112
+ const cores = (_a = opts.cores) !== null && _a !== void 0 ? _a : coreCount();
113
+ const fromEnv = Number.parseInt((_b = opts.env) !== null && _b !== void 0 ? _b : "", 10);
114
+ if (Number.isFinite(fromEnv) && fromEnv > 0) {
115
+ return { value: fromEnv, source: "env" };
116
+ }
117
+ const fromConfig = parseWorkerThreads(opts.configValue, cores);
118
+ if (fromConfig !== null)
119
+ return { value: fromConfig, source: "config" };
120
+ return { value: defaultWorkerThreads(cores), source: "default" };
121
+ }
122
+ function storedWorkerThreads() {
123
+ try {
124
+ const raw = fs.readFileSync(path.join(GLOBAL_ROOT, "config.json"), "utf-8");
125
+ return JSON.parse(raw).workerThreads;
126
+ }
127
+ catch (_a) {
128
+ return undefined;
129
+ }
130
+ }
131
+ /** Read once at load: a running daemon keeps the value it started with. */
132
+ exports.WORKER_THREADS_SETTING = resolveWorkerThreads({
133
+ env: process.env.GMAX_WORKER_THREADS,
134
+ configValue: storedWorkerThreads(),
135
+ });
80
136
  exports.CONFIG = {
81
137
  VECTOR_DIM: 384,
82
138
  COLBERT_DIM: 48,
83
139
  MAX_CHUNK_CHARS: 2000,
84
140
  MAX_CHUNK_LINES: 75,
85
141
  EMBED_BATCH_SIZE: 24,
86
- WORKER_THREADS: DEFAULT_WORKER_THREADS,
142
+ WORKER_THREADS: exports.WORKER_THREADS_SETTING.value,
87
143
  QUERY_PREFIX: "",
88
144
  // Bump when chunk metadata semantics change in a way that requires a full
89
145
  // reindex to take effect. Must equal the latest entry's `v` in
@@ -217,15 +273,6 @@ exports.MAX_WORKER_MEMORY_MB = (() => {
217
273
  const HARD_CEILING = 4096; // 4GB max per worker regardless of system RAM
218
274
  return Math.min(HARD_CEILING, Math.max(2048, Math.floor((os.totalmem() / 1024 / 1024) * 0.5)));
219
275
  })();
220
- const HOME = os.homedir();
221
- // Models, grammars and the pinned HF cache are shared by every store and always
222
- // live on the internal disk. GMAX_HOME moves only the data root: a secondary
223
- // store on an external drive (see src/lib/utils/stores.ts) sets it before this
224
- // module loads, so a project's index can live on the drive with its files.
225
- const SHARED_ROOT = path.join(HOME, ".gmax");
226
- const GLOBAL_ROOT = process.env.GMAX_HOME
227
- ? path.resolve(process.env.GMAX_HOME)
228
- : SHARED_ROOT;
229
276
  exports.PATHS = {
230
277
  globalRoot: GLOBAL_ROOT,
231
278
  sharedRoot: SHARED_ROOT,
@@ -844,6 +844,12 @@ class Daemon {
844
844
  uptime() {
845
845
  return Math.floor((Date.now() - this.startTime) / 1000);
846
846
  }
847
+ /** Worker processes alive in the pool right now (0 before it exists). */
848
+ workerCount() {
849
+ var _a;
850
+ var _b;
851
+ return (_b = (_a = this.workerPool) === null || _a === void 0 ? void 0 : _a.getWorkerPids().length) !== null && _b !== void 0 ? _b : 0;
852
+ }
847
853
  /** True once shared resources (LanceDB + MetaCache) are open. */
848
854
  isReady() {
849
855
  return this.ready;
@@ -49,6 +49,7 @@ exports.startHeartbeat = startHeartbeat;
49
49
  exports.handleCommand = handleCommand;
50
50
  const fs = __importStar(require("node:fs"));
51
51
  const path = __importStar(require("node:path"));
52
+ const config_1 = require("../../config");
52
53
  const logger_1 = require("../utils/logger");
53
54
  const operation_coordinator_1 = require("../utils/operation-coordinator");
54
55
  const path_containment_1 = require("../utils/path-containment");
@@ -183,6 +184,8 @@ function handleCommand(daemon, cmd, conn) {
183
184
  leases: daemon.listWatchLeases(),
184
185
  diskPressure: daemon.getDiskPressure(),
185
186
  mlx: daemon.getMlxStatus(),
187
+ workers: daemon.workerCount(),
188
+ workerThreads: config_1.WORKER_THREADS_SETTING,
186
189
  };
187
190
  case "optimize":
188
191
  // Keeps compaction single-writer: see Daemon.runOptimize.
@@ -43,6 +43,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.spawnDaemon = spawnDaemon;
46
+ exports.spawnDaemonProcess = spawnDaemonProcess;
46
47
  const node_child_process_1 = require("node:child_process");
47
48
  const fs = __importStar(require("node:fs"));
48
49
  const path = __importStar(require("node:path"));
@@ -82,3 +83,36 @@ function spawnDaemon() {
82
83
  }
83
84
  });
84
85
  }
86
+ /**
87
+ * Spawn the daemon itself (`watch --daemon`, not the `-b` launcher), detached,
88
+ * logging to daemon.log. Resolves to the daemon's own PID; throws when the
89
+ * spawn fails.
90
+ */
91
+ function spawnDaemonProcess() {
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ const logFile = path.join(config_1.PATHS.logsDir, "daemon.log");
94
+ const out = (0, log_rotate_1.openRotatedLog)(logFile);
95
+ try {
96
+ const child = (0, node_child_process_1.spawn)(process.argv[0], [process.argv[1], "watch", "--daemon"], {
97
+ detached: true,
98
+ stdio: ["ignore", out, out],
99
+ cwd: process.cwd(),
100
+ env: Object.assign(Object.assign({}, process.env), { GMAX_BACKGROUND: "true" }),
101
+ });
102
+ yield new Promise((resolve, reject) => {
103
+ child.once("spawn", resolve);
104
+ child.once("error", reject);
105
+ });
106
+ child.unref();
107
+ if (child.pid === undefined)
108
+ throw new Error("spawned without a PID");
109
+ return { pid: child.pid, logFile };
110
+ }
111
+ finally {
112
+ try {
113
+ fs.closeSync(out);
114
+ }
115
+ catch (_a) { }
116
+ }
117
+ });
118
+ }
@@ -0,0 +1,151 @@
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.restartDaemon = restartDaemon;
13
+ exports.formatRestartResult = formatRestartResult;
14
+ // Matches the daemon's own drain grace (DRAIN_GRACE_MS in daemon-client): a
15
+ // graceful shutdown can legitimately run this long on a large index.
16
+ const DEFAULT_EXIT_TIMEOUT_MS = 90000;
17
+ const DEFAULT_READY_TIMEOUT_MS = 60000;
18
+ const DEFAULT_POLL_MS = 200;
19
+ function pidOf(resp) {
20
+ return typeof resp.pid === "number" && resp.pid > 0 ? resp.pid : null;
21
+ }
22
+ function restartDaemon(deps_1) {
23
+ return __awaiter(this, arguments, void 0, function* (deps, opts = {}) {
24
+ var _a, _b, _c, _d;
25
+ const exitTimeoutMs = (_a = opts.exitTimeoutMs) !== null && _a !== void 0 ? _a : DEFAULT_EXIT_TIMEOUT_MS;
26
+ const readyTimeoutMs = (_b = opts.readyTimeoutMs) !== null && _b !== void 0 ? _b : DEFAULT_READY_TIMEOUT_MS;
27
+ const pollMs = (_c = opts.pollMs) !== null && _c !== void 0 ? _c : DEFAULT_POLL_MS;
28
+ const result = {
29
+ ok: false,
30
+ previousPid: null,
31
+ stoppedVia: null,
32
+ pid: null,
33
+ version: null,
34
+ workers: null,
35
+ workerThreads: null,
36
+ adopted: false,
37
+ readyMs: null,
38
+ };
39
+ // A busy daemon can miss a short ping while its heartbeat is still fresh;
40
+ // the files catch that case so it is stopped rather than raced.
41
+ const first = yield deps.ping(5000);
42
+ const previousPid = first.ok
43
+ ? ((_d = pidOf(first)) !== null && _d !== void 0 ? _d : deps.liveDaemonPidFromFiles())
44
+ : deps.liveDaemonPidFromFiles();
45
+ result.previousPid = previousPid;
46
+ if (first.ok && previousPid === null) {
47
+ result.error =
48
+ "a daemon answered but did not name its PID; stop it with `gmax watch stop` first";
49
+ return result;
50
+ }
51
+ if (previousPid !== null) {
52
+ const shutdown = yield deps.requestShutdown();
53
+ if (shutdown.ok) {
54
+ result.stoppedVia = "ipc";
55
+ }
56
+ else if (deps.isAlive(previousPid)) {
57
+ // SIGTERM runs the same graceful shutdown as the IPC verb.
58
+ try {
59
+ deps.terminate(previousPid);
60
+ result.stoppedVia = "sigterm";
61
+ }
62
+ catch (_e) { }
63
+ }
64
+ const deadline = deps.now() + exitTimeoutMs;
65
+ while (deps.isAlive(previousPid)) {
66
+ if (deps.now() >= deadline) {
67
+ result.error = `the daemon (PID ${previousPid}) did not exit within ${Math.round(exitTimeoutMs / 1000)}s; it is still running and no second daemon was started`;
68
+ return result;
69
+ }
70
+ yield deps.sleep(pollMs);
71
+ }
72
+ }
73
+ const startedAt = deps.now();
74
+ // Something else (a session hook, an MCP server) may have started a daemon
75
+ // in the gap. Spawning beside it would only lose the lock race, so use it.
76
+ const gap = yield deps.ping(2000);
77
+ let spawnedPid = null;
78
+ if (gap.ok && pidOf(gap) !== previousPid) {
79
+ result.adopted = true;
80
+ }
81
+ else {
82
+ spawnedPid = yield deps.spawn();
83
+ if (spawnedPid === null) {
84
+ result.error = "the new daemon could not be spawned";
85
+ return result;
86
+ }
87
+ }
88
+ const readyBy = startedAt + readyTimeoutMs;
89
+ for (;;) {
90
+ const resp = yield deps.ping(2000);
91
+ const pid = pidOf(resp);
92
+ if (resp.ok && pid !== null && pid !== previousPid) {
93
+ if (resp.ready !== false) {
94
+ result.pid = pid;
95
+ result.version = typeof resp.version === "string" ? resp.version : null;
96
+ if (spawnedPid !== null && pid !== spawnedPid)
97
+ result.adopted = true;
98
+ break;
99
+ }
100
+ }
101
+ else if (spawnedPid !== null && !resp.ok && !deps.isAlive(spawnedPid)) {
102
+ result.error = `the new daemon (PID ${spawnedPid}) exited during startup; see ~/.gmax/logs/daemon.log`;
103
+ return result;
104
+ }
105
+ if (deps.now() >= readyBy) {
106
+ result.error = `the new daemon did not become ready within ${Math.round(readyTimeoutMs / 1000)}s${spawnedPid !== null ? ` (PID ${spawnedPid})` : ""}`;
107
+ result.pid = spawnedPid;
108
+ return result;
109
+ }
110
+ yield deps.sleep(pollMs);
111
+ }
112
+ result.readyMs = deps.now() - startedAt;
113
+ const status = yield deps.status();
114
+ if (status.ok) {
115
+ if (typeof status.workers === "number")
116
+ result.workers = status.workers;
117
+ const threads = status.workerThreads;
118
+ if (threads && typeof threads.value === "number") {
119
+ result.workerThreads = threads.value;
120
+ }
121
+ }
122
+ result.ok = true;
123
+ return result;
124
+ });
125
+ }
126
+ function formatRestartResult(r) {
127
+ var _a;
128
+ const lines = [];
129
+ if (r.previousPid !== null) {
130
+ lines.push(`Stopped daemon (PID ${r.previousPid}).`);
131
+ }
132
+ else if (r.ok || r.pid !== null) {
133
+ lines.push("No daemon was running.");
134
+ }
135
+ if (!r.ok) {
136
+ lines.push(`Restart failed: ${(_a = r.error) !== null && _a !== void 0 ? _a : "unknown error"}.`);
137
+ return lines.join("\n");
138
+ }
139
+ const parts = [`PID ${r.pid}`];
140
+ if (r.version)
141
+ parts.push(`v${r.version}`);
142
+ if (r.workerThreads !== null)
143
+ parts.push(`${r.workerThreads} worker threads`);
144
+ if (r.workers !== null) {
145
+ parts.push(`${r.workers} worker${r.workers === 1 ? "" : "s"} running`);
146
+ }
147
+ if (r.readyMs !== null)
148
+ parts.push(`ready in ${(r.readyMs / 1000).toFixed(1)}s`);
149
+ lines.push(`${r.adopted ? "Daemon already started by another caller" : "Daemon started"} (${parts.join(", ")}).`);
150
+ return lines.join("\n");
151
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.35",
3
+ "version": "0.26.37",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.35",
3
+ "version": "0.26.37",
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",