@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,1313 @@
1
+ // @bun
2
+ import {
3
+ calendarPeriodBounds,
4
+ calendarPeriodWhere,
5
+ formatBillingValue,
6
+ formatReportedUsd,
7
+ hasKnownUsagePrice,
8
+ listMachineRegistry,
9
+ listMachines,
10
+ listModelPricing,
11
+ queryAccountBreakdown,
12
+ queryAgentBreakdown,
13
+ queryBillingSummary,
14
+ querySummary,
15
+ requireCurrentBillingSummary,
16
+ requireCurrentCostSummary,
17
+ sessionOverlapWhere
18
+ } from "./index-ez9thsr7.js";
19
+ // package.json
20
+ var package_default = {
21
+ name: "@hasna/economy",
22
+ version: "0.5.0",
23
+ description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
24
+ repository: {
25
+ type: "git",
26
+ url: "https://github.com/hasna/apps.git",
27
+ directory: "apps/economy"
28
+ },
29
+ bugs: {
30
+ url: "https://github.com/hasna/apps/issues"
31
+ },
32
+ homepage: "https://github.com/hasna/apps/tree/main/apps/economy#readme",
33
+ type: "module",
34
+ main: "dist/index.js",
35
+ types: "dist/index.d.ts",
36
+ bin: {
37
+ economy: "dist/cli/index.js",
38
+ "economy-mcp": "dist/mcp/index.js",
39
+ "economy-serve": "dist/server/index.js",
40
+ "economy-otel": "dist/otel/index.js"
41
+ },
42
+ exports: {
43
+ ".": {
44
+ types: "./dist/index.d.ts",
45
+ import: "./dist/index.js"
46
+ },
47
+ "./sdk": {
48
+ types: "./dist/index.d.ts",
49
+ import: "./dist/index.js"
50
+ }
51
+ },
52
+ files: [
53
+ "dist",
54
+ "docs",
55
+ "postinstall.js",
56
+ "LICENSE",
57
+ "README.md",
58
+ "CHANGELOG.md",
59
+ "SECURITY.md",
60
+ "CONTRIBUTING.md",
61
+ "CODE_OF_CONDUCT.md",
62
+ "!dist/lib/test-hermetic-*"
63
+ ],
64
+ scripts: {
65
+ build: 'bun build src/cli/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && bun build src/mcp/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/server/service-worker.ts --outdir dist/server --target bun --packages external && bun build src/otel/index.ts --outdir dist/otel --target bun --packages external && bun build src/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && tsc --emitDeclarationOnly --outDir dist',
66
+ "build:cli": 'bun build src/cli/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
67
+ "build:mcp": 'bun build src/mcp/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
68
+ "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/server/service-worker.ts --outdir dist/server --target bun --packages external",
69
+ "build:lib": 'bun build src/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
70
+ "artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
71
+ prepack: "bun run build && bun run artifact-scan",
72
+ typecheck: "tsc --noEmit",
73
+ test: "bun test",
74
+ "dev:cli": "bun run src/cli/index.ts",
75
+ "dev:mcp": "bun run src/mcp/index.ts",
76
+ "dev:serve": "bun run src/server/index.ts",
77
+ "dev:otel": "bun run src/otel/index.ts",
78
+ postinstall: "node postinstall.js"
79
+ },
80
+ keywords: [
81
+ "economy",
82
+ "cost",
83
+ "ai",
84
+ "claude",
85
+ "codex",
86
+ "gemini",
87
+ "mcp",
88
+ "cli",
89
+ "budget",
90
+ "tracking"
91
+ ],
92
+ author: "hasna",
93
+ license: "Apache-2.0",
94
+ publishConfig: {
95
+ registry: "https://registry.npmjs.org",
96
+ access: "public"
97
+ },
98
+ dependencies: {
99
+ "@hasna/contracts": "1.0.2",
100
+ "@hasna/events": "^0.1.16",
101
+ "@hasna/projects": "1.0.0",
102
+ "@modelcontextprotocol/sdk": "^1.12.1",
103
+ chalk: "^5.4.1",
104
+ commander: "^13.1.0",
105
+ pg: "8.13.1",
106
+ zod: "^3.24.2",
107
+ "@hasna/trash": "0.2.3"
108
+ },
109
+ devDependencies: {
110
+ "@types/bun": "1.3.14",
111
+ "@types/pg": "^8.20.0",
112
+ "bun-types": "latest",
113
+ typescript: "^5.7.2"
114
+ },
115
+ trustedDependencies: [
116
+ "@hasna/projects"
117
+ ]
118
+ };
119
+
120
+ // src/lib/package-metadata.ts
121
+ var cachedMetadata = null;
122
+ function getPackageMetadata() {
123
+ if (cachedMetadata)
124
+ return cachedMetadata;
125
+ cachedMetadata = {
126
+ name: package_default.name ?? "@hasna/economy",
127
+ version: package_default.version ?? "0.0.0"
128
+ };
129
+ return cachedMetadata;
130
+ }
131
+ var packageMetadata = getPackageMetadata();
132
+
133
+ // src/lib/cloud-storage.ts
134
+ import {
135
+ appConfigDiskValue,
136
+ ClientTransportConfigurationError,
137
+ clientTransportEnvKeys,
138
+ createClientTransport,
139
+ credentialDiskSources,
140
+ credentialOverrideEnvKey,
141
+ credentialPointerEnvKey,
142
+ CREDENTIAL_PROFILE_ENV_KEY,
143
+ keychainConfigValue
144
+ } from "@hasna/contracts/client";
145
+ import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
146
+ var ECONOMY_APP = "economy";
147
+ var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
148
+ function envFlagSet(env, key) {
149
+ const raw = env[key];
150
+ if (raw === undefined)
151
+ return false;
152
+ const value = raw.trim().toLowerCase();
153
+ return value !== "" && value !== "0" && value !== "false" && value !== "no" && value !== "off";
154
+ }
155
+ function localStorageExplicitlyOptedIn(env) {
156
+ return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => envFlagSet(env, key));
157
+ }
158
+ function economyAuthorityEnvKeys() {
159
+ const keys = clientTransportEnvKeys(ECONOMY_APP);
160
+ return [
161
+ ...keys.apiUrlKeys,
162
+ ...keys.apiKeyKeys,
163
+ credentialOverrideEnvKey(ECONOMY_APP),
164
+ credentialPointerEnvKey(ECONOMY_APP),
165
+ CREDENTIAL_PROFILE_ENV_KEY
166
+ ];
167
+ }
168
+ function hasEconomyEnvAuthorityIntent(env) {
169
+ return economyAuthorityEnvKeys().some((key) => (env[key] ?? "").trim().length > 0);
170
+ }
171
+ function hostedAuthorityConfigured(env, options = {}) {
172
+ const keys = clientTransportEnvKeys(ECONOMY_APP);
173
+ if (keys.apiUrlKeys.some((key) => (env[key] ?? "").trim().length > 0))
174
+ return true;
175
+ if (keychainConfigValue(ECONOMY_APP, env, options.credentials?.keychain) !== null)
176
+ return true;
177
+ return appConfigDiskValue(ECONOMY_APP, env, keys.apiUrlKeys) !== null;
178
+ }
179
+ function economyResolverEnv(env) {
180
+ const blanks = economyAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
181
+ if (blanks.length === 0)
182
+ return env;
183
+ const next = { ...env };
184
+ for (const key of blanks)
185
+ delete next[key];
186
+ return next;
187
+ }
188
+ var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
189
+ function isAmbientEconomyEnv(env) {
190
+ if (typeof process !== "undefined" && env === process.env)
191
+ return true;
192
+ return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
193
+ }
194
+ function economyResolverInputs(env, credentials = {}) {
195
+ const normalised = economyResolverEnv(env);
196
+ if (normalised === env)
197
+ return { env: normalised, credentials };
198
+ const keychain = { ...credentials.keychain };
199
+ if (keychain.enabled === undefined && keychain.run === undefined) {
200
+ keychain.enabled = isAmbientEconomyEnv(env);
201
+ }
202
+ return { env: normalised, credentials: { ...credentials, keychain } };
203
+ }
204
+ function resolveEconomyStorageClient(name, env, overrides = {}) {
205
+ const { env: resolverEnv, credentials } = economyResolverInputs(env, overrides.credentials);
206
+ const wired = createClientTransport(name, resolverEnv, {
207
+ credentials,
208
+ ...overrides.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
209
+ ...overrides.headers ? { headers: overrides.headers } : {},
210
+ ...overrides.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
211
+ ...overrides.retry !== undefined ? { retry: overrides.retry } : {},
212
+ ...overrides.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
213
+ });
214
+ return {
215
+ transport: "http",
216
+ client: createHasnaStorageClient(name, wired.client),
217
+ resolution: wired.resolution
218
+ };
219
+ }
220
+ function localEconomyNotice() {
221
+ return `economy: LOCAL mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 reading and writing the on-box ` + `SQLite store, not the hosted fleet. Data in the shared economy API is NOT included.`;
222
+ }
223
+ function credentialRequiredError(env, cause) {
224
+ const keys = clientTransportEnvKeys(ECONOMY_APP);
225
+ const clientEnv = env;
226
+ const detail = cause instanceof Error ? cause.message : `No API key could be resolved for '${ECONOMY_APP}'. Looked in the Keychain (macOS only), ` + `then for a credential file at ${credentialDiskSources(ECONOMY_APP, clientEnv).join(" or ") || "<no HOME set>"}, ` + `then for ${keys.apiKeyKeys[0]} in the environment.`;
227
+ const wayOut = hostedAuthorityConfigured(clientEnv) ? `${keys.apiUrlKeys[0]} (or the Keychain hasna.credentials.${ECONOMY_APP}.api-url item, or ` + `~/.hasna/${ECONOMY_APP}/config/credentials) selects a hosted service, so ${LOCAL_STORAGE_OPT_IN_KEYS[0]} ` + `does not apply; unset the authority to use the local store.` : `Or set ${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1 to explicitly opt into the local store.`;
228
+ return new Error(`Economy fails closed instead of silently serving the local SQLite store: ${detail} ` + `Set ${keys.apiUrlKeys[0]} and ${keys.apiKeyKeys[0]} (or store the key in the Keychain item ` + `hasna.credentials.${ECONOMY_APP}.api-key, or in ~/.hasna/${ECONOMY_APP}/config/credentials) to use the ` + `hosted economy API at ${keys.apiUrlKeys[0]} or the default fleet gateway. ` + wayOut);
229
+ }
230
+ var cache;
231
+ var localNoticePrinted = false;
232
+ function resolveEconomyCloudStorage(env = process.env, overrides) {
233
+ const clientEnv = env;
234
+ if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
235
+ return { active: false, client: null };
236
+ }
237
+ let resolved;
238
+ try {
239
+ resolved = resolveEconomyStorageClient(ECONOMY_APP, clientEnv, overrides ?? {});
240
+ } catch (error) {
241
+ if (error instanceof ClientTransportConfigurationError) {
242
+ throw credentialRequiredError(env, error);
243
+ }
244
+ throw error;
245
+ }
246
+ return { active: true, client: resolved.client };
247
+ }
248
+ function economyCloudStorage(env = process.env) {
249
+ if (cache === undefined) {
250
+ cache = resolveEconomyCloudStorage(env);
251
+ if (!cache.active && !localNoticePrinted) {
252
+ localNoticePrinted = true;
253
+ if (typeof process !== "undefined")
254
+ process.stderr.write(`${localEconomyNotice()}
255
+ `);
256
+ }
257
+ }
258
+ return cache;
259
+ }
260
+ function cleanQuery(query) {
261
+ if (!query)
262
+ return;
263
+ const out = {};
264
+ for (const [k, v] of Object.entries(query)) {
265
+ if (v !== undefined && v !== null && v !== "")
266
+ out[k] = v;
267
+ }
268
+ return Object.keys(out).length > 0 ? out : undefined;
269
+ }
270
+ async function cloudListItems(storage, resource, query) {
271
+ const cleaned = cleanQuery(query);
272
+ const res = await storage.client.list(resource, cleaned ? { query: cleaned } : {});
273
+ return res.items;
274
+ }
275
+ async function cloudObject(storage, path, query) {
276
+ const cleaned = cleanQuery(query);
277
+ const raw = await storage.client.transport.get(path, cleaned ? { query: cleaned } : {});
278
+ if (raw && typeof raw === "object" && "data" in raw) {
279
+ return raw.data;
280
+ }
281
+ return raw;
282
+ }
283
+ function economyTransportReport(env = process.env, overrides) {
284
+ const clientEnv = env;
285
+ if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
286
+ return {
287
+ ok: true,
288
+ selected: true,
289
+ transport: "sqlite",
290
+ source: "local-opt-in",
291
+ authority: null,
292
+ issues: []
293
+ };
294
+ }
295
+ try {
296
+ const resolved = resolveEconomyStorageClient(ECONOMY_APP, clientEnv, overrides ?? {});
297
+ return {
298
+ ok: true,
299
+ selected: true,
300
+ transport: "http",
301
+ source: resolved.resolution.transportSource,
302
+ authority: {
303
+ baseUrl: resolved.resolution.baseUrl,
304
+ apiUrlSource: resolved.resolution.apiUrlSource,
305
+ apiKeySource: resolved.resolution.apiKeySource,
306
+ apiKeyTier: resolved.resolution.apiKeyTier,
307
+ warning: resolved.resolution.warning
308
+ },
309
+ issues: []
310
+ };
311
+ } catch (error) {
312
+ if (error instanceof ClientTransportConfigurationError) {
313
+ return {
314
+ ok: false,
315
+ selected: true,
316
+ transport: "http",
317
+ source: null,
318
+ authority: null,
319
+ issues: [error.message]
320
+ };
321
+ }
322
+ throw error;
323
+ }
324
+ }
325
+
326
+ // src/lib/savings.ts
327
+ function requireCurrentSavingsReport(value) {
328
+ if (!value || typeof value !== "object")
329
+ throw new Error("SAVINGS_REPORT_UNAVAILABLE: Invalid savings response");
330
+ const report = value;
331
+ if (report.comparison_basis !== "included_usage_vs_current_plan_estimate" || !["estimated", "not_applicable", "unavailable"].includes(String(report.comparison_status))) {
332
+ throw new Error("SAVINGS_REPORT_UNAVAILABLE: The service must support the current subscription comparison contract");
333
+ }
334
+ for (const field of ["api_equivalent_usd", "subscription_fee_usd", "on_demand_usd", "saved_usd", "net_comparison_usd"]) {
335
+ const number = report[field];
336
+ if (number !== null && (typeof number !== "number" || !Number.isFinite(number))) {
337
+ throw new Error("SAVINGS_REPORT_UNAVAILABLE: Invalid monetary field " + field);
338
+ }
339
+ }
340
+ if (report.comparison_status === "unavailable" && (report.saved_usd !== null || report.net_comparison_usd !== null)) {
341
+ throw new Error("SAVINGS_REPORT_UNAVAILABLE: Unavailable comparison carries a monetary claim");
342
+ }
343
+ return value;
344
+ }
345
+ function prorateMonthlyFee(monthlyFee, period, now = new Date) {
346
+ if (!Number.isFinite(monthlyFee) || monthlyFee < 0)
347
+ throw new RangeError("Monthly fee must be finite and nonnegative");
348
+ const bounds = calendarPeriodBounds(period, now);
349
+ if (bounds.start_date === null)
350
+ return null;
351
+ let cursor = new Date(bounds.start_date + "T00:00:00.000Z");
352
+ const end = new Date(bounds.end_date_exclusive + "T00:00:00.000Z");
353
+ let fee = 0;
354
+ while (cursor < end) {
355
+ const monthEnd = new Date(Date.UTC(cursor.getUTCFullYear(), cursor.getUTCMonth() + 1, 1));
356
+ const next = new Date(Math.min(end.getTime(), monthEnd.getTime()));
357
+ const days = (next.getTime() - cursor.getTime()) / 86400000;
358
+ const monthDays = new Date(Date.UTC(cursor.getUTCFullYear(), cursor.getUTCMonth() + 1, 0)).getUTCDate();
359
+ fee += monthlyFee / monthDays * days;
360
+ cursor = next;
361
+ }
362
+ return fee;
363
+ }
364
+ function feeFor(plans, period, now) {
365
+ let known = 0;
366
+ let complete = true;
367
+ for (const plan of plans) {
368
+ if (plan.fee_basis !== "observed" && plan.fee_basis !== "configured" || plan.monthly_fee_usd === null || !Number.isFinite(plan.monthly_fee_usd) || plan.monthly_fee_usd < 0) {
369
+ complete = false;
370
+ continue;
371
+ }
372
+ const amount = prorateMonthlyFee(plan.monthly_fee_usd, period, now);
373
+ if (amount === null || !Number.isFinite(amount) || !Number.isFinite(known + amount))
374
+ complete = false;
375
+ else
376
+ known += amount;
377
+ }
378
+ return { known, total: complete ? known : null };
379
+ }
380
+ function summarizeOnDemand(observations, period, now = new Date) {
381
+ const rows = observations.filter((row) => row.metric === "on_demand_usd");
382
+ if (!rows.length)
383
+ return { usd: null, status: "not_observed" };
384
+ const bounds = calendarPeriodBounds(period, now);
385
+ const start = bounds.start_date ? Date.parse(bounds.start_date + "T00:00:00Z") : -Infinity;
386
+ const end = bounds.end_date_exclusive ? Date.parse(bounds.end_date_exclusive + "T00:00:00Z") : Infinity;
387
+ const windows = new Map;
388
+ for (const row of rows) {
389
+ const from = Date.parse(row.window_start ?? "");
390
+ const until = Date.parse(row.window_end ?? "");
391
+ const at = Date.parse(row.source_timestamp ?? row.observed_at ?? "");
392
+ if (row.unit.toLowerCase() !== "usd" || !Number.isFinite(row.value) || row.value < 0 || !row.provider || !row.account_key || !Number.isFinite(from) || !Number.isFinite(until) || from >= until || !Number.isFinite(at) || row.value_kind !== "cumulative" && row.value_kind !== "delta") {
393
+ return { usd: null, status: "unavailable" };
394
+ }
395
+ if (until <= start || from >= end)
396
+ continue;
397
+ if (from < start || until > end)
398
+ return { usd: null, status: "unavailable" };
399
+ const key = JSON.stringify([row.provider, row.account_key, from, until]);
400
+ const previous = windows.get(key);
401
+ if (previous && previous.row.value_kind !== row.value_kind)
402
+ return { usd: null, status: "unavailable" };
403
+ if (previous && ((row.value_kind === "delta" || at === previous.at) && previous.row.value !== row.value)) {
404
+ return { usd: null, status: "unavailable" };
405
+ }
406
+ if (!previous || at > previous.at)
407
+ windows.set(key, { row, at, start: from, end: until });
408
+ }
409
+ const byAccount = new Map;
410
+ let total = 0;
411
+ for (const window of windows.values()) {
412
+ const key = JSON.stringify([window.row.provider, window.row.account_key]);
413
+ const intervals = byAccount.get(key) ?? [];
414
+ if (intervals.some((other) => window.start < other.end && window.end > other.start)) {
415
+ return { usd: null, status: "unavailable" };
416
+ }
417
+ intervals.push(window);
418
+ byAccount.set(key, intervals);
419
+ total += window.row.value;
420
+ }
421
+ return windows.size ? { usd: total, status: "observed" } : { usd: null, status: "not_observed" };
422
+ }
423
+ function buildSavingsSummary(period, usage, plans, observations, agent, now = new Date, prices = []) {
424
+ const selected = usage.filter((row) => !agent || row.agent === agent);
425
+ const activePlans = plans.filter((row) => row.active === 1);
426
+ const selectedPlans = activePlans.filter((row) => !agent || row.agent === agent);
427
+ const sharedPlans = activePlans.filter((row) => !row.agent || !row.account_key);
428
+ const fee = feeFor(selectedPlans, period, now);
429
+ const unallocated = feeFor(sharedPlans, period, now);
430
+ const reasons = new Set;
431
+ let priced = 0, metered = 0, estimated = 0, included = 0, unknownPrices = 0, unknownBilling = 0, estimatedSessions = 0;
432
+ for (const row of selected) {
433
+ if (row.session_count) {
434
+ estimatedSessions += row.session_count;
435
+ reasons.add("billing_route_unknown");
436
+ }
437
+ if (!hasKnownUsagePrice(row, prices)) {
438
+ unknownPrices += row.requests;
439
+ reasons.add("model_price_unknown");
440
+ continue;
441
+ }
442
+ priced += row.cost_usd;
443
+ if (row.cost_basis === "metered_api")
444
+ metered += row.cost_usd;
445
+ else if (row.cost_basis === "subscription_included") {
446
+ included += row.cost_usd;
447
+ const matches = selectedPlans.filter((plan) => !!row.account_key && plan.account_key === row.account_key);
448
+ if (matches.length === 0)
449
+ reasons.add("subscription_account_unmapped");
450
+ if (matches.length > 1)
451
+ reasons.add("subscription_account_ambiguous");
452
+ } else {
453
+ estimated += row.cost_usd;
454
+ unknownBilling += row.requests;
455
+ reasons.add("billing_route_unknown");
456
+ }
457
+ }
458
+ if (unknownPrices)
459
+ reasons.add("model_price_unknown");
460
+ if (unknownBilling)
461
+ reasons.add("billing_route_unknown");
462
+ if (fee.total === null)
463
+ reasons.add(period === "all" ? "historical_plan_fees_unavailable" : "plan_fee_unknown");
464
+ if (included > 0 && !agent && sharedPlans.some((plan) => !plan.account_key))
465
+ reasons.add("subscription_account_unmapped");
466
+ const demand = summarizeOnDemand(observations.filter((row) => !agent || row.agent === agent), period, now);
467
+ const applicable = selectedPlans.length > 0 || selected.some((row) => row.cost_basis === "subscription_included");
468
+ const net = reasons.size ? null : applicable ? included - fee.known : 0;
469
+ const byAgent = {};
470
+ if (!agent) {
471
+ const agents = new Set([...usage.map((row) => row.agent), ...activePlans.flatMap((row) => row.agent ? [row.agent] : [])]);
472
+ for (const name of [...agents].sort()) {
473
+ const result = buildSavingsSummary(period, usage, plans, observations, name, now, prices);
474
+ const { by_agent: _, ...scalars } = result;
475
+ byAgent[name] = scalars;
476
+ }
477
+ }
478
+ return {
479
+ period,
480
+ api_equivalent_usd: reasons.has("model_price_unknown") ? null : priced,
481
+ priced_usage_value_usd: priced,
482
+ recorded_metered_api_usd: metered,
483
+ estimated_usage_value_usd: estimated,
484
+ subscription_fee_usd: fee.total,
485
+ known_subscription_fee_usd: fee.known,
486
+ unallocated_subscription_fee_usd: unallocated.total,
487
+ included_consumed_usd: included,
488
+ on_demand_usd: demand.usd,
489
+ on_demand_status: demand.status,
490
+ saved_usd: net === null ? null : Math.max(0, net),
491
+ net_comparison_usd: net,
492
+ comparison_status: reasons.size ? "unavailable" : applicable ? "estimated" : "not_applicable",
493
+ comparison_basis: "included_usage_vs_current_plan_estimate",
494
+ unavailable_reasons: [...reasons].sort(),
495
+ unknown_price_requests: unknownPrices,
496
+ unknown_billing_requests: unknownBilling,
497
+ estimated_only_sessions: estimatedSessions,
498
+ by_agent: byAgent
499
+ };
500
+ }
501
+ function querySavingsSummary(db, period, agent) {
502
+ const now = new Date;
503
+ const usage = db.prepare(`
504
+ SELECT agent, account_key, cost_basis, model,
505
+ CASE WHEN cost_usd = 0 THEN 1 WHEN cost_usd < 0 THEN 2 ELSE 0 END AS zero_cost,
506
+ COALESCE(SUM(cost_usd), 0) AS cost_usd, COUNT(*) AS requests,
507
+ COALESCE(SUM(input_tokens),0) AS input_tokens, COALESCE(SUM(output_tokens),0) AS output_tokens,
508
+ COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens, COALESCE(SUM(cache_create_tokens),0) AS cache_create_tokens,
509
+ COALESCE(SUM(cache_create_1h_tokens),0) AS cache_create_1h_tokens
510
+ FROM economy_effective_requests WHERE ${calendarPeriodWhere(period, "timestamp", now)}
511
+ GROUP BY agent, account_key, cost_basis, model, CASE WHEN cost_usd = 0 THEN 1 WHEN cost_usd < 0 THEN 2 ELSE 0 END
512
+ `).all();
513
+ const plans = db.prepare("SELECT * FROM subscriptions WHERE active = 1").all();
514
+ usage.push(...db.prepare(`
515
+ SELECT s.agent, s.account_key, 'estimated' AS cost_basis,
516
+ COALESCE(SUM(s.total_cost_usd), 0) AS cost_usd,
517
+ COALESCE(SUM(s.request_count), 0) AS requests, COUNT(*) AS session_count
518
+ FROM economy_effective_sessions s WHERE ${sessionOverlapWhere(period, "s", now)}
519
+ AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = s.id)
520
+ GROUP BY s.agent, s.account_key
521
+ `).all());
522
+ const bounds = calendarPeriodBounds(period, now);
523
+ const windowWhere = bounds.start_date === null ? "1=1" : `(
524
+ (window_start IS NOT NULL AND window_end IS NOT NULL
525
+ AND window_start < '${bounds.end_date_exclusive}T00:00:00.000Z'
526
+ AND window_end > '${bounds.start_date}T00:00:00.000Z')
527
+ OR ((window_start IS NULL OR window_end IS NULL) AND ${calendarPeriodWhere(period, "date", now)})
528
+ )`;
529
+ const observations = db.prepare(`
530
+ SELECT * FROM usage_snapshots WHERE metric = 'on_demand_usd'
531
+ AND ${windowWhere}
532
+ `).all();
533
+ return buildSavingsSummary(period, usage, plans, observations, agent, now, listModelPricing(db));
534
+ }
535
+
536
+ // src/lib/billing-diff.ts
537
+ function queryBillingDiff(db, period, thresholdPct = 15) {
538
+ if (!Number.isFinite(thresholdPct) || thresholdPct < 0)
539
+ throw new RangeError("thresholdPct must be a finite nonnegative number");
540
+ const usage = querySummary(db, period, undefined, true);
541
+ const billing = queryBillingSummary(db, period);
542
+ const rows = db.prepare(`
543
+ SELECT agent, COALESCE(SUM(cost_usd), 0) AS estimated_usd
544
+ FROM economy_effective_requests WHERE ${calendarPeriodWhere(period, "timestamp")}
545
+ GROUP BY agent ORDER BY agent
546
+ `).all();
547
+ return requireCurrentBillingDiffSummary({
548
+ period,
549
+ basis: "unmatched_subtotals",
550
+ estimated_usd: usage.total_usd,
551
+ actual_usd: billing.total_usd,
552
+ usage_summary: usage,
553
+ billing_summary: billing,
554
+ delta_usd: null,
555
+ delta_pct: null,
556
+ threshold_pct: thresholdPct,
557
+ is_alert: null,
558
+ comparable: false,
559
+ incomparable_reason: billing.record_count === 0 ? "no_billing_records" : "account_window_source_unverified",
560
+ by_agent: rows.map((row) => ({ ...row, basis: "stored_request_rows_subtotal", actual_usd: null, delta_usd: null, delta_pct: null })),
561
+ by_provider: billing.by_provider
562
+ });
563
+ }
564
+ function requireCurrentBillingDiffSummary(value) {
565
+ const invalid = () => {
566
+ throw new Error("BILLING_COMPARISON_UNAVAILABLE: The service must return scoped unmatched subtotals");
567
+ };
568
+ if (!value || typeof value !== "object" || Array.isArray(value))
569
+ return invalid();
570
+ const s = value;
571
+ if (s.basis !== "unmatched_subtotals" || s.comparable !== false || s.delta_usd !== null || s.delta_pct !== null || s.is_alert !== null)
572
+ return invalid();
573
+ if (!["today", "yesterday", "week", "month", "year", "all"].includes(String(s.period)) || typeof s.threshold_pct !== "number" || !Number.isFinite(s.threshold_pct) || s.threshold_pct < 0)
574
+ return invalid();
575
+ let usage, billing;
576
+ try {
577
+ usage = requireCurrentCostSummary(s.usage_summary);
578
+ billing = requireCurrentBillingSummary(s.billing_summary);
579
+ } catch {
580
+ return invalid();
581
+ }
582
+ if (usage.period !== s.period || s.estimated_usd !== usage.total_usd || s.actual_usd !== billing.total_usd || billing.recorded_total_usd !== (billing.record_count ? billing.total_usd : null))
583
+ return invalid();
584
+ if (s.incomparable_reason !== (billing.record_count === 0 ? "no_billing_records" : "account_window_source_unverified"))
585
+ return invalid();
586
+ const providers = s.by_provider;
587
+ if (!providers || typeof providers !== "object" || Array.isArray(providers) || !billing.by_provider || typeof billing.by_provider !== "object")
588
+ return invalid();
589
+ const expected = Object.entries(billing.by_provider);
590
+ if (Object.keys(providers).length !== expected.length || expected.some(([key, amount]) => typeof amount !== "number" || !Number.isFinite(amount) || providers[key] !== amount))
591
+ return invalid();
592
+ if (!Array.isArray(s.by_agent) || s.by_agent.some((row) => !row || typeof row !== "object" || typeof row.agent !== "string" || row.basis !== "stored_request_rows_subtotal" || typeof row.estimated_usd !== "number" || !Number.isFinite(row.estimated_usd) || row.actual_usd !== null || row.delta_usd !== null || row.delta_pct !== null))
593
+ return invalid();
594
+ return value;
595
+ }
596
+ function billingDriftCheck(diff) {
597
+ try {
598
+ requireCurrentBillingDiffSummary(diff);
599
+ } catch {
600
+ return { ok: false, msg: "billing drift: UNKNOWN \u2014 summary coverage unsupported; update the service" };
601
+ }
602
+ const detail = diff.incomparable_reason === "no_billing_records" ? "no provider billing records imported for this period" : "account, billing window, and source coverage are not established";
603
+ return { ok: false, msg: `billing drift ${diff.period}: UNKNOWN \u2014 comparison unavailable; ${detail}; ` + `known usage-value subtotal ${formatReportedUsd(diff.estimated_usd)}; provider rows ${formatBillingValue(diff.billing_summary)}` };
604
+ }
605
+
606
+ // src/lib/brief.ts
607
+ var ZERO_REQUEST_TOTALS = {
608
+ sessions: 0,
609
+ request_sessions: 0,
610
+ requests: 0,
611
+ input_tokens: 0,
612
+ output_tokens: 0,
613
+ cache_read_tokens: 0,
614
+ cache_create_tokens: 0,
615
+ cache_create_5m_tokens: 0,
616
+ cache_create_1h_tokens: 0,
617
+ total_tokens: 0,
618
+ cost_usd: 0,
619
+ last_active: null
620
+ };
621
+ var ZERO_SESSION_TOTALS = {
622
+ sessions: 0,
623
+ requests: 0,
624
+ total_tokens: 0,
625
+ cost_usd: 0,
626
+ last_active: null
627
+ };
628
+ function formatAge(timestamp, now) {
629
+ if (!timestamp)
630
+ return "never";
631
+ const t = new Date(timestamp).getTime();
632
+ if (!Number.isFinite(t))
633
+ return "unknown";
634
+ const ageMs = Math.max(0, now.getTime() - t);
635
+ const minutes = Math.floor(ageMs / 60000);
636
+ if (minutes < 1)
637
+ return "<1m";
638
+ if (minutes < 60)
639
+ return `${minutes}m`;
640
+ const hours = Math.floor(minutes / 60);
641
+ if (hours < 48)
642
+ return `${hours}h`;
643
+ return `${Math.floor(hours / 24)}d`;
644
+ }
645
+ function machineFilter(machine) {
646
+ if (!machine || machine === "all")
647
+ return;
648
+ return machine;
649
+ }
650
+ function requestPeriodWhere(period) {
651
+ return calendarPeriodWhere(period, "timestamp");
652
+ }
653
+ function sessionPeriodWhere(period) {
654
+ return calendarPeriodWhere(period, "started_at");
655
+ }
656
+ function baseRequestTotals(row) {
657
+ return {
658
+ ...ZERO_REQUEST_TOTALS,
659
+ ...row,
660
+ sessions: Number(row?.request_sessions ?? row?.sessions ?? 0),
661
+ request_sessions: Number(row?.request_sessions ?? row?.sessions ?? 0),
662
+ requests: Number(row?.requests ?? 0),
663
+ input_tokens: Number(row?.input_tokens ?? 0),
664
+ output_tokens: Number(row?.output_tokens ?? 0),
665
+ cache_read_tokens: Number(row?.cache_read_tokens ?? 0),
666
+ cache_create_tokens: Number(row?.cache_create_tokens ?? 0),
667
+ cache_create_5m_tokens: Number(row?.cache_create_5m_tokens ?? 0),
668
+ cache_create_1h_tokens: Number(row?.cache_create_1h_tokens ?? 0),
669
+ total_tokens: Number(row?.total_tokens ?? 0),
670
+ cost_usd: Number(row?.cost_usd ?? 0),
671
+ last_active: row?.last_active ?? null
672
+ };
673
+ }
674
+ function baseSessionTotals(row) {
675
+ return {
676
+ sessions: Number(row?.sessions ?? 0),
677
+ requests: Number(row?.requests ?? 0),
678
+ total_tokens: Number(row?.total_tokens ?? 0),
679
+ cost_usd: Number(row?.cost_usd ?? 0),
680
+ last_active: row?.last_active ?? null
681
+ };
682
+ }
683
+ function latestTimestamp(...values) {
684
+ return values.filter(Boolean).sort().at(-1) ?? null;
685
+ }
686
+ function combineTotals(requests, sessions) {
687
+ return {
688
+ sessions: requests.request_sessions + sessions.sessions,
689
+ requests: requests.requests + sessions.requests,
690
+ input_tokens: requests.input_tokens,
691
+ output_tokens: requests.output_tokens,
692
+ cache_read_tokens: requests.cache_read_tokens,
693
+ cache_create_tokens: requests.cache_create_tokens,
694
+ cache_create_5m_tokens: requests.cache_create_5m_tokens,
695
+ cache_create_1h_tokens: requests.cache_create_1h_tokens,
696
+ total_tokens: requests.total_tokens + sessions.total_tokens,
697
+ cost_usd: requests.cost_usd + sessions.cost_usd,
698
+ last_active: latestTimestamp(requests.last_active, sessions.last_active)
699
+ };
700
+ }
701
+ function emptyTotals() {
702
+ return combineTotals(ZERO_REQUEST_TOTALS, ZERO_SESSION_TOTALS);
703
+ }
704
+ function requestTotalsSql(where, machine) {
705
+ const params = [];
706
+ let machineClause = "";
707
+ if (machine) {
708
+ machineClause = " AND machine_id = ?";
709
+ params.push(machine);
710
+ }
711
+ return {
712
+ sql: `
713
+ SELECT
714
+ COUNT(DISTINCT session_id) as request_sessions,
715
+ COUNT(*) as requests,
716
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
717
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
718
+ COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
719
+ COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
720
+ COALESCE(SUM(COALESCE(cache_create_5m_tokens, cache_create_tokens)), 0) as cache_create_5m_tokens,
721
+ COALESCE(SUM(COALESCE(cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
722
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
723
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
724
+ MAX(timestamp) as last_active
725
+ FROM economy_effective_requests
726
+ WHERE ${where}${machineClause}
727
+ `,
728
+ params
729
+ };
730
+ }
731
+ function sessionOnlyTotalsSql(where, machine) {
732
+ const params = [];
733
+ let machineClause = "";
734
+ if (machine) {
735
+ machineClause = " AND machine_id = ?";
736
+ params.push(machine);
737
+ }
738
+ return {
739
+ sql: `
740
+ SELECT
741
+ COUNT(*) as sessions,
742
+ COALESCE(SUM(request_count), 0) as requests,
743
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
744
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
745
+ MAX(started_at) as last_active
746
+ FROM economy_effective_sessions
747
+ WHERE ${where}${machineClause}
748
+ AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = economy_effective_sessions.id)
749
+ `,
750
+ params
751
+ };
752
+ }
753
+ function queryRequestTotals(db, where, params, machine) {
754
+ const q = requestTotalsSql(where, machine);
755
+ return baseRequestTotals(db.prepare(q.sql).get(...params, ...q.params));
756
+ }
757
+ function querySessionOnlyTotals(db, where, params, machine) {
758
+ const q = sessionOnlyTotalsSql(where, machine);
759
+ return baseSessionTotals(db.prepare(q.sql).get(...params, ...q.params));
760
+ }
761
+ function queryPeriodTotals(db, period, machine) {
762
+ const summary = querySummary(db, period, machine);
763
+ const requests = queryRequestTotals(db, requestPeriodWhere(period), [], machine);
764
+ const sessions = querySessionOnlyTotals(db, sessionPeriodWhere(period), [], machine);
765
+ const totals = combineTotals(requests, sessions);
766
+ return {
767
+ ...totals,
768
+ sessions: summary.sessions,
769
+ requests: summary.requests,
770
+ total_tokens: summary.tokens,
771
+ cost_usd: summary.total_usd,
772
+ cost_summary: summary
773
+ };
774
+ }
775
+ function querySinceTotals(db, since, machine) {
776
+ const requests = queryRequestTotals(db, "timestamp >= ?", [since], machine);
777
+ const sessions = querySessionOnlyTotals(db, "started_at >= ?", [since], machine);
778
+ return combineTotals(requests, sessions);
779
+ }
780
+ function parseSinceDate(input, now) {
781
+ const value = input.trim();
782
+ const match = value.match(/^(\d+(?:\.\d+)?)(m|h|d|w)$/i);
783
+ if (match) {
784
+ const amount = Number(match[1]);
785
+ const unit = match[2].toLowerCase();
786
+ const multiplier = unit === "m" ? 60000 : unit === "h" ? 60 * 60000 : unit === "d" ? 24 * 60 * 60000 : 7 * 24 * 60 * 60000;
787
+ return {
788
+ timestamp: new Date(now.getTime() - amount * multiplier).toISOString(),
789
+ label: value
790
+ };
791
+ }
792
+ const parsed = new Date(value);
793
+ if (Number.isNaN(parsed.getTime())) {
794
+ throw new Error("--since must be a duration like 24h, 7d, 30m, 2w, or an ISO date");
795
+ }
796
+ return { timestamp: parsed.toISOString(), label: value };
797
+ }
798
+ function queryMachineSinceRows(db, since, machine) {
799
+ const knownMachines = new Set;
800
+ for (const m of listMachines(db, "all")) {
801
+ if (m.machine_id)
802
+ knownMachines.add(m.machine_id);
803
+ }
804
+ for (const m of listMachineRegistry(db)) {
805
+ if (m.machine_id)
806
+ knownMachines.add(m.machine_id);
807
+ }
808
+ if (machine)
809
+ knownMachines.add(machine);
810
+ const requestMachineClause = machine ? " AND machine_id = ?" : "";
811
+ const requestRows = db.prepare(`
812
+ SELECT
813
+ machine_id,
814
+ COUNT(DISTINCT session_id) as request_sessions,
815
+ COUNT(*) as requests,
816
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
817
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
818
+ COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
819
+ COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
820
+ COALESCE(SUM(COALESCE(cache_create_5m_tokens, cache_create_tokens)), 0) as cache_create_5m_tokens,
821
+ COALESCE(SUM(COALESCE(cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
822
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
823
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
824
+ MAX(timestamp) as last_active
825
+ FROM economy_effective_requests
826
+ WHERE timestamp >= ?
827
+ AND machine_id != ''
828
+ ${requestMachineClause}
829
+ GROUP BY machine_id
830
+ `).all(...machine ? [since, machine] : [since]);
831
+ const sessionMachineClause = machine ? " AND machine_id = ?" : "";
832
+ const sessionRows = db.prepare(`
833
+ SELECT
834
+ machine_id,
835
+ COUNT(*) as sessions,
836
+ COALESCE(SUM(request_count), 0) as requests,
837
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
838
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
839
+ MAX(started_at) as last_active
840
+ FROM economy_effective_sessions
841
+ WHERE started_at >= ?
842
+ AND machine_id != ''
843
+ ${sessionMachineClause}
844
+ AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = economy_effective_sessions.id)
845
+ GROUP BY machine_id
846
+ `).all(...machine ? [since, machine] : [since]);
847
+ const groups = new Map;
848
+ for (const row of requestRows) {
849
+ knownMachines.add(row.machine_id);
850
+ groups.set(row.machine_id, combineTotals(baseRequestTotals(row), ZERO_SESSION_TOTALS));
851
+ }
852
+ for (const row of sessionRows) {
853
+ knownMachines.add(row.machine_id);
854
+ const current = groups.get(row.machine_id) ?? emptyTotals();
855
+ const sessions = baseSessionTotals(row);
856
+ groups.set(row.machine_id, {
857
+ ...current,
858
+ sessions: current.sessions + sessions.sessions,
859
+ requests: current.requests + sessions.requests,
860
+ total_tokens: current.total_tokens + sessions.total_tokens,
861
+ cost_usd: current.cost_usd + sessions.cost_usd,
862
+ last_active: latestTimestamp(current.last_active, sessions.last_active)
863
+ });
864
+ }
865
+ const lastData = queryMachineLastData(db, machine);
866
+ const rows = [...knownMachines].filter((m) => !machine || m === machine).map((machineId) => {
867
+ const totals = groups.get(machineId) ?? emptyTotals();
868
+ const lastDataAt = lastData.get(machineId) ?? totals.last_active;
869
+ return {
870
+ machine_id: machineId,
871
+ ...totals,
872
+ last_data_at: lastDataAt,
873
+ last_data_age: ""
874
+ };
875
+ });
876
+ return rows.sort((a, b) => b.cost_usd - a.cost_usd || a.machine_id.localeCompare(b.machine_id));
877
+ }
878
+ function queryMachineLastData(db, machine) {
879
+ const filter = machine ? " AND machine_id = ?" : "";
880
+ const requestRows = db.prepare(`
881
+ SELECT machine_id, MAX(timestamp) as last_at
882
+ FROM economy_effective_requests
883
+ WHERE machine_id != ''${filter}
884
+ GROUP BY machine_id
885
+ `).all(...machine ? [machine] : []);
886
+ const sessionRows = db.prepare(`
887
+ SELECT machine_id, MAX(started_at) as last_at
888
+ FROM economy_effective_sessions
889
+ WHERE machine_id != ''${filter}
890
+ GROUP BY machine_id
891
+ `).all(...machine ? [machine] : []);
892
+ const result = new Map;
893
+ for (const row of [...requestRows, ...sessionRows]) {
894
+ result.set(row.machine_id, latestTimestamp(result.get(row.machine_id), row.last_at));
895
+ }
896
+ return result;
897
+ }
898
+ function queryAgentSinceRows(db, since, machine) {
899
+ const requestMachineClause = machine ? " AND machine_id = ?" : "";
900
+ const sessionMachineClause = machine ? " AND machine_id = ?" : "";
901
+ const rows = db.prepare(`
902
+ WITH request_rows AS (
903
+ SELECT
904
+ agent,
905
+ session_id,
906
+ 1 as requests,
907
+ input_tokens,
908
+ output_tokens,
909
+ cache_read_tokens,
910
+ cache_create_tokens,
911
+ COALESCE(cache_create_5m_tokens, cache_create_tokens) as cache_create_5m_tokens,
912
+ COALESCE(cache_create_1h_tokens, 0) as cache_create_1h_tokens,
913
+ input_tokens + output_tokens + cache_read_tokens + cache_create_tokens as total_tokens,
914
+ cost_usd,
915
+ timestamp as last_active
916
+ FROM economy_effective_requests
917
+ WHERE timestamp >= ?${requestMachineClause}
918
+ ),
919
+ session_only_rows AS (
920
+ SELECT
921
+ agent,
922
+ id as session_id,
923
+ COALESCE(request_count, 0) as requests,
924
+ 0 as input_tokens,
925
+ 0 as output_tokens,
926
+ 0 as cache_read_tokens,
927
+ 0 as cache_create_tokens,
928
+ 0 as cache_create_5m_tokens,
929
+ 0 as cache_create_1h_tokens,
930
+ COALESCE(total_tokens, 0) as total_tokens,
931
+ COALESCE(total_cost_usd, 0) as cost_usd,
932
+ started_at as last_active
933
+ FROM economy_effective_sessions
934
+ WHERE started_at >= ?${sessionMachineClause}
935
+ AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = economy_effective_sessions.id)
936
+ ),
937
+ combined AS (
938
+ SELECT * FROM request_rows
939
+ UNION ALL
940
+ SELECT * FROM session_only_rows
941
+ )
942
+ SELECT
943
+ agent,
944
+ COUNT(DISTINCT session_id) as sessions,
945
+ COALESCE(SUM(requests), 0) as requests,
946
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
947
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
948
+ COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
949
+ COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
950
+ COALESCE(SUM(cache_create_5m_tokens), 0) as cache_create_5m_tokens,
951
+ COALESCE(SUM(cache_create_1h_tokens), 0) as cache_create_1h_tokens,
952
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
953
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
954
+ MAX(last_active) as last_active
955
+ FROM combined
956
+ GROUP BY agent
957
+ ORDER BY cost_usd DESC
958
+ `).all(...machine ? [since, machine, since, machine] : [since, since]);
959
+ if (rows.length > 0)
960
+ return rows;
961
+ return queryAgentBreakdown(db, "all", machine).filter((row) => row.last_active >= since).map(agentBreakdownToBriefRow);
962
+ }
963
+ function agentBreakdownToBriefRow(row) {
964
+ return {
965
+ agent: row.agent,
966
+ sessions: row.sessions,
967
+ requests: row.requests,
968
+ input_tokens: 0,
969
+ output_tokens: 0,
970
+ cache_read_tokens: 0,
971
+ cache_create_tokens: 0,
972
+ cache_create_5m_tokens: 0,
973
+ cache_create_1h_tokens: 0,
974
+ total_tokens: row.total_tokens,
975
+ cost_usd: row.cost_usd,
976
+ last_active: row.last_active
977
+ };
978
+ }
979
+ function normalizeAccountEmail(email) {
980
+ return email.trim().toLowerCase();
981
+ }
982
+ function accountKey(row) {
983
+ const agent = row.agent || row.account_tool || "unknown";
984
+ const email = normalizeAccountEmail(row.account_email);
985
+ if (email)
986
+ return `${agent}:${email}`;
987
+ if (row.account_name)
988
+ return `${agent}:${row.account_name}`;
989
+ return row.account_key || `${agent}:unknown`;
990
+ }
991
+ function addAccountRow(groups, row) {
992
+ if (!row.account_key && !row.account_tool && !row.account_name && !row.account_email)
993
+ return;
994
+ const key = accountKey(row);
995
+ const email = normalizeAccountEmail(row.account_email);
996
+ const current = groups.get(key) ?? {
997
+ account_key: key,
998
+ account_tool: row.agent || row.account_tool || "unknown",
999
+ account_name: row.account_name || email || row.account_key || "unknown",
1000
+ account_email: email || null,
1001
+ account_source: row.account_source || "unknown",
1002
+ sessions: 0,
1003
+ requests: 0,
1004
+ input_tokens: 0,
1005
+ output_tokens: 0,
1006
+ cache_read_tokens: 0,
1007
+ cache_create_tokens: 0,
1008
+ cache_create_5m_tokens: 0,
1009
+ cache_create_1h_tokens: 0,
1010
+ total_tokens: 0,
1011
+ cost_usd: 0,
1012
+ last_active: null
1013
+ };
1014
+ current.sessions += Number(row.sessions ?? 0);
1015
+ if (!current.account_email && email)
1016
+ current.account_email = email;
1017
+ if ((!current.account_name || current.account_name === "unknown") && (row.account_name || email)) {
1018
+ current.account_name = row.account_name || email;
1019
+ }
1020
+ if ((!current.account_source || current.account_source === "unknown") && row.account_source && row.account_source !== "unknown") {
1021
+ current.account_source = row.account_source;
1022
+ }
1023
+ current.requests += Number(row.requests ?? 0);
1024
+ current.input_tokens += Number(row.input_tokens ?? 0);
1025
+ current.output_tokens += Number(row.output_tokens ?? 0);
1026
+ current.cache_read_tokens += Number(row.cache_read_tokens ?? 0);
1027
+ current.cache_create_tokens += Number(row.cache_create_tokens ?? 0);
1028
+ current.cache_create_5m_tokens += Number(row.cache_create_5m_tokens ?? 0);
1029
+ current.cache_create_1h_tokens += Number(row.cache_create_1h_tokens ?? 0);
1030
+ current.total_tokens += Number(row.total_tokens ?? 0);
1031
+ current.cost_usd += Number(row.cost_usd ?? 0);
1032
+ current.last_active = latestTimestamp(current.last_active, row.last_active);
1033
+ groups.set(key, current);
1034
+ }
1035
+ function queryAccountSinceRows(db, since, machine) {
1036
+ const requestMachineClause = machine ? " AND r.machine_id = ?" : "";
1037
+ const sessionMachineClause = machine ? " AND s.machine_id = ?" : "";
1038
+ const requestRows = db.prepare(`
1039
+ SELECT
1040
+ COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), '') as agent,
1041
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as account_key,
1042
+ COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') as account_tool,
1043
+ COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as account_name,
1044
+ COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') as account_email,
1045
+ COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
1046
+ COUNT(DISTINCT r.session_id) as sessions,
1047
+ COUNT(*) as requests,
1048
+ COALESCE(SUM(r.input_tokens), 0) as input_tokens,
1049
+ COALESCE(SUM(r.output_tokens), 0) as output_tokens,
1050
+ COALESCE(SUM(r.cache_read_tokens), 0) as cache_read_tokens,
1051
+ COALESCE(SUM(r.cache_create_tokens), 0) as cache_create_tokens,
1052
+ COALESCE(SUM(COALESCE(r.cache_create_5m_tokens, r.cache_create_tokens, 0)), 0) as cache_create_5m_tokens,
1053
+ COALESCE(SUM(COALESCE(r.cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
1054
+ COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
1055
+ COALESCE(SUM(r.cost_usd), 0) as cost_usd,
1056
+ MAX(r.timestamp) as last_active
1057
+ FROM economy_effective_requests r
1058
+ LEFT JOIN economy_effective_sessions s ON s.id = r.session_id
1059
+ WHERE r.timestamp >= ?${requestMachineClause}
1060
+ GROUP BY
1061
+ COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), ''),
1062
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), ''),
1063
+ COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), ''),
1064
+ COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), ''),
1065
+ COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), ''),
1066
+ COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown')
1067
+ `).all(...machine ? [since, machine] : [since]);
1068
+ const sessionRows = db.prepare(`
1069
+ SELECT
1070
+ s.agent as agent,
1071
+ s.account_key as account_key,
1072
+ s.account_tool as account_tool,
1073
+ s.account_name as account_name,
1074
+ COALESCE(s.account_email, '') as account_email,
1075
+ COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
1076
+ COUNT(DISTINCT s.id) as sessions,
1077
+ COALESCE(SUM(s.request_count), 0) as requests,
1078
+ 0 as input_tokens,
1079
+ 0 as output_tokens,
1080
+ 0 as cache_read_tokens,
1081
+ 0 as cache_create_tokens,
1082
+ 0 as cache_create_5m_tokens,
1083
+ 0 as cache_create_1h_tokens,
1084
+ COALESCE(SUM(s.total_tokens), 0) as total_tokens,
1085
+ COALESCE(SUM(s.total_cost_usd), 0) as cost_usd,
1086
+ MAX(s.started_at) as last_active
1087
+ FROM economy_effective_sessions s
1088
+ WHERE s.started_at >= ?${sessionMachineClause}
1089
+ AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = s.id)
1090
+ GROUP BY
1091
+ s.agent,
1092
+ s.account_key,
1093
+ s.account_tool,
1094
+ s.account_name,
1095
+ COALESCE(s.account_email, ''),
1096
+ COALESCE(NULLIF(s.account_source, ''), 'unknown')
1097
+ `).all(...machine ? [since, machine] : [since]);
1098
+ const groups = new Map;
1099
+ for (const row of requestRows)
1100
+ addAccountRow(groups, row);
1101
+ for (const row of sessionRows)
1102
+ addAccountRow(groups, row);
1103
+ const rows = [...groups.values()];
1104
+ rows.sort((a, b) => b.cost_usd - a.cost_usd || a.account_key.localeCompare(b.account_key));
1105
+ if (rows.length > 0)
1106
+ return rows;
1107
+ return queryAccountBreakdown(db, "all", machine).filter((row) => row.last_active >= since).map(accountBreakdownToBriefRow);
1108
+ }
1109
+ function accountBreakdownToBriefRow(row) {
1110
+ return {
1111
+ account_key: row.account_key,
1112
+ account_tool: row.account_tool,
1113
+ account_name: row.account_name,
1114
+ account_email: row.account_email,
1115
+ account_source: row.account_source,
1116
+ sessions: row.sessions,
1117
+ requests: row.requests,
1118
+ input_tokens: 0,
1119
+ output_tokens: 0,
1120
+ cache_read_tokens: 0,
1121
+ cache_create_tokens: 0,
1122
+ cache_create_5m_tokens: 0,
1123
+ cache_create_1h_tokens: 0,
1124
+ total_tokens: row.total_tokens,
1125
+ cost_usd: row.cost_usd,
1126
+ last_active: row.last_active
1127
+ };
1128
+ }
1129
+ function queryFreshness(db, now, machine, localSync) {
1130
+ const requestFilter = machine ? " AND machine_id = ?" : "";
1131
+ const requestRows = db.prepare(`
1132
+ SELECT machine_id, MAX(timestamp) as max_request_at
1133
+ FROM economy_effective_requests
1134
+ WHERE machine_id != ''${requestFilter}
1135
+ GROUP BY machine_id
1136
+ `).all(...machine ? [machine] : []);
1137
+ const registryRows = listMachineRegistry(db).filter((row) => !machine || row.machine_id === machine);
1138
+ const machines = new Map;
1139
+ for (const row of requestRows) {
1140
+ machines.set(row.machine_id, {
1141
+ machine_id: row.machine_id,
1142
+ max_request_at: row.max_request_at,
1143
+ request_age: formatAge(row.max_request_at, now),
1144
+ last_merge_sync_at: null,
1145
+ merge_sync_age: "never"
1146
+ });
1147
+ }
1148
+ for (const row of registryRows) {
1149
+ const current = machines.get(row.machine_id) ?? {
1150
+ machine_id: row.machine_id,
1151
+ max_request_at: null,
1152
+ request_age: "never",
1153
+ last_merge_sync_at: null,
1154
+ merge_sync_age: "never"
1155
+ };
1156
+ const syncAt = latestTimestamp(row.last_pull_at, row.last_push_at, row.updated_at);
1157
+ current.last_merge_sync_at = syncAt;
1158
+ current.merge_sync_age = formatAge(syncAt, now);
1159
+ machines.set(row.machine_id, current);
1160
+ }
1161
+ if (localSync && (!machine || machine === localSync.machineId)) {
1162
+ const current = machines.get(localSync.machineId) ?? {
1163
+ machine_id: localSync.machineId,
1164
+ max_request_at: null,
1165
+ request_age: "never",
1166
+ last_merge_sync_at: null,
1167
+ merge_sync_age: "never"
1168
+ };
1169
+ current.last_merge_sync_at = latestTimestamp(current.last_merge_sync_at, localSync.syncedAt);
1170
+ current.merge_sync_age = formatAge(current.last_merge_sync_at, now);
1171
+ machines.set(localSync.machineId, current);
1172
+ }
1173
+ const rows = [...machines.values()].sort((a, b) => a.machine_id.localeCompare(b.machine_id));
1174
+ return {
1175
+ machines: rows,
1176
+ max_request_line: rows.length ? rows.map((row) => `${row.machine_id} ${row.max_request_at ?? "never"} (${row.request_age})`).join("; ") : "none",
1177
+ merge_sync_line: rows.length ? rows.map((row) => `${row.machine_id} ${row.last_merge_sync_at ?? "never"} (${row.merge_sync_age})`).join("; ") : "none"
1178
+ };
1179
+ }
1180
+ function buildBrief(db, opts = {}) {
1181
+ const now = opts.now ?? new Date;
1182
+ const machine = machineFilter(opts.machine);
1183
+ const sinceInput = opts.since ?? "24h";
1184
+ const since = parseSinceDate(sinceInput, now);
1185
+ const summaries = [
1186
+ {
1187
+ label: "Today",
1188
+ period: "today",
1189
+ ...queryPeriodTotals(db, "today", machine)
1190
+ },
1191
+ {
1192
+ label: "Week",
1193
+ period: "week",
1194
+ ...queryPeriodTotals(db, "week", machine)
1195
+ },
1196
+ {
1197
+ label: `Since ${since.label}`,
1198
+ period: "since",
1199
+ since: since.timestamp,
1200
+ ...querySinceTotals(db, since.timestamp, machine)
1201
+ }
1202
+ ];
1203
+ const machines = queryMachineSinceRows(db, since.timestamp, machine);
1204
+ for (const row of machines) {
1205
+ row.last_data_age = formatAge(row.last_data_at, now);
1206
+ }
1207
+ return {
1208
+ generated_at: now.toISOString(),
1209
+ machine: machine ?? "all",
1210
+ since: {
1211
+ input: sinceInput,
1212
+ timestamp: since.timestamp,
1213
+ label: since.label
1214
+ },
1215
+ summaries,
1216
+ machines,
1217
+ agents: queryAgentSinceRows(db, since.timestamp, machine),
1218
+ accounts: queryAccountSinceRows(db, since.timestamp, machine),
1219
+ freshness: queryFreshness(db, now, machine, opts.currentMachineId && opts.localSyncAt ? { machineId: opts.currentMachineId, syncedAt: opts.localSyncAt.toISOString() } : undefined)
1220
+ };
1221
+ }
1222
+
1223
+ // src/lib/analytics.ts
1224
+ function queryProjectDetail(db, nameOrPath) {
1225
+ const like = `%${nameOrPath}%`;
1226
+ const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
1227
+ FROM economy_effective_sessions
1228
+ WHERE project_name LIKE ? OR project_path LIKE ?
1229
+ ORDER BY started_at DESC`).all(like, like);
1230
+ if (sessions.length === 0)
1231
+ return null;
1232
+ const first = sessions[0];
1233
+ const totalCost = sessions.reduce((s, r) => s + (r.total_cost_usd ?? 0), 0);
1234
+ const totalTokens = sessions.reduce((s, r) => s + (r.total_tokens ?? 0), 0);
1235
+ const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
1236
+ FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
1237
+ WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
1238
+ AND r.timestamp >= DATE('now', '-14 days')
1239
+ GROUP BY date ORDER BY date ASC`).all(like, like);
1240
+ const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
1241
+ FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
1242
+ WHERE s.project_name LIKE ? OR s.project_path LIKE ?
1243
+ GROUP BY r.model ORDER BY cost_usd DESC LIMIT 5`).all(like, like);
1244
+ 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 }));
1245
+ return {
1246
+ project_name: first.project_name || nameOrPath,
1247
+ project_path: first.project_path || "",
1248
+ total_cost_usd: totalCost,
1249
+ total_tokens: totalTokens,
1250
+ sessions: sessions.length,
1251
+ daily,
1252
+ models,
1253
+ top_sessions
1254
+ };
1255
+ }
1256
+ function exportDateWhere(column, period) {
1257
+ if (!["today", "yesterday", "week", "month", "year", "all"].includes(period)) {
1258
+ throw new RangeError("Invalid export period");
1259
+ }
1260
+ return calendarPeriodWhere(period, column);
1261
+ }
1262
+ function queryExportRows(db, type, period) {
1263
+ if (type === "requests") {
1264
+ const where2 = exportDateWhere("timestamp", period);
1265
+ return db.prepare(`SELECT * FROM economy_effective_requests WHERE ${where2} ORDER BY timestamp ASC`).all();
1266
+ }
1267
+ const where = exportDateWhere("started_at", period);
1268
+ return db.prepare(`SELECT * FROM economy_effective_sessions WHERE ${where} ORDER BY started_at DESC`).all();
1269
+ }
1270
+ function queryRangeStats(db, from, to) {
1271
+ const r = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost, COUNT(*) as requests,
1272
+ COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens+cache_create_tokens),0) as tokens
1273
+ FROM economy_effective_requests WHERE DATE(timestamp) BETWEEN ? AND ?`).get(from, to);
1274
+ const s = db.prepare(`SELECT COUNT(*) as sessions FROM economy_effective_sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
1275
+ return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
1276
+ }
1277
+ function queryForecast(db, now = new Date) {
1278
+ const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
1279
+ const dayOfMonth = now.getDate();
1280
+ const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
1281
+ const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(monthStart);
1282
+ const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
1283
+ const projected = dailyAvg * daysInMonth;
1284
+ const sevenDaysAgo = new Date(now);
1285
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
1286
+ const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
1287
+ const last7DailyAvg = last7.cost / 7;
1288
+ const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
1289
+ FROM economy_effective_requests WHERE DATE(timestamp) >= ? GROUP BY date ORDER BY cost_usd ASC`).all(monthStart);
1290
+ return {
1291
+ day_of_month: dayOfMonth,
1292
+ days_in_month: daysInMonth,
1293
+ spent_so_far_usd: monthSoFar.cost,
1294
+ daily_avg_usd: dailyAvg,
1295
+ projected_usd: projected,
1296
+ last7_daily_avg_usd: last7DailyAvg,
1297
+ last7_projected_usd: last7DailyAvg * daysInMonth,
1298
+ cheapest_day: dailyCosts[0] ?? null,
1299
+ most_expensive_day: dailyCosts[dailyCosts.length - 1] ?? null
1300
+ };
1301
+ }
1302
+ function queryModelEfficiency(db) {
1303
+ return db.prepare(`SELECT model,
1304
+ COALESCE(SUM(input_tokens),0) as input,
1305
+ COALESCE(SUM(output_tokens),0) as output,
1306
+ COALESCE(SUM(cache_read_tokens),0) as cache_read,
1307
+ COALESCE(SUM(cache_create_tokens),0) as cache_write,
1308
+ COUNT(*) as requests,
1309
+ COALESCE(SUM(cost_usd),0) as cost
1310
+ FROM economy_effective_requests GROUP BY model ORDER BY cost DESC`).all();
1311
+ }
1312
+
1313
+ export { packageMetadata, requireCurrentSavingsReport, querySavingsSummary, queryBillingDiff, requireCurrentBillingDiffSummary, billingDriftCheck, buildBrief, queryProjectDetail, queryExportRows, queryRangeStats, queryForecast, queryModelEfficiency, LOCAL_STORAGE_OPT_IN_KEYS, economyCloudStorage, cloudListItems, cloudObject, economyTransportReport };