youmd 0.6.19 → 0.6.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/dist/commands/build.d.ts.map +1 -1
  2. package/dist/commands/build.js +0 -1
  3. package/dist/commands/build.js.map +1 -1
  4. package/dist/commands/chat.d.ts.map +1 -1
  5. package/dist/commands/chat.js +604 -199
  6. package/dist/commands/chat.js.map +1 -1
  7. package/dist/commands/init.d.ts.map +1 -1
  8. package/dist/commands/init.js +21 -1
  9. package/dist/commands/init.js.map +1 -1
  10. package/dist/commands/keys.d.ts.map +1 -1
  11. package/dist/commands/keys.js +3 -3
  12. package/dist/commands/keys.js.map +1 -1
  13. package/dist/commands/link.d.ts.map +1 -1
  14. package/dist/commands/link.js +3 -3
  15. package/dist/commands/link.js.map +1 -1
  16. package/dist/commands/login.d.ts +29 -0
  17. package/dist/commands/login.d.ts.map +1 -1
  18. package/dist/commands/login.js +257 -128
  19. package/dist/commands/login.js.map +1 -1
  20. package/dist/commands/logs.js +0 -1
  21. package/dist/commands/logs.js.map +1 -1
  22. package/dist/commands/mcp.d.ts +14 -0
  23. package/dist/commands/mcp.d.ts.map +1 -1
  24. package/dist/commands/mcp.js +284 -28
  25. package/dist/commands/mcp.js.map +1 -1
  26. package/dist/commands/private.d.ts.map +1 -1
  27. package/dist/commands/private.js +6 -5
  28. package/dist/commands/private.js.map +1 -1
  29. package/dist/commands/project.d.ts.map +1 -1
  30. package/dist/commands/project.js +9 -5
  31. package/dist/commands/project.js.map +1 -1
  32. package/dist/commands/publish.d.ts.map +1 -1
  33. package/dist/commands/publish.js +0 -1
  34. package/dist/commands/publish.js.map +1 -1
  35. package/dist/commands/pull.d.ts +24 -1
  36. package/dist/commands/pull.d.ts.map +1 -1
  37. package/dist/commands/pull.js +114 -14
  38. package/dist/commands/pull.js.map +1 -1
  39. package/dist/commands/push.d.ts +1 -0
  40. package/dist/commands/push.d.ts.map +1 -1
  41. package/dist/commands/push.js +33 -13
  42. package/dist/commands/push.js.map +1 -1
  43. package/dist/commands/register.d.ts +6 -0
  44. package/dist/commands/register.d.ts.map +1 -1
  45. package/dist/commands/register.js +179 -79
  46. package/dist/commands/register.js.map +1 -1
  47. package/dist/commands/skill.d.ts.map +1 -1
  48. package/dist/commands/skill.js +123 -52
  49. package/dist/commands/skill.js.map +1 -1
  50. package/dist/commands/stack.d.ts +16 -0
  51. package/dist/commands/stack.d.ts.map +1 -0
  52. package/dist/commands/stack.js +670 -0
  53. package/dist/commands/stack.js.map +1 -0
  54. package/dist/commands/status.d.ts.map +1 -1
  55. package/dist/commands/status.js +25 -5
  56. package/dist/commands/status.js.map +1 -1
  57. package/dist/commands/sync.d.ts +23 -0
  58. package/dist/commands/sync.d.ts.map +1 -1
  59. package/dist/commands/sync.js +193 -12
  60. package/dist/commands/sync.js.map +1 -1
  61. package/dist/commands/whoami.d.ts.map +1 -1
  62. package/dist/commands/whoami.js +21 -16
  63. package/dist/commands/whoami.js.map +1 -1
  64. package/dist/create.js +0 -0
  65. package/dist/index.js +128 -7
  66. package/dist/index.js.map +1 -1
  67. package/dist/lib/api.d.ts +94 -5
  68. package/dist/lib/api.d.ts.map +1 -1
  69. package/dist/lib/api.js +122 -5
  70. package/dist/lib/api.js.map +1 -1
  71. package/dist/lib/ascii.d.ts +5 -0
  72. package/dist/lib/ascii.d.ts.map +1 -1
  73. package/dist/lib/ascii.js +21 -30
  74. package/dist/lib/ascii.js.map +1 -1
  75. package/dist/lib/canonical-json.d.ts +8 -0
  76. package/dist/lib/canonical-json.d.ts.map +1 -0
  77. package/dist/lib/canonical-json.js +29 -0
  78. package/dist/lib/canonical-json.js.map +1 -0
  79. package/dist/lib/capability-router.d.ts +58 -0
  80. package/dist/lib/capability-router.d.ts.map +1 -0
  81. package/dist/lib/capability-router.js +54 -0
  82. package/dist/lib/capability-router.js.map +1 -0
  83. package/dist/lib/compiler.d.ts +32 -0
  84. package/dist/lib/compiler.d.ts.map +1 -1
  85. package/dist/lib/compiler.js +150 -0
  86. package/dist/lib/compiler.js.map +1 -1
  87. package/dist/lib/config.d.ts +71 -35
  88. package/dist/lib/config.d.ts.map +1 -1
  89. package/dist/lib/config.js +334 -124
  90. package/dist/lib/config.js.map +1 -1
  91. package/dist/lib/decompile.d.ts +6 -1
  92. package/dist/lib/decompile.d.ts.map +1 -1
  93. package/dist/lib/decompile.js +120 -11
  94. package/dist/lib/decompile.js.map +1 -1
  95. package/dist/lib/first-run.d.ts +14 -0
  96. package/dist/lib/first-run.d.ts.map +1 -0
  97. package/dist/lib/first-run.js +97 -0
  98. package/dist/lib/first-run.js.map +1 -0
  99. package/dist/lib/hash.d.ts.map +1 -1
  100. package/dist/lib/hash.js +2 -19
  101. package/dist/lib/hash.js.map +1 -1
  102. package/dist/lib/heartbeat.d.ts +38 -0
  103. package/dist/lib/heartbeat.d.ts.map +1 -0
  104. package/dist/lib/heartbeat.js +70 -0
  105. package/dist/lib/heartbeat.js.map +1 -0
  106. package/dist/lib/host-link.d.ts +123 -0
  107. package/dist/lib/host-link.d.ts.map +1 -0
  108. package/dist/lib/host-link.js +350 -0
  109. package/dist/lib/host-link.js.map +1 -0
  110. package/dist/lib/merge.d.ts +50 -0
  111. package/dist/lib/merge.d.ts.map +1 -0
  112. package/dist/lib/merge.js +167 -0
  113. package/dist/lib/merge.js.map +1 -0
  114. package/dist/lib/onboarding.d.ts +17 -0
  115. package/dist/lib/onboarding.d.ts.map +1 -1
  116. package/dist/lib/onboarding.js +225 -208
  117. package/dist/lib/onboarding.js.map +1 -1
  118. package/dist/lib/project.d.ts +5 -0
  119. package/dist/lib/project.d.ts.map +1 -1
  120. package/dist/lib/project.js +8 -0
  121. package/dist/lib/project.js.map +1 -1
  122. package/dist/lib/projectContext.d.ts +201 -0
  123. package/dist/lib/projectContext.d.ts.map +1 -0
  124. package/dist/lib/projectContext.js +597 -0
  125. package/dist/lib/projectContext.js.map +1 -0
  126. package/dist/lib/render.d.ts +38 -1
  127. package/dist/lib/render.d.ts.map +1 -1
  128. package/dist/lib/render.js +83 -18
  129. package/dist/lib/render.js.map +1 -1
  130. package/dist/lib/skill-catalog.d.ts.map +1 -1
  131. package/dist/lib/skill-catalog.js +22 -2
  132. package/dist/lib/skill-catalog.js.map +1 -1
  133. package/dist/lib/skills.d.ts +20 -5
  134. package/dist/lib/skills.d.ts.map +1 -1
  135. package/dist/lib/skills.js +116 -58
  136. package/dist/lib/skills.js.map +1 -1
  137. package/dist/lib/stackEval.d.ts +87 -0
  138. package/dist/lib/stackEval.d.ts.map +1 -0
  139. package/dist/lib/stackEval.js +233 -0
  140. package/dist/lib/stackEval.js.map +1 -0
  141. package/dist/lib/stackImprove.d.ts +49 -0
  142. package/dist/lib/stackImprove.d.ts.map +1 -0
  143. package/dist/lib/stackImprove.js +313 -0
  144. package/dist/lib/stackImprove.js.map +1 -0
  145. package/dist/lib/stackInstall.d.ts +59 -0
  146. package/dist/lib/stackInstall.d.ts.map +1 -0
  147. package/dist/lib/stackInstall.js +162 -0
  148. package/dist/lib/stackInstall.js.map +1 -0
  149. package/dist/lib/stackSafety.d.ts +126 -0
  150. package/dist/lib/stackSafety.d.ts.map +1 -0
  151. package/dist/lib/stackSafety.js +486 -0
  152. package/dist/lib/stackSafety.js.map +1 -0
  153. package/dist/lib/stackUpdate.d.ts +44 -0
  154. package/dist/lib/stackUpdate.d.ts.map +1 -0
  155. package/dist/lib/stackUpdate.js +256 -0
  156. package/dist/lib/stackUpdate.js.map +1 -0
  157. package/dist/lib/stream.d.ts +116 -0
  158. package/dist/lib/stream.d.ts.map +1 -0
  159. package/dist/lib/stream.js +368 -0
  160. package/dist/lib/stream.js.map +1 -0
  161. package/dist/lib/youstack.d.ts +219 -0
  162. package/dist/lib/youstack.d.ts.map +1 -0
  163. package/dist/lib/youstack.js +1049 -0
  164. package/dist/lib/youstack.js.map +1 -0
  165. package/dist/mcp/registry.d.ts +133 -0
  166. package/dist/mcp/registry.d.ts.map +1 -0
  167. package/dist/mcp/registry.js +833 -0
  168. package/dist/mcp/registry.js.map +1 -0
  169. package/dist/mcp/server.d.ts +106 -5
  170. package/dist/mcp/server.d.ts.map +1 -1
  171. package/dist/mcp/server.js +851 -498
  172. package/dist/mcp/server.js.map +1 -1
  173. package/dist/postinstall.js +11 -2
  174. package/dist/postinstall.js.map +1 -1
  175. package/dist/you.d.ts +1 -0
  176. package/dist/you.js +41 -5
  177. package/dist/you.js.map +1 -1
  178. package/examples/youstack-bamfstack-public/docs/quickstart.md +34 -0
  179. package/examples/youstack-bamfstack-public/prompts/creator-draft-starter.md +15 -0
  180. package/examples/youstack-bamfstack-public/skills/bamfstack-start/SKILL.md +36 -0
  181. package/examples/youstack-bamfstack-public/tests/smoke.md +19 -0
  182. package/examples/youstack-bamfstack-public/workflows/creator-growth-workflow.md +22 -0
  183. package/examples/youstack-bamfstack-public/youstack.json +172 -0
  184. package/examples/youstack-personal/docs/quickstart.md +10 -0
  185. package/examples/youstack-personal/skills/youstack-start/SKILL.md +9 -0
  186. package/examples/youstack-personal/tests/smoke.md +9 -0
  187. package/examples/youstack-personal/workflows/startup.md +7 -0
  188. package/examples/youstack-personal/youstack.json +123 -0
  189. package/package.json +3 -3
  190. package/skills/claude-md-generator.md +1 -1
  191. package/skills/meta-improve.md +8 -3
  192. package/skills/proactive-context-fill.md +1 -1
  193. package/skills/youstack-maintainer.md +128 -0
  194. package/skills/youstack-start.md +78 -0
  195. package/dist/__tests__/api.test.d.ts +0 -2
  196. package/dist/__tests__/api.test.d.ts.map +0 -1
  197. package/dist/__tests__/api.test.js +0 -87
  198. package/dist/__tests__/api.test.js.map +0 -1
  199. package/dist/__tests__/compiler.test.d.ts +0 -2
  200. package/dist/__tests__/compiler.test.d.ts.map +0 -1
  201. package/dist/__tests__/compiler.test.js +0 -127
  202. package/dist/__tests__/compiler.test.js.map +0 -1
  203. package/dist/__tests__/config.test.d.ts +0 -2
  204. package/dist/__tests__/config.test.d.ts.map +0 -1
  205. package/dist/__tests__/config.test.js +0 -79
  206. package/dist/__tests__/config.test.js.map +0 -1
  207. package/dist/__tests__/decompile.test.d.ts +0 -2
  208. package/dist/__tests__/decompile.test.d.ts.map +0 -1
  209. package/dist/__tests__/decompile.test.js +0 -102
  210. package/dist/__tests__/decompile.test.js.map +0 -1
  211. package/dist/__tests__/hash.test.d.ts +0 -2
  212. package/dist/__tests__/hash.test.d.ts.map +0 -1
  213. package/dist/__tests__/hash.test.js +0 -44
  214. package/dist/__tests__/hash.test.js.map +0 -1
  215. package/dist/__tests__/integration.test.d.ts +0 -2
  216. package/dist/__tests__/integration.test.d.ts.map +0 -1
  217. package/dist/__tests__/integration.test.js +0 -284
  218. package/dist/__tests__/integration.test.js.map +0 -1
  219. package/dist/__tests__/skill-renderer.test.d.ts +0 -2
  220. package/dist/__tests__/skill-renderer.test.d.ts.map +0 -1
  221. package/dist/__tests__/skill-renderer.test.js +0 -68
  222. package/dist/__tests__/skill-renderer.test.js.map +0 -1
@@ -44,10 +44,12 @@ const path = __importStar(require("path"));
44
44
  const chalk_1 = __importDefault(require("chalk"));
45
45
  const config_1 = require("../lib/config");
46
46
  const project_1 = require("../lib/project");
47
+ const projectContext_1 = require("../lib/projectContext");
47
48
  const compiler_1 = require("../lib/compiler");
48
49
  const api_1 = require("../lib/api");
49
50
  const skills_1 = require("../lib/skills");
50
51
  const render_1 = require("../lib/render");
52
+ const stream_1 = require("../lib/stream");
51
53
  const onboarding_1 = require("../lib/onboarding");
52
54
  const ascii_1 = require("../lib/ascii");
53
55
  const update_1 = require("../lib/update");
@@ -55,122 +57,25 @@ const update_1 = require("../lib/update");
55
57
  const config_2 = require("../lib/config");
56
58
  const CONVEX_SITE_URL = (0, config_2.getConvexSiteUrl)();
57
59
  const STREAM_URL = `${CONVEX_SITE_URL}/api/v1/chat/stream`;
58
- const CURRENT_VERSION = "0.6.19";
59
- function delay(ms) {
60
- return new Promise((resolve) => setTimeout(resolve, ms));
61
- }
60
+ const CURRENT_VERSION = "0.6.23";
62
61
  // ─── Streaming LLM client ─────────────────────────────────────────────
63
- async function streamLLM(_apiKey, messages, onToken) {
64
- const res = await fetch(STREAM_URL, {
65
- method: "POST",
66
- headers: { "Content-Type": "application/json" },
67
- body: JSON.stringify({ messages }),
68
- signal: AbortSignal.timeout(120000),
69
- });
70
- if (!res.ok) {
71
- const body = await res.text();
72
- throw new Error(`Stream error (${res.status}): ${body}`);
73
- }
74
- if (!res.body) {
75
- throw new Error("No response body from stream endpoint");
76
- }
77
- const reader = res.body.getReader();
78
- const decoder = new TextDecoder();
79
- let fullText = "";
80
- let buffer = "";
81
- while (true) {
82
- const { done, value } = await reader.read();
83
- if (done)
84
- break;
85
- buffer += decoder.decode(value, { stream: true });
86
- // Process complete SSE lines
87
- const lines = buffer.split("\n");
88
- // Keep the last potentially incomplete line in the buffer
89
- buffer = lines.pop() || "";
90
- for (const line of lines) {
91
- const trimmed = line.trim();
92
- if (!trimmed)
93
- continue;
94
- if (trimmed.startsWith("data: ")) {
95
- const data = trimmed.slice(6);
96
- if (data === "[DONE]") {
97
- continue;
98
- }
99
- try {
100
- const parsed = JSON.parse(data);
101
- if (parsed.text) {
102
- fullText += parsed.text;
103
- onToken(parsed.text);
104
- }
105
- }
106
- catch {
107
- // Skip malformed JSON chunks
108
- }
109
- }
110
- }
111
- }
112
- // Process any remaining buffer
113
- if (buffer.trim()) {
114
- const trimmed = buffer.trim();
115
- if (trimmed.startsWith("data: ")) {
116
- const data = trimmed.slice(6);
117
- if (data !== "[DONE]") {
118
- try {
119
- const parsed = JSON.parse(data);
120
- if (parsed.text) {
121
- fullText += parsed.text;
122
- onToken(parsed.text);
123
- }
124
- }
125
- catch {
126
- // Skip
127
- }
128
- }
129
- }
130
- }
131
- return fullText;
132
- }
133
62
  /**
134
- * Call LLM with streaming, falling back to blocking callLLM on failure.
135
- * Returns the full response text.
63
+ * Call LLM with streaming (shared helper: spinner until first token, then
64
+ * live tokens with ```json directive blocks filtered from the display),
65
+ * falling back to blocking callLLM on failure.
66
+ *
67
+ * Returns the full RAW response text — directive blocks intact — so the
68
+ * post-stream parsers (parseUpdatesFromResponse, parseMemorySaves,
69
+ * parsePrivateUpdates, parseProjectUpdates) keep working unchanged.
136
70
  */
137
71
  async function callLLMWithStreaming(apiKey, messages, spinnerLabel) {
138
- const thinkSpinner = new render_1.BrailleSpinner(spinnerLabel);
139
- thinkSpinner.start();
140
- try {
141
- let firstToken = true;
142
- const response = await streamLLM(apiKey, messages, (token) => {
143
- if (firstToken) {
144
- // Clear the spinner line before writing streamed text
145
- thinkSpinner.stop();
146
- process.stdout.write(" ");
147
- firstToken = false;
148
- }
149
- process.stdout.write(token);
150
- });
151
- if (!firstToken) {
152
- // We streamed something -- add trailing newline
153
- process.stdout.write("\n");
154
- }
155
- else {
156
- // No tokens received -- clear spinner
157
- thinkSpinner.stop();
158
- }
159
- return { text: response, streamed: !firstToken };
160
- }
161
- catch {
162
- // Streaming failed -- fall back to blocking call
163
- thinkSpinner.update("streaming unavailable, waiting for response");
164
- try {
165
- const response = await (0, onboarding_1.callLLM)(apiKey, messages);
166
- thinkSpinner.stop();
167
- return { text: response, streamed: false };
168
- }
169
- catch (err) {
170
- thinkSpinner.fail(err instanceof Error ? err.message : "failed");
171
- throw err;
172
- }
173
- }
72
+ return (0, stream_1.streamAssistantTurn)({
73
+ streamUrl: STREAM_URL,
74
+ messages,
75
+ spinnerLabel,
76
+ timeoutMs: 120000,
77
+ fallback: () => (0, onboarding_1.callLLM)(apiKey, messages),
78
+ });
174
79
  }
175
80
  function detectSourcesInMessage(text) {
176
81
  const sources = [];
@@ -210,7 +115,7 @@ function detectSourcesInMessage(text) {
210
115
  }
211
116
  const bareDomainRegex = /(?<![/\w])([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.(?:com|co|io|ai|dev|org|net|app|xyz|me)(?:\/[^\s<>"']*)?)/gi;
212
117
  while ((match = bareDomainRegex.exec(text)) !== null) {
213
- let domain = match[1].replace(/[.,;:)\]]+$/, "");
118
+ const domain = match[1].replace(/[.,;:)\]]+$/, "");
214
119
  if (domain.includes("x.com") || domain.includes("github.com") || domain.includes("linkedin.com"))
215
120
  continue;
216
121
  const url = `https://${domain}`;
@@ -541,7 +446,7 @@ function loadCurrentBundle(bundleDir) {
541
446
  { dir: "profile", label: "Profile" },
542
447
  { dir: "preferences", label: "Preferences" },
543
448
  ];
544
- for (const { dir, label } of dirs) {
449
+ for (const { dir } of dirs) {
545
450
  const dirPath = path.join(bundleDir, dir);
546
451
  if (!fs.existsSync(dirPath))
547
452
  continue;
@@ -605,8 +510,8 @@ function showStatus(bundleDir) {
605
510
  console.log(chalk_1.default.dim(" authenticated: " + ((0, config_1.isAuthenticated)() ? "yes" : "no")));
606
511
  console.log("");
607
512
  }
608
- function showLinkInfo(bundleDir) {
609
- // Try to read username from the about.md or config
513
+ function showLinkInfo() {
514
+ // Try to read username from the config
610
515
  const config = (0, config_1.readGlobalConfig)();
611
516
  const username = config.username || "your-username";
612
517
  console.log("");
@@ -881,9 +786,6 @@ function readDisplayName(bundleDir) {
881
786
  }
882
787
  return (0, config_1.readGlobalConfig)().username || "friend";
883
788
  }
884
- function getRecentProjectNames(limit = 3) {
885
- return (0, project_1.getRecentProjectInsights)(process.cwd(), limit).map((item) => item.name);
886
- }
887
789
  async function printUpdateHint() {
888
790
  const latest = await (0, update_1.checkForCliUpdate)(CURRENT_VERSION);
889
791
  if (!latest)
@@ -969,6 +871,34 @@ function collectHomeAgentSignals() {
969
871
  }
970
872
  return findings;
971
873
  }
874
+ function scoreProjectActionItem(item) {
875
+ const lower = item.toLowerCase();
876
+ let score = 0;
877
+ if (lower.includes("trusted publishing") || (lower.includes("publish") && lower.includes("blocked")))
878
+ score += 80;
879
+ if (lower.includes("configure") || lower.includes("unblock") || lower.includes("blocker"))
880
+ score += 40;
881
+ if (lower.includes("publish") || lower.includes("npm"))
882
+ score += 25;
883
+ if (lower.includes("verify") || lower.includes("test") || lower.includes("qa"))
884
+ score += 15;
885
+ if (lower.includes("build") || lower.includes("proactive") || lower.includes("first-run"))
886
+ score += 10;
887
+ return score;
888
+ }
889
+ function rankProjectActionItems(items) {
890
+ return items
891
+ .map((item, index) => ({ item, index, score: scoreProjectActionItem(item) }))
892
+ .sort((a, b) => b.score - a.score || a.index - b.index)
893
+ .map((entry) => entry.item);
894
+ }
895
+ function shortenActionItem(item, maxChars = 240) {
896
+ if (item.length <= maxChars)
897
+ return item;
898
+ const boundary = item.lastIndexOf(" ", maxChars - 1);
899
+ const end = boundary > 90 ? boundary : maxChars - 1;
900
+ return `${item.slice(0, end).trim()}...`;
901
+ }
972
902
  async function runYouLaunchInvestigation(bundleDir, projectCtx, recentProjects) {
973
903
  const labels = [
974
904
  "sipping bitbucks frappaccino while i look around",
@@ -988,7 +918,6 @@ async function runYouLaunchInvestigation(bundleDir, projectCtx, recentProjects)
988
918
  }, 1100);
989
919
  spinner.start();
990
920
  try {
991
- await delay(600);
992
921
  try {
993
922
  const hasPreferences = fs.existsSync(path.join(bundleDir, "preferences", "agent.md"));
994
923
  const hasDirectives = fs.existsSync(path.join(bundleDir, "directives", "agent.md"));
@@ -1002,8 +931,6 @@ async function runYouLaunchInvestigation(bundleDir, projectCtx, recentProjects)
1002
931
  catch {
1003
932
  // keep scanning other surfaces
1004
933
  }
1005
- const homeSignals = collectHomeAgentSignals();
1006
- findings.push(...homeSignals.slice(0, 3));
1007
934
  if (projectCtx) {
1008
935
  try {
1009
936
  const hasAgents = fs.existsSync(path.join(projectCtx.root, "AGENTS.md"));
@@ -1025,18 +952,30 @@ async function runYouLaunchInvestigation(bundleDir, projectCtx, recentProjects)
1025
952
  else {
1026
953
  findings.push(`${projectCtx.name} still wants a real project-context spine.`);
1027
954
  }
955
+ const actionItems = extractProjectActionItems(projectCtx.root);
956
+ strongestProject = {
957
+ name: projectCtx.name,
958
+ slug: projectCtx.name,
959
+ projectDir: projectCtx.root,
960
+ updatedAt: Date.now(),
961
+ signals: (0, project_1.getProjectMarkerSignals)(projectCtx.root),
962
+ summary: `${projectCtx.name} is the current working repo.`,
963
+ suggestedCommand: `cd ${projectCtx.root} && you`,
964
+ };
1028
965
  if (repoNeedsBootstrap(projectCtx.root)) {
1029
966
  strongestMove = `${projectCtx.name} still wants cleaner agent wiring and project-context scaffolding.`;
1030
967
  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
- };
968
+ strongestProject.signals = ["still wants cleaner agent wiring and project-context scaffolding"];
969
+ strongestProject.summary = `${projectCtx.name} still wants cleaner agent wiring and project-context scaffolding.`;
970
+ strongestProject.suggestedCommand = "youmd skill init-project";
971
+ }
972
+ else if (actionItems.length > 0) {
973
+ strongestMove = actionItems[0];
974
+ strongestProject.signals = actionItems.slice(0, 3);
975
+ strongestProject.summary = actionItems[0];
976
+ }
977
+ else {
978
+ strongestMove = `read ${projectCtx.name}'s project-context and pick the next concrete release-quality fix from actual repo state.`;
1040
979
  }
1041
980
  }
1042
981
  catch {
@@ -1064,7 +1003,8 @@ async function runYouLaunchInvestigation(bundleDir, projectCtx, recentProjects)
1064
1003
  else {
1065
1004
  findings.push("i've got your home bundle loaded, even though we're not inside a project yet.");
1066
1005
  }
1067
- await delay(900);
1006
+ const homeSignals = collectHomeAgentSignals();
1007
+ findings.push(...homeSignals.slice(0, 2));
1068
1008
  spinner.stop("looked through local context");
1069
1009
  return {
1070
1010
  findings: findings.slice(0, 3),
@@ -1155,6 +1095,9 @@ function buildYouLaunchIntro(projectCtx, bundleDir, investigation) {
1155
1095
  || (projectCtx && repoNeedsBootstrap(projectCtx.root)
1156
1096
  ? `${projectCtx.name} still wants cleaner agent wiring and project-context scaffolding.`
1157
1097
  : null)
1098
+ || (projectCtx
1099
+ ? `read ${projectCtx.name}'s project-context and pick the next concrete release-quality fix from actual repo state.`
1100
+ : null)
1158
1101
  || (0, project_1.getTopProjectOpportunity)(recentInsights)?.summary
1159
1102
  || null;
1160
1103
  if (strongestMove) {
@@ -1181,6 +1124,19 @@ function isStartThereIntent(input) {
1181
1124
  "start",
1182
1125
  ].includes(lower);
1183
1126
  }
1127
+ function isContinueNextMoveIntent(input) {
1128
+ const lower = input.toLowerCase().trim().replace(/[.!?]+$/, "");
1129
+ return [
1130
+ "continue",
1131
+ "keep going",
1132
+ "more",
1133
+ "next",
1134
+ "what next",
1135
+ "what should i do next",
1136
+ "next strongest move",
1137
+ "do the next strongest move",
1138
+ ].includes(lower);
1139
+ }
1184
1140
  function isLocalRecentProjectsIntent(input) {
1185
1141
  const lower = input.toLowerCase();
1186
1142
  const mentionsLocalWork = lower.includes("local director") ||
@@ -1330,8 +1286,441 @@ function formatProjectBootstrapToolResult(project, result) {
1330
1286
  `recommended_next_move: read ${project.name}'s project-context and turn the rough docs into a sharper current-state + TODO pass.`,
1331
1287
  ].join("\n");
1332
1288
  }
1289
+ function isLocalToolLoopCandidate(input) {
1290
+ if (isStartThereIntent(input) || isContinueNextMoveIntent(input) || isLocalRecentProjectsIntent(input))
1291
+ return true;
1292
+ const lower = input.toLowerCase();
1293
+ return [
1294
+ "local",
1295
+ "workspace",
1296
+ "workspaces",
1297
+ "filesystem",
1298
+ "file system",
1299
+ "recent projects",
1300
+ "recent work",
1301
+ "working on lately",
1302
+ "project-context",
1303
+ "project context",
1304
+ "agent entrypoint",
1305
+ "agent instructions",
1306
+ "scaffold",
1307
+ "bootstrap",
1308
+ "start there",
1309
+ "sync identity",
1310
+ "sync my identity",
1311
+ "publish identity",
1312
+ "publish my identity",
1313
+ "push my identity",
1314
+ "what next",
1315
+ "next strongest move",
1316
+ ].some((phrase) => lower.includes(phrase));
1317
+ }
1318
+ function userAskedForMutation(input) {
1319
+ const lower = input.toLowerCase();
1320
+ return [
1321
+ "start there",
1322
+ "do it",
1323
+ "go ahead",
1324
+ "scaffold",
1325
+ "bootstrap",
1326
+ "create",
1327
+ "write",
1328
+ "update",
1329
+ "fix",
1330
+ "tighten",
1331
+ "sync",
1332
+ "publish",
1333
+ "push",
1334
+ "upload",
1335
+ ].some((phrase) => lower.includes(phrase));
1336
+ }
1337
+ function safeJsonObject(text) {
1338
+ const trimmed = text.trim();
1339
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
1340
+ const candidate = fenced?.[1]?.trim() || trimmed.match(/\{[\s\S]*\}/)?.[0];
1341
+ if (!candidate)
1342
+ return null;
1343
+ try {
1344
+ const parsed = JSON.parse(candidate);
1345
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
1346
+ ? parsed
1347
+ : null;
1348
+ }
1349
+ catch {
1350
+ return null;
1351
+ }
1352
+ }
1353
+ function normalizeLocalHostToolCall(value) {
1354
+ if (!value)
1355
+ return null;
1356
+ const tool = value.tool;
1357
+ if (tool !== "discover_projects" &&
1358
+ tool !== "read_project_context" &&
1359
+ tool !== "write_project_context" &&
1360
+ tool !== "sync_identity" &&
1361
+ tool !== "respond") {
1362
+ return null;
1363
+ }
1364
+ const mode = value.mode === "publish" || value.mode === "bootstrap" || value.mode === "status"
1365
+ ? value.mode
1366
+ : undefined;
1367
+ return {
1368
+ tool,
1369
+ project: typeof value.project === "string" ? value.project : undefined,
1370
+ mode,
1371
+ reason: typeof value.reason === "string" ? value.reason : undefined,
1372
+ };
1373
+ }
1374
+ function inferLocalHostToolCall(input, launchInvestigation) {
1375
+ const lower = input.toLowerCase();
1376
+ if (isStartThereIntent(input) || isContinueNextMoveIntent(input)) {
1377
+ const project = resolveProjectForTool(launchInvestigation.strongestProject?.name, launchInvestigation);
1378
+ if (project && repoNeedsBootstrap(project.projectDir)) {
1379
+ return { tool: "write_project_context", project: project.name, mode: "bootstrap" };
1380
+ }
1381
+ return { tool: "read_project_context", project: project?.name || launchInvestigation.strongestProject?.name };
1382
+ }
1383
+ if (lower.includes("sync") || lower.includes("publish") || lower.includes("push my identity")) {
1384
+ return { tool: "sync_identity", mode: lower.includes("publish") || lower.includes("push") || lower.includes("sync") ? "publish" : "status" };
1385
+ }
1386
+ if (lower.includes("read") || lower.includes("show") || lower.includes("inspect") || lower.includes("context")) {
1387
+ return { tool: "read_project_context", project: launchInvestigation.strongestProject?.name };
1388
+ }
1389
+ if (lower.includes("scaffold") || lower.includes("bootstrap") || lower.includes("create") || lower.includes("write") || lower.includes("update")) {
1390
+ return { tool: "write_project_context", project: launchInvestigation.strongestProject?.name, mode: "bootstrap" };
1391
+ }
1392
+ return { tool: "discover_projects" };
1393
+ }
1394
+ function collectKnownProjects(launchInvestigation) {
1395
+ const projects = [
1396
+ launchInvestigation.strongestProject,
1397
+ ...scanRecentWorkspaceProjects(12),
1398
+ ...(0, project_1.getRecentProjectInsights)(process.cwd(), 12),
1399
+ ].filter((item) => !!item);
1400
+ const seen = new Set();
1401
+ return projects.filter((project) => {
1402
+ const key = fs.existsSync(project.projectDir)
1403
+ ? fs.realpathSync.native(project.projectDir)
1404
+ : path.resolve(project.projectDir);
1405
+ if (seen.has(key))
1406
+ return false;
1407
+ seen.add(key);
1408
+ return true;
1409
+ });
1410
+ }
1411
+ function resolveProjectForTool(projectRef, launchInvestigation) {
1412
+ const projects = collectKnownProjects(launchInvestigation);
1413
+ if (!projectRef && launchInvestigation.strongestProject)
1414
+ return launchInvestigation.strongestProject;
1415
+ if (!projectRef)
1416
+ return projects[0] || null;
1417
+ const normalized = projectRef.toLowerCase().trim();
1418
+ const byPath = projects.find((project) => path.resolve(project.projectDir) === path.resolve(projectRef));
1419
+ if (byPath)
1420
+ return byPath;
1421
+ return projects.find((project) => project.name.toLowerCase() === normalized ||
1422
+ project.slug.toLowerCase() === normalized ||
1423
+ project.projectDir.toLowerCase().includes(normalized)) || null;
1424
+ }
1425
+ function readSnippet(filePath, maxChars = 1800) {
1426
+ if (!fs.existsSync(filePath))
1427
+ return null;
1428
+ try {
1429
+ const content = fs.readFileSync(filePath, "utf-8").trim();
1430
+ if (!content)
1431
+ return null;
1432
+ return content.length > maxChars ? `${content.slice(0, maxChars)}\n...` : content;
1433
+ }
1434
+ catch {
1435
+ return null;
1436
+ }
1437
+ }
1438
+ function extractProjectActionItems(projectDir, maxItems = 5) {
1439
+ // Read paths come from the single project-context engine: repo
1440
+ // project-context/, the generated .you/ layer, then the global overlay.
1441
+ const files = (0, projectContext_1.projectContextReadPaths)(projectDir, [
1442
+ "TODO.md",
1443
+ "CURRENT_STATE.md",
1444
+ "feature-requests-active.md",
1445
+ ]);
1446
+ const seen = new Set();
1447
+ const items = [];
1448
+ for (const { path: fullPath } of files) {
1449
+ const content = readSnippet(fullPath, 12000);
1450
+ if (!content)
1451
+ continue;
1452
+ for (const rawLine of content.split("\n")) {
1453
+ const line = rawLine.trim();
1454
+ if (!line)
1455
+ continue;
1456
+ const openTask = line.match(/^[-*]\s+\[[\s?~-]\]\s+(.+)$/);
1457
+ const statusTask = line.match(/^[-*]\s+(?:TODO|P0|P1|BLOCKED|OPEN|NEXT|IN PROGRESS|ACTIVE)[:\s-]+(.+)$/i);
1458
+ const headingTask = line.match(/^#{2,4}\s+(?:P0|P1|Next|Open|Active|Blocked|Known issue|Current focus)[:\s-]+(.+)$/i);
1459
+ const candidate = (openTask?.[1] || statusTask?.[1] || headingTask?.[1] || "")
1460
+ .replace(/\s+/g, " ")
1461
+ .replace(/\s+#.*$/, "")
1462
+ .trim();
1463
+ if (!candidate || candidate.length < 8)
1464
+ continue;
1465
+ const normalized = candidate.toLowerCase();
1466
+ if (seen.has(normalized))
1467
+ continue;
1468
+ seen.add(normalized);
1469
+ items.push(shortenActionItem(candidate));
1470
+ }
1471
+ }
1472
+ return rankProjectActionItems(items).slice(0, maxItems);
1473
+ }
1474
+ function formatProjectReadToolResult(project) {
1475
+ const entrypointFiles = [
1476
+ "AGENTS.md",
1477
+ "CLAUDE.md",
1478
+ "package.json",
1479
+ "pyproject.toml",
1480
+ "Cargo.toml",
1481
+ "go.mod",
1482
+ ].map((relativePath) => ({
1483
+ label: relativePath,
1484
+ path: path.join(project.projectDir, relativePath),
1485
+ }));
1486
+ // Context reads route through the single project-context engine (repo
1487
+ // project-context/, .you/ layer, then the global overlay union).
1488
+ const files = [
1489
+ ...entrypointFiles,
1490
+ ...(0, projectContext_1.projectContextReadPaths)(project.projectDir, [
1491
+ "CURRENT_STATE.md",
1492
+ "TODO.md",
1493
+ "PRD.md",
1494
+ "ARCHITECTURE.md",
1495
+ ]),
1496
+ ];
1497
+ const snippets = files
1498
+ .map(({ label, path: fullPath }) => {
1499
+ const snippet = readSnippet(fullPath);
1500
+ return snippet ? `file: ${label}\n${snippet}` : null;
1501
+ })
1502
+ .filter((item) => !!item)
1503
+ .slice(0, 5);
1504
+ const managedContext = (0, project_1.readProjectContext)(project.projectDir);
1505
+ const actionItems = extractProjectActionItems(project.projectDir);
1506
+ const recommendedMove = actionItems.length > 0
1507
+ ? actionItems[0]
1508
+ : `tighten ${project.name}'s current-state and TODO docs from actual repo context.`;
1509
+ return [
1510
+ "tool: read_project_context",
1511
+ "status: ok",
1512
+ `project: ${project.name}`,
1513
+ `project_dir: ${project.projectDir}`,
1514
+ `markers: ${(0, project_1.getProjectMarkerSignals)(project.projectDir).join(", ") || "none"}`,
1515
+ managedContext ? `managed_project_context: ${managedContext.meta.name}` : "managed_project_context: none",
1516
+ actionItems.length > 0 ? `action_items: ${actionItems.join(" | ")}` : "action_items: none found in TODO/current-state files",
1517
+ "",
1518
+ snippets.length > 0 ? snippets.join("\n\n---\n\n") : "no readable project-context or agent entrypoint files found yet.",
1519
+ "",
1520
+ `recommended_next_move: ${recommendedMove}`,
1521
+ ].join("\n");
1522
+ }
1523
+ async function formatIdentitySyncToolResult(bundleDir, publish) {
1524
+ const result = (0, compiler_1.compileBundle)(bundleDir);
1525
+ (0, compiler_1.writeBundle)(bundleDir, result);
1526
+ const lines = [
1527
+ "tool: sync_identity",
1528
+ "status: ok",
1529
+ `bundle_dir: ${bundleDir}`,
1530
+ `compiled_version: ${result.stats.version}`,
1531
+ `sections: ${result.stats.filledSections}/${result.stats.totalSections}`,
1532
+ ];
1533
+ if (!publish) {
1534
+ lines.push("remote_sync: not requested; local bundle compiled only.");
1535
+ lines.push("recommended_next_move: publish if you want this local identity bundle pushed to you.md.");
1536
+ return lines.join("\n");
1537
+ }
1538
+ if (!(0, config_1.isAuthenticated)()) {
1539
+ return [
1540
+ ...lines,
1541
+ "remote_sync: blocked; not authenticated.",
1542
+ "recommended_next_move: run youmd login, then ask me to sync identity again.",
1543
+ ].join("\n");
1544
+ }
1545
+ const youJson = JSON.parse(fs.readFileSync(path.join(bundleDir, "you.json"), "utf-8"));
1546
+ const youMd = fs.readFileSync(path.join(bundleDir, "you.md"), "utf-8");
1547
+ const manifest = JSON.parse(fs.readFileSync(path.join(bundleDir, "manifest.json"), "utf-8"));
1548
+ const uploadRes = await (0, api_1.uploadBundle)({ manifest, youJson, youMd });
1549
+ if (!uploadRes.ok) {
1550
+ return [
1551
+ ...lines,
1552
+ `remote_sync: upload failed with status ${uploadRes.status}.`,
1553
+ "recommended_next_move: inspect auth/API state before retrying identity sync.",
1554
+ ].join("\n");
1555
+ }
1556
+ const publishRes = await (0, api_1.publishLatest)();
1557
+ if (!publishRes.ok) {
1558
+ return [
1559
+ ...lines,
1560
+ `remote_sync: publish failed with status ${publishRes.status}.`,
1561
+ "recommended_next_move: inspect publish API state before retrying identity sync.",
1562
+ ].join("\n");
1563
+ }
1564
+ lines.push(`remote_sync: published v${publishRes.data.version} as ${publishRes.data.username}.`);
1565
+ lines.push(`url: ${publishRes.data.url || `https://you.md/${publishRes.data.username}`}`);
1566
+ lines.push("recommended_next_move: test the updated identity from another agent surface.");
1567
+ return lines.join("\n");
1568
+ }
1569
+ async function chooseLocalHostTool(args) {
1570
+ if (isStartThereIntent(args.userInput) || isContinueNextMoveIntent(args.userInput)) {
1571
+ return inferLocalHostToolCall(args.userInput, args.launchInvestigation);
1572
+ }
1573
+ const projects = collectKnownProjects(args.launchInvestigation)
1574
+ .slice(0, 8)
1575
+ .map((project) => `${project.name} | ${project.projectDir} | ${project.signals.slice(0, 4).join(", ")}`)
1576
+ .join("\n");
1577
+ const prompt = [
1578
+ "You are the local host tool router for the you.md CLI. Return ONLY compact JSON. No prose.",
1579
+ "Available tools:",
1580
+ "- discover_projects: scan local filesystem markers for recent project work.",
1581
+ "- read_project_context: read AGENTS/CLAUDE/package/project-context files for a project.",
1582
+ "- 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.",
1583
+ "- sync_identity: compile identity locally; use mode publish only when the user asks to sync/publish/push identity.",
1584
+ "- respond: no local tool needed.",
1585
+ "",
1586
+ "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\"}",
1587
+ "",
1588
+ "Known projects:",
1589
+ projects || "none yet",
1590
+ "",
1591
+ `User request: ${args.userInput}`,
1592
+ ].join("\n");
1593
+ try {
1594
+ const raw = await (0, onboarding_1.callLLM)(args.apiKey, [
1595
+ { role: "system", content: "Return only valid JSON for local tool routing." },
1596
+ { role: "user", content: prompt },
1597
+ ]);
1598
+ return normalizeLocalHostToolCall(safeJsonObject(raw)) || inferLocalHostToolCall(args.userInput, args.launchInvestigation);
1599
+ }
1600
+ catch {
1601
+ return inferLocalHostToolCall(args.userInput, args.launchInvestigation);
1602
+ }
1603
+ }
1604
+ async function executeLocalHostTool(toolCall, args) {
1605
+ if (toolCall.tool === "discover_projects") {
1606
+ const insights = scanRecentWorkspaceProjects(10);
1607
+ args.launchInvestigation.strongestProject = insights[0] || args.launchInvestigation.strongestProject;
1608
+ args.launchInvestigation.strongestMove = insights[0]?.summary || args.launchInvestigation.strongestMove;
1609
+ return formatLocalRecentProjectsToolResult(insights);
1610
+ }
1611
+ if (toolCall.tool === "read_project_context") {
1612
+ const project = resolveProjectForTool(toolCall.project, args.launchInvestigation);
1613
+ if (!project) {
1614
+ return "tool: read_project_context\nstatus: blocked\nresult: no real local project target is known yet.\nrecommended_next_move: run discover_projects first.";
1615
+ }
1616
+ args.launchInvestigation.strongestProject = project;
1617
+ return formatProjectReadToolResult(project);
1618
+ }
1619
+ if (toolCall.tool === "write_project_context") {
1620
+ const project = resolveProjectForTool(toolCall.project, args.launchInvestigation);
1621
+ if (!project) {
1622
+ return "tool: write_project_context\nstatus: blocked\nresult: no real local project target is known yet.\nrecommended_next_move: run discover_projects first.";
1623
+ }
1624
+ if (!userAskedForMutation(args.userInput)) {
1625
+ return [
1626
+ "tool: write_project_context",
1627
+ "status: blocked",
1628
+ `project: ${project.name}`,
1629
+ "result: user did not explicitly ask for a filesystem mutation.",
1630
+ `recommended_next_move: say "start there" to bootstrap ${project.name}.`,
1631
+ ].join("\n");
1632
+ }
1633
+ const previousCwd = process.cwd();
1634
+ let result;
1635
+ try {
1636
+ process.chdir(project.projectDir);
1637
+ result = (0, skills_1.initProject)({ mode: "additive" });
1638
+ }
1639
+ finally {
1640
+ process.chdir(previousCwd);
1641
+ }
1642
+ args.launchInvestigation.strongestProject = project;
1643
+ return formatProjectBootstrapToolResult(project, result);
1644
+ }
1645
+ if (toolCall.tool === "sync_identity") {
1646
+ const publish = toolCall.mode === "publish" && userAskedForMutation(args.userInput);
1647
+ return await formatIdentitySyncToolResult(args.bundleDir, publish);
1648
+ }
1649
+ return "tool: respond\nstatus: skipped\nresult: no local host tool needed.";
1650
+ }
1333
1651
  async function handleLocalChatIntent(args) {
1334
- const runToolResultThroughModel = async (toolResult, spinnerLabel) => {
1652
+ const summarizeLocalToolResult = (toolResult) => {
1653
+ const get = (key) => {
1654
+ const match = toolResult.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
1655
+ return match?.[1]?.trim() || null;
1656
+ };
1657
+ const tool = get("tool") || "local_tool";
1658
+ const status = get("status") || "done";
1659
+ const project = get("project");
1660
+ const projectDir = get("project_dir") || get("recommended_next_project_dir");
1661
+ const recommended = get("recommended_next_move");
1662
+ const markers = get("markers");
1663
+ const changed = get("changed");
1664
+ const filesRead = Array.from(toolResult.matchAll(/^file:\s*(.+)$/gm))
1665
+ .map((match) => match[1].trim())
1666
+ .slice(0, 5);
1667
+ const actionItems = get("action_items");
1668
+ const displayedActionItems = actionItems
1669
+ ?.split(" | ")
1670
+ .slice(0, 3)
1671
+ .join(" | ");
1672
+ if (status === "blocked") {
1673
+ const result = get("result") || "local tool blocked";
1674
+ return [
1675
+ result,
1676
+ "",
1677
+ `next strongest move: ${recommended || "run a read-only local scan before attempting a write."}`,
1678
+ ].join("\n");
1679
+ }
1680
+ if (tool === "read_project_context") {
1681
+ const lines = [
1682
+ project ? `read ${project}.` : "read the local project.",
1683
+ projectDir ? `path: ${projectDir}` : null,
1684
+ markers ? `markers: ${markers}` : null,
1685
+ filesRead.length > 0 ? `files read: ${filesRead.join(", ")}` : null,
1686
+ displayedActionItems && displayedActionItems !== "none found in TODO/current-state files" ? `found: ${displayedActionItems}` : null,
1687
+ "",
1688
+ `next strongest move: ${recommended || "tighten the current-state and TODO docs from the actual repo context."}`,
1689
+ ].filter((line) => !!line);
1690
+ return lines.join("\n");
1691
+ }
1692
+ if (tool === "project_bootstrap") {
1693
+ return [
1694
+ project ? `bootstrapped ${project}.` : "bootstrapped the project context.",
1695
+ changed || "changed: checked existing wiring.",
1696
+ "",
1697
+ `next strongest move: ${recommended || "read the project-context files and sharpen the current-state pass."}`,
1698
+ ].join("\n");
1699
+ }
1700
+ if (tool === "workspace_recent_projects") {
1701
+ return [
1702
+ "scanned local workspace projects.",
1703
+ project ? `strongest target: ${project}` : get("recommended_next_project") ? `strongest target: ${get("recommended_next_project")}` : null,
1704
+ projectDir ? `path: ${projectDir}` : null,
1705
+ "",
1706
+ `next strongest move: ${recommended || "say \"start there\" to read the strongest local project from actual files."}`,
1707
+ ].filter((line) => !!line).join("\n");
1708
+ }
1709
+ if (tool === "sync_identity") {
1710
+ return [
1711
+ "synced local identity context.",
1712
+ get("remote_sync") || "remote_sync: local compile only.",
1713
+ "",
1714
+ `next strongest move: ${recommended || "test the updated identity from another agent surface."}`,
1715
+ ].join("\n");
1716
+ }
1717
+ return [
1718
+ "local tool finished.",
1719
+ "",
1720
+ `next strongest move: ${recommended || "continue from the local result above."}`,
1721
+ ].join("\n");
1722
+ };
1723
+ const recordLocalToolResult = (toolResult) => {
1335
1724
  args.messages.push({ role: "user", content: args.userInput });
1336
1725
  args.messages.push({
1337
1726
  role: "user",
@@ -1345,56 +1734,57 @@ async function handleLocalChatIntent(args) {
1345
1734
  "Do not ask the user what the local project is if the tool result already names it.",
1346
1735
  "State what the local host actually found, make one concrete recommendation, and end with the exact phrase `next strongest move: ...`.",
1347
1736
  "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`.",
1737
+ "For read_project_context results, summarize what you actually saw and recommend the next concrete local tool action.",
1738
+ "For sync_identity results, state whether local compile or remote publish happened.",
1348
1739
  "Do not end with a question. Keep it under 8 lines. No generic help-desk closer.",
1349
1740
  ].join("\n"),
1350
1741
  });
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
- }
1742
+ const summary = summarizeLocalToolResult(toolResult);
1743
+ args.messages.push({ role: "assistant", content: summary });
1744
+ printAgentMessage(summary);
1356
1745
  };
1357
- if (isLocalRecentProjectsIntent(args.userInput)) {
1358
- const spinner = new render_1.BrailleSpinner("checking local workspaces");
1746
+ if (isLocalToolLoopCandidate(args.userInput)) {
1747
+ const routeSpinner = new render_1.BrailleSpinner("choosing local tool");
1748
+ routeSpinner.start();
1749
+ const toolCall = await chooseLocalHostTool(args);
1750
+ routeSpinner.stop(`selected ${toolCall.tool}`);
1751
+ if (toolCall.tool === "respond")
1752
+ return false;
1753
+ const labelByTool = {
1754
+ discover_projects: "discovering local projects",
1755
+ read_project_context: "reading project context",
1756
+ write_project_context: "writing project context",
1757
+ sync_identity: "syncing identity context",
1758
+ respond: "thinking",
1759
+ };
1760
+ const spinner = new render_1.BrailleSpinner(labelByTool[toolCall.tool]);
1359
1761
  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.";
1762
+ try {
1763
+ const toolResult = await executeLocalHostTool(toolCall, args);
1764
+ const statusLine = toolResult.match(/^status: (.+)$/m)?.[1] || "done";
1765
+ spinner.stop(statusLine);
1766
+ recordLocalToolResult(toolResult);
1767
+ }
1768
+ catch (err) {
1769
+ const message = err instanceof Error ? err.message : "local tool failed";
1770
+ spinner.fail(message);
1771
+ const response = [
1772
+ `local tool failed: ${message}`,
1773
+ "",
1774
+ "next strongest move: retry with a read-only local scan so i can recover from real filesystem state.",
1775
+ ].join("\n");
1373
1776
  args.messages.push({ role: "user", content: args.userInput });
1374
1777
  args.messages.push({ role: "assistant", content: response });
1375
1778
  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
1779
  }
1387
- finally {
1388
- process.chdir(previousCwd);
1389
- }
1390
- spinner.stop(`${project.name} updated`);
1391
- await runToolResultThroughModel(formatProjectBootstrapToolResult(project, result), `summarizing ${project.name} changes`);
1392
1780
  return true;
1393
1781
  }
1394
1782
  return false;
1395
1783
  }
1396
1784
  // ─── Main chat command ────────────────────────────────────────────────
1397
1785
  async function chatCommand() {
1786
+ // Chat is a readline loop — piped/CI stdin would hang or spin
1787
+ (0, render_1.requireInteractiveTTY)();
1398
1788
  const bundleDir = resolveBundleDirForChat();
1399
1789
  if (!bundleDir) {
1400
1790
  console.log("");
@@ -1413,23 +1803,24 @@ async function chatCommand() {
1413
1803
  ? rawProjectCtx
1414
1804
  : null;
1415
1805
  let projectContextBlock = "";
1416
- let activeProjectDir = null;
1806
+ let activeProjectResolution = null;
1417
1807
  if (projectCtx) {
1418
- // Try the new file-system project context first
1808
+ // Single project-context engine: managed store + repo project-context/
1809
+ // overlay (repo wins on conflicts; the union feeds the injection).
1419
1810
  const projectsRoot = (0, project_1.findProjectsRoot)();
1420
- if (projectsRoot) {
1421
- const detected = (0, project_1.detectCurrentProject)(projectsRoot);
1422
- if (detected) {
1423
- activeProjectDir = (0, project_1.getProjectDir)(projectsRoot, detected);
1424
- const injection = (0, project_1.buildProjectContextInjection)(activeProjectDir);
1425
- if (injection) {
1426
- projectContextBlock = `\n\n--- project context ---\n${injection}`;
1427
- }
1811
+ const detected = projectsRoot ? (0, project_1.detectCurrentProject)(projectsRoot) : null;
1812
+ const resolved = (0, projectContext_1.resolveProjectContext)(detected ? { projectName: detected } : {});
1813
+ if (detected || resolved.repoContextDir) {
1814
+ activeProjectResolution = resolved;
1815
+ const merged = (0, projectContext_1.readMergedProjectContext)(detected ? { projectName: detected } : {});
1816
+ const injection = merged ? (0, project_1.buildProjectContextInjectionFromContext)(merged) : null;
1817
+ if (injection) {
1818
+ projectContextBlock = `\n\n--- project context ---\n${injection}`;
1428
1819
  }
1429
1820
  }
1430
1821
  // Fallback to legacy project context if new system didn't produce anything
1431
1822
  if (!projectContextBlock) {
1432
- const projectNotes = (0, config_1.readProjectPrivateNotes)(projectCtx.name);
1823
+ const projectNotes = (0, projectContext_1.readProjectPrivateNotes)(projectCtx.name);
1433
1824
  const parts = [];
1434
1825
  parts.push(`the user is currently working in project: ${projectCtx.name} at ${projectCtx.root}`);
1435
1826
  if (projectCtx.youmdProject?.description) {
@@ -1526,7 +1917,7 @@ async function chatCommand() {
1526
1917
  continue;
1527
1918
  }
1528
1919
  if (lower === "/link") {
1529
- showLinkInfo(bundleDir);
1920
+ showLinkInfo();
1530
1921
  continue;
1531
1922
  }
1532
1923
  if (lower === "/share") {
@@ -1535,13 +1926,13 @@ async function chatCommand() {
1535
1926
  }
1536
1927
  if (lower === "/memory" || lower === "/memories") {
1537
1928
  try {
1538
- const { listMemories } = require("../lib/api");
1539
- const res = await listMemories({ limit: 20 });
1540
- if (res.ok && Array.isArray(res.data) && res.data.length > 0) {
1929
+ const res = await (0, api_1.listMemories)({ limit: 20 });
1930
+ const memories = res.ok ? res.data?.memories ?? [] : [];
1931
+ if (memories.length > 0) {
1541
1932
  const grouped = new Map();
1542
- for (const m of res.data)
1933
+ for (const m of memories)
1543
1934
  grouped.set(m.category, (grouped.get(m.category) || 0) + 1);
1544
- console.log(chalk_1.default.dim(` memory: ${res.data.length} total`));
1935
+ console.log(chalk_1.default.dim(` memory: ${memories.length} total`));
1545
1936
  for (const [cat, count] of grouped) {
1546
1937
  console.log(chalk_1.default.dim(` ${cat}s: ${count}`));
1547
1938
  }
@@ -1559,12 +1950,12 @@ async function chatCommand() {
1559
1950
  if (lower === "/recall" || lower.startsWith("/recall ")) {
1560
1951
  const query = lower.startsWith("/recall ") ? lower.slice(8).trim() : "";
1561
1952
  try {
1562
- const { listMemories } = require("../lib/api");
1563
- const res = await listMemories({ limit: 50 });
1564
- if (res.ok && Array.isArray(res.data)) {
1953
+ const res = await (0, api_1.listMemories)({ limit: 50 });
1954
+ if (res.ok && res.data?.memories) {
1955
+ const memories = res.data.memories;
1565
1956
  const matches = query
1566
- ? res.data.filter((m) => m.content.toLowerCase().includes(query) || m.category.includes(query))
1567
- : res.data.slice(0, 10);
1957
+ ? memories.filter((m) => m.content.toLowerCase().includes(query) || m.category.includes(query))
1958
+ : memories.slice(0, 10);
1568
1959
  if (matches.length > 0) {
1569
1960
  console.log(chalk_1.default.dim(query ? ` ${matches.length} memories matching "${query}":` : " recent memories:"));
1570
1961
  for (const m of matches.slice(0, 10)) {
@@ -1584,8 +1975,7 @@ async function chatCommand() {
1584
1975
  }
1585
1976
  if (lower === "/private") {
1586
1977
  try {
1587
- const { getPrivateContext } = require("../lib/api");
1588
- const res = await getPrivateContext();
1978
+ const res = await (0, api_1.getPrivateContext)();
1589
1979
  if (res.ok && res.data) {
1590
1980
  const p = res.data;
1591
1981
  if (p.privateNotes) {
@@ -1707,6 +2097,7 @@ async function chatCommand() {
1707
2097
  messages,
1708
2098
  launchInvestigation,
1709
2099
  apiKey,
2100
+ bundleDir,
1710
2101
  });
1711
2102
  if (handledLocally)
1712
2103
  continue;
@@ -1845,8 +2236,21 @@ async function chatCommand() {
1845
2236
  console.log(chalk_1.default.green(" [saved private note]"));
1846
2237
  }
1847
2238
  else if (pu.field === "privateProjects" && pu.action === "add" && pu.project) {
1848
- // For projects, we'd need to fetch existing + append — simplified for now
1849
- console.log(chalk_1.default.green(` [saved private project: ${pu.project.name || "unnamed"}]`));
2239
+ const existing = await (0, api_1.getPrivateContext)();
2240
+ const projects = existing.ok ? existing.data?.privateProjects ?? [] : [];
2241
+ const res = await (0, api_1.updatePrivateContext)({
2242
+ privateProjects: [...projects, {
2243
+ name: pu.project.name || "unnamed",
2244
+ description: pu.project.description || "",
2245
+ status: pu.project.status || "active",
2246
+ }],
2247
+ });
2248
+ if (res.ok) {
2249
+ console.log(chalk_1.default.green(` [saved private project: ${pu.project.name || "unnamed"}]`));
2250
+ }
2251
+ else {
2252
+ console.log(chalk_1.default.yellow(" [private context update failed]"));
2253
+ }
1850
2254
  }
1851
2255
  }
1852
2256
  catch {
@@ -1854,13 +2258,15 @@ async function chatCommand() {
1854
2258
  }
1855
2259
  }
1856
2260
  }
1857
- // Handle project context updates
1858
- if (activeProjectDir) {
2261
+ // Handle project context updates — the engine routes context/ files to
2262
+ // the repo project-context/ copy when inside the repo, otherwise to the
2263
+ // global ~/.youmd/projects/<name>/ store.
2264
+ if (activeProjectResolution) {
1859
2265
  const projUpdates = (0, project_1.parseProjectUpdates)(response);
1860
2266
  if (projUpdates.length > 0) {
1861
2267
  for (const pu of projUpdates) {
1862
2268
  try {
1863
- (0, project_1.updateProjectFile)(activeProjectDir, pu.file, pu.content);
2269
+ (0, projectContext_1.writeProjectUpdate)(activeProjectResolution, pu.file, pu.content);
1864
2270
  console.log(chalk_1.default.hex("#C46A3A")(` [updated project context: ${pu.file}]`));
1865
2271
  }
1866
2272
  catch {
@@ -1920,8 +2326,7 @@ function printAgentMessage(text) {
1920
2326
  if (!text)
1921
2327
  return;
1922
2328
  // Use rich terminal renderer for structured content
1923
- const { renderRichResponse } = require("../lib/render");
1924
- console.log(renderRichResponse(text));
2329
+ console.log((0, render_1.renderRichResponse)(text));
1925
2330
  console.log("");
1926
2331
  }
1927
2332
  //# sourceMappingURL=chat.js.map