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,112 @@
1
+ // In-process registry of runtime-loaded plugins (#1043 C-2).
2
+ //
3
+ // `loadRuntimePlugins()` produces a list of `RuntimePlugin`s; this
4
+ // module owns the singleton view of "what's registered right now" and
5
+ // wires the collision policy:
6
+ //
7
+ // 1. Static plugins (`PLUGIN_DEFS` from `plugin-names.ts`) win every
8
+ // collision — built-ins cannot be shadowed.
9
+ // 2. Among runtime plugins, first-loaded wins on duplicate tool name.
10
+ //
11
+ // Both processes (parent server, spawned MCP child) hold their own
12
+ // in-memory copy. They read the same ledger / cache so the contents
13
+ // match without IPC.
14
+
15
+ import type { ToolDefinition } from "gui-chat-protocol";
16
+ import type { RuntimePlugin } from "./runtime-loader.js";
17
+ import { log } from "../system/logger/index.js";
18
+
19
+ const LOG_PREFIX = "plugins/registry";
20
+
21
+ let registry: RuntimePlugin[] = [];
22
+ let toolNameIndex = new Map<string, RuntimePlugin>();
23
+ let oauthAliasIndex = new Map<string, RuntimePlugin>();
24
+
25
+ export interface RegisterResult {
26
+ /** Plugins that landed in the registry. */
27
+ registered: RuntimePlugin[];
28
+ /** Plugins skipped because their tool name collides with a static
29
+ * built-in or an earlier-loaded runtime plugin. */
30
+ collisions: { plugin: RuntimePlugin; reason: "static" | "runtime"; existingTool: string }[];
31
+ /** OAuth callback aliases dropped because two plugins claim the
32
+ * same one. The first-registered plugin keeps the alias; later
33
+ * collisions are logged and skipped (the plugin itself still
34
+ * loads — only its OAuth route is unavailable). */
35
+ oauthAliasCollisions: { plugin: RuntimePlugin; alias: string; existingPlugin: string }[];
36
+ }
37
+
38
+ /** Replace the registry with the given runtime plugins, applying the
39
+ * collision policy described at the top of the file. Static names
40
+ * are passed in by the caller (parent server / MCP child) because
41
+ * the static set differs slightly between contexts (mcp tools vs.
42
+ * plugin-names PLUGIN_DEFS). */
43
+ export function registerRuntimePlugins(staticToolNames: ReadonlySet<string>, plugins: readonly RuntimePlugin[]): RegisterResult {
44
+ const registered: RuntimePlugin[] = [];
45
+ const collisions: RegisterResult["collisions"] = [];
46
+ const oauthAliasCollisions: RegisterResult["oauthAliasCollisions"] = [];
47
+ const seen = new Map<string, RuntimePlugin>();
48
+ const oauthAliases = new Map<string, RuntimePlugin>();
49
+ for (const plugin of plugins) {
50
+ const toolName = plugin.definition.name;
51
+ if (staticToolNames.has(toolName)) {
52
+ collisions.push({ plugin, reason: "static", existingTool: toolName });
53
+ log.warn(LOG_PREFIX, "skipping runtime plugin — name collides with static tool", {
54
+ plugin: plugin.name,
55
+ tool: toolName,
56
+ });
57
+ continue;
58
+ }
59
+ if (seen.has(toolName)) {
60
+ collisions.push({ plugin, reason: "runtime", existingTool: toolName });
61
+ const existing = seen.get(toolName);
62
+ log.warn(LOG_PREFIX, "skipping runtime plugin — name collides with already-loaded runtime plugin", {
63
+ plugin: plugin.name,
64
+ tool: toolName,
65
+ existingPlugin: existing?.name,
66
+ });
67
+ continue;
68
+ }
69
+ seen.set(toolName, plugin);
70
+ registered.push(plugin);
71
+ if (plugin.oauthCallbackAlias !== null) {
72
+ const existingAliasOwner = oauthAliases.get(plugin.oauthCallbackAlias);
73
+ if (existingAliasOwner) {
74
+ oauthAliasCollisions.push({ plugin, alias: plugin.oauthCallbackAlias, existingPlugin: existingAliasOwner.name });
75
+ log.warn(LOG_PREFIX, "skipping OAuth callback alias — already claimed by another runtime plugin", {
76
+ plugin: plugin.name,
77
+ alias: plugin.oauthCallbackAlias,
78
+ existingPlugin: existingAliasOwner.name,
79
+ });
80
+ continue;
81
+ }
82
+ oauthAliases.set(plugin.oauthCallbackAlias, plugin);
83
+ }
84
+ }
85
+ registry = registered;
86
+ toolNameIndex = seen;
87
+ oauthAliasIndex = oauthAliases;
88
+ return { registered, collisions, oauthAliasCollisions };
89
+ }
90
+
91
+ export function getRuntimePlugins(): readonly RuntimePlugin[] {
92
+ return registry;
93
+ }
94
+
95
+ export function getRuntimePluginByToolName(toolName: string): RuntimePlugin | null {
96
+ return toolNameIndex.get(toolName) ?? null;
97
+ }
98
+
99
+ export function getRuntimePluginByOauthAlias(alias: string): RuntimePlugin | null {
100
+ return oauthAliasIndex.get(alias) ?? null;
101
+ }
102
+
103
+ export function getRuntimeToolDefinitions(): readonly ToolDefinition[] {
104
+ return registry.map((entry) => entry.definition);
105
+ }
106
+
107
+ /** Test-only reset. */
108
+ export function _resetRuntimeRegistryForTest(): void {
109
+ registry = [];
110
+ toolNameIndex = new Map();
111
+ oauthAliasIndex = new Map();
112
+ }
@@ -0,0 +1,50 @@
1
+ // Plugin-facing task scheduler API. The host attaches a per-plugin
2
+ // instance of `TasksRuntimeApi` to the `PluginRuntime` it constructs
3
+ // for each plugin (`server/plugins/runtime.ts`). One periodic tick
4
+ // per plugin — the registry id is `plugin:<pkg>`, derived from the
5
+ // caller's pkg name; the plugin doesn't supply one.
6
+ //
7
+ // Plugin authors access this surface via the `MulmoclaudeRuntime`
8
+ // cast, identical to the notifier extension:
9
+ //
10
+ // import type { PluginRuntime } from "gui-chat-protocol";
11
+ // import type { MulmoclaudeRuntime } from "<mulmoclaude>/notifier/runtime-api";
12
+ // export default definePlugin((runtime: PluginRuntime) => {
13
+ // const { tasks } = runtime as MulmoclaudeRuntime;
14
+ // tasks.register({ schedule: { ... }, run: async () => { ... } });
15
+ // });
16
+ //
17
+ // Once the API stabilises (Phase 3 of the Encore plan), this is a
18
+ // candidate for upstreaming into gui-chat-protocol so the cast goes
19
+ // away.
20
+
21
+ /** Same shape as the host task manager's `TaskSchedule`
22
+ * (`server/events/task-manager/index.ts`). Forwarded verbatim to the
23
+ * task manager — no richer types layered at the plugin runtime
24
+ * level. Plugins build "remind 3 weeks before" / "weekly on
25
+ * Tuesdays" logic inside `run()` against their own files. */
26
+ export type PluginTaskSchedule = { type: "interval"; intervalMs: number } | { type: "daily"; time: string }; // "HH:MM" UTC
27
+
28
+ export interface PluginTaskRegistration {
29
+ /** When to fire. Forwarded verbatim to the host task manager. */
30
+ schedule: PluginTaskSchedule;
31
+ /** Tick handler. Errors are caught and logged by the host task
32
+ * manager (`task-manager/index.ts:89`); they do not propagate. */
33
+ run: () => Promise<void>;
34
+ }
35
+
36
+ export interface TasksRuntimeApi {
37
+ /** Register the plugin's single periodic tick.
38
+ *
39
+ * - Registry id is `plugin:<pkg>`; the plugin does not supply one.
40
+ * - Cap-at-1: the second call from the same plugin throws with a
41
+ * friendly message ("Plugin <pkg> already registered a task —
42
+ * only one tick per plugin is allowed").
43
+ * - No `unregister()`. Plugins load once at boot
44
+ * (`server/index.ts:723-`); server-side hot-reload is not
45
+ * supported, so a clean lifecycle hook isn't needed.
46
+ *
47
+ * Throws synchronously on duplicate registration so plugin authors
48
+ * see the failure during setup, not at first tick. */
49
+ register: (task: PluginTaskRegistration) => void;
50
+ }
@@ -0,0 +1,378 @@
1
+ // Plugin runtime construction — the per-plugin scoped object
2
+ // `definePlugin(setup)` factories receive at load time (#1110).
3
+ //
4
+ // Every helper closes over `pkgName` so a plugin's pubsub channel,
5
+ // data dir, log prefix etc. cannot leak across plugins. Path arguments
6
+ // to `files.{data,config}` are normalised to POSIX, then anchored
7
+ // inside the plugin's scope root via `ensureInsideBase` — so misuse of
8
+ // `node:path` on Windows still works and `"../../etc/passwd"` is
9
+ // rejected.
10
+ //
11
+ // This module is server-side only. The browser-side counterpart lives
12
+ // at `src/utils/plugin/runtime.ts`.
13
+
14
+ import path from "node:path";
15
+ import { readFile, readdir, stat as fsStat, unlink as fsUnlink } from "node:fs/promises";
16
+ import { randomUUID } from "node:crypto";
17
+ import type { FileOps, PluginRuntime } from "gui-chat-protocol";
18
+
19
+ import { WORKSPACE_PATHS } from "../workspace/paths.js";
20
+ import { writeFileAtomic } from "../utils/files/atomic.js";
21
+ import { log as hostLog, type Logger } from "../system/logger/index.js";
22
+ import { ensureInsideBase } from "./runtime-loader.js";
23
+ import { ONE_SECOND_MS } from "../utils/time.js";
24
+ import type { IPubSub } from "../events/pub-sub/index.js";
25
+ import * as notifierEngine from "../notifier/engine.js";
26
+ import type { MulmoclaudeRuntime, NotifierRuntimeApi } from "../notifier/runtime-api.js";
27
+ import type { ITaskManager } from "../events/task-manager/index.js";
28
+ import type { TasksRuntimeApi } from "./runtime-tasks-api.js";
29
+ import type { ChatRuntimeApi } from "./runtime-chat-api.js";
30
+ import { startChat } from "../api/routes/agent.js";
31
+ import { PLUGIN_SESSION_ORIGIN_PREFIX } from "../../src/types/session.js";
32
+
33
+ const DEFAULT_FETCH_TIMEOUT_MS = 10 * ONE_SECOND_MS;
34
+
35
+ // ─────────────────────────────────────────────────────────────────────
36
+ // Path normalisation contract (see plans/done/feat-plugin-runtime-extensions-1110.md)
37
+ //
38
+ // 1. Replace `\` with `/` (Windows path.join leak repair).
39
+ // 2. `path.posix.normalize` (folds `..`, `.`, `//`).
40
+ // 3. Reject if the normalised form starts with `..` or `/`
41
+ // (would escape the scope root or be absolute).
42
+ // 4. `path.join(scopeRoot, ...segments)` — platform-aware join so
43
+ // the returned absolute path uses the host OS separator
44
+ // (backslashes on Windows, forward slashes on POSIX). Critical
45
+ // for `ensureInsideBase` to accept the result.
46
+ //
47
+ // Plugin authors should never need `node:path`; the platform meets
48
+ // them where they are even if they mis-import it on Windows.
49
+ // ─────────────────────────────────────────────────────────────────────
50
+
51
+ /** Resolve a plugin-supplied path to an absolute path inside the
52
+ * plugin's scope root. Throws on traversal. Exported for tests.
53
+ *
54
+ * Cross-platform: `scopeRoot` is the host OS's native form (drive
55
+ * letter + backslashes on Windows, forward slashes on POSIX). The
56
+ * user-supplied `rel` is POSIX-relative by contract; we sanitise
57
+ * for the lexical traversal check via `path.posix.*`, then join
58
+ * with `path.join` so the returned absolute path is in the host's
59
+ * native form (which `ensureInsideBase` requires). */
60
+ export function normalizePluginPath(scopeRoot: string, rel: string): string {
61
+ // Defang Windows path.join output and any mixed-separator inputs.
62
+ const slashed = rel.replace(/\\/g, "/");
63
+ // Fold `..` / `.` / repeated `/` lexically (no fs touch yet).
64
+ const normalised = path.posix.normalize(slashed);
65
+ // Reject lexically — anything that escapes the scope root after
66
+ // normalisation either starts with `..` (parent-traversal) or is
67
+ // absolute (`/etc/passwd`). `path.posix.normalize` produces `..`
68
+ // alone if the input is `..` itself, hence the equality branch.
69
+ if (normalised === ".." || normalised.startsWith("../") || normalised.startsWith("/")) {
70
+ throw new Error(`path escapes plugin scope: ${rel}`);
71
+ }
72
+ const segments = normalised === "." ? [] : normalised.split("/");
73
+ // `path.join` (platform-aware) so the result uses the host
74
+ // separator. On Windows that's backslashes; on POSIX, forward
75
+ // slashes — matching `scopeRoot`'s existing form so
76
+ // `ensureInsideBase`'s `path.resolve` + `path.sep` comparison
77
+ // works across both platforms.
78
+ const absolute = path.join(scopeRoot, ...segments);
79
+ // Defence in depth: even though the lexical check above should
80
+ // catch every escape, run `ensureInsideBase` so a future change to
81
+ // either step doesn't silently regress the safety guarantee.
82
+ if (!ensureInsideBase(absolute, scopeRoot)) {
83
+ throw new Error(`path escapes plugin scope: ${rel}`);
84
+ }
85
+ return absolute;
86
+ }
87
+
88
+ // ─────────────────────────────────────────────────────────────────────
89
+ // Scoped FileOps factory
90
+ // ─────────────────────────────────────────────────────────────────────
91
+
92
+ function isErrnoException(value: unknown): value is { code: string } {
93
+ return typeof value === "object" && value !== null && "code" in value && typeof (value as { code: unknown }).code === "string";
94
+ }
95
+
96
+ function makeFileOps(scopeRoot: string): FileOps {
97
+ // The scope root may not exist on first use. Lazy-create at write
98
+ // time (writeFileAtomic does its own mkdir for the file's parent;
99
+ // we ensure the scope root exists for readDir() / stat() etc.).
100
+ // Reads against a never-written plugin throw ENOENT — exposed as
101
+ // `exists() === false` rather than handled here.
102
+
103
+ return {
104
+ async read(rel) {
105
+ return readFile(normalizePluginPath(scopeRoot, rel), "utf-8");
106
+ },
107
+ async readBytes(rel) {
108
+ const buf = await readFile(normalizePluginPath(scopeRoot, rel));
109
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
110
+ },
111
+ async write(rel, content) {
112
+ const abs = normalizePluginPath(scopeRoot, rel);
113
+ await writeFileAtomic(abs, content);
114
+ },
115
+ async readDir(rel) {
116
+ const abs = normalizePluginPath(scopeRoot, rel);
117
+ try {
118
+ return await readdir(abs);
119
+ } catch (err) {
120
+ if (isErrnoException(err) && err.code === "ENOENT") return [];
121
+ throw err;
122
+ }
123
+ },
124
+ async stat(rel) {
125
+ const { mtimeMs, size } = await fsStat(normalizePluginPath(scopeRoot, rel));
126
+ return { mtimeMs, size };
127
+ },
128
+ async exists(rel) {
129
+ try {
130
+ await fsStat(normalizePluginPath(scopeRoot, rel));
131
+ return true;
132
+ } catch (err) {
133
+ if (isErrnoException(err) && err.code === "ENOENT") return false;
134
+ throw err;
135
+ }
136
+ },
137
+ async unlink(rel) {
138
+ try {
139
+ await fsUnlink(normalizePluginPath(scopeRoot, rel));
140
+ } catch (err) {
141
+ if (isErrnoException(err) && err.code === "ENOENT") return;
142
+ throw err;
143
+ }
144
+ },
145
+ };
146
+ }
147
+
148
+ // ─────────────────────────────────────────────────────────────────────
149
+ // Sanitisation: pkg name → on-disk directory segment
150
+ // ─────────────────────────────────────────────────────────────────────
151
+
152
+ /** Convert an npm package name (`@org/foo`) to a single safe directory
153
+ * segment. URL-encodes `/` in scoped names so the scope root stays
154
+ * one level deep — keeps `readdir` predictable and avoids accidental
155
+ * traversal via the package name itself.
156
+ *
157
+ * Exported for tests. */
158
+ export function sanitisePackageNameForFs(pkgName: string): string {
159
+ return encodeURIComponent(pkgName);
160
+ }
161
+
162
+ // ─────────────────────────────────────────────────────────────────────
163
+ // Scoped logger
164
+ // ─────────────────────────────────────────────────────────────────────
165
+
166
+ function makeScopedLogger(pkgName: string): PluginRuntime["log"] {
167
+ const prefix = `plugin/${pkgName}`;
168
+ return {
169
+ debug: (msg, data) => hostLog.debug(prefix, msg, data as Record<string, unknown> | undefined),
170
+ info: (msg, data) => hostLog.info(prefix, msg, data as Record<string, unknown> | undefined),
171
+ warn: (msg, data) => hostLog.warn(prefix, msg, data as Record<string, unknown> | undefined),
172
+ error: (msg, data) => hostLog.error(prefix, msg, data as Record<string, unknown> | undefined),
173
+ };
174
+ }
175
+
176
+ // ─────────────────────────────────────────────────────────────────────
177
+ // Scoped fetch (timeout + optional host allowlist)
178
+ // ─────────────────────────────────────────────────────────────────────
179
+
180
+ function makeScopedFetch(pkgName: string): PluginRuntime["fetch"] {
181
+ return async (url, opts = {}) => {
182
+ if (opts.allowedHosts && opts.allowedHosts.length > 0) {
183
+ const { hostname } = new URL(url);
184
+ if (!opts.allowedHosts.includes(hostname)) {
185
+ throw new Error(`plugin/${pkgName}: host ${hostname} not in allowedHosts`);
186
+ }
187
+ }
188
+ const controller = new AbortController();
189
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
190
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
191
+ try {
192
+ // Forward both the caller's signal (if any) and our timeout
193
+ // signal. AbortSignal.any returns a signal that fires when
194
+ // either input fires — Node 20+.
195
+ const signal = opts.signal ? AbortSignal.any([opts.signal, controller.signal]) : controller.signal;
196
+ // The runtime cast keeps PluginFetchInit narrow (string | Uint8Array)
197
+ // so plugin authors don't need to know the wider DOM BodyInit union.
198
+ const body = opts.body as Parameters<typeof fetch>[1] extends infer T ? (T extends { body?: infer B } ? B : never) : never;
199
+ return await fetch(url, { method: opts.method, headers: opts.headers, body, signal });
200
+ } catch (err) {
201
+ // Re-throw with plugin context so a fan-out failure in the
202
+ // host log immediately points at the responsible plugin
203
+ // instead of an anonymous network error (CodeRabbit review on
204
+ // PR #1124). Distinguishing AbortError → "timeout" is more
205
+ // useful than the bare "AbortError: This operation was aborted"
206
+ // message that surfaces from AbortController.
207
+ if (err instanceof Error && err.name === "AbortError") {
208
+ throw new Error(`plugin/${pkgName}: fetch timed out after ${timeoutMs}ms (${url})`);
209
+ }
210
+ const message = err instanceof Error ? err.message : String(err);
211
+ throw new Error(`plugin/${pkgName}: fetch failed (${url}): ${message}`);
212
+ } finally {
213
+ clearTimeout(timer);
214
+ }
215
+ };
216
+ }
217
+
218
+ function makeScopedFetchJson(pkgName: string, scopedFetch: PluginRuntime["fetch"]): PluginRuntime["fetchJson"] {
219
+ // When `opts.parse` is provided we trust its narrowing; when absent
220
+ // the caller asserts the JSON shape themselves (same contract as
221
+ // `JSON.parse(...) as T`).
222
+ return async function fetchJson<T>(url: string, opts: { parse?: (raw: unknown) => T } & Parameters<PluginRuntime["fetch"]>[1] = {}): Promise<T> {
223
+ const response = await scopedFetch(url, opts);
224
+ if (!response.ok) {
225
+ throw new Error(`plugin/${pkgName}: fetchJson HTTP ${response.status} for ${url}`);
226
+ }
227
+ const raw = (await response.json()) as unknown;
228
+ return opts.parse ? opts.parse(raw) : (raw as T);
229
+ };
230
+ }
231
+
232
+ // ─────────────────────────────────────────────────────────────────────
233
+ // Scoped pubsub publisher
234
+ // ─────────────────────────────────────────────────────────────────────
235
+
236
+ /** Build the channel name for a given plugin's event. Centralised so
237
+ * the server-side publish side and the browser-side subscribe side
238
+ * agree on the format (`plugin:<pkg>:<event>`). Exported for tests
239
+ * and for the browser runtime to import the same constant. */
240
+ export function pluginChannelName(pkgName: string, eventName: string): string {
241
+ return `plugin:${pkgName}:${eventName}`;
242
+ }
243
+
244
+ function makeScopedPubSub(pkgName: string, hostPubSub: IPubSub): PluginRuntime["pubsub"] {
245
+ return {
246
+ publish(eventName, payload) {
247
+ hostPubSub.publish(pluginChannelName(pkgName, eventName), payload);
248
+ },
249
+ };
250
+ }
251
+
252
+ // ─────────────────────────────────────────────────────────────────────
253
+ // Scoped notifier (host extension over gui-chat-protocol's PluginRuntime)
254
+ // ─────────────────────────────────────────────────────────────────────
255
+
256
+ function makeScopedNotifier(pkgName: string): NotifierRuntimeApi {
257
+ // Both `publish` and `clear` are plugin-scoped:
258
+ // - publish forces `pluginPkg` to the caller's pkg name (the
259
+ // plugin literally cannot publish under another's namespace).
260
+ // - clear routes through `clearForPlugin` so a plugin holding
261
+ // another plugin's id (e.g. via a future leak) silently no-ops
262
+ // instead of dismissing it. CodeRabbit review on PR #1198.
263
+ return {
264
+ publish: (input) => notifierEngine.publish({ ...input, pluginPkg: pkgName }),
265
+ clear: (entryId) => notifierEngine.clearForPlugin(pkgName, entryId),
266
+ };
267
+ }
268
+
269
+ // ─────────────────────────────────────────────────────────────────────
270
+ // Scoped tasks (host extension — Phase 1 of Encore plan)
271
+ // ─────────────────────────────────────────────────────────────────────
272
+
273
+ /** Build the registry id used for a plugin's tick. Centralised so the
274
+ * cap-at-1 closure check and the host task manager registration agree
275
+ * on the format. Exported for tests. */
276
+ export function pluginTaskId(pkgName: string): string {
277
+ return `plugin:${pkgName}`;
278
+ }
279
+
280
+ function makeScopedTasks(pkgName: string, taskManager: ITaskManager): TasksRuntimeApi {
281
+ // Cap-at-1 enforced at the runtime-API layer so the plugin author
282
+ // sees a friendly message before the host task manager's generic
283
+ // duplicate-id throw fires. The closure is per-plugin (each plugin
284
+ // gets its own runtime), so `registered` cannot leak across
285
+ // plugins.
286
+ let registered = false;
287
+ return {
288
+ register(task) {
289
+ if (registered) {
290
+ throw new Error(`plugin/${pkgName}: already registered a task — only one tick per plugin is allowed`);
291
+ }
292
+ registered = true;
293
+ taskManager.registerTask({
294
+ id: pluginTaskId(pkgName),
295
+ description: `tick for plugin ${pkgName}`,
296
+ schedule: task.schedule,
297
+ run: () => task.run(),
298
+ });
299
+ },
300
+ };
301
+ }
302
+
303
+ // ─────────────────────────────────────────────────────────────────────
304
+ // Scoped chat (host extension — Phase 1 of Encore plan)
305
+ // ─────────────────────────────────────────────────────────────────────
306
+
307
+ const DEFAULT_PLUGIN_CHAT_ROLE = "general";
308
+
309
+ function makeScopedChat(pkgName: string): ChatRuntimeApi {
310
+ return {
311
+ async start({ initialMessage, role }) {
312
+ const roleId = role ?? DEFAULT_PLUGIN_CHAT_ROLE;
313
+ const chatSessionId = randomUUID();
314
+ const result = await startChat({
315
+ message: initialMessage,
316
+ roleId,
317
+ chatSessionId,
318
+ origin: `${PLUGIN_SESSION_ORIGIN_PREFIX}${pkgName}`,
319
+ });
320
+ if (result.kind === "error") {
321
+ throw new Error(`plugin/${pkgName}: chat.start failed: ${result.error}`);
322
+ }
323
+ return { chatId: chatSessionId };
324
+ },
325
+ };
326
+ }
327
+
328
+ // ─────────────────────────────────────────────────────────────────────
329
+ // Public entry — wire everything into one PluginRuntime
330
+ // ─────────────────────────────────────────────────────────────────────
331
+
332
+ export interface MakePluginRuntimeDeps {
333
+ /** npm package name. Used both for sanitised fs segment + namespace prefix. */
334
+ pkgName: string;
335
+ /** Host pub/sub instance for fanning out scoped events. */
336
+ pubsub: IPubSub;
337
+ /** Locale tag the host has detected (e.g. `"ja"`). Snapshot only;
338
+ * reactive updates ride the frontend `BrowserPluginRuntime.locale`. */
339
+ locale: string;
340
+ /** Host task manager — backs `runtime.tasks.register()`. The same
341
+ * instance the host's own system tasks (journal, chat-index)
342
+ * register against. */
343
+ taskManager: ITaskManager;
344
+ }
345
+
346
+ export function makePluginRuntime(deps: MakePluginRuntimeDeps): MulmoclaudeRuntime {
347
+ const { pkgName, pubsub, locale, taskManager } = deps;
348
+ const seg = sanitisePackageNameForFs(pkgName);
349
+ const dataRoot = path.join(WORKSPACE_PATHS.pluginsData, seg);
350
+ const configRoot = path.join(WORKSPACE_PATHS.pluginsConfig, seg);
351
+ const scopedFetch = makeScopedFetch(pkgName);
352
+
353
+ return {
354
+ pubsub: makeScopedPubSub(pkgName, pubsub),
355
+ locale,
356
+ files: {
357
+ data: makeFileOps(dataRoot),
358
+ config: makeFileOps(configRoot),
359
+ },
360
+ log: makeScopedLogger(pkgName),
361
+ fetch: scopedFetch,
362
+ fetchJson: makeScopedFetchJson(pkgName, scopedFetch),
363
+ // Host extensions over gui-chat-protocol's PluginRuntime. Plugin
364
+ // authors access via `runtime as MulmoclaudeRuntime` for now.
365
+ // Phase 3 of the Encore plan upstreams these into the protocol.
366
+ notifier: makeScopedNotifier(pkgName),
367
+ tasks: makeScopedTasks(pkgName, taskManager),
368
+ chat: makeScopedChat(pkgName),
369
+ };
370
+ }
371
+
372
+ // Re-export host's writeFileAtomic so plugin runtime tests can
373
+ // inspect the same primitive without reaching into utils/.
374
+ export { writeFileAtomic };
375
+
376
+ // Re-export logger type so the loader can type-check sink injection
377
+ // without re-importing from system/logger.
378
+ export type { Logger };
@@ -0,0 +1,72 @@
1
+ // Pure helpers for the translation cache. Kept side-effect free so
2
+ // the unit tests can exercise the lookup / merge / assemble logic
3
+ // without touching the filesystem or any LLM.
4
+
5
+ import type { DictionaryFile } from "./types.js";
6
+
7
+ export function emptyDictionary(): DictionaryFile {
8
+ return { sentences: {} };
9
+ }
10
+
11
+ // `obj[userKey] = value` with `userKey === "__proto__"` triggers the
12
+ // inherited setter on Object.prototype and mutates the prototype
13
+ // chain instead of creating an own property — losing the entry and
14
+ // polluting `obj`'s shape. `Object.defineProperty` bypasses the
15
+ // setter and always creates a normal own data property.
16
+ function safeAssign<V>(target: Record<string, V>, key: string, value: V): void {
17
+ Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
18
+ }
19
+
20
+ export function lookupCached(dict: DictionaryFile, sentence: string, lang: string): string | undefined {
21
+ return dict.sentences[sentence]?.[lang];
22
+ }
23
+
24
+ export interface SplitResult {
25
+ /** Map from input sentence to its cached translation. */
26
+ readonly cached: Map<string, string>;
27
+ /** Distinct sentences that need a fresh LLM translation. */
28
+ readonly misses: readonly string[];
29
+ }
30
+
31
+ export function splitHitMiss(dict: DictionaryFile, sentences: readonly string[], lang: string): SplitResult {
32
+ const cached = new Map<string, string>();
33
+ const missesSet = new Set<string>();
34
+ for (const sentence of sentences) {
35
+ const translated = lookupCached(dict, sentence, lang);
36
+ if (translated !== undefined) {
37
+ cached.set(sentence, translated);
38
+ } else {
39
+ missesSet.add(sentence);
40
+ }
41
+ }
42
+ return { cached, misses: Array.from(missesSet) };
43
+ }
44
+
45
+ export function mergeTranslations(dict: DictionaryFile, lang: string, fresh: ReadonlyMap<string, string>): DictionaryFile {
46
+ const next: Record<string, Record<string, string>> = {};
47
+ for (const [source, langs] of Object.entries(dict.sentences)) {
48
+ safeAssign(next, source, { ...langs });
49
+ }
50
+ for (const [source, translated] of fresh) {
51
+ // `next[source]` would return `Object.prototype` when source is
52
+ // `"__proto__"`; `Object.hasOwn` keeps us in own-property territory.
53
+ const existing = Object.hasOwn(next, source) ? next[source] : {};
54
+ // `lang` is regex-validated upstream (`^[a-z]{2}(?:-[A-Z]{2})?$`)
55
+ // so it cannot be `__proto__` / `constructor` / `prototype`; the
56
+ // outer assignment by user-supplied `source` is the only unsafe
57
+ // site and uses safeAssign.
58
+ existing[lang] = translated;
59
+ safeAssign(next, source, existing);
60
+ }
61
+ return { sentences: next };
62
+ }
63
+
64
+ export function assembleResult(sentences: readonly string[], cached: ReadonlyMap<string, string>, fresh: ReadonlyMap<string, string>): string[] {
65
+ return sentences.map((sentence) => {
66
+ const translated = cached.get(sentence) ?? fresh.get(sentence);
67
+ if (translated === undefined) {
68
+ throw new Error(`[translation] missing translation for ${JSON.stringify(sentence)}`);
69
+ }
70
+ return translated;
71
+ });
72
+ }