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,7 +1,8 @@
|
|
|
1
1
|
import { Router, Request, Response } from "express";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { WORKSPACE_PATHS } from "../../workspace/paths.js";
|
|
5
|
+
import { writeFileAtomic } from "../../utils/files/atomic.js";
|
|
5
6
|
import { stripDataUri } from "../../utils/files/image-store.js";
|
|
6
7
|
import { writeJsonAtomic } from "../../utils/files/json.js";
|
|
7
8
|
import {
|
|
@@ -22,15 +23,16 @@ import {
|
|
|
22
23
|
removeSessionProgressCallback,
|
|
23
24
|
type MulmoScript,
|
|
24
25
|
} from "mulmocast";
|
|
25
|
-
import type
|
|
26
|
+
import { mulmoScriptSchema, type MulmoBeat, type MulmoImagePromptMedia } from "@mulmocast/types";
|
|
26
27
|
import { slugify } from "../../utils/slug.js";
|
|
27
28
|
import { resolveWithinRoot } from "../../utils/files/safe.js";
|
|
28
29
|
import { errorMessage } from "../../utils/errors.js";
|
|
29
30
|
import { badRequest, notFound, serverError } from "../../utils/httpError.js";
|
|
30
|
-
import { getOptionalStringQuery } from "../../utils/request.js";
|
|
31
|
+
import { getOptionalStringQuery, getSessionQuery } from "../../utils/request.js";
|
|
31
32
|
import { log } from "../../system/logger/index.js";
|
|
32
33
|
import { validateUpdateBeatBody, validateUpdateScriptBody } from "./mulmoScriptValidate.js";
|
|
33
34
|
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
35
|
+
import { bindRoute } from "../../utils/router.js";
|
|
34
36
|
import { publishGeneration } from "../../events/session-store/index.js";
|
|
35
37
|
import { GENERATION_KINDS } from "../../../src/types/events.js";
|
|
36
38
|
|
|
@@ -65,8 +67,10 @@ function ensureStoriesReal(): string | null {
|
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
interface SaveMulmoScriptBody {
|
|
68
|
-
script
|
|
70
|
+
script?: MulmoScript;
|
|
69
71
|
filename?: string;
|
|
72
|
+
filePath?: string;
|
|
73
|
+
autoGenerateMovie?: boolean;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
interface RenderBeatBody {
|
|
@@ -82,7 +86,9 @@ interface UploadBeatImageBody {
|
|
|
82
86
|
imageData: string; // base64 data URI
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
|
|
89
|
+
interface ErrorResponse {
|
|
90
|
+
error: string;
|
|
91
|
+
}
|
|
86
92
|
|
|
87
93
|
type BeatImageResponse = { image: string | null } | ErrorResponse;
|
|
88
94
|
type BeatAudioResponse = { audio: string | null } | ErrorResponse;
|
|
@@ -98,31 +104,151 @@ interface FilePathQuery {
|
|
|
98
104
|
filePath?: string;
|
|
99
105
|
}
|
|
100
106
|
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
interface ScriptOutcome {
|
|
108
|
+
script: MulmoScript;
|
|
109
|
+
/** Workspace-relative wire form, e.g. "stories/foo.json". */
|
|
110
|
+
wireFilePath: string;
|
|
111
|
+
/** Realpath, used by movie generation and the dedup set. */
|
|
112
|
+
absoluteFilePath: string;
|
|
113
|
+
message: string;
|
|
114
|
+
}
|
|
103
115
|
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
// Unified entry point — save a fresh `script` OR re-display an existing
|
|
117
|
+
// one referenced by `filePath`. Folding both modes into one route lets
|
|
118
|
+
// the agent (MCP) and the GUI dispatcher hit the same endpoint without
|
|
119
|
+
// either side needing to know which mode the user picked. The MCP layer
|
|
120
|
+
// in `server/agent/plugin-names.ts` routes the tool name straight here,
|
|
121
|
+
// so any per-mode logic on the client would be invisible to it.
|
|
122
|
+
bindRoute(router, API_ROUTES.mulmoScript.save, async (req: Request<object, object, SaveMulmoScriptBody>, res: Response) => {
|
|
123
|
+
const { script, filename, filePath, autoGenerateMovie } = req.body ?? {};
|
|
124
|
+
|
|
125
|
+
const hasScript = script !== undefined && script !== null;
|
|
126
|
+
const hasFilePath = typeof filePath === "string" && filePath !== "";
|
|
127
|
+
if (hasScript === hasFilePath) {
|
|
128
|
+
badRequest(
|
|
129
|
+
res,
|
|
130
|
+
hasScript ? "Provide either `script` or `filePath`, not both." : "Provide either `script` (new presentation) or `filePath` (existing presentation).",
|
|
131
|
+
);
|
|
106
132
|
return;
|
|
107
133
|
}
|
|
108
134
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const title = script.title || "untitled";
|
|
112
|
-
const slug = filename ? filename.replace(/\.json$/, "") : slugify(title);
|
|
113
|
-
const fname = `${slug}-${Date.now()}.json`;
|
|
114
|
-
const filePath = path.join(storiesDir, fname);
|
|
135
|
+
const outcome = hasFilePath ? await loadScriptFromDisk(filePath, res) : await saveScriptToDisk(script as MulmoScript, filename, res);
|
|
136
|
+
if (!outcome) return; // helper already wrote a 4xx/5xx response
|
|
115
137
|
|
|
116
|
-
|
|
138
|
+
if (autoGenerateMovie === true) {
|
|
139
|
+
triggerAutoBackgroundMovie(outcome.absoluteFilePath, outcome.wireFilePath, getSessionQuery(req) || undefined);
|
|
140
|
+
}
|
|
117
141
|
|
|
118
142
|
res.json({
|
|
119
|
-
data: { script, filePath:
|
|
120
|
-
message:
|
|
143
|
+
data: { script: outcome.script, filePath: outcome.wireFilePath },
|
|
144
|
+
message: outcome.message,
|
|
121
145
|
instructions: "Display the storyboard to the user.",
|
|
122
146
|
});
|
|
123
147
|
});
|
|
124
148
|
|
|
125
|
-
|
|
149
|
+
async function saveScriptToDisk(script: MulmoScript, filename: string | undefined, res: Response): Promise<ScriptOutcome | null> {
|
|
150
|
+
// Validate against the same schema the reopen path uses so /save and
|
|
151
|
+
// /load agree on what counts as a valid MulmoScript — otherwise
|
|
152
|
+
// /save could persist a script that /load would later refuse, and
|
|
153
|
+
// autoGenerateMovie could kick off against malformed input.
|
|
154
|
+
const validation = mulmoScriptSchema.safeParse(script);
|
|
155
|
+
if (!validation.success) {
|
|
156
|
+
badRequest(res, "script is not a valid MulmoScript");
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const validatedScript = validation.data;
|
|
160
|
+
|
|
161
|
+
mkdirSync(storiesDir, { recursive: true });
|
|
162
|
+
|
|
163
|
+
const title = validatedScript.title || "untitled";
|
|
164
|
+
// slugify drops `/`, `\`, and `..`, so a hostile `filename` like
|
|
165
|
+
// "../../etc/passwd" can never escape storiesDir via the path.join
|
|
166
|
+
// below — defense in depth on top of the wire-side validation.
|
|
167
|
+
const slug = filename ? slugify(filename.replace(/\.json$/i, "")) : slugify(title);
|
|
168
|
+
const fname = `${slug}-${Date.now()}.json`;
|
|
169
|
+
const writePath = path.join(storiesDir, fname);
|
|
170
|
+
|
|
171
|
+
await writeJsonAtomic(writePath, validatedScript);
|
|
172
|
+
|
|
173
|
+
// Realpath-resolve the freshly written file so `inFlightMovies`
|
|
174
|
+
// entries created from this code path collide with ones produced by
|
|
175
|
+
// `resolveStoryPath` (used by reopen / SSE generateMovie / movie-
|
|
176
|
+
// status). Without this, a symlinked storiesDir would let two movie
|
|
177
|
+
// generations for the same physical file run concurrently because
|
|
178
|
+
// their dedup keys differ (path.join vs realpath).
|
|
179
|
+
let absoluteFilePath: string;
|
|
180
|
+
try {
|
|
181
|
+
absoluteFilePath = realpathSync(writePath);
|
|
182
|
+
} catch {
|
|
183
|
+
absoluteFilePath = writePath;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
script: validatedScript,
|
|
188
|
+
wireFilePath: `stories/${fname}`,
|
|
189
|
+
absoluteFilePath,
|
|
190
|
+
message: `Saved MulmoScript to stories/${fname}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function loadScriptFromDisk(filePath: string, res: Response): Promise<ScriptOutcome | null> {
|
|
195
|
+
if (!filePath.toLowerCase().endsWith(".json")) {
|
|
196
|
+
badRequest(res, "filePath must point to a .json file");
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const absoluteFilePath = resolveStoryPath(filePath, res);
|
|
200
|
+
if (!absoluteFilePath) return null; // resolveStoryPath already responded
|
|
201
|
+
|
|
202
|
+
let raw: string;
|
|
203
|
+
try {
|
|
204
|
+
raw = readFileSync(absoluteFilePath, "utf-8");
|
|
205
|
+
} catch (err) {
|
|
206
|
+
serverError(res, errorMessage(err));
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let parsed: unknown;
|
|
211
|
+
try {
|
|
212
|
+
parsed = JSON.parse(raw);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
badRequest(res, `Invalid JSON: ${errorMessage(err)}`);
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const validation = mulmoScriptSchema.safeParse(parsed);
|
|
219
|
+
if (!validation.success) {
|
|
220
|
+
badRequest(res, "File is not a valid MulmoScript");
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Canonicalize via the realpath-resolved absoluteFilePath so wire
|
|
225
|
+
// forms like "bar.json" or "stories/foo/../bar.json" all collapse
|
|
226
|
+
// to the same "stories/<rel>" key — pendingGenerations and movie-
|
|
227
|
+
// status lookups depend on that stability.
|
|
228
|
+
const wireFilePath = toStoryRef(absoluteFilePath);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
script: validation.data,
|
|
232
|
+
wireFilePath,
|
|
233
|
+
absoluteFilePath,
|
|
234
|
+
message: `Loaded MulmoScript from ${wireFilePath}`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Module-level dedup so a foreground SSE call and a fire-and-forget
|
|
239
|
+
// background call can't race on the same script. Keyed by the realpath
|
|
240
|
+
// (absoluteFilePath) so two different wire spellings of the same file
|
|
241
|
+
// still collide. The set is intentionally process-local — a multi-
|
|
242
|
+
// process deployment would need an external lock; that's out of scope.
|
|
243
|
+
const inFlightMovies = new Set<string>();
|
|
244
|
+
|
|
245
|
+
function triggerAutoBackgroundMovie(absoluteFilePath: string, wireFilePath: string, chatSessionId: string | undefined): void {
|
|
246
|
+
if (inFlightMovies.has(absoluteFilePath)) return;
|
|
247
|
+
inFlightMovies.add(absoluteFilePath);
|
|
248
|
+
void runBackgroundMovieGeneration(absoluteFilePath, wireFilePath, chatSessionId);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
bindRoute(router, API_ROUTES.mulmoScript.updateBeat, async (req: Request<object, object, unknown>, res: Response) => {
|
|
126
252
|
const validation = validateUpdateBeatBody(req.body);
|
|
127
253
|
if (!validation.ok) {
|
|
128
254
|
badRequest(res, validation.error);
|
|
@@ -146,7 +272,7 @@ router.post(API_ROUTES.mulmoScript.updateBeat, async (req: Request<object, objec
|
|
|
146
272
|
res.json({ ok: true });
|
|
147
273
|
});
|
|
148
274
|
|
|
149
|
-
router
|
|
275
|
+
bindRoute(router, API_ROUTES.mulmoScript.updateScript, async (req: Request<object, object, unknown>, res: Response) => {
|
|
150
276
|
const validation = validateUpdateScriptBody(req.body);
|
|
151
277
|
if (!validation.ok) {
|
|
152
278
|
badRequest(res, validation.error);
|
|
@@ -161,7 +287,7 @@ router.post(API_ROUTES.mulmoScript.updateScript, async (req: Request<object, obj
|
|
|
161
287
|
res.json({ ok: true });
|
|
162
288
|
});
|
|
163
289
|
|
|
164
|
-
router
|
|
290
|
+
bindRoute(router, API_ROUTES.mulmoScript.beatImage, async (req: Request<object, BeatImageResponse, object, BeatQuery>, res: Response<BeatImageResponse>) => {
|
|
165
291
|
const { filePath, beatIndex: beatIndexStr } = req.query;
|
|
166
292
|
const beatIndex = beatIndexStr !== undefined ? parseInt(beatIndexStr, 10) : undefined;
|
|
167
293
|
|
|
@@ -180,42 +306,46 @@ router.get(API_ROUTES.mulmoScript.beatImage, async (req: Request<object, BeatIma
|
|
|
180
306
|
});
|
|
181
307
|
});
|
|
182
308
|
|
|
183
|
-
|
|
184
|
-
|
|
309
|
+
bindRoute(
|
|
310
|
+
router,
|
|
311
|
+
API_ROUTES.mulmoScript.movieStatus,
|
|
312
|
+
async (req: Request<object, MovieStatusResponse, object, FilePathQuery>, res: Response<MovieStatusResponse>) => {
|
|
313
|
+
const { filePath } = req.query;
|
|
185
314
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const absoluteFilePath = resolveStoryPath(filePath, res);
|
|
192
|
-
if (!absoluteFilePath) return;
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
const context = await buildContext(absoluteFilePath);
|
|
196
|
-
if (!context) {
|
|
197
|
-
res.json({ moviePath: null });
|
|
315
|
+
if (!filePath) {
|
|
316
|
+
badRequest(res, "filePath is required");
|
|
198
317
|
return;
|
|
199
318
|
}
|
|
200
319
|
|
|
201
|
-
const
|
|
202
|
-
if (!
|
|
203
|
-
res.json({ moviePath: null });
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
320
|
+
const absoluteFilePath = resolveStoryPath(filePath, res);
|
|
321
|
+
if (!absoluteFilePath) return;
|
|
206
322
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
323
|
+
try {
|
|
324
|
+
const context = await buildContext(absoluteFilePath);
|
|
325
|
+
if (!context) {
|
|
326
|
+
res.json({ moviePath: null });
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
213
329
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
330
|
+
const outputPath = movieFilePath(context);
|
|
331
|
+
if (!existsSync(outputPath)) {
|
|
332
|
+
res.json({ moviePath: null });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const movieMtime = statSync(outputPath).mtimeMs;
|
|
337
|
+
const sourceMtime = statSync(absoluteFilePath).mtimeMs;
|
|
338
|
+
if (movieMtime < sourceMtime) {
|
|
339
|
+
res.json({ moviePath: null });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
res.json({ moviePath: toStoryRef(outputPath) });
|
|
344
|
+
} catch (err) {
|
|
345
|
+
serverError(res, errorMessage(err));
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
);
|
|
219
349
|
|
|
220
350
|
function fileToDataUri(filePath: string, mimeType: string): string {
|
|
221
351
|
const data = readFileSync(filePath);
|
|
@@ -250,7 +380,7 @@ function resolveStoryPath(filePath: string, res: Response): string | null {
|
|
|
250
380
|
// Strip the optional "stories/" prefix so the remainder is a path
|
|
251
381
|
// relative to storiesReal. Accepts both "stories/foo.json" (the
|
|
252
382
|
// canonical caller convention) and bare "foo.json".
|
|
253
|
-
const STORIES_PREFIX =
|
|
383
|
+
const STORIES_PREFIX = `stories${path.sep}`;
|
|
254
384
|
const relFromStories =
|
|
255
385
|
filePath === "stories" ? "" : filePath.startsWith(STORIES_PREFIX) || filePath.startsWith("stories/") ? filePath.slice("stories/".length) : filePath;
|
|
256
386
|
// resolveWithinRoot enforces both the realpath boundary AND
|
|
@@ -357,7 +487,7 @@ export async function withStoryContext(
|
|
|
357
487
|
}
|
|
358
488
|
}
|
|
359
489
|
|
|
360
|
-
router
|
|
490
|
+
bindRoute(router, API_ROUTES.mulmoScript.beatAudio, async (req: Request<object, BeatAudioResponse, object, BeatQuery>, res: Response<BeatAudioResponse>) => {
|
|
361
491
|
const { filePath, beatIndex: beatIndexStr } = req.query;
|
|
362
492
|
const beatIndex = beatIndexStr !== undefined ? parseInt(beatIndexStr, 10) : undefined;
|
|
363
493
|
|
|
@@ -389,7 +519,8 @@ router.get(API_ROUTES.mulmoScript.beatAudio, async (req: Request<object, BeatAud
|
|
|
389
519
|
);
|
|
390
520
|
});
|
|
391
521
|
|
|
392
|
-
|
|
522
|
+
bindRoute(
|
|
523
|
+
router,
|
|
393
524
|
API_ROUTES.mulmoScript.generateBeatAudio,
|
|
394
525
|
async (
|
|
395
526
|
req: Request<
|
|
@@ -453,7 +584,7 @@ router.post(
|
|
|
453
584
|
},
|
|
454
585
|
);
|
|
455
586
|
|
|
456
|
-
router
|
|
587
|
+
bindRoute(router, API_ROUTES.mulmoScript.renderBeat, async (req: Request<object, object, RenderBeatBody>, res: Response) => {
|
|
457
588
|
const { filePath, beatIndex, force, chatSessionId } = req.body;
|
|
458
589
|
|
|
459
590
|
if (!filePath || beatIndex === undefined) {
|
|
@@ -493,7 +624,68 @@ router.post(API_ROUTES.mulmoScript.renderBeat, async (req: Request<object, objec
|
|
|
493
624
|
}
|
|
494
625
|
});
|
|
495
626
|
|
|
496
|
-
|
|
627
|
+
type MovieGenerationResult = { ok: true; outputPath: string } | { ok: false; error: string };
|
|
628
|
+
|
|
629
|
+
interface MovieProgressEvent {
|
|
630
|
+
kind: "image" | "audio";
|
|
631
|
+
beatIndex: number;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Shared core for both the SSE-streaming `generateMovie` route and the
|
|
635
|
+
// fire-and-forget background path triggered by `autoGenerateMovie`.
|
|
636
|
+
// Builds the mulmo context, runs images→audio→movie, and reports
|
|
637
|
+
// per-beat progress through the supplied callback. Throws on
|
|
638
|
+
// unexpected pipeline errors; returns a structured failure when the
|
|
639
|
+
// pipeline runs to completion but the output file is missing.
|
|
640
|
+
async function runMovieGeneration(absoluteFilePath: string, onProgressEvent: (event: MovieProgressEvent) => void): Promise<MovieGenerationResult> {
|
|
641
|
+
const context = await buildContext(absoluteFilePath);
|
|
642
|
+
if (!context) return { ok: false, error: "Failed to initialize mulmo context" };
|
|
643
|
+
|
|
644
|
+
const idToIndex = new Map<string, number>();
|
|
645
|
+
(context.studio.script.beats as MulmoBeat[]).forEach((beat, index) => {
|
|
646
|
+
const key = beat.id ?? `__index__${index}`;
|
|
647
|
+
idToIndex.set(key, index);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// Known limitation: addSessionProgressCallback is global, so when two
|
|
651
|
+
// movie generations for *different* scripts run concurrently, both
|
|
652
|
+
// closures are invoked for every beat event and rely on idToIndex to
|
|
653
|
+
// filter out the other run's events. That filter is reliable only
|
|
654
|
+
// when each beat carries an explicit `id`. Beats without one fall
|
|
655
|
+
// back to "__index__${index}", and identical fallback ids across
|
|
656
|
+
// scripts collide → progress meant for script A surfaces on script B.
|
|
657
|
+
// Fixing this properly needs mulmocast to attach a per-run identifier
|
|
658
|
+
// to its progress events (or a global serialization gate); tracked
|
|
659
|
+
// separately, out of scope for this PR.
|
|
660
|
+
const onProgress = (event: { kind: string; sessionType: string; id?: string; inSession: boolean }) => {
|
|
661
|
+
if (event.kind !== "beat" || event.inSession || event.id === undefined) return;
|
|
662
|
+
const beatIndex = idToIndex.get(event.id);
|
|
663
|
+
if (beatIndex === undefined) return;
|
|
664
|
+
if (event.sessionType !== "image" && event.sessionType !== "audio") return;
|
|
665
|
+
onProgressEvent({ kind: event.sessionType, beatIndex });
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
addSessionProgressCallback(onProgress);
|
|
669
|
+
try {
|
|
670
|
+
// Order matters: audio() must run before images(). For html_tailwind
|
|
671
|
+
// beats with `animation: true`, mulmocast only emits the per-beat
|
|
672
|
+
// `_animated.mp4` when the beat's duration is already known (see
|
|
673
|
+
// processHtmlTailwindAnimated in mulmocast). Durations are populated
|
|
674
|
+
// by audio(), so running images() first leaves the .mp4 files
|
|
675
|
+
// missing and movie() then fails in validateBeatSource.
|
|
676
|
+
const audioContext = await audio(context);
|
|
677
|
+
const imagesContext = await images(audioContext);
|
|
678
|
+
await movie(imagesContext);
|
|
679
|
+
|
|
680
|
+
const outputPath = movieFilePath(imagesContext);
|
|
681
|
+
if (!existsSync(outputPath)) return { ok: false, error: "Movie was not generated" };
|
|
682
|
+
return { ok: true, outputPath };
|
|
683
|
+
} finally {
|
|
684
|
+
removeSessionProgressCallback(onProgress);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
bindRoute(router, API_ROUTES.mulmoScript.generateMovie, async (req: Request<object, object, { filePath: string; chatSessionId?: string }>, res: Response) => {
|
|
497
689
|
const { filePath, chatSessionId } = req.body;
|
|
498
690
|
|
|
499
691
|
if (!filePath) {
|
|
@@ -504,66 +696,107 @@ router.post(API_ROUTES.mulmoScript.generateMovie, async (req: Request<object, ob
|
|
|
504
696
|
const absoluteFilePath = resolveStoryPath(filePath, res);
|
|
505
697
|
if (!absoluteFilePath) return;
|
|
506
698
|
|
|
699
|
+
if (inFlightMovies.has(absoluteFilePath)) {
|
|
700
|
+
badRequest(res, "Movie generation is already in progress for this script");
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
507
704
|
res.setHeader("Content-Type", "text/event-stream");
|
|
508
705
|
res.setHeader("Cache-Control", "no-cache");
|
|
509
706
|
res.setHeader("Connection", "keep-alive");
|
|
510
707
|
|
|
511
708
|
const send = (data: unknown) => res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
512
709
|
|
|
710
|
+
inFlightMovies.add(absoluteFilePath);
|
|
513
711
|
publishGeneration(chatSessionId, GENERATION_KINDS.movie, filePath, "", false);
|
|
514
712
|
let genError: string | undefined;
|
|
515
713
|
try {
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
genError = "Failed to initialize mulmo context";
|
|
519
|
-
send({ type: "error", message: genError });
|
|
520
|
-
res.end();
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// Build id → beatIndex map for the progress callback
|
|
525
|
-
const idToIndex = new Map<string, number>();
|
|
526
|
-
(context.studio.script.beats as MulmoBeat[]).forEach((beat, index) => {
|
|
527
|
-
const key = beat.id ?? `__index__${index}`;
|
|
528
|
-
idToIndex.set(key, index);
|
|
714
|
+
const result = await runMovieGeneration(absoluteFilePath, (event) => {
|
|
715
|
+
send({ type: `beat_${event.kind}_done`, beatIndex: event.beatIndex });
|
|
529
716
|
});
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if (beatIndex !== undefined) {
|
|
535
|
-
send({ type: `beat_${event.sessionType}_done`, beatIndex });
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
addSessionProgressCallback(onProgress);
|
|
541
|
-
try {
|
|
542
|
-
const imagesContext = await images(context);
|
|
543
|
-
const audioContext = await audio(imagesContext);
|
|
544
|
-
await movie(audioContext);
|
|
545
|
-
|
|
546
|
-
const outputPath = movieFilePath(audioContext);
|
|
547
|
-
if (!existsSync(outputPath)) {
|
|
548
|
-
genError = "Movie was not generated";
|
|
549
|
-
send({ type: "error", message: genError });
|
|
550
|
-
res.end();
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
send({ type: "done", moviePath: toStoryRef(outputPath) });
|
|
555
|
-
} finally {
|
|
556
|
-
removeSessionProgressCallback(onProgress);
|
|
717
|
+
if (!result.ok) {
|
|
718
|
+
genError = result.error;
|
|
719
|
+
send({ type: "error", message: result.error });
|
|
720
|
+
return;
|
|
557
721
|
}
|
|
722
|
+
send({ type: "done", moviePath: toStoryRef(result.outputPath) });
|
|
558
723
|
} catch (err) {
|
|
559
724
|
genError = errorMessage(err);
|
|
560
725
|
send({ type: "error", message: genError });
|
|
561
726
|
} finally {
|
|
727
|
+
inFlightMovies.delete(absoluteFilePath);
|
|
562
728
|
publishGeneration(chatSessionId, GENERATION_KINDS.movie, filePath, "", true, genError);
|
|
563
729
|
res.end();
|
|
564
730
|
}
|
|
565
731
|
});
|
|
566
732
|
|
|
733
|
+
// Detached movie generation. Reports progress through the same session
|
|
734
|
+
// pendingGenerations channel that the View already watches — so a user
|
|
735
|
+
// opening the canvas mid-generation sees spinners, and a user opening
|
|
736
|
+
// it after completion sees the finished movie loaded from disk by the
|
|
737
|
+
// View's normal mount-time path. Errors are persisted to a
|
|
738
|
+
// `<filename>.error.txt` sidecar next to the script (no synchronous
|
|
739
|
+
// client to alert); any stale sidecar from a previous run is cleared on
|
|
740
|
+
// each new attempt. Triggered server-side from the unified save route
|
|
741
|
+
// when the caller passes `autoGenerateMovie: true`.
|
|
742
|
+
async function runBackgroundMovieGeneration(absoluteFilePath: string, wireFilePath: string, chatSessionId: string | undefined): Promise<void> {
|
|
743
|
+
const errorSidecarPath = `${absoluteFilePath}.error.txt`;
|
|
744
|
+
// Clear stale error from a previous failed run before starting; if it
|
|
745
|
+
// doesn't exist that's fine. Catch any unexpected fs errors silently —
|
|
746
|
+
// the worst case is the user sees an out-of-date error file later.
|
|
747
|
+
try {
|
|
748
|
+
unlinkSync(errorSidecarPath);
|
|
749
|
+
} catch {
|
|
750
|
+
// intentional: ENOENT is the common case, others non-fatal
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
publishGeneration(chatSessionId, GENERATION_KINDS.movie, wireFilePath, "", false);
|
|
754
|
+
let genError: string | undefined;
|
|
755
|
+
try {
|
|
756
|
+
const result = await runMovieGeneration(absoluteFilePath, (event) => {
|
|
757
|
+
// Mirror per-beat completions through the session channel so the
|
|
758
|
+
// View's pendingGenerations watcher reloads the asset off disk.
|
|
759
|
+
// We fire start+finish in two ticks — `setImmediate` lets the SSE
|
|
760
|
+
// writer flush the start event before the finish removes the
|
|
761
|
+
// entry, otherwise Vue's batched reactivity could see a net "no
|
|
762
|
+
// change" and skip the reload.
|
|
763
|
+
const eventKind = event.kind === "image" ? GENERATION_KINDS.beatImage : GENERATION_KINDS.beatAudio;
|
|
764
|
+
const key = String(event.beatIndex);
|
|
765
|
+
publishGeneration(chatSessionId, eventKind, wireFilePath, key, false);
|
|
766
|
+
setImmediate(() => publishGeneration(chatSessionId, eventKind, wireFilePath, key, true));
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
if (!result.ok) {
|
|
770
|
+
genError = result.error;
|
|
771
|
+
writeErrorSidecar(errorSidecarPath, result.error);
|
|
772
|
+
log.warn("mulmo-script", "background movie generation failed", { filePath: wireFilePath, error: result.error });
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
log.info("mulmo-script", "background movie generation done", {
|
|
776
|
+
filePath: wireFilePath,
|
|
777
|
+
outputPath: result.outputPath,
|
|
778
|
+
});
|
|
779
|
+
} catch (err) {
|
|
780
|
+
genError = errorMessage(err);
|
|
781
|
+
writeErrorSidecar(errorSidecarPath, genError);
|
|
782
|
+
log.error("mulmo-script", "background movie generation crashed", { filePath: wireFilePath, error: genError });
|
|
783
|
+
} finally {
|
|
784
|
+
inFlightMovies.delete(absoluteFilePath);
|
|
785
|
+
publishGeneration(chatSessionId, GENERATION_KINDS.movie, wireFilePath, "", true, genError);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function writeErrorSidecar(errorSidecarPath: string, message: string): void {
|
|
790
|
+
try {
|
|
791
|
+
writeFileSync(errorSidecarPath, message);
|
|
792
|
+
} catch (writeErr) {
|
|
793
|
+
log.error("mulmo-script", "failed to write error sidecar", {
|
|
794
|
+
errorSidecarPath,
|
|
795
|
+
error: errorMessage(writeErr),
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
567
800
|
interface CharacterImageQuery {
|
|
568
801
|
filePath?: string;
|
|
569
802
|
key?: string;
|
|
@@ -584,7 +817,8 @@ interface UploadCharacterImageBody {
|
|
|
584
817
|
|
|
585
818
|
type CharacterImageResponse = { image: string | null } | ErrorResponse;
|
|
586
819
|
|
|
587
|
-
|
|
820
|
+
bindRoute(
|
|
821
|
+
router,
|
|
588
822
|
API_ROUTES.mulmoScript.characterImage,
|
|
589
823
|
async (req: Request<object, CharacterImageResponse, object, CharacterImageQuery>, res: Response<CharacterImageResponse>) => {
|
|
590
824
|
const { filePath, key } = req.query;
|
|
@@ -605,26 +839,31 @@ router.get(
|
|
|
605
839
|
},
|
|
606
840
|
);
|
|
607
841
|
|
|
608
|
-
|
|
609
|
-
|
|
842
|
+
bindRoute(
|
|
843
|
+
router,
|
|
844
|
+
API_ROUTES.mulmoScript.uploadBeatImage,
|
|
845
|
+
async (req: Request<object, BeatImageResponse, UploadBeatImageBody>, res: Response<BeatImageResponse>) => {
|
|
846
|
+
const { filePath, beatIndex, imageData } = req.body;
|
|
610
847
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
await withStoryContext(res, filePath, {}, async ({ context }) => {
|
|
617
|
-
const { imagePath } = getBeatPngImagePath(context, beatIndex);
|
|
618
|
-
mkdirSync(path.dirname(imagePath), { recursive: true });
|
|
848
|
+
if (!filePath || beatIndex === undefined || !imageData) {
|
|
849
|
+
badRequest(res, "filePath, beatIndex, and imageData are required");
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
619
852
|
|
|
620
|
-
|
|
621
|
-
|
|
853
|
+
await withStoryContext(res, filePath, {}, async ({ context }) => {
|
|
854
|
+
const { imagePath } = getBeatPngImagePath(context, beatIndex);
|
|
855
|
+
// writeFileAtomic creates parent dirs and prevents a half-
|
|
856
|
+
// written PNG from surviving a crash mid-write (#881 v2).
|
|
857
|
+
const base64 = stripDataUri(imageData);
|
|
858
|
+
await writeFileAtomic(imagePath, Buffer.from(base64, "base64"));
|
|
622
859
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}
|
|
860
|
+
res.json({ image: fileToDataUri(imagePath, "image/png") });
|
|
861
|
+
});
|
|
862
|
+
},
|
|
863
|
+
);
|
|
626
864
|
|
|
627
|
-
|
|
865
|
+
bindRoute(
|
|
866
|
+
router,
|
|
628
867
|
API_ROUTES.mulmoScript.renderCharacter,
|
|
629
868
|
async (req: Request<object, CharacterImageResponse, RenderCharacterBody>, res: Response<CharacterImageResponse>) => {
|
|
630
869
|
const { filePath, key, force, chatSessionId } = req.body;
|
|
@@ -675,7 +914,8 @@ router.post(
|
|
|
675
914
|
},
|
|
676
915
|
);
|
|
677
916
|
|
|
678
|
-
|
|
917
|
+
bindRoute(
|
|
918
|
+
router,
|
|
679
919
|
API_ROUTES.mulmoScript.uploadCharacterImage,
|
|
680
920
|
async (req: Request<object, CharacterImageResponse, UploadCharacterImageBody>, res: Response<CharacterImageResponse>) => {
|
|
681
921
|
const { filePath, key, imageData } = req.body;
|
|
@@ -687,17 +927,17 @@ router.post(
|
|
|
687
927
|
|
|
688
928
|
await withStoryContext(res, filePath, {}, async ({ context }) => {
|
|
689
929
|
const imagePath = getReferenceImagePath(context, key, "png");
|
|
690
|
-
|
|
691
|
-
|
|
930
|
+
// writeFileAtomic creates parent dirs and prevents a half-
|
|
931
|
+
// written PNG from surviving a crash mid-write (#881 v2).
|
|
692
932
|
const base64 = stripDataUri(imageData);
|
|
693
|
-
|
|
933
|
+
await writeFileAtomic(imagePath, Buffer.from(base64, "base64"));
|
|
694
934
|
|
|
695
935
|
res.json({ image: fileToDataUri(imagePath, "image/png") });
|
|
696
936
|
});
|
|
697
937
|
},
|
|
698
938
|
);
|
|
699
939
|
|
|
700
|
-
router
|
|
940
|
+
bindRoute(router, API_ROUTES.mulmoScript.downloadMovie, (req: Request, res: Response) => {
|
|
701
941
|
const moviePath = getOptionalStringQuery(req, "moviePath");
|
|
702
942
|
|
|
703
943
|
if (!moviePath) {
|
|
@@ -17,7 +17,7 @@ function formatZodIssues(
|
|
|
17
17
|
// Zod's `$ZodIssue.path` is `PropertyKey[]` (includes `symbol`).
|
|
18
18
|
// Accept the wider type so callers can pass `safeParse().error.issues`
|
|
19
19
|
// directly; stringify any non-string/number segments at format time.
|
|
20
|
-
issues:
|
|
20
|
+
issues: readonly { message: string; path: readonly PropertyKey[] }[],
|
|
21
21
|
): string {
|
|
22
22
|
if (issues.length === 0) return "invalid shape";
|
|
23
23
|
const head = issues
|
|
@@ -111,7 +111,7 @@ router.get(API_ROUTES.news.readState, (_req: Request, res: Response<ReadState |
|
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
router.put(API_ROUTES.news.readState, async (req: Request, res: Response<ReadState | { error: string }>) => {
|
|
114
|
-
const body = req
|
|
114
|
+
const { body } = req;
|
|
115
115
|
if (!isRecord(body) || !Array.isArray(body.readIds)) {
|
|
116
116
|
badRequest(res, "expected { readIds: string[] }");
|
|
117
117
|
return;
|