standout 0.5.25 → 0.5.27
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/dist/cli.js +2 -2
- package/dist/cursor.d.ts +21 -18
- package/dist/cursor.js +95 -20
- package/dist/payload.js +13 -0
- package/dist/wrapped/aggregate.js +13 -6
- package/dist/wrapped/render.js +13 -7
- package/dist/wrapped/types.d.ts +2 -1
- package/dist/wrapped-client.js +88 -22
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -119,7 +119,7 @@ async function maybeShareWrapped(wrapped) {
|
|
|
119
119
|
process.stderr.write(` opened: ${wrapped.share_url}\n\n`);
|
|
120
120
|
}
|
|
121
121
|
catch (err) {
|
|
122
|
-
process.stderr.write(` couldn't open browser
|
|
122
|
+
process.stderr.write(` couldn't open browser. open it manually: ${wrapped.share_url}\n`);
|
|
123
123
|
process.stderr.write(` (${err instanceof Error ? err.message : err})\n\n`);
|
|
124
124
|
}
|
|
125
125
|
}
|
|
@@ -197,7 +197,7 @@ async function runAgent(jobId) {
|
|
|
197
197
|
await maybeShareWrapped(wrapped);
|
|
198
198
|
}
|
|
199
199
|
else {
|
|
200
|
-
process.stderr.write(" (couldn't generate wrapped right now
|
|
200
|
+
process.stderr.write(" (couldn't generate wrapped right now. continuing profile setup.)\n\n");
|
|
201
201
|
}
|
|
202
202
|
if (!profileChatEnabled()) {
|
|
203
203
|
return;
|
package/dist/cursor.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface CursorStats {
|
|
|
11
11
|
total: number;
|
|
12
12
|
};
|
|
13
13
|
models: string[];
|
|
14
|
+
model_session_counts: Record<string, number>;
|
|
15
|
+
tool_call_counts: Record<string, number>;
|
|
14
16
|
hour_buckets: number[];
|
|
15
17
|
weekend_ratio: number;
|
|
16
18
|
languages: Record<string, number>;
|
|
@@ -18,15 +20,12 @@ export interface CursorStats {
|
|
|
18
20
|
prompt_frequency: PromptFrequency[];
|
|
19
21
|
exchanges: ConversationExchange[];
|
|
20
22
|
interaction: InteractionSignals;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
cache_tokens: number;
|
|
28
|
-
primary_model: string | null;
|
|
29
|
-
}[];
|
|
23
|
+
total_input_tokens: number;
|
|
24
|
+
total_output_tokens: number;
|
|
25
|
+
total_cache_read_tokens: number;
|
|
26
|
+
total_cache_write_tokens: number;
|
|
27
|
+
tokens_estimated: true;
|
|
28
|
+
monthly_buckets: CursorMonthlyBucket[];
|
|
30
29
|
all_time?: {
|
|
31
30
|
total_sessions: number;
|
|
32
31
|
active_days: number;
|
|
@@ -37,18 +36,22 @@ export interface CursorStats {
|
|
|
37
36
|
total_output_tokens: number;
|
|
38
37
|
total_cache_read_tokens: number;
|
|
39
38
|
total_cache_write_tokens: number;
|
|
40
|
-
monthly_buckets:
|
|
41
|
-
month: string;
|
|
42
|
-
sessions: number;
|
|
43
|
-
duration_hours: number;
|
|
44
|
-
input_tokens: number;
|
|
45
|
-
output_tokens: number;
|
|
46
|
-
cache_tokens: number;
|
|
47
|
-
primary_model: string | null;
|
|
48
|
-
}[];
|
|
39
|
+
monthly_buckets: CursorMonthlyBucket[];
|
|
49
40
|
};
|
|
50
41
|
}
|
|
42
|
+
interface CursorMonthlyBucket {
|
|
43
|
+
month: string;
|
|
44
|
+
sessions: number;
|
|
45
|
+
duration_hours: number;
|
|
46
|
+
input_tokens: number;
|
|
47
|
+
output_tokens: number;
|
|
48
|
+
cache_read_tokens: number;
|
|
49
|
+
cache_write_tokens: number;
|
|
50
|
+
cache_tokens: number;
|
|
51
|
+
primary_model: string | null;
|
|
52
|
+
}
|
|
51
53
|
export declare function gatherCursor(opts?: {
|
|
52
54
|
dbPath?: string;
|
|
53
55
|
windowStartMs?: number;
|
|
54
56
|
}): Promise<CursorStats | null>;
|
|
57
|
+
export {};
|
package/dist/cursor.js
CHANGED
|
@@ -16,6 +16,27 @@ const CORRECTION_RE = /\b(no|nope|actually|wrong|incorrect|instead|revert|undo|d
|
|
|
16
16
|
// Mirrors ai-usage.ts — cannot import (would create a module cycle).
|
|
17
17
|
const TRIVIAL_ASK_RE = /^(y|n|yes|yep|yeah|ya|ok|okay|k|sure|go|go ahead|do it|continue|please continue|next|thanks|thank you|thx|ty|nice|cool|perfect|great|good|done|ship it|lgtm|👍|\.|\?)\b[\s.!]*$/i;
|
|
18
18
|
const SHELL_COMMAND_RE = /^(npx|npm|pnpm|yarn|bun|git|cd|ls|cat|node|python|pip|brew|sudo|docker|curl|echo|mkdir|rm|mv|cp)\b/i;
|
|
19
|
+
// Cursor persists no usable token/cost data locally — bubble `tokenCount` is
|
|
20
|
+
// zeroed and composer `usageData` is empty. We estimate both from active
|
|
21
|
+
// duration using the economics of our Claude Code cohort (Cursor runs the same
|
|
22
|
+
// Claude models). The per-hour token split is calibrated so that, priced at the
|
|
23
|
+
// generic retail fallback rate, it totals ~$20/active-hour — the cohort's
|
|
24
|
+
// blended retail $/hour. Surfaced with an "estimated" disclaimer on the cards.
|
|
25
|
+
const CURSOR_EST_TOKENS_PER_HOUR = {
|
|
26
|
+
input: 38_000,
|
|
27
|
+
output: 110_000,
|
|
28
|
+
cacheRead: 24_750_000,
|
|
29
|
+
cacheWrite: 750_000,
|
|
30
|
+
};
|
|
31
|
+
function estimateCursorTokens(hours) {
|
|
32
|
+
const h = hours > 0 ? hours : 0;
|
|
33
|
+
return {
|
|
34
|
+
input: Math.round(CURSOR_EST_TOKENS_PER_HOUR.input * h),
|
|
35
|
+
output: Math.round(CURSOR_EST_TOKENS_PER_HOUR.output * h),
|
|
36
|
+
cacheRead: Math.round(CURSOR_EST_TOKENS_PER_HOUR.cacheRead * h),
|
|
37
|
+
cacheWrite: Math.round(CURSOR_EST_TOKENS_PER_HOUR.cacheWrite * h),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
19
40
|
function isLowSignalAsk(sample) {
|
|
20
41
|
const t = sample.trim();
|
|
21
42
|
return t.length < 12 || TRIVIAL_ASK_RE.test(t) || SHELL_COMMAND_RE.test(t);
|
|
@@ -159,13 +180,20 @@ async function readState(dbPath, Driver) {
|
|
|
159
180
|
continue;
|
|
160
181
|
const composerId = parsed.composerId ??
|
|
161
182
|
row.key.slice("composerData:".length);
|
|
183
|
+
const modelConfig = parsed.modelConfig && typeof parsed.modelConfig === "object"
|
|
184
|
+
? parsed.modelConfig
|
|
185
|
+
: null;
|
|
186
|
+
const modelName = (modelConfig && typeof modelConfig.modelName === "string"
|
|
187
|
+
? modelConfig.modelName
|
|
188
|
+
: null) ??
|
|
189
|
+
(typeof parsed.latestSelectedModel === "string"
|
|
190
|
+
? parsed.latestSelectedModel
|
|
191
|
+
: null);
|
|
162
192
|
composers.set(composerId, {
|
|
163
193
|
composerId,
|
|
164
194
|
createdAt: numericTs(parsed.createdAt),
|
|
165
195
|
lastUpdatedAt: numericTs(parsed.lastUpdatedAt),
|
|
166
|
-
|
|
167
|
-
? parsed.latestSelectedModel
|
|
168
|
-
: null,
|
|
196
|
+
modelName: modelName && modelName !== "default" ? modelName : null,
|
|
169
197
|
});
|
|
170
198
|
}
|
|
171
199
|
if (rows.length < PAGE)
|
|
@@ -196,12 +224,22 @@ async function readState(dbPath, Driver) {
|
|
|
196
224
|
const text = typeof parsed.text === "string" ? parsed.text : null;
|
|
197
225
|
const createdAt = numericTs(parsed.createdAt);
|
|
198
226
|
const filePaths = extractFilePaths(parsed);
|
|
227
|
+
const tool = parsed.toolFormerData && typeof parsed.toolFormerData === "object"
|
|
228
|
+
? parsed.toolFormerData
|
|
229
|
+
: null;
|
|
230
|
+
const toolName = tool &&
|
|
231
|
+
(typeof tool.name === "string"
|
|
232
|
+
? tool.name
|
|
233
|
+
: typeof tool.tool === "string"
|
|
234
|
+
? tool.tool
|
|
235
|
+
: null);
|
|
199
236
|
bubbles.set(parts[2], {
|
|
200
237
|
conversationId,
|
|
201
238
|
type,
|
|
202
239
|
text,
|
|
203
240
|
createdAt,
|
|
204
241
|
filePaths,
|
|
242
|
+
toolName: toolName || null,
|
|
205
243
|
});
|
|
206
244
|
}
|
|
207
245
|
if (rows.length < PAGE)
|
|
@@ -294,10 +332,10 @@ export async function gatherCursor(opts = {}) {
|
|
|
294
332
|
first_session: allTime.first_session,
|
|
295
333
|
last_session: allTime.last_session,
|
|
296
334
|
total_duration_hours: allTime.total_duration_hours,
|
|
297
|
-
total_input_tokens:
|
|
298
|
-
total_output_tokens:
|
|
299
|
-
total_cache_read_tokens:
|
|
300
|
-
total_cache_write_tokens:
|
|
335
|
+
total_input_tokens: allTime.total_input_tokens,
|
|
336
|
+
total_output_tokens: allTime.total_output_tokens,
|
|
337
|
+
total_cache_read_tokens: allTime.total_cache_read_tokens,
|
|
338
|
+
total_cache_write_tokens: allTime.total_cache_write_tokens,
|
|
301
339
|
monthly_buckets: allTime.monthly_buckets,
|
|
302
340
|
};
|
|
303
341
|
return stats.total_sessions > 0 || allTime.total_sessions > 0 ? stats : null;
|
|
@@ -335,6 +373,9 @@ function finalize(raw, opts = {}) {
|
|
|
335
373
|
let userTurns = 0;
|
|
336
374
|
let correctionTurns = 0;
|
|
337
375
|
let countedSessions = 0;
|
|
376
|
+
let toolCalls = 0;
|
|
377
|
+
const modelSessionCounts = {};
|
|
378
|
+
const toolCallCounts = {};
|
|
338
379
|
const monthly = new Map();
|
|
339
380
|
for (const [composerId, composer] of raw.composers.entries()) {
|
|
340
381
|
const entry = sessionBubbles.get(composerId);
|
|
@@ -366,6 +407,10 @@ function finalize(raw, opts = {}) {
|
|
|
366
407
|
month.sessions += 1;
|
|
367
408
|
month.durationMs += durationMs;
|
|
368
409
|
monthly.set(monthKey, month);
|
|
410
|
+
if (composer.modelName) {
|
|
411
|
+
modelSessionCounts[composer.modelName] =
|
|
412
|
+
(modelSessionCounts[composer.modelName] ?? 0) + 1;
|
|
413
|
+
}
|
|
369
414
|
for (const ts of windowTs) {
|
|
370
415
|
const d = new Date(ts);
|
|
371
416
|
if (Number.isNaN(d.getTime()))
|
|
@@ -385,6 +430,11 @@ function finalize(raw, opts = {}) {
|
|
|
385
430
|
if (allFiles.size < MAX_FILE_PATHS)
|
|
386
431
|
allFiles.add(fp);
|
|
387
432
|
}
|
|
433
|
+
if (bubble.toolName) {
|
|
434
|
+
toolCalls += 1;
|
|
435
|
+
toolCallCounts[bubble.toolName] =
|
|
436
|
+
(toolCallCounts[bubble.toolName] ?? 0) + 1;
|
|
437
|
+
}
|
|
388
438
|
}
|
|
389
439
|
const windowSamples = windowBubbles
|
|
390
440
|
.filter((bubble) => bubble.type === 1 && bubble.text)
|
|
@@ -443,6 +493,34 @@ function finalize(raw, opts = {}) {
|
|
|
443
493
|
languages[lang] = (languages[lang] || 0) + 1;
|
|
444
494
|
}
|
|
445
495
|
}
|
|
496
|
+
const monthlyBuckets = [...monthly.entries()]
|
|
497
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
498
|
+
.map(([month, b]) => {
|
|
499
|
+
const hours = +(b.durationMs / 1000 / 3600).toFixed(1);
|
|
500
|
+
const est = estimateCursorTokens(hours);
|
|
501
|
+
return {
|
|
502
|
+
month,
|
|
503
|
+
sessions: b.sessions,
|
|
504
|
+
duration_hours: hours,
|
|
505
|
+
input_tokens: est.input,
|
|
506
|
+
output_tokens: est.output,
|
|
507
|
+
cache_read_tokens: est.cacheRead,
|
|
508
|
+
cache_write_tokens: est.cacheWrite,
|
|
509
|
+
cache_tokens: est.cacheRead + est.cacheWrite,
|
|
510
|
+
primary_model: null,
|
|
511
|
+
};
|
|
512
|
+
});
|
|
513
|
+
// Totals are the exact sum of the (rounded-per-month) buckets so the top-level
|
|
514
|
+
// figures metrics.ts reads match what the card sums from monthly_buckets.
|
|
515
|
+
const totals = monthlyBuckets.reduce((acc, b) => ({
|
|
516
|
+
input: acc.input + b.input_tokens,
|
|
517
|
+
output: acc.output + b.output_tokens,
|
|
518
|
+
cacheRead: acc.cacheRead + b.cache_read_tokens,
|
|
519
|
+
cacheWrite: acc.cacheWrite + b.cache_write_tokens,
|
|
520
|
+
}), { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
521
|
+
const models = Object.entries(modelSessionCounts)
|
|
522
|
+
.sort((a, b) => b[1] - a[1])
|
|
523
|
+
.map(([model]) => model);
|
|
446
524
|
return {
|
|
447
525
|
total_sessions: countedSessions,
|
|
448
526
|
active_days: activeDays.size,
|
|
@@ -458,7 +536,9 @@ function finalize(raw, opts = {}) {
|
|
|
458
536
|
assistant: assistantTotal,
|
|
459
537
|
total: userTotal + assistantTotal,
|
|
460
538
|
},
|
|
461
|
-
models
|
|
539
|
+
models,
|
|
540
|
+
model_session_counts: modelSessionCounts,
|
|
541
|
+
tool_call_counts: toolCallCounts,
|
|
462
542
|
hour_buckets: hourBuckets,
|
|
463
543
|
weekend_ratio: totalEvents > 0 ? +(weekendEvents / totalEvents).toFixed(2) : 0,
|
|
464
544
|
languages,
|
|
@@ -470,18 +550,13 @@ function finalize(raw, opts = {}) {
|
|
|
470
550
|
interaction: {
|
|
471
551
|
user_turns: userTurns,
|
|
472
552
|
correction_turns: correctionTurns,
|
|
473
|
-
tool_calls:
|
|
553
|
+
tool_calls: toolCalls,
|
|
474
554
|
},
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
input_tokens: 0,
|
|
482
|
-
output_tokens: 0,
|
|
483
|
-
cache_tokens: 0,
|
|
484
|
-
primary_model: null,
|
|
485
|
-
})),
|
|
555
|
+
total_input_tokens: totals.input,
|
|
556
|
+
total_output_tokens: totals.output,
|
|
557
|
+
total_cache_read_tokens: totals.cacheRead,
|
|
558
|
+
total_cache_write_tokens: totals.cacheWrite,
|
|
559
|
+
tokens_estimated: true,
|
|
560
|
+
monthly_buckets: monthlyBuckets,
|
|
486
561
|
};
|
|
487
562
|
}
|
package/dist/payload.js
CHANGED
|
@@ -104,6 +104,19 @@ export function capPayload(payload) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
// 2b. Display-only prompt samples (not read by any server card).
|
|
108
|
+
if (over()) {
|
|
109
|
+
for (const t of TOOLS) {
|
|
110
|
+
const tool = asRecord(aiUsage[t]);
|
|
111
|
+
if (tool &&
|
|
112
|
+
Array.isArray(tool.prompt_samples) &&
|
|
113
|
+
tool.prompt_samples.length) {
|
|
114
|
+
tool.prompt_samples = [];
|
|
115
|
+
if (!dropped.includes("prompt_samples"))
|
|
116
|
+
dropped.push("prompt_samples");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
107
120
|
// 3. Commit subjects + repo README excerpts (contributions/blurbs inputs).
|
|
108
121
|
if (over()) {
|
|
109
122
|
const aiCoding = asRecord(p.ai_coding);
|
|
@@ -495,13 +495,15 @@ function computeTokens(aiUsage) {
|
|
|
495
495
|
const cacheTotal = (b) => b.cache_read_tokens !== undefined || b.cache_write_tokens !== undefined
|
|
496
496
|
? (b.cache_read_tokens ?? 0) + (b.cache_write_tokens ?? 0)
|
|
497
497
|
: (b.cache_tokens ?? 0);
|
|
498
|
-
// Cursor exposes no
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
498
|
+
// Cursor exposes no local token counts — its tokens/cost are estimated from
|
|
499
|
+
// active time (see cursor.ts). Flag it so the card discloses the estimate, but
|
|
500
|
+
// only when it was actually used in THIS window (the card is "last 30 days").
|
|
501
|
+
// All-time Cursor sessions don't count: claiming "Cursor too" when they haven't
|
|
502
|
+
// opened it in a month is just wrong.
|
|
502
503
|
const CURSOR_MIN_SESSIONS = 3;
|
|
503
504
|
const cursorStats = aiUsage["cursor"];
|
|
504
|
-
const
|
|
505
|
+
const cursorEstimated = !!(cursorStats && (cursorStats.total_sessions ?? 0) >= CURSOR_MIN_SESSIONS);
|
|
506
|
+
let cursorTokens = 0;
|
|
505
507
|
for (const src of SOURCES) {
|
|
506
508
|
const stats = aiUsage[src];
|
|
507
509
|
if (!stats)
|
|
@@ -512,6 +514,8 @@ function computeTokens(aiUsage) {
|
|
|
512
514
|
const cache = cacheTotal(b);
|
|
513
515
|
workTokens += work;
|
|
514
516
|
cacheTokens += cache;
|
|
517
|
+
if (src === "cursor")
|
|
518
|
+
cursorTokens += work + cache;
|
|
515
519
|
// The per-tool bars + sparkline show the cache-inclusive total (matches the
|
|
516
520
|
// rainbow headline); the "generated" callout is the work-only number.
|
|
517
521
|
byTool[TOOL_LABELS[src] ?? src] =
|
|
@@ -570,7 +574,10 @@ function computeTokens(aiUsage) {
|
|
|
570
574
|
by_tool: sortedByTool,
|
|
571
575
|
six_month_buckets: sixMonth,
|
|
572
576
|
estimated_retail_cost_usd: Math.round(totalCost),
|
|
573
|
-
|
|
577
|
+
cursor_estimated: cursorEstimated,
|
|
578
|
+
cursor_token_share: cursorEstimated && workTokens + cacheTokens > 0
|
|
579
|
+
? cursorTokens / (workTokens + cacheTokens)
|
|
580
|
+
: 0,
|
|
574
581
|
};
|
|
575
582
|
}
|
|
576
583
|
function computeToolRelationship(aiUsage) {
|
package/dist/wrapped/render.js
CHANGED
|
@@ -453,9 +453,9 @@ function cardRankSummary(view) {
|
|
|
453
453
|
.filter((stat) => stat.topNumber !== null)
|
|
454
454
|
.sort((a, b) => a.topNumber - b.topNumber)[0];
|
|
455
455
|
const cohort = rank.sample_size > 0
|
|
456
|
-
? `across ${rank.sample_size.toLocaleString()}
|
|
456
|
+
? `across ${rank.sample_size.toLocaleString()} users`
|
|
457
457
|
: view.cohort_size > 0
|
|
458
|
-
? `among ${view.cohort_size.toLocaleString()}
|
|
458
|
+
? `among ${view.cohort_size.toLocaleString()} users so far`
|
|
459
459
|
: "Standout profile summary";
|
|
460
460
|
const row = (stat) => {
|
|
461
461
|
const top = "top" in stat && stat.top
|
|
@@ -517,6 +517,14 @@ function cardTokens(view) {
|
|
|
517
517
|
const costLine = cost > 0
|
|
518
518
|
? ` ${chalk.dim("would have cost")} ${TITLE_GRADIENT(`~$${cost.toLocaleString()}`)} ${chalk.dim("at retail API rates")}`
|
|
519
519
|
: "";
|
|
520
|
+
// Cursor stores no token counts locally, so its tokens + cost are estimated
|
|
521
|
+
// from active time. Disclose it — wording scales with how much of this card is
|
|
522
|
+
// Cursor (cursor-only users: the whole number is an estimate).
|
|
523
|
+
const cursorNote = t.cursor_estimated
|
|
524
|
+
? t.cursor_token_share >= 0.9
|
|
525
|
+
? chalk.dim(" ≈ estimated from time spent in Cursor (no local token data)")
|
|
526
|
+
: chalk.dim(" includes Cursor (tokens + cost estimated from active time)")
|
|
527
|
+
: "";
|
|
520
528
|
const cachedNote = t.cache_tokens > t.work_tokens
|
|
521
529
|
? chalk.dim(" thankfully, most of them were cached :)")
|
|
522
530
|
: t.cache_tokens > 0
|
|
@@ -533,12 +541,10 @@ function cardTokens(view) {
|
|
|
533
541
|
cachedNote,
|
|
534
542
|
generatedLine,
|
|
535
543
|
costLine,
|
|
544
|
+
cursorNote,
|
|
536
545
|
"",
|
|
537
546
|
" last 30 days, across:",
|
|
538
547
|
toolRows,
|
|
539
|
-
view.tokens.cursor_untracked
|
|
540
|
-
? chalk.dim(" Cursor used too (its tokens aren't tracked locally)")
|
|
541
|
-
: "",
|
|
542
548
|
"",
|
|
543
549
|
deltaLine,
|
|
544
550
|
]
|
|
@@ -875,9 +881,9 @@ export function cardProficiency(view) {
|
|
|
875
881
|
const scoreTop = topPercentNumber(p.score_percentile ?? null);
|
|
876
882
|
const sampleSize = view.rank_summary.sample_size;
|
|
877
883
|
const cohort = scoreTop != null && sampleSize > 0
|
|
878
|
-
? `top ${scoreTop}% of ${sampleSize.toLocaleString()}
|
|
884
|
+
? `top ${scoreTop}% of ${sampleSize.toLocaleString()} users`
|
|
879
885
|
: view.cohort_size > 0
|
|
880
|
-
? `among ${view.cohort_size.toLocaleString()}
|
|
886
|
+
? `among ${view.cohort_size.toLocaleString()} users so far`
|
|
881
887
|
: "";
|
|
882
888
|
const metricRow = (label, value) => value === null
|
|
883
889
|
? null
|
package/dist/wrapped/types.d.ts
CHANGED
|
@@ -115,7 +115,8 @@ export interface WrappedAggregateView {
|
|
|
115
115
|
tokens: number;
|
|
116
116
|
}[];
|
|
117
117
|
estimated_retail_cost_usd: number;
|
|
118
|
-
|
|
118
|
+
cursor_estimated: boolean;
|
|
119
|
+
cursor_token_share: number;
|
|
119
120
|
};
|
|
120
121
|
tool_relationship: {
|
|
121
122
|
kind: "switch";
|
package/dist/wrapped-client.js
CHANGED
|
@@ -10,18 +10,21 @@ const HEADERS = {
|
|
|
10
10
|
// node-fetch UAs on certain paths. Set a clear identity.
|
|
11
11
|
"User-Agent": "Standout/0.4.0 (node)",
|
|
12
12
|
};
|
|
13
|
-
// The request holds a connection open while the backend runs the wrapped's
|
|
14
|
-
// LLM cards, so a transient blip shouldn't permanently drop the wrapped.
|
|
15
13
|
const MAX_ATTEMPTS = 3;
|
|
16
|
-
//
|
|
17
|
-
// server
|
|
18
|
-
//
|
|
19
|
-
|
|
14
|
+
// The POST now returns as soon as the pending row is created (the LLM card deck
|
|
15
|
+
// runs server-side in the background), so it resolves in a couple seconds. The
|
|
16
|
+
// deck is then read by polling GET /wrapped/[id] — short requests that can't be
|
|
17
|
+
// silently dropped mid-generation the way the old long-held POST was.
|
|
18
|
+
const POST_TIMEOUT_MS = 60000;
|
|
19
|
+
const POLL_REQUEST_TIMEOUT_MS = 15000;
|
|
20
|
+
const POLL_INTERVAL_MS = 2000;
|
|
21
|
+
// Generous ceiling — the deck normally lands in well under a minute; this only
|
|
22
|
+
// bounds a stuck/never-completing background pass.
|
|
23
|
+
const POLL_TIMEOUT_MS = 180000;
|
|
20
24
|
const RETRY_BASE_DELAY_MS = 1000;
|
|
21
|
-
//
|
|
22
|
-
// retry failures that prove the request never reached the backend; a
|
|
23
|
-
//
|
|
24
|
-
// duplicate the wrapped.
|
|
25
|
+
// The POST is NOT idempotent — each one makes a new row + background LLM run. Only
|
|
26
|
+
// retry failures that prove the request never reached the backend; a mid-flight
|
|
27
|
+
// drop may mean the row was already created, so replaying would duplicate it.
|
|
25
28
|
const RETRYABLE_CONNECT_CODES = new Set([
|
|
26
29
|
"ECONNREFUSED",
|
|
27
30
|
"ENOTFOUND",
|
|
@@ -37,46 +40,109 @@ function isPreConnectError(err) {
|
|
|
37
40
|
}
|
|
38
41
|
return false;
|
|
39
42
|
}
|
|
43
|
+
// `fetch failed` is undici's catch-all message — the real reason (ECONNRESET,
|
|
44
|
+
// UND_ERR_HEADERS_TIMEOUT, …) lives in err.cause. Walk the chain so failures are
|
|
45
|
+
// actually diagnosable instead of all collapsing to "fetch failed".
|
|
46
|
+
function describeError(err) {
|
|
47
|
+
const parts = [];
|
|
48
|
+
let node = err;
|
|
49
|
+
for (let depth = 0; node && depth < 4; depth++) {
|
|
50
|
+
const e = node;
|
|
51
|
+
const code = typeof e.code === "string" ? e.code : null;
|
|
52
|
+
const message = typeof e.message === "string" ? e.message : null;
|
|
53
|
+
const label = [code, message].filter(Boolean).join(" ");
|
|
54
|
+
if (label && !parts.includes(label))
|
|
55
|
+
parts.push(label);
|
|
56
|
+
node = node?.cause;
|
|
57
|
+
}
|
|
58
|
+
return parts.join(" <- ") || String(err);
|
|
59
|
+
}
|
|
40
60
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
61
|
+
function isReady(computed) {
|
|
62
|
+
if (!computed)
|
|
63
|
+
return false;
|
|
64
|
+
// status is the explicit signal; archetype is a backstop for any response that
|
|
65
|
+
// predates the status field.
|
|
66
|
+
return computed.status === "ready" || computed.archetype != null;
|
|
67
|
+
}
|
|
68
|
+
// Poll GET /wrapped/[id] until the background card deck is persisted. Each request
|
|
69
|
+
// is short and idempotent, so transient blips are retried freely (unlike the POST).
|
|
70
|
+
async function pollComputed(id) {
|
|
71
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
72
|
+
// The deck never lands in under a second; wait once before the first read.
|
|
73
|
+
await sleep(POLL_INTERVAL_MS);
|
|
74
|
+
while (Date.now() < deadline) {
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch(`${STANDOUT_API_URL}/api/public/wrapped/${id}`, {
|
|
77
|
+
headers: HEADERS,
|
|
78
|
+
signal: AbortSignal.timeout(POLL_REQUEST_TIMEOUT_MS),
|
|
79
|
+
});
|
|
80
|
+
if (res.ok) {
|
|
81
|
+
const data = (await res.json());
|
|
82
|
+
if (isReady(data.computed))
|
|
83
|
+
return data.computed ?? null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Transient — keep polling until the deadline.
|
|
88
|
+
}
|
|
89
|
+
await sleep(POLL_INTERVAL_MS);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
41
93
|
export async function createWrapped(args) {
|
|
42
94
|
// Keep the request under Vercel's 4.5MB body cap (else a heavy history 413s
|
|
43
95
|
// before the handler runs). Trims the bulkiest raw fields only when oversized.
|
|
44
|
-
const { payload: profile } = capPayload(args.profile);
|
|
96
|
+
const { payload: profile, bytesAfter } = capPayload(args.profile);
|
|
45
97
|
const body = JSON.stringify({
|
|
46
98
|
profile,
|
|
47
99
|
submission_id: args.submissionId ?? undefined,
|
|
48
100
|
});
|
|
101
|
+
const bodyKb = Math.round(bytesAfter / 1024);
|
|
102
|
+
let created = null;
|
|
49
103
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
50
104
|
try {
|
|
51
105
|
const res = await fetch(`${STANDOUT_API_URL}/api/public/wrapped`, {
|
|
52
106
|
method: "POST",
|
|
53
107
|
headers: HEADERS,
|
|
54
108
|
body,
|
|
55
|
-
signal: AbortSignal.timeout(
|
|
109
|
+
signal: AbortSignal.timeout(POST_TIMEOUT_MS),
|
|
56
110
|
});
|
|
57
111
|
// Any HTTP response means the request reached the server; don't replay it
|
|
58
112
|
// (a 5xx could land after the row was already created).
|
|
59
113
|
if (!res.ok) {
|
|
60
114
|
const text = await res.text().catch(() => "");
|
|
61
|
-
process.stderr.write(`
|
|
115
|
+
process.stderr.write(` Standout had a server error (${res.status}) while building your wrapped. Please try again in a minute.\n` +
|
|
116
|
+
(text ? ` (details: ${text.slice(0, 200)})\n` : ""));
|
|
62
117
|
return null;
|
|
63
118
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
profile: args.profile,
|
|
67
|
-
computed: data.computed,
|
|
68
|
-
share_url: data.share_url,
|
|
69
|
-
});
|
|
70
|
-
return { id: data.id, share_url: data.share_url, view };
|
|
119
|
+
created = (await res.json());
|
|
120
|
+
break;
|
|
71
121
|
}
|
|
72
122
|
catch (err) {
|
|
73
123
|
if (isPreConnectError(err) && attempt < MAX_ATTEMPTS) {
|
|
74
124
|
await sleep(RETRY_BASE_DELAY_MS * attempt);
|
|
75
125
|
continue;
|
|
76
126
|
}
|
|
77
|
-
process.stderr.write(`
|
|
127
|
+
process.stderr.write(` Couldn't reach Standout to build your wrapped. Your network (or a VPN/proxy) dropped the connection.\n` +
|
|
128
|
+
` Check your connection and re-run: npx standout\n` +
|
|
129
|
+
` (details: ${describeError(err)}; body ${bodyKb}KB)\n`);
|
|
78
130
|
return null;
|
|
79
131
|
}
|
|
80
132
|
}
|
|
81
|
-
|
|
133
|
+
if (!created)
|
|
134
|
+
return null;
|
|
135
|
+
// The POST only created a pending row; poll for the finished deck.
|
|
136
|
+
const computed = await pollComputed(created.id);
|
|
137
|
+
if (!computed) {
|
|
138
|
+
process.stderr.write(" Your wrapped is taking longer than usual to finish generating.\n" +
|
|
139
|
+
" Re-run to try again: npx standout\n");
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const view = aggregateView({
|
|
143
|
+
profile: args.profile,
|
|
144
|
+
computed,
|
|
145
|
+
share_url: created.share_url,
|
|
146
|
+
});
|
|
147
|
+
return { id: created.id, share_url: created.share_url, view };
|
|
82
148
|
}
|