@hasna/economy 0.3.7 → 0.3.8

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 (77) hide show
  1. package/CHANGELOG.md +0 -4
  2. package/README.md +28 -61
  3. package/dashboard/README.md +17 -64
  4. package/dist/cli/index.js +221 -114
  5. package/dist/db/cloud.d.ts +3 -4
  6. package/dist/db/cloud.d.ts.map +1 -1
  7. package/dist/db/database.d.ts +2 -2
  8. package/dist/db/database.d.ts.map +1 -1
  9. package/dist/db/dialect.d.ts +13 -0
  10. package/dist/db/dialect.d.ts.map +1 -0
  11. package/dist/db/pg-migrate.d.ts +30 -0
  12. package/dist/db/pg-migrate.d.ts.map +1 -0
  13. package/dist/db/sqlite-adapter.d.ts +36 -0
  14. package/dist/db/sqlite-adapter.d.ts.map +1 -0
  15. package/dist/db/sync-pg.d.ts +1 -1
  16. package/dist/db/sync-pg.d.ts.map +1 -1
  17. package/dist/index.js +129 -56
  18. package/dist/ingest/billing.d.ts +1 -1
  19. package/dist/ingest/billing.d.ts.map +1 -1
  20. package/dist/ingest/claude-quota.d.ts +1 -1
  21. package/dist/ingest/claude-quota.d.ts.map +1 -1
  22. package/dist/ingest/claude.d.ts +1 -1
  23. package/dist/ingest/claude.d.ts.map +1 -1
  24. package/dist/ingest/codex-quota.d.ts +1 -1
  25. package/dist/ingest/codex-quota.d.ts.map +1 -1
  26. package/dist/ingest/codex.d.ts +1 -1
  27. package/dist/ingest/codex.d.ts.map +1 -1
  28. package/dist/ingest/cursor.d.ts +1 -1
  29. package/dist/ingest/cursor.d.ts.map +1 -1
  30. package/dist/ingest/gemini.d.ts +1 -1
  31. package/dist/ingest/gemini.d.ts.map +1 -1
  32. package/dist/ingest/hermes.d.ts +1 -1
  33. package/dist/ingest/hermes.d.ts.map +1 -1
  34. package/dist/ingest/loops.d.ts +1 -1
  35. package/dist/ingest/loops.d.ts.map +1 -1
  36. package/dist/ingest/opencode.d.ts +1 -1
  37. package/dist/ingest/opencode.d.ts.map +1 -1
  38. package/dist/ingest/otel.d.ts +1 -1
  39. package/dist/ingest/otel.d.ts.map +1 -1
  40. package/dist/ingest/pi.d.ts +1 -1
  41. package/dist/ingest/pi.d.ts.map +1 -1
  42. package/dist/ingest/plugin.d.ts +1 -1
  43. package/dist/ingest/plugin.d.ts.map +1 -1
  44. package/dist/lib/accounts.d.ts.map +1 -1
  45. package/dist/lib/analytics.d.ts +1 -1
  46. package/dist/lib/analytics.d.ts.map +1 -1
  47. package/dist/lib/billing-diff.d.ts +1 -1
  48. package/dist/lib/billing-diff.d.ts.map +1 -1
  49. package/dist/lib/brief.d.ts +1 -1
  50. package/dist/lib/brief.d.ts.map +1 -1
  51. package/dist/lib/open-projects.d.ts +10 -2
  52. package/dist/lib/open-projects.d.ts.map +1 -1
  53. package/dist/lib/pricing.d.ts +1 -1
  54. package/dist/lib/pricing.d.ts.map +1 -1
  55. package/dist/lib/savings.d.ts +1 -1
  56. package/dist/lib/savings.d.ts.map +1 -1
  57. package/dist/lib/spikes.d.ts +1 -1
  58. package/dist/lib/spikes.d.ts.map +1 -1
  59. package/dist/lib/sync-all.d.ts +1 -1
  60. package/dist/lib/sync-all.d.ts.map +1 -1
  61. package/dist/lib/webhooks.d.ts +1 -1
  62. package/dist/lib/webhooks.d.ts.map +1 -1
  63. package/dist/mcp/index.js +144 -94
  64. package/dist/mcp/server.d.ts.map +1 -1
  65. package/dist/openapi.d.ts.map +1 -1
  66. package/dist/otel/index.js +69 -4
  67. package/dist/server/index.js +248 -88
  68. package/dist/server/serve.d.ts +1 -1
  69. package/dist/server/serve.d.ts.map +1 -1
  70. package/docs/README.md +12 -0
  71. package/docs/cli.md +88 -0
  72. package/docs/configuration.md +86 -0
  73. package/docs/ingestion.md +47 -0
  74. package/docs/mcp.md +65 -0
  75. package/docs/otel.md +53 -0
  76. package/docs/rest-api.md +89 -0
  77. package/package.json +4 -5
package/dist/cli/index.js CHANGED
@@ -17,6 +17,62 @@ var __export = (target, all) => {
17
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
18
  var __require = import.meta.require;
19
19
 
20
+ // src/db/sqlite-adapter.ts
21
+ import { Database } from "bun:sqlite";
22
+
23
+ class SqliteAdapter {
24
+ db;
25
+ constructor(path) {
26
+ this.db = new Database(path, { create: true });
27
+ this.db.exec("PRAGMA journal_mode=WAL");
28
+ this.db.exec("PRAGMA foreign_keys=ON");
29
+ }
30
+ run(sql, ...params) {
31
+ const result = this.db.prepare(sql).run(...params);
32
+ return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
33
+ }
34
+ get(sql, ...params) {
35
+ return this.db.prepare(sql).get(...params);
36
+ }
37
+ all(sql, ...params) {
38
+ return this.db.prepare(sql).all(...params);
39
+ }
40
+ exec(sql) {
41
+ this.db.exec(sql);
42
+ }
43
+ query(sql) {
44
+ return this.db.query(sql);
45
+ }
46
+ prepare(sql) {
47
+ const stmt = this.db.prepare(sql);
48
+ return {
49
+ run(...params) {
50
+ const r = stmt.run(...params);
51
+ return { changes: r.changes, lastInsertRowid: r.lastInsertRowid };
52
+ },
53
+ get(...params) {
54
+ return stmt.get(...params);
55
+ },
56
+ all(...params) {
57
+ return stmt.all(...params);
58
+ },
59
+ finalize() {
60
+ stmt.finalize();
61
+ }
62
+ };
63
+ }
64
+ close() {
65
+ this.db.close();
66
+ }
67
+ transaction(fn) {
68
+ return this.db.transaction(fn)();
69
+ }
70
+ get raw() {
71
+ return this.db;
72
+ }
73
+ }
74
+ var init_sqlite_adapter = () => {};
75
+
20
76
  // src/lib/pricing.ts
21
77
  var exports_pricing = {};
22
78
  __export(exports_pricing, {
@@ -596,7 +652,6 @@ __export(exports_database, {
596
652
  clearBillingRange: () => clearBillingRange,
597
653
  bulkIngest: () => bulkIngest
598
654
  });
599
- import { SqliteAdapter as Database } from "@hasna/cloud";
600
655
  import { execFileSync } from "child_process";
601
656
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
602
657
  import { hostname, platform } from "os";
@@ -689,7 +744,7 @@ function openDatabase(dbPath, skipSeed = false) {
689
744
  mkdirSync(dir, { recursive: true });
690
745
  }
691
746
  const db = withSqliteBusyRetry(() => {
692
- const opened = new Database(path);
747
+ const opened = new SqliteAdapter(path);
693
748
  try {
694
749
  opened.exec("PRAGMA busy_timeout = 10000");
695
750
  opened.exec("PRAGMA journal_mode = WAL");
@@ -1295,9 +1350,12 @@ function labelForPath(projectPath, projectName) {
1295
1350
  function groupKeyForPath(projectPath, projectName) {
1296
1351
  return labelForPath(projectPath, projectName).trim().toLowerCase();
1297
1352
  }
1298
- function queryProjectBreakdown(db, period = "all", machine) {
1299
- const requestWhere = requestPeriodWhere(period);
1300
- const sessionWhere = sessionPeriodWhere(period);
1353
+ function laterTimestamp(current, candidate) {
1354
+ if (!candidate)
1355
+ return current;
1356
+ return candidate > current ? candidate : current;
1357
+ }
1358
+ function queryProjectBreakdownScoped(db, requestWhere, sessionWhere, requestScopeParams = [], sessionScopeParams = [], machine) {
1301
1359
  const sessionMachineClause = machine ? " AND (machine_id = ? OR id IN (SELECT DISTINCT session_id FROM requests WHERE machine_id = ?))" : "";
1302
1360
  const requestMachineClause = machine ? " AND machine_id = ?" : "";
1303
1361
  const sessionMachineParams = machine ? [machine, machine] : [];
@@ -1305,67 +1363,89 @@ function queryProjectBreakdown(db, period = "all", machine) {
1305
1363
  const sessionOnlyMachineClause = machine ? " AND machine_id = ?" : "";
1306
1364
  const sessionOnlyMachineParams = machine ? [machine] : [];
1307
1365
  const sessions = db.prepare(`
1308
- SELECT id, project_path, project_name, total_cost_usd, started_at
1366
+ SELECT id, project_path, project_name
1309
1367
  FROM sessions
1310
1368
  WHERE (project_path != '' OR project_name != '')${sessionMachineClause}
1311
1369
  `).all(...sessionMachineParams);
1312
1370
  const groups = new Map;
1371
+ const groupKeyBySession = new Map;
1313
1372
  for (const s of sessions) {
1314
1373
  const label = labelForPath(s.project_path, s.project_name);
1315
1374
  if (!label)
1316
1375
  continue;
1317
1376
  const key = groupKeyForPath(s.project_path, s.project_name);
1318
- const g = groups.get(key) ?? { label, sessionIds: [], samplePath: s.project_path };
1319
- g.sessionIds.push(s.id);
1377
+ const g = groups.get(key) ?? { label, samplePath: s.project_path, sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: "" };
1320
1378
  if (!g.samplePath)
1321
1379
  g.samplePath = s.project_path;
1322
1380
  groups.set(key, g);
1381
+ groupKeyBySession.set(s.id, key);
1382
+ }
1383
+ const groupFor = (sessionId) => {
1384
+ const key = groupKeyBySession.get(sessionId);
1385
+ return key === undefined ? undefined : groups.get(key);
1386
+ };
1387
+ const requestRows = db.prepare(`
1388
+ SELECT session_id,
1389
+ COUNT(*) as requests,
1390
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
1391
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1392
+ MAX(timestamp) as last_active
1393
+ FROM requests
1394
+ WHERE ${requestWhere}${requestMachineClause}
1395
+ GROUP BY session_id
1396
+ `).all(...requestScopeParams, ...requestMachineParams);
1397
+ for (const row of requestRows) {
1398
+ const g = groupFor(row.session_id);
1399
+ if (!g)
1400
+ continue;
1401
+ g.sessions += 1;
1402
+ g.requests += row.requests;
1403
+ g.cost_usd += row.cost_usd;
1404
+ g.total_tokens += row.total_tokens;
1405
+ g.last_active = laterTimestamp(g.last_active, row.last_active);
1406
+ }
1407
+ const sessionsWithRequests = new Set(db.prepare(`SELECT DISTINCT session_id FROM requests`).all().map((row) => row.session_id));
1408
+ const sessionOnlyRows = db.prepare(`
1409
+ SELECT id,
1410
+ COALESCE(request_count, 0) as request_count,
1411
+ COALESCE(total_tokens, 0) as total_tokens,
1412
+ COALESCE(total_cost_usd, 0) as total_cost_usd,
1413
+ started_at
1414
+ FROM sessions
1415
+ WHERE ${sessionWhere}${sessionOnlyMachineClause}
1416
+ `).all(...sessionScopeParams, ...sessionOnlyMachineParams);
1417
+ for (const row of sessionOnlyRows) {
1418
+ if (sessionsWithRequests.has(row.id))
1419
+ continue;
1420
+ const g = groupFor(row.id);
1421
+ if (!g)
1422
+ continue;
1423
+ g.sessions += 1;
1424
+ g.requests += row.request_count;
1425
+ g.total_tokens += row.total_tokens;
1426
+ g.cost_usd += row.total_cost_usd;
1427
+ g.last_active = laterTimestamp(g.last_active, row.started_at);
1323
1428
  }
1324
1429
  const result = [];
1325
1430
  for (const g of groups.values()) {
1326
- const placeholders = g.sessionIds.map(() => "?").join(",");
1327
- const reqStats = placeholders.length ? db.prepare(`
1328
- SELECT
1329
- COUNT(DISTINCT session_id) as sessions,
1330
- COUNT(*) as requests,
1331
- COALESCE(SUM(cost_usd), 0) as cost_usd,
1332
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1333
- MAX(timestamp) as last_active
1334
- FROM requests
1335
- WHERE session_id IN (${placeholders})
1336
- AND ${requestWhere}
1337
- ${requestMachineClause}
1338
- `).get(...g.sessionIds, ...requestMachineParams) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
1339
- const sessionOnlyStats = placeholders.length ? db.prepare(`
1340
- SELECT
1341
- COUNT(*) as sessions,
1342
- COALESCE(SUM(request_count), 0) as requests,
1343
- COALESCE(SUM(total_tokens), 0) as total_tokens,
1344
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1345
- MAX(started_at) as last_active
1346
- FROM sessions
1347
- WHERE id IN (${placeholders})
1348
- AND ${sessionWhere}
1349
- ${sessionOnlyMachineClause}
1350
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1351
- `).get(...g.sessionIds, ...sessionOnlyMachineParams) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
1352
- const totalSessions = reqStats.sessions + sessionOnlyStats.sessions;
1353
- if (totalSessions === 0)
1431
+ if (g.sessions === 0)
1354
1432
  continue;
1355
- const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
1356
1433
  result.push({
1357
1434
  project_path: g.samplePath,
1358
1435
  project_name: g.label,
1359
- sessions: totalSessions,
1360
- requests: reqStats.requests + sessionOnlyStats.requests,
1361
- total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
1362
- cost_usd: reqStats.cost_usd + sessionOnlyStats.cost_usd,
1363
- last_active: lastActive
1436
+ sessions: g.sessions,
1437
+ requests: g.requests,
1438
+ total_tokens: g.total_tokens,
1439
+ cost_usd: g.cost_usd,
1440
+ last_active: g.last_active
1364
1441
  });
1365
1442
  }
1366
1443
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1367
1444
  return result;
1368
1445
  }
1446
+ function queryProjectBreakdown(db, period = "all", machine) {
1447
+ return queryProjectBreakdownScoped(db, requestPeriodWhere(period), sessionPeriodWhere(period), [], [], machine);
1448
+ }
1369
1449
  function normalizeAccountEmail(email) {
1370
1450
  return (email ?? "").trim().toLowerCase();
1371
1451
  }
@@ -1639,17 +1719,8 @@ function queryCostCenterBreakdown(db, period = "all", filter = {}) {
1639
1719
  filtered.sort((a, b) => b.cost_usd - a.cost_usd);
1640
1720
  return filtered;
1641
1721
  }
1642
- function queryProjectBreakdownSince(db, since) {
1643
- return db.prepare(`
1644
- SELECT project_path, project_name,
1645
- COUNT(*) as sessions,
1646
- COALESCE(SUM(total_tokens), 0) as total_tokens,
1647
- COALESCE(SUM(request_count), 0) as requests,
1648
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1649
- MAX(started_at) as last_active
1650
- FROM sessions WHERE started_at >= ?
1651
- GROUP BY project_path ORDER BY cost_usd DESC
1652
- `).all(since);
1722
+ function queryProjectBreakdownSince(db, since, machine) {
1723
+ return queryProjectBreakdownScoped(db, "timestamp >= ?", "started_at >= ?", [since], [since], machine);
1653
1724
  }
1654
1725
  function queryAgentBreakdownSince(db, since) {
1655
1726
  return db.prepare(`
@@ -2353,7 +2424,9 @@ var numOr = (v, d = 0) => {
2353
2424
  const num = Number(v);
2354
2425
  return Number.isFinite(num) ? num : d;
2355
2426
  }, strOr = (v, d = "") => v == null ? d : String(v);
2356
- var init_database = () => {};
2427
+ var init_database = __esm(() => {
2428
+ init_sqlite_adapter();
2429
+ });
2357
2430
 
2358
2431
  // src/lib/package-metadata.ts
2359
2432
  import { readFileSync } from "fs";
@@ -3833,7 +3906,7 @@ class LocalStore {
3833
3906
  return query.since ? queryAgentBreakdownSince(this.db(), query.since) : queryAgentBreakdown(this.db(), query.period ?? "all", query.machine);
3834
3907
  }
3835
3908
  async projectBreakdown(query = {}) {
3836
- return query.since ? queryProjectBreakdownSince(this.db(), query.since) : queryProjectBreakdown(this.db(), query.period ?? "all", query.machine);
3909
+ return query.since ? queryProjectBreakdownSince(this.db(), query.since, query.machine) : queryProjectBreakdown(this.db(), query.period ?? "all", query.machine);
3837
3910
  }
3838
3911
  async accountBreakdown(query = {}) {
3839
3912
  return query.since ? queryAccountBreakdownSince(this.db(), query.since) : queryAccountBreakdown(this.db(), query.period ?? "all", query.machine);
@@ -4029,7 +4102,7 @@ class ApiStore {
4029
4102
  return cloudListItems(this.cloud, "breakdown", q({ by: "agent", period: query.period, since: query.since }));
4030
4103
  }
4031
4104
  async projectBreakdown(query = {}) {
4032
- return cloudListItems(this.cloud, "breakdown", q({ by: "project", period: query.period, since: query.since }));
4105
+ return cloudListItems(this.cloud, "breakdown", q({ by: "project", period: query.period, since: query.since, machine: query.machine }));
4033
4106
  }
4034
4107
  async accountBreakdown(query = {}) {
4035
4108
  return cloudListItems(this.cloud, "breakdown", q({ by: "account", period: query.period, since: query.since }));
@@ -4393,6 +4466,7 @@ function agentPaths() {
4393
4466
  var init_paths = () => {};
4394
4467
 
4395
4468
  // src/lib/accounts.ts
4469
+ import { appliedProfileName, resolveStore } from "@hasna/accounts";
4396
4470
  function normalizeEmail(email) {
4397
4471
  return (email ?? "").trim().toLowerCase();
4398
4472
  }
@@ -4447,57 +4521,35 @@ function envOverride(agent, env) {
4447
4521
  account_source: "override"
4448
4522
  };
4449
4523
  }
4450
- function knownToolIds(api) {
4451
- try {
4452
- return new Set(api.listTools().map((tool) => tool.id));
4453
- } catch {
4454
- return new Set;
4455
- }
4456
- }
4457
- function profileForEnvDir(api, tool, env) {
4524
+ async function profileForEnvDir(store, tool, env) {
4458
4525
  const configuredDir = env[tool.envVar];
4459
4526
  if (!configuredDir)
4460
4527
  return null;
4461
4528
  const normalized = normalizeDir(configuredDir);
4462
- try {
4463
- return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
4464
- } catch {
4465
- return null;
4466
- }
4529
+ return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
4467
4530
  }
4468
4531
  async function resolveAccountForAgent(agent, env = process.env) {
4469
4532
  const override = envOverride(agent, env);
4470
4533
  if (override)
4471
4534
  return override;
4472
- let api;
4473
- try {
4474
- api = await import("@hasna/accounts");
4475
- } catch {
4476
- return null;
4477
- }
4478
- const toolIds = knownToolIds(api);
4535
+ const store = resolveStore(env);
4536
+ const tools = new Map((await store.listTools()).map((tool) => [tool.id, tool]));
4479
4537
  for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
4480
- if (!toolIds.has(toolId))
4481
- continue;
4482
- let tool;
4483
- try {
4484
- tool = api.getTool(toolId);
4485
- } catch {
4538
+ const tool = tools.get(toolId);
4539
+ if (!tool)
4486
4540
  continue;
4487
- }
4488
- const envProfile = profileForEnvDir(api, tool, env);
4541
+ const envProfile = await profileForEnvDir(store, tool, env);
4489
4542
  if (envProfile)
4490
4543
  return fromProfile(envProfile, "env");
4491
- try {
4492
- const applied = api.appliedProfile(toolId);
4544
+ const appliedName = appliedProfileName(toolId);
4545
+ if (appliedName) {
4546
+ const applied = await store.findProfile(appliedName, toolId);
4493
4547
  if (applied)
4494
4548
  return fromProfile(applied, "applied");
4495
- } catch {}
4496
- try {
4497
- const current = api.currentProfile(toolId);
4498
- if (current)
4499
- return fromProfile(current, "current");
4500
- } catch {}
4549
+ }
4550
+ const current = await store.currentProfile(toolId);
4551
+ if (current)
4552
+ return fromProfile(current, "current");
4501
4553
  }
4502
4554
  return null;
4503
4555
  }
@@ -6363,13 +6415,14 @@ async function syncOpenProjectsRegistry(db, listActiveProjects) {
6363
6415
  let imported = 0;
6364
6416
  let skipped = 0;
6365
6417
  for (const project of projects) {
6366
- if (!project.path) {
6418
+ const path = resolveProjectPath(project);
6419
+ if (!path) {
6367
6420
  skipped++;
6368
6421
  continue;
6369
6422
  }
6370
6423
  upsertProject(db, {
6371
6424
  id: project.id,
6372
- path: project.path,
6425
+ path,
6373
6426
  name: project.name,
6374
6427
  description: project.description,
6375
6428
  tags: project.tags ?? [],
@@ -6379,6 +6432,7 @@ async function syncOpenProjectsRegistry(db, listActiveProjects) {
6379
6432
  }
6380
6433
  return { imported, skipped };
6381
6434
  }
6435
+ var resolveProjectPath = (project) => project.primary_path || project.path || "";
6382
6436
  var init_open_projects = __esm(() => {
6383
6437
  init_database();
6384
6438
  });
@@ -6743,12 +6797,48 @@ var init_watch = __esm(() => {
6743
6797
  init_notification();
6744
6798
  });
6745
6799
 
6800
+ // src/db/dialect.ts
6801
+ function translateSql(sql, dialect) {
6802
+ if (dialect === "sqlite")
6803
+ return sql;
6804
+ return sqliteToPostgres(sql);
6805
+ }
6806
+ function translateParams(params) {
6807
+ const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
6808
+ return flat.map((p) => p === undefined ? null : p);
6809
+ }
6810
+ function sqliteToPostgres(sql) {
6811
+ let out = sql;
6812
+ let paramIdx = 0;
6813
+ out = out.replace(/\?/g, () => `$${++paramIdx}`);
6814
+ out = out.replace(/datetime\s*\(\s*'now'\s*\)/gi, "NOW()");
6815
+ out = out.replace(/datetime\s*\(\s*'now'\s*,\s*'(-?\d+)\s+(minutes?|hours?|days?|seconds?)'\s*\)/gi, (_match, amount, unit) => {
6816
+ const n = parseInt(amount, 10);
6817
+ const absN = Math.abs(n);
6818
+ const normalizedUnit = unit.toLowerCase().replace(/s$/, "");
6819
+ const plural = absN === 1 ? normalizedUnit : `${normalizedUnit}s`;
6820
+ return n < 0 ? `NOW() - INTERVAL '${absN} ${plural}'` : `NOW() + INTERVAL '${absN} ${plural}'`;
6821
+ });
6822
+ out = out.replace(/lower\s*\(\s*hex\s*\(\s*randomblob\s*\(\s*\d+\s*\)\s*\)\s*\)/gi, "gen_random_uuid()::text");
6823
+ out = out.replace(/GROUP_CONCAT\s*\(/gi, "STRING_AGG(");
6824
+ out = out.replace(/json_extract\s*\(\s*(\w+)\s*,\s*'\$\.(\w+)'\s*\)/gi, (_match, col, key) => `${col}->>'${key}'`);
6825
+ out = out.replace(/\bAUTOINCREMENT\b/gi, "GENERATED ALWAYS AS IDENTITY");
6826
+ out = out.replace(/(?<![I])LIKE/gi, "ILIKE");
6827
+ out = out.replace(/\bIFNULL\s*\(/gi, "COALESCE(");
6828
+ out = out.replace(/INSERT\s+OR\s+REPLACE\s+INTO\s+"?(\w+)"?\s*\(([^)]+)\)\s*VALUES/gi, (_match, table2, colList) => `INSERT INTO "${table2}" (${colList}) VALUES`);
6829
+ out = out.replace(/INSERT\s+OR\s+REPLACE\s+INTO/gi, "INSERT INTO");
6830
+ if (/INSERT\s+OR\s+IGNORE\s+INTO/i.test(out)) {
6831
+ out = out.replace(/INSERT\s+OR\s+IGNORE\s+INTO/gi, "INSERT INTO");
6832
+ out = out.replace(/;?\s*$/, " ON CONFLICT DO NOTHING");
6833
+ }
6834
+ return out;
6835
+ }
6836
+
6746
6837
  // src/db/sync-pg.ts
6747
6838
  import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
6748
6839
  import { fileURLToPath } from "url";
6749
6840
  import { dirname as dirname3, join as join14 } from "path";
6750
6841
  import { existsSync as existsSync15 } from "fs";
6751
- import { translateSql, translateParams } from "@hasna/cloud";
6752
6842
  function translateSqliteDates(sql) {
6753
6843
  let s = sql;
6754
6844
  const isoDate = (expr) => `to_char(${expr}, 'YYYY-MM-DD')`;
@@ -6870,11 +6960,18 @@ class SyncPgAdapter {
6870
6960
  }
6871
6961
  var init_sync_pg = () => {};
6872
6962
 
6963
+ // src/db/pg-migrate.ts
6964
+ import pg from "pg";
6965
+ function resolvePgSsl(dsn) {
6966
+ return dsn.includes("sslmode=require") || dsn.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
6967
+ }
6968
+ var init_pg_migrate = () => {};
6969
+
6873
6970
  // src/db/pg-migrations.ts
6874
6971
  var init_pg_migrations = () => {};
6875
6972
 
6876
6973
  // src/db/cloud.ts
6877
- import pg from "pg";
6974
+ import pg2 from "pg";
6878
6975
  function getCloudDatabaseUrl() {
6879
6976
  return process.env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || process.env["ECONOMY_DATABASE_URL"]?.trim() || process.env["DATABASE_URL"]?.trim() || undefined;
6880
6977
  }
@@ -6898,8 +6995,7 @@ function openCloudDatabase(dsn = getCloudDatabaseUrl()) {
6898
6995
  function createCloudPool(dsn = getCloudDatabaseUrl()) {
6899
6996
  if (!dsn)
6900
6997
  throw new Error("economy cloud mode requires a Postgres DSN (HASNA_ECONOMY_DATABASE_URL)");
6901
- const ssl = dsn.includes("sslmode=require") || dsn.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
6902
- const pool = new pg.Pool({ connectionString: dsn, ssl, max: Number(process.env["ECONOMY_PG_POOL_MAX"] ?? "5"), connectionTimeoutMillis: 1e4 });
6998
+ const pool = new pg2.Pool({ connectionString: dsn, ssl: resolvePgSsl(dsn), max: Number(process.env["ECONOMY_PG_POOL_MAX"] ?? "5"), connectionTimeoutMillis: 1e4 });
6903
6999
  pool.on("error", (err) => {
6904
7000
  console.error(JSON.stringify({ evt: "pg_pool_error", message: err.message }));
6905
7001
  });
@@ -6922,6 +7018,7 @@ function authClientFromPool(pool) {
6922
7018
  }
6923
7019
  var init_cloud = __esm(() => {
6924
7020
  init_sync_pg();
7021
+ init_pg_migrate();
6925
7022
  init_pg_migrations();
6926
7023
  });
6927
7024
 
@@ -7690,6 +7787,15 @@ var init_openapi = __esm(() => {
7690
7787
  type: "string"
7691
7788
  }
7692
7789
  },
7790
+ {
7791
+ name: "since",
7792
+ in: "query",
7793
+ description: "ISO timestamp lower bound; takes precedence over period",
7794
+ schema: {
7795
+ type: "string",
7796
+ format: "date-time"
7797
+ }
7798
+ },
7693
7799
  {
7694
7800
  name: "machine",
7695
7801
  in: "query",
@@ -8783,9 +8889,10 @@ function createHandler(db, options = {}) {
8783
8889
  if (path === "/api/breakdown" && method === "GET") {
8784
8890
  const by = url.searchParams.get("by") ?? "model";
8785
8891
  const period = url.searchParams.get("period") ?? "all";
8892
+ const since = url.searchParams.get("since") ?? undefined;
8786
8893
  const machine = url.searchParams.get("machine") ?? undefined;
8787
8894
  if (by === "project")
8788
- return ok(queryProjectBreakdown(db, period, machine));
8895
+ return ok(since ? queryProjectBreakdownSince(db, since, machine) : queryProjectBreakdown(db, period, machine));
8789
8896
  if (by === "agent")
8790
8897
  return ok(queryAgentBreakdown(db, period, machine));
8791
8898
  if (by === "account")
@@ -10038,7 +10145,7 @@ var ROADMAP_PHASES = [
10038
10145
  {
10039
10146
  id: "phase-9",
10040
10147
  title: "Multi-machine auto sync (4-machine fleet)",
10041
- summary: "Investigation: machine_id + listMachines + manual cloud push/pull exist; @hasna/cloud has incremental sync, conflict resolution, and launchd/systemd schedulers \u2014 but economy does not use them, requests/sessions lack updated_at, ingest is local-only, RDS is hardcoded, and default commands never pull before query.",
10148
+ summary: "Investigation: machine_id + listMachines + manual cloud push/pull exist; the (since-retired) shared sync package had incremental sync, conflict resolution, and launchd/systemd schedulers \u2014 but economy does not use them, requests/sessions lack updated_at, ingest is local-only, RDS is hardcoded, and default commands never pull before query.",
10042
10149
  tasks: [
10043
10150
  {
10044
10151
  id: "9.1",
@@ -10053,7 +10160,7 @@ var ROADMAP_PHASES = [
10053
10160
  },
10054
10161
  {
10055
10162
  id: "9.3",
10056
- title: "Switch cloud sync to @hasna/cloud incrementalSyncPush/Pull with _sync_meta (replace full table scans)",
10163
+ title: "Switch cloud sync to incrementalSyncPush/Pull with _sync_meta (replace full table scans)",
10057
10164
  status: "done",
10058
10165
  deps: ["9.2"]
10059
10166
  },
@@ -10077,7 +10184,7 @@ var ROADMAP_PHASES = [
10077
10184
  },
10078
10185
  {
10079
10186
  id: "9.7",
10080
- title: "CLI economy cloud schedule install|status|remove \u2014 wrap @hasna/cloud registerSyncSchedule (launchd/systemd every 5\u201315m)",
10187
+ title: "CLI economy cloud schedule install|status|remove \u2014 wrap registerSyncSchedule (launchd/systemd every 5\u201315m)",
10081
10188
  status: "done",
10082
10189
  deps: ["9.5"]
10083
10190
  },
@@ -10397,10 +10504,10 @@ function printTodosHelp() {
10397
10504
  console.log(" economy todos list --phase <id> Tasks for one phase (e.g. phase-9)");
10398
10505
  console.log(" economy todos show <task-id> Task detail (e.g. 9.7)");
10399
10506
  console.log();
10400
- console.log(chalk2.bold(" Multi-machine (current vs target)"));
10401
- console.log(" today machine_id on rows, economy machines, manual cloud push/pull/sync");
10402
- console.log(" gap no auto-sync, no updated_at, hardcoded RDS, queries are local-only");
10403
- console.log(" target 4 machines \u2192 cloud PG hub \u2192 auto push/pull every 5\u201315m \u2192 fleet today");
10507
+ console.log(chalk2.bold(" Multi-machine storage"));
10508
+ console.log(" local on-box SQLite; economy sync ingests this machine's agent data");
10509
+ console.log(" cloud CLI/MCP reads and writes the shared /v1 API directly with URL + key");
10510
+ console.log(" fleet machine_id keeps shared summaries and per-machine filters distinct");
10404
10511
  console.log();
10405
10512
  console.log(chalk2.bold(" Agents covered"));
10406
10513
  console.log(" claude Claude Code telemetry + Pro/Max quota (partial)");
@@ -10765,7 +10872,7 @@ function registerExtendedCommands(program) {
10765
10872
  ${chalk4.dim(spendLabel)} ${fmt2(summary.total_usd)}`);
10766
10873
  console.log();
10767
10874
  });
10768
- program.command("savings [period]").description("Subscription vs API-equivalent savings breakdown").option("--agent <agent>", "Filter by agent").option("--json", "Output JSON").action(async (periodArg, opts) => {
10875
+ program.command("savings [period]").description("Subscription vs API-equivalent savings breakdown").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--json", "Output JSON").action(async (periodArg, opts) => {
10769
10876
  const period = parsePeriod(periodArg, "month");
10770
10877
  const agent = parseAgent(opts.agent, "--agent");
10771
10878
  const savings = await getStore().savings(period, agent);
@@ -10784,7 +10891,7 @@ function registerExtendedCommands(program) {
10784
10891
  console.log();
10785
10892
  });
10786
10893
  const subsCmd = program.command("subscriptions").description("Manage subscription plans");
10787
- subsCmd.command("set").description("Set a subscription plan").requiredOption("--provider <name>", "Provider name").requiredOption("--plan <name>", "Plan name").option("--agent <agent>", "Agent scope").option("--fee <usd>", "Monthly fee USD", "0").option("--included <usd>", "Included usage USD", "0").action(async (opts) => {
10894
+ subsCmd.command("set").description("Set a subscription plan").requiredOption("--provider <name>", "Provider name").requiredOption("--plan <name>", "Plan name").option("--agent <agent>", `Agent scope (${AGENTS.join("|")})`).option("--fee <usd>", "Monthly fee USD", "0").option("--included <usd>", "Included usage USD", "0").action(async (opts) => {
10788
10895
  const agent = parseAgent(opts.agent, "--agent") ?? null;
10789
10896
  await getStore().setSubscription({
10790
10897
  agent,
@@ -11115,7 +11222,7 @@ init_store();
11115
11222
  init_transport();
11116
11223
  import { execSync } from "child_process";
11117
11224
  var program = new Command;
11118
- program.name("economy").description("AI coding cost tracker \u2014 Claude, Codex, Gemini, OpenCode, Cursor, Pi, Hermes").version(packageMetadata.version);
11225
+ program.name("economy").description("AI coding cost tracker \u2014 Claude, Takumi, Codex, Gemini, OpenCode, Cursor, Pi, Hermes").version(packageMetadata.version);
11119
11226
  async function autoSync(opts = {}) {
11120
11227
  if (isCloudStore())
11121
11228
  return;
@@ -11429,7 +11536,7 @@ program.command("month").description("Cost summary for this month").action(async
11429
11536
  await autoSync();
11430
11537
  await printSummary("This Month", "month");
11431
11538
  });
11432
- program.command("sessions").description("List coding sessions with costs").option("--agent <agent>", "Filter by agent (claude|takumi|codex|gemini)").option("--project <path>", "Filter by project path").option("--account <query>", "Filter by account key, name, or email").option("--machine <id>", "Filter by machine hostname (e.g. spark01, apple01)").option("--limit <n>", "Number of sessions", "20").option("--format <fmt>", "Output format: table|compact|csv|json", "table").option("--since <date>", "Filter sessions since date or relative (e.g. 2026-03-01, 7d, 30d)").option("--search <query>", "Search by project name, session id prefix, or agent").action(async (opts) => {
11539
+ program.command("sessions").description("List coding sessions with costs").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--project <path>", "Filter by project path").option("--account <query>", "Filter by account key, name, or email").option("--machine <id>", "Filter by machine hostname (e.g. spark01, apple01)").option("--limit <n>", "Number of sessions", "20").option("--format <fmt>", "Output format: table|compact|csv|json", "table").option("--since <date>", "Filter sessions since date or relative (e.g. 2026-03-01, 7d, 30d)").option("--search <query>", "Search by project name, session id prefix, or agent").action(async (opts) => {
11433
11540
  const limit = parsePositiveCliInteger(opts.limit ?? "20", "--limit");
11434
11541
  const agent = parseOptionalCliAgent(opts.agent);
11435
11542
  await autoSync();
@@ -11476,7 +11583,7 @@ program.command("sessions").description("List coding sessions with costs").optio
11476
11583
  ]));
11477
11584
  console.log();
11478
11585
  });
11479
- program.command("top").description("Most expensive sessions").option("-n <n>", "Number of sessions", "10").option("--agent <agent>", "Filter by agent").option("--since <date>", "Filter sessions since date or relative (e.g. 2026-03-01, 7d, 30d)").action(async (opts) => {
11586
+ program.command("top").description("Most expensive sessions").option("-n <n>", "Number of sessions", "10").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--since <date>", "Filter sessions since date or relative (e.g. 2026-03-01, 7d, 30d)").action(async (opts) => {
11480
11587
  const count = parsePositiveCliInteger(opts.n ?? "10", "-n");
11481
11588
  const agent = parseOptionalCliAgent(opts.agent);
11482
11589
  const sinceDate = opts.since ? parseSinceDate2(opts.since) : undefined;
@@ -11607,7 +11714,7 @@ program.command("accounts [period]").description("List account usage by email ad
11607
11714
  printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
11608
11715
  console.log();
11609
11716
  });
11610
- program.command("watch").description("Live stream of incoming costs").option("--interval <seconds>", "Poll interval in seconds", "10").option("--daemon", "Watch agent data directories and sync on change").option("--agent <agent>", "Filter by agent").option("--notify <amount>", "Fire macOS notification when cumulative cost crosses this USD threshold").action(async (opts) => {
11717
+ program.command("watch").description("Live stream of incoming costs").option("--interval <seconds>", "Poll interval in seconds", "10").option("--daemon", "Watch agent data directories and sync on change").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--notify <amount>", "Fire macOS notification when cumulative cost crosses this USD threshold").action(async (opts) => {
11611
11718
  const { watchCosts: watchCosts2 } = await Promise.resolve().then(() => (init_watch(), exports_watch));
11612
11719
  await watchCosts2({
11613
11720
  interval: parsePositiveCliInteger(opts.interval ?? "10", "--interval"),
@@ -11617,7 +11724,7 @@ program.command("watch").description("Live stream of incoming costs").option("--
11617
11724
  });
11618
11725
  });
11619
11726
  var budgetCmd = program.command("budget").description("Manage spending budgets");
11620
- budgetCmd.command("set").description("Set a budget").option("--project <path>", "Project path (omit for global)").option("--cost-center <id>", "Cost center id (for example loop:fleet-evaluator)").option("--period <period>", "Period: daily|weekly|monthly", "monthly").option("--limit <usd>", "Budget limit in USD").option("--alert <percent>", "Alert threshold %", "80").option("--agent <agent>", "Limit to agent (claude|takumi|codex|gemini)").action(async (opts) => {
11727
+ budgetCmd.command("set").description("Set a budget").option("--project <path>", "Project path (omit for global)").option("--cost-center <id>", "Cost center id (for example loop:fleet-evaluator)").option("--period <period>", "Period: daily|weekly|monthly", "monthly").option("--limit <usd>", "Budget limit in USD").option("--alert <percent>", "Alert threshold %", "80").option("--agent <agent>", `Limit to agent (${AGENTS.join("|")})`).action(async (opts) => {
11621
11728
  const limitUsd = parsePositiveCliNumber(opts.limit, "--limit");
11622
11729
  const alertAtPercent = parsePositiveCliNumber(opts.alert ?? "80", "--alert");
11623
11730
  if (alertAtPercent > 100)
@@ -12126,7 +12233,7 @@ menubarCmd.command("stop").description("Quit Economy Bar").action(async () => {
12126
12233
  menubarStop2();
12127
12234
  });
12128
12235
  var goalCmd = program.command("goal").description("Manage spending goals");
12129
- goalCmd.command("set").description("Set a spending goal").option("--period <period>", "Period: day|week|month|year", "month").option("--limit <usd>", "Goal limit in USD").option("--project <path>", "Scope to project path").option("--agent <agent>", "Scope to agent").action(async (opts) => {
12236
+ goalCmd.command("set").description("Set a spending goal").option("--period <period>", "Period: day|week|month|year", "month").option("--limit <usd>", "Goal limit in USD").option("--project <path>", "Scope to project path").option("--agent <agent>", `Scope to agent (${AGENTS.join("|")})`).action(async (opts) => {
12130
12237
  const limitUsd = parsePositiveCliNumber(opts.limit, "--limit");
12131
12238
  const period = requireCliChoice(opts.period, "--period", ["day", "week", "month", "year"]);
12132
12239
  const agent = parseOptionalCliAgent(opts.agent) ?? null;
@@ -1,7 +1,6 @@
1
- import type { DbAdapter } from '@hasna/cloud';
2
1
  import type { AuthQueryClient } from '@hasna/contracts/auth';
3
2
  import pg from 'pg';
4
- import type { SqliteAdapter as Database } from '@hasna/cloud';
3
+ import type { DbAdapter, SqliteAdapter as Database } from './sqlite-adapter.js';
5
4
  /** Resolve the cloud Postgres DSN from the standard env aliases. */
6
5
  export declare function getCloudDatabaseUrl(): string | undefined;
7
6
  /** True when the serve should run in cloud (RDS-direct) mode. */
@@ -10,7 +9,7 @@ export declare function isCloudMode(): boolean;
10
9
  export declare function resolveSigningSecret(): string | undefined;
11
10
  /**
12
11
  * Open a pooled Postgres connection to the shared RDS. Returns a value typed as
13
- * the SQLite `Database` the query layer expects — safe because `PgAdapter`
12
+ * the SQLite `Database` the query layer expects — safe because `SyncPgAdapter`
14
13
  * implements the identical `DbAdapter` surface those functions rely on.
15
14
  */
16
15
  export declare function openCloudDatabase(dsn?: string | undefined): Database;
@@ -19,7 +18,7 @@ export declare function createCloudPool(dsn?: string | undefined): pg.Pool;
19
18
  /**
20
19
  * Adapt a pg Pool to the `@hasna/contracts/auth` `AuthQueryClient` surface
21
20
  * (many/get/execute). This backs the `ApiKeyStore` without pulling in the
22
- * generic vendored kit — economy's storage is the `@hasna/cloud` PG pool.
21
+ * generic vendored kit — economy's storage is this pg pool.
23
22
  */
24
23
  export declare function authClientFromPool(pool: pg.Pool): AuthQueryClient;
25
24
  /** Ordered PG migrations applied by the migration runner (index = version). */
@@ -1 +1 @@
1
- {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/db/cloud.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAI7D,oEAAoE;AACpE,wBAAgB,mBAAmB,IAAI,MAAM,GAAG,SAAS,CAOxD;AAED,iEAAiE;AACjE,wBAAgB,WAAW,IAAI,OAAO,CAKrC;AAED,oFAAoF;AACpF,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,SAAS,CAOzD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,qBAAwB,GAAG,QAAQ,CASvE;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,GAAG,qBAAwB,GAAG,EAAE,CAAC,IAAI,CAYpE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,eAAe,CAcjE;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;AAED,YAAY,EAAE,SAAS,EAAE,CAAA"}
1
+ {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/db/cloud.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAK/E,oEAAoE;AACpE,wBAAgB,mBAAmB,IAAI,MAAM,GAAG,SAAS,CAOxD;AAED,iEAAiE;AACjE,wBAAgB,WAAW,IAAI,OAAO,CAKrC;AAED,oFAAoF;AACpF,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,SAAS,CAOzD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,qBAAwB,GAAG,QAAQ,CASvE;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,GAAG,qBAAwB,GAAG,EAAE,CAAC,IAAI,CAWpE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,eAAe,CAcjE;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;AAED,YAAY,EAAE,SAAS,EAAE,CAAA"}
@@ -1,4 +1,4 @@
1
- import { SqliteAdapter as Database } from '@hasna/cloud';
1
+ import { SqliteAdapter as Database } from './sqlite-adapter.js';
2
2
  import type { EconomyRequest, EconomySession, EconomyProject, CostCenter, CostCenterBreakdown, CostCenterKind, Budget, BudgetStatus, CostSummary, ZeroCostModelBreakdown, ModelBreakdown, ProjectBreakdown, AgentBreakdown, AccountBreakdown, Period, Agent, SessionFilter } from '../types/index.js';
3
3
  export declare function getMachineId(): string;
4
4
  export declare function getDataDir(): string;
@@ -22,7 +22,7 @@ export declare function queryCostCenterBreakdown(db: Database, period?: Period,
22
22
  machine?: string;
23
23
  since?: string;
24
24
  }): CostCenterBreakdown[];
25
- export declare function queryProjectBreakdownSince(db: Database, since: string): ProjectBreakdown[];
25
+ export declare function queryProjectBreakdownSince(db: Database, since: string, machine?: string): ProjectBreakdown[];
26
26
  export declare function queryAgentBreakdownSince(db: Database, since: string): AgentBreakdown[];
27
27
  export declare function queryModelBreakdownSince(db: Database, since: string): ModelBreakdown[];
28
28
  export declare function queryAccountBreakdownSince(db: Database, since: string): AccountBreakdown[];