standout 0.1.0 → 0.5.15

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 (45) hide show
  1. package/README.md +2 -1
  2. package/dist/ai-usage.d.ts +164 -0
  3. package/dist/ai-usage.js +1303 -0
  4. package/dist/api.d.ts +10 -0
  5. package/dist/api.js +86 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +351 -0
  8. package/dist/cursor.d.ts +54 -0
  9. package/dist/cursor.js +487 -0
  10. package/dist/dev-env.d.ts +28 -0
  11. package/dist/dev-env.js +264 -0
  12. package/dist/gather.d.ts +81 -0
  13. package/dist/gather.js +885 -0
  14. package/dist/mcp.d.ts +1 -0
  15. package/dist/mcp.js +25 -0
  16. package/dist/prompt.d.ts +1 -0
  17. package/dist/prompt.js +247 -0
  18. package/dist/redact.d.ts +1 -0
  19. package/dist/redact.js +37 -0
  20. package/dist/tools.d.ts +4 -0
  21. package/dist/tools.js +182 -0
  22. package/dist/twitter-scrape.d.ts +1 -0
  23. package/dist/twitter-scrape.js +267 -0
  24. package/dist/wrapped/aggregate.d.ts +6 -0
  25. package/dist/wrapped/aggregate.js +792 -0
  26. package/dist/wrapped/chrono-critters.d.ts +8 -0
  27. package/dist/wrapped/chrono-critters.js +32 -0
  28. package/dist/wrapped/index.d.ts +3 -0
  29. package/dist/wrapped/index.js +2 -0
  30. package/dist/wrapped/mascots.d.ts +2 -0
  31. package/dist/wrapped/mascots.js +35 -0
  32. package/dist/wrapped/preview.d.ts +1 -0
  33. package/dist/wrapped/preview.js +93 -0
  34. package/dist/wrapped/render.d.ts +14 -0
  35. package/dist/wrapped/render.js +996 -0
  36. package/dist/wrapped/tiers.d.ts +9 -0
  37. package/dist/wrapped/tiers.js +73 -0
  38. package/dist/wrapped/types.d.ts +184 -0
  39. package/dist/wrapped/types.js +4 -0
  40. package/dist/wrapped-client.d.ts +10 -0
  41. package/dist/wrapped-client.js +36 -0
  42. package/dist/wrapped-share.d.ts +3 -0
  43. package/dist/wrapped-share.js +27 -0
  44. package/package.json +35 -8
  45. package/bin/cli.mjs +0 -30
package/dist/api.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export declare const STANDOUT_API_URL: string;
2
+ export declare function enrichProfile(type: string, value: string): Promise<string>;
3
+ export interface SubmitProfileOptions {
4
+ prefetchedProfile?: Record<string, unknown>;
5
+ wrappedId?: string | null;
6
+ }
7
+ export declare function mergeSubmissionProfile(profileData: Record<string, unknown>, prefetchedProfile?: Record<string, unknown>): Record<string, unknown>;
8
+ export declare function linkWrappedToSubmission(wrappedId: string, submissionId: string): Promise<void>;
9
+ export declare function markWrappedShared(wrappedId: string): Promise<void>;
10
+ export declare function submitProfile(profile: string, options?: SubmitProfileOptions): Promise<string>;
package/dist/api.js ADDED
@@ -0,0 +1,86 @@
1
+ export const STANDOUT_API_URL = process.env.STANDOUT_API_URL || "https://standout.work";
2
+ export async function enrichProfile(type, value) {
3
+ try {
4
+ const res = await fetch(`${STANDOUT_API_URL}/api/public/agent-enrich`, {
5
+ method: "POST",
6
+ headers: { "Content-Type": "application/json" },
7
+ body: JSON.stringify({ type, value }),
8
+ });
9
+ if (!res.ok) {
10
+ const error = await res.text();
11
+ return `Enrichment failed (${res.status}): ${error}`;
12
+ }
13
+ return JSON.stringify(await res.json(), null, 2);
14
+ }
15
+ catch (error) {
16
+ return `Enrichment error: ${error instanceof Error ? error.message : String(error)}`;
17
+ }
18
+ }
19
+ export function mergeSubmissionProfile(profileData, prefetchedProfile) {
20
+ if (!prefetchedProfile)
21
+ return profileData;
22
+ return {
23
+ ...profileData,
24
+ // The LLM compiles a recruiter-facing profile and can accidentally omit
25
+ // raw gathered datasets. Preserve the authoritative local scan payloads.
26
+ ai_usage: profileData.ai_usage ?? prefetchedProfile.ai_usage,
27
+ readiness: profileData.readiness ?? prefetchedProfile.readiness,
28
+ local: profileData.local ?? prefetchedProfile.local,
29
+ twitter: profileData.twitter ?? prefetchedProfile.twitter,
30
+ linkedin: profileData.linkedin ?? prefetchedProfile.linkedin,
31
+ ai_coding: {
32
+ ...(prefetchedProfile.ai_coding ?? {}),
33
+ ...(profileData.ai_coding ?? {}),
34
+ },
35
+ dev_environment: profileData.dev_environment ?? prefetchedProfile.dev_environment,
36
+ };
37
+ }
38
+ export async function linkWrappedToSubmission(wrappedId, submissionId) {
39
+ try {
40
+ await fetch(`${STANDOUT_API_URL}/api/public/wrapped/${wrappedId}`, {
41
+ method: "PATCH",
42
+ headers: { "Content-Type": "application/json" },
43
+ body: JSON.stringify({ submission_id: submissionId }),
44
+ });
45
+ }
46
+ catch {
47
+ // Non-critical: the profile submission already succeeded.
48
+ }
49
+ }
50
+ export async function markWrappedShared(wrappedId) {
51
+ try {
52
+ await fetch(`${STANDOUT_API_URL}/api/public/wrapped/${wrappedId}`, {
53
+ method: "PATCH",
54
+ headers: { "Content-Type": "application/json" },
55
+ body: JSON.stringify({ shared: true }),
56
+ });
57
+ }
58
+ catch {
59
+ // Non-critical analytics signal.
60
+ }
61
+ }
62
+ export async function submitProfile(profile, options = {}) {
63
+ try {
64
+ const profileData = JSON.parse(profile);
65
+ const jobId = process.env.STANDOUT_JOB_ID;
66
+ const mergedProfile = mergeSubmissionProfile(profileData, options.prefetchedProfile);
67
+ const payload = jobId ? { ...mergedProfile, jobId } : mergedProfile;
68
+ const res = await fetch(`${STANDOUT_API_URL}/api/public/agent-submit`, {
69
+ method: "POST",
70
+ headers: { "Content-Type": "application/json" },
71
+ body: JSON.stringify(payload),
72
+ });
73
+ if (!res.ok) {
74
+ const error = await res.text();
75
+ return `Submission failed (${res.status}): ${error}`;
76
+ }
77
+ const result = (await res.json());
78
+ if (options.wrappedId && result.id) {
79
+ await linkWrappedToSubmission(options.wrappedId, result.id);
80
+ }
81
+ return `Profile submitted successfully! ID: ${result.id}`;
82
+ }
83
+ catch (error) {
84
+ return `Submission error: ${error instanceof Error ? error.message : String(error)}`;
85
+ }
86
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env node
2
+ import Anthropic from "@anthropic-ai/sdk";
3
+ import chalk from "chalk";
4
+ import { createInterface } from "readline";
5
+ import { markWrappedShared, STANDOUT_API_URL } from "./api.js";
6
+ import { gatherFullEnrichment, gatherProfileBasics, gatherUsageStats, } from "./gather.js";
7
+ import { startMcpServer } from "./mcp.js";
8
+ import { SYSTEM_PROMPT } from "./prompt.js";
9
+ import { TOOLS, executeTool } from "./tools.js";
10
+ import { renderWrappedAll } from "./wrapped/render.js";
11
+ import { createWrapped } from "./wrapped-client.js";
12
+ import { openShareToX } from "./wrapped-share.js";
13
+ const MAX_TURNS = 40;
14
+ // The post-wrapped agent chat (interactive profile-building Q&A) is OFF by
15
+ // default so the command runs to completion non-interactively (e.g. invoked
16
+ // from a Claude Code Bash call with no TTY). Re-enable by setting
17
+ // STANDOUT_PROFILE_CHAT=1 or passing --chat.
18
+ function profileChatEnabled() {
19
+ const env = process.env.STANDOUT_PROFILE_CHAT;
20
+ if (env === "1" || env === "true")
21
+ return true;
22
+ return process.argv.slice(2).includes("--chat");
23
+ }
24
+ async function confirmPrivacy() {
25
+ process.stderr.write("\nDiscover your Claude Code, Codex, and Cursor stats, then see how you rank.\n" +
26
+ "You'll see your cards first, then review before submitting.\n\n" +
27
+ "By continuing, you agree to Standout's Terms and Privacy Policy:\n" +
28
+ "https://standout.work/terms\n" +
29
+ "https://standout.work/privacy-policy\n\n");
30
+ if (process.stdin.isTTY) {
31
+ const answer = await new Promise((resolve) => {
32
+ const iface = createInterface({
33
+ input: process.stdin,
34
+ output: process.stderr,
35
+ });
36
+ iface.question("Continue? [Y/n] ", (ans) => {
37
+ iface.close();
38
+ resolve(ans.trim().toLowerCase());
39
+ });
40
+ });
41
+ if (answer === "n" || answer === "no") {
42
+ process.stderr.write("Cancelled.\n");
43
+ process.exit(0);
44
+ }
45
+ }
46
+ else {
47
+ process.stderr.write("(non-interactive, proceeding)\n");
48
+ }
49
+ }
50
+ function ensureInteractive(reason) {
51
+ if (process.stdin.isTTY)
52
+ return;
53
+ console.error("\n Standout needs an interactive terminal.");
54
+ console.error(` ${reason} requires keyboard input that this environment can't deliver.`);
55
+ console.error("");
56
+ console.error(" Common fixes:");
57
+ console.error(" • Run from a regular terminal window, not from inside another agent or CI.");
58
+ console.error(" • Don't pipe stdin (e.g. `echo y |`) or run in a containerized shell.");
59
+ console.error(" • If you're inside Claude Code or another wrapping tool, exit it and");
60
+ console.error(" run standout directly in your shell.");
61
+ console.error("");
62
+ console.error(" npx standout@latest");
63
+ console.error("");
64
+ process.exit(1);
65
+ }
66
+ function startLoadingLine(label) {
67
+ if (!process.stderr.isTTY) {
68
+ process.stderr.write(` ${label}...\n`);
69
+ return () => { };
70
+ }
71
+ // No blank frame: dots must be visible on the very first render, otherwise the
72
+ // line shows for ~140ms with nothing and looks like it's slow to start.
73
+ const frames = [". ", ".. ", "...", " ..", " ."];
74
+ let frame = 0;
75
+ const render = () => {
76
+ const dots = frames[frame % frames.length];
77
+ const accent = chalk.hex("#ad5cff")(dots);
78
+ const text = chalk.white(label);
79
+ process.stderr.write(`\r\x1b[2K ${accent} ${text}`);
80
+ frame += 1;
81
+ };
82
+ render();
83
+ const timer = setInterval(render, 120);
84
+ return () => {
85
+ clearInterval(timer);
86
+ process.stderr.write("\r\x1b[2K");
87
+ };
88
+ }
89
+ async function askLine(prompt) {
90
+ if (!process.stdin.isTTY)
91
+ return "y";
92
+ return new Promise((resolve) => {
93
+ const iface = createInterface({
94
+ input: process.stdin,
95
+ output: process.stderr,
96
+ });
97
+ iface.question(prompt, (ans) => {
98
+ iface.close();
99
+ resolve(ans.trim());
100
+ });
101
+ });
102
+ }
103
+ async function maybeShareWrapped(wrapped) {
104
+ if (!process.stdin.isTTY) {
105
+ process.stderr.write(` your wrapped: ${wrapped.share_url}\n\n`);
106
+ return;
107
+ }
108
+ const ans = await askLine(chalk.white(" share to X? [Y/n] "));
109
+ process.stderr.write("\n");
110
+ const shared = !ans || ans.toLowerCase() !== "n";
111
+ if (!shared) {
112
+ process.stderr.write(` your wrapped: ${wrapped.share_url}\n\n`);
113
+ return;
114
+ }
115
+ process.stderr.write(" opening x.com to share...\n");
116
+ try {
117
+ await openShareToX(wrapped.view);
118
+ await markWrappedShared(wrapped.id);
119
+ process.stderr.write(" browser opened with prefilled tweet.\n\n");
120
+ }
121
+ catch (err) {
122
+ process.stderr.write(` couldn't open browser — share manually: ${wrapped.share_url}\n`);
123
+ process.stderr.write(` (${err instanceof Error ? err.message : err})\n\n`);
124
+ }
125
+ }
126
+ async function runAgent(jobId) {
127
+ // The wrapped render + share now run non-interactively (no TTY required), so
128
+ // the command completes when invoked from a Claude Code Bash call. Only the
129
+ // optional post-wrapped profile chat needs a real terminal.
130
+ if (profileChatEnabled()) {
131
+ ensureInteractive("The interactive profile chat");
132
+ }
133
+ console.log("\n ┌────────────────────────────────┐");
134
+ console.log(" │ are you really tokenmaxxing? │");
135
+ console.log(" │ Powered by Standout (YC P26) │");
136
+ console.log(" └────────────────────────────────┘\n");
137
+ if (jobId) {
138
+ console.log(` Applying to: ${STANDOUT_API_URL}/jobs/${jobId}\n`);
139
+ }
140
+ // LLM calls are proxied through Standout — users don't need an Anthropic key.
141
+ // Override the SDK's default User-Agent because Vercel's platform firewall
142
+ // matches `Anthropic/JS` as bot traffic and returns 403.
143
+ const client = new Anthropic({
144
+ apiKey: "proxied-by-standout",
145
+ baseURL: `${STANDOUT_API_URL}/api/public/agent-chat`,
146
+ defaultHeaders: {
147
+ "User-Agent": "Standout/0.4.0 (node)",
148
+ },
149
+ });
150
+ await confirmPrivacy();
151
+ const stopInitialLoading = startLoadingLine("Generating your Standout wrapped");
152
+ const usagePromise = gatherUsageStats();
153
+ const basicsPromise = gatherProfileBasics();
154
+ let usageStats;
155
+ let profileBasics;
156
+ try {
157
+ [usageStats, profileBasics] = await Promise.all([
158
+ usagePromise,
159
+ basicsPromise,
160
+ ]);
161
+ }
162
+ finally {
163
+ stopInitialLoading();
164
+ }
165
+ // Enrich the profile AND create the wrapped (all server LLM cards + rank) in one
166
+ // shot under the spinner; the whole deck is then paged at once.
167
+ const wrappedPromise = gatherFullEnrichment({
168
+ aiUsage: usageStats.ai_usage,
169
+ profileBasics,
170
+ verbose: false,
171
+ }).then(async (prefetched) => {
172
+ try {
173
+ const wrapped = await createWrapped({
174
+ profile: prefetched,
175
+ submissionId: null,
176
+ });
177
+ return { wrapped, prefetched };
178
+ }
179
+ catch {
180
+ return { wrapped: null, prefetched };
181
+ }
182
+ });
183
+ // Generate EVERYTHING up front (gather + all the server LLM cards) under one
184
+ // spinner, then page the whole deck at once — no two-pass preview/finale split.
185
+ const stopWrappedLoading = startLoadingLine("Generating your Standout wrapped");
186
+ let wrapped;
187
+ let prefetched;
188
+ try {
189
+ ({ wrapped, prefetched } = await wrappedPromise);
190
+ }
191
+ finally {
192
+ stopWrappedLoading();
193
+ }
194
+ let wrappedId = wrapped?.id ?? null;
195
+ if (wrapped) {
196
+ await renderWrappedAll(wrapped.view);
197
+ await maybeShareWrapped(wrapped);
198
+ }
199
+ else {
200
+ process.stderr.write(" (couldn't generate wrapped right now — continuing profile setup.)\n\n");
201
+ }
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
+ return;
207
+ }
208
+ const jobInstruction = jobId
209
+ ? `I'm applying to Standout job ID "${jobId}" — include this as \`jobId\` when you call standout_submit.\n\n`
210
+ : "";
211
+ const initialMessage = "Build my developer profile.\n\n" +
212
+ jobInstruction +
213
+ "The following context has already been gathered automatically. " +
214
+ "Use it as the source of truth — do not re-run equivalent shell commands.\n\n" +
215
+ "<prefetched_profile>\n" +
216
+ JSON.stringify(prefetched, null, 2) +
217
+ "\n</prefetched_profile>";
218
+ // Mark the first user message as a cache breakpoint so the prefetched JSON
219
+ // (typically 20-40k tokens) is read from cache on every subsequent turn.
220
+ const messages = [
221
+ {
222
+ role: "user",
223
+ content: [
224
+ {
225
+ type: "text",
226
+ text: initialMessage,
227
+ cache_control: { type: "ephemeral" },
228
+ },
229
+ ],
230
+ },
231
+ ];
232
+ let totalInputTokens = 0;
233
+ let totalOutputTokens = 0;
234
+ let totalCacheReadTokens = 0;
235
+ let totalCacheWriteTokens = 0;
236
+ const startedAt = Date.now();
237
+ for (let turn = 0; turn < MAX_TURNS; turn++) {
238
+ const stream = client.messages.stream({
239
+ model: "claude-opus-4-7",
240
+ max_tokens: 16384,
241
+ // System prompt cached — saves the ~1.5k token instruction block on every turn after #1.
242
+ system: [
243
+ {
244
+ type: "text",
245
+ text: SYSTEM_PROMPT,
246
+ cache_control: { type: "ephemeral" },
247
+ },
248
+ ],
249
+ messages,
250
+ // Cache the entire tools array via cache_control on the last tool definition.
251
+ tools: [
252
+ ...TOOLS,
253
+ {
254
+ type: "web_search_20250305",
255
+ name: "web_search",
256
+ max_uses: 5,
257
+ cache_control: { type: "ephemeral" },
258
+ },
259
+ ],
260
+ });
261
+ stream.on("text", (delta) => {
262
+ process.stdout.write(delta);
263
+ });
264
+ const response = await stream.finalMessage();
265
+ totalInputTokens += response.usage.input_tokens;
266
+ totalOutputTokens += response.usage.output_tokens;
267
+ const usage = response.usage;
268
+ totalCacheWriteTokens += usage.cache_creation_input_tokens ?? 0;
269
+ totalCacheReadTokens += usage.cache_read_input_tokens ?? 0;
270
+ // Merge or push assistant message (handles pause_turn continuations)
271
+ const lastMsg = messages[messages.length - 1];
272
+ if (lastMsg?.role === "assistant") {
273
+ // Continuation — merge into previous assistant message
274
+ const prevContent = lastMsg.content;
275
+ lastMsg.content = [...prevContent, ...response.content];
276
+ }
277
+ else {
278
+ messages.push({
279
+ role: "assistant",
280
+ content: response.content,
281
+ });
282
+ }
283
+ if (response.stop_reason === "end_turn") {
284
+ break;
285
+ }
286
+ if (response.stop_reason === "pause_turn") {
287
+ continue;
288
+ }
289
+ if (response.stop_reason === "tool_use") {
290
+ const toolResults = [];
291
+ for (const block of response.content) {
292
+ if (block.type === "tool_use") {
293
+ const result = await executeTool(block.name, block.input, {
294
+ prefetchedProfile: prefetched,
295
+ wrappedId,
296
+ });
297
+ const submittedId = result.match(/Profile submitted successfully! ID: ([0-9a-f-]+)/i)?.[1];
298
+ if (submittedId)
299
+ wrappedId = null;
300
+ toolResults.push({
301
+ type: "tool_result",
302
+ tool_use_id: block.id,
303
+ content: result,
304
+ });
305
+ }
306
+ }
307
+ messages.push({
308
+ role: "user",
309
+ content: toolResults,
310
+ });
311
+ }
312
+ else {
313
+ // Unhandled stop reason (max_tokens, refusal, etc.)
314
+ console.error(`\n Unexpected stop reason: ${response.stop_reason}. Stopping.`);
315
+ break;
316
+ }
317
+ }
318
+ const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
319
+ if (jobId) {
320
+ console.log(`\n Done! Finish your application: ${STANDOUT_API_URL}/jobs/${jobId}`);
321
+ }
322
+ else {
323
+ console.log(`\n Done. Visit ${STANDOUT_API_URL} to see your profile.`);
324
+ }
325
+ const cacheNote = totalCacheReadTokens > 0
326
+ ? ` · cache: ${totalCacheReadTokens.toLocaleString()} read, ${totalCacheWriteTokens.toLocaleString()} written`
327
+ : "";
328
+ console.log(` Tokens: ${totalInputTokens.toLocaleString()} in / ${totalOutputTokens.toLocaleString()} out${cacheNote} in ${elapsed}s\n`);
329
+ }
330
+ async function main() {
331
+ const args = process.argv.slice(2);
332
+ if (args.includes("--gather")) {
333
+ await confirmPrivacy();
334
+ const data = await gatherFullEnrichment();
335
+ process.stdout.write(JSON.stringify(data, null, 2));
336
+ }
337
+ else if (args.includes("--mcp")) {
338
+ await startMcpServer();
339
+ }
340
+ else {
341
+ const jobId = args.find((a) => !a.startsWith("-"));
342
+ if (jobId) {
343
+ process.env.STANDOUT_JOB_ID = jobId;
344
+ }
345
+ await runAgent(jobId);
346
+ }
347
+ }
348
+ main().catch((error) => {
349
+ console.error("Error:", error instanceof Error ? error.message : error);
350
+ process.exit(1);
351
+ });
@@ -0,0 +1,54 @@
1
+ import type { ConversationExchange, InteractionSignals, PromptFrequency } from "./ai-usage.js";
2
+ export interface CursorStats {
3
+ total_sessions: number;
4
+ active_days: number;
5
+ first_session: string | null;
6
+ last_session: string | null;
7
+ total_duration_hours: number;
8
+ message_counts: {
9
+ user: number;
10
+ assistant: number;
11
+ total: number;
12
+ };
13
+ models: string[];
14
+ hour_buckets: number[];
15
+ weekend_ratio: number;
16
+ languages: Record<string, number>;
17
+ prompt_samples: string[];
18
+ prompt_frequency: PromptFrequency[];
19
+ exchanges: ConversationExchange[];
20
+ interaction: InteractionSignals;
21
+ monthly_buckets: {
22
+ month: string;
23
+ sessions: number;
24
+ duration_hours: number;
25
+ input_tokens: number;
26
+ output_tokens: number;
27
+ cache_tokens: number;
28
+ primary_model: string | null;
29
+ }[];
30
+ all_time?: {
31
+ total_sessions: number;
32
+ active_days: number;
33
+ first_session: string | null;
34
+ last_session: string | null;
35
+ total_duration_hours: number;
36
+ total_input_tokens: number;
37
+ total_output_tokens: number;
38
+ total_cache_read_tokens: number;
39
+ total_cache_write_tokens: number;
40
+ monthly_buckets: {
41
+ month: string;
42
+ sessions: number;
43
+ duration_hours: number;
44
+ input_tokens: number;
45
+ output_tokens: number;
46
+ cache_tokens: number;
47
+ primary_model: string | null;
48
+ }[];
49
+ };
50
+ }
51
+ export declare function gatherCursor(opts?: {
52
+ dbPath?: string;
53
+ windowStartMs?: number;
54
+ }): Promise<CursorStats | null>;