getaimeter 0.3.2 → 0.3.4

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.
package/README.md CHANGED
@@ -74,7 +74,7 @@ It **never** sends your prompts, responses, code, or file contents.
74
74
  ## Links
75
75
 
76
76
  - **Dashboard**: [getaimeter.com](https://getaimeter.com)
77
- - **Issues**: [github.com/AlejoCJaworworski/aimeter](https://github.com/AlejoCJaworworski/aimeter)
77
+ - **Issues**: [github.com/Khavel/AIMeter](https://github.com/Khavel/AIMeter/issues)
78
78
 
79
79
  ## License
80
80
 
package/cli.js CHANGED
@@ -335,11 +335,11 @@ function runLogs() {
335
335
  const tail = allLines.slice(-lines).join('\n');
336
336
  console.log(tail);
337
337
 
338
- // Follow mode
338
+ // Follow mode — poll every second (fs.watch is unreliable on Windows)
339
339
  console.log('\n--- Watching for new entries (Ctrl+C to stop) ---\n');
340
340
 
341
341
  let fileSize = fs.statSync(logFile).size;
342
- fs.watch(logFile, () => {
342
+ setInterval(() => {
343
343
  try {
344
344
  const newSize = fs.statSync(logFile).size;
345
345
  if (newSize > fileSize) {
@@ -351,7 +351,7 @@ function runLogs() {
351
351
  fileSize = newSize;
352
352
  }
353
353
  } catch {}
354
- });
354
+ }, 1000);
355
355
  }
356
356
 
357
357
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getaimeter",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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"
@@ -38,7 +38,7 @@
38
38
  ],
39
39
  "repository": {
40
40
  "type": "git",
41
- "url": "git+https://github.com/AlejoCJaworworski/aimeter.git"
41
+ "url": "git+https://github.com/Khavel/AIMeter.git"
42
42
  },
43
43
  "homepage": "https://getaimeter.com",
44
44
  "author": "Alejandro Ceja",
package/watcher.js CHANGED
@@ -120,27 +120,46 @@ function extractNewUsage(filePath) {
120
120
  let obj;
121
121
  try { obj = JSON.parse(trimmed); } catch { continue; }
122
122
 
123
- if (obj.type !== 'assistant' || !obj.message || !obj.message.usage) continue;
123
+ // Normalize entry: support both direct assistant messages and progress-wrapped
124
+ // sub-agent messages (where haiku calls appear as type="progress" with the
125
+ // real message at obj.data.message.message).
126
+ let msg = null;
127
+ let msgId = null; // truthy only for progress entries (used for dedup)
128
+
129
+ if (obj.type === 'assistant' && obj.message && obj.message.usage) {
130
+ msg = obj.message;
131
+ } else if (
132
+ obj.type === 'progress' &&
133
+ obj.data && obj.data.message && obj.data.message.message &&
134
+ obj.data.message.message.usage
135
+ ) {
136
+ msg = obj.data.message.message;
137
+ msgId = msg.id; // progress fires multiple times; dedup by message ID
138
+ }
139
+
140
+ if (!msg) continue;
124
141
 
125
142
  // Skip synthetic/internal messages
126
- if (obj.message.model === '<synthetic>') continue;
143
+ if (msg.model === '<synthetic>') continue;
127
144
 
128
- // Skip internal Claude Code background calls (compaction, routing, etc.)
129
- const model = obj.message.model || '';
130
- if (model.includes('haiku')) continue;
145
+ const model = msg.model || '';
131
146
 
132
- // Check for thinking content blocks (appear in streaming progress messages)
133
- const contentBlocks = obj.message.content || [];
147
+ // Check content blocks for thinking tokens and completion status
148
+ const contentBlocks = msg.content || [];
149
+ const hasTextContent = contentBlocks.some(b => b.type === 'text' || b.type === 'tool_use');
134
150
  for (const block of contentBlocks) {
135
151
  if (block.type === 'thinking' && block.thinking) {
136
152
  pendingThinkingChars = Math.max(pendingThinkingChars, block.thinking.length);
137
153
  }
138
154
  }
139
155
 
140
- // Skip streaming progress messages (stop_reason: null/undefined/missing = not yet complete)
141
- if (!obj.message.stop_reason) continue;
156
+ // Skip streaming in-progress messages:
157
+ // Null stop_reason with ONLY thinking content = streaming reasoning still in progress.
158
+ // Null stop_reason WITH text/tool_use content = complete response (e.g., haiku sub-agent
159
+ // calls that never receive a stop_reason in the JSONL but are finished).
160
+ if (!msg.stop_reason && !hasTextContent) continue;
142
161
 
143
- const u = obj.message.usage;
162
+ const u = msg.usage;
144
163
 
145
164
  // Estimate thinking tokens: ~4 chars per token (conservative estimate)
146
165
  // The API doesn't separate thinking_tokens in the JSONL usage field
@@ -149,10 +168,14 @@ function extractNewUsage(filePath) {
149
168
  : 0;
150
169
  pendingThinkingChars = 0; // Reset for next turn
151
170
 
152
- // Build dedup hash — include line offset for uniqueness
153
- const hash = crypto.createHash('md5')
154
- .update(`${filePath}:${lineOffset}:${model}:${u.input_tokens || 0}:${u.output_tokens || 0}`)
155
- .digest('hex');
171
+ // Build dedup hash.
172
+ // For progress entries, use the message ID so the same message fired multiple
173
+ // times (streaming chunks) counts only once. For assistant entries, use the
174
+ // line offset as before.
175
+ const hashKey = msgId
176
+ ? `${filePath}:msgid:${msgId}`
177
+ : `${filePath}:${lineOffset}:${model}:${u.input_tokens || 0}:${u.output_tokens || 0}`;
178
+ const hash = crypto.createHash('md5').update(hashKey).digest('hex');
156
179
 
157
180
  if (isDuplicate(hash)) continue;
158
181
 
@@ -178,6 +201,10 @@ function extractNewUsage(filePath) {
178
201
  // Report usage events to backend
179
202
  // ---------------------------------------------------------------------------
180
203
 
204
+ async function sleep(ms) {
205
+ return new Promise(r => setTimeout(r, ms));
206
+ }
207
+
181
208
  async function reportEvents(events) {
182
209
  const apiKey = getApiKey();
183
210
  if (!apiKey) {
@@ -186,11 +213,21 @@ async function reportEvents(events) {
186
213
  }
187
214
 
188
215
  for (const evt of events) {
189
- const result = await postUsage(apiKey, evt);
190
- if (result.ok) {
191
- log(`Reported: ${evt.source} ${evt.model} in=${evt.inputTokens} out=${evt.outputTokens} cache_r=${evt.cacheReadTokens}`);
192
- } else {
193
- logError(`Failed to report: HTTP ${result.status} ${result.error || ''}`);
216
+ let attempt = 0;
217
+ while (attempt < 4) {
218
+ const result = await postUsage(apiKey, evt);
219
+ if (result.ok) {
220
+ log(`Reported: ${evt.source} ${evt.model} in=${evt.inputTokens} out=${evt.outputTokens} cache_r=${evt.cacheReadTokens}`);
221
+ break;
222
+ } else if (result.status === 429) {
223
+ attempt++;
224
+ const wait = attempt * 15_000; // 15s, 30s, 45s
225
+ logError(`Rate limited (429). Retry ${attempt}/3 in ${wait / 1000}s...`);
226
+ await sleep(wait);
227
+ } else {
228
+ logError(`Failed to report: HTTP ${result.status} ${result.error || ''}`);
229
+ break;
230
+ }
194
231
  }
195
232
  }
196
233
  }