standout 0.5.26 → 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/payload.js +13 -0
- 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/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);
|
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
|
}
|