mulmoclaude 0.5.2 → 0.6.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 (486) hide show
  1. package/Dockerfile.sandbox +100 -0
  2. package/README.md +17 -4
  3. package/bin/mulmoclaude.js +46 -15
  4. package/bin/prepare-dist.js +18 -2
  5. package/client/assets/chunk-CernVdwh.js +1 -0
  6. package/client/assets/chunk-D8eiyYIV-C1eAZMzz.js +1 -0
  7. package/client/assets/html2canvas-CDGcmOD3-BbPeutDg.js +5 -0
  8. package/client/assets/index-BbgSjFQ8.js +4968 -0
  9. package/client/assets/index-ECD0lgIv.css +2 -0
  10. package/client/assets/{index.es-D4YyL_Dg-BgT6a3Nd.js → index.es-DqtpmBm8-DJdTPdnc.js} +5 -5
  11. package/client/assets/material-symbols-outlined-BLDfUw-_.woff2 +0 -0
  12. package/client/assets/runtime-protocol-vue-6WYa8hAs.js +1 -0
  13. package/client/assets/runtime-vue-BVUzgYGA.js +1 -0
  14. package/client/assets/typeof-DBp4T-Ny-C2xoZtcz.js +1 -0
  15. package/client/assets/vue-1e_vz2LW.js +1 -0
  16. package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +4 -0
  17. package/client/index.html +33 -2
  18. package/package.json +20 -18
  19. package/sandbox-entrypoint.sh +106 -0
  20. package/server/accounting/accountNormalize.ts +32 -0
  21. package/server/accounting/defaultAccounts.ts +87 -0
  22. package/server/accounting/eventPublisher.ts +51 -0
  23. package/server/accounting/journal.ts +252 -0
  24. package/server/accounting/openingBalances.ts +114 -0
  25. package/server/accounting/report.ts +237 -0
  26. package/server/accounting/service.ts +718 -0
  27. package/server/accounting/snapshotCache.ts +333 -0
  28. package/server/accounting/timeSeries.ts +265 -0
  29. package/server/accounting/types.ts +148 -0
  30. package/server/agent/activeTools.ts +128 -0
  31. package/server/agent/attachmentConverter.ts +10 -5
  32. package/server/agent/backend/claude-code.ts +8 -2
  33. package/server/agent/backend/types.ts +1 -1
  34. package/server/agent/config.ts +101 -31
  35. package/server/agent/index.ts +45 -33
  36. package/server/agent/mcp-server.ts +146 -69
  37. package/server/agent/mcp-tools/index.ts +1 -5
  38. package/server/agent/mcp-tools/notify.ts +2 -22
  39. package/server/agent/mcp-tools/x.ts +0 -4
  40. package/server/agent/mcpHealth.ts +168 -0
  41. package/server/agent/plugin-names.ts +20 -77
  42. package/server/agent/prompt.ts +259 -51
  43. package/server/agent/resumeFailover.ts +1 -1
  44. package/server/agent/stream.ts +0 -1
  45. package/server/api/auth/bearerAuth.ts +5 -5
  46. package/server/api/csrfGuard.ts +1 -1
  47. package/server/api/routes/accounting.ts +366 -0
  48. package/server/api/routes/agent.ts +509 -46
  49. package/server/api/routes/attachment.ts +104 -0
  50. package/server/api/routes/chart.ts +2 -1
  51. package/server/api/routes/config.ts +12 -12
  52. package/server/api/routes/files.ts +105 -48
  53. package/server/api/routes/image.ts +70 -25
  54. package/server/api/routes/journal.ts +35 -0
  55. package/server/api/routes/mulmo-script.ts +358 -118
  56. package/server/api/routes/mulmoScriptValidate.ts +1 -1
  57. package/server/api/routes/news.ts +1 -1
  58. package/server/api/routes/notifications.ts +92 -22
  59. package/server/api/routes/notifier.ts +98 -0
  60. package/server/api/routes/pdf.ts +188 -48
  61. package/server/api/routes/plugins.ts +34 -14
  62. package/server/api/routes/presentHtml.ts +58 -3
  63. package/server/api/routes/roles.ts +1 -8
  64. package/server/api/routes/runtime-plugin.ts +224 -0
  65. package/server/api/routes/scheduler.ts +7 -5
  66. package/server/api/routes/schedulerHandlers.ts +1 -1
  67. package/server/api/routes/schedulerTasks.ts +8 -7
  68. package/server/api/routes/sessions.ts +234 -121
  69. package/server/api/routes/skills.ts +56 -51
  70. package/server/api/routes/sources.ts +52 -45
  71. package/server/api/routes/translation.ts +44 -0
  72. package/server/api/routes/wiki/frontmatter.ts +13 -65
  73. package/server/api/routes/wiki/history.ts +261 -0
  74. package/server/api/routes/wiki/pageIndex.ts +1 -1
  75. package/server/api/routes/wiki.ts +50 -26
  76. package/server/events/file-change.ts +83 -0
  77. package/server/events/notifications.ts +247 -91
  78. package/server/events/pub-sub/index.ts +1 -1
  79. package/server/events/relay-client.ts +5 -5
  80. package/server/events/scheduler-adapter.ts +2 -2
  81. package/server/events/session-store/index.ts +110 -22
  82. package/server/events/task-manager/index.ts +10 -9
  83. package/server/index.ts +509 -33
  84. package/server/notifier/engine.ts +419 -0
  85. package/server/notifier/legacy-adapters.ts +76 -0
  86. package/server/notifier/runtime-api.ts +74 -0
  87. package/server/notifier/store.ts +70 -0
  88. package/server/notifier/types.ts +121 -0
  89. package/server/plugins/dev-loader.ts +171 -0
  90. package/server/plugins/dev-watcher.ts +150 -0
  91. package/server/plugins/diagnostics.ts +188 -0
  92. package/server/plugins/preset-list.ts +52 -0
  93. package/server/plugins/preset-loader.ts +112 -0
  94. package/server/plugins/runtime-chat-api.ts +38 -0
  95. package/server/plugins/runtime-loader.ts +430 -0
  96. package/server/plugins/runtime-registry.ts +112 -0
  97. package/server/plugins/runtime-tasks-api.ts +50 -0
  98. package/server/plugins/runtime.ts +378 -0
  99. package/server/services/translation/cache.ts +72 -0
  100. package/server/services/translation/index.ts +106 -0
  101. package/server/services/translation/llm.ts +140 -0
  102. package/server/services/translation/types.ts +35 -0
  103. package/server/system/credentials.ts +13 -2
  104. package/server/system/env.ts +6 -1
  105. package/server/system/logger/formatters.ts +46 -4
  106. package/server/system/logger/index.ts +4 -4
  107. package/server/system/logger/sinks.ts +26 -5
  108. package/server/system/logger/types.ts +2 -2
  109. package/server/utils/dev-plugin-args.d.mts +11 -0
  110. package/server/utils/dev-plugin-args.mjs +43 -0
  111. package/server/utils/errors.ts +13 -4
  112. package/server/utils/files/accounting-io.ts +295 -0
  113. package/server/utils/files/atomic.ts +17 -49
  114. package/server/utils/files/attachment-store.ts +182 -0
  115. package/server/utils/files/html-io.ts +1 -7
  116. package/server/utils/files/html-store.ts +19 -0
  117. package/server/utils/files/image-store.ts +20 -22
  118. package/server/utils/files/index.ts +5 -15
  119. package/server/utils/files/journal-io.ts +7 -35
  120. package/server/utils/files/json.ts +2 -29
  121. package/server/utils/files/markdown-image-fill.ts +6 -37
  122. package/server/utils/files/markdown-store.ts +6 -21
  123. package/server/utils/files/naming.ts +3 -39
  124. package/server/utils/files/plugins-io.ts +100 -0
  125. package/server/utils/files/reference-dirs-io.ts +1 -9
  126. package/server/utils/files/roles-io.ts +2 -10
  127. package/server/utils/files/safe.ts +17 -19
  128. package/server/utils/files/scheduler-io.ts +1 -7
  129. package/server/utils/files/scheduler-overrides-io.ts +3 -12
  130. package/server/utils/files/session-io.ts +21 -30
  131. package/server/utils/files/spreadsheet-store.ts +9 -22
  132. package/server/utils/files/translation-io.ts +46 -0
  133. package/server/utils/files/user-tasks-io.ts +1 -7
  134. package/server/utils/files/workspace-io.ts +3 -79
  135. package/server/utils/gemini.ts +33 -11
  136. package/server/utils/html/htmlArtifactSplicer.ts +41 -0
  137. package/server/utils/markdown/frontmatter.ts +112 -0
  138. package/server/utils/regex.ts +56 -0
  139. package/server/utils/router.ts +41 -0
  140. package/server/utils/slug.ts +5 -3
  141. package/server/utils/time.ts +12 -0
  142. package/server/workspace/chat-index/indexer.ts +15 -2
  143. package/server/workspace/chat-index/summarizer.ts +1 -1
  144. package/server/workspace/custom-dirs.ts +1 -1
  145. package/server/workspace/helps/gemini.md +1 -1
  146. package/server/workspace/helps/guide.md +61 -0
  147. package/server/workspace/helps/index.md +4 -0
  148. package/server/workspace/helps/presenthtml.md +80 -0
  149. package/server/workspace/helps/sandbox.md +7 -0
  150. package/server/workspace/helps/storyteller.md +101 -0
  151. package/server/workspace/helps/telegram.md +1 -0
  152. package/server/workspace/helps/wiki.md +9 -7
  153. package/server/workspace/journal/archivist-cli.ts +7 -33
  154. package/server/workspace/journal/archivist-schemas.ts +5 -43
  155. package/server/workspace/journal/dailyPass.ts +34 -187
  156. package/server/workspace/journal/diff.ts +3 -28
  157. package/server/workspace/journal/index.ts +10 -81
  158. package/server/workspace/journal/indexFile.ts +3 -24
  159. package/server/workspace/journal/latestDaily.ts +51 -0
  160. package/server/workspace/journal/memoryExtractor.ts +4 -20
  161. package/server/workspace/journal/optimizationPass.ts +4 -21
  162. package/server/workspace/journal/paths.ts +4 -23
  163. package/server/workspace/journal/state.ts +6 -29
  164. package/server/workspace/memory/io.ts +213 -0
  165. package/server/workspace/memory/llm-classifier.ts +158 -0
  166. package/server/workspace/memory/migrate.ts +263 -0
  167. package/server/workspace/memory/run.ts +84 -0
  168. package/server/workspace/memory/topic-cluster.ts +218 -0
  169. package/server/workspace/memory/topic-detect.ts +67 -0
  170. package/server/workspace/memory/topic-index-hook.ts +128 -0
  171. package/server/workspace/memory/topic-io.ts +180 -0
  172. package/server/workspace/memory/topic-migrate.ts +248 -0
  173. package/server/workspace/memory/topic-run.ts +172 -0
  174. package/server/workspace/memory/topic-swap.ts +135 -0
  175. package/server/workspace/memory/topic-types.ts +142 -0
  176. package/server/workspace/memory/types.ts +83 -0
  177. package/server/workspace/news/reader.ts +4 -5
  178. package/server/workspace/paths.ts +124 -47
  179. package/server/workspace/roles.ts +2 -11
  180. package/server/workspace/skills/parser.ts +38 -55
  181. package/server/workspace/skills/user-tasks.ts +1 -2
  182. package/server/workspace/skills-preset/mc-library/SKILL.md +188 -0
  183. package/server/workspace/skills-preset.ts +196 -0
  184. package/server/workspace/sources/fetchers/githubIssues.ts +13 -11
  185. package/server/workspace/sources/fetchers/index.ts +1 -1
  186. package/server/workspace/sources/fetchers/rssParser.ts +1 -1
  187. package/server/workspace/sources/pipeline/index.ts +2 -2
  188. package/server/workspace/sources/pipeline/notify.ts +3 -3
  189. package/server/workspace/sources/pipeline/write.ts +2 -2
  190. package/server/workspace/sources/registry.ts +39 -61
  191. package/server/workspace/sources/robots.ts +1 -1
  192. package/server/workspace/tool-trace/classify.ts +2 -1
  193. package/server/workspace/tool-trace/index.ts +1 -1
  194. package/server/workspace/tool-trace/writeSearch.ts +6 -1
  195. package/server/workspace/wiki-backlinks/index.ts +19 -7
  196. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +1 -0
  197. package/server/workspace/wiki-history/hook/snapshot.mjs +98 -0
  198. package/server/workspace/wiki-history/hook/snapshot.ts +135 -0
  199. package/server/workspace/wiki-history/provision.ts +181 -0
  200. package/server/workspace/wiki-pages/io.ts +217 -0
  201. package/server/workspace/wiki-pages/snapshot.ts +380 -0
  202. package/server/workspace/workspace.ts +75 -13
  203. package/src/App.vue +115 -40
  204. package/src/_runtime/protocol-vue.ts +21 -0
  205. package/src/_runtime/vue.ts +22 -0
  206. package/src/components/ChatInput.vue +14 -10
  207. package/src/components/CopyChatButton.vue +76 -0
  208. package/src/components/FileContentRenderer.vue +67 -14
  209. package/src/components/FileTree.vue +2 -2
  210. package/src/components/FilesView.vue +17 -1
  211. package/src/components/NewsView.vue +16 -2
  212. package/src/components/NotificationBell.vue +320 -93
  213. package/src/components/PageChatComposer.vue +5 -4
  214. package/src/components/PluginLauncher.vue +42 -6
  215. package/src/components/PluginScopedRoot.vue +87 -0
  216. package/src/components/RoleSelector.vue +12 -1
  217. package/src/components/RolesView.vue +562 -0
  218. package/src/components/SentAttachmentChip.vue +102 -0
  219. package/src/components/SessionHistoryPanel.vue +109 -20
  220. package/src/components/SessionRoleIcon.vue +7 -4
  221. package/src/components/SessionSidebar.vue +20 -7
  222. package/src/components/SessionTabBar.vue +1 -1
  223. package/src/components/SettingsMcpTab.vue +4 -4
  224. package/src/components/SettingsModal.vue +2 -0
  225. package/src/components/SidebarHeader.vue +16 -5
  226. package/src/components/SourcesManager.vue +23 -9
  227. package/src/components/SourcesView.vue +1 -1
  228. package/src/components/StackView.vue +102 -6
  229. package/src/components/SuggestionsPanel.vue +105 -16
  230. package/src/components/SystemFileBanner.vue +1 -1
  231. package/src/components/TodoExplorer.vue +4 -5
  232. package/src/components/todo/TodoAddDialog.vue +2 -3
  233. package/src/components/todo/TodoEditDialog.vue +1 -2
  234. package/src/components/todo/TodoEditPanel.vue +2 -3
  235. package/src/components/todo/TodoKanbanView.vue +8 -5
  236. package/src/components/todo/TodoListView.vue +3 -5
  237. package/src/components/todo/TodoTableView.vue +7 -5
  238. package/src/composables/useAccountingChannel.ts +58 -0
  239. package/src/composables/useActiveSession.ts +4 -25
  240. package/src/composables/useAppApi.ts +6 -44
  241. package/src/composables/useClipboardCopy.ts +3 -20
  242. package/src/composables/useContentDisplay.ts +33 -2
  243. package/src/composables/useDevPluginReload.ts +23 -0
  244. package/src/composables/useDynamicFavicon.ts +5 -31
  245. package/src/composables/useEventListeners.ts +0 -20
  246. package/src/composables/useExpandedDirs.ts +4 -15
  247. package/src/composables/useFaviconState.ts +12 -46
  248. package/src/composables/useFileChange.ts +53 -0
  249. package/src/composables/useFreshPluginData.ts +6 -43
  250. package/src/composables/useHealth.ts +14 -43
  251. package/src/composables/useImageErrorRepair.ts +104 -0
  252. package/src/composables/useLatestDaily.ts +40 -0
  253. package/src/composables/useMarkdownDoc.ts +39 -0
  254. package/src/composables/useMarkdownLinkHandler.ts +1 -1
  255. package/src/composables/useMcpTools.ts +3 -16
  256. package/src/composables/useNotifications.ts +138 -112
  257. package/src/composables/usePdfDownload.ts +17 -3
  258. package/src/composables/usePendingCalls.ts +8 -26
  259. package/src/composables/usePluginErrorBoundary.ts +68 -0
  260. package/src/composables/usePubSub.ts +9 -17
  261. package/src/composables/useRunElapsed.ts +5 -22
  262. package/src/composables/useSandboxStatus.ts +4 -20
  263. package/src/composables/useSessionDerived.ts +7 -15
  264. package/src/composables/useSessionHistory.ts +70 -29
  265. package/src/composables/useSessionSync.ts +25 -3
  266. package/src/composables/useSkillsList.ts +59 -0
  267. package/src/composables/useTranslatedQueries.ts +109 -0
  268. package/src/config/apiRoutes.ts +181 -80
  269. package/src/config/historyFilters.ts +5 -3
  270. package/src/config/hostEvents.ts +17 -0
  271. package/src/config/mcpCatalog.ts +277 -5
  272. package/src/config/pubsubChannels.ts +134 -12
  273. package/src/config/roles.ts +212 -147
  274. package/src/config/systemFileDescriptors.ts +5 -5
  275. package/src/config/toolNames.ts +52 -30
  276. package/src/config/workspacePaths.ts +26 -2
  277. package/src/lang/de.ts +483 -27
  278. package/src/lang/en.ts +448 -27
  279. package/src/lang/es.ts +474 -27
  280. package/src/lang/fr.ts +476 -27
  281. package/src/lang/ja.ts +465 -27
  282. package/src/lang/ko.ts +466 -27
  283. package/src/lang/pt-BR.ts +473 -27
  284. package/src/lang/zh.ts +463 -27
  285. package/src/lib/vue-i18n.ts +1 -1
  286. package/src/lib/wiki-page/slug.ts +66 -0
  287. package/src/main.ts +85 -0
  288. package/src/plugins/_extras.ts +58 -0
  289. package/src/plugins/_generated/metas.ts +42 -0
  290. package/src/plugins/_generated/registrations.ts +44 -0
  291. package/src/plugins/_generated/server-bindings.ts +47 -0
  292. package/src/plugins/accounting/Preview.vue +106 -0
  293. package/src/plugins/accounting/View.vue +632 -0
  294. package/src/plugins/accounting/actions.ts +34 -0
  295. package/src/plugins/accounting/api.ts +301 -0
  296. package/src/plugins/accounting/components/AccountEditor.vue +250 -0
  297. package/src/plugins/accounting/components/AccountRow.vue +50 -0
  298. package/src/plugins/accounting/components/AccountsList.vue +102 -0
  299. package/src/plugins/accounting/components/AccountsModal.vue +300 -0
  300. package/src/plugins/accounting/components/BalanceSheet.vue +186 -0
  301. package/src/plugins/accounting/components/BookSettings.vue +284 -0
  302. package/src/plugins/accounting/components/BookSwitcher.vue +78 -0
  303. package/src/plugins/accounting/components/DateRangePicker.vue +140 -0
  304. package/src/plugins/accounting/components/JournalEntryForm.vue +504 -0
  305. package/src/plugins/accounting/components/JournalList.vue +553 -0
  306. package/src/plugins/accounting/components/Ledger.vue +206 -0
  307. package/src/plugins/accounting/components/NewBookForm.vue +211 -0
  308. package/src/plugins/accounting/components/OpeningBalancesForm.vue +271 -0
  309. package/src/plugins/accounting/components/ProfitLoss.vue +160 -0
  310. package/src/plugins/accounting/components/accountDraft.ts +13 -0
  311. package/src/plugins/accounting/components/accountNumbering.ts +103 -0
  312. package/src/plugins/accounting/components/accountValidation.ts +75 -0
  313. package/src/plugins/accounting/components/useLatestRequest.ts +44 -0
  314. package/src/plugins/accounting/countries.ts +158 -0
  315. package/src/plugins/accounting/currencies.ts +64 -0
  316. package/src/plugins/accounting/dates.ts +51 -0
  317. package/src/plugins/accounting/definition.ts +199 -0
  318. package/src/plugins/accounting/fiscalYear.ts +136 -0
  319. package/src/plugins/accounting/index.ts +49 -0
  320. package/src/plugins/accounting/meta.ts +91 -0
  321. package/src/plugins/accounting/timeSeriesEnums.ts +16 -0
  322. package/src/plugins/api.ts +125 -0
  323. package/src/plugins/canvas/View.vue +38 -28
  324. package/src/plugins/canvas/definition.ts +10 -8
  325. package/src/plugins/canvas/index.ts +15 -8
  326. package/src/plugins/canvas/meta.ts +12 -0
  327. package/src/plugins/chart/Preview.vue +1 -1
  328. package/src/plugins/chart/View.vue +2 -2
  329. package/src/plugins/chart/definition.ts +12 -2
  330. package/src/plugins/chart/index.ts +15 -7
  331. package/src/plugins/chart/meta.ts +18 -0
  332. package/src/plugins/editImages/definition.ts +44 -0
  333. package/src/plugins/editImages/index.ts +43 -0
  334. package/src/plugins/editImages/meta.ts +5 -0
  335. package/src/plugins/generateImage/View.vue +3 -1
  336. package/src/plugins/generateImage/definition.ts +2 -0
  337. package/src/plugins/generateImage/index.ts +13 -5
  338. package/src/plugins/generateImage/meta.ts +5 -0
  339. package/src/plugins/index.ts +35 -0
  340. package/src/plugins/manageRoles/Preview.vue +7 -4
  341. package/src/plugins/manageRoles/View.vue +12 -8
  342. package/src/plugins/manageRoles/definition.ts +6 -0
  343. package/src/plugins/manageRoles/index.ts +7 -6
  344. package/src/plugins/manageSkills/View.vue +11 -7
  345. package/src/plugins/manageSkills/definition.ts +4 -1
  346. package/src/plugins/manageSkills/index.ts +14 -7
  347. package/src/plugins/manageSkills/meta.ts +21 -0
  348. package/src/plugins/manageSource/definition.ts +4 -1
  349. package/src/plugins/manageSource/index.ts +15 -7
  350. package/src/plugins/manageSource/meta.ts +21 -0
  351. package/src/plugins/markdown/Preview.vue +10 -8
  352. package/src/plugins/markdown/View.vue +84 -17
  353. package/src/plugins/markdown/definition.ts +7 -1
  354. package/src/plugins/markdown/index.ts +15 -8
  355. package/src/plugins/markdown/meta.ts +16 -0
  356. package/src/plugins/meta-types.ts +97 -0
  357. package/src/plugins/metas.ts +224 -0
  358. package/src/plugins/presentForm/Preview.vue +4 -15
  359. package/src/plugins/presentForm/View.vue +35 -78
  360. package/src/plugins/presentForm/definition.ts +7 -6
  361. package/src/plugins/presentForm/index.ts +12 -5
  362. package/src/plugins/presentForm/meta.ts +11 -0
  363. package/src/plugins/presentForm/plugin.ts +8 -9
  364. package/src/plugins/presentForm/types.ts +0 -24
  365. package/src/plugins/presentHtml/Preview.vue +1 -8
  366. package/src/plugins/presentHtml/View.vue +401 -30
  367. package/src/plugins/presentHtml/definition.ts +8 -5
  368. package/src/plugins/presentHtml/index.ts +15 -8
  369. package/src/plugins/presentHtml/meta.ts +14 -0
  370. package/src/plugins/presentMulmoScript/View.vue +327 -107
  371. package/src/plugins/presentMulmoScript/definition.ts +34 -7
  372. package/src/plugins/presentMulmoScript/helpers.ts +4 -5
  373. package/src/plugins/presentMulmoScript/index.ts +20 -7
  374. package/src/plugins/presentMulmoScript/meta.ts +52 -0
  375. package/src/plugins/scheduler/AutomationsPreview.vue +2 -8
  376. package/src/plugins/scheduler/Preview.vue +5 -2
  377. package/src/plugins/scheduler/TasksTab.vue +16 -36
  378. package/src/plugins/scheduler/View.vue +22 -54
  379. package/src/plugins/scheduler/automationsDefinition.ts +14 -9
  380. package/src/plugins/scheduler/automationsMeta.ts +5 -0
  381. package/src/plugins/scheduler/calendarDefinition.ts +4 -7
  382. package/src/plugins/scheduler/calendarMeta.ts +28 -0
  383. package/src/plugins/scheduler/formatSchedule.ts +6 -24
  384. package/src/plugins/scheduler/index.ts +26 -52
  385. package/src/plugins/scope.ts +57 -0
  386. package/src/plugins/server-bindings-types.ts +38 -0
  387. package/src/plugins/server.ts +32 -0
  388. package/src/plugins/skill/Preview.vue +25 -0
  389. package/src/plugins/skill/View.vue +125 -0
  390. package/src/plugins/skill/definition.ts +23 -0
  391. package/src/plugins/skill/index.ts +36 -0
  392. package/src/plugins/skill/plugin.ts +31 -0
  393. package/src/plugins/skill/types.ts +21 -0
  394. package/src/plugins/spreadsheet/Preview.vue +1 -3
  395. package/src/plugins/spreadsheet/View.vue +29 -49
  396. package/src/plugins/spreadsheet/cellHighlights.ts +2 -3
  397. package/src/plugins/spreadsheet/definition.ts +5 -2
  398. package/src/plugins/spreadsheet/index.ts +15 -8
  399. package/src/plugins/spreadsheet/keyboardNav.ts +38 -0
  400. package/src/plugins/spreadsheet/meta.ts +14 -0
  401. package/src/plugins/textResponse/Preview.vue +9 -1
  402. package/src/plugins/textResponse/View.vue +59 -8
  403. package/src/plugins/textResponse/index.ts +11 -3
  404. package/src/plugins/textResponse/plugin.ts +8 -10
  405. package/src/plugins/textResponse/types.ts +28 -0
  406. package/src/plugins/wiki/Preview.vue +6 -4
  407. package/src/plugins/wiki/View.vue +463 -254
  408. package/src/plugins/wiki/components/WikiPageBody.vue +159 -0
  409. package/src/plugins/wiki/helpers.ts +17 -0
  410. package/src/plugins/wiki/history/HistoryDetail.vue +325 -0
  411. package/src/plugins/wiki/history/HistoryTab.vue +167 -0
  412. package/src/plugins/wiki/history/RestoreConfirm.vue +63 -0
  413. package/src/plugins/wiki/history/api.ts +52 -0
  414. package/src/plugins/wiki/history/diff.ts +145 -0
  415. package/src/plugins/wiki/index.ts +42 -32
  416. package/src/plugins/wiki/meta.ts +10 -0
  417. package/src/plugins/wiki/pageEditLoader.ts +53 -0
  418. package/src/plugins/wiki/route.ts +8 -0
  419. package/src/router/guards.ts +2 -1
  420. package/src/router/index.ts +19 -0
  421. package/src/router/pageRoutes.ts +1 -0
  422. package/src/tools/index.ts +50 -51
  423. package/src/tools/runtimeLoader.ts +141 -0
  424. package/src/tools/types.ts +44 -1
  425. package/src/types/notification.ts +23 -0
  426. package/src/types/pastedFile.ts +10 -0
  427. package/src/types/session.ts +61 -3
  428. package/src/types/sse.ts +21 -6
  429. package/src/utils/agent/eventDispatch.ts +12 -9
  430. package/src/utils/agent/pastedAttachment.ts +35 -0
  431. package/src/utils/agent/request.ts +32 -3
  432. package/src/utils/agent/toolCalls.ts +7 -1
  433. package/src/utils/api.ts +1 -1
  434. package/src/utils/chat/exportMarkdown.ts +243 -0
  435. package/src/utils/errors.ts +10 -2
  436. package/src/utils/files/expandedDirs.ts +1 -1
  437. package/src/utils/filesPreview/todoPreview.ts +13 -2
  438. package/src/utils/format/date.ts +1 -3
  439. package/src/utils/format/jsonSyntax.ts +5 -0
  440. package/src/utils/html/iframeHeightReporterScript.ts +62 -0
  441. package/src/utils/html/previewCsp.ts +29 -2
  442. package/src/utils/image/htmlSrcAttrs.ts +122 -0
  443. package/src/utils/image/imageRepairInlineScript.ts +115 -0
  444. package/src/utils/image/resolve.ts +17 -3
  445. package/src/utils/image/rewriteMarkdownImageRefs.ts +62 -9
  446. package/src/utils/markdown/frontmatter.ts +125 -0
  447. package/src/utils/markdown/taskList.ts +7 -2
  448. package/src/utils/plugin/runtime.ts +132 -0
  449. package/src/utils/session/mergeSessions.ts +40 -37
  450. package/src/utils/session/sessionEntries.ts +74 -18
  451. package/src/utils/session/sessionHelpers.ts +54 -10
  452. package/src/utils/tools/result.ts +76 -14
  453. package/src/vite-env.d.ts +6 -0
  454. package/client/assets/html2canvas-Cx501zZr-Bug0qRNv.js +0 -5
  455. package/client/assets/index-CY-WpQUm.css +0 -2
  456. package/client/assets/index-DbTz2Mfs.js +0 -4911
  457. package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
  458. package/server/api/routes/html.ts +0 -114
  459. package/server/api/routes/todos.ts +0 -293
  460. package/server/api/routes/todosColumnsHandlers.ts +0 -333
  461. package/server/api/routes/todosHandlers.ts +0 -274
  462. package/server/api/routes/todosItemsHandlers.ts +0 -386
  463. package/server/utils/files/todos-io.ts +0 -29
  464. package/src/components/NotificationToast.vue +0 -75
  465. package/src/plugins/editImage/definition.ts +0 -27
  466. package/src/plugins/editImage/index.ts +0 -37
  467. package/src/plugins/presentHtml/helpers.ts +0 -72
  468. package/src/plugins/scheduler/LegacySchedulerView.vue +0 -32
  469. package/src/plugins/scheduler/legacyShape.ts +0 -34
  470. package/src/plugins/todo/Preview.vue +0 -68
  471. package/src/plugins/todo/View.vue +0 -378
  472. package/src/plugins/todo/composables/useTodos.ts +0 -179
  473. package/src/plugins/todo/definition.ts +0 -45
  474. package/src/plugins/todo/index.ts +0 -62
  475. package/src/plugins/todo/labels.ts +0 -163
  476. package/src/plugins/todo/priority.ts +0 -98
  477. package/src/plugins/todo/viewModes.ts +0 -19
  478. package/src/plugins/wiki/definition.ts +0 -25
  479. package/src/tools/legacyPluginNames.ts +0 -13
  480. package/src/utils/format/frontmatter.ts +0 -80
  481. package/src/utils/image/rewriteHtmlImageRefs.ts +0 -50
  482. package/src/utils/notification/dispatch.ts +0 -58
  483. /package/client/assets/{purify.es-Fx1Nqyry-BwJECkqS.js → purify.es-Fx1Nqyry-BSVNht6S.js} +0 -0
  484. /package/src/plugins/{editImage → editImages}/Preview.vue +0 -0
  485. /package/src/plugins/{editImage → editImages}/View.vue +0 -0
  486. /package/src/{config/schedulerActions.ts → plugins/scheduler/actions.ts} +0 -0
@@ -1,14 +1,14 @@
1
- import { mkdir, readFile, realpath, writeFile } from "fs/promises";
1
+ import { mkdir, readFile, realpath } from "fs/promises";
2
2
  import path from "path";
3
3
  import { WORKSPACE_DIRS, WORKSPACE_PATHS } from "../../workspace/paths.js";
4
4
  import { shortId } from "../id.js";
5
+ import { writeFileAtomic } from "./atomic.js";
5
6
  import { yearMonthUtc } from "./naming.js";
6
7
  import { resolveWithinRoot } from "./safe.js";
7
8
 
8
9
  const IMAGES_DIR = WORKSPACE_PATHS.images;
9
10
 
10
- // Cached realpath of the images directory. resolveWithinRoot requires
11
- // its root argument to be a realpath so symlinks are handled correctly.
11
+ // resolveWithinRoot needs a realpath as its root so symlinks resolve correctly.
12
12
  let imagesDirReal: string | null = null;
13
13
 
14
14
  async function ensureImagesDir(): Promise<string> {
@@ -18,13 +18,9 @@ async function ensureImagesDir(): Promise<string> {
18
18
  return imagesDirReal;
19
19
  }
20
20
 
21
- // Resolve a workspace-relative image path (e.g. "images/abc123.png")
22
- // into an absolute path that is guaranteed to be inside the images
23
- // directory. Throws on traversal attempts or non-existent files.
21
+ // Throws on traversal. Strips a leading "images/" so callers can pass either the stored form or bare filename.
24
22
  async function safeResolve(relativePath: string): Promise<string> {
25
23
  const root = await ensureImagesDir();
26
- // Strip the leading "images/" prefix so the caller can pass either
27
- // "images/abc.png" (the stored form) or just "abc.png".
28
24
  const name = relativePath.replace(new RegExp(`^${WORKSPACE_DIRS.images}/`), "");
29
25
  const result = resolveWithinRoot(root, name);
30
26
  if (!result) {
@@ -33,40 +29,42 @@ async function safeResolve(relativePath: string): Promise<string> {
33
29
  return result;
34
30
  }
35
31
 
36
- /** Save raw base64 (no data URI prefix) as a PNG file. New files
37
- * land under `images/YYYY/MM/` (UTC) so the dir doesn't accumulate
38
- * unbounded — see #764. Returns the workspace-relative path. */
32
+ // #764 sharded under images/YYYY/MM/ (UTC). Buffer pass-through avoids re-encoding the PNG bytes.
39
33
  export async function saveImage(base64Data: string): Promise<string> {
40
34
  await ensureImagesDir();
41
35
  const partition = yearMonthUtc();
42
- const parentAbs = path.join(IMAGES_DIR, partition);
43
- await mkdir(parentAbs, { recursive: true });
44
36
  const filename = `${shortId()}.png`;
45
- await writeFile(path.join(parentAbs, filename), Buffer.from(base64Data, "base64"));
37
+ const absPath = path.join(IMAGES_DIR, partition, filename);
38
+ await writeFileAtomic(absPath, Buffer.from(base64Data, "base64"));
46
39
  return path.posix.join(WORKSPACE_DIRS.images, partition, filename);
47
40
  }
48
41
 
49
- /** Overwrite an existing image file. The relativePath must start with "images/". */
50
42
  export async function overwriteImage(relativePath: string, base64Data: string): Promise<void> {
51
43
  const absPath = await safeResolve(relativePath);
52
- await writeFile(absPath, Buffer.from(base64Data, "base64"));
44
+ await writeFileAtomic(absPath, Buffer.from(base64Data, "base64"));
53
45
  }
54
46
 
55
- /** Read an image file and return raw base64 (no data URI prefix). */
56
47
  export async function loadImageBase64(relativePath: string): Promise<string> {
57
48
  const absPath = await safeResolve(relativePath);
58
49
  const buf = await readFile(absPath);
59
50
  return buf.toString("base64");
60
51
  }
61
52
 
62
- /** Convert a data URI to raw base64. */
63
53
  export function stripDataUri(dataUri: string): string {
64
54
  return dataUri.replace(/^data:image\/[^;]+;base64,/, "");
65
55
  }
66
56
 
67
- /** Check if a string is a file reference (not a data URI). Accepts
68
- * arbitrary depth under `images/` (e.g. `images/2026/04/abc.png`)
69
- * so the per-month sharded paths from `saveImage` still validate. */
57
+ // Reject `.` / `..` segments split on either `/` or `\` so a
58
+ // traversal-shaped value can't slip past the prefix/suffix gate
59
+ // (Codex review on PR #1084 follow-up to #1052).
60
+ function hasTraversalSegment(value: string): boolean {
61
+ return value.split(/[/\\]/).some((segment) => segment === ".." || segment === ".");
62
+ }
63
+
64
+ // Accepts arbitrary depth so saveImage's images/YYYY/MM/abc.png still validates.
70
65
  export function isImagePath(value: string): boolean {
71
- return value.startsWith(`${WORKSPACE_DIRS.images}/`) && value.endsWith(".png");
66
+ if (!value.startsWith(`${WORKSPACE_DIRS.images}/`)) return false;
67
+ if (!value.endsWith(".png")) return false;
68
+ if (hasTraversalSegment(value)) return false;
69
+ return true;
72
70
  }
@@ -1,21 +1,11 @@
1
- // Consolidated workspace file I/O (#366).
2
- //
3
- // This barrel re-exports every public helper so call sites can do:
4
- //
5
- // import { writeFileAtomic, readWorkspaceText } from "../utils/files/index.js";
6
- //
7
- // Grouped by concern:
8
- //
9
- // atomic.ts — write-then-rename primitives
10
- // safe.ts — ENOENT-swallowing wrappers (stat, readdir, readText, resolveWithinRoot)
11
- // json.ts — JSON read/write (sync legacy + async atomic)
12
- // workspace-io.ts — workspace-aware helpers (path resolve + I/O in one call)
1
+ // #366: barrel for workspace file I/O. atomic = write-rename, safe = ENOENT-swallowing, json = sync read + atomic
2
+ // write, workspace-io = path resolve + I/O in one call.
13
3
 
14
4
  export { writeFileAtomic, writeFileAtomicSync, type WriteAtomicOptions } from "./atomic.js";
15
5
 
16
6
  export { isEnoent, readTextSafeSync, statSafe, statSafeAsync, readDirSafe, readDirSafeAsync, readTextOrNull, resolveWithinRoot } from "./safe.js";
17
7
 
18
- export { loadJsonFile, saveJsonFile, writeJsonAtomic, readJsonOrNull } from "./json.js";
8
+ export { loadJsonFile, writeJsonAtomic, readJsonOrNull } from "./json.js";
19
9
 
20
10
  export {
21
11
  resolveWorkspacePath,
@@ -36,9 +26,9 @@ export {
36
26
  ensureDirUnder,
37
27
  } from "./workspace-io.js";
38
28
 
39
- // ── Domain I/O ──────────────────────────────────────────────────
40
29
  export * from "./session-io.js";
41
- export * from "./todos-io.js";
30
+ // todos-io.js removed (#1145) — todo persistence moved into the
31
+ // `@mulmoclaude/todo-plugin` workspace package.
42
32
  export * from "./scheduler-io.js";
43
33
  export * from "./html-io.js";
44
34
  export * from "./reference-dirs-io.js";
@@ -1,14 +1,3 @@
1
- // Domain I/O: workspace journal (summaries)
2
- // conversations/summaries/_state.json — journal state
3
- // conversations/summaries/_index.md — browseable index
4
- // conversations/summaries/daily/YYYY/MM/DD.md — daily summaries
5
- // conversations/summaries/topics/<slug>.md — topic files
6
- // conversations/summaries/archive/topics/ — archived topics
7
- //
8
- // All functions take optional `root` for test DI.
9
- // Path helpers (summariesRoot, dailyPathFor, topicPathFor) live in
10
- // journal/paths.ts — this module wraps them with I/O.
11
-
12
1
  import path from "node:path";
13
2
  import fsp from "node:fs/promises";
14
3
  import { workspacePath } from "../../workspace/paths.js";
@@ -21,8 +10,6 @@ import { statSync } from "node:fs";
21
10
 
22
11
  const root = (rootOverride?: string) => rootOverride ?? workspacePath;
23
12
 
24
- // ── State ───────────────────────────────────────────────────────
25
-
26
13
  export function journalStateExists(rootOverride?: string): boolean {
27
14
  const filePath = path.join(summariesRoot(root(rootOverride)), STATE_FILE);
28
15
  try {
@@ -49,15 +36,11 @@ export async function writeJournalState(state: unknown, rootOverride?: string):
49
36
  await writeFileAtomic(filePath, JSON.stringify(state, null, 2));
50
37
  }
51
38
 
52
- // ── Index ───────────────────────────────────────────────────────
53
-
54
39
  export async function writeJournalIndex(markdown: string, rootOverride?: string): Promise<void> {
55
40
  const filePath = path.join(summariesRoot(root(rootOverride)), INDEX_FILE);
56
41
  await writeFileAtomic(filePath, markdown);
57
42
  }
58
43
 
59
- // ── Daily summaries ─────────────────────────────────────────────
60
-
61
44
  export async function readDailySummary(date: string, rootOverride?: string): Promise<string | null> {
62
45
  try {
63
46
  return await fsp.readFile(dailyPathFor(root(rootOverride), date), "utf-8");
@@ -74,15 +57,12 @@ export async function writeDailySummary(date: string, content: string, rootOverr
74
57
  await writeFileAtomic(dailyPathFor(root(rootOverride), date), content);
75
58
  }
76
59
 
77
- // ── Topics ──────────────────────────────────────────────────────
78
-
79
60
  export async function readTopicFile(slug: string, rootOverride?: string): Promise<string | null> {
80
61
  try {
81
62
  return await fsp.readFile(topicPathFor(root(rootOverride), slug), "utf-8");
82
63
  } catch (err) {
83
64
  if (isEnoent(err)) return null;
84
- // EACCES/EPERM must propagate — swallowing them would cause
85
- // appendOrCreateTopic to clobber an unreadable file.
65
+ // EACCES/EPERM must propagate — swallowing them would let appendOrCreateTopic clobber an unreadable file.
86
66
  throw err;
87
67
  }
88
68
  }
@@ -91,7 +71,6 @@ export async function writeTopicFile(slug: string, content: string, rootOverride
91
71
  await writeFileAtomic(topicPathFor(root(rootOverride), slug), content);
92
72
  }
93
73
 
94
- /** Append content to an existing topic, or create a new file. */
95
74
  export async function appendOrCreateTopic(slug: string, content: string, rootOverride?: string): Promise<"created" | "updated"> {
96
75
  const existing = await readTopicFile(slug, rootOverride);
97
76
  if (existing === null) {
@@ -102,7 +81,6 @@ export async function appendOrCreateTopic(slug: string, content: string, rootOve
102
81
  return "updated";
103
82
  }
104
83
 
105
- /** List topic slugs (filenames without .md). */
106
84
  export async function listTopicSlugs(rootOverride?: string): Promise<string[]> {
107
85
  const dir = path.join(summariesRoot(root(rootOverride)), TOPICS_DIR);
108
86
  try {
@@ -115,7 +93,6 @@ export async function listTopicSlugs(rootOverride?: string): Promise<string[]> {
115
93
  }
116
94
  }
117
95
 
118
- /** Read all topic files at once. Returns slug→content map. */
119
96
  export async function readAllTopicFiles(rootOverride?: string): Promise<Map<string, string>> {
120
97
  const dir = path.join(summariesRoot(root(rootOverride)), TOPICS_DIR);
121
98
  const out = new Map<string, string>();
@@ -137,8 +114,7 @@ export async function readAllTopicFiles(rootOverride?: string): Promise<Map<stri
137
114
  return out;
138
115
  }
139
116
 
140
- /** Move a topic to the archive directory. Returns false if the
141
- * source doesn't exist or the move fails. */
117
+ // Returns false if the source doesn't exist or the move fails.
142
118
  export async function archiveTopic(slug: string, rootOverride?: string): Promise<boolean> {
143
119
  const src = topicPathFor(root(rootOverride), slug);
144
120
  const dst = path.join(summariesRoot(root(rootOverride)), ARCHIVE_DIR, TOPICS_DIR, `${slug}.md`);
@@ -154,14 +130,17 @@ export async function archiveTopic(slug: string, rootOverride?: string): Promise
154
130
  }
155
131
  }
156
132
 
157
- // ── Daily file listing ──────────────────────────────────────────
158
-
159
133
  export interface DailyFileEntry {
160
134
  year: string;
161
135
  month: string;
162
136
  day: string;
163
137
  }
164
138
 
139
+ const YEAR_RE = /^\d{4}$/;
140
+ const MONTH_RE = /^\d{2}$/;
141
+ const isYearDir = (name: string) => YEAR_RE.test(name);
142
+ const isMonthDir = (name: string) => MONTH_RE.test(name);
143
+
165
144
  export async function listDailyFiles(rootOverride?: string): Promise<DailyFileEntry[]> {
166
145
  const dailyRoot = path.join(summariesRoot(root(rootOverride)), DAILY_DIR);
167
146
  const years = await safeReaddir(dailyRoot);
@@ -173,11 +152,6 @@ export async function listDailyFiles(rootOverride?: string): Promise<DailyFileEn
173
152
  return out;
174
153
  }
175
154
 
176
- const YEAR_RE = /^\d{4}$/;
177
- const MONTH_RE = /^\d{2}$/;
178
- const isYearDir = (name: string) => YEAR_RE.test(name);
179
- const isMonthDir = (name: string) => MONTH_RE.test(name);
180
-
181
155
  async function listDaysForYear(dailyRoot: string, year: string): Promise<DailyFileEntry[]> {
182
156
  const months = await safeReaddir(path.join(dailyRoot, year));
183
157
  const out: DailyFileEntry[] = [];
@@ -200,8 +174,6 @@ async function safeReaddir(dir: string): Promise<string[]> {
200
174
  }
201
175
  }
202
176
 
203
- // ── Archived topic count ────────────────────────────────────────
204
-
205
177
  export async function countArchivedTopics(rootOverride?: string): Promise<number> {
206
178
  const dir = path.join(summariesRoot(root(rootOverride)), ARCHIVE_DIR, TOPICS_DIR);
207
179
  try {
@@ -1,22 +1,9 @@
1
- // JSON file helpers synchronous (legacy) and async (preferred).
2
- //
3
- // Moved from server/utils/file.ts (issue #366 Phase 1). The old
4
- // file re-exports these for backwards compat.
5
-
6
- import { mkdirSync, promises, readFileSync, writeFileSync } from "fs";
7
- import path from "path";
1
+ import { promises, readFileSync } from "fs";
8
2
  import { writeFileAtomic } from "./atomic.js";
9
3
  import { isEnoent } from "./safe.js";
10
4
  import { log } from "../../system/logger/index.js";
11
5
 
12
- // ── Sync helpers ────────────────────────────────────────────────
13
-
14
- /**
15
- * Read and parse a JSON file synchronously. Returns `defaultValue`
16
- * on ENOENT (file not yet created) or JSON corruption (logs the
17
- * error but doesn't crash — user data files must not take down the
18
- * server). Rethrows unexpected errors (EACCES, EPERM).
19
- */
6
+ // Returns defaultValue on ENOENT or parse failure (user data files must not take down the server); rethrows EACCES/EPERM.
20
7
  export function loadJsonFile<T>(filePath: string, defaultValue: T): T {
21
8
  let raw: string;
22
9
  try {
@@ -40,24 +27,10 @@ export function loadJsonFile<T>(filePath: string, defaultValue: T): T {
40
27
  }
41
28
  }
42
29
 
43
- export function saveJsonFile(filePath: string, data: unknown): void {
44
- mkdirSync(path.dirname(filePath), { recursive: true });
45
- writeFileSync(filePath, JSON.stringify(data, null, 2));
46
- }
47
-
48
- // ── Async ───────────────────────────────────────────────────────
49
-
50
- /**
51
- * JSON-pretty-print `data` and write atomically.
52
- */
53
30
  export async function writeJsonAtomic(filePath: string, data: unknown, opts: Parameters<typeof writeFileAtomic>[2] = {}): Promise<void> {
54
31
  await writeFileAtomic(filePath, JSON.stringify(data, null, 2), opts);
55
32
  }
56
33
 
57
- /**
58
- * Read a JSON file and parse it. Returns null if the file is missing,
59
- * unreadable, or malformed.
60
- */
61
34
  export async function readJsonOrNull<T>(filePath: string): Promise<T | null> {
62
35
  try {
63
36
  const content = await promises.readFile(filePath, "utf-8");
@@ -1,14 +1,5 @@
1
- // Replace `![alt](__too_be_replaced_image_path__)` placeholders in
2
- // markdown with real Gemini-generated images saved to the workspace.
3
- // Lives under `server/utils/files/` because it sits at the seam
4
- // between markdown content and on-disk image artifacts; route
5
- // handlers (e.g. `presentDocument`) just hand off the markdown.
6
- //
7
- // Logging policy: every image generation emits start / ok / failed /
8
- // no-data lines, and every batch emits a tally. Per the timeout-policy
9
- // comment in `server/agent/mcp-server.ts`, generative-AI work MUST be
10
- // observable — silent partial failures were the exact failure mode
11
- // that hid the 10 s bridge-timeout bug.
1
+ // Per the timeout-policy comment in server/agent/mcp-server.ts, generative-AI work MUST be observable — silent
2
+ // partial failures hid the 10s bridge-timeout bug. Every image emits start/ok/failed/no-data + per-batch tally.
12
3
  import { generateGeminiImageFromPrompt, isGeminiAvailable } from "../gemini.js";
13
4
  import { errorMessage } from "../errors.js";
14
5
  import { promptMeta } from "../promptMeta.js";
@@ -21,9 +12,7 @@ const LOG_PREFIX = "present-document";
21
12
  async function generateImageFile(prompt: string, index: number, total: number): Promise<string | null> {
22
13
  if (!isGeminiAvailable()) return null;
23
14
  const startedAt = Date.now();
24
- // Prompt is user-controlled and may contain pasted URLs / emails /
25
- // credentials, so we log a `{ length, sha256 }` fingerprint instead
26
- // of a raw prefix. See `server/utils/promptMeta.ts`.
15
+ // Prompt is user-controlled and may contain credentials/PII; promptMeta logs {length, sha256} instead of raw bytes.
27
16
  const meta = promptMeta(prompt);
28
17
  log.info(LOG_PREFIX, "image gen start", {
29
18
  index,
@@ -71,33 +60,13 @@ function logBatchTally(results: PlaceholderResult[], total: number, batchStarted
71
60
  }
72
61
 
73
62
  export function buildReplacement(prompt: string, url: string | null): string {
74
- // `url` is workspace-relative (e.g. "artifacts/images/2026/04/x.png").
75
- // Emit a workspace-root absolute ref ("/...") so the resolution is
76
- // independent of where the markdown file itself lands on disk.
77
- // Since #764, documents shard under `artifacts/documents/YYYY/MM/`,
78
- // and `rewriteMarkdownImageRefs` (front-end) treats a leading "/"
79
- // as "rooted at workspace" — so a markdown reference like
80
- // "![alt](/artifacts/images/...)" works regardless of the document's
81
- // depth. A relative path computed against the unsharded root would
82
- // instead be off by two directory levels and 404 in the canvas.
63
+ // Workspace-rooted "/…" so the ref resolves the same regardless of document depth (#764 sharded documents under
64
+ // artifacts/documents/YYYY/MM/; a relative path would be off by two directory levels).
83
65
  if (url) return `![${prompt}](/${url})`;
84
- // No image: keep the alt text visible as an italic marker so the
85
- // operator can still see what *would* have been generated.
66
+ // No image: leave the alt text as an italic marker so the operator can see what *would* have been generated.
86
67
  return `*🖼️ Image: ${prompt}*`;
87
68
  }
88
69
 
89
- /**
90
- * Replace every `![alt](__too_be_replaced_image_path__)` placeholder
91
- * in the input markdown with a real Gemini-generated image.
92
- *
93
- * - When `GEMINI_API_KEY` is unset, every placeholder degrades to an
94
- * italic text marker (`*🖼️ Image: <alt>*`) so the document still
95
- * renders without broken image refs.
96
- * - On per-image failure, the same fallback applies for that one
97
- * placeholder. Other placeholders proceed independently.
98
- * - All generation runs in parallel via `Promise.all` — typical 9-image
99
- * batches finish in 15-25 s rather than per-image-serial.
100
- */
101
70
  export async function fillMarkdownImagePlaceholders(markdown: string): Promise<string> {
102
71
  const matches = [...markdown.matchAll(IMAGE_PLACEHOLDER)];
103
72
  if (matches.length === 0) return markdown;
@@ -1,44 +1,29 @@
1
- import { mkdir, readFile, writeFile } from "fs/promises";
1
+ import { readFile } from "fs/promises";
2
2
  import path from "path";
3
3
  import { workspacePath } from "../../workspace/workspace.js";
4
4
  import { WORKSPACE_DIRS } from "../../workspace/paths.js";
5
+ import { writeFileAtomic } from "./atomic.js";
5
6
  import { buildArtifactPathRandom } from "./naming.js";
6
7
 
7
- /**
8
- * Save markdown content as a file. Returns the workspace-relative path.
9
- * `prefix` is slugified; a random id is always appended to prevent
10
- * collisions between concurrent writers sharing the same prefix.
11
- *
12
- * `buildArtifactPathRandom` now injects a `YYYY/MM` partition (#764),
13
- * so this function ensures the partition directory exists before
14
- * writing — `writeFile` doesn't create missing parents on its own.
15
- */
8
+ // Random-id suffix prevents collisions between concurrent writers sharing a prefix; #764 sharded under YYYY/MM.
16
9
  export async function saveMarkdown(content: string, prefix: string): Promise<string> {
17
10
  const relPath = buildArtifactPathRandom(WORKSPACE_DIRS.markdowns, prefix, ".md", "document");
18
11
  const absPath = path.join(workspacePath, relPath);
19
- await mkdir(path.dirname(absPath), { recursive: true });
20
- await writeFile(absPath, content, "utf-8");
12
+ await writeFileAtomic(absPath, content);
21
13
  return relPath;
22
14
  }
23
15
 
24
- /** Read a markdown file and return its content. */
25
16
  export async function loadMarkdown(relativePath: string): Promise<string> {
26
17
  const absPath = path.join(workspacePath, relativePath);
27
18
  return readFile(absPath, "utf-8");
28
19
  }
29
20
 
30
- /** Overwrite an existing markdown file. */
31
21
  export async function overwriteMarkdown(relativePath: string, content: string): Promise<void> {
32
22
  const absPath = path.join(workspacePath, relativePath);
33
- await writeFile(absPath, content, "utf-8");
23
+ await writeFileAtomic(absPath, content);
34
24
  }
35
25
 
36
- /** Check if a string is a markdown file path (not inline content).
37
- * Rejects traversal attempts like `artifacts/documents/../outside.md`
38
- * so callers can rely on prefix+suffix alone. Mirrors the
39
- * `isSpreadsheetPath` policy. The server-side `path.join` in
40
- * `overwriteMarkdown` does NOT normalize traversal on its own, so
41
- * this gate is the primary defence — keep it strict. */
26
+ // Strict overwriteMarkdown's path.join doesn't normalize traversal, so this gate is the primary defence.
42
27
  export function isMarkdownPath(value: string): boolean {
43
28
  if (!value.startsWith(`${WORKSPACE_DIRS.markdowns}/`)) return false;
44
29
  if (!value.endsWith(".md")) return false;
@@ -1,59 +1,23 @@
1
- // Workspace file naming conventions.
2
- //
3
- // Centralizes the `slug-${Date.now()}.ext` pattern used across
4
- // multiple plugins (chart, presentHtml, markdown, spreadsheet, image).
5
- // Call sites pass a human title + extension; this module handles
6
- // slugification and timestamp suffixing.
7
-
8
1
  import path from "node:path";
9
2
  import { shortId } from "../id.js";
10
3
  import { slugify } from "../slug.js";
11
4
 
12
- /**
13
- * UTC-based `YYYY/MM` partition segment for new artifacts (#764).
14
- * Keeps each artifact directory from accumulating a flat list of
15
- * thousands of files. UTC is used (rather than local time) so a
16
- * workspace synced across machines / timezones still groups files
17
- * into the same bucket.
18
- *
19
- * Exported for unit tests and callers that need the partition without
20
- * also generating a filename (e.g. saveImage / saveSpreadsheet).
21
- */
5
+ // #764 partitioning. UTC (not local) so a workspace synced across timezones still groups into the same bucket.
22
6
  export function yearMonthUtc(now: Date = new Date()): string {
23
7
  const year = now.getUTCFullYear();
24
8
  const month = String(now.getUTCMonth() + 1).padStart(2, "0");
25
9
  return `${year}/${month}`;
26
10
  }
27
11
 
28
- /**
29
- * Build a workspace-relative path for a new artifact file.
30
- *
31
- * @param dir Workspace-relative directory (e.g. WORKSPACE_DIRS.charts)
32
- * @param title Human-readable title (slugified for the filename)
33
- * @param ext File extension with leading dot (e.g. ".html", ".json")
34
- * @param fallbackSlug Slug to use when title is empty/undefined
35
- * @returns Workspace-relative path like "artifacts/charts/2026/04/sales-1776135210389.chart.json"
36
- */
37
12
  export function buildArtifactPath(dir: string, title: string | undefined, ext: string, fallbackSlug = "file"): string {
38
13
  const slug = title ? slugify(title) || fallbackSlug : fallbackSlug;
39
14
  const fname = `${slug}-${Date.now()}${ext}`;
40
15
  return path.posix.join(dir, yearMonthUtc(), fname);
41
16
  }
42
17
 
43
- /**
44
- * Like `buildArtifactPath`, but appends a random hex id instead of a
45
- * timestamp. Use when multiple concurrent writers may share the same
46
- * prefix within the same millisecond (e.g. LLM-supplied `filenamePrefix`
47
- * on the `presentDocument` route).
48
- *
49
- * @param dir Workspace-relative directory
50
- * @param prefix Human-readable prefix (slugified via `slugify`)
51
- * @param ext File extension with leading dot
52
- * @param fallbackSlug Slug to use when the sanitized prefix is empty
53
- */
18
+ // shortId variant for concurrent writers that share a prefix within the same millisecond (presentDocument route).
54
19
  export function buildArtifactPathRandom(dir: string, prefix: string, ext: string, fallbackSlug = "file"): string {
55
- // Pass fallbackSlug as slugify's default so it overrides slugify's
56
- // built-in "page" default when `prefix` sanitizes to empty.
20
+ // Pass fallbackSlug as slugify's default so it overrides slugify's built-in "page" when `prefix` sanitizes to empty.
57
21
  const slug = slugify(prefix, fallbackSlug);
58
22
  const fname = `${slug}-${shortId()}${ext}`;
59
23
  return path.posix.join(dir, yearMonthUtc(), fname);
@@ -0,0 +1,100 @@
1
+ // Install ledger I/O for runtime-loaded plugins (#1043 C-2).
2
+ //
3
+ // The ledger is `~/mulmoclaude/plugins/plugins.json`, listing every
4
+ // plugin the user has installed via the install CLI / web UI. Each
5
+ // entry pairs the npm package id with the on-disk tgz filename; the
6
+ // loader replays this at boot to know what to extract from
7
+ // `plugins/` into `plugins/.cache/<name>/<version>/`.
8
+ //
9
+ // Truncating or deleting this file removes nothing on disk but
10
+ // "uninstalls" all runtime plugins on the next boot — the tgz files
11
+ // in `plugins/` and the cache mirror are GC'd on the following start.
12
+ // Editing it by hand is a supported recovery path.
13
+ //
14
+ // Reads tolerate missing / malformed JSON (returns []), so a half-
15
+ // written ledger never bricks server boot. Writes go through the
16
+ // atomic helper, so a crashed install can't leave a corrupt file.
17
+
18
+ import { existsSync } from "node:fs";
19
+ import { mkdir, readFile } from "node:fs/promises";
20
+ import path from "node:path";
21
+ import { loadJsonFile, writeJsonAtomic } from "./json.js";
22
+ import { WORKSPACE_PATHS } from "../../workspace/paths.js";
23
+
24
+ export interface LedgerEntry {
25
+ /** npm package name, e.g. `@gui-chat-plugin/weather`. */
26
+ name: string;
27
+ /** Semver string from the tgz's `package.json`, e.g. `0.1.0`. */
28
+ version: string;
29
+ /** Basename of the tgz inside `plugins/`. Joined with
30
+ * `WORKSPACE_PATHS.plugins` to read. */
31
+ tgz: string;
32
+ /** ISO 8601 timestamp of the install. */
33
+ installedAt: string;
34
+ }
35
+
36
+ const isLedgerEntry = (value: unknown): value is LedgerEntry => {
37
+ if (typeof value !== "object" || value === null) return false;
38
+ const obj = value as Record<string, unknown>;
39
+ return typeof obj.name === "string" && typeof obj.version === "string" && typeof obj.tgz === "string" && typeof obj.installedAt === "string";
40
+ };
41
+
42
+ const sanitiseLedger = (raw: unknown): LedgerEntry[] => {
43
+ if (!Array.isArray(raw)) return [];
44
+ return raw.filter(isLedgerEntry);
45
+ };
46
+
47
+ export function readLedger(): LedgerEntry[] {
48
+ const raw = loadJsonFile<unknown>(WORKSPACE_PATHS.pluginsLedger, []);
49
+ return sanitiseLedger(raw);
50
+ }
51
+
52
+ export async function writeLedger(entries: readonly LedgerEntry[]): Promise<void> {
53
+ const dir = path.dirname(WORKSPACE_PATHS.pluginsLedger);
54
+ if (!existsSync(dir)) {
55
+ await mkdir(dir, { recursive: true });
56
+ }
57
+ await writeJsonAtomic(WORKSPACE_PATHS.pluginsLedger, [...entries]);
58
+ }
59
+
60
+ /** Read a runtime-plugin asset (extracted under
61
+ * `plugins/.cache/<name>/<version>/`) and return its bytes plus
62
+ * the inferred Content-Type. The route handler in
63
+ * `runtime-plugin.ts` was previously calling `fs.readFile` directly
64
+ * — per `CLAUDE.md` route handlers must go through a domain helper,
65
+ * so the lookup table + read live here (#1077 review). */
66
+ export interface PluginAsset {
67
+ data: Buffer;
68
+ contentType: string;
69
+ }
70
+
71
+ export async function readPluginAsset(absPath: string): Promise<PluginAsset> {
72
+ const data = await readFile(absPath);
73
+ const ext = path.extname(absPath).toLowerCase();
74
+ return { data, contentType: pluginAssetContentType(ext) };
75
+ }
76
+
77
+ // Lookup table over a switch — flat data structure stays under
78
+ // `sonarjs/cognitive-complexity` while keeping the per-extension
79
+ // mapping easy to scan.
80
+ const PLUGIN_ASSET_CONTENT_TYPES: Readonly<Record<string, string>> = {
81
+ ".js": "application/javascript; charset=utf-8",
82
+ ".mjs": "application/javascript; charset=utf-8",
83
+ ".cjs": "application/javascript; charset=utf-8",
84
+ ".css": "text/css; charset=utf-8",
85
+ ".json": "application/json; charset=utf-8",
86
+ ".map": "application/json; charset=utf-8",
87
+ ".html": "text/html; charset=utf-8",
88
+ ".svg": "image/svg+xml",
89
+ ".png": "image/png",
90
+ ".jpg": "image/jpeg",
91
+ ".jpeg": "image/jpeg",
92
+ ".gif": "image/gif",
93
+ ".webp": "image/webp",
94
+ ".woff": "font/woff",
95
+ ".woff2": "font/woff2",
96
+ };
97
+
98
+ export function pluginAssetContentType(ext: string): string {
99
+ return PLUGIN_ASSET_CONTENT_TYPES[ext] ?? "application/octet-stream";
100
+ }
@@ -1,9 +1,3 @@
1
- // Domain I/O for reference directories.
2
- //
3
- // Reads/writes config/reference-dirs.json and checks host paths.
4
- // All fs access is funneled through shared helpers so path changes
5
- // propagate from a single constant.
6
-
7
1
  import { mkdirSync, statSync } from "fs";
8
2
  import path from "path";
9
3
  import { WORKSPACE_DIRS, workspacePath } from "../../workspace/paths.js";
@@ -17,7 +11,7 @@ function configPath(root: string): string {
17
11
  return path.join(root, WORKSPACE_DIRS.configs, CONFIG_FILE_NAME);
18
12
  }
19
13
 
20
- /** Read reference-dirs.json. Returns [] on missing/corrupt file. */
14
+ // Returns [] on missing/corrupt file.
21
15
  export function readReferenceDirsJson(root?: string): unknown[] {
22
16
  const filePath = configPath(root ?? workspacePath);
23
17
  const parsed = loadJsonFile<unknown>(filePath, []);
@@ -28,14 +22,12 @@ export function readReferenceDirsJson(root?: string): unknown[] {
28
22
  return parsed;
29
23
  }
30
24
 
31
- /** Write reference-dirs.json atomically. Creates config/ if needed. */
32
25
  export function writeReferenceDirsJson(entries: readonly unknown[], root?: string): void {
33
26
  const filePath = configPath(root ?? workspacePath);
34
27
  mkdirSync(path.dirname(filePath), { recursive: true });
35
28
  writeFileAtomicSync(filePath, JSON.stringify(entries, null, 2));
36
29
  }
37
30
 
38
- /** Check whether a host path exists and is a directory. */
39
31
  export function isExistingDirectory(hostPath: string): boolean {
40
32
  try {
41
33
  return statSync(hostPath).isDirectory();