@geminilight/mindos 0.1.0

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 (282) hide show
  1. package/.env.local.example +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +423 -0
  4. package/README_zh.md +423 -0
  5. package/app/README.md +152 -0
  6. package/app/app/api/ask/route.ts +170 -0
  7. package/app/app/api/ask-sessions/route.ts +90 -0
  8. package/app/app/api/auth/route.ts +37 -0
  9. package/app/app/api/backlinks/route.ts +22 -0
  10. package/app/app/api/bootstrap/route.ts +37 -0
  11. package/app/app/api/extract-pdf/route.ts +82 -0
  12. package/app/app/api/file/route.ts +138 -0
  13. package/app/app/api/files/route.ts +12 -0
  14. package/app/app/api/git/route.ts +42 -0
  15. package/app/app/api/graph/route.ts +113 -0
  16. package/app/app/api/recent-files/route.ts +10 -0
  17. package/app/app/api/search/route.ts +17 -0
  18. package/app/app/api/settings/reset-token/route.ts +21 -0
  19. package/app/app/api/settings/route.ts +123 -0
  20. package/app/app/error.tsx +33 -0
  21. package/app/app/globals.css +368 -0
  22. package/app/app/icon.svg +35 -0
  23. package/app/app/layout.tsx +103 -0
  24. package/app/app/login/page.tsx +120 -0
  25. package/app/app/page.tsx +12 -0
  26. package/app/app/view/[...path]/ViewPageClient.tsx +343 -0
  27. package/app/app/view/[...path]/error.tsx +33 -0
  28. package/app/app/view/[...path]/loading.tsx +15 -0
  29. package/app/app/view/[...path]/page.tsx +93 -0
  30. package/app/components/AskFab.tsx +59 -0
  31. package/app/components/AskModal.tsx +398 -0
  32. package/app/components/Backlinks.tsx +75 -0
  33. package/app/components/Breadcrumb.tsx +31 -0
  34. package/app/components/CsvView.tsx +325 -0
  35. package/app/components/DirView.tsx +138 -0
  36. package/app/components/Editor.tsx +124 -0
  37. package/app/components/EditorWrapper.tsx +17 -0
  38. package/app/components/ErrorBoundary.tsx +53 -0
  39. package/app/components/FileTree.tsx +369 -0
  40. package/app/components/HomeContent.tsx +262 -0
  41. package/app/components/JsonView.tsx +27 -0
  42. package/app/components/MarkdownEditor.tsx +95 -0
  43. package/app/components/MarkdownView.tsx +118 -0
  44. package/app/components/SearchModal.tsx +193 -0
  45. package/app/components/SettingsModal.tsx +237 -0
  46. package/app/components/Sidebar.tsx +136 -0
  47. package/app/components/SidebarLayout.tsx +36 -0
  48. package/app/components/TableOfContents.tsx +150 -0
  49. package/app/components/ThemeToggle.tsx +34 -0
  50. package/app/components/WysiwygEditor.tsx +75 -0
  51. package/app/components/ask/FileChip.tsx +30 -0
  52. package/app/components/ask/MentionPopover.tsx +52 -0
  53. package/app/components/ask/MessageList.tsx +126 -0
  54. package/app/components/ask/SessionHistory.tsx +49 -0
  55. package/app/components/renderers/AgentInspectorRenderer.tsx +277 -0
  56. package/app/components/renderers/BacklinksRenderer.tsx +147 -0
  57. package/app/components/renderers/ConfigRenderer.tsx +236 -0
  58. package/app/components/renderers/CsvRenderer.tsx +77 -0
  59. package/app/components/renderers/DiffRenderer.tsx +310 -0
  60. package/app/components/renderers/GraphRenderer.tsx +428 -0
  61. package/app/components/renderers/SummaryRenderer.tsx +251 -0
  62. package/app/components/renderers/TimelineRenderer.tsx +213 -0
  63. package/app/components/renderers/TodoRenderer.tsx +474 -0
  64. package/app/components/renderers/WorkflowRenderer.tsx +404 -0
  65. package/app/components/renderers/csv/BoardView.tsx +146 -0
  66. package/app/components/renderers/csv/ConfigPanel.tsx +117 -0
  67. package/app/components/renderers/csv/EditableCell.tsx +43 -0
  68. package/app/components/renderers/csv/GalleryView.tsx +40 -0
  69. package/app/components/renderers/csv/TableView.tsx +164 -0
  70. package/app/components/renderers/csv/types.ts +87 -0
  71. package/app/components/settings/AiTab.tsx +111 -0
  72. package/app/components/settings/AppearanceTab.tsx +101 -0
  73. package/app/components/settings/KnowledgeTab.tsx +157 -0
  74. package/app/components/settings/PluginsTab.tsx +82 -0
  75. package/app/components/settings/Primitives.tsx +60 -0
  76. package/app/components/settings/ShortcutsTab.tsx +22 -0
  77. package/app/components/settings/types.ts +41 -0
  78. package/app/components/ui/button.tsx +60 -0
  79. package/app/components/ui/dialog.tsx +157 -0
  80. package/app/components/ui/input.tsx +20 -0
  81. package/app/components/ui/scroll-area.tsx +55 -0
  82. package/app/components/ui/toggle.tsx +44 -0
  83. package/app/components/ui/tooltip.tsx +66 -0
  84. package/app/components.json +25 -0
  85. package/app/data/pages/home-dark.png +0 -0
  86. package/app/data/pages/home-mobile-crop.png +0 -0
  87. package/app/data/pages/home-mobile.png +0 -0
  88. package/app/data/pages/home.png +0 -0
  89. package/app/data/pages/view-dir.png +0 -0
  90. package/app/data/pages/view-file-bot.png +0 -0
  91. package/app/data/pages/view-file-dark-crop.png +0 -0
  92. package/app/data/pages/view-file-dark.png +0 -0
  93. package/app/data/pages/view-file-mobile.png +0 -0
  94. package/app/data/pages/view-file-sm.png +0 -0
  95. package/app/data/pages/view-file-top.png +0 -0
  96. package/app/data/pages/view-file.png +0 -0
  97. package/app/eslint.config.mjs +18 -0
  98. package/app/hooks/useAskSession.ts +181 -0
  99. package/app/hooks/useFileUpload.ts +126 -0
  100. package/app/hooks/useMention.ts +65 -0
  101. package/app/lib/LocaleContext.tsx +40 -0
  102. package/app/lib/actions.ts +40 -0
  103. package/app/lib/agent/index.ts +3 -0
  104. package/app/lib/agent/model.ts +18 -0
  105. package/app/lib/agent/prompt.ts +32 -0
  106. package/app/lib/agent/tools.ts +151 -0
  107. package/app/lib/api.ts +55 -0
  108. package/app/lib/core/backlinks.ts +40 -0
  109. package/app/lib/core/csv.ts +28 -0
  110. package/app/lib/core/fs-ops.ts +118 -0
  111. package/app/lib/core/git.ts +50 -0
  112. package/app/lib/core/index.ts +58 -0
  113. package/app/lib/core/lines.ts +89 -0
  114. package/app/lib/core/search.ts +79 -0
  115. package/app/lib/core/security.ts +43 -0
  116. package/app/lib/core/tree.ts +113 -0
  117. package/app/lib/core/types.ts +40 -0
  118. package/app/lib/fs.ts +467 -0
  119. package/app/lib/i18n.ts +300 -0
  120. package/app/lib/jwt.ts +58 -0
  121. package/app/lib/renderers/index.ts +79 -0
  122. package/app/lib/renderers/registry.ts +70 -0
  123. package/app/lib/settings.ts +150 -0
  124. package/app/lib/types.ts +32 -0
  125. package/app/lib/utils.ts +34 -0
  126. package/app/next-env.d.ts +6 -0
  127. package/app/next.config.ts +10 -0
  128. package/app/package-lock.json +15306 -0
  129. package/app/package.json +71 -0
  130. package/app/postcss.config.mjs +7 -0
  131. package/app/proxy.ts +64 -0
  132. package/app/public/file.svg +1 -0
  133. package/app/public/globe.svg +1 -0
  134. package/app/public/landing/index.html +353 -0
  135. package/app/public/landing/style.css +216 -0
  136. package/app/public/logo-square.svg +37 -0
  137. package/app/public/logo.svg +37 -0
  138. package/app/public/next.svg +1 -0
  139. package/app/public/vercel.svg +1 -0
  140. package/app/public/window.svg +1 -0
  141. package/app/scripts/extract-pdf.cjs +56 -0
  142. package/app/tsconfig.json +34 -0
  143. package/app/vitest.config.ts +14 -0
  144. package/assets/demo-flow-zh.html +622 -0
  145. package/assets/images/demo-flow-dark.png +0 -0
  146. package/assets/images/demo-flow-light.png +0 -0
  147. package/assets/images/demo-flow-zh-dark.png +0 -0
  148. package/assets/images/demo-flow-zh-light.png +0 -0
  149. package/assets/images/gui-sync-cv.png +0 -0
  150. package/assets/logo-square.svg +37 -0
  151. package/bin/cli.js +894 -0
  152. package/mcp/README.md +113 -0
  153. package/mcp/package-lock.json +1717 -0
  154. package/mcp/package.json +18 -0
  155. package/mcp/src/index.ts +494 -0
  156. package/mcp/tsconfig.json +13 -0
  157. package/package.json +49 -0
  158. package/scripts/setup.js +675 -0
  159. package/scripts/upgrade-prompt.md +147 -0
  160. package/skills/mindos/SKILL.md +319 -0
  161. package/skills/mindos-zh/SKILL.md +318 -0
  162. package/templates/README.md +31 -0
  163. package/templates/empty/CHANGELOG.md +9 -0
  164. package/templates/empty/CONFIG.json +197 -0
  165. package/templates/empty/CONFIG.md +73 -0
  166. package/templates/empty/INSTRUCTION.md +177 -0
  167. package/templates/empty/README.md +27 -0
  168. package/templates/en/CHANGELOG.md +9 -0
  169. package/templates/en/CONFIG.json +197 -0
  170. package/templates/en/CONFIG.md +73 -0
  171. package/templates/en/INSTRUCTION.md +177 -0
  172. package/templates/en/README.md +27 -0
  173. package/templates/en/TODO.md +13 -0
  174. package/templates/en//360/237/221/244 Profile/INSTRUCTION.md" +21 -0
  175. package/templates/en//360/237/221/244 Profile/README.md" +15 -0
  176. package/templates/en//360/237/221/244 Profile//342/232/231/357/270/217 Preferences.md" +21 -0
  177. package/templates/en//360/237/221/244 Profile//360/237/216/257 Focus.md" +31 -0
  178. package/templates/en//360/237/221/244 Profile//360/237/221/244 Identity.md" +22 -0
  179. package/templates/en//360/237/223/232 Resources/INSTRUCTION.md" +29 -0
  180. package/templates/en//360/237/223/232 Resources/README.md" +21 -0
  181. package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Influencers.csv" +1 -0
  182. package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Products.csv" +1 -0
  183. package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Scholars.csv" +1 -0
  184. package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Tools.csv" +1 -0
  185. package/templates/en//360/237/223/235 Notes/INSTRUCTION.md" +31 -0
  186. package/templates/en//360/237/223/235 Notes/Ideas/README.md" +8 -0
  187. package/templates/en//360/237/223/235 Notes/Ideas//360/237/247/252_example_product_idea.md" +16 -0
  188. package/templates/en//360/237/223/235 Notes/Inbox/README.md" +8 -0
  189. package/templates/en//360/237/223/235 Notes/Inbox//360/237/247/252_example_quick_capture.md" +14 -0
  190. package/templates/en//360/237/223/235 Notes/Meetings/README.md" +8 -0
  191. package/templates/en//360/237/223/235 Notes/Meetings//360/237/247/252_example_meeting_note.md" +17 -0
  192. package/templates/en//360/237/223/235 Notes/README.md" +24 -0
  193. package/templates/en//360/237/223/235 Notes/Waiting/README.md" +8 -0
  194. package/templates/en//360/237/223/235 Notes/Waiting//360/237/247/252_example_blocked_item.md" +16 -0
  195. package/templates/en//360/237/224/204 Workflows/Configurations/README.md" +3 -0
  196. package/templates/en//360/237/224/204 Workflows/Configurations//360/237/247/252_example_config_update_sop.md" +14 -0
  197. package/templates/en//360/237/224/204 Workflows/INSTRUCTION.md" +21 -0
  198. package/templates/en//360/237/224/204 Workflows/Information/README.md" +16 -0
  199. package/templates/en//360/237/224/204 Workflows/Information//360/237/247/252_example_info_capture_sop.md" +13 -0
  200. package/templates/en//360/237/224/204 Workflows/Media/README.md" +16 -0
  201. package/templates/en//360/237/224/204 Workflows/Media//360/237/247/252_example_content_publish_sop.md" +13 -0
  202. package/templates/en//360/237/224/204 Workflows/README.md" +22 -0
  203. package/templates/en//360/237/224/204 Workflows/Research/README.md" +16 -0
  204. package/templates/en//360/237/224/204 Workflows/Research//360/237/247/252_example_lit_review_sop.md" +16 -0
  205. package/templates/en//360/237/224/204 Workflows/Startup/README.md" +3 -0
  206. package/templates/en//360/237/224/204 Workflows/Startup//360/237/247/252_example_weekly_founder_ops.md" +22 -0
  207. package/templates/en//360/237/224/227 Connections/Classmates/README.md" +11 -0
  208. package/templates/en//360/237/224/227 Connections/Classmates//360/237/247/252_example_leo_chen.md" +16 -0
  209. package/templates/en//360/237/224/227 Connections/Colleagues/README.md" +11 -0
  210. package/templates/en//360/237/224/227 Connections/Colleagues//360/237/247/252_example_ethan_zhao.md" +16 -0
  211. package/templates/en//360/237/224/227 Connections/Connections Overview.csv" +5 -0
  212. package/templates/en//360/237/224/227 Connections/Family/README.md" +11 -0
  213. package/templates/en//360/237/224/227 Connections/Family//360/237/247/252_example_james_wang.md" +16 -0
  214. package/templates/en//360/237/224/227 Connections/Friends/README.md" +11 -0
  215. package/templates/en//360/237/224/227 Connections/Friends//360/237/247/252_example_lily_lin.md" +16 -0
  216. package/templates/en//360/237/224/227 Connections/INSTRUCTION.md" +56 -0
  217. package/templates/en//360/237/224/227 Connections/README.md" +20 -0
  218. package/templates/en//360/237/232/200 Projects/Archived/README.md" +9 -0
  219. package/templates/en//360/237/232/200 Projects/Archived//360/237/247/252_example_archived_project_note.md" +14 -0
  220. package/templates/en//360/237/232/200 Projects/INSTRUCTION.md" +29 -0
  221. package/templates/en//360/237/232/200 Projects/Products/README.md" +16 -0
  222. package/templates/en//360/237/232/200 Projects/Products//360/237/247/252_example_product_project_brief.md" +20 -0
  223. package/templates/en//360/237/232/200 Projects/README.md" +21 -0
  224. package/templates/en//360/237/232/200 Projects/Research/README.md" +16 -0
  225. package/templates/en//360/237/232/200 Projects/Research//360/237/247/252_example_research_project_brief.md" +16 -0
  226. package/templates/template-generation-skill.md +79 -0
  227. package/templates/zh/CHANGELOG.md +9 -0
  228. package/templates/zh/CONFIG.json +197 -0
  229. package/templates/zh/CONFIG.md +66 -0
  230. package/templates/zh/INSTRUCTION.md +177 -0
  231. package/templates/zh/README.md +27 -0
  232. package/templates/zh/TODO.md +13 -0
  233. package/templates/zh//360/237/221/244 /347/224/273/345/203/217/INSTRUCTION.md" +28 -0
  234. package/templates/zh//360/237/221/244 /347/224/273/345/203/217/README.md" +20 -0
  235. package/templates/zh//360/237/221/244 /347/224/273/345/203/217//342/232/231/357/270/217 Preferences.md" +21 -0
  236. package/templates/zh//360/237/221/244 /347/224/273/345/203/217//360/237/216/257 Focus.md" +31 -0
  237. package/templates/zh//360/237/221/244 /347/224/273/345/203/217//360/237/221/244 Identity.md" +22 -0
  238. package/templates/zh//360/237/223/232 /350/265/204/346/272/220/INSTRUCTION.md" +29 -0
  239. package/templates/zh//360/237/223/232 /350/265/204/346/272/220/README.md" +21 -0
  240. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI Inferencers.csv" +1 -0
  241. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI /344/272/247/345/223/201.csv" +1 -0
  242. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI /345/255/246/350/200/205/346/270/205/345/215/225.csv" +1 -0
  243. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI /345/267/245/345/205/267/346/270/205/345/215/225.csv" +1 -0
  244. package/templates/zh//360/237/223/235 /347/254/224/350/256/260/INSTRUCTION.md" +31 -0
  245. package/templates/zh//360/237/223/235 /347/254/224/350/256/260/README.md" +24 -0
  246. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//344/274/232/350/256/256/README.md" +8 -0
  247. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//344/274/232/350/256/256//360/237/247/252_example_/344/274/232/350/256/256/347/272/252/350/246/201.md" +17 -0
  248. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//345/276/205/345/217/215/351/246/210/README.md" +8 -0
  249. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//345/276/205/345/217/215/351/246/210//360/237/247/252_example_/345/276/205/345/217/215/351/246/210/344/272/213/351/241/271.md" +16 -0
  250. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//346/203/263/346/263/225/README.md" +8 -0
  251. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//346/203/263/346/263/225//360/237/247/252_example_/344/272/247/345/223/201/346/203/263/346/263/225.md" +16 -0
  252. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//346/224/266/344/273/266/347/256/261/README.md" +8 -0
  253. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//346/224/266/344/273/266/347/256/261//360/237/247/252_example_/344/270/264/346/227/266/351/200/237/350/256/260.md" +13 -0
  254. package/templates/zh//360/237/224/204 /346/265/201/347/250/213/INSTRUCTION.md" +29 -0
  255. package/templates/zh//360/237/224/204 /346/265/201/347/250/213/README.md" +21 -0
  256. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//344/277/241/346/201/257/README.md" +16 -0
  257. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//344/277/241/346/201/257//360/237/247/252_example_/344/277/241/346/201/257/351/207/207/351/233/206/346/265/201/347/250/213.md" +13 -0
  258. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//345/252/222/344/275/223/README.md" +16 -0
  259. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//345/252/222/344/275/223//360/237/247/252_example_/345/206/205/345/256/271/345/217/221/345/270/203/346/265/201/347/250/213.md" +13 -0
  260. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//347/247/221/347/240/224/README.md" +16 -0
  261. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//347/247/221/347/240/224//360/237/247/252_example_/346/226/207/347/214/256/347/273/274/350/277/260/346/265/201/347/250/213.md" +16 -0
  262. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//351/205/215/347/275/256/README.md" +3 -0
  263. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//351/205/215/347/275/256//360/237/247/252_example_/351/205/215/347/275/256/346/233/264/346/226/260/346/265/201/347/250/213.md" +26 -0
  264. package/templates/zh//360/237/224/227 /345/205/263/347/263/273/INSTRUCTION.md" +62 -0
  265. package/templates/zh//360/237/224/227 /345/205/263/347/263/273/README.md" +20 -0
  266. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/205/263/347/263/273/346/200/273/350/247/210.csv" +5 -0
  267. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/220/214/344/272/213/README.md" +11 -0
  268. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/220/214/344/272/213//360/237/247/252_example_/345/220/214/344/272/213/350/265/265/344/270/200/350/276/260.md" +16 -0
  269. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/220/214/345/255/246/README.md" +11 -0
  270. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/220/214/345/255/246//360/237/247/252_example_/345/220/214/345/255/246/351/231/210/347/253/213/346/254/247.md" +16 -0
  271. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/256/266/344/272/272/README.md" +11 -0
  272. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/256/266/344/272/272//360/237/247/252_example_/345/256/266/344/272/272/347/216/213/345/273/272/345/233/275.md" +16 -0
  273. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//346/234/213/345/217/213/README.md" +11 -0
  274. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//346/234/213/345/217/213//360/237/247/252_example_/346/234/213/345/217/213/346/236/227/345/260/217/344/270/275.md" +16 -0
  275. package/templates/zh//360/237/232/200 /351/241/271/347/233/256/INSTRUCTION.md" +31 -0
  276. package/templates/zh//360/237/232/200 /351/241/271/347/233/256/README.md" +21 -0
  277. package/templates/zh//360/237/232/200 /351/241/271/347/233/256//344/272/247/345/223/201/README.md" +16 -0
  278. package/templates/zh//360/237/232/200 /351/241/271/347/233/256//344/272/247/345/223/201//360/237/247/252_example_/344/272/247/345/223/201/351/241/271/347/233/256/347/256/200/346/212/245.md" +20 -0
  279. package/templates/zh//360/237/232/200 /351/241/271/347/233/256//345/267/262/345/275/222/346/241/243/README.md" +9 -0
  280. package/templates/zh//360/237/232/200 /351/241/271/347/233/256//345/267/262/345/275/222/346/241/243//360/237/247/252_example_/345/275/222/346/241/243/351/241/271/347/233/256/350/256/260/345/275/225.md" +15 -0
  281. package/templates/zh//360/237/232/200 /351/241/271/347/233/256//347/247/221/347/240/224/README.md" +16 -0
  282. package/templates/zh//360/237/232/200 /351/241/271/347/233/256//347/247/221/347/240/224//360/237/247/252_example_/347/247/221/347/240/224/351/241/271/347/233/256/347/256/200/346/212/245.md" +16 -0
@@ -0,0 +1,32 @@
1
+ // Agent system prompt — v2: uploaded-file awareness + pdfjs extraction fix
2
+ export const AGENT_SYSTEM_PROMPT = `You are MindOS Agent — an execution-oriented AI assistant for a personal knowledge base.
3
+
4
+ Runtime capabilities already available in this request:
5
+ - bootstrap context (MindOS startup files) is auto-loaded by the server
6
+ - mindos skill guidance is auto-loaded by the server
7
+ - knowledge-base tools are available for file operations
8
+
9
+ How to operate:
10
+ 1. Treat the auto-loaded bootstrap + skill context as your initialization baseline.
11
+ 2. If the task needs fresher or broader evidence, call tools proactively (list/search/read) before concluding.
12
+ 3. Execute edits safely and minimally, then verify outcomes.
13
+
14
+ Tool policy:
15
+ - Always read a file before modifying it.
16
+ - Use search/list tools first when file location is unclear.
17
+ - Prefer targeted edits (update_section / insert_after_heading / append_to_file) over full overwrite.
18
+ - Use write_file only when replacing the whole file is required.
19
+ - INSTRUCTION.md is read-only and must not be modified.
20
+
21
+ Uploaded files:
22
+ - Users may upload local files (PDF, txt, csv, etc.) via the chat interface.
23
+ - The content of uploaded files is ALREADY INCLUDED in this system prompt in a dedicated "⚠️ USER-UPLOADED FILES" section near the end.
24
+ - IMPORTANT: When the user references an uploaded file (e.g. a resume/CV, a report, a document), you MUST use the content from that section directly. Extract specific details, quote relevant passages, and demonstrate that you have read the file thoroughly.
25
+ - Do NOT attempt to use read_file or search tools to find uploaded files — they do not exist in the knowledge base. They are ONLY available in the uploaded files section of this prompt.
26
+ - If the uploaded files section is empty or missing, tell the user the upload may have failed and ask them to re-upload.
27
+
28
+ Response policy:
29
+ - Answer in the user's language.
30
+ - Be concise, concrete, and action-oriented.
31
+ - Use Markdown for structure when it improves clarity.
32
+ - When relevant, explicitly state whether initialization context appears sufficient or if additional tool reads were needed.`;
@@ -0,0 +1,151 @@
1
+ import { tool } from 'ai';
2
+ import { z } from 'zod';
3
+ import {
4
+ searchFiles, getFileContent, getFileTree, getRecentlyModified,
5
+ saveFileContent, createFile, appendToFile, insertAfterHeading, updateSection,
6
+ } from '@/lib/fs';
7
+ import { assertNotProtected } from '@/lib/core';
8
+
9
+ // Max chars per file to avoid token overflow (~100k chars ≈ ~25k tokens)
10
+ const MAX_FILE_CHARS = 20_000;
11
+
12
+ export function truncate(content: string): string {
13
+ if (content.length <= MAX_FILE_CHARS) return content;
14
+ return content.slice(0, MAX_FILE_CHARS) + `\n\n[...truncated — file is ${content.length} chars, showing first ${MAX_FILE_CHARS}]`;
15
+ }
16
+
17
+ /** Checks write-protection using core's assertNotProtected */
18
+ export function assertWritable(filePath: string): void {
19
+ assertNotProtected(filePath, 'modified by AI agent');
20
+ }
21
+
22
+ // ─── Knowledge base tools ─────────────────────────────────────────────────────
23
+
24
+ export const knowledgeBaseTools = {
25
+ list_files: tool({
26
+ description: 'List the full file tree of the knowledge base. Use this to browse what files exist.',
27
+ inputSchema: z.object({}),
28
+ execute: async () => {
29
+ const tree = getFileTree();
30
+ return JSON.stringify(tree, null, 2);
31
+ },
32
+ }),
33
+
34
+ read_file: tool({
35
+ description: 'Read the content of a file by its relative path. Always read a file before modifying it.',
36
+ inputSchema: z.object({ path: z.string().describe('Relative file path, e.g. "Profile/👤 Identity.md"') }),
37
+ execute: async ({ path }) => {
38
+ try {
39
+ return truncate(getFileContent(path));
40
+ } catch (e: unknown) {
41
+ return `Error: ${e instanceof Error ? e.message : String(e)}`;
42
+ }
43
+ },
44
+ }),
45
+
46
+ search: tool({
47
+ description: 'Full-text search across all files in the knowledge base. Returns matching files with context snippets.',
48
+ inputSchema: z.object({ query: z.string().describe('Search query (case-insensitive)') }),
49
+ execute: async ({ query }) => {
50
+ const results = searchFiles(query);
51
+ if (results.length === 0) return 'No results found.';
52
+ return results.map(r => `- **${r.path}**: ${r.snippet}`).join('\n');
53
+ },
54
+ }),
55
+
56
+ get_recent: tool({
57
+ description: 'Get the most recently modified files in the knowledge base.',
58
+ inputSchema: z.object({ limit: z.number().min(1).max(50).default(10).describe('Number of files to return') }),
59
+ execute: async ({ limit }) => {
60
+ const files = getRecentlyModified(limit);
61
+ return files.map(f => `- ${f.path} (${new Date(f.mtime).toISOString()})`).join('\n');
62
+ },
63
+ }),
64
+
65
+ write_file: tool({
66
+ description: 'Overwrite the entire content of an existing file. Use read_file first to see current content. Prefer update_section or insert_after_heading for partial edits.',
67
+ inputSchema: z.object({
68
+ path: z.string().describe('Relative file path'),
69
+ content: z.string().describe('New full content'),
70
+ }),
71
+ execute: async ({ path, content }) => {
72
+ try {
73
+ assertWritable(path);
74
+ saveFileContent(path, content);
75
+ return `File written: ${path}`;
76
+ } catch (e: unknown) {
77
+ return `Error: ${e instanceof Error ? e.message : String(e)}`;
78
+ }
79
+ },
80
+ }),
81
+
82
+ create_file: tool({
83
+ description: 'Create a new file. Only .md and .csv files are allowed. Parent directories are created automatically.',
84
+ inputSchema: z.object({
85
+ path: z.string().describe('Relative file path (must end in .md or .csv)'),
86
+ content: z.string().default('').describe('Initial file content'),
87
+ }),
88
+ execute: async ({ path, content }) => {
89
+ try {
90
+ assertWritable(path);
91
+ createFile(path, content);
92
+ return `File created: ${path}`;
93
+ } catch (e: unknown) {
94
+ return `Error: ${e instanceof Error ? e.message : String(e)}`;
95
+ }
96
+ },
97
+ }),
98
+
99
+ append_to_file: tool({
100
+ description: 'Append text to the end of an existing file. A blank line separator is added automatically.',
101
+ inputSchema: z.object({
102
+ path: z.string().describe('Relative file path'),
103
+ content: z.string().describe('Content to append'),
104
+ }),
105
+ execute: async ({ path, content }) => {
106
+ try {
107
+ assertWritable(path);
108
+ appendToFile(path, content);
109
+ return `Content appended to: ${path}`;
110
+ } catch (e: unknown) {
111
+ return `Error: ${e instanceof Error ? e.message : String(e)}`;
112
+ }
113
+ },
114
+ }),
115
+
116
+ insert_after_heading: tool({
117
+ description: 'Insert content right after a Markdown heading. Useful for adding items under a specific section.',
118
+ inputSchema: z.object({
119
+ path: z.string().describe('Relative file path'),
120
+ heading: z.string().describe('Heading text to find (e.g. "## Tasks" or just "Tasks")'),
121
+ content: z.string().describe('Content to insert after the heading'),
122
+ }),
123
+ execute: async ({ path, heading, content }) => {
124
+ try {
125
+ assertWritable(path);
126
+ insertAfterHeading(path, heading, content);
127
+ return `Content inserted after heading "${heading}" in ${path}`;
128
+ } catch (e: unknown) {
129
+ return `Error: ${e instanceof Error ? e.message : String(e)}`;
130
+ }
131
+ },
132
+ }),
133
+
134
+ update_section: tool({
135
+ description: 'Replace the content of a Markdown section identified by its heading. The section spans from the heading to the next heading of equal or higher level.',
136
+ inputSchema: z.object({
137
+ path: z.string().describe('Relative file path'),
138
+ heading: z.string().describe('Heading text to find (e.g. "## Status")'),
139
+ content: z.string().describe('New content for the section'),
140
+ }),
141
+ execute: async ({ path, heading, content }) => {
142
+ try {
143
+ assertWritable(path);
144
+ updateSection(path, heading, content);
145
+ return `Section "${heading}" updated in ${path}`;
146
+ } catch (e: unknown) {
147
+ return `Error: ${e instanceof Error ? e.message : String(e)}`;
148
+ }
149
+ },
150
+ }),
151
+ };
package/app/lib/api.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Typed fetch wrapper with error handling and optional timeout.
3
+ *
4
+ * - Checks `res.ok` and throws on non-2xx status.
5
+ * - Extracts `{ error }` from JSON error responses when available.
6
+ * - Supports AbortController timeout (default 30s).
7
+ */
8
+
9
+ export class ApiError extends Error {
10
+ status: number;
11
+ constructor(message: string, status: number) {
12
+ super(message);
13
+ this.name = 'ApiError';
14
+ this.status = status;
15
+ }
16
+ }
17
+
18
+ interface ApiFetchOptions extends Omit<RequestInit, 'signal'> {
19
+ /** Timeout in ms (default 30000). Set to 0 to disable. */
20
+ timeout?: number;
21
+ /** External AbortSignal (merged with timeout signal). */
22
+ signal?: AbortSignal;
23
+ }
24
+
25
+ export async function apiFetch<T>(url: string, opts: ApiFetchOptions = {}): Promise<T> {
26
+ const { timeout = 30_000, signal: externalSignal, ...fetchOpts } = opts;
27
+
28
+ let controller: AbortController | undefined;
29
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
30
+
31
+ if (timeout > 0) {
32
+ controller = new AbortController();
33
+ timeoutId = setTimeout(() => controller!.abort(), timeout);
34
+ }
35
+
36
+ // Merge external signal if provided
37
+ const signal = externalSignal ?? controller?.signal;
38
+
39
+ try {
40
+ const res = await fetch(url, { ...fetchOpts, signal });
41
+
42
+ if (!res.ok) {
43
+ let msg = `Request failed (${res.status})`;
44
+ try {
45
+ const body = await res.json();
46
+ if (body?.error) msg = body.error;
47
+ } catch { /* non-JSON error body */ }
48
+ throw new ApiError(msg, res.status);
49
+ }
50
+
51
+ return (await res.json()) as T;
52
+ } finally {
53
+ if (timeoutId) clearTimeout(timeoutId);
54
+ }
55
+ }
@@ -0,0 +1,40 @@
1
+ import path from 'path';
2
+ import { collectAllFiles } from './tree';
3
+ import { readFile } from './fs-ops';
4
+ import type { BacklinkEntry } from './types';
5
+
6
+ /**
7
+ * Finds files that reference the given targetPath via wikilinks,
8
+ * markdown links, or backtick references.
9
+ */
10
+ export function findBacklinks(mindRoot: string, targetPath: string): BacklinkEntry[] {
11
+ const allFiles = collectAllFiles(mindRoot).filter(f => f.endsWith('.md') && f !== targetPath);
12
+ const results: BacklinkEntry[] = [];
13
+ const bname = path.basename(targetPath, '.md');
14
+ const escapedTarget = targetPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
15
+ const escapedBname = bname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16
+
17
+ const patterns = [
18
+ new RegExp(`\\[\\[${escapedBname}(?:[|#][^\\]]*)?\\]\\]`, 'i'),
19
+ new RegExp(`\\[\\[${escapedTarget}(?:[|#][^\\]]*)?\\]\\]`, 'i'),
20
+ new RegExp(`\\[[^\\]]+\\]\\(${escapedTarget}(?:#[^)]*)?\\)`, 'i'),
21
+ new RegExp(`\\[[^\\]]+\\]\\([^)]*${escapedBname}\\.md(?:#[^)]*)?\\)`, 'i'),
22
+ new RegExp('`' + escapedTarget.replace(/\//g, '\\/') + '`'),
23
+ ];
24
+
25
+ for (const filePath of allFiles) {
26
+ let content: string;
27
+ try { content = readFile(mindRoot, filePath); } catch { continue; }
28
+ const lines = content.split('\n');
29
+ for (let i = 0; i < lines.length; i++) {
30
+ if (patterns.some(p => p.test(lines[i]))) {
31
+ const start = Math.max(0, i - 1);
32
+ const end = Math.min(lines.length - 1, i + 1);
33
+ const ctx = lines.slice(start, end + 1).join('\n').trim();
34
+ results.push({ source: filePath, line: i + 1, context: ctx });
35
+ break;
36
+ }
37
+ }
38
+ }
39
+ return results;
40
+ }
@@ -0,0 +1,28 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { resolveSafe } from './security';
4
+
5
+ /**
6
+ * Appends a single row to a CSV file with RFC 4180 escaping.
7
+ * Creates parent directories if they don't exist.
8
+ * Returns the new total row count.
9
+ */
10
+ export function appendCsvRow(mindRoot: string, filePath: string, row: string[]): { newRowCount: number } {
11
+ const resolved = resolveSafe(mindRoot, filePath);
12
+ if (!filePath.endsWith('.csv')) throw new Error('Only .csv files support row append');
13
+
14
+ const escaped = row.map((cell) => {
15
+ if (cell.includes(',') || cell.includes('"') || cell.includes('\n')) {
16
+ return `"${cell.replace(/"/g, '""')}"`;
17
+ }
18
+ return cell;
19
+ });
20
+ const line = escaped.join(',') + '\n';
21
+
22
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
23
+ fs.appendFileSync(resolved, line, 'utf-8');
24
+
25
+ const content = fs.readFileSync(resolved, 'utf-8');
26
+ const newRowCount = content.trim().split('\n').length;
27
+ return { newRowCount };
28
+ }
@@ -0,0 +1,118 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { resolveSafe, assertWithinRoot } from './security';
4
+
5
+ /**
6
+ * Reads the content of a file given a relative path from mindRoot.
7
+ */
8
+ export function readFile(mindRoot: string, filePath: string): string {
9
+ const resolved = resolveSafe(mindRoot, filePath);
10
+ return fs.readFileSync(resolved, 'utf-8');
11
+ }
12
+
13
+ /**
14
+ * Atomically writes content to a file (temp file + rename).
15
+ * Creates parent directories as needed.
16
+ */
17
+ export function writeFile(mindRoot: string, filePath: string, content: string): void {
18
+ const resolved = resolveSafe(mindRoot, filePath);
19
+ const dir = path.dirname(resolved);
20
+ const tmp = path.join(dir, `.tmp-${Date.now()}-${path.basename(resolved)}`);
21
+ try {
22
+ fs.mkdirSync(dir, { recursive: true });
23
+ fs.writeFileSync(tmp, content, 'utf-8');
24
+ fs.renameSync(tmp, resolved);
25
+ } catch (err) {
26
+ try { fs.unlinkSync(tmp); } catch { /* ignore */ }
27
+ throw err;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Creates a new file. Throws if the file already exists.
33
+ * Creates parent directories as needed.
34
+ */
35
+ export function createFile(mindRoot: string, filePath: string, initialContent = ''): void {
36
+ const resolved = resolveSafe(mindRoot, filePath);
37
+ if (fs.existsSync(resolved)) {
38
+ throw new Error(`File already exists: ${filePath}`);
39
+ }
40
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
41
+ fs.writeFileSync(resolved, initialContent, 'utf-8');
42
+ }
43
+
44
+ /**
45
+ * Deletes a file. Throws if the file does not exist.
46
+ */
47
+ export function deleteFile(mindRoot: string, filePath: string): void {
48
+ const resolved = resolveSafe(mindRoot, filePath);
49
+ if (!fs.existsSync(resolved)) {
50
+ throw new Error(`File not found: ${filePath}`);
51
+ }
52
+ fs.unlinkSync(resolved);
53
+ }
54
+
55
+ /**
56
+ * Renames a file within its current directory.
57
+ * newName must be a plain filename (no path separators).
58
+ * Returns the new relative path.
59
+ */
60
+ export function renameFile(mindRoot: string, oldPath: string, newName: string): string {
61
+ if (newName.includes('/') || newName.includes('\\')) {
62
+ throw new Error('Invalid filename: must not contain path separators');
63
+ }
64
+ const root = path.resolve(mindRoot);
65
+ const oldResolved = path.resolve(path.join(root, oldPath));
66
+ assertWithinRoot(oldResolved, root);
67
+
68
+ const dir = path.dirname(oldResolved);
69
+ const newResolved = path.join(dir, newName);
70
+ assertWithinRoot(newResolved, root);
71
+
72
+ if (fs.existsSync(newResolved)) {
73
+ throw new Error('A file with that name already exists');
74
+ }
75
+ fs.renameSync(oldResolved, newResolved);
76
+ return path.relative(root, newResolved);
77
+ }
78
+
79
+ /**
80
+ * Moves a file from one path to another within mindRoot.
81
+ * Returns the new path and a list of files that referenced the old path.
82
+ */
83
+ export function moveFile(
84
+ mindRoot: string,
85
+ fromPath: string,
86
+ toPath: string,
87
+ findBacklinksFn: (mindRoot: string, targetPath: string) => Array<{ source: string }>
88
+ ): { newPath: string; affectedFiles: string[] } {
89
+ const fromResolved = resolveSafe(mindRoot, fromPath);
90
+ const toResolved = resolveSafe(mindRoot, toPath);
91
+ if (!fs.existsSync(fromResolved)) throw new Error(`Source not found: ${fromPath}`);
92
+ if (fs.existsSync(toResolved)) throw new Error(`Destination already exists: ${toPath}`);
93
+ fs.mkdirSync(path.dirname(toResolved), { recursive: true });
94
+ fs.renameSync(fromResolved, toResolved);
95
+ const backlinks = findBacklinksFn(mindRoot, fromPath);
96
+ return { newPath: toPath, affectedFiles: backlinks.map(b => b.source) };
97
+ }
98
+
99
+ /**
100
+ * Returns files sorted by modification time, descending.
101
+ */
102
+ export function getRecentlyModified(
103
+ mindRoot: string,
104
+ allFiles: string[],
105
+ limit = 10
106
+ ): Array<{ path: string; mtime: number; mtimeISO: string }> {
107
+ const withMtime = allFiles.flatMap((filePath) => {
108
+ try {
109
+ const abs = path.join(mindRoot, filePath);
110
+ const stat = fs.statSync(abs);
111
+ return [{ path: filePath, mtime: stat.mtimeMs, mtimeISO: stat.mtime.toISOString() }];
112
+ } catch {
113
+ return [];
114
+ }
115
+ });
116
+ withMtime.sort((a, b) => b.mtime - a.mtime);
117
+ return withMtime.slice(0, limit);
118
+ }
@@ -0,0 +1,50 @@
1
+ import { execSync } from 'child_process';
2
+ import { resolveSafe } from './security';
3
+ import type { GitLogEntry } from './types';
4
+
5
+ /**
6
+ * Checks if the mindRoot directory is inside a git repository.
7
+ */
8
+ export function isGitRepo(mindRoot: string): boolean {
9
+ try {
10
+ execSync('git rev-parse --is-inside-work-tree', { cwd: mindRoot, stdio: 'pipe' });
11
+ return true;
12
+ } catch { return false; }
13
+ }
14
+
15
+ /**
16
+ * Returns git log entries for a given file.
17
+ */
18
+ export function gitLog(mindRoot: string, filePath: string, limit: number): GitLogEntry[] {
19
+ const resolved = resolveSafe(mindRoot, filePath);
20
+ const output = execSync(
21
+ `git log --follow --format="%H%x00%aI%x00%s%x00%an" -n ${limit} -- "${resolved}"`,
22
+ { cwd: mindRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
23
+ ).trim();
24
+ if (!output) return [];
25
+ return output.split('\n').map(line => {
26
+ const [hash, date, message, author] = line.split('\0');
27
+ return { hash, date, message, author };
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Returns the content of a file at a specific git commit.
33
+ */
34
+ export function gitShowFile(mindRoot: string, filePath: string, commitHash: string): string {
35
+ const resolved = resolveSafe(mindRoot, filePath);
36
+ const relFromGitRoot = execSync(
37
+ `git ls-files --full-name "${resolved}"`,
38
+ { cwd: mindRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
39
+ ).trim();
40
+ if (!relFromGitRoot) {
41
+ return execSync(
42
+ `git show ${commitHash}:"${filePath}"`,
43
+ { cwd: mindRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
44
+ );
45
+ }
46
+ return execSync(
47
+ `git show ${commitHash}:"${relFromGitRoot}"`,
48
+ { cwd: mindRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
49
+ );
50
+ }
@@ -0,0 +1,58 @@
1
+ // Types
2
+ export type {
3
+ FileNode,
4
+ SearchResult,
5
+ BacklinkEntry,
6
+ SearchOptions,
7
+ GitLogEntry,
8
+ MoveResult,
9
+ } from './types';
10
+
11
+ // Security
12
+ export {
13
+ assertWithinRoot,
14
+ resolveSafe,
15
+ isRootProtected,
16
+ assertNotProtected,
17
+ } from './security';
18
+
19
+ // File operations
20
+ export {
21
+ readFile,
22
+ writeFile,
23
+ createFile,
24
+ deleteFile,
25
+ renameFile,
26
+ moveFile,
27
+ getRecentlyModified,
28
+ } from './fs-ops';
29
+
30
+ // Tree
31
+ export {
32
+ getFileTree,
33
+ collectAllFiles,
34
+ renderTree,
35
+ } from './tree';
36
+ export type { TreeOptions } from './tree';
37
+
38
+ // Search
39
+ export { searchFiles } from './search';
40
+
41
+ // Line-level operations
42
+ export {
43
+ readLines,
44
+ insertLines,
45
+ updateLines,
46
+ appendToFile,
47
+ insertAfterHeading,
48
+ updateSection,
49
+ } from './lines';
50
+
51
+ // CSV
52
+ export { appendCsvRow } from './csv';
53
+
54
+ // Backlinks
55
+ export { findBacklinks } from './backlinks';
56
+
57
+ // Git
58
+ export { isGitRepo, gitLog, gitShowFile } from './git';
@@ -0,0 +1,89 @@
1
+ import { readFile, writeFile } from './fs-ops';
2
+
3
+ /**
4
+ * Reads a file and returns its content split into lines.
5
+ */
6
+ export function readLines(mindRoot: string, filePath: string): string[] {
7
+ return readFile(mindRoot, filePath).split('\n');
8
+ }
9
+
10
+ /**
11
+ * Validates line indices are within bounds.
12
+ */
13
+ function validateLineRange(totalLines: number, start: number, end: number): void {
14
+ if (start < 0 || end < 0) throw new Error('Invalid line index: indices must be >= 0');
15
+ if (start > end) throw new Error(`Invalid range: start (${start}) > end (${end})`);
16
+ if (start >= totalLines) throw new Error(`Invalid line index: start (${start}) >= total lines (${totalLines})`);
17
+ }
18
+
19
+ /**
20
+ * Inserts lines after the given 0-based index.
21
+ * Use afterIndex = -1 to prepend at the start.
22
+ */
23
+ export function insertLines(mindRoot: string, filePath: string, afterIndex: number, lines: string[]): void {
24
+ const existing = readLines(mindRoot, filePath);
25
+ if (afterIndex >= existing.length) {
26
+ throw new Error(`Invalid after_index: ${afterIndex} >= total lines (${existing.length})`);
27
+ }
28
+ const insertAt = afterIndex < 0 ? 0 : afterIndex + 1;
29
+ existing.splice(insertAt, 0, ...lines);
30
+ writeFile(mindRoot, filePath, existing.join('\n'));
31
+ }
32
+
33
+ /**
34
+ * Replaces lines from startIndex to endIndex (inclusive) with newLines.
35
+ */
36
+ export function updateLines(mindRoot: string, filePath: string, startIndex: number, endIndex: number, newLines: string[]): void {
37
+ const existing = readLines(mindRoot, filePath);
38
+ validateLineRange(existing.length, startIndex, endIndex);
39
+ existing.splice(startIndex, endIndex - startIndex + 1, ...newLines);
40
+ writeFile(mindRoot, filePath, existing.join('\n'));
41
+ }
42
+
43
+ /**
44
+ * Appends content to the end of a file, adding a newline separator if needed.
45
+ */
46
+ export function appendToFile(mindRoot: string, filePath: string, content: string): void {
47
+ const existing = readFile(mindRoot, filePath);
48
+ const separator = existing.length > 0 && !existing.endsWith('\n\n') ? '\n' : '';
49
+ writeFile(mindRoot, filePath, existing + separator + content);
50
+ }
51
+
52
+ /**
53
+ * Inserts content after the first occurrence of a Markdown heading.
54
+ */
55
+ export function insertAfterHeading(mindRoot: string, filePath: string, heading: string, content: string): void {
56
+ const lines = readLines(mindRoot, filePath);
57
+ const idx = lines.findIndex(l => {
58
+ const trimmed = l.trim();
59
+ return trimmed === heading || trimmed.replace(/^#+\s*/, '') === heading.replace(/^#+\s*/, '');
60
+ });
61
+ if (idx === -1) throw new Error(`Heading not found: "${heading}"`);
62
+ let insertAt = idx + 1;
63
+ while (insertAt < lines.length && lines[insertAt].trim() === '') insertAt++;
64
+ insertLines(mindRoot, filePath, insertAt - 1, ['', content]);
65
+ }
66
+
67
+ /**
68
+ * Replaces the content of a Markdown section identified by its heading.
69
+ */
70
+ export function updateSection(mindRoot: string, filePath: string, heading: string, newContent: string): void {
71
+ const lines = readLines(mindRoot, filePath);
72
+ const idx = lines.findIndex(l => {
73
+ const trimmed = l.trim();
74
+ return trimmed === heading || trimmed.replace(/^#+\s*/, '') === heading.replace(/^#+\s*/, '');
75
+ });
76
+ if (idx === -1) throw new Error(`Heading not found: "${heading}"`);
77
+
78
+ const headingLevel = (lines[idx].match(/^#+/) ?? [''])[0].length;
79
+ let sectionEnd = lines.length - 1;
80
+ for (let i = idx + 1; i < lines.length; i++) {
81
+ const m = lines[i].match(/^(#+)\s/);
82
+ if (m && m[1].length <= headingLevel) {
83
+ sectionEnd = i - 1;
84
+ break;
85
+ }
86
+ }
87
+ while (sectionEnd > idx && lines[sectionEnd].trim() === '') sectionEnd--;
88
+ updateLines(mindRoot, filePath, idx + 1, sectionEnd, ['', newContent]);
89
+ }