standout 0.5.26 → 0.5.28

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
@@ -5,6 +5,7 @@ import { createInterface } from "readline";
5
5
  import { markWrappedShared, STANDOUT_API_URL } from "./api.js";
6
6
  import { gatherFullEnrichment, gatherProfileBasics, gatherUsageStats, } from "./gather.js";
7
7
  import { startMcpServer } from "./mcp.js";
8
+ import { configureProxyFromEnv } from "./proxy.js";
8
9
  import { SYSTEM_PROMPT } from "./prompt.js";
9
10
  import { TOOLS, executeTool } from "./tools.js";
10
11
  import { renderWrappedAll } from "./wrapped/render.js";
@@ -119,7 +120,7 @@ async function maybeShareWrapped(wrapped) {
119
120
  process.stderr.write(` opened: ${wrapped.share_url}\n\n`);
120
121
  }
121
122
  catch (err) {
122
- process.stderr.write(` couldn't open browser open it manually: ${wrapped.share_url}\n`);
123
+ process.stderr.write(` couldn't open browser. open it manually: ${wrapped.share_url}\n`);
123
124
  process.stderr.write(` (${err instanceof Error ? err.message : err})\n\n`);
124
125
  }
125
126
  }
@@ -197,7 +198,7 @@ async function runAgent(jobId) {
197
198
  await maybeShareWrapped(wrapped);
198
199
  }
199
200
  else {
200
- process.stderr.write(" (couldn't generate wrapped right now continuing profile setup.)\n\n");
201
+ process.stderr.write(" (couldn't generate wrapped right now. continuing profile setup.)\n\n");
201
202
  }
202
203
  if (!profileChatEnabled()) {
203
204
  return;
@@ -326,6 +327,11 @@ async function runAgent(jobId) {
326
327
  }
327
328
  async function main() {
328
329
  const args = process.argv.slice(2);
330
+ // Route fetch through a configured proxy before any network call — Node's fetch
331
+ // ignores HTTP(S)_PROXY otherwise, which breaks users behind a corporate proxy.
332
+ const proxy = configureProxyFromEnv();
333
+ if (proxy)
334
+ process.stderr.write(` (using proxy ${proxy})\n`);
329
335
  if (args.includes("--gather")) {
330
336
  await confirmPrivacy();
331
337
  const data = await gatherFullEnrichment();
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);
@@ -0,0 +1 @@
1
+ export declare function configureProxyFromEnv(): string | null;
package/dist/proxy.js ADDED
@@ -0,0 +1,27 @@
1
+ import { ProxyAgent, setGlobalDispatcher } from "undici";
2
+ // Node's built-in fetch (undici) ignores HTTP(S)_PROXY env vars by default. On a
3
+ // corporate network that forces traffic through a proxy, undici then connects
4
+ // directly, the proxy/firewall transparently intercepts the TLS handshake, and the
5
+ // client sees a mangled stream (ERR_SSL_PACKET_LENGTH_TOO_LONG / "packet length too
6
+ // long"). Routing fetch through the configured proxy fixes that.
7
+ //
8
+ // Honors the standard env vars (upper + lower case). NO_PROXY is intentionally not
9
+ // handled — we only ever talk to one host (the Standout API), so a global proxy is
10
+ // correct when one is set.
11
+ export function configureProxyFromEnv() {
12
+ const proxy = process.env.HTTPS_PROXY ||
13
+ process.env.https_proxy ||
14
+ process.env.HTTP_PROXY ||
15
+ process.env.http_proxy ||
16
+ process.env.ALL_PROXY ||
17
+ process.env.all_proxy;
18
+ if (!proxy)
19
+ return null;
20
+ try {
21
+ setGlobalDispatcher(new ProxyAgent(proxy));
22
+ return proxy;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
@@ -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,121 @@ 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
+ const detail = describeError(err);
128
+ // A TLS-layer failure means something is intercepting HTTPS (VPN, proxy, or
129
+ // antivirus), not a transient drop — give targeted guidance.
130
+ const tlsIntercept = /ERR_SSL|ERR_TLS|packet length too long|SSL routines|wrong version number|DECRYPTION|sslv3|unknown ca|self.signed|unable to (verify|get local)/i.test(detail);
131
+ if (tlsIntercept) {
132
+ process.stderr.write(` Couldn't reach Standout: a VPN, proxy, or antivirus is intercepting the HTTPS connection.\n` +
133
+ ` Try one of: disconnect the VPN/proxy, switch networks, or set HTTPS_PROXY to your\n` +
134
+ ` corporate proxy (and NODE_EXTRA_CA_CERTS if it uses a custom certificate), then re-run: npx standout\n` +
135
+ ` (details: ${detail}; body ${bodyKb}KB)\n`);
136
+ }
137
+ else {
138
+ process.stderr.write(` Couldn't reach Standout to build your wrapped. Your network (or a VPN/proxy) dropped the connection.\n` +
139
+ ` Check your connection and re-run: npx standout\n` +
140
+ ` (details: ${detail}; body ${bodyKb}KB)\n`);
141
+ }
78
142
  return null;
79
143
  }
80
144
  }
81
- return null;
145
+ if (!created)
146
+ return null;
147
+ // The POST only created a pending row; poll for the finished deck.
148
+ const computed = await pollComputed(created.id);
149
+ if (!computed) {
150
+ process.stderr.write(" Your wrapped is taking longer than usual to finish generating.\n" +
151
+ " Re-run to try again: npx standout\n");
152
+ return null;
153
+ }
154
+ const view = aggregateView({
155
+ profile: args.profile,
156
+ computed,
157
+ share_url: created.share_url,
158
+ });
159
+ return { id: created.id, share_url: created.share_url, view };
82
160
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "standout",
3
- "version": "0.5.26",
3
+ "version": "0.5.28",
4
4
  "description": "Build your developer profile with AI. One command, zero friction.",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",
@@ -32,6 +32,7 @@
32
32
  "gradient-string": "^3.0.0",
33
33
  "open": "^11.0.0",
34
34
  "playwright-core": "^1.59.1",
35
+ "undici": "^6.26.0",
35
36
  "zod": "^3.24.0"
36
37
  },
37
38
  "devDependencies": {