grepmax 0.26.34 → 0.26.36

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.34",
13
+ "version": "0.26.36",
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.
@@ -423,7 +425,7 @@ fixtures/
423
425
  | Variable | Description | Default |
424
426
  | --- | --- | --- |
425
427
  | `GMAX_EMBED_MODE` | Force `cpu` or `gpu` | Auto-detect |
426
- | `GMAX_WORKER_THREADS` | Worker threads for embedding | 50% of cores |
428
+ | `GMAX_WORKER_THREADS` | Worker processes for embedding; overrides `gmax config --worker-threads` | `min(4, max(2, cores/2))` |
427
429
  | `GMAX_WORKER_RSS_RECYCLE_MB` | Recycle workers that remain above this RSS; `0` disables the check | `1536` |
428
430
  | `GMAX_DEBUG` | Debug logging | Off |
429
431
  | `GMAX_SUMMARIZER` | Enable summarizer auto-start (`1`) | Off |
package/dist/bin.js CHANGED
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  exports.commandTarget = commandTarget;
38
+ exports.registeredForm = registeredForm;
38
39
  // Entry point. Picks the gmax home before anything reads config.ts: a command
39
40
  // aimed at a project under a secondary store's prefix (src/lib/utils/stores.ts)
40
41
  // runs against that store, in-process, so the drive it lives on can always
@@ -87,6 +88,46 @@ function selectHome() {
87
88
  process.env.GMAX_NO_AUTOSTART = "1";
88
89
  process.env.GMAX_SECONDARY_STORE = "1";
89
90
  seedConfig(match.store.home);
91
+ alignCwdWithRegistry(match.store.home);
92
+ }
93
+ // Node reports the physical working directory, so a shell standing in
94
+ // ~/Development/packages/x (a symlink onto the drive) looks like
95
+ // /Volumes/External/dev/packages/x, while the store registered the path it was
96
+ // given. When the physical cwd sits inside a registered root's realpath,
97
+ // report the cwd in the registered form so project lookup finds it.
98
+ function registeredForm(cwd, roots, real = realpathOr) {
99
+ for (const root of roots) {
100
+ const rootReal = real(root);
101
+ const rel = path.relative(rootReal, cwd);
102
+ if (rel === "" || (rel && !rel.startsWith("..") && !path.isAbsolute(rel))) {
103
+ return rel ? path.join(root, rel) : root;
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+ function realpathOr(p) {
109
+ try {
110
+ return fs.realpathSync(p);
111
+ }
112
+ catch (_a) {
113
+ return path.resolve(p);
114
+ }
115
+ }
116
+ function alignCwdWithRegistry(home) {
117
+ let roots;
118
+ try {
119
+ const parsed = JSON.parse(fs.readFileSync(path.join(home, "projects.json"), "utf-8"));
120
+ roots = parsed
121
+ .map((p) => p.root)
122
+ .filter((r) => typeof r === "string");
123
+ }
124
+ catch (_a) {
125
+ return;
126
+ }
127
+ const physical = process.cwd();
128
+ const logical = registeredForm(physical, roots);
129
+ if (logical && logical !== physical)
130
+ process.cwd = () => logical;
90
131
  }
91
132
  // A new store embeds with the same model as the primary one.
92
133
  function seedConfig(home) {
@@ -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);
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.34",
3
+ "version": "0.26.36",
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.34",
3
+ "version": "0.26.36",
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",