getaimeter 0.1.3 → 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 -61
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getaimeter",
3
- "version": "0.1.3",
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,31 +137,20 @@ async function reportEvents(events) {
137
137
  // File watcher
138
138
  // ---------------------------------------------------------------------------
139
139
 
140
- const _debounceTimers = new Map();
141
-
142
- function handleFileChange(filePath) {
143
- // Only care about .jsonl files
144
- if (!filePath.endsWith('.jsonl')) return;
145
-
146
- // Normalize path for consistent debounce key (Windows fires with mixed separators/casing)
147
- const normalizedKey = path.resolve(filePath).toLowerCase();
148
-
149
- // Debounce: wait 1000ms after last change before processing
150
- const existing = _debounceTimers.get(normalizedKey);
151
- if (existing) clearTimeout(existing);
152
-
153
- _debounceTimers.set(normalizedKey, setTimeout(async () => {
154
- _debounceTimers.delete(normalizedKey);
155
- try {
156
- const events = extractNewUsage(filePath);
157
- if (events.length > 0) {
158
- await reportEvents(events);
159
- saveState();
160
- }
161
- } catch (err) {
162
- logError(`Processing ${filePath}:`, err.message);
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();
163
150
  }
164
- }, 500));
151
+ } catch (err) {
152
+ logError(`Processing ${filePath}:`, err.message);
153
+ }
165
154
  }
166
155
 
167
156
  /**
@@ -240,45 +229,32 @@ function startWatching() {
240
229
  }
241
230
  saveState();
242
231
 
243
- // Set up fs.watch on each path (works well on macOS/Linux)
244
- const watchers = [];
245
- for (const watchPath of watchPaths) {
246
- try {
247
- const w = fs.watch(watchPath, { recursive: true }, (eventType, filename) => {
248
- if (!filename) return;
249
- const fullPath = path.join(watchPath, filename);
250
- handleFileChange(fullPath);
251
- });
252
-
253
- w.on('error', (err) => {
254
- logError(`Watcher error on ${watchPath}:`, err.message);
255
- });
256
-
257
- watchers.push(w);
258
- log('Watching (fs.watch):', watchPath);
259
- } catch (err) {
260
- logError(`Could not watch ${watchPath}:`, err.message);
261
- }
262
- }
263
-
264
- // Polling fallback — fs.watch is unreliable on Windows for deeply nested dirs.
265
- // 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.
266
234
  const POLL_INTERVAL = 5_000;
267
- const pollInterval = setInterval(() => {
268
- for (const watchPath of watchPaths) {
269
- const files = findJsonlFiles(watchPath);
270
- for (const file of files) {
271
- try {
272
- const currentSize = fs.statSync(file).size;
273
- const lastOffset = getOffset(file);
274
- if (currentSize > lastOffset) {
275
- handleFileChange(file);
276
- }
277
- } 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
+ }
278
252
  }
253
+ } finally {
254
+ polling = false;
279
255
  }
280
256
  }, POLL_INTERVAL);
281
- log(`Polling every ${POLL_INTERVAL / 1000}s as fallback`);
257
+ log(`Polling every ${POLL_INTERVAL / 1000}s`);
282
258
 
283
259
  // Periodic state save
284
260
  const saveInterval = setInterval(() => saveState(), 30_000);
@@ -287,8 +263,6 @@ function startWatching() {
287
263
  return () => {
288
264
  clearInterval(saveInterval);
289
265
  clearInterval(pollInterval);
290
- for (const w of watchers) w.close();
291
- for (const t of _debounceTimers.values()) clearTimeout(t);
292
266
  saveState();
293
267
  log('Watcher stopped.');
294
268
  };