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
@@ -11,6 +11,7 @@ import path from "node:path";
11
11
  import { createHash } from "node:crypto";
12
12
  import { slugify } from "../../utils/slug.js";
13
13
  import { toUtcIsoDate } from "../../utils/date.js";
14
+ import { writeFileAtomic } from "../../utils/files/atomic.js";
14
15
  import { WORKSPACE_DIRS } from "../paths.js";
15
16
 
16
17
  export const SEARCHES_DIR = WORKSPACE_DIRS.searches;
@@ -87,7 +88,11 @@ const defaultDeps: WriteSearchDeps = {
87
88
  mkdir: async (dir) => {
88
89
  await fsp.mkdir(dir, { recursive: true });
89
90
  },
90
- writeFile: (filePath, content) => fsp.writeFile(filePath, content, "utf-8"),
91
+ // Atomic write so a crash mid-flight can't leave a half-written
92
+ // search markdown that the jsonl `contentRef` then points at
93
+ // (#881 v2). Tests inject their own adapter so behaviour stays
94
+ // mockable.
95
+ writeFile: (filePath, content) => writeFileAtomic(filePath, content),
91
96
  };
92
97
 
93
98
  /**
@@ -21,6 +21,7 @@ import { workspacePath as defaultWorkspacePath } from "../workspace.js";
21
21
  import { WORKSPACE_DIRS } from "../paths.js";
22
22
  import { log } from "../../system/logger/index.js";
23
23
  import { updateSessionBacklinks } from "./sessionBacklinks.js";
24
+ import { writeWikiPage } from "../wiki-pages/io.js";
24
25
  import { ONE_SECOND_MS } from "../../utils/time.js";
25
26
 
26
27
  // Small tolerance for filesystem mtime granularity (some filesystems
@@ -35,12 +36,23 @@ export interface WikiBacklinksDeps {
35
36
  writeFile: (filePath: string, content: string) => Promise<void>;
36
37
  }
37
38
 
38
- const defaultDeps: WikiBacklinksDeps = {
39
- readdir: (dir) => fsp.readdir(dir),
40
- stat: (filePath) => fsp.stat(filePath),
41
- readFile: (filePath) => fsp.readFile(filePath, "utf-8"),
42
- writeFile: (filePath, content) => fsp.writeFile(filePath, content, "utf-8"),
43
- };
39
+ // Default writers funnel through `writeWikiPage` so the backlink
40
+ // append participates in the same atomic-write + history pipeline
41
+ // as user / LLM edits. Pre-#763 this was a non-atomic
42
+ // `fsp.writeFile` and the call could leave a half-written page on
43
+ // crash. Editor identity is `"system"` so PR 2's snapshot view can
44
+ // distinguish automated edits from human / LLM ones.
45
+ function buildDefaultDeps(workspaceRoot: string, sessionId: string): WikiBacklinksDeps {
46
+ return {
47
+ readdir: (dir) => fsp.readdir(dir),
48
+ stat: (filePath) => fsp.stat(filePath),
49
+ readFile: (filePath) => fsp.readFile(filePath, "utf-8"),
50
+ writeFile: async (filePath, content) => {
51
+ const slug = path.basename(filePath, ".md");
52
+ await writeWikiPage(slug, content, { editor: "system", sessionId }, { workspaceRoot });
53
+ },
54
+ };
55
+ }
44
56
 
45
57
  export interface MaybeAppendWikiBacklinksOptions {
46
58
  chatSessionId: string;
@@ -52,7 +64,7 @@ export interface MaybeAppendWikiBacklinksOptions {
52
64
  export async function maybeAppendWikiBacklinks(opts: MaybeAppendWikiBacklinksOptions): Promise<void> {
53
65
  if (!opts.chatSessionId) return;
54
66
  const workspaceRoot = opts.workspaceRoot ?? defaultWorkspacePath;
55
- const deps: WikiBacklinksDeps = { ...defaultDeps, ...(opts.deps ?? {}) };
67
+ const deps: WikiBacklinksDeps = { ...buildDefaultDeps(workspaceRoot, opts.chatSessionId), ...(opts.deps ?? {}) };
56
68
  const pagesDir = path.join(workspaceRoot, WORKSPACE_DIRS.wikiPages);
57
69
 
58
70
  const files = await listPageFiles(pagesDir, deps);
@@ -113,6 +113,7 @@ function extractSessionIdFromHref(href: string): string | null {
113
113
  function stripFragmentAndQuery(href: string): string {
114
114
  let end = href.length;
115
115
  const hash = href.indexOf("#");
116
+ // eslint-disable-next-line security/detect-possible-timing-attacks -- numeric indexOf check, not a credential compare
116
117
  if (hash !== -1) end = hash;
117
118
  const query = href.indexOf("?");
118
119
  if (query !== -1 && query < end) end = query;
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+
3
+ // server/workspace/wiki-history/hook/snapshot.ts
4
+ import { readFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import path2 from "node:path";
7
+
8
+ // src/lib/wiki-page/slug.ts
9
+ import path from "node:path";
10
+ function isSafeSlug(slug) {
11
+ if (slug.length === 0) return false;
12
+ if (slug === "." || slug === "..") return false;
13
+ if (slug.includes("/") || slug.includes("\\")) return false;
14
+ if (slug.includes("\0")) return false;
15
+ return true;
16
+ }
17
+ function wikiSlugFromAbsPath(absPath, pagesDir) {
18
+ const rel = path.relative(pagesDir, absPath);
19
+ if (rel.length === 0) return null;
20
+ if (path.isAbsolute(rel)) return null;
21
+ if (rel.includes(path.sep)) return null;
22
+ if (!rel.endsWith(".md")) return null;
23
+ const slug = rel.slice(0, -".md".length);
24
+ if (!isSafeSlug(slug)) return null;
25
+ return slug;
26
+ }
27
+
28
+ // server/workspace/wiki-history/hook/snapshot.ts
29
+ var WORKSPACE_ROOT = path2.join(homedir(), "mulmoclaude");
30
+ var WIKI_PAGES_DIR = path2.join(WORKSPACE_ROOT, "data", "wiki", "pages");
31
+ var TOKEN_PATH = path2.join(WORKSPACE_ROOT, ".session-token");
32
+ var PORT_PATH = path2.join(WORKSPACE_ROOT, ".server-port");
33
+ var SERVER_HOST = process.env.MULMOCLAUDE_HOST ?? "127.0.0.1";
34
+ async function readStdin() {
35
+ const chunks = [];
36
+ for await (const chunk of process.stdin) {
37
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
38
+ }
39
+ const raw = Buffer.concat(chunks).toString("utf-8");
40
+ if (!raw.trim()) return null;
41
+ try {
42
+ return JSON.parse(raw);
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ function extractFilePath(payload) {
48
+ const fromInput = payload.tool_input?.file_path;
49
+ if (typeof fromInput === "string") return fromInput;
50
+ const fromResponse = payload.tool_response?.filePath;
51
+ if (typeof fromResponse === "string") return fromResponse;
52
+ return null;
53
+ }
54
+ function readTokenSafe() {
55
+ try {
56
+ return readFileSync(TOKEN_PATH, "utf-8").trim();
57
+ } catch {
58
+ return "";
59
+ }
60
+ }
61
+ function readPortSafe() {
62
+ try {
63
+ const raw = readFileSync(PORT_PATH, "utf-8").trim();
64
+ const port = Number.parseInt(raw, 10);
65
+ return Number.isInteger(port) && port > 0 && port < 65536 ? port : null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+ async function main() {
71
+ const payload = await readStdin();
72
+ if (!payload) return;
73
+ const filePath = extractFilePath(payload);
74
+ if (!filePath) return;
75
+ const slug = wikiSlugFromAbsPath(filePath, WIKI_PAGES_DIR);
76
+ if (slug === null) return;
77
+ const token = readTokenSafe();
78
+ const port = readPortSafe();
79
+ if (!token || port === null) return;
80
+ const envChatSessionId = process.env.MULMOCLAUDE_CHAT_SESSION_ID;
81
+ const payloadSessionId = typeof payload.session_id === "string" && payload.session_id.length > 0 ? payload.session_id : void 0;
82
+ const sessionId = envChatSessionId && envChatSessionId.length > 0 ? envChatSessionId : payloadSessionId;
83
+ const body = sessionId === void 0 ? { slug } : { slug, sessionId };
84
+ try {
85
+ await fetch(`http://${SERVER_HOST}:${port}/api/wiki/internal/snapshot`, {
86
+ method: "POST",
87
+ headers: {
88
+ "Content-Type": "application/json",
89
+ Authorization: `Bearer ${token}`
90
+ },
91
+ body: JSON.stringify(body)
92
+ });
93
+ } catch {
94
+ }
95
+ }
96
+ main().catch(() => {
97
+ });
98
+ //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic25hcHNob3QudHMiLCAiLi4vLi4vLi4vLi4vc3JjL2xpYi93aWtpLXBhZ2Uvc2x1Zy50cyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiLy8gUG9zdFRvb2xVc2UgaG9vayBzb3VyY2UgXHUyMDE0IHJ1bnMgYWZ0ZXIgZXZlcnkgQ2xhdWRlIENMSSBXcml0ZSAvXG4vLyBFZGl0IHRvb2wgY2FsbC4gRGV0ZWN0cyB3cml0ZXMgdGhhdCB0YXJnZXQgYSB3aWtpIHBhZ2UgZmlsZVxuLy8gYW5kIFBPU1RzIGB7IHNsdWcsIHNlc3Npb25JZD8gfWAgdG8gdGhlIHBhcmVudCBzZXJ2ZXInc1xuLy8gYC9hcGkvd2lraS9pbnRlcm5hbC9zbmFwc2hvdGAgZW5kcG9pbnQgc28gdGhlIHNuYXBzaG90IHBpcGVsaW5lXG4vLyAoIzc2MyBQUiAyKSByZWNvcmRzIHRoZSBuZXcgc3RhdGUuXG4vL1xuLy8gVEhJUyBGSUxFIElTIFRIRSBTT1VSQ0UgT0YgVFJVVEguIEVkaXRzIGhlcmUsIHRoZW4gcnVuXG4vLyBgeWFybiBidWlsZDpob29rc2AgKG9yIGB5YXJuIGJ1aWxkYCkgdG8gcmVnZW5lcmF0ZVxuLy8gYC4vc25hcHNob3QubWpzYC4gVGhlIGJ1bmRsZWQgYC5tanNgIGlzIGNvbW1pdHRlZCB0byBnaXQgc29cbi8vIGBwcm92aXNpb24udHNgIGNhbiByZWFkIGl0IGF0IHNlcnZlciBzdGFydHVwIHdpdGhvdXQgaW52b2tpbmdcbi8vIGVzYnVpbGQgb24gdGhlIHJ1bnRpbWUgcGF0aC4gQ0kgcnVucyBgeWFybiBidWlsZDpob29rcyAmJlxuLy8gZ2l0IGRpZmYgLS1leGl0LWNvZGVgIHRvIGNhdGNoIGEgc3RhbGUgYnVuZGxlLlxuLy9cbi8vIFRoZSBob29rIGV4ZWN1dGVzIGluc2lkZSBDbGF1ZGUgQ0xJJ3MgcHJvY2VzcyBzcGFjZSBcdTIwMTQgbXVzdCBiZVxuLy8gc2VsZi1jb250YWluZWQgRVNNLiBlc2J1aWxkIGJ1bmRsZXMgaW1wb3J0cyBmcm9tIHRoZSBzaGFyZWRcbi8vIGBzcmMvbGliL3dpa2ktcGFnZS9zbHVnLnRzYCBtb2R1bGUgc28gdGhlIGhvb2sgYW5kIHRoZVxuLy8gc2VydmVyLXNpZGUgYGNsYXNzaWZ5QXNXaWtpUGFnZWAgYWdyZWUgb24gd2hhdCBjb3VudHMgYXMgYVxuLy8gd2lraSBwYWdlIChubyBjb3B5LXBhc3RlIGRyaWZ0KS5cblxuaW1wb3J0IHsgcmVhZEZpbGVTeW5jIH0gZnJvbSBcIm5vZGU6ZnNcIjtcbmltcG9ydCB7IGhvbWVkaXIgfSBmcm9tIFwibm9kZTpvc1wiO1xuaW1wb3J0IHBhdGggZnJvbSBcIm5vZGU6cGF0aFwiO1xuaW1wb3J0IHsgd2lraVNsdWdGcm9tQWJzUGF0aCB9IGZyb20gXCIuLi8uLi8uLi8uLi9zcmMvbGliL3dpa2ktcGFnZS9zbHVnLmpzXCI7XG5cbi8vIFdvcmtzcGFjZSBsYXlvdXQgbWF0Y2hlcyBgV09SS1NQQUNFX0RJUlMud2lraVBhZ2VzYC4gV2UgaGFyZC1cbi8vIGNvZGUgXCJkYXRhL3dpa2kvcGFnZXNcIiByYXRoZXIgdGhhbiBpbXBvcnRpbmcgdGhlIGNvbnN0YW50c1xuLy8gYmVjYXVzZSB0aGUgaG9vayBuZWVkcyB0byBzdGF5IGluZGVwZW5kZW50IG9mIHRoZSBzZXJ2ZXInc1xuLy8gcnVudGltZSBjb25maWcgXHUyMDE0IHRoZSBidW5kbGUgbXVzdCB3b3JrIGV2ZW4gd2hlbiB0aGUgc2VydmVyXG4vLyBwcm9jZXNzIGlzbid0IHJ1bm5pbmcgeWV0IChlLmcuIGR1cmluZyBhIHN0YXJ0dXAgcmFjZSkuXG5jb25zdCBXT1JLU1BBQ0VfUk9PVCA9IHBhdGguam9pbihob21lZGlyKCksIFwibXVsbW9jbGF1ZGVcIik7XG5jb25zdCBXSUtJX1BBR0VTX0RJUiA9IHBhdGguam9pbihXT1JLU1BBQ0VfUk9PVCwgXCJkYXRhXCIsIFwid2lraVwiLCBcInBhZ2VzXCIpO1xuY29uc3QgVE9LRU5fUEFUSCA9IHBhdGguam9pbihXT1JLU1BBQ0VfUk9PVCwgXCIuc2Vzc2lvbi10b2tlblwiKTtcbmNvbnN0IFBPUlRfUEFUSCA9IHBhdGguam9pbihXT1JLU1BBQ0VfUk9PVCwgXCIuc2VydmVyLXBvcnRcIik7XG4vLyBJbiBEb2NrZXIgbW9kZSB0aGUgcGFyZW50IHNlcnZlciBydW5zIG9uIHRoZSBob3N0J3MgMTI3LjAuMC4xXG4vLyB3aGljaCB0aGUgY29udGFpbmVyIGNhbid0IHJlYWNoIHZpYSBwbGFpbiBsb29wYmFjay4gVGhlIERvY2tlclxuLy8gc3Bhd24gcGx1bWJpbmcgc2V0cyBNVUxNT0NMQVVERV9IT1NUPWhvc3QuZG9ja2VyLmludGVybmFsIHNvXG4vLyBmZXRjaCgpIHJlc29sdmVzIHRvIHRoZSBob3N0IHNlcnZlci4gT3V0c2lkZSBEb2NrZXIgKG9yIHdoZW5cbi8vIHRoZSB2YXIgaXMgdW5zZXQpIHdlIGZhbGwgYmFjayB0byB0aGUgbG9vcGJhY2sgYWRkcmVzcy5cbmNvbnN0IFNFUlZFUl9IT1NUID0gcHJvY2Vzcy5lbnYuTVVMTU9DTEFVREVfSE9TVCA/PyBcIjEyNy4wLjAuMVwiO1xuXG5pbnRlcmZhY2UgSG9va1BheWxvYWQge1xuICB0b29sX2lucHV0PzogeyBmaWxlX3BhdGg/OiB1bmtub3duIH07XG4gIHRvb2xfcmVzcG9uc2U/OiB7IGZpbGVQYXRoPzogdW5rbm93biB9O1xuICBzZXNzaW9uX2lkPzogdW5rbm93bjtcbn1cblxuYXN5bmMgZnVuY3Rpb24gcmVhZFN0ZGluKCk6IFByb21pc2U8SG9va1BheWxvYWQgfCBudWxsPiB7XG4gIGNvbnN0IGNodW5rczogQnVmZmVyW10gPSBbXTtcbiAgZm9yIGF3YWl0IChjb25zdCBjaHVuayBvZiBwcm9jZXNzLnN0ZGluKSB7XG4gICAgY2h1bmtzLnB1c2godHlwZW9mIGNodW5rID09PSBcInN0cmluZ1wiID8gQnVmZmVyLmZyb20oY2h1bmspIDogY2h1bmspO1xuICB9XG4gIGNvbnN0IHJhdyA9IEJ1ZmZlci5jb25jYXQoY2h1bmtzKS50b1N0cmluZyhcInV0Zi04XCIpO1xuICBpZiAoIXJhdy50cmltKCkpIHJldHVybiBudWxsO1xuICB0cnkge1xuICAgIHJldHVybiBKU09OLnBhcnNlKHJhdykgYXMgSG9va1BheWxvYWQ7XG4gIH0gY2F0Y2gge1xuICAgIHJldHVybiBudWxsO1xuICB9XG59XG5cbmZ1bmN0aW9uIGV4dHJhY3RGaWxlUGF0aChwYXlsb2FkOiBIb29rUGF5bG9hZCk6IHN0cmluZyB8IG51bGwge1xuICAvLyBDbGF1ZGUgQ0xJIGhvb2sgcGF5bG9hZDogeyB0b29sX25hbWUsIHRvb2xfaW5wdXQsIHRvb2xfcmVzcG9uc2UsIC4uLiB9XG4gIC8vIERpZmZlcmVudCB0b29scyBzdXJmYWNlIHRoZSBwYXRoIHVuZGVyIGRpZmZlcmVudCBrZXlzIFx1MjAxNFxuICAvLyBjaGVjayBib3RoIHNoYXBlcyBkZWZlbnNpdmVseS5cbiAgY29uc3QgZnJvbUlucHV0ID0gcGF5bG9hZC50b29sX2lucHV0Py5maWxlX3BhdGg7XG4gIGlmICh0eXBlb2YgZnJvbUlucHV0ID09PSBcInN0cmluZ1wiKSByZXR1cm4gZnJvbUlucHV0O1xuICBjb25zdCBmcm9tUmVzcG9uc2UgPSBwYXlsb2FkLnRvb2xfcmVzcG9uc2U/LmZpbGVQYXRoO1xuICBpZiAodHlwZW9mIGZyb21SZXNwb25zZSA9PT0gXCJzdHJpbmdcIikgcmV0dXJuIGZyb21SZXNwb25zZTtcbiAgcmV0dXJuIG51bGw7XG59XG5cbmZ1bmN0aW9uIHJlYWRUb2tlblNhZmUoKTogc3RyaW5nIHtcbiAgdHJ5IHtcbiAgICByZXR1cm4gcmVhZEZpbGVTeW5jKFRPS0VOX1BBVEgsIFwidXRmLThcIikudHJpbSgpO1xuICB9IGNhdGNoIHtcbiAgICByZXR1cm4gXCJcIjtcbiAgfVxufVxuXG5mdW5jdGlvbiByZWFkUG9ydFNhZmUoKTogbnVtYmVyIHwgbnVsbCB7XG4gIHRyeSB7XG4gICAgY29uc3QgcmF3ID0gcmVhZEZpbGVTeW5jKFBPUlRfUEFUSCwgXCJ1dGYtOFwiKS50cmltKCk7XG4gICAgY29uc3QgcG9ydCA9IE51bWJlci5wYXJzZUludChyYXcsIDEwKTtcbiAgICByZXR1cm4gTnVtYmVyLmlzSW50ZWdlcihwb3J0KSAmJiBwb3J0ID4gMCAmJiBwb3J0IDwgNjU1MzYgPyBwb3J0IDogbnVsbDtcbiAgfSBjYXRjaCB7XG4gICAgcmV0dXJuIG51bGw7XG4gIH1cbn1cblxuYXN5bmMgZnVuY3Rpb24gbWFpbigpOiBQcm9taXNlPHZvaWQ+IHtcbiAgY29uc3QgcGF5bG9hZCA9IGF3YWl0IHJlYWRTdGRpbigpO1xuICBpZiAoIXBheWxvYWQpIHJldHVybjtcblxuICBjb25zdCBmaWxlUGF0aCA9IGV4dHJhY3RGaWxlUGF0aChwYXlsb2FkKTtcbiAgaWYgKCFmaWxlUGF0aCkgcmV0dXJuO1xuICBjb25zdCBzbHVnID0gd2lraVNsdWdGcm9tQWJzUGF0aChmaWxlUGF0aCwgV0lLSV9QQUdFU19ESVIpO1xuICBpZiAoc2x1ZyA9PT0gbnVsbCkgcmV0dXJuO1xuXG4gIGNvbnN0IHRva2VuID0gcmVhZFRva2VuU2FmZSgpO1xuICBjb25zdCBwb3J0ID0gcmVhZFBvcnRTYWZlKCk7XG4gIGlmICghdG9rZW4gfHwgcG9ydCA9PT0gbnVsbCkgcmV0dXJuOyAvLyBzZXJ2ZXIgaXNuJ3QgcmVhY2hhYmxlOyBzaWxlbnQgbm8tb3BcblxuICAvLyBQcmVmZXIgb3VyIGNoYXQtc2Vzc2lvbiBpZCBmcm9tIHRoZSBzcGF3biBlbnYgKCM5NjMpIFx1MjAxNCB0aGVcbiAgLy8gc2VydmVyJ3Mgc2Vzc2lvbiBzdG9yZSBrZXlzIGJ5IGNoYXRTZXNzaW9uSWQsIG5vdCBieSBDbGF1ZGVcbiAgLy8gQ0xJJ3MgaW50ZXJuYWwgc2Vzc2lvbl9pZCwgc28gdGhlIHRvb2xSZXN1bHQgcHVibGlzaCBvbiB0aGVcbiAgLy8gc2VydmVyIHNpZGUgb25seSBtYXRjaGVzIHdoZW4gd2UgZm9yd2FyZCBvdXIgb3duIGlkLiBGYWxsIGJhY2tcbiAgLy8gdG8gQ2xhdWRlIENMSSdzIGBzZXNzaW9uX2lkYCAoc3RpbGwgdXNlZnVsIGFzIGEgdHJhY2luZyB0b2tlblxuICAvLyBpbiB0aGUgc25hcHNob3QgZnJvbnRtYXR0ZXIpIHdoZW4gdGhlIGVudiB2YXIgaXMgYWJzZW50IFx1MjAxNCBlLmcuXG4gIC8vIGFuIG9sZGVyIG11bG1vY2xhdWRlIHNlcnZlciBzcGF3bmluZyBhIG5ld2VyIGhvb2sgYnVuZGxlLlxuICBjb25zdCBlbnZDaGF0U2Vzc2lvbklkID0gcHJvY2Vzcy5lbnYuTVVMTU9DTEFVREVfQ0hBVF9TRVNTSU9OX0lEO1xuICBjb25zdCBwYXlsb2FkU2Vzc2lvbklkID0gdHlwZW9mIHBheWxvYWQuc2Vzc2lvbl9pZCA9PT0gXCJzdHJpbmdcIiAmJiBwYXlsb2FkLnNlc3Npb25faWQubGVuZ3RoID4gMCA/IHBheWxvYWQuc2Vzc2lvbl9pZCA6IHVuZGVmaW5lZDtcbiAgY29uc3Qgc2Vzc2lvbklkID0gZW52Q2hhdFNlc3Npb25JZCAmJiBlbnZDaGF0U2Vzc2lvbklkLmxlbmd0aCA+IDAgPyBlbnZDaGF0U2Vzc2lvbklkIDogcGF5bG9hZFNlc3Npb25JZDtcbiAgY29uc3QgYm9keSA9IHNlc3Npb25JZCA9PT0gdW5kZWZpbmVkID8geyBzbHVnIH0gOiB7IHNsdWcsIHNlc3Npb25JZCB9O1xuXG4gIHRyeSB7XG4gICAgYXdhaXQgZmV0Y2goYGh0dHA6Ly8ke1NFUlZFUl9IT1NUfToke3BvcnR9L2FwaS93aWtpL2ludGVybmFsL3NuYXBzaG90YCwge1xuICAgICAgbWV0aG9kOiBcIlBPU1RcIixcbiAgICAgIGhlYWRlcnM6IHtcbiAgICAgICAgXCJDb250ZW50LVR5cGVcIjogXCJhcHBsaWNhdGlvbi9qc29uXCIsXG4gICAgICAgIEF1dGhvcml6YXRpb246IGBCZWFyZXIgJHt0b2tlbn1gLFxuICAgICAgfSxcbiAgICAgIGJvZHk6IEpTT04uc3RyaW5naWZ5KGJvZHkpLFxuICAgIH0pO1xuICB9IGNhdGNoIHtcbiAgICAvLyBOZXR3b3JrIC8gc2VydmVyIGRvd24gXHUyMDE0IGRyb3Agc2lsZW50bHkuIFRoZSB3aWtpIHBhZ2Ugd3JpdGVcbiAgICAvLyBpdHNlbGYgYWxyZWFkeSBzdWNjZWVkZWQ7IG1pc3Npbmcgb25lIHNuYXBzaG90IGlzIHJlY292ZXJhYmxlXG4gICAgLy8gKHRoZSBuZXh0IHNhdmUgd2lsbCByZWNvcmQgdGhlIG5leHQgc3RhdGUpLlxuICB9XG59XG5cbm1haW4oKS5jYXRjaCgoKSA9PiB7XG4gIC8vIEhvb2tzIHJ1biBzeW5jaHJvbm91c2x5IHRvIHRoZSBMTE0gdG9vbCBmbG93IFx1MjAxNCBuZXZlciB0aHJvdy5cbiAgLy8gQSBidXN0ZWQgaG9vayB3b3VsZCBtYWtlIGBXcml0ZWAgaXRzZWxmIGxvb2sgbGlrZSBpdCBmYWlsZWRcbiAgLy8gdG8gdGhlIExMTSwgd2hpY2ggaXMgbXVjaCB3b3JzZSB0aGFuIGEgbWlzc2luZyBzbmFwc2hvdC5cbn0pO1xuIiwgIi8vIFNoYXJlZCB3aWtpLXBhZ2Ugc2x1ZyBsb2dpYyBcdTIwMTQgcHVyZSBmdW5jdGlvbnMgcmV1c2VkIGJ5OlxuLy9cbi8vICAgLSBzZXJ2ZXIvd29ya3NwYWNlL3dpa2ktcGFnZXMvaW8udHMgKHRoZSB3cml0ZSBjaG9rZXBvaW50KVxuLy8gICAtIHNlcnZlci93b3Jrc3BhY2Uvd2lraS1oaXN0b3J5L2hvb2svc25hcHNob3QudHMgKHRoZVxuLy8gICAgIFBvc3RUb29sVXNlIGhvb2sgc2NyaXB0IHRoYXQgZGV0ZWN0cyBMTE0gV3JpdGUvRWRpdCBvbiBhXG4vLyAgICAgd2lraSBwYWdlIGFuZCB0cmlnZ2VycyBhIHNuYXBzaG90KVxuLy9cbi8vIEJvdGggY2FsbCBzaXRlcyBuZWVkIHRoZSBzYW1lIGFuc3dlcjogXCJnaXZlbiB0aGlzIGFic29sdXRlXG4vLyBwYXRoIGFuZCB0aGUgcGFnZXMgZGlyZWN0b3J5LCBpcyBpdCBhIHdpa2kgcGFnZSwgYW5kIGlmIHNvXG4vLyB3aGF0IHNsdWc/XCIuIEJlZm9yZSB0aGUgZXh0cmFjdGlvbiB0aGUgaG9vayByZS1pbXBsZW1lbnRlZFxuLy8gdGhlIHJ1bGVzIGFzIGEgSlMtYXMtc3RyaW5nIHRlbXBsYXRlIGxpdGVyYWwgdGhhdCBkcmlmdGVkXG4vLyBmcm9tIHRoZSBzZXJ2ZXItc2lkZSBsb2dpYyBcdTIwMTQgc2VlICM5NTEncyBkaXNjdXNzaW9uIG9mIGhvdyB0b1xuLy8gc3RvcCBjb3B5aW5nICNsaW50IGxvZ2ljIGJldHdlZW4gaG9vayBhbmQgc2VydmVyLlxuLy9cbi8vIE9ubHkgYG5vZGU6cGF0aGAgaXMgYWxsb3dlZCBoZXJlIHNvIGVzYnVpbGQgY2FuIGJ1bmRsZSB0aGlzXG4vLyBmaWxlIGludG8gdGhlIGhvb2sgc2NyaXB0IChubyBOb2RlLXNwZWNpZmljIHNlcnZlci1zaWRlXG4vLyBpbXBvcnRzIGxlYWsgaW50byB0aGUgYnVuZGxlKS5cblxuaW1wb3J0IHBhdGggZnJvbSBcIm5vZGU6cGF0aFwiO1xuXG4vKiogUmVqZWN0IHNsdWdzIHRoYXQgd291bGQgZXNjYXBlIGBkYXRhL3dpa2kvcGFnZXMvYCBvbmNlXG4gKiAgam9pbmVkIGJhY2sgaW50byBhIHBhdGgsIG9yIHRoYXQgYXJlIG90aGVyd2lzZSBpbnZhbGlkIGFzXG4gKiAgcGFnZSBmaWxlbmFtZXMuIFRoZSBjaG9rZXBvaW50IG11c3QgZGVmZW5kIGl0c2VsZiBldmVuIHdoZW5cbiAqICBjYWxsZXJzIGRlcml2ZSB0aGUgc2x1ZyBmcm9tIGEgdHJ1c3RlZCBzb3VyY2UgXHUyMDE0IGEgdHlwbyBvclxuICogIGZ1dHVyZSBjYWxsZXIgbWlzdGFrZSBzaG91bGQgZmFpbCBsb3VkLCBub3Qgc2lsZW50bHkgd3JpdGVcbiAqICBvdXRzaWRlIHRoZSB3aWtpIHRyZWUuXG4gKlxuICogIFRoZSBydWxlIGlzIGludGVudGlvbmFsbHkgbmFycm93IFx1MjAxNCBzZXBhcmF0b3JzIC8gYC4uYCAvIE5VTCAvXG4gKiAgZW1wdHkgXHUyMDE0IHNvIGl0IG9ubHkgcmVqZWN0cyB1bmFtYmlndW91cyB2aW9sYXRpb25zLiBBZXN0aGV0aWNcbiAqICBjb25jZXJucyAoZS5nLiBkb3QtcHJlZml4ZWQgZmlsZW5hbWVzKSBhcmUgb3V0IG9mIHNjb3BlOiBhXG4gKiAgcHJlLWV4aXN0aW5nIGBkYXRhL3dpa2kvcGFnZXMvLmZvby5tZGAgc2hvdWxkIHJlbWFpbiB3cml0YWJsZVxuICogIHRocm91Z2ggdGhlIGNob2tlcG9pbnQgKGNvZGV4IHJldmlldyBpdGVyLTIgIzg4MykuICovXG5leHBvcnQgZnVuY3Rpb24gaXNTYWZlU2x1ZyhzbHVnOiBzdHJpbmcpOiBib29sZWFuIHtcbiAgaWYgKHNsdWcubGVuZ3RoID09PSAwKSByZXR1cm4gZmFsc2U7XG4gIGlmIChzbHVnID09PSBcIi5cIiB8fCBzbHVnID09PSBcIi4uXCIpIHJldHVybiBmYWxzZTtcbiAgaWYgKHNsdWcuaW5jbHVkZXMoXCIvXCIpIHx8IHNsdWcuaW5jbHVkZXMoXCJcXFxcXCIpKSByZXR1cm4gZmFsc2U7XG4gIGlmIChzbHVnLmluY2x1ZGVzKFwiXFwwXCIpKSByZXR1cm4gZmFsc2U7XG4gIHJldHVybiB0cnVlO1xufVxuXG4vKiogR2l2ZW4gYW4gYWJzb2x1dGUgcGF0aCBhbmQgdGhlIGFic29sdXRlIGBwYWdlc0RpcmAsIHJldHVybiB0aGVcbiAqICBzbHVnIGlmIGBhYnNQYXRoYCBpcyBhIGRpcmVjdCBgLm1kYCBjaGlsZCBvZiBgcGFnZXNEaXJgLCBlbHNlXG4gKiAgbnVsbC4gUHVyZSBwYXRoLXN0cmluZyBtYXRoIFx1MjAxNCBubyBmcyBJTywgbm8gc3ltbGluayByZXNvbHV0aW9uLlxuICpcbiAqICBDYWxsZXIgcmVzcG9uc2liaWxpdHk6IHBhc3MgYWxyZWFkeS1yZWFscGF0aCdkIHZhbHVlcyBmb3IgYm90aFxuICogIGFyZ3VtZW50cy4gTWl4aW5nIGEgcmVhbHBhdGgnZCBgYWJzUGF0aGAgd2l0aCBhIHN5bWxpbmtlZFxuICogIGBwYWdlc0RpcmAgKG9yIHZpY2UgdmVyc2EpIHNpbGVudGx5IG1pc21hdGNoZXMgYmVjYXVzZVxuICogIGBwYXRoLnJlbGF0aXZlYCBpcyBwbGFpbiBzdHJpbmcgYXJpdGhtZXRpYy4gVGhlIHRyYXAgY2F1c2VkXG4gKiAgIzg4MyByZXZpZXctaXRlci0xIFx1MjAxNCBhIHN5bWxpbmtlZCB3b3Jrc3BhY2Ugc2lsZW50bHkgcm91dGVkXG4gKiAgd2lraSB3cml0ZXMgdGhyb3VnaCB0aGUgZ2VuZXJpYyB3cml0ZXIuICovXG5leHBvcnQgZnVuY3Rpb24gd2lraVNsdWdGcm9tQWJzUGF0aChhYnNQYXRoOiBzdHJpbmcsIHBhZ2VzRGlyOiBzdHJpbmcpOiBzdHJpbmcgfCBudWxsIHtcbiAgY29uc3QgcmVsID0gcGF0aC5yZWxhdGl2ZShwYWdlc0RpciwgYWJzUGF0aCk7XG4gIGlmIChyZWwubGVuZ3RoID09PSAwKSByZXR1cm4gbnVsbDtcbiAgaWYgKHBhdGguaXNBYnNvbHV0ZShyZWwpKSByZXR1cm4gbnVsbDtcbiAgLy8gRGlyZWN0IGNoaWxkIG9ubHkgXHUyMDE0IG5vIG5lc3RlZCBsYXlvdXQgdG9kYXkuIEFueSBzZXBhcmF0b3JcbiAgLy8gbWVhbnMgdGhlIHBhdGggZWl0aGVyIGVzY2FwZXMgKGAuLi9zZWNyZXQubWRgKSBvciBkZXNjZW5kc1xuICAvLyAoYHN1YmRpci9mb28ubWRgKS4gQSBsaXRlcmFsIHBhZ2UgbmFtZSBsaWtlIGAuLmZvby5tZGAgaXMgYVxuICAvLyBzaW5nbGUgc2VnbWVudCB3aXRob3V0IGEgc2VwYXJhdG9yIGFuZCBpcyBhbGxvd2VkIChjb2RleFxuICAvLyBpdGVyLTMgIzg4MyBcdTIwMTQgdGhlIHByaW9yIGBzdGFydHNXaXRoKFwiLi5cIilgIHJ1bGUgd3JvbmdseVxuICAvLyByZWplY3RlZCBpdCkuXG4gIGlmIChyZWwuaW5jbHVkZXMocGF0aC5zZXApKSByZXR1cm4gbnVsbDtcbiAgaWYgKCFyZWwuZW5kc1dpdGgoXCIubWRcIikpIHJldHVybiBudWxsO1xuICBjb25zdCBzbHVnID0gcmVsLnNsaWNlKDAsIC1cIi5tZFwiLmxlbmd0aCk7XG4gIGlmICghaXNTYWZlU2x1ZyhzbHVnKSkgcmV0dXJuIG51bGw7XG4gIHJldHVybiBzbHVnO1xufVxuIl0sCiAgIm1hcHBpbmdzIjogIjs7O0FBbUJBLFNBQVMsb0JBQW9CO0FBQzdCLFNBQVMsZUFBZTtBQUN4QixPQUFPQSxXQUFV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
@@ -0,0 +1,135 @@
1
+ // PostToolUse hook source — runs after every Claude CLI Write /
2
+ // Edit tool call. Detects writes that target a wiki page file
3
+ // and POSTs `{ slug, sessionId? }` to the parent server's
4
+ // `/api/wiki/internal/snapshot` endpoint so the snapshot pipeline
5
+ // (#763 PR 2) records the new state.
6
+ //
7
+ // THIS FILE IS THE SOURCE OF TRUTH. Edits here, then run
8
+ // `yarn build:hooks` (or `yarn build`) to regenerate
9
+ // `./snapshot.mjs`. The bundled `.mjs` is committed to git so
10
+ // `provision.ts` can read it at server startup without invoking
11
+ // esbuild on the runtime path. CI runs `yarn build:hooks &&
12
+ // git diff --exit-code` to catch a stale bundle.
13
+ //
14
+ // The hook executes inside Claude CLI's process space — must be
15
+ // self-contained ESM. esbuild bundles imports from the shared
16
+ // `src/lib/wiki-page/slug.ts` module so the hook and the
17
+ // server-side `classifyAsWikiPage` agree on what counts as a
18
+ // wiki page (no copy-paste drift).
19
+
20
+ import { readFileSync } from "node:fs";
21
+ import { homedir } from "node:os";
22
+ import path from "node:path";
23
+ import { wikiSlugFromAbsPath } from "../../../../src/lib/wiki-page/slug.js";
24
+
25
+ // Workspace layout matches `WORKSPACE_DIRS.wikiPages`. We hard-
26
+ // code "data/wiki/pages" rather than importing the constants
27
+ // because the hook needs to stay independent of the server's
28
+ // runtime config — the bundle must work even when the server
29
+ // process isn't running yet (e.g. during a startup race).
30
+ const WORKSPACE_ROOT = path.join(homedir(), "mulmoclaude");
31
+ const WIKI_PAGES_DIR = path.join(WORKSPACE_ROOT, "data", "wiki", "pages");
32
+ const TOKEN_PATH = path.join(WORKSPACE_ROOT, ".session-token");
33
+ const PORT_PATH = path.join(WORKSPACE_ROOT, ".server-port");
34
+ // In Docker mode the parent server runs on the host's 127.0.0.1
35
+ // which the container can't reach via plain loopback. The Docker
36
+ // spawn plumbing sets MULMOCLAUDE_HOST=host.docker.internal so
37
+ // fetch() resolves to the host server. Outside Docker (or when
38
+ // the var is unset) we fall back to the loopback address.
39
+ const SERVER_HOST = process.env.MULMOCLAUDE_HOST ?? "127.0.0.1";
40
+
41
+ interface HookPayload {
42
+ tool_input?: { file_path?: unknown };
43
+ tool_response?: { filePath?: unknown };
44
+ session_id?: unknown;
45
+ }
46
+
47
+ async function readStdin(): Promise<HookPayload | null> {
48
+ const chunks: Buffer[] = [];
49
+ for await (const chunk of process.stdin) {
50
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
51
+ }
52
+ const raw = Buffer.concat(chunks).toString("utf-8");
53
+ if (!raw.trim()) return null;
54
+ try {
55
+ return JSON.parse(raw) as HookPayload;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ function extractFilePath(payload: HookPayload): string | null {
62
+ // Claude CLI hook payload: { tool_name, tool_input, tool_response, ... }
63
+ // Different tools surface the path under different keys —
64
+ // check both shapes defensively.
65
+ const fromInput = payload.tool_input?.file_path;
66
+ if (typeof fromInput === "string") return fromInput;
67
+ const fromResponse = payload.tool_response?.filePath;
68
+ if (typeof fromResponse === "string") return fromResponse;
69
+ return null;
70
+ }
71
+
72
+ function readTokenSafe(): string {
73
+ try {
74
+ return readFileSync(TOKEN_PATH, "utf-8").trim();
75
+ } catch {
76
+ return "";
77
+ }
78
+ }
79
+
80
+ function readPortSafe(): number | null {
81
+ try {
82
+ const raw = readFileSync(PORT_PATH, "utf-8").trim();
83
+ const port = Number.parseInt(raw, 10);
84
+ return Number.isInteger(port) && port > 0 && port < 65536 ? port : null;
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ async function main(): Promise<void> {
91
+ const payload = await readStdin();
92
+ if (!payload) return;
93
+
94
+ const filePath = extractFilePath(payload);
95
+ if (!filePath) return;
96
+ const slug = wikiSlugFromAbsPath(filePath, WIKI_PAGES_DIR);
97
+ if (slug === null) return;
98
+
99
+ const token = readTokenSafe();
100
+ const port = readPortSafe();
101
+ if (!token || port === null) return; // server isn't reachable; silent no-op
102
+
103
+ // Prefer our chat-session id from the spawn env (#963) — the
104
+ // server's session store keys by chatSessionId, not by Claude
105
+ // CLI's internal session_id, so the toolResult publish on the
106
+ // server side only matches when we forward our own id. Fall back
107
+ // to Claude CLI's `session_id` (still useful as a tracing token
108
+ // in the snapshot frontmatter) when the env var is absent — e.g.
109
+ // an older mulmoclaude server spawning a newer hook bundle.
110
+ const envChatSessionId = process.env.MULMOCLAUDE_CHAT_SESSION_ID;
111
+ const payloadSessionId = typeof payload.session_id === "string" && payload.session_id.length > 0 ? payload.session_id : undefined;
112
+ const sessionId = envChatSessionId && envChatSessionId.length > 0 ? envChatSessionId : payloadSessionId;
113
+ const body = sessionId === undefined ? { slug } : { slug, sessionId };
114
+
115
+ try {
116
+ await fetch(`http://${SERVER_HOST}:${port}/api/wiki/internal/snapshot`, {
117
+ method: "POST",
118
+ headers: {
119
+ "Content-Type": "application/json",
120
+ Authorization: `Bearer ${token}`,
121
+ },
122
+ body: JSON.stringify(body),
123
+ });
124
+ } catch {
125
+ // Network / server down — drop silently. The wiki page write
126
+ // itself already succeeded; missing one snapshot is recoverable
127
+ // (the next save will record the next state).
128
+ }
129
+ }
130
+
131
+ main().catch(() => {
132
+ // Hooks run synchronously to the LLM tool flow — never throw.
133
+ // A busted hook would make `Write` itself look like it failed
134
+ // to the LLM, which is much worse than a missing snapshot.
135
+ });
@@ -0,0 +1,181 @@
1
+ // Workspace provisioning for the LLM wiki-write hook (#763 PR 2 prereq).
2
+ //
3
+ // At server startup, ensure the workspace's `.claude/settings.json`
4
+ // has a `PostToolUse` hook on Write|Edit that runs our snapshot
5
+ // script. The script itself is rewritten from the source-of-truth
6
+ // constant in `hookScript.ts` on every startup so updates ship via
7
+ // the normal mulmoclaude code path — no per-workspace migration.
8
+ //
9
+ // Idempotent: running provisioning twice produces the same on-disk
10
+ // state. Existing user-supplied keys in `settings.json` are
11
+ // preserved; we only touch the one PostToolUse entry that we own
12
+ // (identified by a `mulmoclaudeWikiHistory: true` marker on the
13
+ // hook descriptor).
14
+
15
+ import { readFileSync } from "node:fs";
16
+ import path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { readTextOrNull } from "../../utils/files/safe.js";
19
+ import { writeFileAtomic } from "../../utils/files/atomic.js";
20
+ import { workspacePath as defaultWorkspacePath } from "../workspace.js";
21
+ import { log } from "../../system/logger/index.js";
22
+
23
+ // The esbuild-built bundle is the source of truth for the hook
24
+ // script written into `<workspace>/.claude/hooks/`. Source TS is
25
+ // `hook/snapshot.ts`; `yarn build:hooks` (chained from
26
+ // `yarn build`) regenerates `hook/snapshot.mjs`, and CI fails
27
+ // when the committed bundle drifts from the source. The read
28
+ // happens inside `provisionWikiHistoryHook` (not at module load)
29
+ // so a missing / unreadable bundle degrades to a logged warning
30
+ // without breaking server startup — provisioning is best-effort
31
+ // and the rest of the server still functions (codex review
32
+ // iter-1 #955).
33
+ const HOOK_BUNDLE_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "hook", "snapshot.mjs");
34
+
35
+ function readHookBundle(): string | null {
36
+ try {
37
+ return readFileSync(HOOK_BUNDLE_PATH, "utf-8");
38
+ } catch (err) {
39
+ log.warn("wiki-history", "hook bundle unreadable, skipping provisioning", {
40
+ bundlePath: HOOK_BUNDLE_PATH,
41
+ error: err instanceof Error ? err.message : String(err),
42
+ });
43
+ return null;
44
+ }
45
+ }
46
+
47
+ const SETTINGS_REL = path.join(".claude", "settings.json");
48
+ const HOOK_SCRIPT_REL = path.join(".claude", "hooks", "wiki-snapshot.mjs");
49
+ // Forward-slash form for the shell command field — settings.json
50
+ // is read by Claude CLI, and a hook command is interpreted by a
51
+ // POSIX shell (Mac / Linux / inside the Docker container). We never
52
+ // embed the workspace root directly: the same settings.json is
53
+ // bind-mounted into the container, so a host-absolute path
54
+ // (`/Users/.../mulmoclaude/...`) silently fails inside the container
55
+ // where the workspace lives at `/home/node/mulmoclaude/...`. Claude
56
+ // CLI exports `CLAUDE_PROJECT_DIR` to hooks, which resolves
57
+ // correctly in both contexts.
58
+ const HOOK_SCRIPT_REL_POSIX = ".claude/hooks/wiki-snapshot.mjs";
59
+ const HOOK_COMMAND = `node "$CLAUDE_PROJECT_DIR/${HOOK_SCRIPT_REL_POSIX}"`;
60
+ const OWNER_MARKER = "mulmoclaudeWikiHistory";
61
+
62
+ interface HookCommandEntry {
63
+ type: "command";
64
+ command: string;
65
+ [key: string]: unknown;
66
+ }
67
+
68
+ interface HookMatcher {
69
+ matcher?: string;
70
+ hooks?: HookCommandEntry[];
71
+ [key: string]: unknown;
72
+ }
73
+
74
+ interface SettingsShape {
75
+ hooks?: {
76
+ PostToolUse?: HookMatcher[];
77
+ [key: string]: HookMatcher[] | undefined;
78
+ };
79
+ [key: string]: unknown;
80
+ }
81
+
82
+ export interface ProvisionOptions {
83
+ workspaceRoot?: string;
84
+ }
85
+
86
+ /** Ensure the hook script + `.claude/settings.json` are up to date.
87
+ * Safe to call on every startup. Logs a one-line info on first
88
+ * install, debug-only on subsequent no-op runs. Skips quietly
89
+ * (with a warning) when the bundled hook script can't be read
90
+ * — provisioning is best-effort and the rest of the server is
91
+ * still functional. */
92
+ export async function provisionWikiHistoryHook(opts: ProvisionOptions = {}): Promise<void> {
93
+ const bundle = readHookBundle();
94
+ if (bundle === null) return;
95
+
96
+ const root = opts.workspaceRoot ?? defaultWorkspacePath;
97
+ const scriptPath = path.join(root, HOOK_SCRIPT_REL);
98
+ const settingsPath = path.join(root, SETTINGS_REL);
99
+
100
+ await writeHookScript(scriptPath, bundle);
101
+ const changed = await mergeHookIntoSettings(settingsPath);
102
+ if (changed) {
103
+ log.info("wiki-history", "provisioned wiki-snapshot hook", { settingsPath, scriptPath });
104
+ }
105
+ }
106
+
107
+ async function writeHookScript(absPath: string, bundle: string): Promise<void> {
108
+ // Always overwrite — the esbuild bundle on disk is the source of
109
+ // truth, and rewriting on every startup means a mulmoclaude
110
+ // update propagates without per-workspace migration.
111
+ await writeFileAtomic(absPath, bundle, { mode: 0o700 });
112
+ }
113
+
114
+ async function mergeHookIntoSettings(settingsPath: string): Promise<boolean> {
115
+ const existingRaw = await readTextOrNull(settingsPath);
116
+ const existing: SettingsShape = existingRaw ? safeParse(existingRaw) : {};
117
+
118
+ const desiredHook: HookCommandEntry = {
119
+ type: "command",
120
+ command: HOOK_COMMAND,
121
+ [OWNER_MARKER]: true,
122
+ };
123
+
124
+ const next = upsertOurHook(existing, desiredHook);
125
+ const nextRaw = `${JSON.stringify(next, null, 2)}\n`;
126
+ if (existingRaw === nextRaw) return false;
127
+
128
+ // mode 0644 is fine — settings.json doesn't carry secrets, and
129
+ // matching the user's own conventions for `~/.claude/settings.json`
130
+ // (which is typically world-readable) avoids permission surprises.
131
+ await writeFileAtomic(settingsPath, nextRaw);
132
+ return true;
133
+ }
134
+
135
+ function safeParse(raw: string): SettingsShape {
136
+ try {
137
+ const parsed: unknown = JSON.parse(raw);
138
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
139
+ return parsed as SettingsShape;
140
+ }
141
+ } catch {
142
+ // Fall through — corrupted settings get rebuilt with our entry only.
143
+ }
144
+ return {};
145
+ }
146
+
147
+ function upsertOurHook(settings: SettingsShape, desiredHook: HookCommandEntry): SettingsShape {
148
+ const hooks = settings.hooks ?? {};
149
+ // Normalise to [] when an unexpected shape sits under `PostToolUse`
150
+ // (object, string, etc.). The `?? []` form only handles
151
+ // null/undefined; without this guard, `findIndex` throws and the
152
+ // hook silently fails to install for the session (codex iter-2 #917).
153
+ const rawPostToolUse = hooks.PostToolUse;
154
+ const postToolUse = Array.isArray(rawPostToolUse) ? rawPostToolUse : [];
155
+
156
+ const ownedIndex = postToolUse.findIndex((entry) => entryHasOwnedHook(entry));
157
+ const desiredEntry: HookMatcher = {
158
+ matcher: "Write|Edit",
159
+ hooks: [desiredHook],
160
+ };
161
+
162
+ const nextPostToolUse = [...postToolUse];
163
+ if (ownedIndex === -1) {
164
+ nextPostToolUse.push(desiredEntry);
165
+ } else {
166
+ nextPostToolUse[ownedIndex] = desiredEntry;
167
+ }
168
+
169
+ return {
170
+ ...settings,
171
+ hooks: {
172
+ ...hooks,
173
+ PostToolUse: nextPostToolUse,
174
+ },
175
+ };
176
+ }
177
+
178
+ function entryHasOwnedHook(entry: HookMatcher): boolean {
179
+ if (!Array.isArray(entry.hooks)) return false;
180
+ return entry.hooks.some((hook) => hook[OWNER_MARKER] === true);
181
+ }