@tenex-chat/backend 0.9.1
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/README.md +194 -0
- package/dist/backend-wrapper.cjs +3 -0
- package/dist/src/index.js +331928 -0
- package/package.json +103 -0
- package/src/agents/AgentRegistry.ts +418 -0
- package/src/agents/AgentStorage.ts +1133 -0
- package/src/agents/ConfigResolver.ts +229 -0
- package/src/agents/agent-installer.ts +236 -0
- package/src/agents/agent-loader.ts +241 -0
- package/src/agents/constants.ts +82 -0
- package/src/agents/errors.ts +48 -0
- package/src/agents/execution/AgentExecutor.ts +561 -0
- package/src/agents/execution/ExecutionContextFactory.ts +112 -0
- package/src/agents/execution/MessageCompiler.ts +597 -0
- package/src/agents/execution/MessageSyncer.ts +100 -0
- package/src/agents/execution/PostCompletionChecker.ts +278 -0
- package/src/agents/execution/ProgressMonitor.ts +50 -0
- package/src/agents/execution/RALResolver.ts +177 -0
- package/src/agents/execution/SessionManager.ts +181 -0
- package/src/agents/execution/StreamCallbacks.ts +312 -0
- package/src/agents/execution/StreamExecutionHandler.ts +579 -0
- package/src/agents/execution/StreamSetup.ts +313 -0
- package/src/agents/execution/ToolEventHandlers.ts +239 -0
- package/src/agents/execution/ToolExecutionTracker.ts +498 -0
- package/src/agents/execution/ToolResultUtils.ts +97 -0
- package/src/agents/execution/ToolSupervisionWrapper.ts +174 -0
- package/src/agents/execution/constants.ts +16 -0
- package/src/agents/execution/index.ts +3 -0
- package/src/agents/execution/types.ts +96 -0
- package/src/agents/execution/utils.ts +26 -0
- package/src/agents/index.ts +4 -0
- package/src/agents/script-installer.ts +266 -0
- package/src/agents/supervision/SupervisorLLMService.ts +253 -0
- package/src/agents/supervision/SupervisorOrchestrator.ts +471 -0
- package/src/agents/supervision/heuristics/ConsecutiveToolsWithoutTodoHeuristic.ts +73 -0
- package/src/agents/supervision/heuristics/DelegationClaimHeuristic.ts +80 -0
- package/src/agents/supervision/heuristics/HeuristicRegistry.ts +114 -0
- package/src/agents/supervision/heuristics/PendingTodosHeuristic.ts +93 -0
- package/src/agents/supervision/heuristics/SilentAgentHeuristic.ts +54 -0
- package/src/agents/supervision/heuristics/index.ts +5 -0
- package/src/agents/supervision/index.ts +28 -0
- package/src/agents/supervision/registerHeuristics.ts +110 -0
- package/src/agents/supervision/supervisionHealthCheck.ts +123 -0
- package/src/agents/supervision/types.ts +171 -0
- package/src/agents/tool-names.ts +46 -0
- package/src/agents/tool-normalization.ts +184 -0
- package/src/agents/types/index.ts +2 -0
- package/src/agents/types/runtime.ts +74 -0
- package/src/agents/types/storage.ts +145 -0
- package/src/commands/agent/import/index.ts +6 -0
- package/src/commands/agent/import/openclaw-distiller.ts +57 -0
- package/src/commands/agent/import/openclaw-reader.ts +141 -0
- package/src/commands/agent/import/openclaw.ts +154 -0
- package/src/commands/agent/index.ts +6 -0
- package/src/commands/agent.ts +215 -0
- package/src/commands/daemon.ts +198 -0
- package/src/commands/doctor.ts +134 -0
- package/src/commands/setup/embed.ts +228 -0
- package/src/commands/setup/global-system-prompt.ts +223 -0
- package/src/commands/setup/image.ts +179 -0
- package/src/commands/setup/index.ts +16 -0
- package/src/commands/setup/interactive.ts +95 -0
- package/src/commands/setup/llm.ts +38 -0
- package/src/commands/setup/onboarding.ts +294 -0
- package/src/commands/setup/providers.ts +27 -0
- package/src/constants.ts +34 -0
- package/src/conversations/ConversationDiskReader.ts +148 -0
- package/src/conversations/ConversationRegistry.ts +728 -0
- package/src/conversations/ConversationStore.ts +868 -0
- package/src/conversations/MessageBuilder.ts +866 -0
- package/src/conversations/executionTime.ts +62 -0
- package/src/conversations/formatters/DelegationXmlFormatter.ts +64 -0
- package/src/conversations/formatters/ThreadedConversationFormatter.ts +303 -0
- package/src/conversations/formatters/index.ts +9 -0
- package/src/conversations/formatters/utils/MessageFormatter.ts +46 -0
- package/src/conversations/formatters/utils/TimestampFormatter.ts +56 -0
- package/src/conversations/formatters/utils/TreeBuilder.ts +131 -0
- package/src/conversations/formatters/utils/TreeRenderer.ts +49 -0
- package/src/conversations/index.ts +2 -0
- package/src/conversations/persistence/ToolMessageStorage.ts +143 -0
- package/src/conversations/search/ConversationIndexManager.ts +393 -0
- package/src/conversations/search/QueryParser.ts +114 -0
- package/src/conversations/search/SearchEngine.ts +175 -0
- package/src/conversations/search/SnippetExtractor.ts +345 -0
- package/src/conversations/search/embeddings/ConversationEmbeddingService.ts +484 -0
- package/src/conversations/search/embeddings/ConversationIndexingJob.ts +320 -0
- package/src/conversations/search/embeddings/IndexingStateManager.ts +338 -0
- package/src/conversations/search/embeddings/index.ts +18 -0
- package/src/conversations/search/index.ts +49 -0
- package/src/conversations/search/types.ts +124 -0
- package/src/conversations/services/CategoryManager.ts +160 -0
- package/src/conversations/services/ConversationResolver.ts +296 -0
- package/src/conversations/services/ConversationSummarizer.ts +234 -0
- package/src/conversations/services/MetadataDebounceManager.ts +188 -0
- package/src/conversations/services/index.ts +2 -0
- package/src/conversations/types.ts +148 -0
- package/src/conversations/utils/content-utils.ts +69 -0
- package/src/conversations/utils/image-placeholder.ts +281 -0
- package/src/conversations/utils/image-url-utils.ts +171 -0
- package/src/conversations/utils/multimodal-content.ts +90 -0
- package/src/conversations/utils/tool-result-truncator.ts +159 -0
- package/src/daemon/Daemon.ts +1883 -0
- package/src/daemon/ProjectRuntime.ts +657 -0
- package/src/daemon/RestartState.ts +152 -0
- package/src/daemon/RuntimeLifecycle.ts +268 -0
- package/src/daemon/SubscriptionManager.ts +305 -0
- package/src/daemon/UnixSocketTransport.ts +318 -0
- package/src/daemon/filters/SubscriptionFilterBuilder.ts +119 -0
- package/src/daemon/index.ts +9 -0
- package/src/daemon/routing/DaemonRouter.ts +491 -0
- package/src/daemon/types.ts +150 -0
- package/src/daemon/utils/routing-log.ts +76 -0
- package/src/daemon/utils/telemetry.ts +173 -0
- package/src/event-handler/agentDeletion.ts +383 -0
- package/src/event-handler/index.ts +749 -0
- package/src/event-handler/newConversation.ts +165 -0
- package/src/event-handler/project.ts +166 -0
- package/src/event-handler/reply.ts +18 -0
- package/src/events/NDKAgentDefinition.ts +292 -0
- package/src/events/NDKAgentLesson.ts +106 -0
- package/src/events/NDKEventMetadata.ts +34 -0
- package/src/events/NDKMCPTool.ts +60 -0
- package/src/events/NDKProjectStatus.ts +384 -0
- package/src/events/index.ts +4 -0
- package/src/index.ts +126 -0
- package/src/lib/agent-home.ts +334 -0
- package/src/lib/error-formatter.ts +200 -0
- package/src/lib/fs/filesystem.ts +128 -0
- package/src/lib/fs/index.ts +1 -0
- package/src/lib/json-parser.ts +30 -0
- package/src/lib/string.ts +15 -0
- package/src/lib/time.ts +74 -0
- package/src/llm/ChunkHandler.ts +277 -0
- package/src/llm/FinishHandler.ts +250 -0
- package/src/llm/LLMConfigEditor.ts +154 -0
- package/src/llm/LLMServiceFactory.ts +230 -0
- package/src/llm/MessageProcessor.ts +90 -0
- package/src/llm/RecordingState.ts +37 -0
- package/src/llm/StreamPublisher.ts +40 -0
- package/src/llm/TracingUtils.ts +77 -0
- package/src/llm/chunk-validators.ts +57 -0
- package/src/llm/constants.ts +6 -0
- package/src/llm/index.ts +12 -0
- package/src/llm/meta/MetaModelResolver.ts +352 -0
- package/src/llm/meta/index.ts +11 -0
- package/src/llm/middleware/flight-recorder.ts +188 -0
- package/src/llm/providers/MockProvider.ts +332 -0
- package/src/llm/providers/agent/ClaudeCodeProvider.ts +343 -0
- package/src/llm/providers/agent/ClaudeCodeToolsAdapter.ts +203 -0
- package/src/llm/providers/agent/CodexAppServerProvider.ts +214 -0
- package/src/llm/providers/agent/CodexAppServerToolsAdapter.ts +91 -0
- package/src/llm/providers/agent/index.ts +10 -0
- package/src/llm/providers/base/AgentProvider.ts +107 -0
- package/src/llm/providers/base/BaseProvider.ts +114 -0
- package/src/llm/providers/base/StandardProvider.ts +38 -0
- package/src/llm/providers/base/index.ts +9 -0
- package/src/llm/providers/index.ts +106 -0
- package/src/llm/providers/key-manager.ts +238 -0
- package/src/llm/providers/ollama-models.ts +105 -0
- package/src/llm/providers/openrouter-models.ts +102 -0
- package/src/llm/providers/provider-ids.ts +18 -0
- package/src/llm/providers/registry/ProviderRegistry.ts +414 -0
- package/src/llm/providers/registry/index.ts +7 -0
- package/src/llm/providers/standard/AnthropicProvider.ts +71 -0
- package/src/llm/providers/standard/OllamaProvider.ts +59 -0
- package/src/llm/providers/standard/OpenAIProvider.ts +44 -0
- package/src/llm/providers/standard/OpenRouterProvider.ts +103 -0
- package/src/llm/providers/standard/index.ts +10 -0
- package/src/llm/providers/types.ts +194 -0
- package/src/llm/providers/usage-metadata.ts +78 -0
- package/src/llm/service.ts +713 -0
- package/src/llm/types.ts +167 -0
- package/src/llm/utils/ConfigurationManager.ts +650 -0
- package/src/llm/utils/ConfigurationTester.ts +229 -0
- package/src/llm/utils/ModelSelector.ts +212 -0
- package/src/llm/utils/ProviderConfigUI.ts +177 -0
- package/src/llm/utils/claudeCodePromptCompiler.ts +141 -0
- package/src/llm/utils/codex-models.ts +53 -0
- package/src/llm/utils/context-window-cache.ts +30 -0
- package/src/llm/utils/models-dev-cache.ts +267 -0
- package/src/llm/utils/provider-setup.ts +50 -0
- package/src/llm/utils/tool-errors.ts +78 -0
- package/src/llm/utils/usage.ts +74 -0
- package/src/logging/EventRoutingLogger.ts +205 -0
- package/src/nostr/AgentEventDecoder.ts +357 -0
- package/src/nostr/AgentEventEncoder.ts +677 -0
- package/src/nostr/AgentProfilePublisher.ts +657 -0
- package/src/nostr/AgentPublisher.ts +437 -0
- package/src/nostr/BlossomService.ts +226 -0
- package/src/nostr/InterventionPublisher.ts +132 -0
- package/src/nostr/TagExtractor.ts +228 -0
- package/src/nostr/collectEvents.ts +83 -0
- package/src/nostr/constants.ts +38 -0
- package/src/nostr/encryption.ts +26 -0
- package/src/nostr/index.ts +31 -0
- package/src/nostr/keys.ts +17 -0
- package/src/nostr/kinds.ts +37 -0
- package/src/nostr/ndkClient.ts +72 -0
- package/src/nostr/relays.ts +43 -0
- package/src/nostr/trace-context.ts +39 -0
- package/src/nostr/types.ts +227 -0
- package/src/nostr/utils.ts +84 -0
- package/src/prompts/core/FragmentRegistry.ts +30 -0
- package/src/prompts/core/PromptBuilder.ts +98 -0
- package/src/prompts/core/index.ts +3 -0
- package/src/prompts/core/types.ts +13 -0
- package/src/prompts/fragments/00-global-system-prompt.ts +44 -0
- package/src/prompts/fragments/01-agent-identity.ts +69 -0
- package/src/prompts/fragments/02-agent-home-directory.ts +114 -0
- package/src/prompts/fragments/03-system-reminders-explanation.ts +14 -0
- package/src/prompts/fragments/04-relay-configuration.ts +38 -0
- package/src/prompts/fragments/05-delegation-chain.ts +45 -0
- package/src/prompts/fragments/06-agent-todos.ts +74 -0
- package/src/prompts/fragments/06-todo-usage-guidance.ts +34 -0
- package/src/prompts/fragments/07-meta-project-context.ts +234 -0
- package/src/prompts/fragments/08-active-conversations.ts +382 -0
- package/src/prompts/fragments/09-recent-conversations.ts +153 -0
- package/src/prompts/fragments/10-referenced-article.ts +21 -0
- package/src/prompts/fragments/11-nudges.ts +134 -0
- package/src/prompts/fragments/12-skills.ts +127 -0
- package/src/prompts/fragments/13-available-nudges.ts +122 -0
- package/src/prompts/fragments/15-available-agents.ts +53 -0
- package/src/prompts/fragments/16-stay-in-your-lane.ts +41 -0
- package/src/prompts/fragments/17-todo-before-delegation.ts +39 -0
- package/src/prompts/fragments/20-voice-mode.ts +62 -0
- package/src/prompts/fragments/22-scheduled-tasks.ts +175 -0
- package/src/prompts/fragments/24-retrieved-lessons.ts +26 -0
- package/src/prompts/fragments/25-rag-instructions.ts +333 -0
- package/src/prompts/fragments/26-mcp-resources.ts +237 -0
- package/src/prompts/fragments/27-memorized-reports.ts +77 -0
- package/src/prompts/fragments/28-agent-directed-monitoring.ts +32 -0
- package/src/prompts/fragments/29-rag-collections.ts +50 -0
- package/src/prompts/fragments/30-worktree-context.ts +98 -0
- package/src/prompts/fragments/31-agents-md-guidance.ts +96 -0
- package/src/prompts/fragments/32-process-metrics.ts +72 -0
- package/src/prompts/fragments/debug-mode.ts +48 -0
- package/src/prompts/fragments/delegation-completion.ts +44 -0
- package/src/prompts/fragments/index.ts +91 -0
- package/src/prompts/index.ts +21 -0
- package/src/prompts/utils/systemPromptBuilder.ts +777 -0
- package/src/scripts/migrate-prefix-index.ts +157 -0
- package/src/services/AgentDefinitionMonitor.ts +701 -0
- package/src/services/ConfigService.ts +723 -0
- package/src/services/CooldownRegistry.ts +199 -0
- package/src/services/LLMOperationsRegistry.ts +424 -0
- package/src/services/OwnerAgentListService.ts +354 -0
- package/src/services/PubkeyService.ts +308 -0
- package/src/services/agents/AgentMetadataStore.ts +72 -0
- package/src/services/agents/AgentResolution.ts +59 -0
- package/src/services/agents/EscalationService.ts +281 -0
- package/src/services/agents/NDKAgentDiscovery.ts +95 -0
- package/src/services/agents/index.ts +7 -0
- package/src/services/agents-md/AgentsMdService.ts +184 -0
- package/src/services/agents-md/SystemReminderInjector.ts +238 -0
- package/src/services/agents-md/index.ts +11 -0
- package/src/services/apns/APNsClient.ts +203 -0
- package/src/services/apns/APNsService.ts +358 -0
- package/src/services/apns/index.ts +11 -0
- package/src/services/apns/types.ts +80 -0
- package/src/services/compression/CompressionService.ts +445 -0
- package/src/services/compression/compression-schema.ts +28 -0
- package/src/services/compression/compression-types.ts +74 -0
- package/src/services/compression/compression-utils.ts +587 -0
- package/src/services/config/types.ts +394 -0
- package/src/services/dispatch/AgentDispatchService.ts +937 -0
- package/src/services/dispatch/AgentRouter.ts +181 -0
- package/src/services/dispatch/DelegationCompletionHandler.ts +232 -0
- package/src/services/embedding/EmbeddingProvider.ts +188 -0
- package/src/services/embedding/index.ts +5 -0
- package/src/services/event-context/EventContextService.ts +108 -0
- package/src/services/event-context/index.ts +2 -0
- package/src/services/heuristics/ContextBuilder.ts +106 -0
- package/src/services/heuristics/HeuristicEngine.ts +200 -0
- package/src/services/heuristics/formatters.ts +58 -0
- package/src/services/heuristics/index.ts +12 -0
- package/src/services/heuristics/rules/index.ts +25 -0
- package/src/services/heuristics/rules/todoBeforeDelegation.ts +69 -0
- package/src/services/heuristics/rules/todoReminderOnToolUse.ts +63 -0
- package/src/services/heuristics/types.ts +144 -0
- package/src/services/image/ImageGenerationService.ts +389 -0
- package/src/services/image/index.ts +12 -0
- package/src/services/intervention/InterventionService.ts +1352 -0
- package/src/services/intervention/index.ts +7 -0
- package/src/services/mcp/MCPManager.ts +683 -0
- package/src/services/mcp/McpNotificationDelivery.ts +139 -0
- package/src/services/mcp/McpSubscriptionService.ts +653 -0
- package/src/services/mcp/mcpInstaller.ts +130 -0
- package/src/services/nip46/Nip46SigningLog.ts +81 -0
- package/src/services/nip46/Nip46SigningService.ts +467 -0
- package/src/services/nip46/index.ts +4 -0
- package/src/services/nudge/NudgeService.ts +224 -0
- package/src/services/nudge/NudgeWhitelistService.ts +382 -0
- package/src/services/nudge/index.ts +5 -0
- package/src/services/nudge/types.ts +83 -0
- package/src/services/projects/ProjectContext.ts +672 -0
- package/src/services/projects/ProjectContextStore.ts +102 -0
- package/src/services/projects/index.ts +6 -0
- package/src/services/prompt-compiler/index.ts +15 -0
- package/src/services/prompt-compiler/prompt-compiler-service.ts +1143 -0
- package/src/services/pubkey-gate/PubkeyGateService.ts +93 -0
- package/src/services/pubkey-gate/index.ts +1 -0
- package/src/services/rag/EmbeddingProviderFactory.ts +292 -0
- package/src/services/rag/LanceDBMaintenanceService.ts +211 -0
- package/src/services/rag/RAGDatabaseService.ts +173 -0
- package/src/services/rag/RAGOperations.ts +682 -0
- package/src/services/rag/RAGService.ts +240 -0
- package/src/services/rag/RagSubscriptionService.ts +618 -0
- package/src/services/rag/rag-utils.ts +174 -0
- package/src/services/ral/PendingDelegationsRegistry.ts +168 -0
- package/src/services/ral/RALRegistry.ts +2782 -0
- package/src/services/ral/index.ts +4 -0
- package/src/services/ral/types.ts +292 -0
- package/src/services/reports/LocalReportStore.ts +380 -0
- package/src/services/reports/ReportEmbeddingService.ts +430 -0
- package/src/services/reports/ReportService.ts +440 -0
- package/src/services/reports/articleUtils.ts +52 -0
- package/src/services/reports/index.ts +7 -0
- package/src/services/scheduling/SchedulerService.ts +1057 -0
- package/src/services/scheduling/errors.ts +14 -0
- package/src/services/scheduling/index.ts +7 -0
- package/src/services/scheduling/utils.ts +77 -0
- package/src/services/search/SearchProviderRegistry.ts +78 -0
- package/src/services/search/UnifiedSearchService.ts +218 -0
- package/src/services/search/index.ts +47 -0
- package/src/services/search/projectFilter.ts +22 -0
- package/src/services/search/providers/ConversationSearchProvider.ts +48 -0
- package/src/services/search/providers/LessonSearchProvider.ts +75 -0
- package/src/services/search/providers/ReportSearchProvider.ts +49 -0
- package/src/services/search/types.ts +144 -0
- package/src/services/skill/SkillService.ts +482 -0
- package/src/services/skill/index.ts +2 -0
- package/src/services/skill/types.ts +70 -0
- package/src/services/status/OperationsStatusService.ts +276 -0
- package/src/services/status/ProjectStatusService.ts +522 -0
- package/src/services/status/index.ts +11 -0
- package/src/services/storage/PrefixKVStore.ts +242 -0
- package/src/services/storage/index.ts +1 -0
- package/src/services/system-reminder/SystemReminderUtils.ts +96 -0
- package/src/services/system-reminder/index.ts +7 -0
- package/src/services/trust-pubkeys/TrustPubkeyService.ts +325 -0
- package/src/services/trust-pubkeys/index.ts +2 -0
- package/src/telemetry/ConversationSpanManager.ts +111 -0
- package/src/telemetry/EventLoopMonitor.ts +206 -0
- package/src/telemetry/LLMSpanRegistry.ts +20 -0
- package/src/telemetry/NostrSpanProcessor.ts +89 -0
- package/src/telemetry/ToolCallSpanProcessor.ts +66 -0
- package/src/telemetry/diagnostics.ts +27 -0
- package/src/telemetry/setup.ts +120 -0
- package/src/tools/implementations/agents_discover.ts +121 -0
- package/src/tools/implementations/agents_hire.ts +127 -0
- package/src/tools/implementations/agents_list.ts +96 -0
- package/src/tools/implementations/agents_publish.ts +611 -0
- package/src/tools/implementations/agents_read.ts +173 -0
- package/src/tools/implementations/agents_write.ts +200 -0
- package/src/tools/implementations/ask.ts +411 -0
- package/src/tools/implementations/change_model.ts +141 -0
- package/src/tools/implementations/conversation_get.ts +661 -0
- package/src/tools/implementations/conversation_list.ts +377 -0
- package/src/tools/implementations/conversation_search.ts +370 -0
- package/src/tools/implementations/delegate.ts +327 -0
- package/src/tools/implementations/delegate_crossproject.ts +209 -0
- package/src/tools/implementations/delegate_followup.ts +300 -0
- package/src/tools/implementations/fs_edit.ts +162 -0
- package/src/tools/implementations/fs_glob.ts +182 -0
- package/src/tools/implementations/fs_grep.ts +513 -0
- package/src/tools/implementations/fs_read.ts +332 -0
- package/src/tools/implementations/fs_write.ts +113 -0
- package/src/tools/implementations/generate_image.ts +259 -0
- package/src/tools/implementations/home_fs.ts +515 -0
- package/src/tools/implementations/kill.ts +651 -0
- package/src/tools/implementations/learn.ts +166 -0
- package/src/tools/implementations/lesson-formatter.ts +38 -0
- package/src/tools/implementations/lesson_delete.ts +164 -0
- package/src/tools/implementations/lesson_get.ts +105 -0
- package/src/tools/implementations/lessons_list.ts +153 -0
- package/src/tools/implementations/mcp_resource_read.ts +161 -0
- package/src/tools/implementations/mcp_subscribe.ts +158 -0
- package/src/tools/implementations/mcp_subscription_stop.ts +85 -0
- package/src/tools/implementations/nostr_fetch.ts +149 -0
- package/src/tools/implementations/nostr_publish_as_user.ts +353 -0
- package/src/tools/implementations/project_list.ts +146 -0
- package/src/tools/implementations/rag_add_documents.ts +573 -0
- package/src/tools/implementations/rag_create_collection.ts +65 -0
- package/src/tools/implementations/rag_delete_collection.ts +68 -0
- package/src/tools/implementations/rag_list_collections.ts +77 -0
- package/src/tools/implementations/rag_query.ts +107 -0
- package/src/tools/implementations/rag_subscription_create.ts +105 -0
- package/src/tools/implementations/rag_subscription_delete.ts +80 -0
- package/src/tools/implementations/rag_subscription_get.ts +123 -0
- package/src/tools/implementations/rag_subscription_list.ts +128 -0
- package/src/tools/implementations/report_delete.ts +79 -0
- package/src/tools/implementations/report_read.ts +160 -0
- package/src/tools/implementations/report_write.ts +278 -0
- package/src/tools/implementations/reports_list.ts +77 -0
- package/src/tools/implementations/schedule_task.ts +104 -0
- package/src/tools/implementations/schedule_task_cancel.ts +62 -0
- package/src/tools/implementations/schedule_task_once.ts +128 -0
- package/src/tools/implementations/schedule_tasks_list.ts +79 -0
- package/src/tools/implementations/search.ts +160 -0
- package/src/tools/implementations/shell.ts +553 -0
- package/src/tools/implementations/todo.ts +260 -0
- package/src/tools/implementations/upload_blob.ts +381 -0
- package/src/tools/implementations/web_fetch.ts +153 -0
- package/src/tools/implementations/web_search.ts +250 -0
- package/src/tools/registry.ts +670 -0
- package/src/tools/types.ts +177 -0
- package/src/tools/utils.ts +256 -0
- package/src/types/event-ids.ts +320 -0
- package/src/types/index.ts +46 -0
- package/src/utils/agentFetcher.ts +107 -0
- package/src/utils/cli-error.ts +29 -0
- package/src/utils/conversation-id.ts +27 -0
- package/src/utils/conversation-utils.ts +1 -0
- package/src/utils/delegation-chain.ts +357 -0
- package/src/utils/error-handler.ts +42 -0
- package/src/utils/git/gitignore.ts +69 -0
- package/src/utils/git/index.ts +2 -0
- package/src/utils/git/initializeGitRepo.ts +204 -0
- package/src/utils/git/worktree.ts +260 -0
- package/src/utils/lessonFormatter.ts +70 -0
- package/src/utils/lessonTrust.ts +24 -0
- package/src/utils/lockfile.ts +123 -0
- package/src/utils/logger.ts +149 -0
- package/src/utils/nostr-entity-parser.ts +365 -0
- package/src/utils/process.ts +49 -0
- package/src/wrapper.ts +262 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* McpSubscriptionService - Manages MCP resource subscriptions with notification delivery
|
|
3
|
+
*
|
|
4
|
+
* Enables agents to subscribe to MCP resource updates within a conversation context.
|
|
5
|
+
* When a notification arrives, it delivers a system-reminder message to the agent
|
|
6
|
+
* in the existing conversation and triggers a new AgentExecutor run.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Persistent subscriptions across restarts (JSON file)
|
|
10
|
+
* - Notification delivery as system-reminder messages
|
|
11
|
+
* - Automatic re-subscription on initialization
|
|
12
|
+
* - Per-conversation subscription tracking
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from "node:fs/promises";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { config } from "@/services/ConfigService";
|
|
18
|
+
import { getProjectContext, isProjectContextInitialized } from "@/services/projects";
|
|
19
|
+
import { logger } from "@/utils/logger";
|
|
20
|
+
import { trace } from "@opentelemetry/api";
|
|
21
|
+
|
|
22
|
+
export enum McpSubscriptionStatus {
|
|
23
|
+
ACTIVE = "ACTIVE",
|
|
24
|
+
ERROR = "ERROR",
|
|
25
|
+
STOPPED = "STOPPED",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface McpSubscription {
|
|
29
|
+
/** Unique subscription ID */
|
|
30
|
+
id: string;
|
|
31
|
+
/** Agent pubkey that created the subscription */
|
|
32
|
+
agentPubkey: string;
|
|
33
|
+
/** Agent slug for display purposes */
|
|
34
|
+
agentSlug: string;
|
|
35
|
+
/** MCP server name */
|
|
36
|
+
serverName: string;
|
|
37
|
+
/** Resource URI being subscribed to */
|
|
38
|
+
resourceUri: string;
|
|
39
|
+
/** Conversation ID where notifications should be delivered */
|
|
40
|
+
conversationId: string;
|
|
41
|
+
/** Root event ID of the conversation (for event routing) */
|
|
42
|
+
rootEventId: string;
|
|
43
|
+
/** Project ID (NIP-33 a-tag format) */
|
|
44
|
+
projectId: string;
|
|
45
|
+
/** Human-readable description */
|
|
46
|
+
description: string;
|
|
47
|
+
/** Current subscription status */
|
|
48
|
+
status: McpSubscriptionStatus;
|
|
49
|
+
/** Number of notifications received */
|
|
50
|
+
notificationsReceived: number;
|
|
51
|
+
/** Timestamp of last notification */
|
|
52
|
+
lastNotificationAt?: number;
|
|
53
|
+
/** Last error message if status is ERROR */
|
|
54
|
+
lastError?: string;
|
|
55
|
+
/** Creation timestamp */
|
|
56
|
+
createdAt: number;
|
|
57
|
+
/** Last update timestamp */
|
|
58
|
+
updatedAt: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Callback type for delivering notifications to conversations.
|
|
63
|
+
* Injected by the initialization layer to avoid circular dependencies.
|
|
64
|
+
*/
|
|
65
|
+
export type NotificationDeliveryHandler = (
|
|
66
|
+
subscription: McpSubscription,
|
|
67
|
+
content: string
|
|
68
|
+
) => Promise<void>;
|
|
69
|
+
|
|
70
|
+
/** Minimal MCPManager interface needed for reading resources */
|
|
71
|
+
interface MCPManagerLike {
|
|
72
|
+
readResource(serverName: string, uri: string): Promise<{ contents: Array<Record<string, unknown>> }>;
|
|
73
|
+
isServerRunning(serverName: string): boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class McpSubscriptionService {
|
|
77
|
+
private static instance: McpSubscriptionService;
|
|
78
|
+
private subscriptions: Map<string, McpSubscription> = new Map();
|
|
79
|
+
private persistencePath: string;
|
|
80
|
+
private isInitialized = false;
|
|
81
|
+
private notificationHandler: NotificationDeliveryHandler | null = null;
|
|
82
|
+
/** Per-subscription handler removal functions (returned by MCPManager.addResourceNotificationHandler) */
|
|
83
|
+
private handlerRemovers: Map<string, () => void> = new Map();
|
|
84
|
+
/** Ref-count of active subscriptions per "server::resource" key */
|
|
85
|
+
private resourceRefCounts: Map<string, number> = new Map();
|
|
86
|
+
/** Previously-seen content item IDs per subscription (for delta tracking) */
|
|
87
|
+
private contentSnapshots: Map<string, Set<string>> = new Map();
|
|
88
|
+
|
|
89
|
+
private constructor() {
|
|
90
|
+
const tenexDir = config.getConfigPath();
|
|
91
|
+
this.persistencePath = path.join(tenexDir, "mcp_subscriptions.json");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public static getInstance(): McpSubscriptionService {
|
|
95
|
+
if (!McpSubscriptionService.instance) {
|
|
96
|
+
McpSubscriptionService.instance = new McpSubscriptionService();
|
|
97
|
+
}
|
|
98
|
+
return McpSubscriptionService.instance;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Reset the singleton instance (for testing)
|
|
103
|
+
*/
|
|
104
|
+
public static resetInstance(): void {
|
|
105
|
+
McpSubscriptionService.instance = undefined as unknown as McpSubscriptionService;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Set the notification delivery handler.
|
|
110
|
+
* Must be called before initialize() for notifications to work.
|
|
111
|
+
*/
|
|
112
|
+
public setNotificationHandler(handler: NotificationDeliveryHandler): void {
|
|
113
|
+
this.notificationHandler = handler;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Initialize the service and restore subscriptions from disk.
|
|
118
|
+
* Re-subscribes all active subscriptions with the MCP servers.
|
|
119
|
+
*/
|
|
120
|
+
public async initialize(): Promise<void> {
|
|
121
|
+
if (this.isInitialized) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const tenexDir = path.dirname(this.persistencePath);
|
|
127
|
+
await fs.mkdir(tenexDir, { recursive: true });
|
|
128
|
+
|
|
129
|
+
await this.loadSubscriptions();
|
|
130
|
+
|
|
131
|
+
// Re-subscribe all active subscriptions
|
|
132
|
+
for (const subscription of this.subscriptions.values()) {
|
|
133
|
+
if (subscription.status === McpSubscriptionStatus.ACTIVE) {
|
|
134
|
+
try {
|
|
135
|
+
await this.setupMcpSubscription(subscription);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
subscription.status = McpSubscriptionStatus.ERROR;
|
|
138
|
+
subscription.lastError = error instanceof Error ? error.message : String(error);
|
|
139
|
+
subscription.updatedAt = Date.now();
|
|
140
|
+
logger.warn(`Failed to re-establish subscription '${subscription.id}'`, {
|
|
141
|
+
error: subscription.lastError,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await this.saveSubscriptions();
|
|
148
|
+
this.isInitialized = true;
|
|
149
|
+
|
|
150
|
+
logger.info(`McpSubscriptionService initialized with ${this.subscriptions.size} subscriptions`);
|
|
151
|
+
trace.getActiveSpan()?.addEvent("mcp_subscription.initialized", {
|
|
152
|
+
"subscriptions.count": this.subscriptions.size,
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logger.error("Failed to initialize McpSubscriptionService", { error });
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Create a new MCP resource subscription.
|
|
162
|
+
*/
|
|
163
|
+
public async createSubscription(params: {
|
|
164
|
+
agentPubkey: string;
|
|
165
|
+
agentSlug: string;
|
|
166
|
+
serverName: string;
|
|
167
|
+
resourceUri: string;
|
|
168
|
+
conversationId: string;
|
|
169
|
+
rootEventId: string;
|
|
170
|
+
projectId: string;
|
|
171
|
+
description: string;
|
|
172
|
+
}): Promise<McpSubscription> {
|
|
173
|
+
const id = this.generateSubscriptionId();
|
|
174
|
+
|
|
175
|
+
const subscription: McpSubscription = {
|
|
176
|
+
id,
|
|
177
|
+
agentPubkey: params.agentPubkey,
|
|
178
|
+
agentSlug: params.agentSlug,
|
|
179
|
+
serverName: params.serverName,
|
|
180
|
+
resourceUri: params.resourceUri,
|
|
181
|
+
conversationId: params.conversationId,
|
|
182
|
+
rootEventId: params.rootEventId,
|
|
183
|
+
projectId: params.projectId,
|
|
184
|
+
description: params.description,
|
|
185
|
+
status: McpSubscriptionStatus.ACTIVE,
|
|
186
|
+
notificationsReceived: 0,
|
|
187
|
+
createdAt: Date.now(),
|
|
188
|
+
updatedAt: Date.now(),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Setup MCP subscription with notification handler
|
|
192
|
+
await this.setupMcpSubscription(subscription);
|
|
193
|
+
|
|
194
|
+
this.subscriptions.set(id, subscription);
|
|
195
|
+
await this.saveSubscriptions();
|
|
196
|
+
|
|
197
|
+
logger.info(`Created MCP subscription '${id}'`, {
|
|
198
|
+
agent: params.agentSlug,
|
|
199
|
+
server: params.serverName,
|
|
200
|
+
resource: params.resourceUri,
|
|
201
|
+
conversation: params.conversationId.substring(0, 12),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
trace.getActiveSpan()?.addEvent("mcp_subscription.created", {
|
|
205
|
+
"subscription.id": id,
|
|
206
|
+
"subscription.server": params.serverName,
|
|
207
|
+
"subscription.resource": params.resourceUri,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return subscription;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Stop and remove a subscription.
|
|
215
|
+
*/
|
|
216
|
+
public async stopSubscription(subscriptionId: string, agentPubkey: string): Promise<boolean> {
|
|
217
|
+
const subscription = this.subscriptions.get(subscriptionId);
|
|
218
|
+
|
|
219
|
+
if (!subscription) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Authorization check
|
|
224
|
+
if (subscription.agentPubkey !== agentPubkey) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
// Unsubscribe from MCP server
|
|
230
|
+
await this.teardownMcpSubscription(subscription);
|
|
231
|
+
|
|
232
|
+
// Remove from in-memory state
|
|
233
|
+
this.subscriptions.delete(subscriptionId);
|
|
234
|
+
this.contentSnapshots.delete(subscriptionId);
|
|
235
|
+
await this.saveSubscriptions();
|
|
236
|
+
|
|
237
|
+
logger.info(`Stopped MCP subscription '${subscriptionId}'`);
|
|
238
|
+
trace.getActiveSpan()?.addEvent("mcp_subscription.stopped", {
|
|
239
|
+
"subscription.id": subscriptionId,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return true;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
logger.error(`Failed to stop subscription '${subscriptionId}'`, { error });
|
|
245
|
+
// Still remove from our tracking even if unsubscribe fails
|
|
246
|
+
this.subscriptions.delete(subscriptionId);
|
|
247
|
+
this.contentSnapshots.delete(subscriptionId);
|
|
248
|
+
await this.saveSubscriptions();
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get all active subscriptions for an agent in a conversation.
|
|
255
|
+
*/
|
|
256
|
+
public getSubscriptionsForAgent(
|
|
257
|
+
agentPubkey: string,
|
|
258
|
+
conversationId?: string
|
|
259
|
+
): McpSubscription[] {
|
|
260
|
+
const results: McpSubscription[] = [];
|
|
261
|
+
for (const sub of this.subscriptions.values()) {
|
|
262
|
+
if (sub.agentPubkey !== agentPubkey) continue;
|
|
263
|
+
if (conversationId && sub.conversationId !== conversationId) continue;
|
|
264
|
+
results.push(sub);
|
|
265
|
+
}
|
|
266
|
+
return results;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get a subscription by ID.
|
|
271
|
+
*/
|
|
272
|
+
public getSubscription(subscriptionId: string): McpSubscription | undefined {
|
|
273
|
+
return this.subscriptions.get(subscriptionId);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Check if an agent has any active subscriptions.
|
|
278
|
+
*/
|
|
279
|
+
public hasActiveSubscriptions(agentPubkey: string): boolean {
|
|
280
|
+
for (const sub of this.subscriptions.values()) {
|
|
281
|
+
if (sub.agentPubkey === agentPubkey && sub.status === McpSubscriptionStatus.ACTIVE) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Check if an agent has any subscriptions that can be stopped (ACTIVE or ERROR).
|
|
290
|
+
* Used for dynamic tool injection of mcp_subscription_stop.
|
|
291
|
+
*/
|
|
292
|
+
public hasStoppableSubscriptions(agentPubkey: string): boolean {
|
|
293
|
+
for (const sub of this.subscriptions.values()) {
|
|
294
|
+
if (sub.agentPubkey !== agentPubkey) continue;
|
|
295
|
+
if (sub.status === McpSubscriptionStatus.ACTIVE || sub.status === McpSubscriptionStatus.ERROR) {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Setup MCP resource subscription and notification listener.
|
|
304
|
+
* Uses dispatcher pattern for handlers (no clobbering) and ref-counting for resources.
|
|
305
|
+
*
|
|
306
|
+
* IMPORTANT: Captures mcpManager reference at setup time so push notification
|
|
307
|
+
* handlers can read resources without depending on AsyncLocalStorage context.
|
|
308
|
+
* MCPManager.initialize() runs outside projectContextStore.run(), so push
|
|
309
|
+
* notification callbacks from the MCP SDK fire without project context.
|
|
310
|
+
*/
|
|
311
|
+
private async setupMcpSubscription(subscription: McpSubscription): Promise<void> {
|
|
312
|
+
if (!isProjectContextInitialized()) {
|
|
313
|
+
throw new Error("Project context not available for MCP subscription setup");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const projectCtx = getProjectContext();
|
|
317
|
+
const mcpManager = projectCtx.mcpManager;
|
|
318
|
+
|
|
319
|
+
if (!mcpManager) {
|
|
320
|
+
throw new Error("MCPManager not available in project context");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!mcpManager.isServerRunning(subscription.serverName)) {
|
|
324
|
+
throw new Error(`MCP server '${subscription.serverName}' is not running`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Capture mcpManager reference for use in the notification callback.
|
|
328
|
+
// Push notifications fire from MCP SDK's transport read callbacks, which
|
|
329
|
+
// execute outside projectContextStore.run() scope. Without this capture,
|
|
330
|
+
// the handler would fail to get mcpManager via getProjectContext().
|
|
331
|
+
const capturedMcpManager: MCPManagerLike = mcpManager;
|
|
332
|
+
|
|
333
|
+
// Register per-subscription notification handler (dispatcher-safe, no clobbering)
|
|
334
|
+
const removeHandler = mcpManager.addResourceNotificationHandler(
|
|
335
|
+
subscription.serverName,
|
|
336
|
+
async (notification: { uri: string }) => {
|
|
337
|
+
if (notification.uri !== subscription.resourceUri) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
await this.handleNotification(subscription, notification.uri, capturedMcpManager);
|
|
341
|
+
}
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
// Ref-count: only subscribe to the MCP server resource if this is the first subscription.
|
|
345
|
+
// IMPORTANT: handlerRemovers is set AFTER subscribeToResource succeeds so that
|
|
346
|
+
// teardownMcpSubscription can reliably infer setupSucceeded from its presence.
|
|
347
|
+
const refKey = this.makeResourceRefKey(subscription.serverName, subscription.resourceUri);
|
|
348
|
+
const currentCount = this.resourceRefCounts.get(refKey) ?? 0;
|
|
349
|
+
if (currentCount === 0) {
|
|
350
|
+
try {
|
|
351
|
+
await mcpManager.subscribeToResource(subscription.serverName, subscription.resourceUri);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
// subscribeToResource failed — clean up the handler we already registered
|
|
354
|
+
removeHandler();
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Only record state AFTER subscribe succeeds (or was already active via ref-count)
|
|
360
|
+
this.handlerRemovers.set(subscription.id, removeHandler);
|
|
361
|
+
this.resourceRefCounts.set(refKey, currentCount + 1);
|
|
362
|
+
|
|
363
|
+
logger.info(`MCP subscription '${subscription.id}' active`, {
|
|
364
|
+
server: subscription.serverName,
|
|
365
|
+
resource: subscription.resourceUri,
|
|
366
|
+
resourceRefCount: currentCount + 1,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Teardown MCP subscription.
|
|
372
|
+
* Removes the per-subscription handler and decrements the resource ref-count.
|
|
373
|
+
* Only unsubscribes from the MCP server when the last subscription for a resource is removed.
|
|
374
|
+
*/
|
|
375
|
+
private async teardownMcpSubscription(subscription: McpSubscription): Promise<void> {
|
|
376
|
+
// Remove per-subscription notification handler
|
|
377
|
+
const removeHandler = this.handlerRemovers.get(subscription.id);
|
|
378
|
+
const setupSucceeded = removeHandler !== undefined;
|
|
379
|
+
if (removeHandler) {
|
|
380
|
+
removeHandler();
|
|
381
|
+
this.handlerRemovers.delete(subscription.id);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Only decrement ref-count if setup actually succeeded (handler was registered).
|
|
385
|
+
// If setupMcpSubscription failed, ref-count was never incremented, so decrementing
|
|
386
|
+
// here would corrupt the count for other active subscriptions on the same resource.
|
|
387
|
+
if (!setupSucceeded) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Decrement ref-count; only unsubscribe from MCP server when count reaches 0
|
|
392
|
+
const refKey = this.makeResourceRefKey(subscription.serverName, subscription.resourceUri);
|
|
393
|
+
const currentCount = this.resourceRefCounts.get(refKey) ?? 0;
|
|
394
|
+
const newCount = currentCount - 1;
|
|
395
|
+
|
|
396
|
+
if (newCount <= 0) {
|
|
397
|
+
this.resourceRefCounts.delete(refKey);
|
|
398
|
+
|
|
399
|
+
if (!isProjectContextInitialized()) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
const projectCtx = getProjectContext();
|
|
405
|
+
const mcpManager = projectCtx.mcpManager;
|
|
406
|
+
|
|
407
|
+
if (mcpManager && mcpManager.isServerRunning(subscription.serverName)) {
|
|
408
|
+
await mcpManager.unsubscribeFromResource(
|
|
409
|
+
subscription.serverName,
|
|
410
|
+
subscription.resourceUri
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
} catch (error) {
|
|
414
|
+
logger.warn("Failed to unsubscribe from MCP resource", {
|
|
415
|
+
subscription: subscription.id,
|
|
416
|
+
error: error instanceof Error ? error.message : String(error),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
this.resourceRefCounts.set(refKey, newCount);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Build a ref-count key for a server+resource pair.
|
|
426
|
+
*/
|
|
427
|
+
private makeResourceRefKey(serverName: string, resourceUri: string): string {
|
|
428
|
+
return `${serverName}::${resourceUri}`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Handle an MCP resource push notification.
|
|
433
|
+
* Reads the updated resource content and delivers it to the conversation.
|
|
434
|
+
*
|
|
435
|
+
* Uses the captured mcpManager reference from setup time rather than
|
|
436
|
+
* getProjectContext(), since push notifications fire from the MCP SDK's
|
|
437
|
+
* transport callbacks which execute outside AsyncLocalStorage scope.
|
|
438
|
+
*/
|
|
439
|
+
private async handleNotification(
|
|
440
|
+
subscription: McpSubscription,
|
|
441
|
+
uri: string,
|
|
442
|
+
mcpManager: MCPManagerLike
|
|
443
|
+
): Promise<void> {
|
|
444
|
+
// Guard: if the MCP server is no longer running (e.g. during shutdown),
|
|
445
|
+
// there's no point attempting to read the resource.
|
|
446
|
+
if (!mcpManager.isServerRunning(subscription.serverName)) {
|
|
447
|
+
logger.debug(`Ignoring notification for '${subscription.id}': server '${subscription.serverName}' no longer running`);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
const content = await this.readResourceContent(mcpManager, subscription.serverName, uri);
|
|
453
|
+
|
|
454
|
+
if (!content) {
|
|
455
|
+
logger.debug(`Empty notification for subscription '${subscription.id}'`);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Extract only new items by comparing against previously-seen content
|
|
460
|
+
const newContent = this.extractNewItems(subscription.id, content);
|
|
461
|
+
|
|
462
|
+
if (!newContent) {
|
|
463
|
+
logger.debug(`No new items in notification for subscription '${subscription.id}'`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
await this.deliverNotificationContent(subscription, newContent);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
subscription.status = McpSubscriptionStatus.ERROR;
|
|
470
|
+
subscription.lastError = error instanceof Error ? error.message : String(error);
|
|
471
|
+
subscription.updatedAt = Date.now();
|
|
472
|
+
await this.saveSubscriptions();
|
|
473
|
+
|
|
474
|
+
logger.error(`Failed to handle notification for subscription '${subscription.id}'`, {
|
|
475
|
+
error: subscription.lastError,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Read a resource and return its text content.
|
|
482
|
+
*/
|
|
483
|
+
private async readResourceContent(
|
|
484
|
+
mcpManager: MCPManagerLike,
|
|
485
|
+
serverName: string,
|
|
486
|
+
uri: string
|
|
487
|
+
): Promise<string> {
|
|
488
|
+
const result = await mcpManager.readResource(serverName, uri);
|
|
489
|
+
|
|
490
|
+
const textContents: string[] = [];
|
|
491
|
+
for (const content of result.contents) {
|
|
492
|
+
if ("text" in content && typeof content.text === "string") {
|
|
493
|
+
textContents.push(content.text);
|
|
494
|
+
} else if ("blob" in content && typeof content.blob === "string") {
|
|
495
|
+
textContents.push(`[Binary content: ${content.blob.length} bytes]`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return textContents.join("\n\n");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Deliver notification content to the conversation.
|
|
504
|
+
* Updates subscription metrics and calls the registered notification handler.
|
|
505
|
+
*/
|
|
506
|
+
private async deliverNotificationContent(
|
|
507
|
+
subscription: McpSubscription,
|
|
508
|
+
content: string
|
|
509
|
+
): Promise<void> {
|
|
510
|
+
// Update metrics and recover from ERROR state on successful delivery
|
|
511
|
+
subscription.notificationsReceived++;
|
|
512
|
+
subscription.lastNotificationAt = Date.now();
|
|
513
|
+
subscription.updatedAt = Date.now();
|
|
514
|
+
if (subscription.status === McpSubscriptionStatus.ERROR) {
|
|
515
|
+
subscription.status = McpSubscriptionStatus.ACTIVE;
|
|
516
|
+
subscription.lastError = undefined;
|
|
517
|
+
logger.info(`Subscription '${subscription.id}' recovered from ERROR to ACTIVE`);
|
|
518
|
+
}
|
|
519
|
+
await this.saveSubscriptions();
|
|
520
|
+
|
|
521
|
+
// Deliver notification to conversation
|
|
522
|
+
if (this.notificationHandler) {
|
|
523
|
+
await this.notificationHandler(subscription, content);
|
|
524
|
+
} else {
|
|
525
|
+
logger.warn(`No notification handler registered for subscription '${subscription.id}'`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
logger.debug(`Delivered notification for subscription '${subscription.id}'`, {
|
|
529
|
+
notificationsTotal: subscription.notificationsReceived,
|
|
530
|
+
contentLength: content.length,
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
trace.getActiveSpan()?.addEvent("mcp_subscription.notification_delivered", {
|
|
534
|
+
"subscription.id": subscription.id,
|
|
535
|
+
"notification.content_length": content.length,
|
|
536
|
+
"notification.total": subscription.notificationsReceived,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ========== Delta Tracking ==========
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Extract only new items from resource content by comparing against previously-seen IDs.
|
|
544
|
+
*
|
|
545
|
+
* Content is expected to be a list of JSON objects (one per line).
|
|
546
|
+
* Each item is identified by its `id` field; if absent, the full line is used as the key.
|
|
547
|
+
*
|
|
548
|
+
* Returns only the lines that are new since the last notification, or null if no new items.
|
|
549
|
+
* Updates the snapshot for the next comparison.
|
|
550
|
+
*/
|
|
551
|
+
private extractNewItems(subscriptionId: string, content: string): string | null {
|
|
552
|
+
const lines = content.split("\n").filter((line) => line.trim() !== "");
|
|
553
|
+
|
|
554
|
+
// Build a map of itemId -> line for the current content
|
|
555
|
+
const currentItems = new Map<string, string>();
|
|
556
|
+
for (const line of lines) {
|
|
557
|
+
const itemId = this.extractItemId(line);
|
|
558
|
+
currentItems.set(itemId, line);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const previousIds = this.contentSnapshots.get(subscriptionId);
|
|
562
|
+
|
|
563
|
+
// Update the snapshot with the current set of IDs
|
|
564
|
+
this.contentSnapshots.set(subscriptionId, new Set(currentItems.keys()));
|
|
565
|
+
|
|
566
|
+
// If no previous snapshot exists (first notification), deliver everything
|
|
567
|
+
if (!previousIds) {
|
|
568
|
+
return content;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Find items present now but not in the previous snapshot
|
|
572
|
+
const newLines: string[] = [];
|
|
573
|
+
for (const [itemId, line] of currentItems) {
|
|
574
|
+
if (!previousIds.has(itemId)) {
|
|
575
|
+
newLines.push(line);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (newLines.length === 0) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
logger.debug(`Delta tracking for subscription '${subscriptionId}'`, {
|
|
584
|
+
totalItems: currentItems.size,
|
|
585
|
+
previousItems: previousIds.size,
|
|
586
|
+
newItems: newLines.length,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
return newLines.join("\n");
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Extract a unique identifier from a content line.
|
|
594
|
+
* Attempts to parse the line as JSON and use the `id` field.
|
|
595
|
+
* Falls back to using the full line content as the identifier.
|
|
596
|
+
*/
|
|
597
|
+
private extractItemId(line: string): string {
|
|
598
|
+
try {
|
|
599
|
+
const parsed = JSON.parse(line);
|
|
600
|
+
if (parsed && typeof parsed === "object" && "id" in parsed && parsed.id != null) {
|
|
601
|
+
return String(parsed.id);
|
|
602
|
+
}
|
|
603
|
+
} catch {
|
|
604
|
+
// Not valid JSON — fall through to use the full line
|
|
605
|
+
}
|
|
606
|
+
return line.trim();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ========== Persistence ==========
|
|
610
|
+
|
|
611
|
+
private async loadSubscriptions(): Promise<void> {
|
|
612
|
+
try {
|
|
613
|
+
const data = await fs.readFile(this.persistencePath, "utf-8");
|
|
614
|
+
const subscriptions = JSON.parse(data) as McpSubscription[];
|
|
615
|
+
|
|
616
|
+
for (const sub of subscriptions) {
|
|
617
|
+
this.subscriptions.set(sub.id, sub);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
logger.debug(`Loaded ${subscriptions.length} MCP subscriptions from disk`);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
623
|
+
logger.error("Failed to load MCP subscriptions", { error });
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private async saveSubscriptions(): Promise<void> {
|
|
629
|
+
try {
|
|
630
|
+
const subscriptions = Array.from(this.subscriptions.values());
|
|
631
|
+
await fs.writeFile(this.persistencePath, JSON.stringify(subscriptions, null, 2));
|
|
632
|
+
} catch (error) {
|
|
633
|
+
logger.error("Failed to save MCP subscriptions", { error });
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
private generateSubscriptionId(): string {
|
|
638
|
+
return `mcp-sub-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Shutdown all active subscriptions.
|
|
643
|
+
*/
|
|
644
|
+
public async shutdown(): Promise<void> {
|
|
645
|
+
for (const subscription of this.subscriptions.values()) {
|
|
646
|
+
if (subscription.status === McpSubscriptionStatus.ACTIVE) {
|
|
647
|
+
await this.teardownMcpSubscription(subscription);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
logger.info("McpSubscriptionService shutdown complete");
|
|
652
|
+
}
|
|
653
|
+
}
|