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 +8 -2
- package/dist/payload.js +13 -0
- package/dist/proxy.d.ts +1 -0
- package/dist/proxy.js +27 -0
- package/dist/wrapped-client.js +100 -22
- package/package.json +2 -1
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
|
|
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
|
|
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);
|
package/dist/proxy.d.ts
ADDED
|
@@ -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
|
+
}
|
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,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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|