@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,1133 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { StoredAgentData, ProjectScopedConfig, AgentDefaultConfig, AgentProjectConfig } from "@/agents/types";
|
|
4
|
+
import type { MCPServerConfig } from "@/llm/providers/types";
|
|
5
|
+
import {
|
|
6
|
+
resolveEffectiveConfig,
|
|
7
|
+
deduplicateProjectConfig,
|
|
8
|
+
type ResolvedAgentConfig,
|
|
9
|
+
} from "@/agents/ConfigResolver";
|
|
10
|
+
import { ensureDirectory, fileExists } from "@/lib/fs";
|
|
11
|
+
import { config } from "@/services/ConfigService";
|
|
12
|
+
import { logger } from "@/utils/logger";
|
|
13
|
+
import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
|
14
|
+
import { trace } from "@opentelemetry/api";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Options for `updateDefaultConfig()`.
|
|
18
|
+
*/
|
|
19
|
+
export interface UpdateDefaultConfigOptions {
|
|
20
|
+
/** If true, clears all projectOverrides (default: false) */
|
|
21
|
+
clearProjectOverrides?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Agent data stored in ~/.tenex/agents/<pubkey>.json
|
|
26
|
+
*/
|
|
27
|
+
export interface StoredAgent extends StoredAgentData {
|
|
28
|
+
eventId?: string;
|
|
29
|
+
nsec: string;
|
|
30
|
+
slug: string;
|
|
31
|
+
/**
|
|
32
|
+
* Agent lifecycle status.
|
|
33
|
+
* - 'active': Agent is assigned to at least one project (default behavior)
|
|
34
|
+
* - 'inactive': Agent has been removed from all projects but identity preserved
|
|
35
|
+
*
|
|
36
|
+
* ## Identity Preservation Policy
|
|
37
|
+
* Agent files are NEVER deleted when removed from projects. Instead, they become
|
|
38
|
+
* 'inactive' and retain their pubkey/nsec for potential reactivation.
|
|
39
|
+
*/
|
|
40
|
+
status?: "active" | "inactive";
|
|
41
|
+
/**
|
|
42
|
+
* @deprecated Use pmOverrides instead. Kept for backward compatibility.
|
|
43
|
+
* Will be migrated to pmOverrides on first save.
|
|
44
|
+
*/
|
|
45
|
+
isPMOverride?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Project-scoped PM override flags.
|
|
48
|
+
* Key is project dTag, value is true if this agent is PM for that project.
|
|
49
|
+
* Only one agent per project should have this set to true.
|
|
50
|
+
*/
|
|
51
|
+
pmOverrides?: Record<string, boolean>;
|
|
52
|
+
/**
|
|
53
|
+
* Global PM designation flag.
|
|
54
|
+
* When true, this agent is designated as PM for ALL projects where it exists.
|
|
55
|
+
* Set via kind 24020 TenexAgentConfigUpdate event with ["pm"] tag (without a-tag).
|
|
56
|
+
* Takes precedence over pmOverrides and project tag designations.
|
|
57
|
+
*/
|
|
58
|
+
isPM?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Project-scoped configuration overrides.
|
|
61
|
+
* Key is project dTag, value contains project-specific settings.
|
|
62
|
+
* Set via kind 24020 TenexAgentConfigUpdate events WITH an a-tag specifying the project.
|
|
63
|
+
*
|
|
64
|
+
* ## Priority (highest to lowest)
|
|
65
|
+
* 1. projectConfigs[projectDTag].* (project-scoped from kind 24020 with a-tag)
|
|
66
|
+
* 2. Global fields (llmConfig, tools, isPM) (global from kind 24020 without a-tag)
|
|
67
|
+
* 3. pmOverrides[projectDTag] (legacy, for backward compatibility)
|
|
68
|
+
* 4. Project tag designations (from kind 31933)
|
|
69
|
+
*/
|
|
70
|
+
projectConfigs?: Record<string, ProjectScopedConfig>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Factory function to create a StoredAgent object.
|
|
75
|
+
*
|
|
76
|
+
* Ensures consistent structure and defaults across the codebase.
|
|
77
|
+
* Used by both agent-installer (Nostr agents) and agents_write (local agents).
|
|
78
|
+
*
|
|
79
|
+
* ## Why this exists
|
|
80
|
+
* Before: StoredAgent objects were manually constructed in 2 places with slight differences
|
|
81
|
+
* After: Single factory ensures consistency and makes schema changes easier
|
|
82
|
+
*
|
|
83
|
+
* @param config - Agent configuration
|
|
84
|
+
* @returns StoredAgent ready for saving to disk
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* const agent = createStoredAgent({
|
|
88
|
+
* nsec: signer.nsec,
|
|
89
|
+
* slug: 'my-agent',
|
|
90
|
+
* name: 'My Agent',
|
|
91
|
+
* role: 'assistant',
|
|
92
|
+
* tools: ['fs_read', 'shell'],
|
|
93
|
+
* eventId: 'nostr_event_id',
|
|
94
|
+
* });
|
|
95
|
+
* await agentStorage.saveAgent(agent);
|
|
96
|
+
* await agentStorage.addAgentToProject(pubkey, 'project-dtag');
|
|
97
|
+
*/
|
|
98
|
+
export function createStoredAgent(config: {
|
|
99
|
+
nsec: string;
|
|
100
|
+
slug: string;
|
|
101
|
+
name: string;
|
|
102
|
+
role: string;
|
|
103
|
+
description?: string | null;
|
|
104
|
+
instructions?: string | null;
|
|
105
|
+
useCriteria?: string | null;
|
|
106
|
+
eventId?: string;
|
|
107
|
+
mcpServers?: Record<string, MCPServerConfig>;
|
|
108
|
+
pmOverrides?: Record<string, boolean>;
|
|
109
|
+
defaultConfig?: AgentDefaultConfig;
|
|
110
|
+
projectOverrides?: Record<string, AgentProjectConfig>;
|
|
111
|
+
definitionDTag?: string;
|
|
112
|
+
definitionAuthor?: string;
|
|
113
|
+
definitionCreatedAt?: number;
|
|
114
|
+
}): StoredAgent {
|
|
115
|
+
return {
|
|
116
|
+
eventId: config.eventId,
|
|
117
|
+
nsec: config.nsec,
|
|
118
|
+
slug: config.slug,
|
|
119
|
+
name: config.name,
|
|
120
|
+
role: config.role,
|
|
121
|
+
description: config.description ?? undefined,
|
|
122
|
+
instructions: config.instructions ?? undefined,
|
|
123
|
+
useCriteria: config.useCriteria ?? undefined,
|
|
124
|
+
status: "active",
|
|
125
|
+
mcpServers: config.mcpServers,
|
|
126
|
+
pmOverrides: config.pmOverrides,
|
|
127
|
+
default: config.defaultConfig,
|
|
128
|
+
projectOverrides: config.projectOverrides,
|
|
129
|
+
definitionDTag: config.definitionDTag,
|
|
130
|
+
definitionAuthor: config.definitionAuthor,
|
|
131
|
+
definitionCreatedAt: config.definitionCreatedAt,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if an agent is active.
|
|
137
|
+
*
|
|
138
|
+
* An agent is considered active if:
|
|
139
|
+
* - It has `status: 'active'` explicitly set, OR
|
|
140
|
+
* - It has no status field (treated as active by default)
|
|
141
|
+
*
|
|
142
|
+
* This helper centralizes the logic for determining agent activity status.
|
|
143
|
+
*/
|
|
144
|
+
export function isAgentActive(agent: StoredAgent): boolean {
|
|
145
|
+
if (agent.status === "inactive") {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Slug index entry tracking which projects use this slug
|
|
153
|
+
*/
|
|
154
|
+
interface SlugEntry {
|
|
155
|
+
pubkey: string;
|
|
156
|
+
projectIds: string[];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Index structure for fast lookups
|
|
161
|
+
*/
|
|
162
|
+
interface AgentIndex {
|
|
163
|
+
bySlug: Record<string, SlugEntry>; // slug -> { pubkey, projectIds[] }
|
|
164
|
+
byEventId: Record<string, string>; // eventId -> pubkey
|
|
165
|
+
byProject: Record<string, string[]>; // projectDTag -> pubkey[]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* AgentStorage - Persistent storage layer for agent data
|
|
170
|
+
*
|
|
171
|
+
* ## Responsibility
|
|
172
|
+
* Manages agent data persistence in ~/.tenex/agents/
|
|
173
|
+
* - One JSON file per agent: <pubkey>.json (contains all data including private key)
|
|
174
|
+
* - Fast lookups via index.json (slug → pubkey, eventId → pubkey, project → pubkeys)
|
|
175
|
+
* - Project associations (which agents belong to which projects)
|
|
176
|
+
*
|
|
177
|
+
* ## Architecture
|
|
178
|
+
* - **AgentStorage** (this): Handles ALL persistence operations
|
|
179
|
+
* - **AgentRegistry**: Handles in-memory runtime instances (separate)
|
|
180
|
+
* - **agent-loader**: Orchestrates loading from storage → registry (separate)
|
|
181
|
+
*
|
|
182
|
+
* ## Storage Structure
|
|
183
|
+
* ```
|
|
184
|
+
* ~/.tenex/agents/
|
|
185
|
+
* ├── index.json # Fast lookup index
|
|
186
|
+
* ├── <pubkey1>.json # Agent data + private key
|
|
187
|
+
* └── <pubkey2>.json # Agent data + private key
|
|
188
|
+
* ```
|
|
189
|
+
*
|
|
190
|
+
* ## Usage Pattern
|
|
191
|
+
* 1. **Read operations**: Use load/get methods
|
|
192
|
+
* 2. **Write operations**: Use save/update methods
|
|
193
|
+
* 3. **After updates**: Call AgentRegistry.reloadAgent() to refresh in-memory instances
|
|
194
|
+
*
|
|
195
|
+
* ## Separation of Concerns
|
|
196
|
+
* - Storage (this class): Disk persistence only
|
|
197
|
+
* - Registry (AgentRegistry): Runtime instances only
|
|
198
|
+
* - Updates: storage.update() → registry.reload()
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* // Load agent from disk
|
|
202
|
+
* const agent = await agentStorage.loadAgent(pubkey);
|
|
203
|
+
*
|
|
204
|
+
* // Update default configuration
|
|
205
|
+
* await agentStorage.updateDefaultConfig(pubkey, { model: 'anthropic:claude-opus-4' });
|
|
206
|
+
*
|
|
207
|
+
* // Refresh in-memory instance
|
|
208
|
+
* await agentRegistry.reloadAgent(pubkey);
|
|
209
|
+
*
|
|
210
|
+
* @see AgentRegistry for in-memory runtime management
|
|
211
|
+
* @see agent-loader for loading orchestration
|
|
212
|
+
*/
|
|
213
|
+
export class AgentStorage {
|
|
214
|
+
private agentsDir: string;
|
|
215
|
+
private indexPath: string;
|
|
216
|
+
private index: AgentIndex | null = null;
|
|
217
|
+
|
|
218
|
+
constructor() {
|
|
219
|
+
this.agentsDir = config.getConfigPath("agents");
|
|
220
|
+
this.indexPath = path.join(this.agentsDir, "index.json");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Ensure storage directory exists and load index
|
|
225
|
+
*/
|
|
226
|
+
async initialize(): Promise<void> {
|
|
227
|
+
await ensureDirectory(this.agentsDir);
|
|
228
|
+
await this.loadIndex();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Load the index file or create empty index if it doesn't exist
|
|
233
|
+
*/
|
|
234
|
+
private async loadIndex(): Promise<void> {
|
|
235
|
+
if (await fileExists(this.indexPath)) {
|
|
236
|
+
try {
|
|
237
|
+
const content = await fs.readFile(this.indexPath, "utf-8");
|
|
238
|
+
const rawIndex = JSON.parse(content);
|
|
239
|
+
|
|
240
|
+
// Detect old format: bySlug is Record<string, string> instead of Record<string, SlugEntry>
|
|
241
|
+
const needsMigration = rawIndex.bySlug &&
|
|
242
|
+
Object.values(rawIndex.bySlug).some((val: any) => typeof val === "string");
|
|
243
|
+
|
|
244
|
+
if (needsMigration) {
|
|
245
|
+
logger.info("Migrating agent index from old format to multi-project slug structure");
|
|
246
|
+
this.index = this.migrateIndexFormat(rawIndex);
|
|
247
|
+
|
|
248
|
+
// Verify byProject is populated after migration
|
|
249
|
+
const hasValidByProject = this.index.byProject &&
|
|
250
|
+
Object.keys(this.index.byProject).length > 0;
|
|
251
|
+
|
|
252
|
+
if (!hasValidByProject) {
|
|
253
|
+
logger.warn("Migration produced empty byProject index, rebuilding from agent files");
|
|
254
|
+
await this.rebuildIndex();
|
|
255
|
+
} else {
|
|
256
|
+
await this.saveIndex();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
logger.info("Agent index migration complete", {
|
|
260
|
+
slugCount: Object.keys(this.index.bySlug).length,
|
|
261
|
+
projectCount: Object.keys(this.index.byProject).length,
|
|
262
|
+
});
|
|
263
|
+
} else {
|
|
264
|
+
this.index = rawIndex;
|
|
265
|
+
}
|
|
266
|
+
} catch (error) {
|
|
267
|
+
logger.error("Failed to load agent index, creating new one", { error });
|
|
268
|
+
this.index = { bySlug: {}, byEventId: {}, byProject: {} };
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
this.index = { bySlug: {}, byEventId: {}, byProject: {} };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Save the index file
|
|
277
|
+
*/
|
|
278
|
+
private async saveIndex(): Promise<void> {
|
|
279
|
+
if (!this.index) return;
|
|
280
|
+
await fs.writeFile(this.indexPath, JSON.stringify(this.index, null, 2));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Migrate index from old flat format to new SlugEntry structure.
|
|
285
|
+
* Old format: bySlug[slug] = pubkey
|
|
286
|
+
* New format: bySlug[slug] = { pubkey, projectIds: [] }
|
|
287
|
+
*
|
|
288
|
+
* This function returns a new AgentIndex object and does NOT mutate the input.
|
|
289
|
+
*/
|
|
290
|
+
private migrateIndexFormat(oldIndex: any): AgentIndex {
|
|
291
|
+
const newIndex: AgentIndex = {
|
|
292
|
+
bySlug: {},
|
|
293
|
+
byEventId: oldIndex.byEventId || {},
|
|
294
|
+
byProject: oldIndex.byProject || {},
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Build reverse lookup: pubkey -> projectIds[]
|
|
298
|
+
const pubkeyToProjects: Record<string, string[]> = {};
|
|
299
|
+
for (const [projectDTag, pubkeys] of Object.entries(oldIndex.byProject || {})) {
|
|
300
|
+
for (const pubkey of (pubkeys as string[])) {
|
|
301
|
+
if (!pubkeyToProjects[pubkey]) {
|
|
302
|
+
pubkeyToProjects[pubkey] = [];
|
|
303
|
+
}
|
|
304
|
+
pubkeyToProjects[pubkey].push(projectDTag);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Convert slug index
|
|
309
|
+
for (const [slug, pubkey] of Object.entries(oldIndex.bySlug || {})) {
|
|
310
|
+
if (typeof pubkey === "string") {
|
|
311
|
+
newIndex.bySlug[slug] = {
|
|
312
|
+
pubkey,
|
|
313
|
+
projectIds: pubkeyToProjects[pubkey] || [],
|
|
314
|
+
};
|
|
315
|
+
} else {
|
|
316
|
+
// Already in new format
|
|
317
|
+
newIndex.bySlug[slug] = pubkey as SlugEntry;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return newIndex;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Rebuild index by scanning all agent files.
|
|
326
|
+
*
|
|
327
|
+
* Rebuilds bySlug and byEventId from agent files.
|
|
328
|
+
* byProject cannot be rebuilt from agent files (project associations live only in the index),
|
|
329
|
+
* so it is left empty.
|
|
330
|
+
*
|
|
331
|
+
* ## Slug Index Priority
|
|
332
|
+
* Active agents take precedence over inactive agents for slug ownership.
|
|
333
|
+
* If multiple agents share a slug, the active one becomes canonical.
|
|
334
|
+
* If all agents with a slug are inactive, one is chosen arbitrarily.
|
|
335
|
+
*/
|
|
336
|
+
async rebuildIndex(): Promise<void> {
|
|
337
|
+
const index: AgentIndex = { bySlug: {}, byEventId: {}, byProject: {} };
|
|
338
|
+
// Track which slugs are owned by active agents
|
|
339
|
+
const activeSlugOwners = new Set<string>();
|
|
340
|
+
|
|
341
|
+
const files = await fs.readdir(this.agentsDir);
|
|
342
|
+
for (const file of files) {
|
|
343
|
+
if (!file.endsWith(".json") || file === "index.json") continue;
|
|
344
|
+
|
|
345
|
+
const pubkey = file.slice(0, -5); // Remove .json
|
|
346
|
+
try {
|
|
347
|
+
const agent = await this.loadAgent(pubkey);
|
|
348
|
+
if (!agent) continue;
|
|
349
|
+
|
|
350
|
+
const active = isAgentActive(agent);
|
|
351
|
+
|
|
352
|
+
// Update slug index - active agents take precedence
|
|
353
|
+
const existingEntry = index.bySlug[agent.slug];
|
|
354
|
+
if (existingEntry) {
|
|
355
|
+
if (existingEntry.pubkey !== pubkey) {
|
|
356
|
+
// Different agent with same slug - active takes precedence
|
|
357
|
+
if (active && !activeSlugOwners.has(agent.slug)) {
|
|
358
|
+
index.bySlug[agent.slug] = { pubkey, projectIds: [] };
|
|
359
|
+
activeSlugOwners.add(agent.slug);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Same agent already indexed - no merge needed (no projects in agent files)
|
|
363
|
+
} else {
|
|
364
|
+
index.bySlug[agent.slug] = { pubkey, projectIds: [] };
|
|
365
|
+
if (active) {
|
|
366
|
+
activeSlugOwners.add(agent.slug);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Update eventId index
|
|
371
|
+
if (agent.eventId) {
|
|
372
|
+
index.byEventId[agent.eventId] = pubkey;
|
|
373
|
+
}
|
|
374
|
+
} catch (error) {
|
|
375
|
+
logger.warn(`Failed to index agent file ${file}`, { error });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
this.index = index;
|
|
380
|
+
await this.saveIndex();
|
|
381
|
+
logger.info("Rebuilt agent index", {
|
|
382
|
+
agents: Object.keys(index.bySlug).length,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Find an alternative active agent that can own a slug.
|
|
388
|
+
* Used when the current slug owner becomes inactive.
|
|
389
|
+
*
|
|
390
|
+
* @param slug - The slug to find an alternative owner for
|
|
391
|
+
* @param excludePubkey - Pubkey to exclude from consideration (the current/transitioning owner)
|
|
392
|
+
* @returns The pubkey of an active agent with this slug, or null if none exists
|
|
393
|
+
*/
|
|
394
|
+
private async findAlternativeSlugOwner(slug: string, excludePubkey: string): Promise<string | null> {
|
|
395
|
+
const files = await fs.readdir(this.agentsDir);
|
|
396
|
+
|
|
397
|
+
for (const file of files) {
|
|
398
|
+
if (!file.endsWith(".json") || file === "index.json") continue;
|
|
399
|
+
|
|
400
|
+
const pubkey = file.slice(0, -5); // Remove .json
|
|
401
|
+
if (pubkey === excludePubkey) continue;
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
const agent = await this.loadAgent(pubkey);
|
|
405
|
+
if (!agent) continue;
|
|
406
|
+
|
|
407
|
+
if (agent.slug === slug && isAgentActive(agent)) {
|
|
408
|
+
return pubkey;
|
|
409
|
+
}
|
|
410
|
+
} catch {
|
|
411
|
+
// Skip agents that fail to load
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Load an agent by pubkey
|
|
420
|
+
*/
|
|
421
|
+
async loadAgent(pubkey: string): Promise<StoredAgent | null> {
|
|
422
|
+
const filePath = path.join(this.agentsDir, `${pubkey}.json`);
|
|
423
|
+
|
|
424
|
+
if (!(await fileExists(filePath))) {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
430
|
+
const agent: StoredAgent = JSON.parse(content);
|
|
431
|
+
return agent;
|
|
432
|
+
} catch (error) {
|
|
433
|
+
logger.error(`Failed to load agent ${pubkey}`, { error });
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Clean up old agents with the same slug in overlapping projects.
|
|
440
|
+
* When a new agent is saved with a slug that already exists,
|
|
441
|
+
* remove the old agent from projects that overlap with the new agent.
|
|
442
|
+
*/
|
|
443
|
+
private async cleanupDuplicateSlugs(
|
|
444
|
+
slug: string,
|
|
445
|
+
newPubkey: string,
|
|
446
|
+
newProjects: string[]
|
|
447
|
+
): Promise<void> {
|
|
448
|
+
if (!this.index) await this.loadIndex();
|
|
449
|
+
if (!this.index) return;
|
|
450
|
+
|
|
451
|
+
const existingEntry = this.index.bySlug[slug];
|
|
452
|
+
if (!existingEntry || existingEntry.pubkey === newPubkey) return;
|
|
453
|
+
|
|
454
|
+
// Find overlapping projects using the index (source of truth for project associations)
|
|
455
|
+
const existingProjects = this.getIndexProjectsForAgent(existingEntry.pubkey);
|
|
456
|
+
const overlappingProjects = existingProjects.filter((p) => newProjects.includes(p));
|
|
457
|
+
if (overlappingProjects.length === 0) return;
|
|
458
|
+
|
|
459
|
+
logger.info(`Cleaning up duplicate slug '${slug}'`, {
|
|
460
|
+
oldPubkey: existingEntry.pubkey.substring(0, 8),
|
|
461
|
+
newPubkey: newPubkey.substring(0, 8),
|
|
462
|
+
overlappingProjects,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// Emit telemetry for agent eviction
|
|
466
|
+
trace.getActiveSpan()?.addEvent("agent.slug_conflict_eviction", {
|
|
467
|
+
"conflict.slug": slug,
|
|
468
|
+
"conflict.evicted_pubkey": existingEntry.pubkey,
|
|
469
|
+
"conflict.incoming_pubkey": newPubkey,
|
|
470
|
+
"conflict.overlapping_projects": overlappingProjects.join(", "),
|
|
471
|
+
"conflict.overlapping_count": overlappingProjects.length,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Remove old agent from overlapping projects
|
|
475
|
+
for (const projectDTag of overlappingProjects) {
|
|
476
|
+
await this.removeAgentFromProject(existingEntry.pubkey, projectDTag);
|
|
477
|
+
|
|
478
|
+
// Update slug entry's project list
|
|
479
|
+
existingEntry.projectIds = (existingEntry.projectIds ?? []).filter(p => p !== projectDTag);
|
|
480
|
+
|
|
481
|
+
// Emit per-project eviction event for granular tracking
|
|
482
|
+
trace.getActiveSpan()?.addEvent("agent.evicted_from_project", {
|
|
483
|
+
"eviction.slug": slug,
|
|
484
|
+
"eviction.pubkey": existingEntry.pubkey,
|
|
485
|
+
"eviction.project": projectDTag,
|
|
486
|
+
"eviction.reason": "slug_conflict",
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// If old agent has no projects left, remove the slug entry entirely
|
|
491
|
+
if ((existingEntry.projectIds ?? []).length === 0) {
|
|
492
|
+
delete this.index.bySlug[slug];
|
|
493
|
+
logger.info(`Removed slug entry for '${slug}' - no projects remaining`);
|
|
494
|
+
|
|
495
|
+
trace.getActiveSpan()?.addEvent("agent.slug_entry_deleted", {
|
|
496
|
+
"slug.deleted": slug,
|
|
497
|
+
"slug.pubkey": existingEntry.pubkey,
|
|
498
|
+
"slug.reason": "no_projects_remaining",
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Save an agent and update index
|
|
505
|
+
*/
|
|
506
|
+
async saveAgent(agent: StoredAgent): Promise<void> {
|
|
507
|
+
// Get pubkey from nsec
|
|
508
|
+
const signer = new NDKPrivateKeySigner(agent.nsec);
|
|
509
|
+
const pubkey = signer.pubkey;
|
|
510
|
+
|
|
511
|
+
const filePath = path.join(this.agentsDir, `${pubkey}.json`);
|
|
512
|
+
|
|
513
|
+
// Load existing agent to check for changes
|
|
514
|
+
const existing = await this.loadAgent(pubkey);
|
|
515
|
+
|
|
516
|
+
// Get the agent's current projects from the index for duplicate slug cleanup
|
|
517
|
+
const currentProjects = this.getIndexProjectsForAgent(pubkey);
|
|
518
|
+
|
|
519
|
+
// Clean up old agents with same slug in overlapping projects
|
|
520
|
+
await this.cleanupDuplicateSlugs(agent.slug, pubkey, currentProjects);
|
|
521
|
+
|
|
522
|
+
// Save agent file
|
|
523
|
+
await fs.writeFile(filePath, JSON.stringify(agent, null, 2));
|
|
524
|
+
|
|
525
|
+
// Update index
|
|
526
|
+
if (!this.index) await this.loadIndex();
|
|
527
|
+
if (!this.index) return;
|
|
528
|
+
|
|
529
|
+
// Remove old slug entry if slug changed
|
|
530
|
+
if (existing && existing.slug !== agent.slug) {
|
|
531
|
+
const oldSlugEntry = this.index.bySlug[existing.slug];
|
|
532
|
+
if (oldSlugEntry && oldSlugEntry.pubkey === pubkey) {
|
|
533
|
+
delete this.index.bySlug[existing.slug];
|
|
534
|
+
trace.getActiveSpan()?.addEvent("agent.slug_renamed_cleanup", {
|
|
535
|
+
"slug.old": existing.slug,
|
|
536
|
+
"slug.new": agent.slug,
|
|
537
|
+
"agent.pubkey": pubkey,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Remove old eventId entry if eventId changed
|
|
543
|
+
if (
|
|
544
|
+
existing?.eventId &&
|
|
545
|
+
existing.eventId !== agent.eventId &&
|
|
546
|
+
this.index.byEventId[existing.eventId] === pubkey
|
|
547
|
+
) {
|
|
548
|
+
delete this.index.byEventId[existing.eventId];
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Update bySlug index
|
|
552
|
+
if (isAgentActive(agent)) {
|
|
553
|
+
const agentProjects = this.getIndexProjectsForAgent(pubkey);
|
|
554
|
+
const currentSlugEntry = this.index.bySlug[agent.slug];
|
|
555
|
+
if (!currentSlugEntry || currentSlugEntry.pubkey === pubkey) {
|
|
556
|
+
// Only update if we already own the slug or there's no current owner.
|
|
557
|
+
// Slug takeover for new agents happens in addAgentToProject after cleanup.
|
|
558
|
+
this.index.bySlug[agent.slug] = { pubkey, projectIds: agentProjects };
|
|
559
|
+
}
|
|
560
|
+
} else {
|
|
561
|
+
// For inactive agents, handle slug ownership transition
|
|
562
|
+
const currentOwner = this.index.bySlug[agent.slug];
|
|
563
|
+
if (currentOwner?.pubkey === pubkey) {
|
|
564
|
+
// This agent was the canonical owner but is now inactive
|
|
565
|
+
// Find another active agent with the same slug to take ownership
|
|
566
|
+
const alternativeOwner = await this.findAlternativeSlugOwner(agent.slug, pubkey);
|
|
567
|
+
if (alternativeOwner) {
|
|
568
|
+
const altProjects = this.getIndexProjectsForAgent(alternativeOwner);
|
|
569
|
+
this.index.bySlug[agent.slug] = {
|
|
570
|
+
pubkey: alternativeOwner,
|
|
571
|
+
projectIds: altProjects,
|
|
572
|
+
};
|
|
573
|
+
logger.debug(`Reassigned slug '${agent.slug}' from inactive ${pubkey.substring(0, 8)} to active ${alternativeOwner.substring(0, 8)}`);
|
|
574
|
+
} else {
|
|
575
|
+
// No alternative found - keep entry pointing to this agent with empty projects
|
|
576
|
+
currentOwner.projectIds = this.getIndexProjectsForAgent(pubkey);
|
|
577
|
+
}
|
|
578
|
+
} else if (!currentOwner) {
|
|
579
|
+
// No owner yet - claim it for reactivation lookup purposes
|
|
580
|
+
this.index.bySlug[agent.slug] = { pubkey, projectIds: [] };
|
|
581
|
+
}
|
|
582
|
+
// If another agent owns the slug, don't overwrite
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (agent.eventId) {
|
|
586
|
+
this.index.byEventId[agent.eventId] = pubkey;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
await this.saveIndex();
|
|
590
|
+
logger.debug(`Saved agent ${agent.slug} (${pubkey})`);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Delete an agent and update index.
|
|
595
|
+
*
|
|
596
|
+
* @deprecated This method permanently deletes agent identity (pubkey/nsec).
|
|
597
|
+
* Prefer using removeAgentFromProject() which sets agents to 'inactive' status
|
|
598
|
+
* while preserving their identity for potential reactivation.
|
|
599
|
+
*
|
|
600
|
+
* This method is kept for:
|
|
601
|
+
* - Administrative cleanup of truly orphaned agents
|
|
602
|
+
* - Test teardown
|
|
603
|
+
* - Explicit user-requested deletion
|
|
604
|
+
*
|
|
605
|
+
* @param pubkey - Agent's public key to delete
|
|
606
|
+
*/
|
|
607
|
+
async deleteAgent(pubkey: string): Promise<void> {
|
|
608
|
+
const agent = await this.loadAgent(pubkey);
|
|
609
|
+
if (!agent) return;
|
|
610
|
+
|
|
611
|
+
logger.warn(
|
|
612
|
+
`deleteAgent called for ${agent.slug} (${pubkey.substring(0, 8)}) - ` +
|
|
613
|
+
`this permanently destroys agent identity. Consider using removeAgentFromProject instead.`
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
// Delete file
|
|
617
|
+
const filePath = path.join(this.agentsDir, `${pubkey}.json`);
|
|
618
|
+
await fs.unlink(filePath);
|
|
619
|
+
|
|
620
|
+
// Update index
|
|
621
|
+
if (!this.index) await this.loadIndex();
|
|
622
|
+
if (!this.index) return;
|
|
623
|
+
|
|
624
|
+
// Remove from slug index
|
|
625
|
+
const slugEntry = this.index.bySlug[agent.slug];
|
|
626
|
+
if (slugEntry?.pubkey === pubkey) {
|
|
627
|
+
delete this.index.bySlug[agent.slug];
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Remove from eventId index
|
|
631
|
+
if (agent.eventId && this.index.byEventId[agent.eventId] === pubkey) {
|
|
632
|
+
delete this.index.byEventId[agent.eventId];
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Remove from project index by scanning byProject
|
|
636
|
+
for (const projectDTag of Object.keys(this.index.byProject)) {
|
|
637
|
+
const projectAgents = this.index.byProject[projectDTag];
|
|
638
|
+
if (projectAgents.includes(pubkey)) {
|
|
639
|
+
this.index.byProject[projectDTag] = projectAgents.filter((p) => p !== pubkey);
|
|
640
|
+
if (this.index.byProject[projectDTag].length === 0) {
|
|
641
|
+
delete this.index.byProject[projectDTag];
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
await this.saveIndex();
|
|
647
|
+
logger.info(`Deleted agent ${agent.slug} (${pubkey})`);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Check if any agent (in any project) uses the given slug.
|
|
652
|
+
* Used for global uniqueness checks (e.g., import commands).
|
|
653
|
+
*/
|
|
654
|
+
async slugExists(slug: string): Promise<boolean> {
|
|
655
|
+
if (!this.index) await this.loadIndex();
|
|
656
|
+
return !!this.index?.bySlug[slug];
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Get agent by slug (uses index for O(1) lookup).
|
|
661
|
+
*
|
|
662
|
+
* **DEPRECATED**: Use getAgentBySlugForProject() instead for project-scoped lookups.
|
|
663
|
+
* This method returns the LAST agent saved with this slug, which may not be the
|
|
664
|
+
* correct agent when multiple agents use the same slug across different projects.
|
|
665
|
+
*
|
|
666
|
+
* @deprecated Use getAgentBySlugForProject(slug, projectDTag) instead
|
|
667
|
+
*/
|
|
668
|
+
async getAgentBySlug(slug: string): Promise<StoredAgent | null> {
|
|
669
|
+
if (!this.index) await this.loadIndex();
|
|
670
|
+
if (!this.index) return null;
|
|
671
|
+
|
|
672
|
+
const slugEntry = this.index.bySlug[slug];
|
|
673
|
+
if (!slugEntry) return null;
|
|
674
|
+
|
|
675
|
+
logger.warn("Using deprecated getAgentBySlug() - consider using getAgentBySlugForProject()", {
|
|
676
|
+
slug,
|
|
677
|
+
pubkey: slugEntry.pubkey.substring(0, 8),
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
return this.loadAgent(slugEntry.pubkey);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Get agent by slug within a specific project context.
|
|
685
|
+
* This is the correct method to use when slug may not be globally unique.
|
|
686
|
+
*
|
|
687
|
+
* @param slug - The agent slug to search for
|
|
688
|
+
* @param projectDTag - The project context to search within
|
|
689
|
+
* @returns The agent if found in this project, null otherwise
|
|
690
|
+
*/
|
|
691
|
+
async getAgentBySlugForProject(slug: string, projectDTag: string): Promise<StoredAgent | null> {
|
|
692
|
+
if (!this.index) await this.loadIndex();
|
|
693
|
+
if (!this.index) return null;
|
|
694
|
+
|
|
695
|
+
const slugEntry = this.index.bySlug[slug];
|
|
696
|
+
if (!slugEntry) return null;
|
|
697
|
+
|
|
698
|
+
// Check if this slug is used in the specified project
|
|
699
|
+
if (!(slugEntry.projectIds ?? []).includes(projectDTag)) {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return this.loadAgent(slugEntry.pubkey);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Get agent by eventId (uses index for O(1) lookup)
|
|
708
|
+
*/
|
|
709
|
+
async getAgentByEventId(eventId: string): Promise<StoredAgent | null> {
|
|
710
|
+
if (!this.index) await this.loadIndex();
|
|
711
|
+
if (!this.index) return null;
|
|
712
|
+
|
|
713
|
+
const pubkey = this.index.byEventId[eventId];
|
|
714
|
+
if (!pubkey) return null;
|
|
715
|
+
|
|
716
|
+
return this.loadAgent(pubkey);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Get all agents for a project (uses index for O(1) lookup).
|
|
721
|
+
* Deduplicates by slug, keeping only the agent currently in bySlug index.
|
|
722
|
+
*
|
|
723
|
+
* Only returns active agents - inactive agents (removed from all projects
|
|
724
|
+
* but identity preserved) are filtered out.
|
|
725
|
+
*/
|
|
726
|
+
async getProjectAgents(projectDTag: string): Promise<StoredAgent[]> {
|
|
727
|
+
if (!this.index) await this.loadIndex();
|
|
728
|
+
if (!this.index) return [];
|
|
729
|
+
|
|
730
|
+
const pubkeys = this.index.byProject[projectDTag] || [];
|
|
731
|
+
const agents: StoredAgent[] = [];
|
|
732
|
+
const seenSlugs = new Set<string>();
|
|
733
|
+
|
|
734
|
+
for (const pubkey of pubkeys) {
|
|
735
|
+
const agent = await this.loadAgent(pubkey);
|
|
736
|
+
if (!agent) continue;
|
|
737
|
+
|
|
738
|
+
// Skip inactive agents - they shouldn't appear in project listings
|
|
739
|
+
if (!isAgentActive(agent)) continue;
|
|
740
|
+
|
|
741
|
+
// Skip if we've already seen this slug - keep only the canonical one
|
|
742
|
+
if (seenSlugs.has(agent.slug)) continue;
|
|
743
|
+
|
|
744
|
+
// Only include if this pubkey is the canonical one for this slug
|
|
745
|
+
const slugEntry = this.index.bySlug[agent.slug];
|
|
746
|
+
if (slugEntry?.pubkey === pubkey) {
|
|
747
|
+
agents.push(agent);
|
|
748
|
+
seenSlugs.add(agent.slug);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return agents;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Get all projects for an agent (reverse lookup by pubkey via index)
|
|
757
|
+
*/
|
|
758
|
+
async getAgentProjects(pubkey: string): Promise<string[]> {
|
|
759
|
+
if (!this.index) await this.loadIndex();
|
|
760
|
+
return this.getIndexProjectsForAgent(pubkey);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Scan byProject index and return all dTags where pubkey appears.
|
|
765
|
+
*/
|
|
766
|
+
private getIndexProjectsForAgent(pubkey: string): string[] {
|
|
767
|
+
if (!this.index) return [];
|
|
768
|
+
return Object.entries(this.index.byProject)
|
|
769
|
+
.filter(([, pubkeys]) => pubkeys.includes(pubkey))
|
|
770
|
+
.map(([dTag]) => dTag);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Add an agent to a project.
|
|
775
|
+
*
|
|
776
|
+
* If the agent was previously inactive (removed from all projects), this
|
|
777
|
+
* reactivates the agent, preserving its original identity (pubkey/nsec).
|
|
778
|
+
*/
|
|
779
|
+
async addAgentToProject(pubkey: string, projectDTag: string): Promise<void> {
|
|
780
|
+
const agent = await this.loadAgent(pubkey);
|
|
781
|
+
if (!agent) {
|
|
782
|
+
throw new Error(`Agent ${pubkey} not found`);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (!this.index) await this.loadIndex();
|
|
786
|
+
if (!this.index) return;
|
|
787
|
+
|
|
788
|
+
const wasInactive = !isAgentActive(agent);
|
|
789
|
+
|
|
790
|
+
// Update byProject index
|
|
791
|
+
// Clean up any agent with the same slug already in this project
|
|
792
|
+
await this.cleanupDuplicateSlugs(agent.slug, pubkey, [projectDTag]);
|
|
793
|
+
|
|
794
|
+
if (!this.index.byProject[projectDTag]) {
|
|
795
|
+
this.index.byProject[projectDTag] = [];
|
|
796
|
+
}
|
|
797
|
+
if (!this.index.byProject[projectDTag].includes(pubkey)) {
|
|
798
|
+
this.index.byProject[projectDTag].push(pubkey);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Update bySlug index - the last agent added to a project claims slug ownership
|
|
802
|
+
const slugEntry = this.index.bySlug[agent.slug];
|
|
803
|
+
if (slugEntry?.pubkey === pubkey) {
|
|
804
|
+
slugEntry.projectIds ??= [];
|
|
805
|
+
if (!slugEntry.projectIds.includes(projectDTag)) {
|
|
806
|
+
slugEntry.projectIds.push(projectDTag);
|
|
807
|
+
}
|
|
808
|
+
} else {
|
|
809
|
+
// Take over slug ownership (cleanup already evicted any conflicting agents above)
|
|
810
|
+
this.index.bySlug[agent.slug] = { pubkey, projectIds: [projectDTag] };
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Reactivate if agent was inactive
|
|
814
|
+
agent.status = "active";
|
|
815
|
+
await this.saveAgent(agent);
|
|
816
|
+
|
|
817
|
+
if (wasInactive) {
|
|
818
|
+
logger.info(`Reactivated agent ${agent.slug} (${pubkey.substring(0, 8)}) for project ${projectDTag}`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Remove an agent from a project.
|
|
824
|
+
*
|
|
825
|
+
* ## Identity Preservation Policy
|
|
826
|
+
* When an agent is removed from all projects, it becomes 'inactive' rather than
|
|
827
|
+
* being deleted. This preserves the agent's identity (pubkey/nsec) so that if
|
|
828
|
+
* the same agent is later assigned to a project, it retains its original keys.
|
|
829
|
+
*
|
|
830
|
+
* Inactive agents:
|
|
831
|
+
* - Are NOT returned by getProjectAgents()
|
|
832
|
+
* - Retain their pubkey, nsec, slug, and all configuration
|
|
833
|
+
* - Can be reactivated by addAgentToProject()
|
|
834
|
+
*/
|
|
835
|
+
async removeAgentFromProject(pubkey: string, projectDTag: string): Promise<void> {
|
|
836
|
+
const agent = await this.loadAgent(pubkey);
|
|
837
|
+
if (!agent) return;
|
|
838
|
+
|
|
839
|
+
if (!this.index) await this.loadIndex();
|
|
840
|
+
if (!this.index) return;
|
|
841
|
+
|
|
842
|
+
// Update byProject index
|
|
843
|
+
const projectAgents = this.index.byProject[projectDTag];
|
|
844
|
+
if (projectAgents) {
|
|
845
|
+
this.index.byProject[projectDTag] = projectAgents.filter((p) => p !== pubkey);
|
|
846
|
+
if (this.index.byProject[projectDTag].length === 0) {
|
|
847
|
+
delete this.index.byProject[projectDTag];
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Update bySlug index projectIds
|
|
852
|
+
const slugEntry = this.index.bySlug[agent.slug];
|
|
853
|
+
if (slugEntry?.pubkey === pubkey) {
|
|
854
|
+
slugEntry.projectIds = (slugEntry.projectIds ?? []).filter((p) => p !== projectDTag);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Set status based on remaining projects - NEVER delete agent files
|
|
858
|
+
const remainingProjects = this.getIndexProjectsForAgent(pubkey);
|
|
859
|
+
agent.status = remainingProjects.length === 0 ? "inactive" : "active";
|
|
860
|
+
await this.saveAgent(agent);
|
|
861
|
+
|
|
862
|
+
if (agent.status === "inactive") {
|
|
863
|
+
logger.info(`Agent ${agent.slug} (${pubkey.substring(0, 8)}) marked inactive - identity preserved`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Update an agent's global PM designation flag.
|
|
869
|
+
*
|
|
870
|
+
* When isPM is true, this agent becomes the PM for ALL projects where it exists.
|
|
871
|
+
* This takes precedence over pmOverrides and project tag designations.
|
|
872
|
+
*
|
|
873
|
+
* Updates ONLY the stored data on disk. To refresh the in-memory instance,
|
|
874
|
+
* call AgentRegistry.reloadAgent() after this method.
|
|
875
|
+
*
|
|
876
|
+
* @param pubkey - Agent's public key (hex string)
|
|
877
|
+
* @param isPM - Whether this agent is designated as PM (true/false/undefined to clear)
|
|
878
|
+
* @returns true if updated successfully, false if agent not found
|
|
879
|
+
*/
|
|
880
|
+
async updateAgentIsPM(pubkey: string, isPM: boolean | undefined): Promise<boolean> {
|
|
881
|
+
const agent = await this.loadAgent(pubkey);
|
|
882
|
+
if (!agent) {
|
|
883
|
+
logger.warn(`Agent with pubkey ${pubkey} not found`);
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (isPM === undefined || isPM === false) {
|
|
888
|
+
// Clear the flag if it exists
|
|
889
|
+
delete agent.isPM;
|
|
890
|
+
} else {
|
|
891
|
+
agent.isPM = true;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
await this.saveAgent(agent);
|
|
895
|
+
logger.info(`Updated isPM flag for agent ${agent.name}`, { isPM: agent.isPM });
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Get the effective (resolved) config for an agent, optionally scoped to a project.
|
|
901
|
+
*
|
|
902
|
+
* @param agent - The stored agent
|
|
903
|
+
* @param projectDTag - Optional project dTag for project-scoped resolution
|
|
904
|
+
* @returns ResolvedAgentConfig with effective model and tools
|
|
905
|
+
*/
|
|
906
|
+
getEffectiveConfig(agent: StoredAgent, projectDTag?: string): ResolvedAgentConfig {
|
|
907
|
+
const defaultConfig: AgentDefaultConfig = {
|
|
908
|
+
model: agent.default?.model,
|
|
909
|
+
tools: agent.default?.tools,
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
const projectConfig = projectDTag
|
|
913
|
+
? agent.projectOverrides?.[projectDTag]
|
|
914
|
+
: undefined;
|
|
915
|
+
|
|
916
|
+
return resolveEffectiveConfig(defaultConfig, projectConfig);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Update an agent's default configuration block.
|
|
921
|
+
*
|
|
922
|
+
* A 24020 event with NO a-tag should call this method.
|
|
923
|
+
* Writes to the `default` block in the agent file.
|
|
924
|
+
*
|
|
925
|
+
* @param pubkey - Agent's public key
|
|
926
|
+
* @param updates - Fields to update. Only defined fields are applied.
|
|
927
|
+
* @param options - Optional behavior flags
|
|
928
|
+
* @param options.clearProjectOverrides - If true, clears all projectOverrides (default: false)
|
|
929
|
+
* @returns true if updated successfully, false if agent not found
|
|
930
|
+
*/
|
|
931
|
+
async updateDefaultConfig(
|
|
932
|
+
pubkey: string,
|
|
933
|
+
updates: AgentDefaultConfig,
|
|
934
|
+
options?: UpdateDefaultConfigOptions
|
|
935
|
+
): Promise<boolean> {
|
|
936
|
+
const agent = await this.loadAgent(pubkey);
|
|
937
|
+
if (!agent) {
|
|
938
|
+
logger.warn(`Agent with pubkey ${pubkey} not found`);
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (!agent.default) {
|
|
943
|
+
agent.default = {};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (updates.model !== undefined) {
|
|
947
|
+
agent.default.model = updates.model;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (updates.tools !== undefined) {
|
|
951
|
+
if (updates.tools.length > 0) {
|
|
952
|
+
agent.default.tools = updates.tools;
|
|
953
|
+
} else {
|
|
954
|
+
delete agent.default.tools;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Clean up empty default block
|
|
959
|
+
if (agent.default && Object.keys(agent.default).length === 0) {
|
|
960
|
+
delete agent.default;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Clear all project overrides when a global config update is received
|
|
964
|
+
if (options?.clearProjectOverrides && agent.projectOverrides) {
|
|
965
|
+
delete agent.projectOverrides;
|
|
966
|
+
logger.info(`Cleared projectOverrides for agent ${agent.name} (global config update)`);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
await this.saveAgent(agent);
|
|
970
|
+
logger.info(`Updated default config for agent ${agent.name}`, { updates });
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Update an agent's per-project override configuration.
|
|
976
|
+
*
|
|
977
|
+
* A 24020 event WITH an a-tag should call this method.
|
|
978
|
+
* Writes to `projectOverrides[projectDTag]`.
|
|
979
|
+
*
|
|
980
|
+
* If any provided values equal the defaults after resolution, they are cleared
|
|
981
|
+
* from the override (dedup logic) to keep overrides minimal.
|
|
982
|
+
*
|
|
983
|
+
* If `reset` is true, clears the entire project override.
|
|
984
|
+
*
|
|
985
|
+
* @param pubkey - Agent's public key
|
|
986
|
+
* @param projectDTag - Project dTag to scope the config to
|
|
987
|
+
* @param override - The new project override (full replacement, not merge)
|
|
988
|
+
* @param reset - If true, clear the entire project override instead of setting it
|
|
989
|
+
* @returns true if updated successfully, false if agent not found
|
|
990
|
+
*/
|
|
991
|
+
async updateProjectOverride(
|
|
992
|
+
pubkey: string,
|
|
993
|
+
projectDTag: string,
|
|
994
|
+
override: AgentProjectConfig,
|
|
995
|
+
reset = false
|
|
996
|
+
): Promise<boolean> {
|
|
997
|
+
const agent = await this.loadAgent(pubkey);
|
|
998
|
+
if (!agent) {
|
|
999
|
+
logger.warn(`Agent with pubkey ${pubkey} not found`);
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (reset) {
|
|
1004
|
+
if (agent.projectOverrides) {
|
|
1005
|
+
delete agent.projectOverrides[projectDTag];
|
|
1006
|
+
if (Object.keys(agent.projectOverrides).length === 0) {
|
|
1007
|
+
delete agent.projectOverrides;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
logger.info(`Cleared project override for agent ${agent.name}`, { projectDTag });
|
|
1011
|
+
} else {
|
|
1012
|
+
const defaultConfig: AgentDefaultConfig = {
|
|
1013
|
+
model: agent.default?.model,
|
|
1014
|
+
tools: agent.default?.tools,
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
const deduplicated = deduplicateProjectConfig(defaultConfig, override);
|
|
1018
|
+
|
|
1019
|
+
if (!agent.projectOverrides) {
|
|
1020
|
+
agent.projectOverrides = {};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (Object.keys(deduplicated).length === 0) {
|
|
1024
|
+
delete agent.projectOverrides[projectDTag];
|
|
1025
|
+
if (Object.keys(agent.projectOverrides).length === 0) {
|
|
1026
|
+
delete agent.projectOverrides;
|
|
1027
|
+
}
|
|
1028
|
+
logger.info(`Project override for ${projectDTag} cleared (all fields match defaults)`, {
|
|
1029
|
+
agentSlug: agent.slug,
|
|
1030
|
+
});
|
|
1031
|
+
} else {
|
|
1032
|
+
agent.projectOverrides[projectDTag] = deduplicated;
|
|
1033
|
+
logger.info(`Updated project override for agent ${agent.name}`, {
|
|
1034
|
+
projectDTag,
|
|
1035
|
+
override: deduplicated,
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
await this.saveAgent(agent);
|
|
1041
|
+
return true;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Resolve the effective PM status for an agent in a specific project.
|
|
1046
|
+
* Priority:
|
|
1047
|
+
* 1. agent.isPM (global PM designation via kind 24020 without a-tag)
|
|
1048
|
+
* 2. projectOverrides[projectDTag].isPM (project-scoped PM via kind 24020 with a-tag)
|
|
1049
|
+
* 3. pmOverrides[projectDTag] (legacy, for backward compatibility)
|
|
1050
|
+
*/
|
|
1051
|
+
resolveEffectiveIsPM(agent: StoredAgent, projectDTag: string): boolean {
|
|
1052
|
+
if (agent.isPM === true) {
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
if (agent.projectOverrides?.[projectDTag]?.isPM === true) {
|
|
1056
|
+
return true;
|
|
1057
|
+
}
|
|
1058
|
+
return agent.pmOverrides?.[projectDTag] === true;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Update an agent's project-scoped PM designation.
|
|
1063
|
+
* Writes to projectOverrides[projectDTag].isPM.
|
|
1064
|
+
*
|
|
1065
|
+
* @param pubkey - Agent's public key
|
|
1066
|
+
* @param projectDTag - Project dTag to scope the config to
|
|
1067
|
+
* @param isPM - PM designation (true/false/undefined to clear)
|
|
1068
|
+
* @returns true if updated successfully, false if agent not found
|
|
1069
|
+
*/
|
|
1070
|
+
async updateProjectScopedIsPM(
|
|
1071
|
+
pubkey: string,
|
|
1072
|
+
projectDTag: string,
|
|
1073
|
+
isPM: boolean | undefined
|
|
1074
|
+
): Promise<boolean> {
|
|
1075
|
+
const agent = await this.loadAgent(pubkey);
|
|
1076
|
+
if (!agent) {
|
|
1077
|
+
logger.warn(`Agent with pubkey ${pubkey} not found`);
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (isPM === true) {
|
|
1082
|
+
if (!agent.projectOverrides) {
|
|
1083
|
+
agent.projectOverrides = {};
|
|
1084
|
+
}
|
|
1085
|
+
if (!agent.projectOverrides[projectDTag]) {
|
|
1086
|
+
agent.projectOverrides[projectDTag] = {};
|
|
1087
|
+
}
|
|
1088
|
+
agent.projectOverrides[projectDTag].isPM = true;
|
|
1089
|
+
} else {
|
|
1090
|
+
const override = agent.projectOverrides?.[projectDTag];
|
|
1091
|
+
if (override) {
|
|
1092
|
+
delete override.isPM;
|
|
1093
|
+
if (Object.keys(override).length === 0) {
|
|
1094
|
+
delete agent.projectOverrides![projectDTag];
|
|
1095
|
+
if (Object.keys(agent.projectOverrides!).length === 0) {
|
|
1096
|
+
delete agent.projectOverrides;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
await this.saveAgent(agent);
|
|
1103
|
+
logger.info(`Updated project-scoped PM flag for agent ${agent.name}`, {
|
|
1104
|
+
projectDTag,
|
|
1105
|
+
isPM,
|
|
1106
|
+
});
|
|
1107
|
+
return true;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Get all agents (for debugging/admin purposes)
|
|
1112
|
+
*/
|
|
1113
|
+
async getAllAgents(): Promise<StoredAgent[]> {
|
|
1114
|
+
await ensureDirectory(this.agentsDir);
|
|
1115
|
+
const files = await fs.readdir(this.agentsDir);
|
|
1116
|
+
const agents: StoredAgent[] = [];
|
|
1117
|
+
|
|
1118
|
+
for (const file of files) {
|
|
1119
|
+
if (!file.endsWith(".json") || file === "index.json") continue;
|
|
1120
|
+
|
|
1121
|
+
const pubkey = file.slice(0, -5);
|
|
1122
|
+
const agent = await this.loadAgent(pubkey);
|
|
1123
|
+
if (agent) {
|
|
1124
|
+
agents.push(agent);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return agents;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// Export singleton instance
|
|
1133
|
+
export const agentStorage = new AgentStorage();
|