youmd 0.3.2 → 0.4.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.
Files changed (63) hide show
  1. package/dist/commands/chat.d.ts.map +1 -1
  2. package/dist/commands/chat.js +718 -52
  3. package/dist/commands/chat.js.map +1 -1
  4. package/dist/commands/diff.d.ts +1 -1
  5. package/dist/commands/diff.d.ts.map +1 -1
  6. package/dist/commands/diff.js +261 -8
  7. package/dist/commands/diff.js.map +1 -1
  8. package/dist/commands/export.d.ts +8 -0
  9. package/dist/commands/export.d.ts.map +1 -0
  10. package/dist/commands/export.js +97 -0
  11. package/dist/commands/export.js.map +1 -0
  12. package/dist/commands/init.d.ts.map +1 -1
  13. package/dist/commands/init.js +38 -0
  14. package/dist/commands/init.js.map +1 -1
  15. package/dist/commands/keys.d.ts +2 -1
  16. package/dist/commands/keys.d.ts.map +1 -1
  17. package/dist/commands/keys.js +176 -40
  18. package/dist/commands/keys.js.map +1 -1
  19. package/dist/commands/link.d.ts +7 -0
  20. package/dist/commands/link.d.ts.map +1 -1
  21. package/dist/commands/link.js +235 -59
  22. package/dist/commands/link.js.map +1 -1
  23. package/dist/commands/memories.d.ts +2 -0
  24. package/dist/commands/memories.d.ts.map +1 -0
  25. package/dist/commands/memories.js +113 -0
  26. package/dist/commands/memories.js.map +1 -0
  27. package/dist/commands/private.d.ts +5 -0
  28. package/dist/commands/private.d.ts.map +1 -0
  29. package/dist/commands/private.js +554 -0
  30. package/dist/commands/private.js.map +1 -0
  31. package/dist/commands/project.d.ts +2 -0
  32. package/dist/commands/project.d.ts.map +1 -0
  33. package/dist/commands/project.js +435 -0
  34. package/dist/commands/project.js.map +1 -0
  35. package/dist/commands/pull.d.ts.map +1 -1
  36. package/dist/commands/pull.js +23 -0
  37. package/dist/commands/pull.js.map +1 -1
  38. package/dist/commands/push.d.ts.map +1 -1
  39. package/dist/commands/push.js +21 -0
  40. package/dist/commands/push.js.map +1 -1
  41. package/dist/index.js +37 -7
  42. package/dist/index.js.map +1 -1
  43. package/dist/lib/api.d.ts +85 -0
  44. package/dist/lib/api.d.ts.map +1 -1
  45. package/dist/lib/api.js +83 -0
  46. package/dist/lib/api.js.map +1 -1
  47. package/dist/lib/config.d.ts +51 -0
  48. package/dist/lib/config.d.ts.map +1 -1
  49. package/dist/lib/config.js +182 -0
  50. package/dist/lib/config.js.map +1 -1
  51. package/dist/lib/onboarding.d.ts +2 -1
  52. package/dist/lib/onboarding.d.ts.map +1 -1
  53. package/dist/lib/onboarding.js +18 -11
  54. package/dist/lib/onboarding.js.map +1 -1
  55. package/dist/lib/project.d.ts +87 -0
  56. package/dist/lib/project.d.ts.map +1 -0
  57. package/dist/lib/project.js +345 -0
  58. package/dist/lib/project.js.map +1 -0
  59. package/dist/lib/render.d.ts +71 -0
  60. package/dist/lib/render.d.ts.map +1 -0
  61. package/dist/lib/render.js +286 -0
  62. package/dist/lib/render.js.map +1 -0
  63. package/package.json +1 -1
@@ -42,50 +42,403 @@ const fs = __importStar(require("fs"));
42
42
  const path = __importStar(require("path"));
43
43
  const chalk_1 = __importDefault(require("chalk"));
44
44
  const config_1 = require("../lib/config");
45
+ const project_1 = require("../lib/project");
45
46
  const compiler_1 = require("../lib/compiler");
46
47
  const api_1 = require("../lib/api");
48
+ const render_1 = require("../lib/render");
47
49
  const onboarding_1 = require("../lib/onboarding");
50
+ // ─── URL Detection + Scraping (mirrors web useYouAgent) ──────────────
51
+ const CONVEX_SITE_URL = "https://kindly-cassowary-600.convex.site";
52
+ const STREAM_URL = `${CONVEX_SITE_URL}/api/v1/chat/stream`;
53
+ // ─── Streaming LLM client ─────────────────────────────────────────────
54
+ async function streamLLM(_apiKey, messages, onToken) {
55
+ const res = await fetch(STREAM_URL, {
56
+ method: "POST",
57
+ headers: { "Content-Type": "application/json" },
58
+ body: JSON.stringify({ messages }),
59
+ signal: AbortSignal.timeout(120000),
60
+ });
61
+ if (!res.ok) {
62
+ const body = await res.text();
63
+ throw new Error(`Stream error (${res.status}): ${body}`);
64
+ }
65
+ if (!res.body) {
66
+ throw new Error("No response body from stream endpoint");
67
+ }
68
+ const reader = res.body.getReader();
69
+ const decoder = new TextDecoder();
70
+ let fullText = "";
71
+ let buffer = "";
72
+ while (true) {
73
+ const { done, value } = await reader.read();
74
+ if (done)
75
+ break;
76
+ buffer += decoder.decode(value, { stream: true });
77
+ // Process complete SSE lines
78
+ const lines = buffer.split("\n");
79
+ // Keep the last potentially incomplete line in the buffer
80
+ buffer = lines.pop() || "";
81
+ for (const line of lines) {
82
+ const trimmed = line.trim();
83
+ if (!trimmed)
84
+ continue;
85
+ if (trimmed.startsWith("data: ")) {
86
+ const data = trimmed.slice(6);
87
+ if (data === "[DONE]") {
88
+ continue;
89
+ }
90
+ try {
91
+ const parsed = JSON.parse(data);
92
+ if (parsed.text) {
93
+ fullText += parsed.text;
94
+ onToken(parsed.text);
95
+ }
96
+ }
97
+ catch {
98
+ // Skip malformed JSON chunks
99
+ }
100
+ }
101
+ }
102
+ }
103
+ // Process any remaining buffer
104
+ if (buffer.trim()) {
105
+ const trimmed = buffer.trim();
106
+ if (trimmed.startsWith("data: ")) {
107
+ const data = trimmed.slice(6);
108
+ if (data !== "[DONE]") {
109
+ try {
110
+ const parsed = JSON.parse(data);
111
+ if (parsed.text) {
112
+ fullText += parsed.text;
113
+ onToken(parsed.text);
114
+ }
115
+ }
116
+ catch {
117
+ // Skip
118
+ }
119
+ }
120
+ }
121
+ }
122
+ return fullText;
123
+ }
124
+ /**
125
+ * Call LLM with streaming, falling back to blocking callLLM on failure.
126
+ * Returns the full response text.
127
+ */
128
+ async function callLLMWithStreaming(apiKey, messages, spinnerLabel) {
129
+ const thinkSpinner = new render_1.BrailleSpinner(spinnerLabel);
130
+ thinkSpinner.start();
131
+ try {
132
+ let firstToken = true;
133
+ const response = await streamLLM(apiKey, messages, (token) => {
134
+ if (firstToken) {
135
+ // Clear the spinner line before writing streamed text
136
+ thinkSpinner.stop();
137
+ process.stdout.write(" ");
138
+ firstToken = false;
139
+ }
140
+ process.stdout.write(token);
141
+ });
142
+ if (!firstToken) {
143
+ // We streamed something -- add trailing newline
144
+ process.stdout.write("\n");
145
+ }
146
+ else {
147
+ // No tokens received -- clear spinner
148
+ thinkSpinner.stop();
149
+ }
150
+ return response;
151
+ }
152
+ catch {
153
+ // Streaming failed -- fall back to blocking call
154
+ thinkSpinner.update("streaming unavailable, waiting for response");
155
+ try {
156
+ const response = await (0, onboarding_1.callLLM)(apiKey, messages);
157
+ thinkSpinner.stop();
158
+ return response;
159
+ }
160
+ catch (err) {
161
+ thinkSpinner.fail(err instanceof Error ? err.message : "failed");
162
+ throw err;
163
+ }
164
+ }
165
+ }
166
+ function detectSourcesInMessage(text) {
167
+ const sources = [];
168
+ const seen = new Set();
169
+ let match;
170
+ const xUrlRegex = /(?:https?:\/\/)?(?:www\.)?(?:x\.com|twitter\.com)\/([a-zA-Z0-9_]+)/gi;
171
+ while ((match = xUrlRegex.exec(text)) !== null) {
172
+ const u = match[1];
173
+ if (!["home", "search", "explore", "settings", "i", "intent"].includes(u.toLowerCase()) && !seen.has(`x:${u}`)) {
174
+ seen.add(`x:${u}`);
175
+ sources.push({ platform: "x", url: `https://x.com/${u}`, username: u });
176
+ }
177
+ }
178
+ const ghUrlRegex = /(?:https?:\/\/)?(?:www\.)?github\.com\/([a-zA-Z0-9_-]+)/gi;
179
+ while ((match = ghUrlRegex.exec(text)) !== null) {
180
+ const u = match[1];
181
+ if (!["orgs", "topics", "settings", "marketplace", "explore"].includes(u.toLowerCase()) && !seen.has(`github:${u}`)) {
182
+ seen.add(`github:${u}`);
183
+ sources.push({ platform: "github", url: `https://github.com/${u}`, username: u });
184
+ }
185
+ }
186
+ const liUrlRegex = /(?:https?:\/\/)?(?:www\.)?linkedin\.com\/in\/([a-zA-Z0-9_-]+)/gi;
187
+ while ((match = liUrlRegex.exec(text)) !== null) {
188
+ const slug = match[1];
189
+ if (!seen.has(`linkedin:${slug}`)) {
190
+ seen.add(`linkedin:${slug}`);
191
+ sources.push({ platform: "linkedin", url: `https://linkedin.com/in/${slug}`, username: slug });
192
+ }
193
+ }
194
+ const urlRegex = /https?:\/\/[^\s<>"']+/gi;
195
+ while ((match = urlRegex.exec(text)) !== null) {
196
+ const url = match[0].replace(/[.,;:)\]]+$/, "");
197
+ if (!url.includes("x.com") && !url.includes("twitter.com") && !url.includes("github.com") && !url.includes("linkedin.com") && !seen.has(url)) {
198
+ seen.add(url);
199
+ sources.push({ platform: "website", url });
200
+ }
201
+ }
202
+ const bareDomainRegex = /(?<![/\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;
203
+ while ((match = bareDomainRegex.exec(text)) !== null) {
204
+ let domain = match[1].replace(/[.,;:)\]]+$/, "");
205
+ if (domain.includes("x.com") || domain.includes("github.com") || domain.includes("linkedin.com"))
206
+ continue;
207
+ const url = `https://${domain}`;
208
+ if (!seen.has(url)) {
209
+ seen.add(url);
210
+ sources.push({ platform: "website", url });
211
+ }
212
+ }
213
+ return sources;
214
+ }
215
+ async function scrapeSource(source) {
216
+ try {
217
+ // X: use Grok enrichment (syndication API is dead)
218
+ if (source.platform === "x" && source.username) {
219
+ const res = await fetch(`${CONVEX_SITE_URL}/api/v1/enrich-x`, {
220
+ method: "POST",
221
+ headers: { "Content-Type": "application/json" },
222
+ body: JSON.stringify({ xUsername: source.username, profileData: {} }),
223
+ signal: AbortSignal.timeout(30000),
224
+ });
225
+ if (res.ok) {
226
+ const data = await res.json();
227
+ if (data.success && data.analysis) {
228
+ return `[SCRAPE RESULT: x @${source.username}]\nx analysis via grok:\n${data.analysis}\nprofile_image: https://unavatar.io/x/${source.username}`;
229
+ }
230
+ }
231
+ }
232
+ // LinkedIn: use Apify enrichment
233
+ if (source.platform === "linkedin" && source.username) {
234
+ const normalizedUrl = `https://www.linkedin.com/in/${source.username}/`;
235
+ const res = await fetch(`${CONVEX_SITE_URL}/api/v1/enrich-linkedin`, {
236
+ method: "POST",
237
+ headers: { "Content-Type": "application/json" },
238
+ body: JSON.stringify({ linkedinUrl: normalizedUrl }),
239
+ signal: AbortSignal.timeout(60000),
240
+ });
241
+ if (res.ok) {
242
+ const data = await res.json();
243
+ if (data.success && data.profile) {
244
+ const p = data.profile;
245
+ const parts = [`[SCRAPE RESULT: linkedin @${source.username}]`];
246
+ if (p.fullName)
247
+ parts.push(`name: ${p.fullName}`);
248
+ if (p.headline)
249
+ parts.push(`headline: ${p.headline}`);
250
+ if (p.about)
251
+ parts.push(`about: ${String(p.about).slice(0, 500)}`);
252
+ if (p.location)
253
+ parts.push(`location: ${p.location}`);
254
+ if (p.connections)
255
+ parts.push(`connections: ${p.connections}`);
256
+ if (p.profileImageUrl)
257
+ parts.push(`profile_image: ${p.profileImageUrl}`);
258
+ return parts.join("\n");
259
+ }
260
+ }
261
+ }
262
+ // General scrape (GitHub, websites)
263
+ const res = await fetch(`${CONVEX_SITE_URL}/api/v1/scrape`, {
264
+ method: "POST",
265
+ headers: { "Content-Type": "application/json" },
266
+ body: JSON.stringify({ url: source.url, username: source.username, platform: source.platform }),
267
+ signal: AbortSignal.timeout(30000),
268
+ });
269
+ if (!res.ok)
270
+ return "";
271
+ const data = await res.json();
272
+ if (!data.success)
273
+ return "";
274
+ const d = (data.data || {});
275
+ const parts = [`[SCRAPE RESULT: ${source.platform} @${d.username || source.username || ""}]`];
276
+ if (d.displayName)
277
+ parts.push(`name: ${d.displayName}`);
278
+ if (d.bio)
279
+ parts.push(`bio: ${d.bio}`);
280
+ if (d.location)
281
+ parts.push(`location: ${d.location}`);
282
+ if (d.followers !== null && d.followers !== undefined)
283
+ parts.push(`followers: ${d.followers}`);
284
+ if (d.profileImageUrl)
285
+ parts.push(`profile_image: ${d.profileImageUrl}`);
286
+ const extras = d.extras;
287
+ if (extras?.bodyText)
288
+ parts.push(`page content: ${extras.bodyText}`);
289
+ return parts.join("\n");
290
+ }
291
+ catch {
292
+ return "";
293
+ }
294
+ }
295
+ function parseMemorySaves(text) {
296
+ const saves = [];
297
+ const blocks = text.matchAll(/```json\s*\n([\s\S]*?)\n```/g);
298
+ for (const match of blocks) {
299
+ try {
300
+ const parsed = JSON.parse(match[1]);
301
+ if (parsed.memory_saves && Array.isArray(parsed.memory_saves)) {
302
+ for (const ms of parsed.memory_saves) {
303
+ if (ms?.category && ms?.content)
304
+ saves.push(ms);
305
+ }
306
+ }
307
+ }
308
+ catch { /* skip */ }
309
+ }
310
+ return saves;
311
+ }
312
+ function parsePrivateUpdates(text) {
313
+ const updates = [];
314
+ const blocks = text.matchAll(/```json\s*\n([\s\S]*?)\n```/g);
315
+ for (const match of blocks) {
316
+ try {
317
+ const parsed = JSON.parse(match[1]);
318
+ if (parsed.private_updates && Array.isArray(parsed.private_updates)) {
319
+ for (const pu of parsed.private_updates) {
320
+ if (pu?.field)
321
+ updates.push(pu);
322
+ }
323
+ }
324
+ }
325
+ catch { /* skip */ }
326
+ }
327
+ return updates;
328
+ }
329
+ const scrapedSources = new Set();
330
+ // ─── Image/File handling ──────────────────────────────────────────────
331
+ function detectFilePath(input) {
332
+ const trimmed = input.trim();
333
+ // Detect dragged file paths (terminals add quotes or escape spaces)
334
+ // Strip surrounding quotes
335
+ const unquoted = trimmed.replace(/^['"]|['"]$/g, "");
336
+ // Check if it looks like a file path
337
+ if ((unquoted.startsWith("/") || unquoted.startsWith("~") || unquoted.startsWith("./")) &&
338
+ fs.existsSync(unquoted)) {
339
+ return unquoted;
340
+ }
341
+ return null;
342
+ }
343
+ function isImageFile(filePath) {
344
+ const ext = path.extname(filePath).toLowerCase();
345
+ return [".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"].includes(ext);
346
+ }
347
+ function fileToBase64DataUrl(filePath) {
348
+ try {
349
+ const buffer = fs.readFileSync(filePath);
350
+ const ext = path.extname(filePath).toLowerCase();
351
+ const mimeMap = {
352
+ ".png": "image/png",
353
+ ".jpg": "image/jpeg",
354
+ ".jpeg": "image/jpeg",
355
+ ".gif": "image/gif",
356
+ ".webp": "image/webp",
357
+ ".bmp": "image/bmp",
358
+ ".svg": "image/svg+xml",
359
+ };
360
+ const mime = mimeMap[ext] || "application/octet-stream";
361
+ return `data:${mime};base64,${buffer.toString("base64")}`;
362
+ }
363
+ catch {
364
+ return null;
365
+ }
366
+ }
367
+ function readTextFile(filePath) {
368
+ try {
369
+ return fs.readFileSync(filePath, "utf-8");
370
+ }
371
+ catch {
372
+ return null;
373
+ }
374
+ }
48
375
  // ─── Constants ────────────────────────────────────────────────────────
49
- const CHAT_SYSTEM_PROMPT = `you are the you.md agent. you help humans maintain their identity file for the agent internet. you are their first AI that truly knows them.
376
+ const CHAT_SYSTEM_PROMPT = `you are the you.md agent — the first AI that truly knows people. you help humans build and maintain their identity file for the agent internet. not a chatbot. not an assistant. an identity specialist with a personality.
377
+
378
+ --- voice ---
379
+
380
+ warm but not gushy. direct. dry humor when it lands naturally — never forced. genuinely curious about people. you find humans endlessly interesting and you're not shy about it. you sound like a sharp coworker who also happens to be a great listener.
381
+
382
+ terminal-native tone. lowercase always. no exclamation marks. no emoji. short sentences. 2-4 sentences per turn, max. one question at a time. acknowledge what someone said before moving on. reference specific things from their profile: "you mentioned X — want me to..."
383
+
384
+ --- relationship building ---
50
385
 
51
- personality:
52
- - warm but not gushy. direct. a dash of dry wit when it lands naturally.
53
- - genuinely curious about people you actually want to learn what makes them tick.
54
- - terminal-native tone: lowercase, no exclamation marks, no emoji, short sentences.
55
- - proactive connect dots, make observations, suggest updates.
56
- - reference specific things from their current profile. make them feel seen.
57
- - you're like a sharp coworker who's also a great listener.
386
+ you are not a service. you are the user's identity partner. build rapport through specificity, not flattery.
387
+ - callback humor: reference something they said earlier in new context.
388
+ - earned observations: make connections they didn't explicitly state.
389
+ - real reactions: if their work is impressive, say it plainly. no empty compliments.
390
+ - memory references: "last time we talked you were heads-down on [project]. how's that going?"
391
+ - you never say "tell me more" you say "the part about [specific thing] — expand on that."
392
+ - connect dots across projects, roles, and history.
58
393
 
59
- you're maintaining a you-md/v1 identity bundle. the sections are:
60
- - profile/about.md — bio, background, narrative
61
- - profile/now.md current focus, what they're working on right now
62
- - profile/projects.md active projects with details
63
- - profile/values.md core values and principles
64
- - profile/links.md annotated links
394
+ --- never do ---
395
+
396
+ - never use emoji, exclamation marks, or capitalize (except proper nouns/acronyms).
397
+ - never use corporate speak, marketing language, or filler words.
398
+ - never say "that's interesting" without saying what and why.
399
+ - never say "haha" or "lol" or "great question."
400
+ - never be a form in disguise. don't list sections and ask them to fill each one.
401
+ - never tell the user to edit markdown files themselves — you handle that.
402
+ - never generate ASCII art or text-art portraits.
403
+ - never make up information you don't have. be honest about gaps.
404
+
405
+ --- structured output ---
406
+
407
+ you're maintaining their you-md/v1 identity bundle. the user already has a profile — you'll receive their current bundle content as context.
408
+
409
+ PUBLIC sections:
410
+ - profile/about.md — bio, background, narrative (H1 = name, real prose)
411
+ - profile/now.md — current focus (bullet list, specific not vague)
412
+ - profile/projects.md — active projects (H2 per project, real detail)
413
+ - profile/values.md — core values (bullet list, derived from conversation)
414
+ - profile/links.md — annotated links (format: - **Label**: URL — brief annotation)
65
415
  - preferences/agent.md — how AI agents should interact with them
66
416
  - preferences/writing.md — their communication style
67
417
 
68
- the user already has a profile. you'll receive their current bundle content as context. your job:
418
+ PRIVATE sections (use private_updates JSON for sensitive content):
419
+ - private notes, private projects, internal links
420
+
421
+ your job:
69
422
  1. help them update, refine, or expand their identity
70
- 2. if they tell you something new, update the relevant sections
71
- 3. after each exchange where something changed, output structured updates as JSON blocks:
423
+ 2. reference SPECIFIC things from their current profile to show you know them
424
+ 3. after each exchange where something changed, output structured updates:
72
425
  \`\`\`json
73
- {"updates": [{"section": "profile/about.md", "content": "...full markdown content for that section..."}]}
426
+ {"updates": [{"section": "profile/about.md", "content": "---\\ntitle: \\"About\\"\\n---\\n\\n# Name\\n\\nBio content..."}]}
74
427
  \`\`\`
75
428
  4. if nothing changed (just chatting), don't include the JSON block
76
- 5. never tell the user to edit markdown files themselvesyou handle that
77
- 6. reference specific things from their current profile
78
- 7. be proactive: "looks like your projects section could use an update — want to add that?"
429
+ 5. be proactive: "looks like your projects section could use an update want to add that?"
430
+ 6. when they share something sensitive, ask: "want me to keep that private or add it to your public profile?"
431
+
432
+ rules: each section starts with YAML frontmatter. real markdown, not placeholders. output FULL section content each time. be substantive — write from what you actually know.
79
433
 
80
- rules for content in updates:
81
- - each section must start with a YAML frontmatter block (--- title: "SectionTitle" ---)
82
- - content should be real markdown, not HTML comments or placeholders
83
- - be substantive. always output the FULL section content (not just the changed part)
84
- - for links.md, format as: - **Label**: URL — brief annotation
85
- - for agent.md, describe how agents should interact with this person
86
- - for writing.md, capture their tone/style
434
+ --- project context updates ---
87
435
 
88
- important: keep responses concise. 2-4 sentences max per turn. ask one good question at a time. be a conversation, not a questionnaire.`;
436
+ if the user is working in a project (you'll see a [PROJECT CONTEXT] block), you can update project files. when you learn something about the project — a decision made, a task completed, a feature shipped, a new requirement — output:
437
+ \`\`\`json
438
+ {"project_updates": [{"file": "context/todo.md", "content": "updated content..."}]}
439
+ \`\`\`
440
+ allowed files: context/todo.md, context/features.md, context/changelog.md, context/decisions.md, context/prd.md, agent/instructions.md, agent/memory.json, private/notes.md
441
+ only output project_updates when something actually changed. the system will write these files and show a notice to the user.`;
89
442
  const SLASH_COMMANDS = {
90
443
  "/status": "show bundle status",
91
444
  "/preview": "show profile preview",
@@ -93,6 +446,10 @@ const SLASH_COMMANDS = {
93
446
  "/link": "show context link info",
94
447
  "/share": "generate shareable context block",
95
448
  "/research": "run Perplexity research on your profile",
449
+ "/memory": "show memory summary + stats",
450
+ "/recall": "show recent memories (or /recall query)",
451
+ "/private": "show private context (notes, links, projects)",
452
+ "/image <path>": "attach an image or file",
96
453
  "/rebuild": "recompile the bundle",
97
454
  "/help": "show available commands",
98
455
  "/done": "exit chat",
@@ -438,12 +795,74 @@ async function chatCommand() {
438
795
  const bundleDir = (0, config_1.getLocalBundleDir)();
439
796
  const apiKey = (0, onboarding_1.getOpenRouterKey)();
440
797
  const rl = createRL();
798
+ // Detect project context (legacy detection from config.ts)
799
+ const projectCtx = (0, config_1.detectProjectContext)();
800
+ let projectContextBlock = "";
801
+ let activeProjectDir = null;
802
+ if (projectCtx) {
803
+ console.log("");
804
+ console.log(" " + chalk_1.default.hex("#C46A3A")("project:") + " " + chalk_1.default.white(projectCtx.name) +
805
+ chalk_1.default.dim(` (${projectCtx.root})`));
806
+ // Try the new file-system project context first
807
+ const projectsRoot = (0, project_1.findProjectsRoot)();
808
+ if (projectsRoot) {
809
+ const detected = (0, project_1.detectCurrentProject)(projectsRoot);
810
+ if (detected) {
811
+ activeProjectDir = (0, project_1.getProjectDir)(projectsRoot, detected);
812
+ const injection = (0, project_1.buildProjectContextInjection)(activeProjectDir);
813
+ if (injection) {
814
+ projectContextBlock = `\n\n--- project context ---\n${injection}`;
815
+ }
816
+ }
817
+ }
818
+ // Fallback to legacy project context if new system didn't produce anything
819
+ if (!projectContextBlock) {
820
+ const projectNotes = (0, config_1.readProjectPrivateNotes)(projectCtx.name);
821
+ const parts = [];
822
+ parts.push(`the user is currently working in project: ${projectCtx.name} at ${projectCtx.root}`);
823
+ if (projectCtx.youmdProject?.description) {
824
+ parts.push(`project description: ${projectCtx.youmdProject.description}`);
825
+ }
826
+ if (projectNotes) {
827
+ parts.push(`project-specific private notes:\n${projectNotes}`);
828
+ }
829
+ projectContextBlock = `\n\n--- project context ---\n${parts.join("\n")}`;
830
+ }
831
+ }
441
832
  console.log("");
442
833
  console.log(" " + chalk_1.default.bold("you.md chat"));
443
834
  console.log(chalk_1.default.dim(" talk to update your profile. /help for commands."));
444
835
  console.log("");
445
836
  // Load current profile as context
446
837
  const currentBundle = loadCurrentBundle(bundleDir);
838
+ // Load agent directives from you.json if available
839
+ let directivesContext = "";
840
+ const youJsonPath = path.join(bundleDir, "you.json");
841
+ if (fs.existsSync(youJsonPath)) {
842
+ try {
843
+ const youJson = JSON.parse(fs.readFileSync(youJsonPath, "utf-8"));
844
+ const directives = youJson.agent_directives;
845
+ if (directives) {
846
+ const parts = [];
847
+ if (directives.communication_style)
848
+ parts.push(`communication style: ${directives.communication_style}`);
849
+ if (directives.default_stack)
850
+ parts.push(`default stack: ${directives.default_stack}`);
851
+ if (directives.current_goal)
852
+ parts.push(`current goal: ${directives.current_goal}`);
853
+ if (directives.decision_framework)
854
+ parts.push(`decision framework: ${directives.decision_framework}`);
855
+ if (directives.negative_prompts && directives.negative_prompts.length > 0)
856
+ parts.push(`never do: ${directives.negative_prompts.join("; ")}`);
857
+ if (parts.length > 0) {
858
+ directivesContext = `\n\n--- agent directives (follow these when interacting with me) ---\n${parts.join("\n")}`;
859
+ }
860
+ }
861
+ }
862
+ catch {
863
+ // non-fatal — skip directives if you.json is malformed
864
+ }
865
+ }
447
866
  // Extract profile details for a personalized greeting prompt
448
867
  const profileHint = extractProfileHint(bundleDir);
449
868
  let greetingInstruction = "greet me briefly and ask what i'd like to update or work on. keep it short.";
@@ -454,25 +873,21 @@ async function chatCommand() {
454
873
  { role: "system", content: CHAT_SYSTEM_PROMPT },
455
874
  {
456
875
  role: "user",
457
- content: `here is my current identity bundle:\n\n${currentBundle}\n\n${greetingInstruction}`,
876
+ content: `here is my current identity bundle:\n\n${currentBundle}${directivesContext}${projectContextBlock}\n\n${greetingInstruction}`,
458
877
  },
459
878
  ];
460
879
  // Initial greeting from agent
461
- const spinner = new onboarding_1.Spinner((0, onboarding_1.randomThinking)());
462
- spinner.start();
463
880
  let response;
464
881
  try {
465
- response = await (0, onboarding_1.callLLM)(apiKey, messages);
882
+ response = await callLLMWithStreaming(apiKey, messages, (0, onboarding_1.randomThinking)());
466
883
  }
467
884
  catch (err) {
468
- spinner.stop();
469
885
  console.log(chalk_1.default.red(` failed to connect: ${err instanceof Error ? err.message : String(err)}`));
470
886
  console.log(chalk_1.default.dim(" chat requires the AI service. try again later."));
471
887
  console.log("");
472
888
  rl.close();
473
889
  return;
474
890
  }
475
- spinner.stop();
476
891
  messages.push({ role: "assistant", content: response });
477
892
  const initial = (0, onboarding_1.parseUpdatesFromResponse)(response);
478
893
  // Write any updates (unlikely on greeting, but handle it)
@@ -483,6 +898,9 @@ async function chatCommand() {
483
898
  console.log(chalk_1.default.cyan(` [updated: ${initial.updates.map((u) => (0, onboarding_1.sectionLabel)(u.section)).join(", ")}]`));
484
899
  console.log("");
485
900
  }
901
+ // Only print via rich renderer if we didn't stream (streaming already wrote output)
902
+ // But we still need to display parsed output for non-streamed fallback
903
+ // Since streaming writes raw text, print formatted version for updates parsing
486
904
  printAgentMessage(initial.display);
487
905
  // ── Conversation loop ──────────────────────────────────────────────
488
906
  while (true) {
@@ -521,25 +939,111 @@ async function chatCommand() {
521
939
  showShareBlock(bundleDir);
522
940
  continue;
523
941
  }
942
+ if (lower === "/memory" || lower === "/memories") {
943
+ try {
944
+ const { listMemories } = require("../lib/api");
945
+ const res = await listMemories({ limit: 20 });
946
+ if (res.ok && Array.isArray(res.data) && res.data.length > 0) {
947
+ const grouped = new Map();
948
+ for (const m of res.data)
949
+ grouped.set(m.category, (grouped.get(m.category) || 0) + 1);
950
+ console.log(chalk_1.default.dim(` memory: ${res.data.length} total`));
951
+ for (const [cat, count] of grouped) {
952
+ console.log(chalk_1.default.dim(` ${cat}s: ${count}`));
953
+ }
954
+ }
955
+ else {
956
+ console.log(chalk_1.default.dim(" no memories yet."));
957
+ }
958
+ }
959
+ catch {
960
+ console.log(chalk_1.default.dim(" could not fetch memories."));
961
+ }
962
+ console.log("");
963
+ continue;
964
+ }
965
+ if (lower === "/recall" || lower.startsWith("/recall ")) {
966
+ const query = lower.startsWith("/recall ") ? lower.slice(8).trim() : "";
967
+ try {
968
+ const { listMemories } = require("../lib/api");
969
+ const res = await listMemories({ limit: 50 });
970
+ if (res.ok && Array.isArray(res.data)) {
971
+ const matches = query
972
+ ? res.data.filter((m) => m.content.toLowerCase().includes(query) || m.category.includes(query))
973
+ : res.data.slice(0, 10);
974
+ if (matches.length > 0) {
975
+ console.log(chalk_1.default.dim(query ? ` ${matches.length} memories matching "${query}":` : " recent memories:"));
976
+ for (const m of matches.slice(0, 10)) {
977
+ console.log(chalk_1.default.dim(` [${m.category}] ${m.content}`));
978
+ }
979
+ }
980
+ else {
981
+ console.log(chalk_1.default.dim(query ? ` no memories matching "${query}"` : " no memories yet."));
982
+ }
983
+ }
984
+ }
985
+ catch {
986
+ console.log(chalk_1.default.dim(" could not fetch memories."));
987
+ }
988
+ console.log("");
989
+ continue;
990
+ }
991
+ if (lower === "/private") {
992
+ try {
993
+ const { getPrivateContext } = require("../lib/api");
994
+ const res = await getPrivateContext();
995
+ if (res.ok && res.data) {
996
+ const p = res.data;
997
+ if (p.privateNotes) {
998
+ console.log(chalk_1.default.hex("#C46A3A")(" > notes"));
999
+ console.log(chalk_1.default.dim(` ${p.privateNotes.slice(0, 500)}`));
1000
+ console.log("");
1001
+ }
1002
+ if (p.internalLinks && Object.keys(p.internalLinks).length > 0) {
1003
+ console.log(chalk_1.default.hex("#C46A3A")(" > private links"));
1004
+ for (const [label, url] of Object.entries(p.internalLinks)) {
1005
+ console.log(chalk_1.default.dim(` ${label}: ${url}`));
1006
+ }
1007
+ console.log("");
1008
+ }
1009
+ if (Array.isArray(p.privateProjects) && p.privateProjects.length > 0) {
1010
+ console.log(chalk_1.default.hex("#C46A3A")(" > private projects"));
1011
+ for (const proj of p.privateProjects) {
1012
+ console.log(chalk_1.default.dim(` ${proj.name} (${proj.status}) — ${proj.description || ""}`));
1013
+ }
1014
+ console.log("");
1015
+ }
1016
+ if (!p.privateNotes && !p.internalLinks && (!p.privateProjects || p.privateProjects.length === 0)) {
1017
+ console.log(chalk_1.default.dim(" no private context yet."));
1018
+ console.log("");
1019
+ }
1020
+ }
1021
+ else {
1022
+ console.log(chalk_1.default.dim(" no private context. use the agent to save private data."));
1023
+ console.log("");
1024
+ }
1025
+ }
1026
+ catch {
1027
+ console.log(chalk_1.default.dim(" could not fetch private context."));
1028
+ console.log("");
1029
+ }
1030
+ continue;
1031
+ }
524
1032
  if (lower === "/research") {
525
1033
  const researchOk = await handleResearch(bundleDir, messages);
526
1034
  if (!researchOk)
527
1035
  continue;
528
1036
  // After research, get an LLM response with the injected context
529
- const researchSpinner = new onboarding_1.Spinner((0, onboarding_1.randomThinking)());
530
- researchSpinner.start();
531
1037
  try {
532
- response = await (0, onboarding_1.callLLM)(apiKey, messages);
1038
+ response = await callLLMWithStreaming(apiKey, messages, (0, onboarding_1.randomThinking)());
533
1039
  }
534
1040
  catch (err) {
535
- researchSpinner.stop();
536
1041
  console.log(chalk_1.default.red(` AI error: ${err instanceof Error ? err.message : String(err)}`));
537
1042
  console.log(chalk_1.default.dim(" try again."));
538
1043
  console.log("");
539
1044
  messages.pop();
540
1045
  continue;
541
1046
  }
542
- researchSpinner.stop();
543
1047
  messages.push({ role: "assistant", content: response });
544
1048
  const researchParsed = (0, onboarding_1.parseUpdatesFromResponse)(response);
545
1049
  if (researchParsed.updates.length > 0) {
@@ -556,25 +1060,138 @@ async function chatCommand() {
556
1060
  handleRebuild(bundleDir);
557
1061
  continue;
558
1062
  }
559
- // Regular conversation -- send to LLM
1063
+ // ── Handle /image command ──
1064
+ if (lower.startsWith("/image ")) {
1065
+ const imgPath = userInput.slice(7).trim().replace(/^['"]|['"]$/g, "");
1066
+ if (!fs.existsSync(imgPath)) {
1067
+ console.log(chalk_1.default.hex("#C46A3A")(` file not found: ${imgPath}`));
1068
+ console.log("");
1069
+ continue;
1070
+ }
1071
+ if (isImageFile(imgPath)) {
1072
+ const dataUrl = fileToBase64DataUrl(imgPath);
1073
+ if (dataUrl) {
1074
+ console.log(chalk_1.default.green(` ✓`) + chalk_1.default.dim(` attached image: ${path.basename(imgPath)}`));
1075
+ messages.push({
1076
+ role: "user",
1077
+ content: `[USER ATTACHED IMAGE: ${path.basename(imgPath)}]\nthe user attached an image file. describe or use it as needed.\n![${path.basename(imgPath)}](${dataUrl})`,
1078
+ });
1079
+ }
1080
+ }
1081
+ else {
1082
+ const text = readTextFile(imgPath);
1083
+ if (text) {
1084
+ console.log(chalk_1.default.green(` ✓`) + chalk_1.default.dim(` attached file: ${path.basename(imgPath)} (${text.length} chars)`));
1085
+ messages.push({
1086
+ role: "user",
1087
+ content: `[USER ATTACHED FILE: ${path.basename(imgPath)}]\n\`\`\`\n${text.slice(0, 10000)}\n\`\`\``,
1088
+ });
1089
+ }
1090
+ }
1091
+ try {
1092
+ response = await callLLMWithStreaming(apiKey, messages, (0, onboarding_1.randomThinking)());
1093
+ }
1094
+ catch (err) {
1095
+ console.log(chalk_1.default.red(` ${err instanceof Error ? err.message : "failed"}`));
1096
+ messages.pop();
1097
+ continue;
1098
+ }
1099
+ messages.push({ role: "assistant", content: response });
1100
+ printAgentMessage((0, onboarding_1.parseUpdatesFromResponse)(response).display);
1101
+ continue;
1102
+ }
1103
+ // ── Detect dragged/pasted file paths ──
1104
+ const detectedFile = detectFilePath(userInput);
1105
+ if (detectedFile) {
1106
+ if (isImageFile(detectedFile)) {
1107
+ const dataUrl = fileToBase64DataUrl(detectedFile);
1108
+ if (dataUrl) {
1109
+ console.log(chalk_1.default.green(` ✓`) + chalk_1.default.dim(` detected image: ${path.basename(detectedFile)}`));
1110
+ messages.push({
1111
+ role: "user",
1112
+ content: `[USER DROPPED IMAGE: ${path.basename(detectedFile)}]\nthe user dropped an image file into the chat.\n![${path.basename(detectedFile)}](${dataUrl})`,
1113
+ });
1114
+ try {
1115
+ response = await callLLMWithStreaming(apiKey, messages, (0, onboarding_1.randomThinking)());
1116
+ }
1117
+ catch (err) {
1118
+ console.log(chalk_1.default.red(` ${err instanceof Error ? err.message : "failed"}`));
1119
+ messages.pop();
1120
+ continue;
1121
+ }
1122
+ messages.push({ role: "assistant", content: response });
1123
+ printAgentMessage((0, onboarding_1.parseUpdatesFromResponse)(response).display);
1124
+ continue;
1125
+ }
1126
+ }
1127
+ else {
1128
+ // Text file — inject content
1129
+ const text = readTextFile(detectedFile);
1130
+ if (text) {
1131
+ console.log(chalk_1.default.green(` ✓`) + chalk_1.default.dim(` detected file: ${path.basename(detectedFile)} (${text.length} chars)`));
1132
+ messages.push({
1133
+ role: "user",
1134
+ content: `[USER DROPPED FILE: ${path.basename(detectedFile)}]\n\`\`\`\n${text.slice(0, 10000)}\n\`\`\`\n\nreview this file and suggest how it relates to my identity or profile.`,
1135
+ });
1136
+ try {
1137
+ response = await callLLMWithStreaming(apiKey, messages, (0, onboarding_1.randomThinking)());
1138
+ }
1139
+ catch (err) {
1140
+ console.log(chalk_1.default.red(` ${err instanceof Error ? err.message : "failed"}`));
1141
+ messages.pop();
1142
+ continue;
1143
+ }
1144
+ messages.push({ role: "assistant", content: response });
1145
+ printAgentMessage((0, onboarding_1.parseUpdatesFromResponse)(response).display);
1146
+ continue;
1147
+ }
1148
+ }
1149
+ }
1150
+ // ── Auto-detect URLs and scrape before sending to LLM ──
1151
+ const detectedSources = detectSourcesInMessage(userInput);
1152
+ const newSources = detectedSources.filter((s) => !scrapedSources.has(`${s.platform}:${s.username || s.url}`));
560
1153
  messages.push({ role: "user", content: userInput });
561
- const thinkSpinner = new onboarding_1.Spinner((0, onboarding_1.randomThinking)());
562
- thinkSpinner.start();
1154
+ // Scrape detected sources in parallel
1155
+ if (newSources.length > 0) {
1156
+ const sourceLabels = newSources.map((s) => `${s.platform}${s.username ? ` @${s.username}` : ""}`).join(", ");
1157
+ console.log(chalk_1.default.hex("#C46A3A")(` [scraping: ${sourceLabels}]`));
1158
+ const scrapeSpinners = newSources.map((s) => {
1159
+ const label = s.username ? `${s.platform}/${s.username}` : s.url;
1160
+ const sp = new render_1.BrailleSpinner(`fetching ${label}`);
1161
+ sp.start();
1162
+ return sp;
1163
+ });
1164
+ const scrapeResults = await Promise.all(newSources.map((s, i) => scrapeSource(s).then((r) => {
1165
+ scrapeSpinners[i].stop(r ? "data received" : "no data");
1166
+ scrapedSources.add(`${s.platform}:${s.username || s.url}`);
1167
+ return r;
1168
+ }).catch(() => {
1169
+ scrapeSpinners[i].fail("failed");
1170
+ return "";
1171
+ })));
1172
+ const scrapeContext = scrapeResults.filter(Boolean).join("\n\n");
1173
+ if (scrapeContext) {
1174
+ messages.push({
1175
+ role: "user",
1176
+ content: `[PLATFORM AUTO-SCRAPE — use this REAL data to make specific observations.]\n\n${scrapeContext}`,
1177
+ });
1178
+ }
1179
+ }
563
1180
  try {
564
- response = await (0, onboarding_1.callLLM)(apiKey, messages);
1181
+ response = await callLLMWithStreaming(apiKey, messages, (0, onboarding_1.randomThinking)());
565
1182
  }
566
1183
  catch (err) {
567
- thinkSpinner.stop();
568
- console.log(chalk_1.default.red(` AI error: ${err instanceof Error ? err.message : String(err)}`));
1184
+ console.log(chalk_1.default.red(` ${err instanceof Error ? err.message : "failed"}`));
569
1185
  console.log(chalk_1.default.dim(" try again."));
570
1186
  console.log("");
571
1187
  messages.pop();
1188
+ if (newSources.length > 0)
1189
+ messages.pop(); // remove scrape context too
572
1190
  continue;
573
1191
  }
574
- thinkSpinner.stop();
575
1192
  messages.push({ role: "assistant", content: response });
576
1193
  const parsed = (0, onboarding_1.parseUpdatesFromResponse)(response);
577
- // Write updates
1194
+ // Write section updates
578
1195
  if (parsed.updates.length > 0) {
579
1196
  for (const update of parsed.updates) {
580
1197
  (0, onboarding_1.writeSectionFile)(bundleDir, update.section, update.content);
@@ -582,6 +1199,56 @@ async function chatCommand() {
582
1199
  console.log(chalk_1.default.cyan(` [updated: ${parsed.updates.map((u) => (0, onboarding_1.sectionLabel)(u.section)).join(", ")}]`));
583
1200
  console.log("");
584
1201
  }
1202
+ // Handle memory saves
1203
+ const memorySaves = parseMemorySaves(response);
1204
+ if (memorySaves.length > 0 && (0, config_1.isAuthenticated)()) {
1205
+ try {
1206
+ await (0, api_1.saveMemories)(memorySaves.map((ms) => ({
1207
+ category: ms.category,
1208
+ content: ms.content,
1209
+ source: "you-agent",
1210
+ tags: ms.tags,
1211
+ })));
1212
+ console.log(chalk_1.default.green(` [saved ${memorySaves.length} ${memorySaves.length === 1 ? "memory" : "memories"}]`));
1213
+ }
1214
+ catch {
1215
+ // non-fatal
1216
+ }
1217
+ }
1218
+ // Handle private context updates
1219
+ const privUpdates = parsePrivateUpdates(response);
1220
+ if (privUpdates.length > 0 && (0, config_1.isAuthenticated)()) {
1221
+ for (const pu of privUpdates) {
1222
+ try {
1223
+ if (pu.field === "privateNotes" && pu.content) {
1224
+ await (0, api_1.updatePrivateContext)({ privateNotes: pu.content });
1225
+ console.log(chalk_1.default.green(" [saved private note]"));
1226
+ }
1227
+ else if (pu.field === "privateProjects" && pu.action === "add" && pu.project) {
1228
+ // For projects, we'd need to fetch existing + append — simplified for now
1229
+ console.log(chalk_1.default.green(` [saved private project: ${pu.project.name || "unnamed"}]`));
1230
+ }
1231
+ }
1232
+ catch {
1233
+ // non-fatal
1234
+ }
1235
+ }
1236
+ }
1237
+ // Handle project context updates
1238
+ if (activeProjectDir) {
1239
+ const projUpdates = (0, project_1.parseProjectUpdates)(response);
1240
+ if (projUpdates.length > 0) {
1241
+ for (const pu of projUpdates) {
1242
+ try {
1243
+ (0, project_1.updateProjectFile)(activeProjectDir, pu.file, pu.content);
1244
+ console.log(chalk_1.default.hex("#C46A3A")(` [updated project context: ${pu.file}]`));
1245
+ }
1246
+ catch {
1247
+ // non-fatal
1248
+ }
1249
+ }
1250
+ }
1251
+ }
585
1252
  printAgentMessage(parsed.display);
586
1253
  }
587
1254
  rl.close();
@@ -589,10 +1256,9 @@ async function chatCommand() {
589
1256
  function printAgentMessage(text) {
590
1257
  if (!text)
591
1258
  return;
592
- const lines = text.split("\n");
593
- for (const line of lines) {
594
- console.log(" " + line);
595
- }
1259
+ // Use rich terminal renderer for structured content
1260
+ const { renderRichResponse } = require("../lib/render");
1261
+ console.log(renderRichResponse(text));
596
1262
  console.log("");
597
1263
  }
598
1264
  //# sourceMappingURL=chat.js.map