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.
- package/dist/__tests__/api.test.js +3 -0
- package/dist/__tests__/api.test.js.map +1 -1
- package/dist/__tests__/integration.test.js +15 -8
- package/dist/__tests__/integration.test.js.map +1 -1
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +361 -87
- package/dist/commands/chat.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/ascii.d.ts +1 -0
- package/dist/lib/ascii.d.ts.map +1 -1
- package/dist/lib/ascii.js +72 -1
- package/dist/lib/ascii.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/package.json +1 -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.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(
|
|
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(
|
|
1067
|
+
await delay(900);
|
|
990
1068
|
spinner.stop("looked through local context");
|
|
991
|
-
return {
|
|
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
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
1054
|
-
console.log("
|
|
1055
|
-
|
|
1056
|
-
|
|
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 =
|
|
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
|
|
1137
|
+
lines.push(`hi ${displayName}. i'm U.`);
|
|
1078
1138
|
if (projectCtx) {
|
|
1079
|
-
lines.push(`i
|
|
1139
|
+
lines.push(`i'm inside ${projectCtx.name}.`);
|
|
1080
1140
|
if (repoNeedsBootstrap(projectCtx.root)) {
|
|
1081
|
-
lines.push("
|
|
1141
|
+
lines.push("it still wants cleaner agent wiring.");
|
|
1082
1142
|
}
|
|
1083
1143
|
}
|
|
1084
1144
|
else if (recentProjects.length > 0) {
|
|
1085
|
-
lines.push(`
|
|
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(`
|
|
1148
|
+
lines.push(`strongest opening: ${topOpportunity.summary}`);
|
|
1089
1149
|
}
|
|
1090
1150
|
}
|
|
1091
1151
|
else {
|
|
1092
|
-
lines.push("clean slate. we can
|
|
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
|
-
|
|
1095
|
-
lines.push(
|
|
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
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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) {
|