@moneysiren/cli 0.1.0-alpha.0 → 0.1.0-alpha.10

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 (37) hide show
  1. package/README.md +15 -4
  2. package/dist/apps/cli/src/cli.d.ts +4 -1
  3. package/dist/apps/cli/src/cli.js +20 -3
  4. package/dist/apps/cli/src/commands/install.js +134 -6
  5. package/dist/apps/cli/src/commands/modes.js +18 -10
  6. package/dist/apps/cli/src/commands/runtime.d.ts +5 -0
  7. package/dist/apps/cli/src/commands/runtime.js +366 -0
  8. package/dist/apps/cli/src/desktop-runtime.d.ts +54 -0
  9. package/dist/apps/cli/src/desktop-runtime.js +720 -0
  10. package/dist/apps/cli/src/home.js +27 -0
  11. package/dist/apps/cli/src/index.js +0 -0
  12. package/dist/apps/cli/src/postinstall.js +1 -1
  13. package/dist/apps/cli/src/release-installer.d.ts +57 -0
  14. package/dist/apps/cli/src/release-installer.js +432 -0
  15. package/dist/apps/cli/src/runtime-adapter.js +1 -1
  16. package/dist/apps/cli/src/slash.js +27 -0
  17. package/dist/apps/cli/src/version.d.ts +2 -0
  18. package/dist/apps/cli/src/version.js +2 -0
  19. package/dist/packages/config/src/load.js +3 -0
  20. package/dist/packages/config/src/schema.d.ts +3 -0
  21. package/dist/packages/config/src/schema.js +3 -0
  22. package/dist/packages/local-api/src/server.js +1 -1
  23. package/dist/packages/view-model/src/hud-model.d.ts +74 -0
  24. package/dist/packages/view-model/src/hud-model.js +295 -0
  25. package/dist/packages/view-model/src/index.d.ts +5 -2
  26. package/dist/packages/view-model/src/index.js +4 -1
  27. package/dist/packages/view-model/src/notification-preferences-model.d.ts +30 -2
  28. package/dist/packages/view-model/src/notification-preferences-model.js +183 -1
  29. package/dist/packages/view-model/src/notification-preferences.d.ts +1 -1
  30. package/dist/packages/view-model/src/notification-preferences.js +1 -1
  31. package/dist/packages/view-model/src/sync-state.d.ts +47 -0
  32. package/dist/packages/view-model/src/sync-state.js +140 -0
  33. package/dist/packages/view-model/src/usage-progress.d.ts +22 -0
  34. package/dist/packages/view-model/src/usage-progress.js +57 -0
  35. package/dist/packages/view-model/src/view-model.d.ts +22 -0
  36. package/dist/packages/view-model/src/view-model.js +142 -0
  37. package/package.json +3 -2
@@ -10,6 +10,8 @@ export const NOTIFICATION_WIDGET_KEYS = [
10
10
  "claude_weekly_percent",
11
11
  "codex_five_hour_percent",
12
12
  "codex_weekly_percent",
13
+ "codex_reset_credit_count",
14
+ "codex_reset_credit_expiry",
13
15
  "supabase_usage_health",
14
16
  "cloudflare_month_to_date",
15
17
  ];
@@ -21,6 +23,8 @@ export const LOCAL_CLI_DASHBOARD_METRIC_KEYS = [
21
23
  "weekly_limit_percent",
22
24
  "five_hour_remaining_tokens",
23
25
  "weekly_remaining_tokens",
26
+ "usage_reset_credits",
27
+ "usage_reset_credit_estimate",
24
28
  "context_tokens",
25
29
  "input_tokens",
26
30
  "output_tokens",
@@ -31,6 +35,31 @@ export const LOCAL_CLI_DASHBOARD_METRIC_KEYS = [
31
35
  "tool_calls",
32
36
  "log_files",
33
37
  ];
38
+ export const DASHBOARD_VIEW_KEYS = ["overview", "today", "forecast", "risks"];
39
+ export const DASHBOARD_WIDGET_SIZES = ["compact", "normal", "wide", "full"];
40
+ export const DASHBOARD_WIDGET_KEYS_BY_VIEW = {
41
+ overview: [
42
+ "overview_meta",
43
+ "overview_metrics",
44
+ "overview_trend",
45
+ "overview_grouping",
46
+ "overview_services",
47
+ "overview_insights",
48
+ ],
49
+ today: [
50
+ "today_main",
51
+ "today_rail",
52
+ ],
53
+ forecast: [
54
+ "forecast_metrics",
55
+ "forecast_table",
56
+ "forecast_breakdown",
57
+ ],
58
+ risks: [
59
+ "risks_summary",
60
+ "risks_table",
61
+ ],
62
+ };
34
63
  export const DEFAULT_SELECTED_NOTIFICATION_WIDGET_KEYS = [
35
64
  "month_forecast",
36
65
  "today_live_cost",
@@ -38,12 +67,40 @@ export const DEFAULT_SELECTED_NOTIFICATION_WIDGET_KEYS = [
38
67
  "stale_connection_count",
39
68
  "openai_today_tokens",
40
69
  "codex_five_hour_percent",
70
+ "codex_reset_credit_count",
71
+ "codex_reset_credit_expiry",
72
+ ];
73
+ const LEGACY_SELECTED_NOTIFICATION_WIDGET_KEY_SETS = [
74
+ [
75
+ "month_forecast",
76
+ "today_live_cost",
77
+ "risk_high_count",
78
+ "stale_connection_count",
79
+ "openai_today_tokens",
80
+ "codex_five_hour_percent",
81
+ ],
82
+ [
83
+ "month_forecast",
84
+ "today_live_cost",
85
+ "risk_high_count",
86
+ "stale_connection_count",
87
+ "openai_today_tokens",
88
+ "codex_five_hour_percent",
89
+ "codex_reset_credit_expiry",
90
+ ],
41
91
  ];
42
92
  export const DEFAULT_LOCAL_CLI_DASHBOARD_METRIC_KEYS = [
43
93
  "context_percent",
44
94
  "last_request_tokens",
45
95
  "total_tokens",
96
+ "usage_reset_credits",
46
97
  ];
98
+ export const DEFAULT_DASHBOARD_WIDGET_LAYOUTS = {
99
+ overview: defaultDashboardWidgetLayout("overview"),
100
+ today: defaultDashboardWidgetLayout("today"),
101
+ forecast: defaultDashboardWidgetLayout("forecast"),
102
+ risks: defaultDashboardWidgetLayout("risks"),
103
+ };
47
104
  export const DEFAULT_NOTIFICATION_THRESHOLD_RULES = [
48
105
  {
49
106
  widgetKey: "risk_high_count",
@@ -77,6 +134,13 @@ export const DEFAULT_NOTIFICATION_PREFERENCES = {
77
134
  desktopEnabled: false,
78
135
  dashboard: {
79
136
  localCliMetricKeys: DEFAULT_LOCAL_CLI_DASHBOARD_METRIC_KEYS,
137
+ widgetLayouts: DEFAULT_DASHBOARD_WIDGET_LAYOUTS,
138
+ budget: {
139
+ monthlyBudgetMinor: null,
140
+ currency: "USD",
141
+ warningPercent: 80,
142
+ criticalPercent: 100,
143
+ },
80
144
  },
81
145
  hud: {
82
146
  alwaysOnTop: true,
@@ -125,6 +189,84 @@ function parseDashboardDisplayPreferences(value) {
125
189
  const record = isRecord(value) ? value : {};
126
190
  return {
127
191
  localCliMetricKeys: parseLocalCliDashboardMetricKeys(record.localCliMetricKeys),
192
+ budget: parseDashboardBudgetPreferences(record.budget),
193
+ widgetLayouts: parseDashboardWidgetLayouts(record.widgetLayouts),
194
+ };
195
+ }
196
+ function parseDashboardWidgetLayouts(value) {
197
+ const record = isRecord(value) ? value : {};
198
+ return {
199
+ overview: parseDashboardWidgetLayout("overview", record.overview),
200
+ today: parseDashboardWidgetLayout("today", record.today),
201
+ forecast: parseDashboardWidgetLayout("forecast", record.forecast),
202
+ risks: parseDashboardWidgetLayout("risks", record.risks),
203
+ };
204
+ }
205
+ function parseDashboardWidgetLayout(viewKey, value) {
206
+ const validWidgetKeys = new Set(DASHBOARD_WIDGET_KEYS_BY_VIEW[viewKey]);
207
+ const defaults = defaultDashboardWidgetLayout(viewKey);
208
+ const defaultByKey = new Map(defaults.map((item) => [item.widgetKey, item]));
209
+ const parsed = Array.isArray(value)
210
+ ? value
211
+ .map((item) => {
212
+ if (!isRecord(item) || typeof item.widgetKey !== "string" || !validWidgetKeys.has(item.widgetKey)) {
213
+ return null;
214
+ }
215
+ const widgetKey = item.widgetKey;
216
+ const fallback = defaultByKey.get(widgetKey);
217
+ return {
218
+ widgetKey,
219
+ visible: typeof item.visible === "boolean" ? item.visible : fallback?.visible ?? true,
220
+ size: parseDashboardWidgetSize(item.size, fallback?.size ?? "normal"),
221
+ };
222
+ })
223
+ .filter((item) => item !== null)
224
+ : [];
225
+ const seen = new Set();
226
+ const normalized = parsed.filter((item) => {
227
+ if (seen.has(item.widgetKey)) {
228
+ return false;
229
+ }
230
+ seen.add(item.widgetKey);
231
+ return true;
232
+ });
233
+ const missing = defaults.filter((item) => !seen.has(item.widgetKey));
234
+ return [...normalized, ...missing];
235
+ }
236
+ function defaultDashboardWidgetLayout(viewKey) {
237
+ return DASHBOARD_WIDGET_KEYS_BY_VIEW[viewKey].map((widgetKey) => ({
238
+ widgetKey,
239
+ visible: true,
240
+ size: defaultDashboardWidgetSize(widgetKey),
241
+ }));
242
+ }
243
+ function defaultDashboardWidgetSize(widgetKey) {
244
+ if (widgetKey === "overview_metrics" || widgetKey === "overview_services" || widgetKey === "risks_table") {
245
+ return "full";
246
+ }
247
+ if (widgetKey === "today_main" || widgetKey === "forecast_table") {
248
+ return "wide";
249
+ }
250
+ if (widgetKey === "forecast_breakdown" || widgetKey === "today_rail") {
251
+ return "compact";
252
+ }
253
+ return "normal";
254
+ }
255
+ function parseDashboardWidgetSize(value, fallback) {
256
+ return typeof value === "string" && DASHBOARD_WIDGET_SIZES.includes(value)
257
+ ? value
258
+ : fallback;
259
+ }
260
+ function parseDashboardBudgetPreferences(value) {
261
+ const record = isRecord(value) ? value : {};
262
+ const monthlyBudgetMinor = parseOptionalPositiveInteger(record.monthlyBudgetMinor);
263
+ const warningPercent = clampNumber(record.warningPercent, 1, 999, DEFAULT_NOTIFICATION_PREFERENCES.dashboard.budget.warningPercent);
264
+ const criticalPercent = clampNumber(record.criticalPercent, warningPercent, 999, DEFAULT_NOTIFICATION_PREFERENCES.dashboard.budget.criticalPercent);
265
+ return {
266
+ monthlyBudgetMinor,
267
+ currency: parseCurrency(record.currency, DEFAULT_NOTIFICATION_PREFERENCES.dashboard.budget.currency),
268
+ warningPercent,
269
+ criticalPercent,
128
270
  };
129
271
  }
130
272
  function parseHudPreferences(value, fallbackSelectedWidgets = DEFAULT_NOTIFICATION_PREFERENCES.hud.selectedWidgets) {
@@ -162,7 +304,31 @@ function parseSelectedWidgets(value, fallbackSelectedWidgets = DEFAULT_SELECTED_
162
304
  const selected = Array.isArray(value)
163
305
  ? value.filter((item) => typeof item === "string" && widgetKeys.has(item))
164
306
  : [...fallbackSelectedWidgets];
165
- return selected.length === 0 ? [...fallbackSelectedWidgets] : [...new Set(selected)];
307
+ if (selected.length === 0) {
308
+ return [...fallbackSelectedWidgets];
309
+ }
310
+ const uniqueSelected = [...new Set(selected)];
311
+ return isLegacySelectedWidgetDefault(uniqueSelected)
312
+ ? [...DEFAULT_SELECTED_NOTIFICATION_WIDGET_KEYS]
313
+ : migrateSelectedWidgets(uniqueSelected);
314
+ }
315
+ function migrateSelectedWidgets(selectedWidgets) {
316
+ if (!selectedWidgets.includes("codex_reset_credit_expiry") ||
317
+ selectedWidgets.includes("codex_reset_credit_count")) {
318
+ return [...selectedWidgets];
319
+ }
320
+ const migrated = [];
321
+ for (const widgetKey of selectedWidgets) {
322
+ if (widgetKey === "codex_reset_credit_expiry") {
323
+ migrated.push("codex_reset_credit_count");
324
+ }
325
+ migrated.push(widgetKey);
326
+ }
327
+ return migrated;
328
+ }
329
+ function isLegacySelectedWidgetDefault(selectedWidgets) {
330
+ return LEGACY_SELECTED_NOTIFICATION_WIDGET_KEY_SETS.some((legacySet) => legacySet.length === selectedWidgets.length &&
331
+ legacySet.every((widgetKey, index) => selectedWidgets[index] === widgetKey));
166
332
  }
167
333
  function parseThresholdRules(value) {
168
334
  if (!Array.isArray(value)) {
@@ -203,6 +369,22 @@ function parseNonNegativeNumber(value) {
203
369
  }
204
370
  return Math.max(0, value);
205
371
  }
372
+ function parseOptionalPositiveInteger(value) {
373
+ if (value === null || value === undefined || value === "") {
374
+ return null;
375
+ }
376
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
377
+ return null;
378
+ }
379
+ return Math.round(value);
380
+ }
381
+ function parseCurrency(value, fallback) {
382
+ if (typeof value !== "string") {
383
+ return fallback;
384
+ }
385
+ const normalized = value.trim().toUpperCase();
386
+ return /^[A-Z]{3}$/.test(normalized) ? normalized : fallback;
387
+ }
206
388
  function clampNumber(value, min, max, fallback) {
207
389
  if (typeof value !== "number" || !Number.isFinite(value)) {
208
390
  return fallback;
@@ -1,5 +1,5 @@
1
1
  import { type NotificationPreferenceFileOptions, type NotificationPreferences } from "./notification-preferences-model.js";
2
- export { cloneNotificationPreferences, DEFAULT_NOTIFICATION_PREFERENCES, DEFAULT_NOTIFICATION_THRESHOLD_RULES, DEFAULT_LOCAL_CLI_DASHBOARD_METRIC_KEYS, DEFAULT_SELECTED_NOTIFICATION_WIDGET_KEYS, LOCAL_CLI_DASHBOARD_METRIC_KEYS, NOTIFICATION_WIDGET_KEYS, parseNotificationPreferences, type DigestInterval, type DashboardDisplayPreferences, type LocalCliDashboardMetricKey, type NotificationPreferenceFileOptions, type NotificationPreferences, type NotificationThresholdRule, type NotificationWidgetKey, type ThresholdOperator, } from "./notification-preferences-model.js";
2
+ export { cloneNotificationPreferences, DEFAULT_NOTIFICATION_PREFERENCES, DEFAULT_NOTIFICATION_THRESHOLD_RULES, DEFAULT_LOCAL_CLI_DASHBOARD_METRIC_KEYS, DEFAULT_DASHBOARD_WIDGET_LAYOUTS, DEFAULT_SELECTED_NOTIFICATION_WIDGET_KEYS, DASHBOARD_VIEW_KEYS, DASHBOARD_WIDGET_KEYS_BY_VIEW, DASHBOARD_WIDGET_SIZES, LOCAL_CLI_DASHBOARD_METRIC_KEYS, NOTIFICATION_WIDGET_KEYS, parseNotificationPreferences, type DashboardBudgetPreferences, type DashboardDisplayPreferences, type DashboardViewKey, type DashboardWidgetKey, type DashboardWidgetLayoutItem, type DashboardWidgetLayoutPreferences, type DashboardWidgetSize, type DigestInterval, type LocalCliDashboardMetricKey, type NotificationPreferenceFileOptions, type NotificationPreferences, type NotificationThresholdRule, type NotificationWidgetKey, type ThresholdOperator, } from "./notification-preferences-model.js";
3
3
  export declare function readNotificationPreferencesFile(options?: NotificationPreferenceFileOptions): Promise<NotificationPreferences>;
4
4
  export declare function writeNotificationPreferencesFile(preferences: NotificationPreferences, options?: NotificationPreferenceFileOptions): Promise<NotificationPreferences>;
5
5
  export declare function resolveNotificationPreferencesPath(options?: NotificationPreferenceFileOptions): string;
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { dirname, isAbsolute, join } from "node:path";
3
3
  import { cloneNotificationPreferences, DEFAULT_NOTIFICATION_PREFERENCES, parseNotificationPreferences, } from "./notification-preferences-model.js";
4
- export { cloneNotificationPreferences, DEFAULT_NOTIFICATION_PREFERENCES, DEFAULT_NOTIFICATION_THRESHOLD_RULES, DEFAULT_LOCAL_CLI_DASHBOARD_METRIC_KEYS, DEFAULT_SELECTED_NOTIFICATION_WIDGET_KEYS, LOCAL_CLI_DASHBOARD_METRIC_KEYS, NOTIFICATION_WIDGET_KEYS, parseNotificationPreferences, } from "./notification-preferences-model.js";
4
+ export { cloneNotificationPreferences, DEFAULT_NOTIFICATION_PREFERENCES, DEFAULT_NOTIFICATION_THRESHOLD_RULES, DEFAULT_LOCAL_CLI_DASHBOARD_METRIC_KEYS, DEFAULT_DASHBOARD_WIDGET_LAYOUTS, DEFAULT_SELECTED_NOTIFICATION_WIDGET_KEYS, DASHBOARD_VIEW_KEYS, DASHBOARD_WIDGET_KEYS_BY_VIEW, DASHBOARD_WIDGET_SIZES, LOCAL_CLI_DASHBOARD_METRIC_KEYS, NOTIFICATION_WIDGET_KEYS, parseNotificationPreferences, } from "./notification-preferences-model.js";
5
5
  const PREFERENCES_PATH_ENV = "MONEYSIREN_NOTIFICATION_PREFS_PATH";
6
6
  const DEFAULT_PREFERENCES_PATH = ".moneysiren/notification-preferences.json";
7
7
  export async function readNotificationPreferencesFile(options = {}) {
@@ -0,0 +1,47 @@
1
+ export type AggregateSyncStatus = "ok" | "partial" | "stale" | "error" | "empty";
2
+ export type RiskSeverity = "info" | "warning" | "critical";
3
+ export type SyncStateValue = "fresh" | "stale" | "error" | "not_configured" | "unavailable" | "locked";
4
+ export type SyncErrorCode = "not_configured" | "auth_expired" | "permission_denied" | "rate_limited" | "timeout" | "network" | "upstream_unavailable" | "schema_changed" | "invalid_data" | "local_source_missing" | "local_io" | "internal";
5
+ export interface SyncErrorView {
6
+ code: SyncErrorCode;
7
+ retryable: boolean;
8
+ userActionRequired: boolean;
9
+ message: string;
10
+ }
11
+ export interface ItemSyncView {
12
+ state: SyncStateValue;
13
+ observedAt: string | null;
14
+ lastAttemptAt: string | null;
15
+ lastSuccessAt: string | null;
16
+ freshUntil: string | null;
17
+ staleUntil: string | null;
18
+ ageSeconds: number | null;
19
+ source: string;
20
+ error: SyncErrorView | null;
21
+ lastRefreshFailed: boolean;
22
+ }
23
+ export declare function createSyncError(code: SyncErrorCode, message?: string): SyncErrorView;
24
+ export declare function syncViewFromFreshness(options: {
25
+ freshness: "live" | "stale" | "error" | "unavailable" | "not_configured" | "locked";
26
+ checkedAt: string | null;
27
+ generatedAt: string;
28
+ ttlSeconds?: number | null;
29
+ source: string;
30
+ message?: string | null;
31
+ lastAttemptAt?: string | null;
32
+ lastSuccessAt?: string | null;
33
+ freshUntil?: string | null;
34
+ staleUntil?: string | null;
35
+ lastRefreshFailed?: boolean;
36
+ }): ItemSyncView;
37
+ export declare function summarizeAggregateSync(items: readonly ItemSyncView[]): {
38
+ status: AggregateSyncStatus;
39
+ freshCount: number;
40
+ staleCount: number;
41
+ errorCount: number;
42
+ neutralCount: number;
43
+ lastSuccessAt: string | null;
44
+ };
45
+ export declare function syncStateFromFreshness(freshness: "live" | "stale" | "error" | "unavailable" | "not_configured" | "locked"): SyncStateValue;
46
+ export declare function isNeutralSyncState(state: SyncStateValue): boolean;
47
+ //# sourceMappingURL=sync-state.d.ts.map
@@ -0,0 +1,140 @@
1
+ export function createSyncError(code, message) {
2
+ return {
3
+ code,
4
+ retryable: isRetryableSyncError(code),
5
+ userActionRequired: isUserActionRequiredSyncError(code),
6
+ message: sanitizeSyncMessage(message ?? defaultSyncErrorMessage(code)),
7
+ };
8
+ }
9
+ export function syncViewFromFreshness(options) {
10
+ const state = syncStateFromFreshness(options.freshness);
11
+ const observedAt = options.checkedAt;
12
+ const ageSeconds = observedAt === null
13
+ ? null
14
+ : Math.max(0, Math.floor((Date.parse(options.generatedAt) - Date.parse(observedAt)) / 1000));
15
+ const defaultFreshUntil = observedAt === null || options.ttlSeconds === null || options.ttlSeconds === undefined
16
+ ? null
17
+ : new Date(Date.parse(observedAt) + options.ttlSeconds * 1000).toISOString();
18
+ return {
19
+ state,
20
+ observedAt,
21
+ lastAttemptAt: options.lastAttemptAt ?? observedAt,
22
+ lastSuccessAt: options.lastSuccessAt ?? (state === "error" ? null : observedAt),
23
+ freshUntil: options.freshUntil ?? defaultFreshUntil,
24
+ staleUntil: options.staleUntil ?? null,
25
+ ageSeconds: Number.isFinite(ageSeconds) ? ageSeconds : null,
26
+ source: options.source,
27
+ error: state === "error" || state === "locked" || state === "not_configured"
28
+ ? createSyncError(errorCodeFromState(state, options.message), options.message ?? undefined)
29
+ : null,
30
+ lastRefreshFailed: options.lastRefreshFailed ?? state === "error",
31
+ };
32
+ }
33
+ export function summarizeAggregateSync(items) {
34
+ const actionable = items.filter((item) => !isNeutralSyncState(item.state));
35
+ const freshCount = actionable.filter((item) => item.state === "fresh").length;
36
+ const staleCount = actionable.filter((item) => item.state === "stale").length;
37
+ const errorCount = actionable.filter((item) => item.state === "error" || item.state === "locked").length;
38
+ const neutralCount = items.length - actionable.length;
39
+ const lastSuccessAt = latestIso(items.map((item) => item.lastSuccessAt).filter((value) => value !== null));
40
+ const status = actionable.length === 0
41
+ ? "empty"
42
+ : errorCount > 0 && (freshCount > 0 || staleCount > 0)
43
+ ? "partial"
44
+ : errorCount === actionable.length
45
+ ? "error"
46
+ : freshCount > 0 && staleCount > 0
47
+ ? "partial"
48
+ : staleCount === actionable.length
49
+ ? "stale"
50
+ : "ok";
51
+ return {
52
+ status,
53
+ freshCount,
54
+ staleCount,
55
+ errorCount,
56
+ neutralCount,
57
+ lastSuccessAt,
58
+ };
59
+ }
60
+ export function syncStateFromFreshness(freshness) {
61
+ if (freshness === "live") {
62
+ return "fresh";
63
+ }
64
+ if (freshness === "not_configured" || freshness === "unavailable" || freshness === "locked") {
65
+ return freshness;
66
+ }
67
+ return freshness;
68
+ }
69
+ export function isNeutralSyncState(state) {
70
+ return state === "not_configured" || state === "unavailable";
71
+ }
72
+ function errorCodeFromState(state, message) {
73
+ if (state === "not_configured") {
74
+ return "not_configured";
75
+ }
76
+ if (state === "locked") {
77
+ return "local_io";
78
+ }
79
+ if (message !== undefined && message !== null) {
80
+ const normalized = message.toLowerCase();
81
+ if (normalized.includes("permission") || normalized.includes("403")) {
82
+ return "permission_denied";
83
+ }
84
+ if (normalized.includes("401") || normalized.includes("expired")) {
85
+ return "auth_expired";
86
+ }
87
+ if (normalized.includes("429") || normalized.includes("rate limit")) {
88
+ return "rate_limited";
89
+ }
90
+ if (normalized.includes("timeout")) {
91
+ return "timeout";
92
+ }
93
+ if (normalized.includes("network") || normalized.includes("enotfound") || normalized.includes("econn")) {
94
+ return "network";
95
+ }
96
+ }
97
+ return "internal";
98
+ }
99
+ function isRetryableSyncError(code) {
100
+ return code === "rate_limited" || code === "timeout" || code === "network" || code === "upstream_unavailable";
101
+ }
102
+ function isUserActionRequiredSyncError(code) {
103
+ return code === "not_configured" ||
104
+ code === "auth_expired" ||
105
+ code === "permission_denied" ||
106
+ code === "local_source_missing" ||
107
+ code === "local_io";
108
+ }
109
+ function defaultSyncErrorMessage(code) {
110
+ if (code === "auth_expired") {
111
+ return "Login may have expired. Sign in again and retry.";
112
+ }
113
+ if (code === "permission_denied") {
114
+ return "Permission is not sufficient for this read-only check.";
115
+ }
116
+ if (code === "not_configured" || code === "local_source_missing") {
117
+ return "Local source or credential is not configured.";
118
+ }
119
+ if (code === "rate_limited") {
120
+ return "Provider request limit was reached. Retry later.";
121
+ }
122
+ if (code === "schema_changed") {
123
+ return "Provider response shape changed.";
124
+ }
125
+ return "Live sync failed.";
126
+ }
127
+ function sanitizeSyncMessage(value) {
128
+ return value
129
+ .replace(/https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9/_-]+/g, "[redacted]")
130
+ .replace(/\b(?:sk|sbp|xox[baprs])[-_][A-Za-z0-9_-]+\b/gi, "[redacted]")
131
+ .replace(/\bacct[_-][A-Za-z0-9_-]+\b/gi, "[redacted]")
132
+ .replace(/\b(?:proj|project|invoice)[_-][A-Za-z0-9_-]+\b/gi, "[redacted]")
133
+ .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[redacted]");
134
+ }
135
+ function latestIso(values) {
136
+ return values.length === 0
137
+ ? null
138
+ : [...values].sort((first, second) => second.localeCompare(first))[0] ?? null;
139
+ }
140
+ //# sourceMappingURL=sync-state.js.map
@@ -0,0 +1,22 @@
1
+ export interface UsageProgressView {
2
+ usedPercent: number | null;
3
+ remainingPercent: number | null;
4
+ warningAtPercent: number;
5
+ criticalAtPercent: number;
6
+ }
7
+ export type UsageProgressSeverity = "info" | "warning" | "critical";
8
+ export declare function usageProgressFromUsedPercent(value: number | null | undefined, thresholds?: {
9
+ warningAtPercent?: number;
10
+ criticalAtPercent?: number;
11
+ }): UsageProgressView;
12
+ export declare function usageProgressFromRemainingPercent(value: number | null | undefined, thresholds?: {
13
+ warningAtPercent?: number;
14
+ criticalAtPercent?: number;
15
+ }): UsageProgressView;
16
+ export declare function usageProgressFromTokens(usedTokens: number | null | undefined, remainingTokens: number | null | undefined, thresholds?: {
17
+ warningAtPercent?: number;
18
+ criticalAtPercent?: number;
19
+ }): UsageProgressView;
20
+ export declare function usageProgressSeverity(progress: UsageProgressView): UsageProgressSeverity;
21
+ export declare function normalizePercent(value: number | null | undefined): number | null;
22
+ //# sourceMappingURL=usage-progress.d.ts.map
@@ -0,0 +1,57 @@
1
+ const DEFAULT_WARNING_AT_PERCENT = 80;
2
+ const DEFAULT_CRITICAL_AT_PERCENT = 95;
3
+ export function usageProgressFromUsedPercent(value, thresholds = {}) {
4
+ const usedPercent = normalizePercent(value);
5
+ const warningAtPercent = thresholds.warningAtPercent ?? DEFAULT_WARNING_AT_PERCENT;
6
+ const criticalAtPercent = thresholds.criticalAtPercent ?? DEFAULT_CRITICAL_AT_PERCENT;
7
+ return {
8
+ usedPercent,
9
+ remainingPercent: usedPercent === null ? null : clampPercent(100 - usedPercent),
10
+ warningAtPercent,
11
+ criticalAtPercent,
12
+ };
13
+ }
14
+ export function usageProgressFromRemainingPercent(value, thresholds = {}) {
15
+ const remainingPercent = normalizePercent(value);
16
+ const warningAtPercent = thresholds.warningAtPercent ?? DEFAULT_WARNING_AT_PERCENT;
17
+ const criticalAtPercent = thresholds.criticalAtPercent ?? DEFAULT_CRITICAL_AT_PERCENT;
18
+ return {
19
+ usedPercent: remainingPercent === null ? null : clampPercent(100 - remainingPercent),
20
+ remainingPercent,
21
+ warningAtPercent,
22
+ criticalAtPercent,
23
+ };
24
+ }
25
+ export function usageProgressFromTokens(usedTokens, remainingTokens, thresholds = {}) {
26
+ const used = normalizeNonNegativeNumber(usedTokens);
27
+ const remaining = normalizeNonNegativeNumber(remainingTokens);
28
+ if (used === null || remaining === null) {
29
+ return usageProgressFromUsedPercent(null, thresholds);
30
+ }
31
+ const total = used + remaining;
32
+ return usageProgressFromUsedPercent(total <= 0 ? null : (used / total) * 100, thresholds);
33
+ }
34
+ export function usageProgressSeverity(progress) {
35
+ if (progress.usedPercent === null) {
36
+ return "info";
37
+ }
38
+ if (progress.usedPercent >= progress.criticalAtPercent) {
39
+ return "critical";
40
+ }
41
+ if (progress.usedPercent >= progress.warningAtPercent) {
42
+ return "warning";
43
+ }
44
+ return "info";
45
+ }
46
+ export function normalizePercent(value) {
47
+ return value === null || value === undefined || !Number.isFinite(value)
48
+ ? null
49
+ : clampPercent(value);
50
+ }
51
+ function normalizeNonNegativeNumber(value) {
52
+ return value === null || value === undefined || !Number.isFinite(value) || value < 0 ? null : value;
53
+ }
54
+ function clampPercent(value) {
55
+ return Math.max(0, Math.min(100, Number(value.toFixed(2))));
56
+ }
57
+ //# sourceMappingURL=usage-progress.js.map
@@ -112,6 +112,15 @@ export interface TodayLiveProviderInput {
112
112
  providerKey: string;
113
113
  displayName: string;
114
114
  checkedAt: string | null;
115
+ expiresAt?: string | null;
116
+ ttlSeconds?: number;
117
+ lastAttemptAt?: string | null;
118
+ lastSuccessAt?: string | null;
119
+ freshUntil?: string | null;
120
+ staleUntil?: string | null;
121
+ lastRefreshFailed?: boolean;
122
+ revision?: number;
123
+ message?: string;
115
124
  freshness: "live" | "stale" | "error" | "unavailable" | "not_configured" | "locked";
116
125
  confidence: "high" | "medium" | "low" | "none";
117
126
  todayLiveAmountMinor: number | null;
@@ -126,6 +135,11 @@ export interface TodayLiveMetric {
126
135
  key: string;
127
136
  value: number;
128
137
  unit: string;
138
+ resetAt?: string;
139
+ resetAtLatest?: string;
140
+ itemKey?: string;
141
+ accuracy?: "exact" | "estimated" | "bounded" | "unknown";
142
+ source?: string;
129
143
  }
130
144
  export interface NotificationDigest extends LocalSafeEnvelope {
131
145
  title: string;
@@ -139,6 +153,14 @@ export interface NotificationDigestItem {
139
153
  severity: ViewModelRiskSeverity;
140
154
  label: string;
141
155
  value: string;
156
+ numericValue?: number;
157
+ unit?: string;
158
+ usedPercent?: number;
159
+ remainingPercent?: number;
160
+ resetAt?: string;
161
+ resetAtLatest?: string;
162
+ providerKey?: string;
163
+ accuracy?: "exact" | "estimated" | "bounded" | "unknown";
142
164
  freshness?: TodayLiveProviderView["freshness"];
143
165
  confidence?: TodayLiveProviderView["confidence"];
144
166
  clickPath?: string;