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 +1 -1
- package/cli.js +3 -3
- package/package.json +2 -2
- package/watcher.js +56 -19
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/
|
|
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
|
-
|
|
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.
|
|
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/
|
|
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
|
-
|
|
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 (
|
|
143
|
+
if (msg.model === '<synthetic>') continue;
|
|
127
144
|
|
|
128
|
-
|
|
129
|
-
const model = obj.message.model || '';
|
|
130
|
-
if (model.includes('haiku')) continue;
|
|
145
|
+
const model = msg.model || '';
|
|
131
146
|
|
|
132
|
-
// Check
|
|
133
|
-
const contentBlocks =
|
|
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
|
|
141
|
-
|
|
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 =
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
}
|