grepmax 0.9.1 → 0.9.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.
@@ -208,7 +208,7 @@ exports.serve = new commander_1.Command("serve")
208
208
  const metaCache = new meta_cache_1.MetaCache(paths.lmdbPath);
209
209
  const searcher = new searcher_1.Searcher(vectorDb);
210
210
  // Start live file watcher
211
- let fileWatcher = (0, watcher_1.startWatcher)({
211
+ let fileWatcher = yield (0, watcher_1.startWatcher)({
212
212
  projectRoot,
213
213
  vectorDb,
214
214
  metaCache,
@@ -190,7 +190,7 @@ exports.watch = new commander_1.Command("watch")
190
190
  // Open resources for watcher
191
191
  const metaCache = new meta_cache_1.MetaCache(paths.lmdbPath);
192
192
  // Start watching
193
- const watcher = (0, watcher_1.startWatcher)({
193
+ const watcher = yield (0, watcher_1.startWatcher)({
194
194
  projectRoot,
195
195
  vectorDb,
196
196
  metaCache,
@@ -46,7 +46,7 @@ exports.Daemon = void 0;
46
46
  const fs = __importStar(require("node:fs"));
47
47
  const net = __importStar(require("node:net"));
48
48
  const path = __importStar(require("node:path"));
49
- const chokidar_1 = require("chokidar");
49
+ const watcher = __importStar(require("@parcel/watcher"));
50
50
  const config_1 = require("../../config");
51
51
  const batch_processor_1 = require("../index/batch-processor");
52
52
  const watcher_1 = require("../index/watcher");
@@ -60,8 +60,8 @@ const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
60
60
  const HEARTBEAT_INTERVAL_MS = 60 * 1000;
61
61
  class Daemon {
62
62
  constructor() {
63
- this.watcher = null;
64
63
  this.processors = new Map();
64
+ this.subscriptions = new Map();
65
65
  this.vectorDb = null;
66
66
  this.metaCache = null;
67
67
  this.server = null;
@@ -70,9 +70,6 @@ class Daemon {
70
70
  this.heartbeatInterval = null;
71
71
  this.idleInterval = null;
72
72
  this.shuttingDown = false;
73
- // Sorted longest-first for prefix matching
74
- this.sortedRoots = [];
75
- // Guard against concurrent watchProject/unwatchProject
76
73
  this.pendingOps = new Set();
77
74
  }
78
75
  start() {
@@ -102,43 +99,23 @@ class Daemon {
102
99
  }
103
100
  // 4. Register daemon (only after resources are open)
104
101
  (0, watcher_store_1.registerDaemon)(process.pid);
105
- // 5. Load registered projects and create processors
102
+ // 5. Subscribe to all registered projects
106
103
  const projects = (0, project_registry_1.listProjects)().filter((p) => p.status === "indexed");
107
- const initialRoots = [];
108
104
  for (const p of projects) {
109
- this.addProcessor(p.root);
110
- initialRoots.push(p.root);
105
+ yield this.watchProject(p.root);
111
106
  }
112
- // 6. Create chokidar with all initial roots
113
- // Daemon always uses polling — watching multiple large project trees
114
- // with native fs.watch can exhaust file descriptors even on macOS.
115
- // Polling at 5s intervals is lightweight and reliable for all platforms.
116
- this.watcher = (0, chokidar_1.watch)(initialRoots, {
117
- ignored: watcher_1.WATCHER_IGNORE_PATTERNS,
118
- ignoreInitial: true,
119
- persistent: true,
120
- usePolling: true,
121
- interval: 5000,
122
- binaryInterval: 10000,
123
- });
124
- this.watcher.on("add", (p) => this.routeEvent("change", p));
125
- this.watcher.on("change", (p) => this.routeEvent("change", p));
126
- this.watcher.on("unlink", (p) => this.routeEvent("unlink", p));
127
- this.watcher.on("error", (err) => {
128
- console.error("[daemon] Watcher error:", err);
129
- });
130
- // 7. Heartbeat
107
+ // 6. Heartbeat
131
108
  this.heartbeatInterval = setInterval(() => {
132
109
  (0, watcher_store_1.heartbeat)(process.pid);
133
110
  }, HEARTBEAT_INTERVAL_MS);
134
- // 8. Idle timeout
111
+ // 7. Idle timeout
135
112
  this.idleInterval = setInterval(() => {
136
113
  if (Date.now() - this.lastActivity > IDLE_TIMEOUT_MS) {
137
114
  console.log("[daemon] Idle for 30 minutes, shutting down");
138
115
  this.shutdown();
139
116
  }
140
117
  }, HEARTBEAT_INTERVAL_MS);
141
- // 9. Socket server
118
+ // 8. Socket server
142
119
  this.server = net.createServer((conn) => {
143
120
  let buf = "";
144
121
  conn.on("data", (chunk) => {
@@ -162,7 +139,7 @@ class Daemon {
162
139
  conn.end();
163
140
  });
164
141
  });
165
- conn.on("error", () => { }); // ignore client disconnect
142
+ conn.on("error", () => { });
166
143
  });
167
144
  yield new Promise((resolve, reject) => {
168
145
  this.server.on("error", (err) => {
@@ -189,52 +166,57 @@ class Daemon {
189
166
  return __awaiter(this, void 0, void 0, function* () {
190
167
  if (this.processors.has(root) || this.pendingOps.has(root))
191
168
  return;
192
- if (!this.watcher)
169
+ if (!this.vectorDb || !this.metaCache)
193
170
  return;
194
- this.addProcessor(root);
195
- this.watcher.add(root);
196
- console.log(`[daemon] Watching ${root}`);
197
- });
198
- }
199
- addProcessor(root) {
200
- if (this.processors.has(root))
201
- return;
202
- if (!this.vectorDb || !this.metaCache)
203
- return;
204
- this.pendingOps.add(root);
205
- const processor = new batch_processor_1.ProjectBatchProcessor({
206
- projectRoot: root,
207
- vectorDb: this.vectorDb,
208
- metaCache: this.metaCache,
209
- dataDir: config_1.PATHS.globalRoot,
210
- onReindex: (files, ms) => {
211
- console.log(`[daemon:${path.basename(root)}] Reindexed ${files} file${files !== 1 ? "s" : ""} (${(ms / 1000).toFixed(1)}s)`);
212
- },
213
- onActivity: () => {
171
+ this.pendingOps.add(root);
172
+ const processor = new batch_processor_1.ProjectBatchProcessor({
173
+ projectRoot: root,
174
+ vectorDb: this.vectorDb,
175
+ metaCache: this.metaCache,
176
+ dataDir: config_1.PATHS.globalRoot,
177
+ onReindex: (files, ms) => {
178
+ console.log(`[daemon:${path.basename(root)}] Reindexed ${files} file${files !== 1 ? "s" : ""} (${(ms / 1000).toFixed(1)}s)`);
179
+ },
180
+ onActivity: () => {
181
+ this.lastActivity = Date.now();
182
+ },
183
+ });
184
+ this.processors.set(root, processor);
185
+ // Subscribe with @parcel/watcher — native backend, no polling
186
+ const sub = yield watcher.subscribe(root, (err, events) => {
187
+ if (err) {
188
+ console.error(`[daemon:${path.basename(root)}] Watcher error:`, err);
189
+ return;
190
+ }
191
+ for (const event of events) {
192
+ processor.handleFileEvent(event.type === "delete" ? "unlink" : "change", event.path);
193
+ }
214
194
  this.lastActivity = Date.now();
215
- },
216
- });
217
- this.processors.set(root, processor);
218
- this.rebuildSortedRoots();
219
- (0, watcher_store_1.registerWatcher)({
220
- pid: process.pid,
221
- projectRoot: root,
222
- startTime: Date.now(),
223
- status: "watching",
224
- lastHeartbeat: Date.now(),
195
+ }, { ignore: watcher_1.WATCHER_IGNORE_GLOBS });
196
+ this.subscriptions.set(root, sub);
197
+ (0, watcher_store_1.registerWatcher)({
198
+ pid: process.pid,
199
+ projectRoot: root,
200
+ startTime: Date.now(),
201
+ status: "watching",
202
+ lastHeartbeat: Date.now(),
203
+ });
204
+ this.pendingOps.delete(root);
205
+ console.log(`[daemon] Watching ${root}`);
225
206
  });
226
- this.pendingOps.delete(root);
227
207
  }
228
208
  unwatchProject(root) {
229
209
  return __awaiter(this, void 0, void 0, function* () {
230
- var _a;
231
210
  const processor = this.processors.get(root);
232
211
  if (!processor)
233
212
  return;
234
213
  yield processor.close();
235
- (_a = this.watcher) === null || _a === void 0 ? void 0 : _a.unwatch(root);
214
+ const sub = this.subscriptions.get(root);
215
+ if (sub) {
216
+ yield sub.unsubscribe();
217
+ this.subscriptions.delete(root);
218
+ }
236
219
  this.processors.delete(root);
237
- this.rebuildSortedRoots();
238
220
  (0, watcher_store_1.unregisterWatcherByRoot)(root);
239
221
  console.log(`[daemon] Unwatched ${root}`);
240
222
  });
@@ -250,7 +232,7 @@ class Daemon {
250
232
  }
251
233
  shutdown() {
252
234
  return __awaiter(this, void 0, void 0, function* () {
253
- var _a, _b, _c, _d;
235
+ var _a, _b, _c;
254
236
  if (this.shuttingDown)
255
237
  return;
256
238
  this.shuttingDown = true;
@@ -263,17 +245,20 @@ class Daemon {
263
245
  for (const processor of this.processors.values()) {
264
246
  yield processor.close();
265
247
  }
266
- // Close chokidar
267
- try {
268
- yield ((_a = this.watcher) === null || _a === void 0 ? void 0 : _a.close());
248
+ // Unsubscribe all watchers
249
+ for (const sub of this.subscriptions.values()) {
250
+ try {
251
+ yield sub.unsubscribe();
252
+ }
253
+ catch (_d) { }
269
254
  }
270
- catch (_e) { }
255
+ this.subscriptions.clear();
271
256
  // Close server + socket
272
- (_b = this.server) === null || _b === void 0 ? void 0 : _b.close();
257
+ (_a = this.server) === null || _a === void 0 ? void 0 : _a.close();
273
258
  try {
274
259
  fs.unlinkSync(config_1.PATHS.daemonSocket);
275
260
  }
276
- catch (_f) { }
261
+ catch (_e) { }
277
262
  // Unregister all
278
263
  for (const root of this.processors.keys()) {
279
264
  (0, watcher_store_1.unregisterWatcherByRoot)(root);
@@ -282,33 +267,15 @@ class Daemon {
282
267
  this.processors.clear();
283
268
  // Close shared resources
284
269
  try {
285
- yield ((_c = this.metaCache) === null || _c === void 0 ? void 0 : _c.close());
270
+ yield ((_b = this.metaCache) === null || _b === void 0 ? void 0 : _b.close());
286
271
  }
287
- catch (_g) { }
272
+ catch (_f) { }
288
273
  try {
289
- yield ((_d = this.vectorDb) === null || _d === void 0 ? void 0 : _d.close());
274
+ yield ((_c = this.vectorDb) === null || _c === void 0 ? void 0 : _c.close());
290
275
  }
291
- catch (_h) { }
276
+ catch (_g) { }
292
277
  console.log("[daemon] Shutdown complete");
293
278
  });
294
279
  }
295
- routeEvent(event, absPath) {
296
- const processor = this.findProcessor(absPath);
297
- if (processor) {
298
- processor.handleFileEvent(event, absPath);
299
- }
300
- }
301
- findProcessor(absPath) {
302
- // sortedRoots is longest-first, so first match is the most specific
303
- for (const root of this.sortedRoots) {
304
- if (absPath.startsWith(root) && (absPath.length === root.length || absPath[root.length] === "/")) {
305
- return this.processors.get(root);
306
- }
307
- }
308
- return undefined;
309
- }
310
- rebuildSortedRoots() {
311
- this.sortedRoots = [...this.processors.keys()].sort((a, b) => b.length - a.length);
312
- }
313
280
  }
314
281
  exports.Daemon = Daemon;
@@ -42,52 +42,46 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
42
42
  });
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.WATCHER_IGNORE_PATTERNS = void 0;
45
+ exports.WATCHER_IGNORE_GLOBS = void 0;
46
46
  exports.startWatcher = startWatcher;
47
- const path = __importStar(require("node:path"));
48
- const chokidar_1 = require("chokidar");
47
+ const watcher = __importStar(require("@parcel/watcher"));
49
48
  const batch_processor_1 = require("./batch-processor");
50
- // Chokidar ignored must exclude heavy directories to keep FD count low.
51
- // On macOS, chokidar uses FSEvents (single FD) but falls back to fs.watch()
52
- // (one FD per directory) if FSEvents isn't available or for some subdirs.
53
- exports.WATCHER_IGNORE_PATTERNS = [
54
- "**/node_modules/**",
55
- "**/.git/**",
56
- "**/.gmax/**",
57
- "**/dist/**",
58
- "**/build/**",
59
- "**/out/**",
60
- "**/target/**",
61
- "**/__pycache__/**",
62
- "**/coverage/**",
63
- "**/venv/**",
64
- "**/.next/**",
65
- "**/lancedb/**",
66
- /(^|[/\\])\../, // dotfiles
49
+ // Ignore patterns for @parcel/watcher (micromatch globs + directory names).
50
+ // Directory names are matched at any depth automatically.
51
+ exports.WATCHER_IGNORE_GLOBS = [
52
+ "node_modules",
53
+ ".git",
54
+ ".gmax",
55
+ "dist",
56
+ "build",
57
+ "out",
58
+ "target",
59
+ "__pycache__",
60
+ "coverage",
61
+ "venv",
62
+ ".next",
63
+ "lancedb",
64
+ ".*", // dotfiles
67
65
  ];
68
66
  function startWatcher(opts) {
69
- const { projectRoot } = opts;
70
- const wtag = `watch:${path.basename(projectRoot)}`;
71
- const processor = new batch_processor_1.ProjectBatchProcessor(opts);
72
- // macOS: FSEvents is a single-FD kernel API — no EMFILE risk and no polling.
73
- // Linux: inotify is event-driven but uses one FD per watch; fall back to
74
- // polling for monorepos to avoid hitting ulimit.
75
- // Override with GMAX_WATCH_POLL=1 to force polling on any platform.
76
- const forcePoll = process.env.GMAX_WATCH_POLL === "1";
77
- const usePoll = forcePoll || process.platform !== "darwin";
78
- const watcher = (0, chokidar_1.watch)(projectRoot, Object.assign({ ignored: exports.WATCHER_IGNORE_PATTERNS, ignoreInitial: true, persistent: true }, (usePoll
79
- ? { usePolling: true, interval: 5000, binaryInterval: 10000 }
80
- : {})));
81
- watcher.on("error", (err) => {
82
- console.error(`[${wtag}] Watcher error:`, err);
67
+ return __awaiter(this, void 0, void 0, function* () {
68
+ const { projectRoot } = opts;
69
+ const wtag = `watch:${projectRoot.split("/").pop()}`;
70
+ const processor = new batch_processor_1.ProjectBatchProcessor(opts);
71
+ const subscription = yield watcher.subscribe(projectRoot, (err, events) => {
72
+ if (err) {
73
+ console.error(`[${wtag}] Watcher error:`, err);
74
+ return;
75
+ }
76
+ for (const event of events) {
77
+ processor.handleFileEvent(event.type === "delete" ? "unlink" : "change", event.path);
78
+ }
79
+ }, { ignore: exports.WATCHER_IGNORE_GLOBS });
80
+ return {
81
+ close: () => __awaiter(this, void 0, void 0, function* () {
82
+ yield processor.close();
83
+ yield subscription.unsubscribe();
84
+ }),
85
+ };
83
86
  });
84
- watcher.on("add", (p) => processor.handleFileEvent("change", p));
85
- watcher.on("change", (p) => processor.handleFileEvent("change", p));
86
- watcher.on("unlink", (p) => processor.handleFileEvent("unlink", p));
87
- return {
88
- close: () => __awaiter(this, void 0, void 0, function* () {
89
- yield processor.close();
90
- yield watcher.close();
91
- }),
92
- };
93
87
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "author": "Robert Owens <robowens@me.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -36,9 +36,9 @@
36
36
  "@huggingface/transformers": "^4.0.0",
37
37
  "@lancedb/lancedb": "^0.27.1",
38
38
  "@modelcontextprotocol/sdk": "^1.29.0",
39
+ "@parcel/watcher": "^2.5.6",
39
40
  "apache-arrow": "^18.1.0",
40
41
  "chalk": "^5.6.2",
41
- "chokidar": "^5.0.0",
42
42
  "cli-highlight": "^2.1.11",
43
43
  "commander": "^14.0.2",
44
44
  "dotenv": "^17.2.3",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.9.1",
3
+ "version": "0.9.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",