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,333 @@
1
+ // Monthly balance snapshot cache.
2
+ //
3
+ // Source of truth: the journal JSONL files. Snapshots are derived
4
+ // state — `data/accounting/books/<id>/snapshots/YYYY-MM.json` is
5
+ // only ever a perf optimization. The invariant we maintain:
6
+ //
7
+ // for any (book, period) pair,
8
+ // getOrBuildSnapshot(book, period)
9
+ // ===
10
+ // aggregateBalances(<all entries up to period end>)
11
+ //
12
+ // I.e. running with snapshots and running without snapshots must
13
+ // produce byte-identical results. The unit test for this lives in
14
+ // `test/accounting/test_snapshotCache.ts`.
15
+ //
16
+ // Rebuild policy: writes call `scheduleRebuild(bookId, fromPeriod)`
17
+ // after invalidating stale snapshot files. Each book has at most one
18
+ // rebuild in flight; additional writes during a running rebuild merge
19
+ // into a single queued follow-up (so a burst of N writes runs at most
20
+ // two rebuilds). `getOrBuildSnapshot` keeps a lazy fallback so a
21
+ // report requested before the rebuild reaches that month is still
22
+ // correct — it just builds inline.
23
+ //
24
+ // Test API: `awaitRebuildIdle(bookId)` and `inspectRebuildQueue(bookId)`
25
+ // are diagnostics that let tests assert on queue state without
26
+ // sleep-and-poll. Production code never needs them.
27
+
28
+ import {
29
+ invalidateSnapshotsFrom as ioInvalidateFrom,
30
+ invalidateAllSnapshots as ioInvalidateAll,
31
+ listJournalPeriods,
32
+ readJournalMonth,
33
+ readSnapshot,
34
+ writeSnapshot,
35
+ } from "../utils/files/accounting-io.js";
36
+ import { aggregateBalances } from "./report.js";
37
+ import { publishBookChange } from "./eventPublisher.js";
38
+ import { log } from "../system/logger/index.js";
39
+ import { ACCOUNTING_BOOK_EVENT_KINDS } from "../../src/config/pubsubChannels.js";
40
+ import type { AccountBalance, JournalEntry, MonthSnapshot } from "./types.js";
41
+
42
+ function previousPeriod(period: string): string {
43
+ // YYYY-MM → previous YYYY-MM. December rolls back to the previous
44
+ // year's December.
45
+ const [year, month] = period.split("-").map((segment) => parseInt(segment, 10));
46
+ if (month === 1) return `${(year - 1).toString().padStart(4, "0")}-12`;
47
+ return `${year.toString().padStart(4, "0")}-${(month - 1).toString().padStart(2, "0")}`;
48
+ }
49
+
50
+ function mergeBalances(base: readonly AccountBalance[], delta: readonly AccountBalance[]): AccountBalance[] {
51
+ const map = new Map<string, number>();
52
+ for (const row of base) map.set(row.accountCode, row.netDebit);
53
+ for (const row of delta) {
54
+ map.set(row.accountCode, (map.get(row.accountCode) ?? 0) + row.netDebit);
55
+ }
56
+ return Array.from(map.entries())
57
+ .map(([accountCode, netDebit]) => ({ accountCode, netDebit }))
58
+ .sort((lhs, rhs) => lhs.accountCode.localeCompare(rhs.accountCode));
59
+ }
60
+
61
+ async function buildEmptySnapshot(bookId: string, period: string, workspaceRoot?: string): Promise<MonthSnapshot> {
62
+ const empty: MonthSnapshot = { period, balances: [], builtAt: new Date().toISOString() };
63
+ await writeSnapshot(bookId, empty, workspaceRoot);
64
+ return empty;
65
+ }
66
+
67
+ /** Build a snapshot at end-of-`period` for one book, lazily relying
68
+ * on the previous month's snapshot if it exists. Falls all the way
69
+ * back to the earliest journal month if no upstream snapshot is
70
+ * available. Always writes the result to disk before returning. */
71
+ export async function getOrBuildSnapshot(bookId: string, period: string, workspaceRoot?: string): Promise<MonthSnapshot> {
72
+ const cached = await readSnapshot(bookId, period, workspaceRoot);
73
+ if (cached) return cached;
74
+
75
+ // Earliest journal month determines where the recursion stops.
76
+ // If the book has no journal at all, return an empty snapshot.
77
+ const periods = await listJournalPeriods(bookId, workspaceRoot);
78
+ if (periods.length === 0 || period < periods[0]) {
79
+ return buildEmptySnapshot(bookId, period, workspaceRoot);
80
+ }
81
+
82
+ const { entries } = await readJournalMonth(bookId, period, workspaceRoot);
83
+ const monthDelta = aggregateBalances(entries);
84
+
85
+ // Get the prior month's closing snapshot — recurse, which will
86
+ // either hit cache or build the chain back to the start.
87
+ let priorBalances: readonly AccountBalance[] = [];
88
+ if (period > periods[0]) {
89
+ const prior = previousPeriod(period);
90
+ const priorSnap = await getOrBuildSnapshot(bookId, prior, workspaceRoot);
91
+ priorBalances = priorSnap.balances;
92
+ }
93
+ const merged = mergeBalances(priorBalances, monthDelta);
94
+ const snap: MonthSnapshot = {
95
+ period,
96
+ balances: merged,
97
+ builtAt: new Date().toISOString(),
98
+ };
99
+ await writeSnapshot(bookId, snap, workspaceRoot);
100
+ return snap;
101
+ }
102
+
103
+ /** Compute closing balances at end-of-`period` from journal alone,
104
+ * bypassing the snapshot cache. Used by the byte-equality
105
+ * invariant test, and as a safety net for "compute without
106
+ * trusting cache" paths. */
107
+ export async function balancesAtEndOf(bookId: string, period: string, workspaceRoot?: string): Promise<AccountBalance[]> {
108
+ const periods = await listJournalPeriods(bookId, workspaceRoot);
109
+ const all: JournalEntry[] = [];
110
+ for (const monthKey of periods) {
111
+ if (period < monthKey) break;
112
+ const { entries } = await readJournalMonth(bookId, monthKey, workspaceRoot);
113
+ for (const entry of entries) all.push(entry);
114
+ }
115
+ return aggregateBalances(all);
116
+ }
117
+
118
+ /** Drop snapshots for `fromPeriod` and later. Re-export from
119
+ * accounting-io for callers that conceptually live in the cache
120
+ * layer (so they don't reach into the IO module). */
121
+ export async function invalidateSnapshotsFrom(bookId: string, fromPeriod: string, workspaceRoot?: string): Promise<{ removed: string[] }> {
122
+ return ioInvalidateFrom(bookId, fromPeriod, workspaceRoot);
123
+ }
124
+
125
+ /** Drop all snapshots and rebuild from scratch. Used by the
126
+ * `rebuildSnapshots` admin action. Returns the periods that were
127
+ * rebuilt. */
128
+ export async function rebuildAllSnapshots(bookId: string, workspaceRoot?: string): Promise<{ rebuilt: string[] }> {
129
+ await ioInvalidateAll(bookId, workspaceRoot);
130
+ const periods = await listJournalPeriods(bookId, workspaceRoot);
131
+ for (const monthKey of periods) {
132
+ await getOrBuildSnapshot(bookId, monthKey, workspaceRoot);
133
+ }
134
+ return { rebuilt: periods };
135
+ }
136
+
137
+ // ── async rebuild queue ────────────────────────────────────────────
138
+ //
139
+ // Per-book queue. A `running` promise represents the in-flight
140
+ // rebuild. While it's running, additional `scheduleRebuild` calls
141
+ // merge their `fromPeriod` into `pendingFromPeriod` (taking the
142
+ // minimum so the next pass covers everyone's invalidation), and we
143
+ // kick off a follow-up once the current one resolves.
144
+
145
+ interface RebuildQueueEntry {
146
+ running: Promise<void>;
147
+ pendingFromPeriod: string | null;
148
+ pendingWorkspaceRoot: string | undefined;
149
+ coalescedWriteCount: number;
150
+ runningFromPeriod: string;
151
+ /** Set by `cancelRebuild` (called from `deleteBook`). The runRebuild
152
+ * loop checks before each write so a rebuild cannot resurrect the
153
+ * book directory after `removeBookDir` has run. */
154
+ cancelled: boolean;
155
+ }
156
+
157
+ const rebuildQueues = new Map<string, RebuildQueueEntry>();
158
+
159
+ function minPeriod(lhs: string | null, rhs: string): string {
160
+ if (lhs === null) return rhs;
161
+ return lhs < rhs ? lhs : rhs;
162
+ }
163
+
164
+ function isInvalidatedDuringRebuild(bookId: string, period: string): boolean {
165
+ // A pending invalidation that covers `period` means the queued
166
+ // follow-up rebuild will redo this period. Skip the in-flight
167
+ // write so we don't pollute the cache with stale data while a
168
+ // fresher computation is queued. Without this guard, the
169
+ // sequence "rebuild reads journal → caller writes a new entry →
170
+ // caller invalidates → rebuild writes (stale) snapshot" leaves
171
+ // the cache lying about the latest state.
172
+ const queue = rebuildQueues.get(bookId);
173
+ return queue !== undefined && queue.pendingFromPeriod !== null && period >= queue.pendingFromPeriod;
174
+ }
175
+
176
+ function isCancelled(bookId: string): boolean {
177
+ return rebuildQueues.get(bookId)?.cancelled === true;
178
+ }
179
+
180
+ async function runRebuild(bookId: string, fromPeriod: string, workspaceRoot: string | undefined): Promise<void> {
181
+ const startedAt = Date.now();
182
+ log.info("accounting", "snapshot rebuild started", { bookId, fromPeriod });
183
+ publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.snapshotsRebuilding, period: fromPeriod });
184
+ const periods = await listJournalPeriods(bookId, workspaceRoot);
185
+ const targets = periods.filter((monthKey) => monthKey >= fromPeriod);
186
+ let written = 0;
187
+ for (const monthKey of targets) {
188
+ if (isCancelled(bookId)) break;
189
+ if (isInvalidatedDuringRebuild(bookId, monthKey)) break;
190
+ // Compute fresh from journal — bypasses getOrBuildSnapshot's
191
+ // own write side-effect so the staleness check below is the
192
+ // only writer in the rebuild path.
193
+ const balances = await balancesAtEndOf(bookId, monthKey, workspaceRoot);
194
+ if (isCancelled(bookId)) break;
195
+ if (isInvalidatedDuringRebuild(bookId, monthKey)) break;
196
+ await writeSnapshot(bookId, { period: monthKey, balances, builtAt: new Date().toISOString() }, workspaceRoot);
197
+ if (isCancelled(bookId)) {
198
+ // The book was deleted between our last check and the write —
199
+ // `writeSnapshot` will have re-created the book directory tree
200
+ // via mkdir-recursive. Undo it so we don't leave an orphaned
201
+ // directory after `deleteBook` has run.
202
+ await ioInvalidateFrom(bookId, monthKey, workspaceRoot);
203
+ break;
204
+ }
205
+ if (isInvalidatedDuringRebuild(bookId, monthKey)) {
206
+ // A concurrent invalidate raced ahead between our last check
207
+ // and the disk write. The data we just wrote may be stale
208
+ // relative to the latest journal — undo so the queued
209
+ // follow-up rebuild starts from a clean slate.
210
+ await ioInvalidateFrom(bookId, monthKey, workspaceRoot);
211
+ break;
212
+ }
213
+ written += 1;
214
+ publishBookChange(bookId, { kind: ACCOUNTING_BOOK_EVENT_KINDS.snapshotsReady, period: monthKey });
215
+ }
216
+ log.info("accounting", "snapshot rebuild done", { bookId, periods: written, durationMs: Date.now() - startedAt });
217
+ }
218
+
219
+ function startRebuild(bookId: string, fromPeriod: string, workspaceRoot: string | undefined): RebuildQueueEntry {
220
+ const entry: RebuildQueueEntry = {
221
+ running: Promise.resolve(),
222
+ pendingFromPeriod: null,
223
+ pendingWorkspaceRoot: undefined,
224
+ coalescedWriteCount: 1,
225
+ runningFromPeriod: fromPeriod,
226
+ cancelled: false,
227
+ };
228
+ entry.running = runRebuild(bookId, fromPeriod, workspaceRoot)
229
+ .catch((err) => {
230
+ // A rebuild failure is logged but does not poison the queue —
231
+ // the next `scheduleRebuild` call will start a fresh promise.
232
+ log.error("accounting", "snapshot rebuild failed", { bookId, fromPeriod, error: err instanceof Error ? err.message : String(err) });
233
+ })
234
+ .then(() => {
235
+ // Drain any work that piled up while we were running.
236
+ const current = rebuildQueues.get(bookId);
237
+ if (!current) return;
238
+ // If the book was cancelled mid-rebuild (e.g. deleteBook ran),
239
+ // drop the queue entry entirely — we must not start a successor
240
+ // that would re-create the deleted book directory.
241
+ if (current.cancelled) {
242
+ rebuildQueues.delete(bookId);
243
+ return;
244
+ }
245
+ if (current.pendingFromPeriod !== null) {
246
+ const nextFrom = current.pendingFromPeriod;
247
+ const nextRoot = current.pendingWorkspaceRoot;
248
+ const carriedCount = current.coalescedWriteCount;
249
+ const successor = startRebuild(bookId, nextFrom, nextRoot);
250
+ successor.coalescedWriteCount += carriedCount;
251
+ rebuildQueues.set(bookId, successor);
252
+ } else {
253
+ rebuildQueues.delete(bookId);
254
+ }
255
+ });
256
+ return entry;
257
+ }
258
+
259
+ /** Schedule a background rebuild for `bookId` starting at `fromPeriod`.
260
+ * Multiple calls during an in-flight rebuild coalesce into a single
261
+ * follow-up rebuild that covers the minimum `fromPeriod` seen.
262
+ * Returns immediately — the rebuild runs on its own promise chain. */
263
+ export function scheduleRebuild(bookId: string, fromPeriod: string, workspaceRoot?: string): void {
264
+ const existing = rebuildQueues.get(bookId);
265
+ if (!existing) {
266
+ rebuildQueues.set(bookId, startRebuild(bookId, fromPeriod, workspaceRoot));
267
+ return;
268
+ }
269
+ existing.pendingFromPeriod = minPeriod(existing.pendingFromPeriod, fromPeriod);
270
+ existing.pendingWorkspaceRoot = workspaceRoot;
271
+ existing.coalescedWriteCount += 1;
272
+ }
273
+
274
+ /** Test/diagnostic: resolves when no rebuild is running or queued for
275
+ * `bookId`. Also called by `deleteBook` after `cancelRebuild` to
276
+ * ensure a previously running rebuild has fully stopped before the
277
+ * caller removes the book's directory on disk. */
278
+ export async function awaitRebuildIdle(bookId: string): Promise<void> {
279
+ while (rebuildQueues.has(bookId)) {
280
+ const entry = rebuildQueues.get(bookId);
281
+ if (!entry) return;
282
+ await entry.running;
283
+ }
284
+ }
285
+
286
+ /** Mark the book's in-flight rebuild as cancelled. The runRebuild
287
+ * loop checks before each write and bails out, so a subsequent
288
+ * `removeBookDir` cannot race with a `writeSnapshot` that would
289
+ * re-create the directory tree. Pair with `awaitRebuildIdle(bookId)`
290
+ * to wait for the in-flight rebuild to finish bailing. */
291
+ export function cancelRebuild(bookId: string): void {
292
+ const entry = rebuildQueues.get(bookId);
293
+ if (!entry) return;
294
+ entry.cancelled = true;
295
+ // Drop pending too — a cancelled book should not get a successor
296
+ // rebuild after the in-flight one drains.
297
+ entry.pendingFromPeriod = null;
298
+ }
299
+
300
+ /** Test/diagnostic: snapshot of the per-book queue state. Stable
301
+ * enough to assert against; fields may grow over time. */
302
+ export function inspectRebuildQueue(bookId: string): {
303
+ running: boolean;
304
+ runningFromPeriod: string | null;
305
+ pendingFromPeriod: string | null;
306
+ coalescedWriteCount: number;
307
+ } {
308
+ const entry = rebuildQueues.get(bookId);
309
+ if (!entry) {
310
+ return { running: false, runningFromPeriod: null, pendingFromPeriod: null, coalescedWriteCount: 0 };
311
+ }
312
+ return {
313
+ running: true,
314
+ runningFromPeriod: entry.runningFromPeriod,
315
+ pendingFromPeriod: entry.pendingFromPeriod,
316
+ coalescedWriteCount: entry.coalescedWriteCount,
317
+ };
318
+ }
319
+
320
+ /** Test-only — drain all in-flight rebuilds, then drop queue state.
321
+ * Awaiting first means a leftover rebuild can't continue writing
322
+ * into the next test's tmp dir after we clear the bookkeeping. */
323
+ export async function _resetRebuildQueueForTesting(): Promise<void> {
324
+ // Mark everything cancelled so loops bail at their next checkpoint
325
+ // instead of continuing through every period.
326
+ for (const entry of rebuildQueues.values()) {
327
+ entry.cancelled = true;
328
+ entry.pendingFromPeriod = null;
329
+ }
330
+ const pending = Array.from(rebuildQueues.values()).map((entry) => entry.running);
331
+ await Promise.allSettled(pending);
332
+ rebuildQueues.clear();
333
+ }
@@ -0,0 +1,265 @@
1
+ // Time-series aggregation for the accounting plugin: bucketise a
2
+ // date range by month / fiscal quarter / fiscal year and roll up a
3
+ // metric (revenue / expense / net income / closing balance of a
4
+ // specific account) into a chart-ready `(label, value)[]` series.
5
+ //
6
+ // LLM-facing only — the in-canvas Accounting `<View>` keeps using
7
+ // `getReport` for its tab-driven UI. `getTimeSeries` exists so a
8
+ // single tool round-trip can answer "chart my quarterly revenue
9
+ // over the last two years" without the LLM fanning out N calls and
10
+ // stitching the buckets itself.
11
+ //
12
+ // Pure module: no I/O, no service-layer awareness. Caller hands in
13
+ // the entries / accounts already loaded; we return the points.
14
+
15
+ import type { Account, AccountType, JournalEntry } from "./types.js";
16
+ import { aggregateBalances } from "./report.js";
17
+ import { fiscalYearEndMonth, type FiscalYearEnd } from "../../src/plugins/accounting/fiscalYear.js";
18
+ import {
19
+ TIME_SERIES_GRANULARITIES,
20
+ TIME_SERIES_METRICS,
21
+ type TimeSeriesGranularity,
22
+ type TimeSeriesMetric,
23
+ } from "../../src/plugins/accounting/timeSeriesEnums.js";
24
+
25
+ export { TIME_SERIES_GRANULARITIES, TIME_SERIES_METRICS };
26
+ export type { TimeSeriesGranularity, TimeSeriesMetric };
27
+
28
+ export interface Bucket {
29
+ /** Inclusive YYYY-MM-DD lower bound. */
30
+ from: string;
31
+ /** Inclusive YYYY-MM-DD upper bound. */
32
+ to: string;
33
+ /** Chart x-axis label. Format depends on granularity:
34
+ * "YYYY-MM" / "FY{endYear}-Q{1..4}" / "FY{endYear}". For fiscal
35
+ * years that don't align with the calendar year (Q1/Q2/Q3 books)
36
+ * the FY is named by its END calendar year — matches Japanese
37
+ * "令和7年度" convention (Apr 2025 - Mar 2026 = FY2026). */
38
+ label: string;
39
+ }
40
+
41
+ export interface TimeSeriesPoint {
42
+ label: string;
43
+ from: string;
44
+ to: string;
45
+ /** Single number, natural-sign per metric. Revenue and net income
46
+ * positive when income exceeds expense; expense reported as a
47
+ * positive cost; account balance follows the account's display
48
+ * sign (assets debit-positive, liabilities/equity credit-positive). */
49
+ value: number;
50
+ }
51
+
52
+ // ── date arithmetic (pure, no Date for parsing/formatting) ─────────
53
+
54
+ function pad2(num: number): string {
55
+ return String(num).padStart(2, "0");
56
+ }
57
+
58
+ function fmtYmd(year: number, month: number, day: number): string {
59
+ return `${year}-${pad2(month)}-${pad2(day)}`;
60
+ }
61
+
62
+ interface YmdParts {
63
+ year: number;
64
+ month: number;
65
+ day: number;
66
+ }
67
+
68
+ function parseYmd(value: string): YmdParts {
69
+ const [year, month, day] = value.split("-").map((segment) => parseInt(segment, 10));
70
+ return { year, month, day };
71
+ }
72
+
73
+ function lastDayOf(year: number, month: number): number {
74
+ // UTC day 0 of next month = last day of this month, immune to TZ.
75
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
76
+ }
77
+
78
+ function addDay(date: YmdParts): YmdParts {
79
+ const stepped = new Date(Date.UTC(date.year, date.month - 1, date.day + 1));
80
+ return {
81
+ year: stepped.getUTCFullYear(),
82
+ month: stepped.getUTCMonth() + 1,
83
+ day: stepped.getUTCDate(),
84
+ };
85
+ }
86
+
87
+ // ── fiscal-year arithmetic (year-month, no Date) ───────────────────
88
+
89
+ interface FyAnchor {
90
+ /** Calendar year the fiscal year STARTED in. */
91
+ fyStartYear: number;
92
+ /** Calendar year the fiscal year ENDS in (used for labelling). */
93
+ fyEndYear: number;
94
+ /** First calendar month of the fiscal year (1-based). */
95
+ startMonth: number;
96
+ }
97
+
98
+ function fyAnchorFor(date: YmdParts, end: FiscalYearEnd): FyAnchor {
99
+ const closingMonth = fiscalYearEndMonth(end);
100
+ const startMonth = (closingMonth % 12) + 1; // month after the close
101
+ // FY containing date: started this calendar year if date.month is
102
+ // at-or-after startMonth, else previous calendar year. Q4 books
103
+ // (startMonth = 1) always land in the same calendar year — covered
104
+ // by the same predicate.
105
+ const fyStartYear = date.month >= startMonth ? date.year : date.year - 1;
106
+ const fyEndYear = closingMonth === 12 ? fyStartYear : fyStartYear + 1;
107
+ return { fyStartYear, fyEndYear, startMonth };
108
+ }
109
+
110
+ // ── per-granularity bucket lookup ──────────────────────────────────
111
+
112
+ function monthBucketContaining(date: YmdParts): Bucket {
113
+ return {
114
+ from: fmtYmd(date.year, date.month, 1),
115
+ to: fmtYmd(date.year, date.month, lastDayOf(date.year, date.month)),
116
+ label: `${date.year}-${pad2(date.month)}`,
117
+ };
118
+ }
119
+
120
+ function quarterBucketContaining(date: YmdParts, end: FiscalYearEnd): Bucket {
121
+ const anchor = fyAnchorFor(date, end);
122
+ const offset = (date.month - anchor.startMonth + 12) % 12; // 0..11
123
+ const qIdx = Math.floor(offset / 3); // 0..3
124
+ // Flat month-index from the calendar epoch makes year rollover
125
+ // arithmetic trivial.
126
+ const startFlat = anchor.fyStartYear * 12 + (anchor.startMonth - 1) + qIdx * 3;
127
+ const endFlat = startFlat + 2;
128
+ const qStartYear = Math.floor(startFlat / 12);
129
+ const qStartMonth = (startFlat % 12) + 1;
130
+ const qEndYear = Math.floor(endFlat / 12);
131
+ const qEndMonth = (endFlat % 12) + 1;
132
+ return {
133
+ from: fmtYmd(qStartYear, qStartMonth, 1),
134
+ to: fmtYmd(qEndYear, qEndMonth, lastDayOf(qEndYear, qEndMonth)),
135
+ label: `FY${anchor.fyEndYear}-Q${qIdx + 1}`,
136
+ };
137
+ }
138
+
139
+ function yearBucketContaining(date: YmdParts, end: FiscalYearEnd): Bucket {
140
+ const anchor = fyAnchorFor(date, end);
141
+ const startFlat = anchor.fyStartYear * 12 + (anchor.startMonth - 1);
142
+ const endFlat = startFlat + 11;
143
+ const yStartYear = Math.floor(startFlat / 12);
144
+ const yStartMonth = (startFlat % 12) + 1;
145
+ const yEndYear = Math.floor(endFlat / 12);
146
+ const yEndMonth = (endFlat % 12) + 1;
147
+ return {
148
+ from: fmtYmd(yStartYear, yStartMonth, 1),
149
+ to: fmtYmd(yEndYear, yEndMonth, lastDayOf(yEndYear, yEndMonth)),
150
+ label: `FY${anchor.fyEndYear}`,
151
+ };
152
+ }
153
+
154
+ function bucketContaining(date: YmdParts, granularity: TimeSeriesGranularity, end: FiscalYearEnd): Bucket {
155
+ if (granularity === "month") return monthBucketContaining(date);
156
+ if (granularity === "quarter") return quarterBucketContaining(date, end);
157
+ return yearBucketContaining(date, end);
158
+ }
159
+
160
+ /** Walk inclusive `[from, to]` and return every bucket that overlaps
161
+ * it, ordered ascending by `from`. The first bucket is the one
162
+ * CONTAINING `from` — it can extend earlier than `from`; the last
163
+ * bucket is the one CONTAINING `to` — it can extend past `to`. The
164
+ * caller's response echoes the input range so the LLM can label the
165
+ * chart truthfully ("Revenue Apr 2025 – Sep 2026" even though the
166
+ * outermost buckets cover Apr-Jun 2025 and Jul-Sep 2026). */
167
+ export function bucketize(input: { from: string; to: string; granularity: TimeSeriesGranularity; fiscalYearEnd: FiscalYearEnd }): Bucket[] {
168
+ if (input.from > input.to) return [];
169
+ const start = parseYmd(input.from);
170
+ const result: Bucket[] = [];
171
+ let bucket = bucketContaining(start, input.granularity, input.fiscalYearEnd);
172
+ // Buckets are contiguous; once a bucket's `from` is past `input.to`
173
+ // every subsequent bucket is too.
174
+ while (bucket.from <= input.to) {
175
+ result.push(bucket);
176
+ const next = addDay(parseYmd(bucket.to));
177
+ bucket = bucketContaining(next, input.granularity, input.fiscalYearEnd);
178
+ }
179
+ return result;
180
+ }
181
+
182
+ // ── value computation ──────────────────────────────────────────────
183
+
184
+ /** Convert raw netDebit to natural-sign presentation per account
185
+ * type. Mirrors the helper in `report.ts` (kept private there). */
186
+ function naturalSign(type: AccountType, netDebit: number): number {
187
+ if (type === "asset" || type === "expense") return netDebit;
188
+ return -netDebit;
189
+ }
190
+
191
+ interface PresentationTotals {
192
+ income: number;
193
+ expense: number;
194
+ }
195
+
196
+ /** Sum presented income and expense values across the supplied
197
+ * entries. Used for window-based metrics (revenue / expense /
198
+ * netIncome). Opening entries reference B/S accounts only and so
199
+ * contribute zero to either total — including them is harmless. */
200
+ function presentedPlTotals(entries: readonly JournalEntry[], accountTypeByCode: ReadonlyMap<string, AccountType>): PresentationTotals {
201
+ const balances = aggregateBalances(entries);
202
+ let income = 0;
203
+ let expense = 0;
204
+ for (const row of balances) {
205
+ const type = accountTypeByCode.get(row.accountCode);
206
+ if (!type) continue;
207
+ if (type === "income") income += naturalSign(type, row.netDebit);
208
+ else if (type === "expense") expense += naturalSign(type, row.netDebit);
209
+ }
210
+ return { income, expense };
211
+ }
212
+
213
+ function entriesInWindow(entries: readonly JournalEntry[], from: string, toDate: string): JournalEntry[] {
214
+ return entries.filter((entry) => entry.date >= from && entry.date <= toDate);
215
+ }
216
+
217
+ function entriesUpTo(entries: readonly JournalEntry[], toDate: string): JournalEntry[] {
218
+ return entries.filter((entry) => entry.date <= toDate);
219
+ }
220
+
221
+ function valueForBucket(input: {
222
+ bucket: Bucket;
223
+ entries: readonly JournalEntry[];
224
+ accounts: readonly Account[];
225
+ metric: TimeSeriesMetric;
226
+ accountCode?: string;
227
+ }): number {
228
+ const accountTypeByCode = new Map(input.accounts.map((acct) => [acct.code, acct.type]));
229
+ if (input.metric === "accountBalance") {
230
+ const code = input.accountCode;
231
+ if (!code) return 0; // guarded at the route — defensive zero.
232
+ const type = accountTypeByCode.get(code);
233
+ if (!type) return 0;
234
+ const cumulative = entriesUpTo(input.entries, input.bucket.to);
235
+ const balances = aggregateBalances(cumulative);
236
+ const row = balances.find((balance) => balance.accountCode === code);
237
+ return row ? naturalSign(type, row.netDebit) : 0;
238
+ }
239
+ const window = entriesInWindow(input.entries, input.bucket.from, input.bucket.to);
240
+ const totals = presentedPlTotals(window, accountTypeByCode);
241
+ if (input.metric === "revenue") return totals.income;
242
+ if (input.metric === "expense") return totals.expense;
243
+ return totals.income - totals.expense; // netIncome
244
+ }
245
+
246
+ export function buildTimeSeries(input: {
247
+ buckets: readonly Bucket[];
248
+ entries: readonly JournalEntry[];
249
+ accounts: readonly Account[];
250
+ metric: TimeSeriesMetric;
251
+ accountCode?: string;
252
+ }): TimeSeriesPoint[] {
253
+ return input.buckets.map((bucket) => ({
254
+ label: bucket.label,
255
+ from: bucket.from,
256
+ to: bucket.to,
257
+ value: valueForBucket({
258
+ bucket,
259
+ entries: input.entries,
260
+ accounts: input.accounts,
261
+ metric: input.metric,
262
+ accountCode: input.accountCode,
263
+ }),
264
+ }));
265
+ }