@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,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* APNs push notification service.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* 1. Subscribe to kind 25000 events (encrypted config updates) p-tagging the backend
|
|
6
|
+
* 2. Decrypt NIP-44 content to extract APNs device tokens
|
|
7
|
+
* 3. Manage token store: Map<pubkey, Set<deviceToken>>, persisted to disk
|
|
8
|
+
* 4. Expose notifyIfNeeded() for the ask tool to trigger push notifications
|
|
9
|
+
* 5. Handle token lifecycle (register, refresh, disable, invalidation)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readJsonFile, writeJsonFile } from "@/lib/fs/filesystem";
|
|
13
|
+
import { getNDK } from "@/nostr/ndkClient";
|
|
14
|
+
import { nip44Decrypt } from "@/nostr/encryption";
|
|
15
|
+
import { NDKKind } from "@/nostr/kinds";
|
|
16
|
+
import { config } from "@/services/ConfigService";
|
|
17
|
+
import { logger } from "@/utils/logger";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import type { NDKEvent, NDKSubscription, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
|
20
|
+
import { APNsClient, type APNsClientConfig } from "./APNsClient";
|
|
21
|
+
import type { APNsPayload, APNsSendResult, ConfigUpdateContent, NotificationRequest } from "./types";
|
|
22
|
+
|
|
23
|
+
/** Minimal interface for APNs push delivery (testable). */
|
|
24
|
+
export interface APNsClientLike {
|
|
25
|
+
send(deviceToken: string, payload: APNsPayload): Promise<APNsSendResult>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const LOG_PREFIX = "[APNsService]";
|
|
29
|
+
const TOKENS_FILENAME = "apns-tokens.json";
|
|
30
|
+
|
|
31
|
+
export class APNsService {
|
|
32
|
+
private static instance: APNsService | null = null;
|
|
33
|
+
|
|
34
|
+
private client: APNsClientLike | null = null;
|
|
35
|
+
private subscription: NDKSubscription | null = null;
|
|
36
|
+
private backendSigner: NDKPrivateKeySigner | null = null;
|
|
37
|
+
|
|
38
|
+
/** Factory for creating the APNs client. Overridable for testing. */
|
|
39
|
+
createClient: (config: APNsClientConfig) => APNsClientLike = (c) => new APNsClient(c);
|
|
40
|
+
|
|
41
|
+
/** In-memory token store: pubkey → Set<deviceToken> */
|
|
42
|
+
private tokenStore = new Map<string, Set<string>>();
|
|
43
|
+
|
|
44
|
+
private tokensFilePath: string;
|
|
45
|
+
private initialized = false;
|
|
46
|
+
|
|
47
|
+
private constructor() {
|
|
48
|
+
this.tokensFilePath = path.join(config.getConfigPath("data"), TOKENS_FILENAME);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static getInstance(): APNsService {
|
|
52
|
+
if (!APNsService.instance) {
|
|
53
|
+
APNsService.instance = new APNsService();
|
|
54
|
+
}
|
|
55
|
+
return APNsService.instance;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Reset the singleton (for testing).
|
|
60
|
+
*/
|
|
61
|
+
static resetInstance(): void {
|
|
62
|
+
if (APNsService.instance) {
|
|
63
|
+
APNsService.instance.shutdown();
|
|
64
|
+
}
|
|
65
|
+
APNsService.instance = null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Initialize the APNs service.
|
|
70
|
+
*
|
|
71
|
+
* - Reads APNs config from config.json
|
|
72
|
+
* - Creates the APNsClient with .p8 key
|
|
73
|
+
* - Subscribes to kind 25000 events for token registration
|
|
74
|
+
*/
|
|
75
|
+
async initialize(): Promise<void> {
|
|
76
|
+
if (this.initialized) {
|
|
77
|
+
logger.warn(`${LOG_PREFIX} Already initialized`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const tenexConfig = config.getConfig();
|
|
82
|
+
const apnsConfig = tenexConfig.apns;
|
|
83
|
+
|
|
84
|
+
if (!apnsConfig?.enabled) {
|
|
85
|
+
logger.debug(`${LOG_PREFIX} Disabled (apns.enabled is false or not set)`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Validate required fields
|
|
90
|
+
const { keyPath, keyId, teamId, bundleId } = apnsConfig;
|
|
91
|
+
if (!keyPath || !keyId || !teamId || !bundleId) {
|
|
92
|
+
logger.error(`${LOG_PREFIX} Missing required APNs config fields`, {
|
|
93
|
+
hasKeyPath: !!keyPath,
|
|
94
|
+
hasKeyId: !!keyId,
|
|
95
|
+
hasTeamId: !!teamId,
|
|
96
|
+
hasBundleId: !!bundleId,
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Load persisted tokens before subscribing to new events
|
|
102
|
+
await this.loadTokenStore();
|
|
103
|
+
|
|
104
|
+
// Create the HTTP/2 client
|
|
105
|
+
const clientConfig: APNsClientConfig = {
|
|
106
|
+
keyPath,
|
|
107
|
+
keyId,
|
|
108
|
+
teamId,
|
|
109
|
+
bundleId,
|
|
110
|
+
production: apnsConfig.production ?? false,
|
|
111
|
+
};
|
|
112
|
+
this.client = this.createClient(clientConfig);
|
|
113
|
+
|
|
114
|
+
// Get backend signer for NIP-44 decryption
|
|
115
|
+
this.backendSigner = await config.getBackendSigner();
|
|
116
|
+
const backendPubkey = (await this.backendSigner.user()).pubkey;
|
|
117
|
+
|
|
118
|
+
// Subscribe to kind 25000 events addressed to the backend
|
|
119
|
+
const ndk = getNDK();
|
|
120
|
+
this.subscription = ndk.subscribe(
|
|
121
|
+
{
|
|
122
|
+
kinds: [NDKKind.TenexConfigUpdate as number],
|
|
123
|
+
"#p": [backendPubkey],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
closeOnEose: false,
|
|
127
|
+
onEvent: (event: NDKEvent) => {
|
|
128
|
+
this.handleConfigUpdateEvent(event).catch((err) => {
|
|
129
|
+
logger.error(`${LOG_PREFIX} Error handling config update event`, {
|
|
130
|
+
error: err instanceof Error ? err.message : String(err),
|
|
131
|
+
eventId: event.id?.substring(0, 8),
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
this.initialized = true;
|
|
139
|
+
|
|
140
|
+
logger.info(`${LOG_PREFIX} Initialized`, {
|
|
141
|
+
production: clientConfig.production,
|
|
142
|
+
bundleId: clientConfig.bundleId,
|
|
143
|
+
backendPubkey: backendPubkey.substring(0, 8),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Handle an incoming kind 25000 config update event.
|
|
149
|
+
*
|
|
150
|
+
* Decrypts NIP-44 content and processes APNs token registration/deregistration.
|
|
151
|
+
*/
|
|
152
|
+
private async handleConfigUpdateEvent(event: NDKEvent): Promise<void> {
|
|
153
|
+
if (!this.backendSigner) return;
|
|
154
|
+
|
|
155
|
+
const senderPubkey = event.pubkey;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Decrypt NIP-44 content via nostr layer wrapper
|
|
159
|
+
const decrypted = await nip44Decrypt(senderPubkey, event.content, this.backendSigner);
|
|
160
|
+
const content = JSON.parse(decrypted) as ConfigUpdateContent;
|
|
161
|
+
|
|
162
|
+
if (!content.notifications) {
|
|
163
|
+
logger.debug(`${LOG_PREFIX} Config update has no notifications section, skipping`, {
|
|
164
|
+
sender: senderPubkey.substring(0, 8),
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { enable, apn_token } = content.notifications;
|
|
170
|
+
|
|
171
|
+
if (enable && apn_token) {
|
|
172
|
+
// Register or refresh token
|
|
173
|
+
await this.addToken(senderPubkey, apn_token);
|
|
174
|
+
logger.info(`${LOG_PREFIX} Registered device token`, {
|
|
175
|
+
sender: senderPubkey.substring(0, 8),
|
|
176
|
+
tokenPrefix: apn_token.substring(0, 8),
|
|
177
|
+
totalTokens: this.getTokenCount(senderPubkey),
|
|
178
|
+
});
|
|
179
|
+
} else if (!enable) {
|
|
180
|
+
// Disable: remove all tokens for this user
|
|
181
|
+
const removed = await this.removeAllTokens(senderPubkey);
|
|
182
|
+
logger.info(`${LOG_PREFIX} Disabled notifications for user`, {
|
|
183
|
+
sender: senderPubkey.substring(0, 8),
|
|
184
|
+
tokensRemoved: removed,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
logger.error(`${LOG_PREFIX} Failed to process config update event`, {
|
|
189
|
+
error: error instanceof Error ? error.message : String(error),
|
|
190
|
+
sender: senderPubkey.substring(0, 8),
|
|
191
|
+
eventId: event.id?.substring(0, 8),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Send a push notification if the user has registered tokens.
|
|
198
|
+
*
|
|
199
|
+
* Called from the ask tool when the user is not connected.
|
|
200
|
+
* No-ops gracefully if APNs is not configured or user has no tokens.
|
|
201
|
+
*/
|
|
202
|
+
async notifyIfNeeded(userPubkey: string, request: NotificationRequest): Promise<void> {
|
|
203
|
+
if (!this.client) return;
|
|
204
|
+
|
|
205
|
+
const tokens = this.tokenStore.get(userPubkey);
|
|
206
|
+
if (!tokens || tokens.size === 0) {
|
|
207
|
+
logger.debug(`${LOG_PREFIX} No tokens for user, skipping push`, {
|
|
208
|
+
user: userPubkey.substring(0, 8),
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const payload: APNsPayload = {
|
|
214
|
+
aps: {
|
|
215
|
+
alert: {
|
|
216
|
+
title: request.title,
|
|
217
|
+
body: request.body,
|
|
218
|
+
},
|
|
219
|
+
sound: "default",
|
|
220
|
+
badge: 1,
|
|
221
|
+
},
|
|
222
|
+
conversationId: request.conversationId,
|
|
223
|
+
eventId: request.eventId,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Send to all registered tokens for this user
|
|
227
|
+
const invalidTokens: string[] = [];
|
|
228
|
+
|
|
229
|
+
for (const token of tokens) {
|
|
230
|
+
const result = await this.client.send(token, payload);
|
|
231
|
+
|
|
232
|
+
if (result.success) {
|
|
233
|
+
logger.info(`${LOG_PREFIX} Push notification sent`, {
|
|
234
|
+
user: userPubkey.substring(0, 8),
|
|
235
|
+
tokenPrefix: token.substring(0, 8),
|
|
236
|
+
});
|
|
237
|
+
} else if (result.statusCode === 410 || result.reason === "BadDeviceToken" || result.reason === "Unregistered") {
|
|
238
|
+
// Token is no longer valid
|
|
239
|
+
invalidTokens.push(token);
|
|
240
|
+
logger.warn(`${LOG_PREFIX} Removing invalid token`, {
|
|
241
|
+
user: userPubkey.substring(0, 8),
|
|
242
|
+
tokenPrefix: token.substring(0, 8),
|
|
243
|
+
reason: result.reason,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// Other failures are logged by APNsClient but we don't remove the token
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Clean up invalid tokens
|
|
250
|
+
for (const token of invalidTokens) {
|
|
251
|
+
await this.removeToken(userPubkey, token);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Check if APNs is enabled and has a client configured.
|
|
257
|
+
*/
|
|
258
|
+
isEnabled(): boolean {
|
|
259
|
+
return this.initialized && this.client !== null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Check if a user has any registered tokens.
|
|
264
|
+
*/
|
|
265
|
+
hasTokens(userPubkey: string): boolean {
|
|
266
|
+
const tokens = this.tokenStore.get(userPubkey);
|
|
267
|
+
return tokens !== undefined && tokens.size > 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// =====================================================================================
|
|
271
|
+
// TOKEN STORE MANAGEMENT
|
|
272
|
+
// =====================================================================================
|
|
273
|
+
|
|
274
|
+
private async addToken(pubkey: string, token: string): Promise<void> {
|
|
275
|
+
let tokens = this.tokenStore.get(pubkey);
|
|
276
|
+
if (!tokens) {
|
|
277
|
+
tokens = new Set();
|
|
278
|
+
this.tokenStore.set(pubkey, tokens);
|
|
279
|
+
}
|
|
280
|
+
tokens.add(token);
|
|
281
|
+
await this.persistTokenStore();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private async removeToken(pubkey: string, token: string): Promise<void> {
|
|
285
|
+
const tokens = this.tokenStore.get(pubkey);
|
|
286
|
+
if (tokens) {
|
|
287
|
+
tokens.delete(token);
|
|
288
|
+
if (tokens.size === 0) {
|
|
289
|
+
this.tokenStore.delete(pubkey);
|
|
290
|
+
}
|
|
291
|
+
await this.persistTokenStore();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private async removeAllTokens(pubkey: string): Promise<number> {
|
|
296
|
+
const tokens = this.tokenStore.get(pubkey);
|
|
297
|
+
const count = tokens?.size ?? 0;
|
|
298
|
+
this.tokenStore.delete(pubkey);
|
|
299
|
+
if (count > 0) {
|
|
300
|
+
await this.persistTokenStore();
|
|
301
|
+
}
|
|
302
|
+
return count;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private getTokenCount(pubkey: string): number {
|
|
306
|
+
return this.tokenStore.get(pubkey)?.size ?? 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// =====================================================================================
|
|
310
|
+
// TOKEN STORE PERSISTENCE
|
|
311
|
+
// =====================================================================================
|
|
312
|
+
|
|
313
|
+
private async loadTokenStore(): Promise<void> {
|
|
314
|
+
try {
|
|
315
|
+
const data = await readJsonFile<Record<string, string[]>>(this.tokensFilePath);
|
|
316
|
+
if (!data) return;
|
|
317
|
+
|
|
318
|
+
for (const [pubkey, tokens] of Object.entries(data)) {
|
|
319
|
+
if (Array.isArray(tokens) && tokens.length > 0) {
|
|
320
|
+
this.tokenStore.set(pubkey, new Set(tokens));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
logger.info(`${LOG_PREFIX} Loaded token store from disk`, {
|
|
325
|
+
users: this.tokenStore.size,
|
|
326
|
+
});
|
|
327
|
+
} catch (error) {
|
|
328
|
+
logger.warn(`${LOG_PREFIX} Failed to load token store, starting empty`, {
|
|
329
|
+
error: error instanceof Error ? error.message : String(error),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private async persistTokenStore(): Promise<void> {
|
|
335
|
+
const data: Record<string, string[]> = {};
|
|
336
|
+
for (const [pubkey, tokens] of this.tokenStore) {
|
|
337
|
+
data[pubkey] = [...tokens];
|
|
338
|
+
}
|
|
339
|
+
await writeJsonFile(this.tokensFilePath, data);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// =====================================================================================
|
|
343
|
+
// LIFECYCLE
|
|
344
|
+
// =====================================================================================
|
|
345
|
+
|
|
346
|
+
shutdown(): void {
|
|
347
|
+
if (this.subscription) {
|
|
348
|
+
this.subscription.stop();
|
|
349
|
+
this.subscription = null;
|
|
350
|
+
}
|
|
351
|
+
this.tokenStore.clear();
|
|
352
|
+
this.client = null;
|
|
353
|
+
this.backendSigner = null;
|
|
354
|
+
this.initialized = false;
|
|
355
|
+
|
|
356
|
+
logger.info(`${LOG_PREFIX} Shut down`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { APNsService } from "./APNsService";
|
|
2
|
+
export { APNsClient } from "./APNsClient";
|
|
3
|
+
export type { APNsClientConfig } from "./APNsClient";
|
|
4
|
+
export type {
|
|
5
|
+
APNsPayload,
|
|
6
|
+
APNsSendResult,
|
|
7
|
+
APNsAlert,
|
|
8
|
+
APNsAps,
|
|
9
|
+
ConfigUpdateContent,
|
|
10
|
+
NotificationRequest,
|
|
11
|
+
} from "./types";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* APNs push notification types.
|
|
3
|
+
*
|
|
4
|
+
* Covers device token registration (via kind 25000 events),
|
|
5
|
+
* Apple APNs payload format, and HTTP/2 client response handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// =====================================================================================
|
|
9
|
+
// CONFIG UPDATE EVENT (kind 25000) — decrypted content
|
|
10
|
+
// =====================================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Decrypted content of a kind 25000 config update event.
|
|
14
|
+
* The iOS client publishes this encrypted (NIP-44) to the backend's pubkey.
|
|
15
|
+
*/
|
|
16
|
+
export interface ConfigUpdateContent {
|
|
17
|
+
notifications?: {
|
|
18
|
+
enable: boolean;
|
|
19
|
+
apn_token: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// =====================================================================================
|
|
24
|
+
// APNs PAYLOAD — sent to Apple
|
|
25
|
+
// =====================================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* APNs alert payload structure.
|
|
29
|
+
*/
|
|
30
|
+
export interface APNsAlert {
|
|
31
|
+
title: string;
|
|
32
|
+
body: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The `aps` dictionary inside an APNs payload.
|
|
37
|
+
*/
|
|
38
|
+
export interface APNsAps {
|
|
39
|
+
alert: APNsAlert;
|
|
40
|
+
sound: string;
|
|
41
|
+
badge?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Full APNs payload sent to Apple's HTTP/2 API.
|
|
46
|
+
*/
|
|
47
|
+
export interface APNsPayload {
|
|
48
|
+
aps: APNsAps;
|
|
49
|
+
conversationId?: string;
|
|
50
|
+
eventId?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// =====================================================================================
|
|
54
|
+
// NOTIFICATION REQUEST — internal interface
|
|
55
|
+
// =====================================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Data passed from the ask tool to APNsService.notifyIfNeeded().
|
|
59
|
+
*/
|
|
60
|
+
export interface NotificationRequest {
|
|
61
|
+
title: string;
|
|
62
|
+
body: string;
|
|
63
|
+
conversationId: string;
|
|
64
|
+
eventId: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// =====================================================================================
|
|
68
|
+
// APNs CLIENT RESPONSE
|
|
69
|
+
// =====================================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Result from sending a push notification via APNsClient.
|
|
73
|
+
*/
|
|
74
|
+
export interface APNsSendResult {
|
|
75
|
+
success: boolean;
|
|
76
|
+
statusCode: number;
|
|
77
|
+
reason?: string;
|
|
78
|
+
/** Apple-provided timestamp when the token became invalid (for 410 Gone). */
|
|
79
|
+
timestampMs?: number;
|
|
80
|
+
}
|