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
@@ -0,0 +1,718 @@
1
+ // Service layer for the accounting plugin. Wraps the IO + domain
2
+ // modules into the handful of operations the route + MCP bridge
3
+ // expose. Each function:
4
+ //
5
+ // - performs validation,
6
+ // - mutates the journal / accounts / config files atomically,
7
+ // - invalidates dependent snapshots,
8
+ // - publishes a pub/sub event so subscribers refetch.
9
+ //
10
+ // Snapshot rebuild policy: writes invalidate stale snapshot files
11
+ // synchronously, then call `scheduleRebuild` to rebuild them in the
12
+ // background. `getOrBuildSnapshot` keeps a lazy fallback so a report
13
+ // requested before the rebuild reaches that month still returns the
14
+ // right number — it just builds inline. Both paths are byte-identical
15
+ // (enforced by `test/accounting/test_snapshotCache.ts`).
16
+
17
+ import { randomUUID } from "node:crypto";
18
+
19
+ import {
20
+ appendJournal,
21
+ appendJournalBatch,
22
+ bookExists,
23
+ ensureBookDir,
24
+ invalidateAllSnapshots,
25
+ invalidateSnapshotsFrom,
26
+ isSafeBookId,
27
+ listJournalPeriods,
28
+ periodFromDate,
29
+ readAccounts,
30
+ readConfig,
31
+ readJournalMonth,
32
+ removeBookDir,
33
+ writeAccounts,
34
+ writeConfig,
35
+ } from "../utils/files/accounting-io.js";
36
+ import { findActiveOpening, validateOpening } from "./openingBalances.js";
37
+ import { normalizeStoredAccount } from "./accountNormalize.js";
38
+ import { isValidCalendarDate, localDateString, makeEntry, makeVoidEntries, validateEntry, voidedIdSet } from "./journal.js";
39
+ import { aggregateBalances, buildBalanceSheet, buildLedger, buildProfitLoss } from "./report.js";
40
+ import {
41
+ bucketize,
42
+ buildTimeSeries,
43
+ TIME_SERIES_GRANULARITIES,
44
+ TIME_SERIES_METRICS,
45
+ type TimeSeriesGranularity,
46
+ type TimeSeriesMetric,
47
+ type TimeSeriesPoint,
48
+ } from "./timeSeries.js";
49
+ import { awaitRebuildIdle, balancesAtEndOf, cancelRebuild, getOrBuildSnapshot, rebuildAllSnapshots, scheduleRebuild } from "./snapshotCache.js";
50
+ import { publishBookChange, publishBooksChanged } from "./eventPublisher.js";
51
+ import { DEFAULT_ACCOUNTS } from "./defaultAccounts.js";
52
+ import { log } from "../system/logger/index.js";
53
+ import { ACCOUNTING_BOOK_EVENT_KINDS } from "../../src/config/pubsubChannels.js";
54
+ import { isSupportedCountryCode, SUPPORTED_COUNTRY_CODES, type SupportedCountryCode } from "../../src/plugins/accounting/countries.js";
55
+ import { DEFAULT_FISCAL_YEAR_END, FISCAL_YEAR_ENDS, isFiscalYearEnd, type FiscalYearEnd } from "../../src/plugins/accounting/fiscalYear.js";
56
+ import type { Account, AccountingConfig, BookSummary, JournalEntry, JournalLine, ReportPeriod } from "./types.js";
57
+
58
+ export class AccountingError extends Error {
59
+ constructor(
60
+ public status: number,
61
+ message: string,
62
+ public details?: unknown,
63
+ ) {
64
+ super(message);
65
+ this.name = "AccountingError";
66
+ }
67
+ }
68
+
69
+ const DEFAULT_CURRENCY = "USD";
70
+ const GENERATED_ID_RETRIES = 8;
71
+
72
+ function emptyConfig(): AccountingConfig {
73
+ return { books: [] };
74
+ }
75
+
76
+ async function loadOrInitConfig(workspaceRoot?: string): Promise<AccountingConfig> {
77
+ const cfg = await readConfig(workspaceRoot);
78
+ return cfg ?? emptyConfig();
79
+ }
80
+
81
+ function findBook(config: AccountingConfig, bookId: string): BookSummary | null {
82
+ return config.books.find((book) => book.id === bookId) ?? null;
83
+ }
84
+
85
+ function resolveBookId(config: AccountingConfig, requested: string | undefined): string {
86
+ // Every book-touching action now requires an explicit `bookId` —
87
+ // there's no server-side "active book" to fall back on. Callers
88
+ // are the LLM (which is told to pass bookId on each call) and the
89
+ // View (which tracks the current selection in localStorage).
90
+ if (!requested) {
91
+ throw new AccountingError(400, "bookId is required");
92
+ }
93
+ if (!findBook(config, requested)) {
94
+ throw new AccountingError(404, `book ${JSON.stringify(requested)} not found`);
95
+ }
96
+ return requested;
97
+ }
98
+
99
+ async function generateBookId(config: AccountingConfig, workspaceRoot?: string): Promise<string> {
100
+ // 8 hex chars × small N → collision odds are negligible, but a
101
+ // bounded retry keeps the generator total even if one happens.
102
+ for (let attempt = 0; attempt < GENERATED_ID_RETRIES; attempt += 1) {
103
+ const candidate = `book-${randomUUID().slice(0, 8)}`;
104
+ if (!findBook(config, candidate) && !(await bookExists(candidate, workspaceRoot))) return candidate;
105
+ }
106
+ throw new AccountingError(500, "could not generate a unique book id after several attempts");
107
+ }
108
+
109
+ /** Read every journal entry across every month, in period-sorted
110
+ * order. Used by paths that need a full-history view (opening
111
+ * balance lookups, P/L date filtering). */
112
+ async function readAllEntries(bookId: string, workspaceRoot?: string): Promise<JournalEntry[]> {
113
+ const periods = await listJournalPeriods(bookId, workspaceRoot);
114
+ const all: JournalEntry[] = [];
115
+ for (const monthKey of periods) {
116
+ const { entries, skipped } = await readJournalMonth(bookId, monthKey, workspaceRoot);
117
+ for (const entry of entries) all.push(entry);
118
+ if (skipped > 0) {
119
+ // Aggregations and reports built from a partial parse are
120
+ // misleading — log so an operator can spot a corrupted
121
+ // jsonl file. Reads still proceed with what we could parse;
122
+ // refusing here would lock the user out of the whole book
123
+ // for a single bad line.
124
+ log.warn("accounting", "journal month had unparseable lines", { bookId, period: monthKey, skipped });
125
+ }
126
+ }
127
+ return all;
128
+ }
129
+
130
+ // ── books ──────────────────────────────────────────────────────────
131
+
132
+ export async function listBooks(workspaceRoot?: string): Promise<{ books: BookSummary[] }> {
133
+ const config = await loadOrInitConfig(workspaceRoot);
134
+ return { books: config.books };
135
+ }
136
+
137
+ function unsupportedCountryError(received: unknown): AccountingError {
138
+ return new AccountingError(400, `unsupported country code ${JSON.stringify(received)} — must be one of: ${SUPPORTED_COUNTRY_CODES.join(", ")}`);
139
+ }
140
+
141
+ function unsupportedFiscalYearEndError(received: unknown): AccountingError {
142
+ return new AccountingError(400, `unsupported fiscalYearEnd ${JSON.stringify(received)} — must be one of: ${FISCAL_YEAR_ENDS.join(", ")}`);
143
+ }
144
+
145
+ /** Narrow a free-form `fiscalYearEnd` input. Empty / absent → default
146
+ * (back-compat with old callers and pre-field on-disk books); any
147
+ * other value must match the enum or 400. */
148
+ function narrowFiscalYearEnd(raw: string | undefined): FiscalYearEnd {
149
+ if (raw === undefined || raw === "") return DEFAULT_FISCAL_YEAR_END;
150
+ if (!isFiscalYearEnd(raw)) throw unsupportedFiscalYearEndError(raw);
151
+ return raw;
152
+ }
153
+
154
+ /** Boundary checks shared by updateBook. Throws on the first failure
155
+ * so the surrounding function stays under the cognitive-complexity
156
+ * threshold; each rule is also unit-testable independently via the
157
+ * service entry point. */
158
+ function validateUpdateBookInput(input: { name?: string; country?: string; fiscalYearEnd?: string }): void {
159
+ if (input.name !== undefined && (typeof input.name !== "string" || input.name.trim() === "")) {
160
+ throw new AccountingError(400, "name must be a non-empty string when supplied");
161
+ }
162
+ // Empty string is the explicit "clear the field" sentinel from the
163
+ // settings UI; anything else has to land in the curated list, same
164
+ // contract as createBook.
165
+ if (input.country !== undefined && input.country !== "" && !isSupportedCountryCode(input.country)) {
166
+ throw unsupportedCountryError(input.country);
167
+ }
168
+ // Fiscal-year end has no "clear" path — it always resolves to one
169
+ // of Q1..Q4 (back-compat reads default absent → Q4). Reject any
170
+ // non-empty value that isn't in the enum.
171
+ if (input.fiscalYearEnd !== undefined && input.fiscalYearEnd !== "" && !isFiscalYearEnd(input.fiscalYearEnd)) {
172
+ throw unsupportedFiscalYearEndError(input.fiscalYearEnd);
173
+ }
174
+ }
175
+
176
+ export async function createBook(
177
+ input: { id?: string; name: string; currency?: string; country?: string; fiscalYearEnd?: string },
178
+ workspaceRoot?: string,
179
+ ): Promise<{ book: BookSummary }> {
180
+ if (typeof input.name !== "string" || input.name.trim() === "") {
181
+ throw new AccountingError(400, "name is required");
182
+ }
183
+ // Country, when supplied, must be one of the curated codes — keeps
184
+ // the UI dropdown, the role prompt's per-jurisdiction guidance, and
185
+ // the on-disk JSON in sync. A typo from the LLM or an untrusted
186
+ // client is rejected here rather than silently persisted.
187
+ if (input.country !== undefined && !isSupportedCountryCode(input.country)) {
188
+ throw unsupportedCountryError(input.country);
189
+ }
190
+ const fiscalYearEnd = narrowFiscalYearEnd(input.fiscalYearEnd);
191
+ const config = await loadOrInitConfig(workspaceRoot);
192
+ // Auto-generate when no caller id is supplied — every book,
193
+ // including the very first one, gets a generated id. Explicit
194
+ // caller-supplied ids (from a custom config import or a CLI tool)
195
+ // are kept verbatim so users with their own naming scheme can
196
+ // adopt it.
197
+ const bookId = input.id ?? (await generateBookId(config, workspaceRoot));
198
+ // Guard against caller-supplied path-traversal ids before any
199
+ // fs touch (createBook → ensureBookDir → writeAccounts →
200
+ // writeConfig). Auto-generated ids always pass.
201
+ if (!isSafeBookId(bookId)) {
202
+ throw new AccountingError(400, `invalid book id ${JSON.stringify(bookId)} — allowed characters are A-Z a-z 0-9 _ - (1-64 chars; cannot start with _ or -)`);
203
+ }
204
+ if (findBook(config, bookId)) {
205
+ throw new AccountingError(409, `book ${JSON.stringify(bookId)} already exists`);
206
+ }
207
+ if (await bookExists(bookId, workspaceRoot)) {
208
+ throw new AccountingError(409, `book directory ${JSON.stringify(bookId)} already exists on disk`);
209
+ }
210
+ const book: BookSummary = {
211
+ id: bookId,
212
+ name: input.name,
213
+ currency: input.currency ?? DEFAULT_CURRENCY,
214
+ // Narrowed by the isSupportedCountryCode check above.
215
+ ...(input.country ? { country: input.country as SupportedCountryCode } : {}),
216
+ fiscalYearEnd,
217
+ createdAt: new Date().toISOString(),
218
+ };
219
+ await ensureBookDir(bookId, workspaceRoot);
220
+ await writeAccounts(bookId, [...DEFAULT_ACCOUNTS], workspaceRoot);
221
+ const nextConfig: AccountingConfig = { books: [...config.books, book] };
222
+ await writeConfig(nextConfig, workspaceRoot);
223
+ publishBooksChanged();
224
+ return { book };
225
+ }
226
+
227
+ export async function updateBook(
228
+ input: { bookId: string; name?: string; country?: string; fiscalYearEnd?: string },
229
+ workspaceRoot?: string,
230
+ ): Promise<{ book: BookSummary }> {
231
+ const config = await loadOrInitConfig(workspaceRoot);
232
+ const target = findBook(config, input.bookId);
233
+ if (!target) {
234
+ throw new AccountingError(404, `book ${JSON.stringify(input.bookId)} not found`);
235
+ }
236
+ validateUpdateBookInput(input);
237
+ // Currency intentionally absent — once entries reference per-book
238
+ // amounts, switching currency would silently re-interpret every
239
+ // historical figure. Country / name / fiscalYearEnd are pure metadata;
240
+ // safe to swap. Changing fiscalYearEnd does not move any entries —
241
+ // it only changes how the date-range shortcuts resolve from now on.
242
+ const next: BookSummary = {
243
+ ...target,
244
+ ...(input.name !== undefined ? { name: input.name } : {}),
245
+ ...(input.country !== undefined && input.country !== "" ? { country: input.country as SupportedCountryCode } : {}),
246
+ ...(input.fiscalYearEnd !== undefined && input.fiscalYearEnd !== "" ? { fiscalYearEnd: input.fiscalYearEnd as FiscalYearEnd } : {}),
247
+ };
248
+ // Strip an explicitly-cleared country so the JSON file stays clean
249
+ // (matches the createBook policy of omitting the field when unset).
250
+ if (input.country === "") delete next.country;
251
+ const nextConfig: AccountingConfig = {
252
+ books: config.books.map((book) => (book.id === input.bookId ? next : book)),
253
+ };
254
+ await writeConfig(nextConfig, workspaceRoot);
255
+ publishBooksChanged();
256
+ return { book: next };
257
+ }
258
+
259
+ export async function deleteBook(
260
+ input: { bookId: string; confirm: boolean },
261
+ workspaceRoot?: string,
262
+ ): Promise<{ deletedBookId: string; deletedBookName: string }> {
263
+ if (!input.confirm) {
264
+ throw new AccountingError(400, "deleteBook requires confirm: true");
265
+ }
266
+ const config = await loadOrInitConfig(workspaceRoot);
267
+ const target = findBook(config, input.bookId);
268
+ if (!target) {
269
+ throw new AccountingError(404, `book ${JSON.stringify(input.bookId)} not found`);
270
+ }
271
+ // Stop any in-flight rebuild before removing the directory; otherwise
272
+ // writeSnapshot could re-create the tree via mkdir-recursive after
273
+ // we delete it, leaving an orphaned book folder on disk.
274
+ cancelRebuild(input.bookId);
275
+ await awaitRebuildIdle(input.bookId);
276
+ await removeBookDir(input.bookId, workspaceRoot);
277
+ const remaining = config.books.filter((book) => book.id !== input.bookId);
278
+ await writeConfig({ books: remaining }, workspaceRoot);
279
+ publishBooksChanged();
280
+ // Capture the name BEFORE the splice so the LLM-facing message
281
+ // can reference the human-readable book the user just deleted.
282
+ return { deletedBookId: input.bookId, deletedBookName: target.name };
283
+ }
284
+
285
+ // ── accounts ───────────────────────────────────────────────────────
286
+
287
+ export async function listAccounts(input: { bookId?: string }, workspaceRoot?: string): Promise<{ bookId: string; accounts: Account[] }> {
288
+ const config = await loadOrInitConfig(workspaceRoot);
289
+ const bookId = resolveBookId(config, input.bookId);
290
+ return { bookId, accounts: await readAccounts(bookId, workspaceRoot) };
291
+ }
292
+
293
+ export async function upsertAccount(
294
+ input: { bookId?: string; account: Account },
295
+ workspaceRoot?: string,
296
+ ): Promise<{ bookId: string; account: Account; accounts: Account[] }> {
297
+ const config = await loadOrInitConfig(workspaceRoot);
298
+ const bookId = resolveBookId(config, input.bookId);
299
+ // Account codes starting with `_` are reserved for synthetic
300
+ // rows that the report layer injects (e.g. the
301
+ // `_currentEarnings` row added to the Equity section by
302
+ // buildBalanceSheet). Forbid user accounts in that namespace so
303
+ // a B/S can't display two rows with the same code or
304
+ // accidentally lose a real account behind the synthetic label.
305
+ if (typeof input.account?.code !== "string" || input.account.code.length === 0) {
306
+ throw new AccountingError(400, "account code is required");
307
+ }
308
+ if (input.account.code.startsWith("_")) {
309
+ throw new AccountingError(400, `account code ${JSON.stringify(input.account.code)} is reserved (codes starting with _ are used for synthetic report rows)`);
310
+ }
311
+ const accounts = await readAccounts(bookId, workspaceRoot);
312
+ const existingIdx = accounts.findIndex((account) => account.code === input.account.code);
313
+ const next = [...accounts];
314
+ const oldType = existingIdx >= 0 ? accounts[existingIdx].type : null;
315
+ // Whitelist + active-flag policy lives in normalizeStoredAccount
316
+ // (see ./accountNormalize.ts) so the rules are unit-testable in
317
+ // isolation and this service function stays focused on the
318
+ // file-IO + snapshot-invalidation orchestration.
319
+ const stored = normalizeStoredAccount(input.account, existingIdx >= 0 ? accounts[existingIdx] : undefined);
320
+ if (existingIdx >= 0) {
321
+ next[existingIdx] = stored;
322
+ } else {
323
+ next.push(stored);
324
+ }
325
+ await writeAccounts(bookId, next, workspaceRoot);
326
+ // Type changes affect aggregation across periods — drop every
327
+ // snapshot to be safe. Pure name / note changes don't, but
328
+ // distinguishing isn't worth the complexity.
329
+ if (oldType !== null && oldType !== input.account.type) {
330
+ scheduleRebuild(bookId, "0000-00", workspaceRoot);
331
+ await invalidateAllSnapshots(bookId, workspaceRoot);
332
+ }
333
+ publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.accounts });
334
+ return { bookId, account: { ...input.account }, accounts: next };
335
+ }
336
+
337
+ // ── journal entries ────────────────────────────────────────────────
338
+
339
+ export interface AddEntriesItem {
340
+ date: string;
341
+ lines: JournalLine[];
342
+ memo?: string;
343
+ replacesEntryId?: string;
344
+ }
345
+
346
+ interface BatchValidationFailure {
347
+ index: number;
348
+ errors: unknown;
349
+ }
350
+
351
+ // All-or-nothing validation: collect failures across every entry
352
+ // so the whole batch can be rejected before any write touches disk
353
+ // (a half-applied batch can never end up persisted).
354
+ function collectBatchValidationFailures(items: readonly AddEntriesItem[], accounts: readonly Account[]): BatchValidationFailure[] {
355
+ const failures: BatchValidationFailure[] = [];
356
+ for (let idx = 0; idx < items.length; idx++) {
357
+ const item = items[idx];
358
+ const validation = validateEntry({ date: item.date, lines: item.lines, accounts });
359
+ if (!validation.ok) failures.push({ index: idx, errors: validation.errors });
360
+ }
361
+ return failures;
362
+ }
363
+
364
+ function buildBatchEntries(items: readonly AddEntriesItem[]): JournalEntry[] {
365
+ return items.map((item) => makeEntry({ date: item.date, lines: item.lines, memo: item.memo, kind: "normal", replacesEntryId: item.replacesEntryId }));
366
+ }
367
+
368
+ // Snapshot maintenance is driven from the earliest period in the
369
+ // batch — invalidating from that point covers every later month a
370
+ // single-entry call would have invalidated individually, while
371
+ // collapsing the rebuild + publish work into one round.
372
+ function earliestPeriodOf(entries: readonly JournalEntry[]): string {
373
+ return entries.map((entry) => periodFromDate(entry.date)).reduce((min, period) => (period < min ? period : min));
374
+ }
375
+
376
+ export async function addEntries(
377
+ input: { bookId?: string; entries: AddEntriesItem[] },
378
+ workspaceRoot?: string,
379
+ ): Promise<{ bookId: string; entries: JournalEntry[] }> {
380
+ const config = await loadOrInitConfig(workspaceRoot);
381
+ const bookId = resolveBookId(config, input.bookId);
382
+ if (!Array.isArray(input.entries) || input.entries.length === 0) {
383
+ throw new AccountingError(400, "addEntries: entries must be a non-empty array");
384
+ }
385
+ const accounts = await readAccounts(bookId, workspaceRoot);
386
+ const failures = collectBatchValidationFailures(input.entries, accounts);
387
+ if (failures.length > 0) throw new AccountingError(400, "invalid journal entries", failures);
388
+ const built = buildBatchEntries(input.entries);
389
+ // Two-phase batched write: stage every affected month's full new
390
+ // content, then commit all renames at the end. Same-period
391
+ // batches are fully atomic; multi-period failure window is
392
+ // narrowed to the rename phase only.
393
+ await appendJournalBatch(bookId, built, workspaceRoot);
394
+ const earliestPeriod = earliestPeriodOf(built);
395
+ // scheduleRebuild first (sync, sets pendingFromPeriod) so any
396
+ // in-flight rebuild's `isInvalidatedDuringRebuild` check sees the
397
+ // new pending mark before our invalidate races with its write.
398
+ scheduleRebuild(bookId, earliestPeriod, workspaceRoot);
399
+ await invalidateSnapshotsFrom(bookId, earliestPeriod, workspaceRoot);
400
+ publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.journal, period: earliestPeriod });
401
+ return { bookId, entries: built };
402
+ }
403
+
404
+ async function findEntryById(bookId: string, entryId: string, workspaceRoot?: string): Promise<JournalEntry | null> {
405
+ const periods = await listJournalPeriods(bookId, workspaceRoot);
406
+ for (const monthKey of periods) {
407
+ const { entries } = await readJournalMonth(bookId, monthKey, workspaceRoot);
408
+ const hit = entries.find((entry) => entry.id === entryId);
409
+ if (hit) return hit;
410
+ }
411
+ return null;
412
+ }
413
+
414
+ export async function voidEntry(
415
+ input: { bookId?: string; entryId: string; reason?: string; voidDate?: string },
416
+ workspaceRoot?: string,
417
+ ): Promise<{ bookId: string; reverseEntry: JournalEntry; markerEntry: JournalEntry }> {
418
+ const config = await loadOrInitConfig(workspaceRoot);
419
+ const bookId = resolveBookId(config, input.bookId);
420
+ const target = await findEntryById(bookId, input.entryId, workspaceRoot);
421
+ if (!target) {
422
+ throw new AccountingError(404, `entry ${JSON.stringify(input.entryId)} not found`);
423
+ }
424
+ const voidDate = input.voidDate ?? localDateString();
425
+ const { reverse, marker } = makeVoidEntries(target, input.reason, voidDate);
426
+ await appendJournal(bookId, reverse, workspaceRoot);
427
+ await appendJournal(bookId, marker, workspaceRoot);
428
+ // Period whose snapshot is now stale = the older of the
429
+ // original entry's month and the void's month.
430
+ const fromPeriod = target.date < voidDate ? periodFromDate(target.date) : periodFromDate(voidDate);
431
+ scheduleRebuild(bookId, fromPeriod, workspaceRoot);
432
+ await invalidateSnapshotsFrom(bookId, fromPeriod, workspaceRoot);
433
+ publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.journal, period: fromPeriod });
434
+ return { bookId, reverseEntry: reverse, markerEntry: marker };
435
+ }
436
+
437
+ interface ListEntriesInput {
438
+ bookId?: string;
439
+ from?: string;
440
+ to?: string;
441
+ accountCode?: string;
442
+ }
443
+
444
+ function entryMatchesFilters(entry: JournalEntry, input: ListEntriesInput): boolean {
445
+ if (input.from && entry.date < input.from) return false;
446
+ if (input.to && entry.date > input.to) return false;
447
+ if (input.accountCode && !entry.lines.some((line) => line.accountCode === input.accountCode)) return false;
448
+ return true;
449
+ }
450
+
451
+ export async function listEntries(
452
+ input: ListEntriesInput,
453
+ workspaceRoot?: string,
454
+ ): Promise<{ bookId: string; entries: JournalEntry[]; voidedEntryIds: string[] }> {
455
+ const config = await loadOrInitConfig(workspaceRoot);
456
+ const bookId = resolveBookId(config, input.bookId);
457
+ const periods = await listJournalPeriods(bookId, workspaceRoot);
458
+ const entries: JournalEntry[] = [];
459
+ // Collect voided ids from the *unfiltered* set across every month —
460
+ // an account-filtered query drops void-marker rows (they have no
461
+ // lines), so deriving voided ids from the filtered list misses
462
+ // them and the View loses the strikeout on the cancelled original.
463
+ const allVoidedIds = new Set<string>();
464
+ for (const monthKey of periods) {
465
+ const { entries: monthEntries } = await readJournalMonth(bookId, monthKey, workspaceRoot);
466
+ for (const voidedId of voidedIdSet(monthEntries)) allVoidedIds.add(voidedId);
467
+ if (input.from && monthKey < input.from.slice(0, 7)) continue;
468
+ if (input.to && monthKey > input.to.slice(0, 7)) continue;
469
+ for (const entry of monthEntries) {
470
+ if (entryMatchesFilters(entry, input)) entries.push(entry);
471
+ }
472
+ }
473
+ return { bookId, entries, voidedEntryIds: Array.from(allVoidedIds).sort() };
474
+ }
475
+
476
+ // ── opening balances ───────────────────────────────────────────────
477
+
478
+ export async function getOpeningBalances(input: { bookId?: string }, workspaceRoot?: string): Promise<{ bookId: string; opening: JournalEntry | null }> {
479
+ const config = await loadOrInitConfig(workspaceRoot);
480
+ const bookId = resolveBookId(config, input.bookId);
481
+ const all = await readAllEntries(bookId, workspaceRoot);
482
+ return { bookId, opening: findActiveOpening(all) };
483
+ }
484
+
485
+ export async function setOpeningBalances(
486
+ input: { bookId?: string; asOfDate: string; lines: JournalLine[]; memo?: string },
487
+ workspaceRoot?: string,
488
+ ): Promise<{ bookId: string; openingEntry: JournalEntry; replacedExisting: boolean }> {
489
+ const config = await loadOrInitConfig(workspaceRoot);
490
+ const bookId = resolveBookId(config, input.bookId);
491
+ const accounts = await readAccounts(bookId, workspaceRoot);
492
+ const all = await readAllEntries(bookId, workspaceRoot);
493
+ const validation = validateOpening({
494
+ asOfDate: input.asOfDate,
495
+ lines: input.lines,
496
+ accounts,
497
+ existingEntries: all,
498
+ });
499
+ if (!validation.ok) {
500
+ throw new AccountingError(400, "invalid opening balances", validation.errors);
501
+ }
502
+ // Replace-mode: void any existing active opening so the new one
503
+ // is unambiguous. The marker is dated today (when the void
504
+ // happened), not the original opening date.
505
+ const existing = findActiveOpening(all);
506
+ if (existing) {
507
+ const today = localDateString();
508
+ const { reverse, marker } = makeVoidEntries(existing, "replaced via setOpeningBalances", today);
509
+ await appendJournal(bookId, reverse, workspaceRoot);
510
+ await appendJournal(bookId, marker, workspaceRoot);
511
+ }
512
+ const opening = makeEntry({
513
+ date: input.asOfDate,
514
+ lines: input.lines,
515
+ memo: input.memo ?? "Opening balances",
516
+ kind: "opening",
517
+ });
518
+ await appendJournal(bookId, opening, workspaceRoot);
519
+ scheduleRebuild(bookId, "0000-00", workspaceRoot);
520
+ await invalidateAllSnapshots(bookId, workspaceRoot);
521
+ publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.opening });
522
+ return { bookId, openingEntry: opening, replacedExisting: existing !== null };
523
+ }
524
+
525
+ // ── reports ────────────────────────────────────────────────────────
526
+
527
+ function endDateOfPeriod(period: ReportPeriod): string {
528
+ if (period.kind === "month") {
529
+ const [year, month] = period.period.split("-").map((segment) => parseInt(segment, 10));
530
+ const last = new Date(Date.UTC(year, month, 0)).getUTCDate();
531
+ return `${period.period}-${String(last).padStart(2, "0")}`;
532
+ }
533
+ return period.to;
534
+ }
535
+
536
+ export async function getBalanceSheetReport(
537
+ input: { bookId?: string; period: ReportPeriod },
538
+ workspaceRoot?: string,
539
+ ): Promise<{ bookId: string; balanceSheet: ReturnType<typeof buildBalanceSheet> }> {
540
+ const config = await loadOrInitConfig(workspaceRoot);
541
+ const bookId = resolveBookId(config, input.bookId);
542
+ const accounts = await readAccounts(bookId, workspaceRoot);
543
+ const balances = await balancesAsOf(bookId, input.period, workspaceRoot);
544
+ return {
545
+ bookId,
546
+ balanceSheet: buildBalanceSheet({
547
+ accounts,
548
+ balances,
549
+ asOf: endDateOfPeriod(input.period),
550
+ }),
551
+ };
552
+ }
553
+
554
+ /** Resolve closing balances at the end of a `ReportPeriod`. Month
555
+ * periods hit the snapshot cache; range periods with a mid-month
556
+ * `to` date have to filter the journal directly because the
557
+ * end-of-month snapshot would include activity past `to`. */
558
+ async function balancesAsOf(bookId: string, period: ReportPeriod, workspaceRoot?: string): Promise<ReturnType<typeof aggregateBalances>> {
559
+ if (period.kind === "month") {
560
+ const snap = await getOrBuildSnapshot(bookId, period.period, workspaceRoot);
561
+ return [...snap.balances];
562
+ }
563
+ const all = await readAllEntries(bookId, workspaceRoot);
564
+ const filtered = all.filter((entry) => entry.date <= period.to);
565
+ return aggregateBalances(filtered);
566
+ }
567
+
568
+ export async function getProfitLossReport(
569
+ input: { bookId?: string; period: ReportPeriod },
570
+ workspaceRoot?: string,
571
+ ): Promise<{ bookId: string; profitLoss: ReturnType<typeof buildProfitLoss> }> {
572
+ const config = await loadOrInitConfig(workspaceRoot);
573
+ const bookId = resolveBookId(config, input.bookId);
574
+ const accounts = await readAccounts(bookId, workspaceRoot);
575
+ const all = await readAllEntries(bookId, workspaceRoot);
576
+ const fromDate = input.period.kind === "month" ? `${input.period.period}-01` : input.period.from;
577
+ const toDate = endDateOfPeriod(input.period);
578
+ return { bookId, profitLoss: buildProfitLoss({ accounts, entries: all, from: fromDate, to: toDate }) };
579
+ }
580
+
581
+ export async function getLedgerReport(
582
+ input: { bookId?: string; accountCode: string; period?: ReportPeriod },
583
+ workspaceRoot?: string,
584
+ ): Promise<{ bookId: string; ledger: ReturnType<typeof buildLedger> }> {
585
+ const config = await loadOrInitConfig(workspaceRoot);
586
+ const bookId = resolveBookId(config, input.bookId);
587
+ const accounts = await readAccounts(bookId, workspaceRoot);
588
+ const account = accounts.find((acct) => acct.code === input.accountCode);
589
+ if (!account) {
590
+ throw new AccountingError(404, `account ${JSON.stringify(input.accountCode)} not found`);
591
+ }
592
+ const all = await readAllEntries(bookId, workspaceRoot);
593
+ const fromDate = input.period?.kind === "month" ? `${input.period.period}-01` : input.period?.from;
594
+ const toDate = input.period ? endDateOfPeriod(input.period) : undefined;
595
+ return { bookId, ledger: buildLedger({ account, entries: all, from: fromDate, to: toDate }) };
596
+ }
597
+
598
+ // ── time series ────────────────────────────────────────────────────
599
+
600
+ function ensureValidYmd(label: string, value: unknown): string {
601
+ // Reuse the journal-side helper so impossible days (`2025-02-30`,
602
+ // `2025-13-01`) AND silent year drift (`0099-01-01` → 1999 via
603
+ // `Date.UTC` legacy two-digit handling) are both rejected — rolling
604
+ // a separate regex+lastDay check here missed the latter.
605
+ if (typeof value !== "string" || !isValidCalendarDate(value)) {
606
+ throw new AccountingError(400, `getTimeSeries: ${label} must be a valid YYYY-MM-DD calendar date`);
607
+ }
608
+ return value;
609
+ }
610
+
611
+ function ensureMetric(value: unknown): TimeSeriesMetric {
612
+ if (typeof value !== "string" || !(TIME_SERIES_METRICS as readonly string[]).includes(value)) {
613
+ throw new AccountingError(400, `getTimeSeries: metric must be one of ${TIME_SERIES_METRICS.join(", ")}`);
614
+ }
615
+ return value as TimeSeriesMetric;
616
+ }
617
+
618
+ function ensureGranularity(value: unknown): TimeSeriesGranularity {
619
+ if (typeof value !== "string" || !(TIME_SERIES_GRANULARITIES as readonly string[]).includes(value)) {
620
+ throw new AccountingError(400, `getTimeSeries: granularity must be one of ${TIME_SERIES_GRANULARITIES.join(", ")}`);
621
+ }
622
+ return value as TimeSeriesGranularity;
623
+ }
624
+
625
+ function resolveAccountCode(metric: TimeSeriesMetric, raw: unknown): string | undefined {
626
+ if (metric === "accountBalance") {
627
+ if (typeof raw !== "string" || raw === "") {
628
+ throw new AccountingError(400, "getTimeSeries: accountCode is required when metric is accountBalance");
629
+ }
630
+ return raw;
631
+ }
632
+ if (raw !== undefined && raw !== "") {
633
+ throw new AccountingError(400, "getTimeSeries: accountCode is only allowed when metric is accountBalance");
634
+ }
635
+ return undefined;
636
+ }
637
+
638
+ export interface TimeSeriesReportInput {
639
+ bookId?: string;
640
+ metric: unknown;
641
+ granularity: unknown;
642
+ from: unknown;
643
+ to: unknown;
644
+ accountCode?: unknown;
645
+ }
646
+
647
+ export interface TimeSeriesReport {
648
+ bookId: string;
649
+ metric: TimeSeriesMetric;
650
+ granularity: TimeSeriesGranularity;
651
+ from: string;
652
+ to: string;
653
+ accountCode?: string;
654
+ points: TimeSeriesPoint[];
655
+ }
656
+
657
+ interface ValidatedTimeSeriesInput {
658
+ metric: TimeSeriesMetric;
659
+ granularity: TimeSeriesGranularity;
660
+ from: string;
661
+ toDate: string;
662
+ accountCode: string | undefined;
663
+ }
664
+
665
+ function validateTimeSeriesInput(input: TimeSeriesReportInput): ValidatedTimeSeriesInput {
666
+ const metric = ensureMetric(input.metric);
667
+ const granularity = ensureGranularity(input.granularity);
668
+ const from = ensureValidYmd("from", input.from);
669
+ const toDate = ensureValidYmd("to", input.to);
670
+ if (from > toDate) throw new AccountingError(400, "getTimeSeries: from must be on or before to");
671
+ const accountCode = resolveAccountCode(metric, input.accountCode);
672
+ return { metric, granularity, from, toDate, accountCode };
673
+ }
674
+
675
+ interface TimeSeriesBookContext {
676
+ bookId: string;
677
+ fiscalYearEnd: FiscalYearEnd;
678
+ accounts: Account[];
679
+ }
680
+
681
+ async function loadTimeSeriesBookContext(requestedBookId: string | undefined, workspaceRoot?: string): Promise<TimeSeriesBookContext> {
682
+ const config = await loadOrInitConfig(workspaceRoot);
683
+ const bookId = resolveBookId(config, requestedBookId);
684
+ const book = findBook(config, bookId);
685
+ // resolveBookId guarantees the book exists; this fallback is for
686
+ // the type-checker only.
687
+ const fiscalYearEnd: FiscalYearEnd = book?.fiscalYearEnd ?? DEFAULT_FISCAL_YEAR_END;
688
+ const accounts = await readAccounts(bookId, workspaceRoot);
689
+ return { bookId, fiscalYearEnd, accounts };
690
+ }
691
+
692
+ export async function getTimeSeriesReport(input: TimeSeriesReportInput, workspaceRoot?: string): Promise<TimeSeriesReport> {
693
+ const { metric, granularity, from, toDate, accountCode } = validateTimeSeriesInput(input);
694
+ const { bookId, fiscalYearEnd, accounts } = await loadTimeSeriesBookContext(input.bookId, workspaceRoot);
695
+ if (accountCode && !accounts.some((acct) => acct.code === accountCode)) {
696
+ throw new AccountingError(404, `getTimeSeries: account ${JSON.stringify(accountCode)} not found`);
697
+ }
698
+ const entries = await readAllEntries(bookId, workspaceRoot);
699
+ const buckets = bucketize({ from, to: toDate, granularity, fiscalYearEnd });
700
+ const points = buildTimeSeries({ buckets, entries, accounts, metric, accountCode });
701
+ const report: TimeSeriesReport = { bookId, metric, granularity, from, to: toDate, points };
702
+ if (accountCode) report.accountCode = accountCode;
703
+ return report;
704
+ }
705
+
706
+ // ── snapshot admin ─────────────────────────────────────────────────
707
+
708
+ export async function rebuildSnapshots(input: { bookId?: string }, workspaceRoot?: string): Promise<{ bookId: string; rebuilt: string[] }> {
709
+ const config = await loadOrInitConfig(workspaceRoot);
710
+ const bookId = resolveBookId(config, input.bookId);
711
+ const result = await rebuildAllSnapshots(bookId, workspaceRoot);
712
+ publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.snapshotsReady });
713
+ return { bookId, rebuilt: result.rebuilt };
714
+ }
715
+
716
+ // Direct access for tests / lazy paths that want to bypass the
717
+ // snapshot cache.
718
+ export { aggregateBalances, balancesAtEndOf };