@hasna/economy 0.2.43 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dashboard/README.md +73 -0
  2. package/dist/cli/commands/brief.d.ts +4 -69
  3. package/dist/cli/commands/brief.d.ts.map +1 -1
  4. package/dist/cli/commands/extras.d.ts.map +1 -1
  5. package/dist/cli/commands/tui.d.ts +21 -5
  6. package/dist/cli/commands/tui.d.ts.map +1 -1
  7. package/dist/cli/commands/watch.d.ts.map +1 -1
  8. package/dist/cli/index.js +8227 -6395
  9. package/dist/db/database.d.ts +18 -0
  10. package/dist/db/database.d.ts.map +1 -1
  11. package/dist/db/pg-migrations.d.ts.map +1 -1
  12. package/dist/index.d.ts +1 -10
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1855 -1613
  15. package/dist/lib/analytics.d.ts +68 -0
  16. package/dist/lib/analytics.d.ts.map +1 -0
  17. package/dist/lib/brief.d.ts +72 -0
  18. package/dist/lib/brief.d.ts.map +1 -0
  19. package/dist/lib/cloud-storage.d.ts +48 -0
  20. package/dist/lib/cloud-storage.d.ts.map +1 -0
  21. package/dist/lib/contracts-client/index.d.ts +3 -0
  22. package/dist/lib/contracts-client/index.d.ts.map +1 -0
  23. package/dist/lib/contracts-client/mode.d.ts +20 -0
  24. package/dist/lib/contracts-client/mode.d.ts.map +1 -0
  25. package/dist/lib/contracts-client/storage.d.ts +83 -0
  26. package/dist/lib/contracts-client/storage.d.ts.map +1 -0
  27. package/dist/lib/contracts-client/transport.d.ts +148 -0
  28. package/dist/lib/contracts-client/transport.d.ts.map +1 -0
  29. package/dist/lib/pricing.d.ts +3 -0
  30. package/dist/lib/pricing.d.ts.map +1 -1
  31. package/dist/lib/store/index.d.ts +283 -0
  32. package/dist/lib/store/index.d.ts.map +1 -0
  33. package/dist/lib/sync-all.d.ts +0 -2
  34. package/dist/lib/sync-all.d.ts.map +1 -1
  35. package/dist/mcp/http.d.ts.map +1 -1
  36. package/dist/mcp/index.js +1934 -536
  37. package/dist/mcp/server.d.ts.map +1 -1
  38. package/dist/openapi.d.ts.map +1 -1
  39. package/dist/otel/index.js +21 -267
  40. package/dist/server/index.js +2794 -331
  41. package/dist/server/serve.d.ts.map +1 -1
  42. package/dist/types/index.d.ts +0 -1
  43. package/dist/types/index.d.ts.map +1 -1
  44. package/package.json +4 -2
  45. package/dashboard/dist/assets/charts-BtCbslx4.js +0 -36
  46. package/dashboard/dist/assets/index-BDmtt3RI.css +0 -1
  47. package/dashboard/dist/assets/index-BFwRLqg7.js +0 -6
  48. package/dashboard/dist/assets/vendor-Cz8jb743.js +0 -53
  49. package/dashboard/dist/index.html +0 -16
  50. package/dashboard/dist/logo.jpg +0 -0
  51. package/dashboard/dist/vite.svg +0 -1
@@ -21,8 +21,10 @@ var __require = import.meta.require;
21
21
  var exports_pricing = {};
22
22
  __export(exports_pricing, {
23
23
  normalizeModelName: () => normalizeModelName,
24
+ getPricingFromRows: () => getPricingFromRows,
24
25
  getPricingFromDb: () => getPricingFromDb,
25
26
  getPricing: () => getPricing,
27
+ estimateCostFromRows: () => estimateCostFromRows,
26
28
  ensurePricingSeeded: () => ensurePricingSeeded,
27
29
  computeCostFromDb: () => computeCostFromDb,
28
30
  computeCost: () => computeCost,
@@ -213,6 +215,25 @@ function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens
213
215
  return 0;
214
216
  return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
215
217
  }
218
+ function getPricingFromRows(rows, model) {
219
+ if (isFreeModel(model))
220
+ return FREE_PRICING;
221
+ for (const key of modelLookupKeys(model)) {
222
+ const row = rows.find((r) => r.model === key);
223
+ if (row)
224
+ return modelPricingFromDbRow(row);
225
+ }
226
+ const match = bestModelMatch(model, rows.map((r) => [r.model, r]));
227
+ if (!match)
228
+ return null;
229
+ return modelPricingFromDbRow(match);
230
+ }
231
+ function estimateCostFromRows(rows, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
232
+ const pricing = getPricingFromRows(rows, model) ?? getPricing(model);
233
+ if (!pricing)
234
+ return 0;
235
+ return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
236
+ }
216
237
  function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
217
238
  if (isFreeModel(model))
218
239
  return 0;
@@ -1495,6 +1516,9 @@ function getIngestState(db, source, key) {
1495
1516
  function setIngestState(db, source, key, value) {
1496
1517
  db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
1497
1518
  }
1519
+ function queryRequestsSince(db, since) {
1520
+ return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
1521
+ }
1498
1522
  function upsertBillingDaily(db, row) {
1499
1523
  db.prepare(`
1500
1524
  INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
@@ -1657,13 +1681,374 @@ function dedupeRequests(db) {
1657
1681
  }
1658
1682
  return removed;
1659
1683
  }
1684
+ function ingestSpecs() {
1685
+ return [
1686
+ {
1687
+ table: "requests",
1688
+ pk: ["id"],
1689
+ columns: [
1690
+ "id",
1691
+ "agent",
1692
+ "session_id",
1693
+ "model",
1694
+ "input_tokens",
1695
+ "output_tokens",
1696
+ "cache_read_tokens",
1697
+ "cache_create_tokens",
1698
+ "cache_create_5m_tokens",
1699
+ "cache_create_1h_tokens",
1700
+ "cost_usd",
1701
+ "cost_basis",
1702
+ "duration_ms",
1703
+ "timestamp",
1704
+ "source_request_id",
1705
+ "machine_id",
1706
+ "attribution_tag",
1707
+ "account_key",
1708
+ "account_tool",
1709
+ "account_name",
1710
+ "account_email",
1711
+ "account_source",
1712
+ "updated_at",
1713
+ "synced_at"
1714
+ ],
1715
+ map: (r, now) => [
1716
+ strOr(r["id"]),
1717
+ strOr(r["agent"]),
1718
+ strOr(r["session_id"]),
1719
+ strOr(r["model"]),
1720
+ numOr(r["input_tokens"]),
1721
+ numOr(r["output_tokens"]),
1722
+ numOr(r["cache_read_tokens"]),
1723
+ numOr(r["cache_create_tokens"]),
1724
+ numOr(r["cache_create_5m_tokens"] ?? r["cache_create_tokens"]),
1725
+ numOr(r["cache_create_1h_tokens"]),
1726
+ numOr(r["cost_usd"]),
1727
+ strOr(r["cost_basis"], "estimated"),
1728
+ numOr(r["duration_ms"]),
1729
+ strOr(r["timestamp"]),
1730
+ r["source_request_id"] == null ? null : strOr(r["source_request_id"]),
1731
+ strOr(r["machine_id"]),
1732
+ strOr(r["attribution_tag"]),
1733
+ strOr(r["account_key"]),
1734
+ strOr(r["account_tool"]),
1735
+ strOr(r["account_name"]),
1736
+ strOr(r["account_email"]),
1737
+ strOr(r["account_source"]),
1738
+ strOr(r["updated_at"], now),
1739
+ strOr(r["synced_at"])
1740
+ ]
1741
+ },
1742
+ {
1743
+ table: "sessions",
1744
+ pk: ["id"],
1745
+ columns: [
1746
+ "id",
1747
+ "agent",
1748
+ "project_path",
1749
+ "project_name",
1750
+ "started_at",
1751
+ "ended_at",
1752
+ "total_cost_usd",
1753
+ "total_tokens",
1754
+ "request_count",
1755
+ "machine_id",
1756
+ "attribution_tag",
1757
+ "account_key",
1758
+ "account_tool",
1759
+ "account_name",
1760
+ "account_email",
1761
+ "account_source",
1762
+ "updated_at",
1763
+ "synced_at"
1764
+ ],
1765
+ map: (r, now) => [
1766
+ strOr(r["id"]),
1767
+ strOr(r["agent"]),
1768
+ strOr(r["project_path"]),
1769
+ strOr(r["project_name"]),
1770
+ strOr(r["started_at"]),
1771
+ r["ended_at"] == null ? null : strOr(r["ended_at"]),
1772
+ numOr(r["total_cost_usd"]),
1773
+ numOr(r["total_tokens"]),
1774
+ numOr(r["request_count"]),
1775
+ strOr(r["machine_id"]),
1776
+ strOr(r["attribution_tag"]),
1777
+ strOr(r["account_key"]),
1778
+ strOr(r["account_tool"]),
1779
+ strOr(r["account_name"]),
1780
+ strOr(r["account_email"]),
1781
+ strOr(r["account_source"]),
1782
+ strOr(r["updated_at"], now),
1783
+ strOr(r["synced_at"])
1784
+ ]
1785
+ },
1786
+ {
1787
+ table: "projects",
1788
+ pk: ["id"],
1789
+ columns: ["id", "path", "name", "description", "tags", "created_at"],
1790
+ map: (r) => [
1791
+ strOr(r["id"]),
1792
+ strOr(r["path"]),
1793
+ strOr(r["name"]),
1794
+ r["description"] == null ? null : strOr(r["description"]),
1795
+ typeof r["tags"] === "string" ? r["tags"] : JSON.stringify(r["tags"] ?? []),
1796
+ strOr(r["created_at"])
1797
+ ]
1798
+ },
1799
+ {
1800
+ table: "budgets",
1801
+ pk: ["id"],
1802
+ columns: ["id", "project_path", "agent", "period", "limit_usd", "alert_at_percent", "created_at", "updated_at"],
1803
+ map: (r) => [
1804
+ strOr(r["id"]),
1805
+ r["project_path"] == null ? null : strOr(r["project_path"]),
1806
+ r["agent"] == null ? null : strOr(r["agent"]),
1807
+ strOr(r["period"]),
1808
+ numOr(r["limit_usd"]),
1809
+ numOr(r["alert_at_percent"]),
1810
+ strOr(r["created_at"]),
1811
+ strOr(r["updated_at"])
1812
+ ]
1813
+ },
1814
+ {
1815
+ table: "goals",
1816
+ pk: ["id"],
1817
+ columns: ["id", "period", "project_path", "agent", "limit_usd", "created_at", "updated_at"],
1818
+ map: (r) => [
1819
+ strOr(r["id"]),
1820
+ strOr(r["period"]),
1821
+ r["project_path"] == null ? null : strOr(r["project_path"]),
1822
+ r["agent"] == null ? null : strOr(r["agent"]),
1823
+ numOr(r["limit_usd"]),
1824
+ strOr(r["created_at"]),
1825
+ strOr(r["updated_at"])
1826
+ ]
1827
+ },
1828
+ {
1829
+ table: "billing_daily",
1830
+ pk: ["date", "provider", "description"],
1831
+ columns: ["date", "provider", "description", "cost_usd", "updated_at"],
1832
+ map: (r) => [strOr(r["date"]), strOr(r["provider"]), strOr(r["description"]), numOr(r["cost_usd"]), strOr(r["updated_at"])]
1833
+ },
1834
+ {
1835
+ table: "model_pricing",
1836
+ pk: ["model"],
1837
+ columns: [
1838
+ "model",
1839
+ "input_per_1m",
1840
+ "output_per_1m",
1841
+ "cache_read_per_1m",
1842
+ "cache_write_per_1m",
1843
+ "cache_write_1h_per_1m",
1844
+ "cache_storage_per_1m_hour",
1845
+ "updated_at"
1846
+ ],
1847
+ map: (r) => [
1848
+ strOr(r["model"]),
1849
+ numOr(r["input_per_1m"]),
1850
+ numOr(r["output_per_1m"]),
1851
+ numOr(r["cache_read_per_1m"]),
1852
+ numOr(r["cache_write_per_1m"]),
1853
+ numOr(r["cache_write_1h_per_1m"]),
1854
+ numOr(r["cache_storage_per_1m_hour"]),
1855
+ strOr(r["updated_at"])
1856
+ ]
1857
+ },
1858
+ {
1859
+ table: "subscriptions",
1860
+ pk: ["id"],
1861
+ columns: [
1862
+ "id",
1863
+ "agent",
1864
+ "provider",
1865
+ "plan",
1866
+ "monthly_fee_usd",
1867
+ "included_usage_usd",
1868
+ "billing_cycle_start",
1869
+ "reset_policy",
1870
+ "active",
1871
+ "created_at",
1872
+ "updated_at"
1873
+ ],
1874
+ map: (r) => [
1875
+ strOr(r["id"]),
1876
+ r["agent"] == null ? null : strOr(r["agent"]),
1877
+ strOr(r["provider"]),
1878
+ strOr(r["plan"]),
1879
+ numOr(r["monthly_fee_usd"]),
1880
+ numOr(r["included_usage_usd"]),
1881
+ r["billing_cycle_start"] == null ? null : strOr(r["billing_cycle_start"]),
1882
+ strOr(r["reset_policy"], "monthly"),
1883
+ numOr(r["active"], 1),
1884
+ strOr(r["created_at"]),
1885
+ strOr(r["updated_at"])
1886
+ ]
1887
+ },
1888
+ {
1889
+ table: "usage_snapshots",
1890
+ pk: ["id"],
1891
+ columns: ["id", "agent", "date", "metric", "value", "unit", "machine_id", "updated_at"],
1892
+ map: (r, now) => [
1893
+ strOr(r["id"], `${strOr(r["agent"])}-${strOr(r["date"])}-${strOr(r["metric"])}-${strOr(r["machine_id"])}`),
1894
+ strOr(r["agent"]),
1895
+ strOr(r["date"]),
1896
+ strOr(r["metric"]),
1897
+ numOr(r["value"]),
1898
+ strOr(r["unit"]),
1899
+ strOr(r["machine_id"]),
1900
+ strOr(r["updated_at"], now)
1901
+ ]
1902
+ }
1903
+ ];
1904
+ }
1905
+ function bulkIngest(db, body) {
1906
+ const now = new Date().toISOString();
1907
+ const ingested = {};
1908
+ for (const spec of ingestSpecs()) {
1909
+ const raw = body[spec.table];
1910
+ if (!Array.isArray(raw) || raw.length === 0)
1911
+ continue;
1912
+ const cols = spec.columns;
1913
+ const pkIdx = spec.pk.map((k) => cols.indexOf(k));
1914
+ const deduped = new Map;
1915
+ for (const row of raw) {
1916
+ const values = spec.map(row, now);
1917
+ const key = pkIdx.map((i) => String(values[i])).join("\x00");
1918
+ deduped.set(key, values);
1919
+ }
1920
+ const rows = [...deduped.values()];
1921
+ const updateCols = cols.filter((c) => !spec.pk.includes(c));
1922
+ const conflictAction = updateCols.length ? `DO UPDATE SET ${updateCols.map((c) => `"${c}" = excluded."${c}"`).join(", ")}` : "DO NOTHING";
1923
+ const colList = cols.map((c) => `"${c}"`).join(", ");
1924
+ const conflict = spec.pk.map((c) => `"${c}"`).join(", ");
1925
+ const chunkRows = Math.max(1, Math.floor(20000 / cols.length));
1926
+ const rowPlaceholder = `(${cols.map(() => "?").join(", ")})`;
1927
+ for (let i = 0;i < rows.length; i += chunkRows) {
1928
+ const chunk = rows.slice(i, i + chunkRows);
1929
+ const valuesSql = chunk.map(() => rowPlaceholder).join(", ");
1930
+ const sql = `INSERT INTO ${spec.table} (${colList}) VALUES ${valuesSql} ON CONFLICT (${conflict}) ${conflictAction}`;
1931
+ const params = [];
1932
+ for (const values of chunk)
1933
+ params.push(...values);
1934
+ db.prepare(sql).run(...params);
1935
+ }
1936
+ ingested[spec.table] = rows.length;
1937
+ }
1938
+ const total = Object.values(ingested).reduce((a, b) => a + b, 0);
1939
+ return { ingested, total };
1940
+ }
1941
+ var numOr = (v, d = 0) => {
1942
+ if (v == null)
1943
+ return d;
1944
+ const num = Number(v);
1945
+ return Number.isFinite(num) ? num : d;
1946
+ }, strOr = (v, d = "") => v == null ? d : String(v);
1660
1947
  var init_database = () => {};
1661
1948
 
1949
+ // src/db/sync-pg.ts
1950
+ import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
1951
+ import { fileURLToPath } from "url";
1952
+ import { dirname, join as join9 } from "path";
1953
+ import { existsSync as existsSync10 } from "fs";
1954
+ import { translateSql, translateParams } from "@hasna/cloud";
1955
+ function translateSqliteDates(sql) {
1956
+ let s = sql;
1957
+ const isoDate = (expr) => `to_char(${expr}, 'YYYY-MM-DD')`;
1958
+ s = s.replace(/DATE\('now',\s*'weekday 0',\s*'-7 days'\)/gi, isoDate("(date_trunc('week', now() + interval '1 day') - interval '1 day')"));
1959
+ s = s.replace(/DATE\('now',\s*'start of month'\)/gi, isoDate("date_trunc('month', now())"));
1960
+ s = s.replace(/DATE\('now',\s*'start of year'\)/gi, isoDate("date_trunc('year', now())"));
1961
+ s = s.replace(/DATE\('now',\s*'(-?\d+)\s*days?'\)/gi, (_m, n) => isoDate(`now() + interval '${n} days'`));
1962
+ s = s.replace(/DATE\('now',\s*(\$\d+)\s*\|\|\s*' days'\)/gi, (_m, p) => isoDate(`now() + ((${p}) || ' days')::interval`));
1963
+ s = s.replace(/DATETIME\('now',\s*(\$\d+)\)/gi, (_m, p) => `(now() + (${p})::interval)`);
1964
+ s = s.replace(/DATETIME\('now',\s*'([^']+)'\)/gi, (_m, lit) => `(now() + interval '${lit}')`);
1965
+ const strftimeMap = { "%H": "HH24", "%Y": "YYYY", "%m": "MM", "%d": "DD", "%M": "MI", "%S": "SS" };
1966
+ s = s.replace(/STRFTIME\('([^']+)',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, fmt, col) => {
1967
+ const pgFmt = Object.entries(strftimeMap).reduce((acc, [k, v]) => acc.split(k).join(v), fmt);
1968
+ return `to_char(CAST("${col}" AS timestamptz), '${pgFmt}')`;
1969
+ });
1970
+ s = s.replace(/\bDATE\('now'\)/gi, isoDate("now()"));
1971
+ s = s.replace(/\bDATETIME\('now'\)/gi, "now()");
1972
+ s = s.replace(/\bDATETIME\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `CAST("${col}" AS timestamptz)`);
1973
+ s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `substr("${col}", 1, 10)`);
1974
+ return s;
1975
+ }
1976
+ function resolveWorkerPath() {
1977
+ const here = dirname(fileURLToPath(import.meta.url));
1978
+ const candidates = [
1979
+ join9(here, "pg-sync-worker.ts"),
1980
+ join9(here, "pg-sync-worker.js"),
1981
+ join9(here, "..", "db", "pg-sync-worker.js")
1982
+ ];
1983
+ for (const candidate of candidates)
1984
+ if (existsSync10(candidate))
1985
+ return candidate;
1986
+ return candidates[0];
1987
+ }
1988
+
1989
+ class SyncPgAdapter {
1990
+ worker;
1991
+ port;
1992
+ counter = 0;
1993
+ constructor(dsn) {
1994
+ const workerPath = resolveWorkerPath();
1995
+ this.worker = new Worker(workerPath, { workerData: { dsn } });
1996
+ this.worker.on("error", (err) => console.error(JSON.stringify({ evt: "pg_worker_error", message: err.message })));
1997
+ const channel = new MessageChannel;
1998
+ this.port = channel.port1;
1999
+ this.worker.postMessage({ type: "init", port: channel.port2 }, [channel.port2]);
2000
+ }
2001
+ query(sql, params) {
2002
+ const pgSql = translateSqliteDates(translateSql(sql, "pg"));
2003
+ const pgParams = translateParams(params);
2004
+ const id = ++this.counter;
2005
+ const signal = new SharedArrayBuffer(4);
2006
+ const sig = new Int32Array(signal);
2007
+ Atomics.store(sig, 0, 0);
2008
+ this.worker.postMessage({ id, sql: pgSql, params: pgParams, signal });
2009
+ Atomics.wait(sig, 0, 0);
2010
+ let received = receiveMessageOnPort(this.port);
2011
+ for (let attempt = 0;!received && attempt < 1e6; attempt++)
2012
+ received = receiveMessageOnPort(this.port);
2013
+ if (!received)
2014
+ throw new Error("SyncPgAdapter: no reply from worker");
2015
+ const reply = received.message;
2016
+ if (!reply.ok)
2017
+ throw new Error(reply.error);
2018
+ return { rows: reply.rows, rowCount: reply.rowCount };
2019
+ }
2020
+ run(sql, ...params) {
2021
+ const result = this.query(sql, params);
2022
+ return { changes: result.rowCount, lastInsertRowid: result.rows?.[0]?.["id"] ?? 0 };
2023
+ }
2024
+ get(sql, ...params) {
2025
+ return this.query(sql, params).rows[0] ?? null;
2026
+ }
2027
+ all(sql, ...params) {
2028
+ return this.query(sql, params).rows;
2029
+ }
2030
+ exec(sql) {
2031
+ this.query(sql, []);
2032
+ }
2033
+ prepare(sql) {
2034
+ const self = this;
2035
+ return {
2036
+ run: (...params) => self.run(sql, ...params),
2037
+ get: (...params) => self.get(sql, ...params),
2038
+ all: (...params) => self.all(sql, ...params),
2039
+ finalize: () => {}
2040
+ };
2041
+ }
2042
+ transaction(fn) {
2043
+ return fn();
2044
+ }
2045
+ close() {
2046
+ this.worker.terminate();
2047
+ }
2048
+ }
2049
+ var init_sync_pg = () => {};
2050
+
1662
2051
  // src/db/pg-migrations.ts
1663
- var exports_pg_migrations = {};
1664
- __export(exports_pg_migrations, {
1665
- PG_MIGRATIONS: () => PG_MIGRATIONS
1666
- });
1667
2052
  var PG_MIGRATIONS;
1668
2053
  var init_pg_migrations = __esm(() => {
1669
2054
  PG_MIGRATIONS = [
@@ -1843,125 +2228,51 @@ var init_pg_migrations = __esm(() => {
1843
2228
  `CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
1844
2229
  `CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`,
1845
2230
  `CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key)`,
1846
- `CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`
2231
+ `CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`,
2232
+ `ALTER TABLE requests ALTER COLUMN input_tokens TYPE BIGINT`,
2233
+ `ALTER TABLE requests ALTER COLUMN output_tokens TYPE BIGINT`,
2234
+ `ALTER TABLE requests ALTER COLUMN cache_read_tokens TYPE BIGINT`,
2235
+ `ALTER TABLE requests ALTER COLUMN cache_create_tokens TYPE BIGINT`,
2236
+ `ALTER TABLE requests ALTER COLUMN cache_create_5m_tokens TYPE BIGINT`,
2237
+ `ALTER TABLE requests ALTER COLUMN cache_create_1h_tokens TYPE BIGINT`,
2238
+ `ALTER TABLE requests ALTER COLUMN duration_ms TYPE BIGINT`,
2239
+ `ALTER TABLE sessions ALTER COLUMN total_tokens TYPE BIGINT`,
2240
+ `ALTER TABLE sessions ALTER COLUMN request_count TYPE BIGINT`,
2241
+ `ALTER TABLE requests ALTER COLUMN cost_usd TYPE DOUBLE PRECISION`,
2242
+ `ALTER TABLE sessions ALTER COLUMN total_cost_usd TYPE DOUBLE PRECISION`,
2243
+ `ALTER TABLE budgets ALTER COLUMN limit_usd TYPE DOUBLE PRECISION`,
2244
+ `ALTER TABLE goals ALTER COLUMN limit_usd TYPE DOUBLE PRECISION`,
2245
+ `ALTER TABLE model_pricing ALTER COLUMN input_per_1m TYPE DOUBLE PRECISION`,
2246
+ `ALTER TABLE model_pricing ALTER COLUMN output_per_1m TYPE DOUBLE PRECISION`,
2247
+ `ALTER TABLE model_pricing ALTER COLUMN cache_read_per_1m TYPE DOUBLE PRECISION`,
2248
+ `ALTER TABLE model_pricing ALTER COLUMN cache_write_per_1m TYPE DOUBLE PRECISION`,
2249
+ `ALTER TABLE model_pricing ALTER COLUMN cache_write_1h_per_1m TYPE DOUBLE PRECISION`,
2250
+ `ALTER TABLE model_pricing ALTER COLUMN cache_storage_per_1m_hour TYPE DOUBLE PRECISION`,
2251
+ `ALTER TABLE billing_daily ALTER COLUMN cost_usd TYPE DOUBLE PRECISION`,
2252
+ `ALTER TABLE subscriptions ALTER COLUMN monthly_fee_usd TYPE DOUBLE PRECISION`,
2253
+ `ALTER TABLE subscriptions ALTER COLUMN included_usage_usd TYPE DOUBLE PRECISION`,
2254
+ `ALTER TABLE usage_snapshots ALTER COLUMN value TYPE DOUBLE PRECISION`,
2255
+ `ALTER TABLE savings_daily ALTER COLUMN api_equivalent_usd TYPE DOUBLE PRECISION`,
2256
+ `ALTER TABLE savings_daily ALTER COLUMN subscription_fee_usd TYPE DOUBLE PRECISION`,
2257
+ `ALTER TABLE savings_daily ALTER COLUMN included_consumed_usd TYPE DOUBLE PRECISION`,
2258
+ `ALTER TABLE savings_daily ALTER COLUMN on_demand_usd TYPE DOUBLE PRECISION`,
2259
+ `ALTER TABLE savings_daily ALTER COLUMN saved_usd TYPE DOUBLE PRECISION`
1847
2260
  ];
1848
2261
  });
1849
2262
 
1850
- // src/db/sync-pg.ts
1851
- import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
1852
- import { fileURLToPath } from "url";
1853
- import { dirname, join as join9 } from "path";
1854
- import { existsSync as existsSync10 } from "fs";
1855
- import { translateSql, translateParams } from "@hasna/cloud";
1856
- function translateSqliteDates(sql) {
1857
- let s = sql;
1858
- const isoDate = (expr) => `to_char(${expr}, 'YYYY-MM-DD')`;
1859
- s = s.replace(/DATE\('now',\s*'weekday 0',\s*'-7 days'\)/gi, isoDate("(date_trunc('week', now() + interval '1 day') - interval '1 day')"));
1860
- s = s.replace(/DATE\('now',\s*'start of month'\)/gi, isoDate("date_trunc('month', now())"));
1861
- s = s.replace(/DATE\('now',\s*'start of year'\)/gi, isoDate("date_trunc('year', now())"));
1862
- s = s.replace(/DATE\('now',\s*'(-?\d+)\s*days?'\)/gi, (_m, n) => isoDate(`now() + interval '${n} days'`));
1863
- s = s.replace(/DATE\('now',\s*(\$\d+)\s*\|\|\s*' days'\)/gi, (_m, p) => isoDate(`now() + ((${p}) || ' days')::interval`));
1864
- s = s.replace(/DATETIME\('now',\s*(\$\d+)\)/gi, (_m, p) => `(now() + (${p})::interval)`);
1865
- s = s.replace(/DATETIME\('now',\s*'([^']+)'\)/gi, (_m, lit) => `(now() + interval '${lit}')`);
1866
- const strftimeMap = { "%H": "HH24", "%Y": "YYYY", "%m": "MM", "%d": "DD", "%M": "MI", "%S": "SS" };
1867
- s = s.replace(/STRFTIME\('([^']+)',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, fmt, col) => {
1868
- const pgFmt = Object.entries(strftimeMap).reduce((acc, [k, v]) => acc.split(k).join(v), fmt);
1869
- return `to_char(CAST("${col}" AS timestamptz), '${pgFmt}')`;
1870
- });
1871
- s = s.replace(/\bDATE\('now'\)/gi, isoDate("now()"));
1872
- s = s.replace(/\bDATETIME\('now'\)/gi, "now()");
1873
- s = s.replace(/\bDATETIME\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `CAST("${col}" AS timestamptz)`);
1874
- s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `substr("${col}", 1, 10)`);
1875
- return s;
1876
- }
1877
- function resolveWorkerPath() {
1878
- const here = dirname(fileURLToPath(import.meta.url));
1879
- const candidates = [
1880
- join9(here, "pg-sync-worker.ts"),
1881
- join9(here, "pg-sync-worker.js"),
1882
- join9(here, "..", "db", "pg-sync-worker.js")
1883
- ];
1884
- for (const candidate of candidates)
1885
- if (existsSync10(candidate))
1886
- return candidate;
1887
- return candidates[0];
1888
- }
1889
-
1890
- class SyncPgAdapter {
1891
- worker;
1892
- port;
1893
- counter = 0;
1894
- constructor(dsn) {
1895
- const workerPath = resolveWorkerPath();
1896
- this.worker = new Worker(workerPath, { workerData: { dsn } });
1897
- this.worker.on("error", (err) => console.error(JSON.stringify({ evt: "pg_worker_error", message: err.message })));
1898
- const channel = new MessageChannel;
1899
- this.port = channel.port1;
1900
- this.worker.postMessage({ type: "init", port: channel.port2 }, [channel.port2]);
1901
- }
1902
- query(sql, params) {
1903
- const pgSql = translateSqliteDates(translateSql(sql, "pg"));
1904
- const pgParams = translateParams(params);
1905
- const id = ++this.counter;
1906
- const signal = new SharedArrayBuffer(4);
1907
- const sig = new Int32Array(signal);
1908
- Atomics.store(sig, 0, 0);
1909
- this.worker.postMessage({ id, sql: pgSql, params: pgParams, signal });
1910
- Atomics.wait(sig, 0, 0);
1911
- let received = receiveMessageOnPort(this.port);
1912
- for (let attempt = 0;!received && attempt < 1e6; attempt++)
1913
- received = receiveMessageOnPort(this.port);
1914
- if (!received)
1915
- throw new Error("SyncPgAdapter: no reply from worker");
1916
- const reply = received.message;
1917
- if (!reply.ok)
1918
- throw new Error(reply.error);
1919
- return { rows: reply.rows, rowCount: reply.rowCount };
1920
- }
1921
- run(sql, ...params) {
1922
- const result = this.query(sql, params);
1923
- return { changes: result.rowCount, lastInsertRowid: result.rows?.[0]?.["id"] ?? 0 };
1924
- }
1925
- get(sql, ...params) {
1926
- return this.query(sql, params).rows[0] ?? null;
1927
- }
1928
- all(sql, ...params) {
1929
- return this.query(sql, params).rows;
1930
- }
1931
- exec(sql) {
1932
- this.query(sql, []);
1933
- }
1934
- prepare(sql) {
1935
- const self = this;
1936
- return {
1937
- run: (...params) => self.run(sql, ...params),
1938
- get: (...params) => self.get(sql, ...params),
1939
- all: (...params) => self.all(sql, ...params),
1940
- finalize: () => {}
1941
- };
1942
- }
1943
- transaction(fn) {
1944
- return fn();
1945
- }
1946
- close() {
1947
- this.worker.terminate();
1948
- }
1949
- }
1950
- var init_sync_pg = () => {};
1951
-
1952
2263
  // src/db/cloud.ts
1953
2264
  var exports_cloud = {};
1954
2265
  __export(exports_cloud, {
1955
2266
  resolveSigningSecret: () => resolveSigningSecret,
1956
2267
  openCloudDatabase: () => openCloudDatabase,
1957
2268
  isCloudMode: () => isCloudMode,
1958
- getCloudDatabaseUrl: () => getCloudDatabaseUrl2,
2269
+ getCloudDatabaseUrl: () => getCloudDatabaseUrl,
1959
2270
  createCloudPool: () => createCloudPool,
1960
2271
  cloudMigrations: () => cloudMigrations,
1961
2272
  authClientFromPool: () => authClientFromPool
1962
2273
  });
1963
2274
  import pg from "pg";
1964
- function getCloudDatabaseUrl2() {
2275
+ function getCloudDatabaseUrl() {
1965
2276
  return process.env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || process.env["ECONOMY_DATABASE_URL"]?.trim() || process.env["DATABASE_URL"]?.trim() || undefined;
1966
2277
  }
1967
2278
  function isCloudMode() {
@@ -1970,18 +2281,18 @@ function isCloudMode() {
1970
2281
  return true;
1971
2282
  if (mode === "local")
1972
2283
  return false;
1973
- return Boolean(getCloudDatabaseUrl2());
2284
+ return Boolean(getCloudDatabaseUrl());
1974
2285
  }
1975
2286
  function resolveSigningSecret() {
1976
2287
  return process.env["HASNA_ECONOMY_API_SIGNING_KEY"]?.trim() || process.env["HASNA_API_SIGNING_KEY"]?.trim() || process.env["API_KEY_SIGNING_SECRET"]?.trim() || undefined;
1977
2288
  }
1978
- function openCloudDatabase(dsn = getCloudDatabaseUrl2()) {
2289
+ function openCloudDatabase(dsn = getCloudDatabaseUrl()) {
1979
2290
  if (!dsn) {
1980
2291
  throw new Error("economy cloud mode requires a Postgres DSN: set HASNA_ECONOMY_DATABASE_URL (or ECONOMY_DATABASE_URL / DATABASE_URL)");
1981
2292
  }
1982
2293
  return new SyncPgAdapter(dsn);
1983
2294
  }
1984
- function createCloudPool(dsn = getCloudDatabaseUrl2()) {
2295
+ function createCloudPool(dsn = getCloudDatabaseUrl()) {
1985
2296
  if (!dsn)
1986
2297
  throw new Error("economy cloud mode requires a Postgres DSN (HASNA_ECONOMY_DATABASE_URL)");
1987
2298
  const ssl = dsn.includes("sslmode=require") || dsn.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
@@ -4071,157 +4382,10 @@ async function ingestCodexQuota(db, verbose = false) {
4071
4382
 
4072
4383
  // src/lib/sync-all.ts
4073
4384
  init_database();
4074
-
4075
- // src/lib/cloud-sync.ts
4076
- init_database();
4077
-
4078
- // src/lib/package-metadata.ts
4079
- import { readFileSync as readFileSync8 } from "fs";
4080
- var cachedMetadata = null;
4081
- function getPackageMetadata() {
4082
- if (cachedMetadata)
4083
- return cachedMetadata;
4084
- const raw = readFileSync8(new URL("../../package.json", import.meta.url), "utf8");
4085
- const parsed = JSON.parse(raw);
4086
- cachedMetadata = {
4087
- name: parsed.name ?? "@hasna/economy",
4088
- version: parsed.version ?? "0.0.0"
4089
- };
4090
- return cachedMetadata;
4091
- }
4092
- var packageMetadata = getPackageMetadata();
4093
-
4094
- // src/lib/cloud-sync.ts
4095
- var CLOUD_TABLES = [
4096
- "requests",
4097
- "sessions",
4098
- "projects",
4099
- "budgets",
4100
- "goals",
4101
- "model_pricing",
4102
- "billing_daily",
4103
- "subscriptions",
4104
- "usage_snapshots",
4105
- "savings_daily",
4106
- "machines",
4107
- "ingest_state"
4108
- ];
4109
- function getCloudDatabaseUrl() {
4110
- return process.env["ECONOMY_CLOUD_DATABASE_URL"] ?? process.env["HASNA_ECONOMY_CLOUD_DATABASE_URL"] ?? null;
4111
- }
4112
- function isCloudAutoEnabled() {
4113
- return process.env["ECONOMY_CLOUD_AUTO"] === "1" || process.env["ECONOMY_CLOUD_AUTO"] === "true";
4114
- }
4115
- function getCloudPullIntervalMinutes() {
4116
- const raw = process.env["ECONOMY_CLOUD_PULL_INTERVAL"];
4117
- if (!raw)
4118
- return 15;
4119
- const n = Number(raw);
4120
- return Number.isFinite(n) && n > 0 ? n : 15;
4121
- }
4122
- async function getCloudPg() {
4123
- const url = getCloudDatabaseUrl();
4124
- if (!url) {
4125
- throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
4126
- }
4127
- const { PgAdapterAsync } = await import("@hasna/cloud");
4128
- return new PgAdapterAsync(url);
4129
- }
4130
- async function runCloudMigrations(cloud) {
4131
- const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
4132
- for (const sql of PG_MIGRATIONS2) {
4133
- await cloud.run(sql);
4134
- }
4135
- }
4136
- async function cloudPush(opts) {
4137
- const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
4138
- const cloud = await getCloudPg();
4139
- const local = new SqliteAdapter(getDbPath());
4140
- await runCloudMigrations(cloud);
4141
- const tables = opts?.tables ?? [...CLOUD_TABLES];
4142
- const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
4143
- const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
4144
- touchMachineRegistry(local, "push");
4145
- local.close();
4146
- await cloud.close();
4147
- return { rows, machine: getMachineId() };
4148
- }
4149
- async function cloudPull(opts) {
4150
- const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
4151
- const cloud = await getCloudPg();
4152
- const local = new SqliteAdapter(getDbPath());
4153
- await runCloudMigrations(cloud);
4154
- const tables = opts?.tables ?? [...CLOUD_TABLES];
4155
- const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
4156
- const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
4157
- touchMachineRegistry(local, "pull");
4158
- local.close();
4159
- await cloud.close();
4160
- setLastCloudPull();
4161
- return { rows, machine: getMachineId() };
4162
- }
4163
- function setLastCloudPull(at = new Date().toISOString()) {
4164
- const db = openDatabase();
4165
- db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
4166
- }
4167
- function getLastCloudPull() {
4168
- const db = openDatabase();
4169
- const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
4170
- return row?.value ?? null;
4171
- }
4172
- function shouldPullFromCloud() {
4173
- if (!getCloudDatabaseUrl())
4174
- return false;
4175
- const last = getLastCloudPull();
4176
- if (!last)
4177
- return true;
4178
- const ageMs = Date.now() - new Date(last).getTime();
4179
- return ageMs > getCloudPullIntervalMinutes() * 60000;
4180
- }
4181
- async function maybePullFromCloud() {
4182
- if (!shouldPullFromCloud())
4183
- return false;
4184
- try {
4185
- await cloudPull();
4186
- return true;
4187
- } catch {
4188
- return false;
4189
- }
4190
- }
4191
- async function maybePushAfterIngest() {
4192
- if (!isCloudAutoEnabled() || !getCloudDatabaseUrl())
4193
- return false;
4194
- try {
4195
- await cloudPush();
4196
- return true;
4197
- } catch {
4198
- return false;
4199
- }
4200
- }
4201
- function touchMachineRegistry(db, direction) {
4202
- const now = new Date().toISOString();
4203
- const machine = getMachineId();
4204
- db.prepare(`
4205
- INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
4206
- VALUES (?, ?, ?, ?, ?, ?, ?)
4207
- ON CONFLICT(machine_id) DO UPDATE SET
4208
- hostname = excluded.hostname,
4209
- last_seen_at = excluded.last_seen_at,
4210
- last_push_at = CASE WHEN ? = 'push' THEN excluded.last_push_at ELSE machines.last_push_at END,
4211
- last_pull_at = CASE WHEN ? = 'pull' THEN excluded.last_pull_at ELSE machines.last_pull_at END,
4212
- economy_version = excluded.economy_version,
4213
- updated_at = excluded.updated_at
4214
- `).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
4215
- }
4216
-
4217
- // src/lib/sync-all.ts
4218
4385
  async function syncAll(db, opts = {}) {
4219
4386
  const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
4220
4387
  const all = !anySpecific;
4221
- const useCloud = opts.cloud !== false;
4222
- if (useCloud)
4223
- await maybePullFromCloud();
4224
- const result = { deduped: 0, cloudPulled: false, cloudPushed: false };
4388
+ const result = { deduped: 0 };
4225
4389
  if (all || opts.claude) {
4226
4390
  result.claude = await ingestClaude(db, opts.verbose);
4227
4391
  result.claudeQuota = await ingestClaudeQuota(db, opts.verbose);
@@ -4244,11 +4408,729 @@ async function syncAll(db, opts = {}) {
4244
4408
  result.hermes = await ingestHermes(db, opts.verbose);
4245
4409
  if (opts.dedupe !== false)
4246
4410
  result.deduped = dedupeRequests(db);
4247
- if (useCloud)
4248
- result.cloudPushed = await maybePushAfterIngest();
4249
4411
  return result;
4250
4412
  }
4251
4413
 
4414
+ // src/lib/brief.ts
4415
+ init_database();
4416
+ var ZERO_REQUEST_TOTALS = {
4417
+ sessions: 0,
4418
+ request_sessions: 0,
4419
+ requests: 0,
4420
+ input_tokens: 0,
4421
+ output_tokens: 0,
4422
+ cache_read_tokens: 0,
4423
+ cache_create_tokens: 0,
4424
+ cache_create_5m_tokens: 0,
4425
+ cache_create_1h_tokens: 0,
4426
+ total_tokens: 0,
4427
+ cost_usd: 0,
4428
+ last_active: null
4429
+ };
4430
+ var ZERO_SESSION_TOTALS = {
4431
+ sessions: 0,
4432
+ requests: 0,
4433
+ total_tokens: 0,
4434
+ cost_usd: 0,
4435
+ last_active: null
4436
+ };
4437
+ function formatAge(timestamp, now) {
4438
+ if (!timestamp)
4439
+ return "never";
4440
+ const t = new Date(timestamp).getTime();
4441
+ if (!Number.isFinite(t))
4442
+ return "unknown";
4443
+ const ageMs = Math.max(0, now.getTime() - t);
4444
+ const minutes = Math.floor(ageMs / 60000);
4445
+ if (minutes < 1)
4446
+ return "<1m";
4447
+ if (minutes < 60)
4448
+ return `${minutes}m`;
4449
+ const hours = Math.floor(minutes / 60);
4450
+ if (hours < 48)
4451
+ return `${hours}h`;
4452
+ return `${Math.floor(hours / 24)}d`;
4453
+ }
4454
+ function machineFilter(machine) {
4455
+ if (!machine || machine === "all")
4456
+ return;
4457
+ return machine;
4458
+ }
4459
+ function requestPeriodWhere2(period) {
4460
+ if (period === "today")
4461
+ return `DATE(timestamp) = DATE('now')`;
4462
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
4463
+ }
4464
+ function sessionPeriodWhere2(period) {
4465
+ if (period === "today")
4466
+ return `DATE(started_at) = DATE('now')`;
4467
+ return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
4468
+ }
4469
+ function baseRequestTotals(row) {
4470
+ return {
4471
+ ...ZERO_REQUEST_TOTALS,
4472
+ ...row,
4473
+ sessions: Number(row?.request_sessions ?? row?.sessions ?? 0),
4474
+ request_sessions: Number(row?.request_sessions ?? row?.sessions ?? 0),
4475
+ requests: Number(row?.requests ?? 0),
4476
+ input_tokens: Number(row?.input_tokens ?? 0),
4477
+ output_tokens: Number(row?.output_tokens ?? 0),
4478
+ cache_read_tokens: Number(row?.cache_read_tokens ?? 0),
4479
+ cache_create_tokens: Number(row?.cache_create_tokens ?? 0),
4480
+ cache_create_5m_tokens: Number(row?.cache_create_5m_tokens ?? 0),
4481
+ cache_create_1h_tokens: Number(row?.cache_create_1h_tokens ?? 0),
4482
+ total_tokens: Number(row?.total_tokens ?? 0),
4483
+ cost_usd: Number(row?.cost_usd ?? 0),
4484
+ last_active: row?.last_active ?? null
4485
+ };
4486
+ }
4487
+ function baseSessionTotals(row) {
4488
+ return {
4489
+ sessions: Number(row?.sessions ?? 0),
4490
+ requests: Number(row?.requests ?? 0),
4491
+ total_tokens: Number(row?.total_tokens ?? 0),
4492
+ cost_usd: Number(row?.cost_usd ?? 0),
4493
+ last_active: row?.last_active ?? null
4494
+ };
4495
+ }
4496
+ function latestTimestamp(...values) {
4497
+ return values.filter(Boolean).sort().at(-1) ?? null;
4498
+ }
4499
+ function combineTotals(requests, sessions) {
4500
+ return {
4501
+ sessions: requests.request_sessions + sessions.sessions,
4502
+ requests: requests.requests + sessions.requests,
4503
+ input_tokens: requests.input_tokens,
4504
+ output_tokens: requests.output_tokens,
4505
+ cache_read_tokens: requests.cache_read_tokens,
4506
+ cache_create_tokens: requests.cache_create_tokens,
4507
+ cache_create_5m_tokens: requests.cache_create_5m_tokens,
4508
+ cache_create_1h_tokens: requests.cache_create_1h_tokens,
4509
+ total_tokens: requests.total_tokens + sessions.total_tokens,
4510
+ cost_usd: requests.cost_usd + sessions.cost_usd,
4511
+ last_active: latestTimestamp(requests.last_active, sessions.last_active)
4512
+ };
4513
+ }
4514
+ function emptyTotals() {
4515
+ return combineTotals(ZERO_REQUEST_TOTALS, ZERO_SESSION_TOTALS);
4516
+ }
4517
+ function requestTotalsSql(where, machine) {
4518
+ const params = [];
4519
+ let machineClause = "";
4520
+ if (machine) {
4521
+ machineClause = " AND machine_id = ?";
4522
+ params.push(machine);
4523
+ }
4524
+ return {
4525
+ sql: `
4526
+ SELECT
4527
+ COUNT(DISTINCT session_id) as request_sessions,
4528
+ COUNT(*) as requests,
4529
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
4530
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
4531
+ COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
4532
+ COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
4533
+ COALESCE(SUM(COALESCE(cache_create_5m_tokens, cache_create_tokens)), 0) as cache_create_5m_tokens,
4534
+ COALESCE(SUM(COALESCE(cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
4535
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
4536
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
4537
+ MAX(timestamp) as last_active
4538
+ FROM requests
4539
+ WHERE ${where}${machineClause}
4540
+ `,
4541
+ params
4542
+ };
4543
+ }
4544
+ function sessionOnlyTotalsSql(where, machine) {
4545
+ const params = [];
4546
+ let machineClause = "";
4547
+ if (machine) {
4548
+ machineClause = " AND machine_id = ?";
4549
+ params.push(machine);
4550
+ }
4551
+ return {
4552
+ sql: `
4553
+ SELECT
4554
+ COUNT(*) as sessions,
4555
+ COALESCE(SUM(request_count), 0) as requests,
4556
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
4557
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
4558
+ MAX(started_at) as last_active
4559
+ FROM sessions
4560
+ WHERE ${where}${machineClause}
4561
+ AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
4562
+ `,
4563
+ params
4564
+ };
4565
+ }
4566
+ function queryRequestTotals(db, where, params, machine) {
4567
+ const q = requestTotalsSql(where, machine);
4568
+ return baseRequestTotals(db.prepare(q.sql).get(...params, ...q.params));
4569
+ }
4570
+ function querySessionOnlyTotals(db, where, params, machine) {
4571
+ const q = sessionOnlyTotalsSql(where, machine);
4572
+ return baseSessionTotals(db.prepare(q.sql).get(...params, ...q.params));
4573
+ }
4574
+ function queryPeriodTotals(db, period, machine) {
4575
+ const summary = querySummary(db, period, machine);
4576
+ const requests = queryRequestTotals(db, requestPeriodWhere2(period), [], machine);
4577
+ const sessions = querySessionOnlyTotals(db, sessionPeriodWhere2(period), [], machine);
4578
+ const totals = combineTotals(requests, sessions);
4579
+ return {
4580
+ ...totals,
4581
+ sessions: summary.sessions,
4582
+ requests: summary.requests,
4583
+ total_tokens: summary.tokens,
4584
+ cost_usd: summary.total_usd
4585
+ };
4586
+ }
4587
+ function querySinceTotals(db, since, machine) {
4588
+ const requests = queryRequestTotals(db, "timestamp >= ?", [since], machine);
4589
+ const sessions = querySessionOnlyTotals(db, "started_at >= ?", [since], machine);
4590
+ return combineTotals(requests, sessions);
4591
+ }
4592
+ function parseSinceDate(input, now) {
4593
+ const value = input.trim();
4594
+ const match = value.match(/^(\d+(?:\.\d+)?)(m|h|d|w)$/i);
4595
+ if (match) {
4596
+ const amount = Number(match[1]);
4597
+ const unit = match[2].toLowerCase();
4598
+ const multiplier = unit === "m" ? 60000 : unit === "h" ? 60 * 60000 : unit === "d" ? 24 * 60 * 60000 : 7 * 24 * 60 * 60000;
4599
+ return {
4600
+ timestamp: new Date(now.getTime() - amount * multiplier).toISOString(),
4601
+ label: value
4602
+ };
4603
+ }
4604
+ const parsed = new Date(value);
4605
+ if (Number.isNaN(parsed.getTime())) {
4606
+ throw new Error("--since must be a duration like 24h, 7d, 30m, 2w, or an ISO date");
4607
+ }
4608
+ return { timestamp: parsed.toISOString(), label: value };
4609
+ }
4610
+ function queryMachineSinceRows(db, since, machine) {
4611
+ const knownMachines = new Set;
4612
+ for (const m of listMachines(db, "all")) {
4613
+ if (m.machine_id)
4614
+ knownMachines.add(m.machine_id);
4615
+ }
4616
+ for (const m of listMachineRegistry(db)) {
4617
+ if (m.machine_id)
4618
+ knownMachines.add(m.machine_id);
4619
+ }
4620
+ if (machine)
4621
+ knownMachines.add(machine);
4622
+ const requestMachineClause = machine ? " AND machine_id = ?" : "";
4623
+ const requestRows = db.prepare(`
4624
+ SELECT
4625
+ machine_id,
4626
+ COUNT(DISTINCT session_id) as request_sessions,
4627
+ COUNT(*) as requests,
4628
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
4629
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
4630
+ COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
4631
+ COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
4632
+ COALESCE(SUM(COALESCE(cache_create_5m_tokens, cache_create_tokens)), 0) as cache_create_5m_tokens,
4633
+ COALESCE(SUM(COALESCE(cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
4634
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
4635
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
4636
+ MAX(timestamp) as last_active
4637
+ FROM requests
4638
+ WHERE timestamp >= ?
4639
+ AND machine_id != ''
4640
+ ${requestMachineClause}
4641
+ GROUP BY machine_id
4642
+ `).all(...machine ? [since, machine] : [since]);
4643
+ const sessionMachineClause = machine ? " AND machine_id = ?" : "";
4644
+ const sessionRows = db.prepare(`
4645
+ SELECT
4646
+ machine_id,
4647
+ COUNT(*) as sessions,
4648
+ COALESCE(SUM(request_count), 0) as requests,
4649
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
4650
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
4651
+ MAX(started_at) as last_active
4652
+ FROM sessions
4653
+ WHERE started_at >= ?
4654
+ AND machine_id != ''
4655
+ ${sessionMachineClause}
4656
+ AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
4657
+ GROUP BY machine_id
4658
+ `).all(...machine ? [since, machine] : [since]);
4659
+ const groups = new Map;
4660
+ for (const row of requestRows) {
4661
+ knownMachines.add(row.machine_id);
4662
+ groups.set(row.machine_id, combineTotals(baseRequestTotals(row), ZERO_SESSION_TOTALS));
4663
+ }
4664
+ for (const row of sessionRows) {
4665
+ knownMachines.add(row.machine_id);
4666
+ const current = groups.get(row.machine_id) ?? emptyTotals();
4667
+ const sessions = baseSessionTotals(row);
4668
+ groups.set(row.machine_id, {
4669
+ ...current,
4670
+ sessions: current.sessions + sessions.sessions,
4671
+ requests: current.requests + sessions.requests,
4672
+ total_tokens: current.total_tokens + sessions.total_tokens,
4673
+ cost_usd: current.cost_usd + sessions.cost_usd,
4674
+ last_active: latestTimestamp(current.last_active, sessions.last_active)
4675
+ });
4676
+ }
4677
+ const lastData = queryMachineLastData(db, machine);
4678
+ const rows = [...knownMachines].filter((m) => !machine || m === machine).map((machineId) => {
4679
+ const totals = groups.get(machineId) ?? emptyTotals();
4680
+ const lastDataAt = lastData.get(machineId) ?? totals.last_active;
4681
+ return {
4682
+ machine_id: machineId,
4683
+ ...totals,
4684
+ last_data_at: lastDataAt,
4685
+ last_data_age: ""
4686
+ };
4687
+ });
4688
+ return rows.sort((a, b) => b.cost_usd - a.cost_usd || a.machine_id.localeCompare(b.machine_id));
4689
+ }
4690
+ function queryMachineLastData(db, machine) {
4691
+ const filter = machine ? " AND machine_id = ?" : "";
4692
+ const requestRows = db.prepare(`
4693
+ SELECT machine_id, MAX(timestamp) as last_at
4694
+ FROM requests
4695
+ WHERE machine_id != ''${filter}
4696
+ GROUP BY machine_id
4697
+ `).all(...machine ? [machine] : []);
4698
+ const sessionRows = db.prepare(`
4699
+ SELECT machine_id, MAX(started_at) as last_at
4700
+ FROM sessions
4701
+ WHERE machine_id != ''${filter}
4702
+ GROUP BY machine_id
4703
+ `).all(...machine ? [machine] : []);
4704
+ const result = new Map;
4705
+ for (const row of [...requestRows, ...sessionRows]) {
4706
+ result.set(row.machine_id, latestTimestamp(result.get(row.machine_id), row.last_at));
4707
+ }
4708
+ return result;
4709
+ }
4710
+ function queryAgentSinceRows(db, since, machine) {
4711
+ const requestMachineClause = machine ? " AND machine_id = ?" : "";
4712
+ const sessionMachineClause = machine ? " AND machine_id = ?" : "";
4713
+ const rows = db.prepare(`
4714
+ WITH request_rows AS (
4715
+ SELECT
4716
+ agent,
4717
+ session_id,
4718
+ 1 as requests,
4719
+ input_tokens,
4720
+ output_tokens,
4721
+ cache_read_tokens,
4722
+ cache_create_tokens,
4723
+ COALESCE(cache_create_5m_tokens, cache_create_tokens) as cache_create_5m_tokens,
4724
+ COALESCE(cache_create_1h_tokens, 0) as cache_create_1h_tokens,
4725
+ input_tokens + output_tokens + cache_read_tokens + cache_create_tokens as total_tokens,
4726
+ cost_usd,
4727
+ timestamp as last_active
4728
+ FROM requests
4729
+ WHERE timestamp >= ?${requestMachineClause}
4730
+ ),
4731
+ session_only_rows AS (
4732
+ SELECT
4733
+ agent,
4734
+ id as session_id,
4735
+ COALESCE(request_count, 0) as requests,
4736
+ 0 as input_tokens,
4737
+ 0 as output_tokens,
4738
+ 0 as cache_read_tokens,
4739
+ 0 as cache_create_tokens,
4740
+ 0 as cache_create_5m_tokens,
4741
+ 0 as cache_create_1h_tokens,
4742
+ COALESCE(total_tokens, 0) as total_tokens,
4743
+ COALESCE(total_cost_usd, 0) as cost_usd,
4744
+ started_at as last_active
4745
+ FROM sessions
4746
+ WHERE started_at >= ?${sessionMachineClause}
4747
+ AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
4748
+ ),
4749
+ combined AS (
4750
+ SELECT * FROM request_rows
4751
+ UNION ALL
4752
+ SELECT * FROM session_only_rows
4753
+ )
4754
+ SELECT
4755
+ agent,
4756
+ COUNT(DISTINCT session_id) as sessions,
4757
+ COALESCE(SUM(requests), 0) as requests,
4758
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
4759
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
4760
+ COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
4761
+ COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
4762
+ COALESCE(SUM(cache_create_5m_tokens), 0) as cache_create_5m_tokens,
4763
+ COALESCE(SUM(cache_create_1h_tokens), 0) as cache_create_1h_tokens,
4764
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
4765
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
4766
+ MAX(last_active) as last_active
4767
+ FROM combined
4768
+ GROUP BY agent
4769
+ ORDER BY cost_usd DESC
4770
+ `).all(...machine ? [since, machine, since, machine] : [since, since]);
4771
+ if (rows.length > 0)
4772
+ return rows;
4773
+ return queryAgentBreakdown(db, "all", machine).filter((row) => row.last_active >= since).map(agentBreakdownToBriefRow);
4774
+ }
4775
+ function agentBreakdownToBriefRow(row) {
4776
+ return {
4777
+ agent: row.agent,
4778
+ sessions: row.sessions,
4779
+ requests: row.requests,
4780
+ input_tokens: 0,
4781
+ output_tokens: 0,
4782
+ cache_read_tokens: 0,
4783
+ cache_create_tokens: 0,
4784
+ cache_create_5m_tokens: 0,
4785
+ cache_create_1h_tokens: 0,
4786
+ total_tokens: row.total_tokens,
4787
+ cost_usd: row.cost_usd,
4788
+ last_active: row.last_active
4789
+ };
4790
+ }
4791
+ function normalizeAccountEmail2(email) {
4792
+ return email.trim().toLowerCase();
4793
+ }
4794
+ function accountKey2(row) {
4795
+ const agent = row.agent || row.account_tool || "unknown";
4796
+ const email = normalizeAccountEmail2(row.account_email);
4797
+ if (email)
4798
+ return `${agent}:${email}`;
4799
+ if (row.account_name)
4800
+ return `${agent}:${row.account_name}`;
4801
+ return row.account_key || `${agent}:unknown`;
4802
+ }
4803
+ function addAccountRow(groups, row) {
4804
+ if (!row.account_key && !row.account_tool && !row.account_name && !row.account_email)
4805
+ return;
4806
+ const key = accountKey2(row);
4807
+ const email = normalizeAccountEmail2(row.account_email);
4808
+ const current = groups.get(key) ?? {
4809
+ account_key: key,
4810
+ account_tool: row.agent || row.account_tool || "unknown",
4811
+ account_name: row.account_name || email || row.account_key || "unknown",
4812
+ account_email: email || null,
4813
+ account_source: row.account_source || "unknown",
4814
+ sessions: 0,
4815
+ requests: 0,
4816
+ input_tokens: 0,
4817
+ output_tokens: 0,
4818
+ cache_read_tokens: 0,
4819
+ cache_create_tokens: 0,
4820
+ cache_create_5m_tokens: 0,
4821
+ cache_create_1h_tokens: 0,
4822
+ total_tokens: 0,
4823
+ cost_usd: 0,
4824
+ last_active: null
4825
+ };
4826
+ current.sessions += Number(row.sessions ?? 0);
4827
+ if (!current.account_email && email)
4828
+ current.account_email = email;
4829
+ if ((!current.account_name || current.account_name === "unknown") && (row.account_name || email)) {
4830
+ current.account_name = row.account_name || email;
4831
+ }
4832
+ if ((!current.account_source || current.account_source === "unknown") && row.account_source && row.account_source !== "unknown") {
4833
+ current.account_source = row.account_source;
4834
+ }
4835
+ current.requests += Number(row.requests ?? 0);
4836
+ current.input_tokens += Number(row.input_tokens ?? 0);
4837
+ current.output_tokens += Number(row.output_tokens ?? 0);
4838
+ current.cache_read_tokens += Number(row.cache_read_tokens ?? 0);
4839
+ current.cache_create_tokens += Number(row.cache_create_tokens ?? 0);
4840
+ current.cache_create_5m_tokens += Number(row.cache_create_5m_tokens ?? 0);
4841
+ current.cache_create_1h_tokens += Number(row.cache_create_1h_tokens ?? 0);
4842
+ current.total_tokens += Number(row.total_tokens ?? 0);
4843
+ current.cost_usd += Number(row.cost_usd ?? 0);
4844
+ current.last_active = latestTimestamp(current.last_active, row.last_active);
4845
+ groups.set(key, current);
4846
+ }
4847
+ function queryAccountSinceRows(db, since, machine) {
4848
+ const requestMachineClause = machine ? " AND r.machine_id = ?" : "";
4849
+ const sessionMachineClause = machine ? " AND s.machine_id = ?" : "";
4850
+ const requestRows = db.prepare(`
4851
+ SELECT
4852
+ COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), '') as agent,
4853
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as account_key,
4854
+ COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') as account_tool,
4855
+ COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as account_name,
4856
+ COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') as account_email,
4857
+ COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
4858
+ COUNT(DISTINCT r.session_id) as sessions,
4859
+ COUNT(*) as requests,
4860
+ COALESCE(SUM(r.input_tokens), 0) as input_tokens,
4861
+ COALESCE(SUM(r.output_tokens), 0) as output_tokens,
4862
+ COALESCE(SUM(r.cache_read_tokens), 0) as cache_read_tokens,
4863
+ COALESCE(SUM(r.cache_create_tokens), 0) as cache_create_tokens,
4864
+ COALESCE(SUM(COALESCE(r.cache_create_5m_tokens, r.cache_create_tokens, 0)), 0) as cache_create_5m_tokens,
4865
+ COALESCE(SUM(COALESCE(r.cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
4866
+ COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
4867
+ COALESCE(SUM(r.cost_usd), 0) as cost_usd,
4868
+ MAX(r.timestamp) as last_active
4869
+ FROM requests r
4870
+ LEFT JOIN sessions s ON s.id = r.session_id
4871
+ WHERE r.timestamp >= ?${requestMachineClause}
4872
+ GROUP BY
4873
+ COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), ''),
4874
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), ''),
4875
+ COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), ''),
4876
+ COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), ''),
4877
+ COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), ''),
4878
+ COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown')
4879
+ `).all(...machine ? [since, machine] : [since]);
4880
+ const sessionRows = db.prepare(`
4881
+ SELECT
4882
+ s.agent as agent,
4883
+ s.account_key as account_key,
4884
+ s.account_tool as account_tool,
4885
+ s.account_name as account_name,
4886
+ COALESCE(s.account_email, '') as account_email,
4887
+ COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
4888
+ COUNT(DISTINCT s.id) as sessions,
4889
+ COALESCE(SUM(s.request_count), 0) as requests,
4890
+ 0 as input_tokens,
4891
+ 0 as output_tokens,
4892
+ 0 as cache_read_tokens,
4893
+ 0 as cache_create_tokens,
4894
+ 0 as cache_create_5m_tokens,
4895
+ 0 as cache_create_1h_tokens,
4896
+ COALESCE(SUM(s.total_tokens), 0) as total_tokens,
4897
+ COALESCE(SUM(s.total_cost_usd), 0) as cost_usd,
4898
+ MAX(s.started_at) as last_active
4899
+ FROM sessions s
4900
+ WHERE s.started_at >= ?${sessionMachineClause}
4901
+ AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = s.id)
4902
+ GROUP BY
4903
+ s.agent,
4904
+ s.account_key,
4905
+ s.account_tool,
4906
+ s.account_name,
4907
+ COALESCE(s.account_email, ''),
4908
+ COALESCE(NULLIF(s.account_source, ''), 'unknown')
4909
+ `).all(...machine ? [since, machine] : [since]);
4910
+ const groups = new Map;
4911
+ for (const row of requestRows)
4912
+ addAccountRow(groups, row);
4913
+ for (const row of sessionRows)
4914
+ addAccountRow(groups, row);
4915
+ const rows = [...groups.values()];
4916
+ rows.sort((a, b) => b.cost_usd - a.cost_usd || a.account_key.localeCompare(b.account_key));
4917
+ if (rows.length > 0)
4918
+ return rows;
4919
+ return queryAccountBreakdown(db, "all", machine).filter((row) => row.last_active >= since).map(accountBreakdownToBriefRow);
4920
+ }
4921
+ function accountBreakdownToBriefRow(row) {
4922
+ return {
4923
+ account_key: row.account_key,
4924
+ account_tool: row.account_tool,
4925
+ account_name: row.account_name,
4926
+ account_email: row.account_email,
4927
+ account_source: row.account_source,
4928
+ sessions: row.sessions,
4929
+ requests: row.requests,
4930
+ input_tokens: 0,
4931
+ output_tokens: 0,
4932
+ cache_read_tokens: 0,
4933
+ cache_create_tokens: 0,
4934
+ cache_create_5m_tokens: 0,
4935
+ cache_create_1h_tokens: 0,
4936
+ total_tokens: row.total_tokens,
4937
+ cost_usd: row.cost_usd,
4938
+ last_active: row.last_active
4939
+ };
4940
+ }
4941
+ function queryFreshness(db, now, machine, localSync) {
4942
+ const requestFilter = machine ? " AND machine_id = ?" : "";
4943
+ const requestRows = db.prepare(`
4944
+ SELECT machine_id, MAX(timestamp) as max_request_at
4945
+ FROM requests
4946
+ WHERE machine_id != ''${requestFilter}
4947
+ GROUP BY machine_id
4948
+ `).all(...machine ? [machine] : []);
4949
+ const registryRows = listMachineRegistry(db).filter((row) => !machine || row.machine_id === machine);
4950
+ const machines = new Map;
4951
+ for (const row of requestRows) {
4952
+ machines.set(row.machine_id, {
4953
+ machine_id: row.machine_id,
4954
+ max_request_at: row.max_request_at,
4955
+ request_age: formatAge(row.max_request_at, now),
4956
+ last_merge_sync_at: null,
4957
+ merge_sync_age: "never"
4958
+ });
4959
+ }
4960
+ for (const row of registryRows) {
4961
+ const current = machines.get(row.machine_id) ?? {
4962
+ machine_id: row.machine_id,
4963
+ max_request_at: null,
4964
+ request_age: "never",
4965
+ last_merge_sync_at: null,
4966
+ merge_sync_age: "never"
4967
+ };
4968
+ const syncAt = latestTimestamp(row.last_pull_at, row.last_push_at, row.updated_at);
4969
+ current.last_merge_sync_at = syncAt;
4970
+ current.merge_sync_age = formatAge(syncAt, now);
4971
+ machines.set(row.machine_id, current);
4972
+ }
4973
+ if (localSync && (!machine || machine === localSync.machineId)) {
4974
+ const current = machines.get(localSync.machineId) ?? {
4975
+ machine_id: localSync.machineId,
4976
+ max_request_at: null,
4977
+ request_age: "never",
4978
+ last_merge_sync_at: null,
4979
+ merge_sync_age: "never"
4980
+ };
4981
+ current.last_merge_sync_at = latestTimestamp(current.last_merge_sync_at, localSync.syncedAt);
4982
+ current.merge_sync_age = formatAge(current.last_merge_sync_at, now);
4983
+ machines.set(localSync.machineId, current);
4984
+ }
4985
+ const rows = [...machines.values()].sort((a, b) => a.machine_id.localeCompare(b.machine_id));
4986
+ return {
4987
+ machines: rows,
4988
+ max_request_line: rows.length ? rows.map((row) => `${row.machine_id} ${row.max_request_at ?? "never"} (${row.request_age})`).join("; ") : "none",
4989
+ merge_sync_line: rows.length ? rows.map((row) => `${row.machine_id} ${row.last_merge_sync_at ?? "never"} (${row.merge_sync_age})`).join("; ") : "none"
4990
+ };
4991
+ }
4992
+ function buildBrief(db, opts = {}) {
4993
+ const now = opts.now ?? new Date;
4994
+ const machine = machineFilter(opts.machine);
4995
+ const sinceInput = opts.since ?? "24h";
4996
+ const since = parseSinceDate(sinceInput, now);
4997
+ const summaries = [
4998
+ {
4999
+ label: "Today",
5000
+ period: "today",
5001
+ ...queryPeriodTotals(db, "today", machine)
5002
+ },
5003
+ {
5004
+ label: "Week",
5005
+ period: "week",
5006
+ ...queryPeriodTotals(db, "week", machine)
5007
+ },
5008
+ {
5009
+ label: `Since ${since.label}`,
5010
+ period: "since",
5011
+ since: since.timestamp,
5012
+ ...querySinceTotals(db, since.timestamp, machine)
5013
+ }
5014
+ ];
5015
+ const machines = queryMachineSinceRows(db, since.timestamp, machine);
5016
+ for (const row of machines) {
5017
+ row.last_data_age = formatAge(row.last_data_at, now);
5018
+ }
5019
+ return {
5020
+ generated_at: now.toISOString(),
5021
+ machine: machine ?? "all",
5022
+ since: {
5023
+ input: sinceInput,
5024
+ timestamp: since.timestamp,
5025
+ label: since.label
5026
+ },
5027
+ summaries,
5028
+ machines,
5029
+ agents: queryAgentSinceRows(db, since.timestamp, machine),
5030
+ accounts: queryAccountSinceRows(db, since.timestamp, machine),
5031
+ freshness: queryFreshness(db, now, machine, opts.currentMachineId && opts.localSyncAt ? { machineId: opts.currentMachineId, syncedAt: opts.localSyncAt.toISOString() } : undefined)
5032
+ };
5033
+ }
5034
+
5035
+ // src/lib/analytics.ts
5036
+ function queryProjectDetail(db, nameOrPath) {
5037
+ const like = `%${nameOrPath}%`;
5038
+ const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
5039
+ FROM sessions
5040
+ WHERE project_name LIKE ? OR project_path LIKE ?
5041
+ ORDER BY started_at DESC`).all(like, like);
5042
+ if (sessions.length === 0)
5043
+ return null;
5044
+ const first = sessions[0];
5045
+ const totalCost = sessions.reduce((s, r) => s + (r.total_cost_usd ?? 0), 0);
5046
+ const totalTokens = sessions.reduce((s, r) => s + (r.total_tokens ?? 0), 0);
5047
+ const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
5048
+ FROM requests r JOIN sessions s ON r.session_id = s.id
5049
+ WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
5050
+ AND r.timestamp >= DATE('now', '-14 days')
5051
+ GROUP BY date ORDER BY date ASC`).all(like, like);
5052
+ const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
5053
+ FROM requests r JOIN sessions s ON r.session_id = s.id
5054
+ WHERE s.project_name LIKE ? OR s.project_path LIKE ?
5055
+ GROUP BY r.model ORDER BY cost_usd DESC LIMIT 5`).all(like, like);
5056
+ const top_sessions = [...sessions].sort((a, b) => (b.total_cost_usd ?? 0) - (a.total_cost_usd ?? 0)).slice(0, 5).map((s) => ({ id: s.id, total_cost_usd: s.total_cost_usd ?? 0, started_at: s.started_at }));
5057
+ return {
5058
+ project_name: first.project_name || nameOrPath,
5059
+ project_path: first.project_path || "",
5060
+ total_cost_usd: totalCost,
5061
+ total_tokens: totalTokens,
5062
+ sessions: sessions.length,
5063
+ daily,
5064
+ models,
5065
+ top_sessions
5066
+ };
5067
+ }
5068
+ function exportDateWhere(column, period) {
5069
+ switch (period) {
5070
+ case "today":
5071
+ return `DATE(${column}) = DATE('now')`;
5072
+ case "week":
5073
+ return `${column} >= DATE('now', '-7 days')`;
5074
+ case "all":
5075
+ return "1=1";
5076
+ default:
5077
+ return `${column} >= DATE('now', '-30 days')`;
5078
+ }
5079
+ }
5080
+ function queryExportRows(db, type, period) {
5081
+ if (type === "requests") {
5082
+ const where2 = exportDateWhere("timestamp", period);
5083
+ return db.prepare(`SELECT * FROM requests WHERE ${where2} ORDER BY timestamp ASC`).all();
5084
+ }
5085
+ const where = exportDateWhere("started_at", period);
5086
+ return db.prepare(`SELECT * FROM sessions WHERE ${where} ORDER BY started_at DESC`).all();
5087
+ }
5088
+ function queryRangeStats(db, from, to) {
5089
+ const r = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost, COUNT(*) as requests,
5090
+ COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens+cache_create_tokens),0) as tokens
5091
+ FROM requests WHERE DATE(timestamp) BETWEEN ? AND ?`).get(from, to);
5092
+ const s = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
5093
+ return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
5094
+ }
5095
+ function queryForecast(db, now = new Date) {
5096
+ const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
5097
+ const dayOfMonth = now.getDate();
5098
+ const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
5099
+ const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(monthStart);
5100
+ const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
5101
+ const projected = dailyAvg * daysInMonth;
5102
+ const sevenDaysAgo = new Date(now);
5103
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
5104
+ const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
5105
+ const last7DailyAvg = last7.cost / 7;
5106
+ const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
5107
+ FROM requests WHERE DATE(timestamp) >= ? GROUP BY date ORDER BY cost_usd ASC`).all(monthStart);
5108
+ return {
5109
+ day_of_month: dayOfMonth,
5110
+ days_in_month: daysInMonth,
5111
+ spent_so_far_usd: monthSoFar.cost,
5112
+ daily_avg_usd: dailyAvg,
5113
+ projected_usd: projected,
5114
+ last7_daily_avg_usd: last7DailyAvg,
5115
+ last7_projected_usd: last7DailyAvg * daysInMonth,
5116
+ cheapest_day: dailyCosts[0] ?? null,
5117
+ most_expensive_day: dailyCosts[dailyCosts.length - 1] ?? null
5118
+ };
5119
+ }
5120
+ function queryModelEfficiency(db) {
5121
+ return db.prepare(`SELECT model,
5122
+ COALESCE(SUM(input_tokens),0) as input,
5123
+ COALESCE(SUM(output_tokens),0) as output,
5124
+ COALESCE(SUM(cache_read_tokens),0) as cache_read,
5125
+ COALESCE(SUM(cache_create_tokens),0) as cache_write,
5126
+ COUNT(*) as requests,
5127
+ COALESCE(SUM(cost_usd),0) as cost
5128
+ FROM requests GROUP BY model ORDER BY cost DESC`).all();
5129
+ }
5130
+
5131
+ // src/server/serve.ts
5132
+ init_database();
5133
+
4252
5134
  // src/lib/periods.ts
4253
5135
  function ymd(date) {
4254
5136
  return date.toISOString().substring(0, 10);
@@ -4369,6 +5251,24 @@ function isAuthorizedRequest(req, path) {
4369
5251
  import { randomUUID } from "crypto";
4370
5252
  import { existsSync as existsSync12 } from "fs";
4371
5253
  import { resolve, sep } from "path";
5254
+
5255
+ // src/lib/package-metadata.ts
5256
+ import { readFileSync as readFileSync8 } from "fs";
5257
+ var cachedMetadata = null;
5258
+ function getPackageMetadata() {
5259
+ if (cachedMetadata)
5260
+ return cachedMetadata;
5261
+ const raw = readFileSync8(new URL("../../package.json", import.meta.url), "utf8");
5262
+ const parsed = JSON.parse(raw);
5263
+ cachedMetadata = {
5264
+ name: parsed.name ?? "@hasna/economy",
5265
+ version: parsed.version ?? "0.0.0"
5266
+ };
5267
+ return cachedMetadata;
5268
+ }
5269
+ var packageMetadata = getPackageMetadata();
5270
+
5271
+ // src/server/serve.ts
4372
5272
  init_cloud();
4373
5273
  import { verifyApiKey, ApiKeyStore } from "@hasna/contracts/auth";
4374
5274
 
@@ -4380,85 +5280,1590 @@ var openApiSpec = {
4380
5280
  version: "0.0.0",
4381
5281
  description: "AI coding cost tracker \u2014 self-hosted control plane. Cloud mode is PURE REMOTE per Amendment A1: the serve process reads/writes the shared RDS Postgres directly with API-key auth via @hasna/contracts. All /v1 routes require a valid `economy:*` scoped API key (x-api-key or Authorization: Bearer). The foundation probes /health, /ready, /version are open."
4382
5282
  },
4383
- servers: [{ url: "https://economy.hasna.xyz", description: "prod" }, { url: "http://localhost:3456", description: "local" }],
4384
- security: [{ apiKey: [] }, { bearer: [] }],
5283
+ servers: [
5284
+ {
5285
+ url: "https://economy.hasna.xyz",
5286
+ description: "prod"
5287
+ },
5288
+ {
5289
+ url: "http://localhost:3456",
5290
+ description: "local"
5291
+ }
5292
+ ],
5293
+ security: [
5294
+ {
5295
+ apiKey: []
5296
+ },
5297
+ {
5298
+ bearer: []
5299
+ }
5300
+ ],
4385
5301
  components: {
4386
5302
  securitySchemes: {
4387
- apiKey: { type: "apiKey", in: "header", name: "x-api-key" },
4388
- bearer: { type: "http", scheme: "bearer" }
5303
+ apiKey: {
5304
+ type: "apiKey",
5305
+ in: "header",
5306
+ name: "x-api-key"
5307
+ },
5308
+ bearer: {
5309
+ type: "http",
5310
+ scheme: "bearer"
5311
+ }
4389
5312
  },
4390
5313
  schemas: {
4391
- Envelope: { type: "object", properties: { data: {}, meta: { type: "object", additionalProperties: true } }, required: ["data"] },
4392
- Foundation: { type: "object", properties: { status: { type: "string" }, version: { type: "string" }, mode: { type: "string" }, service: { type: "string" } }, required: ["status", "version", "mode"] },
4393
- Error: { type: "object", properties: { error: { type: "string" }, message: { type: "string" } }, required: ["error"] },
4394
- MutationOk: { type: "object", properties: { ok: { type: "boolean" } } },
4395
- CostSummary: { type: "object", properties: { total_cost_usd: { type: "number" }, total_tokens: { type: "number" }, request_count: { type: "number" }, session_count: { type: "number" }, period: { type: "string" } }, additionalProperties: true },
4396
- Session: { type: "object", properties: { id: { type: "string" }, agent: { type: "string" }, project_path: { type: "string" }, project_name: { type: "string" }, started_at: { type: "string" }, ended_at: { type: "string", nullable: true }, total_cost_usd: { type: "number" }, total_tokens: { type: "number" }, request_count: { type: "number" } }, additionalProperties: true },
4397
- ModelBreakdown: { type: "object", additionalProperties: true },
4398
- ProjectBreakdown: { type: "object", additionalProperties: true },
4399
- AgentBreakdown: { type: "object", additionalProperties: true },
4400
- AccountBreakdown: { type: "object", additionalProperties: true },
4401
- DailyPoint: { type: "object", additionalProperties: true },
4402
- BudgetStatus: { type: "object", properties: { id: { type: "string" }, limit_usd: { type: "number" }, period: { type: "string" }, spent_usd: { type: "number" } }, additionalProperties: true },
4403
- CreateBudgetInput: { type: "object", properties: { limit_usd: { type: "number" }, alert_at_percent: { type: "number" }, period: { type: "string", enum: ["daily", "weekly", "monthly", "day", "week", "month"] }, project_path: { type: "string", nullable: true }, agent: { type: "string", nullable: true } }, required: ["limit_usd"] },
4404
- GoalStatus: { type: "object", additionalProperties: true },
4405
- CreateGoalInput: { type: "object", properties: { period: { type: "string", enum: ["day", "week", "month", "year"] }, limit_usd: { type: "number" }, project_path: { type: "string", nullable: true }, agent: { type: "string", nullable: true } }, required: ["limit_usd"] },
4406
- ModelPricing: { type: "object", additionalProperties: true },
4407
- CreatePricingInput: { type: "object", properties: { model: { type: "string" }, input_per_1m: { type: "number" }, output_per_1m: { type: "number" } }, required: ["model", "input_per_1m", "output_per_1m"] },
4408
- Subscription: { type: "object", additionalProperties: true },
4409
- CreateSubscriptionInput: { type: "object", properties: { provider: { type: "string" }, plan: { type: "string" }, monthly_fee_usd: { type: "number" }, included_usage_usd: { type: "number" }, agent: { type: "string", nullable: true } }, required: ["provider", "plan"] },
4410
- Project: { type: "object", additionalProperties: true },
4411
- CreateProjectInput: { type: "object", properties: { path: { type: "string" }, name: { type: "string" }, description: { type: "string" }, tags: { type: "array", items: { type: "string" } } }, required: ["path"] },
4412
- MachineInfo: { type: "object", additionalProperties: true },
4413
- BillingSummary: { type: "object", additionalProperties: true }
5314
+ Envelope: {
5315
+ type: "object",
5316
+ properties: {
5317
+ data: {},
5318
+ meta: {
5319
+ type: "object",
5320
+ additionalProperties: true
5321
+ }
5322
+ },
5323
+ required: [
5324
+ "data"
5325
+ ]
5326
+ },
5327
+ Foundation: {
5328
+ type: "object",
5329
+ properties: {
5330
+ status: {
5331
+ type: "string"
5332
+ },
5333
+ version: {
5334
+ type: "string"
5335
+ },
5336
+ mode: {
5337
+ type: "string"
5338
+ },
5339
+ service: {
5340
+ type: "string"
5341
+ }
5342
+ },
5343
+ required: [
5344
+ "status",
5345
+ "version",
5346
+ "mode"
5347
+ ]
5348
+ },
5349
+ Error: {
5350
+ type: "object",
5351
+ properties: {
5352
+ error: {
5353
+ type: "string"
5354
+ },
5355
+ message: {
5356
+ type: "string"
5357
+ }
5358
+ },
5359
+ required: [
5360
+ "error"
5361
+ ]
5362
+ },
5363
+ MutationOk: {
5364
+ type: "object",
5365
+ properties: {
5366
+ ok: {
5367
+ type: "boolean"
5368
+ }
5369
+ }
5370
+ },
5371
+ CostSummary: {
5372
+ type: "object",
5373
+ properties: {
5374
+ total_cost_usd: {
5375
+ type: "number"
5376
+ },
5377
+ total_tokens: {
5378
+ type: "number"
5379
+ },
5380
+ request_count: {
5381
+ type: "number"
5382
+ },
5383
+ session_count: {
5384
+ type: "number"
5385
+ },
5386
+ period: {
5387
+ type: "string"
5388
+ }
5389
+ },
5390
+ additionalProperties: true
5391
+ },
5392
+ Session: {
5393
+ type: "object",
5394
+ properties: {
5395
+ id: {
5396
+ type: "string"
5397
+ },
5398
+ agent: {
5399
+ type: "string"
5400
+ },
5401
+ project_path: {
5402
+ type: "string"
5403
+ },
5404
+ project_name: {
5405
+ type: "string"
5406
+ },
5407
+ started_at: {
5408
+ type: "string"
5409
+ },
5410
+ ended_at: {
5411
+ type: "string",
5412
+ nullable: true
5413
+ },
5414
+ total_cost_usd: {
5415
+ type: "number"
5416
+ },
5417
+ total_tokens: {
5418
+ type: "number"
5419
+ },
5420
+ request_count: {
5421
+ type: "number"
5422
+ }
5423
+ },
5424
+ additionalProperties: true
5425
+ },
5426
+ ModelBreakdown: {
5427
+ type: "object",
5428
+ additionalProperties: true
5429
+ },
5430
+ ProjectBreakdown: {
5431
+ type: "object",
5432
+ additionalProperties: true
5433
+ },
5434
+ AgentBreakdown: {
5435
+ type: "object",
5436
+ additionalProperties: true
5437
+ },
5438
+ AccountBreakdown: {
5439
+ type: "object",
5440
+ additionalProperties: true
5441
+ },
5442
+ DailyPoint: {
5443
+ type: "object",
5444
+ additionalProperties: true
5445
+ },
5446
+ BudgetStatus: {
5447
+ type: "object",
5448
+ properties: {
5449
+ id: {
5450
+ type: "string"
5451
+ },
5452
+ limit_usd: {
5453
+ type: "number"
5454
+ },
5455
+ period: {
5456
+ type: "string"
5457
+ },
5458
+ spent_usd: {
5459
+ type: "number"
5460
+ }
5461
+ },
5462
+ additionalProperties: true
5463
+ },
5464
+ CreateBudgetInput: {
5465
+ type: "object",
5466
+ properties: {
5467
+ limit_usd: {
5468
+ type: "number"
5469
+ },
5470
+ alert_at_percent: {
5471
+ type: "number"
5472
+ },
5473
+ period: {
5474
+ type: "string",
5475
+ enum: [
5476
+ "daily",
5477
+ "weekly",
5478
+ "monthly",
5479
+ "day",
5480
+ "week",
5481
+ "month"
5482
+ ]
5483
+ },
5484
+ project_path: {
5485
+ type: "string",
5486
+ nullable: true
5487
+ },
5488
+ agent: {
5489
+ type: "string",
5490
+ nullable: true
5491
+ }
5492
+ },
5493
+ required: [
5494
+ "limit_usd"
5495
+ ]
5496
+ },
5497
+ GoalStatus: {
5498
+ type: "object",
5499
+ additionalProperties: true
5500
+ },
5501
+ CreateGoalInput: {
5502
+ type: "object",
5503
+ properties: {
5504
+ period: {
5505
+ type: "string",
5506
+ enum: [
5507
+ "day",
5508
+ "week",
5509
+ "month",
5510
+ "year"
5511
+ ]
5512
+ },
5513
+ limit_usd: {
5514
+ type: "number"
5515
+ },
5516
+ project_path: {
5517
+ type: "string",
5518
+ nullable: true
5519
+ },
5520
+ agent: {
5521
+ type: "string",
5522
+ nullable: true
5523
+ }
5524
+ },
5525
+ required: [
5526
+ "limit_usd"
5527
+ ]
5528
+ },
5529
+ ModelPricing: {
5530
+ type: "object",
5531
+ additionalProperties: true
5532
+ },
5533
+ CreatePricingInput: {
5534
+ type: "object",
5535
+ properties: {
5536
+ model: {
5537
+ type: "string"
5538
+ },
5539
+ input_per_1m: {
5540
+ type: "number"
5541
+ },
5542
+ output_per_1m: {
5543
+ type: "number"
5544
+ }
5545
+ },
5546
+ required: [
5547
+ "model",
5548
+ "input_per_1m",
5549
+ "output_per_1m"
5550
+ ]
5551
+ },
5552
+ Subscription: {
5553
+ type: "object",
5554
+ additionalProperties: true
5555
+ },
5556
+ CreateSubscriptionInput: {
5557
+ type: "object",
5558
+ properties: {
5559
+ provider: {
5560
+ type: "string"
5561
+ },
5562
+ plan: {
5563
+ type: "string"
5564
+ },
5565
+ monthly_fee_usd: {
5566
+ type: "number"
5567
+ },
5568
+ included_usage_usd: {
5569
+ type: "number"
5570
+ },
5571
+ agent: {
5572
+ type: "string",
5573
+ nullable: true
5574
+ }
5575
+ },
5576
+ required: [
5577
+ "provider",
5578
+ "plan"
5579
+ ]
5580
+ },
5581
+ Project: {
5582
+ type: "object",
5583
+ additionalProperties: true
5584
+ },
5585
+ CreateProjectInput: {
5586
+ type: "object",
5587
+ properties: {
5588
+ path: {
5589
+ type: "string"
5590
+ },
5591
+ name: {
5592
+ type: "string"
5593
+ },
5594
+ description: {
5595
+ type: "string"
5596
+ },
5597
+ tags: {
5598
+ type: "array",
5599
+ items: {
5600
+ type: "string"
5601
+ }
5602
+ }
5603
+ },
5604
+ required: [
5605
+ "path"
5606
+ ]
5607
+ },
5608
+ MachineInfo: {
5609
+ type: "object",
5610
+ additionalProperties: true
5611
+ },
5612
+ BillingSummary: {
5613
+ type: "object",
5614
+ additionalProperties: true
5615
+ }
4414
5616
  }
4415
5617
  },
4416
5618
  paths: {
4417
- "/health": { get: { operationId: "health", summary: "Liveness probe", security: [], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Foundation" } } } } } } },
4418
- "/ready": { get: { operationId: "ready", summary: "Readiness probe (storage reachable + migrated)", security: [], responses: { "200": { description: "ready", content: { "application/json": { schema: { $ref: "#/components/schemas/Foundation" } } } }, "503": { description: "not ready", content: { "application/json": { schema: { $ref: "#/components/schemas/Foundation" } } } } } } },
4419
- "/version": { get: { operationId: "version", summary: "Version probe", security: [], responses: { "200": { description: "version", content: { "application/json": { schema: { $ref: "#/components/schemas/Foundation" } } } } } } },
4420
- "/v1/summary": { get: { operationId: "getSummary", summary: "Cost summary for a period", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/CostSummary" } } } } } } } } },
4421
- "/v1/sessions": { get: { operationId: "listSessions", summary: "List sessions", parameters: [{ name: "agent", in: "query", schema: { type: "string" } }, { name: "project", in: "query", schema: { type: "string" } }, { name: "search", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }, { name: "account", in: "query", schema: { type: "string" } }, { name: "limit", in: "query", schema: { type: "integer" } }, { name: "offset", in: "query", schema: { type: "integer" } }, { name: "since", in: "query", schema: { type: "string" } }, { name: "fields", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/Session" } } } } } } } } } },
4422
- "/v1/sessions/{id}/requests": { get: { operationId: "getSessionRequests", summary: "Requests for a session", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } }, "404": { description: "not found", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } } } },
4423
- "/v1/top": { get: { operationId: "topSessions", summary: "Top sessions by cost", parameters: [{ name: "n", in: "query", schema: { type: "integer" } }, { name: "agent", in: "query", schema: { type: "string" } }, { name: "since", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/Session" } } } } } } } } } },
4424
- "/v1/models": { get: { operationId: "modelBreakdown", summary: "Cost by model", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/ModelBreakdown" } } } } } } } } } },
4425
- "/v1/projects": { get: { operationId: "projectBreakdown", summary: "Cost by project", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/ProjectBreakdown" } } } } } } } } } },
4426
- "/v1/accounts": { get: { operationId: "accountBreakdown", summary: "Cost by account", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/AccountBreakdown" } } } } } } } } } },
4427
- "/v1/breakdown": { get: { operationId: "breakdown", summary: "Cost breakdown by dimension", parameters: [{ name: "by", in: "query", schema: { type: "string", enum: ["model", "project", "agent", "account"] } }, { name: "period", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
4428
- "/v1/daily": { get: { operationId: "daily", summary: "Daily cost points", parameters: [{ name: "days", in: "query", schema: { type: "integer" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/DailyPoint" } } } } } } } } } },
4429
- "/v1/hourly": { get: { operationId: "hourly", summary: "Hourly cost points", parameters: [{ name: "hours", in: "query", schema: { type: "integer" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
4430
- "/v1/machines": { get: { operationId: "listMachines", summary: "Known machines", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/MachineInfo" } } } } } } } } } },
4431
- "/v1/fleet": { get: { operationId: "fleet", summary: "Fleet summary", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
4432
- "/v1/billing": { get: { operationId: "billing", summary: "Provider billing summary", parameters: [{ name: "period", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/BillingSummary" } } } } } } } } },
4433
- "/v1/billing/diff": { get: { operationId: "billingDiff", summary: "Billing vs tracked diff", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "threshold", in: "query", schema: { type: "number" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
4434
- "/v1/usage": { get: { operationId: "usage", summary: "Usage snapshots + summary", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "agent", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
4435
- "/v1/savings": { get: { operationId: "savings", summary: "Savings summary", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "agent", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
5619
+ "/health": {
5620
+ get: {
5621
+ operationId: "health",
5622
+ summary: "Liveness probe",
5623
+ security: [],
5624
+ responses: {
5625
+ "200": {
5626
+ description: "ok",
5627
+ content: {
5628
+ "application/json": {
5629
+ schema: {
5630
+ $ref: "#/components/schemas/Foundation"
5631
+ }
5632
+ }
5633
+ }
5634
+ }
5635
+ }
5636
+ }
5637
+ },
5638
+ "/ready": {
5639
+ get: {
5640
+ operationId: "ready",
5641
+ summary: "Readiness probe (storage reachable + migrated)",
5642
+ security: [],
5643
+ responses: {
5644
+ "200": {
5645
+ description: "ready",
5646
+ content: {
5647
+ "application/json": {
5648
+ schema: {
5649
+ $ref: "#/components/schemas/Foundation"
5650
+ }
5651
+ }
5652
+ }
5653
+ },
5654
+ "503": {
5655
+ description: "not ready",
5656
+ content: {
5657
+ "application/json": {
5658
+ schema: {
5659
+ $ref: "#/components/schemas/Foundation"
5660
+ }
5661
+ }
5662
+ }
5663
+ }
5664
+ }
5665
+ }
5666
+ },
5667
+ "/version": {
5668
+ get: {
5669
+ operationId: "version",
5670
+ summary: "Version probe",
5671
+ security: [],
5672
+ responses: {
5673
+ "200": {
5674
+ description: "version",
5675
+ content: {
5676
+ "application/json": {
5677
+ schema: {
5678
+ $ref: "#/components/schemas/Foundation"
5679
+ }
5680
+ }
5681
+ }
5682
+ }
5683
+ }
5684
+ }
5685
+ },
5686
+ "/v1/summary": {
5687
+ get: {
5688
+ operationId: "getSummary",
5689
+ summary: "Cost summary for a period",
5690
+ parameters: [
5691
+ {
5692
+ name: "period",
5693
+ in: "query",
5694
+ schema: {
5695
+ type: "string"
5696
+ }
5697
+ },
5698
+ {
5699
+ name: "machine",
5700
+ in: "query",
5701
+ schema: {
5702
+ type: "string"
5703
+ }
5704
+ }
5705
+ ],
5706
+ responses: {
5707
+ "200": {
5708
+ description: "ok",
5709
+ content: {
5710
+ "application/json": {
5711
+ schema: {
5712
+ type: "object",
5713
+ properties: {
5714
+ data: {
5715
+ $ref: "#/components/schemas/CostSummary"
5716
+ }
5717
+ }
5718
+ }
5719
+ }
5720
+ }
5721
+ }
5722
+ }
5723
+ }
5724
+ },
5725
+ "/v1/sessions": {
5726
+ get: {
5727
+ operationId: "listSessions",
5728
+ summary: "List sessions",
5729
+ parameters: [
5730
+ {
5731
+ name: "agent",
5732
+ in: "query",
5733
+ schema: {
5734
+ type: "string"
5735
+ }
5736
+ },
5737
+ {
5738
+ name: "project",
5739
+ in: "query",
5740
+ schema: {
5741
+ type: "string"
5742
+ }
5743
+ },
5744
+ {
5745
+ name: "search",
5746
+ in: "query",
5747
+ schema: {
5748
+ type: "string"
5749
+ }
5750
+ },
5751
+ {
5752
+ name: "machine",
5753
+ in: "query",
5754
+ schema: {
5755
+ type: "string"
5756
+ }
5757
+ },
5758
+ {
5759
+ name: "account",
5760
+ in: "query",
5761
+ schema: {
5762
+ type: "string"
5763
+ }
5764
+ },
5765
+ {
5766
+ name: "limit",
5767
+ in: "query",
5768
+ schema: {
5769
+ type: "integer"
5770
+ }
5771
+ },
5772
+ {
5773
+ name: "offset",
5774
+ in: "query",
5775
+ schema: {
5776
+ type: "integer"
5777
+ }
5778
+ },
5779
+ {
5780
+ name: "since",
5781
+ in: "query",
5782
+ schema: {
5783
+ type: "string"
5784
+ }
5785
+ },
5786
+ {
5787
+ name: "fields",
5788
+ in: "query",
5789
+ schema: {
5790
+ type: "string"
5791
+ }
5792
+ }
5793
+ ],
5794
+ responses: {
5795
+ "200": {
5796
+ description: "ok",
5797
+ content: {
5798
+ "application/json": {
5799
+ schema: {
5800
+ type: "object",
5801
+ properties: {
5802
+ data: {
5803
+ type: "array",
5804
+ items: {
5805
+ $ref: "#/components/schemas/Session"
5806
+ }
5807
+ }
5808
+ }
5809
+ }
5810
+ }
5811
+ }
5812
+ }
5813
+ }
5814
+ }
5815
+ },
5816
+ "/v1/sessions/{id}/requests": {
5817
+ get: {
5818
+ operationId: "getSessionRequests",
5819
+ summary: "Requests for a session",
5820
+ parameters: [
5821
+ {
5822
+ name: "id",
5823
+ in: "path",
5824
+ required: true,
5825
+ schema: {
5826
+ type: "string"
5827
+ }
5828
+ }
5829
+ ],
5830
+ responses: {
5831
+ "200": {
5832
+ description: "ok",
5833
+ content: {
5834
+ "application/json": {
5835
+ schema: {
5836
+ $ref: "#/components/schemas/Envelope"
5837
+ }
5838
+ }
5839
+ }
5840
+ },
5841
+ "404": {
5842
+ description: "not found",
5843
+ content: {
5844
+ "application/json": {
5845
+ schema: {
5846
+ $ref: "#/components/schemas/Error"
5847
+ }
5848
+ }
5849
+ }
5850
+ }
5851
+ }
5852
+ }
5853
+ },
5854
+ "/v1/top": {
5855
+ get: {
5856
+ operationId: "topSessions",
5857
+ summary: "Top sessions by cost",
5858
+ parameters: [
5859
+ {
5860
+ name: "n",
5861
+ in: "query",
5862
+ schema: {
5863
+ type: "integer"
5864
+ }
5865
+ },
5866
+ {
5867
+ name: "agent",
5868
+ in: "query",
5869
+ schema: {
5870
+ type: "string"
5871
+ }
5872
+ },
5873
+ {
5874
+ name: "since",
5875
+ in: "query",
5876
+ schema: {
5877
+ type: "string"
5878
+ }
5879
+ }
5880
+ ],
5881
+ responses: {
5882
+ "200": {
5883
+ description: "ok",
5884
+ content: {
5885
+ "application/json": {
5886
+ schema: {
5887
+ type: "object",
5888
+ properties: {
5889
+ data: {
5890
+ type: "array",
5891
+ items: {
5892
+ $ref: "#/components/schemas/Session"
5893
+ }
5894
+ }
5895
+ }
5896
+ }
5897
+ }
5898
+ }
5899
+ }
5900
+ }
5901
+ }
5902
+ },
5903
+ "/v1/models": {
5904
+ get: {
5905
+ operationId: "modelBreakdown",
5906
+ summary: "Cost by model",
5907
+ responses: {
5908
+ "200": {
5909
+ description: "ok",
5910
+ content: {
5911
+ "application/json": {
5912
+ schema: {
5913
+ type: "object",
5914
+ properties: {
5915
+ data: {
5916
+ type: "array",
5917
+ items: {
5918
+ $ref: "#/components/schemas/ModelBreakdown"
5919
+ }
5920
+ }
5921
+ }
5922
+ }
5923
+ }
5924
+ }
5925
+ }
5926
+ }
5927
+ }
5928
+ },
5929
+ "/v1/projects": {
5930
+ get: {
5931
+ operationId: "projectBreakdown",
5932
+ summary: "Cost by project",
5933
+ parameters: [
5934
+ {
5935
+ name: "period",
5936
+ in: "query",
5937
+ schema: {
5938
+ type: "string"
5939
+ }
5940
+ },
5941
+ {
5942
+ name: "machine",
5943
+ in: "query",
5944
+ schema: {
5945
+ type: "string"
5946
+ }
5947
+ }
5948
+ ],
5949
+ responses: {
5950
+ "200": {
5951
+ description: "ok",
5952
+ content: {
5953
+ "application/json": {
5954
+ schema: {
5955
+ type: "object",
5956
+ properties: {
5957
+ data: {
5958
+ type: "array",
5959
+ items: {
5960
+ $ref: "#/components/schemas/ProjectBreakdown"
5961
+ }
5962
+ }
5963
+ }
5964
+ }
5965
+ }
5966
+ }
5967
+ }
5968
+ }
5969
+ }
5970
+ },
5971
+ "/v1/accounts": {
5972
+ get: {
5973
+ operationId: "accountBreakdown",
5974
+ summary: "Cost by account",
5975
+ parameters: [
5976
+ {
5977
+ name: "period",
5978
+ in: "query",
5979
+ schema: {
5980
+ type: "string"
5981
+ }
5982
+ },
5983
+ {
5984
+ name: "machine",
5985
+ in: "query",
5986
+ schema: {
5987
+ type: "string"
5988
+ }
5989
+ }
5990
+ ],
5991
+ responses: {
5992
+ "200": {
5993
+ description: "ok",
5994
+ content: {
5995
+ "application/json": {
5996
+ schema: {
5997
+ type: "object",
5998
+ properties: {
5999
+ data: {
6000
+ type: "array",
6001
+ items: {
6002
+ $ref: "#/components/schemas/AccountBreakdown"
6003
+ }
6004
+ }
6005
+ }
6006
+ }
6007
+ }
6008
+ }
6009
+ }
6010
+ }
6011
+ }
6012
+ },
6013
+ "/v1/breakdown": {
6014
+ get: {
6015
+ operationId: "breakdown",
6016
+ summary: "Cost breakdown by dimension",
6017
+ parameters: [
6018
+ {
6019
+ name: "by",
6020
+ in: "query",
6021
+ schema: {
6022
+ type: "string",
6023
+ enum: [
6024
+ "model",
6025
+ "project",
6026
+ "agent",
6027
+ "account"
6028
+ ]
6029
+ }
6030
+ },
6031
+ {
6032
+ name: "period",
6033
+ in: "query",
6034
+ schema: {
6035
+ type: "string"
6036
+ }
6037
+ },
6038
+ {
6039
+ name: "machine",
6040
+ in: "query",
6041
+ schema: {
6042
+ type: "string"
6043
+ }
6044
+ }
6045
+ ],
6046
+ responses: {
6047
+ "200": {
6048
+ description: "ok",
6049
+ content: {
6050
+ "application/json": {
6051
+ schema: {
6052
+ $ref: "#/components/schemas/Envelope"
6053
+ }
6054
+ }
6055
+ }
6056
+ }
6057
+ }
6058
+ }
6059
+ },
6060
+ "/v1/daily": {
6061
+ get: {
6062
+ operationId: "daily",
6063
+ summary: "Daily cost points",
6064
+ parameters: [
6065
+ {
6066
+ name: "days",
6067
+ in: "query",
6068
+ schema: {
6069
+ type: "integer"
6070
+ }
6071
+ },
6072
+ {
6073
+ name: "machine",
6074
+ in: "query",
6075
+ schema: {
6076
+ type: "string"
6077
+ }
6078
+ }
6079
+ ],
6080
+ responses: {
6081
+ "200": {
6082
+ description: "ok",
6083
+ content: {
6084
+ "application/json": {
6085
+ schema: {
6086
+ type: "object",
6087
+ properties: {
6088
+ data: {
6089
+ type: "array",
6090
+ items: {
6091
+ $ref: "#/components/schemas/DailyPoint"
6092
+ }
6093
+ }
6094
+ }
6095
+ }
6096
+ }
6097
+ }
6098
+ }
6099
+ }
6100
+ }
6101
+ },
6102
+ "/v1/hourly": {
6103
+ get: {
6104
+ operationId: "hourly",
6105
+ summary: "Hourly cost points",
6106
+ parameters: [
6107
+ {
6108
+ name: "hours",
6109
+ in: "query",
6110
+ schema: {
6111
+ type: "integer"
6112
+ }
6113
+ },
6114
+ {
6115
+ name: "machine",
6116
+ in: "query",
6117
+ schema: {
6118
+ type: "string"
6119
+ }
6120
+ }
6121
+ ],
6122
+ responses: {
6123
+ "200": {
6124
+ description: "ok",
6125
+ content: {
6126
+ "application/json": {
6127
+ schema: {
6128
+ $ref: "#/components/schemas/Envelope"
6129
+ }
6130
+ }
6131
+ }
6132
+ }
6133
+ }
6134
+ }
6135
+ },
6136
+ "/v1/machines": {
6137
+ get: {
6138
+ operationId: "listMachines",
6139
+ summary: "Known machines",
6140
+ responses: {
6141
+ "200": {
6142
+ description: "ok",
6143
+ content: {
6144
+ "application/json": {
6145
+ schema: {
6146
+ type: "object",
6147
+ properties: {
6148
+ data: {
6149
+ type: "array",
6150
+ items: {
6151
+ $ref: "#/components/schemas/MachineInfo"
6152
+ }
6153
+ }
6154
+ }
6155
+ }
6156
+ }
6157
+ }
6158
+ }
6159
+ }
6160
+ }
6161
+ },
6162
+ "/v1/fleet": {
6163
+ get: {
6164
+ operationId: "fleet",
6165
+ summary: "Fleet summary",
6166
+ parameters: [
6167
+ {
6168
+ name: "period",
6169
+ in: "query",
6170
+ schema: {
6171
+ type: "string"
6172
+ }
6173
+ },
6174
+ {
6175
+ name: "machine",
6176
+ in: "query",
6177
+ schema: {
6178
+ type: "string"
6179
+ }
6180
+ }
6181
+ ],
6182
+ responses: {
6183
+ "200": {
6184
+ description: "ok",
6185
+ content: {
6186
+ "application/json": {
6187
+ schema: {
6188
+ $ref: "#/components/schemas/Envelope"
6189
+ }
6190
+ }
6191
+ }
6192
+ }
6193
+ }
6194
+ }
6195
+ },
6196
+ "/v1/billing": {
6197
+ get: {
6198
+ operationId: "billing",
6199
+ summary: "Provider billing summary",
6200
+ parameters: [
6201
+ {
6202
+ name: "period",
6203
+ in: "query",
6204
+ schema: {
6205
+ type: "string"
6206
+ }
6207
+ }
6208
+ ],
6209
+ responses: {
6210
+ "200": {
6211
+ description: "ok",
6212
+ content: {
6213
+ "application/json": {
6214
+ schema: {
6215
+ type: "object",
6216
+ properties: {
6217
+ data: {
6218
+ $ref: "#/components/schemas/BillingSummary"
6219
+ }
6220
+ }
6221
+ }
6222
+ }
6223
+ }
6224
+ }
6225
+ }
6226
+ }
6227
+ },
6228
+ "/v1/billing/diff": {
6229
+ get: {
6230
+ operationId: "billingDiff",
6231
+ summary: "Billing vs tracked diff",
6232
+ parameters: [
6233
+ {
6234
+ name: "period",
6235
+ in: "query",
6236
+ schema: {
6237
+ type: "string"
6238
+ }
6239
+ },
6240
+ {
6241
+ name: "threshold",
6242
+ in: "query",
6243
+ schema: {
6244
+ type: "number"
6245
+ }
6246
+ }
6247
+ ],
6248
+ responses: {
6249
+ "200": {
6250
+ description: "ok",
6251
+ content: {
6252
+ "application/json": {
6253
+ schema: {
6254
+ $ref: "#/components/schemas/Envelope"
6255
+ }
6256
+ }
6257
+ }
6258
+ }
6259
+ }
6260
+ }
6261
+ },
6262
+ "/v1/usage": {
6263
+ get: {
6264
+ operationId: "usage",
6265
+ summary: "Usage snapshots + summary",
6266
+ parameters: [
6267
+ {
6268
+ name: "period",
6269
+ in: "query",
6270
+ schema: {
6271
+ type: "string"
6272
+ }
6273
+ },
6274
+ {
6275
+ name: "agent",
6276
+ in: "query",
6277
+ schema: {
6278
+ type: "string"
6279
+ }
6280
+ }
6281
+ ],
6282
+ responses: {
6283
+ "200": {
6284
+ description: "ok",
6285
+ content: {
6286
+ "application/json": {
6287
+ schema: {
6288
+ $ref: "#/components/schemas/Envelope"
6289
+ }
6290
+ }
6291
+ }
6292
+ }
6293
+ }
6294
+ }
6295
+ },
6296
+ "/v1/savings": {
6297
+ get: {
6298
+ operationId: "savings",
6299
+ summary: "Savings summary",
6300
+ parameters: [
6301
+ {
6302
+ name: "period",
6303
+ in: "query",
6304
+ schema: {
6305
+ type: "string"
6306
+ }
6307
+ },
6308
+ {
6309
+ name: "agent",
6310
+ in: "query",
6311
+ schema: {
6312
+ type: "string"
6313
+ }
6314
+ }
6315
+ ],
6316
+ responses: {
6317
+ "200": {
6318
+ description: "ok",
6319
+ content: {
6320
+ "application/json": {
6321
+ schema: {
6322
+ $ref: "#/components/schemas/Envelope"
6323
+ }
6324
+ }
6325
+ }
6326
+ }
6327
+ }
6328
+ }
6329
+ },
4436
6330
  "/v1/budgets": {
4437
- get: { operationId: "listBudgets", summary: "List budgets", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/BudgetStatus" } } } } } } } } },
4438
- post: { operationId: "createBudget", summary: "Create a budget", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreateBudgetInput" } } } }, responses: { "200": { description: "created", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/BudgetStatus" } } } } } } } }
6331
+ get: {
6332
+ operationId: "listBudgets",
6333
+ summary: "List budgets",
6334
+ responses: {
6335
+ "200": {
6336
+ description: "ok",
6337
+ content: {
6338
+ "application/json": {
6339
+ schema: {
6340
+ type: "object",
6341
+ properties: {
6342
+ data: {
6343
+ type: "array",
6344
+ items: {
6345
+ $ref: "#/components/schemas/BudgetStatus"
6346
+ }
6347
+ }
6348
+ }
6349
+ }
6350
+ }
6351
+ }
6352
+ }
6353
+ }
6354
+ },
6355
+ post: {
6356
+ operationId: "createBudget",
6357
+ summary: "Create a budget",
6358
+ requestBody: {
6359
+ required: true,
6360
+ content: {
6361
+ "application/json": {
6362
+ schema: {
6363
+ $ref: "#/components/schemas/CreateBudgetInput"
6364
+ }
6365
+ }
6366
+ }
6367
+ },
6368
+ responses: {
6369
+ "200": {
6370
+ description: "created",
6371
+ content: {
6372
+ "application/json": {
6373
+ schema: {
6374
+ type: "object",
6375
+ properties: {
6376
+ data: {
6377
+ $ref: "#/components/schemas/BudgetStatus"
6378
+ }
6379
+ }
6380
+ }
6381
+ }
6382
+ }
6383
+ }
6384
+ }
6385
+ }
6386
+ },
6387
+ "/v1/budgets/{id}": {
6388
+ delete: {
6389
+ operationId: "deleteBudget",
6390
+ summary: "Delete a budget",
6391
+ parameters: [
6392
+ {
6393
+ name: "id",
6394
+ in: "path",
6395
+ required: true,
6396
+ schema: {
6397
+ type: "string"
6398
+ }
6399
+ }
6400
+ ],
6401
+ responses: {
6402
+ "200": {
6403
+ description: "ok",
6404
+ content: {
6405
+ "application/json": {
6406
+ schema: {
6407
+ $ref: "#/components/schemas/MutationOk"
6408
+ }
6409
+ }
6410
+ }
6411
+ }
6412
+ }
6413
+ }
4439
6414
  },
4440
- "/v1/budgets/{id}": { delete: { operationId: "deleteBudget", summary: "Delete a budget", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
4441
6415
  "/v1/goals": {
4442
- get: { operationId: "listGoals", summary: "List goals", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/GoalStatus" } } } } } } } } },
4443
- post: { operationId: "createGoal", summary: "Create a goal", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreateGoalInput" } } } }, responses: { "200": { description: "created", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/GoalStatus" } } } } } } } }
6416
+ get: {
6417
+ operationId: "listGoals",
6418
+ summary: "List goals",
6419
+ responses: {
6420
+ "200": {
6421
+ description: "ok",
6422
+ content: {
6423
+ "application/json": {
6424
+ schema: {
6425
+ type: "object",
6426
+ properties: {
6427
+ data: {
6428
+ type: "array",
6429
+ items: {
6430
+ $ref: "#/components/schemas/GoalStatus"
6431
+ }
6432
+ }
6433
+ }
6434
+ }
6435
+ }
6436
+ }
6437
+ }
6438
+ }
6439
+ },
6440
+ post: {
6441
+ operationId: "createGoal",
6442
+ summary: "Create a goal",
6443
+ requestBody: {
6444
+ required: true,
6445
+ content: {
6446
+ "application/json": {
6447
+ schema: {
6448
+ $ref: "#/components/schemas/CreateGoalInput"
6449
+ }
6450
+ }
6451
+ }
6452
+ },
6453
+ responses: {
6454
+ "200": {
6455
+ description: "created",
6456
+ content: {
6457
+ "application/json": {
6458
+ schema: {
6459
+ type: "object",
6460
+ properties: {
6461
+ data: {
6462
+ $ref: "#/components/schemas/GoalStatus"
6463
+ }
6464
+ }
6465
+ }
6466
+ }
6467
+ }
6468
+ }
6469
+ }
6470
+ }
6471
+ },
6472
+ "/v1/goals/{id}": {
6473
+ delete: {
6474
+ operationId: "deleteGoal",
6475
+ summary: "Delete a goal",
6476
+ parameters: [
6477
+ {
6478
+ name: "id",
6479
+ in: "path",
6480
+ required: true,
6481
+ schema: {
6482
+ type: "string"
6483
+ }
6484
+ }
6485
+ ],
6486
+ responses: {
6487
+ "200": {
6488
+ description: "ok",
6489
+ content: {
6490
+ "application/json": {
6491
+ schema: {
6492
+ $ref: "#/components/schemas/MutationOk"
6493
+ }
6494
+ }
6495
+ }
6496
+ }
6497
+ }
6498
+ }
4444
6499
  },
4445
- "/v1/goals/{id}": { delete: { operationId: "deleteGoal", summary: "Delete a goal", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
4446
6500
  "/v1/pricing": {
4447
- get: { operationId: "listPricing", summary: "List model pricing", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/ModelPricing" } } } } } } } } },
4448
- post: { operationId: "upsertPricing", summary: "Upsert model pricing", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreatePricingInput" } } } }, responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/ModelPricing" } } } } } } } }
6501
+ get: {
6502
+ operationId: "listPricing",
6503
+ summary: "List model pricing",
6504
+ responses: {
6505
+ "200": {
6506
+ description: "ok",
6507
+ content: {
6508
+ "application/json": {
6509
+ schema: {
6510
+ type: "object",
6511
+ properties: {
6512
+ data: {
6513
+ type: "array",
6514
+ items: {
6515
+ $ref: "#/components/schemas/ModelPricing"
6516
+ }
6517
+ }
6518
+ }
6519
+ }
6520
+ }
6521
+ }
6522
+ }
6523
+ }
6524
+ },
6525
+ post: {
6526
+ operationId: "upsertPricing",
6527
+ summary: "Upsert model pricing",
6528
+ requestBody: {
6529
+ required: true,
6530
+ content: {
6531
+ "application/json": {
6532
+ schema: {
6533
+ $ref: "#/components/schemas/CreatePricingInput"
6534
+ }
6535
+ }
6536
+ }
6537
+ },
6538
+ responses: {
6539
+ "200": {
6540
+ description: "ok",
6541
+ content: {
6542
+ "application/json": {
6543
+ schema: {
6544
+ type: "object",
6545
+ properties: {
6546
+ data: {
6547
+ $ref: "#/components/schemas/ModelPricing"
6548
+ }
6549
+ }
6550
+ }
6551
+ }
6552
+ }
6553
+ }
6554
+ }
6555
+ }
6556
+ },
6557
+ "/v1/pricing/{model}": {
6558
+ delete: {
6559
+ operationId: "deletePricing",
6560
+ summary: "Delete model pricing",
6561
+ parameters: [
6562
+ {
6563
+ name: "model",
6564
+ in: "path",
6565
+ required: true,
6566
+ schema: {
6567
+ type: "string"
6568
+ }
6569
+ }
6570
+ ],
6571
+ responses: {
6572
+ "200": {
6573
+ description: "ok",
6574
+ content: {
6575
+ "application/json": {
6576
+ schema: {
6577
+ $ref: "#/components/schemas/MutationOk"
6578
+ }
6579
+ }
6580
+ }
6581
+ }
6582
+ }
6583
+ }
4449
6584
  },
4450
- "/v1/pricing/{model}": { delete: { operationId: "deletePricing", summary: "Delete model pricing", parameters: [{ name: "model", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
4451
6585
  "/v1/subscriptions": {
4452
- get: { operationId: "listSubscriptions", summary: "List subscriptions", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/Subscription" } } } } } } } } },
4453
- post: { operationId: "upsertSubscription", summary: "Upsert a subscription", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreateSubscriptionInput" } } } }, responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/Subscription" } } } } } } } }
6586
+ get: {
6587
+ operationId: "listSubscriptions",
6588
+ summary: "List subscriptions",
6589
+ responses: {
6590
+ "200": {
6591
+ description: "ok",
6592
+ content: {
6593
+ "application/json": {
6594
+ schema: {
6595
+ type: "object",
6596
+ properties: {
6597
+ data: {
6598
+ type: "array",
6599
+ items: {
6600
+ $ref: "#/components/schemas/Subscription"
6601
+ }
6602
+ }
6603
+ }
6604
+ }
6605
+ }
6606
+ }
6607
+ }
6608
+ }
6609
+ },
6610
+ post: {
6611
+ operationId: "upsertSubscription",
6612
+ summary: "Upsert a subscription",
6613
+ requestBody: {
6614
+ required: true,
6615
+ content: {
6616
+ "application/json": {
6617
+ schema: {
6618
+ $ref: "#/components/schemas/CreateSubscriptionInput"
6619
+ }
6620
+ }
6621
+ }
6622
+ },
6623
+ responses: {
6624
+ "200": {
6625
+ description: "ok",
6626
+ content: {
6627
+ "application/json": {
6628
+ schema: {
6629
+ type: "object",
6630
+ properties: {
6631
+ data: {
6632
+ $ref: "#/components/schemas/Subscription"
6633
+ }
6634
+ }
6635
+ }
6636
+ }
6637
+ }
6638
+ }
6639
+ }
6640
+ }
6641
+ },
6642
+ "/v1/subscriptions/{id}": {
6643
+ delete: {
6644
+ operationId: "deleteSubscription",
6645
+ summary: "Delete a subscription",
6646
+ parameters: [
6647
+ {
6648
+ name: "id",
6649
+ in: "path",
6650
+ required: true,
6651
+ schema: {
6652
+ type: "string"
6653
+ }
6654
+ }
6655
+ ],
6656
+ responses: {
6657
+ "200": {
6658
+ description: "ok",
6659
+ content: {
6660
+ "application/json": {
6661
+ schema: {
6662
+ $ref: "#/components/schemas/MutationOk"
6663
+ }
6664
+ }
6665
+ }
6666
+ }
6667
+ }
6668
+ }
4454
6669
  },
4455
- "/v1/subscriptions/{id}": { delete: { operationId: "deleteSubscription", summary: "Delete a subscription", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
4456
6670
  "/v1/project-registry": {
4457
- get: { operationId: "listProjectRegistry", summary: "List registered projects", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/Project" } } } } } } } } },
4458
- post: { operationId: "upsertProject", summary: "Register a project", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreateProjectInput" } } } }, responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } }
6671
+ get: {
6672
+ operationId: "listProjectRegistry",
6673
+ summary: "List registered projects",
6674
+ responses: {
6675
+ "200": {
6676
+ description: "ok",
6677
+ content: {
6678
+ "application/json": {
6679
+ schema: {
6680
+ type: "object",
6681
+ properties: {
6682
+ data: {
6683
+ type: "array",
6684
+ items: {
6685
+ $ref: "#/components/schemas/Project"
6686
+ }
6687
+ }
6688
+ }
6689
+ }
6690
+ }
6691
+ }
6692
+ }
6693
+ }
6694
+ },
6695
+ post: {
6696
+ operationId: "upsertProject",
6697
+ summary: "Register a project",
6698
+ requestBody: {
6699
+ required: true,
6700
+ content: {
6701
+ "application/json": {
6702
+ schema: {
6703
+ $ref: "#/components/schemas/CreateProjectInput"
6704
+ }
6705
+ }
6706
+ }
6707
+ },
6708
+ responses: {
6709
+ "200": {
6710
+ description: "ok",
6711
+ content: {
6712
+ "application/json": {
6713
+ schema: {
6714
+ $ref: "#/components/schemas/MutationOk"
6715
+ }
6716
+ }
6717
+ }
6718
+ }
6719
+ }
6720
+ }
6721
+ },
6722
+ "/v1/project-registry/{id}": {
6723
+ delete: {
6724
+ operationId: "deleteProject",
6725
+ summary: "Delete a registered project",
6726
+ parameters: [
6727
+ {
6728
+ name: "id",
6729
+ in: "path",
6730
+ required: true,
6731
+ schema: {
6732
+ type: "string"
6733
+ }
6734
+ }
6735
+ ],
6736
+ responses: {
6737
+ "200": {
6738
+ description: "ok",
6739
+ content: {
6740
+ "application/json": {
6741
+ schema: {
6742
+ $ref: "#/components/schemas/MutationOk"
6743
+ }
6744
+ }
6745
+ }
6746
+ }
6747
+ }
6748
+ }
4459
6749
  },
4460
- "/v1/project-registry/{id}": { delete: { operationId: "deleteProject", summary: "Delete a registered project", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
4461
- "/v1/sync": { post: { operationId: "sync", summary: "Trigger an ingest sync", requestBody: { required: false, content: { "application/json": { schema: { type: "object", properties: { sources: { type: "string" } } } } } }, responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } }
6750
+ "/v1/sync": {
6751
+ post: {
6752
+ operationId: "sync",
6753
+ summary: "Trigger an ingest sync",
6754
+ requestBody: {
6755
+ required: false,
6756
+ content: {
6757
+ "application/json": {
6758
+ schema: {
6759
+ type: "object",
6760
+ properties: {
6761
+ sources: {
6762
+ type: "string"
6763
+ }
6764
+ }
6765
+ }
6766
+ }
6767
+ }
6768
+ },
6769
+ responses: {
6770
+ "200": {
6771
+ description: "ok",
6772
+ content: {
6773
+ "application/json": {
6774
+ schema: {
6775
+ $ref: "#/components/schemas/Envelope"
6776
+ }
6777
+ }
6778
+ }
6779
+ }
6780
+ }
6781
+ }
6782
+ },
6783
+ "/v1/ingest": {
6784
+ post: {
6785
+ operationId: "ingest",
6786
+ summary: "Bulk-import local rows into the cloud DB (merge by primary key, idempotent)",
6787
+ requestBody: {
6788
+ required: true,
6789
+ content: {
6790
+ "application/json": {
6791
+ schema: {
6792
+ type: "object",
6793
+ properties: {
6794
+ requests: {
6795
+ type: "array",
6796
+ items: {
6797
+ type: "object"
6798
+ }
6799
+ },
6800
+ sessions: {
6801
+ type: "array",
6802
+ items: {
6803
+ type: "object"
6804
+ }
6805
+ },
6806
+ projects: {
6807
+ type: "array",
6808
+ items: {
6809
+ type: "object"
6810
+ }
6811
+ },
6812
+ budgets: {
6813
+ type: "array",
6814
+ items: {
6815
+ type: "object"
6816
+ }
6817
+ },
6818
+ goals: {
6819
+ type: "array",
6820
+ items: {
6821
+ type: "object"
6822
+ }
6823
+ },
6824
+ billing_daily: {
6825
+ type: "array",
6826
+ items: {
6827
+ type: "object"
6828
+ }
6829
+ },
6830
+ model_pricing: {
6831
+ type: "array",
6832
+ items: {
6833
+ type: "object"
6834
+ }
6835
+ },
6836
+ subscriptions: {
6837
+ type: "array",
6838
+ items: {
6839
+ type: "object"
6840
+ }
6841
+ },
6842
+ usage_snapshots: {
6843
+ type: "array",
6844
+ items: {
6845
+ type: "object"
6846
+ }
6847
+ }
6848
+ }
6849
+ }
6850
+ }
6851
+ }
6852
+ },
6853
+ responses: {
6854
+ "200": {
6855
+ description: "ok",
6856
+ content: {
6857
+ "application/json": {
6858
+ schema: {
6859
+ $ref: "#/components/schemas/Envelope"
6860
+ }
6861
+ }
6862
+ }
6863
+ }
6864
+ }
6865
+ }
6866
+ }
4462
6867
  }
4463
6868
  };
4464
6869
 
@@ -4478,6 +6883,8 @@ var CORS = {
4478
6883
  "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
4479
6884
  "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Economy-Token"
4480
6885
  };
6886
+ var APP = "economy";
6887
+ var ECONOMY_WRITE_SCOPE = `${APP}:write`;
4481
6888
  var AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
4482
6889
  var SYNC_SOURCES = ["all", ...AGENTS];
4483
6890
  var DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
@@ -4523,6 +6930,11 @@ function optionalAgent(value) {
4523
6930
  return null;
4524
6931
  return typeof value === "string" && AGENTS.includes(value) ? value : undefined;
4525
6932
  }
6933
+ function requiredScopesForRequest(method, path) {
6934
+ if (method === "POST" && path === "/api/ingest")
6935
+ return [ECONOMY_WRITE_SCOPE];
6936
+ return;
6937
+ }
4526
6938
  function stringArray(value) {
4527
6939
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
4528
6940
  }
@@ -4585,7 +6997,11 @@ function createHandler(db, options = {}) {
4585
6997
  return json(openApiDocument());
4586
6998
  }
4587
6999
  if (options.authenticator) {
4588
- const decision = await options.authenticator.authenticate(req.headers, { method, path: rawPath });
7000
+ const decision = await options.authenticator.authenticate(req.headers, {
7001
+ method,
7002
+ path: rawPath,
7003
+ requiredScopes: requiredScopesForRequest(method, path)
7004
+ });
4589
7005
  if (!decision.ok)
4590
7006
  return json({ error: decision.reason ?? "unauthorized", message: decision.message }, decision.status || 401);
4591
7007
  } else if (!isAuthorizedRequest(req, path)) {
@@ -4837,15 +7253,22 @@ function createHandler(db, options = {}) {
4837
7253
  } catch {}
4838
7254
  return ok(results);
4839
7255
  }
7256
+ if (path === "/api/ingest" && method === "POST") {
7257
+ const body = await jsonBody(req);
7258
+ if (!body)
7259
+ return err("invalid JSON body");
7260
+ return ok(bulkIngest(db, body));
7261
+ }
4840
7262
  if (path === "/api/usage" && method === "GET") {
4841
7263
  const period = url.searchParams.get("period") ?? "month";
4842
- const agent = url.searchParams.get("agent") ?? undefined;
7264
+ const agentParam = url.searchParams.get("agent") ?? undefined;
7265
+ const agent = agentParam && isAgent(agentParam) ? agentParam : undefined;
4843
7266
  return ok({
4844
7267
  snapshots: queryUsageSnapshots(db, {
4845
- agent: agent && isAgent(agent) ? agent : undefined,
7268
+ agent,
4846
7269
  ...usageSnapshotFilterForPeriod(period)
4847
7270
  }),
4848
- summary: querySummary(db, period, undefined, true)
7271
+ summary: querySummary(db, period, undefined, true, agent)
4849
7272
  });
4850
7273
  }
4851
7274
  if (path === "/api/savings" && method === "GET") {
@@ -4853,6 +7276,47 @@ function createHandler(db, options = {}) {
4853
7276
  const agent = url.searchParams.get("agent") ?? undefined;
4854
7277
  return ok(querySavingsSummary(db, period, agent && isAgent(agent) ? agent : undefined));
4855
7278
  }
7279
+ if (path === "/api/brief" && method === "GET") {
7280
+ const since = url.searchParams.get("since") ?? undefined;
7281
+ const machine = url.searchParams.get("machine") ?? undefined;
7282
+ try {
7283
+ return ok(buildBrief(db, { since, machine }));
7284
+ } catch (e) {
7285
+ return err(e instanceof Error ? e.message : String(e));
7286
+ }
7287
+ }
7288
+ if (path === "/api/projects/detail" && method === "GET") {
7289
+ const q = url.searchParams.get("q")?.trim();
7290
+ if (!q)
7291
+ return err("q is required");
7292
+ return ok(queryProjectDetail(db, q));
7293
+ }
7294
+ if (path === "/api/export" && method === "GET") {
7295
+ const type = url.searchParams.get("type") ?? "sessions";
7296
+ if (type !== "sessions" && type !== "requests")
7297
+ return err("type must be sessions or requests");
7298
+ const period = url.searchParams.get("period") ?? "month";
7299
+ return ok(queryExportRows(db, type, period));
7300
+ }
7301
+ if (path === "/api/compare" && method === "GET") {
7302
+ const from = url.searchParams.get("from")?.trim();
7303
+ const to = url.searchParams.get("to")?.trim();
7304
+ if (!from || !to)
7305
+ return err("from and to are required (YYYY-MM-DD)");
7306
+ return ok(queryRangeStats(db, from, to));
7307
+ }
7308
+ if (path === "/api/forecast" && method === "GET") {
7309
+ return ok(queryForecast(db));
7310
+ }
7311
+ if (path === "/api/efficiency" && method === "GET") {
7312
+ return ok(queryModelEfficiency(db));
7313
+ }
7314
+ if (path === "/api/requests" && method === "GET") {
7315
+ const since = url.searchParams.get("since")?.trim();
7316
+ if (!since)
7317
+ return err("since is required (ISO timestamp)");
7318
+ return ok(queryRequestsSince(db, since));
7319
+ }
4856
7320
  if (path === "/api/subscriptions" && method === "GET") {
4857
7321
  return ok(listSubscriptions(db));
4858
7322
  }
@@ -4943,7 +7407,6 @@ function createHandler(db, options = {}) {
4943
7407
  return err("Not found", 404);
4944
7408
  };
4945
7409
  }
4946
- var APP = "economy";
4947
7410
  function isLocalHost(host) {
4948
7411
  return ["127.0.0.1", "localhost", "::1"].includes(host);
4949
7412
  }
@@ -5032,10 +7495,10 @@ function parsePort(raw, label) {
5032
7495
  return value;
5033
7496
  }
5034
7497
  async function runMigrate() {
5035
- const { getCloudDatabaseUrl: getCloudDatabaseUrl3, createCloudPool: createCloudPool2, authClientFromPool: authClientFromPool2, cloudMigrations: cloudMigrations2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
7498
+ const { getCloudDatabaseUrl: getCloudDatabaseUrl2, createCloudPool: createCloudPool2, authClientFromPool: authClientFromPool2, cloudMigrations: cloudMigrations2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
5036
7499
  const { applyPgMigrations } = await import("@hasna/cloud");
5037
7500
  const { ApiKeyStore: ApiKeyStore2 } = await import("@hasna/contracts/auth");
5038
- const dsn = getCloudDatabaseUrl3();
7501
+ const dsn = getCloudDatabaseUrl2();
5039
7502
  if (!dsn)
5040
7503
  throw new Error("migrate requires a Postgres DSN: set HASNA_ECONOMY_DATABASE_URL (or ECONOMY_DATABASE_URL / DATABASE_URL)");
5041
7504
  const result = await applyPgMigrations(dsn, cloudMigrations2(), "economy");