@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.
- package/README.md +15 -4
- package/dist/apps/cli/src/cli.d.ts +4 -1
- package/dist/apps/cli/src/cli.js +20 -3
- package/dist/apps/cli/src/commands/install.js +134 -6
- package/dist/apps/cli/src/commands/modes.js +18 -10
- package/dist/apps/cli/src/commands/runtime.d.ts +5 -0
- package/dist/apps/cli/src/commands/runtime.js +366 -0
- package/dist/apps/cli/src/desktop-runtime.d.ts +54 -0
- package/dist/apps/cli/src/desktop-runtime.js +720 -0
- package/dist/apps/cli/src/home.js +27 -0
- package/dist/apps/cli/src/index.js +0 -0
- package/dist/apps/cli/src/postinstall.js +1 -1
- package/dist/apps/cli/src/release-installer.d.ts +57 -0
- package/dist/apps/cli/src/release-installer.js +432 -0
- package/dist/apps/cli/src/runtime-adapter.js +1 -1
- package/dist/apps/cli/src/slash.js +27 -0
- package/dist/apps/cli/src/version.d.ts +2 -0
- package/dist/apps/cli/src/version.js +2 -0
- package/dist/packages/config/src/load.js +3 -0
- package/dist/packages/config/src/schema.d.ts +3 -0
- package/dist/packages/config/src/schema.js +3 -0
- package/dist/packages/local-api/src/server.js +1 -1
- package/dist/packages/view-model/src/hud-model.d.ts +74 -0
- package/dist/packages/view-model/src/hud-model.js +295 -0
- package/dist/packages/view-model/src/index.d.ts +5 -2
- package/dist/packages/view-model/src/index.js +4 -1
- package/dist/packages/view-model/src/notification-preferences-model.d.ts +30 -2
- package/dist/packages/view-model/src/notification-preferences-model.js +183 -1
- package/dist/packages/view-model/src/notification-preferences.d.ts +1 -1
- package/dist/packages/view-model/src/notification-preferences.js +1 -1
- package/dist/packages/view-model/src/sync-state.d.ts +47 -0
- package/dist/packages/view-model/src/sync-state.js +140 -0
- package/dist/packages/view-model/src/usage-progress.d.ts +22 -0
- package/dist/packages/view-model/src/usage-progress.js +57 -0
- package/dist/packages/view-model/src/view-model.d.ts +22 -0
- package/dist/packages/view-model/src/view-model.js +142 -0
- 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
|
-
|
|
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
|
|
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;
|