standout 0.5.33 → 0.5.35

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.
Files changed (55) hide show
  1. package/dist/cli.js +7431 -559
  2. package/package.json +9 -3
  3. package/dist/ai-usage.d.ts +0 -164
  4. package/dist/ai-usage.js +0 -1306
  5. package/dist/api.d.ts +0 -10
  6. package/dist/api.js +0 -94
  7. package/dist/browser-session.d.ts +0 -3
  8. package/dist/browser-session.js +0 -227
  9. package/dist/cli.d.ts +0 -2
  10. package/dist/cursor.d.ts +0 -57
  11. package/dist/cursor.js +0 -562
  12. package/dist/dev-env.d.ts +0 -28
  13. package/dist/dev-env.js +0 -264
  14. package/dist/gather.d.ts +0 -82
  15. package/dist/gather.js +0 -922
  16. package/dist/heap.d.ts +0 -1
  17. package/dist/heap.js +0 -25
  18. package/dist/identity.d.ts +0 -8
  19. package/dist/identity.js +0 -29
  20. package/dist/linkedin-scrape.d.ts +0 -2
  21. package/dist/linkedin-scrape.js +0 -56
  22. package/dist/mcp.d.ts +0 -1
  23. package/dist/mcp.js +0 -25
  24. package/dist/payload.d.ts +0 -8
  25. package/dist/payload.js +0 -163
  26. package/dist/prompt.d.ts +0 -1
  27. package/dist/prompt.js +0 -247
  28. package/dist/proxy.d.ts +0 -1
  29. package/dist/proxy.js +0 -27
  30. package/dist/redact.d.ts +0 -1
  31. package/dist/redact.js +0 -37
  32. package/dist/tools.d.ts +0 -4
  33. package/dist/tools.js +0 -182
  34. package/dist/twitter-scrape.d.ts +0 -1
  35. package/dist/twitter-scrape.js +0 -48
  36. package/dist/wrapped/aggregate.d.ts +0 -6
  37. package/dist/wrapped/aggregate.js +0 -811
  38. package/dist/wrapped/chrono-critters.d.ts +0 -8
  39. package/dist/wrapped/chrono-critters.js +0 -32
  40. package/dist/wrapped/index.d.ts +0 -3
  41. package/dist/wrapped/index.js +0 -2
  42. package/dist/wrapped/mascots.d.ts +0 -2
  43. package/dist/wrapped/mascots.js +0 -35
  44. package/dist/wrapped/preview.d.ts +0 -1
  45. package/dist/wrapped/preview.js +0 -97
  46. package/dist/wrapped/render.d.ts +0 -14
  47. package/dist/wrapped/render.js +0 -1025
  48. package/dist/wrapped/tiers.d.ts +0 -9
  49. package/dist/wrapped/tiers.js +0 -73
  50. package/dist/wrapped/types.d.ts +0 -189
  51. package/dist/wrapped/types.js +0 -4
  52. package/dist/wrapped-client.d.ts +0 -16
  53. package/dist/wrapped-client.js +0 -166
  54. package/dist/wrapped-share.d.ts +0 -1
  55. package/dist/wrapped-share.js +0 -6
package/dist/heap.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare function ensureHeapHeadroom(): void;
package/dist/heap.js DELETED
@@ -1,25 +0,0 @@
1
- import { spawnSync } from "child_process";
2
- import { totalmem } from "os";
3
- // Node caps old-space at ~4GB by default regardless of physical RAM, so a big
4
- // machine OOMs at the same point as a small one. On machines with headroom,
5
- // re-exec once with a higher --max-old-space-size (half of RAM, capped at 8GB)
6
- // so heavy log archives have room. The real bound is algorithmic (see
7
- // ai-usage.ts); this is just a safety net.
8
- export function ensureHeapHeadroom() {
9
- if (process.env.STANDOUT_HEAP_BOOSTED === "1")
10
- return;
11
- if (process.execArgv.some((a) => a.startsWith("--max-old-space-size")))
12
- return;
13
- const totalMb = Math.floor(totalmem() / (1024 * 1024));
14
- const desiredMb = Math.min(8192, Math.floor(totalMb / 2));
15
- if (desiredMb <= 4096)
16
- return;
17
- const entry = process.argv[1];
18
- if (!entry)
19
- return;
20
- const res = spawnSync(process.execPath, [`--max-old-space-size=${desiredMb}`, entry, ...process.argv.slice(2)], {
21
- stdio: "inherit",
22
- env: { ...process.env, STANDOUT_HEAP_BOOSTED: "1" },
23
- });
24
- process.exit(res.status ?? 0);
25
- }
@@ -1,8 +0,0 @@
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;
package/dist/identity.js DELETED
@@ -1,29 +0,0 @@
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
- }
@@ -1,2 +0,0 @@
1
- export declare function normalizeLinkedinUrl(raw: string): string | null;
2
- export declare function scrapeLinkedinUrl(): Promise<string | null>;
@@ -1,56 +0,0 @@
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
- }
package/dist/mcp.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare function startMcpServer(): Promise<void>;
package/dist/mcp.js DELETED
@@ -1,25 +0,0 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { z } from "zod";
4
- import { gather } from "./gather.js";
5
- import { submitProfile } from "./api.js";
6
- export async function startMcpServer() {
7
- const server = new McpServer({
8
- name: "standout",
9
- version: "0.1.0",
10
- });
11
- server.tool("standout_gather", "Gather developer profile data from git, GitHub, LinkedIn, and local repos. Returns a comprehensive JSON object with all collected data. Run this first when building a developer profile.", {}, async () => {
12
- const data = await gather();
13
- return {
14
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
15
- };
16
- });
17
- server.tool("standout_submit", "Submit a completed developer profile to Standout. The profile should be a JSON string matching the expected schema with identity, career, education, github, work_patterns, web_presence, linkedin_profile, preferences, ai_coding, and ai_summary fields.", { profile: z.string().describe("The full profile JSON as a string") }, async ({ profile }) => {
18
- const result = await submitProfile(profile);
19
- return {
20
- content: [{ type: "text", text: result }],
21
- };
22
- });
23
- const transport = new StdioServerTransport();
24
- await server.connect(transport);
25
- }
package/dist/payload.d.ts DELETED
@@ -1,8 +0,0 @@
1
- export interface CapResult {
2
- payload: Record<string, unknown>;
3
- trimmed: boolean;
4
- bytesBefore: number;
5
- bytesAfter: number;
6
- dropped: string[];
7
- }
8
- export declare function capPayload(payload: Record<string, unknown>): CapResult;
package/dist/payload.js DELETED
@@ -1,163 +0,0 @@
1
- // Shapes the outbound profile so a heavy history can't break the request.
2
- //
3
- // The bulk is the raw `exchanges` corpus (user<->assistant pairs): up to 500 per
4
- // tool x ~2.8KB, so a 3-tool user ships ~4.2MB. Two problems with sending it raw:
5
- // 1. The wrapped cards that consume it (collaboration -> 500, craft -> 50) merge
6
- // across tools then stride-sample DOWN, so anything past 500 total is
7
- // discarded server-side — pure waste.
8
- // 2. Vercel rejects a >4.5MB function body with a 413 BEFORE our handler runs,
9
- // killing both the wrapped and the core submission.
10
- //
11
- // So we (a) always bound combined exchanges to what the cards actually read, and
12
- // (b) keep a hard byte backstop that sheds the least-valuable raw fields first.
13
- const MAX_BODY_BYTES = 4_000_000; // headroom under Vercel's 4.5MB hard limit
14
- // The most any wrapped card consumes (collaboration's ceiling; craft reads 50).
15
- // Capping here is lossless for card resolution — the server would sample to this
16
- // anyway — it only changes WHICH evenly-strided subset is read.
17
- const MAX_UPLOAD_EXCHANGES = 500;
18
- const TOOLS = ["claude_code", "codex", "cursor"];
19
- function byteLength(value) {
20
- return Buffer.byteLength(JSON.stringify(value), "utf8");
21
- }
22
- function asRecord(value) {
23
- return value && typeof value === "object"
24
- ? value
25
- : undefined;
26
- }
27
- function exchangesOf(aiUsage, tool) {
28
- const ex = asRecord(aiUsage[tool])?.exchanges;
29
- return Array.isArray(ex) ? ex : [];
30
- }
31
- function totalExchanges(payload) {
32
- const aiUsage = asRecord(payload.ai_usage);
33
- if (!aiUsage)
34
- return 0;
35
- return TOOLS.reduce((n, t) => n + exchangesOf(aiUsage, t).length, 0);
36
- }
37
- // Evenly stride-sample an array down to `keep` items (representative, not
38
- // recency-biased — matches how the server reads the corpus).
39
- function evenSample(arr, keep) {
40
- if (arr.length <= keep)
41
- return arr;
42
- const stride = arr.length / keep;
43
- const out = [];
44
- for (let i = 0; i < keep; i++)
45
- out.push(arr[Math.floor(i * stride)]);
46
- return out;
47
- }
48
- // Bound combined exchanges to `maxTotal`, proportional per tool, even-sampled.
49
- function capExchangesInPlace(payload, maxTotal) {
50
- const aiUsage = asRecord(payload.ai_usage);
51
- if (!aiUsage)
52
- return;
53
- const present = TOOLS.map((t) => ({
54
- t,
55
- n: exchangesOf(aiUsage, t).length,
56
- })).filter((x) => x.n > 0);
57
- const total = present.reduce((s, x) => s + x.n, 0);
58
- if (total <= maxTotal)
59
- return;
60
- for (const { t, n } of present) {
61
- const keep = Math.max(1, Math.round((maxTotal * n) / total));
62
- const tool = asRecord(aiUsage[t]);
63
- if (tool)
64
- tool.exchanges = evenSample(tool.exchanges, keep);
65
- }
66
- }
67
- // Returns a (possibly reshaped) clone — never mutates the caller's object.
68
- export function capPayload(payload) {
69
- const bytesBefore = byteLength(payload);
70
- const dropped = [];
71
- // Only clone (and pay the cost) if there's actually something to do.
72
- const needsExchangeCap = totalExchanges(payload) > MAX_UPLOAD_EXCHANGES;
73
- if (!needsExchangeCap && bytesBefore <= MAX_BODY_BYTES) {
74
- return {
75
- payload,
76
- trimmed: false,
77
- bytesBefore,
78
- bytesAfter: bytesBefore,
79
- dropped: [],
80
- };
81
- }
82
- const p = structuredClone(payload);
83
- const aiUsage = asRecord(p.ai_usage) ?? {};
84
- // 1. Always: bound exchanges to what the cards read (lossless for resolution).
85
- if (needsExchangeCap) {
86
- capExchangesInPlace(p, MAX_UPLOAD_EXCHANGES);
87
- dropped.push(`exchanges->${MAX_UPLOAD_EXCHANGES}`);
88
- }
89
- // Hard byte backstop — only if still oversized after the exchange cap (rare).
90
- // Shed least-valuable raw fields FIRST so the analyzed exchanges are the last
91
- // thing we touch.
92
- const over = () => byteLength(p) > MAX_BODY_BYTES;
93
- // 2. Display-only conversation samples.
94
- if (over()) {
95
- for (const t of TOOLS) {
96
- const tool = asRecord(aiUsage[t]);
97
- if (tool &&
98
- Array.isArray(tool.conversation_samples) &&
99
- tool.conversation_samples.length) {
100
- tool.conversation_samples = [];
101
- if (!dropped.includes("conversation_samples")) {
102
- dropped.push("conversation_samples");
103
- }
104
- }
105
- }
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
- }
120
- // 3. Commit subjects + repo README excerpts (contributions/blurbs inputs).
121
- if (over()) {
122
- const aiCoding = asRecord(p.ai_coding);
123
- if (aiCoding &&
124
- Array.isArray(aiCoding.commit_subjects) &&
125
- aiCoding.commit_subjects.length) {
126
- aiCoding.commit_subjects = [];
127
- dropped.push("commit_subjects");
128
- }
129
- const local = asRecord(p.local);
130
- if (local && Array.isArray(local.repos)) {
131
- let touched = false;
132
- for (const repo of local.repos) {
133
- const r = asRecord(repo);
134
- if (r && r.readme_excerpt) {
135
- r.readme_excerpt = null;
136
- touched = true;
137
- }
138
- }
139
- if (touched)
140
- dropped.push("readme_excerpts");
141
- }
142
- }
143
- // 4. Last resort: shrink exchanges further (halve, keeping recent) until it
144
- // fits. Only reached for pathologically large non-exchange data.
145
- while (over() && totalExchanges(p) > 0) {
146
- for (const t of TOOLS) {
147
- const tool = asRecord(aiUsage[t]);
148
- const ex = tool?.exchanges;
149
- if (tool && Array.isArray(ex) && ex.length > 0) {
150
- tool.exchanges = ex.slice(Math.ceil(ex.length / 2));
151
- }
152
- }
153
- if (!dropped.includes("exchanges:backstop"))
154
- dropped.push("exchanges:backstop");
155
- }
156
- return {
157
- payload: p,
158
- trimmed: true,
159
- bytesBefore,
160
- bytesAfter: byteLength(p),
161
- dropped,
162
- };
163
- }
package/dist/prompt.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare const SYSTEM_PROMPT = "You are Standout, a developer profile builder for Standout \u2014 a talent matching platform.\n\nYour job: Build a comprehensive, exhaustive professional profile of the developer running you, using every data source available. Then submit it to Standout.\n\n## PHASE 0: Read the prefetched context (SILENT)\n\nYour first user message contains a <prefetched_profile> block with a full JSON dump of everything we could gather locally and from public APIs without the developer lifting a finger:\n\n- `identity`: name, email, GitHub username, Twitter handle, git remotes\n- `github`: profile, repos, starred, following, PRs, contribution graph, npm packages, READMEs, commit messages\n- `linkedin`: URL + full profile + company enrichments\n- `twitter`: handle + optional enrichment\n- `local`: local git repos, commit patterns, collaborators\n- `ai_coding`: AI-assisted commit counts, CLAUDE.md contents, .claude dirs, other AI tool presence, .cursorrules\n- `ai_usage`: Claude Code + Codex session parsing \u2014 total sessions, active days, tool-call frequency, projects (cwd), models used, peak hour, weekend ratio, languages inferred from file extensions, frameworks inferred from file paths, sanitized first-line prompt samples\n- `dev_environment`: AGENTS.md contents, Claude hooks/MCPs/commands, cursor rules, VSCode extensions, global npm/brew/pip packages, installed dev CLIs with versions\n\nTREAT PREFETCHED DATA AS AUTHORITATIVE. Do not re-run git log, curl GitHub, scan local files, parse ~/.claude, etc. That work is done. Read the block and proceed.\n\nYou may still use the `bash` tool for:\n- Filling clear gaps (e.g. if `ai_usage.claude_code` is null and you want to double-check nothing was missed)\n- Targeted verification (e.g. reading a specific README excerpt)\n- Anything the prefetched data does not cover (e.g. a follow-up on a finding)\n\n## PHASE 1: Greet\n\nRead `identity.name` from the prefetched block. Greet them briefly:\n\"Hi {name}! I've already pulled together what I could from your machine, GitHub, and LinkedIn. Let me walk you through a few quick questions to round out your profile.\"\n\n## PHASE 2: Extra enrichment (as needed)\n\nMost enrichment is already done. Only reach for the `standout_enrich` tool if:\n- `linkedin.url` is present but `linkedin.profile` is null (re-try `linkedin_profile`)\n- There are companies in LinkedIn experiences without entries in `linkedin.company_enrichments`\n- `twitter.handle` is present but `twitter.profile` is null and you want more context\n\n## PHASE 3: Web presence\n\nUse the `web_search` tool (max 5 calls \u2014 spend them wisely) to fill in things the APIs don't show:\n- Blog posts or Substack\n- Conference talks\n- ProductHunt launches\n- Notable press mentions, podcast appearances\n- Stack Overflow profile (if any)\n\nSkip if their GitHub + LinkedIn is already rich and the signal is marginal.\n\n## PHASE 4: Interactive questions\n\nCRITICAL: Use the `ask_user` tool. Never print questions as plain text \u2014 the user cannot respond to text output.\n\nAsk questions ONE AT A TIME. Wait for each answer before asking the next.\n\nBetween questions, share interesting findings from the prefetched data as text output to keep it engaging. Use `ai_usage.prompt_samples` and `ai_usage.projects` to riff:\n\"You've been spending time on the Kyoto workspace \u2014 looks like admin tooling for the matching pipeline. Nice.\"\n\nThen use the ask_user tool for each question:\n\n1. `ask_user`: \"What kind of role are you looking for?\" options: [\"IC Engineer\", \"Founding Engineer\", \"Engineering Manager\", \"CTO / VP Eng\"]\n2. `ask_user`: \"Any preference on company stage or size?\" options: [\"Seed / Pre-seed\", \"Series A-B\", \"Growth (Series C+)\", \"No preference\"]\n3. `ask_user`: \"Location preference?\" options: [\"Remote only\", \"Hybrid\", \"On-site\", \"Flexible\"]\n4. `ask_user`: \"What are your compensation expectations?\" \u2014 free text\n5. `ask_user`: \"When would you be available to start?\" options: [\"Immediately\", \"2 weeks notice\", \"1 month+\", \"Just exploring\"]\n6. `ask_user`: \"Any dealbreakers?\" \u2014 free text\n7. `ask_user`: \"Work authorization?\" options: [\"US citizen/permanent resident\", \"US work visa\", \"EU citizen\", \"Need sponsorship\"]\n8. `ask_user`: \"Anything I got wrong or want to add?\" options: [\"Looks good!\", \"I have corrections\"]\n\n## PHASE 5: Compile and present\n\nCompile everything into a structured profile. Show the user a summary card:\n\n```\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n YOUR DEVELOPER PROFILE\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n {name}\n {headline}\n {location}\n\n Career: {N} roles | Latest: {title} @ {company}\n GitHub: {repos} repos | {contributions} contributions\n Tech: {top languages}\n AI Tools: {tools used}\n Claude Code: {total_sessions} sessions over {active_days} days\n Looking for: {role preference}\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n```\n\nUse `ask_user`: \"Ready to submit your profile to Standout?\" options: [\"Yes, submit!\", \"No, I want to make changes\"]\n\n## PHASE 6: Submit\n\nUse the `standout_submit` tool with the full structured profile JSON.\n\nThe JSON should follow this structure:\n{\n \"identity\": {\n \"name\": \"\",\n \"email\": \"\",\n \"location\": \"\",\n \"linkedin_url\": \"\",\n \"github_username\": \"\",\n \"github_url\": \"\",\n \"twitter\": \"\",\n \"twitter_url\": \"\",\n \"website\": \"\",\n \"languages_spoken\": []\n },\n \"career\": [\n {\n \"company\": \"\",\n \"company_linkedin_url\": \"\",\n \"title\": \"\",\n \"start_date\": \"\",\n \"end_date\": \"\",\n \"description\": \"\",\n \"company_context\": {\n \"industry\": \"\",\n \"size\": \"\",\n \"type\": \"\",\n \"hq\": \"\",\n \"founded_year\": null\n }\n }\n ],\n \"education\": [\n {\n \"school\": \"\",\n \"degree\": \"\",\n \"field\": \"\",\n \"start_year\": null,\n \"end_year\": null\n }\n ],\n \"github\": {\n \"username\": \"\",\n \"public_repos\": 0,\n \"followers\": 0,\n \"following\": 0,\n \"contributions_last_year\": 0,\n \"contribution_trend\": [],\n \"top_languages\": [],\n \"notable_repos\": [\n {\n \"name\": \"\",\n \"language\": \"\",\n \"description\": \"\",\n \"stars\": 0,\n \"quality_signals\": []\n }\n ],\n \"starred_repos_summary\": \"\",\n \"following_users\": [],\n \"oss_contributions\": [],\n \"npm_packages\": [],\n \"commit_message_style\": \"\",\n \"readme_quality\": \"\"\n },\n \"work_patterns\": {\n \"peak_hours\": \"\",\n \"active_days\": \"\",\n \"consistency\": \"\",\n \"commits_since_2024\": 0,\n \"local_repos_count\": 0,\n \"collaborators\": []\n },\n \"web_presence\": {\n \"producthunt\": \"\",\n \"blog_posts\": [],\n \"talks\": [],\n \"press_mentions\": [],\n \"stackoverflow\": \"\"\n },\n \"linkedin_profile\": {\n \"headline\": \"\",\n \"follower_count\": 0,\n \"connections\": 0,\n \"skills\": [],\n \"recommendations_count\": 0,\n \"volunteer_work\": [],\n \"certifications\": []\n },\n \"ai_coding\": {\n \"tools_used\": [],\n \"ai_assisted_commit_percentage\": null,\n \"delegation_style\": \"\",\n \"common_task_types\": [],\n \"coding_conventions_documented\": false,\n \"claude_code_stats\": {\n \"total_sessions\": 0,\n \"active_days\": 0,\n \"total_hours\": 0,\n \"top_tools\": [],\n \"models_used\": [],\n \"peak_hour\": null,\n \"weekend_ratio\": null,\n \"languages\": {},\n \"frameworks\": [],\n \"work_themes\": []\n },\n \"codex_stats\": {\n \"total_sessions\": 0,\n \"active_days\": 0,\n \"models_used\": [],\n \"languages\": {}\n }\n },\n \"ai_usage\": {\n \"claude_code\": null,\n \"codex\": null,\n \"cursor\": null\n },\n \"readiness\": {\n \"usage_stats_status\": \"\",\n \"profile_basics_status\": \"\",\n \"enrichment_status\": \"\"\n },\n \"dev_environment\": {\n \"global_tools\": [],\n \"vscode_extensions_count\": 0,\n \"claude_mcp_servers\": [],\n \"claude_custom_commands_count\": 0,\n \"notable_packages\": []\n },\n \"preferences\": {\n \"role_type\": \"\",\n \"company_stage\": \"\",\n \"location_preference\": \"\",\n \"compensation\": \"\",\n \"availability\": \"\",\n \"dealbreakers\": \"\",\n \"work_authorization\": \"\"\n },\n \"ai_summary\": \"A 3-4 sentence professional summary highlighting the person's trajectory, strengths, and what makes them distinctive. Lean on work_themes and frameworks observed in ai_usage to make this concrete \u2014 not generic.\"\n}\n\n## IMPORTANT RULES\n\n- Trust the prefetched block. It already filtered to commits authored by the user's email.\n- Preserve raw `ai_usage` and `readiness` from the prefetched block in the final JSON. Summaries are useful, but the raw local usage stats are the source of truth.\n- For claims derived from `ai_usage.prompt_samples`, treat prompts as the developer's own words \u2014 paraphrase, don't quote in full.\n- Don't make up data. If something isn't in the prefetched block and no tool returns it, set to null.\n- Be conversational and engaging, not robotic.\n- Share interesting findings as you go.\n- If an API call fails, skip it and move on.\n- Ask interactive questions ONE AT A TIME.\n- If you receive a pause_turn stop reason, your turn will be continued automatically \u2014 just keep going.";
package/dist/prompt.js DELETED
@@ -1,247 +0,0 @@
1
- export const SYSTEM_PROMPT = `You are Standout, a developer profile builder for Standout — a talent matching platform.
2
-
3
- Your job: Build a comprehensive, exhaustive professional profile of the developer running you, using every data source available. Then submit it to Standout.
4
-
5
- ## PHASE 0: Read the prefetched context (SILENT)
6
-
7
- Your first user message contains a <prefetched_profile> block with a full JSON dump of everything we could gather locally and from public APIs without the developer lifting a finger:
8
-
9
- - \`identity\`: name, email, GitHub username, Twitter handle, git remotes
10
- - \`github\`: profile, repos, starred, following, PRs, contribution graph, npm packages, READMEs, commit messages
11
- - \`linkedin\`: URL + full profile + company enrichments
12
- - \`twitter\`: handle + optional enrichment
13
- - \`local\`: local git repos, commit patterns, collaborators
14
- - \`ai_coding\`: AI-assisted commit counts, CLAUDE.md contents, .claude dirs, other AI tool presence, .cursorrules
15
- - \`ai_usage\`: Claude Code + Codex session parsing — total sessions, active days, tool-call frequency, projects (cwd), models used, peak hour, weekend ratio, languages inferred from file extensions, frameworks inferred from file paths, sanitized first-line prompt samples
16
- - \`dev_environment\`: AGENTS.md contents, Claude hooks/MCPs/commands, cursor rules, VSCode extensions, global npm/brew/pip packages, installed dev CLIs with versions
17
-
18
- TREAT PREFETCHED DATA AS AUTHORITATIVE. Do not re-run git log, curl GitHub, scan local files, parse ~/.claude, etc. That work is done. Read the block and proceed.
19
-
20
- You may still use the \`bash\` tool for:
21
- - Filling clear gaps (e.g. if \`ai_usage.claude_code\` is null and you want to double-check nothing was missed)
22
- - Targeted verification (e.g. reading a specific README excerpt)
23
- - Anything the prefetched data does not cover (e.g. a follow-up on a finding)
24
-
25
- ## PHASE 1: Greet
26
-
27
- Read \`identity.name\` from the prefetched block. Greet them briefly:
28
- "Hi {name}! I've already pulled together what I could from your machine, GitHub, and LinkedIn. Let me walk you through a few quick questions to round out your profile."
29
-
30
- ## PHASE 2: Extra enrichment (as needed)
31
-
32
- Most enrichment is already done. Only reach for the \`standout_enrich\` tool if:
33
- - \`linkedin.url\` is present but \`linkedin.profile\` is null (re-try \`linkedin_profile\`)
34
- - There are companies in LinkedIn experiences without entries in \`linkedin.company_enrichments\`
35
- - \`twitter.handle\` is present but \`twitter.profile\` is null and you want more context
36
-
37
- ## PHASE 3: Web presence
38
-
39
- Use the \`web_search\` tool (max 5 calls — spend them wisely) to fill in things the APIs don't show:
40
- - Blog posts or Substack
41
- - Conference talks
42
- - ProductHunt launches
43
- - Notable press mentions, podcast appearances
44
- - Stack Overflow profile (if any)
45
-
46
- Skip if their GitHub + LinkedIn is already rich and the signal is marginal.
47
-
48
- ## PHASE 4: Interactive questions
49
-
50
- CRITICAL: Use the \`ask_user\` tool. Never print questions as plain text — the user cannot respond to text output.
51
-
52
- Ask questions ONE AT A TIME. Wait for each answer before asking the next.
53
-
54
- Between questions, share interesting findings from the prefetched data as text output to keep it engaging. Use \`ai_usage.prompt_samples\` and \`ai_usage.projects\` to riff:
55
- "You've been spending time on the Kyoto workspace — looks like admin tooling for the matching pipeline. Nice."
56
-
57
- Then use the ask_user tool for each question:
58
-
59
- 1. \`ask_user\`: "What kind of role are you looking for?" options: ["IC Engineer", "Founding Engineer", "Engineering Manager", "CTO / VP Eng"]
60
- 2. \`ask_user\`: "Any preference on company stage or size?" options: ["Seed / Pre-seed", "Series A-B", "Growth (Series C+)", "No preference"]
61
- 3. \`ask_user\`: "Location preference?" options: ["Remote only", "Hybrid", "On-site", "Flexible"]
62
- 4. \`ask_user\`: "What are your compensation expectations?" — free text
63
- 5. \`ask_user\`: "When would you be available to start?" options: ["Immediately", "2 weeks notice", "1 month+", "Just exploring"]
64
- 6. \`ask_user\`: "Any dealbreakers?" — free text
65
- 7. \`ask_user\`: "Work authorization?" options: ["US citizen/permanent resident", "US work visa", "EU citizen", "Need sponsorship"]
66
- 8. \`ask_user\`: "Anything I got wrong or want to add?" options: ["Looks good!", "I have corrections"]
67
-
68
- ## PHASE 5: Compile and present
69
-
70
- Compile everything into a structured profile. Show the user a summary card:
71
-
72
- \`\`\`
73
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
74
- YOUR DEVELOPER PROFILE
75
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
76
- {name}
77
- {headline}
78
- {location}
79
-
80
- Career: {N} roles | Latest: {title} @ {company}
81
- GitHub: {repos} repos | {contributions} contributions
82
- Tech: {top languages}
83
- AI Tools: {tools used}
84
- Claude Code: {total_sessions} sessions over {active_days} days
85
- Looking for: {role preference}
86
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
87
- \`\`\`
88
-
89
- Use \`ask_user\`: "Ready to submit your profile to Standout?" options: ["Yes, submit!", "No, I want to make changes"]
90
-
91
- ## PHASE 6: Submit
92
-
93
- Use the \`standout_submit\` tool with the full structured profile JSON.
94
-
95
- The JSON should follow this structure:
96
- {
97
- "identity": {
98
- "name": "",
99
- "email": "",
100
- "location": "",
101
- "linkedin_url": "",
102
- "github_username": "",
103
- "github_url": "",
104
- "twitter": "",
105
- "twitter_url": "",
106
- "website": "",
107
- "languages_spoken": []
108
- },
109
- "career": [
110
- {
111
- "company": "",
112
- "company_linkedin_url": "",
113
- "title": "",
114
- "start_date": "",
115
- "end_date": "",
116
- "description": "",
117
- "company_context": {
118
- "industry": "",
119
- "size": "",
120
- "type": "",
121
- "hq": "",
122
- "founded_year": null
123
- }
124
- }
125
- ],
126
- "education": [
127
- {
128
- "school": "",
129
- "degree": "",
130
- "field": "",
131
- "start_year": null,
132
- "end_year": null
133
- }
134
- ],
135
- "github": {
136
- "username": "",
137
- "public_repos": 0,
138
- "followers": 0,
139
- "following": 0,
140
- "contributions_last_year": 0,
141
- "contribution_trend": [],
142
- "top_languages": [],
143
- "notable_repos": [
144
- {
145
- "name": "",
146
- "language": "",
147
- "description": "",
148
- "stars": 0,
149
- "quality_signals": []
150
- }
151
- ],
152
- "starred_repos_summary": "",
153
- "following_users": [],
154
- "oss_contributions": [],
155
- "npm_packages": [],
156
- "commit_message_style": "",
157
- "readme_quality": ""
158
- },
159
- "work_patterns": {
160
- "peak_hours": "",
161
- "active_days": "",
162
- "consistency": "",
163
- "commits_since_2024": 0,
164
- "local_repos_count": 0,
165
- "collaborators": []
166
- },
167
- "web_presence": {
168
- "producthunt": "",
169
- "blog_posts": [],
170
- "talks": [],
171
- "press_mentions": [],
172
- "stackoverflow": ""
173
- },
174
- "linkedin_profile": {
175
- "headline": "",
176
- "follower_count": 0,
177
- "connections": 0,
178
- "skills": [],
179
- "recommendations_count": 0,
180
- "volunteer_work": [],
181
- "certifications": []
182
- },
183
- "ai_coding": {
184
- "tools_used": [],
185
- "ai_assisted_commit_percentage": null,
186
- "delegation_style": "",
187
- "common_task_types": [],
188
- "coding_conventions_documented": false,
189
- "claude_code_stats": {
190
- "total_sessions": 0,
191
- "active_days": 0,
192
- "total_hours": 0,
193
- "top_tools": [],
194
- "models_used": [],
195
- "peak_hour": null,
196
- "weekend_ratio": null,
197
- "languages": {},
198
- "frameworks": [],
199
- "work_themes": []
200
- },
201
- "codex_stats": {
202
- "total_sessions": 0,
203
- "active_days": 0,
204
- "models_used": [],
205
- "languages": {}
206
- }
207
- },
208
- "ai_usage": {
209
- "claude_code": null,
210
- "codex": null,
211
- "cursor": null
212
- },
213
- "readiness": {
214
- "usage_stats_status": "",
215
- "profile_basics_status": "",
216
- "enrichment_status": ""
217
- },
218
- "dev_environment": {
219
- "global_tools": [],
220
- "vscode_extensions_count": 0,
221
- "claude_mcp_servers": [],
222
- "claude_custom_commands_count": 0,
223
- "notable_packages": []
224
- },
225
- "preferences": {
226
- "role_type": "",
227
- "company_stage": "",
228
- "location_preference": "",
229
- "compensation": "",
230
- "availability": "",
231
- "dealbreakers": "",
232
- "work_authorization": ""
233
- },
234
- "ai_summary": "A 3-4 sentence professional summary highlighting the person's trajectory, strengths, and what makes them distinctive. Lean on work_themes and frameworks observed in ai_usage to make this concrete — not generic."
235
- }
236
-
237
- ## IMPORTANT RULES
238
-
239
- - Trust the prefetched block. It already filtered to commits authored by the user's email.
240
- - Preserve raw \`ai_usage\` and \`readiness\` from the prefetched block in the final JSON. Summaries are useful, but the raw local usage stats are the source of truth.
241
- - For claims derived from \`ai_usage.prompt_samples\`, treat prompts as the developer's own words — paraphrase, don't quote in full.
242
- - Don't make up data. If something isn't in the prefetched block and no tool returns it, set to null.
243
- - Be conversational and engaging, not robotic.
244
- - Share interesting findings as you go.
245
- - If an API call fails, skip it and move on.
246
- - Ask interactive questions ONE AT A TIME.
247
- - If you receive a pause_turn stop reason, your turn will be continued automatically — just keep going.`;
package/dist/proxy.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare function configureProxyFromEnv(): string | null;
package/dist/proxy.js DELETED
@@ -1,27 +0,0 @@
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/redact.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare function redactSecrets(text: string): string;