getaimeter 0.1.4 → 0.1.5

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/watcher.js +35 -69
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getaimeter",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Track your Claude AI usage across CLI, VS Code, and Desktop App. One command to start.",
5
5
  "bin": {
6
6
  "aimeter": "cli.js"
package/watcher.js CHANGED
@@ -137,39 +137,20 @@ async function reportEvents(events) {
137
137
  // File watcher
138
138
  // ---------------------------------------------------------------------------
139
139
 
140
- const _debounceTimers = new Map();
141
- const _processing = new Set();
142
-
143
- function handleFileChange(filePath) {
144
- // Only care about .jsonl files
145
- if (!filePath.endsWith('.jsonl')) return;
146
-
147
- // Normalize path for consistent debounce key (Windows fires with mixed separators/casing)
148
- const normalizedKey = path.resolve(filePath).toLowerCase();
149
-
150
- // Skip if already being processed
151
- if (_processing.has(normalizedKey)) return;
152
-
153
- // Debounce: wait 2000ms after last change before processing
154
- const existing = _debounceTimers.get(normalizedKey);
155
- if (existing) clearTimeout(existing);
156
-
157
- _debounceTimers.set(normalizedKey, setTimeout(async () => {
158
- _debounceTimers.delete(normalizedKey);
159
- if (_processing.has(normalizedKey)) return;
160
- _processing.add(normalizedKey);
161
- try {
162
- const events = extractNewUsage(filePath);
163
- if (events.length > 0) {
164
- await reportEvents(events);
165
- saveState();
166
- }
167
- } catch (err) {
168
- logError(`Processing ${filePath}:`, err.message);
169
- } finally {
170
- _processing.delete(normalizedKey);
140
+ /**
141
+ * Process a single file: extract new usage and report it.
142
+ * Called only from the poll loop — single-threaded, no races.
143
+ */
144
+ async function processFile(filePath) {
145
+ try {
146
+ const events = extractNewUsage(filePath);
147
+ if (events.length > 0) {
148
+ await reportEvents(events);
149
+ saveState();
171
150
  }
172
- }, 2000));
151
+ } catch (err) {
152
+ logError(`Processing ${filePath}:`, err.message);
153
+ }
173
154
  }
174
155
 
175
156
  /**
@@ -248,45 +229,32 @@ function startWatching() {
248
229
  }
249
230
  saveState();
250
231
 
251
- // Set up fs.watch on each path (works well on macOS/Linux)
252
- const watchers = [];
253
- for (const watchPath of watchPaths) {
254
- try {
255
- const w = fs.watch(watchPath, { recursive: true }, (eventType, filename) => {
256
- if (!filename) return;
257
- const fullPath = path.join(watchPath, filename);
258
- handleFileChange(fullPath);
259
- });
260
-
261
- w.on('error', (err) => {
262
- logError(`Watcher error on ${watchPath}:`, err.message);
263
- });
264
-
265
- watchers.push(w);
266
- log('Watching (fs.watch):', watchPath);
267
- } catch (err) {
268
- logError(`Could not watch ${watchPath}:`, err.message);
269
- }
270
- }
271
-
272
- // Polling fallback — fs.watch is unreliable on Windows for deeply nested dirs.
273
- // Every 5 seconds, scan all known JSONL files for size changes.
232
+ // Poll every 5 seconds simple, reliable, no race conditions.
233
+ // fs.watch is unreliable on Windows for deeply nested dirs and fires duplicates.
274
234
  const POLL_INTERVAL = 5_000;
275
- const pollInterval = setInterval(() => {
276
- for (const watchPath of watchPaths) {
277
- const files = findJsonlFiles(watchPath);
278
- for (const file of files) {
279
- try {
280
- const currentSize = fs.statSync(file).size;
281
- const lastOffset = getOffset(file);
282
- if (currentSize > lastOffset) {
283
- handleFileChange(file);
284
- }
285
- } catch {}
235
+ let polling = false;
236
+
237
+ const pollInterval = setInterval(async () => {
238
+ if (polling) return; // skip if previous poll still running
239
+ polling = true;
240
+ try {
241
+ for (const watchPath of watchPaths) {
242
+ const files = findJsonlFiles(watchPath);
243
+ for (const file of files) {
244
+ try {
245
+ const currentSize = fs.statSync(file).size;
246
+ const lastOffset = getOffset(file);
247
+ if (currentSize > lastOffset) {
248
+ await processFile(file);
249
+ }
250
+ } catch {}
251
+ }
286
252
  }
253
+ } finally {
254
+ polling = false;
287
255
  }
288
256
  }, POLL_INTERVAL);
289
- log(`Polling every ${POLL_INTERVAL / 1000}s as fallback`);
257
+ log(`Polling every ${POLL_INTERVAL / 1000}s`);
290
258
 
291
259
  // Periodic state save
292
260
  const saveInterval = setInterval(() => saveState(), 30_000);
@@ -295,8 +263,6 @@ function startWatching() {
295
263
  return () => {
296
264
  clearInterval(saveInterval);
297
265
  clearInterval(pollInterval);
298
- for (const w of watchers) w.close();
299
- for (const t of _debounceTimers.values()) clearTimeout(t);
300
266
  saveState();
301
267
  log('Watcher stopped.');
302
268
  };