getaimeter 0.9.0 → 0.10.0

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 +86 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getaimeter",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Track AI coding costs across Claude, Cursor, Codex, Copilot, and Gemini. MCP server, billing blocks, optimization recommendations.",
5
5
  "bin": {
6
6
  "aimeter": "cli.js",
package/watcher.js CHANGED
@@ -35,6 +35,9 @@ function logError(...args) {
35
35
  // Cache detected sources per file to avoid re-reading headers
36
36
  const _sourceCache = new Map();
37
37
 
38
+ // Cache conversation metadata per file: { conversationId, projectPath }
39
+ const _convMetaCache = new Map();
40
+
38
41
  // Track cumulative token counts per file for Codex CLI (which reports cumulative, not delta)
39
42
  const _codexCumulative = {};
40
43
 
@@ -140,6 +143,76 @@ function detectSource(filePath) {
140
143
  return source;
141
144
  }
142
145
 
146
+ // ---------------------------------------------------------------------------
147
+ // Conversation metadata extraction
148
+ // ---------------------------------------------------------------------------
149
+
150
+ /**
151
+ * Extract conversation ID and project path from a file.
152
+ * - conversationId: file basename without extension (unique per session)
153
+ * - projectPath: cwd from the JSONL header (Claude Code stores this in init/system messages)
154
+ *
155
+ * For subagent files, the conversation ID is inherited from the parent session.
156
+ */
157
+ function getConversationMeta(filePath) {
158
+ if (_convMetaCache.has(filePath)) return _convMetaCache.get(filePath);
159
+
160
+ const normalized = filePath.replace(/\\/g, '/');
161
+
162
+ // Conversation ID = file basename without extension
163
+ let conversationId = path.basename(filePath, '.jsonl');
164
+
165
+ // For subagent files, use the parent session UUID as conversation ID
166
+ const subagentMatch = normalized.match(/\/([^/]+)\/subagents\//);
167
+ if (subagentMatch) {
168
+ conversationId = subagentMatch[1]; // parent session UUID
169
+ }
170
+
171
+ // Extract project path (cwd) from the first few lines of the file
172
+ let projectPath = null;
173
+ try {
174
+ const fd = fs.openSync(filePath, 'r');
175
+ const buf = Buffer.alloc(Math.min(8192, fs.fstatSync(fd).size));
176
+ fs.readSync(fd, buf, 0, buf.length, 0);
177
+ fs.closeSync(fd);
178
+ const header = buf.toString('utf8');
179
+
180
+ for (const line of header.split('\n').slice(0, 10)) {
181
+ if (!line.trim()) continue;
182
+ try {
183
+ const obj = JSON.parse(line.trim());
184
+ // Claude Code: type=system or init messages have cwd
185
+ if (obj.cwd) {
186
+ projectPath = obj.cwd;
187
+ break;
188
+ }
189
+ // Some formats nest it in message or data
190
+ if (obj.message?.cwd) {
191
+ projectPath = obj.message.cwd;
192
+ break;
193
+ }
194
+ // Codex: session_meta may have cwd
195
+ if (obj.type === 'session_meta' && obj.payload?.cwd) {
196
+ projectPath = obj.payload.cwd;
197
+ break;
198
+ }
199
+ } catch {}
200
+ }
201
+ } catch {}
202
+
203
+ // Shorten project path to just the last directory name for privacy/brevity
204
+ if (projectPath) {
205
+ projectPath = projectPath.replace(/\\/g, '/').replace(/\/$/, '');
206
+ // Keep last 2 path segments: "User/project" or just "project"
207
+ const parts = projectPath.split('/');
208
+ projectPath = parts.length > 1 ? parts.slice(-2).join('/') : parts[parts.length - 1];
209
+ }
210
+
211
+ const meta = { conversationId, projectPath };
212
+ _convMetaCache.set(filePath, meta);
213
+ return meta;
214
+ }
215
+
143
216
  // ---------------------------------------------------------------------------
144
217
  // JSONL parsing — extract usage from new bytes in a transcript file
145
218
  // ---------------------------------------------------------------------------
@@ -166,6 +239,7 @@ function extractNewUsage(filePath) {
166
239
  if (lastOffset > 0 && lines.length > 0) lines.shift();
167
240
 
168
241
  const usageEvents = [];
242
+ const convMeta = getConversationMeta(filePath);
169
243
  let lineOffset = lastOffset;
170
244
  let pendingThinkingChars = 0; // Track thinking chars from streaming progress messages
171
245
 
@@ -252,6 +326,8 @@ function extractNewUsage(filePath) {
252
326
  thinkingTokens: deltaReasoning,
253
327
  cacheReadTokens: cachedTokens,
254
328
  cacheWriteTokens: 0,
329
+ conversationId: convMeta.conversationId,
330
+ projectPath: convMeta.projectPath,
255
331
  });
256
332
  continue;
257
333
  }
@@ -272,6 +348,8 @@ function extractNewUsage(filePath) {
272
348
  thinkingTokens: obj.reasoning_tokens || 0,
273
349
  cacheReadTokens: 0,
274
350
  cacheWriteTokens: 0,
351
+ conversationId: convMeta.conversationId,
352
+ projectPath: convMeta.projectPath,
275
353
  });
276
354
  continue;
277
355
  }
@@ -303,6 +381,8 @@ function extractNewUsage(filePath) {
303
381
  thinkingTokens: 0,
304
382
  cacheReadTokens: u.cacheReadTokens || 0,
305
383
  cacheWriteTokens: u.cacheWriteTokens || 0,
384
+ conversationId: convMeta.conversationId,
385
+ projectPath: convMeta.projectPath,
306
386
  });
307
387
  }
308
388
  continue;
@@ -326,6 +406,8 @@ function extractNewUsage(filePath) {
326
406
  thinkingTokens: um.thoughtsTokenCount || 0,
327
407
  cacheReadTokens: um.cachedContentTokenCount || 0,
328
408
  cacheWriteTokens: 0,
409
+ conversationId: convMeta.conversationId,
410
+ projectPath: convMeta.projectPath,
329
411
  });
330
412
  continue;
331
413
  }
@@ -384,6 +466,8 @@ function extractNewUsage(filePath) {
384
466
  thinkingTokens: estimatedThinkingTokens,
385
467
  cacheReadTokens: u.cache_read_input_tokens || 0,
386
468
  cacheWriteTokens: u.cache_creation_input_tokens || 0,
469
+ conversationId: convMeta.conversationId,
470
+ projectPath: convMeta.projectPath,
387
471
  });
388
472
  }
389
473
 
@@ -575,6 +659,8 @@ function extractCursorUsage(dbPath) {
575
659
  thinkingTokens: 0,
576
660
  cacheReadTokens: 0,
577
661
  cacheWriteTokens: 0,
662
+ conversationId: conv.composerId,
663
+ projectPath: null,
578
664
  });
579
665
  } catch {}
580
666
  }