@chrysb/alphaclaw 0.4.2 → 0.4.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.
@@ -1,838 +0,0 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const { DatabaseSync } = require("node:sqlite");
4
-
5
- const kDefaultSessionLimit = 50;
6
- const kMaxSessionLimit = 200;
7
- const kDefaultDays = 30;
8
- const kDefaultMaxPoints = 100;
9
- const kMaxMaxPoints = 1000;
10
- const kDayMs = 24 * 60 * 60 * 1000;
11
- const kTokensPerMillion = 1_000_000;
12
- const kUtcTimeZone = "UTC";
13
- const kDayKeyFormatterCache = new Map();
14
- const kGlobalModelPricing = {
15
- "claude-opus-4-6": { input: 15.0, output: 75.0 },
16
- "claude-sonnet-4-6": { input: 3.0, output: 15.0 },
17
- "claude-haiku-4-6": { input: 0.8, output: 4.0 },
18
- "gpt-5.1-codex": { input: 2.5, output: 10.0 },
19
- "gpt-5.3-codex": { input: 2.5, output: 10.0 },
20
- "gpt-4o": { input: 2.5, output: 10.0 },
21
- "gemini-3-pro-preview": { input: 1.25, output: 5.0 },
22
- "gemini-3-flash-preview": { input: 0.1, output: 0.4 },
23
- "gemini-2.0-flash": { input: 0.1, output: 0.4 },
24
- };
25
-
26
- let db = null;
27
- let usageDbPath = "";
28
-
29
- const coerceInt = (value, fallbackValue = 0) => {
30
- const parsed = Number.parseInt(String(value ?? ""), 10);
31
- return Number.isFinite(parsed) ? parsed : fallbackValue;
32
- };
33
-
34
- const clampInt = (value, minValue, maxValue, fallbackValue) =>
35
- Math.min(maxValue, Math.max(minValue, coerceInt(value, fallbackValue)));
36
-
37
- const normalizeTimeZone = (value) => {
38
- const raw = String(value || "").trim();
39
- if (!raw) return kUtcTimeZone;
40
- try {
41
- new Intl.DateTimeFormat("en-US", { timeZone: raw });
42
- return raw;
43
- } catch {
44
- return kUtcTimeZone;
45
- }
46
- };
47
-
48
- const getDayKeyFormatter = (timeZone) => {
49
- if (kDayKeyFormatterCache.has(timeZone)) {
50
- return kDayKeyFormatterCache.get(timeZone);
51
- }
52
- const formatter = new Intl.DateTimeFormat("en-US", {
53
- timeZone,
54
- year: "numeric",
55
- month: "2-digit",
56
- day: "2-digit",
57
- });
58
- kDayKeyFormatterCache.set(timeZone, formatter);
59
- return formatter;
60
- };
61
-
62
- const toTimeZoneDayKey = (timestampMs, timeZone) => {
63
- const parts = getDayKeyFormatter(timeZone).formatToParts(new Date(timestampMs));
64
- const year = parts.find((part) => part.type === "year")?.value || "0000";
65
- const month = parts.find((part) => part.type === "month")?.value || "01";
66
- const day = parts.find((part) => part.type === "day")?.value || "01";
67
- return `${year}-${month}-${day}`;
68
- };
69
-
70
- const resolvePricing = (model) => {
71
- const normalized = String(model || "").toLowerCase();
72
- if (!normalized) return null;
73
- const exact = kGlobalModelPricing[normalized];
74
- if (exact) return exact;
75
- const matchKey = Object.keys(kGlobalModelPricing).find((key) =>
76
- normalized.includes(key),
77
- );
78
- return matchKey ? kGlobalModelPricing[matchKey] : null;
79
- };
80
-
81
- const deriveCostBreakdown = ({
82
- inputTokens = 0,
83
- outputTokens = 0,
84
- cacheReadTokens = 0,
85
- cacheWriteTokens = 0,
86
- model = "",
87
- }) => {
88
- const pricing = resolvePricing(model);
89
- if (!pricing) {
90
- return {
91
- inputCost: 0,
92
- outputCost: 0,
93
- cacheReadCost: 0,
94
- cacheWriteCost: 0,
95
- totalCost: 0,
96
- pricingFound: false,
97
- };
98
- }
99
- const inputCost = (inputTokens / kTokensPerMillion) * pricing.input;
100
- const outputCost = (outputTokens / kTokensPerMillion) * pricing.output;
101
- const cacheReadCost = 0;
102
- const cacheWriteCost = (cacheWriteTokens / kTokensPerMillion) * pricing.input;
103
- return {
104
- inputCost,
105
- outputCost,
106
- cacheReadCost,
107
- cacheWriteCost,
108
- totalCost: inputCost + outputCost + cacheReadCost + cacheWriteCost,
109
- pricingFound: true,
110
- };
111
- };
112
-
113
- const ensureDb = () => {
114
- if (!db) throw new Error("Usage DB not initialized");
115
- return db;
116
- };
117
-
118
- const safeAlterTable = (database, sql) => {
119
- try {
120
- database.exec(sql);
121
- } catch (err) {
122
- const message = String(err?.message || "").toLowerCase();
123
- if (!message.includes("duplicate column name")) throw err;
124
- }
125
- };
126
-
127
- const ensureSchema = (database) => {
128
- database.exec("PRAGMA journal_mode=WAL;");
129
- database.exec("PRAGMA synchronous=NORMAL;");
130
- database.exec("PRAGMA busy_timeout=5000;");
131
- database.exec(`
132
- CREATE TABLE IF NOT EXISTS usage_events (
133
- id INTEGER PRIMARY KEY AUTOINCREMENT,
134
- timestamp INTEGER NOT NULL,
135
- session_id TEXT,
136
- session_key TEXT,
137
- run_id TEXT,
138
- provider TEXT NOT NULL,
139
- model TEXT NOT NULL,
140
- input_tokens INTEGER NOT NULL DEFAULT 0,
141
- output_tokens INTEGER NOT NULL DEFAULT 0,
142
- cache_read_tokens INTEGER NOT NULL DEFAULT 0,
143
- cache_write_tokens INTEGER NOT NULL DEFAULT 0,
144
- total_tokens INTEGER NOT NULL DEFAULT 0
145
- );
146
- `);
147
- database.exec(`
148
- CREATE INDEX IF NOT EXISTS idx_usage_events_ts
149
- ON usage_events(timestamp DESC);
150
- `);
151
- database.exec(`
152
- CREATE INDEX IF NOT EXISTS idx_usage_events_session
153
- ON usage_events(session_id);
154
- `);
155
- safeAlterTable(
156
- database,
157
- "ALTER TABLE usage_events ADD COLUMN session_key TEXT;",
158
- );
159
- database.exec(`
160
- CREATE INDEX IF NOT EXISTS idx_usage_events_session_key
161
- ON usage_events(session_key);
162
- `);
163
- database.exec(`
164
- CREATE TABLE IF NOT EXISTS usage_daily (
165
- date TEXT NOT NULL,
166
- model TEXT NOT NULL,
167
- provider TEXT,
168
- input_tokens INTEGER NOT NULL DEFAULT 0,
169
- output_tokens INTEGER NOT NULL DEFAULT 0,
170
- cache_read_tokens INTEGER NOT NULL DEFAULT 0,
171
- cache_write_tokens INTEGER NOT NULL DEFAULT 0,
172
- total_tokens INTEGER NOT NULL DEFAULT 0,
173
- turn_count INTEGER NOT NULL DEFAULT 0,
174
- PRIMARY KEY (date, model)
175
- );
176
- `);
177
- database.exec(`
178
- CREATE TABLE IF NOT EXISTS tool_events (
179
- id INTEGER PRIMARY KEY AUTOINCREMENT,
180
- timestamp INTEGER NOT NULL,
181
- session_id TEXT,
182
- session_key TEXT,
183
- tool_name TEXT NOT NULL,
184
- success INTEGER NOT NULL DEFAULT 1,
185
- duration_ms INTEGER
186
- );
187
- `);
188
- database.exec(`
189
- CREATE INDEX IF NOT EXISTS idx_tool_events_session
190
- ON tool_events(session_id);
191
- `);
192
- safeAlterTable(
193
- database,
194
- "ALTER TABLE tool_events ADD COLUMN session_key TEXT;",
195
- );
196
- database.exec(`
197
- CREATE INDEX IF NOT EXISTS idx_tool_events_session_key
198
- ON tool_events(session_key);
199
- `);
200
- };
201
-
202
- const initUsageDb = ({ rootDir }) => {
203
- const dbDir = path.join(rootDir, "db");
204
- fs.mkdirSync(dbDir, { recursive: true });
205
- usageDbPath = path.join(dbDir, "usage.db");
206
- db = new DatabaseSync(usageDbPath);
207
- ensureSchema(db);
208
- return { path: usageDbPath };
209
- };
210
-
211
- const toDayKey = (timestampMs) => new Date(timestampMs).toISOString().slice(0, 10);
212
-
213
- const getPeriodRange = (days, timeZone = kUtcTimeZone) => {
214
- const now = Date.now();
215
- const safeDays = clampInt(days, 1, 3650, kDefaultDays);
216
- const startMs = now - safeDays * kDayMs;
217
- const normalizedTimeZone = normalizeTimeZone(timeZone);
218
- const startDay = normalizedTimeZone === kUtcTimeZone
219
- ? toDayKey(startMs)
220
- : toTimeZoneDayKey(startMs, normalizedTimeZone);
221
- return { now, safeDays, startDay, timeZone: normalizedTimeZone };
222
- };
223
-
224
- const appendCostToRows = (rows) =>
225
- rows.map((row) => {
226
- const inputTokens = coerceInt(row.input_tokens);
227
- const outputTokens = coerceInt(row.output_tokens);
228
- const cacheReadTokens = coerceInt(row.cache_read_tokens);
229
- const cacheWriteTokens = coerceInt(row.cache_write_tokens);
230
- const totalTokens =
231
- coerceInt(row.total_tokens) ||
232
- inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
233
- const cost = deriveCostBreakdown({
234
- inputTokens,
235
- outputTokens,
236
- cacheReadTokens,
237
- cacheWriteTokens,
238
- model: row.model,
239
- });
240
- return {
241
- ...row,
242
- inputTokens,
243
- outputTokens,
244
- cacheReadTokens,
245
- cacheWriteTokens,
246
- totalTokens,
247
- ...cost,
248
- };
249
- });
250
-
251
- const parseAgentAndSourceFromSessionRef = (sessionRef) => {
252
- const raw = String(sessionRef || "").trim();
253
- if (!raw) {
254
- return { agent: "unknown", source: "chat" };
255
- }
256
- const parts = raw.split(":");
257
- const agent =
258
- parts[0] === "agent" && String(parts[1] || "").trim()
259
- ? String(parts[1] || "").trim()
260
- : "unknown";
261
- const source = parts.includes("hook")
262
- ? "hooks"
263
- : parts.includes("cron")
264
- ? "cron"
265
- : "chat";
266
- return { agent, source };
267
- };
268
-
269
- const getAgentCostDistribution = ({
270
- eventsRows = [],
271
- startDay = "",
272
- timeZone = kUtcTimeZone,
273
- }) => {
274
- const byAgent = new Map();
275
- const ensureAgentBucket = (agent) => {
276
- if (byAgent.has(agent)) return byAgent.get(agent);
277
- const bucket = {
278
- agent,
279
- inputTokens: 0,
280
- outputTokens: 0,
281
- cacheReadTokens: 0,
282
- cacheWriteTokens: 0,
283
- totalTokens: 0,
284
- totalCost: 0,
285
- turnCount: 0,
286
- sourceBreakdown: {
287
- chat: {
288
- source: "chat",
289
- inputTokens: 0,
290
- outputTokens: 0,
291
- cacheReadTokens: 0,
292
- cacheWriteTokens: 0,
293
- totalTokens: 0,
294
- totalCost: 0,
295
- turnCount: 0,
296
- },
297
- hooks: {
298
- source: "hooks",
299
- inputTokens: 0,
300
- outputTokens: 0,
301
- cacheReadTokens: 0,
302
- cacheWriteTokens: 0,
303
- totalTokens: 0,
304
- totalCost: 0,
305
- turnCount: 0,
306
- },
307
- cron: {
308
- source: "cron",
309
- inputTokens: 0,
310
- outputTokens: 0,
311
- cacheReadTokens: 0,
312
- cacheWriteTokens: 0,
313
- totalTokens: 0,
314
- totalCost: 0,
315
- turnCount: 0,
316
- },
317
- },
318
- };
319
- byAgent.set(agent, bucket);
320
- return bucket;
321
- };
322
-
323
- for (const eventRow of eventsRows) {
324
- const timestamp = coerceInt(eventRow.timestamp);
325
- const dayKey = timeZone === kUtcTimeZone
326
- ? toDayKey(timestamp)
327
- : toTimeZoneDayKey(timestamp, timeZone);
328
- if (dayKey < startDay) continue;
329
-
330
- const inputTokens = coerceInt(eventRow.input_tokens);
331
- const outputTokens = coerceInt(eventRow.output_tokens);
332
- const cacheReadTokens = coerceInt(eventRow.cache_read_tokens);
333
- const cacheWriteTokens = coerceInt(eventRow.cache_write_tokens);
334
- const totalTokens =
335
- coerceInt(eventRow.total_tokens) ||
336
- inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
337
- const { totalCost } = deriveCostBreakdown({
338
- inputTokens,
339
- outputTokens,
340
- cacheReadTokens,
341
- cacheWriteTokens,
342
- model: eventRow.model,
343
- });
344
- const sessionRef = String(eventRow.session_key || eventRow.session_id || "");
345
- const { agent, source } = parseAgentAndSourceFromSessionRef(sessionRef);
346
- const agentBucket = ensureAgentBucket(agent);
347
- const sourceBucket = agentBucket.sourceBreakdown[source];
348
-
349
- agentBucket.inputTokens += inputTokens;
350
- agentBucket.outputTokens += outputTokens;
351
- agentBucket.cacheReadTokens += cacheReadTokens;
352
- agentBucket.cacheWriteTokens += cacheWriteTokens;
353
- agentBucket.totalTokens += totalTokens;
354
- agentBucket.totalCost += totalCost;
355
- agentBucket.turnCount += 1;
356
-
357
- sourceBucket.inputTokens += inputTokens;
358
- sourceBucket.outputTokens += outputTokens;
359
- sourceBucket.cacheReadTokens += cacheReadTokens;
360
- sourceBucket.cacheWriteTokens += cacheWriteTokens;
361
- sourceBucket.totalTokens += totalTokens;
362
- sourceBucket.totalCost += totalCost;
363
- sourceBucket.turnCount += 1;
364
- }
365
-
366
- const agents = Array.from(byAgent.values())
367
- .map((bucket) => ({
368
- agent: bucket.agent,
369
- inputTokens: bucket.inputTokens,
370
- outputTokens: bucket.outputTokens,
371
- cacheReadTokens: bucket.cacheReadTokens,
372
- cacheWriteTokens: bucket.cacheWriteTokens,
373
- totalTokens: bucket.totalTokens,
374
- totalCost: bucket.totalCost,
375
- turnCount: bucket.turnCount,
376
- sourceBreakdown: ["chat", "hooks", "cron"].map(
377
- (source) => bucket.sourceBreakdown[source],
378
- ),
379
- }))
380
- .sort((a, b) => b.totalCost - a.totalCost);
381
-
382
- return {
383
- agents,
384
- totals: agents.reduce(
385
- (acc, agentBucket) => {
386
- acc.totalCost += Number(agentBucket.totalCost || 0);
387
- acc.totalTokens += Number(agentBucket.totalTokens || 0);
388
- acc.turnCount += Number(agentBucket.turnCount || 0);
389
- return acc;
390
- },
391
- { totalCost: 0, totalTokens: 0, turnCount: 0 },
392
- ),
393
- };
394
- };
395
-
396
- const getDailySummary = ({ days = kDefaultDays, timeZone = kUtcTimeZone } = {}) => {
397
- const database = ensureDb();
398
- const { now, safeDays, startDay, timeZone: normalizedTimeZone } = getPeriodRange(
399
- days,
400
- timeZone,
401
- );
402
- let rows = [];
403
- if (normalizedTimeZone === kUtcTimeZone) {
404
- rows = database
405
- .prepare(`
406
- SELECT
407
- date,
408
- model,
409
- provider,
410
- input_tokens,
411
- output_tokens,
412
- cache_read_tokens,
413
- cache_write_tokens,
414
- total_tokens,
415
- turn_count
416
- FROM usage_daily
417
- WHERE date >= $startDay
418
- ORDER BY date ASC, total_tokens DESC
419
- `)
420
- .all({ $startDay: startDay });
421
- } else {
422
- const lookbackMs = now - (safeDays + 2) * kDayMs;
423
- const eventRows = database
424
- .prepare(`
425
- SELECT
426
- timestamp,
427
- provider,
428
- model,
429
- input_tokens,
430
- output_tokens,
431
- cache_read_tokens,
432
- cache_write_tokens,
433
- total_tokens
434
- FROM usage_events
435
- WHERE timestamp >= $lookbackMs
436
- ORDER BY timestamp ASC
437
- `)
438
- .all({ $lookbackMs: lookbackMs });
439
- const byDateModel = new Map();
440
- for (const eventRow of eventRows) {
441
- const dayKey = toTimeZoneDayKey(coerceInt(eventRow.timestamp), normalizedTimeZone);
442
- if (dayKey < startDay) continue;
443
- const model = String(eventRow.model || "unknown");
444
- const mapKey = `${dayKey}\u0000${model}`;
445
- if (!byDateModel.has(mapKey)) {
446
- byDateModel.set(mapKey, {
447
- date: dayKey,
448
- model,
449
- provider: String(eventRow.provider || "unknown"),
450
- input_tokens: 0,
451
- output_tokens: 0,
452
- cache_read_tokens: 0,
453
- cache_write_tokens: 0,
454
- total_tokens: 0,
455
- turn_count: 0,
456
- });
457
- }
458
- const aggregate = byDateModel.get(mapKey);
459
- aggregate.input_tokens += coerceInt(eventRow.input_tokens);
460
- aggregate.output_tokens += coerceInt(eventRow.output_tokens);
461
- aggregate.cache_read_tokens += coerceInt(eventRow.cache_read_tokens);
462
- aggregate.cache_write_tokens += coerceInt(eventRow.cache_write_tokens);
463
- aggregate.total_tokens += coerceInt(eventRow.total_tokens);
464
- aggregate.turn_count += 1;
465
- if (!aggregate.provider && eventRow.provider) {
466
- aggregate.provider = String(eventRow.provider || "unknown");
467
- }
468
- }
469
- rows = Array.from(byDateModel.values()).sort((a, b) => {
470
- if (a.date === b.date) return b.total_tokens - a.total_tokens;
471
- return a.date.localeCompare(b.date);
472
- });
473
- }
474
- const enriched = appendCostToRows(rows);
475
- const lookbackMs = now - (safeDays + 2) * kDayMs;
476
- const eventsRows = database
477
- .prepare(`
478
- SELECT
479
- timestamp,
480
- session_id,
481
- session_key,
482
- model,
483
- input_tokens,
484
- output_tokens,
485
- cache_read_tokens,
486
- cache_write_tokens,
487
- total_tokens
488
- FROM usage_events
489
- WHERE timestamp >= $lookbackMs
490
- ORDER BY timestamp ASC
491
- `)
492
- .all({ $lookbackMs: lookbackMs });
493
- const costByAgent = getAgentCostDistribution({
494
- eventsRows,
495
- startDay,
496
- timeZone: normalizedTimeZone,
497
- });
498
- const byDate = new Map();
499
- for (const row of enriched) {
500
- if (!byDate.has(row.date)) byDate.set(row.date, []);
501
- byDate.get(row.date).push({
502
- model: row.model,
503
- provider: row.provider,
504
- inputTokens: row.inputTokens,
505
- outputTokens: row.outputTokens,
506
- cacheReadTokens: row.cacheReadTokens,
507
- cacheWriteTokens: row.cacheWriteTokens,
508
- totalTokens: row.totalTokens,
509
- turnCount: coerceInt(row.turn_count),
510
- totalCost: row.totalCost,
511
- inputCost: row.inputCost,
512
- outputCost: row.outputCost,
513
- cacheReadCost: row.cacheReadCost,
514
- cacheWriteCost: row.cacheWriteCost,
515
- pricingFound: row.pricingFound,
516
- });
517
- }
518
- const daily = [];
519
- const totals = {
520
- inputTokens: 0,
521
- outputTokens: 0,
522
- cacheReadTokens: 0,
523
- cacheWriteTokens: 0,
524
- totalTokens: 0,
525
- totalCost: 0,
526
- turnCount: 0,
527
- modelCount: 0,
528
- };
529
- for (const [date, modelRows] of byDate.entries()) {
530
- const aggregate = modelRows.reduce(
531
- (acc, row) => ({
532
- inputTokens: acc.inputTokens + row.inputTokens,
533
- outputTokens: acc.outputTokens + row.outputTokens,
534
- cacheReadTokens: acc.cacheReadTokens + row.cacheReadTokens,
535
- cacheWriteTokens: acc.cacheWriteTokens + row.cacheWriteTokens,
536
- totalTokens: acc.totalTokens + row.totalTokens,
537
- totalCost: acc.totalCost + row.totalCost,
538
- turnCount: acc.turnCount + row.turnCount,
539
- }),
540
- {
541
- inputTokens: 0,
542
- outputTokens: 0,
543
- cacheReadTokens: 0,
544
- cacheWriteTokens: 0,
545
- totalTokens: 0,
546
- totalCost: 0,
547
- turnCount: 0,
548
- },
549
- );
550
- daily.push({ date, ...aggregate, models: modelRows });
551
- totals.inputTokens += aggregate.inputTokens;
552
- totals.outputTokens += aggregate.outputTokens;
553
- totals.cacheReadTokens += aggregate.cacheReadTokens;
554
- totals.cacheWriteTokens += aggregate.cacheWriteTokens;
555
- totals.totalTokens += aggregate.totalTokens;
556
- totals.totalCost += aggregate.totalCost;
557
- totals.turnCount += aggregate.turnCount;
558
- totals.modelCount += modelRows.length;
559
- }
560
- return {
561
- updatedAt: Date.now(),
562
- days: safeDays,
563
- timeZone: normalizedTimeZone,
564
- daily,
565
- totals,
566
- costByAgent,
567
- };
568
- };
569
-
570
- const getSessionsList = ({ limit = kDefaultSessionLimit } = {}) => {
571
- const database = ensureDb();
572
- const safeLimit = clampInt(limit, 1, kMaxSessionLimit, kDefaultSessionLimit);
573
- const rows = database
574
- .prepare(`
575
- SELECT
576
- COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) AS session_ref,
577
- MAX(session_key) AS session_key,
578
- MAX(session_id) AS session_id,
579
- MIN(timestamp) AS first_activity_ms,
580
- MAX(timestamp) AS last_activity_ms,
581
- COUNT(*) AS turn_count,
582
- SUM(input_tokens) AS input_tokens,
583
- SUM(output_tokens) AS output_tokens,
584
- SUM(cache_read_tokens) AS cache_read_tokens,
585
- SUM(cache_write_tokens) AS cache_write_tokens,
586
- SUM(total_tokens) AS total_tokens
587
- FROM usage_events
588
- WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) IS NOT NULL
589
- GROUP BY session_ref
590
- ORDER BY last_activity_ms DESC
591
- LIMIT $limit
592
- `)
593
- .all({ $limit: safeLimit });
594
- return rows.map((row) => {
595
- const modelRows = appendCostToRows(
596
- database
597
- .prepare(`
598
- SELECT
599
- model,
600
- SUM(input_tokens) AS input_tokens,
601
- SUM(output_tokens) AS output_tokens,
602
- SUM(cache_read_tokens) AS cache_read_tokens,
603
- SUM(cache_write_tokens) AS cache_write_tokens,
604
- SUM(total_tokens) AS total_tokens
605
- FROM usage_events
606
- WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
607
- GROUP BY model
608
- ORDER BY total_tokens DESC
609
- `)
610
- .all({ $sessionRef: row.session_ref }),
611
- );
612
- const dominantModel = String(modelRows[0]?.model || "");
613
- const totalCost = modelRows.reduce(
614
- (sum, modelRow) => sum + Number(modelRow.totalCost || 0),
615
- 0,
616
- );
617
- return {
618
- sessionId: row.session_ref,
619
- sessionKey: String(row.session_key || ""),
620
- rawSessionId: String(row.session_id || ""),
621
- firstActivityMs: coerceInt(row.first_activity_ms),
622
- lastActivityMs: coerceInt(row.last_activity_ms),
623
- durationMs: Math.max(
624
- 0,
625
- coerceInt(row.last_activity_ms) - coerceInt(row.first_activity_ms),
626
- ),
627
- turnCount: coerceInt(row.turn_count),
628
- inputTokens: coerceInt(row.input_tokens),
629
- outputTokens: coerceInt(row.output_tokens),
630
- cacheReadTokens: coerceInt(row.cache_read_tokens),
631
- cacheWriteTokens: coerceInt(row.cache_write_tokens),
632
- totalTokens: coerceInt(row.total_tokens),
633
- totalCost,
634
- dominantModel,
635
- };
636
- });
637
- };
638
-
639
- const getSessionDetail = ({ sessionId }) => {
640
- const safeSessionRef = String(sessionId || "").trim();
641
- if (!safeSessionRef) return null;
642
- const database = ensureDb();
643
- const summaryRow = database
644
- .prepare(`
645
- SELECT
646
- MAX(session_key) AS session_key,
647
- MAX(session_id) AS session_id,
648
- MIN(timestamp) AS first_activity_ms,
649
- MAX(timestamp) AS last_activity_ms,
650
- COUNT(*) AS turn_count,
651
- SUM(input_tokens) AS input_tokens,
652
- SUM(output_tokens) AS output_tokens,
653
- SUM(cache_read_tokens) AS cache_read_tokens,
654
- SUM(cache_write_tokens) AS cache_write_tokens,
655
- SUM(total_tokens) AS total_tokens
656
- FROM usage_events
657
- WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
658
- `)
659
- .get({ $sessionRef: safeSessionRef });
660
- if (!summaryRow || !coerceInt(summaryRow.turn_count)) return null;
661
-
662
- const modelRows = appendCostToRows(
663
- database
664
- .prepare(`
665
- SELECT
666
- provider,
667
- model,
668
- COUNT(*) AS turn_count,
669
- SUM(input_tokens) AS input_tokens,
670
- SUM(output_tokens) AS output_tokens,
671
- SUM(cache_read_tokens) AS cache_read_tokens,
672
- SUM(cache_write_tokens) AS cache_write_tokens,
673
- SUM(total_tokens) AS total_tokens
674
- FROM usage_events
675
- WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
676
- GROUP BY provider, model
677
- ORDER BY total_tokens DESC
678
- `)
679
- .all({ $sessionRef: safeSessionRef }),
680
- ).map((row) => ({
681
- provider: row.provider,
682
- model: row.model,
683
- turnCount: coerceInt(row.turn_count),
684
- inputTokens: row.inputTokens,
685
- outputTokens: row.outputTokens,
686
- cacheReadTokens: row.cacheReadTokens,
687
- cacheWriteTokens: row.cacheWriteTokens,
688
- totalTokens: row.totalTokens,
689
- totalCost: row.totalCost,
690
- inputCost: row.inputCost,
691
- outputCost: row.outputCost,
692
- cacheReadCost: row.cacheReadCost,
693
- cacheWriteCost: row.cacheWriteCost,
694
- pricingFound: row.pricingFound,
695
- }));
696
-
697
- const toolRows = database
698
- .prepare(`
699
- SELECT
700
- tool_name,
701
- COUNT(*) AS call_count,
702
- SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) AS success_count,
703
- SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) AS error_count,
704
- AVG(duration_ms) AS avg_duration_ms,
705
- MIN(duration_ms) AS min_duration_ms,
706
- MAX(duration_ms) AS max_duration_ms
707
- FROM tool_events
708
- WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
709
- GROUP BY tool_name
710
- ORDER BY call_count DESC
711
- `)
712
- .all({ $sessionRef: safeSessionRef })
713
- .map((row) => {
714
- const callCount = coerceInt(row.call_count);
715
- const successCount = coerceInt(row.success_count);
716
- const errorCount = coerceInt(row.error_count);
717
- return {
718
- toolName: row.tool_name,
719
- callCount,
720
- successCount,
721
- errorCount,
722
- errorRate: callCount > 0 ? errorCount / callCount : 0,
723
- avgDurationMs: Number(row.avg_duration_ms || 0),
724
- minDurationMs: coerceInt(row.min_duration_ms),
725
- maxDurationMs: coerceInt(row.max_duration_ms),
726
- };
727
- });
728
-
729
- const firstActivityMs = coerceInt(summaryRow.first_activity_ms);
730
- const lastActivityMs = coerceInt(summaryRow.last_activity_ms);
731
- const totalCost = modelRows.reduce(
732
- (sum, modelRow) => sum + Number(modelRow.totalCost || 0),
733
- 0,
734
- );
735
-
736
- return {
737
- sessionId: safeSessionRef,
738
- sessionKey: String(summaryRow.session_key || ""),
739
- rawSessionId: String(summaryRow.session_id || ""),
740
- firstActivityMs,
741
- lastActivityMs,
742
- durationMs: Math.max(0, lastActivityMs - firstActivityMs),
743
- turnCount: coerceInt(summaryRow.turn_count),
744
- inputTokens: coerceInt(summaryRow.input_tokens),
745
- outputTokens: coerceInt(summaryRow.output_tokens),
746
- cacheReadTokens: coerceInt(summaryRow.cache_read_tokens),
747
- cacheWriteTokens: coerceInt(summaryRow.cache_write_tokens),
748
- totalTokens: coerceInt(summaryRow.total_tokens),
749
- totalCost,
750
- modelBreakdown: modelRows,
751
- toolUsage: toolRows,
752
- };
753
- };
754
-
755
- const downsamplePoints = (points, maxPoints) => {
756
- if (points.length <= maxPoints) return points;
757
- const stride = Math.ceil(points.length / maxPoints);
758
- const sampled = [];
759
- for (let index = 0; index < points.length; index += stride) {
760
- sampled.push(points[index]);
761
- }
762
- const lastPoint = points[points.length - 1];
763
- if (sampled[sampled.length - 1]?.timestamp !== lastPoint.timestamp) {
764
- sampled.push(lastPoint);
765
- }
766
- return sampled;
767
- };
768
-
769
- const getSessionTimeSeries = ({ sessionId, maxPoints = kDefaultMaxPoints }) => {
770
- const safeSessionRef = String(sessionId || "").trim();
771
- if (!safeSessionRef) return { sessionId: safeSessionRef, points: [] };
772
- const database = ensureDb();
773
- const rows = database
774
- .prepare(`
775
- SELECT
776
- timestamp,
777
- session_key,
778
- session_id,
779
- model,
780
- input_tokens,
781
- output_tokens,
782
- cache_read_tokens,
783
- cache_write_tokens,
784
- total_tokens
785
- FROM usage_events
786
- WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
787
- ORDER BY timestamp ASC
788
- `)
789
- .all({ $sessionRef: safeSessionRef });
790
- let cumulativeTokens = 0;
791
- let cumulativeCost = 0;
792
- const points = rows.map((row) => {
793
- const inputTokens = coerceInt(row.input_tokens);
794
- const outputTokens = coerceInt(row.output_tokens);
795
- const cacheReadTokens = coerceInt(row.cache_read_tokens);
796
- const cacheWriteTokens = coerceInt(row.cache_write_tokens);
797
- const totalTokens =
798
- coerceInt(row.total_tokens) ||
799
- inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
800
- const cost = deriveCostBreakdown({
801
- inputTokens,
802
- outputTokens,
803
- cacheReadTokens,
804
- cacheWriteTokens,
805
- model: row.model,
806
- });
807
- cumulativeTokens += totalTokens;
808
- cumulativeCost += cost.totalCost;
809
- return {
810
- timestamp: coerceInt(row.timestamp),
811
- sessionKey: String(row.session_key || ""),
812
- rawSessionId: String(row.session_id || ""),
813
- model: String(row.model || ""),
814
- inputTokens,
815
- outputTokens,
816
- cacheReadTokens,
817
- cacheWriteTokens,
818
- totalTokens,
819
- cost: cost.totalCost,
820
- cumulativeTokens,
821
- cumulativeCost,
822
- };
823
- });
824
- const safeMaxPoints = clampInt(maxPoints, 10, kMaxMaxPoints, kDefaultMaxPoints);
825
- return {
826
- sessionId: safeSessionRef,
827
- points: downsamplePoints(points, safeMaxPoints),
828
- };
829
- };
830
-
831
- module.exports = {
832
- initUsageDb,
833
- getDailySummary,
834
- getSessionsList,
835
- getSessionDetail,
836
- getSessionTimeSeries,
837
- kGlobalModelPricing,
838
- };