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
@@ -12,8 +12,11 @@ import { fillMarkdownImagePlaceholders } from "../../utils/files/markdown-image-
12
12
  import { saveMarkdown, overwriteMarkdown, isMarkdownPath } from "../../utils/files/markdown-store.js";
13
13
  import { saveSpreadsheet, overwriteSpreadsheet, isSpreadsheetPath } from "../../utils/files/spreadsheet-store.js";
14
14
  import { API_ROUTES } from "../../../src/config/apiRoutes.js";
15
+ import { bindRoute } from "../../utils/router.js";
16
+ import { collectPluginMetaDiagnostics } from "../../plugins/diagnostics.js";
15
17
  import { log } from "../../system/logger/index.js";
16
18
  import { previewSnippet } from "../../utils/logPreview.js";
19
+ import { publishFileChange } from "../../events/file-change.js";
17
20
 
18
21
  const router = Router();
19
22
 
@@ -66,6 +69,7 @@ interface PresentDocumentBody {
66
69
 
67
70
  interface PresentDocumentSuccess {
68
71
  message: string;
72
+ instructions: string;
69
73
  title: string;
70
74
  data: { markdown: string; filenamePrefix: string };
71
75
  }
@@ -74,8 +78,9 @@ interface PresentDocumentError {
74
78
  error: string;
75
79
  }
76
80
 
77
- router.post(
78
- API_ROUTES.plugins.presentDocument,
81
+ bindRoute(
82
+ router,
83
+ API_ROUTES.markdown.create,
79
84
  async (req: Request<object, unknown, PresentDocumentBody>, res: Response<PresentDocumentSuccess | PresentDocumentError>) => {
80
85
  const { title, markdown, filenamePrefix } = req.body;
81
86
  log.info("plugins", "presentDocument: start", {
@@ -92,7 +97,8 @@ router.post(
92
97
  const markdownPath = await saveMarkdown(filledMarkdown, filenamePrefix);
93
98
  log.info("plugins", "presentDocument: ok", { markdownPath, bytes: filledMarkdown.length });
94
99
  res.json({
95
- message: `Document "${title}" is ready.`,
100
+ message: `Saved markdown to ${markdownPath}`,
101
+ instructions: "Acknowledge that the document has been presented to the user.",
96
102
  title,
97
103
  data: { markdown: markdownPath, filenamePrefix },
98
104
  });
@@ -117,8 +123,9 @@ interface UpdateMarkdownError {
117
123
  error: string;
118
124
  }
119
125
 
120
- router.put(
121
- API_ROUTES.plugins.updateMarkdown,
126
+ bindRoute(
127
+ router,
128
+ API_ROUTES.markdown.update,
122
129
  async (req: Request<object, unknown, UpdateMarkdownBody>, res: Response<UpdateMarkdownResponse | UpdateMarkdownError>) => {
123
130
  const { relativePath, markdown } = req.body;
124
131
  log.info("plugins", "updateMarkdown: start", {
@@ -140,6 +147,7 @@ router.put(
140
147
  try {
141
148
  await overwriteMarkdown(relativePath, markdown);
142
149
  log.info("plugins", "updateMarkdown: ok", { pathPreview: previewSnippet(relativePath), bytes: markdown.length });
150
+ void publishFileChange(relativePath);
143
151
  res.json({ path: relativePath });
144
152
  } catch (err) {
145
153
  log.error("plugins", "updateMarkdown: threw", { pathPreview: previewSnippet(relativePath), error: errorMessage(err) });
@@ -156,8 +164,9 @@ router.put(
156
164
  // than fabricating a fake context.
157
165
 
158
166
  // presentSpreadsheet — validate, then save sheets to disk
159
- router.post(
160
- API_ROUTES.plugins.presentSpreadsheet,
167
+ bindRoute(
168
+ router,
169
+ API_ROUTES.spreadsheet.create,
161
170
  wrapPluginExecute<SpreadsheetArgs, unknown>(async (req) => {
162
171
  const result = await executeSpreadsheet(req.body);
163
172
  if (!Array.isArray(result.data.sheets)) {
@@ -184,8 +193,9 @@ interface UpdateSpreadsheetError {
184
193
  error: string;
185
194
  }
186
195
 
187
- router.put(
188
- API_ROUTES.plugins.updateSpreadsheet,
196
+ bindRoute(
197
+ router,
198
+ API_ROUTES.spreadsheet.update,
189
199
  async (req: Request<object, unknown, UpdateSpreadsheetBody>, res: Response<UpdateSpreadsheetResponse | UpdateSpreadsheetError>) => {
190
200
  const { relativePath, sheets } = req.body;
191
201
  log.info("plugins", "updateSpreadsheet: start", {
@@ -228,8 +238,9 @@ router.post(
228
238
  );
229
239
 
230
240
  // presentForm — form
231
- router.post(
232
- API_ROUTES.plugins.form,
241
+ bindRoute(
242
+ router,
243
+ API_ROUTES.form.dispatch,
233
244
  wrapPluginExecute((req) => executeForm(null as never, req.body)),
234
245
  );
235
246
 
@@ -240,11 +251,12 @@ router.post(
240
251
  const BLANK_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=";
241
252
 
242
253
  // openCanvas — drawing canvas
243
- router.post(
244
- API_ROUTES.plugins.canvas,
254
+ bindRoute(
255
+ router,
256
+ API_ROUTES.canvas.dispatch,
245
257
  wrapPluginExecute(async () => {
246
258
  const imagePath = await saveImage(BLANK_PNG_BASE64);
247
- const base = await executeOpenCanvas();
259
+ const base = await executeOpenCanvas(imagePath);
248
260
  return { ...base, data: { imageData: imagePath, prompt: "" } };
249
261
  }),
250
262
  );
@@ -255,4 +267,12 @@ router.post(
255
267
  wrapPluginExecute((req) => executePresent3D(null as never, req.body)),
256
268
  );
257
269
 
270
+ // META aggregator diagnostics — boot-time host/plugin or plugin/plugin
271
+ // key collisions. The frontend fetches this once at mount so a tab
272
+ // that opens after the boot-time `publishNotification` fired still
273
+ // gets the warning. Empty array when clean.
274
+ router.get(API_ROUTES.plugins.diagnostics, (_req, res) => {
275
+ res.json({ diagnostics: collectPluginMetaDiagnostics() });
276
+ });
277
+
258
278
  export default router;
@@ -2,11 +2,14 @@ import { Router, Request, Response } from "express";
2
2
  import { WORKSPACE_DIRS } from "../../workspace/paths.js";
3
3
  import { writeWorkspaceText } from "../../utils/files/workspace-io.js";
4
4
  import { buildArtifactPath } from "../../utils/files/naming.js";
5
+ import { overwriteHtml, isHtmlPath } from "../../utils/files/html-store.js";
5
6
  import { errorMessage } from "../../utils/errors.js";
6
7
  import { badRequest, serverError } from "../../utils/httpError.js";
7
8
  import { API_ROUTES } from "../../../src/config/apiRoutes.js";
9
+ import { bindRoute } from "../../utils/router.js";
8
10
  import { log } from "../../system/logger/index.js";
9
11
  import { previewSnippet } from "../../utils/logPreview.js";
12
+ import { publishFileChange } from "../../events/file-change.js";
10
13
 
11
14
  const router = Router();
12
15
 
@@ -18,7 +21,7 @@ interface PresentHtmlBody {
18
21
  interface PresentHtmlSuccessResponse {
19
22
  message: string;
20
23
  instructions: string;
21
- data: { html: string; title?: string; filePath: string };
24
+ data: { title?: string; filePath: string };
22
25
  }
23
26
 
24
27
  interface PresentHtmlErrorResponse {
@@ -27,7 +30,7 @@ interface PresentHtmlErrorResponse {
27
30
 
28
31
  type PresentHtmlResponse = PresentHtmlSuccessResponse | PresentHtmlErrorResponse;
29
32
 
30
- router.post(API_ROUTES.html.present, async (req: Request<object, unknown, PresentHtmlBody>, res: Response<PresentHtmlResponse>) => {
33
+ bindRoute(router, API_ROUTES.html.create, async (req: Request<object, unknown, PresentHtmlBody>, res: Response<PresentHtmlResponse>) => {
31
34
  const { html, title } = req.body;
32
35
  log.info("html", "present: start", {
33
36
  titlePreview: typeof title === "string" ? previewSnippet(title) : undefined,
@@ -43,10 +46,12 @@ router.post(API_ROUTES.html.present, async (req: Request<object, unknown, Presen
43
46
  const filePath = buildArtifactPath(WORKSPACE_DIRS.htmls, title, ".html", "page");
44
47
  await writeWorkspaceText(filePath, html);
45
48
  log.info("html", "present: ok", { filePath, bytes: html.length });
49
+ // Fire-and-forget: any subscribed View tab refetches via cache-bust.
50
+ void publishFileChange(filePath);
46
51
  res.json({
47
52
  message: `Saved HTML to ${filePath}`,
48
53
  instructions: "Acknowledge that the HTML page has been presented to the user.",
49
- data: { html, title, filePath },
54
+ data: { title, filePath },
50
55
  });
51
56
  } catch (err) {
52
57
  log.error("html", "present: threw", { error: errorMessage(err) });
@@ -54,4 +59,54 @@ router.post(API_ROUTES.html.present, async (req: Request<object, unknown, Presen
54
59
  }
55
60
  });
56
61
 
62
+ // Update html file on disk (user edits in View). Body carries the
63
+ // workspace-relative path verbatim (e.g.
64
+ // `artifacts/html/2026/04/page-abc.html`) so the route doesn't have to
65
+ // reconstruct one from a basename — same shape as presentDocument.updateMarkdown.
66
+ interface UpdateHtmlBody {
67
+ relativePath: string;
68
+ html: string;
69
+ }
70
+
71
+ interface UpdateHtmlSuccessResponse {
72
+ path: string;
73
+ }
74
+
75
+ interface UpdateHtmlErrorResponse {
76
+ error: string;
77
+ }
78
+
79
+ bindRoute(
80
+ router,
81
+ API_ROUTES.html.update,
82
+ async (req: Request<object, unknown, UpdateHtmlBody>, res: Response<UpdateHtmlSuccessResponse | UpdateHtmlErrorResponse>) => {
83
+ const { relativePath, html } = req.body;
84
+ log.info("html", "update: start", {
85
+ pathPreview: typeof relativePath === "string" ? previewSnippet(relativePath) : undefined,
86
+ bytes: typeof html === "string" ? html.length : undefined,
87
+ });
88
+ if (!html) {
89
+ log.warn("html", "update: missing html");
90
+ badRequest(res, "html is required");
91
+ return;
92
+ }
93
+ if (!relativePath || !isHtmlPath(relativePath)) {
94
+ log.warn("html", "update: invalid relativePath", {
95
+ pathPreview: typeof relativePath === "string" ? previewSnippet(relativePath) : undefined,
96
+ });
97
+ badRequest(res, "invalid html relativePath");
98
+ return;
99
+ }
100
+ try {
101
+ await overwriteHtml(relativePath, html);
102
+ log.info("html", "update: ok", { pathPreview: previewSnippet(relativePath), bytes: html.length });
103
+ void publishFileChange(relativePath);
104
+ res.json({ path: relativePath });
105
+ } catch (err) {
106
+ log.error("html", "update: threw", { pathPreview: previewSnippet(relativePath), error: errorMessage(err) });
107
+ serverError(res, errorMessage(err));
108
+ }
109
+ },
110
+ );
111
+
57
112
  export default router;
@@ -104,14 +104,7 @@ function saveRoleResult(input: ManageRolesInput, sessionId: string): Record<stri
104
104
  const { role, isRename } = validated;
105
105
  const { action, oldRoleId } = input;
106
106
 
107
- // Strip switchRole before saving — it is injected at load time by server/roles.ts
108
- const pluginsToSave = role.availablePlugins ?? [];
109
- const roleToSave = {
110
- ...role,
111
- availablePlugins: pluginsToSave.filter((plugin) => plugin !== "switchRole"),
112
- };
113
-
114
- saveRole(role.id, roleToSave);
107
+ saveRole(role.id, role);
115
108
  // On rename, remove the old file even if its id matches a built-in —
116
109
  // a file at `config/roles/<builtin>.json` is a user-created override,
117
110
  // not the built-in itself (which lives in BUILTIN_ROLES). Leaving it
@@ -0,0 +1,224 @@
1
+ // HTTP routes for runtime-loaded plugins (#1043 C-2).
2
+ //
3
+ // GET /api/plugins/runtime/list
4
+ // → { plugins: [{ name, version, toolName, description }, …] }
5
+ //
6
+ // POST /api/plugins/runtime/:pkg/dispatch
7
+ // body: <args> directly — same convention as static plugin
8
+ // endpoints (see server/api/routes/plugins.ts), so
9
+ // mcp-server's generic `postJson(endpoint, args)` works
10
+ // unchanged for runtime plugins.
11
+ // → whatever the plugin's `execute()` returns (forwarded as JSON)
12
+ //
13
+ // GET /api/plugins/runtime/:pkg/oauth/callback?code=&state=&error=
14
+ // Generic OAuth redirect receiver (#1162). Spotify (and any
15
+ // future OAuth-using runtime plugin) registers this URL with
16
+ // its provider's developer dashboard; the browser comes back
17
+ // here after consent. Host extracts `:pkg`, registry-looks-up
18
+ // the plugin, forwards as `kind: "oauthCallback"` dispatch
19
+ // args, and renders the plugin's returned `html` (or a fallback)
20
+ // back to the browser. Bearer-auth-EXEMPT (no Authorization
21
+ // header on a redirect); CSRF defended by the plugin's
22
+ // single-use `state`.
23
+ //
24
+ // GET /api/plugins/runtime/:pkg/:version/*
25
+ // Static-mount of the extracted cache directory; the frontend
26
+ // loader uses this for `import("/api/plugins/runtime/<pkg>/<ver>/dist/vue.js")`.
27
+ //
28
+ // The registry is owned by `server/plugins/runtime-registry.ts` and
29
+ // populated at boot from the install ledger. A 404 from any of these
30
+ // routes means the plugin isn't installed (or failed to load — see
31
+ // boot logs).
32
+
33
+ import { realpathSync } from "node:fs";
34
+ import { Router, type Request, type Response } from "express";
35
+ import { API_ROUTES } from "../../../src/config/apiRoutes.js";
36
+ import { getRuntimePluginByOauthAlias, getRuntimePlugins } from "../../plugins/runtime-registry.js";
37
+ import { notFound, serverError } from "../../utils/httpError.js";
38
+ import { errorMessage } from "../../utils/errors.js";
39
+ import { isRecord } from "../../utils/types.js";
40
+ import { resolveWithinRoot } from "../../utils/files/safe.js";
41
+ import { readPluginAsset } from "../../utils/files/plugins-io.js";
42
+ import { log } from "../../system/logger/index.js";
43
+
44
+ const LOG_PREFIX = "api/plugins/runtime";
45
+
46
+ const router = Router();
47
+
48
+ interface ListedPlugin {
49
+ name: string;
50
+ version: string;
51
+ toolName: string;
52
+ description: string;
53
+ /** Absolute URL prefix the frontend uses for static-mount fetches. */
54
+ assetBase: string;
55
+ }
56
+
57
+ router.get(API_ROUTES.plugins.runtimeList, (_req: Request, res: Response<{ plugins: ListedPlugin[] }>) => {
58
+ const plugins = getRuntimePlugins().map<ListedPlugin>((entry) => ({
59
+ name: entry.name,
60
+ version: entry.version,
61
+ toolName: entry.definition.name,
62
+ description: entry.definition.description,
63
+ assetBase: `/api/plugins/runtime/${encodeURIComponent(entry.name)}/${encodeURIComponent(entry.version)}`,
64
+ }));
65
+ res.json({ plugins });
66
+ });
67
+
68
+ router.post(API_ROUTES.plugins.runtimeDispatch, async (req: Request<{ pkg: string }>, res: Response) => {
69
+ const pkg = decodeURIComponent(req.params.pkg);
70
+ const plugin = getRuntimePlugins().find((entry) => entry.name === pkg);
71
+ if (!plugin) {
72
+ notFound(res, `runtime plugin "${pkg}" not registered`);
73
+ return;
74
+ }
75
+ if (!plugin.execute) {
76
+ serverError(res, `runtime plugin "${pkg}" has no execute() — the package's dist/index.js must export a function under "${plugin.definition.name}"`);
77
+ return;
78
+ }
79
+ const args = isRecord(req.body) ? req.body : {};
80
+ try {
81
+ // gui-chat-protocol's ToolPluginCore.execute is
82
+ // `(context: ToolContext, args) => Promise<ToolResult>`. The
83
+ // server has no UI-side state to share, so context is an empty
84
+ // object — but it MUST be the first arg, otherwise the plugin
85
+ // destructures its args from `undefined` and the call fails with
86
+ // "Cannot destructure property '<field>' of '<arg>' as it is
87
+ // undefined".
88
+ const context = {};
89
+ const result = await plugin.execute(context, args);
90
+ // Forward whatever the plugin returns as the response body
91
+ // (mirrors static plugin routes — see plugins.ts). MCP server
92
+ // spreads this into the toolResult event downstream.
93
+ res.json(result);
94
+ } catch (err) {
95
+ log.error(LOG_PREFIX, "execute failed", { pkg, error: errorMessage(err) });
96
+ serverError(res, `plugin execute failed: ${errorMessage(err)}`);
97
+ }
98
+ });
99
+
100
+ // Generic OAuth callback receiver (#1162). The plugin owns state
101
+ // validation + token exchange — the host's role is just URL routing
102
+ // + HTML response rendering. The plugin returns
103
+ // `{ html?: string; message?: string }` from its `oauthCallback`
104
+ // kind handler; the host renders `html` verbatim if present, else
105
+ // falls back to a minimal default.
106
+ function buildOauthCallbackArgs(query: Request["query"]) {
107
+ const { code, state, error: providerError } = query;
108
+ return {
109
+ kind: "oauthCallback" as const,
110
+ code: typeof code === "string" ? code : undefined,
111
+ state: typeof state === "string" ? state : undefined,
112
+ error: typeof providerError === "string" ? providerError : undefined,
113
+ };
114
+ }
115
+
116
+ function sendOauthCallbackResult(res: Response, result: unknown): void {
117
+ const html = isRecord(result) && typeof result.html === "string" ? result.html : null;
118
+ const message = isRecord(result) && typeof result.message === "string" ? result.message : "";
119
+ const ok = isRecord(result) && result.ok === true;
120
+ res
121
+ .status(ok ? 200 : 400)
122
+ .type("text/html")
123
+ .send(html ?? renderFallbackCallbackHtml(ok ? "OAuth complete" : "OAuth failed", message || "(no message)"));
124
+ }
125
+
126
+ router.get(API_ROUTES.plugins.runtimeOauthCallback, async (req: Request<{ alias: string }>, res: Response) => {
127
+ const { alias } = req.params;
128
+ const plugin = getRuntimePluginByOauthAlias(alias);
129
+ if (!plugin) {
130
+ notFound(res, `no runtime plugin registered for OAuth callback alias "${alias}"`);
131
+ return;
132
+ }
133
+ if (!plugin.execute) {
134
+ serverError(res, `runtime plugin "${plugin.name}" has no execute()`);
135
+ return;
136
+ }
137
+ try {
138
+ const result = await plugin.execute({}, buildOauthCallbackArgs(req.query));
139
+ sendOauthCallbackResult(res, result);
140
+ } catch (err) {
141
+ log.error(LOG_PREFIX, "oauth callback dispatch threw", { alias, plugin: plugin.name, error: errorMessage(err) });
142
+ res
143
+ .status(500)
144
+ .type("text/html")
145
+ .send(renderFallbackCallbackHtml("Plugin error", errorMessage(err)));
146
+ }
147
+ });
148
+
149
+ function renderFallbackCallbackHtml(title: string, body: string): string {
150
+ // Minimal fallback when the plugin doesn't return its own HTML.
151
+ // Plugins are encouraged to return a richer page; this is just a
152
+ // safety net so the browser always gets something readable.
153
+ const escape = (value: string) =>
154
+ value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
155
+ return `<!doctype html><html lang="en"><meta charset="utf-8"><title>${escape(title)}</title>
156
+ <style>body{font-family:system-ui,sans-serif;max-width:40rem;margin:4rem auto;padding:0 1rem;color:#111}h1{margin-bottom:1rem}pre{white-space:pre-wrap;background:#f5f5f5;padding:1rem;border-radius:.5rem}</style>
157
+ <h1>${escape(title)}</h1>
158
+ <pre>${escape(body)}</pre>
159
+ </html>`;
160
+ }
161
+
162
+ // Static-mount of an installed plugin's directory. Resolution flow:
163
+ //
164
+ // 1. Look up `(pkg, version)` in the runtime registry. Presets and
165
+ // user-installed plugins are both registered server-side, with
166
+ // cachePath set from a trusted source (preset list or workspace
167
+ // ledger). If the URL doesn't match any registered entry, 404 —
168
+ // this is the trust boundary that prevents arbitrary-file reads
169
+ // via percent-encoded `../` in `pkg` / `version` (the bearer-
170
+ // auth exemption makes this an unauthenticated path).
171
+ // 2. realpath the registered cachePath. Symlinks inside the
172
+ // extracted tree (e.g. dist/foo.js → /etc/passwd) cannot escape
173
+ // because `resolveWithinRoot(rootReal, subPath)` rejects any
174
+ // target that resolves outside the plugin's own root.
175
+ //
176
+ // The earlier "must be inside WORKSPACE_PATHS.pluginCache" anchor is
177
+ // gone — presets live under `node_modules/<pkg>/`, not in the
178
+ // workspace cache. The registry-membership check replaces that
179
+ // anchor: the registry is server-set, so its cachePath values are
180
+ // already trusted regardless of where on disk they point.
181
+ /** Look up a registered plugin and return the realpath of its root.
182
+ * Returns null when the (pkg, version) pair is not registered, when
183
+ * the cachePath does not exist on disk, or when realpath fails.
184
+ * Exported for tests. */
185
+ export function resolvePluginRoot(pkg: string, version: string): string | null {
186
+ const plugin = getRuntimePlugins().find((entry) => entry.name === pkg && entry.version === version);
187
+ if (!plugin) return null;
188
+ try {
189
+ return realpathSync(plugin.cachePath);
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+
195
+ router.get(API_ROUTES.plugins.runtimeAsset, async (req: Request<{ pkg: string; version: string; splat?: string | string[] }>, res: Response) => {
196
+ const pkg = decodeURIComponent(req.params.pkg);
197
+ const version = decodeURIComponent(req.params.version);
198
+ // Express 5 returns `splat` as `string[]` when the wildcard
199
+ // matched multiple segments, `string` for a single segment, and
200
+ // empty/undefined for an empty wildcard. Normalise to the joined
201
+ // path so downstream `path.join` works on every shape.
202
+ const rawSplat = req.params.splat;
203
+ const subPath = Array.isArray(rawSplat) ? rawSplat.join("/") : (rawSplat ?? "");
204
+ const rootReal = resolvePluginRoot(pkg, version);
205
+ if (!rootReal) {
206
+ notFound(res, "asset not found");
207
+ return;
208
+ }
209
+ const resolved = resolveWithinRoot(rootReal, subPath);
210
+ if (!resolved) {
211
+ notFound(res, "asset not found");
212
+ return;
213
+ }
214
+ try {
215
+ const { data, contentType } = await readPluginAsset(resolved);
216
+ res.setHeader("Content-Type", contentType);
217
+ res.send(data);
218
+ } catch (err) {
219
+ log.error(LOG_PREFIX, "asset read failed", { pkg, version, subPath, error: errorMessage(err) });
220
+ serverError(res, "asset read failed");
221
+ }
222
+ });
223
+
224
+ export default router;
@@ -3,12 +3,13 @@ import { loadSchedulerItems, saveSchedulerItems } from "../../utils/files/schedu
3
3
  import { dispatchScheduler, type SchedulerActionInput } from "./schedulerHandlers.js";
4
4
  import { respondWithDispatchResult, type DispatchSuccessResponse, type DispatchErrorResponse } from "./dispatchResponse.js";
5
5
  import { API_ROUTES } from "../../../src/config/apiRoutes.js";
6
+ import { bindRoute } from "../../utils/router.js";
6
7
  import { SESSION_ORIGINS } from "../../../src/types/session.js";
7
8
  import { loadUserTasks, validateAndCreate, refreshUserTasks } from "../../workspace/skills/user-tasks.js";
8
9
  import { saveUserTasks } from "../../utils/files/user-tasks-io.js";
9
10
  import { startChat } from "./agent.js";
10
11
  import { log } from "../../system/logger/index.js";
11
- import { SCHEDULER_ACTIONS, TASK_ACTIONS } from "../../../src/config/schedulerActions.js";
12
+ import { SCHEDULER_ACTIONS, TASK_ACTIONS } from "../../../src/plugins/scheduler/actions.js";
12
13
  import { badRequest, notFound, serverError } from "../../utils/httpError.js";
13
14
  import { errorMessage } from "../../utils/errors.js";
14
15
  import { makeUuid } from "../../utils/id.js";
@@ -30,7 +31,7 @@ function saveItems(items: ScheduledItem[]): void {
30
31
  saveSchedulerItems(items);
31
32
  }
32
33
 
33
- router.get(API_ROUTES.scheduler.base, (_req: Request, res: Response<{ data: { items: ScheduledItem[] } }>) => {
34
+ bindRoute(router, API_ROUTES.scheduler.list, (_req: Request, res: Response<{ data: { items: ScheduledItem[] } }>) => {
34
35
  res.json({ data: { items: loadItems() } });
35
36
  });
36
37
 
@@ -43,8 +44,9 @@ interface SchedulerBody extends SchedulerActionInput {
43
44
  roleId?: string;
44
45
  }
45
46
 
46
- router.post(
47
- API_ROUTES.scheduler.base,
47
+ bindRoute(
48
+ router,
49
+ API_ROUTES.scheduler.dispatch,
48
50
  async (req: Request<object, unknown, SchedulerBody>, res: Response<DispatchSuccessResponse<ScheduledItem> | DispatchErrorResponse | unknown>) => {
49
51
  const { action, ...input } = req.body;
50
52
 
@@ -108,7 +110,7 @@ async function handleTaskAction(action: string, input: Record<string, unknown>,
108
110
  notFound(res, `task not found: ${taskId}`);
109
111
  return;
110
112
  }
111
- const name = tasks[idx].name;
113
+ const { name } = tasks[idx];
112
114
  tasks.splice(idx, 1);
113
115
  await saveUserTasks(tasks);
114
116
  await refreshUserTasks();
@@ -86,7 +86,7 @@ function applyPropPatch(current: ScheduledItem["props"], patch: Record<string, s
86
86
  const next: ScheduledItem["props"] = { ...current };
87
87
  for (const [key, value] of Object.entries(patch)) {
88
88
  if (value === null) {
89
- delete next[key];
89
+ Reflect.deleteProperty(next, key);
90
90
  } else {
91
91
  next[key] = value;
92
92
  }
@@ -11,6 +11,7 @@ import { Router, type Request, type Response } from "express";
11
11
  import { getSchedulerTasks, getSchedulerLogs } from "../../events/scheduler-adapter.js";
12
12
  import type { TaskLogEntry } from "@receptron/task-scheduler";
13
13
  import { API_ROUTES } from "../../../src/config/apiRoutes.js";
14
+ import { bindRoute } from "../../utils/router.js";
14
15
  import { SESSION_ORIGINS } from "../../../src/types/session.js";
15
16
  import { loadUserTasks, validateAndCreate, applyUpdate, withUserTaskLock } from "../../workspace/skills/user-tasks.js";
16
17
  import { badRequest, notFound, serverError } from "../../utils/httpError.js";
@@ -24,7 +25,7 @@ const router = Router();
24
25
 
25
26
  // ── List all tasks ──────────────────────────────────────────────
26
27
 
27
- router.get(API_ROUTES.scheduler.tasks, (_req: Request, res: Response) => {
28
+ bindRoute(router, API_ROUTES.scheduler.tasksList, (_req: Request, res: Response) => {
28
29
  log.info("scheduler-tasks", "list: start");
29
30
  try {
30
31
  // getSchedulerTasks() returns system-only tasks (registered via
@@ -49,7 +50,7 @@ router.get(API_ROUTES.scheduler.tasks, (_req: Request, res: Response) => {
49
50
 
50
51
  // ── Create user task ────────────────────────────────────────────
51
52
 
52
- router.post(API_ROUTES.scheduler.tasks, async (req: Request, res: Response) => {
53
+ bindRoute(router, API_ROUTES.scheduler.tasksCreate, async (req: Request, res: Response) => {
53
54
  log.info("scheduler-tasks", "create: start");
54
55
  const validated = validateAndCreate(req.body);
55
56
  if (validated.kind === "error") {
@@ -74,7 +75,7 @@ router.post(API_ROUTES.scheduler.tasks, async (req: Request, res: Response) => {
74
75
 
75
76
  // ── Update user task ────────────────────────────────────────────
76
77
 
77
- router.put(API_ROUTES.scheduler.task, async (req: Request<{ id: string }>, res: Response) => {
78
+ bindRoute(router, API_ROUTES.scheduler.taskUpdate, async (req: Request<{ id: string }>, res: Response) => {
78
79
  const { id: taskId } = req.params;
79
80
  log.info("scheduler-tasks", "update: start", { taskId });
80
81
  try {
@@ -102,7 +103,7 @@ router.put(API_ROUTES.scheduler.task, async (req: Request<{ id: string }>, res:
102
103
 
103
104
  // ── Delete user task ────────────────────────────────────────────
104
105
 
105
- router.delete(API_ROUTES.scheduler.task, async (req: Request<{ id: string }>, res: Response) => {
106
+ bindRoute(router, API_ROUTES.scheduler.taskDelete, async (req: Request<{ id: string }>, res: Response) => {
106
107
  const { id: taskId } = req.params;
107
108
  log.info("scheduler-tasks", "delete: start", { taskId });
108
109
  try {
@@ -128,7 +129,7 @@ router.delete(API_ROUTES.scheduler.task, async (req: Request<{ id: string }>, re
128
129
 
129
130
  // ── Manual trigger ──────────────────────────────────────────────
130
131
 
131
- router.post(API_ROUTES.scheduler.taskRun, async (req: Request<{ id: string }>, res: Response) => {
132
+ bindRoute(router, API_ROUTES.scheduler.taskRun, async (req: Request<{ id: string }>, res: Response) => {
132
133
  const { id: taskId } = req.params;
133
134
  log.info("scheduler-tasks", "run: start", { taskId });
134
135
  // Check user tasks first
@@ -176,11 +177,11 @@ interface LogQuery {
176
177
  limit?: string;
177
178
  }
178
179
 
179
- router.get(API_ROUTES.scheduler.logs, async (req: Request<object, unknown, object, LogQuery>, res: Response<{ logs: TaskLogEntry[] }>) => {
180
+ bindRoute(router, API_ROUTES.scheduler.logs, async (req: Request<object, unknown, object, LogQuery>, res: Response<{ logs: TaskLogEntry[] }>) => {
180
181
  const MAX_LIMIT = 500;
181
182
  const rawLimitStr = getOptionalStringQuery(req, "limit");
182
183
  const rawLimit = rawLimitStr ? parseInt(rawLimitStr, 10) : undefined;
183
- const limit = Number.isFinite(rawLimit) && rawLimit! > 0 ? Math.min(rawLimit!, MAX_LIMIT) : undefined;
184
+ const limit = rawLimit !== undefined && Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, MAX_LIMIT) : undefined;
184
185
  const taskId = getOptionalStringQuery(req, "taskId");
185
186
  log.info("scheduler-tasks", "logs: start", { taskId, limit });
186
187
  try {