@mastra/code-sdk 1.0.3-alpha.0 → 1.1.0-alpha.2

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 (258) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/acp/agent.js +155 -167
  3. package/dist/acp/agent.js.map +1 -1
  4. package/dist/acp/event-mapper.js +193 -168
  5. package/dist/acp/event-mapper.js.map +1 -1
  6. package/dist/acp/index.js +60 -49
  7. package/dist/acp/index.js.map +1 -1
  8. package/dist/acp/server.js +31 -30
  9. package/dist/acp/server.js.map +1 -1
  10. package/dist/acp.js +1 -4
  11. package/dist/agents/credential-resolver.js +40 -29
  12. package/dist/agents/credential-resolver.js.map +1 -1
  13. package/dist/agents/custom-provider-source.js +16 -9
  14. package/dist/agents/custom-provider-source.js.map +1 -1
  15. package/dist/agents/instructions.js +27 -36
  16. package/dist/agents/instructions.js.map +1 -1
  17. package/dist/agents/mastracode-gateway.js +355 -428
  18. package/dist/agents/mastracode-gateway.js.map +1 -1
  19. package/dist/agents/memory.js +87 -78
  20. package/dist/agents/memory.js.map +1 -1
  21. package/dist/agents/model.js +114 -119
  22. package/dist/agents/model.js.map +1 -1
  23. package/dist/agents/modes/build.js +12 -13
  24. package/dist/agents/modes/build.js.map +1 -1
  25. package/dist/agents/modes/explore.js +14 -13
  26. package/dist/agents/modes/explore.js.map +1 -1
  27. package/dist/agents/modes/plan.js +14 -15
  28. package/dist/agents/modes/plan.js.map +1 -1
  29. package/dist/agents/prompts/agent-instructions.js +78 -69
  30. package/dist/agents/prompts/agent-instructions.js.map +1 -1
  31. package/dist/agents/prompts/build.js +19 -16
  32. package/dist/agents/prompts/build.js.map +1 -1
  33. package/dist/agents/prompts/fast.js +8 -4
  34. package/dist/agents/prompts/fast.js.map +1 -1
  35. package/dist/agents/prompts/index.js +48 -47
  36. package/dist/agents/prompts/index.js.map +1 -1
  37. package/dist/agents/prompts/model.js +6 -5
  38. package/dist/agents/prompts/model.js.map +1 -1
  39. package/dist/agents/prompts/plan.js +20 -16
  40. package/dist/agents/prompts/plan.js.map +1 -1
  41. package/dist/agents/prompts/tool-guidance.js +77 -109
  42. package/dist/agents/prompts/tool-guidance.js.map +1 -1
  43. package/dist/agents/sandbox-filesystem.js +245 -259
  44. package/dist/agents/sandbox-filesystem.js.map +1 -1
  45. package/dist/agents/sandbox-reattach.js +9 -11
  46. package/dist/agents/sandbox-reattach.js.map +1 -1
  47. package/dist/agents/thread-caveman-state.js +92 -81
  48. package/dist/agents/thread-caveman-state.js.map +1 -1
  49. package/dist/agents/tool-availability.js +62 -80
  50. package/dist/agents/tool-availability.js.map +1 -1
  51. package/dist/agents/tools.js +99 -139
  52. package/dist/agents/tools.js.map +1 -1
  53. package/dist/agents/workspace.js +232 -225
  54. package/dist/agents/workspace.js.map +1 -1
  55. package/dist/analytics.js +139 -128
  56. package/dist/analytics.js.map +1 -1
  57. package/dist/auth/authorization-input.js +39 -25
  58. package/dist/auth/authorization-input.js.map +1 -1
  59. package/dist/auth/device-code.js +112 -80
  60. package/dist/auth/device-code.js.map +1 -1
  61. package/dist/auth/index.js +2 -9
  62. package/dist/auth/pkce.js +27 -16
  63. package/dist/auth/pkce.js.map +1 -1
  64. package/dist/auth/providers/anthropic.js +107 -93
  65. package/dist/auth/providers/anthropic.js.map +1 -1
  66. package/dist/auth/providers/github-copilot.js +304 -293
  67. package/dist/auth/providers/github-copilot.js.map +1 -1
  68. package/dist/auth/providers/openai-codex.js +442 -460
  69. package/dist/auth/providers/openai-codex.js.map +1 -1
  70. package/dist/auth/providers/xai.js +179 -145
  71. package/dist/auth/providers/xai.js.map +1 -1
  72. package/dist/auth/storage.js +182 -175
  73. package/dist/auth/storage.js.map +1 -1
  74. package/dist/auth/types.js +1 -1
  75. package/dist/clipboard/index.js +206 -172
  76. package/dist/clipboard/index.js.map +1 -1
  77. package/dist/constants.js +11 -16
  78. package/dist/constants.js.map +1 -1
  79. package/dist/error-classification.js +19 -11
  80. package/dist/error-classification.js.map +1 -1
  81. package/dist/evals/context-builder.js +145 -104
  82. package/dist/evals/context-builder.js.map +1 -1
  83. package/dist/evals/index.js +4 -7
  84. package/dist/evals/scorers/classify-command.js +62 -41
  85. package/dist/evals/scorers/classify-command.js.map +1 -1
  86. package/dist/evals/scorers/efficiency.js +239 -181
  87. package/dist/evals/scorers/efficiency.js.map +1 -1
  88. package/dist/evals/scorers/extract-tools.js +51 -47
  89. package/dist/evals/scorers/extract-tools.js.map +1 -1
  90. package/dist/evals/scorers/index.js +1 -5
  91. package/dist/evals/scorers/outcome.js +321 -212
  92. package/dist/evals/scorers/outcome.js.map +1 -1
  93. package/dist/goal-manager.js +266 -261
  94. package/dist/goal-manager.js.map +1 -1
  95. package/dist/goal-signal.js +14 -13
  96. package/dist/goal-signal.js.map +1 -1
  97. package/dist/headless/cli.js +153 -154
  98. package/dist/headless/cli.js.map +1 -1
  99. package/dist/headless/flags.js +180 -178
  100. package/dist/headless/flags.js.map +1 -1
  101. package/dist/headless/format.js +72 -71
  102. package/dist/headless/format.js.map +1 -1
  103. package/dist/headless/index.js +5 -31
  104. package/dist/headless/policy.js +35 -31
  105. package/dist/headless/policy.js.map +1 -1
  106. package/dist/headless/run-mc.js +332 -334
  107. package/dist/headless/run-mc.js.map +1 -1
  108. package/dist/headless/types.js +16 -7
  109. package/dist/headless/types.js.map +1 -1
  110. package/dist/hooks/config.js +56 -61
  111. package/dist/hooks/config.js.map +1 -1
  112. package/dist/hooks/executor.js +118 -116
  113. package/dist/hooks/executor.js.map +1 -1
  114. package/dist/hooks/index.js +4 -15
  115. package/dist/hooks/manager.js +221 -280
  116. package/dist/hooks/manager.js.map +1 -1
  117. package/dist/hooks/types.js +5 -4
  118. package/dist/hooks/types.js.map +1 -1
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +632 -712
  121. package/dist/index.js.map +1 -1
  122. package/dist/ipc/ipc-reporter.js +19 -22
  123. package/dist/ipc/ipc-reporter.js.map +1 -1
  124. package/dist/lsp/client.js +248 -306
  125. package/dist/lsp/client.js.map +1 -1
  126. package/dist/lsp/index.js +1 -6
  127. package/dist/lsp/language.js +39 -43
  128. package/dist/lsp/language.js.map +1 -1
  129. package/dist/lsp/manager.js +78 -93
  130. package/dist/lsp/manager.js.map +1 -1
  131. package/dist/lsp/server.js +151 -120
  132. package/dist/lsp/server.js.map +1 -1
  133. package/dist/lsp/workspace.js +50 -47
  134. package/dist/lsp/workspace.js.map +1 -1
  135. package/dist/mcp/config.js +210 -199
  136. package/dist/mcp/config.js.map +1 -1
  137. package/dist/mcp/index.js +2 -9
  138. package/dist/mcp/manager.js +482 -475
  139. package/dist/mcp/manager.js.map +1 -1
  140. package/dist/mcp/types.js +1 -1
  141. package/dist/onboarding/custom-packs.js +14 -17
  142. package/dist/onboarding/custom-packs.js.map +1 -1
  143. package/dist/onboarding/custom-providers.js +22 -20
  144. package/dist/onboarding/custom-providers.js.map +1 -1
  145. package/dist/onboarding/index.d.ts +1 -1
  146. package/dist/onboarding/index.d.ts.map +1 -1
  147. package/dist/onboarding/index.js +3 -21
  148. package/dist/onboarding/om-settings.d.ts +5 -0
  149. package/dist/onboarding/om-settings.d.ts.map +1 -1
  150. package/dist/onboarding/om-settings.js +41 -20
  151. package/dist/onboarding/om-settings.js.map +1 -1
  152. package/dist/onboarding/packs.d.ts +4 -6
  153. package/dist/onboarding/packs.d.ts.map +1 -1
  154. package/dist/onboarding/packs.js +154 -105
  155. package/dist/onboarding/packs.js.map +1 -1
  156. package/dist/onboarding/settings.js +575 -517
  157. package/dist/onboarding/settings.js.map +1 -1
  158. package/dist/permissions.js +112 -99
  159. package/dist/permissions.js.map +1 -1
  160. package/dist/plugin.js +21 -23
  161. package/dist/plugin.js.map +1 -1
  162. package/dist/plugins/dependencies.js +64 -87
  163. package/dist/plugins/dependencies.js.map +1 -1
  164. package/dist/plugins/install.js +159 -169
  165. package/dist/plugins/install.js.map +1 -1
  166. package/dist/plugins/loader.js +170 -185
  167. package/dist/plugins/loader.js.map +1 -1
  168. package/dist/plugins/manager.js +374 -380
  169. package/dist/plugins/manager.js.map +1 -1
  170. package/dist/plugins/manifest.js +38 -63
  171. package/dist/plugins/manifest.js.map +1 -1
  172. package/dist/plugins/package-link.js +48 -64
  173. package/dist/plugins/package-link.js.map +1 -1
  174. package/dist/plugins/paths.js +18 -19
  175. package/dist/plugins/paths.js.map +1 -1
  176. package/dist/plugins/registry.js +97 -81
  177. package/dist/plugins/registry.js.map +1 -1
  178. package/dist/plugins/scaffold.js +62 -98
  179. package/dist/plugins/scaffold.js.map +1 -1
  180. package/dist/plugins/types.js +1 -1
  181. package/dist/processors/plan-rejection-abort.js +45 -44
  182. package/dist/processors/plan-rejection-abort.js.map +1 -1
  183. package/dist/providers/amazon-bedrock-gateway.js +134 -125
  184. package/dist/providers/amazon-bedrock-gateway.js.map +1 -1
  185. package/dist/providers/amazon-bedrock.js +68 -47
  186. package/dist/providers/amazon-bedrock.js.map +1 -1
  187. package/dist/providers/claude-max.js +153 -133
  188. package/dist/providers/claude-max.js.map +1 -1
  189. package/dist/providers/github-copilot.js +232 -230
  190. package/dist/providers/github-copilot.js.map +1 -1
  191. package/dist/providers/openai-codex.js +271 -260
  192. package/dist/providers/openai-codex.js.map +1 -1
  193. package/dist/providers/xai.js +57 -50
  194. package/dist/providers/xai.js.map +1 -1
  195. package/dist/schema.js +91 -89
  196. package/dist/schema.js.map +1 -1
  197. package/dist/theme-palette.js +18 -14
  198. package/dist/theme-palette.js.map +1 -1
  199. package/dist/tool-names.js +46 -37
  200. package/dist/tool-names.js.map +1 -1
  201. package/dist/tools/index.js +2 -8
  202. package/dist/tools/request-sandbox-access.js +71 -80
  203. package/dist/tools/request-sandbox-access.js.map +1 -1
  204. package/dist/tools/utils.js +26 -16
  205. package/dist/tools/utils.js.map +1 -1
  206. package/dist/tools/web-search.js +51 -55
  207. package/dist/tools/web-search.js.map +1 -1
  208. package/dist/utils/binaries.js +65 -61
  209. package/dist/utils/binaries.js.map +1 -1
  210. package/dist/utils/debug-log.js +51 -47
  211. package/dist/utils/debug-log.js.map +1 -1
  212. package/dist/utils/errors.js +157 -192
  213. package/dist/utils/errors.js.map +1 -1
  214. package/dist/utils/gateway-sync.js +49 -37
  215. package/dist/utils/gateway-sync.js.map +1 -1
  216. package/dist/utils/path-security.js +7 -6
  217. package/dist/utils/path-security.js.map +1 -1
  218. package/dist/utils/plan-diff.js +58 -39
  219. package/dist/utils/plan-diff.js.map +1 -1
  220. package/dist/utils/plans.js +92 -74
  221. package/dist/utils/plans.js.map +1 -1
  222. package/dist/utils/project.js +318 -256
  223. package/dist/utils/project.js.map +1 -1
  224. package/dist/utils/signals-pubsub.js +117 -99
  225. package/dist/utils/signals-pubsub.js.map +1 -1
  226. package/dist/utils/slash-command-loader.js +118 -154
  227. package/dist/utils/slash-command-loader.js.map +1 -1
  228. package/dist/utils/slash-command-processor.js +92 -83
  229. package/dist/utils/slash-command-processor.js.map +1 -1
  230. package/dist/utils/stdin-pipe.js +87 -63
  231. package/dist/utils/stdin-pipe.js.map +1 -1
  232. package/dist/utils/storage-factory.js +104 -84
  233. package/dist/utils/storage-factory.js.map +1 -1
  234. package/dist/utils/storage-maintenance.js +249 -211
  235. package/dist/utils/storage-maintenance.js.map +1 -1
  236. package/dist/utils/thread-lock.js +96 -92
  237. package/dist/utils/thread-lock.js.map +1 -1
  238. package/dist/utils/token-estimator.js +12 -12
  239. package/dist/utils/token-estimator.js.map +1 -1
  240. package/dist/utils/update-check.d.ts +1 -1
  241. package/dist/utils/update-check.js +341 -259
  242. package/dist/utils/update-check.js.map +1 -1
  243. package/dist/voice/stt-registry.js +131 -113
  244. package/dist/voice/stt-registry.js.map +1 -1
  245. package/package.json +11 -11
  246. package/dist/acp.js.map +0 -1
  247. package/dist/auth/index.js.map +0 -1
  248. package/dist/auth/types.js.map +0 -1
  249. package/dist/evals/index.js.map +0 -1
  250. package/dist/evals/scorers/index.js.map +0 -1
  251. package/dist/headless/index.js.map +0 -1
  252. package/dist/hooks/index.js.map +0 -1
  253. package/dist/lsp/index.js.map +0 -1
  254. package/dist/mcp/index.js.map +0 -1
  255. package/dist/mcp/types.js.map +0 -1
  256. package/dist/onboarding/index.js.map +0 -1
  257. package/dist/plugins/types.js.map +0 -1
  258. package/dist/tools/index.js.map +0 -1
@@ -1,43 +1,62 @@
1
+ //#region src/utils/plan-diff.ts
2
+ /** Produce structured diff entries between two plan texts using an LCS diff. */
1
3
  function generatePlanDiff(oldText, newText) {
2
- const oldLines = oldText.split(/\r?\n/);
3
- const newLines = newText.split(/\r?\n/);
4
- const n = oldLines.length;
5
- const m = newLines.length;
6
- const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
7
- for (let i2 = n - 1; i2 >= 0; i2--) {
8
- for (let j2 = m - 1; j2 >= 0; j2--) {
9
- lcs[i2][j2] = oldLines[i2] === newLines[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
10
- }
11
- }
12
- const entries = [];
13
- let i = 0;
14
- let j = 0;
15
- while (i < n && j < m) {
16
- if (oldLines[i] === newLines[j]) {
17
- entries.push({ type: "context", text: oldLines[i] });
18
- i++;
19
- j++;
20
- } else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
21
- entries.push({ type: "removed", text: oldLines[i] });
22
- i++;
23
- } else {
24
- entries.push({ type: "added", text: newLines[j] });
25
- j++;
26
- }
27
- }
28
- while (i < n) entries.push({ type: "removed", text: oldLines[i++] });
29
- while (j < m) entries.push({ type: "added", text: newLines[j++] });
30
- return entries;
4
+ const oldLines = oldText.split(/\r?\n/);
5
+ const newLines = newText.split(/\r?\n/);
6
+ const n = oldLines.length;
7
+ const m = newLines.length;
8
+ const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
9
+ for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) lcs[i][j] = oldLines[i] === newLines[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
10
+ const entries = [];
11
+ let i = 0;
12
+ let j = 0;
13
+ while (i < n && j < m) if (oldLines[i] === newLines[j]) {
14
+ entries.push({
15
+ type: "context",
16
+ text: oldLines[i]
17
+ });
18
+ i++;
19
+ j++;
20
+ } else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
21
+ entries.push({
22
+ type: "removed",
23
+ text: oldLines[i]
24
+ });
25
+ i++;
26
+ } else {
27
+ entries.push({
28
+ type: "added",
29
+ text: newLines[j]
30
+ });
31
+ j++;
32
+ }
33
+ while (i < n) entries.push({
34
+ type: "removed",
35
+ text: oldLines[i++]
36
+ });
37
+ while (j < m) entries.push({
38
+ type: "added",
39
+ text: newLines[j++]
40
+ });
41
+ return entries;
31
42
  }
32
- function shouldShowDiff(previousPlan, plan, maxChangedRatio = 0.5) {
33
- if (!previousPlan || previousPlan === plan) return false;
34
- const entries = generatePlanDiff(previousPlan, plan);
35
- const changed = entries.filter((e) => e.type !== "context").length;
36
- if (changed === 0) return false;
37
- return changed / Math.max(entries.length, 1) <= maxChangedRatio;
43
+ /**
44
+ * Decide whether a diff is worth rendering instead of just showing the full new plan.
45
+ *
46
+ * We generate the real diff and compare its size (added + removed lines) against the size
47
+ * of the whole diff (added + removed + unchanged context). When most of the diff is changes
48
+ * rather than shared context, the diff is no clearer than the full text, so callers fall
49
+ * back to rendering the whole plan. A genuinely new plan (no previous text, or no shared
50
+ * lines) is therefore shown in full.
51
+ */
52
+ function shouldShowDiff(previousPlan, plan, maxChangedRatio = .5) {
53
+ if (!previousPlan || previousPlan === plan) return false;
54
+ const entries = generatePlanDiff(previousPlan, plan);
55
+ const changed = entries.filter((e) => e.type !== "context").length;
56
+ if (changed === 0) return false;
57
+ return changed / Math.max(entries.length, 1) <= maxChangedRatio;
38
58
  }
39
- export {
40
- generatePlanDiff,
41
- shouldShowDiff
42
- };
59
+ //#endregion
60
+ export { generatePlanDiff, shouldShowDiff };
61
+
43
62
  //# sourceMappingURL=plan-diff.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/plan-diff.ts"],"sourcesContent":["/**\n * Line-level diffing for plan revisions.\n *\n * Uses a real LCS (longest common subsequence) diff so that inserting or deleting a few\n * lines doesn't mark everything after it as changed. Both the rendered approval diff and\n * the \"is a diff worth showing\" heuristic share this so the heuristic measures the actual\n * diff rather than predicting one.\n */\n\nexport interface DiffEntry {\n type: 'added' | 'removed' | 'context';\n text: string;\n}\n\n/** Produce structured diff entries between two plan texts using an LCS diff. */\nexport function generatePlanDiff(oldText: string, newText: string): DiffEntry[] {\n const oldLines = oldText.split(/\\r?\\n/);\n const newLines = newText.split(/\\r?\\n/);\n const n = oldLines.length;\n const m = newLines.length;\n\n // LCS length table.\n const lcs: number[][] = Array.from({ length: n + 1 }, () => new Array<number>(m + 1).fill(0));\n for (let i = n - 1; i >= 0; i--) {\n for (let j = m - 1; j >= 0; j--) {\n lcs[i]![j] = oldLines[i] === newLines[j] ? lcs[i + 1]![j + 1]! + 1 : Math.max(lcs[i + 1]![j]!, lcs[i]![j + 1]!);\n }\n }\n\n const entries: DiffEntry[] = [];\n let i = 0;\n let j = 0;\n while (i < n && j < m) {\n if (oldLines[i] === newLines[j]) {\n entries.push({ type: 'context', text: oldLines[i]! });\n i++;\n j++;\n } else if (lcs[i + 1]![j]! >= lcs[i]![j + 1]!) {\n entries.push({ type: 'removed', text: oldLines[i]! });\n i++;\n } else {\n entries.push({ type: 'added', text: newLines[j]! });\n j++;\n }\n }\n while (i < n) entries.push({ type: 'removed', text: oldLines[i++]! });\n while (j < m) entries.push({ type: 'added', text: newLines[j++]! });\n\n return entries;\n}\n\n/**\n * Decide whether a diff is worth rendering instead of just showing the full new plan.\n *\n * We generate the real diff and compare its size (added + removed lines) against the size\n * of the whole diff (added + removed + unchanged context). When most of the diff is changes\n * rather than shared context, the diff is no clearer than the full text, so callers fall\n * back to rendering the whole plan. A genuinely new plan (no previous text, or no shared\n * lines) is therefore shown in full.\n */\nexport function shouldShowDiff(previousPlan: string, plan: string, maxChangedRatio = 0.5): boolean {\n if (!previousPlan || previousPlan === plan) return false;\n\n const entries = generatePlanDiff(previousPlan, plan);\n const changed = entries.filter(e => e.type !== 'context').length;\n if (changed === 0) return false;\n\n // changed / total entries == diff size relative to the full plan view.\n return changed / Math.max(entries.length, 1) <= maxChangedRatio;\n}\n"],"mappings":"AAeO,SAAS,iBAAiB,SAAiB,SAA8B;AAC9E,QAAM,WAAW,QAAQ,MAAM,OAAO;AACtC,QAAM,WAAW,QAAQ,MAAM,OAAO;AACtC,QAAM,IAAI,SAAS;AACnB,QAAM,IAAI,SAAS;AAGnB,QAAM,MAAkB,MAAM,KAAK,EAAE,QAAQ,IAAI,EAAE,GAAG,MAAM,IAAI,MAAc,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;AAC5F,WAASA,KAAI,IAAI,GAAGA,MAAK,GAAGA,MAAK;AAC/B,aAASC,KAAI,IAAI,GAAGA,MAAK,GAAGA,MAAK;AAC/B,UAAID,EAAC,EAAGC,EAAC,IAAI,SAASD,EAAC,MAAM,SAASC,EAAC,IAAI,IAAID,KAAI,CAAC,EAAGC,KAAI,CAAC,IAAK,IAAI,KAAK,IAAI,IAAID,KAAI,CAAC,EAAGC,EAAC,GAAI,IAAID,EAAC,EAAGC,KAAI,CAAC,CAAE;AAAA,IAChH;AAAA,EACF;AAEA,QAAM,UAAuB,CAAC;AAC9B,MAAI,IAAI;AACR,MAAI,IAAI;AACR,SAAO,IAAI,KAAK,IAAI,GAAG;AACrB,QAAI,SAAS,CAAC,MAAM,SAAS,CAAC,GAAG;AAC/B,cAAQ,KAAK,EAAE,MAAM,WAAW,MAAM,SAAS,CAAC,EAAG,CAAC;AACpD;AACA;AAAA,IACF,WAAW,IAAI,IAAI,CAAC,EAAG,CAAC,KAAM,IAAI,CAAC,EAAG,IAAI,CAAC,GAAI;AAC7C,cAAQ,KAAK,EAAE,MAAM,WAAW,MAAM,SAAS,CAAC,EAAG,CAAC;AACpD;AAAA,IACF,OAAO;AACL,cAAQ,KAAK,EAAE,MAAM,SAAS,MAAM,SAAS,CAAC,EAAG,CAAC;AAClD;AAAA,IACF;AAAA,EACF;AACA,SAAO,IAAI,EAAG,SAAQ,KAAK,EAAE,MAAM,WAAW,MAAM,SAAS,GAAG,EAAG,CAAC;AACpE,SAAO,IAAI,EAAG,SAAQ,KAAK,EAAE,MAAM,SAAS,MAAM,SAAS,GAAG,EAAG,CAAC;AAElE,SAAO;AACT;AAWO,SAAS,eAAe,cAAsB,MAAc,kBAAkB,KAAc;AACjG,MAAI,CAAC,gBAAgB,iBAAiB,KAAM,QAAO;AAEnD,QAAM,UAAU,iBAAiB,cAAc,IAAI;AACnD,QAAM,UAAU,QAAQ,OAAO,OAAK,EAAE,SAAS,SAAS,EAAE;AAC1D,MAAI,YAAY,EAAG,QAAO;AAG1B,SAAO,UAAU,KAAK,IAAI,QAAQ,QAAQ,CAAC,KAAK;AAClD;","names":["i","j"]}
1
+ {"version":3,"file":"plan-diff.js","names":[],"sources":["../../src/utils/plan-diff.ts"],"sourcesContent":["/**\n * Line-level diffing for plan revisions.\n *\n * Uses a real LCS (longest common subsequence) diff so that inserting or deleting a few\n * lines doesn't mark everything after it as changed. Both the rendered approval diff and\n * the \"is a diff worth showing\" heuristic share this so the heuristic measures the actual\n * diff rather than predicting one.\n */\n\nexport interface DiffEntry {\n type: 'added' | 'removed' | 'context';\n text: string;\n}\n\n/** Produce structured diff entries between two plan texts using an LCS diff. */\nexport function generatePlanDiff(oldText: string, newText: string): DiffEntry[] {\n const oldLines = oldText.split(/\\r?\\n/);\n const newLines = newText.split(/\\r?\\n/);\n const n = oldLines.length;\n const m = newLines.length;\n\n // LCS length table.\n const lcs: number[][] = Array.from({ length: n + 1 }, () => new Array<number>(m + 1).fill(0));\n for (let i = n - 1; i >= 0; i--) {\n for (let j = m - 1; j >= 0; j--) {\n lcs[i]![j] = oldLines[i] === newLines[j] ? lcs[i + 1]![j + 1]! + 1 : Math.max(lcs[i + 1]![j]!, lcs[i]![j + 1]!);\n }\n }\n\n const entries: DiffEntry[] = [];\n let i = 0;\n let j = 0;\n while (i < n && j < m) {\n if (oldLines[i] === newLines[j]) {\n entries.push({ type: 'context', text: oldLines[i]! });\n i++;\n j++;\n } else if (lcs[i + 1]![j]! >= lcs[i]![j + 1]!) {\n entries.push({ type: 'removed', text: oldLines[i]! });\n i++;\n } else {\n entries.push({ type: 'added', text: newLines[j]! });\n j++;\n }\n }\n while (i < n) entries.push({ type: 'removed', text: oldLines[i++]! });\n while (j < m) entries.push({ type: 'added', text: newLines[j++]! });\n\n return entries;\n}\n\n/**\n * Decide whether a diff is worth rendering instead of just showing the full new plan.\n *\n * We generate the real diff and compare its size (added + removed lines) against the size\n * of the whole diff (added + removed + unchanged context). When most of the diff is changes\n * rather than shared context, the diff is no clearer than the full text, so callers fall\n * back to rendering the whole plan. A genuinely new plan (no previous text, or no shared\n * lines) is therefore shown in full.\n */\nexport function shouldShowDiff(previousPlan: string, plan: string, maxChangedRatio = 0.5): boolean {\n if (!previousPlan || previousPlan === plan) return false;\n\n const entries = generatePlanDiff(previousPlan, plan);\n const changed = entries.filter(e => e.type !== 'context').length;\n if (changed === 0) return false;\n\n // changed / total entries == diff size relative to the full plan view.\n return changed / Math.max(entries.length, 1) <= maxChangedRatio;\n}\n"],"mappings":";;AAeA,SAAgB,iBAAiB,SAAiB,SAA8B;CAC9E,MAAM,WAAW,QAAQ,MAAM,OAAO;CACtC,MAAM,WAAW,QAAQ,MAAM,OAAO;CACtC,MAAM,IAAI,SAAS;CACnB,MAAM,IAAI,SAAS;CAGnB,MAAM,MAAkB,MAAM,KAAK,EAAE,QAAQ,IAAI,EAAE,SAAS,IAAI,MAAc,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;CAC5F,KAAK,IAAI,IAAI,IAAI,GAAG,KAAK,GAAG,KAC1B,KAAK,IAAI,IAAI,IAAI,GAAG,KAAK,GAAG,KAC1B,IAAI,EAAE,CAAE,KAAK,SAAS,OAAO,SAAS,KAAK,IAAI,IAAI,EAAE,CAAE,IAAI,KAAM,IAAI,KAAK,IAAI,IAAI,IAAI,EAAE,CAAE,IAAK,IAAI,EAAE,CAAE,IAAI,EAAG;CAIlH,MAAM,UAAuB,CAAC;CAC9B,IAAI,IAAI;CACR,IAAI,IAAI;CACR,OAAO,IAAI,KAAK,IAAI,GAClB,IAAI,SAAS,OAAO,SAAS,IAAI;EAC/B,QAAQ,KAAK;GAAE,MAAM;GAAW,MAAM,SAAS;EAAI,CAAC;EACpD;EACA;CACF,OAAO,IAAI,IAAI,IAAI,EAAE,CAAE,MAAO,IAAI,EAAE,CAAE,IAAI,IAAK;EAC7C,QAAQ,KAAK;GAAE,MAAM;GAAW,MAAM,SAAS;EAAI,CAAC;EACpD;CACF,OAAO;EACL,QAAQ,KAAK;GAAE,MAAM;GAAS,MAAM,SAAS;EAAI,CAAC;EAClD;CACF;CAEF,OAAO,IAAI,GAAG,QAAQ,KAAK;EAAE,MAAM;EAAW,MAAM,SAAS;CAAM,CAAC;CACpE,OAAO,IAAI,GAAG,QAAQ,KAAK;EAAE,MAAM;EAAS,MAAM,SAAS;CAAM,CAAC;CAElE,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,eAAe,cAAsB,MAAc,kBAAkB,IAAc;CACjG,IAAI,CAAC,gBAAgB,iBAAiB,MAAM,OAAO;CAEnD,MAAM,UAAU,iBAAiB,cAAc,IAAI;CACnD,MAAM,UAAU,QAAQ,QAAO,MAAK,EAAE,SAAS,SAAS,CAAC,CAAC;CAC1D,IAAI,YAAY,GAAG,OAAO;CAG1B,OAAO,UAAU,KAAK,IAAI,QAAQ,QAAQ,CAAC,KAAK;AAClD"}
@@ -1,99 +1,117 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
1
  import { DEFAULT_CONFIG_DIR } from "../constants.js";
4
2
  import { getAppDataDir } from "./project.js";
3
+ import path from "path";
4
+ import fs from "fs/promises";
5
+ //#region src/utils/plans.ts
6
+ /** Global plans directory for approved-plan archives. */
5
7
  function getPlansDir() {
6
- return process.env.MASTRA_PLANS_DIR ?? path.join(getAppDataDir(), "plans");
8
+ return process.env.MASTRA_PLANS_DIR ?? path.join(getAppDataDir(), "plans");
7
9
  }
10
+ /** Local (project-scoped) plans directory where the agent writes named plan files. */
8
11
  function getLocalPlansDir(projectPath) {
9
- return path.join(projectPath, DEFAULT_CONFIG_DIR, "plans");
12
+ return path.join(projectPath, DEFAULT_CONFIG_DIR, "plans");
10
13
  }
14
+ /** Workspace-relative directory the agent writes plan files into. */
11
15
  function getLocalPlansRelativeDir() {
12
- return path.join(DEFAULT_CONFIG_DIR, "plans");
16
+ return path.join(DEFAULT_CONFIG_DIR, "plans");
13
17
  }
14
18
  function slugify(str) {
15
- const slug = str.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
16
- return slug || "untitled";
19
+ return str.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "untitled";
17
20
  }
21
+ /** Derive a plan filename from a title (e.g. `add-dark-mode.md`). */
18
22
  function getPlanFilename(title) {
19
- return `${slugify(title)}.md`;
23
+ return `${slugify(title)}.md`;
20
24
  }
25
+ /**
26
+ * Suggested workspace-relative path for a new plan file, shown in plan-mode prompts.
27
+ * Without a title we fall back to a generic name the agent can rename later.
28
+ */
21
29
  function getSuggestedPlanRelativePath(title) {
22
- const filename = title ? getPlanFilename(title) : "plan.md";
23
- return path.join(getLocalPlansRelativeDir(), filename);
30
+ const filename = title ? getPlanFilename(title) : "plan.md";
31
+ return path.join(getLocalPlansRelativeDir(), filename);
24
32
  }
33
+ /**
34
+ * Resolve a plan path submitted by the agent (absolute or project-relative) to an
35
+ * absolute path. Returns `undefined` when no usable path was provided.
36
+ */
25
37
  function resolvePlanPath(projectPath, submittedPath) {
26
- if (!submittedPath) return void 0;
27
- return path.isAbsolute(submittedPath) ? submittedPath : path.resolve(projectPath, submittedPath);
38
+ if (!submittedPath) return void 0;
39
+ return path.isAbsolute(submittedPath) ? submittedPath : path.resolve(projectPath, submittedPath);
28
40
  }
41
+ /**
42
+ * Whether `targetPath` (absolute or project-relative) is a valid plan file: a `.md`
43
+ * file located directly inside the project's `.mastracode/plans/` directory. Used by the
44
+ * plan-mode write guard so the agent can write any named plan file there, but nothing
45
+ * outside that directory.
46
+ */
29
47
  function isPlanFilePath(projectPath, targetPath) {
30
- const abs = resolvePlanPath(projectPath, targetPath);
31
- if (!abs) return false;
32
- if (path.extname(abs).toLowerCase() !== ".md") return false;
33
- const plansDir = path.resolve(getLocalPlansDir(projectPath));
34
- const rel = path.relative(plansDir, abs);
35
- if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return false;
36
- return !rel.includes(path.sep);
48
+ const abs = resolvePlanPath(projectPath, targetPath);
49
+ if (!abs) return false;
50
+ if (path.extname(abs).toLowerCase() !== ".md") return false;
51
+ const plansDir = path.resolve(getLocalPlansDir(projectPath));
52
+ const rel = path.relative(plansDir, abs);
53
+ if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return false;
54
+ return !rel.includes(path.sep);
37
55
  }
38
56
  async function savePlanToDisk(opts) {
39
- const { title, plan, resourceId } = opts;
40
- const plansDir = opts.plansDir ?? getPlansDir();
41
- const baseDir = path.resolve(plansDir);
42
- const dir = path.resolve(baseDir, resourceId);
43
- const rel = path.relative(baseDir, dir);
44
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
45
- throw new Error(`Invalid resourceId: ${resourceId}`);
46
- }
47
- await fs.mkdir(dir, { recursive: true });
48
- const now = /* @__PURE__ */ new Date();
49
- const timestamp = now.toISOString().replace(/:/g, "-");
50
- const slug = slugify(title);
51
- const filename = `${timestamp}-${slug}.md`;
52
- const content = `# ${title}
53
-
54
- Approved: ${now.toISOString()}
55
-
56
- ${plan}
57
- `;
58
- await fs.writeFile(path.join(dir, filename), content, "utf-8");
57
+ const { title, plan, resourceId } = opts;
58
+ const plansDir = opts.plansDir ?? getPlansDir();
59
+ const baseDir = path.resolve(plansDir);
60
+ const dir = path.resolve(baseDir, resourceId);
61
+ const rel = path.relative(baseDir, dir);
62
+ if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`Invalid resourceId: ${resourceId}`);
63
+ await fs.mkdir(dir, { recursive: true });
64
+ const now = /* @__PURE__ */ new Date();
65
+ const filename = `${now.toISOString().replace(/:/g, "-")}-${slugify(title)}.md`;
66
+ const content = `# ${title}\n\nApproved: ${now.toISOString()}\n\n${plan}\n`;
67
+ await fs.writeFile(path.join(dir, filename), content, "utf-8");
59
68
  }
69
+ /**
70
+ * Read a plan markdown file by absolute path.
71
+ *
72
+ * The leading `# <title>` heading (if present) is parsed as the title and the remaining
73
+ * content is returned as the plan body. Returns `undefined` when the file does not exist.
74
+ */
60
75
  async function readPlanFile(absPath) {
61
- let raw;
62
- try {
63
- raw = await fs.readFile(absPath, "utf-8");
64
- } catch {
65
- return void 0;
66
- }
67
- const lines = raw.split(/\r?\n/);
68
- const headingIndex = lines.findIndex((line) => line.trim().length > 0);
69
- const heading = headingIndex >= 0 ? lines[headingIndex] : void 0;
70
- if (heading?.startsWith("# ")) {
71
- const title = heading.slice(2).trim();
72
- const plan = lines.slice(headingIndex + 1).join("\n").replace(/^\n+/, "").trimEnd();
73
- return { title, plan };
74
- }
75
- return { title: "", plan: raw.trimEnd() };
76
+ let raw;
77
+ try {
78
+ raw = await fs.readFile(absPath, "utf-8");
79
+ } catch {
80
+ return;
81
+ }
82
+ const lines = raw.split(/\r?\n/);
83
+ const headingIndex = lines.findIndex((line) => line.trim().length > 0);
84
+ const heading = headingIndex >= 0 ? lines[headingIndex] : void 0;
85
+ if (heading?.startsWith("# ")) return {
86
+ title: heading.slice(2).trim(),
87
+ plan: lines.slice(headingIndex + 1).join("\n").replace(/^\n+/, "").trimEnd()
88
+ };
89
+ return {
90
+ title: "",
91
+ plan: raw.trimEnd()
92
+ };
76
93
  }
94
+ /**
95
+ * Approve the plan file at `planPath`: write a timestamped copy to the global plans
96
+ * archive so approved plans are findable later. The local named plan file is left in
97
+ * place so the user can review every plan made over time.
98
+ *
99
+ * Returns the local plan filename (e.g. `add-dark-mode.md`), or `undefined` when there
100
+ * was no plan file to approve.
101
+ */
77
102
  async function approvePlanFile(opts) {
78
- const { planPath, title, resourceId, plansDir } = opts;
79
- const current = await readPlanFile(planPath);
80
- if (!current) {
81
- return void 0;
82
- }
83
- const resolvedTitle = title || current.title || "Implementation Plan";
84
- await savePlanToDisk({ title: resolvedTitle, plan: current.plan, resourceId, plansDir });
85
- return path.basename(planPath);
103
+ const { planPath, title, resourceId, plansDir } = opts;
104
+ const current = await readPlanFile(planPath);
105
+ if (!current) return;
106
+ await savePlanToDisk({
107
+ title: title || current.title || "Implementation Plan",
108
+ plan: current.plan,
109
+ resourceId,
110
+ plansDir
111
+ });
112
+ return path.basename(planPath);
86
113
  }
87
- export {
88
- approvePlanFile,
89
- getLocalPlansDir,
90
- getLocalPlansRelativeDir,
91
- getPlanFilename,
92
- getPlansDir,
93
- getSuggestedPlanRelativePath,
94
- isPlanFilePath,
95
- readPlanFile,
96
- resolvePlanPath,
97
- savePlanToDisk
98
- };
114
+ //#endregion
115
+ export { approvePlanFile, getLocalPlansDir, getLocalPlansRelativeDir, getPlanFilename, getPlansDir, getSuggestedPlanRelativePath, isPlanFilePath, readPlanFile, resolvePlanPath, savePlanToDisk };
116
+
99
117
  //# sourceMappingURL=plans.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/plans.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { DEFAULT_CONFIG_DIR } from '../constants.js';\nimport { getAppDataDir } from './project.js';\n\n/** Global plans directory for approved-plan archives. */\nexport function getPlansDir(): string {\n return process.env.MASTRA_PLANS_DIR ?? path.join(getAppDataDir(), 'plans');\n}\n\n/** Local (project-scoped) plans directory where the agent writes named plan files. */\nexport function getLocalPlansDir(projectPath: string): string {\n return path.join(projectPath, DEFAULT_CONFIG_DIR, 'plans');\n}\n\n/** Workspace-relative directory the agent writes plan files into. */\nexport function getLocalPlansRelativeDir(): string {\n return path.join(DEFAULT_CONFIG_DIR, 'plans');\n}\n\nfunction slugify(str: string): string {\n const slug = str\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n return slug || 'untitled';\n}\n\n/** Derive a plan filename from a title (e.g. `add-dark-mode.md`). */\nexport function getPlanFilename(title: string): string {\n return `${slugify(title)}.md`;\n}\n\n/**\n * Suggested workspace-relative path for a new plan file, shown in plan-mode prompts.\n * Without a title we fall back to a generic name the agent can rename later.\n */\nexport function getSuggestedPlanRelativePath(title?: string): string {\n const filename = title ? getPlanFilename(title) : 'plan.md';\n return path.join(getLocalPlansRelativeDir(), filename);\n}\n\n/**\n * Resolve a plan path submitted by the agent (absolute or project-relative) to an\n * absolute path. Returns `undefined` when no usable path was provided.\n */\nexport function resolvePlanPath(projectPath: string, submittedPath: string): string | undefined {\n if (!submittedPath) return undefined;\n return path.isAbsolute(submittedPath) ? submittedPath : path.resolve(projectPath, submittedPath);\n}\n\n/**\n * Whether `targetPath` (absolute or project-relative) is a valid plan file: a `.md`\n * file located directly inside the project's `.mastracode/plans/` directory. Used by the\n * plan-mode write guard so the agent can write any named plan file there, but nothing\n * outside that directory.\n */\nexport function isPlanFilePath(projectPath: string, targetPath: string): boolean {\n const abs = resolvePlanPath(projectPath, targetPath);\n if (!abs) return false;\n if (path.extname(abs).toLowerCase() !== '.md') return false;\n\n const plansDir = path.resolve(getLocalPlansDir(projectPath));\n const rel = path.relative(plansDir, abs);\n // Must be directly inside the plans dir (no nested subdirectories, no escaping it).\n if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) return false;\n return !rel.includes(path.sep);\n}\n\nexport async function savePlanToDisk(opts: {\n title: string;\n plan: string;\n resourceId: string;\n plansDir?: string;\n}): Promise<void> {\n const { title, plan, resourceId } = opts;\n const plansDir = opts.plansDir ?? getPlansDir();\n const baseDir = path.resolve(plansDir);\n const dir = path.resolve(baseDir, resourceId);\n const rel = path.relative(baseDir, dir);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Invalid resourceId: ${resourceId}`);\n }\n\n await fs.mkdir(dir, { recursive: true });\n\n const now = new Date();\n const timestamp = now.toISOString().replace(/:/g, '-');\n const slug = slugify(title);\n const filename = `${timestamp}-${slug}.md`;\n\n const content = `# ${title}\\n\\nApproved: ${now.toISOString()}\\n\\n${plan}\\n`;\n\n await fs.writeFile(path.join(dir, filename), content, 'utf-8');\n}\n\n/**\n * Read a plan markdown file by absolute path.\n *\n * The leading `# <title>` heading (if present) is parsed as the title and the remaining\n * content is returned as the plan body. Returns `undefined` when the file does not exist.\n */\nexport async function readPlanFile(absPath: string): Promise<{ title: string; plan: string } | undefined> {\n let raw: string;\n try {\n raw = await fs.readFile(absPath, 'utf-8');\n } catch {\n return undefined;\n }\n\n const lines = raw.split(/\\r?\\n/);\n const headingIndex = lines.findIndex(line => line.trim().length > 0);\n const heading = headingIndex >= 0 ? lines[headingIndex] : undefined;\n if (heading?.startsWith('# ')) {\n const title = heading.slice(2).trim();\n const plan = lines\n .slice(headingIndex + 1)\n .join('\\n')\n .replace(/^\\n+/, '')\n .trimEnd();\n return { title, plan };\n }\n\n return { title: '', plan: raw.trimEnd() };\n}\n\n/**\n * Approve the plan file at `planPath`: write a timestamped copy to the global plans\n * archive so approved plans are findable later. The local named plan file is left in\n * place so the user can review every plan made over time.\n *\n * Returns the local plan filename (e.g. `add-dark-mode.md`), or `undefined` when there\n * was no plan file to approve.\n */\nexport async function approvePlanFile(opts: {\n planPath: string;\n title: string;\n resourceId: string;\n plansDir?: string;\n}): Promise<string | undefined> {\n const { planPath, title, resourceId, plansDir } = opts;\n\n const current = await readPlanFile(planPath);\n if (!current) {\n return undefined;\n }\n\n const resolvedTitle = title || current.title || 'Implementation Plan';\n\n // Global archive (timestamped, never overwritten) so approved plans are findable later.\n await savePlanToDisk({ title: resolvedTitle, plan: current.plan, resourceId, plansDir });\n\n return path.basename(planPath);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"file":"plans.js","names":[],"sources":["../../src/utils/plans.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { DEFAULT_CONFIG_DIR } from '../constants.js';\nimport { getAppDataDir } from './project.js';\n\n/** Global plans directory for approved-plan archives. */\nexport function getPlansDir(): string {\n return process.env.MASTRA_PLANS_DIR ?? path.join(getAppDataDir(), 'plans');\n}\n\n/** Local (project-scoped) plans directory where the agent writes named plan files. */\nexport function getLocalPlansDir(projectPath: string): string {\n return path.join(projectPath, DEFAULT_CONFIG_DIR, 'plans');\n}\n\n/** Workspace-relative directory the agent writes plan files into. */\nexport function getLocalPlansRelativeDir(): string {\n return path.join(DEFAULT_CONFIG_DIR, 'plans');\n}\n\nfunction slugify(str: string): string {\n const slug = str\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n return slug || 'untitled';\n}\n\n/** Derive a plan filename from a title (e.g. `add-dark-mode.md`). */\nexport function getPlanFilename(title: string): string {\n return `${slugify(title)}.md`;\n}\n\n/**\n * Suggested workspace-relative path for a new plan file, shown in plan-mode prompts.\n * Without a title we fall back to a generic name the agent can rename later.\n */\nexport function getSuggestedPlanRelativePath(title?: string): string {\n const filename = title ? getPlanFilename(title) : 'plan.md';\n return path.join(getLocalPlansRelativeDir(), filename);\n}\n\n/**\n * Resolve a plan path submitted by the agent (absolute or project-relative) to an\n * absolute path. Returns `undefined` when no usable path was provided.\n */\nexport function resolvePlanPath(projectPath: string, submittedPath: string): string | undefined {\n if (!submittedPath) return undefined;\n return path.isAbsolute(submittedPath) ? submittedPath : path.resolve(projectPath, submittedPath);\n}\n\n/**\n * Whether `targetPath` (absolute or project-relative) is a valid plan file: a `.md`\n * file located directly inside the project's `.mastracode/plans/` directory. Used by the\n * plan-mode write guard so the agent can write any named plan file there, but nothing\n * outside that directory.\n */\nexport function isPlanFilePath(projectPath: string, targetPath: string): boolean {\n const abs = resolvePlanPath(projectPath, targetPath);\n if (!abs) return false;\n if (path.extname(abs).toLowerCase() !== '.md') return false;\n\n const plansDir = path.resolve(getLocalPlansDir(projectPath));\n const rel = path.relative(plansDir, abs);\n // Must be directly inside the plans dir (no nested subdirectories, no escaping it).\n if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) return false;\n return !rel.includes(path.sep);\n}\n\nexport async function savePlanToDisk(opts: {\n title: string;\n plan: string;\n resourceId: string;\n plansDir?: string;\n}): Promise<void> {\n const { title, plan, resourceId } = opts;\n const plansDir = opts.plansDir ?? getPlansDir();\n const baseDir = path.resolve(plansDir);\n const dir = path.resolve(baseDir, resourceId);\n const rel = path.relative(baseDir, dir);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(`Invalid resourceId: ${resourceId}`);\n }\n\n await fs.mkdir(dir, { recursive: true });\n\n const now = new Date();\n const timestamp = now.toISOString().replace(/:/g, '-');\n const slug = slugify(title);\n const filename = `${timestamp}-${slug}.md`;\n\n const content = `# ${title}\\n\\nApproved: ${now.toISOString()}\\n\\n${plan}\\n`;\n\n await fs.writeFile(path.join(dir, filename), content, 'utf-8');\n}\n\n/**\n * Read a plan markdown file by absolute path.\n *\n * The leading `# <title>` heading (if present) is parsed as the title and the remaining\n * content is returned as the plan body. Returns `undefined` when the file does not exist.\n */\nexport async function readPlanFile(absPath: string): Promise<{ title: string; plan: string } | undefined> {\n let raw: string;\n try {\n raw = await fs.readFile(absPath, 'utf-8');\n } catch {\n return undefined;\n }\n\n const lines = raw.split(/\\r?\\n/);\n const headingIndex = lines.findIndex(line => line.trim().length > 0);\n const heading = headingIndex >= 0 ? lines[headingIndex] : undefined;\n if (heading?.startsWith('# ')) {\n const title = heading.slice(2).trim();\n const plan = lines\n .slice(headingIndex + 1)\n .join('\\n')\n .replace(/^\\n+/, '')\n .trimEnd();\n return { title, plan };\n }\n\n return { title: '', plan: raw.trimEnd() };\n}\n\n/**\n * Approve the plan file at `planPath`: write a timestamped copy to the global plans\n * archive so approved plans are findable later. The local named plan file is left in\n * place so the user can review every plan made over time.\n *\n * Returns the local plan filename (e.g. `add-dark-mode.md`), or `undefined` when there\n * was no plan file to approve.\n */\nexport async function approvePlanFile(opts: {\n planPath: string;\n title: string;\n resourceId: string;\n plansDir?: string;\n}): Promise<string | undefined> {\n const { planPath, title, resourceId, plansDir } = opts;\n\n const current = await readPlanFile(planPath);\n if (!current) {\n return undefined;\n }\n\n const resolvedTitle = title || current.title || 'Implementation Plan';\n\n // Global archive (timestamped, never overwritten) so approved plans are findable later.\n await savePlanToDisk({ title: resolvedTitle, plan: current.plan, resourceId, plansDir });\n\n return path.basename(planPath);\n}\n"],"mappings":"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"}