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
@@ -35,6 +35,7 @@ import { normalizeCategories, type CategorySlug } from "../../workspace/sources/
35
35
  import { badRequest, conflict, sendError, serverError } from "../../utils/httpError.js";
36
36
  import { errorMessage } from "../../utils/errors.js";
37
37
  import { API_ROUTES } from "../../../src/config/apiRoutes.js";
38
+ import { bindRoute } from "../../utils/router.js";
38
39
  import { isNonEmptyString, isRecord } from "../../utils/types.js";
39
40
 
40
41
  const router = Router();
@@ -55,7 +56,7 @@ interface ErrorResponse {
55
56
  error: string;
56
57
  }
57
58
 
58
- router.get(API_ROUTES.sources.list, async (_req: Request, res: Response<ListSourcesResponse | ErrorResponse>) => {
59
+ bindRoute(router, API_ROUTES.sources.list, async (_req: Request, res: Response<ListSourcesResponse | ErrorResponse>) => {
59
60
  try {
60
61
  const sources = await listSources(workspacePath);
61
62
  res.json({ sources });
@@ -88,45 +89,49 @@ interface RegisterSourceResponse {
88
89
  classifyRationale?: string;
89
90
  }
90
91
 
91
- router.post(API_ROUTES.sources.create, async (req: Request<object, unknown, RegisterSourceBody>, res: Response<RegisterSourceResponse | ErrorResponse>) => {
92
- const parsed = parseRegisterBody(req.body ?? {});
93
- if ("error" in parsed) {
94
- sendError(res, parsed.status, parsed.error);
95
- return;
96
- }
97
- const existing = await readSource(workspacePath, parsed.slug);
98
- if (existing) {
99
- conflict(res, `source "${parsed.slug}" already exists`);
100
- return;
101
- }
102
- const { categories, rationale } = await resolveCategories(parsed);
103
- const source: Source = {
104
- slug: parsed.slug,
105
- title: parsed.title,
106
- url: parsed.url,
107
- fetcherKind: parsed.fetcherKind,
108
- fetcherParams: parsed.fetcherParams,
109
- schedule: parsed.schedule,
110
- categories,
111
- maxItemsPerFetch: parsed.maxItemsPerFetch,
112
- addedAt: new Date().toISOString(),
113
- notes: parsed.notes,
114
- };
115
- try {
116
- await writeSource(workspacePath, source);
117
- } catch (err) {
118
- serverError(res, errorMessage(err, "failed to write source"));
119
- return;
120
- }
121
- log.info("sources", "source registered", {
122
- slug: parsed.slug,
123
- fetcherKind: parsed.fetcherKind,
124
- });
125
- res.status(201).json({
126
- source,
127
- ...(rationale !== undefined && { classifyRationale: rationale }),
128
- });
129
- });
92
+ bindRoute(
93
+ router,
94
+ API_ROUTES.sources.create,
95
+ async (req: Request<object, unknown, RegisterSourceBody>, res: Response<RegisterSourceResponse | ErrorResponse>) => {
96
+ const parsed = parseRegisterBody(req.body ?? {});
97
+ if ("error" in parsed) {
98
+ sendError(res, parsed.status, parsed.error);
99
+ return;
100
+ }
101
+ const existing = await readSource(workspacePath, parsed.slug);
102
+ if (existing) {
103
+ conflict(res, `source "${parsed.slug}" already exists`);
104
+ return;
105
+ }
106
+ const { categories, rationale } = await resolveCategories(parsed);
107
+ const source: Source = {
108
+ slug: parsed.slug,
109
+ title: parsed.title,
110
+ url: parsed.url,
111
+ fetcherKind: parsed.fetcherKind,
112
+ fetcherParams: parsed.fetcherParams,
113
+ schedule: parsed.schedule,
114
+ categories,
115
+ maxItemsPerFetch: parsed.maxItemsPerFetch,
116
+ addedAt: new Date().toISOString(),
117
+ notes: parsed.notes,
118
+ };
119
+ try {
120
+ await writeSource(workspacePath, source);
121
+ } catch (err) {
122
+ serverError(res, errorMessage(err, "failed to write source"));
123
+ return;
124
+ }
125
+ log.info("sources", "source registered", {
126
+ slug: parsed.slug,
127
+ fetcherKind: parsed.fetcherKind,
128
+ });
129
+ res.status(201).json({
130
+ source,
131
+ ...(rationale !== undefined && { classifyRationale: rationale }),
132
+ });
133
+ },
134
+ );
130
135
 
131
136
  // --- DELETE /api/sources/:slug ------------------------------------------
132
137
 
@@ -139,7 +144,7 @@ interface DeleteSourceResponse {
139
144
  stateRemoved: boolean;
140
145
  }
141
146
 
142
- router.delete(API_ROUTES.sources.remove, async (req: Request<DeleteSourceParams>, res: Response<DeleteSourceResponse | ErrorResponse>) => {
147
+ bindRoute(router, API_ROUTES.sources.remove, async (req: Request<DeleteSourceParams>, res: Response<DeleteSourceResponse | ErrorResponse>) => {
143
148
  const { slug } = req.params;
144
149
  if (!isValidSlug(slug)) {
145
150
  badRequest(res, "invalid slug");
@@ -159,7 +164,7 @@ interface RebuildBody {
159
164
  scheduleType?: unknown;
160
165
  }
161
166
 
162
- router.post(API_ROUTES.sources.rebuild, async (req: Request<object, unknown, RebuildBody>, res: Response<ErrorResponse | Record<string, unknown>>) => {
167
+ bindRoute(router, API_ROUTES.sources.rebuild, async (req: Request<object, unknown, RebuildBody>, res: Response<ErrorResponse | Record<string, unknown>>) => {
163
168
  const scheduleType = validateSchedule(req.body?.scheduleType, "daily");
164
169
  if (!scheduleType) {
165
170
  badRequest(res, `scheduleType must be one of: ${[...SOURCE_SCHEDULES].join(", ")}`);
@@ -239,7 +244,7 @@ interface ManageSourceSuccess {
239
244
 
240
245
  const MANAGE_ACTIONS = new Set(["list", "register", "remove", "rebuild"]);
241
246
 
242
- router.post(API_ROUTES.sources.manage, async (req: Request<object, unknown, ManageSourceBody>, res: Response<ManageSourceSuccess | ErrorResponse>) => {
247
+ bindRoute(router, API_ROUTES.sources.manage, async (req: Request<object, unknown, ManageSourceBody>, res: Response<ManageSourceSuccess | ErrorResponse>) => {
243
248
  const action = req.body?.action;
244
249
  if (typeof action !== "string" || !MANAGE_ACTIONS.has(action)) {
245
250
  badRequest(res, `action must be one of: ${[...MANAGE_ACTIONS].join(", ")}`);
@@ -258,7 +263,6 @@ router.post(API_ROUTES.sources.manage, async (req: Request<object, unknown, Mana
258
263
  return;
259
264
  case "rebuild":
260
265
  await handleRebuild(res);
261
- return;
262
266
  }
263
267
  } catch (err) {
264
268
  log.warn("sources", "manage failed", { action, error: String(err) });
@@ -433,7 +437,10 @@ interface ParsedRegisterBody {
433
437
  skipClassify: boolean;
434
438
  }
435
439
 
436
- type ParseError = { status: number; error: string };
440
+ interface ParseError {
441
+ status: number;
442
+ error: string;
443
+ }
437
444
 
438
445
  function parseRegisterBody(body: RegisterSourceBody): ParsedRegisterBody | ParseError {
439
446
  const title = typeof body.title === "string" ? body.title.trim() : "";
@@ -0,0 +1,44 @@
1
+ // Translation HTTP route — POST /api/translation. Thin handler that
2
+ // delegates to the translation service; validation lives there.
3
+
4
+ import { Router, type Request, type Response } from "express";
5
+ import { API_ROUTES } from "../../../src/config/apiRoutes.js";
6
+ import { createTranslationService, TranslationInputError } from "../../services/translation/index.js";
7
+ import { defaultTranslateBatch } from "../../services/translation/llm.js";
8
+ import { log } from "../../system/logger/index.js";
9
+ import type { TranslateBatchFn, TranslateRequest, TranslateResponse } from "../../services/translation/types.js";
10
+
11
+ export interface TranslationRouteDeps {
12
+ /** Override for tests — defaults to the live workspace root. */
13
+ workspaceRoot?: string;
14
+ /** Override for tests — defaults to the production claude-CLI backend. */
15
+ translateBatch?: TranslateBatchFn;
16
+ }
17
+
18
+ interface TranslateErrorBody {
19
+ error: string;
20
+ }
21
+
22
+ export function createTranslationRouter(deps: TranslationRouteDeps = {}): Router {
23
+ const router = Router();
24
+ const service = createTranslationService({
25
+ translateBatch: deps.translateBatch ?? defaultTranslateBatch,
26
+ workspaceRoot: deps.workspaceRoot,
27
+ });
28
+
29
+ router.post(API_ROUTES.translation.translate, async (req: Request, res: Response<TranslateResponse | TranslateErrorBody>) => {
30
+ try {
31
+ const result = await service.translate(req.body as TranslateRequest);
32
+ res.json(result);
33
+ } catch (err) {
34
+ if (err instanceof TranslationInputError) {
35
+ res.status(400).json({ error: err.message });
36
+ return;
37
+ }
38
+ log.error("translation-route", "translate failed", { error: String(err) });
39
+ res.status(500).json({ error: "translation failed" });
40
+ }
41
+ });
42
+
43
+ return router;
44
+ }
@@ -1,47 +1,18 @@
1
- // Narrow YAML frontmatter reader for wiki page files. We only need
2
- // the `tags:` field, so a tiny regex-based parser beats pulling in a
3
- // full YAML dependency. Supports both flow style (`tags: [a, b, c]`)
4
- // and block-list style:
1
+ // Narrow `tags:` field reader for wiki page files. Built on the
2
+ // shared `parseFrontmatter` util (#895 PR C) — js-yaml handles
3
+ // both flow style (`tags: [a, b, c]`) and block-list style:
5
4
  //
6
5
  // tags:
7
6
  // - a
8
7
  // - b
9
8
  //
9
+ // out of the box, so we just normalise the resulting strings.
10
+ //
10
11
  // Anything unparseable returns `[]` — callers use this for a
11
12
  // best-effort comparison against index.md, so a noisy file should
12
13
  // degrade silently, not throw.
13
14
 
14
- // Match `- value` with any leading indentation. Keeps to linear
15
- // matching (no lazy quantifier) so sonarjs/slow-regex stays happy.
16
- const BLOCK_LIST_ITEM_PATTERN = /^\s*-\s+(\S.*)$/;
17
-
18
- // Pull the inner list from a line that starts with `tags:` and
19
- // contains a `[...]` flow list. Returns null when the line isn't a
20
- // flow-style tags line. Bracket matching is done with `indexOf` so
21
- // we don't need a lazy-quantified regex.
22
- function extractFlowTagsCell(line: string): string | null {
23
- const trimmed = line.trimStart();
24
- if (!trimmed.startsWith("tags:")) return null;
25
- const open = trimmed.indexOf("[");
26
- if (open === -1) return null;
27
- const close = trimmed.indexOf("]", open + 1);
28
- if (close === -1) return null;
29
- return trimmed.slice(open + 1, close);
30
- }
31
-
32
- // Extract the YAML frontmatter body with plain string scanning so
33
- // we don't rely on a lazy-quantified regex (which ESLint's slow-regex
34
- // rule flags for super-linear backtracking when the closing fence is
35
- // missing). Returns null when no well-formed `---\n…\n---` block is
36
- // present at the top of the file.
37
- function extractFrontmatterBody(content: string): string | null {
38
- if (!content.startsWith("---")) return null;
39
- const after = content.indexOf("\n");
40
- if (after === -1) return null;
41
- const close = content.indexOf("\n---", after);
42
- if (close === -1) return null;
43
- return content.slice(after + 1, close);
44
- }
15
+ import { parseFrontmatter } from "../../../utils/markdown/frontmatter.js";
45
16
 
46
17
  function cleanTagToken(token: string): string {
47
18
  return token
@@ -51,36 +22,13 @@ function cleanTagToken(token: string): string {
51
22
  .toLowerCase();
52
23
  }
53
24
 
54
- function parseFlowList(cell: string): string[] {
55
- return cell
56
- .split(",")
25
+ export function parseFrontmatterTags(content: string): string[] {
26
+ const parsed = parseFrontmatter(content);
27
+ if (!parsed.hasHeader) return [];
28
+ const tagsValue = parsed.meta.tags;
29
+ if (!Array.isArray(tagsValue)) return [];
30
+ return tagsValue
31
+ .filter((item): item is string => typeof item === "string")
57
32
  .map(cleanTagToken)
58
33
  .filter((token) => token.length > 0);
59
34
  }
60
-
61
- function parseBlockList(lines: string[], startIndex: number): string[] {
62
- const tags: string[] = [];
63
- for (let i = startIndex + 1; i < lines.length; i++) {
64
- const line = lines[i];
65
- // Block list ends at the first line that isn't a list item —
66
- // blank line, next key, or unindented text.
67
- if (/^\S/.test(line) || line.trim() === "") break;
68
- const match = BLOCK_LIST_ITEM_PATTERN.exec(line);
69
- if (!match) break;
70
- const token = cleanTagToken(match[1].trimEnd());
71
- if (token.length > 0) tags.push(token);
72
- }
73
- return tags;
74
- }
75
-
76
- export function parseFrontmatterTags(content: string): string[] {
77
- const body = extractFrontmatterBody(content);
78
- if (body === null) return [];
79
- const lines = body.split(/\r?\n/);
80
- for (let i = 0; i < lines.length; i++) {
81
- const flow = extractFlowTagsCell(lines[i]);
82
- if (flow !== null) return parseFlowList(flow);
83
- if (/^tags:\s*$/.test(lines[i])) return parseBlockList(lines, i);
84
- }
85
- return [];
86
- }
@@ -0,0 +1,261 @@
1
+ // Wiki page edit-history routes (#763 PR 2). Three endpoints:
2
+ //
3
+ // GET /api/wiki/pages/:slug/history — list snapshots (meta-only)
4
+ // GET /api/wiki/pages/:slug/history/:stamp — read one snapshot
5
+ // POST /api/wiki/pages/:slug/history/:stamp/restore — round-trip the
6
+ // snapshot through `writeWikiPage` (which snapshots the restore
7
+ // itself, so undo stays cheap).
8
+ //
9
+ // Path safety: both `:slug` and `:stamp` are validated *before*
10
+ // they are joined with the workspace root. The slug check matches
11
+ // `wiki-pages/io.ts`'s `isSafeSlug`; the stamp check is the
12
+ // `FILENAME_RE` shape exposed via `isSafeStamp`.
13
+
14
+ import { Router, type Request, type Response } from "express";
15
+ import path from "node:path";
16
+ import { randomUUID } from "node:crypto";
17
+ import { TOOL_NAMES } from "../../../../src/config/toolNames.js";
18
+ import { hasMeaningfulChange, writeWikiPage } from "../../../workspace/wiki-pages/io.js";
19
+ import { WORKSPACE_DIRS } from "../../../workspace/paths.js";
20
+ import { isSafeStamp, listSnapshots, readSnapshot, stripSnapshotMeta } from "../../../workspace/wiki-pages/snapshot.js";
21
+ import { mergeFrontmatter, serializeWithFrontmatter } from "../../../utils/markdown/frontmatter.js";
22
+ import { badRequest, notFound } from "../../../utils/httpError.js";
23
+ import { readTextOrNull } from "../../../utils/files/safe.js";
24
+ import { workspacePath } from "../../../workspace/workspace.js";
25
+ import { pushToolResult } from "../../../events/session-store/index.js";
26
+ import { log } from "../../../system/logger/index.js";
27
+
28
+ const router = Router();
29
+
30
+ // Mirrors `isSafeSlug` from wiki-pages/io.ts (kept independent so
31
+ // the route layer doesn't import the helper through a circular
32
+ // dependency — io.ts already imports snapshot.ts).
33
+ function isSafeSlug(slug: string): boolean {
34
+ if (slug.length === 0) return false;
35
+ if (slug === "." || slug === "..") return false;
36
+ if (slug.includes("/") || slug.includes("\\")) return false;
37
+ if (slug.includes("\0")) return false;
38
+ return true;
39
+ }
40
+
41
+ // Restore is a write under the user's workspace; record a short
42
+ // reason on the new snapshot so the history reads "Restored from
43
+ // 2026-04-28T01-23-45-789Z" rather than an empty cell. Editor stays
44
+ // `user` because the human triggered the restore — same shape as
45
+ // every other UI-driven save today.
46
+ function restoreReason(stamp: string): string {
47
+ return `Restored from ${stamp}`;
48
+ }
49
+
50
+ // Re-build the previous snapshot's content as a single
51
+ // frontmatter+body string for `hasMeaningfulChange` to compare
52
+ // against. `_snapshot_*` keys are stripped first — those are
53
+ // snapshot-event metadata, not part of the page itself, and
54
+ // keeping them in the diff would always flag a difference.
55
+ async function loadPreviousSnapshotContent(slug: string): Promise<string | null> {
56
+ const recent = await listSnapshots(slug, { workspaceRoot: workspacePath });
57
+ if (recent.length === 0) return null;
58
+ const latest = await readSnapshot(slug, recent[0].stamp, { workspaceRoot: workspacePath });
59
+ if (latest === null) return null;
60
+ return serializeWithFrontmatter(stripSnapshotMeta(latest.meta), latest.body);
61
+ }
62
+
63
+ router.get("/pages/:slug/history", async (req: Request<{ slug: string }>, res: Response) => {
64
+ const { slug } = req.params;
65
+ if (!isSafeSlug(slug)) {
66
+ badRequest(res, "Unsafe slug");
67
+ return;
68
+ }
69
+ // Don't gate on the live page existing. Snapshots are non-destructive
70
+ // and outlive their page — gating here would make history disappear
71
+ // exactly when the user needs it (deleted/renamed page → can't see
72
+ // history → can't restore). An empty list still answers "no history"
73
+ // unambiguously (codex review iter-2 #917).
74
+ const snapshots = await listSnapshots(slug);
75
+ res.json({ slug, snapshots });
76
+ });
77
+
78
+ router.get("/pages/:slug/history/:stamp", async (req: Request<{ slug: string; stamp: string }>, res: Response) => {
79
+ const { slug, stamp } = req.params;
80
+ if (!isSafeSlug(slug)) {
81
+ badRequest(res, "Unsafe slug");
82
+ return;
83
+ }
84
+ if (!isSafeStamp(stamp)) {
85
+ badRequest(res, "Unsafe stamp");
86
+ return;
87
+ }
88
+ const snapshot = await readSnapshot(slug, stamp);
89
+ if (snapshot === null) {
90
+ notFound(res, `snapshot not found: ${slug}/${stamp}`);
91
+ return;
92
+ }
93
+ res.json({ slug, snapshot });
94
+ });
95
+
96
+ router.post("/pages/:slug/history/:stamp/restore", async (req: Request<{ slug: string; stamp: string }>, res: Response) => {
97
+ const { slug, stamp } = req.params;
98
+ if (!isSafeSlug(slug)) {
99
+ badRequest(res, "Unsafe slug");
100
+ return;
101
+ }
102
+ if (!isSafeStamp(stamp)) {
103
+ badRequest(res, "Unsafe stamp");
104
+ return;
105
+ }
106
+ const snapshot = await readSnapshot(slug, stamp);
107
+ if (snapshot === null) {
108
+ notFound(res, `snapshot not found: ${slug}/${stamp}`);
109
+ return;
110
+ }
111
+
112
+ // Strip `_snapshot_*` keys before writing — they describe the
113
+ // *original* save event and would be misleading on the restored
114
+ // page. `writeWikiPage` will re-stamp `updated` and the new
115
+ // snapshot will get a fresh `_snapshot_ts` for the restore event.
116
+ const liveMeta = stripSnapshotMeta(snapshot.meta);
117
+ const restoredContent = serializeWithFrontmatter(mergeFrontmatter({}, liveMeta), snapshot.body);
118
+
119
+ // forceSnapshot=true so a "restore to identical content" still
120
+ // produces an audit entry — without it the no-op gate in
121
+ // writeWikiPage would swallow the restore silently.
122
+ await writeWikiPage(slug, restoredContent, {
123
+ editor: "user",
124
+ reason: restoreReason(stamp),
125
+ forceSnapshot: true,
126
+ });
127
+ log.info("wiki", "history restore", { slug, stamp });
128
+ res.json({ slug, restored: { fromStamp: stamp } });
129
+ });
130
+
131
+ // ── Internal endpoint (LLM write hook callback) ────────────────
132
+ //
133
+ // Hit by `<workspace>/.claude/hooks/wiki-snapshot.mjs` after the
134
+ // claude CLI completes a `Write` / `Edit` tool call. The hook
135
+ // computes the slug from the file path it just touched and
136
+ // passes it here; the server resolves the slug to its OWN
137
+ // `data/wiki/pages/` filesystem location, reads disk state, and
138
+ // drops a snapshot through the same `appendSnapshot` path the
139
+ // in-process writers use. Always tagged `editor: "llm"` —
140
+ // user-driven writes go through the regular `writeWikiPage`
141
+ // path with their own editor identity.
142
+ //
143
+ // Why slug-not-absPath: in Docker mode the hook runs inside the
144
+ // container where the workspace lives at `/home/node/mulmoclaude/`
145
+ // while the server (running on the host) sees the same files at
146
+ // `/Users/<user>/mulmoclaude/`. Sending the absolute path forces
147
+ // either side to translate; sending the slug lets each side keep
148
+ // its own filesystem view.
149
+ //
150
+ // `sessionId` lets the snapshot carry the chat-session identifier
151
+ // that drove the write, surfaced from Claude CLI's `session_id`
152
+ // hook payload field. There is no `reason` — the LLM doesn't
153
+ // supply one, and in-process callers (writeWikiPage) attach
154
+ // their own reasons through `WikiWriteMeta` directly.
155
+ //
156
+ // Bearer auth applies via the global `app.use("/api", bearerAuth)`
157
+ // in server/index.ts; no extra check needed here.
158
+
159
+ interface InternalSnapshotBody {
160
+ slug?: string;
161
+ sessionId?: string;
162
+ }
163
+
164
+ router.post("/internal/snapshot", async (req: Request<object, unknown, InternalSnapshotBody>, res: Response) => {
165
+ const { slug, sessionId } = req.body ?? {};
166
+ if (typeof slug !== "string" || slug.length === 0) {
167
+ badRequest(res, "slug required");
168
+ return;
169
+ }
170
+ if (!isSafeSlug(slug)) {
171
+ badRequest(res, "slug is not safe");
172
+ return;
173
+ }
174
+
175
+ const pagePath = path.join(workspacePath, WORKSPACE_DIRS.wikiPages, `${slug}.md`);
176
+ const content = await readTextOrNull(pagePath);
177
+ if (content === null) {
178
+ notFound(res, "wiki page not found on disk");
179
+ return;
180
+ }
181
+
182
+ // Dedupe against the most recent snapshot — Write/Edit hooks
183
+ // fire for every tool call, including ones that only re-stamp
184
+ // `updated` / `editor` without touching the body. Without this
185
+ // guard the history page accumulates duplicate entries (user
186
+ // report 2026-04-30: two identical bodies snapped 2.6s apart).
187
+ // `hasMeaningfulChange` already drives the in-process
188
+ // `writeWikiPage` path; reusing it keeps both paths aligned.
189
+ const previousContent = await loadPreviousSnapshotContent(slug);
190
+ if (previousContent !== null && !hasMeaningfulChange(previousContent, content)) {
191
+ log.info("wiki", "internal snapshot skipped — no meaningful change since previous snapshot", { slug });
192
+ res.json({ slug, ok: true, skipped: "no-meaningful-change" });
193
+ return;
194
+ }
195
+
196
+ // The hook only fires for claude-CLI-driven writes — by
197
+ // construction the agent is the actor. User-driven manual saves
198
+ // go through writeWikiPage in-process and never reach here.
199
+ const { appendSnapshot } = await import("../../../workspace/wiki-pages/snapshot.js");
200
+ const stamp = await appendSnapshot(
201
+ slug,
202
+ null,
203
+ content,
204
+ {
205
+ editor: "llm",
206
+ ...(typeof sessionId === "string" && sessionId.length > 0 && { sessionId }),
207
+ },
208
+ { workspaceRoot: workspacePath },
209
+ );
210
+ log.info("wiki", "internal snapshot recorded", { slug });
211
+
212
+ // Stage 3a (#963): publish a synthetic `manageWiki` toolResult
213
+ // into the session timeline so the canvas shows what the LLM
214
+ // just wrote. The View dispatch (existing manageWiki plugin)
215
+ // picks up the new `page-edit` action and fetches the snapshot
216
+ // body via /api/wiki/pages/:slug/history/:stamp on render —
217
+ // JSONL stays small (~150 bytes per write) because we store
218
+ // the snapshot reference, not the body. `pagePath` is a GC
219
+ // fallback: if the snapshot is gc'd before render, the View
220
+ // falls back to reading the live page file.
221
+ // Wrapped in try/catch so a publish failure (e.g. JSONL append
222
+ // throws) doesn't fail the whole route — the snapshot was
223
+ // already written, and the hook is fire-and-forget. Without
224
+ // this guard the route would 500 even though the wiki write
225
+ // itself succeeded; the next save would still snapshot fine,
226
+ // but the canvas would silently lose this one preview
227
+ // (CodeRabbit review).
228
+ if (typeof sessionId === "string" && sessionId.length > 0) {
229
+ try {
230
+ const outcome = await pushToolResult(sessionId, {
231
+ uuid: randomUUID(),
232
+ toolName: TOOL_NAMES.manageWiki,
233
+ data: {
234
+ // `"page-edit"` is the action discriminator the wiki
235
+ // plugin's `View.vue` switches on. It's repeated in the
236
+ // plugin and in `src/plugins/wiki/pageEditLoader.ts`; a
237
+ // shared `WIKI_ACTIONS` const would be the cleaner home
238
+ // but that's a multi-file refactor — out of scope for
239
+ // this CR follow-up.
240
+ action: "page-edit",
241
+ title: slug,
242
+ slug,
243
+ stamp,
244
+ pagePath: path.posix.join(WORKSPACE_DIRS.wikiPages, `${slug}.md`),
245
+ },
246
+ });
247
+ if (outcome.kind === "skipped") {
248
+ log.warn("wiki", "page-edit toolResult publish skipped", { slug, reason: outcome.reason });
249
+ }
250
+ } catch (err) {
251
+ log.warn("wiki", "page-edit toolResult publish failed", {
252
+ slug,
253
+ error: err instanceof Error ? err.message : String(err),
254
+ });
255
+ }
256
+ }
257
+
258
+ res.json({ slug, ok: true });
259
+ });
260
+
261
+ export default router;
@@ -39,7 +39,7 @@ export async function getPageIndex(pagesDir: string): Promise<PageIndex> {
39
39
  const slugs = new Map<string, string>();
40
40
  for (const entry of entries) {
41
41
  if (!entry.isFile()) continue;
42
- const name = entry.name;
42
+ const { name } = entry;
43
43
  if (!name.endsWith(".md")) continue;
44
44
  slugs.set(name.slice(0, -".md".length), name);
45
45
  }