mulmoclaude 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 (408) hide show
  1. package/README.md +44 -0
  2. package/bin/mulmoclaude.js +202 -0
  3. package/bin/prepare-dist.js +93 -0
  4. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
  5. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
  6. package/client/assets/index-D8rhwXLq.js +4906 -0
  7. package/client/assets/index-KNLBjwuh.css +1 -0
  8. package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
  9. package/client/assets/material-icons-Dr0goTwe.woff +0 -0
  10. package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
  11. package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
  12. package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
  13. package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
  14. package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
  15. package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
  16. package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
  17. package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
  18. package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
  19. package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
  20. package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
  21. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
  22. package/client/index.html +28 -0
  23. package/package.json +66 -0
  24. package/server/agent/attachmentConverter.ts +270 -0
  25. package/server/agent/config.ts +414 -0
  26. package/server/agent/index.ts +260 -0
  27. package/server/agent/mcp-server.ts +412 -0
  28. package/server/agent/mcp-tools/index.ts +63 -0
  29. package/server/agent/mcp-tools/x.ts +188 -0
  30. package/server/agent/plugin-names.ts +75 -0
  31. package/server/agent/prompt.ts +349 -0
  32. package/server/agent/resumeFailover.ts +129 -0
  33. package/server/agent/sandboxMounts.ts +329 -0
  34. package/server/agent/stream.ts +194 -0
  35. package/server/api/auth/bearerAuth.ts +61 -0
  36. package/server/api/auth/token.ts +98 -0
  37. package/server/api/csrfGuard.ts +85 -0
  38. package/server/api/routes/agent.ts +478 -0
  39. package/server/api/routes/chart.ts +98 -0
  40. package/server/api/routes/chat-index.ts +46 -0
  41. package/server/api/routes/config.ts +258 -0
  42. package/server/api/routes/dispatchResponse.ts +79 -0
  43. package/server/api/routes/files.ts +812 -0
  44. package/server/api/routes/html.ts +101 -0
  45. package/server/api/routes/image.ts +169 -0
  46. package/server/api/routes/mulmo-script.ts +712 -0
  47. package/server/api/routes/mulmoScriptValidate.ts +101 -0
  48. package/server/api/routes/notifications.ts +69 -0
  49. package/server/api/routes/pdf.ts +163 -0
  50. package/server/api/routes/plugins.ts +276 -0
  51. package/server/api/routes/presentHtml.ts +48 -0
  52. package/server/api/routes/roles.ts +125 -0
  53. package/server/api/routes/scheduler.ts +153 -0
  54. package/server/api/routes/schedulerHandlers.ts +151 -0
  55. package/server/api/routes/schedulerTasks.ts +163 -0
  56. package/server/api/routes/sessions.ts +294 -0
  57. package/server/api/routes/sessionsCursor.ts +59 -0
  58. package/server/api/routes/skills.ts +195 -0
  59. package/server/api/routes/sources.ts +540 -0
  60. package/server/api/routes/todos.ts +263 -0
  61. package/server/api/routes/todosColumnsHandlers.ts +347 -0
  62. package/server/api/routes/todosHandlers.ts +274 -0
  63. package/server/api/routes/todosItemsHandlers.ts +386 -0
  64. package/server/api/routes/wiki/pageIndex.ts +53 -0
  65. package/server/api/routes/wiki.ts +363 -0
  66. package/server/api/sandboxStatus.ts +64 -0
  67. package/server/events/notifications.ts +160 -0
  68. package/server/events/pub-sub/index.ts +45 -0
  69. package/server/events/relay-client.ts +288 -0
  70. package/server/events/scheduler-adapter.ts +302 -0
  71. package/server/events/session-store/index.ts +492 -0
  72. package/server/events/task-manager/index.ts +181 -0
  73. package/server/index.ts +572 -0
  74. package/server/system/config.ts +243 -0
  75. package/server/system/credentials.ts +220 -0
  76. package/server/system/docker.ts +97 -0
  77. package/server/system/env.ts +109 -0
  78. package/server/system/logger/config.ts +112 -0
  79. package/server/system/logger/formatters.ts +40 -0
  80. package/server/system/logger/index.ts +53 -0
  81. package/server/system/logger/rotation.ts +37 -0
  82. package/server/system/logger/sinks.ts +101 -0
  83. package/server/system/logger/types.ts +29 -0
  84. package/server/utils/date.ts +57 -0
  85. package/server/utils/errors.ts +7 -0
  86. package/server/utils/fetch.ts +27 -0
  87. package/server/utils/files/atomic.ts +125 -0
  88. package/server/utils/files/html-io.ts +20 -0
  89. package/server/utils/files/image-store.ts +66 -0
  90. package/server/utils/files/index.ts +45 -0
  91. package/server/utils/files/journal-io.ts +213 -0
  92. package/server/utils/files/json.ts +69 -0
  93. package/server/utils/files/markdown-store.ts +33 -0
  94. package/server/utils/files/naming.ts +50 -0
  95. package/server/utils/files/reference-dirs-io.ts +45 -0
  96. package/server/utils/files/roles-io.ts +45 -0
  97. package/server/utils/files/safe.ts +106 -0
  98. package/server/utils/files/scheduler-io.ts +20 -0
  99. package/server/utils/files/scheduler-overrides-io.ts +64 -0
  100. package/server/utils/files/session-io.ts +136 -0
  101. package/server/utils/files/spreadsheet-store.ts +63 -0
  102. package/server/utils/files/todos-io.ts +29 -0
  103. package/server/utils/files/user-tasks-io.ts +25 -0
  104. package/server/utils/files/workspace-io.ts +221 -0
  105. package/server/utils/gemini.ts +59 -0
  106. package/server/utils/gitignore.ts +69 -0
  107. package/server/utils/http.ts +15 -0
  108. package/server/utils/httpError.ts +61 -0
  109. package/server/utils/id.ts +16 -0
  110. package/server/utils/json.ts +83 -0
  111. package/server/utils/logBackgroundError.ts +22 -0
  112. package/server/utils/markdown.ts +82 -0
  113. package/server/utils/request.ts +29 -0
  114. package/server/utils/slug.ts +50 -0
  115. package/server/utils/spawn.ts +62 -0
  116. package/server/utils/time.ts +34 -0
  117. package/server/utils/types.ts +47 -0
  118. package/server/workspace/chat-index/index.ts +153 -0
  119. package/server/workspace/chat-index/indexer.ts +209 -0
  120. package/server/workspace/chat-index/paths.ts +34 -0
  121. package/server/workspace/chat-index/summarizer.ts +247 -0
  122. package/server/workspace/chat-index/types.ts +38 -0
  123. package/server/workspace/custom-dirs.ts +220 -0
  124. package/server/workspace/helps/business.md +104 -0
  125. package/server/workspace/helps/github.md +23 -0
  126. package/server/workspace/helps/index.md +60 -0
  127. package/server/workspace/helps/mulmoscript.md +249 -0
  128. package/server/workspace/helps/sandbox.md +90 -0
  129. package/server/workspace/helps/spreadsheet.md +43 -0
  130. package/server/workspace/helps/telegram.md +135 -0
  131. package/server/workspace/helps/wiki.md +131 -0
  132. package/server/workspace/journal/archivist.ts +386 -0
  133. package/server/workspace/journal/dailyPass.ts +743 -0
  134. package/server/workspace/journal/diff.ts +71 -0
  135. package/server/workspace/journal/index.ts +185 -0
  136. package/server/workspace/journal/indexFile.ts +136 -0
  137. package/server/workspace/journal/linkRewrite.ts +4 -0
  138. package/server/workspace/journal/memoryExtractor.ts +130 -0
  139. package/server/workspace/journal/optimizationPass.ts +160 -0
  140. package/server/workspace/journal/paths.ts +76 -0
  141. package/server/workspace/journal/state.ts +125 -0
  142. package/server/workspace/paths.ts +158 -0
  143. package/server/workspace/reference-dirs.ts +252 -0
  144. package/server/workspace/roles.ts +37 -0
  145. package/server/workspace/skills/discovery.ts +125 -0
  146. package/server/workspace/skills/index.ts +10 -0
  147. package/server/workspace/skills/parser.ts +144 -0
  148. package/server/workspace/skills/paths.ts +41 -0
  149. package/server/workspace/skills/scheduler.ts +149 -0
  150. package/server/workspace/skills/types.ts +30 -0
  151. package/server/workspace/skills/user-tasks.ts +257 -0
  152. package/server/workspace/skills/writer.ts +189 -0
  153. package/server/workspace/sources/arxivDiscovery.ts +182 -0
  154. package/server/workspace/sources/classifier.ts +268 -0
  155. package/server/workspace/sources/fetchers/arxiv.ts +170 -0
  156. package/server/workspace/sources/fetchers/github.ts +106 -0
  157. package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
  158. package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
  159. package/server/workspace/sources/fetchers/index.ts +71 -0
  160. package/server/workspace/sources/fetchers/registerAll.ts +15 -0
  161. package/server/workspace/sources/fetchers/rss.ts +141 -0
  162. package/server/workspace/sources/fetchers/rssParser.ts +295 -0
  163. package/server/workspace/sources/httpFetcher.ts +230 -0
  164. package/server/workspace/sources/interests.ts +120 -0
  165. package/server/workspace/sources/paths.ts +110 -0
  166. package/server/workspace/sources/pipeline/dedup.ts +60 -0
  167. package/server/workspace/sources/pipeline/fetch.ts +136 -0
  168. package/server/workspace/sources/pipeline/index.ts +249 -0
  169. package/server/workspace/sources/pipeline/notify.ts +72 -0
  170. package/server/workspace/sources/pipeline/plan.ts +66 -0
  171. package/server/workspace/sources/pipeline/summarize.ts +189 -0
  172. package/server/workspace/sources/pipeline/write.ts +185 -0
  173. package/server/workspace/sources/rateLimiter.ts +148 -0
  174. package/server/workspace/sources/registry.ts +326 -0
  175. package/server/workspace/sources/robots.ts +271 -0
  176. package/server/workspace/sources/sourceState.ts +135 -0
  177. package/server/workspace/sources/taxonomy.ts +74 -0
  178. package/server/workspace/sources/types.ts +144 -0
  179. package/server/workspace/sources/urls.ts +112 -0
  180. package/server/workspace/tool-trace/classify.ts +114 -0
  181. package/server/workspace/tool-trace/index.ts +250 -0
  182. package/server/workspace/tool-trace/writeSearch.ts +98 -0
  183. package/server/workspace/wiki-backlinks/index.ts +107 -0
  184. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
  185. package/server/workspace/workspace.ts +66 -0
  186. package/src/App.vue +720 -0
  187. package/src/assets/mulmo_bw.png +0 -0
  188. package/src/components/CanvasViewToggle.vue +27 -0
  189. package/src/components/ChatAttachmentPreview.vue +45 -0
  190. package/src/components/ChatImagePreview.vue +17 -0
  191. package/src/components/ChatInput.vue +208 -0
  192. package/src/components/FileContentHeader.vue +49 -0
  193. package/src/components/FileContentRenderer.vue +162 -0
  194. package/src/components/FileTree.vue +115 -0
  195. package/src/components/FileTreePane.vue +85 -0
  196. package/src/components/FilesView.vue +206 -0
  197. package/src/components/LockStatusPopup.vue +111 -0
  198. package/src/components/NotificationBell.vue +131 -0
  199. package/src/components/NotificationToast.vue +72 -0
  200. package/src/components/PluginLauncher.vue +138 -0
  201. package/src/components/RightSidebar.vue +113 -0
  202. package/src/components/RoleSelector.vue +64 -0
  203. package/src/components/SessionHistoryPanel.vue +176 -0
  204. package/src/components/SessionTabBar.vue +81 -0
  205. package/src/components/SettingsMcpTab.vue +350 -0
  206. package/src/components/SettingsModal.vue +275 -0
  207. package/src/components/SettingsReferenceDirsTab.vue +173 -0
  208. package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
  209. package/src/components/SidebarHeader.vue +69 -0
  210. package/src/components/StackView.vue +360 -0
  211. package/src/components/SuggestionsPanel.vue +65 -0
  212. package/src/components/TodoExplorer.vue +358 -0
  213. package/src/components/ToolResultsPanel.vue +77 -0
  214. package/src/components/todo/TodoAddDialog.vue +131 -0
  215. package/src/components/todo/TodoEditDialog.vue +47 -0
  216. package/src/components/todo/TodoEditPanel.vue +113 -0
  217. package/src/components/todo/TodoKanbanView.vue +249 -0
  218. package/src/components/todo/TodoListView.vue +79 -0
  219. package/src/components/todo/TodoTableView.vue +177 -0
  220. package/src/composables/useActiveSession.ts +40 -0
  221. package/src/composables/useAppApi.ts +45 -0
  222. package/src/composables/useCanvasViewMode.ts +121 -0
  223. package/src/composables/useChatScroll.ts +47 -0
  224. package/src/composables/useClickOutside.ts +26 -0
  225. package/src/composables/useClipboardCopy.ts +44 -0
  226. package/src/composables/useContentDisplay.ts +52 -0
  227. package/src/composables/useDebugBeat.ts +23 -0
  228. package/src/composables/useDynamicFavicon.ts +115 -0
  229. package/src/composables/useEventListeners.ts +42 -0
  230. package/src/composables/useExpandedDirs.ts +64 -0
  231. package/src/composables/useFaviconState.ts +30 -0
  232. package/src/composables/useFileSelection.ts +115 -0
  233. package/src/composables/useFileSortMode.ts +24 -0
  234. package/src/composables/useFileTree.ts +85 -0
  235. package/src/composables/useFreshPluginData.ts +89 -0
  236. package/src/composables/useHealth.ts +38 -0
  237. package/src/composables/useImeAwareEnter.ts +57 -0
  238. package/src/composables/useKeyNavigation.ts +60 -0
  239. package/src/composables/useMarkdownLinkHandler.ts +46 -0
  240. package/src/composables/useMarkdownMode.ts +17 -0
  241. package/src/composables/useMcpTools.ts +71 -0
  242. package/src/composables/useMergedSessions.ts +27 -0
  243. package/src/composables/useNotifications.ts +90 -0
  244. package/src/composables/usePdfDownload.ts +60 -0
  245. package/src/composables/usePendingCalls.ts +77 -0
  246. package/src/composables/usePubSub.ts +85 -0
  247. package/src/composables/useRightSidebar.ts +23 -0
  248. package/src/composables/useRoles.ts +34 -0
  249. package/src/composables/useSandboxStatus.ts +67 -0
  250. package/src/composables/useSelectedResult.ts +49 -0
  251. package/src/composables/useSessionDerived.ts +51 -0
  252. package/src/composables/useSessionHistory.ts +81 -0
  253. package/src/composables/useSessionSync.ts +57 -0
  254. package/src/composables/useViewLayout.ts +55 -0
  255. package/src/config/apiRoutes.ts +173 -0
  256. package/src/config/pubsubChannels.ts +45 -0
  257. package/src/config/roles.ts +335 -0
  258. package/src/config/schedulerActions.ts +25 -0
  259. package/src/config/toolNames.ts +71 -0
  260. package/src/config/workspacePaths.ts +24 -0
  261. package/src/index.css +107 -0
  262. package/src/main.ts +25 -0
  263. package/src/plugins/canvas/Preview.vue +13 -0
  264. package/src/plugins/canvas/View.vue +333 -0
  265. package/src/plugins/canvas/definition.ts +38 -0
  266. package/src/plugins/canvas/index.ts +36 -0
  267. package/src/plugins/chart/Preview.vue +49 -0
  268. package/src/plugins/chart/View.vue +143 -0
  269. package/src/plugins/chart/definition.ts +58 -0
  270. package/src/plugins/chart/index.ts +52 -0
  271. package/src/plugins/editImage/Preview.vue +13 -0
  272. package/src/plugins/editImage/View.vue +13 -0
  273. package/src/plugins/editImage/definition.ts +27 -0
  274. package/src/plugins/editImage/index.ts +36 -0
  275. package/src/plugins/generateImage/Preview.vue +13 -0
  276. package/src/plugins/generateImage/View.vue +33 -0
  277. package/src/plugins/generateImage/definition.ts +32 -0
  278. package/src/plugins/generateImage/index.ts +56 -0
  279. package/src/plugins/manageRoles/Preview.vue +49 -0
  280. package/src/plugins/manageRoles/View.vue +525 -0
  281. package/src/plugins/manageRoles/definition.ts +43 -0
  282. package/src/plugins/manageRoles/index.ts +47 -0
  283. package/src/plugins/manageSkills/Preview.vue +21 -0
  284. package/src/plugins/manageSkills/View.vue +321 -0
  285. package/src/plugins/manageSkills/definition.ts +49 -0
  286. package/src/plugins/manageSkills/index.ts +49 -0
  287. package/src/plugins/manageSource/Preview.vue +33 -0
  288. package/src/plugins/manageSource/View.vue +697 -0
  289. package/src/plugins/manageSource/definition.ts +63 -0
  290. package/src/plugins/manageSource/index.ts +66 -0
  291. package/src/plugins/markdown/Preview.vue +77 -0
  292. package/src/plugins/markdown/View.vue +476 -0
  293. package/src/plugins/markdown/definition.ts +50 -0
  294. package/src/plugins/markdown/index.ts +36 -0
  295. package/src/plugins/presentHtml/Preview.vue +25 -0
  296. package/src/plugins/presentHtml/View.vue +52 -0
  297. package/src/plugins/presentHtml/definition.ts +27 -0
  298. package/src/plugins/presentHtml/helpers.ts +72 -0
  299. package/src/plugins/presentHtml/index.ts +41 -0
  300. package/src/plugins/presentMulmoScript/Preview.vue +23 -0
  301. package/src/plugins/presentMulmoScript/View.vue +1166 -0
  302. package/src/plugins/presentMulmoScript/definition.ts +95 -0
  303. package/src/plugins/presentMulmoScript/helpers.ts +162 -0
  304. package/src/plugins/presentMulmoScript/index.ts +40 -0
  305. package/src/plugins/scheduler/Preview.vue +67 -0
  306. package/src/plugins/scheduler/TasksTab.vue +205 -0
  307. package/src/plugins/scheduler/View.vue +565 -0
  308. package/src/plugins/scheduler/definition.ts +57 -0
  309. package/src/plugins/scheduler/index.ts +45 -0
  310. package/src/plugins/scheduler/viewModes.ts +26 -0
  311. package/src/plugins/spreadsheet/Preview.vue +29 -0
  312. package/src/plugins/spreadsheet/View.vue +997 -0
  313. package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
  314. package/src/plugins/spreadsheet/definition.ts +121 -0
  315. package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
  316. package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
  317. package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
  318. package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
  319. package/src/plugins/spreadsheet/engine/engine.ts +176 -0
  320. package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
  321. package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
  322. package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
  323. package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
  324. package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
  325. package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
  326. package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
  327. package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
  328. package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
  329. package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
  330. package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
  331. package/src/plugins/spreadsheet/engine/index.ts +27 -0
  332. package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
  333. package/src/plugins/spreadsheet/engine/parser.ts +143 -0
  334. package/src/plugins/spreadsheet/engine/registry.ts +150 -0
  335. package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
  336. package/src/plugins/spreadsheet/engine/types.ts +64 -0
  337. package/src/plugins/spreadsheet/index.ts +36 -0
  338. package/src/plugins/textResponse/Preview.vue +94 -0
  339. package/src/plugins/textResponse/View.vue +503 -0
  340. package/src/plugins/textResponse/definition.ts +34 -0
  341. package/src/plugins/textResponse/index.ts +27 -0
  342. package/src/plugins/textResponse/plugin.ts +29 -0
  343. package/src/plugins/textResponse/samples.ts +97 -0
  344. package/src/plugins/textResponse/types.ts +11 -0
  345. package/src/plugins/todo/Preview.vue +63 -0
  346. package/src/plugins/todo/View.vue +364 -0
  347. package/src/plugins/todo/composables/useTodos.ts +177 -0
  348. package/src/plugins/todo/definition.ts +45 -0
  349. package/src/plugins/todo/index.ts +61 -0
  350. package/src/plugins/todo/labels.ts +163 -0
  351. package/src/plugins/todo/priority.ts +98 -0
  352. package/src/plugins/todo/viewModes.ts +19 -0
  353. package/src/plugins/ui-image/ImagePreview.vue +23 -0
  354. package/src/plugins/ui-image/ImageView.vue +34 -0
  355. package/src/plugins/ui-image/index.ts +3 -0
  356. package/src/plugins/ui-image/types.ts +4 -0
  357. package/src/plugins/wiki/Preview.vue +65 -0
  358. package/src/plugins/wiki/View.vue +342 -0
  359. package/src/plugins/wiki/definition.ts +25 -0
  360. package/src/plugins/wiki/helpers.ts +59 -0
  361. package/src/plugins/wiki/index.ts +52 -0
  362. package/src/router/guards.ts +61 -0
  363. package/src/router/index.ts +50 -0
  364. package/src/tools/index.ts +52 -0
  365. package/src/tools/types.ts +27 -0
  366. package/src/types/events.ts +16 -0
  367. package/src/types/fileTree.ts +13 -0
  368. package/src/types/notification.ts +67 -0
  369. package/src/types/session.ts +116 -0
  370. package/src/types/sse.ts +90 -0
  371. package/src/types/toolCallHistory.ts +13 -0
  372. package/src/utils/agent/eventDispatch.ts +74 -0
  373. package/src/utils/agent/request.ts +55 -0
  374. package/src/utils/agent/toolCalls.ts +62 -0
  375. package/src/utils/api.ts +218 -0
  376. package/src/utils/canvas/viewMode.ts +46 -0
  377. package/src/utils/dom/authTokenMeta.ts +20 -0
  378. package/src/utils/dom/clickOutside.ts +11 -0
  379. package/src/utils/dom/externalLink.ts +57 -0
  380. package/src/utils/dom/scrollable.ts +24 -0
  381. package/src/utils/errors.ts +11 -0
  382. package/src/utils/files/expandedDirs.ts +25 -0
  383. package/src/utils/files/filename.ts +12 -0
  384. package/src/utils/files/sortChildren.ts +20 -0
  385. package/src/utils/filesPreview/schedulerPreview.ts +38 -0
  386. package/src/utils/filesPreview/todoPreview.ts +40 -0
  387. package/src/utils/format/date.ts +85 -0
  388. package/src/utils/format/frontmatter.ts +80 -0
  389. package/src/utils/format/jsonSyntax.ts +109 -0
  390. package/src/utils/html/previewCsp.ts +65 -0
  391. package/src/utils/image/resolve.ts +8 -0
  392. package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
  393. package/src/utils/markdown/extractFirstH1.ts +39 -0
  394. package/src/utils/notification/dispatch.ts +22 -0
  395. package/src/utils/path/relativeLink.ts +130 -0
  396. package/src/utils/role/icon.ts +20 -0
  397. package/src/utils/role/merge.ts +10 -0
  398. package/src/utils/role/plugins.ts +12 -0
  399. package/src/utils/session/mergeSessions.ts +103 -0
  400. package/src/utils/session/seedRoleDefault.ts +35 -0
  401. package/src/utils/session/sessionEntries.ts +121 -0
  402. package/src/utils/session/sessionFactory.ts +22 -0
  403. package/src/utils/session/sessionHelpers.ts +99 -0
  404. package/src/utils/tools/dedup.ts +17 -0
  405. package/src/utils/tools/mcp.ts +33 -0
  406. package/src/utils/tools/pendingCalls.ts +16 -0
  407. package/src/utils/tools/result.ts +40 -0
  408. package/src/utils/types.ts +44 -0
@@ -0,0 +1,76 @@
1
+ // Pure path / slug helpers for the workspace journal. Nothing here
2
+ // touches the filesystem — every function is a straightforward
3
+ // string transformation so it can be exhaustively unit-tested.
4
+
5
+ import path from "node:path";
6
+ import { WORKSPACE_DIRS } from "../paths.js";
7
+ import { isValidIsoDate } from "../../utils/date.js";
8
+
9
+ // Directory layout under workspace/conversations/summaries/ is an
10
+ // implementation detail of the journal module; keep it centralised
11
+ // here so tests and callers all agree on the structure.
12
+ export const SUMMARIES_DIR = WORKSPACE_DIRS.summaries;
13
+ export const STATE_FILE = "_state.json";
14
+ export const INDEX_FILE = "_index.md";
15
+ export const DAILY_DIR = "daily";
16
+ export const TOPICS_DIR = "topics";
17
+ export const ARCHIVE_DIR = "archive";
18
+
19
+ // Absolute path to the summaries root inside a workspace.
20
+ export function summariesRoot(workspaceRoot: string): string {
21
+ return path.join(workspaceRoot, SUMMARIES_DIR);
22
+ }
23
+
24
+ // summaries/daily/YYYY/MM/DD.md for a given ISO-ish date ("YYYY-MM-DD").
25
+ // Throws if `isoDate` is not exactly YYYY-MM-DD — catches typos at
26
+ // the boundary instead of producing "undefined/undefined.md" paths
27
+ // downstream.
28
+ export function dailyPathFor(workspaceRoot: string, isoDate: string): string {
29
+ if (!isValidIsoDate(isoDate)) {
30
+ throw new Error(`[journal] dailyPathFor: expected YYYY-MM-DD, got "${isoDate}"`);
31
+ }
32
+ const [year, month, day] = isoDate.split("-");
33
+ return path.join(summariesRoot(workspaceRoot), DAILY_DIR, year, month, `${day}.md`);
34
+ }
35
+
36
+ // summaries/topics/<slug>.md
37
+ export function topicPathFor(workspaceRoot: string, slug: string): string {
38
+ return path.join(summariesRoot(workspaceRoot), TOPICS_DIR, `${slug}.md`);
39
+ }
40
+
41
+ // summaries/archive/topics/<slug>.md — where the optimizer moves
42
+ // merged or stale topic files.
43
+ export function archivedTopicPathFor(workspaceRoot: string, slug: string): string {
44
+ return path.join(summariesRoot(workspaceRoot), ARCHIVE_DIR, TOPICS_DIR, `${slug}.md`);
45
+ }
46
+
47
+ // Re-export for backwards compatibility — callers that import
48
+ // toIsoDate from journal/paths keep working.
49
+ export { toLocalIsoDate as toIsoDate } from "../../utils/date.js";
50
+
51
+ // Convert a free-form topic name into a filesystem-safe slug.
52
+ // Rules:
53
+ // - Lowercase ASCII letters, digits, and hyphens only
54
+ // - Whitespace and punctuation collapse to a single hyphen
55
+ // - Non-ASCII characters (Japanese, emoji) are dropped; if the
56
+ // result is empty we fall back to "topic" so we always yield a
57
+ // valid filename (LLMs occasionally emit pure-Japanese topic
58
+ // names; the markdown body still holds the original title for
59
+ // display, this slug is only the filesystem key)
60
+ // - Leading/trailing hyphens stripped
61
+ // - Empty-string input yields "topic"
62
+ export function slugify(raw: string): string {
63
+ const lowered = raw.toLowerCase();
64
+ // Replace runs of non-ASCII-alnum with a single hyphen. Because
65
+ // we use `+` on a character class, this single pass already
66
+ // collapses runs — no second dedupe pass needed.
67
+ const hyphenated = lowered.replace(/[^a-z0-9]+/g, "-");
68
+ // Trim leading/trailing hyphens without a regex — sonarjs/slow-regex
69
+ // flags `^-+` / `-+$` patterns even though these inputs are tiny.
70
+ let start = 0;
71
+ let end = hyphenated.length;
72
+ while (start < end && hyphenated[start] === "-") start++;
73
+ while (end > start && hyphenated[end - 1] === "-") end--;
74
+ const trimmed = hyphenated.slice(start, end);
75
+ return trimmed.length > 0 ? trimmed : "topic";
76
+ }
@@ -0,0 +1,125 @@
1
+ // Journal state file schema + persistence. The state file tracks
2
+ // what the archivist has already done so we only re-process new or
3
+ // changed sessions on each run.
4
+ //
5
+ // The pure bits (default creation, schema validation, interval
6
+ // arithmetic) live at the top of the file so tests can exercise
7
+ // them without touching disk. Filesystem helpers at the bottom wrap
8
+ // those pure functions with atomic read/write.
9
+
10
+ import {
11
+ readJournalState as readJournalStateRaw,
12
+ writeJournalState as writeJournalStateRaw,
13
+ journalStateExists as journalStateExistsRaw,
14
+ } from "../../utils/files/journal-io.js";
15
+ import { ONE_HOUR_MS, ONE_DAY_MS } from "../../utils/time.js";
16
+ import { isRecord } from "../../utils/types.js";
17
+
18
+ // Bump this when the schema changes in a backwards-incompatible way.
19
+ // Older state files are treated as corrupted and replaced with a
20
+ // fresh default (ingest everything from scratch) — cheap because it
21
+ // only costs one extra archivist pass.
22
+ export const JOURNAL_STATE_VERSION = 1;
23
+
24
+ export interface ProcessedSessionRecord {
25
+ // mtime (ms since epoch) of the session's .jsonl file when we
26
+ // last ingested it. If mtime advances on the next run, the session
27
+ // has appended events and needs re-ingest.
28
+ lastMtimeMs: number;
29
+ }
30
+
31
+ export interface JournalState {
32
+ version: number;
33
+ lastDailyRunAt: string | null;
34
+ lastOptimizationRunAt: string | null;
35
+ dailyIntervalHours: number;
36
+ optimizationIntervalDays: number;
37
+ processedSessions: Record<string, ProcessedSessionRecord>;
38
+ knownTopics: string[];
39
+ }
40
+
41
+ export const DEFAULT_DAILY_INTERVAL_HOURS = 1;
42
+ export const DEFAULT_OPTIMIZATION_INTERVAL_DAYS = 7;
43
+
44
+ // --- Pure helpers (unit-testable without disk) ---------------------
45
+
46
+ export function defaultState(): JournalState {
47
+ return {
48
+ version: JOURNAL_STATE_VERSION,
49
+ lastDailyRunAt: null,
50
+ lastOptimizationRunAt: null,
51
+ dailyIntervalHours: DEFAULT_DAILY_INTERVAL_HOURS,
52
+ optimizationIntervalDays: DEFAULT_OPTIMIZATION_INTERVAL_DAYS,
53
+ processedSessions: {},
54
+ knownTopics: [],
55
+ };
56
+ }
57
+
58
+ // Narrow an `unknown` into a JournalState. Accepts partial / missing
59
+ // fields and fills defaults — users can hand-edit the file to change
60
+ // intervals and we want to be forgiving.
61
+ export function parseState(raw: unknown): JournalState {
62
+ if (!isRecord(raw)) return defaultState();
63
+ const obj = raw as Record<string, unknown>;
64
+
65
+ // Version mismatch → throw it all out. Cheap to rebuild.
66
+ if (obj.version !== JOURNAL_STATE_VERSION) return defaultState();
67
+
68
+ const d = defaultState();
69
+ return {
70
+ version: JOURNAL_STATE_VERSION,
71
+ lastDailyRunAt: typeof obj.lastDailyRunAt === "string" ? obj.lastDailyRunAt : null,
72
+ lastOptimizationRunAt: typeof obj.lastOptimizationRunAt === "string" ? obj.lastOptimizationRunAt : null,
73
+ dailyIntervalHours: typeof obj.dailyIntervalHours === "number" && obj.dailyIntervalHours > 0 ? obj.dailyIntervalHours : d.dailyIntervalHours,
74
+ optimizationIntervalDays:
75
+ typeof obj.optimizationIntervalDays === "number" && obj.optimizationIntervalDays > 0 ? obj.optimizationIntervalDays : d.optimizationIntervalDays,
76
+ processedSessions: parseProcessedSessions(obj.processedSessions),
77
+ knownTopics: Array.isArray(obj.knownTopics) ? obj.knownTopics.filter((t): t is string => typeof t === "string") : [],
78
+ };
79
+ }
80
+
81
+ function parseProcessedSessions(raw: unknown): Record<string, ProcessedSessionRecord> {
82
+ if (!isRecord(raw)) return {};
83
+ const out: Record<string, ProcessedSessionRecord> = {};
84
+ for (const [id, rec] of Object.entries(raw as Record<string, unknown>)) {
85
+ if (!isRecord(rec)) continue;
86
+ const mtime = (rec as Record<string, unknown>).lastMtimeMs;
87
+ if (typeof mtime === "number" && mtime >= 0) {
88
+ out[id] = { lastMtimeMs: mtime };
89
+ }
90
+ }
91
+ return out;
92
+ }
93
+
94
+ // Has the configured daily interval elapsed since the last run? A
95
+ // null lastDailyRunAt means "never run" → always due.
96
+ export function isDailyDue(state: JournalState, nowMs: number): boolean {
97
+ if (state.lastDailyRunAt === null) return true;
98
+ const last = Date.parse(state.lastDailyRunAt);
99
+ if (Number.isNaN(last)) return true;
100
+ const intervalMs = state.dailyIntervalHours * ONE_HOUR_MS;
101
+ return nowMs - last >= intervalMs;
102
+ }
103
+
104
+ export function isOptimizationDue(state: JournalState, nowMs: number): boolean {
105
+ if (state.lastOptimizationRunAt === null) return true;
106
+ const last = Date.parse(state.lastOptimizationRunAt);
107
+ if (Number.isNaN(last)) return true;
108
+ const intervalMs = state.optimizationIntervalDays * ONE_DAY_MS;
109
+ return nowMs - last >= intervalMs;
110
+ }
111
+
112
+ // --- Filesystem helpers (delegated to journal-io) --------------------
113
+
114
+ export async function readState(workspaceRoot: string): Promise<JournalState> {
115
+ const raw = await readJournalStateRaw<unknown>(null, workspaceRoot);
116
+ return parseState(raw);
117
+ }
118
+
119
+ export async function writeState(workspaceRoot: string, state: JournalState): Promise<void> {
120
+ await writeJournalStateRaw(state, workspaceRoot);
121
+ }
122
+
123
+ export function stateFileExists(workspaceRoot: string): boolean {
124
+ return journalStateExistsRaw(workspaceRoot);
125
+ }
@@ -0,0 +1,158 @@
1
+ // Single source of truth for workspace directory / file names and
2
+ // their absolute paths. The record below uses workspace-relative
3
+ // paths (possibly multi-segment, e.g. `config/roles`) as values; code
4
+ // looks up via `WORKSPACE_PATHS.<key>` to get the absolute form.
5
+ //
6
+ // Layout grouping (issue #284):
7
+ //
8
+ // config/ settings + roles + helps
9
+ // conversations/ chat + memory.md + summaries
10
+ // data/ user-managed (wiki, todos, calendar, contacts,
11
+ // scheduler, sources, transports)
12
+ // artifacts/ LLM-generated (charts, html, images, documents,
13
+ // spreadsheets, stories, news)
14
+ //
15
+ // Existing workspaces need the one-shot `scripts/migrate-workspace-284.ts`
16
+ // script run before first startup with this code. `server/workspace.ts`
17
+ // detects the pre-migration layout at boot and aborts with a pointer
18
+ // to the script.
19
+ //
20
+ // When adding a new top-level directory: add the name to the
21
+ // `WORKSPACE_DIRS` record below. The absolute path is derived
22
+ // automatically via `WORKSPACE_PATHS`.
23
+
24
+ import os from "os";
25
+ import path from "path";
26
+
27
+ // Workspace root. Hard-coded to `~/mulmoclaude` — there is no
28
+ // WORKSPACE_PATH env override today; changing the location
29
+ // requires a code edit or a symlink. Re-exported by
30
+ // `server/workspace.ts` for backwards compatibility of existing
31
+ // callers that `import { workspacePath } from "./workspace.js"`.
32
+ export const workspacePath = path.join(os.homedir(), "mulmoclaude");
33
+
34
+ // Workspace-relative paths. Keys are the stable code-side identifiers
35
+ // (e.g. `markdowns` — unchanged for call-site compatibility); values
36
+ // are the on-disk paths, grouped per issue #284.
37
+ export const WORKSPACE_DIRS = {
38
+ // conversations/
39
+ chat: "conversations/chat",
40
+ summaries: "conversations/summaries",
41
+ // Tool-trace output for WebSearch (one .md per search, referenced
42
+ // from chat JSONL `contentRef`). Lives alongside chat/ so search
43
+ // trace and chat session share the same grouping.
44
+ searches: "conversations/searches",
45
+ // data/
46
+ wiki: "data/wiki",
47
+ todos: "data/todos",
48
+ calendar: "data/calendar",
49
+ contacts: "data/contacts",
50
+ scheduler: "data/scheduler",
51
+ sources: "data/sources",
52
+ transports: "data/transports",
53
+ // artifacts/
54
+ charts: "artifacts/charts",
55
+ // `markdowns` key preserved for call-site compatibility; on-disk
56
+ // name is `documents` for clarity.
57
+ markdowns: "artifacts/documents",
58
+ // `htmls` = `presentHtml` plugin output (many files, persistent).
59
+ // On-disk normalized to lowercase `html`.
60
+ htmls: "artifacts/html",
61
+ // Distinct from `htmls`: scratch buffer for the `/api/html`
62
+ // generate-and-preview route. One file (`current.html`), always
63
+ // overwritten. Kept separate so reloading a saved HTML artifact
64
+ // doesn't clobber the current preview.
65
+ html: "artifacts/html-scratch",
66
+ images: "artifacts/images",
67
+ spreadsheets: "artifacts/spreadsheets",
68
+ stories: "artifacts/stories",
69
+ news: "artifacts/news",
70
+ // config/
71
+ configs: "config",
72
+ roles: "config/roles",
73
+ helps: "config/helps",
74
+ // Nested subdirs inside a top-level grouping. Kept here (rather
75
+ // than module-local constants) when multiple modules need to
76
+ // reference the same nested path — e.g. wiki/pages/ is used by
77
+ // the wiki route, the wiki-backlinks driver, and the system
78
+ // prompt hint.
79
+ wikiPages: "data/wiki/pages",
80
+ wikiSources: "data/wiki/sources",
81
+ // Development — git-cloned repositories (#256).
82
+ github: "github",
83
+ } as const;
84
+
85
+ // Well-known individual files — imported from the shared
86
+ // src/config/workspacePaths.ts (single source of truth for both
87
+ // server and frontend). Re-exported so server callers keep the
88
+ // same `import { WORKSPACE_FILES } from "./paths.js"` they use.
89
+ import { WORKSPACE_FILES } from "../../src/config/workspacePaths.js";
90
+ export { WORKSPACE_FILES };
91
+
92
+ // Absolute paths, built once at module load from `workspacePath`.
93
+ // The `workspacePath` const is itself fixed (reads `os.homedir()`
94
+ // at process start — no env override, see `server/workspace.ts`),
95
+ // so freezing these paths is safe.
96
+ export const WORKSPACE_PATHS = {
97
+ chat: path.join(workspacePath, WORKSPACE_DIRS.chat),
98
+ todos: path.join(workspacePath, WORKSPACE_DIRS.todos),
99
+ calendar: path.join(workspacePath, WORKSPACE_DIRS.calendar),
100
+ contacts: path.join(workspacePath, WORKSPACE_DIRS.contacts),
101
+ scheduler: path.join(workspacePath, WORKSPACE_DIRS.scheduler),
102
+ roles: path.join(workspacePath, WORKSPACE_DIRS.roles),
103
+ stories: path.join(workspacePath, WORKSPACE_DIRS.stories),
104
+ images: path.join(workspacePath, WORKSPACE_DIRS.images),
105
+ markdowns: path.join(workspacePath, WORKSPACE_DIRS.markdowns),
106
+ spreadsheets: path.join(workspacePath, WORKSPACE_DIRS.spreadsheets),
107
+ charts: path.join(workspacePath, WORKSPACE_DIRS.charts),
108
+ configs: path.join(workspacePath, WORKSPACE_DIRS.configs),
109
+ helps: path.join(workspacePath, WORKSPACE_DIRS.helps),
110
+ wiki: path.join(workspacePath, WORKSPACE_DIRS.wiki),
111
+ news: path.join(workspacePath, WORKSPACE_DIRS.news),
112
+ sources: path.join(workspacePath, WORKSPACE_DIRS.sources),
113
+ summaries: path.join(workspacePath, WORKSPACE_DIRS.summaries),
114
+ searches: path.join(workspacePath, WORKSPACE_DIRS.searches),
115
+ htmls: path.join(workspacePath, WORKSPACE_DIRS.htmls),
116
+ html: path.join(workspacePath, WORKSPACE_DIRS.html),
117
+ transports: path.join(workspacePath, WORKSPACE_DIRS.transports),
118
+ github: path.join(workspacePath, WORKSPACE_DIRS.github),
119
+ // nested subdirs
120
+ wikiPages: path.join(workspacePath, WORKSPACE_DIRS.wikiPages),
121
+ wikiSources: path.join(workspacePath, WORKSPACE_DIRS.wikiSources),
122
+ // files
123
+ memory: path.join(workspacePath, WORKSPACE_FILES.memory),
124
+ sessionToken: path.join(workspacePath, WORKSPACE_FILES.sessionToken),
125
+ wikiIndex: path.join(workspacePath, WORKSPACE_FILES.wikiIndex),
126
+ wikiLog: path.join(workspacePath, WORKSPACE_FILES.wikiLog),
127
+ wikiSchema: path.join(workspacePath, WORKSPACE_FILES.wikiSchema),
128
+ wikiSummary: path.join(workspacePath, WORKSPACE_FILES.wikiSummary),
129
+ summariesIndex: path.join(workspacePath, WORKSPACE_FILES.summariesIndex),
130
+ todosItems: path.join(workspacePath, WORKSPACE_FILES.todosItems),
131
+ todosColumns: path.join(workspacePath, WORKSPACE_FILES.todosColumns),
132
+ schedulerItems: path.join(workspacePath, WORKSPACE_FILES.schedulerItems),
133
+ schedulerUserTasks: path.join(workspacePath, WORKSPACE_FILES.schedulerUserTasks),
134
+ schedulerOverrides: path.join(workspacePath, WORKSPACE_FILES.schedulerOverrides),
135
+ } as const;
136
+
137
+ export type WorkspaceDirKey = keyof typeof WORKSPACE_DIRS;
138
+ export type WorkspacePathKey = keyof typeof WORKSPACE_PATHS;
139
+
140
+ // Directories `initWorkspace()` creates eagerly on server start.
141
+ // Kept as a subset of `WORKSPACE_DIRS` so new entries are additive
142
+ // without touching `server/workspace.ts`. Everything *not* on this
143
+ // list is created lazily (first write) by its owning module.
144
+ export const EAGER_WORKSPACE_DIRS: readonly WorkspaceDirKey[] = [
145
+ "chat",
146
+ "todos",
147
+ "calendar",
148
+ "contacts",
149
+ "scheduler",
150
+ "roles",
151
+ "stories",
152
+ "images",
153
+ "markdowns",
154
+ "spreadsheets",
155
+ "charts",
156
+ "configs",
157
+ "github",
158
+ ];
@@ -0,0 +1,252 @@
1
+ // User-defined reference directories (#455).
2
+ //
3
+ // Loaded from `config/reference-dirs.json`. Users can specify external
4
+ // directories that the agent can read (but not write to).
5
+ //
6
+ // Docker mode: mounted as `:ro` — filesystem-enforced read-only.
7
+ // Non-Docker mode: prompt-based restriction only.
8
+
9
+ import { createHash } from "crypto";
10
+ import path from "path";
11
+ import os from "os";
12
+ import { log } from "../system/logger/index.js";
13
+ import { readReferenceDirsJson, writeReferenceDirsJson, isExistingDirectory } from "../utils/files/reference-dirs-io.js";
14
+ import { isRecord } from "../utils/types.js";
15
+
16
+ // ── Types ───────────────────────────────────────────────────────
17
+
18
+ export interface ReferenceDirEntry {
19
+ /** Absolute host path to the directory. */
20
+ hostPath: string;
21
+ /** Short label shown in prompt and UI. */
22
+ label: string;
23
+ }
24
+
25
+ // ── Constants ───────────────────────────────────────────────────
26
+
27
+ const MAX_ENTRIES = 20;
28
+ const MAX_LABEL_LENGTH = 100;
29
+ const CONTAINER_MOUNT_ROOT = "/mnt/readonly";
30
+
31
+ /** Home-relative directories that must never be mounted. */
32
+ const HOME_RELATIVE_BLOCKED = [".ssh", ".aws", ".gnupg", ".config/gh", ".kube", ".docker"];
33
+
34
+ /** Absolute system paths that must never be mounted. */
35
+ const SYSTEM_BLOCKED_PREFIXES = ["/etc", "/root", "/var", "/proc", "/sys", "/boot", "/private/etc", "/private/var", "/System", "/Library"];
36
+
37
+ // eslint-disable-next-line no-control-regex
38
+ const CONTROL_CHAR_RE_G = /[\x00-\x1f]/g;
39
+
40
+ // ── Validation ──────────────────────────────────────────────────
41
+
42
+ function expandHome(p: string): string {
43
+ if (p.startsWith("~/")) {
44
+ return path.join(os.homedir(), p.slice(2));
45
+ }
46
+ return p;
47
+ }
48
+
49
+ function isSensitivePath(absPath: string): boolean {
50
+ const normalized = path.resolve(absPath);
51
+
52
+ // Reject filesystem root
53
+ if (normalized === path.parse(normalized).root) return true;
54
+
55
+ const home = os.homedir();
56
+
57
+ // Block $HOME itself (transitively exposes .ssh etc.)
58
+ if (normalized === home) return true;
59
+
60
+ // Block home-relative sensitive dirs
61
+ if (
62
+ HOME_RELATIVE_BLOCKED.some((bp) => {
63
+ const full = path.join(home, bp);
64
+ return normalized === full || normalized.startsWith(full + path.sep);
65
+ })
66
+ ) {
67
+ return true;
68
+ }
69
+
70
+ // Block system directories
71
+ return SYSTEM_BLOCKED_PREFIXES.some((p) => normalized === p || normalized.startsWith(p + path.sep));
72
+ }
73
+
74
+ function sanitizeLabel(raw: string): string {
75
+ if (typeof raw !== "string") return "";
76
+ return raw.replace(CONTROL_CHAR_RE_G, " ").trim().slice(0, MAX_LABEL_LENGTH);
77
+ }
78
+
79
+ function hasTraversalSegment(p: string): boolean {
80
+ return p.split(path.sep).some((seg) => seg === "..");
81
+ }
82
+
83
+ function validateEntry(raw: unknown): ReferenceDirEntry | null {
84
+ if (!isRecord(raw)) return null;
85
+ const obj = raw as Record<string, unknown>;
86
+
87
+ const rawPath = typeof obj.hostPath === "string" ? obj.hostPath : "";
88
+ if (!rawPath) return null;
89
+
90
+ const expanded = expandHome(rawPath);
91
+
92
+ // Must be absolute
93
+ if (!path.isAbsolute(expanded)) return null;
94
+
95
+ // Normalize to collapse . and // segments
96
+ const absPath = path.resolve(expanded);
97
+
98
+ // Reject actual ".." traversal segments (not substrings in filenames)
99
+ if (hasTraversalSegment(expanded)) return null;
100
+
101
+ // Block sensitive directories
102
+ if (isSensitivePath(absPath)) {
103
+ log.warn("reference-dirs", "blocked sensitive path", { path: absPath });
104
+ return null;
105
+ }
106
+
107
+ const label = sanitizeLabel(String(obj.label ?? path.basename(absPath)));
108
+
109
+ return { hostPath: absPath, label };
110
+ }
111
+
112
+ // ── Load ────────────────────────────────────────────────────────
113
+
114
+ export function loadReferenceDirs(root?: string): ReferenceDirEntry[] {
115
+ const parsed = readReferenceDirsJson(root);
116
+ const seenLabels = new Set<string>();
117
+ const entries = parsed
118
+ .slice(0, MAX_ENTRIES)
119
+ .map(validateEntry)
120
+ .filter((e): e is ReferenceDirEntry => {
121
+ if (!e) return false;
122
+ // Deduplicate labels — first entry wins
123
+ if (seenLabels.has(e.label)) return false;
124
+ seenLabels.add(e.label);
125
+ return true;
126
+ });
127
+
128
+ const skipped = parsed.length - entries.length;
129
+ if (skipped > 0) {
130
+ log.warn("reference-dirs", "skipped invalid entries", { skipped });
131
+ }
132
+ return entries;
133
+ }
134
+
135
+ // ── Save ────────────────────────────────────────────────────────
136
+
137
+ export function saveReferenceDirs(entries: readonly ReferenceDirEntry[], root?: string): void {
138
+ writeReferenceDirsJson(entries, root);
139
+ invalidateCache();
140
+ }
141
+
142
+ // ── Validate input array (for API) ─────────────────────────────
143
+
144
+ export function validateReferenceDirs(raw: unknown): { entries: ReferenceDirEntry[] } | { error: string } {
145
+ if (!Array.isArray(raw)) {
146
+ return { error: "expected an array" };
147
+ }
148
+ if (raw.length > MAX_ENTRIES) {
149
+ return { error: `too many entries (max ${MAX_ENTRIES})` };
150
+ }
151
+ const entries: ReferenceDirEntry[] = [];
152
+ const errors: string[] = [];
153
+ raw.forEach((item, i) => {
154
+ const entry = validateEntry(item);
155
+ if (entry) {
156
+ entries.push(entry);
157
+ } else {
158
+ const p = isRecord(item) ? String((item as Record<string, unknown>).hostPath ?? "") : "";
159
+ errors.push(`entry ${i}: invalid or blocked path "${p}"`);
160
+ }
161
+ });
162
+ if (errors.length > 0) {
163
+ return { error: errors.join("; ") };
164
+ }
165
+
166
+ // Reject duplicate labels — @ref/<label> routing requires uniqueness
167
+ const seenLabels = new Set<string>();
168
+ for (const entry of entries) {
169
+ if (seenLabels.has(entry.label)) {
170
+ return { error: `duplicate label "${entry.label}"` };
171
+ }
172
+ seenLabels.add(entry.label);
173
+ }
174
+ return { entries };
175
+ }
176
+
177
+ // ── Cached loader (for system prompt + Docker mounts) ───────────
178
+
179
+ let cachedEntries: ReferenceDirEntry[] | null = null;
180
+
181
+ export function getCachedReferenceDirs(): readonly ReferenceDirEntry[] {
182
+ if (cachedEntries === null) {
183
+ cachedEntries = loadReferenceDirs();
184
+ }
185
+ return cachedEntries;
186
+ }
187
+
188
+ function invalidateCache(): void {
189
+ cachedEntries = null;
190
+ }
191
+
192
+ // ── Docker mount args ───────────────────────────────────────────
193
+
194
+ /** Container path for a reference directory.
195
+ * Disambiguates with a short hash suffix to prevent collisions
196
+ * when different host paths share the same basename. */
197
+ export function containerPath(entry: ReferenceDirEntry): string {
198
+ const basename = path.basename(entry.hostPath);
199
+ const hash = createHash("sha256").update(entry.hostPath).digest("hex").slice(0, 8);
200
+ return path.posix.join(CONTAINER_MOUNT_ROOT, `${basename}-${hash}`);
201
+ }
202
+
203
+ /**
204
+ * Return Docker `-v` args for read-only reference directory mounts.
205
+ * Skips entries whose host path doesn't exist.
206
+ */
207
+ export function referenceDirMountArgs(entries: readonly ReferenceDirEntry[]): string[] {
208
+ const args: string[] = [];
209
+ for (const entry of entries) {
210
+ if (!isExistingDirectory(entry.hostPath)) {
211
+ log.info("reference-dirs", "skipped (not found or not a directory)", {
212
+ path: entry.hostPath,
213
+ });
214
+ continue;
215
+ }
216
+ const host = entry.hostPath.replace(/\\/g, "/");
217
+ args.push("-v", `${host}:${containerPath(entry)}:ro`);
218
+ }
219
+ return args;
220
+ }
221
+
222
+ // ── System prompt snippet ───────────────────────────────────────
223
+
224
+ export function buildReferenceDirsPrompt(entries: readonly ReferenceDirEntry[], useDocker: boolean): string {
225
+ if (entries.length === 0) return "";
226
+
227
+ const lines = [
228
+ "",
229
+ "## Reference Directories (Read-Only)",
230
+ "",
231
+ "The user has configured external directories for reference.",
232
+ "You may READ files in these directories but MUST NOT write, modify, or delete anything in them.",
233
+ "",
234
+ ];
235
+
236
+ for (const e of entries) {
237
+ const mountPath = useDocker ? containerPath(e) : e.hostPath;
238
+ lines.push(`- \`${mountPath}\` — ${e.label}`);
239
+ }
240
+
241
+ if (!useDocker) {
242
+ lines.push("");
243
+ lines.push(
244
+ "**Important**: These directories are outside the workspace. " +
245
+ "Do not create, edit, or delete files in them. " +
246
+ "Only use read operations (read, glob, grep).",
247
+ );
248
+ }
249
+
250
+ lines.push("");
251
+ return lines.join("\n");
252
+ }
@@ -0,0 +1,37 @@
1
+ import path from "node:path";
2
+ import { BUILTIN_ROLES, RoleSchema, type Role } from "../../src/config/roles.js";
3
+ import { WORKSPACE_DIRS } from "./paths.js";
4
+ import { readdirUnderSync, readTextUnderSync } from "../utils/files/workspace-io.js";
5
+ import { workspacePath } from "./paths.js";
6
+
7
+ function withSwitchRole(role: Role): Role {
8
+ if (role.availablePlugins.includes("switchRole")) return role;
9
+ return {
10
+ ...role,
11
+ availablePlugins: [...role.availablePlugins, "switchRole"],
12
+ };
13
+ }
14
+
15
+ export function loadCustomRoles(): Role[] {
16
+ return readdirUnderSync(workspacePath, WORKSPACE_DIRS.roles)
17
+ .filter((f) => f.endsWith(".json"))
18
+ .flatMap((f) => {
19
+ try {
20
+ const raw = readTextUnderSync(workspacePath, path.posix.join(WORKSPACE_DIRS.roles, f));
21
+ if (!raw) return [];
22
+ return [withSwitchRole(RoleSchema.parse(JSON.parse(raw)))];
23
+ } catch {
24
+ return [];
25
+ }
26
+ });
27
+ }
28
+
29
+ export function loadAllRoles(): Role[] {
30
+ const custom = loadCustomRoles();
31
+ const builtIn = BUILTIN_ROLES.filter((r) => !custom.find((c) => c.id === r.id));
32
+ return [...builtIn, ...custom];
33
+ }
34
+
35
+ export function getRole(id: string): Role {
36
+ return loadAllRoles().find((r) => r.id === id) ?? BUILTIN_ROLES[0];
37
+ }