@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,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentsMdService - Service for discovering and reading AGENTS.md files
|
|
3
|
+
*
|
|
4
|
+
* AGENTS.md files provide contextual guidelines for AI agents working in specific
|
|
5
|
+
* directories. These files are discovered hierarchically and injected as system
|
|
6
|
+
* reminders after relevant tool results (like fs_read).
|
|
7
|
+
*
|
|
8
|
+
* Key behaviors:
|
|
9
|
+
* - Finds all AGENTS.md files from a path up to the project root
|
|
10
|
+
* - Returns them in order from most specific to most general
|
|
11
|
+
* - Caches file contents to avoid repeated reads
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFile, stat } from "node:fs/promises";
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { trace } from "@opentelemetry/api";
|
|
18
|
+
|
|
19
|
+
export interface AgentsMdFile {
|
|
20
|
+
/** Absolute path to the AGENTS.md file */
|
|
21
|
+
path: string;
|
|
22
|
+
/** Directory containing the AGENTS.md file */
|
|
23
|
+
directory: string;
|
|
24
|
+
/** Content of the AGENTS.md file */
|
|
25
|
+
content: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Singleton service for AGENTS.md file discovery and caching
|
|
30
|
+
*/
|
|
31
|
+
class AgentsMdServiceImpl {
|
|
32
|
+
/** Cache of AGENTS.md file contents by absolute path */
|
|
33
|
+
private contentCache = new Map<string, string | null>();
|
|
34
|
+
|
|
35
|
+
/** Cache expiry time in milliseconds (5 minutes) */
|
|
36
|
+
private readonly CACHE_TTL = 5 * 60 * 1000;
|
|
37
|
+
|
|
38
|
+
/** Timestamps for cache entries */
|
|
39
|
+
private cacheTimestamps = new Map<string, number>();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Find all AGENTS.md files from the given path up to the project root.
|
|
43
|
+
*
|
|
44
|
+
* @param targetPath - The file or directory path being accessed
|
|
45
|
+
* @param projectRoot - The project root directory (stops search here)
|
|
46
|
+
* @returns Array of AGENTS.md files, ordered from most specific to most general
|
|
47
|
+
*/
|
|
48
|
+
async findAgentsMdFiles(
|
|
49
|
+
targetPath: string,
|
|
50
|
+
projectRoot: string
|
|
51
|
+
): Promise<AgentsMdFile[]> {
|
|
52
|
+
const absoluteTargetPath = resolve(targetPath);
|
|
53
|
+
const absoluteProjectRoot = resolve(projectRoot);
|
|
54
|
+
|
|
55
|
+
// Start from the directory containing the target
|
|
56
|
+
let currentDir = existsSync(absoluteTargetPath) && !absoluteTargetPath.endsWith("/")
|
|
57
|
+
? (await this.isDirectory(absoluteTargetPath) ? absoluteTargetPath : dirname(absoluteTargetPath))
|
|
58
|
+
: dirname(absoluteTargetPath);
|
|
59
|
+
|
|
60
|
+
const results: AgentsMdFile[] = [];
|
|
61
|
+
const visited = new Set<string>();
|
|
62
|
+
|
|
63
|
+
// Walk up the directory tree until we reach the project root (inclusive)
|
|
64
|
+
// Note: startsWith handles equality case, so no need for explicit === check
|
|
65
|
+
while (currentDir.startsWith(absoluteProjectRoot)) {
|
|
66
|
+
// Avoid infinite loop
|
|
67
|
+
if (visited.has(currentDir)) break;
|
|
68
|
+
visited.add(currentDir);
|
|
69
|
+
|
|
70
|
+
const agentsMdPath = join(currentDir, "AGENTS.md");
|
|
71
|
+
const content = await this.readAgentsMd(agentsMdPath);
|
|
72
|
+
|
|
73
|
+
if (content !== null) {
|
|
74
|
+
results.push({
|
|
75
|
+
path: agentsMdPath,
|
|
76
|
+
directory: currentDir,
|
|
77
|
+
content,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Stop at project root
|
|
82
|
+
if (currentDir === absoluteProjectRoot) break;
|
|
83
|
+
|
|
84
|
+
// Move up one directory
|
|
85
|
+
const parent = dirname(currentDir);
|
|
86
|
+
if (parent === currentDir) break; // Reached filesystem root
|
|
87
|
+
currentDir = parent;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return results;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if the project root has an AGENTS.md file.
|
|
95
|
+
*
|
|
96
|
+
* @param projectRoot - The project root directory
|
|
97
|
+
* @returns true if AGENTS.md exists at the project root
|
|
98
|
+
*/
|
|
99
|
+
async hasRootAgentsMd(projectRoot: string): Promise<boolean> {
|
|
100
|
+
const agentsMdPath = join(resolve(projectRoot), "AGENTS.md");
|
|
101
|
+
const content = await this.readAgentsMd(agentsMdPath);
|
|
102
|
+
return content !== null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get the content of the root AGENTS.md file.
|
|
107
|
+
*
|
|
108
|
+
* @param projectRoot - The project root directory
|
|
109
|
+
* @returns The content of the AGENTS.md file, or null if not found
|
|
110
|
+
*/
|
|
111
|
+
async getRootAgentsMdContent(projectRoot: string): Promise<string | null> {
|
|
112
|
+
const agentsMdPath = join(resolve(projectRoot), "AGENTS.md");
|
|
113
|
+
return this.readAgentsMd(agentsMdPath);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Read an AGENTS.md file with caching.
|
|
118
|
+
*/
|
|
119
|
+
private async readAgentsMd(absolutePath: string): Promise<string | null> {
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
const cachedTimestamp = this.cacheTimestamps.get(absolutePath);
|
|
122
|
+
|
|
123
|
+
// Check if cache entry exists and is still valid
|
|
124
|
+
if (cachedTimestamp && now - cachedTimestamp < this.CACHE_TTL) {
|
|
125
|
+
const cached = this.contentCache.get(absolutePath);
|
|
126
|
+
if (cached !== undefined) {
|
|
127
|
+
return cached;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Read from disk
|
|
132
|
+
try {
|
|
133
|
+
if (!existsSync(absolutePath)) {
|
|
134
|
+
this.contentCache.set(absolutePath, null);
|
|
135
|
+
this.cacheTimestamps.set(absolutePath, now);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const content = await readFile(absolutePath, "utf-8");
|
|
140
|
+
this.contentCache.set(absolutePath, content);
|
|
141
|
+
this.cacheTimestamps.set(absolutePath, now);
|
|
142
|
+
return content;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
// Trace error for debugging (file not found after existsSync is unexpected)
|
|
145
|
+
trace.getActiveSpan?.()?.addEvent("agents_md.read_error", {
|
|
146
|
+
"agents_md.path": absolutePath,
|
|
147
|
+
"agents_md.error": error instanceof Error ? error.message : String(error),
|
|
148
|
+
});
|
|
149
|
+
this.contentCache.set(absolutePath, null);
|
|
150
|
+
this.cacheTimestamps.set(absolutePath, now);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if a path is a directory
|
|
157
|
+
*/
|
|
158
|
+
private async isDirectory(path: string): Promise<boolean> {
|
|
159
|
+
try {
|
|
160
|
+
const stats = await stat(path);
|
|
161
|
+
return stats.isDirectory();
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Clear the cache (useful for testing)
|
|
169
|
+
*/
|
|
170
|
+
clearCache(): void {
|
|
171
|
+
this.contentCache.clear();
|
|
172
|
+
this.cacheTimestamps.clear();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Invalidate a specific path in the cache
|
|
177
|
+
*/
|
|
178
|
+
invalidatePath(absolutePath: string): void {
|
|
179
|
+
this.contentCache.delete(absolutePath);
|
|
180
|
+
this.cacheTimestamps.delete(absolutePath);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const agentsMdService = new AgentsMdServiceImpl();
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SystemReminderInjector - Injects AGENTS.md content as system reminders
|
|
3
|
+
*
|
|
4
|
+
* This module handles the injection of AGENTS.md content into tool results,
|
|
5
|
+
* following these rules:
|
|
6
|
+
*
|
|
7
|
+
* 1. When a file/directory is read, find all AGENTS.md files from that path
|
|
8
|
+
* up to the project root
|
|
9
|
+
* 2. Inject system reminders after the tool output
|
|
10
|
+
* 3. Track which AGENTS.md files have already been shown (non-truncated)
|
|
11
|
+
* to avoid duplication
|
|
12
|
+
* 4. Format multiple AGENTS.md files in a single system-reminder block
|
|
13
|
+
* with clear path attribution
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { resolve, relative } from "node:path";
|
|
17
|
+
import { agentsMdService, type AgentsMdFile } from "./AgentsMdService";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Tracker for which AGENTS.md files have been shown in the current context
|
|
21
|
+
*/
|
|
22
|
+
export interface AgentsMdVisibilityTracker {
|
|
23
|
+
/**
|
|
24
|
+
* Check if an AGENTS.md file has already been shown (is visible)
|
|
25
|
+
*/
|
|
26
|
+
isVisible(agentsMdPath: string): boolean;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Mark an AGENTS.md file as visible (shown in a non-truncated tool result)
|
|
30
|
+
*/
|
|
31
|
+
markVisible(agentsMdPath: string): void;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get all visible AGENTS.md paths
|
|
35
|
+
*/
|
|
36
|
+
getVisiblePaths(): Set<string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a visibility tracker for AGENTS.md files
|
|
41
|
+
*/
|
|
42
|
+
export function createAgentsMdVisibilityTracker(): AgentsMdVisibilityTracker {
|
|
43
|
+
const visiblePaths = new Set<string>();
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
isVisible(agentsMdPath: string): boolean {
|
|
47
|
+
return visiblePaths.has(resolve(agentsMdPath));
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
markVisible(agentsMdPath: string): void {
|
|
51
|
+
visiblePaths.add(resolve(agentsMdPath));
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
getVisiblePaths(): Set<string> {
|
|
55
|
+
return visiblePaths;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Result from checking for relevant AGENTS.md system reminders
|
|
62
|
+
*/
|
|
63
|
+
export interface SystemReminderResult {
|
|
64
|
+
/** Whether any new system reminders need to be injected */
|
|
65
|
+
hasReminders: boolean;
|
|
66
|
+
/** The formatted system reminder content (empty if no reminders) */
|
|
67
|
+
content: string;
|
|
68
|
+
/** The AGENTS.md files that were included */
|
|
69
|
+
includedFiles: AgentsMdFile[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Format AGENTS.md files into a system reminder block.
|
|
74
|
+
*
|
|
75
|
+
* The format groups multiple files with clear path attribution:
|
|
76
|
+
* ```
|
|
77
|
+
* <system-reminder>
|
|
78
|
+
* # AGENTS.md from /path/to/directory
|
|
79
|
+
*
|
|
80
|
+
* [content of AGENTS.md]
|
|
81
|
+
*
|
|
82
|
+
* # AGENTS.md from /parent/directory
|
|
83
|
+
*
|
|
84
|
+
* [content of parent AGENTS.md]
|
|
85
|
+
* </system-reminder>
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function formatSystemReminder(
|
|
89
|
+
files: AgentsMdFile[],
|
|
90
|
+
projectRoot: string
|
|
91
|
+
): string {
|
|
92
|
+
if (files.length === 0) {
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const sections = files.map((file) => {
|
|
97
|
+
const relativePath = relative(projectRoot, file.directory);
|
|
98
|
+
const displayPath = relativePath || "(project root)";
|
|
99
|
+
return `# AGENTS.md from ${displayPath}\n\n${file.content.trim()}`;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return `\n<system-reminder>\n${sections.join("\n\n")}\n</system-reminder>`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get system reminders for a file read operation.
|
|
107
|
+
*
|
|
108
|
+
* This function:
|
|
109
|
+
* 1. Finds all AGENTS.md files from the target path up to project root
|
|
110
|
+
* 2. Filters out already-visible files (using the tracker)
|
|
111
|
+
* 3. Marks newly-found files as visible
|
|
112
|
+
* 4. Returns formatted system reminder content
|
|
113
|
+
*
|
|
114
|
+
* @param targetPath - The file/directory path being read
|
|
115
|
+
* @param projectRoot - The project root directory
|
|
116
|
+
* @param tracker - Visibility tracker for deduplication
|
|
117
|
+
* @param isTruncated - Whether the tool result is truncated (don't mark as visible if so)
|
|
118
|
+
* @returns System reminder result with content and metadata
|
|
119
|
+
*/
|
|
120
|
+
export async function getSystemRemindersForPath(
|
|
121
|
+
targetPath: string,
|
|
122
|
+
projectRoot: string,
|
|
123
|
+
tracker: AgentsMdVisibilityTracker,
|
|
124
|
+
isTruncated: boolean = false
|
|
125
|
+
): Promise<SystemReminderResult> {
|
|
126
|
+
const absoluteProjectRoot = resolve(projectRoot);
|
|
127
|
+
|
|
128
|
+
// Find all AGENTS.md files from the target path up to project root
|
|
129
|
+
const allFiles = await agentsMdService.findAgentsMdFiles(targetPath, absoluteProjectRoot);
|
|
130
|
+
|
|
131
|
+
// Filter out already-visible files
|
|
132
|
+
const newFiles = allFiles.filter((file) => !tracker.isVisible(file.path));
|
|
133
|
+
|
|
134
|
+
if (newFiles.length === 0) {
|
|
135
|
+
return {
|
|
136
|
+
hasReminders: false,
|
|
137
|
+
content: "",
|
|
138
|
+
includedFiles: [],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Only mark as visible if the tool result is NOT truncated
|
|
143
|
+
// (truncated results aren't really "visible" to the model)
|
|
144
|
+
if (!isTruncated) {
|
|
145
|
+
for (const file of newFiles) {
|
|
146
|
+
tracker.markVisible(file.path);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const content = formatSystemReminder(newFiles, absoluteProjectRoot);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
hasReminders: true,
|
|
154
|
+
content,
|
|
155
|
+
includedFiles: newFiles,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if a tool name is one that reads files/directories
|
|
161
|
+
* and should trigger AGENTS.md system reminder injection.
|
|
162
|
+
*/
|
|
163
|
+
export function shouldInjectForTool(toolName: string): boolean {
|
|
164
|
+
// Tools that read files/directories
|
|
165
|
+
const fileReadTools = [
|
|
166
|
+
"fs_read",
|
|
167
|
+
"Read", // Claude Code's Read tool
|
|
168
|
+
"mcp__filesystem__read_file",
|
|
169
|
+
"mcp__filesystem__read_directory",
|
|
170
|
+
"mcp__filesystem__list_directory",
|
|
171
|
+
"mcp__filesystem__get_file_info",
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
// Check for exact match or pattern match
|
|
175
|
+
return fileReadTools.includes(toolName) ||
|
|
176
|
+
toolName.startsWith("mcp__") && toolName.includes("read");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Extract the path from a tool's input arguments.
|
|
181
|
+
* Different tools use different parameter names for the path.
|
|
182
|
+
*/
|
|
183
|
+
export function extractPathFromToolInput(input: unknown): string | null {
|
|
184
|
+
if (!input || typeof input !== "object") {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const inputObj = input as Record<string, unknown>;
|
|
189
|
+
|
|
190
|
+
// fs_read and similar tools use 'path'
|
|
191
|
+
if ("path" in inputObj && typeof inputObj.path === "string") {
|
|
192
|
+
return inputObj.path;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Some tools use 'file_path'
|
|
196
|
+
if ("file_path" in inputObj && typeof inputObj.file_path === "string") {
|
|
197
|
+
return inputObj.file_path;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Some tools use 'directory'
|
|
201
|
+
if ("directory" in inputObj && typeof inputObj.directory === "string") {
|
|
202
|
+
return inputObj.directory;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Append system reminder content to a tool result output.
|
|
210
|
+
*
|
|
211
|
+
* @param output - The original tool result output
|
|
212
|
+
* @param reminderContent - The system reminder content to append
|
|
213
|
+
* @returns The modified output with system reminder appended
|
|
214
|
+
*/
|
|
215
|
+
export function appendSystemReminderToOutput(
|
|
216
|
+
output: unknown,
|
|
217
|
+
reminderContent: string
|
|
218
|
+
): unknown {
|
|
219
|
+
if (typeof output === "string") {
|
|
220
|
+
return output + reminderContent;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (output && typeof output === "object" && "value" in output) {
|
|
224
|
+
const existingOutput = output as { type?: string; value: unknown };
|
|
225
|
+
const valueStr = typeof existingOutput.value === "string"
|
|
226
|
+
? existingOutput.value
|
|
227
|
+
: JSON.stringify(existingOutput.value);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
...existingOutput,
|
|
231
|
+
value: valueStr + reminderContent,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// For other formats, try to stringify and append
|
|
236
|
+
const outputStr = typeof output === "string" ? output : JSON.stringify(output);
|
|
237
|
+
return outputStr + reminderContent;
|
|
238
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { agentsMdService, type AgentsMdFile } from "./AgentsMdService";
|
|
2
|
+
export {
|
|
3
|
+
createAgentsMdVisibilityTracker,
|
|
4
|
+
formatSystemReminder,
|
|
5
|
+
getSystemRemindersForPath,
|
|
6
|
+
shouldInjectForTool,
|
|
7
|
+
extractPathFromToolInput,
|
|
8
|
+
appendSystemReminderToOutput,
|
|
9
|
+
type AgentsMdVisibilityTracker,
|
|
10
|
+
type SystemReminderResult,
|
|
11
|
+
} from "./SystemReminderInjector";
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP/2 client for Apple Push Notification service (APNs).
|
|
3
|
+
*
|
|
4
|
+
* Handles JWT token generation (ES256) and push delivery.
|
|
5
|
+
* Uses native fetch (Bun supports HTTP/2 via ALPN).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as crypto from "node:crypto";
|
|
10
|
+
import { logger } from "@/utils/logger";
|
|
11
|
+
import type { APNsPayload, APNsSendResult } from "./types";
|
|
12
|
+
|
|
13
|
+
const LOG_PREFIX = "[APNsClient]";
|
|
14
|
+
|
|
15
|
+
/** JWT tokens are valid for up to 1 hour; we refresh at 50 minutes. */
|
|
16
|
+
const JWT_REFRESH_INTERVAL_MS = 50 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
/** APNs production endpoint */
|
|
19
|
+
const APNS_PRODUCTION_HOST = "https://api.push.apple.com";
|
|
20
|
+
|
|
21
|
+
/** APNs sandbox endpoint */
|
|
22
|
+
const APNS_SANDBOX_HOST = "https://api.sandbox.push.apple.com";
|
|
23
|
+
|
|
24
|
+
export interface APNsClientConfig {
|
|
25
|
+
keyPath: string;
|
|
26
|
+
keyId: string;
|
|
27
|
+
teamId: string;
|
|
28
|
+
bundleId: string;
|
|
29
|
+
production: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Base64url encode a buffer (no padding).
|
|
34
|
+
*/
|
|
35
|
+
function base64url(buf: Buffer): string {
|
|
36
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class APNsClient {
|
|
40
|
+
private config: APNsClientConfig;
|
|
41
|
+
private signingKey: crypto.KeyObject | null = null;
|
|
42
|
+
private cachedJwt: string | null = null;
|
|
43
|
+
private jwtIssuedAt = 0;
|
|
44
|
+
private fetchFn: typeof fetch;
|
|
45
|
+
|
|
46
|
+
constructor(clientConfig: APNsClientConfig, fetchFn?: typeof fetch) {
|
|
47
|
+
this.config = clientConfig;
|
|
48
|
+
this.fetchFn = fetchFn ?? fetch;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load the .p8 private key from disk.
|
|
53
|
+
* Called lazily on first send.
|
|
54
|
+
*/
|
|
55
|
+
private loadSigningKey(): void {
|
|
56
|
+
const keyData = fs.readFileSync(this.config.keyPath, "utf-8");
|
|
57
|
+
this.signingKey = crypto.createPrivateKey(keyData);
|
|
58
|
+
|
|
59
|
+
logger.info(`${LOG_PREFIX} Loaded APNs signing key`, {
|
|
60
|
+
keyId: this.config.keyId,
|
|
61
|
+
teamId: this.config.teamId,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate a JWT for APNs authentication (ES256).
|
|
67
|
+
* Caches the token and refreshes before expiry.
|
|
68
|
+
*/
|
|
69
|
+
private getJwt(): string {
|
|
70
|
+
const now = Math.floor(Date.now() / 1000);
|
|
71
|
+
|
|
72
|
+
// Return cached JWT if still fresh
|
|
73
|
+
if (this.cachedJwt && (now - this.jwtIssuedAt) * 1000 < JWT_REFRESH_INTERVAL_MS) {
|
|
74
|
+
return this.cachedJwt;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!this.signingKey) {
|
|
78
|
+
this.loadSigningKey();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// JWT Header
|
|
82
|
+
const header = base64url(
|
|
83
|
+
Buffer.from(JSON.stringify({ alg: "ES256", kid: this.config.keyId }))
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// JWT Payload
|
|
87
|
+
const payload = base64url(
|
|
88
|
+
Buffer.from(JSON.stringify({ iss: this.config.teamId, iat: now }))
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Sign
|
|
92
|
+
if (!this.signingKey) {
|
|
93
|
+
throw new Error("Signing key not loaded");
|
|
94
|
+
}
|
|
95
|
+
const signingInput = `${header}.${payload}`;
|
|
96
|
+
const sign = crypto.createSign("SHA256");
|
|
97
|
+
sign.update(signingInput);
|
|
98
|
+
const derSignature = sign.sign(this.signingKey);
|
|
99
|
+
|
|
100
|
+
// Convert DER signature to raw r||s (64 bytes) for ES256
|
|
101
|
+
const rawSignature = derToRaw(derSignature);
|
|
102
|
+
const signature = base64url(rawSignature);
|
|
103
|
+
|
|
104
|
+
this.cachedJwt = `${signingInput}.${signature}`;
|
|
105
|
+
this.jwtIssuedAt = now;
|
|
106
|
+
|
|
107
|
+
logger.debug(`${LOG_PREFIX} Generated new JWT`, { iat: now });
|
|
108
|
+
|
|
109
|
+
return this.cachedJwt;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Send a push notification to a single device token.
|
|
114
|
+
*/
|
|
115
|
+
async send(deviceToken: string, payload: APNsPayload): Promise<APNsSendResult> {
|
|
116
|
+
const host = this.config.production ? APNS_PRODUCTION_HOST : APNS_SANDBOX_HOST;
|
|
117
|
+
const url = `${host}/3/device/${deviceToken}`;
|
|
118
|
+
const body = JSON.stringify(payload);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const jwt = this.getJwt();
|
|
122
|
+
const response = await this.fetchFn(url, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: {
|
|
125
|
+
"authorization": `bearer ${jwt}`,
|
|
126
|
+
"apns-topic": this.config.bundleId,
|
|
127
|
+
"apns-push-type": "alert",
|
|
128
|
+
"apns-priority": "10",
|
|
129
|
+
"content-type": "application/json",
|
|
130
|
+
},
|
|
131
|
+
body,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const statusCode = response.status;
|
|
135
|
+
|
|
136
|
+
if (statusCode === 200) {
|
|
137
|
+
return { success: true, statusCode };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Parse error response
|
|
141
|
+
const errorBody = await response.json().catch(() => ({})) as Record<string, unknown>;
|
|
142
|
+
const reason = (errorBody.reason as string) ?? "unknown";
|
|
143
|
+
|
|
144
|
+
logger.warn(`${LOG_PREFIX} APNs rejected push`, {
|
|
145
|
+
statusCode,
|
|
146
|
+
reason,
|
|
147
|
+
deviceToken: deviceToken.substring(0, 8),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const result: APNsSendResult = { success: false, statusCode, reason };
|
|
151
|
+
|
|
152
|
+
// 410 Gone means the token is no longer valid
|
|
153
|
+
if (statusCode === 410 && typeof errorBody.timestamp === "number") {
|
|
154
|
+
result.timestampMs = errorBody.timestamp as number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return result;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
logger.error(`${LOG_PREFIX} Failed to send push notification`, {
|
|
160
|
+
error: error instanceof Error ? error.message : String(error),
|
|
161
|
+
deviceToken: deviceToken.substring(0, 8),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return { success: false, statusCode: 0, reason: "network_error" };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Convert a DER-encoded ECDSA signature to raw r||s format (64 bytes for P-256).
|
|
171
|
+
* DER format: 0x30 <total_len> 0x02 <r_len> <r> 0x02 <s_len> <s>
|
|
172
|
+
*/
|
|
173
|
+
function derToRaw(der: Buffer): Buffer {
|
|
174
|
+
const raw = Buffer.alloc(64);
|
|
175
|
+
|
|
176
|
+
// Parse DER structure
|
|
177
|
+
let offset = 2; // skip 0x30 and total length
|
|
178
|
+
|
|
179
|
+
// Read r
|
|
180
|
+
offset += 1; // skip 0x02
|
|
181
|
+
const rLen = der[offset] ?? 0;
|
|
182
|
+
offset += 1;
|
|
183
|
+
const rBytes = der.subarray(offset, offset + rLen);
|
|
184
|
+
offset += rLen;
|
|
185
|
+
|
|
186
|
+
// Read s
|
|
187
|
+
offset += 1; // skip 0x02
|
|
188
|
+
const sLen = der[offset] ?? 0;
|
|
189
|
+
offset += 1;
|
|
190
|
+
const sBytes = der.subarray(offset, offset + sLen);
|
|
191
|
+
|
|
192
|
+
// Copy r (right-aligned, strip leading zero if present)
|
|
193
|
+
const rStart = rBytes[0] === 0 ? 1 : 0;
|
|
194
|
+
const rActual = rBytes.subarray(rStart);
|
|
195
|
+
rActual.copy(raw, 32 - rActual.length);
|
|
196
|
+
|
|
197
|
+
// Copy s (right-aligned, strip leading zero if present)
|
|
198
|
+
const sStart = sBytes[0] === 0 ? 1 : 0;
|
|
199
|
+
const sActual = sBytes.subarray(sStart);
|
|
200
|
+
sActual.copy(raw, 64 - sActual.length);
|
|
201
|
+
|
|
202
|
+
return raw;
|
|
203
|
+
}
|