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,101 @@
1
+ // Request-body validators for the mulmo-script PUT endpoints. Split
2
+ // from `mulmo-script.ts` so the pure shape checks can be unit-tested
3
+ // without spinning up Express.
4
+ //
5
+ // The `@mulmocast/types` package exports zod schemas that mirror the
6
+ // canonical MulmoScript / MulmoBeat shapes. Using them here keeps the
7
+ // server and client agreeing on what a "valid script" is — the same
8
+ // schemas back client-side edit-time validation in
9
+ // `src/plugins/presentMulmoScript/View.vue`.
10
+
11
+ import { mulmoBeatSchema, mulmoScriptSchema } from "@mulmocast/types";
12
+ import { isRecord } from "../../utils/types.js";
13
+
14
+ export type ValidationResult<T> = { ok: true; value: T } | { ok: false; error: string };
15
+
16
+ function formatZodIssues(
17
+ // Zod's `$ZodIssue.path` is `PropertyKey[]` (includes `symbol`).
18
+ // Accept the wider type so callers can pass `safeParse().error.issues`
19
+ // directly; stringify any non-string/number segments at format time.
20
+ issues: ReadonlyArray<{ message: string; path: ReadonlyArray<PropertyKey> }>,
21
+ ): string {
22
+ if (issues.length === 0) return "invalid shape";
23
+ const head = issues
24
+ .slice(0, 3)
25
+ .map((i) => {
26
+ const pathStr = i.path.length > 0 ? i.path.map((seg) => String(seg)).join(".") : "<root>";
27
+ return `${pathStr}: ${i.message}`;
28
+ })
29
+ .join("; ");
30
+ return issues.length > 3 ? `${head} (+${issues.length - 3} more)` : head;
31
+ }
32
+
33
+ /**
34
+ * Validate the `update-script` request body. Returns the parsed,
35
+ * schema-conformant script on success, or a human-readable error
36
+ * suitable for sending back as a 400 response.
37
+ */
38
+ export function validateUpdateScriptBody(body: unknown): ValidationResult<{
39
+ filePath: string;
40
+ script: unknown;
41
+ }> {
42
+ if (!isRecord(body)) {
43
+ return { ok: false, error: "body must be an object" };
44
+ }
45
+ if (typeof body.filePath !== "string" || body.filePath === "") {
46
+ return { ok: false, error: "filePath must be a non-empty string" };
47
+ }
48
+ if (body.script === undefined) {
49
+ return { ok: false, error: "script is required" };
50
+ }
51
+ const parsed = mulmoScriptSchema.safeParse(body.script);
52
+ if (!parsed.success) {
53
+ return {
54
+ ok: false,
55
+ error: `invalid script: ${formatZodIssues(parsed.error.issues)}`,
56
+ };
57
+ }
58
+ return {
59
+ ok: true,
60
+ value: { filePath: body.filePath as string, script: parsed.data },
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Validate the `update-beat` request body. `beatIndex` is allowed
66
+ * to be any non-negative integer; the handler still bounds-checks
67
+ * against the actual script length after reading the file.
68
+ */
69
+ export function validateUpdateBeatBody(body: unknown): ValidationResult<{
70
+ filePath: string;
71
+ beatIndex: number;
72
+ beat: unknown;
73
+ }> {
74
+ if (!isRecord(body)) {
75
+ return { ok: false, error: "body must be an object" };
76
+ }
77
+ if (typeof body.filePath !== "string" || body.filePath === "") {
78
+ return { ok: false, error: "filePath must be a non-empty string" };
79
+ }
80
+ if (typeof body.beatIndex !== "number" || !Number.isInteger(body.beatIndex) || body.beatIndex < 0) {
81
+ return { ok: false, error: "beatIndex must be a non-negative integer" };
82
+ }
83
+ if (body.beat === undefined) {
84
+ return { ok: false, error: "beat is required" };
85
+ }
86
+ const parsed = mulmoBeatSchema.safeParse(body.beat);
87
+ if (!parsed.success) {
88
+ return {
89
+ ok: false,
90
+ error: `invalid beat: ${formatZodIssues(parsed.error.issues)}`,
91
+ };
92
+ }
93
+ return {
94
+ ok: true,
95
+ value: {
96
+ filePath: body.filePath as string,
97
+ beatIndex: body.beatIndex as number,
98
+ beat: parsed.data,
99
+ },
100
+ };
101
+ }
@@ -0,0 +1,69 @@
1
+ // PoC push endpoint — proves the server can fire a delayed message
2
+ // simultaneously to every open Web tab (pub-sub) and every bridge
3
+ // (chat-service `pushToBridge`). Stepping stone for the in-app
4
+ // notification center (#144) and external-channel notifications
5
+ // (#142); see plans/feat-notification-push-scaffold.md for the
6
+ // motivation and the production plan.
7
+ //
8
+ // Usage:
9
+ // curl -X POST http://localhost:3001/api/notifications/test \
10
+ // -H "Authorization: Bearer $(cat ~/mulmoclaude/.session-token)" \
11
+ // -H "Content-Type: application/json" \
12
+ // -d '{"message":"hello","delaySeconds":5}'
13
+ //
14
+ // The route is exported as a factory so the host wiring can inject
15
+ // the pub-sub publisher and the chat-service push handle without
16
+ // this file pulling in either module directly.
17
+
18
+ import { Router, type Request, type Response } from "express";
19
+ import { scheduleTestNotification, type NotificationDeps, type ScheduleNotificationOptions } from "../../events/notifications.js";
20
+ import { log } from "../../system/logger/index.js";
21
+ import { API_ROUTES } from "../../../src/config/apiRoutes.js";
22
+
23
+ interface TestRequestBody {
24
+ message?: unknown;
25
+ delaySeconds?: unknown;
26
+ transportId?: unknown;
27
+ chatId?: unknown;
28
+ }
29
+
30
+ interface TestResponse {
31
+ firesAt: string;
32
+ delaySeconds: number;
33
+ }
34
+
35
+ function parseBody(body: TestRequestBody): ScheduleNotificationOptions {
36
+ const opts: ScheduleNotificationOptions = {};
37
+ if (typeof body.message === "string" && body.message.length > 0) {
38
+ opts.message = body.message;
39
+ }
40
+ if (typeof body.delaySeconds === "number") {
41
+ opts.delaySeconds = body.delaySeconds;
42
+ }
43
+ if (typeof body.transportId === "string" && body.transportId.length > 0) {
44
+ opts.transportId = body.transportId;
45
+ }
46
+ if (typeof body.chatId === "string" && body.chatId.length > 0) {
47
+ opts.chatId = body.chatId;
48
+ }
49
+ return opts;
50
+ }
51
+
52
+ export function createNotificationsRouter(deps: NotificationDeps): Router {
53
+ const router = Router();
54
+ router.post(API_ROUTES.notifications.test, (req: Request<object, unknown, TestRequestBody>, res: Response<TestResponse>) => {
55
+ const opts = parseBody(req.body ?? {});
56
+ const scheduled = scheduleTestNotification(opts, deps);
57
+ log.info("notifications", "scheduled test push", {
58
+ delaySeconds: scheduled.delaySeconds,
59
+ firesAt: scheduled.firesAt,
60
+ transportId: opts.transportId,
61
+ chatId: opts.chatId,
62
+ });
63
+ res.status(202).json({
64
+ firesAt: scheduled.firesAt,
65
+ delaySeconds: scheduled.delaySeconds,
66
+ });
67
+ });
68
+ return router;
69
+ }
@@ -0,0 +1,163 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { Router, Request, Response } from "express";
4
+ import { marked } from "marked";
5
+ import puppeteer from "puppeteer";
6
+ import { errorMessage } from "../../utils/errors.js";
7
+ import { badRequest, serverError } from "../../utils/httpError.js";
8
+ import { WORKSPACE_DIRS } from "../../workspace/paths.js";
9
+ import { resolveWithinRoot, readBinarySafeSync } from "../../utils/files/safe.js";
10
+ import { resolveWorkspacePath } from "../../utils/files/workspace-io.js";
11
+ import { log } from "../../system/logger/index.js";
12
+ import { API_ROUTES } from "../../../src/config/apiRoutes.js";
13
+
14
+ const router = Router();
15
+
16
+ const MARKDOWN_CSS = `
17
+ body {
18
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
19
+ font-size: 13px;
20
+ line-height: 1.6;
21
+ color: #1f2937;
22
+ max-width: 800px;
23
+ margin: 0 auto;
24
+ padding: 32px 48px;
25
+ }
26
+ h1 { font-size: 1.75rem; font-weight: 700; margin: 0 0 0.75rem; color: #111827; }
27
+ h2 { font-size: 1.25rem; font-weight: 600; margin: 1.5rem 0 0.5rem; color: #1f2937; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.25rem; }
28
+ h3 { font-size: 1rem; font-weight: 600; margin: 1rem 0 0.4rem; color: #374151; }
29
+ p { margin: 0 0 0.75rem; }
30
+ ul, ol { margin: 0 0 0.75rem 1.5rem; }
31
+ li { margin-bottom: 0.2rem; }
32
+ ul { list-style-type: disc; }
33
+ ol { list-style-type: decimal; }
34
+ code { background: #f3f4f6; padding: 0.1rem 0.3rem; border-radius: 0.25rem; font-size: 0.85em; font-family: monospace; }
35
+ pre { background: #f3f4f6; padding: 0.75rem; border-radius: 0.375rem; overflow-x: auto; margin: 0 0 0.75rem; }
36
+ pre code { background: none; padding: 0; }
37
+ blockquote { border-left: 3px solid #d1d5db; padding-left: 1rem; color: #6b7280; margin: 0.75rem 0; }
38
+ hr { border: none; border-top: 1px solid #e5e7eb; margin: 1.25rem 0; }
39
+ table { border-collapse: collapse; width: 100%; margin: 0 0 0.75rem; font-size: 0.875rem; }
40
+ th, td { border: 1px solid #e5e7eb; padding: 0.5rem 0.75rem; text-align: left; }
41
+ th { background: #f9fafb; font-weight: 600; }
42
+ strong { font-weight: 600; }
43
+ a { color: #2563eb; }
44
+ img { max-width: 100%; height: auto; }
45
+ `;
46
+
47
+ const MIME_BY_EXT: Record<string, string> = {
48
+ ".png": "image/png",
49
+ ".jpg": "image/jpeg",
50
+ ".jpeg": "image/jpeg",
51
+ ".gif": "image/gif",
52
+ ".svg": "image/svg+xml",
53
+ ".webp": "image/webp",
54
+ };
55
+
56
+ // Realpath of the workspace, resolved once at module load. Used to
57
+ // validate that image paths resolved relative to markdowns/ stay
58
+ // inside the workspace after symlink resolution.
59
+ const workspaceReal = fs.realpathSync(resolveWorkspacePath(""));
60
+
61
+ /**
62
+ * Inline local images as base64 data URIs so Puppeteer can render them.
63
+ * Markdown files live in workspace/artifacts/documents/ and reference
64
+ * images as "../images/xyz.png" → workspace/artifacts/images/xyz.png.
65
+ *
66
+ * Paths are validated against the workspace root via resolveWithinRoot
67
+ * so an attacker-controlled <img src="../../../etc/passwd"> can't read
68
+ * files outside the workspace.
69
+ */
70
+ function inlineImages(html: string): string {
71
+ const baseDir = path.join(workspaceReal, WORKSPACE_DIRS.markdowns);
72
+ return html.replace(/(<img\s[^>]*src=")([^"]+)(")/g, (_match, before: string, src: string, after: string) => {
73
+ if (src.startsWith("data:") || src.startsWith("http")) {
74
+ return _match;
75
+ }
76
+ // Resolve the image path relative to markdowns/ but require the
77
+ // final realpath to stay inside the workspace root. markdowns/
78
+ // references like "../images/foo.png" are common so we can't
79
+ // restrict to markdowns/ itself.
80
+ const unsafeAbs = path.resolve(baseDir, src);
81
+ // Make unsafeAbs relative to the workspace for the
82
+ // resolveWithinRoot check (it expects a relative path).
83
+ const relToWorkspace = path.relative(workspaceReal, unsafeAbs);
84
+ if (relToWorkspace.startsWith("..") || path.isAbsolute(relToWorkspace)) {
85
+ log.warn("pdf", "image path escapes workspace", { src });
86
+ return _match;
87
+ }
88
+ const abs = resolveWithinRoot(workspaceReal, relToWorkspace);
89
+ if (!abs) {
90
+ log.warn("pdf", "image path rejected by safe-resolve", { src });
91
+ return _match;
92
+ }
93
+ const buf = readBinarySafeSync(abs);
94
+ if (!buf) {
95
+ log.warn("pdf", "could not read image", { abs });
96
+ return _match;
97
+ }
98
+ const ext = path.extname(abs).toLowerCase();
99
+ const mime = MIME_BY_EXT[ext] ?? "application/octet-stream";
100
+ return `${before}data:${mime};base64,${buf.toString("base64")}${after}`;
101
+ });
102
+ }
103
+
104
+ function wrapHtml(body: string, css: string): string {
105
+ return `<!DOCTYPE html>
106
+ <html>
107
+ <head>
108
+ <meta charset="utf-8">
109
+ <style>${css}</style>
110
+ </head>
111
+ <body>${body}</body>
112
+ </html>`;
113
+ }
114
+
115
+ async function renderPdf(fullHtml: string, format: "Letter" | "A4" = "Letter"): Promise<Buffer> {
116
+ const browser = await puppeteer.launch({ headless: true });
117
+ try {
118
+ const page = await browser.newPage();
119
+ await page.setContent(fullHtml, { waitUntil: "networkidle0" });
120
+ const pdfBuffer = await page.pdf({
121
+ format,
122
+ margin: { top: "16mm", bottom: "16mm", left: "16mm", right: "16mm" },
123
+ printBackground: true,
124
+ });
125
+ return Buffer.from(pdfBuffer);
126
+ } finally {
127
+ await browser.close();
128
+ }
129
+ }
130
+
131
+ function sendPdf(res: Response, buffer: Buffer, filename: string): void {
132
+ const safeFilename = filename.endsWith(".pdf") ? filename : `${filename}.pdf`;
133
+ res.setHeader("Content-Type", "application/pdf");
134
+ res.setHeader("Content-Disposition", `attachment; filename="document.pdf"; filename*=UTF-8''${encodeURIComponent(safeFilename)}`);
135
+ res.send(buffer);
136
+ }
137
+
138
+ interface PdfMarkdownBody {
139
+ markdown: string;
140
+ filename?: string;
141
+ format?: "Letter" | "A4";
142
+ }
143
+
144
+ router.post(API_ROUTES.pdf.markdown, async (req: Request<object, unknown, PdfMarkdownBody>, res: Response) => {
145
+ const { markdown, filename = "document.pdf", format = "Letter" } = req.body;
146
+
147
+ if (!markdown) {
148
+ badRequest(res, "markdown is required");
149
+ return;
150
+ }
151
+
152
+ try {
153
+ log.info("pdf", "markdown", { filename, length: markdown.length });
154
+ const html = inlineImages(await marked.parse(markdown));
155
+ const buffer = await renderPdf(wrapHtml(html, MARKDOWN_CSS), format);
156
+ sendPdf(res, buffer, filename);
157
+ } catch (err) {
158
+ log.error("pdf", "generation failed", { error: String(err) });
159
+ serverError(res, `PDF generation failed: ${errorMessage(err)}`);
160
+ }
161
+ });
162
+
163
+ export default router;
@@ -0,0 +1,276 @@
1
+ import path from "path";
2
+ import { Router, Request, Response } from "express";
3
+ import { executeMindMap } from "@gui-chat-plugin/mindmap";
4
+ import { executeSpreadsheet, type SpreadsheetArgs } from "../../../src/plugins/spreadsheet/definition.js";
5
+ import { executeQuiz } from "@mulmochat-plugin/quiz";
6
+ import { executeForm } from "@mulmochat-plugin/form";
7
+ import { executeOpenCanvas } from "../../../src/plugins/canvas/definition.js";
8
+ import { executePresent3D } from "@gui-chat-plugin/present3d";
9
+ import { generateGeminiImageFromPrompt, isGeminiAvailable } from "../../utils/gemini.js";
10
+ import { errorMessage } from "../../utils/errors.js";
11
+ import { badRequest, serverError } from "../../utils/httpError.js";
12
+ import { log } from "../../system/logger/index.js";
13
+ import { saveImage } from "../../utils/files/image-store.js";
14
+ import { saveMarkdown, overwriteMarkdown, isMarkdownPath } from "../../utils/files/markdown-store.js";
15
+ import { saveSpreadsheet, overwriteSpreadsheet, isSpreadsheetPath } from "../../utils/files/spreadsheet-store.js";
16
+ import { API_ROUTES } from "../../../src/config/apiRoutes.js";
17
+ import { WORKSPACE_DIRS } from "../../workspace/paths.js";
18
+
19
+ const router = Router();
20
+
21
+ interface PluginErrorResponse {
22
+ message: string;
23
+ }
24
+
25
+ // Wraps a plugin's `execute*` invocation in an Express handler. Each
26
+ // plugin route used to inline the same try/catch + 500 response shell;
27
+ // this collapses them to one line per route.
28
+ //
29
+ // The callback receives the Express request and is responsible for
30
+ // pulling whatever it needs out of `req.body` and forwarding it to
31
+ // the plugin's execute function. `req.body` is `any` by Express
32
+ // default and each plugin's execute function does its own runtime
33
+ // validation — matching the behavior of the inline handlers this
34
+ // replaces.
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ function wrapPluginExecute<TBody = any, TResult = unknown>(
37
+ execute: (req: Request<object, unknown, TBody>) => Promise<TResult>,
38
+ ): (req: Request<object, unknown, TBody>, res: Response<TResult | PluginErrorResponse>) => Promise<void> {
39
+ return async (req, res) => {
40
+ try {
41
+ const result = await execute(req);
42
+ res.json(result);
43
+ } catch (err) {
44
+ res.status(500).json({ message: errorMessage(err) });
45
+ }
46
+ };
47
+ }
48
+
49
+ const IMAGE_PLACEHOLDER = /!\[([^\]]+)\]\(\/?__too_be_replaced_image_path__\)/g;
50
+
51
+ async function generateImageFile(prompt: string): Promise<string | null> {
52
+ if (!isGeminiAvailable()) return null;
53
+ try {
54
+ const { imageData } = await generateGeminiImageFromPrompt(prompt);
55
+ if (imageData) return saveImage(imageData);
56
+ log.warn("present-document", "Gemini returned no image data for prompt", {
57
+ promptPreview: prompt.slice(0, 80),
58
+ });
59
+ } catch (err) {
60
+ // Surface the failure so missing-image symptoms in the canvas
61
+ // are debuggable from the server log instead of vanishing.
62
+ log.warn("present-document", "Gemini image generation failed", {
63
+ error: errorMessage(err),
64
+ promptPreview: prompt.slice(0, 80),
65
+ });
66
+ }
67
+ return null;
68
+ }
69
+
70
+ async function fillImagePlaceholders(markdown: string): Promise<string> {
71
+ const matches = [...markdown.matchAll(IMAGE_PLACEHOLDER)];
72
+ if (matches.length === 0) return markdown;
73
+ // Only attempt generation when Gemini is wired up; otherwise the
74
+ // placeholder still gets stripped below so we don't leak a broken
75
+ // <img src="...__too_be_replaced_image_path__"> into the rendered
76
+ // document.
77
+ const geminiOk = isGeminiAvailable();
78
+ if (!geminiOk) {
79
+ log.warn("present-document", "GEMINI_API_KEY not set — image placeholders will render as text markers", { placeholderCount: matches.length });
80
+ }
81
+
82
+ const results = await Promise.all(
83
+ matches.map(async (m) => ({
84
+ full: m[0],
85
+ prompt: m[1],
86
+ url: geminiOk ? await generateImageFile(m[1]) : null,
87
+ })),
88
+ );
89
+
90
+ // Surface a single tally line so the operator can see the
91
+ // success rate even when most calls go through. The per-call
92
+ // error already lands at warn from generateImageFile's catch.
93
+ if (geminiOk) {
94
+ const failed = results.filter((r) => !r.url).length;
95
+ if (failed > 0) {
96
+ log.warn("present-document", "image generation had failures", {
97
+ failed,
98
+ total: results.length,
99
+ });
100
+ }
101
+ }
102
+
103
+ let filled = markdown;
104
+ for (const { full, prompt, url } of results) {
105
+ // On success → real image. On failure / no key → italic text
106
+ // marker so the alt prompt still surfaces but no broken image
107
+ // 404s through the View. The user can re-render later once
108
+ // GEMINI_API_KEY is set.
109
+ filled = filled.replace(
110
+ full,
111
+ // `url` is workspace-relative (e.g. "artifacts/images/xxx.png").
112
+ // The document lives at "artifacts/documents/yyy.md". Compute a
113
+ // relative path from the document's directory so the markdown
114
+ // image reference resolves correctly.
115
+ url ? `![${prompt}](${path.posix.relative(WORKSPACE_DIRS.markdowns, url)})` : `*🖼️ Image: ${prompt}*`,
116
+ );
117
+ }
118
+ return filled;
119
+ }
120
+
121
+ // presentDocument — fills image placeholders via Gemini if API key is available
122
+ interface PresentDocumentBody {
123
+ title: string;
124
+ markdown: string;
125
+ filenamePrefix: string;
126
+ }
127
+
128
+ interface PresentDocumentSuccess {
129
+ message: string;
130
+ title: string;
131
+ data: { markdown: string; filenamePrefix: string };
132
+ }
133
+
134
+ interface PresentDocumentError {
135
+ error: string;
136
+ }
137
+
138
+ router.post(
139
+ API_ROUTES.plugins.presentDocument,
140
+ async (req: Request<object, unknown, PresentDocumentBody>, res: Response<PresentDocumentSuccess | PresentDocumentError>) => {
141
+ const { title, markdown, filenamePrefix } = req.body;
142
+ if (typeof filenamePrefix !== "string" || filenamePrefix.trim().length === 0) {
143
+ badRequest(res, "filenamePrefix is required");
144
+ return;
145
+ }
146
+ const filledMarkdown = await fillImagePlaceholders(markdown);
147
+ const markdownPath = await saveMarkdown(filledMarkdown, filenamePrefix);
148
+ res.json({
149
+ message: `Document "${title}" is ready.`,
150
+ title,
151
+ data: { markdown: markdownPath, filenamePrefix },
152
+ });
153
+ },
154
+ );
155
+
156
+ // Update markdown file on disk (user edits in View)
157
+ interface UpdateMarkdownBody {
158
+ markdown: string;
159
+ }
160
+
161
+ interface UpdateMarkdownResponse {
162
+ path: string;
163
+ }
164
+
165
+ interface UpdateMarkdownError {
166
+ error: string;
167
+ }
168
+
169
+ router.put(
170
+ API_ROUTES.plugins.updateMarkdown,
171
+ async (req: Request<{ filename: string }, unknown, UpdateMarkdownBody>, res: Response<UpdateMarkdownResponse | UpdateMarkdownError>) => {
172
+ const relativePath = `${WORKSPACE_DIRS.markdowns}/${req.params.filename}`;
173
+ const { markdown } = req.body;
174
+ if (!markdown) {
175
+ badRequest(res, "markdown is required");
176
+ return;
177
+ }
178
+ if (!isMarkdownPath(relativePath)) {
179
+ badRequest(res, "invalid markdown path");
180
+ return;
181
+ }
182
+ try {
183
+ await overwriteMarkdown(relativePath, markdown);
184
+ res.json({ path: relativePath });
185
+ } catch (err) {
186
+ serverError(res, errorMessage(err));
187
+ }
188
+ },
189
+ );
190
+
191
+ // `null as never` in the calls below: each plugin's `execute*`
192
+ // function expects a client-side context object as its first
193
+ // argument. The server-side bridge has no such context — these
194
+ // functions only touch their second arg (the request body) on this
195
+ // path — so we satisfy the type signature with a never cast rather
196
+ // than fabricating a fake context.
197
+
198
+ // presentSpreadsheet — validate, then save sheets to disk
199
+ router.post(
200
+ API_ROUTES.plugins.presentSpreadsheet,
201
+ wrapPluginExecute<SpreadsheetArgs, unknown>(async (req) => {
202
+ const result = await executeSpreadsheet(req.body);
203
+ if (!Array.isArray(result.data.sheets)) {
204
+ throw new Error("Expected sheets array from executeSpreadsheet");
205
+ }
206
+ const sheetsPath = await saveSpreadsheet(result.data.sheets);
207
+ return { ...result, data: { ...result.data, sheets: sheetsPath } };
208
+ }),
209
+ );
210
+
211
+ // Update spreadsheet file on disk (user edits in View)
212
+ interface UpdateSpreadsheetBody {
213
+ sheets: unknown[];
214
+ }
215
+
216
+ interface UpdateSpreadsheetResponse {
217
+ path: string;
218
+ }
219
+
220
+ interface UpdateSpreadsheetError {
221
+ error: string;
222
+ }
223
+
224
+ router.put(
225
+ API_ROUTES.plugins.updateSpreadsheet,
226
+ async (req: Request<{ filename: string }, unknown, UpdateSpreadsheetBody>, res: Response<UpdateSpreadsheetResponse | UpdateSpreadsheetError>) => {
227
+ const relativePath = `${WORKSPACE_DIRS.spreadsheets}/${req.params.filename}`;
228
+ const { sheets } = req.body;
229
+ if (!Array.isArray(sheets)) {
230
+ badRequest(res, "sheets must be an array");
231
+ return;
232
+ }
233
+ if (!isSpreadsheetPath(relativePath)) {
234
+ badRequest(res, "invalid spreadsheet path");
235
+ return;
236
+ }
237
+ try {
238
+ await overwriteSpreadsheet(relativePath, sheets);
239
+ res.json({ path: relativePath });
240
+ } catch (err) {
241
+ serverError(res, errorMessage(err));
242
+ }
243
+ },
244
+ );
245
+
246
+ // createMindMap — uses package execute for node layout computation
247
+ router.post(
248
+ API_ROUTES.plugins.mindmap,
249
+ wrapPluginExecute((req) => executeMindMap(null as never, req.body)),
250
+ );
251
+
252
+ // putQuestions — quiz
253
+ router.post(
254
+ API_ROUTES.plugins.quiz,
255
+ wrapPluginExecute((req) => executeQuiz(null as never, req.body)),
256
+ );
257
+
258
+ // presentForm — form
259
+ router.post(
260
+ API_ROUTES.plugins.form,
261
+ wrapPluginExecute((req) => executeForm(null as never, req.body)),
262
+ );
263
+
264
+ // openCanvas — drawing canvas
265
+ router.post(
266
+ API_ROUTES.plugins.canvas,
267
+ wrapPluginExecute(() => executeOpenCanvas()),
268
+ );
269
+
270
+ // present3d — 3D visualization
271
+ router.post(
272
+ API_ROUTES.plugins.present3d,
273
+ wrapPluginExecute((req) => executePresent3D(null as never, req.body)),
274
+ );
275
+
276
+ export default router;
@@ -0,0 +1,48 @@
1
+ import { Router, Request, Response } from "express";
2
+ import { WORKSPACE_DIRS } from "../../workspace/paths.js";
3
+ import { writeWorkspaceText } from "../../utils/files/workspace-io.js";
4
+ import { buildArtifactPath } from "../../utils/files/naming.js";
5
+ import { errorMessage } from "../../utils/errors.js";
6
+ import { badRequest, serverError } from "../../utils/httpError.js";
7
+ import { API_ROUTES } from "../../../src/config/apiRoutes.js";
8
+
9
+ const router = Router();
10
+
11
+ interface PresentHtmlBody {
12
+ html: string;
13
+ title?: string;
14
+ }
15
+
16
+ interface PresentHtmlSuccessResponse {
17
+ message: string;
18
+ instructions: string;
19
+ data: { html: string; title?: string; filePath: string };
20
+ }
21
+
22
+ interface PresentHtmlErrorResponse {
23
+ error: string;
24
+ }
25
+
26
+ type PresentHtmlResponse = PresentHtmlSuccessResponse | PresentHtmlErrorResponse;
27
+
28
+ router.post(API_ROUTES.html.present, async (req: Request<object, unknown, PresentHtmlBody>, res: Response<PresentHtmlResponse>) => {
29
+ const { html, title } = req.body;
30
+ if (!html) {
31
+ badRequest(res, "html is required");
32
+ return;
33
+ }
34
+
35
+ try {
36
+ const filePath = buildArtifactPath(WORKSPACE_DIRS.htmls, title, ".html", "page");
37
+ await writeWorkspaceText(filePath, html);
38
+ res.json({
39
+ message: `Saved HTML to ${filePath}`,
40
+ instructions: "Acknowledge that the HTML page has been presented to the user.",
41
+ data: { html, title, filePath },
42
+ });
43
+ } catch (err) {
44
+ serverError(res, errorMessage(err));
45
+ }
46
+ });
47
+
48
+ export default router;