ai-burnrate-tool 1.0.1 → 1.0.3

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.
@@ -38,6 +38,14 @@
38
38
  "cacheRead": 0.3,
39
39
  "cacheWrite": 3.75
40
40
  },
41
+ "claude-opus-4-5": {
42
+ "label": "Claude Opus 4.5+",
43
+ "provider": "anthropic",
44
+ "input": 5.0,
45
+ "output": 25.0,
46
+ "cacheRead": 0.5,
47
+ "cacheWrite": 6.25
48
+ },
41
49
  "claude-opus-4": {
42
50
  "label": "Claude Opus 4.x",
43
51
  "provider": "anthropic",
@@ -65,10 +73,10 @@
65
73
  "claude-haiku-4": {
66
74
  "label": "Claude Haiku 4.x",
67
75
  "provider": "anthropic",
68
- "input": 0.8,
69
- "output": 4.0,
70
- "cacheRead": 0.08,
71
- "cacheWrite": 1.0
76
+ "input": 1.0,
77
+ "output": 5.0,
78
+ "cacheRead": 0.1,
79
+ "cacheWrite": 1.25
72
80
  },
73
81
  "gemini-4-argon": {
74
82
  "label": "Gemini 4 Argon",
@@ -1,7 +1,85 @@
1
1
  import fs from 'node:fs';
2
+ import { createRequire } from 'node:module';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import { calculateTurnCost, loadPricing } from './pricing.js';
6
+ const require = createRequire(import.meta.url);
7
+ function loadAntigravityTitles(geminiDir) {
8
+ const titles = new Map();
9
+ let DatabaseSync;
10
+ try {
11
+ ({ DatabaseSync } = require('node:sqlite'));
12
+ }
13
+ catch {
14
+ return titles;
15
+ }
16
+ const dbCandidates = [
17
+ path.join(geminiDir, 'antigravity-cli', 'conversation_summaries.db'),
18
+ path.join(geminiDir, 'antigravity', 'conversation_summaries.db'),
19
+ path.join(geminiDir, 'conversation_summaries.db'),
20
+ ];
21
+ for (const dbPath of dbCandidates) {
22
+ if (!fs.existsSync(dbPath))
23
+ continue;
24
+ let db = null;
25
+ try {
26
+ db = new DatabaseSync(dbPath, { readOnly: true });
27
+ const rows = db
28
+ .prepare("SELECT conversation_id, title FROM conversation_summaries WHERE title != ''")
29
+ .all();
30
+ for (const row of rows) {
31
+ if (row.conversation_id && row.title && !titles.has(row.conversation_id)) {
32
+ titles.set(row.conversation_id, row.title.trim());
33
+ }
34
+ }
35
+ }
36
+ catch {
37
+ // Ignore locked or incompatible SQLite DB
38
+ }
39
+ finally {
40
+ try {
41
+ db?.close?.();
42
+ }
43
+ catch {
44
+ // ignore close error
45
+ }
46
+ }
47
+ }
48
+ return titles;
49
+ }
50
+ function cleanChatTitle(raw, maxLen = 72) {
51
+ const userReqMatch = raw.match(/<USER_REQUEST>([\s\S]*?)<\/USER_REQUEST>/i);
52
+ const base = userReqMatch ? userReqMatch[1] : raw;
53
+ const cleaned = base
54
+ .replace(/<local-command-[^>]*>[\s\S]*?<\/local-command-[^>]*>/gi, ' ')
55
+ .replace(/<command-[^>]*>[\s\S]*?<\/command-[^>]*>/gi, ' ')
56
+ .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, ' ')
57
+ .replace(/<[^>]+>/g, ' ')
58
+ .replace(/^#+\s*/g, '')
59
+ .replace(/\s+/g, ' ')
60
+ .trim();
61
+ if (!cleaned)
62
+ return '';
63
+ return cleaned.length > maxLen ? `${cleaned.slice(0, maxLen - 1).trimEnd()}…` : cleaned;
64
+ }
65
+ function extractClaudeUserText(msgContent) {
66
+ if (typeof msgContent === 'string')
67
+ return msgContent;
68
+ if (Array.isArray(msgContent)) {
69
+ return msgContent
70
+ .filter((part) => part && part.type === 'text' && typeof part.text === 'string')
71
+ .map((part) => part.text)
72
+ .join(' ');
73
+ }
74
+ return '';
75
+ }
76
+ function formatSlugTitle(slug) {
77
+ return slug
78
+ .split(/[-_]+/)
79
+ .filter(Boolean)
80
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
81
+ .join(' ');
82
+ }
5
83
  const fileCache = new Map();
6
84
  export function getWatchRoots() {
7
85
  const home = os.homedir();
@@ -126,7 +204,7 @@ function extractAntigravitySessionId(filePath) {
126
204
  return match[1];
127
205
  return path.basename(filePath, '.jsonl');
128
206
  }
129
- function parseAntigravityFile(filePath, stat, pricing) {
207
+ function parseAntigravityFile(filePath, stat, pricing, antigravityTitles) {
130
208
  let content;
131
209
  try {
132
210
  content = fs.readFileSync(filePath, 'utf-8');
@@ -139,6 +217,7 @@ function parseAntigravityFile(filePath, stat, pricing) {
139
217
  let currentModel = 'Gemini 4 Argon';
140
218
  let workspace = null;
141
219
  let lastUserPromptChars = 0;
220
+ let firstUserTitle = '';
142
221
  const turnHistory = [];
143
222
  let startedAt = '';
144
223
  let lastActiveAt = '';
@@ -164,6 +243,11 @@ function parseAntigravityFile(filePath, stat, pricing) {
164
243
  lastActiveAt = timestamp;
165
244
  if (obj.type === 'USER_INPUT' && typeof obj.content === 'string') {
166
245
  lastUserPromptChars = obj.content.length;
246
+ if (!firstUserTitle) {
247
+ const candidateTitle = cleanChatTitle(obj.content);
248
+ if (candidateTitle)
249
+ firstUserTitle = candidateTitle;
250
+ }
167
251
  const modelMatch = obj.content.match(/Model Selection`?\s+from\s+[^\r\n]+?\s+to\s+([^\r\n]+?)\.(?:\s|$)/i);
168
252
  if (modelMatch && modelMatch[1]) {
169
253
  currentModel = modelMatch[1].trim();
@@ -245,8 +329,10 @@ function parseAntigravityFile(filePath, stat, pricing) {
245
329
  }
246
330
  if (turnHistory.length === 0)
247
331
  return null;
332
+ const sessionTitle = antigravityTitles?.get(sessionId) || firstUserTitle || sessionId;
248
333
  return summarizeSession({
249
334
  sessionId,
335
+ sessionTitle,
250
336
  source: 'antigravity',
251
337
  workspace: toRelativeHomePath(workspace || 'Global / CLI'),
252
338
  filePath: toRelativeHomePath(filePath),
@@ -269,6 +355,11 @@ function parseClaudeFile(filePath, stat, pricing) {
269
355
  let startedAt = '';
270
356
  let lastActiveAt = '';
271
357
  let lastUserPromptChars = 0;
358
+ let customTitle = '';
359
+ let summaryTitle = '';
360
+ let slugTitle = '';
361
+ let firstUserTitle = '';
362
+ let fallbackSlashTitle = '';
272
363
  const turnMap = new Map();
273
364
  let fallbackCounter = 0;
274
365
  for (const rawLine of lines) {
@@ -288,6 +379,15 @@ function parseClaudeFile(filePath, stat, pricing) {
288
379
  else if (typeof obj.session_id === 'string' && obj.session_id) {
289
380
  sessionId = obj.session_id;
290
381
  }
382
+ if (typeof obj.customTitle === 'string' && obj.customTitle.trim()) {
383
+ customTitle = cleanChatTitle(obj.customTitle);
384
+ }
385
+ if (obj.type === 'summary' && typeof obj.summary === 'string' && obj.summary.trim()) {
386
+ summaryTitle = cleanChatTitle(obj.summary);
387
+ }
388
+ if (!slugTitle && typeof obj.slug === 'string' && obj.slug.trim()) {
389
+ slugTitle = obj.slug.trim();
390
+ }
291
391
  if (typeof obj.cwd === 'string' && obj.cwd.trim()) {
292
392
  workspace = obj.cwd.trim();
293
393
  }
@@ -306,6 +406,19 @@ function parseClaudeFile(filePath, stat, pricing) {
306
406
  const msgContent = obj.message?.content ?? obj.content;
307
407
  const text = typeof msgContent === 'string' ? msgContent : JSON.stringify(msgContent ?? '');
308
408
  lastUserPromptChars = text.length;
409
+ if (!firstUserTitle) {
410
+ const userText = extractClaudeUserText(msgContent);
411
+ const candidate = cleanChatTitle(userText);
412
+ if (candidate && candidate !== '[Request interrupted by user]') {
413
+ if (candidate.startsWith('/')) {
414
+ if (!fallbackSlashTitle)
415
+ fallbackSlashTitle = candidate;
416
+ }
417
+ else {
418
+ firstUserTitle = candidate;
419
+ }
420
+ }
421
+ }
309
422
  continue;
310
423
  }
311
424
  const isAssistant = obj.type === 'assistant' || obj.message?.role === 'assistant';
@@ -375,8 +488,15 @@ function parseClaudeFile(filePath, stat, pricing) {
375
488
  if (turnHistory.length === 0)
376
489
  return null;
377
490
  const resolvedWs = workspace || decodeClaudeProjectFolder(filePath);
491
+ const sessionTitle = customTitle ||
492
+ summaryTitle ||
493
+ firstUserTitle ||
494
+ (slugTitle ? formatSlugTitle(slugTitle) : '') ||
495
+ fallbackSlashTitle ||
496
+ sessionId;
378
497
  return summarizeSession({
379
498
  sessionId,
499
+ sessionTitle,
380
500
  source: 'claude',
381
501
  workspace: toRelativeHomePath(resolvedWs),
382
502
  filePath: toRelativeHomePath(filePath),
@@ -423,6 +543,7 @@ function summarizeSession(params) {
423
543
  }
424
544
  return {
425
545
  sessionId: params.sessionId,
546
+ sessionTitle: params.sessionTitle,
426
547
  source: params.source,
427
548
  workspace: params.workspace,
428
549
  workspaceName: formatWorkspaceName(params.workspace),
@@ -443,7 +564,7 @@ function summarizeSession(params) {
443
564
  turnHistory: params.turnHistory,
444
565
  };
445
566
  }
446
- export function parseSingleFile(filePath, source, pricing = loadPricing()) {
567
+ export function parseSingleFile(filePath, source, pricing = loadPricing(), antigravityTitles) {
447
568
  if (shouldSkipJsonlFile(filePath, source))
448
569
  return null;
449
570
  let stat;
@@ -454,12 +575,18 @@ export function parseSingleFile(filePath, source, pricing = loadPricing()) {
454
575
  fileCache.delete(filePath);
455
576
  return null;
456
577
  }
578
+ const resolvedTitles = source === 'antigravity' ? antigravityTitles || loadAntigravityTitles(getWatchRoots().geminiDir) : undefined;
457
579
  const cached = fileCache.get(filePath);
458
580
  if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
581
+ if (cached.session && resolvedTitles) {
582
+ const dbTitle = resolvedTitles.get(cached.session.sessionId);
583
+ if (dbTitle)
584
+ cached.session.sessionTitle = dbTitle;
585
+ }
459
586
  return cached.session;
460
587
  }
461
588
  const session = source === 'antigravity'
462
- ? parseAntigravityFile(filePath, stat, pricing)
589
+ ? parseAntigravityFile(filePath, stat, pricing, resolvedTitles)
463
590
  : parseClaudeFile(filePath, stat, pricing);
464
591
  fileCache.set(filePath, {
465
592
  mtimeMs: stat.mtimeMs,
@@ -474,11 +601,12 @@ export function scanAllLogs(forceClearCache = false) {
474
601
  }
475
602
  const pricing = loadPricing(forceClearCache);
476
603
  const { geminiDir, claudeDir } = getWatchRoots();
604
+ const antigravityTitles = loadAntigravityTitles(geminiDir);
477
605
  const geminiFiles = collectJsonlFiles(geminiDir, 'antigravity');
478
606
  const claudeFiles = collectJsonlFiles(claudeDir, 'claude');
479
607
  const sessions = [];
480
608
  for (const file of geminiFiles) {
481
- const session = parseSingleFile(file, 'antigravity', pricing);
609
+ const session = parseSingleFile(file, 'antigravity', pricing, antigravityTitles);
482
610
  if (session)
483
611
  sessions.push(session);
484
612
  }
@@ -275,6 +275,7 @@ export async function syncOfficialPricing() {
275
275
  {
276
276
  ourKey: 'claude-sonnet-4',
277
277
  litellmKeys: [
278
+ 'claude-sonnet-4-6',
278
279
  'claude-sonnet-4-5-20250929',
279
280
  'claude-sonnet-4-5',
280
281
  'anthropic.claude-sonnet-4-5-20250929-v1:0',
@@ -290,6 +291,15 @@ export async function syncOfficialPricing() {
290
291
  'claude-3-5-sonnet-latest',
291
292
  ],
292
293
  },
294
+ {
295
+ ourKey: 'claude-opus-4-5',
296
+ litellmKeys: [
297
+ 'claude-opus-4-6',
298
+ 'claude-opus-4-5-20251101',
299
+ 'claude-opus-4-5',
300
+ 'anthropic.claude-opus-4-5-20251101-v1:0',
301
+ ],
302
+ },
293
303
  {
294
304
  ourKey: 'claude-opus-4',
295
305
  litellmKeys: [
@@ -446,6 +456,9 @@ export function resolveModelKey(rawModel, source, pricing) {
446
456
  return cleaned;
447
457
  }
448
458
  if (cleaned.includes('opus')) {
459
+ if (/opus[-_.\s]?4[-_.\s]?[5-9]/.test(cleaned) && pricing.models['claude-opus-4-5']) {
460
+ return 'claude-opus-4-5';
461
+ }
449
462
  return cleaned.includes('4') ? 'claude-opus-4' : 'claude-3-opus';
450
463
  }
451
464
  if (cleaned.includes('haiku')) {