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.
- package/.cursor/commands/commit-push.md +18 -0
- package/.github/agents/architect.md +26 -0
- package/.github/agents/engineer.md +29 -0
- package/.github/agents/qa-reviewer.md +31 -0
- package/.github/agents/researcher.md +30 -0
- package/.github/agents/troubleshooter.md +33 -0
- package/.github/copilot-instructions.md +55 -0
- package/.github/pull_request_template.md +21 -0
- package/.github/workflows/build-binaries.yml +76 -0
- package/.github/workflows/ci.yml +70 -0
- package/.github/workflows/release-on-main.yml +73 -0
- package/.prompts/log.md +9 -0
- package/AGENTS.md +84 -0
- package/CHANGELOG.md +25 -0
- package/CLAUDE.md +130 -0
- package/CONTRIBUTING.md +76 -0
- package/LICENSE +22 -0
- package/README.md +165 -0
- package/bin/ai-or-die.js +203 -0
- package/docs/.nojekyll +1 -0
- package/docs/README.md +37 -0
- package/docs/adrs/0000-template.md +35 -0
- package/docs/adrs/0001-bridge-base-class.md +53 -0
- package/docs/adrs/0002-devtunnels-over-ngrok.md +56 -0
- package/docs/adrs/0003-multi-tool-architecture.md +71 -0
- package/docs/adrs/0004-cross-platform-support.md +101 -0
- package/docs/adrs/0005-single-binary-distribution.md +58 -0
- package/docs/agent-instructions/00-philosophy.md +55 -0
- package/docs/agent-instructions/01-research-and-web.md +49 -0
- package/docs/agent-instructions/02-testing-and-validation.md +63 -0
- package/docs/agent-instructions/03-tooling-and-pipelines.md +59 -0
- package/docs/architecture/bridge-pattern.md +510 -0
- package/docs/architecture/overview.md +216 -0
- package/docs/architecture/websocket-protocol.md +609 -0
- package/docs/history/README.md +26 -0
- package/docs/specs/authentication.md +167 -0
- package/docs/specs/bridges.md +210 -0
- package/docs/specs/client-app.md +308 -0
- package/docs/specs/e2e-testing.md +311 -0
- package/docs/specs/server.md +334 -0
- package/docs/specs/session-store.md +170 -0
- package/docs/specs/usage-analytics.md +342 -0
- package/nul +0 -0
- package/package.json +54 -0
- package/scripts/build-sea.js +187 -0
- package/scripts/pty-sea-shim.js +21 -0
- package/scripts/publish-both.sh +21 -0
- package/scripts/release-pr.sh +73 -0
- package/scripts/smoke-test-binary.js +190 -0
- package/scripts/validate.ps1 +25 -0
- package/scripts/validate.sh +16 -0
- package/sea-bootstrap.js +54 -0
- package/site/ADVANCED_ANALYTICS.md +174 -0
- package/site/index.html +151 -0
- package/site/script.js +17 -0
- package/site/style.css +60 -0
- package/src/base-bridge.js +340 -0
- package/src/claude-bridge.js +48 -0
- package/src/codex-bridge.js +27 -0
- package/src/copilot-bridge.js +29 -0
- package/src/gemini-bridge.js +26 -0
- package/src/public/app.js +2123 -0
- package/src/public/auth.js +244 -0
- package/src/public/icon-generator.js +26 -0
- package/src/public/icons.js +36 -0
- package/src/public/index.html +397 -0
- package/src/public/manifest.json +45 -0
- package/src/public/plan-detector.js +186 -0
- package/src/public/service-worker.js +108 -0
- package/src/public/session-manager.js +1124 -0
- package/src/public/splits.js +574 -0
- package/src/public/style.css +2090 -0
- package/src/server.js +1269 -0
- package/src/terminal-bridge.js +49 -0
- package/src/usage-analytics.js +494 -0
- package/src/usage-reader.js +895 -0
- package/src/utils/auth.js +123 -0
- 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;
|