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
@@ -0,0 +1,243 @@
1
+ // Convert an in-memory chat session (a sequence of ToolResultComplete
2
+ // items + per-uuid timestamps) into a self-contained Markdown document
3
+ // suitable for pasting into a doc, an issue, or another chat.
4
+ //
5
+ // Each turn is rendered as a `## ⬜︎ Speaker · HH:MM` heading followed
6
+ // by the message body, with `---` horizontal rules between turns.
7
+ // Headings inside message bodies are demoted by 2 levels (e.g. an
8
+ // assistant `# Heading` becomes `### Heading`) so the speaker headings
9
+ // always sit above message-internal structure in the document outline.
10
+ //
11
+ // Tool calls (anything other than `text-response`) render as a `## ⬛︎
12
+ // toolName HH:MM` heading — a compact marker showing which tools the
13
+ // assistant invoked. Tool payloads are intentionally omitted to keep
14
+ // the export readable; users wanting fidelity can view the raw JSONL.
15
+ // The one exception is `presentDocument`: its `data.markdown` is
16
+ // itself a piece of prose worth reading out of context, so we inline
17
+ // the document body (demoted by 2 levels) under the marker. In real
18
+ // sessions `data.markdown` is usually a workspace path
19
+ // (`artifacts/documents/*.md`) rather than inline text, so the export
20
+ // is async — callers pass a `readFile` resolver that reads the file
21
+ // off the workspace, mirroring the in-app Markdown View's loader.
22
+
23
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
24
+ import { isRecord } from "../types";
25
+
26
+ const TEXT_RESPONSE_TOOL = "text-response";
27
+ const PRESENT_DOCUMENT_TOOL = "presentDocument";
28
+
29
+ /** Heuristic for the file-path mode of presentDocument's `markdown` field
30
+ * (server-side documents stored under `artifacts/documents/*.md`). When
31
+ * matched, the value is a path — not inline content — and the export
32
+ * has to read the file off the workspace via the caller-supplied
33
+ * resolver to inline the body. */
34
+ function looksLikeDocumentPath(value: string): boolean {
35
+ return value.endsWith(".md") && value.startsWith("artifacts/documents/");
36
+ }
37
+
38
+ const ROLE_LABELS = {
39
+ user: "⬜︎ You",
40
+ assistant: "⬛︎ Assistant",
41
+ system: "◇ System",
42
+ } as const;
43
+
44
+ type Role = keyof typeof ROLE_LABELS;
45
+
46
+ export interface ExportChatOptions {
47
+ /** Friendly role / persona name shown in the document title (e.g. "General"). */
48
+ sessionRoleName?: string;
49
+ /** ISO string for the document's "Exported …" line. Defaults to `new Date()`. */
50
+ exportedAt?: string;
51
+ /** Per-uuid epoch-ms map matching `ActiveSession.resultTimestamps`. */
52
+ resultTimestamps?: Map<string, number>;
53
+ /** Resolver for workspace-relative file paths (currently the
54
+ * `artifacts/documents/*.md` form used by presentDocument). Returns
55
+ * the file's text content, or null if the read fails. Omit it to
56
+ * skip file-mode resolution and emit only the marker line. */
57
+ readFile?: (path: string) => Promise<string | null>;
58
+ }
59
+
60
+ /** Format `epochMs` as `HH:MM` in 24h, locale-independent. */
61
+ function formatHHMM(epochMs: number): string {
62
+ const date = new Date(epochMs);
63
+ const hours = String(date.getHours()).padStart(2, "0");
64
+ const minutes = String(date.getMinutes()).padStart(2, "0");
65
+ return `${hours}:${minutes}`;
66
+ }
67
+
68
+ /** Narrow `data?.role` to a known speaker label. Defaults to "assistant". */
69
+ function roleOf(result: ToolResultComplete): Role {
70
+ const { data } = result;
71
+ // Own-property check via Object.prototype.hasOwnProperty so an
72
+ // inherited key on the runtime ROLE_LABELS object (e.g. `toString`,
73
+ // or anything that crawled in via Object.prototype pollution)
74
+ // can't satisfy the gate and produce a `## undefined` speaker line
75
+ // (#1065 review).
76
+ if (isRecord(data) && typeof data.role === "string" && Object.prototype.hasOwnProperty.call(ROLE_LABELS, data.role)) {
77
+ return data.role as Role;
78
+ }
79
+ return "assistant";
80
+ }
81
+
82
+ /** Pull the displayable text from a text-response result. Falls back to
83
+ * `message` for older/saved sessions where `data.text` may be missing. */
84
+ function textOf(result: ToolResultComplete): string {
85
+ const { data } = result;
86
+ if (isRecord(data) && typeof data.text === "string") return data.text;
87
+ return result.message ?? "";
88
+ }
89
+
90
+ function isTextResponse(result: ToolResultComplete): boolean {
91
+ return result.toolName === TEXT_RESPONSE_TOOL;
92
+ }
93
+
94
+ // Allow up to 3 leading spaces before the fence run, per GFM. A
95
+ // 4-space indent is a regular indented code block and isn't a fence
96
+ // in the first place; the heading-demotion path doesn't need to skip
97
+ // inside those because they preserve the literal characters anyway,
98
+ // but a real fence indented by 1-3 spaces (legal GFM) would
99
+ // previously slip past `matchFenceRun` and any `# heading` line
100
+ // inside got mistakenly demoted (#1065 review).
101
+ const FENCE_RUN_RE = /^ {0,3}(`{3,}|~{3,})/;
102
+ const ATX_HEADING_RE = /^(#{1,6})([ \t].*)$/;
103
+
104
+ interface OpenFence {
105
+ char: "`" | "~";
106
+ len: number;
107
+ }
108
+
109
+ /** Match the leading run of fence characters on `line` (allowing up
110
+ * to 3 leading spaces, per GFM), if any. Captures the fence char +
111
+ * length so the closing logic can apply GFM's rules (close fence
112
+ * must be the same char and at least as long as the open). */
113
+ function matchFenceRun(line: string): OpenFence | null {
114
+ const match = FENCE_RUN_RE.exec(line);
115
+ if (!match) return null;
116
+ const [, run] = match;
117
+ return { char: run[0] as "`" | "~", len: run.length };
118
+ }
119
+
120
+ /** A line closes `open` only when it uses the same fence char, has at
121
+ * least as many of them, and carries no info-string after the run.
122
+ * Anything else inside the fence is content (including a different
123
+ * fence type or a shorter run). The fence run can sit after up to
124
+ * 3 leading spaces; the closing check looks at content after the
125
+ * fence run wherever it lands on the line. */
126
+ function isClosingFence(line: string, fence: OpenFence, open: OpenFence): boolean {
127
+ if (fence.char !== open.char) return false;
128
+ if (fence.len < open.len) return false;
129
+ const runStart = line.search(/[`~]/);
130
+ return runStart >= 0 ? line.slice(runStart + fence.len).trim() === "" : false;
131
+ }
132
+
133
+ /** Demote every ATX heading inside `markdown` by `levels` (`#` → `#`+levels),
134
+ * capping at h6. Skips lines inside fenced code blocks so `# comment`
135
+ * lines in code samples are left alone. Honours GFM fence rules: a
136
+ * block opened with N backticks (or N tildes) only closes on a line of
137
+ * the same character with ≥N of them and nothing else, so nested
138
+ * shorter fences and the opposite fence char both count as content. */
139
+ function demoteHeadings(markdown: string, levels: number): string {
140
+ if (levels <= 0 || markdown.length === 0) return markdown;
141
+ const out: string[] = [];
142
+ let openFence: OpenFence | null = null;
143
+ for (const line of markdown.split(/\r\n|\r|\n/)) {
144
+ const fence = matchFenceRun(line);
145
+ if (openFence !== null) {
146
+ if (fence !== null && isClosingFence(line, fence, openFence)) {
147
+ openFence = null;
148
+ }
149
+ out.push(line);
150
+ continue;
151
+ }
152
+ if (fence !== null) {
153
+ openFence = fence;
154
+ out.push(line);
155
+ continue;
156
+ }
157
+ const match = ATX_HEADING_RE.exec(line);
158
+ if (!match) {
159
+ out.push(line);
160
+ continue;
161
+ }
162
+ const [, hashes, rest] = match;
163
+ const newDepth = Math.min(6, hashes.length + levels);
164
+ out.push(`${"#".repeat(newDepth)}${rest}`);
165
+ }
166
+ return out.join("\n");
167
+ }
168
+
169
+ function renderTextTurn(result: ToolResultComplete, timestamps: Map<string, number>): string {
170
+ const role = roleOf(result);
171
+ const epochMs = timestamps.get(result.uuid);
172
+ // Use `!== undefined` rather than truthiness so a 0 (Unix epoch)
173
+ // timestamp still renders. Practically irrelevant for live chat, but
174
+ // it removes a foot-gun for any synthetic / migrated session that
175
+ // ends up with that boundary value.
176
+ const time = epochMs !== undefined ? ` · ${formatHHMM(epochMs)}` : "";
177
+ // Speaker is `##`; demote any in-body heading by 2 so it always sits
178
+ // strictly below the speaker (`#` → `###`, `##` → `####`, …).
179
+ const body = demoteHeadings(textOf(result).trim(), 2);
180
+ return body.length > 0 ? `## ${ROLE_LABELS[role]}${time}\n\n${body}` : `## ${ROLE_LABELS[role]}${time}`;
181
+ }
182
+
183
+ /** Resolve presentDocument's `data.markdown` to inline content. If the
184
+ * value is a workspace path, defer to `readFile`; otherwise treat it
185
+ * as inline markdown. Returns null when the data is missing/empty or
186
+ * the file read fails. */
187
+ async function presentDocumentBody(result: ToolResultComplete, readFile: ExportChatOptions["readFile"]): Promise<string | null> {
188
+ const { data } = result;
189
+ if (!isRecord(data)) return null;
190
+ const { markdown } = data;
191
+ if (typeof markdown !== "string" || markdown.length === 0) return null;
192
+ if (!looksLikeDocumentPath(markdown)) return markdown;
193
+ if (!readFile) return null;
194
+ // Honour the documented "returns null on failure" contract — a
195
+ // single rejected resolver shouldn't blow up the whole export.
196
+ try {
197
+ return await readFile(markdown);
198
+ } catch {
199
+ return null;
200
+ }
201
+ }
202
+
203
+ async function renderToolTurn(result: ToolResultComplete, timestamps: Map<string, number>, readFile: ExportChatOptions["readFile"]): Promise<string> {
204
+ const epochMs = timestamps.get(result.uuid);
205
+ const time = epochMs !== undefined ? ` ${formatHHMM(epochMs)}` : "";
206
+ const marker = `## ⬛︎ ${result.toolName}${time}`;
207
+
208
+ if (result.toolName === PRESENT_DOCUMENT_TOOL) {
209
+ const documentBody = await presentDocumentBody(result, readFile);
210
+ if (documentBody !== null) {
211
+ return `${marker}\n\n${demoteHeadings(documentBody.trim(), 2)}`;
212
+ }
213
+ }
214
+
215
+ return marker;
216
+ }
217
+
218
+ /** Build the document header. Title and the "Exported" subtitle are
219
+ * intentionally plain — the conversation body is what the reader cares
220
+ * about. The horizontal rule separates header from first turn. */
221
+ function renderHeader(opts: ExportChatOptions): string {
222
+ const role = opts.sessionRoleName?.trim();
223
+ const exportedAt = new Date(opts.exportedAt ?? new Date().toISOString());
224
+ const dateStamp = exportedAt.toISOString().slice(0, 16).replace("T", " ");
225
+ const title = role ? `# Conversation · ${role}` : "# Conversation";
226
+ return `${title}\n\n*Exported ${dateStamp} UTC*\n\n---`;
227
+ }
228
+
229
+ /** Convert a chat session to a Markdown string. Async because
230
+ * presentDocument's body may live on disk and need a `readFile` round
231
+ * trip. Returns at minimum a non-empty header so callers never have
232
+ * to special-case empty sessions. */
233
+ export async function exportChatToMarkdown(results: readonly ToolResultComplete[], options: ExportChatOptions = {}): Promise<string> {
234
+ const timestamps = options.resultTimestamps ?? new Map<string, number>();
235
+ const turns = await Promise.all(
236
+ results.map((result) =>
237
+ isTextResponse(result) ? Promise.resolve(renderTextTurn(result, timestamps)) : renderToolTurn(result, timestamps, options.readFile),
238
+ ),
239
+ );
240
+ const body = turns.join("\n\n---\n\n");
241
+ const header = renderHeader(options);
242
+ return body.length > 0 ? `${header}\n\n${body}\n` : `${header}\n`;
243
+ }
@@ -6,13 +6,21 @@
6
6
  // `err instanceof Error ? err.message : String(err)` — searching for
7
7
  // one canonical helper is easier than grepping for the inline form.
8
8
  //
9
+ // Non-Error objects with a `details` (gRPC convention) or `message`
10
+ // string field have that field surfaced — without this, gRPC errors
11
+ // like `{ code, details, metadata }` show up as `[object Object]`.
12
+ //
9
13
  // The optional `fallback` covers the common idiom of surfacing a
10
14
  // descriptive message ("Invalid JSON", "Connection error.") when a
11
- // throw turns out to be a non-Error value — otherwise `String(err)`
12
- // yields noise like `[object Object]`.
15
+ // throw turns out to be a non-Error value.
13
16
 
14
17
  export function errorMessage(err: unknown, fallback?: string): string {
15
18
  if (err instanceof Error) return err.message;
19
+ if (err !== null && typeof err === "object") {
20
+ const obj = err as { details?: unknown; message?: unknown };
21
+ if (typeof obj.details === "string" && obj.details) return obj.details;
22
+ if (typeof obj.message === "string" && obj.message) return obj.message;
23
+ }
16
24
  if (fallback !== undefined) return fallback;
17
25
  return String(err);
18
26
  }
@@ -6,7 +6,7 @@ export const EXPANDED_DIRS_STORAGE_KEY = "files_expanded_dirs";
6
6
  // Default: only the workspace root ("") is expanded — matches the
7
7
  // pre-persistence behavior of FileTree.vue, where nested dirs start
8
8
  // collapsed so opening Files mode doesn't render the whole tree.
9
- const DEFAULT_EXPANDED: ReadonlyArray<string> = [""];
9
+ const DEFAULT_EXPANDED: readonly string[] = [""];
10
10
 
11
11
  export function parseStoredExpandedDirs(raw: string | null): Set<string> {
12
12
  if (raw === null) return new Set(DEFAULT_EXPANDED);
@@ -3,10 +3,21 @@
3
3
  // Extracted from FilesView.vue (#507 step 8).
4
4
 
5
5
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
6
- import type { StatusColumn, TodoData, TodoItem } from "../../plugins/todo/index";
6
+ import type { StatusColumn, TodoData, TodoItem } from "@mulmoclaude/todo-plugin/shared";
7
7
  import { WORKSPACE_FILES } from "../../config/workspacePaths";
8
8
  import { isRecord } from "../types";
9
9
 
10
+ // `WORKSPACE_FILES.todosItems` lives under
11
+ // `data/plugins/%40mulmoclaude%2Ftodo-plugin/todos.json` — the
12
+ // directory name is the URL-encoded npm package (#1145). When the
13
+ // path round-trips through Vue Router (deep-link via the URL bar),
14
+ // the encoded segment gets decoded once to `@mulmoclaude/todo-plugin`,
15
+ // so a strict equality check against the literal constant misses
16
+ // that case. Compare against both encoded and decoded forms so the
17
+ // comparison works for both router-decoded deep links and
18
+ // tree-click flows that preserve the encoded literal.
19
+ const TODOS_ITEMS_PATHS: ReadonlySet<string> = new Set([WORKSPACE_FILES.todosItems, decodeURIComponent(WORKSPACE_FILES.todosItems)]);
20
+
10
21
  function isTodoItem(value: unknown): value is TodoItem {
11
22
  if (!isRecord(value)) return false;
12
23
  if (typeof value["id"] !== "string" || typeof value["text"] !== "string") return false;
@@ -20,7 +31,7 @@ function isTodoItemArray(value: unknown): value is TodoItem[] {
20
31
  }
21
32
 
22
33
  export function toTodoExplorerResult(selectedPath: string | null, rawText: string | null): ToolResultComplete<TodoData> | null {
23
- if (selectedPath !== WORKSPACE_FILES.todosItems) return null;
34
+ if (selectedPath === null || !TODOS_ITEMS_PATHS.has(selectedPath)) return null;
24
35
  if (rawText === null) return null;
25
36
  let parsed: unknown;
26
37
  try {
@@ -5,9 +5,7 @@
5
5
  /** "Apr 11 06:32" — short month + day + 24h time. */
6
6
  export function formatDate(iso: string): string {
7
7
  const date = new Date(iso);
8
- return (
9
- date.toLocaleDateString(undefined, { month: "short", day: "numeric" }) + " " + date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
10
- );
8
+ return `${date.toLocaleDateString(undefined, { month: "short", day: "numeric" })} ${date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}`;
11
9
  }
12
10
 
13
11
  /** "Apr 11 06:32" — same format as formatDate but from epoch ms. */
@@ -25,6 +25,11 @@ export const JSON_TOKEN_CLASS: Record<JsonTokenType, string> = {
25
25
  // sonarjs/regex-complexity and is easier to reason about.
26
26
  const STRING_RE = /^"(?:[^"\\]|\\.)*"/;
27
27
  const KEYWORD_RE = /^(?:true|false|null)\b/;
28
+ // Bounded JSON number parser — each `\d+` runs over a digits-only
29
+ // class with hard delimiters (`.`, `e`, `E`, `+`, `-`) between
30
+ // segments. Linear in input length; safe-regex flags the optional
31
+ // groups generically.
32
+ // eslint-disable-next-line security/detect-unsafe-regex -- bounded JSON number parser, no nested-quantifier overlap
28
33
  const NUMBER_RE = /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/;
29
34
  const WS_RE = /^\s+/;
30
35
  const PUNCT_RE = /^[{}[\]:,]/;
@@ -0,0 +1,62 @@
1
+ // Tiny inline script injected into every `/artifacts/html/...` document
2
+ // so the parent (StackView in chat) can size each iframe to its
3
+ // rendered content height — despite the sandbox treating the iframe as
4
+ // cross-origin (#1219 follow-up).
5
+ //
6
+ // Why this matters: presentHtml's iframe runs with
7
+ // `sandbox="allow-scripts"` (deliberate — `allow-same-origin` would let
8
+ // LLM-generated HTML read the parent's cookies / localStorage / bearer
9
+ // token). Without `allow-same-origin`, the parent's
10
+ // `iframe.contentDocument.documentElement.scrollHeight` access throws
11
+ // cross-origin, leaving the iframe at the browser-default 150px and
12
+ // clipping the actual chart / Sankey / report inside.
13
+ //
14
+ // The reporter runs INSIDE the iframe (under its null origin), reads
15
+ // only its own document height, and posts a message to its parent. The
16
+ // parent then sets `iframe.style.height` from the reported value. No
17
+ // parent state is exposed to the iframe.
18
+ //
19
+ // Posts on initial `load`, on every subsequent body resize (charts that
20
+ // populate after DOMContentLoaded, web-font settling, etc.), and once
21
+ // more on `DOMContentLoaded` so the first paint is also sized.
22
+ //
23
+ // On iframe WIDTH changes:
24
+ //
25
+ // 1. Fires a synthetic `window.resize` event so libraries hooked into
26
+ // that signal (Chart.js, ECharts, custom code) get the nudge.
27
+ // 2. Calls `Plotly.Plots.resize` on every `.js-plotly-plot` /
28
+ // `.plotly-graph-div` element if Plotly is loaded. Plotly v2's
29
+ // `responsive: true` is supposed to catch this via its own
30
+ // ResizeObserver, but in the sandboxed iframe context the observer
31
+ // sometimes misses the change. Calling resize directly is the
32
+ // documented escape hatch and a no-op when the chart was already
33
+ // correctly sized. Gated on `window.Plotly` so HTML pages without
34
+ // Plotly are unaffected.
35
+ //
36
+ // Pairs with the listener in `src/components/StackView.vue`. Message
37
+ // shape: `{ type: "mc-iframe-height", height: <pixels> }`. The listener
38
+ // matches the iframe via `event.source === iframe.contentWindow`, then
39
+ // `iframe.style.setProperty("height", "<n>px", "important")` (the
40
+ // `!important` defeats the stack-natural `:deep(.h-full)` override).
41
+
42
+ const REPORTER_SCRIPT = `(()=>{const p=()=>{try{parent.postMessage({type:"mc-iframe-height",height:document.documentElement.scrollHeight},"*")}catch(e){}};const r=()=>{try{window.dispatchEvent(new Event("resize"))}catch(e){}try{const P=window.Plotly;if(P&&P.Plots&&P.Plots.resize){document.querySelectorAll(".js-plotly-plot,.plotly-graph-div").forEach(el=>{try{P.Plots.resize(el)}catch(e){}})}}catch(e){}};addEventListener("DOMContentLoaded",p);addEventListener("load",p);if(window.ResizeObserver&&document.documentElement){let w=0;new ResizeObserver((es)=>{p();const nw=es[0]&&es[0].contentRect?es[0].contentRect.width:0;if(nw&&nw!==w){w=nw;r()}}).observe(document.documentElement)}})();`;
43
+
44
+ export const HEIGHT_REPORTER_SCRIPT_TAG = `<script>${REPORTER_SCRIPT}</script>`;
45
+
46
+ const BODY_CLOSE_RE = /<\/body\s*>/gi;
47
+
48
+ /** Splice the height-reporter `<script>` tag immediately before the
49
+ * document's last `</body>`. Pure string operation — no DOM parsing,
50
+ * linear time in input length, idempotent in effect (the script is
51
+ * one-shot; duplicates produce duplicate postMessages, the parent
52
+ * listener handles them identically). When `</body>` is missing
53
+ * (server-streamed HTML, partial output, hand-written fragment), the
54
+ * tag is appended at the end so the script still loads. */
55
+ export function injectHeightReporterScript(html: string): string {
56
+ if (!html) return html;
57
+ const matches = [...html.matchAll(BODY_CLOSE_RE)];
58
+ if (matches.length === 0) return html + HEIGHT_REPORTER_SCRIPT_TAG;
59
+ const idx = matches[matches.length - 1].index;
60
+ if (idx === undefined) return html + HEIGHT_REPORTER_SCRIPT_TAG;
61
+ return `${html.slice(0, idx)}${HEIGHT_REPORTER_SCRIPT_TAG}${html.slice(idx)}`;
62
+ }
@@ -14,14 +14,31 @@ export const HTML_PREVIEW_CSP_ALLOWED_CDNS: readonly string[] = [
14
14
  "https://cdnjs.cloudflare.com",
15
15
  "https://fonts.googleapis.com",
16
16
  "https://fonts.gstatic.com",
17
+ // Plotly's official CDN. The LLM defaults to this URL when it
18
+ // includes a Sankey or other Plotly chart in presentHtml output —
19
+ // Plotly's docs recommend it, so unconditioned LLM output ends up
20
+ // pointing here. Also reachable through jsdelivr, but adding the
21
+ // first-party CDN keeps historical artifacts (where the URL is
22
+ // already baked into the file on disk) rendering correctly.
23
+ "https://cdn.plot.ly",
17
24
  ];
18
25
 
19
26
  /**
20
27
  * Build the CSP string. Split from the wrapper so tests can exercise
21
28
  * the policy without HTML-template noise.
29
+ *
30
+ * `origin`, when provided, replaces `'self'` in `img-src`. The preview
31
+ * iframe is `sandbox="allow-scripts"` only, so its document has an
32
+ * opaque origin: Safari/WebKit matches `'self'` against the (opaque)
33
+ * origin tuple and rejects every same-origin image request. Chrome
34
+ * matches `'self'` against the document URL and works either way. Pass
35
+ * the explicit server origin from HTTP-header callers; leave it
36
+ * undefined for the `srcdoc` fallback (where `'self'` is meaningless
37
+ * either way and there are no same-origin refs to resolve).
22
38
  */
23
- export function buildHtmlPreviewCsp(cdns: readonly string[] = HTML_PREVIEW_CSP_ALLOWED_CDNS): string {
39
+ export function buildHtmlPreviewCsp(origin?: string, cdns: readonly string[] = HTML_PREVIEW_CSP_ALLOWED_CDNS): string {
24
40
  const cdnList = cdns.join(" ");
41
+ const imgSelf = origin ?? "'self'";
25
42
  return [
26
43
  "default-src 'none'",
27
44
  // LLM-authored HTML almost always uses inline <script> blocks
@@ -37,13 +54,23 @@ export function buildHtmlPreviewCsp(cdns: readonly string[] = HTML_PREVIEW_CSP_A
37
54
  // could exfiltrate data via image requests even with connect-src
38
55
  // blocked. Widen via HTML_PREVIEW_CSP_ALLOWED_CDNS if LLM output
39
56
  // legitimately needs more hosts.
40
- `img-src 'self' ${cdnList} data: blob:`,
57
+ `img-src ${imgSelf} ${cdnList} data: blob:`,
41
58
  // Block XHR / fetch / WebSocket so previews can't phone home or
42
59
  // exfiltrate anything the inline scripts happen to compute.
43
60
  "connect-src 'none'",
44
61
  ].join("; ");
45
62
  }
46
63
 
64
+ /**
65
+ * Build the CSP string for the print-mode hidden iframe (presentHtml's
66
+ * printToPdf). Same policy as the preview header with the explicit
67
+ * server origin substituted for `'self'` — see `buildHtmlPreviewCsp`
68
+ * for why the substitution is required.
69
+ */
70
+ export function buildPrintCspContent(origin: string, cdns: readonly string[] = HTML_PREVIEW_CSP_ALLOWED_CDNS): string {
71
+ return buildHtmlPreviewCsp(origin, cdns);
72
+ }
73
+
47
74
  const CSP_META_NONCE = ""; // reserved for future use (per-render nonce)
48
75
 
49
76
  /**
@@ -0,0 +1,122 @@
1
+ // Shared HTML-tag URL rewriter — used by:
2
+ // - browser markdown surface (`rewriteImgSrcAttrsInHtml` in
3
+ // `rewriteMarkdownImageRefs.ts`) → rewrites to
4
+ // `/api/files/raw?path=...`
5
+ // - server PDF surface (`inlineImages` in
6
+ // `server/api/routes/pdf.ts`) → rewrites to `data:` URIs
7
+ //
8
+ // Both used to keep their own copy of the same regex shape with a
9
+ // `// Mirrors the shape used by …` comment. That mirroring drifts the
10
+ // moment one side adds a tag (`<source>`, `<video poster>`) and the
11
+ // other doesn't. Single helper here, two callers, one tag list — the
12
+ // drift becomes structurally impossible (#1011 Stage B).
13
+ //
14
+ // `srcset` (comma-separated descriptor list) and SVG `<image href>` /
15
+ // CSS `url()` are deliberately out of scope — see the deferred-list
16
+ // comment on `RESOLVABLE_TAG_ATTRS` below.
17
+
18
+ // Tag (lowercased) → URL-bearing attribute(s). Adding a row here
19
+ // extends both Markdown and PDF surfaces simultaneously.
20
+ //
21
+ // Deferred (NOT here):
22
+ // - `srcset` on `<img>` / `<source>` — comma-separated list with
23
+ // descriptors (`url 1x, url2 2x`), needs a separate split/rewrite
24
+ // pass. Tracked under #1011 Stage B follow-up.
25
+ // - SVG `<image href>` — gap table item #9, low priority per plan
26
+ // §修正提案 P3-A.
27
+ // - CSS `url()` in `style=` attributes — gap table item #8, same
28
+ // priority.
29
+ export const RESOLVABLE_TAG_ATTRS: Readonly<Record<string, readonly string[]>> = {
30
+ img: ["src"],
31
+ source: ["src"],
32
+ video: ["poster", "src"],
33
+ audio: ["src"],
34
+ };
35
+
36
+ // Outer regex: scan any tag whose name appears in `RESOLVABLE_TAG_ATTRS`,
37
+ // respecting quoted attribute values so `>` inside e.g. `alt="x>y"`
38
+ // doesn't terminate the tag early. The body is one of:
39
+ // - any non-`>` non-quote char `[^>"']`
40
+ // - a complete double-quoted span `"[^"]*"`
41
+ // - a complete single-quoted span `'[^']*'`
42
+ // All branches bounded — no nested quantifiers, no overlap.
43
+ //
44
+ // The tag-name alternation is hand-listed rather than computed from
45
+ // `Object.keys(RESOLVABLE_TAG_ATTRS)` so the regex is a const string
46
+ // (lint-friendly) and the alternation order matches the readable
47
+ // declaration order. Adding a tag means: update the map AND the
48
+ // alternation here. The unit test in test_htmlSrcAttrs.ts pins this
49
+ // in lockstep so the two never disagree silently.
50
+ //
51
+ // eslint-disable-next-line security/detect-unsafe-regex -- bounded alternatives, ReDoS-safe (test in test_htmlSrcAttrs.ts)
52
+ const RESOLVABLE_TAG_OUTER_RE = /<(?:img|source|video|audio)\b(?:[^>"']|"[^"]*"|'[^']*')*\/?>/gi;
53
+ // Tag-name extractor for the matched outer tag. Anchored so we only
54
+ // read the leading `<name`, never an attribute value that happens to
55
+ // look like a tag.
56
+ const TAG_NAME_RE = /^<([a-z]+)/i;
57
+
58
+ // Attribute iterator: walks each `name=value` pair inside a tag. The
59
+ // leading `\s+` ensures we only match real attribute boundaries, not
60
+ // `src=` text embedded inside another attribute's quoted value.
61
+ // Capture groups:
62
+ // 1: leading whitespace
63
+ // 2: attribute name
64
+ // 3: `=` with surrounding spaces (only when value present)
65
+ // 4: full quoted/unquoted value (unused but captured for clarity)
66
+ // 5: double-quoted value (without quotes)
67
+ // 6: single-quoted value (without quotes)
68
+ // 7: unquoted value — refuses leading `"` / `'` so a malformed
69
+ // `<img src="aaaa` (no closing quote) doesn't capture the stray
70
+ // quote as the value
71
+ //
72
+ // All quantifiers bounded — verified ReDoS-safe in test_htmlSrcAttrs.ts.
73
+ // eslint-disable-next-line sonarjs/slow-regex, sonarjs/regex-complexity, security/detect-unsafe-regex -- bounded quantifiers, ReDoS-safe (test in test_htmlSrcAttrs.ts)
74
+ const ATTR_ITER_RE = /(\s+)([A-Za-z][\w:-]*)(?:(\s*=\s*)("([^"]*)"|'([^']*)'|([^\s>"'][^\s>]*)))?/g;
75
+
76
+ /** Transform every URL-bearing attribute on a recognised tag.
77
+ *
78
+ * `transform` is invoked once per matching attribute value. Return:
79
+ * - `string` to substitute the value (callee is responsible for
80
+ * not breaking out of the surrounding quotes — most callers
81
+ * route through `encodeURIComponent` or a fixed-prefix path)
82
+ * - `null` to leave the attribute untouched (e.g. external URL,
83
+ * `data:` URI, escape-the-workspace path)
84
+ *
85
+ * Other attributes (alt, class, style, …) and `src=`-shaped text
86
+ * inside their quoted values are preserved verbatim because we
87
+ * parse attribute-by-attribute, not by free-form regex.
88
+ *
89
+ * Recognised tags + attributes live in `RESOLVABLE_TAG_ATTRS`. Any
90
+ * tag whose name isn't in the map is returned untouched. Any
91
+ * attribute on a recognised tag whose name isn't in the map's entry
92
+ * is also untouched. */
93
+ export function transformResolvableUrlsInHtml(html: string, transform: (url: string) => string | null): string {
94
+ if (!html) return html;
95
+ return html.replace(RESOLVABLE_TAG_OUTER_RE, (tag) => {
96
+ const tagNameMatch = TAG_NAME_RE.exec(tag);
97
+ if (!tagNameMatch) return tag;
98
+ const resolvableAttrs = RESOLVABLE_TAG_ATTRS[tagNameMatch[1].toLowerCase()];
99
+ if (!resolvableAttrs) return tag;
100
+ return tag.replace(ATTR_ITER_RE, (...captures: unknown[]) => replaceAttrIfResolvable(captures, resolvableAttrs, transform));
101
+ });
102
+ }
103
+
104
+ function replaceAttrIfResolvable(captures: unknown[], resolvableAttrs: readonly string[], transform: (url: string) => string | null): string {
105
+ const [full, leading, name, eqWithSpaces, , doubleQuoted, singleQuoted, bare] = captures as [
106
+ string,
107
+ string,
108
+ string,
109
+ string | undefined,
110
+ string | undefined,
111
+ string | undefined,
112
+ string | undefined,
113
+ string | undefined,
114
+ ];
115
+ if (!eqWithSpaces || !resolvableAttrs.includes(name.toLowerCase())) return full;
116
+ const value = (doubleQuoted ?? singleQuoted ?? bare ?? "").trim();
117
+ if (!value) return full;
118
+ const replacement = transform(value);
119
+ if (replacement === null) return full;
120
+ const quote = doubleQuoted !== undefined ? '"' : singleQuoted !== undefined ? "'" : '"';
121
+ return `${leading}${name}${eqWithSpaces}${quote}${replacement}${quote}`;
122
+ }