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.
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +595 -96
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/skill.js +2 -2
- package/dist/commands/skill.js.map +1 -1
- package/dist/index.js +79 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/ascii.d.ts +5 -0
- package/dist/lib/ascii.d.ts.map +1 -1
- package/dist/lib/ascii.js +34 -2
- package/dist/lib/ascii.js.map +1 -1
- package/dist/lib/first-run.d.ts +14 -0
- package/dist/lib/first-run.d.ts.map +1 -0
- package/dist/lib/first-run.js +97 -0
- package/dist/lib/first-run.js.map +1 -0
- package/dist/lib/onboarding.js +1 -1
- package/dist/lib/onboarding.js.map +1 -1
- package/dist/lib/project.d.ts +2 -0
- package/dist/lib/project.d.ts.map +1 -1
- package/dist/lib/project.js +173 -22
- package/dist/lib/project.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/you.js +1 -1
- package/dist/you.js.map +1 -1
- package/package.json +1 -1
- package/dist/__tests__/api.test.d.ts +0 -2
- package/dist/__tests__/api.test.d.ts.map +0 -1
- package/dist/__tests__/api.test.js +0 -84
- package/dist/__tests__/api.test.js.map +0 -1
- package/dist/__tests__/compiler.test.d.ts +0 -2
- package/dist/__tests__/compiler.test.d.ts.map +0 -1
- package/dist/__tests__/compiler.test.js +0 -127
- package/dist/__tests__/compiler.test.js.map +0 -1
- package/dist/__tests__/config.test.d.ts +0 -2
- package/dist/__tests__/config.test.d.ts.map +0 -1
- package/dist/__tests__/config.test.js +0 -79
- package/dist/__tests__/config.test.js.map +0 -1
- package/dist/__tests__/decompile.test.d.ts +0 -2
- package/dist/__tests__/decompile.test.d.ts.map +0 -1
- package/dist/__tests__/decompile.test.js +0 -102
- package/dist/__tests__/decompile.test.js.map +0 -1
- package/dist/__tests__/hash.test.d.ts +0 -2
- package/dist/__tests__/hash.test.d.ts.map +0 -1
- package/dist/__tests__/hash.test.js +0 -44
- package/dist/__tests__/hash.test.js.map +0 -1
- package/dist/__tests__/integration.test.d.ts +0 -2
- package/dist/__tests__/integration.test.d.ts.map +0 -1
- package/dist/__tests__/integration.test.js +0 -277
- package/dist/__tests__/integration.test.js.map +0 -1
- package/dist/__tests__/skill-renderer.test.d.ts +0 -2
- package/dist/__tests__/skill-renderer.test.d.ts.map +0 -1
- package/dist/__tests__/skill-renderer.test.js +0 -68
- package/dist/__tests__/skill-renderer.test.js.map +0 -1
package/dist/commands/chat.js
CHANGED
|
@@ -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.
|
|
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(
|
|
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
|
-
|
|
1048
|
-
|
|
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(
|
|
1067
|
+
await delay(900);
|
|
1055
1068
|
spinner.stop("looked through local context");
|
|
1056
|
-
return {
|
|
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
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
-
|
|
1119
|
-
console.log("
|
|
1120
|
-
|
|
1121
|
-
|
|
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 =
|
|
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
|
|
1137
|
+
lines.push(`hi ${displayName}. i'm U.`);
|
|
1143
1138
|
if (projectCtx) {
|
|
1144
|
-
lines.push(`i
|
|
1139
|
+
lines.push(`i'm inside ${projectCtx.name}.`);
|
|
1145
1140
|
if (repoNeedsBootstrap(projectCtx.root)) {
|
|
1146
|
-
lines.push("
|
|
1141
|
+
lines.push("it still wants cleaner agent wiring.");
|
|
1147
1142
|
}
|
|
1148
1143
|
}
|
|
1149
1144
|
else if (recentProjects.length > 0) {
|
|
1150
|
-
lines.push(`
|
|
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(`
|
|
1148
|
+
lines.push(`strongest opening: ${topOpportunity.summary}`);
|
|
1154
1149
|
}
|
|
1155
1150
|
}
|
|
1156
1151
|
else {
|
|
1157
|
-
lines.push("clean slate. we can
|
|
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(`
|
|
1174
|
-
lines.push("say \"start there\" and i'll take it
|
|
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
|
|
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
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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) {
|