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
|
@@ -23,12 +23,14 @@
|
|
|
23
23
|
// For a one-shot "fire one of every target kind" run,
|
|
24
24
|
// scripts/dev/fire-sample-notifications.sh drives this endpoint.
|
|
25
25
|
//
|
|
26
|
-
//
|
|
27
|
-
// the
|
|
28
|
-
//
|
|
26
|
+
// PR 4 of feat-encore made `publishNotification()` a thin wrapper
|
|
27
|
+
// over the notifier engine, so this route no longer needs injected
|
|
28
|
+
// pubsub / bridge deps — bridge push fans out via the legacy
|
|
29
|
+
// adapter subscribed to the engine, and `scheduleTestNotification`
|
|
30
|
+
// just calls the wrapper.
|
|
29
31
|
|
|
30
32
|
import { Router, type Request, type Response } from "express";
|
|
31
|
-
import { scheduleTestNotification, type
|
|
33
|
+
import { scheduleTestNotification, type ScheduleNotificationOptions } from "../../events/notifications.js";
|
|
32
34
|
import { log } from "../../system/logger/index.js";
|
|
33
35
|
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
34
36
|
import {
|
|
@@ -37,6 +39,7 @@ import {
|
|
|
37
39
|
NOTIFICATION_VIEWS,
|
|
38
40
|
type NotificationAction,
|
|
39
41
|
type NotificationKind,
|
|
42
|
+
type NotificationTarget,
|
|
40
43
|
} from "../../../src/types/notification.js";
|
|
41
44
|
import { isRecord } from "../../utils/types.js";
|
|
42
45
|
|
|
@@ -45,7 +48,6 @@ interface TestRequestBody {
|
|
|
45
48
|
body?: unknown;
|
|
46
49
|
delaySeconds?: unknown;
|
|
47
50
|
transportId?: unknown;
|
|
48
|
-
chatId?: unknown;
|
|
49
51
|
kind?: unknown;
|
|
50
52
|
action?: unknown;
|
|
51
53
|
}
|
|
@@ -63,22 +65,94 @@ function parseKind(value: unknown): NotificationKind | undefined {
|
|
|
63
65
|
return KIND_SET.has(value) ? (value as NotificationKind) : undefined;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
68
|
+
// `path` / `slug` / `anchor` / etc. arrive as `unknown` from the JSON
|
|
69
|
+
// body. The URL builders in `legacyActionToNavigateTarget` assume each
|
|
70
|
+
// field is `string | undefined`; passing a number through (e.g.
|
|
71
|
+
// `path: 123`) would crash later inside `setTimeout` — after the 202
|
|
72
|
+
// is sent — when `path.split("/")` runs. So validate per-view here:
|
|
73
|
+
// any required field that isn't a non-empty string, or any optional
|
|
74
|
+
// field that is present but isn't a string, drops the whole action.
|
|
75
|
+
// The notification still fires; it just lands without a click target.
|
|
76
|
+
function isOptionalString(value: unknown): boolean {
|
|
77
|
+
return value === undefined || typeof value === "string";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function asOptionalString(value: unknown): string | undefined {
|
|
81
|
+
return typeof value === "string" ? value : undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Per-view validators — split out so the dispatcher stays under the
|
|
85
|
+
// cognitive-complexity threshold. Each validator returns either the
|
|
86
|
+
// typed target or `undefined` for any field that doesn't match the
|
|
87
|
+
// `src/types/notification.ts` discriminated union.
|
|
88
|
+
|
|
89
|
+
function validateChatTarget(value: Record<string, unknown>): NotificationTarget | undefined {
|
|
90
|
+
// sessionId required: chat without it would bounce off the catch-all redirect.
|
|
91
|
+
if (typeof value.sessionId !== "string" || value.sessionId.length === 0) return undefined;
|
|
92
|
+
if (!isOptionalString(value.resultUuid)) return undefined;
|
|
93
|
+
return { view: NOTIFICATION_VIEWS.chat, sessionId: value.sessionId, resultUuid: asOptionalString(value.resultUuid) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validateTodosTarget(value: Record<string, unknown>): NotificationTarget | undefined {
|
|
97
|
+
if (!isOptionalString(value.itemId)) return undefined;
|
|
98
|
+
return { view: NOTIFICATION_VIEWS.todos, itemId: asOptionalString(value.itemId) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function validateAutomationsTarget(value: Record<string, unknown>): NotificationTarget | undefined {
|
|
102
|
+
if (!isOptionalString(value.taskId)) return undefined;
|
|
103
|
+
return { view: NOTIFICATION_VIEWS.automations, taskId: asOptionalString(value.taskId) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function validateSourcesTarget(value: Record<string, unknown>): NotificationTarget | undefined {
|
|
107
|
+
if (!isOptionalString(value.slug)) return undefined;
|
|
108
|
+
return { view: NOTIFICATION_VIEWS.sources, slug: asOptionalString(value.slug) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function validateFilesTarget(value: Record<string, unknown>): NotificationTarget | undefined {
|
|
112
|
+
if (!isOptionalString(value.path)) return undefined;
|
|
113
|
+
return { view: NOTIFICATION_VIEWS.files, path: asOptionalString(value.path) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function validateWikiTarget(value: Record<string, unknown>): NotificationTarget | undefined {
|
|
117
|
+
if (!isOptionalString(value.slug)) return undefined;
|
|
118
|
+
if (!isOptionalString(value.anchor)) return undefined;
|
|
119
|
+
return { view: NOTIFICATION_VIEWS.wiki, slug: asOptionalString(value.slug), anchor: asOptionalString(value.anchor) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validateNavigateTarget(value: unknown): NotificationTarget | undefined {
|
|
123
|
+
if (!isRecord(value)) return undefined;
|
|
124
|
+
if (typeof value.view !== "string" || !VIEW_SET.has(value.view)) return undefined;
|
|
125
|
+
switch (value.view) {
|
|
126
|
+
case NOTIFICATION_VIEWS.chat:
|
|
127
|
+
return validateChatTarget(value);
|
|
128
|
+
case NOTIFICATION_VIEWS.todos:
|
|
129
|
+
return validateTodosTarget(value);
|
|
130
|
+
case NOTIFICATION_VIEWS.calendar:
|
|
131
|
+
return { view: NOTIFICATION_VIEWS.calendar };
|
|
132
|
+
case NOTIFICATION_VIEWS.automations:
|
|
133
|
+
return validateAutomationsTarget(value);
|
|
134
|
+
case NOTIFICATION_VIEWS.sources:
|
|
135
|
+
return validateSourcesTarget(value);
|
|
136
|
+
case NOTIFICATION_VIEWS.files:
|
|
137
|
+
return validateFilesTarget(value);
|
|
138
|
+
case NOTIFICATION_VIEWS.wiki:
|
|
139
|
+
return validateWikiTarget(value);
|
|
140
|
+
default:
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Exported for unit tests. Production callers go through
|
|
146
|
+
* `createNotificationsRouter` and never see this function directly. */
|
|
147
|
+
export function parseAction(value: unknown): NotificationAction | undefined {
|
|
72
148
|
if (!isRecord(value)) return undefined;
|
|
73
149
|
if (value.type === NOTIFICATION_ACTION_TYPES.none) {
|
|
74
150
|
return { type: NOTIFICATION_ACTION_TYPES.none };
|
|
75
151
|
}
|
|
76
152
|
if (value.type !== NOTIFICATION_ACTION_TYPES.navigate) return undefined;
|
|
77
|
-
const target = value.target;
|
|
78
|
-
if (!
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
return { type: NOTIFICATION_ACTION_TYPES.navigate, target: target as NotificationAction extends { target: infer T } ? T : never };
|
|
153
|
+
const target = validateNavigateTarget(value.target);
|
|
154
|
+
if (!target) return undefined;
|
|
155
|
+
return { type: NOTIFICATION_ACTION_TYPES.navigate, target };
|
|
82
156
|
}
|
|
83
157
|
|
|
84
158
|
function parseBody(body: TestRequestBody): ScheduleNotificationOptions {
|
|
@@ -95,9 +169,6 @@ function parseBody(body: TestRequestBody): ScheduleNotificationOptions {
|
|
|
95
169
|
if (typeof body.transportId === "string" && body.transportId.length > 0) {
|
|
96
170
|
opts.transportId = body.transportId;
|
|
97
171
|
}
|
|
98
|
-
if (typeof body.chatId === "string" && body.chatId.length > 0) {
|
|
99
|
-
opts.chatId = body.chatId;
|
|
100
|
-
}
|
|
101
172
|
const kind = parseKind(body.kind);
|
|
102
173
|
if (kind) opts.kind = kind;
|
|
103
174
|
const action = parseAction(body.action);
|
|
@@ -105,16 +176,15 @@ function parseBody(body: TestRequestBody): ScheduleNotificationOptions {
|
|
|
105
176
|
return opts;
|
|
106
177
|
}
|
|
107
178
|
|
|
108
|
-
export function createNotificationsRouter(
|
|
179
|
+
export function createNotificationsRouter(): Router {
|
|
109
180
|
const router = Router();
|
|
110
181
|
router.post(API_ROUTES.notifications.test, (req: Request<object, unknown, TestRequestBody>, res: Response<TestResponse>) => {
|
|
111
182
|
const opts = parseBody(req.body ?? {});
|
|
112
|
-
const scheduled = scheduleTestNotification(opts
|
|
183
|
+
const scheduled = scheduleTestNotification(opts);
|
|
113
184
|
log.info("notifications", "scheduled test push", {
|
|
114
185
|
delaySeconds: scheduled.delaySeconds,
|
|
115
186
|
firesAt: scheduled.firesAt,
|
|
116
187
|
transportId: opts.transportId,
|
|
117
|
-
chatId: opts.chatId,
|
|
118
188
|
});
|
|
119
189
|
res.status(202).json({
|
|
120
190
|
firesAt: scheduled.firesAt,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Single dispatch endpoint for the host UI's bell popup. Body shape:
|
|
2
|
+
// `{ action: "clear" | "cancel" | "list" | "listHistory", ... }`.
|
|
3
|
+
//
|
|
4
|
+
// Trust boundary — `publish` is INTENTIONALLY not an HTTP action.
|
|
5
|
+
// The only legitimate publishers are in-process: plugins go through
|
|
6
|
+
// `runtime.notifier.publish` (auto-binds `pluginPkg` to the calling
|
|
7
|
+
// plugin's pkg name in `server/plugins/runtime.ts:makeScopedNotifier`)
|
|
8
|
+
// and host-internal modules call `engine.publish` directly. Exposing
|
|
9
|
+
// `publish` over HTTP would let any holder of the bearer token
|
|
10
|
+
// publish under any plugin's namespace, since the route layer cannot
|
|
11
|
+
// authenticate which plugin (if any) made the request — bearer auth
|
|
12
|
+
// only proves "the caller is on this machine and knows the token,"
|
|
13
|
+
// not "the caller is plugin X." If a future feature genuinely needs
|
|
14
|
+
// remote publish, it must arrive with caller-identity headers and a
|
|
15
|
+
// per-pkg auth check; until then this surface is host-UI-only.
|
|
16
|
+
//
|
|
17
|
+
// `clear` / `cancel` are deliberately host-scoped (no `pluginPkg`):
|
|
18
|
+
// the bell popup belongs to the host, sees every plugin's entries,
|
|
19
|
+
// and must be able to dismiss any of them. Plugin-scoped clears (the
|
|
20
|
+
// per-plugin isolation property) live on the in-process runtime API
|
|
21
|
+
// only — `engine.clearForPlugin` is not reachable from this route.
|
|
22
|
+
|
|
23
|
+
import { Router, type Request, type Response } from "express";
|
|
24
|
+
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
25
|
+
import { cancel, clear, listAll, listHistory } from "../../notifier/engine.js";
|
|
26
|
+
import type { NotifierEntry, NotifierHistoryEntry } from "../../notifier/types.js";
|
|
27
|
+
import { log } from "../../system/logger/index.js";
|
|
28
|
+
|
|
29
|
+
interface DispatchBody {
|
|
30
|
+
action?: unknown;
|
|
31
|
+
// clear / cancel
|
|
32
|
+
id?: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type DispatchResponse = { ok: true } | { entries: NotifierEntry[] } | { history: NotifierHistoryEntry[] } | { error: string };
|
|
36
|
+
|
|
37
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
38
|
+
return typeof value === "string" && value.length > 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const notifierRouter: Router = Router();
|
|
42
|
+
|
|
43
|
+
notifierRouter.post(API_ROUTES.notifier.dispatch, async (req: Request<object, DispatchResponse, DispatchBody>, res: Response<DispatchResponse>) => {
|
|
44
|
+
const body = req.body ?? {};
|
|
45
|
+
const { action } = body;
|
|
46
|
+
try {
|
|
47
|
+
switch (action) {
|
|
48
|
+
case "clear": {
|
|
49
|
+
if (!isNonEmptyString(body.id)) {
|
|
50
|
+
res.status(400).json({ error: "id required" });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await clear(body.id);
|
|
54
|
+
res.json({ ok: true });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
case "cancel": {
|
|
58
|
+
if (!isNonEmptyString(body.id)) {
|
|
59
|
+
res.status(400).json({ error: "id required" });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await cancel(body.id);
|
|
63
|
+
res.json({ ok: true });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
case "list": {
|
|
67
|
+
const entries = await listAll();
|
|
68
|
+
res.json({ entries });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
case "listHistory": {
|
|
72
|
+
const history = await listHistory();
|
|
73
|
+
res.json({ history });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// `publish` falls through to the default handler. See the
|
|
77
|
+
// trust-boundary note at the top of this file — exposing it
|
|
78
|
+
// over HTTP would let any bearer-token holder spoof an
|
|
79
|
+
// arbitrary `pluginPkg`. Use the in-process runtime API.
|
|
80
|
+
default:
|
|
81
|
+
res.status(400).json({ error: `unknown action: ${typeof action === "string" ? action : "<missing>"}` });
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
// Detailed error stays in the server log for triage; the HTTP
|
|
85
|
+
// response gets an opaque message so a parser-thrown filesystem
|
|
86
|
+
// path / internal stack frame can't leak to the client. Echoing
|
|
87
|
+
// `String(err)` would have given e.g. `Error: ENOENT, open
|
|
88
|
+
// '/Users/<...>/active.json'` to anyone holding the bearer token
|
|
89
|
+
// (CodeRabbit review on PR #1196).
|
|
90
|
+
log.error("notifier-route", "dispatch failed", {
|
|
91
|
+
action: typeof action === "string" ? action : "<unknown>",
|
|
92
|
+
error: String(err),
|
|
93
|
+
});
|
|
94
|
+
res.status(500).json({ error: "internal error" });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export default notifierRouter;
|
package/server/api/routes/pdf.ts
CHANGED
|
@@ -8,8 +8,10 @@ import { badRequest, serverError } from "../../utils/httpError.js";
|
|
|
8
8
|
import { WORKSPACE_DIRS } from "../../workspace/paths.js";
|
|
9
9
|
import { resolveWithinRoot, readBinarySafeSync } from "../../utils/files/safe.js";
|
|
10
10
|
import { resolveWorkspacePath } from "../../utils/files/workspace-io.js";
|
|
11
|
+
import { parseFrontmatter } from "../../utils/markdown/frontmatter.js";
|
|
11
12
|
import { log } from "../../system/logger/index.js";
|
|
12
13
|
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
14
|
+
import { transformResolvableUrlsInHtml } from "../../../src/utils/image/htmlSrcAttrs.js";
|
|
13
15
|
|
|
14
16
|
const router = Router();
|
|
15
17
|
|
|
@@ -58,54 +60,160 @@ const MIME_BY_EXT: Record<string, string> = {
|
|
|
58
60
|
// inside the workspace after symlink resolution.
|
|
59
61
|
const defaultWorkspaceRoot = realpathSync(resolveWorkspacePath(""));
|
|
60
62
|
|
|
63
|
+
export interface InlineImagesOptions {
|
|
64
|
+
/** Workspace root absolute path. Defaults to the lazily-resolved
|
|
65
|
+
* realpath of the configured workspace. */
|
|
66
|
+
workspaceRoot?: string;
|
|
67
|
+
/** Workspace-relative directory the markdown source lives in,
|
|
68
|
+
* used to resolve `../foo.png`-style references. e.g.
|
|
69
|
+
* `"data/wiki/pages"` for Wiki page PDFs. Defaults to
|
|
70
|
+
* `WORKSPACE_DIRS.markdowns` for legacy callers. Inputs are
|
|
71
|
+
* rejected if they're absolute or contain `..` segments — the
|
|
72
|
+
* workspace boundary is enforced anyway by `resolveWithinRoot`,
|
|
73
|
+
* but rejecting up-front gives a clearer log line than a
|
|
74
|
+
* silently-broken image. */
|
|
75
|
+
sourceDir?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Tag scanning + attribute iteration live in the shared helper
|
|
79
|
+
// (`src/utils/image/htmlSrcAttrs.ts`) so the Markdown surface
|
|
80
|
+
// (`rewriteImgSrcAttrsInHtml`) and this PDF surface stay in lockstep.
|
|
81
|
+
// Adding a tag / attribute (Stage B's `<source>` / `<video poster>`
|
|
82
|
+
// / `<audio src>`) updates both surfaces with one diff (#1011 Stage B).
|
|
83
|
+
|
|
84
|
+
function isSafeSourceDir(dir: string): boolean {
|
|
85
|
+
if (!dir) return true;
|
|
86
|
+
if (path.isAbsolute(dir)) return false;
|
|
87
|
+
return !dir.split(/[/\\]/).some((segment) => segment === "..");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Resolve a workspace-rooted-or-relative `src` value to an absolute
|
|
91
|
+
// path on disk, validated to stay inside the workspace root. Returns
|
|
92
|
+
// null on any failure (escape attempt, missing file, malformed path).
|
|
93
|
+
// Logs the reason so the developer can grep when a PDF image is
|
|
94
|
+
// missing.
|
|
95
|
+
function resolveImageAbsPath(src: string, workspaceRoot: string, baseDir: string): string | null {
|
|
96
|
+
// Strip query / fragment before any filesystem-level resolution —
|
|
97
|
+
// a `<img src="foo.png?v=123">` cache-bust must still find the
|
|
98
|
+
// file at `foo.png` on disk. Without this strip, `path.resolve`
|
|
99
|
+
// bakes the `?v=123` into the candidate path and the safe-resolve
|
|
100
|
+
// / readBinarySafeSync reject (codex review iter-2 #1028).
|
|
101
|
+
const pathPart = urlPathname(src);
|
|
102
|
+
// LLM-generated HTML often emits leading-slash workspace-rooted
|
|
103
|
+
// paths like "/artifacts/images/2026/04/foo.png" (web convention).
|
|
104
|
+
// Treat those as workspace-relative; otherwise path.resolve below
|
|
105
|
+
// sees the slash as host-absolute and the safe-resolve rejects.
|
|
106
|
+
const workspaceRooted = pathPart.startsWith("/");
|
|
107
|
+
const resolveBase = workspaceRooted ? workspaceRoot : baseDir;
|
|
108
|
+
const relSrc = workspaceRooted ? pathPart.slice(1) : pathPart;
|
|
109
|
+
const unsafeAbs = path.resolve(resolveBase, relSrc);
|
|
110
|
+
const relToWorkspace = path.relative(workspaceRoot, unsafeAbs);
|
|
111
|
+
if (relToWorkspace.startsWith("..") || path.isAbsolute(relToWorkspace)) {
|
|
112
|
+
log.warn("pdf", "image path escapes workspace", { src });
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const abs = resolveWithinRoot(workspaceRoot, relToWorkspace);
|
|
116
|
+
if (!abs) {
|
|
117
|
+
log.warn("pdf", "image path rejected by safe-resolve", { src });
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return abs;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function loadImageAsDataUri(abs: string): string | null {
|
|
124
|
+
const buf = readBinarySafeSync(abs);
|
|
125
|
+
if (!buf) {
|
|
126
|
+
log.warn("pdf", "could not read image", { abs });
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const ext = path.extname(abs).toLowerCase();
|
|
130
|
+
const mime = MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
131
|
+
return `data:${mime};base64,${buf.toString("base64")}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Video / audio extensions Stage B (#1011) added to the rewriter.
|
|
135
|
+
// In PDF rendering puppeteer can't play them, so inlining a large
|
|
136
|
+
// `.mp4` as base64 just blows up the HTML and times out the page
|
|
137
|
+
// load (`Navigation timeout of 30000 ms exceeded`). The `<video
|
|
138
|
+
// poster>` attribute IS an image and stays inlined — that's the
|
|
139
|
+
// only thing the user actually sees in a PDF anyway. The `<video
|
|
140
|
+
// src>` / `<source src>` (in a `<video>`) / `<audio src>` URL is
|
|
141
|
+
// left as the original relative path; puppeteer's fetch will fail
|
|
142
|
+
// quickly and `networkidle0` still resolves.
|
|
143
|
+
//
|
|
144
|
+
// Anchored at end-of-pathname (callers strip query / fragment first
|
|
145
|
+
// via `urlPathname` below) so a query-string-only mention of the
|
|
146
|
+
// extension doesn't false-positive — e.g. `foo.png?clip.mp4` must
|
|
147
|
+
// inline the PNG, not be treated as an mp4 (codex review iter-1).
|
|
148
|
+
const PDF_SKIP_MEDIA_EXT_RE = /\.(mp4|webm|mov|m4v|ogv|mp3|ogg|oga|wav|m4a|aac)$/i;
|
|
149
|
+
|
|
150
|
+
function urlPathname(url: string): string {
|
|
151
|
+
const queryStart = url.indexOf("?");
|
|
152
|
+
const fragStart = url.indexOf("#");
|
|
153
|
+
const limit = [queryStart, fragStart].filter((idx) => idx >= 0).reduce((min, idx) => Math.min(min, idx), url.length);
|
|
154
|
+
return url.slice(0, limit);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Whether a URL points at a video / audio file that should NOT be
|
|
158
|
+
* base64-inlined into a PDF. Exported for direct unit testing —
|
|
159
|
+
* filesystem-level resolver checks (`resolveImageAbsPath`) can mask
|
|
160
|
+
* the regex's behaviour because a missing file also returns null,
|
|
161
|
+
* so the in-line callback's output looks identical for "skip" vs
|
|
162
|
+
* "resolve-failed". */
|
|
163
|
+
export function shouldSkipMediaForPdf(url: string): boolean {
|
|
164
|
+
return PDF_SKIP_MEDIA_EXT_RE.test(urlPathname(url));
|
|
165
|
+
}
|
|
166
|
+
|
|
61
167
|
/**
|
|
62
|
-
* Inline local images as base64 data URIs so Puppeteer can render
|
|
63
|
-
*
|
|
64
|
-
*
|
|
168
|
+
* Inline local images as base64 data URIs so Puppeteer can render
|
|
169
|
+
* them. Resolves URL-bearing attributes (currently `<img src>`,
|
|
170
|
+
* `<source src>`, `<video poster|src>`, `<audio src>`) against
|
|
171
|
+
* `sourceDir` (workspace-relative); for example, a Wiki page
|
|
172
|
+
* (`data/wiki/pages/X.md`) referencing
|
|
173
|
+
* `../../../artifacts/images/foo.png` resolves to
|
|
174
|
+
* `artifacts/images/foo.png`.
|
|
175
|
+
*
|
|
176
|
+
* Handles double-quoted, single-quoted, and unquoted values. Skips
|
|
177
|
+
* `data:` URIs and `http(s)` URLs. Refuses values that escape the
|
|
178
|
+
* workspace root after resolution — the workspace boundary is
|
|
179
|
+
* enforced by `resolveWithinRoot`, regardless of `sourceDir`.
|
|
65
180
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
181
|
+
* Tag + attribute coverage is shared with the browser markdown
|
|
182
|
+
* surface (`rewriteImgSrcAttrsInHtml`) via `RESOLVABLE_TAG_ATTRS` in
|
|
183
|
+
* `src/utils/image/htmlSrcAttrs.ts`.
|
|
69
184
|
*/
|
|
70
|
-
export function inlineImages(html: string,
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (!buf) {
|
|
103
|
-
log.warn("pdf", "could not read image", { abs });
|
|
104
|
-
return _match;
|
|
105
|
-
}
|
|
106
|
-
const ext = path.extname(abs).toLowerCase();
|
|
107
|
-
const mime = MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
108
|
-
return `${before}data:${mime};base64,${buf.toString("base64")}${after}`;
|
|
185
|
+
export function inlineImages(html: string, options: InlineImagesOptions = {}): string {
|
|
186
|
+
const workspaceRoot = options.workspaceRoot ?? defaultWorkspaceRoot;
|
|
187
|
+
// Defensive type guard: a malformed request body could send
|
|
188
|
+
// `baseDir: null` / `baseDir: 42` / etc. Coerce anything non-
|
|
189
|
+
// string to undefined so the legacy default kicks in instead of
|
|
190
|
+
// `path.join` throwing on a non-string.
|
|
191
|
+
const requestedDir = typeof options.sourceDir === "string" ? options.sourceDir : undefined;
|
|
192
|
+
// Distinguish "explicitly empty" (= workspace root, e.g. a top-
|
|
193
|
+
// level `README.md`) from "absent" (= legacy `markdowns/` default
|
|
194
|
+
// for chat callers). Without this, the `||` collapse would route
|
|
195
|
+
// every workspace-root file through the legacy default.
|
|
196
|
+
const hasRequestedDir = requestedDir !== undefined;
|
|
197
|
+
const dirIsSafe = !hasRequestedDir || isSafeSourceDir(requestedDir);
|
|
198
|
+
if (hasRequestedDir && !dirIsSafe) {
|
|
199
|
+
log.warn("pdf", "rejecting unsafe sourceDir, falling back to default", { sourceDir: requestedDir });
|
|
200
|
+
}
|
|
201
|
+
const sourceDir = dirIsSafe && hasRequestedDir ? requestedDir : WORKSPACE_DIRS.markdowns;
|
|
202
|
+
const baseDir = path.join(workspaceRoot, sourceDir);
|
|
203
|
+
return transformResolvableUrlsInHtml(html, (url) => {
|
|
204
|
+
// Narrow to exact `http://` / `https://` prefixes so a relative
|
|
205
|
+
// path like `http-assets/logo.png` isn't misclassified as
|
|
206
|
+
// external (CR follow-up on #1023).
|
|
207
|
+
if (url.startsWith("data:") || url.startsWith("http://") || url.startsWith("https://")) return null;
|
|
208
|
+
// Skip media (mp4 / mp3 / webm / ...) — see PDF_SKIP_MEDIA_EXT_RE
|
|
209
|
+
// comment. `<video poster="x.png">` still inlines because the
|
|
210
|
+
// poster value's pathname ends in an image extension. The
|
|
211
|
+
// pathname slice means `foo.png?cacheBust=clip.mp4` correctly
|
|
212
|
+
// routes through the PNG inline path (codex iter-1).
|
|
213
|
+
if (shouldSkipMediaForPdf(url)) return null;
|
|
214
|
+
const abs = resolveImageAbsPath(url, workspaceRoot, baseDir);
|
|
215
|
+
if (!abs) return null;
|
|
216
|
+
return loadImageAsDataUri(abs);
|
|
109
217
|
});
|
|
110
218
|
}
|
|
111
219
|
|
|
@@ -147,10 +255,41 @@ interface PdfMarkdownBody {
|
|
|
147
255
|
markdown: string;
|
|
148
256
|
filename?: string;
|
|
149
257
|
format?: "Letter" | "A4";
|
|
258
|
+
/** Workspace-relative source directory of the markdown (e.g.
|
|
259
|
+
* `"data/wiki/pages"` for Wiki pages). Used to resolve relative
|
|
260
|
+
* `<img>` references against the right base. Omit for the legacy
|
|
261
|
+
* `markdowns/` default. Validated server-side; absolute paths
|
|
262
|
+
* and `..` segments are rejected. */
|
|
263
|
+
baseDir?: string;
|
|
264
|
+
/** When true, strip a leading YAML frontmatter envelope before
|
|
265
|
+
* rendering so `title:` / `tags:` etc don't appear as plain text
|
|
266
|
+
* on page 1 of the PDF. Wiki pages use this. Markdown / Text
|
|
267
|
+
* Response callers omit (default false) so a chat-generated
|
|
268
|
+
* document that *literally* starts with `---\n…\n---\n` is
|
|
269
|
+
* preserved verbatim. */
|
|
270
|
+
stripFrontmatter?: boolean;
|
|
150
271
|
}
|
|
151
272
|
|
|
152
273
|
router.post(API_ROUTES.pdf.markdown, async (req: Request<object, unknown, PdfMarkdownBody>, res: Response) => {
|
|
153
|
-
const {
|
|
274
|
+
const { body } = req;
|
|
275
|
+
// Express only sets `req.body` after `express.json()` parses a JSON
|
|
276
|
+
// payload. A client that sends raw JSON `null`, an array, or omits
|
|
277
|
+
// the body entirely would land here with body = null / [] / undefined,
|
|
278
|
+
// and the per-field guards below would throw on the first property
|
|
279
|
+
// dereference. Bail out cleanly with a 400 instead.
|
|
280
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
281
|
+
badRequest(res, "request body must be a JSON object");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// Defensive type guards: a malformed JSON body could send
|
|
285
|
+
// `baseDir: null` / `stripFrontmatter: "yes"` / etc. Coerce
|
|
286
|
+
// anything off-shape to its safe default rather than letting a
|
|
287
|
+
// downstream `path.join` / boolean check throw.
|
|
288
|
+
const markdown = typeof body.markdown === "string" ? body.markdown : "";
|
|
289
|
+
const filename = typeof body.filename === "string" ? body.filename : "document.pdf";
|
|
290
|
+
const format: "Letter" | "A4" = body.format === "A4" ? "A4" : "Letter";
|
|
291
|
+
const baseDir = typeof body.baseDir === "string" ? body.baseDir : undefined;
|
|
292
|
+
const stripFrontmatter = body.stripFrontmatter === true;
|
|
154
293
|
|
|
155
294
|
if (!markdown) {
|
|
156
295
|
badRequest(res, "markdown is required");
|
|
@@ -158,8 +297,9 @@ router.post(API_ROUTES.pdf.markdown, async (req: Request<object, unknown, PdfMar
|
|
|
158
297
|
}
|
|
159
298
|
|
|
160
299
|
try {
|
|
161
|
-
log.info("pdf", "markdown", { filename, length: markdown.length });
|
|
162
|
-
const
|
|
300
|
+
log.info("pdf", "markdown", { filename, length: markdown.length, baseDir, stripFrontmatter });
|
|
301
|
+
const source = stripFrontmatter ? parseFrontmatter(markdown).body : markdown;
|
|
302
|
+
const html = inlineImages(await marked.parse(source), { sourceDir: baseDir });
|
|
163
303
|
const buffer = await renderPdf(wrapHtml(html, MARKDOWN_CSS), format);
|
|
164
304
|
sendPdf(res, buffer, filename);
|
|
165
305
|
} catch (err) {
|