@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.
- package/README.md +11 -3
- package/dist/apps/cli/src/cli.d.ts +3 -0
- package/dist/apps/cli/src/cli.js +18 -2
- package/dist/apps/cli/src/commands/install.js +10 -1
- package/dist/apps/cli/src/commands/modes.js +16 -11
- 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/postinstall.js +1 -1
- package/dist/apps/cli/src/release-installer.d.ts +4 -1
- package/dist/apps/cli/src/release-installer.js +47 -8
- 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 +1 -1
- package/dist/apps/cli/src/version.js +1 -1
- 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
|
@@ -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.
|
|
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",
|