@hasna/economy 0.4.0 → 0.5.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 (202) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +12 -9
  3. package/dist/chunks/agent-registry-store-tpnhx3wj.js +39 -0
  4. package/dist/chunks/billing-bkpxy4kv.js +13 -0
  5. package/dist/chunks/config-j9mv3ehk.js +15 -0
  6. package/dist/chunks/index-2hwzmfgv.js +3407 -0
  7. package/dist/chunks/index-axh32d30.js +1313 -0
  8. package/dist/chunks/index-emvs5ph2.js +2085 -0
  9. package/dist/chunks/index-ez9thsr7.js +3440 -0
  10. package/dist/chunks/index-hesxq90g.js +62 -0
  11. package/dist/chunks/index-k540hfww.js +258 -0
  12. package/dist/chunks/index-kgy9b6bf.js +451 -0
  13. package/dist/chunks/index-kn5572kd.js +124 -0
  14. package/dist/chunks/index-p35cyhc4.js +4 -0
  15. package/dist/chunks/index-w3bvv0ns.js +26 -0
  16. package/dist/chunks/index-zpvea08j.js +3372 -0
  17. package/dist/chunks/menubar-6xtkt17h.js +296 -0
  18. package/dist/chunks/open-projects-6z4pq6mx.js +39 -0
  19. package/dist/chunks/pricing-f80p81gr.js +29 -0
  20. package/dist/chunks/pricing-vf2pycmh.js +30 -0
  21. package/dist/chunks/pricing-y21mcjsa.js +30 -0
  22. package/dist/chunks/serve-0k69e4h9.js +4655 -0
  23. package/dist/chunks/sqlite-store-501f32ts.js +258 -0
  24. package/dist/chunks/sqlite-store-fq4natnf.js +256 -0
  25. package/dist/chunks/sqlite-store-st99garx.js +258 -0
  26. package/dist/chunks/third-party-sqlite-5e85fxa2.js +11 -0
  27. package/dist/chunks/tui-7f8f951w.js +22 -0
  28. package/dist/chunks/watch-mm1j39vy.js +196 -0
  29. package/dist/chunks/webhooks-k4fh2t9c.js +109 -0
  30. package/dist/cli/commands/brief.d.ts.map +1 -1
  31. package/dist/cli/commands/extras.d.ts.map +1 -1
  32. package/dist/cli/commands/journal.d.ts +3 -0
  33. package/dist/cli/commands/journal.d.ts.map +1 -0
  34. package/dist/cli/commands/menubar.d.ts +97 -4
  35. package/dist/cli/commands/menubar.d.ts.map +1 -1
  36. package/dist/cli/commands/tui.d.ts +1 -1
  37. package/dist/cli/commands/tui.d.ts.map +1 -1
  38. package/dist/cli/commands/watch.d.ts.map +1 -1
  39. package/dist/cli/index.js +1358 -10114
  40. package/dist/db/cloud.d.ts +7 -9
  41. package/dist/db/cloud.d.ts.map +1 -1
  42. package/dist/db/database.d.ts +14 -12
  43. package/dist/db/database.d.ts.map +1 -1
  44. package/dist/db/ingest-concurrency-worker.d.ts +2 -0
  45. package/dist/db/ingest-concurrency-worker.d.ts.map +1 -0
  46. package/dist/db/ingest-errors.d.ts +9 -0
  47. package/dist/db/ingest-errors.d.ts.map +1 -0
  48. package/dist/db/ingest-schema.d.ts +3 -0
  49. package/dist/db/ingest-schema.d.ts.map +1 -0
  50. package/dist/db/ingest-specs.d.ts +8 -0
  51. package/dist/db/ingest-specs.d.ts.map +1 -0
  52. package/dist/db/ingest-test-contract.d.ts +3 -0
  53. package/dist/db/ingest-test-contract.d.ts.map +1 -0
  54. package/dist/db/ingest-validation.d.ts +12 -0
  55. package/dist/db/ingest-validation.d.ts.map +1 -0
  56. package/dist/db/ingest.d.ts +27 -0
  57. package/dist/db/ingest.d.ts.map +1 -0
  58. package/dist/db/legacy-schema.d.ts +7 -0
  59. package/dist/db/legacy-schema.d.ts.map +1 -0
  60. package/dist/db/observation-contract.d.ts +3 -0
  61. package/dist/db/observation-contract.d.ts.map +1 -0
  62. package/dist/db/observation-provenance.d.ts +23 -0
  63. package/dist/db/observation-provenance.d.ts.map +1 -0
  64. package/dist/db/observation-schema.d.ts +24 -0
  65. package/dist/db/observation-schema.d.ts.map +1 -0
  66. package/dist/db/pg-connection.d.ts +10 -0
  67. package/dist/db/pg-connection.d.ts.map +1 -0
  68. package/dist/db/pg-migrate.d.ts +4 -9
  69. package/dist/db/pg-migrate.d.ts.map +1 -1
  70. package/dist/db/pg-migrations.d.ts.map +1 -1
  71. package/dist/db/pg-protocol.d.ts +28 -0
  72. package/dist/db/pg-protocol.d.ts.map +1 -0
  73. package/dist/db/sqlite-store.d.ts +4 -0
  74. package/dist/db/sqlite-store.d.ts.map +1 -0
  75. package/dist/db/sync-pg.d.ts +17 -21
  76. package/dist/db/sync-pg.d.ts.map +1 -1
  77. package/dist/db/third-party-sqlite.d.ts +10 -0
  78. package/dist/db/third-party-sqlite.d.ts.map +1 -0
  79. package/dist/index.d.ts +3 -0
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +1161 -2464
  82. package/dist/ingest/claude-quota.d.ts.map +1 -1
  83. package/dist/ingest/claude.d.ts.map +1 -1
  84. package/dist/ingest/codex-quota.d.ts.map +1 -1
  85. package/dist/ingest/codex.d.ts +25 -0
  86. package/dist/ingest/codex.d.ts.map +1 -1
  87. package/dist/ingest/collection-outcome.d.ts +17 -0
  88. package/dist/ingest/collection-outcome.d.ts.map +1 -0
  89. package/dist/ingest/cursor.d.ts.map +1 -1
  90. package/dist/ingest/gemini.d.ts.map +1 -1
  91. package/dist/ingest/hermes.d.ts +1 -1
  92. package/dist/ingest/hermes.d.ts.map +1 -1
  93. package/dist/ingest/loops.d.ts.map +1 -1
  94. package/dist/ingest/opencode.d.ts +1 -1
  95. package/dist/ingest/opencode.d.ts.map +1 -1
  96. package/dist/ingest/pi.d.ts +1 -1
  97. package/dist/ingest/pi.d.ts.map +1 -1
  98. package/dist/ingest/quota-observation.d.ts +16 -0
  99. package/dist/ingest/quota-observation.d.ts.map +1 -0
  100. package/dist/ingest/source-time.d.ts +4 -0
  101. package/dist/ingest/source-time.d.ts.map +1 -0
  102. package/dist/lib/accounts-store.d.ts +39 -13
  103. package/dist/lib/accounts-store.d.ts.map +1 -1
  104. package/dist/lib/analytics.d.ts.map +1 -1
  105. package/dist/lib/api-display-url.d.ts +1 -1
  106. package/dist/lib/autosync-gate.d.ts +12 -5
  107. package/dist/lib/autosync-gate.d.ts.map +1 -1
  108. package/dist/lib/billing-diff.d.ts +18 -40
  109. package/dist/lib/billing-diff.d.ts.map +1 -1
  110. package/dist/lib/brief.d.ts +2 -0
  111. package/dist/lib/brief.d.ts.map +1 -1
  112. package/dist/lib/budget-decimal.d.ts +14 -0
  113. package/dist/lib/budget-decimal.d.ts.map +1 -0
  114. package/dist/lib/cloud-ingest.d.ts +15 -0
  115. package/dist/lib/cloud-ingest.d.ts.map +1 -1
  116. package/dist/lib/cloud-storage.d.ts.map +1 -1
  117. package/dist/lib/ingest-batches.d.ts +21 -0
  118. package/dist/lib/ingest-batches.d.ts.map +1 -0
  119. package/dist/lib/ingest-journal-client.d.ts +37 -0
  120. package/dist/lib/ingest-journal-client.d.ts.map +1 -0
  121. package/dist/lib/ingest-journal-lock.d.ts +5 -0
  122. package/dist/lib/ingest-journal-lock.d.ts.map +1 -0
  123. package/dist/lib/ingest-journal-maintenance.d.ts +92 -0
  124. package/dist/lib/ingest-journal-maintenance.d.ts.map +1 -0
  125. package/dist/lib/ingest-journal-operation.d.ts +55 -0
  126. package/dist/lib/ingest-journal-operation.d.ts.map +1 -0
  127. package/dist/lib/ingest-journal.d.ts +73 -0
  128. package/dist/lib/ingest-journal.d.ts.map +1 -0
  129. package/dist/lib/money-display.d.ts +13 -0
  130. package/dist/lib/money-display.d.ts.map +1 -0
  131. package/dist/lib/periods.d.ts +12 -0
  132. package/dist/lib/periods.d.ts.map +1 -1
  133. package/dist/lib/pricing.d.ts +14 -0
  134. package/dist/lib/pricing.d.ts.map +1 -1
  135. package/dist/lib/savings.d.ts +64 -7
  136. package/dist/lib/savings.d.ts.map +1 -1
  137. package/dist/lib/store/index.d.ts +25 -21
  138. package/dist/lib/store/index.d.ts.map +1 -1
  139. package/dist/lib/summary-contract.d.ts +32 -0
  140. package/dist/lib/summary-contract.d.ts.map +1 -0
  141. package/dist/lib/summary-test-contract.d.ts +3 -0
  142. package/dist/lib/summary-test-contract.d.ts.map +1 -0
  143. package/dist/lib/sync-all.d.ts +4 -0
  144. package/dist/lib/sync-all.d.ts.map +1 -1
  145. package/dist/lib/sync-maintenance.d.ts.map +1 -1
  146. package/dist/lib/sync-report.d.ts +4 -0
  147. package/dist/lib/sync-report.d.ts.map +1 -0
  148. package/dist/lib/test-keychain-fixture.d.ts +9 -0
  149. package/dist/lib/test-keychain-fixture.d.ts.map +1 -0
  150. package/dist/mcp/agent-registry-store.d.ts +30 -0
  151. package/dist/mcp/agent-registry-store.d.ts.map +1 -0
  152. package/dist/mcp/agent-registry.d.ts +9 -16
  153. package/dist/mcp/agent-registry.d.ts.map +1 -1
  154. package/dist/mcp/index.js +2281 -3457
  155. package/dist/mcp/ingest-journal-tool.d.ts +2 -0
  156. package/dist/mcp/ingest-journal-tool.d.ts.map +1 -0
  157. package/dist/mcp/server.d.ts.map +1 -1
  158. package/dist/openapi.d.ts.map +1 -1
  159. package/dist/otel/index.js +1950 -288
  160. package/dist/server/human-auth.d.ts +20 -0
  161. package/dist/server/human-auth.d.ts.map +1 -0
  162. package/dist/server/index.js +10401 -6695
  163. package/dist/server/legacy-inventory.d.ts +52 -0
  164. package/dist/server/legacy-inventory.d.ts.map +1 -0
  165. package/dist/server/legacy-operations.d.ts +64 -0
  166. package/dist/server/legacy-operations.d.ts.map +1 -0
  167. package/dist/server/legacy-plan.d.ts +55 -0
  168. package/dist/server/legacy-plan.d.ts.map +1 -0
  169. package/dist/server/legacy-preservation.d.ts +58 -0
  170. package/dist/server/legacy-preservation.d.ts.map +1 -0
  171. package/dist/server/pg-sync-worker.js +195 -21
  172. package/dist/server/serve.d.ts +20 -16
  173. package/dist/server/serve.d.ts.map +1 -1
  174. package/dist/server/service-admission.d.ts +10 -0
  175. package/dist/server/service-admission.d.ts.map +1 -0
  176. package/dist/server/service-dispatcher.d.ts +45 -0
  177. package/dist/server/service-dispatcher.d.ts.map +1 -0
  178. package/dist/server/service-operations.d.ts +5 -0
  179. package/dist/server/service-operations.d.ts.map +1 -0
  180. package/dist/server/service-protocol.d.ts +50 -0
  181. package/dist/server/service-protocol.d.ts.map +1 -0
  182. package/dist/server/service-worker.d.ts +2 -0
  183. package/dist/server/service-worker.d.ts.map +1 -0
  184. package/dist/server/service-worker.js +8029 -0
  185. package/dist/types/index.d.ts +66 -0
  186. package/dist/types/index.d.ts.map +1 -1
  187. package/docs/README.md +1 -1
  188. package/docs/cli.md +1 -1
  189. package/docs/configuration.md +5 -5
  190. package/docs/cost-reporting.md +49 -0
  191. package/docs/historical-data-reconciliation.md +203 -0
  192. package/docs/ingest-journal.md +108 -0
  193. package/docs/ingestion.md +60 -4
  194. package/docs/mcp.md +2 -2
  195. package/docs/native-budget-save.md +84 -0
  196. package/docs/native-human-auth.md +102 -0
  197. package/docs/rest-api.md +2 -2
  198. package/package.json +15 -12
  199. package/dist/lib/test-hermetic-accounts.d.ts +0 -7
  200. package/dist/lib/test-hermetic-accounts.d.ts.map +0 -1
  201. package/dist/lib/test-hermetic-fleet-env.d.ts +0 -4
  202. package/dist/lib/test-hermetic-fleet-env.d.ts.map +0 -1
@@ -0,0 +1,62 @@
1
+ // @bun
2
+ import {
3
+ getDataDir
4
+ } from "./index-ez9thsr7.js";
5
+
6
+ // src/lib/config.ts
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
8
+ import { dirname, join } from "path";
9
+ function getConfigPath() {
10
+ return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join(getDataDir(), "config.json");
11
+ }
12
+ var DEFAULTS = {
13
+ port: 3456,
14
+ "default-period": "today",
15
+ "auto-sync": true,
16
+ "sync-interval": 30,
17
+ "alert-thresholds": [5, 10, 25, 50, 100],
18
+ "webhook-url": null
19
+ };
20
+ function loadConfig() {
21
+ try {
22
+ const configPath = getConfigPath();
23
+ if (existsSync(configPath)) {
24
+ const raw = readFileSync(configPath, "utf-8");
25
+ return { ...DEFAULTS, ...JSON.parse(raw) };
26
+ }
27
+ } catch {}
28
+ return { ...DEFAULTS };
29
+ }
30
+ function saveConfig(config) {
31
+ const configPath = getConfigPath();
32
+ const dir = dirname(configPath);
33
+ if (!existsSync(dir))
34
+ mkdirSync(dir, { recursive: true });
35
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + `
36
+ `);
37
+ }
38
+ function getConfigValue(key) {
39
+ const config = loadConfig();
40
+ return config[key] ?? null;
41
+ }
42
+ function setConfigValue(key, value) {
43
+ const config = loadConfig();
44
+ let parsed = value;
45
+ if (value === "true")
46
+ parsed = true;
47
+ else if (value === "false")
48
+ parsed = false;
49
+ else if (value === "null")
50
+ parsed = null;
51
+ else if (!isNaN(Number(value)))
52
+ parsed = Number(value);
53
+ else if (value.startsWith("[")) {
54
+ try {
55
+ parsed = JSON.parse(value);
56
+ } catch {}
57
+ }
58
+ config[key] = parsed;
59
+ saveConfig(config);
60
+ }
61
+
62
+ export { loadConfig, saveConfig, getConfigValue, setConfigValue };
@@ -0,0 +1,258 @@
1
+ // @bun
2
+ import {
3
+ clearBillingRange,
4
+ upsertBillingDaily
5
+ } from "./index-ez9thsr7.js";
6
+
7
+ // src/ingest/billing.ts
8
+ import { readFileSync } from "fs";
9
+ function getAnthropicAdminKey() {
10
+ return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
11
+ }
12
+ function getOpenAIAdminKey() {
13
+ return process.env["HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY"] ?? process.env["OPENAI_ADMIN_API_KEY"] ?? null;
14
+ }
15
+ function getGeminiBillingExportPath() {
16
+ return process.env["HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH"] ?? process.env["HASNAXYZ_ECONOMY_GEMINI_BILLING_EXPORT_PATH"] ?? process.env["GEMINI_BILLING_EXPORT_PATH"] ?? null;
17
+ }
18
+ function toISODate(d) {
19
+ return d.toISOString().substring(0, 10);
20
+ }
21
+ function isValidDateParts(year, month, day) {
22
+ const d = new Date(Date.UTC(year, month - 1, day));
23
+ return d.getUTCFullYear() === year && d.getUTCMonth() === month - 1 && d.getUTCDate() === day;
24
+ }
25
+ function formatDateParts(year, month, day) {
26
+ return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
27
+ }
28
+ function parseDate(value) {
29
+ if (typeof value === "number" && Number.isInteger(value)) {
30
+ return value >= 100001 && value <= 999912 ? parseDate(String(value)) : null;
31
+ }
32
+ if (typeof value !== "string" || !value.trim())
33
+ return null;
34
+ const trimmed = value.trim();
35
+ const compactMonth = /^(\d{4})(\d{2})$/.exec(trimmed);
36
+ if (compactMonth) {
37
+ const year = Number(compactMonth[1]);
38
+ const month = Number(compactMonth[2]);
39
+ return isValidDateParts(year, month, 1) ? formatDateParts(year, month, 1) : null;
40
+ }
41
+ const dashedMonth = /^(\d{4})-(\d{2})$/.exec(trimmed);
42
+ if (dashedMonth) {
43
+ const year = Number(dashedMonth[1]);
44
+ const month = Number(dashedMonth[2]);
45
+ return isValidDateParts(year, month, 1) ? formatDateParts(year, month, 1) : null;
46
+ }
47
+ const isoDate = /^(\d{4})-(\d{2})-(\d{2})(?:$|[T\s])/.exec(trimmed);
48
+ if (isoDate) {
49
+ const year = Number(isoDate[1]);
50
+ const month = Number(isoDate[2]);
51
+ const day = Number(isoDate[3]);
52
+ if (!isValidDateParts(year, month, day))
53
+ return null;
54
+ if (trimmed.length === 10)
55
+ return formatDateParts(year, month, day);
56
+ }
57
+ const d = new Date(trimmed);
58
+ if (Number.isNaN(d.getTime()))
59
+ return null;
60
+ return toISODate(d);
61
+ }
62
+ function parseCsv(content) {
63
+ const lines = content.split(/\r?\n/).filter((line) => line.trim());
64
+ if (lines.length < 2)
65
+ return [];
66
+ const headers = parseCsvLine(lines[0]).map((h) => h.trim());
67
+ return lines.slice(1).map((line) => {
68
+ const values = parseCsvLine(line);
69
+ return Object.fromEntries(headers.map((header, i) => [header, values[i]?.trim() ?? ""]));
70
+ });
71
+ }
72
+ function parseCsvLine(line) {
73
+ const values = [];
74
+ let value = "";
75
+ let quoted = false;
76
+ for (let i = 0;i < line.length; i++) {
77
+ const char = line[i];
78
+ if (char === '"') {
79
+ if (quoted && line[i + 1] === '"') {
80
+ value += '"';
81
+ i++;
82
+ } else {
83
+ quoted = !quoted;
84
+ }
85
+ } else if (char === "," && !quoted) {
86
+ values.push(value);
87
+ value = "";
88
+ } else {
89
+ value += char;
90
+ }
91
+ }
92
+ values.push(value);
93
+ return values;
94
+ }
95
+ function parseBillingRows(content) {
96
+ const trimmed = content.trim();
97
+ if (!trimmed)
98
+ return [];
99
+ try {
100
+ const parsed = JSON.parse(trimmed);
101
+ if (Array.isArray(parsed))
102
+ return parsed;
103
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed["rows"])) {
104
+ return parsed["rows"];
105
+ }
106
+ } catch {}
107
+ const jsonlRows = [];
108
+ for (const line of trimmed.split(/\r?\n/)) {
109
+ try {
110
+ const parsed = JSON.parse(line);
111
+ if (parsed && typeof parsed === "object")
112
+ jsonlRows.push(parsed);
113
+ } catch {
114
+ jsonlRows.length = 0;
115
+ break;
116
+ }
117
+ }
118
+ if (jsonlRows.length > 0)
119
+ return jsonlRows;
120
+ return parseCsv(content);
121
+ }
122
+ async function syncAnthropicBilling(db, opts = {}) {
123
+ const key = getAnthropicAdminKey();
124
+ if (!key)
125
+ throw new Error("Missing Anthropic admin key (HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY)");
126
+ const now = new Date;
127
+ const end = opts.toDate ? new Date(opts.toDate) : new Date(now.getTime() + 24 * 3600000);
128
+ const days = opts.days ?? 31;
129
+ const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
130
+ const startIso = start.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
131
+ const endIso = end.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
132
+ let totalUsd = 0;
133
+ const buckets = [];
134
+ let nextPage;
135
+ do {
136
+ const url = new URL("https://api.anthropic.com/v1/organizations/cost_report");
137
+ url.searchParams.set("starting_at", startIso);
138
+ url.searchParams.set("ending_at", endIso);
139
+ url.searchParams.set("bucket_width", "1d");
140
+ url.searchParams.set("limit", "31");
141
+ url.searchParams.append("group_by[]", "description");
142
+ if (nextPage)
143
+ url.searchParams.set("page", nextPage);
144
+ const res = await fetch(url.toString(), {
145
+ headers: { "anthropic-version": "2023-06-01", "x-api-key": key }
146
+ });
147
+ const data = await res.json();
148
+ if (data.error)
149
+ throw new Error(`Anthropic API: ${data.error.message}`);
150
+ if (data.data)
151
+ buckets.push(...data.data);
152
+ nextPage = data.has_more ? data.next_page : undefined;
153
+ } while (nextPage);
154
+ const fromDateStr = toISODate(start);
155
+ const toDateStr = toISODate(new Date(end.getTime() - 1000));
156
+ clearBillingRange(db, "anthropic", fromDateStr, toDateStr);
157
+ const updatedAt = new Date().toISOString();
158
+ for (const bucket of buckets) {
159
+ const date = bucket.starting_at.substring(0, 10);
160
+ for (const r of bucket.results) {
161
+ const usd = Number(r.amount) / 100;
162
+ if (usd === 0)
163
+ continue;
164
+ const desc = (r.description ?? "unknown").substring(0, 200);
165
+ upsertBillingDaily(db, { date, provider: "anthropic", description: desc, cost_usd: usd, updated_at: updatedAt });
166
+ totalUsd += usd;
167
+ }
168
+ }
169
+ return { days: buckets.length, totalUsd };
170
+ }
171
+ async function syncOpenAIBilling(db, opts = {}) {
172
+ const key = getOpenAIAdminKey();
173
+ if (!key)
174
+ throw new Error("Missing OpenAI admin key (HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY)");
175
+ const now = new Date;
176
+ const end = opts.toDate ? new Date(opts.toDate) : now;
177
+ const days = opts.days ?? 31;
178
+ const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
179
+ const startSec = Math.floor(start.getTime() / 1000);
180
+ const endSec = Math.floor(end.getTime() / 1000);
181
+ let totalUsd = 0;
182
+ const buckets = [];
183
+ let nextPage;
184
+ do {
185
+ const url = new URL("https://api.openai.com/v1/organization/costs");
186
+ url.searchParams.set("start_time", String(startSec));
187
+ url.searchParams.set("end_time", String(endSec));
188
+ url.searchParams.set("bucket_width", "1d");
189
+ url.searchParams.set("limit", "31");
190
+ url.searchParams.append("group_by[]", "line_item");
191
+ if (nextPage)
192
+ url.searchParams.set("page", nextPage);
193
+ const res = await fetch(url.toString(), {
194
+ headers: { Authorization: `Bearer ${key}` }
195
+ });
196
+ const data = await res.json();
197
+ if (data.error)
198
+ throw new Error(`OpenAI API: ${data.error.message}`);
199
+ if (data.data)
200
+ buckets.push(...data.data);
201
+ nextPage = data.has_more ? data.next_page : undefined;
202
+ } while (nextPage);
203
+ const fromDateStr = toISODate(start);
204
+ const toDateStr = toISODate(new Date(end.getTime() - 1000));
205
+ clearBillingRange(db, "openai", fromDateStr, toDateStr);
206
+ const updatedAt = new Date().toISOString();
207
+ for (const bucket of buckets) {
208
+ const date = new Date(bucket.start_time * 1000).toISOString().substring(0, 10);
209
+ for (const r of bucket.results) {
210
+ const usd = Number(r.amount?.value ?? 0);
211
+ if (usd === 0)
212
+ continue;
213
+ const desc = (r.line_item ?? "unknown").substring(0, 200);
214
+ upsertBillingDaily(db, { date, provider: "openai", description: desc, cost_usd: usd, updated_at: updatedAt });
215
+ totalUsd += usd;
216
+ }
217
+ }
218
+ return { days: buckets.length, totalUsd };
219
+ }
220
+ async function syncGeminiBilling(db, opts = {}) {
221
+ const exportPath = getGeminiBillingExportPath();
222
+ if (!exportPath) {
223
+ return {
224
+ days: 0,
225
+ totalUsd: 0,
226
+ skipped: "Missing Gemini billing export path (HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH, HASNAXYZ_ECONOMY_GEMINI_BILLING_EXPORT_PATH, or GEMINI_BILLING_EXPORT_PATH)"
227
+ };
228
+ }
229
+ const now = new Date;
230
+ const end = opts.toDate ? new Date(opts.toDate) : now;
231
+ const days = opts.days ?? 31;
232
+ const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
233
+ const fromDateStr = toISODate(start);
234
+ const toDateStr = toISODate(end);
235
+ const rows = parseBillingRows(readFileSync(exportPath, "utf-8"));
236
+ clearBillingRange(db, "gemini", fromDateStr, toDateStr);
237
+ const updatedAt = new Date().toISOString();
238
+ let totalUsd = 0;
239
+ const seenDays = new Set;
240
+ for (const row of rows) {
241
+ const date = parseDate(row["date"] ?? row["usage_start_time"] ?? row["start_time"] ?? row["invoice.month"]);
242
+ if (!date || date < fromDateStr || date > toDateStr)
243
+ continue;
244
+ const rawCost = row["cost_usd"] ?? row["costUsd"] ?? row["cost"] ?? row["amount"];
245
+ const costUsd = Number(rawCost);
246
+ if (!Number.isFinite(costUsd) || costUsd === 0)
247
+ continue;
248
+ const service = row["service.description"] ?? row["service"] ?? row["provider"] ?? "";
249
+ const sku = row["sku.description"] ?? row["sku"] ?? row["description"] ?? "Gemini API";
250
+ const description = `${String(service || "Google AI")}: ${String(sku)}`.substring(0, 200);
251
+ upsertBillingDaily(db, { date, provider: "gemini", description, cost_usd: costUsd, updated_at: updatedAt });
252
+ totalUsd += costUsd;
253
+ seenDays.add(date);
254
+ }
255
+ return { days: seenDays.size, totalUsd };
256
+ }
257
+
258
+ export { syncAnthropicBilling, syncOpenAIBilling, syncGeminiBilling };