@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,292 @@
|
|
|
1
|
+
/** Role types that can be used for message injection */
|
|
2
|
+
export type InjectionRole = "user" | "system";
|
|
3
|
+
|
|
4
|
+
/** Result of injecting a message into an active RAL */
|
|
5
|
+
export interface InjectionResult {
|
|
6
|
+
/** The active RAL entry, if found */
|
|
7
|
+
activeRal?: RALRegistryEntry;
|
|
8
|
+
/** Whether the message was queued for injection */
|
|
9
|
+
queued: boolean;
|
|
10
|
+
/** Whether an active streaming run was aborted */
|
|
11
|
+
aborted: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Todo Types
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
export type TodoStatus = "pending" | "in_progress" | "done" | "skipped";
|
|
19
|
+
|
|
20
|
+
export interface TodoItem {
|
|
21
|
+
/** Unique identifier - can be custom or auto-generated from title */
|
|
22
|
+
id: string;
|
|
23
|
+
/** Human-readable title */
|
|
24
|
+
title: string;
|
|
25
|
+
/** Detailed description of what needs to be done */
|
|
26
|
+
description: string;
|
|
27
|
+
/** Current status of the todo item */
|
|
28
|
+
status: TodoStatus;
|
|
29
|
+
/** Required when status='skipped' - explains why item was skipped */
|
|
30
|
+
skipReason?: string;
|
|
31
|
+
/** Timestamp when item was created */
|
|
32
|
+
createdAt: number;
|
|
33
|
+
/** Timestamp of last status change */
|
|
34
|
+
updatedAt: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface BasePendingDelegation {
|
|
38
|
+
delegationConversationId: string;
|
|
39
|
+
recipientPubkey: string;
|
|
40
|
+
senderPubkey: string;
|
|
41
|
+
prompt: string;
|
|
42
|
+
/** Which RAL created this delegation (for provenance tracking) */
|
|
43
|
+
ralNumber: number;
|
|
44
|
+
/**
|
|
45
|
+
* If true, this delegation has been killed via the kill tool.
|
|
46
|
+
* Completion events for killed delegations should be ignored.
|
|
47
|
+
* This prevents the race condition where a delegation completes
|
|
48
|
+
* after being killed but before the abort fully propagates.
|
|
49
|
+
*/
|
|
50
|
+
killed?: boolean;
|
|
51
|
+
/** Timestamp when delegation was killed (for debugging) */
|
|
52
|
+
killedAt?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface StandardDelegation extends BasePendingDelegation {
|
|
56
|
+
type?: "standard";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface FollowupDelegation extends BasePendingDelegation {
|
|
60
|
+
type: "followup";
|
|
61
|
+
/** The event ID of the followup message (needed for routing responses) */
|
|
62
|
+
followupEventId?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ExternalDelegation extends BasePendingDelegation {
|
|
66
|
+
type: "external";
|
|
67
|
+
projectId?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface AskDelegation extends BasePendingDelegation {
|
|
71
|
+
type: "ask";
|
|
72
|
+
suggestions?: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type PendingDelegation =
|
|
76
|
+
| StandardDelegation
|
|
77
|
+
| FollowupDelegation
|
|
78
|
+
| ExternalDelegation
|
|
79
|
+
| AskDelegation;
|
|
80
|
+
|
|
81
|
+
export interface DelegationMessage {
|
|
82
|
+
senderPubkey: string;
|
|
83
|
+
recipientPubkey: string;
|
|
84
|
+
content: string;
|
|
85
|
+
timestamp: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Discriminated union for completed delegations.
|
|
90
|
+
* Either successfully completed or aborted with a reason.
|
|
91
|
+
*/
|
|
92
|
+
export type CompletedDelegation =
|
|
93
|
+
| {
|
|
94
|
+
delegationConversationId: string;
|
|
95
|
+
recipientPubkey: string;
|
|
96
|
+
senderPubkey: string;
|
|
97
|
+
transcript: DelegationMessage[];
|
|
98
|
+
completedAt: number;
|
|
99
|
+
/** Which RAL created this delegation (for provenance tracking) */
|
|
100
|
+
ralNumber: number;
|
|
101
|
+
status: "completed";
|
|
102
|
+
}
|
|
103
|
+
| {
|
|
104
|
+
delegationConversationId: string;
|
|
105
|
+
recipientPubkey: string;
|
|
106
|
+
senderPubkey: string;
|
|
107
|
+
transcript: DelegationMessage[];
|
|
108
|
+
completedAt: number;
|
|
109
|
+
/** Which RAL created this delegation (for provenance tracking) */
|
|
110
|
+
ralNumber: number;
|
|
111
|
+
status: "aborted";
|
|
112
|
+
abortReason: string;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export interface QueuedInjection {
|
|
116
|
+
role: InjectionRole;
|
|
117
|
+
content: string;
|
|
118
|
+
queuedAt: number;
|
|
119
|
+
/** If true, message is included in LLM context but NOT persisted to ConversationStore */
|
|
120
|
+
ephemeral?: boolean;
|
|
121
|
+
/** Original sender pubkey (for message attribution when sender differs from expected) */
|
|
122
|
+
senderPubkey?: string;
|
|
123
|
+
/** Original Nostr event ID (for deduplication - prevents double-insertion via both addEvent and injection paths) */
|
|
124
|
+
eventId?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface RALRegistryEntry {
|
|
128
|
+
id: string;
|
|
129
|
+
/** Sequential number for this RAL within the conversation (1, 2, 3...) */
|
|
130
|
+
ralNumber: number;
|
|
131
|
+
agentPubkey: string;
|
|
132
|
+
/** The project this RAL belongs to - required for multi-project isolation in daemon mode */
|
|
133
|
+
projectId: string;
|
|
134
|
+
/** The conversation this RAL belongs to - RAL is scoped per agent+conversation */
|
|
135
|
+
conversationId: string;
|
|
136
|
+
queuedInjections: QueuedInjection[];
|
|
137
|
+
/** Whether the agent is currently streaming a response */
|
|
138
|
+
isStreaming: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Map of currently executing tool call IDs to their tool info.
|
|
141
|
+
* Multiple tools can execute concurrently. ACTING state is derived from activeTools.size > 0.
|
|
142
|
+
* Keyed by toolCallId (not toolName) to properly track concurrent calls of the same tool.
|
|
143
|
+
* Value contains tool name (for display) and startedAt timestamp (for duration tracking).
|
|
144
|
+
*/
|
|
145
|
+
activeTools: Map<string, { name: string; startedAt: number }>;
|
|
146
|
+
/** Most recently started tool name (derived from activeTools, for display only) */
|
|
147
|
+
currentTool?: string;
|
|
148
|
+
/** Start time of the most recently started tool (derived from activeTools, for display only) */
|
|
149
|
+
toolStartedAt?: number;
|
|
150
|
+
createdAt: number;
|
|
151
|
+
lastActivityAt: number;
|
|
152
|
+
/** The original event that triggered this RAL - used for proper tagging on resumption */
|
|
153
|
+
originalTriggeringEventId?: string;
|
|
154
|
+
/** OTEL trace ID for correlating stop events with the agent execution */
|
|
155
|
+
traceId?: string;
|
|
156
|
+
/** OTEL span ID of the agent execution span - used as parent for stop spans */
|
|
157
|
+
executionSpanId?: string;
|
|
158
|
+
/** Accumulated LLM runtime in milliseconds across all streaming sessions */
|
|
159
|
+
accumulatedRuntime: number;
|
|
160
|
+
/** Last reported runtime in milliseconds - used to calculate incremental runtime */
|
|
161
|
+
lastReportedRuntime: number;
|
|
162
|
+
/** Timestamp when current LLM stream started (for calculating duration) - immutable for stream lifetime */
|
|
163
|
+
llmStreamStartTime?: number;
|
|
164
|
+
/** Checkpoint timestamp for incremental runtime reporting mid-stream (resets on each consume) */
|
|
165
|
+
lastRuntimeCheckpointAt?: number;
|
|
166
|
+
/** Heuristic state - namespaced under 'heuristics' */
|
|
167
|
+
heuristics?: {
|
|
168
|
+
/** Pending violations waiting to be injected */
|
|
169
|
+
pendingViolations: Array<{
|
|
170
|
+
id: string;
|
|
171
|
+
title: string;
|
|
172
|
+
message: string;
|
|
173
|
+
severity: "warning" | "error";
|
|
174
|
+
timestamp: number;
|
|
175
|
+
heuristicId: string;
|
|
176
|
+
}>;
|
|
177
|
+
/** Set of violation IDs shown in this RAL (for deduplication) */
|
|
178
|
+
shownViolationIds: Set<string>;
|
|
179
|
+
/** O(1) precomputed summary for heuristic evaluation */
|
|
180
|
+
summary?: {
|
|
181
|
+
/** Tool call history (bounded to last N tools) */
|
|
182
|
+
recentTools: Array<{ name: string; timestamp: number }>;
|
|
183
|
+
/** Flags for quick checks */
|
|
184
|
+
flags: {
|
|
185
|
+
hasTodoWrite: boolean;
|
|
186
|
+
hasDelegation: boolean;
|
|
187
|
+
hasVerification: boolean;
|
|
188
|
+
hasGitAgentCommit: boolean;
|
|
189
|
+
};
|
|
190
|
+
/** Pending delegation count for this RAL (O(1) counter) */
|
|
191
|
+
pendingDelegationCount: number;
|
|
192
|
+
};
|
|
193
|
+
/** Tool args storage: toolCallId -> args (for passing to heuristics) */
|
|
194
|
+
toolArgs?: Map<string, unknown>;
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface StopExecutionSignal {
|
|
199
|
+
__stopExecution: true;
|
|
200
|
+
pendingDelegations: PendingDelegation[];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check if value is a direct (unwrapped) StopExecutionSignal
|
|
205
|
+
*/
|
|
206
|
+
function isDirectStopExecutionSignal(value: unknown): value is StopExecutionSignal {
|
|
207
|
+
return (
|
|
208
|
+
typeof value === "object" &&
|
|
209
|
+
value !== null &&
|
|
210
|
+
"__stopExecution" in value &&
|
|
211
|
+
(value as StopExecutionSignal).__stopExecution === true
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Try to extract a StopExecutionSignal from an MCP-wrapped response.
|
|
217
|
+
*
|
|
218
|
+
* Claude Code SDK wraps tool results in MCP format:
|
|
219
|
+
* - Array format: [{ type: "text", text: '{"__stopExecution":true,...}' }]
|
|
220
|
+
* - Object format: { content: [{ type: "text", text: "..." }] }
|
|
221
|
+
*
|
|
222
|
+
* Returns the parsed StopExecutionSignal if found, null otherwise.
|
|
223
|
+
*/
|
|
224
|
+
function extractFromMCPWrapped(value: unknown): StopExecutionSignal | null {
|
|
225
|
+
// Handle both array format and { content: [...] } format
|
|
226
|
+
const content = Array.isArray(value)
|
|
227
|
+
? value
|
|
228
|
+
: (typeof value === "object" && value !== null && "content" in value)
|
|
229
|
+
? (value as { content: unknown[] }).content
|
|
230
|
+
: null;
|
|
231
|
+
|
|
232
|
+
if (!Array.isArray(content)) return null;
|
|
233
|
+
|
|
234
|
+
// Find the text item in the content array
|
|
235
|
+
const textItem = content.find((c: unknown) =>
|
|
236
|
+
typeof c === "object" &&
|
|
237
|
+
c !== null &&
|
|
238
|
+
(c as { type?: string }).type === "text"
|
|
239
|
+
) as { text?: string } | undefined;
|
|
240
|
+
|
|
241
|
+
if (!textItem?.text) return null;
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const parsed = JSON.parse(textItem.text);
|
|
245
|
+
if (isDirectStopExecutionSignal(parsed)) {
|
|
246
|
+
return parsed;
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// Not valid JSON, not a stop signal
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Type guard for StopExecutionSignal.
|
|
257
|
+
*
|
|
258
|
+
* Handles both:
|
|
259
|
+
* 1. Direct StopExecutionSignal objects (from standard AI SDK providers)
|
|
260
|
+
* 2. MCP-wrapped responses (from Claude Code SDK where tool results get
|
|
261
|
+
* wrapped in [{ type: "text", text: "..." }] format)
|
|
262
|
+
*/
|
|
263
|
+
export function isStopExecutionSignal(value: unknown): value is StopExecutionSignal {
|
|
264
|
+
// Check direct format first (most common case)
|
|
265
|
+
if (isDirectStopExecutionSignal(value)) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check MCP-wrapped format (Claude Code SDK)
|
|
270
|
+
return extractFromMCPWrapped(value) !== null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Extract the pending delegations from a StopExecutionSignal.
|
|
275
|
+
*
|
|
276
|
+
* Works with both direct and MCP-wrapped formats.
|
|
277
|
+
* Returns null if value is not a StopExecutionSignal.
|
|
278
|
+
*/
|
|
279
|
+
export function extractPendingDelegations(value: unknown): PendingDelegation[] | null {
|
|
280
|
+
// Check direct format first
|
|
281
|
+
if (isDirectStopExecutionSignal(value)) {
|
|
282
|
+
return value.pendingDelegations;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Check MCP-wrapped format
|
|
286
|
+
const extracted = extractFromMCPWrapped(value);
|
|
287
|
+
if (extracted) {
|
|
288
|
+
return extracted.pendingDelegations;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join, resolve, relative, basename, dirname } from "node:path";
|
|
4
|
+
import { isPathWithinDirectory } from "@/lib/agent-home";
|
|
5
|
+
import { logger } from "@/utils/logger";
|
|
6
|
+
import { getProjectContext } from "@/services/projects/ProjectContext";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Error thrown when a slug fails validation
|
|
10
|
+
*/
|
|
11
|
+
export class InvalidSlugError extends Error {
|
|
12
|
+
constructor(slug: string, reason: string) {
|
|
13
|
+
super(`Invalid report slug "${slug}": ${reason}`);
|
|
14
|
+
this.name = "InvalidSlugError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Metadata stored alongside each local report to track its Nostr origin
|
|
20
|
+
*/
|
|
21
|
+
export interface LocalReportMetadata {
|
|
22
|
+
/** Addressable reference in NIP-33 format (kind:pubkey:identifier) */
|
|
23
|
+
addressableRef: string;
|
|
24
|
+
/** Unix timestamp when the report was created/published */
|
|
25
|
+
createdAt: number;
|
|
26
|
+
/** The slug (d-tag) of the report */
|
|
27
|
+
slug: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* LocalReportStore manages local file storage for reports.
|
|
33
|
+
*
|
|
34
|
+
* Reports are stored at: $TENEX_BASE_DIR/projects/<projectDTag>/reports/<slug>.md
|
|
35
|
+
* Metadata is stored at: $TENEX_BASE_DIR/projects/<projectDTag>/reports/.metadata/<slug>.json
|
|
36
|
+
*
|
|
37
|
+
* This provides:
|
|
38
|
+
* - Fast local reads without Nostr lookups
|
|
39
|
+
* - Single source of truth for multi-agent collaboration
|
|
40
|
+
* - Backwards compatibility via subscription hydration
|
|
41
|
+
* - Project isolation to prevent d-tag conflicts across projects
|
|
42
|
+
*/
|
|
43
|
+
export class LocalReportStore {
|
|
44
|
+
private _basePath: string | null = null;
|
|
45
|
+
private _projectId: string | null = null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize the store for a project.
|
|
49
|
+
* Must be called once at startup before using any methods.
|
|
50
|
+
* @param metadataPath The project metadata path (e.g., ~/.tenex/projects/<dTag>)
|
|
51
|
+
*/
|
|
52
|
+
initialize(metadataPath: string): void {
|
|
53
|
+
this._basePath = dirname(metadataPath); // ~/.tenex/projects
|
|
54
|
+
this._projectId = basename(metadataPath); // <dTag>
|
|
55
|
+
logger.info(`[LocalReportStore] Initialized for project ${this._projectId}`, {
|
|
56
|
+
basePath: this._basePath,
|
|
57
|
+
projectId: this._projectId,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if the store has been initialized
|
|
63
|
+
*/
|
|
64
|
+
isInitialized(): boolean {
|
|
65
|
+
return this._projectId !== null && this._basePath !== null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the project ID (d-tag)
|
|
70
|
+
*/
|
|
71
|
+
get projectId(): string | null {
|
|
72
|
+
return this._projectId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the path to the reports directory
|
|
77
|
+
* Requires initialization with a project context
|
|
78
|
+
*/
|
|
79
|
+
getReportsDir(): string {
|
|
80
|
+
if (!this._basePath || !this._projectId) {
|
|
81
|
+
throw new Error("LocalReportStore.initialize() must be called before getReportsDir()");
|
|
82
|
+
}
|
|
83
|
+
return join(this._basePath, this._projectId, "reports");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get the path to the metadata directory
|
|
88
|
+
*/
|
|
89
|
+
private getMetadataDir(): string {
|
|
90
|
+
if (!this._basePath || !this._projectId) {
|
|
91
|
+
throw new Error("LocalReportStore.initialize() must be called before getMetadataDir()");
|
|
92
|
+
}
|
|
93
|
+
return join(this._basePath, this._projectId, "reports", ".metadata");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validate a slug to prevent path traversal attacks.
|
|
98
|
+
* Slugs must:
|
|
99
|
+
* - Not be empty
|
|
100
|
+
* - Not contain path separators (/, \)
|
|
101
|
+
* - Not contain parent directory references (..)
|
|
102
|
+
* - Not start or end with dots
|
|
103
|
+
* - Only contain alphanumeric characters, hyphens, underscores
|
|
104
|
+
*
|
|
105
|
+
* @param slug The slug to validate
|
|
106
|
+
* @throws InvalidSlugError if the slug is invalid
|
|
107
|
+
*/
|
|
108
|
+
validateSlug(slug: string): void {
|
|
109
|
+
if (!slug || slug.trim() === "") {
|
|
110
|
+
throw new InvalidSlugError(slug, "slug cannot be empty");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check for path separators
|
|
114
|
+
if (slug.includes("/") || slug.includes("\\")) {
|
|
115
|
+
throw new InvalidSlugError(slug, "slug cannot contain path separators (/ or \\)");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check for parent directory references
|
|
119
|
+
if (slug.includes("..")) {
|
|
120
|
+
throw new InvalidSlugError(slug, "slug cannot contain parent directory references (..)");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check for leading/trailing dots (could be hidden files or special dirs)
|
|
124
|
+
if (slug.startsWith(".") || slug.endsWith(".")) {
|
|
125
|
+
throw new InvalidSlugError(slug, "slug cannot start or end with a dot");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Only allow safe characters: alphanumeric, hyphens, underscores
|
|
129
|
+
const safeSlugPattern = /^[a-zA-Z0-9_-]+$/;
|
|
130
|
+
if (!safeSlugPattern.test(slug)) {
|
|
131
|
+
throw new InvalidSlugError(
|
|
132
|
+
slug,
|
|
133
|
+
"slug can only contain alphanumeric characters, hyphens (-), and underscores (_)"
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get a safe, validated path for a report file.
|
|
140
|
+
* Validates the slug and ensures the resulting path is within the reports directory.
|
|
141
|
+
*
|
|
142
|
+
* @param slug The report slug
|
|
143
|
+
* @param extension The file extension (default: "md")
|
|
144
|
+
* @returns The validated file path
|
|
145
|
+
* @throws InvalidSlugError if the slug is invalid or path escapes the reports directory
|
|
146
|
+
*/
|
|
147
|
+
private getSafePath(slug: string, extension: "md" | "json" = "md"): string {
|
|
148
|
+
this.validateSlug(slug);
|
|
149
|
+
|
|
150
|
+
const baseDir = extension === "md" ? this.getReportsDir() : this.getMetadataDir();
|
|
151
|
+
const filePath = resolve(join(baseDir, `${slug}.${extension}`));
|
|
152
|
+
|
|
153
|
+
// Double-check: ensure resolved path is still within the base directory
|
|
154
|
+
const relativePath = relative(baseDir, filePath);
|
|
155
|
+
if (relativePath.startsWith("..") || relativePath.includes("..")) {
|
|
156
|
+
throw new InvalidSlugError(slug, "resolved path escapes the reports directory");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return filePath;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Ensure the reports and metadata directories exist
|
|
164
|
+
*/
|
|
165
|
+
async ensureDirectories(): Promise<void> {
|
|
166
|
+
await mkdir(this.getReportsDir(), { recursive: true });
|
|
167
|
+
await mkdir(this.getMetadataDir(), { recursive: true });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get the file path for a report's content
|
|
172
|
+
* @throws InvalidSlugError if the slug is invalid
|
|
173
|
+
*/
|
|
174
|
+
getReportPath(slug: string): string {
|
|
175
|
+
return this.getSafePath(slug, "md");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get the file path for a report's metadata
|
|
180
|
+
* @throws InvalidSlugError if the slug is invalid
|
|
181
|
+
*/
|
|
182
|
+
private getMetadataPath(slug: string): string {
|
|
183
|
+
return this.getSafePath(slug, "json");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Write a report to local storage with its metadata
|
|
188
|
+
* @param slug The report slug (d-tag)
|
|
189
|
+
* @param content The markdown content
|
|
190
|
+
* @param metadata The Nostr event metadata
|
|
191
|
+
*/
|
|
192
|
+
async writeReport(slug: string, content: string, metadata: LocalReportMetadata): Promise<void> {
|
|
193
|
+
await this.ensureDirectories();
|
|
194
|
+
|
|
195
|
+
const reportPath = this.getReportPath(slug);
|
|
196
|
+
const metadataPath = this.getMetadataPath(slug);
|
|
197
|
+
|
|
198
|
+
// Write content file
|
|
199
|
+
await writeFile(reportPath, content, "utf-8");
|
|
200
|
+
|
|
201
|
+
// Write metadata file
|
|
202
|
+
await writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
203
|
+
|
|
204
|
+
logger.debug("📁 Saved report to local storage", {
|
|
205
|
+
slug,
|
|
206
|
+
path: reportPath,
|
|
207
|
+
contentLength: content.length,
|
|
208
|
+
addressableRef: metadata.addressableRef.substring(0, 20),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Read a report's content from local storage
|
|
214
|
+
* @param slug The report slug (d-tag)
|
|
215
|
+
* @returns The report content or null if not found
|
|
216
|
+
*/
|
|
217
|
+
async readReport(slug: string): Promise<string | null> {
|
|
218
|
+
const reportPath = this.getReportPath(slug);
|
|
219
|
+
|
|
220
|
+
if (!existsSync(reportPath)) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const content = await readFile(reportPath, "utf-8");
|
|
226
|
+
return content;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
logger.warn("📁 Failed to read local report", {
|
|
229
|
+
slug,
|
|
230
|
+
path: reportPath,
|
|
231
|
+
error: error instanceof Error ? error.message : String(error),
|
|
232
|
+
});
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Read a report's metadata from local storage
|
|
239
|
+
* @param slug The report slug (d-tag)
|
|
240
|
+
* @returns The metadata or null if not found
|
|
241
|
+
*/
|
|
242
|
+
async readMetadata(slug: string): Promise<LocalReportMetadata | null> {
|
|
243
|
+
const metadataPath = this.getMetadataPath(slug);
|
|
244
|
+
|
|
245
|
+
if (!existsSync(metadataPath)) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const content = await readFile(metadataPath, "utf-8");
|
|
251
|
+
return JSON.parse(content) as LocalReportMetadata;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
logger.warn("📁 Failed to read local report metadata", {
|
|
254
|
+
slug,
|
|
255
|
+
path: metadataPath,
|
|
256
|
+
error: error instanceof Error ? error.message : String(error),
|
|
257
|
+
});
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Check if a report exists locally
|
|
264
|
+
* @param slug The report slug (d-tag)
|
|
265
|
+
*/
|
|
266
|
+
exists(slug: string): boolean {
|
|
267
|
+
return existsSync(this.getReportPath(slug));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Check if a Nostr event is newer than the local copy
|
|
272
|
+
* Used for hydration from subscription
|
|
273
|
+
* @param slug The report slug (d-tag)
|
|
274
|
+
* @param eventCreatedAt The Nostr event's created_at timestamp
|
|
275
|
+
* @returns true if the event is newer and should update the local copy
|
|
276
|
+
*/
|
|
277
|
+
async isNewerThanLocal(slug: string, eventCreatedAt: number): Promise<boolean> {
|
|
278
|
+
const metadata = await this.readMetadata(slug);
|
|
279
|
+
|
|
280
|
+
if (!metadata) {
|
|
281
|
+
// No local copy exists, so the event is "newer"
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Compare timestamps
|
|
286
|
+
return eventCreatedAt > metadata.createdAt;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Hydrate local storage from a Nostr event if it's newer
|
|
291
|
+
* @param slug The report slug (d-tag)
|
|
292
|
+
* @param content The report content
|
|
293
|
+
* @param addressableRef The addressable reference in NIP-33 format (kind:pubkey:identifier)
|
|
294
|
+
* @param createdAt The Nostr event's created_at timestamp
|
|
295
|
+
* @returns true if the local copy was updated
|
|
296
|
+
*/
|
|
297
|
+
async hydrateFromNostr(
|
|
298
|
+
slug: string,
|
|
299
|
+
content: string,
|
|
300
|
+
addressableRef: string,
|
|
301
|
+
createdAt: number
|
|
302
|
+
): Promise<boolean> {
|
|
303
|
+
const isNewer = await this.isNewerThanLocal(slug, createdAt);
|
|
304
|
+
|
|
305
|
+
if (!isNewer) {
|
|
306
|
+
logger.debug("📁 Skipping hydration - local copy is current or newer", {
|
|
307
|
+
slug,
|
|
308
|
+
eventCreatedAt: createdAt,
|
|
309
|
+
});
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
await this.writeReport(slug, content, {
|
|
314
|
+
addressableRef,
|
|
315
|
+
createdAt,
|
|
316
|
+
slug,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
logger.info("📁 Hydrated local report from Nostr", {
|
|
320
|
+
slug,
|
|
321
|
+
addressableRef: addressableRef.substring(0, 20),
|
|
322
|
+
createdAt,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Check if a path is within the reports directory
|
|
330
|
+
* Used to block direct writes via fs_write
|
|
331
|
+
* Uses secure path normalization to prevent path traversal attacks.
|
|
332
|
+
* @param path The path to check
|
|
333
|
+
* @throws Error if the store has not been initialized
|
|
334
|
+
*/
|
|
335
|
+
isPathInReportsDir(path: string): boolean {
|
|
336
|
+
// CRITICAL: Throw if not initialized to prevent silent bypass of protection
|
|
337
|
+
if (!this.isInitialized()) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
"LocalReportStore.isPathInReportsDir() called before initialization. " +
|
|
340
|
+
"This indicates a bug - the store must be initialized during project startup."
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Use secure path containment check that handles .., symlinks, case differences
|
|
345
|
+
return isPathWithinDirectory(path, this.getReportsDir());
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Reset the store (for testing purposes)
|
|
350
|
+
*/
|
|
351
|
+
reset(): void {
|
|
352
|
+
this._basePath = null;
|
|
353
|
+
this._projectId = null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Create a new LocalReportStore instance.
|
|
359
|
+
* Each project should have its own instance managed by ProjectContext.
|
|
360
|
+
*/
|
|
361
|
+
export function createLocalReportStore(): LocalReportStore {
|
|
362
|
+
return new LocalReportStore();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get the LocalReportStore for the current project context.
|
|
367
|
+
* This is a convenience function that gets the store from ProjectContext.
|
|
368
|
+
*
|
|
369
|
+
* @throws Error if called outside of a project context
|
|
370
|
+
*/
|
|
371
|
+
export function getLocalReportStore(): LocalReportStore {
|
|
372
|
+
const context = getProjectContext();
|
|
373
|
+
if (!context.localReportStore) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
"LocalReportStore not available in project context. " +
|
|
376
|
+
"This indicates a bug - the store should be initialized during project startup."
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
return context.localReportStore;
|
|
380
|
+
}
|