mulmoclaude 0.5.3 → 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 (483) hide show
  1. package/README.md +17 -4
  2. package/bin/mulmoclaude.js +46 -15
  3. package/client/assets/chunk-CernVdwh.js +1 -0
  4. package/client/assets/chunk-D8eiyYIV-C1eAZMzz.js +1 -0
  5. package/client/assets/html2canvas-CDGcmOD3-BbPeutDg.js +5 -0
  6. package/client/assets/index-BbgSjFQ8.js +4968 -0
  7. package/client/assets/index-ECD0lgIv.css +2 -0
  8. package/client/assets/{index.es-D4YyL_Dg-BgT6a3Nd.js → index.es-DqtpmBm8-DJdTPdnc.js} +5 -5
  9. package/client/assets/material-symbols-outlined-BLDfUw-_.woff2 +0 -0
  10. package/client/assets/runtime-protocol-vue-6WYa8hAs.js +1 -0
  11. package/client/assets/runtime-vue-BVUzgYGA.js +1 -0
  12. package/client/assets/typeof-DBp4T-Ny-C2xoZtcz.js +1 -0
  13. package/client/assets/vue-1e_vz2LW.js +1 -0
  14. package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +4 -0
  15. package/client/index.html +33 -2
  16. package/package.json +17 -17
  17. package/server/accounting/accountNormalize.ts +32 -0
  18. package/server/accounting/defaultAccounts.ts +87 -0
  19. package/server/accounting/eventPublisher.ts +51 -0
  20. package/server/accounting/journal.ts +252 -0
  21. package/server/accounting/openingBalances.ts +114 -0
  22. package/server/accounting/report.ts +237 -0
  23. package/server/accounting/service.ts +718 -0
  24. package/server/accounting/snapshotCache.ts +333 -0
  25. package/server/accounting/timeSeries.ts +265 -0
  26. package/server/accounting/types.ts +148 -0
  27. package/server/agent/activeTools.ts +128 -0
  28. package/server/agent/attachmentConverter.ts +10 -5
  29. package/server/agent/backend/claude-code.ts +8 -2
  30. package/server/agent/backend/types.ts +1 -1
  31. package/server/agent/config.ts +101 -31
  32. package/server/agent/index.ts +45 -33
  33. package/server/agent/mcp-server.ts +146 -69
  34. package/server/agent/mcp-tools/index.ts +1 -5
  35. package/server/agent/mcp-tools/notify.ts +2 -22
  36. package/server/agent/mcp-tools/x.ts +0 -4
  37. package/server/agent/mcpHealth.ts +168 -0
  38. package/server/agent/plugin-names.ts +20 -77
  39. package/server/agent/prompt.ts +259 -51
  40. package/server/agent/resumeFailover.ts +1 -1
  41. package/server/agent/stream.ts +0 -1
  42. package/server/api/auth/bearerAuth.ts +5 -5
  43. package/server/api/csrfGuard.ts +1 -1
  44. package/server/api/routes/accounting.ts +366 -0
  45. package/server/api/routes/agent.ts +509 -46
  46. package/server/api/routes/attachment.ts +104 -0
  47. package/server/api/routes/chart.ts +2 -1
  48. package/server/api/routes/config.ts +12 -12
  49. package/server/api/routes/files.ts +105 -48
  50. package/server/api/routes/image.ts +70 -25
  51. package/server/api/routes/journal.ts +35 -0
  52. package/server/api/routes/mulmo-script.ts +358 -118
  53. package/server/api/routes/mulmoScriptValidate.ts +1 -1
  54. package/server/api/routes/news.ts +1 -1
  55. package/server/api/routes/notifications.ts +92 -22
  56. package/server/api/routes/notifier.ts +98 -0
  57. package/server/api/routes/pdf.ts +188 -48
  58. package/server/api/routes/plugins.ts +34 -14
  59. package/server/api/routes/presentHtml.ts +58 -3
  60. package/server/api/routes/roles.ts +1 -8
  61. package/server/api/routes/runtime-plugin.ts +224 -0
  62. package/server/api/routes/scheduler.ts +7 -5
  63. package/server/api/routes/schedulerHandlers.ts +1 -1
  64. package/server/api/routes/schedulerTasks.ts +8 -7
  65. package/server/api/routes/sessions.ts +234 -121
  66. package/server/api/routes/skills.ts +56 -51
  67. package/server/api/routes/sources.ts +52 -45
  68. package/server/api/routes/translation.ts +44 -0
  69. package/server/api/routes/wiki/frontmatter.ts +13 -65
  70. package/server/api/routes/wiki/history.ts +261 -0
  71. package/server/api/routes/wiki/pageIndex.ts +1 -1
  72. package/server/api/routes/wiki.ts +50 -26
  73. package/server/events/file-change.ts +83 -0
  74. package/server/events/notifications.ts +247 -91
  75. package/server/events/pub-sub/index.ts +1 -1
  76. package/server/events/relay-client.ts +5 -5
  77. package/server/events/scheduler-adapter.ts +2 -2
  78. package/server/events/session-store/index.ts +110 -22
  79. package/server/events/task-manager/index.ts +10 -9
  80. package/server/index.ts +509 -33
  81. package/server/notifier/engine.ts +419 -0
  82. package/server/notifier/legacy-adapters.ts +76 -0
  83. package/server/notifier/runtime-api.ts +74 -0
  84. package/server/notifier/store.ts +70 -0
  85. package/server/notifier/types.ts +121 -0
  86. package/server/plugins/dev-loader.ts +171 -0
  87. package/server/plugins/dev-watcher.ts +150 -0
  88. package/server/plugins/diagnostics.ts +188 -0
  89. package/server/plugins/preset-list.ts +52 -0
  90. package/server/plugins/preset-loader.ts +112 -0
  91. package/server/plugins/runtime-chat-api.ts +38 -0
  92. package/server/plugins/runtime-loader.ts +430 -0
  93. package/server/plugins/runtime-registry.ts +112 -0
  94. package/server/plugins/runtime-tasks-api.ts +50 -0
  95. package/server/plugins/runtime.ts +378 -0
  96. package/server/services/translation/cache.ts +72 -0
  97. package/server/services/translation/index.ts +106 -0
  98. package/server/services/translation/llm.ts +140 -0
  99. package/server/services/translation/types.ts +35 -0
  100. package/server/system/credentials.ts +13 -2
  101. package/server/system/env.ts +6 -1
  102. package/server/system/logger/formatters.ts +46 -4
  103. package/server/system/logger/index.ts +4 -4
  104. package/server/system/logger/sinks.ts +26 -5
  105. package/server/system/logger/types.ts +2 -2
  106. package/server/utils/dev-plugin-args.d.mts +11 -0
  107. package/server/utils/dev-plugin-args.mjs +43 -0
  108. package/server/utils/errors.ts +13 -4
  109. package/server/utils/files/accounting-io.ts +295 -0
  110. package/server/utils/files/atomic.ts +17 -49
  111. package/server/utils/files/attachment-store.ts +182 -0
  112. package/server/utils/files/html-io.ts +1 -7
  113. package/server/utils/files/html-store.ts +19 -0
  114. package/server/utils/files/image-store.ts +20 -22
  115. package/server/utils/files/index.ts +5 -15
  116. package/server/utils/files/journal-io.ts +7 -35
  117. package/server/utils/files/json.ts +2 -29
  118. package/server/utils/files/markdown-image-fill.ts +6 -37
  119. package/server/utils/files/markdown-store.ts +6 -21
  120. package/server/utils/files/naming.ts +3 -39
  121. package/server/utils/files/plugins-io.ts +100 -0
  122. package/server/utils/files/reference-dirs-io.ts +1 -9
  123. package/server/utils/files/roles-io.ts +2 -10
  124. package/server/utils/files/safe.ts +17 -19
  125. package/server/utils/files/scheduler-io.ts +1 -7
  126. package/server/utils/files/scheduler-overrides-io.ts +3 -12
  127. package/server/utils/files/session-io.ts +21 -30
  128. package/server/utils/files/spreadsheet-store.ts +9 -22
  129. package/server/utils/files/translation-io.ts +46 -0
  130. package/server/utils/files/user-tasks-io.ts +1 -7
  131. package/server/utils/files/workspace-io.ts +3 -79
  132. package/server/utils/gemini.ts +33 -11
  133. package/server/utils/html/htmlArtifactSplicer.ts +41 -0
  134. package/server/utils/markdown/frontmatter.ts +112 -0
  135. package/server/utils/regex.ts +56 -0
  136. package/server/utils/router.ts +41 -0
  137. package/server/utils/slug.ts +5 -3
  138. package/server/utils/time.ts +12 -0
  139. package/server/workspace/chat-index/indexer.ts +15 -2
  140. package/server/workspace/chat-index/summarizer.ts +1 -1
  141. package/server/workspace/custom-dirs.ts +1 -1
  142. package/server/workspace/helps/gemini.md +1 -1
  143. package/server/workspace/helps/guide.md +61 -0
  144. package/server/workspace/helps/index.md +4 -0
  145. package/server/workspace/helps/presenthtml.md +80 -0
  146. package/server/workspace/helps/sandbox.md +7 -0
  147. package/server/workspace/helps/storyteller.md +101 -0
  148. package/server/workspace/helps/telegram.md +1 -0
  149. package/server/workspace/helps/wiki.md +9 -7
  150. package/server/workspace/journal/archivist-cli.ts +7 -33
  151. package/server/workspace/journal/archivist-schemas.ts +5 -43
  152. package/server/workspace/journal/dailyPass.ts +34 -187
  153. package/server/workspace/journal/diff.ts +3 -28
  154. package/server/workspace/journal/index.ts +10 -81
  155. package/server/workspace/journal/indexFile.ts +3 -24
  156. package/server/workspace/journal/latestDaily.ts +51 -0
  157. package/server/workspace/journal/memoryExtractor.ts +4 -20
  158. package/server/workspace/journal/optimizationPass.ts +4 -21
  159. package/server/workspace/journal/paths.ts +4 -23
  160. package/server/workspace/journal/state.ts +6 -29
  161. package/server/workspace/memory/io.ts +213 -0
  162. package/server/workspace/memory/llm-classifier.ts +158 -0
  163. package/server/workspace/memory/migrate.ts +263 -0
  164. package/server/workspace/memory/run.ts +84 -0
  165. package/server/workspace/memory/topic-cluster.ts +218 -0
  166. package/server/workspace/memory/topic-detect.ts +67 -0
  167. package/server/workspace/memory/topic-index-hook.ts +128 -0
  168. package/server/workspace/memory/topic-io.ts +180 -0
  169. package/server/workspace/memory/topic-migrate.ts +248 -0
  170. package/server/workspace/memory/topic-run.ts +172 -0
  171. package/server/workspace/memory/topic-swap.ts +135 -0
  172. package/server/workspace/memory/topic-types.ts +142 -0
  173. package/server/workspace/memory/types.ts +83 -0
  174. package/server/workspace/news/reader.ts +4 -5
  175. package/server/workspace/paths.ts +124 -47
  176. package/server/workspace/roles.ts +2 -11
  177. package/server/workspace/skills/parser.ts +38 -55
  178. package/server/workspace/skills/user-tasks.ts +1 -2
  179. package/server/workspace/skills-preset/mc-library/SKILL.md +188 -0
  180. package/server/workspace/skills-preset.ts +196 -0
  181. package/server/workspace/sources/fetchers/githubIssues.ts +13 -11
  182. package/server/workspace/sources/fetchers/index.ts +1 -1
  183. package/server/workspace/sources/fetchers/rssParser.ts +1 -1
  184. package/server/workspace/sources/pipeline/index.ts +2 -2
  185. package/server/workspace/sources/pipeline/notify.ts +3 -3
  186. package/server/workspace/sources/pipeline/write.ts +2 -2
  187. package/server/workspace/sources/registry.ts +39 -61
  188. package/server/workspace/sources/robots.ts +1 -1
  189. package/server/workspace/tool-trace/classify.ts +2 -1
  190. package/server/workspace/tool-trace/index.ts +1 -1
  191. package/server/workspace/tool-trace/writeSearch.ts +6 -1
  192. package/server/workspace/wiki-backlinks/index.ts +19 -7
  193. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +1 -0
  194. package/server/workspace/wiki-history/hook/snapshot.mjs +98 -0
  195. package/server/workspace/wiki-history/hook/snapshot.ts +135 -0
  196. package/server/workspace/wiki-history/provision.ts +181 -0
  197. package/server/workspace/wiki-pages/io.ts +217 -0
  198. package/server/workspace/wiki-pages/snapshot.ts +380 -0
  199. package/server/workspace/workspace.ts +75 -13
  200. package/src/App.vue +115 -40
  201. package/src/_runtime/protocol-vue.ts +21 -0
  202. package/src/_runtime/vue.ts +22 -0
  203. package/src/components/ChatInput.vue +14 -10
  204. package/src/components/CopyChatButton.vue +76 -0
  205. package/src/components/FileContentRenderer.vue +67 -14
  206. package/src/components/FileTree.vue +2 -2
  207. package/src/components/FilesView.vue +17 -1
  208. package/src/components/NewsView.vue +16 -2
  209. package/src/components/NotificationBell.vue +320 -93
  210. package/src/components/PageChatComposer.vue +5 -4
  211. package/src/components/PluginLauncher.vue +42 -6
  212. package/src/components/PluginScopedRoot.vue +87 -0
  213. package/src/components/RoleSelector.vue +12 -1
  214. package/src/components/RolesView.vue +562 -0
  215. package/src/components/SentAttachmentChip.vue +102 -0
  216. package/src/components/SessionHistoryPanel.vue +109 -20
  217. package/src/components/SessionRoleIcon.vue +7 -4
  218. package/src/components/SessionSidebar.vue +20 -7
  219. package/src/components/SessionTabBar.vue +1 -1
  220. package/src/components/SettingsMcpTab.vue +4 -4
  221. package/src/components/SettingsModal.vue +2 -0
  222. package/src/components/SidebarHeader.vue +16 -5
  223. package/src/components/SourcesManager.vue +23 -9
  224. package/src/components/SourcesView.vue +1 -1
  225. package/src/components/StackView.vue +102 -6
  226. package/src/components/SuggestionsPanel.vue +105 -16
  227. package/src/components/SystemFileBanner.vue +1 -1
  228. package/src/components/TodoExplorer.vue +4 -5
  229. package/src/components/todo/TodoAddDialog.vue +2 -3
  230. package/src/components/todo/TodoEditDialog.vue +1 -2
  231. package/src/components/todo/TodoEditPanel.vue +2 -3
  232. package/src/components/todo/TodoKanbanView.vue +8 -5
  233. package/src/components/todo/TodoListView.vue +3 -5
  234. package/src/components/todo/TodoTableView.vue +7 -5
  235. package/src/composables/useAccountingChannel.ts +58 -0
  236. package/src/composables/useActiveSession.ts +4 -25
  237. package/src/composables/useAppApi.ts +6 -44
  238. package/src/composables/useClipboardCopy.ts +3 -20
  239. package/src/composables/useContentDisplay.ts +33 -2
  240. package/src/composables/useDevPluginReload.ts +23 -0
  241. package/src/composables/useDynamicFavicon.ts +5 -31
  242. package/src/composables/useEventListeners.ts +0 -20
  243. package/src/composables/useExpandedDirs.ts +4 -15
  244. package/src/composables/useFaviconState.ts +12 -46
  245. package/src/composables/useFileChange.ts +53 -0
  246. package/src/composables/useFreshPluginData.ts +6 -43
  247. package/src/composables/useHealth.ts +14 -43
  248. package/src/composables/useImageErrorRepair.ts +104 -0
  249. package/src/composables/useLatestDaily.ts +40 -0
  250. package/src/composables/useMarkdownDoc.ts +39 -0
  251. package/src/composables/useMarkdownLinkHandler.ts +1 -1
  252. package/src/composables/useMcpTools.ts +3 -16
  253. package/src/composables/useNotifications.ts +138 -112
  254. package/src/composables/usePdfDownload.ts +17 -3
  255. package/src/composables/usePendingCalls.ts +8 -26
  256. package/src/composables/usePluginErrorBoundary.ts +68 -0
  257. package/src/composables/usePubSub.ts +9 -17
  258. package/src/composables/useRunElapsed.ts +5 -22
  259. package/src/composables/useSandboxStatus.ts +4 -20
  260. package/src/composables/useSessionDerived.ts +7 -15
  261. package/src/composables/useSessionHistory.ts +70 -29
  262. package/src/composables/useSessionSync.ts +25 -3
  263. package/src/composables/useSkillsList.ts +59 -0
  264. package/src/composables/useTranslatedQueries.ts +109 -0
  265. package/src/config/apiRoutes.ts +181 -80
  266. package/src/config/historyFilters.ts +5 -3
  267. package/src/config/hostEvents.ts +17 -0
  268. package/src/config/mcpCatalog.ts +277 -5
  269. package/src/config/pubsubChannels.ts +134 -12
  270. package/src/config/roles.ts +212 -147
  271. package/src/config/systemFileDescriptors.ts +5 -5
  272. package/src/config/toolNames.ts +52 -30
  273. package/src/config/workspacePaths.ts +26 -2
  274. package/src/lang/de.ts +483 -27
  275. package/src/lang/en.ts +448 -27
  276. package/src/lang/es.ts +474 -27
  277. package/src/lang/fr.ts +476 -27
  278. package/src/lang/ja.ts +465 -27
  279. package/src/lang/ko.ts +466 -27
  280. package/src/lang/pt-BR.ts +473 -27
  281. package/src/lang/zh.ts +463 -27
  282. package/src/lib/vue-i18n.ts +1 -1
  283. package/src/lib/wiki-page/slug.ts +66 -0
  284. package/src/main.ts +85 -0
  285. package/src/plugins/_extras.ts +58 -0
  286. package/src/plugins/_generated/metas.ts +42 -0
  287. package/src/plugins/_generated/registrations.ts +44 -0
  288. package/src/plugins/_generated/server-bindings.ts +47 -0
  289. package/src/plugins/accounting/Preview.vue +106 -0
  290. package/src/plugins/accounting/View.vue +632 -0
  291. package/src/plugins/accounting/actions.ts +34 -0
  292. package/src/plugins/accounting/api.ts +301 -0
  293. package/src/plugins/accounting/components/AccountEditor.vue +250 -0
  294. package/src/plugins/accounting/components/AccountRow.vue +50 -0
  295. package/src/plugins/accounting/components/AccountsList.vue +102 -0
  296. package/src/plugins/accounting/components/AccountsModal.vue +300 -0
  297. package/src/plugins/accounting/components/BalanceSheet.vue +186 -0
  298. package/src/plugins/accounting/components/BookSettings.vue +284 -0
  299. package/src/plugins/accounting/components/BookSwitcher.vue +78 -0
  300. package/src/plugins/accounting/components/DateRangePicker.vue +140 -0
  301. package/src/plugins/accounting/components/JournalEntryForm.vue +504 -0
  302. package/src/plugins/accounting/components/JournalList.vue +553 -0
  303. package/src/plugins/accounting/components/Ledger.vue +206 -0
  304. package/src/plugins/accounting/components/NewBookForm.vue +211 -0
  305. package/src/plugins/accounting/components/OpeningBalancesForm.vue +271 -0
  306. package/src/plugins/accounting/components/ProfitLoss.vue +160 -0
  307. package/src/plugins/accounting/components/accountDraft.ts +13 -0
  308. package/src/plugins/accounting/components/accountNumbering.ts +103 -0
  309. package/src/plugins/accounting/components/accountValidation.ts +75 -0
  310. package/src/plugins/accounting/components/useLatestRequest.ts +44 -0
  311. package/src/plugins/accounting/countries.ts +158 -0
  312. package/src/plugins/accounting/currencies.ts +64 -0
  313. package/src/plugins/accounting/dates.ts +51 -0
  314. package/src/plugins/accounting/definition.ts +199 -0
  315. package/src/plugins/accounting/fiscalYear.ts +136 -0
  316. package/src/plugins/accounting/index.ts +49 -0
  317. package/src/plugins/accounting/meta.ts +91 -0
  318. package/src/plugins/accounting/timeSeriesEnums.ts +16 -0
  319. package/src/plugins/api.ts +125 -0
  320. package/src/plugins/canvas/View.vue +38 -28
  321. package/src/plugins/canvas/definition.ts +10 -8
  322. package/src/plugins/canvas/index.ts +15 -8
  323. package/src/plugins/canvas/meta.ts +12 -0
  324. package/src/plugins/chart/Preview.vue +1 -1
  325. package/src/plugins/chart/View.vue +2 -2
  326. package/src/plugins/chart/definition.ts +12 -2
  327. package/src/plugins/chart/index.ts +15 -7
  328. package/src/plugins/chart/meta.ts +18 -0
  329. package/src/plugins/editImages/definition.ts +44 -0
  330. package/src/plugins/editImages/index.ts +43 -0
  331. package/src/plugins/editImages/meta.ts +5 -0
  332. package/src/plugins/generateImage/View.vue +3 -1
  333. package/src/plugins/generateImage/definition.ts +2 -0
  334. package/src/plugins/generateImage/index.ts +13 -5
  335. package/src/plugins/generateImage/meta.ts +5 -0
  336. package/src/plugins/index.ts +35 -0
  337. package/src/plugins/manageRoles/Preview.vue +7 -4
  338. package/src/plugins/manageRoles/View.vue +12 -8
  339. package/src/plugins/manageRoles/definition.ts +6 -0
  340. package/src/plugins/manageRoles/index.ts +7 -6
  341. package/src/plugins/manageSkills/View.vue +11 -7
  342. package/src/plugins/manageSkills/definition.ts +4 -1
  343. package/src/plugins/manageSkills/index.ts +14 -7
  344. package/src/plugins/manageSkills/meta.ts +21 -0
  345. package/src/plugins/manageSource/definition.ts +4 -1
  346. package/src/plugins/manageSource/index.ts +15 -7
  347. package/src/plugins/manageSource/meta.ts +21 -0
  348. package/src/plugins/markdown/Preview.vue +10 -8
  349. package/src/plugins/markdown/View.vue +84 -17
  350. package/src/plugins/markdown/definition.ts +7 -1
  351. package/src/plugins/markdown/index.ts +15 -8
  352. package/src/plugins/markdown/meta.ts +16 -0
  353. package/src/plugins/meta-types.ts +97 -0
  354. package/src/plugins/metas.ts +224 -0
  355. package/src/plugins/presentForm/Preview.vue +4 -15
  356. package/src/plugins/presentForm/View.vue +35 -78
  357. package/src/plugins/presentForm/definition.ts +7 -6
  358. package/src/plugins/presentForm/index.ts +12 -5
  359. package/src/plugins/presentForm/meta.ts +11 -0
  360. package/src/plugins/presentForm/plugin.ts +8 -9
  361. package/src/plugins/presentForm/types.ts +0 -24
  362. package/src/plugins/presentHtml/Preview.vue +1 -8
  363. package/src/plugins/presentHtml/View.vue +401 -30
  364. package/src/plugins/presentHtml/definition.ts +8 -5
  365. package/src/plugins/presentHtml/index.ts +15 -8
  366. package/src/plugins/presentHtml/meta.ts +14 -0
  367. package/src/plugins/presentMulmoScript/View.vue +327 -107
  368. package/src/plugins/presentMulmoScript/definition.ts +34 -7
  369. package/src/plugins/presentMulmoScript/helpers.ts +4 -5
  370. package/src/plugins/presentMulmoScript/index.ts +20 -7
  371. package/src/plugins/presentMulmoScript/meta.ts +52 -0
  372. package/src/plugins/scheduler/AutomationsPreview.vue +2 -8
  373. package/src/plugins/scheduler/Preview.vue +5 -2
  374. package/src/plugins/scheduler/TasksTab.vue +16 -36
  375. package/src/plugins/scheduler/View.vue +22 -54
  376. package/src/plugins/scheduler/automationsDefinition.ts +14 -9
  377. package/src/plugins/scheduler/automationsMeta.ts +5 -0
  378. package/src/plugins/scheduler/calendarDefinition.ts +4 -7
  379. package/src/plugins/scheduler/calendarMeta.ts +28 -0
  380. package/src/plugins/scheduler/formatSchedule.ts +6 -24
  381. package/src/plugins/scheduler/index.ts +26 -52
  382. package/src/plugins/scope.ts +57 -0
  383. package/src/plugins/server-bindings-types.ts +38 -0
  384. package/src/plugins/server.ts +32 -0
  385. package/src/plugins/skill/Preview.vue +25 -0
  386. package/src/plugins/skill/View.vue +125 -0
  387. package/src/plugins/skill/definition.ts +23 -0
  388. package/src/plugins/skill/index.ts +36 -0
  389. package/src/plugins/skill/plugin.ts +31 -0
  390. package/src/plugins/skill/types.ts +21 -0
  391. package/src/plugins/spreadsheet/Preview.vue +1 -3
  392. package/src/plugins/spreadsheet/View.vue +29 -49
  393. package/src/plugins/spreadsheet/cellHighlights.ts +2 -3
  394. package/src/plugins/spreadsheet/definition.ts +5 -2
  395. package/src/plugins/spreadsheet/index.ts +15 -8
  396. package/src/plugins/spreadsheet/keyboardNav.ts +38 -0
  397. package/src/plugins/spreadsheet/meta.ts +14 -0
  398. package/src/plugins/textResponse/Preview.vue +9 -1
  399. package/src/plugins/textResponse/View.vue +59 -8
  400. package/src/plugins/textResponse/index.ts +11 -3
  401. package/src/plugins/textResponse/plugin.ts +8 -10
  402. package/src/plugins/textResponse/types.ts +28 -0
  403. package/src/plugins/wiki/Preview.vue +6 -4
  404. package/src/plugins/wiki/View.vue +463 -254
  405. package/src/plugins/wiki/components/WikiPageBody.vue +159 -0
  406. package/src/plugins/wiki/helpers.ts +17 -0
  407. package/src/plugins/wiki/history/HistoryDetail.vue +325 -0
  408. package/src/plugins/wiki/history/HistoryTab.vue +167 -0
  409. package/src/plugins/wiki/history/RestoreConfirm.vue +63 -0
  410. package/src/plugins/wiki/history/api.ts +52 -0
  411. package/src/plugins/wiki/history/diff.ts +145 -0
  412. package/src/plugins/wiki/index.ts +42 -32
  413. package/src/plugins/wiki/meta.ts +10 -0
  414. package/src/plugins/wiki/pageEditLoader.ts +53 -0
  415. package/src/plugins/wiki/route.ts +8 -0
  416. package/src/router/guards.ts +2 -1
  417. package/src/router/index.ts +19 -0
  418. package/src/router/pageRoutes.ts +1 -0
  419. package/src/tools/index.ts +50 -51
  420. package/src/tools/runtimeLoader.ts +141 -0
  421. package/src/tools/types.ts +44 -1
  422. package/src/types/notification.ts +23 -0
  423. package/src/types/pastedFile.ts +10 -0
  424. package/src/types/session.ts +61 -3
  425. package/src/types/sse.ts +21 -6
  426. package/src/utils/agent/eventDispatch.ts +12 -9
  427. package/src/utils/agent/pastedAttachment.ts +35 -0
  428. package/src/utils/agent/request.ts +32 -3
  429. package/src/utils/agent/toolCalls.ts +7 -1
  430. package/src/utils/api.ts +1 -1
  431. package/src/utils/chat/exportMarkdown.ts +243 -0
  432. package/src/utils/errors.ts +10 -2
  433. package/src/utils/files/expandedDirs.ts +1 -1
  434. package/src/utils/filesPreview/todoPreview.ts +13 -2
  435. package/src/utils/format/date.ts +1 -3
  436. package/src/utils/format/jsonSyntax.ts +5 -0
  437. package/src/utils/html/iframeHeightReporterScript.ts +62 -0
  438. package/src/utils/html/previewCsp.ts +29 -2
  439. package/src/utils/image/htmlSrcAttrs.ts +122 -0
  440. package/src/utils/image/imageRepairInlineScript.ts +115 -0
  441. package/src/utils/image/resolve.ts +17 -3
  442. package/src/utils/image/rewriteMarkdownImageRefs.ts +62 -9
  443. package/src/utils/markdown/frontmatter.ts +125 -0
  444. package/src/utils/markdown/taskList.ts +7 -2
  445. package/src/utils/plugin/runtime.ts +132 -0
  446. package/src/utils/session/mergeSessions.ts +40 -37
  447. package/src/utils/session/sessionEntries.ts +74 -18
  448. package/src/utils/session/sessionHelpers.ts +54 -10
  449. package/src/utils/tools/result.ts +76 -14
  450. package/src/vite-env.d.ts +6 -0
  451. package/client/assets/html2canvas-Cx501zZr-Bug0qRNv.js +0 -5
  452. package/client/assets/index-CY-WpQUm.css +0 -2
  453. package/client/assets/index-DbTz2Mfs.js +0 -4911
  454. package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
  455. package/server/api/routes/html.ts +0 -114
  456. package/server/api/routes/todos.ts +0 -293
  457. package/server/api/routes/todosColumnsHandlers.ts +0 -333
  458. package/server/api/routes/todosHandlers.ts +0 -274
  459. package/server/api/routes/todosItemsHandlers.ts +0 -386
  460. package/server/utils/files/todos-io.ts +0 -29
  461. package/src/components/NotificationToast.vue +0 -75
  462. package/src/plugins/editImage/definition.ts +0 -27
  463. package/src/plugins/editImage/index.ts +0 -37
  464. package/src/plugins/presentHtml/helpers.ts +0 -72
  465. package/src/plugins/scheduler/LegacySchedulerView.vue +0 -32
  466. package/src/plugins/scheduler/legacyShape.ts +0 -34
  467. package/src/plugins/todo/Preview.vue +0 -68
  468. package/src/plugins/todo/View.vue +0 -378
  469. package/src/plugins/todo/composables/useTodos.ts +0 -179
  470. package/src/plugins/todo/definition.ts +0 -45
  471. package/src/plugins/todo/index.ts +0 -62
  472. package/src/plugins/todo/labels.ts +0 -163
  473. package/src/plugins/todo/priority.ts +0 -98
  474. package/src/plugins/todo/viewModes.ts +0 -19
  475. package/src/plugins/wiki/definition.ts +0 -25
  476. package/src/tools/legacyPluginNames.ts +0 -13
  477. package/src/utils/format/frontmatter.ts +0 -80
  478. package/src/utils/image/rewriteHtmlImageRefs.ts +0 -50
  479. package/src/utils/notification/dispatch.ts +0 -58
  480. /package/client/assets/{purify.es-Fx1Nqyry-BwJECkqS.js → purify.es-Fx1Nqyry-BSVNht6S.js} +0 -0
  481. /package/src/plugins/{editImage → editImages}/Preview.vue +0 -0
  482. /package/src/plugins/{editImage → editImages}/View.vue +0 -0
  483. /package/src/{config/schedulerActions.ts → plugins/scheduler/actions.ts} +0 -0
@@ -0,0 +1,248 @@
1
+ // Atomic-to-topic migration (#1070 PR-A).
2
+ //
3
+ // Reads the existing #1029-style atomic entries from
4
+ // `conversations/memory/`, runs a clusterer, and writes the
5
+ // proposed topic layout to a STAGING dir
6
+ // `conversations/memory.next/`. Does NOT swap. The user runs
7
+ // `topic-swap.ts` after reviewing.
8
+ //
9
+ // Library only — `runTopicMigrationOnce` (in PR-B) decides when
10
+ // to call this from server startup.
11
+ //
12
+ // CLEANUP 2026-07-01: see `topic-run.ts` — this file is part of
13
+ // the one-shot atomic → topic migration chain and goes when the
14
+ // chain goes.
15
+
16
+ import { mkdir, rm } from "node:fs/promises";
17
+ import path from "node:path";
18
+
19
+ import { writeFileAtomic } from "../../utils/files/atomic.js";
20
+ import { log } from "../../system/logger/index.js";
21
+ import { errorMessage } from "../../utils/errors.js";
22
+ import { WORKSPACE_DIRS } from "../paths.js";
23
+ import { loadAllMemoryEntries } from "./io.js";
24
+ import { MEMORY_TYPES, type MemoryEntry, type MemoryType } from "./types.js";
25
+ import type { ClusterMap, ClusterTopic, MemoryClusterer } from "./topic-cluster.js";
26
+ import { MAX_TOPIC_SLUG_LENGTH } from "./topic-types.js";
27
+
28
+ export interface TopicMigrationResult {
29
+ /** Whether anything was emitted to the staging dir. */
30
+ noop: boolean;
31
+ /** Atomic entries that fed the cluster call. */
32
+ inputCount: number;
33
+ /** Topic files written to the staging dir, per type. */
34
+ topicCounts: Record<MemoryType, number>;
35
+ /** Bullets the clusterer omitted (sum across types). */
36
+ bulletsLost: number;
37
+ /** Where the staging dir lives. */
38
+ stagingPath: string;
39
+ }
40
+
41
+ export function topicStagingPath(workspaceRoot: string): string {
42
+ return path.join(workspaceRoot, WORKSPACE_DIRS.memoryStaging);
43
+ }
44
+
45
+ interface WrittenTopic {
46
+ type: MemoryType;
47
+ topic: ClusterTopic;
48
+ /** Slug actually written. May differ from `topic.topic` when a
49
+ * collision was resolved with a `-2` / `-3` suffix. */
50
+ writtenSlug: string;
51
+ }
52
+
53
+ export async function clusterAtomicIntoStaging(workspaceRoot: string, clusterer: MemoryClusterer): Promise<TopicMigrationResult> {
54
+ const entries = await loadAllMemoryEntries(workspaceRoot);
55
+ const stagingPath = topicStagingPath(workspaceRoot);
56
+ if (entries.length === 0) {
57
+ return emptyResult(stagingPath);
58
+ }
59
+ log.info("memory", "topic-migrate: clustering", { entryCount: entries.length });
60
+ // Wipe stale staging BEFORE the cluster call. If the clusterer
61
+ // returns null or throws, we leave the workspace with no staging
62
+ // dir at all — that's the correct "migration didn't complete"
63
+ // signal. The earlier flow only cleared on success and could
64
+ // leave a stale tree in place after a failed run, which a later
65
+ // swap would happily promote.
66
+ await resetStaging(stagingPath);
67
+ let map: ClusterMap | null = null;
68
+ let clustererThrew = false;
69
+ try {
70
+ map = await clusterer(entries);
71
+ } catch (err) {
72
+ clustererThrew = true;
73
+ log.error("memory", "topic-migrate: clusterer threw", { error: errorMessage(err) });
74
+ }
75
+ if (!map) {
76
+ // Only warn about a graceful null return when the clusterer
77
+ // didn't already log a throw. Without this, a hard failure
78
+ // (claude CLI missing, schema mismatch, etc.) showed up as
79
+ // BOTH `clusterer threw` AND `clusterer returned null`, which
80
+ // misleads readers into thinking there were two distinct
81
+ // failure modes (#1072 review).
82
+ if (!clustererThrew) log.warn("memory", "topic-migrate: clusterer returned null");
83
+ await rm(stagingPath, { recursive: true, force: true });
84
+ return { ...emptyResult(stagingPath), inputCount: entries.length };
85
+ }
86
+
87
+ const result: TopicMigrationResult = {
88
+ noop: false,
89
+ inputCount: entries.length,
90
+ topicCounts: { preference: 0, interest: 0, fact: 0, reference: 0 },
91
+ bulletsLost: countBulletsLost(entries, map),
92
+ stagingPath,
93
+ };
94
+ const written: WrittenTopic[] = [];
95
+ for (const type of MEMORY_TYPES) {
96
+ const usedSlugs = new Set<string>();
97
+ for (const topic of map[type]) {
98
+ const writtenSlug = pickUniqueSlug(topic.topic, usedSlugs);
99
+ try {
100
+ await writeTopicFileToStaging(stagingPath, type, topic, writtenSlug);
101
+ usedSlugs.add(writtenSlug);
102
+ written.push({ type, topic, writtenSlug });
103
+ result.topicCounts[type] += 1;
104
+ } catch (err) {
105
+ log.warn("memory", "topic-migrate: write failed", { type, topic: topic.topic, writtenSlug, error: errorMessage(err) });
106
+ }
107
+ }
108
+ }
109
+ await writeStagingIndex(stagingPath, written);
110
+ log.info("memory", "topic-migrate: staging ready", {
111
+ stagingPath,
112
+ topicCounts: result.topicCounts,
113
+ bulletsLost: result.bulletsLost,
114
+ });
115
+ return result;
116
+ }
117
+
118
+ // Pick a slug that hasn't been used yet within this type. The
119
+ // clusterer may return two topics that would normalise to the same
120
+ // slug (e.g. "Music" and "music"); without this guard the second
121
+ // would silently overwrite the first.
122
+ //
123
+ // The base slug is trimmed if needed so `base + "-N"` still fits the
124
+ // `MAX_TOPIC_SLUG_LENGTH` cap that `isSafeTopicSlug` enforces. A
125
+ // 60-char slug colliding with a prior write would otherwise produce a
126
+ // 62-char filename that the writer rejects (and the reader would
127
+ // then refuse to load on the next session). After trimming we strip
128
+ // any trailing `-` so the suffix ("-N") is the only separator at the
129
+ // boundary.
130
+ function pickUniqueSlug(base: string, used: Set<string>): string {
131
+ if (!used.has(base)) return base;
132
+ let counter = 2;
133
+ while (true) {
134
+ const suffix = `-${counter}`;
135
+ const room = MAX_TOPIC_SLUG_LENGTH - suffix.length;
136
+ const trimmedBase = trimTrailingDash(base.slice(0, room));
137
+ const candidate = trimmedBase.length > 0 ? `${trimmedBase}${suffix}` : `topic${suffix}`;
138
+ if (!used.has(candidate)) return candidate;
139
+ counter += 1;
140
+ }
141
+ }
142
+
143
+ function trimTrailingDash(text: string): string {
144
+ let end = text.length;
145
+ while (end > 0 && text[end - 1] === "-") end -= 1;
146
+ return text.slice(0, end);
147
+ }
148
+
149
+ function emptyResult(stagingPath: string): TopicMigrationResult {
150
+ return {
151
+ noop: true,
152
+ inputCount: 0,
153
+ topicCounts: { preference: 0, interest: 0, fact: 0, reference: 0 },
154
+ bulletsLost: 0,
155
+ stagingPath,
156
+ };
157
+ }
158
+
159
+ async function resetStaging(stagingPath: string): Promise<void> {
160
+ // Stale staging from a prior run is wiped — the user's review
161
+ // signal is the diff, so we always emit a fresh tree.
162
+ await rm(stagingPath, { recursive: true, force: true });
163
+ await mkdir(stagingPath, { recursive: true });
164
+ }
165
+
166
+ async function writeTopicFileToStaging(stagingPath: string, type: MemoryType, topic: ClusterTopic, writtenSlug: string): Promise<void> {
167
+ const dir = path.join(stagingPath, type);
168
+ await mkdir(dir, { recursive: true });
169
+ const absPath = path.join(dir, `${writtenSlug}.md`);
170
+ const body = renderTopicBody(topic, writtenSlug);
171
+ const content = `---\ntype: ${type}\ntopic: ${writtenSlug}\n---\n\n${body}`;
172
+ await writeFileAtomic(absPath, content, { uniqueTmp: true });
173
+ }
174
+
175
+ function renderTopicBody(topic: ClusterTopic, writtenSlug: string): string {
176
+ const heading = humaniseTopic(writtenSlug);
177
+ const lines: string[] = [`# ${heading}`, ""];
178
+ if (topic.unsectionedBullets && topic.unsectionedBullets.length > 0) {
179
+ for (const bullet of topic.unsectionedBullets) {
180
+ lines.push(`- ${bullet}`);
181
+ }
182
+ lines.push("");
183
+ }
184
+ if (topic.sections) {
185
+ for (const section of topic.sections) {
186
+ lines.push(`## ${section.heading}`, "");
187
+ for (const bullet of section.bullets) {
188
+ lines.push(`- ${bullet}`);
189
+ }
190
+ lines.push("");
191
+ }
192
+ }
193
+ return lines.join("\n");
194
+ }
195
+
196
+ function humaniseTopic(slug: string): string {
197
+ // ASCII-friendly humaniser: split on `-`, capitalise each word.
198
+ // Non-ASCII slugs (which fall back to a hash) render as the slug
199
+ // itself; the user can rename later in the file explorer.
200
+ return slug
201
+ .split("-")
202
+ .map((part) => (part.length > 0 ? part[0].toUpperCase() + part.slice(1) : part))
203
+ .join(" ");
204
+ }
205
+
206
+ // Build the staging-side `MEMORY.md` from the topics we successfully
207
+ // wrote to disk. Using the cluster map directly would link to files
208
+ // that failed to write (e.g. one bad slug would leave a broken index
209
+ // entry that swap promotes); this list is the disk-truth.
210
+ async function writeStagingIndex(stagingPath: string, written: WrittenTopic[]): Promise<void> {
211
+ const lines: string[] = ["# Memory Index", ""];
212
+ for (const type of MEMORY_TYPES) {
213
+ const inType = written.filter((entry) => entry.type === type);
214
+ if (inType.length === 0) continue;
215
+ lines.push(`## ${type}`, "");
216
+ const sorted = [...inType].sort((left, right) => left.writtenSlug.localeCompare(right.writtenSlug));
217
+ for (const entry of sorted) {
218
+ lines.push(formatStagingIndexLine(entry));
219
+ }
220
+ lines.push("");
221
+ }
222
+ if (written.length === 0) {
223
+ lines.push("_(no entries yet)_", "");
224
+ }
225
+ await writeFileAtomic(path.join(stagingPath, "MEMORY.md"), lines.join("\n"), { uniqueTmp: true });
226
+ }
227
+
228
+ function formatStagingIndexLine(entry: WrittenTopic): string {
229
+ const link = `${entry.type}/${entry.writtenSlug}.md`;
230
+ const headings = (entry.topic.sections ?? []).map((section) => section.heading);
231
+ if (headings.length === 0) return `- ${link}`;
232
+ return `- ${link} — ${headings.join(", ")}`;
233
+ }
234
+
235
+ function countBulletsLost(entries: readonly MemoryEntry[], map: ClusterMap): number {
236
+ let placed = 0;
237
+ for (const type of MEMORY_TYPES) {
238
+ for (const topic of map[type]) {
239
+ if (topic.unsectionedBullets) placed += topic.unsectionedBullets.length;
240
+ if (topic.sections) {
241
+ for (const section of topic.sections) {
242
+ placed += section.bullets.length;
243
+ }
244
+ }
245
+ }
246
+ }
247
+ return Math.max(0, entries.length - placed);
248
+ }
@@ -0,0 +1,172 @@
1
+ // One-shot topic-based migration entry point used by server startup
2
+ // (#1070 PR-B). Mirrors `runMemoryMigrationOnce` from #1029 PR-B
3
+ // but targets the topic-format restructure instead of the legacy
4
+ // memory.md flow.
5
+ //
6
+ // Idempotent: returns immediately when there is nothing to do —
7
+ // the workspace is already topic-format, there are no atomic
8
+ // entries to migrate, or the legacy `memory.md` is still in
9
+ // flight. When staging is already present from a prior crash mid-
10
+ // swap, this runner retries the swap rather than burning another
11
+ // LLM cluster call. Failures are logged and swallowed so the
12
+ // server can continue serving traffic.
13
+ //
14
+ // Concurrency: cluster runs in the background while the agent
15
+ // continues serving requests. Atomic-format reads / writes stay in
16
+ // effect right up until the swap completes; the next request after
17
+ // the swap sees the new topic layout.
18
+ //
19
+ // CLEANUP 2026-07-01: this is one-shot migration code for the
20
+ // atomic → topic transition (#1070). After every active workspace
21
+ // has been swapped to the topic format, this file plus
22
+ // `topic-migrate.ts`, `topic-cluster.ts`, `topic-swap.ts`, the CLI
23
+ // helper at `scripts/memory-swap-topic-staging.ts`, the
24
+ // `yarn memory:swap` script, and the migration call in
25
+ // `server/index.ts` can be deleted in one sweep. Topic-format
26
+ // reading / writing (`topic-types.ts`, `topic-io.ts`,
27
+ // `topic-detect.ts` — minus the atomic-format branch) stays.
28
+
29
+ import { existsSync, statSync } from "node:fs";
30
+ import path from "node:path";
31
+
32
+ import { runClaudeCli, ClaudeCliNotFoundError, type Summarize } from "../journal/archivist-cli.js";
33
+ import { WORKSPACE_DIRS, WORKSPACE_FILES } from "../paths.js";
34
+ import { loadAllMemoryEntries } from "./io.js";
35
+ import { makeLlmMemoryClusterer } from "./topic-cluster.js";
36
+ import { clusterAtomicIntoStaging, topicStagingPath } from "./topic-migrate.js";
37
+ import { swapStagingIntoMemory } from "./topic-swap.js";
38
+ import { MEMORY_TYPES } from "./types.js";
39
+ import { errorMessage } from "../../utils/errors.js";
40
+ import { log } from "../../system/logger/index.js";
41
+
42
+ export interface RunTopicMigrationDeps {
43
+ /** Override the summarize callback (useful for tests). Defaults to
44
+ * the production `runClaudeCli` which spawns the Claude CLI. */
45
+ summarize?: Summarize;
46
+ }
47
+
48
+ // Strict variant of `hasTopicFormat` that only looks at `memory/`,
49
+ // not `memory.next/`. The shared `hasTopicFormat` is intentionally
50
+ // swap-tolerant so prompt routing doesn't fall back to atomic-format
51
+ // rules during the rename window — but the runner's idempotency
52
+ // guard needs the OPPOSITE: when only `memory.next/` exists (a
53
+ // swap-in-progress or a crash mid-swap), the runner must drop into
54
+ // the "existing staging detected" retry-swap branch below, not exit.
55
+ function memoryTreeIsTopicFormat(workspaceRoot: string): boolean {
56
+ const memoryRoot = path.join(workspaceRoot, WORKSPACE_DIRS.memoryDir);
57
+ for (const type of MEMORY_TYPES) {
58
+ try {
59
+ if (statSync(path.join(memoryRoot, type)).isDirectory()) return true;
60
+ } catch {
61
+ // ENOENT / EACCES → keep looking; only the actual presence of
62
+ // a `memory/<type>` dir signals migration completed.
63
+ }
64
+ }
65
+ return false;
66
+ }
67
+
68
+ export async function runTopicMigrationOnce(workspaceRoot: string, deps: RunTopicMigrationDeps = {}): Promise<void> {
69
+ if (memoryTreeIsTopicFormat(workspaceRoot)) {
70
+ log.debug("memory", "topic-run: workspace already uses topic format, skipping");
71
+ return;
72
+ }
73
+ const stagingPath = topicStagingPath(workspaceRoot);
74
+ // If staging is left over from a prior run that crashed between
75
+ // cluster and swap, just retry the swap. Re-clustering would burn
76
+ // another LLM call and (because clusterAtomicIntoStaging wipes
77
+ // staging up front) discard the prior cluster result.
78
+ if (existsSync(stagingPath)) {
79
+ log.info("memory", "topic-run: existing staging detected, retrying swap", { stagingPath });
80
+ await runSwap(workspaceRoot);
81
+ return;
82
+ }
83
+ // Don't trip over an in-progress legacy `memory.md` migration from
84
+ // #1029 PR-B. We mirror the conditions under which
85
+ // `runMemoryMigrationOnce` would actually run — legacy file
86
+ // present, past the placeholder threshold, AND `.backup` absent.
87
+ // The `.backup` check is load-bearing: when both `memory.md` and
88
+ // `.backup` exist, the legacy runner refuses to re-process (the
89
+ // backup signals "already done; user re-introduced the file"),
90
+ // and without this clause the topic runner would defer
91
+ // indefinitely waiting for a migration that's never going to
92
+ // happen.
93
+ //
94
+ // One guarded `statSync` (no `existsSync` first): the legacy
95
+ // migration runs in parallel and can rename / delete `memory.md`
96
+ // between an `existsSync` check and a follow-up `statSync`,
97
+ // turning the race into an unhandled rejection because this whole
98
+ // function is invoked as a floating promise on startup. ENOENT
99
+ // means the legacy file isn't there (or just got renamed away),
100
+ // so there's nothing to defer for; any other error is swallowed
101
+ // and the runner proceeds — a permission glitch should never block
102
+ // the topic restructure.
103
+ const legacyPath = path.join(workspaceRoot, WORKSPACE_FILES.memory);
104
+ let legacyStat: ReturnType<typeof statSync> | null;
105
+ try {
106
+ legacyStat = statSync(legacyPath);
107
+ } catch {
108
+ legacyStat = null;
109
+ }
110
+ if (legacyStat && legacyStat.size >= 64) {
111
+ const backupPath = `${legacyPath}.backup`;
112
+ if (!existsSync(backupPath)) {
113
+ log.debug("memory", "topic-run: legacy memory.md still in flight, deferring", { legacyPath });
114
+ return;
115
+ }
116
+ }
117
+ const entries = await loadAllMemoryEntries(workspaceRoot);
118
+ if (entries.length === 0) {
119
+ log.debug("memory", "topic-run: no atomic entries to migrate, skipping");
120
+ return;
121
+ }
122
+ const summarize = deps.summarize ?? runClaudeCli;
123
+ const clusterer = makeLlmMemoryClusterer({ summarize });
124
+ log.info("memory", "topic-run: starting", { entryCount: entries.length });
125
+ try {
126
+ const result = await clusterAtomicIntoStaging(workspaceRoot, clusterer);
127
+ if (result.noop) {
128
+ // `clusterAtomicIntoStaging` already logged the failure cause
129
+ // (`clusterer threw` or `clusterer returned null`) and rm'd
130
+ // the staging dir. Logging "staged" here would tell the user
131
+ // to `diff` a directory that no longer exists (#1076 review).
132
+ log.warn("memory", "topic-run: cluster did not produce staging — see prior log entry");
133
+ return;
134
+ }
135
+ log.info("memory", "topic-run: staged", {
136
+ stagingPath: result.stagingPath,
137
+ topicCounts: result.topicCounts,
138
+ bulletsLost: result.bulletsLost,
139
+ });
140
+ await runSwap(workspaceRoot);
141
+ } catch (err) {
142
+ // Defensive: `makeLlmMemoryClusterer` swallows summarize errors
143
+ // and returns null today, and `clusterAtomicIntoStaging` doesn't
144
+ // re-throw, so this branch is currently unreachable. Kept so a
145
+ // future change in the clusterer error contract surfaces a
146
+ // visible log instead of an unhandled rejection (the runner is
147
+ // invoked as a floating promise on startup).
148
+ if (err instanceof ClaudeCliNotFoundError) {
149
+ log.warn("memory", "topic-run: claude CLI not on PATH; topic restructure deferred");
150
+ return;
151
+ }
152
+ log.error("memory", "topic-run: cluster threw", { error: errorMessage(err) });
153
+ }
154
+ }
155
+
156
+ // Swap staging into the live memory dir. The atomic format is
157
+ // parked under `memory/.atomic-backup/<ts>/` so misclassified
158
+ // migrations can be rolled back by hand without losing data.
159
+ // Failures leave staging in place; the next server start hits the
160
+ // "existing staging detected" branch above and retries.
161
+ async function runSwap(workspaceRoot: string): Promise<void> {
162
+ const result = await swapStagingIntoMemory(workspaceRoot);
163
+ if (result.swapped) {
164
+ log.info("memory", "topic-run: swap complete — workspace now uses topic format", {
165
+ backupPath: result.backupPath,
166
+ });
167
+ } else {
168
+ log.warn("memory", "topic-run: swap did not complete, leaving staging in place for retry", {
169
+ reason: result.reason ?? "unknown",
170
+ });
171
+ }
172
+ }
@@ -0,0 +1,135 @@
1
+ // Swap `conversations/memory/` ↔ `conversations/memory.next/`
2
+ // after the user has approved the staging diff (#1070 PR-A).
3
+ //
4
+ // Library only — invoked from a CLI helper or the agent (a tool
5
+ // surface for the agent will land later). The swap is intentionally
6
+ // NOT auto-run: the whole point of staging is to give the user a
7
+ // chance to inspect.
8
+ //
9
+ // CLEANUP 2026-07-01: see `topic-run.ts` — this file is part of
10
+ // the one-shot atomic → topic migration chain and goes when the
11
+ // chain goes.
12
+ //
13
+ // Swap mechanics:
14
+ // memory/ → memory/.atomic-backup-<ts>/
15
+ // memory.next/ → memory/
16
+ //
17
+ // The backup name carries a timestamp so re-runs (after a follow-up
18
+ // migration on a richer workspace) don't clobber prior backups.
19
+
20
+ import { mkdir, rename, stat } from "node:fs/promises";
21
+ import path from "node:path";
22
+
23
+ import { log } from "../../system/logger/index.js";
24
+ import { errorMessage } from "../../utils/errors.js";
25
+ import { WORKSPACE_DIRS } from "../paths.js";
26
+ import { topicStagingPath } from "./topic-migrate.js";
27
+
28
+ export interface SwapResult {
29
+ /** True when a swap actually happened. */
30
+ swapped: boolean;
31
+ /** Where the prior atomic layout was moved. Null if no prior data. */
32
+ backupPath: string | null;
33
+ /** Reason when `swapped: false`. */
34
+ reason?: string;
35
+ }
36
+
37
+ export async function swapStagingIntoMemory(workspaceRoot: string): Promise<SwapResult> {
38
+ const stagingPath = topicStagingPath(workspaceRoot);
39
+ const memoryPath = path.join(workspaceRoot, WORKSPACE_DIRS.memoryDir);
40
+ const stagingExists = await pathExists(stagingPath);
41
+ if (!stagingExists) {
42
+ return { swapped: false, backupPath: null, reason: "staging dir not found" };
43
+ }
44
+
45
+ let backupPath: string | null = null;
46
+ if (await pathExists(memoryPath)) {
47
+ backupPath = await pickBackupPath(memoryPath);
48
+ try {
49
+ await rename(memoryPath, backupPath);
50
+ } catch (err) {
51
+ log.error("memory", "topic-swap: backup rename failed", { from: memoryPath, to: backupPath, error: errorMessage(err) });
52
+ return { swapped: false, backupPath: null, reason: "backup rename failed" };
53
+ }
54
+ }
55
+
56
+ try {
57
+ await rename(stagingPath, memoryPath);
58
+ } catch (err) {
59
+ log.error("memory", "topic-swap: staging rename failed", { from: stagingPath, to: memoryPath, error: errorMessage(err) });
60
+ // Try to put the backup back so the workspace isn't left empty.
61
+ let rollbackFailed = false;
62
+ if (backupPath) {
63
+ try {
64
+ await rename(backupPath, memoryPath);
65
+ } catch (rollbackErr) {
66
+ rollbackFailed = true;
67
+ log.error("memory", "topic-swap: rollback failed; manual intervention needed", {
68
+ backupPath,
69
+ memoryPath,
70
+ error: errorMessage(rollbackErr),
71
+ });
72
+ }
73
+ }
74
+ // When rollback ALSO fails, the prior data still lives at
75
+ // `backupPath`. Returning `backupPath: null` would tell callers
76
+ // "no recovery point exists" and they'd give up — surface the
77
+ // path so a human (or a retry loop) can move it back manually
78
+ // (#1072 review).
79
+ return { swapped: false, backupPath: rollbackFailed ? backupPath : null, reason: "staging rename failed" };
80
+ }
81
+
82
+ // Park the backup INSIDE the new memory dir so it travels with
83
+ // the workspace. A flat sibling backup (`memory.atomic-backup`)
84
+ // is also fine but clutters `conversations/`.
85
+ if (backupPath) {
86
+ const inside = path.join(memoryPath, ".atomic-backup");
87
+ await mkdir(inside, { recursive: true });
88
+ const finalLocation = path.join(inside, path.basename(backupPath));
89
+ try {
90
+ await rename(backupPath, finalLocation);
91
+ backupPath = finalLocation;
92
+ } catch (err) {
93
+ log.warn("memory", "topic-swap: failed to park backup inside memory/, leaving at sibling location", {
94
+ backupPath,
95
+ error: errorMessage(err),
96
+ });
97
+ }
98
+ }
99
+
100
+ log.info("memory", "topic-swap: done", { backupPath, memoryPath });
101
+ return { swapped: true, backupPath };
102
+ }
103
+
104
+ async function pathExists(target: string): Promise<boolean> {
105
+ try {
106
+ await stat(target);
107
+ return true;
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+
113
+ // Builds an unused backup path. We use a coarse timestamp suffix so
114
+ // re-runs sort chronologically and don't collide.
115
+ async function pickBackupPath(memoryPath: string): Promise<string> {
116
+ const parent = path.dirname(memoryPath);
117
+ const stamp = formatTimestamp(new Date());
118
+ const base = `memory.atomic-backup-${stamp}`;
119
+ let candidate = path.join(parent, base);
120
+ let counter = 2;
121
+ while (await pathExists(candidate)) {
122
+ candidate = path.join(parent, `${base}-${counter}`);
123
+ counter += 1;
124
+ }
125
+ return candidate;
126
+ }
127
+
128
+ function formatTimestamp(date: Date): string {
129
+ const year = date.getUTCFullYear();
130
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
131
+ const day = String(date.getUTCDate()).padStart(2, "0");
132
+ const hour = String(date.getUTCHours()).padStart(2, "0");
133
+ const minute = String(date.getUTCMinutes()).padStart(2, "0");
134
+ return `${year}${month}${day}-${hour}${minute}`;
135
+ }