@subvalue/cli 0.1.3 → 0.1.5
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/PRE_RELEASE.md +7 -7
- package/README.md +7 -5
- package/dist/PRICING.md +15 -16
- package/dist/packages/cli/src/index.js +109 -20
- package/dist/packages/cli/src/protocol.js +13 -0
- package/dist/packages/cli/src/runtime.js +44 -14
- package/dist/packages/cli/src/server.js +306 -62
- package/dist/packages/core/src/calendar.js +41 -18
- package/dist/packages/core/src/coverage.js +12 -0
- package/dist/packages/core/src/demo.js +61 -9
- package/dist/packages/core/src/metadata.js +52 -9
- package/dist/packages/core/src/pricing/current.js +346 -297
- package/dist/packages/core/src/pricing/index.js +373 -73
- package/dist/packages/core/src/providers/claude/index.js +93 -21
- package/dist/packages/core/src/providers/codex/index.js +213 -46
- package/dist/packages/core/src/scanner.js +226 -46
- package/dist/packages/core/src/security.js +127 -34
- package/dist/packages/core/src/storage.js +215 -47
- package/dist/packages/core/src/subscriptions.js +146 -21
- package/dist/packages/core/src/summary.js +410 -59
- package/dist/packages/core/src/types.js +73 -18
- package/dist/packages/core/src/usage-json.js +166 -47
- package/dist/packages/receipt/src/index.js +392 -82
- package/dist/packages/ui/src/brand.js +43 -27
- package/dist/public/app.css +2303 -61
- package/dist/public/apps/web/src/landing.js +209 -8
- package/dist/public/apps/web/src/main.js +1143 -98
- package/dist/public/index.html +24 -1
- package/dist/public/landing.css +3286 -80
- package/dist/public/landing.html +21 -1
- package/dist/public/packages/core/src/calendar.js +41 -18
- package/dist/public/packages/core/src/coverage.js +12 -0
- package/dist/public/packages/core/src/subscriptions.js +146 -21
- package/dist/public/packages/receipt/src/index.js +392 -82
- package/dist/public/packages/ui/src/brand.js +43 -27
- package/dist/public/privacy.css +197 -9
- package/package.json +1 -1
package/dist/public/landing.html
CHANGED
|
@@ -1 +1,21 @@
|
|
|
1
|
-
<!doctype html
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
|
+
<meta name="theme-color" content="#080b0d" />
|
|
7
|
+
<meta
|
|
8
|
+
name="description"
|
|
9
|
+
content="See what your Codex and Claude Code usage would have cost via API. Local, read-only, no account."
|
|
10
|
+
/>
|
|
11
|
+
<title>SubValue — Know what your AI subscription is worth</title>
|
|
12
|
+
<link rel="icon" href="./icon.svg" />
|
|
13
|
+
<link rel="preload" href="/fonts/inter-latin.woff2" as="font" type="font/woff2" crossorigin />
|
|
14
|
+
<link rel="preload" href="/fonts/plex-mono.woff2" as="font" type="font/woff2" crossorigin />
|
|
15
|
+
<link rel="stylesheet" href="./landing.css" />
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<div id="landing"></div>
|
|
19
|
+
<script type="module" src="./landing.js"></script>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -1,24 +1,47 @@
|
|
|
1
1
|
/** Count whole local calendar months; rolling presets are never subscription comparisons. */
|
|
2
|
-
export function fullCalendarMonths(range
|
|
3
|
-
|
|
2
|
+
export function fullCalendarMonths(range
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
) {
|
|
8
|
+
if (range.preset === '7d' || range.preset === '30d') return null;
|
|
4
9
|
// Browser timezone overrides must not change the server's calendar selection.
|
|
5
|
-
if(range.calendarMonths!==undefined)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
if (range.calendarMonths !== undefined)
|
|
11
|
+
return Number.isInteger(range.calendarMonths) &&
|
|
12
|
+
range.calendarMonths > 0 &&
|
|
13
|
+
range.calendarMonths <= 1200
|
|
14
|
+
? range.calendarMonths
|
|
15
|
+
: null;
|
|
16
|
+
const start = new Date(range.from),
|
|
17
|
+
end = new Date(range.until);
|
|
18
|
+
const boundary = (date ) =>
|
|
19
|
+
Number.isFinite(+date) &&
|
|
20
|
+
date.getDate() === 1 &&
|
|
21
|
+
date.getHours() === 0 &&
|
|
22
|
+
date.getMinutes() === 0 &&
|
|
23
|
+
date.getSeconds() === 0 &&
|
|
24
|
+
date.getMilliseconds() === 0;
|
|
25
|
+
if (!boundary(start) || !boundary(end)) return null;
|
|
26
|
+
const months = (end.getFullYear() - start.getFullYear()) * 12 + end.getMonth() - start.getMonth();
|
|
27
|
+
return months > 0 && months <= 1200 ? months : null;
|
|
11
28
|
}
|
|
12
29
|
/** Civil-date movement uses UTC arithmetic so DST never drops or repeats a day. */
|
|
13
|
-
export function shiftCalendarDays(day
|
|
14
|
-
if(!/^\d{4}-\d{2}-\d{2}$/.test(day)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
30
|
+
export function shiftCalendarDays(day , days ) {
|
|
31
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(day) || !Number.isInteger(days) || Math.abs(days) > 36600)
|
|
32
|
+
return null;
|
|
33
|
+
const date = new Date(day + 'T12:00:00.000Z');
|
|
34
|
+
if (!Number.isFinite(+date) || date.toISOString().slice(0, 10) !== day) return null;
|
|
35
|
+
date.setUTCDate(date.getUTCDate() + days);
|
|
36
|
+
const result = date.toISOString().slice(0, 10);
|
|
37
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(result) ? result : null;
|
|
19
38
|
}
|
|
20
|
-
export function shiftCalendarMonth(month
|
|
21
|
-
if(!/^(?:19|20|21)\d{2}-(?:0[1-9]|1[0-2])$/.test(month)
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
export function shiftCalendarMonth(month , months ) {
|
|
40
|
+
if (!/^(?:19|20|21)\d{2}-(?:0[1-9]|1[0-2])$/.test(month) || !Number.isInteger(months))
|
|
41
|
+
return null;
|
|
42
|
+
const ordinal = Number(month.slice(0, 4)) * 12 + Number(month.slice(5)) - 1 + months,
|
|
43
|
+
year = Math.floor(ordinal / 12);
|
|
44
|
+
return year >= 1900 && year <= 2199
|
|
45
|
+
? `${year}-${String((ordinal % 12) + 1).padStart(2, '0')}`
|
|
46
|
+
: null;
|
|
24
47
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Reserve 100% for fully priced usage; missing coverage never becomes zero. */
|
|
2
|
+
export function formatPricingCoverage(coverage ) {
|
|
3
|
+
if (coverage === null || !Number.isFinite(coverage) || coverage < 0 || coverage > 1) {
|
|
4
|
+
return 'Unknown';
|
|
5
|
+
}
|
|
6
|
+
if (coverage === 1) return '100%';
|
|
7
|
+
if (coverage === 0) return '0%';
|
|
8
|
+
const percent = Math.round(coverage * 100);
|
|
9
|
+
if (percent === 100) return '<100%';
|
|
10
|
+
if (percent === 0) return '<1%';
|
|
11
|
+
return `${percent}%`;
|
|
12
|
+
}
|
|
@@ -1,22 +1,147 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export const subscriptionVerified = '2026-10-05';
|
|
13
|
+
const openai = 'https://learn.chatgpt.com/docs/pricing',
|
|
14
|
+
claude = 'https://claude.com/pricing';
|
|
15
|
+
export const subscriptionPlans = [
|
|
16
|
+
{
|
|
17
|
+
id: 'chatgpt-free',
|
|
18
|
+
provider: 'codex',
|
|
19
|
+
name: 'ChatGPT Free',
|
|
20
|
+
monthly: 0,
|
|
21
|
+
interval: 'monthly',
|
|
22
|
+
source: openai,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'chatgpt-go',
|
|
26
|
+
provider: 'codex',
|
|
27
|
+
name: 'ChatGPT Go',
|
|
28
|
+
monthly: 8,
|
|
29
|
+
interval: 'monthly',
|
|
30
|
+
source: openai,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'chatgpt-plus',
|
|
34
|
+
provider: 'codex',
|
|
35
|
+
name: 'ChatGPT Plus',
|
|
36
|
+
monthly: 20,
|
|
37
|
+
interval: 'monthly',
|
|
38
|
+
source: openai,
|
|
39
|
+
},
|
|
40
|
+
...[100, 200, 500].map((monthly) => ({
|
|
41
|
+
id: `chatgpt-pro-${monthly}`,
|
|
42
|
+
provider: 'codex' ,
|
|
43
|
+
name: 'ChatGPT Pro',
|
|
44
|
+
monthly,
|
|
45
|
+
interval: 'monthly' ,
|
|
46
|
+
source: openai,
|
|
47
|
+
})),
|
|
48
|
+
{
|
|
49
|
+
id: 'chatgpt-business-monthly',
|
|
50
|
+
provider: 'codex',
|
|
51
|
+
name: 'ChatGPT Business',
|
|
52
|
+
monthly: 25,
|
|
53
|
+
interval: 'monthly',
|
|
54
|
+
perSeat: true,
|
|
55
|
+
source: openai,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'chatgpt-business-annual',
|
|
59
|
+
provider: 'codex',
|
|
60
|
+
name: 'ChatGPT Business',
|
|
61
|
+
monthly: 20,
|
|
62
|
+
annual: 240,
|
|
63
|
+
interval: 'annual',
|
|
64
|
+
perSeat: true,
|
|
65
|
+
source: openai,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'claude-pro-monthly',
|
|
69
|
+
provider: 'claude',
|
|
70
|
+
name: 'Claude Pro',
|
|
71
|
+
monthly: 20,
|
|
72
|
+
interval: 'monthly',
|
|
73
|
+
source: claude,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'claude-pro-annual',
|
|
77
|
+
provider: 'claude',
|
|
78
|
+
name: 'Claude Pro',
|
|
79
|
+
monthly: 200 / 12,
|
|
80
|
+
annual: 200,
|
|
81
|
+
interval: 'annual',
|
|
82
|
+
source: claude,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'claude-max-5x',
|
|
86
|
+
provider: 'claude',
|
|
87
|
+
name: 'Claude Max 5x',
|
|
88
|
+
monthly: 100,
|
|
89
|
+
interval: 'monthly',
|
|
90
|
+
source: 'https://support.claude.com/en/articles/11049741-what-is-the-max-plan',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'claude-max-20x',
|
|
94
|
+
provider: 'claude',
|
|
95
|
+
name: 'Claude Max 20x',
|
|
96
|
+
monthly: 200,
|
|
97
|
+
interval: 'monthly',
|
|
98
|
+
source: 'https://support.claude.com/en/articles/11049741-what-is-the-max-plan',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'claude-team-standard-monthly',
|
|
102
|
+
provider: 'claude',
|
|
103
|
+
name: 'Claude Team · Standard seat',
|
|
104
|
+
monthly: 25,
|
|
105
|
+
interval: 'monthly',
|
|
106
|
+
perSeat: true,
|
|
107
|
+
source: claude,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'claude-team-standard-annual',
|
|
111
|
+
provider: 'claude',
|
|
112
|
+
name: 'Claude Team · Standard seat',
|
|
113
|
+
monthly: 20,
|
|
114
|
+
annual: 240,
|
|
115
|
+
interval: 'annual',
|
|
116
|
+
perSeat: true,
|
|
117
|
+
source: claude,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'claude-team-premium-monthly',
|
|
121
|
+
provider: 'claude',
|
|
122
|
+
name: 'Claude Team · Premium seat',
|
|
123
|
+
monthly: 125,
|
|
124
|
+
interval: 'monthly',
|
|
125
|
+
perSeat: true,
|
|
126
|
+
source: claude,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 'claude-team-premium-annual',
|
|
130
|
+
provider: 'claude',
|
|
131
|
+
name: 'Claude Team · Premium seat',
|
|
132
|
+
monthly: 100,
|
|
133
|
+
annual: 1200,
|
|
134
|
+
interval: 'annual',
|
|
135
|
+
perSeat: true,
|
|
136
|
+
source: claude,
|
|
137
|
+
},
|
|
20
138
|
];
|
|
21
|
-
export function subscriptionPlan(
|
|
22
|
-
|
|
139
|
+
export function subscriptionPlan(
|
|
140
|
+
provider ,
|
|
141
|
+
id ,
|
|
142
|
+
) {
|
|
143
|
+
return subscriptionPlans.find((p) => p.provider === provider && p.id === id);
|
|
144
|
+
}
|
|
145
|
+
export function subscriptionPlanLabel(p ) {
|
|
146
|
+
return `${p.name} · $${p.interval === 'annual' ? p.annual + '/year' : p.monthly + '/month'}${p.perSeat ? ' / seat' : ''}`;
|
|
147
|
+
}
|