@hasna/economy 0.2.43 → 0.3.1

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 (51) hide show
  1. package/dashboard/README.md +73 -0
  2. package/dist/cli/commands/brief.d.ts +4 -69
  3. package/dist/cli/commands/brief.d.ts.map +1 -1
  4. package/dist/cli/commands/extras.d.ts.map +1 -1
  5. package/dist/cli/commands/tui.d.ts +21 -5
  6. package/dist/cli/commands/tui.d.ts.map +1 -1
  7. package/dist/cli/commands/watch.d.ts.map +1 -1
  8. package/dist/cli/index.js +8227 -6395
  9. package/dist/db/database.d.ts +18 -0
  10. package/dist/db/database.d.ts.map +1 -1
  11. package/dist/db/pg-migrations.d.ts.map +1 -1
  12. package/dist/index.d.ts +1 -10
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1855 -1613
  15. package/dist/lib/analytics.d.ts +68 -0
  16. package/dist/lib/analytics.d.ts.map +1 -0
  17. package/dist/lib/brief.d.ts +72 -0
  18. package/dist/lib/brief.d.ts.map +1 -0
  19. package/dist/lib/cloud-storage.d.ts +48 -0
  20. package/dist/lib/cloud-storage.d.ts.map +1 -0
  21. package/dist/lib/contracts-client/index.d.ts +3 -0
  22. package/dist/lib/contracts-client/index.d.ts.map +1 -0
  23. package/dist/lib/contracts-client/mode.d.ts +20 -0
  24. package/dist/lib/contracts-client/mode.d.ts.map +1 -0
  25. package/dist/lib/contracts-client/storage.d.ts +83 -0
  26. package/dist/lib/contracts-client/storage.d.ts.map +1 -0
  27. package/dist/lib/contracts-client/transport.d.ts +148 -0
  28. package/dist/lib/contracts-client/transport.d.ts.map +1 -0
  29. package/dist/lib/pricing.d.ts +3 -0
  30. package/dist/lib/pricing.d.ts.map +1 -1
  31. package/dist/lib/store/index.d.ts +283 -0
  32. package/dist/lib/store/index.d.ts.map +1 -0
  33. package/dist/lib/sync-all.d.ts +0 -2
  34. package/dist/lib/sync-all.d.ts.map +1 -1
  35. package/dist/mcp/http.d.ts.map +1 -1
  36. package/dist/mcp/index.js +1934 -536
  37. package/dist/mcp/server.d.ts.map +1 -1
  38. package/dist/openapi.d.ts.map +1 -1
  39. package/dist/otel/index.js +21 -267
  40. package/dist/server/index.js +2794 -331
  41. package/dist/server/serve.d.ts.map +1 -1
  42. package/dist/types/index.d.ts +0 -1
  43. package/dist/types/index.d.ts.map +1 -1
  44. package/package.json +4 -2
  45. package/dashboard/dist/assets/charts-BtCbslx4.js +0 -36
  46. package/dashboard/dist/assets/index-BDmtt3RI.css +0 -1
  47. package/dashboard/dist/assets/index-BFwRLqg7.js +0 -6
  48. package/dashboard/dist/assets/vendor-Cz8jb743.js +0 -53
  49. package/dashboard/dist/index.html +0 -16
  50. package/dashboard/dist/logo.jpg +0 -0
  51. package/dashboard/dist/vite.svg +0 -1
package/dist/index.js CHANGED
@@ -14,14 +14,15 @@ var __export = (target, all) => {
14
14
  });
15
15
  };
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
- var __require = import.meta.require;
18
17
 
19
18
  // src/lib/pricing.ts
20
19
  var exports_pricing = {};
21
20
  __export(exports_pricing, {
22
21
  normalizeModelName: () => normalizeModelName,
22
+ getPricingFromRows: () => getPricingFromRows,
23
23
  getPricingFromDb: () => getPricingFromDb,
24
24
  getPricing: () => getPricing,
25
+ estimateCostFromRows: () => estimateCostFromRows,
25
26
  ensurePricingSeeded: () => ensurePricingSeeded,
26
27
  computeCostFromDb: () => computeCostFromDb,
27
28
  computeCost: () => computeCost,
@@ -212,6 +213,25 @@ function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens
212
213
  return 0;
213
214
  return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
214
215
  }
216
+ function getPricingFromRows(rows, model) {
217
+ if (isFreeModel(model))
218
+ return FREE_PRICING;
219
+ for (const key of modelLookupKeys(model)) {
220
+ const row = rows.find((r) => r.model === key);
221
+ if (row)
222
+ return modelPricingFromDbRow(row);
223
+ }
224
+ const match = bestModelMatch(model, rows.map((r) => [r.model, r]));
225
+ if (!match)
226
+ return null;
227
+ return modelPricingFromDbRow(match);
228
+ }
229
+ function estimateCostFromRows(rows, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
230
+ const pricing = getPricingFromRows(rows, model) ?? getPricing(model);
231
+ if (!pricing)
232
+ return 0;
233
+ return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
234
+ }
215
235
  function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
216
236
  if (isFreeModel(model))
217
237
  return 0;
@@ -514,41 +534,9 @@ var init_pricing = __esm(() => {
514
534
 
515
535
  // src/db/database.ts
516
536
  import { SqliteAdapter as Database } from "@hasna/cloud";
517
- import { execFileSync } from "child_process";
518
537
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
519
- import { hostname, platform } from "os";
520
538
  import { homedir } from "os";
521
539
  import { join } from "path";
522
- function normalizeMachineId(value) {
523
- const id = value?.trim().toLowerCase().split(".")[0];
524
- return id && id.length > 0 ? id : null;
525
- }
526
- function macHostMachineId() {
527
- if (platform() !== "darwin")
528
- return null;
529
- for (const key of ["LocalHostName", "ComputerName", "HostName"]) {
530
- try {
531
- const value = execFileSync("/usr/sbin/scutil", ["--get", key], {
532
- encoding: "utf8",
533
- stdio: ["ignore", "pipe", "ignore"],
534
- timeout: 1000
535
- });
536
- const id = normalizeMachineId(value);
537
- if (id && id !== "mac" && id !== "localhost")
538
- return id;
539
- } catch {}
540
- }
541
- return null;
542
- }
543
- function getMachineId() {
544
- const envMachine = normalizeMachineId(process.env["ECONOMY_MACHINE_ID"]);
545
- if (envMachine)
546
- return envMachine;
547
- const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
548
- if (hostMachine === "mac" || hostMachine === "localhost")
549
- return macHostMachineId() ?? hostMachine;
550
- return hostMachine;
551
- }
552
540
  function getDataDir() {
553
541
  const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
554
542
  const newDir = join(home, ".hasna", "economy");
@@ -895,56 +883,6 @@ function requestPeriodWhere(period) {
895
883
  return "1=1";
896
884
  }
897
885
  }
898
- function upsertRequest(db, req) {
899
- const now = req.updated_at ?? new Date().toISOString();
900
- db.prepare(`
901
- INSERT OR REPLACE INTO requests
902
- (id, agent, session_id, model, input_tokens, output_tokens,
903
- cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
904
- cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
905
- source_request_id, machine_id, attribution_tag, account_key, account_tool,
906
- account_name, account_email, account_source, updated_at, synced_at)
907
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
908
- `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
909
- }
910
- function upsertSession(db, session) {
911
- const now = session.updated_at ?? new Date().toISOString();
912
- db.prepare(`
913
- INSERT OR REPLACE INTO sessions
914
- (id, agent, project_path, project_name, started_at, ended_at,
915
- total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
916
- account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
917
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
918
- `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
919
- }
920
- function rollupSession(db, sessionId) {
921
- db.prepare(`
922
- UPDATE sessions SET
923
- total_cost_usd = (SELECT COALESCE(SUM(cost_usd), 0) FROM requests WHERE session_id = ?),
924
- total_tokens = (SELECT COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) FROM requests WHERE session_id = ?),
925
- request_count = (SELECT COUNT(*) FROM requests WHERE session_id = ?),
926
- ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
927
- started_at = CASE WHEN started_at = '' OR started_at IS NULL
928
- THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
929
- ELSE started_at END,
930
- account_key = CASE WHEN account_key = '' OR account_key IS NULL
931
- THEN COALESCE((SELECT account_key FROM requests WHERE session_id = ? AND account_key != '' ORDER BY timestamp DESC LIMIT 1), '')
932
- ELSE account_key END,
933
- account_tool = CASE WHEN account_tool = '' OR account_tool IS NULL
934
- THEN COALESCE((SELECT account_tool FROM requests WHERE session_id = ? AND account_tool != '' ORDER BY timestamp DESC LIMIT 1), '')
935
- ELSE account_tool END,
936
- account_name = CASE WHEN account_name = '' OR account_name IS NULL
937
- THEN COALESCE((SELECT account_name FROM requests WHERE session_id = ? AND account_name != '' ORDER BY timestamp DESC LIMIT 1), '')
938
- ELSE account_name END,
939
- account_email = CASE WHEN account_email = '' OR account_email IS NULL
940
- THEN COALESCE((SELECT account_email FROM requests WHERE session_id = ? AND account_email != '' ORDER BY timestamp DESC LIMIT 1), '')
941
- ELSE account_email END,
942
- account_source = CASE WHEN account_source = '' OR account_source IS NULL
943
- THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
944
- ELSE account_source END
945
- WHERE id = ?
946
- `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
947
- }
948
886
  function querySessions(db, filter = {}) {
949
887
  const conditions = [];
950
888
  const params = [];
@@ -1033,21 +971,6 @@ function querySummary(db, period, machine, allMachines = false, agent) {
1033
971
  period
1034
972
  };
1035
973
  }
1036
- function queryZeroCostTokenizedModels(db, limit = 10) {
1037
- return db.prepare(`
1038
- SELECT agent,
1039
- model,
1040
- COUNT(*) as requests,
1041
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1042
- MAX(timestamp) as last_seen
1043
- FROM requests
1044
- WHERE cost_usd = 0
1045
- AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)
1046
- GROUP BY agent, model
1047
- ORDER BY requests DESC, total_tokens DESC
1048
- LIMIT ?
1049
- `).all(limit);
1050
- }
1051
974
  function queryModelBreakdown(db) {
1052
975
  return db.prepare(`
1053
976
  SELECT model, agent,
@@ -1385,6 +1308,148 @@ function queryAccountBreakdown(db, period = "all", machine) {
1385
1308
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1386
1309
  return result;
1387
1310
  }
1311
+ function queryProjectBreakdownSince(db, since) {
1312
+ return db.prepare(`
1313
+ SELECT project_path, project_name,
1314
+ COUNT(*) as sessions,
1315
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
1316
+ COALESCE(SUM(request_count), 0) as requests,
1317
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1318
+ MAX(started_at) as last_active
1319
+ FROM sessions WHERE started_at >= ?
1320
+ GROUP BY project_path ORDER BY cost_usd DESC
1321
+ `).all(since);
1322
+ }
1323
+ function queryAgentBreakdownSince(db, since) {
1324
+ return db.prepare(`
1325
+ SELECT agent,
1326
+ COUNT(DISTINCT session_id) as sessions,
1327
+ COUNT(*) as requests,
1328
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1329
+ COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
1330
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
1331
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1332
+ MAX(timestamp) as last_active
1333
+ FROM requests
1334
+ WHERE timestamp >= ?
1335
+ GROUP BY agent
1336
+ ORDER BY api_equivalent_usd DESC
1337
+ `).all(since);
1338
+ }
1339
+ function queryModelBreakdownSince(db, since) {
1340
+ return db.prepare(`
1341
+ SELECT model, agent,
1342
+ COUNT(*) as requests,
1343
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
1344
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
1345
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1346
+ COALESCE(SUM(cost_usd), 0) as cost_usd
1347
+ FROM requests WHERE timestamp >= ?
1348
+ GROUP BY model, agent ORDER BY cost_usd DESC
1349
+ `).all(since);
1350
+ }
1351
+ function queryAccountBreakdownSince(db, since) {
1352
+ return db.prepare(`
1353
+ WITH request_rows AS (
1354
+ SELECT
1355
+ r.session_id as session_id,
1356
+ COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
1357
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as raw_account_key,
1358
+ COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as raw_account_name,
1359
+ LOWER(TRIM(COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), ''))) as raw_account_email,
1360
+ COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
1361
+ 1 as requests,
1362
+ COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
1363
+ COALESCE(r.cost_usd, 0) as cost_usd,
1364
+ COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
1365
+ r.timestamp as last_active
1366
+ FROM requests r
1367
+ LEFT JOIN sessions s ON s.id = r.session_id
1368
+ WHERE r.timestamp >= ?
1369
+ AND (
1370
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
1371
+ OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
1372
+ OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
1373
+ OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
1374
+ )
1375
+ ),
1376
+ session_only_rows AS (
1377
+ SELECT
1378
+ s.id as session_id,
1379
+ COALESCE(NULLIF(s.agent, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
1380
+ s.account_key as raw_account_key,
1381
+ s.account_name as raw_account_name,
1382
+ LOWER(TRIM(COALESCE(s.account_email, ''))) as raw_account_email,
1383
+ COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
1384
+ COALESCE(s.request_count, 0) as requests,
1385
+ COALESCE(s.total_tokens, 0) as total_tokens,
1386
+ COALESCE(s.total_cost_usd, 0) as cost_usd,
1387
+ 'estimated' as cost_basis,
1388
+ s.started_at as last_active
1389
+ FROM sessions s
1390
+ WHERE s.started_at >= ?
1391
+ AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
1392
+ AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
1393
+ ),
1394
+ normalized AS (
1395
+ SELECT
1396
+ CASE
1397
+ WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
1398
+ WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
1399
+ ELSE raw_account_key
1400
+ END as account_key,
1401
+ account_agent as account_tool,
1402
+ raw_account_name as account_name,
1403
+ raw_account_email as account_email,
1404
+ account_source,
1405
+ session_id,
1406
+ requests,
1407
+ total_tokens,
1408
+ cost_usd,
1409
+ cost_basis,
1410
+ last_active
1411
+ FROM request_rows
1412
+ UNION ALL
1413
+ SELECT
1414
+ CASE
1415
+ WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
1416
+ WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
1417
+ ELSE raw_account_key
1418
+ END as account_key,
1419
+ account_agent as account_tool,
1420
+ raw_account_name as account_name,
1421
+ raw_account_email as account_email,
1422
+ account_source,
1423
+ session_id,
1424
+ requests,
1425
+ total_tokens,
1426
+ cost_usd,
1427
+ cost_basis,
1428
+ last_active
1429
+ FROM session_only_rows
1430
+ )
1431
+ SELECT account_key,
1432
+ account_tool,
1433
+ COALESCE(MAX(NULLIF(account_name, '')), MAX(NULLIF(account_email, '')), account_key) as account_name,
1434
+ NULLIF(account_email, '') as account_email,
1435
+ COALESCE(MAX(NULLIF(account_source, 'unknown')), 'unknown') as account_source,
1436
+ COUNT(DISTINCT session_id) as sessions,
1437
+ COALESCE(SUM(requests), 0) as requests,
1438
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
1439
+ COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
1440
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
1441
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1442
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1443
+ COALESCE(SUM(CASE WHEN cost_basis NOT IN ('metered_api', 'subscription_included', 'unknown') THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1444
+ COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
1445
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
1446
+ MAX(last_active) as last_active
1447
+ FROM normalized
1448
+ WHERE account_key != ''
1449
+ GROUP BY account_key, account_tool, account_email
1450
+ ORDER BY api_equivalent_usd DESC
1451
+ `).all(since, since);
1452
+ }
1388
1453
  function queryDailyBreakdown(db, days = 30, machine) {
1389
1454
  const machineClause = machine ? " AND machine_id = ?" : "";
1390
1455
  const params = machine ? [`-${days}`, machine] : [`-${days}`];
@@ -1396,21 +1461,6 @@ function queryDailyBreakdown(db, days = 30, machine) {
1396
1461
  ORDER BY date ASC
1397
1462
  `).all(...params);
1398
1463
  }
1399
- function queryHourlyBreakdown(db, machine, hours) {
1400
- const clauses = hours == null ? [`DATE(timestamp) = DATE('now')`] : [`DATETIME(timestamp) >= DATETIME('now', ?)`];
1401
- const params = hours == null ? [] : [`-${hours} hours`];
1402
- if (machine) {
1403
- clauses.push("machine_id = ?");
1404
- params.push(machine);
1405
- }
1406
- return db.prepare(`
1407
- SELECT STRFTIME('%H', timestamp) as hour, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
1408
- FROM requests
1409
- WHERE ${clauses.join(" AND ")}
1410
- GROUP BY STRFTIME('%H', timestamp), agent
1411
- ORDER BY hour ASC
1412
- `).all(...params);
1413
- }
1414
1464
  function upsertProject(db, project) {
1415
1465
  db.prepare(`
1416
1466
  INSERT OR REPLACE INTO projects (id, path, name, description, tags, created_at)
@@ -1423,9 +1473,6 @@ function getProject(db, path) {
1423
1473
  return null;
1424
1474
  return { ...row, tags: JSON.parse(row["tags"] ?? "[]") };
1425
1475
  }
1426
- function listProjects(db) {
1427
- return db.prepare(`SELECT * FROM projects ORDER BY created_at DESC`).all().map((row) => ({ ...row, tags: JSON.parse(row["tags"] ?? "[]") }));
1428
- }
1429
1476
  function deleteProject(db, path) {
1430
1477
  db.prepare(`DELETE FROM projects WHERE path = ?`).run(path);
1431
1478
  }
@@ -1508,25 +1555,9 @@ function getGoalStatuses(db) {
1508
1555
  };
1509
1556
  });
1510
1557
  }
1511
- function getIngestState(db, source, key) {
1512
- const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
1513
- return row?.value ?? null;
1514
- }
1515
- function setIngestState(db, source, key, value) {
1516
- db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
1517
- }
1518
1558
  function queryRequestsSince(db, since) {
1519
1559
  return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
1520
1560
  }
1521
- function upsertBillingDaily(db, row) {
1522
- db.prepare(`
1523
- INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
1524
- VALUES (?, ?, ?, ?, ?)
1525
- `).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
1526
- }
1527
- function clearBillingRange(db, provider, fromDate, toDate) {
1528
- db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
1529
- }
1530
1561
  function queryBillingSummary(db, period) {
1531
1562
  const where = period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
1532
1563
  const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
@@ -1538,6 +1569,13 @@ function queryBillingSummary(db, period) {
1538
1569
  }
1539
1570
  return { total_usd: total, by_provider };
1540
1571
  }
1572
+ function getSessionDetail(db, id) {
1573
+ const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(id, `${id}%`);
1574
+ if (!session)
1575
+ return null;
1576
+ const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC`).all(session["id"]);
1577
+ return { session, requests };
1578
+ }
1541
1579
  function listMachines(db, period = "all") {
1542
1580
  const rWhere = requestPeriodWhere(period);
1543
1581
  const sWhere = sessionPeriodWhere(period);
@@ -1630,14 +1668,6 @@ function listSubscriptions(db) {
1630
1668
  function deleteSubscription(db, id) {
1631
1669
  db.prepare(`DELETE FROM subscriptions WHERE id = ?`).run(id);
1632
1670
  }
1633
- function upsertUsageSnapshot(db, snap) {
1634
- const now = snap.updated_at ?? new Date().toISOString();
1635
- const id = snap.id ?? `${snap.agent}-${snap.date}-${snap.metric}-${snap.machine_id}`;
1636
- db.prepare(`
1637
- INSERT OR REPLACE INTO usage_snapshots (id, agent, date, metric, value, unit, machine_id, updated_at)
1638
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1639
- `).run(id, snap.agent, snap.date, snap.metric, snap.value, snap.unit, snap.machine_id, now);
1640
- }
1641
1671
  function queryUsageSnapshots(db, opts = {}) {
1642
1672
  const conditions = [];
1643
1673
  const params = [];
@@ -1659,27 +1689,6 @@ function queryUsageSnapshots(db, opts = {}) {
1659
1689
  function listMachineRegistry(db) {
1660
1690
  return db.prepare(`SELECT * FROM machines ORDER BY last_seen_at DESC`).all();
1661
1691
  }
1662
- function dedupeRequests(db) {
1663
- const dupes = db.prepare(`
1664
- SELECT source_request_id, agent, COALESCE(machine_id, '') as machine_id, MIN(id) as keep_id, COUNT(*) as cnt
1665
- FROM requests
1666
- WHERE source_request_id != '' AND source_request_id IS NOT NULL
1667
- GROUP BY source_request_id, agent, COALESCE(machine_id, '')
1668
- HAVING cnt > 1
1669
- `).all();
1670
- let removed = 0;
1671
- for (const row of dupes) {
1672
- const result = db.prepare(`
1673
- DELETE FROM requests
1674
- WHERE source_request_id = ?
1675
- AND agent = ?
1676
- AND COALESCE(machine_id, '') = ?
1677
- AND id != ?
1678
- `).run(row.source_request_id, row.agent, row.machine_id, row.keep_id);
1679
- removed += result.changes;
1680
- }
1681
- return removed;
1682
- }
1683
1692
  var init_database = () => {};
1684
1693
 
1685
1694
  // src/lib/agents.ts
@@ -1702,1542 +1711,1775 @@ var COST_BASIS = [
1702
1711
  function isAgent(value) {
1703
1712
  return AGENTS.includes(value);
1704
1713
  }
1705
- // src/index.ts
1714
+ // src/lib/store/index.ts
1706
1715
  init_database();
1707
1716
  init_pricing();
1717
+ import { randomUUID } from "crypto";
1708
1718
 
1709
- // src/lib/gatherer.ts
1710
- init_database();
1711
- var SYSTEM_PROMPT = "You are a cost-aware AI assistant that tracks API usage, identifies expensive patterns, and helps optimize spending.";
1712
- function hasCostData(summary) {
1713
- return summary.total_usd > 0 || summary.sessions > 0 || summary.requests > 0 || summary.tokens > 0;
1714
- }
1715
- var gatherTrainingData = async (options = {}) => {
1716
- const limit = options.limit ?? 500;
1717
- const examples = [];
1718
- try {
1719
- const db = openDatabase();
1720
- const periods = ["today", "week", "month", "all"];
1721
- for (const period of periods) {
1722
- try {
1723
- const s = querySummary(db, period);
1724
- if (!hasCostData(s))
1725
- continue;
1726
- examples.push({
1727
- messages: [
1728
- { role: "system", content: SYSTEM_PROMPT },
1729
- { role: "user", content: `What did I spend on AI ${period === "all" ? "in total" : period}?` },
1730
- {
1731
- role: "assistant",
1732
- content: `${period === "all" ? "Total" : period.charAt(0).toUpperCase() + period.slice(1)} AI spending: $${s.total_usd.toFixed(4)} across ${s.sessions} session(s), ${s.requests} request(s), ${s.tokens.toLocaleString()} tokens.`
1733
- }
1734
- ]
1735
- });
1736
- } catch {}
1737
- }
1738
- const sessions = querySessions(db, {
1739
- limit: Math.min(Math.floor(limit / 4), 50),
1740
- since: options.since?.toISOString().substring(0, 10)
1741
- });
1742
- for (const s of sessions) {
1743
- examples.push({
1744
- messages: [
1745
- { role: "system", content: SYSTEM_PROMPT },
1746
- {
1747
- role: "user",
1748
- content: `How much did the session "${s.id.substring(0, 12)}" cost?`
1749
- },
1750
- {
1751
- role: "assistant",
1752
- content: `Session ${s.id.substring(0, 12)} (${s.agent}, project: ${s.project_name || "unknown"}): $${s.total_cost_usd.toFixed(4)}, ${s.total_tokens.toLocaleString()} tokens, ${s.request_count} requests. Started: ${s.started_at.substring(0, 16)}.`
1753
- }
1754
- ]
1755
- });
1756
- examples.push({
1757
- messages: [
1758
- { role: "system", content: SYSTEM_PROMPT },
1759
- {
1760
- role: "user",
1761
- content: `What was the token usage for session ${s.id.substring(0, 12)}?`
1762
- },
1763
- {
1764
- role: "assistant",
1765
- content: `Session ${s.id.substring(0, 12)} used ${s.total_tokens.toLocaleString()} tokens across ${s.request_count} requests on project "${s.project_name || "unknown"}" (${s.agent}).`
1766
- }
1767
- ]
1768
- });
1769
- if (examples.length >= limit)
1770
- break;
1771
- }
1772
- const modelBreakdown = queryModelBreakdown(db);
1773
- if (modelBreakdown.length > 0) {
1774
- const topModels = modelBreakdown.slice(0, 5);
1775
- examples.push({
1776
- messages: [
1777
- { role: "system", content: SYSTEM_PROMPT },
1778
- { role: "user", content: "Which AI models have I spent the most on?" },
1779
- {
1780
- role: "assistant",
1781
- content: `Model cost breakdown (top ${topModels.length}):
1782
- ${topModels.map((m) => `- ${m.model} (${m.agent}): $${m.cost_usd.toFixed(4)}, ${m.requests} requests, ${m.total_tokens.toLocaleString()} tokens`).join(`
1783
- `)}`
1784
- }
1785
- ]
1786
- });
1787
- for (const m of topModels) {
1788
- examples.push({
1789
- messages: [
1790
- { role: "system", content: SYSTEM_PROMPT },
1791
- { role: "user", content: `How much have I spent on ${m.model}?` },
1792
- {
1793
- role: "assistant",
1794
- content: `${m.model} (${m.agent}): $${m.cost_usd.toFixed(4)} total across ${m.requests.toLocaleString()} requests and ${m.total_tokens.toLocaleString()} tokens.`
1795
- }
1796
- ]
1797
- });
1798
- }
1799
- }
1800
- const projectBreakdown = queryProjectBreakdown(db);
1801
- if (projectBreakdown.length > 0) {
1802
- const topProjects = projectBreakdown.slice(0, 5);
1803
- examples.push({
1804
- messages: [
1805
- { role: "system", content: SYSTEM_PROMPT },
1806
- { role: "user", content: "Which projects are costing the most?" },
1807
- {
1808
- role: "assistant",
1809
- content: `Project cost breakdown (top ${topProjects.length}):
1810
- ${topProjects.map((p) => `- ${p.project_name || "unknown"}: $${p.cost_usd.toFixed(4)}, ${p.sessions} sessions`).join(`
1811
- `)}`
1812
- }
1813
- ]
1814
- });
1815
- for (const p of topProjects.slice(0, 3)) {
1816
- examples.push({
1817
- messages: [
1818
- { role: "system", content: SYSTEM_PROMPT },
1819
- { role: "user", content: `What is the AI spend for project "${p.project_name}"?` },
1820
- {
1821
- role: "assistant",
1822
- content: `Project "${p.project_name}": $${p.cost_usd.toFixed(4)} across ${p.sessions} session(s) and ${p.requests.toLocaleString()} requests. Last active: ${p.last_active?.substring(0, 10) ?? "unknown"}.`
1823
- }
1824
- ]
1825
- });
1826
- }
1827
- }
1828
- try {
1829
- const budgets = getBudgetStatuses(db);
1830
- if (budgets.length > 0) {
1831
- examples.push({
1832
- messages: [
1833
- { role: "system", content: SYSTEM_PROMPT },
1834
- { role: "user", content: "How am I tracking against my AI spending budgets?" },
1835
- {
1836
- role: "assistant",
1837
- content: `Budget status:
1838
- ${budgets.map((b) => `- ${b.project_path ?? "global"} (${b.period}): $${b.current_spend_usd.toFixed(4)} / $${b.limit_usd.toFixed(2)} (${b.percent_used.toFixed(1)}%) \u2014 ${b.is_over_limit ? "OVER LIMIT" : b.is_over_alert ? "ALERT" : "OK"}`).join(`
1839
- `)}`
1840
- }
1841
- ]
1842
- });
1843
- }
1844
- } catch {}
1845
- try {
1846
- const goals = getGoalStatuses(db);
1847
- if (goals.length > 0) {
1848
- examples.push({
1849
- messages: [
1850
- { role: "system", content: SYSTEM_PROMPT },
1851
- { role: "user", content: "Am I on track with my AI cost reduction goals?" },
1852
- {
1853
- role: "assistant",
1854
- content: `Goal progress:
1855
- ${goals.map((g) => `- ${g.period} goal (${g.project_path ?? g.agent ?? "global"}): $${g.current_spend_usd.toFixed(4)} / $${g.limit_usd.toFixed(2)} (${g.percent_used.toFixed(1)}%) \u2014 ${g.is_over ? "OVER" : g.is_at_risk ? "AT RISK" : "ON TRACK"}`).join(`
1856
- `)}`
1857
- }
1858
- ]
1859
- });
1860
- }
1861
- } catch {}
1862
- const weekSummary = querySummary(db, "week");
1863
- const monthSummary = querySummary(db, "month");
1864
- if (weekSummary.total_usd > 0) {
1865
- const projectedMonthly = weekSummary.total_usd / 7 * 30;
1866
- examples.push({
1867
- messages: [
1868
- { role: "system", content: SYSTEM_PROMPT },
1869
- { role: "user", content: "Based on my usage, what will I spend this month?" },
1870
- {
1871
- role: "assistant",
1872
- content: `Based on your last 7 days ($${weekSummary.total_usd.toFixed(4)}), projected monthly spend is approximately $${projectedMonthly.toFixed(2)}. Current month-to-date: $${monthSummary.total_usd.toFixed(4)}.`
1873
- }
1874
- ]
1875
- });
1876
- }
1877
- if (modelBreakdown.length > 1) {
1878
- const expensive = modelBreakdown[0];
1879
- const cheaper = modelBreakdown[modelBreakdown.length - 1];
1880
- examples.push({
1881
- messages: [
1882
- { role: "system", content: SYSTEM_PROMPT },
1883
- { role: "user", content: "How can I reduce my AI API costs?" },
1884
- {
1885
- role: "assistant",
1886
- content: `Your most expensive model is ${expensive.model} at $${expensive.cost_usd.toFixed(4)}. Consider switching some workloads to ${cheaper.model} ($${cheaper.cost_usd.toFixed(4)}) for cost savings. Cache frequently repeated prompts to reduce cache-miss costs.`
1887
- }
1888
- ]
1889
- });
1890
- }
1891
- } catch {}
1892
- const finalExamples = examples.slice(0, limit);
1893
- return { source: "economy", examples: finalExamples, count: finalExamples.length };
1894
- };
1895
- // src/lib/model-config.ts
1896
- init_database();
1897
- import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
1898
- import { dirname, join as join2 } from "path";
1899
- var DEFAULT_MODEL = "gpt-4o-mini";
1900
- function getModelConfigPath() {
1901
- return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join2(getDataDir(), "config.json");
1902
- }
1903
- function loadConfig() {
1904
- try {
1905
- const configPath = getModelConfigPath();
1906
- if (existsSync2(configPath)) {
1907
- return JSON.parse(readFileSync(configPath, "utf-8"));
1908
- }
1909
- } catch {}
1910
- return {};
1911
- }
1912
- function saveConfig(config) {
1913
- const configPath = getModelConfigPath();
1914
- const dir = dirname(configPath);
1915
- if (!existsSync2(dir))
1916
- mkdirSync2(dir, { recursive: true });
1917
- writeFileSync(configPath, JSON.stringify(config, null, 2) + `
1918
- `);
1919
- }
1920
- function getActiveModel() {
1921
- return loadConfig().activeModel ?? DEFAULT_MODEL;
1922
- }
1923
- function setActiveModel(id) {
1924
- const config = loadConfig();
1925
- config.activeModel = id;
1926
- saveConfig(config);
1927
- }
1928
- function clearActiveModel() {
1929
- const config = loadConfig();
1930
- delete config.activeModel;
1931
- saveConfig(config);
1932
- }
1933
- // src/lib/open-projects.ts
1934
- init_database();
1935
- async function syncOpenProjectsRegistry(db, listActiveProjects) {
1936
- let listProjects2 = listActiveProjects;
1937
- if (!listProjects2) {
1938
- const projectsApi = await import("@hasna/projects");
1939
- listProjects2 = projectsApi.listProjects;
1940
- }
1941
- const projects = listProjects2({ status: "active", limit: 5000 });
1942
- let imported = 0;
1943
- let skipped = 0;
1944
- for (const project of projects) {
1945
- if (!project.path) {
1946
- skipped++;
1947
- continue;
1948
- }
1949
- upsertProject(db, {
1950
- id: project.id,
1951
- path: project.path,
1952
- name: project.name,
1953
- description: project.description,
1954
- tags: project.tags ?? [],
1955
- created_at: project.created_at
1956
- });
1957
- imported++;
1719
+ // src/lib/savings.ts
1720
+ function periodWhere2(period, column) {
1721
+ switch (period) {
1722
+ case "today":
1723
+ return `DATE(${column}) = DATE('now')`;
1724
+ case "yesterday":
1725
+ return `DATE(${column}) = DATE('now', '-1 day')`;
1726
+ case "week":
1727
+ return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
1728
+ case "month":
1729
+ return `${column} >= DATE('now', 'start of month')`;
1730
+ case "year":
1731
+ return `${column} >= DATE('now', 'start of year')`;
1732
+ case "all":
1733
+ return "1=1";
1958
1734
  }
1959
- return { imported, skipped };
1960
- }
1961
- // src/lib/peer-sync.ts
1962
- init_database();
1963
- import { Database as BunDatabase } from "bun:sqlite";
1964
- import { existsSync as existsSync3 } from "fs";
1965
-
1966
- // src/lib/package-metadata.ts
1967
- import { readFileSync as readFileSync2 } from "fs";
1968
- var cachedMetadata = null;
1969
- function getPackageMetadata() {
1970
- if (cachedMetadata)
1971
- return cachedMetadata;
1972
- const raw = readFileSync2(new URL("../../package.json", import.meta.url), "utf8");
1973
- const parsed = JSON.parse(raw);
1974
- cachedMetadata = {
1975
- name: parsed.name ?? "@hasna/economy",
1976
- version: parsed.version ?? "0.0.0"
1977
- };
1978
- return cachedMetadata;
1979
- }
1980
- var packageMetadata = getPackageMetadata();
1981
-
1982
- // src/lib/peer-sync.ts
1983
- var GENERIC_PEER_TABLES = [
1984
- "usage_snapshots",
1985
- "subscriptions",
1986
- "billing_daily",
1987
- "savings_daily",
1988
- "budgets",
1989
- "goals",
1990
- "model_pricing",
1991
- "machines"
1992
- ];
1993
- function quoteIdent(identifier) {
1994
- return `"${identifier.replace(/"/g, '""')}"`;
1995
- }
1996
- function tableExists(db, table) {
1997
- const row = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`).get(table);
1998
- return Boolean(row);
1999
1735
  }
2000
- function tableColumns(db, table) {
2001
- if (!tableExists(db, table))
2002
- return [];
2003
- return db.prepare(`PRAGMA table_info(${quoteIdent(table)})`).all();
2004
- }
2005
- function commonColumns(source, target, table) {
2006
- const sourceCols = new Set(tableColumns(source, table).map((c) => c.name));
2007
- return tableColumns(target, table).map((c) => c.name).filter((c) => sourceCols.has(c));
2008
- }
2009
- function primaryKeyColumns(db, table) {
2010
- return tableColumns(db, table).filter((c) => c.pk > 0).sort((a, b) => a.pk - b.pk).map((c) => c.name);
2011
- }
2012
- function selectRows(source, table, columns) {
2013
- if (columns.length === 0)
2014
- return [];
2015
- const select = columns.map(quoteIdent).join(", ");
2016
- return source.prepare(`SELECT ${select} FROM ${quoteIdent(table)}`).all();
1736
+ function prorateMonthlyFee(monthlyFee, period) {
1737
+ const now = new Date;
1738
+ const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
1739
+ switch (period) {
1740
+ case "today":
1741
+ case "yesterday":
1742
+ return monthlyFee / daysInMonth;
1743
+ case "week":
1744
+ return monthlyFee / daysInMonth * 7;
1745
+ case "month":
1746
+ return monthlyFee;
1747
+ case "year":
1748
+ return monthlyFee * 12;
1749
+ case "all":
1750
+ return monthlyFee;
1751
+ }
2017
1752
  }
2018
- function rowByKey(target, table, keyColumns, row) {
2019
- if (keyColumns.length === 0)
2020
- return null;
2021
- if (keyColumns.some((c) => row[c] == null))
2022
- return null;
2023
- const where = keyColumns.map((c) => `${quoteIdent(c)} = ?`).join(" AND ");
2024
- return target.prepare(`SELECT * FROM ${quoteIdent(table)} WHERE ${where}`).get(...keyColumns.map((c) => row[c]));
1753
+ function proratedIncludedConsumed(includedUsage, includedCap, period) {
1754
+ const cap = prorateMonthlyFee(includedCap, period);
1755
+ return cap > 0 ? Math.min(includedUsage, cap) : includedUsage;
2025
1756
  }
2026
- function hasId(target, table, id) {
2027
- return target.prepare(`SELECT id, machine_id FROM ${quoteIdent(table)} WHERE id = ?`).get(id);
1757
+ function computeSavedUsd(apiEquivalent, onDemand, subscriptionFee) {
1758
+ return Math.max(0, apiEquivalent - onDemand - subscriptionFee);
2028
1759
  }
2029
- function shouldReplace(source, existing) {
2030
- if (!existing)
2031
- return true;
2032
- const sourceUpdated = source["updated_at"];
2033
- const existingUpdated = existing["updated_at"];
2034
- if (typeof sourceUpdated === "string" && typeof existingUpdated === "string" && existingUpdated !== "") {
2035
- return sourceUpdated >= existingUpdated;
2036
- }
2037
- return true;
2038
- }
2039
- function normalizeRow(row, columns, sourceMachine, now) {
2040
- const next = { ...row };
2041
- if (columns.includes("machine_id") && (!next["machine_id"] || next["machine_id"] === "")) {
2042
- next["machine_id"] = sourceMachine;
2043
- }
2044
- if (columns.includes("updated_at") && (!next["updated_at"] || next["updated_at"] === "")) {
2045
- next["updated_at"] = next["timestamp"] ?? next["started_at"] ?? next["created_at"] ?? now;
2046
- }
2047
- if (columns.includes("synced_at") && next["synced_at"] == null)
2048
- next["synced_at"] = "";
2049
- if (columns.includes("attribution_tag") && next["attribution_tag"] == null)
2050
- next["attribution_tag"] = "";
2051
- return next;
2052
- }
2053
- function insertOrReplace(target, table, columns, row) {
2054
- const colSql = columns.map(quoteIdent).join(", ");
2055
- const placeholders = columns.map(() => "?").join(", ");
2056
- target.prepare(`
2057
- INSERT OR REPLACE INTO ${quoteIdent(table)} (${colSql})
2058
- VALUES (${placeholders})
2059
- `).run(...columns.map((c) => row[c] ?? null));
2060
- }
2061
- function collisionId(target, table, machine, originalId) {
2062
- const base = `${machine || "peer"}:${originalId}`;
2063
- const baseRow = hasId(target, table, base);
2064
- if (!baseRow || String(baseRow["machine_id"] ?? "") === machine)
2065
- return base;
2066
- for (let i = 2;; i++) {
2067
- const candidate = `${base}:${i}`;
2068
- const row = hasId(target, table, candidate);
2069
- if (!row || String(row["machine_id"] ?? "") === machine)
2070
- return candidate;
2071
- }
2072
- }
2073
- function mergeIdentityTable(target, source, table, sourceMachine, now, sessionIdMap) {
2074
- const stats = { table, inserted: 0, updated: 0, skipped: 0, collisions: 0 };
2075
- const columns = commonColumns(source, target, table);
2076
- const rows = selectRows(source, table, columns);
2077
- const idMap = new Map;
2078
- for (const raw of rows) {
2079
- const row = normalizeRow(raw, columns, sourceMachine, now);
2080
- const originalId = String(row["id"] ?? "");
2081
- if (!originalId) {
2082
- stats.skipped++;
2083
- continue;
2084
- }
2085
- const machine = String(row["machine_id"] ?? "");
2086
- const directExisting = hasId(target, table, originalId);
2087
- if (directExisting && String(directExisting["machine_id"] ?? "") !== machine) {
2088
- row["id"] = collisionId(target, table, machine, originalId);
2089
- stats.collisions++;
2090
- }
2091
- if (table === "requests" && sessionIdMap) {
2092
- const originalSessionId = String(row["session_id"] ?? "");
2093
- row["session_id"] = sessionIdMap.get(originalSessionId) ?? originalSessionId;
2094
- }
2095
- const existing = hasId(target, table, String(row["id"]));
2096
- idMap.set(originalId, String(row["id"]));
2097
- if (existing && !shouldReplace(row, existing)) {
2098
- stats.skipped++;
2099
- continue;
2100
- }
2101
- insertOrReplace(target, table, columns, row);
2102
- if (existing)
2103
- stats.updated++;
2104
- else
2105
- stats.inserted++;
2106
- }
2107
- return { stats, idMap };
2108
- }
2109
- function mergeProjects(target, source) {
2110
- const table = "projects";
2111
- const stats = { table, inserted: 0, updated: 0, skipped: 0, collisions: 0 };
2112
- const columns = commonColumns(source, target, table);
2113
- const rows = selectRows(source, table, columns);
2114
- for (const raw of rows) {
2115
- const row = { ...raw };
2116
- const path = String(row["path"] ?? "");
2117
- const id = String(row["id"] ?? "");
2118
- if (!path || !id) {
2119
- stats.skipped++;
2120
- continue;
2121
- }
2122
- const existingByPath = target.prepare(`SELECT * FROM projects WHERE path = ?`).get(path);
2123
- if (existingByPath) {
2124
- row["id"] = existingByPath["id"] ?? id;
2125
- insertOrReplace(target, table, columns, row);
2126
- stats.updated++;
2127
- continue;
2128
- }
2129
- const existingById = target.prepare(`SELECT * FROM projects WHERE id = ?`).get(id);
2130
- if (existingById && String(existingById["path"] ?? "") !== path) {
2131
- row["id"] = `peer:${id}`;
2132
- stats.collisions++;
2133
- while (target.prepare(`SELECT id FROM projects WHERE id = ?`).get(row["id"])) {
2134
- row["id"] = `peer:${String(row["id"])}`;
2135
- }
1760
+ function querySavingsSummary(db, period, agent) {
1761
+ const where = periodWhere2(period, "timestamp");
1762
+ const agentClause = agent ? " AND agent = ?" : "";
1763
+ const params = agent ? [agent] : [];
1764
+ const apiRow = db.prepare(`
1765
+ SELECT COALESCE(SUM(cost_usd), 0) as total
1766
+ FROM requests
1767
+ WHERE ${where}${agentClause}
1768
+ AND COALESCE(cost_basis, 'estimated') IN ('metered_api', 'estimated', 'unknown')
1769
+ `).get(...params);
1770
+ const includedRow = db.prepare(`
1771
+ SELECT COALESCE(SUM(cost_usd), 0) as total
1772
+ FROM requests
1773
+ WHERE ${where}${agentClause}
1774
+ AND cost_basis = 'subscription_included'
1775
+ `).get(...params);
1776
+ const subWhere = periodWhere2(period, "date");
1777
+ const onDemandRow = db.prepare(`
1778
+ SELECT COALESCE(SUM(value), 0) as total
1779
+ FROM usage_snapshots
1780
+ WHERE ${subWhere}${agent ? " AND agent = ?" : ""}
1781
+ AND metric = 'on_demand_usd'
1782
+ `).get(...params);
1783
+ const subs = db.prepare(`
1784
+ SELECT
1785
+ COALESCE(SUM(monthly_fee_usd), 0) as fee,
1786
+ COALESCE(SUM(included_usage_usd), 0) as included
1787
+ FROM subscriptions
1788
+ WHERE active = 1${agent ? " AND (agent = ? OR agent IS NULL)" : ""}
1789
+ `).get(...agent ? [agent] : []);
1790
+ const subscriptionFee = prorateMonthlyFee(subs.fee, period);
1791
+ const apiEquivalent = apiRow.total + includedRow.total;
1792
+ const includedConsumed = proratedIncludedConsumed(includedRow.total, subs.included, period);
1793
+ const onDemand = onDemandRow.total;
1794
+ const saved = computeSavedUsd(apiEquivalent, onDemand, subscriptionFee);
1795
+ const byAgent = {};
1796
+ if (!agent) {
1797
+ const onDemandByAgent = new Map;
1798
+ for (const row of db.prepare(`
1799
+ SELECT agent, COALESCE(SUM(value), 0) as total
1800
+ FROM usage_snapshots
1801
+ WHERE ${subWhere}
1802
+ AND metric = 'on_demand_usd'
1803
+ GROUP BY agent
1804
+ `).all()) {
1805
+ onDemandByAgent.set(row.agent, row.total);
2136
1806
  }
2137
- insertOrReplace(target, table, columns, row);
2138
- stats.inserted++;
2139
- }
2140
- return stats;
2141
- }
2142
- function mergeGenericTable(target, source, table, sourceMachine, now) {
2143
- const stats = { table, inserted: 0, updated: 0, skipped: 0, collisions: 0 };
2144
- const columns = commonColumns(source, target, table);
2145
- const keyColumns = primaryKeyColumns(target, table).filter((c) => columns.includes(c));
2146
- const rows = selectRows(source, table, columns);
2147
- for (const raw of rows) {
2148
- const row = normalizeRow(raw, columns, sourceMachine, now);
2149
- const existing = rowByKey(target, table, keyColumns, row);
2150
- if (existing && !shouldReplace(row, existing)) {
2151
- stats.skipped++;
2152
- continue;
1807
+ const subscriptionByAgent = new Map;
1808
+ for (const row of db.prepare(`
1809
+ SELECT agent,
1810
+ COALESCE(SUM(monthly_fee_usd), 0) as fee,
1811
+ COALESCE(SUM(included_usage_usd), 0) as included
1812
+ FROM subscriptions
1813
+ WHERE active = 1 AND agent IS NOT NULL
1814
+ GROUP BY agent
1815
+ `).all()) {
1816
+ subscriptionByAgent.set(row.agent, row);
2153
1817
  }
2154
- insertOrReplace(target, table, columns, row);
2155
- if (existing)
2156
- stats.updated++;
2157
- else
2158
- stats.inserted++;
2159
- }
2160
- return stats;
2161
- }
2162
- function detectSourceMachine(source, fallback) {
2163
- if (fallback && fallback.trim())
2164
- return fallback.trim();
2165
- const counts = new Map;
2166
- for (const table of ["sessions", "requests", "usage_snapshots"]) {
2167
- if (!tableExists(source, table))
2168
- continue;
2169
- const rows = source.prepare(`
2170
- SELECT machine_id, COUNT(*) as cnt
2171
- FROM ${quoteIdent(table)}
2172
- WHERE machine_id != '' AND machine_id IS NOT NULL
2173
- GROUP BY machine_id
1818
+ const globalSubs = db.prepare(`
1819
+ SELECT
1820
+ COALESCE(SUM(monthly_fee_usd), 0) as fee,
1821
+ COALESCE(SUM(included_usage_usd), 0) as included
1822
+ FROM subscriptions
1823
+ WHERE active = 1 AND agent IS NULL
1824
+ `).get();
1825
+ const rows = db.prepare(`
1826
+ SELECT agent,
1827
+ COALESCE(SUM(cost_usd), 0) as api_eq,
1828
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as included
1829
+ FROM requests WHERE ${where}
1830
+ GROUP BY agent
2174
1831
  `).all();
2175
- for (const row of rows) {
2176
- counts.set(row.machine_id, (counts.get(row.machine_id) ?? 0) + row.cnt);
2177
- }
2178
- }
2179
- let best = "";
2180
- let bestCount = -1;
2181
- for (const [machine, count] of counts.entries()) {
2182
- if (count > bestCount) {
2183
- best = machine;
2184
- bestCount = count;
2185
- }
2186
- }
2187
- return best || "peer";
2188
- }
2189
- function ensureMachineRegistry(target, machine, now) {
2190
- if (!machine)
2191
- return;
2192
- target.prepare(`
2193
- INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
2194
- VALUES (?, ?, ?, NULL, ?, ?, ?)
2195
- ON CONFLICT(machine_id) DO UPDATE SET
2196
- hostname = COALESCE(NULLIF(machines.hostname, ''), excluded.hostname),
2197
- last_seen_at = CASE
2198
- WHEN machines.last_seen_at IS NULL OR machines.last_seen_at < excluded.last_seen_at THEN excluded.last_seen_at
2199
- ELSE machines.last_seen_at
2200
- END,
2201
- last_pull_at = excluded.last_pull_at,
2202
- economy_version = excluded.economy_version,
2203
- updated_at = excluded.updated_at
2204
- `).run(machine, machine, now, now, packageMetadata.version, now);
2205
- }
2206
- function openSourceDatabase(path) {
2207
- try {
2208
- return new BunDatabase(path, { readonly: true });
2209
- } catch {
2210
- return new BunDatabase(path);
2211
- }
2212
- }
2213
- function mergePeerDatabase(target, sourcePath, opts = {}) {
2214
- if (!existsSync3(sourcePath))
2215
- throw new Error(`source database does not exist: ${sourcePath}`);
2216
- const source = openSourceDatabase(sourcePath);
2217
- const now = opts.now ?? new Date().toISOString();
2218
- const sourceMachine = detectSourceMachine(source, opts.sourceMachine);
2219
- const tables = [];
2220
- try {
2221
- target.exec("PRAGMA foreign_keys = OFF");
2222
- target.exec("BEGIN IMMEDIATE");
2223
- try {
2224
- tables.push(mergeProjects(target, source));
2225
- const sessionMerge = mergeIdentityTable(target, source, "sessions", sourceMachine, now);
2226
- tables.push(sessionMerge.stats);
2227
- tables.push(mergeIdentityTable(target, source, "requests", sourceMachine, now, sessionMerge.idMap).stats);
2228
- for (const table of GENERIC_PEER_TABLES) {
2229
- tables.push(mergeGenericTable(target, source, table, sourceMachine, now));
2230
- }
2231
- ensureMachineRegistry(target, sourceMachine, now);
2232
- target.exec("COMMIT");
2233
- } catch (err) {
2234
- target.exec("ROLLBACK");
2235
- throw err;
2236
- } finally {
2237
- target.exec("PRAGMA foreign_keys = ON");
1832
+ const totalAgentApiEq = rows.reduce((sum, row) => sum + row.api_eq, 0);
1833
+ for (const row of db.prepare(`
1834
+ SELECT agent,
1835
+ COALESCE(SUM(cost_usd), 0) as api_eq,
1836
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as included
1837
+ FROM requests WHERE ${where}
1838
+ GROUP BY agent
1839
+ `).all()) {
1840
+ const agentSubs = subscriptionByAgent.get(row.agent) ?? { fee: 0, included: 0 };
1841
+ const globalShare = totalAgentApiEq > 0 ? row.api_eq / totalAgentApiEq : 0;
1842
+ const agentFee = prorateMonthlyFee(agentSubs.fee + globalSubs.fee * globalShare, period);
1843
+ const agentIncludedCap = agentSubs.included + globalSubs.included * globalShare;
1844
+ const agentIncludedConsumed = proratedIncludedConsumed(row.included, agentIncludedCap, period);
1845
+ const agentOnDemand = onDemandByAgent.get(row.agent) ?? 0;
1846
+ byAgent[row.agent] = {
1847
+ api_equivalent_usd: row.api_eq,
1848
+ subscription_fee_usd: agentFee,
1849
+ included_consumed_usd: agentIncludedConsumed,
1850
+ on_demand_usd: agentOnDemand,
1851
+ saved_usd: computeSavedUsd(row.api_eq, agentOnDemand, agentFee)
1852
+ };
2238
1853
  }
2239
- } finally {
2240
- source.close();
2241
1854
  }
2242
- const deduped = dedupeRequests(target);
2243
- const rowsWritten = tables.reduce((sum, table) => sum + table.inserted + table.updated, 0);
2244
- const collisions = tables.reduce((sum, table) => sum + table.collisions, 0);
2245
1855
  return {
2246
- source_path: sourcePath,
2247
- source_machine: sourceMachine,
2248
- rows_written: rowsWritten,
2249
- collisions,
2250
- deduped,
2251
- tables: tables.filter((t) => t.inserted || t.updated || t.skipped || t.collisions)
1856
+ period,
1857
+ api_equivalent_usd: apiEquivalent,
1858
+ subscription_fee_usd: subscriptionFee,
1859
+ included_consumed_usd: includedConsumed,
1860
+ on_demand_usd: onDemand,
1861
+ saved_usd: saved,
1862
+ by_agent: byAgent
2252
1863
  };
2253
1864
  }
2254
- // src/lib/fleet-sync.ts
1865
+
1866
+ // src/lib/billing-diff.ts
2255
1867
  init_database();
2256
- import { spawnSync } from "child_process";
2257
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, renameSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
2258
- import { join as join3, resolve } from "path";
2259
- import { homedir as homedir2 } from "os";
2260
- import { Database as Database2 } from "bun:sqlite";
2261
- var DEFAULT_TIMEOUT_MS = 120000;
2262
- var DEFAULT_MACHINES = ["spark02", "apple03"];
2263
- function runEconomyFleetSync(options = {}) {
2264
- const generatedAt = (options.now ?? new Date).toISOString();
2265
- const timeoutMs = normalizePositiveInteger(options.timeoutMs, DEFAULT_TIMEOUT_MS);
2266
- const runner = options.runner ?? spawnCommand;
2267
- const dryRun = Boolean(options.dryRun);
2268
- const machines = ((options.machines?.length) ? options.machines : DEFAULT_MACHINES).filter(Boolean);
2269
- const cacheDir = resolve(options.cacheDir ?? join3(homedir2(), ".hasna", "economy", "fleet-sync-cache"));
2270
- const remoteSnapshotDir = options.remoteSnapshotDir ?? "$HOME/.hasna/economy/fleet-sync-snapshots";
2271
- const localSync = options.localSync !== false;
2272
- const remoteSync = options.remoteSync !== false;
2273
- const local = {
2274
- status: dryRun ? "dry_run" : localSync ? "skipped" : "skipped",
2275
- db_path: getDbPath()
2276
- };
2277
- if (localSync) {
2278
- if (dryRun) {
2279
- local.status = "dry_run";
2280
- } else {
2281
- local.sync = runner("economy", ["sync"], { timeoutMs });
2282
- local.status = local.sync.code === 0 ? "synced" : "failed";
2283
- }
2284
- }
2285
- const remote = machines.map((machine) => syncMachine(machine, {
2286
- generatedAt,
2287
- timeoutMs,
2288
- runner,
2289
- dryRun,
2290
- remoteSync,
2291
- cacheDir,
2292
- remoteSnapshotDir
2293
- }));
1868
+ var PROVIDER_TO_AGENT = {
1869
+ anthropic: "claude",
1870
+ openai: "codex",
1871
+ gemini: "gemini",
1872
+ google: "gemini"
1873
+ };
1874
+ function queryBillingDiff(db, period, thresholdPct = 15) {
1875
+ const estimated = querySummary(db, period, undefined, true);
1876
+ const actual = queryBillingSummary(db, period);
1877
+ const delta = estimated.total_usd - actual.total_usd;
1878
+ const deltaPct = actual.total_usd > 0 ? delta / actual.total_usd * 100 : 0;
1879
+ const agentRows = db.prepare(`
1880
+ SELECT agent, COALESCE(SUM(cost_usd), 0) as estimated_usd
1881
+ FROM requests
1882
+ WHERE ${periodWhere3(period, "timestamp")}
1883
+ GROUP BY agent
1884
+ `).all();
1885
+ const by_agent = agentRows.map((row) => {
1886
+ const provider = Object.entries(PROVIDER_TO_AGENT).find(([, a]) => a === row.agent)?.[0];
1887
+ const actualUsd = provider ? actual.by_provider[provider] ?? 0 : 0;
1888
+ const rowDelta = row.estimated_usd - actualUsd;
1889
+ const rowPct = actualUsd > 0 ? rowDelta / actualUsd * 100 : 0;
1890
+ return {
1891
+ agent: row.agent,
1892
+ estimated_usd: row.estimated_usd,
1893
+ actual_usd: actualUsd,
1894
+ delta_usd: rowDelta,
1895
+ delta_pct: rowPct
1896
+ };
1897
+ }).sort((a, b) => Math.abs(b.delta_usd) - Math.abs(a.delta_usd));
2294
1898
  return {
2295
- schema: "open-economy.fleet-sync.v1",
2296
- generated_at: generatedAt,
2297
- dry_run: dryRun,
2298
- local,
2299
- remote,
2300
- summary: {
2301
- machines: remote.length,
2302
- merged: remote.filter((row) => row.status === "merged").length,
2303
- failed: remote.filter((row) => row.status === "failed").length,
2304
- dry_run: remote.filter((row) => row.status === "dry_run").length + (dryRun ? 1 : 0)
2305
- }
1899
+ period,
1900
+ estimated_usd: estimated.total_usd,
1901
+ actual_usd: actual.total_usd,
1902
+ delta_usd: delta,
1903
+ delta_pct: deltaPct,
1904
+ threshold_pct: thresholdPct,
1905
+ is_alert: Math.abs(deltaPct) > thresholdPct,
1906
+ by_agent,
1907
+ by_provider: actual.by_provider
2306
1908
  };
2307
1909
  }
2308
- function syncMachine(machine, context) {
2309
- const safeMachine = machine.replace(/[^A-Za-z0-9_.-]/g, "_");
2310
- const stamp = context.generatedAt.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
2311
- const snapshotName = `${safeMachine}-${stamp}.economy.db`;
2312
- const remoteSnapshotPath = `${context.remoteSnapshotDir.replace(/\/$/, "")}/${snapshotName}`;
2313
- const tmpLocal = join3(context.cacheDir, `${snapshotName}.tmp`);
2314
- const verifiedLocal = join3(context.cacheDir, snapshotName);
2315
- if (context.dryRun) {
2316
- return { machine, status: "dry_run", remote_snapshot_path: remoteSnapshotPath };
2317
- }
2318
- mkdirSync3(context.cacheDir, { recursive: true });
2319
- rmSync(tmpLocal, { force: true });
2320
- const result = {
2321
- machine,
2322
- status: "failed",
2323
- remote_snapshot_path: remoteSnapshotPath
2324
- };
2325
- if (context.remoteSync) {
2326
- result.remote_sync = context.runner("ssh", [machine, "economy sync"], { timeoutMs: context.timeoutMs });
2327
- if (result.remote_sync.code !== 0) {
2328
- result.error = "remote economy sync failed";
2329
- return result;
2330
- }
1910
+ function periodWhere3(period, column) {
1911
+ switch (period) {
1912
+ case "today":
1913
+ return `DATE(${column}) = DATE('now')`;
1914
+ case "yesterday":
1915
+ return `DATE(${column}) = DATE('now', '-1 day')`;
1916
+ case "week":
1917
+ return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
1918
+ case "month":
1919
+ return `${column} >= DATE('now', 'start of month')`;
1920
+ case "year":
1921
+ return `${column} >= DATE('now', 'start of year')`;
1922
+ case "all":
1923
+ return "1=1";
2331
1924
  }
2332
- result.remote_snapshot = context.runner("ssh", [machine, remoteSnapshotCommand(context.remoteSnapshotDir, snapshotName)], { timeoutMs: context.timeoutMs });
2333
- if (result.remote_snapshot.code !== 0) {
2334
- result.error = "remote snapshot failed";
2335
- return result;
2336
- }
2337
- const emittedPath = result.remote_snapshot.stdout.trim().split(/\r?\n/).at(-1)?.trim() || remoteSnapshotPath;
2338
- if (!isSafeRemoteSnapshotPath(emittedPath, context.remoteSnapshotDir)) {
2339
- result.error = `remote snapshot path refused: ${emittedPath}`;
2340
- return result;
2341
- }
2342
- result.remote_snapshot_path = emittedPath;
2343
- result.pull = context.runner("scp", [`${machine}:${emittedPath}`, tmpLocal], { timeoutMs: context.timeoutMs });
2344
- context.runner("ssh", [machine, `rm -f ${shellQuote(emittedPath)}`], { timeoutMs: context.timeoutMs });
2345
- if (result.pull.code !== 0) {
2346
- result.error = "snapshot pull failed; no cached DB merged";
2347
- rmSync(tmpLocal, { force: true });
2348
- return result;
2349
- }
2350
- const integrity = verifySqliteIntegrity(tmpLocal);
2351
- result.local_integrity = integrity;
2352
- if (!integrity.ok) {
2353
- result.error = "pulled snapshot failed local integrity check";
2354
- rmSync(tmpLocal, { force: true });
2355
- return result;
2356
- }
2357
- renameSync(tmpLocal, verifiedLocal);
2358
- const db = openDatabase();
2359
- result.merge = mergePeerDatabase(db, verifiedLocal, { sourceMachine: machine });
2360
- result.status = "merged";
2361
- return result;
2362
1925
  }
2363
- function remoteSnapshotCommand(remoteDir, filename) {
2364
- const dirExpr = remoteDir.startsWith("$HOME/") ? `"${remoteDir.replace(/"/g, "\\\"")}"` : shellQuote(remoteDir);
2365
- const safeFilename = filename.replace(/[^A-Za-z0-9_.-]/g, "_");
2366
- return [
2367
- "set -euo pipefail",
2368
- 'db="${HASNA_ECONOMY_DB_PATH:-${ECONOMY_DB:-$HOME/.hasna/economy/economy.db}}"',
2369
- `snapshot_dir=${dirExpr}`,
2370
- `out="$snapshot_dir/${safeFilename}"`,
2371
- 'test -f "$db"',
2372
- 'mkdir -p "$snapshot_dir"',
2373
- 'rm -f "$out"',
2374
- `sqlite3 "$db" "VACUUM INTO '$out'"`,
2375
- 'test "$(sqlite3 "$out" "PRAGMA integrity_check;")" = "ok"',
2376
- 'printf "%s\\n" "$out"'
2377
- ].join("; ");
2378
- }
2379
- function isSafeRemoteSnapshotPath(path, remoteSnapshotDir = "$HOME/.hasna/economy/fleet-sync-snapshots") {
2380
- if (!path || path.includes("\x00") || path.includes(`
2381
- `) || path.includes("\r"))
2382
- return false;
2383
- if (!path.endsWith(".db"))
2384
- return false;
2385
- if (path.includes(".."))
2386
- return false;
2387
- const prefix = remoteSnapshotDir.replace(/\/$/, "");
2388
- return path.startsWith(`${prefix}/`) || path.startsWith("$HOME/.hasna/economy/fleet-sync-snapshots/") || path.includes("/.hasna/economy/fleet-sync-snapshots/");
2389
- }
2390
- function verifySqliteIntegrity(path) {
2391
- if (!existsSync4(path))
2392
- return { ok: false, detail: "file does not exist" };
2393
- let db;
2394
- try {
2395
- db = new Database2(path, { readonly: true });
2396
- const row = db.query("PRAGMA integrity_check").get();
2397
- const detail = Object.values(row ?? {})[0] ?? "";
2398
- return { ok: detail.toLowerCase() === "ok", detail: detail || "no integrity result" };
2399
- } catch (error) {
2400
- return { ok: false, detail: error instanceof Error ? error.message : String(error) };
2401
- } finally {
2402
- db?.close();
2403
- }
2404
- }
2405
- function writeFleetSyncReport(reportDir, result) {
2406
- mkdirSync3(reportDir, { recursive: true });
2407
- const path = join3(resolve(reportDir), `${result.generated_at.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z")}.json`);
2408
- writeFileSync2(path, `${JSON.stringify(result, null, 2)}
2409
- `);
2410
- return path;
2411
- }
2412
- function spawnCommand(command, args, options) {
2413
- const result = spawnSync(command, args, {
2414
- encoding: "utf8",
2415
- timeout: options.timeoutMs,
2416
- maxBuffer: 1024 * 1024,
2417
- stdio: ["ignore", "pipe", "pipe"]
2418
- });
2419
- return {
2420
- code: result.status,
2421
- stdout: result.stdout ?? "",
2422
- stderr: result.stderr ?? "",
2423
- ...result.error ? { error: result.error.message } : {}
2424
- };
2425
- }
2426
- function shellQuote(value) {
2427
- return `'${value.replace(/'/g, "'\\''")}'`;
2428
- }
2429
- function normalizePositiveInteger(value, fallback) {
2430
- if (value == null || !Number.isFinite(value) || value < 1)
2431
- return fallback;
2432
- return Math.floor(value);
2433
- }
2434
- // src/ingest/claude.ts
2435
- init_database();
2436
- init_pricing();
2437
- import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync5, statSync as statSync2 } from "fs";
2438
- import { homedir as homedir3 } from "os";
2439
- import { join as join4, basename } from "path";
2440
1926
 
2441
- // src/lib/savings.ts
2442
- function defaultCostBasisForAgent(agent) {
2443
- if (agent === "claude")
2444
- return "metered_api";
2445
- if (agent === "cursor")
2446
- return "subscription_included";
2447
- return "estimated";
1927
+ // src/lib/periods.ts
1928
+ function ymd(date) {
1929
+ return date.toISOString().substring(0, 10);
1930
+ }
1931
+ function usageSnapshotFilterForPeriod(period) {
1932
+ const now = new Date;
1933
+ switch (period) {
1934
+ case "today":
1935
+ return { date: ymd(now) };
1936
+ case "yesterday": {
1937
+ const yesterday = new Date(now);
1938
+ yesterday.setUTCDate(yesterday.getUTCDate() - 1);
1939
+ return { date: ymd(yesterday) };
1940
+ }
1941
+ case "week": {
1942
+ const weekAgo = new Date(now);
1943
+ weekAgo.setUTCDate(weekAgo.getUTCDate() - 7);
1944
+ return { since: ymd(weekAgo) };
1945
+ }
1946
+ case "month":
1947
+ return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1))) };
1948
+ case "year":
1949
+ return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), 0, 1))) };
1950
+ case "all":
1951
+ return {};
1952
+ }
2448
1953
  }
2449
1954
 
2450
- // src/lib/accounts.ts
2451
- var AGENT_ACCOUNT_TOOLS = {
2452
- claude: ["claude"],
2453
- takumi: ["takumi", "claude"],
2454
- codex: ["codex"],
2455
- gemini: ["gemini"],
2456
- opencode: ["opencode"],
2457
- cursor: ["cursor"],
2458
- pi: ["pi"],
2459
- hermes: ["hermes"]
1955
+ // src/lib/brief.ts
1956
+ init_database();
1957
+ var ZERO_REQUEST_TOTALS = {
1958
+ sessions: 0,
1959
+ request_sessions: 0,
1960
+ requests: 0,
1961
+ input_tokens: 0,
1962
+ output_tokens: 0,
1963
+ cache_read_tokens: 0,
1964
+ cache_create_tokens: 0,
1965
+ cache_create_5m_tokens: 0,
1966
+ cache_create_1h_tokens: 0,
1967
+ total_tokens: 0,
1968
+ cost_usd: 0,
1969
+ last_active: null
2460
1970
  };
2461
- function normalizeEmail(email) {
2462
- return (email ?? "").trim().toLowerCase();
1971
+ var ZERO_SESSION_TOTALS = {
1972
+ sessions: 0,
1973
+ requests: 0,
1974
+ total_tokens: 0,
1975
+ cost_usd: 0,
1976
+ last_active: null
1977
+ };
1978
+ function formatAge(timestamp, now) {
1979
+ if (!timestamp)
1980
+ return "never";
1981
+ const t = new Date(timestamp).getTime();
1982
+ if (!Number.isFinite(t))
1983
+ return "unknown";
1984
+ const ageMs = Math.max(0, now.getTime() - t);
1985
+ const minutes = Math.floor(ageMs / 60000);
1986
+ if (minutes < 1)
1987
+ return "<1m";
1988
+ if (minutes < 60)
1989
+ return `${minutes}m`;
1990
+ const hours = Math.floor(minutes / 60);
1991
+ if (hours < 48)
1992
+ return `${hours}h`;
1993
+ return `${Math.floor(hours / 24)}d`;
1994
+ }
1995
+ function machineFilter(machine) {
1996
+ if (!machine || machine === "all")
1997
+ return;
1998
+ return machine;
2463
1999
  }
2464
- function accountKey(tool, name, email) {
2465
- const normalizedEmail = normalizeEmail(email);
2466
- return `${tool}:${normalizedEmail || name}`;
2000
+ function requestPeriodWhere2(period) {
2001
+ if (period === "today")
2002
+ return `DATE(timestamp) = DATE('now')`;
2003
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
2467
2004
  }
2468
- function normalizeDir(value) {
2469
- return value.replace(/\/+$/, "");
2005
+ function sessionPeriodWhere2(period) {
2006
+ if (period === "today")
2007
+ return `DATE(started_at) = DATE('now')`;
2008
+ return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
2470
2009
  }
2471
- function fromProfile(profile, source) {
2010
+ function baseRequestTotals(row) {
2472
2011
  return {
2473
- account_key: accountKey(profile.tool, profile.name, profile.email),
2474
- account_tool: profile.tool,
2475
- account_name: profile.name,
2476
- ...profile.email ? { account_email: normalizeEmail(profile.email) } : {},
2477
- account_source: source
2012
+ ...ZERO_REQUEST_TOTALS,
2013
+ ...row,
2014
+ sessions: Number(row?.request_sessions ?? row?.sessions ?? 0),
2015
+ request_sessions: Number(row?.request_sessions ?? row?.sessions ?? 0),
2016
+ requests: Number(row?.requests ?? 0),
2017
+ input_tokens: Number(row?.input_tokens ?? 0),
2018
+ output_tokens: Number(row?.output_tokens ?? 0),
2019
+ cache_read_tokens: Number(row?.cache_read_tokens ?? 0),
2020
+ cache_create_tokens: Number(row?.cache_create_tokens ?? 0),
2021
+ cache_create_5m_tokens: Number(row?.cache_create_5m_tokens ?? 0),
2022
+ cache_create_1h_tokens: Number(row?.cache_create_1h_tokens ?? 0),
2023
+ total_tokens: Number(row?.total_tokens ?? 0),
2024
+ cost_usd: Number(row?.cost_usd ?? 0),
2025
+ last_active: row?.last_active ?? null
2478
2026
  };
2479
2027
  }
2480
- function fromOverride(raw, agent) {
2481
- const value = raw.trim();
2482
- if (!value)
2483
- return null;
2484
- const candidateTool = AGENT_ACCOUNT_TOOLS[agent][0] ?? agent;
2485
- const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
2486
- if (!tool || !name)
2487
- return null;
2488
- const email = name.includes("@") ? normalizeEmail(name) : undefined;
2028
+ function baseSessionTotals(row) {
2489
2029
  return {
2490
- account_key: accountKey(tool, name, email),
2491
- account_tool: tool,
2492
- account_name: name,
2493
- ...email ? { account_email: email } : {},
2494
- account_source: "override"
2030
+ sessions: Number(row?.sessions ?? 0),
2031
+ requests: Number(row?.requests ?? 0),
2032
+ total_tokens: Number(row?.total_tokens ?? 0),
2033
+ cost_usd: Number(row?.cost_usd ?? 0),
2034
+ last_active: row?.last_active ?? null
2495
2035
  };
2496
2036
  }
2497
- function envOverride(agent, env) {
2498
- const agentPrefix = agent.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2499
- const raw = env[`ECONOMY_${agentPrefix}_ACCOUNT_KEY`] ?? env[`ECONOMY_${agentPrefix}_ACCOUNT`] ?? env["ECONOMY_ACCOUNT_KEY"] ?? env["ECONOMY_ACCOUNT"];
2500
- if (raw)
2501
- return fromOverride(raw, agent);
2502
- const tool = env[`ECONOMY_${agentPrefix}_ACCOUNT_TOOL`] ?? env["ECONOMY_ACCOUNT_TOOL"];
2503
- const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
2504
- if (!tool || !name)
2505
- return null;
2506
- const email = normalizeEmail(env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"]);
2037
+ function latestTimestamp(...values) {
2038
+ return values.filter(Boolean).sort().at(-1) ?? null;
2039
+ }
2040
+ function combineTotals(requests, sessions) {
2507
2041
  return {
2508
- account_key: accountKey(tool, name, email),
2509
- account_tool: tool,
2510
- account_name: name,
2511
- ...email ? { account_email: email } : {},
2512
- account_source: "override"
2042
+ sessions: requests.request_sessions + sessions.sessions,
2043
+ requests: requests.requests + sessions.requests,
2044
+ input_tokens: requests.input_tokens,
2045
+ output_tokens: requests.output_tokens,
2046
+ cache_read_tokens: requests.cache_read_tokens,
2047
+ cache_create_tokens: requests.cache_create_tokens,
2048
+ cache_create_5m_tokens: requests.cache_create_5m_tokens,
2049
+ cache_create_1h_tokens: requests.cache_create_1h_tokens,
2050
+ total_tokens: requests.total_tokens + sessions.total_tokens,
2051
+ cost_usd: requests.cost_usd + sessions.cost_usd,
2052
+ last_active: latestTimestamp(requests.last_active, sessions.last_active)
2513
2053
  };
2514
2054
  }
2515
- function knownToolIds(api) {
2516
- try {
2517
- return new Set(api.listTools().map((tool) => tool.id));
2518
- } catch {
2519
- return new Set;
2520
- }
2055
+ function emptyTotals() {
2056
+ return combineTotals(ZERO_REQUEST_TOTALS, ZERO_SESSION_TOTALS);
2521
2057
  }
2522
- function profileForEnvDir(api, tool, env) {
2523
- const configuredDir = env[tool.envVar];
2524
- if (!configuredDir)
2525
- return null;
2526
- const normalized = normalizeDir(configuredDir);
2527
- try {
2528
- return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
2529
- } catch {
2530
- return null;
2058
+ function requestTotalsSql(where, machine) {
2059
+ const params = [];
2060
+ let machineClause = "";
2061
+ if (machine) {
2062
+ machineClause = " AND machine_id = ?";
2063
+ params.push(machine);
2531
2064
  }
2065
+ return {
2066
+ sql: `
2067
+ SELECT
2068
+ COUNT(DISTINCT session_id) as request_sessions,
2069
+ COUNT(*) as requests,
2070
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
2071
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
2072
+ COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
2073
+ COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
2074
+ COALESCE(SUM(COALESCE(cache_create_5m_tokens, cache_create_tokens)), 0) as cache_create_5m_tokens,
2075
+ COALESCE(SUM(COALESCE(cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
2076
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
2077
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
2078
+ MAX(timestamp) as last_active
2079
+ FROM requests
2080
+ WHERE ${where}${machineClause}
2081
+ `,
2082
+ params
2083
+ };
2532
2084
  }
2533
- async function resolveAccountForAgent(agent, env = process.env) {
2534
- const override = envOverride(agent, env);
2535
- if (override)
2536
- return override;
2537
- let api;
2538
- try {
2539
- api = await import("@hasna/accounts");
2540
- } catch {
2541
- return null;
2542
- }
2543
- const toolIds = knownToolIds(api);
2544
- for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
2545
- if (!toolIds.has(toolId))
2546
- continue;
2547
- let tool;
2548
- try {
2549
- tool = api.getTool(toolId);
2550
- } catch {
2551
- continue;
2552
- }
2553
- const envProfile = profileForEnvDir(api, tool, env);
2554
- if (envProfile)
2555
- return fromProfile(envProfile, "env");
2556
- try {
2557
- const applied = api.appliedProfile(toolId);
2558
- if (applied)
2559
- return fromProfile(applied, "applied");
2560
- } catch {}
2561
- try {
2562
- const current = api.currentProfile(toolId);
2563
- if (current)
2564
- return fromProfile(current, "current");
2565
- } catch {}
2085
+ function sessionOnlyTotalsSql(where, machine) {
2086
+ const params = [];
2087
+ let machineClause = "";
2088
+ if (machine) {
2089
+ machineClause = " AND machine_id = ?";
2090
+ params.push(machine);
2566
2091
  }
2567
- return null;
2092
+ return {
2093
+ sql: `
2094
+ SELECT
2095
+ COUNT(*) as sessions,
2096
+ COALESCE(SUM(request_count), 0) as requests,
2097
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
2098
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
2099
+ MAX(started_at) as last_active
2100
+ FROM sessions
2101
+ WHERE ${where}${machineClause}
2102
+ AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
2103
+ `,
2104
+ params
2105
+ };
2106
+ }
2107
+ function queryRequestTotals(db, where, params, machine) {
2108
+ const q = requestTotalsSql(where, machine);
2109
+ return baseRequestTotals(db.prepare(q.sql).get(...params, ...q.params));
2110
+ }
2111
+ function querySessionOnlyTotals(db, where, params, machine) {
2112
+ const q = sessionOnlyTotalsSql(where, machine);
2113
+ return baseSessionTotals(db.prepare(q.sql).get(...params, ...q.params));
2568
2114
  }
2569
- function withAccount(record, account) {
2570
- if (!account)
2571
- return record;
2115
+ function queryPeriodTotals(db, period, machine) {
2116
+ const summary = querySummary(db, period, machine);
2117
+ const requests = queryRequestTotals(db, requestPeriodWhere2(period), [], machine);
2118
+ const sessions = querySessionOnlyTotals(db, sessionPeriodWhere2(period), [], machine);
2119
+ const totals = combineTotals(requests, sessions);
2572
2120
  return {
2573
- ...record,
2574
- account_key: account.account_key,
2575
- account_tool: account.account_tool,
2576
- account_name: account.account_name,
2577
- account_email: account.account_email ?? "",
2578
- account_source: account.account_source
2121
+ ...totals,
2122
+ sessions: summary.sessions,
2123
+ requests: summary.requests,
2124
+ total_tokens: summary.tokens,
2125
+ cost_usd: summary.total_usd
2579
2126
  };
2580
2127
  }
2581
-
2582
- // src/ingest/claude.ts
2583
- function autoDetectProject(cwd, projects) {
2584
- return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
2585
- }
2586
- var CLAUDE_PROJECTS_DIR = join4(homedir3(), ".claude", "projects");
2587
- var TAKUMI_PROJECTS_DIR = join4(homedir3(), ".takumi", "projects");
2588
- function dirNameToPath(dirName) {
2589
- return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
2590
- }
2591
- function collectJsonlFiles(projectDir) {
2592
- const files = [];
2593
- function walk(dir) {
2594
- try {
2595
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
2596
- if (entry.isDirectory())
2597
- walk(join4(dir, entry.name));
2598
- else if (entry.name.endsWith(".jsonl"))
2599
- files.push(join4(dir, entry.name));
2600
- }
2601
- } catch {}
2602
- }
2603
- walk(projectDir);
2604
- return files;
2605
- }
2606
- async function ingestClaude(db, verbose = false, projectsDir = CLAUDE_PROJECTS_DIR) {
2607
- return ingestJsonlProjects(db, projectsDir, "claude", verbose);
2608
- }
2609
- async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
2610
- return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
2611
- }
2612
- async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
2613
- if (!existsSync5(projectsDir)) {
2614
- if (verbose)
2615
- console.log(`${agentName} projects dir not found:`, projectsDir);
2616
- return { files: 0, requests: 0, sessions: 0 };
2617
- }
2618
- const machineId = getMachineId();
2619
- let totalFiles = 0;
2620
- let totalRequests = 0;
2621
- const touchedSessions = new Set;
2622
- const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
2623
- const account = await resolveAccountForAgent(agentName);
2624
- const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
2625
- for (const projectDirEntry of projectDirs) {
2626
- const projectDirPath = join4(projectsDir, projectDirEntry.name);
2627
- const projectPath = dirNameToPath(projectDirEntry.name);
2628
- const jsonlFiles = collectJsonlFiles(projectDirPath);
2629
- for (const filePath of jsonlFiles) {
2630
- const stateKey = filePath.replace(projectsDir, "");
2631
- let fileMtime = "0";
2632
- try {
2633
- fileMtime = statSync2(filePath).mtimeMs.toString();
2634
- } catch {
2635
- continue;
2636
- }
2637
- const processed = getIngestState(db, agentName, stateKey);
2638
- if (processed === fileMtime)
2639
- continue;
2640
- let lines;
2641
- try {
2642
- lines = readFileSync3(filePath, "utf-8").split(`
2643
- `).filter((l) => l.trim());
2644
- } catch {
2645
- continue;
2646
- }
2647
- const fileBasename = basename(filePath, ".jsonl");
2648
- const isUuid = /^[0-9a-f-]{36}$/.test(fileBasename);
2649
- let sessionId = isUuid ? fileBasename : fileBasename.replace(/^agent-/, "");
2650
- let sessionCwd = projectPath;
2651
- for (const line of lines) {
2652
- let entry;
2653
- try {
2654
- entry = JSON.parse(line);
2655
- } catch {
2656
- continue;
2657
- }
2658
- if (entry.sessionId)
2659
- sessionId = entry.sessionId;
2660
- if (entry.cwd)
2661
- sessionCwd = entry.cwd;
2662
- if (entry.message?.role !== "assistant")
2663
- continue;
2664
- const usage = entry.message.usage;
2665
- if (!usage)
2666
- continue;
2667
- const model = entry.message.model;
2668
- if (!model)
2669
- continue;
2670
- const inputTokens = usage.input_tokens ?? 0;
2671
- const outputTokens = usage.output_tokens ?? 0;
2672
- const cacheWrite5mTokens = usage.cache_creation?.ephemeral_5m_input_tokens ?? usage.cache_creation_input_tokens ?? 0;
2673
- const cacheWrite1hTokens = usage.cache_creation?.ephemeral_1h_input_tokens ?? 0;
2674
- const cacheWriteTokens = cacheWrite5mTokens + cacheWrite1hTokens;
2675
- const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
2676
- const timestamp = entry.timestamp ?? new Date().toISOString();
2677
- if (inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens === 0)
2678
- continue;
2679
- let costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens);
2680
- costUsd = applyClaudeModifiers(costUsd, model, usage, entry);
2681
- const serverToolUse = usage.server_tool_use;
2682
- if (serverToolUse?.web_search_requests) {
2683
- costUsd += serverToolUse.web_search_requests * 0.01;
2684
- }
2685
- const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
2686
- const reqId = `${agentName}-${sourceRequestId}`;
2687
- upsertRequest(db, withAccount({
2688
- id: reqId,
2689
- agent: agentName,
2690
- session_id: sessionId,
2691
- model,
2692
- input_tokens: inputTokens,
2693
- output_tokens: outputTokens,
2694
- cache_read_tokens: cacheReadTokens,
2695
- cache_create_tokens: cacheWriteTokens,
2696
- cache_create_5m_tokens: cacheWrite5mTokens,
2697
- cache_create_1h_tokens: cacheWrite1hTokens,
2698
- cost_usd: costUsd,
2699
- cost_basis: defaultCostBasisForAgent(agentName),
2700
- duration_ms: 0,
2701
- timestamp,
2702
- source_request_id: sourceRequestId,
2703
- machine_id: machineId
2704
- }, account));
2705
- if (!touchedSessions.has(sessionId)) {
2706
- const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
2707
- if (!existing) {
2708
- const effectiveCwd = sessionCwd || projectPath;
2709
- const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
2710
- const session = {
2711
- id: sessionId,
2712
- agent: agentName,
2713
- project_path: detectedProject ? detectedProject.path : effectiveCwd,
2714
- project_name: detectedProject ? detectedProject.name : "",
2715
- started_at: timestamp,
2716
- ended_at: null,
2717
- total_cost_usd: 0,
2718
- total_tokens: 0,
2719
- request_count: 0,
2720
- machine_id: machineId
2721
- };
2722
- upsertSession(db, withAccount(session, account));
2723
- }
2724
- touchedSessions.add(sessionId);
2725
- }
2726
- totalRequests++;
2727
- }
2728
- setIngestState(db, agentName, stateKey, fileMtime);
2729
- totalFiles++;
2730
- }
2128
+ function querySinceTotals(db, since, machine) {
2129
+ const requests = queryRequestTotals(db, "timestamp >= ?", [since], machine);
2130
+ const sessions = querySessionOnlyTotals(db, "started_at >= ?", [since], machine);
2131
+ return combineTotals(requests, sessions);
2132
+ }
2133
+ function parseSinceDate(input, now) {
2134
+ const value = input.trim();
2135
+ const match = value.match(/^(\d+(?:\.\d+)?)(m|h|d|w)$/i);
2136
+ if (match) {
2137
+ const amount = Number(match[1]);
2138
+ const unit = match[2].toLowerCase();
2139
+ const multiplier = unit === "m" ? 60000 : unit === "h" ? 60 * 60000 : unit === "d" ? 24 * 60 * 60000 : 7 * 24 * 60 * 60000;
2140
+ return {
2141
+ timestamp: new Date(now.getTime() - amount * multiplier).toISOString(),
2142
+ label: value
2143
+ };
2731
2144
  }
2732
- for (const sessionId of touchedSessions) {
2733
- rollupSession(db, sessionId);
2145
+ const parsed = new Date(value);
2146
+ if (Number.isNaN(parsed.getTime())) {
2147
+ throw new Error("--since must be a duration like 24h, 7d, 30m, 2w, or an ISO date");
2734
2148
  }
2735
- return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
2149
+ return { timestamp: parsed.toISOString(), label: value };
2736
2150
  }
2737
- function applyClaudeModifiers(costUsd, model, usage, entry) {
2738
- let multiplier = 1;
2739
- const speed = usage.speed ?? entry.message?.speed ?? entry.speed;
2740
- if (speed === "fast" && model.includes("opus-4-6")) {
2741
- multiplier *= 6;
2151
+ function queryMachineSinceRows(db, since, machine) {
2152
+ const knownMachines = new Set;
2153
+ for (const m of listMachines(db, "all")) {
2154
+ if (m.machine_id)
2155
+ knownMachines.add(m.machine_id);
2156
+ }
2157
+ for (const m of listMachineRegistry(db)) {
2158
+ if (m.machine_id)
2159
+ knownMachines.add(m.machine_id);
2742
2160
  }
2743
- const inferenceGeo = usage.inference_geo ?? entry.message?.inference_geo ?? entry.inference_geo;
2744
- if (inferenceGeo && ["us", "us-only", "us_only"].includes(inferenceGeo) && supportsClaudeDataResidencyPricing(model)) {
2745
- multiplier *= 1.1;
2161
+ if (machine)
2162
+ knownMachines.add(machine);
2163
+ const requestMachineClause = machine ? " AND machine_id = ?" : "";
2164
+ const requestRows = db.prepare(`
2165
+ SELECT
2166
+ machine_id,
2167
+ COUNT(DISTINCT session_id) as request_sessions,
2168
+ COUNT(*) as requests,
2169
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
2170
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
2171
+ COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
2172
+ COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
2173
+ COALESCE(SUM(COALESCE(cache_create_5m_tokens, cache_create_tokens)), 0) as cache_create_5m_tokens,
2174
+ COALESCE(SUM(COALESCE(cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
2175
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
2176
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
2177
+ MAX(timestamp) as last_active
2178
+ FROM requests
2179
+ WHERE timestamp >= ?
2180
+ AND machine_id != ''
2181
+ ${requestMachineClause}
2182
+ GROUP BY machine_id
2183
+ `).all(...machine ? [since, machine] : [since]);
2184
+ const sessionMachineClause = machine ? " AND machine_id = ?" : "";
2185
+ const sessionRows = db.prepare(`
2186
+ SELECT
2187
+ machine_id,
2188
+ COUNT(*) as sessions,
2189
+ COALESCE(SUM(request_count), 0) as requests,
2190
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
2191
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
2192
+ MAX(started_at) as last_active
2193
+ FROM sessions
2194
+ WHERE started_at >= ?
2195
+ AND machine_id != ''
2196
+ ${sessionMachineClause}
2197
+ AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
2198
+ GROUP BY machine_id
2199
+ `).all(...machine ? [since, machine] : [since]);
2200
+ const groups = new Map;
2201
+ for (const row of requestRows) {
2202
+ knownMachines.add(row.machine_id);
2203
+ groups.set(row.machine_id, combineTotals(baseRequestTotals(row), ZERO_SESSION_TOTALS));
2204
+ }
2205
+ for (const row of sessionRows) {
2206
+ knownMachines.add(row.machine_id);
2207
+ const current = groups.get(row.machine_id) ?? emptyTotals();
2208
+ const sessions = baseSessionTotals(row);
2209
+ groups.set(row.machine_id, {
2210
+ ...current,
2211
+ sessions: current.sessions + sessions.sessions,
2212
+ requests: current.requests + sessions.requests,
2213
+ total_tokens: current.total_tokens + sessions.total_tokens,
2214
+ cost_usd: current.cost_usd + sessions.cost_usd,
2215
+ last_active: latestTimestamp(current.last_active, sessions.last_active)
2216
+ });
2746
2217
  }
2747
- return costUsd * multiplier;
2218
+ const lastData = queryMachineLastData(db, machine);
2219
+ const rows = [...knownMachines].filter((m) => !machine || m === machine).map((machineId) => {
2220
+ const totals = groups.get(machineId) ?? emptyTotals();
2221
+ const lastDataAt = lastData.get(machineId) ?? totals.last_active;
2222
+ return {
2223
+ machine_id: machineId,
2224
+ ...totals,
2225
+ last_data_at: lastDataAt,
2226
+ last_data_age: ""
2227
+ };
2228
+ });
2229
+ return rows.sort((a, b) => b.cost_usd - a.cost_usd || a.machine_id.localeCompare(b.machine_id));
2748
2230
  }
2749
- function supportsClaudeDataResidencyPricing(model) {
2750
- const normalized = normalizeModelName(model);
2751
- const match = normalized.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?(?:-|$)/);
2752
- if (!match)
2753
- return false;
2754
- const major = Number(match[2]);
2755
- const minor = match[3] ? Number(match[3]) : 0;
2756
- return major > 4 || major === 4 && minor >= 6;
2231
+ function queryMachineLastData(db, machine) {
2232
+ const filter = machine ? " AND machine_id = ?" : "";
2233
+ const requestRows = db.prepare(`
2234
+ SELECT machine_id, MAX(timestamp) as last_at
2235
+ FROM requests
2236
+ WHERE machine_id != ''${filter}
2237
+ GROUP BY machine_id
2238
+ `).all(...machine ? [machine] : []);
2239
+ const sessionRows = db.prepare(`
2240
+ SELECT machine_id, MAX(started_at) as last_at
2241
+ FROM sessions
2242
+ WHERE machine_id != ''${filter}
2243
+ GROUP BY machine_id
2244
+ `).all(...machine ? [machine] : []);
2245
+ const result = new Map;
2246
+ for (const row of [...requestRows, ...sessionRows]) {
2247
+ result.set(row.machine_id, latestTimestamp(result.get(row.machine_id), row.last_at));
2248
+ }
2249
+ return result;
2757
2250
  }
2758
- // src/ingest/codex.ts
2759
- init_database();
2760
- init_pricing();
2761
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
2762
- import { homedir as homedir4 } from "os";
2763
- import { join as join5, basename as basename2 } from "path";
2764
- import { Database as BunDatabase2 } from "bun:sqlite";
2765
- var DEFAULT_CODEX_DB_PATH = join5(homedir4(), ".codex", "state_5.sqlite");
2766
- var DEFAULT_CODEX_CONFIG_PATH = join5(homedir4(), ".codex", "config.toml");
2767
- var DEFAULT_CODEWITH_DB_PATH = join5(homedir4(), ".codewith", "state_5.sqlite");
2768
- var DEFAULT_CODEWITH_CONFIG_PATH = join5(homedir4(), ".codewith", "config.toml");
2769
- var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
2770
- function codexDbPath() {
2771
- return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
2772
- }
2773
- function codexConfigPath() {
2774
- return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
2775
- }
2776
- function codewithDbPath() {
2777
- return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ?? DEFAULT_CODEWITH_DB_PATH;
2778
- }
2779
- function codewithConfigPath() {
2780
- return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ?? DEFAULT_CODEWITH_CONFIG_PATH;
2781
- }
2782
- function codexSources() {
2783
- const explicitCodexPath = process.env["HASNA_ECONOMY_CODEX_DB_PATH"];
2784
- const explicitCodewithPath = process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"];
2785
- const sources = [{
2786
- label: "Codex",
2787
- dbPath: codexDbPath(),
2788
- configPath: codexConfigPath(),
2789
- sessionPrefix: "codex",
2790
- stateSource: "codex"
2791
- }];
2792
- if (!explicitCodexPath || explicitCodewithPath) {
2793
- sources.push({
2794
- label: "Codewith",
2795
- dbPath: codewithDbPath(),
2796
- configPath: codewithConfigPath(),
2797
- sessionPrefix: "codex-codewith",
2798
- stateSource: "codex-codewith"
2251
+ function queryAgentSinceRows(db, since, machine) {
2252
+ const requestMachineClause = machine ? " AND machine_id = ?" : "";
2253
+ const sessionMachineClause = machine ? " AND machine_id = ?" : "";
2254
+ const rows = db.prepare(`
2255
+ WITH request_rows AS (
2256
+ SELECT
2257
+ agent,
2258
+ session_id,
2259
+ 1 as requests,
2260
+ input_tokens,
2261
+ output_tokens,
2262
+ cache_read_tokens,
2263
+ cache_create_tokens,
2264
+ COALESCE(cache_create_5m_tokens, cache_create_tokens) as cache_create_5m_tokens,
2265
+ COALESCE(cache_create_1h_tokens, 0) as cache_create_1h_tokens,
2266
+ input_tokens + output_tokens + cache_read_tokens + cache_create_tokens as total_tokens,
2267
+ cost_usd,
2268
+ timestamp as last_active
2269
+ FROM requests
2270
+ WHERE timestamp >= ?${requestMachineClause}
2271
+ ),
2272
+ session_only_rows AS (
2273
+ SELECT
2274
+ agent,
2275
+ id as session_id,
2276
+ COALESCE(request_count, 0) as requests,
2277
+ 0 as input_tokens,
2278
+ 0 as output_tokens,
2279
+ 0 as cache_read_tokens,
2280
+ 0 as cache_create_tokens,
2281
+ 0 as cache_create_5m_tokens,
2282
+ 0 as cache_create_1h_tokens,
2283
+ COALESCE(total_tokens, 0) as total_tokens,
2284
+ COALESCE(total_cost_usd, 0) as cost_usd,
2285
+ started_at as last_active
2286
+ FROM sessions
2287
+ WHERE started_at >= ?${sessionMachineClause}
2288
+ AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
2289
+ ),
2290
+ combined AS (
2291
+ SELECT * FROM request_rows
2292
+ UNION ALL
2293
+ SELECT * FROM session_only_rows
2294
+ )
2295
+ SELECT
2296
+ agent,
2297
+ COUNT(DISTINCT session_id) as sessions,
2298
+ COALESCE(SUM(requests), 0) as requests,
2299
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
2300
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
2301
+ COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
2302
+ COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
2303
+ COALESCE(SUM(cache_create_5m_tokens), 0) as cache_create_5m_tokens,
2304
+ COALESCE(SUM(cache_create_1h_tokens), 0) as cache_create_1h_tokens,
2305
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
2306
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
2307
+ MAX(last_active) as last_active
2308
+ FROM combined
2309
+ GROUP BY agent
2310
+ ORDER BY cost_usd DESC
2311
+ `).all(...machine ? [since, machine, since, machine] : [since, since]);
2312
+ if (rows.length > 0)
2313
+ return rows;
2314
+ return queryAgentBreakdown(db, "all", machine).filter((row) => row.last_active >= since).map(agentBreakdownToBriefRow);
2315
+ }
2316
+ function agentBreakdownToBriefRow(row) {
2317
+ return {
2318
+ agent: row.agent,
2319
+ sessions: row.sessions,
2320
+ requests: row.requests,
2321
+ input_tokens: 0,
2322
+ output_tokens: 0,
2323
+ cache_read_tokens: 0,
2324
+ cache_create_tokens: 0,
2325
+ cache_create_5m_tokens: 0,
2326
+ cache_create_1h_tokens: 0,
2327
+ total_tokens: row.total_tokens,
2328
+ cost_usd: row.cost_usd,
2329
+ last_active: row.last_active
2330
+ };
2331
+ }
2332
+ function normalizeAccountEmail2(email) {
2333
+ return email.trim().toLowerCase();
2334
+ }
2335
+ function accountKey(row) {
2336
+ const agent = row.agent || row.account_tool || "unknown";
2337
+ const email = normalizeAccountEmail2(row.account_email);
2338
+ if (email)
2339
+ return `${agent}:${email}`;
2340
+ if (row.account_name)
2341
+ return `${agent}:${row.account_name}`;
2342
+ return row.account_key || `${agent}:unknown`;
2343
+ }
2344
+ function addAccountRow(groups, row) {
2345
+ if (!row.account_key && !row.account_tool && !row.account_name && !row.account_email)
2346
+ return;
2347
+ const key = accountKey(row);
2348
+ const email = normalizeAccountEmail2(row.account_email);
2349
+ const current = groups.get(key) ?? {
2350
+ account_key: key,
2351
+ account_tool: row.agent || row.account_tool || "unknown",
2352
+ account_name: row.account_name || email || row.account_key || "unknown",
2353
+ account_email: email || null,
2354
+ account_source: row.account_source || "unknown",
2355
+ sessions: 0,
2356
+ requests: 0,
2357
+ input_tokens: 0,
2358
+ output_tokens: 0,
2359
+ cache_read_tokens: 0,
2360
+ cache_create_tokens: 0,
2361
+ cache_create_5m_tokens: 0,
2362
+ cache_create_1h_tokens: 0,
2363
+ total_tokens: 0,
2364
+ cost_usd: 0,
2365
+ last_active: null
2366
+ };
2367
+ current.sessions += Number(row.sessions ?? 0);
2368
+ if (!current.account_email && email)
2369
+ current.account_email = email;
2370
+ if ((!current.account_name || current.account_name === "unknown") && (row.account_name || email)) {
2371
+ current.account_name = row.account_name || email;
2372
+ }
2373
+ if ((!current.account_source || current.account_source === "unknown") && row.account_source && row.account_source !== "unknown") {
2374
+ current.account_source = row.account_source;
2375
+ }
2376
+ current.requests += Number(row.requests ?? 0);
2377
+ current.input_tokens += Number(row.input_tokens ?? 0);
2378
+ current.output_tokens += Number(row.output_tokens ?? 0);
2379
+ current.cache_read_tokens += Number(row.cache_read_tokens ?? 0);
2380
+ current.cache_create_tokens += Number(row.cache_create_tokens ?? 0);
2381
+ current.cache_create_5m_tokens += Number(row.cache_create_5m_tokens ?? 0);
2382
+ current.cache_create_1h_tokens += Number(row.cache_create_1h_tokens ?? 0);
2383
+ current.total_tokens += Number(row.total_tokens ?? 0);
2384
+ current.cost_usd += Number(row.cost_usd ?? 0);
2385
+ current.last_active = latestTimestamp(current.last_active, row.last_active);
2386
+ groups.set(key, current);
2387
+ }
2388
+ function queryAccountSinceRows(db, since, machine) {
2389
+ const requestMachineClause = machine ? " AND r.machine_id = ?" : "";
2390
+ const sessionMachineClause = machine ? " AND s.machine_id = ?" : "";
2391
+ const requestRows = db.prepare(`
2392
+ SELECT
2393
+ COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), '') as agent,
2394
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as account_key,
2395
+ COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') as account_tool,
2396
+ COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as account_name,
2397
+ COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') as account_email,
2398
+ COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
2399
+ COUNT(DISTINCT r.session_id) as sessions,
2400
+ COUNT(*) as requests,
2401
+ COALESCE(SUM(r.input_tokens), 0) as input_tokens,
2402
+ COALESCE(SUM(r.output_tokens), 0) as output_tokens,
2403
+ COALESCE(SUM(r.cache_read_tokens), 0) as cache_read_tokens,
2404
+ COALESCE(SUM(r.cache_create_tokens), 0) as cache_create_tokens,
2405
+ COALESCE(SUM(COALESCE(r.cache_create_5m_tokens, r.cache_create_tokens, 0)), 0) as cache_create_5m_tokens,
2406
+ COALESCE(SUM(COALESCE(r.cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
2407
+ COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
2408
+ COALESCE(SUM(r.cost_usd), 0) as cost_usd,
2409
+ MAX(r.timestamp) as last_active
2410
+ FROM requests r
2411
+ LEFT JOIN sessions s ON s.id = r.session_id
2412
+ WHERE r.timestamp >= ?${requestMachineClause}
2413
+ GROUP BY
2414
+ COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), ''),
2415
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), ''),
2416
+ COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), ''),
2417
+ COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), ''),
2418
+ COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), ''),
2419
+ COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown')
2420
+ `).all(...machine ? [since, machine] : [since]);
2421
+ const sessionRows = db.prepare(`
2422
+ SELECT
2423
+ s.agent as agent,
2424
+ s.account_key as account_key,
2425
+ s.account_tool as account_tool,
2426
+ s.account_name as account_name,
2427
+ COALESCE(s.account_email, '') as account_email,
2428
+ COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
2429
+ COUNT(DISTINCT s.id) as sessions,
2430
+ COALESCE(SUM(s.request_count), 0) as requests,
2431
+ 0 as input_tokens,
2432
+ 0 as output_tokens,
2433
+ 0 as cache_read_tokens,
2434
+ 0 as cache_create_tokens,
2435
+ 0 as cache_create_5m_tokens,
2436
+ 0 as cache_create_1h_tokens,
2437
+ COALESCE(SUM(s.total_tokens), 0) as total_tokens,
2438
+ COALESCE(SUM(s.total_cost_usd), 0) as cost_usd,
2439
+ MAX(s.started_at) as last_active
2440
+ FROM sessions s
2441
+ WHERE s.started_at >= ?${sessionMachineClause}
2442
+ AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = s.id)
2443
+ GROUP BY
2444
+ s.agent,
2445
+ s.account_key,
2446
+ s.account_tool,
2447
+ s.account_name,
2448
+ COALESCE(s.account_email, ''),
2449
+ COALESCE(NULLIF(s.account_source, ''), 'unknown')
2450
+ `).all(...machine ? [since, machine] : [since]);
2451
+ const groups = new Map;
2452
+ for (const row of requestRows)
2453
+ addAccountRow(groups, row);
2454
+ for (const row of sessionRows)
2455
+ addAccountRow(groups, row);
2456
+ const rows = [...groups.values()];
2457
+ rows.sort((a, b) => b.cost_usd - a.cost_usd || a.account_key.localeCompare(b.account_key));
2458
+ if (rows.length > 0)
2459
+ return rows;
2460
+ return queryAccountBreakdown(db, "all", machine).filter((row) => row.last_active >= since).map(accountBreakdownToBriefRow);
2461
+ }
2462
+ function accountBreakdownToBriefRow(row) {
2463
+ return {
2464
+ account_key: row.account_key,
2465
+ account_tool: row.account_tool,
2466
+ account_name: row.account_name,
2467
+ account_email: row.account_email,
2468
+ account_source: row.account_source,
2469
+ sessions: row.sessions,
2470
+ requests: row.requests,
2471
+ input_tokens: 0,
2472
+ output_tokens: 0,
2473
+ cache_read_tokens: 0,
2474
+ cache_create_tokens: 0,
2475
+ cache_create_5m_tokens: 0,
2476
+ cache_create_1h_tokens: 0,
2477
+ total_tokens: row.total_tokens,
2478
+ cost_usd: row.cost_usd,
2479
+ last_active: row.last_active
2480
+ };
2481
+ }
2482
+ function queryFreshness(db, now, machine, localSync) {
2483
+ const requestFilter = machine ? " AND machine_id = ?" : "";
2484
+ const requestRows = db.prepare(`
2485
+ SELECT machine_id, MAX(timestamp) as max_request_at
2486
+ FROM requests
2487
+ WHERE machine_id != ''${requestFilter}
2488
+ GROUP BY machine_id
2489
+ `).all(...machine ? [machine] : []);
2490
+ const registryRows = listMachineRegistry(db).filter((row) => !machine || row.machine_id === machine);
2491
+ const machines = new Map;
2492
+ for (const row of requestRows) {
2493
+ machines.set(row.machine_id, {
2494
+ machine_id: row.machine_id,
2495
+ max_request_at: row.max_request_at,
2496
+ request_age: formatAge(row.max_request_at, now),
2497
+ last_merge_sync_at: null,
2498
+ merge_sync_age: "never"
2799
2499
  });
2800
2500
  }
2801
- const seen = new Set;
2802
- return sources.filter((source) => {
2803
- if (seen.has(source.dbPath))
2804
- return false;
2805
- seen.add(source.dbPath);
2806
- return true;
2807
- });
2501
+ for (const row of registryRows) {
2502
+ const current = machines.get(row.machine_id) ?? {
2503
+ machine_id: row.machine_id,
2504
+ max_request_at: null,
2505
+ request_age: "never",
2506
+ last_merge_sync_at: null,
2507
+ merge_sync_age: "never"
2508
+ };
2509
+ const syncAt = latestTimestamp(row.last_pull_at, row.last_push_at, row.updated_at);
2510
+ current.last_merge_sync_at = syncAt;
2511
+ current.merge_sync_age = formatAge(syncAt, now);
2512
+ machines.set(row.machine_id, current);
2513
+ }
2514
+ if (localSync && (!machine || machine === localSync.machineId)) {
2515
+ const current = machines.get(localSync.machineId) ?? {
2516
+ machine_id: localSync.machineId,
2517
+ max_request_at: null,
2518
+ request_age: "never",
2519
+ last_merge_sync_at: null,
2520
+ merge_sync_age: "never"
2521
+ };
2522
+ current.last_merge_sync_at = latestTimestamp(current.last_merge_sync_at, localSync.syncedAt);
2523
+ current.merge_sync_age = formatAge(current.last_merge_sync_at, now);
2524
+ machines.set(localSync.machineId, current);
2525
+ }
2526
+ const rows = [...machines.values()].sort((a, b) => a.machine_id.localeCompare(b.machine_id));
2527
+ return {
2528
+ machines: rows,
2529
+ max_request_line: rows.length ? rows.map((row) => `${row.machine_id} ${row.max_request_at ?? "never"} (${row.request_age})`).join("; ") : "none",
2530
+ merge_sync_line: rows.length ? rows.map((row) => `${row.machine_id} ${row.last_merge_sync_at ?? "never"} (${row.merge_sync_age})`).join("; ") : "none"
2531
+ };
2808
2532
  }
2809
- function readCodexModel(configPath = codexConfigPath()) {
2810
- if (!existsSync6(configPath))
2811
- return "gpt-5-codex";
2812
- try {
2813
- const content = readFileSync4(configPath, "utf-8");
2814
- const match = content.match(/^model\s*=\s*"([^"]+)"/m);
2815
- return match?.[1] ?? "gpt-5-codex";
2816
- } catch {
2817
- return "gpt-5-codex";
2818
- }
2819
- }
2820
- function buildThreadQuery(codexDb) {
2821
- const cols = new Set(codexDb.prepare(`PRAGMA table_info(threads)`).all().map((c) => c.name));
2822
- const modelSelect = cols.has("model") ? "model" : "NULL AS model";
2823
- const rolloutSelect = cols.has("rollout_path") ? "rollout_path" : "NULL AS rollout_path";
2824
- const providerSelect = cols.has("model_provider") ? "model_provider" : "NULL AS model_provider";
2825
- return `
2826
- SELECT id, ${rolloutSelect}, cwd, created_at, updated_at, tokens_used, title,
2827
- ${providerSelect}, ${modelSelect}
2828
- FROM threads WHERE tokens_used > 0
2829
- `;
2830
- }
2831
- function openCodexDb(dbPath, verbose) {
2832
- let lastError;
2833
- for (const readonly of [true, false]) {
2834
- let codexDb = null;
2835
- try {
2836
- codexDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
2837
- codexDb.prepare("PRAGMA schema_version").get();
2838
- return codexDb;
2839
- } catch (error) {
2840
- lastError = error;
2841
- codexDb?.close();
2533
+ function buildBrief(db, opts = {}) {
2534
+ const now = opts.now ?? new Date;
2535
+ const machine = machineFilter(opts.machine);
2536
+ const sinceInput = opts.since ?? "24h";
2537
+ const since = parseSinceDate(sinceInput, now);
2538
+ const summaries = [
2539
+ {
2540
+ label: "Today",
2541
+ period: "today",
2542
+ ...queryPeriodTotals(db, "today", machine)
2543
+ },
2544
+ {
2545
+ label: "Week",
2546
+ period: "week",
2547
+ ...queryPeriodTotals(db, "week", machine)
2548
+ },
2549
+ {
2550
+ label: `Since ${since.label}`,
2551
+ period: "since",
2552
+ since: since.timestamp,
2553
+ ...querySinceTotals(db, since.timestamp, machine)
2842
2554
  }
2555
+ ];
2556
+ const machines = queryMachineSinceRows(db, since.timestamp, machine);
2557
+ for (const row of machines) {
2558
+ row.last_data_age = formatAge(row.last_data_at, now);
2843
2559
  }
2844
- if (verbose) {
2845
- const message = lastError instanceof Error ? lastError.message : String(lastError);
2846
- console.log("Codex DB unreadable:", dbPath, message);
2560
+ return {
2561
+ generated_at: now.toISOString(),
2562
+ machine: machine ?? "all",
2563
+ since: {
2564
+ input: sinceInput,
2565
+ timestamp: since.timestamp,
2566
+ label: since.label
2567
+ },
2568
+ summaries,
2569
+ machines,
2570
+ agents: queryAgentSinceRows(db, since.timestamp, machine),
2571
+ accounts: queryAccountSinceRows(db, since.timestamp, machine),
2572
+ freshness: queryFreshness(db, now, machine, opts.currentMachineId && opts.localSyncAt ? { machineId: opts.currentMachineId, syncedAt: opts.localSyncAt.toISOString() } : undefined)
2573
+ };
2574
+ }
2575
+
2576
+ // src/lib/analytics.ts
2577
+ function queryProjectDetail(db, nameOrPath) {
2578
+ const like = `%${nameOrPath}%`;
2579
+ const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
2580
+ FROM sessions
2581
+ WHERE project_name LIKE ? OR project_path LIKE ?
2582
+ ORDER BY started_at DESC`).all(like, like);
2583
+ if (sessions.length === 0)
2584
+ return null;
2585
+ const first = sessions[0];
2586
+ const totalCost = sessions.reduce((s, r) => s + (r.total_cost_usd ?? 0), 0);
2587
+ const totalTokens = sessions.reduce((s, r) => s + (r.total_tokens ?? 0), 0);
2588
+ const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
2589
+ FROM requests r JOIN sessions s ON r.session_id = s.id
2590
+ WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
2591
+ AND r.timestamp >= DATE('now', '-14 days')
2592
+ GROUP BY date ORDER BY date ASC`).all(like, like);
2593
+ const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
2594
+ FROM requests r JOIN sessions s ON r.session_id = s.id
2595
+ WHERE s.project_name LIKE ? OR s.project_path LIKE ?
2596
+ GROUP BY r.model ORDER BY cost_usd DESC LIMIT 5`).all(like, like);
2597
+ const top_sessions = [...sessions].sort((a, b) => (b.total_cost_usd ?? 0) - (a.total_cost_usd ?? 0)).slice(0, 5).map((s) => ({ id: s.id, total_cost_usd: s.total_cost_usd ?? 0, started_at: s.started_at }));
2598
+ return {
2599
+ project_name: first.project_name || nameOrPath,
2600
+ project_path: first.project_path || "",
2601
+ total_cost_usd: totalCost,
2602
+ total_tokens: totalTokens,
2603
+ sessions: sessions.length,
2604
+ daily,
2605
+ models,
2606
+ top_sessions
2607
+ };
2608
+ }
2609
+ function exportDateWhere(column, period) {
2610
+ switch (period) {
2611
+ case "today":
2612
+ return `DATE(${column}) = DATE('now')`;
2613
+ case "week":
2614
+ return `${column} >= DATE('now', '-7 days')`;
2615
+ case "all":
2616
+ return "1=1";
2617
+ default:
2618
+ return `${column} >= DATE('now', '-30 days')`;
2619
+ }
2620
+ }
2621
+ function queryExportRows(db, type, period) {
2622
+ if (type === "requests") {
2623
+ const where2 = exportDateWhere("timestamp", period);
2624
+ return db.prepare(`SELECT * FROM requests WHERE ${where2} ORDER BY timestamp ASC`).all();
2625
+ }
2626
+ const where = exportDateWhere("started_at", period);
2627
+ return db.prepare(`SELECT * FROM sessions WHERE ${where} ORDER BY started_at DESC`).all();
2628
+ }
2629
+ function queryRangeStats(db, from, to) {
2630
+ const r = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost, COUNT(*) as requests,
2631
+ COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens+cache_create_tokens),0) as tokens
2632
+ FROM requests WHERE DATE(timestamp) BETWEEN ? AND ?`).get(from, to);
2633
+ const s = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
2634
+ return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
2635
+ }
2636
+ function queryForecast(db, now = new Date) {
2637
+ const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
2638
+ const dayOfMonth = now.getDate();
2639
+ const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
2640
+ const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(monthStart);
2641
+ const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
2642
+ const projected = dailyAvg * daysInMonth;
2643
+ const sevenDaysAgo = new Date(now);
2644
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
2645
+ const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
2646
+ const last7DailyAvg = last7.cost / 7;
2647
+ const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
2648
+ FROM requests WHERE DATE(timestamp) >= ? GROUP BY date ORDER BY cost_usd ASC`).all(monthStart);
2649
+ return {
2650
+ day_of_month: dayOfMonth,
2651
+ days_in_month: daysInMonth,
2652
+ spent_so_far_usd: monthSoFar.cost,
2653
+ daily_avg_usd: dailyAvg,
2654
+ projected_usd: projected,
2655
+ last7_daily_avg_usd: last7DailyAvg,
2656
+ last7_projected_usd: last7DailyAvg * daysInMonth,
2657
+ cheapest_day: dailyCosts[0] ?? null,
2658
+ most_expensive_day: dailyCosts[dailyCosts.length - 1] ?? null
2659
+ };
2660
+ }
2661
+ function queryModelEfficiency(db) {
2662
+ return db.prepare(`SELECT model,
2663
+ COALESCE(SUM(input_tokens),0) as input,
2664
+ COALESCE(SUM(output_tokens),0) as output,
2665
+ COALESCE(SUM(cache_read_tokens),0) as cache_read,
2666
+ COALESCE(SUM(cache_create_tokens),0) as cache_write,
2667
+ COUNT(*) as requests,
2668
+ COALESCE(SUM(cost_usd),0) as cost
2669
+ FROM requests GROUP BY model ORDER BY cost DESC`).all();
2670
+ }
2671
+
2672
+ // src/lib/contracts-client/mode.ts
2673
+ var DEPRECATED_STORAGE_MODE_ALIASES = ["remote", "hybrid", "self_hosted"];
2674
+ function normalizeStorageMode(value) {
2675
+ const normalized = value.trim().toLowerCase().replace(/-/g, "_");
2676
+ if (normalized === "local")
2677
+ return { mode: "local", deprecatedAlias: null };
2678
+ if (normalized === "cloud")
2679
+ return { mode: "cloud", deprecatedAlias: null };
2680
+ if (DEPRECATED_STORAGE_MODE_ALIASES.includes(normalized)) {
2681
+ return { mode: "cloud", deprecatedAlias: normalized };
2682
+ }
2683
+ throw new Error(`Unknown storage mode: ${value}. Use local or cloud.`);
2684
+ }
2685
+ function envToken(name) {
2686
+ return name.toUpperCase().replace(/-/g, "_");
2687
+ }
2688
+
2689
+ // src/lib/contracts-client/transport.ts
2690
+ function defaultCloudBaseUrl(name) {
2691
+ return `https://${name}.hasna.xyz`;
2692
+ }
2693
+ function clientTransportEnvKeys(name) {
2694
+ const token = envToken(name);
2695
+ return {
2696
+ modeKeys: [
2697
+ `HASNA_${token}_STORAGE_MODE`,
2698
+ `HASNA_${token}_MODE`,
2699
+ `${token}_STORAGE_MODE`,
2700
+ `${token}_MODE`
2701
+ ],
2702
+ apiUrlKeys: [`HASNA_${token}_API_URL`, `${token}_API_URL`],
2703
+ apiKeyKeys: [`HASNA_${token}_API_KEY`, `${token}_API_KEY`]
2704
+ };
2705
+ }
2706
+ function firstEnv(env, keys) {
2707
+ for (const key of keys) {
2708
+ const value = env[key]?.trim();
2709
+ if (value)
2710
+ return { key, value };
2847
2711
  }
2848
2712
  return null;
2849
2713
  }
2850
- function readTokenEvents(rolloutPath) {
2851
- if (!rolloutPath || !existsSync6(rolloutPath))
2852
- return [];
2853
- const fallbackUsages = new Map;
2854
- let fallbackTimestamp;
2855
- let aggregate = null;
2856
- for (const line of readFileSync4(rolloutPath, "utf-8").split(`
2857
- `)) {
2858
- if (!line.trim())
2859
- continue;
2860
- let entry;
2861
- try {
2862
- entry = JSON.parse(line);
2863
- } catch {
2864
- continue;
2865
- }
2866
- if (!entry || typeof entry !== "object")
2867
- continue;
2868
- const payload = entry["payload"];
2869
- if (!payload || payload["type"] !== "token_count")
2870
- continue;
2871
- const info = payload["info"];
2872
- const timestamp = entry["timestamp"];
2873
- const entryTimestamp = typeof timestamp === "string" ? timestamp : undefined;
2874
- const totalUsage = info?.["total_token_usage"];
2875
- if (totalUsage && tokenTotal(totalUsage) > 0) {
2876
- aggregate = { usage: totalUsage, timestamp: entryTimestamp };
2877
- continue;
2714
+ function toV1BaseUrl(apiUrl) {
2715
+ const url = new URL(apiUrl);
2716
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
2717
+ throw new Error("API URL must use http or https.");
2718
+ }
2719
+ let path = url.pathname.replace(/\/+$/, "");
2720
+ if (path.endsWith("/v1"))
2721
+ path = path.slice(0, -"/v1".length);
2722
+ url.pathname = `${path}/v1`;
2723
+ url.search = "";
2724
+ url.hash = "";
2725
+ return url.toString().replace(/\/+$/, "");
2726
+ }
2727
+ function resolveClientTransport(name, env = process.env) {
2728
+ const keys = clientTransportEnvKeys(name);
2729
+ const modeHit = firstEnv(env, keys.modeKeys);
2730
+ const urlHit = firstEnv(env, keys.apiUrlKeys);
2731
+ const keyHit = firstEnv(env, keys.apiKeyKeys);
2732
+ let mode = "local";
2733
+ let deprecatedAlias = null;
2734
+ let modeSource = "default";
2735
+ const warnings = [];
2736
+ if (modeHit) {
2737
+ const normalized = normalizeStorageMode(modeHit.value);
2738
+ mode = normalized.mode;
2739
+ deprecatedAlias = normalized.deprecatedAlias;
2740
+ modeSource = modeHit.key;
2741
+ if (deprecatedAlias) {
2742
+ warnings.push(`Deprecated mode '${deprecatedAlias}' from ${modeHit.key} is treated as 'cloud'. Prefer ${keys.modeKeys[0]}=cloud.`);
2878
2743
  }
2879
- const usage = info?.["last_token_usage"];
2880
- if (!usage)
2881
- continue;
2882
- if (tokenTotal(usage) <= 0)
2883
- continue;
2884
- const key = JSON.stringify(usage);
2885
- if (!fallbackUsages.has(key))
2886
- fallbackUsages.set(key, usage);
2887
- fallbackTimestamp = entryTimestamp ?? fallbackTimestamp;
2888
- }
2889
- if (aggregate)
2890
- return [aggregate];
2891
- if (fallbackUsages.size === 0)
2892
- return [];
2893
- return [{ usage: sumTokenUsages([...fallbackUsages.values()]), timestamp: fallbackTimestamp }];
2894
- }
2895
- function tokenTotal(usage) {
2896
- return usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
2897
- }
2898
- function sumTokenUsages(usages) {
2899
- const result = {
2900
- input_tokens: 0,
2901
- cached_input_tokens: 0,
2902
- output_tokens: 0,
2903
- reasoning_output_tokens: 0,
2904
- total_tokens: 0
2905
- };
2906
- for (const usage of usages) {
2907
- result.input_tokens = (result.input_tokens ?? 0) + (usage.input_tokens ?? 0);
2908
- result.cached_input_tokens = (result.cached_input_tokens ?? 0) + (usage.cached_input_tokens ?? 0);
2909
- result.output_tokens = (result.output_tokens ?? 0) + (usage.output_tokens ?? 0);
2910
- result.reasoning_output_tokens = (result.reasoning_output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0);
2911
- result.total_tokens = (result.total_tokens ?? 0) + tokenTotal(usage);
2744
+ } else if (urlHit && keyHit) {
2745
+ mode = "cloud";
2746
+ modeSource = `${urlHit.key}+${keyHit.key}`;
2912
2747
  }
2913
- return result;
2748
+ if (mode === "local") {
2749
+ return {
2750
+ transport: "local",
2751
+ mode,
2752
+ deprecatedAlias,
2753
+ modeSource,
2754
+ baseUrl: null,
2755
+ apiUrlSource: null,
2756
+ apiKeyPresent: Boolean(keyHit),
2757
+ apiKeySource: keyHit ? keyHit.key : null,
2758
+ misconfigured: false,
2759
+ warning: warnings.length > 0 ? warnings.join(" ") : null
2760
+ };
2761
+ }
2762
+ if (!keyHit) {
2763
+ warnings.push(`${modeSource}=cloud but no API key is set (${keys.apiKeyKeys[0]}). Refusing to route to cloud; using local store. Set ${keys.apiKeyKeys[0]} to enable the cloud client.`);
2764
+ return {
2765
+ transport: "local",
2766
+ mode,
2767
+ deprecatedAlias,
2768
+ modeSource,
2769
+ baseUrl: null,
2770
+ apiUrlSource: null,
2771
+ apiKeyPresent: false,
2772
+ apiKeySource: null,
2773
+ misconfigured: true,
2774
+ warning: warnings.join(" ")
2775
+ };
2776
+ }
2777
+ const rawUrl = urlHit?.value ?? defaultCloudBaseUrl(name);
2778
+ const apiUrlSource = urlHit ? urlHit.key : "default";
2779
+ let baseUrl;
2780
+ try {
2781
+ baseUrl = toV1BaseUrl(rawUrl);
2782
+ } catch (error) {
2783
+ const message = error instanceof Error ? error.message : String(error);
2784
+ warnings.push(`Invalid API URL from ${apiUrlSource}: ${message}. Using local store.`);
2785
+ return {
2786
+ transport: "local",
2787
+ mode,
2788
+ deprecatedAlias,
2789
+ modeSource,
2790
+ baseUrl: null,
2791
+ apiUrlSource: null,
2792
+ apiKeyPresent: true,
2793
+ apiKeySource: keyHit.key,
2794
+ misconfigured: true,
2795
+ warning: warnings.join(" ")
2796
+ };
2797
+ }
2798
+ return {
2799
+ transport: "cloud-http",
2800
+ mode,
2801
+ deprecatedAlias,
2802
+ modeSource,
2803
+ baseUrl,
2804
+ apiUrlSource,
2805
+ apiKeyPresent: true,
2806
+ apiKeySource: keyHit.key,
2807
+ misconfigured: false,
2808
+ warning: warnings.length > 0 ? warnings.join(" ") : null
2809
+ };
2914
2810
  }
2915
- function fallbackEvents(totalTokens) {
2916
- const inputTokens = Math.floor(totalTokens * 0.6);
2917
- return [{
2918
- usage: {
2919
- input_tokens: inputTokens,
2920
- cached_input_tokens: 0,
2921
- output_tokens: totalTokens - inputTokens,
2922
- total_tokens: totalTokens
2811
+
2812
+ class HasnaHttpError extends Error {
2813
+ status;
2814
+ method;
2815
+ path;
2816
+ body;
2817
+ constructor(method, path, status, body) {
2818
+ super(`Hasna cloud request failed: ${method} ${path} -> ${status}`);
2819
+ this.name = "HasnaHttpError";
2820
+ this.status = status;
2821
+ this.method = method;
2822
+ this.path = path;
2823
+ this.body = body;
2824
+ }
2825
+ }
2826
+ var DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
2827
+ var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
2828
+ function appendQuery(path, query) {
2829
+ if (!query)
2830
+ return path;
2831
+ const params = query instanceof URLSearchParams ? query : new URLSearchParams;
2832
+ if (!(query instanceof URLSearchParams)) {
2833
+ for (const [key, value] of Object.entries(query)) {
2834
+ if (value === null || value === undefined)
2835
+ continue;
2836
+ if (Array.isArray(value)) {
2837
+ for (const v of value)
2838
+ params.append(key, String(v));
2839
+ } else {
2840
+ params.append(key, String(value));
2841
+ }
2923
2842
  }
2924
- }];
2925
- }
2926
- async function ingestCodex(db, verbose = false) {
2927
- const machineId = getMachineId();
2928
- let ingested = 0;
2929
- let requests = 0;
2930
- const account = await resolveAccountForAgent("codex");
2931
- for (const source of codexSources()) {
2932
- if (!existsSync6(source.dbPath)) {
2933
- if (verbose)
2934
- console.log(`${source.label} DB not found:`, source.dbPath);
2935
- continue;
2843
+ }
2844
+ const qs = params.toString();
2845
+ if (!qs)
2846
+ return path;
2847
+ return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
2848
+ }
2849
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2850
+ function createHasnaHttpTransport(options) {
2851
+ const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
2852
+ const base = options.baseUrl.replace(/\/+$/, "");
2853
+ const timeoutMs = options.timeoutMs ?? 30000;
2854
+ const sleep = options.sleepImpl ?? defaultSleep;
2855
+ const defaultRetry = options.retry;
2856
+ function resolveRetry(callRetry) {
2857
+ const chosen = callRetry !== undefined ? callRetry : defaultRetry;
2858
+ if (chosen === false)
2859
+ return null;
2860
+ const r = chosen ?? {};
2861
+ return {
2862
+ retries: r.retries ?? 2,
2863
+ baseDelayMs: r.baseDelayMs ?? 200,
2864
+ maxDelayMs: r.maxDelayMs ?? 2000,
2865
+ retryStatuses: r.retryStatuses ?? [...DEFAULT_RETRY_STATUSES]
2866
+ };
2867
+ }
2868
+ async function once(method, rel, url, body, opts) {
2869
+ const headers = {
2870
+ "x-api-key": options.apiKey,
2871
+ Authorization: `Bearer ${options.apiKey}`,
2872
+ Accept: "application/json",
2873
+ ...options.headers ?? {},
2874
+ ...opts.headers ?? {}
2875
+ };
2876
+ if (opts.idempotencyKey)
2877
+ headers["Idempotency-Key"] = opts.idempotencyKey;
2878
+ const init = { method, headers };
2879
+ if (body !== undefined) {
2880
+ headers["Content-Type"] = "application/json";
2881
+ init.body = JSON.stringify(body);
2936
2882
  }
2937
- let codexDb = null;
2883
+ const controller = new AbortController;
2884
+ const onAbort = () => controller.abort();
2885
+ if (opts.signal) {
2886
+ if (opts.signal.aborted)
2887
+ controller.abort();
2888
+ else
2889
+ opts.signal.addEventListener("abort", onAbort, { once: true });
2890
+ }
2891
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? timeoutMs);
2892
+ init.signal = controller.signal;
2893
+ let response;
2938
2894
  try {
2939
- codexDb = openCodexDb(source.dbPath, verbose);
2940
- if (!codexDb)
2941
- continue;
2942
- const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
2943
- for (const thread of threads) {
2944
- const model = thread.model ?? readCodexModel(source.configPath);
2945
- const stateValue = `${CODEX_INGEST_VERSION}:${thread.updated_at}:${thread.tokens_used}:${model}`;
2946
- const processed = getIngestState(db, source.stateSource, thread.id);
2947
- if (processed === stateValue)
2948
- continue;
2949
- const projectPath = thread.cwd ?? "";
2950
- const projectName = projectPath ? basename2(projectPath) : "unknown";
2951
- const sessionId = `${source.sessionPrefix}-${thread.id}`;
2952
- const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
2953
- const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
2954
- upsertSession(db, withAccount({
2955
- id: sessionId,
2956
- agent: "codex",
2957
- project_path: projectPath,
2958
- project_name: projectName,
2959
- started_at: startedAt,
2960
- ended_at: endedAt,
2961
- total_cost_usd: 0,
2962
- total_tokens: 0,
2963
- request_count: 0,
2964
- machine_id: machineId
2965
- }, account));
2966
- const events = readTokenEvents(thread.rollout_path);
2967
- const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
2968
- const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
2969
- db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
2970
- tokenEvents.forEach((event, index) => {
2971
- const usage = event.usage;
2972
- const inputTotal = usage.input_tokens ?? 0;
2973
- const cacheReadTokens = usage.cached_input_tokens ?? 0;
2974
- const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
2975
- const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
2976
- const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
2977
- const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
2978
- const requestId = `${sessionId}-${index}`;
2979
- upsertRequest(db, withAccount({
2980
- id: requestId,
2981
- agent: "codex",
2982
- session_id: sessionId,
2983
- model,
2984
- input_tokens: inputTokens,
2985
- output_tokens: outputTokens,
2986
- cache_read_tokens: cacheReadTokens,
2987
- cache_create_tokens: 0,
2988
- cost_usd: costUsd,
2989
- cost_basis: defaultCostBasisForAgent("codex"),
2990
- duration_ms: 0,
2991
- timestamp,
2992
- source_request_id: requestId,
2993
- machine_id: machineId
2994
- }, account));
2995
- requests++;
2996
- });
2997
- rollupSession(db, sessionId);
2998
- setIngestState(db, source.stateSource, thread.id, stateValue);
2999
- ingested++;
3000
- if (verbose)
3001
- console.log(`${source.label} session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
3002
- }
2895
+ response = await fetchImpl(url, init);
2896
+ } catch (error) {
2897
+ const err = error instanceof Error ? error : new Error(String(error));
2898
+ if (opts.signal?.aborted)
2899
+ return { ok: false, retryable: false, error: err };
2900
+ return { ok: false, retryable: true, error: err };
3003
2901
  } finally {
3004
- codexDb?.close();
2902
+ clearTimeout(timer);
2903
+ if (opts.signal)
2904
+ opts.signal.removeEventListener("abort", onAbort);
2905
+ }
2906
+ const text = await response.text();
2907
+ let parsed = undefined;
2908
+ if (text.length > 0) {
2909
+ try {
2910
+ parsed = JSON.parse(text);
2911
+ } catch {
2912
+ parsed = text;
2913
+ }
2914
+ }
2915
+ if (!response.ok) {
2916
+ const retry = resolveRetry(opts.retry);
2917
+ const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
2918
+ return { ok: false, retryable, error: new HasnaHttpError(method, rel, response.status, parsed) };
3005
2919
  }
2920
+ return { ok: true, value: parsed };
2921
+ }
2922
+ async function request(method, path, body, opts = {}) {
2923
+ const upper = method.toUpperCase();
2924
+ const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
2925
+ const url = `${base}${rel}`;
2926
+ const retry = resolveRetry(opts.retry);
2927
+ const methodRetryable = IDEMPOTENT_METHODS.has(upper) || Boolean(opts.idempotencyKey);
2928
+ const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
2929
+ let last = null;
2930
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2931
+ const result = await once(upper, rel, url, body, opts);
2932
+ if (result.ok)
2933
+ return result.value;
2934
+ last = result;
2935
+ const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
2936
+ if (!canRetry)
2937
+ break;
2938
+ const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
2939
+ const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
2940
+ await sleep(backoff + jitter);
2941
+ }
2942
+ throw last.error;
3006
2943
  }
3007
- return { sessions: ingested, requests };
2944
+ return {
2945
+ baseUrl: base,
2946
+ request,
2947
+ get: (path, opts) => request("GET", path, undefined, opts),
2948
+ post: (path, body, opts) => request("POST", path, body, opts),
2949
+ put: (path, body, opts) => request("PUT", path, body, opts),
2950
+ patch: (path, body, opts) => request("PATCH", path, body, opts),
2951
+ del: (path, body, opts) => request("DELETE", path, body, opts)
2952
+ };
3008
2953
  }
3009
- // src/ingest/gemini.ts
3010
- init_database();
3011
- init_pricing();
3012
- import { readdirSync as readdirSync3, readFileSync as readFileSync5, existsSync as existsSync7, statSync as statSync3 } from "fs";
3013
- import { homedir as homedir5 } from "os";
3014
- import { join as join6, basename as basename3 } from "path";
3015
- var DEFAULT_GEMINI_TMP_DIR = join6(homedir5(), ".gemini", "tmp");
3016
- var DEFAULT_GEMINI_HISTORY_DIR = join6(homedir5(), ".gemini", "history");
3017
- function geminiTmpDir() {
3018
- return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
3019
- }
3020
- function geminiHistoryDir() {
3021
- return process.env["HASNA_ECONOMY_GEMINI_HISTORY_DIR"] ?? DEFAULT_GEMINI_HISTORY_DIR;
3022
- }
3023
- function numberField(...values) {
3024
- for (const value of values) {
3025
- if (typeof value === "number" && Number.isFinite(value))
3026
- return value;
3027
- }
3028
- return 0;
3029
- }
3030
- function listProjectDirs(...roots) {
3031
- const dirs = new Set;
3032
- for (const root of roots) {
3033
- if (!existsSync7(root))
3034
- continue;
3035
- try {
3036
- for (const entry of readdirSync3(root, { withFileTypes: true })) {
3037
- if (entry.isDirectory())
3038
- dirs.add(join6(root, entry.name));
3039
- }
3040
- } catch {}
2954
+ function createClientTransport(name, env = process.env, overrides) {
2955
+ const resolution = resolveClientTransport(name, env);
2956
+ if (resolution.misconfigured) {
2957
+ throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for cloud mode.`);
3041
2958
  }
3042
- return [...dirs];
2959
+ if (resolution.transport === "local" || !resolution.baseUrl) {
2960
+ return { transport: "local", client: null, resolution };
2961
+ }
2962
+ const keys = clientTransportEnvKeys(name);
2963
+ const apiKey = firstEnv(env, keys.apiKeyKeys)?.value;
2964
+ if (!apiKey) {
2965
+ throw new Error(`Client for '${name}' resolved to cloud-http without an API key.`);
2966
+ }
2967
+ return {
2968
+ transport: "cloud-http",
2969
+ client: createHasnaHttpTransport({
2970
+ name,
2971
+ baseUrl: resolution.baseUrl,
2972
+ apiKey,
2973
+ ...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
2974
+ ...overrides?.headers ? { headers: overrides.headers } : {},
2975
+ ...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
2976
+ ...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
2977
+ ...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
2978
+ }),
2979
+ resolution
2980
+ };
3043
2981
  }
3044
- function projectRoot(projectDir, chatData) {
3045
- if (chatData.projectPath)
3046
- return chatData.projectPath;
3047
- if (chatData.project_path)
3048
- return chatData.project_path;
3049
- const rootFile = join6(projectDir, ".project_root");
3050
- try {
3051
- if (existsSync7(rootFile))
3052
- return readFileSync5(rootFile, "utf-8").trim();
3053
- } catch {}
3054
- return "";
3055
- }
3056
- async function ingestGemini(db, verbose) {
3057
- const tmpDir = geminiTmpDir();
3058
- const historyDir = geminiHistoryDir();
3059
- if (!existsSync7(tmpDir) && !existsSync7(historyDir)) {
3060
- if (verbose)
3061
- console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
3062
- return { sessions: 0, requests: 0 };
3063
- }
3064
- const machineId = getMachineId();
3065
- let totalSessions = 0;
3066
- let totalRequests = 0;
3067
- const touchedSessions = new Set;
3068
- const account = await resolveAccountForAgent("gemini");
3069
- const projectDirs = listProjectDirs(tmpDir, historyDir);
3070
- for (const projectDir of projectDirs) {
3071
- const chatsDir = join6(projectDir, "chats");
3072
- if (!existsSync7(chatsDir))
3073
- continue;
3074
- let chatFiles = [];
3075
- try {
3076
- chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join6(chatsDir, f));
3077
- } catch {
3078
- continue;
2982
+
2983
+ // src/lib/contracts-client/storage.ts
2984
+ function resourcePath(resource) {
2985
+ const trimmed = resource.replace(/^\/+|\/+$/g, "");
2986
+ if (!trimmed)
2987
+ throw new Error("resource must be a non-empty path segment");
2988
+ return `/${trimmed}`;
2989
+ }
2990
+ function entityPath(resource, id) {
2991
+ if (id === undefined || id === null || `${id}`.length === 0) {
2992
+ throw new Error("id must be a non-empty string");
2993
+ }
2994
+ return `${resourcePath(resource)}/${encodeURIComponent(String(id))}`;
2995
+ }
2996
+ function newIdempotencyKey() {
2997
+ const g = globalThis;
2998
+ if (g.crypto?.randomUUID)
2999
+ return g.crypto.randomUUID();
3000
+ return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
3001
+ }
3002
+ function extractItems(raw) {
3003
+ if (Array.isArray(raw))
3004
+ return raw;
3005
+ if (raw && typeof raw === "object") {
3006
+ const obj = raw;
3007
+ for (const key of ["items", "data", "results", "rows", "records"]) {
3008
+ if (Array.isArray(obj[key]))
3009
+ return obj[key];
3010
+ }
3011
+ }
3012
+ return [];
3013
+ }
3014
+ function extractTotal(raw) {
3015
+ if (raw && typeof raw === "object") {
3016
+ const obj = raw;
3017
+ for (const key of ["total", "count", "totalCount", "total_count"]) {
3018
+ if (typeof obj[key] === "number")
3019
+ return obj[key];
3079
3020
  }
3080
- for (const filePath of chatFiles) {
3081
- const stateKey = filePath.replace(homedir5(), "~");
3082
- let fileMtime = "0";
3021
+ }
3022
+ return null;
3023
+ }
3024
+ function extractCursor(raw) {
3025
+ if (raw && typeof raw === "object") {
3026
+ const obj = raw;
3027
+ for (const key of ["cursor", "nextCursor", "next_cursor", "next"]) {
3028
+ if (typeof obj[key] === "string")
3029
+ return obj[key];
3030
+ }
3031
+ }
3032
+ return null;
3033
+ }
3034
+ function createHasnaStorageClient(name, transport) {
3035
+ return {
3036
+ name,
3037
+ baseUrl: transport.baseUrl,
3038
+ transport,
3039
+ async list(resource, options = {}) {
3040
+ const raw = await transport.get(resourcePath(resource), options);
3041
+ return {
3042
+ items: extractItems(raw),
3043
+ total: extractTotal(raw),
3044
+ cursor: extractCursor(raw),
3045
+ raw
3046
+ };
3047
+ },
3048
+ async get(resource, id, options = {}) {
3083
3049
  try {
3084
- fileMtime = statSync3(filePath).mtimeMs.toString();
3085
- } catch {
3086
- continue;
3050
+ return await transport.get(entityPath(resource, id), options);
3051
+ } catch (error) {
3052
+ if (error instanceof HasnaHttpError && error.status === 404)
3053
+ return null;
3054
+ throw error;
3087
3055
  }
3088
- const processed = getIngestState(db, "gemini", stateKey);
3089
- if (processed === fileMtime)
3090
- continue;
3091
- let chatData;
3056
+ },
3057
+ async create(resource, body, options = {}) {
3058
+ const { idempotencyKey, ...rest } = options;
3059
+ return transport.post(resourcePath(resource), body, {
3060
+ ...rest,
3061
+ idempotencyKey: idempotencyKey ?? newIdempotencyKey()
3062
+ });
3063
+ },
3064
+ async update(resource, id, patch, options = {}) {
3065
+ const { method = "PATCH", idempotencyKey, ...rest } = options;
3066
+ const call = method === "PUT" ? transport.put : transport.patch;
3067
+ return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
3068
+ },
3069
+ async delete(resource, id, options = {}) {
3092
3070
  try {
3093
- chatData = JSON.parse(readFileSync5(filePath, "utf-8"));
3094
- } catch {
3095
- continue;
3096
- }
3097
- const sessionId = chatData.sessionId ?? chatData.id ?? basename3(filePath, ".json");
3098
- if (!sessionId)
3099
- continue;
3100
- const startTime = chatData.startTime ?? new Date().toISOString();
3101
- const projectPath = projectRoot(projectDir, chatData);
3102
- const projectName = projectPath ? basename3(projectPath) : "";
3103
- const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
3104
- if (!existing) {
3105
- const session = {
3106
- id: sessionId,
3107
- agent: "gemini",
3108
- project_path: projectPath,
3109
- project_name: projectName,
3110
- started_at: startTime,
3111
- ended_at: chatData.lastUpdated ?? null,
3112
- total_cost_usd: 0,
3113
- total_tokens: 0,
3114
- request_count: 0,
3115
- machine_id: machineId
3116
- };
3117
- upsertSession(db, withAccount(session, account));
3118
- totalSessions++;
3119
- }
3120
- touchedSessions.add(sessionId);
3121
- for (const [index, message] of (chatData.messages ?? []).entries()) {
3122
- const usage = message.usage ?? message.usageMetadata ?? message.response?.usageMetadata;
3123
- if (!usage)
3124
- continue;
3125
- const model = message.model ?? message.response?.modelVersion ?? message.response?.model ?? chatData.model;
3126
- if (!model)
3127
- continue;
3128
- const toolUsePromptTokens = numberField(usage.toolUsePromptTokenCount, usage.tool_use_prompt_token_count);
3129
- const inputTotal = numberField(usage.inputTokens, usage.input_tokens, usage.promptTokenCount, usage.prompt_token_count) + toolUsePromptTokens;
3130
- const cacheReadTokens = numberField(usage.cachedInputTokens, usage.cache_read_tokens, usage.cachedContentTokenCount, usage.cached_content_token_count);
3131
- const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
3132
- const thoughtsTokens = numberField(usage.thoughtsTokenCount, usage.thoughts_token_count);
3133
- const outputTokens = numberField(usage.outputTokens, usage.output_tokens, usage.candidatesTokenCount, usage.candidates_token_count) + thoughtsTokens;
3134
- const totalTokens = numberField(usage.totalTokens, usage.total_tokens, usage.totalTokenCount, usage.total_token_count);
3135
- if (inputTokens + outputTokens + cacheReadTokens + totalTokens === 0)
3136
- continue;
3137
- const computedCost = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
3138
- const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
3139
- const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
3140
- const requestId = `gemini-${sessionId}-${message.id ?? index}`;
3141
- upsertRequest(db, withAccount({
3142
- id: requestId,
3143
- agent: "gemini",
3144
- session_id: sessionId,
3145
- model,
3146
- input_tokens: inputTokens,
3147
- output_tokens: outputTokens,
3148
- cache_read_tokens: cacheReadTokens,
3149
- cache_create_tokens: 0,
3150
- cost_usd: costUsd,
3151
- cost_basis: defaultCostBasisForAgent("gemini"),
3152
- duration_ms: 0,
3153
- timestamp,
3154
- source_request_id: message.id ?? requestId,
3155
- machine_id: machineId
3156
- }, account));
3157
- totalRequests++;
3071
+ await transport.del(entityPath(resource, id), undefined, options);
3072
+ } catch (error) {
3073
+ if (error instanceof HasnaHttpError && error.status === 404)
3074
+ return;
3075
+ throw error;
3158
3076
  }
3159
- setIngestState(db, "gemini", stateKey, fileMtime);
3160
3077
  }
3078
+ };
3079
+ }
3080
+ function resolveStorageClient(name, env = process.env, overrides) {
3081
+ const wired = createClientTransport(name, env, overrides);
3082
+ if (wired.transport === "cloud-http") {
3083
+ return { transport: "cloud-http", client: createHasnaStorageClient(name, wired.client) };
3161
3084
  }
3162
- for (const sessionId of touchedSessions) {
3163
- rollupSession(db, sessionId);
3085
+ return { transport: "local", client: null };
3086
+ }
3087
+
3088
+ // src/lib/cloud-storage.ts
3089
+ var ECONOMY_APP = "economy";
3090
+ var cache;
3091
+ function resolveEconomyCloudStorage(env = process.env, overrides) {
3092
+ const resolved = resolveStorageClient(ECONOMY_APP, env, overrides);
3093
+ return resolved.transport === "cloud-http" ? { active: true, client: resolved.client } : { active: false, client: null };
3094
+ }
3095
+ function economyCloudStorage(env = process.env) {
3096
+ if (cache === undefined)
3097
+ cache = resolveEconomyCloudStorage(env);
3098
+ return cache;
3099
+ }
3100
+ function cleanQuery(query) {
3101
+ if (!query)
3102
+ return;
3103
+ const out = {};
3104
+ for (const [k, v] of Object.entries(query)) {
3105
+ if (v !== undefined && v !== null && v !== "")
3106
+ out[k] = v;
3164
3107
  }
3165
- return { sessions: totalSessions, requests: totalRequests };
3108
+ return Object.keys(out).length > 0 ? out : undefined;
3109
+ }
3110
+ async function cloudListItems(storage, resource, query) {
3111
+ const cleaned = cleanQuery(query);
3112
+ const res = await storage.client.list(resource, cleaned ? { query: cleaned } : {});
3113
+ return res.items;
3114
+ }
3115
+ async function cloudObject(storage, path, query) {
3116
+ const cleaned = cleanQuery(query);
3117
+ const raw = await storage.client.transport.get(path, cleaned ? { query: cleaned } : {});
3118
+ if (raw && typeof raw === "object" && "data" in raw) {
3119
+ return raw.data;
3120
+ }
3121
+ return raw;
3122
+ }
3123
+
3124
+ // src/lib/store/index.ts
3125
+ class LocalStore {
3126
+ transport = "local";
3127
+ _db;
3128
+ db() {
3129
+ if (!this._db) {
3130
+ this._db = openDatabase();
3131
+ ensurePricingSeeded(this._db);
3132
+ }
3133
+ return this._db;
3134
+ }
3135
+ async summary(period, machine) {
3136
+ return querySummary(this.db(), period, machine);
3137
+ }
3138
+ async sessions(filter) {
3139
+ return querySessions(this.db(), filter);
3140
+ }
3141
+ async topSessions(n, agent, since) {
3142
+ return queryTopSessions(this.db(), n, agent, since);
3143
+ }
3144
+ async sessionDetail(id) {
3145
+ return getSessionDetail(this.db(), id);
3146
+ }
3147
+ async modelBreakdown(query = {}) {
3148
+ return query.since ? queryModelBreakdownSince(this.db(), query.since) : queryModelBreakdown(this.db());
3149
+ }
3150
+ async agentBreakdown(query = {}) {
3151
+ return query.since ? queryAgentBreakdownSince(this.db(), query.since) : queryAgentBreakdown(this.db(), query.period ?? "all", query.machine);
3152
+ }
3153
+ async projectBreakdown(query = {}) {
3154
+ return query.since ? queryProjectBreakdownSince(this.db(), query.since) : queryProjectBreakdown(this.db(), query.period ?? "all", query.machine);
3155
+ }
3156
+ async accountBreakdown(query = {}) {
3157
+ return query.since ? queryAccountBreakdownSince(this.db(), query.since) : queryAccountBreakdown(this.db(), query.period ?? "all", query.machine);
3158
+ }
3159
+ async accounts(period) {
3160
+ return queryAccountBreakdown(this.db(), period);
3161
+ }
3162
+ async daily(days, machine) {
3163
+ return queryDailyBreakdown(this.db(), days, machine);
3164
+ }
3165
+ async machines() {
3166
+ return listMachines(this.db());
3167
+ }
3168
+ async fleet(period) {
3169
+ return {
3170
+ summary: querySummary(this.db(), period, undefined, true),
3171
+ machines: listMachines(this.db(), period),
3172
+ registry: listMachineRegistry(this.db())
3173
+ };
3174
+ }
3175
+ async billingSummary(period) {
3176
+ return queryBillingSummary(this.db(), period);
3177
+ }
3178
+ async billingDiff(period) {
3179
+ return queryBillingDiff(this.db(), period);
3180
+ }
3181
+ async usage(period, agent) {
3182
+ const snapshots = queryUsageSnapshots(this.db(), { agent, ...usageSnapshotFilterForPeriod(period) });
3183
+ const summary = querySummary(this.db(), period, undefined, true, agent);
3184
+ return { snapshots, summary };
3185
+ }
3186
+ async savings(period, agent) {
3187
+ return querySavingsSummary(this.db(), period, agent);
3188
+ }
3189
+ async listBudgets() {
3190
+ return getBudgetStatuses(this.db());
3191
+ }
3192
+ async listGoals() {
3193
+ return getGoalStatuses(this.db());
3194
+ }
3195
+ async listPricing() {
3196
+ return listModelPricing(this.db());
3197
+ }
3198
+ async listSubscriptions() {
3199
+ return listSubscriptions(this.db());
3200
+ }
3201
+ async listProjects() {
3202
+ return queryProjectBreakdown(this.db());
3203
+ }
3204
+ async brief(query = {}) {
3205
+ return buildBrief(this.db(), {
3206
+ since: query.since,
3207
+ machine: query.machine,
3208
+ currentMachineId: query.currentMachineId,
3209
+ localSyncAt: query.localSyncAt
3210
+ });
3211
+ }
3212
+ async projectDetail(nameOrPath) {
3213
+ return queryProjectDetail(this.db(), nameOrPath);
3214
+ }
3215
+ async exportRows(type, period) {
3216
+ return queryExportRows(this.db(), type, period);
3217
+ }
3218
+ async rangeStats(from, to) {
3219
+ return queryRangeStats(this.db(), from, to);
3220
+ }
3221
+ async forecast() {
3222
+ return queryForecast(this.db());
3223
+ }
3224
+ async efficiency() {
3225
+ return queryModelEfficiency(this.db());
3226
+ }
3227
+ async recentRequests(since) {
3228
+ return queryRequestsSince(this.db(), since);
3229
+ }
3230
+ async estimate(input) {
3231
+ return estimateCostFromRows(listModelPricing(this.db()), input.model, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, input.cacheWrite1hTokens ?? 0, input.cacheStorageTokenHours ?? 0);
3232
+ }
3233
+ async setBudget(input) {
3234
+ const now = new Date().toISOString();
3235
+ const budget = { id: randomUUID(), ...input, created_at: now, updated_at: now };
3236
+ upsertBudget(this.db(), budget);
3237
+ return budget.id;
3238
+ }
3239
+ async removeBudget(id) {
3240
+ deleteBudget(this.db(), id);
3241
+ }
3242
+ async setGoal(input) {
3243
+ const now = new Date().toISOString();
3244
+ upsertGoal(this.db(), { id: randomUUID(), ...input, created_at: now, updated_at: now });
3245
+ }
3246
+ async removeGoal(id) {
3247
+ deleteGoal(this.db(), id);
3248
+ }
3249
+ async setPricing(input) {
3250
+ upsertModelPricing(this.db(), { ...input, updated_at: new Date().toISOString() });
3251
+ }
3252
+ async removePricing(model) {
3253
+ deleteModelPricing(this.db(), model);
3254
+ }
3255
+ async setSubscription(input) {
3256
+ const now = new Date().toISOString();
3257
+ const row = {
3258
+ id: input.id ?? randomUUID(),
3259
+ provider: input.provider,
3260
+ plan: input.plan,
3261
+ agent: input.agent,
3262
+ monthly_fee_usd: input.monthly_fee_usd,
3263
+ included_usage_usd: input.included_usage_usd,
3264
+ billing_cycle_start: input.billing_cycle_start,
3265
+ reset_policy: input.reset_policy,
3266
+ active: input.active,
3267
+ created_at: now,
3268
+ updated_at: now
3269
+ };
3270
+ upsertSubscription(this.db(), row);
3271
+ return row;
3272
+ }
3273
+ async removeSubscription(id) {
3274
+ deleteSubscription(this.db(), id);
3275
+ }
3276
+ async addProject(path, name) {
3277
+ upsertProject(this.db(), {
3278
+ id: randomUUID(),
3279
+ path,
3280
+ name,
3281
+ description: null,
3282
+ tags: [],
3283
+ created_at: new Date().toISOString()
3284
+ });
3285
+ }
3286
+ async renameProject(path, name) {
3287
+ const existing = getProject(this.db(), path);
3288
+ if (!existing)
3289
+ throw new Error("Project not found");
3290
+ upsertProject(this.db(), { ...existing, name });
3291
+ }
3292
+ async removeProject(path) {
3293
+ deleteProject(this.db(), path);
3294
+ }
3295
+ }
3296
+ function q(params) {
3297
+ const out = {};
3298
+ for (const [k, v] of Object.entries(params))
3299
+ if (v !== undefined && v !== null && v !== "")
3300
+ out[k] = v;
3301
+ return out;
3302
+ }
3303
+
3304
+ class ApiStore {
3305
+ cloud;
3306
+ transport = "cloud-http";
3307
+ constructor(cloud) {
3308
+ this.cloud = cloud;
3309
+ }
3310
+ async summary(period, machine) {
3311
+ return cloudObject(this.cloud, "/summary", q({ period, machine }));
3312
+ }
3313
+ async sessions(filter) {
3314
+ return cloudListItems(this.cloud, "sessions", q({ ...filter }));
3315
+ }
3316
+ async topSessions(n, agent, since) {
3317
+ return cloudListItems(this.cloud, "top", q({ n, agent, since }));
3318
+ }
3319
+ async sessionDetail(id) {
3320
+ const matches = await cloudListItems(this.cloud, "sessions", q({ search: id, limit: 1 }));
3321
+ const session = matches[0];
3322
+ if (!session)
3323
+ return null;
3324
+ const requests = await cloudObject(this.cloud, `/sessions/${encodeURIComponent(String(session["id"]))}/requests`);
3325
+ return { session, requests: requests ?? [] };
3326
+ }
3327
+ async modelBreakdown(query = {}) {
3328
+ return cloudListItems(this.cloud, "breakdown", q({ by: "model", period: query.period, since: query.since }));
3329
+ }
3330
+ async agentBreakdown(query = {}) {
3331
+ return cloudListItems(this.cloud, "breakdown", q({ by: "agent", period: query.period, since: query.since }));
3332
+ }
3333
+ async projectBreakdown(query = {}) {
3334
+ return cloudListItems(this.cloud, "breakdown", q({ by: "project", period: query.period, since: query.since }));
3335
+ }
3336
+ async accountBreakdown(query = {}) {
3337
+ return cloudListItems(this.cloud, "breakdown", q({ by: "account", period: query.period, since: query.since }));
3338
+ }
3339
+ async accounts(period) {
3340
+ return cloudListItems(this.cloud, "accounts", q({ period }));
3341
+ }
3342
+ async daily(days, machine) {
3343
+ return cloudListItems(this.cloud, "daily", q({ days, machine }));
3344
+ }
3345
+ async machines() {
3346
+ return cloudListItems(this.cloud, "machines");
3347
+ }
3348
+ async fleet(period) {
3349
+ const data = await cloudObject(this.cloud, "/fleet", q({ period }));
3350
+ return {
3351
+ summary: data.summary,
3352
+ machines: data.machines ?? [],
3353
+ registry: data.registry ?? []
3354
+ };
3355
+ }
3356
+ async billingSummary(period) {
3357
+ return cloudObject(this.cloud, "/billing", q({ period }));
3358
+ }
3359
+ async billingDiff(period) {
3360
+ return cloudObject(this.cloud, "/billing/diff", q({ period }));
3361
+ }
3362
+ async usage(period, agent) {
3363
+ return cloudObject(this.cloud, "/usage", q({ period, agent }));
3364
+ }
3365
+ async savings(period, agent) {
3366
+ return cloudObject(this.cloud, "/savings", q({ period, agent }));
3367
+ }
3368
+ async listBudgets() {
3369
+ return cloudListItems(this.cloud, "budgets");
3370
+ }
3371
+ async listGoals() {
3372
+ return cloudListItems(this.cloud, "goals");
3373
+ }
3374
+ async listPricing() {
3375
+ return cloudListItems(this.cloud, "pricing");
3376
+ }
3377
+ async listSubscriptions() {
3378
+ return cloudListItems(this.cloud, "subscriptions");
3379
+ }
3380
+ async listProjects() {
3381
+ return cloudListItems(this.cloud, "projects");
3382
+ }
3383
+ async brief(query = {}) {
3384
+ return cloudObject(this.cloud, "/brief", q({ since: query.since, machine: query.machine }));
3385
+ }
3386
+ async projectDetail(nameOrPath) {
3387
+ return cloudObject(this.cloud, "/projects/detail", q({ q: nameOrPath }));
3388
+ }
3389
+ async exportRows(type, period) {
3390
+ return cloudListItems(this.cloud, "export", q({ type, period }));
3391
+ }
3392
+ async rangeStats(from, to) {
3393
+ return cloudObject(this.cloud, "/compare", q({ from, to }));
3394
+ }
3395
+ async forecast() {
3396
+ return cloudObject(this.cloud, "/forecast");
3397
+ }
3398
+ async efficiency() {
3399
+ return cloudListItems(this.cloud, "efficiency");
3400
+ }
3401
+ async recentRequests(since) {
3402
+ return cloudListItems(this.cloud, "requests", q({ since }));
3403
+ }
3404
+ async estimate(input) {
3405
+ const rows = await this.listPricing();
3406
+ return estimateCostFromRows(rows, input.model, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, input.cacheWrite1hTokens ?? 0, input.cacheStorageTokenHours ?? 0);
3407
+ }
3408
+ async setBudget(input) {
3409
+ const created = await this.cloud.client.create("budgets", {
3410
+ project_path: input.project_path,
3411
+ ...input.agent ? { agent: input.agent } : {},
3412
+ period: input.period,
3413
+ limit_usd: input.limit_usd,
3414
+ alert_at_percent: input.alert_at_percent
3415
+ });
3416
+ return created?.id ?? "";
3417
+ }
3418
+ async removeBudget(id) {
3419
+ await this.cloud.client.delete("budgets", id);
3420
+ }
3421
+ async setGoal(input) {
3422
+ await this.cloud.client.create("goals", {
3423
+ period: input.period,
3424
+ project_path: input.project_path,
3425
+ ...input.agent ? { agent: input.agent } : {},
3426
+ limit_usd: input.limit_usd
3427
+ });
3428
+ }
3429
+ async removeGoal(id) {
3430
+ await this.cloud.client.delete("goals", id);
3431
+ }
3432
+ async setPricing(input) {
3433
+ await this.cloud.client.create("pricing", { ...input, updated_at: new Date().toISOString() });
3434
+ }
3435
+ async removePricing(model) {
3436
+ await this.cloud.client.delete("pricing", model);
3437
+ }
3438
+ async setSubscription(input) {
3439
+ const now = new Date().toISOString();
3440
+ const row = {
3441
+ id: input.id ?? randomUUID(),
3442
+ provider: input.provider,
3443
+ plan: input.plan,
3444
+ agent: input.agent,
3445
+ monthly_fee_usd: input.monthly_fee_usd,
3446
+ included_usage_usd: input.included_usage_usd,
3447
+ billing_cycle_start: input.billing_cycle_start,
3448
+ reset_policy: input.reset_policy,
3449
+ active: input.active,
3450
+ created_at: now,
3451
+ updated_at: now
3452
+ };
3453
+ await this.cloud.client.create("subscriptions", row);
3454
+ return row;
3455
+ }
3456
+ async removeSubscription(id) {
3457
+ await this.cloud.client.delete("subscriptions", id);
3458
+ }
3459
+ async addProject(path, name) {
3460
+ await this.cloud.client.create("project-registry", { path, name, description: null, tags: [] });
3461
+ }
3462
+ async renameProject(path, name) {
3463
+ await this.cloud.client.delete("project-registry", path);
3464
+ await this.cloud.client.create("project-registry", { path, name, description: null, tags: [] });
3465
+ }
3466
+ async removeProject(path) {
3467
+ await this.cloud.client.delete("project-registry", path);
3468
+ }
3469
+ }
3470
+ function getStore(env = process.env) {
3471
+ const cloud = economyCloudStorage(env);
3472
+ return cloud.active ? new ApiStore(cloud) : new LocalStore;
3473
+ }
3474
+ function isCloudStore(env = process.env) {
3475
+ return economyCloudStorage(env).active;
3166
3476
  }
3167
3477
  export {
3168
- writeFleetSyncReport,
3169
- verifySqliteIntegrity,
3170
- upsertUsageSnapshot,
3171
- upsertSubscription,
3172
- upsertSession,
3173
- upsertRequest,
3174
- upsertProject,
3175
- upsertModelPricing,
3176
- upsertGoal,
3177
- upsertBudget,
3178
- upsertBillingDaily,
3179
- syncOpenProjectsRegistry,
3180
- setIngestState,
3181
- setActiveModel,
3182
- seedModelPricing,
3183
- runEconomyFleetSync,
3184
- rollupSession,
3185
- readCodexModel,
3186
- queryZeroCostTokenizedModels,
3187
- queryUsageSnapshots,
3188
- queryTopSessions,
3189
- querySummary,
3190
- querySessions,
3191
- queryRequestsSince,
3192
- queryProjectBreakdown,
3193
- queryModelBreakdown,
3194
- queryHourlyBreakdown,
3195
- queryDailyBreakdown,
3196
- queryBillingSummary,
3197
- queryAgentBreakdown,
3198
- queryAccountBreakdown,
3199
- openDatabase,
3200
- normalizeModelName,
3201
- mergePeerDatabase,
3202
- listSubscriptions,
3203
- listProjects,
3204
- listModelPricing,
3205
- listMachines,
3206
- listMachineRegistry,
3207
- listGoals,
3208
- listBudgets,
3209
- isSafeRemoteSnapshotPath,
3478
+ isCloudStore,
3210
3479
  isAgent,
3211
- ingestTakumi,
3212
- ingestJsonlProjects,
3213
- ingestGemini,
3214
- ingestCodex,
3215
- ingestClaude,
3216
- getProject,
3217
- getPricingFromDb,
3218
- getPricing,
3219
- getModelPricing,
3220
- getMachineId,
3221
- getIngestState,
3222
- getGoalStatuses,
3223
- getDbPath,
3224
- getDataDir,
3225
- getBudgetStatuses,
3226
- getActiveModel,
3227
- gatherTrainingData,
3228
- ensurePricingSeeded,
3229
- deleteSubscription,
3230
- deleteProject,
3231
- deleteModelPricing,
3232
- deleteGoal,
3233
- deleteBudget,
3234
- dedupeRequests,
3235
- computeCostFromDb,
3236
- computeCost,
3237
- clearBillingRange,
3238
- clearActiveModel,
3239
- DEFAULT_PRICING,
3240
- DEFAULT_MODEL,
3480
+ getStore,
3481
+ LocalStore,
3241
3482
  COST_BASIS,
3483
+ ApiStore,
3242
3484
  AGENTS
3243
3485
  };