standout 0.5.29 → 0.5.31

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.
@@ -0,0 +1,3 @@
1
+ import { type BrowserContext } from "playwright-core";
2
+ export declare const NAV_TIMEOUT_MS = 20000;
3
+ export declare function scrapeFromChromeSession<T>(extract: (context: BrowserContext) => Promise<T | null>): Promise<T | null>;
@@ -0,0 +1,227 @@
1
+ import { chromium } from "playwright-core";
2
+ import { existsSync, readdirSync, statSync, mkdtempSync, mkdirSync, copyFileSync, rmSync, readFileSync, } from "fs";
3
+ import { homedir, platform, tmpdir } from "os";
4
+ import { join } from "path";
5
+ import { spawn } from "child_process";
6
+ const DEBUG_PORT_WAIT_MS = 10000;
7
+ export const NAV_TIMEOUT_MS = 20000;
8
+ const TOTAL_TIMEOUT_MS = 40000;
9
+ function wait(ms) {
10
+ return new Promise((r) => setTimeout(r, ms));
11
+ }
12
+ function findChromeExecutable() {
13
+ const os = platform();
14
+ const candidates = os === "darwin"
15
+ ? [
16
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
17
+ "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
18
+ "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
19
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
20
+ ]
21
+ : os === "linux"
22
+ ? [
23
+ "/usr/bin/google-chrome",
24
+ "/usr/bin/google-chrome-stable",
25
+ "/usr/bin/chromium",
26
+ "/usr/bin/chromium-browser",
27
+ "/snap/bin/chromium",
28
+ ]
29
+ : os === "win32"
30
+ ? [
31
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
32
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
33
+ ]
34
+ : [];
35
+ for (const path of candidates) {
36
+ if (existsSync(path))
37
+ return path;
38
+ }
39
+ return null;
40
+ }
41
+ function findChromeProfileRoot() {
42
+ const os = platform();
43
+ const home = homedir();
44
+ const candidates = os === "darwin"
45
+ ? [
46
+ join(home, "Library", "Application Support", "Google", "Chrome"),
47
+ join(home, "Library", "Application Support", "Chromium"),
48
+ ]
49
+ : os === "linux"
50
+ ? [
51
+ join(home, ".config", "google-chrome"),
52
+ join(home, ".config", "chromium"),
53
+ ]
54
+ : os === "win32"
55
+ ? [join(home, "AppData", "Local", "Google", "Chrome", "User Data")]
56
+ : [];
57
+ for (const path of candidates) {
58
+ if (existsSync(path) && existsSync(join(path, "Local State")))
59
+ return path;
60
+ }
61
+ return null;
62
+ }
63
+ function listProfilesByMtime(profileRoot) {
64
+ try {
65
+ return readdirSync(profileRoot)
66
+ .filter((d) => existsSync(join(profileRoot, d, "Cookies")))
67
+ .map((d) => ({
68
+ name: d,
69
+ mtime: statSync(join(profileRoot, d, "Cookies")).mtimeMs,
70
+ }))
71
+ .sort((a, b) => b.mtime - a.mtime)
72
+ .map((p) => p.name);
73
+ }
74
+ catch {
75
+ return [];
76
+ }
77
+ }
78
+ function prepareProfileCopy(profileRoot, profileName) {
79
+ try {
80
+ const tempRoot = mkdtempSync(join(tmpdir(), "standout-chrome-"));
81
+ mkdirSync(join(tempRoot, "Default"), { recursive: true });
82
+ const localState = join(profileRoot, "Local State");
83
+ if (existsSync(localState)) {
84
+ copyFileSync(localState, join(tempRoot, "Local State"));
85
+ }
86
+ const sourceDir = join(profileRoot, profileName);
87
+ for (const name of ["Cookies", "Cookies-journal", "Preferences"]) {
88
+ const src = join(sourceDir, name);
89
+ if (existsSync(src)) {
90
+ copyFileSync(src, join(tempRoot, "Default", name));
91
+ }
92
+ }
93
+ return tempRoot;
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ async function waitForDebugPort(userDataDir, timeoutMs) {
100
+ const deadline = Date.now() + timeoutMs;
101
+ const portFile = join(userDataDir, "DevToolsActivePort");
102
+ while (Date.now() < deadline) {
103
+ if (existsSync(portFile)) {
104
+ try {
105
+ const [port] = readFileSync(portFile, "utf-8").trim().split("\n");
106
+ if (port && /^\d+$/.test(port))
107
+ return parseInt(port, 10);
108
+ }
109
+ catch {
110
+ // keep polling
111
+ }
112
+ }
113
+ await wait(200);
114
+ }
115
+ return null;
116
+ }
117
+ async function spawnChrome(executablePath, userDataDir) {
118
+ const os = platform();
119
+ const args = [
120
+ "--headless=new",
121
+ `--user-data-dir=${userDataDir}`,
122
+ "--remote-debugging-port=0",
123
+ "--no-first-run",
124
+ "--no-default-browser-check",
125
+ "--disable-sync",
126
+ "--disable-features=AutofillServerCommunication",
127
+ ];
128
+ const cleanupFns = [];
129
+ if (os === "darwin") {
130
+ // Use `open -n -a` to force a new Chrome process instance on macOS
131
+ // (direct binary launch gets intercepted by the singleton running Chrome).
132
+ const appBundle = executablePath.replace(/\.app\/Contents\/MacOS\/.*$/, ".app");
133
+ const child = spawn("open", ["-n", "-a", appBundle, "--args", ...args], {
134
+ detached: true,
135
+ stdio: "ignore",
136
+ });
137
+ child.unref();
138
+ cleanupFns.push(() => {
139
+ try {
140
+ spawn("pkill", ["-f", `user-data-dir=${userDataDir}`]);
141
+ }
142
+ catch {
143
+ // skip
144
+ }
145
+ });
146
+ }
147
+ else {
148
+ const child = spawn(executablePath, args, {
149
+ detached: true,
150
+ stdio: "ignore",
151
+ });
152
+ child.unref();
153
+ cleanupFns.push(() => {
154
+ try {
155
+ if (child.pid)
156
+ process.kill(child.pid);
157
+ }
158
+ catch {
159
+ // skip
160
+ }
161
+ });
162
+ }
163
+ const port = await waitForDebugPort(userDataDir, DEBUG_PORT_WAIT_MS);
164
+ return {
165
+ port,
166
+ cleanup: () => cleanupFns.forEach((fn) => fn()),
167
+ };
168
+ }
169
+ async function withTimeout(promise, ms) {
170
+ return Promise.race([
171
+ promise,
172
+ new Promise((resolve) => setTimeout(() => resolve(null), ms)),
173
+ ]);
174
+ }
175
+ async function withProfileContext(executablePath, profileRoot, profileName, extract) {
176
+ const tempProfile = prepareProfileCopy(profileRoot, profileName);
177
+ if (!tempProfile)
178
+ return null;
179
+ const { port, cleanup } = await spawnChrome(executablePath, tempProfile);
180
+ let browser = null;
181
+ try {
182
+ if (!port)
183
+ return null;
184
+ browser = await chromium.connectOverCDP(`http://localhost:${port}`);
185
+ const contexts = browser.contexts();
186
+ const context = contexts[0] ?? (await browser.newContext());
187
+ return await extract(context);
188
+ }
189
+ catch {
190
+ return null;
191
+ }
192
+ finally {
193
+ if (browser)
194
+ await browser.close().catch(() => undefined);
195
+ cleanup();
196
+ await wait(500);
197
+ try {
198
+ rmSync(tempProfile, { recursive: true, force: true });
199
+ }
200
+ catch {
201
+ // skip
202
+ }
203
+ }
204
+ }
205
+ // Headless, silent read of the user's logged-in Chrome session. Copies each
206
+ // Chrome profile (newest-cookie first) into a throwaway dir, opens it headless,
207
+ // and hands the browser context to `extract`. Returns the first non-null result;
208
+ // any failure (no Chrome, no session, checkpoint, timeout) resolves to null.
209
+ export async function scrapeFromChromeSession(extract) {
210
+ const executablePath = findChromeExecutable();
211
+ if (!executablePath)
212
+ return null;
213
+ const profileRoot = findChromeProfileRoot();
214
+ if (!profileRoot)
215
+ return null;
216
+ const profiles = listProfilesByMtime(profileRoot);
217
+ if (profiles.length === 0)
218
+ return null;
219
+ return withTimeout((async () => {
220
+ for (const profile of profiles) {
221
+ const result = await withProfileContext(executablePath, profileRoot, profile, extract).catch(() => null);
222
+ if (result)
223
+ return result;
224
+ }
225
+ return null;
226
+ })(), TOTAL_TIMEOUT_MS);
227
+ }
package/dist/cli.js CHANGED
@@ -136,7 +136,9 @@ async function runAgent(jobId) {
136
136
  console.log(" │ are you really tokenmaxxing? │");
137
137
  console.log(" │ Powered by Standout (YC P26) │");
138
138
  console.log(" └────────────────────────────────┘\n");
139
- if (jobId) {
139
+ // Only frame the token as a job in the application flow. Otherwise it may be a
140
+ // leaderboard group (`npx standout stanford`), confirmed after the wrapped runs.
141
+ if (jobId && profileChatEnabled()) {
140
142
  console.log(` Applying to: ${STANDOUT_API_URL}/jobs/${jobId}\n`);
141
143
  }
142
144
  // LLM calls are proxied through Standout — users don't need an Anthropic key.
@@ -175,6 +177,7 @@ async function runAgent(jobId) {
175
177
  const wrapped = await createWrapped({
176
178
  profile: prefetched,
177
179
  submissionId: null,
180
+ group: jobId ?? null,
178
181
  });
179
182
  return { wrapped, prefetched };
180
183
  }
@@ -196,6 +199,9 @@ async function runAgent(jobId) {
196
199
  let wrappedId = wrapped?.id ?? null;
197
200
  if (wrapped) {
198
201
  await renderWrappedAll(wrapped.view);
202
+ if (wrapped.group) {
203
+ process.stderr.write(` you're on the ${wrapped.group.label} leaderboard — see where you rank.\n\n`);
204
+ }
199
205
  await maybeShareWrapped(wrapped);
200
206
  }
201
207
  else {
package/dist/gather.d.ts CHANGED
@@ -25,7 +25,7 @@ export interface GatheredProfile {
25
25
  };
26
26
  linkedin: {
27
27
  url: string | null;
28
- url_resolution_source: "email" | "ai_search" | null;
28
+ url_resolution_source: "email" | "ai_search" | "chrome_session" | null;
29
29
  profile: Record<string, unknown> | null;
30
30
  company_enrichments: Record<string, Record<string, unknown>>;
31
31
  };
package/dist/gather.js CHANGED
@@ -6,6 +6,8 @@ import { STANDOUT_API_URL } from "./api.js";
6
6
  import { gatherAiUsage } from "./ai-usage.js";
7
7
  import { gatherDevEnvironment } from "./dev-env.js";
8
8
  import { scrapeTwitterHandle } from "./twitter-scrape.js";
9
+ import { scrapeLinkedinUrl } from "./linkedin-scrape.js";
10
+ import { resolveDisplayName } from "./identity.js";
9
11
  import { redactSecrets } from "./redact.js";
10
12
  export function emptyGatheredProfile(readiness = {
11
13
  usage_stats_status: "unavailable",
@@ -627,6 +629,22 @@ export async function gather(options = {}) {
627
629
  linkedinResolutionSource = "ai_search";
628
630
  }
629
631
  }
632
+ // Fallback: read a logged-in LinkedIn session from the local Chrome profile
633
+ // (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
+ if (!linkedinUrl) {
636
+ gatherLog(options, "Checking for a LinkedIn session in Chrome...\n");
637
+ try {
638
+ const scraped = await scrapeLinkedinUrl();
639
+ if (scraped) {
640
+ linkedinUrl = scraped;
641
+ linkedinResolutionSource = "chrome_session";
642
+ }
643
+ }
644
+ catch {
645
+ // silent
646
+ }
647
+ }
630
648
  if (linkedinUrl) {
631
649
  linkedinProfile = await enrichApi("linkedin_profile", linkedinUrl);
632
650
  // Company enrichment
@@ -821,6 +839,13 @@ export async function gather(options = {}) {
821
839
  githubEnrichPromise,
822
840
  ]);
823
841
  gatherLog(options, "Done gathering.\n");
842
+ const resolvedName = resolveDisplayName({
843
+ gitName: name,
844
+ githubName: ghProfile?.name,
845
+ linkedinProfile: linkedinProfile,
846
+ githubUsername,
847
+ twitterHandle,
848
+ });
824
849
  return {
825
850
  readiness: {
826
851
  usage_stats_status: hasAnyUsage(aiUsage) ? "ready" : "unavailable",
@@ -828,7 +853,7 @@ export async function gather(options = {}) {
828
853
  enrichment_status: "ready",
829
854
  },
830
855
  identity: {
831
- name: name || null,
856
+ name: resolvedName,
832
857
  email: email || null,
833
858
  github_username: githubUsername,
834
859
  twitter_username: twitterHandle,
@@ -0,0 +1,8 @@
1
+ export declare function linkedinFullName(profile: Record<string, unknown> | null | undefined): string | null;
2
+ export declare function resolveDisplayName(opts: {
3
+ gitName: string | null | undefined;
4
+ githubName: unknown;
5
+ linkedinProfile: Record<string, unknown> | null | undefined;
6
+ githubUsername: string | null | undefined;
7
+ twitterHandle: string | null | undefined;
8
+ }): string | null;
@@ -0,0 +1,29 @@
1
+ function cleanName(value) {
2
+ return typeof value === "string" && value.trim() ? value.trim() : null;
3
+ }
4
+ // LinkedIn enrichment exposes the display name as `full_name`, falling back to
5
+ // first/last when only those are present.
6
+ export function linkedinFullName(profile) {
7
+ if (!profile)
8
+ return null;
9
+ const full = cleanName(profile.full_name);
10
+ if (full)
11
+ return full;
12
+ const joined = [cleanName(profile.first_name), cleanName(profile.last_name)]
13
+ .filter(Boolean)
14
+ .join(" ")
15
+ .trim();
16
+ return joined || null;
17
+ }
18
+ // Identity name fallback chain. The self-set git name stays primary; when it's
19
+ // missing we fall back to real names (GitHub, then LinkedIn) and finally to
20
+ // handles (GitHub, then Twitter) so a wrapped is never anonymous when any
21
+ // identity signal exists.
22
+ export function resolveDisplayName(opts) {
23
+ return (cleanName(opts.gitName) ??
24
+ cleanName(opts.githubName) ??
25
+ linkedinFullName(opts.linkedinProfile) ??
26
+ cleanName(opts.githubUsername) ??
27
+ cleanName(opts.twitterHandle) ??
28
+ null);
29
+ }
@@ -0,0 +1,2 @@
1
+ export declare function normalizeLinkedinUrl(raw: string): string | null;
2
+ export declare function scrapeLinkedinUrl(): Promise<string | null>;
@@ -0,0 +1,56 @@
1
+ import { NAV_TIMEOUT_MS, scrapeFromChromeSession } from "./browser-session.js";
2
+ // LinkedIn bounces unauthenticated / new-device sessions to one of these.
3
+ const BLOCKED_RE = /\/(login|uas\/login|checkpoint|authwall)/i;
4
+ // Reduce any LinkedIn profile URL to the canonical vanity form, or null if it
5
+ // isn't a real member profile (e.g. the `/in/me` self-redirect target itself).
6
+ export function normalizeLinkedinUrl(raw) {
7
+ try {
8
+ const url = new URL(raw);
9
+ if (!/(^|\.)linkedin\.com$/i.test(url.hostname))
10
+ return null;
11
+ const match = url.pathname.match(/\/in\/([^/]+)/i);
12
+ const vanity = match?.[1] ? decodeURIComponent(match[1]).trim() : null;
13
+ if (!vanity || vanity.toLowerCase() === "me")
14
+ return null;
15
+ return `https://www.linkedin.com/in/${vanity}/`;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ async function extractLinkedinUrl(context) {
22
+ const cookies = await context.cookies(["https://www.linkedin.com"]);
23
+ if (!cookies.find((c) => c.name === "li_at"))
24
+ return null;
25
+ const page = await context.newPage();
26
+ try {
27
+ // `/in/me/` redirects to the logged-in member's own vanity profile.
28
+ await page.goto("https://www.linkedin.com/in/me/", {
29
+ waitUntil: "domcontentloaded",
30
+ timeout: NAV_TIMEOUT_MS,
31
+ });
32
+ if (BLOCKED_RE.test(page.url()))
33
+ return null;
34
+ const fromRedirect = normalizeLinkedinUrl(page.url());
35
+ if (fromRedirect)
36
+ return fromRedirect;
37
+ // Backup signal if the redirect didn't land on a vanity URL.
38
+ try {
39
+ const canonical = page.locator('link[rel="canonical"]').first();
40
+ await canonical.waitFor({ timeout: 3000 });
41
+ const href = await canonical.getAttribute("href");
42
+ if (href)
43
+ return normalizeLinkedinUrl(href);
44
+ }
45
+ catch {
46
+ // skip
47
+ }
48
+ return null;
49
+ }
50
+ finally {
51
+ await page.close().catch(() => undefined);
52
+ }
53
+ }
54
+ export async function scrapeLinkedinUrl() {
55
+ return scrapeFromChromeSession(extractLinkedinUrl);
56
+ }
@@ -1,267 +1,48 @@
1
- import { chromium } from "playwright-core";
2
- import { existsSync, readdirSync, statSync, mkdtempSync, mkdirSync, copyFileSync, rmSync, readFileSync, } from "fs";
3
- import { homedir, platform, tmpdir } from "os";
4
- import { join } from "path";
5
- import { spawn } from "child_process";
6
- const DEBUG_PORT_WAIT_MS = 10000;
7
- const NAV_TIMEOUT_MS = 20000;
8
- const TOTAL_TIMEOUT_MS = 40000;
9
- function wait(ms) {
10
- return new Promise((r) => setTimeout(r, ms));
11
- }
12
- function findChromeExecutable() {
13
- const os = platform();
14
- const candidates = os === "darwin"
15
- ? [
16
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
17
- "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
18
- "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
19
- "/Applications/Chromium.app/Contents/MacOS/Chromium",
20
- ]
21
- : os === "linux"
22
- ? [
23
- "/usr/bin/google-chrome",
24
- "/usr/bin/google-chrome-stable",
25
- "/usr/bin/chromium",
26
- "/usr/bin/chromium-browser",
27
- "/snap/bin/chromium",
28
- ]
29
- : os === "win32"
30
- ? [
31
- "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
32
- "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
33
- ]
34
- : [];
35
- for (const path of candidates) {
36
- if (existsSync(path))
37
- return path;
38
- }
39
- return null;
40
- }
41
- function findChromeProfileRoot() {
42
- const os = platform();
43
- const home = homedir();
44
- const candidates = os === "darwin"
45
- ? [
46
- join(home, "Library", "Application Support", "Google", "Chrome"),
47
- join(home, "Library", "Application Support", "Chromium"),
48
- ]
49
- : os === "linux"
50
- ? [
51
- join(home, ".config", "google-chrome"),
52
- join(home, ".config", "chromium"),
53
- ]
54
- : os === "win32"
55
- ? [join(home, "AppData", "Local", "Google", "Chrome", "User Data")]
56
- : [];
57
- for (const path of candidates) {
58
- if (existsSync(path) && existsSync(join(path, "Local State")))
59
- return path;
60
- }
61
- return null;
62
- }
63
- function listProfilesByMtime(profileRoot) {
64
- try {
65
- return readdirSync(profileRoot)
66
- .filter((d) => existsSync(join(profileRoot, d, "Cookies")))
67
- .map((d) => ({
68
- name: d,
69
- mtime: statSync(join(profileRoot, d, "Cookies")).mtimeMs,
70
- }))
71
- .sort((a, b) => b.mtime - a.mtime)
72
- .map((p) => p.name);
73
- }
74
- catch {
75
- return [];
76
- }
77
- }
78
- function prepareProfileCopy(profileRoot, profileName) {
79
- try {
80
- const tempRoot = mkdtempSync(join(tmpdir(), "standout-chrome-"));
81
- mkdirSync(join(tempRoot, "Default"), { recursive: true });
82
- const localState = join(profileRoot, "Local State");
83
- if (existsSync(localState)) {
84
- copyFileSync(localState, join(tempRoot, "Local State"));
85
- }
86
- const sourceDir = join(profileRoot, profileName);
87
- for (const name of ["Cookies", "Cookies-journal", "Preferences"]) {
88
- const src = join(sourceDir, name);
89
- if (existsSync(src)) {
90
- copyFileSync(src, join(tempRoot, "Default", name));
91
- }
92
- }
93
- return tempRoot;
94
- }
95
- catch {
96
- return null;
97
- }
98
- }
99
- async function waitForDebugPort(userDataDir, timeoutMs) {
100
- const deadline = Date.now() + timeoutMs;
101
- const portFile = join(userDataDir, "DevToolsActivePort");
102
- while (Date.now() < deadline) {
103
- if (existsSync(portFile)) {
104
- try {
105
- const [port] = readFileSync(portFile, "utf-8").trim().split("\n");
106
- if (port && /^\d+$/.test(port))
107
- return parseInt(port, 10);
108
- }
109
- catch {
110
- // keep polling
111
- }
112
- }
113
- await wait(200);
114
- }
115
- return null;
116
- }
117
- async function spawnChrome(executablePath, userDataDir) {
118
- const os = platform();
119
- const args = [
120
- "--headless=new",
121
- `--user-data-dir=${userDataDir}`,
122
- "--remote-debugging-port=0",
123
- "--no-first-run",
124
- "--no-default-browser-check",
125
- "--disable-sync",
126
- "--disable-features=AutofillServerCommunication",
127
- ];
128
- const cleanupFns = [];
129
- if (os === "darwin") {
130
- // Use `open -n -a` to force a new Chrome process instance on macOS
131
- // (direct binary launch gets intercepted by the singleton running Chrome).
132
- const appBundle = executablePath.replace(/\.app\/Contents\/MacOS\/.*$/, ".app");
133
- const child = spawn("open", ["-n", "-a", appBundle, "--args", ...args], {
134
- detached: true,
135
- stdio: "ignore",
136
- });
137
- child.unref();
138
- cleanupFns.push(() => {
139
- try {
140
- spawn("pkill", ["-f", `user-data-dir=${userDataDir}`]);
141
- }
142
- catch {
143
- // skip
144
- }
145
- });
146
- }
147
- else {
148
- const child = spawn(executablePath, args, {
149
- detached: true,
150
- stdio: "ignore",
151
- });
152
- child.unref();
153
- cleanupFns.push(() => {
154
- try {
155
- if (child.pid)
156
- process.kill(child.pid);
157
- }
158
- catch {
159
- // skip
160
- }
161
- });
162
- }
163
- const port = await waitForDebugPort(userDataDir, DEBUG_PORT_WAIT_MS);
164
- return {
165
- port,
166
- cleanup: () => cleanupFns.forEach((fn) => fn()),
167
- };
168
- }
169
- async function extractHandleFromProfile(executablePath, profileRoot, profileName) {
170
- const tempProfile = prepareProfileCopy(profileRoot, profileName);
171
- if (!tempProfile)
1
+ import { NAV_TIMEOUT_MS, scrapeFromChromeSession } from "./browser-session.js";
2
+ async function extractTwitterHandle(context) {
3
+ const cookies = await context.cookies([
4
+ "https://x.com",
5
+ "https://twitter.com",
6
+ ]);
7
+ if (!cookies.find((c) => c.name === "auth_token"))
172
8
  return null;
173
- const { port, cleanup } = await spawnChrome(executablePath, tempProfile);
174
- let browser = null;
9
+ const page = await context.newPage();
175
10
  try {
176
- if (!port)
177
- return null;
178
- browser = await chromium.connectOverCDP(`http://localhost:${port}`);
179
- const contexts = browser.contexts();
180
- const context = contexts[0] ?? (await browser.newContext());
181
- const cookies = await context.cookies([
182
- "https://x.com",
183
- "https://twitter.com",
184
- ]);
185
- const authToken = cookies.find((c) => c.name === "auth_token");
186
- if (!authToken)
11
+ await page.goto("https://x.com/home", {
12
+ waitUntil: "domcontentloaded",
13
+ timeout: NAV_TIMEOUT_MS,
14
+ });
15
+ if (/\/(login|i\/flow\/login)/.test(page.url()))
187
16
  return null;
188
- const page = await context.newPage();
189
17
  try {
190
- await page.goto("https://x.com/home", {
191
- waitUntil: "domcontentloaded",
192
- timeout: NAV_TIMEOUT_MS,
193
- });
194
- const url = page.url();
195
- if (/\/(login|i\/flow\/login)/.test(url))
196
- return null;
197
- try {
198
- const link = page.locator('[data-testid="AppTabBar_Profile_Link"]');
199
- await link.first().waitFor({ timeout: 5000 });
200
- const href = await link.first().getAttribute("href");
201
- if (href) {
202
- const handle = href.replace(/^\//, "").trim();
203
- if (handle && !handle.includes("/"))
204
- return handle;
205
- }
206
- }
207
- catch {
208
- // fall through
209
- }
210
- try {
211
- const nameElem = page.locator('[data-testid="UserName"] span:has-text("@")');
212
- const text = await nameElem.first().innerText({ timeout: 3000 });
213
- const match = text.match(/@([A-Za-z0-9_]+)/);
214
- if (match)
215
- return match[1];
216
- }
217
- catch {
218
- // skip
18
+ const link = page.locator('[data-testid="AppTabBar_Profile_Link"]');
19
+ await link.first().waitFor({ timeout: 5000 });
20
+ const href = await link.first().getAttribute("href");
21
+ if (href) {
22
+ const handle = href.replace(/^\//, "").trim();
23
+ if (handle && !handle.includes("/"))
24
+ return handle;
219
25
  }
220
- return null;
221
- }
222
- finally {
223
- await page.close().catch(() => undefined);
224
26
  }
225
- }
226
- catch {
227
- return null;
228
- }
229
- finally {
230
- if (browser) {
231
- await browser.close().catch(() => undefined);
27
+ catch {
28
+ // fall through
232
29
  }
233
- cleanup();
234
- await wait(500);
235
30
  try {
236
- rmSync(tempProfile, { recursive: true, force: true });
31
+ const nameElem = page.locator('[data-testid="UserName"] span:has-text("@")');
32
+ const text = await nameElem.first().innerText({ timeout: 3000 });
33
+ const match = text.match(/@([A-Za-z0-9_]+)/);
34
+ if (match)
35
+ return match[1];
237
36
  }
238
37
  catch {
239
38
  // skip
240
39
  }
40
+ return null;
41
+ }
42
+ finally {
43
+ await page.close().catch(() => undefined);
241
44
  }
242
- }
243
- async function withTimeout(promise, ms) {
244
- return Promise.race([
245
- promise,
246
- new Promise((resolve) => setTimeout(() => resolve(null), ms)),
247
- ]);
248
45
  }
249
46
  export async function scrapeTwitterHandle() {
250
- const executablePath = findChromeExecutable();
251
- if (!executablePath)
252
- return null;
253
- const profileRoot = findChromeProfileRoot();
254
- if (!profileRoot)
255
- return null;
256
- const profiles = listProfilesByMtime(profileRoot);
257
- if (profiles.length === 0)
258
- return null;
259
- return withTimeout((async () => {
260
- for (const profile of profiles) {
261
- const handle = await extractHandleFromProfile(executablePath, profileRoot, profile).catch(() => null);
262
- if (handle)
263
- return handle;
264
- }
265
- return null;
266
- })(), TOTAL_TIMEOUT_MS);
47
+ return scrapeFromChromeSession(extractTwitterHandle);
267
48
  }
@@ -1,10 +1,16 @@
1
1
  import type { WrappedAggregateView } from "./wrapped/types.js";
2
+ export interface WrappedGroup {
3
+ slug: string;
4
+ label: string;
5
+ }
2
6
  export interface WrappedReady {
3
7
  id: string;
4
8
  share_url: string;
5
9
  view: WrappedAggregateView;
10
+ group: WrappedGroup | null;
6
11
  }
7
12
  export declare function createWrapped(args: {
8
13
  profile: Record<string, unknown>;
9
14
  submissionId?: string | null;
15
+ group?: string | null;
10
16
  }): Promise<WrappedReady | null>;
@@ -97,6 +97,7 @@ export async function createWrapped(args) {
97
97
  const body = JSON.stringify({
98
98
  profile,
99
99
  submission_id: args.submissionId ?? undefined,
100
+ group: args.group ?? undefined,
100
101
  });
101
102
  const bodyKb = Math.round(bytesAfter / 1024);
102
103
  let created = null;
@@ -156,5 +157,10 @@ export async function createWrapped(args) {
156
157
  computed,
157
158
  share_url: created.share_url,
158
159
  });
159
- return { id: created.id, share_url: created.share_url, view };
160
+ return {
161
+ id: created.id,
162
+ share_url: created.share_url,
163
+ view,
164
+ group: created.group ?? null,
165
+ };
160
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "standout",
3
- "version": "0.5.29",
3
+ "version": "0.5.31",
4
4
  "description": "Build your developer profile with AI. One command, zero friction.",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",