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,14 +1,14 @@
|
|
|
1
|
-
import { mkdir, readFile, realpath
|
|
1
|
+
import { mkdir, readFile, realpath } from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { WORKSPACE_DIRS, WORKSPACE_PATHS } from "../../workspace/paths.js";
|
|
4
4
|
import { shortId } from "../id.js";
|
|
5
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
5
6
|
import { yearMonthUtc } from "./naming.js";
|
|
6
7
|
import { resolveWithinRoot } from "./safe.js";
|
|
7
8
|
|
|
8
9
|
const IMAGES_DIR = WORKSPACE_PATHS.images;
|
|
9
10
|
|
|
10
|
-
//
|
|
11
|
-
// its root argument to be a realpath so symlinks are handled correctly.
|
|
11
|
+
// resolveWithinRoot needs a realpath as its root so symlinks resolve correctly.
|
|
12
12
|
let imagesDirReal: string | null = null;
|
|
13
13
|
|
|
14
14
|
async function ensureImagesDir(): Promise<string> {
|
|
@@ -18,13 +18,9 @@ async function ensureImagesDir(): Promise<string> {
|
|
|
18
18
|
return imagesDirReal;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
// into an absolute path that is guaranteed to be inside the images
|
|
23
|
-
// directory. Throws on traversal attempts or non-existent files.
|
|
21
|
+
// Throws on traversal. Strips a leading "images/" so callers can pass either the stored form or bare filename.
|
|
24
22
|
async function safeResolve(relativePath: string): Promise<string> {
|
|
25
23
|
const root = await ensureImagesDir();
|
|
26
|
-
// Strip the leading "images/" prefix so the caller can pass either
|
|
27
|
-
// "images/abc.png" (the stored form) or just "abc.png".
|
|
28
24
|
const name = relativePath.replace(new RegExp(`^${WORKSPACE_DIRS.images}/`), "");
|
|
29
25
|
const result = resolveWithinRoot(root, name);
|
|
30
26
|
if (!result) {
|
|
@@ -33,40 +29,42 @@ async function safeResolve(relativePath: string): Promise<string> {
|
|
|
33
29
|
return result;
|
|
34
30
|
}
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
* land under `images/YYYY/MM/` (UTC) so the dir doesn't accumulate
|
|
38
|
-
* unbounded — see #764. Returns the workspace-relative path. */
|
|
32
|
+
// #764 sharded under images/YYYY/MM/ (UTC). Buffer pass-through avoids re-encoding the PNG bytes.
|
|
39
33
|
export async function saveImage(base64Data: string): Promise<string> {
|
|
40
34
|
await ensureImagesDir();
|
|
41
35
|
const partition = yearMonthUtc();
|
|
42
|
-
const parentAbs = path.join(IMAGES_DIR, partition);
|
|
43
|
-
await mkdir(parentAbs, { recursive: true });
|
|
44
36
|
const filename = `${shortId()}.png`;
|
|
45
|
-
|
|
37
|
+
const absPath = path.join(IMAGES_DIR, partition, filename);
|
|
38
|
+
await writeFileAtomic(absPath, Buffer.from(base64Data, "base64"));
|
|
46
39
|
return path.posix.join(WORKSPACE_DIRS.images, partition, filename);
|
|
47
40
|
}
|
|
48
41
|
|
|
49
|
-
/** Overwrite an existing image file. The relativePath must start with "images/". */
|
|
50
42
|
export async function overwriteImage(relativePath: string, base64Data: string): Promise<void> {
|
|
51
43
|
const absPath = await safeResolve(relativePath);
|
|
52
|
-
await
|
|
44
|
+
await writeFileAtomic(absPath, Buffer.from(base64Data, "base64"));
|
|
53
45
|
}
|
|
54
46
|
|
|
55
|
-
/** Read an image file and return raw base64 (no data URI prefix). */
|
|
56
47
|
export async function loadImageBase64(relativePath: string): Promise<string> {
|
|
57
48
|
const absPath = await safeResolve(relativePath);
|
|
58
49
|
const buf = await readFile(absPath);
|
|
59
50
|
return buf.toString("base64");
|
|
60
51
|
}
|
|
61
52
|
|
|
62
|
-
/** Convert a data URI to raw base64. */
|
|
63
53
|
export function stripDataUri(dataUri: string): string {
|
|
64
54
|
return dataUri.replace(/^data:image\/[^;]+;base64,/, "");
|
|
65
55
|
}
|
|
66
56
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
57
|
+
// Reject `.` / `..` segments split on either `/` or `\` so a
|
|
58
|
+
// traversal-shaped value can't slip past the prefix/suffix gate
|
|
59
|
+
// (Codex review on PR #1084 follow-up to #1052).
|
|
60
|
+
function hasTraversalSegment(value: string): boolean {
|
|
61
|
+
return value.split(/[/\\]/).some((segment) => segment === ".." || segment === ".");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Accepts arbitrary depth so saveImage's images/YYYY/MM/abc.png still validates.
|
|
70
65
|
export function isImagePath(value: string): boolean {
|
|
71
|
-
|
|
66
|
+
if (!value.startsWith(`${WORKSPACE_DIRS.images}/`)) return false;
|
|
67
|
+
if (!value.endsWith(".png")) return false;
|
|
68
|
+
if (hasTraversalSegment(value)) return false;
|
|
69
|
+
return true;
|
|
72
70
|
}
|
|
@@ -1,21 +1,11 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// This barrel re-exports every public helper so call sites can do:
|
|
4
|
-
//
|
|
5
|
-
// import { writeFileAtomic, readWorkspaceText } from "../utils/files/index.js";
|
|
6
|
-
//
|
|
7
|
-
// Grouped by concern:
|
|
8
|
-
//
|
|
9
|
-
// atomic.ts — write-then-rename primitives
|
|
10
|
-
// safe.ts — ENOENT-swallowing wrappers (stat, readdir, readText, resolveWithinRoot)
|
|
11
|
-
// json.ts — JSON read/write (sync legacy + async atomic)
|
|
12
|
-
// workspace-io.ts — workspace-aware helpers (path resolve + I/O in one call)
|
|
1
|
+
// #366: barrel for workspace file I/O. atomic = write-rename, safe = ENOENT-swallowing, json = sync read + atomic
|
|
2
|
+
// write, workspace-io = path resolve + I/O in one call.
|
|
13
3
|
|
|
14
4
|
export { writeFileAtomic, writeFileAtomicSync, type WriteAtomicOptions } from "./atomic.js";
|
|
15
5
|
|
|
16
6
|
export { isEnoent, readTextSafeSync, statSafe, statSafeAsync, readDirSafe, readDirSafeAsync, readTextOrNull, resolveWithinRoot } from "./safe.js";
|
|
17
7
|
|
|
18
|
-
export { loadJsonFile,
|
|
8
|
+
export { loadJsonFile, writeJsonAtomic, readJsonOrNull } from "./json.js";
|
|
19
9
|
|
|
20
10
|
export {
|
|
21
11
|
resolveWorkspacePath,
|
|
@@ -36,9 +26,9 @@ export {
|
|
|
36
26
|
ensureDirUnder,
|
|
37
27
|
} from "./workspace-io.js";
|
|
38
28
|
|
|
39
|
-
// ── Domain I/O ──────────────────────────────────────────────────
|
|
40
29
|
export * from "./session-io.js";
|
|
41
|
-
|
|
30
|
+
// todos-io.js removed (#1145) — todo persistence moved into the
|
|
31
|
+
// `@mulmoclaude/todo-plugin` workspace package.
|
|
42
32
|
export * from "./scheduler-io.js";
|
|
43
33
|
export * from "./html-io.js";
|
|
44
34
|
export * from "./reference-dirs-io.js";
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
// Domain I/O: workspace journal (summaries)
|
|
2
|
-
// conversations/summaries/_state.json — journal state
|
|
3
|
-
// conversations/summaries/_index.md — browseable index
|
|
4
|
-
// conversations/summaries/daily/YYYY/MM/DD.md — daily summaries
|
|
5
|
-
// conversations/summaries/topics/<slug>.md — topic files
|
|
6
|
-
// conversations/summaries/archive/topics/ — archived topics
|
|
7
|
-
//
|
|
8
|
-
// All functions take optional `root` for test DI.
|
|
9
|
-
// Path helpers (summariesRoot, dailyPathFor, topicPathFor) live in
|
|
10
|
-
// journal/paths.ts — this module wraps them with I/O.
|
|
11
|
-
|
|
12
1
|
import path from "node:path";
|
|
13
2
|
import fsp from "node:fs/promises";
|
|
14
3
|
import { workspacePath } from "../../workspace/paths.js";
|
|
@@ -21,8 +10,6 @@ import { statSync } from "node:fs";
|
|
|
21
10
|
|
|
22
11
|
const root = (rootOverride?: string) => rootOverride ?? workspacePath;
|
|
23
12
|
|
|
24
|
-
// ── State ───────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
13
|
export function journalStateExists(rootOverride?: string): boolean {
|
|
27
14
|
const filePath = path.join(summariesRoot(root(rootOverride)), STATE_FILE);
|
|
28
15
|
try {
|
|
@@ -49,15 +36,11 @@ export async function writeJournalState(state: unknown, rootOverride?: string):
|
|
|
49
36
|
await writeFileAtomic(filePath, JSON.stringify(state, null, 2));
|
|
50
37
|
}
|
|
51
38
|
|
|
52
|
-
// ── Index ───────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
39
|
export async function writeJournalIndex(markdown: string, rootOverride?: string): Promise<void> {
|
|
55
40
|
const filePath = path.join(summariesRoot(root(rootOverride)), INDEX_FILE);
|
|
56
41
|
await writeFileAtomic(filePath, markdown);
|
|
57
42
|
}
|
|
58
43
|
|
|
59
|
-
// ── Daily summaries ─────────────────────────────────────────────
|
|
60
|
-
|
|
61
44
|
export async function readDailySummary(date: string, rootOverride?: string): Promise<string | null> {
|
|
62
45
|
try {
|
|
63
46
|
return await fsp.readFile(dailyPathFor(root(rootOverride), date), "utf-8");
|
|
@@ -74,15 +57,12 @@ export async function writeDailySummary(date: string, content: string, rootOverr
|
|
|
74
57
|
await writeFileAtomic(dailyPathFor(root(rootOverride), date), content);
|
|
75
58
|
}
|
|
76
59
|
|
|
77
|
-
// ── Topics ──────────────────────────────────────────────────────
|
|
78
|
-
|
|
79
60
|
export async function readTopicFile(slug: string, rootOverride?: string): Promise<string | null> {
|
|
80
61
|
try {
|
|
81
62
|
return await fsp.readFile(topicPathFor(root(rootOverride), slug), "utf-8");
|
|
82
63
|
} catch (err) {
|
|
83
64
|
if (isEnoent(err)) return null;
|
|
84
|
-
// EACCES/EPERM must propagate — swallowing them would
|
|
85
|
-
// appendOrCreateTopic to clobber an unreadable file.
|
|
65
|
+
// EACCES/EPERM must propagate — swallowing them would let appendOrCreateTopic clobber an unreadable file.
|
|
86
66
|
throw err;
|
|
87
67
|
}
|
|
88
68
|
}
|
|
@@ -91,7 +71,6 @@ export async function writeTopicFile(slug: string, content: string, rootOverride
|
|
|
91
71
|
await writeFileAtomic(topicPathFor(root(rootOverride), slug), content);
|
|
92
72
|
}
|
|
93
73
|
|
|
94
|
-
/** Append content to an existing topic, or create a new file. */
|
|
95
74
|
export async function appendOrCreateTopic(slug: string, content: string, rootOverride?: string): Promise<"created" | "updated"> {
|
|
96
75
|
const existing = await readTopicFile(slug, rootOverride);
|
|
97
76
|
if (existing === null) {
|
|
@@ -102,7 +81,6 @@ export async function appendOrCreateTopic(slug: string, content: string, rootOve
|
|
|
102
81
|
return "updated";
|
|
103
82
|
}
|
|
104
83
|
|
|
105
|
-
/** List topic slugs (filenames without .md). */
|
|
106
84
|
export async function listTopicSlugs(rootOverride?: string): Promise<string[]> {
|
|
107
85
|
const dir = path.join(summariesRoot(root(rootOverride)), TOPICS_DIR);
|
|
108
86
|
try {
|
|
@@ -115,7 +93,6 @@ export async function listTopicSlugs(rootOverride?: string): Promise<string[]> {
|
|
|
115
93
|
}
|
|
116
94
|
}
|
|
117
95
|
|
|
118
|
-
/** Read all topic files at once. Returns slug→content map. */
|
|
119
96
|
export async function readAllTopicFiles(rootOverride?: string): Promise<Map<string, string>> {
|
|
120
97
|
const dir = path.join(summariesRoot(root(rootOverride)), TOPICS_DIR);
|
|
121
98
|
const out = new Map<string, string>();
|
|
@@ -137,8 +114,7 @@ export async function readAllTopicFiles(rootOverride?: string): Promise<Map<stri
|
|
|
137
114
|
return out;
|
|
138
115
|
}
|
|
139
116
|
|
|
140
|
-
|
|
141
|
-
* source doesn't exist or the move fails. */
|
|
117
|
+
// Returns false if the source doesn't exist or the move fails.
|
|
142
118
|
export async function archiveTopic(slug: string, rootOverride?: string): Promise<boolean> {
|
|
143
119
|
const src = topicPathFor(root(rootOverride), slug);
|
|
144
120
|
const dst = path.join(summariesRoot(root(rootOverride)), ARCHIVE_DIR, TOPICS_DIR, `${slug}.md`);
|
|
@@ -154,14 +130,17 @@ export async function archiveTopic(slug: string, rootOverride?: string): Promise
|
|
|
154
130
|
}
|
|
155
131
|
}
|
|
156
132
|
|
|
157
|
-
// ── Daily file listing ──────────────────────────────────────────
|
|
158
|
-
|
|
159
133
|
export interface DailyFileEntry {
|
|
160
134
|
year: string;
|
|
161
135
|
month: string;
|
|
162
136
|
day: string;
|
|
163
137
|
}
|
|
164
138
|
|
|
139
|
+
const YEAR_RE = /^\d{4}$/;
|
|
140
|
+
const MONTH_RE = /^\d{2}$/;
|
|
141
|
+
const isYearDir = (name: string) => YEAR_RE.test(name);
|
|
142
|
+
const isMonthDir = (name: string) => MONTH_RE.test(name);
|
|
143
|
+
|
|
165
144
|
export async function listDailyFiles(rootOverride?: string): Promise<DailyFileEntry[]> {
|
|
166
145
|
const dailyRoot = path.join(summariesRoot(root(rootOverride)), DAILY_DIR);
|
|
167
146
|
const years = await safeReaddir(dailyRoot);
|
|
@@ -173,11 +152,6 @@ export async function listDailyFiles(rootOverride?: string): Promise<DailyFileEn
|
|
|
173
152
|
return out;
|
|
174
153
|
}
|
|
175
154
|
|
|
176
|
-
const YEAR_RE = /^\d{4}$/;
|
|
177
|
-
const MONTH_RE = /^\d{2}$/;
|
|
178
|
-
const isYearDir = (name: string) => YEAR_RE.test(name);
|
|
179
|
-
const isMonthDir = (name: string) => MONTH_RE.test(name);
|
|
180
|
-
|
|
181
155
|
async function listDaysForYear(dailyRoot: string, year: string): Promise<DailyFileEntry[]> {
|
|
182
156
|
const months = await safeReaddir(path.join(dailyRoot, year));
|
|
183
157
|
const out: DailyFileEntry[] = [];
|
|
@@ -200,8 +174,6 @@ async function safeReaddir(dir: string): Promise<string[]> {
|
|
|
200
174
|
}
|
|
201
175
|
}
|
|
202
176
|
|
|
203
|
-
// ── Archived topic count ────────────────────────────────────────
|
|
204
|
-
|
|
205
177
|
export async function countArchivedTopics(rootOverride?: string): Promise<number> {
|
|
206
178
|
const dir = path.join(summariesRoot(root(rootOverride)), ARCHIVE_DIR, TOPICS_DIR);
|
|
207
179
|
try {
|
|
@@ -1,22 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// Moved from server/utils/file.ts (issue #366 Phase 1). The old
|
|
4
|
-
// file re-exports these for backwards compat.
|
|
5
|
-
|
|
6
|
-
import { mkdirSync, promises, readFileSync, writeFileSync } from "fs";
|
|
7
|
-
import path from "path";
|
|
1
|
+
import { promises, readFileSync } from "fs";
|
|
8
2
|
import { writeFileAtomic } from "./atomic.js";
|
|
9
3
|
import { isEnoent } from "./safe.js";
|
|
10
4
|
import { log } from "../../system/logger/index.js";
|
|
11
5
|
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Read and parse a JSON file synchronously. Returns `defaultValue`
|
|
16
|
-
* on ENOENT (file not yet created) or JSON corruption (logs the
|
|
17
|
-
* error but doesn't crash — user data files must not take down the
|
|
18
|
-
* server). Rethrows unexpected errors (EACCES, EPERM).
|
|
19
|
-
*/
|
|
6
|
+
// Returns defaultValue on ENOENT or parse failure (user data files must not take down the server); rethrows EACCES/EPERM.
|
|
20
7
|
export function loadJsonFile<T>(filePath: string, defaultValue: T): T {
|
|
21
8
|
let raw: string;
|
|
22
9
|
try {
|
|
@@ -40,24 +27,10 @@ export function loadJsonFile<T>(filePath: string, defaultValue: T): T {
|
|
|
40
27
|
}
|
|
41
28
|
}
|
|
42
29
|
|
|
43
|
-
export function saveJsonFile(filePath: string, data: unknown): void {
|
|
44
|
-
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
45
|
-
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ── Async ───────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* JSON-pretty-print `data` and write atomically.
|
|
52
|
-
*/
|
|
53
30
|
export async function writeJsonAtomic(filePath: string, data: unknown, opts: Parameters<typeof writeFileAtomic>[2] = {}): Promise<void> {
|
|
54
31
|
await writeFileAtomic(filePath, JSON.stringify(data, null, 2), opts);
|
|
55
32
|
}
|
|
56
33
|
|
|
57
|
-
/**
|
|
58
|
-
* Read a JSON file and parse it. Returns null if the file is missing,
|
|
59
|
-
* unreadable, or malformed.
|
|
60
|
-
*/
|
|
61
34
|
export async function readJsonOrNull<T>(filePath: string): Promise<T | null> {
|
|
62
35
|
try {
|
|
63
36
|
const content = await promises.readFile(filePath, "utf-8");
|
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// Lives under `server/utils/files/` because it sits at the seam
|
|
4
|
-
// between markdown content and on-disk image artifacts; route
|
|
5
|
-
// handlers (e.g. `presentDocument`) just hand off the markdown.
|
|
6
|
-
//
|
|
7
|
-
// Logging policy: every image generation emits start / ok / failed /
|
|
8
|
-
// no-data lines, and every batch emits a tally. Per the timeout-policy
|
|
9
|
-
// comment in `server/agent/mcp-server.ts`, generative-AI work MUST be
|
|
10
|
-
// observable — silent partial failures were the exact failure mode
|
|
11
|
-
// that hid the 10 s bridge-timeout bug.
|
|
1
|
+
// Per the timeout-policy comment in server/agent/mcp-server.ts, generative-AI work MUST be observable — silent
|
|
2
|
+
// partial failures hid the 10s bridge-timeout bug. Every image emits start/ok/failed/no-data + per-batch tally.
|
|
12
3
|
import { generateGeminiImageFromPrompt, isGeminiAvailable } from "../gemini.js";
|
|
13
4
|
import { errorMessage } from "../errors.js";
|
|
14
5
|
import { promptMeta } from "../promptMeta.js";
|
|
@@ -21,9 +12,7 @@ const LOG_PREFIX = "present-document";
|
|
|
21
12
|
async function generateImageFile(prompt: string, index: number, total: number): Promise<string | null> {
|
|
22
13
|
if (!isGeminiAvailable()) return null;
|
|
23
14
|
const startedAt = Date.now();
|
|
24
|
-
// Prompt is user-controlled and may contain
|
|
25
|
-
// credentials, so we log a `{ length, sha256 }` fingerprint instead
|
|
26
|
-
// of a raw prefix. See `server/utils/promptMeta.ts`.
|
|
15
|
+
// Prompt is user-controlled and may contain credentials/PII; promptMeta logs {length, sha256} instead of raw bytes.
|
|
27
16
|
const meta = promptMeta(prompt);
|
|
28
17
|
log.info(LOG_PREFIX, "image gen start", {
|
|
29
18
|
index,
|
|
@@ -71,33 +60,13 @@ function logBatchTally(results: PlaceholderResult[], total: number, batchStarted
|
|
|
71
60
|
}
|
|
72
61
|
|
|
73
62
|
export function buildReplacement(prompt: string, url: string | null): string {
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// independent of where the markdown file itself lands on disk.
|
|
77
|
-
// Since #764, documents shard under `artifacts/documents/YYYY/MM/`,
|
|
78
|
-
// and `rewriteMarkdownImageRefs` (front-end) treats a leading "/"
|
|
79
|
-
// as "rooted at workspace" — so a markdown reference like
|
|
80
|
-
// "" works regardless of the document's
|
|
81
|
-
// depth. A relative path computed against the unsharded root would
|
|
82
|
-
// instead be off by two directory levels and 404 in the canvas.
|
|
63
|
+
// Workspace-rooted "/…" so the ref resolves the same regardless of document depth (#764 sharded documents under
|
|
64
|
+
// artifacts/documents/YYYY/MM/; a relative path would be off by two directory levels).
|
|
83
65
|
if (url) return ``;
|
|
84
|
-
// No image:
|
|
85
|
-
// operator can still see what *would* have been generated.
|
|
66
|
+
// No image: leave the alt text as an italic marker so the operator can see what *would* have been generated.
|
|
86
67
|
return `*🖼️ Image: ${prompt}*`;
|
|
87
68
|
}
|
|
88
69
|
|
|
89
|
-
/**
|
|
90
|
-
* Replace every `` placeholder
|
|
91
|
-
* in the input markdown with a real Gemini-generated image.
|
|
92
|
-
*
|
|
93
|
-
* - When `GEMINI_API_KEY` is unset, every placeholder degrades to an
|
|
94
|
-
* italic text marker (`*🖼️ Image: <alt>*`) so the document still
|
|
95
|
-
* renders without broken image refs.
|
|
96
|
-
* - On per-image failure, the same fallback applies for that one
|
|
97
|
-
* placeholder. Other placeholders proceed independently.
|
|
98
|
-
* - All generation runs in parallel via `Promise.all` — typical 9-image
|
|
99
|
-
* batches finish in 15-25 s rather than per-image-serial.
|
|
100
|
-
*/
|
|
101
70
|
export async function fillMarkdownImagePlaceholders(markdown: string): Promise<string> {
|
|
102
71
|
const matches = [...markdown.matchAll(IMAGE_PLACEHOLDER)];
|
|
103
72
|
if (matches.length === 0) return markdown;
|
|
@@ -1,44 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { workspacePath } from "../../workspace/workspace.js";
|
|
4
4
|
import { WORKSPACE_DIRS } from "../../workspace/paths.js";
|
|
5
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
5
6
|
import { buildArtifactPathRandom } from "./naming.js";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
* Save markdown content as a file. Returns the workspace-relative path.
|
|
9
|
-
* `prefix` is slugified; a random id is always appended to prevent
|
|
10
|
-
* collisions between concurrent writers sharing the same prefix.
|
|
11
|
-
*
|
|
12
|
-
* `buildArtifactPathRandom` now injects a `YYYY/MM` partition (#764),
|
|
13
|
-
* so this function ensures the partition directory exists before
|
|
14
|
-
* writing — `writeFile` doesn't create missing parents on its own.
|
|
15
|
-
*/
|
|
8
|
+
// Random-id suffix prevents collisions between concurrent writers sharing a prefix; #764 sharded under YYYY/MM.
|
|
16
9
|
export async function saveMarkdown(content: string, prefix: string): Promise<string> {
|
|
17
10
|
const relPath = buildArtifactPathRandom(WORKSPACE_DIRS.markdowns, prefix, ".md", "document");
|
|
18
11
|
const absPath = path.join(workspacePath, relPath);
|
|
19
|
-
await
|
|
20
|
-
await writeFile(absPath, content, "utf-8");
|
|
12
|
+
await writeFileAtomic(absPath, content);
|
|
21
13
|
return relPath;
|
|
22
14
|
}
|
|
23
15
|
|
|
24
|
-
/** Read a markdown file and return its content. */
|
|
25
16
|
export async function loadMarkdown(relativePath: string): Promise<string> {
|
|
26
17
|
const absPath = path.join(workspacePath, relativePath);
|
|
27
18
|
return readFile(absPath, "utf-8");
|
|
28
19
|
}
|
|
29
20
|
|
|
30
|
-
/** Overwrite an existing markdown file. */
|
|
31
21
|
export async function overwriteMarkdown(relativePath: string, content: string): Promise<void> {
|
|
32
22
|
const absPath = path.join(workspacePath, relativePath);
|
|
33
|
-
await
|
|
23
|
+
await writeFileAtomic(absPath, content);
|
|
34
24
|
}
|
|
35
25
|
|
|
36
|
-
|
|
37
|
-
* Rejects traversal attempts like `artifacts/documents/../outside.md`
|
|
38
|
-
* so callers can rely on prefix+suffix alone. Mirrors the
|
|
39
|
-
* `isSpreadsheetPath` policy. The server-side `path.join` in
|
|
40
|
-
* `overwriteMarkdown` does NOT normalize traversal on its own, so
|
|
41
|
-
* this gate is the primary defence — keep it strict. */
|
|
26
|
+
// Strict — overwriteMarkdown's path.join doesn't normalize traversal, so this gate is the primary defence.
|
|
42
27
|
export function isMarkdownPath(value: string): boolean {
|
|
43
28
|
if (!value.startsWith(`${WORKSPACE_DIRS.markdowns}/`)) return false;
|
|
44
29
|
if (!value.endsWith(".md")) return false;
|
|
@@ -1,59 +1,23 @@
|
|
|
1
|
-
// Workspace file naming conventions.
|
|
2
|
-
//
|
|
3
|
-
// Centralizes the `slug-${Date.now()}.ext` pattern used across
|
|
4
|
-
// multiple plugins (chart, presentHtml, markdown, spreadsheet, image).
|
|
5
|
-
// Call sites pass a human title + extension; this module handles
|
|
6
|
-
// slugification and timestamp suffixing.
|
|
7
|
-
|
|
8
1
|
import path from "node:path";
|
|
9
2
|
import { shortId } from "../id.js";
|
|
10
3
|
import { slugify } from "../slug.js";
|
|
11
4
|
|
|
12
|
-
|
|
13
|
-
* UTC-based `YYYY/MM` partition segment for new artifacts (#764).
|
|
14
|
-
* Keeps each artifact directory from accumulating a flat list of
|
|
15
|
-
* thousands of files. UTC is used (rather than local time) so a
|
|
16
|
-
* workspace synced across machines / timezones still groups files
|
|
17
|
-
* into the same bucket.
|
|
18
|
-
*
|
|
19
|
-
* Exported for unit tests and callers that need the partition without
|
|
20
|
-
* also generating a filename (e.g. saveImage / saveSpreadsheet).
|
|
21
|
-
*/
|
|
5
|
+
// #764 partitioning. UTC (not local) so a workspace synced across timezones still groups into the same bucket.
|
|
22
6
|
export function yearMonthUtc(now: Date = new Date()): string {
|
|
23
7
|
const year = now.getUTCFullYear();
|
|
24
8
|
const month = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
25
9
|
return `${year}/${month}`;
|
|
26
10
|
}
|
|
27
11
|
|
|
28
|
-
/**
|
|
29
|
-
* Build a workspace-relative path for a new artifact file.
|
|
30
|
-
*
|
|
31
|
-
* @param dir Workspace-relative directory (e.g. WORKSPACE_DIRS.charts)
|
|
32
|
-
* @param title Human-readable title (slugified for the filename)
|
|
33
|
-
* @param ext File extension with leading dot (e.g. ".html", ".json")
|
|
34
|
-
* @param fallbackSlug Slug to use when title is empty/undefined
|
|
35
|
-
* @returns Workspace-relative path like "artifacts/charts/2026/04/sales-1776135210389.chart.json"
|
|
36
|
-
*/
|
|
37
12
|
export function buildArtifactPath(dir: string, title: string | undefined, ext: string, fallbackSlug = "file"): string {
|
|
38
13
|
const slug = title ? slugify(title) || fallbackSlug : fallbackSlug;
|
|
39
14
|
const fname = `${slug}-${Date.now()}${ext}`;
|
|
40
15
|
return path.posix.join(dir, yearMonthUtc(), fname);
|
|
41
16
|
}
|
|
42
17
|
|
|
43
|
-
|
|
44
|
-
* Like `buildArtifactPath`, but appends a random hex id instead of a
|
|
45
|
-
* timestamp. Use when multiple concurrent writers may share the same
|
|
46
|
-
* prefix within the same millisecond (e.g. LLM-supplied `filenamePrefix`
|
|
47
|
-
* on the `presentDocument` route).
|
|
48
|
-
*
|
|
49
|
-
* @param dir Workspace-relative directory
|
|
50
|
-
* @param prefix Human-readable prefix (slugified via `slugify`)
|
|
51
|
-
* @param ext File extension with leading dot
|
|
52
|
-
* @param fallbackSlug Slug to use when the sanitized prefix is empty
|
|
53
|
-
*/
|
|
18
|
+
// shortId variant for concurrent writers that share a prefix within the same millisecond (presentDocument route).
|
|
54
19
|
export function buildArtifactPathRandom(dir: string, prefix: string, ext: string, fallbackSlug = "file"): string {
|
|
55
|
-
// Pass fallbackSlug as slugify's default so it overrides slugify's
|
|
56
|
-
// built-in "page" default when `prefix` sanitizes to empty.
|
|
20
|
+
// Pass fallbackSlug as slugify's default so it overrides slugify's built-in "page" when `prefix` sanitizes to empty.
|
|
57
21
|
const slug = slugify(prefix, fallbackSlug);
|
|
58
22
|
const fname = `${slug}-${shortId()}${ext}`;
|
|
59
23
|
return path.posix.join(dir, yearMonthUtc(), fname);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Install ledger I/O for runtime-loaded plugins (#1043 C-2).
|
|
2
|
+
//
|
|
3
|
+
// The ledger is `~/mulmoclaude/plugins/plugins.json`, listing every
|
|
4
|
+
// plugin the user has installed via the install CLI / web UI. Each
|
|
5
|
+
// entry pairs the npm package id with the on-disk tgz filename; the
|
|
6
|
+
// loader replays this at boot to know what to extract from
|
|
7
|
+
// `plugins/` into `plugins/.cache/<name>/<version>/`.
|
|
8
|
+
//
|
|
9
|
+
// Truncating or deleting this file removes nothing on disk but
|
|
10
|
+
// "uninstalls" all runtime plugins on the next boot — the tgz files
|
|
11
|
+
// in `plugins/` and the cache mirror are GC'd on the following start.
|
|
12
|
+
// Editing it by hand is a supported recovery path.
|
|
13
|
+
//
|
|
14
|
+
// Reads tolerate missing / malformed JSON (returns []), so a half-
|
|
15
|
+
// written ledger never bricks server boot. Writes go through the
|
|
16
|
+
// atomic helper, so a crashed install can't leave a corrupt file.
|
|
17
|
+
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
19
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { loadJsonFile, writeJsonAtomic } from "./json.js";
|
|
22
|
+
import { WORKSPACE_PATHS } from "../../workspace/paths.js";
|
|
23
|
+
|
|
24
|
+
export interface LedgerEntry {
|
|
25
|
+
/** npm package name, e.g. `@gui-chat-plugin/weather`. */
|
|
26
|
+
name: string;
|
|
27
|
+
/** Semver string from the tgz's `package.json`, e.g. `0.1.0`. */
|
|
28
|
+
version: string;
|
|
29
|
+
/** Basename of the tgz inside `plugins/`. Joined with
|
|
30
|
+
* `WORKSPACE_PATHS.plugins` to read. */
|
|
31
|
+
tgz: string;
|
|
32
|
+
/** ISO 8601 timestamp of the install. */
|
|
33
|
+
installedAt: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isLedgerEntry = (value: unknown): value is LedgerEntry => {
|
|
37
|
+
if (typeof value !== "object" || value === null) return false;
|
|
38
|
+
const obj = value as Record<string, unknown>;
|
|
39
|
+
return typeof obj.name === "string" && typeof obj.version === "string" && typeof obj.tgz === "string" && typeof obj.installedAt === "string";
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const sanitiseLedger = (raw: unknown): LedgerEntry[] => {
|
|
43
|
+
if (!Array.isArray(raw)) return [];
|
|
44
|
+
return raw.filter(isLedgerEntry);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function readLedger(): LedgerEntry[] {
|
|
48
|
+
const raw = loadJsonFile<unknown>(WORKSPACE_PATHS.pluginsLedger, []);
|
|
49
|
+
return sanitiseLedger(raw);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function writeLedger(entries: readonly LedgerEntry[]): Promise<void> {
|
|
53
|
+
const dir = path.dirname(WORKSPACE_PATHS.pluginsLedger);
|
|
54
|
+
if (!existsSync(dir)) {
|
|
55
|
+
await mkdir(dir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
await writeJsonAtomic(WORKSPACE_PATHS.pluginsLedger, [...entries]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Read a runtime-plugin asset (extracted under
|
|
61
|
+
* `plugins/.cache/<name>/<version>/`) and return its bytes plus
|
|
62
|
+
* the inferred Content-Type. The route handler in
|
|
63
|
+
* `runtime-plugin.ts` was previously calling `fs.readFile` directly
|
|
64
|
+
* — per `CLAUDE.md` route handlers must go through a domain helper,
|
|
65
|
+
* so the lookup table + read live here (#1077 review). */
|
|
66
|
+
export interface PluginAsset {
|
|
67
|
+
data: Buffer;
|
|
68
|
+
contentType: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function readPluginAsset(absPath: string): Promise<PluginAsset> {
|
|
72
|
+
const data = await readFile(absPath);
|
|
73
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
74
|
+
return { data, contentType: pluginAssetContentType(ext) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Lookup table over a switch — flat data structure stays under
|
|
78
|
+
// `sonarjs/cognitive-complexity` while keeping the per-extension
|
|
79
|
+
// mapping easy to scan.
|
|
80
|
+
const PLUGIN_ASSET_CONTENT_TYPES: Readonly<Record<string, string>> = {
|
|
81
|
+
".js": "application/javascript; charset=utf-8",
|
|
82
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
83
|
+
".cjs": "application/javascript; charset=utf-8",
|
|
84
|
+
".css": "text/css; charset=utf-8",
|
|
85
|
+
".json": "application/json; charset=utf-8",
|
|
86
|
+
".map": "application/json; charset=utf-8",
|
|
87
|
+
".html": "text/html; charset=utf-8",
|
|
88
|
+
".svg": "image/svg+xml",
|
|
89
|
+
".png": "image/png",
|
|
90
|
+
".jpg": "image/jpeg",
|
|
91
|
+
".jpeg": "image/jpeg",
|
|
92
|
+
".gif": "image/gif",
|
|
93
|
+
".webp": "image/webp",
|
|
94
|
+
".woff": "font/woff",
|
|
95
|
+
".woff2": "font/woff2",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export function pluginAssetContentType(ext: string): string {
|
|
99
|
+
return PLUGIN_ASSET_CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
100
|
+
}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
// Domain I/O for reference directories.
|
|
2
|
-
//
|
|
3
|
-
// Reads/writes config/reference-dirs.json and checks host paths.
|
|
4
|
-
// All fs access is funneled through shared helpers so path changes
|
|
5
|
-
// propagate from a single constant.
|
|
6
|
-
|
|
7
1
|
import { mkdirSync, statSync } from "fs";
|
|
8
2
|
import path from "path";
|
|
9
3
|
import { WORKSPACE_DIRS, workspacePath } from "../../workspace/paths.js";
|
|
@@ -17,7 +11,7 @@ function configPath(root: string): string {
|
|
|
17
11
|
return path.join(root, WORKSPACE_DIRS.configs, CONFIG_FILE_NAME);
|
|
18
12
|
}
|
|
19
13
|
|
|
20
|
-
|
|
14
|
+
// Returns [] on missing/corrupt file.
|
|
21
15
|
export function readReferenceDirsJson(root?: string): unknown[] {
|
|
22
16
|
const filePath = configPath(root ?? workspacePath);
|
|
23
17
|
const parsed = loadJsonFile<unknown>(filePath, []);
|
|
@@ -28,14 +22,12 @@ export function readReferenceDirsJson(root?: string): unknown[] {
|
|
|
28
22
|
return parsed;
|
|
29
23
|
}
|
|
30
24
|
|
|
31
|
-
/** Write reference-dirs.json atomically. Creates config/ if needed. */
|
|
32
25
|
export function writeReferenceDirsJson(entries: readonly unknown[], root?: string): void {
|
|
33
26
|
const filePath = configPath(root ?? workspacePath);
|
|
34
27
|
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
35
28
|
writeFileAtomicSync(filePath, JSON.stringify(entries, null, 2));
|
|
36
29
|
}
|
|
37
30
|
|
|
38
|
-
/** Check whether a host path exists and is a directory. */
|
|
39
31
|
export function isExistingDirectory(hostPath: string): boolean {
|
|
40
32
|
try {
|
|
41
33
|
return statSync(hostPath).isDirectory();
|