@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,130 @@
|
|
|
1
|
+
import type { NDKMCPTool } from "@/events/NDKMCPTool";
|
|
2
|
+
import { config } from "@/services/ConfigService";
|
|
3
|
+
import type { MCPServerConfig } from "@/services/config/types";
|
|
4
|
+
import { logger } from "@/utils/logger";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Installs an MCP server from an NDKMCPTool event into a project's configuration
|
|
8
|
+
* @param metadataPath The project metadata path (~/.tenex/projects/{dTag})
|
|
9
|
+
*/
|
|
10
|
+
export async function installMCPServerFromEvent(
|
|
11
|
+
metadataPath: string,
|
|
12
|
+
mcpTool: NDKMCPTool
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
const serverName = mcpTool.slug;
|
|
15
|
+
const command = mcpTool.command;
|
|
16
|
+
|
|
17
|
+
if (!command) {
|
|
18
|
+
throw new Error(`MCP tool event ${mcpTool.id} is missing command tag`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Parse command and args
|
|
22
|
+
const [cmd, ...args] = command.split(" ");
|
|
23
|
+
|
|
24
|
+
// Build server config with event ID
|
|
25
|
+
const serverConfig: MCPServerConfig = {
|
|
26
|
+
command: cmd || "",
|
|
27
|
+
args,
|
|
28
|
+
description: mcpTool.description,
|
|
29
|
+
eventId: mcpTool.id, // Track the event ID
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Load existing MCP config from the metadata path
|
|
33
|
+
const mcpConfig = await config.loadTenexMCP(metadataPath);
|
|
34
|
+
|
|
35
|
+
// Check if this event ID is already installed (only if we have an event ID)
|
|
36
|
+
if (mcpTool.id && (await isMCPToolInstalled(metadataPath, mcpTool.id))) {
|
|
37
|
+
logger.info(`MCP tool with event ID ${mcpTool.id} already installed`, { metadataPath });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check if server with same name already exists
|
|
42
|
+
if (mcpConfig.servers[serverName]) {
|
|
43
|
+
// If it exists without an event ID and we're adding one with an event ID, update it
|
|
44
|
+
if (!mcpConfig.servers[serverName].eventId && mcpTool.id) {
|
|
45
|
+
logger.info(`Updating existing MCP server '${serverName}' with event ID`, {
|
|
46
|
+
metadataPath,
|
|
47
|
+
eventId: mcpTool.id,
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
logger.info(`MCP server '${serverName}' already exists`, { metadataPath });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Add new server
|
|
56
|
+
mcpConfig.servers[serverName] = serverConfig;
|
|
57
|
+
|
|
58
|
+
// Save config directly to metadata path
|
|
59
|
+
await config.saveTenexMCP(metadataPath, mcpConfig);
|
|
60
|
+
|
|
61
|
+
logger.info(`Auto-installed MCP server: ${serverName}`, {
|
|
62
|
+
metadataPath,
|
|
63
|
+
command: cmd,
|
|
64
|
+
args,
|
|
65
|
+
eventId: mcpTool.id,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Checks if an MCP tool with a given event ID is already installed
|
|
71
|
+
* @param metadataPath The project metadata path (~/.tenex/projects/{dTag})
|
|
72
|
+
*/
|
|
73
|
+
export async function isMCPToolInstalled(metadataPath: string, eventId: string): Promise<boolean> {
|
|
74
|
+
// Load from the metadata path directly
|
|
75
|
+
const mcpConfig = await config.loadTenexMCP(metadataPath);
|
|
76
|
+
|
|
77
|
+
// Check if any server has this event ID
|
|
78
|
+
for (const serverConfig of Object.values(mcpConfig.servers)) {
|
|
79
|
+
if (serverConfig.eventId === eventId) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Gets all installed MCP tool event IDs (only those that have event IDs)
|
|
89
|
+
* @param metadataPath The project metadata path (~/.tenex/projects/{dTag})
|
|
90
|
+
*/
|
|
91
|
+
export async function getInstalledMCPEventIds(metadataPath: string): Promise<Set<string>> {
|
|
92
|
+
// Load from the metadata path directly
|
|
93
|
+
const mcpConfig = await config.loadTenexMCP(metadataPath);
|
|
94
|
+
const eventIds = new Set<string>();
|
|
95
|
+
|
|
96
|
+
for (const serverConfig of Object.values(mcpConfig.servers)) {
|
|
97
|
+
// Only add if eventId exists (some MCP tools are manually installed without event IDs)
|
|
98
|
+
if (serverConfig.eventId) {
|
|
99
|
+
eventIds.add(serverConfig.eventId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return eventIds;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Removes an MCP server by its event ID
|
|
108
|
+
* @param metadataPath The project metadata path (~/.tenex/projects/{dTag})
|
|
109
|
+
*/
|
|
110
|
+
export async function removeMCPServerByEventId(metadataPath: string, eventId: string): Promise<void> {
|
|
111
|
+
// Load from the metadata path directly
|
|
112
|
+
const mcpConfig = await config.loadTenexMCP(metadataPath);
|
|
113
|
+
|
|
114
|
+
// Find and remove servers with this event ID
|
|
115
|
+
let removed = false;
|
|
116
|
+
for (const [serverName, serverConfig] of Object.entries(mcpConfig.servers)) {
|
|
117
|
+
if (serverConfig.eventId === eventId) {
|
|
118
|
+
delete mcpConfig.servers[serverName];
|
|
119
|
+
removed = true;
|
|
120
|
+
logger.info(`Removed MCP server '${serverName}' with event ID ${eventId}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (removed) {
|
|
125
|
+
// Save updated config directly to metadata path
|
|
126
|
+
await config.saveTenexMCP(metadataPath, mcpConfig);
|
|
127
|
+
} else {
|
|
128
|
+
logger.warn(`No MCP server found with event ID ${eventId}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { config } from "@/services/ConfigService";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Operations logged by the NIP-46 signing system.
|
|
7
|
+
*/
|
|
8
|
+
export type Nip46LogOperation =
|
|
9
|
+
| "signer_connect"
|
|
10
|
+
| "sign_request"
|
|
11
|
+
| "sign_success"
|
|
12
|
+
| "sign_timeout"
|
|
13
|
+
| "sign_rejected"
|
|
14
|
+
| "sign_error"
|
|
15
|
+
| "event_published";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A single log entry for NIP-46 signing activity.
|
|
19
|
+
*/
|
|
20
|
+
export interface Nip46LogEntry {
|
|
21
|
+
ts: string;
|
|
22
|
+
op: Nip46LogOperation;
|
|
23
|
+
requestId?: string;
|
|
24
|
+
ownerPubkey?: string; // First 12 chars
|
|
25
|
+
eventKind?: number;
|
|
26
|
+
agentAction?: string;
|
|
27
|
+
agentPubkey?: string; // First 12 chars
|
|
28
|
+
pTagCount?: number;
|
|
29
|
+
signerType?: "nip46";
|
|
30
|
+
durationMs?: number;
|
|
31
|
+
error?: string;
|
|
32
|
+
eventId?: string;
|
|
33
|
+
trigger?: string; // What triggered this signing request
|
|
34
|
+
eventTags?: string[][]; // Full tags array of the event being signed
|
|
35
|
+
eventContent?: string; // Content field of the event being signed
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Dedicated JSONL log writer for NIP-46 signing operations.
|
|
40
|
+
* Writes to ~/.tenex/daemon/nip46-signing.log, independent from daemon.log.
|
|
41
|
+
*/
|
|
42
|
+
export class Nip46SigningLog {
|
|
43
|
+
private static instance: Nip46SigningLog | null = null;
|
|
44
|
+
private logPath: string;
|
|
45
|
+
|
|
46
|
+
private constructor() {
|
|
47
|
+
const daemonDir = config.getConfigPath("daemon");
|
|
48
|
+
fs.mkdirSync(daemonDir, { recursive: true });
|
|
49
|
+
this.logPath = path.join(daemonDir, "nip46-signing.log");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static getInstance(): Nip46SigningLog {
|
|
53
|
+
if (!Nip46SigningLog.instance) {
|
|
54
|
+
Nip46SigningLog.instance = new Nip46SigningLog();
|
|
55
|
+
}
|
|
56
|
+
return Nip46SigningLog.instance;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Append a log entry as a single JSONL line.
|
|
61
|
+
*/
|
|
62
|
+
log(entry: Omit<Nip46LogEntry, "ts">): void {
|
|
63
|
+
const fullEntry: Nip46LogEntry = {
|
|
64
|
+
ts: new Date().toISOString(),
|
|
65
|
+
...entry,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
fs.appendFileSync(this.logPath, JSON.stringify(fullEntry) + "\n");
|
|
70
|
+
} catch {
|
|
71
|
+
// Silent failure - logging should never crash the daemon
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convenience: truncate a pubkey to 12 chars for logging.
|
|
77
|
+
*/
|
|
78
|
+
static truncatePubkey(pubkey: string): string {
|
|
79
|
+
return pubkey.substring(0, 12);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import { getNDK } from "@/nostr/ndkClient";
|
|
3
|
+
import { getRelayUrls } from "@/nostr/relays";
|
|
4
|
+
import { config } from "@/services/ConfigService";
|
|
5
|
+
import { logger } from "@/utils/logger";
|
|
6
|
+
import {
|
|
7
|
+
type NDKEvent,
|
|
8
|
+
NDKNip46Signer,
|
|
9
|
+
NDKPrivateKeySigner,
|
|
10
|
+
} from "@nostr-dev-kit/ndk";
|
|
11
|
+
import { Nip46SigningLog } from "./Nip46SigningLog";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default configuration values for NIP-46 signing.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULTS = {
|
|
17
|
+
SIGNING_TIMEOUT_MS: 30_000,
|
|
18
|
+
CONNECT_TIMEOUT_MS: 30_000,
|
|
19
|
+
MAX_RETRIES: 2,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Result of a NIP-46 signing attempt.
|
|
24
|
+
*
|
|
25
|
+
* - `signed` — signing succeeded
|
|
26
|
+
* - `user_rejected` — user/bunker explicitly rejected
|
|
27
|
+
* - `failed` — timeout/network error; event will not be published
|
|
28
|
+
*/
|
|
29
|
+
export type SignResult =
|
|
30
|
+
| { outcome: "signed" }
|
|
31
|
+
| { outcome: "user_rejected"; reason: string }
|
|
32
|
+
| { outcome: "failed"; reason: string };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Race a promise against a timeout, cleaning up the timer regardless of outcome.
|
|
36
|
+
* Prevents leaked timers that cause unhandled promise rejections.
|
|
37
|
+
*/
|
|
38
|
+
function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
|
|
39
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
40
|
+
return Promise.race([
|
|
41
|
+
promise,
|
|
42
|
+
new Promise<never>((_, reject) => {
|
|
43
|
+
timer = setTimeout(() => reject(new Error(message)), ms);
|
|
44
|
+
}),
|
|
45
|
+
]).finally(() => clearTimeout(timer));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Singleton service that manages NIP-46 remote signing for kind 14199 events.
|
|
50
|
+
*
|
|
51
|
+
* Responsibilities:
|
|
52
|
+
* - Lazy initialization of NDKNip46Signer per owner pubkey
|
|
53
|
+
* - Timeout-wrapped signing with configurable retries
|
|
54
|
+
* - Per-owner mutex to serialize signing requests
|
|
55
|
+
* - Clear failure logging when signing fails (no fallback to backend key)
|
|
56
|
+
*/
|
|
57
|
+
export class Nip46SigningService {
|
|
58
|
+
private static instance: Nip46SigningService | null = null;
|
|
59
|
+
|
|
60
|
+
/** One NIP-46 signer per owner pubkey */
|
|
61
|
+
private signers = new Map<string, NDKNip46Signer>();
|
|
62
|
+
|
|
63
|
+
/** Per-owner promise chain for serialized signing */
|
|
64
|
+
private ownerLocks = new Map<string, Promise<void>>();
|
|
65
|
+
|
|
66
|
+
/** Track connection state per owner */
|
|
67
|
+
private connectedOwners = new Set<string>();
|
|
68
|
+
|
|
69
|
+
private signingLog: Nip46SigningLog;
|
|
70
|
+
|
|
71
|
+
private constructor() {
|
|
72
|
+
this.signingLog = Nip46SigningLog.getInstance();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static getInstance(): Nip46SigningService {
|
|
76
|
+
if (!Nip46SigningService.instance) {
|
|
77
|
+
Nip46SigningService.instance = new Nip46SigningService();
|
|
78
|
+
}
|
|
79
|
+
return Nip46SigningService.instance;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// =====================================================================================
|
|
83
|
+
// CONFIGURATION HELPERS
|
|
84
|
+
// =====================================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if NIP-46 signing is globally enabled.
|
|
88
|
+
*/
|
|
89
|
+
isEnabled(): boolean {
|
|
90
|
+
try {
|
|
91
|
+
const cfg = config.getConfig();
|
|
92
|
+
return cfg.nip46?.enabled === true;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if NIP-46 is enabled and configured for a specific owner.
|
|
100
|
+
*/
|
|
101
|
+
isEnabledForOwner(_ownerPubkey: string): boolean {
|
|
102
|
+
if (!this.isEnabled()) return false;
|
|
103
|
+
// NIP-46 is enabled for all whitelisted owners;
|
|
104
|
+
// we auto-construct bunker URIs when not explicitly configured
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the bunker URI for an owner. If not explicitly configured,
|
|
110
|
+
* auto-constructs: bunker://<ownerPubkey>?relay=<firstRelay>
|
|
111
|
+
*/
|
|
112
|
+
getBunkerUri(ownerPubkey: string): string {
|
|
113
|
+
const cfg = config.getConfig();
|
|
114
|
+
const ownerConfig = cfg.nip46?.owners?.[ownerPubkey];
|
|
115
|
+
|
|
116
|
+
if (ownerConfig?.bunkerUri) {
|
|
117
|
+
return ownerConfig.bunkerUri;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Auto-construct bunker URI
|
|
121
|
+
const relays = getRelayUrls();
|
|
122
|
+
const relay = relays[0] || "wss://tenex.chat";
|
|
123
|
+
return `bunker://${ownerPubkey}?relay=${encodeURIComponent(relay)}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private getSigningTimeout(): number {
|
|
127
|
+
try {
|
|
128
|
+
return config.getConfig().nip46?.signingTimeoutMs ?? DEFAULTS.SIGNING_TIMEOUT_MS;
|
|
129
|
+
} catch {
|
|
130
|
+
return DEFAULTS.SIGNING_TIMEOUT_MS;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private getMaxRetries(): number {
|
|
135
|
+
try {
|
|
136
|
+
return config.getConfig().nip46?.maxRetries ?? DEFAULTS.MAX_RETRIES;
|
|
137
|
+
} catch {
|
|
138
|
+
return DEFAULTS.MAX_RETRIES;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// =====================================================================================
|
|
143
|
+
// SIGNER MANAGEMENT
|
|
144
|
+
// =====================================================================================
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get or create an NDKNip46Signer for a specific owner.
|
|
148
|
+
* Lazy initialization — signer created on first request, not at daemon startup.
|
|
149
|
+
*/
|
|
150
|
+
private async getOrCreateSigner(ownerPubkey: string): Promise<NDKNip46Signer> {
|
|
151
|
+
const existing = this.signers.get(ownerPubkey);
|
|
152
|
+
if (existing) return existing;
|
|
153
|
+
|
|
154
|
+
const ndk = getNDK();
|
|
155
|
+
const backendNsec = await config.ensureBackendPrivateKey();
|
|
156
|
+
const localSigner = new NDKPrivateKeySigner(backendNsec);
|
|
157
|
+
const bunkerUri = this.getBunkerUri(ownerPubkey);
|
|
158
|
+
|
|
159
|
+
logger.info("[NIP-46] Creating signer for owner", {
|
|
160
|
+
ownerPubkey: ownerPubkey.substring(0, 12),
|
|
161
|
+
bunkerUri: bunkerUri.substring(0, 60),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const signer = NDKNip46Signer.bunker(ndk, bunkerUri, localSigner);
|
|
165
|
+
|
|
166
|
+
// Handle auth URL — log it so operator can approve if needed
|
|
167
|
+
signer.on("authUrl", (url: string) => {
|
|
168
|
+
logger.info("[NIP-46] Auth URL required", {
|
|
169
|
+
ownerPubkey: ownerPubkey.substring(0, 12),
|
|
170
|
+
url,
|
|
171
|
+
});
|
|
172
|
+
this.signingLog.log({
|
|
173
|
+
op: "signer_connect",
|
|
174
|
+
ownerPubkey: Nip46SigningLog.truncatePubkey(ownerPubkey),
|
|
175
|
+
error: `auth_url_required: ${url}`,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
this.signers.set(ownerPubkey, signer);
|
|
180
|
+
return signer;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Ensure the signer for an owner is connected (blockUntilReady).
|
|
185
|
+
* Wrapped with timeout to prevent hanging forever.
|
|
186
|
+
*/
|
|
187
|
+
private async ensureConnected(ownerPubkey: string): Promise<NDKNip46Signer> {
|
|
188
|
+
const signer = await this.getOrCreateSigner(ownerPubkey);
|
|
189
|
+
|
|
190
|
+
if (this.connectedOwners.has(ownerPubkey)) {
|
|
191
|
+
return signer;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const startMs = Date.now();
|
|
195
|
+
const requestId = crypto.randomUUID().substring(0, 8);
|
|
196
|
+
|
|
197
|
+
this.signingLog.log({
|
|
198
|
+
op: "signer_connect",
|
|
199
|
+
requestId,
|
|
200
|
+
ownerPubkey: Nip46SigningLog.truncatePubkey(ownerPubkey),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
await withTimeout(
|
|
205
|
+
signer.blockUntilReady(),
|
|
206
|
+
DEFAULTS.CONNECT_TIMEOUT_MS,
|
|
207
|
+
"NIP-46 connect timed out",
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
this.connectedOwners.add(ownerPubkey);
|
|
211
|
+
const durationMs = Date.now() - startMs;
|
|
212
|
+
|
|
213
|
+
logger.info("[NIP-46] Signer connected", {
|
|
214
|
+
ownerPubkey: ownerPubkey.substring(0, 12),
|
|
215
|
+
durationMs,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
this.signingLog.log({
|
|
219
|
+
op: "signer_connect",
|
|
220
|
+
requestId,
|
|
221
|
+
ownerPubkey: Nip46SigningLog.truncatePubkey(ownerPubkey),
|
|
222
|
+
durationMs,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return signer;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
const durationMs = Date.now() - startMs;
|
|
228
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
229
|
+
|
|
230
|
+
logger.error("[NIP-46] Signer connection failed", {
|
|
231
|
+
ownerPubkey: ownerPubkey.substring(0, 12),
|
|
232
|
+
error: errorMsg,
|
|
233
|
+
durationMs,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
this.signingLog.log({
|
|
237
|
+
op: "sign_error",
|
|
238
|
+
requestId,
|
|
239
|
+
ownerPubkey: Nip46SigningLog.truncatePubkey(ownerPubkey),
|
|
240
|
+
error: `connect_failed: ${errorMsg}`,
|
|
241
|
+
durationMs,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Remove from cache so next attempt creates a fresh signer
|
|
245
|
+
this.signers.delete(ownerPubkey);
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// =====================================================================================
|
|
251
|
+
// PER-OWNER MUTEX
|
|
252
|
+
// =====================================================================================
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Serialize operations per owner pubkey to avoid concurrent NIP-46 signing.
|
|
256
|
+
*/
|
|
257
|
+
private async withOwnerLock<T>(ownerPubkey: string, fn: () => Promise<T>): Promise<T> {
|
|
258
|
+
const existing = this.ownerLocks.get(ownerPubkey) ?? Promise.resolve();
|
|
259
|
+
let resolve: () => void;
|
|
260
|
+
const next = new Promise<void>((r) => (resolve = r));
|
|
261
|
+
this.ownerLocks.set(ownerPubkey, next);
|
|
262
|
+
|
|
263
|
+
await existing;
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
return await fn();
|
|
267
|
+
} finally {
|
|
268
|
+
resolve!();
|
|
269
|
+
if (this.ownerLocks.get(ownerPubkey) === next) {
|
|
270
|
+
this.ownerLocks.delete(ownerPubkey);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// =====================================================================================
|
|
276
|
+
// SIGNING
|
|
277
|
+
// =====================================================================================
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Sign an NDKEvent using NIP-46 remote signing for the given owner.
|
|
281
|
+
*
|
|
282
|
+
* @param ownerPubkey - The owner's hex pubkey (the "user" in NIP-46 terms)
|
|
283
|
+
* @param event - The NDKEvent to sign (will be mutated: pubkey set to ownerPubkey)
|
|
284
|
+
* @returns SignResult indicating success, explicit rejection, or transient failure
|
|
285
|
+
*/
|
|
286
|
+
async signEvent(ownerPubkey: string, event: NDKEvent, trigger?: string): Promise<SignResult> {
|
|
287
|
+
return this.withOwnerLock(ownerPubkey, () =>
|
|
288
|
+
this.signEventInternal(ownerPubkey, event, trigger)
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private async signEventInternal(ownerPubkey: string, event: NDKEvent, trigger?: string): Promise<SignResult> {
|
|
293
|
+
const requestId = crypto.randomUUID().substring(0, 8);
|
|
294
|
+
const signingTimeout = this.getSigningTimeout();
|
|
295
|
+
const maxRetries = this.getMaxRetries();
|
|
296
|
+
const startMs = Date.now();
|
|
297
|
+
|
|
298
|
+
this.signingLog.log({
|
|
299
|
+
op: "sign_request",
|
|
300
|
+
requestId,
|
|
301
|
+
ownerPubkey: Nip46SigningLog.truncatePubkey(ownerPubkey),
|
|
302
|
+
eventKind: event.kind,
|
|
303
|
+
pTagCount: event.tags.filter((t) => t[0] === "p").length,
|
|
304
|
+
trigger,
|
|
305
|
+
eventTags: event.tags,
|
|
306
|
+
eventContent: event.content,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
let lastError: Error | null = null;
|
|
310
|
+
|
|
311
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
312
|
+
try {
|
|
313
|
+
const signer = await this.ensureConnected(ownerPubkey);
|
|
314
|
+
|
|
315
|
+
// Set the event pubkey to the owner before signing
|
|
316
|
+
event.pubkey = ownerPubkey;
|
|
317
|
+
|
|
318
|
+
logger.info("[NIP-46] Pre-sign payload", {
|
|
319
|
+
requestId,
|
|
320
|
+
attempt,
|
|
321
|
+
kind: event.kind,
|
|
322
|
+
pubkey: event.pubkey?.substring(0, 12),
|
|
323
|
+
tags: JSON.stringify(event.tags),
|
|
324
|
+
content: event.content,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
await withTimeout(
|
|
328
|
+
event.sign(signer),
|
|
329
|
+
signingTimeout,
|
|
330
|
+
"NIP-46 sign timed out",
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
const durationMs = Date.now() - startMs;
|
|
334
|
+
|
|
335
|
+
this.signingLog.log({
|
|
336
|
+
op: "sign_success",
|
|
337
|
+
requestId,
|
|
338
|
+
ownerPubkey: Nip46SigningLog.truncatePubkey(ownerPubkey),
|
|
339
|
+
eventKind: event.kind,
|
|
340
|
+
signerType: "nip46",
|
|
341
|
+
durationMs,
|
|
342
|
+
eventId: event.id,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
logger.info("[NIP-46] Event signed successfully", {
|
|
346
|
+
ownerPubkey: ownerPubkey.substring(0, 12),
|
|
347
|
+
eventKind: event.kind,
|
|
348
|
+
eventId: event.id?.substring(0, 12),
|
|
349
|
+
durationMs,
|
|
350
|
+
attempt,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
return { outcome: "signed" };
|
|
354
|
+
} catch (error) {
|
|
355
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
356
|
+
const errorMsg = lastError.message;
|
|
357
|
+
|
|
358
|
+
// User rejection — never retry
|
|
359
|
+
if (this.isUserRejection(errorMsg)) {
|
|
360
|
+
const durationMs = Date.now() - startMs;
|
|
361
|
+
|
|
362
|
+
this.signingLog.log({
|
|
363
|
+
op: "sign_rejected",
|
|
364
|
+
requestId,
|
|
365
|
+
ownerPubkey: Nip46SigningLog.truncatePubkey(ownerPubkey),
|
|
366
|
+
eventKind: event.kind,
|
|
367
|
+
error: errorMsg,
|
|
368
|
+
durationMs,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
logger.warn("[NIP-46] Signing rejected by user", {
|
|
372
|
+
ownerPubkey: ownerPubkey.substring(0, 12),
|
|
373
|
+
eventKind: event.kind,
|
|
374
|
+
error: errorMsg,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
return { outcome: "user_rejected", reason: errorMsg };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Timeout or relay error — retry if attempts remain
|
|
381
|
+
if (attempt < maxRetries) {
|
|
382
|
+
logger.warn("[NIP-46] Signing failed, retrying", {
|
|
383
|
+
ownerPubkey: ownerPubkey.substring(0, 12),
|
|
384
|
+
eventKind: event.kind,
|
|
385
|
+
attempt: attempt + 1,
|
|
386
|
+
maxRetries,
|
|
387
|
+
error: errorMsg,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Reset signer on connection errors
|
|
391
|
+
if (errorMsg.includes("connect") || errorMsg.includes("timed out")) {
|
|
392
|
+
this.signers.delete(ownerPubkey);
|
|
393
|
+
this.connectedOwners.delete(ownerPubkey);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// All retries exhausted
|
|
402
|
+
const durationMs = Date.now() - startMs;
|
|
403
|
+
const errorMsg = lastError?.message ?? "unknown error";
|
|
404
|
+
|
|
405
|
+
if (errorMsg.includes("timed out")) {
|
|
406
|
+
this.signingLog.log({
|
|
407
|
+
op: "sign_timeout",
|
|
408
|
+
requestId,
|
|
409
|
+
ownerPubkey: Nip46SigningLog.truncatePubkey(ownerPubkey),
|
|
410
|
+
eventKind: event.kind,
|
|
411
|
+
error: errorMsg,
|
|
412
|
+
durationMs,
|
|
413
|
+
});
|
|
414
|
+
} else {
|
|
415
|
+
this.signingLog.log({
|
|
416
|
+
op: "sign_error",
|
|
417
|
+
requestId,
|
|
418
|
+
ownerPubkey: Nip46SigningLog.truncatePubkey(ownerPubkey),
|
|
419
|
+
eventKind: event.kind,
|
|
420
|
+
error: errorMsg,
|
|
421
|
+
durationMs,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
logger.error("[NIP-46] Signing failed after all retries", {
|
|
426
|
+
ownerPubkey: ownerPubkey.substring(0, 12),
|
|
427
|
+
eventKind: event.kind,
|
|
428
|
+
retries: maxRetries,
|
|
429
|
+
error: errorMsg,
|
|
430
|
+
durationMs,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
return { outcome: "failed", reason: errorMsg };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Detect user rejection errors — these should never be retried.
|
|
438
|
+
*/
|
|
439
|
+
private isUserRejection(errorMsg: string): boolean {
|
|
440
|
+
const rejectionPatterns = [
|
|
441
|
+
"rejected",
|
|
442
|
+
"denied",
|
|
443
|
+
"refused",
|
|
444
|
+
"not authorized",
|
|
445
|
+
"user declined",
|
|
446
|
+
];
|
|
447
|
+
const lower = errorMsg.toLowerCase();
|
|
448
|
+
return rejectionPatterns.some((p) => lower.includes(p));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// =====================================================================================
|
|
452
|
+
// LIFECYCLE
|
|
453
|
+
// =====================================================================================
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Clean up all NIP-46 signer connections.
|
|
457
|
+
*/
|
|
458
|
+
async shutdown(): Promise<void> {
|
|
459
|
+
logger.info("[NIP-46] Shutting down signing service", {
|
|
460
|
+
activeSingers: this.signers.size,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
this.signers.clear();
|
|
464
|
+
this.connectedOwners.clear();
|
|
465
|
+
this.ownerLocks.clear();
|
|
466
|
+
}
|
|
467
|
+
}
|