@subvalue/cli 0.1.5 → 0.1.6
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 +6 -2
- package/dist/packages/cli/src/server.js +74 -27
- package/dist/packages/core/src/billing.js +112 -0
- package/dist/packages/core/src/summary.js +69 -70
- package/dist/packages/core/src/types.js +10 -2
- package/dist/packages/receipt/src/index.js +158 -16
- package/dist/public/app.css +185 -0
- package/dist/public/apps/web/src/landing.js +8 -2
- package/dist/public/apps/web/src/main.js +243 -44
- package/dist/public/landing.css +185 -0
- package/dist/public/packages/core/src/billing.js +112 -0
- package/dist/public/packages/receipt/src/index.js +158 -16
- package/package.json +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { fullCalendarMonths, shiftCalendarMonth } from './calendar.js';
|
|
4
|
+
|
|
5
|
+
function civilDate(date ) {
|
|
6
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
7
|
+
}
|
|
8
|
+
export function billingPeriodKey(range ) {
|
|
9
|
+
return (
|
|
10
|
+
(range.calendarFrom ?? civilDate(new Date(range.from))) +
|
|
11
|
+
':' +
|
|
12
|
+
(range.calendarTo ?? civilDate(new Date(Date.parse(range.until) - 1)))
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
export function withPeriodBilling(
|
|
16
|
+
settings ,
|
|
17
|
+
provider ,
|
|
18
|
+
range ,
|
|
19
|
+
billing ,
|
|
20
|
+
) {
|
|
21
|
+
const next = structuredClone(settings),
|
|
22
|
+
exactKey = billingPeriodKey(range),
|
|
23
|
+
monthly = fullCalendarMonths(range) === 1,
|
|
24
|
+
key = monthly ? exactKey.slice(0, 7) : exactKey,
|
|
25
|
+
declarations = monthly ? (next.monthlyBilling ??= {}) : (next.periodBilling ??= {});
|
|
26
|
+
// A rolling range can share dates with a calendar month. Editing that month
|
|
27
|
+
// supersedes an earlier exact-date declaration instead of leaving a stale override.
|
|
28
|
+
if (monthly && next.periodBilling?.[exactKey]?.[provider]) {
|
|
29
|
+
delete next.periodBilling[exactKey][provider];
|
|
30
|
+
if (!Object.keys(next.periodBilling[exactKey]).length) delete next.periodBilling[exactKey];
|
|
31
|
+
}
|
|
32
|
+
declarations[key] ??= {};
|
|
33
|
+
if (billing) declarations[key][provider] = billing;
|
|
34
|
+
else delete declarations[key][provider];
|
|
35
|
+
if (!Object.keys(declarations[key]).length) delete declarations[key];
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
export function billingForSelection(settings , provider , range ) {
|
|
39
|
+
const exact = settings.periodBilling?.[billingPeriodKey(range)]?.[provider];
|
|
40
|
+
if (exact) return exact;
|
|
41
|
+
if (fullCalendarMonths(range) === 1)
|
|
42
|
+
return billingForMonth(settings, provider, billingPeriodKey(range).slice(0, 7));
|
|
43
|
+
return billingForMonth({ ...settings, monthlyBilling: undefined }, provider, '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Keep the dashboard action limited to mixed billing or a second provider without a plan. */
|
|
47
|
+
export function showExpenseAction(
|
|
48
|
+
provider ,
|
|
49
|
+
providers ,
|
|
50
|
+
) {
|
|
51
|
+
if (!provider.records) return false;
|
|
52
|
+
if (provider.mode === 'MIXED') return true;
|
|
53
|
+
return (
|
|
54
|
+
(provider.mode === 'API' ||
|
|
55
|
+
provider.mode === 'NO_SUBSCRIPTION' ||
|
|
56
|
+
provider.mode === 'UNKNOWN') &&
|
|
57
|
+
providers.some(
|
|
58
|
+
(other) =>
|
|
59
|
+
other.provider !== provider.provider &&
|
|
60
|
+
other.records > 0 &&
|
|
61
|
+
(other.mode === 'SUBSCRIPTION' || other.mode === 'MIXED') &&
|
|
62
|
+
(other.monthly != null || (other.comparison.subscription ?? 0) > 0),
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A default is a user preference, not evidence of how a historical request was billed. */
|
|
68
|
+
export function billingForMonth(settings , provider , month ) {
|
|
69
|
+
const saved = settings.monthlyBilling?.[month]?.[provider] ?? settings.billing[provider];
|
|
70
|
+
return {
|
|
71
|
+
...saved,
|
|
72
|
+
mode: saved.mode === 'UNKNOWN' || saved.mode === 'NO_SUBSCRIPTION' ? 'API' : saved.mode,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function billingForRange(settings , provider , range ) {
|
|
77
|
+
const count = fullCalendarMonths(range);
|
|
78
|
+
const exact = settings.periodBilling?.[billingPeriodKey(range)]?.[provider];
|
|
79
|
+
const start = new Date(range.from);
|
|
80
|
+
const first =
|
|
81
|
+
range.calendarFrom?.slice(0, 7) ??
|
|
82
|
+
`${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, '0')}`;
|
|
83
|
+
const months = Array.from(
|
|
84
|
+
{ length: count ?? 1 },
|
|
85
|
+
(_, index) => exact ?? billingForMonth(settings, provider, shiftCalendarMonth(first, index) ),
|
|
86
|
+
);
|
|
87
|
+
const mode = months.every((b) => b.mode === months[0].mode)
|
|
88
|
+
? months[0].mode
|
|
89
|
+
: 'MIXED';
|
|
90
|
+
const subscriptionKnown = months.every((b) => b.mode === 'API' || b.monthly !== null);
|
|
91
|
+
const apiKnown = months.every((b) => b.mode === 'SUBSCRIPTION' || b.apiSpend != null);
|
|
92
|
+
const subscription =
|
|
93
|
+
count !== null && subscriptionKnown
|
|
94
|
+
? months.reduce((total, b) => total + (b.mode === 'API' ? 0 : b.monthly ), 0)
|
|
95
|
+
: null;
|
|
96
|
+
const apiSpend =
|
|
97
|
+
count !== null && apiKnown
|
|
98
|
+
? exact
|
|
99
|
+
? exact.mode === 'SUBSCRIPTION'
|
|
100
|
+
? 0
|
|
101
|
+
: exact.apiSpend
|
|
102
|
+
: months.reduce((total, b) => total + (b.mode === 'SUBSCRIPTION' ? 0 : b.apiSpend ), 0)
|
|
103
|
+
: null;
|
|
104
|
+
return {
|
|
105
|
+
mode,
|
|
106
|
+
monthly: months.length === 1 ? months[0].monthly : null,
|
|
107
|
+
subscription,
|
|
108
|
+
apiSpend,
|
|
109
|
+
paid: subscription !== null && apiSpend !== null ? subscription + apiSpend : null,
|
|
110
|
+
needsBilling: !subscriptionKnown || !apiKnown,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
|
|
2
|
+
|
|
2
3
|
import { fullCalendarMonths } from '../../core/src/calendar.js';
|
|
3
4
|
import { formatPricingCoverage } from '../../core/src/coverage.js';
|
|
4
|
-
import { product } from '../../ui/src/brand.js';
|
|
5
|
+
import { product, wordmarkSvg } from '../../ui/src/brand.js';
|
|
5
6
|
/** Only public, aggregated fields needed by the receipt and its example. */
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
|
|
@@ -14,7 +15,8 @@ import { product } from '../../ui/src/brand.js';
|
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
|
|
@@ -37,23 +39,67 @@ export const money = (n , signed = false) =>
|
|
|
37
39
|
? 'Unavailable'
|
|
38
40
|
: `${signed && n >= 0 ? '+' : ''}${new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n)}`;
|
|
39
41
|
/** Compare only the displayed priced amount; preserve uncertainty and billing eligibility. */
|
|
40
|
-
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
export function comparisonForDisplay(s ) {
|
|
41
48
|
const comparison = s.total.comparison,
|
|
42
49
|
amount = s.total.apiEquivalent ?? s.total.knownSubtotal;
|
|
43
50
|
if (fullCalendarMonths(s.range) === null)
|
|
44
51
|
return {
|
|
45
52
|
...comparison,
|
|
46
53
|
subscription: null,
|
|
54
|
+
apiSpend: null,
|
|
55
|
+
paid: null,
|
|
47
56
|
value: null,
|
|
48
57
|
roi: null,
|
|
49
58
|
breakEven: null,
|
|
50
59
|
outcome: 'neutral',
|
|
60
|
+
scope: [],
|
|
61
|
+
apiEquivalent: null,
|
|
51
62
|
};
|
|
52
|
-
|
|
63
|
+
const paidFor = (p ) =>
|
|
64
|
+
p.comparison?.paid !== undefined
|
|
65
|
+
? p.comparison.paid
|
|
66
|
+
: p.mode === 'SUBSCRIPTION'
|
|
67
|
+
? (p.comparison?.subscription ?? null)
|
|
68
|
+
: null;
|
|
69
|
+
const active = s.providers.filter((p) => p.records > 0),
|
|
70
|
+
included = active.filter((p) => paidFor(p) !== null);
|
|
71
|
+
if (included.length > 0 && included.length < active.length) {
|
|
72
|
+
const subscription = included.reduce((n, p) => n + (p.comparison?.subscription ?? 0), 0),
|
|
73
|
+
apiSpend = included.reduce((n, p) => n + (p.comparison?.apiSpend ?? 0), 0),
|
|
74
|
+
paid = included.reduce((n, p) => n + paidFor(p) , 0),
|
|
75
|
+
priced = included.map((p) => p.apiEquivalent ?? p.knownSubtotal),
|
|
76
|
+
scopedAmount = priced.every((cost) => cost !== null)
|
|
77
|
+
? priced.reduce ((n, cost) => n + cost , 0)
|
|
78
|
+
: null,
|
|
79
|
+
certain = !s.historyPartial && included.every((p) => p.apiEquivalent !== null);
|
|
80
|
+
return {
|
|
81
|
+
subscription,
|
|
82
|
+
apiSpend,
|
|
83
|
+
paid,
|
|
84
|
+
value: scopedAmount === null ? null : scopedAmount - paid,
|
|
85
|
+
roi: scopedAmount === null || paid <= 0 ? null : scopedAmount / paid,
|
|
86
|
+
outcome: certain && included.length === 1 ? included[0].comparison .outcome : 'neutral',
|
|
87
|
+
breakEven: certain && included.length === 1 ? included[0].comparison .breakEven : null,
|
|
88
|
+
reason: 'Providers with declared expenses only',
|
|
89
|
+
scope: included.map((p) => p.provider),
|
|
90
|
+
apiEquivalent: scopedAmount,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const paid = comparison.paid === undefined ? comparison.subscription : comparison.paid;
|
|
94
|
+
if (amount === null || paid === null)
|
|
95
|
+
return { ...comparison, paid, scope: [], apiEquivalent: amount };
|
|
53
96
|
return {
|
|
54
97
|
...comparison,
|
|
55
|
-
|
|
56
|
-
|
|
98
|
+
paid,
|
|
99
|
+
value: comparison.value ?? amount - paid,
|
|
100
|
+
roi: comparison.roi ?? (paid > 0 ? amount / paid : null),
|
|
101
|
+
scope: [],
|
|
102
|
+
apiEquivalent: amount,
|
|
57
103
|
};
|
|
58
104
|
}
|
|
59
105
|
export function receiptModel(s , generatedAt = new Date()) {
|
|
@@ -86,12 +132,33 @@ export function receiptModel(s , generatedAt = new Date())
|
|
|
86
132
|
strong: true,
|
|
87
133
|
});
|
|
88
134
|
const comp = comparisonForDisplay(s);
|
|
89
|
-
if (comp.
|
|
135
|
+
if (comp.scope.length)
|
|
136
|
+
rows.push(
|
|
137
|
+
{ left: '', right: '' },
|
|
138
|
+
{
|
|
139
|
+
left:
|
|
140
|
+
comp.scope
|
|
141
|
+
.map((provider) => (provider === 'codex' ? 'CODEX' : 'CLAUDE CODE'))
|
|
142
|
+
.join(' + ') + ' COMPARISON',
|
|
143
|
+
right: '',
|
|
144
|
+
strong: true,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
left: 'API Equivalent',
|
|
148
|
+
right: comp.apiEquivalent === null ? '—' : money(comp.apiEquivalent),
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
if (comp.subscription != null && comp.subscription > 0)
|
|
90
152
|
rows.push({ left: 'Subscription Cost', right: money(comp.subscription) });
|
|
153
|
+
if (comp.apiSpend != null && (comp.apiSpend > 0 || comp.subscription === 0)) {
|
|
154
|
+
rows.push({ left: 'API Paid', right: money(comp.apiSpend) });
|
|
155
|
+
if (comp.subscription != null && comp.subscription > 0)
|
|
156
|
+
rows.push({ left: 'Total Paid', right: money(comp.paid ?? null), strong: true });
|
|
157
|
+
}
|
|
91
158
|
const value = comp.value;
|
|
92
159
|
if (value !== null)
|
|
93
160
|
rows.push({
|
|
94
|
-
left: '
|
|
161
|
+
left: 'Total Saved',
|
|
95
162
|
right: money(value, true),
|
|
96
163
|
accent: value >= 0 ? 'positive' : 'negative',
|
|
97
164
|
strong: true,
|
|
@@ -179,8 +246,53 @@ export async function receiptFonts() {
|
|
|
179
246
|
await Promise.all([
|
|
180
247
|
document.fonts.load('14px "Plex Mono"'),
|
|
181
248
|
document.fonts.load('600 14px "Plex Mono"'),
|
|
249
|
+
receiptBrandAssets(document),
|
|
182
250
|
]);
|
|
183
251
|
}
|
|
252
|
+
// Decode the same bundled product marks used by the dashboard before drawing.
|
|
253
|
+
// Canvas previews and exported PNGs therefore include identical local artwork.
|
|
254
|
+
const brandAssets = new WeakMap ();
|
|
255
|
+
const decodedBrands = new WeakMap ();
|
|
256
|
+
const decodedWordmarks = new WeakMap ();
|
|
257
|
+
function receiptBrandAssets(document ) {
|
|
258
|
+
let assets = brandAssets.get(document);
|
|
259
|
+
if (!assets) {
|
|
260
|
+
assets = Promise.all(
|
|
261
|
+
['codex-outline.svg', 'claude-code-clawd.svg'].map(async (file) => {
|
|
262
|
+
const image = document.createElement('img');
|
|
263
|
+
image.src = `/brands/${file}`;
|
|
264
|
+
await image.decode();
|
|
265
|
+
return image;
|
|
266
|
+
}),
|
|
267
|
+
).then(async ([codex, claude]) => {
|
|
268
|
+
const svg = wordmarkSvg();
|
|
269
|
+
if (svg) {
|
|
270
|
+
const dimensions = /viewBox="0 0 (\d+) (\d+)"/.exec(svg) ;
|
|
271
|
+
const images = await Promise.all(
|
|
272
|
+
(['dark', 'light'] ).map(async (theme) => {
|
|
273
|
+
const image = document.createElement('img');
|
|
274
|
+
const artwork = svg
|
|
275
|
+
.replace('<svg ', `<svg width="${dimensions[1]}" height="${dimensions[2]}" `)
|
|
276
|
+
.replace('fill="currentColor"', `fill="${theme === 'dark' ? '#eef2f4' : '#22272d'}"`)
|
|
277
|
+
.replaceAll(
|
|
278
|
+
'class="brand-name-accent"',
|
|
279
|
+
`fill="${theme === 'dark' ? '#37ec85' : '#137744'}"`,
|
|
280
|
+
);
|
|
281
|
+
image.src = 'data:image/svg+xml,' + encodeURIComponent(artwork);
|
|
282
|
+
await image.decode();
|
|
283
|
+
return image;
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
decodedWordmarks.set(document, { dark: images[0], light: images[1] });
|
|
287
|
+
}
|
|
288
|
+
const marks = { codex, claude };
|
|
289
|
+
decodedBrands.set(document, marks);
|
|
290
|
+
return marks;
|
|
291
|
+
});
|
|
292
|
+
brandAssets.set(document, assets);
|
|
293
|
+
}
|
|
294
|
+
return assets;
|
|
295
|
+
}
|
|
184
296
|
const paperTextures = new WeakMap ();
|
|
185
297
|
function paperTexture(canvas ) {
|
|
186
298
|
const document = canvas.ownerDocument;
|
|
@@ -202,6 +314,8 @@ function paperTexture(canvas ) {
|
|
|
202
314
|
paperTextures.set(document, texture);
|
|
203
315
|
return texture;
|
|
204
316
|
}
|
|
317
|
+
const isTokenRow = (row ) =>
|
|
318
|
+
['Input Tokens', 'Output Tokens', 'Cache Reads', 'Tokens', 'Known Tokens'].includes(row.left);
|
|
205
319
|
const rowHeight = (row ) =>
|
|
206
320
|
!row.left && !row.right
|
|
207
321
|
? 34
|
|
@@ -211,7 +325,9 @@ const rowHeight = (row ) =>
|
|
|
211
325
|
? 46
|
|
212
326
|
: row.accent
|
|
213
327
|
? 32
|
|
214
|
-
:
|
|
328
|
+
: isTokenRow(row)
|
|
329
|
+
? 24
|
|
330
|
+
: 28;
|
|
215
331
|
export function drawReceipt(
|
|
216
332
|
canvas ,
|
|
217
333
|
model ,
|
|
@@ -313,7 +429,13 @@ export function drawReceipt(
|
|
|
313
429
|
c.stroke();
|
|
314
430
|
c.setLineDash([]);
|
|
315
431
|
};
|
|
316
|
-
|
|
432
|
+
const wordmark = decodedWordmarks.get(canvas.ownerDocument)?.[theme];
|
|
433
|
+
if (wordmark) {
|
|
434
|
+
// Compact, centered lettering only; no symbol or terminal cursor on the paper.
|
|
435
|
+
const logoWidth = 156,
|
|
436
|
+
logoHeight = logoWidth * (wordmark.naturalHeight / wordmark.naturalWidth);
|
|
437
|
+
c.drawImage(wordmark, (width - logoWidth) / 2, 64 - logoHeight, logoWidth, logoHeight);
|
|
438
|
+
} else text(product.name, width / 2, 64, 29, '#f0f0ef', 'center');
|
|
317
439
|
text('USAGE RECEIPT', width / 2, 92, 13, '#b6b8bd', 'center');
|
|
318
440
|
if (model.demo) text('EXAMPLE DATA', width / 2, 112, 10, '#a3a5a9', 'center');
|
|
319
441
|
text('Period', left, 137, 12.5, '#b4b5b9');
|
|
@@ -331,7 +453,8 @@ export function drawReceipt(
|
|
|
331
453
|
}
|
|
332
454
|
const provider = !row.right,
|
|
333
455
|
multiple = row.left === 'VALUE MULTIPLE';
|
|
334
|
-
const label =
|
|
456
|
+
const label =
|
|
457
|
+
row.left === 'CODEX' ? 'Codex' : row.left === 'CLAUDE CODE' ? 'Claude Code' : row.left;
|
|
335
458
|
const color =
|
|
336
459
|
row.accent === 'positive'
|
|
337
460
|
? '#37ec85'
|
|
@@ -340,18 +463,37 @@ export function drawReceipt(
|
|
|
340
463
|
: row.strong
|
|
341
464
|
? '#e1e2e4'
|
|
342
465
|
: '#bdc0c6';
|
|
343
|
-
const
|
|
466
|
+
const tokenRow = isTokenRow(row);
|
|
467
|
+
const valueSize = multiple ? 25 : row.accent ? 22 : row.strong ? 18 : tokenRow ? 13 : 16;
|
|
344
468
|
c.font = `${printSize(valueSize)}px ${mono}`;
|
|
345
469
|
const valueWidth = c.measureText(row.right).width;
|
|
470
|
+
const brand = row.left === 'CODEX' ? 'codex' : row.left === 'CLAUDE CODE' ? 'claude' : null;
|
|
471
|
+
const mark = brand && decodedBrands.get(canvas.ownerDocument)?.[brand];
|
|
472
|
+
let labelLeft = left;
|
|
473
|
+
if (mark) {
|
|
474
|
+
const size = 22;
|
|
475
|
+
const markHeight = size * (mark.naturalHeight / mark.naturalWidth);
|
|
476
|
+
if (brand === 'codex') {
|
|
477
|
+
const tinted = canvas.ownerDocument.createElement('canvas');
|
|
478
|
+
tinted.width = tinted.height = size * scale;
|
|
479
|
+
const context = tinted.getContext('2d') ;
|
|
480
|
+
context.drawImage(mark, 0, 0, tinted.width, tinted.height);
|
|
481
|
+
context.globalCompositeOperation = 'source-in';
|
|
482
|
+
context.fillStyle = ink('#e1e2e4');
|
|
483
|
+
context.fillRect(0, 0, tinted.width, tinted.height);
|
|
484
|
+
c.drawImage(tinted, left, y - 17, size, size);
|
|
485
|
+
} else c.drawImage(mark, left, y - 14, size, markHeight);
|
|
486
|
+
labelLeft += size + 8;
|
|
487
|
+
}
|
|
346
488
|
text(
|
|
347
489
|
label,
|
|
348
|
-
|
|
490
|
+
labelLeft,
|
|
349
491
|
y,
|
|
350
|
-
multiple ? 17 : provider ? 17 :
|
|
492
|
+
multiple ? 17 : provider ? 17 : tokenRow ? 12 : 14,
|
|
351
493
|
multiple ? '#e1e2e4' : color,
|
|
352
494
|
'left',
|
|
353
495
|
provider,
|
|
354
|
-
provider ?
|
|
496
|
+
provider ? right - labelLeft : width - 62 - valueWidth,
|
|
355
497
|
);
|
|
356
498
|
text(row.right, right, y, valueSize, row.accent ? color : '#e3e4e7', 'right', row.strong, 160);
|
|
357
499
|
y += step;
|