@moneysiren/cli 0.1.0-alpha.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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/dist/apps/cli/src/cli.d.ts +56 -0
  4. package/dist/apps/cli/src/cli.js +182 -0
  5. package/dist/apps/cli/src/commands/dashboard.d.ts +3 -0
  6. package/dist/apps/cli/src/commands/dashboard.js +239 -0
  7. package/dist/apps/cli/src/commands/doctor.d.ts +3 -0
  8. package/dist/apps/cli/src/commands/doctor.js +25 -0
  9. package/dist/apps/cli/src/commands/init.d.ts +3 -0
  10. package/dist/apps/cli/src/commands/init.js +18 -0
  11. package/dist/apps/cli/src/commands/install.d.ts +3 -0
  12. package/dist/apps/cli/src/commands/install.js +116 -0
  13. package/dist/apps/cli/src/commands/modes.d.ts +3 -0
  14. package/dist/apps/cli/src/commands/modes.js +65 -0
  15. package/dist/apps/cli/src/commands/notify.d.ts +3 -0
  16. package/dist/apps/cli/src/commands/notify.js +430 -0
  17. package/dist/apps/cli/src/commands/report.d.ts +3 -0
  18. package/dist/apps/cli/src/commands/report.js +206 -0
  19. package/dist/apps/cli/src/commands/runtime.d.ts +5 -0
  20. package/dist/apps/cli/src/commands/runtime.js +133 -0
  21. package/dist/apps/cli/src/commands/shared.d.ts +9 -0
  22. package/dist/apps/cli/src/commands/shared.js +29 -0
  23. package/dist/apps/cli/src/commands/summary.d.ts +3 -0
  24. package/dist/apps/cli/src/commands/summary.js +15 -0
  25. package/dist/apps/cli/src/commands/sync.d.ts +3 -0
  26. package/dist/apps/cli/src/commands/sync.js +393 -0
  27. package/dist/apps/cli/src/commands/theme.d.ts +3 -0
  28. package/dist/apps/cli/src/commands/theme.js +181 -0
  29. package/dist/apps/cli/src/home.d.ts +7 -0
  30. package/dist/apps/cli/src/home.js +97 -0
  31. package/dist/apps/cli/src/index.d.ts +3 -0
  32. package/dist/apps/cli/src/index.js +14 -0
  33. package/dist/apps/cli/src/install-profile.d.ts +35 -0
  34. package/dist/apps/cli/src/install-profile.js +124 -0
  35. package/dist/apps/cli/src/install-selector.d.ts +10 -0
  36. package/dist/apps/cli/src/install-selector.js +66 -0
  37. package/dist/apps/cli/src/interactive.d.ts +3 -0
  38. package/dist/apps/cli/src/interactive.js +32 -0
  39. package/dist/apps/cli/src/postinstall.d.ts +3 -0
  40. package/dist/apps/cli/src/postinstall.js +42 -0
  41. package/dist/apps/cli/src/runtime-adapter.d.ts +24 -0
  42. package/dist/apps/cli/src/runtime-adapter.js +185 -0
  43. package/dist/apps/cli/src/slash.d.ts +15 -0
  44. package/dist/apps/cli/src/slash.js +202 -0
  45. package/dist/apps/cli/src/summary-model.d.ts +51 -0
  46. package/dist/apps/cli/src/summary-model.js +136 -0
  47. package/dist/apps/cli/src/theme.d.ts +18 -0
  48. package/dist/apps/cli/src/theme.js +118 -0
  49. package/dist/packages/config/src/index.d.ts +3 -0
  50. package/dist/packages/config/src/index.js +3 -0
  51. package/dist/packages/config/src/load.d.ts +3 -0
  52. package/dist/packages/config/src/load.js +77 -0
  53. package/dist/packages/config/src/schema.d.ts +46 -0
  54. package/dist/packages/config/src/schema.js +25 -0
  55. package/dist/packages/connectors/aws/src/cost-explorer.d.ts +34 -0
  56. package/dist/packages/connectors/aws/src/cost-explorer.js +43 -0
  57. package/dist/packages/connectors/aws/src/index.d.ts +35 -0
  58. package/dist/packages/connectors/aws/src/index.js +67 -0
  59. package/dist/packages/connectors/aws/src/normalize.d.ts +69 -0
  60. package/dist/packages/connectors/aws/src/normalize.js +141 -0
  61. package/dist/packages/connectors/aws/src/sdk-client.d.ts +6 -0
  62. package/dist/packages/connectors/aws/src/sdk-client.js +21 -0
  63. package/dist/packages/connectors/cloudflare/src/client.d.ts +23 -0
  64. package/dist/packages/connectors/cloudflare/src/client.js +107 -0
  65. package/dist/packages/connectors/cloudflare/src/index.d.ts +33 -0
  66. package/dist/packages/connectors/cloudflare/src/index.js +81 -0
  67. package/dist/packages/connectors/cloudflare/src/normalize.d.ts +113 -0
  68. package/dist/packages/connectors/cloudflare/src/normalize.js +288 -0
  69. package/dist/packages/connectors/mock/src/index.d.ts +58 -0
  70. package/dist/packages/connectors/mock/src/index.js +66 -0
  71. package/dist/packages/connectors/openai/src/index.d.ts +55 -0
  72. package/dist/packages/connectors/openai/src/index.js +169 -0
  73. package/dist/packages/connectors/openai/src/normalize.d.ts +91 -0
  74. package/dist/packages/connectors/openai/src/normalize.js +180 -0
  75. package/dist/packages/connectors/supabase/src/client.d.ts +22 -0
  76. package/dist/packages/connectors/supabase/src/client.js +132 -0
  77. package/dist/packages/connectors/supabase/src/index.d.ts +33 -0
  78. package/dist/packages/connectors/supabase/src/index.js +87 -0
  79. package/dist/packages/connectors/supabase/src/normalize.d.ts +106 -0
  80. package/dist/packages/connectors/supabase/src/normalize.js +266 -0
  81. package/dist/packages/core/src/collector.d.ts +12 -0
  82. package/dist/packages/core/src/collector.js +68 -0
  83. package/dist/packages/core/src/index.d.ts +5 -0
  84. package/dist/packages/core/src/index.js +4 -0
  85. package/dist/packages/core/src/provider.d.ts +18 -0
  86. package/dist/packages/core/src/provider.js +2 -0
  87. package/dist/packages/core/src/risk-engine.d.ts +9 -0
  88. package/dist/packages/core/src/risk-engine.js +4 -0
  89. package/dist/packages/core/src/snapshots.d.ts +49 -0
  90. package/dist/packages/core/src/snapshots.js +9 -0
  91. package/dist/packages/db/src/client.d.ts +11 -0
  92. package/dist/packages/db/src/client.js +14 -0
  93. package/dist/packages/db/src/index.d.ts +6 -0
  94. package/dist/packages/db/src/index.js +6 -0
  95. package/dist/packages/db/src/local-store.d.ts +161 -0
  96. package/dist/packages/db/src/local-store.js +623 -0
  97. package/dist/packages/db/src/migrate.d.ts +17 -0
  98. package/dist/packages/db/src/migrate.js +35 -0
  99. package/dist/packages/db/src/schema.d.ts +5 -0
  100. package/dist/packages/db/src/schema.js +120 -0
  101. package/dist/packages/db/src/sqlite-bin.d.ts +3 -0
  102. package/dist/packages/db/src/sqlite-bin.js +16 -0
  103. package/dist/packages/local-api/src/index.d.ts +2 -0
  104. package/dist/packages/local-api/src/index.js +2 -0
  105. package/dist/packages/local-api/src/server.d.ts +36 -0
  106. package/dist/packages/local-api/src/server.js +310 -0
  107. package/dist/packages/report/src/daily.d.ts +24 -0
  108. package/dist/packages/report/src/daily.js +9 -0
  109. package/dist/packages/report/src/index.d.ts +4 -0
  110. package/dist/packages/report/src/index.js +4 -0
  111. package/dist/packages/report/src/korean.d.ts +3 -0
  112. package/dist/packages/report/src/korean.js +62 -0
  113. package/dist/packages/report/src/slack.d.ts +34 -0
  114. package/dist/packages/report/src/slack.js +134 -0
  115. package/dist/packages/runtime/src/index.d.ts +2 -0
  116. package/dist/packages/runtime/src/index.js +2 -0
  117. package/dist/packages/runtime/src/runtime.d.ts +26 -0
  118. package/dist/packages/runtime/src/runtime.js +182 -0
  119. package/dist/packages/view-model/src/index.d.ts +3 -0
  120. package/dist/packages/view-model/src/index.js +3 -0
  121. package/dist/packages/view-model/src/notification-preferences-model.d.ts +47 -0
  122. package/dist/packages/view-model/src/notification-preferences-model.js +218 -0
  123. package/dist/packages/view-model/src/notification-preferences.d.ts +6 -0
  124. package/dist/packages/view-model/src/notification-preferences.js +36 -0
  125. package/dist/packages/view-model/src/view-model.d.ts +193 -0
  126. package/dist/packages/view-model/src/view-model.js +684 -0
  127. package/package.json +49 -0
  128. package/scripts/postinstall.mjs +11 -0
@@ -0,0 +1,684 @@
1
+ import { DEFAULT_NOTIFICATION_PREFERENCES, } from "./notification-preferences.js";
2
+ const DEFAULT_CURRENCY = "USD";
3
+ const OPENAI_PROVIDER_KEY = "openai";
4
+ const AWS_PROVIDER_KEY = "aws";
5
+ const SUPABASE_PROVIDER_KEY = "supabase";
6
+ const CLOUDFLARE_PROVIDER_KEY = "cloudflare";
7
+ const CODEX_CLI_PROVIDER_KEY = "codex-cli";
8
+ const CLAUDE_CLI_PROVIDER_KEY = "claude-cli";
9
+ const SENSITIVE_TEXT_PATTERN = /(https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9/_-]+|\b(?:sk|sbp|xox[baprs])[-_][A-Za-z0-9_-]+\b|\bacct[_-][A-Za-z0-9_-]+\b|\b(?:proj|project)[_-][A-Za-z0-9_-]+\b|\b(?:in|invoice)[_-][A-Za-z0-9_-]+\b|[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,})/gi;
10
+ const EMPTY_STORE = {
11
+ providers: [],
12
+ usageSnapshots: [],
13
+ billingSnapshots: [],
14
+ serviceHealthSnapshots: [],
15
+ costEstimates: [],
16
+ alerts: [],
17
+ };
18
+ export async function readOperationsOverview(options = {}) {
19
+ const store = await resolveStore(options);
20
+ const now = options.now?.() ?? new Date();
21
+ return buildOperationsOverview(store, {
22
+ generatedAt: now.toISOString(),
23
+ });
24
+ }
25
+ export function buildOperationsOverview(store, options) {
26
+ const providerNames = new Map(store.providers.map((provider) => [provider.key, safeText(provider.displayName)]));
27
+ const providers = store.providers.map((provider) => {
28
+ const providerKey = safeText(provider.key);
29
+ const billingSnapshots = store.billingSnapshots.filter((snapshot) => snapshot.providerKey === provider.key);
30
+ const costEstimates = store.costEstimates.filter((estimate) => estimate.providerKey === provider.key);
31
+ const usageSnapshots = store.usageSnapshots.filter((snapshot) => snapshot.providerKey === provider.key);
32
+ const healthSnapshots = store.serviceHealthSnapshots.filter((snapshot) => snapshot.providerKey === provider.key);
33
+ const alerts = store.alerts.filter((alert) => alert.providerKey === provider.key);
34
+ const healthStatus = summarizeHealth(healthSnapshots.map((snapshot) => snapshot.status));
35
+ return {
36
+ providerKey,
37
+ displayName: safeText(provider.displayName),
38
+ estimatedAmountMinor: sum(costEstimates.map((estimate) => estimate.estimatedAmountMinor)),
39
+ billingAmountMinor: sum(billingSnapshots.map((snapshot) => snapshot.amountMinor)),
40
+ currency: summarizeCurrency([
41
+ ...costEstimates.map((estimate) => estimate.currency),
42
+ ...billingSnapshots.map((snapshot) => snapshot.currency),
43
+ ]),
44
+ usageSnapshotCount: usageSnapshots.length,
45
+ costEstimateCount: costEstimates.length,
46
+ latestCollectedAt: latestIso([
47
+ ...billingSnapshots.map((snapshot) => snapshot.collectedAt),
48
+ ...costEstimates.map((estimate) => estimate.collectedAt),
49
+ ...usageSnapshots.map((snapshot) => snapshot.collectedAt),
50
+ ...healthSnapshots.map((snapshot) => snapshot.collectedAt),
51
+ ]),
52
+ healthStatus,
53
+ riskLevel: summarizeRisk(alerts.map((alert) => alert.severity), healthStatus),
54
+ alertCount: alerts.length,
55
+ };
56
+ });
57
+ const alerts = store.alerts
58
+ .map((alert) => {
59
+ const providerKey = alert.providerKey === undefined ? null : safeText(alert.providerKey);
60
+ return {
61
+ providerKey,
62
+ displayName: alert.providerKey === undefined
63
+ ? null
64
+ : providerNames.get(alert.providerKey) ?? safeText(alert.providerKey),
65
+ severity: alert.severity,
66
+ category: safeText(alert.category),
67
+ title: safeText(alert.title),
68
+ message: safeText(alert.message),
69
+ createdAt: alert.createdAt,
70
+ };
71
+ })
72
+ .sort((first, second) => second.createdAt.localeCompare(first.createdAt))
73
+ .slice(0, 5);
74
+ const summaryCurrency = summarizeCurrency([
75
+ ...store.costEstimates.map((estimate) => estimate.currency),
76
+ ...store.billingSnapshots.map((snapshot) => snapshot.currency),
77
+ ]);
78
+ return {
79
+ generatedAt: options.generatedAt,
80
+ localOnly: true,
81
+ secretsReturned: false,
82
+ source: store.providers.length === 0 ? "empty" : "sqlite",
83
+ summary: {
84
+ providerCount: store.providers.length,
85
+ connectedProviderCount: providers.filter((provider) => provider.latestCollectedAt !== null).length,
86
+ totalEstimatedAmountMinor: sum(store.costEstimates.map((estimate) => estimate.estimatedAmountMinor)),
87
+ totalBillingAmountMinor: sum(store.billingSnapshots.map((snapshot) => snapshot.amountMinor)),
88
+ currency: summaryCurrency,
89
+ usageSnapshotCount: store.usageSnapshots.length,
90
+ costEstimateCount: store.costEstimates.length,
91
+ alertCount: store.alerts.length,
92
+ criticalAlertCount: store.alerts.filter((alert) => alert.severity === "critical").length,
93
+ healthStatus: summarizeHealth(providers.map((provider) => provider.healthStatus)),
94
+ },
95
+ providers,
96
+ alerts,
97
+ };
98
+ }
99
+ export async function readTodayLiveView(options = {}) {
100
+ const now = options.now?.() ?? new Date();
101
+ const store = await resolveStore(options);
102
+ const buildOptions = {
103
+ generatedAt: now.toISOString(),
104
+ now,
105
+ timezone: options.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC",
106
+ ...(options.liveProviders === undefined ? {} : { liveProviders: options.liveProviders }),
107
+ };
108
+ return buildTodayLiveView(store, buildOptions);
109
+ }
110
+ export function buildTodayLiveView(store, options) {
111
+ const dateKey = dateKeyInTimezone(options.now, options.timezone);
112
+ const providers = options.liveProviders === undefined
113
+ ? todayProvidersFromStore(store, dateKey)
114
+ : options.liveProviders.map((provider) => ({
115
+ ...provider,
116
+ providerKey: safeText(provider.providerKey),
117
+ displayName: safeText(provider.displayName),
118
+ metrics: (provider.metrics ?? []).map((metric) => ({
119
+ key: safeText(metric.key),
120
+ value: metric.value,
121
+ unit: safeText(metric.unit),
122
+ })),
123
+ }));
124
+ const includedProviders = providers.filter((provider) => provider.included && provider.todayLiveAmountMinor !== null);
125
+ const currency = singleCurrency(includedProviders.map((provider) => provider.currency)) ?? DEFAULT_CURRENCY;
126
+ const canSum = includedProviders.length > 0 && includedProviders.every((provider) => provider.currency === currency);
127
+ return {
128
+ generatedAt: options.generatedAt,
129
+ localOnly: true,
130
+ secretsReturned: false,
131
+ timezone: options.timezone,
132
+ dateKey,
133
+ cacheState: providers.length === 0
134
+ ? "empty"
135
+ : providers.some((provider) => provider.freshness === "live")
136
+ ? "fresh"
137
+ : "stale",
138
+ summary: {
139
+ providerCount: providers.length,
140
+ includedProviderCount: includedProviders.length,
141
+ todayLiveAmountMinor: canSum ? sum(includedProviders.map((provider) => provider.todayLiveAmountMinor ?? 0)) : null,
142
+ currency,
143
+ },
144
+ providers,
145
+ };
146
+ }
147
+ export async function readNotificationDigest(options = {}) {
148
+ const overview = options.overview ?? await readOperationsOverview(options);
149
+ const todayLive = options.todayLive ?? await readTodayLiveView(options);
150
+ return buildNotificationDigest(overview, todayLive, options.notificationPreferences);
151
+ }
152
+ export function buildNotificationDigest(overview, todayLive, preferences = DEFAULT_NOTIFICATION_PREFERENCES) {
153
+ const criticalAlerts = overview.summary.criticalAlertCount;
154
+ const warningAlerts = overview.summary.alertCount - criticalAlerts;
155
+ const status = criticalAlerts > 0
156
+ ? "critical"
157
+ : warningAlerts > 0 || overview.summary.healthStatus !== "ok"
158
+ ? "attention"
159
+ : "ok";
160
+ const items = buildDigestItems(overview, todayLive, {
161
+ criticalAlerts,
162
+ warningAlerts,
163
+ });
164
+ return {
165
+ generatedAt: overview.generatedAt,
166
+ localOnly: true,
167
+ secretsReturned: false,
168
+ title: "MoneySiren",
169
+ status,
170
+ suppressedReason: preferences.enabled
171
+ ? preferences.digestEnabled
172
+ ? null
173
+ : "digest_disabled"
174
+ : "notifications_disabled",
175
+ items: preferences.enabled && preferences.digestEnabled
176
+ ? filterDigestItems(items, preferences.selectedWidgets)
177
+ : [],
178
+ };
179
+ }
180
+ export async function readTrayMenuModel(options = {}) {
181
+ const digest = options.digest ?? await readNotificationDigest(options);
182
+ return buildTrayMenuModel(digest);
183
+ }
184
+ function filterDigestItems(items, selectedWidgets) {
185
+ const itemsByWidget = new Map(items.map((item) => [item.widgetKey, item]));
186
+ return selectedWidgets.flatMap((widgetKey) => {
187
+ const item = itemsByWidget.get(widgetKey);
188
+ return item === undefined ? [] : [item];
189
+ });
190
+ }
191
+ export function buildTrayMenuModel(digest) {
192
+ const summaryItem = digest.items.find((item) => item.kind === "summary");
193
+ const statusLabel = digest.status === "ok"
194
+ ? digest.suppressedReason === null
195
+ ? "All monitored providers OK"
196
+ : "Notifications paused"
197
+ : digest.status === "critical"
198
+ ? "Critical alert needs attention"
199
+ : "Provider attention needed";
200
+ return {
201
+ generatedAt: digest.generatedAt,
202
+ localOnly: true,
203
+ secretsReturned: false,
204
+ title: digest.title,
205
+ subtitle: summaryItem?.value ?? "No local data",
206
+ status: digest.status,
207
+ items: [
208
+ {
209
+ id: "status",
210
+ label: digest.suppressedReason === null ? statusLabel : `${statusLabel}: ${digest.suppressedReason}`,
211
+ enabled: false,
212
+ kind: "status",
213
+ },
214
+ ...digest.items.map((item) => ({
215
+ id: `widget-${item.widgetKey}`,
216
+ label: `${item.label}: ${item.value}`,
217
+ enabled: false,
218
+ kind: "status",
219
+ ...(item.clickPath === undefined ? {} : { urlPath: item.clickPath }),
220
+ })),
221
+ ...(digest.items.length === 0
222
+ ? []
223
+ : [{
224
+ id: "separator-widgets",
225
+ label: "",
226
+ enabled: false,
227
+ kind: "separator",
228
+ }]),
229
+ {
230
+ id: "show-hud",
231
+ label: "Show HUD",
232
+ enabled: true,
233
+ kind: "command",
234
+ action: "show_hud",
235
+ urlPath: "/hud?locale=ko",
236
+ },
237
+ {
238
+ id: "open-dashboard",
239
+ label: "Open Dashboard",
240
+ enabled: true,
241
+ kind: "command",
242
+ action: "open_url",
243
+ urlPath: "/ko/dashboard/overview",
244
+ },
245
+ {
246
+ id: "open-notification-settings",
247
+ label: "Notification Settings",
248
+ enabled: true,
249
+ kind: "command",
250
+ action: "open_url",
251
+ urlPath: "/ko/settings/notifications",
252
+ },
253
+ {
254
+ id: "refresh-now",
255
+ label: "Refresh Now",
256
+ enabled: true,
257
+ kind: "command",
258
+ action: "refresh_live",
259
+ },
260
+ {
261
+ id: "separator-main",
262
+ label: "",
263
+ enabled: false,
264
+ kind: "separator",
265
+ },
266
+ {
267
+ id: "quit",
268
+ label: "Quit",
269
+ enabled: true,
270
+ kind: "command",
271
+ action: "quit",
272
+ },
273
+ ],
274
+ };
275
+ }
276
+ function buildDigestItems(overview, todayLive, alerts) {
277
+ const riskCount = alerts.criticalAlerts + alerts.warningAlerts;
278
+ const staleConnectionCount = todayLive.providers.filter((provider) => provider.freshness === "stale" ||
279
+ provider.freshness === "error" ||
280
+ provider.freshness === "locked" ||
281
+ provider.freshness === "unavailable").length;
282
+ const awsOverview = findOverviewProvider(overview, AWS_PROVIDER_KEY);
283
+ const openAiToday = amountFromTodayProviders(todayLive, OPENAI_PROVIDER_KEY);
284
+ const openAiTokens = tokenTotalFromProviders(todayProviders(todayLive, OPENAI_PROVIDER_KEY));
285
+ const supabaseOverview = findOverviewProvider(overview, SUPABASE_PROVIDER_KEY);
286
+ const supabaseToday = firstTodayProvider(todayLive, SUPABASE_PROVIDER_KEY);
287
+ const cloudflareOverview = findOverviewProvider(overview, CLOUDFLARE_PROVIDER_KEY);
288
+ const cloudflareToday = amountFromTodayProviders(todayLive, CLOUDFLARE_PROVIDER_KEY);
289
+ return [
290
+ {
291
+ widgetKey: "month_forecast",
292
+ kind: "summary",
293
+ severity: "info",
294
+ label: "Month estimate",
295
+ value: formatMinorAmount(overview.summary.totalEstimatedAmountMinor, overview.summary.currency),
296
+ clickPath: "/ko/dashboard/forecast",
297
+ },
298
+ {
299
+ widgetKey: "today_live_cost",
300
+ kind: "live",
301
+ severity: "info",
302
+ label: "Today live",
303
+ value: todayLive.summary.todayLiveAmountMinor === null
304
+ ? "Not available"
305
+ : formatMinorAmount(todayLive.summary.todayLiveAmountMinor, todayLive.summary.currency),
306
+ clickPath: "/ko/dashboard/today",
307
+ },
308
+ {
309
+ widgetKey: "risk_high_count",
310
+ kind: "risk",
311
+ severity: alerts.criticalAlerts > 0 ? "critical" : alerts.warningAlerts > 0 ? "warning" : "info",
312
+ label: "High risks",
313
+ value: String(riskCount),
314
+ clickPath: "/ko/dashboard/risks",
315
+ },
316
+ {
317
+ widgetKey: "stale_connection_count",
318
+ kind: "risk",
319
+ severity: staleConnectionCount > 0 ? "warning" : "info",
320
+ label: "Stale connections",
321
+ value: String(staleConnectionCount),
322
+ clickPath: "/ko/settings/connections",
323
+ },
324
+ {
325
+ widgetKey: "aws_month_forecast",
326
+ kind: "summary",
327
+ severity: providerRiskSeverity(awsOverview),
328
+ label: "AWS month estimate",
329
+ value: awsOverview === undefined
330
+ ? "Not available"
331
+ : formatMinorAmount(awsOverview.estimatedAmountMinor, awsOverview.currency),
332
+ clickPath: "/ko/services/aws",
333
+ },
334
+ {
335
+ widgetKey: "openai_today_cost",
336
+ kind: "live",
337
+ severity: "info",
338
+ label: "OpenAI today",
339
+ value: openAiToday === null ? "Not available" : formatMinorAmount(openAiToday.amountMinor, openAiToday.currency),
340
+ clickPath: "/ko/services/openai",
341
+ },
342
+ {
343
+ widgetKey: "openai_today_tokens",
344
+ kind: "usage",
345
+ severity: "info",
346
+ label: "OpenAI tokens",
347
+ value: openAiTokens === null ? "Not available" : formatTokens(openAiTokens),
348
+ clickPath: "/ko/services/openai",
349
+ },
350
+ cliRemainingPercentItem({
351
+ widgetKey: "claude_five_hour_percent",
352
+ label: "Claude 5h remaining",
353
+ providerKey: CLAUDE_CLI_PROVIDER_KEY,
354
+ todayLive,
355
+ usedPercentMetricKey: "five_hour_limit_percent",
356
+ remainingTokensMetricKey: "five_hour_remaining_tokens",
357
+ usedTokensMetricKey: "five_hour_tokens",
358
+ clickPath: "/ko/services/claude-cli",
359
+ }),
360
+ cliRemainingPercentItem({
361
+ widgetKey: "claude_weekly_percent",
362
+ label: "Claude weekly remaining",
363
+ providerKey: CLAUDE_CLI_PROVIDER_KEY,
364
+ todayLive,
365
+ usedPercentMetricKey: "weekly_limit_percent",
366
+ remainingTokensMetricKey: "weekly_remaining_tokens",
367
+ usedTokensMetricKey: "weekly_tokens",
368
+ clickPath: "/ko/services/claude-cli",
369
+ }),
370
+ cliRemainingPercentItem({
371
+ widgetKey: "codex_five_hour_percent",
372
+ label: "Codex 5h remaining",
373
+ providerKey: CODEX_CLI_PROVIDER_KEY,
374
+ todayLive,
375
+ usedPercentMetricKey: "five_hour_limit_percent",
376
+ remainingTokensMetricKey: "five_hour_remaining_tokens",
377
+ usedTokensMetricKey: "five_hour_tokens",
378
+ clickPath: "/ko/services/codex-cli",
379
+ }),
380
+ cliRemainingPercentItem({
381
+ widgetKey: "codex_weekly_percent",
382
+ label: "Codex weekly remaining",
383
+ providerKey: CODEX_CLI_PROVIDER_KEY,
384
+ todayLive,
385
+ usedPercentMetricKey: "weekly_limit_percent",
386
+ remainingTokensMetricKey: "weekly_remaining_tokens",
387
+ usedTokensMetricKey: "weekly_tokens",
388
+ clickPath: "/ko/services/codex-cli",
389
+ }),
390
+ {
391
+ widgetKey: "supabase_usage_health",
392
+ kind: "health",
393
+ severity: healthSeverity(supabaseOverview?.healthStatus),
394
+ label: "Supabase health",
395
+ value: providerHealthValue(supabaseOverview, supabaseToday),
396
+ ...(supabaseToday === undefined
397
+ ? {}
398
+ : {
399
+ freshness: supabaseToday.freshness,
400
+ confidence: supabaseToday.confidence,
401
+ }),
402
+ clickPath: "/ko/services/supabase",
403
+ },
404
+ {
405
+ widgetKey: "cloudflare_month_to_date",
406
+ kind: "summary",
407
+ severity: providerRiskSeverity(cloudflareOverview),
408
+ label: "Cloudflare MTD",
409
+ value: cloudflareOverview === undefined
410
+ ? cloudflareToday === null
411
+ ? "Not available"
412
+ : formatMinorAmount(cloudflareToday.amountMinor, cloudflareToday.currency)
413
+ : formatMinorAmount(cloudflareOverview.estimatedAmountMinor, cloudflareOverview.currency),
414
+ clickPath: "/ko/services/cloudflare",
415
+ },
416
+ ];
417
+ }
418
+ function cliRemainingPercentItem(options) {
419
+ const providers = todayProviders(options.todayLive, options.providerKey);
420
+ const percent = remainingPercentFromMetrics(providers, options.remainingTokensMetricKey, options.usedTokensMetricKey, options.usedPercentMetricKey);
421
+ const firstProvider = providers[0];
422
+ return {
423
+ widgetKey: options.widgetKey,
424
+ kind: "usage",
425
+ severity: remainingPercentSeverity(percent),
426
+ label: options.label,
427
+ value: percent === null ? "Not available" : formatPercent(percent),
428
+ ...(firstProvider === undefined
429
+ ? {}
430
+ : {
431
+ freshness: firstProvider.freshness,
432
+ confidence: firstProvider.confidence,
433
+ }),
434
+ clickPath: options.clickPath,
435
+ };
436
+ }
437
+ async function resolveStore(options) {
438
+ if (options.store !== undefined) {
439
+ return options.store;
440
+ }
441
+ return options.readStore === undefined ? EMPTY_STORE : options.readStore();
442
+ }
443
+ function todayProvidersFromStore(store, dateKey) {
444
+ const providerNames = new Map(store.providers.map((provider) => [provider.key, safeText(provider.displayName)]));
445
+ const estimatesByProvider = new Map();
446
+ for (const estimate of store.costEstimates) {
447
+ if (!dateKeyMatchesIso(estimate.collectedAt, dateKey)) {
448
+ continue;
449
+ }
450
+ estimatesByProvider.set(estimate.providerKey, [
451
+ ...(estimatesByProvider.get(estimate.providerKey) ?? []),
452
+ estimate,
453
+ ]);
454
+ }
455
+ return [...estimatesByProvider.entries()]
456
+ .map(([providerKey, estimates]) => {
457
+ const currency = singleCurrency(estimates.map((estimate) => estimate.currency)) ?? DEFAULT_CURRENCY;
458
+ const canInclude = estimates.length > 0 && estimates.every((estimate) => estimate.currency === currency);
459
+ return {
460
+ providerKey: safeText(providerKey),
461
+ displayName: providerNames.get(providerKey) ?? safeText(providerKey),
462
+ checkedAt: latestIso(estimates.map((estimate) => estimate.collectedAt)),
463
+ freshness: "stale",
464
+ confidence: highestConfidence(estimates.map((estimate) => estimate.confidence)),
465
+ todayLiveAmountMinor: canInclude ? sum(estimates.map((estimate) => estimate.estimatedAmountMinor)) : null,
466
+ currency,
467
+ included: canInclude,
468
+ metrics: [],
469
+ };
470
+ })
471
+ .sort((first, second) => first.providerKey.localeCompare(second.providerKey));
472
+ }
473
+ function findOverviewProvider(overview, providerKey) {
474
+ return overview.providers.find((provider) => provider.providerKey === providerKey);
475
+ }
476
+ function todayProviders(todayLive, providerKey) {
477
+ return todayLive.providers.filter((provider) => provider.providerKey === providerKey);
478
+ }
479
+ function firstTodayProvider(todayLive, providerKey) {
480
+ return todayProviders(todayLive, providerKey)[0];
481
+ }
482
+ function amountFromTodayProviders(todayLive, providerKey) {
483
+ const providers = todayProviders(todayLive, providerKey).filter((provider) => provider.included && provider.todayLiveAmountMinor !== null);
484
+ if (providers.length === 0) {
485
+ return null;
486
+ }
487
+ const currency = singleCurrency(providers.map((provider) => provider.currency));
488
+ if (currency === null) {
489
+ return null;
490
+ }
491
+ return {
492
+ amountMinor: sum(providers.map((provider) => provider.todayLiveAmountMinor ?? 0)),
493
+ currency,
494
+ };
495
+ }
496
+ function tokenTotalFromProviders(providers) {
497
+ const totalTokens = metricSum(providers, "total_tokens");
498
+ if (totalTokens !== null) {
499
+ return totalTokens;
500
+ }
501
+ const componentTokens = sumNullable([
502
+ metricSum(providers, "input_tokens"),
503
+ metricSum(providers, "output_tokens"),
504
+ metricSum(providers, "cache_tokens"),
505
+ metricSum(providers, "reasoning_tokens"),
506
+ ]);
507
+ return componentTokens === 0 ? null : componentTokens;
508
+ }
509
+ function metricSum(providers, metricKey) {
510
+ let found = false;
511
+ let total = 0;
512
+ for (const provider of providers) {
513
+ for (const metric of provider.metrics) {
514
+ if (metric.key !== metricKey) {
515
+ continue;
516
+ }
517
+ found = true;
518
+ total += metric.value;
519
+ }
520
+ }
521
+ return found ? total : null;
522
+ }
523
+ function metricFirst(providers, metricKey) {
524
+ for (const provider of providers) {
525
+ const metric = provider.metrics.find((item) => item.key === metricKey);
526
+ if (metric !== undefined) {
527
+ return metric.value;
528
+ }
529
+ }
530
+ return null;
531
+ }
532
+ function remainingPercentFromMetrics(providers, remainingTokensMetricKey, usedTokensMetricKey, usedPercentMetricKey) {
533
+ const remainingTokens = metricSum(providers, remainingTokensMetricKey);
534
+ const usedTokens = metricSum(providers, usedTokensMetricKey);
535
+ if (remainingTokens !== null && usedTokens !== null) {
536
+ const totalTokens = remainingTokens + usedTokens;
537
+ if (totalTokens > 0) {
538
+ return clampPercent((remainingTokens / totalTokens) * 100);
539
+ }
540
+ }
541
+ const usedPercent = metricFirst(providers, usedPercentMetricKey);
542
+ return usedPercent === null ? null : clampPercent(100 - usedPercent);
543
+ }
544
+ function providerRiskSeverity(provider) {
545
+ if (provider?.riskLevel === "critical") {
546
+ return "critical";
547
+ }
548
+ if (provider?.riskLevel === "warning") {
549
+ return "warning";
550
+ }
551
+ return "info";
552
+ }
553
+ function healthSeverity(status) {
554
+ if (status === "down") {
555
+ return "critical";
556
+ }
557
+ if (status === "degraded" || status === "unknown") {
558
+ return "warning";
559
+ }
560
+ return "info";
561
+ }
562
+ function remainingPercentSeverity(percent) {
563
+ if (percent === null) {
564
+ return "info";
565
+ }
566
+ if (percent <= 10) {
567
+ return "critical";
568
+ }
569
+ if (percent <= 25) {
570
+ return "warning";
571
+ }
572
+ return "info";
573
+ }
574
+ function providerHealthValue(overviewProvider, todayProvider) {
575
+ if (overviewProvider === undefined && todayProvider === undefined) {
576
+ return "Not available";
577
+ }
578
+ const values = [
579
+ overviewProvider === undefined ? null : `health ${overviewProvider.healthStatus}`,
580
+ todayProvider === undefined ? null : `live ${todayProvider.freshness}`,
581
+ ].filter((value) => value !== null);
582
+ return values.join(" / ");
583
+ }
584
+ function summarizeHealth(statuses) {
585
+ if (statuses.includes("down")) {
586
+ return "down";
587
+ }
588
+ if (statuses.includes("degraded")) {
589
+ return "degraded";
590
+ }
591
+ if (statuses.includes("unknown") || statuses.length === 0) {
592
+ return "unknown";
593
+ }
594
+ return "ok";
595
+ }
596
+ function summarizeRisk(severities, healthStatus) {
597
+ if (severities.includes("critical") || healthStatus === "down") {
598
+ return "critical";
599
+ }
600
+ if (severities.includes("warning") || healthStatus === "degraded" || healthStatus === "unknown") {
601
+ return "warning";
602
+ }
603
+ return "low";
604
+ }
605
+ function highestConfidence(values) {
606
+ if (values.includes("high")) {
607
+ return "high";
608
+ }
609
+ if (values.includes("medium")) {
610
+ return "medium";
611
+ }
612
+ if (values.includes("low")) {
613
+ return "low";
614
+ }
615
+ return "none";
616
+ }
617
+ function summarizeCurrency(currencies) {
618
+ const normalized = new Set(currencies
619
+ .map((currency) => safeText(currency.toUpperCase()))
620
+ .filter((currency) => currency.length > 0));
621
+ if (normalized.size === 0) {
622
+ return DEFAULT_CURRENCY;
623
+ }
624
+ if (normalized.size > 1) {
625
+ return "MIXED";
626
+ }
627
+ return [...normalized][0] ?? DEFAULT_CURRENCY;
628
+ }
629
+ function singleCurrency(currencies) {
630
+ const normalized = currencies
631
+ .map((currency) => safeText(currency.toUpperCase()))
632
+ .filter((currency) => currency.length > 0);
633
+ const values = new Set(normalized);
634
+ return values.size === 1 ? normalized[0] ?? null : null;
635
+ }
636
+ function latestIso(values) {
637
+ return values.length === 0
638
+ ? null
639
+ : [...values].sort((first, second) => second.localeCompare(first))[0] ?? null;
640
+ }
641
+ function dateKeyMatchesIso(value, dateKey) {
642
+ return value.startsWith(dateKey);
643
+ }
644
+ function dateKeyInTimezone(date, timezone) {
645
+ const parts = new Intl.DateTimeFormat("en-US", {
646
+ timeZone: timezone,
647
+ year: "numeric",
648
+ month: "2-digit",
649
+ day: "2-digit",
650
+ }).formatToParts(date);
651
+ return [
652
+ parts.find((part) => part.type === "year")?.value ?? "1970",
653
+ parts.find((part) => part.type === "month")?.value ?? "01",
654
+ parts.find((part) => part.type === "day")?.value ?? "01",
655
+ ].join("-");
656
+ }
657
+ function formatMinorAmount(amountMinor, currency) {
658
+ return `${currency} ${(amountMinor / 100).toFixed(2)}`;
659
+ }
660
+ function formatTokens(tokens) {
661
+ return new Intl.NumberFormat("en-US", {
662
+ maximumFractionDigits: 0,
663
+ }).format(tokens);
664
+ }
665
+ function formatPercent(percent) {
666
+ const rounded = Number.isInteger(percent) ? percent.toFixed(0) : percent.toFixed(1);
667
+ return `${rounded}%`;
668
+ }
669
+ function sum(values) {
670
+ return values.reduce((total, value) => total + value, 0);
671
+ }
672
+ function sumNullable(values) {
673
+ return values.reduce((total, value) => total + (value ?? 0), 0);
674
+ }
675
+ function clampPercent(value) {
676
+ if (!Number.isFinite(value)) {
677
+ return 0;
678
+ }
679
+ return Math.max(0, Math.min(100, value));
680
+ }
681
+ function safeText(value) {
682
+ return value.replace(SENSITIVE_TEXT_PATTERN, "[redacted]");
683
+ }
684
+ //# sourceMappingURL=view-model.js.map