youmd 0.6.13 → 0.6.21

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 (55) hide show
  1. package/dist/commands/chat.d.ts.map +1 -1
  2. package/dist/commands/chat.js +595 -96
  3. package/dist/commands/chat.js.map +1 -1
  4. package/dist/commands/login.js +1 -1
  5. package/dist/commands/login.js.map +1 -1
  6. package/dist/commands/skill.js +2 -2
  7. package/dist/commands/skill.js.map +1 -1
  8. package/dist/index.js +79 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/ascii.d.ts +5 -0
  11. package/dist/lib/ascii.d.ts.map +1 -1
  12. package/dist/lib/ascii.js +34 -2
  13. package/dist/lib/ascii.js.map +1 -1
  14. package/dist/lib/first-run.d.ts +14 -0
  15. package/dist/lib/first-run.d.ts.map +1 -0
  16. package/dist/lib/first-run.js +97 -0
  17. package/dist/lib/first-run.js.map +1 -0
  18. package/dist/lib/onboarding.js +1 -1
  19. package/dist/lib/onboarding.js.map +1 -1
  20. package/dist/lib/project.d.ts +2 -0
  21. package/dist/lib/project.d.ts.map +1 -1
  22. package/dist/lib/project.js +173 -22
  23. package/dist/lib/project.js.map +1 -1
  24. package/dist/mcp/server.js +1 -1
  25. package/dist/you.js +1 -1
  26. package/dist/you.js.map +1 -1
  27. package/package.json +1 -1
  28. package/dist/__tests__/api.test.d.ts +0 -2
  29. package/dist/__tests__/api.test.d.ts.map +0 -1
  30. package/dist/__tests__/api.test.js +0 -84
  31. package/dist/__tests__/api.test.js.map +0 -1
  32. package/dist/__tests__/compiler.test.d.ts +0 -2
  33. package/dist/__tests__/compiler.test.d.ts.map +0 -1
  34. package/dist/__tests__/compiler.test.js +0 -127
  35. package/dist/__tests__/compiler.test.js.map +0 -1
  36. package/dist/__tests__/config.test.d.ts +0 -2
  37. package/dist/__tests__/config.test.d.ts.map +0 -1
  38. package/dist/__tests__/config.test.js +0 -79
  39. package/dist/__tests__/config.test.js.map +0 -1
  40. package/dist/__tests__/decompile.test.d.ts +0 -2
  41. package/dist/__tests__/decompile.test.d.ts.map +0 -1
  42. package/dist/__tests__/decompile.test.js +0 -102
  43. package/dist/__tests__/decompile.test.js.map +0 -1
  44. package/dist/__tests__/hash.test.d.ts +0 -2
  45. package/dist/__tests__/hash.test.d.ts.map +0 -1
  46. package/dist/__tests__/hash.test.js +0 -44
  47. package/dist/__tests__/hash.test.js.map +0 -1
  48. package/dist/__tests__/integration.test.d.ts +0 -2
  49. package/dist/__tests__/integration.test.d.ts.map +0 -1
  50. package/dist/__tests__/integration.test.js +0 -277
  51. package/dist/__tests__/integration.test.js.map +0 -1
  52. package/dist/__tests__/skill-renderer.test.d.ts +0 -2
  53. package/dist/__tests__/skill-renderer.test.d.ts.map +0 -1
  54. package/dist/__tests__/skill-renderer.test.js +0 -68
  55. package/dist/__tests__/skill-renderer.test.js.map +0 -1
@@ -46,6 +46,7 @@ const config_1 = require("../lib/config");
46
46
  const project_1 = require("../lib/project");
47
47
  const compiler_1 = require("../lib/compiler");
48
48
  const api_1 = require("../lib/api");
49
+ const skills_1 = require("../lib/skills");
49
50
  const render_1 = require("../lib/render");
50
51
  const onboarding_1 = require("../lib/onboarding");
51
52
  const ascii_1 = require("../lib/ascii");
@@ -54,7 +55,7 @@ const update_1 = require("../lib/update");
54
55
  const config_2 = require("../lib/config");
55
56
  const CONVEX_SITE_URL = (0, config_2.getConvexSiteUrl)();
56
57
  const STREAM_URL = `${CONVEX_SITE_URL}/api/v1/chat/stream`;
57
- const CURRENT_VERSION = "0.6.13";
58
+ const CURRENT_VERSION = "0.6.21";
58
59
  function delay(ms) {
59
60
  return new Promise((resolve) => setTimeout(resolve, ms));
60
61
  }
@@ -980,13 +981,14 @@ async function runYouLaunchInvestigation(bundleDir, projectCtx, recentProjects)
980
981
  const findings = [];
981
982
  let strongestMove;
982
983
  let strongestCommand;
984
+ let strongestProject;
983
985
  const rotation = setInterval(() => {
984
986
  spinner.update(labels[labelIndex % labels.length]);
985
987
  labelIndex += 1;
986
988
  }, 1100);
987
989
  spinner.start();
988
990
  try {
989
- await delay(160);
991
+ await delay(600);
990
992
  try {
991
993
  const hasPreferences = fs.existsSync(path.join(bundleDir, "preferences", "agent.md"));
992
994
  const hasDirectives = fs.existsSync(path.join(bundleDir, "directives", "agent.md"));
@@ -1026,6 +1028,15 @@ async function runYouLaunchInvestigation(bundleDir, projectCtx, recentProjects)
1026
1028
  if (repoNeedsBootstrap(projectCtx.root)) {
1027
1029
  strongestMove = `${projectCtx.name} still wants cleaner agent wiring and project-context scaffolding.`;
1028
1030
  strongestCommand = "youmd skill init-project";
1031
+ strongestProject = {
1032
+ name: projectCtx.name,
1033
+ slug: projectCtx.name,
1034
+ projectDir: projectCtx.root,
1035
+ updatedAt: Date.now(),
1036
+ signals: ["still wants cleaner agent wiring and project-context scaffolding"],
1037
+ summary: `${projectCtx.name} still wants cleaner agent wiring and project-context scaffolding.`,
1038
+ suggestedCommand: "youmd skill init-project",
1039
+ };
1029
1040
  }
1030
1041
  }
1031
1042
  catch {
@@ -1044,16 +1055,24 @@ async function runYouLaunchInvestigation(bundleDir, projectCtx, recentProjects)
1044
1055
  findings.push(`${insights[0].name} already looks pretty well-shaped, so i can go deeper instead of scaffolding basics.`);
1045
1056
  }
1046
1057
  else if (opportunities.length > 0) {
1047
- strongestMove = opportunities[0].summary;
1048
- strongestCommand = opportunities[0].suggestedCommand;
1058
+ const strongest = opportunities[0];
1059
+ strongestMove = strongest.summary;
1060
+ strongestCommand = strongest.suggestedCommand;
1061
+ strongestProject = strongest;
1049
1062
  }
1050
1063
  }
1051
1064
  else {
1052
1065
  findings.push("i've got your home bundle loaded, even though we're not inside a project yet.");
1053
1066
  }
1054
- await delay(220);
1067
+ await delay(900);
1055
1068
  spinner.stop("looked through local context");
1056
- return { findings: findings.slice(0, 6), strongestMove, strongestCommand };
1069
+ return {
1070
+ findings: findings.slice(0, 3),
1071
+ strongestMove,
1072
+ strongestCommand,
1073
+ strongestProject,
1074
+ recentProjects: recentProjects.slice(0, 3),
1075
+ };
1057
1076
  }
1058
1077
  finally {
1059
1078
  clearInterval(rotation);
@@ -1068,7 +1087,7 @@ async function printChatOpening(bundleDir, projectCtx) {
1068
1087
  const recentInsights = (0, project_1.getRecentProjectInsights)(process.cwd(), 6);
1069
1088
  const recentProjects = (0, project_1.getFeaturedRecentProjectNames)(recentInsights, 6);
1070
1089
  const launchSurface = process.env.YOUMD_LAUNCH_SURFACE;
1071
- let investigation = { findings: [] };
1090
+ let investigation = { findings: [], recentProjects: recentProjects.slice(0, 3) };
1072
1091
  (0, ascii_1.printYouLogo)();
1073
1092
  let didShowPortrait = false;
1074
1093
  if (launchSurface !== "you") {
@@ -1083,6 +1102,7 @@ async function printChatOpening(bundleDir, projectCtx) {
1083
1102
  currentProject: projectCtx?.name,
1084
1103
  recentProjects,
1085
1104
  portraitLines,
1105
+ compact: true,
1086
1106
  })
1087
1107
  : false;
1088
1108
  }
@@ -1090,74 +1110,46 @@ async function printChatOpening(bundleDir, projectCtx) {
1090
1110
  console.log("");
1091
1111
  console.log(" " + ACCENT("there you are.") + " " + DIM("your portrait is loaded."));
1092
1112
  }
1093
- if (launchSurface === "you") {
1094
- console.log("");
1095
- investigation = await runYouLaunchInvestigation(bundleDir, projectCtx, recentProjects);
1096
- if (investigation.findings.length > 0) {
1097
- console.log("");
1098
- console.log(" " + ACCENT("i checked a few corners."));
1099
- for (const finding of investigation.findings) {
1100
- console.log(" " + DIM("· ") + chalk_1.default.white(finding));
1101
- }
1102
- }
1103
- }
1104
1113
  console.log("");
1105
- if (launchSurface === "you") {
1106
- console.log(" " + ACCENT("u is here.") + " " + DIM(`good to see you, ${displayName}.`));
1107
- }
1108
- else {
1109
- console.log(" " + ACCENT("u is here.") + " " + DIM(`good to see you, ${user}.`));
1110
- }
1111
- if (projectCtx) {
1112
- console.log(" " + DIM("current project: ") + chalk_1.default.white(projectCtx.name) + DIM(` (${projectCtx.root})`));
1113
- if (repoNeedsBootstrap(projectCtx.root)) {
1114
- console.log(" " + ACCENT("i spotted an opening.") + " " + DIM("this repo still wants AGENTS/project-context wiring."));
1115
- console.log(" " + DIM("say the word or run ") + chalk_1.default.cyan("youmd skill init-project") + DIM(" and i'll set it up."));
1114
+ investigation = await runYouLaunchInvestigation(bundleDir, projectCtx, recentProjects);
1115
+ if (investigation.findings.length > 0) {
1116
+ console.log("");
1117
+ console.log(" " + ACCENT("found:"));
1118
+ for (const finding of investigation.findings.slice(0, 2)) {
1119
+ console.log(" " + DIM("· ") + chalk_1.default.white(finding));
1116
1120
  }
1117
1121
  }
1118
- else {
1119
- console.log(" " + DIM("i don't see a repo context here yet, but i can still help with your identity, links, memories, and private context."));
1120
- }
1121
- if (recentProjects.length > 0) {
1122
- console.log(" " + DIM("recently active: ") + recentProjects.map((name) => chalk_1.default.cyan(name)).join(DIM(", ")));
1123
- }
1124
- const topOpportunity = !projectCtx ? (0, project_1.getTopProjectOpportunity)(recentInsights) : null;
1125
- if (topOpportunity) {
1126
- console.log(" " + ACCENT("next opening i see.") + " " + DIM(topOpportunity.summary));
1127
- console.log(" " + DIM("run:"));
1128
- console.log(" " + chalk_1.default.cyan(topOpportunity.suggestedCommand));
1129
- console.log(" " + DIM("then i'll help tighten it up."));
1122
+ if (launchSurface !== "you") {
1123
+ console.log("");
1124
+ console.log(" " + chalk_1.default.bold("you.md chat"));
1125
+ console.log(" " + DIM(`local context loaded for ${user}.`));
1130
1126
  }
1131
1127
  console.log("");
1132
- console.log(" " + chalk_1.default.bold("you.md chat"));
1133
- console.log(" " + DIM("talk naturally. i'll update your identity, spot useful structure, and suggest next moves."));
1134
- console.log("");
1135
1128
  return investigation;
1136
1129
  }
1137
1130
  function buildYouLaunchIntro(projectCtx, bundleDir, investigation) {
1138
1131
  const displayName = readDisplayName(bundleDir).split(" ")[0];
1139
1132
  const recentInsights = (0, project_1.getRecentProjectInsights)(process.cwd(), 6);
1140
- const recentProjects = (0, project_1.getFeaturedRecentProjectNames)(recentInsights, 6);
1133
+ const recentProjects = investigation.recentProjects.length > 0
1134
+ ? investigation.recentProjects
1135
+ : (0, project_1.getFeaturedRecentProjectNames)(recentInsights, 3);
1141
1136
  const lines = [];
1142
- lines.push(`hi ${displayName}. i'm U — i help other agents know you.`);
1137
+ lines.push(`hi ${displayName}. i'm U.`);
1143
1138
  if (projectCtx) {
1144
- lines.push(`i already clocked that you're inside ${projectCtx.name}.`);
1139
+ lines.push(`i'm inside ${projectCtx.name}.`);
1145
1140
  if (repoNeedsBootstrap(projectCtx.root)) {
1146
- lines.push("this repo still wants cleaner agent wiring, so i can scaffold that whenever you want.");
1141
+ lines.push("it still wants cleaner agent wiring.");
1147
1142
  }
1148
1143
  }
1149
1144
  else if (recentProjects.length > 0) {
1150
- lines.push(`recently you've been orbiting ${recentProjects.slice(0, 3).join(", ")}.`);
1145
+ lines.push(`recent orbit: ${recentProjects.slice(0, 3).join(", ")}.`);
1151
1146
  const topOpportunity = (0, project_1.getTopProjectOpportunity)(recentInsights);
1152
1147
  if (topOpportunity) {
1153
- lines.push(`biggest opening i see: ${topOpportunity.summary}`);
1148
+ lines.push(`strongest opening: ${topOpportunity.summary}`);
1154
1149
  }
1155
1150
  }
1156
1151
  else {
1157
- lines.push("clean slate. we can still shape your identity, private context, or project structure from here.");
1158
- }
1159
- if (investigation.findings.length > 0) {
1160
- lines.push(`quick read: ${investigation.findings.slice(0, 2).join(" ")}`);
1152
+ lines.push("clean slate. we can shape identity, private context, or project structure from here.");
1161
1153
  }
1162
1154
  const strongestMove = investigation.strongestMove
1163
1155
  || (projectCtx && repoNeedsBootstrap(projectCtx.root)
@@ -1165,19 +1157,547 @@ function buildYouLaunchIntro(projectCtx, bundleDir, investigation) {
1165
1157
  : null)
1166
1158
  || (0, project_1.getTopProjectOpportunity)(recentInsights)?.summary
1167
1159
  || null;
1168
- const strongestCommand = investigation.strongestCommand
1169
- || (projectCtx && repoNeedsBootstrap(projectCtx.root) ? "youmd skill init-project" : null)
1170
- || (0, project_1.getTopProjectOpportunity)(recentInsights)?.suggestedCommand
1171
- || null;
1172
1160
  if (strongestMove) {
1173
- lines.push(`the strongest move i can see right now: ${strongestMove}`);
1174
- lines.push("say \"start there\" and i'll take it, or redirect me.");
1161
+ lines.push(`next strongest move: ${strongestMove}`);
1162
+ lines.push("say \"start there\" and i'll take it.");
1175
1163
  }
1176
1164
  else {
1177
- lines.push("point me at the next thing and i'll move first instead of waiting around.");
1165
+ lines.push("point me at the next thing and i'll move first.");
1178
1166
  }
1179
1167
  return lines.join("\n\n");
1180
1168
  }
1169
+ function isStartThereIntent(input) {
1170
+ const lower = input.toLowerCase().trim().replace(/[.!?]+$/, "");
1171
+ return [
1172
+ "start there",
1173
+ "start there please",
1174
+ "do that",
1175
+ "do it",
1176
+ "take it",
1177
+ "go",
1178
+ "go ahead",
1179
+ "yes",
1180
+ "yep",
1181
+ "start",
1182
+ ].includes(lower);
1183
+ }
1184
+ function isLocalRecentProjectsIntent(input) {
1185
+ const lower = input.toLowerCase();
1186
+ const mentionsLocalWork = lower.includes("local director") ||
1187
+ lower.includes("local directory") ||
1188
+ lower.includes("local filesystem") ||
1189
+ lower.includes("my local") ||
1190
+ lower.includes("on my computer") ||
1191
+ lower.includes("workspace") ||
1192
+ lower.includes("workspaces");
1193
+ const asksRecentWork = lower.includes("recently touched") ||
1194
+ lower.includes("most recently") ||
1195
+ lower.includes("what i've been working") ||
1196
+ lower.includes("what ive been working") ||
1197
+ lower.includes("working on lately") ||
1198
+ lower.includes("recent projects");
1199
+ return mentionsLocalWork && asksRecentWork;
1200
+ }
1201
+ function getLocalWorkspaceRoots() {
1202
+ return (0, project_1.getWorkspaceRootCandidates)(process.cwd());
1203
+ }
1204
+ function getRecentFileMtime(projectDir, maxFiles = 1200) {
1205
+ const skipDirs = new Set([
1206
+ ".git",
1207
+ "node_modules",
1208
+ ".next",
1209
+ "dist",
1210
+ "build",
1211
+ ".turbo",
1212
+ ".vercel",
1213
+ "coverage",
1214
+ ".cache",
1215
+ ]);
1216
+ let latest = 0;
1217
+ let visited = 0;
1218
+ const stack = [{ dir: projectDir, depth: 0 }];
1219
+ while (stack.length > 0 && visited < maxFiles) {
1220
+ const current = stack.pop();
1221
+ if (!current)
1222
+ break;
1223
+ let entries = [];
1224
+ try {
1225
+ entries = fs.readdirSync(current.dir, { withFileTypes: true });
1226
+ }
1227
+ catch {
1228
+ continue;
1229
+ }
1230
+ for (const entry of entries) {
1231
+ if (visited >= maxFiles)
1232
+ break;
1233
+ if (entry.name.startsWith(".") && entry.name !== ".youmd-project")
1234
+ continue;
1235
+ const fullPath = path.join(current.dir, entry.name);
1236
+ try {
1237
+ const stat = fs.statSync(fullPath);
1238
+ latest = Math.max(latest, stat.mtimeMs);
1239
+ visited += 1;
1240
+ if (entry.isDirectory() && current.depth < 3 && !skipDirs.has(entry.name)) {
1241
+ stack.push({ dir: fullPath, depth: current.depth + 1 });
1242
+ }
1243
+ }
1244
+ catch {
1245
+ // keep scanning; one unreadable file should not break the local read.
1246
+ }
1247
+ }
1248
+ }
1249
+ return latest;
1250
+ }
1251
+ function scanRecentWorkspaceProjects(limit = 8) {
1252
+ const insights = [];
1253
+ const seen = new Set();
1254
+ for (const root of getLocalWorkspaceRoots()) {
1255
+ let entries = [];
1256
+ try {
1257
+ entries = fs.readdirSync(root, { withFileTypes: true });
1258
+ }
1259
+ catch {
1260
+ continue;
1261
+ }
1262
+ for (const entry of entries) {
1263
+ if (!entry.isDirectory() || entry.name.startsWith("."))
1264
+ continue;
1265
+ const projectDir = path.join(root, entry.name);
1266
+ let realDir = projectDir;
1267
+ try {
1268
+ realDir = fs.realpathSync.native(projectDir);
1269
+ }
1270
+ catch {
1271
+ // use unresolved path below
1272
+ }
1273
+ if (seen.has(realDir))
1274
+ continue;
1275
+ seen.add(realDir);
1276
+ const markerSignals = (0, project_1.getProjectMarkerSignals)(projectDir);
1277
+ if (markerSignals.length === 0)
1278
+ continue;
1279
+ const updatedAt = Math.max(getRecentFileMtime(projectDir), statMtimeMs(projectDir) || 0);
1280
+ insights.push({
1281
+ name: entry.name,
1282
+ slug: entry.name,
1283
+ projectDir,
1284
+ updatedAt,
1285
+ signals: markerSignals,
1286
+ summary: `${entry.name} touched ${formatRelativeTimeFromMs(updatedAt)}${markerSignals.length > 0 ? `; found ${markerSignals.slice(0, 3).join(", ")}` : ""}.`,
1287
+ suggestedCommand: `cd ${projectDir} && you`,
1288
+ });
1289
+ }
1290
+ }
1291
+ return insights.sort((a, b) => b.updatedAt - a.updatedAt).slice(0, limit);
1292
+ }
1293
+ function formatLocalRecentProjectsToolResult(insights) {
1294
+ if (insights.length === 0) {
1295
+ const roots = getLocalWorkspaceRoots();
1296
+ return roots.length === 0
1297
+ ? "tool: workspace_recent_projects\nstatus: empty\nresult: checked common local workspace roots and did not find one. users can set YOUMD_WORKSPACE_ROOTS to add explicit roots."
1298
+ : "tool: workspace_recent_projects\nstatus: empty\nresult: found the workspace root, but no project folders showed up in the first scan.";
1299
+ }
1300
+ const lines = [
1301
+ "tool: workspace_recent_projects",
1302
+ "status: ok",
1303
+ "projects:",
1304
+ ...insights.slice(0, 6).map((item, index) => {
1305
+ const markers = item.signals.length > 0 ? ` — ${item.signals.slice(0, 3).join(", ")}` : "";
1306
+ return `${index + 1}. ${item.name} — ${item.projectDir} — touched ${formatRelativeTimeFromMs(item.updatedAt)}${markers}`;
1307
+ }),
1308
+ "",
1309
+ `recommended_next_project: ${insights[0].name}`,
1310
+ `recommended_next_project_dir: ${insights[0].projectDir}`,
1311
+ `recommended_next_command: cd ${insights[0].projectDir} && you`,
1312
+ `recommended_next_move: say "start there" to open ${insights[0].name} and tighten the agent entrypoint from actual files, not a guessed summary.`,
1313
+ ];
1314
+ return lines.join("\n");
1315
+ }
1316
+ function formatProjectBootstrapToolResult(project, result) {
1317
+ const changed = result.steps
1318
+ .filter((step) => step.ok && step.detail && !step.detail.includes("unchanged") && !step.detail.includes("already present"))
1319
+ .map((step) => `${step.name}: ${step.detail}`);
1320
+ const checked = result.steps.filter((step) => step.ok).length;
1321
+ return [
1322
+ "tool: project_bootstrap",
1323
+ "status: ok",
1324
+ `project: ${project.name}`,
1325
+ `project_dir: ${project.projectDir}`,
1326
+ changed.length > 0
1327
+ ? `changed: ${changed.slice(0, 6).join("; ")}`
1328
+ : `changed: none; checked ${checked} bootstrap steps; everything important was already present.`,
1329
+ "",
1330
+ `recommended_next_move: read ${project.name}'s project-context and turn the rough docs into a sharper current-state + TODO pass.`,
1331
+ ].join("\n");
1332
+ }
1333
+ function isLocalToolLoopCandidate(input) {
1334
+ if (isStartThereIntent(input) || isLocalRecentProjectsIntent(input))
1335
+ return true;
1336
+ const lower = input.toLowerCase();
1337
+ return [
1338
+ "local",
1339
+ "workspace",
1340
+ "workspaces",
1341
+ "filesystem",
1342
+ "file system",
1343
+ "recent projects",
1344
+ "recent work",
1345
+ "working on lately",
1346
+ "project-context",
1347
+ "project context",
1348
+ "agent entrypoint",
1349
+ "agent instructions",
1350
+ "scaffold",
1351
+ "bootstrap",
1352
+ "start there",
1353
+ "sync identity",
1354
+ "sync my identity",
1355
+ "publish identity",
1356
+ "publish my identity",
1357
+ "push my identity",
1358
+ ].some((phrase) => lower.includes(phrase));
1359
+ }
1360
+ function userAskedForMutation(input) {
1361
+ const lower = input.toLowerCase();
1362
+ return [
1363
+ "start there",
1364
+ "do it",
1365
+ "go ahead",
1366
+ "scaffold",
1367
+ "bootstrap",
1368
+ "create",
1369
+ "write",
1370
+ "update",
1371
+ "fix",
1372
+ "tighten",
1373
+ "sync",
1374
+ "publish",
1375
+ "push",
1376
+ "upload",
1377
+ ].some((phrase) => lower.includes(phrase));
1378
+ }
1379
+ function safeJsonObject(text) {
1380
+ const trimmed = text.trim();
1381
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
1382
+ const candidate = fenced?.[1]?.trim() || trimmed.match(/\{[\s\S]*\}/)?.[0];
1383
+ if (!candidate)
1384
+ return null;
1385
+ try {
1386
+ const parsed = JSON.parse(candidate);
1387
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
1388
+ ? parsed
1389
+ : null;
1390
+ }
1391
+ catch {
1392
+ return null;
1393
+ }
1394
+ }
1395
+ function normalizeLocalHostToolCall(value) {
1396
+ if (!value)
1397
+ return null;
1398
+ const tool = value.tool;
1399
+ if (tool !== "discover_projects" &&
1400
+ tool !== "read_project_context" &&
1401
+ tool !== "write_project_context" &&
1402
+ tool !== "sync_identity" &&
1403
+ tool !== "respond") {
1404
+ return null;
1405
+ }
1406
+ const mode = value.mode === "publish" || value.mode === "bootstrap" || value.mode === "status"
1407
+ ? value.mode
1408
+ : undefined;
1409
+ return {
1410
+ tool,
1411
+ project: typeof value.project === "string" ? value.project : undefined,
1412
+ mode,
1413
+ reason: typeof value.reason === "string" ? value.reason : undefined,
1414
+ };
1415
+ }
1416
+ function inferLocalHostToolCall(input, launchInvestigation) {
1417
+ const lower = input.toLowerCase();
1418
+ if (isStartThereIntent(input)) {
1419
+ return { tool: "write_project_context", project: launchInvestigation.strongestProject?.name, mode: "bootstrap" };
1420
+ }
1421
+ if (lower.includes("sync") || lower.includes("publish") || lower.includes("push my identity")) {
1422
+ return { tool: "sync_identity", mode: lower.includes("publish") || lower.includes("push") || lower.includes("sync") ? "publish" : "status" };
1423
+ }
1424
+ if (lower.includes("read") || lower.includes("show") || lower.includes("inspect") || lower.includes("context")) {
1425
+ return { tool: "read_project_context", project: launchInvestigation.strongestProject?.name };
1426
+ }
1427
+ if (lower.includes("scaffold") || lower.includes("bootstrap") || lower.includes("create") || lower.includes("write") || lower.includes("update")) {
1428
+ return { tool: "write_project_context", project: launchInvestigation.strongestProject?.name, mode: "bootstrap" };
1429
+ }
1430
+ return { tool: "discover_projects" };
1431
+ }
1432
+ function collectKnownProjects(launchInvestigation) {
1433
+ const projects = [
1434
+ launchInvestigation.strongestProject,
1435
+ ...scanRecentWorkspaceProjects(12),
1436
+ ...(0, project_1.getRecentProjectInsights)(process.cwd(), 12),
1437
+ ].filter((item) => !!item);
1438
+ const seen = new Set();
1439
+ return projects.filter((project) => {
1440
+ const key = fs.existsSync(project.projectDir)
1441
+ ? fs.realpathSync.native(project.projectDir)
1442
+ : path.resolve(project.projectDir);
1443
+ if (seen.has(key))
1444
+ return false;
1445
+ seen.add(key);
1446
+ return true;
1447
+ });
1448
+ }
1449
+ function resolveProjectForTool(projectRef, launchInvestigation) {
1450
+ const projects = collectKnownProjects(launchInvestigation);
1451
+ if (!projectRef && launchInvestigation.strongestProject)
1452
+ return launchInvestigation.strongestProject;
1453
+ if (!projectRef)
1454
+ return projects[0] || null;
1455
+ const normalized = projectRef.toLowerCase().trim();
1456
+ const byPath = projects.find((project) => path.resolve(project.projectDir) === path.resolve(projectRef));
1457
+ if (byPath)
1458
+ return byPath;
1459
+ return projects.find((project) => project.name.toLowerCase() === normalized ||
1460
+ project.slug.toLowerCase() === normalized ||
1461
+ project.projectDir.toLowerCase().includes(normalized)) || null;
1462
+ }
1463
+ function readSnippet(filePath, maxChars = 1800) {
1464
+ if (!fs.existsSync(filePath))
1465
+ return null;
1466
+ try {
1467
+ const content = fs.readFileSync(filePath, "utf-8").trim();
1468
+ if (!content)
1469
+ return null;
1470
+ return content.length > maxChars ? `${content.slice(0, maxChars)}\n...` : content;
1471
+ }
1472
+ catch {
1473
+ return null;
1474
+ }
1475
+ }
1476
+ function formatProjectReadToolResult(project) {
1477
+ const files = [
1478
+ "AGENTS.md",
1479
+ "CLAUDE.md",
1480
+ "package.json",
1481
+ "pyproject.toml",
1482
+ "Cargo.toml",
1483
+ "go.mod",
1484
+ "project-context/CURRENT_STATE.md",
1485
+ "project-context/TODO.md",
1486
+ "project-context/PRD.md",
1487
+ "project-context/ARCHITECTURE.md",
1488
+ ".you/project-context/CURRENT_STATE.md",
1489
+ ".you/project-context/TODO.md",
1490
+ ];
1491
+ const snippets = files
1492
+ .map((relativePath) => {
1493
+ const snippet = readSnippet(path.join(project.projectDir, relativePath));
1494
+ return snippet ? `file: ${relativePath}\n${snippet}` : null;
1495
+ })
1496
+ .filter((item) => !!item)
1497
+ .slice(0, 5);
1498
+ const managedContext = (0, project_1.readProjectContext)(project.projectDir);
1499
+ return [
1500
+ "tool: read_project_context",
1501
+ "status: ok",
1502
+ `project: ${project.name}`,
1503
+ `project_dir: ${project.projectDir}`,
1504
+ `markers: ${(0, project_1.getProjectMarkerSignals)(project.projectDir).join(", ") || "none"}`,
1505
+ managedContext ? `managed_project_context: ${managedContext.meta.name}` : "managed_project_context: none",
1506
+ "",
1507
+ snippets.length > 0 ? snippets.join("\n\n---\n\n") : "no readable project-context or agent entrypoint files found yet.",
1508
+ "",
1509
+ `recommended_next_move: write a sharper current-state + TODO pass for ${project.name} if the user wants this project tightened.`,
1510
+ ].join("\n");
1511
+ }
1512
+ async function formatIdentitySyncToolResult(bundleDir, publish) {
1513
+ const result = (0, compiler_1.compileBundle)(bundleDir);
1514
+ (0, compiler_1.writeBundle)(bundleDir, result);
1515
+ const lines = [
1516
+ "tool: sync_identity",
1517
+ "status: ok",
1518
+ `bundle_dir: ${bundleDir}`,
1519
+ `compiled_version: ${result.stats.version}`,
1520
+ `sections: ${result.stats.filledSections}/${result.stats.totalSections}`,
1521
+ ];
1522
+ if (!publish) {
1523
+ lines.push("remote_sync: not requested; local bundle compiled only.");
1524
+ lines.push("recommended_next_move: publish if you want this local identity bundle pushed to you.md.");
1525
+ return lines.join("\n");
1526
+ }
1527
+ if (!(0, config_1.isAuthenticated)()) {
1528
+ return [
1529
+ ...lines,
1530
+ "remote_sync: blocked; not authenticated.",
1531
+ "recommended_next_move: run youmd login, then ask me to sync identity again.",
1532
+ ].join("\n");
1533
+ }
1534
+ const youJson = JSON.parse(fs.readFileSync(path.join(bundleDir, "you.json"), "utf-8"));
1535
+ const youMd = fs.readFileSync(path.join(bundleDir, "you.md"), "utf-8");
1536
+ const manifest = JSON.parse(fs.readFileSync(path.join(bundleDir, "manifest.json"), "utf-8"));
1537
+ const uploadRes = await (0, api_1.uploadBundle)({ manifest, youJson, youMd });
1538
+ if (!uploadRes.ok) {
1539
+ return [
1540
+ ...lines,
1541
+ `remote_sync: upload failed with status ${uploadRes.status}.`,
1542
+ "recommended_next_move: inspect auth/API state before retrying identity sync.",
1543
+ ].join("\n");
1544
+ }
1545
+ const publishRes = await (0, api_1.publishLatest)();
1546
+ if (!publishRes.ok) {
1547
+ return [
1548
+ ...lines,
1549
+ `remote_sync: publish failed with status ${publishRes.status}.`,
1550
+ "recommended_next_move: inspect publish API state before retrying identity sync.",
1551
+ ].join("\n");
1552
+ }
1553
+ lines.push(`remote_sync: published v${publishRes.data.version} as ${publishRes.data.username}.`);
1554
+ lines.push(`url: ${publishRes.data.url || `https://you.md/${publishRes.data.username}`}`);
1555
+ lines.push("recommended_next_move: test the updated identity from another agent surface.");
1556
+ return lines.join("\n");
1557
+ }
1558
+ async function chooseLocalHostTool(args) {
1559
+ const projects = collectKnownProjects(args.launchInvestigation)
1560
+ .slice(0, 8)
1561
+ .map((project) => `${project.name} | ${project.projectDir} | ${project.signals.slice(0, 4).join(", ")}`)
1562
+ .join("\n");
1563
+ const prompt = [
1564
+ "You are the local host tool router for the you.md CLI. Return ONLY compact JSON. No prose.",
1565
+ "Available tools:",
1566
+ "- discover_projects: scan local filesystem markers for recent project work.",
1567
+ "- read_project_context: read AGENTS/CLAUDE/package/project-context files for a project.",
1568
+ "- write_project_context: bootstrap/tighten project context files for a project. Use only when the user asks to start, scaffold, update, write, fix, or tighten.",
1569
+ "- sync_identity: compile identity locally; use mode publish only when the user asks to sync/publish/push identity.",
1570
+ "- respond: no local tool needed.",
1571
+ "",
1572
+ "JSON shape: {\"tool\":\"discover_projects|read_project_context|write_project_context|sync_identity|respond\",\"project\":\"optional project name/path\",\"mode\":\"status|publish|bootstrap\",\"reason\":\"short\"}",
1573
+ "",
1574
+ "Known projects:",
1575
+ projects || "none yet",
1576
+ "",
1577
+ `User request: ${args.userInput}`,
1578
+ ].join("\n");
1579
+ try {
1580
+ const raw = await (0, onboarding_1.callLLM)(args.apiKey, [
1581
+ { role: "system", content: "Return only valid JSON for local tool routing." },
1582
+ { role: "user", content: prompt },
1583
+ ]);
1584
+ return normalizeLocalHostToolCall(safeJsonObject(raw)) || inferLocalHostToolCall(args.userInput, args.launchInvestigation);
1585
+ }
1586
+ catch {
1587
+ return inferLocalHostToolCall(args.userInput, args.launchInvestigation);
1588
+ }
1589
+ }
1590
+ async function executeLocalHostTool(toolCall, args) {
1591
+ if (toolCall.tool === "discover_projects") {
1592
+ const insights = scanRecentWorkspaceProjects(10);
1593
+ args.launchInvestigation.strongestProject = insights[0] || args.launchInvestigation.strongestProject;
1594
+ args.launchInvestigation.strongestMove = insights[0]?.summary || args.launchInvestigation.strongestMove;
1595
+ return formatLocalRecentProjectsToolResult(insights);
1596
+ }
1597
+ if (toolCall.tool === "read_project_context") {
1598
+ const project = resolveProjectForTool(toolCall.project, args.launchInvestigation);
1599
+ if (!project) {
1600
+ return "tool: read_project_context\nstatus: blocked\nresult: no real local project target is known yet.\nrecommended_next_move: run discover_projects first.";
1601
+ }
1602
+ args.launchInvestigation.strongestProject = project;
1603
+ return formatProjectReadToolResult(project);
1604
+ }
1605
+ if (toolCall.tool === "write_project_context") {
1606
+ const project = resolveProjectForTool(toolCall.project, args.launchInvestigation);
1607
+ if (!project) {
1608
+ return "tool: write_project_context\nstatus: blocked\nresult: no real local project target is known yet.\nrecommended_next_move: run discover_projects first.";
1609
+ }
1610
+ if (!userAskedForMutation(args.userInput)) {
1611
+ return [
1612
+ "tool: write_project_context",
1613
+ "status: blocked",
1614
+ `project: ${project.name}`,
1615
+ "result: user did not explicitly ask for a filesystem mutation.",
1616
+ `recommended_next_move: say "start there" to bootstrap ${project.name}.`,
1617
+ ].join("\n");
1618
+ }
1619
+ const previousCwd = process.cwd();
1620
+ let result;
1621
+ try {
1622
+ process.chdir(project.projectDir);
1623
+ result = (0, skills_1.initProject)({ mode: "additive" });
1624
+ }
1625
+ finally {
1626
+ process.chdir(previousCwd);
1627
+ }
1628
+ args.launchInvestigation.strongestProject = project;
1629
+ return formatProjectBootstrapToolResult(project, result);
1630
+ }
1631
+ if (toolCall.tool === "sync_identity") {
1632
+ const publish = toolCall.mode === "publish" && userAskedForMutation(args.userInput);
1633
+ return await formatIdentitySyncToolResult(args.bundleDir, publish);
1634
+ }
1635
+ return "tool: respond\nstatus: skipped\nresult: no local host tool needed.";
1636
+ }
1637
+ async function handleLocalChatIntent(args) {
1638
+ const runToolResultThroughModel = async (toolResult, spinnerLabel) => {
1639
+ args.messages.push({ role: "user", content: args.userInput });
1640
+ args.messages.push({
1641
+ role: "user",
1642
+ content: [
1643
+ "--- local host tool result ---",
1644
+ toolResult,
1645
+ "--- instructions ---",
1646
+ "Use this local tool result as ground truth. Do not say you cannot access the filesystem; the CLI host just accessed it for you.",
1647
+ "Use recommended_next_project exactly as the target unless the user explicitly asks for a different project.",
1648
+ "Do not say you are scraping, pulling, reading, opening, or updating anything now unless that completed action appears in the tool result.",
1649
+ "Do not ask the user what the local project is if the tool result already names it.",
1650
+ "State what the local host actually found, make one concrete recommendation, and end with the exact phrase `next strongest move: ...`.",
1651
+ "For workspace scans, reuse recommended_next_move as the final next strongest move. The supported follow-up command is `start there`; do not invent commands like `open PROJECT`.",
1652
+ "For read_project_context results, summarize what you actually saw and recommend the next concrete local tool action.",
1653
+ "For sync_identity results, state whether local compile or remote publish happened.",
1654
+ "Do not end with a question. Keep it under 8 lines. No generic help-desk closer.",
1655
+ ].join("\n"),
1656
+ });
1657
+ const result = await callLLMWithStreaming(args.apiKey, args.messages, spinnerLabel);
1658
+ args.messages.push({ role: "assistant", content: result.text });
1659
+ if (!result.streamed) {
1660
+ printAgentMessage((0, onboarding_1.parseUpdatesFromResponse)(result.text).display);
1661
+ }
1662
+ };
1663
+ if (isLocalToolLoopCandidate(args.userInput)) {
1664
+ const routeSpinner = new render_1.BrailleSpinner("choosing local tool");
1665
+ routeSpinner.start();
1666
+ const toolCall = await chooseLocalHostTool(args);
1667
+ routeSpinner.stop(`selected ${toolCall.tool}`);
1668
+ if (toolCall.tool === "respond")
1669
+ return false;
1670
+ const labelByTool = {
1671
+ discover_projects: "discovering local projects",
1672
+ read_project_context: "reading project context",
1673
+ write_project_context: "writing project context",
1674
+ sync_identity: "syncing identity context",
1675
+ respond: "thinking",
1676
+ };
1677
+ const spinner = new render_1.BrailleSpinner(labelByTool[toolCall.tool]);
1678
+ spinner.start();
1679
+ try {
1680
+ const toolResult = await executeLocalHostTool(toolCall, args);
1681
+ const statusLine = toolResult.match(/^status: (.+)$/m)?.[1] || "done";
1682
+ spinner.stop(statusLine);
1683
+ await runToolResultThroughModel(toolResult, `summarizing ${toolCall.tool}`);
1684
+ }
1685
+ catch (err) {
1686
+ const message = err instanceof Error ? err.message : "local tool failed";
1687
+ spinner.fail(message);
1688
+ const response = [
1689
+ `local tool failed: ${message}`,
1690
+ "",
1691
+ "next strongest move: retry with a read-only local scan so i can recover from real filesystem state.",
1692
+ ].join("\n");
1693
+ args.messages.push({ role: "user", content: args.userInput });
1694
+ args.messages.push({ role: "assistant", content: response });
1695
+ printAgentMessage(response);
1696
+ }
1697
+ return true;
1698
+ }
1699
+ return false;
1700
+ }
1181
1701
  // ─── Main chat command ────────────────────────────────────────────────
1182
1702
  async function chatCommand() {
1183
1703
  const bundleDir = resolveBundleDirForChat();
@@ -1274,41 +1794,11 @@ async function chatCommand() {
1274
1794
  content: `here is my current identity bundle:\n\n${currentBundle}${directivesContext}${projectContextBlock}\n\n${greetingInstruction}`,
1275
1795
  },
1276
1796
  ];
1277
- // Initial greeting from agent
1278
- if (process.env.YOUMD_LAUNCH_SURFACE === "you") {
1279
- const proactiveIntro = buildYouLaunchIntro(projectCtx, bundleDir, launchInvestigation);
1280
- messages.push({ role: "assistant", content: proactiveIntro });
1281
- printAgentMessage(proactiveIntro);
1282
- }
1283
- else {
1284
- let response;
1285
- let streamed = false;
1286
- try {
1287
- const result = await callLLMWithStreaming(apiKey, messages, (0, onboarding_1.randomThinking)());
1288
- response = result.text;
1289
- streamed = result.streamed;
1290
- }
1291
- catch (err) {
1292
- console.log(chalk_1.default.red(` failed to connect: ${err instanceof Error ? err.message : String(err)}`));
1293
- console.log(chalk_1.default.dim(" chat requires the AI service. try again later."));
1294
- console.log("");
1295
- rl.close();
1296
- return;
1297
- }
1298
- messages.push({ role: "assistant", content: response });
1299
- const initial = (0, onboarding_1.parseUpdatesFromResponse)(response);
1300
- // Write any updates (unlikely on greeting, but handle it)
1301
- if (initial.updates.length > 0) {
1302
- for (const update of initial.updates) {
1303
- (0, onboarding_1.writeSectionFile)(bundleDir, update.section, update.content);
1304
- }
1305
- console.log(chalk_1.default.cyan(` [updated: ${initial.updates.map((u) => (0, onboarding_1.sectionLabel)(u.section)).join(", ")}]`));
1306
- console.log("");
1307
- }
1308
- if (!streamed) {
1309
- printAgentMessage(initial.display);
1310
- }
1311
- }
1797
+ // Initial greeting is local and action-aware. The remote model should not invent
1798
+ // filesystem capabilities before the CLI has decided what it can actually do.
1799
+ const proactiveIntro = buildYouLaunchIntro(projectCtx, bundleDir, launchInvestigation);
1800
+ messages.push({ role: "assistant", content: proactiveIntro });
1801
+ printAgentMessage(proactiveIntro);
1312
1802
  // ── Conversation loop ──────────────────────────────────────────────
1313
1803
  let response = "";
1314
1804
  let streamed = false;
@@ -1517,6 +2007,15 @@ async function chatCommand() {
1517
2007
  }
1518
2008
  continue;
1519
2009
  }
2010
+ const handledLocally = await handleLocalChatIntent({
2011
+ userInput,
2012
+ messages,
2013
+ launchInvestigation,
2014
+ apiKey,
2015
+ bundleDir,
2016
+ });
2017
+ if (handledLocally)
2018
+ continue;
1520
2019
  // ── Detect dragged/pasted file paths ──
1521
2020
  const detectedFile = detectFilePath(userInput);
1522
2021
  if (detectedFile) {