@hasna/economy 0.2.42 → 0.3.0

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 (61) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dashboard/README.md +73 -0
  3. package/dist/cli/commands/brief.d.ts +4 -69
  4. package/dist/cli/commands/brief.d.ts.map +1 -1
  5. package/dist/cli/commands/extras.d.ts.map +1 -1
  6. package/dist/cli/commands/tui.d.ts +21 -5
  7. package/dist/cli/commands/tui.d.ts.map +1 -1
  8. package/dist/cli/commands/watch.d.ts.map +1 -1
  9. package/dist/cli/index.js +8253 -6105
  10. package/dist/db/cloud.d.ts +28 -0
  11. package/dist/db/cloud.d.ts.map +1 -0
  12. package/dist/db/database.d.ts +18 -0
  13. package/dist/db/database.d.ts.map +1 -1
  14. package/dist/db/pg-migrations.d.ts.map +1 -1
  15. package/dist/db/pg-sync-worker.d.ts +2 -0
  16. package/dist/db/pg-sync-worker.d.ts.map +1 -0
  17. package/dist/db/sync-pg.d.ts +23 -0
  18. package/dist/db/sync-pg.d.ts.map +1 -0
  19. package/dist/index.d.ts +1 -10
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1855 -1613
  22. package/dist/lib/analytics.d.ts +68 -0
  23. package/dist/lib/analytics.d.ts.map +1 -0
  24. package/dist/lib/brief.d.ts +72 -0
  25. package/dist/lib/brief.d.ts.map +1 -0
  26. package/dist/lib/cloud-storage.d.ts +48 -0
  27. package/dist/lib/cloud-storage.d.ts.map +1 -0
  28. package/dist/lib/contracts-client/index.d.ts +3 -0
  29. package/dist/lib/contracts-client/index.d.ts.map +1 -0
  30. package/dist/lib/contracts-client/mode.d.ts +20 -0
  31. package/dist/lib/contracts-client/mode.d.ts.map +1 -0
  32. package/dist/lib/contracts-client/storage.d.ts +83 -0
  33. package/dist/lib/contracts-client/storage.d.ts.map +1 -0
  34. package/dist/lib/contracts-client/transport.d.ts +148 -0
  35. package/dist/lib/contracts-client/transport.d.ts.map +1 -0
  36. package/dist/lib/pricing.d.ts +3 -0
  37. package/dist/lib/pricing.d.ts.map +1 -1
  38. package/dist/lib/store/index.d.ts +283 -0
  39. package/dist/lib/store/index.d.ts.map +1 -0
  40. package/dist/lib/sync-all.d.ts +0 -2
  41. package/dist/lib/sync-all.d.ts.map +1 -1
  42. package/dist/mcp/http.d.ts.map +1 -1
  43. package/dist/mcp/index.js +1934 -536
  44. package/dist/mcp/server.d.ts.map +1 -1
  45. package/dist/openapi.d.ts +3 -0
  46. package/dist/openapi.d.ts.map +1 -0
  47. package/dist/otel/index.js +21 -267
  48. package/dist/server/index.js +3145 -321
  49. package/dist/server/pg-sync-worker.js +31 -0
  50. package/dist/server/serve.d.ts +31 -1
  51. package/dist/server/serve.d.ts.map +1 -1
  52. package/dist/types/index.d.ts +0 -1
  53. package/dist/types/index.d.ts.map +1 -1
  54. package/package.json +9 -4
  55. package/dashboard/dist/assets/charts-BtCbslx4.js +0 -36
  56. package/dashboard/dist/assets/index-BDmtt3RI.css +0 -1
  57. package/dashboard/dist/assets/index-BFwRLqg7.js +0 -6
  58. package/dashboard/dist/assets/vendor-Cz8jb743.js +0 -53
  59. package/dashboard/dist/index.html +0 -16
  60. package/dashboard/dist/logo.jpg +0 -0
  61. 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,10 +2228,103 @@ 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
 
2263
+ // src/db/cloud.ts
2264
+ var exports_cloud = {};
2265
+ __export(exports_cloud, {
2266
+ resolveSigningSecret: () => resolveSigningSecret,
2267
+ openCloudDatabase: () => openCloudDatabase,
2268
+ isCloudMode: () => isCloudMode,
2269
+ getCloudDatabaseUrl: () => getCloudDatabaseUrl,
2270
+ createCloudPool: () => createCloudPool,
2271
+ cloudMigrations: () => cloudMigrations,
2272
+ authClientFromPool: () => authClientFromPool
2273
+ });
2274
+ import pg from "pg";
2275
+ function getCloudDatabaseUrl() {
2276
+ return process.env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || process.env["ECONOMY_DATABASE_URL"]?.trim() || process.env["DATABASE_URL"]?.trim() || undefined;
2277
+ }
2278
+ function isCloudMode() {
2279
+ const mode = process.env["HASNA_ECONOMY_STORAGE_MODE"]?.trim().toLowerCase();
2280
+ if (mode === "cloud")
2281
+ return true;
2282
+ if (mode === "local")
2283
+ return false;
2284
+ return Boolean(getCloudDatabaseUrl());
2285
+ }
2286
+ function resolveSigningSecret() {
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;
2288
+ }
2289
+ function openCloudDatabase(dsn = getCloudDatabaseUrl()) {
2290
+ if (!dsn) {
2291
+ throw new Error("economy cloud mode requires a Postgres DSN: set HASNA_ECONOMY_DATABASE_URL (or ECONOMY_DATABASE_URL / DATABASE_URL)");
2292
+ }
2293
+ return new SyncPgAdapter(dsn);
2294
+ }
2295
+ function createCloudPool(dsn = getCloudDatabaseUrl()) {
2296
+ if (!dsn)
2297
+ throw new Error("economy cloud mode requires a Postgres DSN (HASNA_ECONOMY_DATABASE_URL)");
2298
+ const ssl = dsn.includes("sslmode=require") || dsn.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
2299
+ const pool = new pg.Pool({ connectionString: dsn, ssl, max: Number(process.env["ECONOMY_PG_POOL_MAX"] ?? "5"), connectionTimeoutMillis: 1e4 });
2300
+ pool.on("error", (err) => {
2301
+ console.error(JSON.stringify({ evt: "pg_pool_error", message: err.message }));
2302
+ });
2303
+ return pool;
2304
+ }
2305
+ function authClientFromPool(pool) {
2306
+ return {
2307
+ async many(sql, params = []) {
2308
+ const res = await pool.query(sql, params);
2309
+ return res.rows;
2310
+ },
2311
+ async get(sql, params = []) {
2312
+ const res = await pool.query(sql, params);
2313
+ return res.rows[0] ?? null;
2314
+ },
2315
+ async execute(sql, params = []) {
2316
+ await pool.query(sql, params);
2317
+ }
2318
+ };
2319
+ }
2320
+ function cloudMigrations() {
2321
+ return PG_MIGRATIONS;
2322
+ }
2323
+ var init_cloud = __esm(() => {
2324
+ init_sync_pg();
2325
+ init_pg_migrations();
2326
+ });
2327
+
1850
2328
  // src/ingest/billing.ts
1851
2329
  var exports_billing = {};
1852
2330
  __export(exports_billing, {
@@ -2143,15 +2621,15 @@ var init_open_projects = __esm(() => {
2143
2621
  });
2144
2622
 
2145
2623
  // src/lib/config.ts
2146
- import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
2147
- import { dirname, join as join9 } from "path";
2624
+ import { existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
2625
+ import { dirname as dirname2, join as join10 } from "path";
2148
2626
  function getConfigPath() {
2149
- return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join9(getDataDir(), "config.json");
2627
+ return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join10(getDataDir(), "config.json");
2150
2628
  }
2151
2629
  function loadConfig() {
2152
2630
  try {
2153
2631
  const configPath = getConfigPath();
2154
- if (existsSync10(configPath)) {
2632
+ if (existsSync11(configPath)) {
2155
2633
  const raw = readFileSync10(configPath, "utf-8");
2156
2634
  return { ...DEFAULTS, ...JSON.parse(raw) };
2157
2635
  }
@@ -3904,157 +4382,10 @@ async function ingestCodexQuota(db, verbose = false) {
3904
4382
 
3905
4383
  // src/lib/sync-all.ts
3906
4384
  init_database();
3907
-
3908
- // src/lib/cloud-sync.ts
3909
- init_database();
3910
-
3911
- // src/lib/package-metadata.ts
3912
- import { readFileSync as readFileSync8 } from "fs";
3913
- var cachedMetadata = null;
3914
- function getPackageMetadata() {
3915
- if (cachedMetadata)
3916
- return cachedMetadata;
3917
- const raw = readFileSync8(new URL("../../package.json", import.meta.url), "utf8");
3918
- const parsed = JSON.parse(raw);
3919
- cachedMetadata = {
3920
- name: parsed.name ?? "@hasna/economy",
3921
- version: parsed.version ?? "0.0.0"
3922
- };
3923
- return cachedMetadata;
3924
- }
3925
- var packageMetadata = getPackageMetadata();
3926
-
3927
- // src/lib/cloud-sync.ts
3928
- var CLOUD_TABLES = [
3929
- "requests",
3930
- "sessions",
3931
- "projects",
3932
- "budgets",
3933
- "goals",
3934
- "model_pricing",
3935
- "billing_daily",
3936
- "subscriptions",
3937
- "usage_snapshots",
3938
- "savings_daily",
3939
- "machines",
3940
- "ingest_state"
3941
- ];
3942
- function getCloudDatabaseUrl() {
3943
- return process.env["ECONOMY_CLOUD_DATABASE_URL"] ?? process.env["HASNA_ECONOMY_CLOUD_DATABASE_URL"] ?? null;
3944
- }
3945
- function isCloudAutoEnabled() {
3946
- return process.env["ECONOMY_CLOUD_AUTO"] === "1" || process.env["ECONOMY_CLOUD_AUTO"] === "true";
3947
- }
3948
- function getCloudPullIntervalMinutes() {
3949
- const raw = process.env["ECONOMY_CLOUD_PULL_INTERVAL"];
3950
- if (!raw)
3951
- return 15;
3952
- const n = Number(raw);
3953
- return Number.isFinite(n) && n > 0 ? n : 15;
3954
- }
3955
- async function getCloudPg() {
3956
- const url = getCloudDatabaseUrl();
3957
- if (!url) {
3958
- throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
3959
- }
3960
- const { PgAdapterAsync } = await import("@hasna/cloud");
3961
- return new PgAdapterAsync(url);
3962
- }
3963
- async function runCloudMigrations(cloud) {
3964
- const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
3965
- for (const sql of PG_MIGRATIONS2) {
3966
- await cloud.run(sql);
3967
- }
3968
- }
3969
- async function cloudPush(opts) {
3970
- const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
3971
- const cloud = await getCloudPg();
3972
- const local = new SqliteAdapter(getDbPath());
3973
- await runCloudMigrations(cloud);
3974
- const tables = opts?.tables ?? [...CLOUD_TABLES];
3975
- const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
3976
- const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
3977
- touchMachineRegistry(local, "push");
3978
- local.close();
3979
- await cloud.close();
3980
- return { rows, machine: getMachineId() };
3981
- }
3982
- async function cloudPull(opts) {
3983
- const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
3984
- const cloud = await getCloudPg();
3985
- const local = new SqliteAdapter(getDbPath());
3986
- await runCloudMigrations(cloud);
3987
- const tables = opts?.tables ?? [...CLOUD_TABLES];
3988
- const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
3989
- const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
3990
- touchMachineRegistry(local, "pull");
3991
- local.close();
3992
- await cloud.close();
3993
- setLastCloudPull();
3994
- return { rows, machine: getMachineId() };
3995
- }
3996
- function setLastCloudPull(at = new Date().toISOString()) {
3997
- const db = openDatabase();
3998
- db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
3999
- }
4000
- function getLastCloudPull() {
4001
- const db = openDatabase();
4002
- const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
4003
- return row?.value ?? null;
4004
- }
4005
- function shouldPullFromCloud() {
4006
- if (!getCloudDatabaseUrl())
4007
- return false;
4008
- const last = getLastCloudPull();
4009
- if (!last)
4010
- return true;
4011
- const ageMs = Date.now() - new Date(last).getTime();
4012
- return ageMs > getCloudPullIntervalMinutes() * 60000;
4013
- }
4014
- async function maybePullFromCloud() {
4015
- if (!shouldPullFromCloud())
4016
- return false;
4017
- try {
4018
- await cloudPull();
4019
- return true;
4020
- } catch {
4021
- return false;
4022
- }
4023
- }
4024
- async function maybePushAfterIngest() {
4025
- if (!isCloudAutoEnabled() || !getCloudDatabaseUrl())
4026
- return false;
4027
- try {
4028
- await cloudPush();
4029
- return true;
4030
- } catch {
4031
- return false;
4032
- }
4033
- }
4034
- function touchMachineRegistry(db, direction) {
4035
- const now = new Date().toISOString();
4036
- const machine = getMachineId();
4037
- db.prepare(`
4038
- INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
4039
- VALUES (?, ?, ?, ?, ?, ?, ?)
4040
- ON CONFLICT(machine_id) DO UPDATE SET
4041
- hostname = excluded.hostname,
4042
- last_seen_at = excluded.last_seen_at,
4043
- last_push_at = CASE WHEN ? = 'push' THEN excluded.last_push_at ELSE machines.last_push_at END,
4044
- last_pull_at = CASE WHEN ? = 'pull' THEN excluded.last_pull_at ELSE machines.last_pull_at END,
4045
- economy_version = excluded.economy_version,
4046
- updated_at = excluded.updated_at
4047
- `).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
4048
- }
4049
-
4050
- // src/lib/sync-all.ts
4051
4385
  async function syncAll(db, opts = {}) {
4052
4386
  const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
4053
4387
  const all = !anySpecific;
4054
- const useCloud = opts.cloud !== false;
4055
- if (useCloud)
4056
- await maybePullFromCloud();
4057
- const result = { deduped: 0, cloudPulled: false, cloudPushed: false };
4388
+ const result = { deduped: 0 };
4058
4389
  if (all || opts.claude) {
4059
4390
  result.claude = await ingestClaude(db, opts.verbose);
4060
4391
  result.claudeQuota = await ingestClaudeQuota(db, opts.verbose);
@@ -4077,158 +4408,2505 @@ async function syncAll(db, opts = {}) {
4077
4408
  result.hermes = await ingestHermes(db, opts.verbose);
4078
4409
  if (opts.dedupe !== false)
4079
4410
  result.deduped = dedupeRequests(db);
4080
- if (useCloud)
4081
- result.cloudPushed = await maybePushAfterIngest();
4082
4411
  return result;
4083
4412
  }
4084
4413
 
4085
- // src/lib/periods.ts
4086
- function ymd(date) {
4087
- return date.toISOString().substring(0, 10);
4088
- }
4089
- function usageSnapshotFilterForPeriod(period) {
4090
- const now = new Date;
4091
- switch (period) {
4092
- case "today":
4093
- return { date: ymd(now) };
4094
- case "yesterday": {
4095
- const yesterday = new Date(now);
4096
- yesterday.setUTCDate(yesterday.getUTCDate() - 1);
4097
- return { date: ymd(yesterday) };
4098
- }
4099
- case "week": {
4100
- const weekAgo = new Date(now);
4101
- weekAgo.setUTCDate(weekAgo.getUTCDate() - 7);
4102
- return { since: ymd(weekAgo) };
4103
- }
4104
- case "month":
4105
- return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1))) };
4106
- case "year":
4107
- return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), 0, 1))) };
4108
- case "all":
4109
- return {};
4110
- }
4111
- }
4112
-
4113
- // src/lib/billing-diff.ts
4414
+ // src/lib/brief.ts
4114
4415
  init_database();
4115
- var PROVIDER_TO_AGENT = {
4116
- anthropic: "claude",
4117
- openai: "codex",
4118
- gemini: "gemini",
4119
- google: "gemini"
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
4120
4429
  };
4121
- function queryBillingDiff(db, period, thresholdPct = 15) {
4122
- const estimated = querySummary(db, period, undefined, true);
4123
- const actual = queryBillingSummary(db, period);
4124
- const delta = estimated.total_usd - actual.total_usd;
4125
- const deltaPct = actual.total_usd > 0 ? delta / actual.total_usd * 100 : 0;
4126
- const agentRows = db.prepare(`
4127
- SELECT agent, COALESCE(SUM(cost_usd), 0) as estimated_usd
4128
- FROM requests
4129
- WHERE ${periodWhere3(period, "timestamp")}
4130
- GROUP BY agent
4131
- `).all();
4132
- const by_agent = agentRows.map((row) => {
4133
- const provider = Object.entries(PROVIDER_TO_AGENT).find(([, a]) => a === row.agent)?.[0];
4134
- const actualUsd = provider ? actual.by_provider[provider] ?? 0 : 0;
4135
- const rowDelta = row.estimated_usd - actualUsd;
4136
- const rowPct = actualUsd > 0 ? rowDelta / actualUsd * 100 : 0;
4137
- return {
4138
- agent: row.agent,
4139
- estimated_usd: row.estimated_usd,
4140
- actual_usd: actualUsd,
4141
- delta_usd: rowDelta,
4142
- delta_pct: rowPct
4143
- };
4144
- }).sort((a, b) => Math.abs(b.delta_usd) - Math.abs(a.delta_usd));
4145
- return {
4146
- period,
4147
- estimated_usd: estimated.total_usd,
4148
- actual_usd: actual.total_usd,
4149
- delta_usd: delta,
4150
- delta_pct: deltaPct,
4151
- threshold_pct: thresholdPct,
4152
- is_alert: Math.abs(deltaPct) > thresholdPct,
4153
- by_agent,
4154
- by_provider: actual.by_provider
4155
- };
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;
4156
4458
  }
4157
- function periodWhere3(period, column) {
4158
- switch (period) {
4159
- case "today":
4160
- return `DATE(${column}) = DATE('now')`;
4161
- case "yesterday":
4162
- return `DATE(${column}) = DATE('now', '-1 day')`;
4163
- case "week":
4164
- return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
4165
- case "month":
4166
- return `${column} >= DATE('now', 'start of month')`;
4167
- case "year":
4168
- return `${column} >= DATE('now', 'start of year')`;
4169
- case "all":
4170
- return "1=1";
4171
- }
4459
+ function requestPeriodWhere2(period) {
4460
+ if (period === "today")
4461
+ return `DATE(timestamp) = DATE('now')`;
4462
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
4172
4463
  }
4173
-
4174
- // src/server/serve.ts
4175
- init_database();
4176
-
4177
- // src/lib/serve-auth.ts
4178
- function getServeApiToken() {
4179
- return process.env["ECONOMY_API_TOKEN"] ?? process.env["HASNA_ECONOMY_API_TOKEN"];
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')`;
4180
4468
  }
4181
- function getServeBindHost() {
4182
- const explicit = process.env["ECONOMY_BIND"] ?? process.env["ECONOMY_HOST"];
4183
- if (explicit)
4184
- return explicit;
4185
- return getServeApiToken() ? "127.0.0.1" : "0.0.0.0";
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
+ };
4186
4486
  }
4187
- function isAuthorizedRequest(req, path) {
4188
- const token = getServeApiToken();
4189
- if (!token)
4190
- return true;
4191
- if (path === "/health")
4192
- return true;
4193
- const auth = req.headers.get("Authorization");
4194
- if (auth === `Bearer ${token}`)
4195
- return true;
4196
- if (req.headers.get("X-Economy-Token") === token)
4197
- return true;
4198
- return false;
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
+ };
4199
4495
  }
4200
-
4201
- // src/server/serve.ts
4202
- import { randomUUID } from "crypto";
4203
- import { existsSync as existsSync11 } from "fs";
4204
- import { resolve, sep } from "path";
4205
- var CORS = {
4206
- "Access-Control-Allow-Origin": "*",
4207
- "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
4208
- "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Economy-Token"
4209
- };
4210
- var AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
4211
- var SYNC_SOURCES = ["all", ...AGENTS];
4212
- var DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
4213
- function json(data, status = 200) {
4214
- return new Response(JSON.stringify(data), {
4215
- status,
4216
- headers: { "Content-Type": "application/json", ...CORS }
4217
- });
4496
+ function latestTimestamp(...values) {
4497
+ return values.filter(Boolean).sort().at(-1) ?? null;
4218
4498
  }
4219
- function ok(data, meta) {
4220
- return json({ data, meta: meta ?? {} });
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
+ };
4221
4513
  }
4222
- function err(message, status = 400) {
4223
- return json({ error: message }, status);
4514
+ function emptyTotals() {
4515
+ return combineTotals(ZERO_REQUEST_TOTALS, ZERO_SESSION_TOTALS);
4224
4516
  }
4225
- function normalizeBudgetPeriod(value) {
4226
- switch (value) {
4227
- case "day":
4228
- case "daily":
4229
- return "daily";
4230
- case "week":
4231
- case "weekly":
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
+
5134
+ // src/lib/periods.ts
5135
+ function ymd(date) {
5136
+ return date.toISOString().substring(0, 10);
5137
+ }
5138
+ function usageSnapshotFilterForPeriod(period) {
5139
+ const now = new Date;
5140
+ switch (period) {
5141
+ case "today":
5142
+ return { date: ymd(now) };
5143
+ case "yesterday": {
5144
+ const yesterday = new Date(now);
5145
+ yesterday.setUTCDate(yesterday.getUTCDate() - 1);
5146
+ return { date: ymd(yesterday) };
5147
+ }
5148
+ case "week": {
5149
+ const weekAgo = new Date(now);
5150
+ weekAgo.setUTCDate(weekAgo.getUTCDate() - 7);
5151
+ return { since: ymd(weekAgo) };
5152
+ }
5153
+ case "month":
5154
+ return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1))) };
5155
+ case "year":
5156
+ return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), 0, 1))) };
5157
+ case "all":
5158
+ return {};
5159
+ }
5160
+ }
5161
+
5162
+ // src/lib/billing-diff.ts
5163
+ init_database();
5164
+ var PROVIDER_TO_AGENT = {
5165
+ anthropic: "claude",
5166
+ openai: "codex",
5167
+ gemini: "gemini",
5168
+ google: "gemini"
5169
+ };
5170
+ function queryBillingDiff(db, period, thresholdPct = 15) {
5171
+ const estimated = querySummary(db, period, undefined, true);
5172
+ const actual = queryBillingSummary(db, period);
5173
+ const delta = estimated.total_usd - actual.total_usd;
5174
+ const deltaPct = actual.total_usd > 0 ? delta / actual.total_usd * 100 : 0;
5175
+ const agentRows = db.prepare(`
5176
+ SELECT agent, COALESCE(SUM(cost_usd), 0) as estimated_usd
5177
+ FROM requests
5178
+ WHERE ${periodWhere3(period, "timestamp")}
5179
+ GROUP BY agent
5180
+ `).all();
5181
+ const by_agent = agentRows.map((row) => {
5182
+ const provider = Object.entries(PROVIDER_TO_AGENT).find(([, a]) => a === row.agent)?.[0];
5183
+ const actualUsd = provider ? actual.by_provider[provider] ?? 0 : 0;
5184
+ const rowDelta = row.estimated_usd - actualUsd;
5185
+ const rowPct = actualUsd > 0 ? rowDelta / actualUsd * 100 : 0;
5186
+ return {
5187
+ agent: row.agent,
5188
+ estimated_usd: row.estimated_usd,
5189
+ actual_usd: actualUsd,
5190
+ delta_usd: rowDelta,
5191
+ delta_pct: rowPct
5192
+ };
5193
+ }).sort((a, b) => Math.abs(b.delta_usd) - Math.abs(a.delta_usd));
5194
+ return {
5195
+ period,
5196
+ estimated_usd: estimated.total_usd,
5197
+ actual_usd: actual.total_usd,
5198
+ delta_usd: delta,
5199
+ delta_pct: deltaPct,
5200
+ threshold_pct: thresholdPct,
5201
+ is_alert: Math.abs(deltaPct) > thresholdPct,
5202
+ by_agent,
5203
+ by_provider: actual.by_provider
5204
+ };
5205
+ }
5206
+ function periodWhere3(period, column) {
5207
+ switch (period) {
5208
+ case "today":
5209
+ return `DATE(${column}) = DATE('now')`;
5210
+ case "yesterday":
5211
+ return `DATE(${column}) = DATE('now', '-1 day')`;
5212
+ case "week":
5213
+ return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
5214
+ case "month":
5215
+ return `${column} >= DATE('now', 'start of month')`;
5216
+ case "year":
5217
+ return `${column} >= DATE('now', 'start of year')`;
5218
+ case "all":
5219
+ return "1=1";
5220
+ }
5221
+ }
5222
+
5223
+ // src/server/serve.ts
5224
+ init_database();
5225
+
5226
+ // src/lib/serve-auth.ts
5227
+ function getServeApiToken() {
5228
+ return process.env["ECONOMY_API_TOKEN"] ?? process.env["HASNA_ECONOMY_API_TOKEN"];
5229
+ }
5230
+ function getServeBindHost() {
5231
+ const explicit = process.env["ECONOMY_BIND"] ?? process.env["ECONOMY_HOST"];
5232
+ if (explicit)
5233
+ return explicit;
5234
+ return getServeApiToken() ? "127.0.0.1" : "0.0.0.0";
5235
+ }
5236
+ function isAuthorizedRequest(req, path) {
5237
+ const token = getServeApiToken();
5238
+ if (!token)
5239
+ return true;
5240
+ if (path === "/health")
5241
+ return true;
5242
+ const auth = req.headers.get("Authorization");
5243
+ if (auth === `Bearer ${token}`)
5244
+ return true;
5245
+ if (req.headers.get("X-Economy-Token") === token)
5246
+ return true;
5247
+ return false;
5248
+ }
5249
+
5250
+ // src/server/serve.ts
5251
+ import { randomUUID } from "crypto";
5252
+ import { existsSync as existsSync12 } from "fs";
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
5272
+ init_cloud();
5273
+ import { verifyApiKey, ApiKeyStore } from "@hasna/contracts/auth";
5274
+
5275
+ // src/openapi.ts
5276
+ var openApiSpec = {
5277
+ openapi: "3.0.3",
5278
+ info: {
5279
+ title: "@hasna/economy self-hosted API",
5280
+ version: "0.0.0",
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."
5282
+ },
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
+ ],
5301
+ components: {
5302
+ securitySchemes: {
5303
+ apiKey: {
5304
+ type: "apiKey",
5305
+ in: "header",
5306
+ name: "x-api-key"
5307
+ },
5308
+ bearer: {
5309
+ type: "http",
5310
+ scheme: "bearer"
5311
+ }
5312
+ },
5313
+ schemas: {
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
+ }
5616
+ }
5617
+ },
5618
+ paths: {
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
+ },
6330
+ "/v1/budgets": {
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
+ }
6414
+ },
6415
+ "/v1/goals": {
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
+ }
6499
+ },
6500
+ "/v1/pricing": {
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
+ }
6584
+ },
6585
+ "/v1/subscriptions": {
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
+ }
6669
+ },
6670
+ "/v1/project-registry": {
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
+ }
6749
+ },
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
+ }
6867
+ }
6868
+ };
6869
+
6870
+ // src/server/serve.ts
6871
+ function openApiDocument() {
6872
+ const spec = openApiSpec;
6873
+ return { ...spec, info: { ...spec["info"], version: packageMetadata.version } };
6874
+ }
6875
+ function serveMode() {
6876
+ return isCloudMode() ? "self_hosted" : "local";
6877
+ }
6878
+ function foundationEnvelope(status, extra = {}) {
6879
+ return { status, version: packageMetadata.version, mode: serveMode(), service: "economy", ...extra };
6880
+ }
6881
+ var CORS = {
6882
+ "Access-Control-Allow-Origin": "*",
6883
+ "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
6884
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Economy-Token"
6885
+ };
6886
+ var APP = "economy";
6887
+ var ECONOMY_WRITE_SCOPE = `${APP}:write`;
6888
+ var AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
6889
+ var SYNC_SOURCES = ["all", ...AGENTS];
6890
+ var DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
6891
+ function json(data, status = 200) {
6892
+ return new Response(JSON.stringify(data), {
6893
+ status,
6894
+ headers: { "Content-Type": "application/json", ...CORS }
6895
+ });
6896
+ }
6897
+ function ok(data, meta) {
6898
+ return json({ data, meta: meta ?? {} });
6899
+ }
6900
+ function err(message, status = 400) {
6901
+ return json({ error: message }, status);
6902
+ }
6903
+ function normalizeBudgetPeriod(value) {
6904
+ switch (value) {
6905
+ case "day":
6906
+ case "daily":
6907
+ return "daily";
6908
+ case "week":
6909
+ case "weekly":
4232
6910
  return "weekly";
4233
6911
  case "month":
4234
6912
  case "monthly":
@@ -4252,6 +6930,11 @@ function optionalAgent(value) {
4252
6930
  return null;
4253
6931
  return typeof value === "string" && AGENTS.includes(value) ? value : undefined;
4254
6932
  }
6933
+ function requiredScopesForRequest(method, path) {
6934
+ if (method === "POST" && path === "/api/ingest")
6935
+ return [ECONOMY_WRITE_SCOPE];
6936
+ return;
6937
+ }
4255
6938
  function stringArray(value) {
4256
6939
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
4257
6940
  }
@@ -4267,19 +6950,20 @@ function dashboardPath(root, pathname) {
4267
6950
  const filePath = resolve(rootPath, relativePath);
4268
6951
  return filePath === rootPath || filePath.startsWith(rootPath + sep) ? filePath : null;
4269
6952
  }
6953
+ var FOUNDATION_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz", "/version", "/openapi.json"]);
4270
6954
  function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
4271
6955
  return async function fetch2(req) {
4272
6956
  const url = new URL(req.url);
4273
- if (url.pathname.startsWith("/api") || url.pathname === "/health") {
6957
+ if (url.pathname.startsWith("/api") || url.pathname.startsWith("/v1") || FOUNDATION_PATHS.has(url.pathname)) {
4274
6958
  return apiHandler(req);
4275
6959
  }
4276
- if (existsSync11(dashboardDir)) {
6960
+ if (existsSync12(dashboardDir)) {
4277
6961
  const filePath = dashboardPath(dashboardDir, url.pathname);
4278
- if (filePath && existsSync11(filePath)) {
6962
+ if (filePath && existsSync12(filePath)) {
4279
6963
  return new Response(Bun.file(filePath));
4280
6964
  }
4281
6965
  const indexPath = dashboardPath(dashboardDir, "/");
4282
- if (indexPath && existsSync11(indexPath)) {
6966
+ if (indexPath && existsSync12(indexPath)) {
4283
6967
  return new Response(Bun.file(indexPath));
4284
6968
  }
4285
6969
  }
@@ -4291,15 +6975,38 @@ function applyFields(obj, fields) {
4291
6975
  return obj;
4292
6976
  return Object.fromEntries(fields.map((f) => [f, obj[f] ?? null]));
4293
6977
  }
4294
- function createHandler(db) {
6978
+ function createHandler(db, options = {}) {
4295
6979
  return async function handler(req) {
4296
6980
  const url = new URL(req.url);
4297
- const path = url.pathname;
6981
+ const rawPath = url.pathname;
6982
+ const path = rawPath.startsWith("/v1/") ? "/api" + rawPath.slice(3) : rawPath === "/v1" ? "/api" : rawPath;
4298
6983
  const method = req.method;
4299
6984
  if (method === "OPTIONS")
4300
6985
  return new Response(null, { status: 204, headers: CORS });
4301
- if (!isAuthorizedRequest(req, path))
6986
+ if (method === "GET" && (rawPath === "/health" || rawPath === "/healthz")) {
6987
+ return json(foundationEnvelope("ok"));
6988
+ }
6989
+ if (method === "GET" && (rawPath === "/version" || rawPath === "/v1/version")) {
6990
+ return json(foundationEnvelope("ok"));
6991
+ }
6992
+ if (method === "GET" && (rawPath === "/ready" || rawPath === "/readyz")) {
6993
+ const result = options.readyCheck ? await options.readyCheck() : { ready: true };
6994
+ return json(foundationEnvelope(result.ready ? "ready" : "not_ready", result.detail ? { detail: result.detail } : {}), result.ready ? 200 : 503);
6995
+ }
6996
+ if (method === "GET" && rawPath === "/openapi.json") {
6997
+ return json(openApiDocument());
6998
+ }
6999
+ if (options.authenticator) {
7000
+ const decision = await options.authenticator.authenticate(req.headers, {
7001
+ method,
7002
+ path: rawPath,
7003
+ requiredScopes: requiredScopesForRequest(method, path)
7004
+ });
7005
+ if (!decision.ok)
7006
+ return json({ error: decision.reason ?? "unauthorized", message: decision.message }, decision.status || 401);
7007
+ } else if (!isAuthorizedRequest(req, path)) {
4302
7008
  return err("Unauthorized", 401);
7009
+ }
4303
7010
  if (path === "/health")
4304
7011
  return ok({ status: "ok", ts: new Date().toISOString() });
4305
7012
  if (path === "/api/summary" && method === "GET") {
@@ -4546,15 +7253,22 @@ function createHandler(db) {
4546
7253
  } catch {}
4547
7254
  return ok(results);
4548
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
+ }
4549
7262
  if (path === "/api/usage" && method === "GET") {
4550
7263
  const period = url.searchParams.get("period") ?? "month";
4551
- const agent = url.searchParams.get("agent") ?? undefined;
7264
+ const agentParam = url.searchParams.get("agent") ?? undefined;
7265
+ const agent = agentParam && isAgent(agentParam) ? agentParam : undefined;
4552
7266
  return ok({
4553
7267
  snapshots: queryUsageSnapshots(db, {
4554
- agent: agent && isAgent(agent) ? agent : undefined,
7268
+ agent,
4555
7269
  ...usageSnapshotFilterForPeriod(period)
4556
7270
  }),
4557
- summary: querySummary(db, period, undefined, true)
7271
+ summary: querySummary(db, period, undefined, true, agent)
4558
7272
  });
4559
7273
  }
4560
7274
  if (path === "/api/savings" && method === "GET") {
@@ -4562,6 +7276,47 @@ function createHandler(db) {
4562
7276
  const agent = url.searchParams.get("agent") ?? undefined;
4563
7277
  return ok(querySavingsSummary(db, period, agent && isAgent(agent) ? agent : undefined));
4564
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
+ }
4565
7320
  if (path === "/api/subscriptions" && method === "GET") {
4566
7321
  return ok(listSubscriptions(db));
4567
7322
  }
@@ -4652,27 +7407,68 @@ function createHandler(db) {
4652
7407
  return err("Not found", 404);
4653
7408
  };
4654
7409
  }
7410
+ function isLocalHost(host) {
7411
+ return ["127.0.0.1", "localhost", "::1"].includes(host);
7412
+ }
4655
7413
  function startServer(port = 3456, options = {}) {
4656
- const db = options.db ?? openDatabase();
4657
- ensurePricingSeeded(db);
4658
- const apiHandler = createHandler(db);
4659
- const hostname2 = options.hostname ?? getServeBindHost();
7414
+ const cloud = options.db ? false : isCloudMode();
7415
+ const hostname2 = options.hostname ?? (cloud ? process.env["ECONOMY_HOST"] ?? "0.0.0.0" : getServeBindHost());
7416
+ const log = options.log ?? console.log;
7417
+ const handlerOptions = {};
7418
+ let db;
7419
+ if (cloud) {
7420
+ db = openCloudDatabase();
7421
+ const pool = createCloudPool();
7422
+ const signingSecret = resolveSigningSecret();
7423
+ if (signingSecret) {
7424
+ const keys = new ApiKeyStore(authClientFromPool(pool));
7425
+ keys.ensureSchema().catch((e) => log(`api_keys ensureSchema: ${e instanceof Error ? e.message : String(e)}`));
7426
+ handlerOptions.authenticator = verifyApiKey({
7427
+ app: APP,
7428
+ signingSecret,
7429
+ isRevoked: keys.statusChecker(),
7430
+ audit: (event) => log(JSON.stringify({ evt: "api_auth", outcome: event.outcome, kid: event.kid, reason: event.reason, path: event.path, status: event.status }))
7431
+ });
7432
+ } else if (!isLocalHost(hostname2)) {
7433
+ throw new Error("economy-serve on a non-local host requires an API signing secret (HASNA_ECONOMY_API_SIGNING_KEY / API_KEY_SIGNING_SECRET)");
7434
+ }
7435
+ handlerOptions.readyCheck = async () => {
7436
+ try {
7437
+ const res = await pool.query("SELECT to_regclass($1) AS reg", ["public.requests"]);
7438
+ if (!res.rows[0]?.reg)
7439
+ return { ready: false, detail: "pending_migrations:requests" };
7440
+ return { ready: true };
7441
+ } catch (error) {
7442
+ return { ready: false, detail: error instanceof Error ? error.message : "storage_unreachable" };
7443
+ }
7444
+ };
7445
+ } else {
7446
+ db = options.db ?? openDatabase();
7447
+ ensurePricingSeeded(db);
7448
+ }
7449
+ const apiHandler = createHandler(db, handlerOptions);
4660
7450
  const server = Bun.serve({
4661
7451
  port,
4662
7452
  hostname: hostname2,
4663
7453
  fetch: createServerFetch(apiHandler, options.dashboardDir)
4664
7454
  });
4665
7455
  const address = `http://${hostname2 === "0.0.0.0" ? "localhost" : hostname2}:${server.port}`;
4666
- const log = options.log ?? console.log;
4667
7456
  log(`economy-serve listening on ${address}`);
4668
7457
  return server;
4669
7458
  }
4670
7459
 
4671
7460
  // src/server/index.ts
4672
7461
  function printHelp() {
4673
- console.log(`Usage: economy-serve [options]
7462
+ console.log(`Usage: economy-serve [command] [options]
4674
7463
 
4675
7464
  REST API server for ${packageMetadata.name}
7465
+ Foundation probes: GET /health, /ready, /version -> { status, version, mode }
7466
+ Versioned API: /v1/* (API-key auth via @hasna/contracts in cloud mode)
7467
+
7468
+ Commands:
7469
+ (default) start the HTTP server
7470
+ migrate apply the cloud Postgres schema + api_keys, then exit
7471
+ version print { status, version, mode }
4676
7472
 
4677
7473
  Options:
4678
7474
  -p, --port <port> Port to bind (default: ECONOMY_PORT or 3456)
@@ -4698,18 +7494,46 @@ function parsePort(raw, label) {
4698
7494
  }
4699
7495
  return value;
4700
7496
  }
7497
+ async function runMigrate() {
7498
+ const { getCloudDatabaseUrl: getCloudDatabaseUrl2, createCloudPool: createCloudPool2, authClientFromPool: authClientFromPool2, cloudMigrations: cloudMigrations2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
7499
+ const { applyPgMigrations } = await import("@hasna/cloud");
7500
+ const { ApiKeyStore: ApiKeyStore2 } = await import("@hasna/contracts/auth");
7501
+ const dsn = getCloudDatabaseUrl2();
7502
+ if (!dsn)
7503
+ throw new Error("migrate requires a Postgres DSN: set HASNA_ECONOMY_DATABASE_URL (or ECONOMY_DATABASE_URL / DATABASE_URL)");
7504
+ const result = await applyPgMigrations(dsn, cloudMigrations2(), "economy");
7505
+ const pool = createCloudPool2(dsn);
7506
+ try {
7507
+ await new ApiKeyStore2(authClientFromPool2(pool)).ensureSchema();
7508
+ } finally {
7509
+ await pool.end().catch(() => {});
7510
+ }
7511
+ console.log(JSON.stringify({ evt: "migrate", service: result.service, applied: result.applied, alreadyApplied: result.alreadyApplied, errors: result.errors, api_keys: "ensured" }));
7512
+ if (result.errors.length)
7513
+ process.exit(1);
7514
+ }
4701
7515
  var args = process.argv.slice(2);
4702
- if (args.includes("--help") || args.includes("-h")) {
4703
- printHelp();
7516
+ var sub = args[0];
7517
+ if (sub === "version") {
7518
+ console.log(JSON.stringify({ status: "ok", version: packageMetadata.version, mode: process.env["HASNA_ECONOMY_STORAGE_MODE"] === "cloud" ? "self_hosted" : "local" }));
4704
7519
  process.exit(0);
4705
7520
  }
4706
- if (args.includes("--version") || args.includes("-V")) {
7521
+ if (sub === "migrate") {
7522
+ runMigrate().catch((error) => {
7523
+ console.error(error instanceof Error ? error.message : String(error));
7524
+ process.exit(1);
7525
+ });
7526
+ } else if (args.includes("--help") || args.includes("-h")) {
7527
+ printHelp();
7528
+ process.exit(0);
7529
+ } else if (args.includes("--version") || args.includes("-V")) {
4707
7530
  console.log(packageMetadata.version);
4708
7531
  process.exit(0);
4709
- }
4710
- try {
4711
- startServer(resolvePort(args));
4712
- } catch (error) {
4713
- console.error(error instanceof Error ? error.message : String(error));
4714
- process.exit(1);
7532
+ } else {
7533
+ try {
7534
+ startServer(resolvePort(args));
7535
+ } catch (error) {
7536
+ console.error(error instanceof Error ? error.message : String(error));
7537
+ process.exit(1);
7538
+ }
4715
7539
  }