@hasna/economy 0.3.6 → 0.3.7

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.
package/dist/mcp/index.js CHANGED
@@ -669,6 +669,16 @@ function addColumnIfMissing(db, table, column, definition) {
669
669
  }
670
670
  function initSchema(db) {
671
671
  db.exec(`
672
+ CREATE TABLE IF NOT EXISTS cost_centers (
673
+ id TEXT PRIMARY KEY,
674
+ kind TEXT NOT NULL,
675
+ name TEXT NOT NULL,
676
+ repo_path TEXT,
677
+ labels_json TEXT DEFAULT '{}',
678
+ created_at TEXT NOT NULL,
679
+ updated_at TEXT NOT NULL
680
+ );
681
+
672
682
  CREATE TABLE IF NOT EXISTS requests (
673
683
  id TEXT PRIMARY KEY,
674
684
  agent TEXT NOT NULL,
@@ -685,6 +695,7 @@ function initSchema(db) {
685
695
  timestamp TEXT NOT NULL,
686
696
  source_request_id TEXT,
687
697
  machine_id TEXT DEFAULT '',
698
+ cost_center_id TEXT,
688
699
  account_key TEXT DEFAULT '',
689
700
  account_tool TEXT DEFAULT '',
690
701
  account_name TEXT DEFAULT '',
@@ -703,6 +714,7 @@ function initSchema(db) {
703
714
  total_tokens INTEGER DEFAULT 0,
704
715
  request_count INTEGER DEFAULT 0,
705
716
  machine_id TEXT DEFAULT '',
717
+ cost_center_id TEXT,
706
718
  account_key TEXT DEFAULT '',
707
719
  account_tool TEXT DEFAULT '',
708
720
  account_name TEXT DEFAULT '',
@@ -723,6 +735,7 @@ function initSchema(db) {
723
735
  id TEXT PRIMARY KEY,
724
736
  project_path TEXT,
725
737
  agent TEXT,
738
+ cost_center_id TEXT,
726
739
  period TEXT NOT NULL,
727
740
  limit_usd REAL NOT NULL,
728
741
  alert_at_percent INTEGER DEFAULT 80,
@@ -753,6 +766,7 @@ function initSchema(db) {
753
766
  CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
754
767
  CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
755
768
  CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
769
+ CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
756
770
 
757
771
  CREATE TABLE IF NOT EXISTS model_pricing (
758
772
  model TEXT PRIMARY KEY,
@@ -839,6 +853,12 @@ function initSchema(db) {
839
853
  `);
840
854
  addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
841
855
  addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
856
+ if (addColumnIfMissing(db, "cost_centers", "updated_at", `TEXT DEFAULT ''`)) {
857
+ db.exec(`UPDATE cost_centers SET updated_at = created_at WHERE updated_at = '' OR updated_at IS NULL`);
858
+ }
859
+ addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
860
+ addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
861
+ addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
842
862
  if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
843
863
  db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
844
864
  }
@@ -867,6 +887,10 @@ function initSchema(db) {
867
887
  CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
868
888
  CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
869
889
  CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
890
+ CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id);
891
+ CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id);
892
+ CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
893
+ CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
870
894
  `);
871
895
  }
872
896
  function periodWhere(period) {
@@ -917,6 +941,14 @@ function requestPeriodWhere(period) {
917
941
  return "1=1";
918
942
  }
919
943
  }
944
+ function upsertCostCenter(db, center) {
945
+ const updatedAt = center.updated_at ?? center.created_at;
946
+ db.prepare(`
947
+ INSERT OR REPLACE INTO cost_centers
948
+ (id, kind, name, repo_path, labels_json, created_at, updated_at)
949
+ VALUES (?, ?, ?, ?, ?, ?, ?)
950
+ `).run(center.id, center.kind, center.name, center.repo_path ?? null, center.labels_json ?? "{}", center.created_at, updatedAt);
951
+ }
920
952
  function upsertRequest(db, req) {
921
953
  const now = req.updated_at ?? new Date().toISOString();
922
954
  db.prepare(`
@@ -924,10 +956,10 @@ function upsertRequest(db, req) {
924
956
  (id, agent, session_id, model, input_tokens, output_tokens,
925
957
  cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
926
958
  cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
927
- source_request_id, machine_id, attribution_tag, account_key, account_tool,
959
+ source_request_id, machine_id, cost_center_id, attribution_tag, account_key, account_tool,
928
960
  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 ?? "");
961
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
962
+ `).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
963
  }
932
964
  function upsertSession(db, session) {
933
965
  const now = session.updated_at ?? new Date().toISOString();
@@ -935,9 +967,9 @@ function upsertSession(db, session) {
935
967
  INSERT OR REPLACE INTO sessions
936
968
  (id, agent, project_path, project_name, started_at, ended_at,
937
969
  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 ?? "");
970
+ cost_center_id, account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
971
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
972
+ `).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
973
  }
942
974
  function rollupSession(db, sessionId) {
943
975
  db.prepare(`
@@ -963,9 +995,12 @@ function rollupSession(db, sessionId) {
963
995
  ELSE account_email END,
964
996
  account_source = CASE WHEN account_source = '' OR account_source IS NULL
965
997
  THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
966
- ELSE account_source END
998
+ ELSE account_source END,
999
+ cost_center_id = CASE WHEN cost_center_id = '' OR cost_center_id IS NULL
1000
+ 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)
1001
+ ELSE cost_center_id END
967
1002
  WHERE id = ?
968
- `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
1003
+ `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
969
1004
  }
970
1005
  function querySessions(db, filter = {}) {
971
1006
  const conditions = [];
@@ -1392,6 +1427,139 @@ function queryAccountBreakdown(db, period = "all", machine) {
1392
1427
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1393
1428
  return result;
1394
1429
  }
1430
+ function kindFromCostCenterId(id) {
1431
+ const prefix = id.split(":", 1)[0] ?? "";
1432
+ return ["loop", "app", "repo", "service", "team"].includes(prefix) ? prefix : "";
1433
+ }
1434
+ function addCostCenterBreakdownRow(groups, row, sessionOnly) {
1435
+ if (!row.cost_center_id)
1436
+ return;
1437
+ const derivedKind = row.kind || kindFromCostCenterId(row.cost_center_id) || "service";
1438
+ const group = groups.get(row.cost_center_id) ?? {
1439
+ cost_center_id: row.cost_center_id,
1440
+ kind: derivedKind,
1441
+ name: row.name || row.cost_center_id,
1442
+ repo_path: row.repo_path ?? null,
1443
+ labels_json: row.labels_json || "{}",
1444
+ sessionIds: new Set,
1445
+ requests: 0,
1446
+ total_tokens: 0,
1447
+ api_equivalent_usd: 0,
1448
+ metered_api_usd: 0,
1449
+ subscription_included_usd: 0,
1450
+ estimated_usd: 0,
1451
+ unknown_usd: 0,
1452
+ last_active: ""
1453
+ };
1454
+ group.kind = derivedKind;
1455
+ if (row.name)
1456
+ group.name = row.name;
1457
+ if (row.repo_path && !group.repo_path)
1458
+ group.repo_path = row.repo_path;
1459
+ if (row.labels_json && row.labels_json !== "{}")
1460
+ group.labels_json = row.labels_json;
1461
+ if (row.session_id)
1462
+ group.sessionIds.add(row.session_id);
1463
+ group.requests += row.requests;
1464
+ group.total_tokens += row.total_tokens;
1465
+ group.api_equivalent_usd += row.cost_usd;
1466
+ if (sessionOnly) {
1467
+ group.estimated_usd += row.cost_usd;
1468
+ } else if (row.cost_basis === "metered_api") {
1469
+ group.metered_api_usd += row.cost_usd;
1470
+ } else if (row.cost_basis === "subscription_included") {
1471
+ group.subscription_included_usd += row.cost_usd;
1472
+ } else if (row.cost_basis === "unknown") {
1473
+ group.unknown_usd += row.cost_usd;
1474
+ } else {
1475
+ group.estimated_usd += row.cost_usd;
1476
+ }
1477
+ if (!group.last_active || row.last_active > group.last_active)
1478
+ group.last_active = row.last_active;
1479
+ groups.set(row.cost_center_id, group);
1480
+ }
1481
+ function queryCostCenterBreakdown(db, period = "all", filter = {}) {
1482
+ const requestWhere = filter.since ? "r.timestamp >= ?" : requestPeriodWhere(period);
1483
+ const sessionWhere = filter.since ? "s.started_at >= ?" : sessionPeriodWhere(period);
1484
+ const requestMachineClause = filter.machine ? " AND r.machine_id = ?" : "";
1485
+ const sessionMachineClause = filter.machine ? " AND s.machine_id = ?" : "";
1486
+ const requestParams = [];
1487
+ const sessionParams = [];
1488
+ if (filter.since) {
1489
+ requestParams.push(filter.since);
1490
+ sessionParams.push(filter.since);
1491
+ }
1492
+ if (filter.machine) {
1493
+ requestParams.push(filter.machine);
1494
+ sessionParams.push(filter.machine);
1495
+ }
1496
+ const groups = new Map;
1497
+ const requestRows = db.prepare(`
1498
+ SELECT
1499
+ COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) as cost_center_id,
1500
+ COALESCE(cc.kind, '') as kind,
1501
+ COALESCE(cc.name, COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))) as name,
1502
+ cc.repo_path as repo_path,
1503
+ COALESCE(cc.labels_json, '{}') as labels_json,
1504
+ r.session_id as session_id,
1505
+ 1 as requests,
1506
+ COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
1507
+ COALESCE(r.cost_usd, 0) as cost_usd,
1508
+ COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
1509
+ r.timestamp as last_active
1510
+ FROM requests r
1511
+ LEFT JOIN sessions s ON s.id = r.session_id
1512
+ LEFT JOIN cost_centers cc ON cc.id = COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))
1513
+ WHERE ${requestWhere}${requestMachineClause}
1514
+ AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) IS NOT NULL
1515
+ AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) != ''
1516
+ `).all(...requestParams);
1517
+ for (const row of requestRows)
1518
+ addCostCenterBreakdownRow(groups, row, false);
1519
+ const sessionOnlyRows = db.prepare(`
1520
+ SELECT
1521
+ s.cost_center_id as cost_center_id,
1522
+ COALESCE(cc.kind, '') as kind,
1523
+ COALESCE(cc.name, s.cost_center_id) as name,
1524
+ cc.repo_path as repo_path,
1525
+ COALESCE(cc.labels_json, '{}') as labels_json,
1526
+ s.id as session_id,
1527
+ COALESCE(s.request_count, 0) as requests,
1528
+ COALESCE(s.total_tokens, 0) as total_tokens,
1529
+ COALESCE(s.total_cost_usd, 0) as cost_usd,
1530
+ 'estimated' as cost_basis,
1531
+ s.started_at as last_active
1532
+ FROM sessions s
1533
+ LEFT JOIN cost_centers cc ON cc.id = s.cost_center_id
1534
+ WHERE ${sessionWhere}${sessionMachineClause}
1535
+ AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
1536
+ AND s.cost_center_id IS NOT NULL
1537
+ AND s.cost_center_id != ''
1538
+ `).all(...sessionParams);
1539
+ for (const row of sessionOnlyRows)
1540
+ addCostCenterBreakdownRow(groups, row, true);
1541
+ const result = [...groups.values()].map((group) => ({
1542
+ cost_center_id: group.cost_center_id,
1543
+ kind: group.kind,
1544
+ name: group.name,
1545
+ repo_path: group.repo_path,
1546
+ labels_json: group.labels_json,
1547
+ sessions: group.sessionIds.size,
1548
+ requests: group.requests,
1549
+ total_tokens: group.total_tokens,
1550
+ api_equivalent_usd: group.api_equivalent_usd,
1551
+ billable_usd: group.metered_api_usd,
1552
+ metered_api_usd: group.metered_api_usd,
1553
+ subscription_included_usd: group.subscription_included_usd,
1554
+ estimated_usd: group.estimated_usd,
1555
+ unknown_usd: group.unknown_usd,
1556
+ cost_usd: group.api_equivalent_usd,
1557
+ last_active: group.last_active
1558
+ }));
1559
+ const filtered = filter.kind ? result.filter((row) => row.kind === filter.kind) : result;
1560
+ filtered.sort((a, b) => b.cost_usd - a.cost_usd);
1561
+ return filtered;
1562
+ }
1395
1563
  function queryProjectBreakdownSince(db, since) {
1396
1564
  return db.prepare(`
1397
1565
  SELECT project_path, project_name,
@@ -1563,9 +1731,9 @@ function deleteProject(db, path) {
1563
1731
  function upsertBudget(db, budget) {
1564
1732
  db.prepare(`
1565
1733
  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);
1734
+ (id, project_path, agent, cost_center_id, period, limit_usd, alert_at_percent, created_at, updated_at)
1735
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1736
+ `).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
1737
  }
1570
1738
  function listBudgets(db) {
1571
1739
  return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
@@ -1587,6 +1755,10 @@ function getBudgetStatuses(db) {
1587
1755
  spendQuery += ` AND agent = ?`;
1588
1756
  params.push(b.agent);
1589
1757
  }
1758
+ if (b.cost_center_id) {
1759
+ spendQuery += ` AND COALESCE(NULLIF(cost_center_id, ''), (SELECT NULLIF(s.cost_center_id, '') FROM sessions s WHERE s.id = requests.session_id)) = ?`;
1760
+ params.push(b.cost_center_id);
1761
+ }
1590
1762
  const row = db.prepare(spendQuery).get(...params);
1591
1763
  const spend = row.spend;
1592
1764
  const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
@@ -3137,32 +3309,219 @@ function statSyncSafe(path) {
3137
3309
  }
3138
3310
  }
3139
3311
 
3312
+ // src/ingest/loops.ts
3313
+ init_database();
3314
+ init_pricing();
3315
+ import { existsSync as existsSync8 } from "fs";
3316
+ import { homedir as homedir8 } from "os";
3317
+ import { join as join8 } from "path";
3318
+ import { Database as BunDatabase2 } from "bun:sqlite";
3319
+ var DEFAULT_LOOPS_DB_PATH = join8(homedir8(), ".hasna", "loops", "loops.db");
3320
+ var LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
3321
+ function loopsDbPath() {
3322
+ return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? DEFAULT_LOOPS_DB_PATH;
3323
+ }
3324
+ function loopsModel() {
3325
+ return process.env["HASNA_ECONOMY_LOOPS_MODEL"] ?? process.env["ECONOMY_LOOPS_MODEL"] ?? "gpt-5.3-codex";
3326
+ }
3327
+ function openLoopsDb(dbPath, verbose) {
3328
+ let lastError;
3329
+ for (const readonly of [true, false]) {
3330
+ let loopsDb = null;
3331
+ try {
3332
+ loopsDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
3333
+ loopsDb.prepare("PRAGMA schema_version").get();
3334
+ return loopsDb;
3335
+ } catch (error) {
3336
+ lastError = error;
3337
+ loopsDb?.close();
3338
+ }
3339
+ }
3340
+ if (verbose) {
3341
+ const message = lastError instanceof Error ? lastError.message : String(lastError);
3342
+ console.log("OpenLoops DB unreadable:", dbPath, message);
3343
+ }
3344
+ return null;
3345
+ }
3346
+ function slug(value) {
3347
+ return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
3348
+ }
3349
+ function parseTarget(raw) {
3350
+ if (!raw)
3351
+ return {};
3352
+ try {
3353
+ const parsed = JSON.parse(raw);
3354
+ return parsed && typeof parsed === "object" ? parsed : {};
3355
+ } catch {
3356
+ return {};
3357
+ }
3358
+ }
3359
+ function stringValue(value) {
3360
+ return typeof value === "string" ? value.trim() : "";
3361
+ }
3362
+ function accountFields(row) {
3363
+ const target = parseTarget(row.target_json);
3364
+ const tool = stringValue(row.account_tool) || stringValue(target["account_tool"]) || stringValue(target["tool"]) || stringValue(target["agent"]);
3365
+ const profile = stringValue(row.account_profile) || stringValue(target["account_profile"]) || stringValue(target["profile"]) || stringValue(target["account"]);
3366
+ if (!tool || !profile) {
3367
+ return { account_key: "", account_tool: tool, account_name: profile, account_source: "" };
3368
+ }
3369
+ return {
3370
+ account_key: `${tool}:${profile}`,
3371
+ account_tool: tool,
3372
+ account_name: profile,
3373
+ account_source: "loops"
3374
+ };
3375
+ }
3376
+ function readLoopGoalRuns(loopsDb) {
3377
+ return loopsDb.prepare(`
3378
+ SELECT
3379
+ gr.id as goal_run_id,
3380
+ gr.goal_id as goal_id,
3381
+ COALESCE(gr.loop_id, g.loop_id, lr.loop_id) as loop_id,
3382
+ COALESCE(gr.loop_run_id, g.loop_run_id, lr.id) as loop_run_id,
3383
+ COALESCE(l.name, lr.loop_name, 'unknown-loop') as loop_name,
3384
+ l.labels_json as labels_json,
3385
+ l.target_json as target_json,
3386
+ g.objective as objective,
3387
+ gr.phase as phase,
3388
+ gr.status as status,
3389
+ COALESCE(gr.tokens_used, 0) as tokens_used,
3390
+ gr.created_at as created_at,
3391
+ gr.updated_at as updated_at,
3392
+ lr.started_at as loop_run_started_at,
3393
+ wsr.account_profile as account_profile,
3394
+ wsr.account_tool as account_tool
3395
+ FROM goal_runs gr
3396
+ LEFT JOIN goals g ON g.id = gr.goal_id
3397
+ LEFT JOIN loop_runs lr ON lr.id = COALESCE(gr.loop_run_id, g.loop_run_id)
3398
+ LEFT JOIN loops l ON l.id = COALESCE(gr.loop_id, g.loop_id, lr.loop_id)
3399
+ LEFT JOIN (
3400
+ SELECT goal_run_id, MIN(account_profile) as account_profile, MIN(account_tool) as account_tool
3401
+ FROM workflow_step_runs
3402
+ WHERE goal_run_id IS NOT NULL
3403
+ GROUP BY goal_run_id
3404
+ ) wsr ON wsr.goal_run_id = gr.id
3405
+ WHERE COALESCE(gr.tokens_used, 0) > 0
3406
+ ORDER BY gr.created_at ASC, gr.id ASC
3407
+ `).all();
3408
+ }
3409
+ async function ingestLoops(db, verbose = false) {
3410
+ const dbPath = loopsDbPath();
3411
+ if (!existsSync8(dbPath)) {
3412
+ if (verbose)
3413
+ console.log("OpenLoops DB not found:", dbPath);
3414
+ return { sessions: 0, requests: 0 };
3415
+ }
3416
+ const model = loopsModel();
3417
+ const machineId = getMachineId();
3418
+ let loopsDb = null;
3419
+ let requests = 0;
3420
+ const sessions = new Set;
3421
+ try {
3422
+ loopsDb = openLoopsDb(dbPath, verbose);
3423
+ if (!loopsDb)
3424
+ return { sessions: 0, requests: 0 };
3425
+ for (const row of readLoopGoalRuns(loopsDb)) {
3426
+ const loopName = row.loop_name || "unknown-loop";
3427
+ const costCenterId = `loop:${slug(loopName)}`;
3428
+ const sessionId = `loops-${row.loop_run_id ?? row.loop_id ?? row.goal_id}`;
3429
+ const requestId = `loops-${row.goal_run_id}`;
3430
+ const timestamp = row.updated_at ?? row.created_at ?? row.loop_run_started_at ?? new Date().toISOString();
3431
+ const stateValue = `${LOOPS_INGEST_VERSION}:${row.updated_at ?? ""}:${row.tokens_used}:${model}:${costCenterId}`;
3432
+ if (getIngestState(db, "loops", row.goal_run_id) === stateValue)
3433
+ continue;
3434
+ upsertCostCenter(db, {
3435
+ id: costCenterId,
3436
+ kind: "loop",
3437
+ name: loopName,
3438
+ repo_path: null,
3439
+ labels_json: row.labels_json ?? "{}",
3440
+ created_at: row.created_at ?? timestamp
3441
+ });
3442
+ const account = accountFields(row);
3443
+ upsertSession(db, {
3444
+ id: sessionId,
3445
+ agent: "loop",
3446
+ project_path: "",
3447
+ project_name: loopName,
3448
+ started_at: row.loop_run_started_at ?? row.created_at ?? timestamp,
3449
+ ended_at: row.updated_at ?? null,
3450
+ total_cost_usd: 0,
3451
+ total_tokens: 0,
3452
+ request_count: 0,
3453
+ machine_id: machineId,
3454
+ cost_center_id: costCenterId,
3455
+ attribution_tag: "loops-orchestration",
3456
+ ...account
3457
+ });
3458
+ const inputTokens = row.tokens_used;
3459
+ const outputTokens = 0;
3460
+ upsertRequest(db, {
3461
+ id: requestId,
3462
+ agent: "loop",
3463
+ session_id: sessionId,
3464
+ model,
3465
+ input_tokens: inputTokens,
3466
+ output_tokens: outputTokens,
3467
+ cache_read_tokens: 0,
3468
+ cache_create_tokens: 0,
3469
+ cost_usd: computeCostFromDb(db, model, inputTokens, outputTokens),
3470
+ cost_basis: "estimated",
3471
+ duration_ms: 0,
3472
+ timestamp,
3473
+ source_request_id: requestId,
3474
+ machine_id: machineId,
3475
+ cost_center_id: costCenterId,
3476
+ attribution_tag: "loops-orchestration",
3477
+ ...account
3478
+ });
3479
+ sessions.add(sessionId);
3480
+ requests++;
3481
+ setIngestState(db, "loops", row.goal_run_id, stateValue);
3482
+ if (verbose)
3483
+ console.log(`OpenLoops ${loopName}: ${row.tokens_used} orchestration tokens`);
3484
+ }
3485
+ } catch (error) {
3486
+ if (verbose) {
3487
+ const message = error instanceof Error ? error.message : String(error);
3488
+ console.log("OpenLoops ingest skipped:", message);
3489
+ }
3490
+ return { sessions: 0, requests: 0 };
3491
+ } finally {
3492
+ loopsDb?.close();
3493
+ }
3494
+ for (const sessionId of sessions)
3495
+ rollupSession(db, sessionId);
3496
+ return { sessions: sessions.size, requests };
3497
+ }
3498
+
3140
3499
  // src/ingest/claude-quota.ts
3141
3500
  init_database();
3142
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
3501
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
3143
3502
 
3144
3503
  // src/lib/paths.ts
3145
- import { homedir as homedir8 } from "os";
3146
- import { join as join8 } from "path";
3504
+ import { homedir as homedir9 } from "os";
3505
+ import { join as join9 } from "path";
3147
3506
  function getHomeDir() {
3148
- return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir8();
3507
+ return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir9();
3149
3508
  }
3150
3509
  function agentPaths() {
3151
3510
  const home = getHomeDir();
3152
3511
  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")
3512
+ claudeProjects: join9(home, ".claude", "projects"),
3513
+ claudeCredentials: join9(home, ".claude", ".credentials.json"),
3514
+ takumiProjects: join9(home, ".takumi", "projects"),
3515
+ codexDir: join9(home, ".codex"),
3516
+ codexDb: join9(home, ".codex", "state_5.sqlite"),
3517
+ codexAuth: join9(home, ".codex", "auth.json"),
3518
+ codexConfig: join9(home, ".codex", "config.toml"),
3519
+ geminiTmp: join9(home, ".gemini", "tmp"),
3520
+ geminiHistory: join9(home, ".gemini", "history"),
3521
+ opencodeMessages: join9(home, ".local", "share", "opencode", "storage", "message"),
3522
+ piSessions: join9(home, ".pi", "agent", "sessions"),
3523
+ hermesDir: join9(home, ".hermes"),
3524
+ hermesDb: join9(home, ".hermes", "state.db")
3166
3525
  };
3167
3526
  }
3168
3527
 
@@ -3174,7 +3533,7 @@ function readClaudeToken() {
3174
3533
  const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
3175
3534
  if (fromEnv)
3176
3535
  return { token: fromEnv };
3177
- if (!existsSync8(CREDENTIALS_PATH))
3536
+ if (!existsSync9(CREDENTIALS_PATH))
3178
3537
  return null;
3179
3538
  try {
3180
3539
  const creds = JSON.parse(readFileSync7(CREDENTIALS_PATH, "utf-8"));
@@ -3313,14 +3672,14 @@ async function ingestClaudeQuota(db, verbose = false) {
3313
3672
 
3314
3673
  // src/ingest/codex-quota.ts
3315
3674
  init_database();
3316
- import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
3675
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
3317
3676
  var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
3318
3677
  function readCodexAuth() {
3319
3678
  const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
3320
3679
  if (fromEnv)
3321
3680
  return { token: fromEnv, authMode: "chatgpt" };
3322
3681
  const authPath = agentPaths().codexAuth;
3323
- if (!existsSync9(authPath))
3682
+ if (!existsSync10(authPath))
3324
3683
  return null;
3325
3684
  try {
3326
3685
  const auth = JSON.parse(readFileSync8(authPath, "utf-8"));
@@ -3453,7 +3812,7 @@ async function ingestCodexQuota(db, verbose = false) {
3453
3812
  // src/lib/sync-all.ts
3454
3813
  init_database();
3455
3814
  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);
3815
+ const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes || opts.loops);
3457
3816
  const all = !anySpecific;
3458
3817
  const result = { deduped: 0 };
3459
3818
  if (all || opts.claude) {
@@ -3476,6 +3835,8 @@ async function syncAll(db, opts = {}) {
3476
3835
  result.pi = await ingestPi(db, opts.verbose);
3477
3836
  if (all || opts.hermes)
3478
3837
  result.hermes = await ingestHermes(db, opts.verbose);
3838
+ if (all || opts.loops)
3839
+ result.loops = await ingestLoops(db, opts.verbose);
3479
3840
  if (opts.dedupe !== false)
3480
3841
  result.deduped = dedupeRequests(db);
3481
3842
  return result;
@@ -4794,6 +5155,13 @@ class LocalStore {
4794
5155
  async accountBreakdown(query = {}) {
4795
5156
  return query.since ? queryAccountBreakdownSince(this.db(), query.since) : queryAccountBreakdown(this.db(), query.period ?? "all", query.machine);
4796
5157
  }
5158
+ async costCenterBreakdown(query = {}) {
5159
+ return queryCostCenterBreakdown(this.db(), query.period ?? "all", {
5160
+ kind: query.kind,
5161
+ machine: query.machine,
5162
+ since: query.since
5163
+ });
5164
+ }
4797
5165
  async accounts(period) {
4798
5166
  return queryAccountBreakdown(this.db(), period);
4799
5167
  }
@@ -4983,6 +5351,14 @@ class ApiStore {
4983
5351
  async accountBreakdown(query = {}) {
4984
5352
  return cloudListItems(this.cloud, "breakdown", q({ by: "account", period: query.period, since: query.since }));
4985
5353
  }
5354
+ async costCenterBreakdown(query = {}) {
5355
+ const rows = await cloudListItems(this.cloud, "breakdown", q({
5356
+ by: query.kind ?? "cost-center",
5357
+ period: query.period,
5358
+ since: query.since
5359
+ }));
5360
+ return rows.filter((row) => Boolean(row?.cost_center_id));
5361
+ }
4986
5362
  async accounts(period) {
4987
5363
  return cloudListItems(this.cloud, "accounts", q({ period }));
4988
5364
  }
@@ -5056,6 +5432,7 @@ class ApiStore {
5056
5432
  const created = await this.cloud.client.create("budgets", {
5057
5433
  project_path: input.project_path,
5058
5434
  ...input.agent ? { agent: input.agent } : {},
5435
+ ...input.cost_center_id ? { cost_center_id: input.cost_center_id } : {},
5059
5436
  period: input.period,
5060
5437
  limit_usd: input.limit_usd,
5061
5438
  alert_at_percent: input.alert_at_percent
@@ -5146,6 +5523,7 @@ function buildServer() {
5146
5523
  name: MCP_NAME,
5147
5524
  version: packageMetadata.version
5148
5525
  });
5526
+ const SYNC_SOURCES = ["all", ...AGENTS, "loops"];
5149
5527
  const TOOL_NAMES = [
5150
5528
  "get_cost_summary",
5151
5529
  "get_sessions",
@@ -5154,6 +5532,7 @@ function buildServer() {
5154
5532
  "get_project_breakdown",
5155
5533
  "get_agent_breakdown",
5156
5534
  "get_account_breakdown",
5535
+ "get_cost_center_breakdown",
5157
5536
  "get_budget_status",
5158
5537
  "set_budget",
5159
5538
  "remove_budget",
@@ -5186,30 +5565,31 @@ function buildServer() {
5186
5565
  get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
5187
5566
  get_sessions: `agent(${AGENTS.join("|")}), project(partial), account?(key/name/email), machine?(hostname), limit(20) -> compact session table`,
5188
5567
  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",
5568
+ list_machines: "limit(20), verbose?, json? -> machine_id, sessions, requests, cost, last_active",
5569
+ get_model_breakdown: "limit(20), verbose?, json? -> model, requests, tokens, cost",
5570
+ get_project_breakdown: "period?(today|week|month|year|all), limit(20), verbose?, json? -> project_name, sessions, tokens, cost",
5192
5571
  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",
5572
+ get_account_breakdown: "period?(today|week|month|year|all), limit(20), verbose?, json? -> account profile, sessions, requests, tokens, api-equivalent, billable, included",
5573
+ get_cost_center_breakdown: "period?(today|week|month|year|all), kind?(loop|app|repo|service|team) -> cost center, sessions, requests, tokens, cost",
5574
+ get_budget_status: "limit(20), verbose?, json? -> budget limits, current spend, percent_used, is_over_alert",
5575
+ set_budget: "period(daily|weekly|monthly), limit_usd, project_path?, agent?, cost_center_id?, alert_at_percent? -> create budget",
5196
5576
  remove_budget: "id -> delete budget",
5197
- get_pricing: "no params -> model pricing rows with input, output, cache read/write, and cache storage rates",
5577
+ get_pricing: "limit(20), verbose?, json? -> model pricing rows with input, output, cache read/write, and cache storage rates",
5198
5578
  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
5579
  remove_pricing: "model -> delete pricing row",
5200
5580
  get_daily: "days(30) -> daily cost table grouped by date and agent",
5201
5581
  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`,
5582
+ get_session_detail: "session_id(prefix ok), limit(20), verbose? -> per-request breakdown with model, tokens, cost",
5583
+ get_usage: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}), limit(20), json? -> usage snapshots and all-machine summary`,
5584
+ get_savings: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}), limit(20), json? -> subscription/API-equivalent savings`,
5205
5585
  estimate_cost: "model, input_tokens?, output_tokens? -> pre-flight cost estimate priced from the store pricing table",
5206
5586
  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`,
5587
+ set_subscription: `provider, plan, monthly_fee_usd?, included_usage_usd?, agent?(${AGENTS.join("|")}), json? -> create/update subscription plan`,
5208
5588
  remove_subscription: "id -> delete subscription plan",
5209
- sync: `sources(all|${AGENTS.join("|")}) -> ingest latest cost data`,
5589
+ sync: `sources(${SYNC_SOURCES.join("|")}), json? -> ingest latest cost data`,
5210
5590
  search_tools: "query substring -> tool name list",
5211
5591
  describe_tools: "names[] -> one-line parameter hints",
5212
- get_goals: "no params -> goal progress summary",
5592
+ get_goals: "limit(20), verbose?, json? -> goal progress summary",
5213
5593
  set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
5214
5594
  remove_goal: "id -> delete goal",
5215
5595
  register_agent: "name, session_id? -> register agent session",
@@ -5220,6 +5600,20 @@ function buildServer() {
5220
5600
  };
5221
5601
  const fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
5222
5602
  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);
5603
+ const DEFAULT_MCP_ROW_LIMIT = 20;
5604
+ function clampLimit(limit, fallback = DEFAULT_MCP_ROW_LIMIT, max = 100) {
5605
+ if (limit == null)
5606
+ return fallback;
5607
+ if (!Number.isFinite(limit))
5608
+ return fallback;
5609
+ return Math.max(1, Math.min(max, Math.floor(limit)));
5610
+ }
5611
+ function rowLimit(limit, verbose) {
5612
+ return verbose ? Number.POSITIVE_INFINITY : clampLimit(limit);
5613
+ }
5614
+ function hiddenRowsHint(total, shown, noun) {
5615
+ return total > shown ? `... ${total - shown} more ${noun} hidden; call with limit, verbose=true, or json=true.` : undefined;
5616
+ }
5223
5617
  function fmtSession(s) {
5224
5618
  const id = String(s["id"] ?? "").slice(0, 8);
5225
5619
  const agent = String(s["agent"] ?? "");
@@ -5228,6 +5622,26 @@ function buildServer() {
5228
5622
  const tok = fmtTok(Number(s["total_tokens"] ?? 0));
5229
5623
  return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
5230
5624
  }
5625
+ function compactPrimitive(value) {
5626
+ if (value == null)
5627
+ return "none";
5628
+ if (typeof value === "number")
5629
+ return Number.isFinite(value) ? value.toLocaleString("en-US") : String(value);
5630
+ if (typeof value === "boolean")
5631
+ return value ? "yes" : "no";
5632
+ return String(value);
5633
+ }
5634
+ function compactObject(value) {
5635
+ if (value == null)
5636
+ return "none";
5637
+ if (Array.isArray(value))
5638
+ return `${value.length} rows`;
5639
+ if (typeof value !== "object")
5640
+ return compactPrimitive(value);
5641
+ const entries = Object.entries(value);
5642
+ const primitives = entries.filter(([, entry]) => entry == null || ["string", "number", "boolean"].includes(typeof entry)).slice(0, 6).map(([key, entry]) => `${key}=${compactPrimitive(entry)}`);
5643
+ return primitives.length ? primitives.join(", ") : `${entries.length} fields`;
5644
+ }
5231
5645
  function text(text2) {
5232
5646
  return { content: [{ type: "text", text: text2 }] };
5233
5647
  }
@@ -5282,22 +5696,34 @@ function buildServer() {
5282
5696
  return text(lines.join(`
5283
5697
  `));
5284
5698
  });
5285
- server.tool("get_model_breakdown", "Cost per model. No params.", {}, async () => {
5699
+ 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
5700
  const rows = await store.modelBreakdown();
5701
+ if (json)
5702
+ return text(JSON.stringify(rows, null, 2));
5703
+ const visibleRows = rows.slice(0, rowLimit(limit, verbose));
5287
5704
  const lines = ["model agent reqs tokens cost"];
5288
- for (const row of rows) {
5705
+ for (const row of visibleRows) {
5289
5706
  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
5707
  }
5708
+ const hint = hiddenRowsHint(rows.length, visibleRows.length, "models");
5709
+ if (hint)
5710
+ lines.push(hint);
5291
5711
  return text(lines.join(`
5292
5712
  `));
5293
5713
  });
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 }) => {
5714
+ 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
5715
  const rows = await store.projectBreakdown({ period: period ?? "all" });
5716
+ if (json)
5717
+ return text(JSON.stringify(rows, null, 2));
5718
+ const visibleRows = rows.slice(0, rowLimit(limit, verbose));
5296
5719
  const lines = ["project sessions tokens cost"];
5297
- for (const row of rows) {
5720
+ for (const row of visibleRows) {
5298
5721
  const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
5299
5722
  lines.push(`${name.padEnd(21)}${String(row["sessions"]).padEnd(9)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
5300
5723
  }
5724
+ const hint = hiddenRowsHint(rows.length, visibleRows.length, "projects");
5725
+ if (hint)
5726
+ lines.push(hint);
5301
5727
  return text(lines.join(`
5302
5728
  `));
5303
5729
  });
@@ -5312,29 +5738,55 @@ function buildServer() {
5312
5738
  return text(lines.join(`
5313
5739
  `));
5314
5740
  });
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 }) => {
5741
+ 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
5742
  const rows = await store.accountBreakdown({ period: period ?? "all" });
5743
+ if (json)
5744
+ return text(JSON.stringify(rows, null, 2));
5317
5745
  if (rows.length === 0)
5318
5746
  return text("No account-attributed sessions yet.");
5747
+ const visibleRows = rows.slice(0, rowLimit(limit, verbose));
5319
5748
  const lines = ["account agent sessions requests tokens api_eq billable included"];
5320
- for (const row of rows) {
5749
+ for (const row of visibleRows) {
5321
5750
  const label = String(row["account_email"] || row["account_name"] || row["account_key"] || "\u2014").slice(0, 20);
5322
5751
  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
5752
  }
5753
+ const hint = hiddenRowsHint(rows.length, visibleRows.length, "accounts");
5754
+ if (hint)
5755
+ lines.push(hint);
5324
5756
  return text(lines.join(`
5325
5757
  `));
5326
5758
  });
5327
- server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. No params.", {}, async () => {
5759
+ server.tool("get_cost_center_breakdown", "Cost per cost center. Params: period(today|week|month|year|all), kind(loop|app|repo|service|team).", {
5760
+ period: z.enum(["today", "week", "month", "year", "all"]).optional(),
5761
+ kind: z.enum(["loop", "app", "repo", "service", "team"]).optional()
5762
+ }, async ({ period, kind }) => {
5763
+ const rows = await store.costCenterBreakdown({ period: period ?? "all", kind });
5764
+ if (rows.length === 0)
5765
+ return text("No cost-center usage yet.");
5766
+ const lines = ["kind cost_center sessions requests tokens cost"];
5767
+ for (const row of rows) {
5768
+ 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"]))}`);
5769
+ }
5770
+ return text(lines.join(`
5771
+ `));
5772
+ });
5773
+ 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
5774
  const budgets = await store.listBudgets();
5775
+ if (json)
5776
+ return text(JSON.stringify(budgets, null, 2));
5329
5777
  if (budgets.length === 0)
5330
5778
  return text("No budgets set.");
5779
+ const visibleBudgets = budgets.slice(0, rowLimit(limit, verbose));
5331
5780
  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);
5781
+ for (const budget of visibleBudgets) {
5782
+ const scope = String(budget["cost_center_id"] ?? budget["project_path"] ?? "global").slice(0, 20);
5334
5783
  const pct = Number(budget["percent_used"]).toFixed(1);
5335
5784
  const status = budget["is_over_limit"] ? "OVER" : budget["is_over_alert"] ? "ALERT" : "OK";
5336
5785
  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
5786
  }
5787
+ const hint = hiddenRowsHint(budgets.length, visibleBudgets.length, "budgets");
5788
+ if (hint)
5789
+ lines.push(hint);
5338
5790
  return text(lines.join(`
5339
5791
  `));
5340
5792
  });
@@ -5343,11 +5795,13 @@ function buildServer() {
5343
5795
  limit_usd: z.number().positive(),
5344
5796
  project_path: z.string().optional(),
5345
5797
  agent: z.enum(AGENTS).optional(),
5798
+ cost_center_id: z.string().optional(),
5346
5799
  alert_at_percent: z.number().positive().max(100).optional()
5347
- }, async ({ period, limit_usd, project_path, agent, alert_at_percent }) => {
5800
+ }, async ({ period, limit_usd, project_path, agent, cost_center_id, alert_at_percent }) => {
5348
5801
  const id = await store.setBudget({
5349
5802
  project_path: project_path ?? null,
5350
5803
  agent: agent ?? null,
5804
+ cost_center_id: cost_center_id ?? null,
5351
5805
  period,
5352
5806
  limit_usd,
5353
5807
  alert_at_percent: alert_at_percent ?? 80
@@ -5358,12 +5812,18 @@ function buildServer() {
5358
5812
  await store.removeBudget(id);
5359
5813
  return text("Budget removed.");
5360
5814
  });
5361
- server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage.", {}, async () => {
5815
+ 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
5816
  const rows = await store.listPricing();
5817
+ if (json)
5818
+ return text(JSON.stringify(rows, null, 2));
5819
+ const visibleRows = rows.slice(0, rowLimit(limit, verbose));
5363
5820
  const lines = ["model input output cache-r cache-w cache-1h storage-h"];
5364
- for (const row of rows) {
5821
+ for (const row of visibleRows) {
5365
5822
  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
5823
  }
5824
+ const hint = hiddenRowsHint(rows.length, visibleRows.length, "pricing rows");
5825
+ if (hint)
5826
+ lines.push(hint);
5367
5827
  return text(lines.join(`
5368
5828
  `));
5369
5829
  });
@@ -5428,11 +5888,14 @@ function buildServer() {
5428
5888
  return text(lines.join(`
5429
5889
  `));
5430
5890
  });
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 }) => {
5891
+ 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
5892
  const detail = await store.sessionDetail(session_id);
5433
5893
  if (!detail)
5434
5894
  return textError(`Session not found: ${session_id}`);
5435
- const { session, requests } = detail;
5895
+ const { session, requests: allRequests } = detail;
5896
+ const requestLimit = clampLimit(limit, verbose ? 50 : DEFAULT_MCP_ROW_LIMIT);
5897
+ const requestCount = { count: allRequests.length };
5898
+ const requests = allRequests.slice(0, requestLimit);
5436
5899
  const lines = [
5437
5900
  `session: ${String(session["id"]).slice(0, 16)}`,
5438
5901
  `agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
@@ -5440,27 +5903,85 @@ function buildServer() {
5440
5903
  "",
5441
5904
  "time model input output cache-r cache-5m cache-1h cost"
5442
5905
  ];
5443
- for (const request of requests.slice(0, 50)) {
5906
+ for (const request of requests) {
5444
5907
  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
5908
  }
5909
+ if (requestCount.count > requests.length) {
5910
+ lines.push(`... ${requestCount.count - requests.length} more requests hidden; call with limit or verbose=true for more.`);
5911
+ }
5446
5912
  return text(lines.join(`
5447
5913
  `));
5448
5914
  });
5449
- server.tool("sync", `Ingest new cost data. sources: all|${AGENTS.join("|")}`, { sources: z.enum(["all", ...AGENTS]).optional() }, async ({ sources }) => {
5915
+ 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
5916
  if (isCloudStore()) {
5451
5917
  return text("cloud mode: ingest is a local-only operation; nothing to sync (reads/writes route to the cloud API)");
5452
5918
  }
5453
5919
  const selected = sources ?? "all";
5454
5920
  const opts = selected === "all" ? {} : { [selected]: true };
5455
5921
  const result = await syncAll(localDb(), opts);
5456
- return text(JSON.stringify(result, null, 2));
5922
+ if (json)
5923
+ return text(JSON.stringify(result, null, 2));
5924
+ const lines = [
5925
+ `sync: ${selected}`,
5926
+ `deduped: ${result.deduped}`
5927
+ ];
5928
+ for (const [source, value] of Object.entries(result)) {
5929
+ if (source === "deduped")
5930
+ continue;
5931
+ if (value == null)
5932
+ continue;
5933
+ lines.push(`${source}: ${compactObject(value)}`);
5934
+ }
5935
+ lines.push("Use json=true for the full sync result.");
5936
+ return text(lines.join(`
5937
+ `));
5457
5938
  });
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 }) => {
5939
+ 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
5940
  const p = period ?? "month";
5460
- return text(JSON.stringify(await store.usage(p, agent), null, 2));
5941
+ const { snapshots: snaps, summary } = await store.usage(p, agent);
5942
+ if (json)
5943
+ return text(JSON.stringify({ snapshots: snaps, summary }, null, 2));
5944
+ const snapshotLimit = clampLimit(limit);
5945
+ const lines = [
5946
+ `period: ${p}${agent ? ` agent: ${agent}` : ""}`,
5947
+ `fleet: ${fmtUsd(summary.total_usd)} sessions: ${summary.sessions} requests: ${summary.requests.toLocaleString()} tokens: ${fmtTok(summary.tokens)}`,
5948
+ `snapshots: ${snaps.length}${snaps.length > snapshotLimit ? ` (showing ${snapshotLimit})` : ""}`
5949
+ ];
5950
+ if (snaps.length > 0) {
5951
+ lines.push("", "date agent metric value unit machine");
5952
+ for (const snapshot of snaps.slice(0, snapshotLimit)) {
5953
+ 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"}`);
5954
+ }
5955
+ if (snaps.length > snapshotLimit)
5956
+ lines.push(`... ${snaps.length - snapshotLimit} more snapshots hidden; call with limit or json=true.`);
5957
+ }
5958
+ return text(lines.join(`
5959
+ `));
5461
5960
  });
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));
5961
+ 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 }) => {
5962
+ const savings = await store.savings(period ?? "month", agent);
5963
+ if (json)
5964
+ return text(JSON.stringify(savings, null, 2));
5965
+ const lines = [
5966
+ `period: ${savings.period}${agent ? ` agent: ${agent}` : ""}`,
5967
+ `api_equivalent: ${fmtUsd(savings.api_equivalent_usd)}`,
5968
+ `subscription_fee: ${fmtUsd(savings.subscription_fee_usd)}`,
5969
+ `included_consumed: ${fmtUsd(savings.included_consumed_usd)}`,
5970
+ `on_demand: ${fmtUsd(savings.on_demand_usd)}`,
5971
+ `saved: ${fmtUsd(savings.saved_usd)}`
5972
+ ];
5973
+ const rows = Object.entries(savings.by_agent ?? {});
5974
+ if (rows.length > 0) {
5975
+ const agentLimit = clampLimit(limit);
5976
+ lines.push("", `by_agent: ${rows.length}${rows.length > agentLimit ? ` (showing ${agentLimit})` : ""}`, "agent api_eq sub_fee included on_demand saved");
5977
+ for (const [agentName, row] of rows.slice(0, agentLimit)) {
5978
+ 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))}`);
5979
+ }
5980
+ if (rows.length > agentLimit)
5981
+ lines.push(`... ${rows.length - agentLimit} more agents hidden; call with limit or json=true.`);
5982
+ }
5983
+ return text(lines.join(`
5984
+ `));
5464
5985
  });
5465
5986
  server.tool("list_subscriptions", "List configured subscription plans used by savings calculations.", {}, async () => {
5466
5987
  const rows = await store.listSubscriptions();
@@ -5482,7 +6003,8 @@ function buildServer() {
5482
6003
  included_usage_usd: z.number().nonnegative().optional(),
5483
6004
  billing_cycle_start: z.string().optional(),
5484
6005
  reset_policy: z.string().optional(),
5485
- active: z.boolean().optional()
6006
+ active: z.boolean().optional(),
6007
+ json: z.boolean().optional()
5486
6008
  }, async (input) => {
5487
6009
  const row = await store.setSubscription({
5488
6010
  id: input.id,
@@ -5495,7 +6017,14 @@ function buildServer() {
5495
6017
  reset_policy: input.reset_policy ?? "monthly",
5496
6018
  active: input.active === false ? 0 : 1
5497
6019
  });
5498
- return text(JSON.stringify(row, null, 2));
6020
+ if (input.json)
6021
+ return text(JSON.stringify(row, null, 2));
6022
+ return text(`Subscription set: ${row.id}
6023
+ provider: ${row.provider}
6024
+ plan: ${row.plan}
6025
+ agent: ${row.agent ?? "all"}
6026
+ fee: ${fmtUsd(row.monthly_fee_usd)}
6027
+ included: ${fmtUsd(row.included_usage_usd)}`);
5499
6028
  });
5500
6029
  server.tool("remove_subscription", "Delete a subscription plan by id.", { id: z.string() }, async ({ id }) => {
5501
6030
  await store.removeSubscription(id);
@@ -5505,17 +6034,23 @@ function buildServer() {
5505
6034
  const cost = await store.estimate({ model, inputTokens: input_tokens ?? 0, outputTokens: output_tokens ?? 0 });
5506
6035
  return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
5507
6036
  });
5508
- server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
6037
+ 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
6038
  const goals = await store.listGoals();
6039
+ if (json)
6040
+ return text(JSON.stringify(goals, null, 2));
5510
6041
  if (goals.length === 0)
5511
6042
  return text("No goals set.");
6043
+ const visibleGoals = goals.slice(0, rowLimit(limit, verbose));
5512
6044
  const lines = ["period scope limit spent used% status"];
5513
- for (const goal of goals) {
6045
+ for (const goal of visibleGoals) {
5514
6046
  const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
5515
6047
  const pct = Number(goal["percent_used"]).toFixed(1);
5516
6048
  const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
5517
6049
  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
6050
  }
6051
+ const hint = hiddenRowsHint(goals.length, visibleGoals.length, "goals");
6052
+ if (hint)
6053
+ lines.push(hint);
5519
6054
  return text(lines.join(`
5520
6055
  `));
5521
6056
  });
@@ -5537,14 +6072,20 @@ function buildServer() {
5537
6072
  await store.removeGoal(id);
5538
6073
  return text("Goal removed.");
5539
6074
  });
5540
- server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
6075
+ 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
6076
  const machines = await store.machines();
6077
+ if (json)
6078
+ return text(JSON.stringify(machines, null, 2));
5542
6079
  if (machines.length === 0)
5543
6080
  return text(`No machine data yet. Current machine: ${getMachineId()}`);
6081
+ const visibleMachines = machines.slice(0, rowLimit(limit, verbose));
5544
6082
  const lines = ["machine sessions requests cost last_active"];
5545
- for (const m of machines) {
6083
+ for (const m of visibleMachines) {
5546
6084
  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
6085
  }
6086
+ const hint = hiddenRowsHint(machines.length, visibleMachines.length, "machines");
6087
+ if (hint)
6088
+ lines.push(hint);
5548
6089
  lines.push(`
5549
6090
  current machine: ${getMachineId()}`);
5550
6091
  return text(lines.join(`