youmd 0.6.11 → 0.6.19

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.
@@ -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.11";
58
+ const CURRENT_VERSION = "0.6.19";
58
59
  function delay(ms) {
59
60
  return new Promise((resolve) => setTimeout(resolve, ms));
60
61
  }
@@ -915,6 +916,59 @@ function countMarkdownFiles(dir) {
915
916
  return 0;
916
917
  }
917
918
  }
919
+ function formatRelativeTimeFromMs(ms) {
920
+ const diff = Date.now() - ms;
921
+ const hour = 60 * 60 * 1000;
922
+ const day = 24 * hour;
923
+ if (diff < hour)
924
+ return "within the last hour";
925
+ if (diff < day)
926
+ return `${Math.max(1, Math.floor(diff / hour))}h ago`;
927
+ return `${Math.max(1, Math.floor(diff / day))}d ago`;
928
+ }
929
+ function statMtimeMs(filePath) {
930
+ try {
931
+ return fs.statSync(filePath).mtimeMs;
932
+ }
933
+ catch {
934
+ return null;
935
+ }
936
+ }
937
+ function collectHomeAgentSignals() {
938
+ const findings = [];
939
+ const home = os.homedir();
940
+ const homeAgents = path.join(home, "AGENTS.md");
941
+ const homeClaude = path.join(home, "CLAUDE.md");
942
+ const claudeConfig = path.join(home, ".claude", "CLAUDE.md");
943
+ const claudeProjects = path.join(home, ".claude", "projects");
944
+ const codexProjects = path.join(home, ".codex", "projects");
945
+ const legacyCodexProjects = path.join(home, ".Codex", "projects");
946
+ const stackSyncSkill = path.join(home, ".claude", "skills", "agent-stack-sync");
947
+ const homeInstructionFiles = [homeAgents, homeClaude, claudeConfig].filter((filePath) => fs.existsSync(filePath));
948
+ if (homeInstructionFiles.length > 0) {
949
+ findings.push("your home-level agent instructions are present, so i can anchor on shared guidance instead of guessing.");
950
+ }
951
+ const recentHomeInstruction = homeInstructionFiles
952
+ .map((filePath) => statMtimeMs(filePath))
953
+ .filter((value) => typeof value === "number")
954
+ .sort((a, b) => b - a)[0];
955
+ if (recentHomeInstruction) {
956
+ findings.push(`your shared agent docs were touched ${formatRelativeTimeFromMs(recentHomeInstruction)}.`);
957
+ }
958
+ const recentSessionRoots = [claudeProjects, codexProjects, legacyCodexProjects]
959
+ .map((dir) => ({ dir, mtime: statMtimeMs(dir) }))
960
+ .filter((entry) => !!entry.mtime)
961
+ .sort((a, b) => b.mtime - a.mtime);
962
+ if (recentSessionRoots.length > 0) {
963
+ const freshest = recentSessionRoots[0];
964
+ const label = freshest.dir.includes(".claude") ? "claude" : "codex";
965
+ findings.push(`there's fresh ${label}-side session activity under ${freshest.dir} from ${formatRelativeTimeFromMs(freshest.mtime)}.`);
966
+ }
967
+ if (fs.existsSync(stackSyncSkill)) {
968
+ findings.push("your shared stack-sync skill is installed, so i can lean on mirrored cross-agent context too.");
969
+ }
970
+ return findings;
971
+ }
918
972
  async function runYouLaunchInvestigation(bundleDir, projectCtx, recentProjects) {
919
973
  const labels = [
920
974
  "sipping bitbucks frappaccino while i look around",
@@ -925,13 +979,16 @@ async function runYouLaunchInvestigation(bundleDir, projectCtx, recentProjects)
925
979
  const spinner = new render_1.BrailleSpinner(labels[0]);
926
980
  let labelIndex = 1;
927
981
  const findings = [];
982
+ let strongestMove;
983
+ let strongestCommand;
984
+ let strongestProject;
928
985
  const rotation = setInterval(() => {
929
986
  spinner.update(labels[labelIndex % labels.length]);
930
987
  labelIndex += 1;
931
988
  }, 1100);
932
989
  spinner.start();
933
990
  try {
934
- await delay(160);
991
+ await delay(600);
935
992
  try {
936
993
  const hasPreferences = fs.existsSync(path.join(bundleDir, "preferences", "agent.md"));
937
994
  const hasDirectives = fs.existsSync(path.join(bundleDir, "directives", "agent.md"));
@@ -945,6 +1002,8 @@ async function runYouLaunchInvestigation(bundleDir, projectCtx, recentProjects)
945
1002
  catch {
946
1003
  // keep scanning other surfaces
947
1004
  }
1005
+ const homeSignals = collectHomeAgentSignals();
1006
+ findings.push(...homeSignals.slice(0, 3));
948
1007
  if (projectCtx) {
949
1008
  try {
950
1009
  const hasAgents = fs.existsSync(path.join(projectCtx.root, "AGENTS.md"));
@@ -966,6 +1025,19 @@ async function runYouLaunchInvestigation(bundleDir, projectCtx, recentProjects)
966
1025
  else {
967
1026
  findings.push(`${projectCtx.name} still wants a real project-context spine.`);
968
1027
  }
1028
+ if (repoNeedsBootstrap(projectCtx.root)) {
1029
+ strongestMove = `${projectCtx.name} still wants cleaner agent wiring and project-context scaffolding.`;
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
+ };
1040
+ }
969
1041
  }
970
1042
  catch {
971
1043
  findings.push(`${projectCtx.name} is open, but one of the local context probes tripped over itself.`);
@@ -982,13 +1054,25 @@ async function runYouLaunchInvestigation(bundleDir, projectCtx, recentProjects)
982
1054
  if (opportunities.length === 0 && insights.length > 0) {
983
1055
  findings.push(`${insights[0].name} already looks pretty well-shaped, so i can go deeper instead of scaffolding basics.`);
984
1056
  }
1057
+ else if (opportunities.length > 0) {
1058
+ const strongest = opportunities[0];
1059
+ strongestMove = strongest.summary;
1060
+ strongestCommand = strongest.suggestedCommand;
1061
+ strongestProject = strongest;
1062
+ }
985
1063
  }
986
1064
  else {
987
1065
  findings.push("i've got your home bundle loaded, even though we're not inside a project yet.");
988
1066
  }
989
- await delay(220);
1067
+ await delay(900);
990
1068
  spinner.stop("looked through local context");
991
- return { findings: findings.slice(0, 4) };
1069
+ return {
1070
+ findings: findings.slice(0, 3),
1071
+ strongestMove,
1072
+ strongestCommand,
1073
+ strongestProject,
1074
+ recentProjects: recentProjects.slice(0, 3),
1075
+ };
992
1076
  }
993
1077
  finally {
994
1078
  clearInterval(rotation);
@@ -1003,7 +1087,7 @@ async function printChatOpening(bundleDir, projectCtx) {
1003
1087
  const recentInsights = (0, project_1.getRecentProjectInsights)(process.cwd(), 6);
1004
1088
  const recentProjects = (0, project_1.getFeaturedRecentProjectNames)(recentInsights, 6);
1005
1089
  const launchSurface = process.env.YOUMD_LAUNCH_SURFACE;
1006
- let investigation = { findings: [] };
1090
+ let investigation = { findings: [], recentProjects: recentProjects.slice(0, 3) };
1007
1091
  (0, ascii_1.printYouLogo)();
1008
1092
  let didShowPortrait = false;
1009
1093
  if (launchSurface !== "you") {
@@ -1018,6 +1102,7 @@ async function printChatOpening(bundleDir, projectCtx) {
1018
1102
  currentProject: projectCtx?.name,
1019
1103
  recentProjects,
1020
1104
  portraitLines,
1105
+ compact: true,
1021
1106
  })
1022
1107
  : false;
1023
1108
  }
@@ -1025,78 +1110,289 @@ async function printChatOpening(bundleDir, projectCtx) {
1025
1110
  console.log("");
1026
1111
  console.log(" " + ACCENT("there you are.") + " " + DIM("your portrait is loaded."));
1027
1112
  }
1028
- if (launchSurface === "you") {
1029
- console.log("");
1030
- investigation = await runYouLaunchInvestigation(bundleDir, projectCtx, recentProjects);
1031
- if (investigation.findings.length > 0) {
1032
- console.log("");
1033
- console.log(" " + ACCENT("i checked a few corners."));
1034
- for (const finding of investigation.findings) {
1035
- console.log(" " + DIM("· ") + chalk_1.default.white(finding));
1036
- }
1037
- }
1038
- }
1039
1113
  console.log("");
1040
- if (launchSurface === "you") {
1041
- console.log(" " + ACCENT("u is here.") + " " + DIM(`good to see you, ${displayName}.`));
1042
- }
1043
- else {
1044
- console.log(" " + ACCENT("u is here.") + " " + DIM(`good to see you, ${user}.`));
1045
- }
1046
- if (projectCtx) {
1047
- console.log(" " + DIM("current project: ") + chalk_1.default.white(projectCtx.name) + DIM(` (${projectCtx.root})`));
1048
- if (repoNeedsBootstrap(projectCtx.root)) {
1049
- console.log(" " + ACCENT("i spotted an opening.") + " " + DIM("this repo still wants AGENTS/project-context wiring."));
1050
- 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));
1051
1120
  }
1052
1121
  }
1053
- else {
1054
- 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."));
1055
- }
1056
- if (recentProjects.length > 0) {
1057
- console.log(" " + DIM("recently active: ") + recentProjects.map((name) => chalk_1.default.cyan(name)).join(DIM(", ")));
1058
- }
1059
- const topOpportunity = !projectCtx ? (0, project_1.getTopProjectOpportunity)(recentInsights) : null;
1060
- if (topOpportunity) {
1061
- console.log(" " + ACCENT("next opening i see.") + " " + DIM(topOpportunity.summary));
1062
- console.log(" " + DIM("run:"));
1063
- console.log(" " + chalk_1.default.cyan(topOpportunity.suggestedCommand));
1064
- 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}.`));
1065
1126
  }
1066
1127
  console.log("");
1067
- console.log(" " + chalk_1.default.bold("you.md chat"));
1068
- console.log(" " + DIM("talk naturally. i'll update your identity, spot useful structure, and suggest next moves."));
1069
- console.log("");
1070
1128
  return investigation;
1071
1129
  }
1072
1130
  function buildYouLaunchIntro(projectCtx, bundleDir, investigation) {
1073
1131
  const displayName = readDisplayName(bundleDir).split(" ")[0];
1074
1132
  const recentInsights = (0, project_1.getRecentProjectInsights)(process.cwd(), 6);
1075
- 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);
1076
1136
  const lines = [];
1077
- lines.push(`hi ${displayName}. i'm U — i help other agents know you.`);
1137
+ lines.push(`hi ${displayName}. i'm U.`);
1078
1138
  if (projectCtx) {
1079
- lines.push(`i already clocked that you're inside ${projectCtx.name}.`);
1139
+ lines.push(`i'm inside ${projectCtx.name}.`);
1080
1140
  if (repoNeedsBootstrap(projectCtx.root)) {
1081
- 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.");
1082
1142
  }
1083
1143
  }
1084
1144
  else if (recentProjects.length > 0) {
1085
- lines.push(`recently you've been orbiting ${recentProjects.slice(0, 3).join(", ")}.`);
1145
+ lines.push(`recent orbit: ${recentProjects.slice(0, 3).join(", ")}.`);
1086
1146
  const topOpportunity = (0, project_1.getTopProjectOpportunity)(recentInsights);
1087
1147
  if (topOpportunity) {
1088
- lines.push(`biggest opening i see: ${topOpportunity.summary}`);
1148
+ lines.push(`strongest opening: ${topOpportunity.summary}`);
1089
1149
  }
1090
1150
  }
1091
1151
  else {
1092
- lines.push("clean slate. we can still shape your identity, private context, or project structure from here.");
1152
+ lines.push("clean slate. we can shape identity, private context, or project structure from here.");
1153
+ }
1154
+ const strongestMove = investigation.strongestMove
1155
+ || (projectCtx && repoNeedsBootstrap(projectCtx.root)
1156
+ ? `${projectCtx.name} still wants cleaner agent wiring and project-context scaffolding.`
1157
+ : null)
1158
+ || (0, project_1.getTopProjectOpportunity)(recentInsights)?.summary
1159
+ || null;
1160
+ if (strongestMove) {
1161
+ lines.push(`next strongest move: ${strongestMove}`);
1162
+ lines.push("say \"start there\" and i'll take it.");
1093
1163
  }
1094
- if (investigation.findings.length > 0) {
1095
- lines.push(`quick read: ${investigation.findings.slice(0, 2).join(" ")}`);
1164
+ else {
1165
+ lines.push("point me at the next thing and i'll move first.");
1096
1166
  }
1097
- lines.push("what are we moving forward right now?");
1098
1167
  return lines.join("\n\n");
1099
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
+ async function handleLocalChatIntent(args) {
1334
+ const runToolResultThroughModel = async (toolResult, spinnerLabel) => {
1335
+ args.messages.push({ role: "user", content: args.userInput });
1336
+ args.messages.push({
1337
+ role: "user",
1338
+ content: [
1339
+ "--- local host tool result ---",
1340
+ toolResult,
1341
+ "--- instructions ---",
1342
+ "Use this local tool result as ground truth. Do not say you cannot access the filesystem; the CLI host just accessed it for you.",
1343
+ "Use recommended_next_project exactly as the target unless the user explicitly asks for a different project.",
1344
+ "Do not say you are scraping, pulling, reading, opening, or updating anything now unless that completed action appears in the tool result.",
1345
+ "Do not ask the user what the local project is if the tool result already names it.",
1346
+ "State what the local host actually found, make one concrete recommendation, and end with the exact phrase `next strongest move: ...`.",
1347
+ "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`.",
1348
+ "Do not end with a question. Keep it under 8 lines. No generic help-desk closer.",
1349
+ ].join("\n"),
1350
+ });
1351
+ const result = await callLLMWithStreaming(args.apiKey, args.messages, spinnerLabel);
1352
+ args.messages.push({ role: "assistant", content: result.text });
1353
+ if (!result.streamed) {
1354
+ printAgentMessage((0, onboarding_1.parseUpdatesFromResponse)(result.text).display);
1355
+ }
1356
+ };
1357
+ if (isLocalRecentProjectsIntent(args.userInput)) {
1358
+ const spinner = new render_1.BrailleSpinner("checking local workspaces");
1359
+ spinner.start();
1360
+ await delay(700);
1361
+ const insights = scanRecentWorkspaceProjects(8);
1362
+ spinner.stop("local workspace scanned");
1363
+ args.launchInvestigation.strongestProject = insights[0] || args.launchInvestigation.strongestProject;
1364
+ args.launchInvestigation.strongestMove = insights[0]?.summary || args.launchInvestigation.strongestMove;
1365
+ await runToolResultThroughModel(formatLocalRecentProjectsToolResult(insights), "turning local project scan into a useful read");
1366
+ return true;
1367
+ }
1368
+ if (isStartThereIntent(args.userInput)) {
1369
+ const project = args.launchInvestigation.strongestProject ||
1370
+ (0, project_1.getTopProjectOpportunity)((0, project_1.getRecentProjectInsights)(process.cwd(), 8));
1371
+ if (!project || !fs.existsSync(project.projectDir)) {
1372
+ const response = "i don't have a real local target for that yet. ask me to scan local workspaces and i'll pick from actual folders.\n\nnext strongest move: scan local recent projects first.";
1373
+ args.messages.push({ role: "user", content: args.userInput });
1374
+ args.messages.push({ role: "assistant", content: response });
1375
+ printAgentMessage(response);
1376
+ return true;
1377
+ }
1378
+ const spinner = new render_1.BrailleSpinner(`opening ${project.name} from local disk`);
1379
+ spinner.start();
1380
+ await delay(500);
1381
+ const previousCwd = process.cwd();
1382
+ let result;
1383
+ try {
1384
+ process.chdir(project.projectDir);
1385
+ result = (0, skills_1.initProject)({ mode: "additive" });
1386
+ }
1387
+ finally {
1388
+ process.chdir(previousCwd);
1389
+ }
1390
+ spinner.stop(`${project.name} updated`);
1391
+ await runToolResultThroughModel(formatProjectBootstrapToolResult(project, result), `summarizing ${project.name} changes`);
1392
+ return true;
1393
+ }
1394
+ return false;
1395
+ }
1100
1396
  // ─── Main chat command ────────────────────────────────────────────────
1101
1397
  async function chatCommand() {
1102
1398
  const bundleDir = resolveBundleDirForChat();
@@ -1193,41 +1489,11 @@ async function chatCommand() {
1193
1489
  content: `here is my current identity bundle:\n\n${currentBundle}${directivesContext}${projectContextBlock}\n\n${greetingInstruction}`,
1194
1490
  },
1195
1491
  ];
1196
- // Initial greeting from agent
1197
- if (process.env.YOUMD_LAUNCH_SURFACE === "you") {
1198
- const proactiveIntro = buildYouLaunchIntro(projectCtx, bundleDir, launchInvestigation);
1199
- messages.push({ role: "assistant", content: proactiveIntro });
1200
- printAgentMessage(proactiveIntro);
1201
- }
1202
- else {
1203
- let response;
1204
- let streamed = false;
1205
- try {
1206
- const result = await callLLMWithStreaming(apiKey, messages, (0, onboarding_1.randomThinking)());
1207
- response = result.text;
1208
- streamed = result.streamed;
1209
- }
1210
- catch (err) {
1211
- console.log(chalk_1.default.red(` failed to connect: ${err instanceof Error ? err.message : String(err)}`));
1212
- console.log(chalk_1.default.dim(" chat requires the AI service. try again later."));
1213
- console.log("");
1214
- rl.close();
1215
- return;
1216
- }
1217
- messages.push({ role: "assistant", content: response });
1218
- const initial = (0, onboarding_1.parseUpdatesFromResponse)(response);
1219
- // Write any updates (unlikely on greeting, but handle it)
1220
- if (initial.updates.length > 0) {
1221
- for (const update of initial.updates) {
1222
- (0, onboarding_1.writeSectionFile)(bundleDir, update.section, update.content);
1223
- }
1224
- console.log(chalk_1.default.cyan(` [updated: ${initial.updates.map((u) => (0, onboarding_1.sectionLabel)(u.section)).join(", ")}]`));
1225
- console.log("");
1226
- }
1227
- if (!streamed) {
1228
- printAgentMessage(initial.display);
1229
- }
1230
- }
1492
+ // Initial greeting is local and action-aware. The remote model should not invent
1493
+ // filesystem capabilities before the CLI has decided what it can actually do.
1494
+ const proactiveIntro = buildYouLaunchIntro(projectCtx, bundleDir, launchInvestigation);
1495
+ messages.push({ role: "assistant", content: proactiveIntro });
1496
+ printAgentMessage(proactiveIntro);
1231
1497
  // ── Conversation loop ──────────────────────────────────────────────
1232
1498
  let response = "";
1233
1499
  let streamed = false;
@@ -1436,6 +1702,14 @@ async function chatCommand() {
1436
1702
  }
1437
1703
  continue;
1438
1704
  }
1705
+ const handledLocally = await handleLocalChatIntent({
1706
+ userInput,
1707
+ messages,
1708
+ launchInvestigation,
1709
+ apiKey,
1710
+ });
1711
+ if (handledLocally)
1712
+ continue;
1439
1713
  // ── Detect dragged/pasted file paths ──
1440
1714
  const detectedFile = detectFilePath(userInput);
1441
1715
  if (detectedFile) {