grepmax 0.8.0 → 0.8.2

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.
@@ -43,10 +43,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.add = void 0;
46
- const node_child_process_1 = require("node:child_process");
47
46
  const path = __importStar(require("node:path"));
48
47
  const commander_1 = require("commander");
49
48
  const grammar_loader_1 = require("../lib/index/grammar-loader");
49
+ const index_config_1 = require("../lib/index/index-config");
50
50
  const sync_helpers_1 = require("../lib/index/sync-helpers");
51
51
  const syncer_1 = require("../lib/index/syncer");
52
52
  const setup_helpers_1 = require("../lib/setup/setup-helpers");
@@ -55,7 +55,7 @@ const exit_1 = require("../lib/utils/exit");
55
55
  const project_marker_1 = require("../lib/utils/project-marker");
56
56
  const project_registry_1 = require("../lib/utils/project-registry");
57
57
  const project_root_1 = require("../lib/utils/project-root");
58
- const index_config_1 = require("../lib/index/index-config");
58
+ const watcher_launcher_1 = require("../lib/utils/watcher-launcher");
59
59
  exports.add = new commander_1.Command("add")
60
60
  .description("Add a project to the gmax index")
61
61
  .argument("[dir]", "Directory to add (defaults to current directory)")
@@ -75,11 +75,27 @@ Examples:
75
75
  const projectName = path.basename(projectRoot);
76
76
  // Check if already registered
77
77
  const existing = (0, project_registry_1.getProject)(projectRoot);
78
- if (existing || (0, project_marker_1.hasMarker)(projectRoot)) {
79
- console.log(`${projectName} is already added (${(_b = existing === null || existing === void 0 ? void 0 : existing.chunkCount) !== null && _b !== void 0 ? _b : 0} chunks).`);
78
+ if (existing) {
79
+ console.log(`${projectName} is already added (${(_b = existing.chunkCount) !== null && _b !== void 0 ? _b : 0} chunks).`);
80
80
  console.log(`Run \`gmax index\` to re-index, or \`gmax index --reset\` for a full rebuild.`);
81
81
  return;
82
82
  }
83
+ // Check if a parent project already covers this path
84
+ const parent = (0, project_registry_1.getParentProject)(projectRoot);
85
+ if (parent) {
86
+ console.log(`Already covered by ${path.basename(parent.root)} (${parent.root}).`);
87
+ console.log(`Use \`gmax status\` to see indexed projects.`);
88
+ return;
89
+ }
90
+ // If this is a parent of existing projects, absorb them
91
+ const children = (0, project_registry_1.getChildProjects)(projectRoot);
92
+ if (children.length > 0) {
93
+ const names = children.map((c) => c.name).join(", ");
94
+ console.log(`Absorbing ${children.length} sub-project(s): ${names}`);
95
+ for (const child of children) {
96
+ (0, project_registry_1.removeProject)(child.root);
97
+ }
98
+ }
83
99
  // Create marker file
84
100
  (0, project_marker_1.createMarker)(projectRoot);
85
101
  // Register as pending
@@ -109,11 +125,21 @@ Examples:
109
125
  projectRoot,
110
126
  onProgress,
111
127
  });
128
+ // Update registry: pending → indexed
129
+ (0, project_registry_1.registerProject)({
130
+ root: projectRoot,
131
+ name: projectName,
132
+ vectorDim: globalConfig.vectorDim,
133
+ modelTier: globalConfig.modelTier,
134
+ embedMode: globalConfig.embedMode,
135
+ lastIndexed: new Date().toISOString(),
136
+ chunkCount: result.indexed,
137
+ status: "indexed",
138
+ });
112
139
  const failedSuffix = result.failedFiles > 0 ? ` · ${result.failedFiles} failed` : "";
113
140
  spinner.succeed(`Added ${projectName} (${result.total} files, ${result.indexed} chunks${failedSuffix})`);
114
141
  }
115
142
  catch (e) {
116
- // Update status to error
117
143
  (0, project_registry_1.registerProject)({
118
144
  root: projectRoot,
119
145
  name: projectName,
@@ -127,14 +153,13 @@ Examples:
127
153
  spinner.fail(`Failed to index ${projectName}`);
128
154
  throw e;
129
155
  }
130
- // Start watcher in background
131
- try {
132
- const child = (0, node_child_process_1.spawn)(process.argv[0], [process.argv[1], "watch", "--path", projectRoot], { detached: true, stdio: "ignore" });
133
- child.unref();
134
- console.log(`Watcher started (PID: ${child.pid})`);
156
+ // Start watcher
157
+ const launched = (0, watcher_launcher_1.launchWatcher)(projectRoot);
158
+ if (launched.ok) {
159
+ console.log(`Watcher started (PID: ${launched.pid})`);
135
160
  }
136
- catch (_c) {
137
- console.log(`Note: could not start watcher. Run: gmax watch --path ${projectRoot} -b`);
161
+ else if (launched.reason === "spawn-failed") {
162
+ console.warn(`[add] ${launched.message}`);
138
163
  }
139
164
  }
140
165
  catch (error) {
@@ -147,7 +172,7 @@ Examples:
147
172
  try {
148
173
  yield vectorDb.close();
149
174
  }
150
- catch (_d) { }
175
+ catch (_c) { }
151
176
  }
152
177
  yield (0, exit_1.gracefulExit)();
153
178
  }
@@ -130,7 +130,10 @@ function installPlugin() {
130
130
  const startScript = `
131
131
  const { spawn } = require("child_process");
132
132
  const fs = require("fs");
133
- const out = fs.openSync("/tmp/gmax.log", "a");
133
+ const path = require("path");
134
+ const logDir = path.join(require("os").homedir(), ".gmax", "logs");
135
+ fs.mkdirSync(logDir, { recursive: true });
136
+ const out = fs.openSync(path.join(logDir, "gmax.log"), "a");
134
137
  const child = spawn("gmax", ["serve"], { detached: true, stdio: ["ignore", out, out] });
135
138
  child.unref();
136
139
  `;
@@ -43,17 +43,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.index = void 0;
46
- const node_child_process_1 = require("node:child_process");
47
46
  const path = __importStar(require("node:path"));
48
47
  const commander_1 = require("commander");
48
+ const index_config_1 = require("../lib/index/index-config");
49
49
  const grammar_loader_1 = require("../lib/index/grammar-loader");
50
50
  const sync_helpers_1 = require("../lib/index/sync-helpers");
51
51
  const syncer_1 = require("../lib/index/syncer");
52
52
  const setup_helpers_1 = require("../lib/setup/setup-helpers");
53
53
  const vector_db_1 = require("../lib/store/vector-db");
54
54
  const exit_1 = require("../lib/utils/exit");
55
+ const project_registry_1 = require("../lib/utils/project-registry");
55
56
  const project_root_1 = require("../lib/utils/project-root");
56
- const watcher_registry_1 = require("../lib/utils/watcher-registry");
57
+ const watcher_launcher_1 = require("../lib/utils/watcher-launcher");
58
+ const watcher_store_1 = require("../lib/utils/watcher-store");
57
59
  exports.index = new commander_1.Command("index")
58
60
  .description("Index the current directory and create searchable store")
59
61
  .option("-d, --dry-run", "Dry run the indexing process (no actual file syncing)", false)
@@ -87,6 +89,12 @@ Examples:
87
89
  ? path.resolve(options.path)
88
90
  : process.cwd();
89
91
  const projectRoot = (_a = (0, project_root_1.findProjectRoot)(indexRoot)) !== null && _a !== void 0 ? _a : indexRoot;
92
+ // Project must be registered before reindexing
93
+ if (!(0, project_registry_1.getProject)(projectRoot)) {
94
+ console.error(`This project hasn't been added yet.\n\nRun: gmax add ${projectRoot}\n`);
95
+ process.exitCode = 1;
96
+ return;
97
+ }
90
98
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
91
99
  vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
92
100
  if (options.reset) {
@@ -97,7 +105,7 @@ Examples:
97
105
  // Ensure grammars are present before indexing (silent if already exist)
98
106
  yield (0, grammar_loader_1.ensureGrammars)(console.log, { silent: true });
99
107
  // Stop any watcher that covers this project — it holds the shared lock
100
- const watcher = (0, watcher_registry_1.getWatcherCoveringPath)(projectRoot);
108
+ const watcher = (0, watcher_store_1.getWatcherCoveringPath)(projectRoot);
101
109
  let restartWatcher = null;
102
110
  if (watcher) {
103
111
  console.log(`Stopping watcher (PID: ${watcher.pid}) for ${path.basename(watcher.projectRoot)}...`);
@@ -107,11 +115,11 @@ Examples:
107
115
  catch (_b) { }
108
116
  // Wait for process to exit (up to 5s)
109
117
  for (let i = 0; i < 50; i++) {
110
- if (!(0, watcher_registry_1.isProcessRunning)(watcher.pid))
118
+ if (!(0, watcher_store_1.isProcessRunning)(watcher.pid))
111
119
  break;
112
120
  yield new Promise((r) => setTimeout(r, 100));
113
121
  }
114
- (0, watcher_registry_1.unregisterWatcher)(watcher.pid);
122
+ (0, watcher_store_1.unregisterWatcher)(watcher.pid);
115
123
  restartWatcher = {
116
124
  pid: watcher.pid,
117
125
  projectRoot: watcher.projectRoot,
@@ -138,6 +146,18 @@ Examples:
138
146
  }));
139
147
  return;
140
148
  }
149
+ // Update registry with new stats
150
+ const globalConfig = (0, index_config_1.readGlobalConfig)();
151
+ (0, project_registry_1.registerProject)({
152
+ root: projectRoot,
153
+ name: path.basename(projectRoot),
154
+ vectorDim: globalConfig.vectorDim,
155
+ modelTier: globalConfig.modelTier,
156
+ embedMode: globalConfig.embedMode,
157
+ lastIndexed: new Date().toISOString(),
158
+ chunkCount: result.indexed,
159
+ status: "indexed",
160
+ });
141
161
  const failedSuffix = result.failedFiles > 0 ? ` • ${result.failedFiles} failed` : "";
142
162
  spinner.succeed(`Indexing complete(${result.processed} / ${result.total}) • indexed ${result.indexed}${failedSuffix} `);
143
163
  }
@@ -148,13 +168,12 @@ Examples:
148
168
  finally {
149
169
  // Restart the watcher if we stopped one
150
170
  if (restartWatcher) {
151
- try {
152
- const child = (0, node_child_process_1.spawn)(process.argv[0], [process.argv[1], "watch", "--path", restartWatcher.projectRoot], { detached: true, stdio: "ignore" });
153
- child.unref();
154
- console.log(`Restarted watcher for ${path.basename(restartWatcher.projectRoot)} (PID: ${child.pid})`);
171
+ const launched = (0, watcher_launcher_1.launchWatcher)(restartWatcher.projectRoot);
172
+ if (launched.ok) {
173
+ console.log(`Restarted watcher for ${path.basename(restartWatcher.projectRoot)} (PID: ${launched.pid})`);
155
174
  }
156
- catch (_c) {
157
- console.log(`Note: could not restart watcher. Run: gmax watch --path ${restartWatcher.projectRoot} -b`);
175
+ else if (launched.reason === "spawn-failed") {
176
+ console.warn(`[index] ${launched.message}`);
158
177
  }
159
178
  }
160
179
  }
@@ -77,7 +77,8 @@ const format_helpers_1 = require("../lib/utils/format-helpers");
77
77
  const import_extractor_1 = require("../lib/utils/import-extractor");
78
78
  const project_registry_1 = require("../lib/utils/project-registry");
79
79
  const project_root_1 = require("../lib/utils/project-root");
80
- const watcher_registry_1 = require("../lib/utils/watcher-registry");
80
+ const watcher_launcher_1 = require("../lib/utils/watcher-launcher");
81
+ const watcher_store_1 = require("../lib/utils/watcher-store");
81
82
  // ---------------------------------------------------------------------------
82
83
  // Tool definitions
83
84
  // ---------------------------------------------------------------------------
@@ -331,8 +332,8 @@ exports.mcp = new commander_1.Command("mcp")
331
332
  return;
332
333
  _indexing = true;
333
334
  _indexProgress = "starting...";
334
- console.log("[MCP] First-time index for this project...");
335
- const child = (0, node_child_process_1.spawn)(process.argv[0], [process.argv[1], "index", "--path", projectRoot], { detached: true, stdio: "ignore" });
335
+ console.log("[MCP] First-time setup for this project...");
336
+ const child = (0, node_child_process_1.spawn)(process.argv[0], [process.argv[1], "add", projectRoot], { detached: true, stdio: "ignore" });
336
337
  _indexChildPid = (_a = child.pid) !== null && _a !== void 0 ? _a : null;
337
338
  child.unref();
338
339
  _indexProgress = `PID ${_indexChildPid}`;
@@ -342,7 +343,7 @@ exports.mcp = new commander_1.Command("mcp")
342
343
  _indexChildPid = null;
343
344
  if (code === 0) {
344
345
  _indexReady = true;
345
- console.log("[MCP] First-time indexing complete.");
346
+ console.log("[MCP] First-time setup complete.");
346
347
  }
347
348
  else {
348
349
  console.error(`[MCP] Indexing failed (exit code: ${code})`);
@@ -352,14 +353,10 @@ exports.mcp = new commander_1.Command("mcp")
352
353
  }
353
354
  // --- Background watcher ---
354
355
  function ensureWatcher() {
355
- if ((0, watcher_registry_1.getWatcherCoveringPath)(projectRoot))
356
- return;
357
- const child = (0, node_child_process_1.spawn)("gmax", ["watch", "-b", "--path", projectRoot], {
358
- detached: true,
359
- stdio: "ignore",
360
- });
361
- child.unref();
362
- console.log(`[MCP] Started background watcher for ${projectRoot}`);
356
+ const result = (0, watcher_launcher_1.launchWatcher)(projectRoot);
357
+ if (result.ok && !result.reused) {
358
+ console.log(`[MCP] Started background watcher for ${projectRoot} (PID: ${result.pid})`);
359
+ }
363
360
  }
364
361
  // --- Tool handlers ---
365
362
  function handleSemanticSearch(args_1) {
@@ -611,6 +608,7 @@ exports.mcp = new commander_1.Command("mcp")
611
608
  }
612
609
  function handleCodeSkeleton(args) {
613
610
  return __awaiter(this, void 0, void 0, function* () {
611
+ ensureWatcher();
614
612
  const target = String(args.target || "");
615
613
  if (!target)
616
614
  return err("Missing required parameter: target");
@@ -725,6 +723,7 @@ exports.mcp = new commander_1.Command("mcp")
725
723
  }
726
724
  function handleTraceCalls(args) {
727
725
  return __awaiter(this, void 0, void 0, function* () {
726
+ ensureWatcher();
728
727
  const symbol = String(args.symbol || "");
729
728
  if (!symbol)
730
729
  return err("Missing required parameter: symbol");
@@ -806,6 +805,7 @@ exports.mcp = new commander_1.Command("mcp")
806
805
  }
807
806
  function handleListSymbols(args) {
808
807
  return __awaiter(this, void 0, void 0, function* () {
808
+ ensureWatcher();
809
809
  const pattern = typeof args.pattern === "string" ? args.pattern : undefined;
810
810
  const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 100);
811
811
  const pathPrefix = typeof args.path === "string" ? args.path : undefined;
@@ -892,7 +892,7 @@ exports.mcp = new commander_1.Command("mcp")
892
892
  const stats = yield db.getStats();
893
893
  const fileCount = yield db.getDistinctFileCount();
894
894
  // Watcher status
895
- const watcher = (0, watcher_registry_1.getWatcherCoveringPath)(projectRoot);
895
+ const watcher = (0, watcher_store_1.getWatcherCoveringPath)(projectRoot);
896
896
  let watcherLine = "Watcher: not running";
897
897
  if (watcher) {
898
898
  const status = (_a = watcher.status) !== null && _a !== void 0 ? _a : "unknown";
@@ -1096,6 +1096,7 @@ exports.mcp = new commander_1.Command("mcp")
1096
1096
  }
1097
1097
  function handleRelatedFiles(args) {
1098
1098
  return __awaiter(this, void 0, void 0, function* () {
1099
+ ensureWatcher();
1099
1100
  const file = String(args.file || "");
1100
1101
  if (!file)
1101
1102
  return err("Missing required parameter: file");
@@ -1198,6 +1199,7 @@ exports.mcp = new commander_1.Command("mcp")
1198
1199
  function handleRecentChanges(args) {
1199
1200
  return __awaiter(this, void 0, void 0, function* () {
1200
1201
  var _a, e_1, _b, _c;
1202
+ ensureWatcher();
1201
1203
  const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 50);
1202
1204
  const root = typeof args.root === "string"
1203
1205
  ? path.resolve(args.root)
@@ -49,10 +49,11 @@ const commander_1 = require("commander");
49
49
  const meta_cache_1 = require("../lib/store/meta-cache");
50
50
  const vector_db_1 = require("../lib/store/vector-db");
51
51
  const exit_1 = require("../lib/utils/exit");
52
+ const process_1 = require("../lib/utils/process");
52
53
  const project_marker_1 = require("../lib/utils/project-marker");
53
54
  const project_registry_1 = require("../lib/utils/project-registry");
54
55
  const project_root_1 = require("../lib/utils/project-root");
55
- const watcher_registry_1 = require("../lib/utils/watcher-registry");
56
+ const watcher_store_1 = require("../lib/utils/watcher-store");
56
57
  function confirm(message) {
57
58
  const rl = readline.createInterface({
58
59
  input: process.stdin,
@@ -99,19 +100,11 @@ Examples:
99
100
  }
100
101
  }
101
102
  // Stop any watcher
102
- const watcher = (0, watcher_registry_1.getWatcherForProject)(projectRoot);
103
+ const watcher = (0, watcher_store_1.getWatcherForProject)(projectRoot);
103
104
  if (watcher) {
104
105
  console.log(`Stopping watcher (PID: ${watcher.pid})...`);
105
- try {
106
- process.kill(watcher.pid, "SIGTERM");
107
- }
108
- catch (_b) { }
109
- for (let i = 0; i < 50; i++) {
110
- if (!(0, watcher_registry_1.isProcessRunning)(watcher.pid))
111
- break;
112
- yield new Promise((r) => setTimeout(r, 100));
113
- }
114
- (0, watcher_registry_1.unregisterWatcher)(watcher.pid);
106
+ yield (0, process_1.killProcess)(watcher.pid);
107
+ (0, watcher_store_1.unregisterWatcher)(watcher.pid);
115
108
  }
116
109
  // Delete vectors from LanceDB
117
110
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
@@ -139,13 +132,13 @@ Examples:
139
132
  try {
140
133
  metaCache.close();
141
134
  }
142
- catch (_c) { }
135
+ catch (_b) { }
143
136
  }
144
137
  if (vectorDb) {
145
138
  try {
146
139
  yield vectorDb.close();
147
140
  }
148
- catch (_d) { }
141
+ catch (_c) { }
149
142
  }
150
143
  yield (0, exit_1.gracefulExit)();
151
144
  }
@@ -452,20 +452,18 @@ Examples:
452
452
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
453
453
  // Propagate project root to worker processes
454
454
  process.env.GMAX_PROJECT_ROOT = projectRoot;
455
- // Check if project is registered (skip for --sync which auto-indexes)
456
- if (!options.sync) {
457
- const checkRoot = options.root
458
- ? (_c = (0, project_root_1.findProjectRoot)(path.resolve(options.root))) !== null && _c !== void 0 ? _c : path.resolve(options.root)
459
- : projectRoot;
460
- const project = (0, project_registry_1.getProject)(checkRoot);
461
- if (!project) {
462
- console.error(`This project hasn't been added to gmax yet.\n\nRun: gmax add ${checkRoot}\n`);
463
- process.exitCode = 1;
464
- return;
465
- }
466
- if (project.status === "pending") {
467
- console.warn("This project is still being indexed. Results may be incomplete.\n");
468
- }
455
+ // Check if project is registered
456
+ const checkRoot = options.root
457
+ ? (_c = (0, project_root_1.findProjectRoot)(path.resolve(options.root))) !== null && _c !== void 0 ? _c : path.resolve(options.root)
458
+ : projectRoot;
459
+ const project = (0, project_registry_1.getProject)(checkRoot);
460
+ if (!project) {
461
+ console.error(`This project hasn't been added to gmax yet.\n\nRun: gmax add ${checkRoot}\n`);
462
+ process.exitCode = 1;
463
+ return;
464
+ }
465
+ if (project.status === "pending") {
466
+ console.warn("This project is still being indexed. Results may be incomplete.\n");
469
467
  }
470
468
  vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
471
469
  // Check for active indexing lock and warn if present
@@ -506,6 +504,20 @@ Examples:
506
504
  return;
507
505
  }
508
506
  yield vectorDb.createFTSIndex();
507
+ // Update registry after sync
508
+ const { readGlobalConfig } = yield Promise.resolve().then(() => __importStar(require("../lib/index/index-config")));
509
+ const { registerProject } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/project-registry")));
510
+ const gc = readGlobalConfig();
511
+ registerProject({
512
+ root: projectRoot,
513
+ name: path.basename(projectRoot),
514
+ vectorDim: gc.vectorDim,
515
+ modelTier: gc.modelTier,
516
+ embedMode: gc.embedMode,
517
+ lastIndexed: new Date().toISOString(),
518
+ chunkCount: result.indexed,
519
+ status: "indexed",
520
+ });
509
521
  const failedSuffix = result.failedFiles > 0 ? ` • ${result.failedFiles} failed` : "";
510
522
  spinner.succeed(`${options.sync ? "Indexing" : "Initial indexing"} complete (${result.processed}/${result.total}) • indexed ${result.indexed}${failedSuffix}`);
511
523
  }
@@ -516,15 +528,10 @@ Examples:
516
528
  }
517
529
  // Ensure a watcher is running for live reindexing
518
530
  if (!process.env.VITEST && !((_d = process.env.NODE_ENV) === null || _d === void 0 ? void 0 : _d.includes("test"))) {
519
- try {
520
- const { execFileSync } = yield Promise.resolve().then(() => __importStar(require("node:child_process")));
521
- execFileSync("gmax", ["watch", "-b", "--path", projectRoot], {
522
- timeout: 5000,
523
- stdio: "ignore",
524
- });
525
- }
526
- catch (_v) {
527
- // Watcher may already be running — ignore
531
+ const { launchWatcher } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/watcher-launcher")));
532
+ const launched = launchWatcher(projectRoot);
533
+ if (!launched.ok && launched.reason === "spawn-failed") {
534
+ console.warn(`[search] ${launched.message}`);
528
535
  }
529
536
  }
530
537
  const searcher = new searcher_1.Searcher(vectorDb);
@@ -566,7 +573,7 @@ Examples:
566
573
  return defs.some((d) => regex.test(d));
567
574
  });
568
575
  }
569
- catch (_w) {
576
+ catch (_v) {
570
577
  // Invalid regex — skip
571
578
  }
572
579
  }
@@ -670,7 +677,7 @@ Examples:
670
677
  }
671
678
  }
672
679
  }
673
- catch (_x) { }
680
+ catch (_w) { }
674
681
  }
675
682
  return;
676
683
  }
@@ -772,7 +779,7 @@ Examples:
772
779
  console.log(lines.join("\n"));
773
780
  }
774
781
  }
775
- catch (_y) {
782
+ catch (_x) {
776
783
  // Trace failed — skip silently
777
784
  }
778
785
  }
@@ -59,6 +59,7 @@ const setup_helpers_1 = require("../lib/setup/setup-helpers");
59
59
  const meta_cache_1 = require("../lib/store/meta-cache");
60
60
  const vector_db_1 = require("../lib/store/vector-db");
61
61
  const exit_1 = require("../lib/utils/exit");
62
+ const log_rotate_1 = require("../lib/utils/log-rotate");
62
63
  const project_root_1 = require("../lib/utils/project-root");
63
64
  const server_registry_1 = require("../lib/utils/server-registry");
64
65
  function isMlxServerUp() {
@@ -84,8 +85,7 @@ function startMlxServer(mlxModel) {
84
85
  const serverDir = candidates.find((d) => fs.existsSync(path.join(d, "server.py")));
85
86
  if (!serverDir)
86
87
  return null;
87
- const logPath = "/tmp/mlx-embed-server.log";
88
- const out = fs.openSync(logPath, "a");
88
+ const out = (0, log_rotate_1.openRotatedLog)(path.join(config_1.PATHS.logsDir, "mlx-embed-server.log"));
89
89
  const env = Object.assign({}, process.env);
90
90
  if (mlxModel) {
91
91
  env.MLX_EMBED_MODEL = mlxModel;
@@ -121,14 +121,12 @@ exports.serve = new commander_1.Command("serve")
121
121
  const args = process.argv
122
122
  .slice(2)
123
123
  .filter((arg) => arg !== "-b" && arg !== "--background");
124
- const logDir = path.join(config_1.PATHS.globalRoot, "logs");
125
- fs.mkdirSync(logDir, { recursive: true });
126
124
  const safeName = path
127
125
  .basename(projectRoot)
128
126
  .replace(/[^a-zA-Z0-9._-]/g, "_");
129
- const logFile = path.join(logDir, `server-${safeName}.log`);
130
- const out = fs.openSync(logFile, "a");
131
- const err = fs.openSync(logFile, "a");
127
+ const logFile = path.join(config_1.PATHS.logsDir, `server-${safeName}.log`);
128
+ const out = (0, log_rotate_1.openRotatedLog)(logFile);
129
+ const err = (0, log_rotate_1.openRotatedLog)(logFile);
132
130
  const child = (0, node_child_process_1.spawn)(process.argv[0], [process.argv[1], ...args], {
133
131
  detached: true,
134
132
  stdio: ["ignore", out, err],
@@ -50,7 +50,7 @@ const exit_1 = require("../lib/utils/exit");
50
50
  const lock_1 = require("../lib/utils/lock");
51
51
  const project_registry_1 = require("../lib/utils/project-registry");
52
52
  const project_root_1 = require("../lib/utils/project-root");
53
- const watcher_registry_1 = require("../lib/utils/watcher-registry");
53
+ const watcher_store_1 = require("../lib/utils/watcher-store");
54
54
  const config_1 = require("../config");
55
55
  const style = {
56
56
  bold: (s) => `\x1b[1m${s}\x1b[22m`,
@@ -99,7 +99,7 @@ Examples:
99
99
  var _a;
100
100
  const globalConfig = (0, index_config_1.readGlobalConfig)();
101
101
  const projects = (0, project_registry_1.listProjects)();
102
- (0, watcher_registry_1.listWatchers)(); // cleans stale entries as side effect
102
+ (0, watcher_store_1.listWatchers)(); // cleans stale entries as side effect
103
103
  const indexing = (0, lock_1.isLocked)(config_1.PATHS.globalRoot);
104
104
  const currentRoot = (0, project_root_1.findProjectRoot)(process.cwd());
105
105
  // Header
@@ -114,7 +114,7 @@ Examples:
114
114
  console.log();
115
115
  for (const project of projects) {
116
116
  const isCurrent = project.root === currentRoot;
117
- const watcher = (0, watcher_registry_1.getWatcherForProject)(project.root);
117
+ const watcher = (0, watcher_store_1.getWatcherForProject)(project.root);
118
118
  // Status column
119
119
  let statusStr;
120
120
  const projectStatus = (_a = project.status) !== null && _a !== void 0 ? _a : "indexed";
@@ -44,21 +44,23 @@ 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");
50
+ const index_config_1 = require("../lib/index/index-config");
51
51
  const filter_builder_1 = require("../lib/utils/filter-builder");
52
52
  const syncer_1 = require("../lib/index/syncer");
53
53
  const watcher_1 = require("../lib/index/watcher");
54
54
  const meta_cache_1 = require("../lib/store/meta-cache");
55
55
  const vector_db_1 = require("../lib/store/vector-db");
56
56
  const exit_1 = require("../lib/utils/exit");
57
+ const log_rotate_1 = require("../lib/utils/log-rotate");
58
+ const process_1 = require("../lib/utils/process");
59
+ const project_registry_1 = require("../lib/utils/project-registry");
57
60
  const project_root_1 = require("../lib/utils/project-root");
58
- const watcher_registry_1 = require("../lib/utils/watcher-registry");
61
+ const watcher_store_1 = require("../lib/utils/watcher-store");
59
62
  const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
60
63
  const IDLE_CHECK_INTERVAL_MS = 60 * 1000; // check every minute
61
- const MAX_LOG_BYTES = 5 * 1024 * 1024; // 5 MB — rotate log when exceeded
62
64
  exports.watch = new commander_1.Command("watch")
63
65
  .description("Start background file watcher for live reindexing")
64
66
  .option("-b, --background", "Run watcher in background and exit")
@@ -71,8 +73,8 @@ exports.watch = new commander_1.Command("watch")
71
73
  : (_a = (0, project_root_1.findProjectRoot)(process.cwd())) !== null && _a !== void 0 ? _a : process.cwd();
72
74
  const projectName = path.basename(projectRoot);
73
75
  // Check if watcher already running (exact match or parent covering this dir)
74
- const existing = (_b = (0, watcher_registry_1.getWatcherForProject)(projectRoot)) !== null && _b !== void 0 ? _b : (0, watcher_registry_1.getWatcherCoveringPath)(projectRoot);
75
- if (existing && (0, watcher_registry_1.isProcessRunning)(existing.pid)) {
76
+ const existing = (_b = (0, watcher_store_1.getWatcherForProject)(projectRoot)) !== null && _b !== void 0 ? _b : (0, watcher_store_1.getWatcherCoveringPath)(projectRoot);
77
+ if (existing && (0, watcher_store_1.isProcessRunning)(existing.pid)) {
76
78
  console.log(`Watcher already running for ${path.basename(existing.projectRoot)} (PID: ${existing.pid})`);
77
79
  return;
78
80
  }
@@ -81,20 +83,9 @@ exports.watch = new commander_1.Command("watch")
81
83
  const args = process.argv
82
84
  .slice(2)
83
85
  .filter((arg) => arg !== "-b" && arg !== "--background");
84
- const logDir = path.join(config_1.PATHS.globalRoot, "logs");
85
- fs.mkdirSync(logDir, { recursive: true });
86
86
  const safeName = projectName.replace(/[^a-zA-Z0-9._-]/g, "_");
87
- const logFile = path.join(logDir, `watch-${safeName}.log`);
88
- // Rotate log if it exceeds MAX_LOG_BYTES
89
- try {
90
- const logStat = fs.statSync(logFile);
91
- if (logStat.size > MAX_LOG_BYTES) {
92
- const prev = `${logFile}.prev`;
93
- fs.renameSync(logFile, prev);
94
- }
95
- }
96
- catch (_c) { }
97
- const out = fs.openSync(logFile, "a");
87
+ const logFile = path.join(config_1.PATHS.logsDir, `watch-${safeName}.log`);
88
+ const out = (0, log_rotate_1.openRotatedLog)(logFile);
98
89
  const child = (0, node_child_process_1.spawn)(process.argv[0], [process.argv[1], ...args], {
99
90
  detached: true,
100
91
  stdio: ["ignore", out, out],
@@ -106,12 +97,20 @@ exports.watch = new commander_1.Command("watch")
106
97
  process.exit(0);
107
98
  }
108
99
  // --- Foreground mode ---
100
+ // Migrate legacy watchers.json to LMDB on first use
101
+ (0, watcher_store_1.migrateFromJson)();
102
+ // Watcher requires project to be registered
103
+ if (!(0, project_registry_1.getProject)(projectRoot)) {
104
+ console.error(`[watch:${projectName}] Project not registered. Run: gmax add ${projectRoot}`);
105
+ process.exitCode = 1;
106
+ return;
107
+ }
109
108
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
110
109
  // Propagate project root to worker processes
111
110
  process.env.GMAX_PROJECT_ROOT = paths.root;
112
111
  console.log(`[watch:${projectName}] Starting...`);
113
112
  // Register early so MCP can see status
114
- (0, watcher_registry_1.registerWatcher)({
113
+ (0, watcher_store_1.registerWatcher)({
115
114
  pid: process.pid,
116
115
  projectRoot,
117
116
  startTime: Date.now(),
@@ -129,10 +128,22 @@ exports.watch = new commander_1.Command("watch")
129
128
  .toArray();
130
129
  if (indexed.length === 0) {
131
130
  console.log(`[watch:${projectName}] No index found for ${projectRoot}, running initial sync...`);
132
- yield (0, syncer_1.initialSync)({ projectRoot });
131
+ const syncResult = yield (0, syncer_1.initialSync)({ projectRoot });
132
+ // Update registry after sync
133
+ const globalConfig = (0, index_config_1.readGlobalConfig)();
134
+ (0, project_registry_1.registerProject)({
135
+ root: projectRoot,
136
+ name: projectName,
137
+ vectorDim: globalConfig.vectorDim,
138
+ modelTier: globalConfig.modelTier,
139
+ embedMode: globalConfig.embedMode,
140
+ lastIndexed: new Date().toISOString(),
141
+ chunkCount: syncResult.indexed,
142
+ status: "indexed",
143
+ });
133
144
  console.log(`[watch:${projectName}] Initial sync complete.`);
134
145
  }
135
- (0, watcher_registry_1.updateWatcherStatus)(process.pid, "watching");
146
+ (0, watcher_store_1.updateWatcherStatus)(process.pid, "watching");
136
147
  // Open resources for watcher
137
148
  const metaCache = new meta_cache_1.MetaCache(paths.lmdbPath);
138
149
  // Start watching
@@ -144,10 +155,14 @@ exports.watch = new commander_1.Command("watch")
144
155
  onReindex: (files, ms) => {
145
156
  console.log(`[watch:${projectName}] Reindexed ${files} file${files !== 1 ? "s" : ""} (${(ms / 1000).toFixed(1)}s)`);
146
157
  lastActivity = Date.now();
147
- (0, watcher_registry_1.updateWatcherStatus)(process.pid, "watching", Date.now());
158
+ (0, watcher_store_1.updateWatcherStatus)(process.pid, "watching", Date.now());
148
159
  },
149
160
  });
150
161
  console.log(`[watch:${projectName}] File watcher active`);
162
+ // Heartbeat — update LMDB every 60s so other processes can detect liveliness
163
+ const heartbeatInterval = setInterval(() => {
164
+ (0, watcher_store_1.heartbeat)(process.pid);
165
+ }, IDLE_CHECK_INTERVAL_MS);
151
166
  // Idle timeout
152
167
  let lastActivity = Date.now();
153
168
  if (options.idleTimeout !== false) {
@@ -161,13 +176,14 @@ exports.watch = new commander_1.Command("watch")
161
176
  // Graceful shutdown
162
177
  function shutdown() {
163
178
  return __awaiter(this, void 0, void 0, function* () {
179
+ clearInterval(heartbeatInterval);
164
180
  try {
165
181
  yield watcher.close();
166
182
  }
167
183
  catch (_a) { }
168
184
  yield metaCache.close();
169
185
  yield vectorDb.close();
170
- (0, watcher_registry_1.unregisterWatcher)(process.pid);
186
+ (0, watcher_store_1.unregisterWatcher)(process.pid);
171
187
  yield (0, exit_1.gracefulExit)();
172
188
  });
173
189
  }
@@ -179,7 +195,7 @@ exports.watch
179
195
  .command("status")
180
196
  .description("Show running watchers")
181
197
  .action(() => __awaiter(void 0, void 0, void 0, function* () {
182
- const watchers = (0, watcher_registry_1.listWatchers)();
198
+ const watchers = (0, watcher_store_1.listWatchers)();
183
199
  if (watchers.length === 0) {
184
200
  console.log("No running watchers.");
185
201
  yield (0, exit_1.gracefulExit)();
@@ -199,33 +215,32 @@ exports.watch
199
215
  .action((options) => __awaiter(void 0, void 0, void 0, function* () {
200
216
  var _a;
201
217
  if (options.all) {
202
- const watchers = (0, watcher_registry_1.listWatchers)();
218
+ const watchers = (0, watcher_store_1.listWatchers)();
203
219
  for (const w of watchers) {
204
- try {
205
- process.kill(w.pid, "SIGTERM");
206
- (0, watcher_registry_1.unregisterWatcher)(w.pid);
220
+ const killed = yield (0, process_1.killProcess)(w.pid);
221
+ (0, watcher_store_1.unregisterWatcher)(w.pid);
222
+ if (!killed) {
223
+ console.warn(`Warning: PID ${w.pid} did not exit after SIGKILL`);
207
224
  }
208
- catch (_b) { }
209
225
  }
210
226
  console.log(`Stopped ${watchers.length} watcher(s).`);
211
227
  yield (0, exit_1.gracefulExit)();
212
228
  return;
213
229
  }
214
230
  const projectRoot = (_a = (0, project_root_1.findProjectRoot)(process.cwd())) !== null && _a !== void 0 ? _a : process.cwd();
215
- const watcher = (0, watcher_registry_1.getWatcherForProject)(projectRoot);
231
+ const watcher = (0, watcher_store_1.getWatcherForProject)(projectRoot);
216
232
  if (!watcher) {
217
233
  console.log("No watcher running for this project.");
218
234
  yield (0, exit_1.gracefulExit)();
219
235
  return;
220
236
  }
221
- try {
222
- process.kill(watcher.pid, "SIGTERM");
223
- (0, watcher_registry_1.unregisterWatcher)(watcher.pid);
237
+ const killed = yield (0, process_1.killProcess)(watcher.pid);
238
+ (0, watcher_store_1.unregisterWatcher)(watcher.pid);
239
+ if (killed) {
224
240
  console.log(`Stopped watcher (PID: ${watcher.pid})`);
225
241
  }
226
- catch (_c) {
227
- console.log("Watcher process not found.");
228
- (0, watcher_registry_1.unregisterWatcher)(watcher.pid);
242
+ else {
243
+ console.warn(`Warning: watcher PID ${watcher.pid} did not exit`);
229
244
  }
230
245
  yield (0, exit_1.gracefulExit)();
231
246
  }));
package/dist/config.js CHANGED
@@ -93,6 +93,7 @@ exports.PATHS = {
93
93
  globalRoot: GLOBAL_ROOT,
94
94
  models: path.join(GLOBAL_ROOT, "models"),
95
95
  grammars: path.join(GLOBAL_ROOT, "grammars"),
96
+ logsDir: path.join(GLOBAL_ROOT, "logs"),
96
97
  // Centralized index storage — one database for all indexed directories
97
98
  lancedbDir: path.join(GLOBAL_ROOT, "lancedb"),
98
99
  cacheDir: path.join(GLOBAL_ROOT, "cache"),
@@ -61,7 +61,6 @@ const vector_db_1 = require("../store/vector-db");
61
61
  const filter_builder_1 = require("../utils/filter-builder");
62
62
  // isIndexableFile no longer used — extension check inlined for performance
63
63
  const lock_1 = require("../utils/lock");
64
- const project_registry_1 = require("../utils/project-registry");
65
64
  const project_root_1 = require("../utils/project-root");
66
65
  const pool_1 = require("../workers/pool");
67
66
  const index_config_1 = require("./index-config");
@@ -507,18 +506,6 @@ function initialSync(options) {
507
506
  // Write model config so future runs can detect model changes
508
507
  if (!dryRun) {
509
508
  (0, index_config_1.writeIndexConfig)(paths.configPath);
510
- // Register project in global registry
511
- const globalConfig = (0, index_config_1.readGlobalConfig)();
512
- (0, project_registry_1.registerProject)({
513
- root: paths.root,
514
- name: path.basename(paths.root),
515
- vectorDim: globalConfig.vectorDim,
516
- modelTier: globalConfig.modelTier,
517
- embedMode: globalConfig.embedMode,
518
- lastIndexed: new Date().toISOString(),
519
- chunkCount: indexed,
520
- status: "indexed",
521
- });
522
509
  }
523
510
  // Finalize total so callers can display a meaningful summary.
524
511
  total = processed;
@@ -136,9 +136,11 @@ function startWatcher(opts) {
136
136
  const vectors = [];
137
137
  const metaUpdates = new Map();
138
138
  const metaDeletes = [];
139
+ const attempted = new Set();
139
140
  for (const [absPath, event] of batch) {
140
141
  if (batchAc.signal.aborted)
141
142
  break;
143
+ attempted.add(absPath);
142
144
  if (event === "unlink") {
143
145
  deletes.push(absPath);
144
146
  metaDeletes.push(absPath);
@@ -200,6 +202,12 @@ function startWatcher(opts) {
200
202
  }
201
203
  }
202
204
  }
205
+ // Requeue files that weren't attempted (aborted or pool unhealthy)
206
+ for (const [absPath, event] of batch) {
207
+ if (!attempted.has(absPath) && !pending.has(absPath)) {
208
+ pending.set(absPath, event);
209
+ }
210
+ }
203
211
  // Flush to VectorDB: insert first, then delete old (preserving new)
204
212
  const newIds = vectors.map((v) => v.id);
205
213
  if (vectors.length > 0) {
@@ -0,0 +1,55 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.openRotatedLog = openRotatedLog;
37
+ const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
39
+ const MAX_LOG_BYTES = 5 * 1024 * 1024; // 5 MB
40
+ /**
41
+ * Open a log file with rotation. Creates parent directories if needed.
42
+ * Rotates {name}.log -> {name}.log.prev when size exceeds maxBytes.
43
+ * Returns an fd suitable for stdio redirection.
44
+ */
45
+ function openRotatedLog(logPath, maxBytes = MAX_LOG_BYTES) {
46
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
47
+ try {
48
+ const stat = fs.statSync(logPath);
49
+ if (stat.size > maxBytes) {
50
+ fs.renameSync(logPath, `${logPath}.prev`);
51
+ }
52
+ }
53
+ catch (_a) { }
54
+ return fs.openSync(logPath, "a");
55
+ }
@@ -0,0 +1,45 @@
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.killProcess = killProcess;
13
+ const watcher_store_1 = require("./watcher-store");
14
+ /**
15
+ * Send SIGTERM, wait up to 3s, then SIGKILL if still alive.
16
+ * Returns true if process is confirmed dead.
17
+ */
18
+ function killProcess(pid) {
19
+ return __awaiter(this, void 0, void 0, function* () {
20
+ try {
21
+ process.kill(pid, "SIGTERM");
22
+ }
23
+ catch (_a) {
24
+ return false;
25
+ }
26
+ // Poll up to 3s for graceful exit
27
+ for (let i = 0; i < 30; i++) {
28
+ if (!(0, watcher_store_1.isProcessRunning)(pid))
29
+ return true;
30
+ yield new Promise((r) => setTimeout(r, 100));
31
+ }
32
+ // Force kill
33
+ try {
34
+ process.kill(pid, "SIGKILL");
35
+ }
36
+ catch (_b) { }
37
+ // Give SIGKILL a moment
38
+ for (let i = 0; i < 10; i++) {
39
+ if (!(0, watcher_store_1.isProcessRunning)(pid))
40
+ return true;
41
+ yield new Promise((r) => setTimeout(r, 100));
42
+ }
43
+ return !(0, watcher_store_1.isProcessRunning)(pid);
44
+ });
45
+ }
@@ -43,6 +43,8 @@ exports.registerProject = registerProject;
43
43
  exports.listProjects = listProjects;
44
44
  exports.getProject = getProject;
45
45
  exports.removeProject = removeProject;
46
+ exports.getParentProject = getParentProject;
47
+ exports.getChildProjects = getChildProjects;
46
48
  const fs = __importStar(require("node:fs"));
47
49
  const path = __importStar(require("node:path"));
48
50
  const config_1 = require("../../config");
@@ -81,3 +83,17 @@ function removeProject(root) {
81
83
  const entries = loadRegistry().filter((e) => e.root !== root);
82
84
  saveRegistry(entries);
83
85
  }
86
+ /**
87
+ * Find a registered parent that covers this path, if any.
88
+ */
89
+ function getParentProject(root) {
90
+ const resolved = root.endsWith("/") ? root : `${root}/`;
91
+ return loadRegistry().find((e) => e.root !== root && resolved.startsWith(e.root.endsWith("/") ? e.root : `${e.root}/`));
92
+ }
93
+ /**
94
+ * Find registered projects that are children of this path.
95
+ */
96
+ function getChildProjects(root) {
97
+ const prefix = root.endsWith("/") ? root : `${root}/`;
98
+ return loadRegistry().filter((e) => e.root !== root && e.root.startsWith(prefix));
99
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ /**
3
+ * Centralized watcher launch logic.
4
+ * Single function that all code paths use to spawn a watcher.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.launchWatcher = launchWatcher;
8
+ const node_child_process_1 = require("node:child_process");
9
+ const project_registry_1 = require("./project-registry");
10
+ const watcher_store_1 = require("./watcher-store");
11
+ function launchWatcher(projectRoot) {
12
+ var _a;
13
+ // 1. Project must be registered
14
+ const project = (0, project_registry_1.getProject)(projectRoot);
15
+ if (!project) {
16
+ return {
17
+ ok: false,
18
+ reason: "not-registered",
19
+ message: `Project not registered. Run: gmax add ${projectRoot}`,
20
+ };
21
+ }
22
+ // 2. Check if watcher already running
23
+ const existing = (_a = (0, watcher_store_1.getWatcherForProject)(projectRoot)) !== null && _a !== void 0 ? _a : (0, watcher_store_1.getWatcherCoveringPath)(projectRoot);
24
+ if (existing && (0, watcher_store_1.isProcessRunning)(existing.pid)) {
25
+ return { ok: true, pid: existing.pid, reused: true };
26
+ }
27
+ // 3. Spawn
28
+ try {
29
+ const child = (0, node_child_process_1.spawn)(process.argv[0], [process.argv[1], "watch", "--path", projectRoot, "-b"], { detached: true, stdio: "ignore" });
30
+ child.unref();
31
+ if (child.pid) {
32
+ return { ok: true, pid: child.pid, reused: false };
33
+ }
34
+ return {
35
+ ok: false,
36
+ reason: "spawn-failed",
37
+ message: `Spawn returned no PID for ${projectRoot}`,
38
+ };
39
+ }
40
+ catch (err) {
41
+ const msg = err instanceof Error ? err.message : String(err);
42
+ return {
43
+ ok: false,
44
+ reason: "spawn-failed",
45
+ message: `Failed to start watcher for ${projectRoot}: ${msg}`,
46
+ };
47
+ }
48
+ }
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ /**
3
+ * LMDB-backed watcher registry — replaces the JSON-based watcher-registry.ts.
4
+ *
5
+ * Provides ACID transactions for watcher state, eliminating race conditions
6
+ * when multiple processes (Claude sessions, MCP, CLI) read/write concurrently.
7
+ *
8
+ * Stored in ~/.gmax/cache/watchers.lmdb
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.registerWatcher = registerWatcher;
45
+ exports.updateWatcherStatus = updateWatcherStatus;
46
+ exports.heartbeat = heartbeat;
47
+ exports.unregisterWatcher = unregisterWatcher;
48
+ exports.getWatcherForProject = getWatcherForProject;
49
+ exports.getWatcherCoveringPath = getWatcherCoveringPath;
50
+ exports.listWatchers = listWatchers;
51
+ exports.migrateFromJson = migrateFromJson;
52
+ exports.isProcessRunning = isProcessRunning;
53
+ const fs = __importStar(require("node:fs"));
54
+ const path = __importStar(require("node:path"));
55
+ const lmdb_1 = require("lmdb");
56
+ const config_1 = require("../../config");
57
+ const STORE_PATH = path.join(config_1.PATHS.cacheDir, "watchers.lmdb");
58
+ const HEARTBEAT_STALE_MS = 5 * 60 * 1000; // 5 minutes
59
+ let _db = null;
60
+ function getDb() {
61
+ if (!_db) {
62
+ fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true });
63
+ _db = (0, lmdb_1.open)({
64
+ path: STORE_PATH,
65
+ compression: true,
66
+ });
67
+ }
68
+ return _db;
69
+ }
70
+ function isProcessRunning(pid) {
71
+ try {
72
+ process.kill(pid, 0);
73
+ return true;
74
+ }
75
+ catch (_a) {
76
+ return false;
77
+ }
78
+ }
79
+ function isAlive(info) {
80
+ if (!isProcessRunning(info.pid))
81
+ return false;
82
+ // If heartbeat exists and is stale, treat as dead (possibly deadlocked)
83
+ if (info.lastHeartbeat && Date.now() - info.lastHeartbeat > HEARTBEAT_STALE_MS) {
84
+ return false;
85
+ }
86
+ return true;
87
+ }
88
+ function registerWatcher(info) {
89
+ const db = getDb();
90
+ // Prune any existing dead entry for this project
91
+ const existing = db.get(info.projectRoot);
92
+ if (existing && !isAlive(existing)) {
93
+ db.remove(info.projectRoot);
94
+ }
95
+ db.put(info.projectRoot, Object.assign(Object.assign({}, info), { lastHeartbeat: Date.now() }));
96
+ }
97
+ function updateWatcherStatus(pid, status, lastReindex) {
98
+ const db = getDb();
99
+ // Find entry by PID (iterate since key is projectRoot)
100
+ for (const { key, value } of db.getRange()) {
101
+ if (value && value.pid === pid) {
102
+ db.put(String(key), Object.assign(Object.assign(Object.assign({}, value), { status, lastHeartbeat: Date.now() }), (lastReindex ? { lastReindex } : {})));
103
+ return;
104
+ }
105
+ }
106
+ }
107
+ function heartbeat(pid) {
108
+ const db = getDb();
109
+ for (const { key, value } of db.getRange()) {
110
+ if (value && value.pid === pid) {
111
+ db.put(String(key), Object.assign(Object.assign({}, value), { lastHeartbeat: Date.now() }));
112
+ return;
113
+ }
114
+ }
115
+ }
116
+ function unregisterWatcher(pid) {
117
+ const db = getDb();
118
+ for (const { key, value } of db.getRange()) {
119
+ if (value && value.pid === pid) {
120
+ db.remove(String(key));
121
+ return;
122
+ }
123
+ }
124
+ }
125
+ function getWatcherForProject(projectRoot) {
126
+ const db = getDb();
127
+ const info = db.get(projectRoot);
128
+ if (!info)
129
+ return undefined;
130
+ if (isAlive(info))
131
+ return info;
132
+ // Clean stale entry
133
+ db.remove(projectRoot);
134
+ return undefined;
135
+ }
136
+ function getWatcherCoveringPath(dir) {
137
+ const resolved = dir.endsWith("/") ? dir : `${dir}/`;
138
+ const db = getDb();
139
+ for (const { key, value } of db.getRange()) {
140
+ if (!value)
141
+ continue;
142
+ const root = String(key);
143
+ const prefix = root.endsWith("/") ? root : `${root}/`;
144
+ if (resolved.startsWith(prefix) && isAlive(value)) {
145
+ return value;
146
+ }
147
+ }
148
+ return undefined;
149
+ }
150
+ function listWatchers() {
151
+ const db = getDb();
152
+ const alive = [];
153
+ const dead = [];
154
+ for (const { key, value } of db.getRange()) {
155
+ if (!value)
156
+ continue;
157
+ if (isAlive(value)) {
158
+ alive.push(value);
159
+ }
160
+ else {
161
+ dead.push(String(key));
162
+ }
163
+ }
164
+ // Prune dead entries
165
+ for (const key of dead) {
166
+ db.remove(key);
167
+ }
168
+ return alive;
169
+ }
170
+ /**
171
+ * Migrate from legacy watchers.json if it exists.
172
+ * Call once on startup.
173
+ */
174
+ function migrateFromJson() {
175
+ const jsonPath = path.join(config_1.PATHS.globalRoot, "watchers.json");
176
+ if (!fs.existsSync(jsonPath))
177
+ return;
178
+ try {
179
+ const raw = fs.readFileSync(jsonPath, "utf-8");
180
+ const entries = JSON.parse(raw);
181
+ const db = getDb();
182
+ for (const entry of entries) {
183
+ if (entry.projectRoot && isProcessRunning(entry.pid)) {
184
+ db.put(entry.projectRoot, Object.assign(Object.assign({}, entry), { lastHeartbeat: Date.now() }));
185
+ }
186
+ }
187
+ // Remove legacy file
188
+ fs.unlinkSync(jsonPath);
189
+ }
190
+ catch (_a) {
191
+ // Best effort — ignore
192
+ }
193
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "author": "Robert Owens <robowens@me.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.8.0",
3
+ "version": "0.8.2",
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",
@@ -45,7 +45,18 @@ function findMlxServerDir() {
45
45
  function startPythonServer(serverDir, scriptName, logName) {
46
46
  if (!serverDir) return;
47
47
 
48
- const logPath = `/tmp/${logName}.log`;
48
+ const logDir = _path.join(require("node:os").homedir(), ".gmax", "logs");
49
+ fs.mkdirSync(logDir, { recursive: true });
50
+ const logPath = _path.join(logDir, `${logName}.log`);
51
+
52
+ // Rotate if > 5MB (same threshold as watch.ts)
53
+ try {
54
+ const stat = fs.statSync(logPath);
55
+ if (stat.size > 5 * 1024 * 1024) {
56
+ fs.renameSync(logPath, `${logPath}.prev`);
57
+ }
58
+ } catch {}
59
+
49
60
  const out = fs.openSync(logPath, "a");
50
61
 
51
62
  const child = spawn("uv", ["run", "python", scriptName], {
@@ -57,7 +68,23 @@ function startPythonServer(serverDir, scriptName, logName) {
57
68
  child.unref();
58
69
  }
59
70
 
71
+ function isProjectRegistered() {
72
+ try {
73
+ const projectsPath = _path.join(
74
+ require("node:os").homedir(),
75
+ ".gmax",
76
+ "projects.json",
77
+ );
78
+ const projects = JSON.parse(require("node:fs").readFileSync(projectsPath, "utf-8"));
79
+ const cwd = process.cwd();
80
+ return projects.some((p) => cwd.startsWith(p.root));
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
60
86
  function startWatcher() {
87
+ if (!isProjectRegistered()) return;
61
88
  try {
62
89
  execFileSync("gmax", ["watch", "-b"], { timeout: 5000, stdio: "ignore" });
63
90
  } catch {
@@ -24,16 +24,17 @@ Bash(gmax "auth handler" --role ORCHESTRATION --lang ts --agent -m 3)
24
24
 
25
25
  ## Project management
26
26
 
27
- Projects must be added before they can be searched:
27
+ Projects must be added before CLI search works. MCP tools auto-add on first use, but CLI requires an explicit step:
28
28
 
29
29
  ```
30
- gmax add # add current directory to the index
30
+ gmax add # add + index current directory
31
31
  gmax add ~/projects/myapp # add a specific project
32
32
  gmax status # see all indexed projects and their state
33
33
  gmax remove # remove current project from the index
34
+ gmax index # reindex an already-added project
34
35
  ```
35
36
 
36
- If search returns "This project hasn't been added to gmax yet", run `gmax add` first.
37
+ If search returns "This project hasn't been added to gmax yet", run `Bash(gmax add)` first.
37
38
 
38
39
  ## CLI commands
39
40