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.
- package/Dockerfile.sandbox +100 -0
- package/README.md +17 -4
- package/bin/mulmoclaude.js +46 -15
- package/bin/prepare-dist.js +18 -2
- package/client/assets/chunk-CernVdwh.js +1 -0
- package/client/assets/chunk-D8eiyYIV-C1eAZMzz.js +1 -0
- package/client/assets/html2canvas-CDGcmOD3-BbPeutDg.js +5 -0
- package/client/assets/index-BbgSjFQ8.js +4968 -0
- package/client/assets/index-ECD0lgIv.css +2 -0
- package/client/assets/{index.es-D4YyL_Dg-BgT6a3Nd.js → index.es-DqtpmBm8-DJdTPdnc.js} +5 -5
- package/client/assets/material-symbols-outlined-BLDfUw-_.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-6WYa8hAs.js +1 -0
- package/client/assets/runtime-vue-BVUzgYGA.js +1 -0
- package/client/assets/typeof-DBp4T-Ny-C2xoZtcz.js +1 -0
- package/client/assets/vue-1e_vz2LW.js +1 -0
- package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +4 -0
- package/client/index.html +33 -2
- package/package.json +20 -18
- package/sandbox-entrypoint.sh +106 -0
- package/server/accounting/accountNormalize.ts +32 -0
- package/server/accounting/defaultAccounts.ts +87 -0
- package/server/accounting/eventPublisher.ts +51 -0
- package/server/accounting/journal.ts +252 -0
- package/server/accounting/openingBalances.ts +114 -0
- package/server/accounting/report.ts +237 -0
- package/server/accounting/service.ts +718 -0
- package/server/accounting/snapshotCache.ts +333 -0
- package/server/accounting/timeSeries.ts +265 -0
- package/server/accounting/types.ts +148 -0
- package/server/agent/activeTools.ts +128 -0
- package/server/agent/attachmentConverter.ts +10 -5
- package/server/agent/backend/claude-code.ts +8 -2
- package/server/agent/backend/types.ts +1 -1
- package/server/agent/config.ts +101 -31
- package/server/agent/index.ts +45 -33
- package/server/agent/mcp-server.ts +146 -69
- package/server/agent/mcp-tools/index.ts +1 -5
- package/server/agent/mcp-tools/notify.ts +2 -22
- package/server/agent/mcp-tools/x.ts +0 -4
- package/server/agent/mcpHealth.ts +168 -0
- package/server/agent/plugin-names.ts +20 -77
- package/server/agent/prompt.ts +259 -51
- package/server/agent/resumeFailover.ts +1 -1
- package/server/agent/stream.ts +0 -1
- package/server/api/auth/bearerAuth.ts +5 -5
- package/server/api/csrfGuard.ts +1 -1
- package/server/api/routes/accounting.ts +366 -0
- package/server/api/routes/agent.ts +509 -46
- package/server/api/routes/attachment.ts +104 -0
- package/server/api/routes/chart.ts +2 -1
- package/server/api/routes/config.ts +12 -12
- package/server/api/routes/files.ts +105 -48
- package/server/api/routes/image.ts +70 -25
- package/server/api/routes/journal.ts +35 -0
- package/server/api/routes/mulmo-script.ts +358 -118
- package/server/api/routes/mulmoScriptValidate.ts +1 -1
- package/server/api/routes/news.ts +1 -1
- package/server/api/routes/notifications.ts +92 -22
- package/server/api/routes/notifier.ts +98 -0
- package/server/api/routes/pdf.ts +188 -48
- package/server/api/routes/plugins.ts +34 -14
- package/server/api/routes/presentHtml.ts +58 -3
- package/server/api/routes/roles.ts +1 -8
- package/server/api/routes/runtime-plugin.ts +224 -0
- package/server/api/routes/scheduler.ts +7 -5
- package/server/api/routes/schedulerHandlers.ts +1 -1
- package/server/api/routes/schedulerTasks.ts +8 -7
- package/server/api/routes/sessions.ts +234 -121
- package/server/api/routes/skills.ts +56 -51
- package/server/api/routes/sources.ts +52 -45
- package/server/api/routes/translation.ts +44 -0
- package/server/api/routes/wiki/frontmatter.ts +13 -65
- package/server/api/routes/wiki/history.ts +261 -0
- package/server/api/routes/wiki/pageIndex.ts +1 -1
- package/server/api/routes/wiki.ts +50 -26
- package/server/events/file-change.ts +83 -0
- package/server/events/notifications.ts +247 -91
- package/server/events/pub-sub/index.ts +1 -1
- package/server/events/relay-client.ts +5 -5
- package/server/events/scheduler-adapter.ts +2 -2
- package/server/events/session-store/index.ts +110 -22
- package/server/events/task-manager/index.ts +10 -9
- package/server/index.ts +509 -33
- package/server/notifier/engine.ts +419 -0
- package/server/notifier/legacy-adapters.ts +76 -0
- package/server/notifier/runtime-api.ts +74 -0
- package/server/notifier/store.ts +70 -0
- package/server/notifier/types.ts +121 -0
- package/server/plugins/dev-loader.ts +171 -0
- package/server/plugins/dev-watcher.ts +150 -0
- package/server/plugins/diagnostics.ts +188 -0
- package/server/plugins/preset-list.ts +52 -0
- package/server/plugins/preset-loader.ts +112 -0
- package/server/plugins/runtime-chat-api.ts +38 -0
- package/server/plugins/runtime-loader.ts +430 -0
- package/server/plugins/runtime-registry.ts +112 -0
- package/server/plugins/runtime-tasks-api.ts +50 -0
- package/server/plugins/runtime.ts +378 -0
- package/server/services/translation/cache.ts +72 -0
- package/server/services/translation/index.ts +106 -0
- package/server/services/translation/llm.ts +140 -0
- package/server/services/translation/types.ts +35 -0
- package/server/system/credentials.ts +13 -2
- package/server/system/env.ts +6 -1
- package/server/system/logger/formatters.ts +46 -4
- package/server/system/logger/index.ts +4 -4
- package/server/system/logger/sinks.ts +26 -5
- package/server/system/logger/types.ts +2 -2
- package/server/utils/dev-plugin-args.d.mts +11 -0
- package/server/utils/dev-plugin-args.mjs +43 -0
- package/server/utils/errors.ts +13 -4
- package/server/utils/files/accounting-io.ts +295 -0
- package/server/utils/files/atomic.ts +17 -49
- package/server/utils/files/attachment-store.ts +182 -0
- package/server/utils/files/html-io.ts +1 -7
- package/server/utils/files/html-store.ts +19 -0
- package/server/utils/files/image-store.ts +20 -22
- package/server/utils/files/index.ts +5 -15
- package/server/utils/files/journal-io.ts +7 -35
- package/server/utils/files/json.ts +2 -29
- package/server/utils/files/markdown-image-fill.ts +6 -37
- package/server/utils/files/markdown-store.ts +6 -21
- package/server/utils/files/naming.ts +3 -39
- package/server/utils/files/plugins-io.ts +100 -0
- package/server/utils/files/reference-dirs-io.ts +1 -9
- package/server/utils/files/roles-io.ts +2 -10
- package/server/utils/files/safe.ts +17 -19
- package/server/utils/files/scheduler-io.ts +1 -7
- package/server/utils/files/scheduler-overrides-io.ts +3 -12
- package/server/utils/files/session-io.ts +21 -30
- package/server/utils/files/spreadsheet-store.ts +9 -22
- package/server/utils/files/translation-io.ts +46 -0
- package/server/utils/files/user-tasks-io.ts +1 -7
- package/server/utils/files/workspace-io.ts +3 -79
- package/server/utils/gemini.ts +33 -11
- package/server/utils/html/htmlArtifactSplicer.ts +41 -0
- package/server/utils/markdown/frontmatter.ts +112 -0
- package/server/utils/regex.ts +56 -0
- package/server/utils/router.ts +41 -0
- package/server/utils/slug.ts +5 -3
- package/server/utils/time.ts +12 -0
- package/server/workspace/chat-index/indexer.ts +15 -2
- package/server/workspace/chat-index/summarizer.ts +1 -1
- package/server/workspace/custom-dirs.ts +1 -1
- package/server/workspace/helps/gemini.md +1 -1
- package/server/workspace/helps/guide.md +61 -0
- package/server/workspace/helps/index.md +4 -0
- package/server/workspace/helps/presenthtml.md +80 -0
- package/server/workspace/helps/sandbox.md +7 -0
- package/server/workspace/helps/storyteller.md +101 -0
- package/server/workspace/helps/telegram.md +1 -0
- package/server/workspace/helps/wiki.md +9 -7
- package/server/workspace/journal/archivist-cli.ts +7 -33
- package/server/workspace/journal/archivist-schemas.ts +5 -43
- package/server/workspace/journal/dailyPass.ts +34 -187
- package/server/workspace/journal/diff.ts +3 -28
- package/server/workspace/journal/index.ts +10 -81
- package/server/workspace/journal/indexFile.ts +3 -24
- package/server/workspace/journal/latestDaily.ts +51 -0
- package/server/workspace/journal/memoryExtractor.ts +4 -20
- package/server/workspace/journal/optimizationPass.ts +4 -21
- package/server/workspace/journal/paths.ts +4 -23
- package/server/workspace/journal/state.ts +6 -29
- package/server/workspace/memory/io.ts +213 -0
- package/server/workspace/memory/llm-classifier.ts +158 -0
- package/server/workspace/memory/migrate.ts +263 -0
- package/server/workspace/memory/run.ts +84 -0
- package/server/workspace/memory/topic-cluster.ts +218 -0
- package/server/workspace/memory/topic-detect.ts +67 -0
- package/server/workspace/memory/topic-index-hook.ts +128 -0
- package/server/workspace/memory/topic-io.ts +180 -0
- package/server/workspace/memory/topic-migrate.ts +248 -0
- package/server/workspace/memory/topic-run.ts +172 -0
- package/server/workspace/memory/topic-swap.ts +135 -0
- package/server/workspace/memory/topic-types.ts +142 -0
- package/server/workspace/memory/types.ts +83 -0
- package/server/workspace/news/reader.ts +4 -5
- package/server/workspace/paths.ts +124 -47
- package/server/workspace/roles.ts +2 -11
- package/server/workspace/skills/parser.ts +38 -55
- package/server/workspace/skills/user-tasks.ts +1 -2
- package/server/workspace/skills-preset/mc-library/SKILL.md +188 -0
- package/server/workspace/skills-preset.ts +196 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +13 -11
- package/server/workspace/sources/fetchers/index.ts +1 -1
- package/server/workspace/sources/fetchers/rssParser.ts +1 -1
- package/server/workspace/sources/pipeline/index.ts +2 -2
- package/server/workspace/sources/pipeline/notify.ts +3 -3
- package/server/workspace/sources/pipeline/write.ts +2 -2
- package/server/workspace/sources/registry.ts +39 -61
- package/server/workspace/sources/robots.ts +1 -1
- package/server/workspace/tool-trace/classify.ts +2 -1
- package/server/workspace/tool-trace/index.ts +1 -1
- package/server/workspace/tool-trace/writeSearch.ts +6 -1
- package/server/workspace/wiki-backlinks/index.ts +19 -7
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +1 -0
- package/server/workspace/wiki-history/hook/snapshot.mjs +98 -0
- package/server/workspace/wiki-history/hook/snapshot.ts +135 -0
- package/server/workspace/wiki-history/provision.ts +181 -0
- package/server/workspace/wiki-pages/io.ts +217 -0
- package/server/workspace/wiki-pages/snapshot.ts +380 -0
- package/server/workspace/workspace.ts +75 -13
- package/src/App.vue +115 -40
- package/src/_runtime/protocol-vue.ts +21 -0
- package/src/_runtime/vue.ts +22 -0
- package/src/components/ChatInput.vue +14 -10
- package/src/components/CopyChatButton.vue +76 -0
- package/src/components/FileContentRenderer.vue +67 -14
- package/src/components/FileTree.vue +2 -2
- package/src/components/FilesView.vue +17 -1
- package/src/components/NewsView.vue +16 -2
- package/src/components/NotificationBell.vue +320 -93
- package/src/components/PageChatComposer.vue +5 -4
- package/src/components/PluginLauncher.vue +42 -6
- package/src/components/PluginScopedRoot.vue +87 -0
- package/src/components/RoleSelector.vue +12 -1
- package/src/components/RolesView.vue +562 -0
- package/src/components/SentAttachmentChip.vue +102 -0
- package/src/components/SessionHistoryPanel.vue +109 -20
- package/src/components/SessionRoleIcon.vue +7 -4
- package/src/components/SessionSidebar.vue +20 -7
- package/src/components/SessionTabBar.vue +1 -1
- package/src/components/SettingsMcpTab.vue +4 -4
- package/src/components/SettingsModal.vue +2 -0
- package/src/components/SidebarHeader.vue +16 -5
- package/src/components/SourcesManager.vue +23 -9
- package/src/components/SourcesView.vue +1 -1
- package/src/components/StackView.vue +102 -6
- package/src/components/SuggestionsPanel.vue +105 -16
- package/src/components/SystemFileBanner.vue +1 -1
- package/src/components/TodoExplorer.vue +4 -5
- package/src/components/todo/TodoAddDialog.vue +2 -3
- package/src/components/todo/TodoEditDialog.vue +1 -2
- package/src/components/todo/TodoEditPanel.vue +2 -3
- package/src/components/todo/TodoKanbanView.vue +8 -5
- package/src/components/todo/TodoListView.vue +3 -5
- package/src/components/todo/TodoTableView.vue +7 -5
- package/src/composables/useAccountingChannel.ts +58 -0
- package/src/composables/useActiveSession.ts +4 -25
- package/src/composables/useAppApi.ts +6 -44
- package/src/composables/useClipboardCopy.ts +3 -20
- package/src/composables/useContentDisplay.ts +33 -2
- package/src/composables/useDevPluginReload.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +5 -31
- package/src/composables/useEventListeners.ts +0 -20
- package/src/composables/useExpandedDirs.ts +4 -15
- package/src/composables/useFaviconState.ts +12 -46
- package/src/composables/useFileChange.ts +53 -0
- package/src/composables/useFreshPluginData.ts +6 -43
- package/src/composables/useHealth.ts +14 -43
- package/src/composables/useImageErrorRepair.ts +104 -0
- package/src/composables/useLatestDaily.ts +40 -0
- package/src/composables/useMarkdownDoc.ts +39 -0
- package/src/composables/useMarkdownLinkHandler.ts +1 -1
- package/src/composables/useMcpTools.ts +3 -16
- package/src/composables/useNotifications.ts +138 -112
- package/src/composables/usePdfDownload.ts +17 -3
- package/src/composables/usePendingCalls.ts +8 -26
- package/src/composables/usePluginErrorBoundary.ts +68 -0
- package/src/composables/usePubSub.ts +9 -17
- package/src/composables/useRunElapsed.ts +5 -22
- package/src/composables/useSandboxStatus.ts +4 -20
- package/src/composables/useSessionDerived.ts +7 -15
- package/src/composables/useSessionHistory.ts +70 -29
- package/src/composables/useSessionSync.ts +25 -3
- package/src/composables/useSkillsList.ts +59 -0
- package/src/composables/useTranslatedQueries.ts +109 -0
- package/src/config/apiRoutes.ts +181 -80
- package/src/config/historyFilters.ts +5 -3
- package/src/config/hostEvents.ts +17 -0
- package/src/config/mcpCatalog.ts +277 -5
- package/src/config/pubsubChannels.ts +134 -12
- package/src/config/roles.ts +212 -147
- package/src/config/systemFileDescriptors.ts +5 -5
- package/src/config/toolNames.ts +52 -30
- package/src/config/workspacePaths.ts +26 -2
- package/src/lang/de.ts +483 -27
- package/src/lang/en.ts +448 -27
- package/src/lang/es.ts +474 -27
- package/src/lang/fr.ts +476 -27
- package/src/lang/ja.ts +465 -27
- package/src/lang/ko.ts +466 -27
- package/src/lang/pt-BR.ts +473 -27
- package/src/lang/zh.ts +463 -27
- package/src/lib/vue-i18n.ts +1 -1
- package/src/lib/wiki-page/slug.ts +66 -0
- package/src/main.ts +85 -0
- package/src/plugins/_extras.ts +58 -0
- package/src/plugins/_generated/metas.ts +42 -0
- package/src/plugins/_generated/registrations.ts +44 -0
- package/src/plugins/_generated/server-bindings.ts +47 -0
- package/src/plugins/accounting/Preview.vue +106 -0
- package/src/plugins/accounting/View.vue +632 -0
- package/src/plugins/accounting/actions.ts +34 -0
- package/src/plugins/accounting/api.ts +301 -0
- package/src/plugins/accounting/components/AccountEditor.vue +250 -0
- package/src/plugins/accounting/components/AccountRow.vue +50 -0
- package/src/plugins/accounting/components/AccountsList.vue +102 -0
- package/src/plugins/accounting/components/AccountsModal.vue +300 -0
- package/src/plugins/accounting/components/BalanceSheet.vue +186 -0
- package/src/plugins/accounting/components/BookSettings.vue +284 -0
- package/src/plugins/accounting/components/BookSwitcher.vue +78 -0
- package/src/plugins/accounting/components/DateRangePicker.vue +140 -0
- package/src/plugins/accounting/components/JournalEntryForm.vue +504 -0
- package/src/plugins/accounting/components/JournalList.vue +553 -0
- package/src/plugins/accounting/components/Ledger.vue +206 -0
- package/src/plugins/accounting/components/NewBookForm.vue +211 -0
- package/src/plugins/accounting/components/OpeningBalancesForm.vue +271 -0
- package/src/plugins/accounting/components/ProfitLoss.vue +160 -0
- package/src/plugins/accounting/components/accountDraft.ts +13 -0
- package/src/plugins/accounting/components/accountNumbering.ts +103 -0
- package/src/plugins/accounting/components/accountValidation.ts +75 -0
- package/src/plugins/accounting/components/useLatestRequest.ts +44 -0
- package/src/plugins/accounting/countries.ts +158 -0
- package/src/plugins/accounting/currencies.ts +64 -0
- package/src/plugins/accounting/dates.ts +51 -0
- package/src/plugins/accounting/definition.ts +199 -0
- package/src/plugins/accounting/fiscalYear.ts +136 -0
- package/src/plugins/accounting/index.ts +49 -0
- package/src/plugins/accounting/meta.ts +91 -0
- package/src/plugins/accounting/timeSeriesEnums.ts +16 -0
- package/src/plugins/api.ts +125 -0
- package/src/plugins/canvas/View.vue +38 -28
- package/src/plugins/canvas/definition.ts +10 -8
- package/src/plugins/canvas/index.ts +15 -8
- package/src/plugins/canvas/meta.ts +12 -0
- package/src/plugins/chart/Preview.vue +1 -1
- package/src/plugins/chart/View.vue +2 -2
- package/src/plugins/chart/definition.ts +12 -2
- package/src/plugins/chart/index.ts +15 -7
- package/src/plugins/chart/meta.ts +18 -0
- package/src/plugins/editImages/definition.ts +44 -0
- package/src/plugins/editImages/index.ts +43 -0
- package/src/plugins/editImages/meta.ts +5 -0
- package/src/plugins/generateImage/View.vue +3 -1
- package/src/plugins/generateImage/definition.ts +2 -0
- package/src/plugins/generateImage/index.ts +13 -5
- package/src/plugins/generateImage/meta.ts +5 -0
- package/src/plugins/index.ts +35 -0
- package/src/plugins/manageRoles/Preview.vue +7 -4
- package/src/plugins/manageRoles/View.vue +12 -8
- package/src/plugins/manageRoles/definition.ts +6 -0
- package/src/plugins/manageRoles/index.ts +7 -6
- package/src/plugins/manageSkills/View.vue +11 -7
- package/src/plugins/manageSkills/definition.ts +4 -1
- package/src/plugins/manageSkills/index.ts +14 -7
- package/src/plugins/manageSkills/meta.ts +21 -0
- package/src/plugins/manageSource/definition.ts +4 -1
- package/src/plugins/manageSource/index.ts +15 -7
- package/src/plugins/manageSource/meta.ts +21 -0
- package/src/plugins/markdown/Preview.vue +10 -8
- package/src/plugins/markdown/View.vue +84 -17
- package/src/plugins/markdown/definition.ts +7 -1
- package/src/plugins/markdown/index.ts +15 -8
- package/src/plugins/markdown/meta.ts +16 -0
- package/src/plugins/meta-types.ts +97 -0
- package/src/plugins/metas.ts +224 -0
- package/src/plugins/presentForm/Preview.vue +4 -15
- package/src/plugins/presentForm/View.vue +35 -78
- package/src/plugins/presentForm/definition.ts +7 -6
- package/src/plugins/presentForm/index.ts +12 -5
- package/src/plugins/presentForm/meta.ts +11 -0
- package/src/plugins/presentForm/plugin.ts +8 -9
- package/src/plugins/presentForm/types.ts +0 -24
- package/src/plugins/presentHtml/Preview.vue +1 -8
- package/src/plugins/presentHtml/View.vue +401 -30
- package/src/plugins/presentHtml/definition.ts +8 -5
- package/src/plugins/presentHtml/index.ts +15 -8
- package/src/plugins/presentHtml/meta.ts +14 -0
- package/src/plugins/presentMulmoScript/View.vue +327 -107
- package/src/plugins/presentMulmoScript/definition.ts +34 -7
- package/src/plugins/presentMulmoScript/helpers.ts +4 -5
- package/src/plugins/presentMulmoScript/index.ts +20 -7
- package/src/plugins/presentMulmoScript/meta.ts +52 -0
- package/src/plugins/scheduler/AutomationsPreview.vue +2 -8
- package/src/plugins/scheduler/Preview.vue +5 -2
- package/src/plugins/scheduler/TasksTab.vue +16 -36
- package/src/plugins/scheduler/View.vue +22 -54
- package/src/plugins/scheduler/automationsDefinition.ts +14 -9
- package/src/plugins/scheduler/automationsMeta.ts +5 -0
- package/src/plugins/scheduler/calendarDefinition.ts +4 -7
- package/src/plugins/scheduler/calendarMeta.ts +28 -0
- package/src/plugins/scheduler/formatSchedule.ts +6 -24
- package/src/plugins/scheduler/index.ts +26 -52
- package/src/plugins/scope.ts +57 -0
- package/src/plugins/server-bindings-types.ts +38 -0
- package/src/plugins/server.ts +32 -0
- package/src/plugins/skill/Preview.vue +25 -0
- package/src/plugins/skill/View.vue +125 -0
- package/src/plugins/skill/definition.ts +23 -0
- package/src/plugins/skill/index.ts +36 -0
- package/src/plugins/skill/plugin.ts +31 -0
- package/src/plugins/skill/types.ts +21 -0
- package/src/plugins/spreadsheet/Preview.vue +1 -3
- package/src/plugins/spreadsheet/View.vue +29 -49
- package/src/plugins/spreadsheet/cellHighlights.ts +2 -3
- package/src/plugins/spreadsheet/definition.ts +5 -2
- package/src/plugins/spreadsheet/index.ts +15 -8
- package/src/plugins/spreadsheet/keyboardNav.ts +38 -0
- package/src/plugins/spreadsheet/meta.ts +14 -0
- package/src/plugins/textResponse/Preview.vue +9 -1
- package/src/plugins/textResponse/View.vue +59 -8
- package/src/plugins/textResponse/index.ts +11 -3
- package/src/plugins/textResponse/plugin.ts +8 -10
- package/src/plugins/textResponse/types.ts +28 -0
- package/src/plugins/wiki/Preview.vue +6 -4
- package/src/plugins/wiki/View.vue +463 -254
- package/src/plugins/wiki/components/WikiPageBody.vue +159 -0
- package/src/plugins/wiki/helpers.ts +17 -0
- package/src/plugins/wiki/history/HistoryDetail.vue +325 -0
- package/src/plugins/wiki/history/HistoryTab.vue +167 -0
- package/src/plugins/wiki/history/RestoreConfirm.vue +63 -0
- package/src/plugins/wiki/history/api.ts +52 -0
- package/src/plugins/wiki/history/diff.ts +145 -0
- package/src/plugins/wiki/index.ts +42 -32
- package/src/plugins/wiki/meta.ts +10 -0
- package/src/plugins/wiki/pageEditLoader.ts +53 -0
- package/src/plugins/wiki/route.ts +8 -0
- package/src/router/guards.ts +2 -1
- package/src/router/index.ts +19 -0
- package/src/router/pageRoutes.ts +1 -0
- package/src/tools/index.ts +50 -51
- package/src/tools/runtimeLoader.ts +141 -0
- package/src/tools/types.ts +44 -1
- package/src/types/notification.ts +23 -0
- package/src/types/pastedFile.ts +10 -0
- package/src/types/session.ts +61 -3
- package/src/types/sse.ts +21 -6
- package/src/utils/agent/eventDispatch.ts +12 -9
- package/src/utils/agent/pastedAttachment.ts +35 -0
- package/src/utils/agent/request.ts +32 -3
- package/src/utils/agent/toolCalls.ts +7 -1
- package/src/utils/api.ts +1 -1
- package/src/utils/chat/exportMarkdown.ts +243 -0
- package/src/utils/errors.ts +10 -2
- package/src/utils/files/expandedDirs.ts +1 -1
- package/src/utils/filesPreview/todoPreview.ts +13 -2
- package/src/utils/format/date.ts +1 -3
- package/src/utils/format/jsonSyntax.ts +5 -0
- package/src/utils/html/iframeHeightReporterScript.ts +62 -0
- package/src/utils/html/previewCsp.ts +29 -2
- package/src/utils/image/htmlSrcAttrs.ts +122 -0
- package/src/utils/image/imageRepairInlineScript.ts +115 -0
- package/src/utils/image/resolve.ts +17 -3
- package/src/utils/image/rewriteMarkdownImageRefs.ts +62 -9
- package/src/utils/markdown/frontmatter.ts +125 -0
- package/src/utils/markdown/taskList.ts +7 -2
- package/src/utils/plugin/runtime.ts +132 -0
- package/src/utils/session/mergeSessions.ts +40 -37
- package/src/utils/session/sessionEntries.ts +74 -18
- package/src/utils/session/sessionHelpers.ts +54 -10
- package/src/utils/tools/result.ts +76 -14
- package/src/vite-env.d.ts +6 -0
- package/client/assets/html2canvas-Cx501zZr-Bug0qRNv.js +0 -5
- package/client/assets/index-CY-WpQUm.css +0 -2
- package/client/assets/index-DbTz2Mfs.js +0 -4911
- package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
- package/server/api/routes/html.ts +0 -114
- package/server/api/routes/todos.ts +0 -293
- package/server/api/routes/todosColumnsHandlers.ts +0 -333
- package/server/api/routes/todosHandlers.ts +0 -274
- package/server/api/routes/todosItemsHandlers.ts +0 -386
- package/server/utils/files/todos-io.ts +0 -29
- package/src/components/NotificationToast.vue +0 -75
- package/src/plugins/editImage/definition.ts +0 -27
- package/src/plugins/editImage/index.ts +0 -37
- package/src/plugins/presentHtml/helpers.ts +0 -72
- package/src/plugins/scheduler/LegacySchedulerView.vue +0 -32
- package/src/plugins/scheduler/legacyShape.ts +0 -34
- package/src/plugins/todo/Preview.vue +0 -68
- package/src/plugins/todo/View.vue +0 -378
- package/src/plugins/todo/composables/useTodos.ts +0 -179
- package/src/plugins/todo/definition.ts +0 -45
- package/src/plugins/todo/index.ts +0 -62
- package/src/plugins/todo/labels.ts +0 -163
- package/src/plugins/todo/priority.ts +0 -98
- package/src/plugins/todo/viewModes.ts +0 -19
- package/src/plugins/wiki/definition.ts +0 -25
- package/src/tools/legacyPluginNames.ts +0 -13
- package/src/utils/format/frontmatter.ts +0 -80
- package/src/utils/image/rewriteHtmlImageRefs.ts +0 -50
- package/src/utils/notification/dispatch.ts +0 -58
- /package/client/assets/{purify.es-Fx1Nqyry-BwJECkqS.js → purify.es-Fx1Nqyry-BSVNht6S.js} +0 -0
- /package/src/plugins/{editImage → editImages}/Preview.vue +0 -0
- /package/src/plugins/{editImage → editImages}/View.vue +0 -0
- /package/src/{config/schedulerActions.ts → plugins/scheduler/actions.ts} +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Default chart of accounts seeded into a freshly created book.
|
|
2
|
+
// The active set is intentionally minimal — covers the common
|
|
3
|
+
// categories users need to record their first opening balance and
|
|
4
|
+
// post their first entries, without overwhelming a brand-new user.
|
|
5
|
+
//
|
|
6
|
+
// A second tier of `active: false` entries is included so the user
|
|
7
|
+
// can flip on common-but-not-universal accounts (Inventory, Travel,
|
|
8
|
+
// Depreciation Expense, …) from Manage Accounts with one click
|
|
9
|
+
// rather than typing them in by hand. Inactive accounts stay
|
|
10
|
+
// hidden from journal entry / ledger dropdowns until the user
|
|
11
|
+
// reactivates them. Tax-related accounts (1400 / 2400) are an
|
|
12
|
+
// exception — they ship active by default since almost every
|
|
13
|
+
// jurisdiction levies a consumption / sales / VAT tax.
|
|
14
|
+
|
|
15
|
+
import type { Account } from "./types.js";
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_ACCOUNTS: readonly Account[] = [
|
|
18
|
+
// Assets
|
|
19
|
+
{ code: "1000", name: "Cash", type: "asset" },
|
|
20
|
+
{ code: "1001", name: "Petty Cash", type: "asset", active: false },
|
|
21
|
+
{ code: "1010", name: "Bank — Checking", type: "asset" },
|
|
22
|
+
{ code: "1020", name: "Bank — Savings", type: "asset" },
|
|
23
|
+
{ code: "1100", name: "Accounts Receivable", type: "asset" },
|
|
24
|
+
{ code: "1200", name: "Inventory", type: "asset", active: false },
|
|
25
|
+
{ code: "1300", name: "Prepaid Expenses", type: "asset", active: false },
|
|
26
|
+
// 14xx is the reserved "tax-related current assets" band — pairs
|
|
27
|
+
// with 24xx on the liability side for the tax-excluded (税抜)
|
|
28
|
+
// booking method: input tax paid on purchases sits here as an
|
|
29
|
+
// asset and is netted against output-tax collected at filing
|
|
30
|
+
// time. The Ledger view's T-number column and the
|
|
31
|
+
// JournalEntryForm's per-line taxRegistrationId input key off
|
|
32
|
+
// the 14xx prefix only (see `isTaxAccountCode`) — the
|
|
33
|
+
// counterparty registration ID is load-bearing for input-tax
|
|
34
|
+
// credit on purchases, not for the seller-side liability — so
|
|
35
|
+
// any custom 14xx account a user adds participates without an
|
|
36
|
+
// opt-in step. Active by default — most jurisdictions levy a
|
|
37
|
+
// consumption / sales / VAT tax; tax-free contexts can
|
|
38
|
+
// deactivate from Manage Accounts.
|
|
39
|
+
// 1400 was briefly named "Sales Tax Receivable" — that label
|
|
40
|
+
// conventionally means *output* tax billed to customers but not
|
|
41
|
+
// yet collected. Renamed to "Input Tax Receivable" so the
|
|
42
|
+
// purchase-side meaning matches the 14xx / 24xx booking pair and
|
|
43
|
+
// the non-US naming the rest of the world uses (EU "Input VAT" /
|
|
44
|
+
// UK VAT input / Japan 仮払消費税). CodeRabbit review on PR #1120.
|
|
45
|
+
{ code: "1400", name: "Input Tax Receivable", type: "asset" },
|
|
46
|
+
{ code: "1500", name: "Equipment", type: "asset" },
|
|
47
|
+
{ code: "1510", name: "Furniture & Fixtures", type: "asset", active: false },
|
|
48
|
+
{ code: "1520", name: "Vehicles", type: "asset", active: false },
|
|
49
|
+
{ code: "1590", name: "Accumulated Depreciation", type: "asset", active: false },
|
|
50
|
+
// Liabilities
|
|
51
|
+
{ code: "2000", name: "Accounts Payable", type: "liability" },
|
|
52
|
+
{ code: "2100", name: "Credit Card", type: "liability" },
|
|
53
|
+
{ code: "2200", name: "Loans Payable", type: "liability" },
|
|
54
|
+
{ code: "2300", name: "Accrued Expenses", type: "liability", active: false },
|
|
55
|
+
// 24xx is the reserved "tax-related current liabilities" band;
|
|
56
|
+
// pairs with 14xx on the asset side. See the 1400 comment above.
|
|
57
|
+
{ code: "2400", name: "Sales Tax Payable", type: "liability" },
|
|
58
|
+
{ code: "2500", name: "Payroll Liabilities", type: "liability", active: false },
|
|
59
|
+
// Equity
|
|
60
|
+
// Required for opening balances: setOpeningBalances dumps the
|
|
61
|
+
// plug into "Retained Earnings" by convention.
|
|
62
|
+
{ code: "3000", name: "Owner's Equity", type: "equity" },
|
|
63
|
+
{ code: "3100", name: "Retained Earnings", type: "equity" },
|
|
64
|
+
{ code: "3200", name: "Owner's Draws", type: "equity", active: false },
|
|
65
|
+
// Income
|
|
66
|
+
{ code: "4000", name: "Sales", type: "income" },
|
|
67
|
+
{ code: "4010", name: "Service Revenue", type: "income", active: false },
|
|
68
|
+
{ code: "4100", name: "Other Income", type: "income" },
|
|
69
|
+
{ code: "4200", name: "Interest Income", type: "income", active: false },
|
|
70
|
+
{ code: "4300", name: "Sales Returns & Discounts", type: "income", active: false },
|
|
71
|
+
// Expenses
|
|
72
|
+
{ code: "5000", name: "Cost of Goods Sold", type: "expense" },
|
|
73
|
+
{ code: "5100", name: "Rent", type: "expense" },
|
|
74
|
+
{ code: "5200", name: "Utilities", type: "expense" },
|
|
75
|
+
{ code: "5300", name: "Salaries", type: "expense" },
|
|
76
|
+
{ code: "5400", name: "Office Supplies", type: "expense" },
|
|
77
|
+
{ code: "5500", name: "Advertising & Marketing", type: "expense", active: false },
|
|
78
|
+
{ code: "5600", name: "Travel", type: "expense", active: false },
|
|
79
|
+
{ code: "5610", name: "Meals & Entertainment", type: "expense", active: false },
|
|
80
|
+
{ code: "5700", name: "Professional Fees", type: "expense", active: false },
|
|
81
|
+
{ code: "5710", name: "Insurance", type: "expense", active: false },
|
|
82
|
+
{ code: "5720", name: "Software & Subscriptions", type: "expense", active: false },
|
|
83
|
+
{ code: "5730", name: "Bank Fees", type: "expense", active: false },
|
|
84
|
+
{ code: "5800", name: "Depreciation Expense", type: "expense", active: false },
|
|
85
|
+
{ code: "5810", name: "Taxes", type: "expense", active: false },
|
|
86
|
+
{ code: "5900", name: "Miscellaneous Expense", type: "expense" },
|
|
87
|
+
];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Pub/sub publisher for the accounting plugin. Mirror of
|
|
2
|
+
// `server/events/file-change.ts`: module singleton + init function +
|
|
3
|
+
// fire-and-forget publish helpers. The init wiring lives in
|
|
4
|
+
// `server/index.ts` next to `initFileChangePublisher`.
|
|
5
|
+
//
|
|
6
|
+
// Channel names + payload shapes are imported from
|
|
7
|
+
// `src/config/pubsubChannels.ts` so the publisher cannot drift from
|
|
8
|
+
// the View-side subscribers.
|
|
9
|
+
|
|
10
|
+
import { accountingBookChannel, PUBSUB_CHANNELS, type AccountingBookChannelPayload } from "../../src/config/pubsubChannels.js";
|
|
11
|
+
import { log } from "../system/logger/index.js";
|
|
12
|
+
import type { IPubSub } from "../events/pub-sub/index.js";
|
|
13
|
+
|
|
14
|
+
let pubsub: IPubSub | null = null;
|
|
15
|
+
|
|
16
|
+
export function initAccountingEventPublisher(instance: IPubSub): void {
|
|
17
|
+
pubsub = instance;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function safePublish(channel: string, payload: unknown): void {
|
|
21
|
+
if (!pubsub) return;
|
|
22
|
+
try {
|
|
23
|
+
pubsub.publish(channel, payload);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
// Same fire-and-forget rationale as the file-change publisher:
|
|
26
|
+
// dropping one event is better than crashing the server.
|
|
27
|
+
log.warn("accounting", "publish failed; subscribers will miss this event", {
|
|
28
|
+
channel,
|
|
29
|
+
error: err instanceof Error ? err.message : String(err),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Per-book change notification. `period` should be the entry's
|
|
35
|
+
* YYYY-MM bucket (or the earliest invalidated month for snapshot
|
|
36
|
+
* events). */
|
|
37
|
+
export function publishBookChange(bookId: string, payload: AccountingBookChannelPayload): void {
|
|
38
|
+
safePublish(accountingBookChannel(bookId), payload);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Fired when the *list* of books changes (createBook, deleteBook).
|
|
42
|
+
* Payload is intentionally empty — subscribers refetch from
|
|
43
|
+
* /api/accounting. */
|
|
44
|
+
export function publishBooksChanged(): void {
|
|
45
|
+
safePublish(PUBSUB_CHANNELS.accountingBooks, {});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Test-only — drop the module singleton so each test starts clean. */
|
|
49
|
+
export function _resetAccountingEventPublisherForTesting(): void {
|
|
50
|
+
pubsub = null;
|
|
51
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Pure validation + creation logic for journal entries. No fs access
|
|
2
|
+
// here — callers wire the validated entry to `appendJournal` in
|
|
3
|
+
// `server/utils/files/accounting-io.ts`.
|
|
4
|
+
//
|
|
5
|
+
// Double-entry rule: every entry's lines must satisfy
|
|
6
|
+
// Σ debit === Σ credit (within a tolerance of 0.005 — see
|
|
7
|
+
// EQUALITY_TOLERANCE below)
|
|
8
|
+
//
|
|
9
|
+
// Append-only: there is no `editEntry`. Corrections are made by
|
|
10
|
+
// `voidEntry` (creates a reversing pair) followed by a fresh
|
|
11
|
+
// `addEntries` call for the corrected booking.
|
|
12
|
+
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
|
|
15
|
+
import type { Account, JournalEntry, JournalLine } from "./types.js";
|
|
16
|
+
|
|
17
|
+
/** Floating-point tolerance for the debit = credit check. Currency
|
|
18
|
+
* amounts arrive as JavaScript numbers (the on-wire format is JSON,
|
|
19
|
+
* so amounts are doubles). 0.005 keeps two-decimal currency math
|
|
20
|
+
* honest while accepting the floating-point noise of summing
|
|
21
|
+
* many lines. */
|
|
22
|
+
const EQUALITY_TOLERANCE = 0.005;
|
|
23
|
+
|
|
24
|
+
/** Defensive cap on `JournalLine.taxRegistrationId`. Real-world IDs
|
|
25
|
+
* are short (JP T-numbers are 14 chars, EU VAT IDs ≤ 14, GSTIN is
|
|
26
|
+
* 15, ABN is 11). 32 covers every documented format with comfortable
|
|
27
|
+
* margin while still rejecting accidental paste-bombs. Validation
|
|
28
|
+
* applies to the *trimmed* value so a string of pure whitespace
|
|
29
|
+
* doesn't trip the limit (it normalises to absent). */
|
|
30
|
+
export const MAX_TAX_REGISTRATION_ID_LENGTH = 32;
|
|
31
|
+
|
|
32
|
+
export interface ValidationError {
|
|
33
|
+
field: string;
|
|
34
|
+
message: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ValidationResult {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
errors: ValidationError[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function lineHasExactlyOneSide(line: JournalLine): boolean {
|
|
43
|
+
const hasDebit = typeof line.debit === "number" && line.debit !== 0;
|
|
44
|
+
const hasCredit = typeof line.credit === "number" && line.credit !== 0;
|
|
45
|
+
return hasDebit !== hasCredit;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isNonNegativeNumber(value: unknown): value is number {
|
|
49
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Build today's `YYYY-MM-DD` from the host's local timezone.
|
|
53
|
+
* Centralised here so server-side defaults (the void-date on
|
|
54
|
+
* `voidEntry`, the today() stamp on opening replacements, etc.)
|
|
55
|
+
* agree with the client-side `localDateString()` from
|
|
56
|
+
* `src/plugins/accounting/dates.ts`. `toISOString().slice(0, 10)`
|
|
57
|
+
* would emit a UTC date instead — which silently flips into
|
|
58
|
+
* tomorrow / yesterday in negative-offset timezones. */
|
|
59
|
+
export function localDateString(now: Date = new Date()): string {
|
|
60
|
+
const year = now.getFullYear();
|
|
61
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
62
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
63
|
+
return `${year}-${month}-${day}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Validate that `date` is both shaped as YYYY-MM-DD AND represents
|
|
67
|
+
* a real calendar day. The bare regex accepts impossible values
|
|
68
|
+
* like 2026-02-31 or 2026-13-01 which would then poison
|
|
69
|
+
* `periodFromDate`, sort orders, and snapshot keys. We reparse
|
|
70
|
+
* through the Date constructor and roundtrip-format to catch
|
|
71
|
+
* silent normalisation (e.g. "2026-02-30" → Mar 02). */
|
|
72
|
+
export function isValidCalendarDate(date: string): boolean {
|
|
73
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return false;
|
|
74
|
+
const [year, month, day] = date.split("-").map((segment) => parseInt(segment, 10));
|
|
75
|
+
const parsed = new Date(Date.UTC(year, month - 1, day));
|
|
76
|
+
return parsed.getUTCFullYear() === year && parsed.getUTCMonth() === month - 1 && parsed.getUTCDate() === day;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Returns Σ debit − Σ credit. Used by callers that need the actual
|
|
80
|
+
* imbalance value (e.g. the OpeningBalancesForm shows live diff). */
|
|
81
|
+
export function netBalance(lines: readonly JournalLine[]): number {
|
|
82
|
+
let net = 0;
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
if (typeof line.debit === "number") net += line.debit;
|
|
85
|
+
if (typeof line.credit === "number") net -= line.credit;
|
|
86
|
+
}
|
|
87
|
+
return net;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Pure validation. Does not throw; returns a list of issues so the
|
|
91
|
+
* REST handler can return a structured 400 instead of an opaque
|
|
92
|
+
* 500. */
|
|
93
|
+
function validateLine(line: JournalLine, idx: number, accountCodes: ReadonlySet<string>, errors: ValidationError[]): void {
|
|
94
|
+
if (!line.accountCode || !accountCodes.has(line.accountCode)) {
|
|
95
|
+
errors.push({ field: `lines[${idx}].accountCode`, message: `unknown account code ${JSON.stringify(line.accountCode)}` });
|
|
96
|
+
}
|
|
97
|
+
if (line.debit !== undefined && !isNonNegativeNumber(line.debit)) {
|
|
98
|
+
errors.push({ field: `lines[${idx}].debit`, message: "debit must be a non-negative finite number" });
|
|
99
|
+
}
|
|
100
|
+
if (line.credit !== undefined && !isNonNegativeNumber(line.credit)) {
|
|
101
|
+
errors.push({ field: `lines[${idx}].credit`, message: "credit must be a non-negative finite number" });
|
|
102
|
+
}
|
|
103
|
+
if (!lineHasExactlyOneSide(line)) {
|
|
104
|
+
errors.push({ field: `lines[${idx}]`, message: "each line must set exactly one of debit or credit (and to a non-zero amount)" });
|
|
105
|
+
}
|
|
106
|
+
if (line.taxRegistrationId !== undefined) {
|
|
107
|
+
if (typeof line.taxRegistrationId !== "string") {
|
|
108
|
+
errors.push({ field: `lines[${idx}].taxRegistrationId`, message: "must be a string" });
|
|
109
|
+
} else if (line.taxRegistrationId.trim().length > MAX_TAX_REGISTRATION_ID_LENGTH) {
|
|
110
|
+
errors.push({
|
|
111
|
+
field: `lines[${idx}].taxRegistrationId`,
|
|
112
|
+
message: `must be at most ${MAX_TAX_REGISTRATION_ID_LENGTH} characters (got ${line.taxRegistrationId.trim().length})`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Normalize a journal line before persistence: trim string fields
|
|
119
|
+
* and drop empty-string optionals so the JSONL doesn't accumulate
|
|
120
|
+
* noise like `"taxRegistrationId":""`. Pure — does not mutate
|
|
121
|
+
* `line`. */
|
|
122
|
+
function normalizeLine(line: JournalLine): JournalLine {
|
|
123
|
+
const out: JournalLine = { ...line };
|
|
124
|
+
if (typeof out.taxRegistrationId === "string") {
|
|
125
|
+
const trimmed = out.taxRegistrationId.trim();
|
|
126
|
+
if (trimmed === "") delete out.taxRegistrationId;
|
|
127
|
+
else out.taxRegistrationId = trimmed;
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function validateEntry(input: { date: string; lines: readonly JournalLine[]; accounts: readonly Account[] }): ValidationResult {
|
|
133
|
+
const errors: ValidationError[] = [];
|
|
134
|
+
if (!isValidCalendarDate(input.date)) {
|
|
135
|
+
errors.push({ field: "date", message: `expected YYYY-MM-DD calendar date, got ${JSON.stringify(input.date)}` });
|
|
136
|
+
}
|
|
137
|
+
if (!Array.isArray(input.lines) || input.lines.length < 2) {
|
|
138
|
+
errors.push({ field: "lines", message: "an entry needs at least two lines (one debit, one credit)" });
|
|
139
|
+
return { ok: false, errors };
|
|
140
|
+
}
|
|
141
|
+
const accountCodes = new Set(input.accounts.map((account) => account.code));
|
|
142
|
+
input.lines.forEach((line, idx) => validateLine(line, idx, accountCodes, errors));
|
|
143
|
+
const net = netBalance(input.lines);
|
|
144
|
+
if (Math.abs(net) > EQUALITY_TOLERANCE) {
|
|
145
|
+
errors.push({ field: "lines", message: `Σ debit − Σ credit = ${net.toFixed(4)}; entry must balance` });
|
|
146
|
+
}
|
|
147
|
+
return { ok: errors.length === 0, errors };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Build a JournalEntry — validation is the caller's responsibility
|
|
151
|
+
* (it should have called `validateEntry` first). The id is a fresh
|
|
152
|
+
* UUID; createdAt is the wall clock at the moment of creation.
|
|
153
|
+
* Lines are normalized so optional string fields don't persist as
|
|
154
|
+
* empty strings. */
|
|
155
|
+
export function makeEntry(input: {
|
|
156
|
+
date: string;
|
|
157
|
+
lines: readonly JournalLine[];
|
|
158
|
+
memo?: string;
|
|
159
|
+
kind?: JournalEntry["kind"];
|
|
160
|
+
replacesEntryId?: string;
|
|
161
|
+
}): JournalEntry {
|
|
162
|
+
const entry: JournalEntry = {
|
|
163
|
+
id: randomUUID(),
|
|
164
|
+
date: input.date,
|
|
165
|
+
kind: input.kind ?? "normal",
|
|
166
|
+
lines: input.lines.map(normalizeLine),
|
|
167
|
+
memo: input.memo,
|
|
168
|
+
createdAt: new Date().toISOString(),
|
|
169
|
+
};
|
|
170
|
+
if (input.replacesEntryId) entry.replacesEntryId = input.replacesEntryId;
|
|
171
|
+
return entry;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Pick the most descriptive memo from the original entry to quote
|
|
175
|
+
* in the voiding entry's memo. Precedence: entry-level memo →
|
|
176
|
+
* first non-empty line memo → null (caller falls back to a
|
|
177
|
+
* date-only template). */
|
|
178
|
+
function originalMemoToQuote(target: JournalEntry): string | null {
|
|
179
|
+
if (target.memo && target.memo.trim() !== "") return target.memo;
|
|
180
|
+
for (const line of target.lines) {
|
|
181
|
+
if (line.memo && line.memo.trim() !== "") return line.memo;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Build the human-readable memo that goes on the voiding entry.
|
|
187
|
+
* Format: `void of '<original memo>' on <original date>` (or the
|
|
188
|
+
* no-memo fallback when the original carried no memo). The reason
|
|
189
|
+
* the user typed is appended after a colon when present. */
|
|
190
|
+
export function voidMemo(target: JournalEntry, reason: string | undefined): string {
|
|
191
|
+
const quoted = originalMemoToQuote(target);
|
|
192
|
+
const base = quoted !== null ? `void of '${quoted}' on ${target.date}` : `void of entry on ${target.date}`;
|
|
193
|
+
return reason && reason.trim() !== "" ? `${base}: ${reason}` : base;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Build the reversing pair for a voided entry. The `void` entry
|
|
197
|
+
* swaps debit / credit on every line so the net effect is zero;
|
|
198
|
+
* the `void-marker` is a zero-line entry that exists purely to
|
|
199
|
+
* carry the `voidedEntryId` reference and the user's reason. The
|
|
200
|
+
* marker keeps `listEntries` queries simple — filtering by
|
|
201
|
+
* `kind: "void-marker"` surfaces every voided id without scanning
|
|
202
|
+
* for matching pairs. */
|
|
203
|
+
export function makeVoidEntries(target: JournalEntry, reason: string | undefined, voidDate: string): { reverse: JournalEntry; marker: JournalEntry } {
|
|
204
|
+
const swappedLines: JournalLine[] = target.lines.map((line) => {
|
|
205
|
+
const swapped: JournalLine = {
|
|
206
|
+
accountCode: line.accountCode,
|
|
207
|
+
debit: line.credit,
|
|
208
|
+
credit: line.debit,
|
|
209
|
+
memo: line.memo,
|
|
210
|
+
};
|
|
211
|
+
// Preserve the counterparty tax-registration ID on each reversed
|
|
212
|
+
// line so the audit trail survives the void — without it, the
|
|
213
|
+
// reversing pair would silently drop the input-tax-credit
|
|
214
|
+
// documentation and a later report scan couldn't reconstruct
|
|
215
|
+
// which T-number / VAT ID the original input tax was tied to.
|
|
216
|
+
if (line.taxRegistrationId !== undefined) swapped.taxRegistrationId = line.taxRegistrationId;
|
|
217
|
+
return swapped;
|
|
218
|
+
});
|
|
219
|
+
const reverse: JournalEntry = {
|
|
220
|
+
id: randomUUID(),
|
|
221
|
+
date: voidDate,
|
|
222
|
+
kind: "void",
|
|
223
|
+
lines: swappedLines,
|
|
224
|
+
memo: voidMemo(target, reason),
|
|
225
|
+
voidedEntryId: target.id,
|
|
226
|
+
voidReason: reason,
|
|
227
|
+
createdAt: new Date().toISOString(),
|
|
228
|
+
};
|
|
229
|
+
const marker: JournalEntry = {
|
|
230
|
+
id: randomUUID(),
|
|
231
|
+
date: voidDate,
|
|
232
|
+
kind: "void-marker",
|
|
233
|
+
lines: [],
|
|
234
|
+
voidedEntryId: target.id,
|
|
235
|
+
voidReason: reason,
|
|
236
|
+
createdAt: new Date().toISOString(),
|
|
237
|
+
};
|
|
238
|
+
return { reverse, marker };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Returns the set of entry ids that have been voided — built from
|
|
242
|
+
* every `void-marker` entry's `voidedEntryId`. Reports use this to
|
|
243
|
+
* exclude original-and-reverse pairs from the activity listing
|
|
244
|
+
* (the netting is automatic in B/S aggregates because the reverse
|
|
245
|
+
* entry has equal-and-opposite lines). */
|
|
246
|
+
export function voidedIdSet(entries: readonly JournalEntry[]): Set<string> {
|
|
247
|
+
const set = new Set<string>();
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
if (entry.kind === "void-marker" && entry.voidedEntryId) set.add(entry.voidedEntryId);
|
|
250
|
+
}
|
|
251
|
+
return set;
|
|
252
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Opening balance ("year-start B/S") logic. Adoption flow: a user
|
|
2
|
+
// migrating from another bookkeeping system enters their existing
|
|
3
|
+
// asset / liability / equity balances as of a chosen `asOfDate`,
|
|
4
|
+
// instead of replaying their entire historical journal.
|
|
5
|
+
//
|
|
6
|
+
// Stored as a single `kind: "opening"` entry in the regular journal
|
|
7
|
+
// — keeps the journal as the single source of truth, and makes
|
|
8
|
+
// reports treat the opening as just an early entry without special
|
|
9
|
+
// branches in aggregation.
|
|
10
|
+
//
|
|
11
|
+
// Replacing an existing opening: void the old, append the new. The
|
|
12
|
+
// route handler is responsible for ordering this with snapshot
|
|
13
|
+
// invalidation so the "before" snapshots get dropped.
|
|
14
|
+
|
|
15
|
+
import type { Account, JournalEntry, JournalLine } from "./types.js";
|
|
16
|
+
import { BALANCE_SHEET_ACCOUNT_TYPES } from "./types.js";
|
|
17
|
+
import { isValidCalendarDate, netBalance, voidedIdSet } from "./journal.js";
|
|
18
|
+
|
|
19
|
+
const EQUALITY_TOLERANCE = 0.005;
|
|
20
|
+
|
|
21
|
+
export interface OpeningValidationError {
|
|
22
|
+
field: string;
|
|
23
|
+
message: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface OpeningValidationResult {
|
|
27
|
+
ok: boolean;
|
|
28
|
+
errors: OpeningValidationError[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Find the existing opening entry for a book, if any. Multiple
|
|
32
|
+
* openings shouldn't coexist (the route enforces void-then-append),
|
|
33
|
+
* but if they do the most recent by `createdAt` wins so callers
|
|
34
|
+
* always see one canonical opening. */
|
|
35
|
+
export function findActiveOpening(entries: readonly JournalEntry[]): JournalEntry | null {
|
|
36
|
+
const voided = voidedIdSet(entries);
|
|
37
|
+
let active: JournalEntry | null = null;
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (entry.kind !== "opening") continue;
|
|
40
|
+
if (voided.has(entry.id)) continue;
|
|
41
|
+
if (!active || entry.createdAt > active.createdAt) active = entry;
|
|
42
|
+
}
|
|
43
|
+
return active;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface OpeningValidationInput {
|
|
47
|
+
asOfDate: string;
|
|
48
|
+
lines: readonly JournalLine[];
|
|
49
|
+
accounts: readonly Account[];
|
|
50
|
+
existingEntries: readonly JournalEntry[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function validateLineAccountTypes(input: OpeningValidationInput, errors: OpeningValidationError[]): void {
|
|
54
|
+
const accountByCode = new Map(input.accounts.map((account) => [account.code, account]));
|
|
55
|
+
input.lines.forEach((line, idx) => {
|
|
56
|
+
const acct = accountByCode.get(line.accountCode);
|
|
57
|
+
if (!acct) {
|
|
58
|
+
errors.push({ field: `lines[${idx}].accountCode`, message: `unknown account code ${JSON.stringify(line.accountCode)}` });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!BALANCE_SHEET_ACCOUNT_TYPES.includes(acct.type)) {
|
|
62
|
+
errors.push({
|
|
63
|
+
field: `lines[${idx}].accountCode`,
|
|
64
|
+
message: `account ${acct.code} is type ${acct.type}; opening balances may only reference balance-sheet accounts (asset / liability / equity)`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateAsOfPredatesEverything(input: OpeningValidationInput, errors: OpeningValidationError[]): void {
|
|
71
|
+
// The point of the rule is "you can't enter an opening dated
|
|
72
|
+
// 2026-01-01 if you've already booked transactions in December
|
|
73
|
+
// 2025" — that would silently change the meaning of those
|
|
74
|
+
// December transactions. Existing openings (about to be
|
|
75
|
+
// replaced) and already-voided entries are exempt.
|
|
76
|
+
const voided = voidedIdSet(input.existingEntries);
|
|
77
|
+
for (const entry of input.existingEntries) {
|
|
78
|
+
if (entry.kind === "opening") continue;
|
|
79
|
+
if (entry.kind === "void-marker") continue;
|
|
80
|
+
if (voided.has(entry.id)) continue;
|
|
81
|
+
if (entry.date < input.asOfDate) {
|
|
82
|
+
errors.push({
|
|
83
|
+
field: "asOfDate",
|
|
84
|
+
message: `cannot set opening as of ${input.asOfDate}: existing entry ${entry.id} dated ${entry.date} is older. Void it first or pick an earlier asOfDate.`,
|
|
85
|
+
});
|
|
86
|
+
break; // one error is enough — listing every conflicting entry would be noisy
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Validate inputs for `setOpeningBalances`. Caller passes the full
|
|
92
|
+
* list of journal entries in the book so we can check the
|
|
93
|
+
* "asOfDate must precede every other entry" rule. An opening with
|
|
94
|
+
* zero lines is accepted as a no-op marker — it satisfies the
|
|
95
|
+
* "book has an opening" gate the UI uses without committing the
|
|
96
|
+
* user to specific balances on day one (they can replace it
|
|
97
|
+
* later). */
|
|
98
|
+
export function validateOpening(input: OpeningValidationInput): OpeningValidationResult {
|
|
99
|
+
const errors: OpeningValidationError[] = [];
|
|
100
|
+
if (!isValidCalendarDate(input.asOfDate)) {
|
|
101
|
+
errors.push({ field: "asOfDate", message: `expected YYYY-MM-DD calendar date, got ${JSON.stringify(input.asOfDate)}` });
|
|
102
|
+
}
|
|
103
|
+
if (!Array.isArray(input.lines)) {
|
|
104
|
+
errors.push({ field: "lines", message: "lines must be an array" });
|
|
105
|
+
return { ok: false, errors };
|
|
106
|
+
}
|
|
107
|
+
validateLineAccountTypes(input, errors);
|
|
108
|
+
const net = netBalance(input.lines);
|
|
109
|
+
if (Math.abs(net) > EQUALITY_TOLERANCE) {
|
|
110
|
+
errors.push({ field: "lines", message: `Σ debit − Σ credit = ${net.toFixed(4)}; opening must balance` });
|
|
111
|
+
}
|
|
112
|
+
validateAsOfPredatesEverything(input, errors);
|
|
113
|
+
return { ok: errors.length === 0, errors };
|
|
114
|
+
}
|