@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.
Files changed (427) hide show
  1. package/README.md +194 -0
  2. package/dist/backend-wrapper.cjs +3 -0
  3. package/dist/src/index.js +331928 -0
  4. package/package.json +103 -0
  5. package/src/agents/AgentRegistry.ts +418 -0
  6. package/src/agents/AgentStorage.ts +1133 -0
  7. package/src/agents/ConfigResolver.ts +229 -0
  8. package/src/agents/agent-installer.ts +236 -0
  9. package/src/agents/agent-loader.ts +241 -0
  10. package/src/agents/constants.ts +82 -0
  11. package/src/agents/errors.ts +48 -0
  12. package/src/agents/execution/AgentExecutor.ts +561 -0
  13. package/src/agents/execution/ExecutionContextFactory.ts +112 -0
  14. package/src/agents/execution/MessageCompiler.ts +597 -0
  15. package/src/agents/execution/MessageSyncer.ts +100 -0
  16. package/src/agents/execution/PostCompletionChecker.ts +278 -0
  17. package/src/agents/execution/ProgressMonitor.ts +50 -0
  18. package/src/agents/execution/RALResolver.ts +177 -0
  19. package/src/agents/execution/SessionManager.ts +181 -0
  20. package/src/agents/execution/StreamCallbacks.ts +312 -0
  21. package/src/agents/execution/StreamExecutionHandler.ts +579 -0
  22. package/src/agents/execution/StreamSetup.ts +313 -0
  23. package/src/agents/execution/ToolEventHandlers.ts +239 -0
  24. package/src/agents/execution/ToolExecutionTracker.ts +498 -0
  25. package/src/agents/execution/ToolResultUtils.ts +97 -0
  26. package/src/agents/execution/ToolSupervisionWrapper.ts +174 -0
  27. package/src/agents/execution/constants.ts +16 -0
  28. package/src/agents/execution/index.ts +3 -0
  29. package/src/agents/execution/types.ts +96 -0
  30. package/src/agents/execution/utils.ts +26 -0
  31. package/src/agents/index.ts +4 -0
  32. package/src/agents/script-installer.ts +266 -0
  33. package/src/agents/supervision/SupervisorLLMService.ts +253 -0
  34. package/src/agents/supervision/SupervisorOrchestrator.ts +471 -0
  35. package/src/agents/supervision/heuristics/ConsecutiveToolsWithoutTodoHeuristic.ts +73 -0
  36. package/src/agents/supervision/heuristics/DelegationClaimHeuristic.ts +80 -0
  37. package/src/agents/supervision/heuristics/HeuristicRegistry.ts +114 -0
  38. package/src/agents/supervision/heuristics/PendingTodosHeuristic.ts +93 -0
  39. package/src/agents/supervision/heuristics/SilentAgentHeuristic.ts +54 -0
  40. package/src/agents/supervision/heuristics/index.ts +5 -0
  41. package/src/agents/supervision/index.ts +28 -0
  42. package/src/agents/supervision/registerHeuristics.ts +110 -0
  43. package/src/agents/supervision/supervisionHealthCheck.ts +123 -0
  44. package/src/agents/supervision/types.ts +171 -0
  45. package/src/agents/tool-names.ts +46 -0
  46. package/src/agents/tool-normalization.ts +184 -0
  47. package/src/agents/types/index.ts +2 -0
  48. package/src/agents/types/runtime.ts +74 -0
  49. package/src/agents/types/storage.ts +145 -0
  50. package/src/commands/agent/import/index.ts +6 -0
  51. package/src/commands/agent/import/openclaw-distiller.ts +57 -0
  52. package/src/commands/agent/import/openclaw-reader.ts +141 -0
  53. package/src/commands/agent/import/openclaw.ts +154 -0
  54. package/src/commands/agent/index.ts +6 -0
  55. package/src/commands/agent.ts +215 -0
  56. package/src/commands/daemon.ts +198 -0
  57. package/src/commands/doctor.ts +134 -0
  58. package/src/commands/setup/embed.ts +228 -0
  59. package/src/commands/setup/global-system-prompt.ts +223 -0
  60. package/src/commands/setup/image.ts +179 -0
  61. package/src/commands/setup/index.ts +16 -0
  62. package/src/commands/setup/interactive.ts +95 -0
  63. package/src/commands/setup/llm.ts +38 -0
  64. package/src/commands/setup/onboarding.ts +294 -0
  65. package/src/commands/setup/providers.ts +27 -0
  66. package/src/constants.ts +34 -0
  67. package/src/conversations/ConversationDiskReader.ts +148 -0
  68. package/src/conversations/ConversationRegistry.ts +728 -0
  69. package/src/conversations/ConversationStore.ts +868 -0
  70. package/src/conversations/MessageBuilder.ts +866 -0
  71. package/src/conversations/executionTime.ts +62 -0
  72. package/src/conversations/formatters/DelegationXmlFormatter.ts +64 -0
  73. package/src/conversations/formatters/ThreadedConversationFormatter.ts +303 -0
  74. package/src/conversations/formatters/index.ts +9 -0
  75. package/src/conversations/formatters/utils/MessageFormatter.ts +46 -0
  76. package/src/conversations/formatters/utils/TimestampFormatter.ts +56 -0
  77. package/src/conversations/formatters/utils/TreeBuilder.ts +131 -0
  78. package/src/conversations/formatters/utils/TreeRenderer.ts +49 -0
  79. package/src/conversations/index.ts +2 -0
  80. package/src/conversations/persistence/ToolMessageStorage.ts +143 -0
  81. package/src/conversations/search/ConversationIndexManager.ts +393 -0
  82. package/src/conversations/search/QueryParser.ts +114 -0
  83. package/src/conversations/search/SearchEngine.ts +175 -0
  84. package/src/conversations/search/SnippetExtractor.ts +345 -0
  85. package/src/conversations/search/embeddings/ConversationEmbeddingService.ts +484 -0
  86. package/src/conversations/search/embeddings/ConversationIndexingJob.ts +320 -0
  87. package/src/conversations/search/embeddings/IndexingStateManager.ts +338 -0
  88. package/src/conversations/search/embeddings/index.ts +18 -0
  89. package/src/conversations/search/index.ts +49 -0
  90. package/src/conversations/search/types.ts +124 -0
  91. package/src/conversations/services/CategoryManager.ts +160 -0
  92. package/src/conversations/services/ConversationResolver.ts +296 -0
  93. package/src/conversations/services/ConversationSummarizer.ts +234 -0
  94. package/src/conversations/services/MetadataDebounceManager.ts +188 -0
  95. package/src/conversations/services/index.ts +2 -0
  96. package/src/conversations/types.ts +148 -0
  97. package/src/conversations/utils/content-utils.ts +69 -0
  98. package/src/conversations/utils/image-placeholder.ts +281 -0
  99. package/src/conversations/utils/image-url-utils.ts +171 -0
  100. package/src/conversations/utils/multimodal-content.ts +90 -0
  101. package/src/conversations/utils/tool-result-truncator.ts +159 -0
  102. package/src/daemon/Daemon.ts +1883 -0
  103. package/src/daemon/ProjectRuntime.ts +657 -0
  104. package/src/daemon/RestartState.ts +152 -0
  105. package/src/daemon/RuntimeLifecycle.ts +268 -0
  106. package/src/daemon/SubscriptionManager.ts +305 -0
  107. package/src/daemon/UnixSocketTransport.ts +318 -0
  108. package/src/daemon/filters/SubscriptionFilterBuilder.ts +119 -0
  109. package/src/daemon/index.ts +9 -0
  110. package/src/daemon/routing/DaemonRouter.ts +491 -0
  111. package/src/daemon/types.ts +150 -0
  112. package/src/daemon/utils/routing-log.ts +76 -0
  113. package/src/daemon/utils/telemetry.ts +173 -0
  114. package/src/event-handler/agentDeletion.ts +383 -0
  115. package/src/event-handler/index.ts +749 -0
  116. package/src/event-handler/newConversation.ts +165 -0
  117. package/src/event-handler/project.ts +166 -0
  118. package/src/event-handler/reply.ts +18 -0
  119. package/src/events/NDKAgentDefinition.ts +292 -0
  120. package/src/events/NDKAgentLesson.ts +106 -0
  121. package/src/events/NDKEventMetadata.ts +34 -0
  122. package/src/events/NDKMCPTool.ts +60 -0
  123. package/src/events/NDKProjectStatus.ts +384 -0
  124. package/src/events/index.ts +4 -0
  125. package/src/index.ts +126 -0
  126. package/src/lib/agent-home.ts +334 -0
  127. package/src/lib/error-formatter.ts +200 -0
  128. package/src/lib/fs/filesystem.ts +128 -0
  129. package/src/lib/fs/index.ts +1 -0
  130. package/src/lib/json-parser.ts +30 -0
  131. package/src/lib/string.ts +15 -0
  132. package/src/lib/time.ts +74 -0
  133. package/src/llm/ChunkHandler.ts +277 -0
  134. package/src/llm/FinishHandler.ts +250 -0
  135. package/src/llm/LLMConfigEditor.ts +154 -0
  136. package/src/llm/LLMServiceFactory.ts +230 -0
  137. package/src/llm/MessageProcessor.ts +90 -0
  138. package/src/llm/RecordingState.ts +37 -0
  139. package/src/llm/StreamPublisher.ts +40 -0
  140. package/src/llm/TracingUtils.ts +77 -0
  141. package/src/llm/chunk-validators.ts +57 -0
  142. package/src/llm/constants.ts +6 -0
  143. package/src/llm/index.ts +12 -0
  144. package/src/llm/meta/MetaModelResolver.ts +352 -0
  145. package/src/llm/meta/index.ts +11 -0
  146. package/src/llm/middleware/flight-recorder.ts +188 -0
  147. package/src/llm/providers/MockProvider.ts +332 -0
  148. package/src/llm/providers/agent/ClaudeCodeProvider.ts +343 -0
  149. package/src/llm/providers/agent/ClaudeCodeToolsAdapter.ts +203 -0
  150. package/src/llm/providers/agent/CodexAppServerProvider.ts +214 -0
  151. package/src/llm/providers/agent/CodexAppServerToolsAdapter.ts +91 -0
  152. package/src/llm/providers/agent/index.ts +10 -0
  153. package/src/llm/providers/base/AgentProvider.ts +107 -0
  154. package/src/llm/providers/base/BaseProvider.ts +114 -0
  155. package/src/llm/providers/base/StandardProvider.ts +38 -0
  156. package/src/llm/providers/base/index.ts +9 -0
  157. package/src/llm/providers/index.ts +106 -0
  158. package/src/llm/providers/key-manager.ts +238 -0
  159. package/src/llm/providers/ollama-models.ts +105 -0
  160. package/src/llm/providers/openrouter-models.ts +102 -0
  161. package/src/llm/providers/provider-ids.ts +18 -0
  162. package/src/llm/providers/registry/ProviderRegistry.ts +414 -0
  163. package/src/llm/providers/registry/index.ts +7 -0
  164. package/src/llm/providers/standard/AnthropicProvider.ts +71 -0
  165. package/src/llm/providers/standard/OllamaProvider.ts +59 -0
  166. package/src/llm/providers/standard/OpenAIProvider.ts +44 -0
  167. package/src/llm/providers/standard/OpenRouterProvider.ts +103 -0
  168. package/src/llm/providers/standard/index.ts +10 -0
  169. package/src/llm/providers/types.ts +194 -0
  170. package/src/llm/providers/usage-metadata.ts +78 -0
  171. package/src/llm/service.ts +713 -0
  172. package/src/llm/types.ts +167 -0
  173. package/src/llm/utils/ConfigurationManager.ts +650 -0
  174. package/src/llm/utils/ConfigurationTester.ts +229 -0
  175. package/src/llm/utils/ModelSelector.ts +212 -0
  176. package/src/llm/utils/ProviderConfigUI.ts +177 -0
  177. package/src/llm/utils/claudeCodePromptCompiler.ts +141 -0
  178. package/src/llm/utils/codex-models.ts +53 -0
  179. package/src/llm/utils/context-window-cache.ts +30 -0
  180. package/src/llm/utils/models-dev-cache.ts +267 -0
  181. package/src/llm/utils/provider-setup.ts +50 -0
  182. package/src/llm/utils/tool-errors.ts +78 -0
  183. package/src/llm/utils/usage.ts +74 -0
  184. package/src/logging/EventRoutingLogger.ts +205 -0
  185. package/src/nostr/AgentEventDecoder.ts +357 -0
  186. package/src/nostr/AgentEventEncoder.ts +677 -0
  187. package/src/nostr/AgentProfilePublisher.ts +657 -0
  188. package/src/nostr/AgentPublisher.ts +437 -0
  189. package/src/nostr/BlossomService.ts +226 -0
  190. package/src/nostr/InterventionPublisher.ts +132 -0
  191. package/src/nostr/TagExtractor.ts +228 -0
  192. package/src/nostr/collectEvents.ts +83 -0
  193. package/src/nostr/constants.ts +38 -0
  194. package/src/nostr/encryption.ts +26 -0
  195. package/src/nostr/index.ts +31 -0
  196. package/src/nostr/keys.ts +17 -0
  197. package/src/nostr/kinds.ts +37 -0
  198. package/src/nostr/ndkClient.ts +72 -0
  199. package/src/nostr/relays.ts +43 -0
  200. package/src/nostr/trace-context.ts +39 -0
  201. package/src/nostr/types.ts +227 -0
  202. package/src/nostr/utils.ts +84 -0
  203. package/src/prompts/core/FragmentRegistry.ts +30 -0
  204. package/src/prompts/core/PromptBuilder.ts +98 -0
  205. package/src/prompts/core/index.ts +3 -0
  206. package/src/prompts/core/types.ts +13 -0
  207. package/src/prompts/fragments/00-global-system-prompt.ts +44 -0
  208. package/src/prompts/fragments/01-agent-identity.ts +69 -0
  209. package/src/prompts/fragments/02-agent-home-directory.ts +114 -0
  210. package/src/prompts/fragments/03-system-reminders-explanation.ts +14 -0
  211. package/src/prompts/fragments/04-relay-configuration.ts +38 -0
  212. package/src/prompts/fragments/05-delegation-chain.ts +45 -0
  213. package/src/prompts/fragments/06-agent-todos.ts +74 -0
  214. package/src/prompts/fragments/06-todo-usage-guidance.ts +34 -0
  215. package/src/prompts/fragments/07-meta-project-context.ts +234 -0
  216. package/src/prompts/fragments/08-active-conversations.ts +382 -0
  217. package/src/prompts/fragments/09-recent-conversations.ts +153 -0
  218. package/src/prompts/fragments/10-referenced-article.ts +21 -0
  219. package/src/prompts/fragments/11-nudges.ts +134 -0
  220. package/src/prompts/fragments/12-skills.ts +127 -0
  221. package/src/prompts/fragments/13-available-nudges.ts +122 -0
  222. package/src/prompts/fragments/15-available-agents.ts +53 -0
  223. package/src/prompts/fragments/16-stay-in-your-lane.ts +41 -0
  224. package/src/prompts/fragments/17-todo-before-delegation.ts +39 -0
  225. package/src/prompts/fragments/20-voice-mode.ts +62 -0
  226. package/src/prompts/fragments/22-scheduled-tasks.ts +175 -0
  227. package/src/prompts/fragments/24-retrieved-lessons.ts +26 -0
  228. package/src/prompts/fragments/25-rag-instructions.ts +333 -0
  229. package/src/prompts/fragments/26-mcp-resources.ts +237 -0
  230. package/src/prompts/fragments/27-memorized-reports.ts +77 -0
  231. package/src/prompts/fragments/28-agent-directed-monitoring.ts +32 -0
  232. package/src/prompts/fragments/29-rag-collections.ts +50 -0
  233. package/src/prompts/fragments/30-worktree-context.ts +98 -0
  234. package/src/prompts/fragments/31-agents-md-guidance.ts +96 -0
  235. package/src/prompts/fragments/32-process-metrics.ts +72 -0
  236. package/src/prompts/fragments/debug-mode.ts +48 -0
  237. package/src/prompts/fragments/delegation-completion.ts +44 -0
  238. package/src/prompts/fragments/index.ts +91 -0
  239. package/src/prompts/index.ts +21 -0
  240. package/src/prompts/utils/systemPromptBuilder.ts +777 -0
  241. package/src/scripts/migrate-prefix-index.ts +157 -0
  242. package/src/services/AgentDefinitionMonitor.ts +701 -0
  243. package/src/services/ConfigService.ts +723 -0
  244. package/src/services/CooldownRegistry.ts +199 -0
  245. package/src/services/LLMOperationsRegistry.ts +424 -0
  246. package/src/services/OwnerAgentListService.ts +354 -0
  247. package/src/services/PubkeyService.ts +308 -0
  248. package/src/services/agents/AgentMetadataStore.ts +72 -0
  249. package/src/services/agents/AgentResolution.ts +59 -0
  250. package/src/services/agents/EscalationService.ts +281 -0
  251. package/src/services/agents/NDKAgentDiscovery.ts +95 -0
  252. package/src/services/agents/index.ts +7 -0
  253. package/src/services/agents-md/AgentsMdService.ts +184 -0
  254. package/src/services/agents-md/SystemReminderInjector.ts +238 -0
  255. package/src/services/agents-md/index.ts +11 -0
  256. package/src/services/apns/APNsClient.ts +203 -0
  257. package/src/services/apns/APNsService.ts +358 -0
  258. package/src/services/apns/index.ts +11 -0
  259. package/src/services/apns/types.ts +80 -0
  260. package/src/services/compression/CompressionService.ts +445 -0
  261. package/src/services/compression/compression-schema.ts +28 -0
  262. package/src/services/compression/compression-types.ts +74 -0
  263. package/src/services/compression/compression-utils.ts +587 -0
  264. package/src/services/config/types.ts +394 -0
  265. package/src/services/dispatch/AgentDispatchService.ts +937 -0
  266. package/src/services/dispatch/AgentRouter.ts +181 -0
  267. package/src/services/dispatch/DelegationCompletionHandler.ts +232 -0
  268. package/src/services/embedding/EmbeddingProvider.ts +188 -0
  269. package/src/services/embedding/index.ts +5 -0
  270. package/src/services/event-context/EventContextService.ts +108 -0
  271. package/src/services/event-context/index.ts +2 -0
  272. package/src/services/heuristics/ContextBuilder.ts +106 -0
  273. package/src/services/heuristics/HeuristicEngine.ts +200 -0
  274. package/src/services/heuristics/formatters.ts +58 -0
  275. package/src/services/heuristics/index.ts +12 -0
  276. package/src/services/heuristics/rules/index.ts +25 -0
  277. package/src/services/heuristics/rules/todoBeforeDelegation.ts +69 -0
  278. package/src/services/heuristics/rules/todoReminderOnToolUse.ts +63 -0
  279. package/src/services/heuristics/types.ts +144 -0
  280. package/src/services/image/ImageGenerationService.ts +389 -0
  281. package/src/services/image/index.ts +12 -0
  282. package/src/services/intervention/InterventionService.ts +1352 -0
  283. package/src/services/intervention/index.ts +7 -0
  284. package/src/services/mcp/MCPManager.ts +683 -0
  285. package/src/services/mcp/McpNotificationDelivery.ts +139 -0
  286. package/src/services/mcp/McpSubscriptionService.ts +653 -0
  287. package/src/services/mcp/mcpInstaller.ts +130 -0
  288. package/src/services/nip46/Nip46SigningLog.ts +81 -0
  289. package/src/services/nip46/Nip46SigningService.ts +467 -0
  290. package/src/services/nip46/index.ts +4 -0
  291. package/src/services/nudge/NudgeService.ts +224 -0
  292. package/src/services/nudge/NudgeWhitelistService.ts +382 -0
  293. package/src/services/nudge/index.ts +5 -0
  294. package/src/services/nudge/types.ts +83 -0
  295. package/src/services/projects/ProjectContext.ts +672 -0
  296. package/src/services/projects/ProjectContextStore.ts +102 -0
  297. package/src/services/projects/index.ts +6 -0
  298. package/src/services/prompt-compiler/index.ts +15 -0
  299. package/src/services/prompt-compiler/prompt-compiler-service.ts +1143 -0
  300. package/src/services/pubkey-gate/PubkeyGateService.ts +93 -0
  301. package/src/services/pubkey-gate/index.ts +1 -0
  302. package/src/services/rag/EmbeddingProviderFactory.ts +292 -0
  303. package/src/services/rag/LanceDBMaintenanceService.ts +211 -0
  304. package/src/services/rag/RAGDatabaseService.ts +173 -0
  305. package/src/services/rag/RAGOperations.ts +682 -0
  306. package/src/services/rag/RAGService.ts +240 -0
  307. package/src/services/rag/RagSubscriptionService.ts +618 -0
  308. package/src/services/rag/rag-utils.ts +174 -0
  309. package/src/services/ral/PendingDelegationsRegistry.ts +168 -0
  310. package/src/services/ral/RALRegistry.ts +2782 -0
  311. package/src/services/ral/index.ts +4 -0
  312. package/src/services/ral/types.ts +292 -0
  313. package/src/services/reports/LocalReportStore.ts +380 -0
  314. package/src/services/reports/ReportEmbeddingService.ts +430 -0
  315. package/src/services/reports/ReportService.ts +440 -0
  316. package/src/services/reports/articleUtils.ts +52 -0
  317. package/src/services/reports/index.ts +7 -0
  318. package/src/services/scheduling/SchedulerService.ts +1057 -0
  319. package/src/services/scheduling/errors.ts +14 -0
  320. package/src/services/scheduling/index.ts +7 -0
  321. package/src/services/scheduling/utils.ts +77 -0
  322. package/src/services/search/SearchProviderRegistry.ts +78 -0
  323. package/src/services/search/UnifiedSearchService.ts +218 -0
  324. package/src/services/search/index.ts +47 -0
  325. package/src/services/search/projectFilter.ts +22 -0
  326. package/src/services/search/providers/ConversationSearchProvider.ts +48 -0
  327. package/src/services/search/providers/LessonSearchProvider.ts +75 -0
  328. package/src/services/search/providers/ReportSearchProvider.ts +49 -0
  329. package/src/services/search/types.ts +144 -0
  330. package/src/services/skill/SkillService.ts +482 -0
  331. package/src/services/skill/index.ts +2 -0
  332. package/src/services/skill/types.ts +70 -0
  333. package/src/services/status/OperationsStatusService.ts +276 -0
  334. package/src/services/status/ProjectStatusService.ts +522 -0
  335. package/src/services/status/index.ts +11 -0
  336. package/src/services/storage/PrefixKVStore.ts +242 -0
  337. package/src/services/storage/index.ts +1 -0
  338. package/src/services/system-reminder/SystemReminderUtils.ts +96 -0
  339. package/src/services/system-reminder/index.ts +7 -0
  340. package/src/services/trust-pubkeys/TrustPubkeyService.ts +325 -0
  341. package/src/services/trust-pubkeys/index.ts +2 -0
  342. package/src/telemetry/ConversationSpanManager.ts +111 -0
  343. package/src/telemetry/EventLoopMonitor.ts +206 -0
  344. package/src/telemetry/LLMSpanRegistry.ts +20 -0
  345. package/src/telemetry/NostrSpanProcessor.ts +89 -0
  346. package/src/telemetry/ToolCallSpanProcessor.ts +66 -0
  347. package/src/telemetry/diagnostics.ts +27 -0
  348. package/src/telemetry/setup.ts +120 -0
  349. package/src/tools/implementations/agents_discover.ts +121 -0
  350. package/src/tools/implementations/agents_hire.ts +127 -0
  351. package/src/tools/implementations/agents_list.ts +96 -0
  352. package/src/tools/implementations/agents_publish.ts +611 -0
  353. package/src/tools/implementations/agents_read.ts +173 -0
  354. package/src/tools/implementations/agents_write.ts +200 -0
  355. package/src/tools/implementations/ask.ts +411 -0
  356. package/src/tools/implementations/change_model.ts +141 -0
  357. package/src/tools/implementations/conversation_get.ts +661 -0
  358. package/src/tools/implementations/conversation_list.ts +377 -0
  359. package/src/tools/implementations/conversation_search.ts +370 -0
  360. package/src/tools/implementations/delegate.ts +327 -0
  361. package/src/tools/implementations/delegate_crossproject.ts +209 -0
  362. package/src/tools/implementations/delegate_followup.ts +300 -0
  363. package/src/tools/implementations/fs_edit.ts +162 -0
  364. package/src/tools/implementations/fs_glob.ts +182 -0
  365. package/src/tools/implementations/fs_grep.ts +513 -0
  366. package/src/tools/implementations/fs_read.ts +332 -0
  367. package/src/tools/implementations/fs_write.ts +113 -0
  368. package/src/tools/implementations/generate_image.ts +259 -0
  369. package/src/tools/implementations/home_fs.ts +515 -0
  370. package/src/tools/implementations/kill.ts +651 -0
  371. package/src/tools/implementations/learn.ts +166 -0
  372. package/src/tools/implementations/lesson-formatter.ts +38 -0
  373. package/src/tools/implementations/lesson_delete.ts +164 -0
  374. package/src/tools/implementations/lesson_get.ts +105 -0
  375. package/src/tools/implementations/lessons_list.ts +153 -0
  376. package/src/tools/implementations/mcp_resource_read.ts +161 -0
  377. package/src/tools/implementations/mcp_subscribe.ts +158 -0
  378. package/src/tools/implementations/mcp_subscription_stop.ts +85 -0
  379. package/src/tools/implementations/nostr_fetch.ts +149 -0
  380. package/src/tools/implementations/nostr_publish_as_user.ts +353 -0
  381. package/src/tools/implementations/project_list.ts +146 -0
  382. package/src/tools/implementations/rag_add_documents.ts +573 -0
  383. package/src/tools/implementations/rag_create_collection.ts +65 -0
  384. package/src/tools/implementations/rag_delete_collection.ts +68 -0
  385. package/src/tools/implementations/rag_list_collections.ts +77 -0
  386. package/src/tools/implementations/rag_query.ts +107 -0
  387. package/src/tools/implementations/rag_subscription_create.ts +105 -0
  388. package/src/tools/implementations/rag_subscription_delete.ts +80 -0
  389. package/src/tools/implementations/rag_subscription_get.ts +123 -0
  390. package/src/tools/implementations/rag_subscription_list.ts +128 -0
  391. package/src/tools/implementations/report_delete.ts +79 -0
  392. package/src/tools/implementations/report_read.ts +160 -0
  393. package/src/tools/implementations/report_write.ts +278 -0
  394. package/src/tools/implementations/reports_list.ts +77 -0
  395. package/src/tools/implementations/schedule_task.ts +104 -0
  396. package/src/tools/implementations/schedule_task_cancel.ts +62 -0
  397. package/src/tools/implementations/schedule_task_once.ts +128 -0
  398. package/src/tools/implementations/schedule_tasks_list.ts +79 -0
  399. package/src/tools/implementations/search.ts +160 -0
  400. package/src/tools/implementations/shell.ts +553 -0
  401. package/src/tools/implementations/todo.ts +260 -0
  402. package/src/tools/implementations/upload_blob.ts +381 -0
  403. package/src/tools/implementations/web_fetch.ts +153 -0
  404. package/src/tools/implementations/web_search.ts +250 -0
  405. package/src/tools/registry.ts +670 -0
  406. package/src/tools/types.ts +177 -0
  407. package/src/tools/utils.ts +256 -0
  408. package/src/types/event-ids.ts +320 -0
  409. package/src/types/index.ts +46 -0
  410. package/src/utils/agentFetcher.ts +107 -0
  411. package/src/utils/cli-error.ts +29 -0
  412. package/src/utils/conversation-id.ts +27 -0
  413. package/src/utils/conversation-utils.ts +1 -0
  414. package/src/utils/delegation-chain.ts +357 -0
  415. package/src/utils/error-handler.ts +42 -0
  416. package/src/utils/git/gitignore.ts +69 -0
  417. package/src/utils/git/index.ts +2 -0
  418. package/src/utils/git/initializeGitRepo.ts +204 -0
  419. package/src/utils/git/worktree.ts +260 -0
  420. package/src/utils/lessonFormatter.ts +70 -0
  421. package/src/utils/lessonTrust.ts +24 -0
  422. package/src/utils/lockfile.ts +123 -0
  423. package/src/utils/logger.ts +149 -0
  424. package/src/utils/nostr-entity-parser.ts +365 -0
  425. package/src/utils/process.ts +49 -0
  426. package/src/wrapper.ts +262 -0
  427. package/tsconfig.json +41 -0
@@ -0,0 +1,4 @@
1
+ export { RALRegistry } from "./RALRegistry";
2
+ export type { RALRegistryEvents } from "./RALRegistry";
3
+ export { PendingDelegationsRegistry } from "./PendingDelegationsRegistry";
4
+ export * from "./types";
@@ -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
+ }