standout 0.5.31 → 0.5.32
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 +223 -10
- package/dist/gather.d.ts +1 -0
- package/dist/gather.js +30 -18
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import Anthropic from "@anthropic-ai/sdk";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
+
import http from "node:http";
|
|
4
5
|
import { createInterface } from "readline";
|
|
5
6
|
import { markWrappedShared, STANDOUT_API_URL } from "./api.js";
|
|
7
|
+
import { capPayload } from "./payload.js";
|
|
6
8
|
import { gatherFullEnrichment, gatherProfileBasics, gatherUsageStats, } from "./gather.js";
|
|
7
9
|
import { ensureHeapHeadroom } from "./heap.js";
|
|
8
10
|
import { startMcpServer } from "./mcp.js";
|
|
9
11
|
import { configureProxyFromEnv } from "./proxy.js";
|
|
10
12
|
import { SYSTEM_PROMPT } from "./prompt.js";
|
|
11
13
|
import { TOOLS, executeTool } from "./tools.js";
|
|
14
|
+
import { aggregateView } from "./wrapped/aggregate.js";
|
|
12
15
|
import { renderWrappedAll } from "./wrapped/render.js";
|
|
13
16
|
import { createWrapped } from "./wrapped-client.js";
|
|
14
17
|
import { openWrapped } from "./wrapped-share.js";
|
|
@@ -23,6 +26,114 @@ function profileChatEnabled() {
|
|
|
23
26
|
return true;
|
|
24
27
|
return process.argv.slice(2).includes("--chat");
|
|
25
28
|
}
|
|
29
|
+
// Local-only mode: compute and render the wrapped entirely on this machine. No
|
|
30
|
+
// profile is uploaded, no wrapped row is created, no leaderboard entry is made,
|
|
31
|
+
// and gathering skips every network call. Enable with --local / --no-upload or
|
|
32
|
+
// STANDOUT_LOCAL=1.
|
|
33
|
+
function localOnlyEnabled() {
|
|
34
|
+
const env = process.env.STANDOUT_LOCAL;
|
|
35
|
+
if (env === "1" || env === "true")
|
|
36
|
+
return true;
|
|
37
|
+
const args = process.argv.slice(2);
|
|
38
|
+
return args.includes("--local") || args.includes("--no-upload");
|
|
39
|
+
}
|
|
40
|
+
function printBanner() {
|
|
41
|
+
console.log("\n ┌────────────────────────────────┐");
|
|
42
|
+
console.log(" │ are you really tokenmaxxing? │");
|
|
43
|
+
console.log(" │ Powered by Standout (YC P26) │");
|
|
44
|
+
console.log(" └────────────────────────────────┘\n");
|
|
45
|
+
}
|
|
46
|
+
// First step of the default flow: an arrow-key menu to pick full (cloud) vs
|
|
47
|
+
// local-only. Non-interactive shells (CI, agents) can't answer, so they keep the
|
|
48
|
+
// full flow — pass --local / STANDOUT_LOCAL=1 to force local without a prompt.
|
|
49
|
+
const MODE_OPTIONS = [
|
|
50
|
+
{
|
|
51
|
+
key: "full",
|
|
52
|
+
title: "Full",
|
|
53
|
+
tag: " (recommended)",
|
|
54
|
+
lines: [
|
|
55
|
+
"Your full AI wrapped: LLM-written cards, your score, and a leaderboard rank.",
|
|
56
|
+
"Reads your local stats plus your public GitHub / LinkedIn / X.",
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
key: "local",
|
|
61
|
+
title: "Local only",
|
|
62
|
+
tag: " — private & offline",
|
|
63
|
+
lines: [
|
|
64
|
+
"Stays entirely on your computer. Nothing uploaded, nothing saved.",
|
|
65
|
+
"Just your local AI-tool stats — no LLM cards, score, or ranking.",
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
function renderModeMenu(selected) {
|
|
70
|
+
const out = [
|
|
71
|
+
"",
|
|
72
|
+
chalk.white(" How do you want to run Standout?"),
|
|
73
|
+
"",
|
|
74
|
+
];
|
|
75
|
+
MODE_OPTIONS.forEach((opt, i) => {
|
|
76
|
+
const active = i === selected;
|
|
77
|
+
const pointer = active ? chalk.hex("#ad5cff")("❯ ") : " ";
|
|
78
|
+
const title = active
|
|
79
|
+
? chalk.bold.white(opt.title) + chalk.dim(opt.tag)
|
|
80
|
+
: chalk.dim(opt.title + opt.tag);
|
|
81
|
+
out.push(`${pointer}${title}`);
|
|
82
|
+
for (const line of opt.lines)
|
|
83
|
+
out.push(chalk.dim(` ${line}`));
|
|
84
|
+
out.push("");
|
|
85
|
+
});
|
|
86
|
+
out.push(chalk.dim(" ↑/↓ to move · enter to select"));
|
|
87
|
+
return out.join("\n");
|
|
88
|
+
}
|
|
89
|
+
async function chooseMode() {
|
|
90
|
+
if (!process.stdin.isTTY)
|
|
91
|
+
return "full";
|
|
92
|
+
let selected = 0;
|
|
93
|
+
const draw = () => {
|
|
94
|
+
const text = renderModeMenu(selected);
|
|
95
|
+
process.stderr.write(text + "\n");
|
|
96
|
+
return text.split("\n").length;
|
|
97
|
+
};
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const stdin = process.stdin;
|
|
100
|
+
stdin.setRawMode(true);
|
|
101
|
+
stdin.resume();
|
|
102
|
+
stdin.setEncoding("utf8");
|
|
103
|
+
let lines = draw();
|
|
104
|
+
const redraw = () => {
|
|
105
|
+
// Jump back to the menu's first line and clear it before redrawing.
|
|
106
|
+
process.stderr.write(`\x1b[${lines}A\x1b[0J`);
|
|
107
|
+
lines = draw();
|
|
108
|
+
};
|
|
109
|
+
const cleanup = () => {
|
|
110
|
+
stdin.setRawMode(false);
|
|
111
|
+
stdin.pause();
|
|
112
|
+
stdin.removeListener("data", onData);
|
|
113
|
+
};
|
|
114
|
+
const onData = (data) => {
|
|
115
|
+
if (data === "\x03") {
|
|
116
|
+
cleanup();
|
|
117
|
+
process.stderr.write("\n Cancelled.\n");
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
else if (data === "\r" || data === "\n") {
|
|
121
|
+
cleanup();
|
|
122
|
+
process.stderr.write("\n");
|
|
123
|
+
resolve(MODE_OPTIONS[selected].key);
|
|
124
|
+
}
|
|
125
|
+
else if (data === "\x1b[A" || data === "k") {
|
|
126
|
+
selected = (selected + MODE_OPTIONS.length - 1) % MODE_OPTIONS.length;
|
|
127
|
+
redraw();
|
|
128
|
+
}
|
|
129
|
+
else if (data === "\x1b[B" || data === "j") {
|
|
130
|
+
selected = (selected + 1) % MODE_OPTIONS.length;
|
|
131
|
+
redraw();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
stdin.on("data", onData);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
26
137
|
async function confirmPrivacy() {
|
|
27
138
|
process.stderr.write("\nDiscover your Claude Code, Codex, and Cursor stats, then see how you rank.\n" +
|
|
28
139
|
"You'll see your cards first, then review before submitting.\n\n" +
|
|
@@ -125,6 +236,94 @@ async function maybeShareWrapped(wrapped) {
|
|
|
125
236
|
process.stderr.write(` (${err instanceof Error ? err.message : err})\n\n`);
|
|
126
237
|
}
|
|
127
238
|
}
|
|
239
|
+
// Fully offline path: gather only local signals and render the wrapped deck in
|
|
240
|
+
// the terminal without ever contacting Standout. Reads only local fields, so the
|
|
241
|
+
// deck looks the same minus the server-computed bits — the LLM-written
|
|
242
|
+
// archetype/zinger (sensible defaults are used) and the leaderboard
|
|
243
|
+
// rank/percentiles (omitted, since those need the server cohort).
|
|
244
|
+
async function renderLocalTerminal(prefetched) {
|
|
245
|
+
const view = aggregateView({
|
|
246
|
+
profile: prefetched,
|
|
247
|
+
computed: {},
|
|
248
|
+
share_url: "",
|
|
249
|
+
});
|
|
250
|
+
await renderWrappedAll(view);
|
|
251
|
+
}
|
|
252
|
+
// Local path: gather on-device, then open the real wrapped UI on localhost. The
|
|
253
|
+
// gathered profile is held only in this process and served from an ephemeral
|
|
254
|
+
// in-memory HTTP server — nothing is written to disk, no DB row, no leaderboard
|
|
255
|
+
// entry. The page renders the card image via a stateless endpoint (also not
|
|
256
|
+
// stored) and shows the share options. `--terminal` forces the fully-offline
|
|
257
|
+
// ASCII deck instead.
|
|
258
|
+
async function runLocal() {
|
|
259
|
+
const terminalOnly = process.argv.slice(2).includes("--terminal");
|
|
260
|
+
const stop = startLoadingLine("Putting together your AI highlights");
|
|
261
|
+
let prefetched;
|
|
262
|
+
try {
|
|
263
|
+
prefetched = await gatherFullEnrichment({
|
|
264
|
+
localOnly: true,
|
|
265
|
+
verbose: false,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
stop();
|
|
270
|
+
}
|
|
271
|
+
// Always page the deck in the terminal first.
|
|
272
|
+
await renderLocalTerminal(prefetched);
|
|
273
|
+
if (terminalOnly) {
|
|
274
|
+
process.stderr.write("\n Local run complete. To publish your wrapped and see how you rank: npx standout\n\n");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// Then open the full wrapped (card image + share options) on localhost.
|
|
278
|
+
// Trim the bulkiest raw fields so the in-memory body stays well under the
|
|
279
|
+
// render endpoint's request cap, same as the upload path.
|
|
280
|
+
const { payload } = capPayload(prefetched);
|
|
281
|
+
const body = JSON.stringify(payload);
|
|
282
|
+
const server = http.createServer((req, res) => {
|
|
283
|
+
// The wrapped page (https) reads this over http://localhost, which browsers
|
|
284
|
+
// treat as a trustworthy origin. Allow any origin for the simple GET.
|
|
285
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
286
|
+
if (req.url && req.url.startsWith("/profile.json")) {
|
|
287
|
+
res.setHeader("Content-Type", "application/json");
|
|
288
|
+
res.end(body);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
res.statusCode = 404;
|
|
292
|
+
res.end("not found");
|
|
293
|
+
});
|
|
294
|
+
await new Promise((resolve, reject) => {
|
|
295
|
+
server.on("error", reject);
|
|
296
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
297
|
+
});
|
|
298
|
+
const address = server.address();
|
|
299
|
+
const port = address && typeof address === "object" ? address.port : null;
|
|
300
|
+
if (!port) {
|
|
301
|
+
server.close();
|
|
302
|
+
process.stderr.write("\n Couldn't start the local server, so the browser view is unavailable.\n" +
|
|
303
|
+
" Your wrapped is shown above. Re-run to try the browser view: npx standout --local\n\n");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const url = `${STANDOUT_API_URL}/wrapped/live?port=${port}`;
|
|
307
|
+
process.stderr.write(`\n Your wrapped is ready — nothing was uploaded or saved.\n` +
|
|
308
|
+
` ${url}\n\n` +
|
|
309
|
+
` Opening your browser. Keep this terminal open while you view or share.\n` +
|
|
310
|
+
` Press Ctrl+C when you're done.\n\n`);
|
|
311
|
+
try {
|
|
312
|
+
await openWrapped(url);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
process.stderr.write(` Couldn't open the browser automatically. Open this link manually:\n ${url}\n\n`);
|
|
316
|
+
}
|
|
317
|
+
// Hold the process open (the in-memory server keeps the data available) until
|
|
318
|
+
// the user exits; then drop everything.
|
|
319
|
+
await new Promise((resolve) => {
|
|
320
|
+
process.on("SIGINT", () => {
|
|
321
|
+
server.close();
|
|
322
|
+
process.stderr.write("\n Closed. Nothing was saved.\n");
|
|
323
|
+
resolve();
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
}
|
|
128
327
|
async function runAgent(jobId) {
|
|
129
328
|
// The wrapped render + share now run non-interactively (no TTY required), so
|
|
130
329
|
// the command completes when invoked from a Claude Code Bash call. Only the
|
|
@@ -132,10 +331,6 @@ async function runAgent(jobId) {
|
|
|
132
331
|
if (profileChatEnabled()) {
|
|
133
332
|
ensureInteractive("The interactive profile chat");
|
|
134
333
|
}
|
|
135
|
-
console.log("\n ┌────────────────────────────────┐");
|
|
136
|
-
console.log(" │ are you really tokenmaxxing? │");
|
|
137
|
-
console.log(" │ Powered by Standout (YC P26) │");
|
|
138
|
-
console.log(" └────────────────────────────────┘\n");
|
|
139
334
|
// Only frame the token as a job in the application flow. Otherwise it may be a
|
|
140
335
|
// leaderboard group (`npx standout stanford`), confirmed after the wrapped runs.
|
|
141
336
|
if (jobId && profileChatEnabled()) {
|
|
@@ -340,19 +535,37 @@ async function main() {
|
|
|
340
535
|
const proxy = configureProxyFromEnv();
|
|
341
536
|
if (proxy)
|
|
342
537
|
process.stderr.write(` (using proxy ${proxy})\n`);
|
|
538
|
+
const localOnly = localOnlyEnabled();
|
|
343
539
|
if (args.includes("--gather")) {
|
|
344
|
-
|
|
345
|
-
|
|
540
|
+
// Local-only gather skips the upload-consent gate (nothing leaves the box).
|
|
541
|
+
if (!localOnly)
|
|
542
|
+
await confirmPrivacy();
|
|
543
|
+
const data = await gatherFullEnrichment({ localOnly });
|
|
346
544
|
process.stdout.write(JSON.stringify(data, null, 2));
|
|
545
|
+
return;
|
|
347
546
|
}
|
|
348
|
-
|
|
547
|
+
if (args.includes("--mcp")) {
|
|
349
548
|
await startMcpServer();
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const jobId = args.find((a) => !a.startsWith("-"));
|
|
552
|
+
printBanner();
|
|
553
|
+
// Pick the mode. An explicit --local/--no-upload/STANDOUT_LOCAL forces local;
|
|
554
|
+
// a group/job arg (`npx standout stanford`) is an explicit full-flow intent; a
|
|
555
|
+
// bare `npx standout` asks the user as the first step.
|
|
556
|
+
let mode;
|
|
557
|
+
if (localOnly)
|
|
558
|
+
mode = "local";
|
|
559
|
+
else if (jobId)
|
|
560
|
+
mode = "full";
|
|
561
|
+
else
|
|
562
|
+
mode = await chooseMode();
|
|
563
|
+
if (mode === "local") {
|
|
564
|
+
await runLocal();
|
|
350
565
|
}
|
|
351
566
|
else {
|
|
352
|
-
|
|
353
|
-
if (jobId) {
|
|
567
|
+
if (jobId)
|
|
354
568
|
process.env.STANDOUT_JOB_ID = jobId;
|
|
355
|
-
}
|
|
356
569
|
await runAgent(jobId);
|
|
357
570
|
}
|
|
358
571
|
}
|
package/dist/gather.d.ts
CHANGED
|
@@ -72,6 +72,7 @@ export interface FullEnrichmentOptions {
|
|
|
72
72
|
aiUsage?: AiUsage;
|
|
73
73
|
profileBasics?: ProfileBasics;
|
|
74
74
|
verbose?: boolean;
|
|
75
|
+
localOnly?: boolean;
|
|
75
76
|
}
|
|
76
77
|
export declare function emptyGatheredProfile(readiness?: GatherReadiness): GatheredProfile;
|
|
77
78
|
export declare function gatherUsageStats(): Promise<Pick<GatheredProfile, "ai_usage" | "readiness">>;
|
package/dist/gather.js
CHANGED
|
@@ -445,6 +445,7 @@ function trimNpmPackage(p) {
|
|
|
445
445
|
};
|
|
446
446
|
}
|
|
447
447
|
export async function gather(options = {}) {
|
|
448
|
+
const localOnly = options.localOnly === true;
|
|
448
449
|
const profileBasics = options.profileBasics?.readiness.profile_basics_status === "ready"
|
|
449
450
|
? options.profileBasics
|
|
450
451
|
: undefined;
|
|
@@ -468,24 +469,32 @@ export async function gather(options = {}) {
|
|
|
468
469
|
remoteCandidates.push(match[1]);
|
|
469
470
|
}
|
|
470
471
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
472
|
+
if (localOnly) {
|
|
473
|
+
// No network to verify User vs Org — take the first remote candidate as a
|
|
474
|
+
// best-effort handle. Only used to label identity; the deck falls back to
|
|
475
|
+
// the git name regardless.
|
|
476
|
+
githubUsername = remoteCandidates[0] ?? null;
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
// Resolve to a USER (not an Organization) profile
|
|
480
|
+
for (const candidate of remoteCandidates) {
|
|
481
|
+
const profile = (await fetchGitHub(`https://api.github.com/users/${sanitize(candidate)}`));
|
|
482
|
+
if (profile?.type === "User" && profile.login) {
|
|
483
|
+
githubUsername = profile.login;
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
477
486
|
}
|
|
478
487
|
}
|
|
479
488
|
}
|
|
480
489
|
// Fallback: search GitHub by email (public profile email match)
|
|
481
|
-
if (!githubUsername && email) {
|
|
490
|
+
if (!githubUsername && email && !localOnly) {
|
|
482
491
|
const searchResult = (await fetchGitHub(`https://api.github.com/search/users?q=${encodeURIComponent(email)}+in:email`));
|
|
483
492
|
if (searchResult?.items?.[0]) {
|
|
484
493
|
githubUsername = searchResult.items[0].login;
|
|
485
494
|
}
|
|
486
495
|
}
|
|
487
496
|
// Fallback: search commits by author-email (catches users with private email)
|
|
488
|
-
if (!githubUsername && email) {
|
|
497
|
+
if (!githubUsername && email && !localOnly) {
|
|
489
498
|
const commitSearch = (await fetchGitHub(`https://api.github.com/search/commits?q=author-email:${encodeURIComponent(email)}`));
|
|
490
499
|
const login = commitSearch?.items?.[0]?.author?.login;
|
|
491
500
|
if (login)
|
|
@@ -497,7 +506,7 @@ export async function gather(options = {}) {
|
|
|
497
506
|
gatherLog(options, "Gathering GitHub data...\n");
|
|
498
507
|
const canUseCachedGithubBasics = Boolean(profileBasics?.identity.github_username) &&
|
|
499
508
|
profileBasics?.identity.github_username === githubUsername;
|
|
500
|
-
const [ghProfile, ghRepos, ghStarred, ghFollowing, ghEvents, ghPrs, ghContrib, ghNpm,] = safeUsername
|
|
509
|
+
const [ghProfile, ghRepos, ghStarred, ghFollowing, ghEvents, ghPrs, ghContrib, ghNpm,] = safeUsername && !localOnly
|
|
501
510
|
? await Promise.all([
|
|
502
511
|
canUseCachedGithubBasics
|
|
503
512
|
? Promise.resolve(profileBasics?.github.profile ?? null)
|
|
@@ -520,7 +529,7 @@ export async function gather(options = {}) {
|
|
|
520
529
|
const repoContents = {};
|
|
521
530
|
const readmes = {};
|
|
522
531
|
const commitMessages = {};
|
|
523
|
-
if (safeUsername) {
|
|
532
|
+
if (safeUsername && !localOnly) {
|
|
524
533
|
const contentPromises = topRepos.map(async (repo) => {
|
|
525
534
|
const repoName = repo.name;
|
|
526
535
|
const contents = (await fetchGitHub(`https://api.github.com/repos/${safeUsername}/${repoName}/contents`));
|
|
@@ -555,7 +564,7 @@ export async function gather(options = {}) {
|
|
|
555
564
|
}
|
|
556
565
|
// Phase 2c: AI tool usage + dev environment (run in background during LinkedIn)
|
|
557
566
|
gatherLog(options, "Scanning AI tool usage and dev environment...\n");
|
|
558
|
-
const githubEnrichPromise = safeUsername
|
|
567
|
+
const githubEnrichPromise = safeUsername && !localOnly
|
|
559
568
|
? enrichApi("github", safeUsername).catch(() => null)
|
|
560
569
|
: Promise.resolve(null);
|
|
561
570
|
const aiUsagePromise = options.aiUsage
|
|
@@ -583,7 +592,7 @@ export async function gather(options = {}) {
|
|
|
583
592
|
let linkedinResolutionSource = null;
|
|
584
593
|
let linkedinProfile = null;
|
|
585
594
|
const companyEnrichments = {};
|
|
586
|
-
if (safeEmail) {
|
|
595
|
+
if (safeEmail && !localOnly) {
|
|
587
596
|
const emailResult = await enrichApi("email_to_linkedin", email);
|
|
588
597
|
if (emailResult && typeof emailResult === "object") {
|
|
589
598
|
linkedinUrl =
|
|
@@ -594,7 +603,7 @@ export async function gather(options = {}) {
|
|
|
594
603
|
}
|
|
595
604
|
}
|
|
596
605
|
// Fallback: name + signals via AI web search when email lookup comes up empty
|
|
597
|
-
if (!linkedinUrl && ghProfile && name) {
|
|
606
|
+
if (!linkedinUrl && ghProfile && name && !localOnly) {
|
|
598
607
|
gatherLog(options, "Resolving LinkedIn via name + signals...\n");
|
|
599
608
|
const profile = ghProfile;
|
|
600
609
|
const signals = [];
|
|
@@ -631,8 +640,10 @@ export async function gather(options = {}) {
|
|
|
631
640
|
}
|
|
632
641
|
// Fallback: read a logged-in LinkedIn session from the local Chrome profile
|
|
633
642
|
// (headless, silent). Same technique as the X handle scrape — the only anchor
|
|
634
|
-
// for users with no git email/name and no GitHub remote.
|
|
635
|
-
|
|
643
|
+
// for users with no git email/name and no GitHub remote. Reads the local
|
|
644
|
+
// browser but resolves a URL the enrich proxy would then look up, so it's
|
|
645
|
+
// skipped in local-only mode too.
|
|
646
|
+
if (!linkedinUrl && !localOnly) {
|
|
636
647
|
gatherLog(options, "Checking for a LinkedIn session in Chrome...\n");
|
|
637
648
|
try {
|
|
638
649
|
const scraped = await scrapeLinkedinUrl();
|
|
@@ -678,7 +689,7 @@ export async function gather(options = {}) {
|
|
|
678
689
|
}
|
|
679
690
|
}
|
|
680
691
|
// Fallback: scrape the user's logged-in Chrome session for an X handle.
|
|
681
|
-
if (!twitterHandle) {
|
|
692
|
+
if (!twitterHandle && !localOnly) {
|
|
682
693
|
gatherLog(options, "Checking for an X/Twitter session in Chrome...\n");
|
|
683
694
|
try {
|
|
684
695
|
const scraped = await scrapeTwitterHandle();
|
|
@@ -691,7 +702,7 @@ export async function gather(options = {}) {
|
|
|
691
702
|
}
|
|
692
703
|
const safeTwitter = twitterHandle ? sanitize(twitterHandle) : null;
|
|
693
704
|
let twitterProfile = null;
|
|
694
|
-
if (safeTwitter) {
|
|
705
|
+
if (safeTwitter && !localOnly) {
|
|
695
706
|
twitterProfile = await enrichApi("twitter", safeTwitter);
|
|
696
707
|
}
|
|
697
708
|
const twitterUrl = safeTwitter ? `https://x.com/${safeTwitter}` : null;
|
|
@@ -850,7 +861,8 @@ export async function gather(options = {}) {
|
|
|
850
861
|
readiness: {
|
|
851
862
|
usage_stats_status: hasAnyUsage(aiUsage) ? "ready" : "unavailable",
|
|
852
863
|
profile_basics_status: "ready",
|
|
853
|
-
|
|
864
|
+
// Local-only mode performs no network enrichment.
|
|
865
|
+
enrichment_status: localOnly ? "unavailable" : "ready",
|
|
854
866
|
},
|
|
855
867
|
identity: {
|
|
856
868
|
name: resolvedName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "standout",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.32",
|
|
4
4
|
"description": "Build your developer profile with AI. One command, zero friction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cli.js",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsc",
|
|
21
|
-
"dev": "
|
|
21
|
+
"dev": "tsc && node dist/cli.js",
|
|
22
22
|
"test": "tsx --test tests/*.test.ts",
|
|
23
23
|
"preview:tiers": "tsx src/wrapped/preview.ts",
|
|
24
24
|
"typecheck": "tsc --noEmit",
|