ai-or-die 0.1.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 (78) hide show
  1. package/.cursor/commands/commit-push.md +18 -0
  2. package/.github/agents/architect.md +26 -0
  3. package/.github/agents/engineer.md +29 -0
  4. package/.github/agents/qa-reviewer.md +31 -0
  5. package/.github/agents/researcher.md +30 -0
  6. package/.github/agents/troubleshooter.md +33 -0
  7. package/.github/copilot-instructions.md +55 -0
  8. package/.github/pull_request_template.md +21 -0
  9. package/.github/workflows/build-binaries.yml +76 -0
  10. package/.github/workflows/ci.yml +70 -0
  11. package/.github/workflows/release-on-main.yml +73 -0
  12. package/.prompts/log.md +9 -0
  13. package/AGENTS.md +84 -0
  14. package/CHANGELOG.md +25 -0
  15. package/CLAUDE.md +130 -0
  16. package/CONTRIBUTING.md +76 -0
  17. package/LICENSE +22 -0
  18. package/README.md +165 -0
  19. package/bin/ai-or-die.js +203 -0
  20. package/docs/.nojekyll +1 -0
  21. package/docs/README.md +37 -0
  22. package/docs/adrs/0000-template.md +35 -0
  23. package/docs/adrs/0001-bridge-base-class.md +53 -0
  24. package/docs/adrs/0002-devtunnels-over-ngrok.md +56 -0
  25. package/docs/adrs/0003-multi-tool-architecture.md +71 -0
  26. package/docs/adrs/0004-cross-platform-support.md +101 -0
  27. package/docs/adrs/0005-single-binary-distribution.md +58 -0
  28. package/docs/agent-instructions/00-philosophy.md +55 -0
  29. package/docs/agent-instructions/01-research-and-web.md +49 -0
  30. package/docs/agent-instructions/02-testing-and-validation.md +63 -0
  31. package/docs/agent-instructions/03-tooling-and-pipelines.md +59 -0
  32. package/docs/architecture/bridge-pattern.md +510 -0
  33. package/docs/architecture/overview.md +216 -0
  34. package/docs/architecture/websocket-protocol.md +609 -0
  35. package/docs/history/README.md +26 -0
  36. package/docs/specs/authentication.md +167 -0
  37. package/docs/specs/bridges.md +210 -0
  38. package/docs/specs/client-app.md +308 -0
  39. package/docs/specs/e2e-testing.md +311 -0
  40. package/docs/specs/server.md +334 -0
  41. package/docs/specs/session-store.md +170 -0
  42. package/docs/specs/usage-analytics.md +342 -0
  43. package/nul +0 -0
  44. package/package.json +54 -0
  45. package/scripts/build-sea.js +187 -0
  46. package/scripts/pty-sea-shim.js +21 -0
  47. package/scripts/publish-both.sh +21 -0
  48. package/scripts/release-pr.sh +73 -0
  49. package/scripts/smoke-test-binary.js +190 -0
  50. package/scripts/validate.ps1 +25 -0
  51. package/scripts/validate.sh +16 -0
  52. package/sea-bootstrap.js +54 -0
  53. package/site/ADVANCED_ANALYTICS.md +174 -0
  54. package/site/index.html +151 -0
  55. package/site/script.js +17 -0
  56. package/site/style.css +60 -0
  57. package/src/base-bridge.js +340 -0
  58. package/src/claude-bridge.js +48 -0
  59. package/src/codex-bridge.js +27 -0
  60. package/src/copilot-bridge.js +29 -0
  61. package/src/gemini-bridge.js +26 -0
  62. package/src/public/app.js +2123 -0
  63. package/src/public/auth.js +244 -0
  64. package/src/public/icon-generator.js +26 -0
  65. package/src/public/icons.js +36 -0
  66. package/src/public/index.html +397 -0
  67. package/src/public/manifest.json +45 -0
  68. package/src/public/plan-detector.js +186 -0
  69. package/src/public/service-worker.js +108 -0
  70. package/src/public/session-manager.js +1124 -0
  71. package/src/public/splits.js +574 -0
  72. package/src/public/style.css +2090 -0
  73. package/src/server.js +1269 -0
  74. package/src/terminal-bridge.js +49 -0
  75. package/src/usage-analytics.js +494 -0
  76. package/src/usage-reader.js +895 -0
  77. package/src/utils/auth.js +123 -0
  78. package/src/utils/session-store.js +181 -0
@@ -0,0 +1,895 @@
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const readline = require('readline');
5
+ const { createReadStream } = require('fs');
6
+
7
+ class UsageReader {
8
+ constructor(sessionDurationHours = 5) {
9
+ this.claudeProjectsPath = path.join(os.homedir(), '.claude', 'projects');
10
+ this.cache = null;
11
+ this.cacheTime = null;
12
+ this.cacheTimeout = 5000; // Cache for 5 seconds for more real-time updates
13
+ this.sessionDurationHours = sessionDurationHours; // Default 5 hours from first message
14
+ this.overlappingSessions = []; // Track overlapping sessions
15
+ }
16
+
17
+ /**
18
+ * Normalize model names for consistent categorization
19
+ */
20
+ normalizeModelName(model) {
21
+ if (!model || typeof model !== 'string') {
22
+ return 'unknown';
23
+ }
24
+
25
+ const modelLower = model.toLowerCase();
26
+
27
+ if (modelLower.includes('opus')) {
28
+ return 'opus';
29
+ } else if (modelLower.includes('sonnet')) {
30
+ return 'sonnet';
31
+ } else if (modelLower.includes('haiku')) {
32
+ return 'haiku';
33
+ }
34
+
35
+ return 'unknown';
36
+ }
37
+
38
+ /**
39
+ * Create unique hash for deduplication based on message_id and request_id
40
+ */
41
+ createUniqueHash(entry) {
42
+ // Extract message ID from various possible locations
43
+ const messageId = entry.message_id ||
44
+ entry.messageId ||
45
+ (entry.message && entry.message.id) ||
46
+ null;
47
+
48
+ // Extract request ID from various possible locations
49
+ const requestId = entry.request_id ||
50
+ entry.requestId ||
51
+ null;
52
+
53
+ // Create hash if we have both IDs
54
+ if (messageId && requestId) {
55
+ return `${messageId}:${requestId}`;
56
+ }
57
+
58
+ return null;
59
+ }
60
+
61
+
62
+ async getUsageStats(hoursBack = 24) {
63
+ // Use cache if fresh
64
+ if (this.cache && this.cacheTime && (Date.now() - this.cacheTime < this.cacheTimeout)) {
65
+ return this.cache;
66
+ }
67
+
68
+ try {
69
+
70
+ const cutoffTime = new Date(Date.now() - (hoursBack * 60 * 60 * 1000));
71
+ const entries = await this.readAllEntries(cutoffTime);
72
+
73
+ // Calculate statistics
74
+ const stats = this.calculateStats(entries, hoursBack);
75
+
76
+ // Cache the results
77
+ this.cache = stats;
78
+ this.cacheTime = Date.now();
79
+
80
+ return stats;
81
+ } catch (error) {
82
+ console.error('Error reading usage stats:', error);
83
+ return null;
84
+ }
85
+ }
86
+
87
+ async getCurrentSessionStats() {
88
+ try {
89
+
90
+ // Use new session logic based on daily boundaries and cascading 5-hour sessions
91
+ const currentSession = await this.getCurrentSession();
92
+
93
+ if (!currentSession) {
94
+ return null;
95
+ }
96
+
97
+ // Get all entries for the current day
98
+ const startOfDay = this.getStartOfCurrentDay();
99
+ const allTodayEntries = await this.readAllEntries(startOfDay);
100
+
101
+ if (allTodayEntries.length === 0) {
102
+ return null;
103
+ }
104
+
105
+ // Filter entries to only include those in the current session
106
+ const sessionEntries = allTodayEntries.filter(entry => {
107
+ const entryTime = new Date(entry.timestamp);
108
+ return entryTime >= currentSession.startTime && entryTime <= currentSession.endTime;
109
+ });
110
+
111
+ // Sort entries chronologically
112
+ sessionEntries.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
113
+
114
+ // Calculate statistics for the current session window
115
+ const stats = {
116
+ requests: 0,
117
+ inputTokens: 0,
118
+ outputTokens: 0,
119
+ cacheCreationTokens: 0,
120
+ cacheReadTokens: 0,
121
+ cacheTokens: 0,
122
+ totalTokens: 0,
123
+ totalCost: 0,
124
+ models: {},
125
+ sessionStartTime: currentSession.startTime.toISOString(),
126
+ lastUpdate: null,
127
+ sessionId: currentSession.sessionId,
128
+ sessionNumber: currentSession.sessionNumber, // Add session number
129
+ isExpired: new Date() > currentSession.endTime,
130
+ remainingTokens: null
131
+ };
132
+
133
+ // Aggregate session data
134
+ for (const entry of sessionEntries) {
135
+ stats.requests++;
136
+ stats.inputTokens += entry.inputTokens;
137
+ stats.outputTokens += entry.outputTokens;
138
+ stats.cacheCreationTokens += entry.cacheCreationTokens;
139
+ stats.cacheReadTokens += entry.cacheReadTokens;
140
+ stats.totalCost += entry.totalCost;
141
+ stats.lastUpdate = entry.timestamp;
142
+
143
+ // Track by model
144
+ const model = entry.model || 'unknown';
145
+ if (!stats.models[model]) {
146
+ stats.models[model] = {
147
+ requests: 0,
148
+ inputTokens: 0,
149
+ outputTokens: 0,
150
+ cost: 0
151
+ };
152
+ }
153
+
154
+ stats.models[model].requests++;
155
+ stats.models[model].inputTokens += entry.inputTokens;
156
+ stats.models[model].outputTokens += entry.outputTokens;
157
+ stats.models[model].cost += entry.totalCost;
158
+ }
159
+
160
+ stats.cacheTokens = stats.cacheCreationTokens + stats.cacheReadTokens;
161
+ // Total tokens only includes input and output (matching claude-monitor behavior)
162
+ stats.totalTokens = stats.inputTokens + stats.outputTokens;
163
+
164
+ return stats;
165
+ } catch (error) {
166
+ console.error('Error reading current session stats:', error);
167
+ return null;
168
+ }
169
+ }
170
+
171
+ async getAllTimeUsageStats() {
172
+ try {
173
+
174
+ // Read ALL entries from ALL projects (no time cutoff)
175
+ const entries = await this.readAllEntries(new Date(0));
176
+
177
+ // Calculate statistics for all time
178
+ const stats = {
179
+ requests: 0,
180
+ inputTokens: 0,
181
+ outputTokens: 0,
182
+ cacheCreationTokens: 0,
183
+ cacheReadTokens: 0,
184
+ cacheTokens: 0,
185
+ totalTokens: 0,
186
+ totalCost: 0,
187
+ models: {},
188
+ firstRequest: null,
189
+ lastRequest: null
190
+ };
191
+
192
+ // Aggregate all data
193
+ for (const entry of entries) {
194
+ stats.requests++;
195
+ stats.inputTokens += entry.inputTokens;
196
+ stats.outputTokens += entry.outputTokens;
197
+ stats.cacheCreationTokens += entry.cacheCreationTokens;
198
+ stats.cacheReadTokens += entry.cacheReadTokens;
199
+ stats.totalCost += entry.totalCost;
200
+
201
+ // Track first and last request times
202
+ if (!stats.firstRequest || new Date(entry.timestamp) < new Date(stats.firstRequest)) {
203
+ stats.firstRequest = entry.timestamp;
204
+ }
205
+ if (!stats.lastRequest || new Date(entry.timestamp) > new Date(stats.lastRequest)) {
206
+ stats.lastRequest = entry.timestamp;
207
+ }
208
+
209
+ // Track by model
210
+ const model = entry.model || 'unknown';
211
+ if (!stats.models[model]) {
212
+ stats.models[model] = {
213
+ requests: 0,
214
+ inputTokens: 0,
215
+ outputTokens: 0,
216
+ cost: 0
217
+ };
218
+ }
219
+
220
+ stats.models[model].requests++;
221
+ stats.models[model].inputTokens += entry.inputTokens;
222
+ stats.models[model].outputTokens += entry.outputTokens;
223
+ stats.models[model].cost += entry.totalCost;
224
+ }
225
+
226
+ stats.cacheTokens = stats.cacheCreationTokens + stats.cacheReadTokens;
227
+ // Total tokens only includes input and output (matching claude-monitor behavior)
228
+ stats.totalTokens = stats.inputTokens + stats.outputTokens;
229
+
230
+ return stats;
231
+ } catch (error) {
232
+ console.error('Error reading all-time usage stats:', error);
233
+ return null;
234
+ }
235
+ }
236
+
237
+ async readAllEntries(cutoffTime) {
238
+ const entries = [];
239
+
240
+ try {
241
+ // Find all JSONL files
242
+ const files = await this.findJsonlFiles();
243
+
244
+ // Read entries from each file
245
+ for (const file of files) {
246
+ const fileEntries = await this.readJsonlFile(file, cutoffTime);
247
+ entries.push(...fileEntries);
248
+ }
249
+
250
+ // Sort by timestamp
251
+ entries.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
252
+
253
+ return entries;
254
+ } catch (error) {
255
+ console.error('Error reading entries:', error);
256
+ return [];
257
+ }
258
+ }
259
+
260
+ async readRecentEntries(cutoffTime) {
261
+ const entries = [];
262
+
263
+ try {
264
+ // Find only JSONL files modified in the last 24 hours
265
+ const files = await this.findJsonlFiles(true);
266
+
267
+ // Read entries from each recent file
268
+ for (const file of files) {
269
+ const fileEntries = await this.readJsonlFile(file, cutoffTime);
270
+ entries.push(...fileEntries);
271
+ }
272
+
273
+ // Sort by timestamp
274
+ entries.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
275
+
276
+ return entries;
277
+ } catch (error) {
278
+ console.error('Error reading recent entries:', error);
279
+ return [];
280
+ }
281
+ }
282
+
283
+ async getMostRecentSessionFile() {
284
+ try {
285
+ // Get the current working directory to find the right project folder
286
+ const cwd = process.cwd();
287
+ // Claude uses format: -home-user-Development-project
288
+ const projectDirName = cwd.replace(/[\\/]/g, '-'); // Handle both Unix / and Windows \ separators
289
+ let projectPath = path.join(this.claudeProjectsPath, projectDirName);
290
+
291
+ // Check if the project directory exists
292
+ try {
293
+ await fs.access(projectPath);
294
+ } catch (err) {
295
+ console.log(`Project directory not found: ${projectPath}`);
296
+ return null;
297
+ }
298
+
299
+ // Get all JSONL files in the project directory
300
+ const files = await fs.readdir(projectPath);
301
+ const jsonlFiles = files.filter(f => f.endsWith('.jsonl'));
302
+
303
+ if (jsonlFiles.length === 0) {
304
+ return null;
305
+ }
306
+
307
+ // Get file stats and find the most recently modified
308
+ let mostRecentFile = null;
309
+ let mostRecentTime = 0;
310
+
311
+ for (const file of jsonlFiles) {
312
+ const filePath = path.join(projectPath, file);
313
+ const stat = await fs.stat(filePath);
314
+
315
+ if (stat.mtime.getTime() > mostRecentTime) {
316
+ mostRecentTime = stat.mtime.getTime();
317
+ mostRecentFile = filePath;
318
+ }
319
+ }
320
+
321
+ // Using most recent session file
322
+ return mostRecentFile;
323
+ } catch (error) {
324
+ console.error('Error finding most recent session file:', error);
325
+ return null;
326
+ }
327
+ }
328
+
329
+ async findJsonlFiles(onlyRecent = false) {
330
+ const files = [];
331
+
332
+ try {
333
+ const projectDirs = await fs.readdir(this.claudeProjectsPath);
334
+
335
+ for (const projectDir of projectDirs) {
336
+ const projectPath = path.join(this.claudeProjectsPath, projectDir);
337
+ const stat = await fs.stat(projectPath);
338
+
339
+ if (stat.isDirectory()) {
340
+ const projectFiles = await fs.readdir(projectPath);
341
+ const jsonlFiles = projectFiles.filter(f => f.endsWith('.jsonl'));
342
+
343
+ // If onlyRecent is true, only include files modified in the last 24 hours
344
+ for (const jsonlFile of jsonlFiles) {
345
+ const filePath = path.join(projectPath, jsonlFile);
346
+
347
+ if (onlyRecent) {
348
+ const fileStat = await fs.stat(filePath);
349
+ const hoursSinceModified = (Date.now() - fileStat.mtime.getTime()) / (1000 * 60 * 60);
350
+
351
+ // Only include files modified in the last 24 hours
352
+ if (hoursSinceModified <= 24) {
353
+ files.push(filePath);
354
+ }
355
+ } else {
356
+ files.push(filePath);
357
+ }
358
+ }
359
+ }
360
+ }
361
+ } catch (error) {
362
+ console.error('Error finding JSONL files:', error);
363
+ }
364
+
365
+ return files;
366
+ }
367
+
368
+ async readJsonlFile(filePath, cutoffTime) {
369
+ const entries = [];
370
+ // File-level deduplication cache - prevents duplicates within this file only
371
+ const fileProcessedEntries = new Set();
372
+
373
+ return new Promise((resolve) => {
374
+ const rl = readline.createInterface({
375
+ input: createReadStream(filePath),
376
+ crlfDelay: Infinity
377
+ });
378
+
379
+ rl.on('line', (line) => {
380
+ try {
381
+ const entry = JSON.parse(line);
382
+
383
+ // Filter by timestamp
384
+ if (entry.timestamp && new Date(entry.timestamp) >= cutoffTime) {
385
+ // Check for duplicate entries using unique hash (file-level deduplication)
386
+ const uniqueHash = this.createUniqueHash(entry);
387
+ if (uniqueHash && fileProcessedEntries.has(uniqueHash)) {
388
+ // Skip duplicate entry within this file
389
+ return;
390
+ }
391
+
392
+ // Extract relevant data - check for usage in both locations
393
+ const usage = entry.usage || (entry.message && entry.message.usage);
394
+ const rawModel = entry.model || (entry.message && entry.message.model) || 'unknown';
395
+ const model = this.normalizeModelName(rawModel);
396
+
397
+ // Check if this is an assistant message with usage data
398
+ if ((entry.type === 'assistant' || (entry.message && entry.message.role === 'assistant')) && usage) {
399
+ const inputTokens = usage.input_tokens || 0;
400
+ const outputTokens = usage.output_tokens || 0;
401
+ const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
402
+ const cacheReadTokens = usage.cache_read_input_tokens || 0;
403
+
404
+ // Calculate cost based on Claude's actual pricing model
405
+ // These prices match Claude's current cost calculations (2025)
406
+ let totalCost = 0;
407
+ if (model === 'opus') {
408
+ // Claude 4.1 Opus pricing: $15/$75 per million tokens
409
+ totalCost = (inputTokens * 0.000015) + (outputTokens * 0.000075);
410
+ // Cache costs: creation same as input, read is 10% of input
411
+ totalCost += (cacheCreationTokens * 0.000015) + (cacheReadTokens * 0.0000015);
412
+ } else if (model === 'sonnet') {
413
+ // Claude 4.0 Sonnet pricing: $3/$15 per million tokens
414
+ totalCost = (inputTokens * 0.000003) + (outputTokens * 0.000015);
415
+ totalCost += (cacheCreationTokens * 0.000003) + (cacheReadTokens * 0.0000003);
416
+ } else if (model === 'haiku') {
417
+ // Claude 3 Haiku pricing (legacy)
418
+ totalCost = (inputTokens * 0.00000025) + (outputTokens * 0.00000125);
419
+ totalCost += (cacheCreationTokens * 0.00000025) + (cacheReadTokens * 0.000000025);
420
+ }
421
+
422
+ // Use total_cost from usage if available, but check if it's in cents
423
+ let finalCost = totalCost;
424
+ if (usage.total_cost !== undefined) {
425
+ // If total_cost is greater than 1, it's likely in cents
426
+ finalCost = usage.total_cost > 1 ? usage.total_cost / 100 : usage.total_cost;
427
+ }
428
+
429
+ const processedEntry = {
430
+ timestamp: entry.timestamp,
431
+ model: model,
432
+ inputTokens: inputTokens,
433
+ outputTokens: outputTokens,
434
+ cacheCreationTokens: cacheCreationTokens,
435
+ cacheReadTokens: cacheReadTokens,
436
+ totalCost: finalCost,
437
+ sessionId: entry.sessionId,
438
+ messageId: entry.message_id || entry.messageId || (entry.message && entry.message.id) || null,
439
+ requestId: entry.request_id || entry.requestId || null
440
+ };
441
+
442
+ entries.push(processedEntry);
443
+
444
+ // Mark this entry as processed within this file if we have a unique hash
445
+ if (uniqueHash) {
446
+ fileProcessedEntries.add(uniqueHash);
447
+ }
448
+ }
449
+ }
450
+ } catch (e) {
451
+ // Ignore malformed lines
452
+ }
453
+ });
454
+
455
+ rl.on('close', () => {
456
+ resolve(entries);
457
+ });
458
+
459
+ rl.on('error', (error) => {
460
+ console.error('Error reading file:', filePath, error);
461
+ resolve(entries);
462
+ });
463
+ });
464
+ }
465
+
466
+ calculateStats(entries, hoursBack) {
467
+ if (!entries || entries.length === 0) {
468
+ return {
469
+ requests: 0,
470
+ totalTokens: 0,
471
+ inputTokens: 0,
472
+ outputTokens: 0,
473
+ cacheTokens: 0,
474
+ totalCost: 0,
475
+ periodHours: hoursBack,
476
+ firstEntry: null,
477
+ lastEntry: null,
478
+ models: {},
479
+ hourlyRate: 0,
480
+ projectedDaily: 0
481
+ };
482
+ }
483
+
484
+ const stats = {
485
+ requests: entries.length,
486
+ totalTokens: 0,
487
+ inputTokens: 0,
488
+ outputTokens: 0,
489
+ cacheCreationTokens: 0,
490
+ cacheReadTokens: 0,
491
+ cacheTokens: 0, // Combined cache tokens for display
492
+ totalCost: 0,
493
+ periodHours: hoursBack,
494
+ firstEntry: entries[0].timestamp,
495
+ lastEntry: entries[entries.length - 1].timestamp,
496
+ models: {},
497
+ hourlyRate: 0,
498
+ projectedDaily: 0
499
+ };
500
+
501
+ // Aggregate data
502
+ for (const entry of entries) {
503
+ stats.inputTokens += entry.inputTokens;
504
+ stats.outputTokens += entry.outputTokens;
505
+ stats.cacheCreationTokens += entry.cacheCreationTokens;
506
+ stats.cacheReadTokens += entry.cacheReadTokens;
507
+ stats.totalCost += entry.totalCost;
508
+
509
+ // Track by model
510
+ if (!stats.models[entry.model]) {
511
+ stats.models[entry.model] = {
512
+ requests: 0,
513
+ inputTokens: 0,
514
+ outputTokens: 0,
515
+ cost: 0
516
+ };
517
+ }
518
+
519
+ stats.models[entry.model].requests++;
520
+ stats.models[entry.model].inputTokens += entry.inputTokens;
521
+ stats.models[entry.model].outputTokens += entry.outputTokens;
522
+ stats.models[entry.model].cost += entry.totalCost;
523
+ }
524
+
525
+ stats.cacheTokens = stats.cacheCreationTokens + stats.cacheReadTokens;
526
+ // Total tokens should only include input and output (not cache creation)
527
+ // This matches Claude's actual token counting
528
+ stats.totalTokens = stats.inputTokens + stats.outputTokens;
529
+
530
+ // Calculate rates
531
+ if (entries.length > 0) {
532
+ const actualHours = (new Date(stats.lastEntry) - new Date(stats.firstEntry)) / (1000 * 60 * 60);
533
+ if (actualHours > 0) {
534
+ stats.hourlyRate = stats.requests / actualHours;
535
+ stats.projectedDaily = stats.hourlyRate * 24;
536
+
537
+ // Calculate burn rate
538
+ stats.tokensPerHour = stats.totalTokens / actualHours;
539
+ stats.costPerHour = stats.totalCost / actualHours;
540
+ }
541
+ }
542
+
543
+ // Add percentage calculations based on typical limits
544
+ // These are rough estimates - actual limits vary by plan
545
+ const estimatedDailyLimit = 100; // Rough estimate
546
+ const estimatedTokenLimit = 1000000; // Rough estimate
547
+
548
+ stats.requestPercentage = (stats.projectedDaily / estimatedDailyLimit) * 100;
549
+ stats.tokenPercentage = ((stats.tokensPerHour * 24) / estimatedTokenLimit) * 100;
550
+
551
+ return stats;
552
+ }
553
+
554
+ // Get usage for a specific Claude session ID
555
+ async getSessionUsageById(sessionId) {
556
+ try {
557
+ if (!sessionId) {
558
+ return null;
559
+ }
560
+
561
+ // Find the JSONL file for this session
562
+ const sessionFile = path.join(this.claudeProjectsPath, path.basename(process.cwd()).replace(/[^a-zA-Z0-9-]/g, '-'), `${sessionId}.jsonl`);
563
+
564
+ // Check if the file exists
565
+ try {
566
+ await fs.access(sessionFile);
567
+ } catch (err) {
568
+ // Session file not found
569
+ return null;
570
+ }
571
+
572
+ // Read all entries from this session's file
573
+ const entries = await this.readJsonlFile(sessionFile, new Date(0)); // Read all entries
574
+
575
+ // Calculate session-specific stats
576
+ const sessionStats = {
577
+ requests: 0,
578
+ inputTokens: 0,
579
+ outputTokens: 0,
580
+ cacheCreationTokens: 0,
581
+ cacheReadTokens: 0,
582
+ cacheTokens: 0,
583
+ totalCost: 0,
584
+ models: {},
585
+ sessionId: sessionId,
586
+ lastUpdate: null,
587
+ firstRequestTime: null
588
+ };
589
+
590
+ // Aggregate all session data
591
+ for (const entry of entries) {
592
+ sessionStats.requests++;
593
+ sessionStats.inputTokens += entry.inputTokens;
594
+ sessionStats.outputTokens += entry.outputTokens;
595
+ sessionStats.cacheCreationTokens += entry.cacheCreationTokens;
596
+ sessionStats.cacheReadTokens += entry.cacheReadTokens;
597
+ sessionStats.totalCost += entry.totalCost;
598
+ sessionStats.lastUpdate = entry.timestamp;
599
+
600
+ // Track the first request timestamp
601
+ if (!sessionStats.firstRequestTime) {
602
+ sessionStats.firstRequestTime = entry.timestamp;
603
+ }
604
+
605
+ // Track by model
606
+ const model = entry.model || 'unknown';
607
+ if (!sessionStats.models[model]) {
608
+ sessionStats.models[model] = {
609
+ requests: 0,
610
+ inputTokens: 0,
611
+ outputTokens: 0,
612
+ cost: 0
613
+ };
614
+ }
615
+
616
+ sessionStats.models[model].requests++;
617
+ sessionStats.models[model].inputTokens += entry.inputTokens;
618
+ sessionStats.models[model].outputTokens += entry.outputTokens;
619
+ sessionStats.models[model].cost += entry.totalCost;
620
+ }
621
+
622
+ sessionStats.cacheTokens = sessionStats.cacheCreationTokens + sessionStats.cacheReadTokens;
623
+ // Total tokens should only include input and output
624
+ sessionStats.totalTokens = sessionStats.inputTokens + sessionStats.outputTokens;
625
+
626
+ return sessionStats;
627
+ } catch (error) {
628
+ console.error('Error getting session usage:', error);
629
+ return null;
630
+ }
631
+ }
632
+
633
+ // Legacy method - keeping for compatibility
634
+ async getSessionUsage(sessionStartTime) {
635
+ // This method is kept for backward compatibility
636
+ // New implementation uses getSessionUsageById
637
+ return null;
638
+ }
639
+
640
+ // Detect overlapping sessions within rolling windows
641
+ async detectOverlappingSessions() {
642
+ try {
643
+ const now = new Date();
644
+ const lookbackHours = this.sessionDurationHours * 2; // Look back twice the session duration
645
+ const cutoff = new Date(now - lookbackHours * 60 * 60 * 1000);
646
+ const entries = await this.readAllEntries(cutoff);
647
+
648
+ if (entries.length === 0) return [];
649
+
650
+ // Group entries into sessions based on time gaps
651
+ const sessions = [];
652
+ let currentSession = null;
653
+
654
+ for (const entry of entries) {
655
+ if (!currentSession) {
656
+ currentSession = {
657
+ startTime: entry.timestamp,
658
+ endTime: new Date(new Date(entry.timestamp).getTime() + this.sessionDurationHours * 60 * 60 * 1000),
659
+ entries: [entry],
660
+ totalTokens: entry.inputTokens + entry.outputTokens,
661
+ totalCost: entry.totalCost
662
+ };
663
+ } else {
664
+ const timeSinceLastEntry = new Date(entry.timestamp) - new Date(currentSession.entries[currentSession.entries.length - 1].timestamp);
665
+ const gapHours = timeSinceLastEntry / (1000 * 60 * 60);
666
+
667
+ if (gapHours < this.sessionDurationHours) {
668
+ // Part of the same session
669
+ currentSession.entries.push(entry);
670
+ currentSession.totalTokens += entry.inputTokens + entry.outputTokens;
671
+ currentSession.totalCost += entry.totalCost;
672
+ } else {
673
+ // New session
674
+ sessions.push(currentSession);
675
+ currentSession = {
676
+ startTime: entry.timestamp,
677
+ endTime: new Date(new Date(entry.timestamp).getTime() + this.sessionDurationHours * 60 * 60 * 1000),
678
+ entries: [entry],
679
+ totalTokens: entry.inputTokens + entry.outputTokens,
680
+ totalCost: entry.totalCost
681
+ };
682
+ }
683
+ }
684
+ }
685
+
686
+ if (currentSession) {
687
+ sessions.push(currentSession);
688
+ }
689
+
690
+ // Find overlapping sessions
691
+ const overlapping = [];
692
+ for (let i = 0; i < sessions.length; i++) {
693
+ for (let j = i + 1; j < sessions.length; j++) {
694
+ const session1 = sessions[i];
695
+ const session2 = sessions[j];
696
+
697
+ // Check if sessions overlap
698
+ if (new Date(session1.startTime) < new Date(session2.endTime) &&
699
+ new Date(session2.startTime) < new Date(session1.endTime)) {
700
+ overlapping.push({
701
+ session1: session1,
702
+ session2: session2,
703
+ overlapStart: new Date(Math.max(new Date(session1.startTime), new Date(session2.startTime))),
704
+ overlapEnd: new Date(Math.min(new Date(session1.endTime), new Date(session2.endTime)))
705
+ });
706
+ }
707
+ }
708
+ }
709
+
710
+ this.overlappingSessions = overlapping;
711
+ return sessions;
712
+ } catch (error) {
713
+ console.error('Error detecting overlapping sessions:', error);
714
+ return [];
715
+ }
716
+ }
717
+
718
+ // Generate a session ID from timestamp
719
+ generateSessionId(timestamp) {
720
+ return `session_${new Date(timestamp).getTime()}`;
721
+ }
722
+
723
+ // Calculate burn rate for a given time window
724
+ async calculateBurnRate(minutes = 60) {
725
+ try {
726
+ const cutoff = new Date(Date.now() - minutes * 60 * 1000);
727
+ const entries = await this.readRecentEntries(cutoff);
728
+
729
+ if (entries.length < 2) {
730
+ return { rate: 0, confidence: 0 };
731
+ }
732
+
733
+ const totalTokens = entries.reduce((sum, e) => sum + e.inputTokens + e.outputTokens, 0);
734
+ const duration = (new Date(entries[entries.length - 1].timestamp) - new Date(entries[0].timestamp)) / 1000 / 60;
735
+
736
+ if (duration === 0) {
737
+ return { rate: 0, confidence: 0 };
738
+ }
739
+
740
+ const rate = totalTokens / duration; // tokens per minute
741
+ const confidence = Math.min(entries.length / 10, 1); // Higher confidence with more data points
742
+
743
+ return { rate, confidence, dataPoints: entries.length };
744
+ } catch (error) {
745
+ console.error('Error calculating burn rate:', error);
746
+ return { rate: 0, confidence: 0 };
747
+ }
748
+ }
749
+
750
+ // Get recent sessions for display
751
+ async getRecentSessions(limit = 5) {
752
+ try {
753
+ const entries = await this.readAllEntries(new Date(Date.now() - (24 * 60 * 60 * 1000)));
754
+
755
+ // Group by session ID
756
+ const sessions = {};
757
+ for (const entry of entries) {
758
+ const sessionId = entry.sessionId || 'unknown';
759
+ if (!sessions[sessionId]) {
760
+ sessions[sessionId] = {
761
+ sessionId,
762
+ startTime: entry.timestamp,
763
+ endTime: entry.timestamp,
764
+ requests: 0,
765
+ totalTokens: 0,
766
+ cost: 0
767
+ };
768
+ }
769
+
770
+ sessions[sessionId].endTime = entry.timestamp;
771
+ sessions[sessionId].requests++;
772
+ sessions[sessionId].totalTokens += (entry.inputTokens + entry.outputTokens);
773
+ sessions[sessionId].cost += entry.totalCost;
774
+ }
775
+
776
+ // Convert to array and sort by end time
777
+ const sessionArray = Object.values(sessions);
778
+ sessionArray.sort((a, b) => new Date(b.endTime) - new Date(a.endTime));
779
+
780
+ return sessionArray.slice(0, limit);
781
+ } catch (error) {
782
+ console.error('Error getting recent sessions:', error);
783
+ return [];
784
+ }
785
+ }
786
+
787
+ // Helper function to get start of current day (midnight)
788
+ getStartOfCurrentDay() {
789
+ const now = new Date();
790
+ const startOfDay = new Date(now);
791
+ startOfDay.setHours(0, 0, 0, 0);
792
+ return startOfDay;
793
+ }
794
+
795
+ // Helper function to find all sessions for the current day
796
+ async getDailySessionBoundaries() {
797
+ try {
798
+ const startOfDay = this.getStartOfCurrentDay();
799
+ const endOfDay = new Date(startOfDay);
800
+ endOfDay.setHours(23, 59, 59, 999);
801
+
802
+ // Get all entries for the current day
803
+ const entries = await this.readAllEntries(startOfDay);
804
+
805
+ if (entries.length === 0) {
806
+ return [];
807
+ }
808
+
809
+ // Filter entries to only include today's entries
810
+ const todayEntries = entries.filter(entry => {
811
+ const entryTime = new Date(entry.timestamp);
812
+ return entryTime >= startOfDay && entryTime <= endOfDay;
813
+ });
814
+
815
+ if (todayEntries.length === 0) {
816
+ return [];
817
+ }
818
+
819
+ // Sort entries chronologically (oldest first)
820
+ todayEntries.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
821
+
822
+ // Find session boundaries
823
+ const sessions = [];
824
+ let sessionNumber = 1;
825
+ let currentSessionStart = null;
826
+ let processedEntries = new Set();
827
+
828
+ for (const entry of todayEntries) {
829
+ if (processedEntries.has(entry.timestamp)) {
830
+ continue;
831
+ }
832
+
833
+ const entryTime = new Date(entry.timestamp);
834
+
835
+ // If no current session or this entry is after the current session ends
836
+ if (!currentSessionStart || entryTime >= new Date(currentSessionStart.getTime() + (this.sessionDurationHours * 60 * 60 * 1000))) {
837
+ // Round down to the nearest hour for session start
838
+ const sessionStart = new Date(entryTime);
839
+ sessionStart.setMinutes(0, 0, 0);
840
+
841
+ // Session ends 5 hours later or at midnight, whichever is earlier
842
+ const sessionEnd = new Date(sessionStart.getTime() + (this.sessionDurationHours * 60 * 60 * 1000));
843
+ const midnightEnd = new Date(endOfDay);
844
+ const actualSessionEnd = sessionEnd > midnightEnd ? midnightEnd : sessionEnd;
845
+
846
+ sessions.push({
847
+ sessionNumber: sessionNumber,
848
+ startTime: sessionStart,
849
+ endTime: actualSessionEnd,
850
+ sessionId: this.generateSessionId(sessionStart.toISOString())
851
+ });
852
+
853
+ currentSessionStart = sessionStart;
854
+ sessionNumber++;
855
+
856
+ // Mark all entries in this session as processed
857
+ for (const e of todayEntries) {
858
+ const eTime = new Date(e.timestamp);
859
+ if (eTime >= sessionStart && eTime <= actualSessionEnd) {
860
+ processedEntries.add(e.timestamp);
861
+ }
862
+ }
863
+ }
864
+ }
865
+
866
+ return sessions;
867
+ } catch (error) {
868
+ console.error('Error getting daily session boundaries:', error);
869
+ return [];
870
+ }
871
+ }
872
+
873
+ // Helper function to find which session is currently active
874
+ async getCurrentSession() {
875
+ try {
876
+ const now = new Date();
877
+ const sessions = await this.getDailySessionBoundaries();
878
+
879
+ // Find the session that contains the current time
880
+ for (const session of sessions) {
881
+ if (now >= session.startTime && now <= session.endTime) {
882
+ return session;
883
+ }
884
+ }
885
+
886
+ // No active session found
887
+ return null;
888
+ } catch (error) {
889
+ console.error('Error getting current session:', error);
890
+ return null;
891
+ }
892
+ }
893
+ }
894
+
895
+ module.exports = UsageReader;