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
|
@@ -19,6 +19,9 @@ import { prependJournalPointer } from "../../agent/prompt.js";
|
|
|
19
19
|
import { buildTranscriptPreamble, isStaleSessionError } from "../../agent/resumeFailover.js";
|
|
20
20
|
import { getOrCreateSession, beginRun, endRun, cancelRun, pushSessionEvent, pushToolResult, getActiveSessionIds } from "../../events/session-store/index.js";
|
|
21
21
|
import { workspacePath } from "../../workspace/workspace.js";
|
|
22
|
+
import { discoverSkills } from "../../workspace/skills/discovery.js";
|
|
23
|
+
import type { Skill } from "../../workspace/skills/types.js";
|
|
24
|
+
import { isRecord } from "../../utils/types.js";
|
|
22
25
|
import { maybeRunJournal } from "../../workspace/journal/index.js";
|
|
23
26
|
import { maybeIndexSession } from "../../workspace/chat-index/index.js";
|
|
24
27
|
import { maybeAppendWikiBacklinks } from "../../workspace/wiki-backlinks/index.js";
|
|
@@ -41,7 +44,9 @@ import { isSessionOrigin, type SessionOrigin } from "../../../src/types/session.
|
|
|
41
44
|
// import { publishNotification } from "../../events/notifications.js";
|
|
42
45
|
import { env } from "../../system/env.js";
|
|
43
46
|
import type { Attachment } from "@mulmobridge/protocol";
|
|
44
|
-
import {
|
|
47
|
+
import { isImagePath, loadImageBase64 } from "../../utils/files/image-store.js";
|
|
48
|
+
import { isAttachmentPath, loadAttachmentBase64, inferMimeFromExtension, saveAttachment } from "../../utils/files/attachment-store.js";
|
|
49
|
+
import { errorMessage } from "../../utils/errors.js";
|
|
45
50
|
|
|
46
51
|
const router = Router();
|
|
47
52
|
const PORT = env.port;
|
|
@@ -73,20 +78,6 @@ router.post(API_ROUTES.agent.internal.toolResult, async (req: Request<object, un
|
|
|
73
78
|
res.json({ ok: outcome.kind === "processed" });
|
|
74
79
|
});
|
|
75
80
|
|
|
76
|
-
// Called by the MCP server to trigger a role switch on the frontend
|
|
77
|
-
interface SwitchRoleBody {
|
|
78
|
-
roleId: string;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
router.post(API_ROUTES.agent.internal.switchRole, async (req: Request<object, unknown, SwitchRoleBody>, res: Response<OkResponse>) => {
|
|
82
|
-
const chatSessionId = getSessionQuery(req);
|
|
83
|
-
pushSessionEvent(chatSessionId, {
|
|
84
|
-
type: EVENT_TYPES.switchRole,
|
|
85
|
-
roleId: req.body.roleId,
|
|
86
|
-
});
|
|
87
|
-
res.json({ ok: true });
|
|
88
|
-
});
|
|
89
|
-
|
|
90
81
|
// Cancel a running agent session by killing the Claude CLI process.
|
|
91
82
|
interface CancelBody {
|
|
92
83
|
chatSessionId: string;
|
|
@@ -111,6 +102,18 @@ export interface StartChatParams {
|
|
|
111
102
|
message: string;
|
|
112
103
|
roleId: string;
|
|
113
104
|
chatSessionId: string;
|
|
105
|
+
/** Bridge-only legacy carrier for "the user picked this image".
|
|
106
|
+
* No in-tree bridge sets it today; it remains on the type so
|
|
107
|
+
* external bridge clients that populate it from older protocol
|
|
108
|
+
* versions continue to work. Only workspace paths
|
|
109
|
+
* (`data/attachments/...` or `artifacts/images/...`) are accepted
|
|
110
|
+
* — `data:` URLs are no longer supported and are dropped with a
|
|
111
|
+
* warn. Bridges that need to ship raw bytes should use the
|
|
112
|
+
* modern `attachments[]` field with `{ mimeType, data }` entries;
|
|
113
|
+
* those get persisted to `data/attachments/YYYY/MM/` server-side
|
|
114
|
+
* and rewritten as path-bearing attachments. The Vue UI never
|
|
115
|
+
* sets this — paste/drop and sidebar picks ride on
|
|
116
|
+
* `attachments[]` as path-only entries directly. */
|
|
114
117
|
selectedImageData?: string;
|
|
115
118
|
attachments?: Attachment[];
|
|
116
119
|
/** Where this session originates (#486). Accepts string for
|
|
@@ -133,6 +136,10 @@ export type StartChatResult = { kind: "started"; chatSessionId: string } | { kin
|
|
|
133
136
|
|
|
134
137
|
export async function startChat(params: StartChatParams): Promise<StartChatResult> {
|
|
135
138
|
const { message, roleId, chatSessionId, selectedImageData, attachments, userTimezone } = params;
|
|
139
|
+
// Bridge-only compat: external bridge clients may still populate
|
|
140
|
+
// `selectedImageData`. Fold it into `attachments` so the rest of
|
|
141
|
+
// this function only deals with one input shape.
|
|
142
|
+
const normalisedAttachments = mergeBridgeSelectedImage(selectedImageData, attachments);
|
|
136
143
|
|
|
137
144
|
if (!message || !roleId || !chatSessionId) {
|
|
138
145
|
return {
|
|
@@ -159,7 +166,6 @@ export async function startChat(params: StartChatParams): Promise<StartChatResul
|
|
|
159
166
|
getOrCreateSession(chatSessionId, {
|
|
160
167
|
roleId,
|
|
161
168
|
resultsFilePath,
|
|
162
|
-
selectedImageData,
|
|
163
169
|
startedAt: now,
|
|
164
170
|
updatedAt: now,
|
|
165
171
|
hasUnread: persistedHasUnread,
|
|
@@ -176,10 +182,40 @@ export async function startChat(params: StartChatParams): Promise<StartChatResul
|
|
|
176
182
|
return { kind: "error", error: "Session is already running", status: 409 };
|
|
177
183
|
}
|
|
178
184
|
|
|
179
|
-
// Run is committed.
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
185
|
+
// Run is committed. Process attachments next so any failure here
|
|
186
|
+
// rolls the run back via `endRun` before we persist or broadcast a
|
|
187
|
+
// user message — leaving an orphan turn on disk when the request
|
|
188
|
+
// ultimately rejects would mislead every viewer of this session.
|
|
189
|
+
// Three things happen in this block, all guarded together:
|
|
190
|
+
// 1. Bridge inline-bytes (`{ data, mimeType }`) get saved to
|
|
191
|
+
// `data/attachments/YYYY/MM/` and rewritten as path-bearing
|
|
192
|
+
// attachments. After this every Attachment has a `path`.
|
|
193
|
+
// 2. `collectAttachedPaths` extracts the workspace paths to
|
|
194
|
+
// persist on the user JSONL line and broadcast on the SSE
|
|
195
|
+
// event so the UI can render chips for the turn.
|
|
196
|
+
// 3. `prepareRequestExtras` loads bytes off disk for the LLM
|
|
197
|
+
// request. With (1) above this is now a pure path-only walk.
|
|
198
|
+
// A malformed body (e.g. `attachments` not an array) or a
|
|
199
|
+
// filesystem I/O failure is logged and converted to a 400 here;
|
|
200
|
+
// beginRun is rolled back so subsequent turns aren't rejected with
|
|
201
|
+
// 409 forever.
|
|
202
|
+
let attachedPaths: string[];
|
|
203
|
+
let extras: RequestExtras;
|
|
204
|
+
try {
|
|
205
|
+
const persistedAttachments = await persistInlineBytesAsPaths(normalisedAttachments);
|
|
206
|
+
attachedPaths = collectAttachedPaths(persistedAttachments);
|
|
207
|
+
extras = await prepareRequestExtras(persistedAttachments);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
log.warn("agent", "attachment processing failed — rolling back run", { chatSessionId, error: errorMessage(err) });
|
|
210
|
+
abortController.abort();
|
|
211
|
+
endRun(chatSessionId);
|
|
212
|
+
return { kind: "error", error: "Invalid attachments payload", status: 400 };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Now persist the user message so callers (and other tabs) see the
|
|
216
|
+
// turn. Metadata first — it powers the sidebar title cache; the
|
|
217
|
+
// append follows so the jsonl is always a superset of what metadata
|
|
218
|
+
// advertised.
|
|
183
219
|
const validOrigin = isSessionOrigin(params.origin) ? params.origin : undefined;
|
|
184
220
|
if (isFirstTurn) {
|
|
185
221
|
await createSessionMeta(chatSessionId, roleId, message, undefined, validOrigin);
|
|
@@ -191,7 +227,10 @@ export async function startChat(params: StartChatParams): Promise<StartChatResul
|
|
|
191
227
|
}
|
|
192
228
|
|
|
193
229
|
// Append user message for this turn
|
|
194
|
-
await appendSessionLine(
|
|
230
|
+
await appendSessionLine(
|
|
231
|
+
chatSessionId,
|
|
232
|
+
JSON.stringify({ source: "user", type: EVENT_TYPES.text, message, ...(attachedPaths.length > 0 ? { attachments: attachedPaths } : {}) }),
|
|
233
|
+
);
|
|
195
234
|
|
|
196
235
|
// Broadcast the user message so other tabs viewing this session
|
|
197
236
|
// see the input in real time. Runs AFTER beginRun so a 409 never
|
|
@@ -200,6 +239,7 @@ export async function startChat(params: StartChatParams): Promise<StartChatResul
|
|
|
200
239
|
type: EVENT_TYPES.text,
|
|
201
240
|
source: "user",
|
|
202
241
|
message,
|
|
242
|
+
...(attachedPaths.length > 0 ? { attachments: attachedPaths } : {}),
|
|
203
243
|
});
|
|
204
244
|
|
|
205
245
|
const role = getRole(roleId);
|
|
@@ -213,7 +253,8 @@ export async function startChat(params: StartChatParams): Promise<StartChatResul
|
|
|
213
253
|
resumed: Boolean(claudeSessionId),
|
|
214
254
|
});
|
|
215
255
|
|
|
216
|
-
const
|
|
256
|
+
const baseMessage = claudeSessionId ? message : prependJournalPointer(message, workspacePath);
|
|
257
|
+
const decoratedMessage = withAttachedFileMarker(baseMessage, extras.attachedFilePaths);
|
|
217
258
|
|
|
218
259
|
runAgentInBackground({
|
|
219
260
|
decoratedMessage,
|
|
@@ -224,7 +265,7 @@ export async function startChat(params: StartChatParams): Promise<StartChatResul
|
|
|
224
265
|
resultsFilePath,
|
|
225
266
|
requestStartedAt,
|
|
226
267
|
toolArgsCache: createArgsCache(),
|
|
227
|
-
attachments:
|
|
268
|
+
attachments: extras.attachments,
|
|
228
269
|
userTimezone,
|
|
229
270
|
origin: validOrigin,
|
|
230
271
|
});
|
|
@@ -234,35 +275,244 @@ export async function startChat(params: StartChatParams): Promise<StartChatResul
|
|
|
234
275
|
|
|
235
276
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
236
277
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
278
|
+
interface RequestExtras {
|
|
279
|
+
attachments: Attachment[] | undefined;
|
|
280
|
+
/** Workspace-relative paths of every file the user attached or
|
|
281
|
+
* selected for this turn, in declaration order. Surfaced to the
|
|
282
|
+
* LLM via one `[Attached file: <path>]` line per entry, prepended
|
|
283
|
+
* to the user message so path-passing tools (e.g. `editImages`)
|
|
284
|
+
* and the LLM itself can reference each file by path.
|
|
285
|
+
* `persistInlineBytesAsPaths` ensures every well-formed attachment
|
|
286
|
+
* carries a path before this runs, so this is empty only when the
|
|
287
|
+
* request had no attachments at all (or every entry was malformed
|
|
288
|
+
* and dropped). */
|
|
289
|
+
attachedFilePaths: string[];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Pluck workspace-relative paths out of `attachments[]`. Used for
|
|
293
|
+
* persistence + broadcast of the user message: the Vue UI renders
|
|
294
|
+
* these as attachment chips next to the chat bubble.
|
|
295
|
+
* `persistInlineBytesAsPaths` runs first, so by the time we get
|
|
296
|
+
* here every well-formed entry already carries a `path` and chips
|
|
297
|
+
* round-trip for bridge attachments too — not just Vue uploads.
|
|
298
|
+
* Order matches declaration order so chip order matches the order
|
|
299
|
+
* the user attached them.
|
|
300
|
+
*
|
|
301
|
+
* Each path is validated against the same allow-list `loadFromPath`
|
|
302
|
+
* uses (`data/attachments/...` or `artifacts/images/...png`). A
|
|
303
|
+
* request can otherwise pin a bogus path on the chat record + SSE
|
|
304
|
+
* + LLM marker even though `loadFromPath` would refuse to read it
|
|
305
|
+
* (#1052 review).
|
|
306
|
+
*
|
|
307
|
+
* Defensive: `Array.isArray` guards against a malformed HTTP body
|
|
308
|
+
* where `attachments` is a truthy non-array. Without it `for...of`
|
|
309
|
+
* would throw and bypass the rollback path that calls `endRun`,
|
|
310
|
+
* leaving the session locked as running (#1052 review). */
|
|
311
|
+
export function collectAttachedPaths(attachments: Attachment[] | undefined): string[] {
|
|
312
|
+
if (!Array.isArray(attachments) || attachments.length === 0) return [];
|
|
313
|
+
const paths: string[] = [];
|
|
314
|
+
for (const att of attachments) {
|
|
315
|
+
if (typeof att.path !== "string" || att.path.length === 0) continue;
|
|
316
|
+
if (!isAttachmentPath(att.path) && !isImagePath(att.path)) continue;
|
|
317
|
+
paths.push(att.path);
|
|
318
|
+
}
|
|
319
|
+
return paths;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Bridge-only compat: external bridge clients may still ship a
|
|
323
|
+
* picked image via `StartChatParams.selectedImageData`. Convert
|
|
324
|
+
* that single value to a synthetic `Attachment` and prepend it to
|
|
325
|
+
* the explicit `attachments` array so downstream code only has to
|
|
326
|
+
* understand one input shape. The Vue UI never reaches this branch
|
|
327
|
+
* — it sends path-only attachments directly. */
|
|
328
|
+
function mergeBridgeSelectedImage(selectedImageData: string | undefined, attachments: Attachment[] | undefined): Attachment[] | undefined {
|
|
329
|
+
const synthetic = synthesiseBridgeAttachment(selectedImageData);
|
|
330
|
+
if (!synthetic) return Array.isArray(attachments) ? attachments : undefined;
|
|
331
|
+
return Array.isArray(attachments) && attachments.length > 0 ? [synthetic, ...attachments] : [synthetic];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Convert a legacy `selectedImageData` carrier to an `Attachment`.
|
|
335
|
+
* Only workspace paths (`data/attachments/...` or `artifacts/images/
|
|
336
|
+
* ...`) are accepted — `data:` URLs are no longer supported. A
|
|
337
|
+
* bridge that still wants to ship raw bytes should populate the
|
|
338
|
+
* modern `attachments[]` field with `{ mimeType, data }` instead;
|
|
339
|
+
* `persistInlineBytesAsPaths` then writes those to
|
|
340
|
+
* `data/attachments/YYYY/MM/` and turns them into path-bearing
|
|
341
|
+
* entries before any other processing. */
|
|
342
|
+
function synthesiseBridgeAttachment(selectedImageData: string | undefined): Attachment | undefined {
|
|
343
|
+
if (!selectedImageData) return undefined;
|
|
344
|
+
if (isAttachmentPath(selectedImageData) || isImagePath(selectedImageData)) {
|
|
345
|
+
return { path: selectedImageData };
|
|
346
|
+
}
|
|
347
|
+
log.warn("agent", "bridge selectedImageData is not a workspace path — dropping (data: URLs are no longer supported)", {
|
|
348
|
+
valuePreview: selectedImageData.slice(0, 64),
|
|
349
|
+
});
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Persist any inline-bytes attachment to disk as a path-bearing
|
|
354
|
+
* entry. Bridges over the socket transport (Telegram, LINE, ...)
|
|
355
|
+
* ship raw bytes via `Attachment.data` + `Attachment.mimeType`; the
|
|
356
|
+
* Vue UI uploads to disk before posting and already carries a
|
|
357
|
+
* `path`. By rewriting inline bytes into
|
|
358
|
+
* `data/attachments/YYYY/MM/<id>.<ext>` here we get two properties
|
|
359
|
+
* the rest of the pipeline relies on:
|
|
360
|
+
*
|
|
361
|
+
* 1. Every well-formed attachment carries a workspace path, so
|
|
362
|
+
* chips can round-trip for bridge turns the same way they do
|
|
363
|
+
* for Vue paste/drop turns (no `data:` chips, no special
|
|
364
|
+
* cases downstream).
|
|
365
|
+
* 2. `prepareRequestExtras` becomes a path-only walk — the inline
|
|
366
|
+
* (`{ data, mimeType }`) shape no longer flows past this layer.
|
|
367
|
+
*
|
|
368
|
+
* Defensive: `Array.isArray` mirrors the guard in
|
|
369
|
+
* `collectAttachedPaths` so a malformed payload doesn't throw and
|
|
370
|
+
* bypass `endRun`. A failed save bubbles up so the caller can
|
|
371
|
+
* reject the turn — silently dropping the file would persist the
|
|
372
|
+
* user message without the attachment they sent and breaks the
|
|
373
|
+
* persistence/broadcast contract this layer is enforcing (#1052
|
|
374
|
+
* review). The caller's try/catch wraps the whole attachment-prep
|
|
375
|
+
* block and rolls the run back via `endRun`, so the failure path
|
|
376
|
+
* is well-defined: the user gets a 400, the session unlocks, and
|
|
377
|
+
* no orphan turn lands in jsonl. Entries with neither path nor
|
|
378
|
+
* inline bytes are still dropped (warn) — that's a malformed entry,
|
|
379
|
+
* not an I/O failure. */
|
|
380
|
+
async function persistInlineBytesAsPaths(attachments: Attachment[] | undefined): Promise<Attachment[] | undefined> {
|
|
381
|
+
if (!Array.isArray(attachments) || attachments.length === 0) return undefined;
|
|
242
382
|
const result: Attachment[] = [];
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
383
|
+
for (const att of attachments) {
|
|
384
|
+
if (typeof att.path === "string" && att.path.length > 0) {
|
|
385
|
+
result.push(att);
|
|
386
|
+
continue;
|
|
247
387
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
388
|
+
if (typeof att.data === "string" && att.data.length > 0 && typeof att.mimeType === "string" && att.mimeType.length > 0) {
|
|
389
|
+
const saved = await saveAttachment(att.data, att.mimeType);
|
|
390
|
+
result.push({ path: saved.relativePath, mimeType: saved.mimeType });
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
log.warn("agent", "attachment has neither path nor inline bytes — dropping");
|
|
251
394
|
}
|
|
252
395
|
return result.length > 0 ? result : undefined;
|
|
253
396
|
}
|
|
254
397
|
|
|
398
|
+
/** Walk `attachments[]` once, loading bytes from disk for every
|
|
399
|
+
* path-bearing entry, and collect every path so the caller can emit
|
|
400
|
+
* one `[Attached file: <path>]` marker per file. Two path roots
|
|
401
|
+
* are accepted:
|
|
402
|
+
*
|
|
403
|
+
* - `data/attachments/...` — paste/drop/file-picker uploads (any
|
|
404
|
+
* MIME type from the chat input's accept list) and the persisted
|
|
405
|
+
* form of bridge inline-bytes attachments. MIME is inferred from
|
|
406
|
+
* the extension chosen at save time.
|
|
407
|
+
* - `artifacts/images/...png` — generated / canvas / edited images
|
|
408
|
+
* a user picked from the sidebar. Always image/png.
|
|
409
|
+
*
|
|
410
|
+
* Bytes are loaded so Claude still "sees" each file as a content
|
|
411
|
+
* block on this turn, AND every path is returned separately so the
|
|
412
|
+
* caller marks them in the LLM-bound message. If a file can't be
|
|
413
|
+
* read, its path hint is still emitted — the LLM knows what was
|
|
414
|
+
* attached and can call Read to load it. Multi-file flows (e.g.
|
|
415
|
+
* paste one image + pick another in the sidebar → "combine these")
|
|
416
|
+
* rely on every path showing up in the marker so `editImages` can
|
|
417
|
+
* receive the full list in `imagePaths`.
|
|
418
|
+
*
|
|
419
|
+
* Inline (`{ data, mimeType }`) entries no longer reach this layer —
|
|
420
|
+
* `persistInlineBytesAsPaths` rewrites them as path-bearing entries
|
|
421
|
+
* before this runs. */
|
|
422
|
+
export async function prepareRequestExtras(attachments: Attachment[] | undefined): Promise<RequestExtras> {
|
|
423
|
+
if (!Array.isArray(attachments) || attachments.length === 0) {
|
|
424
|
+
return { attachments: undefined, attachedFilePaths: [] };
|
|
425
|
+
}
|
|
426
|
+
const result: Attachment[] = [];
|
|
427
|
+
const attachedFilePaths: string[] = [];
|
|
428
|
+
for (const att of attachments) {
|
|
429
|
+
if (typeof att.path !== "string" || att.path.length === 0) {
|
|
430
|
+
log.warn("agent", "attachment has no path after normalisation — dropping");
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
const resolved = await loadFromPath(att.path, att.mimeType);
|
|
434
|
+
if (!resolved) continue;
|
|
435
|
+
// Only emit the `[Attached file: …]` marker when the file was
|
|
436
|
+
// actually loaded — otherwise the LLM gets told a bogus path
|
|
437
|
+
// exists (Codex review on PR #1084 follow-up to #1052).
|
|
438
|
+
result.push(resolved);
|
|
439
|
+
attachedFilePaths.push(att.path);
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
attachments: result.length > 0 ? result : undefined,
|
|
443
|
+
attachedFilePaths,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function loadFromPath(value: string, declaredMimeType: string | undefined): Promise<Attachment | undefined> {
|
|
448
|
+
if (isAttachmentPath(value)) return loadAttachmentFromPath(value, declaredMimeType);
|
|
449
|
+
if (isImagePath(value)) return loadImageFromPath(value, declaredMimeType);
|
|
450
|
+
log.warn("agent", "attachment path is outside allowed roots — dropping", { path: value });
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function loadAttachmentFromPath(value: string, declaredMimeType: string | undefined): Promise<Attachment | undefined> {
|
|
455
|
+
const mimeType = declaredMimeType ?? inferMimeFromExtension(value);
|
|
456
|
+
if (!mimeType) {
|
|
457
|
+
log.warn("agent", "attachment path has unknown extension — skipping bytes", { path: value });
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
const data = await loadAttachmentBase64(value);
|
|
462
|
+
return { mimeType, data, path: value };
|
|
463
|
+
} catch (err) {
|
|
464
|
+
log.warn("agent", "failed to load attachment bytes from path", { path: value, error: errorMessage(err) });
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function loadImageFromPath(value: string, declaredMimeType: string | undefined): Promise<Attachment | undefined> {
|
|
470
|
+
try {
|
|
471
|
+
const data = await loadImageBase64(value);
|
|
472
|
+
return { mimeType: declaredMimeType ?? "image/png", data, path: value };
|
|
473
|
+
} catch (err) {
|
|
474
|
+
log.warn("agent", "failed to load selected-image bytes from path", { path: value, error: errorMessage(err) });
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Drop any path containing characters that could break the
|
|
480
|
+
// `[Attached file: <path>]` marker line (`\r`, `\n`) or its closing
|
|
481
|
+
// bracket (`]`). Such a path would let request-controlled input
|
|
482
|
+
// inject arbitrary text into the privileged prompt prefix — the
|
|
483
|
+
// path itself reaches the marker straight from the request body.
|
|
484
|
+
// CodeRabbit review on #1045.
|
|
485
|
+
const UNSAFE_MARKER_CHARS_RE = /[\r\n\]]/;
|
|
486
|
+
|
|
487
|
+
/** Marker prepended to the LLM-bound user message that tells the
|
|
488
|
+
* model which workspace files are attached / selected for this turn.
|
|
489
|
+
* One `[Attached file: <path>]` line is emitted per path so multi-
|
|
490
|
+
* file flows (e.g. paste one image + pick another → "combine these")
|
|
491
|
+
* surface every path to the model — `editImages` then receives the
|
|
492
|
+
* full list in `imagePaths`. The user's persisted (jsonl) and
|
|
493
|
+
* broadcast (UI) message is the raw text — these marker lines are
|
|
494
|
+
* added strictly on the path to Claude. The system prompt teaches
|
|
495
|
+
* the model how to interpret them. */
|
|
496
|
+
export function withAttachedFileMarker(message: string, attachedFilePaths: string[]): string {
|
|
497
|
+
const safePaths = attachedFilePaths.filter((relPath) => !UNSAFE_MARKER_CHARS_RE.test(relPath));
|
|
498
|
+
if (safePaths.length === 0) return message;
|
|
499
|
+
const markerLines = safePaths.map((relPath) => `[Attached file: ${relPath}]`).join("\n");
|
|
500
|
+
return `${markerLines}\n\n${message}`;
|
|
501
|
+
}
|
|
502
|
+
|
|
255
503
|
// ── HTTP route ──────────────────────────────────────────────────────
|
|
256
504
|
|
|
257
|
-
// HTTP route body — used by the Vue UI only.
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
505
|
+
// HTTP route body — used by the Vue UI only. Paste/drop and sidebar
|
|
506
|
+
// pick both ride on `attachments[]` as path-only entries; the server
|
|
507
|
+
// reads bytes from disk and emits the `[Attached file: <path>]`
|
|
508
|
+
// marker. Bridges go through the socket relay (see chat-service)
|
|
509
|
+
// and supply attachments with inline base64 bytes; both shapes
|
|
510
|
+
// share the same `Attachment` type. See plans/done/refactor-edit-images-array.md.
|
|
261
511
|
interface AgentBody {
|
|
262
512
|
message: string;
|
|
263
513
|
roleId: string;
|
|
264
514
|
chatSessionId: string;
|
|
265
|
-
|
|
515
|
+
attachments?: Attachment[];
|
|
266
516
|
userTimezone?: string;
|
|
267
517
|
}
|
|
268
518
|
|
|
@@ -308,13 +558,77 @@ interface BackgroundRunParams {
|
|
|
308
558
|
// `textAccumulator` collects streaming text chunks so we write
|
|
309
559
|
// one consolidated line to the jsonl instead of per-chunk lines
|
|
310
560
|
// (which would appear as separate cards on session reload).
|
|
561
|
+
//
|
|
562
|
+
// `pendingSkill` is set when a `tool_call` with `toolName === "Skill"`
|
|
563
|
+
// arrives. The next non-empty text flush IS the SKILL.md body that
|
|
564
|
+
// Claude CLI synthesises — gets tagged as `type: "skill"` instead of
|
|
565
|
+
// `type: "text"` and consumes the flag. (#1218)
|
|
566
|
+
//
|
|
567
|
+
// `toolUseId` is tracked alongside the slug so we can recognise the
|
|
568
|
+
// matching `tool_call_result` (which Claude CLI emits between the
|
|
569
|
+
// Skill tool_call and the body) and let it pass without clearing.
|
|
570
|
+
// Any OTHER non-text event between the Skill tool_call and the body
|
|
571
|
+
// flush is treated as a sequence break and clears the flag — covers
|
|
572
|
+
// the leak path Codex iter-2 flagged where a tool_call_result with
|
|
573
|
+
// a different `toolUseId` (or a `claudeSessionId`, or a flush at
|
|
574
|
+
// run-end) would otherwise leave `pendingSkill` set so a much-later
|
|
575
|
+
// unrelated assistant text gets mis-tagged as `type: "skill"`.
|
|
311
576
|
interface EventContext {
|
|
312
577
|
chatSessionId: string;
|
|
313
578
|
resultsFilePath: string;
|
|
314
579
|
toolArgsCache: ReturnType<typeof createArgsCache>;
|
|
315
580
|
textAccumulator: string[];
|
|
581
|
+
pendingSkill: { skillName: string; toolUseId: string } | null;
|
|
316
582
|
}
|
|
317
583
|
|
|
584
|
+
const CLAUDE_CLI_SKILL_BODY_PREFIX = "Base directory for this skill: ";
|
|
585
|
+
|
|
586
|
+
// #1218 — state-machine helpers for `pendingSkill`. Extracted so
|
|
587
|
+
// `handleAgentEvent` stays under the cognitive-complexity cap and so
|
|
588
|
+
// the leak-fix logic (Codex iter-2: clear on mismatched
|
|
589
|
+
// tool_call_result + claudeSessionId, in addition to the iter-1
|
|
590
|
+
// "non-Skill tool_call" clear) lives in one named place.
|
|
591
|
+
//
|
|
592
|
+
// When the model invokes a skill, Claude CLI emits the SKILL.md
|
|
593
|
+
// body as the next assistant text. We track that expectation here:
|
|
594
|
+
// the structural signal is `toolName === "Skill"` + `args.skill`
|
|
595
|
+
// slug, not a body-text prefix, so it survives any rewording Claude
|
|
596
|
+
// CLI might do to the synthesised body.
|
|
597
|
+
|
|
598
|
+
// Narrow the helpers to only the slot they read/mutate so the
|
|
599
|
+
// state-machine unit test in test/agent/ doesn't have to construct
|
|
600
|
+
// the full `EventContext` (chatSessionId, resultsFilePath, etc).
|
|
601
|
+
type PendingSkillSlot = Pick<EventContext, "pendingSkill">;
|
|
602
|
+
|
|
603
|
+
function updatePendingSkillOnToolCall(ctx: PendingSkillSlot, event: { toolName: string; toolUseId: string; args: unknown }): void {
|
|
604
|
+
// Any non-Skill tool_call resets the pending state. Without this,
|
|
605
|
+
// a Skill call followed by another tool (Bash, etc.) without a
|
|
606
|
+
// body flush in between would leak `pendingSkill` and miscategorise
|
|
607
|
+
// a later unrelated assistant text as a skill body.
|
|
608
|
+
if (event.toolName !== "Skill") {
|
|
609
|
+
ctx.pendingSkill = null;
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const skillSlug = isRecord(event.args) && typeof event.args.skill === "string" ? event.args.skill : null;
|
|
613
|
+
ctx.pendingSkill = skillSlug ? { skillName: skillSlug, toolUseId: event.toolUseId } : null;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// The Skill's own tool_call_result (matching toolUseId) carries
|
|
617
|
+
// "Launching skill: X" content; the body follows in the next text
|
|
618
|
+
// event so we leave `pendingSkill` set. A tool_call_result with any
|
|
619
|
+
// OTHER id means a different tool's result interleaved before the
|
|
620
|
+
// body — sequence broken, clear the flag so a later unrelated
|
|
621
|
+
// assistant text isn't mis-tagged as `type: "skill"`.
|
|
622
|
+
function updatePendingSkillOnToolCallResult(ctx: PendingSkillSlot, toolUseId: string): void {
|
|
623
|
+
if (ctx.pendingSkill && toolUseId !== ctx.pendingSkill.toolUseId) {
|
|
624
|
+
ctx.pendingSkill = null;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Exported for the unit test in test/agent/test_pendingSkillStateMachine.ts.
|
|
629
|
+
export const _updatePendingSkillOnToolCallForTest = updatePendingSkillOnToolCall;
|
|
630
|
+
export const _updatePendingSkillOnToolCallResultForTest = updatePendingSkillOnToolCallResult;
|
|
631
|
+
|
|
318
632
|
// Returns true if the event was handled "out of band" (no pub-sub
|
|
319
633
|
// broadcast, no jsonl append). Right now only `claudeSessionId`
|
|
320
634
|
// events fall into that bucket — they update meta and are otherwise
|
|
@@ -323,6 +637,10 @@ interface EventContext {
|
|
|
323
637
|
async function handleAgentEvent(event: Awaited<ReturnType<typeof runAgent>> extends AsyncGenerator<infer E> ? E : never, ctx: EventContext): Promise<void> {
|
|
324
638
|
if (event.type === EVENT_TYPES.claudeSessionId) {
|
|
325
639
|
await flushTextAccumulator(ctx);
|
|
640
|
+
// claudeSessionId is a meta event — never part of a Skill→body
|
|
641
|
+
// sequence. Clear pendingSkill so a flag set earlier in the run
|
|
642
|
+
// can't leak into a later unrelated assistant text.
|
|
643
|
+
ctx.pendingSkill = null;
|
|
326
644
|
await setClaudeId(ctx.chatSessionId, event.id);
|
|
327
645
|
return;
|
|
328
646
|
}
|
|
@@ -340,6 +658,7 @@ async function handleAgentEvent(event: Awaited<ReturnType<typeof runAgent>> exte
|
|
|
340
658
|
// lose already-streamed text.
|
|
341
659
|
await flushTextAccumulator(ctx);
|
|
342
660
|
if (event.type === EVENT_TYPES.toolCall) {
|
|
661
|
+
updatePendingSkillOnToolCall(ctx, event);
|
|
343
662
|
log.info("agent-tool", "call", {
|
|
344
663
|
chatSessionId: ctx.chatSessionId,
|
|
345
664
|
toolName: event.toolName,
|
|
@@ -347,6 +666,7 @@ async function handleAgentEvent(event: Awaited<ReturnType<typeof runAgent>> exte
|
|
|
347
666
|
argsPreview: previewJson(event.args),
|
|
348
667
|
});
|
|
349
668
|
} else if (event.type === EVENT_TYPES.toolCallResult) {
|
|
669
|
+
updatePendingSkillOnToolCallResult(ctx, event.toolUseId);
|
|
350
670
|
// Look up the toolName from the cache *before* recordToolEvent
|
|
351
671
|
// runs (it deletes the cache entry on result).
|
|
352
672
|
const cached = ctx.toolArgsCache.get(event.toolUseId);
|
|
@@ -373,11 +693,26 @@ async function handleAgentEvent(event: Awaited<ReturnType<typeof runAgent>> exte
|
|
|
373
693
|
// jsonl line. Called at the end of each agent run (success or error)
|
|
374
694
|
// so the session transcript has exactly one assistant text entry
|
|
375
695
|
// per response, not N per-chunk entries.
|
|
696
|
+
//
|
|
697
|
+
// When `ctx.pendingSkill` is set (preceding tool_call had
|
|
698
|
+
// `toolName === "Skill"`), the flushed text is the SKILL.md body
|
|
699
|
+
// Claude CLI synthesised — write it as `type: "skill"` instead of
|
|
700
|
+
// `type: "text"` and consume the flag (#1218).
|
|
376
701
|
async function flushTextAccumulator(ctx: EventContext): Promise<void> {
|
|
377
702
|
if (ctx.textAccumulator.length === 0) return;
|
|
378
703
|
const fullText = ctx.textAccumulator.join("");
|
|
379
704
|
ctx.textAccumulator.length = 0;
|
|
380
705
|
if (!fullText) return;
|
|
706
|
+
|
|
707
|
+
// Empty-string flushes (already handled above) don't consume
|
|
708
|
+
// pendingSkill — only the actual skill body should clear it.
|
|
709
|
+
const skill = ctx.pendingSkill;
|
|
710
|
+
ctx.pendingSkill = null;
|
|
711
|
+
|
|
712
|
+
if (skill) {
|
|
713
|
+
await writeSkillEntry(ctx, skill.skillName, fullText);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
381
716
|
await appendSessionLine(
|
|
382
717
|
ctx.chatSessionId,
|
|
383
718
|
JSON.stringify({
|
|
@@ -388,6 +723,133 @@ async function flushTextAccumulator(ctx: EventContext): Promise<void> {
|
|
|
388
723
|
);
|
|
389
724
|
}
|
|
390
725
|
|
|
726
|
+
// Resolve the loaded skill against `discoverSkills()` to attach
|
|
727
|
+
// scope + path metadata, then write the consolidated assistant entry
|
|
728
|
+
// as `type: "skill"`. The body's full text is preserved in `message`
|
|
729
|
+
// (archival + the canvas's expand-on-click affordance). A live SSE
|
|
730
|
+
// `type: "skill"` event is also broadcast so observing tabs can
|
|
731
|
+
// replace the streamed text bubble with a collapsed skill card
|
|
732
|
+
// without waiting for a session reload.
|
|
733
|
+
//
|
|
734
|
+
// Claude CLI sometimes concatenates the LLM's actual reply to the
|
|
735
|
+
// synthesised SKILL.md body in the same text block (no `tool_call`
|
|
736
|
+
// or `content_block_stop` boundary between them — see PR #1220
|
|
737
|
+
// comment for the shiritori reproducer). We split that here using
|
|
738
|
+
// the SKILL.md body on disk as a structural delimiter; the reply
|
|
739
|
+
// portion gets persisted as a SECOND entry of `type: "text"` so it
|
|
740
|
+
// stays visible after the user collapses the skill card.
|
|
741
|
+
async function writeSkillEntry(ctx: EventContext, skillName: string, body: string): Promise<void> {
|
|
742
|
+
const resolved = await resolveSkillMetadata(skillName);
|
|
743
|
+
// Canary: skill detection is sequence-based (not body-prefix based),
|
|
744
|
+
// but we still cross-check the prefix as a format-drift signal.
|
|
745
|
+
// If Claude CLI ever changes its synthesised body shape, this warn
|
|
746
|
+
// surfaces before any user-visible regression.
|
|
747
|
+
if (!body.startsWith(CLAUDE_CLI_SKILL_BODY_PREFIX)) {
|
|
748
|
+
log.warn("agent", "Skill tool followed by text NOT starting with the expected Claude CLI prefix — body shape may have changed", {
|
|
749
|
+
skillName,
|
|
750
|
+
expectedPrefix: CLAUDE_CLI_SKILL_BODY_PREFIX,
|
|
751
|
+
actualPreview: body.slice(0, 80),
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
// A second canary: the SKILL.md body should appear verbatim inside
|
|
755
|
+
// the synthesised text. Failure means either discovery missed the
|
|
756
|
+
// skill (already logged by `resolveSkillMetadata`) or Claude CLI
|
|
757
|
+
// changed how it inlines the body (worth investigating).
|
|
758
|
+
if (resolved.body && !body.includes(resolved.body.trim())) {
|
|
759
|
+
log.warn("agent", "Claude CLI text does not contain the SKILL.md body verbatim — body split may be incorrect", {
|
|
760
|
+
skillName,
|
|
761
|
+
bodyBytes: body.length,
|
|
762
|
+
skillFileBytes: resolved.body.length,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
const { skillPart, replyPart } = splitSkillAndReply(body, resolved.body);
|
|
766
|
+
|
|
767
|
+
const skillPayload = {
|
|
768
|
+
source: "assistant",
|
|
769
|
+
type: EVENT_TYPES.skill,
|
|
770
|
+
skillName,
|
|
771
|
+
skillScope: resolved.scope,
|
|
772
|
+
skillPath: resolved.path,
|
|
773
|
+
skillDescription: resolved.description,
|
|
774
|
+
message: skillPart,
|
|
775
|
+
};
|
|
776
|
+
pushSessionEvent(ctx.chatSessionId, skillPayload as Record<string, unknown>);
|
|
777
|
+
await appendSessionLine(ctx.chatSessionId, JSON.stringify(skillPayload));
|
|
778
|
+
|
|
779
|
+
if (replyPart) {
|
|
780
|
+
const textPayload = { source: "assistant", type: EVENT_TYPES.text, message: replyPart };
|
|
781
|
+
pushSessionEvent(ctx.chatSessionId, textPayload as Record<string, unknown>);
|
|
782
|
+
await appendSessionLine(ctx.chatSessionId, JSON.stringify(textPayload));
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
interface SkillMetadata {
|
|
787
|
+
scope: "user" | "project" | "unknown";
|
|
788
|
+
path: string | null;
|
|
789
|
+
/** From the SKILL.md frontmatter `description:` field. Used by the
|
|
790
|
+
* host's collapsed-skill card — Claude CLI strips frontmatter from
|
|
791
|
+
* the synthesised body, so the renderer can't re-extract this from
|
|
792
|
+
* `message`. Resolved here from `discoverSkills()` instead. */
|
|
793
|
+
description: string | null;
|
|
794
|
+
/** SKILL.md body (frontmatter already stripped by `discoverSkills`).
|
|
795
|
+
* Used as a structural delimiter to split the text Claude CLI
|
|
796
|
+
* emits — the body Claude CLI inlines is character-for-character
|
|
797
|
+
* this same string, with the LLM's actual reply concatenated
|
|
798
|
+
* after. */
|
|
799
|
+
body: string | null;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async function resolveSkillMetadata(skillName: string): Promise<SkillMetadata> {
|
|
803
|
+
try {
|
|
804
|
+
const skills: Skill[] = await discoverSkills({ workspaceRoot: workspacePath });
|
|
805
|
+
const found = skills.find((skill) => skill.name === skillName);
|
|
806
|
+
if (!found) return { scope: "unknown", path: null, description: null, body: null };
|
|
807
|
+
return { scope: found.source, path: found.path, description: found.description, body: found.body };
|
|
808
|
+
} catch (err) {
|
|
809
|
+
// Discovery failure is benign — keep tagging the entry so the UI
|
|
810
|
+
// can still collapse it; just leave metadata empty.
|
|
811
|
+
log.warn("agent", "skill metadata lookup failed — emitting entry without scope/path/description/body", {
|
|
812
|
+
skillName,
|
|
813
|
+
error: err instanceof Error ? err.message : String(err),
|
|
814
|
+
});
|
|
815
|
+
return { scope: "unknown", path: null, description: null, body: null };
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** Split the consolidated text Claude CLI emits after a `Skill`
|
|
820
|
+
* tool_call into the SKILL.md body half (synthesised by Claude CLI)
|
|
821
|
+
* and the LLM's actual reply half. Without this, the entire blob
|
|
822
|
+
* gets tagged `type: "skill"`, so when the user collapses the
|
|
823
|
+
* card their actual reply disappears (#1218 reproducer: shiritori
|
|
824
|
+
* skill, where the body ends with a "respond now" instruction and
|
|
825
|
+
* the LLM's first move is concatenated to the same text block).
|
|
826
|
+
*
|
|
827
|
+
* Structural split: the SKILL.md body is on disk, available via
|
|
828
|
+
* `discoverSkills()`. We find that exact substring inside the
|
|
829
|
+
* message and slice. Optional `ARGUMENTS: <user_input>` line that
|
|
830
|
+
* Claude CLI appends when the SKILL.md uses `{{ARGUMENTS}}` is
|
|
831
|
+
* consumed too. Returns the whole message as `skillPart` with
|
|
832
|
+
* empty `replyPart` when `skillBody` is empty (discovery missed)
|
|
833
|
+
* or not found verbatim (Claude CLI changed the inlining format —
|
|
834
|
+
* the canary log warn fires in that case). */
|
|
835
|
+
function splitSkillAndReply(message: string, skillBody: string | null): { skillPart: string; replyPart: string } {
|
|
836
|
+
if (!skillBody) return { skillPart: message, replyPart: "" };
|
|
837
|
+
const trimmedBody = skillBody.trim();
|
|
838
|
+
if (!trimmedBody) return { skillPart: message, replyPart: "" };
|
|
839
|
+
const idx = message.indexOf(trimmedBody);
|
|
840
|
+
if (idx < 0) return { skillPart: message, replyPart: "" };
|
|
841
|
+
let cursor = idx + trimmedBody.length;
|
|
842
|
+
// Skip the optional ARGUMENTS line + trailing whitespace.
|
|
843
|
+
const argMatch = /^\s*ARGUMENTS:[^\n]*\n?/.exec(message.slice(cursor));
|
|
844
|
+
if (argMatch) cursor += argMatch[0].length;
|
|
845
|
+
const skillPart = message.slice(0, cursor).trimEnd();
|
|
846
|
+
const replyPart = message.slice(cursor).replace(/^\s+/, "");
|
|
847
|
+
return { skillPart, replyPart };
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Exported for the unit test in test/agent/test_skillBodySplit.ts.
|
|
851
|
+
export const _splitSkillAndReplyForTest = splitSkillAndReply;
|
|
852
|
+
|
|
391
853
|
// Helper kept commented (instead of deleted) alongside the
|
|
392
854
|
// publishNotification call below — see the duplicate-notification
|
|
393
855
|
// comment near `endRun()` in `runAgentInBackground` for context.
|
|
@@ -418,6 +880,7 @@ async function runAgentInBackground(params: BackgroundRunParams): Promise<void>
|
|
|
418
880
|
resultsFilePath,
|
|
419
881
|
toolArgsCache,
|
|
420
882
|
textAccumulator: [],
|
|
883
|
+
pendingSkill: null,
|
|
421
884
|
};
|
|
422
885
|
|
|
423
886
|
// Retry budget for the stale `--resume` id fail-over (#211). Only
|
|
@@ -431,17 +894,17 @@ async function runAgentInBackground(params: BackgroundRunParams): Promise<void>
|
|
|
431
894
|
try {
|
|
432
895
|
while (true) {
|
|
433
896
|
let staleSessionDetected = false;
|
|
434
|
-
for await (const event of runAgent(
|
|
435
|
-
currentMessage,
|
|
897
|
+
for await (const event of runAgent({
|
|
898
|
+
message: currentMessage,
|
|
436
899
|
role,
|
|
437
900
|
workspacePath,
|
|
438
|
-
chatSessionId,
|
|
439
|
-
PORT,
|
|
440
|
-
currentClaudeSessionId,
|
|
901
|
+
sessionId: chatSessionId,
|
|
902
|
+
port: PORT,
|
|
903
|
+
claudeSessionId: currentClaudeSessionId,
|
|
441
904
|
abortSignal,
|
|
442
905
|
attachments,
|
|
443
906
|
userTimezone,
|
|
444
|
-
)) {
|
|
907
|
+
})) {
|
|
445
908
|
if (failoverAttemptsRemaining > 0 && event.type === EVENT_TYPES.error && typeof event.message === "string" && isStaleSessionError(event.message)) {
|
|
446
909
|
// Swallow the error — we're about to recover. `break`
|
|
447
910
|
// abandons the current generator; since the event is only
|