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
@@ -2,7 +2,7 @@ import { Router, Request, Response } from "express";
2
2
  import path from "path";
3
3
  import { WORKSPACE_PATHS } from "../../workspace/paths.js";
4
4
  import { readTextSafeSync, readTextSafe } from "../../utils/files/safe.js";
5
- import { writeFileAtomic } from "../../utils/files/atomic.js";
5
+ import { writeWikiPage } from "../../workspace/wiki-pages/io.js";
6
6
  import { getPageIndex } from "./wiki/pageIndex.js";
7
7
  import { parseFrontmatterTags } from "./wiki/frontmatter.js";
8
8
  import { badRequest, notFound } from "../../utils/httpError.js";
@@ -15,6 +15,7 @@ import { previewSnippet } from "../../utils/logPreview.js";
15
15
  // different name avoids the no-shadow clash without renaming the
16
16
  // long-standing local.
17
17
  import { errorMessage as formatError } from "../../utils/errors.js";
18
+ import { BULLET_LINK_PATTERN, BULLET_WIKI_LINK_PATTERN } from "../../utils/regex.js";
18
19
 
19
20
  const router = Router();
20
21
 
@@ -44,12 +45,12 @@ export function wikiSlugify(text: string): string {
44
45
  }
45
46
 
46
47
  const TABLE_SEPARATOR_PATTERN = /^\|[\s|:-]+\|$/;
47
- // Capture the href (group 2) alongside the title (group 1) so we can
48
- // derive the slug from the file name instead of re-slugifying the
49
- // title. This matters for non-ASCII titles like "さくらインターネット"
48
+ // Bullet-link patterns (BULLET_LINK_PATTERN, BULLET_WIKI_LINK_PATTERN)
49
+ // live in `server/utils/regex.ts` alongside other server regex audit
50
+ // notes. Capture the href (group 2) alongside the title (group 1) so
51
+ // we can derive the slug from the file name instead of re-slugifying
52
+ // the title — important for non-ASCII titles like "さくらインターネット"
50
53
  // where `wikiSlugify` returns "" and the slug would otherwise be lost.
51
- const BULLET_LINK_PATTERN = /^[-*]\s+\[([^\]]+)\]\(([^)]*)\)(?:\s*[—–-]\s*(.*))?/;
52
- const BULLET_WIKI_LINK_PATTERN = /^[-*]\s+\[\[([^\]]+)\]\](?:\s*[—–-]\s*(.*))?/;
53
54
  // Unicode-aware tag body: any letter or number in any script
54
55
  // (so Japanese / Chinese / Korean tags like `#クラウド` or `#可視化`
55
56
  // work), plus `-` and `_` as internal joiners. First char is a
@@ -103,6 +104,28 @@ export function buildTableColumnMap(headerRow: string): Map<string, number> {
103
104
  return map;
104
105
  }
105
106
 
107
+ interface TableColumnIndices {
108
+ slug: number;
109
+ title: number;
110
+ summary: number;
111
+ /** Undefined when the table has no `tags` column — caller skips
112
+ * the tags lookup entirely and the row gets `tags: []`. */
113
+ tags: number | undefined;
114
+ }
115
+
116
+ /** Resolve the per-column indices the row parser needs. Falls back
117
+ * to positional defaults (0/1/2) when the table has no header map.
118
+ * "summary" is the canonical column name; "description" is accepted
119
+ * as a legacy alias used by older fixtures. */
120
+ function resolveTableColumnIndices(columnMap: Map<string, number> | null): TableColumnIndices {
121
+ return {
122
+ slug: columnMap?.get("slug") ?? 0,
123
+ title: columnMap?.get("title") ?? 1,
124
+ summary: columnMap?.get("summary") ?? columnMap?.get("description") ?? 2,
125
+ tags: columnMap?.get("tags"),
126
+ };
127
+ }
128
+
106
129
  // Each parser returns the entry it produced (if any). The parent
107
130
  // loop tries them in order; the first non-null result wins.
108
131
  function parseTableRow(trimmed: string, columnMap: Map<string, number> | null): WikiPageEntry | null {
@@ -111,19 +134,14 @@ function parseTableRow(trimmed: string, columnMap: Map<string, number> | null):
111
134
  .slice(1, -1)
112
135
  .map((column) => column.trim().replace(/^`|`$/g, ""));
113
136
  if (cols.length < 2) return null;
114
- const slugIdx = columnMap?.get("slug") ?? 0;
115
- const titleIdx = columnMap?.get("title") ?? 1;
116
- // Accept either "summary" (the canonical column name in
117
- // server/workspace/helps/wiki.md) or "description" (used by the
118
- // existing unit test fixture). Fall back to column 2 when the
119
- // table has no header map.
120
- const summaryIdx = columnMap?.get("summary") ?? columnMap?.get("description") ?? 2;
121
- const tagsIdx = columnMap?.get("tags");
122
- const slug = cols[slugIdx] ?? "";
123
- const title = cols[titleIdx] || slug;
124
- const description = cols[summaryIdx] ?? "";
125
- const tags = tagsIdx !== undefined ? parseTagsCell(cols[tagsIdx] ?? "") : [];
137
+
138
+ const idx = resolveTableColumnIndices(columnMap);
139
+ const slug = cols[idx.slug] ?? "";
140
+ const title = cols[idx.title] || slug;
126
141
  if (!slug || !title) return null;
142
+
143
+ const description = cols[idx.summary] ?? "";
144
+ const tags = idx.tags !== undefined ? parseTagsCell(cols[idx.tags] ?? "") : [];
127
145
  return { title, slug, description, tags };
128
146
  }
129
147
 
@@ -233,8 +251,9 @@ async function resolvePagePath(pageName: string): Promise<string | null> {
233
251
  const indexContent = readFileOrEmpty(indexFile());
234
252
  const entries = parseIndexEntries(indexContent);
235
253
  const titleMatch = entries.find((entry) => entry.title === pageName);
236
- if (titleMatch && slugs.has(titleMatch.slug)) {
237
- return path.join(dir, slugs.get(titleMatch.slug)!);
254
+ if (titleMatch) {
255
+ const file = slugs.get(titleMatch.slug);
256
+ if (file) return path.join(dir, file);
238
257
  }
239
258
 
240
259
  return null;
@@ -320,7 +339,7 @@ function buildIndexResponse(action: string): WikiResponse {
320
339
  // instructions distinctions that the GET and POST handlers share.
321
340
  export function buildPageResponseData(args: { action: string; pageName: string; resolvedTitle: string; content: string; exists: boolean }): WikiResponse {
322
341
  const { action, pageName, resolvedTitle, content, exists } = args;
323
- const hasContent = !!content;
342
+ const hasContent = Boolean(content);
324
343
  // Three states:
325
344
  // 1. !exists → page file is missing entirely.
326
345
  // 2. exists && !hasContent → page file exists but is empty (e.g.,
@@ -370,7 +389,7 @@ export function toPageResponse(args: { action: string; pageName: string; filePat
370
389
  pageName,
371
390
  resolvedTitle,
372
391
  content,
373
- exists: !!filePath,
392
+ exists: Boolean(filePath),
374
393
  });
375
394
  }
376
395
 
@@ -508,9 +527,14 @@ type SaveOutcome = { ok: true; absPath: string } | { ok: false; reason: "not-fou
508
527
  async function saveExistingPage(pageName: string, content: string): Promise<SaveOutcome> {
509
528
  const absPath = await resolvePagePath(pageName);
510
529
  if (!absPath) return { ok: false, reason: "not-found" };
511
- // Atomic write: tmp file alongside the destination, fsync, rename.
512
- // Prevents a crashed write from leaving the wiki page truncated.
513
- await writeFileAtomic(absPath, content);
530
+ // Funnel through the wiki-page write helper. Atomic write is
531
+ // guaranteed inside; the helper also routes the (old, new) pair
532
+ // to the snapshot pipeline (#763 PR 2 — currently a no-op stub).
533
+ // Editor identity defaults to "user" here because the route is
534
+ // hit by both LLM (`manageWiki` MCP) and frontend saves; PR 2
535
+ // disambiguates them via a request-side flag.
536
+ const slug = path.basename(absPath, ".md");
537
+ await writeWikiPage(slug, content, { editor: "user" });
514
538
  return { ok: true, absPath };
515
539
  }
516
540
 
@@ -529,7 +553,7 @@ async function handleSaveAction(
529
553
  badRequest(res, "pageName required for save action");
530
554
  return;
531
555
  }
532
- const content = req.body.content;
556
+ const { content } = req.body;
533
557
  if (typeof content !== "string") {
534
558
  log.warn("wiki", "POST save: missing content", { pageNamePreview: previewSnippet(pageName) });
535
559
  badRequest(res, "content (string) required for save action");
@@ -0,0 +1,83 @@
1
+ // File-change publisher. Single place that knows how to broadcast
2
+ // "this workspace file changed" — every route that writes through the
3
+ // app should call `publishFileChange(relPath)` after a successful
4
+ // write so that subscribed UI tabs (and other browsers) refetch.
5
+ //
6
+ // Module-singleton + `init…` pattern, mirroring `session-store`. The
7
+ // singleton is null at import time so unit-tested route handlers can
8
+ // skip wiring and simply observe a no-op publish.
9
+ //
10
+ // Channel name + payload shape live in `src/config/pubsubChannels.ts`
11
+ // so subscribers can't drift from the publisher.
12
+
13
+ import { stat } from "node:fs/promises";
14
+ import path from "node:path";
15
+ import type { IPubSub } from "./pub-sub/index.js";
16
+ import { fileChannel, toPosixWorkspacePath, type FileChannelPayload } from "../../src/config/pubsubChannels.js";
17
+ import { workspacePath } from "../workspace/workspace.js";
18
+ import { maybeRegenerateTopicIndex, TOPIC_INDEX_RELATIVE_PATH } from "../workspace/memory/topic-index-hook.js";
19
+ import { log } from "../system/logger/index.js";
20
+
21
+ let pubsub: IPubSub | null = null;
22
+
23
+ export function initFileChangePublisher(instance: IPubSub): void {
24
+ pubsub = instance;
25
+ }
26
+
27
+ /**
28
+ * Publish a file-change event for a workspace-relative path. Reads
29
+ * the post-write `mtimeMs` via `stat` so subscribers get a monotonic
30
+ * timestamp suitable for both cache-busting (`?v=<mtime>`) and
31
+ * out-of-order drops. If the stat fails (file just got deleted, race
32
+ * with another writer, etc.) we fall back to `Date.now()` rather than
33
+ * dropping the event — losing the notification is worse than an
34
+ * approximate timestamp.
35
+ */
36
+ export async function publishFileChange(relativePath: string): Promise<void> {
37
+ if (!pubsub) return;
38
+ const absPath = path.join(workspacePath, relativePath);
39
+ let mtimeMs: number;
40
+ try {
41
+ ({ mtimeMs } = await stat(absPath));
42
+ } catch (err) {
43
+ log.warn("file-change", "stat failed; falling back to Date.now()", {
44
+ pathPreview: relativePath,
45
+ error: err instanceof Error ? err.message : String(err),
46
+ });
47
+ mtimeMs = Date.now();
48
+ }
49
+ // Normalise once so `payload.path` and the channel suffix can't drift
50
+ // on Windows / mixed-separator inputs.
51
+ const posixPath = toPosixWorkspacePath(relativePath);
52
+ const payload: FileChannelPayload = { path: posixPath, mtimeMs };
53
+ // Callers fire-and-forget via `void publishFileChange(...)`, so a
54
+ // throw here would surface as an unhandled rejection (Node terminates
55
+ // the process by default). Missing one notification is strictly less
56
+ // bad than killing the server.
57
+ try {
58
+ pubsub.publish(fileChannel(posixPath), payload);
59
+ } catch (err) {
60
+ log.warn("file-change", "publish failed; subscribers will miss this event", {
61
+ pathPreview: posixPath,
62
+ error: err instanceof Error ? err.message : String(err),
63
+ });
64
+ }
65
+ // Side-effect hook: keep the topic-format MEMORY.md index in sync
66
+ // when a user edits a topic file via the file explorer (#1032).
67
+ // No-op for non-topic paths. Fire-and-forget so the publish path
68
+ // stays fast; errors log internally.
69
+ //
70
+ // When regen actually runs, also emit a change event for the
71
+ // index file itself so a FilesView tab pinned to MEMORY.md
72
+ // refreshes the moment the rebuild lands. The recursive call is
73
+ // bounded: `MEMORY.md` is excluded by `isTopicFilePath`, so the
74
+ // second pass returns false without triggering another regen.
75
+ maybeRegenerateTopicIndex(posixPath)
76
+ .then((didRegen) => (didRegen ? publishFileChange(TOPIC_INDEX_RELATIVE_PATH) : undefined))
77
+ .catch(() => {});
78
+ }
79
+
80
+ /** Test-only — clear the module singleton so each test starts clean. */
81
+ export function _resetFileChangePublisherForTesting(): void {
82
+ pubsub = null;
83
+ }
@@ -1,128 +1,285 @@
1
- // Notification system (#144).
1
+ // Legacy host-side `publishNotification()` entry point — now a thin
2
+ // wrapper over the new notifier engine (PR 4 of feat-encore).
2
3
  //
3
- // Publishes NotificationPayload to:
4
- // 1. Web pub-sub → bell badge + panel
5
- // 2. Chat-service bridge → Telegram / CLI
4
+ // The signature is preserved so the existing host call sites
5
+ // (`server/agent/mcp-tools/notify.ts`, `server/api/routes/notifications.ts`,
6
+ // `server/workspace/sources/pipeline/notify.ts`,
7
+ // `server/plugins/diagnostics.ts`) keep working without source changes.
8
+ // Internally it now:
9
+ // 1. Maps the legacy `NotificationKind` source category to a
10
+ // `pluginPkg` ("todo"/"scheduler"/"agent"/"journal" keep their
11
+ // names; "push"/"bridge"/"system" collapse under "host").
12
+ // 2. Maps `priority` to engine `severity` ("normal" → "nudge",
13
+ // "high" → "urgent").
14
+ // 3. Flattens the typed `NotificationAction` to a relative URL
15
+ // string for `navigateTarget` (forwarded only as metadata —
16
+ // legacy entries publish with `lifecycle: "fyi"`, so the
17
+ // `action`-lifecycle rules don't apply and clicking still routes).
18
+ // 4. Stashes the legacy fields (`kind`, `priority`, `action`,
19
+ // `i18n`, `transportId`, `sessionId`, the caller-supplied dedup
20
+ // `id`) on `pluginData` so the bell can preserve icon, i18n
21
+ // localization, and transport routing.
6
22
  //
7
- // Callers (trigger sources) use `publishNotification()` to fire.
8
- // In-memory store keeps the last N notifications for the bell panel.
9
- // publishNotification() is wrapped in try-catch so failures never
10
- // propagate to callers (e.g. endRun in session-store).
23
+ // Bridge push (`chat-service.pushToBridge`) and macOS Reminder push
24
+ // no longer happen inline here they're owned by adapters that
25
+ // subscribe to the `notifier` pubsub channel. See
26
+ // `server/notifier/legacy-adapters.ts`.
11
27
 
12
- import { PUBSUB_CHANNELS } from "../../src/config/pubsubChannels.js";
28
+ import { PAGE_ROUTES } from "../../src/router/pageRoutes.js";
13
29
  import {
14
- NOTIFICATION_KINDS,
15
- NOTIFICATION_ICONS,
16
30
  NOTIFICATION_ACTION_TYPES,
31
+ NOTIFICATION_KINDS,
17
32
  NOTIFICATION_PRIORITIES,
18
- type NotificationPayload,
19
- type NotificationKind,
33
+ NOTIFICATION_VIEWS,
20
34
  type NotificationAction,
35
+ type NotificationI18n,
36
+ type NotificationKind,
21
37
  type NotificationPriority,
22
38
  } from "../../src/types/notification.js";
39
+ import { publish as notifierPublish } from "../notifier/engine.js";
40
+ import type { NotifierSeverity } from "../notifier/types.js";
23
41
  import { ONE_SECOND_MS, MAX_NOTIFICATION_DELAY_SEC } from "../utils/time.js";
24
42
  import { log } from "../system/logger/index.js";
25
43
  import { makeUuid } from "../utils/id.js";
26
- import { pushToMacosReminder } from "../system/macosNotify.js";
27
44
 
28
- // ── Dependencies (injected at startup) ──────────────────────────
45
+ // ── Public types ────────────────────────────────────────────────
29
46
 
30
- export interface NotificationDeps {
31
- publish: (channel: string, payload: unknown) => void;
32
- pushToBridge: (transportId: string, chatId: string, message: string) => void;
47
+ export interface PublishNotificationOpts {
48
+ kind: NotificationKind;
49
+ title: string;
50
+ body?: string;
51
+ action?: NotificationAction;
52
+ priority?: NotificationPriority;
53
+ sessionId?: string;
54
+ transportId?: string;
55
+ /** Override the auto-generated UUID with a caller-supplied stable
56
+ * id. Used by the plugin-meta diagnostics: the same diagnostic
57
+ * id is returned from `/api/plugins/diagnostics`, and `pluginData`
58
+ * carries it so `announcePluginMetaDiagnostics` can dedupe across
59
+ * reboots without piling identical entries into `active.json`. */
60
+ id?: string;
61
+ /** vue-i18n keys + params for clients to localize the title/body.
62
+ * Server-side `title` / `body` stay set as English fallbacks for
63
+ * logs, macOS Reminder push, and bridge push paths. */
64
+ i18n?: NotificationI18n;
33
65
  }
34
66
 
35
- let deps: NotificationDeps | null = null;
67
+ /** Discriminated marker on `NotifierEntry.pluginData` for entries
68
+ * produced by the legacy `publishNotification()` wrapper. The bell
69
+ * reads this to preserve the legacy icon, i18n localization, and
70
+ * transport routing. New direct callers of `notifier.publish()`
71
+ * publish without this shape. */
72
+ export interface LegacyNotifierPluginData {
73
+ legacy: true;
74
+ /** Caller-supplied stable id (e.g. plugin-meta diagnostic id), or
75
+ * the auto-generated UUID otherwise. Distinct from
76
+ * `NotifierEntry.id`, which the engine assigns. */
77
+ legacyId: string;
78
+ kind: NotificationKind;
79
+ priority: NotificationPriority;
80
+ action: NotificationAction;
81
+ i18n?: NotificationI18n;
82
+ transportId?: string;
83
+ sessionId?: string;
84
+ }
36
85
 
37
- export function initNotifications(injected: NotificationDeps): void {
38
- deps = injected;
86
+ export function isLegacyNotifierPluginData(value: unknown): value is LegacyNotifierPluginData {
87
+ if (value === null || typeof value !== "object") return false;
88
+ const rec = value as Record<string, unknown>;
89
+ return rec.legacy === true && typeof rec.legacyId === "string" && typeof rec.kind === "string";
39
90
  }
40
91
 
41
- // ── In-memory store ─────────────────────────────────────────────
92
+ // ── Mapping helpers ─────────────────────────────────────────────
42
93
 
43
- const MAX_STORED = 50;
44
- const store: NotificationPayload[] = [];
94
+ /** Map legacy `NotificationKind` → `pluginPkg`. Source categories
95
+ * that come from a domain feature (todo / scheduler / agent /
96
+ * journal) keep their names so a future runtime plugin can adopt the
97
+ * pluginPkg unchanged; the host-internal categories (push, bridge,
98
+ * system) collapse to "host". */
99
+ export function legacyKindToPluginPkg(kind: NotificationKind): string {
100
+ switch (kind) {
101
+ case NOTIFICATION_KINDS.todo:
102
+ case NOTIFICATION_KINDS.scheduler:
103
+ case NOTIFICATION_KINDS.agent:
104
+ case NOTIFICATION_KINDS.journal:
105
+ return kind;
106
+ case NOTIFICATION_KINDS.push:
107
+ case NOTIFICATION_KINDS.bridge:
108
+ case NOTIFICATION_KINDS.system:
109
+ default:
110
+ return "host";
111
+ }
112
+ }
45
113
 
46
- export function getRecentNotifications(): readonly NotificationPayload[] {
47
- return store;
114
+ /** Map legacy `priority` engine `severity`. The legacy badge was
115
+ * red unconditionally; mapping `normal` → `nudge` keeps the bell
116
+ * visibly amber so users still see "you have notifications," while
117
+ * `high` → `urgent` preserves the red escalation for diagnostics
118
+ * and similar attention-grabbing items. `info` is reserved for
119
+ * future non-bell-coloring callers. */
120
+ export function legacyPriorityToSeverity(priority: NotificationPriority | undefined): NotifierSeverity {
121
+ return priority === NOTIFICATION_PRIORITIES.high ? "urgent" : "nudge";
48
122
  }
49
123
 
50
- // ── Publish ─────────────────────────────────────────────────────
124
+ // ── Per-view URL builders ───────────────────────────────────────
125
+ //
126
+ // Split out from `legacyActionToNavigateTarget` so the dispatcher
127
+ // stays under the cognitive-complexity threshold. Each builder takes
128
+ // the typed target slice it cares about and returns either a relative
129
+ // URL or `undefined` (the latter only when a required field is
130
+ // missing — chat without sessionId).
51
131
 
52
- export interface PublishNotificationOpts {
53
- kind: NotificationKind;
54
- title: string;
55
- body?: string;
56
- action?: NotificationAction;
57
- priority?: NotificationPriority;
58
- sessionId?: string;
59
- transportId?: string;
132
+ type NavigateTarget = Extract<NotificationAction, { type: "navigate" }>["target"];
133
+
134
+ // User/content-derived path segments (slugs, anchors, item ids) ride
135
+ // through `encodeURIComponent` so reserved characters like `?`, `#`,
136
+ // `/`, `%`, `&`, ` ` don't change the URL's structure when interpolated.
137
+ // `PAGE_ROUTES.*` and the literal `pages` segment are static literals
138
+ // — encoding them is unnecessary noise.
139
+ //
140
+ // Dot segments (`.` and `..`) survive `encodeURIComponent` (the dots
141
+ // aren't reserved characters) and would let a slug or path component
142
+ // jump out of its view's namespace once the browser / router applies
143
+ // path normalization. `/files/../chat/sess` collapses to `/chat/sess`,
144
+ // for example. `isSafePathComponent` rejects those segments; consumers
145
+ // either fall back to the view's index (single-component fields like
146
+ // `slug` / `itemId`) or drop the navigate target entirely (chat,
147
+ // where `sessionId` is required, has no usable index).
148
+
149
+ function isSafePathComponent(segment: string): boolean {
150
+ return segment !== "." && segment !== "..";
60
151
  }
61
152
 
62
- export function publishNotification(opts: PublishNotificationOpts): void {
63
- try {
64
- const payload: NotificationPayload = {
65
- id: makeUuid(),
66
- kind: opts.kind,
67
- title: opts.title,
68
- body: opts.body,
69
- icon: NOTIFICATION_ICONS[opts.kind],
70
- action: opts.action ?? { type: NOTIFICATION_ACTION_TYPES.none },
71
- firedAt: new Date().toISOString(),
72
- priority: opts.priority ?? NOTIFICATION_PRIORITIES.normal,
73
- sessionId: opts.sessionId,
74
- transportId: opts.transportId,
75
- };
76
-
77
- // Store for bell panel
78
- store.unshift(payload);
79
- if (store.length > MAX_STORED) store.length = MAX_STORED;
80
-
81
- // Push to Web UI via pub-sub
82
- if (deps) {
83
- deps.publish(PUBSUB_CHANNELS.notifications, payload);
84
- }
85
-
86
- // Push to bridge (Telegram/CLI)
87
- if (deps && opts.transportId) {
88
- deps.pushToBridge(opts.transportId, "notifications", formatBridgeMessage(payload));
89
- }
90
-
91
- // Push to macOS Reminders (#789). No-op unless
92
- // MACOS_REMINDER_NOTIFICATIONS=1 + darwin. Fire-and-forget so a
93
- // slow / failing osascript can't block the bell update.
94
- void pushToMacosReminder(payload.title, payload.body);
95
-
96
- log.info("notifications", "published", {
97
- kind: payload.kind,
98
- title: payload.title,
99
- id: payload.id,
100
- });
101
- } catch (err) {
102
- // Never let notification failures break the caller (e.g. endRun).
103
- log.warn("notifications", "publish failed", { error: String(err) });
153
+ function buildChatTarget(target: Extract<NavigateTarget, { view: typeof NOTIFICATION_VIEWS.chat }>): string | undefined {
154
+ // No sessionId → drop the action; bouncing off the catch-all
155
+ // redirect is worse UX than a non-clickable entry. Dot-segment
156
+ // sessionId would normalize off /chat, so drop too.
157
+ if (!target.sessionId || !isSafePathComponent(target.sessionId)) return undefined;
158
+ const sessionPath = `/${PAGE_ROUTES.chat}/${encodeURIComponent(target.sessionId)}`;
159
+ if (target.resultUuid) return `${sessionPath}?result=${encodeURIComponent(target.resultUuid)}`;
160
+ return sessionPath;
161
+ }
162
+
163
+ function buildFilesTarget(target: Extract<NavigateTarget, { view: typeof NOTIFICATION_VIEWS.files }>): string {
164
+ // Files uses a catch-all (`/files/:pathMatch(.*)`); empty path
165
+ // lands on the index. Each segment is encoded so spaces / special
166
+ // characters don't break the URL. A path containing `.` or `..`
167
+ // segments would normalize out of /files, so refuse to build a
168
+ // path target for it — the index is the safest fallback.
169
+ if (!target.path) return `/${PAGE_ROUTES.files}`;
170
+ const segments = target.path.split("/").filter(Boolean);
171
+ if (segments.some((segment) => !isSafePathComponent(segment))) return `/${PAGE_ROUTES.files}`;
172
+ return `/${PAGE_ROUTES.files}/${segments.map(encodeURIComponent).join("/")}`;
173
+ }
174
+
175
+ function buildWikiTarget(target: Extract<NavigateTarget, { view: typeof NOTIFICATION_VIEWS.wiki }>): string {
176
+ // Slug rides as a path component → dot-segment-check it. The
177
+ // anchor lives in the URL fragment, which doesn't participate in
178
+ // path normalization, so it doesn't need the same guard.
179
+ const slug = target.slug && isSafePathComponent(target.slug) ? `/pages/${encodeURIComponent(target.slug)}` : "";
180
+ const hash = target.anchor ? `#${encodeURIComponent(target.anchor)}` : "";
181
+ return `/${PAGE_ROUTES.wiki}${slug}${hash}`;
182
+ }
183
+
184
+ function buildSingleSegmentTarget(view: string, segment: string | undefined): string {
185
+ // Helper for views with a single optional path component: todos
186
+ // (itemId), automations (taskId), sources (slug). Unsafe values
187
+ // fall back to the view's index — a soft-fail since these views
188
+ // all have a usable index page.
189
+ if (segment && isSafePathComponent(segment)) return `/${view}/${encodeURIComponent(segment)}`;
190
+ return `/${view}`;
191
+ }
192
+
193
+ function buildNavigateTarget(target: NavigateTarget): string | undefined {
194
+ switch (target.view) {
195
+ case NOTIFICATION_VIEWS.chat:
196
+ return buildChatTarget(target);
197
+ case NOTIFICATION_VIEWS.todos:
198
+ return buildSingleSegmentTarget(PAGE_ROUTES.todos, target.itemId);
199
+ case NOTIFICATION_VIEWS.calendar:
200
+ return `/${PAGE_ROUTES.calendar}`;
201
+ case NOTIFICATION_VIEWS.automations:
202
+ return buildSingleSegmentTarget(PAGE_ROUTES.automations, target.taskId);
203
+ case NOTIFICATION_VIEWS.sources:
204
+ return buildSingleSegmentTarget(PAGE_ROUTES.sources, target.slug);
205
+ case NOTIFICATION_VIEWS.files:
206
+ return buildFilesTarget(target);
207
+ case NOTIFICATION_VIEWS.wiki:
208
+ return buildWikiTarget(target);
209
+ default:
210
+ return undefined;
104
211
  }
105
212
  }
106
213
 
107
- function formatBridgeMessage(payload: NotificationPayload): string {
108
- const icon = payload.kind === NOTIFICATION_KINDS.agent ? "\u2705" : "\u{1F514}";
109
- const parts = [icon, payload.title];
110
- if (payload.body) parts.push(payload.body);
111
- return parts.join(" ");
214
+ /** Flatten a typed `NotificationAction` to a relative URL string the
215
+ * bell can hand straight to `router.push`. Returns `undefined` for
216
+ * `action: { type: "none" }`, missing required fields (e.g. a chat
217
+ * navigate without `sessionId`), or unknown views. The engine's
218
+ * `navigateTarget` validation requires same-origin paths starting
219
+ * with a single "/", which every branch below satisfies. */
220
+ export function legacyActionToNavigateTarget(action: NotificationAction | undefined): string | undefined {
221
+ if (!action || action.type !== NOTIFICATION_ACTION_TYPES.navigate) return undefined;
222
+ return buildNavigateTarget(action.target);
223
+ }
224
+
225
+ // ── Publish ─────────────────────────────────────────────────────
226
+
227
+ /**
228
+ * Host-only entry point for firing notifications.
229
+ *
230
+ * **Plugins MUST NOT call this directly.** Plugin code (anything under
231
+ * `packages/*-plugin/`) MUST publish through `runtime.notifier.publish`
232
+ * (see `server/notifier/runtime-api.ts`), which auto-binds `pluginPkg`
233
+ * to the calling plugin so plugins cannot impersonate each other.
234
+ *
235
+ * This wrapper exists for host-side callers (`server/agent/`,
236
+ * `server/api/routes/`, `server/workspace/`, `server/plugins/diagnostics.ts`)
237
+ * that don't have a `PluginRuntime` to hand. It forwards into
238
+ * `notifier.publish` with `lifecycle: "fyi"` and stashes legacy fields
239
+ * on `pluginData` so the bell can preserve icon / i18n / transport
240
+ * semantics. Bridge + macOS push are owned by separate adapters
241
+ * subscribed to the `notifier` pubsub channel, not by this function.
242
+ */
243
+ export function publishNotification(opts: PublishNotificationOpts): void {
244
+ const legacyId = opts.id ?? makeUuid();
245
+ const action: NotificationAction = opts.action ?? { type: NOTIFICATION_ACTION_TYPES.none };
246
+ const pluginData: LegacyNotifierPluginData = {
247
+ legacy: true,
248
+ legacyId,
249
+ kind: opts.kind,
250
+ priority: opts.priority ?? NOTIFICATION_PRIORITIES.normal,
251
+ action,
252
+ i18n: opts.i18n,
253
+ transportId: opts.transportId,
254
+ sessionId: opts.sessionId,
255
+ };
256
+ // Fire-and-forget — the engine queues writes through its own
257
+ // coordinator. A persistence failure logs but never throws to the
258
+ // caller (matches the legacy try/catch contract that protected
259
+ // call sites like endRun).
260
+ notifierPublish({
261
+ pluginPkg: legacyKindToPluginPkg(opts.kind),
262
+ severity: legacyPriorityToSeverity(opts.priority),
263
+ lifecycle: "fyi",
264
+ title: opts.title,
265
+ body: opts.body,
266
+ navigateTarget: legacyActionToNavigateTarget(action),
267
+ pluginData,
268
+ }).catch((err) => {
269
+ log.warn("notifications", "publish failed", { error: String(err), legacyId, kind: opts.kind });
270
+ });
112
271
  }
113
272
 
114
273
  // ── Legacy test notification (kept for PoC endpoint) ────────────
115
274
 
116
275
  export const DEFAULT_NOTIFICATION_MESSAGE = "Test notification";
117
276
  export const DEFAULT_NOTIFICATION_TRANSPORT_ID = "cli";
118
- export const DEFAULT_NOTIFICATION_CHAT_ID = "notifications";
119
277
 
120
278
  export interface ScheduleNotificationOptions {
121
279
  message?: string;
122
280
  body?: string;
123
281
  delaySeconds?: number;
124
282
  transportId?: string;
125
- chatId?: string;
126
283
  // Optional deep-link action — lets dev-side callers fire a
127
284
  // notification that navigates to a specific permalink when
128
285
  // clicked. Without this the fired notification has no click
@@ -140,10 +297,13 @@ export interface ScheduledNotification {
140
297
  cancel: () => void;
141
298
  }
142
299
 
143
- export function scheduleTestNotification(opts: ScheduleNotificationOptions, legacyDeps: NotificationDeps): ScheduledNotification {
300
+ /** PoC: schedules one `publishNotification` after a delay. Bridge
301
+ * push is no longer sent inline here — the bridge adapter (subscribed
302
+ * to the notifier channel) fans out to bridges based on the entry's
303
+ * `pluginData.transportId`. */
304
+ export function scheduleTestNotification(opts: ScheduleNotificationOptions): ScheduledNotification {
144
305
  const message = opts.message ?? DEFAULT_NOTIFICATION_MESSAGE;
145
306
  const transportId = opts.transportId ?? DEFAULT_NOTIFICATION_TRANSPORT_ID;
146
- const chatId = opts.chatId ?? DEFAULT_NOTIFICATION_CHAT_ID;
147
307
  const delaySeconds = clampDelay(opts.delaySeconds);
148
308
  const delayMs = delaySeconds * ONE_SECOND_MS;
149
309
  const kind = opts.kind ?? NOTIFICATION_KINDS.push;
@@ -156,13 +316,9 @@ export function scheduleTestNotification(opts: ScheduleNotificationOptions, lega
156
316
  title: message,
157
317
  body: opts.body,
158
318
  priority: NOTIFICATION_PRIORITIES.normal,
159
- // When the caller supplied an action, pass it through so the
160
- // bell clicks into the requested permalink. Otherwise leave
161
- // it undefined so publishNotification falls back to the
162
- // "navigate: none" default.
163
319
  action: opts.action,
320
+ transportId,
164
321
  });
165
- legacyDeps.pushToBridge(transportId, chatId, message);
166
322
  }, delayMs);
167
323
 
168
324
  return {
@@ -3,7 +3,7 @@ import { Server as IOServer } from "socket.io";
3
3
 
4
4
  export interface IPubSub {
5
5
  /** Publish data to all clients subscribed to this channel. */
6
- publish(channel: string, data: unknown): void;
6
+ publish: (channel: string, data: unknown) => void;
7
7
  }
8
8
 
9
9
  // Channel names are treated as socket.io rooms — one room per