mulmoclaude 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile.sandbox +100 -0
- package/README.md +17 -4
- package/bin/mulmoclaude.js +46 -15
- package/bin/prepare-dist.js +18 -2
- package/client/assets/chunk-CernVdwh.js +1 -0
- package/client/assets/chunk-D8eiyYIV-C1eAZMzz.js +1 -0
- package/client/assets/html2canvas-CDGcmOD3-BbPeutDg.js +5 -0
- package/client/assets/index-BbgSjFQ8.js +4968 -0
- package/client/assets/index-ECD0lgIv.css +2 -0
- package/client/assets/{index.es-D4YyL_Dg-BgT6a3Nd.js → index.es-DqtpmBm8-DJdTPdnc.js} +5 -5
- package/client/assets/material-symbols-outlined-BLDfUw-_.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-6WYa8hAs.js +1 -0
- package/client/assets/runtime-vue-BVUzgYGA.js +1 -0
- package/client/assets/typeof-DBp4T-Ny-C2xoZtcz.js +1 -0
- package/client/assets/vue-1e_vz2LW.js +1 -0
- package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +4 -0
- package/client/index.html +33 -2
- package/package.json +20 -18
- package/sandbox-entrypoint.sh +106 -0
- package/server/accounting/accountNormalize.ts +32 -0
- package/server/accounting/defaultAccounts.ts +87 -0
- package/server/accounting/eventPublisher.ts +51 -0
- package/server/accounting/journal.ts +252 -0
- package/server/accounting/openingBalances.ts +114 -0
- package/server/accounting/report.ts +237 -0
- package/server/accounting/service.ts +718 -0
- package/server/accounting/snapshotCache.ts +333 -0
- package/server/accounting/timeSeries.ts +265 -0
- package/server/accounting/types.ts +148 -0
- package/server/agent/activeTools.ts +128 -0
- package/server/agent/attachmentConverter.ts +10 -5
- package/server/agent/backend/claude-code.ts +8 -2
- package/server/agent/backend/types.ts +1 -1
- package/server/agent/config.ts +101 -31
- package/server/agent/index.ts +45 -33
- package/server/agent/mcp-server.ts +146 -69
- package/server/agent/mcp-tools/index.ts +1 -5
- package/server/agent/mcp-tools/notify.ts +2 -22
- package/server/agent/mcp-tools/x.ts +0 -4
- package/server/agent/mcpHealth.ts +168 -0
- package/server/agent/plugin-names.ts +20 -77
- package/server/agent/prompt.ts +259 -51
- package/server/agent/resumeFailover.ts +1 -1
- package/server/agent/stream.ts +0 -1
- package/server/api/auth/bearerAuth.ts +5 -5
- package/server/api/csrfGuard.ts +1 -1
- package/server/api/routes/accounting.ts +366 -0
- package/server/api/routes/agent.ts +509 -46
- package/server/api/routes/attachment.ts +104 -0
- package/server/api/routes/chart.ts +2 -1
- package/server/api/routes/config.ts +12 -12
- package/server/api/routes/files.ts +105 -48
- package/server/api/routes/image.ts +70 -25
- package/server/api/routes/journal.ts +35 -0
- package/server/api/routes/mulmo-script.ts +358 -118
- package/server/api/routes/mulmoScriptValidate.ts +1 -1
- package/server/api/routes/news.ts +1 -1
- package/server/api/routes/notifications.ts +92 -22
- package/server/api/routes/notifier.ts +98 -0
- package/server/api/routes/pdf.ts +188 -48
- package/server/api/routes/plugins.ts +34 -14
- package/server/api/routes/presentHtml.ts +58 -3
- package/server/api/routes/roles.ts +1 -8
- package/server/api/routes/runtime-plugin.ts +224 -0
- package/server/api/routes/scheduler.ts +7 -5
- package/server/api/routes/schedulerHandlers.ts +1 -1
- package/server/api/routes/schedulerTasks.ts +8 -7
- package/server/api/routes/sessions.ts +234 -121
- package/server/api/routes/skills.ts +56 -51
- package/server/api/routes/sources.ts +52 -45
- package/server/api/routes/translation.ts +44 -0
- package/server/api/routes/wiki/frontmatter.ts +13 -65
- package/server/api/routes/wiki/history.ts +261 -0
- package/server/api/routes/wiki/pageIndex.ts +1 -1
- package/server/api/routes/wiki.ts +50 -26
- package/server/events/file-change.ts +83 -0
- package/server/events/notifications.ts +247 -91
- package/server/events/pub-sub/index.ts +1 -1
- package/server/events/relay-client.ts +5 -5
- package/server/events/scheduler-adapter.ts +2 -2
- package/server/events/session-store/index.ts +110 -22
- package/server/events/task-manager/index.ts +10 -9
- package/server/index.ts +509 -33
- package/server/notifier/engine.ts +419 -0
- package/server/notifier/legacy-adapters.ts +76 -0
- package/server/notifier/runtime-api.ts +74 -0
- package/server/notifier/store.ts +70 -0
- package/server/notifier/types.ts +121 -0
- package/server/plugins/dev-loader.ts +171 -0
- package/server/plugins/dev-watcher.ts +150 -0
- package/server/plugins/diagnostics.ts +188 -0
- package/server/plugins/preset-list.ts +52 -0
- package/server/plugins/preset-loader.ts +112 -0
- package/server/plugins/runtime-chat-api.ts +38 -0
- package/server/plugins/runtime-loader.ts +430 -0
- package/server/plugins/runtime-registry.ts +112 -0
- package/server/plugins/runtime-tasks-api.ts +50 -0
- package/server/plugins/runtime.ts +378 -0
- package/server/services/translation/cache.ts +72 -0
- package/server/services/translation/index.ts +106 -0
- package/server/services/translation/llm.ts +140 -0
- package/server/services/translation/types.ts +35 -0
- package/server/system/credentials.ts +13 -2
- package/server/system/env.ts +6 -1
- package/server/system/logger/formatters.ts +46 -4
- package/server/system/logger/index.ts +4 -4
- package/server/system/logger/sinks.ts +26 -5
- package/server/system/logger/types.ts +2 -2
- package/server/utils/dev-plugin-args.d.mts +11 -0
- package/server/utils/dev-plugin-args.mjs +43 -0
- package/server/utils/errors.ts +13 -4
- package/server/utils/files/accounting-io.ts +295 -0
- package/server/utils/files/atomic.ts +17 -49
- package/server/utils/files/attachment-store.ts +182 -0
- package/server/utils/files/html-io.ts +1 -7
- package/server/utils/files/html-store.ts +19 -0
- package/server/utils/files/image-store.ts +20 -22
- package/server/utils/files/index.ts +5 -15
- package/server/utils/files/journal-io.ts +7 -35
- package/server/utils/files/json.ts +2 -29
- package/server/utils/files/markdown-image-fill.ts +6 -37
- package/server/utils/files/markdown-store.ts +6 -21
- package/server/utils/files/naming.ts +3 -39
- package/server/utils/files/plugins-io.ts +100 -0
- package/server/utils/files/reference-dirs-io.ts +1 -9
- package/server/utils/files/roles-io.ts +2 -10
- package/server/utils/files/safe.ts +17 -19
- package/server/utils/files/scheduler-io.ts +1 -7
- package/server/utils/files/scheduler-overrides-io.ts +3 -12
- package/server/utils/files/session-io.ts +21 -30
- package/server/utils/files/spreadsheet-store.ts +9 -22
- package/server/utils/files/translation-io.ts +46 -0
- package/server/utils/files/user-tasks-io.ts +1 -7
- package/server/utils/files/workspace-io.ts +3 -79
- package/server/utils/gemini.ts +33 -11
- package/server/utils/html/htmlArtifactSplicer.ts +41 -0
- package/server/utils/markdown/frontmatter.ts +112 -0
- package/server/utils/regex.ts +56 -0
- package/server/utils/router.ts +41 -0
- package/server/utils/slug.ts +5 -3
- package/server/utils/time.ts +12 -0
- package/server/workspace/chat-index/indexer.ts +15 -2
- package/server/workspace/chat-index/summarizer.ts +1 -1
- package/server/workspace/custom-dirs.ts +1 -1
- package/server/workspace/helps/gemini.md +1 -1
- package/server/workspace/helps/guide.md +61 -0
- package/server/workspace/helps/index.md +4 -0
- package/server/workspace/helps/presenthtml.md +80 -0
- package/server/workspace/helps/sandbox.md +7 -0
- package/server/workspace/helps/storyteller.md +101 -0
- package/server/workspace/helps/telegram.md +1 -0
- package/server/workspace/helps/wiki.md +9 -7
- package/server/workspace/journal/archivist-cli.ts +7 -33
- package/server/workspace/journal/archivist-schemas.ts +5 -43
- package/server/workspace/journal/dailyPass.ts +34 -187
- package/server/workspace/journal/diff.ts +3 -28
- package/server/workspace/journal/index.ts +10 -81
- package/server/workspace/journal/indexFile.ts +3 -24
- package/server/workspace/journal/latestDaily.ts +51 -0
- package/server/workspace/journal/memoryExtractor.ts +4 -20
- package/server/workspace/journal/optimizationPass.ts +4 -21
- package/server/workspace/journal/paths.ts +4 -23
- package/server/workspace/journal/state.ts +6 -29
- package/server/workspace/memory/io.ts +213 -0
- package/server/workspace/memory/llm-classifier.ts +158 -0
- package/server/workspace/memory/migrate.ts +263 -0
- package/server/workspace/memory/run.ts +84 -0
- package/server/workspace/memory/topic-cluster.ts +218 -0
- package/server/workspace/memory/topic-detect.ts +67 -0
- package/server/workspace/memory/topic-index-hook.ts +128 -0
- package/server/workspace/memory/topic-io.ts +180 -0
- package/server/workspace/memory/topic-migrate.ts +248 -0
- package/server/workspace/memory/topic-run.ts +172 -0
- package/server/workspace/memory/topic-swap.ts +135 -0
- package/server/workspace/memory/topic-types.ts +142 -0
- package/server/workspace/memory/types.ts +83 -0
- package/server/workspace/news/reader.ts +4 -5
- package/server/workspace/paths.ts +124 -47
- package/server/workspace/roles.ts +2 -11
- package/server/workspace/skills/parser.ts +38 -55
- package/server/workspace/skills/user-tasks.ts +1 -2
- package/server/workspace/skills-preset/mc-library/SKILL.md +188 -0
- package/server/workspace/skills-preset.ts +196 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +13 -11
- package/server/workspace/sources/fetchers/index.ts +1 -1
- package/server/workspace/sources/fetchers/rssParser.ts +1 -1
- package/server/workspace/sources/pipeline/index.ts +2 -2
- package/server/workspace/sources/pipeline/notify.ts +3 -3
- package/server/workspace/sources/pipeline/write.ts +2 -2
- package/server/workspace/sources/registry.ts +39 -61
- package/server/workspace/sources/robots.ts +1 -1
- package/server/workspace/tool-trace/classify.ts +2 -1
- package/server/workspace/tool-trace/index.ts +1 -1
- package/server/workspace/tool-trace/writeSearch.ts +6 -1
- package/server/workspace/wiki-backlinks/index.ts +19 -7
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +1 -0
- package/server/workspace/wiki-history/hook/snapshot.mjs +98 -0
- package/server/workspace/wiki-history/hook/snapshot.ts +135 -0
- package/server/workspace/wiki-history/provision.ts +181 -0
- package/server/workspace/wiki-pages/io.ts +217 -0
- package/server/workspace/wiki-pages/snapshot.ts +380 -0
- package/server/workspace/workspace.ts +75 -13
- package/src/App.vue +115 -40
- package/src/_runtime/protocol-vue.ts +21 -0
- package/src/_runtime/vue.ts +22 -0
- package/src/components/ChatInput.vue +14 -10
- package/src/components/CopyChatButton.vue +76 -0
- package/src/components/FileContentRenderer.vue +67 -14
- package/src/components/FileTree.vue +2 -2
- package/src/components/FilesView.vue +17 -1
- package/src/components/NewsView.vue +16 -2
- package/src/components/NotificationBell.vue +320 -93
- package/src/components/PageChatComposer.vue +5 -4
- package/src/components/PluginLauncher.vue +42 -6
- package/src/components/PluginScopedRoot.vue +87 -0
- package/src/components/RoleSelector.vue +12 -1
- package/src/components/RolesView.vue +562 -0
- package/src/components/SentAttachmentChip.vue +102 -0
- package/src/components/SessionHistoryPanel.vue +109 -20
- package/src/components/SessionRoleIcon.vue +7 -4
- package/src/components/SessionSidebar.vue +20 -7
- package/src/components/SessionTabBar.vue +1 -1
- package/src/components/SettingsMcpTab.vue +4 -4
- package/src/components/SettingsModal.vue +2 -0
- package/src/components/SidebarHeader.vue +16 -5
- package/src/components/SourcesManager.vue +23 -9
- package/src/components/SourcesView.vue +1 -1
- package/src/components/StackView.vue +102 -6
- package/src/components/SuggestionsPanel.vue +105 -16
- package/src/components/SystemFileBanner.vue +1 -1
- package/src/components/TodoExplorer.vue +4 -5
- package/src/components/todo/TodoAddDialog.vue +2 -3
- package/src/components/todo/TodoEditDialog.vue +1 -2
- package/src/components/todo/TodoEditPanel.vue +2 -3
- package/src/components/todo/TodoKanbanView.vue +8 -5
- package/src/components/todo/TodoListView.vue +3 -5
- package/src/components/todo/TodoTableView.vue +7 -5
- package/src/composables/useAccountingChannel.ts +58 -0
- package/src/composables/useActiveSession.ts +4 -25
- package/src/composables/useAppApi.ts +6 -44
- package/src/composables/useClipboardCopy.ts +3 -20
- package/src/composables/useContentDisplay.ts +33 -2
- package/src/composables/useDevPluginReload.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +5 -31
- package/src/composables/useEventListeners.ts +0 -20
- package/src/composables/useExpandedDirs.ts +4 -15
- package/src/composables/useFaviconState.ts +12 -46
- package/src/composables/useFileChange.ts +53 -0
- package/src/composables/useFreshPluginData.ts +6 -43
- package/src/composables/useHealth.ts +14 -43
- package/src/composables/useImageErrorRepair.ts +104 -0
- package/src/composables/useLatestDaily.ts +40 -0
- package/src/composables/useMarkdownDoc.ts +39 -0
- package/src/composables/useMarkdownLinkHandler.ts +1 -1
- package/src/composables/useMcpTools.ts +3 -16
- package/src/composables/useNotifications.ts +138 -112
- package/src/composables/usePdfDownload.ts +17 -3
- package/src/composables/usePendingCalls.ts +8 -26
- package/src/composables/usePluginErrorBoundary.ts +68 -0
- package/src/composables/usePubSub.ts +9 -17
- package/src/composables/useRunElapsed.ts +5 -22
- package/src/composables/useSandboxStatus.ts +4 -20
- package/src/composables/useSessionDerived.ts +7 -15
- package/src/composables/useSessionHistory.ts +70 -29
- package/src/composables/useSessionSync.ts +25 -3
- package/src/composables/useSkillsList.ts +59 -0
- package/src/composables/useTranslatedQueries.ts +109 -0
- package/src/config/apiRoutes.ts +181 -80
- package/src/config/historyFilters.ts +5 -3
- package/src/config/hostEvents.ts +17 -0
- package/src/config/mcpCatalog.ts +277 -5
- package/src/config/pubsubChannels.ts +134 -12
- package/src/config/roles.ts +212 -147
- package/src/config/systemFileDescriptors.ts +5 -5
- package/src/config/toolNames.ts +52 -30
- package/src/config/workspacePaths.ts +26 -2
- package/src/lang/de.ts +483 -27
- package/src/lang/en.ts +448 -27
- package/src/lang/es.ts +474 -27
- package/src/lang/fr.ts +476 -27
- package/src/lang/ja.ts +465 -27
- package/src/lang/ko.ts +466 -27
- package/src/lang/pt-BR.ts +473 -27
- package/src/lang/zh.ts +463 -27
- package/src/lib/vue-i18n.ts +1 -1
- package/src/lib/wiki-page/slug.ts +66 -0
- package/src/main.ts +85 -0
- package/src/plugins/_extras.ts +58 -0
- package/src/plugins/_generated/metas.ts +42 -0
- package/src/plugins/_generated/registrations.ts +44 -0
- package/src/plugins/_generated/server-bindings.ts +47 -0
- package/src/plugins/accounting/Preview.vue +106 -0
- package/src/plugins/accounting/View.vue +632 -0
- package/src/plugins/accounting/actions.ts +34 -0
- package/src/plugins/accounting/api.ts +301 -0
- package/src/plugins/accounting/components/AccountEditor.vue +250 -0
- package/src/plugins/accounting/components/AccountRow.vue +50 -0
- package/src/plugins/accounting/components/AccountsList.vue +102 -0
- package/src/plugins/accounting/components/AccountsModal.vue +300 -0
- package/src/plugins/accounting/components/BalanceSheet.vue +186 -0
- package/src/plugins/accounting/components/BookSettings.vue +284 -0
- package/src/plugins/accounting/components/BookSwitcher.vue +78 -0
- package/src/plugins/accounting/components/DateRangePicker.vue +140 -0
- package/src/plugins/accounting/components/JournalEntryForm.vue +504 -0
- package/src/plugins/accounting/components/JournalList.vue +553 -0
- package/src/plugins/accounting/components/Ledger.vue +206 -0
- package/src/plugins/accounting/components/NewBookForm.vue +211 -0
- package/src/plugins/accounting/components/OpeningBalancesForm.vue +271 -0
- package/src/plugins/accounting/components/ProfitLoss.vue +160 -0
- package/src/plugins/accounting/components/accountDraft.ts +13 -0
- package/src/plugins/accounting/components/accountNumbering.ts +103 -0
- package/src/plugins/accounting/components/accountValidation.ts +75 -0
- package/src/plugins/accounting/components/useLatestRequest.ts +44 -0
- package/src/plugins/accounting/countries.ts +158 -0
- package/src/plugins/accounting/currencies.ts +64 -0
- package/src/plugins/accounting/dates.ts +51 -0
- package/src/plugins/accounting/definition.ts +199 -0
- package/src/plugins/accounting/fiscalYear.ts +136 -0
- package/src/plugins/accounting/index.ts +49 -0
- package/src/plugins/accounting/meta.ts +91 -0
- package/src/plugins/accounting/timeSeriesEnums.ts +16 -0
- package/src/plugins/api.ts +125 -0
- package/src/plugins/canvas/View.vue +38 -28
- package/src/plugins/canvas/definition.ts +10 -8
- package/src/plugins/canvas/index.ts +15 -8
- package/src/plugins/canvas/meta.ts +12 -0
- package/src/plugins/chart/Preview.vue +1 -1
- package/src/plugins/chart/View.vue +2 -2
- package/src/plugins/chart/definition.ts +12 -2
- package/src/plugins/chart/index.ts +15 -7
- package/src/plugins/chart/meta.ts +18 -0
- package/src/plugins/editImages/definition.ts +44 -0
- package/src/plugins/editImages/index.ts +43 -0
- package/src/plugins/editImages/meta.ts +5 -0
- package/src/plugins/generateImage/View.vue +3 -1
- package/src/plugins/generateImage/definition.ts +2 -0
- package/src/plugins/generateImage/index.ts +13 -5
- package/src/plugins/generateImage/meta.ts +5 -0
- package/src/plugins/index.ts +35 -0
- package/src/plugins/manageRoles/Preview.vue +7 -4
- package/src/plugins/manageRoles/View.vue +12 -8
- package/src/plugins/manageRoles/definition.ts +6 -0
- package/src/plugins/manageRoles/index.ts +7 -6
- package/src/plugins/manageSkills/View.vue +11 -7
- package/src/plugins/manageSkills/definition.ts +4 -1
- package/src/plugins/manageSkills/index.ts +14 -7
- package/src/plugins/manageSkills/meta.ts +21 -0
- package/src/plugins/manageSource/definition.ts +4 -1
- package/src/plugins/manageSource/index.ts +15 -7
- package/src/plugins/manageSource/meta.ts +21 -0
- package/src/plugins/markdown/Preview.vue +10 -8
- package/src/plugins/markdown/View.vue +84 -17
- package/src/plugins/markdown/definition.ts +7 -1
- package/src/plugins/markdown/index.ts +15 -8
- package/src/plugins/markdown/meta.ts +16 -0
- package/src/plugins/meta-types.ts +97 -0
- package/src/plugins/metas.ts +224 -0
- package/src/plugins/presentForm/Preview.vue +4 -15
- package/src/plugins/presentForm/View.vue +35 -78
- package/src/plugins/presentForm/definition.ts +7 -6
- package/src/plugins/presentForm/index.ts +12 -5
- package/src/plugins/presentForm/meta.ts +11 -0
- package/src/plugins/presentForm/plugin.ts +8 -9
- package/src/plugins/presentForm/types.ts +0 -24
- package/src/plugins/presentHtml/Preview.vue +1 -8
- package/src/plugins/presentHtml/View.vue +401 -30
- package/src/plugins/presentHtml/definition.ts +8 -5
- package/src/plugins/presentHtml/index.ts +15 -8
- package/src/plugins/presentHtml/meta.ts +14 -0
- package/src/plugins/presentMulmoScript/View.vue +327 -107
- package/src/plugins/presentMulmoScript/definition.ts +34 -7
- package/src/plugins/presentMulmoScript/helpers.ts +4 -5
- package/src/plugins/presentMulmoScript/index.ts +20 -7
- package/src/plugins/presentMulmoScript/meta.ts +52 -0
- package/src/plugins/scheduler/AutomationsPreview.vue +2 -8
- package/src/plugins/scheduler/Preview.vue +5 -2
- package/src/plugins/scheduler/TasksTab.vue +16 -36
- package/src/plugins/scheduler/View.vue +22 -54
- package/src/plugins/scheduler/automationsDefinition.ts +14 -9
- package/src/plugins/scheduler/automationsMeta.ts +5 -0
- package/src/plugins/scheduler/calendarDefinition.ts +4 -7
- package/src/plugins/scheduler/calendarMeta.ts +28 -0
- package/src/plugins/scheduler/formatSchedule.ts +6 -24
- package/src/plugins/scheduler/index.ts +26 -52
- package/src/plugins/scope.ts +57 -0
- package/src/plugins/server-bindings-types.ts +38 -0
- package/src/plugins/server.ts +32 -0
- package/src/plugins/skill/Preview.vue +25 -0
- package/src/plugins/skill/View.vue +125 -0
- package/src/plugins/skill/definition.ts +23 -0
- package/src/plugins/skill/index.ts +36 -0
- package/src/plugins/skill/plugin.ts +31 -0
- package/src/plugins/skill/types.ts +21 -0
- package/src/plugins/spreadsheet/Preview.vue +1 -3
- package/src/plugins/spreadsheet/View.vue +29 -49
- package/src/plugins/spreadsheet/cellHighlights.ts +2 -3
- package/src/plugins/spreadsheet/definition.ts +5 -2
- package/src/plugins/spreadsheet/index.ts +15 -8
- package/src/plugins/spreadsheet/keyboardNav.ts +38 -0
- package/src/plugins/spreadsheet/meta.ts +14 -0
- package/src/plugins/textResponse/Preview.vue +9 -1
- package/src/plugins/textResponse/View.vue +59 -8
- package/src/plugins/textResponse/index.ts +11 -3
- package/src/plugins/textResponse/plugin.ts +8 -10
- package/src/plugins/textResponse/types.ts +28 -0
- package/src/plugins/wiki/Preview.vue +6 -4
- package/src/plugins/wiki/View.vue +463 -254
- package/src/plugins/wiki/components/WikiPageBody.vue +159 -0
- package/src/plugins/wiki/helpers.ts +17 -0
- package/src/plugins/wiki/history/HistoryDetail.vue +325 -0
- package/src/plugins/wiki/history/HistoryTab.vue +167 -0
- package/src/plugins/wiki/history/RestoreConfirm.vue +63 -0
- package/src/plugins/wiki/history/api.ts +52 -0
- package/src/plugins/wiki/history/diff.ts +145 -0
- package/src/plugins/wiki/index.ts +42 -32
- package/src/plugins/wiki/meta.ts +10 -0
- package/src/plugins/wiki/pageEditLoader.ts +53 -0
- package/src/plugins/wiki/route.ts +8 -0
- package/src/router/guards.ts +2 -1
- package/src/router/index.ts +19 -0
- package/src/router/pageRoutes.ts +1 -0
- package/src/tools/index.ts +50 -51
- package/src/tools/runtimeLoader.ts +141 -0
- package/src/tools/types.ts +44 -1
- package/src/types/notification.ts +23 -0
- package/src/types/pastedFile.ts +10 -0
- package/src/types/session.ts +61 -3
- package/src/types/sse.ts +21 -6
- package/src/utils/agent/eventDispatch.ts +12 -9
- package/src/utils/agent/pastedAttachment.ts +35 -0
- package/src/utils/agent/request.ts +32 -3
- package/src/utils/agent/toolCalls.ts +7 -1
- package/src/utils/api.ts +1 -1
- package/src/utils/chat/exportMarkdown.ts +243 -0
- package/src/utils/errors.ts +10 -2
- package/src/utils/files/expandedDirs.ts +1 -1
- package/src/utils/filesPreview/todoPreview.ts +13 -2
- package/src/utils/format/date.ts +1 -3
- package/src/utils/format/jsonSyntax.ts +5 -0
- package/src/utils/html/iframeHeightReporterScript.ts +62 -0
- package/src/utils/html/previewCsp.ts +29 -2
- package/src/utils/image/htmlSrcAttrs.ts +122 -0
- package/src/utils/image/imageRepairInlineScript.ts +115 -0
- package/src/utils/image/resolve.ts +17 -3
- package/src/utils/image/rewriteMarkdownImageRefs.ts +62 -9
- package/src/utils/markdown/frontmatter.ts +125 -0
- package/src/utils/markdown/taskList.ts +7 -2
- package/src/utils/plugin/runtime.ts +132 -0
- package/src/utils/session/mergeSessions.ts +40 -37
- package/src/utils/session/sessionEntries.ts +74 -18
- package/src/utils/session/sessionHelpers.ts +54 -10
- package/src/utils/tools/result.ts +76 -14
- package/src/vite-env.d.ts +6 -0
- package/client/assets/html2canvas-Cx501zZr-Bug0qRNv.js +0 -5
- package/client/assets/index-CY-WpQUm.css +0 -2
- package/client/assets/index-DbTz2Mfs.js +0 -4911
- package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
- package/server/api/routes/html.ts +0 -114
- package/server/api/routes/todos.ts +0 -293
- package/server/api/routes/todosColumnsHandlers.ts +0 -333
- package/server/api/routes/todosHandlers.ts +0 -274
- package/server/api/routes/todosItemsHandlers.ts +0 -386
- package/server/utils/files/todos-io.ts +0 -29
- package/src/components/NotificationToast.vue +0 -75
- package/src/plugins/editImage/definition.ts +0 -27
- package/src/plugins/editImage/index.ts +0 -37
- package/src/plugins/presentHtml/helpers.ts +0 -72
- package/src/plugins/scheduler/LegacySchedulerView.vue +0 -32
- package/src/plugins/scheduler/legacyShape.ts +0 -34
- package/src/plugins/todo/Preview.vue +0 -68
- package/src/plugins/todo/View.vue +0 -378
- package/src/plugins/todo/composables/useTodos.ts +0 -179
- package/src/plugins/todo/definition.ts +0 -45
- package/src/plugins/todo/index.ts +0 -62
- package/src/plugins/todo/labels.ts +0 -163
- package/src/plugins/todo/priority.ts +0 -98
- package/src/plugins/todo/viewModes.ts +0 -19
- package/src/plugins/wiki/definition.ts +0 -25
- package/src/tools/legacyPluginNames.ts +0 -13
- package/src/utils/format/frontmatter.ts +0 -80
- package/src/utils/image/rewriteHtmlImageRefs.ts +0 -50
- package/src/utils/notification/dispatch.ts +0 -58
- /package/client/assets/{purify.es-Fx1Nqyry-BwJECkqS.js → purify.es-Fx1Nqyry-BSVNht6S.js} +0 -0
- /package/src/plugins/{editImage → editImages}/Preview.vue +0 -0
- /package/src/plugins/{editImage → editImages}/View.vue +0 -0
- /package/src/{config/schedulerActions.ts → plugins/scheduler/actions.ts} +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// In-process registry of runtime-loaded plugins (#1043 C-2).
|
|
2
|
+
//
|
|
3
|
+
// `loadRuntimePlugins()` produces a list of `RuntimePlugin`s; this
|
|
4
|
+
// module owns the singleton view of "what's registered right now" and
|
|
5
|
+
// wires the collision policy:
|
|
6
|
+
//
|
|
7
|
+
// 1. Static plugins (`PLUGIN_DEFS` from `plugin-names.ts`) win every
|
|
8
|
+
// collision — built-ins cannot be shadowed.
|
|
9
|
+
// 2. Among runtime plugins, first-loaded wins on duplicate tool name.
|
|
10
|
+
//
|
|
11
|
+
// Both processes (parent server, spawned MCP child) hold their own
|
|
12
|
+
// in-memory copy. They read the same ledger / cache so the contents
|
|
13
|
+
// match without IPC.
|
|
14
|
+
|
|
15
|
+
import type { ToolDefinition } from "gui-chat-protocol";
|
|
16
|
+
import type { RuntimePlugin } from "./runtime-loader.js";
|
|
17
|
+
import { log } from "../system/logger/index.js";
|
|
18
|
+
|
|
19
|
+
const LOG_PREFIX = "plugins/registry";
|
|
20
|
+
|
|
21
|
+
let registry: RuntimePlugin[] = [];
|
|
22
|
+
let toolNameIndex = new Map<string, RuntimePlugin>();
|
|
23
|
+
let oauthAliasIndex = new Map<string, RuntimePlugin>();
|
|
24
|
+
|
|
25
|
+
export interface RegisterResult {
|
|
26
|
+
/** Plugins that landed in the registry. */
|
|
27
|
+
registered: RuntimePlugin[];
|
|
28
|
+
/** Plugins skipped because their tool name collides with a static
|
|
29
|
+
* built-in or an earlier-loaded runtime plugin. */
|
|
30
|
+
collisions: { plugin: RuntimePlugin; reason: "static" | "runtime"; existingTool: string }[];
|
|
31
|
+
/** OAuth callback aliases dropped because two plugins claim the
|
|
32
|
+
* same one. The first-registered plugin keeps the alias; later
|
|
33
|
+
* collisions are logged and skipped (the plugin itself still
|
|
34
|
+
* loads — only its OAuth route is unavailable). */
|
|
35
|
+
oauthAliasCollisions: { plugin: RuntimePlugin; alias: string; existingPlugin: string }[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Replace the registry with the given runtime plugins, applying the
|
|
39
|
+
* collision policy described at the top of the file. Static names
|
|
40
|
+
* are passed in by the caller (parent server / MCP child) because
|
|
41
|
+
* the static set differs slightly between contexts (mcp tools vs.
|
|
42
|
+
* plugin-names PLUGIN_DEFS). */
|
|
43
|
+
export function registerRuntimePlugins(staticToolNames: ReadonlySet<string>, plugins: readonly RuntimePlugin[]): RegisterResult {
|
|
44
|
+
const registered: RuntimePlugin[] = [];
|
|
45
|
+
const collisions: RegisterResult["collisions"] = [];
|
|
46
|
+
const oauthAliasCollisions: RegisterResult["oauthAliasCollisions"] = [];
|
|
47
|
+
const seen = new Map<string, RuntimePlugin>();
|
|
48
|
+
const oauthAliases = new Map<string, RuntimePlugin>();
|
|
49
|
+
for (const plugin of plugins) {
|
|
50
|
+
const toolName = plugin.definition.name;
|
|
51
|
+
if (staticToolNames.has(toolName)) {
|
|
52
|
+
collisions.push({ plugin, reason: "static", existingTool: toolName });
|
|
53
|
+
log.warn(LOG_PREFIX, "skipping runtime plugin — name collides with static tool", {
|
|
54
|
+
plugin: plugin.name,
|
|
55
|
+
tool: toolName,
|
|
56
|
+
});
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (seen.has(toolName)) {
|
|
60
|
+
collisions.push({ plugin, reason: "runtime", existingTool: toolName });
|
|
61
|
+
const existing = seen.get(toolName);
|
|
62
|
+
log.warn(LOG_PREFIX, "skipping runtime plugin — name collides with already-loaded runtime plugin", {
|
|
63
|
+
plugin: plugin.name,
|
|
64
|
+
tool: toolName,
|
|
65
|
+
existingPlugin: existing?.name,
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
seen.set(toolName, plugin);
|
|
70
|
+
registered.push(plugin);
|
|
71
|
+
if (plugin.oauthCallbackAlias !== null) {
|
|
72
|
+
const existingAliasOwner = oauthAliases.get(plugin.oauthCallbackAlias);
|
|
73
|
+
if (existingAliasOwner) {
|
|
74
|
+
oauthAliasCollisions.push({ plugin, alias: plugin.oauthCallbackAlias, existingPlugin: existingAliasOwner.name });
|
|
75
|
+
log.warn(LOG_PREFIX, "skipping OAuth callback alias — already claimed by another runtime plugin", {
|
|
76
|
+
plugin: plugin.name,
|
|
77
|
+
alias: plugin.oauthCallbackAlias,
|
|
78
|
+
existingPlugin: existingAliasOwner.name,
|
|
79
|
+
});
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
oauthAliases.set(plugin.oauthCallbackAlias, plugin);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
registry = registered;
|
|
86
|
+
toolNameIndex = seen;
|
|
87
|
+
oauthAliasIndex = oauthAliases;
|
|
88
|
+
return { registered, collisions, oauthAliasCollisions };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getRuntimePlugins(): readonly RuntimePlugin[] {
|
|
92
|
+
return registry;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getRuntimePluginByToolName(toolName: string): RuntimePlugin | null {
|
|
96
|
+
return toolNameIndex.get(toolName) ?? null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getRuntimePluginByOauthAlias(alias: string): RuntimePlugin | null {
|
|
100
|
+
return oauthAliasIndex.get(alias) ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getRuntimeToolDefinitions(): readonly ToolDefinition[] {
|
|
104
|
+
return registry.map((entry) => entry.definition);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Test-only reset. */
|
|
108
|
+
export function _resetRuntimeRegistryForTest(): void {
|
|
109
|
+
registry = [];
|
|
110
|
+
toolNameIndex = new Map();
|
|
111
|
+
oauthAliasIndex = new Map();
|
|
112
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Plugin-facing task scheduler API. The host attaches a per-plugin
|
|
2
|
+
// instance of `TasksRuntimeApi` to the `PluginRuntime` it constructs
|
|
3
|
+
// for each plugin (`server/plugins/runtime.ts`). One periodic tick
|
|
4
|
+
// per plugin — the registry id is `plugin:<pkg>`, derived from the
|
|
5
|
+
// caller's pkg name; the plugin doesn't supply one.
|
|
6
|
+
//
|
|
7
|
+
// Plugin authors access this surface via the `MulmoclaudeRuntime`
|
|
8
|
+
// cast, identical to the notifier extension:
|
|
9
|
+
//
|
|
10
|
+
// import type { PluginRuntime } from "gui-chat-protocol";
|
|
11
|
+
// import type { MulmoclaudeRuntime } from "<mulmoclaude>/notifier/runtime-api";
|
|
12
|
+
// export default definePlugin((runtime: PluginRuntime) => {
|
|
13
|
+
// const { tasks } = runtime as MulmoclaudeRuntime;
|
|
14
|
+
// tasks.register({ schedule: { ... }, run: async () => { ... } });
|
|
15
|
+
// });
|
|
16
|
+
//
|
|
17
|
+
// Once the API stabilises (Phase 3 of the Encore plan), this is a
|
|
18
|
+
// candidate for upstreaming into gui-chat-protocol so the cast goes
|
|
19
|
+
// away.
|
|
20
|
+
|
|
21
|
+
/** Same shape as the host task manager's `TaskSchedule`
|
|
22
|
+
* (`server/events/task-manager/index.ts`). Forwarded verbatim to the
|
|
23
|
+
* task manager — no richer types layered at the plugin runtime
|
|
24
|
+
* level. Plugins build "remind 3 weeks before" / "weekly on
|
|
25
|
+
* Tuesdays" logic inside `run()` against their own files. */
|
|
26
|
+
export type PluginTaskSchedule = { type: "interval"; intervalMs: number } | { type: "daily"; time: string }; // "HH:MM" UTC
|
|
27
|
+
|
|
28
|
+
export interface PluginTaskRegistration {
|
|
29
|
+
/** When to fire. Forwarded verbatim to the host task manager. */
|
|
30
|
+
schedule: PluginTaskSchedule;
|
|
31
|
+
/** Tick handler. Errors are caught and logged by the host task
|
|
32
|
+
* manager (`task-manager/index.ts:89`); they do not propagate. */
|
|
33
|
+
run: () => Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TasksRuntimeApi {
|
|
37
|
+
/** Register the plugin's single periodic tick.
|
|
38
|
+
*
|
|
39
|
+
* - Registry id is `plugin:<pkg>`; the plugin does not supply one.
|
|
40
|
+
* - Cap-at-1: the second call from the same plugin throws with a
|
|
41
|
+
* friendly message ("Plugin <pkg> already registered a task —
|
|
42
|
+
* only one tick per plugin is allowed").
|
|
43
|
+
* - No `unregister()`. Plugins load once at boot
|
|
44
|
+
* (`server/index.ts:723-`); server-side hot-reload is not
|
|
45
|
+
* supported, so a clean lifecycle hook isn't needed.
|
|
46
|
+
*
|
|
47
|
+
* Throws synchronously on duplicate registration so plugin authors
|
|
48
|
+
* see the failure during setup, not at first tick. */
|
|
49
|
+
register: (task: PluginTaskRegistration) => void;
|
|
50
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// Plugin runtime construction — the per-plugin scoped object
|
|
2
|
+
// `definePlugin(setup)` factories receive at load time (#1110).
|
|
3
|
+
//
|
|
4
|
+
// Every helper closes over `pkgName` so a plugin's pubsub channel,
|
|
5
|
+
// data dir, log prefix etc. cannot leak across plugins. Path arguments
|
|
6
|
+
// to `files.{data,config}` are normalised to POSIX, then anchored
|
|
7
|
+
// inside the plugin's scope root via `ensureInsideBase` — so misuse of
|
|
8
|
+
// `node:path` on Windows still works and `"../../etc/passwd"` is
|
|
9
|
+
// rejected.
|
|
10
|
+
//
|
|
11
|
+
// This module is server-side only. The browser-side counterpart lives
|
|
12
|
+
// at `src/utils/plugin/runtime.ts`.
|
|
13
|
+
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { readFile, readdir, stat as fsStat, unlink as fsUnlink } from "node:fs/promises";
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import type { FileOps, PluginRuntime } from "gui-chat-protocol";
|
|
18
|
+
|
|
19
|
+
import { WORKSPACE_PATHS } from "../workspace/paths.js";
|
|
20
|
+
import { writeFileAtomic } from "../utils/files/atomic.js";
|
|
21
|
+
import { log as hostLog, type Logger } from "../system/logger/index.js";
|
|
22
|
+
import { ensureInsideBase } from "./runtime-loader.js";
|
|
23
|
+
import { ONE_SECOND_MS } from "../utils/time.js";
|
|
24
|
+
import type { IPubSub } from "../events/pub-sub/index.js";
|
|
25
|
+
import * as notifierEngine from "../notifier/engine.js";
|
|
26
|
+
import type { MulmoclaudeRuntime, NotifierRuntimeApi } from "../notifier/runtime-api.js";
|
|
27
|
+
import type { ITaskManager } from "../events/task-manager/index.js";
|
|
28
|
+
import type { TasksRuntimeApi } from "./runtime-tasks-api.js";
|
|
29
|
+
import type { ChatRuntimeApi } from "./runtime-chat-api.js";
|
|
30
|
+
import { startChat } from "../api/routes/agent.js";
|
|
31
|
+
import { PLUGIN_SESSION_ORIGIN_PREFIX } from "../../src/types/session.js";
|
|
32
|
+
|
|
33
|
+
const DEFAULT_FETCH_TIMEOUT_MS = 10 * ONE_SECOND_MS;
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
36
|
+
// Path normalisation contract (see plans/done/feat-plugin-runtime-extensions-1110.md)
|
|
37
|
+
//
|
|
38
|
+
// 1. Replace `\` with `/` (Windows path.join leak repair).
|
|
39
|
+
// 2. `path.posix.normalize` (folds `..`, `.`, `//`).
|
|
40
|
+
// 3. Reject if the normalised form starts with `..` or `/`
|
|
41
|
+
// (would escape the scope root or be absolute).
|
|
42
|
+
// 4. `path.join(scopeRoot, ...segments)` — platform-aware join so
|
|
43
|
+
// the returned absolute path uses the host OS separator
|
|
44
|
+
// (backslashes on Windows, forward slashes on POSIX). Critical
|
|
45
|
+
// for `ensureInsideBase` to accept the result.
|
|
46
|
+
//
|
|
47
|
+
// Plugin authors should never need `node:path`; the platform meets
|
|
48
|
+
// them where they are even if they mis-import it on Windows.
|
|
49
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/** Resolve a plugin-supplied path to an absolute path inside the
|
|
52
|
+
* plugin's scope root. Throws on traversal. Exported for tests.
|
|
53
|
+
*
|
|
54
|
+
* Cross-platform: `scopeRoot` is the host OS's native form (drive
|
|
55
|
+
* letter + backslashes on Windows, forward slashes on POSIX). The
|
|
56
|
+
* user-supplied `rel` is POSIX-relative by contract; we sanitise
|
|
57
|
+
* for the lexical traversal check via `path.posix.*`, then join
|
|
58
|
+
* with `path.join` so the returned absolute path is in the host's
|
|
59
|
+
* native form (which `ensureInsideBase` requires). */
|
|
60
|
+
export function normalizePluginPath(scopeRoot: string, rel: string): string {
|
|
61
|
+
// Defang Windows path.join output and any mixed-separator inputs.
|
|
62
|
+
const slashed = rel.replace(/\\/g, "/");
|
|
63
|
+
// Fold `..` / `.` / repeated `/` lexically (no fs touch yet).
|
|
64
|
+
const normalised = path.posix.normalize(slashed);
|
|
65
|
+
// Reject lexically — anything that escapes the scope root after
|
|
66
|
+
// normalisation either starts with `..` (parent-traversal) or is
|
|
67
|
+
// absolute (`/etc/passwd`). `path.posix.normalize` produces `..`
|
|
68
|
+
// alone if the input is `..` itself, hence the equality branch.
|
|
69
|
+
if (normalised === ".." || normalised.startsWith("../") || normalised.startsWith("/")) {
|
|
70
|
+
throw new Error(`path escapes plugin scope: ${rel}`);
|
|
71
|
+
}
|
|
72
|
+
const segments = normalised === "." ? [] : normalised.split("/");
|
|
73
|
+
// `path.join` (platform-aware) so the result uses the host
|
|
74
|
+
// separator. On Windows that's backslashes; on POSIX, forward
|
|
75
|
+
// slashes — matching `scopeRoot`'s existing form so
|
|
76
|
+
// `ensureInsideBase`'s `path.resolve` + `path.sep` comparison
|
|
77
|
+
// works across both platforms.
|
|
78
|
+
const absolute = path.join(scopeRoot, ...segments);
|
|
79
|
+
// Defence in depth: even though the lexical check above should
|
|
80
|
+
// catch every escape, run `ensureInsideBase` so a future change to
|
|
81
|
+
// either step doesn't silently regress the safety guarantee.
|
|
82
|
+
if (!ensureInsideBase(absolute, scopeRoot)) {
|
|
83
|
+
throw new Error(`path escapes plugin scope: ${rel}`);
|
|
84
|
+
}
|
|
85
|
+
return absolute;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
89
|
+
// Scoped FileOps factory
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
function isErrnoException(value: unknown): value is { code: string } {
|
|
93
|
+
return typeof value === "object" && value !== null && "code" in value && typeof (value as { code: unknown }).code === "string";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function makeFileOps(scopeRoot: string): FileOps {
|
|
97
|
+
// The scope root may not exist on first use. Lazy-create at write
|
|
98
|
+
// time (writeFileAtomic does its own mkdir for the file's parent;
|
|
99
|
+
// we ensure the scope root exists for readDir() / stat() etc.).
|
|
100
|
+
// Reads against a never-written plugin throw ENOENT — exposed as
|
|
101
|
+
// `exists() === false` rather than handled here.
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
async read(rel) {
|
|
105
|
+
return readFile(normalizePluginPath(scopeRoot, rel), "utf-8");
|
|
106
|
+
},
|
|
107
|
+
async readBytes(rel) {
|
|
108
|
+
const buf = await readFile(normalizePluginPath(scopeRoot, rel));
|
|
109
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
110
|
+
},
|
|
111
|
+
async write(rel, content) {
|
|
112
|
+
const abs = normalizePluginPath(scopeRoot, rel);
|
|
113
|
+
await writeFileAtomic(abs, content);
|
|
114
|
+
},
|
|
115
|
+
async readDir(rel) {
|
|
116
|
+
const abs = normalizePluginPath(scopeRoot, rel);
|
|
117
|
+
try {
|
|
118
|
+
return await readdir(abs);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
if (isErrnoException(err) && err.code === "ENOENT") return [];
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
async stat(rel) {
|
|
125
|
+
const { mtimeMs, size } = await fsStat(normalizePluginPath(scopeRoot, rel));
|
|
126
|
+
return { mtimeMs, size };
|
|
127
|
+
},
|
|
128
|
+
async exists(rel) {
|
|
129
|
+
try {
|
|
130
|
+
await fsStat(normalizePluginPath(scopeRoot, rel));
|
|
131
|
+
return true;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (isErrnoException(err) && err.code === "ENOENT") return false;
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
async unlink(rel) {
|
|
138
|
+
try {
|
|
139
|
+
await fsUnlink(normalizePluginPath(scopeRoot, rel));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (isErrnoException(err) && err.code === "ENOENT") return;
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
149
|
+
// Sanitisation: pkg name → on-disk directory segment
|
|
150
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
/** Convert an npm package name (`@org/foo`) to a single safe directory
|
|
153
|
+
* segment. URL-encodes `/` in scoped names so the scope root stays
|
|
154
|
+
* one level deep — keeps `readdir` predictable and avoids accidental
|
|
155
|
+
* traversal via the package name itself.
|
|
156
|
+
*
|
|
157
|
+
* Exported for tests. */
|
|
158
|
+
export function sanitisePackageNameForFs(pkgName: string): string {
|
|
159
|
+
return encodeURIComponent(pkgName);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
163
|
+
// Scoped logger
|
|
164
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
function makeScopedLogger(pkgName: string): PluginRuntime["log"] {
|
|
167
|
+
const prefix = `plugin/${pkgName}`;
|
|
168
|
+
return {
|
|
169
|
+
debug: (msg, data) => hostLog.debug(prefix, msg, data as Record<string, unknown> | undefined),
|
|
170
|
+
info: (msg, data) => hostLog.info(prefix, msg, data as Record<string, unknown> | undefined),
|
|
171
|
+
warn: (msg, data) => hostLog.warn(prefix, msg, data as Record<string, unknown> | undefined),
|
|
172
|
+
error: (msg, data) => hostLog.error(prefix, msg, data as Record<string, unknown> | undefined),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
177
|
+
// Scoped fetch (timeout + optional host allowlist)
|
|
178
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
function makeScopedFetch(pkgName: string): PluginRuntime["fetch"] {
|
|
181
|
+
return async (url, opts = {}) => {
|
|
182
|
+
if (opts.allowedHosts && opts.allowedHosts.length > 0) {
|
|
183
|
+
const { hostname } = new URL(url);
|
|
184
|
+
if (!opts.allowedHosts.includes(hostname)) {
|
|
185
|
+
throw new Error(`plugin/${pkgName}: host ${hostname} not in allowedHosts`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const controller = new AbortController();
|
|
189
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
190
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
191
|
+
try {
|
|
192
|
+
// Forward both the caller's signal (if any) and our timeout
|
|
193
|
+
// signal. AbortSignal.any returns a signal that fires when
|
|
194
|
+
// either input fires — Node 20+.
|
|
195
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, controller.signal]) : controller.signal;
|
|
196
|
+
// The runtime cast keeps PluginFetchInit narrow (string | Uint8Array)
|
|
197
|
+
// so plugin authors don't need to know the wider DOM BodyInit union.
|
|
198
|
+
const body = opts.body as Parameters<typeof fetch>[1] extends infer T ? (T extends { body?: infer B } ? B : never) : never;
|
|
199
|
+
return await fetch(url, { method: opts.method, headers: opts.headers, body, signal });
|
|
200
|
+
} catch (err) {
|
|
201
|
+
// Re-throw with plugin context so a fan-out failure in the
|
|
202
|
+
// host log immediately points at the responsible plugin
|
|
203
|
+
// instead of an anonymous network error (CodeRabbit review on
|
|
204
|
+
// PR #1124). Distinguishing AbortError → "timeout" is more
|
|
205
|
+
// useful than the bare "AbortError: This operation was aborted"
|
|
206
|
+
// message that surfaces from AbortController.
|
|
207
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
208
|
+
throw new Error(`plugin/${pkgName}: fetch timed out after ${timeoutMs}ms (${url})`);
|
|
209
|
+
}
|
|
210
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
211
|
+
throw new Error(`plugin/${pkgName}: fetch failed (${url}): ${message}`);
|
|
212
|
+
} finally {
|
|
213
|
+
clearTimeout(timer);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function makeScopedFetchJson(pkgName: string, scopedFetch: PluginRuntime["fetch"]): PluginRuntime["fetchJson"] {
|
|
219
|
+
// When `opts.parse` is provided we trust its narrowing; when absent
|
|
220
|
+
// the caller asserts the JSON shape themselves (same contract as
|
|
221
|
+
// `JSON.parse(...) as T`).
|
|
222
|
+
return async function fetchJson<T>(url: string, opts: { parse?: (raw: unknown) => T } & Parameters<PluginRuntime["fetch"]>[1] = {}): Promise<T> {
|
|
223
|
+
const response = await scopedFetch(url, opts);
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
throw new Error(`plugin/${pkgName}: fetchJson HTTP ${response.status} for ${url}`);
|
|
226
|
+
}
|
|
227
|
+
const raw = (await response.json()) as unknown;
|
|
228
|
+
return opts.parse ? opts.parse(raw) : (raw as T);
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
233
|
+
// Scoped pubsub publisher
|
|
234
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
/** Build the channel name for a given plugin's event. Centralised so
|
|
237
|
+
* the server-side publish side and the browser-side subscribe side
|
|
238
|
+
* agree on the format (`plugin:<pkg>:<event>`). Exported for tests
|
|
239
|
+
* and for the browser runtime to import the same constant. */
|
|
240
|
+
export function pluginChannelName(pkgName: string, eventName: string): string {
|
|
241
|
+
return `plugin:${pkgName}:${eventName}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function makeScopedPubSub(pkgName: string, hostPubSub: IPubSub): PluginRuntime["pubsub"] {
|
|
245
|
+
return {
|
|
246
|
+
publish(eventName, payload) {
|
|
247
|
+
hostPubSub.publish(pluginChannelName(pkgName, eventName), payload);
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
253
|
+
// Scoped notifier (host extension over gui-chat-protocol's PluginRuntime)
|
|
254
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
function makeScopedNotifier(pkgName: string): NotifierRuntimeApi {
|
|
257
|
+
// Both `publish` and `clear` are plugin-scoped:
|
|
258
|
+
// - publish forces `pluginPkg` to the caller's pkg name (the
|
|
259
|
+
// plugin literally cannot publish under another's namespace).
|
|
260
|
+
// - clear routes through `clearForPlugin` so a plugin holding
|
|
261
|
+
// another plugin's id (e.g. via a future leak) silently no-ops
|
|
262
|
+
// instead of dismissing it. CodeRabbit review on PR #1198.
|
|
263
|
+
return {
|
|
264
|
+
publish: (input) => notifierEngine.publish({ ...input, pluginPkg: pkgName }),
|
|
265
|
+
clear: (entryId) => notifierEngine.clearForPlugin(pkgName, entryId),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
270
|
+
// Scoped tasks (host extension — Phase 1 of Encore plan)
|
|
271
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
/** Build the registry id used for a plugin's tick. Centralised so the
|
|
274
|
+
* cap-at-1 closure check and the host task manager registration agree
|
|
275
|
+
* on the format. Exported for tests. */
|
|
276
|
+
export function pluginTaskId(pkgName: string): string {
|
|
277
|
+
return `plugin:${pkgName}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function makeScopedTasks(pkgName: string, taskManager: ITaskManager): TasksRuntimeApi {
|
|
281
|
+
// Cap-at-1 enforced at the runtime-API layer so the plugin author
|
|
282
|
+
// sees a friendly message before the host task manager's generic
|
|
283
|
+
// duplicate-id throw fires. The closure is per-plugin (each plugin
|
|
284
|
+
// gets its own runtime), so `registered` cannot leak across
|
|
285
|
+
// plugins.
|
|
286
|
+
let registered = false;
|
|
287
|
+
return {
|
|
288
|
+
register(task) {
|
|
289
|
+
if (registered) {
|
|
290
|
+
throw new Error(`plugin/${pkgName}: already registered a task — only one tick per plugin is allowed`);
|
|
291
|
+
}
|
|
292
|
+
registered = true;
|
|
293
|
+
taskManager.registerTask({
|
|
294
|
+
id: pluginTaskId(pkgName),
|
|
295
|
+
description: `tick for plugin ${pkgName}`,
|
|
296
|
+
schedule: task.schedule,
|
|
297
|
+
run: () => task.run(),
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
304
|
+
// Scoped chat (host extension — Phase 1 of Encore plan)
|
|
305
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
const DEFAULT_PLUGIN_CHAT_ROLE = "general";
|
|
308
|
+
|
|
309
|
+
function makeScopedChat(pkgName: string): ChatRuntimeApi {
|
|
310
|
+
return {
|
|
311
|
+
async start({ initialMessage, role }) {
|
|
312
|
+
const roleId = role ?? DEFAULT_PLUGIN_CHAT_ROLE;
|
|
313
|
+
const chatSessionId = randomUUID();
|
|
314
|
+
const result = await startChat({
|
|
315
|
+
message: initialMessage,
|
|
316
|
+
roleId,
|
|
317
|
+
chatSessionId,
|
|
318
|
+
origin: `${PLUGIN_SESSION_ORIGIN_PREFIX}${pkgName}`,
|
|
319
|
+
});
|
|
320
|
+
if (result.kind === "error") {
|
|
321
|
+
throw new Error(`plugin/${pkgName}: chat.start failed: ${result.error}`);
|
|
322
|
+
}
|
|
323
|
+
return { chatId: chatSessionId };
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
329
|
+
// Public entry — wire everything into one PluginRuntime
|
|
330
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
export interface MakePluginRuntimeDeps {
|
|
333
|
+
/** npm package name. Used both for sanitised fs segment + namespace prefix. */
|
|
334
|
+
pkgName: string;
|
|
335
|
+
/** Host pub/sub instance for fanning out scoped events. */
|
|
336
|
+
pubsub: IPubSub;
|
|
337
|
+
/** Locale tag the host has detected (e.g. `"ja"`). Snapshot only;
|
|
338
|
+
* reactive updates ride the frontend `BrowserPluginRuntime.locale`. */
|
|
339
|
+
locale: string;
|
|
340
|
+
/** Host task manager — backs `runtime.tasks.register()`. The same
|
|
341
|
+
* instance the host's own system tasks (journal, chat-index)
|
|
342
|
+
* register against. */
|
|
343
|
+
taskManager: ITaskManager;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function makePluginRuntime(deps: MakePluginRuntimeDeps): MulmoclaudeRuntime {
|
|
347
|
+
const { pkgName, pubsub, locale, taskManager } = deps;
|
|
348
|
+
const seg = sanitisePackageNameForFs(pkgName);
|
|
349
|
+
const dataRoot = path.join(WORKSPACE_PATHS.pluginsData, seg);
|
|
350
|
+
const configRoot = path.join(WORKSPACE_PATHS.pluginsConfig, seg);
|
|
351
|
+
const scopedFetch = makeScopedFetch(pkgName);
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
pubsub: makeScopedPubSub(pkgName, pubsub),
|
|
355
|
+
locale,
|
|
356
|
+
files: {
|
|
357
|
+
data: makeFileOps(dataRoot),
|
|
358
|
+
config: makeFileOps(configRoot),
|
|
359
|
+
},
|
|
360
|
+
log: makeScopedLogger(pkgName),
|
|
361
|
+
fetch: scopedFetch,
|
|
362
|
+
fetchJson: makeScopedFetchJson(pkgName, scopedFetch),
|
|
363
|
+
// Host extensions over gui-chat-protocol's PluginRuntime. Plugin
|
|
364
|
+
// authors access via `runtime as MulmoclaudeRuntime` for now.
|
|
365
|
+
// Phase 3 of the Encore plan upstreams these into the protocol.
|
|
366
|
+
notifier: makeScopedNotifier(pkgName),
|
|
367
|
+
tasks: makeScopedTasks(pkgName, taskManager),
|
|
368
|
+
chat: makeScopedChat(pkgName),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Re-export host's writeFileAtomic so plugin runtime tests can
|
|
373
|
+
// inspect the same primitive without reaching into utils/.
|
|
374
|
+
export { writeFileAtomic };
|
|
375
|
+
|
|
376
|
+
// Re-export logger type so the loader can type-check sink injection
|
|
377
|
+
// without re-importing from system/logger.
|
|
378
|
+
export type { Logger };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Pure helpers for the translation cache. Kept side-effect free so
|
|
2
|
+
// the unit tests can exercise the lookup / merge / assemble logic
|
|
3
|
+
// without touching the filesystem or any LLM.
|
|
4
|
+
|
|
5
|
+
import type { DictionaryFile } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export function emptyDictionary(): DictionaryFile {
|
|
8
|
+
return { sentences: {} };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// `obj[userKey] = value` with `userKey === "__proto__"` triggers the
|
|
12
|
+
// inherited setter on Object.prototype and mutates the prototype
|
|
13
|
+
// chain instead of creating an own property — losing the entry and
|
|
14
|
+
// polluting `obj`'s shape. `Object.defineProperty` bypasses the
|
|
15
|
+
// setter and always creates a normal own data property.
|
|
16
|
+
function safeAssign<V>(target: Record<string, V>, key: string, value: V): void {
|
|
17
|
+
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function lookupCached(dict: DictionaryFile, sentence: string, lang: string): string | undefined {
|
|
21
|
+
return dict.sentences[sentence]?.[lang];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SplitResult {
|
|
25
|
+
/** Map from input sentence to its cached translation. */
|
|
26
|
+
readonly cached: Map<string, string>;
|
|
27
|
+
/** Distinct sentences that need a fresh LLM translation. */
|
|
28
|
+
readonly misses: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function splitHitMiss(dict: DictionaryFile, sentences: readonly string[], lang: string): SplitResult {
|
|
32
|
+
const cached = new Map<string, string>();
|
|
33
|
+
const missesSet = new Set<string>();
|
|
34
|
+
for (const sentence of sentences) {
|
|
35
|
+
const translated = lookupCached(dict, sentence, lang);
|
|
36
|
+
if (translated !== undefined) {
|
|
37
|
+
cached.set(sentence, translated);
|
|
38
|
+
} else {
|
|
39
|
+
missesSet.add(sentence);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { cached, misses: Array.from(missesSet) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function mergeTranslations(dict: DictionaryFile, lang: string, fresh: ReadonlyMap<string, string>): DictionaryFile {
|
|
46
|
+
const next: Record<string, Record<string, string>> = {};
|
|
47
|
+
for (const [source, langs] of Object.entries(dict.sentences)) {
|
|
48
|
+
safeAssign(next, source, { ...langs });
|
|
49
|
+
}
|
|
50
|
+
for (const [source, translated] of fresh) {
|
|
51
|
+
// `next[source]` would return `Object.prototype` when source is
|
|
52
|
+
// `"__proto__"`; `Object.hasOwn` keeps us in own-property territory.
|
|
53
|
+
const existing = Object.hasOwn(next, source) ? next[source] : {};
|
|
54
|
+
// `lang` is regex-validated upstream (`^[a-z]{2}(?:-[A-Z]{2})?$`)
|
|
55
|
+
// so it cannot be `__proto__` / `constructor` / `prototype`; the
|
|
56
|
+
// outer assignment by user-supplied `source` is the only unsafe
|
|
57
|
+
// site and uses safeAssign.
|
|
58
|
+
existing[lang] = translated;
|
|
59
|
+
safeAssign(next, source, existing);
|
|
60
|
+
}
|
|
61
|
+
return { sentences: next };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function assembleResult(sentences: readonly string[], cached: ReadonlyMap<string, string>, fresh: ReadonlyMap<string, string>): string[] {
|
|
65
|
+
return sentences.map((sentence) => {
|
|
66
|
+
const translated = cached.get(sentence) ?? fresh.get(sentence);
|
|
67
|
+
if (translated === undefined) {
|
|
68
|
+
throw new Error(`[translation] missing translation for ${JSON.stringify(sentence)}`);
|
|
69
|
+
}
|
|
70
|
+
return translated;
|
|
71
|
+
});
|
|
72
|
+
}
|