youmd 0.4.1 → 0.4.9

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.
@@ -49,6 +49,7 @@ exports.scrapeProfile = scrapeProfile;
49
49
  exports.researchUser = researchUser;
50
50
  exports.getOpenRouterKey = getOpenRouterKey;
51
51
  exports.randomThinking = randomThinking;
52
+ exports.randomLabel = randomLabel;
52
53
  const readline = __importStar(require("readline"));
53
54
  const fs = __importStar(require("fs"));
54
55
  const path = __importStar(require("path"));
@@ -56,6 +57,9 @@ const os = __importStar(require("os"));
56
57
  const chalk_1 = __importDefault(require("chalk"));
57
58
  const config_1 = require("./config");
58
59
  const compiler_1 = require("./compiler");
60
+ const render_1 = require("./render");
61
+ Object.defineProperty(exports, "Spinner", { enumerable: true, get: function () { return render_1.BrailleSpinner; } });
62
+ const ascii_1 = require("./ascii");
59
63
  // ─── Constants ────────────────────────────────────────────────────────
60
64
  const CHAT_PROXY_URL = "https://kindly-cassowary-600.convex.site/api/v1/chat";
61
65
  exports.CHAT_PROXY_URL = CHAT_PROXY_URL;
@@ -66,6 +70,139 @@ exports.RESEARCH_URL = RESEARCH_URL;
66
70
  const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
67
71
  const OPENROUTER_MODEL = "anthropic/claude-sonnet-4";
68
72
  const USERNAME_RE = /^[a-z0-9][a-z0-9_-]{1,38}[a-z0-9]$/;
73
+ // ─── Categorized spinner labels ──────────────────────────────────────
74
+ const SPINNER_LABELS = {
75
+ llm: [
76
+ "reading between your lines",
77
+ "connecting the dots",
78
+ "processing your essence",
79
+ "building your identity constellation",
80
+ "decoding your professional DNA",
81
+ "triangulating your vibe",
82
+ "computing your main character energy",
83
+ "calibrating to your wavelength",
84
+ "reverse-engineering your personality",
85
+ "translating you into agent-speak",
86
+ ],
87
+ scrape: [
88
+ "pulling your digital footprint",
89
+ "scanning your corner of the internet",
90
+ "downloading your online soul",
91
+ "harvesting your web presence",
92
+ "reading your digital tea leaves",
93
+ "indexing your online persona",
94
+ ],
95
+ compile: [
96
+ "assembling your identity bundle",
97
+ "weaving your narrative thread",
98
+ "crystallizing who you are",
99
+ "forging your identity file",
100
+ "encoding your identity bundle",
101
+ "compiling your context mosaic",
102
+ ],
103
+ research: [
104
+ "researching you across the internet",
105
+ "asking the internet about you",
106
+ "mining your digital trail",
107
+ "running background checks (the fun kind)",
108
+ "interviewing your web presence",
109
+ "surveying your corner of the internet",
110
+ ],
111
+ };
112
+ function randomLabel(category) {
113
+ const labels = SPINNER_LABELS[category];
114
+ return labels[Math.floor(Math.random() * labels.length)];
115
+ }
116
+ // ─── ASCII portrait placeholder ──────────────────────────────────────
117
+ const PORTRAIT_COMMENTS = [
118
+ "looking sharp in monochrome.",
119
+ "not bad for a pile of unicode characters.",
120
+ "your pixels have good energy.",
121
+ "the internet looks good on you.",
122
+ "a face only a terminal could love. (that's a compliment.)",
123
+ "you render well at low resolution.",
124
+ ];
125
+ function randomPortraitComment() {
126
+ return PORTRAIT_COMMENTS[Math.floor(Math.random() * PORTRAIT_COMMENTS.length)];
127
+ }
128
+ function showPortraitPlaceholder(handle) {
129
+ const accent = chalk_1.default.hex("#C46A3A");
130
+ const dim = chalk_1.default.dim;
131
+ const displayHandle = handle.startsWith("@") ? handle : `@${handle}`;
132
+ const innerWidth = Math.max(displayHandle.length + 4, 28);
133
+ const portraitLine = "\u2591\u2592\u2593\u2588 portrait loaded \u2588\u2593\u2592\u2591";
134
+ const portraitPad = Math.max(0, innerWidth - portraitLine.length);
135
+ const handlePad = Math.max(0, innerWidth - displayHandle.length);
136
+ console.log("");
137
+ console.log(" " + dim("\u250C" + "\u2500".repeat(innerWidth + 2) + "\u2510"));
138
+ console.log(" " + dim("\u2502") + " " + accent(portraitLine) + " ".repeat(portraitPad) + dim("\u2502"));
139
+ console.log(" " + dim("\u2502") + " " + chalk_1.default.white(displayHandle) + " ".repeat(handlePad) + dim("\u2502"));
140
+ console.log(" " + dim("\u2514" + "\u2500".repeat(innerWidth + 2) + "\u2518"));
141
+ console.log("");
142
+ console.log(" " + accent(randomPortraitComment()));
143
+ console.log("");
144
+ }
145
+ // ─── ASCII logo ──────────────────────────────────────────────────────
146
+ const ASCII_LOGO_LINES = [
147
+ " \u2566 \u2566 \u2554\u2550\u2557 \u2566 \u2566 \u2554\u2566\u2557 \u2554\u2566\u2557",
148
+ " \u255A\u2566\u255D \u2551 \u2551 \u2551 \u2551 \u2551\u2551\u2551 \u2551\u2551",
149
+ " \u2569 \u255A\u2550\u255D \u255A\u2550\u255D \u2550\u2569\u255D \u2550\u2569\u255D",
150
+ ];
151
+ function delay(ms) {
152
+ return new Promise((resolve) => setTimeout(resolve, ms));
153
+ }
154
+ async function showAsciiLogo() {
155
+ const accent = chalk_1.default.hex("#C46A3A");
156
+ console.log("");
157
+ for (const line of ASCII_LOGO_LINES) {
158
+ console.log(" " + accent(line));
159
+ await delay(100);
160
+ }
161
+ console.log("");
162
+ console.log(" " + chalk_1.default.dim("the identity file for the agent internet"));
163
+ console.log("");
164
+ }
165
+ async function multiSelectPrompt(rl, question, options) {
166
+ console.log(" " + chalk_1.default.hex("#C46A3A")(question));
167
+ console.log("");
168
+ for (let i = 0; i < options.length; i++) {
169
+ console.log(` ${chalk_1.default.dim(`${i + 1}.`)} ${options[i].label}`);
170
+ }
171
+ console.log("");
172
+ const answer = await ask(rl, chalk_1.default.hex("#C46A3A")(" > ") + chalk_1.default.dim("type numbers separated by commas (e.g. 1,3,5): "));
173
+ if (!answer.trim())
174
+ return [];
175
+ const indices = answer
176
+ .split(",")
177
+ .map((s) => parseInt(s.trim(), 10))
178
+ .filter((n) => !isNaN(n) && n >= 1 && n <= options.length);
179
+ const selected = indices.map((i) => options[i - 1].value);
180
+ if (selected.length > 0) {
181
+ console.log(" " +
182
+ chalk_1.default.hex("#C46A3A")("\u2713") +
183
+ " " +
184
+ chalk_1.default.dim(selected.join(", ")));
185
+ }
186
+ console.log("");
187
+ return selected;
188
+ }
189
+ const CODING_AGENTS = [
190
+ { label: "Claude Code", value: "Claude Code" },
191
+ { label: "Codex CLI", value: "Codex CLI" },
192
+ { label: "Cursor", value: "Cursor" },
193
+ { label: "OpenClaw", value: "OpenClaw" },
194
+ { label: "Windsurf", value: "Windsurf" },
195
+ { label: "Other", value: "Other" },
196
+ ];
197
+ const AI_APPS = [
198
+ { label: "ChatGPT", value: "ChatGPT" },
199
+ { label: "Claude (web/app)", value: "Claude" },
200
+ { label: "Grok", value: "Grok" },
201
+ { label: "Gemini", value: "Gemini" },
202
+ { label: "Perplexity", value: "Perplexity" },
203
+ { label: "Other", value: "Other" },
204
+ ];
205
+ // ─── Constants ────────────────────────────────────────────────────────
69
206
  const THINKING_PHRASES = [
70
207
  "reading your about page like a respectful detective",
71
208
  "absorbing your linkedin energy",
@@ -179,6 +316,11 @@ personality:
179
316
  - proactive — don't just wait for answers, connect dots, make observations, suggest things.
180
317
  - reference specific things you learn about them. make them feel seen.
181
318
  - you're like a sharp coworker who's also a great listener.
319
+ - you have dry, sharp humor. make the user smile at least once per exchange.
320
+ - when you see something impressive in their profile, react genuinely — not with fake enthusiasm, but with specific appreciation.
321
+ - use lowercase always, no exclamation marks, but occasionally drop a witty aside or self-aware joke about being an AI building someone's identity.
322
+ - reference pop culture, tech culture, or internet humor when it fits naturally.
323
+ - when showing scraped data, react to specific details with personality — e.g. "bamf.com? bold domain choice. respect."
182
324
 
183
325
  you're building a you-md/v1 identity bundle. the sections are:
184
326
  - profile/about.md — bio, background, narrative
@@ -222,7 +364,13 @@ rules for content in updates:
222
364
 
223
365
  when you think the profile is rich enough (at least about, now, projects, and values have substance), suggest finishing by saying something like "your bundle is looking solid. ready to publish, or want to keep going?"
224
366
 
225
- important: keep responses concise. 2-4 sentences max per turn. ask one good question at a time, not a list. be a conversation, not a questionnaire.`;
367
+ important rules:
368
+ - keep responses concise. 2-4 sentences max per turn. be a conversation, not a questionnaire.
369
+ - ALWAYS ask ONE question at a time. never two questions in one message.
370
+ - keep your analysis/observations to 2-3 sentences MAX, then ask your single question.
371
+ - the question should be on its OWN LINE, separated by a blank line from the analysis.
372
+ - if the user sends "skip" or just presses Enter with no text, move on to the next topic without pushing.
373
+ - after 3 skips in a row, wrap up and show what you've built so far.`;
226
374
  exports.SYSTEM_PROMPT = SYSTEM_PROMPT;
227
375
  // ─── Helpers ──────────────────────────────────────────────────────────
228
376
  function createRL() {
@@ -343,7 +491,12 @@ async function researchUser(params) {
343
491
  return null;
344
492
  }
345
493
  }
494
+ function hasScrapeData(data) {
495
+ return !!(data.name || data.bio || data.followers !== undefined || data.location || data.website);
496
+ }
346
497
  function displayScrapeResult(label, data) {
498
+ if (!hasScrapeData(data))
499
+ return;
347
500
  console.log("");
348
501
  console.log(" " + chalk_1.default.bold(`${label} profile:`));
349
502
  if (data.name)
@@ -360,40 +513,6 @@ function displayScrapeResult(label, data) {
360
513
  console.log(" website: " + chalk_1.default.dim(data.website));
361
514
  console.log("");
362
515
  }
363
- // ─── Spinner ──────────────────────────────────────────────────────────
364
- class Spinner {
365
- constructor(label) {
366
- this.interval = null;
367
- this.frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
368
- this.frameIndex = 0;
369
- this.startTime = 0;
370
- this.label = label || randomThinking();
371
- }
372
- start() {
373
- this.startTime = Date.now();
374
- this.interval = setInterval(() => {
375
- this.frameIndex = (this.frameIndex + 1) % this.frames.length;
376
- const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
377
- const time = elapsed >= 2 ? chalk_1.default.dim(` ${elapsed}s`) : "";
378
- process.stdout.write(`\r ${chalk_1.default.hex("#C46A3A")(this.frames[this.frameIndex])} ${chalk_1.default.dim(this.label)}${time} `);
379
- }, 80);
380
- }
381
- stop(result) {
382
- if (this.interval) {
383
- clearInterval(this.interval);
384
- this.interval = null;
385
- }
386
- const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
387
- const time = elapsed >= 1 ? chalk_1.default.dim(` ${elapsed}s`) : "";
388
- if (result) {
389
- process.stdout.write(`\r ${chalk_1.default.green("\u2713")} ${chalk_1.default.dim(this.label)}${time} ${chalk_1.default.dim(result)}\n`);
390
- }
391
- else {
392
- process.stdout.write("\r" + " ".repeat(80) + "\r");
393
- }
394
- }
395
- }
396
- exports.Spinner = Spinner;
397
516
  async function callLLM(apiKey, messages) {
398
517
  // Try the you.md proxy first (no API key needed)
399
518
  try {
@@ -549,11 +668,11 @@ async function runFallbackMode(rl, info) {
549
668
  console.log("");
550
669
  console.log(chalk_1.default.dim(" chat service unavailable. running in manual mode."));
551
670
  console.log("");
552
- const tagline = await ask(rl, chalk_1.default.green(" > ") + "give me your one-liner. what do you do? ");
553
- const nowFocus = await ask(rl, chalk_1.default.green(" > ") + "what are you focused on right now? ");
554
- const projects = await ask(rl, chalk_1.default.green(" > ") + "name your top projects (comma-separated): ");
555
- const values = await ask(rl, chalk_1.default.green(" > ") + "what principles guide your work? ");
556
- const agentPrefs = await ask(rl, chalk_1.default.green(" > ") +
671
+ const tagline = await ask(rl, chalk_1.default.hex("#C46A3A")(" > ") + "give me your one-liner. what do you do? ");
672
+ const nowFocus = await ask(rl, chalk_1.default.hex("#C46A3A")(" > ") + "what are you focused on right now? ");
673
+ const projects = await ask(rl, chalk_1.default.hex("#C46A3A")(" > ") + "name your top projects (comma-separated): ");
674
+ const values = await ask(rl, chalk_1.default.hex("#C46A3A")(" > ") + "what principles guide your work? ");
675
+ const agentPrefs = await ask(rl, chalk_1.default.hex("#C46A3A")(" > ") +
557
676
  "how should AI agents talk to you? (e.g., direct, casual, formal): ");
558
677
  writeSectionFile(bundleDir, "profile/about.md", `---\ntitle: "About"\n---\n\n# ${info.name}\n\n${tagline}\n`);
559
678
  writeSectionFile(bundleDir, "profile/now.md", `---\ntitle: "Now"\n---\n\n${nowFocus || "<!-- What are you working on right now? -->"}\n`);
@@ -583,7 +702,7 @@ async function runFallbackMode(rl, info) {
583
702
  await finishBundle(bundleDir, info.username, info.name);
584
703
  }
585
704
  // ─── AI conversation mode ─────────────────────────────────────────────
586
- async function runAIMode(rl, info, apiKey, scraped, research) {
705
+ async function runAIMode(rl, info, apiKey, scraped, research, agentContext) {
587
706
  const bundleDir = (0, config_1.getLocalBundleDir)();
588
707
  const profileDir = path.join(bundleDir, "profile");
589
708
  const preferencesDir = path.join(bundleDir, "preferences");
@@ -593,7 +712,7 @@ async function runAIMode(rl, info, apiKey, scraped, research) {
593
712
  // ── Fetch website content if provided ──────────────────────────────
594
713
  let websiteContent = "";
595
714
  if (info.website) {
596
- const fetchSpinner = new Spinner("reading your site");
715
+ const fetchSpinner = new render_1.BrailleSpinner(randomLabel("scrape"));
597
716
  fetchSpinner.start();
598
717
  websiteContent = await fetchWebsiteContent(info.website);
599
718
  fetchSpinner.stop();
@@ -624,6 +743,10 @@ async function runAIMode(rl, info, apiKey, scraped, research) {
624
743
  - name: ${info.name}
625
744
  - username: ${info.username}
626
745
  ${linksInfo.length > 0 ? linksInfo.map((l) => `- ${l}`).join("\n") : "- no links provided"}`;
746
+ // Inject agent/tool selections
747
+ if (agentContext && agentContext.length > 0) {
748
+ initialUserMessage += `\n\nagent/tool preferences:\n${agentContext.map((c) => `- ${c}`).join("\n")}`;
749
+ }
627
750
  // Add scraped social profile data
628
751
  if (scraped?.twitter) {
629
752
  const t = scraped.twitter;
@@ -689,7 +812,7 @@ generate initial profile sections from what you know, show a brief summary, and
689
812
  { role: "user", content: initialUserMessage },
690
813
  ];
691
814
  // ── Initial LLM call ──────────────────────────────────────────────
692
- let spinner = new Spinner(randomThinking());
815
+ let spinner = new render_1.BrailleSpinner(randomLabel("llm"));
693
816
  spinner.start();
694
817
  let response;
695
818
  try {
@@ -736,24 +859,114 @@ generate initial profile sections from what you know, show a brief summary, and
736
859
  console.log("");
737
860
  // ── Conversation loop ──────────────────────────────────────────────
738
861
  let exchangeCount = 0;
862
+ let skipCount = 0;
739
863
  while (true) {
740
- const userInput = await ask(rl, chalk_1.default.green(" > ") + "");
741
- if (!userInput)
742
- continue;
864
+ const userInput = await ask(rl, chalk_1.default.hex("#C46A3A")(" > ") + "");
743
865
  if (isDonePhrase(userInput)) {
744
866
  break;
745
867
  }
746
- messages.push({ role: "user", content: userInput });
868
+ // Handle skip: empty input or "skip"
869
+ const isSkip = !userInput || userInput.toLowerCase().trim() === "skip";
870
+ if (isSkip) {
871
+ skipCount++;
872
+ if (skipCount >= 3) {
873
+ // After 3 skips, wrap up automatically
874
+ console.log(chalk_1.default.dim(" wrapping up with what we have so far."));
875
+ console.log("");
876
+ break;
877
+ }
878
+ // Inject system message telling the agent to move on
879
+ messages.push({
880
+ role: "user",
881
+ content: "(skip)",
882
+ });
883
+ messages.push({
884
+ role: "system",
885
+ content: "the user skipped this question. ask about the next topic on your list (projects, values, preferences, now, etc.) without dwelling on the skipped one. keep moving forward.",
886
+ });
887
+ }
888
+ else {
889
+ skipCount = 0;
890
+ messages.push({ role: "user", content: userInput });
891
+ // ── Auto-detect and auto-crawl URLs in user message ──
892
+ const urlRegex = /(?:https?:\/\/[^\s<>"']+|(?<![/\w])([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.(?:com|co|io|ai|dev|org|net|app|xyz|me))(?:\/[^\s<>"']*)?)/gi;
893
+ const detectedUrls = [];
894
+ let urlMatch;
895
+ const seenUrls = new Set();
896
+ while ((urlMatch = urlRegex.exec(userInput)) !== null) {
897
+ let url = urlMatch[0].replace(/[.,;:)\]]+$/, "");
898
+ if (!url.startsWith("http"))
899
+ url = `https://${url}`;
900
+ if (!seenUrls.has(url)) {
901
+ seenUrls.add(url);
902
+ detectedUrls.push(url);
903
+ }
904
+ }
905
+ if (detectedUrls.length > 0) {
906
+ console.log("");
907
+ const scrapeResults = [];
908
+ for (const url of detectedUrls) {
909
+ const domain = url.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
910
+ const sp = new render_1.BrailleSpinner(`${randomLabel("scrape")} (${domain})`);
911
+ sp.start();
912
+ try {
913
+ const res = await fetch(SCRAPE_URL, {
914
+ method: "POST",
915
+ headers: { "Content-Type": "application/json" },
916
+ body: JSON.stringify({ url, platform: "website" }),
917
+ signal: AbortSignal.timeout(30000),
918
+ });
919
+ if (res.ok) {
920
+ const data = (await res.json());
921
+ if (data.success && data.data) {
922
+ const d = data.data;
923
+ const parts = [`[SCRAPE RESULT: ${domain}]`];
924
+ if (d.displayName)
925
+ parts.push(`title: ${d.displayName}`);
926
+ if (d.bio)
927
+ parts.push(`description: ${d.bio}`);
928
+ if (d.extras && d.extras.bodyText) {
929
+ parts.push(`content: ${String(d.extras.bodyText).slice(0, 1500)}`);
930
+ }
931
+ scrapeResults.push(parts.join("\n"));
932
+ sp.stop(`${domain} scraped`);
933
+ }
934
+ else {
935
+ sp.stop(`${domain} — no data`);
936
+ }
937
+ }
938
+ else {
939
+ sp.fail(`${domain} — failed`);
940
+ }
941
+ }
942
+ catch {
943
+ sp.fail(`${domain} — timeout`);
944
+ }
945
+ }
946
+ if (scrapeResults.length > 0) {
947
+ messages.push({
948
+ role: "user",
949
+ content: `[PLATFORM AUTO-SCRAPE — real data from URLs the user mentioned. use this to describe their projects accurately. never ask the user to repeat information that was just scraped.]\n\n${scrapeResults.join("\n\n")}`,
950
+ });
951
+ }
952
+ console.log("");
953
+ }
954
+ }
747
955
  exchangeCount++;
748
956
  // After 3+ exchanges, hint to the agent it can suggest wrapping up
957
+ let ephemeralCount = 0;
749
958
  if (exchangeCount >= 3) {
750
959
  const hintMsg = {
751
960
  role: "system",
752
961
  content: "the user has provided several rounds of input. if the profile feels rich enough (about, now, projects, values all have substance), you can suggest wrapping up. but if there are obvious gaps, keep asking.",
753
962
  };
754
963
  messages.push(hintMsg);
964
+ ephemeralCount++;
755
965
  }
756
- spinner = new Spinner(randomThinking());
966
+ // Also count the skip system message as ephemeral
967
+ if (isSkip)
968
+ ephemeralCount++;
969
+ spinner = new render_1.BrailleSpinner(randomLabel("llm"));
757
970
  spinner.start();
758
971
  try {
759
972
  response = await callLLM(apiKey, messages);
@@ -763,16 +976,18 @@ generate initial profile sections from what you know, show a brief summary, and
763
976
  console.log(chalk_1.default.red(` AI error: ${err instanceof Error ? err.message : String(err)}`));
764
977
  console.log(chalk_1.default.dim(" try again, or type 'done' to finish."));
765
978
  console.log("");
766
- // Remove failed messages
767
- messages.pop(); // remove hint if added
768
- if (exchangeCount >= 3)
979
+ // Remove all ephemeral + user messages from this failed turn
980
+ for (let j = 0; j < ephemeralCount + 1; j++)
769
981
  messages.pop();
982
+ if (isSkip)
983
+ messages.pop(); // also remove the "(skip)" user message
770
984
  continue;
771
985
  }
772
986
  spinner.stop();
773
- // Remove the hint message from history (don't pollute context)
774
- if (exchangeCount >= 3) {
775
- messages.pop(); // remove the hint
987
+ // Remove ephemeral system messages from history (don't pollute context)
988
+ // They sit between the user message and the response we're about to push
989
+ for (let j = 0; j < ephemeralCount; j++) {
990
+ messages.pop();
776
991
  }
777
992
  messages.push({ role: "assistant", content: response });
778
993
  const parsed = parseUpdatesFromResponse(response);
@@ -791,7 +1006,7 @@ generate initial profile sections from what you know, show a brief summary, and
791
1006
  if (lowerDisplay.includes("ready to publish") ||
792
1007
  lowerDisplay.includes("bundle is looking solid") ||
793
1008
  lowerDisplay.includes("ready to go")) {
794
- const answer = await ask(rl, chalk_1.default.green(" > ") + "");
1009
+ const answer = await ask(rl, chalk_1.default.hex("#C46A3A")(" > ") + "");
795
1010
  if (isDonePhrase(answer) ||
796
1011
  answer.toLowerCase().includes("publish") ||
797
1012
  answer.toLowerCase().includes("yes") ||
@@ -801,7 +1016,7 @@ generate initial profile sections from what you know, show a brief summary, and
801
1016
  }
802
1017
  else {
803
1018
  messages.push({ role: "user", content: answer });
804
- spinner = new Spinner(randomThinking());
1019
+ spinner = new render_1.BrailleSpinner(randomLabel("llm"));
805
1020
  spinner.start();
806
1021
  try {
807
1022
  response = await callLLM(apiKey, messages);
@@ -832,69 +1047,85 @@ generate initial profile sections from what you know, show a brief summary, and
832
1047
  function printAgentMessage(text) {
833
1048
  if (!text)
834
1049
  return;
1050
+ // Ensure paragraphs have proper blank line separation
1051
+ const normalizedText = text
1052
+ .replace(/\n{3,}/g, "\n\n") // collapse 3+ newlines to 2
1053
+ .replace(/([.?!])\n([a-z])/g, "$1\n\n$2"); // add blank line between sentences that run together
835
1054
  const { renderRichResponse } = require("./render");
836
- console.log(renderRichResponse(text));
1055
+ const rendered = renderRichResponse(normalizedText);
1056
+ // Find the last line that ends with "?" and highlight it in accent color
1057
+ const lines = rendered.split("\n");
1058
+ let lastQuestionIdx = -1;
1059
+ for (let i = lines.length - 1; i >= 0; i--) {
1060
+ // Strip ANSI codes to check for trailing "?"
1061
+ const stripped = lines[i].replace(/\x1b\[[0-9;]*m/g, "").trim();
1062
+ if (stripped.endsWith("?")) {
1063
+ lastQuestionIdx = i;
1064
+ break;
1065
+ }
1066
+ }
1067
+ if (lastQuestionIdx >= 0) {
1068
+ // Replace that line with accent-colored version (strip existing styling, re-apply accent)
1069
+ const raw = lines[lastQuestionIdx].replace(/\x1b\[[0-9;]*m/g, "");
1070
+ // Preserve leading whitespace
1071
+ const leadingSpace = raw.match(/^(\s*)/)?.[1] || "";
1072
+ const content = raw.trim();
1073
+ lines[lastQuestionIdx] = leadingSpace + chalk_1.default.hex("#C46A3A")(content);
1074
+ }
1075
+ console.log(lines.join("\n"));
837
1076
  console.log("");
838
1077
  }
839
1078
  // ─── Finish and compile ───────────────────────────────────────────────
840
1079
  async function finishBundle(bundleDir, username, name) {
841
1080
  console.log("");
842
- const compileSpinner = new Spinner("compiling your context bundle");
1081
+ const compileSpinner = new render_1.BrailleSpinner(randomLabel("compile"));
843
1082
  compileSpinner.start();
844
1083
  await new Promise((r) => setTimeout(r, 600));
845
1084
  const result = (0, compiler_1.compileBundle)(bundleDir);
846
1085
  (0, compiler_1.writeBundle)(bundleDir, result);
847
1086
  compileSpinner.stop();
848
1087
  console.log(" " +
849
- chalk_1.default.green("done") +
1088
+ chalk_1.default.hex("#C46A3A")("done") +
850
1089
  chalk_1.default.dim(` -- bundle compiled (v${result.bundle.version})`));
851
1090
  // Show final preview with stats
852
1091
  const stats = showBundlePreview(bundleDir);
853
1092
  console.log(chalk_1.default.dim(` ${stats.fileCount} files, ${stats.filledCount} sections filled`));
854
1093
  console.log("");
855
- // Context link
856
- console.log(" " + chalk_1.default.bold("your context file is ready:"));
857
- console.log(" " + chalk_1.default.cyan(`https://you.md/${username}/context`));
1094
+ const accent = chalk_1.default.hex("#C46A3A");
1095
+ // What's next guide
1096
+ console.log(" " + accent("what's next:"));
858
1097
  console.log("");
859
- // Publish flow
860
- if ((0, config_1.isAuthenticated)()) {
861
- console.log(" you're authenticated. publish with: " +
862
- chalk_1.default.cyan("youmd publish"));
863
- }
864
- else {
865
- console.log(" " + chalk_1.default.bold("to go live:"));
866
- console.log(" 1. claim your username at " +
867
- chalk_1.default.cyan("https://you.md/claim"));
868
- console.log(" 2. " + chalk_1.default.cyan("youmd login --key <your-api-key>"));
869
- console.log(" 3. " + chalk_1.default.cyan("youmd publish"));
870
- }
1098
+ console.log(` 1. ${chalk_1.default.cyan("youmd login")} ${chalk_1.default.dim("-- connect to you.md (get an API key from the dashboard)")}`);
1099
+ console.log(` 2. ${chalk_1.default.cyan("youmd push")} ${chalk_1.default.dim("-- publish your profile to you.md/" + username)}`);
1100
+ console.log(` 3. ${chalk_1.default.cyan("youmd sync --watch")} ${chalk_1.default.dim("-- auto-sync changes as you edit files")}`);
1101
+ console.log(` 4. ${chalk_1.default.cyan("youmd chat")} ${chalk_1.default.dim("-- talk to the agent to update your profile")}`);
871
1102
  console.log("");
872
- console.log(" " + chalk_1.default.bold("using your identity with AI agents:"));
873
- console.log(" add this to your system prompt or CLAUDE.md:");
874
- console.log(chalk_1.default.dim(` "my identity file: https://you.md/${username}/context"`));
1103
+ console.log(" " + accent("using with claude code or other agents:"));
875
1104
  console.log("");
876
- console.log(" " +
877
- chalk_1.default.dim("run ") +
878
- chalk_1.default.cyan("youmd build") +
879
- chalk_1.default.dim(" anytime to recompile, or ") +
880
- chalk_1.default.cyan("youmd chat") +
881
- chalk_1.default.dim(" to keep editing with AI."));
1105
+ console.log(` ${chalk_1.default.cyan("youmd status")} ${chalk_1.default.dim("-- check your bundle")}`);
1106
+ console.log(` ${chalk_1.default.cyan("youmd private notes append \"...\"")}`);
1107
+ console.log(` ${chalk_1.default.dim(" -- save agent preferences")}`);
1108
+ console.log(` ${chalk_1.default.cyan("youmd link create")} ${chalk_1.default.dim("-- create a shareable context link")}`);
1109
+ console.log(` ${chalk_1.default.cyan("youmd project init")} ${chalk_1.default.dim("-- set up project-specific context")}`);
1110
+ console.log("");
1111
+ // Context link
1112
+ console.log(" " + chalk_1.default.bold("your context file is ready:"));
1113
+ console.log(" " + chalk_1.default.cyan(`https://you.md/${username}/context`));
882
1114
  console.log("");
883
1115
  console.log(" " + chalk_1.default.bold(`welcome to the agent internet, ${name}.`));
884
1116
  console.log("");
885
1117
  }
886
1118
  async function runOnboarding() {
887
1119
  const rl = createRL();
888
- console.log("");
889
- console.log(" " + chalk_1.default.bold("you.md"));
890
- console.log(" " + chalk_1.default.dim("your identity file for the agent internet"));
891
- console.log("");
1120
+ // ── ASCII logo splash ──────────────────────────────────────────────
1121
+ // Real YOU logo — same block-character font as the homepage hero
1122
+ (0, ascii_1.printYouLogo)();
892
1123
  // ── Phase 1: Identity basics (fast, no LLM) ────────────────────────
893
1124
  // Username
894
1125
  let username = "";
895
1126
  let usernameValid = false;
896
1127
  while (!usernameValid) {
897
- username = await ask(rl, chalk_1.default.green(" > ") + "pick a username: ");
1128
+ username = await ask(rl, chalk_1.default.hex("#C46A3A")(" > ") + "pick a username: ");
898
1129
  if (!username) {
899
1130
  console.log(chalk_1.default.red(" username is required"));
900
1131
  continue;
@@ -908,7 +1139,25 @@ async function runOnboarding() {
908
1139
  process.stdout.write(chalk_1.default.dim(" checking... "));
909
1140
  const result = await checkUsernameRemote(username);
910
1141
  if (result.available) {
911
- console.log(chalk_1.default.green(username + " is yours."));
1142
+ // Check if a profile already exists on you.md for this username
1143
+ let profileExists = false;
1144
+ try {
1145
+ const profileRes = await fetch(`https://you.md/${encodeURIComponent(username)}/context`, { method: "HEAD", signal: AbortSignal.timeout(5000) });
1146
+ profileExists = profileRes.ok;
1147
+ }
1148
+ catch {
1149
+ // ignore -- assume no profile
1150
+ }
1151
+ if (profileExists) {
1152
+ console.log(chalk_1.default.green(username + " is available."));
1153
+ console.log(chalk_1.default.dim(` a profile for @${username} already exists on you.md -- you can claim it after login with `) +
1154
+ chalk_1.default.cyan("youmd login") +
1155
+ chalk_1.default.dim(" then ") +
1156
+ chalk_1.default.cyan("youmd pull"));
1157
+ }
1158
+ else {
1159
+ console.log(chalk_1.default.green(username + " is yours."));
1160
+ }
912
1161
  usernameValid = true;
913
1162
  }
914
1163
  else {
@@ -917,31 +1166,126 @@ async function runOnboarding() {
917
1166
  }
918
1167
  }
919
1168
  console.log("");
920
- const name = await ask(rl, chalk_1.default.green(" > ") + "what's your name? ");
921
- const website = await ask(rl, chalk_1.default.green(" > ") +
1169
+ const name = await ask(rl, chalk_1.default.hex("#C46A3A")(" > ") + "what's your name? ");
1170
+ const website = await ask(rl, chalk_1.default.hex("#C46A3A")(" > ") +
922
1171
  "website URL " +
923
1172
  chalk_1.default.dim("(optional)") +
924
1173
  ": ");
925
- const twitter = await ask(rl, chalk_1.default.green(" > ") +
1174
+ const twitter = await ask(rl, chalk_1.default.hex("#C46A3A")(" > ") +
926
1175
  "X/Twitter username " +
927
1176
  chalk_1.default.dim("(optional, e.g. @houston)") +
928
1177
  ": ");
929
- const github = await ask(rl, chalk_1.default.green(" > ") +
1178
+ const github = await ask(rl, chalk_1.default.hex("#C46A3A")(" > ") +
930
1179
  "GitHub username " +
931
1180
  chalk_1.default.dim("(optional)") +
932
1181
  ": ");
933
- const linkedin = await ask(rl, chalk_1.default.green(" > ") +
1182
+ const linkedin = await ask(rl, chalk_1.default.hex("#C46A3A")(" > ") +
934
1183
  "LinkedIn URL " +
935
1184
  chalk_1.default.dim("(optional)") +
936
1185
  ": ");
937
1186
  console.log("");
1187
+ // ── Render REAL ASCII portrait from first social handle ──────────
1188
+ const earlyTwitter = (twitter || "").replace(/^@/, "").trim();
1189
+ const earlyGithub = (github || "").trim();
1190
+ const firstHandle = earlyTwitter || earlyGithub;
1191
+ if (firstHandle) {
1192
+ // Get the profile image URL — GitHub is most reliable for direct image access
1193
+ const imageUrl = earlyGithub
1194
+ ? `https://avatars.githubusercontent.com/${earlyGithub}?s=200`
1195
+ : `https://unavatar.io/x/${earlyTwitter}`;
1196
+ const portraitSpinner = new render_1.BrailleSpinner("fetching your profile image");
1197
+ portraitSpinner.start();
1198
+ // Preload the image silently, then stop spinner before rendering
1199
+ let portraitLines = null;
1200
+ try {
1201
+ const Jimp = (await Promise.resolve().then(() => __importStar(require("jimp")))).default;
1202
+ const img = await Jimp.read(imageUrl);
1203
+ portraitSpinner.stop("got it — rendering portrait");
1204
+ console.log("");
1205
+ // Now render the portrait line by line (the actual wow moment)
1206
+ const RAMP = `$@B%8&#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}?-_+~<>i!lI;:,". `;
1207
+ const cols = 60;
1208
+ const rows = Math.floor(cols * (img.getHeight() / img.getWidth()) * 0.46);
1209
+ img.resize(cols, rows);
1210
+ img.contrast(0.3);
1211
+ img.brightness(0.05);
1212
+ portraitLines = [];
1213
+ for (let y = 0; y < rows; y++) {
1214
+ let coloredLine = "";
1215
+ let plainLine = "";
1216
+ for (let x = 0; x < cols; x++) {
1217
+ const pixel = Jimp.intToRGBA(img.getPixelColor(x, y));
1218
+ const lum = 0.299 * pixel.r + 0.587 * pixel.g + 0.114 * pixel.b;
1219
+ const ch = RAMP[Math.floor((lum / 255) * (RAMP.length - 1))];
1220
+ plainLine += ch;
1221
+ // Orange-tinted colors based on luminance
1222
+ const brightness = Math.floor((lum / 255) * 100);
1223
+ if (brightness < 10) {
1224
+ coloredLine += chalk_1.default.hidden(ch);
1225
+ }
1226
+ else if (brightness < 30) {
1227
+ coloredLine += chalk_1.default.hex("#5A3018")(ch);
1228
+ }
1229
+ else if (brightness < 50) {
1230
+ coloredLine += chalk_1.default.hex("#8A4828")(ch);
1231
+ }
1232
+ else if (brightness < 70) {
1233
+ coloredLine += chalk_1.default.hex("#B06038")(ch);
1234
+ }
1235
+ else if (brightness < 85) {
1236
+ coloredLine += chalk_1.default.hex("#C46A3A")(ch);
1237
+ }
1238
+ else {
1239
+ coloredLine += chalk_1.default.hex("#E09060")(ch);
1240
+ }
1241
+ }
1242
+ portraitLines.push(plainLine);
1243
+ process.stdout.write(` ${coloredLine}\n`);
1244
+ }
1245
+ }
1246
+ catch {
1247
+ portraitSpinner.stop("couldn't fetch image — we'll try again later");
1248
+ }
1249
+ if (portraitLines) {
1250
+ console.log("");
1251
+ console.log(" " + chalk_1.default.hex("#C46A3A")(randomPortraitComment()));
1252
+ console.log("");
1253
+ // Save portrait data locally for push to web API
1254
+ const bundleDir = (0, config_1.getLocalBundleDir)();
1255
+ try {
1256
+ const portraitData = {
1257
+ lines: portraitLines,
1258
+ cols: 60,
1259
+ rows: portraitLines.length,
1260
+ format: "classic",
1261
+ sourceUrl: imageUrl,
1262
+ generatedAt: Date.now(),
1263
+ };
1264
+ fs.mkdirSync(bundleDir, { recursive: true });
1265
+ fs.writeFileSync(path.join(bundleDir, "portrait.json"), JSON.stringify(portraitData, null, 2));
1266
+ // Also save the source image URL as avatarUrl in config
1267
+ const config = (0, config_1.readGlobalConfig)();
1268
+ config.avatarUrl = imageUrl;
1269
+ (0, config_1.writeGlobalConfig)(config);
1270
+ }
1271
+ catch {
1272
+ // non-fatal — portrait save failed silently
1273
+ }
1274
+ }
1275
+ else {
1276
+ portraitSpinner.stop("couldn't render — no worries, we'll try again later");
1277
+ }
1278
+ }
1279
+ // ── Multi-select: coding agents and AI apps ───────────────────────
1280
+ const selectedAgents = await multiSelectPrompt(rl, "which coding agents do you use?", CODING_AGENTS);
1281
+ const selectedApps = await multiSelectPrompt(rl, "which AI apps do you regularly use?", AI_APPS);
938
1282
  // ── Scrape social profiles ────────────────────────────────────────
939
1283
  const twitterHandle = (twitter || "").replace(/^@/, "").trim();
940
1284
  const githubHandle = (github || "").trim();
941
1285
  let twitterData = null;
942
1286
  let githubData = null;
943
1287
  if (twitterHandle) {
944
- const scrapeSpinner = new Spinner("scanning your X profile");
1288
+ const scrapeSpinner = new render_1.BrailleSpinner(randomLabel("scrape"));
945
1289
  scrapeSpinner.start();
946
1290
  twitterData = await scrapeProfile(`https://x.com/${twitterHandle}`);
947
1291
  scrapeSpinner.stop();
@@ -954,7 +1298,7 @@ async function runOnboarding() {
954
1298
  }
955
1299
  }
956
1300
  if (githubHandle) {
957
- const scrapeSpinner = new Spinner("reading your GitHub");
1301
+ const scrapeSpinner = new render_1.BrailleSpinner(randomLabel("scrape"));
958
1302
  scrapeSpinner.start();
959
1303
  githubData = await scrapeProfile(`https://github.com/${githubHandle}`);
960
1304
  scrapeSpinner.stop();
@@ -978,7 +1322,7 @@ async function runOnboarding() {
978
1322
  links.push(linkedin);
979
1323
  let researchData = null;
980
1324
  if (name || twitterHandle || githubHandle) {
981
- const researchSpinner = new Spinner("researching you across the internet");
1325
+ const researchSpinner = new render_1.BrailleSpinner(randomLabel("research"));
982
1326
  researchSpinner.start();
983
1327
  researchData = await researchUser({
984
1328
  name: name || username,
@@ -1016,8 +1360,16 @@ async function runOnboarding() {
1016
1360
  const userApiKey = getOpenRouterKey();
1017
1361
  console.log(chalk_1.default.dim(" cool. let's build your identity."));
1018
1362
  console.log("");
1363
+ // Build agent context from multi-select
1364
+ const agentContext = [];
1365
+ if (selectedAgents.length > 0) {
1366
+ agentContext.push(`coding agents they use: ${selectedAgents.join(", ")}`);
1367
+ }
1368
+ if (selectedApps.length > 0) {
1369
+ agentContext.push(`AI apps they regularly use: ${selectedApps.join(", ")}`);
1370
+ }
1019
1371
  try {
1020
- await runAIMode(rl, basicInfo, userApiKey, { twitter: twitterData, github: githubData }, researchData);
1372
+ await runAIMode(rl, basicInfo, userApiKey, { twitter: twitterData, github: githubData }, researchData, agentContext.length > 0 ? agentContext : undefined);
1021
1373
  }
1022
1374
  catch {
1023
1375
  console.log(chalk_1.default.dim(" switching to manual mode."));
@@ -1052,7 +1404,7 @@ async function createBundle(info) {
1052
1404
  const result = (0, compiler_1.compileBundle)(bundleDir);
1053
1405
  (0, compiler_1.writeBundle)(bundleDir, result);
1054
1406
  console.log(" " +
1055
- chalk_1.default.green("done") +
1407
+ chalk_1.default.hex("#C46A3A")("done") +
1056
1408
  chalk_1.default.dim(` -- bundle compiled (v${result.bundle.version})`));
1057
1409
  console.log("");
1058
1410
  showBundlePreview(bundleDir);