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,52 @@
|
|
|
1
|
+
// Preset plugins shipped with the repo (#1043 C-2 follow-up).
|
|
2
|
+
//
|
|
3
|
+
// Each entry is a published npm package that lives in mulmoclaude's
|
|
4
|
+
// `node_modules`; the boot loader registers it through the same path
|
|
5
|
+
// as a user-installed runtime plugin (workspace ledger), so the
|
|
6
|
+
// frontend dynamic-import + Vue View pipeline runs end-to-end on
|
|
7
|
+
// every fresh checkout — no manual `yarn plugin:install` needed for
|
|
8
|
+
// testing or for first-launch UX.
|
|
9
|
+
//
|
|
10
|
+
// Presets and user-installed plugins share the runtime registry. On
|
|
11
|
+
// tool-name collision the preset wins (loaded first; static MCP
|
|
12
|
+
// built-ins still win over both).
|
|
13
|
+
//
|
|
14
|
+
// Adding a preset:
|
|
15
|
+
// 1. `yarn add <package>` (or extend an existing dep)
|
|
16
|
+
// 2. Append a row below
|
|
17
|
+
// 3. Restart the server
|
|
18
|
+
//
|
|
19
|
+
// Removing a preset:
|
|
20
|
+
// 1. Remove the row
|
|
21
|
+
// 2. Optionally `yarn remove <package>`
|
|
22
|
+
// 3. Restart
|
|
23
|
+
|
|
24
|
+
export interface PresetPlugin {
|
|
25
|
+
/** npm package name (the directory under `node_modules`). */
|
|
26
|
+
packageName: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const PRESET_PLUGINS: readonly PresetPlugin[] = [
|
|
30
|
+
// #1145 — runtime-plugin shape of the built-in todo plugin.
|
|
31
|
+
// Loaded as a preset (resolved via `node_modules/@mulmoclaude/todo-plugin/`
|
|
32
|
+
// through the yarn-workspaces symlink) so it boots on every fresh
|
|
33
|
+
// checkout. Owns `manageTodoList` end-to-end now that the static
|
|
34
|
+
// entry under `src/plugins/todo/` has been removed.
|
|
35
|
+
{ packageName: "@mulmoclaude/todo-plugin" },
|
|
36
|
+
// #1162 — Spotify integration (Liked Songs / playlists / recently
|
|
37
|
+
// played). PR 1 ships OAuth + token persistence; PR 2 adds the
|
|
38
|
+
// listening-data kinds and the Vue View. Loaded the same way as
|
|
39
|
+
// todo-plugin via the workspace symlink at
|
|
40
|
+
// `node_modules/@mulmoclaude/spotify-plugin/`.
|
|
41
|
+
{ packageName: "@mulmoclaude/spotify-plugin" },
|
|
42
|
+
// #1175 / #1169 PR-A — recipe-book runtime plugin. First slice of
|
|
43
|
+
// the Cooking Coach plugin set. Stores recipes as one markdown file
|
|
44
|
+
// per recipe under the plugin's `files.data` scope; demonstrates
|
|
45
|
+
// markdown-per-record storage on the v0.3 runtime API.
|
|
46
|
+
{ packageName: "@mulmoclaude/recipe-book-plugin" },
|
|
47
|
+
// Encore plan PR 1 follow-up — dev-only debug playground plugin.
|
|
48
|
+
// Owns the standalone `/debug` page; the toolbar entry is gated on
|
|
49
|
+
// `VITE_DEV_MODE=1` so production builds hide the launcher button
|
|
50
|
+
// (the page itself is still reachable by typing the URL).
|
|
51
|
+
{ packageName: "@mulmoclaude/debug-plugin" },
|
|
52
|
+
];
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Preset plugin loader (#1043 C-2 follow-up).
|
|
2
|
+
//
|
|
3
|
+
// Reads `server/plugins/preset-list.ts` at boot and resolves each
|
|
4
|
+
// entry against the package's installed location via Node's standard
|
|
5
|
+
// module-resolution (`import.meta.resolve`). The package is already
|
|
6
|
+
// laid out by `npm install` / `yarn install`; no tgz unpack step.
|
|
7
|
+
// The result is the same `RuntimePlugin` shape user-installed plugins
|
|
8
|
+
// produce, so both flows share the runtime registry, the dispatch
|
|
9
|
+
// route, and the asset route.
|
|
10
|
+
//
|
|
11
|
+
// Why not a hardcoded `node_modules/<pkg>` join: yarn workspaces
|
|
12
|
+
// hoist deps to the repo root, so the launcher sees the package in
|
|
13
|
+
// `<repo>/node_modules/...` while `<repo>/packages/mulmoclaude/node_modules/`
|
|
14
|
+
// is empty. `import.meta.resolve` walks the same way Node does at
|
|
15
|
+
// runtime, so it finds the package wherever the package manager put
|
|
16
|
+
// it. (npm's flat install doesn't have this issue — it lands under
|
|
17
|
+
// `<package>/node_modules/...` directly. Both work with the same
|
|
18
|
+
// resolver call.)
|
|
19
|
+
//
|
|
20
|
+
// Trust model: the package name comes from the server-side hardcoded
|
|
21
|
+
// preset list, not from user input. We trust the resolved path
|
|
22
|
+
// without an `ensureInsideBase` anchor because there is no base —
|
|
23
|
+
// the resolver returns wherever the package manager chose. The asset
|
|
24
|
+
// route's `realpathSync(plugin.cachePath)` still pins to whatever was
|
|
25
|
+
// resolved here at registration time; a route caller cannot redirect
|
|
26
|
+
// to a different path via URL params.
|
|
27
|
+
//
|
|
28
|
+
// Failures don't abort boot. A missing preset (install drift, rare)
|
|
29
|
+
// logs a warning; healthy presets still register.
|
|
30
|
+
|
|
31
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
32
|
+
import path from "node:path";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
import { PRESET_PLUGINS } from "./preset-list.js";
|
|
35
|
+
import { loadPluginFromCacheDir, type LoaderDeps, type RuntimePlugin } from "./runtime-loader.js";
|
|
36
|
+
import { log } from "../system/logger/index.js";
|
|
37
|
+
|
|
38
|
+
const LOG_PREFIX = "plugins/preset";
|
|
39
|
+
|
|
40
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
41
|
+
const __dirname = path.dirname(__filename);
|
|
42
|
+
|
|
43
|
+
interface PackageJsonShape {
|
|
44
|
+
version?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Resolve the on-disk root of a preset package by walking up from
|
|
48
|
+
* this file looking for `<dir>/node_modules/<pkg>/`. Mirrors Node's
|
|
49
|
+
* CommonJS resolution algorithm so it works in every layout the
|
|
50
|
+
* installer might choose:
|
|
51
|
+
*
|
|
52
|
+
* - yarn workspaces: deps hoisted to the repo root's `node_modules`
|
|
53
|
+
* - npm flat install: package's `node_modules` directly
|
|
54
|
+
* - npm nested install: under a parent package's `node_modules`
|
|
55
|
+
*
|
|
56
|
+
* Why not `import.meta.resolve('<pkg>/package.json')`: many packages
|
|
57
|
+
* (including `@gui-chat-plugin/*`) ship an `exports` field that
|
|
58
|
+
* doesn't expose `./package.json`, so the ESM resolver throws
|
|
59
|
+
* `ERR_PACKAGE_PATH_NOT_EXPORTED`. The walk-up sidesteps the
|
|
60
|
+
* exports gate entirely. */
|
|
61
|
+
function resolvePresetRoot(packageName: string): string | null {
|
|
62
|
+
let dir = __dirname;
|
|
63
|
+
while (true) {
|
|
64
|
+
const candidate = path.join(dir, "node_modules", packageName);
|
|
65
|
+
if (existsSync(path.join(candidate, "package.json"))) {
|
|
66
|
+
return candidate;
|
|
67
|
+
}
|
|
68
|
+
const parent = path.dirname(dir);
|
|
69
|
+
if (parent === dir) return null;
|
|
70
|
+
dir = parent;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function loadOnePreset(packageName: string, deps: LoaderDeps = {}): Promise<RuntimePlugin | null> {
|
|
75
|
+
const cachePath = resolvePresetRoot(packageName);
|
|
76
|
+
if (!cachePath) {
|
|
77
|
+
log.warn(LOG_PREFIX, "preset package not resolvable — run `yarn install`?", { packageName });
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const pkgJsonPath = path.join(cachePath, "package.json");
|
|
81
|
+
let pkg: PackageJsonShape;
|
|
82
|
+
try {
|
|
83
|
+
pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8")) as PackageJsonShape;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
log.warn(LOG_PREFIX, "preset package.json read/parse failed", { packageName, error: String(err) });
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const { version } = pkg;
|
|
89
|
+
if (typeof version !== "string" || version.length === 0) {
|
|
90
|
+
log.warn(LOG_PREFIX, "preset package has no version", { packageName });
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return loadPluginFromCacheDir(packageName, version, cachePath, deps);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Load every preset declared in `server/plugins/preset-list.ts`.
|
|
97
|
+
* Returns the loaded set; failures are logged and silently
|
|
98
|
+
* skipped.
|
|
99
|
+
*
|
|
100
|
+
* Pass `deps.runtimeFactory` from the parent server so factory-shape
|
|
101
|
+
* presets get a real runtime; the MCP child can omit it (definition-
|
|
102
|
+
* only). */
|
|
103
|
+
export async function loadPresetPlugins(deps: LoaderDeps = {}): Promise<RuntimePlugin[]> {
|
|
104
|
+
if (PRESET_PLUGINS.length === 0) return [];
|
|
105
|
+
const loaded: RuntimePlugin[] = [];
|
|
106
|
+
for (const entry of PRESET_PLUGINS) {
|
|
107
|
+
const plugin = await loadOnePreset(entry.packageName, deps);
|
|
108
|
+
if (plugin) loaded.push(plugin);
|
|
109
|
+
}
|
|
110
|
+
log.info(LOG_PREFIX, "loaded", { requested: PRESET_PLUGINS.length, succeeded: loaded.length });
|
|
111
|
+
return loaded;
|
|
112
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Plugin-facing chat API. The host attaches a per-plugin instance of
|
|
2
|
+
// `ChatRuntimeApi` to the `PluginRuntime` it constructs for each
|
|
3
|
+
// plugin (`server/plugins/runtime.ts`).
|
|
4
|
+
//
|
|
5
|
+
// `start()` opens a normal chat seeded with a plugin-supplied first
|
|
6
|
+
// message. The seed is persisted as the first user turn; Claude
|
|
7
|
+
// responds to it as if the user had typed it. Pair the returned
|
|
8
|
+
// `chatId` with `runtime.notifier.publish({ navigateTarget:
|
|
9
|
+
// `/chat/${chatId}`, ... })` so the user can land on the chat from
|
|
10
|
+
// the bell. The chat is permanent — appears in the user's chat list
|
|
11
|
+
// like any other.
|
|
12
|
+
//
|
|
13
|
+
// Plugin authors access this surface via the `MulmoclaudeRuntime`
|
|
14
|
+
// cast. See `runtime-tasks-api.ts` for the same pattern.
|
|
15
|
+
|
|
16
|
+
export interface ChatStartInput {
|
|
17
|
+
/** First user turn. Phrase as the user would — Claude reads it as a
|
|
18
|
+
* user instruction and responds. Visually marked as plugin-seeded
|
|
19
|
+
* in the chat history so the user can tell it came from a plugin,
|
|
20
|
+
* not from them. */
|
|
21
|
+
initialMessage: string;
|
|
22
|
+
/** Role to start the chat in. Defaults to `"general"`. */
|
|
23
|
+
role?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ChatStartResult {
|
|
27
|
+
/** New chat session id. Pair with notifier `navigateTarget` to send
|
|
28
|
+
* the user there. */
|
|
29
|
+
chatId: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ChatRuntimeApi {
|
|
33
|
+
/** Open a new chat seeded with `initialMessage` as the first user
|
|
34
|
+
* turn. Returns the new chat session id. No cap on calls per
|
|
35
|
+
* plugin. Throws if the underlying `startChat` fails (invalid
|
|
36
|
+
* role, internal error). */
|
|
37
|
+
start: (input: ChatStartInput) => Promise<ChatStartResult>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
// Runtime plugin loader (#1043 C-2).
|
|
2
|
+
//
|
|
3
|
+
// At server boot:
|
|
4
|
+
// 1. Read the install ledger (`plugins/plugins.json`).
|
|
5
|
+
// 2. For each entry, ensure the tgz is extracted to
|
|
6
|
+
// `plugins/.cache/<name>/<version>/` (cache hit = skip).
|
|
7
|
+
// 3. Dynamic-import the plugin's `dist/index.js` to pull out
|
|
8
|
+
// `TOOL_DEFINITION`. The plugin module is bundled (per the
|
|
9
|
+
// contract documented in `docs/plugin-development.md`) so its
|
|
10
|
+
// bare imports resolve against the on-disk chunk siblings, not
|
|
11
|
+
// against a non-existent `node_modules/` underneath the cache.
|
|
12
|
+
//
|
|
13
|
+
// This module is called from BOTH the parent server (`server/index.ts`)
|
|
14
|
+
// and the spawned MCP child (`server/agent/mcp-server.ts`) — they share
|
|
15
|
+
// the cache, so the second call is fast (no re-extract).
|
|
16
|
+
//
|
|
17
|
+
// Failures don't abort boot. A bad ledger entry, missing tgz, or a
|
|
18
|
+
// definition that fails to import gets logged and skipped; healthy
|
|
19
|
+
// plugins still load.
|
|
20
|
+
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { execFileSync } from "node:child_process";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { pathToFileURL } from "node:url";
|
|
25
|
+
import type { PluginRuntime, ToolDefinition } from "gui-chat-protocol";
|
|
26
|
+
import { isPluginFactory } from "gui-chat-protocol";
|
|
27
|
+
import { WORKSPACE_PATHS } from "../workspace/paths.js";
|
|
28
|
+
import { readLedger, type LedgerEntry } from "../utils/files/plugins-io.js";
|
|
29
|
+
import { log } from "../system/logger/index.js";
|
|
30
|
+
|
|
31
|
+
const LOG_PREFIX = "plugins/runtime";
|
|
32
|
+
|
|
33
|
+
export interface RuntimePlugin {
|
|
34
|
+
/** npm package name, e.g. `@gui-chat-plugin/weather`. */
|
|
35
|
+
name: string;
|
|
36
|
+
/** Semver string from the tgz's `package.json`. */
|
|
37
|
+
version: string;
|
|
38
|
+
/** Absolute path to the extracted plugin directory under
|
|
39
|
+
* `plugins/.cache/<name>/<version>/`. */
|
|
40
|
+
cachePath: string;
|
|
41
|
+
/** TOOL_DEFINITION export from the plugin's `dist/index.js`. The
|
|
42
|
+
* shape matches static plugins in `plugin-names.ts`, so the same
|
|
43
|
+
* MCP merge / dispatch path applies. */
|
|
44
|
+
definition: ToolDefinition;
|
|
45
|
+
/** Server-side handler the dispatch route calls. The convention
|
|
46
|
+
* across @gui-chat-plugin packages is to export it under the same
|
|
47
|
+
* key as `TOOL_DEFINITION.name` (e.g. weather → `fetchWeather`,
|
|
48
|
+
* browse → `browse`); we capture it at load time so the dispatch
|
|
49
|
+
* route doesn't have to re-resolve. The signature follows
|
|
50
|
+
* gui-chat-protocol's `ToolPluginCore.execute`:
|
|
51
|
+
* `(context: ToolContext, args) => Promise<ToolResult>` — context
|
|
52
|
+
* first, args second. `null` means the module shipped a
|
|
53
|
+
* TOOL_DEFINITION but no matching handler — the dispatch will 500
|
|
54
|
+
* with a useful message. */
|
|
55
|
+
execute: ((context: unknown, args: unknown) => unknown) | null;
|
|
56
|
+
/** Optional short, URL-safe alias for the OAuth callback route
|
|
57
|
+
* (#1162). When the plugin's module exports
|
|
58
|
+
* `OAUTH_CALLBACK_ALIAS = "<alias>"`, the host registers
|
|
59
|
+
* `/api/plugins/runtime/oauth-callback/<alias>` and forwards
|
|
60
|
+
* `kind: "oauthCallback"` dispatch args to this plugin. The
|
|
61
|
+
* package-name-in-path shape was rejected by Spotify Dashboard
|
|
62
|
+
* because of the percent-encoded `@` / `/` characters, so OAuth
|
|
63
|
+
* plugins declare a short alias (e.g. `"spotify"`) instead. Null
|
|
64
|
+
* for non-OAuth plugins. */
|
|
65
|
+
oauthCallbackAlias: string | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface PackageJson {
|
|
69
|
+
name?: string;
|
|
70
|
+
version?: string;
|
|
71
|
+
exports?: string | Record<string, unknown>;
|
|
72
|
+
main?: string;
|
|
73
|
+
module?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;
|
|
77
|
+
|
|
78
|
+
const isToolDefinition = (value: unknown): value is ToolDefinition => {
|
|
79
|
+
if (!isRecord(value)) return false;
|
|
80
|
+
return typeof value.name === "string" && typeof value.description === "string";
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Resolve the entry-point path from a plugin's `package.json`. Covers
|
|
84
|
+
* the four legal `exports` shapes per the Node.js spec, then falls
|
|
85
|
+
* through to `module` / `main` for legacy packages.
|
|
86
|
+
*
|
|
87
|
+
* Legal `exports` forms (Node.js docs):
|
|
88
|
+
* 1. String sugar: `"exports": "./index.js"`
|
|
89
|
+
* 2. Conditional root: `"exports": { "import": "./esm.js", … }`
|
|
90
|
+
* 3. Subpath map: `"exports": { ".": "./index.js", "./util": "./util.js", … }`
|
|
91
|
+
* 4. Subpath + cond.: `"exports": { ".": { "import": "./esm.js", "require": "./cjs.js" } }`
|
|
92
|
+
*
|
|
93
|
+
* Earlier the loader only matched form (4) and fell straight through
|
|
94
|
+
* to `module` / `main`. Real npm packages using forms (1)-(3) failed
|
|
95
|
+
* to load (#1077 review). */
|
|
96
|
+
function resolveEntrySpecifier(pkg: PackageJson): string | null {
|
|
97
|
+
const fromExports = resolveExportsField(pkg.exports);
|
|
98
|
+
if (fromExports) return fromExports;
|
|
99
|
+
if (typeof pkg.module === "string") return pkg.module;
|
|
100
|
+
if (typeof pkg.main === "string") return pkg.main;
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveExportsField(exportsField: PackageJson["exports"]): string | null {
|
|
105
|
+
// Form 1: top-level string sugar.
|
|
106
|
+
// Form A: array fallback chain (e.g. ["./dist/index.js", "./fallback.js"]).
|
|
107
|
+
// Form 2: conditional root (e.g. `{ import, require, default }`).
|
|
108
|
+
// Form 3 / 4: subpath map keyed by `.` — value can be a string,
|
|
109
|
+
// array, or condition object (Codex review iter-2 on #1116).
|
|
110
|
+
const root = pickExportsTarget(exportsField);
|
|
111
|
+
if (root) return root;
|
|
112
|
+
if (isRecord(exportsField)) {
|
|
113
|
+
return pickExportsTarget(exportsField["."]);
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Walk a Node.js `exports` value and return the first string entry
|
|
119
|
+
* point this ESM loader can use. Handles every legal target shape:
|
|
120
|
+
*
|
|
121
|
+
* - string: return it directly.
|
|
122
|
+
* - array: try each element in order; first resolvable wins.
|
|
123
|
+
* Per the spec arrays are a fallback chain — Node tries each
|
|
124
|
+
* until one resolves on disk. We don't probe disk here, so
|
|
125
|
+
* "first string-or-resolvable-condition" approximates that.
|
|
126
|
+
* - condition object: prefer `import` (ESM-specific), then
|
|
127
|
+
* `default` (universal fallback).
|
|
128
|
+
* - everything else (e.g. only `require`, only nested arrays of
|
|
129
|
+
* condition objects with no string targets): null. */
|
|
130
|
+
function pickExportsTarget(value: unknown): string | null {
|
|
131
|
+
if (typeof value === "string") return value;
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
for (const entry of value) {
|
|
134
|
+
const resolved = pickExportsTarget(entry);
|
|
135
|
+
if (resolved) return resolved;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
if (!isRecord(value)) return null;
|
|
140
|
+
// Condition object — `import` first (the ESM-specific condition
|
|
141
|
+
// match), then `default` (the universal fallback that "always
|
|
142
|
+
// matches" per the Node.js spec).
|
|
143
|
+
const fromImport = pickExportsTarget(value.import);
|
|
144
|
+
if (fromImport) return fromImport;
|
|
145
|
+
return pickExportsTarget(value.default);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Sentinel file written at the end of a successful extract. The
|
|
149
|
+
* loader uses its presence (not just `existsSync(cachePath)`) as the
|
|
150
|
+
* cache-validity check, so a partial extract (interrupted tar, ENOSPC
|
|
151
|
+
* half-write) is detected and re-extracted on the next boot instead
|
|
152
|
+
* of becoming a permanent broken state. */
|
|
153
|
+
export const EXTRACT_MARKER = ".extract-complete";
|
|
154
|
+
|
|
155
|
+
export function isCacheValid(cachePath: string): boolean {
|
|
156
|
+
return existsSync(path.join(cachePath, EXTRACT_MARKER));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Run `tar xzf` to extract a tgz into the version-keyed cache slot.
|
|
160
|
+
* `--strip-components=1` drops the `package/` prefix that npm packs
|
|
161
|
+
* add. `execFileSync` (not `execSync`) so paths bypass shell parsing
|
|
162
|
+
* and never trip on metacharacters in workspace paths. Synchronous
|
|
163
|
+
* because boot is single-threaded and the alternative (a stream
|
|
164
|
+
* pipeline) adds dependencies for no benefit.
|
|
165
|
+
*
|
|
166
|
+
* On failure, the partial directory is removed so the next boot
|
|
167
|
+
* re-extracts cleanly (no sticky broken state). The completion
|
|
168
|
+
* marker is written ONLY after tar exits 0 — readers should test
|
|
169
|
+
* `isCacheValid()`, not `existsSync(cachePath)`. */
|
|
170
|
+
function extractTgz(tgzAbs: string, destDir: string): void {
|
|
171
|
+
// Wipe any leftover from a previous failed extract before writing.
|
|
172
|
+
if (existsSync(destDir)) rmSync(destDir, { recursive: true, force: true });
|
|
173
|
+
mkdirSync(destDir, { recursive: true });
|
|
174
|
+
try {
|
|
175
|
+
execFileSync("tar", ["-xzf", tgzAbs, "-C", destDir, "--strip-components=1"], { stdio: "pipe" });
|
|
176
|
+
writeFileSync(path.join(destDir, EXTRACT_MARKER), "");
|
|
177
|
+
} catch (err) {
|
|
178
|
+
// Tear down the partial tree so isCacheValid() stays false next boot.
|
|
179
|
+
try {
|
|
180
|
+
rmSync(destDir, { recursive: true, force: true });
|
|
181
|
+
} catch {
|
|
182
|
+
// best-effort cleanup
|
|
183
|
+
}
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function readPackageJson(cachePath: string): PackageJson | null {
|
|
189
|
+
const pkgPath = path.join(cachePath, "package.json");
|
|
190
|
+
try {
|
|
191
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8")) as PackageJson;
|
|
192
|
+
} catch (err) {
|
|
193
|
+
log.warn(LOG_PREFIX, "package.json read/parse failed", { path: pkgPath, error: String(err) });
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Optional per-plugin runtime factory (#1110). When provided, plugins
|
|
199
|
+
* whose `dist/index.js` exports a factory via `export default
|
|
200
|
+
* definePlugin(...)` get the constructed runtime injected at load
|
|
201
|
+
* time. When omitted, factory plugins are loaded with a stub runtime
|
|
202
|
+
* whose handler call throws — fine for the MCP child process which
|
|
203
|
+
* only needs `TOOL_DEFINITION` for `tools/list`; not OK for the
|
|
204
|
+
* parent server which actually invokes the handler. The parent
|
|
205
|
+
* always passes `runtimeFactory`; the child does not. */
|
|
206
|
+
export interface LoaderDeps {
|
|
207
|
+
runtimeFactory?: (pkgName: string) => PluginRuntime;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Stub runtime — the factory pattern requires SOME runtime to call,
|
|
211
|
+
* even if we only care about extracting `TOOL_DEFINITION`. Methods
|
|
212
|
+
* throw rather than silently no-op so a plugin that mistakenly does
|
|
213
|
+
* I/O at setup time fails loudly during boot instead of at first
|
|
214
|
+
* call.
|
|
215
|
+
*
|
|
216
|
+
* Two host extensions (`tasks`, `chat` — Phase 1 of the Encore plan)
|
|
217
|
+
* are also stubbed here so plugins that destructure `runtime as
|
|
218
|
+
* MulmoclaudeRuntime` and call `tasks.register(...)` at setup time
|
|
219
|
+
* don't crash the definition-only load (e.g. the MCP child
|
|
220
|
+
* extracting `tools/list`). `tasks.register` is a SILENT no-op
|
|
221
|
+
* because tick registration is parent-only by design — the child
|
|
222
|
+
* process has no task manager to register against. `chat.start`
|
|
223
|
+
* matches the throw pattern of `fetch` / `files`, since starting a
|
|
224
|
+
* chat is a parent-only side effect a plugin should never trigger
|
|
225
|
+
* at setup time. The stub deliberately doesn't carry the
|
|
226
|
+
* `MulmoclaudeRuntime` type — the cast lives at the plugin call
|
|
227
|
+
* site (Phase 3 of Encore upstreams these into PluginRuntime). */
|
|
228
|
+
function makeStubRuntime(name: string): PluginRuntime {
|
|
229
|
+
const error = (operation: string) => () => {
|
|
230
|
+
throw new Error(`plugin/${name}: runtime.${operation} unavailable in this process (definition-only load)`);
|
|
231
|
+
};
|
|
232
|
+
const fileOps = {
|
|
233
|
+
read: error("files.read"),
|
|
234
|
+
readBytes: error("files.readBytes"),
|
|
235
|
+
write: error("files.write"),
|
|
236
|
+
readDir: error("files.readDir"),
|
|
237
|
+
stat: error("files.stat"),
|
|
238
|
+
exists: error("files.exists"),
|
|
239
|
+
unlink: error("files.unlink"),
|
|
240
|
+
};
|
|
241
|
+
const stub = {
|
|
242
|
+
pubsub: { publish: () => undefined },
|
|
243
|
+
locale: "en",
|
|
244
|
+
files: { data: fileOps, config: fileOps },
|
|
245
|
+
log: { debug: () => undefined, info: () => undefined, warn: () => undefined, error: () => undefined },
|
|
246
|
+
fetch: error("fetch") as unknown as PluginRuntime["fetch"],
|
|
247
|
+
fetchJson: error("fetchJson") as unknown as PluginRuntime["fetchJson"],
|
|
248
|
+
notifier: {
|
|
249
|
+
publish: error("notifier.publish") as unknown as (...args: unknown[]) => Promise<{ id: string }>,
|
|
250
|
+
clear: error("notifier.clear") as unknown as (id: string) => Promise<void>,
|
|
251
|
+
},
|
|
252
|
+
tasks: {
|
|
253
|
+
// Silent no-op: definition-only loads (MCP child) shouldn't fail
|
|
254
|
+
// just because the plugin declared a tick at setup time.
|
|
255
|
+
register: () => undefined,
|
|
256
|
+
},
|
|
257
|
+
chat: {
|
|
258
|
+
start: error("chat.start") as unknown as (...args: unknown[]) => Promise<{ chatId: string }>,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
// Returned typed as `PluginRuntime` — host extensions (notifier /
|
|
262
|
+
// tasks / chat) are accessed by plugins via the `MulmoclaudeRuntime`
|
|
263
|
+
// cast and thus aren't part of the stub's nominal type yet.
|
|
264
|
+
return stub as unknown as PluginRuntime;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Two carrier shapes (#1110 backward compatibility):
|
|
268
|
+
*
|
|
269
|
+
* 1. **Factory** (`export default definePlugin(setup)`) — `mod.default`
|
|
270
|
+
* is a function. Call it with the plugin's runtime; the return
|
|
271
|
+
* value carries `TOOL_DEFINITION` and the handler keyed by
|
|
272
|
+
* `TOOL_DEFINITION.name`. Closure over runtime gives the handler
|
|
273
|
+
* access to scoped pubsub / files / log without per-call context
|
|
274
|
+
* threading.
|
|
275
|
+
* 2. **Legacy** (`export const TOOL_DEFINITION = ...; export async
|
|
276
|
+
* function <name>(context, args) ...`) — `mod.TOOL_DEFINITION` and
|
|
277
|
+
* `mod[<name>]` directly. Continues to work; existing
|
|
278
|
+
* @gui-chat-plugin packages don't need to migrate.
|
|
279
|
+
*/
|
|
280
|
+
function resolveCarrier(name: string, mod: Record<string, unknown>, deps: LoaderDeps): { carrier: Record<string, unknown>; usingFactory: boolean } {
|
|
281
|
+
const defaultExport = mod.default;
|
|
282
|
+
if (isPluginFactory(defaultExport)) {
|
|
283
|
+
const runtime = deps.runtimeFactory ? deps.runtimeFactory(name) : makeStubRuntime(name);
|
|
284
|
+
return { carrier: defaultExport(runtime) as unknown as Record<string, unknown>, usingFactory: true };
|
|
285
|
+
}
|
|
286
|
+
return { carrier: mod, usingFactory: false };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Pull the executable from a carrier. Factory-shape handlers take
|
|
290
|
+
* `(args)` only (runtime is closed over) so we wrap to discard the
|
|
291
|
+
* legacy `context` arg the dispatch route passes. Legacy-shape
|
|
292
|
+
* handlers keep the `(context, args)` signature unchanged. Returns
|
|
293
|
+
* null when no function is exported under `definition.name` — the
|
|
294
|
+
* plugin still appears in `tools/list` but dispatch logs + 500s. */
|
|
295
|
+
function resolveExecute(
|
|
296
|
+
carrier: Record<string, unknown>,
|
|
297
|
+
definitionName: string,
|
|
298
|
+
usingFactory: boolean,
|
|
299
|
+
): ((context: unknown, args: unknown) => unknown) | null {
|
|
300
|
+
const handler = carrier[definitionName];
|
|
301
|
+
if (typeof handler !== "function") return null;
|
|
302
|
+
if (usingFactory) {
|
|
303
|
+
const factoryHandler = handler as (args: unknown) => unknown;
|
|
304
|
+
return (_context, args) => factoryHandler(args);
|
|
305
|
+
}
|
|
306
|
+
return handler as (context: unknown, args: unknown) => unknown;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Read the optional `OAUTH_CALLBACK_ALIAS` named export. Validated
|
|
310
|
+
* against `^[a-z0-9][a-z0-9-]{0,30}$` — short, lowercase,
|
|
311
|
+
* URL-friendly, can't be `..` or contain `/`. A bad alias is logged
|
|
312
|
+
* and ignored (the plugin still loads; just skips the OAuth route
|
|
313
|
+
* registration). */
|
|
314
|
+
const OAUTH_CALLBACK_ALIAS_RE = /^[a-z0-9][a-z0-9-]{0,30}$/;
|
|
315
|
+
function resolveOauthCallbackAlias(name: string, mod: Record<string, unknown>): string | null {
|
|
316
|
+
const raw = mod.OAUTH_CALLBACK_ALIAS;
|
|
317
|
+
if (raw === undefined) return null;
|
|
318
|
+
if (typeof raw !== "string" || !OAUTH_CALLBACK_ALIAS_RE.test(raw)) {
|
|
319
|
+
log.warn(LOG_PREFIX, "OAUTH_CALLBACK_ALIAS export is not a valid alias — ignoring", { name, raw: String(raw) });
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
return raw;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Load a plugin from an already-extracted cache directory. Pure
|
|
326
|
+
* function — accepts paths explicitly, so tests don't need a real
|
|
327
|
+
* workspace. Returns null on any structural failure (missing
|
|
328
|
+
* package.json, missing TOOL_DEFINITION, broken import); the caller
|
|
329
|
+
* treats nulls as "skip". */
|
|
330
|
+
export async function loadPluginFromCacheDir(name: string, version: string, cachePath: string, deps: LoaderDeps = {}): Promise<RuntimePlugin | null> {
|
|
331
|
+
const pkg = readPackageJson(cachePath);
|
|
332
|
+
if (!pkg) return null;
|
|
333
|
+
const entrySpec = resolveEntrySpecifier(pkg);
|
|
334
|
+
if (!entrySpec) {
|
|
335
|
+
log.warn(LOG_PREFIX, "no entry specifier in package.json — skipping", { name });
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
const entryAbs = path.join(cachePath, entrySpec);
|
|
339
|
+
try {
|
|
340
|
+
const mod = (await import(pathToFileURL(entryAbs).href)) as Record<string, unknown>;
|
|
341
|
+
const { carrier, usingFactory } = resolveCarrier(name, mod, deps);
|
|
342
|
+
const definition = carrier.TOOL_DEFINITION;
|
|
343
|
+
if (!isToolDefinition(definition)) {
|
|
344
|
+
log.warn(LOG_PREFIX, "no TOOL_DEFINITION export — skipping", { name, entrySpec, shape: usingFactory ? "factory" : "legacy" });
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
const execute = resolveExecute(carrier, definition.name, usingFactory);
|
|
348
|
+
if (!execute) {
|
|
349
|
+
log.warn(LOG_PREFIX, "no execute handler matching TOOL_DEFINITION.name — dispatch will fail", {
|
|
350
|
+
name,
|
|
351
|
+
expectedExport: definition.name,
|
|
352
|
+
shape: usingFactory ? "factory" : "legacy",
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
const oauthCallbackAlias = resolveOauthCallbackAlias(name, mod);
|
|
356
|
+
return { name, version, cachePath, definition, execute, oauthCallbackAlias };
|
|
357
|
+
} catch (err) {
|
|
358
|
+
log.error(LOG_PREFIX, "import failed", { name, entrySpec, error: String(err) });
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Lexical anchor: confirm `candidate` resolves inside `base`. Catches
|
|
364
|
+
* malformed ledger entries (`name` containing `../../etc`) before we
|
|
365
|
+
* touch the disk. The asset route trusts registry membership, so a
|
|
366
|
+
* registered cachePath that escaped the base would expose arbitrary
|
|
367
|
+
* files via the unauthenticated GET — this check is the first
|
|
368
|
+
* line of defence (defence-in-depth: realpath after extract is the
|
|
369
|
+
* symlink-escape backstop). Exported for testing. */
|
|
370
|
+
export function ensureInsideBase(candidate: string, base: string): boolean {
|
|
371
|
+
const resolvedCandidate = path.resolve(candidate);
|
|
372
|
+
const resolvedBase = path.resolve(base);
|
|
373
|
+
return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(resolvedBase + path.sep);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function loadOne(entry: LedgerEntry, deps: LoaderDeps = {}): Promise<RuntimePlugin | null> {
|
|
377
|
+
const tgzAbs = path.join(WORKSPACE_PATHS.plugins, entry.tgz);
|
|
378
|
+
const cachePath = path.join(WORKSPACE_PATHS.pluginCache, entry.name, entry.version);
|
|
379
|
+
// Anchor checks BEFORE any disk probe (`existsSync` / `realpath`).
|
|
380
|
+
// The ledger has two separate user-controlled fields — `tgz` and
|
|
381
|
+
// (`name`, `version`) — and each joins against a different base
|
|
382
|
+
// (`WORKSPACE_PATHS.plugins` vs. `pluginCache`). Both must stay
|
|
383
|
+
// inside their respective bases; otherwise even a stat-only probe
|
|
384
|
+
// would touch a path outside the intended roots.
|
|
385
|
+
if (!ensureInsideBase(tgzAbs, WORKSPACE_PATHS.plugins)) {
|
|
386
|
+
log.warn(LOG_PREFIX, "ledger entry tgz escapes plugins root — skipping", { name: entry.name, tgz: entry.tgz });
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
if (!ensureInsideBase(cachePath, WORKSPACE_PATHS.pluginCache)) {
|
|
390
|
+
log.warn(LOG_PREFIX, "ledger entry escapes plugin cache root — skipping", {
|
|
391
|
+
name: entry.name,
|
|
392
|
+
version: entry.version,
|
|
393
|
+
});
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
if (!existsSync(tgzAbs)) {
|
|
397
|
+
log.warn(LOG_PREFIX, "tgz missing — skipping", { name: entry.name, tgz: entry.tgz });
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
if (!isCacheValid(cachePath)) {
|
|
401
|
+
try {
|
|
402
|
+
extractTgz(tgzAbs, cachePath);
|
|
403
|
+
log.info(LOG_PREFIX, "extracted", { name: entry.name, version: entry.version });
|
|
404
|
+
} catch (err) {
|
|
405
|
+
log.error(LOG_PREFIX, "extract failed", { name: entry.name, error: String(err) });
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return loadPluginFromCacheDir(entry.name, entry.version, cachePath, deps);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Read the ledger and load every healthy plugin. Returns the loaded
|
|
413
|
+
* set; failures are logged and silently skipped (see module
|
|
414
|
+
* comment).
|
|
415
|
+
*
|
|
416
|
+
* Pass `deps.runtimeFactory` from the parent server so factory-shape
|
|
417
|
+
* plugins (`export default definePlugin(...)`) get a real runtime.
|
|
418
|
+
* The MCP child can call without deps — its only consumer is
|
|
419
|
+
* `tools/list` which needs `TOOL_DEFINITION` only. */
|
|
420
|
+
export async function loadRuntimePlugins(deps: LoaderDeps = {}): Promise<RuntimePlugin[]> {
|
|
421
|
+
const entries = readLedger();
|
|
422
|
+
if (entries.length === 0) return [];
|
|
423
|
+
const loaded: RuntimePlugin[] = [];
|
|
424
|
+
for (const entry of entries) {
|
|
425
|
+
const plugin = await loadOne(entry, deps);
|
|
426
|
+
if (plugin) loaded.push(plugin);
|
|
427
|
+
}
|
|
428
|
+
log.info(LOG_PREFIX, "loaded", { requested: entries.length, succeeded: loaded.length });
|
|
429
|
+
return loaded;
|
|
430
|
+
}
|