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 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 open it manually: ${wrapped.share_url}\n`);
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 continuing profile setup.)\n\n");
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);
@@ -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
- // 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;
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
- // Wrapped creation is NOT idempotent — each POST makes a new row + LLM run. Only
22
- // retry failures that prove the request never reached the backend; a timeout or a
23
- // mid-flight drop may mean the server already created it, so replaying would
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(REQUEST_TIMEOUT_MS),
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(`[wrapped] backend returned ${res.status}${text ? `: ${text.slice(0, 200)}` : ""}\n`);
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
- const data = (await res.json());
65
- const view = aggregateView({
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(`[wrapped] failed to reach backend: ${err instanceof Error ? err.message : err}\n`);
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
- return null;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "standout",
3
- "version": "0.5.26",
3
+ "version": "0.5.27",
4
4
  "description": "Build your developer profile with AI. One command, zero friction.",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",