@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,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Todo Tools - Conversation-scoped todo list management for agents
|
|
3
|
+
*
|
|
4
|
+
* Provides todo_write functionality for full state replacement.
|
|
5
|
+
* Todos are stored on the Conversation object and persisted with it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ConversationToolContext } from "@/tools/types";
|
|
9
|
+
import type { ConversationStore } from "@/conversations/ConversationStore";
|
|
10
|
+
import type { TodoItem, TodoStatus } from "@/services/ral/types";
|
|
11
|
+
import type { AISdkTool } from "@/tools/types";
|
|
12
|
+
import { tool } from "ai";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Helper functions
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
function getCurrentTimestamp(): number {
|
|
20
|
+
return Date.now();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Simple deterministic hash for generating fallback IDs.
|
|
25
|
+
* Returns a hex string based on the input.
|
|
26
|
+
*/
|
|
27
|
+
function simpleHash(str: string): string {
|
|
28
|
+
let hash = 0;
|
|
29
|
+
for (let i = 0; i < str.length; i++) {
|
|
30
|
+
const char = str.charCodeAt(i);
|
|
31
|
+
hash = ((hash << 5) - hash) + char;
|
|
32
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
33
|
+
}
|
|
34
|
+
// Convert to positive hex string
|
|
35
|
+
return Math.abs(hash).toString(16);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generates a slug-style ID from a title.
|
|
40
|
+
* Converts to lowercase, replaces spaces/special chars with hyphens, and removes consecutive hyphens.
|
|
41
|
+
* Falls back to a deterministic hash if the title produces an empty slug (e.g., emoji-only or non-ASCII titles).
|
|
42
|
+
*/
|
|
43
|
+
function generateIdFromTitle(title: string): string {
|
|
44
|
+
const slug = title
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replace(/[^a-z0-9\s-]/g, "") // Remove special characters except spaces and hyphens
|
|
47
|
+
.replace(/\s+/g, "-") // Replace spaces with hyphens
|
|
48
|
+
.replace(/-+/g, "-") // Replace consecutive hyphens with single hyphen
|
|
49
|
+
.replace(/^-|-$/g, ""); // Remove leading/trailing hyphens
|
|
50
|
+
|
|
51
|
+
// Fall back to hash if slug is empty (non-ASCII/emoji-only titles)
|
|
52
|
+
if (slug === "") {
|
|
53
|
+
return `todo-${simpleHash(title)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return slug;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validates and writes the complete todo list, replacing all existing items.
|
|
61
|
+
* Implements safety check to prevent accidental deletions.
|
|
62
|
+
*/
|
|
63
|
+
function writeTodosToConversation(
|
|
64
|
+
conversation: ConversationStore,
|
|
65
|
+
agentPubkey: string,
|
|
66
|
+
newItems: Array<{
|
|
67
|
+
id: string;
|
|
68
|
+
title: string;
|
|
69
|
+
description?: string; // undefined means "preserve existing" for updates
|
|
70
|
+
status: TodoStatus;
|
|
71
|
+
skip_reason?: string;
|
|
72
|
+
}>,
|
|
73
|
+
force: boolean
|
|
74
|
+
): {
|
|
75
|
+
success: boolean;
|
|
76
|
+
items: TodoItem[];
|
|
77
|
+
error?: string;
|
|
78
|
+
missingIds?: string[];
|
|
79
|
+
} {
|
|
80
|
+
const existingTodos = conversation.getTodos(agentPubkey);
|
|
81
|
+
const now = getCurrentTimestamp();
|
|
82
|
+
|
|
83
|
+
// Build a map of new item IDs for quick lookup
|
|
84
|
+
const newItemIds = new Set(newItems.map((item) => item.id));
|
|
85
|
+
|
|
86
|
+
// Check for duplicate IDs in the input array
|
|
87
|
+
if (newItemIds.size !== newItems.length) {
|
|
88
|
+
const seen = new Set<string>();
|
|
89
|
+
const duplicates: string[] = [];
|
|
90
|
+
for (const item of newItems) {
|
|
91
|
+
if (seen.has(item.id)) {
|
|
92
|
+
duplicates.push(item.id);
|
|
93
|
+
}
|
|
94
|
+
seen.add(item.id);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
items: [],
|
|
99
|
+
error: `Duplicate IDs in input: ${duplicates.join(", ")}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Safety check: find any existing IDs that are missing from the new list
|
|
104
|
+
const existingIds = existingTodos.map((t) => t.id);
|
|
105
|
+
const missingIds = existingIds.filter((id) => !newItemIds.has(id));
|
|
106
|
+
|
|
107
|
+
if (missingIds.length > 0 && !force) {
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
items: [],
|
|
111
|
+
error: `Safety check failed: ${missingIds.length} existing item(s) would be removed. Use force=true to allow removal.`,
|
|
112
|
+
missingIds,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Validate skip_reason requirement
|
|
117
|
+
for (const item of newItems) {
|
|
118
|
+
if (item.status === "skipped" && !item.skip_reason) {
|
|
119
|
+
return {
|
|
120
|
+
success: false,
|
|
121
|
+
items: [],
|
|
122
|
+
error: `Validation failed: skip_reason is required when status='skipped' (item: ${item.id})`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Build the new todo list, preserving timestamps for existing items
|
|
128
|
+
// Array order determines position (index-based ordering)
|
|
129
|
+
const existingMap = new Map(existingTodos.map((t) => [t.id, t]));
|
|
130
|
+
const newTodos: TodoItem[] = newItems.map((item) => {
|
|
131
|
+
const existing = existingMap.get(item.id);
|
|
132
|
+
return {
|
|
133
|
+
id: item.id,
|
|
134
|
+
title: item.title,
|
|
135
|
+
// Preserve existing description if not provided in update (undefined means "keep existing")
|
|
136
|
+
description: item.description ?? existing?.description ?? "",
|
|
137
|
+
status: item.status,
|
|
138
|
+
skipReason: item.skip_reason,
|
|
139
|
+
createdAt: existing?.createdAt ?? now,
|
|
140
|
+
updatedAt: existing && existing.status === item.status ? existing.updatedAt : now,
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Persist the new list
|
|
145
|
+
conversation.setTodos(agentPubkey, newTodos);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
success: true,
|
|
149
|
+
items: newTodos,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// todo_write
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
const todoWriteItemSchema = z.object({
|
|
158
|
+
id: z.string().optional().describe("Optional unique identifier. Auto-generated from title if not provided."),
|
|
159
|
+
title: z.string().describe("Short human-readable title for the todo item"),
|
|
160
|
+
description: z.string().optional().describe("Optional detailed description. When updating existing items, omitting preserves the current description. For new items, defaults to empty string."),
|
|
161
|
+
status: z
|
|
162
|
+
.enum(["pending", "in_progress", "done", "skipped"])
|
|
163
|
+
.describe("Current status of the item"),
|
|
164
|
+
skip_reason: z
|
|
165
|
+
.string()
|
|
166
|
+
.optional()
|
|
167
|
+
.describe("Required when status='skipped' - explain why this item was skipped"),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const todoWriteSchema = z.object({
|
|
171
|
+
todos: z.array(todoWriteItemSchema).describe("The complete todo list. All items must be provided - this replaces the entire list."),
|
|
172
|
+
force: z
|
|
173
|
+
.boolean()
|
|
174
|
+
.optional()
|
|
175
|
+
.default(false)
|
|
176
|
+
.describe("When true, allows removing items from the list. Default false (safety check prevents removals)."),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
type TodoWriteInput = z.infer<typeof todoWriteSchema>;
|
|
180
|
+
|
|
181
|
+
interface TodoWriteOutput {
|
|
182
|
+
success: boolean;
|
|
183
|
+
message: string;
|
|
184
|
+
totalItems: number;
|
|
185
|
+
error?: string;
|
|
186
|
+
missingIds?: string[];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function executeTodoWrite(
|
|
190
|
+
input: TodoWriteInput,
|
|
191
|
+
context: ConversationToolContext
|
|
192
|
+
): Promise<TodoWriteOutput> {
|
|
193
|
+
const conversation = context.getConversation();
|
|
194
|
+
|
|
195
|
+
const result = writeTodosToConversation(
|
|
196
|
+
conversation,
|
|
197
|
+
context.agent.pubkey,
|
|
198
|
+
input.todos.map((item) => ({
|
|
199
|
+
id: item.id ?? generateIdFromTitle(item.title),
|
|
200
|
+
title: item.title,
|
|
201
|
+
description: item.description, // Pass undefined to preserve existing; handled in writeTodosToConversation
|
|
202
|
+
status: item.status as TodoStatus,
|
|
203
|
+
skip_reason: item.skip_reason,
|
|
204
|
+
})),
|
|
205
|
+
input.force ?? false
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Generate a concise message based on the operation
|
|
209
|
+
let message: string;
|
|
210
|
+
if (!result.success) {
|
|
211
|
+
message = result.error || "Failed to write todos";
|
|
212
|
+
} else if (result.items.length === 0) {
|
|
213
|
+
message = "Todo list cleared";
|
|
214
|
+
} else if (result.items.length === 1) {
|
|
215
|
+
message = `Todo list updated with 1 item`;
|
|
216
|
+
} else {
|
|
217
|
+
message = `Todo list updated with ${result.items.length} items`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
success: result.success,
|
|
222
|
+
message,
|
|
223
|
+
totalItems: result.items.length,
|
|
224
|
+
error: result.error,
|
|
225
|
+
missingIds: result.missingIds,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function createTodoWriteTool(context: ConversationToolContext): AISdkTool {
|
|
230
|
+
const aiTool = tool({
|
|
231
|
+
description:
|
|
232
|
+
"Write the complete todo list, replacing all existing items. Provide ALL items you want to exist - " +
|
|
233
|
+
"this is a full state replacement. By default, removing existing items is blocked (safety check). " +
|
|
234
|
+
"Set force=true to allow item removal. Each item requires: title and status. " +
|
|
235
|
+
"Optional: id (auto-generated from title if not provided), description (preserved on update if omitted, empty for new items). " +
|
|
236
|
+
"Use skip_reason when status='skipped'. " +
|
|
237
|
+
"NOTE: If an existing item used a custom ID and you omit the id field, the auto-generated ID won't match, " +
|
|
238
|
+
"which will trigger the safety check (or require force=true). Always include IDs for items with custom IDs.",
|
|
239
|
+
inputSchema: todoWriteSchema,
|
|
240
|
+
execute: async (input: TodoWriteInput) => {
|
|
241
|
+
return await executeTodoWrite(input, context);
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
Object.defineProperty(aiTool, "getHumanReadableContent", {
|
|
246
|
+
value: ({ todos }: TodoWriteInput) => {
|
|
247
|
+
if (todos.length === 0) {
|
|
248
|
+
return "Clearing todo list";
|
|
249
|
+
}
|
|
250
|
+
if (todos.length === 1) {
|
|
251
|
+
return `Writing todo list with 1 item: "${todos[0].title}"`;
|
|
252
|
+
}
|
|
253
|
+
return `Writing todo list with ${todos.length} items`;
|
|
254
|
+
},
|
|
255
|
+
enumerable: false,
|
|
256
|
+
configurable: true,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return aiTool as AISdkTool;
|
|
260
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload Blob Tool
|
|
3
|
+
*
|
|
4
|
+
* Uploads files, URLs, or base64 blobs to a Blossom server using Nostr authentication.
|
|
5
|
+
* Supports downloading from URLs, reading local files, and handling base64-encoded data.
|
|
6
|
+
*
|
|
7
|
+
* The tool delegates Blossom upload operations to BlossomService in the nostr layer,
|
|
8
|
+
* keeping NDK usage centralized.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "node:fs/promises";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import type { ToolExecutionContext, AISdkTool } from "@/tools/types";
|
|
14
|
+
import { BlossomService } from "@/nostr/BlossomService";
|
|
15
|
+
import { config } from "@/services/ConfigService";
|
|
16
|
+
import { logger } from "@/utils/logger";
|
|
17
|
+
import { tool } from "ai";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
|
|
20
|
+
const MAX_BLOB_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
21
|
+
const DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
22
|
+
const DEFAULT_BLOSSOM_SERVER = "https://blossom.primal.net";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load Blossom server URL from config.
|
|
26
|
+
* Falls back to default if config is unavailable.
|
|
27
|
+
*/
|
|
28
|
+
async function loadBlossomServerUrl(): Promise<string> {
|
|
29
|
+
try {
|
|
30
|
+
const tenexConfig = await config.loadTenexConfig(config.getGlobalPath());
|
|
31
|
+
return tenexConfig.blossomServerUrl || DEFAULT_BLOSSOM_SERVER;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
logger.warn("[upload_blob] Failed to load Blossom config, using default", {
|
|
34
|
+
error: error instanceof Error ? error.message : String(error),
|
|
35
|
+
});
|
|
36
|
+
return DEFAULT_BLOSSOM_SERVER;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const uploadBlobSchema = z.object({
|
|
41
|
+
input: z
|
|
42
|
+
.string()
|
|
43
|
+
.describe(
|
|
44
|
+
"REQUIRED: The source to upload - can be a file path (e.g., /path/to/file.jpg), URL to download from (e.g., https://example.com/image.jpg), or base64-encoded blob data. This parameter must be named 'input', not 'url' or 'file'."
|
|
45
|
+
),
|
|
46
|
+
mimeType: z
|
|
47
|
+
.string()
|
|
48
|
+
.nullable()
|
|
49
|
+
.describe(
|
|
50
|
+
"MIME type of the data (e.g., 'image/jpeg', 'video/mp4'). If not provided, it will be detected from the file extension, URL response headers, or data"
|
|
51
|
+
),
|
|
52
|
+
description: z
|
|
53
|
+
.string()
|
|
54
|
+
.nullable()
|
|
55
|
+
.describe("Optional description of the upload for the authorization event"),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
type UploadBlobInput = z.infer<typeof uploadBlobSchema>;
|
|
59
|
+
|
|
60
|
+
interface UploadBlobOutput {
|
|
61
|
+
url: string;
|
|
62
|
+
sha256: string;
|
|
63
|
+
size: number;
|
|
64
|
+
type?: string;
|
|
65
|
+
uploaded?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Enforce size limit on blob data
|
|
70
|
+
*/
|
|
71
|
+
function enforceSizeLimit(bytes: number): void {
|
|
72
|
+
if (bytes > MAX_BLOB_SIZE_BYTES) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Blob size ${bytes} bytes exceeds limit of ${MAX_BLOB_SIZE_BYTES} bytes. Please provide a smaller file.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Detect MIME type from file extension or data magic bytes
|
|
81
|
+
*/
|
|
82
|
+
function detectMimeType(filePath?: string, data?: Buffer): string {
|
|
83
|
+
// Try file extension first
|
|
84
|
+
if (filePath) {
|
|
85
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
86
|
+
const mimeTypes: Record<string, string> = {
|
|
87
|
+
".jpg": "image/jpeg",
|
|
88
|
+
".jpeg": "image/jpeg",
|
|
89
|
+
".png": "image/png",
|
|
90
|
+
".gif": "image/gif",
|
|
91
|
+
".webp": "image/webp",
|
|
92
|
+
".mp4": "video/mp4",
|
|
93
|
+
".mov": "video/quicktime",
|
|
94
|
+
".avi": "video/x-msvideo",
|
|
95
|
+
".webm": "video/webm",
|
|
96
|
+
".mp3": "audio/mpeg",
|
|
97
|
+
".wav": "audio/wav",
|
|
98
|
+
".pdf": "application/pdf",
|
|
99
|
+
".json": "application/json",
|
|
100
|
+
".txt": "text/plain",
|
|
101
|
+
};
|
|
102
|
+
if (mimeTypes[ext]) {
|
|
103
|
+
return mimeTypes[ext];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Try magic bytes detection
|
|
108
|
+
if (data && data.length > 4) {
|
|
109
|
+
const header = data.slice(0, 4).toString("hex");
|
|
110
|
+
if (header.startsWith("ffd8ff")) return "image/jpeg";
|
|
111
|
+
if (header === "89504e47") return "image/png";
|
|
112
|
+
if (header === "47494638") return "image/gif";
|
|
113
|
+
if (header.startsWith("52494646") && data.length > 12) {
|
|
114
|
+
if (data.slice(8, 12).toString("hex") === "57454250") {
|
|
115
|
+
return "image/webp";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return "application/octet-stream";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if input is a URL
|
|
125
|
+
*/
|
|
126
|
+
function isURL(input: string): boolean {
|
|
127
|
+
try {
|
|
128
|
+
const url = new URL(input);
|
|
129
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Download media from URL
|
|
137
|
+
*/
|
|
138
|
+
async function downloadFromURL(
|
|
139
|
+
url: string
|
|
140
|
+
): Promise<{ data: Buffer; mimeType?: string; filename?: string }> {
|
|
141
|
+
logger.info("[upload_blob] Downloading from URL", { url });
|
|
142
|
+
|
|
143
|
+
const controller = new AbortController();
|
|
144
|
+
const timeout = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetch(url, {
|
|
148
|
+
headers: {
|
|
149
|
+
"User-Agent": "TENEX/1.0 (Blossom Upload Tool)",
|
|
150
|
+
},
|
|
151
|
+
signal: controller.signal,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Failed to download from URL: ${response.status} ${response.statusText}`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check declared size before downloading body
|
|
161
|
+
const declaredSize = response.headers.get("content-length");
|
|
162
|
+
if (declaredSize) {
|
|
163
|
+
const parsed = Number.parseInt(declaredSize, 10);
|
|
164
|
+
if (Number.isFinite(parsed)) {
|
|
165
|
+
enforceSizeLimit(parsed);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Get content type from headers
|
|
170
|
+
const contentType = response.headers.get("content-type");
|
|
171
|
+
const mimeType = contentType?.split(";")[0].trim();
|
|
172
|
+
|
|
173
|
+
// Try to extract filename from Content-Disposition header or URL
|
|
174
|
+
let filename: string | undefined;
|
|
175
|
+
const contentDisposition = response.headers.get("content-disposition");
|
|
176
|
+
if (contentDisposition) {
|
|
177
|
+
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
|
178
|
+
if (filenameMatch) {
|
|
179
|
+
filename = filenameMatch[1].replace(/['"]/g, "");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!filename) {
|
|
184
|
+
// Try to extract filename from URL
|
|
185
|
+
const urlPath = new URL(url).pathname;
|
|
186
|
+
const pathSegments = urlPath.split("/");
|
|
187
|
+
const lastSegment = pathSegments[pathSegments.length - 1];
|
|
188
|
+
if (lastSegment?.includes(".")) {
|
|
189
|
+
filename = lastSegment;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
194
|
+
const data = Buffer.from(arrayBuffer);
|
|
195
|
+
enforceSizeLimit(data.length);
|
|
196
|
+
|
|
197
|
+
logger.info("[upload_blob] Downloaded from URL", {
|
|
198
|
+
size: data.length,
|
|
199
|
+
mimeType,
|
|
200
|
+
filename,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return { data, mimeType, filename };
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Download timed out after ${DOWNLOAD_TIMEOUT_MS}ms while fetching ${url}`,
|
|
208
|
+
{ cause: error }
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
throw error;
|
|
212
|
+
} finally {
|
|
213
|
+
clearTimeout(timeout);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Resolve input to data buffer and MIME type
|
|
219
|
+
*/
|
|
220
|
+
async function resolveInput(
|
|
221
|
+
dataInput: string,
|
|
222
|
+
providedMimeType: string | null
|
|
223
|
+
): Promise<{ data: Buffer; mimeType: string; description: string }> {
|
|
224
|
+
// Handle URL download
|
|
225
|
+
if (isURL(dataInput)) {
|
|
226
|
+
const downloadResult = await downloadFromURL(dataInput);
|
|
227
|
+
return {
|
|
228
|
+
data: downloadResult.data,
|
|
229
|
+
mimeType:
|
|
230
|
+
providedMimeType ||
|
|
231
|
+
downloadResult.mimeType ||
|
|
232
|
+
detectMimeType(downloadResult.filename, downloadResult.data),
|
|
233
|
+
description: downloadResult.filename || "Upload from URL",
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Handle base64 data (with or without data URL prefix)
|
|
238
|
+
if (dataInput.startsWith("data:") || dataInput.includes(",")) {
|
|
239
|
+
const base64Data = dataInput.includes(",") ? dataInput.split(",")[1] : dataInput;
|
|
240
|
+
let mimeType: string;
|
|
241
|
+
|
|
242
|
+
// Extract MIME type from data URL if present
|
|
243
|
+
if (dataInput.startsWith("data:")) {
|
|
244
|
+
const matches = dataInput.match(/^data:([^;]+);/);
|
|
245
|
+
mimeType = matches ? matches[1] : (providedMimeType || "application/octet-stream");
|
|
246
|
+
} else {
|
|
247
|
+
mimeType = providedMimeType || "application/octet-stream";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const data = Buffer.from(base64Data, "base64");
|
|
251
|
+
enforceSizeLimit(data.length);
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
data,
|
|
255
|
+
mimeType,
|
|
256
|
+
description: "Upload blob data",
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Handle file path
|
|
261
|
+
const filePath = path.resolve(dataInput);
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
await fs.access(filePath);
|
|
265
|
+
} catch {
|
|
266
|
+
throw new Error(`File not found: ${filePath}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const stats = await fs.stat(filePath);
|
|
270
|
+
enforceSizeLimit(stats.size);
|
|
271
|
+
|
|
272
|
+
const data = await fs.readFile(filePath);
|
|
273
|
+
const mimeType = providedMimeType || detectMimeType(filePath, data);
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
data,
|
|
277
|
+
mimeType,
|
|
278
|
+
description: `Upload ${path.basename(filePath)}`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Execute the upload_blob tool
|
|
284
|
+
*/
|
|
285
|
+
async function executeUploadBlob(
|
|
286
|
+
input: UploadBlobInput,
|
|
287
|
+
context: ToolExecutionContext
|
|
288
|
+
): Promise<UploadBlobOutput> {
|
|
289
|
+
const { input: dataInput, mimeType: providedMimeType, description: providedDescription } = input;
|
|
290
|
+
|
|
291
|
+
// Validate that input is provided
|
|
292
|
+
if (!dataInput) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
"The 'input' parameter is required. Pass the URL, file path, or base64 data via { input: '...' }. Note: The parameter name is 'input', not 'url' or 'file'."
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
logger.info("[upload_blob] Starting blob upload", {
|
|
299
|
+
isURL: isURL(dataInput),
|
|
300
|
+
hasFilePath: !isURL(dataInput) && !dataInput.startsWith("data:") && !dataInput.includes(","),
|
|
301
|
+
hasMimeType: !!providedMimeType,
|
|
302
|
+
description: providedDescription,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Resolve input to data buffer
|
|
306
|
+
const resolved = await resolveInput(dataInput, providedMimeType);
|
|
307
|
+
const uploadDescription = providedDescription || resolved.description;
|
|
308
|
+
|
|
309
|
+
logger.info("[upload_blob] Resolved input", {
|
|
310
|
+
size: resolved.data.length,
|
|
311
|
+
mimeType: resolved.mimeType,
|
|
312
|
+
description: uploadDescription,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Upload to Blossom using the service (delegates NDK usage to nostr layer)
|
|
316
|
+
// Layer 3 (tools) loads config and passes serverUrl to Layer 2 (nostr)
|
|
317
|
+
const blossomServerUrl = await loadBlossomServerUrl();
|
|
318
|
+
const blossomService = new BlossomService(context.agent);
|
|
319
|
+
const result = await blossomService.upload(resolved.data, resolved.mimeType, {
|
|
320
|
+
serverUrl: blossomServerUrl,
|
|
321
|
+
description: uploadDescription,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
logger.info("[upload_blob] Upload successful", {
|
|
325
|
+
url: result.url,
|
|
326
|
+
sha256: result.sha256,
|
|
327
|
+
size: result.size,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Create the upload_blob tool for AI SDK
|
|
335
|
+
*/
|
|
336
|
+
export function createUploadBlobTool(context: ToolExecutionContext): AISdkTool {
|
|
337
|
+
const aiTool = tool({
|
|
338
|
+
description: `Upload files, URLs, or base64 blobs to a Blossom server.
|
|
339
|
+
|
|
340
|
+
IMPORTANT: The parameter is named 'input' (not 'url' or 'file').
|
|
341
|
+
|
|
342
|
+
Pass the source via the 'input' parameter:
|
|
343
|
+
- URLs: { input: "https://example.com/image.jpg" }
|
|
344
|
+
- File paths: { input: "/path/to/file.jpg" }
|
|
345
|
+
- Base64 data: { input: "data:image/jpeg;base64,..." } or { input: "<base64_string>" }
|
|
346
|
+
|
|
347
|
+
Optional parameters:
|
|
348
|
+
- mimeType: Specify MIME type (auto-detected if not provided)
|
|
349
|
+
- description: Add a description for the upload
|
|
350
|
+
|
|
351
|
+
The Blossom server URL is configured in .tenex/config.json (default: https://blossom.primal.net).
|
|
352
|
+
Returns the URL of the uploaded media with appropriate file extension.`,
|
|
353
|
+
inputSchema: uploadBlobSchema,
|
|
354
|
+
execute: async (input: UploadBlobInput) => {
|
|
355
|
+
return await executeUploadBlob(input, context);
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Add human-readable content generation
|
|
360
|
+
Object.defineProperty(aiTool, "getHumanReadableContent", {
|
|
361
|
+
value: (args: UploadBlobInput | undefined) => {
|
|
362
|
+
if (!args || !args.input) {
|
|
363
|
+
return "Uploading blob data";
|
|
364
|
+
}
|
|
365
|
+
const { input, description } = args;
|
|
366
|
+
|
|
367
|
+
if (isURL(input)) {
|
|
368
|
+
const url = new URL(input);
|
|
369
|
+
return `Downloading and uploading from ${url.hostname}${description ? ` - ${description}` : ""}`;
|
|
370
|
+
}
|
|
371
|
+
if (!input.startsWith("data:") && !input.includes(",")) {
|
|
372
|
+
return `Uploading file: ${path.basename(input)}${description ? ` - ${description}` : ""}`;
|
|
373
|
+
}
|
|
374
|
+
return `Uploading blob data${description ? ` - ${description}` : ""}`;
|
|
375
|
+
},
|
|
376
|
+
enumerable: false,
|
|
377
|
+
configurable: true,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
return aiTool as AISdkTool;
|
|
381
|
+
}
|