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
@@ -1,25 +1,13 @@
1
- // Pure builder for summaries/_index.md. Takes in-memory listings of
2
- // the journal's current topic / daily files and returns the full
3
- // markdown for the index. All filesystem walking happens in the
4
- // caller; this function is deterministic and easy to snapshot-test.
5
-
6
1
  import { isoDateOnly } from "../../utils/date.js";
7
2
 
8
3
  export interface IndexTopicEntry {
9
- // Filesystem slug (matches topics/<slug>.md).
10
4
  slug: string;
11
- // Optional human-readable title extracted from the topic file's
12
- // first H1 heading. Falls back to `slug` if absent so the index
13
- // row always reads sensibly.
14
5
  title?: string;
15
- // ISO timestamp of the last write to the topic file. Rendered
16
- // for "stale topic" visibility.
17
6
  lastUpdatedIso?: string;
18
7
  }
19
8
 
20
9
  export interface IndexDailyEntry {
21
- // YYYY-MM-DD in local time. Matches the folder layout.
22
- date: string;
10
+ date: string; // YYYY-MM-DD local
23
11
  }
24
12
 
25
13
  export interface IndexInputs {
@@ -27,8 +15,6 @@ export interface IndexInputs {
27
15
  days: readonly IndexDailyEntry[];
28
16
  archivedTopicCount: number;
29
17
  builtAtIso: string;
30
- // How many "Recent days" rows to list before collapsing the
31
- // remainder. The full listing still lives under daily/ on disk.
32
18
  maxRecentDays?: number;
33
19
  }
34
20
 
@@ -56,8 +42,7 @@ export function renderTopicsSection(topics: readonly IndexTopicEntry[]): string[
56
42
  lines.push("_No topics yet._");
57
43
  return lines;
58
44
  }
59
- // Newest-first by last update (topics with no timestamp sort
60
- // last, ordered alphabetically among themselves for stability).
45
+ // Newest-first; topics with no timestamp sort last, alphabetical among themselves for stability.
61
46
  const sorted = [...topics].sort(compareTopicsNewestFirst);
62
47
  for (const topicEntry of sorted) {
63
48
  lines.push(renderTopicRow(topicEntry));
@@ -71,7 +56,6 @@ export function renderRecentDaysSection(days: readonly IndexDailyEntry[], maxRec
71
56
  lines.push("_No daily entries yet._");
72
57
  return lines;
73
58
  }
74
- // Newest-first by date string (YYYY-MM-DD sorts lexically).
75
59
  const sorted = [...days].sort((leftDay, rightDay) => {
76
60
  if (leftDay.date < rightDay.date) return 1;
77
61
  if (leftDay.date > rightDay.date) return -1;
@@ -100,8 +84,7 @@ export function renderArchiveSection(archivedTopicCount: number): string[] {
100
84
  return lines;
101
85
  }
102
86
 
103
- // Parse an ISO timestamp into a numeric sort key. Invalid or missing
104
- // timestamps get -Infinity so they sort to the bottom (oldest).
87
+ // Invalid/missing ISO -Infinity so they sort to the bottom.
105
88
  function topicSortKey(entry: IndexTopicEntry): number {
106
89
  if (!entry.lastUpdatedIso) return -Infinity;
107
90
  const timestampMs = Date.parse(entry.lastUpdatedIso);
@@ -117,14 +100,10 @@ function compareBySlug(leftEntry: IndexTopicEntry, rightEntry: IndexTopicEntry):
117
100
  function compareTopicsNewestFirst(leftEntry: IndexTopicEntry, rightEntry: IndexTopicEntry): number {
118
101
  const leftKey = topicSortKey(leftEntry);
119
102
  const rightKey = topicSortKey(rightEntry);
120
- // Both valid timestamps → compare numerically.
121
- // One or both invalid (-Infinity) → valid wins; if both invalid,
122
- // fall through to the slug tie-breaker.
123
103
  const leftIsValid = Number.isFinite(leftKey);
124
104
  const rightIsValid = Number.isFinite(rightKey);
125
105
  if (leftIsValid && rightIsValid && rightKey !== leftKey) return rightKey - leftKey;
126
106
  if (leftIsValid !== rightIsValid) return leftIsValid ? -1 : 1;
127
- // Tie-break on slug for determinism.
128
107
  return compareBySlug(leftEntry, rightEntry);
129
108
  }
130
109
 
@@ -0,0 +1,51 @@
1
+ // #876 top-bar "today's journal" shortcut falls back to the most recent existing day when today's summary is missing.
2
+ // Backtracks YYYY → MM → DD.md so an empty 2026/05/ still finds 2026/04. Bounded I/O: O(years × months) worst case.
3
+
4
+ import path from "node:path";
5
+ import fsp from "node:fs/promises";
6
+ import { isEnoent } from "../../utils/files/safe.js";
7
+ import { summariesRoot, DAILY_DIR } from "./paths.js";
8
+
9
+ export interface LatestDailyResult {
10
+ /** Workspace-relative path, e.g. "conversations/summaries/daily/2026/04/26.md" */
11
+ path: string;
12
+ /** ISO-ish date "YYYY-MM-DD" matching the file's location. */
13
+ isoDate: string;
14
+ }
15
+
16
+ const YEAR_RE = /^\d{4}$/;
17
+ const MONTH_RE = /^\d{2}$/;
18
+ const DAY_FILE_RE = /^(\d{2})\.md$/;
19
+
20
+ async function listSorted(dir: string, pattern: RegExp): Promise<string[]> {
21
+ let entries: string[];
22
+ try {
23
+ entries = await fsp.readdir(dir);
24
+ } catch (err) {
25
+ if (isEnoent(err)) return [];
26
+ throw err;
27
+ }
28
+ return entries
29
+ .filter((name) => pattern.test(name))
30
+ .sort()
31
+ .reverse();
32
+ }
33
+
34
+ export async function findLatestDaily(workspaceRoot: string): Promise<LatestDailyResult | null> {
35
+ const dailyRoot = path.join(summariesRoot(workspaceRoot), DAILY_DIR);
36
+ const years = await listSorted(dailyRoot, YEAR_RE);
37
+ for (const year of years) {
38
+ const months = await listSorted(path.join(dailyRoot, year), MONTH_RE);
39
+ for (const month of months) {
40
+ const days = await listSorted(path.join(dailyRoot, year, month), DAY_FILE_RE);
41
+ if (days.length === 0) continue;
42
+ const [dayFile] = days;
43
+ const dayMatch = DAY_FILE_RE.exec(dayFile);
44
+ if (!dayMatch) continue;
45
+ const [, day] = dayMatch;
46
+ const relPath = path.posix.join("conversations", "summaries", DAILY_DIR, year, month, dayFile);
47
+ return { path: relPath, isoDate: `${year}-${month}-${day}` };
48
+ }
49
+ }
50
+ return null;
51
+ }
@@ -1,11 +1,5 @@
1
- // Extracts durable user facts from chat session excerpts and
2
- // appends them to memory.md. Called at the end of the journal
3
- // daily pass so new facts are picked up even if the agent didn't
4
- // write them during the conversation.
5
- //
6
- // Only appends facts that aren't already in memory.md — the LLM
7
- // receives the current memory content as context and is instructed
8
- // to return ONLY new facts.
1
+ // Run at end of the journal daily pass so durable user facts are picked up even if the agent didn't memo them in-conversation.
2
+ // LLM receives existing memory.md as context and is instructed to return ONLY new facts (we still de-dupe defensively).
9
3
 
10
4
  import { readFileSync, existsSync } from "fs";
11
5
  import path from "path";
@@ -40,16 +34,10 @@ Rules:
40
34
 
41
35
  export interface MemoryExtractionDeps {
42
36
  workspaceRoot: string;
43
- /** The excerpts from today's dirty sessions, concatenated. */
44
37
  excerpts: string;
45
- /** Spawns claude CLI and returns stdout. Injectable for tests. */
46
38
  summarize: (systemPrompt: string, userPrompt: string) => Promise<string>;
47
39
  }
48
40
 
49
- /**
50
- * Extract new user facts from chat excerpts and append to memory.md.
51
- * Returns the number of facts appended (0 if none found or on error).
52
- */
53
41
  export async function extractAndAppendMemory(deps: MemoryExtractionDeps): Promise<number> {
54
42
  const memoryPath = path.join(deps.workspaceRoot, WORKSPACE_FILES.memory);
55
43
  const existingMemory = existsSync(memoryPath) ? readFileSync(memoryPath, "utf-8") : "";
@@ -80,18 +68,16 @@ export async function extractAndAppendMemory(deps: MemoryExtractionDeps): Promis
80
68
  return factsToAppend.length;
81
69
  }
82
70
 
83
- /** Build the user prompt with existing memory + new excerpts. */
84
71
  export function buildUserPrompt(existingMemory: string, excerpts: string): string {
85
72
  const parts: string[] = [];
86
73
  if (existingMemory.trim()) {
87
- parts.push("## Already known (do NOT repeat these):\n\n" + existingMemory);
74
+ parts.push(`## Already known (do NOT repeat these):\n\n${existingMemory}`);
88
75
  }
89
- parts.push("## New chat excerpts:\n\n" + excerpts);
76
+ parts.push(`## New chat excerpts:\n\n${excerpts}`);
90
77
  parts.push("\nExtract any NEW user facts not already in the 'Already known' section above. If none, output: NONE");
91
78
  return parts.join("\n\n");
92
79
  }
93
80
 
94
- /** Parse LLM output into a list of fact strings. */
95
81
  export function parseExtractedFacts(raw: string): string[] {
96
82
  const trimmed = raw.trim();
97
83
  if (trimmed === "NONE" || trimmed === "") return [];
@@ -106,7 +92,6 @@ function normalizeFact(fact: string): string {
106
92
  return fact.replace(/^- /, "").trim().toLowerCase();
107
93
  }
108
94
 
109
- /** Remove facts that already exist in the current memory content. */
110
95
  export function filterNewFacts(existingMemory: string, facts: readonly string[]): string[] {
111
96
  const seen = new Set(parseExtractedFacts(existingMemory).map(normalizeFact));
112
97
  const out: string[] = [];
@@ -119,7 +104,6 @@ export function filterNewFacts(existingMemory: string, facts: readonly string[])
119
104
  return out;
120
105
  }
121
106
 
122
- /** Append facts to existing memory content. */
123
107
  export function appendFacts(existing: string, facts: string[]): string {
124
108
  const trimmed = existing.trimEnd();
125
109
  const factsBlock = facts.join("\n");
@@ -1,8 +1,3 @@
1
- // Weekly-ish topic optimization pass: merge near-duplicates, move
2
- // stale topics into archive/. Separate file from dailyPass so the
3
- // two can evolve independently and so the optimizer stays opt-in
4
- // from the top-level runner.
5
-
6
1
  import { workspacePath as defaultWorkspacePath } from "../workspace.js";
7
2
  import { writeTopicFile, readAllTopicFiles, archiveTopic } from "../../utils/files/journal-io.js";
8
3
  import {
@@ -17,7 +12,6 @@ import { slugify } from "./paths.js";
17
12
  import type { JournalState } from "./state.js";
18
13
  import { log } from "../../system/logger/index.js";
19
14
 
20
- // How many characters of each topic file we hand to the optimizer.
21
15
  // Enough to judge duplication without blowing up the prompt.
22
16
  const OPTIMIZER_HEAD_CHARS = 500;
23
17
 
@@ -33,10 +27,7 @@ export interface OptimizationPassResult {
33
27
  skippedReason?: string;
34
28
  }
35
29
 
36
- // Pure planner: turns the optimizer's raw merge instructions into a
37
- // concrete list of slug-level operations. Empty merges (where every
38
- // source resolves to the merge target itself) are dropped, and slugs
39
- // are normalized via slugify so the I/O layer never has to.
30
+ // Drops empty merges (every source resolves to the target itself); normalizes via slugify so the I/O layer never has to.
40
31
  export interface MergePlanItem {
41
32
  intoSlug: string;
42
33
  fromSlugs: string[];
@@ -60,8 +51,6 @@ export function planMerges(merges: readonly RawMerge[]): MergePlanItem[] {
60
51
  return plans;
61
52
  }
62
53
 
63
- // Pure transform: returns the next JournalState with any slug in
64
- // `removed` filtered out of knownTopics.
65
54
  export function applyRemovedTopics(state: JournalState, removed: ReadonlySet<string>): JournalState {
66
55
  return {
67
56
  ...state,
@@ -73,10 +62,7 @@ async function executeMergePlans(workspaceRoot: string, plans: MergePlanItem[],
73
62
  for (const plan of plans) {
74
63
  await writeTopicFile(plan.intoSlug, plan.newContent, workspaceRoot);
75
64
  for (const src of plan.fromSlugs) {
76
- // Only record the merge as successful if the source file
77
- // actually moved. If archiveTopic fails (missing file, IO
78
- // error) we leave the source out of the removed set so the
79
- // in-memory knownTopics state stays accurate.
65
+ // Skip on archive failure so in-memory knownTopics stays accurate (the source file didn't actually move).
80
66
  if (!(await archiveTopic(src, workspaceRoot))) continue;
81
67
  removed.add(src);
82
68
  mergedSlugs.push(src);
@@ -107,9 +93,7 @@ export async function runOptimizationPass(
107
93
 
108
94
  const topics = await loadTopicHeads(workspaceRoot);
109
95
  if (topics.length < 2) {
110
- // Nothing to optimise — need at least 2 topics for a merge to
111
- // be meaningful, and archiving a single topic would leave an
112
- // empty journal which feels wrong.
96
+ // Need at least 2 topics for a merge to be meaningful; archiving a sole topic would leave an empty journal.
113
97
  result.skipped = true;
114
98
  result.skippedReason = "fewer than 2 topics";
115
99
  return { nextState: { ...state }, result };
@@ -138,8 +122,7 @@ export async function runOptimizationPass(
138
122
 
139
123
  const removed = new Set<string>();
140
124
 
141
- // Apply merges first, then archives (which skip slugs already
142
- // removed by a merge).
125
+ // Merges first, then archives (which skip slugs already removed by a merge).
143
126
  await executeMergePlans(workspaceRoot, planMerges(parsed.merges), removed, result.mergedSlugs);
144
127
  await executeArchives(workspaceRoot, parsed.archives, removed, result.archivedSlugs);
145
128
 
@@ -1,15 +1,8 @@
1
- // Pure path / slug helpers for the workspace journal. Nothing here
2
- // touches the filesystem — every function is a straightforward
3
- // string transformation so it can be exhaustively unit-tested.
4
-
5
1
  import path from "node:path";
6
2
  import { WORKSPACE_DIRS } from "../paths.js";
7
3
  import { isValidIsoDate } from "../../utils/date.js";
8
4
  import { slugify as slugifyCanonical } from "../../utils/slug.js";
9
5
 
10
- // Directory layout under workspace/conversations/summaries/ is an
11
- // implementation detail of the journal module; keep it centralised
12
- // here so tests and callers all agree on the structure.
13
6
  export const SUMMARIES_DIR = WORKSPACE_DIRS.summaries;
14
7
  export const STATE_FILE = "_state.json";
15
8
  export const INDEX_FILE = "_index.md";
@@ -17,16 +10,12 @@ export const DAILY_DIR = "daily";
17
10
  export const TOPICS_DIR = "topics";
18
11
  export const ARCHIVE_DIR = "archive";
19
12
 
20
- // Absolute path to the summaries root inside a workspace.
21
13
  export function summariesRoot(workspaceRoot: string): string {
22
14
  return path.join(workspaceRoot, SUMMARIES_DIR);
23
15
  }
24
16
 
25
- // summaries/daily/YYYY/MM/DD.md for a given ISO-ish date ("YYYY-MM-DD").
26
- // Throws if `isoDate` is not exactly YYYY-MM-DD — catches typos at
27
- // the boundary instead of producing "undefined/undefined.md" paths
28
- // downstream.
29
17
  export function dailyPathFor(workspaceRoot: string, isoDate: string): string {
18
+ // Throw at the boundary so a typo doesn't produce "undefined/undefined.md".
30
19
  if (!isValidIsoDate(isoDate)) {
31
20
  throw new Error(`[journal] dailyPathFor: expected YYYY-MM-DD, got "${isoDate}"`);
32
21
  }
@@ -34,27 +23,19 @@ export function dailyPathFor(workspaceRoot: string, isoDate: string): string {
34
23
  return path.join(summariesRoot(workspaceRoot), DAILY_DIR, year, month, `${day}.md`);
35
24
  }
36
25
 
37
- // summaries/topics/<slug>.md
38
26
  export function topicPathFor(workspaceRoot: string, slug: string): string {
39
27
  return path.join(summariesRoot(workspaceRoot), TOPICS_DIR, `${slug}.md`);
40
28
  }
41
29
 
42
- // summaries/archive/topics/<slug>.md — where the optimizer moves
43
- // merged or stale topic files.
44
30
  export function archivedTopicPathFor(workspaceRoot: string, slug: string): string {
45
31
  return path.join(summariesRoot(workspaceRoot), ARCHIVE_DIR, TOPICS_DIR, `${slug}.md`);
46
32
  }
47
33
 
48
- // Re-export for backwards compatibility — callers that import
49
- // toIsoDate from journal/paths keep working.
50
34
  export { toLocalIsoDate as toIsoDate } from "../../utils/date.js";
51
35
 
52
- // Convert a free-form topic name into a filesystem-safe slug. Thin
53
- // wrapper around the canonical `slugify` (server/utils/slug.ts) with
54
- // the journal-specific fallback "topic". See #732 for why journal
55
- // stopped using its own ASCII-only impl: pure-non-ASCII topic names
56
- // (e.g. "プロジェクトA" / "プロジェクトB") all collapsed to "topic"
57
- // and silently overwrote each other's summary files.
36
+ // Falls through to slugifyCanonical with a journal-specific "topic" fallback.
37
+ // #732 retired the journal's own ASCII-only impl that collapsed every
38
+ // non-ASCII topic name onto a single overwriting "topic".
58
39
  export function slugify(raw: string): string {
59
40
  return slugifyCanonical(raw, "topic");
60
41
  }
@@ -1,12 +1,3 @@
1
- // Journal state file schema + persistence. The state file tracks
2
- // what the archivist has already done so we only re-process new or
3
- // changed sessions on each run.
4
- //
5
- // The pure bits (default creation, schema validation, interval
6
- // arithmetic) live at the top of the file so tests can exercise
7
- // them without touching disk. Filesystem helpers at the bottom wrap
8
- // those pure functions with atomic read/write.
9
-
10
1
  import {
11
2
  readJournalState as readJournalStateRaw,
12
3
  writeJournalState as writeJournalStateRaw,
@@ -16,16 +7,12 @@ import { ONE_HOUR_MS, ONE_DAY_MS } from "../../utils/time.js";
16
7
  import { log } from "../../system/logger/index.js";
17
8
  import { isRecord } from "../../utils/types.js";
18
9
 
19
- // Bump this when the schema changes in a backwards-incompatible way.
20
- // Older state files are treated as corrupted and replaced with a
21
- // fresh default (ingest everything from scratch) — cheap because it
22
- // only costs one extra archivist pass.
10
+ // Bump on backwards-incompatible schema changes older state files
11
+ // are discarded and rebuilt (one extra archivist pass).
23
12
  export const JOURNAL_STATE_VERSION = 1;
24
13
 
25
14
  export interface ProcessedSessionRecord {
26
- // mtime (ms since epoch) of the session's .jsonl file when we
27
- // last ingested it. If mtime advances on the next run, the session
28
- // has appended events and needs re-ingest.
15
+ // mtime at last ingest; advance triggers re-ingest of appended events.
29
16
  lastMtimeMs: number;
30
17
  }
31
18
 
@@ -42,8 +29,6 @@ export interface JournalState {
42
29
  export const DEFAULT_DAILY_INTERVAL_HOURS = 1;
43
30
  export const DEFAULT_OPTIMIZATION_INTERVAL_DAYS = 7;
44
31
 
45
- // --- Pure helpers (unit-testable without disk) ---------------------
46
-
47
32
  export function defaultState(): JournalState {
48
33
  return {
49
34
  version: JOURNAL_STATE_VERSION,
@@ -56,17 +41,13 @@ export function defaultState(): JournalState {
56
41
  };
57
42
  }
58
43
 
59
- // Narrow an `unknown` into a JournalState. Accepts partial / missing
60
- // fields and fills defaults — users can hand-edit the file to change
61
- // intervals and we want to be forgiving.
44
+ // Forgiving toward partial / hand-edited input fill defaults instead of throwing.
62
45
  export function parseState(raw: unknown): JournalState {
63
46
  if (!isRecord(raw)) return defaultState();
64
47
  const obj = raw as Record<string, unknown>;
65
48
 
66
- // Version mismatch throw it all out. Cheap to rebuild — but
67
- // surface the event in the log so a postmortem can distinguish a
68
- // forced re-ingest from "first run after install" / a deleted
69
- // state file. (#799 PR1)
49
+ // Log the version-mismatch reset (#799 PR1) so postmortems can tell
50
+ // it apart from a missing-file first run.
70
51
  if (obj.version !== JOURNAL_STATE_VERSION) {
71
52
  log.info("journal", "state schema version mismatch — resetting", {
72
53
  from: obj.version,
@@ -101,8 +82,6 @@ function parseProcessedSessions(raw: unknown): Record<string, ProcessedSessionRe
101
82
  return out;
102
83
  }
103
84
 
104
- // Has the configured daily interval elapsed since the last run? A
105
- // null lastDailyRunAt means "never run" → always due.
106
85
  export function isDailyDue(state: JournalState, nowMs: number): boolean {
107
86
  if (state.lastDailyRunAt === null) return true;
108
87
  const last = Date.parse(state.lastDailyRunAt);
@@ -119,8 +98,6 @@ export function isOptimizationDue(state: JournalState, nowMs: number): boolean {
119
98
  return nowMs - last >= intervalMs;
120
99
  }
121
100
 
122
- // --- Filesystem helpers (delegated to journal-io) --------------------
123
-
124
101
  export async function readState(workspaceRoot: string): Promise<JournalState> {
125
102
  const raw = await readJournalStateRaw<unknown>(null, workspaceRoot);
126
103
  return parseState(raw);
@@ -0,0 +1,213 @@
1
+ // Memory storage IO (#1029 PR-A).
2
+ //
3
+ // Read all entries, write a single entry, regenerate the index file.
4
+ // Reader is forgiving: a corrupt frontmatter on one entry logs and
5
+ // is skipped, the rest still load. Writer is atomic.
6
+ //
7
+ // `MEMORY.md` is rebuilt from the live frontmatters whenever
8
+ // `regenerateIndex` is called — so every write that changes name /
9
+ // description / type should be followed by a regenerate. The legacy
10
+ // `memory.md` is intentionally NOT touched here; migration handles it.
11
+
12
+ import { mkdir } from "node:fs/promises";
13
+ import type { Stats } from "node:fs";
14
+ import path from "node:path";
15
+
16
+ import { parseFrontmatter, serializeWithFrontmatter } from "../../utils/markdown/frontmatter.js";
17
+ import { writeFileAtomic } from "../../utils/files/atomic.js";
18
+ import { WORKSPACE_DIRS, WORKSPACE_FILES } from "../paths.js";
19
+ import { readDirSafe, readDirSafeAsync, readTextSafe, readTextSafeSync, statSafe, statSafeAsync } from "../../utils/files/safe.js";
20
+ import { log } from "../../system/logger/index.js";
21
+ import { isMemoryType, type MemoryEntry, type MemoryType } from "./types.js";
22
+
23
+ // On-disk directory and index file for typed memory entries.
24
+ // Callers pass the workspace root; this returns the absolute paths.
25
+ export function memoryDirOf(workspaceRoot: string): string {
26
+ return path.join(workspaceRoot, WORKSPACE_DIRS.memoryDir);
27
+ }
28
+
29
+ export function memoryIndexOf(workspaceRoot: string): string {
30
+ return path.join(workspaceRoot, WORKSPACE_FILES.memoryIndex);
31
+ }
32
+
33
+ // Load every entry from `<workspaceRoot>/conversations/memory/`. The
34
+ // index file (`MEMORY.md`) and any sub-directory (e.g. future
35
+ // `archived/` from #1033) are skipped. Files that fail frontmatter
36
+ // parsing or carry an unknown `type` are logged and excluded.
37
+ export async function loadAllMemoryEntries(workspaceRoot: string): Promise<MemoryEntry[]> {
38
+ const dir = memoryDirOf(workspaceRoot);
39
+ const filenames = await listEntryFiles(dir);
40
+ const loaded: MemoryEntry[] = [];
41
+ for (const filename of filenames) {
42
+ const parsed = await readMemoryFile(path.join(dir, filename));
43
+ if (parsed) loaded.push(parsed);
44
+ }
45
+ return loaded;
46
+ }
47
+
48
+ // Persist a single entry. Filename is `<slug>.md`. Returns the
49
+ // workspace-relative path that was written so callers can log /
50
+ // reference the destination. Slugs are validated against
51
+ // `isSafeMemorySlug` — `..` segments, separators, dotfiles, and
52
+ // reserved filenames are rejected so a caller-supplied slug cannot
53
+ // escape the memory directory or collide with `MEMORY.md`. PR-B
54
+ // will surface this same check at the agent-write boundary.
55
+ export async function writeMemoryEntry(workspaceRoot: string, entry: MemoryEntry): Promise<string> {
56
+ if (!isSafeMemorySlug(entry.slug)) {
57
+ throw new Error(`refusing to write memory entry with unsafe slug: ${JSON.stringify(entry.slug)}`);
58
+ }
59
+ const dir = memoryDirOf(workspaceRoot);
60
+ await mkdir(dir, { recursive: true });
61
+ const filename = `${entry.slug}.md`;
62
+ const absPath = path.join(dir, filename);
63
+ const content = serializeWithFrontmatter({ name: entry.name, description: entry.description, type: entry.type }, entry.body);
64
+ await writeFileAtomic(absPath, content, { uniqueTmp: true });
65
+ return path.posix.join(WORKSPACE_DIRS.memoryDir, filename);
66
+ }
67
+
68
+ // Slug shape gate. Allows arbitrary unicode (so non-ASCII names slug
69
+ // fine via `slugifyMemoryName`'s hash fallback) but rejects anything
70
+ // that would let a caller escape the memory directory or shadow
71
+ // reserved filenames.
72
+ export function isSafeMemorySlug(slug: string): boolean {
73
+ if (typeof slug !== "string") return false;
74
+ if (slug.length === 0) return false;
75
+ if (slug.length > 200) return false;
76
+ if (slug.includes("/") || slug.includes("\\")) return false;
77
+ if (slug.includes("\0")) return false;
78
+ // `.`, `..`, leading `.` (dotfiles), or any case-fold of `MEMORY`
79
+ // would conflict with how the reader scans the directory. The
80
+ // case-fold covers `Memory` / `memory` collisions on macOS /
81
+ // Windows where the filesystem is case-insensitive — a case-
82
+ // sensitive comparison would let `slug = "Memory"` produce
83
+ // `Memory.md`, which aliases `MEMORY.md` and shadows the index.
84
+ if (slug.startsWith(".")) return false;
85
+ if (slug.toUpperCase() === "MEMORY") return false;
86
+ return true;
87
+ }
88
+
89
+ // Rebuild `MEMORY.md` from current entry frontmatters. Sorted by type
90
+ // (preference / interest / fact / reference order) then by name so
91
+ // the index reads consistently. Empty memory directory writes a
92
+ // placeholder index (still useful as the marker that the new layout
93
+ // is in effect).
94
+ export async function regenerateIndex(workspaceRoot: string): Promise<void> {
95
+ const dir = memoryDirOf(workspaceRoot);
96
+ await mkdir(dir, { recursive: true });
97
+ const all = await loadAllMemoryEntries(workspaceRoot);
98
+ const sorted = [...all].sort(compareEntries);
99
+ const lines: string[] = ["# Memory", ""];
100
+ for (const entry of sorted) {
101
+ lines.push(`- [${entry.name}](${entry.slug}.md) — ${entry.description}`);
102
+ }
103
+ if (sorted.length === 0) lines.push("_(no entries yet)_");
104
+ lines.push("");
105
+ await writeFileAtomic(memoryIndexOf(workspaceRoot), lines.join("\n"), { uniqueTmp: true });
106
+ }
107
+
108
+ const TYPE_ORDER: readonly MemoryType[] = ["preference", "interest", "fact", "reference"];
109
+
110
+ function typeOrderKey(type: MemoryType): number {
111
+ const idx = TYPE_ORDER.indexOf(type);
112
+ return idx < 0 ? TYPE_ORDER.length : idx;
113
+ }
114
+
115
+ function compareEntries(left: MemoryEntry, right: MemoryEntry): number {
116
+ const typeDelta = typeOrderKey(left.type) - typeOrderKey(right.type);
117
+ if (typeDelta !== 0) return typeDelta;
118
+ return left.name.localeCompare(right.name);
119
+ }
120
+
121
+ async function listEntryFiles(dir: string): Promise<string[]> {
122
+ // Missing memory dir → treat as empty. `readDirSafeAsync` already
123
+ // returns `[]` on ENOENT, so no explicit branch needed.
124
+ const dirents = await readDirSafeAsync(dir);
125
+ const candidates: string[] = [];
126
+ for (const dirent of dirents) {
127
+ if (!isCandidateName(dirent.name)) continue;
128
+ if (dirent.isFile()) {
129
+ candidates.push(dirent.name);
130
+ continue;
131
+ }
132
+ // Symlinks / non-regular entries: stat to resolve, skip on
133
+ // failure or non-file. A corrupt entry should not poison the
134
+ // read path.
135
+ const stat = await statSafeAsync(path.join(dir, dirent.name));
136
+ if (stat?.isFile()) candidates.push(dirent.name);
137
+ }
138
+ return candidates.sort();
139
+ }
140
+
141
+ // Synchronous mirror of `loadAllMemoryEntries` for the agent prompt
142
+ // builder, which is sync all the way up. Same skip rules and
143
+ // frontmatter validation — corrupt entries log once and are excluded
144
+ // instead of leaking raw markdown into the system prompt.
145
+ export function loadAllMemoryEntriesSync(workspaceRoot: string): MemoryEntry[] {
146
+ const dir = memoryDirOf(workspaceRoot);
147
+ const dirents = readDirSafe(dir);
148
+ const filenames: string[] = [];
149
+ for (const dirent of dirents) {
150
+ if (!isCandidateName(dirent.name)) continue;
151
+ if (dirent.isFile()) {
152
+ filenames.push(dirent.name);
153
+ continue;
154
+ }
155
+ const stat: Stats | null = statSafe(path.join(dir, dirent.name));
156
+ if (stat?.isFile()) filenames.push(dirent.name);
157
+ }
158
+ filenames.sort();
159
+ const loaded: MemoryEntry[] = [];
160
+ for (const filename of filenames) {
161
+ const absPath = path.join(dir, filename);
162
+ const raw = readTextSafeSync(absPath);
163
+ if (raw === null) {
164
+ log.warn("memory", "failed to read entry", { path: absPath });
165
+ continue;
166
+ }
167
+ const entry = parseMemoryFile(absPath, raw);
168
+ if (entry) loaded.push(entry);
169
+ }
170
+ return loaded;
171
+ }
172
+
173
+ function isCandidateName(name: string): boolean {
174
+ if (name === "MEMORY.md") return false;
175
+ if (!name.endsWith(".md")) return false;
176
+ if (name.startsWith(".")) return false;
177
+ return true;
178
+ }
179
+
180
+ async function readMemoryFile(absPath: string): Promise<MemoryEntry | null> {
181
+ const raw = await readTextSafe(absPath);
182
+ if (raw === null) {
183
+ log.warn("memory", "failed to read entry", { path: absPath });
184
+ return null;
185
+ }
186
+ return parseMemoryFile(absPath, raw);
187
+ }
188
+
189
+ // Shared frontmatter validator used by both async and sync loaders.
190
+ // Returns null on missing envelope / unknown type / blank required
191
+ // fields; callers log + skip.
192
+ function parseMemoryFile(absPath: string, raw: string): MemoryEntry | null {
193
+ const parsed = parseFrontmatter(raw);
194
+ if (!parsed.hasHeader) {
195
+ log.warn("memory", "entry missing frontmatter", { path: absPath });
196
+ return null;
197
+ }
198
+ const name = stringField(parsed.meta.name);
199
+ const description = stringField(parsed.meta.description);
200
+ const { type } = parsed.meta;
201
+ if (!name || !description || !isMemoryType(type)) {
202
+ log.warn("memory", "entry frontmatter incomplete", { path: absPath });
203
+ return null;
204
+ }
205
+ const slug = path.basename(absPath, ".md");
206
+ return { name, description, type, body: parsed.body, slug };
207
+ }
208
+
209
+ function stringField(value: unknown): string | null {
210
+ if (typeof value !== "string") return null;
211
+ const trimmed = value.trim();
212
+ return trimmed.length > 0 ? trimmed : null;
213
+ }