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
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
// Domain I/O: custom roles
|
|
2
|
-
// config/roles/<id>.json
|
|
3
|
-
//
|
|
4
|
-
// Optional `root` for test DI.
|
|
5
|
-
|
|
6
1
|
import path from "node:path";
|
|
7
2
|
import { mkdirSync, statSync, unlinkSync } from "node:fs";
|
|
8
|
-
import { WORKSPACE_DIRS } from "../../workspace/paths.js";
|
|
9
|
-
import { workspacePath } from "../../workspace/paths.js";
|
|
3
|
+
import { WORKSPACE_DIRS, workspacePath } from "../../workspace/paths.js";
|
|
10
4
|
import { writeFileAtomicSync } from "./atomic.js";
|
|
11
5
|
import { isEnoent } from "./safe.js";
|
|
12
6
|
|
|
@@ -16,7 +10,6 @@ function roleFilePath(roleId: string, workspaceRoot?: string): string {
|
|
|
16
10
|
return path.join(root(workspaceRoot), WORKSPACE_DIRS.roles, `${roleId}.json`);
|
|
17
11
|
}
|
|
18
12
|
|
|
19
|
-
/** Check if a custom role file exists. */
|
|
20
13
|
export function roleExists(roleId: string, workspaceRoot?: string): boolean {
|
|
21
14
|
try {
|
|
22
15
|
statSync(roleFilePath(roleId, workspaceRoot));
|
|
@@ -26,7 +19,7 @@ export function roleExists(roleId: string, workspaceRoot?: string): boolean {
|
|
|
26
19
|
}
|
|
27
20
|
}
|
|
28
21
|
|
|
29
|
-
|
|
22
|
+
// Returns false if not found.
|
|
30
23
|
export function deleteRole(roleId: string, workspaceRoot?: string): boolean {
|
|
31
24
|
try {
|
|
32
25
|
unlinkSync(roleFilePath(roleId, workspaceRoot));
|
|
@@ -37,7 +30,6 @@ export function deleteRole(roleId: string, workspaceRoot?: string): boolean {
|
|
|
37
30
|
}
|
|
38
31
|
}
|
|
39
32
|
|
|
40
|
-
/** Save (create or overwrite) a custom role file atomically. */
|
|
41
33
|
export function saveRole(roleId: string, data: unknown, workspaceRoot?: string): void {
|
|
42
34
|
const dir = path.join(root(workspaceRoot), WORKSPACE_DIRS.roles);
|
|
43
35
|
mkdirSync(dir, { recursive: true });
|
|
@@ -1,22 +1,14 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// `resolveWithinRoot` is the realpath-based path-traversal check
|
|
5
|
-
// that underpins every endpoint serving files out of the workspace.
|
|
6
|
-
//
|
|
7
|
-
// Moved from server/utils/fs.ts (issue #366 Phase 1). The old
|
|
8
|
-
// file re-exports these for backwards compat.
|
|
1
|
+
// Wrappers that swallow ENOENT/EACCES so callers branch on `result === null` instead of try/catch.
|
|
2
|
+
// resolveWithinRoot is the realpath-based traversal check used by every endpoint serving workspace files.
|
|
9
3
|
|
|
10
4
|
import { Dirent, Stats, promises, readFileSync, readdirSync, realpathSync, statSync } from "fs";
|
|
11
5
|
import path from "path";
|
|
12
6
|
import { isErrorWithCode } from "../types.js";
|
|
13
7
|
|
|
14
|
-
/** Check if an error is ENOENT (file/dir not found). */
|
|
15
8
|
export function isEnoent(err: unknown): boolean {
|
|
16
9
|
return isErrorWithCode(err) && err.code === "ENOENT";
|
|
17
10
|
}
|
|
18
11
|
|
|
19
|
-
/** Read a binary file by absolute path. Null on ENOENT. */
|
|
20
12
|
export function readBinarySafeSync(absPath: string): Buffer | null {
|
|
21
13
|
try {
|
|
22
14
|
return readFileSync(absPath);
|
|
@@ -25,7 +17,6 @@ export function readBinarySafeSync(absPath: string): Buffer | null {
|
|
|
25
17
|
}
|
|
26
18
|
}
|
|
27
19
|
|
|
28
|
-
/** Read a text file by absolute path (async). Null on ENOENT. */
|
|
29
20
|
export async function readTextSafe(absPath: string): Promise<string | null> {
|
|
30
21
|
try {
|
|
31
22
|
return await promises.readFile(absPath, "utf-8");
|
|
@@ -34,7 +25,6 @@ export async function readTextSafe(absPath: string): Promise<string | null> {
|
|
|
34
25
|
}
|
|
35
26
|
}
|
|
36
27
|
|
|
37
|
-
/** Read a text file by absolute path (sync). Null on ENOENT. */
|
|
38
28
|
export function readTextSafeSync(absPath: string): string | null {
|
|
39
29
|
try {
|
|
40
30
|
return readFileSync(absPath, "utf-8");
|
|
@@ -83,13 +73,21 @@ export async function readTextOrNull(file: string): Promise<string | null> {
|
|
|
83
73
|
}
|
|
84
74
|
}
|
|
85
75
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
76
|
+
// True if any segment of `relPath` (split on either `/` or `\`)
|
|
77
|
+
// starts with a dot — the same policy `express.static({ dotfiles:
|
|
78
|
+
// "deny" })` applies. Splits on both separators because
|
|
79
|
+
// `decodeURIComponent` of `%5C` produces a literal `\`, and on
|
|
80
|
+
// Windows `path.normalize` (used downstream by `resolveWithinRoot`)
|
|
81
|
+
// treats `\` as a separator. Without the dual split, a request like
|
|
82
|
+
// `/dir%5C.hidden.html` decodes to `dir\.hidden.html` → splits on
|
|
83
|
+
// `/` as one segment `dir\.hidden.html` (no leading dot) → bypasses
|
|
84
|
+
// the guard on Windows even though `path.normalize` later resolves
|
|
85
|
+
// it to `dir/.hidden.html`. (Codex review on PR #1082.)
|
|
86
|
+
export function containsDotfileSegment(relPath: string): boolean {
|
|
87
|
+
return relPath.split(/[/\\]/).some((segment) => segment.startsWith("."));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// `rootReal` MUST already be a realpath. Returns null on traversal or if either path doesn't exist on disk.
|
|
93
91
|
export function resolveWithinRoot(rootReal: string, relPath: string): string | null {
|
|
94
92
|
const normalized = path.normalize(relPath || "");
|
|
95
93
|
const resolved = path.resolve(rootReal, normalized);
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// data/scheduler/items.json
|
|
3
|
-
//
|
|
4
|
-
// Sync API. Optional `root` for test DI.
|
|
5
|
-
|
|
6
|
-
import { WORKSPACE_FILES } from "../../workspace/paths.js";
|
|
7
|
-
import { workspacePath } from "../../workspace/paths.js";
|
|
1
|
+
import { WORKSPACE_FILES, workspacePath } from "../../workspace/paths.js";
|
|
8
2
|
import { resolvePath } from "./workspace-io.js";
|
|
9
3
|
import { loadJsonFile } from "./json.js";
|
|
10
4
|
import { writeFileAtomicSync } from "./atomic.js";
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
// Domain I/O for scheduler override config.
|
|
2
|
-
//
|
|
3
|
-
// Reads/writes config/scheduler/overrides.json. Each key is a
|
|
4
|
-
// system task ID (e.g. "system:journal"), value overrides the
|
|
5
|
-
// default schedule.
|
|
6
|
-
|
|
7
1
|
import { mkdirSync } from "fs";
|
|
8
2
|
import path from "path";
|
|
9
3
|
import { workspacePath } from "../../workspace/paths.js";
|
|
@@ -14,15 +8,14 @@ import { log } from "../../system/logger/index.js";
|
|
|
14
8
|
import { isRecord } from "../types.js";
|
|
15
9
|
|
|
16
10
|
export interface ScheduleOverride {
|
|
17
|
-
/** Override interval in milliseconds (for interval-type schedules). */
|
|
18
11
|
intervalMs?: number;
|
|
19
|
-
|
|
12
|
+
// "HH:MM" in UTC for daily schedules.
|
|
20
13
|
time?: string;
|
|
21
14
|
}
|
|
22
15
|
|
|
23
16
|
export type ScheduleOverrides = Record<string, ScheduleOverride>;
|
|
24
17
|
|
|
25
|
-
|
|
18
|
+
// Strict HH:MM — rejects 99:99 etc.
|
|
26
19
|
export const UTC_HH_MM_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
27
20
|
|
|
28
21
|
function isScheduleOverride(value: unknown): value is ScheduleOverride {
|
|
@@ -30,7 +23,6 @@ function isScheduleOverride(value: unknown): value is ScheduleOverride {
|
|
|
30
23
|
const obj = value;
|
|
31
24
|
const hasInterval = "intervalMs" in obj && typeof obj.intervalMs === "number" && obj.intervalMs > 0;
|
|
32
25
|
const hasTime = "time" in obj && typeof obj.time === "string" && UTC_HH_MM_RE.test(obj.time);
|
|
33
|
-
// At least one valid field required
|
|
34
26
|
return hasInterval || hasTime;
|
|
35
27
|
}
|
|
36
28
|
|
|
@@ -38,7 +30,7 @@ function overridesPath(root?: string): string {
|
|
|
38
30
|
return path.join(root ?? workspacePath, WORKSPACE_FILES.schedulerOverrides);
|
|
39
31
|
}
|
|
40
32
|
|
|
41
|
-
|
|
33
|
+
// Filters out invalid entries with a warning.
|
|
42
34
|
export function loadSchedulerOverrides(root?: string): ScheduleOverrides {
|
|
43
35
|
const raw = loadJsonFile<unknown>(overridesPath(root), {});
|
|
44
36
|
if (!isRecord(raw)) {
|
|
@@ -56,7 +48,6 @@ export function loadSchedulerOverrides(root?: string): ScheduleOverrides {
|
|
|
56
48
|
return result;
|
|
57
49
|
}
|
|
58
50
|
|
|
59
|
-
/** Save schedule overrides atomically. Creates directory if needed. */
|
|
60
51
|
export function saveSchedulerOverrides(overrides: ScheduleOverrides, root?: string): void {
|
|
61
52
|
const filePath = overridesPath(root);
|
|
62
53
|
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -1,19 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
// conversations/chat/<id>.json — metadata
|
|
3
|
-
// conversations/chat/<id>.jsonl — event log
|
|
4
|
-
//
|
|
5
|
-
// All functions take optional `root` for test DI.
|
|
6
|
-
|
|
7
|
-
import { appendFile } from "fs/promises";
|
|
1
|
+
import { appendFile, rm } from "fs/promises";
|
|
8
2
|
import path from "node:path";
|
|
9
|
-
import { WORKSPACE_DIRS } from "../../workspace/paths.js";
|
|
10
|
-
import { workspacePath } from "../../workspace/paths.js";
|
|
3
|
+
import { WORKSPACE_DIRS, workspacePath } from "../../workspace/paths.js";
|
|
11
4
|
import { readTextUnder, writeTextUnder, resolvePath, ensureWorkspaceDir } from "./workspace-io.js";
|
|
5
|
+
import type { SessionOrigin } from "../../../src/types/session.js";
|
|
12
6
|
|
|
13
7
|
const CHAT = WORKSPACE_DIRS.chat;
|
|
14
8
|
const root = (rootOverride?: string) => rootOverride ?? workspacePath;
|
|
15
9
|
|
|
16
|
-
/** Ensure the chat directory exists. Called once at session start. */
|
|
17
10
|
export function ensureChatDir(): void {
|
|
18
11
|
ensureWorkspaceDir(CHAT);
|
|
19
12
|
}
|
|
@@ -26,21 +19,19 @@ function jsonlRel(sessionId: string): string {
|
|
|
26
19
|
return path.posix.join(CHAT, `${sessionId}.jsonl`);
|
|
27
20
|
}
|
|
28
21
|
|
|
29
|
-
// ── Meta ────────────────────────────────────────────────────────
|
|
30
|
-
|
|
31
22
|
export interface SessionMeta {
|
|
32
23
|
roleId?: string;
|
|
33
24
|
startedAt?: string;
|
|
34
25
|
firstUserMessage?: string;
|
|
35
26
|
claudeSessionId?: string;
|
|
36
27
|
hasUnread?: boolean;
|
|
37
|
-
|
|
28
|
+
isBookmarked?: boolean;
|
|
29
|
+
origin?: SessionOrigin;
|
|
38
30
|
[key: string]: unknown;
|
|
39
31
|
}
|
|
40
32
|
|
|
41
33
|
export type ReadMetaResult = { kind: "missing" } | { kind: "ok"; meta: SessionMeta } | { kind: "corrupt"; raw: string };
|
|
42
34
|
|
|
43
|
-
/** Read session metadata with full outcome discrimination. */
|
|
44
35
|
export async function readSessionMetaFull(sessionId: string, rootOverride?: string): Promise<ReadMetaResult> {
|
|
45
36
|
const raw = await readTextUnder(root(rootOverride), metaRel(sessionId));
|
|
46
37
|
if (raw === null) return { kind: "missing" };
|
|
@@ -51,8 +42,7 @@ export async function readSessionMetaFull(sessionId: string, rootOverride?: stri
|
|
|
51
42
|
}
|
|
52
43
|
}
|
|
53
44
|
|
|
54
|
-
|
|
55
|
-
* (callers that need to distinguish use readSessionMetaFull). */
|
|
45
|
+
// Treats corrupt as null — callers that need to distinguish use readSessionMetaFull.
|
|
56
46
|
export async function readSessionMeta(sessionId: string, rootOverride?: string): Promise<SessionMeta | null> {
|
|
57
47
|
const result = await readSessionMetaFull(sessionId, rootOverride);
|
|
58
48
|
return result.kind === "ok" ? result.meta : null;
|
|
@@ -103,18 +93,25 @@ export async function updateHasUnread(sessionId: string, hasUnread: boolean, roo
|
|
|
103
93
|
await writeSessionMeta(sessionId, { ...meta, hasUnread }, rootOverride);
|
|
104
94
|
}
|
|
105
95
|
|
|
106
|
-
|
|
96
|
+
export async function updateIsBookmarked(sessionId: string, isBookmarked: boolean, rootOverride?: string): Promise<void> {
|
|
97
|
+
const meta = await readSessionMeta(sessionId, rootOverride);
|
|
98
|
+
if (!meta) return;
|
|
99
|
+
await writeSessionMeta(sessionId, { ...meta, isBookmarked }, rootOverride);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Hard-deletes the session's .jsonl event log and .json meta sidecar.
|
|
103
|
+
// Missing files are tolerated — callers may invoke this for sessions
|
|
104
|
+
// whose meta or jsonl was never written (e.g. a crash mid-create).
|
|
105
|
+
export async function deleteSessionFiles(sessionId: string, rootOverride?: string): Promise<void> {
|
|
106
|
+
await rm(sessionJsonlAbsPath(sessionId, rootOverride), { force: true });
|
|
107
|
+
await rm(sessionMetaAbsPath(sessionId, rootOverride), { force: true });
|
|
108
|
+
}
|
|
107
109
|
|
|
108
110
|
export function sessionJsonlAbsPath(sessionId: string, rootOverride?: string): string {
|
|
109
111
|
return resolvePath(root(rootOverride), jsonlRel(sessionId));
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
* Resolve the absolute path of a session's metadata JSON file. The
|
|
114
|
-
* jsonl variant is the event log; this one is the sidecar that holds
|
|
115
|
-
* `hasUnread`, `roleId`, `startedAt`, `origin`, etc. Its mtime bumps
|
|
116
|
-
* whenever any of those fields change via `writeSessionMeta`.
|
|
117
|
-
*/
|
|
114
|
+
// .json sidecar to the event-log jsonl. mtime bumps on every writeSessionMeta — used as a "session changed" signal.
|
|
118
115
|
export function sessionMetaAbsPath(sessionId: string, rootOverride?: string): string {
|
|
119
116
|
return resolvePath(root(rootOverride), metaRel(sessionId));
|
|
120
117
|
}
|
|
@@ -123,13 +120,7 @@ export async function readSessionJsonl(sessionId: string, rootOverride?: string)
|
|
|
123
120
|
return readTextUnder(root(rootOverride), jsonlRel(sessionId));
|
|
124
121
|
}
|
|
125
122
|
|
|
126
|
-
|
|
127
|
-
* Append a single line to the session event log (JSONL format).
|
|
128
|
-
*
|
|
129
|
-
* The function **ensures a trailing `\n`** — callers pass the raw
|
|
130
|
-
* content and don't need to worry about line termination. This
|
|
131
|
-
* prevents JSONL parse failures from missing newlines.
|
|
132
|
-
*/
|
|
123
|
+
// Always ends with `\n` to prevent JSONL parse failures from a missing terminator.
|
|
133
124
|
export async function appendSessionLine(sessionId: string, line: string, rootOverride?: string): Promise<void> {
|
|
134
125
|
const normalized = line.endsWith("\n") ? line : `${line}\n`;
|
|
135
126
|
await appendFile(resolvePath(root(rootOverride), jsonlRel(sessionId)), normalized);
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { mkdir, realpath
|
|
1
|
+
import { mkdir, realpath } from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { WORKSPACE_DIRS, WORKSPACE_PATHS } from "../../workspace/paths.js";
|
|
4
4
|
import { shortId } from "../id.js";
|
|
5
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
5
6
|
import { yearMonthUtc } from "./naming.js";
|
|
6
7
|
import { resolveWithinRoot } from "./safe.js";
|
|
7
8
|
|
|
8
9
|
const SPREADSHEETS_DIR = WORKSPACE_PATHS.spreadsheets;
|
|
9
10
|
|
|
10
|
-
//
|
|
11
|
-
// requires its root argument to be a realpath so symlinks are handled
|
|
12
|
-
// correctly. Matches the pattern used in image-store.ts.
|
|
11
|
+
// resolveWithinRoot needs a realpath as its root so symlinks resolve correctly (same pattern as image-store).
|
|
13
12
|
let spreadsheetsDirReal: string | null = null;
|
|
14
13
|
|
|
15
14
|
async function ensureSpreadsheetsDir(): Promise<string> {
|
|
@@ -19,13 +18,9 @@ async function ensureSpreadsheetsDir(): Promise<string> {
|
|
|
19
18
|
return spreadsheetsDirReal;
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
//
|
|
23
|
-
// into an absolute path guaranteed to be inside the spreadsheets directory.
|
|
24
|
-
// Throws on traversal attempts.
|
|
21
|
+
// Throws on traversal. Strips a leading "spreadsheets/" so callers can pass either the stored form or bare filename.
|
|
25
22
|
async function safeResolve(relativePath: string): Promise<string> {
|
|
26
23
|
const root = await ensureSpreadsheetsDir();
|
|
27
|
-
// Strip the leading "spreadsheets/" prefix so callers can pass either
|
|
28
|
-
// the stored form or just the filename.
|
|
29
24
|
const name = relativePath.replace(new RegExp(`^${WORKSPACE_DIRS.spreadsheets}/`), "");
|
|
30
25
|
const result = resolveWithinRoot(root, name);
|
|
31
26
|
if (!result) {
|
|
@@ -34,33 +29,25 @@ async function safeResolve(relativePath: string): Promise<string> {
|
|
|
34
29
|
return result;
|
|
35
30
|
}
|
|
36
31
|
|
|
37
|
-
|
|
38
|
-
* `spreadsheets/YYYY/MM/` (UTC) so the dir doesn't accumulate
|
|
39
|
-
* unbounded — see #764. Returns the workspace-relative path. */
|
|
32
|
+
// #764 sharded under spreadsheets/YYYY/MM/ (UTC) so the dir doesn't grow unbounded; #881 atomic.
|
|
40
33
|
export async function saveSpreadsheet(sheets: unknown[]): Promise<string> {
|
|
41
34
|
await ensureSpreadsheetsDir();
|
|
42
35
|
const partition = yearMonthUtc();
|
|
43
|
-
const parentAbs = path.join(SPREADSHEETS_DIR, partition);
|
|
44
|
-
await mkdir(parentAbs, { recursive: true });
|
|
45
36
|
const filename = `${shortId()}.json`;
|
|
46
|
-
|
|
37
|
+
const absPath = path.join(SPREADSHEETS_DIR, partition, filename);
|
|
38
|
+
await writeFileAtomic(absPath, JSON.stringify(sheets));
|
|
47
39
|
return path.posix.join(WORKSPACE_DIRS.spreadsheets, partition, filename);
|
|
48
40
|
}
|
|
49
41
|
|
|
50
|
-
/** Overwrite an existing spreadsheet file. */
|
|
51
42
|
export async function overwriteSpreadsheet(relativePath: string, sheets: unknown[]): Promise<void> {
|
|
52
43
|
const absPath = await safeResolve(relativePath);
|
|
53
|
-
await
|
|
44
|
+
await writeFileAtomic(absPath, JSON.stringify(sheets));
|
|
54
45
|
}
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
* Rejects traversal attempts like "spreadsheets/../outside.json"
|
|
58
|
-
* so the caller can't rely on the prefix/suffix alone. */
|
|
47
|
+
// Reject "spreadsheets/../outside.json" early; realpath check still runs server-side, but catch obvious cases here.
|
|
59
48
|
export function isSpreadsheetPath(value: string): boolean {
|
|
60
49
|
if (!value.startsWith(`${WORKSPACE_DIRS.spreadsheets}/`)) return false;
|
|
61
50
|
if (!value.endsWith(".json")) return false;
|
|
62
|
-
// Forbid .. segments anywhere in the path — a realpath check still
|
|
63
|
-
// happens server-side, but this catches obvious cases early.
|
|
64
51
|
const normalized = path.posix.normalize(value);
|
|
65
52
|
if (normalized !== value) return false;
|
|
66
53
|
if (normalized.includes("..")) return false;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Domain file I/O for the translation cache. One JSON file per
|
|
2
|
+
// namespace under `data/translation/`. All writes go through the
|
|
3
|
+
// atomic helper per project rule.
|
|
4
|
+
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { WORKSPACE_DIRS, workspacePath } from "../../workspace/paths.js";
|
|
7
|
+
import { loadJsonFile, writeJsonAtomic } from "./json.js";
|
|
8
|
+
import { emptyDictionary } from "../../services/translation/cache.js";
|
|
9
|
+
import type { DictionaryFile } from "../../services/translation/types.js";
|
|
10
|
+
|
|
11
|
+
function root(workspaceRoot?: string): string {
|
|
12
|
+
return workspaceRoot ?? workspacePath;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function dictionaryPath(namespace: string, workspaceRoot?: string): string {
|
|
16
|
+
return path.join(root(workspaceRoot), WORKSPACE_DIRS.translation, `${namespace}.json`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Cache files live under the user's workspace and can be hand-edited
|
|
20
|
+
// or corrupted; treat the disk shape as untrusted and fall back to
|
|
21
|
+
// an empty dictionary on anything we can't recognize. Without this
|
|
22
|
+
// guard a `{}` or `{ sentences: null }` file would crash later at
|
|
23
|
+
// `dict.sentences[sentence]` and turn every request for the namespace
|
|
24
|
+
// into a 500 until the file was repaired.
|
|
25
|
+
function isValidDictionary(value: unknown): value is DictionaryFile {
|
|
26
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
27
|
+
const { sentences } = value as { sentences?: unknown };
|
|
28
|
+
if (typeof sentences !== "object" || sentences === null || Array.isArray(sentences)) return false;
|
|
29
|
+
for (const inner of Object.values(sentences)) {
|
|
30
|
+
if (typeof inner !== "object" || inner === null || Array.isArray(inner)) return false;
|
|
31
|
+
for (const translated of Object.values(inner as Record<string, unknown>)) {
|
|
32
|
+
if (typeof translated !== "string") return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function loadDictionary(namespace: string, workspaceRoot?: string): DictionaryFile {
|
|
39
|
+
const raw = loadJsonFile<unknown>(dictionaryPath(namespace, workspaceRoot), null);
|
|
40
|
+
if (!isValidDictionary(raw)) return emptyDictionary();
|
|
41
|
+
return raw;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function saveDictionary(namespace: string, dict: DictionaryFile, workspaceRoot?: string): Promise<void> {
|
|
45
|
+
await writeJsonAtomic(dictionaryPath(namespace, workspaceRoot), dict);
|
|
46
|
+
}
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
// Domain I/O: user-created scheduled tasks
|
|
2
|
-
// config/scheduler/tasks.json
|
|
3
|
-
//
|
|
4
|
-
// Optional `root` parameter for test DI (defaults to workspacePath).
|
|
5
|
-
|
|
6
1
|
import path from "path";
|
|
7
2
|
import { mkdir } from "fs/promises";
|
|
8
|
-
import { WORKSPACE_FILES } from "../../workspace/paths.js";
|
|
9
|
-
import { workspacePath } from "../../workspace/paths.js";
|
|
3
|
+
import { WORKSPACE_FILES, workspacePath } from "../../workspace/paths.js";
|
|
10
4
|
import { resolvePath } from "./workspace-io.js";
|
|
11
5
|
import { loadJsonFile } from "./json.js";
|
|
12
6
|
import { writeFileAtomic } from "./atomic.js";
|
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// Combines WORKSPACE_PATHS (path resolution) with the atomic/safe
|
|
5
|
-
// helpers (I/O primitives) so call sites never need raw `path.join`
|
|
6
|
-
// + raw `fs.*` for workspace files.
|
|
7
|
-
//
|
|
8
|
-
// All writes go through writeFileAtomic so concurrent readers always
|
|
9
|
-
// see a consistent file — never a half-written one.
|
|
10
|
-
//
|
|
11
|
-
// All reads swallow ENOENT and return null / fallback so callers can
|
|
12
|
-
// do `if (!content)` instead of try/catch.
|
|
1
|
+
// All writes go through writeFileAtomic so concurrent readers never see a half-written file. All reads swallow ENOENT
|
|
2
|
+
// and return null/fallback so callers can branch on `!content` instead of try/catch.
|
|
13
3
|
|
|
14
4
|
import { Stats, mkdirSync, promises, readFileSync, readdirSync, statSync } from "fs";
|
|
15
5
|
import path from "path";
|
|
@@ -24,24 +14,10 @@ function rethrowUnexpected(err: unknown, context: string): null {
|
|
|
24
14
|
throw err;
|
|
25
15
|
}
|
|
26
16
|
|
|
27
|
-
// ── Path resolution ─────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Resolve a workspace-relative path to an absolute path.
|
|
31
|
-
* Use this instead of `path.join(workspacePath, rel)` in
|
|
32
|
-
* implementation code — keeps the workspace root reference in
|
|
33
|
-
* one place.
|
|
34
|
-
*/
|
|
35
17
|
export function resolveWorkspacePath(relPath: string): string {
|
|
36
18
|
return path.join(workspacePath, relPath);
|
|
37
19
|
}
|
|
38
20
|
|
|
39
|
-
// ── Read ────────────────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Read a text file under the workspace. Returns null on ENOENT;
|
|
43
|
-
* logs and re-throws unexpected errors (EACCES, EPERM, etc.).
|
|
44
|
-
*/
|
|
45
21
|
export async function readWorkspaceText(relPath: string): Promise<string | null> {
|
|
46
22
|
try {
|
|
47
23
|
return await promises.readFile(resolveWorkspacePath(relPath), "utf-8");
|
|
@@ -50,7 +26,6 @@ export async function readWorkspaceText(relPath: string): Promise<string | null>
|
|
|
50
26
|
}
|
|
51
27
|
}
|
|
52
28
|
|
|
53
|
-
/** Sync variant. Same ENOENT-only swallow contract. */
|
|
54
29
|
export function readWorkspaceTextSync(relPath: string): string | null {
|
|
55
30
|
try {
|
|
56
31
|
return readFileSync(resolveWorkspacePath(relPath), "utf-8");
|
|
@@ -59,10 +34,6 @@ export function readWorkspaceTextSync(relPath: string): string | null {
|
|
|
59
34
|
}
|
|
60
35
|
}
|
|
61
36
|
|
|
62
|
-
/**
|
|
63
|
-
* Read and parse a JSON file under the workspace. Returns
|
|
64
|
-
* `fallback` if the file is missing, unreadable, or malformed.
|
|
65
|
-
*/
|
|
66
37
|
export async function readWorkspaceJson<T>(relPath: string, fallback: T): Promise<T> {
|
|
67
38
|
const text = await readWorkspaceText(relPath);
|
|
68
39
|
if (text === null) return fallback;
|
|
@@ -73,7 +44,6 @@ export async function readWorkspaceJson<T>(relPath: string, fallback: T): Promis
|
|
|
73
44
|
}
|
|
74
45
|
}
|
|
75
46
|
|
|
76
|
-
/** Sync variant of `readWorkspaceJson`. */
|
|
77
47
|
export function readWorkspaceJsonSync<T>(relPath: string, fallback: T): T {
|
|
78
48
|
const text = readWorkspaceTextSync(relPath);
|
|
79
49
|
if (text === null) return fallback;
|
|
@@ -84,53 +54,23 @@ export function readWorkspaceJsonSync<T>(relPath: string, fallback: T): T {
|
|
|
84
54
|
}
|
|
85
55
|
}
|
|
86
56
|
|
|
87
|
-
// ── Write ───────────────────────────────────────────────────────
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Write a text file under the workspace atomically.
|
|
91
|
-
* Parent directories are created if missing.
|
|
92
|
-
*/
|
|
93
57
|
export async function writeWorkspaceText(relPath: string, content: string, opts?: { mode?: number }): Promise<void> {
|
|
94
58
|
await writeFileAtomic(resolveWorkspacePath(relPath), content, opts);
|
|
95
59
|
}
|
|
96
60
|
|
|
97
|
-
/** Sync variant for startup / init paths. */
|
|
98
61
|
export function writeWorkspaceTextSync(relPath: string, content: string, opts?: { mode?: number }): void {
|
|
99
62
|
writeFileAtomicSync(resolveWorkspacePath(relPath), content, opts);
|
|
100
63
|
}
|
|
101
64
|
|
|
102
|
-
/**
|
|
103
|
-
* Write a JSON value under the workspace atomically.
|
|
104
|
-
* Pretty-printed with 2-space indent.
|
|
105
|
-
*/
|
|
106
65
|
export async function writeWorkspaceJson(relPath: string, data: unknown, opts?: { mode?: number }): Promise<void> {
|
|
107
66
|
await writeFileAtomic(resolveWorkspacePath(relPath), JSON.stringify(data, null, 2), opts);
|
|
108
67
|
}
|
|
109
68
|
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// Modules that take `root` as a parameter (journal, sources, etc.)
|
|
113
|
-
// use these instead of raw path.join + fs.*. Same contract as the
|
|
114
|
-
// workspace-* helpers, but root is caller-supplied.
|
|
115
|
-
//
|
|
116
|
-
// **IMPORTANT — internal paths only.** These helpers do NOT guard
|
|
117
|
-
// against `..` traversal. They are designed for domain I/O modules
|
|
118
|
-
// that pass compile-time-fixed relative paths like
|
|
119
|
-
// `${WORKSPACE_DIRS.chat}/${id}.json`. User-supplied or HTTP-body
|
|
120
|
-
// paths MUST go through `resolveWithinRoot()` in `safe.ts` instead.
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Resolve root + relPath. Replaces raw `path.join(root, rel)`.
|
|
124
|
-
*
|
|
125
|
-
* For **internal fixed paths only** — never pass user input as
|
|
126
|
-
* `relPath`. Use `resolveWithinRoot()` for user-supplied paths.
|
|
127
|
-
*/
|
|
69
|
+
// **Internal fixed paths only.** No `..` traversal guard — user-supplied paths MUST go through resolveWithinRoot() in safe.ts.
|
|
128
70
|
export function resolvePath(root: string, relPath: string): string {
|
|
129
71
|
return path.join(root, relPath);
|
|
130
72
|
}
|
|
131
73
|
|
|
132
|
-
/** Read text under an arbitrary root. Null on ENOENT; rethrows
|
|
133
|
-
* unexpected errors. */
|
|
134
74
|
export async function readTextUnder(root: string, relPath: string): Promise<string | null> {
|
|
135
75
|
try {
|
|
136
76
|
return await promises.readFile(path.join(root, relPath), "utf-8");
|
|
@@ -139,12 +79,10 @@ export async function readTextUnder(root: string, relPath: string): Promise<stri
|
|
|
139
79
|
}
|
|
140
80
|
}
|
|
141
81
|
|
|
142
|
-
/** Write atomically under an arbitrary root. */
|
|
143
82
|
export async function writeTextUnder(root: string, relPath: string, content: string): Promise<void> {
|
|
144
83
|
await writeFileAtomic(path.join(root, relPath), content);
|
|
145
84
|
}
|
|
146
85
|
|
|
147
|
-
/** Sync read text under a root. Null on ENOENT. */
|
|
148
86
|
export function readTextUnderSync(root: string, relPath: string): string | null {
|
|
149
87
|
try {
|
|
150
88
|
return readFileSync(path.join(root, relPath), "utf-8");
|
|
@@ -153,7 +91,6 @@ export function readTextUnderSync(root: string, relPath: string): string | null
|
|
|
153
91
|
}
|
|
154
92
|
}
|
|
155
93
|
|
|
156
|
-
/** Sync readdir under a root. Empty on ENOENT. */
|
|
157
94
|
export function readdirUnderSync(root: string, relPath: string): string[] {
|
|
158
95
|
try {
|
|
159
96
|
return readdirSync(path.join(root, relPath));
|
|
@@ -166,7 +103,6 @@ export function readdirUnderSync(root: string, relPath: string): string[] {
|
|
|
166
103
|
}
|
|
167
104
|
}
|
|
168
105
|
|
|
169
|
-
/** Readdir under a root. Empty on ENOENT; rethrows unexpected. */
|
|
170
106
|
export async function readdirUnder(root: string, relPath: string): Promise<string[]> {
|
|
171
107
|
try {
|
|
172
108
|
return await promises.readdir(path.join(root, relPath));
|
|
@@ -179,7 +115,6 @@ export async function readdirUnder(root: string, relPath: string): Promise<strin
|
|
|
179
115
|
}
|
|
180
116
|
}
|
|
181
117
|
|
|
182
|
-
/** Stat under a root. Null on ENOENT; rethrows unexpected. */
|
|
183
118
|
export async function statUnder(root: string, relPath: string): Promise<Stats | null> {
|
|
184
119
|
try {
|
|
185
120
|
return await promises.stat(path.join(root, relPath));
|
|
@@ -188,17 +123,10 @@ export async function statUnder(root: string, relPath: string): Promise<Stats |
|
|
|
188
123
|
}
|
|
189
124
|
}
|
|
190
125
|
|
|
191
|
-
/** Ensure a directory exists under a root. */
|
|
192
126
|
export async function ensureDirUnder(root: string, relPath: string): Promise<void> {
|
|
193
127
|
await promises.mkdir(path.join(root, relPath), { recursive: true });
|
|
194
128
|
}
|
|
195
129
|
|
|
196
|
-
// ── Existence ───────────────────────────────────────────────────
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Check whether a workspace-relative path exists on disk.
|
|
200
|
-
* Returns false on ENOENT; rethrows unexpected errors.
|
|
201
|
-
*/
|
|
202
130
|
export function existsInWorkspace(relPath: string): boolean {
|
|
203
131
|
try {
|
|
204
132
|
statSync(resolveWorkspacePath(relPath));
|
|
@@ -212,10 +140,6 @@ export function existsInWorkspace(relPath: string): boolean {
|
|
|
212
140
|
}
|
|
213
141
|
}
|
|
214
142
|
|
|
215
|
-
/**
|
|
216
|
-
* Ensure a workspace-relative directory exists. Creates it
|
|
217
|
-
* (including parents) if missing. Idempotent.
|
|
218
|
-
*/
|
|
219
143
|
export function ensureWorkspaceDir(relPath: string): void {
|
|
220
144
|
mkdirSync(resolveWorkspacePath(relPath), { recursive: true });
|
|
221
145
|
}
|