youmd 0.2.1 → 0.3.0
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/commands/build.d.ts +1 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +70 -9
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/chat.d.ts +2 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +598 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/commands/publish.d.ts.map +1 -1
- package/dist/commands/publish.js +28 -18
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +69 -13
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +10 -3
- package/dist/commands/whoami.js.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/onboarding.d.ts +75 -0
- package/dist/lib/onboarding.d.ts.map +1 -1
- package/dist/lib/onboarding.js +915 -164
- package/dist/lib/onboarding.js.map +1 -1
- package/package.json +2 -2
package/dist/lib/onboarding.js
CHANGED
|
@@ -36,15 +36,195 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.RESEARCH_URL = exports.SCRAPE_URL = exports.CHAT_PROXY_URL = exports.BUNDLE_SECTIONS = exports.SYSTEM_PROMPT = exports.Spinner = void 0;
|
|
39
40
|
exports.runOnboarding = runOnboarding;
|
|
40
41
|
exports.createBundle = createBundle;
|
|
42
|
+
exports.callLLM = callLLM;
|
|
43
|
+
exports.parseUpdatesFromResponse = parseUpdatesFromResponse;
|
|
44
|
+
exports.writeSectionFile = writeSectionFile;
|
|
45
|
+
exports.sectionLabel = sectionLabel;
|
|
46
|
+
exports.showBundlePreview = showBundlePreview;
|
|
47
|
+
exports.fetchWebsiteContent = fetchWebsiteContent;
|
|
48
|
+
exports.scrapeProfile = scrapeProfile;
|
|
49
|
+
exports.researchUser = researchUser;
|
|
50
|
+
exports.getOpenRouterKey = getOpenRouterKey;
|
|
51
|
+
exports.randomThinking = randomThinking;
|
|
41
52
|
const readline = __importStar(require("readline"));
|
|
42
53
|
const fs = __importStar(require("fs"));
|
|
43
54
|
const path = __importStar(require("path"));
|
|
55
|
+
const os = __importStar(require("os"));
|
|
44
56
|
const chalk_1 = __importDefault(require("chalk"));
|
|
45
57
|
const config_1 = require("./config");
|
|
46
58
|
const compiler_1 = require("./compiler");
|
|
47
|
-
// ───
|
|
59
|
+
// ─── Constants ────────────────────────────────────────────────────────
|
|
60
|
+
const CHAT_PROXY_URL = "https://kindly-cassowary-600.convex.site/api/v1/chat";
|
|
61
|
+
exports.CHAT_PROXY_URL = CHAT_PROXY_URL;
|
|
62
|
+
const SCRAPE_URL = "https://kindly-cassowary-600.convex.site/api/v1/scrape";
|
|
63
|
+
exports.SCRAPE_URL = SCRAPE_URL;
|
|
64
|
+
const RESEARCH_URL = "https://kindly-cassowary-600.convex.site/api/v1/research";
|
|
65
|
+
exports.RESEARCH_URL = RESEARCH_URL;
|
|
66
|
+
const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
67
|
+
const OPENROUTER_MODEL = "anthropic/claude-sonnet-4";
|
|
68
|
+
const USERNAME_RE = /^[a-z0-9][a-z0-9_-]{1,38}[a-z0-9]$/;
|
|
69
|
+
const THINKING_PHRASES = [
|
|
70
|
+
"reading your about page like a respectful detective",
|
|
71
|
+
"absorbing your linkedin energy",
|
|
72
|
+
"scanning your timeline vibe",
|
|
73
|
+
"learning how you think out loud",
|
|
74
|
+
"decoding your digital footprint",
|
|
75
|
+
"mapping your professional universe",
|
|
76
|
+
"translating you into agent-speak",
|
|
77
|
+
"assembling the puzzle pieces",
|
|
78
|
+
"teaching LLMs about you",
|
|
79
|
+
"studying your sentence structure",
|
|
80
|
+
"finding your narrative thread",
|
|
81
|
+
"distilling your essence",
|
|
82
|
+
"cataloging your side projects",
|
|
83
|
+
"indexing your strong opinions",
|
|
84
|
+
"capturing your voice signature",
|
|
85
|
+
"building your identity constellation",
|
|
86
|
+
"compiling your context bundle",
|
|
87
|
+
"downloading your professional soul",
|
|
88
|
+
"converting vibes to structured data",
|
|
89
|
+
"weaving your story",
|
|
90
|
+
"crystallizing your identity",
|
|
91
|
+
"grokking your whole deal",
|
|
92
|
+
"connecting the dots",
|
|
93
|
+
"processing your essence",
|
|
94
|
+
"structuring your identity",
|
|
95
|
+
"learning you",
|
|
96
|
+
"calibrating to your wavelength",
|
|
97
|
+
"reading between your lines",
|
|
98
|
+
"mapping your expertise graph",
|
|
99
|
+
"analyzing your voice patterns",
|
|
100
|
+
"extracting your narrative arcs",
|
|
101
|
+
"building your knowledge panel",
|
|
102
|
+
"computing your identity fingerprint",
|
|
103
|
+
"synthesizing your public presence",
|
|
104
|
+
"parsing your career trajectory",
|
|
105
|
+
"understanding your builder instinct",
|
|
106
|
+
"decrypting your communication style",
|
|
107
|
+
"profiling your creative output",
|
|
108
|
+
"charting your professional constellation",
|
|
109
|
+
"rendering you in agent-speak",
|
|
110
|
+
"encoding your identity bundle",
|
|
111
|
+
"absorbing your origin story",
|
|
112
|
+
"discovering what makes you tick",
|
|
113
|
+
"mapping your values system",
|
|
114
|
+
"tracing your impact footprint",
|
|
115
|
+
"archiving your digital presence",
|
|
116
|
+
"curating your identity artifacts",
|
|
117
|
+
"assembling your context mosaic",
|
|
118
|
+
"beaming you up to the agent internet",
|
|
119
|
+
"initializing your identity protocol",
|
|
120
|
+
"reverse-engineering your personality",
|
|
121
|
+
"calculating your main character energy",
|
|
122
|
+
"triangulating your vibe",
|
|
123
|
+
"cross-referencing your footnotes",
|
|
124
|
+
"auditing your digital paper trail",
|
|
125
|
+
"interviewing your web presence",
|
|
126
|
+
"surveying your corner of the internet",
|
|
127
|
+
"speed-reading your life story",
|
|
128
|
+
"taking notes on your trajectory",
|
|
129
|
+
"measuring your signal-to-noise ratio",
|
|
130
|
+
"detecting your communication wavelength",
|
|
131
|
+
"parsing your professional DNA",
|
|
132
|
+
"sketching your identity blueprint",
|
|
133
|
+
"constructing your context scaffold",
|
|
134
|
+
"loading your universe into memory",
|
|
135
|
+
"generating your identity hash",
|
|
136
|
+
"composing your digital portrait",
|
|
137
|
+
"calibrating the you.md protocol",
|
|
138
|
+
"translating your experience into structure",
|
|
139
|
+
"wiring your identity into the network",
|
|
140
|
+
];
|
|
141
|
+
const DONE_PHRASES = [
|
|
142
|
+
"done",
|
|
143
|
+
"publish",
|
|
144
|
+
"that's it",
|
|
145
|
+
"thats it",
|
|
146
|
+
"looks good",
|
|
147
|
+
"i'm done",
|
|
148
|
+
"im done",
|
|
149
|
+
"ship it",
|
|
150
|
+
"good enough",
|
|
151
|
+
"let's go",
|
|
152
|
+
"lets go",
|
|
153
|
+
"ready",
|
|
154
|
+
"finish",
|
|
155
|
+
"all good",
|
|
156
|
+
"that's all",
|
|
157
|
+
"thats all",
|
|
158
|
+
"nothing else",
|
|
159
|
+
"nah",
|
|
160
|
+
"no",
|
|
161
|
+
"nope",
|
|
162
|
+
];
|
|
163
|
+
const BUNDLE_SECTIONS = [
|
|
164
|
+
"profile/about.md",
|
|
165
|
+
"profile/now.md",
|
|
166
|
+
"profile/projects.md",
|
|
167
|
+
"profile/values.md",
|
|
168
|
+
"profile/links.md",
|
|
169
|
+
"preferences/agent.md",
|
|
170
|
+
"preferences/writing.md",
|
|
171
|
+
];
|
|
172
|
+
exports.BUNDLE_SECTIONS = BUNDLE_SECTIONS;
|
|
173
|
+
const SYSTEM_PROMPT = `you are the you.md agent. you help humans build their identity file for the agent internet. you are their first AI that truly knows them.
|
|
174
|
+
|
|
175
|
+
personality:
|
|
176
|
+
- warm but not gushy. direct. a dash of dry wit when it lands naturally.
|
|
177
|
+
- genuinely curious about people — you actually want to learn what makes them tick.
|
|
178
|
+
- terminal-native tone: lowercase, no exclamation marks, no emoji, short sentences.
|
|
179
|
+
- proactive — don't just wait for answers, connect dots, make observations, suggest things.
|
|
180
|
+
- reference specific things you learn about them. make them feel seen.
|
|
181
|
+
- you're like a sharp coworker who's also a great listener.
|
|
182
|
+
|
|
183
|
+
you're building a you-md/v1 identity bundle. the sections are:
|
|
184
|
+
- profile/about.md — bio, background, narrative
|
|
185
|
+
- profile/now.md — current focus, what they're working on right now
|
|
186
|
+
- profile/projects.md — active projects with details
|
|
187
|
+
- profile/values.md — core values and principles
|
|
188
|
+
- profile/links.md — annotated links (website, socials, repos)
|
|
189
|
+
- preferences/agent.md — how AI agents should interact with them
|
|
190
|
+
- preferences/writing.md — their communication style
|
|
191
|
+
|
|
192
|
+
your job:
|
|
193
|
+
1. analyze what you know about the person from their URLs and conversation
|
|
194
|
+
2. ask follow-up questions to fill gaps — be conversational, not interrogative
|
|
195
|
+
3. after each exchange, output structured updates as JSON blocks:
|
|
196
|
+
\`\`\`json
|
|
197
|
+
{"updates": [{"section": "profile/about.md", "content": "...markdown content..."}]}
|
|
198
|
+
\`\`\`
|
|
199
|
+
4. keep the conversation going until you have enough for a rich identity bundle
|
|
200
|
+
5. never tell the user to edit markdown files themselves — you handle all of that
|
|
201
|
+
6. reference specific things you learned about them
|
|
202
|
+
7. if someone shares links, immediately offer to pull context from them
|
|
203
|
+
8. be proactive: "i noticed you mentioned X — want me to add that to your projects?"
|
|
204
|
+
9. occasionally remind them of what you've captured so far
|
|
205
|
+
|
|
206
|
+
conversational style examples:
|
|
207
|
+
- "cool. let me go read your site."
|
|
208
|
+
- "ok so you're basically a linkedin whisperer. noted."
|
|
209
|
+
- "that's a solid stack. let me capture that."
|
|
210
|
+
- "interesting — so you're more on the strategy side than pure engineering?"
|
|
211
|
+
- "i've got a good picture of what you do. want to tell me what you actually care about?"
|
|
212
|
+
- "that's a lot of projects. which one keeps you up at night?"
|
|
213
|
+
- "your bundle is looking solid. ready to publish, or should we keep going?"
|
|
214
|
+
|
|
215
|
+
rules for content in updates:
|
|
216
|
+
- each section must start with a YAML frontmatter block (--- title: "SectionTitle" ---)
|
|
217
|
+
- content should be real markdown, not HTML comments or placeholders
|
|
218
|
+
- be substantive. write real prose based on what you know.
|
|
219
|
+
- for links.md, format as: - **Label**: URL — brief annotation
|
|
220
|
+
- for agent.md, describe how agents should interact with this person
|
|
221
|
+
- for writing.md, capture their tone/style from how they've been talking to you
|
|
222
|
+
|
|
223
|
+
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
|
+
|
|
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.`;
|
|
226
|
+
exports.SYSTEM_PROMPT = SYSTEM_PROMPT;
|
|
227
|
+
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
48
228
|
function createRL() {
|
|
49
229
|
return readline.createInterface({
|
|
50
230
|
input: process.stdin,
|
|
@@ -58,18 +238,18 @@ function ask(rl, question) {
|
|
|
58
238
|
});
|
|
59
239
|
});
|
|
60
240
|
}
|
|
61
|
-
function
|
|
62
|
-
return
|
|
241
|
+
function randomThinking() {
|
|
242
|
+
return THINKING_PHRASES[Math.floor(Math.random() * THINKING_PHRASES.length)];
|
|
243
|
+
}
|
|
244
|
+
function isDonePhrase(input) {
|
|
245
|
+
const lower = input.toLowerCase().trim();
|
|
246
|
+
return DONE_PHRASES.some((p) => lower === p || lower.startsWith(p + " "));
|
|
63
247
|
}
|
|
64
|
-
// ─── Username validation ─────────────────────────────────────────────
|
|
65
|
-
const USERNAME_RE = /^[a-z0-9][a-z0-9_-]{1,38}[a-z0-9]$/;
|
|
66
248
|
function validateUsernameLocal(username) {
|
|
67
|
-
if (username.length < 3)
|
|
249
|
+
if (username.length < 3)
|
|
68
250
|
return "must be at least 3 characters";
|
|
69
|
-
|
|
70
|
-
if (username.length > 40) {
|
|
251
|
+
if (username.length > 40)
|
|
71
252
|
return "must be 40 characters or fewer";
|
|
72
|
-
}
|
|
73
253
|
if (!USERNAME_RE.test(username)) {
|
|
74
254
|
return "lowercase letters, numbers, hyphens, and underscores only";
|
|
75
255
|
}
|
|
@@ -79,110 +259,635 @@ async function checkUsernameRemote(username) {
|
|
|
79
259
|
try {
|
|
80
260
|
const url = `https://kindly-cassowary-600.convex.site/api/v1/check-username?username=${encodeURIComponent(username)}`;
|
|
81
261
|
const res = await fetch(url);
|
|
82
|
-
if (!res.ok)
|
|
83
|
-
return { available: true, reason: null };
|
|
84
|
-
}
|
|
262
|
+
if (!res.ok)
|
|
263
|
+
return { available: true, reason: null };
|
|
85
264
|
const data = await res.json();
|
|
86
265
|
return data;
|
|
87
266
|
}
|
|
88
267
|
catch {
|
|
89
|
-
// Network error -- don't block onboarding
|
|
90
268
|
return { available: true, reason: null };
|
|
91
269
|
}
|
|
92
270
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
271
|
+
function getOpenRouterKey() {
|
|
272
|
+
if (process.env.OPENROUTER_API_KEY) {
|
|
273
|
+
return process.env.OPENROUTER_API_KEY;
|
|
274
|
+
}
|
|
275
|
+
const configPath = path.join(os.homedir(), ".youmd", "config.json");
|
|
276
|
+
try {
|
|
277
|
+
if (fs.existsSync(configPath)) {
|
|
278
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
279
|
+
const config = JSON.parse(raw);
|
|
280
|
+
if (config.openrouterKey)
|
|
281
|
+
return config.openrouterKey;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// ignore
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
103
288
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
289
|
+
// ─── Website fetcher ──────────────────────────────────────────────────
|
|
290
|
+
async function fetchWebsiteContent(url) {
|
|
291
|
+
try {
|
|
292
|
+
const normalizedUrl = url.startsWith("http://") || url.startsWith("https://")
|
|
293
|
+
? url
|
|
294
|
+
: "https://" + url;
|
|
295
|
+
const res = await fetch(normalizedUrl, {
|
|
296
|
+
headers: { "User-Agent": "youmd-bot/1.0" },
|
|
297
|
+
signal: AbortSignal.timeout(10000),
|
|
298
|
+
});
|
|
299
|
+
const html = await res.text();
|
|
300
|
+
const text = html
|
|
301
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
|
|
302
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
|
|
303
|
+
.replace(/<[^>]+>/g, " ")
|
|
304
|
+
.replace(/\s+/g, " ")
|
|
305
|
+
.trim();
|
|
306
|
+
return text.slice(0, 4000);
|
|
113
307
|
}
|
|
114
|
-
|
|
115
|
-
|
|
308
|
+
catch {
|
|
309
|
+
return "";
|
|
116
310
|
}
|
|
117
|
-
|
|
118
|
-
|
|
311
|
+
}
|
|
312
|
+
async function scrapeProfile(url) {
|
|
313
|
+
try {
|
|
314
|
+
const res = await fetch(SCRAPE_URL, {
|
|
315
|
+
method: "POST",
|
|
316
|
+
headers: { "Content-Type": "application/json" },
|
|
317
|
+
body: JSON.stringify({ url }),
|
|
318
|
+
signal: AbortSignal.timeout(30000),
|
|
319
|
+
});
|
|
320
|
+
if (!res.ok)
|
|
321
|
+
return null;
|
|
322
|
+
const data = await res.json();
|
|
323
|
+
return data;
|
|
119
324
|
}
|
|
120
|
-
|
|
121
|
-
|
|
325
|
+
catch {
|
|
326
|
+
return null;
|
|
122
327
|
}
|
|
123
|
-
lines.push("");
|
|
124
|
-
return lines.join("\n");
|
|
125
328
|
}
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
329
|
+
async function researchUser(params) {
|
|
330
|
+
try {
|
|
331
|
+
const res = await fetch(RESEARCH_URL, {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: { "Content-Type": "application/json" },
|
|
334
|
+
body: JSON.stringify(params),
|
|
335
|
+
signal: AbortSignal.timeout(60000),
|
|
336
|
+
});
|
|
337
|
+
if (!res.ok)
|
|
338
|
+
return null;
|
|
339
|
+
const data = await res.json();
|
|
340
|
+
return data;
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
133
345
|
}
|
|
134
|
-
function
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
346
|
+
function displayScrapeResult(label, data) {
|
|
347
|
+
console.log("");
|
|
348
|
+
console.log(" " + chalk_1.default.bold(`${label} profile:`));
|
|
349
|
+
if (data.name)
|
|
350
|
+
console.log(" name: " + chalk_1.default.cyan(data.name));
|
|
351
|
+
if (data.bio)
|
|
352
|
+
console.log(" bio: " + chalk_1.default.dim(data.bio.slice(0, 120)));
|
|
353
|
+
if (data.followers !== undefined)
|
|
354
|
+
console.log(" followers: " + chalk_1.default.cyan(String(data.followers)));
|
|
355
|
+
if (data.following !== undefined)
|
|
356
|
+
console.log(" following: " + chalk_1.default.cyan(String(data.following)));
|
|
357
|
+
if (data.location)
|
|
358
|
+
console.log(" location: " + chalk_1.default.dim(data.location));
|
|
359
|
+
if (data.website)
|
|
360
|
+
console.log(" website: " + chalk_1.default.dim(data.website));
|
|
361
|
+
console.log("");
|
|
141
362
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
363
|
+
// ─── Spinner ──────────────────────────────────────────────────────────
|
|
364
|
+
class Spinner {
|
|
365
|
+
constructor(label) {
|
|
366
|
+
this.interval = null;
|
|
367
|
+
this.frames = [" ", ". ", ".. ", "..."];
|
|
368
|
+
this.frameIndex = 0;
|
|
369
|
+
this.label = label || randomThinking();
|
|
370
|
+
}
|
|
371
|
+
start() {
|
|
372
|
+
process.stdout.write(chalk_1.default.dim(` ${this.label}...`));
|
|
373
|
+
this.interval = setInterval(() => {
|
|
374
|
+
this.frameIndex = (this.frameIndex + 1) % this.frames.length;
|
|
375
|
+
const f = this.frames[this.frameIndex];
|
|
376
|
+
process.stdout.write(`\r${chalk_1.default.dim(` ${this.label}${f}`)}`);
|
|
377
|
+
}, 300);
|
|
378
|
+
}
|
|
379
|
+
stop() {
|
|
380
|
+
if (this.interval) {
|
|
381
|
+
clearInterval(this.interval);
|
|
382
|
+
this.interval = null;
|
|
383
|
+
}
|
|
384
|
+
process.stdout.write("\r" + " ".repeat(80) + "\r");
|
|
385
|
+
}
|
|
149
386
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
387
|
+
exports.Spinner = Spinner;
|
|
388
|
+
async function callLLM(apiKey, messages) {
|
|
389
|
+
// Try the you.md proxy first (no API key needed)
|
|
390
|
+
try {
|
|
391
|
+
const proxyRes = await fetch(CHAT_PROXY_URL, {
|
|
392
|
+
method: "POST",
|
|
393
|
+
headers: { "Content-Type": "application/json" },
|
|
394
|
+
body: JSON.stringify({ messages }),
|
|
395
|
+
signal: AbortSignal.timeout(60000),
|
|
396
|
+
});
|
|
397
|
+
if (proxyRes.ok) {
|
|
398
|
+
const proxyData = (await proxyRes.json());
|
|
399
|
+
if (proxyData.content)
|
|
400
|
+
return proxyData.content;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
// Proxy failed — fall through to direct call if key available
|
|
405
|
+
}
|
|
406
|
+
// Fall back to direct OpenRouter call if user has their own key
|
|
407
|
+
if (!apiKey) {
|
|
408
|
+
throw new Error("Chat service unavailable");
|
|
409
|
+
}
|
|
410
|
+
const res = await fetch(OPENROUTER_URL, {
|
|
411
|
+
method: "POST",
|
|
412
|
+
headers: {
|
|
413
|
+
Authorization: `Bearer ${apiKey}`,
|
|
414
|
+
"Content-Type": "application/json",
|
|
415
|
+
"HTTP-Referer": "https://you.md",
|
|
416
|
+
"X-Title": "you.md CLI",
|
|
417
|
+
},
|
|
418
|
+
body: JSON.stringify({
|
|
419
|
+
model: OPENROUTER_MODEL,
|
|
420
|
+
messages,
|
|
421
|
+
max_tokens: 4096,
|
|
422
|
+
temperature: 0.7,
|
|
423
|
+
}),
|
|
424
|
+
signal: AbortSignal.timeout(60000),
|
|
425
|
+
});
|
|
426
|
+
if (!res.ok) {
|
|
427
|
+
const body = await res.text();
|
|
428
|
+
throw new Error(`OpenRouter API error (${res.status}): ${body}`);
|
|
429
|
+
}
|
|
430
|
+
const data = (await res.json());
|
|
431
|
+
if (!data.choices || data.choices.length === 0) {
|
|
432
|
+
throw new Error("Empty response from OpenRouter");
|
|
433
|
+
}
|
|
434
|
+
return data.choices[0].message.content;
|
|
435
|
+
}
|
|
436
|
+
function parseUpdatesFromResponse(text) {
|
|
437
|
+
// Try to extract JSON block with {"updates": [...]}
|
|
438
|
+
const jsonMatch = text.match(/```json\s*\n([\s\S]*?)\n```/);
|
|
439
|
+
let updates = [];
|
|
440
|
+
let display = text;
|
|
441
|
+
if (jsonMatch) {
|
|
442
|
+
// Remove the JSON block from display text
|
|
443
|
+
display = text.replace(/```json\s*\n[\s\S]*?\n```/, "").trim();
|
|
444
|
+
try {
|
|
445
|
+
const parsed = JSON.parse(jsonMatch[1]);
|
|
446
|
+
// Handle both formats: {"updates": [...]} and bare [...]
|
|
447
|
+
const arr = Array.isArray(parsed)
|
|
448
|
+
? parsed
|
|
449
|
+
: Array.isArray(parsed.updates)
|
|
450
|
+
? parsed.updates
|
|
451
|
+
: [];
|
|
452
|
+
updates = arr.filter((u) => u &&
|
|
453
|
+
typeof u.section === "string" &&
|
|
454
|
+
typeof u.content === "string" &&
|
|
455
|
+
BUNDLE_SECTIONS.includes(u.section));
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// Failed to parse JSON updates -- continue without them
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return { display, updates };
|
|
462
|
+
}
|
|
463
|
+
function writeSectionFile(bundleDir, section, content) {
|
|
464
|
+
const filePath = path.join(bundleDir, section);
|
|
465
|
+
const dir = path.dirname(filePath);
|
|
466
|
+
if (!fs.existsSync(dir)) {
|
|
467
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
468
|
+
}
|
|
469
|
+
fs.writeFileSync(filePath, content);
|
|
470
|
+
}
|
|
471
|
+
function sectionLabel(section) {
|
|
472
|
+
const name = path.basename(section, ".md");
|
|
473
|
+
const dir = path.dirname(section);
|
|
474
|
+
return `${dir}/${name}`;
|
|
158
475
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
476
|
+
// ─── Profile preview box ──────────────────────────────────────────────
|
|
477
|
+
function showProfileBox(name, headline, tags) {
|
|
478
|
+
const lines = [name, headline, tags].filter(Boolean);
|
|
479
|
+
const maxLen = Math.max(...lines.map((l) => l.length), 40);
|
|
480
|
+
const width = maxLen + 4;
|
|
481
|
+
console.log("");
|
|
482
|
+
console.log(" " + chalk_1.default.dim("\u250C" + "\u2500".repeat(width) + "\u2510"));
|
|
483
|
+
for (const line of lines) {
|
|
484
|
+
const padded = line + " ".repeat(width - line.length - 2);
|
|
485
|
+
console.log(" " + chalk_1.default.dim("\u2502") + " " + padded + chalk_1.default.dim("\u2502"));
|
|
486
|
+
}
|
|
487
|
+
console.log(" " + chalk_1.default.dim("\u2514" + "\u2500".repeat(width) + "\u2518"));
|
|
488
|
+
console.log("");
|
|
489
|
+
}
|
|
490
|
+
// ─── Bundle preview ───────────────────────────────────────────────────
|
|
491
|
+
function showBundlePreview(bundleDir) {
|
|
492
|
+
let fileCount = 0;
|
|
493
|
+
let filledCount = 0;
|
|
494
|
+
console.log("");
|
|
495
|
+
console.log(" " + chalk_1.default.bold("your identity bundle:"));
|
|
496
|
+
console.log("");
|
|
497
|
+
const sections = [
|
|
498
|
+
{ dir: "profile", label: "profile" },
|
|
499
|
+
{ dir: "preferences", label: "preferences" },
|
|
500
|
+
];
|
|
501
|
+
for (const { dir, label } of sections) {
|
|
502
|
+
const dirPath = path.join(bundleDir, dir);
|
|
503
|
+
if (!fs.existsSync(dirPath))
|
|
504
|
+
continue;
|
|
505
|
+
const files = fs
|
|
506
|
+
.readdirSync(dirPath)
|
|
507
|
+
.filter((f) => f.endsWith(".md"))
|
|
508
|
+
.sort();
|
|
509
|
+
if (files.length === 0)
|
|
510
|
+
continue;
|
|
511
|
+
console.log(" " + chalk_1.default.bold(label));
|
|
512
|
+
for (let i = 0; i < files.length; i++) {
|
|
513
|
+
const file = files[i];
|
|
514
|
+
fileCount++;
|
|
515
|
+
const filePath = path.join(dirPath, file);
|
|
516
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
517
|
+
const contentLines = raw
|
|
518
|
+
.replace(/---[\s\S]*?---/, "")
|
|
519
|
+
.trim()
|
|
520
|
+
.split("\n");
|
|
521
|
+
const preview = contentLines[0] && contentLines[0].replace(/^#+\s*/, "").trim();
|
|
522
|
+
const isLast = i === files.length - 1;
|
|
523
|
+
const connector = isLast ? "\\--" : "|--";
|
|
524
|
+
const name = path.basename(file, ".md");
|
|
525
|
+
const hasContent = contentLines.filter((l) => l.trim() && !l.startsWith("<!--")).length > 0;
|
|
526
|
+
if (hasContent)
|
|
527
|
+
filledCount++;
|
|
528
|
+
console.log(` ${connector} ${chalk_1.default.cyan(name + ".md")}${hasContent ? chalk_1.default.dim(" -- " + (preview || "").slice(0, 50)) : chalk_1.default.dim(" (empty)")}`);
|
|
529
|
+
}
|
|
530
|
+
console.log("");
|
|
531
|
+
}
|
|
532
|
+
return { fileCount, filledCount };
|
|
533
|
+
}
|
|
534
|
+
async function runFallbackMode(rl, info) {
|
|
535
|
+
const bundleDir = (0, config_1.getLocalBundleDir)();
|
|
536
|
+
const profileDir = path.join(bundleDir, "profile");
|
|
537
|
+
const preferencesDir = path.join(bundleDir, "preferences");
|
|
538
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
539
|
+
fs.mkdirSync(preferencesDir, { recursive: true });
|
|
540
|
+
console.log("");
|
|
541
|
+
console.log(chalk_1.default.dim(" chat service unavailable. running in manual mode."));
|
|
542
|
+
console.log("");
|
|
543
|
+
const tagline = await ask(rl, chalk_1.default.green(" > ") + "give me your one-liner. what do you do? ");
|
|
544
|
+
const nowFocus = await ask(rl, chalk_1.default.green(" > ") + "what are you focused on right now? ");
|
|
545
|
+
const projects = await ask(rl, chalk_1.default.green(" > ") + "name your top projects (comma-separated): ");
|
|
546
|
+
const values = await ask(rl, chalk_1.default.green(" > ") + "what principles guide your work? ");
|
|
547
|
+
const agentPrefs = await ask(rl, chalk_1.default.green(" > ") +
|
|
548
|
+
"how should AI agents talk to you? (e.g., direct, casual, formal): ");
|
|
549
|
+
writeSectionFile(bundleDir, "profile/about.md", `---\ntitle: "About"\n---\n\n# ${info.name}\n\n${tagline}\n`);
|
|
550
|
+
writeSectionFile(bundleDir, "profile/now.md", `---\ntitle: "Now"\n---\n\n${nowFocus || "<!-- What are you working on right now? -->"}\n`);
|
|
551
|
+
const projectList = projects
|
|
552
|
+
? projects
|
|
553
|
+
.split(",")
|
|
554
|
+
.map((p) => p.trim())
|
|
555
|
+
.filter(Boolean)
|
|
556
|
+
.map((p) => `## ${p}\n`)
|
|
557
|
+
.join("\n")
|
|
558
|
+
: "<!-- List your projects here -->\n";
|
|
559
|
+
writeSectionFile(bundleDir, "profile/projects.md", `---\ntitle: "Projects"\n---\n\n${projectList}`);
|
|
560
|
+
writeSectionFile(bundleDir, "profile/values.md", `---\ntitle: "Values"\n---\n\n${values || "<!-- What do you care about? -->"}\n`);
|
|
561
|
+
const linkLines = [];
|
|
562
|
+
if (info.website)
|
|
563
|
+
linkLines.push(`- **Website**: ${info.website}`);
|
|
564
|
+
if (info.linkedin)
|
|
565
|
+
linkLines.push(`- **LinkedIn**: ${info.linkedin}`);
|
|
566
|
+
if (info.twitter)
|
|
567
|
+
linkLines.push(`- **X/Twitter**: ${info.twitter}`);
|
|
568
|
+
if (info.github)
|
|
569
|
+
linkLines.push(`- **GitHub**: ${info.github}`);
|
|
570
|
+
writeSectionFile(bundleDir, "profile/links.md", `---\ntitle: "Links"\n---\n\n${linkLines.length > 0 ? linkLines.join("\n") : "<!-- Add your links here -->"}\n`);
|
|
571
|
+
writeSectionFile(bundleDir, "preferences/agent.md", `---\ntitle: "Agent"\n---\n\n${agentPrefs ? `Communication style: ${agentPrefs}` : "<!-- How should AI agents interact with you? -->"}\n`);
|
|
572
|
+
writeSectionFile(bundleDir, "preferences/writing.md", `---\ntitle: "Writing"\n---\n\n<!-- Your writing style and tone preferences. -->\n`);
|
|
573
|
+
(0, config_1.writeLocalConfig)({ version: 0, sources: [] });
|
|
574
|
+
await finishBundle(bundleDir, info.username, info.name);
|
|
575
|
+
}
|
|
576
|
+
// ─── AI conversation mode ─────────────────────────────────────────────
|
|
577
|
+
async function runAIMode(rl, info, apiKey, scraped, research) {
|
|
578
|
+
const bundleDir = (0, config_1.getLocalBundleDir)();
|
|
579
|
+
const profileDir = path.join(bundleDir, "profile");
|
|
580
|
+
const preferencesDir = path.join(bundleDir, "preferences");
|
|
581
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
582
|
+
fs.mkdirSync(preferencesDir, { recursive: true });
|
|
583
|
+
(0, config_1.writeLocalConfig)({ version: 0, sources: [] });
|
|
584
|
+
// ── Fetch website content if provided ──────────────────────────────
|
|
585
|
+
let websiteContent = "";
|
|
586
|
+
if (info.website) {
|
|
587
|
+
const fetchSpinner = new Spinner("reading your site");
|
|
588
|
+
fetchSpinner.start();
|
|
589
|
+
websiteContent = await fetchWebsiteContent(info.website);
|
|
590
|
+
fetchSpinner.stop();
|
|
591
|
+
if (websiteContent) {
|
|
592
|
+
console.log(chalk_1.default.dim(" pulled content from ") +
|
|
593
|
+
chalk_1.default.cyan(info.website) +
|
|
594
|
+
chalk_1.default.dim(` (${websiteContent.length} chars)`));
|
|
595
|
+
console.log("");
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
console.log(chalk_1.default.dim(" couldn't reach ") +
|
|
599
|
+
chalk_1.default.cyan(info.website) +
|
|
600
|
+
chalk_1.default.dim(" -- no worries, we'll work with what you tell me."));
|
|
601
|
+
console.log("");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// ── Build initial context ──────────────────────────────────────────
|
|
605
|
+
const linksInfo = [];
|
|
606
|
+
if (info.website)
|
|
607
|
+
linksInfo.push(`Website: ${info.website}`);
|
|
608
|
+
if (info.linkedin)
|
|
609
|
+
linksInfo.push(`LinkedIn: ${info.linkedin}`);
|
|
610
|
+
if (info.twitter)
|
|
611
|
+
linksInfo.push(`X/Twitter: ${info.twitter}`);
|
|
612
|
+
if (info.github)
|
|
613
|
+
linksInfo.push(`GitHub: ${info.github}`);
|
|
614
|
+
let initialUserMessage = `here's what i know so far:
|
|
615
|
+
- name: ${info.name}
|
|
616
|
+
- username: ${info.username}
|
|
617
|
+
${linksInfo.length > 0 ? linksInfo.map((l) => `- ${l}`).join("\n") : "- no links provided"}`;
|
|
618
|
+
// Add scraped social profile data
|
|
619
|
+
if (scraped?.twitter) {
|
|
620
|
+
const t = scraped.twitter;
|
|
621
|
+
initialUserMessage += `\n\ni scraped their X/Twitter profile:`;
|
|
622
|
+
if (t.name)
|
|
623
|
+
initialUserMessage += `\n- display name: ${t.name}`;
|
|
624
|
+
if (t.bio)
|
|
625
|
+
initialUserMessage += `\n- bio: ${t.bio}`;
|
|
626
|
+
if (t.followers !== undefined)
|
|
627
|
+
initialUserMessage += `\n- followers: ${t.followers}`;
|
|
628
|
+
if (t.location)
|
|
629
|
+
initialUserMessage += `\n- location: ${t.location}`;
|
|
630
|
+
if (t.posts && t.posts.length > 0) {
|
|
631
|
+
initialUserMessage += `\n- recent posts:`;
|
|
632
|
+
for (const post of t.posts.slice(0, 5)) {
|
|
633
|
+
initialUserMessage += `\n - "${post.text.slice(0, 200)}"`;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (scraped?.github) {
|
|
638
|
+
const g = scraped.github;
|
|
639
|
+
initialUserMessage += `\n\ni scraped their GitHub profile:`;
|
|
640
|
+
if (g.name)
|
|
641
|
+
initialUserMessage += `\n- display name: ${g.name}`;
|
|
642
|
+
if (g.bio)
|
|
643
|
+
initialUserMessage += `\n- bio: ${g.bio}`;
|
|
644
|
+
if (g.followers !== undefined)
|
|
645
|
+
initialUserMessage += `\n- followers: ${g.followers}`;
|
|
646
|
+
if (g.location)
|
|
647
|
+
initialUserMessage += `\n- location: ${g.location}`;
|
|
648
|
+
}
|
|
649
|
+
// Add Perplexity research results
|
|
650
|
+
if (research) {
|
|
651
|
+
const researchText = research.summary ||
|
|
652
|
+
research.content ||
|
|
653
|
+
(research.findings ? research.findings.join("\n") : null);
|
|
654
|
+
if (researchText) {
|
|
655
|
+
initialUserMessage += `\n\ni also ran deep research (via Perplexity) on this person. here's what i found:\n---\n${researchText.slice(0, 4000)}\n---`;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (websiteContent) {
|
|
659
|
+
initialUserMessage += `
|
|
660
|
+
|
|
661
|
+
i also fetched their website content. here's what the site says:
|
|
662
|
+
---
|
|
663
|
+
${websiteContent}
|
|
162
664
|
---
|
|
163
665
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
666
|
+
analyze everything you know -- the scraped profiles, research, and website content. comment on what you found -- be specific about their work, role, company, anything interesting. then generate initial profile sections from everything you know. after showing what you found, ask what else they want to add.`;
|
|
667
|
+
}
|
|
668
|
+
else if (scraped?.twitter || scraped?.github || research) {
|
|
669
|
+
initialUserMessage += `
|
|
670
|
+
|
|
671
|
+
analyze everything you know from the scraped profiles and research. comment on what you found -- be specific about their work, background, anything interesting. then generate initial profile sections. after showing what you found, ask what else they want to add.`;
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
initialUserMessage += `
|
|
675
|
+
|
|
676
|
+
generate initial profile sections from what you know, show a brief summary, and ask conversational follow-up questions to learn more.`;
|
|
677
|
+
}
|
|
678
|
+
const messages = [
|
|
679
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
680
|
+
{ role: "user", content: initialUserMessage },
|
|
681
|
+
];
|
|
682
|
+
// ── Initial LLM call ──────────────────────────────────────────────
|
|
683
|
+
let spinner = new Spinner(randomThinking());
|
|
684
|
+
spinner.start();
|
|
685
|
+
let response;
|
|
686
|
+
try {
|
|
687
|
+
response = await callLLM(apiKey, messages);
|
|
688
|
+
}
|
|
689
|
+
catch (err) {
|
|
690
|
+
spinner.stop();
|
|
691
|
+
console.log(chalk_1.default.red(` failed to connect to AI: ${err instanceof Error ? err.message : String(err)}`));
|
|
692
|
+
console.log(chalk_1.default.dim(" falling back to manual mode."));
|
|
693
|
+
console.log("");
|
|
694
|
+
await runFallbackMode(rl, info);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
spinner.stop();
|
|
698
|
+
messages.push({ role: "assistant", content: response });
|
|
699
|
+
const initial = parseUpdatesFromResponse(response);
|
|
700
|
+
// Write initial sections
|
|
701
|
+
if (initial.updates.length > 0) {
|
|
702
|
+
for (const update of initial.updates) {
|
|
703
|
+
writeSectionFile(bundleDir, update.section, update.content);
|
|
704
|
+
}
|
|
705
|
+
console.log(chalk_1.default.cyan(` [wrote ${initial.updates.length} section${initial.updates.length === 1 ? "" : "s"}: ${initial.updates.map((u) => sectionLabel(u.section)).join(", ")}]`));
|
|
706
|
+
console.log("");
|
|
707
|
+
}
|
|
708
|
+
// Show agent message
|
|
709
|
+
printAgentMessage(initial.display);
|
|
710
|
+
// Show mini profile box after initial generation
|
|
711
|
+
if (initial.updates.length > 0) {
|
|
712
|
+
const aboutUpdate = initial.updates.find((u) => u.section === "profile/about.md");
|
|
713
|
+
if (aboutUpdate) {
|
|
714
|
+
// Extract headline from about content
|
|
715
|
+
const lines = aboutUpdate.content
|
|
716
|
+
.replace(/---[\s\S]*?---/, "")
|
|
717
|
+
.trim()
|
|
718
|
+
.split("\n")
|
|
719
|
+
.filter((l) => l.trim() && !l.startsWith("#"));
|
|
720
|
+
const headline = lines[0] || "";
|
|
721
|
+
showProfileBox(info.name, headline.slice(0, 60), "");
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
console.log(chalk_1.default.dim(" want to tell me more? i can ask about your projects, what you're working on now,"));
|
|
725
|
+
console.log(chalk_1.default.dim(' your values, how you like AI to talk to you -- or just tell me anything.'));
|
|
726
|
+
console.log(chalk_1.default.dim(' type "done" when you\'re ready to publish.'));
|
|
727
|
+
console.log("");
|
|
728
|
+
// ── Conversation loop ──────────────────────────────────────────────
|
|
729
|
+
let exchangeCount = 0;
|
|
730
|
+
while (true) {
|
|
731
|
+
const userInput = await ask(rl, chalk_1.default.green(" > ") + "");
|
|
732
|
+
if (!userInput)
|
|
733
|
+
continue;
|
|
734
|
+
if (isDonePhrase(userInput)) {
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
messages.push({ role: "user", content: userInput });
|
|
738
|
+
exchangeCount++;
|
|
739
|
+
// After 3+ exchanges, hint to the agent it can suggest wrapping up
|
|
740
|
+
if (exchangeCount >= 3) {
|
|
741
|
+
const hintMsg = {
|
|
742
|
+
role: "system",
|
|
743
|
+
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.",
|
|
744
|
+
};
|
|
745
|
+
messages.push(hintMsg);
|
|
746
|
+
}
|
|
747
|
+
spinner = new Spinner(randomThinking());
|
|
748
|
+
spinner.start();
|
|
749
|
+
try {
|
|
750
|
+
response = await callLLM(apiKey, messages);
|
|
751
|
+
}
|
|
752
|
+
catch (err) {
|
|
753
|
+
spinner.stop();
|
|
754
|
+
console.log(chalk_1.default.red(` AI error: ${err instanceof Error ? err.message : String(err)}`));
|
|
755
|
+
console.log(chalk_1.default.dim(" try again, or type 'done' to finish."));
|
|
756
|
+
console.log("");
|
|
757
|
+
// Remove failed messages
|
|
758
|
+
messages.pop(); // remove hint if added
|
|
759
|
+
if (exchangeCount >= 3)
|
|
760
|
+
messages.pop();
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
spinner.stop();
|
|
764
|
+
// Remove the hint message from history (don't pollute context)
|
|
765
|
+
if (exchangeCount >= 3) {
|
|
766
|
+
messages.pop(); // remove the hint
|
|
767
|
+
}
|
|
768
|
+
messages.push({ role: "assistant", content: response });
|
|
769
|
+
const parsed = parseUpdatesFromResponse(response);
|
|
770
|
+
// Write updates
|
|
771
|
+
if (parsed.updates.length > 0) {
|
|
772
|
+
for (const update of parsed.updates) {
|
|
773
|
+
writeSectionFile(bundleDir, update.section, update.content);
|
|
774
|
+
}
|
|
775
|
+
console.log(chalk_1.default.cyan(` [updated: ${parsed.updates.map((u) => sectionLabel(u.section)).join(", ")}]`));
|
|
776
|
+
console.log("");
|
|
777
|
+
}
|
|
778
|
+
// Show agent message
|
|
779
|
+
printAgentMessage(parsed.display);
|
|
780
|
+
// Check if agent is suggesting we're done
|
|
781
|
+
const lowerDisplay = parsed.display.toLowerCase();
|
|
782
|
+
if (lowerDisplay.includes("ready to publish") ||
|
|
783
|
+
lowerDisplay.includes("bundle is looking solid") ||
|
|
784
|
+
lowerDisplay.includes("ready to go")) {
|
|
785
|
+
const answer = await ask(rl, chalk_1.default.green(" > ") + "");
|
|
786
|
+
if (isDonePhrase(answer) ||
|
|
787
|
+
answer.toLowerCase().includes("publish") ||
|
|
788
|
+
answer.toLowerCase().includes("yes") ||
|
|
789
|
+
answer.toLowerCase().includes("yeah") ||
|
|
790
|
+
answer.toLowerCase().includes("yep")) {
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
messages.push({ role: "user", content: answer });
|
|
795
|
+
spinner = new Spinner(randomThinking());
|
|
796
|
+
spinner.start();
|
|
797
|
+
try {
|
|
798
|
+
response = await callLLM(apiKey, messages);
|
|
799
|
+
}
|
|
800
|
+
catch (err) {
|
|
801
|
+
spinner.stop();
|
|
802
|
+
console.log(chalk_1.default.red(` AI error: ${err instanceof Error ? err.message : String(err)}`));
|
|
803
|
+
messages.pop();
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
spinner.stop();
|
|
807
|
+
messages.push({ role: "assistant", content: response });
|
|
808
|
+
const more = parseUpdatesFromResponse(response);
|
|
809
|
+
if (more.updates.length > 0) {
|
|
810
|
+
for (const update of more.updates) {
|
|
811
|
+
writeSectionFile(bundleDir, update.section, update.content);
|
|
812
|
+
}
|
|
813
|
+
console.log(chalk_1.default.cyan(` [updated: ${more.updates.map((u) => sectionLabel(u.section)).join(", ")}]`));
|
|
814
|
+
console.log("");
|
|
815
|
+
}
|
|
816
|
+
printAgentMessage(more.display);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
// Finish up
|
|
821
|
+
await finishBundle(bundleDir, info.username, info.name);
|
|
167
822
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
823
|
+
function printAgentMessage(text) {
|
|
824
|
+
if (!text)
|
|
825
|
+
return;
|
|
826
|
+
const lines = text.split("\n");
|
|
827
|
+
for (const line of lines) {
|
|
828
|
+
console.log(" " + line);
|
|
829
|
+
}
|
|
830
|
+
console.log("");
|
|
831
|
+
}
|
|
832
|
+
// ─── Finish and compile ───────────────────────────────────────────────
|
|
833
|
+
async function finishBundle(bundleDir, username, name) {
|
|
834
|
+
console.log("");
|
|
835
|
+
const compileSpinner = new Spinner("compiling your context bundle");
|
|
836
|
+
compileSpinner.start();
|
|
837
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
838
|
+
const result = (0, compiler_1.compileBundle)(bundleDir);
|
|
839
|
+
(0, compiler_1.writeBundle)(bundleDir, result);
|
|
840
|
+
compileSpinner.stop();
|
|
841
|
+
console.log(" " +
|
|
842
|
+
chalk_1.default.green("done") +
|
|
843
|
+
chalk_1.default.dim(` -- bundle compiled (v${result.bundle.version})`));
|
|
844
|
+
// Show final preview with stats
|
|
845
|
+
const stats = showBundlePreview(bundleDir);
|
|
846
|
+
console.log(chalk_1.default.dim(` ${stats.fileCount} files, ${stats.filledCount} sections filled`));
|
|
847
|
+
console.log("");
|
|
848
|
+
// Context link
|
|
849
|
+
console.log(" " + chalk_1.default.bold("your context file is ready:"));
|
|
850
|
+
console.log(" " + chalk_1.default.cyan(`https://you.md/${username}/context`));
|
|
851
|
+
console.log("");
|
|
852
|
+
// Publish flow
|
|
853
|
+
if ((0, config_1.isAuthenticated)()) {
|
|
854
|
+
console.log(" you're authenticated. publish with: " +
|
|
855
|
+
chalk_1.default.cyan("youmd publish"));
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
console.log(" " + chalk_1.default.bold("to go live:"));
|
|
859
|
+
console.log(" 1. claim your username at " +
|
|
860
|
+
chalk_1.default.cyan("https://you.md/claim"));
|
|
861
|
+
console.log(" 2. " + chalk_1.default.cyan("youmd login --key <your-api-key>"));
|
|
862
|
+
console.log(" 3. " + chalk_1.default.cyan("youmd publish"));
|
|
863
|
+
}
|
|
864
|
+
console.log("");
|
|
865
|
+
console.log(" " + chalk_1.default.bold("using your identity with AI agents:"));
|
|
866
|
+
console.log(" add this to your system prompt or CLAUDE.md:");
|
|
867
|
+
console.log(chalk_1.default.dim(` "my identity file: https://you.md/${username}/context"`));
|
|
868
|
+
console.log("");
|
|
869
|
+
console.log(" " +
|
|
870
|
+
chalk_1.default.dim("run ") +
|
|
871
|
+
chalk_1.default.cyan("youmd build") +
|
|
872
|
+
chalk_1.default.dim(" anytime to recompile, or ") +
|
|
873
|
+
chalk_1.default.cyan("youmd chat") +
|
|
874
|
+
chalk_1.default.dim(" to keep editing with AI."));
|
|
875
|
+
console.log("");
|
|
876
|
+
console.log(" " + chalk_1.default.bold(`welcome to the agent internet, ${name}.`));
|
|
877
|
+
console.log("");
|
|
172
878
|
}
|
|
173
879
|
async function runOnboarding() {
|
|
174
880
|
const rl = createRL();
|
|
175
881
|
console.log("");
|
|
176
882
|
console.log(" " + chalk_1.default.bold("you.md"));
|
|
177
|
-
console.log(" your identity file for the agent internet");
|
|
178
|
-
console.log("");
|
|
179
|
-
console.log(" Agents have soul.md. Humans need you.md.");
|
|
883
|
+
console.log(" " + chalk_1.default.dim("your identity file for the agent internet"));
|
|
180
884
|
console.log("");
|
|
181
|
-
// ──
|
|
885
|
+
// ── Phase 1: Identity basics (fast, no LLM) ────────────────────────
|
|
886
|
+
// Username
|
|
182
887
|
let username = "";
|
|
183
888
|
let usernameValid = false;
|
|
184
889
|
while (!usernameValid) {
|
|
185
|
-
username = await ask(rl, chalk_1.default.green("
|
|
890
|
+
username = await ask(rl, chalk_1.default.green(" > ") + "pick a username: ");
|
|
186
891
|
if (!username) {
|
|
187
892
|
console.log(chalk_1.default.red(" username is required"));
|
|
188
893
|
continue;
|
|
@@ -193,10 +898,10 @@ async function runOnboarding() {
|
|
|
193
898
|
continue;
|
|
194
899
|
}
|
|
195
900
|
username = username.toLowerCase();
|
|
196
|
-
process.stdout.write(" checking... ");
|
|
901
|
+
process.stdout.write(chalk_1.default.dim(" checking... "));
|
|
197
902
|
const result = await checkUsernameRemote(username);
|
|
198
903
|
if (result.available) {
|
|
199
|
-
console.log(chalk_1.default.green(username + " is
|
|
904
|
+
console.log(chalk_1.default.green(username + " is yours."));
|
|
200
905
|
usernameValid = true;
|
|
201
906
|
}
|
|
202
907
|
else {
|
|
@@ -205,98 +910,144 @@ async function runOnboarding() {
|
|
|
205
910
|
}
|
|
206
911
|
}
|
|
207
912
|
console.log("");
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
913
|
+
const name = await ask(rl, chalk_1.default.green(" > ") + "what's your name? ");
|
|
914
|
+
const website = await ask(rl, chalk_1.default.green(" > ") +
|
|
915
|
+
"website URL " +
|
|
916
|
+
chalk_1.default.dim("(optional)") +
|
|
917
|
+
": ");
|
|
918
|
+
const twitter = await ask(rl, chalk_1.default.green(" > ") +
|
|
919
|
+
"X/Twitter username " +
|
|
920
|
+
chalk_1.default.dim("(optional, e.g. @houston)") +
|
|
921
|
+
": ");
|
|
922
|
+
const github = await ask(rl, chalk_1.default.green(" > ") +
|
|
923
|
+
"GitHub username " +
|
|
924
|
+
chalk_1.default.dim("(optional)") +
|
|
925
|
+
": ");
|
|
926
|
+
const linkedin = await ask(rl, chalk_1.default.green(" > ") +
|
|
927
|
+
"LinkedIn URL " +
|
|
928
|
+
chalk_1.default.dim("(optional)") +
|
|
929
|
+
": ");
|
|
215
930
|
console.log("");
|
|
216
|
-
// ──
|
|
217
|
-
|
|
931
|
+
// ── Scrape social profiles ────────────────────────────────────────
|
|
932
|
+
const twitterHandle = (twitter || "").replace(/^@/, "").trim();
|
|
933
|
+
const githubHandle = (github || "").trim();
|
|
934
|
+
let twitterData = null;
|
|
935
|
+
let githubData = null;
|
|
936
|
+
if (twitterHandle) {
|
|
937
|
+
const scrapeSpinner = new Spinner("scanning your X profile");
|
|
938
|
+
scrapeSpinner.start();
|
|
939
|
+
twitterData = await scrapeProfile(`https://x.com/${twitterHandle}`);
|
|
940
|
+
scrapeSpinner.stop();
|
|
941
|
+
if (twitterData) {
|
|
942
|
+
displayScrapeResult("X", twitterData);
|
|
943
|
+
}
|
|
944
|
+
else {
|
|
945
|
+
console.log(chalk_1.default.dim(" couldn't pull X profile -- no worries."));
|
|
946
|
+
console.log("");
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (githubHandle) {
|
|
950
|
+
const scrapeSpinner = new Spinner("reading your GitHub");
|
|
951
|
+
scrapeSpinner.start();
|
|
952
|
+
githubData = await scrapeProfile(`https://github.com/${githubHandle}`);
|
|
953
|
+
scrapeSpinner.stop();
|
|
954
|
+
if (githubData) {
|
|
955
|
+
displayScrapeResult("GitHub", githubData);
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
console.log(chalk_1.default.dim(" couldn't pull GitHub profile -- no worries."));
|
|
959
|
+
console.log("");
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
// ── Research the user via Perplexity ──────────────────────────────
|
|
963
|
+
const links = [];
|
|
964
|
+
if (website)
|
|
965
|
+
links.push(website);
|
|
966
|
+
if (twitterHandle)
|
|
967
|
+
links.push(`https://x.com/${twitterHandle}`);
|
|
968
|
+
if (githubHandle)
|
|
969
|
+
links.push(`https://github.com/${githubHandle}`);
|
|
970
|
+
if (linkedin)
|
|
971
|
+
links.push(linkedin);
|
|
972
|
+
let researchData = null;
|
|
973
|
+
if (name || twitterHandle || githubHandle) {
|
|
974
|
+
const researchSpinner = new Spinner("researching you across the internet");
|
|
975
|
+
researchSpinner.start();
|
|
976
|
+
researchData = await researchUser({
|
|
977
|
+
name: name || username,
|
|
978
|
+
username: twitterHandle || githubHandle || username,
|
|
979
|
+
links: links.length > 0 ? links : undefined,
|
|
980
|
+
});
|
|
981
|
+
researchSpinner.stop();
|
|
982
|
+
if (researchData) {
|
|
983
|
+
const summary = researchData.summary ||
|
|
984
|
+
researchData.content ||
|
|
985
|
+
(researchData.findings ? researchData.findings.join(" ") : null);
|
|
986
|
+
if (summary) {
|
|
987
|
+
console.log(chalk_1.default.dim(" research complete -- found context about you."));
|
|
988
|
+
console.log("");
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
const twitterUrl = twitterHandle ? `https://x.com/${twitterHandle}` : "";
|
|
993
|
+
const githubUrl = githubHandle ? `https://github.com/${githubHandle}` : "";
|
|
994
|
+
const basicInfo = {
|
|
218
995
|
username,
|
|
219
996
|
name: name || username,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
997
|
+
website: website || "",
|
|
998
|
+
linkedin: linkedin || "",
|
|
999
|
+
twitter: twitterUrl,
|
|
1000
|
+
github: githubUrl,
|
|
1001
|
+
};
|
|
1002
|
+
// Check for existing bundle
|
|
1003
|
+
const bundleDir = (0, config_1.getLocalBundleDir)();
|
|
1004
|
+
if (fs.existsSync(bundleDir)) {
|
|
1005
|
+
console.log(chalk_1.default.yellow(" .youmd/ already exists. overwriting profile files."));
|
|
1006
|
+
console.log("");
|
|
1007
|
+
}
|
|
1008
|
+
// ── Phase 2: AI conversation ──────────────────────────────────────
|
|
1009
|
+
const userApiKey = getOpenRouterKey();
|
|
1010
|
+
console.log(chalk_1.default.dim(" cool. let's build your identity."));
|
|
1011
|
+
console.log("");
|
|
1012
|
+
try {
|
|
1013
|
+
await runAIMode(rl, basicInfo, userApiKey, { twitter: twitterData, github: githubData }, researchData);
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
console.log(chalk_1.default.dim(" switching to manual mode."));
|
|
1017
|
+
await runFallbackMode(rl, basicInfo);
|
|
1018
|
+
}
|
|
1019
|
+
rl.close();
|
|
225
1020
|
}
|
|
1021
|
+
// Re-export for backward compatibility with create.ts
|
|
226
1022
|
async function createBundle(info) {
|
|
227
1023
|
const bundleDir = (0, config_1.getLocalBundleDir)();
|
|
228
1024
|
const profileDir = path.join(bundleDir, "profile");
|
|
229
1025
|
const preferencesDir = path.join(bundleDir, "preferences");
|
|
230
|
-
// Check if directory already exists
|
|
231
|
-
if (fs.existsSync(bundleDir)) {
|
|
232
|
-
console.log(chalk_1.default.yellow(" warning: .youmd/ directory already exists. Overwriting profile files."));
|
|
233
|
-
console.log("");
|
|
234
|
-
}
|
|
235
|
-
console.log(" Building your identity bundle...");
|
|
236
|
-
// Create directories
|
|
237
1026
|
fs.mkdirSync(profileDir, { recursive: true });
|
|
238
1027
|
fs.mkdirSync(preferencesDir, { recursive: true });
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
fs.writeFileSync(path.join(preferencesDir, "writing.md"), generateWritingMd());
|
|
257
|
-
// Write local config
|
|
258
|
-
(0, config_1.writeLocalConfig)({
|
|
259
|
-
version: 0,
|
|
260
|
-
sources: [],
|
|
261
|
-
});
|
|
262
|
-
// Compile the bundle
|
|
1028
|
+
writeSectionFile(bundleDir, "profile/about.md", `---\ntitle: "About"\n---\n\n# ${info.name}\n\n${info.tagline}\n`);
|
|
1029
|
+
const linkLines = [];
|
|
1030
|
+
if (info.website)
|
|
1031
|
+
linkLines.push(`- **Website**: ${info.website}`);
|
|
1032
|
+
if (info.linkedin)
|
|
1033
|
+
linkLines.push(`- **LinkedIn**: ${info.linkedin}`);
|
|
1034
|
+
if (info.twitter)
|
|
1035
|
+
linkLines.push(`- **X/Twitter**: ${info.twitter}`);
|
|
1036
|
+
if (info.github)
|
|
1037
|
+
linkLines.push(`- **GitHub**: ${info.github}`);
|
|
1038
|
+
writeSectionFile(bundleDir, "profile/links.md", `---\ntitle: "Links"\n---\n\n${linkLines.length > 0 ? linkLines.join("\n") : "<!-- Add your links here -->"}\n`);
|
|
1039
|
+
writeSectionFile(bundleDir, "profile/now.md", `---\ntitle: "Now"\n---\n\n<!-- What are you working on right now? -->\n`);
|
|
1040
|
+
writeSectionFile(bundleDir, "profile/projects.md", `---\ntitle: "Projects"\n---\n\n<!-- Your projects go here -->\n`);
|
|
1041
|
+
writeSectionFile(bundleDir, "profile/values.md", `---\ntitle: "Values"\n---\n\n<!-- What principles guide your work? -->\n`);
|
|
1042
|
+
writeSectionFile(bundleDir, "preferences/agent.md", `---\ntitle: "Agent"\n---\n\n<!-- How should AI agents interact with you? -->\n`);
|
|
1043
|
+
writeSectionFile(bundleDir, "preferences/writing.md", `---\ntitle: "Writing"\n---\n\n<!-- Your writing style and tone. -->\n`);
|
|
1044
|
+
(0, config_1.writeLocalConfig)({ version: 0, sources: [] });
|
|
263
1045
|
const result = (0, compiler_1.compileBundle)(bundleDir);
|
|
264
|
-
treeStep("Compiling you.json", false);
|
|
265
|
-
treeStep("Generating you.md", false);
|
|
266
|
-
// Write compiled output
|
|
267
1046
|
(0, compiler_1.writeBundle)(bundleDir, result);
|
|
268
|
-
treeStep("Writing manifest.json", true);
|
|
269
|
-
console.log("");
|
|
270
1047
|
console.log(" " +
|
|
271
1048
|
chalk_1.default.green("done") +
|
|
272
|
-
|
|
273
|
-
result.bundle.version +
|
|
274
|
-
")");
|
|
275
|
-
console.log("");
|
|
276
|
-
// ── Preview ────────────────────────────────────────────────────────
|
|
277
|
-
console.log(" " + chalk_1.default.bold(info.name));
|
|
278
|
-
console.log(" " + info.tagline);
|
|
279
|
-
console.log("");
|
|
280
|
-
// ── Next steps ─────────────────────────────────────────────────────
|
|
281
|
-
console.log(" Next steps:");
|
|
282
|
-
console.log("");
|
|
283
|
-
console.log(" 1. Edit your profile in .youmd/profile/");
|
|
284
|
-
console.log(" Flesh out about.md, now.md, projects.md, values.md");
|
|
285
|
-
console.log("");
|
|
286
|
-
console.log(" 2. Build and preview");
|
|
287
|
-
console.log(" " + chalk_1.default.cyan("youmd build"));
|
|
288
|
-
console.log(" " + chalk_1.default.cyan("youmd preview"));
|
|
289
|
-
console.log("");
|
|
290
|
-
console.log(" 3. Create an account to publish");
|
|
291
|
-
console.log(" Visit " +
|
|
292
|
-
chalk_1.default.cyan("https://you.md/claim") +
|
|
293
|
-
' to claim "' +
|
|
294
|
-
info.username +
|
|
295
|
-
'"');
|
|
296
|
-
console.log(" Then: " + chalk_1.default.cyan("youmd login --key <your-api-key>"));
|
|
297
|
-
console.log(" Then: " + chalk_1.default.cyan("youmd publish"));
|
|
298
|
-
console.log("");
|
|
299
|
-
console.log(" Your bundle is ready. Make it yours.");
|
|
1049
|
+
chalk_1.default.dim(` -- bundle compiled (v${result.bundle.version})`));
|
|
300
1050
|
console.log("");
|
|
1051
|
+
showBundlePreview(bundleDir);
|
|
301
1052
|
}
|
|
302
1053
|
//# sourceMappingURL=onboarding.js.map
|