standout 0.5.21 → 0.5.23
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/api.js +9 -1
- package/dist/cli.js +2 -2
- package/dist/payload.d.ts +8 -0
- package/dist/payload.js +150 -0
- package/dist/wrapped/aggregate.js +3 -0
- package/dist/wrapped/preview.js +3 -0
- package/dist/wrapped/render.js +20 -2
- package/dist/wrapped/types.d.ts +3 -0
- package/dist/wrapped-client.js +9 -3
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { capPayload } from "./payload.js";
|
|
1
2
|
export const STANDOUT_API_URL = process.env.STANDOUT_API_URL || "https://standout.work";
|
|
3
|
+
// Matches the server's request budget so the client doesn't hang forever (the
|
|
4
|
+
// submit handler has no client timeout otherwise) nor abort a still-valid run.
|
|
5
|
+
const SUBMIT_TIMEOUT_MS = 300000;
|
|
2
6
|
export async function enrichProfile(type, value) {
|
|
3
7
|
try {
|
|
4
8
|
const res = await fetch(`${STANDOUT_API_URL}/api/public/agent-enrich`, {
|
|
@@ -65,10 +69,14 @@ export async function submitProfile(profile, options = {}) {
|
|
|
65
69
|
const jobId = process.env.STANDOUT_JOB_ID;
|
|
66
70
|
const mergedProfile = mergeSubmissionProfile(profileData, options.prefetchedProfile);
|
|
67
71
|
const payload = jobId ? { ...mergedProfile, jobId } : mergedProfile;
|
|
72
|
+
// Trim under Vercel's 4.5MB body cap so a heavy history can't 413 the core
|
|
73
|
+
// submission before the handler runs.
|
|
74
|
+
const { payload: capped } = capPayload(payload);
|
|
68
75
|
const res = await fetch(`${STANDOUT_API_URL}/api/public/agent-submit`, {
|
|
69
76
|
method: "POST",
|
|
70
77
|
headers: { "Content-Type": "application/json" },
|
|
71
|
-
body: JSON.stringify(
|
|
78
|
+
body: JSON.stringify(capped),
|
|
79
|
+
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
72
80
|
});
|
|
73
81
|
if (!res.ok) {
|
|
74
82
|
const error = await res.text();
|
package/dist/cli.js
CHANGED
|
@@ -148,7 +148,7 @@ async function runAgent(jobId) {
|
|
|
148
148
|
},
|
|
149
149
|
});
|
|
150
150
|
await confirmPrivacy();
|
|
151
|
-
const stopInitialLoading = startLoadingLine("
|
|
151
|
+
const stopInitialLoading = startLoadingLine("Putting together your AI highlights");
|
|
152
152
|
const usagePromise = gatherUsageStats();
|
|
153
153
|
const basicsPromise = gatherProfileBasics();
|
|
154
154
|
let usageStats;
|
|
@@ -182,7 +182,7 @@ async function runAgent(jobId) {
|
|
|
182
182
|
});
|
|
183
183
|
// Generate EVERYTHING up front (gather + all the server LLM cards) under one
|
|
184
184
|
// spinner, then page the whole deck at once — no two-pass preview/finale split.
|
|
185
|
-
const stopWrappedLoading = startLoadingLine("
|
|
185
|
+
const stopWrappedLoading = startLoadingLine("Putting together your AI highlights");
|
|
186
186
|
let wrapped;
|
|
187
187
|
let prefetched;
|
|
188
188
|
try {
|
package/dist/payload.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Shapes the outbound profile so a heavy history can't break the request.
|
|
2
|
+
//
|
|
3
|
+
// The bulk is the raw `exchanges` corpus (user<->assistant pairs): up to 500 per
|
|
4
|
+
// tool x ~2.8KB, so a 3-tool user ships ~4.2MB. Two problems with sending it raw:
|
|
5
|
+
// 1. The wrapped cards that consume it (collaboration -> 500, craft -> 50) merge
|
|
6
|
+
// across tools then stride-sample DOWN, so anything past 500 total is
|
|
7
|
+
// discarded server-side — pure waste.
|
|
8
|
+
// 2. Vercel rejects a >4.5MB function body with a 413 BEFORE our handler runs,
|
|
9
|
+
// killing both the wrapped and the core submission.
|
|
10
|
+
//
|
|
11
|
+
// So we (a) always bound combined exchanges to what the cards actually read, and
|
|
12
|
+
// (b) keep a hard byte backstop that sheds the least-valuable raw fields first.
|
|
13
|
+
const MAX_BODY_BYTES = 4_000_000; // headroom under Vercel's 4.5MB hard limit
|
|
14
|
+
// The most any wrapped card consumes (collaboration's ceiling; craft reads 50).
|
|
15
|
+
// Capping here is lossless for card resolution — the server would sample to this
|
|
16
|
+
// anyway — it only changes WHICH evenly-strided subset is read.
|
|
17
|
+
const MAX_UPLOAD_EXCHANGES = 500;
|
|
18
|
+
const TOOLS = ["claude_code", "codex", "cursor"];
|
|
19
|
+
function byteLength(value) {
|
|
20
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
21
|
+
}
|
|
22
|
+
function asRecord(value) {
|
|
23
|
+
return value && typeof value === "object"
|
|
24
|
+
? value
|
|
25
|
+
: undefined;
|
|
26
|
+
}
|
|
27
|
+
function exchangesOf(aiUsage, tool) {
|
|
28
|
+
const ex = asRecord(aiUsage[tool])?.exchanges;
|
|
29
|
+
return Array.isArray(ex) ? ex : [];
|
|
30
|
+
}
|
|
31
|
+
function totalExchanges(payload) {
|
|
32
|
+
const aiUsage = asRecord(payload.ai_usage);
|
|
33
|
+
if (!aiUsage)
|
|
34
|
+
return 0;
|
|
35
|
+
return TOOLS.reduce((n, t) => n + exchangesOf(aiUsage, t).length, 0);
|
|
36
|
+
}
|
|
37
|
+
// Evenly stride-sample an array down to `keep` items (representative, not
|
|
38
|
+
// recency-biased — matches how the server reads the corpus).
|
|
39
|
+
function evenSample(arr, keep) {
|
|
40
|
+
if (arr.length <= keep)
|
|
41
|
+
return arr;
|
|
42
|
+
const stride = arr.length / keep;
|
|
43
|
+
const out = [];
|
|
44
|
+
for (let i = 0; i < keep; i++)
|
|
45
|
+
out.push(arr[Math.floor(i * stride)]);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
// Bound combined exchanges to `maxTotal`, proportional per tool, even-sampled.
|
|
49
|
+
function capExchangesInPlace(payload, maxTotal) {
|
|
50
|
+
const aiUsage = asRecord(payload.ai_usage);
|
|
51
|
+
if (!aiUsage)
|
|
52
|
+
return;
|
|
53
|
+
const present = TOOLS.map((t) => ({
|
|
54
|
+
t,
|
|
55
|
+
n: exchangesOf(aiUsage, t).length,
|
|
56
|
+
})).filter((x) => x.n > 0);
|
|
57
|
+
const total = present.reduce((s, x) => s + x.n, 0);
|
|
58
|
+
if (total <= maxTotal)
|
|
59
|
+
return;
|
|
60
|
+
for (const { t, n } of present) {
|
|
61
|
+
const keep = Math.max(1, Math.round((maxTotal * n) / total));
|
|
62
|
+
const tool = asRecord(aiUsage[t]);
|
|
63
|
+
if (tool)
|
|
64
|
+
tool.exchanges = evenSample(tool.exchanges, keep);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Returns a (possibly reshaped) clone — never mutates the caller's object.
|
|
68
|
+
export function capPayload(payload) {
|
|
69
|
+
const bytesBefore = byteLength(payload);
|
|
70
|
+
const dropped = [];
|
|
71
|
+
// Only clone (and pay the cost) if there's actually something to do.
|
|
72
|
+
const needsExchangeCap = totalExchanges(payload) > MAX_UPLOAD_EXCHANGES;
|
|
73
|
+
if (!needsExchangeCap && bytesBefore <= MAX_BODY_BYTES) {
|
|
74
|
+
return {
|
|
75
|
+
payload,
|
|
76
|
+
trimmed: false,
|
|
77
|
+
bytesBefore,
|
|
78
|
+
bytesAfter: bytesBefore,
|
|
79
|
+
dropped: [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const p = structuredClone(payload);
|
|
83
|
+
const aiUsage = asRecord(p.ai_usage) ?? {};
|
|
84
|
+
// 1. Always: bound exchanges to what the cards read (lossless for resolution).
|
|
85
|
+
if (needsExchangeCap) {
|
|
86
|
+
capExchangesInPlace(p, MAX_UPLOAD_EXCHANGES);
|
|
87
|
+
dropped.push(`exchanges->${MAX_UPLOAD_EXCHANGES}`);
|
|
88
|
+
}
|
|
89
|
+
// Hard byte backstop — only if still oversized after the exchange cap (rare).
|
|
90
|
+
// Shed least-valuable raw fields FIRST so the analyzed exchanges are the last
|
|
91
|
+
// thing we touch.
|
|
92
|
+
const over = () => byteLength(p) > MAX_BODY_BYTES;
|
|
93
|
+
// 2. Display-only conversation samples.
|
|
94
|
+
if (over()) {
|
|
95
|
+
for (const t of TOOLS) {
|
|
96
|
+
const tool = asRecord(aiUsage[t]);
|
|
97
|
+
if (tool &&
|
|
98
|
+
Array.isArray(tool.conversation_samples) &&
|
|
99
|
+
tool.conversation_samples.length) {
|
|
100
|
+
tool.conversation_samples = [];
|
|
101
|
+
if (!dropped.includes("conversation_samples")) {
|
|
102
|
+
dropped.push("conversation_samples");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// 3. Commit subjects + repo README excerpts (contributions/blurbs inputs).
|
|
108
|
+
if (over()) {
|
|
109
|
+
const aiCoding = asRecord(p.ai_coding);
|
|
110
|
+
if (aiCoding &&
|
|
111
|
+
Array.isArray(aiCoding.commit_subjects) &&
|
|
112
|
+
aiCoding.commit_subjects.length) {
|
|
113
|
+
aiCoding.commit_subjects = [];
|
|
114
|
+
dropped.push("commit_subjects");
|
|
115
|
+
}
|
|
116
|
+
const local = asRecord(p.local);
|
|
117
|
+
if (local && Array.isArray(local.repos)) {
|
|
118
|
+
let touched = false;
|
|
119
|
+
for (const repo of local.repos) {
|
|
120
|
+
const r = asRecord(repo);
|
|
121
|
+
if (r && r.readme_excerpt) {
|
|
122
|
+
r.readme_excerpt = null;
|
|
123
|
+
touched = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (touched)
|
|
127
|
+
dropped.push("readme_excerpts");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// 4. Last resort: shrink exchanges further (halve, keeping recent) until it
|
|
131
|
+
// fits. Only reached for pathologically large non-exchange data.
|
|
132
|
+
while (over() && totalExchanges(p) > 0) {
|
|
133
|
+
for (const t of TOOLS) {
|
|
134
|
+
const tool = asRecord(aiUsage[t]);
|
|
135
|
+
const ex = tool?.exchanges;
|
|
136
|
+
if (tool && Array.isArray(ex) && ex.length > 0) {
|
|
137
|
+
tool.exchanges = ex.slice(Math.ceil(ex.length / 2));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!dropped.includes("exchanges:backstop"))
|
|
141
|
+
dropped.push("exchanges:backstop");
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
payload: p,
|
|
145
|
+
trimmed: true,
|
|
146
|
+
bytesBefore,
|
|
147
|
+
bytesAfter: byteLength(p),
|
|
148
|
+
dropped,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -466,6 +466,9 @@ export function aggregateView(args) {
|
|
|
466
466
|
active_days_percentile: rankSummary.active_days_percentile ?? null,
|
|
467
467
|
tokens_percentile: rankSummary.tokens_percentile ?? null,
|
|
468
468
|
commit_days_percentile: rankSummary.commit_days_percentile ?? null,
|
|
469
|
+
total_tokens_percentile: rankSummary.total_tokens_percentile ?? null,
|
|
470
|
+
avg_agents_percentile: rankSummary.avg_agents_percentile ?? null,
|
|
471
|
+
weekend_pct_percentile: rankSummary.weekend_pct_percentile ?? null,
|
|
469
472
|
},
|
|
470
473
|
proficiency: parseProficiency(computedObj.proficiency),
|
|
471
474
|
tokens,
|
package/dist/wrapped/preview.js
CHANGED
|
@@ -73,6 +73,9 @@ function fixture(s) {
|
|
|
73
73
|
active_days_percentile: s.consistency,
|
|
74
74
|
tokens_percentile: s.intensity,
|
|
75
75
|
commit_days_percentile: s.intensity,
|
|
76
|
+
total_tokens_percentile: s.intensity,
|
|
77
|
+
avg_agents_percentile: s.intensity,
|
|
78
|
+
weekend_pct_percentile: s.consistency,
|
|
76
79
|
},
|
|
77
80
|
proficiency: {
|
|
78
81
|
score: s.score,
|
package/dist/wrapped/render.js
CHANGED
|
@@ -200,10 +200,12 @@ function card1Open(view) {
|
|
|
200
200
|
: perDay >= 1
|
|
201
201
|
? "a daily habit at this point."
|
|
202
202
|
: "just getting warmed up.";
|
|
203
|
+
const badge = benchmarkBadge(view, view.rank_summary.hours_percentile, "of devs by session-hours");
|
|
203
204
|
const lines = [
|
|
204
205
|
chalk.dim("YOU MANAGED"),
|
|
205
206
|
"",
|
|
206
207
|
bigNumber(` ${hours} session-hours`),
|
|
208
|
+
...(badge ? ["", badge] : []),
|
|
207
209
|
"",
|
|
208
210
|
chalk.white(` ${verdict}`),
|
|
209
211
|
chalk.dim(" across parallel agent sessions, last 30 days"),
|
|
@@ -259,6 +261,7 @@ function cardRhythm(view) {
|
|
|
259
261
|
: "no clear peak";
|
|
260
262
|
const weekendPct = Math.round(view.weekend_ratio * 100);
|
|
261
263
|
const weekdayPct = 100 - weekendPct;
|
|
264
|
+
const weekendBadge = benchmarkBadge(view, view.rank_summary.weekend_pct_percentile, "of devs by weekend coding");
|
|
262
265
|
const isNightOwl = critter?.key === "night_owl";
|
|
263
266
|
const subhead = isNightOwl
|
|
264
267
|
? "while the 9-5ers sleep, you ship."
|
|
@@ -278,6 +281,7 @@ function cardRhythm(view) {
|
|
|
278
281
|
"",
|
|
279
282
|
` ${chalk.white("weekdays".padEnd(9))} ${bar(weekdayPct, 100, 20)} ${chalk.dim(weekdayPct + "%")}`,
|
|
280
283
|
` ${chalk.white("weekends".padEnd(9))} ${bar(weekendPct, 100, 20)} ${chalk.dim(weekendPct + "%")}`,
|
|
284
|
+
...(weekendBadge ? ["", weekendBadge] : []),
|
|
281
285
|
"",
|
|
282
286
|
];
|
|
283
287
|
if (view.rhythm_comment) {
|
|
@@ -395,6 +399,17 @@ function topPercent(percentile) {
|
|
|
395
399
|
const top = topPercentNumber(percentile);
|
|
396
400
|
return top === null ? null : `top ${top}%`;
|
|
397
401
|
}
|
|
402
|
+
// Inline "top X%" benchmark for a single metric card, gated on the same 100-user
|
|
403
|
+
// cohort that unlocks the rest of the rankings. Returns null (line omitted) for
|
|
404
|
+
// early adopters or until the metric has a percentile.
|
|
405
|
+
function benchmarkBadge(view, percentile, dimension) {
|
|
406
|
+
if (view.early_adopter)
|
|
407
|
+
return null;
|
|
408
|
+
const top = topPercentNumber(percentile);
|
|
409
|
+
if (top === null)
|
|
410
|
+
return null;
|
|
411
|
+
return ` ${chalk.hex("#ff8a00")(`top ${top}%`)} ${chalk.dim(dimension)}`;
|
|
412
|
+
}
|
|
398
413
|
function cardRankSummary(view) {
|
|
399
414
|
if (!usageReady(view) || view.total_sessions <= 0)
|
|
400
415
|
return "";
|
|
@@ -503,6 +518,7 @@ function cardTokens(view) {
|
|
|
503
518
|
chalk.dim("YOU BURNED THIS MANY TOKENS"),
|
|
504
519
|
"",
|
|
505
520
|
` ${totalLine}`,
|
|
521
|
+
benchmarkBadge(view, view.rank_summary.total_tokens_percentile, "of devs by tokens") ?? "",
|
|
506
522
|
"",
|
|
507
523
|
cachedNote,
|
|
508
524
|
generatedLine,
|
|
@@ -738,10 +754,12 @@ function cardConcurrency(view) {
|
|
|
738
754
|
const longest = days >= 1.5
|
|
739
755
|
? `${days.toFixed(0)} days`
|
|
740
756
|
: `${Math.round(c.longest_session_hours)}h`;
|
|
757
|
+
const badge = benchmarkBadge(view, view.rank_summary.avg_agents_percentile, "of devs by agents juggled");
|
|
741
758
|
const lines = [
|
|
742
759
|
chalk.dim("HOW MANY AGENTS YOU JUGGLE"),
|
|
743
760
|
"",
|
|
744
761
|
` ${bigNumber(`~${c.open_tab_avg}`)} ${chalk.white("agents open at once")}`,
|
|
762
|
+
...(badge ? ["", badge] : []),
|
|
745
763
|
"",
|
|
746
764
|
` ${chalk.white("peak".padEnd(14))} ${chalk.dim(`${c.open_tab_peak} at once`)}`,
|
|
747
765
|
` ${chalk.white("longest tab".padEnd(14))} ${chalk.dim(`${longest} open`)}`,
|
|
@@ -937,7 +955,7 @@ export async function renderWrappedPreview(view) {
|
|
|
937
955
|
// CLI runs that separately. Returns whether the user quit early.
|
|
938
956
|
export async function renderWrappedAll(view) {
|
|
939
957
|
process.stdout.write("\n");
|
|
940
|
-
process.stdout.write(" " + TITLE_GRADIENT.multiline("░▒▓
|
|
958
|
+
process.stdout.write(" " + TITLE_GRADIENT.multiline("░▒▓ YOUR AI WRAPPED ▓▒░") + "\n");
|
|
941
959
|
process.stdout.write(chalk.dim(` ${view.name.toLowerCase()} · last 30 days\n\n`));
|
|
942
960
|
const cards = buildRenderableCards(view, "final");
|
|
943
961
|
if (cards.length === 0) {
|
|
@@ -958,7 +976,7 @@ export async function renderWrappedAll(view) {
|
|
|
958
976
|
async function renderWrappedCards(view, mode) {
|
|
959
977
|
// Title banner
|
|
960
978
|
process.stdout.write("\n");
|
|
961
|
-
process.stdout.write(" " + TITLE_GRADIENT.multiline("░▒▓
|
|
979
|
+
process.stdout.write(" " + TITLE_GRADIENT.multiline("░▒▓ YOUR AI WRAPPED ▓▒░") + "\n");
|
|
962
980
|
process.stdout.write(chalk.dim(` ${view.name.toLowerCase()} · last 30 days\n\n`));
|
|
963
981
|
// Render each card; cards that return empty string get skipped (e.g. tool
|
|
964
982
|
// relationship when there's not enough data, models when no model captured).
|
package/dist/wrapped/types.d.ts
CHANGED
|
@@ -94,6 +94,9 @@ export interface WrappedAggregateView {
|
|
|
94
94
|
active_days_percentile: number | null;
|
|
95
95
|
tokens_percentile: number | null;
|
|
96
96
|
commit_days_percentile: number | null;
|
|
97
|
+
total_tokens_percentile: number | null;
|
|
98
|
+
avg_agents_percentile: number | null;
|
|
99
|
+
weekend_pct_percentile: number | null;
|
|
97
100
|
};
|
|
98
101
|
proficiency: ProficiencyView | null;
|
|
99
102
|
tokens: {
|
package/dist/wrapped-client.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// "version" in package.json in the same PR — that bump is the ONLY trigger for the
|
|
3
3
|
// npm publish on merge. Land a change without it and the fix never reaches users.
|
|
4
4
|
import { STANDOUT_API_URL } from "./api.js";
|
|
5
|
+
import { capPayload } from "./payload.js";
|
|
5
6
|
import { aggregateView } from "./wrapped/aggregate.js";
|
|
6
7
|
const HEADERS = {
|
|
7
8
|
"Content-Type": "application/json",
|
|
@@ -12,8 +13,10 @@ const HEADERS = {
|
|
|
12
13
|
// The request holds a connection open while the backend runs the wrapped's
|
|
13
14
|
// LLM cards, so a transient blip shouldn't permanently drop the wrapped.
|
|
14
15
|
const MAX_ATTEMPTS = 3;
|
|
15
|
-
//
|
|
16
|
-
|
|
16
|
+
// Matches the server route's 300s maxDuration: the client only gives up once the
|
|
17
|
+
// server itself would have. A shorter client timeout would abort a still-valid
|
|
18
|
+
// slow run and orphan the wrapped the server goes on to create.
|
|
19
|
+
const REQUEST_TIMEOUT_MS = 300000;
|
|
17
20
|
const RETRY_BASE_DELAY_MS = 1000;
|
|
18
21
|
// Wrapped creation is NOT idempotent — each POST makes a new row + LLM run. Only
|
|
19
22
|
// retry failures that prove the request never reached the backend; a timeout or a
|
|
@@ -36,8 +39,11 @@ function isPreConnectError(err) {
|
|
|
36
39
|
}
|
|
37
40
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
38
41
|
export async function createWrapped(args) {
|
|
42
|
+
// Keep the request under Vercel's 4.5MB body cap (else a heavy history 413s
|
|
43
|
+
// before the handler runs). Trims the bulkiest raw fields only when oversized.
|
|
44
|
+
const { payload: profile } = capPayload(args.profile);
|
|
39
45
|
const body = JSON.stringify({
|
|
40
|
-
profile
|
|
46
|
+
profile,
|
|
41
47
|
submission_id: args.submissionId ?? undefined,
|
|
42
48
|
});
|
|
43
49
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|