grepmax 0.9.0 → 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() {
@@ -100,37 +97,25 @@ class Daemon {
100
97
  console.error("[daemon] Failed to open shared resources:", err);
101
98
  throw err;
102
99
  }
103
- // 4. Create chokidar (empty projects added via watchProject)
104
- const forcePoll = process.env.GMAX_WATCH_POLL === "1";
105
- const usePoll = forcePoll || process.platform !== "darwin";
106
- this.watcher = (0, chokidar_1.watch)([], Object.assign({ ignored: watcher_1.WATCHER_IGNORE_PATTERNS, ignoreInitial: true, persistent: true }, (usePoll
107
- ? { usePolling: true, interval: 5000, binaryInterval: 10000 }
108
- : {})));
109
- this.watcher.on("add", (p) => this.routeEvent("change", p));
110
- this.watcher.on("change", (p) => this.routeEvent("change", p));
111
- this.watcher.on("unlink", (p) => this.routeEvent("unlink", p));
112
- this.watcher.on("error", (err) => {
113
- console.error("[daemon] Watcher error:", err);
114
- });
115
- // 5. Register daemon (only after resources are open)
100
+ // 4. Register daemon (only after resources are open)
116
101
  (0, watcher_store_1.registerDaemon)(process.pid);
117
- // 6. Load registered projects
102
+ // 5. Subscribe to all registered projects
118
103
  const projects = (0, project_registry_1.listProjects)().filter((p) => p.status === "indexed");
119
104
  for (const p of projects) {
120
105
  yield this.watchProject(p.root);
121
106
  }
122
- // 7. Heartbeat
107
+ // 6. Heartbeat
123
108
  this.heartbeatInterval = setInterval(() => {
124
109
  (0, watcher_store_1.heartbeat)(process.pid);
125
110
  }, HEARTBEAT_INTERVAL_MS);
126
- // 8. Idle timeout
111
+ // 7. Idle timeout
127
112
  this.idleInterval = setInterval(() => {
128
113
  if (Date.now() - this.lastActivity > IDLE_TIMEOUT_MS) {
129
114
  console.log("[daemon] Idle for 30 minutes, shutting down");
130
115
  this.shutdown();
131
116
  }
132
117
  }, HEARTBEAT_INTERVAL_MS);
133
- // 9. Socket server
118
+ // 8. Socket server
134
119
  this.server = net.createServer((conn) => {
135
120
  let buf = "";
136
121
  conn.on("data", (chunk) => {
@@ -154,7 +139,7 @@ class Daemon {
154
139
  conn.end();
155
140
  });
156
141
  });
157
- conn.on("error", () => { }); // ignore client disconnect
142
+ conn.on("error", () => { });
158
143
  });
159
144
  yield new Promise((resolve, reject) => {
160
145
  this.server.on("error", (err) => {
@@ -181,7 +166,7 @@ class Daemon {
181
166
  return __awaiter(this, void 0, void 0, function* () {
182
167
  if (this.processors.has(root) || this.pendingOps.has(root))
183
168
  return;
184
- if (!this.vectorDb || !this.metaCache || !this.watcher)
169
+ if (!this.vectorDb || !this.metaCache)
185
170
  return;
186
171
  this.pendingOps.add(root);
187
172
  const processor = new batch_processor_1.ProjectBatchProcessor({
@@ -196,9 +181,19 @@ class Daemon {
196
181
  this.lastActivity = Date.now();
197
182
  },
198
183
  });
199
- this.watcher.add(root);
200
184
  this.processors.set(root, processor);
201
- this.rebuildSortedRoots();
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
+ }
194
+ this.lastActivity = Date.now();
195
+ }, { ignore: watcher_1.WATCHER_IGNORE_GLOBS });
196
+ this.subscriptions.set(root, sub);
202
197
  (0, watcher_store_1.registerWatcher)({
203
198
  pid: process.pid,
204
199
  projectRoot: root,
@@ -212,14 +207,16 @@ class Daemon {
212
207
  }
213
208
  unwatchProject(root) {
214
209
  return __awaiter(this, void 0, void 0, function* () {
215
- var _a;
216
210
  const processor = this.processors.get(root);
217
211
  if (!processor)
218
212
  return;
219
213
  yield processor.close();
220
- (_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
+ }
221
219
  this.processors.delete(root);
222
- this.rebuildSortedRoots();
223
220
  (0, watcher_store_1.unregisterWatcherByRoot)(root);
224
221
  console.log(`[daemon] Unwatched ${root}`);
225
222
  });
@@ -235,7 +232,7 @@ class Daemon {
235
232
  }
236
233
  shutdown() {
237
234
  return __awaiter(this, void 0, void 0, function* () {
238
- var _a, _b, _c, _d;
235
+ var _a, _b, _c;
239
236
  if (this.shuttingDown)
240
237
  return;
241
238
  this.shuttingDown = true;
@@ -248,17 +245,20 @@ class Daemon {
248
245
  for (const processor of this.processors.values()) {
249
246
  yield processor.close();
250
247
  }
251
- // Close chokidar
252
- try {
253
- 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) { }
254
254
  }
255
- catch (_e) { }
255
+ this.subscriptions.clear();
256
256
  // Close server + socket
257
- (_b = this.server) === null || _b === void 0 ? void 0 : _b.close();
257
+ (_a = this.server) === null || _a === void 0 ? void 0 : _a.close();
258
258
  try {
259
259
  fs.unlinkSync(config_1.PATHS.daemonSocket);
260
260
  }
261
- catch (_f) { }
261
+ catch (_e) { }
262
262
  // Unregister all
263
263
  for (const root of this.processors.keys()) {
264
264
  (0, watcher_store_1.unregisterWatcherByRoot)(root);
@@ -267,33 +267,15 @@ class Daemon {
267
267
  this.processors.clear();
268
268
  // Close shared resources
269
269
  try {
270
- 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());
271
271
  }
272
- catch (_g) { }
272
+ catch (_f) { }
273
273
  try {
274
- 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());
275
275
  }
276
- catch (_h) { }
276
+ catch (_g) { }
277
277
  console.log("[daemon] Shutdown complete");
278
278
  });
279
279
  }
280
- routeEvent(event, absPath) {
281
- const processor = this.findProcessor(absPath);
282
- if (processor) {
283
- processor.handleFileEvent(event, absPath);
284
- }
285
- }
286
- findProcessor(absPath) {
287
- // sortedRoots is longest-first, so first match is the most specific
288
- for (const root of this.sortedRoots) {
289
- if (absPath.startsWith(root) && (absPath.length === root.length || absPath[root.length] === "/")) {
290
- return this.processors.get(root);
291
- }
292
- }
293
- return undefined;
294
- }
295
- rebuildSortedRoots() {
296
- this.sortedRoots = [...this.processors.keys()].sort((a, b) => b.length - a.length);
297
- }
298
280
  }
299
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
  }
@@ -159,6 +159,8 @@ function listWatchers() {
159
159
  for (const { key, value } of db.getRange()) {
160
160
  if (!value)
161
161
  continue;
162
+ if (String(key) === exports.DAEMON_KEY)
163
+ continue;
162
164
  if (isAlive(value)) {
163
165
  alive.push(value);
164
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.9.0",
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.0",
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",