@hasna/economy 0.4.0 → 0.5.1

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