@moneysiren/cli 0.1.0-alpha.1 → 0.1.0-alpha.11

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 (36) hide show
  1. package/README.md +11 -3
  2. package/dist/apps/cli/src/cli.d.ts +3 -0
  3. package/dist/apps/cli/src/cli.js +18 -2
  4. package/dist/apps/cli/src/commands/install.js +10 -1
  5. package/dist/apps/cli/src/commands/modes.js +16 -11
  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/postinstall.js +1 -1
  12. package/dist/apps/cli/src/release-installer.d.ts +4 -1
  13. package/dist/apps/cli/src/release-installer.js +47 -8
  14. package/dist/apps/cli/src/runtime-adapter.js +1 -1
  15. package/dist/apps/cli/src/slash.js +27 -0
  16. package/dist/apps/cli/src/version.d.ts +1 -1
  17. package/dist/apps/cli/src/version.js +1 -1
  18. package/dist/packages/config/src/load.js +3 -0
  19. package/dist/packages/config/src/schema.d.ts +3 -0
  20. package/dist/packages/config/src/schema.js +3 -0
  21. package/dist/packages/local-api/src/server.js +1 -1
  22. package/dist/packages/view-model/src/hud-model.d.ts +74 -0
  23. package/dist/packages/view-model/src/hud-model.js +295 -0
  24. package/dist/packages/view-model/src/index.d.ts +5 -2
  25. package/dist/packages/view-model/src/index.js +4 -1
  26. package/dist/packages/view-model/src/notification-preferences-model.d.ts +30 -2
  27. package/dist/packages/view-model/src/notification-preferences-model.js +183 -1
  28. package/dist/packages/view-model/src/notification-preferences.d.ts +1 -1
  29. package/dist/packages/view-model/src/notification-preferences.js +1 -1
  30. package/dist/packages/view-model/src/sync-state.d.ts +47 -0
  31. package/dist/packages/view-model/src/sync-state.js +140 -0
  32. package/dist/packages/view-model/src/usage-progress.d.ts +22 -0
  33. package/dist/packages/view-model/src/usage-progress.js +57 -0
  34. package/dist/packages/view-model/src/view-model.d.ts +22 -0
  35. package/dist/packages/view-model/src/view-model.js +142 -0
  36. package/package.json +3 -2
@@ -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;
@@ -4,8 +4,10 @@ const OPENAI_PROVIDER_KEY = "openai";
4
4
  const AWS_PROVIDER_KEY = "aws";
5
5
  const SUPABASE_PROVIDER_KEY = "supabase";
6
6
  const CLOUDFLARE_PROVIDER_KEY = "cloudflare";
7
+ const CODEX_APP_PROVIDER_KEY = "codex-app";
7
8
  const CODEX_CLI_PROVIDER_KEY = "codex-cli";
8
9
  const CLAUDE_CLI_PROVIDER_KEY = "claude-cli";
10
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
9
11
  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
12
  const EMPTY_STORE = {
11
13
  providers: [],
@@ -115,10 +117,23 @@ export function buildTodayLiveView(store, options) {
115
117
  ...provider,
116
118
  providerKey: safeText(provider.providerKey),
117
119
  displayName: safeText(provider.displayName),
120
+ ...(provider.expiresAt === undefined ? {} : { expiresAt: provider.expiresAt }),
121
+ ...(provider.lastAttemptAt === undefined ? {} : { lastAttemptAt: provider.lastAttemptAt }),
122
+ ...(provider.lastSuccessAt === undefined ? {} : { lastSuccessAt: provider.lastSuccessAt }),
123
+ ...(provider.freshUntil === undefined ? {} : { freshUntil: provider.freshUntil }),
124
+ ...(provider.staleUntil === undefined ? {} : { staleUntil: provider.staleUntil }),
125
+ ...(provider.lastRefreshFailed === undefined ? {} : { lastRefreshFailed: provider.lastRefreshFailed }),
126
+ ...(provider.revision === undefined ? {} : { revision: provider.revision }),
127
+ ...(provider.message === undefined ? {} : { message: safeText(provider.message) }),
118
128
  metrics: (provider.metrics ?? []).map((metric) => ({
119
129
  key: safeText(metric.key),
120
130
  value: metric.value,
121
131
  unit: safeText(metric.unit),
132
+ ...(metric.resetAt === undefined ? {} : { resetAt: safeText(metric.resetAt) }),
133
+ ...(metric.resetAtLatest === undefined ? {} : { resetAtLatest: safeText(metric.resetAtLatest) }),
134
+ ...(metric.itemKey === undefined ? {} : { itemKey: safeText(metric.itemKey) }),
135
+ ...(metric.accuracy === undefined ? {} : { accuracy: metric.accuracy }),
136
+ ...(metric.source === undefined ? {} : { source: safeText(metric.source) }),
122
137
  })),
123
138
  }));
124
139
  const includedProviders = providers.filter((provider) => provider.included && provider.todayLiveAmountMinor !== null);
@@ -387,6 +402,8 @@ function buildDigestItems(overview, todayLive, alerts) {
387
402
  usedTokensMetricKey: "weekly_tokens",
388
403
  clickPath: "/ko/services/codex-cli",
389
404
  }),
405
+ codexResetCreditCountItem(todayLive),
406
+ codexResetCreditExpiryItem(todayLive),
390
407
  {
391
408
  widgetKey: "supabase_usage_health",
392
409
  kind: "health",
@@ -425,6 +442,13 @@ function cliRemainingPercentItem(options) {
425
442
  severity: remainingPercentSeverity(percent),
426
443
  label: options.label,
427
444
  value: percent === null ? "Not available" : formatPercent(percent),
445
+ ...(percent === null ? {} : {
446
+ numericValue: percent,
447
+ unit: "percent",
448
+ remainingPercent: percent,
449
+ usedPercent: clampPercent(100 - percent),
450
+ }),
451
+ providerKey: options.providerKey,
428
452
  ...(firstProvider === undefined
429
453
  ? {}
430
454
  : {
@@ -434,6 +458,68 @@ function cliRemainingPercentItem(options) {
434
458
  clickPath: options.clickPath,
435
459
  };
436
460
  }
461
+ function codexResetCreditCountItem(todayLive) {
462
+ const providers = [
463
+ ...todayProviders(todayLive, CODEX_APP_PROVIDER_KEY),
464
+ ...todayProviders(todayLive, CODEX_CLI_PROVIDER_KEY),
465
+ ];
466
+ const firstProvider = providers[0];
467
+ const metricEntry = firstMetricEntry(providers, "usage_reset_credits");
468
+ const count = metricEntry?.metric.value ?? null;
469
+ const clickProviderKey = metricEntry?.provider.providerKey ?? CODEX_CLI_PROVIDER_KEY;
470
+ return {
471
+ widgetKey: "codex_reset_credit_count",
472
+ kind: "usage",
473
+ severity: "info",
474
+ label: "Codex reset credits",
475
+ value: count === null ? "Not available" : formatCount(count),
476
+ ...(count === null ? {} : {
477
+ numericValue: count,
478
+ unit: "count",
479
+ }),
480
+ ...(metricEntry === undefined ? {} : { providerKey: metricEntry.provider.providerKey }),
481
+ ...(firstProvider === undefined
482
+ ? {}
483
+ : {
484
+ freshness: firstProvider.freshness,
485
+ confidence: firstProvider.confidence,
486
+ }),
487
+ clickPath: `/ko/services/${clickProviderKey}`,
488
+ };
489
+ }
490
+ function codexResetCreditExpiryItem(todayLive) {
491
+ const providers = [
492
+ ...todayProviders(todayLive, CODEX_APP_PROVIDER_KEY),
493
+ ...todayProviders(todayLive, CODEX_CLI_PROVIDER_KEY),
494
+ ];
495
+ const firstProvider = providers[0];
496
+ const exactExpiry = earliestMetricResetAt(providers, "usage_reset_credit");
497
+ const estimatedExpiry = earliestMetricResetAt(providers, "usage_reset_credit_estimate");
498
+ const earliestExpiry = exactExpiry ?? estimatedExpiry;
499
+ const metric = earliestResetCreditMetric(providers, exactExpiry === null ? "usage_reset_credit_estimate" : "usage_reset_credit", earliestExpiry);
500
+ const metricProviderKey = metric === undefined ? undefined : providerForMetric(providers, metric);
501
+ const daysUntil = earliestExpiry === null
502
+ ? null
503
+ : Math.ceil((Date.parse(earliestExpiry) - Date.parse(todayLive.generatedAt)) / MS_PER_DAY);
504
+ return {
505
+ widgetKey: "codex_reset_credit_expiry",
506
+ kind: "usage",
507
+ severity: resetCreditExpirySeverity(daysUntil),
508
+ label: "Codex reset credit expiry",
509
+ value: resetCreditExpiryValue(daysUntil),
510
+ ...(earliestExpiry === null ? {} : { resetAt: earliestExpiry }),
511
+ ...(metric?.resetAtLatest === undefined ? {} : { resetAtLatest: metric.resetAtLatest }),
512
+ ...(metric?.accuracy === undefined ? {} : { accuracy: metric.accuracy }),
513
+ ...(metricProviderKey === undefined ? {} : { providerKey: metricProviderKey }),
514
+ ...(firstProvider === undefined
515
+ ? {}
516
+ : {
517
+ freshness: firstProvider.freshness,
518
+ confidence: firstProvider.confidence,
519
+ }),
520
+ clickPath: "/ko/services/codex-cli",
521
+ };
522
+ }
437
523
  async function resolveStore(options) {
438
524
  if (options.store !== undefined) {
439
525
  return options.store;
@@ -529,6 +615,33 @@ function metricFirst(providers, metricKey) {
529
615
  }
530
616
  return null;
531
617
  }
618
+ function firstMetricEntry(providers, metricKey) {
619
+ for (const provider of providers) {
620
+ const metric = provider.metrics.find((item) => item.key === metricKey);
621
+ if (metric !== undefined) {
622
+ return { provider, metric };
623
+ }
624
+ }
625
+ return undefined;
626
+ }
627
+ function earliestMetricResetAt(providers, metricKey) {
628
+ const values = providers.flatMap((provider) => provider.metrics
629
+ .filter((metric) => metric.key === metricKey && metric.resetAt !== undefined)
630
+ .map((metric) => metric.resetAt)
631
+ .filter((value) => Number.isFinite(Date.parse(value))));
632
+ return values.sort((first, second) => Date.parse(first) - Date.parse(second))[0] ?? null;
633
+ }
634
+ function earliestResetCreditMetric(providers, metricKey, resetAt) {
635
+ if (resetAt === null) {
636
+ return undefined;
637
+ }
638
+ return providers
639
+ .flatMap((provider) => provider.metrics)
640
+ .find((metric) => metric.key === metricKey && metric.resetAt === resetAt);
641
+ }
642
+ function providerForMetric(providers, target) {
643
+ return providers.find((provider) => provider.metrics.some((metric) => metric === target))?.providerKey;
644
+ }
532
645
  function remainingPercentFromMetrics(providers, remainingTokensMetricKey, usedTokensMetricKey, usedPercentMetricKey) {
533
646
  const remainingTokens = metricSum(providers, remainingTokensMetricKey);
534
647
  const usedTokens = metricSum(providers, usedTokensMetricKey);
@@ -541,6 +654,30 @@ function remainingPercentFromMetrics(providers, remainingTokensMetricKey, usedTo
541
654
  const usedPercent = metricFirst(providers, usedPercentMetricKey);
542
655
  return usedPercent === null ? null : clampPercent(100 - usedPercent);
543
656
  }
657
+ function resetCreditExpirySeverity(daysUntil) {
658
+ if (daysUntil === null) {
659
+ return "info";
660
+ }
661
+ if (daysUntil <= 1) {
662
+ return "critical";
663
+ }
664
+ if (daysUntil <= 7) {
665
+ return "warning";
666
+ }
667
+ return "info";
668
+ }
669
+ function resetCreditExpiryValue(daysUntil) {
670
+ if (daysUntil === null) {
671
+ return "Not available";
672
+ }
673
+ if (daysUntil <= 0) {
674
+ return "May expire now";
675
+ }
676
+ if (daysUntil === 1) {
677
+ return "May expire within 1 day";
678
+ }
679
+ return `May expire within ${daysUntil} days`;
680
+ }
544
681
  function providerRiskSeverity(provider) {
545
682
  if (provider?.riskLevel === "critical") {
546
683
  return "critical";
@@ -662,6 +799,11 @@ function formatTokens(tokens) {
662
799
  maximumFractionDigits: 0,
663
800
  }).format(tokens);
664
801
  }
802
+ function formatCount(value) {
803
+ return new Intl.NumberFormat("en-US", {
804
+ maximumFractionDigits: 0,
805
+ }).format(value);
806
+ }
665
807
  function formatPercent(percent) {
666
808
  const rounded = Number.isInteger(percent) ? percent.toFixed(0) : percent.toFixed(1);
667
809
  return `${rounded}%`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneysiren/cli",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.11",
4
4
  "description": "Local-first cloud/SaaS usage, status, and expected billing CLI for MoneySiren.",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -10,7 +10,8 @@
10
10
  "node": ">=20.11.0"
11
11
  },
12
12
  "bin": {
13
- "moneysiren": "dist/apps/cli/src/index.js"
13
+ "moneysiren": "dist/apps/cli/src/index.js",
14
+ "msiren": "dist/apps/cli/src/index.js"
14
15
  },
15
16
  "main": "dist/apps/cli/src/cli.js",
16
17
  "types": "dist/apps/cli/src/cli.d.ts",