@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/cli/index.js CHANGED
@@ -544,6 +544,7 @@ __export(exports_database, {
544
544
  upsertProject: () => upsertProject,
545
545
  upsertModelPricing: () => upsertModelPricing,
546
546
  upsertGoal: () => upsertGoal,
547
+ upsertCostCenter: () => upsertCostCenter,
547
548
  upsertBudget: () => upsertBudget,
548
549
  upsertBillingDaily: () => upsertBillingDaily,
549
550
  setIngestState: () => setIngestState,
@@ -561,6 +562,7 @@ __export(exports_database, {
561
562
  queryModelBreakdown: () => queryModelBreakdown,
562
563
  queryHourlyBreakdown: () => queryHourlyBreakdown,
563
564
  queryDailyBreakdown: () => queryDailyBreakdown,
565
+ queryCostCenterBreakdown: () => queryCostCenterBreakdown,
564
566
  queryBillingSummary: () => queryBillingSummary,
565
567
  queryAgentBreakdownSince: () => queryAgentBreakdownSince,
566
568
  queryAgentBreakdown: () => queryAgentBreakdown,
@@ -583,6 +585,7 @@ __export(exports_database, {
583
585
  getGoalStatuses: () => getGoalStatuses,
584
586
  getDbPath: () => getDbPath,
585
587
  getDataDir: () => getDataDir,
588
+ getCostCenter: () => getCostCenter,
586
589
  getBudgetStatuses: () => getBudgetStatuses,
587
590
  deleteSubscription: () => deleteSubscription,
588
591
  deleteProject: () => deleteProject,
@@ -727,6 +730,16 @@ function addColumnIfMissing(db, table, column, definition) {
727
730
  }
728
731
  function initSchema(db) {
729
732
  db.exec(`
733
+ CREATE TABLE IF NOT EXISTS cost_centers (
734
+ id TEXT PRIMARY KEY,
735
+ kind TEXT NOT NULL,
736
+ name TEXT NOT NULL,
737
+ repo_path TEXT,
738
+ labels_json TEXT DEFAULT '{}',
739
+ created_at TEXT NOT NULL,
740
+ updated_at TEXT NOT NULL
741
+ );
742
+
730
743
  CREATE TABLE IF NOT EXISTS requests (
731
744
  id TEXT PRIMARY KEY,
732
745
  agent TEXT NOT NULL,
@@ -743,6 +756,7 @@ function initSchema(db) {
743
756
  timestamp TEXT NOT NULL,
744
757
  source_request_id TEXT,
745
758
  machine_id TEXT DEFAULT '',
759
+ cost_center_id TEXT,
746
760
  account_key TEXT DEFAULT '',
747
761
  account_tool TEXT DEFAULT '',
748
762
  account_name TEXT DEFAULT '',
@@ -761,6 +775,7 @@ function initSchema(db) {
761
775
  total_tokens INTEGER DEFAULT 0,
762
776
  request_count INTEGER DEFAULT 0,
763
777
  machine_id TEXT DEFAULT '',
778
+ cost_center_id TEXT,
764
779
  account_key TEXT DEFAULT '',
765
780
  account_tool TEXT DEFAULT '',
766
781
  account_name TEXT DEFAULT '',
@@ -781,6 +796,7 @@ function initSchema(db) {
781
796
  id TEXT PRIMARY KEY,
782
797
  project_path TEXT,
783
798
  agent TEXT,
799
+ cost_center_id TEXT,
784
800
  period TEXT NOT NULL,
785
801
  limit_usd REAL NOT NULL,
786
802
  alert_at_percent INTEGER DEFAULT 80,
@@ -811,6 +827,7 @@ function initSchema(db) {
811
827
  CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
812
828
  CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
813
829
  CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
830
+ CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
814
831
 
815
832
  CREATE TABLE IF NOT EXISTS model_pricing (
816
833
  model TEXT PRIMARY KEY,
@@ -897,6 +914,12 @@ function initSchema(db) {
897
914
  `);
898
915
  addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
899
916
  addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
917
+ if (addColumnIfMissing(db, "cost_centers", "updated_at", `TEXT DEFAULT ''`)) {
918
+ db.exec(`UPDATE cost_centers SET updated_at = created_at WHERE updated_at = '' OR updated_at IS NULL`);
919
+ }
920
+ addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
921
+ addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
922
+ addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
900
923
  if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
901
924
  db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
902
925
  }
@@ -925,6 +948,10 @@ function initSchema(db) {
925
948
  CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
926
949
  CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
927
950
  CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
951
+ CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id);
952
+ CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id);
953
+ CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
954
+ CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
928
955
  `);
929
956
  }
930
957
  function periodWhere(period) {
@@ -975,6 +1002,17 @@ function requestPeriodWhere(period) {
975
1002
  return "1=1";
976
1003
  }
977
1004
  }
1005
+ function upsertCostCenter(db, center) {
1006
+ const updatedAt = center.updated_at ?? center.created_at;
1007
+ db.prepare(`
1008
+ INSERT OR REPLACE INTO cost_centers
1009
+ (id, kind, name, repo_path, labels_json, created_at, updated_at)
1010
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1011
+ `).run(center.id, center.kind, center.name, center.repo_path ?? null, center.labels_json ?? "{}", center.created_at, updatedAt);
1012
+ }
1013
+ function getCostCenter(db, id) {
1014
+ return db.prepare(`SELECT * FROM cost_centers WHERE id = ?`).get(id);
1015
+ }
978
1016
  function upsertRequest(db, req) {
979
1017
  const now = req.updated_at ?? new Date().toISOString();
980
1018
  db.prepare(`
@@ -982,10 +1020,10 @@ function upsertRequest(db, req) {
982
1020
  (id, agent, session_id, model, input_tokens, output_tokens,
983
1021
  cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
984
1022
  cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
985
- source_request_id, machine_id, attribution_tag, account_key, account_tool,
1023
+ source_request_id, machine_id, cost_center_id, attribution_tag, account_key, account_tool,
986
1024
  account_name, account_email, account_source, updated_at, synced_at)
987
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
988
- `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
1025
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1026
+ `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.cost_center_id ?? null, req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
989
1027
  }
990
1028
  function upsertSession(db, session) {
991
1029
  const now = session.updated_at ?? new Date().toISOString();
@@ -993,9 +1031,9 @@ function upsertSession(db, session) {
993
1031
  INSERT OR REPLACE INTO sessions
994
1032
  (id, agent, project_path, project_name, started_at, ended_at,
995
1033
  total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
996
- account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
997
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
998
- `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
1034
+ cost_center_id, account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
1035
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1036
+ `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", session.cost_center_id ?? null, session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
999
1037
  }
1000
1038
  function rollupSession(db, sessionId) {
1001
1039
  db.prepare(`
@@ -1021,9 +1059,12 @@ function rollupSession(db, sessionId) {
1021
1059
  ELSE account_email END,
1022
1060
  account_source = CASE WHEN account_source = '' OR account_source IS NULL
1023
1061
  THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
1024
- ELSE account_source END
1062
+ ELSE account_source END,
1063
+ cost_center_id = CASE WHEN cost_center_id = '' OR cost_center_id IS NULL
1064
+ 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)
1065
+ ELSE cost_center_id END
1025
1066
  WHERE id = ?
1026
- `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
1067
+ `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
1027
1068
  }
1028
1069
  function querySessions(db, filter = {}) {
1029
1070
  const conditions = [];
@@ -1465,6 +1506,139 @@ function queryAccountBreakdown(db, period = "all", machine) {
1465
1506
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1466
1507
  return result;
1467
1508
  }
1509
+ function kindFromCostCenterId(id) {
1510
+ const prefix = id.split(":", 1)[0] ?? "";
1511
+ return ["loop", "app", "repo", "service", "team"].includes(prefix) ? prefix : "";
1512
+ }
1513
+ function addCostCenterBreakdownRow(groups, row, sessionOnly) {
1514
+ if (!row.cost_center_id)
1515
+ return;
1516
+ const derivedKind = row.kind || kindFromCostCenterId(row.cost_center_id) || "service";
1517
+ const group = groups.get(row.cost_center_id) ?? {
1518
+ cost_center_id: row.cost_center_id,
1519
+ kind: derivedKind,
1520
+ name: row.name || row.cost_center_id,
1521
+ repo_path: row.repo_path ?? null,
1522
+ labels_json: row.labels_json || "{}",
1523
+ sessionIds: new Set,
1524
+ requests: 0,
1525
+ total_tokens: 0,
1526
+ api_equivalent_usd: 0,
1527
+ metered_api_usd: 0,
1528
+ subscription_included_usd: 0,
1529
+ estimated_usd: 0,
1530
+ unknown_usd: 0,
1531
+ last_active: ""
1532
+ };
1533
+ group.kind = derivedKind;
1534
+ if (row.name)
1535
+ group.name = row.name;
1536
+ if (row.repo_path && !group.repo_path)
1537
+ group.repo_path = row.repo_path;
1538
+ if (row.labels_json && row.labels_json !== "{}")
1539
+ group.labels_json = row.labels_json;
1540
+ if (row.session_id)
1541
+ group.sessionIds.add(row.session_id);
1542
+ group.requests += row.requests;
1543
+ group.total_tokens += row.total_tokens;
1544
+ group.api_equivalent_usd += row.cost_usd;
1545
+ if (sessionOnly) {
1546
+ group.estimated_usd += row.cost_usd;
1547
+ } else if (row.cost_basis === "metered_api") {
1548
+ group.metered_api_usd += row.cost_usd;
1549
+ } else if (row.cost_basis === "subscription_included") {
1550
+ group.subscription_included_usd += row.cost_usd;
1551
+ } else if (row.cost_basis === "unknown") {
1552
+ group.unknown_usd += row.cost_usd;
1553
+ } else {
1554
+ group.estimated_usd += row.cost_usd;
1555
+ }
1556
+ if (!group.last_active || row.last_active > group.last_active)
1557
+ group.last_active = row.last_active;
1558
+ groups.set(row.cost_center_id, group);
1559
+ }
1560
+ function queryCostCenterBreakdown(db, period = "all", filter = {}) {
1561
+ const requestWhere = filter.since ? "r.timestamp >= ?" : requestPeriodWhere(period);
1562
+ const sessionWhere = filter.since ? "s.started_at >= ?" : sessionPeriodWhere(period);
1563
+ const requestMachineClause = filter.machine ? " AND r.machine_id = ?" : "";
1564
+ const sessionMachineClause = filter.machine ? " AND s.machine_id = ?" : "";
1565
+ const requestParams = [];
1566
+ const sessionParams = [];
1567
+ if (filter.since) {
1568
+ requestParams.push(filter.since);
1569
+ sessionParams.push(filter.since);
1570
+ }
1571
+ if (filter.machine) {
1572
+ requestParams.push(filter.machine);
1573
+ sessionParams.push(filter.machine);
1574
+ }
1575
+ const groups = new Map;
1576
+ const requestRows = db.prepare(`
1577
+ SELECT
1578
+ COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) as cost_center_id,
1579
+ COALESCE(cc.kind, '') as kind,
1580
+ COALESCE(cc.name, COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))) as name,
1581
+ cc.repo_path as repo_path,
1582
+ COALESCE(cc.labels_json, '{}') as labels_json,
1583
+ r.session_id as session_id,
1584
+ 1 as requests,
1585
+ COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
1586
+ COALESCE(r.cost_usd, 0) as cost_usd,
1587
+ COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
1588
+ r.timestamp as last_active
1589
+ FROM requests r
1590
+ LEFT JOIN sessions s ON s.id = r.session_id
1591
+ LEFT JOIN cost_centers cc ON cc.id = COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))
1592
+ WHERE ${requestWhere}${requestMachineClause}
1593
+ AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) IS NOT NULL
1594
+ AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) != ''
1595
+ `).all(...requestParams);
1596
+ for (const row of requestRows)
1597
+ addCostCenterBreakdownRow(groups, row, false);
1598
+ const sessionOnlyRows = db.prepare(`
1599
+ SELECT
1600
+ s.cost_center_id as cost_center_id,
1601
+ COALESCE(cc.kind, '') as kind,
1602
+ COALESCE(cc.name, s.cost_center_id) as name,
1603
+ cc.repo_path as repo_path,
1604
+ COALESCE(cc.labels_json, '{}') as labels_json,
1605
+ s.id as session_id,
1606
+ COALESCE(s.request_count, 0) as requests,
1607
+ COALESCE(s.total_tokens, 0) as total_tokens,
1608
+ COALESCE(s.total_cost_usd, 0) as cost_usd,
1609
+ 'estimated' as cost_basis,
1610
+ s.started_at as last_active
1611
+ FROM sessions s
1612
+ LEFT JOIN cost_centers cc ON cc.id = s.cost_center_id
1613
+ WHERE ${sessionWhere}${sessionMachineClause}
1614
+ AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
1615
+ AND s.cost_center_id IS NOT NULL
1616
+ AND s.cost_center_id != ''
1617
+ `).all(...sessionParams);
1618
+ for (const row of sessionOnlyRows)
1619
+ addCostCenterBreakdownRow(groups, row, true);
1620
+ const result = [...groups.values()].map((group) => ({
1621
+ cost_center_id: group.cost_center_id,
1622
+ kind: group.kind,
1623
+ name: group.name,
1624
+ repo_path: group.repo_path,
1625
+ labels_json: group.labels_json,
1626
+ sessions: group.sessionIds.size,
1627
+ requests: group.requests,
1628
+ total_tokens: group.total_tokens,
1629
+ api_equivalent_usd: group.api_equivalent_usd,
1630
+ billable_usd: group.metered_api_usd,
1631
+ metered_api_usd: group.metered_api_usd,
1632
+ subscription_included_usd: group.subscription_included_usd,
1633
+ estimated_usd: group.estimated_usd,
1634
+ unknown_usd: group.unknown_usd,
1635
+ cost_usd: group.api_equivalent_usd,
1636
+ last_active: group.last_active
1637
+ }));
1638
+ const filtered = filter.kind ? result.filter((row) => row.kind === filter.kind) : result;
1639
+ filtered.sort((a, b) => b.cost_usd - a.cost_usd);
1640
+ return filtered;
1641
+ }
1468
1642
  function queryProjectBreakdownSince(db, since) {
1469
1643
  return db.prepare(`
1470
1644
  SELECT project_path, project_name,
@@ -1654,9 +1828,9 @@ function deleteProject(db, path) {
1654
1828
  function upsertBudget(db, budget) {
1655
1829
  db.prepare(`
1656
1830
  INSERT OR REPLACE INTO budgets
1657
- (id, project_path, agent, period, limit_usd, alert_at_percent, created_at, updated_at)
1658
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1659
- `).run(budget.id, budget.project_path ?? null, budget.agent ?? null, budget.period, budget.limit_usd, budget.alert_at_percent, budget.created_at, budget.updated_at);
1831
+ (id, project_path, agent, cost_center_id, period, limit_usd, alert_at_percent, created_at, updated_at)
1832
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1833
+ `).run(budget.id, budget.project_path ?? null, budget.agent ?? null, budget.cost_center_id ?? null, budget.period, budget.limit_usd, budget.alert_at_percent, budget.created_at, budget.updated_at);
1660
1834
  }
1661
1835
  function listBudgets(db) {
1662
1836
  return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
@@ -1678,6 +1852,10 @@ function getBudgetStatuses(db) {
1678
1852
  spendQuery += ` AND agent = ?`;
1679
1853
  params.push(b.agent);
1680
1854
  }
1855
+ if (b.cost_center_id) {
1856
+ spendQuery += ` AND COALESCE(NULLIF(cost_center_id, ''), (SELECT NULLIF(s.cost_center_id, '') FROM sessions s WHERE s.id = requests.session_id)) = ?`;
1857
+ params.push(b.cost_center_id);
1858
+ }
1681
1859
  const row = db.prepare(spendQuery).get(...params);
1682
1860
  const spend = row.spend;
1683
1861
  const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
@@ -3660,6 +3838,13 @@ class LocalStore {
3660
3838
  async accountBreakdown(query = {}) {
3661
3839
  return query.since ? queryAccountBreakdownSince(this.db(), query.since) : queryAccountBreakdown(this.db(), query.period ?? "all", query.machine);
3662
3840
  }
3841
+ async costCenterBreakdown(query = {}) {
3842
+ return queryCostCenterBreakdown(this.db(), query.period ?? "all", {
3843
+ kind: query.kind,
3844
+ machine: query.machine,
3845
+ since: query.since
3846
+ });
3847
+ }
3663
3848
  async accounts(period) {
3664
3849
  return queryAccountBreakdown(this.db(), period);
3665
3850
  }
@@ -3849,6 +4034,14 @@ class ApiStore {
3849
4034
  async accountBreakdown(query = {}) {
3850
4035
  return cloudListItems(this.cloud, "breakdown", q({ by: "account", period: query.period, since: query.since }));
3851
4036
  }
4037
+ async costCenterBreakdown(query = {}) {
4038
+ const rows = await cloudListItems(this.cloud, "breakdown", q({
4039
+ by: query.kind ?? "cost-center",
4040
+ period: query.period,
4041
+ since: query.since
4042
+ }));
4043
+ return rows.filter((row) => Boolean(row?.cost_center_id));
4044
+ }
3852
4045
  async accounts(period) {
3853
4046
  return cloudListItems(this.cloud, "accounts", q({ period }));
3854
4047
  }
@@ -3922,6 +4115,7 @@ class ApiStore {
3922
4115
  const created = await this.cloud.client.create("budgets", {
3923
4116
  project_path: input.project_path,
3924
4117
  ...input.agent ? { agent: input.agent } : {},
4118
+ ...input.cost_center_id ? { cost_center_id: input.cost_center_id } : {},
3925
4119
  period: input.period,
3926
4120
  limit_usd: input.limit_usd,
3927
4121
  alert_at_percent: input.alert_at_percent
@@ -5368,13 +5562,202 @@ var init_hermes = __esm(() => {
5368
5562
  HERMES_DB = join11(homedir8(), ".hermes", "state.db");
5369
5563
  });
5370
5564
 
5565
+ // src/ingest/loops.ts
5566
+ import { existsSync as existsSync10 } from "fs";
5567
+ import { homedir as homedir9 } from "os";
5568
+ import { join as join12 } from "path";
5569
+ import { Database as BunDatabase2 } from "bun:sqlite";
5570
+ function loopsDbPath() {
5571
+ return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? DEFAULT_LOOPS_DB_PATH;
5572
+ }
5573
+ function loopsModel() {
5574
+ return process.env["HASNA_ECONOMY_LOOPS_MODEL"] ?? process.env["ECONOMY_LOOPS_MODEL"] ?? "gpt-5.3-codex";
5575
+ }
5576
+ function openLoopsDb(dbPath, verbose) {
5577
+ let lastError;
5578
+ for (const readonly of [true, false]) {
5579
+ let loopsDb = null;
5580
+ try {
5581
+ loopsDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
5582
+ loopsDb.prepare("PRAGMA schema_version").get();
5583
+ return loopsDb;
5584
+ } catch (error) {
5585
+ lastError = error;
5586
+ loopsDb?.close();
5587
+ }
5588
+ }
5589
+ if (verbose) {
5590
+ const message = lastError instanceof Error ? lastError.message : String(lastError);
5591
+ console.log("OpenLoops DB unreadable:", dbPath, message);
5592
+ }
5593
+ return null;
5594
+ }
5595
+ function slug(value) {
5596
+ return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
5597
+ }
5598
+ function parseTarget(raw) {
5599
+ if (!raw)
5600
+ return {};
5601
+ try {
5602
+ const parsed = JSON.parse(raw);
5603
+ return parsed && typeof parsed === "object" ? parsed : {};
5604
+ } catch {
5605
+ return {};
5606
+ }
5607
+ }
5608
+ function stringValue(value) {
5609
+ return typeof value === "string" ? value.trim() : "";
5610
+ }
5611
+ function accountFields(row) {
5612
+ const target = parseTarget(row.target_json);
5613
+ const tool = stringValue(row.account_tool) || stringValue(target["account_tool"]) || stringValue(target["tool"]) || stringValue(target["agent"]);
5614
+ const profile = stringValue(row.account_profile) || stringValue(target["account_profile"]) || stringValue(target["profile"]) || stringValue(target["account"]);
5615
+ if (!tool || !profile) {
5616
+ return { account_key: "", account_tool: tool, account_name: profile, account_source: "" };
5617
+ }
5618
+ return {
5619
+ account_key: `${tool}:${profile}`,
5620
+ account_tool: tool,
5621
+ account_name: profile,
5622
+ account_source: "loops"
5623
+ };
5624
+ }
5625
+ function readLoopGoalRuns(loopsDb) {
5626
+ return loopsDb.prepare(`
5627
+ SELECT
5628
+ gr.id as goal_run_id,
5629
+ gr.goal_id as goal_id,
5630
+ COALESCE(gr.loop_id, g.loop_id, lr.loop_id) as loop_id,
5631
+ COALESCE(gr.loop_run_id, g.loop_run_id, lr.id) as loop_run_id,
5632
+ COALESCE(l.name, lr.loop_name, 'unknown-loop') as loop_name,
5633
+ l.labels_json as labels_json,
5634
+ l.target_json as target_json,
5635
+ g.objective as objective,
5636
+ gr.phase as phase,
5637
+ gr.status as status,
5638
+ COALESCE(gr.tokens_used, 0) as tokens_used,
5639
+ gr.created_at as created_at,
5640
+ gr.updated_at as updated_at,
5641
+ lr.started_at as loop_run_started_at,
5642
+ wsr.account_profile as account_profile,
5643
+ wsr.account_tool as account_tool
5644
+ FROM goal_runs gr
5645
+ LEFT JOIN goals g ON g.id = gr.goal_id
5646
+ LEFT JOIN loop_runs lr ON lr.id = COALESCE(gr.loop_run_id, g.loop_run_id)
5647
+ LEFT JOIN loops l ON l.id = COALESCE(gr.loop_id, g.loop_id, lr.loop_id)
5648
+ LEFT JOIN (
5649
+ SELECT goal_run_id, MIN(account_profile) as account_profile, MIN(account_tool) as account_tool
5650
+ FROM workflow_step_runs
5651
+ WHERE goal_run_id IS NOT NULL
5652
+ GROUP BY goal_run_id
5653
+ ) wsr ON wsr.goal_run_id = gr.id
5654
+ WHERE COALESCE(gr.tokens_used, 0) > 0
5655
+ ORDER BY gr.created_at ASC, gr.id ASC
5656
+ `).all();
5657
+ }
5658
+ async function ingestLoops(db, verbose = false) {
5659
+ const dbPath = loopsDbPath();
5660
+ if (!existsSync10(dbPath)) {
5661
+ if (verbose)
5662
+ console.log("OpenLoops DB not found:", dbPath);
5663
+ return { sessions: 0, requests: 0 };
5664
+ }
5665
+ const model = loopsModel();
5666
+ const machineId = getMachineId();
5667
+ let loopsDb = null;
5668
+ let requests = 0;
5669
+ const sessions = new Set;
5670
+ try {
5671
+ loopsDb = openLoopsDb(dbPath, verbose);
5672
+ if (!loopsDb)
5673
+ return { sessions: 0, requests: 0 };
5674
+ for (const row of readLoopGoalRuns(loopsDb)) {
5675
+ const loopName = row.loop_name || "unknown-loop";
5676
+ const costCenterId = `loop:${slug(loopName)}`;
5677
+ const sessionId = `loops-${row.loop_run_id ?? row.loop_id ?? row.goal_id}`;
5678
+ const requestId = `loops-${row.goal_run_id}`;
5679
+ const timestamp = row.updated_at ?? row.created_at ?? row.loop_run_started_at ?? new Date().toISOString();
5680
+ const stateValue = `${LOOPS_INGEST_VERSION}:${row.updated_at ?? ""}:${row.tokens_used}:${model}:${costCenterId}`;
5681
+ if (getIngestState(db, "loops", row.goal_run_id) === stateValue)
5682
+ continue;
5683
+ upsertCostCenter(db, {
5684
+ id: costCenterId,
5685
+ kind: "loop",
5686
+ name: loopName,
5687
+ repo_path: null,
5688
+ labels_json: row.labels_json ?? "{}",
5689
+ created_at: row.created_at ?? timestamp
5690
+ });
5691
+ const account = accountFields(row);
5692
+ upsertSession(db, {
5693
+ id: sessionId,
5694
+ agent: "loop",
5695
+ project_path: "",
5696
+ project_name: loopName,
5697
+ started_at: row.loop_run_started_at ?? row.created_at ?? timestamp,
5698
+ ended_at: row.updated_at ?? null,
5699
+ total_cost_usd: 0,
5700
+ total_tokens: 0,
5701
+ request_count: 0,
5702
+ machine_id: machineId,
5703
+ cost_center_id: costCenterId,
5704
+ attribution_tag: "loops-orchestration",
5705
+ ...account
5706
+ });
5707
+ const inputTokens = row.tokens_used;
5708
+ const outputTokens = 0;
5709
+ upsertRequest(db, {
5710
+ id: requestId,
5711
+ agent: "loop",
5712
+ session_id: sessionId,
5713
+ model,
5714
+ input_tokens: inputTokens,
5715
+ output_tokens: outputTokens,
5716
+ cache_read_tokens: 0,
5717
+ cache_create_tokens: 0,
5718
+ cost_usd: computeCostFromDb(db, model, inputTokens, outputTokens),
5719
+ cost_basis: "estimated",
5720
+ duration_ms: 0,
5721
+ timestamp,
5722
+ source_request_id: requestId,
5723
+ machine_id: machineId,
5724
+ cost_center_id: costCenterId,
5725
+ attribution_tag: "loops-orchestration",
5726
+ ...account
5727
+ });
5728
+ sessions.add(sessionId);
5729
+ requests++;
5730
+ setIngestState(db, "loops", row.goal_run_id, stateValue);
5731
+ if (verbose)
5732
+ console.log(`OpenLoops ${loopName}: ${row.tokens_used} orchestration tokens`);
5733
+ }
5734
+ } catch (error) {
5735
+ if (verbose) {
5736
+ const message = error instanceof Error ? error.message : String(error);
5737
+ console.log("OpenLoops ingest skipped:", message);
5738
+ }
5739
+ return { sessions: 0, requests: 0 };
5740
+ } finally {
5741
+ loopsDb?.close();
5742
+ }
5743
+ for (const sessionId of sessions)
5744
+ rollupSession(db, sessionId);
5745
+ return { sessions: sessions.size, requests };
5746
+ }
5747
+ var DEFAULT_LOOPS_DB_PATH, LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
5748
+ var init_loops = __esm(() => {
5749
+ init_database();
5750
+ init_pricing();
5751
+ DEFAULT_LOOPS_DB_PATH = join12(homedir9(), ".hasna", "loops", "loops.db");
5752
+ });
5753
+
5371
5754
  // src/ingest/claude-quota.ts
5372
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
5755
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
5373
5756
  function readClaudeToken() {
5374
5757
  const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
5375
5758
  if (fromEnv)
5376
5759
  return { token: fromEnv };
5377
- if (!existsSync10(CREDENTIALS_PATH))
5760
+ if (!existsSync11(CREDENTIALS_PATH))
5378
5761
  return null;
5379
5762
  try {
5380
5763
  const creds = JSON.parse(readFileSync8(CREDENTIALS_PATH, "utf-8"));
@@ -5518,13 +5901,13 @@ var init_claude_quota = __esm(() => {
5518
5901
  });
5519
5902
 
5520
5903
  // src/ingest/codex-quota.ts
5521
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
5904
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
5522
5905
  function readCodexAuth() {
5523
5906
  const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
5524
5907
  if (fromEnv)
5525
5908
  return { token: fromEnv, authMode: "chatgpt" };
5526
5909
  const authPath = agentPaths().codexAuth;
5527
- if (!existsSync11(authPath))
5910
+ if (!existsSync12(authPath))
5528
5911
  return null;
5529
5912
  try {
5530
5913
  const auth = JSON.parse(readFileSync9(authPath, "utf-8"));
@@ -5662,7 +6045,7 @@ var init_codex_quota = __esm(() => {
5662
6045
 
5663
6046
  // src/lib/sync-all.ts
5664
6047
  async function syncAll(db, opts = {}) {
5665
- const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
6048
+ const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes || opts.loops);
5666
6049
  const all = !anySpecific;
5667
6050
  const result = { deduped: 0 };
5668
6051
  if (all || opts.claude) {
@@ -5685,6 +6068,8 @@ async function syncAll(db, opts = {}) {
5685
6068
  result.pi = await ingestPi(db, opts.verbose);
5686
6069
  if (all || opts.hermes)
5687
6070
  result.hermes = await ingestHermes(db, opts.verbose);
6071
+ if (all || opts.loops)
6072
+ result.loops = await ingestLoops(db, opts.verbose);
5688
6073
  if (opts.dedupe !== false)
5689
6074
  result.deduped = dedupeRequests(db);
5690
6075
  return result;
@@ -5697,6 +6082,7 @@ var init_sync_all = __esm(() => {
5697
6082
  init_cursor();
5698
6083
  init_pi();
5699
6084
  init_hermes();
6085
+ init_loops();
5700
6086
  init_claude_quota();
5701
6087
  init_codex_quota();
5702
6088
  init_database();
@@ -6005,15 +6391,15 @@ __export(exports_config, {
6005
6391
  loadConfig: () => loadConfig2,
6006
6392
  getConfigValue: () => getConfigValue
6007
6393
  });
6008
- import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
6009
- import { dirname as dirname2, join as join12 } from "path";
6394
+ import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
6395
+ import { dirname as dirname2, join as join13 } from "path";
6010
6396
  function getConfigPath() {
6011
- return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join12(getDataDir(), "config.json");
6397
+ return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join13(getDataDir(), "config.json");
6012
6398
  }
6013
6399
  function loadConfig2() {
6014
6400
  try {
6015
6401
  const configPath = getConfigPath();
6016
- if (existsSync12(configPath)) {
6402
+ if (existsSync13(configPath)) {
6017
6403
  const raw = readFileSync11(configPath, "utf-8");
6018
6404
  return { ...DEFAULTS, ...JSON.parse(raw) };
6019
6405
  }
@@ -6023,7 +6409,7 @@ function loadConfig2() {
6023
6409
  function saveConfig2(config) {
6024
6410
  const configPath = getConfigPath();
6025
6411
  const dir = dirname2(configPath);
6026
- if (!existsSync12(dir))
6412
+ if (!existsSync13(dir))
6027
6413
  mkdirSync3(dir, { recursive: true });
6028
6414
  writeFileSync2(configPath, JSON.stringify(config, null, 2) + `
6029
6415
  `);
@@ -6124,6 +6510,8 @@ async function checkAndFireWebhooks(db) {
6124
6510
  event: "budget_alert",
6125
6511
  budget_id: b.id,
6126
6512
  project: b.project_path ?? "global",
6513
+ cost_center_id: b.cost_center_id ?? null,
6514
+ scope: b.cost_center_id ?? b.project_path ?? "global",
6127
6515
  period: b.period,
6128
6516
  spend: b.current_spend_usd,
6129
6517
  limit: b.limit_usd,
@@ -6167,7 +6555,7 @@ var init_webhooks = __esm(() => {
6167
6555
  });
6168
6556
 
6169
6557
  // src/lib/watch-paths.ts
6170
- import { existsSync as existsSync13 } from "fs";
6558
+ import { existsSync as existsSync14 } from "fs";
6171
6559
  function getWatchPaths() {
6172
6560
  const p = agentPaths();
6173
6561
  const candidates = [
@@ -6180,7 +6568,7 @@ function getWatchPaths() {
6180
6568
  p.piSessions,
6181
6569
  p.hermesDir
6182
6570
  ];
6183
- return candidates.filter((path) => existsSync13(path));
6571
+ return candidates.filter((path) => existsSync14(path));
6184
6572
  }
6185
6573
  var init_watch_paths = __esm(() => {
6186
6574
  init_paths();
@@ -6358,8 +6746,8 @@ var init_watch = __esm(() => {
6358
6746
  // src/db/sync-pg.ts
6359
6747
  import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
6360
6748
  import { fileURLToPath } from "url";
6361
- import { dirname as dirname3, join as join13 } from "path";
6362
- import { existsSync as existsSync14 } from "fs";
6749
+ import { dirname as dirname3, join as join14 } from "path";
6750
+ import { existsSync as existsSync15 } from "fs";
6363
6751
  import { translateSql, translateParams } from "@hasna/cloud";
6364
6752
  function translateSqliteDates(sql) {
6365
6753
  let s = sql;
@@ -6385,12 +6773,12 @@ function translateSqliteDates(sql) {
6385
6773
  function resolveWorkerPath() {
6386
6774
  const here = dirname3(fileURLToPath(import.meta.url));
6387
6775
  const candidates = [
6388
- join13(here, "pg-sync-worker.ts"),
6389
- join13(here, "pg-sync-worker.js"),
6390
- join13(here, "..", "db", "pg-sync-worker.js")
6776
+ join14(here, "pg-sync-worker.ts"),
6777
+ join14(here, "pg-sync-worker.js"),
6778
+ join14(here, "..", "db", "pg-sync-worker.js")
6391
6779
  ];
6392
6780
  for (const candidate of candidates)
6393
- if (existsSync14(candidate))
6781
+ if (existsSync15(candidate))
6394
6782
  return candidate;
6395
6783
  return candidates[0];
6396
6784
  }
@@ -8144,7 +8532,7 @@ __export(exports_serve, {
8144
8532
  createHandler: () => createHandler
8145
8533
  });
8146
8534
  import { randomUUID as randomUUID2 } from "crypto";
8147
- import { existsSync as existsSync15 } from "fs";
8535
+ import { existsSync as existsSync16 } from "fs";
8148
8536
  import { resolve, sep } from "path";
8149
8537
  import { verifyApiKey, ApiKeyStore } from "@hasna/contracts/auth";
8150
8538
  function openApiDocument() {
@@ -8227,13 +8615,13 @@ function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
8227
8615
  if (url.pathname.startsWith("/api") || url.pathname.startsWith("/v1") || FOUNDATION_PATHS.has(url.pathname)) {
8228
8616
  return apiHandler(req);
8229
8617
  }
8230
- if (existsSync15(dashboardDir)) {
8618
+ if (existsSync16(dashboardDir)) {
8231
8619
  const filePath = dashboardPath(dashboardDir, url.pathname);
8232
- if (filePath && existsSync15(filePath)) {
8620
+ if (filePath && existsSync16(filePath)) {
8233
8621
  return new Response(Bun.file(filePath));
8234
8622
  }
8235
8623
  const indexPath = dashboardPath(dashboardDir, "/");
8236
- if (indexPath && existsSync15(indexPath)) {
8624
+ if (indexPath && existsSync16(indexPath)) {
8237
8625
  return new Response(Bun.file(indexPath));
8238
8626
  }
8239
8627
  }
@@ -8402,6 +8790,10 @@ function createHandler(db, options = {}) {
8402
8790
  return ok(queryAgentBreakdown(db, period, machine));
8403
8791
  if (by === "account")
8404
8792
  return ok(queryAccountBreakdown(db, period, machine));
8793
+ if (by === "cost-center")
8794
+ return ok(queryCostCenterBreakdown(db, period, { machine }));
8795
+ if (["loop", "app", "repo", "service", "team"].includes(by))
8796
+ return ok(queryCostCenterBreakdown(db, period, { kind: by, machine }));
8405
8797
  return ok(queryModelBreakdown(db));
8406
8798
  }
8407
8799
  if (path === "/api/budgets" && method === "GET") {
@@ -8425,6 +8817,7 @@ function createHandler(db, options = {}) {
8425
8817
  id: randomUUID2(),
8426
8818
  project_path: body["project_path"] ?? null,
8427
8819
  agent,
8820
+ cost_center_id: optionalString(body["cost_center_id"]) ?? null,
8428
8821
  period: normalizeBudgetPeriod(body["period"]),
8429
8822
  limit_usd: limitUsd,
8430
8823
  alert_at_percent: alertAtPercent,
@@ -8766,7 +9159,7 @@ var init_serve = __esm(() => {
8766
9159
  };
8767
9160
  ECONOMY_WRITE_SCOPE = `${APP}:write`;
8768
9161
  AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
8769
- SYNC_SOURCES = ["all", ...AGENTS];
9162
+ SYNC_SOURCES = ["all", ...AGENTS, "loops"];
8770
9163
  DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
8771
9164
  FOUNDATION_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz", "/version", "/openapi.json"]);
8772
9165
  });
@@ -8781,14 +9174,14 @@ __export(exports_menubar, {
8781
9174
  });
8782
9175
  import chalk6 from "chalk";
8783
9176
  import { execFileSync as execFileSync3 } from "child_process";
8784
- import { cpSync, existsSync as existsSync16, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
9177
+ import { cpSync, existsSync as existsSync17, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
8785
9178
  import { tmpdir, arch } from "os";
8786
- import { join as join14 } from "path";
9179
+ import { join as join15 } from "path";
8787
9180
  function getArch() {
8788
9181
  return arch() === "arm64" ? "arm64" : "x86_64";
8789
9182
  }
8790
9183
  function isInstalled() {
8791
- return existsSync16(APP_PATH);
9184
+ return existsSync17(APP_PATH);
8792
9185
  }
8793
9186
  function isRunning() {
8794
9187
  try {
@@ -8825,8 +9218,8 @@ async function menubarInstall(opts) {
8825
9218
  console.error(chalk6.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
8826
9219
  process.exit(1);
8827
9220
  }
8828
- const zipPath = join14(tmpdir(), `economy-bar-${cpuArch}.zip`);
8829
- const extractDir = join14(tmpdir(), "economy-bar-extracted");
9221
+ const zipPath = join15(tmpdir(), `economy-bar-${cpuArch}.zip`);
9222
+ const extractDir = join15(tmpdir(), "economy-bar-extracted");
8830
9223
  console.log(chalk6.cyan(`\u2192 Downloading ${assetUrl}...`));
8831
9224
  try {
8832
9225
  const res = await fetch(assetUrl, { signal: AbortSignal.timeout(60000) });
@@ -8846,7 +9239,7 @@ async function menubarInstall(opts) {
8846
9239
  execFileSync3("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "ignore" });
8847
9240
  if (isInstalled())
8848
9241
  rmSync(APP_PATH, { recursive: true, force: true });
8849
- cpSync(join14(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
9242
+ cpSync(join15(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
8850
9243
  try {
8851
9244
  execFileSync3("xattr", ["-rd", "com.apple.quarantine", APP_PATH], { stdio: "ignore" });
8852
9245
  } catch {}
@@ -9950,6 +10343,8 @@ var ROADMAP_PHASES = [
9950
10343
  ]
9951
10344
  }
9952
10345
  ];
10346
+ var DEFAULT_TODO_LIST_LIMIT = 20;
10347
+ var TODO_TITLE_WIDTH = 140;
9953
10348
  function statusLabel(status) {
9954
10349
  if (status === "done")
9955
10350
  return chalk2.green("done");
@@ -9963,6 +10358,30 @@ function countByStatus(tasks) {
9963
10358
  return acc;
9964
10359
  }, { pending: 0, in_progress: 0, done: 0 });
9965
10360
  }
10361
+ function truncateText(value, max) {
10362
+ if (value.length <= max)
10363
+ return value;
10364
+ return `${value.slice(0, Math.max(0, max - 3)).trimEnd()}...`;
10365
+ }
10366
+ function parseListLimit(value) {
10367
+ if (value == null)
10368
+ return;
10369
+ const parsed = Number(value);
10370
+ if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
10371
+ console.error(chalk2.red("--limit must be a positive integer"));
10372
+ process.exit(1);
10373
+ }
10374
+ return parsed;
10375
+ }
10376
+ function matchingTasks(phases, status) {
10377
+ const rows = [];
10378
+ for (const phase of phases) {
10379
+ const tasks = status ? phase.tasks.filter((task) => task.status === status) : phase.tasks;
10380
+ for (const task of tasks)
10381
+ rows.push({ phase, task });
10382
+ }
10383
+ return rows;
10384
+ }
9966
10385
  function printTodosHelp() {
9967
10386
  console.log();
9968
10387
  console.log(chalk2.bold.cyan(" Economy Roadmap \u2014 fleet, usage & savings"));
@@ -9972,7 +10391,9 @@ function printTodosHelp() {
9972
10391
  console.log();
9973
10392
  console.log(chalk2.bold(" Commands"));
9974
10393
  console.log(" economy todos Overview of phases and progress");
9975
- console.log(" economy todos list All granular tasks");
10394
+ console.log(" economy todos list Compact task list (first 20 by default)");
10395
+ console.log(" economy todos list --verbose Full task list with dependencies");
10396
+ console.log(" economy todos list --json Machine-readable roadmap payload");
9976
10397
  console.log(" economy todos list --phase <id> Tasks for one phase (e.g. phase-9)");
9977
10398
  console.log(" economy todos show <task-id> Task detail (e.g. 9.7)");
9978
10399
  console.log();
@@ -10014,7 +10435,8 @@ function printTodosOverview() {
10014
10435
  console.log(chalk2.dim(` ${phase.summary}`));
10015
10436
  }
10016
10437
  console.log();
10017
- console.log(chalk2.dim(" Run: economy todos list"));
10438
+ console.log(chalk2.dim(" Run: economy todos list --limit 20"));
10439
+ console.log(chalk2.dim(" Run: economy todos list --verbose"));
10018
10440
  console.log(chalk2.dim(" Run: economy todos --help"));
10019
10441
  console.log();
10020
10442
  }
@@ -10025,21 +10447,55 @@ function printTodosList(opts) {
10025
10447
  console.log(chalk2.dim(`Valid phases: ${ROADMAP_PHASES.map((phase) => phase.id).join(", ")}`));
10026
10448
  return;
10027
10449
  }
10450
+ const allMatches = matchingTasks(phases, opts.status);
10451
+ const limit = opts.limit ?? (opts.verbose ? Number.POSITIVE_INFINITY : DEFAULT_TODO_LIST_LIMIT);
10452
+ const visible = allMatches.slice(0, limit);
10453
+ if (opts.json) {
10454
+ const filteredPhases = phases.map((phase) => ({
10455
+ ...phase,
10456
+ tasks: opts.status ? phase.tasks.filter((task) => task.status === opts.status) : phase.tasks
10457
+ })).filter((phase) => phase.tasks.length > 0);
10458
+ console.log(JSON.stringify({
10459
+ version: ROADMAP_VERSION,
10460
+ goal: ROADMAP_GOAL,
10461
+ total: allMatches.length,
10462
+ phases: filteredPhases
10463
+ }, null, 2));
10464
+ return;
10465
+ }
10028
10466
  console.log();
10467
+ console.log(chalk2.bold.cyan(" Economy Roadmap Tasks"));
10468
+ console.log(chalk2.dim(` ${allMatches.length} matching tasks \xB7 showing ${visible.length}${visible.length < allMatches.length ? ` of ${allMatches.length}` : ""}`));
10469
+ if (!opts.verbose && visible.length < allMatches.length) {
10470
+ console.log(chalk2.dim(" Use --verbose for all tasks or economy todos show <task-id> for details."));
10471
+ }
10472
+ console.log();
10473
+ const visibleByPhase = new Map;
10474
+ for (const { phase, task } of visible) {
10475
+ const tasks = visibleByPhase.get(phase.id) ?? [];
10476
+ tasks.push(task);
10477
+ visibleByPhase.set(phase.id, tasks);
10478
+ }
10029
10479
  for (const phase of phases) {
10030
- const tasks = opts.status ? phase.tasks.filter((task) => task.status === opts.status) : phase.tasks;
10480
+ const tasks = visibleByPhase.get(phase.id) ?? [];
10031
10481
  if (tasks.length === 0)
10032
10482
  continue;
10033
10483
  console.log(chalk2.bold.cyan(` ${phase.id} \u2014 ${phase.title}`));
10034
- console.log(chalk2.dim(` ${phase.summary}`));
10484
+ if (opts.verbose)
10485
+ console.log(chalk2.dim(` ${phase.summary}`));
10035
10486
  console.log();
10036
10487
  for (const task of tasks) {
10037
10488
  const deps = task.deps?.length ? chalk2.dim(` deps: ${task.deps.join(", ")}`) : "";
10038
- console.log(` ${chalk2.dim(task.id.padEnd(5))} ${statusLabel(task.status).padEnd(14)} ${task.title}${deps ? `
10489
+ const title = opts.verbose ? task.title : truncateText(task.title, TODO_TITLE_WIDTH);
10490
+ console.log(` ${chalk2.dim(task.id.padEnd(5))} ${statusLabel(task.status).padEnd(14)} ${title}${opts.verbose && deps ? `
10039
10491
  ${" ".repeat(22)}${deps}` : ""}`);
10040
10492
  }
10041
10493
  console.log();
10042
10494
  }
10495
+ if (visible.length < allMatches.length) {
10496
+ console.log(chalk2.dim(` ... ${allMatches.length - visible.length} more tasks hidden. Use --limit <n>, --verbose, --phase <id>, or economy todos show <task-id>.`));
10497
+ console.log();
10498
+ }
10043
10499
  }
10044
10500
  function printTodoShow(taskId) {
10045
10501
  for (const phase of ROADMAP_PHASES) {
@@ -10069,13 +10525,19 @@ function registerTodosCommand(program) {
10069
10525
  }
10070
10526
  printTodosOverview();
10071
10527
  });
10072
- todosCmd.command("list").description("List granular roadmap tasks").option("--phase <id>", "Filter by phase id (e.g. phase-4)").option("--status <status>", "Filter by status: pending|in_progress|done").action((opts) => {
10528
+ todosCmd.command("list").description("List granular roadmap tasks").option("--phase <id>", "Filter by phase id (e.g. phase-4)").option("--status <status>", "Filter by status: pending|in_progress|done").option("--limit <n>", "Maximum tasks to print (default: 20)").option("--verbose", "Print all matching tasks with phase summaries and dependencies").option("--json", "Output the full filtered roadmap as JSON").action((opts) => {
10073
10529
  const status = opts.status;
10074
10530
  if (status && !["pending", "in_progress", "done"].includes(status)) {
10075
10531
  console.error(chalk2.red("--status must be one of: pending, in_progress, done"));
10076
10532
  process.exit(1);
10077
10533
  }
10078
- printTodosList({ phase: opts.phase, status });
10534
+ printTodosList({
10535
+ phase: opts.phase,
10536
+ status,
10537
+ limit: parseListLimit(opts.limit),
10538
+ verbose: Boolean(opts.verbose),
10539
+ json: Boolean(opts.json)
10540
+ });
10079
10541
  });
10080
10542
  todosCmd.command("show <task-id>").description("Show a single task (e.g. 4.2)").action((taskId) => {
10081
10543
  printTodoShow(taskId);
@@ -10259,8 +10721,18 @@ function parsePeriod(value, fallback = "month") {
10259
10721
  }
10260
10722
  return p;
10261
10723
  }
10724
+ function parseLimit(value, fallback, verbose) {
10725
+ if (verbose && value == null)
10726
+ return Number.POSITIVE_INFINITY;
10727
+ const parsed = Number(value ?? String(fallback));
10728
+ if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
10729
+ console.error(chalk4.red("--limit must be a positive integer"));
10730
+ process.exit(1);
10731
+ }
10732
+ return parsed;
10733
+ }
10262
10734
  function registerExtendedCommands(program) {
10263
- program.command("usage [period]").description("Show subscription usage quotas and consumption metrics").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--json", "Output JSON").action(async (periodArg, opts) => {
10735
+ program.command("usage [period]").description("Show subscription usage quotas and consumption metrics").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--limit <n>", "Maximum snapshot rows to print (default: 20)").option("--verbose", "Show all snapshot rows").option("--json", "Output JSON").action(async (periodArg, opts) => {
10264
10736
  const period = parsePeriod(periodArg, "month");
10265
10737
  const agent = parseAgent(opts.agent, "--agent");
10266
10738
  const { snapshots: snaps, summary } = await getStore().usage(period, agent);
@@ -10274,9 +10746,19 @@ function registerExtendedCommands(program) {
10274
10746
  if (snaps.length === 0) {
10275
10747
  console.log(chalk4.yellow(" No usage snapshots yet. Run: economy sync --cursor"));
10276
10748
  } else {
10277
- for (const s of snaps) {
10749
+ const limit = parseLimit(opts.limit, 20, opts.verbose);
10750
+ const visibleSnaps = snaps.slice(0, limit);
10751
+ if (visibleSnaps.length < snaps.length) {
10752
+ console.log(chalk4.dim(` ${snaps.length} snapshots \xB7 showing ${visibleSnaps.length}`));
10753
+ console.log();
10754
+ }
10755
+ for (const s of visibleSnaps) {
10278
10756
  console.log(` ${chalk4.white(s.agent.padEnd(10))} ${s.metric.padEnd(28)} ${s.value}${s.unit ? ` ${s.unit}` : ""} ${chalk4.dim(s.date)}`);
10279
10757
  }
10758
+ if (visibleSnaps.length < snaps.length) {
10759
+ console.log(chalk4.dim(`
10760
+ ... ${snaps.length - visibleSnaps.length} more snapshots hidden. Use --limit <n>, --verbose, or --json.`));
10761
+ }
10280
10762
  }
10281
10763
  const spendLabel = agent ? `${agent} API-equivalent spend:` : "Fleet API-equivalent spend:";
10282
10764
  console.log(`
@@ -10316,7 +10798,7 @@ function registerExtendedCommands(program) {
10316
10798
  });
10317
10799
  console.log(chalk4.green(`\u2713 Subscription set: ${opts.provider} / ${opts.plan}`));
10318
10800
  });
10319
- subsCmd.command("list").description("List subscription plans").option("--json", "Output JSON").action(async (opts) => {
10801
+ subsCmd.command("list").description("List subscription plans").option("--limit <n>", "Maximum subscription rows to print (default: 20)").option("--verbose", "Show all subscription rows").option("--json", "Output JSON").action(async (opts) => {
10320
10802
  const rows = await getStore().listSubscriptions();
10321
10803
  if (opts.json) {
10322
10804
  console.log(JSON.stringify(rows, null, 2));
@@ -10326,10 +10808,20 @@ function registerExtendedCommands(program) {
10326
10808
  console.log(chalk4.yellow("No subscriptions configured."));
10327
10809
  return;
10328
10810
  }
10811
+ const limit = parseLimit(opts.limit, 20, opts.verbose);
10812
+ const visibleRows = rows.slice(0, limit);
10329
10813
  console.log();
10330
- for (const r of rows) {
10814
+ if (visibleRows.length < rows.length) {
10815
+ console.log(chalk4.dim(` ${rows.length} subscriptions \xB7 showing ${visibleRows.length}`));
10816
+ console.log();
10817
+ }
10818
+ for (const r of visibleRows) {
10331
10819
  console.log(` ${chalk4.white(r.provider)} / ${r.plan} ${fmt2(r.monthly_fee_usd)}/mo included ${fmt2(r.included_usage_usd)} ${r.agent ?? "all agents"}`);
10332
10820
  }
10821
+ if (visibleRows.length < rows.length) {
10822
+ console.log(chalk4.dim(`
10823
+ ... ${rows.length - visibleRows.length} more subscriptions hidden. Use --limit <n>, --verbose, or --json.`));
10824
+ }
10333
10825
  console.log();
10334
10826
  });
10335
10827
  subsCmd.command("remove <id>").description("Remove subscription by id").action(async (id) => {
@@ -10453,20 +10945,29 @@ function registerExtendedCommands(program) {
10453
10945
  }
10454
10946
  }
10455
10947
  function registerFleetCommands(program) {
10456
- program.command("fleet").description("Fleet-wide summaries across all machines").option("--period <p>", "Period", "today").option("--json", "Output JSON").action(async (opts) => {
10948
+ program.command("fleet").description("Fleet-wide summaries across all machines").option("--period <p>", "Period", "today").option("--limit <n>", "Maximum machine rows to print (default: 20)").option("--verbose", "Show all machine rows").option("--json", "Output JSON").action(async (opts) => {
10457
10949
  const period = parsePeriod(opts.period, "today");
10458
10950
  const { summary, machines, registry } = await getStore().fleet(period);
10459
10951
  if (opts.json) {
10460
10952
  console.log(JSON.stringify({ period, summary, machines, registry }, null, 2));
10461
10953
  return;
10462
10954
  }
10955
+ const limit = parseLimit(opts.limit, 20, opts.verbose);
10956
+ const visibleMachines = machines.slice(0, limit);
10463
10957
  console.log();
10464
10958
  console.log(chalk4.bold.cyan(` Fleet \u2014 ${period}`));
10465
10959
  console.log(` Total: ${fmt2(summary.total_usd)} \xB7 ${summary.sessions} sessions \xB7 ${summary.requests} requests`);
10960
+ if (visibleMachines.length < machines.length) {
10961
+ console.log(chalk4.dim(` ${machines.length} machines \xB7 showing ${visibleMachines.length}`));
10962
+ }
10466
10963
  console.log();
10467
- for (const m of machines) {
10964
+ for (const m of visibleMachines) {
10468
10965
  console.log(` ${chalk4.white(m.machine_id.padEnd(12))} ${fmt2(m.total_cost_usd).padEnd(10)} ${m.sessions} sessions`);
10469
10966
  }
10967
+ if (visibleMachines.length < machines.length) {
10968
+ console.log(chalk4.dim(`
10969
+ ... ${machines.length - visibleMachines.length} more machines hidden. Use --limit <n>, --verbose, or --json.`));
10970
+ }
10470
10971
  console.log();
10471
10972
  });
10472
10973
  program.command("completion <shell>").description("Print shell completion script (bash, zsh, or fish)").action((shell) => {
@@ -10721,6 +11222,16 @@ function parseOptionalCliAgent(value) {
10721
11222
  fail(e instanceof Error ? e.message : String(e));
10722
11223
  }
10723
11224
  }
11225
+ function resolveRowLimit(value, verbose, fallback) {
11226
+ if (verbose && value == null)
11227
+ return Number.POSITIVE_INFINITY;
11228
+ return parsePositiveCliInteger(value ?? String(fallback), "--limit");
11229
+ }
11230
+ function printHiddenRowsHint(total, shown, detail) {
11231
+ if (total <= shown)
11232
+ return;
11233
+ console.log(chalk7.dim(` ... ${total - shown} more rows hidden. ${detail}`));
11234
+ }
10724
11235
  function requireCliChoice(value, option, allowed) {
10725
11236
  const selected = value ?? allowed[0];
10726
11237
  if (allowed.includes(selected))
@@ -10820,14 +11331,14 @@ program.action(async () => {
10820
11331
  }
10821
11332
  console.log();
10822
11333
  });
10823
- program.command("sync").description("Ingest cost data from all coding agents").option("--claude", "Only ingest Claude Code telemetry").option("--takumi", "Only ingest Takumi sessions").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("--opencode", "Only ingest OpenCode sessions").option("--cursor", "Only ingest Cursor usage").option("--pi", "Only ingest Pi sessions").option("--hermes", "Only ingest Hermes sessions").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").option("--backfill-machine", "Tag existing records that have no machine_id with current hostname").option("--recalculate", "Recalculate costs for all requests with cost_usd = 0").action(async (opts) => {
11334
+ program.command("sync").description("Ingest cost data from all coding agents").option("--claude", "Only ingest Claude Code telemetry").option("--takumi", "Only ingest Takumi sessions").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("--opencode", "Only ingest OpenCode sessions").option("--cursor", "Only ingest Cursor usage").option("--pi", "Only ingest Pi sessions").option("--hermes", "Only ingest Hermes sessions").option("--loops", "Only ingest OpenLoops orchestration/judge token usage").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").option("--backfill-machine", "Tag existing records that have no machine_id with current hostname").option("--recalculate", "Recalculate costs for all requests with cost_usd = 0").action(async (opts) => {
10824
11335
  if (isCloudStore()) {
10825
11336
  console.log(chalk7.yellow("cloud mode: ingest is a local-only operation; nothing to sync (reads/writes route to the cloud API)"));
10826
11337
  return;
10827
11338
  }
10828
11339
  const db = openDatabase();
10829
11340
  ensurePricingSeeded(db);
10830
- const anySpecific = opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes;
11341
+ const anySpecific = opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes || opts.loops;
10831
11342
  if (!anySpecific || opts.verbose) {
10832
11343
  try {
10833
11344
  const { syncOpenProjectsRegistry: syncOpenProjectsRegistry2 } = await Promise.resolve().then(() => (init_open_projects(), exports_open_projects));
@@ -10840,7 +11351,7 @@ program.command("sync").description("Ingest cost data from all coding agents").o
10840
11351
  }
10841
11352
  }
10842
11353
  if (opts.force) {
10843
- const ingestSources = [...AGENTS, "codex-codewith"];
11354
+ const ingestSources = [...AGENTS, "codex-codewith", "loops"];
10844
11355
  db.exec(`DELETE FROM ingest_state WHERE source IN (${ingestSources.map((a) => `'${a}'`).join(", ")})`);
10845
11356
  if (opts.verbose)
10846
11357
  console.log(chalk7.dim("Cleared ingest cache"));
@@ -10855,6 +11366,7 @@ program.command("sync").description("Ingest cost data from all coding agents").o
10855
11366
  cursor: opts.cursor,
10856
11367
  pi: opts.pi,
10857
11368
  hermes: opts.hermes,
11369
+ loops: opts.loops,
10858
11370
  verbose: opts.verbose
10859
11371
  });
10860
11372
  console.log(chalk7.green(`\u2713 deduped ${result.deduped}`));
@@ -10984,22 +11496,36 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
10984
11496
  ]));
10985
11497
  console.log();
10986
11498
  });
10987
- program.command("breakdown").description("Cost breakdown by model, agent, project, or account").option("--by <dimension>", "Dimension: model|agent|project|account", "model").option("--since <date>", "Filter since date or relative (e.g. 2026-03-01, 7d, 30d)").action(async (opts) => {
11499
+ program.command("breakdown").description("Cost breakdown by model, agent, project, account, or cost center").option("--by <dimension>", "Dimension: model|agent|project|account|cost-center|loop|app|repo", "model").option("--since <date>", "Filter since date or relative (e.g. 2026-03-01, 7d, 30d)").option("--limit <n>", "Maximum breakdown rows to print (default: 20)").option("--verbose", "Show all breakdown rows").option("--json", "Output JSON").action(async (opts) => {
10988
11500
  const store = getStore();
11501
+ const by = requireCliChoice(opts.by, "--by", ["model", "agent", "project", "account", "cost-center", "loop", "app", "repo"]);
10989
11502
  const since = opts.since ? parseSinceDate2(opts.since) : undefined;
11503
+ const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
11504
+ const costCenterKinds = new Set(["loop", "app", "repo"]);
10990
11505
  console.log();
10991
- if (opts.by === "project") {
11506
+ if (by === "project") {
10992
11507
  const rows = await store.projectBreakdown({ since });
10993
- printTable(["Project", "Sessions", "Requests", "Tokens", "Cost"], rows.map((r) => [
11508
+ if (opts.json) {
11509
+ console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
11510
+ return;
11511
+ }
11512
+ const visibleRows = rows.slice(0, limit);
11513
+ printTable(["Project", "Sessions", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
10994
11514
  chalk7.white(r.project_name || chalk7.dim("unknown")),
10995
11515
  String(r.sessions),
10996
11516
  String(r.requests),
10997
11517
  chalk7.cyan(fmtTokens2(r.total_tokens)),
10998
11518
  fmt4(r.cost_usd)
10999
11519
  ]));
11000
- } else if (opts.by === "agent") {
11520
+ printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
11521
+ } else if (by === "agent") {
11001
11522
  const rows = await store.agentBreakdown({ since });
11002
- printTable(["Agent", "Sessions", "Requests", "Tokens", "API Eq", "Billable", "Included"], rows.map((r) => [
11523
+ if (opts.json) {
11524
+ console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
11525
+ return;
11526
+ }
11527
+ const visibleRows = rows.slice(0, limit);
11528
+ printTable(["Agent", "Sessions", "Requests", "Tokens", "API Eq", "Billable", "Included"], visibleRows.map((r) => [
11003
11529
  fmtAgent(r.agent),
11004
11530
  String(r.sessions),
11005
11531
  String(r.requests),
@@ -11008,22 +11534,57 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
11008
11534
  fmt4(r.billable_usd),
11009
11535
  fmt4(r.subscription_included_usd)
11010
11536
  ]));
11011
- } else if (opts.by === "account") {
11012
- printAccountBreakdown(await store.accountBreakdown({ since }));
11537
+ printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
11538
+ } else if (by === "account") {
11539
+ const rows = await store.accountBreakdown({ since });
11540
+ if (opts.json) {
11541
+ console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
11542
+ return;
11543
+ }
11544
+ const visibleRows = rows.slice(0, limit);
11545
+ printAccountBreakdown(visibleRows);
11546
+ printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
11547
+ } else if (by === "cost-center" || costCenterKinds.has(by)) {
11548
+ const kind = costCenterKinds.has(by) ? by : undefined;
11549
+ const rows = await store.costCenterBreakdown({ since, kind });
11550
+ if (opts.json) {
11551
+ console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
11552
+ return;
11553
+ }
11554
+ if (rows.length === 0) {
11555
+ console.log(chalk7.yellow("No cost-center usage yet. Run `economy sync --loops` or ingest app/service usage with /ingest."));
11556
+ } else {
11557
+ const visibleRows = rows.slice(0, limit);
11558
+ printTable(["Kind", "Cost Center", "Sessions", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
11559
+ chalk7.white(r.kind),
11560
+ chalk7.white(r.name),
11561
+ String(r.sessions),
11562
+ String(r.requests),
11563
+ chalk7.cyan(fmtTokens2(r.total_tokens)),
11564
+ fmt4(r.cost_usd)
11565
+ ]));
11566
+ printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
11567
+ }
11013
11568
  } else {
11014
11569
  const rows = await store.modelBreakdown({ since });
11015
- printTable(["Model", "Agent", "Requests", "Tokens", "Cost"], rows.map((r) => [
11570
+ if (opts.json) {
11571
+ console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
11572
+ return;
11573
+ }
11574
+ const visibleRows = rows.slice(0, limit);
11575
+ printTable(["Model", "Agent", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
11016
11576
  chalk7.white(r.model),
11017
11577
  fmtAgent(r.agent),
11018
11578
  String(r.requests),
11019
11579
  chalk7.cyan(fmtTokens2(r.total_tokens)),
11020
11580
  fmt4(r.cost_usd)
11021
11581
  ]));
11582
+ printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
11022
11583
  }
11023
11584
  console.log();
11024
11585
  });
11025
11586
  var ACCOUNT_PERIODS = ["today", "week", "month", "year", "all"];
11026
- program.command("accounts [period]").description("List account usage by email address and coding agent").option("--json", "Output JSON").action(async (periodArg, opts) => {
11587
+ program.command("accounts [period]").description("List account usage by email address and coding agent").option("--limit <n>", "Maximum account rows to print (default: 20)").option("--verbose", "Show all account rows").option("--json", "Output JSON").action(async (periodArg, opts) => {
11027
11588
  const period = requireCliChoice(periodArg, "period", ACCOUNT_PERIODS);
11028
11589
  const rows = await getStore().accounts(period);
11029
11590
  if (opts.json) {
@@ -11036,8 +11597,14 @@ program.command("accounts [period]").description("List account usage by email ad
11036
11597
  }
11037
11598
  console.log();
11038
11599
  console.log(chalk7.bold.cyan(` Accounts \u2014 ${period}`));
11600
+ const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
11601
+ const visibleRows = rows.slice(0, limit);
11602
+ if (visibleRows.length < rows.length) {
11603
+ console.log(chalk7.dim(` ${rows.length} rows \xB7 showing ${visibleRows.length}`));
11604
+ }
11039
11605
  console.log();
11040
- printAccountBreakdown(rows);
11606
+ printAccountBreakdown(visibleRows);
11607
+ printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
11041
11608
  console.log();
11042
11609
  });
11043
11610
  program.command("watch").description("Live stream of incoming costs").option("--interval <seconds>", "Poll interval in seconds", "10").option("--daemon", "Watch agent data directories and sync on change").option("--agent <agent>", "Filter by agent").option("--notify <amount>", "Fire macOS notification when cumulative cost crosses this USD threshold").action(async (opts) => {
@@ -11050,7 +11617,7 @@ program.command("watch").description("Live stream of incoming costs").option("--
11050
11617
  });
11051
11618
  });
11052
11619
  var budgetCmd = program.command("budget").description("Manage spending budgets");
11053
- budgetCmd.command("set").description("Set a budget").option("--project <path>", "Project path (omit for global)").option("--period <period>", "Period: daily|weekly|monthly", "monthly").option("--limit <usd>", "Budget limit in USD").option("--alert <percent>", "Alert threshold %", "80").option("--agent <agent>", "Limit to agent (claude|takumi|codex|gemini)").action(async (opts) => {
11620
+ budgetCmd.command("set").description("Set a budget").option("--project <path>", "Project path (omit for global)").option("--cost-center <id>", "Cost center id (for example loop:fleet-evaluator)").option("--period <period>", "Period: daily|weekly|monthly", "monthly").option("--limit <usd>", "Budget limit in USD").option("--alert <percent>", "Alert threshold %", "80").option("--agent <agent>", "Limit to agent (claude|takumi|codex|gemini)").action(async (opts) => {
11054
11621
  const limitUsd = parsePositiveCliNumber(opts.limit, "--limit");
11055
11622
  const alertAtPercent = parsePositiveCliNumber(opts.alert ?? "80", "--alert");
11056
11623
  if (alertAtPercent > 100)
@@ -11060,25 +11627,32 @@ budgetCmd.command("set").description("Set a budget").option("--project <path>",
11060
11627
  await getStore().setBudget({
11061
11628
  project_path: opts.project ?? null,
11062
11629
  agent,
11630
+ cost_center_id: opts.costCenter ?? null,
11063
11631
  period,
11064
11632
  limit_usd: limitUsd,
11065
11633
  alert_at_percent: alertAtPercent
11066
11634
  });
11067
- console.log(chalk7.green(`\u2713 Budget set: ${opts.project ?? "global"} \u2014 ${period} $${limitUsd}`));
11635
+ console.log(chalk7.green(`\u2713 Budget set: ${opts.costCenter ?? opts.project ?? "global"} \u2014 ${period} $${limitUsd}`));
11068
11636
  });
11069
- budgetCmd.command("list").description("List all budgets").action(async () => {
11637
+ budgetCmd.command("list").description("List all budgets").option("--limit <n>", "Maximum budget rows to print (default: 20)").option("--verbose", "Show all budget rows").action(async (opts) => {
11070
11638
  const statuses = await getStore().listBudgets();
11071
11639
  if (statuses.length === 0) {
11072
11640
  console.log(chalk7.yellow("No budgets set."));
11073
11641
  return;
11074
11642
  }
11643
+ const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
11644
+ const visibleStatuses = statuses.slice(0, limit);
11075
11645
  console.log();
11076
- printTable(["Scope", "Period", "Limit", "Spent", "Used%", "Status"], statuses.map((b) => {
11646
+ if (visibleStatuses.length < statuses.length) {
11647
+ console.log(chalk7.dim(` ${statuses.length} budgets \xB7 showing ${visibleStatuses.length}`));
11648
+ console.log();
11649
+ }
11650
+ printTable(["Scope", "Period", "Limit", "Spent", "Used%", "Status"], visibleStatuses.map((b) => {
11077
11651
  const pct = b.percent_used.toFixed(1);
11078
11652
  const status = b.is_over_limit ? chalk7.red("OVER") : b.is_over_alert ? chalk7.yellow("ALERT") : chalk7.green("OK");
11079
11653
  const pctColor = b.is_over_limit ? chalk7.red(pct + "%") : b.is_over_alert ? chalk7.yellow(pct + "%") : chalk7.green(pct + "%");
11080
11654
  return [
11081
- chalk7.white(b.project_path ?? "global"),
11655
+ chalk7.white(b.cost_center_id ?? b.project_path ?? "global"),
11082
11656
  b.period,
11083
11657
  fmt4(b.limit_usd),
11084
11658
  fmt4(b.current_spend_usd),
@@ -11086,6 +11660,7 @@ budgetCmd.command("list").description("List all budgets").action(async () => {
11086
11660
  status
11087
11661
  ];
11088
11662
  }));
11663
+ printHiddenRowsHint(statuses.length, visibleStatuses.length, "Use --limit <n> or --verbose.");
11089
11664
  console.log();
11090
11665
  });
11091
11666
  budgetCmd.command("remove <id>").description("Remove a budget by ID").action(async (id) => {
@@ -11099,20 +11674,27 @@ projectCmd.command("add <path>").description("Add a project").option("--name <na
11099
11674
  await getStore().addProject(path, name);
11100
11675
  console.log(chalk7.green(`\u2713 Project added: ${path}`));
11101
11676
  });
11102
- projectCmd.command("list").description("List all projects with costs").action(async () => {
11677
+ projectCmd.command("list").description("List all projects with costs").option("--limit <n>", "Maximum project rows to print (default: 20)").option("--verbose", "Show all project rows").action(async (opts) => {
11103
11678
  const projects = await getStore().listProjects();
11104
11679
  if (projects.length === 0) {
11105
11680
  console.log(chalk7.yellow("No projects tracked yet."));
11106
11681
  return;
11107
11682
  }
11683
+ const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
11684
+ const visibleProjects = projects.slice(0, limit);
11108
11685
  console.log();
11109
- printTable(["Project", "Path", "Sessions", "Cost", "Last Active"], projects.map((p) => [
11686
+ if (visibleProjects.length < projects.length) {
11687
+ console.log(chalk7.dim(` ${projects.length} projects \xB7 showing ${visibleProjects.length}`));
11688
+ console.log();
11689
+ }
11690
+ printTable(["Project", "Path", "Sessions", "Cost", "Last Active"], visibleProjects.map((p) => [
11110
11691
  chalk7.white(p.project_name || chalk7.dim("unknown")),
11111
11692
  chalk7.dim(p.project_path.substring(0, 40)),
11112
11693
  String(p.sessions),
11113
11694
  fmt4(p.cost_usd),
11114
11695
  chalk7.dim(p.last_active?.substring(0, 16) ?? "\u2014")
11115
11696
  ]));
11697
+ printHiddenRowsHint(projects.length, visibleProjects.length, "Use --limit <n>, --verbose, or economy project show <path>.");
11116
11698
  console.log();
11117
11699
  });
11118
11700
  projectCmd.command("remove <path>").description("Remove a project (keeps historical data)").action(async (path) => {
@@ -11217,10 +11799,16 @@ configCmd.action(async () => {
11217
11799
  console.log();
11218
11800
  });
11219
11801
  var pricingCmd = program.command("pricing").description("Manage model pricing rates");
11220
- pricingCmd.command("list").description("List all model prices").action(async () => {
11802
+ pricingCmd.command("list").description("List all model prices").option("--limit <n>", "Maximum pricing rows to print (default: 50)").option("--verbose", "Show all pricing rows").action(async (opts) => {
11221
11803
  const rows = await getStore().listPricing();
11804
+ const limit = resolveRowLimit(opts.limit, opts.verbose, 50);
11805
+ const visibleRows = rows.slice(0, limit);
11222
11806
  console.log();
11223
- printTable(["Model", "Input/1M", "Output/1M", "CacheR/1M", "CacheW/1M", "CacheStorage/1M-h", "Out/1k"], rows.map((r) => [
11807
+ if (visibleRows.length < rows.length) {
11808
+ console.log(chalk7.dim(` ${rows.length} pricing rows \xB7 showing ${visibleRows.length}`));
11809
+ console.log();
11810
+ }
11811
+ printTable(["Model", "Input/1M", "Output/1M", "CacheR/1M", "CacheW/1M", "CacheStorage/1M-h", "Out/1k"], visibleRows.map((r) => [
11224
11812
  chalk7.white(r.model),
11225
11813
  fmt4(r.input_per_1m),
11226
11814
  fmt4(r.output_per_1m),
@@ -11229,6 +11817,7 @@ pricingCmd.command("list").description("List all model prices").action(async ()
11229
11817
  fmt4(r.cache_storage_per_1m_hour ?? 0),
11230
11818
  chalk7.dim(fmt4(r.output_per_1m / 1000))
11231
11819
  ]));
11820
+ printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n> or --verbose.");
11232
11821
  console.log();
11233
11822
  });
11234
11823
  pricingCmd.command("set <model>").description("Set pricing for a model").option("--input <usd>", "Input price per 1M tokens").option("--output <usd>", "Output price per 1M tokens").option("--cache-read <usd>", "Cache read price per 1M tokens", "0").option("--cache-write <usd>", "5-minute cache write price per 1M tokens", "0").option("--cache-write-1h <usd>", "1-hour cache write price per 1M tokens", "0").option("--cache-storage <usd>", "Context cache storage price per 1M token-hours", "0").action(async (model, opts) => {
@@ -11323,7 +11912,8 @@ Gemini (~/.gemini/settings.json):`));
11323
11912
  }
11324
11913
  console.log();
11325
11914
  });
11326
- program.command("session <id>").description("Show detailed breakdown of a single session").action(async (id) => {
11915
+ program.command("session <id>").description("Show detailed breakdown of a single session").option("--limit <n>", "Number of request rows to show (default: 20)").option("--verbose", "Show up to 50 request rows").action(async (id, opts) => {
11916
+ const requestLimit = parsePositiveCliInteger(opts.limit ?? (opts.verbose ? "50" : "20"), "--limit");
11327
11917
  await autoSync();
11328
11918
  const detail = await getStore().sessionDetail(id);
11329
11919
  if (!detail) {
@@ -11344,10 +11934,11 @@ program.command("session <id>").description("Show detailed breakdown of a single
11344
11934
  ["Requests", fmtCount(session["request_count"])]
11345
11935
  ]);
11346
11936
  if (requests.length > 0) {
11937
+ const visibleRequests = requests.slice(0, requestLimit);
11347
11938
  console.log(chalk7.dim(`
11348
- Requests (${requests.length}):
11939
+ Requests (${visibleRequests.length}${visibleRequests.length < requests.length ? ` of ${requests.length}` : ""}):
11349
11940
  `));
11350
- printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"], requests.slice(0, 50).map((r) => [
11941
+ printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"], visibleRequests.map((r) => [
11351
11942
  chalk7.dim(String(r["timestamp"]).substring(11, 19)),
11352
11943
  chalk7.white(String(r["model"]).substring(0, 22)),
11353
11944
  fmtTokens2(r["input_tokens"]),
@@ -11356,12 +11947,13 @@ program.command("session <id>").description("Show detailed breakdown of a single
11356
11947
  fmtTokens2(r["cache_create_tokens"]),
11357
11948
  fmt4(r["cost_usd"])
11358
11949
  ]));
11359
- if (requests.length > 50)
11360
- console.log(chalk7.dim(` ... and ${requests.length - 50} more requests`));
11950
+ if (requests.length > visibleRequests.length) {
11951
+ console.log(chalk7.dim(` ... ${requests.length - visibleRequests.length} more requests hidden. Use --limit <n> or --verbose.`));
11952
+ }
11361
11953
  }
11362
11954
  console.log();
11363
11955
  });
11364
- program.command("machines").description("List all machines that have synced data").action(async () => {
11956
+ program.command("machines").description("List all machines that have synced data").option("--limit <n>", "Maximum machine rows to print (default: 20)").option("--verbose", "Show all machine rows").action(async (opts) => {
11365
11957
  await autoSync();
11366
11958
  const machines = await getStore().machines();
11367
11959
  const current = getMachineId();
@@ -11369,16 +11961,22 @@ program.command("machines").description("List all machines that have synced data
11369
11961
  console.log(chalk7.yellow(`No machine data yet. Current machine: ${current}`));
11370
11962
  return;
11371
11963
  }
11964
+ const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
11965
+ const visibleMachines = machines.slice(0, limit);
11372
11966
  console.log();
11373
11967
  console.log(chalk7.bold.cyan(" Machines"));
11968
+ if (visibleMachines.length < machines.length) {
11969
+ console.log(chalk7.dim(` ${machines.length} machines \xB7 showing ${visibleMachines.length}`));
11970
+ }
11374
11971
  console.log();
11375
- printTable(["Machine", "Sessions", "Requests", "Cost", "Last Active"], machines.map((m) => [
11972
+ printTable(["Machine", "Sessions", "Requests", "Cost", "Last Active"], visibleMachines.map((m) => [
11376
11973
  m.machine_id === current ? chalk7.green(`${m.machine_id} (this)`) : chalk7.white(m.machine_id),
11377
11974
  fmtCount(m.sessions),
11378
11975
  fmtCount(m.requests),
11379
11976
  fmt4(m.total_cost_usd),
11380
11977
  chalk7.dim(m.last_active?.substring(0, 16) ?? "\u2014")
11381
11978
  ]));
11979
+ printHiddenRowsHint(machines.length, visibleMachines.length, "Use --limit <n> or --verbose.");
11382
11980
  console.log(`
11383
11981
  ${chalk7.dim("Current machine:")} ${chalk7.bold(current)}`);
11384
11982
  console.log();
@@ -11540,14 +12138,20 @@ goalCmd.command("set").description("Set a spending goal").option("--period <peri
11540
12138
  });
11541
12139
  console.log(chalk7.green(`\u2713 Goal set: ${period} $${limitUsd}${opts.project ? ` (${opts.project})` : ""}`));
11542
12140
  });
11543
- goalCmd.command("list").description("List all goals with progress").action(async () => {
12141
+ goalCmd.command("list").description("List all goals with progress").option("--limit <n>", "Maximum goal rows to print (default: 20)").option("--verbose", "Show all goal rows").action(async (opts) => {
11544
12142
  const statuses = await getStore().listGoals();
11545
12143
  if (statuses.length === 0) {
11546
12144
  console.log(chalk7.yellow("No goals set."));
11547
12145
  return;
11548
12146
  }
12147
+ const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
12148
+ const visibleStatuses = statuses.slice(0, limit);
11549
12149
  console.log();
11550
- printTable(["Period", "Scope", "Limit", "Spent", "Used%", "Status"], statuses.map((g) => {
12150
+ if (visibleStatuses.length < statuses.length) {
12151
+ console.log(chalk7.dim(` ${statuses.length} goals \xB7 showing ${visibleStatuses.length}`));
12152
+ console.log();
12153
+ }
12154
+ printTable(["Period", "Scope", "Limit", "Spent", "Used%", "Status"], visibleStatuses.map((g) => {
11551
12155
  const pct = g.percent_used.toFixed(1);
11552
12156
  const scope = g.project_path ?? g.agent ?? "global";
11553
12157
  const status = g.is_over ? chalk7.red("OVER") : g.is_at_risk ? chalk7.yellow("AT RISK") : chalk7.green("ON TRACK");
@@ -11561,20 +12165,23 @@ goalCmd.command("list").description("List all goals with progress").action(async
11561
12165
  status
11562
12166
  ];
11563
12167
  }));
12168
+ printHiddenRowsHint(statuses.length, visibleStatuses.length, "Use --limit <n> or --verbose.");
11564
12169
  console.log();
11565
12170
  });
11566
12171
  goalCmd.command("remove <id>").description("Remove a goal").action(async (id) => {
11567
12172
  await getStore().removeGoal(id);
11568
12173
  console.log(chalk7.green(`\u2713 Goal removed`));
11569
12174
  });
11570
- goalCmd.command("status").description("Quick goal progress summary").action(async () => {
12175
+ goalCmd.command("status").description("Quick goal progress summary").option("--limit <n>", "Maximum goal rows to print (default: 20)").option("--verbose", "Show all goal rows").action(async (opts) => {
11571
12176
  const statuses = await getStore().listGoals();
11572
12177
  if (statuses.length === 0) {
11573
12178
  console.log(chalk7.yellow("No goals set."));
11574
12179
  return;
11575
12180
  }
12181
+ const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
12182
+ const visibleStatuses = statuses.slice(0, limit);
11576
12183
  console.log();
11577
- for (const g of statuses) {
12184
+ for (const g of visibleStatuses) {
11578
12185
  const scope = g.project_path ?? g.agent ?? "global";
11579
12186
  const pct = Math.min(g.percent_used, 100);
11580
12187
  const barFilled = Math.round(pct / 10);
@@ -11584,6 +12191,7 @@ goalCmd.command("status").description("Quick goal progress summary").action(asyn
11584
12191
  const label = `${g.period} (${scope})`.padEnd(20);
11585
12192
  console.log(` ${label} ${bar} ${fmt4(g.current_spend_usd)} / ${fmt4(g.limit_usd)} (${g.percent_used.toFixed(0)}%) ${statusStr}`);
11586
12193
  }
12194
+ printHiddenRowsHint(statuses.length, visibleStatuses.length, "Use --limit <n> or --verbose.");
11587
12195
  console.log();
11588
12196
  });
11589
12197
  program.command("remove <type> <id>").alias("rm").description("Remove a record. Type: budget | project | goal | pricing").action(async (type, id) => {