@hasna/economy 0.3.6 → 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 (92) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +73 -60
  3. package/dashboard/README.md +17 -64
  4. package/dist/cli/commands/extras.d.ts.map +1 -1
  5. package/dist/cli/commands/todos.d.ts +3 -0
  6. package/dist/cli/commands/todos.d.ts.map +1 -1
  7. package/dist/cli/index.js +914 -199
  8. package/dist/db/cloud.d.ts +3 -4
  9. package/dist/db/cloud.d.ts.map +1 -1
  10. package/dist/db/database.d.ts +10 -3
  11. package/dist/db/database.d.ts.map +1 -1
  12. package/dist/db/dialect.d.ts +13 -0
  13. package/dist/db/dialect.d.ts.map +1 -0
  14. package/dist/db/pg-migrate.d.ts +30 -0
  15. package/dist/db/pg-migrate.d.ts.map +1 -0
  16. package/dist/db/pg-migrations.d.ts.map +1 -1
  17. package/dist/db/sqlite-adapter.d.ts +36 -0
  18. package/dist/db/sqlite-adapter.d.ts.map +1 -0
  19. package/dist/db/sync-pg.d.ts +1 -1
  20. package/dist/db/sync-pg.d.ts.map +1 -1
  21. package/dist/index.js +309 -59
  22. package/dist/ingest/billing.d.ts +1 -1
  23. package/dist/ingest/billing.d.ts.map +1 -1
  24. package/dist/ingest/claude-quota.d.ts +1 -1
  25. package/dist/ingest/claude-quota.d.ts.map +1 -1
  26. package/dist/ingest/claude.d.ts +1 -1
  27. package/dist/ingest/claude.d.ts.map +1 -1
  28. package/dist/ingest/codex-quota.d.ts +1 -1
  29. package/dist/ingest/codex-quota.d.ts.map +1 -1
  30. package/dist/ingest/codex.d.ts +1 -1
  31. package/dist/ingest/codex.d.ts.map +1 -1
  32. package/dist/ingest/cursor.d.ts +1 -1
  33. package/dist/ingest/cursor.d.ts.map +1 -1
  34. package/dist/ingest/gemini.d.ts +1 -1
  35. package/dist/ingest/gemini.d.ts.map +1 -1
  36. package/dist/ingest/hermes.d.ts +1 -1
  37. package/dist/ingest/hermes.d.ts.map +1 -1
  38. package/dist/ingest/loops.d.ts +6 -0
  39. package/dist/ingest/loops.d.ts.map +1 -0
  40. package/dist/ingest/opencode.d.ts +1 -1
  41. package/dist/ingest/opencode.d.ts.map +1 -1
  42. package/dist/ingest/otel.d.ts +16 -3
  43. package/dist/ingest/otel.d.ts.map +1 -1
  44. package/dist/ingest/pi.d.ts +1 -1
  45. package/dist/ingest/pi.d.ts.map +1 -1
  46. package/dist/ingest/plugin.d.ts +1 -1
  47. package/dist/ingest/plugin.d.ts.map +1 -1
  48. package/dist/lib/accounts.d.ts.map +1 -1
  49. package/dist/lib/analytics.d.ts +1 -1
  50. package/dist/lib/analytics.d.ts.map +1 -1
  51. package/dist/lib/billing-diff.d.ts +1 -1
  52. package/dist/lib/billing-diff.d.ts.map +1 -1
  53. package/dist/lib/brief.d.ts +1 -1
  54. package/dist/lib/brief.d.ts.map +1 -1
  55. package/dist/lib/open-projects.d.ts +10 -2
  56. package/dist/lib/open-projects.d.ts.map +1 -1
  57. package/dist/lib/pricing.d.ts +1 -1
  58. package/dist/lib/pricing.d.ts.map +1 -1
  59. package/dist/lib/savings.d.ts +1 -1
  60. package/dist/lib/savings.d.ts.map +1 -1
  61. package/dist/lib/spikes.d.ts +1 -1
  62. package/dist/lib/spikes.d.ts.map +1 -1
  63. package/dist/lib/store/index.d.ts +9 -1
  64. package/dist/lib/store/index.d.ts.map +1 -1
  65. package/dist/lib/sync-all.d.ts +3 -1
  66. package/dist/lib/sync-all.d.ts.map +1 -1
  67. package/dist/lib/webhooks.d.ts +1 -1
  68. package/dist/lib/webhooks.d.ts.map +1 -1
  69. package/dist/mcp/index.js +757 -166
  70. package/dist/mcp/server.d.ts.map +1 -1
  71. package/dist/openapi.d.ts.map +1 -1
  72. package/dist/otel/index.js +262 -27
  73. package/dist/server/index.js +683 -135
  74. package/dist/server/serve.d.ts +1 -1
  75. package/dist/server/serve.d.ts.map +1 -1
  76. package/dist/types/index.d.ts +37 -4
  77. package/dist/types/index.d.ts.map +1 -1
  78. package/docs/README.md +12 -0
  79. package/docs/cli.md +88 -0
  80. package/docs/configuration.md +86 -0
  81. package/docs/ingestion.md +47 -0
  82. package/docs/mcp.md +65 -0
  83. package/docs/otel.md +53 -0
  84. package/docs/rest-api.md +89 -0
  85. package/package.json +4 -5
  86. package/dist/lib/cloud-sync.d.ts +0 -35
  87. package/dist/lib/cloud-sync.d.ts.map +0 -1
  88. package/dist/lib/fleet-sync.d.ts +0 -61
  89. package/dist/lib/fleet-sync.d.ts.map +0 -1
  90. package/dist/lib/peer-sync.d.ts +0 -21
  91. package/dist/lib/peer-sync.d.ts.map +0 -1
  92. package/dist/storage.js +0 -2746
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, {
@@ -544,6 +600,7 @@ __export(exports_database, {
544
600
  upsertProject: () => upsertProject,
545
601
  upsertModelPricing: () => upsertModelPricing,
546
602
  upsertGoal: () => upsertGoal,
603
+ upsertCostCenter: () => upsertCostCenter,
547
604
  upsertBudget: () => upsertBudget,
548
605
  upsertBillingDaily: () => upsertBillingDaily,
549
606
  setIngestState: () => setIngestState,
@@ -561,6 +618,7 @@ __export(exports_database, {
561
618
  queryModelBreakdown: () => queryModelBreakdown,
562
619
  queryHourlyBreakdown: () => queryHourlyBreakdown,
563
620
  queryDailyBreakdown: () => queryDailyBreakdown,
621
+ queryCostCenterBreakdown: () => queryCostCenterBreakdown,
564
622
  queryBillingSummary: () => queryBillingSummary,
565
623
  queryAgentBreakdownSince: () => queryAgentBreakdownSince,
566
624
  queryAgentBreakdown: () => queryAgentBreakdown,
@@ -583,6 +641,7 @@ __export(exports_database, {
583
641
  getGoalStatuses: () => getGoalStatuses,
584
642
  getDbPath: () => getDbPath,
585
643
  getDataDir: () => getDataDir,
644
+ getCostCenter: () => getCostCenter,
586
645
  getBudgetStatuses: () => getBudgetStatuses,
587
646
  deleteSubscription: () => deleteSubscription,
588
647
  deleteProject: () => deleteProject,
@@ -593,7 +652,6 @@ __export(exports_database, {
593
652
  clearBillingRange: () => clearBillingRange,
594
653
  bulkIngest: () => bulkIngest
595
654
  });
596
- import { SqliteAdapter as Database } from "@hasna/cloud";
597
655
  import { execFileSync } from "child_process";
598
656
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
599
657
  import { hostname, platform } from "os";
@@ -686,7 +744,7 @@ function openDatabase(dbPath, skipSeed = false) {
686
744
  mkdirSync(dir, { recursive: true });
687
745
  }
688
746
  const db = withSqliteBusyRetry(() => {
689
- const opened = new Database(path);
747
+ const opened = new SqliteAdapter(path);
690
748
  try {
691
749
  opened.exec("PRAGMA busy_timeout = 10000");
692
750
  opened.exec("PRAGMA journal_mode = WAL");
@@ -727,6 +785,16 @@ function addColumnIfMissing(db, table, column, definition) {
727
785
  }
728
786
  function initSchema(db) {
729
787
  db.exec(`
788
+ CREATE TABLE IF NOT EXISTS cost_centers (
789
+ id TEXT PRIMARY KEY,
790
+ kind TEXT NOT NULL,
791
+ name TEXT NOT NULL,
792
+ repo_path TEXT,
793
+ labels_json TEXT DEFAULT '{}',
794
+ created_at TEXT NOT NULL,
795
+ updated_at TEXT NOT NULL
796
+ );
797
+
730
798
  CREATE TABLE IF NOT EXISTS requests (
731
799
  id TEXT PRIMARY KEY,
732
800
  agent TEXT NOT NULL,
@@ -743,6 +811,7 @@ function initSchema(db) {
743
811
  timestamp TEXT NOT NULL,
744
812
  source_request_id TEXT,
745
813
  machine_id TEXT DEFAULT '',
814
+ cost_center_id TEXT,
746
815
  account_key TEXT DEFAULT '',
747
816
  account_tool TEXT DEFAULT '',
748
817
  account_name TEXT DEFAULT '',
@@ -761,6 +830,7 @@ function initSchema(db) {
761
830
  total_tokens INTEGER DEFAULT 0,
762
831
  request_count INTEGER DEFAULT 0,
763
832
  machine_id TEXT DEFAULT '',
833
+ cost_center_id TEXT,
764
834
  account_key TEXT DEFAULT '',
765
835
  account_tool TEXT DEFAULT '',
766
836
  account_name TEXT DEFAULT '',
@@ -781,6 +851,7 @@ function initSchema(db) {
781
851
  id TEXT PRIMARY KEY,
782
852
  project_path TEXT,
783
853
  agent TEXT,
854
+ cost_center_id TEXT,
784
855
  period TEXT NOT NULL,
785
856
  limit_usd REAL NOT NULL,
786
857
  alert_at_percent INTEGER DEFAULT 80,
@@ -811,6 +882,7 @@ function initSchema(db) {
811
882
  CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
812
883
  CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
813
884
  CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
885
+ CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
814
886
 
815
887
  CREATE TABLE IF NOT EXISTS model_pricing (
816
888
  model TEXT PRIMARY KEY,
@@ -897,6 +969,12 @@ function initSchema(db) {
897
969
  `);
898
970
  addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
899
971
  addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
972
+ if (addColumnIfMissing(db, "cost_centers", "updated_at", `TEXT DEFAULT ''`)) {
973
+ db.exec(`UPDATE cost_centers SET updated_at = created_at WHERE updated_at = '' OR updated_at IS NULL`);
974
+ }
975
+ addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
976
+ addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
977
+ addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
900
978
  if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
901
979
  db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
902
980
  }
@@ -925,6 +1003,10 @@ function initSchema(db) {
925
1003
  CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
926
1004
  CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
927
1005
  CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
1006
+ CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id);
1007
+ CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id);
1008
+ CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
1009
+ CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
928
1010
  `);
929
1011
  }
930
1012
  function periodWhere(period) {
@@ -975,6 +1057,17 @@ function requestPeriodWhere(period) {
975
1057
  return "1=1";
976
1058
  }
977
1059
  }
1060
+ function upsertCostCenter(db, center) {
1061
+ const updatedAt = center.updated_at ?? center.created_at;
1062
+ db.prepare(`
1063
+ INSERT OR REPLACE INTO cost_centers
1064
+ (id, kind, name, repo_path, labels_json, created_at, updated_at)
1065
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1066
+ `).run(center.id, center.kind, center.name, center.repo_path ?? null, center.labels_json ?? "{}", center.created_at, updatedAt);
1067
+ }
1068
+ function getCostCenter(db, id) {
1069
+ return db.prepare(`SELECT * FROM cost_centers WHERE id = ?`).get(id);
1070
+ }
978
1071
  function upsertRequest(db, req) {
979
1072
  const now = req.updated_at ?? new Date().toISOString();
980
1073
  db.prepare(`
@@ -982,10 +1075,10 @@ function upsertRequest(db, req) {
982
1075
  (id, agent, session_id, model, input_tokens, output_tokens,
983
1076
  cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
984
1077
  cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
985
- source_request_id, machine_id, attribution_tag, account_key, account_tool,
1078
+ source_request_id, machine_id, cost_center_id, attribution_tag, account_key, account_tool,
986
1079
  account_name, account_email, account_source, updated_at, synced_at)
987
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
988
- `).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 ?? "");
1080
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1081
+ `).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.cost_center_id ?? null, 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 ?? "");
989
1082
  }
990
1083
  function upsertSession(db, session) {
991
1084
  const now = session.updated_at ?? new Date().toISOString();
@@ -993,9 +1086,9 @@ function upsertSession(db, session) {
993
1086
  INSERT OR REPLACE INTO sessions
994
1087
  (id, agent, project_path, project_name, started_at, ended_at,
995
1088
  total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
996
- account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
997
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
998
- `).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 ?? "");
1089
+ cost_center_id, account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
1090
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1091
+ `).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.cost_center_id ?? null, session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
999
1092
  }
1000
1093
  function rollupSession(db, sessionId) {
1001
1094
  db.prepare(`
@@ -1021,9 +1114,12 @@ function rollupSession(db, sessionId) {
1021
1114
  ELSE account_email END,
1022
1115
  account_source = CASE WHEN account_source = '' OR account_source IS NULL
1023
1116
  THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
1024
- ELSE account_source END
1117
+ ELSE account_source END,
1118
+ cost_center_id = CASE WHEN cost_center_id = '' OR cost_center_id IS NULL
1119
+ THEN COALESCE((SELECT cost_center_id FROM requests WHERE session_id = ? AND cost_center_id IS NOT NULL AND cost_center_id != '' ORDER BY timestamp DESC LIMIT 1), NULL)
1120
+ ELSE cost_center_id END
1025
1121
  WHERE id = ?
1026
- `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
1122
+ `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
1027
1123
  }
1028
1124
  function querySessions(db, filter = {}) {
1029
1125
  const conditions = [];
@@ -1254,9 +1350,12 @@ function labelForPath(projectPath, projectName) {
1254
1350
  function groupKeyForPath(projectPath, projectName) {
1255
1351
  return labelForPath(projectPath, projectName).trim().toLowerCase();
1256
1352
  }
1257
- function queryProjectBreakdown(db, period = "all", machine) {
1258
- const requestWhere = requestPeriodWhere(period);
1259
- 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) {
1260
1359
  const sessionMachineClause = machine ? " AND (machine_id = ? OR id IN (SELECT DISTINCT session_id FROM requests WHERE machine_id = ?))" : "";
1261
1360
  const requestMachineClause = machine ? " AND machine_id = ?" : "";
1262
1361
  const sessionMachineParams = machine ? [machine, machine] : [];
@@ -1264,67 +1363,89 @@ function queryProjectBreakdown(db, period = "all", machine) {
1264
1363
  const sessionOnlyMachineClause = machine ? " AND machine_id = ?" : "";
1265
1364
  const sessionOnlyMachineParams = machine ? [machine] : [];
1266
1365
  const sessions = db.prepare(`
1267
- SELECT id, project_path, project_name, total_cost_usd, started_at
1366
+ SELECT id, project_path, project_name
1268
1367
  FROM sessions
1269
1368
  WHERE (project_path != '' OR project_name != '')${sessionMachineClause}
1270
1369
  `).all(...sessionMachineParams);
1271
1370
  const groups = new Map;
1371
+ const groupKeyBySession = new Map;
1272
1372
  for (const s of sessions) {
1273
1373
  const label = labelForPath(s.project_path, s.project_name);
1274
1374
  if (!label)
1275
1375
  continue;
1276
1376
  const key = groupKeyForPath(s.project_path, s.project_name);
1277
- const g = groups.get(key) ?? { label, sessionIds: [], samplePath: s.project_path };
1278
- 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: "" };
1279
1378
  if (!g.samplePath)
1280
1379
  g.samplePath = s.project_path;
1281
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);
1282
1428
  }
1283
1429
  const result = [];
1284
1430
  for (const g of groups.values()) {
1285
- const placeholders = g.sessionIds.map(() => "?").join(",");
1286
- const reqStats = placeholders.length ? db.prepare(`
1287
- SELECT
1288
- COUNT(DISTINCT session_id) as sessions,
1289
- COUNT(*) as requests,
1290
- COALESCE(SUM(cost_usd), 0) as cost_usd,
1291
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1292
- MAX(timestamp) as last_active
1293
- FROM requests
1294
- WHERE session_id IN (${placeholders})
1295
- AND ${requestWhere}
1296
- ${requestMachineClause}
1297
- `).get(...g.sessionIds, ...requestMachineParams) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
1298
- const sessionOnlyStats = placeholders.length ? db.prepare(`
1299
- SELECT
1300
- COUNT(*) as sessions,
1301
- COALESCE(SUM(request_count), 0) as requests,
1302
- COALESCE(SUM(total_tokens), 0) as total_tokens,
1303
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1304
- MAX(started_at) as last_active
1305
- FROM sessions
1306
- WHERE id IN (${placeholders})
1307
- AND ${sessionWhere}
1308
- ${sessionOnlyMachineClause}
1309
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1310
- `).get(...g.sessionIds, ...sessionOnlyMachineParams) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
1311
- const totalSessions = reqStats.sessions + sessionOnlyStats.sessions;
1312
- if (totalSessions === 0)
1431
+ if (g.sessions === 0)
1313
1432
  continue;
1314
- const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
1315
1433
  result.push({
1316
1434
  project_path: g.samplePath,
1317
1435
  project_name: g.label,
1318
- sessions: totalSessions,
1319
- requests: reqStats.requests + sessionOnlyStats.requests,
1320
- total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
1321
- cost_usd: reqStats.cost_usd + sessionOnlyStats.cost_usd,
1322
- 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
1323
1441
  });
1324
1442
  }
1325
1443
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1326
1444
  return result;
1327
1445
  }
1446
+ function queryProjectBreakdown(db, period = "all", machine) {
1447
+ return queryProjectBreakdownScoped(db, requestPeriodWhere(period), sessionPeriodWhere(period), [], [], machine);
1448
+ }
1328
1449
  function normalizeAccountEmail(email) {
1329
1450
  return (email ?? "").trim().toLowerCase();
1330
1451
  }
@@ -1465,17 +1586,141 @@ function queryAccountBreakdown(db, period = "all", machine) {
1465
1586
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1466
1587
  return result;
1467
1588
  }
1468
- function queryProjectBreakdownSince(db, since) {
1469
- return db.prepare(`
1470
- SELECT project_path, project_name,
1471
- COUNT(*) as sessions,
1472
- COALESCE(SUM(total_tokens), 0) as total_tokens,
1473
- COALESCE(SUM(request_count), 0) as requests,
1474
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1475
- MAX(started_at) as last_active
1476
- FROM sessions WHERE started_at >= ?
1477
- GROUP BY project_path ORDER BY cost_usd DESC
1478
- `).all(since);
1589
+ function kindFromCostCenterId(id) {
1590
+ const prefix = id.split(":", 1)[0] ?? "";
1591
+ return ["loop", "app", "repo", "service", "team"].includes(prefix) ? prefix : "";
1592
+ }
1593
+ function addCostCenterBreakdownRow(groups, row, sessionOnly) {
1594
+ if (!row.cost_center_id)
1595
+ return;
1596
+ const derivedKind = row.kind || kindFromCostCenterId(row.cost_center_id) || "service";
1597
+ const group = groups.get(row.cost_center_id) ?? {
1598
+ cost_center_id: row.cost_center_id,
1599
+ kind: derivedKind,
1600
+ name: row.name || row.cost_center_id,
1601
+ repo_path: row.repo_path ?? null,
1602
+ labels_json: row.labels_json || "{}",
1603
+ sessionIds: new Set,
1604
+ requests: 0,
1605
+ total_tokens: 0,
1606
+ api_equivalent_usd: 0,
1607
+ metered_api_usd: 0,
1608
+ subscription_included_usd: 0,
1609
+ estimated_usd: 0,
1610
+ unknown_usd: 0,
1611
+ last_active: ""
1612
+ };
1613
+ group.kind = derivedKind;
1614
+ if (row.name)
1615
+ group.name = row.name;
1616
+ if (row.repo_path && !group.repo_path)
1617
+ group.repo_path = row.repo_path;
1618
+ if (row.labels_json && row.labels_json !== "{}")
1619
+ group.labels_json = row.labels_json;
1620
+ if (row.session_id)
1621
+ group.sessionIds.add(row.session_id);
1622
+ group.requests += row.requests;
1623
+ group.total_tokens += row.total_tokens;
1624
+ group.api_equivalent_usd += row.cost_usd;
1625
+ if (sessionOnly) {
1626
+ group.estimated_usd += row.cost_usd;
1627
+ } else if (row.cost_basis === "metered_api") {
1628
+ group.metered_api_usd += row.cost_usd;
1629
+ } else if (row.cost_basis === "subscription_included") {
1630
+ group.subscription_included_usd += row.cost_usd;
1631
+ } else if (row.cost_basis === "unknown") {
1632
+ group.unknown_usd += row.cost_usd;
1633
+ } else {
1634
+ group.estimated_usd += row.cost_usd;
1635
+ }
1636
+ if (!group.last_active || row.last_active > group.last_active)
1637
+ group.last_active = row.last_active;
1638
+ groups.set(row.cost_center_id, group);
1639
+ }
1640
+ function queryCostCenterBreakdown(db, period = "all", filter = {}) {
1641
+ const requestWhere = filter.since ? "r.timestamp >= ?" : requestPeriodWhere(period);
1642
+ const sessionWhere = filter.since ? "s.started_at >= ?" : sessionPeriodWhere(period);
1643
+ const requestMachineClause = filter.machine ? " AND r.machine_id = ?" : "";
1644
+ const sessionMachineClause = filter.machine ? " AND s.machine_id = ?" : "";
1645
+ const requestParams = [];
1646
+ const sessionParams = [];
1647
+ if (filter.since) {
1648
+ requestParams.push(filter.since);
1649
+ sessionParams.push(filter.since);
1650
+ }
1651
+ if (filter.machine) {
1652
+ requestParams.push(filter.machine);
1653
+ sessionParams.push(filter.machine);
1654
+ }
1655
+ const groups = new Map;
1656
+ const requestRows = db.prepare(`
1657
+ SELECT
1658
+ COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) as cost_center_id,
1659
+ COALESCE(cc.kind, '') as kind,
1660
+ COALESCE(cc.name, COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))) as name,
1661
+ cc.repo_path as repo_path,
1662
+ COALESCE(cc.labels_json, '{}') as labels_json,
1663
+ r.session_id as session_id,
1664
+ 1 as requests,
1665
+ COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
1666
+ COALESCE(r.cost_usd, 0) as cost_usd,
1667
+ COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
1668
+ r.timestamp as last_active
1669
+ FROM requests r
1670
+ LEFT JOIN sessions s ON s.id = r.session_id
1671
+ LEFT JOIN cost_centers cc ON cc.id = COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))
1672
+ WHERE ${requestWhere}${requestMachineClause}
1673
+ AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) IS NOT NULL
1674
+ AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) != ''
1675
+ `).all(...requestParams);
1676
+ for (const row of requestRows)
1677
+ addCostCenterBreakdownRow(groups, row, false);
1678
+ const sessionOnlyRows = db.prepare(`
1679
+ SELECT
1680
+ s.cost_center_id as cost_center_id,
1681
+ COALESCE(cc.kind, '') as kind,
1682
+ COALESCE(cc.name, s.cost_center_id) as name,
1683
+ cc.repo_path as repo_path,
1684
+ COALESCE(cc.labels_json, '{}') as labels_json,
1685
+ s.id as session_id,
1686
+ COALESCE(s.request_count, 0) as requests,
1687
+ COALESCE(s.total_tokens, 0) as total_tokens,
1688
+ COALESCE(s.total_cost_usd, 0) as cost_usd,
1689
+ 'estimated' as cost_basis,
1690
+ s.started_at as last_active
1691
+ FROM sessions s
1692
+ LEFT JOIN cost_centers cc ON cc.id = s.cost_center_id
1693
+ WHERE ${sessionWhere}${sessionMachineClause}
1694
+ AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
1695
+ AND s.cost_center_id IS NOT NULL
1696
+ AND s.cost_center_id != ''
1697
+ `).all(...sessionParams);
1698
+ for (const row of sessionOnlyRows)
1699
+ addCostCenterBreakdownRow(groups, row, true);
1700
+ const result = [...groups.values()].map((group) => ({
1701
+ cost_center_id: group.cost_center_id,
1702
+ kind: group.kind,
1703
+ name: group.name,
1704
+ repo_path: group.repo_path,
1705
+ labels_json: group.labels_json,
1706
+ sessions: group.sessionIds.size,
1707
+ requests: group.requests,
1708
+ total_tokens: group.total_tokens,
1709
+ api_equivalent_usd: group.api_equivalent_usd,
1710
+ billable_usd: group.metered_api_usd,
1711
+ metered_api_usd: group.metered_api_usd,
1712
+ subscription_included_usd: group.subscription_included_usd,
1713
+ estimated_usd: group.estimated_usd,
1714
+ unknown_usd: group.unknown_usd,
1715
+ cost_usd: group.api_equivalent_usd,
1716
+ last_active: group.last_active
1717
+ }));
1718
+ const filtered = filter.kind ? result.filter((row) => row.kind === filter.kind) : result;
1719
+ filtered.sort((a, b) => b.cost_usd - a.cost_usd);
1720
+ return filtered;
1721
+ }
1722
+ function queryProjectBreakdownSince(db, since, machine) {
1723
+ return queryProjectBreakdownScoped(db, "timestamp >= ?", "started_at >= ?", [since], [since], machine);
1479
1724
  }
1480
1725
  function queryAgentBreakdownSince(db, since) {
1481
1726
  return db.prepare(`
@@ -1654,9 +1899,9 @@ function deleteProject(db, path) {
1654
1899
  function upsertBudget(db, budget) {
1655
1900
  db.prepare(`
1656
1901
  INSERT OR REPLACE INTO budgets
1657
- (id, project_path, agent, period, limit_usd, alert_at_percent, created_at, updated_at)
1658
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1659
- `).run(budget.id, budget.project_path ?? null, budget.agent ?? null, budget.period, budget.limit_usd, budget.alert_at_percent, budget.created_at, budget.updated_at);
1902
+ (id, project_path, agent, cost_center_id, period, limit_usd, alert_at_percent, created_at, updated_at)
1903
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1904
+ `).run(budget.id, budget.project_path ?? null, budget.agent ?? null, budget.cost_center_id ?? null, budget.period, budget.limit_usd, budget.alert_at_percent, budget.created_at, budget.updated_at);
1660
1905
  }
1661
1906
  function listBudgets(db) {
1662
1907
  return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
@@ -1678,6 +1923,10 @@ function getBudgetStatuses(db) {
1678
1923
  spendQuery += ` AND agent = ?`;
1679
1924
  params.push(b.agent);
1680
1925
  }
1926
+ if (b.cost_center_id) {
1927
+ spendQuery += ` AND COALESCE(NULLIF(cost_center_id, ''), (SELECT NULLIF(s.cost_center_id, '') FROM sessions s WHERE s.id = requests.session_id)) = ?`;
1928
+ params.push(b.cost_center_id);
1929
+ }
1681
1930
  const row = db.prepare(spendQuery).get(...params);
1682
1931
  const spend = row.spend;
1683
1932
  const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
@@ -2175,7 +2424,9 @@ var numOr = (v, d = 0) => {
2175
2424
  const num = Number(v);
2176
2425
  return Number.isFinite(num) ? num : d;
2177
2426
  }, strOr = (v, d = "") => v == null ? d : String(v);
2178
- var init_database = () => {};
2427
+ var init_database = __esm(() => {
2428
+ init_sqlite_adapter();
2429
+ });
2179
2430
 
2180
2431
  // src/lib/package-metadata.ts
2181
2432
  import { readFileSync } from "fs";
@@ -3655,11 +3906,18 @@ class LocalStore {
3655
3906
  return query.since ? queryAgentBreakdownSince(this.db(), query.since) : queryAgentBreakdown(this.db(), query.period ?? "all", query.machine);
3656
3907
  }
3657
3908
  async projectBreakdown(query = {}) {
3658
- 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);
3659
3910
  }
3660
3911
  async accountBreakdown(query = {}) {
3661
3912
  return query.since ? queryAccountBreakdownSince(this.db(), query.since) : queryAccountBreakdown(this.db(), query.period ?? "all", query.machine);
3662
3913
  }
3914
+ async costCenterBreakdown(query = {}) {
3915
+ return queryCostCenterBreakdown(this.db(), query.period ?? "all", {
3916
+ kind: query.kind,
3917
+ machine: query.machine,
3918
+ since: query.since
3919
+ });
3920
+ }
3663
3921
  async accounts(period) {
3664
3922
  return queryAccountBreakdown(this.db(), period);
3665
3923
  }
@@ -3844,11 +4102,19 @@ class ApiStore {
3844
4102
  return cloudListItems(this.cloud, "breakdown", q({ by: "agent", period: query.period, since: query.since }));
3845
4103
  }
3846
4104
  async projectBreakdown(query = {}) {
3847
- 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 }));
3848
4106
  }
3849
4107
  async accountBreakdown(query = {}) {
3850
4108
  return cloudListItems(this.cloud, "breakdown", q({ by: "account", period: query.period, since: query.since }));
3851
4109
  }
4110
+ async costCenterBreakdown(query = {}) {
4111
+ const rows = await cloudListItems(this.cloud, "breakdown", q({
4112
+ by: query.kind ?? "cost-center",
4113
+ period: query.period,
4114
+ since: query.since
4115
+ }));
4116
+ return rows.filter((row) => Boolean(row?.cost_center_id));
4117
+ }
3852
4118
  async accounts(period) {
3853
4119
  return cloudListItems(this.cloud, "accounts", q({ period }));
3854
4120
  }
@@ -3922,6 +4188,7 @@ class ApiStore {
3922
4188
  const created = await this.cloud.client.create("budgets", {
3923
4189
  project_path: input.project_path,
3924
4190
  ...input.agent ? { agent: input.agent } : {},
4191
+ ...input.cost_center_id ? { cost_center_id: input.cost_center_id } : {},
3925
4192
  period: input.period,
3926
4193
  limit_usd: input.limit_usd,
3927
4194
  alert_at_percent: input.alert_at_percent
@@ -4199,6 +4466,7 @@ function agentPaths() {
4199
4466
  var init_paths = () => {};
4200
4467
 
4201
4468
  // src/lib/accounts.ts
4469
+ import { appliedProfileName, resolveStore } from "@hasna/accounts";
4202
4470
  function normalizeEmail(email) {
4203
4471
  return (email ?? "").trim().toLowerCase();
4204
4472
  }
@@ -4253,57 +4521,35 @@ function envOverride(agent, env) {
4253
4521
  account_source: "override"
4254
4522
  };
4255
4523
  }
4256
- function knownToolIds(api) {
4257
- try {
4258
- return new Set(api.listTools().map((tool) => tool.id));
4259
- } catch {
4260
- return new Set;
4261
- }
4262
- }
4263
- function profileForEnvDir(api, tool, env) {
4524
+ async function profileForEnvDir(store, tool, env) {
4264
4525
  const configuredDir = env[tool.envVar];
4265
4526
  if (!configuredDir)
4266
4527
  return null;
4267
4528
  const normalized = normalizeDir(configuredDir);
4268
- try {
4269
- return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
4270
- } catch {
4271
- return null;
4272
- }
4529
+ return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
4273
4530
  }
4274
4531
  async function resolveAccountForAgent(agent, env = process.env) {
4275
4532
  const override = envOverride(agent, env);
4276
4533
  if (override)
4277
4534
  return override;
4278
- let api;
4279
- try {
4280
- api = await import("@hasna/accounts");
4281
- } catch {
4282
- return null;
4283
- }
4284
- const toolIds = knownToolIds(api);
4535
+ const store = resolveStore(env);
4536
+ const tools = new Map((await store.listTools()).map((tool) => [tool.id, tool]));
4285
4537
  for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
4286
- if (!toolIds.has(toolId))
4287
- continue;
4288
- let tool;
4289
- try {
4290
- tool = api.getTool(toolId);
4291
- } catch {
4538
+ const tool = tools.get(toolId);
4539
+ if (!tool)
4292
4540
  continue;
4293
- }
4294
- const envProfile = profileForEnvDir(api, tool, env);
4541
+ const envProfile = await profileForEnvDir(store, tool, env);
4295
4542
  if (envProfile)
4296
4543
  return fromProfile(envProfile, "env");
4297
- try {
4298
- const applied = api.appliedProfile(toolId);
4544
+ const appliedName = appliedProfileName(toolId);
4545
+ if (appliedName) {
4546
+ const applied = await store.findProfile(appliedName, toolId);
4299
4547
  if (applied)
4300
4548
  return fromProfile(applied, "applied");
4301
- } catch {}
4302
- try {
4303
- const current = api.currentProfile(toolId);
4304
- if (current)
4305
- return fromProfile(current, "current");
4306
- } catch {}
4549
+ }
4550
+ const current = await store.currentProfile(toolId);
4551
+ if (current)
4552
+ return fromProfile(current, "current");
4307
4553
  }
4308
4554
  return null;
4309
4555
  }
@@ -5368,13 +5614,202 @@ var init_hermes = __esm(() => {
5368
5614
  HERMES_DB = join11(homedir8(), ".hermes", "state.db");
5369
5615
  });
5370
5616
 
5617
+ // src/ingest/loops.ts
5618
+ import { existsSync as existsSync10 } from "fs";
5619
+ import { homedir as homedir9 } from "os";
5620
+ import { join as join12 } from "path";
5621
+ import { Database as BunDatabase2 } from "bun:sqlite";
5622
+ function loopsDbPath() {
5623
+ return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? DEFAULT_LOOPS_DB_PATH;
5624
+ }
5625
+ function loopsModel() {
5626
+ return process.env["HASNA_ECONOMY_LOOPS_MODEL"] ?? process.env["ECONOMY_LOOPS_MODEL"] ?? "gpt-5.3-codex";
5627
+ }
5628
+ function openLoopsDb(dbPath, verbose) {
5629
+ let lastError;
5630
+ for (const readonly of [true, false]) {
5631
+ let loopsDb = null;
5632
+ try {
5633
+ loopsDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
5634
+ loopsDb.prepare("PRAGMA schema_version").get();
5635
+ return loopsDb;
5636
+ } catch (error) {
5637
+ lastError = error;
5638
+ loopsDb?.close();
5639
+ }
5640
+ }
5641
+ if (verbose) {
5642
+ const message = lastError instanceof Error ? lastError.message : String(lastError);
5643
+ console.log("OpenLoops DB unreadable:", dbPath, message);
5644
+ }
5645
+ return null;
5646
+ }
5647
+ function slug(value) {
5648
+ return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
5649
+ }
5650
+ function parseTarget(raw) {
5651
+ if (!raw)
5652
+ return {};
5653
+ try {
5654
+ const parsed = JSON.parse(raw);
5655
+ return parsed && typeof parsed === "object" ? parsed : {};
5656
+ } catch {
5657
+ return {};
5658
+ }
5659
+ }
5660
+ function stringValue(value) {
5661
+ return typeof value === "string" ? value.trim() : "";
5662
+ }
5663
+ function accountFields(row) {
5664
+ const target = parseTarget(row.target_json);
5665
+ const tool = stringValue(row.account_tool) || stringValue(target["account_tool"]) || stringValue(target["tool"]) || stringValue(target["agent"]);
5666
+ const profile = stringValue(row.account_profile) || stringValue(target["account_profile"]) || stringValue(target["profile"]) || stringValue(target["account"]);
5667
+ if (!tool || !profile) {
5668
+ return { account_key: "", account_tool: tool, account_name: profile, account_source: "" };
5669
+ }
5670
+ return {
5671
+ account_key: `${tool}:${profile}`,
5672
+ account_tool: tool,
5673
+ account_name: profile,
5674
+ account_source: "loops"
5675
+ };
5676
+ }
5677
+ function readLoopGoalRuns(loopsDb) {
5678
+ return loopsDb.prepare(`
5679
+ SELECT
5680
+ gr.id as goal_run_id,
5681
+ gr.goal_id as goal_id,
5682
+ COALESCE(gr.loop_id, g.loop_id, lr.loop_id) as loop_id,
5683
+ COALESCE(gr.loop_run_id, g.loop_run_id, lr.id) as loop_run_id,
5684
+ COALESCE(l.name, lr.loop_name, 'unknown-loop') as loop_name,
5685
+ l.labels_json as labels_json,
5686
+ l.target_json as target_json,
5687
+ g.objective as objective,
5688
+ gr.phase as phase,
5689
+ gr.status as status,
5690
+ COALESCE(gr.tokens_used, 0) as tokens_used,
5691
+ gr.created_at as created_at,
5692
+ gr.updated_at as updated_at,
5693
+ lr.started_at as loop_run_started_at,
5694
+ wsr.account_profile as account_profile,
5695
+ wsr.account_tool as account_tool
5696
+ FROM goal_runs gr
5697
+ LEFT JOIN goals g ON g.id = gr.goal_id
5698
+ LEFT JOIN loop_runs lr ON lr.id = COALESCE(gr.loop_run_id, g.loop_run_id)
5699
+ LEFT JOIN loops l ON l.id = COALESCE(gr.loop_id, g.loop_id, lr.loop_id)
5700
+ LEFT JOIN (
5701
+ SELECT goal_run_id, MIN(account_profile) as account_profile, MIN(account_tool) as account_tool
5702
+ FROM workflow_step_runs
5703
+ WHERE goal_run_id IS NOT NULL
5704
+ GROUP BY goal_run_id
5705
+ ) wsr ON wsr.goal_run_id = gr.id
5706
+ WHERE COALESCE(gr.tokens_used, 0) > 0
5707
+ ORDER BY gr.created_at ASC, gr.id ASC
5708
+ `).all();
5709
+ }
5710
+ async function ingestLoops(db, verbose = false) {
5711
+ const dbPath = loopsDbPath();
5712
+ if (!existsSync10(dbPath)) {
5713
+ if (verbose)
5714
+ console.log("OpenLoops DB not found:", dbPath);
5715
+ return { sessions: 0, requests: 0 };
5716
+ }
5717
+ const model = loopsModel();
5718
+ const machineId = getMachineId();
5719
+ let loopsDb = null;
5720
+ let requests = 0;
5721
+ const sessions = new Set;
5722
+ try {
5723
+ loopsDb = openLoopsDb(dbPath, verbose);
5724
+ if (!loopsDb)
5725
+ return { sessions: 0, requests: 0 };
5726
+ for (const row of readLoopGoalRuns(loopsDb)) {
5727
+ const loopName = row.loop_name || "unknown-loop";
5728
+ const costCenterId = `loop:${slug(loopName)}`;
5729
+ const sessionId = `loops-${row.loop_run_id ?? row.loop_id ?? row.goal_id}`;
5730
+ const requestId = `loops-${row.goal_run_id}`;
5731
+ const timestamp = row.updated_at ?? row.created_at ?? row.loop_run_started_at ?? new Date().toISOString();
5732
+ const stateValue = `${LOOPS_INGEST_VERSION}:${row.updated_at ?? ""}:${row.tokens_used}:${model}:${costCenterId}`;
5733
+ if (getIngestState(db, "loops", row.goal_run_id) === stateValue)
5734
+ continue;
5735
+ upsertCostCenter(db, {
5736
+ id: costCenterId,
5737
+ kind: "loop",
5738
+ name: loopName,
5739
+ repo_path: null,
5740
+ labels_json: row.labels_json ?? "{}",
5741
+ created_at: row.created_at ?? timestamp
5742
+ });
5743
+ const account = accountFields(row);
5744
+ upsertSession(db, {
5745
+ id: sessionId,
5746
+ agent: "loop",
5747
+ project_path: "",
5748
+ project_name: loopName,
5749
+ started_at: row.loop_run_started_at ?? row.created_at ?? timestamp,
5750
+ ended_at: row.updated_at ?? null,
5751
+ total_cost_usd: 0,
5752
+ total_tokens: 0,
5753
+ request_count: 0,
5754
+ machine_id: machineId,
5755
+ cost_center_id: costCenterId,
5756
+ attribution_tag: "loops-orchestration",
5757
+ ...account
5758
+ });
5759
+ const inputTokens = row.tokens_used;
5760
+ const outputTokens = 0;
5761
+ upsertRequest(db, {
5762
+ id: requestId,
5763
+ agent: "loop",
5764
+ session_id: sessionId,
5765
+ model,
5766
+ input_tokens: inputTokens,
5767
+ output_tokens: outputTokens,
5768
+ cache_read_tokens: 0,
5769
+ cache_create_tokens: 0,
5770
+ cost_usd: computeCostFromDb(db, model, inputTokens, outputTokens),
5771
+ cost_basis: "estimated",
5772
+ duration_ms: 0,
5773
+ timestamp,
5774
+ source_request_id: requestId,
5775
+ machine_id: machineId,
5776
+ cost_center_id: costCenterId,
5777
+ attribution_tag: "loops-orchestration",
5778
+ ...account
5779
+ });
5780
+ sessions.add(sessionId);
5781
+ requests++;
5782
+ setIngestState(db, "loops", row.goal_run_id, stateValue);
5783
+ if (verbose)
5784
+ console.log(`OpenLoops ${loopName}: ${row.tokens_used} orchestration tokens`);
5785
+ }
5786
+ } catch (error) {
5787
+ if (verbose) {
5788
+ const message = error instanceof Error ? error.message : String(error);
5789
+ console.log("OpenLoops ingest skipped:", message);
5790
+ }
5791
+ return { sessions: 0, requests: 0 };
5792
+ } finally {
5793
+ loopsDb?.close();
5794
+ }
5795
+ for (const sessionId of sessions)
5796
+ rollupSession(db, sessionId);
5797
+ return { sessions: sessions.size, requests };
5798
+ }
5799
+ var DEFAULT_LOOPS_DB_PATH, LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
5800
+ var init_loops = __esm(() => {
5801
+ init_database();
5802
+ init_pricing();
5803
+ DEFAULT_LOOPS_DB_PATH = join12(homedir9(), ".hasna", "loops", "loops.db");
5804
+ });
5805
+
5371
5806
  // src/ingest/claude-quota.ts
5372
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
5807
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
5373
5808
  function readClaudeToken() {
5374
5809
  const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
5375
5810
  if (fromEnv)
5376
5811
  return { token: fromEnv };
5377
- if (!existsSync10(CREDENTIALS_PATH))
5812
+ if (!existsSync11(CREDENTIALS_PATH))
5378
5813
  return null;
5379
5814
  try {
5380
5815
  const creds = JSON.parse(readFileSync8(CREDENTIALS_PATH, "utf-8"));
@@ -5518,13 +5953,13 @@ var init_claude_quota = __esm(() => {
5518
5953
  });
5519
5954
 
5520
5955
  // src/ingest/codex-quota.ts
5521
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
5956
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
5522
5957
  function readCodexAuth() {
5523
5958
  const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
5524
5959
  if (fromEnv)
5525
5960
  return { token: fromEnv, authMode: "chatgpt" };
5526
5961
  const authPath = agentPaths().codexAuth;
5527
- if (!existsSync11(authPath))
5962
+ if (!existsSync12(authPath))
5528
5963
  return null;
5529
5964
  try {
5530
5965
  const auth = JSON.parse(readFileSync9(authPath, "utf-8"));
@@ -5662,7 +6097,7 @@ var init_codex_quota = __esm(() => {
5662
6097
 
5663
6098
  // src/lib/sync-all.ts
5664
6099
  async function syncAll(db, opts = {}) {
5665
- const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
6100
+ const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes || opts.loops);
5666
6101
  const all = !anySpecific;
5667
6102
  const result = { deduped: 0 };
5668
6103
  if (all || opts.claude) {
@@ -5685,6 +6120,8 @@ async function syncAll(db, opts = {}) {
5685
6120
  result.pi = await ingestPi(db, opts.verbose);
5686
6121
  if (all || opts.hermes)
5687
6122
  result.hermes = await ingestHermes(db, opts.verbose);
6123
+ if (all || opts.loops)
6124
+ result.loops = await ingestLoops(db, opts.verbose);
5688
6125
  if (opts.dedupe !== false)
5689
6126
  result.deduped = dedupeRequests(db);
5690
6127
  return result;
@@ -5697,6 +6134,7 @@ var init_sync_all = __esm(() => {
5697
6134
  init_cursor();
5698
6135
  init_pi();
5699
6136
  init_hermes();
6137
+ init_loops();
5700
6138
  init_claude_quota();
5701
6139
  init_codex_quota();
5702
6140
  init_database();
@@ -5977,13 +6415,14 @@ async function syncOpenProjectsRegistry(db, listActiveProjects) {
5977
6415
  let imported = 0;
5978
6416
  let skipped = 0;
5979
6417
  for (const project of projects) {
5980
- if (!project.path) {
6418
+ const path = resolveProjectPath(project);
6419
+ if (!path) {
5981
6420
  skipped++;
5982
6421
  continue;
5983
6422
  }
5984
6423
  upsertProject(db, {
5985
6424
  id: project.id,
5986
- path: project.path,
6425
+ path,
5987
6426
  name: project.name,
5988
6427
  description: project.description,
5989
6428
  tags: project.tags ?? [],
@@ -5993,6 +6432,7 @@ async function syncOpenProjectsRegistry(db, listActiveProjects) {
5993
6432
  }
5994
6433
  return { imported, skipped };
5995
6434
  }
6435
+ var resolveProjectPath = (project) => project.primary_path || project.path || "";
5996
6436
  var init_open_projects = __esm(() => {
5997
6437
  init_database();
5998
6438
  });
@@ -6005,15 +6445,15 @@ __export(exports_config, {
6005
6445
  loadConfig: () => loadConfig2,
6006
6446
  getConfigValue: () => getConfigValue
6007
6447
  });
6008
- import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
6009
- import { dirname as dirname2, join as join12 } from "path";
6448
+ import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
6449
+ import { dirname as dirname2, join as join13 } from "path";
6010
6450
  function getConfigPath() {
6011
- return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join12(getDataDir(), "config.json");
6451
+ return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join13(getDataDir(), "config.json");
6012
6452
  }
6013
6453
  function loadConfig2() {
6014
6454
  try {
6015
6455
  const configPath = getConfigPath();
6016
- if (existsSync12(configPath)) {
6456
+ if (existsSync13(configPath)) {
6017
6457
  const raw = readFileSync11(configPath, "utf-8");
6018
6458
  return { ...DEFAULTS, ...JSON.parse(raw) };
6019
6459
  }
@@ -6023,7 +6463,7 @@ function loadConfig2() {
6023
6463
  function saveConfig2(config) {
6024
6464
  const configPath = getConfigPath();
6025
6465
  const dir = dirname2(configPath);
6026
- if (!existsSync12(dir))
6466
+ if (!existsSync13(dir))
6027
6467
  mkdirSync3(dir, { recursive: true });
6028
6468
  writeFileSync2(configPath, JSON.stringify(config, null, 2) + `
6029
6469
  `);
@@ -6124,6 +6564,8 @@ async function checkAndFireWebhooks(db) {
6124
6564
  event: "budget_alert",
6125
6565
  budget_id: b.id,
6126
6566
  project: b.project_path ?? "global",
6567
+ cost_center_id: b.cost_center_id ?? null,
6568
+ scope: b.cost_center_id ?? b.project_path ?? "global",
6127
6569
  period: b.period,
6128
6570
  spend: b.current_spend_usd,
6129
6571
  limit: b.limit_usd,
@@ -6167,7 +6609,7 @@ var init_webhooks = __esm(() => {
6167
6609
  });
6168
6610
 
6169
6611
  // src/lib/watch-paths.ts
6170
- import { existsSync as existsSync13 } from "fs";
6612
+ import { existsSync as existsSync14 } from "fs";
6171
6613
  function getWatchPaths() {
6172
6614
  const p = agentPaths();
6173
6615
  const candidates = [
@@ -6180,7 +6622,7 @@ function getWatchPaths() {
6180
6622
  p.piSessions,
6181
6623
  p.hermesDir
6182
6624
  ];
6183
- return candidates.filter((path) => existsSync13(path));
6625
+ return candidates.filter((path) => existsSync14(path));
6184
6626
  }
6185
6627
  var init_watch_paths = __esm(() => {
6186
6628
  init_paths();
@@ -6355,12 +6797,48 @@ var init_watch = __esm(() => {
6355
6797
  init_notification();
6356
6798
  });
6357
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
+
6358
6837
  // src/db/sync-pg.ts
6359
6838
  import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
6360
6839
  import { fileURLToPath } from "url";
6361
- import { dirname as dirname3, join as join13 } from "path";
6362
- import { existsSync as existsSync14 } from "fs";
6363
- import { translateSql, translateParams } from "@hasna/cloud";
6840
+ import { dirname as dirname3, join as join14 } from "path";
6841
+ import { existsSync as existsSync15 } from "fs";
6364
6842
  function translateSqliteDates(sql) {
6365
6843
  let s = sql;
6366
6844
  const isoDate = (expr) => `to_char(${expr}, 'YYYY-MM-DD')`;
@@ -6385,12 +6863,12 @@ function translateSqliteDates(sql) {
6385
6863
  function resolveWorkerPath() {
6386
6864
  const here = dirname3(fileURLToPath(import.meta.url));
6387
6865
  const candidates = [
6388
- join13(here, "pg-sync-worker.ts"),
6389
- join13(here, "pg-sync-worker.js"),
6390
- join13(here, "..", "db", "pg-sync-worker.js")
6866
+ join14(here, "pg-sync-worker.ts"),
6867
+ join14(here, "pg-sync-worker.js"),
6868
+ join14(here, "..", "db", "pg-sync-worker.js")
6391
6869
  ];
6392
6870
  for (const candidate of candidates)
6393
- if (existsSync14(candidate))
6871
+ if (existsSync15(candidate))
6394
6872
  return candidate;
6395
6873
  return candidates[0];
6396
6874
  }
@@ -6482,11 +6960,18 @@ class SyncPgAdapter {
6482
6960
  }
6483
6961
  var init_sync_pg = () => {};
6484
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
+
6485
6970
  // src/db/pg-migrations.ts
6486
6971
  var init_pg_migrations = () => {};
6487
6972
 
6488
6973
  // src/db/cloud.ts
6489
- import pg from "pg";
6974
+ import pg2 from "pg";
6490
6975
  function getCloudDatabaseUrl() {
6491
6976
  return process.env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || process.env["ECONOMY_DATABASE_URL"]?.trim() || process.env["DATABASE_URL"]?.trim() || undefined;
6492
6977
  }
@@ -6510,8 +6995,7 @@ function openCloudDatabase(dsn = getCloudDatabaseUrl()) {
6510
6995
  function createCloudPool(dsn = getCloudDatabaseUrl()) {
6511
6996
  if (!dsn)
6512
6997
  throw new Error("economy cloud mode requires a Postgres DSN (HASNA_ECONOMY_DATABASE_URL)");
6513
- const ssl = dsn.includes("sslmode=require") || dsn.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
6514
- 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 });
6515
6999
  pool.on("error", (err) => {
6516
7000
  console.error(JSON.stringify({ evt: "pg_pool_error", message: err.message }));
6517
7001
  });
@@ -6534,6 +7018,7 @@ function authClientFromPool(pool) {
6534
7018
  }
6535
7019
  var init_cloud = __esm(() => {
6536
7020
  init_sync_pg();
7021
+ init_pg_migrate();
6537
7022
  init_pg_migrations();
6538
7023
  });
6539
7024
 
@@ -7302,6 +7787,15 @@ var init_openapi = __esm(() => {
7302
7787
  type: "string"
7303
7788
  }
7304
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
+ },
7305
7799
  {
7306
7800
  name: "machine",
7307
7801
  in: "query",
@@ -8144,7 +8638,7 @@ __export(exports_serve, {
8144
8638
  createHandler: () => createHandler
8145
8639
  });
8146
8640
  import { randomUUID as randomUUID2 } from "crypto";
8147
- import { existsSync as existsSync15 } from "fs";
8641
+ import { existsSync as existsSync16 } from "fs";
8148
8642
  import { resolve, sep } from "path";
8149
8643
  import { verifyApiKey, ApiKeyStore } from "@hasna/contracts/auth";
8150
8644
  function openApiDocument() {
@@ -8227,13 +8721,13 @@ function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
8227
8721
  if (url.pathname.startsWith("/api") || url.pathname.startsWith("/v1") || FOUNDATION_PATHS.has(url.pathname)) {
8228
8722
  return apiHandler(req);
8229
8723
  }
8230
- if (existsSync15(dashboardDir)) {
8724
+ if (existsSync16(dashboardDir)) {
8231
8725
  const filePath = dashboardPath(dashboardDir, url.pathname);
8232
- if (filePath && existsSync15(filePath)) {
8726
+ if (filePath && existsSync16(filePath)) {
8233
8727
  return new Response(Bun.file(filePath));
8234
8728
  }
8235
8729
  const indexPath = dashboardPath(dashboardDir, "/");
8236
- if (indexPath && existsSync15(indexPath)) {
8730
+ if (indexPath && existsSync16(indexPath)) {
8237
8731
  return new Response(Bun.file(indexPath));
8238
8732
  }
8239
8733
  }
@@ -8395,13 +8889,18 @@ function createHandler(db, options = {}) {
8395
8889
  if (path === "/api/breakdown" && method === "GET") {
8396
8890
  const by = url.searchParams.get("by") ?? "model";
8397
8891
  const period = url.searchParams.get("period") ?? "all";
8892
+ const since = url.searchParams.get("since") ?? undefined;
8398
8893
  const machine = url.searchParams.get("machine") ?? undefined;
8399
8894
  if (by === "project")
8400
- return ok(queryProjectBreakdown(db, period, machine));
8895
+ return ok(since ? queryProjectBreakdownSince(db, since, machine) : queryProjectBreakdown(db, period, machine));
8401
8896
  if (by === "agent")
8402
8897
  return ok(queryAgentBreakdown(db, period, machine));
8403
8898
  if (by === "account")
8404
8899
  return ok(queryAccountBreakdown(db, period, machine));
8900
+ if (by === "cost-center")
8901
+ return ok(queryCostCenterBreakdown(db, period, { machine }));
8902
+ if (["loop", "app", "repo", "service", "team"].includes(by))
8903
+ return ok(queryCostCenterBreakdown(db, period, { kind: by, machine }));
8405
8904
  return ok(queryModelBreakdown(db));
8406
8905
  }
8407
8906
  if (path === "/api/budgets" && method === "GET") {
@@ -8425,6 +8924,7 @@ function createHandler(db, options = {}) {
8425
8924
  id: randomUUID2(),
8426
8925
  project_path: body["project_path"] ?? null,
8427
8926
  agent,
8927
+ cost_center_id: optionalString(body["cost_center_id"]) ?? null,
8428
8928
  period: normalizeBudgetPeriod(body["period"]),
8429
8929
  limit_usd: limitUsd,
8430
8930
  alert_at_percent: alertAtPercent,
@@ -8766,7 +9266,7 @@ var init_serve = __esm(() => {
8766
9266
  };
8767
9267
  ECONOMY_WRITE_SCOPE = `${APP}:write`;
8768
9268
  AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
8769
- SYNC_SOURCES = ["all", ...AGENTS];
9269
+ SYNC_SOURCES = ["all", ...AGENTS, "loops"];
8770
9270
  DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
8771
9271
  FOUNDATION_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz", "/version", "/openapi.json"]);
8772
9272
  });
@@ -8781,14 +9281,14 @@ __export(exports_menubar, {
8781
9281
  });
8782
9282
  import chalk6 from "chalk";
8783
9283
  import { execFileSync as execFileSync3 } from "child_process";
8784
- import { cpSync, existsSync as existsSync16, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
9284
+ import { cpSync, existsSync as existsSync17, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
8785
9285
  import { tmpdir, arch } from "os";
8786
- import { join as join14 } from "path";
9286
+ import { join as join15 } from "path";
8787
9287
  function getArch() {
8788
9288
  return arch() === "arm64" ? "arm64" : "x86_64";
8789
9289
  }
8790
9290
  function isInstalled() {
8791
- return existsSync16(APP_PATH);
9291
+ return existsSync17(APP_PATH);
8792
9292
  }
8793
9293
  function isRunning() {
8794
9294
  try {
@@ -8825,8 +9325,8 @@ async function menubarInstall(opts) {
8825
9325
  console.error(chalk6.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
8826
9326
  process.exit(1);
8827
9327
  }
8828
- const zipPath = join14(tmpdir(), `economy-bar-${cpuArch}.zip`);
8829
- const extractDir = join14(tmpdir(), "economy-bar-extracted");
9328
+ const zipPath = join15(tmpdir(), `economy-bar-${cpuArch}.zip`);
9329
+ const extractDir = join15(tmpdir(), "economy-bar-extracted");
8830
9330
  console.log(chalk6.cyan(`\u2192 Downloading ${assetUrl}...`));
8831
9331
  try {
8832
9332
  const res = await fetch(assetUrl, { signal: AbortSignal.timeout(60000) });
@@ -8846,7 +9346,7 @@ async function menubarInstall(opts) {
8846
9346
  execFileSync3("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "ignore" });
8847
9347
  if (isInstalled())
8848
9348
  rmSync(APP_PATH, { recursive: true, force: true });
8849
- cpSync(join14(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
9349
+ cpSync(join15(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
8850
9350
  try {
8851
9351
  execFileSync3("xattr", ["-rd", "com.apple.quarantine", APP_PATH], { stdio: "ignore" });
8852
9352
  } catch {}
@@ -9645,7 +10145,7 @@ var ROADMAP_PHASES = [
9645
10145
  {
9646
10146
  id: "phase-9",
9647
10147
  title: "Multi-machine auto sync (4-machine fleet)",
9648
- 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.",
9649
10149
  tasks: [
9650
10150
  {
9651
10151
  id: "9.1",
@@ -9660,7 +10160,7 @@ var ROADMAP_PHASES = [
9660
10160
  },
9661
10161
  {
9662
10162
  id: "9.3",
9663
- 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)",
9664
10164
  status: "done",
9665
10165
  deps: ["9.2"]
9666
10166
  },
@@ -9684,7 +10184,7 @@ var ROADMAP_PHASES = [
9684
10184
  },
9685
10185
  {
9686
10186
  id: "9.7",
9687
- 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)",
9688
10188
  status: "done",
9689
10189
  deps: ["9.5"]
9690
10190
  },
@@ -9950,6 +10450,8 @@ var ROADMAP_PHASES = [
9950
10450
  ]
9951
10451
  }
9952
10452
  ];
10453
+ var DEFAULT_TODO_LIST_LIMIT = 20;
10454
+ var TODO_TITLE_WIDTH = 140;
9953
10455
  function statusLabel(status) {
9954
10456
  if (status === "done")
9955
10457
  return chalk2.green("done");
@@ -9963,6 +10465,30 @@ function countByStatus(tasks) {
9963
10465
  return acc;
9964
10466
  }, { pending: 0, in_progress: 0, done: 0 });
9965
10467
  }
10468
+ function truncateText(value, max) {
10469
+ if (value.length <= max)
10470
+ return value;
10471
+ return `${value.slice(0, Math.max(0, max - 3)).trimEnd()}...`;
10472
+ }
10473
+ function parseListLimit(value) {
10474
+ if (value == null)
10475
+ return;
10476
+ const parsed = Number(value);
10477
+ if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
10478
+ console.error(chalk2.red("--limit must be a positive integer"));
10479
+ process.exit(1);
10480
+ }
10481
+ return parsed;
10482
+ }
10483
+ function matchingTasks(phases, status) {
10484
+ const rows = [];
10485
+ for (const phase of phases) {
10486
+ const tasks = status ? phase.tasks.filter((task) => task.status === status) : phase.tasks;
10487
+ for (const task of tasks)
10488
+ rows.push({ phase, task });
10489
+ }
10490
+ return rows;
10491
+ }
9966
10492
  function printTodosHelp() {
9967
10493
  console.log();
9968
10494
  console.log(chalk2.bold.cyan(" Economy Roadmap \u2014 fleet, usage & savings"));
@@ -9972,14 +10498,16 @@ function printTodosHelp() {
9972
10498
  console.log();
9973
10499
  console.log(chalk2.bold(" Commands"));
9974
10500
  console.log(" economy todos Overview of phases and progress");
9975
- console.log(" economy todos list All granular tasks");
10501
+ console.log(" economy todos list Compact task list (first 20 by default)");
10502
+ console.log(" economy todos list --verbose Full task list with dependencies");
10503
+ console.log(" economy todos list --json Machine-readable roadmap payload");
9976
10504
  console.log(" economy todos list --phase <id> Tasks for one phase (e.g. phase-9)");
9977
10505
  console.log(" economy todos show <task-id> Task detail (e.g. 9.7)");
9978
10506
  console.log();
9979
- console.log(chalk2.bold(" Multi-machine (current vs target)"));
9980
- console.log(" today machine_id on rows, economy machines, manual cloud push/pull/sync");
9981
- console.log(" gap no auto-sync, no updated_at, hardcoded RDS, queries are local-only");
9982
- 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");
9983
10511
  console.log();
9984
10512
  console.log(chalk2.bold(" Agents covered"));
9985
10513
  console.log(" claude Claude Code telemetry + Pro/Max quota (partial)");
@@ -10014,7 +10542,8 @@ function printTodosOverview() {
10014
10542
  console.log(chalk2.dim(` ${phase.summary}`));
10015
10543
  }
10016
10544
  console.log();
10017
- console.log(chalk2.dim(" Run: economy todos list"));
10545
+ console.log(chalk2.dim(" Run: economy todos list --limit 20"));
10546
+ console.log(chalk2.dim(" Run: economy todos list --verbose"));
10018
10547
  console.log(chalk2.dim(" Run: economy todos --help"));
10019
10548
  console.log();
10020
10549
  }
@@ -10025,21 +10554,55 @@ function printTodosList(opts) {
10025
10554
  console.log(chalk2.dim(`Valid phases: ${ROADMAP_PHASES.map((phase) => phase.id).join(", ")}`));
10026
10555
  return;
10027
10556
  }
10557
+ const allMatches = matchingTasks(phases, opts.status);
10558
+ const limit = opts.limit ?? (opts.verbose ? Number.POSITIVE_INFINITY : DEFAULT_TODO_LIST_LIMIT);
10559
+ const visible = allMatches.slice(0, limit);
10560
+ if (opts.json) {
10561
+ const filteredPhases = phases.map((phase) => ({
10562
+ ...phase,
10563
+ tasks: opts.status ? phase.tasks.filter((task) => task.status === opts.status) : phase.tasks
10564
+ })).filter((phase) => phase.tasks.length > 0);
10565
+ console.log(JSON.stringify({
10566
+ version: ROADMAP_VERSION,
10567
+ goal: ROADMAP_GOAL,
10568
+ total: allMatches.length,
10569
+ phases: filteredPhases
10570
+ }, null, 2));
10571
+ return;
10572
+ }
10573
+ console.log();
10574
+ console.log(chalk2.bold.cyan(" Economy Roadmap Tasks"));
10575
+ console.log(chalk2.dim(` ${allMatches.length} matching tasks \xB7 showing ${visible.length}${visible.length < allMatches.length ? ` of ${allMatches.length}` : ""}`));
10576
+ if (!opts.verbose && visible.length < allMatches.length) {
10577
+ console.log(chalk2.dim(" Use --verbose for all tasks or economy todos show <task-id> for details."));
10578
+ }
10028
10579
  console.log();
10580
+ const visibleByPhase = new Map;
10581
+ for (const { phase, task } of visible) {
10582
+ const tasks = visibleByPhase.get(phase.id) ?? [];
10583
+ tasks.push(task);
10584
+ visibleByPhase.set(phase.id, tasks);
10585
+ }
10029
10586
  for (const phase of phases) {
10030
- const tasks = opts.status ? phase.tasks.filter((task) => task.status === opts.status) : phase.tasks;
10587
+ const tasks = visibleByPhase.get(phase.id) ?? [];
10031
10588
  if (tasks.length === 0)
10032
10589
  continue;
10033
10590
  console.log(chalk2.bold.cyan(` ${phase.id} \u2014 ${phase.title}`));
10034
- console.log(chalk2.dim(` ${phase.summary}`));
10591
+ if (opts.verbose)
10592
+ console.log(chalk2.dim(` ${phase.summary}`));
10035
10593
  console.log();
10036
10594
  for (const task of tasks) {
10037
10595
  const deps = task.deps?.length ? chalk2.dim(` deps: ${task.deps.join(", ")}`) : "";
10038
- console.log(` ${chalk2.dim(task.id.padEnd(5))} ${statusLabel(task.status).padEnd(14)} ${task.title}${deps ? `
10596
+ const title = opts.verbose ? task.title : truncateText(task.title, TODO_TITLE_WIDTH);
10597
+ console.log(` ${chalk2.dim(task.id.padEnd(5))} ${statusLabel(task.status).padEnd(14)} ${title}${opts.verbose && deps ? `
10039
10598
  ${" ".repeat(22)}${deps}` : ""}`);
10040
10599
  }
10041
10600
  console.log();
10042
10601
  }
10602
+ if (visible.length < allMatches.length) {
10603
+ console.log(chalk2.dim(` ... ${allMatches.length - visible.length} more tasks hidden. Use --limit <n>, --verbose, --phase <id>, or economy todos show <task-id>.`));
10604
+ console.log();
10605
+ }
10043
10606
  }
10044
10607
  function printTodoShow(taskId) {
10045
10608
  for (const phase of ROADMAP_PHASES) {
@@ -10069,13 +10632,19 @@ function registerTodosCommand(program) {
10069
10632
  }
10070
10633
  printTodosOverview();
10071
10634
  });
10072
- todosCmd.command("list").description("List granular roadmap tasks").option("--phase <id>", "Filter by phase id (e.g. phase-4)").option("--status <status>", "Filter by status: pending|in_progress|done").action((opts) => {
10635
+ todosCmd.command("list").description("List granular roadmap tasks").option("--phase <id>", "Filter by phase id (e.g. phase-4)").option("--status <status>", "Filter by status: pending|in_progress|done").option("--limit <n>", "Maximum tasks to print (default: 20)").option("--verbose", "Print all matching tasks with phase summaries and dependencies").option("--json", "Output the full filtered roadmap as JSON").action((opts) => {
10073
10636
  const status = opts.status;
10074
10637
  if (status && !["pending", "in_progress", "done"].includes(status)) {
10075
10638
  console.error(chalk2.red("--status must be one of: pending, in_progress, done"));
10076
10639
  process.exit(1);
10077
10640
  }
10078
- printTodosList({ phase: opts.phase, status });
10641
+ printTodosList({
10642
+ phase: opts.phase,
10643
+ status,
10644
+ limit: parseListLimit(opts.limit),
10645
+ verbose: Boolean(opts.verbose),
10646
+ json: Boolean(opts.json)
10647
+ });
10079
10648
  });
10080
10649
  todosCmd.command("show <task-id>").description("Show a single task (e.g. 4.2)").action((taskId) => {
10081
10650
  printTodoShow(taskId);
@@ -10259,8 +10828,18 @@ function parsePeriod(value, fallback = "month") {
10259
10828
  }
10260
10829
  return p;
10261
10830
  }
10831
+ function parseLimit(value, fallback, verbose) {
10832
+ if (verbose && value == null)
10833
+ return Number.POSITIVE_INFINITY;
10834
+ const parsed = Number(value ?? String(fallback));
10835
+ if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
10836
+ console.error(chalk4.red("--limit must be a positive integer"));
10837
+ process.exit(1);
10838
+ }
10839
+ return parsed;
10840
+ }
10262
10841
  function registerExtendedCommands(program) {
10263
- program.command("usage [period]").description("Show subscription usage quotas and consumption metrics").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--json", "Output JSON").action(async (periodArg, opts) => {
10842
+ program.command("usage [period]").description("Show subscription usage quotas and consumption metrics").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--limit <n>", "Maximum snapshot rows to print (default: 20)").option("--verbose", "Show all snapshot rows").option("--json", "Output JSON").action(async (periodArg, opts) => {
10264
10843
  const period = parsePeriod(periodArg, "month");
10265
10844
  const agent = parseAgent(opts.agent, "--agent");
10266
10845
  const { snapshots: snaps, summary } = await getStore().usage(period, agent);
@@ -10274,16 +10853,26 @@ function registerExtendedCommands(program) {
10274
10853
  if (snaps.length === 0) {
10275
10854
  console.log(chalk4.yellow(" No usage snapshots yet. Run: economy sync --cursor"));
10276
10855
  } else {
10277
- for (const s of snaps) {
10856
+ const limit = parseLimit(opts.limit, 20, opts.verbose);
10857
+ const visibleSnaps = snaps.slice(0, limit);
10858
+ if (visibleSnaps.length < snaps.length) {
10859
+ console.log(chalk4.dim(` ${snaps.length} snapshots \xB7 showing ${visibleSnaps.length}`));
10860
+ console.log();
10861
+ }
10862
+ for (const s of visibleSnaps) {
10278
10863
  console.log(` ${chalk4.white(s.agent.padEnd(10))} ${s.metric.padEnd(28)} ${s.value}${s.unit ? ` ${s.unit}` : ""} ${chalk4.dim(s.date)}`);
10279
10864
  }
10865
+ if (visibleSnaps.length < snaps.length) {
10866
+ console.log(chalk4.dim(`
10867
+ ... ${snaps.length - visibleSnaps.length} more snapshots hidden. Use --limit <n>, --verbose, or --json.`));
10868
+ }
10280
10869
  }
10281
10870
  const spendLabel = agent ? `${agent} API-equivalent spend:` : "Fleet API-equivalent spend:";
10282
10871
  console.log(`
10283
10872
  ${chalk4.dim(spendLabel)} ${fmt2(summary.total_usd)}`);
10284
10873
  console.log();
10285
10874
  });
10286
- 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) => {
10287
10876
  const period = parsePeriod(periodArg, "month");
10288
10877
  const agent = parseAgent(opts.agent, "--agent");
10289
10878
  const savings = await getStore().savings(period, agent);
@@ -10302,7 +10891,7 @@ function registerExtendedCommands(program) {
10302
10891
  console.log();
10303
10892
  });
10304
10893
  const subsCmd = program.command("subscriptions").description("Manage subscription plans");
10305
- 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) => {
10306
10895
  const agent = parseAgent(opts.agent, "--agent") ?? null;
10307
10896
  await getStore().setSubscription({
10308
10897
  agent,
@@ -10316,7 +10905,7 @@ function registerExtendedCommands(program) {
10316
10905
  });
10317
10906
  console.log(chalk4.green(`\u2713 Subscription set: ${opts.provider} / ${opts.plan}`));
10318
10907
  });
10319
- subsCmd.command("list").description("List subscription plans").option("--json", "Output JSON").action(async (opts) => {
10908
+ subsCmd.command("list").description("List subscription plans").option("--limit <n>", "Maximum subscription rows to print (default: 20)").option("--verbose", "Show all subscription rows").option("--json", "Output JSON").action(async (opts) => {
10320
10909
  const rows = await getStore().listSubscriptions();
10321
10910
  if (opts.json) {
10322
10911
  console.log(JSON.stringify(rows, null, 2));
@@ -10326,10 +10915,20 @@ function registerExtendedCommands(program) {
10326
10915
  console.log(chalk4.yellow("No subscriptions configured."));
10327
10916
  return;
10328
10917
  }
10918
+ const limit = parseLimit(opts.limit, 20, opts.verbose);
10919
+ const visibleRows = rows.slice(0, limit);
10329
10920
  console.log();
10330
- for (const r of rows) {
10921
+ if (visibleRows.length < rows.length) {
10922
+ console.log(chalk4.dim(` ${rows.length} subscriptions \xB7 showing ${visibleRows.length}`));
10923
+ console.log();
10924
+ }
10925
+ for (const r of visibleRows) {
10331
10926
  console.log(` ${chalk4.white(r.provider)} / ${r.plan} ${fmt2(r.monthly_fee_usd)}/mo included ${fmt2(r.included_usage_usd)} ${r.agent ?? "all agents"}`);
10332
10927
  }
10928
+ if (visibleRows.length < rows.length) {
10929
+ console.log(chalk4.dim(`
10930
+ ... ${rows.length - visibleRows.length} more subscriptions hidden. Use --limit <n>, --verbose, or --json.`));
10931
+ }
10333
10932
  console.log();
10334
10933
  });
10335
10934
  subsCmd.command("remove <id>").description("Remove subscription by id").action(async (id) => {
@@ -10453,20 +11052,29 @@ function registerExtendedCommands(program) {
10453
11052
  }
10454
11053
  }
10455
11054
  function registerFleetCommands(program) {
10456
- program.command("fleet").description("Fleet-wide summaries across all machines").option("--period <p>", "Period", "today").option("--json", "Output JSON").action(async (opts) => {
11055
+ program.command("fleet").description("Fleet-wide summaries across all machines").option("--period <p>", "Period", "today").option("--limit <n>", "Maximum machine rows to print (default: 20)").option("--verbose", "Show all machine rows").option("--json", "Output JSON").action(async (opts) => {
10457
11056
  const period = parsePeriod(opts.period, "today");
10458
11057
  const { summary, machines, registry } = await getStore().fleet(period);
10459
11058
  if (opts.json) {
10460
11059
  console.log(JSON.stringify({ period, summary, machines, registry }, null, 2));
10461
11060
  return;
10462
11061
  }
11062
+ const limit = parseLimit(opts.limit, 20, opts.verbose);
11063
+ const visibleMachines = machines.slice(0, limit);
10463
11064
  console.log();
10464
11065
  console.log(chalk4.bold.cyan(` Fleet \u2014 ${period}`));
10465
11066
  console.log(` Total: ${fmt2(summary.total_usd)} \xB7 ${summary.sessions} sessions \xB7 ${summary.requests} requests`);
11067
+ if (visibleMachines.length < machines.length) {
11068
+ console.log(chalk4.dim(` ${machines.length} machines \xB7 showing ${visibleMachines.length}`));
11069
+ }
10466
11070
  console.log();
10467
- for (const m of machines) {
11071
+ for (const m of visibleMachines) {
10468
11072
  console.log(` ${chalk4.white(m.machine_id.padEnd(12))} ${fmt2(m.total_cost_usd).padEnd(10)} ${m.sessions} sessions`);
10469
11073
  }
11074
+ if (visibleMachines.length < machines.length) {
11075
+ console.log(chalk4.dim(`
11076
+ ... ${machines.length - visibleMachines.length} more machines hidden. Use --limit <n>, --verbose, or --json.`));
11077
+ }
10470
11078
  console.log();
10471
11079
  });
10472
11080
  program.command("completion <shell>").description("Print shell completion script (bash, zsh, or fish)").action((shell) => {
@@ -10614,7 +11222,7 @@ init_store();
10614
11222
  init_transport();
10615
11223
  import { execSync } from "child_process";
10616
11224
  var program = new Command;
10617
- 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);
10618
11226
  async function autoSync(opts = {}) {
10619
11227
  if (isCloudStore())
10620
11228
  return;
@@ -10721,6 +11329,16 @@ function parseOptionalCliAgent(value) {
10721
11329
  fail(e instanceof Error ? e.message : String(e));
10722
11330
  }
10723
11331
  }
11332
+ function resolveRowLimit(value, verbose, fallback) {
11333
+ if (verbose && value == null)
11334
+ return Number.POSITIVE_INFINITY;
11335
+ return parsePositiveCliInteger(value ?? String(fallback), "--limit");
11336
+ }
11337
+ function printHiddenRowsHint(total, shown, detail) {
11338
+ if (total <= shown)
11339
+ return;
11340
+ console.log(chalk7.dim(` ... ${total - shown} more rows hidden. ${detail}`));
11341
+ }
10724
11342
  function requireCliChoice(value, option, allowed) {
10725
11343
  const selected = value ?? allowed[0];
10726
11344
  if (allowed.includes(selected))
@@ -10820,14 +11438,14 @@ program.action(async () => {
10820
11438
  }
10821
11439
  console.log();
10822
11440
  });
10823
- program.command("sync").description("Ingest cost data from all coding agents").option("--claude", "Only ingest Claude Code telemetry").option("--takumi", "Only ingest Takumi sessions").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("--opencode", "Only ingest OpenCode sessions").option("--cursor", "Only ingest Cursor usage").option("--pi", "Only ingest Pi sessions").option("--hermes", "Only ingest Hermes sessions").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").option("--backfill-machine", "Tag existing records that have no machine_id with current hostname").option("--recalculate", "Recalculate costs for all requests with cost_usd = 0").action(async (opts) => {
11441
+ program.command("sync").description("Ingest cost data from all coding agents").option("--claude", "Only ingest Claude Code telemetry").option("--takumi", "Only ingest Takumi sessions").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("--opencode", "Only ingest OpenCode sessions").option("--cursor", "Only ingest Cursor usage").option("--pi", "Only ingest Pi sessions").option("--hermes", "Only ingest Hermes sessions").option("--loops", "Only ingest OpenLoops orchestration/judge token usage").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").option("--backfill-machine", "Tag existing records that have no machine_id with current hostname").option("--recalculate", "Recalculate costs for all requests with cost_usd = 0").action(async (opts) => {
10824
11442
  if (isCloudStore()) {
10825
11443
  console.log(chalk7.yellow("cloud mode: ingest is a local-only operation; nothing to sync (reads/writes route to the cloud API)"));
10826
11444
  return;
10827
11445
  }
10828
11446
  const db = openDatabase();
10829
11447
  ensurePricingSeeded(db);
10830
- const anySpecific = opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes;
11448
+ const anySpecific = opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes || opts.loops;
10831
11449
  if (!anySpecific || opts.verbose) {
10832
11450
  try {
10833
11451
  const { syncOpenProjectsRegistry: syncOpenProjectsRegistry2 } = await Promise.resolve().then(() => (init_open_projects(), exports_open_projects));
@@ -10840,7 +11458,7 @@ program.command("sync").description("Ingest cost data from all coding agents").o
10840
11458
  }
10841
11459
  }
10842
11460
  if (opts.force) {
10843
- const ingestSources = [...AGENTS, "codex-codewith"];
11461
+ const ingestSources = [...AGENTS, "codex-codewith", "loops"];
10844
11462
  db.exec(`DELETE FROM ingest_state WHERE source IN (${ingestSources.map((a) => `'${a}'`).join(", ")})`);
10845
11463
  if (opts.verbose)
10846
11464
  console.log(chalk7.dim("Cleared ingest cache"));
@@ -10855,6 +11473,7 @@ program.command("sync").description("Ingest cost data from all coding agents").o
10855
11473
  cursor: opts.cursor,
10856
11474
  pi: opts.pi,
10857
11475
  hermes: opts.hermes,
11476
+ loops: opts.loops,
10858
11477
  verbose: opts.verbose
10859
11478
  });
10860
11479
  console.log(chalk7.green(`\u2713 deduped ${result.deduped}`));
@@ -10917,7 +11536,7 @@ program.command("month").description("Cost summary for this month").action(async
10917
11536
  await autoSync();
10918
11537
  await printSummary("This Month", "month");
10919
11538
  });
10920
- 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) => {
10921
11540
  const limit = parsePositiveCliInteger(opts.limit ?? "20", "--limit");
10922
11541
  const agent = parseOptionalCliAgent(opts.agent);
10923
11542
  await autoSync();
@@ -10964,7 +11583,7 @@ program.command("sessions").description("List coding sessions with costs").optio
10964
11583
  ]));
10965
11584
  console.log();
10966
11585
  });
10967
- 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) => {
10968
11587
  const count = parsePositiveCliInteger(opts.n ?? "10", "-n");
10969
11588
  const agent = parseOptionalCliAgent(opts.agent);
10970
11589
  const sinceDate = opts.since ? parseSinceDate2(opts.since) : undefined;
@@ -10984,22 +11603,36 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
10984
11603
  ]));
10985
11604
  console.log();
10986
11605
  });
10987
- program.command("breakdown").description("Cost breakdown by model, agent, project, or account").option("--by <dimension>", "Dimension: model|agent|project|account", "model").option("--since <date>", "Filter since date or relative (e.g. 2026-03-01, 7d, 30d)").action(async (opts) => {
11606
+ program.command("breakdown").description("Cost breakdown by model, agent, project, account, or cost center").option("--by <dimension>", "Dimension: model|agent|project|account|cost-center|loop|app|repo", "model").option("--since <date>", "Filter since date or relative (e.g. 2026-03-01, 7d, 30d)").option("--limit <n>", "Maximum breakdown rows to print (default: 20)").option("--verbose", "Show all breakdown rows").option("--json", "Output JSON").action(async (opts) => {
10988
11607
  const store = getStore();
11608
+ const by = requireCliChoice(opts.by, "--by", ["model", "agent", "project", "account", "cost-center", "loop", "app", "repo"]);
10989
11609
  const since = opts.since ? parseSinceDate2(opts.since) : undefined;
11610
+ const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
11611
+ const costCenterKinds = new Set(["loop", "app", "repo"]);
10990
11612
  console.log();
10991
- if (opts.by === "project") {
11613
+ if (by === "project") {
10992
11614
  const rows = await store.projectBreakdown({ since });
10993
- printTable(["Project", "Sessions", "Requests", "Tokens", "Cost"], rows.map((r) => [
11615
+ if (opts.json) {
11616
+ console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
11617
+ return;
11618
+ }
11619
+ const visibleRows = rows.slice(0, limit);
11620
+ printTable(["Project", "Sessions", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
10994
11621
  chalk7.white(r.project_name || chalk7.dim("unknown")),
10995
11622
  String(r.sessions),
10996
11623
  String(r.requests),
10997
11624
  chalk7.cyan(fmtTokens2(r.total_tokens)),
10998
11625
  fmt4(r.cost_usd)
10999
11626
  ]));
11000
- } else if (opts.by === "agent") {
11627
+ printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
11628
+ } else if (by === "agent") {
11001
11629
  const rows = await store.agentBreakdown({ since });
11002
- printTable(["Agent", "Sessions", "Requests", "Tokens", "API Eq", "Billable", "Included"], rows.map((r) => [
11630
+ if (opts.json) {
11631
+ console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
11632
+ return;
11633
+ }
11634
+ const visibleRows = rows.slice(0, limit);
11635
+ printTable(["Agent", "Sessions", "Requests", "Tokens", "API Eq", "Billable", "Included"], visibleRows.map((r) => [
11003
11636
  fmtAgent(r.agent),
11004
11637
  String(r.sessions),
11005
11638
  String(r.requests),
@@ -11008,22 +11641,57 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
11008
11641
  fmt4(r.billable_usd),
11009
11642
  fmt4(r.subscription_included_usd)
11010
11643
  ]));
11011
- } else if (opts.by === "account") {
11012
- printAccountBreakdown(await store.accountBreakdown({ since }));
11644
+ printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
11645
+ } else if (by === "account") {
11646
+ const rows = await store.accountBreakdown({ since });
11647
+ if (opts.json) {
11648
+ console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
11649
+ return;
11650
+ }
11651
+ const visibleRows = rows.slice(0, limit);
11652
+ printAccountBreakdown(visibleRows);
11653
+ printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
11654
+ } else if (by === "cost-center" || costCenterKinds.has(by)) {
11655
+ const kind = costCenterKinds.has(by) ? by : undefined;
11656
+ const rows = await store.costCenterBreakdown({ since, kind });
11657
+ if (opts.json) {
11658
+ console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
11659
+ return;
11660
+ }
11661
+ if (rows.length === 0) {
11662
+ console.log(chalk7.yellow("No cost-center usage yet. Run `economy sync --loops` or ingest app/service usage with /ingest."));
11663
+ } else {
11664
+ const visibleRows = rows.slice(0, limit);
11665
+ printTable(["Kind", "Cost Center", "Sessions", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
11666
+ chalk7.white(r.kind),
11667
+ chalk7.white(r.name),
11668
+ String(r.sessions),
11669
+ String(r.requests),
11670
+ chalk7.cyan(fmtTokens2(r.total_tokens)),
11671
+ fmt4(r.cost_usd)
11672
+ ]));
11673
+ printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
11674
+ }
11013
11675
  } else {
11014
11676
  const rows = await store.modelBreakdown({ since });
11015
- printTable(["Model", "Agent", "Requests", "Tokens", "Cost"], rows.map((r) => [
11677
+ if (opts.json) {
11678
+ console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
11679
+ return;
11680
+ }
11681
+ const visibleRows = rows.slice(0, limit);
11682
+ printTable(["Model", "Agent", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
11016
11683
  chalk7.white(r.model),
11017
11684
  fmtAgent(r.agent),
11018
11685
  String(r.requests),
11019
11686
  chalk7.cyan(fmtTokens2(r.total_tokens)),
11020
11687
  fmt4(r.cost_usd)
11021
11688
  ]));
11689
+ printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
11022
11690
  }
11023
11691
  console.log();
11024
11692
  });
11025
11693
  var ACCOUNT_PERIODS = ["today", "week", "month", "year", "all"];
11026
- program.command("accounts [period]").description("List account usage by email address and coding agent").option("--json", "Output JSON").action(async (periodArg, opts) => {
11694
+ program.command("accounts [period]").description("List account usage by email address and coding agent").option("--limit <n>", "Maximum account rows to print (default: 20)").option("--verbose", "Show all account rows").option("--json", "Output JSON").action(async (periodArg, opts) => {
11027
11695
  const period = requireCliChoice(periodArg, "period", ACCOUNT_PERIODS);
11028
11696
  const rows = await getStore().accounts(period);
11029
11697
  if (opts.json) {
@@ -11036,11 +11704,17 @@ program.command("accounts [period]").description("List account usage by email ad
11036
11704
  }
11037
11705
  console.log();
11038
11706
  console.log(chalk7.bold.cyan(` Accounts \u2014 ${period}`));
11707
+ const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
11708
+ const visibleRows = rows.slice(0, limit);
11709
+ if (visibleRows.length < rows.length) {
11710
+ console.log(chalk7.dim(` ${rows.length} rows \xB7 showing ${visibleRows.length}`));
11711
+ }
11039
11712
  console.log();
11040
- printAccountBreakdown(rows);
11713
+ printAccountBreakdown(visibleRows);
11714
+ printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
11041
11715
  console.log();
11042
11716
  });
11043
- 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) => {
11044
11718
  const { watchCosts: watchCosts2 } = await Promise.resolve().then(() => (init_watch(), exports_watch));
11045
11719
  await watchCosts2({
11046
11720
  interval: parsePositiveCliInteger(opts.interval ?? "10", "--interval"),
@@ -11050,7 +11724,7 @@ program.command("watch").description("Live stream of incoming costs").option("--
11050
11724
  });
11051
11725
  });
11052
11726
  var budgetCmd = program.command("budget").description("Manage spending budgets");
11053
- budgetCmd.command("set").description("Set a budget").option("--project <path>", "Project path (omit for global)").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) => {
11054
11728
  const limitUsd = parsePositiveCliNumber(opts.limit, "--limit");
11055
11729
  const alertAtPercent = parsePositiveCliNumber(opts.alert ?? "80", "--alert");
11056
11730
  if (alertAtPercent > 100)
@@ -11060,25 +11734,32 @@ budgetCmd.command("set").description("Set a budget").option("--project <path>",
11060
11734
  await getStore().setBudget({
11061
11735
  project_path: opts.project ?? null,
11062
11736
  agent,
11737
+ cost_center_id: opts.costCenter ?? null,
11063
11738
  period,
11064
11739
  limit_usd: limitUsd,
11065
11740
  alert_at_percent: alertAtPercent
11066
11741
  });
11067
- console.log(chalk7.green(`\u2713 Budget set: ${opts.project ?? "global"} \u2014 ${period} $${limitUsd}`));
11742
+ console.log(chalk7.green(`\u2713 Budget set: ${opts.costCenter ?? opts.project ?? "global"} \u2014 ${period} $${limitUsd}`));
11068
11743
  });
11069
- budgetCmd.command("list").description("List all budgets").action(async () => {
11744
+ budgetCmd.command("list").description("List all budgets").option("--limit <n>", "Maximum budget rows to print (default: 20)").option("--verbose", "Show all budget rows").action(async (opts) => {
11070
11745
  const statuses = await getStore().listBudgets();
11071
11746
  if (statuses.length === 0) {
11072
11747
  console.log(chalk7.yellow("No budgets set."));
11073
11748
  return;
11074
11749
  }
11750
+ const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
11751
+ const visibleStatuses = statuses.slice(0, limit);
11075
11752
  console.log();
11076
- printTable(["Scope", "Period", "Limit", "Spent", "Used%", "Status"], statuses.map((b) => {
11753
+ if (visibleStatuses.length < statuses.length) {
11754
+ console.log(chalk7.dim(` ${statuses.length} budgets \xB7 showing ${visibleStatuses.length}`));
11755
+ console.log();
11756
+ }
11757
+ printTable(["Scope", "Period", "Limit", "Spent", "Used%", "Status"], visibleStatuses.map((b) => {
11077
11758
  const pct = b.percent_used.toFixed(1);
11078
11759
  const status = b.is_over_limit ? chalk7.red("OVER") : b.is_over_alert ? chalk7.yellow("ALERT") : chalk7.green("OK");
11079
11760
  const pctColor = b.is_over_limit ? chalk7.red(pct + "%") : b.is_over_alert ? chalk7.yellow(pct + "%") : chalk7.green(pct + "%");
11080
11761
  return [
11081
- chalk7.white(b.project_path ?? "global"),
11762
+ chalk7.white(b.cost_center_id ?? b.project_path ?? "global"),
11082
11763
  b.period,
11083
11764
  fmt4(b.limit_usd),
11084
11765
  fmt4(b.current_spend_usd),
@@ -11086,6 +11767,7 @@ budgetCmd.command("list").description("List all budgets").action(async () => {
11086
11767
  status
11087
11768
  ];
11088
11769
  }));
11770
+ printHiddenRowsHint(statuses.length, visibleStatuses.length, "Use --limit <n> or --verbose.");
11089
11771
  console.log();
11090
11772
  });
11091
11773
  budgetCmd.command("remove <id>").description("Remove a budget by ID").action(async (id) => {
@@ -11099,20 +11781,27 @@ projectCmd.command("add <path>").description("Add a project").option("--name <na
11099
11781
  await getStore().addProject(path, name);
11100
11782
  console.log(chalk7.green(`\u2713 Project added: ${path}`));
11101
11783
  });
11102
- projectCmd.command("list").description("List all projects with costs").action(async () => {
11784
+ projectCmd.command("list").description("List all projects with costs").option("--limit <n>", "Maximum project rows to print (default: 20)").option("--verbose", "Show all project rows").action(async (opts) => {
11103
11785
  const projects = await getStore().listProjects();
11104
11786
  if (projects.length === 0) {
11105
11787
  console.log(chalk7.yellow("No projects tracked yet."));
11106
11788
  return;
11107
11789
  }
11790
+ const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
11791
+ const visibleProjects = projects.slice(0, limit);
11108
11792
  console.log();
11109
- printTable(["Project", "Path", "Sessions", "Cost", "Last Active"], projects.map((p) => [
11793
+ if (visibleProjects.length < projects.length) {
11794
+ console.log(chalk7.dim(` ${projects.length} projects \xB7 showing ${visibleProjects.length}`));
11795
+ console.log();
11796
+ }
11797
+ printTable(["Project", "Path", "Sessions", "Cost", "Last Active"], visibleProjects.map((p) => [
11110
11798
  chalk7.white(p.project_name || chalk7.dim("unknown")),
11111
11799
  chalk7.dim(p.project_path.substring(0, 40)),
11112
11800
  String(p.sessions),
11113
11801
  fmt4(p.cost_usd),
11114
11802
  chalk7.dim(p.last_active?.substring(0, 16) ?? "\u2014")
11115
11803
  ]));
11804
+ printHiddenRowsHint(projects.length, visibleProjects.length, "Use --limit <n>, --verbose, or economy project show <path>.");
11116
11805
  console.log();
11117
11806
  });
11118
11807
  projectCmd.command("remove <path>").description("Remove a project (keeps historical data)").action(async (path) => {
@@ -11217,10 +11906,16 @@ configCmd.action(async () => {
11217
11906
  console.log();
11218
11907
  });
11219
11908
  var pricingCmd = program.command("pricing").description("Manage model pricing rates");
11220
- pricingCmd.command("list").description("List all model prices").action(async () => {
11909
+ pricingCmd.command("list").description("List all model prices").option("--limit <n>", "Maximum pricing rows to print (default: 50)").option("--verbose", "Show all pricing rows").action(async (opts) => {
11221
11910
  const rows = await getStore().listPricing();
11911
+ const limit = resolveRowLimit(opts.limit, opts.verbose, 50);
11912
+ const visibleRows = rows.slice(0, limit);
11222
11913
  console.log();
11223
- printTable(["Model", "Input/1M", "Output/1M", "CacheR/1M", "CacheW/1M", "CacheStorage/1M-h", "Out/1k"], rows.map((r) => [
11914
+ if (visibleRows.length < rows.length) {
11915
+ console.log(chalk7.dim(` ${rows.length} pricing rows \xB7 showing ${visibleRows.length}`));
11916
+ console.log();
11917
+ }
11918
+ printTable(["Model", "Input/1M", "Output/1M", "CacheR/1M", "CacheW/1M", "CacheStorage/1M-h", "Out/1k"], visibleRows.map((r) => [
11224
11919
  chalk7.white(r.model),
11225
11920
  fmt4(r.input_per_1m),
11226
11921
  fmt4(r.output_per_1m),
@@ -11229,6 +11924,7 @@ pricingCmd.command("list").description("List all model prices").action(async ()
11229
11924
  fmt4(r.cache_storage_per_1m_hour ?? 0),
11230
11925
  chalk7.dim(fmt4(r.output_per_1m / 1000))
11231
11926
  ]));
11927
+ printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n> or --verbose.");
11232
11928
  console.log();
11233
11929
  });
11234
11930
  pricingCmd.command("set <model>").description("Set pricing for a model").option("--input <usd>", "Input price per 1M tokens").option("--output <usd>", "Output price per 1M tokens").option("--cache-read <usd>", "Cache read price per 1M tokens", "0").option("--cache-write <usd>", "5-minute cache write price per 1M tokens", "0").option("--cache-write-1h <usd>", "1-hour cache write price per 1M tokens", "0").option("--cache-storage <usd>", "Context cache storage price per 1M token-hours", "0").action(async (model, opts) => {
@@ -11323,7 +12019,8 @@ Gemini (~/.gemini/settings.json):`));
11323
12019
  }
11324
12020
  console.log();
11325
12021
  });
11326
- program.command("session <id>").description("Show detailed breakdown of a single session").action(async (id) => {
12022
+ program.command("session <id>").description("Show detailed breakdown of a single session").option("--limit <n>", "Number of request rows to show (default: 20)").option("--verbose", "Show up to 50 request rows").action(async (id, opts) => {
12023
+ const requestLimit = parsePositiveCliInteger(opts.limit ?? (opts.verbose ? "50" : "20"), "--limit");
11327
12024
  await autoSync();
11328
12025
  const detail = await getStore().sessionDetail(id);
11329
12026
  if (!detail) {
@@ -11344,10 +12041,11 @@ program.command("session <id>").description("Show detailed breakdown of a single
11344
12041
  ["Requests", fmtCount(session["request_count"])]
11345
12042
  ]);
11346
12043
  if (requests.length > 0) {
12044
+ const visibleRequests = requests.slice(0, requestLimit);
11347
12045
  console.log(chalk7.dim(`
11348
- Requests (${requests.length}):
12046
+ Requests (${visibleRequests.length}${visibleRequests.length < requests.length ? ` of ${requests.length}` : ""}):
11349
12047
  `));
11350
- printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"], requests.slice(0, 50).map((r) => [
12048
+ printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"], visibleRequests.map((r) => [
11351
12049
  chalk7.dim(String(r["timestamp"]).substring(11, 19)),
11352
12050
  chalk7.white(String(r["model"]).substring(0, 22)),
11353
12051
  fmtTokens2(r["input_tokens"]),
@@ -11356,12 +12054,13 @@ program.command("session <id>").description("Show detailed breakdown of a single
11356
12054
  fmtTokens2(r["cache_create_tokens"]),
11357
12055
  fmt4(r["cost_usd"])
11358
12056
  ]));
11359
- if (requests.length > 50)
11360
- console.log(chalk7.dim(` ... and ${requests.length - 50} more requests`));
12057
+ if (requests.length > visibleRequests.length) {
12058
+ console.log(chalk7.dim(` ... ${requests.length - visibleRequests.length} more requests hidden. Use --limit <n> or --verbose.`));
12059
+ }
11361
12060
  }
11362
12061
  console.log();
11363
12062
  });
11364
- program.command("machines").description("List all machines that have synced data").action(async () => {
12063
+ program.command("machines").description("List all machines that have synced data").option("--limit <n>", "Maximum machine rows to print (default: 20)").option("--verbose", "Show all machine rows").action(async (opts) => {
11365
12064
  await autoSync();
11366
12065
  const machines = await getStore().machines();
11367
12066
  const current = getMachineId();
@@ -11369,16 +12068,22 @@ program.command("machines").description("List all machines that have synced data
11369
12068
  console.log(chalk7.yellow(`No machine data yet. Current machine: ${current}`));
11370
12069
  return;
11371
12070
  }
12071
+ const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
12072
+ const visibleMachines = machines.slice(0, limit);
11372
12073
  console.log();
11373
12074
  console.log(chalk7.bold.cyan(" Machines"));
12075
+ if (visibleMachines.length < machines.length) {
12076
+ console.log(chalk7.dim(` ${machines.length} machines \xB7 showing ${visibleMachines.length}`));
12077
+ }
11374
12078
  console.log();
11375
- printTable(["Machine", "Sessions", "Requests", "Cost", "Last Active"], machines.map((m) => [
12079
+ printTable(["Machine", "Sessions", "Requests", "Cost", "Last Active"], visibleMachines.map((m) => [
11376
12080
  m.machine_id === current ? chalk7.green(`${m.machine_id} (this)`) : chalk7.white(m.machine_id),
11377
12081
  fmtCount(m.sessions),
11378
12082
  fmtCount(m.requests),
11379
12083
  fmt4(m.total_cost_usd),
11380
12084
  chalk7.dim(m.last_active?.substring(0, 16) ?? "\u2014")
11381
12085
  ]));
12086
+ printHiddenRowsHint(machines.length, visibleMachines.length, "Use --limit <n> or --verbose.");
11382
12087
  console.log(`
11383
12088
  ${chalk7.dim("Current machine:")} ${chalk7.bold(current)}`);
11384
12089
  console.log();
@@ -11528,7 +12233,7 @@ menubarCmd.command("stop").description("Quit Economy Bar").action(async () => {
11528
12233
  menubarStop2();
11529
12234
  });
11530
12235
  var goalCmd = program.command("goal").description("Manage spending goals");
11531
- 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) => {
11532
12237
  const limitUsd = parsePositiveCliNumber(opts.limit, "--limit");
11533
12238
  const period = requireCliChoice(opts.period, "--period", ["day", "week", "month", "year"]);
11534
12239
  const agent = parseOptionalCliAgent(opts.agent) ?? null;
@@ -11540,14 +12245,20 @@ goalCmd.command("set").description("Set a spending goal").option("--period <peri
11540
12245
  });
11541
12246
  console.log(chalk7.green(`\u2713 Goal set: ${period} $${limitUsd}${opts.project ? ` (${opts.project})` : ""}`));
11542
12247
  });
11543
- goalCmd.command("list").description("List all goals with progress").action(async () => {
12248
+ goalCmd.command("list").description("List all goals with progress").option("--limit <n>", "Maximum goal rows to print (default: 20)").option("--verbose", "Show all goal rows").action(async (opts) => {
11544
12249
  const statuses = await getStore().listGoals();
11545
12250
  if (statuses.length === 0) {
11546
12251
  console.log(chalk7.yellow("No goals set."));
11547
12252
  return;
11548
12253
  }
12254
+ const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
12255
+ const visibleStatuses = statuses.slice(0, limit);
11549
12256
  console.log();
11550
- printTable(["Period", "Scope", "Limit", "Spent", "Used%", "Status"], statuses.map((g) => {
12257
+ if (visibleStatuses.length < statuses.length) {
12258
+ console.log(chalk7.dim(` ${statuses.length} goals \xB7 showing ${visibleStatuses.length}`));
12259
+ console.log();
12260
+ }
12261
+ printTable(["Period", "Scope", "Limit", "Spent", "Used%", "Status"], visibleStatuses.map((g) => {
11551
12262
  const pct = g.percent_used.toFixed(1);
11552
12263
  const scope = g.project_path ?? g.agent ?? "global";
11553
12264
  const status = g.is_over ? chalk7.red("OVER") : g.is_at_risk ? chalk7.yellow("AT RISK") : chalk7.green("ON TRACK");
@@ -11561,20 +12272,23 @@ goalCmd.command("list").description("List all goals with progress").action(async
11561
12272
  status
11562
12273
  ];
11563
12274
  }));
12275
+ printHiddenRowsHint(statuses.length, visibleStatuses.length, "Use --limit <n> or --verbose.");
11564
12276
  console.log();
11565
12277
  });
11566
12278
  goalCmd.command("remove <id>").description("Remove a goal").action(async (id) => {
11567
12279
  await getStore().removeGoal(id);
11568
12280
  console.log(chalk7.green(`\u2713 Goal removed`));
11569
12281
  });
11570
- goalCmd.command("status").description("Quick goal progress summary").action(async () => {
12282
+ goalCmd.command("status").description("Quick goal progress summary").option("--limit <n>", "Maximum goal rows to print (default: 20)").option("--verbose", "Show all goal rows").action(async (opts) => {
11571
12283
  const statuses = await getStore().listGoals();
11572
12284
  if (statuses.length === 0) {
11573
12285
  console.log(chalk7.yellow("No goals set."));
11574
12286
  return;
11575
12287
  }
12288
+ const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
12289
+ const visibleStatuses = statuses.slice(0, limit);
11576
12290
  console.log();
11577
- for (const g of statuses) {
12291
+ for (const g of visibleStatuses) {
11578
12292
  const scope = g.project_path ?? g.agent ?? "global";
11579
12293
  const pct = Math.min(g.percent_used, 100);
11580
12294
  const barFilled = Math.round(pct / 10);
@@ -11584,6 +12298,7 @@ goalCmd.command("status").description("Quick goal progress summary").action(asyn
11584
12298
  const label = `${g.period} (${scope})`.padEnd(20);
11585
12299
  console.log(` ${label} ${bar} ${fmt4(g.current_spend_usd)} / ${fmt4(g.limit_usd)} (${g.percent_used.toFixed(0)}%) ${statusStr}`);
11586
12300
  }
12301
+ printHiddenRowsHint(statuses.length, visibleStatuses.length, "Use --limit <n> or --verbose.");
11587
12302
  console.log();
11588
12303
  });
11589
12304
  program.command("remove <type> <id>").alias("rm").description("Remove a record. Type: budget | project | goal | pricing").action(async (type, id) => {