mulmoclaude 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (486) hide show
  1. package/Dockerfile.sandbox +100 -0
  2. package/README.md +17 -4
  3. package/bin/mulmoclaude.js +46 -15
  4. package/bin/prepare-dist.js +18 -2
  5. package/client/assets/chunk-CernVdwh.js +1 -0
  6. package/client/assets/chunk-D8eiyYIV-C1eAZMzz.js +1 -0
  7. package/client/assets/html2canvas-CDGcmOD3-BbPeutDg.js +5 -0
  8. package/client/assets/index-BbgSjFQ8.js +4968 -0
  9. package/client/assets/index-ECD0lgIv.css +2 -0
  10. package/client/assets/{index.es-D4YyL_Dg-BgT6a3Nd.js → index.es-DqtpmBm8-DJdTPdnc.js} +5 -5
  11. package/client/assets/material-symbols-outlined-BLDfUw-_.woff2 +0 -0
  12. package/client/assets/runtime-protocol-vue-6WYa8hAs.js +1 -0
  13. package/client/assets/runtime-vue-BVUzgYGA.js +1 -0
  14. package/client/assets/typeof-DBp4T-Ny-C2xoZtcz.js +1 -0
  15. package/client/assets/vue-1e_vz2LW.js +1 -0
  16. package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +4 -0
  17. package/client/index.html +33 -2
  18. package/package.json +20 -18
  19. package/sandbox-entrypoint.sh +106 -0
  20. package/server/accounting/accountNormalize.ts +32 -0
  21. package/server/accounting/defaultAccounts.ts +87 -0
  22. package/server/accounting/eventPublisher.ts +51 -0
  23. package/server/accounting/journal.ts +252 -0
  24. package/server/accounting/openingBalances.ts +114 -0
  25. package/server/accounting/report.ts +237 -0
  26. package/server/accounting/service.ts +718 -0
  27. package/server/accounting/snapshotCache.ts +333 -0
  28. package/server/accounting/timeSeries.ts +265 -0
  29. package/server/accounting/types.ts +148 -0
  30. package/server/agent/activeTools.ts +128 -0
  31. package/server/agent/attachmentConverter.ts +10 -5
  32. package/server/agent/backend/claude-code.ts +8 -2
  33. package/server/agent/backend/types.ts +1 -1
  34. package/server/agent/config.ts +101 -31
  35. package/server/agent/index.ts +45 -33
  36. package/server/agent/mcp-server.ts +146 -69
  37. package/server/agent/mcp-tools/index.ts +1 -5
  38. package/server/agent/mcp-tools/notify.ts +2 -22
  39. package/server/agent/mcp-tools/x.ts +0 -4
  40. package/server/agent/mcpHealth.ts +168 -0
  41. package/server/agent/plugin-names.ts +20 -77
  42. package/server/agent/prompt.ts +259 -51
  43. package/server/agent/resumeFailover.ts +1 -1
  44. package/server/agent/stream.ts +0 -1
  45. package/server/api/auth/bearerAuth.ts +5 -5
  46. package/server/api/csrfGuard.ts +1 -1
  47. package/server/api/routes/accounting.ts +366 -0
  48. package/server/api/routes/agent.ts +509 -46
  49. package/server/api/routes/attachment.ts +104 -0
  50. package/server/api/routes/chart.ts +2 -1
  51. package/server/api/routes/config.ts +12 -12
  52. package/server/api/routes/files.ts +105 -48
  53. package/server/api/routes/image.ts +70 -25
  54. package/server/api/routes/journal.ts +35 -0
  55. package/server/api/routes/mulmo-script.ts +358 -118
  56. package/server/api/routes/mulmoScriptValidate.ts +1 -1
  57. package/server/api/routes/news.ts +1 -1
  58. package/server/api/routes/notifications.ts +92 -22
  59. package/server/api/routes/notifier.ts +98 -0
  60. package/server/api/routes/pdf.ts +188 -48
  61. package/server/api/routes/plugins.ts +34 -14
  62. package/server/api/routes/presentHtml.ts +58 -3
  63. package/server/api/routes/roles.ts +1 -8
  64. package/server/api/routes/runtime-plugin.ts +224 -0
  65. package/server/api/routes/scheduler.ts +7 -5
  66. package/server/api/routes/schedulerHandlers.ts +1 -1
  67. package/server/api/routes/schedulerTasks.ts +8 -7
  68. package/server/api/routes/sessions.ts +234 -121
  69. package/server/api/routes/skills.ts +56 -51
  70. package/server/api/routes/sources.ts +52 -45
  71. package/server/api/routes/translation.ts +44 -0
  72. package/server/api/routes/wiki/frontmatter.ts +13 -65
  73. package/server/api/routes/wiki/history.ts +261 -0
  74. package/server/api/routes/wiki/pageIndex.ts +1 -1
  75. package/server/api/routes/wiki.ts +50 -26
  76. package/server/events/file-change.ts +83 -0
  77. package/server/events/notifications.ts +247 -91
  78. package/server/events/pub-sub/index.ts +1 -1
  79. package/server/events/relay-client.ts +5 -5
  80. package/server/events/scheduler-adapter.ts +2 -2
  81. package/server/events/session-store/index.ts +110 -22
  82. package/server/events/task-manager/index.ts +10 -9
  83. package/server/index.ts +509 -33
  84. package/server/notifier/engine.ts +419 -0
  85. package/server/notifier/legacy-adapters.ts +76 -0
  86. package/server/notifier/runtime-api.ts +74 -0
  87. package/server/notifier/store.ts +70 -0
  88. package/server/notifier/types.ts +121 -0
  89. package/server/plugins/dev-loader.ts +171 -0
  90. package/server/plugins/dev-watcher.ts +150 -0
  91. package/server/plugins/diagnostics.ts +188 -0
  92. package/server/plugins/preset-list.ts +52 -0
  93. package/server/plugins/preset-loader.ts +112 -0
  94. package/server/plugins/runtime-chat-api.ts +38 -0
  95. package/server/plugins/runtime-loader.ts +430 -0
  96. package/server/plugins/runtime-registry.ts +112 -0
  97. package/server/plugins/runtime-tasks-api.ts +50 -0
  98. package/server/plugins/runtime.ts +378 -0
  99. package/server/services/translation/cache.ts +72 -0
  100. package/server/services/translation/index.ts +106 -0
  101. package/server/services/translation/llm.ts +140 -0
  102. package/server/services/translation/types.ts +35 -0
  103. package/server/system/credentials.ts +13 -2
  104. package/server/system/env.ts +6 -1
  105. package/server/system/logger/formatters.ts +46 -4
  106. package/server/system/logger/index.ts +4 -4
  107. package/server/system/logger/sinks.ts +26 -5
  108. package/server/system/logger/types.ts +2 -2
  109. package/server/utils/dev-plugin-args.d.mts +11 -0
  110. package/server/utils/dev-plugin-args.mjs +43 -0
  111. package/server/utils/errors.ts +13 -4
  112. package/server/utils/files/accounting-io.ts +295 -0
  113. package/server/utils/files/atomic.ts +17 -49
  114. package/server/utils/files/attachment-store.ts +182 -0
  115. package/server/utils/files/html-io.ts +1 -7
  116. package/server/utils/files/html-store.ts +19 -0
  117. package/server/utils/files/image-store.ts +20 -22
  118. package/server/utils/files/index.ts +5 -15
  119. package/server/utils/files/journal-io.ts +7 -35
  120. package/server/utils/files/json.ts +2 -29
  121. package/server/utils/files/markdown-image-fill.ts +6 -37
  122. package/server/utils/files/markdown-store.ts +6 -21
  123. package/server/utils/files/naming.ts +3 -39
  124. package/server/utils/files/plugins-io.ts +100 -0
  125. package/server/utils/files/reference-dirs-io.ts +1 -9
  126. package/server/utils/files/roles-io.ts +2 -10
  127. package/server/utils/files/safe.ts +17 -19
  128. package/server/utils/files/scheduler-io.ts +1 -7
  129. package/server/utils/files/scheduler-overrides-io.ts +3 -12
  130. package/server/utils/files/session-io.ts +21 -30
  131. package/server/utils/files/spreadsheet-store.ts +9 -22
  132. package/server/utils/files/translation-io.ts +46 -0
  133. package/server/utils/files/user-tasks-io.ts +1 -7
  134. package/server/utils/files/workspace-io.ts +3 -79
  135. package/server/utils/gemini.ts +33 -11
  136. package/server/utils/html/htmlArtifactSplicer.ts +41 -0
  137. package/server/utils/markdown/frontmatter.ts +112 -0
  138. package/server/utils/regex.ts +56 -0
  139. package/server/utils/router.ts +41 -0
  140. package/server/utils/slug.ts +5 -3
  141. package/server/utils/time.ts +12 -0
  142. package/server/workspace/chat-index/indexer.ts +15 -2
  143. package/server/workspace/chat-index/summarizer.ts +1 -1
  144. package/server/workspace/custom-dirs.ts +1 -1
  145. package/server/workspace/helps/gemini.md +1 -1
  146. package/server/workspace/helps/guide.md +61 -0
  147. package/server/workspace/helps/index.md +4 -0
  148. package/server/workspace/helps/presenthtml.md +80 -0
  149. package/server/workspace/helps/sandbox.md +7 -0
  150. package/server/workspace/helps/storyteller.md +101 -0
  151. package/server/workspace/helps/telegram.md +1 -0
  152. package/server/workspace/helps/wiki.md +9 -7
  153. package/server/workspace/journal/archivist-cli.ts +7 -33
  154. package/server/workspace/journal/archivist-schemas.ts +5 -43
  155. package/server/workspace/journal/dailyPass.ts +34 -187
  156. package/server/workspace/journal/diff.ts +3 -28
  157. package/server/workspace/journal/index.ts +10 -81
  158. package/server/workspace/journal/indexFile.ts +3 -24
  159. package/server/workspace/journal/latestDaily.ts +51 -0
  160. package/server/workspace/journal/memoryExtractor.ts +4 -20
  161. package/server/workspace/journal/optimizationPass.ts +4 -21
  162. package/server/workspace/journal/paths.ts +4 -23
  163. package/server/workspace/journal/state.ts +6 -29
  164. package/server/workspace/memory/io.ts +213 -0
  165. package/server/workspace/memory/llm-classifier.ts +158 -0
  166. package/server/workspace/memory/migrate.ts +263 -0
  167. package/server/workspace/memory/run.ts +84 -0
  168. package/server/workspace/memory/topic-cluster.ts +218 -0
  169. package/server/workspace/memory/topic-detect.ts +67 -0
  170. package/server/workspace/memory/topic-index-hook.ts +128 -0
  171. package/server/workspace/memory/topic-io.ts +180 -0
  172. package/server/workspace/memory/topic-migrate.ts +248 -0
  173. package/server/workspace/memory/topic-run.ts +172 -0
  174. package/server/workspace/memory/topic-swap.ts +135 -0
  175. package/server/workspace/memory/topic-types.ts +142 -0
  176. package/server/workspace/memory/types.ts +83 -0
  177. package/server/workspace/news/reader.ts +4 -5
  178. package/server/workspace/paths.ts +124 -47
  179. package/server/workspace/roles.ts +2 -11
  180. package/server/workspace/skills/parser.ts +38 -55
  181. package/server/workspace/skills/user-tasks.ts +1 -2
  182. package/server/workspace/skills-preset/mc-library/SKILL.md +188 -0
  183. package/server/workspace/skills-preset.ts +196 -0
  184. package/server/workspace/sources/fetchers/githubIssues.ts +13 -11
  185. package/server/workspace/sources/fetchers/index.ts +1 -1
  186. package/server/workspace/sources/fetchers/rssParser.ts +1 -1
  187. package/server/workspace/sources/pipeline/index.ts +2 -2
  188. package/server/workspace/sources/pipeline/notify.ts +3 -3
  189. package/server/workspace/sources/pipeline/write.ts +2 -2
  190. package/server/workspace/sources/registry.ts +39 -61
  191. package/server/workspace/sources/robots.ts +1 -1
  192. package/server/workspace/tool-trace/classify.ts +2 -1
  193. package/server/workspace/tool-trace/index.ts +1 -1
  194. package/server/workspace/tool-trace/writeSearch.ts +6 -1
  195. package/server/workspace/wiki-backlinks/index.ts +19 -7
  196. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +1 -0
  197. package/server/workspace/wiki-history/hook/snapshot.mjs +98 -0
  198. package/server/workspace/wiki-history/hook/snapshot.ts +135 -0
  199. package/server/workspace/wiki-history/provision.ts +181 -0
  200. package/server/workspace/wiki-pages/io.ts +217 -0
  201. package/server/workspace/wiki-pages/snapshot.ts +380 -0
  202. package/server/workspace/workspace.ts +75 -13
  203. package/src/App.vue +115 -40
  204. package/src/_runtime/protocol-vue.ts +21 -0
  205. package/src/_runtime/vue.ts +22 -0
  206. package/src/components/ChatInput.vue +14 -10
  207. package/src/components/CopyChatButton.vue +76 -0
  208. package/src/components/FileContentRenderer.vue +67 -14
  209. package/src/components/FileTree.vue +2 -2
  210. package/src/components/FilesView.vue +17 -1
  211. package/src/components/NewsView.vue +16 -2
  212. package/src/components/NotificationBell.vue +320 -93
  213. package/src/components/PageChatComposer.vue +5 -4
  214. package/src/components/PluginLauncher.vue +42 -6
  215. package/src/components/PluginScopedRoot.vue +87 -0
  216. package/src/components/RoleSelector.vue +12 -1
  217. package/src/components/RolesView.vue +562 -0
  218. package/src/components/SentAttachmentChip.vue +102 -0
  219. package/src/components/SessionHistoryPanel.vue +109 -20
  220. package/src/components/SessionRoleIcon.vue +7 -4
  221. package/src/components/SessionSidebar.vue +20 -7
  222. package/src/components/SessionTabBar.vue +1 -1
  223. package/src/components/SettingsMcpTab.vue +4 -4
  224. package/src/components/SettingsModal.vue +2 -0
  225. package/src/components/SidebarHeader.vue +16 -5
  226. package/src/components/SourcesManager.vue +23 -9
  227. package/src/components/SourcesView.vue +1 -1
  228. package/src/components/StackView.vue +102 -6
  229. package/src/components/SuggestionsPanel.vue +105 -16
  230. package/src/components/SystemFileBanner.vue +1 -1
  231. package/src/components/TodoExplorer.vue +4 -5
  232. package/src/components/todo/TodoAddDialog.vue +2 -3
  233. package/src/components/todo/TodoEditDialog.vue +1 -2
  234. package/src/components/todo/TodoEditPanel.vue +2 -3
  235. package/src/components/todo/TodoKanbanView.vue +8 -5
  236. package/src/components/todo/TodoListView.vue +3 -5
  237. package/src/components/todo/TodoTableView.vue +7 -5
  238. package/src/composables/useAccountingChannel.ts +58 -0
  239. package/src/composables/useActiveSession.ts +4 -25
  240. package/src/composables/useAppApi.ts +6 -44
  241. package/src/composables/useClipboardCopy.ts +3 -20
  242. package/src/composables/useContentDisplay.ts +33 -2
  243. package/src/composables/useDevPluginReload.ts +23 -0
  244. package/src/composables/useDynamicFavicon.ts +5 -31
  245. package/src/composables/useEventListeners.ts +0 -20
  246. package/src/composables/useExpandedDirs.ts +4 -15
  247. package/src/composables/useFaviconState.ts +12 -46
  248. package/src/composables/useFileChange.ts +53 -0
  249. package/src/composables/useFreshPluginData.ts +6 -43
  250. package/src/composables/useHealth.ts +14 -43
  251. package/src/composables/useImageErrorRepair.ts +104 -0
  252. package/src/composables/useLatestDaily.ts +40 -0
  253. package/src/composables/useMarkdownDoc.ts +39 -0
  254. package/src/composables/useMarkdownLinkHandler.ts +1 -1
  255. package/src/composables/useMcpTools.ts +3 -16
  256. package/src/composables/useNotifications.ts +138 -112
  257. package/src/composables/usePdfDownload.ts +17 -3
  258. package/src/composables/usePendingCalls.ts +8 -26
  259. package/src/composables/usePluginErrorBoundary.ts +68 -0
  260. package/src/composables/usePubSub.ts +9 -17
  261. package/src/composables/useRunElapsed.ts +5 -22
  262. package/src/composables/useSandboxStatus.ts +4 -20
  263. package/src/composables/useSessionDerived.ts +7 -15
  264. package/src/composables/useSessionHistory.ts +70 -29
  265. package/src/composables/useSessionSync.ts +25 -3
  266. package/src/composables/useSkillsList.ts +59 -0
  267. package/src/composables/useTranslatedQueries.ts +109 -0
  268. package/src/config/apiRoutes.ts +181 -80
  269. package/src/config/historyFilters.ts +5 -3
  270. package/src/config/hostEvents.ts +17 -0
  271. package/src/config/mcpCatalog.ts +277 -5
  272. package/src/config/pubsubChannels.ts +134 -12
  273. package/src/config/roles.ts +212 -147
  274. package/src/config/systemFileDescriptors.ts +5 -5
  275. package/src/config/toolNames.ts +52 -30
  276. package/src/config/workspacePaths.ts +26 -2
  277. package/src/lang/de.ts +483 -27
  278. package/src/lang/en.ts +448 -27
  279. package/src/lang/es.ts +474 -27
  280. package/src/lang/fr.ts +476 -27
  281. package/src/lang/ja.ts +465 -27
  282. package/src/lang/ko.ts +466 -27
  283. package/src/lang/pt-BR.ts +473 -27
  284. package/src/lang/zh.ts +463 -27
  285. package/src/lib/vue-i18n.ts +1 -1
  286. package/src/lib/wiki-page/slug.ts +66 -0
  287. package/src/main.ts +85 -0
  288. package/src/plugins/_extras.ts +58 -0
  289. package/src/plugins/_generated/metas.ts +42 -0
  290. package/src/plugins/_generated/registrations.ts +44 -0
  291. package/src/plugins/_generated/server-bindings.ts +47 -0
  292. package/src/plugins/accounting/Preview.vue +106 -0
  293. package/src/plugins/accounting/View.vue +632 -0
  294. package/src/plugins/accounting/actions.ts +34 -0
  295. package/src/plugins/accounting/api.ts +301 -0
  296. package/src/plugins/accounting/components/AccountEditor.vue +250 -0
  297. package/src/plugins/accounting/components/AccountRow.vue +50 -0
  298. package/src/plugins/accounting/components/AccountsList.vue +102 -0
  299. package/src/plugins/accounting/components/AccountsModal.vue +300 -0
  300. package/src/plugins/accounting/components/BalanceSheet.vue +186 -0
  301. package/src/plugins/accounting/components/BookSettings.vue +284 -0
  302. package/src/plugins/accounting/components/BookSwitcher.vue +78 -0
  303. package/src/plugins/accounting/components/DateRangePicker.vue +140 -0
  304. package/src/plugins/accounting/components/JournalEntryForm.vue +504 -0
  305. package/src/plugins/accounting/components/JournalList.vue +553 -0
  306. package/src/plugins/accounting/components/Ledger.vue +206 -0
  307. package/src/plugins/accounting/components/NewBookForm.vue +211 -0
  308. package/src/plugins/accounting/components/OpeningBalancesForm.vue +271 -0
  309. package/src/plugins/accounting/components/ProfitLoss.vue +160 -0
  310. package/src/plugins/accounting/components/accountDraft.ts +13 -0
  311. package/src/plugins/accounting/components/accountNumbering.ts +103 -0
  312. package/src/plugins/accounting/components/accountValidation.ts +75 -0
  313. package/src/plugins/accounting/components/useLatestRequest.ts +44 -0
  314. package/src/plugins/accounting/countries.ts +158 -0
  315. package/src/plugins/accounting/currencies.ts +64 -0
  316. package/src/plugins/accounting/dates.ts +51 -0
  317. package/src/plugins/accounting/definition.ts +199 -0
  318. package/src/plugins/accounting/fiscalYear.ts +136 -0
  319. package/src/plugins/accounting/index.ts +49 -0
  320. package/src/plugins/accounting/meta.ts +91 -0
  321. package/src/plugins/accounting/timeSeriesEnums.ts +16 -0
  322. package/src/plugins/api.ts +125 -0
  323. package/src/plugins/canvas/View.vue +38 -28
  324. package/src/plugins/canvas/definition.ts +10 -8
  325. package/src/plugins/canvas/index.ts +15 -8
  326. package/src/plugins/canvas/meta.ts +12 -0
  327. package/src/plugins/chart/Preview.vue +1 -1
  328. package/src/plugins/chart/View.vue +2 -2
  329. package/src/plugins/chart/definition.ts +12 -2
  330. package/src/plugins/chart/index.ts +15 -7
  331. package/src/plugins/chart/meta.ts +18 -0
  332. package/src/plugins/editImages/definition.ts +44 -0
  333. package/src/plugins/editImages/index.ts +43 -0
  334. package/src/plugins/editImages/meta.ts +5 -0
  335. package/src/plugins/generateImage/View.vue +3 -1
  336. package/src/plugins/generateImage/definition.ts +2 -0
  337. package/src/plugins/generateImage/index.ts +13 -5
  338. package/src/plugins/generateImage/meta.ts +5 -0
  339. package/src/plugins/index.ts +35 -0
  340. package/src/plugins/manageRoles/Preview.vue +7 -4
  341. package/src/plugins/manageRoles/View.vue +12 -8
  342. package/src/plugins/manageRoles/definition.ts +6 -0
  343. package/src/plugins/manageRoles/index.ts +7 -6
  344. package/src/plugins/manageSkills/View.vue +11 -7
  345. package/src/plugins/manageSkills/definition.ts +4 -1
  346. package/src/plugins/manageSkills/index.ts +14 -7
  347. package/src/plugins/manageSkills/meta.ts +21 -0
  348. package/src/plugins/manageSource/definition.ts +4 -1
  349. package/src/plugins/manageSource/index.ts +15 -7
  350. package/src/plugins/manageSource/meta.ts +21 -0
  351. package/src/plugins/markdown/Preview.vue +10 -8
  352. package/src/plugins/markdown/View.vue +84 -17
  353. package/src/plugins/markdown/definition.ts +7 -1
  354. package/src/plugins/markdown/index.ts +15 -8
  355. package/src/plugins/markdown/meta.ts +16 -0
  356. package/src/plugins/meta-types.ts +97 -0
  357. package/src/plugins/metas.ts +224 -0
  358. package/src/plugins/presentForm/Preview.vue +4 -15
  359. package/src/plugins/presentForm/View.vue +35 -78
  360. package/src/plugins/presentForm/definition.ts +7 -6
  361. package/src/plugins/presentForm/index.ts +12 -5
  362. package/src/plugins/presentForm/meta.ts +11 -0
  363. package/src/plugins/presentForm/plugin.ts +8 -9
  364. package/src/plugins/presentForm/types.ts +0 -24
  365. package/src/plugins/presentHtml/Preview.vue +1 -8
  366. package/src/plugins/presentHtml/View.vue +401 -30
  367. package/src/plugins/presentHtml/definition.ts +8 -5
  368. package/src/plugins/presentHtml/index.ts +15 -8
  369. package/src/plugins/presentHtml/meta.ts +14 -0
  370. package/src/plugins/presentMulmoScript/View.vue +327 -107
  371. package/src/plugins/presentMulmoScript/definition.ts +34 -7
  372. package/src/plugins/presentMulmoScript/helpers.ts +4 -5
  373. package/src/plugins/presentMulmoScript/index.ts +20 -7
  374. package/src/plugins/presentMulmoScript/meta.ts +52 -0
  375. package/src/plugins/scheduler/AutomationsPreview.vue +2 -8
  376. package/src/plugins/scheduler/Preview.vue +5 -2
  377. package/src/plugins/scheduler/TasksTab.vue +16 -36
  378. package/src/plugins/scheduler/View.vue +22 -54
  379. package/src/plugins/scheduler/automationsDefinition.ts +14 -9
  380. package/src/plugins/scheduler/automationsMeta.ts +5 -0
  381. package/src/plugins/scheduler/calendarDefinition.ts +4 -7
  382. package/src/plugins/scheduler/calendarMeta.ts +28 -0
  383. package/src/plugins/scheduler/formatSchedule.ts +6 -24
  384. package/src/plugins/scheduler/index.ts +26 -52
  385. package/src/plugins/scope.ts +57 -0
  386. package/src/plugins/server-bindings-types.ts +38 -0
  387. package/src/plugins/server.ts +32 -0
  388. package/src/plugins/skill/Preview.vue +25 -0
  389. package/src/plugins/skill/View.vue +125 -0
  390. package/src/plugins/skill/definition.ts +23 -0
  391. package/src/plugins/skill/index.ts +36 -0
  392. package/src/plugins/skill/plugin.ts +31 -0
  393. package/src/plugins/skill/types.ts +21 -0
  394. package/src/plugins/spreadsheet/Preview.vue +1 -3
  395. package/src/plugins/spreadsheet/View.vue +29 -49
  396. package/src/plugins/spreadsheet/cellHighlights.ts +2 -3
  397. package/src/plugins/spreadsheet/definition.ts +5 -2
  398. package/src/plugins/spreadsheet/index.ts +15 -8
  399. package/src/plugins/spreadsheet/keyboardNav.ts +38 -0
  400. package/src/plugins/spreadsheet/meta.ts +14 -0
  401. package/src/plugins/textResponse/Preview.vue +9 -1
  402. package/src/plugins/textResponse/View.vue +59 -8
  403. package/src/plugins/textResponse/index.ts +11 -3
  404. package/src/plugins/textResponse/plugin.ts +8 -10
  405. package/src/plugins/textResponse/types.ts +28 -0
  406. package/src/plugins/wiki/Preview.vue +6 -4
  407. package/src/plugins/wiki/View.vue +463 -254
  408. package/src/plugins/wiki/components/WikiPageBody.vue +159 -0
  409. package/src/plugins/wiki/helpers.ts +17 -0
  410. package/src/plugins/wiki/history/HistoryDetail.vue +325 -0
  411. package/src/plugins/wiki/history/HistoryTab.vue +167 -0
  412. package/src/plugins/wiki/history/RestoreConfirm.vue +63 -0
  413. package/src/plugins/wiki/history/api.ts +52 -0
  414. package/src/plugins/wiki/history/diff.ts +145 -0
  415. package/src/plugins/wiki/index.ts +42 -32
  416. package/src/plugins/wiki/meta.ts +10 -0
  417. package/src/plugins/wiki/pageEditLoader.ts +53 -0
  418. package/src/plugins/wiki/route.ts +8 -0
  419. package/src/router/guards.ts +2 -1
  420. package/src/router/index.ts +19 -0
  421. package/src/router/pageRoutes.ts +1 -0
  422. package/src/tools/index.ts +50 -51
  423. package/src/tools/runtimeLoader.ts +141 -0
  424. package/src/tools/types.ts +44 -1
  425. package/src/types/notification.ts +23 -0
  426. package/src/types/pastedFile.ts +10 -0
  427. package/src/types/session.ts +61 -3
  428. package/src/types/sse.ts +21 -6
  429. package/src/utils/agent/eventDispatch.ts +12 -9
  430. package/src/utils/agent/pastedAttachment.ts +35 -0
  431. package/src/utils/agent/request.ts +32 -3
  432. package/src/utils/agent/toolCalls.ts +7 -1
  433. package/src/utils/api.ts +1 -1
  434. package/src/utils/chat/exportMarkdown.ts +243 -0
  435. package/src/utils/errors.ts +10 -2
  436. package/src/utils/files/expandedDirs.ts +1 -1
  437. package/src/utils/filesPreview/todoPreview.ts +13 -2
  438. package/src/utils/format/date.ts +1 -3
  439. package/src/utils/format/jsonSyntax.ts +5 -0
  440. package/src/utils/html/iframeHeightReporterScript.ts +62 -0
  441. package/src/utils/html/previewCsp.ts +29 -2
  442. package/src/utils/image/htmlSrcAttrs.ts +122 -0
  443. package/src/utils/image/imageRepairInlineScript.ts +115 -0
  444. package/src/utils/image/resolve.ts +17 -3
  445. package/src/utils/image/rewriteMarkdownImageRefs.ts +62 -9
  446. package/src/utils/markdown/frontmatter.ts +125 -0
  447. package/src/utils/markdown/taskList.ts +7 -2
  448. package/src/utils/plugin/runtime.ts +132 -0
  449. package/src/utils/session/mergeSessions.ts +40 -37
  450. package/src/utils/session/sessionEntries.ts +74 -18
  451. package/src/utils/session/sessionHelpers.ts +54 -10
  452. package/src/utils/tools/result.ts +76 -14
  453. package/src/vite-env.d.ts +6 -0
  454. package/client/assets/html2canvas-Cx501zZr-Bug0qRNv.js +0 -5
  455. package/client/assets/index-CY-WpQUm.css +0 -2
  456. package/client/assets/index-DbTz2Mfs.js +0 -4911
  457. package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
  458. package/server/api/routes/html.ts +0 -114
  459. package/server/api/routes/todos.ts +0 -293
  460. package/server/api/routes/todosColumnsHandlers.ts +0 -333
  461. package/server/api/routes/todosHandlers.ts +0 -274
  462. package/server/api/routes/todosItemsHandlers.ts +0 -386
  463. package/server/utils/files/todos-io.ts +0 -29
  464. package/src/components/NotificationToast.vue +0 -75
  465. package/src/plugins/editImage/definition.ts +0 -27
  466. package/src/plugins/editImage/index.ts +0 -37
  467. package/src/plugins/presentHtml/helpers.ts +0 -72
  468. package/src/plugins/scheduler/LegacySchedulerView.vue +0 -32
  469. package/src/plugins/scheduler/legacyShape.ts +0 -34
  470. package/src/plugins/todo/Preview.vue +0 -68
  471. package/src/plugins/todo/View.vue +0 -378
  472. package/src/plugins/todo/composables/useTodos.ts +0 -179
  473. package/src/plugins/todo/definition.ts +0 -45
  474. package/src/plugins/todo/index.ts +0 -62
  475. package/src/plugins/todo/labels.ts +0 -163
  476. package/src/plugins/todo/priority.ts +0 -98
  477. package/src/plugins/todo/viewModes.ts +0 -19
  478. package/src/plugins/wiki/definition.ts +0 -25
  479. package/src/tools/legacyPluginNames.ts +0 -13
  480. package/src/utils/format/frontmatter.ts +0 -80
  481. package/src/utils/image/rewriteHtmlImageRefs.ts +0 -50
  482. package/src/utils/notification/dispatch.ts +0 -58
  483. /package/client/assets/{purify.es-Fx1Nqyry-BwJECkqS.js → purify.es-Fx1Nqyry-BSVNht6S.js} +0 -0
  484. /package/src/plugins/{editImage → editImages}/Preview.vue +0 -0
  485. /package/src/plugins/{editImage → editImages}/View.vue +0 -0
  486. /package/src/{config/schedulerActions.ts → plugins/scheduler/actions.ts} +0 -0
@@ -0,0 +1,52 @@
1
+ // Preset plugins shipped with the repo (#1043 C-2 follow-up).
2
+ //
3
+ // Each entry is a published npm package that lives in mulmoclaude's
4
+ // `node_modules`; the boot loader registers it through the same path
5
+ // as a user-installed runtime plugin (workspace ledger), so the
6
+ // frontend dynamic-import + Vue View pipeline runs end-to-end on
7
+ // every fresh checkout — no manual `yarn plugin:install` needed for
8
+ // testing or for first-launch UX.
9
+ //
10
+ // Presets and user-installed plugins share the runtime registry. On
11
+ // tool-name collision the preset wins (loaded first; static MCP
12
+ // built-ins still win over both).
13
+ //
14
+ // Adding a preset:
15
+ // 1. `yarn add <package>` (or extend an existing dep)
16
+ // 2. Append a row below
17
+ // 3. Restart the server
18
+ //
19
+ // Removing a preset:
20
+ // 1. Remove the row
21
+ // 2. Optionally `yarn remove <package>`
22
+ // 3. Restart
23
+
24
+ export interface PresetPlugin {
25
+ /** npm package name (the directory under `node_modules`). */
26
+ packageName: string;
27
+ }
28
+
29
+ export const PRESET_PLUGINS: readonly PresetPlugin[] = [
30
+ // #1145 — runtime-plugin shape of the built-in todo plugin.
31
+ // Loaded as a preset (resolved via `node_modules/@mulmoclaude/todo-plugin/`
32
+ // through the yarn-workspaces symlink) so it boots on every fresh
33
+ // checkout. Owns `manageTodoList` end-to-end now that the static
34
+ // entry under `src/plugins/todo/` has been removed.
35
+ { packageName: "@mulmoclaude/todo-plugin" },
36
+ // #1162 — Spotify integration (Liked Songs / playlists / recently
37
+ // played). PR 1 ships OAuth + token persistence; PR 2 adds the
38
+ // listening-data kinds and the Vue View. Loaded the same way as
39
+ // todo-plugin via the workspace symlink at
40
+ // `node_modules/@mulmoclaude/spotify-plugin/`.
41
+ { packageName: "@mulmoclaude/spotify-plugin" },
42
+ // #1175 / #1169 PR-A — recipe-book runtime plugin. First slice of
43
+ // the Cooking Coach plugin set. Stores recipes as one markdown file
44
+ // per recipe under the plugin's `files.data` scope; demonstrates
45
+ // markdown-per-record storage on the v0.3 runtime API.
46
+ { packageName: "@mulmoclaude/recipe-book-plugin" },
47
+ // Encore plan PR 1 follow-up — dev-only debug playground plugin.
48
+ // Owns the standalone `/debug` page; the toolbar entry is gated on
49
+ // `VITE_DEV_MODE=1` so production builds hide the launcher button
50
+ // (the page itself is still reachable by typing the URL).
51
+ { packageName: "@mulmoclaude/debug-plugin" },
52
+ ];
@@ -0,0 +1,112 @@
1
+ // Preset plugin loader (#1043 C-2 follow-up).
2
+ //
3
+ // Reads `server/plugins/preset-list.ts` at boot and resolves each
4
+ // entry against the package's installed location via Node's standard
5
+ // module-resolution (`import.meta.resolve`). The package is already
6
+ // laid out by `npm install` / `yarn install`; no tgz unpack step.
7
+ // The result is the same `RuntimePlugin` shape user-installed plugins
8
+ // produce, so both flows share the runtime registry, the dispatch
9
+ // route, and the asset route.
10
+ //
11
+ // Why not a hardcoded `node_modules/<pkg>` join: yarn workspaces
12
+ // hoist deps to the repo root, so the launcher sees the package in
13
+ // `<repo>/node_modules/...` while `<repo>/packages/mulmoclaude/node_modules/`
14
+ // is empty. `import.meta.resolve` walks the same way Node does at
15
+ // runtime, so it finds the package wherever the package manager put
16
+ // it. (npm's flat install doesn't have this issue — it lands under
17
+ // `<package>/node_modules/...` directly. Both work with the same
18
+ // resolver call.)
19
+ //
20
+ // Trust model: the package name comes from the server-side hardcoded
21
+ // preset list, not from user input. We trust the resolved path
22
+ // without an `ensureInsideBase` anchor because there is no base —
23
+ // the resolver returns wherever the package manager chose. The asset
24
+ // route's `realpathSync(plugin.cachePath)` still pins to whatever was
25
+ // resolved here at registration time; a route caller cannot redirect
26
+ // to a different path via URL params.
27
+ //
28
+ // Failures don't abort boot. A missing preset (install drift, rare)
29
+ // logs a warning; healthy presets still register.
30
+
31
+ import { existsSync, readFileSync } from "node:fs";
32
+ import path from "node:path";
33
+ import { fileURLToPath } from "node:url";
34
+ import { PRESET_PLUGINS } from "./preset-list.js";
35
+ import { loadPluginFromCacheDir, type LoaderDeps, type RuntimePlugin } from "./runtime-loader.js";
36
+ import { log } from "../system/logger/index.js";
37
+
38
+ const LOG_PREFIX = "plugins/preset";
39
+
40
+ const __filename = fileURLToPath(import.meta.url);
41
+ const __dirname = path.dirname(__filename);
42
+
43
+ interface PackageJsonShape {
44
+ version?: string;
45
+ }
46
+
47
+ /** Resolve the on-disk root of a preset package by walking up from
48
+ * this file looking for `<dir>/node_modules/<pkg>/`. Mirrors Node's
49
+ * CommonJS resolution algorithm so it works in every layout the
50
+ * installer might choose:
51
+ *
52
+ * - yarn workspaces: deps hoisted to the repo root's `node_modules`
53
+ * - npm flat install: package's `node_modules` directly
54
+ * - npm nested install: under a parent package's `node_modules`
55
+ *
56
+ * Why not `import.meta.resolve('<pkg>/package.json')`: many packages
57
+ * (including `@gui-chat-plugin/*`) ship an `exports` field that
58
+ * doesn't expose `./package.json`, so the ESM resolver throws
59
+ * `ERR_PACKAGE_PATH_NOT_EXPORTED`. The walk-up sidesteps the
60
+ * exports gate entirely. */
61
+ function resolvePresetRoot(packageName: string): string | null {
62
+ let dir = __dirname;
63
+ while (true) {
64
+ const candidate = path.join(dir, "node_modules", packageName);
65
+ if (existsSync(path.join(candidate, "package.json"))) {
66
+ return candidate;
67
+ }
68
+ const parent = path.dirname(dir);
69
+ if (parent === dir) return null;
70
+ dir = parent;
71
+ }
72
+ }
73
+
74
+ async function loadOnePreset(packageName: string, deps: LoaderDeps = {}): Promise<RuntimePlugin | null> {
75
+ const cachePath = resolvePresetRoot(packageName);
76
+ if (!cachePath) {
77
+ log.warn(LOG_PREFIX, "preset package not resolvable — run `yarn install`?", { packageName });
78
+ return null;
79
+ }
80
+ const pkgJsonPath = path.join(cachePath, "package.json");
81
+ let pkg: PackageJsonShape;
82
+ try {
83
+ pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8")) as PackageJsonShape;
84
+ } catch (err) {
85
+ log.warn(LOG_PREFIX, "preset package.json read/parse failed", { packageName, error: String(err) });
86
+ return null;
87
+ }
88
+ const { version } = pkg;
89
+ if (typeof version !== "string" || version.length === 0) {
90
+ log.warn(LOG_PREFIX, "preset package has no version", { packageName });
91
+ return null;
92
+ }
93
+ return loadPluginFromCacheDir(packageName, version, cachePath, deps);
94
+ }
95
+
96
+ /** Load every preset declared in `server/plugins/preset-list.ts`.
97
+ * Returns the loaded set; failures are logged and silently
98
+ * skipped.
99
+ *
100
+ * Pass `deps.runtimeFactory` from the parent server so factory-shape
101
+ * presets get a real runtime; the MCP child can omit it (definition-
102
+ * only). */
103
+ export async function loadPresetPlugins(deps: LoaderDeps = {}): Promise<RuntimePlugin[]> {
104
+ if (PRESET_PLUGINS.length === 0) return [];
105
+ const loaded: RuntimePlugin[] = [];
106
+ for (const entry of PRESET_PLUGINS) {
107
+ const plugin = await loadOnePreset(entry.packageName, deps);
108
+ if (plugin) loaded.push(plugin);
109
+ }
110
+ log.info(LOG_PREFIX, "loaded", { requested: PRESET_PLUGINS.length, succeeded: loaded.length });
111
+ return loaded;
112
+ }
@@ -0,0 +1,38 @@
1
+ // Plugin-facing chat API. The host attaches a per-plugin instance of
2
+ // `ChatRuntimeApi` to the `PluginRuntime` it constructs for each
3
+ // plugin (`server/plugins/runtime.ts`).
4
+ //
5
+ // `start()` opens a normal chat seeded with a plugin-supplied first
6
+ // message. The seed is persisted as the first user turn; Claude
7
+ // responds to it as if the user had typed it. Pair the returned
8
+ // `chatId` with `runtime.notifier.publish({ navigateTarget:
9
+ // `/chat/${chatId}`, ... })` so the user can land on the chat from
10
+ // the bell. The chat is permanent — appears in the user's chat list
11
+ // like any other.
12
+ //
13
+ // Plugin authors access this surface via the `MulmoclaudeRuntime`
14
+ // cast. See `runtime-tasks-api.ts` for the same pattern.
15
+
16
+ export interface ChatStartInput {
17
+ /** First user turn. Phrase as the user would — Claude reads it as a
18
+ * user instruction and responds. Visually marked as plugin-seeded
19
+ * in the chat history so the user can tell it came from a plugin,
20
+ * not from them. */
21
+ initialMessage: string;
22
+ /** Role to start the chat in. Defaults to `"general"`. */
23
+ role?: string;
24
+ }
25
+
26
+ export interface ChatStartResult {
27
+ /** New chat session id. Pair with notifier `navigateTarget` to send
28
+ * the user there. */
29
+ chatId: string;
30
+ }
31
+
32
+ export interface ChatRuntimeApi {
33
+ /** Open a new chat seeded with `initialMessage` as the first user
34
+ * turn. Returns the new chat session id. No cap on calls per
35
+ * plugin. Throws if the underlying `startChat` fails (invalid
36
+ * role, internal error). */
37
+ start: (input: ChatStartInput) => Promise<ChatStartResult>;
38
+ }
@@ -0,0 +1,430 @@
1
+ // Runtime plugin loader (#1043 C-2).
2
+ //
3
+ // At server boot:
4
+ // 1. Read the install ledger (`plugins/plugins.json`).
5
+ // 2. For each entry, ensure the tgz is extracted to
6
+ // `plugins/.cache/<name>/<version>/` (cache hit = skip).
7
+ // 3. Dynamic-import the plugin's `dist/index.js` to pull out
8
+ // `TOOL_DEFINITION`. The plugin module is bundled (per the
9
+ // contract documented in `docs/plugin-development.md`) so its
10
+ // bare imports resolve against the on-disk chunk siblings, not
11
+ // against a non-existent `node_modules/` underneath the cache.
12
+ //
13
+ // This module is called from BOTH the parent server (`server/index.ts`)
14
+ // and the spawned MCP child (`server/agent/mcp-server.ts`) — they share
15
+ // the cache, so the second call is fast (no re-extract).
16
+ //
17
+ // Failures don't abort boot. A bad ledger entry, missing tgz, or a
18
+ // definition that fails to import gets logged and skipped; healthy
19
+ // plugins still load.
20
+
21
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
22
+ import { execFileSync } from "node:child_process";
23
+ import path from "node:path";
24
+ import { pathToFileURL } from "node:url";
25
+ import type { PluginRuntime, ToolDefinition } from "gui-chat-protocol";
26
+ import { isPluginFactory } from "gui-chat-protocol";
27
+ import { WORKSPACE_PATHS } from "../workspace/paths.js";
28
+ import { readLedger, type LedgerEntry } from "../utils/files/plugins-io.js";
29
+ import { log } from "../system/logger/index.js";
30
+
31
+ const LOG_PREFIX = "plugins/runtime";
32
+
33
+ export interface RuntimePlugin {
34
+ /** npm package name, e.g. `@gui-chat-plugin/weather`. */
35
+ name: string;
36
+ /** Semver string from the tgz's `package.json`. */
37
+ version: string;
38
+ /** Absolute path to the extracted plugin directory under
39
+ * `plugins/.cache/<name>/<version>/`. */
40
+ cachePath: string;
41
+ /** TOOL_DEFINITION export from the plugin's `dist/index.js`. The
42
+ * shape matches static plugins in `plugin-names.ts`, so the same
43
+ * MCP merge / dispatch path applies. */
44
+ definition: ToolDefinition;
45
+ /** Server-side handler the dispatch route calls. The convention
46
+ * across @gui-chat-plugin packages is to export it under the same
47
+ * key as `TOOL_DEFINITION.name` (e.g. weather → `fetchWeather`,
48
+ * browse → `browse`); we capture it at load time so the dispatch
49
+ * route doesn't have to re-resolve. The signature follows
50
+ * gui-chat-protocol's `ToolPluginCore.execute`:
51
+ * `(context: ToolContext, args) => Promise<ToolResult>` — context
52
+ * first, args second. `null` means the module shipped a
53
+ * TOOL_DEFINITION but no matching handler — the dispatch will 500
54
+ * with a useful message. */
55
+ execute: ((context: unknown, args: unknown) => unknown) | null;
56
+ /** Optional short, URL-safe alias for the OAuth callback route
57
+ * (#1162). When the plugin's module exports
58
+ * `OAUTH_CALLBACK_ALIAS = "<alias>"`, the host registers
59
+ * `/api/plugins/runtime/oauth-callback/<alias>` and forwards
60
+ * `kind: "oauthCallback"` dispatch args to this plugin. The
61
+ * package-name-in-path shape was rejected by Spotify Dashboard
62
+ * because of the percent-encoded `@` / `/` characters, so OAuth
63
+ * plugins declare a short alias (e.g. `"spotify"`) instead. Null
64
+ * for non-OAuth plugins. */
65
+ oauthCallbackAlias: string | null;
66
+ }
67
+
68
+ interface PackageJson {
69
+ name?: string;
70
+ version?: string;
71
+ exports?: string | Record<string, unknown>;
72
+ main?: string;
73
+ module?: string;
74
+ }
75
+
76
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;
77
+
78
+ const isToolDefinition = (value: unknown): value is ToolDefinition => {
79
+ if (!isRecord(value)) return false;
80
+ return typeof value.name === "string" && typeof value.description === "string";
81
+ };
82
+
83
+ /** Resolve the entry-point path from a plugin's `package.json`. Covers
84
+ * the four legal `exports` shapes per the Node.js spec, then falls
85
+ * through to `module` / `main` for legacy packages.
86
+ *
87
+ * Legal `exports` forms (Node.js docs):
88
+ * 1. String sugar: `"exports": "./index.js"`
89
+ * 2. Conditional root: `"exports": { "import": "./esm.js", … }`
90
+ * 3. Subpath map: `"exports": { ".": "./index.js", "./util": "./util.js", … }`
91
+ * 4. Subpath + cond.: `"exports": { ".": { "import": "./esm.js", "require": "./cjs.js" } }`
92
+ *
93
+ * Earlier the loader only matched form (4) and fell straight through
94
+ * to `module` / `main`. Real npm packages using forms (1)-(3) failed
95
+ * to load (#1077 review). */
96
+ function resolveEntrySpecifier(pkg: PackageJson): string | null {
97
+ const fromExports = resolveExportsField(pkg.exports);
98
+ if (fromExports) return fromExports;
99
+ if (typeof pkg.module === "string") return pkg.module;
100
+ if (typeof pkg.main === "string") return pkg.main;
101
+ return null;
102
+ }
103
+
104
+ function resolveExportsField(exportsField: PackageJson["exports"]): string | null {
105
+ // Form 1: top-level string sugar.
106
+ // Form A: array fallback chain (e.g. ["./dist/index.js", "./fallback.js"]).
107
+ // Form 2: conditional root (e.g. `{ import, require, default }`).
108
+ // Form 3 / 4: subpath map keyed by `.` — value can be a string,
109
+ // array, or condition object (Codex review iter-2 on #1116).
110
+ const root = pickExportsTarget(exportsField);
111
+ if (root) return root;
112
+ if (isRecord(exportsField)) {
113
+ return pickExportsTarget(exportsField["."]);
114
+ }
115
+ return null;
116
+ }
117
+
118
+ /** Walk a Node.js `exports` value and return the first string entry
119
+ * point this ESM loader can use. Handles every legal target shape:
120
+ *
121
+ * - string: return it directly.
122
+ * - array: try each element in order; first resolvable wins.
123
+ * Per the spec arrays are a fallback chain — Node tries each
124
+ * until one resolves on disk. We don't probe disk here, so
125
+ * "first string-or-resolvable-condition" approximates that.
126
+ * - condition object: prefer `import` (ESM-specific), then
127
+ * `default` (universal fallback).
128
+ * - everything else (e.g. only `require`, only nested arrays of
129
+ * condition objects with no string targets): null. */
130
+ function pickExportsTarget(value: unknown): string | null {
131
+ if (typeof value === "string") return value;
132
+ if (Array.isArray(value)) {
133
+ for (const entry of value) {
134
+ const resolved = pickExportsTarget(entry);
135
+ if (resolved) return resolved;
136
+ }
137
+ return null;
138
+ }
139
+ if (!isRecord(value)) return null;
140
+ // Condition object — `import` first (the ESM-specific condition
141
+ // match), then `default` (the universal fallback that "always
142
+ // matches" per the Node.js spec).
143
+ const fromImport = pickExportsTarget(value.import);
144
+ if (fromImport) return fromImport;
145
+ return pickExportsTarget(value.default);
146
+ }
147
+
148
+ /** Sentinel file written at the end of a successful extract. The
149
+ * loader uses its presence (not just `existsSync(cachePath)`) as the
150
+ * cache-validity check, so a partial extract (interrupted tar, ENOSPC
151
+ * half-write) is detected and re-extracted on the next boot instead
152
+ * of becoming a permanent broken state. */
153
+ export const EXTRACT_MARKER = ".extract-complete";
154
+
155
+ export function isCacheValid(cachePath: string): boolean {
156
+ return existsSync(path.join(cachePath, EXTRACT_MARKER));
157
+ }
158
+
159
+ /** Run `tar xzf` to extract a tgz into the version-keyed cache slot.
160
+ * `--strip-components=1` drops the `package/` prefix that npm packs
161
+ * add. `execFileSync` (not `execSync`) so paths bypass shell parsing
162
+ * and never trip on metacharacters in workspace paths. Synchronous
163
+ * because boot is single-threaded and the alternative (a stream
164
+ * pipeline) adds dependencies for no benefit.
165
+ *
166
+ * On failure, the partial directory is removed so the next boot
167
+ * re-extracts cleanly (no sticky broken state). The completion
168
+ * marker is written ONLY after tar exits 0 — readers should test
169
+ * `isCacheValid()`, not `existsSync(cachePath)`. */
170
+ function extractTgz(tgzAbs: string, destDir: string): void {
171
+ // Wipe any leftover from a previous failed extract before writing.
172
+ if (existsSync(destDir)) rmSync(destDir, { recursive: true, force: true });
173
+ mkdirSync(destDir, { recursive: true });
174
+ try {
175
+ execFileSync("tar", ["-xzf", tgzAbs, "-C", destDir, "--strip-components=1"], { stdio: "pipe" });
176
+ writeFileSync(path.join(destDir, EXTRACT_MARKER), "");
177
+ } catch (err) {
178
+ // Tear down the partial tree so isCacheValid() stays false next boot.
179
+ try {
180
+ rmSync(destDir, { recursive: true, force: true });
181
+ } catch {
182
+ // best-effort cleanup
183
+ }
184
+ throw err;
185
+ }
186
+ }
187
+
188
+ function readPackageJson(cachePath: string): PackageJson | null {
189
+ const pkgPath = path.join(cachePath, "package.json");
190
+ try {
191
+ return JSON.parse(readFileSync(pkgPath, "utf-8")) as PackageJson;
192
+ } catch (err) {
193
+ log.warn(LOG_PREFIX, "package.json read/parse failed", { path: pkgPath, error: String(err) });
194
+ return null;
195
+ }
196
+ }
197
+
198
+ /** Optional per-plugin runtime factory (#1110). When provided, plugins
199
+ * whose `dist/index.js` exports a factory via `export default
200
+ * definePlugin(...)` get the constructed runtime injected at load
201
+ * time. When omitted, factory plugins are loaded with a stub runtime
202
+ * whose handler call throws — fine for the MCP child process which
203
+ * only needs `TOOL_DEFINITION` for `tools/list`; not OK for the
204
+ * parent server which actually invokes the handler. The parent
205
+ * always passes `runtimeFactory`; the child does not. */
206
+ export interface LoaderDeps {
207
+ runtimeFactory?: (pkgName: string) => PluginRuntime;
208
+ }
209
+
210
+ /** Stub runtime — the factory pattern requires SOME runtime to call,
211
+ * even if we only care about extracting `TOOL_DEFINITION`. Methods
212
+ * throw rather than silently no-op so a plugin that mistakenly does
213
+ * I/O at setup time fails loudly during boot instead of at first
214
+ * call.
215
+ *
216
+ * Two host extensions (`tasks`, `chat` — Phase 1 of the Encore plan)
217
+ * are also stubbed here so plugins that destructure `runtime as
218
+ * MulmoclaudeRuntime` and call `tasks.register(...)` at setup time
219
+ * don't crash the definition-only load (e.g. the MCP child
220
+ * extracting `tools/list`). `tasks.register` is a SILENT no-op
221
+ * because tick registration is parent-only by design — the child
222
+ * process has no task manager to register against. `chat.start`
223
+ * matches the throw pattern of `fetch` / `files`, since starting a
224
+ * chat is a parent-only side effect a plugin should never trigger
225
+ * at setup time. The stub deliberately doesn't carry the
226
+ * `MulmoclaudeRuntime` type — the cast lives at the plugin call
227
+ * site (Phase 3 of Encore upstreams these into PluginRuntime). */
228
+ function makeStubRuntime(name: string): PluginRuntime {
229
+ const error = (operation: string) => () => {
230
+ throw new Error(`plugin/${name}: runtime.${operation} unavailable in this process (definition-only load)`);
231
+ };
232
+ const fileOps = {
233
+ read: error("files.read"),
234
+ readBytes: error("files.readBytes"),
235
+ write: error("files.write"),
236
+ readDir: error("files.readDir"),
237
+ stat: error("files.stat"),
238
+ exists: error("files.exists"),
239
+ unlink: error("files.unlink"),
240
+ };
241
+ const stub = {
242
+ pubsub: { publish: () => undefined },
243
+ locale: "en",
244
+ files: { data: fileOps, config: fileOps },
245
+ log: { debug: () => undefined, info: () => undefined, warn: () => undefined, error: () => undefined },
246
+ fetch: error("fetch") as unknown as PluginRuntime["fetch"],
247
+ fetchJson: error("fetchJson") as unknown as PluginRuntime["fetchJson"],
248
+ notifier: {
249
+ publish: error("notifier.publish") as unknown as (...args: unknown[]) => Promise<{ id: string }>,
250
+ clear: error("notifier.clear") as unknown as (id: string) => Promise<void>,
251
+ },
252
+ tasks: {
253
+ // Silent no-op: definition-only loads (MCP child) shouldn't fail
254
+ // just because the plugin declared a tick at setup time.
255
+ register: () => undefined,
256
+ },
257
+ chat: {
258
+ start: error("chat.start") as unknown as (...args: unknown[]) => Promise<{ chatId: string }>,
259
+ },
260
+ };
261
+ // Returned typed as `PluginRuntime` — host extensions (notifier /
262
+ // tasks / chat) are accessed by plugins via the `MulmoclaudeRuntime`
263
+ // cast and thus aren't part of the stub's nominal type yet.
264
+ return stub as unknown as PluginRuntime;
265
+ }
266
+
267
+ /** Two carrier shapes (#1110 backward compatibility):
268
+ *
269
+ * 1. **Factory** (`export default definePlugin(setup)`) — `mod.default`
270
+ * is a function. Call it with the plugin's runtime; the return
271
+ * value carries `TOOL_DEFINITION` and the handler keyed by
272
+ * `TOOL_DEFINITION.name`. Closure over runtime gives the handler
273
+ * access to scoped pubsub / files / log without per-call context
274
+ * threading.
275
+ * 2. **Legacy** (`export const TOOL_DEFINITION = ...; export async
276
+ * function <name>(context, args) ...`) — `mod.TOOL_DEFINITION` and
277
+ * `mod[<name>]` directly. Continues to work; existing
278
+ * @gui-chat-plugin packages don't need to migrate.
279
+ */
280
+ function resolveCarrier(name: string, mod: Record<string, unknown>, deps: LoaderDeps): { carrier: Record<string, unknown>; usingFactory: boolean } {
281
+ const defaultExport = mod.default;
282
+ if (isPluginFactory(defaultExport)) {
283
+ const runtime = deps.runtimeFactory ? deps.runtimeFactory(name) : makeStubRuntime(name);
284
+ return { carrier: defaultExport(runtime) as unknown as Record<string, unknown>, usingFactory: true };
285
+ }
286
+ return { carrier: mod, usingFactory: false };
287
+ }
288
+
289
+ /** Pull the executable from a carrier. Factory-shape handlers take
290
+ * `(args)` only (runtime is closed over) so we wrap to discard the
291
+ * legacy `context` arg the dispatch route passes. Legacy-shape
292
+ * handlers keep the `(context, args)` signature unchanged. Returns
293
+ * null when no function is exported under `definition.name` — the
294
+ * plugin still appears in `tools/list` but dispatch logs + 500s. */
295
+ function resolveExecute(
296
+ carrier: Record<string, unknown>,
297
+ definitionName: string,
298
+ usingFactory: boolean,
299
+ ): ((context: unknown, args: unknown) => unknown) | null {
300
+ const handler = carrier[definitionName];
301
+ if (typeof handler !== "function") return null;
302
+ if (usingFactory) {
303
+ const factoryHandler = handler as (args: unknown) => unknown;
304
+ return (_context, args) => factoryHandler(args);
305
+ }
306
+ return handler as (context: unknown, args: unknown) => unknown;
307
+ }
308
+
309
+ /** Read the optional `OAUTH_CALLBACK_ALIAS` named export. Validated
310
+ * against `^[a-z0-9][a-z0-9-]{0,30}$` — short, lowercase,
311
+ * URL-friendly, can't be `..` or contain `/`. A bad alias is logged
312
+ * and ignored (the plugin still loads; just skips the OAuth route
313
+ * registration). */
314
+ const OAUTH_CALLBACK_ALIAS_RE = /^[a-z0-9][a-z0-9-]{0,30}$/;
315
+ function resolveOauthCallbackAlias(name: string, mod: Record<string, unknown>): string | null {
316
+ const raw = mod.OAUTH_CALLBACK_ALIAS;
317
+ if (raw === undefined) return null;
318
+ if (typeof raw !== "string" || !OAUTH_CALLBACK_ALIAS_RE.test(raw)) {
319
+ log.warn(LOG_PREFIX, "OAUTH_CALLBACK_ALIAS export is not a valid alias — ignoring", { name, raw: String(raw) });
320
+ return null;
321
+ }
322
+ return raw;
323
+ }
324
+
325
+ /** Load a plugin from an already-extracted cache directory. Pure
326
+ * function — accepts paths explicitly, so tests don't need a real
327
+ * workspace. Returns null on any structural failure (missing
328
+ * package.json, missing TOOL_DEFINITION, broken import); the caller
329
+ * treats nulls as "skip". */
330
+ export async function loadPluginFromCacheDir(name: string, version: string, cachePath: string, deps: LoaderDeps = {}): Promise<RuntimePlugin | null> {
331
+ const pkg = readPackageJson(cachePath);
332
+ if (!pkg) return null;
333
+ const entrySpec = resolveEntrySpecifier(pkg);
334
+ if (!entrySpec) {
335
+ log.warn(LOG_PREFIX, "no entry specifier in package.json — skipping", { name });
336
+ return null;
337
+ }
338
+ const entryAbs = path.join(cachePath, entrySpec);
339
+ try {
340
+ const mod = (await import(pathToFileURL(entryAbs).href)) as Record<string, unknown>;
341
+ const { carrier, usingFactory } = resolveCarrier(name, mod, deps);
342
+ const definition = carrier.TOOL_DEFINITION;
343
+ if (!isToolDefinition(definition)) {
344
+ log.warn(LOG_PREFIX, "no TOOL_DEFINITION export — skipping", { name, entrySpec, shape: usingFactory ? "factory" : "legacy" });
345
+ return null;
346
+ }
347
+ const execute = resolveExecute(carrier, definition.name, usingFactory);
348
+ if (!execute) {
349
+ log.warn(LOG_PREFIX, "no execute handler matching TOOL_DEFINITION.name — dispatch will fail", {
350
+ name,
351
+ expectedExport: definition.name,
352
+ shape: usingFactory ? "factory" : "legacy",
353
+ });
354
+ }
355
+ const oauthCallbackAlias = resolveOauthCallbackAlias(name, mod);
356
+ return { name, version, cachePath, definition, execute, oauthCallbackAlias };
357
+ } catch (err) {
358
+ log.error(LOG_PREFIX, "import failed", { name, entrySpec, error: String(err) });
359
+ return null;
360
+ }
361
+ }
362
+
363
+ /** Lexical anchor: confirm `candidate` resolves inside `base`. Catches
364
+ * malformed ledger entries (`name` containing `../../etc`) before we
365
+ * touch the disk. The asset route trusts registry membership, so a
366
+ * registered cachePath that escaped the base would expose arbitrary
367
+ * files via the unauthenticated GET — this check is the first
368
+ * line of defence (defence-in-depth: realpath after extract is the
369
+ * symlink-escape backstop). Exported for testing. */
370
+ export function ensureInsideBase(candidate: string, base: string): boolean {
371
+ const resolvedCandidate = path.resolve(candidate);
372
+ const resolvedBase = path.resolve(base);
373
+ return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(resolvedBase + path.sep);
374
+ }
375
+
376
+ async function loadOne(entry: LedgerEntry, deps: LoaderDeps = {}): Promise<RuntimePlugin | null> {
377
+ const tgzAbs = path.join(WORKSPACE_PATHS.plugins, entry.tgz);
378
+ const cachePath = path.join(WORKSPACE_PATHS.pluginCache, entry.name, entry.version);
379
+ // Anchor checks BEFORE any disk probe (`existsSync` / `realpath`).
380
+ // The ledger has two separate user-controlled fields — `tgz` and
381
+ // (`name`, `version`) — and each joins against a different base
382
+ // (`WORKSPACE_PATHS.plugins` vs. `pluginCache`). Both must stay
383
+ // inside their respective bases; otherwise even a stat-only probe
384
+ // would touch a path outside the intended roots.
385
+ if (!ensureInsideBase(tgzAbs, WORKSPACE_PATHS.plugins)) {
386
+ log.warn(LOG_PREFIX, "ledger entry tgz escapes plugins root — skipping", { name: entry.name, tgz: entry.tgz });
387
+ return null;
388
+ }
389
+ if (!ensureInsideBase(cachePath, WORKSPACE_PATHS.pluginCache)) {
390
+ log.warn(LOG_PREFIX, "ledger entry escapes plugin cache root — skipping", {
391
+ name: entry.name,
392
+ version: entry.version,
393
+ });
394
+ return null;
395
+ }
396
+ if (!existsSync(tgzAbs)) {
397
+ log.warn(LOG_PREFIX, "tgz missing — skipping", { name: entry.name, tgz: entry.tgz });
398
+ return null;
399
+ }
400
+ if (!isCacheValid(cachePath)) {
401
+ try {
402
+ extractTgz(tgzAbs, cachePath);
403
+ log.info(LOG_PREFIX, "extracted", { name: entry.name, version: entry.version });
404
+ } catch (err) {
405
+ log.error(LOG_PREFIX, "extract failed", { name: entry.name, error: String(err) });
406
+ return null;
407
+ }
408
+ }
409
+ return loadPluginFromCacheDir(entry.name, entry.version, cachePath, deps);
410
+ }
411
+
412
+ /** Read the ledger and load every healthy plugin. Returns the loaded
413
+ * set; failures are logged and silently skipped (see module
414
+ * comment).
415
+ *
416
+ * Pass `deps.runtimeFactory` from the parent server so factory-shape
417
+ * plugins (`export default definePlugin(...)`) get a real runtime.
418
+ * The MCP child can call without deps — its only consumer is
419
+ * `tools/list` which needs `TOOL_DEFINITION` only. */
420
+ export async function loadRuntimePlugins(deps: LoaderDeps = {}): Promise<RuntimePlugin[]> {
421
+ const entries = readLedger();
422
+ if (entries.length === 0) return [];
423
+ const loaded: RuntimePlugin[] = [];
424
+ for (const entry of entries) {
425
+ const plugin = await loadOne(entry, deps);
426
+ if (plugin) loaded.push(plugin);
427
+ }
428
+ log.info(LOG_PREFIX, "loaded", { requested: entries.length, succeeded: loaded.length });
429
+ return loaded;
430
+ }