standout 0.2.0 → 0.5.16

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/ai-usage.js CHANGED
@@ -726,7 +726,8 @@ async function parseCodexFileTargets(filePath, targetInputs) {
726
726
  recordConversationUserTurn(target, sessionId, sample, longSample);
727
727
  // Style counters are local-only (no LLM) — measure the
728
728
  // full prompt so a late "?" still counts.
729
- const fullSample = sanitizePrompt(text, Number.POSITIVE_INFINITY) ?? sample;
729
+ const fullSample = sanitizePrompt(text, Number.POSITIVE_INFINITY) ??
730
+ sample;
730
731
  recordConvoStyle(target.raw, fullSample, ts, sessionId);
731
732
  }
732
733
  break;
package/dist/cli.js CHANGED
@@ -144,7 +144,7 @@ async function runAgent(jobId) {
144
144
  apiKey: "proxied-by-standout",
145
145
  baseURL: `${STANDOUT_API_URL}/api/public/agent-chat`,
146
146
  defaultHeaders: {
147
- "User-Agent": "Standout/0.2.0 (node)",
147
+ "User-Agent": "Standout/0.4.0 (node)",
148
148
  },
149
149
  });
150
150
  await confirmPrivacy();
@@ -200,9 +200,6 @@ async function runAgent(jobId) {
200
200
  process.stderr.write(" (couldn't generate wrapped right now — continuing profile setup.)\n\n");
201
201
  }
202
202
  if (!profileChatEnabled()) {
203
- if (wrappedId) {
204
- process.stderr.write(" Profile chat disabled — run with --chat (or STANDOUT_PROFILE_CHAT=1) to build a full profile.\n\n");
205
- }
206
203
  return;
207
204
  }
208
205
  const jobInstruction = jobId
package/dist/gather.js CHANGED
@@ -296,6 +296,8 @@ async function enrichApi(type, value, context) {
296
296
  function sanitize(input) {
297
297
  return input.replace(/[^a-zA-Z0-9@._+\-]/g, "");
298
298
  }
299
+ // Co-author signatures left by AI coding tools (Claude, Copilot, Cursor, etc.).
300
+ const AI_COAUTHOR_RE = /claude|anthropic|copilot|cursor|devin-ai|aider|codeium|windsurf|sourcegraph|tabnine|google-labs-jules/i;
299
301
  function findGitRepos() {
300
302
  const dirs = [
301
303
  "Document",
@@ -749,20 +751,34 @@ export async function gather(options = {}) {
749
751
  gatherLog(options, "Analyzing AI coding tools...\n");
750
752
  let totalCommits = 0;
751
753
  let aiAssistedCommits = 0;
752
- // Sum commits since 2024 across ALL local repos (matching the git email), not
753
- // just the current directory one repo badly under-counts a multi-repo dev.
754
+ // Count commits since 2024 across ALL local repos (matching the git email),
755
+ // deduped by SHA so worktrees/clones of the same repo count once. A commit is
756
+ // AI-assisted when it carries a co-author trailer from a known AI coding tool —
757
+ // matched case-insensitively via git's trailer parser, not a literal grep.
754
758
  if (safeEmail) {
759
+ const fieldSep = String.fromCharCode(0x1f);
760
+ const recordSep = String.fromCharCode(0x1e);
761
+ const allCommitShas = new Set();
762
+ const aiCommitShas = new Set();
755
763
  for (const gitDir of gitRepoPaths.slice(0, 30)) {
756
764
  const repoPath = gitDir.replace(/\/\.git$/, "");
757
- const total = parseInt(execSafe(`git -C "${repoPath}" log --author="${safeEmail}" --format="%H" --since="2024-01-01" | wc -l`), 10) || 0;
758
- if (total === 0)
759
- continue;
760
- totalCommits += total;
761
- // Co-Authored-By at the commit level (not trailer lines: %b is multi-line
762
- // and squash-merges stack several trailers per commit).
763
- aiAssistedCommits +=
764
- parseInt(execSafe(`git -C "${repoPath}" log --author="${safeEmail}" --since="2024-01-01" --grep="Co-Authored-By" --format="%H" | wc -l`), 10) || 0;
765
+ const log = execSafe(`git -C "${repoPath}" log --author="${safeEmail}" --since="2024-01-01" --format="%H%x1f%(trailers:key=Co-authored-by,valueonly)%x1e"`);
766
+ for (const entry of log.split(recordSep)) {
767
+ const trimmed = entry.trim();
768
+ if (!trimmed)
769
+ continue;
770
+ const sepIdx = trimmed.indexOf(fieldSep);
771
+ const sha = sepIdx === -1 ? trimmed : trimmed.slice(0, sepIdx);
772
+ const coauthors = sepIdx === -1 ? "" : trimmed.slice(sepIdx + 1);
773
+ if (!sha)
774
+ continue;
775
+ allCommitShas.add(sha);
776
+ if (coauthors && AI_COAUTHOR_RE.test(coauthors))
777
+ aiCommitShas.add(sha);
778
+ }
765
779
  }
780
+ totalCommits = allCommitShas.size;
781
+ aiAssistedCommits = aiCommitShas.size;
766
782
  }
767
783
  // Recent commit subjects (last 90 days) — feed the "what you shipped" summary.
768
784
  let commitSubjects = [];
package/dist/prompt.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const SYSTEM_PROMPT = "You are Standout, a developer profile builder for the Standout 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.";
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 CHANGED
@@ -1,4 +1,4 @@
1
- export const SYSTEM_PROMPT = `You are Standout, a developer profile builder for the Standout talent matching platform.
1
+ export const SYSTEM_PROMPT = `You are Standout, a developer profile builder for Standout — a talent matching platform.
2
2
 
3
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
4
 
@@ -117,6 +117,7 @@ function parseProficiency(v) {
117
117
  base: num(o.base) ?? o.score,
118
118
  craft_bonus: num(o.craft_bonus) ?? 0,
119
119
  top_dimension: dim,
120
+ score_percentile: num(o.score_percentile),
120
121
  comment: strOrNull(o.comment) ?? "",
121
122
  };
122
123
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,93 @@
1
+ // Dev preview of the proficiency finale card for every score tier.
2
+ // npm run preview:tiers (from packages/agent)
3
+ // One representative profile per tier so you can eyeball figures, copy, and fit.
4
+ import { cardProficiency } from "./render.js";
5
+ import { TIERS } from "./tiers.js";
6
+ const SAMPLES = [
7
+ {
8
+ score: 22,
9
+ intensity: 18,
10
+ consistency: 27,
11
+ craft_bonus: 0,
12
+ zinger: "rome wasn't built in one prompt",
13
+ archetype: "Builder",
14
+ comment: "showing up and chipping away, the loop is forming",
15
+ },
16
+ {
17
+ score: 48,
18
+ intensity: 52,
19
+ consistency: 43,
20
+ craft_bonus: 4,
21
+ zinger: "poking at everything, afraid of nothing",
22
+ archetype: "Full-Stack Generalist",
23
+ comment: "curious prompts, broad reach across the stack",
24
+ },
25
+ {
26
+ score: 68,
27
+ intensity: 74,
28
+ consistency: 60,
29
+ craft_bonus: 7,
30
+ zinger: "the trend line only goes one way",
31
+ archetype: "AI-Native Engineer",
32
+ comment: "you out-shipped most of the field this month",
33
+ },
34
+ {
35
+ score: 81,
36
+ intensity: 83,
37
+ consistency: 71,
38
+ craft_bonus: 13,
39
+ zinger: "you don't prompt, you incant",
40
+ archetype: "Founding Engineer",
41
+ comment: "you prompt with intent and ship like it's a sport",
42
+ },
43
+ {
44
+ score: 90,
45
+ intensity: 93,
46
+ consistency: 82,
47
+ craft_bonus: 15,
48
+ zinger: "ship like it's an olympic sport",
49
+ archetype: "Founding Engineer",
50
+ comment: "raw horsepower, clean prompting, relentless shipping",
51
+ },
52
+ {
53
+ score: 97,
54
+ intensity: 98,
55
+ consistency: 93,
56
+ craft_bonus: 18,
57
+ zinger: "one with the machine, frankly unfair",
58
+ archetype: "Founding Engineer",
59
+ comment: "top of the field on every axis that matters",
60
+ },
61
+ ];
62
+ function fixture(s) {
63
+ return {
64
+ mascot_pose: "default",
65
+ archetype_label: s.archetype,
66
+ zinger: s.zinger,
67
+ share_url: "standout.work/w/a1b2c3",
68
+ cohort_size: 4210,
69
+ rank_summary: {
70
+ sample_size: 4210,
71
+ hours_percentile: s.intensity,
72
+ sessions_percentile: s.intensity,
73
+ active_days_percentile: s.consistency,
74
+ tokens_percentile: s.intensity,
75
+ commit_days_percentile: s.intensity,
76
+ },
77
+ proficiency: {
78
+ score: s.score,
79
+ intensity: s.intensity,
80
+ consistency: s.consistency,
81
+ craft: 70,
82
+ base: s.score - s.craft_bonus,
83
+ craft_bonus: s.craft_bonus,
84
+ top_dimension: "intensity",
85
+ comment: s.comment,
86
+ },
87
+ };
88
+ }
89
+ for (let i = 0; i < SAMPLES.length; i++) {
90
+ process.stdout.write(`\n ${TIERS[i].label} (${TIERS[i].min}+)\n`);
91
+ process.stdout.write(cardProficiency(fixture(SAMPLES[i])));
92
+ process.stdout.write("\n");
93
+ }
@@ -1,4 +1,5 @@
1
1
  import type { WrappedAggregateView } from "./types.js";
2
+ export declare function cardProficiency(view: WrappedAggregateView): string;
2
3
  export interface RenderResult {
3
4
  shared: boolean;
4
5
  cancelled: boolean;
@@ -4,6 +4,7 @@ import gradient from "gradient-string";
4
4
  import { createInterface } from "readline";
5
5
  import { chronoCritterForHour } from "./chrono-critters.js";
6
6
  import { MASCOTS } from "./mascots.js";
7
+ import { tierForScore } from "./tiers.js";
7
8
  const BOX_WIDTH = 56;
8
9
  const SPARK_BARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
9
10
  const TITLE_GRADIENT = gradient(["#ff8a00", "#ff4d6d", "#ad5cff"]);
@@ -85,6 +86,23 @@ function wrapWords(text, width) {
85
86
  function bigNumber(s) {
86
87
  return TITLE_GRADIENT(s);
87
88
  }
89
+ // Inner content width of a box() — BOX_WIDTH minus borders (2) and h-padding (4).
90
+ const INNER_WIDTH = BOX_WIDTH - 6;
91
+ function leftPad(visibleLen) {
92
+ return " ".repeat(Math.max(0, Math.floor((INNER_WIDTH - visibleLen) / 2)));
93
+ }
94
+ function centerText(raw, color) {
95
+ const pad = leftPad(raw.length);
96
+ return pad + (color ? color(raw) : raw);
97
+ }
98
+ // Center a multi-line ASCII figure as a block — one shared left margin keeps the
99
+ // art's internal alignment intact.
100
+ function centerBlock(art, color) {
101
+ const lines = art.split("\n");
102
+ const width = Math.max(...lines.map((l) => l.length));
103
+ const pad = leftPad(width);
104
+ return lines.map((l) => pad + (color ? color(l) : l)).join("\n");
105
+ }
88
106
  function bar(value, max, width) {
89
107
  if (max === 0)
90
108
  return " ".repeat(width);
@@ -462,17 +480,6 @@ function cardTokens(view) {
462
480
  })
463
481
  .join("\n")
464
482
  : chalk.dim(" (no tool breakdown available)");
465
- const sparkline = t.six_month_buckets.length > 0
466
- ? t.six_month_buckets
467
- .map((b) => {
468
- const s = sparklineForValue(b.tokens, t.six_month_buckets);
469
- return chalk.hex("#ad5cff")(s);
470
- })
471
- .join(" ")
472
- : "";
473
- const monthLabels = t.six_month_buckets
474
- .map((b) => b.month.slice(5))
475
- .join(" ");
476
483
  const deltaLine = t.multiplier_vs_prior !== null && t.multiplier_vs_prior >= 1.05
477
484
  ? chalk.hex("#ff8a00")(` ↑ ${t.multiplier_vs_prior.toFixed(1)}× vs last period`)
478
485
  : t.multiplier_vs_prior !== null && t.multiplier_vs_prior < 0.95
@@ -484,11 +491,6 @@ function cardTokens(view) {
484
491
  const costLine = cost > 0
485
492
  ? ` ${chalk.dim("would have cost")} ${TITLE_GRADIENT(`~$${cost.toLocaleString()}`)} ${chalk.dim("at retail API rates")}`
486
493
  : "";
487
- const burnVerdict = t.work_tokens >= 50_000_000
488
- ? "you personally keep the GPUs warm."
489
- : t.work_tokens >= 10_000_000
490
- ? "that's a lot of thinking, outsourced."
491
- : "tokens well spent.";
492
494
  const cachedNote = t.cache_tokens > t.work_tokens
493
495
  ? chalk.dim(" thankfully, most of them were cached :)")
494
496
  : t.cache_tokens > 0
@@ -513,19 +515,11 @@ function cardTokens(view) {
513
515
  : "",
514
516
  "",
515
517
  deltaLine,
516
- sparkline ? ` ${sparkline} ${chalk.dim(monthLabels)}` : "",
517
- "",
518
- ` ${chalk.dim(burnVerdict)}`,
519
518
  ]
520
519
  .filter(Boolean)
521
520
  .join("\n");
522
521
  return box(lines, { borderColor: "yellow" });
523
522
  }
524
- function sparklineForValue(value, buckets) {
525
- const max = Math.max(...buckets.map((b) => b.tokens), 1);
526
- const idx = Math.min(SPARK_BARS.length - 1, Math.floor((value / max) * (SPARK_BARS.length - 1)));
527
- return SPARK_BARS[idx];
528
- }
529
523
  function cardToolRelationship(view) {
530
524
  if (!usageReady(view))
531
525
  return "";
@@ -835,7 +829,7 @@ function card9Reveal(view) {
835
829
  }
836
830
  // Finale: the shareable /100. Fuses the cohort ranking and the persona reveal —
837
831
  // score + standing + mascot + archetype + zinger + the three score bars + callout.
838
- function cardProficiency(view) {
832
+ export function cardProficiency(view) {
839
833
  const mascot = MASCOTS[view.mascot_pose] ?? MASCOTS.default;
840
834
  const archetype = bigNumber(view.archetype_label.toUpperCase());
841
835
  const p = view.proficiency;
@@ -849,56 +843,51 @@ function cardProficiency(view) {
849
843
  ` ${archetype}`,
850
844
  "",
851
845
  ` ${chalk.italic.white(`"${view.zinger}"`)}`,
852
- "",
853
- chalk.dim(" " + view.share_url),
854
846
  ].join("\n"), { borderColor: "yellow" });
855
847
  }
856
- const rank = view.rank_summary;
857
- const bestTop = [
858
- rank.hours_percentile,
859
- rank.tokens_percentile,
860
- rank.commit_days_percentile,
861
- rank.sessions_percentile,
862
- rank.active_days_percentile,
863
- ]
864
- .map(topPercentNumber)
865
- .filter((n) => n !== null)
866
- .sort((a, b) => a - b)[0];
867
- const cohort = bestTop != null && rank.sample_size > 0
868
- ? `top ${bestTop}% of ${rank.sample_size.toLocaleString()} Standout users`
848
+ const scoreTop = topPercentNumber(p.score_percentile ?? null);
849
+ const sampleSize = view.rank_summary.sample_size;
850
+ const cohort = scoreTop != null && sampleSize > 0
851
+ ? `top ${scoreTop}% of ${sampleSize.toLocaleString()} Standout users`
869
852
  : view.cohort_size > 0
870
853
  ? `among ${view.cohort_size.toLocaleString()} Standout users so far`
871
854
  : "";
872
855
  const metricRow = (label, value) => value === null
873
856
  ? null
874
- : ` ${chalk.white(label.padEnd(12))} ${bar(value, 100, 10)} ${chalk.dim(String(value))}`;
857
+ : ` ${chalk.white(label.padEnd(12))} ${bar(value, 100, 10)} ${chalk.dim(String(value))}`;
875
858
  // Craft is shown only as its bonus contribution (+N), never its raw score, so
876
859
  // the headline never looks smaller than a sub-bar it's built from.
877
860
  const bars = [
878
861
  metricRow("intensity", p.intensity),
879
862
  metricRow("consistency", p.consistency),
880
863
  p.craft_bonus > 0
881
- ? ` ${chalk.white("craft".padEnd(12))} ${chalk.hex("#ff8a00")(`+ ${p.craft_bonus}`)}${chalk.dim(" clean prompting")}`
864
+ ? ` ${chalk.white("craft".padEnd(12))} ${chalk.hex("#ff8a00")(`+${p.craft_bonus}`)} ${chalk.dim("clean prompting")}`
882
865
  : null,
883
866
  ].filter((r) => r !== null);
867
+ // Pre-wrap variable-length copy: a line wider than the box makes boxen re-wrap
868
+ // the WHOLE card through wrap-ansi, which trims the leading spaces our centering
869
+ // relies on. Keeping every line within INNER_WIDTH avoids that.
870
+ const zingerLines = wrapWords(`"${view.zinger}"`, INNER_WIDTH).map((l) => centerText(l, chalk.italic.white));
871
+ const commentLines = p.comment
872
+ ? wrapWords(p.comment, INNER_WIDTH - 4).map((l, i) => i === 0
873
+ ? ` ${chalk.hex("#ff8a00")(">")} ${chalk.white(l)}`
874
+ : ` ${chalk.white(l)}`)
875
+ : [];
876
+ // The tier figure is the hero of the finale — it replaces the mascot pose here
877
+ // so the card gains a reward reveal without growing taller.
878
+ const tier = tierForScore(p.score);
884
879
  const lines = [
885
- chalk.dim(" YOUR AI PROFICIENCY"),
886
- "",
887
- ` ${bigNumber(`${p.score} / 100`)}`,
888
- cohort ? ` ${chalk.dim(cohort)}` : null,
880
+ centerBlock(tier.art, chalk.hex("#ff8a00")),
889
881
  "",
890
- chalk.hex("#ff8a00")(mascot),
882
+ centerText(`── ${tier.label} ──`, bigNumber),
883
+ centerText(`${p.score} / 100`, bigNumber),
884
+ cohort ? centerText(cohort, chalk.dim) : null,
891
885
  "",
892
- ` ${archetype}`,
893
- ` ${chalk.italic.white(`"${view.zinger}"`)}`,
886
+ centerText(view.archetype_label, chalk.bold.hex("#ad5cff")),
887
+ ...zingerLines,
894
888
  "",
895
889
  ...bars,
896
- "",
897
- p.comment
898
- ? ` ${chalk.hex("#ff8a00")(">")} ${chalk.white(p.comment)}`
899
- : null,
900
- p.comment ? "" : null,
901
- chalk.dim(" " + view.share_url),
890
+ ...commentLines,
902
891
  ].filter((l) => l !== null);
903
892
  return box(lines.join("\n"), { borderColor: "yellow" });
904
893
  }
@@ -0,0 +1,9 @@
1
+ export type TierKey = "still_figuring" | "curious_cat" | "rising_star" | "prompt_wizard" | "escape_velocity" | "unicorn";
2
+ export interface Tier {
3
+ key: TierKey;
4
+ label: string;
5
+ min: number;
6
+ art: string;
7
+ }
8
+ export declare const TIERS: Tier[];
9
+ export declare function tierForScore(score: number): Tier;
@@ -0,0 +1,73 @@
1
+ // Score-tier ladder for the headline /100. Pure function of the score so it
2
+ // works on older payloads (no server field needed) and stays unit-testable.
3
+ // Each tier carries an ASCII figure shown as the hero of the finale card.
4
+ const UNICORN = ` ,.. /
5
+ ,' ';
6
+ ,,.__ _,' /'; .
7
+ :',' ~~~~ '. '~
8
+ ' ( ) )::,
9
+ '. '..=----=..-~ .;'
10
+ ' ;' :: ':. '"
11
+ (: ': ;)
12
+ \\\\ '" ./
13
+ '" '"`;
14
+ const ROCKETSHIP = ` .
15
+ .'.
16
+ |o|
17
+ .'o'.
18
+ |.-.|
19
+ ' '
20
+ ( )
21
+ )
22
+ ( )`;
23
+ const WIZARD = ` (\\. \\ ,/)
24
+ \\( |\\ )/
25
+ //\\ | \\ /\\\\
26
+ (/ /\\_#oo#_/\\ \\)
27
+ \\/\\ #### /\\/
28
+ \`##'`;
29
+ const RISING_STAR = ` .
30
+ ,O,
31
+ ,OOO,
32
+ 'oooooOOOOOooooo'
33
+ \`OOOOOOOOOOO\`
34
+ \`OOOOOOO\`
35
+ OOOO'OOOO
36
+ OOO' 'OOO
37
+ O' 'O`;
38
+ const CURIOUS_CAT = ` |\\__/,| (\`\\
39
+ |_ _ |.--.) )
40
+ ( T ) /
41
+ (((^_(((/(((_/`;
42
+ const GINGERBREAD = ` ,--.
43
+ _(*_*)_
44
+ (_ o _)
45
+ / o \\
46
+ (_/ \\_)`;
47
+ // Ordered low → high; tierForScore walks from the top down.
48
+ export const TIERS = [
49
+ {
50
+ key: "still_figuring",
51
+ label: "STILL FIGURING IT OUT",
52
+ min: 1,
53
+ art: GINGERBREAD,
54
+ },
55
+ { key: "curious_cat", label: "CURIOUS CAT", min: 35, art: CURIOUS_CAT },
56
+ { key: "rising_star", label: "RISING STAR", min: 60, art: RISING_STAR },
57
+ { key: "prompt_wizard", label: "PROMPT WIZARD", min: 77, art: WIZARD },
58
+ {
59
+ key: "escape_velocity",
60
+ label: "ESCAPE VELOCITY",
61
+ min: 86,
62
+ art: ROCKETSHIP,
63
+ },
64
+ { key: "unicorn", label: "UNICORN", min: 95, art: UNICORN },
65
+ ];
66
+ export function tierForScore(score) {
67
+ const s = Number.isFinite(score) ? score : 0;
68
+ for (let i = TIERS.length - 1; i >= 0; i--) {
69
+ if (s >= TIERS[i].min)
70
+ return TIERS[i];
71
+ }
72
+ return TIERS[0];
73
+ }
@@ -163,6 +163,7 @@ export interface ProficiencyView {
163
163
  base: number;
164
164
  craft_bonus: number;
165
165
  top_dimension: "intensity" | "consistency" | "craft";
166
+ score_percentile: number | null;
166
167
  comment: string;
167
168
  }
168
169
  export interface CollaborationView {
@@ -4,7 +4,7 @@ const HEADERS = {
4
4
  "Content-Type": "application/json",
5
5
  // Vercel BotID flagged Anthropic/JS as bot traffic; same goes for default
6
6
  // node-fetch UAs on certain paths. Set a clear identity.
7
- "User-Agent": "Standout/0.2.0 (node)",
7
+ "User-Agent": "Standout/0.4.0 (node)",
8
8
  };
9
9
  export async function createWrapped(args) {
10
10
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "standout",
3
- "version": "0.2.0",
3
+ "version": "0.5.16",
4
4
  "description": "Build your developer profile with AI. One command, zero friction.",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",
@@ -20,6 +20,7 @@
20
20
  "build": "tsc",
21
21
  "dev": "tsx src/cli.ts",
22
22
  "test": "tsx --test tests/*.test.ts",
23
+ "preview:tiers": "tsx src/wrapped/preview.ts",
23
24
  "typecheck": "tsc --noEmit",
24
25
  "prepublishOnly": "npm run build"
25
26
  },