calvyn-code 0.14.0

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 (1718) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -0
  3. package/README.zh-CN.md +180 -0
  4. package/acp_adapter/__init__.py +1 -0
  5. package/acp_adapter/__main__.py +5 -0
  6. package/acp_adapter/auth.py +68 -0
  7. package/acp_adapter/bootstrap/__init__.py +0 -0
  8. package/acp_adapter/bootstrap/bootstrap_browser_tools.ps1 +288 -0
  9. package/acp_adapter/bootstrap/bootstrap_browser_tools.sh +399 -0
  10. package/acp_adapter/entry.py +292 -0
  11. package/acp_adapter/events.py +265 -0
  12. package/acp_adapter/permissions.py +148 -0
  13. package/acp_adapter/server.py +1713 -0
  14. package/acp_adapter/session.py +629 -0
  15. package/acp_adapter/tools.py +1180 -0
  16. package/agent/__init__.py +6 -0
  17. package/agent/__pycache__/__init__.cpython-312.pyc +0 -0
  18. package/agent/__pycache__/account_usage.cpython-312.pyc +0 -0
  19. package/agent/__pycache__/anthropic_adapter.cpython-312.pyc +0 -0
  20. package/agent/__pycache__/async_utils.cpython-312.pyc +0 -0
  21. package/agent/__pycache__/auxiliary_client.cpython-312.pyc +0 -0
  22. package/agent/__pycache__/codex_responses_adapter.cpython-312.pyc +0 -0
  23. package/agent/__pycache__/context_compressor.cpython-312.pyc +0 -0
  24. package/agent/__pycache__/context_engine.cpython-312.pyc +0 -0
  25. package/agent/__pycache__/context_references.cpython-312.pyc +0 -0
  26. package/agent/__pycache__/credential_pool.cpython-312.pyc +0 -0
  27. package/agent/__pycache__/curator.cpython-312.pyc +0 -0
  28. package/agent/__pycache__/display.cpython-312.pyc +0 -0
  29. package/agent/__pycache__/error_classifier.cpython-312.pyc +0 -0
  30. package/agent/__pycache__/file_safety.cpython-312.pyc +0 -0
  31. package/agent/__pycache__/google_code_assist.cpython-312.pyc +0 -0
  32. package/agent/__pycache__/google_oauth.cpython-312.pyc +0 -0
  33. package/agent/__pycache__/i18n.cpython-312.pyc +0 -0
  34. package/agent/__pycache__/image_gen_provider.cpython-312.pyc +0 -0
  35. package/agent/__pycache__/image_gen_registry.cpython-312.pyc +0 -0
  36. package/agent/__pycache__/insights.cpython-312.pyc +0 -0
  37. package/agent/__pycache__/lmstudio_reasoning.cpython-312.pyc +0 -0
  38. package/agent/__pycache__/manual_compression_feedback.cpython-312.pyc +0 -0
  39. package/agent/__pycache__/markdown_tables.cpython-312.pyc +0 -0
  40. package/agent/__pycache__/memory_manager.cpython-312.pyc +0 -0
  41. package/agent/__pycache__/memory_provider.cpython-312.pyc +0 -0
  42. package/agent/__pycache__/model_metadata.cpython-312.pyc +0 -0
  43. package/agent/__pycache__/models_dev.cpython-312.pyc +0 -0
  44. package/agent/__pycache__/moonshot_schema.cpython-312.pyc +0 -0
  45. package/agent/__pycache__/onboarding.cpython-312.pyc +0 -0
  46. package/agent/__pycache__/portal_tags.cpython-312.pyc +0 -0
  47. package/agent/__pycache__/prompt_builder.cpython-312.pyc +0 -0
  48. package/agent/__pycache__/prompt_caching.cpython-312.pyc +0 -0
  49. package/agent/__pycache__/redact.cpython-312.pyc +0 -0
  50. package/agent/__pycache__/retry_utils.cpython-312.pyc +0 -0
  51. package/agent/__pycache__/shell_hooks.cpython-312.pyc +0 -0
  52. package/agent/__pycache__/skill_commands.cpython-312.pyc +0 -0
  53. package/agent/__pycache__/skill_preprocessing.cpython-312.pyc +0 -0
  54. package/agent/__pycache__/skill_utils.cpython-312.pyc +0 -0
  55. package/agent/__pycache__/subdirectory_hints.cpython-312.pyc +0 -0
  56. package/agent/__pycache__/think_scrubber.cpython-312.pyc +0 -0
  57. package/agent/__pycache__/title_generator.cpython-312.pyc +0 -0
  58. package/agent/__pycache__/tool_guardrails.cpython-312.pyc +0 -0
  59. package/agent/__pycache__/tool_result_classification.cpython-312.pyc +0 -0
  60. package/agent/__pycache__/trajectory.cpython-312.pyc +0 -0
  61. package/agent/__pycache__/usage_pricing.cpython-312.pyc +0 -0
  62. package/agent/__pycache__/video_gen_provider.cpython-312.pyc +0 -0
  63. package/agent/__pycache__/video_gen_registry.cpython-312.pyc +0 -0
  64. package/agent/__pycache__/web_search_provider.cpython-312.pyc +0 -0
  65. package/agent/__pycache__/web_search_registry.cpython-312.pyc +0 -0
  66. package/agent/account_usage.py +326 -0
  67. package/agent/anthropic_adapter.py +2087 -0
  68. package/agent/async_utils.py +68 -0
  69. package/agent/auxiliary_client.py +4893 -0
  70. package/agent/bedrock_adapter.py +1276 -0
  71. package/agent/codex_responses_adapter.py +1084 -0
  72. package/agent/context_compressor.py +1583 -0
  73. package/agent/context_engine.py +211 -0
  74. package/agent/context_references.py +519 -0
  75. package/agent/copilot_acp_client.py +684 -0
  76. package/agent/credential_pool.py +1780 -0
  77. package/agent/credential_sources.py +449 -0
  78. package/agent/curator.py +1782 -0
  79. package/agent/curator_backup.py +694 -0
  80. package/agent/display.py +987 -0
  81. package/agent/error_classifier.py +1058 -0
  82. package/agent/file_safety.py +112 -0
  83. package/agent/gemini_cloudcode_adapter.py +909 -0
  84. package/agent/gemini_native_adapter.py +971 -0
  85. package/agent/gemini_schema.py +99 -0
  86. package/agent/google_code_assist.py +452 -0
  87. package/agent/google_oauth.py +1062 -0
  88. package/agent/i18n.py +258 -0
  89. package/agent/image_gen_provider.py +243 -0
  90. package/agent/image_gen_registry.py +145 -0
  91. package/agent/image_routing.py +301 -0
  92. package/agent/insights.py +931 -0
  93. package/agent/lmstudio_reasoning.py +48 -0
  94. package/agent/lsp/__init__.py +106 -0
  95. package/agent/lsp/__pycache__/__init__.cpython-312.pyc +0 -0
  96. package/agent/lsp/__pycache__/cli.cpython-312.pyc +0 -0
  97. package/agent/lsp/__pycache__/client.cpython-312.pyc +0 -0
  98. package/agent/lsp/__pycache__/eventlog.cpython-312.pyc +0 -0
  99. package/agent/lsp/__pycache__/manager.cpython-312.pyc +0 -0
  100. package/agent/lsp/__pycache__/protocol.cpython-312.pyc +0 -0
  101. package/agent/lsp/__pycache__/servers.cpython-312.pyc +0 -0
  102. package/agent/lsp/__pycache__/workspace.cpython-312.pyc +0 -0
  103. package/agent/lsp/cli.py +308 -0
  104. package/agent/lsp/client.py +930 -0
  105. package/agent/lsp/eventlog.py +213 -0
  106. package/agent/lsp/install.py +376 -0
  107. package/agent/lsp/manager.py +644 -0
  108. package/agent/lsp/protocol.py +196 -0
  109. package/agent/lsp/range_shift.py +149 -0
  110. package/agent/lsp/reporter.py +78 -0
  111. package/agent/lsp/servers.py +1040 -0
  112. package/agent/lsp/workspace.py +223 -0
  113. package/agent/manual_compression_feedback.py +49 -0
  114. package/agent/markdown_tables.py +309 -0
  115. package/agent/memory_manager.py +556 -0
  116. package/agent/memory_provider.py +279 -0
  117. package/agent/model_metadata.py +1827 -0
  118. package/agent/models_dev.py +724 -0
  119. package/agent/moonshot_schema.py +231 -0
  120. package/agent/nous_rate_guard.py +326 -0
  121. package/agent/onboarding.py +193 -0
  122. package/agent/plugin_llm.py +1046 -0
  123. package/agent/portal_tags.py +64 -0
  124. package/agent/prompt_builder.py +1457 -0
  125. package/agent/prompt_caching.py +79 -0
  126. package/agent/rate_limit_tracker.py +246 -0
  127. package/agent/redact.py +403 -0
  128. package/agent/retry_utils.py +57 -0
  129. package/agent/shell_hooks.py +837 -0
  130. package/agent/skill_commands.py +502 -0
  131. package/agent/skill_preprocessing.py +131 -0
  132. package/agent/skill_utils.py +512 -0
  133. package/agent/subdirectory_hints.py +224 -0
  134. package/agent/think_scrubber.py +386 -0
  135. package/agent/title_generator.py +171 -0
  136. package/agent/tool_guardrails.py +458 -0
  137. package/agent/tool_result_classification.py +26 -0
  138. package/agent/trajectory.py +56 -0
  139. package/agent/transports/__init__.py +68 -0
  140. package/agent/transports/__pycache__/__init__.cpython-312.pyc +0 -0
  141. package/agent/transports/__pycache__/anthropic.cpython-312.pyc +0 -0
  142. package/agent/transports/__pycache__/base.cpython-312.pyc +0 -0
  143. package/agent/transports/__pycache__/bedrock.cpython-312.pyc +0 -0
  144. package/agent/transports/__pycache__/chat_completions.cpython-312.pyc +0 -0
  145. package/agent/transports/__pycache__/codex.cpython-312.pyc +0 -0
  146. package/agent/transports/__pycache__/types.cpython-312.pyc +0 -0
  147. package/agent/transports/anthropic.py +179 -0
  148. package/agent/transports/base.py +89 -0
  149. package/agent/transports/bedrock.py +154 -0
  150. package/agent/transports/chat_completions.py +614 -0
  151. package/agent/transports/codex.py +283 -0
  152. package/agent/transports/codex_app_server.py +368 -0
  153. package/agent/transports/codex_app_server_session.py +810 -0
  154. package/agent/transports/codex_event_projector.py +312 -0
  155. package/agent/transports/hermes_tools_mcp_server.py +233 -0
  156. package/agent/transports/types.py +162 -0
  157. package/agent/usage_pricing.py +877 -0
  158. package/agent/video_gen_provider.py +300 -0
  159. package/agent/video_gen_registry.py +117 -0
  160. package/agent/web_search_provider.py +221 -0
  161. package/agent/web_search_registry.py +262 -0
  162. package/assets/banner.png +0 -0
  163. package/batch_runner.py +1303 -0
  164. package/bin/calvyn.js +67 -0
  165. package/calvyn_bootstrap.py +130 -0
  166. package/calvyn_constants.py +346 -0
  167. package/calvyn_logging.py +390 -0
  168. package/calvyn_state.py +2967 -0
  169. package/calvyn_time.py +105 -0
  170. package/cli.py +14160 -0
  171. package/cron/__init__.py +42 -0
  172. package/cron/__pycache__/__init__.cpython-312.pyc +0 -0
  173. package/cron/__pycache__/jobs.cpython-312.pyc +0 -0
  174. package/cron/__pycache__/scheduler.cpython-312.pyc +0 -0
  175. package/cron/jobs.py +1160 -0
  176. package/cron/scheduler.py +1832 -0
  177. package/gateway/__init__.py +35 -0
  178. package/gateway/__pycache__/__init__.cpython-312.pyc +0 -0
  179. package/gateway/__pycache__/channel_directory.cpython-312.pyc +0 -0
  180. package/gateway/__pycache__/config.cpython-312.pyc +0 -0
  181. package/gateway/__pycache__/delivery.cpython-312.pyc +0 -0
  182. package/gateway/__pycache__/display_config.cpython-312.pyc +0 -0
  183. package/gateway/__pycache__/hooks.cpython-312.pyc +0 -0
  184. package/gateway/__pycache__/pairing.cpython-312.pyc +0 -0
  185. package/gateway/__pycache__/platform_registry.cpython-312.pyc +0 -0
  186. package/gateway/__pycache__/restart.cpython-312.pyc +0 -0
  187. package/gateway/__pycache__/run.cpython-312.pyc +0 -0
  188. package/gateway/__pycache__/runtime_footer.cpython-312.pyc +0 -0
  189. package/gateway/__pycache__/session.cpython-312.pyc +0 -0
  190. package/gateway/__pycache__/session_context.cpython-312.pyc +0 -0
  191. package/gateway/__pycache__/shutdown_forensics.cpython-312.pyc +0 -0
  192. package/gateway/__pycache__/slash_access.cpython-312.pyc +0 -0
  193. package/gateway/__pycache__/status.cpython-312.pyc +0 -0
  194. package/gateway/__pycache__/stream_consumer.cpython-312.pyc +0 -0
  195. package/gateway/__pycache__/whatsapp_identity.cpython-312.pyc +0 -0
  196. package/gateway/assets/telegram-botfather-threads-settings.jpg +0 -0
  197. package/gateway/builtin_hooks/__init__.py +1 -0
  198. package/gateway/channel_directory.py +357 -0
  199. package/gateway/config.py +1873 -0
  200. package/gateway/delivery.py +258 -0
  201. package/gateway/display_config.py +206 -0
  202. package/gateway/hooks.py +210 -0
  203. package/gateway/mirror.py +179 -0
  204. package/gateway/pairing.py +322 -0
  205. package/gateway/platform_registry.py +260 -0
  206. package/gateway/platforms/ADDING_A_PLATFORM.md +374 -0
  207. package/gateway/platforms/__init__.py +45 -0
  208. package/gateway/platforms/__pycache__/__init__.cpython-312.pyc +0 -0
  209. package/gateway/platforms/__pycache__/base.cpython-312.pyc +0 -0
  210. package/gateway/platforms/__pycache__/helpers.cpython-312.pyc +0 -0
  211. package/gateway/platforms/__pycache__/telegram.cpython-312.pyc +0 -0
  212. package/gateway/platforms/__pycache__/telegram_network.cpython-312.pyc +0 -0
  213. package/gateway/platforms/__pycache__/yuanbao.cpython-312.pyc +0 -0
  214. package/gateway/platforms/__pycache__/yuanbao_media.cpython-312.pyc +0 -0
  215. package/gateway/platforms/__pycache__/yuanbao_proto.cpython-312.pyc +0 -0
  216. package/gateway/platforms/_http_client_limits.py +84 -0
  217. package/gateway/platforms/api_server.py +3488 -0
  218. package/gateway/platforms/base.py +3747 -0
  219. package/gateway/platforms/bluebubbles.py +937 -0
  220. package/gateway/platforms/dingtalk.py +1473 -0
  221. package/gateway/platforms/discord.py +5584 -0
  222. package/gateway/platforms/email.py +773 -0
  223. package/gateway/platforms/feishu.py +5059 -0
  224. package/gateway/platforms/feishu_comment.py +1382 -0
  225. package/gateway/platforms/feishu_comment_rules.py +430 -0
  226. package/gateway/platforms/helpers.py +279 -0
  227. package/gateway/platforms/homeassistant.py +449 -0
  228. package/gateway/platforms/matrix.py +2777 -0
  229. package/gateway/platforms/mattermost.py +852 -0
  230. package/gateway/platforms/msgraph_webhook.py +397 -0
  231. package/gateway/platforms/qqbot/__init__.py +91 -0
  232. package/gateway/platforms/qqbot/adapter.py +3072 -0
  233. package/gateway/platforms/qqbot/chunked_upload.py +602 -0
  234. package/gateway/platforms/qqbot/constants.py +74 -0
  235. package/gateway/platforms/qqbot/crypto.py +45 -0
  236. package/gateway/platforms/qqbot/keyboards.py +473 -0
  237. package/gateway/platforms/qqbot/onboard.py +220 -0
  238. package/gateway/platforms/qqbot/utils.py +71 -0
  239. package/gateway/platforms/signal.py +1518 -0
  240. package/gateway/platforms/signal_rate_limit.py +369 -0
  241. package/gateway/platforms/slack.py +3028 -0
  242. package/gateway/platforms/sms.py +377 -0
  243. package/gateway/platforms/telegram.py +4836 -0
  244. package/gateway/platforms/telegram_network.py +249 -0
  245. package/gateway/platforms/webhook.py +806 -0
  246. package/gateway/platforms/wecom.py +1610 -0
  247. package/gateway/platforms/wecom_callback.py +403 -0
  248. package/gateway/platforms/wecom_crypto.py +142 -0
  249. package/gateway/platforms/weixin.py +2170 -0
  250. package/gateway/platforms/whatsapp.py +1283 -0
  251. package/gateway/platforms/yuanbao.py +4873 -0
  252. package/gateway/platforms/yuanbao_media.py +645 -0
  253. package/gateway/platforms/yuanbao_proto.py +1209 -0
  254. package/gateway/platforms/yuanbao_sticker.py +558 -0
  255. package/gateway/restart.py +20 -0
  256. package/gateway/run.py +17074 -0
  257. package/gateway/runtime_footer.py +150 -0
  258. package/gateway/session.py +1399 -0
  259. package/gateway/session_context.py +156 -0
  260. package/gateway/shutdown_forensics.py +462 -0
  261. package/gateway/slash_access.py +229 -0
  262. package/gateway/status.py +972 -0
  263. package/gateway/sticker_cache.py +111 -0
  264. package/gateway/stream_consumer.py +1286 -0
  265. package/gateway/whatsapp_identity.py +156 -0
  266. package/hermes_cli/__init__.py +47 -0
  267. package/hermes_cli/__pycache__/__init__.cpython-312.pyc +0 -0
  268. package/hermes_cli/__pycache__/_parser.cpython-312.pyc +0 -0
  269. package/hermes_cli/__pycache__/auth.cpython-312.pyc +0 -0
  270. package/hermes_cli/__pycache__/banner.cpython-312.pyc +0 -0
  271. package/hermes_cli/__pycache__/browser_connect.cpython-312.pyc +0 -0
  272. package/hermes_cli/__pycache__/callbacks.cpython-312.pyc +0 -0
  273. package/hermes_cli/__pycache__/checkpoints.cpython-312.pyc +0 -0
  274. package/hermes_cli/__pycache__/cli_output.cpython-312.pyc +0 -0
  275. package/hermes_cli/__pycache__/codex_models.cpython-312.pyc +0 -0
  276. package/hermes_cli/__pycache__/codex_runtime_switch.cpython-312.pyc +0 -0
  277. package/hermes_cli/__pycache__/colors.cpython-312.pyc +0 -0
  278. package/hermes_cli/__pycache__/commands.cpython-312.pyc +0 -0
  279. package/hermes_cli/__pycache__/config.cpython-312.pyc +0 -0
  280. package/hermes_cli/__pycache__/copilot_auth.cpython-312.pyc +0 -0
  281. package/hermes_cli/__pycache__/curator.cpython-312.pyc +0 -0
  282. package/hermes_cli/__pycache__/curses_ui.cpython-312.pyc +0 -0
  283. package/hermes_cli/__pycache__/debug.cpython-312.pyc +0 -0
  284. package/hermes_cli/__pycache__/default_soul.cpython-312.pyc +0 -0
  285. package/hermes_cli/__pycache__/env_loader.cpython-312.pyc +0 -0
  286. package/hermes_cli/__pycache__/fallback_cmd.cpython-312.pyc +0 -0
  287. package/hermes_cli/__pycache__/gateway.cpython-312.pyc +0 -0
  288. package/hermes_cli/__pycache__/gateway_windows.cpython-312.pyc +0 -0
  289. package/hermes_cli/__pycache__/goals.cpython-312.pyc +0 -0
  290. package/hermes_cli/__pycache__/inventory.cpython-312.pyc +0 -0
  291. package/hermes_cli/__pycache__/kanban.cpython-312.pyc +0 -0
  292. package/hermes_cli/__pycache__/kanban_db.cpython-312.pyc +0 -0
  293. package/hermes_cli/__pycache__/main.cpython-312.pyc +0 -0
  294. package/hermes_cli/__pycache__/model_catalog.cpython-312.pyc +0 -0
  295. package/hermes_cli/__pycache__/model_normalize.cpython-312.pyc +0 -0
  296. package/hermes_cli/__pycache__/model_switch.cpython-312.pyc +0 -0
  297. package/hermes_cli/__pycache__/models.cpython-312.pyc +0 -0
  298. package/hermes_cli/__pycache__/nous_subscription.cpython-312.pyc +0 -0
  299. package/hermes_cli/__pycache__/pairing.cpython-312.pyc +0 -0
  300. package/hermes_cli/__pycache__/platforms.cpython-312.pyc +0 -0
  301. package/hermes_cli/__pycache__/plugins.cpython-312.pyc +0 -0
  302. package/hermes_cli/__pycache__/profiles.cpython-312.pyc +0 -0
  303. package/hermes_cli/__pycache__/providers.cpython-312.pyc +0 -0
  304. package/hermes_cli/__pycache__/pt_input_extras.cpython-312.pyc +0 -0
  305. package/hermes_cli/__pycache__/runtime_provider.cpython-312.pyc +0 -0
  306. package/hermes_cli/__pycache__/security_advisories.cpython-312.pyc +0 -0
  307. package/hermes_cli/__pycache__/setup.cpython-312.pyc +0 -0
  308. package/hermes_cli/__pycache__/skills_hub.cpython-312.pyc +0 -0
  309. package/hermes_cli/__pycache__/skin_engine.cpython-312.pyc +0 -0
  310. package/hermes_cli/__pycache__/stdio.cpython-312.pyc +0 -0
  311. package/hermes_cli/__pycache__/timeouts.cpython-312.pyc +0 -0
  312. package/hermes_cli/__pycache__/tips.cpython-312.pyc +0 -0
  313. package/hermes_cli/__pycache__/tools_config.cpython-312.pyc +0 -0
  314. package/hermes_cli/__pycache__/voice.cpython-312.pyc +0 -0
  315. package/hermes_cli/_parser.py +365 -0
  316. package/hermes_cli/_subprocess_compat.py +175 -0
  317. package/hermes_cli/auth.py +6299 -0
  318. package/hermes_cli/auth_commands.py +749 -0
  319. package/hermes_cli/azure_detect.py +300 -0
  320. package/hermes_cli/backup.py +938 -0
  321. package/hermes_cli/banner.py +703 -0
  322. package/hermes_cli/browser_connect.py +139 -0
  323. package/hermes_cli/callbacks.py +243 -0
  324. package/hermes_cli/checkpoints.py +244 -0
  325. package/hermes_cli/claw.py +810 -0
  326. package/hermes_cli/cli_output.py +78 -0
  327. package/hermes_cli/clipboard.py +495 -0
  328. package/hermes_cli/codex_models.py +198 -0
  329. package/hermes_cli/codex_runtime_plugin_migration.py +757 -0
  330. package/hermes_cli/codex_runtime_switch.py +266 -0
  331. package/hermes_cli/colors.py +38 -0
  332. package/hermes_cli/commands.py +1728 -0
  333. package/hermes_cli/completion.py +315 -0
  334. package/hermes_cli/config.py +5382 -0
  335. package/hermes_cli/copilot_auth.py +392 -0
  336. package/hermes_cli/cron.py +313 -0
  337. package/hermes_cli/curator.py +598 -0
  338. package/hermes_cli/curses_ui.py +472 -0
  339. package/hermes_cli/debug.py +747 -0
  340. package/hermes_cli/default_soul.py +11 -0
  341. package/hermes_cli/dep_ensure.py +107 -0
  342. package/hermes_cli/dingtalk_auth.py +293 -0
  343. package/hermes_cli/doctor.py +1863 -0
  344. package/hermes_cli/dump.py +326 -0
  345. package/hermes_cli/env_loader.py +175 -0
  346. package/hermes_cli/fallback_cmd.py +361 -0
  347. package/hermes_cli/gateway.py +5422 -0
  348. package/hermes_cli/gateway_windows.py +692 -0
  349. package/hermes_cli/goals.py +757 -0
  350. package/hermes_cli/hooks.py +385 -0
  351. package/hermes_cli/inventory.py +240 -0
  352. package/hermes_cli/kanban.py +2252 -0
  353. package/hermes_cli/kanban_db.py +4840 -0
  354. package/hermes_cli/kanban_diagnostics.py +776 -0
  355. package/hermes_cli/kanban_specify.py +266 -0
  356. package/hermes_cli/logs.py +391 -0
  357. package/hermes_cli/main.py +12396 -0
  358. package/hermes_cli/mcp_config.py +781 -0
  359. package/hermes_cli/memory_setup.py +465 -0
  360. package/hermes_cli/model_catalog.py +330 -0
  361. package/hermes_cli/model_normalize.py +473 -0
  362. package/hermes_cli/model_switch.py +1777 -0
  363. package/hermes_cli/models.py +3789 -0
  364. package/hermes_cli/nous_subscription.py +799 -0
  365. package/hermes_cli/oneshot.py +351 -0
  366. package/hermes_cli/pairing.py +115 -0
  367. package/hermes_cli/platforms.py +83 -0
  368. package/hermes_cli/plugins.py +1562 -0
  369. package/hermes_cli/plugins_cmd.py +1587 -0
  370. package/hermes_cli/profile_distribution.py +703 -0
  371. package/hermes_cli/profiles.py +1319 -0
  372. package/hermes_cli/providers.py +720 -0
  373. package/hermes_cli/proxy/__init__.py +20 -0
  374. package/hermes_cli/proxy/adapters/__init__.py +35 -0
  375. package/hermes_cli/proxy/adapters/base.py +94 -0
  376. package/hermes_cli/proxy/adapters/nous_portal.py +137 -0
  377. package/hermes_cli/proxy/cli.py +141 -0
  378. package/hermes_cli/proxy/server.py +265 -0
  379. package/hermes_cli/pt_input_extras.py +83 -0
  380. package/hermes_cli/pty_bridge.py +237 -0
  381. package/hermes_cli/relaunch.py +205 -0
  382. package/hermes_cli/runtime_provider.py +1428 -0
  383. package/hermes_cli/security_advisories.py +452 -0
  384. package/hermes_cli/setup.py +3559 -0
  385. package/hermes_cli/skills_config.py +177 -0
  386. package/hermes_cli/skills_hub.py +1595 -0
  387. package/hermes_cli/skin_engine.py +929 -0
  388. package/hermes_cli/slack_cli.py +160 -0
  389. package/hermes_cli/status.py +550 -0
  390. package/hermes_cli/stdio.py +252 -0
  391. package/hermes_cli/timeouts.py +82 -0
  392. package/hermes_cli/tips.py +487 -0
  393. package/hermes_cli/tools_config.py +3151 -0
  394. package/hermes_cli/uninstall.py +681 -0
  395. package/hermes_cli/vercel_auth.py +70 -0
  396. package/hermes_cli/voice.py +846 -0
  397. package/hermes_cli/web_server.py +4438 -0
  398. package/hermes_cli/webhook.py +275 -0
  399. package/locales/af.yaml +350 -0
  400. package/locales/de.yaml +350 -0
  401. package/locales/en.yaml +365 -0
  402. package/locales/es.yaml +350 -0
  403. package/locales/fr.yaml +350 -0
  404. package/locales/ga.yaml +354 -0
  405. package/locales/hu.yaml +350 -0
  406. package/locales/it.yaml +350 -0
  407. package/locales/ja.yaml +350 -0
  408. package/locales/ko.yaml +350 -0
  409. package/locales/pt.yaml +350 -0
  410. package/locales/ru.yaml +350 -0
  411. package/locales/tr.yaml +350 -0
  412. package/locales/uk.yaml +350 -0
  413. package/locales/zh-hant.yaml +350 -0
  414. package/locales/zh.yaml +350 -0
  415. package/mcp_serve.py +898 -0
  416. package/model_tools.py +899 -0
  417. package/optional-skills/DESCRIPTION.md +24 -0
  418. package/optional-skills/autonomous-ai-agents/DESCRIPTION.md +2 -0
  419. package/optional-skills/autonomous-ai-agents/blackbox/SKILL.md +144 -0
  420. package/optional-skills/autonomous-ai-agents/honcho/SKILL.md +431 -0
  421. package/optional-skills/blockchain/evm/SKILL.md +211 -0
  422. package/optional-skills/blockchain/evm/scripts/evm_client.py +1508 -0
  423. package/optional-skills/blockchain/hyperliquid/SKILL.md +211 -0
  424. package/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1660 -0
  425. package/optional-skills/blockchain/solana/SKILL.md +208 -0
  426. package/optional-skills/blockchain/solana/scripts/solana_client.py +698 -0
  427. package/optional-skills/communication/DESCRIPTION.md +1 -0
  428. package/optional-skills/communication/one-three-one-rule/SKILL.md +104 -0
  429. package/optional-skills/creative/blender-mcp/SKILL.md +117 -0
  430. package/optional-skills/creative/concept-diagrams/SKILL.md +362 -0
  431. package/optional-skills/creative/concept-diagrams/examples/apartment-floor-plan-conversion.md +244 -0
  432. package/optional-skills/creative/concept-diagrams/examples/automated-password-reset-flow.md +276 -0
  433. package/optional-skills/creative/concept-diagrams/examples/autonomous-llm-research-agent-flow.md +240 -0
  434. package/optional-skills/creative/concept-diagrams/examples/banana-journey-tree-to-smoothie.md +161 -0
  435. package/optional-skills/creative/concept-diagrams/examples/commercial-aircraft-structure.md +209 -0
  436. package/optional-skills/creative/concept-diagrams/examples/cpu-ooo-microarchitecture.md +236 -0
  437. package/optional-skills/creative/concept-diagrams/examples/electricity-grid-flow.md +182 -0
  438. package/optional-skills/creative/concept-diagrams/examples/feature-film-production-pipeline.md +172 -0
  439. package/optional-skills/creative/concept-diagrams/examples/hospital-emergency-department-flow.md +165 -0
  440. package/optional-skills/creative/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md +114 -0
  441. package/optional-skills/creative/concept-diagrams/examples/place-order-uml-sequence.md +325 -0
  442. package/optional-skills/creative/concept-diagrams/examples/smart-city-infrastructure.md +173 -0
  443. package/optional-skills/creative/concept-diagrams/examples/smartphone-layer-anatomy.md +154 -0
  444. package/optional-skills/creative/concept-diagrams/examples/sn2-reaction-mechanism.md +247 -0
  445. package/optional-skills/creative/concept-diagrams/examples/wind-turbine-structure.md +338 -0
  446. package/optional-skills/creative/concept-diagrams/references/dashboard-patterns.md +43 -0
  447. package/optional-skills/creative/concept-diagrams/references/infrastructure-patterns.md +144 -0
  448. package/optional-skills/creative/concept-diagrams/references/physical-shape-cookbook.md +42 -0
  449. package/optional-skills/creative/concept-diagrams/templates/template.html +174 -0
  450. package/optional-skills/creative/hyperframes/SKILL.md +191 -0
  451. package/optional-skills/creative/hyperframes/references/cli.md +185 -0
  452. package/optional-skills/creative/hyperframes/references/composition.md +129 -0
  453. package/optional-skills/creative/hyperframes/references/features.md +289 -0
  454. package/optional-skills/creative/hyperframes/references/gsap.md +136 -0
  455. package/optional-skills/creative/hyperframes/references/troubleshooting.md +137 -0
  456. package/optional-skills/creative/hyperframes/references/website-to-video.md +145 -0
  457. package/optional-skills/creative/hyperframes/scripts/setup.sh +135 -0
  458. package/optional-skills/creative/kanban-video-orchestrator/SKILL.md +207 -0
  459. package/optional-skills/creative/kanban-video-orchestrator/assets/brief.md.tmpl +79 -0
  460. package/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +185 -0
  461. package/optional-skills/creative/kanban-video-orchestrator/assets/soul.md.tmpl +38 -0
  462. package/optional-skills/creative/kanban-video-orchestrator/references/examples.md +227 -0
  463. package/optional-skills/creative/kanban-video-orchestrator/references/intake.md +166 -0
  464. package/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +276 -0
  465. package/optional-skills/creative/kanban-video-orchestrator/references/monitoring.md +180 -0
  466. package/optional-skills/creative/kanban-video-orchestrator/references/role-archetypes.md +298 -0
  467. package/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +317 -0
  468. package/optional-skills/creative/kanban-video-orchestrator/scripts/bootstrap_pipeline.py +501 -0
  469. package/optional-skills/creative/kanban-video-orchestrator/scripts/monitor.py +195 -0
  470. package/optional-skills/creative/meme-generation/EXAMPLES.md +46 -0
  471. package/optional-skills/creative/meme-generation/SKILL.md +130 -0
  472. package/optional-skills/creative/meme-generation/scripts/generate_meme.py +471 -0
  473. package/optional-skills/creative/meme-generation/scripts/templates.json +97 -0
  474. package/optional-skills/devops/cli/SKILL.md +156 -0
  475. package/optional-skills/devops/cli/references/app-discovery.md +112 -0
  476. package/optional-skills/devops/cli/references/authentication.md +59 -0
  477. package/optional-skills/devops/cli/references/cli-reference.md +104 -0
  478. package/optional-skills/devops/cli/references/running-apps.md +171 -0
  479. package/optional-skills/devops/docker-management/SKILL.md +281 -0
  480. package/optional-skills/devops/pinggy-tunnel/SKILL.md +309 -0
  481. package/optional-skills/devops/watchers/SKILL.md +112 -0
  482. package/optional-skills/devops/watchers/scripts/_watermark.py +148 -0
  483. package/optional-skills/devops/watchers/scripts/watch_github.py +168 -0
  484. package/optional-skills/devops/watchers/scripts/watch_http_json.py +131 -0
  485. package/optional-skills/devops/watchers/scripts/watch_rss.py +121 -0
  486. package/optional-skills/dogfood/DESCRIPTION.md +3 -0
  487. package/optional-skills/dogfood/adversarial-ux-test/SKILL.md +191 -0
  488. package/optional-skills/email/agentmail/SKILL.md +126 -0
  489. package/optional-skills/finance/3-statement-model/SKILL.md +433 -0
  490. package/optional-skills/finance/3-statement-model/references/formatting.md +118 -0
  491. package/optional-skills/finance/3-statement-model/references/formulas.md +292 -0
  492. package/optional-skills/finance/3-statement-model/references/sec-filings.md +125 -0
  493. package/optional-skills/finance/comps-analysis/SKILL.md +662 -0
  494. package/optional-skills/finance/dcf-model/SKILL.md +1270 -0
  495. package/optional-skills/finance/dcf-model/TROUBLESHOOTING.md +40 -0
  496. package/optional-skills/finance/dcf-model/requirements.txt +7 -0
  497. package/optional-skills/finance/dcf-model/scripts/validate_dcf.py +292 -0
  498. package/optional-skills/finance/excel-author/SKILL.md +244 -0
  499. package/optional-skills/finance/excel-author/scripts/recalc.py +88 -0
  500. package/optional-skills/finance/lbo-model/SKILL.md +291 -0
  501. package/optional-skills/finance/merger-model/SKILL.md +144 -0
  502. package/optional-skills/finance/pptx-author/SKILL.md +173 -0
  503. package/optional-skills/finance/stocks/SKILL.md +95 -0
  504. package/optional-skills/finance/stocks/scripts/stocks_client.py +755 -0
  505. package/optional-skills/health/DESCRIPTION.md +1 -0
  506. package/optional-skills/health/fitness-nutrition/SKILL.md +256 -0
  507. package/optional-skills/health/fitness-nutrition/references/FORMULAS.md +100 -0
  508. package/optional-skills/health/fitness-nutrition/scripts/body_calc.py +210 -0
  509. package/optional-skills/health/fitness-nutrition/scripts/nutrition_search.py +86 -0
  510. package/optional-skills/health/neuroskill-bci/SKILL.md +459 -0
  511. package/optional-skills/health/neuroskill-bci/references/api.md +286 -0
  512. package/optional-skills/health/neuroskill-bci/references/metrics.md +220 -0
  513. package/optional-skills/health/neuroskill-bci/references/protocols.md +452 -0
  514. package/optional-skills/mcp/DESCRIPTION.md +3 -0
  515. package/optional-skills/mcp/fastmcp/SKILL.md +300 -0
  516. package/optional-skills/mcp/fastmcp/references/fastmcp-cli.md +110 -0
  517. package/optional-skills/mcp/fastmcp/scripts/scaffold_fastmcp.py +56 -0
  518. package/optional-skills/mcp/fastmcp/templates/api_wrapper.py +54 -0
  519. package/optional-skills/mcp/fastmcp/templates/database_server.py +77 -0
  520. package/optional-skills/mcp/fastmcp/templates/file_processor.py +55 -0
  521. package/optional-skills/mcp/mcporter/SKILL.md +123 -0
  522. package/optional-skills/migration/DESCRIPTION.md +2 -0
  523. package/optional-skills/migration/openclaw-migration/SKILL.md +298 -0
  524. package/optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py +3136 -0
  525. package/optional-skills/mlops/accelerate/SKILL.md +336 -0
  526. package/optional-skills/mlops/accelerate/references/custom-plugins.md +453 -0
  527. package/optional-skills/mlops/accelerate/references/megatron-integration.md +489 -0
  528. package/optional-skills/mlops/accelerate/references/performance.md +525 -0
  529. package/optional-skills/mlops/chroma/SKILL.md +410 -0
  530. package/optional-skills/mlops/chroma/references/integration.md +38 -0
  531. package/optional-skills/mlops/clip/SKILL.md +257 -0
  532. package/optional-skills/mlops/clip/references/applications.md +207 -0
  533. package/optional-skills/mlops/faiss/SKILL.md +225 -0
  534. package/optional-skills/mlops/faiss/references/index_types.md +280 -0
  535. package/optional-skills/mlops/flash-attention/SKILL.md +367 -0
  536. package/optional-skills/mlops/flash-attention/references/benchmarks.md +215 -0
  537. package/optional-skills/mlops/flash-attention/references/transformers-integration.md +293 -0
  538. package/optional-skills/mlops/guidance/SKILL.md +576 -0
  539. package/optional-skills/mlops/guidance/references/backends.md +554 -0
  540. package/optional-skills/mlops/guidance/references/constraints.md +674 -0
  541. package/optional-skills/mlops/guidance/references/examples.md +767 -0
  542. package/optional-skills/mlops/huggingface-tokenizers/SKILL.md +520 -0
  543. package/optional-skills/mlops/huggingface-tokenizers/references/algorithms.md +653 -0
  544. package/optional-skills/mlops/huggingface-tokenizers/references/integration.md +637 -0
  545. package/optional-skills/mlops/huggingface-tokenizers/references/pipeline.md +723 -0
  546. package/optional-skills/mlops/huggingface-tokenizers/references/training.md +565 -0
  547. package/optional-skills/mlops/inference/outlines/SKILL.md +656 -0
  548. package/optional-skills/mlops/inference/outlines/references/backends.md +615 -0
  549. package/optional-skills/mlops/inference/outlines/references/examples.md +773 -0
  550. package/optional-skills/mlops/inference/outlines/references/json_generation.md +652 -0
  551. package/optional-skills/mlops/instructor/SKILL.md +744 -0
  552. package/optional-skills/mlops/instructor/references/examples.md +107 -0
  553. package/optional-skills/mlops/instructor/references/providers.md +70 -0
  554. package/optional-skills/mlops/instructor/references/validation.md +606 -0
  555. package/optional-skills/mlops/lambda-labs/SKILL.md +549 -0
  556. package/optional-skills/mlops/lambda-labs/references/advanced-usage.md +611 -0
  557. package/optional-skills/mlops/lambda-labs/references/troubleshooting.md +530 -0
  558. package/optional-skills/mlops/llava/SKILL.md +308 -0
  559. package/optional-skills/mlops/llava/references/training.md +197 -0
  560. package/optional-skills/mlops/modal/SKILL.md +345 -0
  561. package/optional-skills/mlops/modal/references/advanced-usage.md +503 -0
  562. package/optional-skills/mlops/modal/references/troubleshooting.md +494 -0
  563. package/optional-skills/mlops/nemo-curator/SKILL.md +387 -0
  564. package/optional-skills/mlops/nemo-curator/references/deduplication.md +87 -0
  565. package/optional-skills/mlops/nemo-curator/references/filtering.md +102 -0
  566. package/optional-skills/mlops/peft/SKILL.md +435 -0
  567. package/optional-skills/mlops/peft/references/advanced-usage.md +514 -0
  568. package/optional-skills/mlops/peft/references/troubleshooting.md +480 -0
  569. package/optional-skills/mlops/pinecone/SKILL.md +362 -0
  570. package/optional-skills/mlops/pinecone/references/deployment.md +181 -0
  571. package/optional-skills/mlops/pytorch-fsdp/SKILL.md +130 -0
  572. package/optional-skills/mlops/pytorch-fsdp/references/index.md +7 -0
  573. package/optional-skills/mlops/pytorch-fsdp/references/other.md +4261 -0
  574. package/optional-skills/mlops/pytorch-lightning/SKILL.md +350 -0
  575. package/optional-skills/mlops/pytorch-lightning/references/callbacks.md +436 -0
  576. package/optional-skills/mlops/pytorch-lightning/references/distributed.md +490 -0
  577. package/optional-skills/mlops/pytorch-lightning/references/hyperparameter-tuning.md +556 -0
  578. package/optional-skills/mlops/qdrant/SKILL.md +497 -0
  579. package/optional-skills/mlops/qdrant/references/advanced-usage.md +648 -0
  580. package/optional-skills/mlops/qdrant/references/troubleshooting.md +631 -0
  581. package/optional-skills/mlops/saelens/SKILL.md +390 -0
  582. package/optional-skills/mlops/saelens/references/README.md +69 -0
  583. package/optional-skills/mlops/saelens/references/api.md +333 -0
  584. package/optional-skills/mlops/saelens/references/tutorials.md +318 -0
  585. package/optional-skills/mlops/simpo/SKILL.md +223 -0
  586. package/optional-skills/mlops/simpo/references/datasets.md +478 -0
  587. package/optional-skills/mlops/simpo/references/hyperparameters.md +452 -0
  588. package/optional-skills/mlops/simpo/references/loss-functions.md +350 -0
  589. package/optional-skills/mlops/slime/SKILL.md +468 -0
  590. package/optional-skills/mlops/slime/references/api-reference.md +392 -0
  591. package/optional-skills/mlops/slime/references/troubleshooting.md +386 -0
  592. package/optional-skills/mlops/stable-diffusion/SKILL.md +523 -0
  593. package/optional-skills/mlops/stable-diffusion/references/advanced-usage.md +716 -0
  594. package/optional-skills/mlops/stable-diffusion/references/troubleshooting.md +555 -0
  595. package/optional-skills/mlops/tensorrt-llm/SKILL.md +191 -0
  596. package/optional-skills/mlops/tensorrt-llm/references/multi-gpu.md +298 -0
  597. package/optional-skills/mlops/tensorrt-llm/references/optimization.md +242 -0
  598. package/optional-skills/mlops/tensorrt-llm/references/serving.md +470 -0
  599. package/optional-skills/mlops/torchtitan/SKILL.md +362 -0
  600. package/optional-skills/mlops/torchtitan/references/checkpoint.md +181 -0
  601. package/optional-skills/mlops/torchtitan/references/custom-models.md +258 -0
  602. package/optional-skills/mlops/torchtitan/references/float8.md +133 -0
  603. package/optional-skills/mlops/torchtitan/references/fsdp.md +126 -0
  604. package/optional-skills/mlops/training/axolotl/SKILL.md +166 -0
  605. package/optional-skills/mlops/training/axolotl/references/api.md +5548 -0
  606. package/optional-skills/mlops/training/axolotl/references/dataset-formats.md +1029 -0
  607. package/optional-skills/mlops/training/axolotl/references/index.md +15 -0
  608. package/optional-skills/mlops/training/axolotl/references/other.md +3563 -0
  609. package/optional-skills/mlops/training/trl-fine-tuning/SKILL.md +463 -0
  610. package/optional-skills/mlops/training/trl-fine-tuning/references/dpo-variants.md +227 -0
  611. package/optional-skills/mlops/training/trl-fine-tuning/references/grpo-training.md +504 -0
  612. package/optional-skills/mlops/training/trl-fine-tuning/references/online-rl.md +82 -0
  613. package/optional-skills/mlops/training/trl-fine-tuning/references/reward-modeling.md +122 -0
  614. package/optional-skills/mlops/training/trl-fine-tuning/references/sft-training.md +168 -0
  615. package/optional-skills/mlops/training/trl-fine-tuning/templates/basic_grpo_training.py +228 -0
  616. package/optional-skills/mlops/training/unsloth/SKILL.md +84 -0
  617. package/optional-skills/mlops/training/unsloth/references/index.md +7 -0
  618. package/optional-skills/mlops/training/unsloth/references/llms-full.md +16799 -0
  619. package/optional-skills/mlops/training/unsloth/references/llms-txt.md +12044 -0
  620. package/optional-skills/mlops/training/unsloth/references/llms.md +82 -0
  621. package/optional-skills/mlops/whisper/SKILL.md +321 -0
  622. package/optional-skills/mlops/whisper/references/languages.md +189 -0
  623. package/optional-skills/productivity/canvas/SKILL.md +98 -0
  624. package/optional-skills/productivity/canvas/scripts/canvas_api.py +157 -0
  625. package/optional-skills/productivity/here-now/SKILL.md +217 -0
  626. package/optional-skills/productivity/here-now/scripts/drive.sh +406 -0
  627. package/optional-skills/productivity/here-now/scripts/publish.sh +445 -0
  628. package/optional-skills/productivity/memento-flashcards/SKILL.md +324 -0
  629. package/optional-skills/productivity/memento-flashcards/scripts/memento_cards.py +353 -0
  630. package/optional-skills/productivity/memento-flashcards/scripts/youtube_quiz.py +88 -0
  631. package/optional-skills/productivity/shop-app/SKILL.md +340 -0
  632. package/optional-skills/productivity/shopify/SKILL.md +373 -0
  633. package/optional-skills/productivity/siyuan/SKILL.md +298 -0
  634. package/optional-skills/productivity/telephony/SKILL.md +418 -0
  635. package/optional-skills/productivity/telephony/scripts/telephony.py +1343 -0
  636. package/optional-skills/research/bioinformatics/SKILL.md +235 -0
  637. package/optional-skills/research/darwinian-evolver/SKILL.md +199 -0
  638. package/optional-skills/research/darwinian-evolver/scripts/parrot_openrouter.py +218 -0
  639. package/optional-skills/research/darwinian-evolver/scripts/show_snapshot.py +69 -0
  640. package/optional-skills/research/darwinian-evolver/templates/custom_problem_template.py +240 -0
  641. package/optional-skills/research/domain-intel/SKILL.md +97 -0
  642. package/optional-skills/research/domain-intel/scripts/domain_intel.py +397 -0
  643. package/optional-skills/research/drug-discovery/SKILL.md +227 -0
  644. package/optional-skills/research/drug-discovery/references/ADMET_REFERENCE.md +66 -0
  645. package/optional-skills/research/drug-discovery/scripts/chembl_target.py +53 -0
  646. package/optional-skills/research/drug-discovery/scripts/ro5_screen.py +44 -0
  647. package/optional-skills/research/duckduckgo-search/SKILL.md +238 -0
  648. package/optional-skills/research/duckduckgo-search/scripts/duckduckgo.sh +28 -0
  649. package/optional-skills/research/gitnexus-explorer/SKILL.md +214 -0
  650. package/optional-skills/research/gitnexus-explorer/scripts/proxy.mjs +92 -0
  651. package/optional-skills/research/osint-investigation/SKILL.md +277 -0
  652. package/optional-skills/research/osint-investigation/references/sources/courtlistener.md +98 -0
  653. package/optional-skills/research/osint-investigation/references/sources/gdelt.md +104 -0
  654. package/optional-skills/research/osint-investigation/references/sources/icij-offshore.md +104 -0
  655. package/optional-skills/research/osint-investigation/references/sources/nyc-acris.md +90 -0
  656. package/optional-skills/research/osint-investigation/references/sources/ofac-sdn.md +92 -0
  657. package/optional-skills/research/osint-investigation/references/sources/opencorporates.md +103 -0
  658. package/optional-skills/research/osint-investigation/references/sources/sec-edgar.md +83 -0
  659. package/optional-skills/research/osint-investigation/references/sources/senate-ld.md +89 -0
  660. package/optional-skills/research/osint-investigation/references/sources/usaspending.md +97 -0
  661. package/optional-skills/research/osint-investigation/references/sources/wayback.md +93 -0
  662. package/optional-skills/research/osint-investigation/references/sources/wikipedia.md +107 -0
  663. package/optional-skills/research/osint-investigation/scripts/_http.py +82 -0
  664. package/optional-skills/research/osint-investigation/scripts/_normalize.py +67 -0
  665. package/optional-skills/research/osint-investigation/scripts/build_findings.py +221 -0
  666. package/optional-skills/research/osint-investigation/scripts/entity_resolution.py +228 -0
  667. package/optional-skills/research/osint-investigation/scripts/fetch_courtlistener.py +149 -0
  668. package/optional-skills/research/osint-investigation/scripts/fetch_gdelt.py +162 -0
  669. package/optional-skills/research/osint-investigation/scripts/fetch_icij_offshore.py +234 -0
  670. package/optional-skills/research/osint-investigation/scripts/fetch_nyc_acris.py +203 -0
  671. package/optional-skills/research/osint-investigation/scripts/fetch_ofac_sdn.py +175 -0
  672. package/optional-skills/research/osint-investigation/scripts/fetch_opencorporates.py +192 -0
  673. package/optional-skills/research/osint-investigation/scripts/fetch_sec_edgar.py +184 -0
  674. package/optional-skills/research/osint-investigation/scripts/fetch_senate_ld.py +146 -0
  675. package/optional-skills/research/osint-investigation/scripts/fetch_usaspending.py +170 -0
  676. package/optional-skills/research/osint-investigation/scripts/fetch_wayback.py +142 -0
  677. package/optional-skills/research/osint-investigation/scripts/fetch_wikipedia.py +267 -0
  678. package/optional-skills/research/osint-investigation/scripts/timing_analysis.py +253 -0
  679. package/optional-skills/research/osint-investigation/templates/source-template.md +59 -0
  680. package/optional-skills/research/parallel-cli/SKILL.md +391 -0
  681. package/optional-skills/research/qmd/SKILL.md +441 -0
  682. package/optional-skills/research/scrapling/SKILL.md +336 -0
  683. package/optional-skills/research/searxng-search/SKILL.md +212 -0
  684. package/optional-skills/research/searxng-search/scripts/searxng.sh +22 -0
  685. package/optional-skills/security/1password/SKILL.md +163 -0
  686. package/optional-skills/security/1password/references/cli-examples.md +31 -0
  687. package/optional-skills/security/1password/references/get-started.md +21 -0
  688. package/optional-skills/security/DESCRIPTION.md +3 -0
  689. package/optional-skills/security/oss-forensics/SKILL.md +423 -0
  690. package/optional-skills/security/oss-forensics/references/evidence-types.md +89 -0
  691. package/optional-skills/security/oss-forensics/references/github-archive-guide.md +184 -0
  692. package/optional-skills/security/oss-forensics/references/investigation-templates.md +131 -0
  693. package/optional-skills/security/oss-forensics/references/recovery-techniques.md +164 -0
  694. package/optional-skills/security/oss-forensics/scripts/evidence-store.py +313 -0
  695. package/optional-skills/security/oss-forensics/templates/forensic-report.md +151 -0
  696. package/optional-skills/security/oss-forensics/templates/malicious-package-report.md +43 -0
  697. package/optional-skills/security/sherlock/SKILL.md +193 -0
  698. package/optional-skills/software-development/rest-graphql-debug/SKILL.md +514 -0
  699. package/optional-skills/web-development/DESCRIPTION.md +5 -0
  700. package/optional-skills/web-development/page-agent/SKILL.md +190 -0
  701. package/package.json +78 -0
  702. package/plugins/__init__.py +1 -0
  703. package/plugins/__pycache__/__init__.cpython-312.pyc +0 -0
  704. package/plugins/context_engine/__init__.py +219 -0
  705. package/plugins/disk-cleanup/README.md +51 -0
  706. package/plugins/disk-cleanup/__init__.py +316 -0
  707. package/plugins/disk-cleanup/disk_cleanup.py +497 -0
  708. package/plugins/disk-cleanup/plugin.yaml +7 -0
  709. package/plugins/example-dashboard/dashboard/manifest.json +14 -0
  710. package/plugins/example-dashboard/dashboard/plugin_api.py +17 -0
  711. package/plugins/google_meet/README.md +131 -0
  712. package/plugins/google_meet/SKILL.md +148 -0
  713. package/plugins/google_meet/__init__.py +103 -0
  714. package/plugins/google_meet/audio_bridge.py +244 -0
  715. package/plugins/google_meet/cli.py +479 -0
  716. package/plugins/google_meet/meet_bot.py +852 -0
  717. package/plugins/google_meet/node/__init__.py +54 -0
  718. package/plugins/google_meet/node/cli.py +125 -0
  719. package/plugins/google_meet/node/client.py +107 -0
  720. package/plugins/google_meet/node/protocol.py +124 -0
  721. package/plugins/google_meet/node/registry.py +113 -0
  722. package/plugins/google_meet/node/server.py +201 -0
  723. package/plugins/google_meet/plugin.yaml +16 -0
  724. package/plugins/google_meet/process_manager.py +324 -0
  725. package/plugins/google_meet/realtime/__init__.py +10 -0
  726. package/plugins/google_meet/realtime/openai_client.py +332 -0
  727. package/plugins/google_meet/tools.py +348 -0
  728. package/plugins/hermes-achievements/LICENSE +21 -0
  729. package/plugins/hermes-achievements/README.md +150 -0
  730. package/plugins/hermes-achievements/dashboard/dist/index.js +732 -0
  731. package/plugins/hermes-achievements/dashboard/dist/style.css +146 -0
  732. package/plugins/hermes-achievements/dashboard/manifest.json +11 -0
  733. package/plugins/hermes-achievements/dashboard/plugin_api.py +1062 -0
  734. package/plugins/hermes-achievements/docs/achievements-performance-implementation-plan.md +157 -0
  735. package/plugins/hermes-achievements/docs/achievements-performance-implementation-spec.md +219 -0
  736. package/plugins/hermes-achievements/docs/achievements-performance-spec.md +174 -0
  737. package/plugins/hermes-achievements/docs/assets/achievements-dashboard-hd.png +0 -0
  738. package/plugins/hermes-achievements/docs/assets/achievements-tier-showcase-hd.png +0 -0
  739. package/plugins/hermes-achievements/tests/test_achievement_engine.py +156 -0
  740. package/plugins/image_gen/openai/__init__.py +303 -0
  741. package/plugins/image_gen/openai/__pycache__/__init__.cpython-312.pyc +0 -0
  742. package/plugins/image_gen/openai/plugin.yaml +7 -0
  743. package/plugins/image_gen/openai-codex/__init__.py +378 -0
  744. package/plugins/image_gen/openai-codex/__pycache__/__init__.cpython-312.pyc +0 -0
  745. package/plugins/image_gen/openai-codex/plugin.yaml +5 -0
  746. package/plugins/image_gen/xai/__init__.py +316 -0
  747. package/plugins/image_gen/xai/__pycache__/__init__.cpython-312.pyc +0 -0
  748. package/plugins/image_gen/xai/plugin.yaml +7 -0
  749. package/plugins/kanban/dashboard/dist/index.js +3143 -0
  750. package/plugins/kanban/dashboard/dist/style.css +1500 -0
  751. package/plugins/kanban/dashboard/manifest.json +14 -0
  752. package/plugins/kanban/dashboard/plugin_api.py +1612 -0
  753. package/plugins/kanban/systemd/hermes-kanban-dispatcher.service +32 -0
  754. package/plugins/memory/__init__.py +408 -0
  755. package/plugins/memory/byterover/README.md +41 -0
  756. package/plugins/memory/byterover/__init__.py +384 -0
  757. package/plugins/memory/byterover/plugin.yaml +9 -0
  758. package/plugins/memory/hindsight/README.md +138 -0
  759. package/plugins/memory/hindsight/__init__.py +1758 -0
  760. package/plugins/memory/hindsight/plugin.yaml +8 -0
  761. package/plugins/memory/holographic/README.md +36 -0
  762. package/plugins/memory/holographic/__init__.py +409 -0
  763. package/plugins/memory/holographic/holographic.py +203 -0
  764. package/plugins/memory/holographic/plugin.yaml +5 -0
  765. package/plugins/memory/holographic/retrieval.py +593 -0
  766. package/plugins/memory/holographic/store.py +579 -0
  767. package/plugins/memory/honcho/README.md +328 -0
  768. package/plugins/memory/honcho/__init__.py +1329 -0
  769. package/plugins/memory/honcho/cli.py +1452 -0
  770. package/plugins/memory/honcho/client.py +784 -0
  771. package/plugins/memory/honcho/plugin.yaml +7 -0
  772. package/plugins/memory/honcho/session.py +1255 -0
  773. package/plugins/memory/mem0/README.md +38 -0
  774. package/plugins/memory/mem0/__init__.py +374 -0
  775. package/plugins/memory/mem0/plugin.yaml +5 -0
  776. package/plugins/memory/openviking/README.md +40 -0
  777. package/plugins/memory/openviking/__init__.py +945 -0
  778. package/plugins/memory/openviking/plugin.yaml +9 -0
  779. package/plugins/memory/retaindb/README.md +40 -0
  780. package/plugins/memory/retaindb/__init__.py +767 -0
  781. package/plugins/memory/retaindb/plugin.yaml +7 -0
  782. package/plugins/memory/supermemory/README.md +99 -0
  783. package/plugins/memory/supermemory/__init__.py +792 -0
  784. package/plugins/memory/supermemory/plugin.yaml +5 -0
  785. package/plugins/model-providers/README.md +70 -0
  786. package/plugins/model-providers/ai-gateway/__init__.py +43 -0
  787. package/plugins/model-providers/ai-gateway/__pycache__/__init__.cpython-312.pyc +0 -0
  788. package/plugins/model-providers/ai-gateway/plugin.yaml +5 -0
  789. package/plugins/model-providers/alibaba/__init__.py +13 -0
  790. package/plugins/model-providers/alibaba/__pycache__/__init__.cpython-312.pyc +0 -0
  791. package/plugins/model-providers/alibaba/plugin.yaml +5 -0
  792. package/plugins/model-providers/alibaba-coding-plan/__init__.py +21 -0
  793. package/plugins/model-providers/alibaba-coding-plan/__pycache__/__init__.cpython-312.pyc +0 -0
  794. package/plugins/model-providers/alibaba-coding-plan/plugin.yaml +5 -0
  795. package/plugins/model-providers/anthropic/__init__.py +52 -0
  796. package/plugins/model-providers/anthropic/__pycache__/__init__.cpython-312.pyc +0 -0
  797. package/plugins/model-providers/anthropic/plugin.yaml +5 -0
  798. package/plugins/model-providers/arcee/__init__.py +13 -0
  799. package/plugins/model-providers/arcee/__pycache__/__init__.cpython-312.pyc +0 -0
  800. package/plugins/model-providers/arcee/plugin.yaml +5 -0
  801. package/plugins/model-providers/azure-foundry/__init__.py +21 -0
  802. package/plugins/model-providers/azure-foundry/__pycache__/__init__.cpython-312.pyc +0 -0
  803. package/plugins/model-providers/azure-foundry/plugin.yaml +5 -0
  804. package/plugins/model-providers/bedrock/__init__.py +29 -0
  805. package/plugins/model-providers/bedrock/__pycache__/__init__.cpython-312.pyc +0 -0
  806. package/plugins/model-providers/bedrock/plugin.yaml +5 -0
  807. package/plugins/model-providers/copilot/__init__.py +58 -0
  808. package/plugins/model-providers/copilot/__pycache__/__init__.cpython-312.pyc +0 -0
  809. package/plugins/model-providers/copilot/plugin.yaml +5 -0
  810. package/plugins/model-providers/copilot-acp/__init__.py +34 -0
  811. package/plugins/model-providers/copilot-acp/__pycache__/__init__.cpython-312.pyc +0 -0
  812. package/plugins/model-providers/copilot-acp/plugin.yaml +5 -0
  813. package/plugins/model-providers/custom/__init__.py +68 -0
  814. package/plugins/model-providers/custom/__pycache__/__init__.cpython-312.pyc +0 -0
  815. package/plugins/model-providers/custom/plugin.yaml +5 -0
  816. package/plugins/model-providers/deepseek/__init__.py +99 -0
  817. package/plugins/model-providers/deepseek/__pycache__/__init__.cpython-312.pyc +0 -0
  818. package/plugins/model-providers/deepseek/plugin.yaml +5 -0
  819. package/plugins/model-providers/gemini/__init__.py +72 -0
  820. package/plugins/model-providers/gemini/__pycache__/__init__.cpython-312.pyc +0 -0
  821. package/plugins/model-providers/gemini/plugin.yaml +5 -0
  822. package/plugins/model-providers/gmi/__init__.py +31 -0
  823. package/plugins/model-providers/gmi/__pycache__/__init__.cpython-312.pyc +0 -0
  824. package/plugins/model-providers/gmi/plugin.yaml +5 -0
  825. package/plugins/model-providers/huggingface/__init__.py +20 -0
  826. package/plugins/model-providers/huggingface/__pycache__/__init__.cpython-312.pyc +0 -0
  827. package/plugins/model-providers/huggingface/plugin.yaml +5 -0
  828. package/plugins/model-providers/kilocode/__init__.py +14 -0
  829. package/plugins/model-providers/kilocode/__pycache__/__init__.cpython-312.pyc +0 -0
  830. package/plugins/model-providers/kilocode/plugin.yaml +5 -0
  831. package/plugins/model-providers/kimi-coding/__init__.py +71 -0
  832. package/plugins/model-providers/kimi-coding/__pycache__/__init__.cpython-312.pyc +0 -0
  833. package/plugins/model-providers/kimi-coding/plugin.yaml +5 -0
  834. package/plugins/model-providers/minimax/__init__.py +45 -0
  835. package/plugins/model-providers/minimax/__pycache__/__init__.cpython-312.pyc +0 -0
  836. package/plugins/model-providers/minimax/plugin.yaml +5 -0
  837. package/plugins/model-providers/nous/__init__.py +54 -0
  838. package/plugins/model-providers/nous/__pycache__/__init__.cpython-312.pyc +0 -0
  839. package/plugins/model-providers/nous/plugin.yaml +5 -0
  840. package/plugins/model-providers/novita/__init__.py +27 -0
  841. package/plugins/model-providers/novita/__pycache__/__init__.cpython-312.pyc +0 -0
  842. package/plugins/model-providers/novita/plugin.yaml +5 -0
  843. package/plugins/model-providers/nvidia/__init__.py +21 -0
  844. package/plugins/model-providers/nvidia/__pycache__/__init__.cpython-312.pyc +0 -0
  845. package/plugins/model-providers/nvidia/plugin.yaml +5 -0
  846. package/plugins/model-providers/ollama-cloud/__init__.py +14 -0
  847. package/plugins/model-providers/ollama-cloud/__pycache__/__init__.cpython-312.pyc +0 -0
  848. package/plugins/model-providers/ollama-cloud/plugin.yaml +5 -0
  849. package/plugins/model-providers/openai-codex/__init__.py +15 -0
  850. package/plugins/model-providers/openai-codex/__pycache__/__init__.cpython-312.pyc +0 -0
  851. package/plugins/model-providers/openai-codex/plugin.yaml +5 -0
  852. package/plugins/model-providers/opencode-zen/__init__.py +30 -0
  853. package/plugins/model-providers/opencode-zen/__pycache__/__init__.cpython-312.pyc +0 -0
  854. package/plugins/model-providers/opencode-zen/plugin.yaml +5 -0
  855. package/plugins/model-providers/openrouter/__init__.py +115 -0
  856. package/plugins/model-providers/openrouter/__pycache__/__init__.cpython-312.pyc +0 -0
  857. package/plugins/model-providers/openrouter/plugin.yaml +5 -0
  858. package/plugins/model-providers/qwen-oauth/__init__.py +82 -0
  859. package/plugins/model-providers/qwen-oauth/__pycache__/__init__.cpython-312.pyc +0 -0
  860. package/plugins/model-providers/qwen-oauth/plugin.yaml +5 -0
  861. package/plugins/model-providers/stepfun/__init__.py +14 -0
  862. package/plugins/model-providers/stepfun/__pycache__/__init__.cpython-312.pyc +0 -0
  863. package/plugins/model-providers/stepfun/plugin.yaml +5 -0
  864. package/plugins/model-providers/xai/__init__.py +15 -0
  865. package/plugins/model-providers/xai/__pycache__/__init__.cpython-312.pyc +0 -0
  866. package/plugins/model-providers/xai/plugin.yaml +5 -0
  867. package/plugins/model-providers/xiaomi/__init__.py +14 -0
  868. package/plugins/model-providers/xiaomi/__pycache__/__init__.cpython-312.pyc +0 -0
  869. package/plugins/model-providers/xiaomi/plugin.yaml +5 -0
  870. package/plugins/model-providers/zai/__init__.py +21 -0
  871. package/plugins/model-providers/zai/__pycache__/__init__.cpython-312.pyc +0 -0
  872. package/plugins/model-providers/zai/plugin.yaml +5 -0
  873. package/plugins/observability/langfuse/README.md +53 -0
  874. package/plugins/observability/langfuse/__init__.py +1004 -0
  875. package/plugins/observability/langfuse/plugin.yaml +14 -0
  876. package/plugins/platforms/google_chat/__init__.py +3 -0
  877. package/plugins/platforms/google_chat/__pycache__/__init__.cpython-312.pyc +0 -0
  878. package/plugins/platforms/google_chat/__pycache__/adapter.cpython-312.pyc +0 -0
  879. package/plugins/platforms/google_chat/adapter.py +3343 -0
  880. package/plugins/platforms/google_chat/oauth.py +639 -0
  881. package/plugins/platforms/google_chat/plugin.yaml +39 -0
  882. package/plugins/platforms/irc/__init__.py +3 -0
  883. package/plugins/platforms/irc/__pycache__/__init__.cpython-312.pyc +0 -0
  884. package/plugins/platforms/irc/__pycache__/adapter.cpython-312.pyc +0 -0
  885. package/plugins/platforms/irc/adapter.py +969 -0
  886. package/plugins/platforms/irc/plugin.yaml +54 -0
  887. package/plugins/platforms/line/__init__.py +3 -0
  888. package/plugins/platforms/line/__pycache__/__init__.cpython-312.pyc +0 -0
  889. package/plugins/platforms/line/__pycache__/adapter.cpython-312.pyc +0 -0
  890. package/plugins/platforms/line/adapter.py +1639 -0
  891. package/plugins/platforms/line/plugin.yaml +65 -0
  892. package/plugins/platforms/simplex/__init__.py +3 -0
  893. package/plugins/platforms/simplex/__pycache__/__init__.cpython-312.pyc +0 -0
  894. package/plugins/platforms/simplex/__pycache__/adapter.cpython-312.pyc +0 -0
  895. package/plugins/platforms/simplex/adapter.py +746 -0
  896. package/plugins/platforms/simplex/plugin.yaml +37 -0
  897. package/plugins/platforms/teams/__init__.py +3 -0
  898. package/plugins/platforms/teams/__pycache__/__init__.cpython-312.pyc +0 -0
  899. package/plugins/platforms/teams/__pycache__/adapter.cpython-312.pyc +0 -0
  900. package/plugins/platforms/teams/adapter.py +1188 -0
  901. package/plugins/platforms/teams/plugin.yaml +48 -0
  902. package/plugins/spotify/__init__.py +66 -0
  903. package/plugins/spotify/__pycache__/__init__.cpython-312.pyc +0 -0
  904. package/plugins/spotify/__pycache__/client.cpython-312.pyc +0 -0
  905. package/plugins/spotify/__pycache__/tools.cpython-312.pyc +0 -0
  906. package/plugins/spotify/client.py +435 -0
  907. package/plugins/spotify/plugin.yaml +13 -0
  908. package/plugins/spotify/tools.py +454 -0
  909. package/plugins/teams_pipeline/__init__.py +23 -0
  910. package/plugins/teams_pipeline/cli.py +463 -0
  911. package/plugins/teams_pipeline/meetings.py +333 -0
  912. package/plugins/teams_pipeline/models.py +350 -0
  913. package/plugins/teams_pipeline/pipeline.py +692 -0
  914. package/plugins/teams_pipeline/plugin.yaml +9 -0
  915. package/plugins/teams_pipeline/runtime.py +135 -0
  916. package/plugins/teams_pipeline/store.py +194 -0
  917. package/plugins/teams_pipeline/subscriptions.py +249 -0
  918. package/plugins/video_gen/fal/__init__.py +523 -0
  919. package/plugins/video_gen/fal/__pycache__/__init__.cpython-312.pyc +0 -0
  920. package/plugins/video_gen/fal/plugin.yaml +7 -0
  921. package/plugins/video_gen/xai/__init__.py +441 -0
  922. package/plugins/video_gen/xai/__pycache__/__init__.cpython-312.pyc +0 -0
  923. package/plugins/video_gen/xai/plugin.yaml +7 -0
  924. package/plugins/web/__init__.py +7 -0
  925. package/plugins/web/__pycache__/__init__.cpython-312.pyc +0 -0
  926. package/plugins/web/brave_free/__init__.py +14 -0
  927. package/plugins/web/brave_free/__pycache__/__init__.cpython-312.pyc +0 -0
  928. package/plugins/web/brave_free/__pycache__/provider.cpython-312.pyc +0 -0
  929. package/plugins/web/brave_free/plugin.yaml +7 -0
  930. package/plugins/web/brave_free/provider.py +137 -0
  931. package/plugins/web/ddgs/__init__.py +15 -0
  932. package/plugins/web/ddgs/__pycache__/__init__.cpython-312.pyc +0 -0
  933. package/plugins/web/ddgs/__pycache__/provider.cpython-312.pyc +0 -0
  934. package/plugins/web/ddgs/plugin.yaml +7 -0
  935. package/plugins/web/ddgs/provider.py +104 -0
  936. package/plugins/web/exa/__init__.py +15 -0
  937. package/plugins/web/exa/__pycache__/__init__.cpython-312.pyc +0 -0
  938. package/plugins/web/exa/__pycache__/provider.cpython-312.pyc +0 -0
  939. package/plugins/web/exa/plugin.yaml +7 -0
  940. package/plugins/web/exa/provider.py +212 -0
  941. package/plugins/web/firecrawl/__init__.py +28 -0
  942. package/plugins/web/firecrawl/__pycache__/__init__.cpython-312.pyc +0 -0
  943. package/plugins/web/firecrawl/__pycache__/provider.cpython-312.pyc +0 -0
  944. package/plugins/web/firecrawl/plugin.yaml +7 -0
  945. package/plugins/web/firecrawl/provider.py +773 -0
  946. package/plugins/web/parallel/__init__.py +16 -0
  947. package/plugins/web/parallel/__pycache__/__init__.cpython-312.pyc +0 -0
  948. package/plugins/web/parallel/__pycache__/provider.cpython-312.pyc +0 -0
  949. package/plugins/web/parallel/plugin.yaml +7 -0
  950. package/plugins/web/parallel/provider.py +291 -0
  951. package/plugins/web/searxng/__init__.py +15 -0
  952. package/plugins/web/searxng/__pycache__/__init__.cpython-312.pyc +0 -0
  953. package/plugins/web/searxng/__pycache__/provider.cpython-312.pyc +0 -0
  954. package/plugins/web/searxng/plugin.yaml +7 -0
  955. package/plugins/web/searxng/provider.py +140 -0
  956. package/plugins/web/tavily/__init__.py +15 -0
  957. package/plugins/web/tavily/__pycache__/__init__.cpython-312.pyc +0 -0
  958. package/plugins/web/tavily/__pycache__/provider.cpython-312.pyc +0 -0
  959. package/plugins/web/tavily/plugin.yaml +7 -0
  960. package/plugins/web/tavily/provider.py +285 -0
  961. package/providers/README.md +78 -0
  962. package/providers/__init__.py +192 -0
  963. package/providers/__pycache__/__init__.cpython-312.pyc +0 -0
  964. package/providers/__pycache__/base.cpython-312.pyc +0 -0
  965. package/providers/base.py +184 -0
  966. package/pyproject.toml +255 -0
  967. package/run_agent.py +16409 -0
  968. package/scripts/benchmark_browser_eval.py +138 -0
  969. package/scripts/build_model_catalog.py +95 -0
  970. package/scripts/build_skills_index.py +325 -0
  971. package/scripts/check-windows-footguns.py +624 -0
  972. package/scripts/contributor_audit.py +473 -0
  973. package/scripts/discord-voice-doctor.py +396 -0
  974. package/scripts/hermes-gateway +416 -0
  975. package/scripts/install.cmd +28 -0
  976. package/scripts/install.ps1 +1611 -0
  977. package/scripts/install.sh +2007 -0
  978. package/scripts/install_psutil_android.py +117 -0
  979. package/scripts/keystroke_diagnostic.py +81 -0
  980. package/scripts/kill_modal.sh +34 -0
  981. package/scripts/lib/node-bootstrap.sh +238 -0
  982. package/scripts/lint_diff.py +207 -0
  983. package/scripts/postinstall.js +150 -0
  984. package/scripts/profile-tui.py +626 -0
  985. package/scripts/release.py +1680 -0
  986. package/scripts/run_tests.sh +129 -0
  987. package/scripts/sample_and_compress.py +409 -0
  988. package/scripts/setup_open_webui.sh +349 -0
  989. package/scripts/whatsapp-bridge/allowlist.js +88 -0
  990. package/scripts/whatsapp-bridge/allowlist.test.mjs +80 -0
  991. package/scripts/whatsapp-bridge/bridge.js +729 -0
  992. package/scripts/whatsapp-bridge/package-lock.json +2141 -0
  993. package/scripts/whatsapp-bridge/package.json +19 -0
  994. package/skills/apple/DESCRIPTION.md +2 -0
  995. package/skills/apple/apple-notes/SKILL.md +90 -0
  996. package/skills/apple/apple-reminders/SKILL.md +98 -0
  997. package/skills/apple/findmy/SKILL.md +131 -0
  998. package/skills/apple/imessage/SKILL.md +102 -0
  999. package/skills/apple/macos-computer-use/SKILL.md +201 -0
  1000. package/skills/autonomous-ai-agents/DESCRIPTION.md +3 -0
  1001. package/skills/autonomous-ai-agents/claude-code/SKILL.md +745 -0
  1002. package/skills/autonomous-ai-agents/codex/SKILL.md +130 -0
  1003. package/skills/autonomous-ai-agents/hermes-agent/SKILL.md +1014 -0
  1004. package/skills/autonomous-ai-agents/opencode/SKILL.md +219 -0
  1005. package/skills/creative/DESCRIPTION.md +3 -0
  1006. package/skills/creative/architecture-diagram/SKILL.md +148 -0
  1007. package/skills/creative/architecture-diagram/templates/template.html +319 -0
  1008. package/skills/creative/ascii-art/SKILL.md +322 -0
  1009. package/skills/creative/ascii-video/README.md +290 -0
  1010. package/skills/creative/ascii-video/SKILL.md +241 -0
  1011. package/skills/creative/ascii-video/references/architecture.md +802 -0
  1012. package/skills/creative/ascii-video/references/composition.md +892 -0
  1013. package/skills/creative/ascii-video/references/effects.md +1865 -0
  1014. package/skills/creative/ascii-video/references/inputs.md +685 -0
  1015. package/skills/creative/ascii-video/references/optimization.md +688 -0
  1016. package/skills/creative/ascii-video/references/scenes.md +1011 -0
  1017. package/skills/creative/ascii-video/references/shaders.md +1385 -0
  1018. package/skills/creative/ascii-video/references/troubleshooting.md +367 -0
  1019. package/skills/creative/baoyu-comic/PORT_NOTES.md +77 -0
  1020. package/skills/creative/baoyu-comic/SKILL.md +247 -0
  1021. package/skills/creative/baoyu-comic/references/analysis-framework.md +176 -0
  1022. package/skills/creative/baoyu-comic/references/art-styles/chalk.md +101 -0
  1023. package/skills/creative/baoyu-comic/references/art-styles/ink-brush.md +97 -0
  1024. package/skills/creative/baoyu-comic/references/art-styles/ligne-claire.md +75 -0
  1025. package/skills/creative/baoyu-comic/references/art-styles/manga.md +93 -0
  1026. package/skills/creative/baoyu-comic/references/art-styles/minimalist.md +84 -0
  1027. package/skills/creative/baoyu-comic/references/art-styles/realistic.md +89 -0
  1028. package/skills/creative/baoyu-comic/references/auto-selection.md +71 -0
  1029. package/skills/creative/baoyu-comic/references/base-prompt.md +98 -0
  1030. package/skills/creative/baoyu-comic/references/character-template.md +180 -0
  1031. package/skills/creative/baoyu-comic/references/layouts/cinematic.md +23 -0
  1032. package/skills/creative/baoyu-comic/references/layouts/dense.md +23 -0
  1033. package/skills/creative/baoyu-comic/references/layouts/four-panel.md +40 -0
  1034. package/skills/creative/baoyu-comic/references/layouts/mixed.md +23 -0
  1035. package/skills/creative/baoyu-comic/references/layouts/splash.md +23 -0
  1036. package/skills/creative/baoyu-comic/references/layouts/standard.md +23 -0
  1037. package/skills/creative/baoyu-comic/references/layouts/webtoon.md +30 -0
  1038. package/skills/creative/baoyu-comic/references/ohmsha-guide.md +85 -0
  1039. package/skills/creative/baoyu-comic/references/partial-workflows.md +106 -0
  1040. package/skills/creative/baoyu-comic/references/presets/concept-story.md +121 -0
  1041. package/skills/creative/baoyu-comic/references/presets/four-panel.md +107 -0
  1042. package/skills/creative/baoyu-comic/references/presets/ohmsha.md +114 -0
  1043. package/skills/creative/baoyu-comic/references/presets/shoujo.md +116 -0
  1044. package/skills/creative/baoyu-comic/references/presets/wuxia.md +110 -0
  1045. package/skills/creative/baoyu-comic/references/storyboard-template.md +143 -0
  1046. package/skills/creative/baoyu-comic/references/tones/action.md +110 -0
  1047. package/skills/creative/baoyu-comic/references/tones/dramatic.md +95 -0
  1048. package/skills/creative/baoyu-comic/references/tones/energetic.md +105 -0
  1049. package/skills/creative/baoyu-comic/references/tones/neutral.md +63 -0
  1050. package/skills/creative/baoyu-comic/references/tones/romantic.md +100 -0
  1051. package/skills/creative/baoyu-comic/references/tones/vintage.md +104 -0
  1052. package/skills/creative/baoyu-comic/references/tones/warm.md +94 -0
  1053. package/skills/creative/baoyu-comic/references/workflow.md +401 -0
  1054. package/skills/creative/baoyu-infographic/PORT_NOTES.md +43 -0
  1055. package/skills/creative/baoyu-infographic/SKILL.md +237 -0
  1056. package/skills/creative/baoyu-infographic/references/analysis-framework.md +182 -0
  1057. package/skills/creative/baoyu-infographic/references/base-prompt.md +43 -0
  1058. package/skills/creative/baoyu-infographic/references/layouts/bento-grid.md +41 -0
  1059. package/skills/creative/baoyu-infographic/references/layouts/binary-comparison.md +48 -0
  1060. package/skills/creative/baoyu-infographic/references/layouts/bridge.md +41 -0
  1061. package/skills/creative/baoyu-infographic/references/layouts/circular-flow.md +41 -0
  1062. package/skills/creative/baoyu-infographic/references/layouts/comic-strip.md +41 -0
  1063. package/skills/creative/baoyu-infographic/references/layouts/comparison-matrix.md +41 -0
  1064. package/skills/creative/baoyu-infographic/references/layouts/dashboard.md +41 -0
  1065. package/skills/creative/baoyu-infographic/references/layouts/dense-modules.md +72 -0
  1066. package/skills/creative/baoyu-infographic/references/layouts/funnel.md +41 -0
  1067. package/skills/creative/baoyu-infographic/references/layouts/hierarchical-layers.md +48 -0
  1068. package/skills/creative/baoyu-infographic/references/layouts/hub-spoke.md +41 -0
  1069. package/skills/creative/baoyu-infographic/references/layouts/iceberg.md +41 -0
  1070. package/skills/creative/baoyu-infographic/references/layouts/isometric-map.md +41 -0
  1071. package/skills/creative/baoyu-infographic/references/layouts/jigsaw.md +41 -0
  1072. package/skills/creative/baoyu-infographic/references/layouts/linear-progression.md +48 -0
  1073. package/skills/creative/baoyu-infographic/references/layouts/periodic-table.md +41 -0
  1074. package/skills/creative/baoyu-infographic/references/layouts/story-mountain.md +41 -0
  1075. package/skills/creative/baoyu-infographic/references/layouts/structural-breakdown.md +48 -0
  1076. package/skills/creative/baoyu-infographic/references/layouts/tree-branching.md +41 -0
  1077. package/skills/creative/baoyu-infographic/references/layouts/venn-diagram.md +41 -0
  1078. package/skills/creative/baoyu-infographic/references/layouts/winding-roadmap.md +41 -0
  1079. package/skills/creative/baoyu-infographic/references/structured-content-template.md +244 -0
  1080. package/skills/creative/baoyu-infographic/references/styles/aged-academia.md +36 -0
  1081. package/skills/creative/baoyu-infographic/references/styles/bold-graphic.md +36 -0
  1082. package/skills/creative/baoyu-infographic/references/styles/chalkboard.md +61 -0
  1083. package/skills/creative/baoyu-infographic/references/styles/claymation.md +29 -0
  1084. package/skills/creative/baoyu-infographic/references/styles/corporate-memphis.md +29 -0
  1085. package/skills/creative/baoyu-infographic/references/styles/craft-handmade.md +44 -0
  1086. package/skills/creative/baoyu-infographic/references/styles/cyberpunk-neon.md +29 -0
  1087. package/skills/creative/baoyu-infographic/references/styles/hand-drawn-edu.md +63 -0
  1088. package/skills/creative/baoyu-infographic/references/styles/ikea-manual.md +29 -0
  1089. package/skills/creative/baoyu-infographic/references/styles/kawaii.md +29 -0
  1090. package/skills/creative/baoyu-infographic/references/styles/knolling.md +29 -0
  1091. package/skills/creative/baoyu-infographic/references/styles/lego-brick.md +29 -0
  1092. package/skills/creative/baoyu-infographic/references/styles/morandi-journal.md +60 -0
  1093. package/skills/creative/baoyu-infographic/references/styles/origami.md +29 -0
  1094. package/skills/creative/baoyu-infographic/references/styles/pixel-art.md +29 -0
  1095. package/skills/creative/baoyu-infographic/references/styles/pop-laboratory.md +48 -0
  1096. package/skills/creative/baoyu-infographic/references/styles/retro-pop-grid.md +47 -0
  1097. package/skills/creative/baoyu-infographic/references/styles/storybook-watercolor.md +29 -0
  1098. package/skills/creative/baoyu-infographic/references/styles/subway-map.md +29 -0
  1099. package/skills/creative/baoyu-infographic/references/styles/technical-schematic.md +36 -0
  1100. package/skills/creative/baoyu-infographic/references/styles/ui-wireframe.md +29 -0
  1101. package/skills/creative/claude-design/SKILL.md +591 -0
  1102. package/skills/creative/comfyui/SKILL.md +612 -0
  1103. package/skills/creative/comfyui/references/official-cli.md +255 -0
  1104. package/skills/creative/comfyui/references/rest-api.md +312 -0
  1105. package/skills/creative/comfyui/references/template-integrity.md +243 -0
  1106. package/skills/creative/comfyui/references/workflow-format.md +226 -0
  1107. package/skills/creative/comfyui/scripts/_common.py +835 -0
  1108. package/skills/creative/comfyui/scripts/auto_fix_deps.py +225 -0
  1109. package/skills/creative/comfyui/scripts/check_deps.py +437 -0
  1110. package/skills/creative/comfyui/scripts/comfyui_setup.sh +286 -0
  1111. package/skills/creative/comfyui/scripts/extract_schema.py +315 -0
  1112. package/skills/creative/comfyui/scripts/fetch_logs.py +158 -0
  1113. package/skills/creative/comfyui/scripts/hardware_check.py +497 -0
  1114. package/skills/creative/comfyui/scripts/health_check.py +223 -0
  1115. package/skills/creative/comfyui/scripts/run_batch.py +243 -0
  1116. package/skills/creative/comfyui/scripts/run_workflow.py +796 -0
  1117. package/skills/creative/comfyui/scripts/ws_monitor.py +267 -0
  1118. package/skills/creative/comfyui/tests/README.md +50 -0
  1119. package/skills/creative/comfyui/tests/conftest.py +64 -0
  1120. package/skills/creative/comfyui/tests/pytest.ini +5 -0
  1121. package/skills/creative/comfyui/tests/test_check_deps.py +68 -0
  1122. package/skills/creative/comfyui/tests/test_cloud_integration.py +95 -0
  1123. package/skills/creative/comfyui/tests/test_common.py +447 -0
  1124. package/skills/creative/comfyui/tests/test_extract_schema.py +185 -0
  1125. package/skills/creative/comfyui/tests/test_run_workflow.py +213 -0
  1126. package/skills/creative/comfyui/workflows/README.md +86 -0
  1127. package/skills/creative/comfyui/workflows/animatediff_video.json +64 -0
  1128. package/skills/creative/comfyui/workflows/flux_dev_txt2img.json +78 -0
  1129. package/skills/creative/comfyui/workflows/sd15_txt2img.json +49 -0
  1130. package/skills/creative/comfyui/workflows/sdxl_img2img.json +54 -0
  1131. package/skills/creative/comfyui/workflows/sdxl_inpaint.json +59 -0
  1132. package/skills/creative/comfyui/workflows/sdxl_txt2img.json +49 -0
  1133. package/skills/creative/comfyui/workflows/upscale_4x.json +27 -0
  1134. package/skills/creative/comfyui/workflows/wan_video_t2v.json +69 -0
  1135. package/skills/creative/creative-ideation/SKILL.md +152 -0
  1136. package/skills/creative/creative-ideation/references/full-prompt-library.md +110 -0
  1137. package/skills/creative/design-md/SKILL.md +199 -0
  1138. package/skills/creative/design-md/templates/starter.md +99 -0
  1139. package/skills/creative/excalidraw/SKILL.md +199 -0
  1140. package/skills/creative/excalidraw/references/colors.md +44 -0
  1141. package/skills/creative/excalidraw/references/dark-mode.md +68 -0
  1142. package/skills/creative/excalidraw/references/examples.md +141 -0
  1143. package/skills/creative/excalidraw/scripts/upload.py +133 -0
  1144. package/skills/creative/humanizer/LICENSE +21 -0
  1145. package/skills/creative/humanizer/SKILL.md +578 -0
  1146. package/skills/creative/manim-video/README.md +23 -0
  1147. package/skills/creative/manim-video/SKILL.md +269 -0
  1148. package/skills/creative/manim-video/references/animation-design-thinking.md +161 -0
  1149. package/skills/creative/manim-video/references/animations.md +282 -0
  1150. package/skills/creative/manim-video/references/camera-and-3d.md +135 -0
  1151. package/skills/creative/manim-video/references/decorations.md +202 -0
  1152. package/skills/creative/manim-video/references/equations.md +216 -0
  1153. package/skills/creative/manim-video/references/graphs-and-data.md +163 -0
  1154. package/skills/creative/manim-video/references/mobjects.md +333 -0
  1155. package/skills/creative/manim-video/references/paper-explainer.md +255 -0
  1156. package/skills/creative/manim-video/references/production-quality.md +190 -0
  1157. package/skills/creative/manim-video/references/rendering.md +185 -0
  1158. package/skills/creative/manim-video/references/scene-planning.md +118 -0
  1159. package/skills/creative/manim-video/references/troubleshooting.md +135 -0
  1160. package/skills/creative/manim-video/references/updaters-and-trackers.md +260 -0
  1161. package/skills/creative/manim-video/references/visual-design.md +124 -0
  1162. package/skills/creative/manim-video/scripts/setup.sh +14 -0
  1163. package/skills/creative/p5js/README.md +64 -0
  1164. package/skills/creative/p5js/SKILL.md +556 -0
  1165. package/skills/creative/p5js/references/animation.md +439 -0
  1166. package/skills/creative/p5js/references/color-systems.md +352 -0
  1167. package/skills/creative/p5js/references/core-api.md +410 -0
  1168. package/skills/creative/p5js/references/export-pipeline.md +566 -0
  1169. package/skills/creative/p5js/references/interaction.md +398 -0
  1170. package/skills/creative/p5js/references/shapes-and-geometry.md +300 -0
  1171. package/skills/creative/p5js/references/troubleshooting.md +532 -0
  1172. package/skills/creative/p5js/references/typography.md +302 -0
  1173. package/skills/creative/p5js/references/visual-effects.md +895 -0
  1174. package/skills/creative/p5js/references/webgl-and-3d.md +423 -0
  1175. package/skills/creative/p5js/scripts/export-frames.js +179 -0
  1176. package/skills/creative/p5js/scripts/render.sh +108 -0
  1177. package/skills/creative/p5js/scripts/serve.sh +28 -0
  1178. package/skills/creative/p5js/scripts/setup.sh +87 -0
  1179. package/skills/creative/p5js/templates/viewer.html +395 -0
  1180. package/skills/creative/pixel-art/ATTRIBUTION.md +54 -0
  1181. package/skills/creative/pixel-art/SKILL.md +218 -0
  1182. package/skills/creative/pixel-art/references/palettes.md +49 -0
  1183. package/skills/creative/pixel-art/scripts/__init__.py +0 -0
  1184. package/skills/creative/pixel-art/scripts/palettes.py +167 -0
  1185. package/skills/creative/pixel-art/scripts/pixel_art.py +162 -0
  1186. package/skills/creative/pixel-art/scripts/pixel_art_video.py +345 -0
  1187. package/skills/creative/popular-web-designs/SKILL.md +214 -0
  1188. package/skills/creative/popular-web-designs/templates/airbnb.md +259 -0
  1189. package/skills/creative/popular-web-designs/templates/airtable.md +102 -0
  1190. package/skills/creative/popular-web-designs/templates/apple.md +326 -0
  1191. package/skills/creative/popular-web-designs/templates/bmw.md +193 -0
  1192. package/skills/creative/popular-web-designs/templates/cal.md +272 -0
  1193. package/skills/creative/popular-web-designs/templates/claude.md +325 -0
  1194. package/skills/creative/popular-web-designs/templates/clay.md +317 -0
  1195. package/skills/creative/popular-web-designs/templates/clickhouse.md +294 -0
  1196. package/skills/creative/popular-web-designs/templates/cohere.md +279 -0
  1197. package/skills/creative/popular-web-designs/templates/coinbase.md +142 -0
  1198. package/skills/creative/popular-web-designs/templates/composio.md +320 -0
  1199. package/skills/creative/popular-web-designs/templates/cursor.md +322 -0
  1200. package/skills/creative/popular-web-designs/templates/elevenlabs.md +278 -0
  1201. package/skills/creative/popular-web-designs/templates/expo.md +294 -0
  1202. package/skills/creative/popular-web-designs/templates/figma.md +233 -0
  1203. package/skills/creative/popular-web-designs/templates/framer.md +259 -0
  1204. package/skills/creative/popular-web-designs/templates/hashicorp.md +291 -0
  1205. package/skills/creative/popular-web-designs/templates/ibm.md +345 -0
  1206. package/skills/creative/popular-web-designs/templates/intercom.md +159 -0
  1207. package/skills/creative/popular-web-designs/templates/kraken.md +138 -0
  1208. package/skills/creative/popular-web-designs/templates/linear.app.md +380 -0
  1209. package/skills/creative/popular-web-designs/templates/lovable.md +311 -0
  1210. package/skills/creative/popular-web-designs/templates/minimax.md +270 -0
  1211. package/skills/creative/popular-web-designs/templates/mintlify.md +339 -0
  1212. package/skills/creative/popular-web-designs/templates/miro.md +121 -0
  1213. package/skills/creative/popular-web-designs/templates/mistral.ai.md +274 -0
  1214. package/skills/creative/popular-web-designs/templates/mongodb.md +279 -0
  1215. package/skills/creative/popular-web-designs/templates/notion.md +322 -0
  1216. package/skills/creative/popular-web-designs/templates/nvidia.md +306 -0
  1217. package/skills/creative/popular-web-designs/templates/ollama.md +280 -0
  1218. package/skills/creative/popular-web-designs/templates/opencode.ai.md +294 -0
  1219. package/skills/creative/popular-web-designs/templates/pinterest.md +243 -0
  1220. package/skills/creative/popular-web-designs/templates/posthog.md +269 -0
  1221. package/skills/creative/popular-web-designs/templates/raycast.md +281 -0
  1222. package/skills/creative/popular-web-designs/templates/replicate.md +274 -0
  1223. package/skills/creative/popular-web-designs/templates/resend.md +316 -0
  1224. package/skills/creative/popular-web-designs/templates/revolut.md +198 -0
  1225. package/skills/creative/popular-web-designs/templates/runwayml.md +257 -0
  1226. package/skills/creative/popular-web-designs/templates/sanity.md +370 -0
  1227. package/skills/creative/popular-web-designs/templates/sentry.md +275 -0
  1228. package/skills/creative/popular-web-designs/templates/spacex.md +207 -0
  1229. package/skills/creative/popular-web-designs/templates/spotify.md +259 -0
  1230. package/skills/creative/popular-web-designs/templates/stripe.md +335 -0
  1231. package/skills/creative/popular-web-designs/templates/supabase.md +268 -0
  1232. package/skills/creative/popular-web-designs/templates/superhuman.md +265 -0
  1233. package/skills/creative/popular-web-designs/templates/together.ai.md +276 -0
  1234. package/skills/creative/popular-web-designs/templates/uber.md +308 -0
  1235. package/skills/creative/popular-web-designs/templates/vercel.md +323 -0
  1236. package/skills/creative/popular-web-designs/templates/voltagent.md +336 -0
  1237. package/skills/creative/popular-web-designs/templates/warp.md +266 -0
  1238. package/skills/creative/popular-web-designs/templates/webflow.md +105 -0
  1239. package/skills/creative/popular-web-designs/templates/wise.md +186 -0
  1240. package/skills/creative/popular-web-designs/templates/x.ai.md +270 -0
  1241. package/skills/creative/popular-web-designs/templates/zapier.md +341 -0
  1242. package/skills/creative/pretext/SKILL.md +220 -0
  1243. package/skills/creative/pretext/references/patterns.md +258 -0
  1244. package/skills/creative/pretext/templates/donut-orbit.html +1468 -0
  1245. package/skills/creative/pretext/templates/hello-orb-flow.html +95 -0
  1246. package/skills/creative/sketch/SKILL.md +218 -0
  1247. package/skills/creative/songwriting-and-ai-music/SKILL.md +287 -0
  1248. package/skills/creative/touchdesigner-mcp/SKILL.md +356 -0
  1249. package/skills/creative/touchdesigner-mcp/references/3d-scene.md +275 -0
  1250. package/skills/creative/touchdesigner-mcp/references/animation.md +221 -0
  1251. package/skills/creative/touchdesigner-mcp/references/audio-reactive.md +175 -0
  1252. package/skills/creative/touchdesigner-mcp/references/dat-scripting.md +352 -0
  1253. package/skills/creative/touchdesigner-mcp/references/external-data.md +322 -0
  1254. package/skills/creative/touchdesigner-mcp/references/geometry-comp.md +121 -0
  1255. package/skills/creative/touchdesigner-mcp/references/glsl.md +151 -0
  1256. package/skills/creative/touchdesigner-mcp/references/layout-compositor.md +131 -0
  1257. package/skills/creative/touchdesigner-mcp/references/mcp-tools.md +382 -0
  1258. package/skills/creative/touchdesigner-mcp/references/midi-osc.md +211 -0
  1259. package/skills/creative/touchdesigner-mcp/references/network-patterns.md +966 -0
  1260. package/skills/creative/touchdesigner-mcp/references/operator-tips.md +106 -0
  1261. package/skills/creative/touchdesigner-mcp/references/operators.md +239 -0
  1262. package/skills/creative/touchdesigner-mcp/references/panel-ui.md +281 -0
  1263. package/skills/creative/touchdesigner-mcp/references/particles.md +245 -0
  1264. package/skills/creative/touchdesigner-mcp/references/pitfalls.md +704 -0
  1265. package/skills/creative/touchdesigner-mcp/references/postfx.md +183 -0
  1266. package/skills/creative/touchdesigner-mcp/references/projection-mapping.md +211 -0
  1267. package/skills/creative/touchdesigner-mcp/references/python-api.md +463 -0
  1268. package/skills/creative/touchdesigner-mcp/references/replicator.md +198 -0
  1269. package/skills/creative/touchdesigner-mcp/references/troubleshooting.md +244 -0
  1270. package/skills/creative/touchdesigner-mcp/scripts/setup.sh +115 -0
  1271. package/skills/data-science/DESCRIPTION.md +3 -0
  1272. package/skills/data-science/jupyter-live-kernel/SKILL.md +167 -0
  1273. package/skills/devops/kanban-orchestrator/SKILL.md +189 -0
  1274. package/skills/devops/kanban-worker/SKILL.md +184 -0
  1275. package/skills/devops/webhook-subscriptions/SKILL.md +204 -0
  1276. package/skills/diagramming/DESCRIPTION.md +3 -0
  1277. package/skills/dogfood/SKILL.md +162 -0
  1278. package/skills/dogfood/references/issue-taxonomy.md +109 -0
  1279. package/skills/dogfood/templates/dogfood-report-template.md +86 -0
  1280. package/skills/domain/DESCRIPTION.md +24 -0
  1281. package/skills/email/DESCRIPTION.md +3 -0
  1282. package/skills/email/himalaya/SKILL.md +299 -0
  1283. package/skills/email/himalaya/references/configuration.md +227 -0
  1284. package/skills/email/himalaya/references/message-composition.md +199 -0
  1285. package/skills/gaming/DESCRIPTION.md +3 -0
  1286. package/skills/gaming/minecraft-modpack-server/SKILL.md +187 -0
  1287. package/skills/gaming/pokemon-player/SKILL.md +216 -0
  1288. package/skills/gifs/DESCRIPTION.md +3 -0
  1289. package/skills/github/DESCRIPTION.md +3 -0
  1290. package/skills/github/codebase-inspection/SKILL.md +116 -0
  1291. package/skills/github/github-auth/SKILL.md +247 -0
  1292. package/skills/github/github-auth/scripts/gh-env.sh +66 -0
  1293. package/skills/github/github-code-review/SKILL.md +481 -0
  1294. package/skills/github/github-code-review/references/review-output-template.md +74 -0
  1295. package/skills/github/github-issues/SKILL.md +370 -0
  1296. package/skills/github/github-issues/templates/bug-report.md +35 -0
  1297. package/skills/github/github-issues/templates/feature-request.md +31 -0
  1298. package/skills/github/github-pr-workflow/SKILL.md +367 -0
  1299. package/skills/github/github-pr-workflow/references/ci-troubleshooting.md +183 -0
  1300. package/skills/github/github-pr-workflow/references/conventional-commits.md +71 -0
  1301. package/skills/github/github-pr-workflow/templates/pr-body-bugfix.md +35 -0
  1302. package/skills/github/github-pr-workflow/templates/pr-body-feature.md +33 -0
  1303. package/skills/github/github-repo-management/SKILL.md +516 -0
  1304. package/skills/github/github-repo-management/references/github-api-cheatsheet.md +161 -0
  1305. package/skills/index-cache/anthropics_skills_skills_.json +1 -0
  1306. package/skills/index-cache/claude_marketplace_anthropics_skills.json +1 -0
  1307. package/skills/index-cache/lobehub_index.json +1 -0
  1308. package/skills/index-cache/openai_skills_skills_.json +1 -0
  1309. package/skills/inference-sh/DESCRIPTION.md +19 -0
  1310. package/skills/mcp/DESCRIPTION.md +3 -0
  1311. package/skills/mcp/native-mcp/SKILL.md +357 -0
  1312. package/skills/media/DESCRIPTION.md +3 -0
  1313. package/skills/media/gif-search/SKILL.md +91 -0
  1314. package/skills/media/heartmula/SKILL.md +171 -0
  1315. package/skills/media/songsee/SKILL.md +83 -0
  1316. package/skills/media/spotify/SKILL.md +135 -0
  1317. package/skills/media/youtube-content/SKILL.md +73 -0
  1318. package/skills/media/youtube-content/references/output-formats.md +56 -0
  1319. package/skills/media/youtube-content/scripts/fetch_transcript.py +124 -0
  1320. package/skills/mlops/DESCRIPTION.md +3 -0
  1321. package/skills/mlops/evaluation/DESCRIPTION.md +3 -0
  1322. package/skills/mlops/evaluation/lm-evaluation-harness/SKILL.md +498 -0
  1323. package/skills/mlops/evaluation/lm-evaluation-harness/references/api-evaluation.md +490 -0
  1324. package/skills/mlops/evaluation/lm-evaluation-harness/references/benchmark-guide.md +488 -0
  1325. package/skills/mlops/evaluation/lm-evaluation-harness/references/custom-tasks.md +602 -0
  1326. package/skills/mlops/evaluation/lm-evaluation-harness/references/distributed-eval.md +519 -0
  1327. package/skills/mlops/evaluation/weights-and-biases/SKILL.md +594 -0
  1328. package/skills/mlops/evaluation/weights-and-biases/references/artifacts.md +584 -0
  1329. package/skills/mlops/evaluation/weights-and-biases/references/integrations.md +700 -0
  1330. package/skills/mlops/evaluation/weights-and-biases/references/sweeps.md +847 -0
  1331. package/skills/mlops/huggingface-hub/SKILL.md +81 -0
  1332. package/skills/mlops/inference/DESCRIPTION.md +3 -0
  1333. package/skills/mlops/inference/llama-cpp/SKILL.md +249 -0
  1334. package/skills/mlops/inference/llama-cpp/references/advanced-usage.md +504 -0
  1335. package/skills/mlops/inference/llama-cpp/references/hub-discovery.md +168 -0
  1336. package/skills/mlops/inference/llama-cpp/references/optimization.md +89 -0
  1337. package/skills/mlops/inference/llama-cpp/references/quantization.md +243 -0
  1338. package/skills/mlops/inference/llama-cpp/references/server.md +150 -0
  1339. package/skills/mlops/inference/llama-cpp/references/troubleshooting.md +442 -0
  1340. package/skills/mlops/inference/obliteratus/SKILL.md +342 -0
  1341. package/skills/mlops/inference/obliteratus/references/analysis-modules.md +166 -0
  1342. package/skills/mlops/inference/obliteratus/references/methods-guide.md +141 -0
  1343. package/skills/mlops/inference/obliteratus/templates/abliteration-config.yaml +33 -0
  1344. package/skills/mlops/inference/obliteratus/templates/analysis-study.yaml +40 -0
  1345. package/skills/mlops/inference/obliteratus/templates/batch-abliteration.yaml +41 -0
  1346. package/skills/mlops/inference/vllm/SKILL.md +372 -0
  1347. package/skills/mlops/inference/vllm/references/optimization.md +226 -0
  1348. package/skills/mlops/inference/vllm/references/quantization.md +284 -0
  1349. package/skills/mlops/inference/vllm/references/server-deployment.md +255 -0
  1350. package/skills/mlops/inference/vllm/references/troubleshooting.md +447 -0
  1351. package/skills/mlops/models/DESCRIPTION.md +3 -0
  1352. package/skills/mlops/models/audiocraft/SKILL.md +568 -0
  1353. package/skills/mlops/models/audiocraft/references/advanced-usage.md +666 -0
  1354. package/skills/mlops/models/audiocraft/references/troubleshooting.md +504 -0
  1355. package/skills/mlops/models/segment-anything/SKILL.md +506 -0
  1356. package/skills/mlops/models/segment-anything/references/advanced-usage.md +589 -0
  1357. package/skills/mlops/models/segment-anything/references/troubleshooting.md +484 -0
  1358. package/skills/mlops/research/DESCRIPTION.md +3 -0
  1359. package/skills/mlops/research/dspy/SKILL.md +594 -0
  1360. package/skills/mlops/research/dspy/references/examples.md +663 -0
  1361. package/skills/mlops/research/dspy/references/modules.md +475 -0
  1362. package/skills/mlops/research/dspy/references/optimizers.md +566 -0
  1363. package/skills/mlops/training/DESCRIPTION.md +3 -0
  1364. package/skills/mlops/vector-databases/DESCRIPTION.md +3 -0
  1365. package/skills/note-taking/DESCRIPTION.md +3 -0
  1366. package/skills/note-taking/obsidian/SKILL.md +61 -0
  1367. package/skills/productivity/DESCRIPTION.md +3 -0
  1368. package/skills/productivity/airtable/SKILL.md +229 -0
  1369. package/skills/productivity/google-workspace/SKILL.md +335 -0
  1370. package/skills/productivity/google-workspace/references/gmail-search-syntax.md +63 -0
  1371. package/skills/productivity/google-workspace/scripts/_hermes_home.py +43 -0
  1372. package/skills/productivity/google-workspace/scripts/google_api.py +1221 -0
  1373. package/skills/productivity/google-workspace/scripts/gws_bridge.py +108 -0
  1374. package/skills/productivity/google-workspace/scripts/setup.py +454 -0
  1375. package/skills/productivity/linear/SKILL.md +380 -0
  1376. package/skills/productivity/linear/scripts/linear_api.py +445 -0
  1377. package/skills/productivity/maps/SKILL.md +195 -0
  1378. package/skills/productivity/maps/scripts/maps_client.py +1298 -0
  1379. package/skills/productivity/nano-pdf/SKILL.md +52 -0
  1380. package/skills/productivity/notion/SKILL.md +448 -0
  1381. package/skills/productivity/notion/references/block-types.md +112 -0
  1382. package/skills/productivity/ocr-and-documents/DESCRIPTION.md +3 -0
  1383. package/skills/productivity/ocr-and-documents/SKILL.md +172 -0
  1384. package/skills/productivity/ocr-and-documents/scripts/extract_marker.py +87 -0
  1385. package/skills/productivity/ocr-and-documents/scripts/extract_pymupdf.py +98 -0
  1386. package/skills/productivity/powerpoint/LICENSE.txt +30 -0
  1387. package/skills/productivity/powerpoint/SKILL.md +237 -0
  1388. package/skills/productivity/powerpoint/editing.md +205 -0
  1389. package/skills/productivity/powerpoint/pptxgenjs.md +420 -0
  1390. package/skills/productivity/powerpoint/scripts/__init__.py +0 -0
  1391. package/skills/productivity/powerpoint/scripts/add_slide.py +195 -0
  1392. package/skills/productivity/powerpoint/scripts/clean.py +286 -0
  1393. package/skills/productivity/powerpoint/scripts/office/helpers/__init__.py +0 -0
  1394. package/skills/productivity/powerpoint/scripts/office/helpers/merge_runs.py +199 -0
  1395. package/skills/productivity/powerpoint/scripts/office/helpers/simplify_redlines.py +197 -0
  1396. package/skills/productivity/powerpoint/scripts/office/pack.py +159 -0
  1397. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  1398. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  1399. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  1400. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  1401. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  1402. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  1403. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  1404. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  1405. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  1406. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  1407. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  1408. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  1409. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  1410. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  1411. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  1412. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  1413. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  1414. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  1415. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  1416. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  1417. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  1418. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  1419. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  1420. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  1421. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  1422. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  1423. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  1424. package/skills/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-contentTypes.xsd +42 -0
  1425. package/skills/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-coreProperties.xsd +50 -0
  1426. package/skills/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-digSig.xsd +49 -0
  1427. package/skills/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-relationships.xsd +33 -0
  1428. package/skills/productivity/powerpoint/scripts/office/schemas/mce/mc.xsd +75 -0
  1429. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  1430. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  1431. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  1432. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  1433. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  1434. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  1435. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  1436. package/skills/productivity/teams-meeting-pipeline/SKILL.md +116 -0
  1437. package/skills/red-teaming/godmode/SKILL.md +404 -0
  1438. package/skills/red-teaming/godmode/references/jailbreak-templates.md +128 -0
  1439. package/skills/red-teaming/godmode/references/refusal-detection.md +142 -0
  1440. package/skills/red-teaming/godmode/scripts/auto_jailbreak.py +769 -0
  1441. package/skills/red-teaming/godmode/scripts/godmode_race.py +530 -0
  1442. package/skills/red-teaming/godmode/scripts/load_godmode.py +45 -0
  1443. package/skills/red-teaming/godmode/scripts/parseltongue.py +550 -0
  1444. package/skills/red-teaming/godmode/templates/prefill-subtle.json +10 -0
  1445. package/skills/red-teaming/godmode/templates/prefill.json +18 -0
  1446. package/skills/research/DESCRIPTION.md +3 -0
  1447. package/skills/research/arxiv/SKILL.md +282 -0
  1448. package/skills/research/arxiv/scripts/search_arxiv.py +114 -0
  1449. package/skills/research/blogwatcher/SKILL.md +137 -0
  1450. package/skills/research/llm-wiki/SKILL.md +507 -0
  1451. package/skills/research/polymarket/SKILL.md +77 -0
  1452. package/skills/research/polymarket/references/api-endpoints.md +220 -0
  1453. package/skills/research/polymarket/scripts/polymarket.py +284 -0
  1454. package/skills/research/research-paper-writing/SKILL.md +2377 -0
  1455. package/skills/research/research-paper-writing/references/autoreason-methodology.md +394 -0
  1456. package/skills/research/research-paper-writing/references/checklists.md +434 -0
  1457. package/skills/research/research-paper-writing/references/citation-workflow.md +564 -0
  1458. package/skills/research/research-paper-writing/references/experiment-patterns.md +728 -0
  1459. package/skills/research/research-paper-writing/references/human-evaluation.md +476 -0
  1460. package/skills/research/research-paper-writing/references/paper-types.md +481 -0
  1461. package/skills/research/research-paper-writing/references/reviewer-guidelines.md +433 -0
  1462. package/skills/research/research-paper-writing/references/sources.md +191 -0
  1463. package/skills/research/research-paper-writing/references/writing-guide.md +474 -0
  1464. package/skills/research/research-paper-writing/templates/README.md +251 -0
  1465. package/skills/research/research-paper-writing/templates/aaai2026/README.md +534 -0
  1466. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
  1467. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026-unified-template.tex +952 -0
  1468. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026.bib +111 -0
  1469. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026.bst +1493 -0
  1470. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026.sty +315 -0
  1471. package/skills/research/research-paper-writing/templates/acl/README.md +50 -0
  1472. package/skills/research/research-paper-writing/templates/acl/acl.sty +312 -0
  1473. package/skills/research/research-paper-writing/templates/acl/acl_latex.tex +377 -0
  1474. package/skills/research/research-paper-writing/templates/acl/acl_lualatex.tex +101 -0
  1475. package/skills/research/research-paper-writing/templates/acl/acl_natbib.bst +1940 -0
  1476. package/skills/research/research-paper-writing/templates/acl/anthology.bib.txt +26 -0
  1477. package/skills/research/research-paper-writing/templates/acl/custom.bib +70 -0
  1478. package/skills/research/research-paper-writing/templates/acl/formatting.md +326 -0
  1479. package/skills/research/research-paper-writing/templates/colm2025/README.md +3 -0
  1480. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.bib +11 -0
  1481. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.bst +1440 -0
  1482. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.pdf +0 -0
  1483. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.sty +218 -0
  1484. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.tex +305 -0
  1485. package/skills/research/research-paper-writing/templates/colm2025/fancyhdr.sty +485 -0
  1486. package/skills/research/research-paper-writing/templates/colm2025/math_commands.tex +508 -0
  1487. package/skills/research/research-paper-writing/templates/colm2025/natbib.sty +1246 -0
  1488. package/skills/research/research-paper-writing/templates/iclr2026/fancyhdr.sty +485 -0
  1489. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.bib +24 -0
  1490. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.bst +1440 -0
  1491. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.pdf +0 -0
  1492. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.sty +246 -0
  1493. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.tex +414 -0
  1494. package/skills/research/research-paper-writing/templates/iclr2026/math_commands.tex +508 -0
  1495. package/skills/research/research-paper-writing/templates/iclr2026/natbib.sty +1246 -0
  1496. package/skills/research/research-paper-writing/templates/icml2026/algorithm.sty +79 -0
  1497. package/skills/research/research-paper-writing/templates/icml2026/algorithmic.sty +201 -0
  1498. package/skills/research/research-paper-writing/templates/icml2026/example_paper.bib +75 -0
  1499. package/skills/research/research-paper-writing/templates/icml2026/example_paper.pdf +0 -0
  1500. package/skills/research/research-paper-writing/templates/icml2026/example_paper.tex +662 -0
  1501. package/skills/research/research-paper-writing/templates/icml2026/fancyhdr.sty +864 -0
  1502. package/skills/research/research-paper-writing/templates/icml2026/icml2026.bst +1443 -0
  1503. package/skills/research/research-paper-writing/templates/icml2026/icml2026.sty +767 -0
  1504. package/skills/research/research-paper-writing/templates/icml2026/icml_numpapers.pdf +0 -0
  1505. package/skills/research/research-paper-writing/templates/neurips2025/Makefile +36 -0
  1506. package/skills/research/research-paper-writing/templates/neurips2025/extra_pkgs.tex +53 -0
  1507. package/skills/research/research-paper-writing/templates/neurips2025/main.tex +38 -0
  1508. package/skills/research/research-paper-writing/templates/neurips2025/neurips.sty +382 -0
  1509. package/skills/smart-home/DESCRIPTION.md +3 -0
  1510. package/skills/smart-home/openhue/SKILL.md +109 -0
  1511. package/skills/social-media/DESCRIPTION.md +3 -0
  1512. package/skills/social-media/xurl/SKILL.md +414 -0
  1513. package/skills/software-development/debugging-hermes-tui-commands/SKILL.md +152 -0
  1514. package/skills/software-development/hermes-agent-skill-authoring/SKILL.md +165 -0
  1515. package/skills/software-development/node-inspect-debugger/SKILL.md +319 -0
  1516. package/skills/software-development/plan/SKILL.md +58 -0
  1517. package/skills/software-development/python-debugpy/SKILL.md +375 -0
  1518. package/skills/software-development/requesting-code-review/SKILL.md +280 -0
  1519. package/skills/software-development/spike/SKILL.md +197 -0
  1520. package/skills/software-development/subagent-driven-development/SKILL.md +352 -0
  1521. package/skills/software-development/subagent-driven-development/references/context-budget-discipline.md +53 -0
  1522. package/skills/software-development/subagent-driven-development/references/gates-taxonomy.md +93 -0
  1523. package/skills/software-development/systematic-debugging/SKILL.md +367 -0
  1524. package/skills/software-development/test-driven-development/SKILL.md +343 -0
  1525. package/skills/software-development/writing-plans/SKILL.md +297 -0
  1526. package/skills/yuanbao/SKILL.md +108 -0
  1527. package/tools/__init__.py +25 -0
  1528. package/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  1529. package/tools/__pycache__/approval.cpython-312.pyc +0 -0
  1530. package/tools/__pycache__/binary_extensions.cpython-312.pyc +0 -0
  1531. package/tools/__pycache__/browser_camofox.cpython-312.pyc +0 -0
  1532. package/tools/__pycache__/browser_camofox_state.cpython-312.pyc +0 -0
  1533. package/tools/__pycache__/browser_cdp_tool.cpython-312.pyc +0 -0
  1534. package/tools/__pycache__/browser_dialog_tool.cpython-312.pyc +0 -0
  1535. package/tools/__pycache__/browser_supervisor.cpython-312.pyc +0 -0
  1536. package/tools/__pycache__/browser_tool.cpython-312.pyc +0 -0
  1537. package/tools/__pycache__/budget_config.cpython-312.pyc +0 -0
  1538. package/tools/__pycache__/checkpoint_manager.cpython-312.pyc +0 -0
  1539. package/tools/__pycache__/clarify_gateway.cpython-312.pyc +0 -0
  1540. package/tools/__pycache__/clarify_tool.cpython-312.pyc +0 -0
  1541. package/tools/__pycache__/code_execution_tool.cpython-312.pyc +0 -0
  1542. package/tools/__pycache__/computer_use_tool.cpython-312.pyc +0 -0
  1543. package/tools/__pycache__/cronjob_tools.cpython-312.pyc +0 -0
  1544. package/tools/__pycache__/debug_helpers.cpython-312.pyc +0 -0
  1545. package/tools/__pycache__/delegate_tool.cpython-312.pyc +0 -0
  1546. package/tools/__pycache__/discord_tool.cpython-312.pyc +0 -0
  1547. package/tools/__pycache__/feishu_doc_tool.cpython-312.pyc +0 -0
  1548. package/tools/__pycache__/feishu_drive_tool.cpython-312.pyc +0 -0
  1549. package/tools/__pycache__/file_operations.cpython-312.pyc +0 -0
  1550. package/tools/__pycache__/file_state.cpython-312.pyc +0 -0
  1551. package/tools/__pycache__/file_tools.cpython-312.pyc +0 -0
  1552. package/tools/__pycache__/homeassistant_tool.cpython-312.pyc +0 -0
  1553. package/tools/__pycache__/image_generation_tool.cpython-312.pyc +0 -0
  1554. package/tools/__pycache__/interrupt.cpython-312.pyc +0 -0
  1555. package/tools/__pycache__/kanban_tools.cpython-312.pyc +0 -0
  1556. package/tools/__pycache__/lazy_deps.cpython-312.pyc +0 -0
  1557. package/tools/__pycache__/managed_tool_gateway.cpython-312.pyc +0 -0
  1558. package/tools/__pycache__/mcp_tool.cpython-312.pyc +0 -0
  1559. package/tools/__pycache__/memory_tool.cpython-312.pyc +0 -0
  1560. package/tools/__pycache__/mixture_of_agents_tool.cpython-312.pyc +0 -0
  1561. package/tools/__pycache__/openrouter_client.cpython-312.pyc +0 -0
  1562. package/tools/__pycache__/process_registry.cpython-312.pyc +0 -0
  1563. package/tools/__pycache__/registry.cpython-312.pyc +0 -0
  1564. package/tools/__pycache__/schema_sanitizer.cpython-312.pyc +0 -0
  1565. package/tools/__pycache__/send_message_tool.cpython-312.pyc +0 -0
  1566. package/tools/__pycache__/session_search_tool.cpython-312.pyc +0 -0
  1567. package/tools/__pycache__/skill_manager_tool.cpython-312.pyc +0 -0
  1568. package/tools/__pycache__/skill_provenance.cpython-312.pyc +0 -0
  1569. package/tools/__pycache__/skill_usage.cpython-312.pyc +0 -0
  1570. package/tools/__pycache__/skills_guard.cpython-312.pyc +0 -0
  1571. package/tools/__pycache__/skills_sync.cpython-312.pyc +0 -0
  1572. package/tools/__pycache__/skills_tool.cpython-312.pyc +0 -0
  1573. package/tools/__pycache__/slash_confirm.cpython-312.pyc +0 -0
  1574. package/tools/__pycache__/terminal_tool.cpython-312.pyc +0 -0
  1575. package/tools/__pycache__/tirith_security.cpython-312.pyc +0 -0
  1576. package/tools/__pycache__/todo_tool.cpython-312.pyc +0 -0
  1577. package/tools/__pycache__/tool_backend_helpers.cpython-312.pyc +0 -0
  1578. package/tools/__pycache__/tool_result_storage.cpython-312.pyc +0 -0
  1579. package/tools/__pycache__/tts_tool.cpython-312.pyc +0 -0
  1580. package/tools/__pycache__/url_safety.cpython-312.pyc +0 -0
  1581. package/tools/__pycache__/video_generation_tool.cpython-312.pyc +0 -0
  1582. package/tools/__pycache__/vision_tools.cpython-312.pyc +0 -0
  1583. package/tools/__pycache__/voice_mode.cpython-312.pyc +0 -0
  1584. package/tools/__pycache__/web_tools.cpython-312.pyc +0 -0
  1585. package/tools/__pycache__/website_policy.cpython-312.pyc +0 -0
  1586. package/tools/__pycache__/x_search_tool.cpython-312.pyc +0 -0
  1587. package/tools/__pycache__/xai_http.cpython-312.pyc +0 -0
  1588. package/tools/__pycache__/yuanbao_tools.cpython-312.pyc +0 -0
  1589. package/tools/ansi_strip.py +44 -0
  1590. package/tools/approval.py +1392 -0
  1591. package/tools/binary_extensions.py +42 -0
  1592. package/tools/browser_camofox.py +700 -0
  1593. package/tools/browser_camofox_state.py +48 -0
  1594. package/tools/browser_cdp_tool.py +569 -0
  1595. package/tools/browser_dialog_tool.py +148 -0
  1596. package/tools/browser_providers/__init__.py +10 -0
  1597. package/tools/browser_providers/__pycache__/__init__.cpython-312.pyc +0 -0
  1598. package/tools/browser_providers/__pycache__/base.cpython-312.pyc +0 -0
  1599. package/tools/browser_providers/__pycache__/browser_use.cpython-312.pyc +0 -0
  1600. package/tools/browser_providers/__pycache__/browserbase.cpython-312.pyc +0 -0
  1601. package/tools/browser_providers/__pycache__/firecrawl.cpython-312.pyc +0 -0
  1602. package/tools/browser_providers/base.py +59 -0
  1603. package/tools/browser_providers/browser_use.py +225 -0
  1604. package/tools/browser_providers/browserbase.py +222 -0
  1605. package/tools/browser_providers/firecrawl.py +112 -0
  1606. package/tools/browser_supervisor.py +1457 -0
  1607. package/tools/browser_tool.py +3676 -0
  1608. package/tools/budget_config.py +51 -0
  1609. package/tools/checkpoint_manager.py +1639 -0
  1610. package/tools/clarify_gateway.py +278 -0
  1611. package/tools/clarify_tool.py +141 -0
  1612. package/tools/code_execution_tool.py +1782 -0
  1613. package/tools/computer_use/__init__.py +43 -0
  1614. package/tools/computer_use/__pycache__/__init__.cpython-312.pyc +0 -0
  1615. package/tools/computer_use/__pycache__/backend.cpython-312.pyc +0 -0
  1616. package/tools/computer_use/__pycache__/schema.cpython-312.pyc +0 -0
  1617. package/tools/computer_use/__pycache__/tool.cpython-312.pyc +0 -0
  1618. package/tools/computer_use/backend.py +150 -0
  1619. package/tools/computer_use/cua_backend.py +682 -0
  1620. package/tools/computer_use/schema.py +191 -0
  1621. package/tools/computer_use/tool.py +521 -0
  1622. package/tools/computer_use_tool.py +39 -0
  1623. package/tools/credential_files.py +437 -0
  1624. package/tools/cronjob_tools.py +719 -0
  1625. package/tools/debug_helpers.py +106 -0
  1626. package/tools/delegate_tool.py +2797 -0
  1627. package/tools/discord_tool.py +959 -0
  1628. package/tools/env_passthrough.py +145 -0
  1629. package/tools/environments/__init__.py +14 -0
  1630. package/tools/environments/__pycache__/__init__.cpython-312.pyc +0 -0
  1631. package/tools/environments/__pycache__/base.cpython-312.pyc +0 -0
  1632. package/tools/environments/__pycache__/docker.cpython-312.pyc +0 -0
  1633. package/tools/environments/__pycache__/file_sync.cpython-312.pyc +0 -0
  1634. package/tools/environments/__pycache__/local.cpython-312.pyc +0 -0
  1635. package/tools/environments/__pycache__/managed_modal.cpython-312.pyc +0 -0
  1636. package/tools/environments/__pycache__/modal.cpython-312.pyc +0 -0
  1637. package/tools/environments/__pycache__/modal_utils.cpython-312.pyc +0 -0
  1638. package/tools/environments/__pycache__/singularity.cpython-312.pyc +0 -0
  1639. package/tools/environments/__pycache__/ssh.cpython-312.pyc +0 -0
  1640. package/tools/environments/base.py +844 -0
  1641. package/tools/environments/daytona.py +270 -0
  1642. package/tools/environments/docker.py +656 -0
  1643. package/tools/environments/file_sync.py +400 -0
  1644. package/tools/environments/local.py +658 -0
  1645. package/tools/environments/managed_modal.py +282 -0
  1646. package/tools/environments/modal.py +479 -0
  1647. package/tools/environments/modal_utils.py +199 -0
  1648. package/tools/environments/singularity.py +263 -0
  1649. package/tools/environments/ssh.py +295 -0
  1650. package/tools/environments/vercel_sandbox.py +655 -0
  1651. package/tools/feishu_doc_tool.py +138 -0
  1652. package/tools/feishu_drive_tool.py +431 -0
  1653. package/tools/file_operations.py +1825 -0
  1654. package/tools/file_state.py +332 -0
  1655. package/tools/file_tools.py +1172 -0
  1656. package/tools/fuzzy_match.py +703 -0
  1657. package/tools/homeassistant_tool.py +513 -0
  1658. package/tools/image_generation_tool.py +1098 -0
  1659. package/tools/interrupt.py +98 -0
  1660. package/tools/kanban_tools.py +1139 -0
  1661. package/tools/lazy_deps.py +608 -0
  1662. package/tools/managed_tool_gateway.py +168 -0
  1663. package/tools/mcp_oauth.py +633 -0
  1664. package/tools/mcp_oauth_manager.py +607 -0
  1665. package/tools/mcp_tool.py +3483 -0
  1666. package/tools/memory_tool.py +584 -0
  1667. package/tools/microsoft_graph_auth.py +245 -0
  1668. package/tools/microsoft_graph_client.py +408 -0
  1669. package/tools/mixture_of_agents_tool.py +542 -0
  1670. package/tools/neutts_samples/jo.txt +1 -0
  1671. package/tools/neutts_samples/jo.wav +0 -0
  1672. package/tools/neutts_synth.py +104 -0
  1673. package/tools/openrouter_client.py +33 -0
  1674. package/tools/osv_check.py +155 -0
  1675. package/tools/patch_parser.py +592 -0
  1676. package/tools/path_security.py +43 -0
  1677. package/tools/process_registry.py +1534 -0
  1678. package/tools/registry.py +589 -0
  1679. package/tools/schema_sanitizer.py +370 -0
  1680. package/tools/send_message_tool.py +1900 -0
  1681. package/tools/session_search_tool.py +613 -0
  1682. package/tools/skill_manager_tool.py +932 -0
  1683. package/tools/skill_provenance.py +78 -0
  1684. package/tools/skill_usage.py +610 -0
  1685. package/tools/skills_guard.py +932 -0
  1686. package/tools/skills_hub.py +3263 -0
  1687. package/tools/skills_sync.py +432 -0
  1688. package/tools/skills_tool.py +1569 -0
  1689. package/tools/slash_confirm.py +167 -0
  1690. package/tools/terminal_tool.py +2376 -0
  1691. package/tools/tirith_security.py +775 -0
  1692. package/tools/todo_tool.py +277 -0
  1693. package/tools/tool_backend_helpers.py +144 -0
  1694. package/tools/tool_output_limits.py +92 -0
  1695. package/tools/tool_result_storage.py +232 -0
  1696. package/tools/transcription_tools.py +936 -0
  1697. package/tools/tts_tool.py +2285 -0
  1698. package/tools/url_safety.py +330 -0
  1699. package/tools/video_generation_tool.py +561 -0
  1700. package/tools/vision_tools.py +1422 -0
  1701. package/tools/voice_mode.py +1019 -0
  1702. package/tools/web_tools.py +1551 -0
  1703. package/tools/website_policy.py +283 -0
  1704. package/tools/x_search_tool.py +424 -0
  1705. package/tools/xai_http.py +83 -0
  1706. package/tools/yuanbao_tools.py +736 -0
  1707. package/toolset_distributions.py +364 -0
  1708. package/toolsets.py +866 -0
  1709. package/trajectory_compressor.py +1509 -0
  1710. package/tui_gateway/__init__.py +0 -0
  1711. package/tui_gateway/entry.py +251 -0
  1712. package/tui_gateway/event_publisher.py +126 -0
  1713. package/tui_gateway/render.py +49 -0
  1714. package/tui_gateway/server.py +6623 -0
  1715. package/tui_gateway/slash_worker.py +76 -0
  1716. package/tui_gateway/transport.py +219 -0
  1717. package/tui_gateway/ws.py +178 -0
  1718. package/utils.py +361 -0
@@ -0,0 +1,2777 @@
1
+ """Matrix gateway adapter.
2
+
3
+ Connects to any Matrix homeserver (self-hosted or matrix.org) via the
4
+ mautrix Python SDK. Supports optional end-to-end encryption (E2EE)
5
+ when installed with ``pip install "mautrix[encryption]"``.
6
+
7
+ Environment variables:
8
+ MATRIX_HOMESERVER Homeserver URL (e.g. https://matrix.example.org)
9
+ MATRIX_ACCESS_TOKEN Access token (preferred auth method)
10
+ MATRIX_USER_ID Full user ID (@bot:server) — required for password login
11
+ MATRIX_PASSWORD Password (alternative to access token)
12
+ MATRIX_ENCRYPTION Set "true" to enable E2EE
13
+ MATRIX_DEVICE_ID Stable device ID for E2EE persistence across restarts
14
+ MATRIX_PROXY HTTP(S) or SOCKS proxy URL for Matrix traffic
15
+ MATRIX_ALLOWED_USERS Comma-separated Matrix user IDs (@user:server)
16
+ MATRIX_HOME_ROOM Room ID for cron/notification delivery
17
+ MATRIX_REACTIONS Set "false" to disable processing lifecycle reactions
18
+ (eyes/checkmark/cross). Default: true
19
+ MATRIX_REQUIRE_MENTION Require @mention in rooms (default: true)
20
+ MATRIX_FREE_RESPONSE_ROOMS Comma-separated room IDs exempt from mention requirement (alias of matrix.free_response_rooms)
21
+ MATRIX_ALLOWED_ROOMS Comma-separated room IDs; if set, bot ONLY responds in these rooms (whitelist, DMs exempt; alias of matrix.allowed_rooms)
22
+ MATRIX_AUTO_THREAD Auto-create threads for room messages (default: true)
23
+ MATRIX_DM_AUTO_THREAD Auto-create threads for DM messages (default: false)
24
+ MATRIX_RECOVERY_KEY Recovery key for cross-signing verification after device key rotation
25
+ MATRIX_DM_MENTION_THREADS Create a thread when bot is @mentioned in a DM (default: false)
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import asyncio
31
+ import logging
32
+ import mimetypes
33
+ import os
34
+ import re
35
+ import time
36
+ from dataclasses import dataclass
37
+
38
+ from html import escape as _html_escape
39
+ from pathlib import Path
40
+ from typing import Any, Dict, Optional, Set
41
+
42
+ try:
43
+ from mautrix.types import (
44
+ ContentURI,
45
+ EventID,
46
+ EventType,
47
+ PaginationDirection,
48
+ PresenceState,
49
+ RoomCreatePreset,
50
+ RoomID,
51
+ SyncToken,
52
+ TrustState,
53
+ UserID,
54
+ )
55
+ except ImportError:
56
+ # Stubs so the module is importable without mautrix installed.
57
+ # check_matrix_requirements() will return False and the adapter
58
+ # won't be instantiated in production, but tests may exercise
59
+ # adapter methods so stubs must have the right attributes.
60
+ ContentURI = EventID = RoomID = SyncToken = UserID = str # type: ignore[misc,assignment]
61
+
62
+ class _EventTypeStub: # type: ignore[no-redef]
63
+ ROOM_MESSAGE = "m.room.message"
64
+ REACTION = "m.reaction"
65
+ ROOM_ENCRYPTED = "m.room.encrypted"
66
+ ROOM_NAME = "m.room.name"
67
+
68
+ EventType = _EventTypeStub # type: ignore[misc,assignment]
69
+
70
+ class _PaginationDirectionStub: # type: ignore[no-redef]
71
+ BACKWARD = "b"
72
+ FORWARD = "f"
73
+
74
+ PaginationDirection = _PaginationDirectionStub # type: ignore[misc,assignment]
75
+
76
+ class _PresenceStateStub: # type: ignore[no-redef]
77
+ ONLINE = "online"
78
+ OFFLINE = "offline"
79
+ UNAVAILABLE = "unavailable"
80
+
81
+ PresenceState = _PresenceStateStub # type: ignore[misc,assignment]
82
+
83
+ class _RoomCreatePresetStub: # type: ignore[no-redef]
84
+ PRIVATE = "private_chat"
85
+ PUBLIC = "public_chat"
86
+ TRUSTED_PRIVATE = "trusted_private_chat"
87
+
88
+ RoomCreatePreset = _RoomCreatePresetStub # type: ignore[misc,assignment]
89
+
90
+ class _TrustStateStub: # type: ignore[no-redef]
91
+ UNVERIFIED = 0
92
+ VERIFIED = 1
93
+
94
+ TrustState = _TrustStateStub # type: ignore[misc,assignment]
95
+
96
+ from gateway.config import Platform, PlatformConfig
97
+ from gateway.platforms.base import (
98
+ BasePlatformAdapter,
99
+ MessageEvent,
100
+ MessageType,
101
+ ProcessingOutcome,
102
+ SendResult,
103
+ resolve_proxy_url,
104
+ proxy_kwargs_for_aiohttp,
105
+ )
106
+ from gateway.platforms.helpers import ThreadParticipationTracker
107
+
108
+ logger = logging.getLogger(__name__)
109
+
110
+
111
+ @dataclass
112
+ class _MatrixApprovalPrompt:
113
+ """Tracks a pending Matrix reaction-based exec approval prompt."""
114
+
115
+ def __init__(self, session_key: str, chat_id: str, message_id: str, resolved: bool = False):
116
+ self.session_key = session_key
117
+ self.chat_id = chat_id
118
+ self.message_id = message_id
119
+ self.resolved = resolved
120
+ self.bot_reaction_events: dict[str, str] = {} # emoji -> event_id
121
+
122
+ # Matrix message size limit (4000 chars practical, spec has no hard limit
123
+ # but clients render poorly above this).
124
+ MAX_MESSAGE_LENGTH = 4000
125
+
126
+ # Store directory for E2EE keys and sync state.
127
+ # Uses get_hermes_home() so each profile gets its own Matrix store.
128
+ from calvyn_constants import get_hermes_dir as _get_hermes_dir
129
+
130
+ _STORE_DIR = _get_hermes_dir("platforms/matrix/store", "matrix/store")
131
+ _CRYPTO_DB_PATH = _STORE_DIR / "crypto.db"
132
+
133
+ # Grace period: ignore messages older than this many seconds before startup.
134
+ _STARTUP_GRACE_SECONDS = 5
135
+
136
+ _OUTBOUND_MENTION_RE = re.compile(
137
+ r"(?<![\w/])(@[0-9A-Za-z._=/-]+:[0-9A-Za-z.-]+(?::\d+)?)"
138
+ )
139
+
140
+ _E2EE_INSTALL_HINT = (
141
+ "Install with: pip install 'mautrix[encryption]' (requires libolm C library)"
142
+ )
143
+
144
+ _MATRIX_IMAGE_FILENAME_EXTS = frozenset({
145
+ ".jpg",
146
+ ".jpeg",
147
+ ".png",
148
+ ".gif",
149
+ ".webp",
150
+ ".bmp",
151
+ ".svg",
152
+ ".heic",
153
+ ".heif",
154
+ ".avif",
155
+ })
156
+
157
+
158
+ def _looks_like_matrix_image_filename(text: str) -> bool:
159
+ """Return True when Matrix image body text is probably just a transport filename.
160
+
161
+ Matrix ``m.image`` events commonly populate ``content.body`` with the uploaded
162
+ filename when the user did not add a caption. Treating that raw filename as
163
+ user-authored text confuses downstream vision enrichment.
164
+ """
165
+ candidate = str(text or "").strip()
166
+ if not candidate or "\n" in candidate or candidate.endswith("/"):
167
+ return False
168
+
169
+ name = Path(candidate).name
170
+ if not name or name != candidate:
171
+ return False
172
+
173
+ suffix = Path(name).suffix.lower()
174
+ if not suffix:
175
+ return False
176
+
177
+ guessed_type, _ = mimetypes.guess_type(name)
178
+ if guessed_type and guessed_type.startswith("image/"):
179
+ return True
180
+ return suffix in _MATRIX_IMAGE_FILENAME_EXTS
181
+
182
+
183
+ def _create_matrix_session(proxy_url: str | None):
184
+ """Create an ``aiohttp.ClientSession`` whose proxy applies to *all* requests.
185
+
186
+ mautrix's ``HTTPAPI._send()`` calls ``session.request()`` without forwarding
187
+ per-request ``proxy=`` kwargs. For HTTP(S) proxies we use aiohttp's native
188
+ ``proxy=`` session parameter which sets a default for every request. For SOCKS
189
+ we use ``aiohttp_socks.ProxyConnector`` (connector-level).
190
+ When no proxy is configured we enable ``trust_env`` so standard env vars
191
+ (``HTTP_PROXY`` / ``HTTPS_PROXY``) are honoured automatically.
192
+ """
193
+ import aiohttp
194
+
195
+ if not proxy_url:
196
+ return aiohttp.ClientSession(trust_env=True)
197
+
198
+ if proxy_url.split("://")[0].lower().startswith("socks"):
199
+ try:
200
+ from aiohttp_socks import ProxyConnector
201
+
202
+ return aiohttp.ClientSession(
203
+ connector=ProxyConnector.from_url(proxy_url, rdns=True),
204
+ )
205
+ except ImportError:
206
+ logger.warning(
207
+ "aiohttp_socks not installed — SOCKS proxy %s ignored. "
208
+ "Run: pip install aiohttp-socks",
209
+ proxy_url,
210
+ )
211
+ return aiohttp.ClientSession(trust_env=True)
212
+
213
+ return aiohttp.ClientSession(proxy=proxy_url)
214
+
215
+
216
+ def _check_e2ee_deps() -> bool:
217
+ """Return True if mautrix E2EE dependencies (python-olm) are available."""
218
+ try:
219
+ from mautrix.crypto import OlmMachine # noqa: F401
220
+
221
+ return True
222
+ except (ImportError, AttributeError):
223
+ return False
224
+
225
+
226
+ def check_matrix_requirements() -> bool:
227
+ """Return True if the Matrix adapter can be used.
228
+
229
+ Lazy-installs mautrix via ``tools.lazy_deps.ensure("platform.matrix")``
230
+ on first call if not present. Rebinds all module-level type globals on success.
231
+ """
232
+ token = os.getenv("MATRIX_ACCESS_TOKEN", "")
233
+ password = os.getenv("MATRIX_PASSWORD", "")
234
+ homeserver = os.getenv("MATRIX_HOMESERVER", "")
235
+
236
+ if not token and not password:
237
+ logger.debug("Matrix: neither MATRIX_ACCESS_TOKEN nor MATRIX_PASSWORD set")
238
+ return False
239
+ if not homeserver:
240
+ logger.warning("Matrix: MATRIX_HOMESERVER not set")
241
+ return False
242
+ try:
243
+ import mautrix # noqa: F401
244
+ except ImportError:
245
+ def _import():
246
+ from mautrix.types import (
247
+ ContentURI, EventID, EventType, PaginationDirection,
248
+ PresenceState, RoomCreatePreset, RoomID, SyncToken,
249
+ TrustState, UserID,
250
+ )
251
+ return {
252
+ "ContentURI": ContentURI,
253
+ "EventID": EventID,
254
+ "EventType": EventType,
255
+ "PaginationDirection": PaginationDirection,
256
+ "PresenceState": PresenceState,
257
+ "RoomCreatePreset": RoomCreatePreset,
258
+ "RoomID": RoomID,
259
+ "SyncToken": SyncToken,
260
+ "TrustState": TrustState,
261
+ "UserID": UserID,
262
+ }
263
+
264
+ from tools.lazy_deps import ensure_and_bind
265
+ if not ensure_and_bind("platform.matrix", _import, globals(), prompt=False):
266
+ logger.warning(
267
+ "Matrix: mautrix not installed. Run: pip install 'mautrix[encryption]'"
268
+ )
269
+ return False
270
+
271
+ # If encryption is requested, verify E2EE deps are available at startup
272
+ # rather than silently degrading to plaintext-only at connect time.
273
+ encryption_requested = os.getenv("MATRIX_ENCRYPTION", "").lower() in {
274
+ "true",
275
+ "1",
276
+ "yes",
277
+ }
278
+ if encryption_requested and not _check_e2ee_deps():
279
+ logger.error(
280
+ "Matrix: MATRIX_ENCRYPTION=true but E2EE dependencies are missing. %s. "
281
+ "Without this, encrypted rooms will not work. "
282
+ "Set MATRIX_ENCRYPTION=false to disable E2EE.",
283
+ _E2EE_INSTALL_HINT,
284
+ )
285
+ return False
286
+
287
+ return True
288
+
289
+
290
+ class _CryptoStateStore:
291
+ """Adapter that satisfies the mautrix crypto StateStore interface.
292
+
293
+ OlmMachine requires a StateStore with ``is_encrypted``,
294
+ ``get_encryption_info``, and ``find_shared_rooms``. The basic
295
+ ``MemoryStateStore`` from ``mautrix.client`` doesn't implement these,
296
+ so we provide simple implementations that consult the client's room
297
+ state.
298
+ """
299
+
300
+ def __init__(self, client_state_store: Any, joined_rooms: set):
301
+ self._ss = client_state_store
302
+ self._joined_rooms = joined_rooms
303
+
304
+ async def is_encrypted(self, room_id: str) -> bool:
305
+ return (await self.get_encryption_info(room_id)) is not None
306
+
307
+ async def get_encryption_info(self, room_id: str):
308
+ if hasattr(self._ss, "get_encryption_info"):
309
+ return await self._ss.get_encryption_info(room_id)
310
+ return None
311
+
312
+ async def find_shared_rooms(self, user_id: str) -> list:
313
+ # Return all joined rooms — simple but correct for a single-user bot.
314
+ return list(self._joined_rooms)
315
+
316
+
317
+ class MatrixAdapter(BasePlatformAdapter):
318
+ """Gateway adapter for Matrix (any homeserver)."""
319
+
320
+ # Threshold for detecting Matrix client-side message splits.
321
+ # When a chunk is near the ~4000-char practical limit, a continuation
322
+ # is almost certain.
323
+ _SPLIT_THRESHOLD = 3900
324
+
325
+ def __init__(self, config: PlatformConfig):
326
+ super().__init__(config, Platform.MATRIX)
327
+
328
+ self._homeserver: str = (
329
+ config.extra.get("homeserver", "") or os.getenv("MATRIX_HOMESERVER", "")
330
+ ).rstrip("/")
331
+ self._access_token: str = config.token or os.getenv("MATRIX_ACCESS_TOKEN", "")
332
+ self._user_id: str = config.extra.get("user_id", "") or os.getenv(
333
+ "MATRIX_USER_ID", ""
334
+ )
335
+ self._password: str = config.extra.get("password", "") or os.getenv(
336
+ "MATRIX_PASSWORD", ""
337
+ )
338
+ self._encryption: bool = config.extra.get(
339
+ "encryption",
340
+ os.getenv("MATRIX_ENCRYPTION", "").lower() in {"true", "1", "yes"},
341
+ )
342
+ self._device_id: str = config.extra.get("device_id", "") or os.getenv(
343
+ "MATRIX_DEVICE_ID", ""
344
+ )
345
+
346
+ self._client: Any = None # mautrix.client.Client
347
+ self._crypto_db: Any = None # mautrix.util.async_db.Database
348
+ self._sync_task: Optional[asyncio.Task] = None
349
+ self._closing = False
350
+ self._startup_ts: float = 0.0
351
+
352
+ # Cache: room_id → bool (is DM)
353
+ self._dm_rooms: Dict[str, bool] = {}
354
+ # Set of room IDs we've joined
355
+ self._joined_rooms: Set[str] = set()
356
+ # Event deduplication (bounded deque keeps newest entries)
357
+ from collections import deque
358
+
359
+ self._processed_events: deque = deque(maxlen=1000)
360
+ self._processed_events_set: set = set()
361
+
362
+ # Buffer for undecrypted events pending key receipt.
363
+ # Each entry: (room_id, event, timestamp)
364
+
365
+ # Thread participation tracking (for require_mention bypass)
366
+ self._threads = ThreadParticipationTracker("matrix")
367
+
368
+ # Mention/thread gating — parsed once from env vars.
369
+ self._require_mention: bool = os.getenv(
370
+ "MATRIX_REQUIRE_MENTION", "true"
371
+ ).lower() not in {"false", "0", "no"}
372
+ free_rooms_raw = config.extra.get("free_response_rooms")
373
+ if free_rooms_raw is None:
374
+ free_rooms_raw = os.getenv("MATRIX_FREE_RESPONSE_ROOMS", "")
375
+ if isinstance(free_rooms_raw, list):
376
+ self._free_rooms: Set[str] = {
377
+ str(r).strip() for r in free_rooms_raw if str(r).strip()
378
+ }
379
+ else:
380
+ self._free_rooms: Set[str] = {
381
+ r.strip() for r in str(free_rooms_raw).split(",") if r.strip()
382
+ }
383
+ # If non-empty, bot ONLY responds in these rooms (whitelist); DMs exempt.
384
+ allowed_rooms_raw = config.extra.get("allowed_rooms")
385
+ if allowed_rooms_raw is None:
386
+ allowed_rooms_raw = os.getenv("MATRIX_ALLOWED_ROOMS", "")
387
+ if isinstance(allowed_rooms_raw, list):
388
+ self._allowed_rooms: Set[str] = {
389
+ str(r).strip() for r in allowed_rooms_raw if str(r).strip()
390
+ }
391
+ else:
392
+ self._allowed_rooms: Set[str] = {
393
+ r.strip() for r in str(allowed_rooms_raw).split(",") if r.strip()
394
+ }
395
+ self._auto_thread: bool = os.getenv("MATRIX_AUTO_THREAD", "true").lower() in {
396
+ "true",
397
+ "1",
398
+ "yes",
399
+ }
400
+ self._dm_auto_thread: bool = os.getenv(
401
+ "MATRIX_DM_AUTO_THREAD", "false"
402
+ ).lower() in {"true", "1", "yes"}
403
+ self._dm_mention_threads: bool = os.getenv(
404
+ "MATRIX_DM_MENTION_THREADS", "false"
405
+ ).lower() in {"true", "1", "yes"}
406
+
407
+ # Reactions: configurable via MATRIX_REACTIONS (default: true).
408
+ self._reactions_enabled: bool = os.getenv(
409
+ "MATRIX_REACTIONS", "true"
410
+ ).lower() not in {"false", "0", "no"}
411
+ self._pending_reactions: dict[tuple[str, str], str] = {}
412
+ # Delay before redacting reactions so Matrix homeservers have time to
413
+ # deliver the final message event without tripping "missing event"
414
+ # errors in some clients. 5s is empirically safe; not user-tunable —
415
+ # if that changes, add a config.yaml entry rather than an env var.
416
+ self._reaction_redaction_delay_seconds = 5.0
417
+ self._reaction_redaction_tasks: Set[asyncio.Task] = set()
418
+
419
+ # Proxy support — resolve once at init, reuse for all HTTP traffic.
420
+ self._proxy_url: str | None = resolve_proxy_url(platform_env_var="MATRIX_PROXY")
421
+ if self._proxy_url:
422
+ logger.info("Matrix: proxy configured — %s", self._proxy_url)
423
+
424
+ # Text batching: merge rapid successive messages (Telegram-style).
425
+ # Matrix clients split long messages around 4000 chars.
426
+ self._text_batch_delay_seconds = float(
427
+ os.getenv("HERMES_MATRIX_TEXT_BATCH_DELAY_SECONDS", "0.6")
428
+ )
429
+ self._text_batch_split_delay_seconds = float(
430
+ os.getenv("HERMES_MATRIX_TEXT_BATCH_SPLIT_DELAY_SECONDS", "2.0")
431
+ )
432
+ self._pending_text_batches: Dict[str, MessageEvent] = {}
433
+ self._pending_text_batch_tasks: Dict[str, asyncio.Task] = {}
434
+
435
+ # Matrix reaction-based dangerous command approvals.
436
+ self._approval_reaction_map = {
437
+ "✅": "once",
438
+ "❎": "deny",
439
+ }
440
+ self._approval_prompts_by_event: Dict[str, _MatrixApprovalPrompt] = {}
441
+ self._approval_prompt_by_session: Dict[str, str] = {}
442
+ allowed_users_raw = os.getenv("MATRIX_ALLOWED_USERS", "")
443
+ self._allowed_user_ids: Set[str] = {
444
+ u.strip() for u in allowed_users_raw.split(",") if u.strip()
445
+ }
446
+
447
+ def _is_duplicate_event(self, event_id) -> bool:
448
+ """Return True if this event was already processed. Tracks the ID otherwise."""
449
+ if not event_id:
450
+ return False
451
+ if event_id in self._processed_events_set:
452
+ return True
453
+ if len(self._processed_events) == self._processed_events.maxlen:
454
+ evicted = self._processed_events[0]
455
+ self._processed_events_set.discard(evicted)
456
+ self._processed_events.append(event_id)
457
+ self._processed_events_set.add(event_id)
458
+ return False
459
+
460
+ # ------------------------------------------------------------------
461
+ # E2EE helpers
462
+ # ------------------------------------------------------------------
463
+
464
+ @staticmethod
465
+ def _extract_server_ed25519(device_keys_obj: Any) -> Optional[str]:
466
+ """Extract the ed25519 identity key from a DeviceKeys object."""
467
+ for kid, kval in (getattr(device_keys_obj, "keys", {}) or {}).items():
468
+ if str(kid).startswith("ed25519:"):
469
+ return str(kval)
470
+ return None
471
+
472
+ async def _reverify_keys_after_upload(
473
+ self, client: Any, local_ed25519: str
474
+ ) -> bool:
475
+ """Re-query the server after share_keys() and verify our ed25519 key matches."""
476
+ try:
477
+ resp = await client.query_keys({client.mxid: [client.device_id]})
478
+ dk = getattr(resp, "device_keys", {}) or {}
479
+ ud = dk.get(str(client.mxid)) or {}
480
+ dev = ud.get(str(client.device_id))
481
+ if dev:
482
+ server_ed = self._extract_server_ed25519(dev)
483
+ if server_ed != local_ed25519:
484
+ logger.error(
485
+ "Matrix: device %s has immutable identity keys that "
486
+ "don't match this installation. Generate a new access "
487
+ "token with a fresh device.",
488
+ client.device_id,
489
+ )
490
+ return False
491
+ except Exception as exc:
492
+ logger.error("Matrix: post-upload key verification failed: %s", exc, exc_info=True)
493
+ return False
494
+ return True
495
+
496
+ async def _verify_device_keys_on_server(self, client: Any, olm: Any) -> bool:
497
+ """Verify our device keys are on the homeserver after loading crypto state.
498
+
499
+ Returns True if keys are valid or were successfully re-uploaded.
500
+ Returns False if verification fails (caller should refuse E2EE).
501
+ """
502
+ try:
503
+ resp = await client.query_keys({client.mxid: [client.device_id]})
504
+ except Exception as exc:
505
+ logger.error(
506
+ "Matrix: cannot verify device keys on server: %s — refusing E2EE",
507
+ exc,
508
+ exc_info=True,
509
+ )
510
+ return False
511
+
512
+ device_keys_map = getattr(resp, "device_keys", {}) or {}
513
+ our_user_devices = device_keys_map.get(str(client.mxid)) or {}
514
+ our_keys = our_user_devices.get(str(client.device_id))
515
+ local_ed25519 = olm.account.identity_keys.get("ed25519")
516
+
517
+ if not our_keys:
518
+ logger.warning("Matrix: device keys missing from server — re-uploading")
519
+ olm.account.shared = False
520
+ try:
521
+ await olm.share_keys()
522
+ except Exception as exc:
523
+ logger.error("Matrix: failed to re-upload device keys: %s", exc, exc_info=True)
524
+ return False
525
+ return await self._reverify_keys_after_upload(client, local_ed25519)
526
+
527
+ server_ed25519 = self._extract_server_ed25519(our_keys)
528
+
529
+ if server_ed25519 != local_ed25519:
530
+ if olm.account.shared:
531
+ logger.error(
532
+ "Matrix: server has different identity keys for device %s — "
533
+ "local crypto state is stale. Delete %s and restart.",
534
+ client.device_id,
535
+ _CRYPTO_DB_PATH,
536
+ )
537
+ return False
538
+
539
+ logger.warning(
540
+ "Matrix: server has stale keys for device %s — attempting re-upload",
541
+ client.device_id,
542
+ )
543
+ try:
544
+ await client.api.request(
545
+ client.api.Method.DELETE
546
+ if hasattr(client.api, "Method")
547
+ else "DELETE",
548
+ f"/_matrix/client/v3/devices/{client.device_id}",
549
+ )
550
+ logger.info(
551
+ "Matrix: deleted stale device %s from server", client.device_id
552
+ )
553
+ except Exception:
554
+ pass
555
+ try:
556
+ await olm.share_keys()
557
+ except Exception as exc:
558
+ logger.error(
559
+ "Matrix: cannot upload device keys for %s: %s. "
560
+ "Try generating a new access token to get a fresh device.",
561
+ client.device_id,
562
+ exc,
563
+ exc_info=True,
564
+ )
565
+ return False
566
+ return await self._reverify_keys_after_upload(client, local_ed25519)
567
+
568
+ return True
569
+
570
+ # ------------------------------------------------------------------
571
+ # Required overrides
572
+ # ------------------------------------------------------------------
573
+
574
+ async def connect(self) -> bool:
575
+ """Connect to the Matrix homeserver and start syncing."""
576
+ from mautrix.api import HTTPAPI
577
+ from mautrix.client import Client
578
+ from mautrix.client.state_store import MemoryStateStore, MemorySyncStore
579
+
580
+ if not self._homeserver:
581
+ logger.error("Matrix: homeserver URL not configured")
582
+ return False
583
+
584
+ # Ensure store dir exists for E2EE key persistence.
585
+ _STORE_DIR.mkdir(parents=True, exist_ok=True)
586
+
587
+ # Create the HTTP API layer.
588
+ client_session = _create_matrix_session(self._proxy_url)
589
+ api = HTTPAPI(
590
+ base_url=self._homeserver,
591
+ token=self._access_token or "",
592
+ client_session=client_session,
593
+ )
594
+
595
+ # Create the client.
596
+ state_store = MemoryStateStore()
597
+ sync_store = MemorySyncStore()
598
+ client = Client(
599
+ mxid=UserID(self._user_id) if self._user_id else UserID(""),
600
+ device_id=self._device_id or None,
601
+ api=api,
602
+ state_store=state_store,
603
+ sync_store=sync_store,
604
+ )
605
+
606
+ self._client = client
607
+
608
+ # Authenticate.
609
+ if self._access_token:
610
+ api.token = self._access_token
611
+
612
+ # Validate the token and learn user_id / device_id.
613
+ try:
614
+ resp = await client.whoami()
615
+ resolved_user_id = getattr(resp, "user_id", "") or self._user_id
616
+ resolved_device_id = getattr(resp, "device_id", "")
617
+ if resolved_user_id:
618
+ self._user_id = str(resolved_user_id)
619
+ client.mxid = UserID(self._user_id)
620
+
621
+ # Prefer user-configured device_id for stable E2EE identity.
622
+ effective_device_id = self._device_id or resolved_device_id
623
+ if effective_device_id:
624
+ client.device_id = effective_device_id
625
+
626
+ logger.info(
627
+ "Matrix: using access token for %s%s",
628
+ self._user_id or "(unknown user)",
629
+ f" (device {effective_device_id})" if effective_device_id else "",
630
+ )
631
+ except Exception as exc:
632
+ logger.error(
633
+ "Matrix: whoami failed — check MATRIX_ACCESS_TOKEN and MATRIX_HOMESERVER: %s",
634
+ exc,
635
+ exc_info=True,
636
+ )
637
+ await api.session.close()
638
+ return False
639
+ elif self._password and self._user_id:
640
+ try:
641
+ resp = await client.login(
642
+ identifier=self._user_id,
643
+ password=self._password,
644
+ device_name="Hermes Agent",
645
+ device_id=self._device_id or None,
646
+ )
647
+ if resp and hasattr(resp, "device_id"):
648
+ client.device_id = resp.device_id
649
+ logger.info("Matrix: logged in as %s", self._user_id)
650
+ except Exception as exc:
651
+ logger.error("Matrix: login failed — %s", exc)
652
+ await api.session.close()
653
+ return False
654
+ else:
655
+ logger.error(
656
+ "Matrix: need MATRIX_ACCESS_TOKEN or MATRIX_USER_ID + MATRIX_PASSWORD"
657
+ )
658
+ await api.session.close()
659
+ return False
660
+
661
+ # Set up E2EE if requested.
662
+ if self._encryption:
663
+ if not _check_e2ee_deps():
664
+ logger.error(
665
+ "Matrix: MATRIX_ENCRYPTION=true but E2EE dependencies are missing. %s. "
666
+ "Refusing to connect — encrypted rooms would silently fail.",
667
+ _E2EE_INSTALL_HINT,
668
+ )
669
+ await api.session.close()
670
+ return False
671
+ try:
672
+ from mautrix.crypto import OlmMachine
673
+ from mautrix.crypto.store.asyncpg import PgCryptoStore
674
+ from mautrix.util.async_db import Database
675
+
676
+ _STORE_DIR.mkdir(parents=True, exist_ok=True)
677
+
678
+ # Remove legacy pickle file from pre-SQLite era.
679
+ legacy_pickle = _STORE_DIR / "crypto_store.pickle"
680
+ if legacy_pickle.exists():
681
+ logger.info(
682
+ "Matrix: removing legacy crypto_store.pickle (migrated to SQLite)"
683
+ )
684
+ legacy_pickle.unlink()
685
+
686
+ # Open SQLite-backed crypto store.
687
+ crypto_db = Database.create(
688
+ f"sqlite:///{_CRYPTO_DB_PATH}",
689
+ upgrade_table=PgCryptoStore.upgrade_table,
690
+ )
691
+ await crypto_db.start()
692
+ self._crypto_db = crypto_db
693
+
694
+ _acct_id = self._user_id or "hermes"
695
+ _pickle_key = f"{_acct_id}:{self._device_id or 'default'}"
696
+ crypto_store = PgCryptoStore(
697
+ account_id=_acct_id,
698
+ pickle_key=_pickle_key,
699
+ db=crypto_db,
700
+ )
701
+ await crypto_store.open()
702
+
703
+ # Bind the store to the runtime device_id before any
704
+ # put_account() runs. PgCryptoStore defaults _device_id
705
+ # to "" and its crypto_account UPSERT never updates the
706
+ # device_id column on conflict — so once put_account
707
+ # writes blank, it stays blank forever. That breaks
708
+ # every downstream device-scoped olm operation: peer
709
+ # to-device ciphertext can't find our identity key and
710
+ # no megolm sessions ever land. Setting _device_id here
711
+ # (in-memory; the on-disk row may not exist yet) makes
712
+ # the first put_account write the correct value.
713
+ # DeviceID is a NewType(str) so plain str works at runtime.
714
+ if client.device_id:
715
+ await crypto_store.put_device_id(client.device_id)
716
+
717
+ crypto_state = _CryptoStateStore(state_store, self._joined_rooms)
718
+ olm = OlmMachine(client, crypto_store, crypto_state)
719
+
720
+ # Accept unverified devices so senders share Megolm
721
+ # session keys with us automatically.
722
+ olm.share_keys_min_trust = TrustState.UNVERIFIED
723
+ olm.send_keys_min_trust = TrustState.UNVERIFIED
724
+
725
+ await olm.load()
726
+
727
+ # Verify our device keys are still on the homeserver.
728
+ if not await self._verify_device_keys_on_server(client, olm):
729
+ await crypto_db.stop()
730
+ await api.session.close()
731
+ return False
732
+
733
+ # Proactively flush one-time keys to detect stale OTK
734
+ # conflicts early. When crypto state is wiped but the
735
+ # same device ID is reused, the server may still hold OTKs
736
+ # signed with the old ed25519 key. Identity key re-upload
737
+ # succeeds but OTK uploads fail ("already exists" with
738
+ # mismatched signature). Peers then cannot establish Olm
739
+ # sessions and all new messages are undecryptable.
740
+ try:
741
+ await olm.share_keys()
742
+ except Exception as exc:
743
+ exc_str = str(exc)
744
+ if "already exists" in exc_str:
745
+ logger.error(
746
+ "Matrix: device %s has stale one-time keys on the "
747
+ "server signed with a previous identity key. "
748
+ "Peers cannot establish new Olm sessions with "
749
+ "this device. Delete the device from the "
750
+ "homeserver and restart, or generate a new "
751
+ "access token to get a fresh device ID.",
752
+ client.device_id,
753
+ )
754
+ await crypto_db.stop()
755
+ await api.session.close()
756
+ return False
757
+ # Non-OTK errors are transient (network, etc.) — log
758
+ # but allow startup to continue.
759
+ logger.warning(
760
+ "Matrix: share_keys() warning during startup: %s",
761
+ exc,
762
+ )
763
+
764
+ # Import cross-signing private keys from SSSS and self-sign
765
+ # the current device. Required after any device-key rotation
766
+ # (fresh crypto.db, share_keys re-upload) — otherwise the
767
+ # device's self-signing signature is stale and peers refuse
768
+ # to share Megolm sessions with the rotated device.
769
+ recovery_key = os.getenv("MATRIX_RECOVERY_KEY", "").strip()
770
+ if recovery_key:
771
+ try:
772
+ await olm.verify_with_recovery_key(recovery_key)
773
+ logger.info("Matrix: cross-signing verified via recovery key")
774
+ except Exception as exc:
775
+ logger.warning(
776
+ "Matrix: recovery key verification failed: %s", exc
777
+ )
778
+ else:
779
+ # No recovery key — bootstrap cross-signing if the bot
780
+ # has none yet. Without this, Element shows "Encrypted
781
+ # by a device not verified by its owner" on every
782
+ # message from this bot, indefinitely. mautrix's
783
+ # generate_recovery_key does the full flow: generates
784
+ # MSK/SSK/USK, uploads private keys to SSSS, publishes
785
+ # public keys to the homeserver, and signs the current
786
+ # device with the new SSK. Some homeservers require UIA
787
+ # for /keys/device_signing/upload — those will need an
788
+ # alternate path; Continuwuity and Synapse-with-shared-
789
+ # secret accept the unauthenticated upload.
790
+ try:
791
+ own_xsign = await olm.get_own_cross_signing_public_keys()
792
+ except Exception as exc:
793
+ own_xsign = None
794
+ logger.warning(
795
+ "Matrix: cross-signing key lookup failed: %s", exc
796
+ )
797
+ if own_xsign is None:
798
+ try:
799
+ new_recovery_key = await olm.generate_recovery_key()
800
+ logger.warning(
801
+ "Matrix: bootstrapped cross-signing for %s. "
802
+ "SAVE THIS RECOVERY KEY — set "
803
+ "MATRIX_RECOVERY_KEY for future restarts so "
804
+ "the bot can re-sign its device after key "
805
+ "rotation: %s",
806
+ client.mxid,
807
+ new_recovery_key,
808
+ )
809
+ except Exception as exc:
810
+ logger.warning(
811
+ "Matrix: cross-signing bootstrap failed "
812
+ "(non-fatal — Element will show 'not "
813
+ "verified by its owner'): %s",
814
+ exc,
815
+ )
816
+
817
+ client.crypto = olm
818
+ logger.info(
819
+ "Matrix: E2EE enabled (store: %s%s)",
820
+ str(_CRYPTO_DB_PATH),
821
+ f", device_id={client.device_id}" if client.device_id else "",
822
+ )
823
+ except Exception as exc:
824
+ logger.error(
825
+ "Matrix: failed to create E2EE client: %s. %s",
826
+ exc,
827
+ _E2EE_INSTALL_HINT,
828
+ )
829
+ await api.session.close()
830
+ return False
831
+
832
+ # Register event handlers.
833
+ from mautrix.client import InternalEventType as IntEvt
834
+ from mautrix.client.dispatcher import MembershipEventDispatcher
835
+
836
+ # Without this the INVITE handler below never fires.
837
+ client.add_dispatcher(MembershipEventDispatcher)
838
+
839
+ client.add_event_handler(EventType.ROOM_MESSAGE, self._on_room_message)
840
+ client.add_event_handler(EventType.REACTION, self._on_reaction)
841
+ client.add_event_handler(IntEvt.INVITE, self._on_invite)
842
+
843
+ # Initial sync to catch up, then start background sync.
844
+ self._startup_ts = time.time()
845
+ self._closing = False
846
+
847
+ try:
848
+ sync_data = await client.sync(timeout=10000, full_state=True)
849
+ if isinstance(sync_data, dict):
850
+ rooms_join = sync_data.get("rooms", {}).get("join", {})
851
+ self._joined_rooms.clear()
852
+ self._joined_rooms.update(rooms_join.keys())
853
+ # Store the next_batch token so incremental syncs start
854
+ # from where the initial sync left off.
855
+ nb = sync_data.get("next_batch")
856
+ if nb:
857
+ await client.sync_store.put_next_batch(nb)
858
+ logger.info(
859
+ "Matrix: initial sync complete, joined %d rooms",
860
+ len(self._joined_rooms),
861
+ )
862
+ # Build DM room cache from m.direct account data.
863
+ await self._refresh_dm_cache()
864
+
865
+ # Dispatch events from the initial sync so the OlmMachine
866
+ # receives to-device key shares queued while we were offline.
867
+ try:
868
+ tasks = client.handle_sync(sync_data)
869
+ if tasks:
870
+ await asyncio.gather(*tasks)
871
+ except Exception as exc:
872
+ logger.warning("Matrix: initial sync event dispatch error: %s", exc)
873
+ await self._join_pending_invites(sync_data)
874
+ else:
875
+ logger.warning(
876
+ "Matrix: initial sync returned unexpected type %s",
877
+ type(sync_data).__name__,
878
+ )
879
+ except Exception as exc:
880
+ logger.warning("Matrix: initial sync error: %s", exc)
881
+
882
+ # Share keys after initial sync if E2EE is enabled.
883
+ if self._encryption and getattr(client, "crypto", None):
884
+ try:
885
+ await client.crypto.share_keys()
886
+ except Exception as exc:
887
+ logger.warning("Matrix: initial key share failed: %s", exc)
888
+
889
+ # Start the sync loop.
890
+ self._sync_task = asyncio.create_task(self._sync_loop())
891
+ self._mark_connected()
892
+ return True
893
+
894
+ async def disconnect(self) -> None:
895
+ """Disconnect from Matrix."""
896
+ self._closing = True
897
+
898
+ if self._sync_task and not self._sync_task.done():
899
+ self._sync_task.cancel()
900
+ try:
901
+ await self._sync_task
902
+ except (asyncio.CancelledError, Exception):
903
+ pass
904
+
905
+ redaction_tasks = list(self._reaction_redaction_tasks)
906
+ for task in redaction_tasks:
907
+ if not task.done():
908
+ task.cancel()
909
+ if redaction_tasks:
910
+ await asyncio.gather(*redaction_tasks, return_exceptions=True)
911
+ self._reaction_redaction_tasks.clear()
912
+
913
+ # Close the SQLite crypto store database.
914
+ if hasattr(self, "_crypto_db") and self._crypto_db:
915
+ try:
916
+ await self._crypto_db.stop()
917
+ except Exception as exc:
918
+ logger.debug("Matrix: could not close crypto DB on disconnect: %s", exc)
919
+
920
+ if self._client:
921
+ try:
922
+ await self._client.api.session.close()
923
+ except Exception:
924
+ pass
925
+ self._client = None
926
+
927
+ logger.info("Matrix: disconnected")
928
+
929
+ async def send(
930
+ self,
931
+ chat_id: str,
932
+ content: str,
933
+ reply_to: Optional[str] = None,
934
+ metadata: Optional[Dict[str, Any]] = None,
935
+ ) -> SendResult:
936
+ """Send a message to a Matrix room."""
937
+
938
+ if not content:
939
+ return SendResult(success=True)
940
+
941
+ formatted = self.format_message(content)
942
+ chunks = self.truncate_message(formatted, MAX_MESSAGE_LENGTH)
943
+
944
+ last_event_id = None
945
+ for i, chunk in enumerate(chunks):
946
+ msg_content = self._build_text_message_content(chunk)
947
+
948
+ # Reply-to support.
949
+ if reply_to:
950
+ msg_content["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_to}}
951
+
952
+ # Thread support: if metadata has thread_id, send as threaded reply.
953
+ thread_id = (metadata or {}).get("thread_id")
954
+ if thread_id:
955
+ relates_to = msg_content.get("m.relates_to", {})
956
+ relates_to["rel_type"] = "m.thread"
957
+ relates_to["event_id"] = thread_id
958
+ relates_to["is_falling_back"] = True
959
+ if reply_to and "m.in_reply_to" not in relates_to:
960
+ relates_to["m.in_reply_to"] = {"event_id": reply_to}
961
+ msg_content["m.relates_to"] = relates_to
962
+
963
+ try:
964
+ event_id = await asyncio.wait_for(
965
+ self._client.send_message_event(
966
+ RoomID(chat_id),
967
+ EventType.ROOM_MESSAGE,
968
+ msg_content,
969
+ ),
970
+ timeout=45,
971
+ )
972
+ last_event_id = str(event_id)
973
+ logger.info("Matrix: sent event %s to %s", last_event_id, chat_id)
974
+ except Exception as exc:
975
+ # On E2EE errors, retry after sharing keys.
976
+ if self._encryption and getattr(self._client, "crypto", None):
977
+ try:
978
+ await self._client.crypto.share_keys()
979
+ event_id = await asyncio.wait_for(
980
+ self._client.send_message_event(
981
+ RoomID(chat_id),
982
+ EventType.ROOM_MESSAGE,
983
+ msg_content,
984
+ ),
985
+ timeout=45,
986
+ )
987
+ last_event_id = str(event_id)
988
+ logger.info(
989
+ "Matrix: sent event %s to %s (after key share)",
990
+ last_event_id,
991
+ chat_id,
992
+ )
993
+ continue
994
+ except Exception as retry_exc:
995
+ logger.error(
996
+ "Matrix: failed to send to %s after retry: %s",
997
+ chat_id,
998
+ retry_exc,
999
+ )
1000
+ return SendResult(success=False, error=str(retry_exc))
1001
+ logger.error("Matrix: failed to send to %s: %s", chat_id, exc)
1002
+ return SendResult(success=False, error=str(exc))
1003
+
1004
+ return SendResult(success=True, message_id=last_event_id)
1005
+
1006
+ async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
1007
+ """Return room name and type (dm/group)."""
1008
+ name = chat_id
1009
+ chat_type = "dm" if await self._is_dm_room(chat_id) else "group"
1010
+
1011
+ if self._client:
1012
+ try:
1013
+ name_evt = await self._client.get_state_event(
1014
+ RoomID(chat_id),
1015
+ EventType.ROOM_NAME,
1016
+ )
1017
+ if name_evt and hasattr(name_evt, "name") and name_evt.name:
1018
+ name = name_evt.name
1019
+ except Exception:
1020
+ pass
1021
+
1022
+ return {"name": name, "type": chat_type}
1023
+
1024
+ # ------------------------------------------------------------------
1025
+ # Optional overrides
1026
+ # ------------------------------------------------------------------
1027
+
1028
+ async def send_typing(
1029
+ self, chat_id: str, metadata: Optional[Dict[str, Any]] = None
1030
+ ) -> None:
1031
+ """Send a typing indicator."""
1032
+ if self._client:
1033
+ try:
1034
+ await self._client.set_typing(RoomID(chat_id), timeout=30000)
1035
+ except Exception:
1036
+ pass
1037
+
1038
+ async def stop_typing(self, chat_id: str) -> None:
1039
+ """Clear the typing indicator."""
1040
+ if self._client:
1041
+ try:
1042
+ await self._client.set_typing(RoomID(chat_id), timeout=0)
1043
+ except Exception:
1044
+ pass
1045
+
1046
+
1047
+ async def edit_message(
1048
+ self, chat_id: str, message_id: str, content: str, *, finalize: bool = False
1049
+ ) -> SendResult:
1050
+ """Edit an existing message (via m.replace)."""
1051
+
1052
+ formatted = self.format_message(content)
1053
+ new_content = self._build_text_message_content(formatted)
1054
+ msg_content: Dict[str, Any] = {
1055
+ "msgtype": "m.text",
1056
+ "body": f"* {formatted}",
1057
+ "m.new_content": new_content,
1058
+ }
1059
+ if "m.mentions" in new_content:
1060
+ msg_content["m.mentions"] = new_content["m.mentions"]
1061
+ if "formatted_body" in new_content:
1062
+ msg_content["format"] = "org.matrix.custom.html"
1063
+ msg_content["formatted_body"] = f'* {new_content["formatted_body"]}'
1064
+ msg_content["m.relates_to"] = {
1065
+ "rel_type": "m.replace",
1066
+ "event_id": message_id,
1067
+ }
1068
+
1069
+ try:
1070
+ event_id = await self._client.send_message_event(
1071
+ RoomID(chat_id),
1072
+ EventType.ROOM_MESSAGE,
1073
+ msg_content,
1074
+ )
1075
+ return SendResult(success=True, message_id=str(event_id))
1076
+ except Exception as exc:
1077
+ return SendResult(success=False, error=str(exc))
1078
+
1079
+ async def send_image(
1080
+ self,
1081
+ chat_id: str,
1082
+ image_url: str,
1083
+ caption: Optional[str] = None,
1084
+ reply_to: Optional[str] = None,
1085
+ metadata: Optional[Dict[str, Any]] = None,
1086
+ ) -> SendResult:
1087
+ """Download an image URL and upload it to Matrix."""
1088
+ from tools.url_safety import is_safe_url
1089
+
1090
+ if not is_safe_url(image_url):
1091
+ logger.warning("Matrix: blocked unsafe image URL (SSRF protection)")
1092
+ return await super().send_image(
1093
+ chat_id, image_url, caption, reply_to, metadata=metadata
1094
+ )
1095
+
1096
+ try:
1097
+ # Try aiohttp first (always available), fall back to httpx
1098
+ try:
1099
+ import aiohttp as _aiohttp
1100
+ _sess_kw, _req_kw = proxy_kwargs_for_aiohttp(self._proxy_url)
1101
+ async with _aiohttp.ClientSession(**_sess_kw) as http:
1102
+ async with http.get(
1103
+ image_url,
1104
+ timeout=_aiohttp.ClientTimeout(total=30),
1105
+ **_req_kw,
1106
+ ) as resp:
1107
+ resp.raise_for_status()
1108
+ data = await resp.read()
1109
+ ct = resp.content_type or "image/png"
1110
+ fname = (
1111
+ image_url.rsplit("/", 1)[-1].split("?")[0] or "image.png"
1112
+ )
1113
+ except ImportError:
1114
+ import httpx
1115
+ _httpx_kw: dict = {}
1116
+ if self._proxy_url:
1117
+ _httpx_kw["proxy"] = self._proxy_url
1118
+ async with httpx.AsyncClient(**_httpx_kw) as http:
1119
+ resp = await http.get(image_url, follow_redirects=True, timeout=30)
1120
+ resp.raise_for_status()
1121
+ data = resp.content
1122
+ ct = resp.headers.get("content-type", "image/png")
1123
+ fname = image_url.rsplit("/", 1)[-1].split("?")[0] or "image.png"
1124
+ except Exception as exc:
1125
+ logger.warning("Matrix: failed to download image %s: %s", image_url, exc)
1126
+ return await self.send(
1127
+ chat_id, f"{caption or ''}\n{image_url}".strip(), reply_to
1128
+ )
1129
+
1130
+ return await self._upload_and_send(
1131
+ chat_id, data, fname, ct, "m.image", caption, reply_to, metadata
1132
+ )
1133
+
1134
+ async def send_image_file(
1135
+ self,
1136
+ chat_id: str,
1137
+ image_path: str,
1138
+ caption: Optional[str] = None,
1139
+ reply_to: Optional[str] = None,
1140
+ metadata: Optional[Dict[str, Any]] = None,
1141
+ ) -> SendResult:
1142
+ """Upload a local image file to Matrix."""
1143
+ return await self._send_local_file(
1144
+ chat_id, image_path, "m.image", caption, reply_to, metadata=metadata
1145
+ )
1146
+
1147
+ async def send_document(
1148
+ self,
1149
+ chat_id: str,
1150
+ file_path: str,
1151
+ caption: Optional[str] = None,
1152
+ file_name: Optional[str] = None,
1153
+ reply_to: Optional[str] = None,
1154
+ metadata: Optional[Dict[str, Any]] = None,
1155
+ ) -> SendResult:
1156
+ """Upload a local file as a document."""
1157
+ return await self._send_local_file(
1158
+ chat_id, file_path, "m.file", caption, reply_to, file_name, metadata
1159
+ )
1160
+
1161
+ async def send_voice(
1162
+ self,
1163
+ chat_id: str,
1164
+ audio_path: str,
1165
+ caption: Optional[str] = None,
1166
+ reply_to: Optional[str] = None,
1167
+ metadata: Optional[Dict[str, Any]] = None,
1168
+ ) -> SendResult:
1169
+ """Upload an audio file as a voice message (MSC3245 native voice)."""
1170
+ return await self._send_local_file(
1171
+ chat_id,
1172
+ audio_path,
1173
+ "m.audio",
1174
+ caption,
1175
+ reply_to,
1176
+ metadata=metadata,
1177
+ is_voice=True,
1178
+ )
1179
+
1180
+ async def send_video(
1181
+ self,
1182
+ chat_id: str,
1183
+ video_path: str,
1184
+ caption: Optional[str] = None,
1185
+ reply_to: Optional[str] = None,
1186
+ metadata: Optional[Dict[str, Any]] = None,
1187
+ ) -> SendResult:
1188
+ """Upload a video file."""
1189
+ return await self._send_local_file(
1190
+ chat_id, video_path, "m.video", caption, reply_to, metadata=metadata
1191
+ )
1192
+
1193
+ async def send_exec_approval(
1194
+ self,
1195
+ chat_id: str,
1196
+ command: str,
1197
+ session_key: str,
1198
+ description: str = "dangerous command",
1199
+ metadata: Optional[dict] = None,
1200
+ ) -> SendResult:
1201
+ """Send a reaction-based exec approval prompt for Matrix."""
1202
+ if not self._client:
1203
+ return SendResult(success=False, error="Not connected")
1204
+
1205
+ cmd_preview = command[:2000] + "..." if len(command) > 2000 else command
1206
+ text = (
1207
+ "⚠️ **Dangerous command requires approval**\n"
1208
+ f"```\n{cmd_preview}\n```\n"
1209
+ f"Reason: {description}\n\n"
1210
+ "Reply `/approve` to execute, `/approve session` to approve this pattern for the session, "
1211
+ "`/approve always` to approve permanently, or `/deny` to cancel.\n\n"
1212
+ "You can also click the reaction to approve:\n"
1213
+ "✅ = /approve\n"
1214
+ "❎ = /deny"
1215
+ )
1216
+
1217
+ result = await self.send(chat_id, text, metadata=metadata)
1218
+ if not result.success or not result.message_id:
1219
+ return result
1220
+
1221
+ prompt = _MatrixApprovalPrompt(
1222
+ session_key=session_key,
1223
+ chat_id=chat_id,
1224
+ message_id=result.message_id,
1225
+ )
1226
+ old_event = self._approval_prompt_by_session.get(session_key)
1227
+ if old_event:
1228
+ self._approval_prompts_by_event.pop(old_event, None)
1229
+ self._approval_prompts_by_event[result.message_id] = prompt
1230
+ self._approval_prompt_by_session[session_key] = result.message_id
1231
+
1232
+ for emoji in ("✅", "❎"):
1233
+ try:
1234
+ reaction_result = await self._send_reaction(chat_id, result.message_id, emoji)
1235
+ # Save the bot's reaction event_id for later cleanup
1236
+ if reaction_result:
1237
+ prompt.bot_reaction_events[emoji] = str(reaction_result)
1238
+ except Exception as exc:
1239
+ logger.debug("Matrix: failed to add approval reaction %s: %s", emoji, exc)
1240
+
1241
+ return result
1242
+
1243
+ def format_message(self, content: str) -> str:
1244
+ """Pass-through — Matrix supports standard Markdown natively."""
1245
+ # Strip image markdown; media is uploaded separately.
1246
+ content = re.sub(r"!\[([^\]]*)\]\(([^)]+)\)", r"\2", content)
1247
+ return content
1248
+
1249
+ # ------------------------------------------------------------------
1250
+ # File helpers
1251
+ # ------------------------------------------------------------------
1252
+
1253
+ async def _upload_and_send(
1254
+ self,
1255
+ room_id: str,
1256
+ data: bytes,
1257
+ filename: str,
1258
+ content_type: str,
1259
+ msgtype: str,
1260
+ caption: Optional[str] = None,
1261
+ reply_to: Optional[str] = None,
1262
+ metadata: Optional[Dict[str, Any]] = None,
1263
+ is_voice: bool = False,
1264
+ ) -> SendResult:
1265
+ """Upload bytes to Matrix and send as a media message."""
1266
+
1267
+ upload_data = data
1268
+ encrypted_file = None
1269
+ if self._encryption and getattr(self._client, "crypto", None):
1270
+ state_store = getattr(self._client, "state_store", None)
1271
+ if state_store:
1272
+ try:
1273
+ room_encrypted = bool(await state_store.is_encrypted(RoomID(room_id)))
1274
+ except Exception:
1275
+ room_encrypted = False
1276
+ if room_encrypted:
1277
+ try:
1278
+ from mautrix.crypto.attachments import encrypt_attachment
1279
+ upload_data, encrypted_file = encrypt_attachment(data)
1280
+ except Exception as exc:
1281
+ logger.error("Matrix: attachment encryption failed: %s", exc)
1282
+ return SendResult(success=False, error=str(exc))
1283
+
1284
+ # Upload to homeserver.
1285
+ try:
1286
+ mxc_url = await self._client.upload_media(
1287
+ upload_data,
1288
+ mime_type=content_type,
1289
+ filename=filename,
1290
+ size=len(upload_data),
1291
+ )
1292
+ except Exception as exc:
1293
+ logger.error("Matrix: upload failed: %s", exc)
1294
+ return SendResult(success=False, error=str(exc))
1295
+
1296
+ # Build media message content.
1297
+ msg_content: Dict[str, Any] = {
1298
+ "msgtype": msgtype,
1299
+ "body": caption or filename,
1300
+ "info": {
1301
+ "mimetype": content_type,
1302
+ "size": len(data),
1303
+ },
1304
+ }
1305
+ if encrypted_file is not None:
1306
+ file_payload = encrypted_file.serialize()
1307
+ file_payload["url"] = str(mxc_url)
1308
+ msg_content["file"] = file_payload
1309
+ else:
1310
+ msg_content["url"] = str(mxc_url)
1311
+
1312
+ # Add MSC3245 voice flag for native voice messages.
1313
+ if is_voice:
1314
+ msg_content["org.matrix.msc3245.voice"] = {}
1315
+
1316
+ if reply_to:
1317
+ msg_content["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_to}}
1318
+
1319
+ thread_id = (metadata or {}).get("thread_id")
1320
+ if thread_id:
1321
+ relates_to = msg_content.get("m.relates_to", {})
1322
+ relates_to["rel_type"] = "m.thread"
1323
+ relates_to["event_id"] = thread_id
1324
+ relates_to["is_falling_back"] = True
1325
+ msg_content["m.relates_to"] = relates_to
1326
+
1327
+ try:
1328
+ event_id = await self._client.send_message_event(
1329
+ RoomID(room_id),
1330
+ EventType.ROOM_MESSAGE,
1331
+ msg_content,
1332
+ )
1333
+ return SendResult(success=True, message_id=str(event_id))
1334
+ except Exception as exc:
1335
+ return SendResult(success=False, error=str(exc))
1336
+
1337
+ async def _send_local_file(
1338
+ self,
1339
+ room_id: str,
1340
+ file_path: str,
1341
+ msgtype: str,
1342
+ caption: Optional[str] = None,
1343
+ reply_to: Optional[str] = None,
1344
+ file_name: Optional[str] = None,
1345
+ metadata: Optional[Dict[str, Any]] = None,
1346
+ is_voice: bool = False,
1347
+ ) -> SendResult:
1348
+ """Read a local file and upload it."""
1349
+ p = Path(file_path).expanduser()
1350
+ if not p.exists():
1351
+ return await self.send(
1352
+ room_id, f"{caption or ''}\n(file not found: {file_path})", reply_to
1353
+ )
1354
+
1355
+ fname = file_name or p.name
1356
+ ct = mimetypes.guess_type(fname)[0] or "application/octet-stream"
1357
+ data = p.read_bytes()
1358
+
1359
+ return await self._upload_and_send(
1360
+ room_id, data, fname, ct, msgtype, caption, reply_to, metadata, is_voice
1361
+ )
1362
+
1363
+ # ------------------------------------------------------------------
1364
+ # Sync loop
1365
+ # ------------------------------------------------------------------
1366
+
1367
+ async def _sync_loop(self) -> None:
1368
+ """Continuously sync with the homeserver."""
1369
+ client = self._client
1370
+ # Resume from the token stored during the initial sync.
1371
+ next_batch = await client.sync_store.get_next_batch()
1372
+ while not self._closing:
1373
+ try:
1374
+ # Wrap in asyncio.wait_for to guard against TCP-level hangs
1375
+ # that the Matrix long-poll timeout cannot catch. Long-poll
1376
+ # is 30s, so 45s gives 15s slack for network drain.
1377
+ sync_data = await asyncio.wait_for(
1378
+ client.sync(
1379
+ since=next_batch,
1380
+ timeout=30000,
1381
+ ),
1382
+ timeout=45.0,
1383
+ )
1384
+
1385
+ # nio returns SyncError objects (not exceptions) for auth
1386
+ # failures like M_UNKNOWN_TOKEN. Detect and stop immediately.
1387
+ _sync_msg = getattr(sync_data, "message", None)
1388
+ if _sync_msg and isinstance(_sync_msg, str):
1389
+ _lower = _sync_msg.lower()
1390
+ if "m_unknown_token" in _lower or "unknown_token" in _lower:
1391
+ logger.error(
1392
+ "Matrix: permanent auth error from sync: %s — stopping",
1393
+ _sync_msg,
1394
+ )
1395
+ return
1396
+
1397
+ if isinstance(sync_data, dict):
1398
+ # Update joined rooms from sync response.
1399
+ rooms_join = sync_data.get("rooms", {}).get("join", {})
1400
+ if rooms_join:
1401
+ self._joined_rooms.update(rooms_join.keys())
1402
+
1403
+ # Advance the sync token so the next request is
1404
+ # incremental instead of a full initial sync.
1405
+ nb = sync_data.get("next_batch")
1406
+ if nb:
1407
+ next_batch = nb
1408
+ await client.sync_store.put_next_batch(nb)
1409
+
1410
+ # Dispatch events to registered handlers so that
1411
+ # _on_room_message / _on_reaction / _on_invite fire.
1412
+ try:
1413
+ tasks = client.handle_sync(sync_data)
1414
+ if tasks:
1415
+ await asyncio.gather(*tasks)
1416
+ except Exception as exc:
1417
+ logger.warning("Matrix: sync event dispatch error: %s", exc)
1418
+ await self._join_pending_invites(sync_data)
1419
+
1420
+ except asyncio.CancelledError:
1421
+ return
1422
+ except Exception as exc:
1423
+ if self._closing:
1424
+ return
1425
+ # Detect permanent auth/permission failures.
1426
+ err_str = str(exc).lower()
1427
+ if (
1428
+ "401" in err_str
1429
+ or "403" in err_str
1430
+ or "unauthorized" in err_str
1431
+ or "forbidden" in err_str
1432
+ ):
1433
+ logger.error(
1434
+ "Matrix: permanent auth error: %s — stopping sync", exc
1435
+ )
1436
+ return
1437
+ logger.warning("Matrix: sync error: %s — retrying in 5s", exc)
1438
+ await asyncio.sleep(5)
1439
+
1440
+ # ------------------------------------------------------------------
1441
+ # Event callbacks
1442
+ # ------------------------------------------------------------------
1443
+
1444
+ def _is_self_sender(self, sender: str) -> bool:
1445
+ """Return True if the sender refers to the bot's own account.
1446
+
1447
+ Matrix user IDs are byte-compared after trimming whitespace and
1448
+ lowercasing — some homeservers normalize the localpart case
1449
+ differently at different API surfaces, and the reply-loop tail
1450
+ of the "hall of mirrors" bug (#15763) has been observed with the
1451
+ bot's own account bypassing a case-sensitive equality check.
1452
+
1453
+ When ``self._user_id`` is empty (whoami hasn't resolved yet, or
1454
+ login failed), we cannot prove a sender is NOT us, so we return
1455
+ True defensively — an unidentified bot dropping its own events
1456
+ is always preferable to falling into an echo loop.
1457
+ """
1458
+ own = (self._user_id or "").strip().lower()
1459
+ if not own:
1460
+ return True
1461
+ return sender.strip().lower() == own
1462
+
1463
+ @staticmethod
1464
+ def _is_system_or_bridge_sender(sender: str) -> bool:
1465
+ """Return True if the sender looks like a system / bridge / appservice
1466
+ identity rather than a real user.
1467
+
1468
+ Appservice namespaces on Matrix conventionally prefix bot / puppet
1469
+ user IDs with an underscore (e.g. ``@_telegram_12345:server``,
1470
+ ``@_discord_999:server``, ``@_slack_...:server``). Server-notices
1471
+ bots and bridge-controller bots on many homeservers use the same
1472
+ pattern.
1473
+
1474
+ We treat these as system identities for pairing purposes: they
1475
+ should never be offered a pairing code, because an operator
1476
+ approving the code would hand the bridge itself permanent
1477
+ authorization — and every outbound message relayed by the bridge
1478
+ would then loop back into the agent as an "authorized user
1479
+ message", which is the root of issue #15763.
1480
+
1481
+ Matches:
1482
+ ``@_something:server`` — appservice namespace convention
1483
+ ``@:server`` — malformed / empty localpart
1484
+ ``:server`` — malformed, no leading ``@``
1485
+ """
1486
+ s = (sender or "").strip()
1487
+ if not s:
1488
+ return True
1489
+ # Localpart is everything between leading '@' and ':'
1490
+ if s.startswith("@"):
1491
+ s = s[1:]
1492
+ if ":" in s:
1493
+ localpart, _, _ = s.partition(":")
1494
+ else:
1495
+ localpart = s
1496
+ if not localpart:
1497
+ return True
1498
+ return localpart.startswith("_")
1499
+
1500
+ async def _on_room_message(self, event: Any) -> None:
1501
+ """Handle incoming room message events (text, media)."""
1502
+ room_id = str(getattr(event, "room_id", ""))
1503
+ sender = str(getattr(event, "sender", ""))
1504
+
1505
+ # Diagnostic: confirm the callback is firing at all when DEBUG is on.
1506
+ # Helps users troubleshoot silent inbound issues like #5819, #7914, #12614.
1507
+ logger.debug(
1508
+ "Matrix: callback fired — event %s from %s in %s",
1509
+ getattr(event, "event_id", "?"),
1510
+ sender,
1511
+ room_id,
1512
+ )
1513
+
1514
+ # Ignore own messages (case-insensitive; also drops when our own
1515
+ # user_id hasn't been resolved yet — see _is_self_sender docstring
1516
+ # and issue #15763).
1517
+ if self._is_self_sender(sender):
1518
+ return
1519
+
1520
+ # Ignore appservice / bridge / system identities so they never
1521
+ # trigger the pairing flow. Once a bridge user is paired, every
1522
+ # outbound message it relays would loop back as an authorized
1523
+ # user message (the "hall of mirrors" in #15763).
1524
+ if self._is_system_or_bridge_sender(sender):
1525
+ logger.debug(
1526
+ "Matrix: ignoring system/bridge sender %s in %s",
1527
+ sender,
1528
+ room_id,
1529
+ )
1530
+ return
1531
+
1532
+ # Deduplicate by event ID.
1533
+ event_id = str(getattr(event, "event_id", ""))
1534
+ if self._is_duplicate_event(event_id):
1535
+ return
1536
+
1537
+ # Startup grace: ignore old messages from initial sync.
1538
+ raw_ts = (
1539
+ getattr(event, "timestamp", None)
1540
+ or getattr(event, "server_timestamp", None)
1541
+ or 0
1542
+ )
1543
+ event_ts = raw_ts / 1000.0 if raw_ts else 0.0
1544
+ if event_ts and event_ts < self._startup_ts - _STARTUP_GRACE_SECONDS:
1545
+ return
1546
+
1547
+ # Extract content from the event.
1548
+ content = getattr(event, "content", None)
1549
+ if content is None:
1550
+ return
1551
+
1552
+ # Get msgtype — either from content object or raw dict.
1553
+ if hasattr(content, "msgtype"):
1554
+ msgtype = str(content.msgtype)
1555
+ elif isinstance(content, dict):
1556
+ msgtype = content.get("msgtype", "")
1557
+ else:
1558
+ msgtype = ""
1559
+
1560
+ # Determine source content dict for relation/thread extraction.
1561
+ if isinstance(content, dict):
1562
+ source_content = content
1563
+ elif hasattr(content, "serialize"):
1564
+ source_content = content.serialize()
1565
+ else:
1566
+ source_content = {}
1567
+
1568
+ relates_to = source_content.get("m.relates_to", {})
1569
+
1570
+ # Skip edits (m.replace relation).
1571
+ if relates_to.get("rel_type") == "m.replace":
1572
+ return
1573
+
1574
+ # Ignore m.notice to prevent bot-to-bot loops (m.notice is the
1575
+ # conventional msgtype for bot responses in the Matrix ecosystem).
1576
+ if msgtype == "m.notice":
1577
+ return
1578
+
1579
+ # Dispatch by msgtype.
1580
+ media_msgtypes = ("m.image", "m.audio", "m.video", "m.file")
1581
+ if msgtype in media_msgtypes:
1582
+ await self._handle_media_message(
1583
+ room_id, sender, event_id, event_ts, source_content, relates_to, msgtype
1584
+ )
1585
+ elif msgtype == "m.text":
1586
+ await self._handle_text_message(
1587
+ room_id, sender, event_id, event_ts, source_content, relates_to
1588
+ )
1589
+
1590
+ async def _resolve_message_context(
1591
+ self,
1592
+ room_id: str,
1593
+ sender: str,
1594
+ event_id: str,
1595
+ body: str,
1596
+ source_content: dict,
1597
+ relates_to: dict,
1598
+ ) -> Optional[tuple]:
1599
+ """Shared mention/thread/DM gating for text and media handlers.
1600
+
1601
+ Returns (body, is_dm, chat_type, thread_id, display_name, source)
1602
+ or None if the message should be dropped (mention gating).
1603
+ """
1604
+ is_dm = await self._is_dm_room(room_id)
1605
+ chat_type = "dm" if is_dm else "group"
1606
+
1607
+ thread_id = None
1608
+ if relates_to.get("rel_type") == "m.thread":
1609
+ thread_id = relates_to.get("event_id")
1610
+
1611
+ formatted_body = source_content.get("formatted_body")
1612
+ # m.mentions.user_ids (MSC3952 / Matrix v1.7) — authoritative mention signal.
1613
+ mentions_block = source_content.get("m.mentions") or {}
1614
+ mention_user_ids = (
1615
+ mentions_block.get("user_ids") if isinstance(mentions_block, dict) else None
1616
+ )
1617
+ is_mentioned = self._is_bot_mentioned(body, formatted_body, mention_user_ids)
1618
+
1619
+ # Require-mention gating.
1620
+ if not is_dm:
1621
+ # allowed_rooms check (whitelist — must pass before other gating).
1622
+ # When set, messages from rooms NOT in this whitelist are silently
1623
+ # ignored, even if @mentioned. DMs are already excluded above.
1624
+ if self._allowed_rooms and room_id not in self._allowed_rooms:
1625
+ logger.debug(
1626
+ "Matrix: ignoring message %s in %s — room not in "
1627
+ "MATRIX_ALLOWED_ROOMS whitelist",
1628
+ event_id,
1629
+ room_id,
1630
+ )
1631
+ return None
1632
+
1633
+ is_free_room = room_id in self._free_rooms
1634
+ in_bot_thread = bool(thread_id and thread_id in self._threads)
1635
+ if self._require_mention and not is_free_room and not in_bot_thread:
1636
+ if not is_mentioned:
1637
+ logger.debug(
1638
+ "Matrix: ignoring message %s in %s — no @mention "
1639
+ "(set MATRIX_REQUIRE_MENTION=false to disable)",
1640
+ event_id,
1641
+ room_id,
1642
+ )
1643
+ return None
1644
+
1645
+ # DM mention-thread.
1646
+ if is_dm and not thread_id and self._dm_mention_threads and is_mentioned:
1647
+ thread_id = event_id
1648
+ self._threads.mark(thread_id)
1649
+
1650
+ # Strip mention from body (only when mention-gating is active).
1651
+ if is_mentioned and self._require_mention:
1652
+ body = self._strip_mention(body)
1653
+
1654
+ # Auto-thread.
1655
+ if not thread_id and ((not is_dm and self._auto_thread) or (is_dm and self._dm_auto_thread)):
1656
+ thread_id = event_id
1657
+ self._threads.mark(thread_id)
1658
+
1659
+ display_name = await self._get_display_name(room_id, sender)
1660
+ source = self.build_source(
1661
+ chat_id=room_id,
1662
+ chat_type=chat_type,
1663
+ user_id=sender,
1664
+ user_name=display_name,
1665
+ thread_id=thread_id,
1666
+ )
1667
+
1668
+ if thread_id:
1669
+ self._threads.mark(thread_id)
1670
+
1671
+ self._background_read_receipt(room_id, event_id)
1672
+
1673
+ return body, is_dm, chat_type, thread_id, display_name, source
1674
+
1675
+ async def _handle_text_message(
1676
+ self,
1677
+ room_id: str,
1678
+ sender: str,
1679
+ event_id: str,
1680
+ event_ts: float,
1681
+ source_content: dict,
1682
+ relates_to: dict,
1683
+ ) -> None:
1684
+ """Process a text message event."""
1685
+ body = source_content.get("body", "") or ""
1686
+ if not body:
1687
+ return
1688
+
1689
+ ctx = await self._resolve_message_context(
1690
+ room_id,
1691
+ sender,
1692
+ event_id,
1693
+ body,
1694
+ source_content,
1695
+ relates_to,
1696
+ )
1697
+ if ctx is None:
1698
+ return
1699
+ body, is_dm, chat_type, thread_id, display_name, source = ctx
1700
+
1701
+ # Reply-to detection.
1702
+ reply_to = None
1703
+ in_reply_to = relates_to.get("m.in_reply_to", {})
1704
+ if in_reply_to:
1705
+ reply_to = in_reply_to.get("event_id")
1706
+
1707
+ # Strip reply fallback from body.
1708
+ if reply_to and body.startswith("> "):
1709
+ lines = body.split("\n")
1710
+ stripped = []
1711
+ past_fallback = False
1712
+ for line in lines:
1713
+ if not past_fallback:
1714
+ if line.startswith("> ") or line == ">":
1715
+ continue
1716
+ if line == "":
1717
+ past_fallback = True
1718
+ continue
1719
+ past_fallback = True
1720
+ stripped.append(line)
1721
+ body = "\n".join(stripped) if stripped else body
1722
+
1723
+ msg_type = MessageType.TEXT
1724
+ if body.startswith(("!", "/")):
1725
+ msg_type = MessageType.COMMAND
1726
+
1727
+ msg_event = MessageEvent(
1728
+ text=body,
1729
+ message_type=msg_type,
1730
+ source=source,
1731
+ raw_message=source_content,
1732
+ message_id=event_id,
1733
+ reply_to_message_id=reply_to,
1734
+ )
1735
+
1736
+ if msg_type == MessageType.TEXT and self._text_batch_delay_seconds > 0:
1737
+ self._enqueue_text_event(msg_event)
1738
+ else:
1739
+ await self.handle_message(msg_event)
1740
+
1741
+ async def _handle_media_message(
1742
+ self,
1743
+ room_id: str,
1744
+ sender: str,
1745
+ event_id: str,
1746
+ event_ts: float,
1747
+ source_content: dict,
1748
+ relates_to: dict,
1749
+ msgtype: str,
1750
+ ) -> None:
1751
+ """Process a media message event (image, audio, video, file)."""
1752
+ body = source_content.get("body", "") or ""
1753
+ url = source_content.get("url", "")
1754
+
1755
+ # Convert mxc:// to HTTP URL for downstream processing.
1756
+ http_url = ""
1757
+ if url and url.startswith("mxc://"):
1758
+ http_url = self._mxc_to_http(url)
1759
+
1760
+ # Extract MIME type from content info.
1761
+ content_info = source_content.get("info", {})
1762
+ if not isinstance(content_info, dict):
1763
+ content_info = {}
1764
+ event_mimetype = content_info.get("mimetype", "")
1765
+
1766
+ # For encrypted media, the URL may be in file.url.
1767
+ file_content = source_content.get("file", {})
1768
+ if not url and isinstance(file_content, dict):
1769
+ url = file_content.get("url", "") or ""
1770
+ if url and url.startswith("mxc://"):
1771
+ http_url = self._mxc_to_http(url)
1772
+
1773
+ is_encrypted_media = bool(
1774
+ file_content and isinstance(file_content, dict) and file_content.get("url")
1775
+ )
1776
+
1777
+ media_type = "application/octet-stream"
1778
+ msg_type = MessageType.DOCUMENT
1779
+ is_voice_message = False
1780
+
1781
+ if msgtype == "m.image":
1782
+ msg_type = MessageType.PHOTO
1783
+ media_type = event_mimetype or "image/png"
1784
+ elif msgtype == "m.audio":
1785
+ if source_content.get("org.matrix.msc3245.voice") is not None:
1786
+ is_voice_message = True
1787
+ msg_type = MessageType.VOICE
1788
+ else:
1789
+ msg_type = MessageType.AUDIO
1790
+ media_type = event_mimetype or "audio/ogg"
1791
+ elif msgtype == "m.video":
1792
+ msg_type = MessageType.VIDEO
1793
+ media_type = event_mimetype or "video/mp4"
1794
+ elif event_mimetype:
1795
+ media_type = event_mimetype
1796
+
1797
+ # Cache media locally when downstream tools need a real file path.
1798
+ cached_path = None
1799
+ should_cache_locally = msg_type in {
1800
+ MessageType.PHOTO, MessageType.AUDIO, MessageType.VIDEO, MessageType.DOCUMENT,
1801
+ } or is_voice_message or is_encrypted_media
1802
+ if should_cache_locally and url:
1803
+ try:
1804
+ file_bytes = await self._client.download_media(ContentURI(url))
1805
+ if file_bytes is not None:
1806
+ if is_encrypted_media:
1807
+ from mautrix.crypto.attachments import decrypt_attachment
1808
+
1809
+ hashes_value = (
1810
+ file_content.get("hashes")
1811
+ if isinstance(file_content, dict)
1812
+ else None
1813
+ )
1814
+ hash_value = (
1815
+ hashes_value.get("sha256")
1816
+ if isinstance(hashes_value, dict)
1817
+ else None
1818
+ )
1819
+
1820
+ key_value = (
1821
+ file_content.get("key")
1822
+ if isinstance(file_content, dict)
1823
+ else None
1824
+ )
1825
+ if isinstance(key_value, dict):
1826
+ key_value = key_value.get("k")
1827
+
1828
+ iv_value = (
1829
+ file_content.get("iv")
1830
+ if isinstance(file_content, dict)
1831
+ else None
1832
+ )
1833
+
1834
+ if key_value and hash_value and iv_value:
1835
+ file_bytes = decrypt_attachment(
1836
+ file_bytes, key_value, hash_value, iv_value
1837
+ )
1838
+ else:
1839
+ logger.warning(
1840
+ "[Matrix] Encrypted media event missing decryption metadata for %s",
1841
+ event_id,
1842
+ )
1843
+ file_bytes = None
1844
+
1845
+ if file_bytes is not None:
1846
+ from gateway.platforms.base import (
1847
+ cache_audio_from_bytes,
1848
+ cache_document_from_bytes,
1849
+ cache_image_from_bytes,
1850
+ )
1851
+
1852
+ if msg_type == MessageType.PHOTO:
1853
+ ext_map = {
1854
+ "image/jpeg": ".jpg",
1855
+ "image/png": ".png",
1856
+ "image/gif": ".gif",
1857
+ "image/webp": ".webp",
1858
+ }
1859
+ ext = ext_map.get(media_type, ".jpg")
1860
+ cached_path = cache_image_from_bytes(file_bytes, ext=ext)
1861
+ logger.info("[Matrix] Cached user image at %s", cached_path)
1862
+ elif msg_type in {MessageType.AUDIO, MessageType.VOICE}:
1863
+ ext = (
1864
+ Path(
1865
+ body
1866
+ or (
1867
+ "voice.ogg" if is_voice_message else "audio.ogg"
1868
+ )
1869
+ ).suffix
1870
+ or ".ogg"
1871
+ )
1872
+ cached_path = cache_audio_from_bytes(file_bytes, ext=ext)
1873
+ else:
1874
+ filename = body or (
1875
+ "video.mp4"
1876
+ if msg_type == MessageType.VIDEO
1877
+ else "document"
1878
+ )
1879
+ cached_path = cache_document_from_bytes(
1880
+ file_bytes, filename
1881
+ )
1882
+ except Exception as e:
1883
+ logger.warning("[Matrix] Failed to cache media: %s", e)
1884
+
1885
+ ctx = await self._resolve_message_context(
1886
+ room_id,
1887
+ sender,
1888
+ event_id,
1889
+ body,
1890
+ source_content,
1891
+ relates_to,
1892
+ )
1893
+ if ctx is None:
1894
+ return
1895
+ body, is_dm, chat_type, thread_id, display_name, source = ctx
1896
+
1897
+ if msgtype == "m.image" and _looks_like_matrix_image_filename(body):
1898
+ body = ""
1899
+
1900
+ allow_http_fallback = bool(http_url) and not is_encrypted_media
1901
+ media_urls = (
1902
+ [cached_path]
1903
+ if cached_path
1904
+ else ([http_url] if allow_http_fallback else None)
1905
+ )
1906
+ media_types = [media_type] if media_urls else None
1907
+
1908
+ msg_event = MessageEvent(
1909
+ text=body,
1910
+ message_type=msg_type,
1911
+ source=source,
1912
+ raw_message=source_content,
1913
+ message_id=event_id,
1914
+ media_urls=media_urls,
1915
+ media_types=media_types,
1916
+ )
1917
+
1918
+ await self.handle_message(msg_event)
1919
+
1920
+ async def _on_invite(self, event: Any) -> None:
1921
+ """Auto-join rooms when invited."""
1922
+
1923
+ room_id = str(getattr(event, "room_id", ""))
1924
+
1925
+ logger.info(
1926
+ "Matrix: invited to %s — joining",
1927
+ room_id,
1928
+ )
1929
+ await self._join_room_by_id(room_id)
1930
+
1931
+ async def _join_room_by_id(self, room_id: str) -> bool:
1932
+ """Join a room by ID and refresh local caches on success."""
1933
+ if not room_id:
1934
+ return False
1935
+ if room_id in self._joined_rooms:
1936
+ return True
1937
+ try:
1938
+ await self._client.join_room(RoomID(room_id))
1939
+ self._joined_rooms.add(room_id)
1940
+ logger.info("Matrix: joined %s", room_id)
1941
+ await self._refresh_dm_cache()
1942
+ return True
1943
+ except Exception as exc:
1944
+ logger.warning("Matrix: error joining %s: %s", room_id, exc)
1945
+ return False
1946
+
1947
+ async def _join_pending_invites(self, sync_data: Dict[str, Any]) -> None:
1948
+ """Join rooms still present in rooms.invite after sync processing."""
1949
+ rooms = sync_data.get("rooms", {}) if isinstance(sync_data, dict) else {}
1950
+ invites = rooms.get("invite", {})
1951
+ if not isinstance(invites, dict):
1952
+ return
1953
+ for room_id in invites:
1954
+ if room_id in self._joined_rooms:
1955
+ continue
1956
+ logger.info("Matrix: reconciling pending invite for %s", room_id)
1957
+ await self._join_room_by_id(str(room_id))
1958
+
1959
+ # ------------------------------------------------------------------
1960
+ # Reactions (send, receive, processing lifecycle)
1961
+ # ------------------------------------------------------------------
1962
+
1963
+ async def _send_reaction(
1964
+ self,
1965
+ room_id: str,
1966
+ event_id: str,
1967
+ emoji: str,
1968
+ ) -> Optional[str]:
1969
+ """Send an emoji reaction to a message in a room.
1970
+ Returns the reaction event_id on success, None on failure.
1971
+ """
1972
+
1973
+ if not self._client:
1974
+ return None
1975
+ content = {
1976
+ "m.relates_to": {
1977
+ "rel_type": "m.annotation",
1978
+ "event_id": event_id,
1979
+ "key": emoji,
1980
+ }
1981
+ }
1982
+ try:
1983
+ resp_event_id = await self._client.send_message_event(
1984
+ RoomID(room_id),
1985
+ EventType.REACTION,
1986
+ content,
1987
+ )
1988
+ logger.debug("Matrix: sent reaction %s to %s", emoji, event_id)
1989
+ return str(resp_event_id)
1990
+ except Exception as exc:
1991
+ logger.debug("Matrix: reaction send error: %s", exc)
1992
+ return None
1993
+
1994
+ async def _redact_reaction(
1995
+ self,
1996
+ room_id: str,
1997
+ reaction_event_id: str,
1998
+ reason: str = "",
1999
+ ) -> bool:
2000
+ """Remove a reaction by redacting its event."""
2001
+ return await self.redact_message(room_id, reaction_event_id, reason)
2002
+
2003
+ def _schedule_reaction_redaction(
2004
+ self,
2005
+ room_id: str,
2006
+ reaction_event_id: str,
2007
+ reason: str = "",
2008
+ ) -> None:
2009
+ """Redact a reaction after a short delay so message delivery settles."""
2010
+
2011
+ async def _redact_later() -> None:
2012
+ try:
2013
+ if self._reaction_redaction_delay_seconds:
2014
+ await asyncio.sleep(self._reaction_redaction_delay_seconds)
2015
+ if not await self._redact_reaction(room_id, reaction_event_id, reason):
2016
+ logger.debug(
2017
+ "Matrix: failed to redact reaction %s", reaction_event_id
2018
+ )
2019
+ except asyncio.CancelledError:
2020
+ raise
2021
+ except Exception as exc:
2022
+ logger.debug(
2023
+ "Matrix: delayed reaction redaction failed for %s: %s",
2024
+ reaction_event_id,
2025
+ exc,
2026
+ )
2027
+
2028
+ task = asyncio.create_task(_redact_later())
2029
+ self._reaction_redaction_tasks.add(task)
2030
+ task.add_done_callback(self._reaction_redaction_tasks.discard)
2031
+
2032
+ async def on_processing_start(self, event: MessageEvent) -> None:
2033
+ """Add eyes reaction when the agent starts processing a message."""
2034
+ if not self._reactions_enabled:
2035
+ return
2036
+ msg_id = event.message_id
2037
+ room_id = event.source.chat_id
2038
+ if msg_id and room_id:
2039
+ reaction_event_id = await self._send_reaction(room_id, msg_id, "\U0001f440")
2040
+ if reaction_event_id:
2041
+ self._pending_reactions[(room_id, msg_id)] = reaction_event_id
2042
+
2043
+ async def on_processing_complete(
2044
+ self,
2045
+ event: MessageEvent,
2046
+ outcome: ProcessingOutcome,
2047
+ ) -> None:
2048
+ """Replace eyes with checkmark (success) or cross (failure)."""
2049
+ if not self._reactions_enabled:
2050
+ return
2051
+ msg_id = event.message_id
2052
+ room_id = event.source.chat_id
2053
+ if not msg_id or not room_id:
2054
+ return
2055
+ if outcome == ProcessingOutcome.CANCELLED:
2056
+ return
2057
+ reaction_key = (room_id, msg_id)
2058
+ if reaction_key in self._pending_reactions:
2059
+ eyes_event_id = self._pending_reactions.pop(reaction_key)
2060
+ self._schedule_reaction_redaction(
2061
+ room_id,
2062
+ eyes_event_id,
2063
+ "processing complete",
2064
+ )
2065
+ await self._send_reaction(
2066
+ room_id,
2067
+ msg_id,
2068
+ "\u2705" if outcome == ProcessingOutcome.SUCCESS else "\u274c",
2069
+ )
2070
+
2071
+ async def _on_reaction(self, event: Any) -> None:
2072
+ """Handle incoming reaction events."""
2073
+ sender = str(getattr(event, "sender", ""))
2074
+ if self._is_self_sender(sender):
2075
+ return
2076
+ event_id = str(getattr(event, "event_id", ""))
2077
+ if self._is_duplicate_event(event_id):
2078
+ return
2079
+
2080
+ room_id = str(getattr(event, "room_id", ""))
2081
+ content = getattr(event, "content", None)
2082
+ if content:
2083
+ relates_to = (
2084
+ content.get("m.relates_to", {})
2085
+ if isinstance(content, dict)
2086
+ else getattr(content, "relates_to", {})
2087
+ )
2088
+ reacts_to = ""
2089
+ key = ""
2090
+ if isinstance(relates_to, dict):
2091
+ reacts_to = relates_to.get("event_id", "")
2092
+ key = relates_to.get("key", "")
2093
+ elif hasattr(relates_to, "event_id"):
2094
+ reacts_to = str(getattr(relates_to, "event_id", ""))
2095
+ key = str(getattr(relates_to, "key", ""))
2096
+ logger.info(
2097
+ "Matrix: reaction %s from %s on %s in %s",
2098
+ key,
2099
+ sender,
2100
+ reacts_to,
2101
+ room_id,
2102
+ )
2103
+
2104
+ # Check if this reaction resolves a pending approval prompt.
2105
+ prompt = self._approval_prompts_by_event.get(reacts_to)
2106
+ if prompt and not prompt.resolved:
2107
+ if room_id != prompt.chat_id:
2108
+ return
2109
+ if self._allowed_user_ids and sender not in self._allowed_user_ids:
2110
+ logger.info(
2111
+ "Matrix: ignoring approval reaction from unauthorized user %s on %s",
2112
+ sender, reacts_to,
2113
+ )
2114
+ return
2115
+ choice = self._approval_reaction_map.get(key)
2116
+ if not choice:
2117
+ return
2118
+ try:
2119
+ from tools.approval import resolve_gateway_approval
2120
+
2121
+ count = resolve_gateway_approval(prompt.session_key, choice)
2122
+ if count:
2123
+ prompt.resolved = True
2124
+ self._approval_prompts_by_event.pop(reacts_to, None)
2125
+ self._approval_prompt_by_session.pop(prompt.session_key, None)
2126
+ logger.info(
2127
+ "Matrix reaction resolved %d approval(s) for session %s "
2128
+ "(choice=%s, user=%s)",
2129
+ count, prompt.session_key, choice, sender,
2130
+ )
2131
+ # Redact bot's seed reactions, leaving only the user's
2132
+ await self._redact_bot_approval_reactions(room_id, prompt)
2133
+ except Exception as exc:
2134
+ logger.error("Failed to resolve gateway approval from Matrix reaction: %s", exc)
2135
+
2136
+ async def _redact_bot_approval_reactions(
2137
+ self,
2138
+ room_id: str,
2139
+ prompt: "_MatrixApprovalPrompt",
2140
+ ) -> None:
2141
+ """Redact the bot's seed ✅/❎ reactions, leaving only the user's reaction."""
2142
+ for emoji, evt_id in prompt.bot_reaction_events.items():
2143
+ self._schedule_reaction_redaction(room_id, evt_id, "approval resolved")
2144
+ logger.debug("Matrix: scheduled bot reaction redaction %s (%s)", emoji, evt_id)
2145
+
2146
+ # ------------------------------------------------------------------
2147
+ # Text message aggregation (handles Matrix client-side splits)
2148
+ # ------------------------------------------------------------------
2149
+
2150
+ def _text_batch_key(self, event: MessageEvent) -> str:
2151
+ """Session-scoped key for text message batching."""
2152
+ from gateway.session import build_session_key
2153
+
2154
+ return build_session_key(
2155
+ event.source,
2156
+ group_sessions_per_user=self.config.extra.get(
2157
+ "group_sessions_per_user", True
2158
+ ),
2159
+ thread_sessions_per_user=self.config.extra.get(
2160
+ "thread_sessions_per_user", False
2161
+ ),
2162
+ )
2163
+
2164
+ def _enqueue_text_event(self, event: MessageEvent) -> None:
2165
+ """Buffer a text event and reset the flush timer."""
2166
+ key = self._text_batch_key(event)
2167
+ existing = self._pending_text_batches.get(key)
2168
+ chunk_len = len(event.text or "")
2169
+ if existing is None:
2170
+ event._last_chunk_len = chunk_len # type: ignore[attr-defined]
2171
+ self._pending_text_batches[key] = event
2172
+ else:
2173
+ if event.text:
2174
+ existing.text = (
2175
+ f"{existing.text}\n{event.text}" if existing.text else event.text
2176
+ )
2177
+ existing._last_chunk_len = chunk_len # type: ignore[attr-defined]
2178
+ if event.media_urls:
2179
+ existing.media_urls.extend(event.media_urls)
2180
+ existing.media_types.extend(event.media_types)
2181
+
2182
+ prior_task = self._pending_text_batch_tasks.get(key)
2183
+ if prior_task and not prior_task.done():
2184
+ prior_task.cancel()
2185
+ self._pending_text_batch_tasks[key] = asyncio.create_task(
2186
+ self._flush_text_batch(key)
2187
+ )
2188
+
2189
+ async def _flush_text_batch(self, key: str) -> None:
2190
+ """Wait for the quiet period then dispatch the aggregated text."""
2191
+ current_task = asyncio.current_task()
2192
+ try:
2193
+ pending = self._pending_text_batches.get(key)
2194
+ last_len = getattr(pending, "_last_chunk_len", 0) if pending else 0
2195
+ if last_len >= self._SPLIT_THRESHOLD:
2196
+ delay = self._text_batch_split_delay_seconds
2197
+ else:
2198
+ delay = self._text_batch_delay_seconds
2199
+ await asyncio.sleep(delay)
2200
+ event = self._pending_text_batches.pop(key, None)
2201
+ if not event:
2202
+ return
2203
+ logger.info(
2204
+ "[Matrix] Flushing text batch %s (%d chars)",
2205
+ key,
2206
+ len(event.text or ""),
2207
+ )
2208
+ await self.handle_message(event)
2209
+ finally:
2210
+ if self._pending_text_batch_tasks.get(key) is current_task:
2211
+ self._pending_text_batch_tasks.pop(key, None)
2212
+
2213
+ # ------------------------------------------------------------------
2214
+ # Read receipts
2215
+ # ------------------------------------------------------------------
2216
+
2217
+ def _background_read_receipt(self, room_id: str, event_id: str) -> None:
2218
+ """Fire-and-forget read receipt with error logging."""
2219
+
2220
+ async def _send() -> None:
2221
+ try:
2222
+ await self.send_read_receipt(room_id, event_id)
2223
+ except Exception as exc: # pragma: no cover — defensive
2224
+ logger.debug("Matrix: background read receipt failed: %s", exc)
2225
+
2226
+ asyncio.ensure_future(_send())
2227
+
2228
+ async def send_read_receipt(self, room_id: str, event_id: str) -> bool:
2229
+ """Send a read receipt (m.read) for an event."""
2230
+ if not self._client:
2231
+ return False
2232
+ try:
2233
+ room = RoomID(room_id)
2234
+ event = EventID(event_id)
2235
+ if hasattr(self._client, "set_fully_read_marker"):
2236
+ await self._client.set_fully_read_marker(room, event, event)
2237
+ elif hasattr(self._client, "send_receipt"):
2238
+ await self._client.send_receipt(room, event)
2239
+ elif hasattr(self._client, "set_read_markers"):
2240
+ await self._client.set_read_markers(
2241
+ room,
2242
+ fully_read_event=event,
2243
+ read_receipt=event,
2244
+ )
2245
+ else:
2246
+ logger.debug("Matrix: client has no read receipt method")
2247
+ return False
2248
+ logger.debug("Matrix: sent read receipt for %s in %s", event_id, room_id)
2249
+ return True
2250
+ except Exception as exc:
2251
+ logger.debug("Matrix: read receipt failed: %s", exc)
2252
+ return False
2253
+
2254
+ # ------------------------------------------------------------------
2255
+ # Message redaction
2256
+ # ------------------------------------------------------------------
2257
+
2258
+ async def redact_message(
2259
+ self,
2260
+ room_id: str,
2261
+ event_id: str,
2262
+ reason: str = "",
2263
+ ) -> bool:
2264
+ """Redact (delete) a message or event from a room."""
2265
+ if not self._client:
2266
+ return False
2267
+ try:
2268
+ await self._client.redact(
2269
+ RoomID(room_id),
2270
+ EventID(event_id),
2271
+ reason=reason or None,
2272
+ )
2273
+ logger.info("Matrix: redacted %s in %s", event_id, room_id)
2274
+ return True
2275
+ except Exception as exc:
2276
+ logger.warning("Matrix: redact error: %s", exc)
2277
+ return False
2278
+
2279
+ # ------------------------------------------------------------------
2280
+ # Room creation & management
2281
+ # ------------------------------------------------------------------
2282
+
2283
+ async def create_room(
2284
+ self,
2285
+ name: str = "",
2286
+ topic: str = "",
2287
+ invite: Optional[list] = None,
2288
+ is_direct: bool = False,
2289
+ preset: str = "private_chat",
2290
+ ) -> Optional[str]:
2291
+ """Create a new Matrix room."""
2292
+ if not self._client:
2293
+ return None
2294
+ try:
2295
+ preset_enum = {
2296
+ "private_chat": RoomCreatePreset.PRIVATE,
2297
+ "public_chat": RoomCreatePreset.PUBLIC,
2298
+ "trusted_private_chat": RoomCreatePreset.TRUSTED_PRIVATE,
2299
+ }.get(preset, RoomCreatePreset.PRIVATE)
2300
+ invitees = [UserID(u) for u in (invite or [])]
2301
+ room_id = await self._client.create_room(
2302
+ name=name or None,
2303
+ topic=topic or None,
2304
+ invitees=invitees,
2305
+ is_direct=is_direct,
2306
+ preset=preset_enum,
2307
+ )
2308
+ room_id_str = str(room_id)
2309
+ self._joined_rooms.add(room_id_str)
2310
+ logger.info("Matrix: created room %s (%s)", room_id_str, name or "unnamed")
2311
+ return room_id_str
2312
+ except Exception as exc:
2313
+ logger.warning("Matrix: create_room error: %s", exc)
2314
+ return None
2315
+
2316
+ async def invite_user(self, room_id: str, user_id: str) -> bool:
2317
+ """Invite a user to a room."""
2318
+ if not self._client:
2319
+ return False
2320
+ try:
2321
+ await self._client.invite_user(RoomID(room_id), UserID(user_id))
2322
+ logger.info("Matrix: invited %s to %s", user_id, room_id)
2323
+ return True
2324
+ except Exception as exc:
2325
+ logger.warning("Matrix: invite error: %s", exc)
2326
+ return False
2327
+
2328
+ # ------------------------------------------------------------------
2329
+ # Presence
2330
+ # ------------------------------------------------------------------
2331
+
2332
+ _VALID_PRESENCE_STATES = frozenset(("online", "offline", "unavailable"))
2333
+
2334
+ async def set_presence(self, state: str = "online", status_msg: str = "") -> bool:
2335
+ """Set the bot's presence status."""
2336
+ if not self._client:
2337
+ return False
2338
+ if state not in self._VALID_PRESENCE_STATES:
2339
+ logger.warning("Matrix: invalid presence state %r", state)
2340
+ return False
2341
+ try:
2342
+ presence_map = {
2343
+ "online": PresenceState.ONLINE,
2344
+ "offline": PresenceState.OFFLINE,
2345
+ "unavailable": PresenceState.UNAVAILABLE,
2346
+ }
2347
+ await self._client.set_presence(
2348
+ presence=presence_map[state],
2349
+ status=status_msg or None,
2350
+ )
2351
+ logger.debug("Matrix: presence set to %s", state)
2352
+ return True
2353
+ except Exception as exc:
2354
+ logger.debug("Matrix: set_presence failed: %s", exc)
2355
+ return False
2356
+
2357
+ # ------------------------------------------------------------------
2358
+ # Emote & notice message types
2359
+ # ------------------------------------------------------------------
2360
+
2361
+ async def _send_simple_message(
2362
+ self,
2363
+ chat_id: str,
2364
+ text: str,
2365
+ msgtype: str,
2366
+ ) -> SendResult:
2367
+ """Send a simple message (emote, notice) with optional HTML formatting."""
2368
+ if not self._client or not text:
2369
+ return SendResult(success=False, error="No client or empty text")
2370
+
2371
+ msg_content = self._build_text_message_content(text, msgtype=msgtype)
2372
+
2373
+ try:
2374
+ event_id = await self._client.send_message_event(
2375
+ RoomID(chat_id),
2376
+ EventType.ROOM_MESSAGE,
2377
+ msg_content,
2378
+ )
2379
+ return SendResult(success=True, message_id=str(event_id))
2380
+ except Exception as exc:
2381
+ return SendResult(success=False, error=str(exc))
2382
+
2383
+ # ------------------------------------------------------------------
2384
+ # Helpers
2385
+ # ------------------------------------------------------------------
2386
+
2387
+ async def _is_dm_room(self, room_id: str) -> bool:
2388
+ """Check if a room is a DM."""
2389
+ if self._dm_rooms.get(room_id, False):
2390
+ return True
2391
+ # Fallback: check member count via state store.
2392
+ state_store = (
2393
+ getattr(self._client, "state_store", None) if self._client else None
2394
+ )
2395
+ if state_store:
2396
+ try:
2397
+ members = await state_store.get_members(room_id)
2398
+ if members and len(members) == 2:
2399
+ return True
2400
+ except Exception:
2401
+ pass
2402
+ return False
2403
+
2404
+ async def _refresh_dm_cache(self) -> None:
2405
+ """Refresh the DM room cache from m.direct account data."""
2406
+ if not self._client:
2407
+ return
2408
+
2409
+ dm_data: Optional[Dict] = None
2410
+
2411
+ try:
2412
+ resp = await self._client.get_account_data("m.direct")
2413
+ if hasattr(resp, "content"):
2414
+ dm_data = resp.content
2415
+ elif isinstance(resp, dict):
2416
+ dm_data = resp
2417
+ except Exception as exc:
2418
+ logger.debug("Matrix: get_account_data('m.direct') failed: %s", exc)
2419
+
2420
+ if dm_data is None:
2421
+ return
2422
+
2423
+ dm_room_ids: Set[str] = set()
2424
+ for user_id, rooms in dm_data.items():
2425
+ if isinstance(rooms, list):
2426
+ dm_room_ids.update(str(r) for r in rooms)
2427
+
2428
+ self._dm_rooms = {rid: (rid in dm_room_ids) for rid in self._joined_rooms}
2429
+
2430
+ # ------------------------------------------------------------------
2431
+ # Mention detection helpers
2432
+ # ------------------------------------------------------------------
2433
+
2434
+ def _build_text_message_content(self, text: str, msgtype: str = "m.text") -> Dict[str, Any]:
2435
+ """Build Matrix text content with HTML and outbound mention metadata."""
2436
+ msg_content: Dict[str, Any] = {"msgtype": msgtype, "body": text}
2437
+ mention_user_ids = self._extract_outbound_mentions(text)
2438
+ if mention_user_ids:
2439
+ msg_content["m.mentions"] = {"user_ids": mention_user_ids}
2440
+
2441
+ html_source = self._inject_outbound_mention_links(text)
2442
+ html = self._markdown_to_html(html_source)
2443
+ if html and html != text:
2444
+ msg_content["format"] = "org.matrix.custom.html"
2445
+ msg_content["formatted_body"] = html
2446
+
2447
+ return msg_content
2448
+
2449
+ def _extract_outbound_mentions(self, text: str) -> list[str]:
2450
+ """Return unique Matrix user IDs mentioned in outbound text."""
2451
+ protected, _ = self._protect_outbound_mention_regions(text)
2452
+ seen: Set[str] = set()
2453
+ mentions: list[str] = []
2454
+ for match in _OUTBOUND_MENTION_RE.finditer(protected):
2455
+ user_id = match.group(1)
2456
+ if user_id not in seen:
2457
+ seen.add(user_id)
2458
+ mentions.append(user_id)
2459
+ return mentions
2460
+
2461
+ def _inject_outbound_mention_links(self, text: str) -> str:
2462
+ """Wrap outbound Matrix mentions in markdown links outside code spans."""
2463
+ if not text:
2464
+ return text
2465
+
2466
+ protected, placeholders = self._protect_outbound_mention_regions(text)
2467
+
2468
+ linked = _OUTBOUND_MENTION_RE.sub(
2469
+ lambda match: f"[{match.group(1)}](https://matrix.to/#/{match.group(1)})",
2470
+ protected,
2471
+ )
2472
+
2473
+ for idx, original in enumerate(placeholders):
2474
+ linked = linked.replace(f"\x00MENTION_PROTECTED{idx}\x00", original)
2475
+
2476
+ return linked
2477
+
2478
+ def _protect_outbound_mention_regions(self, text: str) -> tuple[str, list[str]]:
2479
+ """Protect markdown regions where outbound mentions should stay literal."""
2480
+ placeholders: list[str] = []
2481
+
2482
+ def _protect(fragment: str) -> str:
2483
+ idx = len(placeholders)
2484
+ placeholders.append(fragment)
2485
+ return f"\x00MENTION_PROTECTED{idx}\x00"
2486
+
2487
+ protected = re.sub(
2488
+ r"```[\s\S]*?```",
2489
+ lambda match: _protect(match.group(0)),
2490
+ text or "",
2491
+ )
2492
+ protected = re.sub(
2493
+ r"`[^`\n]+`",
2494
+ lambda match: _protect(match.group(0)),
2495
+ protected,
2496
+ )
2497
+ protected = re.sub(
2498
+ r"\[[^\]]+\]\([^)]+\)",
2499
+ lambda match: _protect(match.group(0)),
2500
+ protected,
2501
+ )
2502
+
2503
+ return protected, placeholders
2504
+
2505
+ def _is_bot_mentioned(
2506
+ self,
2507
+ body: str,
2508
+ formatted_body: Optional[str] = None,
2509
+ mention_user_ids: Optional[list] = None,
2510
+ ) -> bool:
2511
+ """Return True if the bot is mentioned in the message.
2512
+
2513
+ Per MSC3952, ``m.mentions.user_ids`` is the authoritative mention
2514
+ signal in the Matrix spec. When the sender's client populates that
2515
+ field with the bot's user-id, we trust it — even when the visible
2516
+ body text does not contain an explicit ``@bot`` string (some clients
2517
+ only render mention "pills" in ``formatted_body`` or use display
2518
+ names).
2519
+ """
2520
+ # m.mentions.user_ids — authoritative per MSC3952 / Matrix v1.7.
2521
+ if mention_user_ids and self._user_id and self._user_id in mention_user_ids:
2522
+ return True
2523
+ if not body and not formatted_body:
2524
+ return False
2525
+ if self._user_id and self._user_id in body:
2526
+ return True
2527
+ if self._user_id and ":" in self._user_id:
2528
+ localpart = self._user_id.split(":")[0].lstrip("@")
2529
+ if localpart and re.search(
2530
+ r"\b" + re.escape(localpart) + r"\b", body, re.IGNORECASE
2531
+ ):
2532
+ return True
2533
+ if formatted_body and self._user_id:
2534
+ if f"matrix.to/#/{self._user_id}" in formatted_body:
2535
+ return True
2536
+ return False
2537
+
2538
+ def _strip_mention(self, body: str) -> str:
2539
+ """Remove explicit bot mentions from message body.
2540
+
2541
+ Important: only strip explicit mention tokens (``@user:server`` or
2542
+ ``@localpart``). Do NOT strip bare words matching the bot localpart,
2543
+ otherwise normal phrases like "Hermes Agent" become "Agent".
2544
+ """
2545
+ if not body:
2546
+ return ""
2547
+
2548
+ # Strip explicit full MXID mentions.
2549
+ if self._user_id:
2550
+ body = body.replace(self._user_id, "")
2551
+
2552
+ # Strip explicit @localpart mentions only (not bare localpart words).
2553
+ if self._user_id and ":" in self._user_id:
2554
+ localpart = self._user_id.split(":")[0].lstrip("@")
2555
+ if localpart:
2556
+ body = re.sub(
2557
+ r'(?<![\w])@' + re.escape(localpart) + r'\b',
2558
+ '',
2559
+ body,
2560
+ flags=re.IGNORECASE,
2561
+ )
2562
+
2563
+ # Normalize spacing after mention removal.
2564
+ body = re.sub(r'[ \t]{2,}', ' ', body)
2565
+ body = re.sub(r'\s+([,.;:!?])', r'\1', body)
2566
+ return body.strip()
2567
+
2568
+ async def _get_display_name(self, room_id: str, user_id: str) -> str:
2569
+ """Get a user's display name in a room, falling back to user_id."""
2570
+ state_store = (
2571
+ getattr(self._client, "state_store", None) if self._client else None
2572
+ )
2573
+ if state_store:
2574
+ try:
2575
+ member = await state_store.get_member(room_id, user_id)
2576
+ if member and getattr(member, "displayname", None):
2577
+ return member.displayname
2578
+ except Exception:
2579
+ pass
2580
+ # Strip the @...:server format to just the localpart.
2581
+ if user_id.startswith("@") and ":" in user_id:
2582
+ return user_id[1:].split(":")[0]
2583
+ return user_id
2584
+
2585
+ def _mxc_to_http(self, mxc_url: str) -> str:
2586
+ """Convert mxc://server/media_id to an HTTP download URL."""
2587
+ if not mxc_url.startswith("mxc://"):
2588
+ return mxc_url
2589
+ parts = mxc_url[6:] # strip mxc://
2590
+ return f"{self._homeserver}/_matrix/client/v1/media/download/{parts}"
2591
+
2592
+ def _markdown_to_html(self, text: str) -> str:
2593
+ """Convert Markdown to Matrix-compatible HTML (org.matrix.custom.html).
2594
+
2595
+ Uses the ``markdown`` library when available (installed with the
2596
+ ``matrix`` extra). Falls back to a comprehensive regex converter
2597
+ that handles fenced code blocks, inline code, headers, bold,
2598
+ italic, strikethrough, links, blockquotes, lists, and horizontal
2599
+ rules — everything the Matrix HTML spec allows.
2600
+ """
2601
+ try:
2602
+ import markdown as _md
2603
+
2604
+ md = _md.Markdown(
2605
+ extensions=["fenced_code", "tables", "nl2br", "sane_lists"],
2606
+ )
2607
+ if "html_block" in md.preprocessors:
2608
+ md.preprocessors.deregister("html_block")
2609
+
2610
+ html = md.convert(text)
2611
+ md.reset()
2612
+
2613
+ if html.count("<p>") == 1:
2614
+ html = html.replace("<p>", "").replace("</p>", "")
2615
+ return html
2616
+ except ImportError:
2617
+ pass
2618
+
2619
+ return self._markdown_to_html_fallback(text)
2620
+
2621
+ # ------------------------------------------------------------------
2622
+ # Regex-based Markdown -> HTML (no extra dependencies)
2623
+ # ------------------------------------------------------------------
2624
+
2625
+ @staticmethod
2626
+ def _sanitize_link_url(url: str) -> str:
2627
+ """Sanitize a URL for use in an href attribute."""
2628
+ stripped = url.strip()
2629
+ scheme = stripped.split(":", 1)[0].lower().strip() if ":" in stripped else ""
2630
+ if scheme in {"javascript", "data", "vbscript"}:
2631
+ return ""
2632
+ return stripped.replace('"', "&quot;")
2633
+
2634
+ @staticmethod
2635
+ def _markdown_to_html_fallback(text: str) -> str:
2636
+ """Comprehensive regex Markdown-to-HTML for Matrix."""
2637
+ placeholders: list = []
2638
+
2639
+ def _protect_html(html_fragment: str) -> str:
2640
+ idx = len(placeholders)
2641
+ placeholders.append(html_fragment)
2642
+ return f"\x00PROTECTED{idx}\x00"
2643
+
2644
+ # Fenced code blocks: ```lang\n...\n```
2645
+ result = re.sub(
2646
+ r"```(\w*)\n(.*?)```",
2647
+ lambda m: _protect_html(
2648
+ f'<pre><code class="language-{_html_escape(m.group(1))}">'
2649
+ f"{_html_escape(m.group(2))}</code></pre>"
2650
+ if m.group(1)
2651
+ else f"<pre><code>{_html_escape(m.group(2))}</code></pre>"
2652
+ ),
2653
+ text,
2654
+ flags=re.DOTALL,
2655
+ )
2656
+
2657
+ # Inline code: `code`
2658
+ result = re.sub(
2659
+ r"`([^`\n]+)`",
2660
+ lambda m: _protect_html(f"<code>{_html_escape(m.group(1))}</code>"),
2661
+ result,
2662
+ )
2663
+
2664
+ # Extract and protect markdown links before escaping.
2665
+ result = re.sub(
2666
+ r"\[([^\]]+)\]\(([^)]+)\)",
2667
+ lambda m: _protect_html(
2668
+ '<a href="{}">{}</a>'.format(
2669
+ MatrixAdapter._sanitize_link_url(m.group(2)),
2670
+ _html_escape(m.group(1)),
2671
+ )
2672
+ ),
2673
+ result,
2674
+ )
2675
+
2676
+ # HTML-escape remaining text.
2677
+ parts = re.split(r"(\x00PROTECTED\d+\x00)", result)
2678
+ for idx, part in enumerate(parts):
2679
+ if not part.startswith("\x00PROTECTED"):
2680
+ parts[idx] = _html_escape(part)
2681
+ result = "".join(parts)
2682
+
2683
+ # Block-level transforms (line-oriented).
2684
+ lines = result.split("\n")
2685
+ out_lines: list = []
2686
+ i = 0
2687
+ while i < len(lines):
2688
+ line = lines[i]
2689
+
2690
+ # Horizontal rule
2691
+ if re.match(r"^[\s]*([-*_])\s*\1\s*\1[\s\-*_]*$", line):
2692
+ out_lines.append("<hr>")
2693
+ i += 1
2694
+ continue
2695
+
2696
+ # Headers
2697
+ hdr = re.match(r"^(#{1,6})\s+(.+)$", line)
2698
+ if hdr:
2699
+ level = len(hdr.group(1))
2700
+ out_lines.append(f"<h{level}>{hdr.group(2).strip()}</h{level}>")
2701
+ i += 1
2702
+ continue
2703
+
2704
+ # Blockquote
2705
+ if (
2706
+ line.startswith("&gt; ")
2707
+ or line == "&gt;"
2708
+ or line.startswith("> ")
2709
+ or line == ">"
2710
+ ):
2711
+ bq_lines = []
2712
+ while i < len(lines) and (
2713
+ lines[i].startswith("&gt; ")
2714
+ or lines[i] == "&gt;"
2715
+ or lines[i].startswith("> ")
2716
+ or lines[i] == ">"
2717
+ ):
2718
+ ln = lines[i]
2719
+ if ln.startswith("&gt; "):
2720
+ bq_lines.append(ln[5:])
2721
+ elif ln.startswith("> "):
2722
+ bq_lines.append(ln[2:])
2723
+ else:
2724
+ bq_lines.append("")
2725
+ i += 1
2726
+ out_lines.append(f"<blockquote>{'<br>'.join(bq_lines)}</blockquote>")
2727
+ continue
2728
+
2729
+ # Unordered list
2730
+ ul_match = re.match(r"^[\s]*[-*+]\s+(.+)$", line)
2731
+ if ul_match:
2732
+ items = []
2733
+ while i < len(lines) and re.match(r"^[\s]*[-*+]\s+(.+)$", lines[i]):
2734
+ items.append(re.match(r"^[\s]*[-*+]\s+(.+)$", lines[i]).group(1))
2735
+ i += 1
2736
+ li = "".join(f"<li>{item}</li>" for item in items)
2737
+ out_lines.append(f"<ul>{li}</ul>")
2738
+ continue
2739
+
2740
+ # Ordered list
2741
+ ol_match = re.match(r"^[\s]*\d+[.)]\s+(.+)$", line)
2742
+ if ol_match:
2743
+ items = []
2744
+ while i < len(lines) and re.match(r"^[\s]*\d+[.)]\s+(.+)$", lines[i]):
2745
+ items.append(re.match(r"^[\s]*\d+[.)]\s+(.+)$", lines[i]).group(1))
2746
+ i += 1
2747
+ li = "".join(f"<li>{item}</li>" for item in items)
2748
+ out_lines.append(f"<ol>{li}</ol>")
2749
+ continue
2750
+
2751
+ out_lines.append(line)
2752
+ i += 1
2753
+
2754
+ result = "\n".join(out_lines)
2755
+
2756
+ # Inline transforms.
2757
+ result = re.sub(
2758
+ r"\*\*(.+?)\*\*", r"<strong>\1</strong>", result, flags=re.DOTALL
2759
+ )
2760
+ result = re.sub(r"__(.+?)__", r"<strong>\1</strong>", result, flags=re.DOTALL)
2761
+ result = re.sub(r"\*(.+?)\*", r"<em>\1</em>", result, flags=re.DOTALL)
2762
+ result = re.sub(
2763
+ r"(?<!\w)_(.+?)_(?!\w)", r"<em>\1</em>", result, flags=re.DOTALL
2764
+ )
2765
+ result = re.sub(r"~~(.+?)~~", r"<del>\1</del>", result, flags=re.DOTALL)
2766
+ result = re.sub(r"\n", "<br>\n", result)
2767
+ result = re.sub(
2768
+ r"<br>\n(</?(?:pre|blockquote|h[1-6]|ul|ol|li|hr))", r"\n\1", result
2769
+ )
2770
+ result = re.sub(r"(</(?:pre|blockquote|h[1-6]|ul|ol|li)>)<br>", r"\1", result)
2771
+
2772
+ # Restore protected regions.
2773
+ for idx, original in enumerate(placeholders):
2774
+ result = result.replace(f"\x00PROTECTED{idx}\x00", original)
2775
+
2776
+ return result
2777
+