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
@@ -26,10 +26,32 @@
26
26
  </div>
27
27
  <div class="markdown-content-wrapper">
28
28
  <div class="p-4">
29
+ <!-- Frontmatter properties panel (FileContentRenderer-style)
30
+ — only rendered when the file has a `---\n...\n---`
31
+ header. Lazy-on-write means most existing files don't
32
+ have one yet (#895). -->
33
+ <div v-if="mdDoc.fields.length > 0" class="mb-3 rounded border border-gray-200 bg-gray-50 p-3 text-xs">
34
+ <div v-for="field in mdDoc.fields" :key="field.key" class="flex items-baseline gap-2 py-0.5">
35
+ <span class="font-semibold text-gray-600 shrink-0">{{ field.key }}:</span>
36
+ <template v-if="Array.isArray(field.value)">
37
+ <span class="flex flex-wrap gap-1">
38
+ <span
39
+ v-for="(item, idx) in field.value"
40
+ :key="String(idx) + ':' + formatScalarField(item)"
41
+ class="rounded-full bg-white border border-gray-300 px-2 py-0.5 text-gray-700"
42
+ >
43
+ {{ formatScalarField(item) }}
44
+ </span>
45
+ </span>
46
+ </template>
47
+ <span v-else class="text-gray-800 break-words">{{ formatScalarField(field.value) }}</span>
48
+ </div>
49
+ </div>
29
50
  <!-- Click delegation: a single listener on the wrapper picks
30
51
  up every interactive checkbox inserted by v-html. We
31
52
  cannot bind @click directly on each `<input>` because
32
53
  v-html bypasses Vue's template compiler. -->
54
+ <!-- eslint-disable-next-line vue/no-v-html -- marked.parse output of app-owned markdown content; trusted in-process render -->
33
55
  <div class="markdown-content prose prose-slate max-w-none" @click="onMarkdownClick" v-html="renderedHtml"></div>
34
56
  </div>
35
57
  </div>
@@ -58,18 +80,20 @@
58
80
  import { computed, ref, watch, nextTick } from "vue";
59
81
  import { useI18n } from "vue-i18n";
60
82
  import { marked } from "marked";
61
-
62
- const { t } = useI18n();
83
+ import { formatScalarField, useMarkdownDoc } from "../../composables/useMarkdownDoc";
63
84
  import type { ToolResult } from "gui-chat-protocol";
64
- import { isFilePath, type MarkdownToolData } from "./definition";
85
+ import { isFilePath, type MarkdownToolData, type DocumentEndpoints } from "./definition";
65
86
  import { rewriteMarkdownImageRefs } from "../../utils/image/rewriteMarkdownImageRefs";
66
87
  import { findTaskLines, makeTasksInteractive, toggleTaskAt } from "../../utils/markdown/taskList";
67
88
  import { usePdfDownload } from "../../composables/usePdfDownload";
68
89
  import { apiGet, apiPut } from "../../utils/api";
69
- import { API_ROUTES } from "../../config/apiRoutes";
90
+ import { pluginEndpoints } from "../api";
70
91
  import { useClipboardCopy } from "../../composables/useClipboardCopy";
71
92
  import { buildPdfFilename } from "../../utils/files/filename";
72
93
  import { useAppApi } from "../../composables/useAppApi";
94
+ import { useFileChange } from "../../composables/useFileChange";
95
+
96
+ const { t } = useI18n();
73
97
 
74
98
  const props = defineProps<{
75
99
  selectedResult: ToolResult<MarkdownToolData>;
@@ -94,6 +118,9 @@ const loadError = ref<string | null>(null);
94
118
  const markdownContent = ref("");
95
119
  const editableMarkdown = ref("");
96
120
 
121
+ const endpoints = pluginEndpoints<DocumentEndpoints>("markdown");
122
+ const filesEndpoints = pluginEndpoints<{ content: string }>("files");
123
+
97
124
  async function fetchMarkdownContent(): Promise<void> {
98
125
  loadError.value = null;
99
126
  const raw = props.selectedResult.data?.markdown;
@@ -104,7 +131,7 @@ async function fetchMarkdownContent(): Promise<void> {
104
131
  }
105
132
  if (isFilePath(raw)) {
106
133
  loading.value = true;
107
- const result = await apiGet<{ content?: string }>(API_ROUTES.files.content, {
134
+ const result = await apiGet<{ content?: string }>(filesEndpoints.content, {
108
135
  path: raw,
109
136
  });
110
137
  if (!result.ok) {
@@ -128,9 +155,43 @@ async function fetchMarkdownContent(): Promise<void> {
128
155
  // Fetch on mount
129
156
  fetchMarkdownContent();
130
157
 
131
- const hasChanges = computed(() => {
132
- return editableMarkdown.value !== markdownContent.value;
158
+ const hasChanges = computed(() => editableMarkdown.value !== markdownContent.value);
159
+
160
+ // Subscribe to per-file change events so any tab / browser / agent run
161
+ // that overwrites the file refreshes this view automatically. The path
162
+ // passed in is the workspace-relative `data.markdown` (only valid when
163
+ // `isFilePath` — inline legacy content has no on-disk twin).
164
+ const watchedPath = computed(() => {
165
+ const raw = props.selectedResult.data?.markdown;
166
+ return typeof raw === "string" && isFilePath(raw) ? raw : null;
133
167
  });
168
+ const { version: fileVersion } = useFileChange(watchedPath);
169
+
170
+ // Declared early so the `fileVersion` watcher below can reach into the
171
+ // `<details>` element to close the editor when a remote write lands.
172
+ const sourceDetails = ref<HTMLDetailsElement>();
173
+
174
+ // Remote write: refetch so the rendered view tracks disk. If the
175
+ // editor is open we close it first — `fileVersion` only fires once
176
+ // per remote write, so leaving the panel open and skipping the fetch
177
+ // would strand the view on stale content until the next write
178
+ // (#1001 P1). Discarding in-progress edits is rare enough to be
179
+ // acceptable; a "remote changed" banner is queued for a follow-up —
180
+ // see plans/done/feat-file-change-pubsub.md.
181
+ watch(fileVersion, (current, previous) => {
182
+ if (current === 0 || current === previous) return;
183
+ if (sourceDetails.value?.open) {
184
+ sourceDetails.value.open = false;
185
+ }
186
+ void fetchMarkdownContent();
187
+ });
188
+
189
+ // Frontmatter-aware view of the loaded content — separates the
190
+ // `---\n...\n---` header (rendered as a properties panel) from the
191
+ // markdown body (passed to marked). Without this split the header
192
+ // would render as a stray `<hr>` plus key:value plain text in
193
+ // every file the LLM saved with frontmatter (#895 PR A).
194
+ const mdDoc = useMarkdownDoc(markdownContent);
134
195
 
135
196
  const renderedHtml = computed(() => {
136
197
  if (!markdownContent.value) return "";
@@ -142,7 +203,7 @@ const renderedHtml = computed(() => {
142
203
  // references get rewritten.
143
204
  const raw = props.selectedResult.data?.markdown;
144
205
  const basePath = typeof raw === "string" && isFilePath(raw) ? raw.slice(0, raw.lastIndexOf("/")) : "";
145
- const withImages = rewriteMarkdownImageRefs(markdownContent.value, basePath);
206
+ const withImages = rewriteMarkdownImageRefs(mdDoc.value.body, basePath);
146
207
  // Strip the `disabled=""` attribute marked puts on GFM task
147
208
  // checkboxes and tag them so `onMarkdownClick` can find them
148
209
  // (#775). Inline content (no file backing) gets the same
@@ -167,12 +228,11 @@ watch(
167
228
  },
168
229
  );
169
230
 
170
- const sourceDetails = ref<HTMLDetailsElement>();
171
231
  const editing = ref(false);
172
232
  const { copied, copy } = useClipboardCopy();
173
233
 
174
234
  function onDetailsToggle(event: Event) {
175
- const open = (event.target as HTMLDetailsElement).open;
235
+ const { open } = event.target as HTMLDetailsElement;
176
236
  editing.value = open;
177
237
  if (!open) {
178
238
  editableMarkdown.value = markdownContent.value;
@@ -194,7 +254,7 @@ async function downloadPdf() {
194
254
  if (!markdownContent.value) return;
195
255
  const prefix = props.selectedResult.data?.filenamePrefix;
196
256
  const rawName = prefix || props.selectedResult.title || "";
197
- const uuid = props.selectedResult.uuid;
257
+ const { uuid } = props.selectedResult;
198
258
  const filename = buildPdfFilename({
199
259
  name: rawName,
200
260
  fallback: "document",
@@ -215,7 +275,7 @@ async function applyMarkdown() {
215
275
  // (e.g. the YYYY/MM partitions added in #764).
216
276
  if (isFilePath(raw)) {
217
277
  saving.value = true;
218
- const result = await apiPut<unknown>(API_ROUTES.plugins.updateMarkdown, {
278
+ const result = await apiPut<unknown>(endpoints.update.url, {
219
279
  relativePath: raw,
220
280
  markdown: editableMarkdown.value,
221
281
  });
@@ -267,7 +327,7 @@ async function persistTaskMarkdown(relativePath: string, markdown: string): Prom
267
327
  // on screen, and persisting it would clobber unrelated state.
268
328
  if (props.selectedResult.data?.markdown !== relativePath) return;
269
329
 
270
- const result = await apiPut<unknown>(API_ROUTES.plugins.updateMarkdown, {
330
+ const result = await apiPut<unknown>(endpoints.update.url, {
271
331
  relativePath,
272
332
  markdown,
273
333
  });
@@ -294,7 +354,7 @@ async function persistTaskMarkdown(relativePath: string, markdown: string): Prom
294
354
  }
295
355
 
296
356
  function onMarkdownClick(event: MouseEvent): void {
297
- const target = event.target;
357
+ const { target } = event;
298
358
  if (!(target instanceof HTMLInputElement)) return;
299
359
  if (target.type !== "checkbox") return;
300
360
  if (!target.classList.contains("md-task")) return;
@@ -317,20 +377,27 @@ function onMarkdownClick(event: MouseEvent): void {
317
377
  // it as a task; marked treats it as code) — toggling source by
318
378
  // index would corrupt the file. Refuse all clicks when this
319
379
  // happens.
320
- const sourceTasks = findTaskLines(markdownContent.value);
380
+ // Walk only the body (the same source `marked` rendered) so
381
+ // frontmatter contents containing `- [ ]`-shaped YAML never
382
+ // collide with task counting (#895 PR A). The prefix is
383
+ // preserved byte-for-byte and re-attached after the toggle.
384
+ const { body } = mdDoc.value;
385
+ const prefix = markdownContent.value.slice(0, markdownContent.value.length - body.length);
386
+ const sourceTasks = findTaskLines(body);
321
387
  if (sourceTasks.length !== taskInputs.length) {
322
388
  target.checked = !target.checked;
323
389
  saveError.value = t("pluginMarkdown.taskCountMismatch");
324
390
  return;
325
391
  }
326
392
 
327
- const updated = toggleTaskAt(markdownContent.value, taskIndex);
328
- if (updated === null) {
393
+ const updatedBody = toggleTaskAt(body, taskIndex);
394
+ if (updatedBody === null) {
329
395
  // Source/DOM drift — refuse to write something we can't trace.
330
396
  target.checked = !target.checked;
331
397
  return;
332
398
  }
333
399
 
400
+ const updated = prefix + updatedBody;
334
401
  // Optimistic local update — v-html will re-render and the
335
402
  // textarea (if anyone opens it next) sees the same content.
336
403
  markdownContent.value = updated;
@@ -1,6 +1,12 @@
1
1
  import type { ToolDefinition } from "gui-chat-protocol";
2
+ import { META } from "./meta";
3
+ import type { ResolvedRoute } from "../meta-types";
2
4
 
3
- export const TOOL_NAME = "presentDocument";
5
+ export const TOOL_NAME = META.toolName;
6
+ /** Resolved-URL view of the markdown plugin's routes — `{ method,
7
+ * url }` per route key, with the `/api/markdown` prefix already
8
+ * composed by the host. */
9
+ export type DocumentEndpoints = { readonly [K in keyof typeof META.apiRoutes]: ResolvedRoute };
4
10
 
5
11
  export interface MarkdownToolData {
6
12
  markdown: string;
@@ -1,18 +1,20 @@
1
- import type { ToolPlugin } from "../../tools/types";
1
+ import type { PluginRegistration, ToolPlugin } from "../../tools/types";
2
2
  import type { ToolResult } from "gui-chat-protocol";
3
- import toolDefinition, { TOOL_NAME } from "./definition";
4
- import type { MarkdownToolData } from "./definition";
3
+ import toolDefinition, { TOOL_NAME, type DocumentEndpoints, type MarkdownToolData } from "./definition";
4
+ import { pluginEndpoints } from "../api";
5
+ import { wrapWithScope } from "../scope";
5
6
  import View from "./View.vue";
6
7
  import Preview from "./Preview.vue";
7
- import { apiPost } from "../../utils/api";
8
- import { API_ROUTES } from "../../config/apiRoutes";
8
+ import { apiCall } from "../../utils/api";
9
9
  import { makeUuid } from "../../utils/id";
10
10
 
11
11
  const markdownPlugin: ToolPlugin<MarkdownToolData> = {
12
12
  toolDefinition,
13
13
 
14
14
  async execute(_context, args) {
15
- const result = await apiPost<ToolResult<MarkdownToolData>>(API_ROUTES.plugins.presentDocument, args);
15
+ const endpoints = pluginEndpoints<DocumentEndpoints>("markdown");
16
+ const { method, url } = endpoints.create;
17
+ const result = await apiCall<ToolResult<MarkdownToolData>>(url, { method, body: args });
16
18
  if (!result.ok) {
17
19
  return {
18
20
  toolName: TOOL_NAME,
@@ -29,9 +31,14 @@ const markdownPlugin: ToolPlugin<MarkdownToolData> = {
29
31
 
30
32
  isEnabled: () => true,
31
33
  generatingMessage: "Creating document...",
32
- viewComponent: View,
33
- previewComponent: Preview,
34
+ viewComponent: wrapWithScope("markdown", View),
35
+ previewComponent: wrapWithScope("markdown", Preview),
34
36
  };
35
37
 
36
38
  export default markdownPlugin;
37
39
  export { TOOL_NAME };
40
+
41
+ export const REGISTRATION: PluginRegistration = {
42
+ toolName: TOOL_NAME,
43
+ entry: markdownPlugin,
44
+ };
@@ -0,0 +1,16 @@
1
+ import { definePluginMeta } from "../meta-types";
2
+
3
+ export const META = definePluginMeta({
4
+ toolName: "presentDocument",
5
+ apiNamespace: "markdown",
6
+ apiRoutes: {
7
+ /** POST /api/markdown — create a new markdown document. */
8
+ create: { method: "POST", path: "" },
9
+ /** PUT /api/markdown/update — overwrite an existing document.
10
+ * Body carries the workspace-relative path so the route doesn't
11
+ * have to reconstruct one from a basename — required after #764
12
+ * sharded artifact storage by YYYY/MM. */
13
+ update: { method: "PUT", path: "/update" },
14
+ },
15
+ mcpDispatch: "create",
16
+ });
@@ -0,0 +1,97 @@
1
+ // Shape every built-in plugin's `meta.ts` exports as `META`.
2
+ // Host aggregators (src/config/*, server/workspace/paths.ts) iterate
3
+ // over `BUILT_IN_PLUGIN_METAS` and auto-merge the per-dimension
4
+ // records. Plugin-specific literals never appear in host code — the
5
+ // plugin owns what the plugin owns.
6
+ //
7
+ // Browser-safe: no Vue / no Node-only imports. Both server and
8
+ // frontend can import this file (and via it, every plugin's META).
9
+
10
+ /** HTTP methods the route registry recognises. */
11
+ export type RouteMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
12
+
13
+ /** A single route the plugin owns. The host composes the full URL
14
+ * as `/api/<apiNamespace><path>`; consumers read both `method` and
15
+ * the resolved `url` from the host registry. */
16
+ export interface RouteSpec {
17
+ readonly method: RouteMethod;
18
+ /** Sub-path under the namespace (`""` = the namespace root,
19
+ * `"/:id"` = a parameterised child). Express path syntax. */
20
+ readonly path: string;
21
+ }
22
+
23
+ /** Resolved route — what the host registry hands back to consumers
24
+ * (server route registration AND client `apiCall`). The plugin
25
+ * declares `{ method, path }`; the host adds the `/api/<ns>` prefix
26
+ * and exposes `{ method, url }`. */
27
+ export interface ResolvedRoute {
28
+ readonly method: RouteMethod;
29
+ /** Full Express path including `/api/<namespace>` prefix. */
30
+ readonly url: string;
31
+ }
32
+
33
+ /** Type-checking helper for a plugin's `meta.ts` literal. The
34
+ * `const` type parameter narrows nested literals (`toolName:
35
+ * "manageX"`, `apiRoutes.foo.path: "/:id"`, …) so host aggregators
36
+ * see the same string-literal types they would with
37
+ * `as const satisfies PluginMeta` — minus the dual annotation
38
+ * noise. Plugin authors write:
39
+ *
40
+ * ```ts
41
+ * export const META = definePluginMeta({
42
+ * toolName: "manageX",
43
+ * apiNamespace: "x",
44
+ * apiRoutes: {
45
+ * dispatch: { method: "POST", path: "" },
46
+ * },
47
+ * });
48
+ * ``` */
49
+ export function definePluginMeta<const T extends PluginMeta>(meta: T): T {
50
+ return meta;
51
+ }
52
+
53
+ /** A plugin's central-registry-facing metadata. */
54
+ export interface PluginMeta {
55
+ /** MCP tool name string the LLM and JSONL files use. */
56
+ readonly toolName: string;
57
+ /** URL prefix segment under `/api/`. The plugin owns this slug;
58
+ * the host owns the `/api/` prefix. Defaults to `toolName` if
59
+ * omitted but most plugins prefer a shorter slug (e.g.
60
+ * `"accounting"` rather than `"manageAccounting"`).
61
+ *
62
+ * Also doubles as the outer key under `API_ROUTES` — so
63
+ * `API_ROUTES.<apiNamespace>.<routeKey>` is `{ method, url }`. */
64
+ readonly apiNamespace?: string;
65
+ /** Routes owned by this plugin. Each value declares an HTTP
66
+ * method and a sub-path; the host composes the full URL as
67
+ * `/api/<apiNamespace><path>`. */
68
+ readonly apiRoutes?: Readonly<Record<string, RouteSpec>>;
69
+ /** MCP-bridge dispatch route key. Names the entry in `apiRoutes`
70
+ * the MCP server posts tool calls to. Lets `BUILT_IN_SERVER_BINDINGS`
71
+ * derive the binding's URL from META instead of repeating it. */
72
+ readonly mcpDispatch?: string;
73
+ /** Workspace-relative directories owned by this plugin (flat
74
+ * keys). Merged into the central `WORKSPACE_DIRS` so existing
75
+ * call sites (`WORKSPACE_DIRS.accounting`) keep working. */
76
+ readonly workspaceDirs?: Readonly<Record<string, string>>;
77
+ /** Static pubsub channel names owned by this plugin (flat keys).
78
+ * Merged into the central `PUBSUB_CHANNELS`. Channel factories
79
+ * (e.g. `bookChannel(bookId)`) are not part of this map — they
80
+ * live as separate named exports in the plugin's `meta.ts`
81
+ * because their signatures are plugin-specific. */
82
+ readonly staticChannels?: Readonly<Record<string, string>>;
83
+ }
84
+
85
+ /** Substitute `:param` placeholders in a route URL with caller-
86
+ * provided values. URL-encodes each value so `/foo bar` and
87
+ * `?injection=1` survive intact. */
88
+ export function buildRouteUrl(route: ResolvedRoute, params?: Readonly<Record<string, string | number>>): string {
89
+ if (!params) return route.url;
90
+ let { url } = route;
91
+ for (const [key, value] of Object.entries(params)) {
92
+ const placeholder = `:${key}`;
93
+ if (!url.includes(placeholder)) continue;
94
+ url = url.split(placeholder).join(encodeURIComponent(String(value)));
95
+ }
96
+ return url;
97
+ }
@@ -0,0 +1,224 @@
1
+ // Vue-free barrel of every built-in plugin's `META` (the central-
2
+ // registry-facing metadata). Host aggregators (src/config/*,
3
+ // server/workspace/paths.ts) iterate over `BUILT_IN_PLUGIN_METAS`
4
+ // and auto-merge per-dimension records — they never hold
5
+ // plugin-specific literals.
6
+ //
7
+ // Why a separate barrel from `src/plugins/index.ts`?
8
+ // `src/plugins/index.ts` exports `BUILT_IN_PLUGINS` (plugin
9
+ // REGISTRATIONS, including Vue View / Preview components). Server
10
+ // code can't import Vue, so this file gives server-side aggregators
11
+ // a Vue-free entry point that still co-locates the per-plugin
12
+ // metadata.
13
+ //
14
+ // **Auto-generated**. The list comes from
15
+ // `_generated/metas.ts` which is rewritten by
16
+ // `scripts/codegen-plugin-barrels.ts` (run via `predev` / `prebuild`
17
+ // hooks). Adding a plugin means dropping a `meta.ts` into the new
18
+ // `src/plugins/<name>/` directory; this file does not need editing.
19
+
20
+ import type { PluginMeta } from "./meta-types";
21
+ import { GENERATED_PLUGIN_METAS } from "./_generated/metas";
22
+
23
+ export const BUILT_IN_PLUGIN_METAS = GENERATED_PLUGIN_METAS;
24
+ export type BuiltInPluginMetas = typeof BUILT_IN_PLUGIN_METAS;
25
+
26
+ // ────────────────────────────────────────────────────────────────
27
+ // Collision detection
28
+ // ────────────────────────────────────────────────────────────────
29
+ //
30
+ // Aggregators spread plugin-owned records into host records. Without
31
+ // a guard, a plugin claiming a host-reserved key (`apiNamespace:
32
+ // "agent"`) or two plugins claiming the same key (both with
33
+ // `workspaceDirs.images`) would silently win the merge and route
34
+ // real traffic to the wrong handler.
35
+ //
36
+ // We don't `throw` at module load — that would brick the whole app
37
+ // for a single buggy plugin (especially relevant once user-installed
38
+ // runtime plugins land). Instead the helpers below are pure,
39
+ // returning collision lists; callers decide the policy:
40
+ //
41
+ // - Built-in aggregators FILTER colliding plugin keys before merge
42
+ // (host wins / first-registered plugin wins).
43
+ // - `server/plugins/diagnostics.ts` collects the lists at boot,
44
+ // surfaces them via `log.warn` + a system notification, and
45
+ // persists them so a UI mounting later can still display them.
46
+
47
+ /** A plugin key colliding with a host-owned key in one aggregator. */
48
+ export interface HostPluginCollision {
49
+ /** Aggregator label (`"API_ROUTES"`, `"WORKSPACE_DIRS"`, …). */
50
+ label: string;
51
+ /** The key claimed by both host and plugin. */
52
+ key: string;
53
+ /** `toolName` of the plugin claiming it. Empty for legacy callers
54
+ * that don't pass per-key plugin attribution. */
55
+ plugin: string;
56
+ }
57
+
58
+ /** Two plugins claiming the same key in the same dimension. */
59
+ export interface IntraPluginCollision {
60
+ /** Which dimension the duplicate appears in. */
61
+ dimension: "toolName" | "apiNamespace" | "workspaceDirs" | "staticChannels";
62
+ /** The duplicated key. */
63
+ key: string;
64
+ /** `toolName`s of the two plugins claiming it (first-registered, second). */
65
+ plugins: [string, string];
66
+ }
67
+
68
+ /** Pure check — does any plugin key shadow a host key? Returns the
69
+ * list of colliding keys (empty when clean). Aggregators call this
70
+ * to decide which plugin keys to drop during the merge. */
71
+ export function findHostPluginCollisions(hostRecord: Readonly<Record<string, unknown>>, pluginRecord: Readonly<Record<string, unknown>>): readonly string[] {
72
+ const hostKeys = new Set(Object.keys(hostRecord));
73
+ return Object.keys(pluginRecord).filter((key) => hostKeys.has(key));
74
+ }
75
+
76
+ /** Build an attributed collision list — one entry per (key, plugin)
77
+ * pair, where `pluginAttribution[key]` names the plugin claiming
78
+ * that key. Used by aggregators that aggregate ACROSS plugins
79
+ * (workspaceDirs, staticChannels) where each key may come from a
80
+ * different plugin. */
81
+ export function attributeHostPluginCollisions(
82
+ label: string,
83
+ hostRecord: Readonly<Record<string, unknown>>,
84
+ pluginRecord: Readonly<Record<string, unknown>>,
85
+ pluginByKey: Readonly<Record<string, string>>,
86
+ ): HostPluginCollision[] {
87
+ return findHostPluginCollisions(hostRecord, pluginRecord).map((key) => ({ label, key, plugin: pluginByKey[key] ?? "" }));
88
+ }
89
+
90
+ /** Build a first-write-wins aggregate of a per-plugin record across
91
+ * all plugins. Duplicate keys (= "intra-plugin collision") are
92
+ * reported in the returned `collisions` list with the first-claiming
93
+ * plugin AND the offender; the offender's value is dropped — runtime
94
+ * routes to the first plugin's handler.
95
+ *
96
+ * This is the fix for Codex review iter-3+: previously each
97
+ * aggregator used `Object.fromEntries` / `Object.assign` to merge,
98
+ * which is JS-level last-write-wins. The diagnostic ran AFTER the
99
+ * merge and could only describe what was already lost, with the
100
+ * warning text contradicting actual behavior ("second is ignored"
101
+ * vs runtime's "second wins"). With this builder the merge IS the
102
+ * detection point — first-write semantics are enforced. */
103
+ export function buildPluginAggregate<V>(
104
+ metas: readonly PluginMeta[],
105
+ extract: (meta: PluginMeta) => Readonly<Record<string, V>> | undefined,
106
+ dimension: IntraPluginCollision["dimension"],
107
+ ): { aggregate: Record<string, V>; owner: Record<string, string>; collisions: IntraPluginCollision[] } {
108
+ const aggregate: Record<string, V> = {};
109
+ const owner: Record<string, string> = {};
110
+ const collisions: IntraPluginCollision[] = [];
111
+ for (const meta of metas) {
112
+ const record = extract(meta);
113
+ if (!record) continue;
114
+ for (const [key, value] of Object.entries(record)) {
115
+ const priorPlugin = owner[key];
116
+ if (priorPlugin !== undefined) {
117
+ // Two distinct plugins claim the same key. Keep the first
118
+ // entry; report the offender so diagnostics can warn.
119
+ collisions.push({ dimension, key, plugins: [priorPlugin, meta.toolName] });
120
+ continue;
121
+ }
122
+ aggregate[key] = value;
123
+ owner[key] = meta.toolName;
124
+ }
125
+ }
126
+ return { aggregate, owner, collisions };
127
+ }
128
+
129
+ /** Filter a plugin record so only the keys that survive the merge
130
+ * policy remain: keys not claimed by the host. Returns the cleaned
131
+ * record AND the list of (label, key, plugin) drops so diagnostics
132
+ * can report them.
133
+ *
134
+ * Intra-plugin collisions are filtered EARLIER by
135
+ * `buildPluginAggregate`; by the time this function runs the input
136
+ * is already first-write-wins-clean. */
137
+ export function filterPluginKeys<V>(
138
+ label: string,
139
+ hostKeys: ReadonlySet<string>,
140
+ pluginRecord: Readonly<Record<string, V>>,
141
+ pluginByKey: Readonly<Record<string, string>>,
142
+ ): { cleaned: Record<string, V>; dropped: HostPluginCollision[] } {
143
+ const cleaned: Record<string, V> = {};
144
+ const dropped: HostPluginCollision[] = [];
145
+ for (const [key, value] of Object.entries(pluginRecord)) {
146
+ if (hostKeys.has(key)) {
147
+ dropped.push({ label, key, plugin: pluginByKey[key] ?? "" });
148
+ continue;
149
+ }
150
+ cleaned[key] = value;
151
+ }
152
+ return { cleaned, dropped };
153
+ }
154
+
155
+ // ────────────────────────────────────────────────────────────────
156
+ // Host aggregate facade
157
+ // ────────────────────────────────────────────────────────────────
158
+ //
159
+ // `buildPluginAggregate` + `filterPluginKeys` always come paired:
160
+ // gather plugin contributions, drop the ones that collide with host
161
+ // keys, spread into the host record. Each of the four aggregator
162
+ // files (toolNames, apiRoutes, pubsubChannels, paths.ts) was repeating
163
+ // the same ~25-line shape. `defineHostAggregate` collapses the
164
+ // runtime half to one call so a new dimension (e.g. featureFlags) is
165
+ // one line at the call site, not five copy-pastes.
166
+ //
167
+ // The TYPE-LEVEL mapped shape stays at the call site — each
168
+ // aggregator's literal-preserving type (`PluginWorkspaceDirsMap`,
169
+ // `PluginApiRoutesMap`, …) is dimension-specific and would lose
170
+ // fidelity if collapsed into a generic helper. The runtime helper
171
+ // returns `Record<string, V>` and the call site narrows it.
172
+
173
+ export interface HostAggregateOptions<V> {
174
+ /** Aggregator label used in `HostPluginCollision.label` (e.g.
175
+ * `"TOOL_NAMES"`, `"API_ROUTES"`). */
176
+ readonly label: string;
177
+ /** Host-owned record. Plugin keys that collide with these are
178
+ * dropped from the merge (host wins) and reported in
179
+ * `hostCollisions`. */
180
+ readonly hostRecord: Readonly<Record<string, V>>;
181
+ /** Per-plugin extractor — returns the contribution this plugin
182
+ * makes to this dimension, or `undefined` to skip. */
183
+ readonly extract: (meta: PluginMeta) => Readonly<Record<string, V>> | undefined;
184
+ /** Dimension label tagged onto every emitted
185
+ * `IntraPluginCollision`. */
186
+ readonly dimension: IntraPluginCollision["dimension"];
187
+ /** Extra reserved keys to drop on collision, ON TOP OF
188
+ * `Object.keys(hostRecord)`. Used when a sibling map shares a
189
+ * namespace with this aggregate — e.g. `WORKSPACE_DIRS` reserves
190
+ * `WORKSPACE_FILES` keys so a plugin can't smuggle in
191
+ * `workspaceDirs: { memory: ... }` and silently disagree with
192
+ * `WORKSPACE_FILES.memory` (CR review #1125). The reserved keys
193
+ * are NOT added to `merged` — only used for collision filtering. */
194
+ readonly additionalReservedKeys?: ReadonlySet<string>;
195
+ }
196
+
197
+ export interface HostAggregate<V> {
198
+ /** Merged record: host fields plus every plugin contribution that
199
+ * survived collision filtering. */
200
+ readonly merged: Record<string, V>;
201
+ /** Plugin keys dropped because a host record claimed the same
202
+ * key. */
203
+ readonly hostCollisions: readonly HostPluginCollision[];
204
+ /** Two plugins claiming the same key — the second is dropped
205
+ * (first-write-wins) and reported here. */
206
+ readonly intraCollisions: readonly IntraPluginCollision[];
207
+ }
208
+
209
+ /** Run the standard "build per-plugin aggregate, drop host
210
+ * collisions, spread into host record" pipeline in one call. The
211
+ * caller still owns the literal-preserving type cast on `merged`. */
212
+ export function defineHostAggregate<V>(metas: readonly PluginMeta[], opts: HostAggregateOptions<V>): HostAggregate<V> {
213
+ const { aggregate, owner, collisions } = buildPluginAggregate(metas, opts.extract, opts.dimension);
214
+ const reserved = new Set<string>(Object.keys(opts.hostRecord));
215
+ if (opts.additionalReservedKeys) {
216
+ for (const key of opts.additionalReservedKeys) reserved.add(key);
217
+ }
218
+ const { cleaned, dropped } = filterPluginKeys(opts.label, reserved, aggregate, owner);
219
+ return {
220
+ merged: { ...opts.hostRecord, ...cleaned },
221
+ hostCollisions: dropped,
222
+ intraCollisions: collisions,
223
+ };
224
+ }
@@ -1,7 +1,6 @@
1
1
  <template>
2
2
  <div class="w-full h-full flex flex-col items-center justify-center p-4 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg border-2 border-gray-200">
3
3
  <div class="text-center">
4
- <!-- Form Icon -->
5
4
  <svg class="w-12 h-12 mx-auto mb-3 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
6
5
  <path
7
6
  stroke-linecap="round"
@@ -11,15 +10,12 @@
11
10
  />
12
11
  </svg>
13
12
 
14
- <!-- Form Title or Default -->
15
13
  <h3 class="text-gray-900 font-bold text-lg mb-1 line-clamp-2">
16
14
  {{ formData?.title || t("pluginPresentForm.fallbackTitle") }}
17
15
  </h3>
18
16
 
19
- <!-- Field Count -->
20
17
  <p class="text-gray-600 text-sm mb-2">{{ t("pluginPresentForm.fieldCount", { count: fieldCount }, fieldCount) }}</p>
21
18
 
22
- <!-- Completion Status -->
23
19
  <div v-if="!isSubmitted" class="flex items-center justify-center gap-2">
24
20
  <div class="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
25
21
  <div class="h-full bg-blue-600 transition-all duration-300" :style="{ width: `${completionPercentage}%` }" />
@@ -27,7 +23,6 @@
27
23
  <span class="text-xs text-gray-500">{{ `${completionPercentage}%` }}</span>
28
24
  </div>
29
25
 
30
- <!-- Submitted Badge -->
31
26
  <div v-else class="inline-flex items-center gap-1 px-3 py-1 bg-green-100 text-green-700 rounded-full text-xs font-semibold">
32
27
  <svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
33
28
  <path
@@ -62,22 +57,16 @@ const props = defineProps<{
62
57
 
63
58
  const formData = computed<FormData | null>(() => {
64
59
  if (props.result?.toolName === "presentForm") {
65
- return props.result.jsonData as FormData;
60
+ return (props.result.data ?? props.result.jsonData) as FormData;
66
61
  }
67
62
  return null;
68
63
  });
69
64
 
70
- const viewState = computed<FormViewState | null>(() => {
71
- return (props.result?.viewState as unknown as FormViewState) || null;
72
- });
65
+ const viewState = computed<FormViewState | null>(() => (props.result?.viewState as unknown as FormViewState) || null);
73
66
 
74
- const fieldCount = computed(() => {
75
- return formData.value?.fields.length || 0;
76
- });
67
+ const fieldCount = computed(() => formData.value?.fields.length || 0);
77
68
 
78
- const isSubmitted = computed(() => {
79
- return viewState.value?.submitted || false;
80
- });
69
+ const isSubmitted = computed(() => viewState.value?.submitted || false);
81
70
 
82
71
  const completionPercentage = computed(() => {
83
72
  if (!formData.value || isSubmitted.value) return 100;