@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/mcp/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, {
@@ -535,7 +591,6 @@ var init_pricing = __esm(() => {
535
591
  });
536
592
 
537
593
  // src/db/database.ts
538
- import { SqliteAdapter as Database } from "@hasna/cloud";
539
594
  import { execFileSync } from "child_process";
540
595
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
541
596
  import { hostname, platform } from "os";
@@ -628,7 +683,7 @@ function openDatabase(dbPath, skipSeed = false) {
628
683
  mkdirSync(dir, { recursive: true });
629
684
  }
630
685
  const db = withSqliteBusyRetry(() => {
631
- const opened = new Database(path);
686
+ const opened = new SqliteAdapter(path);
632
687
  try {
633
688
  opened.exec("PRAGMA busy_timeout = 10000");
634
689
  opened.exec("PRAGMA journal_mode = WAL");
@@ -669,6 +724,16 @@ function addColumnIfMissing(db, table, column, definition) {
669
724
  }
670
725
  function initSchema(db) {
671
726
  db.exec(`
727
+ CREATE TABLE IF NOT EXISTS cost_centers (
728
+ id TEXT PRIMARY KEY,
729
+ kind TEXT NOT NULL,
730
+ name TEXT NOT NULL,
731
+ repo_path TEXT,
732
+ labels_json TEXT DEFAULT '{}',
733
+ created_at TEXT NOT NULL,
734
+ updated_at TEXT NOT NULL
735
+ );
736
+
672
737
  CREATE TABLE IF NOT EXISTS requests (
673
738
  id TEXT PRIMARY KEY,
674
739
  agent TEXT NOT NULL,
@@ -685,6 +750,7 @@ function initSchema(db) {
685
750
  timestamp TEXT NOT NULL,
686
751
  source_request_id TEXT,
687
752
  machine_id TEXT DEFAULT '',
753
+ cost_center_id TEXT,
688
754
  account_key TEXT DEFAULT '',
689
755
  account_tool TEXT DEFAULT '',
690
756
  account_name TEXT DEFAULT '',
@@ -703,6 +769,7 @@ function initSchema(db) {
703
769
  total_tokens INTEGER DEFAULT 0,
704
770
  request_count INTEGER DEFAULT 0,
705
771
  machine_id TEXT DEFAULT '',
772
+ cost_center_id TEXT,
706
773
  account_key TEXT DEFAULT '',
707
774
  account_tool TEXT DEFAULT '',
708
775
  account_name TEXT DEFAULT '',
@@ -723,6 +790,7 @@ function initSchema(db) {
723
790
  id TEXT PRIMARY KEY,
724
791
  project_path TEXT,
725
792
  agent TEXT,
793
+ cost_center_id TEXT,
726
794
  period TEXT NOT NULL,
727
795
  limit_usd REAL NOT NULL,
728
796
  alert_at_percent INTEGER DEFAULT 80,
@@ -753,6 +821,7 @@ function initSchema(db) {
753
821
  CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
754
822
  CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
755
823
  CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
824
+ CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
756
825
 
757
826
  CREATE TABLE IF NOT EXISTS model_pricing (
758
827
  model TEXT PRIMARY KEY,
@@ -839,6 +908,12 @@ function initSchema(db) {
839
908
  `);
840
909
  addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
841
910
  addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
911
+ if (addColumnIfMissing(db, "cost_centers", "updated_at", `TEXT DEFAULT ''`)) {
912
+ db.exec(`UPDATE cost_centers SET updated_at = created_at WHERE updated_at = '' OR updated_at IS NULL`);
913
+ }
914
+ addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
915
+ addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
916
+ addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
842
917
  if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
843
918
  db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
844
919
  }
@@ -867,6 +942,10 @@ function initSchema(db) {
867
942
  CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
868
943
  CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
869
944
  CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
945
+ CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id);
946
+ CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id);
947
+ CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
948
+ CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
870
949
  `);
871
950
  }
872
951
  function periodWhere(period) {
@@ -917,6 +996,14 @@ function requestPeriodWhere(period) {
917
996
  return "1=1";
918
997
  }
919
998
  }
999
+ function upsertCostCenter(db, center) {
1000
+ const updatedAt = center.updated_at ?? center.created_at;
1001
+ db.prepare(`
1002
+ INSERT OR REPLACE INTO cost_centers
1003
+ (id, kind, name, repo_path, labels_json, created_at, updated_at)
1004
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1005
+ `).run(center.id, center.kind, center.name, center.repo_path ?? null, center.labels_json ?? "{}", center.created_at, updatedAt);
1006
+ }
920
1007
  function upsertRequest(db, req) {
921
1008
  const now = req.updated_at ?? new Date().toISOString();
922
1009
  db.prepare(`
@@ -924,10 +1011,10 @@ function upsertRequest(db, req) {
924
1011
  (id, agent, session_id, model, input_tokens, output_tokens,
925
1012
  cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
926
1013
  cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
927
- source_request_id, machine_id, attribution_tag, account_key, account_tool,
1014
+ source_request_id, machine_id, cost_center_id, attribution_tag, account_key, account_tool,
928
1015
  account_name, account_email, account_source, updated_at, synced_at)
929
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
930
- `).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 ?? "");
1016
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1017
+ `).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 ?? "");
931
1018
  }
932
1019
  function upsertSession(db, session) {
933
1020
  const now = session.updated_at ?? new Date().toISOString();
@@ -935,9 +1022,9 @@ function upsertSession(db, session) {
935
1022
  INSERT OR REPLACE INTO sessions
936
1023
  (id, agent, project_path, project_name, started_at, ended_at,
937
1024
  total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
938
- account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
939
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
940
- `).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 ?? "");
1025
+ cost_center_id, account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
1026
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1027
+ `).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 ?? "");
941
1028
  }
942
1029
  function rollupSession(db, sessionId) {
943
1030
  db.prepare(`
@@ -963,9 +1050,12 @@ function rollupSession(db, sessionId) {
963
1050
  ELSE account_email END,
964
1051
  account_source = CASE WHEN account_source = '' OR account_source IS NULL
965
1052
  THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
966
- ELSE account_source END
1053
+ ELSE account_source END,
1054
+ cost_center_id = CASE WHEN cost_center_id = '' OR cost_center_id IS NULL
1055
+ 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)
1056
+ ELSE cost_center_id END
967
1057
  WHERE id = ?
968
- `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
1058
+ `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
969
1059
  }
970
1060
  function querySessions(db, filter = {}) {
971
1061
  const conditions = [];
@@ -1181,9 +1271,12 @@ function labelForPath(projectPath, projectName) {
1181
1271
  function groupKeyForPath(projectPath, projectName) {
1182
1272
  return labelForPath(projectPath, projectName).trim().toLowerCase();
1183
1273
  }
1184
- function queryProjectBreakdown(db, period = "all", machine) {
1185
- const requestWhere = requestPeriodWhere(period);
1186
- const sessionWhere = sessionPeriodWhere(period);
1274
+ function laterTimestamp(current, candidate) {
1275
+ if (!candidate)
1276
+ return current;
1277
+ return candidate > current ? candidate : current;
1278
+ }
1279
+ function queryProjectBreakdownScoped(db, requestWhere, sessionWhere, requestScopeParams = [], sessionScopeParams = [], machine) {
1187
1280
  const sessionMachineClause = machine ? " AND (machine_id = ? OR id IN (SELECT DISTINCT session_id FROM requests WHERE machine_id = ?))" : "";
1188
1281
  const requestMachineClause = machine ? " AND machine_id = ?" : "";
1189
1282
  const sessionMachineParams = machine ? [machine, machine] : [];
@@ -1191,67 +1284,89 @@ function queryProjectBreakdown(db, period = "all", machine) {
1191
1284
  const sessionOnlyMachineClause = machine ? " AND machine_id = ?" : "";
1192
1285
  const sessionOnlyMachineParams = machine ? [machine] : [];
1193
1286
  const sessions = db.prepare(`
1194
- SELECT id, project_path, project_name, total_cost_usd, started_at
1287
+ SELECT id, project_path, project_name
1195
1288
  FROM sessions
1196
1289
  WHERE (project_path != '' OR project_name != '')${sessionMachineClause}
1197
1290
  `).all(...sessionMachineParams);
1198
1291
  const groups = new Map;
1292
+ const groupKeyBySession = new Map;
1199
1293
  for (const s of sessions) {
1200
1294
  const label = labelForPath(s.project_path, s.project_name);
1201
1295
  if (!label)
1202
1296
  continue;
1203
1297
  const key = groupKeyForPath(s.project_path, s.project_name);
1204
- const g = groups.get(key) ?? { label, sessionIds: [], samplePath: s.project_path };
1205
- g.sessionIds.push(s.id);
1298
+ const g = groups.get(key) ?? { label, samplePath: s.project_path, sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: "" };
1206
1299
  if (!g.samplePath)
1207
1300
  g.samplePath = s.project_path;
1208
1301
  groups.set(key, g);
1302
+ groupKeyBySession.set(s.id, key);
1303
+ }
1304
+ const groupFor = (sessionId) => {
1305
+ const key = groupKeyBySession.get(sessionId);
1306
+ return key === undefined ? undefined : groups.get(key);
1307
+ };
1308
+ const requestRows = db.prepare(`
1309
+ SELECT session_id,
1310
+ COUNT(*) as requests,
1311
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
1312
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1313
+ MAX(timestamp) as last_active
1314
+ FROM requests
1315
+ WHERE ${requestWhere}${requestMachineClause}
1316
+ GROUP BY session_id
1317
+ `).all(...requestScopeParams, ...requestMachineParams);
1318
+ for (const row of requestRows) {
1319
+ const g = groupFor(row.session_id);
1320
+ if (!g)
1321
+ continue;
1322
+ g.sessions += 1;
1323
+ g.requests += row.requests;
1324
+ g.cost_usd += row.cost_usd;
1325
+ g.total_tokens += row.total_tokens;
1326
+ g.last_active = laterTimestamp(g.last_active, row.last_active);
1327
+ }
1328
+ const sessionsWithRequests = new Set(db.prepare(`SELECT DISTINCT session_id FROM requests`).all().map((row) => row.session_id));
1329
+ const sessionOnlyRows = db.prepare(`
1330
+ SELECT id,
1331
+ COALESCE(request_count, 0) as request_count,
1332
+ COALESCE(total_tokens, 0) as total_tokens,
1333
+ COALESCE(total_cost_usd, 0) as total_cost_usd,
1334
+ started_at
1335
+ FROM sessions
1336
+ WHERE ${sessionWhere}${sessionOnlyMachineClause}
1337
+ `).all(...sessionScopeParams, ...sessionOnlyMachineParams);
1338
+ for (const row of sessionOnlyRows) {
1339
+ if (sessionsWithRequests.has(row.id))
1340
+ continue;
1341
+ const g = groupFor(row.id);
1342
+ if (!g)
1343
+ continue;
1344
+ g.sessions += 1;
1345
+ g.requests += row.request_count;
1346
+ g.total_tokens += row.total_tokens;
1347
+ g.cost_usd += row.total_cost_usd;
1348
+ g.last_active = laterTimestamp(g.last_active, row.started_at);
1209
1349
  }
1210
1350
  const result = [];
1211
1351
  for (const g of groups.values()) {
1212
- const placeholders = g.sessionIds.map(() => "?").join(",");
1213
- const reqStats = placeholders.length ? db.prepare(`
1214
- SELECT
1215
- COUNT(DISTINCT session_id) as sessions,
1216
- COUNT(*) as requests,
1217
- COALESCE(SUM(cost_usd), 0) as cost_usd,
1218
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1219
- MAX(timestamp) as last_active
1220
- FROM requests
1221
- WHERE session_id IN (${placeholders})
1222
- AND ${requestWhere}
1223
- ${requestMachineClause}
1224
- `).get(...g.sessionIds, ...requestMachineParams) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
1225
- const sessionOnlyStats = placeholders.length ? db.prepare(`
1226
- SELECT
1227
- COUNT(*) as sessions,
1228
- COALESCE(SUM(request_count), 0) as requests,
1229
- COALESCE(SUM(total_tokens), 0) as total_tokens,
1230
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1231
- MAX(started_at) as last_active
1232
- FROM sessions
1233
- WHERE id IN (${placeholders})
1234
- AND ${sessionWhere}
1235
- ${sessionOnlyMachineClause}
1236
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1237
- `).get(...g.sessionIds, ...sessionOnlyMachineParams) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
1238
- const totalSessions = reqStats.sessions + sessionOnlyStats.sessions;
1239
- if (totalSessions === 0)
1352
+ if (g.sessions === 0)
1240
1353
  continue;
1241
- const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
1242
1354
  result.push({
1243
1355
  project_path: g.samplePath,
1244
1356
  project_name: g.label,
1245
- sessions: totalSessions,
1246
- requests: reqStats.requests + sessionOnlyStats.requests,
1247
- total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
1248
- cost_usd: reqStats.cost_usd + sessionOnlyStats.cost_usd,
1249
- last_active: lastActive
1357
+ sessions: g.sessions,
1358
+ requests: g.requests,
1359
+ total_tokens: g.total_tokens,
1360
+ cost_usd: g.cost_usd,
1361
+ last_active: g.last_active
1250
1362
  });
1251
1363
  }
1252
1364
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1253
1365
  return result;
1254
1366
  }
1367
+ function queryProjectBreakdown(db, period = "all", machine) {
1368
+ return queryProjectBreakdownScoped(db, requestPeriodWhere(period), sessionPeriodWhere(period), [], [], machine);
1369
+ }
1255
1370
  function normalizeAccountEmail(email) {
1256
1371
  return (email ?? "").trim().toLowerCase();
1257
1372
  }
@@ -1392,17 +1507,141 @@ function queryAccountBreakdown(db, period = "all", machine) {
1392
1507
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1393
1508
  return result;
1394
1509
  }
1395
- function queryProjectBreakdownSince(db, since) {
1396
- return db.prepare(`
1397
- SELECT project_path, project_name,
1398
- COUNT(*) as sessions,
1399
- COALESCE(SUM(total_tokens), 0) as total_tokens,
1400
- COALESCE(SUM(request_count), 0) as requests,
1401
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1402
- MAX(started_at) as last_active
1403
- FROM sessions WHERE started_at >= ?
1404
- GROUP BY project_path ORDER BY cost_usd DESC
1405
- `).all(since);
1510
+ function kindFromCostCenterId(id) {
1511
+ const prefix = id.split(":", 1)[0] ?? "";
1512
+ return ["loop", "app", "repo", "service", "team"].includes(prefix) ? prefix : "";
1513
+ }
1514
+ function addCostCenterBreakdownRow(groups, row, sessionOnly) {
1515
+ if (!row.cost_center_id)
1516
+ return;
1517
+ const derivedKind = row.kind || kindFromCostCenterId(row.cost_center_id) || "service";
1518
+ const group = groups.get(row.cost_center_id) ?? {
1519
+ cost_center_id: row.cost_center_id,
1520
+ kind: derivedKind,
1521
+ name: row.name || row.cost_center_id,
1522
+ repo_path: row.repo_path ?? null,
1523
+ labels_json: row.labels_json || "{}",
1524
+ sessionIds: new Set,
1525
+ requests: 0,
1526
+ total_tokens: 0,
1527
+ api_equivalent_usd: 0,
1528
+ metered_api_usd: 0,
1529
+ subscription_included_usd: 0,
1530
+ estimated_usd: 0,
1531
+ unknown_usd: 0,
1532
+ last_active: ""
1533
+ };
1534
+ group.kind = derivedKind;
1535
+ if (row.name)
1536
+ group.name = row.name;
1537
+ if (row.repo_path && !group.repo_path)
1538
+ group.repo_path = row.repo_path;
1539
+ if (row.labels_json && row.labels_json !== "{}")
1540
+ group.labels_json = row.labels_json;
1541
+ if (row.session_id)
1542
+ group.sessionIds.add(row.session_id);
1543
+ group.requests += row.requests;
1544
+ group.total_tokens += row.total_tokens;
1545
+ group.api_equivalent_usd += row.cost_usd;
1546
+ if (sessionOnly) {
1547
+ group.estimated_usd += row.cost_usd;
1548
+ } else if (row.cost_basis === "metered_api") {
1549
+ group.metered_api_usd += row.cost_usd;
1550
+ } else if (row.cost_basis === "subscription_included") {
1551
+ group.subscription_included_usd += row.cost_usd;
1552
+ } else if (row.cost_basis === "unknown") {
1553
+ group.unknown_usd += row.cost_usd;
1554
+ } else {
1555
+ group.estimated_usd += row.cost_usd;
1556
+ }
1557
+ if (!group.last_active || row.last_active > group.last_active)
1558
+ group.last_active = row.last_active;
1559
+ groups.set(row.cost_center_id, group);
1560
+ }
1561
+ function queryCostCenterBreakdown(db, period = "all", filter = {}) {
1562
+ const requestWhere = filter.since ? "r.timestamp >= ?" : requestPeriodWhere(period);
1563
+ const sessionWhere = filter.since ? "s.started_at >= ?" : sessionPeriodWhere(period);
1564
+ const requestMachineClause = filter.machine ? " AND r.machine_id = ?" : "";
1565
+ const sessionMachineClause = filter.machine ? " AND s.machine_id = ?" : "";
1566
+ const requestParams = [];
1567
+ const sessionParams = [];
1568
+ if (filter.since) {
1569
+ requestParams.push(filter.since);
1570
+ sessionParams.push(filter.since);
1571
+ }
1572
+ if (filter.machine) {
1573
+ requestParams.push(filter.machine);
1574
+ sessionParams.push(filter.machine);
1575
+ }
1576
+ const groups = new Map;
1577
+ const requestRows = db.prepare(`
1578
+ SELECT
1579
+ COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) as cost_center_id,
1580
+ COALESCE(cc.kind, '') as kind,
1581
+ COALESCE(cc.name, COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))) as name,
1582
+ cc.repo_path as repo_path,
1583
+ COALESCE(cc.labels_json, '{}') as labels_json,
1584
+ r.session_id as session_id,
1585
+ 1 as requests,
1586
+ COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
1587
+ COALESCE(r.cost_usd, 0) as cost_usd,
1588
+ COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
1589
+ r.timestamp as last_active
1590
+ FROM requests r
1591
+ LEFT JOIN sessions s ON s.id = r.session_id
1592
+ LEFT JOIN cost_centers cc ON cc.id = COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))
1593
+ WHERE ${requestWhere}${requestMachineClause}
1594
+ AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) IS NOT NULL
1595
+ AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) != ''
1596
+ `).all(...requestParams);
1597
+ for (const row of requestRows)
1598
+ addCostCenterBreakdownRow(groups, row, false);
1599
+ const sessionOnlyRows = db.prepare(`
1600
+ SELECT
1601
+ s.cost_center_id as cost_center_id,
1602
+ COALESCE(cc.kind, '') as kind,
1603
+ COALESCE(cc.name, s.cost_center_id) as name,
1604
+ cc.repo_path as repo_path,
1605
+ COALESCE(cc.labels_json, '{}') as labels_json,
1606
+ s.id as session_id,
1607
+ COALESCE(s.request_count, 0) as requests,
1608
+ COALESCE(s.total_tokens, 0) as total_tokens,
1609
+ COALESCE(s.total_cost_usd, 0) as cost_usd,
1610
+ 'estimated' as cost_basis,
1611
+ s.started_at as last_active
1612
+ FROM sessions s
1613
+ LEFT JOIN cost_centers cc ON cc.id = s.cost_center_id
1614
+ WHERE ${sessionWhere}${sessionMachineClause}
1615
+ AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
1616
+ AND s.cost_center_id IS NOT NULL
1617
+ AND s.cost_center_id != ''
1618
+ `).all(...sessionParams);
1619
+ for (const row of sessionOnlyRows)
1620
+ addCostCenterBreakdownRow(groups, row, true);
1621
+ const result = [...groups.values()].map((group) => ({
1622
+ cost_center_id: group.cost_center_id,
1623
+ kind: group.kind,
1624
+ name: group.name,
1625
+ repo_path: group.repo_path,
1626
+ labels_json: group.labels_json,
1627
+ sessions: group.sessionIds.size,
1628
+ requests: group.requests,
1629
+ total_tokens: group.total_tokens,
1630
+ api_equivalent_usd: group.api_equivalent_usd,
1631
+ billable_usd: group.metered_api_usd,
1632
+ metered_api_usd: group.metered_api_usd,
1633
+ subscription_included_usd: group.subscription_included_usd,
1634
+ estimated_usd: group.estimated_usd,
1635
+ unknown_usd: group.unknown_usd,
1636
+ cost_usd: group.api_equivalent_usd,
1637
+ last_active: group.last_active
1638
+ }));
1639
+ const filtered = filter.kind ? result.filter((row) => row.kind === filter.kind) : result;
1640
+ filtered.sort((a, b) => b.cost_usd - a.cost_usd);
1641
+ return filtered;
1642
+ }
1643
+ function queryProjectBreakdownSince(db, since, machine) {
1644
+ return queryProjectBreakdownScoped(db, "timestamp >= ?", "started_at >= ?", [since], [since], machine);
1406
1645
  }
1407
1646
  function queryAgentBreakdownSince(db, since) {
1408
1647
  return db.prepare(`
@@ -1563,9 +1802,9 @@ function deleteProject(db, path) {
1563
1802
  function upsertBudget(db, budget) {
1564
1803
  db.prepare(`
1565
1804
  INSERT OR REPLACE INTO budgets
1566
- (id, project_path, agent, period, limit_usd, alert_at_percent, created_at, updated_at)
1567
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1568
- `).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);
1805
+ (id, project_path, agent, cost_center_id, period, limit_usd, alert_at_percent, created_at, updated_at)
1806
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1807
+ `).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);
1569
1808
  }
1570
1809
  function listBudgets(db) {
1571
1810
  return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
@@ -1587,6 +1826,10 @@ function getBudgetStatuses(db) {
1587
1826
  spendQuery += ` AND agent = ?`;
1588
1827
  params.push(b.agent);
1589
1828
  }
1829
+ if (b.cost_center_id) {
1830
+ spendQuery += ` AND COALESCE(NULLIF(cost_center_id, ''), (SELECT NULLIF(s.cost_center_id, '') FROM sessions s WHERE s.id = requests.session_id)) = ?`;
1831
+ params.push(b.cost_center_id);
1832
+ }
1590
1833
  const row = db.prepare(spendQuery).get(...params);
1591
1834
  const spend = row.spend;
1592
1835
  const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
@@ -1812,7 +2055,9 @@ function dedupeRequests(db) {
1812
2055
  }
1813
2056
  return removed;
1814
2057
  }
1815
- var init_database = () => {};
2058
+ var init_database = __esm(() => {
2059
+ init_sqlite_adapter();
2060
+ });
1816
2061
 
1817
2062
  // src/mcp/index.ts
1818
2063
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -1836,7 +2081,6 @@ var packageMetadata = getPackageMetadata();
1836
2081
  // src/mcp/server.ts
1837
2082
  init_database();
1838
2083
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1839
- import { registerCloudTools } from "@hasna/cloud";
1840
2084
  import { registerAgentTools } from "@hasna/agent-registry";
1841
2085
  import { z } from "zod";
1842
2086
 
@@ -2002,6 +2246,7 @@ function defaultCostBasisForAgent(agent) {
2002
2246
  }
2003
2247
 
2004
2248
  // src/lib/accounts.ts
2249
+ import { appliedProfileName, resolveStore } from "@hasna/accounts";
2005
2250
  var AGENT_ACCOUNT_TOOLS = {
2006
2251
  claude: ["claude"],
2007
2252
  takumi: ["takumi", "claude"],
@@ -2066,57 +2311,35 @@ function envOverride(agent, env) {
2066
2311
  account_source: "override"
2067
2312
  };
2068
2313
  }
2069
- function knownToolIds(api) {
2070
- try {
2071
- return new Set(api.listTools().map((tool) => tool.id));
2072
- } catch {
2073
- return new Set;
2074
- }
2075
- }
2076
- function profileForEnvDir(api, tool, env) {
2314
+ async function profileForEnvDir(store, tool, env) {
2077
2315
  const configuredDir = env[tool.envVar];
2078
2316
  if (!configuredDir)
2079
2317
  return null;
2080
2318
  const normalized = normalizeDir(configuredDir);
2081
- try {
2082
- return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
2083
- } catch {
2084
- return null;
2085
- }
2319
+ return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
2086
2320
  }
2087
2321
  async function resolveAccountForAgent(agent, env = process.env) {
2088
2322
  const override = envOverride(agent, env);
2089
2323
  if (override)
2090
2324
  return override;
2091
- let api;
2092
- try {
2093
- api = await import("@hasna/accounts");
2094
- } catch {
2095
- return null;
2096
- }
2097
- const toolIds = knownToolIds(api);
2325
+ const store = resolveStore(env);
2326
+ const tools = new Map((await store.listTools()).map((tool) => [tool.id, tool]));
2098
2327
  for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
2099
- if (!toolIds.has(toolId))
2100
- continue;
2101
- let tool;
2102
- try {
2103
- tool = api.getTool(toolId);
2104
- } catch {
2328
+ const tool = tools.get(toolId);
2329
+ if (!tool)
2105
2330
  continue;
2106
- }
2107
- const envProfile = profileForEnvDir(api, tool, env);
2331
+ const envProfile = await profileForEnvDir(store, tool, env);
2108
2332
  if (envProfile)
2109
2333
  return fromProfile(envProfile, "env");
2110
- try {
2111
- const applied = api.appliedProfile(toolId);
2334
+ const appliedName = appliedProfileName(toolId);
2335
+ if (appliedName) {
2336
+ const applied = await store.findProfile(appliedName, toolId);
2112
2337
  if (applied)
2113
2338
  return fromProfile(applied, "applied");
2114
- } catch {}
2115
- try {
2116
- const current = api.currentProfile(toolId);
2117
- if (current)
2118
- return fromProfile(current, "current");
2119
- } catch {}
2339
+ }
2340
+ const current = await store.currentProfile(toolId);
2341
+ if (current)
2342
+ return fromProfile(current, "current");
2120
2343
  }
2121
2344
  return null;
2122
2345
  }
@@ -3137,32 +3360,219 @@ function statSyncSafe(path) {
3137
3360
  }
3138
3361
  }
3139
3362
 
3363
+ // src/ingest/loops.ts
3364
+ init_database();
3365
+ init_pricing();
3366
+ import { existsSync as existsSync8 } from "fs";
3367
+ import { homedir as homedir8 } from "os";
3368
+ import { join as join8 } from "path";
3369
+ import { Database as BunDatabase2 } from "bun:sqlite";
3370
+ var DEFAULT_LOOPS_DB_PATH = join8(homedir8(), ".hasna", "loops", "loops.db");
3371
+ var LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
3372
+ function loopsDbPath() {
3373
+ return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? DEFAULT_LOOPS_DB_PATH;
3374
+ }
3375
+ function loopsModel() {
3376
+ return process.env["HASNA_ECONOMY_LOOPS_MODEL"] ?? process.env["ECONOMY_LOOPS_MODEL"] ?? "gpt-5.3-codex";
3377
+ }
3378
+ function openLoopsDb(dbPath, verbose) {
3379
+ let lastError;
3380
+ for (const readonly of [true, false]) {
3381
+ let loopsDb = null;
3382
+ try {
3383
+ loopsDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
3384
+ loopsDb.prepare("PRAGMA schema_version").get();
3385
+ return loopsDb;
3386
+ } catch (error) {
3387
+ lastError = error;
3388
+ loopsDb?.close();
3389
+ }
3390
+ }
3391
+ if (verbose) {
3392
+ const message = lastError instanceof Error ? lastError.message : String(lastError);
3393
+ console.log("OpenLoops DB unreadable:", dbPath, message);
3394
+ }
3395
+ return null;
3396
+ }
3397
+ function slug(value) {
3398
+ return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
3399
+ }
3400
+ function parseTarget(raw) {
3401
+ if (!raw)
3402
+ return {};
3403
+ try {
3404
+ const parsed = JSON.parse(raw);
3405
+ return parsed && typeof parsed === "object" ? parsed : {};
3406
+ } catch {
3407
+ return {};
3408
+ }
3409
+ }
3410
+ function stringValue(value) {
3411
+ return typeof value === "string" ? value.trim() : "";
3412
+ }
3413
+ function accountFields(row) {
3414
+ const target = parseTarget(row.target_json);
3415
+ const tool = stringValue(row.account_tool) || stringValue(target["account_tool"]) || stringValue(target["tool"]) || stringValue(target["agent"]);
3416
+ const profile = stringValue(row.account_profile) || stringValue(target["account_profile"]) || stringValue(target["profile"]) || stringValue(target["account"]);
3417
+ if (!tool || !profile) {
3418
+ return { account_key: "", account_tool: tool, account_name: profile, account_source: "" };
3419
+ }
3420
+ return {
3421
+ account_key: `${tool}:${profile}`,
3422
+ account_tool: tool,
3423
+ account_name: profile,
3424
+ account_source: "loops"
3425
+ };
3426
+ }
3427
+ function readLoopGoalRuns(loopsDb) {
3428
+ return loopsDb.prepare(`
3429
+ SELECT
3430
+ gr.id as goal_run_id,
3431
+ gr.goal_id as goal_id,
3432
+ COALESCE(gr.loop_id, g.loop_id, lr.loop_id) as loop_id,
3433
+ COALESCE(gr.loop_run_id, g.loop_run_id, lr.id) as loop_run_id,
3434
+ COALESCE(l.name, lr.loop_name, 'unknown-loop') as loop_name,
3435
+ l.labels_json as labels_json,
3436
+ l.target_json as target_json,
3437
+ g.objective as objective,
3438
+ gr.phase as phase,
3439
+ gr.status as status,
3440
+ COALESCE(gr.tokens_used, 0) as tokens_used,
3441
+ gr.created_at as created_at,
3442
+ gr.updated_at as updated_at,
3443
+ lr.started_at as loop_run_started_at,
3444
+ wsr.account_profile as account_profile,
3445
+ wsr.account_tool as account_tool
3446
+ FROM goal_runs gr
3447
+ LEFT JOIN goals g ON g.id = gr.goal_id
3448
+ LEFT JOIN loop_runs lr ON lr.id = COALESCE(gr.loop_run_id, g.loop_run_id)
3449
+ LEFT JOIN loops l ON l.id = COALESCE(gr.loop_id, g.loop_id, lr.loop_id)
3450
+ LEFT JOIN (
3451
+ SELECT goal_run_id, MIN(account_profile) as account_profile, MIN(account_tool) as account_tool
3452
+ FROM workflow_step_runs
3453
+ WHERE goal_run_id IS NOT NULL
3454
+ GROUP BY goal_run_id
3455
+ ) wsr ON wsr.goal_run_id = gr.id
3456
+ WHERE COALESCE(gr.tokens_used, 0) > 0
3457
+ ORDER BY gr.created_at ASC, gr.id ASC
3458
+ `).all();
3459
+ }
3460
+ async function ingestLoops(db, verbose = false) {
3461
+ const dbPath = loopsDbPath();
3462
+ if (!existsSync8(dbPath)) {
3463
+ if (verbose)
3464
+ console.log("OpenLoops DB not found:", dbPath);
3465
+ return { sessions: 0, requests: 0 };
3466
+ }
3467
+ const model = loopsModel();
3468
+ const machineId = getMachineId();
3469
+ let loopsDb = null;
3470
+ let requests = 0;
3471
+ const sessions = new Set;
3472
+ try {
3473
+ loopsDb = openLoopsDb(dbPath, verbose);
3474
+ if (!loopsDb)
3475
+ return { sessions: 0, requests: 0 };
3476
+ for (const row of readLoopGoalRuns(loopsDb)) {
3477
+ const loopName = row.loop_name || "unknown-loop";
3478
+ const costCenterId = `loop:${slug(loopName)}`;
3479
+ const sessionId = `loops-${row.loop_run_id ?? row.loop_id ?? row.goal_id}`;
3480
+ const requestId = `loops-${row.goal_run_id}`;
3481
+ const timestamp = row.updated_at ?? row.created_at ?? row.loop_run_started_at ?? new Date().toISOString();
3482
+ const stateValue = `${LOOPS_INGEST_VERSION}:${row.updated_at ?? ""}:${row.tokens_used}:${model}:${costCenterId}`;
3483
+ if (getIngestState(db, "loops", row.goal_run_id) === stateValue)
3484
+ continue;
3485
+ upsertCostCenter(db, {
3486
+ id: costCenterId,
3487
+ kind: "loop",
3488
+ name: loopName,
3489
+ repo_path: null,
3490
+ labels_json: row.labels_json ?? "{}",
3491
+ created_at: row.created_at ?? timestamp
3492
+ });
3493
+ const account = accountFields(row);
3494
+ upsertSession(db, {
3495
+ id: sessionId,
3496
+ agent: "loop",
3497
+ project_path: "",
3498
+ project_name: loopName,
3499
+ started_at: row.loop_run_started_at ?? row.created_at ?? timestamp,
3500
+ ended_at: row.updated_at ?? null,
3501
+ total_cost_usd: 0,
3502
+ total_tokens: 0,
3503
+ request_count: 0,
3504
+ machine_id: machineId,
3505
+ cost_center_id: costCenterId,
3506
+ attribution_tag: "loops-orchestration",
3507
+ ...account
3508
+ });
3509
+ const inputTokens = row.tokens_used;
3510
+ const outputTokens = 0;
3511
+ upsertRequest(db, {
3512
+ id: requestId,
3513
+ agent: "loop",
3514
+ session_id: sessionId,
3515
+ model,
3516
+ input_tokens: inputTokens,
3517
+ output_tokens: outputTokens,
3518
+ cache_read_tokens: 0,
3519
+ cache_create_tokens: 0,
3520
+ cost_usd: computeCostFromDb(db, model, inputTokens, outputTokens),
3521
+ cost_basis: "estimated",
3522
+ duration_ms: 0,
3523
+ timestamp,
3524
+ source_request_id: requestId,
3525
+ machine_id: machineId,
3526
+ cost_center_id: costCenterId,
3527
+ attribution_tag: "loops-orchestration",
3528
+ ...account
3529
+ });
3530
+ sessions.add(sessionId);
3531
+ requests++;
3532
+ setIngestState(db, "loops", row.goal_run_id, stateValue);
3533
+ if (verbose)
3534
+ console.log(`OpenLoops ${loopName}: ${row.tokens_used} orchestration tokens`);
3535
+ }
3536
+ } catch (error) {
3537
+ if (verbose) {
3538
+ const message = error instanceof Error ? error.message : String(error);
3539
+ console.log("OpenLoops ingest skipped:", message);
3540
+ }
3541
+ return { sessions: 0, requests: 0 };
3542
+ } finally {
3543
+ loopsDb?.close();
3544
+ }
3545
+ for (const sessionId of sessions)
3546
+ rollupSession(db, sessionId);
3547
+ return { sessions: sessions.size, requests };
3548
+ }
3549
+
3140
3550
  // src/ingest/claude-quota.ts
3141
3551
  init_database();
3142
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
3552
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
3143
3553
 
3144
3554
  // src/lib/paths.ts
3145
- import { homedir as homedir8 } from "os";
3146
- import { join as join8 } from "path";
3555
+ import { homedir as homedir9 } from "os";
3556
+ import { join as join9 } from "path";
3147
3557
  function getHomeDir() {
3148
- return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir8();
3558
+ return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir9();
3149
3559
  }
3150
3560
  function agentPaths() {
3151
3561
  const home = getHomeDir();
3152
3562
  return {
3153
- claudeProjects: join8(home, ".claude", "projects"),
3154
- claudeCredentials: join8(home, ".claude", ".credentials.json"),
3155
- takumiProjects: join8(home, ".takumi", "projects"),
3156
- codexDir: join8(home, ".codex"),
3157
- codexDb: join8(home, ".codex", "state_5.sqlite"),
3158
- codexAuth: join8(home, ".codex", "auth.json"),
3159
- codexConfig: join8(home, ".codex", "config.toml"),
3160
- geminiTmp: join8(home, ".gemini", "tmp"),
3161
- geminiHistory: join8(home, ".gemini", "history"),
3162
- opencodeMessages: join8(home, ".local", "share", "opencode", "storage", "message"),
3163
- piSessions: join8(home, ".pi", "agent", "sessions"),
3164
- hermesDir: join8(home, ".hermes"),
3165
- hermesDb: join8(home, ".hermes", "state.db")
3563
+ claudeProjects: join9(home, ".claude", "projects"),
3564
+ claudeCredentials: join9(home, ".claude", ".credentials.json"),
3565
+ takumiProjects: join9(home, ".takumi", "projects"),
3566
+ codexDir: join9(home, ".codex"),
3567
+ codexDb: join9(home, ".codex", "state_5.sqlite"),
3568
+ codexAuth: join9(home, ".codex", "auth.json"),
3569
+ codexConfig: join9(home, ".codex", "config.toml"),
3570
+ geminiTmp: join9(home, ".gemini", "tmp"),
3571
+ geminiHistory: join9(home, ".gemini", "history"),
3572
+ opencodeMessages: join9(home, ".local", "share", "opencode", "storage", "message"),
3573
+ piSessions: join9(home, ".pi", "agent", "sessions"),
3574
+ hermesDir: join9(home, ".hermes"),
3575
+ hermesDb: join9(home, ".hermes", "state.db")
3166
3576
  };
3167
3577
  }
3168
3578
 
@@ -3174,7 +3584,7 @@ function readClaudeToken() {
3174
3584
  const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
3175
3585
  if (fromEnv)
3176
3586
  return { token: fromEnv };
3177
- if (!existsSync8(CREDENTIALS_PATH))
3587
+ if (!existsSync9(CREDENTIALS_PATH))
3178
3588
  return null;
3179
3589
  try {
3180
3590
  const creds = JSON.parse(readFileSync7(CREDENTIALS_PATH, "utf-8"));
@@ -3313,14 +3723,14 @@ async function ingestClaudeQuota(db, verbose = false) {
3313
3723
 
3314
3724
  // src/ingest/codex-quota.ts
3315
3725
  init_database();
3316
- import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
3726
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
3317
3727
  var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
3318
3728
  function readCodexAuth() {
3319
3729
  const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
3320
3730
  if (fromEnv)
3321
3731
  return { token: fromEnv, authMode: "chatgpt" };
3322
3732
  const authPath = agentPaths().codexAuth;
3323
- if (!existsSync9(authPath))
3733
+ if (!existsSync10(authPath))
3324
3734
  return null;
3325
3735
  try {
3326
3736
  const auth = JSON.parse(readFileSync8(authPath, "utf-8"));
@@ -3453,7 +3863,7 @@ async function ingestCodexQuota(db, verbose = false) {
3453
3863
  // src/lib/sync-all.ts
3454
3864
  init_database();
3455
3865
  async function syncAll(db, opts = {}) {
3456
- const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
3866
+ const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes || opts.loops);
3457
3867
  const all = !anySpecific;
3458
3868
  const result = { deduped: 0 };
3459
3869
  if (all || opts.claude) {
@@ -3476,6 +3886,8 @@ async function syncAll(db, opts = {}) {
3476
3886
  result.pi = await ingestPi(db, opts.verbose);
3477
3887
  if (all || opts.hermes)
3478
3888
  result.hermes = await ingestHermes(db, opts.verbose);
3889
+ if (all || opts.loops)
3890
+ result.loops = await ingestLoops(db, opts.verbose);
3479
3891
  if (opts.dedupe !== false)
3480
3892
  result.deduped = dedupeRequests(db);
3481
3893
  return result;
@@ -4789,11 +5201,18 @@ class LocalStore {
4789
5201
  return query.since ? queryAgentBreakdownSince(this.db(), query.since) : queryAgentBreakdown(this.db(), query.period ?? "all", query.machine);
4790
5202
  }
4791
5203
  async projectBreakdown(query = {}) {
4792
- return query.since ? queryProjectBreakdownSince(this.db(), query.since) : queryProjectBreakdown(this.db(), query.period ?? "all", query.machine);
5204
+ return query.since ? queryProjectBreakdownSince(this.db(), query.since, query.machine) : queryProjectBreakdown(this.db(), query.period ?? "all", query.machine);
4793
5205
  }
4794
5206
  async accountBreakdown(query = {}) {
4795
5207
  return query.since ? queryAccountBreakdownSince(this.db(), query.since) : queryAccountBreakdown(this.db(), query.period ?? "all", query.machine);
4796
5208
  }
5209
+ async costCenterBreakdown(query = {}) {
5210
+ return queryCostCenterBreakdown(this.db(), query.period ?? "all", {
5211
+ kind: query.kind,
5212
+ machine: query.machine,
5213
+ since: query.since
5214
+ });
5215
+ }
4797
5216
  async accounts(period) {
4798
5217
  return queryAccountBreakdown(this.db(), period);
4799
5218
  }
@@ -4978,11 +5397,19 @@ class ApiStore {
4978
5397
  return cloudListItems(this.cloud, "breakdown", q({ by: "agent", period: query.period, since: query.since }));
4979
5398
  }
4980
5399
  async projectBreakdown(query = {}) {
4981
- return cloudListItems(this.cloud, "breakdown", q({ by: "project", period: query.period, since: query.since }));
5400
+ return cloudListItems(this.cloud, "breakdown", q({ by: "project", period: query.period, since: query.since, machine: query.machine }));
4982
5401
  }
4983
5402
  async accountBreakdown(query = {}) {
4984
5403
  return cloudListItems(this.cloud, "breakdown", q({ by: "account", period: query.period, since: query.since }));
4985
5404
  }
5405
+ async costCenterBreakdown(query = {}) {
5406
+ const rows = await cloudListItems(this.cloud, "breakdown", q({
5407
+ by: query.kind ?? "cost-center",
5408
+ period: query.period,
5409
+ since: query.since
5410
+ }));
5411
+ return rows.filter((row) => Boolean(row?.cost_center_id));
5412
+ }
4986
5413
  async accounts(period) {
4987
5414
  return cloudListItems(this.cloud, "accounts", q({ period }));
4988
5415
  }
@@ -5056,6 +5483,7 @@ class ApiStore {
5056
5483
  const created = await this.cloud.client.create("budgets", {
5057
5484
  project_path: input.project_path,
5058
5485
  ...input.agent ? { agent: input.agent } : {},
5486
+ ...input.cost_center_id ? { cost_center_id: input.cost_center_id } : {},
5059
5487
  period: input.period,
5060
5488
  limit_usd: input.limit_usd,
5061
5489
  alert_at_percent: input.alert_at_percent
@@ -5146,6 +5574,7 @@ function buildServer() {
5146
5574
  name: MCP_NAME,
5147
5575
  version: packageMetadata.version
5148
5576
  });
5577
+ const SYNC_SOURCES = ["all", ...AGENTS, "loops"];
5149
5578
  const TOOL_NAMES = [
5150
5579
  "get_cost_summary",
5151
5580
  "get_sessions",
@@ -5154,6 +5583,7 @@ function buildServer() {
5154
5583
  "get_project_breakdown",
5155
5584
  "get_agent_breakdown",
5156
5585
  "get_account_breakdown",
5586
+ "get_cost_center_breakdown",
5157
5587
  "get_budget_status",
5158
5588
  "set_budget",
5159
5589
  "remove_budget",
@@ -5186,30 +5616,31 @@ function buildServer() {
5186
5616
  get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
5187
5617
  get_sessions: `agent(${AGENTS.join("|")}), project(partial), account?(key/name/email), machine?(hostname), limit(20) -> compact session table`,
5188
5618
  get_top_sessions: `n(10), agent(${AGENTS.join("|")}) -> top sessions by cost`,
5189
- list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
5190
- get_model_breakdown: "no params -> model, requests, tokens, cost",
5191
- get_project_breakdown: "period?(today|week|month|year|all) -> project_name, sessions, tokens, cost",
5619
+ list_machines: "limit(20), verbose?, json? -> machine_id, sessions, requests, cost, last_active",
5620
+ get_model_breakdown: "limit(20), verbose?, json? -> model, requests, tokens, cost",
5621
+ get_project_breakdown: "period?(today|week|month|year|all), limit(20), verbose?, json? -> project_name, sessions, tokens, cost",
5192
5622
  get_agent_breakdown: "period?(today|week|month|year|all) -> agent, sessions, requests, tokens, api-equivalent, billable, included",
5193
- get_account_breakdown: "period?(today|week|month|year|all) -> account profile, sessions, requests, tokens, api-equivalent, billable, included",
5194
- get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
5195
- set_budget: "period(daily|weekly|monthly), limit_usd, project_path?, agent?, alert_at_percent? -> create budget",
5623
+ get_account_breakdown: "period?(today|week|month|year|all), limit(20), verbose?, json? -> account profile, sessions, requests, tokens, api-equivalent, billable, included",
5624
+ get_cost_center_breakdown: "period?(today|week|month|year|all), kind?(loop|app|repo|service|team) -> cost center, sessions, requests, tokens, cost",
5625
+ get_budget_status: "limit(20), verbose?, json? -> budget limits, current spend, percent_used, is_over_alert",
5626
+ set_budget: "period(daily|weekly|monthly), limit_usd, project_path?, agent?, cost_center_id?, alert_at_percent? -> create budget",
5196
5627
  remove_budget: "id -> delete budget",
5197
- get_pricing: "no params -> model pricing rows with input, output, cache read/write, and cache storage rates",
5628
+ get_pricing: "limit(20), verbose?, json? -> model pricing rows with input, output, cache read/write, and cache storage rates",
5198
5629
  set_pricing: "model, input_per_1m, output_per_1m, cache_read_per_1m?, cache_write_per_1m?, cache_write_1h_per_1m?, cache_storage_per_1m_hour? -> create/update pricing",
5199
5630
  remove_pricing: "model -> delete pricing row",
5200
5631
  get_daily: "days(30) -> daily cost table grouped by date and agent",
5201
5632
  get_billing_summary: "period(today|yesterday|week|month|year|all) -> actual provider billing totals",
5202
- get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
5203
- get_usage: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}) -> usage snapshots and all-machine summary`,
5204
- get_savings: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}) -> subscription/API-equivalent savings`,
5633
+ get_session_detail: "session_id(prefix ok), limit(20), verbose? -> per-request breakdown with model, tokens, cost",
5634
+ get_usage: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}), limit(20), json? -> usage snapshots and all-machine summary`,
5635
+ get_savings: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}), limit(20), json? -> subscription/API-equivalent savings`,
5205
5636
  estimate_cost: "model, input_tokens?, output_tokens? -> pre-flight cost estimate priced from the store pricing table",
5206
5637
  list_subscriptions: "no params -> configured subscription plans and included usage",
5207
- set_subscription: `provider, plan, monthly_fee_usd?, included_usage_usd?, agent?(${AGENTS.join("|")}) -> create/update subscription plan`,
5638
+ set_subscription: `provider, plan, monthly_fee_usd?, included_usage_usd?, agent?(${AGENTS.join("|")}), json? -> create/update subscription plan`,
5208
5639
  remove_subscription: "id -> delete subscription plan",
5209
- sync: `sources(all|${AGENTS.join("|")}) -> ingest latest cost data`,
5640
+ sync: `sources(${SYNC_SOURCES.join("|")}), json? -> ingest latest cost data`,
5210
5641
  search_tools: "query substring -> tool name list",
5211
5642
  describe_tools: "names[] -> one-line parameter hints",
5212
- get_goals: "no params -> goal progress summary",
5643
+ get_goals: "limit(20), verbose?, json? -> goal progress summary",
5213
5644
  set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
5214
5645
  remove_goal: "id -> delete goal",
5215
5646
  register_agent: "name, session_id? -> register agent session",
@@ -5220,6 +5651,20 @@ function buildServer() {
5220
5651
  };
5221
5652
  const fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
5222
5653
  const fmtTok = (n) => n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
5654
+ const DEFAULT_MCP_ROW_LIMIT = 20;
5655
+ function clampLimit(limit, fallback = DEFAULT_MCP_ROW_LIMIT, max = 100) {
5656
+ if (limit == null)
5657
+ return fallback;
5658
+ if (!Number.isFinite(limit))
5659
+ return fallback;
5660
+ return Math.max(1, Math.min(max, Math.floor(limit)));
5661
+ }
5662
+ function rowLimit(limit, verbose) {
5663
+ return verbose ? Number.POSITIVE_INFINITY : clampLimit(limit);
5664
+ }
5665
+ function hiddenRowsHint(total, shown, noun) {
5666
+ return total > shown ? `... ${total - shown} more ${noun} hidden; call with limit, verbose=true, or json=true.` : undefined;
5667
+ }
5223
5668
  function fmtSession(s) {
5224
5669
  const id = String(s["id"] ?? "").slice(0, 8);
5225
5670
  const agent = String(s["agent"] ?? "");
@@ -5228,6 +5673,26 @@ function buildServer() {
5228
5673
  const tok = fmtTok(Number(s["total_tokens"] ?? 0));
5229
5674
  return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
5230
5675
  }
5676
+ function compactPrimitive(value) {
5677
+ if (value == null)
5678
+ return "none";
5679
+ if (typeof value === "number")
5680
+ return Number.isFinite(value) ? value.toLocaleString("en-US") : String(value);
5681
+ if (typeof value === "boolean")
5682
+ return value ? "yes" : "no";
5683
+ return String(value);
5684
+ }
5685
+ function compactObject(value) {
5686
+ if (value == null)
5687
+ return "none";
5688
+ if (Array.isArray(value))
5689
+ return `${value.length} rows`;
5690
+ if (typeof value !== "object")
5691
+ return compactPrimitive(value);
5692
+ const entries = Object.entries(value);
5693
+ const primitives = entries.filter(([, entry]) => entry == null || ["string", "number", "boolean"].includes(typeof entry)).slice(0, 6).map(([key, entry]) => `${key}=${compactPrimitive(entry)}`);
5694
+ return primitives.length ? primitives.join(", ") : `${entries.length} fields`;
5695
+ }
5231
5696
  function text(text2) {
5232
5697
  return { content: [{ type: "text", text: text2 }] };
5233
5698
  }
@@ -5282,22 +5747,34 @@ function buildServer() {
5282
5747
  return text(lines.join(`
5283
5748
  `));
5284
5749
  });
5285
- server.tool("get_model_breakdown", "Cost per model. No params.", {}, async () => {
5750
+ server.tool("get_model_breakdown", "Cost per model. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
5286
5751
  const rows = await store.modelBreakdown();
5752
+ if (json)
5753
+ return text(JSON.stringify(rows, null, 2));
5754
+ const visibleRows = rows.slice(0, rowLimit(limit, verbose));
5287
5755
  const lines = ["model agent reqs tokens cost"];
5288
- for (const row of rows) {
5756
+ for (const row of visibleRows) {
5289
5757
  lines.push(`${String(row["model"]).slice(0, 30).padEnd(31)}${String(row["agent"]).padEnd(10)}${String(row["requests"]).padEnd(8)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
5290
5758
  }
5759
+ const hint = hiddenRowsHint(rows.length, visibleRows.length, "models");
5760
+ if (hint)
5761
+ lines.push(hint);
5291
5762
  return text(lines.join(`
5292
5763
  `));
5293
5764
  });
5294
- server.tool("get_project_breakdown", "Cost per project. Params: period(today|week|month|year|all).", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
5765
+ server.tool("get_project_breakdown", "Cost per project. Params: period(today|week|month|year|all), limit(20), verbose, json.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ period, limit, verbose, json }) => {
5295
5766
  const rows = await store.projectBreakdown({ period: period ?? "all" });
5767
+ if (json)
5768
+ return text(JSON.stringify(rows, null, 2));
5769
+ const visibleRows = rows.slice(0, rowLimit(limit, verbose));
5296
5770
  const lines = ["project sessions tokens cost"];
5297
- for (const row of rows) {
5771
+ for (const row of visibleRows) {
5298
5772
  const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
5299
5773
  lines.push(`${name.padEnd(21)}${String(row["sessions"]).padEnd(9)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
5300
5774
  }
5775
+ const hint = hiddenRowsHint(rows.length, visibleRows.length, "projects");
5776
+ if (hint)
5777
+ lines.push(hint);
5301
5778
  return text(lines.join(`
5302
5779
  `));
5303
5780
  });
@@ -5312,29 +5789,55 @@ function buildServer() {
5312
5789
  return text(lines.join(`
5313
5790
  `));
5314
5791
  });
5315
- server.tool("get_account_breakdown", "Cost per account/profile. Params: period(today|week|month|year|all). Shows API-equivalent, billable API, and subscription-included usage.", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
5792
+ server.tool("get_account_breakdown", "Cost per account/profile. Params: period(today|week|month|year|all), limit(20), verbose, json. Shows API-equivalent, billable API, and subscription-included usage.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ period, limit, verbose, json }) => {
5316
5793
  const rows = await store.accountBreakdown({ period: period ?? "all" });
5794
+ if (json)
5795
+ return text(JSON.stringify(rows, null, 2));
5317
5796
  if (rows.length === 0)
5318
5797
  return text("No account-attributed sessions yet.");
5798
+ const visibleRows = rows.slice(0, rowLimit(limit, verbose));
5319
5799
  const lines = ["account agent sessions requests tokens api_eq billable included"];
5320
- for (const row of rows) {
5800
+ for (const row of visibleRows) {
5321
5801
  const label = String(row["account_email"] || row["account_name"] || row["account_key"] || "\u2014").slice(0, 20);
5322
5802
  lines.push(`${label.padEnd(21)}` + `${String(row["account_tool"] ?? "").slice(0, 10).padEnd(11)}` + `${String(row["sessions"]).padEnd(9)}` + `${String(row["requests"]).padEnd(9)}` + `${fmtTok(Number(row["total_tokens"])).padEnd(9)}` + `${fmtUsd(Number(row["api_equivalent_usd"] ?? row["cost_usd"])).padEnd(10)}` + `${fmtUsd(Number(row["billable_usd"] ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row["subscription_included_usd"] ?? 0))}`);
5323
5803
  }
5804
+ const hint = hiddenRowsHint(rows.length, visibleRows.length, "accounts");
5805
+ if (hint)
5806
+ lines.push(hint);
5807
+ return text(lines.join(`
5808
+ `));
5809
+ });
5810
+ server.tool("get_cost_center_breakdown", "Cost per cost center. Params: period(today|week|month|year|all), kind(loop|app|repo|service|team).", {
5811
+ period: z.enum(["today", "week", "month", "year", "all"]).optional(),
5812
+ kind: z.enum(["loop", "app", "repo", "service", "team"]).optional()
5813
+ }, async ({ period, kind }) => {
5814
+ const rows = await store.costCenterBreakdown({ period: period ?? "all", kind });
5815
+ if (rows.length === 0)
5816
+ return text("No cost-center usage yet.");
5817
+ const lines = ["kind cost_center sessions requests tokens cost"];
5818
+ for (const row of rows) {
5819
+ lines.push(`${String(row["kind"]).slice(0, 8).padEnd(9)}` + `${String(row["name"] || row["cost_center_id"] || "\u2014").slice(0, 20).padEnd(21)}` + `${String(row["sessions"]).padEnd(9)}` + `${String(row["requests"]).padEnd(9)}` + `${fmtTok(Number(row["total_tokens"])).padEnd(9)}` + `${fmtUsd(Number(row["cost_usd"]))}`);
5820
+ }
5324
5821
  return text(lines.join(`
5325
5822
  `));
5326
5823
  });
5327
- server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. No params.", {}, async () => {
5824
+ server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
5328
5825
  const budgets = await store.listBudgets();
5826
+ if (json)
5827
+ return text(JSON.stringify(budgets, null, 2));
5329
5828
  if (budgets.length === 0)
5330
5829
  return text("No budgets set.");
5830
+ const visibleBudgets = budgets.slice(0, rowLimit(limit, verbose));
5331
5831
  const lines = ["scope period spent limit used% status"];
5332
- for (const budget of budgets) {
5333
- const scope = String(budget["project_path"] ?? "global").slice(0, 20);
5832
+ for (const budget of visibleBudgets) {
5833
+ const scope = String(budget["cost_center_id"] ?? budget["project_path"] ?? "global").slice(0, 20);
5334
5834
  const pct = Number(budget["percent_used"]).toFixed(1);
5335
5835
  const status = budget["is_over_limit"] ? "OVER" : budget["is_over_alert"] ? "ALERT" : "OK";
5336
5836
  lines.push(`${scope.padEnd(21)}${String(budget["period"]).padEnd(9)}${fmtUsd(Number(budget["current_spend_usd"])).padEnd(11)}${fmtUsd(Number(budget["limit_usd"])).padEnd(11)}${pct}%`.padEnd(49) + ` ${status}`);
5337
5837
  }
5838
+ const hint = hiddenRowsHint(budgets.length, visibleBudgets.length, "budgets");
5839
+ if (hint)
5840
+ lines.push(hint);
5338
5841
  return text(lines.join(`
5339
5842
  `));
5340
5843
  });
@@ -5343,11 +5846,13 @@ function buildServer() {
5343
5846
  limit_usd: z.number().positive(),
5344
5847
  project_path: z.string().optional(),
5345
5848
  agent: z.enum(AGENTS).optional(),
5849
+ cost_center_id: z.string().optional(),
5346
5850
  alert_at_percent: z.number().positive().max(100).optional()
5347
- }, async ({ period, limit_usd, project_path, agent, alert_at_percent }) => {
5851
+ }, async ({ period, limit_usd, project_path, agent, cost_center_id, alert_at_percent }) => {
5348
5852
  const id = await store.setBudget({
5349
5853
  project_path: project_path ?? null,
5350
5854
  agent: agent ?? null,
5855
+ cost_center_id: cost_center_id ?? null,
5351
5856
  period,
5352
5857
  limit_usd,
5353
5858
  alert_at_percent: alert_at_percent ?? 80
@@ -5358,12 +5863,18 @@ function buildServer() {
5358
5863
  await store.removeBudget(id);
5359
5864
  return text("Budget removed.");
5360
5865
  });
5361
- server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage.", {}, async () => {
5866
+ server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
5362
5867
  const rows = await store.listPricing();
5868
+ if (json)
5869
+ return text(JSON.stringify(rows, null, 2));
5870
+ const visibleRows = rows.slice(0, rowLimit(limit, verbose));
5363
5871
  const lines = ["model input output cache-r cache-w cache-1h storage-h"];
5364
- for (const row of rows) {
5872
+ for (const row of visibleRows) {
5365
5873
  lines.push(`${row.model.slice(0, 30).padEnd(31)}` + `${fmtUsd(row.input_per_1m).padEnd(9)}` + `${fmtUsd(row.output_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_read_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_write_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_write_1h_per_1m ?? 0).padEnd(9)}` + `${fmtUsd(row.cache_storage_per_1m_hour ?? 0)}`);
5366
5874
  }
5875
+ const hint = hiddenRowsHint(rows.length, visibleRows.length, "pricing rows");
5876
+ if (hint)
5877
+ lines.push(hint);
5367
5878
  return text(lines.join(`
5368
5879
  `));
5369
5880
  });
@@ -5428,11 +5939,14 @@ function buildServer() {
5428
5939
  return text(lines.join(`
5429
5940
  `));
5430
5941
  });
5431
- server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok)", { session_id: z.string() }, async ({ session_id }) => {
5942
+ server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok), limit(20), verbose.", { session_id: z.string(), limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional() }, async ({ session_id, limit, verbose }) => {
5432
5943
  const detail = await store.sessionDetail(session_id);
5433
5944
  if (!detail)
5434
5945
  return textError(`Session not found: ${session_id}`);
5435
- const { session, requests } = detail;
5946
+ const { session, requests: allRequests } = detail;
5947
+ const requestLimit = clampLimit(limit, verbose ? 50 : DEFAULT_MCP_ROW_LIMIT);
5948
+ const requestCount = { count: allRequests.length };
5949
+ const requests = allRequests.slice(0, requestLimit);
5436
5950
  const lines = [
5437
5951
  `session: ${String(session["id"]).slice(0, 16)}`,
5438
5952
  `agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
@@ -5440,27 +5954,85 @@ function buildServer() {
5440
5954
  "",
5441
5955
  "time model input output cache-r cache-5m cache-1h cost"
5442
5956
  ];
5443
- for (const request of requests.slice(0, 50)) {
5957
+ for (const request of requests) {
5444
5958
  lines.push(`${String(request["timestamp"]).slice(11, 19)} ` + `${String(request["model"]).slice(0, 22).padEnd(23)}` + `${fmtTok(Number(request["input_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["output_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["cache_read_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["cache_create_5m_tokens"] ?? request["cache_create_tokens"] ?? 0)).padEnd(9)}` + `${fmtTok(Number(request["cache_create_1h_tokens"] ?? 0)).padEnd(9)}` + `${fmtUsd(Number(request["cost_usd"]))}`);
5445
5959
  }
5960
+ if (requestCount.count > requests.length) {
5961
+ lines.push(`... ${requestCount.count - requests.length} more requests hidden; call with limit or verbose=true for more.`);
5962
+ }
5446
5963
  return text(lines.join(`
5447
5964
  `));
5448
5965
  });
5449
- server.tool("sync", `Ingest new cost data. sources: all|${AGENTS.join("|")}`, { sources: z.enum(["all", ...AGENTS]).optional() }, async ({ sources }) => {
5966
+ server.tool("sync", `Ingest new cost data. sources: ${SYNC_SOURCES.join("|")}. Set json=true for the full result object.`, { sources: z.enum([...SYNC_SOURCES]).optional(), json: z.boolean().optional() }, async ({ sources, json }) => {
5450
5967
  if (isCloudStore()) {
5451
5968
  return text("cloud mode: ingest is a local-only operation; nothing to sync (reads/writes route to the cloud API)");
5452
5969
  }
5453
5970
  const selected = sources ?? "all";
5454
5971
  const opts = selected === "all" ? {} : { [selected]: true };
5455
5972
  const result = await syncAll(localDb(), opts);
5456
- return text(JSON.stringify(result, null, 2));
5973
+ if (json)
5974
+ return text(JSON.stringify(result, null, 2));
5975
+ const lines = [
5976
+ `sync: ${selected}`,
5977
+ `deduped: ${result.deduped}`
5978
+ ];
5979
+ for (const [source, value] of Object.entries(result)) {
5980
+ if (source === "deduped")
5981
+ continue;
5982
+ if (value == null)
5983
+ continue;
5984
+ lines.push(`${source}: ${compactObject(value)}`);
5985
+ }
5986
+ lines.push("Use json=true for the full sync result.");
5987
+ return text(lines.join(`
5988
+ `));
5457
5989
  });
5458
- server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month|year|all", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
5990
+ server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month|year|all, agent, limit(20). Set json=true for full data.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional(), limit: z.number().int().positive().max(100).optional(), json: z.boolean().optional() }, async ({ period, agent, limit, json }) => {
5459
5991
  const p = period ?? "month";
5460
- return text(JSON.stringify(await store.usage(p, agent), null, 2));
5992
+ const { snapshots: snaps, summary } = await store.usage(p, agent);
5993
+ if (json)
5994
+ return text(JSON.stringify({ snapshots: snaps, summary }, null, 2));
5995
+ const snapshotLimit = clampLimit(limit);
5996
+ const lines = [
5997
+ `period: ${p}${agent ? ` agent: ${agent}` : ""}`,
5998
+ `fleet: ${fmtUsd(summary.total_usd)} sessions: ${summary.sessions} requests: ${summary.requests.toLocaleString()} tokens: ${fmtTok(summary.tokens)}`,
5999
+ `snapshots: ${snaps.length}${snaps.length > snapshotLimit ? ` (showing ${snapshotLimit})` : ""}`
6000
+ ];
6001
+ if (snaps.length > 0) {
6002
+ lines.push("", "date agent metric value unit machine");
6003
+ for (const snapshot of snaps.slice(0, snapshotLimit)) {
6004
+ lines.push(`${snapshot.date.padEnd(12)}` + `${snapshot.agent.slice(0, 10).padEnd(11)}` + `${snapshot.metric.slice(0, 27).padEnd(28)}` + `${compactPrimitive(snapshot.value).padEnd(13)}` + `${(snapshot.unit ?? "").slice(0, 8).padEnd(9)}` + `${snapshot.machine_id ?? "\u2014"}`);
6005
+ }
6006
+ if (snaps.length > snapshotLimit)
6007
+ lines.push(`... ${snaps.length - snapshotLimit} more snapshots hidden; call with limit or json=true.`);
6008
+ }
6009
+ return text(lines.join(`
6010
+ `));
5461
6011
  });
5462
- server.tool("get_savings", "Subscription vs API savings summary", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
5463
- return text(JSON.stringify(await store.savings(period ?? "month", agent), null, 2));
6012
+ server.tool("get_savings", "Subscription vs API savings summary. Params: period, agent, limit(20). Set json=true for full data.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional(), limit: z.number().int().positive().max(100).optional(), json: z.boolean().optional() }, async ({ period, agent, limit, json }) => {
6013
+ const savings = await store.savings(period ?? "month", agent);
6014
+ if (json)
6015
+ return text(JSON.stringify(savings, null, 2));
6016
+ const lines = [
6017
+ `period: ${savings.period}${agent ? ` agent: ${agent}` : ""}`,
6018
+ `api_equivalent: ${fmtUsd(savings.api_equivalent_usd)}`,
6019
+ `subscription_fee: ${fmtUsd(savings.subscription_fee_usd)}`,
6020
+ `included_consumed: ${fmtUsd(savings.included_consumed_usd)}`,
6021
+ `on_demand: ${fmtUsd(savings.on_demand_usd)}`,
6022
+ `saved: ${fmtUsd(savings.saved_usd)}`
6023
+ ];
6024
+ const rows = Object.entries(savings.by_agent ?? {});
6025
+ if (rows.length > 0) {
6026
+ const agentLimit = clampLimit(limit);
6027
+ lines.push("", `by_agent: ${rows.length}${rows.length > agentLimit ? ` (showing ${agentLimit})` : ""}`, "agent api_eq sub_fee included on_demand saved");
6028
+ for (const [agentName, row] of rows.slice(0, agentLimit)) {
6029
+ lines.push(`${agentName.slice(0, 10).padEnd(11)}` + `${fmtUsd(Number(row.api_equivalent_usd ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row.subscription_fee_usd ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row.included_consumed_usd ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row.on_demand_usd ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row.saved_usd ?? 0))}`);
6030
+ }
6031
+ if (rows.length > agentLimit)
6032
+ lines.push(`... ${rows.length - agentLimit} more agents hidden; call with limit or json=true.`);
6033
+ }
6034
+ return text(lines.join(`
6035
+ `));
5464
6036
  });
5465
6037
  server.tool("list_subscriptions", "List configured subscription plans used by savings calculations.", {}, async () => {
5466
6038
  const rows = await store.listSubscriptions();
@@ -5482,7 +6054,8 @@ function buildServer() {
5482
6054
  included_usage_usd: z.number().nonnegative().optional(),
5483
6055
  billing_cycle_start: z.string().optional(),
5484
6056
  reset_policy: z.string().optional(),
5485
- active: z.boolean().optional()
6057
+ active: z.boolean().optional(),
6058
+ json: z.boolean().optional()
5486
6059
  }, async (input) => {
5487
6060
  const row = await store.setSubscription({
5488
6061
  id: input.id,
@@ -5495,7 +6068,14 @@ function buildServer() {
5495
6068
  reset_policy: input.reset_policy ?? "monthly",
5496
6069
  active: input.active === false ? 0 : 1
5497
6070
  });
5498
- return text(JSON.stringify(row, null, 2));
6071
+ if (input.json)
6072
+ return text(JSON.stringify(row, null, 2));
6073
+ return text(`Subscription set: ${row.id}
6074
+ provider: ${row.provider}
6075
+ plan: ${row.plan}
6076
+ agent: ${row.agent ?? "all"}
6077
+ fee: ${fmtUsd(row.monthly_fee_usd)}
6078
+ included: ${fmtUsd(row.included_usage_usd)}`);
5499
6079
  });
5500
6080
  server.tool("remove_subscription", "Delete a subscription plan by id.", { id: z.string() }, async ({ id }) => {
5501
6081
  await store.removeSubscription(id);
@@ -5505,17 +6085,23 @@ function buildServer() {
5505
6085
  const cost = await store.estimate({ model, inputTokens: input_tokens ?? 0, outputTokens: output_tokens ?? 0 });
5506
6086
  return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
5507
6087
  });
5508
- server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
6088
+ server.tool("get_goals", "All spending goals with current progress. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
5509
6089
  const goals = await store.listGoals();
6090
+ if (json)
6091
+ return text(JSON.stringify(goals, null, 2));
5510
6092
  if (goals.length === 0)
5511
6093
  return text("No goals set.");
6094
+ const visibleGoals = goals.slice(0, rowLimit(limit, verbose));
5512
6095
  const lines = ["period scope limit spent used% status"];
5513
- for (const goal of goals) {
6096
+ for (const goal of visibleGoals) {
5514
6097
  const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
5515
6098
  const pct = Number(goal["percent_used"]).toFixed(1);
5516
6099
  const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
5517
6100
  lines.push(`${String(goal["period"]).padEnd(9)}${scope.padEnd(21)}${fmtUsd(Number(goal["limit_usd"])).padEnd(11)}${fmtUsd(Number(goal["current_spend_usd"])).padEnd(11)}${pct}% ${status}`);
5518
6101
  }
6102
+ const hint = hiddenRowsHint(goals.length, visibleGoals.length, "goals");
6103
+ if (hint)
6104
+ lines.push(hint);
5519
6105
  return text(lines.join(`
5520
6106
  `));
5521
6107
  });
@@ -5537,14 +6123,20 @@ function buildServer() {
5537
6123
  await store.removeGoal(id);
5538
6124
  return text("Goal removed.");
5539
6125
  });
5540
- server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
6126
+ server.tool("list_machines", "List all machines that have synced data. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
5541
6127
  const machines = await store.machines();
6128
+ if (json)
6129
+ return text(JSON.stringify(machines, null, 2));
5542
6130
  if (machines.length === 0)
5543
6131
  return text(`No machine data yet. Current machine: ${getMachineId()}`);
6132
+ const visibleMachines = machines.slice(0, rowLimit(limit, verbose));
5544
6133
  const lines = ["machine sessions requests cost last_active"];
5545
- for (const m of machines) {
6134
+ for (const m of visibleMachines) {
5546
6135
  lines.push(`${m.machine_id.padEnd(17)}${String(m.sessions).padEnd(10)}${String(m.requests).padEnd(10)}${fmtUsd(m.total_cost_usd).padEnd(12)}${m.last_active?.substring(0, 16) ?? "\u2014"}`);
5547
6136
  }
6137
+ const hint = hiddenRowsHint(machines.length, visibleMachines.length, "machines");
6138
+ if (hint)
6139
+ lines.push(hint);
5548
6140
  lines.push(`
5549
6141
  current machine: ${getMachineId()}`);
5550
6142
  return text(lines.join(`
@@ -5563,7 +6155,6 @@ current machine: ${getMachineId()}`);
5563
6155
  return textError(String(error));
5564
6156
  }
5565
6157
  });
5566
- registerCloudTools(server, MCP_NAME);
5567
6158
  return server;
5568
6159
  }
5569
6160