@subvalue/cli 0.1.4 → 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/PRE_RELEASE.md +1 -1
- package/README.md +6 -2
- package/dist/packages/cli/src/server.js +75 -28
- package/dist/packages/core/src/billing.js +112 -0
- package/dist/packages/core/src/summary.js +69 -68
- package/dist/packages/core/src/types.js +11 -2
- package/dist/packages/receipt/src/index.js +165 -17
- package/dist/packages/ui/src/brand.js +1 -0
- package/dist/public/app.css +210 -2
- package/dist/public/apps/web/src/landing.js +20 -5
- package/dist/public/apps/web/src/main.js +268 -69
- package/dist/public/landing.css +265 -29
- package/dist/public/packages/core/src/billing.js +112 -0
- package/dist/public/packages/receipt/src/index.js +165 -17
- package/dist/public/packages/ui/src/brand.js +1 -0
- package/package.json +1 -1
|
@@ -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,12 +325,19 @@ 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 ,
|
|
218
334
|
theme = 'dark',
|
|
335
|
+
presentation = 'standard',
|
|
219
336
|
) {
|
|
337
|
+
// A public example needs larger print when viewed inside the dashboard preview.
|
|
338
|
+
// The paper, layout and export renderer remain shared with the local application.
|
|
339
|
+
const printSize = (size ) =>
|
|
340
|
+
presentation === 'preview' ? Math.max(13, size * 1.12) : size;
|
|
220
341
|
const light = theme === 'light';
|
|
221
342
|
const ink = (color ) =>
|
|
222
343
|
light
|
|
@@ -288,6 +409,7 @@ export function drawReceipt(
|
|
|
288
409
|
bold = false,
|
|
289
410
|
maxWidth = width - 48,
|
|
290
411
|
) => {
|
|
412
|
+
size = printSize(size);
|
|
291
413
|
c.font = `${bold ? '600 ' : ''}${size}px ${mono}`;
|
|
292
414
|
while (c.measureText(value).width > maxWidth && size > 8) {
|
|
293
415
|
size -= 0.5;
|
|
@@ -307,7 +429,13 @@ export function drawReceipt(
|
|
|
307
429
|
c.stroke();
|
|
308
430
|
c.setLineDash([]);
|
|
309
431
|
};
|
|
310
|
-
|
|
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');
|
|
311
439
|
text('USAGE RECEIPT', width / 2, 92, 13, '#b6b8bd', 'center');
|
|
312
440
|
if (model.demo) text('EXAMPLE DATA', width / 2, 112, 10, '#a3a5a9', 'center');
|
|
313
441
|
text('Period', left, 137, 12.5, '#b4b5b9');
|
|
@@ -325,7 +453,8 @@ export function drawReceipt(
|
|
|
325
453
|
}
|
|
326
454
|
const provider = !row.right,
|
|
327
455
|
multiple = row.left === 'VALUE MULTIPLE';
|
|
328
|
-
const label =
|
|
456
|
+
const label =
|
|
457
|
+
row.left === 'CODEX' ? 'Codex' : row.left === 'CLAUDE CODE' ? 'Claude Code' : row.left;
|
|
329
458
|
const color =
|
|
330
459
|
row.accent === 'positive'
|
|
331
460
|
? '#37ec85'
|
|
@@ -334,18 +463,37 @@ export function drawReceipt(
|
|
|
334
463
|
: row.strong
|
|
335
464
|
? '#e1e2e4'
|
|
336
465
|
: '#bdc0c6';
|
|
337
|
-
const
|
|
338
|
-
|
|
466
|
+
const tokenRow = isTokenRow(row);
|
|
467
|
+
const valueSize = multiple ? 25 : row.accent ? 22 : row.strong ? 18 : tokenRow ? 13 : 16;
|
|
468
|
+
c.font = `${printSize(valueSize)}px ${mono}`;
|
|
339
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
|
+
}
|
|
340
488
|
text(
|
|
341
489
|
label,
|
|
342
|
-
|
|
490
|
+
labelLeft,
|
|
343
491
|
y,
|
|
344
|
-
multiple ? 17 : provider ? 17 :
|
|
492
|
+
multiple ? 17 : provider ? 17 : tokenRow ? 12 : 14,
|
|
345
493
|
multiple ? '#e1e2e4' : color,
|
|
346
494
|
'left',
|
|
347
495
|
provider,
|
|
348
|
-
provider ?
|
|
496
|
+
provider ? right - labelLeft : width - 62 - valueWidth,
|
|
349
497
|
);
|
|
350
498
|
text(row.right, right, y, valueSize, row.accent ? color : '#e3e4e7', 'right', row.strong, 160);
|
|
351
499
|
y += step;
|