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,3028 @@
1
+ """
2
+ Slack platform adapter.
3
+
4
+ Uses slack-bolt (Python) with Socket Mode for:
5
+ - Receiving messages from channels and DMs
6
+ - Sending responses back
7
+ - Handling slash commands
8
+ - Thread support
9
+ """
10
+
11
+ import asyncio
12
+ import contextvars
13
+ import json
14
+ import logging
15
+ import os
16
+ import re
17
+ import time
18
+ from dataclasses import dataclass, field
19
+ from typing import Dict, Optional, Any, Tuple, List
20
+
21
+ try:
22
+ from slack_bolt.async_app import AsyncApp
23
+ from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
24
+ from slack_sdk.web.async_client import AsyncWebClient
25
+ import aiohttp
26
+ SLACK_AVAILABLE = True
27
+ except ImportError:
28
+ SLACK_AVAILABLE = False
29
+ AsyncApp = Any
30
+ AsyncSocketModeHandler = Any
31
+ AsyncWebClient = Any
32
+
33
+ import sys
34
+ from pathlib import Path as _Path
35
+ sys.path.insert(0, str(_Path(__file__).resolve().parents[2]))
36
+
37
+ from gateway.config import Platform, PlatformConfig
38
+ from gateway.platforms.helpers import MessageDeduplicator
39
+ from gateway.platforms.base import (
40
+ BasePlatformAdapter,
41
+ MessageEvent,
42
+ MessageType,
43
+ ProcessingOutcome,
44
+ SendResult,
45
+ SUPPORTED_DOCUMENT_TYPES,
46
+ is_host_excluded_by_no_proxy,
47
+ resolve_proxy_url,
48
+ safe_url_for_log,
49
+ cache_document_from_bytes,
50
+ )
51
+
52
+
53
+ logger = logging.getLogger(__name__)
54
+
55
+ # ContextVar carrying the user_id of the slash-command invoker.
56
+ # Set in _handle_slash_command, read in send() to match the correct
57
+ # stashed response_url when multiple users issue commands on the same
58
+ # channel concurrently. ContextVars propagate to child asyncio.Tasks
59
+ # (Python 3.7+), so the value set in _handle_slash_command's task is
60
+ # visible in _process_message_background's child task.
61
+ _slash_user_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
62
+ "_slash_user_id", default=None,
63
+ )
64
+
65
+
66
+ @dataclass
67
+ class _ThreadContextCache:
68
+ """Cache entry for fetched thread context."""
69
+ content: str
70
+ fetched_at: float = field(default_factory=time.monotonic)
71
+ message_count: int = 0
72
+ parent_text: str = "" # Raw text of the thread parent (for reply_to_text injection)
73
+
74
+
75
+ def check_slack_requirements() -> bool:
76
+ """Check if Slack dependencies are available.
77
+
78
+ Lazy-installs slack-bolt/slack-sdk via ``tools.lazy_deps.ensure("platform.slack")``
79
+ on first call if not present. Rebinds all module-level globals on success.
80
+ """
81
+ if SLACK_AVAILABLE:
82
+ return True
83
+
84
+ def _import():
85
+ from slack_bolt.async_app import AsyncApp
86
+ from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
87
+ from slack_sdk.web.async_client import AsyncWebClient
88
+ import aiohttp
89
+ return {
90
+ "AsyncApp": AsyncApp,
91
+ "AsyncSocketModeHandler": AsyncSocketModeHandler,
92
+ "AsyncWebClient": AsyncWebClient,
93
+ "aiohttp": aiohttp,
94
+ "SLACK_AVAILABLE": True,
95
+ }
96
+
97
+ from tools.lazy_deps import ensure_and_bind
98
+ return ensure_and_bind("platform.slack", _import, globals(), prompt=False)
99
+
100
+
101
+ def _extract_text_from_slack_blocks(blocks: list) -> str:
102
+ """Extract readable text from Slack Block Kit blocks, including quoted/forwarded content.
103
+
104
+ Slack's modern WYSIWYG composer sends messages with a ``blocks`` array
105
+ containing ``rich_text`` elements. When a user forwards or quotes another
106
+ message, the quoted content appears as nested ``rich_text_quote`` elements
107
+ that are *not* included in the plain ``text`` field of the event.
108
+
109
+ This helper walks the rich-text tree recursively and returns readable lines,
110
+ preserving quotes, list items, and preformatted blocks so the agent can see
111
+ forwarded/quoted content instead of only the lossy plain-text field.
112
+ """
113
+ if not blocks:
114
+ return ""
115
+
116
+ parts: list[str] = []
117
+
118
+ def _render_inline_elements(elements: list) -> str:
119
+ """Render inline elements (text, link, channel, user, emoji, etc.)."""
120
+ pieces: list[str] = []
121
+ for el in elements:
122
+ el_type = el.get("type", "")
123
+ if el_type == "text":
124
+ pieces.append(el.get("text", ""))
125
+ elif el_type == "link":
126
+ url = el.get("url", "")
127
+ text = el.get("text", "") or url
128
+ pieces.append(f"{text} ({url})")
129
+ elif el_type == "channel":
130
+ pieces.append(f"<#{el.get('channel_id', '')}>")
131
+ elif el_type == "user":
132
+ pieces.append(f"<@{el.get('user_id', '')}>")
133
+ elif el_type == "usergroup":
134
+ pieces.append(f"<!subteam^{el.get('usergroup_id', '')}>")
135
+ elif el_type == "emoji":
136
+ pieces.append(f":{el.get('name', '')}:")
137
+ elif el_type == "broadcast":
138
+ pieces.append(f"<!{el.get('range', 'here')}>")
139
+ elif el_type == "date":
140
+ pieces.append(el.get("fallback", ""))
141
+ return "".join(pieces)
142
+
143
+ def _append_line(text: str, quote_depth: int = 0, bullet: str = "") -> None:
144
+ if not text or not text.strip():
145
+ return
146
+ prefix = ((">" * quote_depth) + " ") if quote_depth else ""
147
+ parts.append(f"{prefix}{bullet}{text}".rstrip())
148
+
149
+ def _walk_elements(elements: list, quote_depth: int = 0, bullet: str = "") -> None:
150
+ for elem in elements:
151
+ elem_type = elem.get("type", "")
152
+
153
+ if elem_type == "rich_text_section":
154
+ _append_line(
155
+ _render_inline_elements(elem.get("elements", [])),
156
+ quote_depth=quote_depth,
157
+ bullet=bullet,
158
+ )
159
+ elif elem_type == "rich_text_quote":
160
+ _walk_elements(elem.get("elements", []), quote_depth=quote_depth + 1)
161
+ elif elem_type == "rich_text_list":
162
+ list_style = elem.get("style")
163
+ for idx, item in enumerate(elem.get("elements", [])):
164
+ item_bullet = "• " if list_style == "bullet" else f"{idx + 1}. "
165
+ _walk_elements([item], quote_depth=quote_depth, bullet=item_bullet)
166
+ elif elem_type == "rich_text_preformatted":
167
+ code_lines: list[str] = []
168
+ for child in elem.get("elements", []):
169
+ child_type = child.get("type", "")
170
+ if child_type == "rich_text_section":
171
+ rendered = _render_inline_elements(child.get("elements", []))
172
+ else:
173
+ rendered = _render_inline_elements([child])
174
+ if rendered:
175
+ code_lines.append(rendered)
176
+ code_text = "\n".join(code_lines)
177
+ if code_text:
178
+ lang = elem.get("language", "")
179
+ _append_line(f"```{lang}\n{code_text}\n```", quote_depth=quote_depth, bullet=bullet)
180
+ else:
181
+ rendered = _render_inline_elements([elem])
182
+ if rendered:
183
+ _append_line(rendered, quote_depth=quote_depth, bullet=bullet)
184
+
185
+ for block in blocks:
186
+ if (block or {}).get("type") == "rich_text":
187
+ _walk_elements(block.get("elements", []))
188
+
189
+ return "\n".join(parts)
190
+
191
+
192
+ def _serialize_slack_blocks_for_agent(blocks: list, max_chars: int = 6000) -> str:
193
+ """Return a compact, redacted JSON view of the current message's Block Kit payload."""
194
+ if not blocks:
195
+ return ""
196
+
197
+ if all((block or {}).get("type") == "rich_text" for block in blocks):
198
+ return ""
199
+
200
+ scalar_allowlist = {
201
+ "type",
202
+ "block_id",
203
+ "action_id",
204
+ "style",
205
+ "dispatch_action",
206
+ "optional",
207
+ "multiple",
208
+ "emoji",
209
+ }
210
+ recursive_allowlist = {
211
+ "text",
212
+ "title",
213
+ "description",
214
+ "label",
215
+ "placeholder",
216
+ "accessory",
217
+ "fields",
218
+ "elements",
219
+ "options",
220
+ "option_groups",
221
+ "confirm",
222
+ "submit",
223
+ "close",
224
+ "hint",
225
+ }
226
+
227
+ def _sanitize(value):
228
+ if isinstance(value, list):
229
+ return [item for item in (_sanitize(v) for v in value) if item not in (None, {}, [], "")]
230
+ if isinstance(value, dict):
231
+ sanitized = {}
232
+ for key, item in value.items():
233
+ if key in scalar_allowlist:
234
+ sanitized[key] = item
235
+ elif key in recursive_allowlist:
236
+ cleaned = _sanitize(item)
237
+ if cleaned not in (None, {}, [], ""):
238
+ sanitized[key] = cleaned
239
+ return sanitized
240
+ if isinstance(value, (str, int, float, bool)) or value is None:
241
+ return value
242
+ return repr(value)
243
+
244
+ try:
245
+ payload = json.dumps(_sanitize(blocks), ensure_ascii=False, indent=2)
246
+ except Exception:
247
+ payload = repr(blocks)
248
+
249
+ if len(payload) > max_chars:
250
+ payload = payload[: max_chars - 18].rstrip() + "\n... [truncated]"
251
+
252
+ return f"[Slack Block Kit payload for this message]\n```json\n{payload}\n```"
253
+
254
+
255
+ def _apply_slack_proxy(client: Any, proxy_url: Optional[str]) -> None:
256
+ """Apply a resolved proxy to a Slack SDK client or clear it explicitly."""
257
+ if hasattr(client, "proxy"):
258
+ client.proxy = proxy_url
259
+
260
+
261
+ _SLACK_PROXY_HOSTS = (
262
+ "slack.com",
263
+ "files.slack.com",
264
+ "wss-primary.slack.com",
265
+ )
266
+
267
+
268
+ def _resolve_slack_proxy_url() -> Optional[str]:
269
+ """Resolve a proxy URL that Slack SDK clients can safely use."""
270
+ proxy_url = resolve_proxy_url()
271
+ if not proxy_url:
272
+ return None
273
+
274
+ normalized = proxy_url.lower()
275
+ if not normalized.startswith(("http://", "https://")):
276
+ logger.info(
277
+ "[Slack] Ignoring unsupported proxy scheme for Slack transport: %s",
278
+ safe_url_for_log(proxy_url),
279
+ )
280
+ return None
281
+
282
+ if any(is_host_excluded_by_no_proxy(host) for host in _SLACK_PROXY_HOSTS):
283
+ logger.info("[Slack] NO_PROXY bypasses Slack proxy configuration")
284
+ return None
285
+
286
+ return proxy_url
287
+
288
+
289
+ class SlackAdapter(BasePlatformAdapter):
290
+ """
291
+ Slack bot adapter using Socket Mode.
292
+
293
+ Requires two tokens:
294
+ - SLACK_BOT_TOKEN (xoxb-...) for API calls
295
+ - SLACK_APP_TOKEN (xapp-...) for Socket Mode connection
296
+
297
+ Features:
298
+ - DMs and channel messages (mention-gated in channels)
299
+ - Thread support
300
+ - File/image/audio attachments
301
+ - Slash commands (/hermes)
302
+ - Typing indicators (not natively supported by Slack bots)
303
+ """
304
+
305
+ MAX_MESSAGE_LENGTH = 39000 # Slack API allows 40,000 chars; leave margin
306
+
307
+ def __init__(self, config: PlatformConfig):
308
+ super().__init__(config, Platform.SLACK)
309
+ self._app: Optional[Any] = None
310
+ self._handler: Optional[Any] = None
311
+ self._bot_user_id: Optional[str] = None
312
+ self._user_name_cache: Dict[str, str] = {} # user_id → display name
313
+ self._socket_mode_task: Optional[asyncio.Task] = None
314
+ # Multi-workspace support
315
+ self._team_clients: Dict[str, Any] = {} # team_id → WebClient
316
+ self._team_bot_user_ids: Dict[str, str] = {} # team_id → bot_user_id
317
+ self._channel_team: Dict[str, str] = {} # channel_id → team_id
318
+ # Dedup cache: prevents duplicate bot responses when Socket Mode
319
+ # reconnects redeliver events.
320
+ self._dedup = MessageDeduplicator()
321
+ # Track pending approval message_ts → resolved flag to prevent
322
+ # double-clicks on approval buttons.
323
+ self._approval_resolved: Dict[str, bool] = {}
324
+ # Track timestamps of messages sent by the bot so we can respond
325
+ # to thread replies even without an explicit @mention.
326
+ self._bot_message_ts: set = set()
327
+ self._BOT_TS_MAX = 5000 # cap to avoid unbounded growth
328
+ # Track threads where the bot has been @mentioned — once mentioned,
329
+ # respond to ALL subsequent messages in that thread automatically.
330
+ self._mentioned_threads: set = set()
331
+ self._MENTIONED_THREADS_MAX = 5000
332
+ # Assistant thread metadata keyed by (channel_id, thread_ts). Slack's
333
+ # AI Assistant lifecycle events can arrive before/alongside message
334
+ # events, and they carry the user/thread identity needed for stable
335
+ # session + memory scoping.
336
+ self._assistant_threads: Dict[Tuple[str, str], Dict[str, str]] = {}
337
+ self._ASSISTANT_THREADS_MAX = 5000
338
+ # Cache for _fetch_thread_context results: cache_key → _ThreadContextCache
339
+ self._thread_context_cache: Dict[str, _ThreadContextCache] = {}
340
+ self._THREAD_CACHE_TTL = 60.0
341
+ # Track message IDs that should get reaction lifecycle (DMs / @mentions).
342
+ self._reacting_message_ids: set = set()
343
+ # Track active assistant thread status indicators so stop_typing can
344
+ # clear them (chat_id → thread_ts).
345
+ self._active_status_threads: Dict[str, str] = {}
346
+ # Slash-command contexts: stash response_url + user_id so send()
347
+ # can route the first reply ephemerally. Keyed by
348
+ # (channel_id, user_id) to avoid cross-user collisions.
349
+ # Each value: {"response_url": str, "ts": float}
350
+ self._slash_command_contexts: Dict[Tuple[str, str], Dict[str, Any]] = {}
351
+
352
+ def _describe_slack_api_error(self, response: Any, *, file_obj: Optional[Dict[str, Any]] = None) -> Optional[str]:
353
+ """Convert Slack API auth/permission failures into actionable user-facing text."""
354
+ if response is None or not hasattr(response, "get"):
355
+ return None
356
+
357
+ error = str(response.get("error", "") or "").strip()
358
+ if not error:
359
+ return None
360
+
361
+ file_label = str((file_obj or {}).get("name") or (file_obj or {}).get("id") or "this attachment")
362
+ needed = str(response.get("needed", "") or "").strip()
363
+ provided = str(response.get("provided", "") or "").strip()
364
+ reinstall_hint = " Update the Slack app scopes/settings and reinstall the app to the workspace."
365
+ provided_hint = f" Current bot scopes: {provided}." if provided else ""
366
+
367
+ if error == "missing_scope":
368
+ needed_hint = f"Missing scope: {needed}." if needed else "Missing required Slack scope."
369
+ return f"Slack attachment access failed for {file_label}. {needed_hint}{provided_hint}{reinstall_hint}"
370
+ if error in {"not_authed", "invalid_auth", "account_inactive", "token_revoked"}:
371
+ return f"Slack attachment access failed for {file_label} because the bot token is not authorized ({error}). Refresh the token/reinstall the app."
372
+ if error in {"file_not_found", "file_deleted"}:
373
+ return f"Slack attachment {file_label} is no longer available ({error})."
374
+ if error in {"access_denied", "file_access_denied", "no_permission", "not_allowed_token_type", "restricted_action"}:
375
+ return f"Slack attachment access failed for {file_label} because the bot does not have permission ({error}). Check workspace permissions/scopes and reinstall if needed."
376
+ return None
377
+
378
+ def _describe_slack_download_failure(self, exc: Exception, *, file_obj: Optional[Dict[str, Any]] = None) -> Optional[str]:
379
+ """Translate Slack download exceptions into user-facing attachment diagnostics."""
380
+ file_label = str((file_obj or {}).get("name") or (file_obj or {}).get("id") or "this attachment")
381
+
382
+ response = getattr(exc, "response", None)
383
+ api_detail = self._describe_slack_api_error(response, file_obj=file_obj)
384
+ if api_detail:
385
+ return api_detail
386
+
387
+ try:
388
+ import httpx
389
+ except Exception: # pragma: no cover
390
+ httpx = None
391
+
392
+ if httpx is not None and isinstance(exc, httpx.HTTPStatusError):
393
+ status = exc.response.status_code
394
+ if status == 401:
395
+ return f"Slack attachment access failed for {file_label} with HTTP 401. The bot token is not authorized for this file."
396
+ if status == 403:
397
+ return f"Slack attachment access failed for {file_label} with HTTP 403. The bot likely lacks permission or scope to read this file."
398
+ if status == 404:
399
+ return f"Slack attachment {file_label} returned HTTP 404 and is no longer reachable."
400
+
401
+ message = str(exc)
402
+ if "Slack returned HTML instead of media" in message or "non-image data" in message:
403
+ return (
404
+ f"Slack attachment access failed for {file_label}: Slack returned an HTML/login or non-media response. "
405
+ "This usually means a scope, auth, or file-permission problem."
406
+ )
407
+ return None
408
+
409
+ # ------------------------------------------------------------------
410
+ # Slash-command ephemeral helpers
411
+ # ------------------------------------------------------------------
412
+
413
+ _SLASH_CTX_TTL = 120.0 # seconds — response_url is valid for 30 min;
414
+ # we use a much shorter TTL to avoid routing unrelated messages
415
+ # as ephemeral if the command handler was slow or dropped.
416
+
417
+ def _pop_slash_context(
418
+ self, chat_id: str,
419
+ ) -> Optional[Dict[str, Any]]:
420
+ """Return and remove the slash-command context for *chat_id*, if fresh.
421
+
422
+ Contexts older than ``_SLASH_CTX_TTL`` seconds are silently discarded.
423
+
424
+ Uses the ``_slash_user_id`` ContextVar (set in ``_handle_slash_command``)
425
+ to match the exact ``(channel_id, user_id)`` key. This prevents a
426
+ concurrent slash command from a different user on the same channel from
427
+ stealing another user's ephemeral context. Falls back to a
428
+ channel-only scan when the ContextVar is unset (e.g. send() called
429
+ from a non-slash code path — should not match anything).
430
+ """
431
+ now = time.monotonic()
432
+ # Clean up stale entries on every lookup — dict is small.
433
+ stale_keys = [
434
+ k for k, v in self._slash_command_contexts.items()
435
+ if now - v["ts"] > self._SLASH_CTX_TTL
436
+ ]
437
+ for k in stale_keys:
438
+ self._slash_command_contexts.pop(k, None)
439
+
440
+ # Precise match: (channel_id, user_id) from ContextVar.
441
+ uid = _slash_user_id.get()
442
+ if uid:
443
+ return self._slash_command_contexts.pop((chat_id, uid), None)
444
+
445
+ # Fallback: channel-only scan (only reachable when ContextVar is
446
+ # unset, i.e. send() called outside a slash-command async context).
447
+ match_key = None
448
+ for key in list(self._slash_command_contexts):
449
+ if key[0] == chat_id:
450
+ match_key = key
451
+ break
452
+ if match_key is None:
453
+ return None
454
+ return self._slash_command_contexts.pop(match_key)
455
+
456
+ async def _send_slash_ephemeral(
457
+ self,
458
+ ctx: Dict[str, Any],
459
+ content: str,
460
+ ) -> "SendResult":
461
+ """Replace the initial ephemeral ack via ``response_url``.
462
+
463
+ Slack's ``response_url`` accepts a POST with ``replace_original``
464
+ for up to 30 minutes after the slash command was invoked. This
465
+ lets us swap the "Running /cmd…" placeholder with the real reply,
466
+ and the message stays ephemeral ("Only visible to you").
467
+
468
+ Falls back to a simple ``True`` SendResult if the POST fails —
469
+ the user already saw the initial ack, so a delivery failure here
470
+ is non-critical.
471
+ """
472
+ formatted = self.format_message(content)
473
+ # Slack's response_url has the same ~40k char limit as chat_postMessage.
474
+ # Truncate to MAX_MESSAGE_LENGTH and use only the first chunk — the
475
+ # response_url replaces a single ephemeral ack, so multi-chunk isn't
476
+ # possible. Long responses are rare for command replies.
477
+ chunks = self.truncate_message(formatted, self.MAX_MESSAGE_LENGTH)
478
+ text = chunks[0] if chunks else formatted
479
+ payload = {
480
+ "response_type": "ephemeral",
481
+ "replace_original": True,
482
+ "text": text,
483
+ }
484
+ try:
485
+ async with aiohttp.ClientSession() as session:
486
+ async with session.post(
487
+ ctx["response_url"],
488
+ json=payload,
489
+ timeout=aiohttp.ClientTimeout(total=10),
490
+ ) as resp:
491
+ if resp.status == 200:
492
+ return SendResult(success=True, message_id=None)
493
+ body = await resp.text()
494
+ logger.warning(
495
+ "[Slack] response_url POST returned %s: %s",
496
+ resp.status,
497
+ body[:200],
498
+ )
499
+ except Exception as e:
500
+ logger.warning(
501
+ "[Slack] response_url POST failed: %s", e,
502
+ )
503
+ # Non-fatal — the user saw the initial ack already.
504
+ return SendResult(success=True, message_id=None)
505
+
506
+ async def connect(self) -> bool:
507
+ """Connect to Slack via Socket Mode."""
508
+ if not SLACK_AVAILABLE:
509
+ logger.error(
510
+ "[Slack] slack-bolt not installed. Run: pip install slack-bolt",
511
+ )
512
+ return False
513
+
514
+ raw_token = self.config.token
515
+ app_token = os.getenv("SLACK_APP_TOKEN")
516
+
517
+ if not raw_token:
518
+ logger.error("[Slack] SLACK_BOT_TOKEN not set")
519
+ return False
520
+ if not app_token:
521
+ logger.error("[Slack] SLACK_APP_TOKEN not set")
522
+ return False
523
+
524
+ proxy_url = _resolve_slack_proxy_url()
525
+ if proxy_url:
526
+ logger.info("[Slack] Using proxy for Slack transport: %s", safe_url_for_log(proxy_url))
527
+
528
+ # Support comma-separated bot tokens for multi-workspace
529
+ bot_tokens = [t.strip() for t in raw_token.split(",") if t.strip()]
530
+
531
+ # Also load tokens from OAuth token file
532
+ from calvyn_constants import get_hermes_home
533
+ tokens_file = get_hermes_home() / "slack_tokens.json"
534
+ if tokens_file.exists():
535
+ try:
536
+ saved = json.loads(tokens_file.read_text(encoding="utf-8"))
537
+ for team_id, entry in saved.items():
538
+ tok = entry.get("token", "") if isinstance(entry, dict) else ""
539
+ if tok and tok not in bot_tokens:
540
+ bot_tokens.append(tok)
541
+ team_label = entry.get("team_name", team_id) if isinstance(entry, dict) else team_id
542
+ logger.info("[Slack] Loaded saved token for workspace %s", team_label)
543
+ except Exception as e:
544
+ logger.warning("[Slack] Failed to read %s: %s", tokens_file, e)
545
+
546
+ lock_acquired = False
547
+ try:
548
+ if not self._acquire_platform_lock('slack-app-token', app_token, 'Slack app token'):
549
+ return False
550
+ lock_acquired = True
551
+
552
+ # Close any previous handler before creating a new one so that
553
+ # calling connect() a second time (e.g. during a gateway restart or
554
+ # in-process reconnect attempt) does not leave a zombie Socket Mode
555
+ # connection alive. Both the old and new connections would otherwise
556
+ # receive every Slack event and dispatch it twice, producing double
557
+ # responses — the same bug that affected DiscordAdapter (#18187).
558
+ if self._handler is not None:
559
+ try:
560
+ await self._handler.close_async()
561
+ except Exception:
562
+ logger.debug("[%s] Failed to close previous Slack handler", self.name)
563
+ finally:
564
+ self._handler = None
565
+ self._app = None
566
+
567
+ # First token is the primary — used for AsyncApp / Socket Mode
568
+ primary_token = bot_tokens[0]
569
+ self._app = AsyncApp(token=primary_token)
570
+ _apply_slack_proxy(self._app.client, proxy_url)
571
+
572
+ # Register each bot token and map team_id → client
573
+ for token in bot_tokens:
574
+ client = AsyncWebClient(token=token)
575
+ _apply_slack_proxy(client, proxy_url)
576
+ auth_response = await client.auth_test()
577
+ team_id = auth_response.get("team_id", "")
578
+ bot_user_id = auth_response.get("user_id", "")
579
+ bot_name = auth_response.get("user", "unknown")
580
+ team_name = auth_response.get("team", "unknown")
581
+
582
+ self._team_clients[team_id] = client
583
+ self._team_bot_user_ids[team_id] = bot_user_id
584
+
585
+ # First token sets the primary bot_user_id (backward compat)
586
+ if self._bot_user_id is None:
587
+ self._bot_user_id = bot_user_id
588
+
589
+ logger.info(
590
+ "[Slack] Authenticated as @%s in workspace %s (team: %s)",
591
+ bot_name, team_name, team_id,
592
+ )
593
+
594
+ # Register message event handler
595
+ @self._app.event("message")
596
+ async def handle_message_event(event, say):
597
+ await self._handle_slack_message(event)
598
+
599
+ # Handle app_mention explicitly. In some Slack app configurations,
600
+ # channel mentions arrive only as app_mention events rather than the
601
+ # generic message event. Forward them into the normal message
602
+ # pipeline so @mentions reliably produce replies.
603
+ # NOTE: when Slack fires BOTH message and app_mention for the same
604
+ # @mention, they share the same event ts — the dedup in
605
+ # _handle_slack_message (MessageDeduplicator) suppresses the second.
606
+ @self._app.event("app_mention")
607
+ async def handle_app_mention(event, say):
608
+ await self._handle_slack_message(event)
609
+
610
+ # File lifecycle events can arrive around snippet uploads even when
611
+ # the actual user message is what we care about. Ack them so Slack
612
+ # doesn't log noisy 404 "unhandled request" warnings.
613
+ @self._app.event("file_shared")
614
+ async def handle_file_shared(event, say):
615
+ pass
616
+
617
+ @self._app.event("file_created")
618
+ async def handle_file_created(event, say):
619
+ pass
620
+
621
+ @self._app.event("file_change")
622
+ async def handle_file_change(event, say):
623
+ pass
624
+
625
+ @self._app.event("assistant_thread_started")
626
+ async def handle_assistant_thread_started(event, say):
627
+ await self._handle_assistant_thread_lifecycle_event(event)
628
+
629
+ @self._app.event("assistant_thread_context_changed")
630
+ async def handle_assistant_thread_context_changed(event, say):
631
+ await self._handle_assistant_thread_lifecycle_event(event)
632
+
633
+ # Register slash command handler(s)
634
+ #
635
+ # Every gateway command from COMMAND_REGISTRY is a native Slack
636
+ # slash, matching Discord and Telegram's model (e.g. /btw, /stop,
637
+ # /model work directly without /hermes prefix). A single regex
638
+ # matcher dispatches all of them to one handler so we don't need
639
+ # N identical @app.command() decorators.
640
+ #
641
+ # The slash commands must ALSO be declared in the Slack app
642
+ # manifest (see `hermes slack manifest`). In Socket Mode, Slack
643
+ # routes the command event through the socket regardless of the
644
+ # manifest's request URL, but it will not deliver an event for
645
+ # a slash command the manifest doesn't declare.
646
+ from hermes_cli.commands import slack_native_slashes
647
+ import re as _re
648
+
649
+ _slash_names = [name for name, _d, _h in slack_native_slashes()]
650
+ if _slash_names:
651
+ _slash_pattern = _re.compile(
652
+ r"^/(?:" + "|".join(_re.escape(n) for n in _slash_names) + r")$"
653
+ )
654
+ else: # pragma: no cover - registry always non-empty
655
+ _slash_pattern = _re.compile(r"^/hermes$")
656
+
657
+ @self._app.command(_slash_pattern)
658
+ async def handle_hermes_command(ack, command):
659
+ slash = (command.get("command") or "").lstrip("/")
660
+ await ack(
661
+ response_type="ephemeral",
662
+ text=f"Running `/{slash}`…",
663
+ )
664
+ await self._handle_slash_command(command)
665
+
666
+ # Register Block Kit action handlers for approval buttons
667
+ for _action_id in (
668
+ "hermes_approve_once",
669
+ "hermes_approve_session",
670
+ "hermes_approve_always",
671
+ "hermes_deny",
672
+ ):
673
+ self._app.action(_action_id)(self._handle_approval_action)
674
+
675
+ # Register Block Kit action handlers for slash-confirm buttons
676
+ # (generic three-option prompts; see tools/slash_confirm.py).
677
+ for _action_id in (
678
+ "hermes_confirm_once",
679
+ "hermes_confirm_always",
680
+ "hermes_confirm_cancel",
681
+ ):
682
+ self._app.action(_action_id)(self._handle_slash_confirm_action)
683
+
684
+ # Start Socket Mode handler in background
685
+ self._handler = AsyncSocketModeHandler(self._app, app_token, proxy=proxy_url)
686
+ _apply_slack_proxy(self._handler.client, proxy_url)
687
+ self._socket_mode_task = asyncio.create_task(self._handler.start_async())
688
+
689
+ self._running = True
690
+ logger.info(
691
+ "[Slack] Socket Mode connected (%d workspace(s))",
692
+ len(self._team_clients),
693
+ )
694
+ return True
695
+
696
+ except Exception as e: # pragma: no cover - defensive logging
697
+ logger.error("[Slack] Connection failed: %s", e, exc_info=True)
698
+ return False
699
+ finally:
700
+ if lock_acquired and not self._running:
701
+ self._release_platform_lock()
702
+
703
+ async def create_handoff_thread(
704
+ self,
705
+ parent_chat_id: str,
706
+ name: str,
707
+ ) -> Optional[str]:
708
+ """Create a Slack thread anchor for a session handoff.
709
+
710
+ Slack threads are anchored to a parent message (``thread_ts``), not
711
+ a channel-level construct. So we post a seed message into the home
712
+ channel and return its ``ts`` — the watcher uses that as the
713
+ ``thread_id`` for subsequent sends.
714
+
715
+ Returns the seed message ts as a string, or ``None`` on failure.
716
+ """
717
+ if not self._app:
718
+ return None
719
+ try:
720
+ client = self._get_client(parent_chat_id)
721
+ if client is None:
722
+ return None
723
+ seed_text = f":thread: Hermes handoff — *{(name or 'session').strip()[:80]}*"
724
+ result = await client.chat_postMessage(
725
+ channel=parent_chat_id,
726
+ text=seed_text,
727
+ )
728
+ ts = result.get("ts") if isinstance(result, dict) else getattr(result, "get", lambda _k, _d=None: None)("ts")
729
+ if ts:
730
+ return str(ts)
731
+ except Exception as exc:
732
+ logger.warning(
733
+ "[%s] Handoff thread: seed-post failed for channel %s: %s",
734
+ self.name, parent_chat_id, exc,
735
+ )
736
+ return None
737
+
738
+ async def disconnect(self) -> None:
739
+ """Disconnect from Slack."""
740
+ if self._handler:
741
+ try:
742
+ await self._handler.close_async()
743
+ except Exception as e: # pragma: no cover - defensive logging
744
+ logger.warning("[Slack] Error while closing Socket Mode handler: %s", e, exc_info=True)
745
+ self._running = False
746
+
747
+ self._release_platform_lock()
748
+
749
+ logger.info("[Slack] Disconnected")
750
+
751
+ def _get_client(self, chat_id: str) -> Any:
752
+ """Return the workspace-specific WebClient for a channel."""
753
+ team_id = self._channel_team.get(chat_id)
754
+ if team_id and team_id in self._team_clients:
755
+ return self._team_clients[team_id]
756
+ return self._app.client # fallback to primary
757
+
758
+ async def send(
759
+ self,
760
+ chat_id: str,
761
+ content: str,
762
+ reply_to: Optional[str] = None,
763
+ metadata: Optional[Dict[str, Any]] = None,
764
+ ) -> SendResult:
765
+ """Send a message to a Slack channel or DM."""
766
+ if not self._app:
767
+ return SendResult(success=False, error="Not connected")
768
+
769
+ try:
770
+ # Check for a pending slash-command context. When the user ran a
771
+ # native slash command (e.g. /q, /stop, /model), the initial ack
772
+ # already showed an ephemeral "Running /cmd…" message. If we have
773
+ # a stashed response_url for this channel, replace that ack with
774
+ # the actual command reply ephemerally instead of posting publicly.
775
+ slash_ctx = self._pop_slash_context(chat_id)
776
+ if slash_ctx:
777
+ return await self._send_slash_ephemeral(
778
+ slash_ctx, content,
779
+ )
780
+
781
+ # Convert standard markdown → Slack mrkdwn
782
+ formatted = self.format_message(content)
783
+
784
+ # Split long messages, preserving code block boundaries
785
+ chunks = self.truncate_message(formatted, self.MAX_MESSAGE_LENGTH)
786
+
787
+ thread_ts = self._resolve_thread_ts(reply_to, metadata)
788
+ last_result = None
789
+
790
+ # reply_broadcast: also post thread replies to the main channel.
791
+ # Controlled via platform config: gateway.slack.reply_broadcast
792
+ broadcast = self.config.extra.get("reply_broadcast", False)
793
+
794
+ for i, chunk in enumerate(chunks):
795
+ kwargs = {
796
+ "channel": chat_id,
797
+ "text": chunk,
798
+ "mrkdwn": True,
799
+ }
800
+ if thread_ts:
801
+ kwargs["thread_ts"] = thread_ts
802
+ # Only broadcast the first chunk of the first reply
803
+ if broadcast and i == 0:
804
+ kwargs["reply_broadcast"] = True
805
+
806
+ last_result = await self._get_client(chat_id).chat_postMessage(**kwargs)
807
+
808
+ # Clear Slack Assistant status as soon as the final message is posted.
809
+ if thread_ts:
810
+ await self.stop_typing(chat_id)
811
+
812
+ # Track the sent message ts so we can auto-respond to thread
813
+ # replies without requiring @mention.
814
+ sent_ts = last_result.get("ts") if last_result else None
815
+ if sent_ts:
816
+ self._bot_message_ts.add(sent_ts)
817
+ # Also register the thread root so replies-to-my-replies work
818
+ if thread_ts:
819
+ self._bot_message_ts.add(thread_ts)
820
+ if len(self._bot_message_ts) > self._BOT_TS_MAX:
821
+ excess = len(self._bot_message_ts) - self._BOT_TS_MAX // 2
822
+ for old_ts in list(self._bot_message_ts)[:excess]:
823
+ self._bot_message_ts.discard(old_ts)
824
+
825
+ return SendResult(
826
+ success=True,
827
+ message_id=sent_ts,
828
+ raw_response=last_result,
829
+ )
830
+
831
+ except Exception as e: # pragma: no cover - defensive logging
832
+ logger.error("[Slack] Send error: %s", e, exc_info=True)
833
+ return SendResult(success=False, error=str(e))
834
+
835
+ async def send_private_notice(
836
+ self,
837
+ chat_id: str,
838
+ user_id: str,
839
+ content: str,
840
+ reply_to: Optional[str] = None,
841
+ metadata: Optional[Dict[str, Any]] = None,
842
+ ) -> SendResult:
843
+ """Send a Slack ephemeral message visible only to one user."""
844
+ if not self._app:
845
+ return SendResult(success=False, error="Not connected")
846
+ if not chat_id or not user_id:
847
+ return SendResult(success=False, error="chat_id and user_id are required")
848
+
849
+ try:
850
+ formatted = self.format_message(content)
851
+ thread_ts = self._resolve_thread_ts(reply_to, metadata)
852
+ kwargs = {
853
+ "channel": chat_id,
854
+ "user": user_id,
855
+ "text": formatted,
856
+ "mrkdwn": True,
857
+ }
858
+ if thread_ts:
859
+ kwargs["thread_ts"] = thread_ts
860
+
861
+ result = await self._get_client(chat_id).chat_postEphemeral(**kwargs)
862
+ return SendResult(
863
+ success=True,
864
+ message_id=result.get("message_ts") or result.get("ts"),
865
+ raw_response=result,
866
+ )
867
+ except Exception as e: # pragma: no cover - defensive logging
868
+ logger.error("[Slack] Ephemeral send error: %s", e, exc_info=True)
869
+ return SendResult(success=False, error=str(e))
870
+
871
+ async def edit_message(
872
+ self,
873
+ chat_id: str,
874
+ message_id: str,
875
+ content: str,
876
+ *,
877
+ finalize: bool = False,
878
+ ) -> SendResult:
879
+ """Edit a previously sent Slack message."""
880
+ if not self._app:
881
+ return SendResult(success=False, error="Not connected")
882
+ try:
883
+ formatted = self.format_message(content)
884
+ await self._get_client(chat_id).chat_update(
885
+ channel=chat_id,
886
+ ts=message_id,
887
+ text=formatted,
888
+ )
889
+ if finalize:
890
+ await self.stop_typing(chat_id)
891
+ return SendResult(success=True, message_id=message_id)
892
+ except Exception as e: # pragma: no cover - defensive logging
893
+ logger.error(
894
+ "[Slack] Failed to edit message %s in channel %s: %s",
895
+ message_id,
896
+ chat_id,
897
+ e,
898
+ exc_info=True,
899
+ )
900
+ return SendResult(success=False, error=str(e))
901
+
902
+ async def send_typing(self, chat_id: str, metadata=None) -> None:
903
+ """Show a typing/status indicator using assistant.threads.setStatus.
904
+
905
+ Displays "is thinking..." next to the bot name in a thread.
906
+ Requires the assistant:write or chat:write scope.
907
+ Auto-clears when the bot sends a reply to the thread.
908
+ """
909
+ if not self._app:
910
+ return
911
+
912
+ thread_ts = None
913
+ if metadata:
914
+ thread_ts = metadata.get("thread_id") or metadata.get("thread_ts")
915
+
916
+ if not thread_ts:
917
+ return # Can only set status in a thread context
918
+
919
+ self._active_status_threads[chat_id] = thread_ts
920
+ try:
921
+ await self._get_client(chat_id).assistant_threads_setStatus(
922
+ channel_id=chat_id,
923
+ thread_ts=thread_ts,
924
+ status="is thinking...",
925
+ )
926
+ except Exception as e:
927
+ # Silently ignore — may lack assistant:write scope or not be
928
+ # in an assistant-enabled context. Falls back to reactions.
929
+ logger.debug("[Slack] assistant.threads.setStatus failed: %s", e)
930
+
931
+ async def stop_typing(self, chat_id: str, metadata=None) -> None:
932
+ """Clear the assistant thread status indicator."""
933
+ if not self._app:
934
+ return
935
+ thread_ts = self._active_status_threads.pop(chat_id, None)
936
+ if not thread_ts:
937
+ return
938
+ try:
939
+ await self._get_client(chat_id).assistant_threads_setStatus(
940
+ channel_id=chat_id,
941
+ thread_ts=thread_ts,
942
+ status="",
943
+ )
944
+ except Exception as e:
945
+ logger.debug("[Slack] assistant.threads.setStatus clear failed: %s", e)
946
+
947
+ def _dm_top_level_threads_as_sessions(self) -> bool:
948
+ """Whether top-level Slack DMs get per-message session threads.
949
+
950
+ Defaults to ``True`` so each visible DM reply thread is isolated as its
951
+ own Hermes session — matching the per-thread behavior channels already
952
+ have. Set ``platforms.slack.extra.dm_top_level_threads_as_sessions``
953
+ to ``false`` in config.yaml to revert to the legacy behavior where all
954
+ top-level DMs share one continuous session.
955
+ """
956
+ raw = self.config.extra.get("dm_top_level_threads_as_sessions")
957
+ if raw is None:
958
+ return True # default: each DM thread is its own session
959
+ return str(raw).strip().lower() in {"1", "true", "yes", "on"}
960
+
961
+ def _resolve_thread_ts(
962
+ self,
963
+ reply_to: Optional[str] = None,
964
+ metadata: Optional[Dict[str, Any]] = None,
965
+ ) -> Optional[str]:
966
+ """Resolve the correct thread_ts for a Slack API call.
967
+
968
+ Prefers metadata thread_id (the thread parent's ts, set by the
969
+ gateway) over reply_to (which may be a child message's ts).
970
+
971
+ When ``reply_in_thread`` is ``false`` in the platform extra config,
972
+ top-level channel messages receive direct channel replies instead of
973
+ thread replies. Messages that originate inside an existing thread are
974
+ always replied to in-thread to preserve conversation context.
975
+ """
976
+ # When reply_in_thread is disabled (default: True for backward compat),
977
+ # only thread messages that are already part of an existing thread.
978
+ # For top-level channel messages, the inbound handler sets
979
+ # metadata.thread_id to the message's own ts as a session-keying
980
+ # fallback (see the `thread_ts = event.get("thread_ts") or ts` branch),
981
+ # so metadata alone can't distinguish a real thread reply from a
982
+ # top-level message. reply_to is the incoming message's own id, so
983
+ # when thread_id == reply_to the "thread" is synthetic and we reply
984
+ # directly in the channel instead.
985
+ if not self.config.extra.get("reply_in_thread", True):
986
+ md = metadata or {}
987
+ existing_thread = md.get("thread_id") or md.get("thread_ts")
988
+ if existing_thread and reply_to and existing_thread == reply_to:
989
+ existing_thread = None
990
+ return existing_thread or None
991
+
992
+ if metadata:
993
+ if metadata.get("thread_id"):
994
+ return metadata["thread_id"]
995
+ if metadata.get("thread_ts"):
996
+ return metadata["thread_ts"]
997
+ return reply_to
998
+
999
+ async def _upload_file(
1000
+ self,
1001
+ chat_id: str,
1002
+ file_path: str,
1003
+ caption: Optional[str] = None,
1004
+ reply_to: Optional[str] = None,
1005
+ metadata: Optional[Dict[str, Any]] = None,
1006
+ ) -> SendResult:
1007
+ """Upload a local file to Slack."""
1008
+ if not self._app:
1009
+ return SendResult(success=False, error="Not connected")
1010
+
1011
+ if not os.path.exists(file_path):
1012
+ raise FileNotFoundError(f"File not found: {file_path}")
1013
+
1014
+ thread_ts = self._resolve_thread_ts(reply_to, metadata)
1015
+ last_exc = None
1016
+ for attempt in range(3):
1017
+ try:
1018
+ result = await self._get_client(chat_id).files_upload_v2(
1019
+ channel=chat_id,
1020
+ file=file_path,
1021
+ filename=os.path.basename(file_path),
1022
+ initial_comment=caption or "",
1023
+ thread_ts=thread_ts,
1024
+ )
1025
+ self._record_uploaded_file_thread(chat_id, thread_ts)
1026
+ return SendResult(success=True, raw_response=result)
1027
+ except Exception as exc:
1028
+ last_exc = exc
1029
+ if not self._is_retryable_upload_error(exc) or attempt >= 2:
1030
+ raise
1031
+ logger.debug(
1032
+ "[Slack] Upload retry %d/2 for %s: %s",
1033
+ attempt + 1,
1034
+ file_path,
1035
+ exc,
1036
+ )
1037
+ await asyncio.sleep(1.5 * (attempt + 1))
1038
+
1039
+ raise last_exc
1040
+
1041
+ async def send_multiple_images(
1042
+ self,
1043
+ chat_id: str,
1044
+ images: List[Tuple[str, str]],
1045
+ metadata: Optional[Dict[str, Any]] = None,
1046
+ human_delay: float = 0.0,
1047
+ ) -> None:
1048
+ """Send a batch of images as a single Slack message with multiple file uploads.
1049
+
1050
+ Uses ``files_upload_v2`` with its ``file_uploads`` parameter so all
1051
+ images show up attached to one ``initial_comment`` message instead
1052
+ of N separate messages. Falls back to the base per-image loop on
1053
+ any failure.
1054
+
1055
+ The batch limit is 10 file uploads per call (Slack server-side cap).
1056
+ """
1057
+ if not self._app:
1058
+ return
1059
+ if not images:
1060
+ return
1061
+
1062
+ try:
1063
+ import httpx as _httpx
1064
+ from urllib.parse import unquote as _unquote
1065
+ from tools.url_safety import is_safe_url as _is_safe_url
1066
+ except Exception:
1067
+ await super().send_multiple_images(chat_id, images, metadata, human_delay)
1068
+ return
1069
+
1070
+ thread_ts = self._resolve_thread_ts(None, metadata)
1071
+
1072
+ CHUNK = 10
1073
+ chunks = [images[i:i + CHUNK] for i in range(0, len(images), CHUNK)]
1074
+
1075
+ for chunk_idx, chunk in enumerate(chunks):
1076
+ if human_delay > 0 and chunk_idx > 0:
1077
+ await asyncio.sleep(human_delay)
1078
+
1079
+ file_uploads: List[Dict[str, Any]] = []
1080
+ initial_comment_parts: List[str] = []
1081
+ try:
1082
+ async with _httpx.AsyncClient(timeout=30.0, follow_redirects=True) as http_client:
1083
+ for image_url, alt_text in chunk:
1084
+ if alt_text:
1085
+ initial_comment_parts.append(alt_text)
1086
+
1087
+ if image_url.startswith("file://"):
1088
+ local_path = _unquote(image_url[7:])
1089
+ if not os.path.exists(local_path):
1090
+ logger.warning("[Slack] Skipping missing image: %s", local_path)
1091
+ continue
1092
+ file_uploads.append({
1093
+ "file": local_path,
1094
+ "filename": os.path.basename(local_path),
1095
+ })
1096
+ else:
1097
+ if not _is_safe_url(image_url):
1098
+ logger.warning("[Slack] Blocked unsafe image URL in batch")
1099
+ continue
1100
+ try:
1101
+ response = await http_client.get(image_url)
1102
+ response.raise_for_status()
1103
+ ext = "png"
1104
+ ct = response.headers.get("content-type", "")
1105
+ if "jpeg" in ct or "jpg" in ct:
1106
+ ext = "jpg"
1107
+ elif "gif" in ct:
1108
+ ext = "gif"
1109
+ elif "webp" in ct:
1110
+ ext = "webp"
1111
+ file_uploads.append({
1112
+ "content": response.content,
1113
+ "filename": f"image_{len(file_uploads)}.{ext}",
1114
+ })
1115
+ except Exception as dl_err:
1116
+ logger.warning(
1117
+ "[Slack] Download failed for %s: %s",
1118
+ safe_url_for_log(image_url), dl_err,
1119
+ )
1120
+ continue
1121
+
1122
+ if not file_uploads:
1123
+ continue
1124
+
1125
+ initial_comment = "\n".join(initial_comment_parts) if initial_comment_parts else ""
1126
+ logger.info(
1127
+ "[Slack] Sending %d image(s) in single files_upload_v2 (chunk %d/%d)",
1128
+ len(file_uploads), chunk_idx + 1, len(chunks),
1129
+ )
1130
+ result = await self._get_client(chat_id).files_upload_v2(
1131
+ channel=chat_id,
1132
+ file_uploads=file_uploads,
1133
+ initial_comment=initial_comment,
1134
+ thread_ts=thread_ts,
1135
+ )
1136
+ self._record_uploaded_file_thread(chat_id, thread_ts)
1137
+ _ = result
1138
+ except Exception as e:
1139
+ logger.warning(
1140
+ "[Slack] Multi-image files_upload_v2 failed (chunk %d/%d), falling back to per-image: %s",
1141
+ chunk_idx + 1, len(chunks), e,
1142
+ exc_info=True,
1143
+ )
1144
+ await super().send_multiple_images(chat_id, chunk, metadata, human_delay=human_delay)
1145
+
1146
+ def _record_uploaded_file_thread(self, chat_id: str, thread_ts: Optional[str]) -> None:
1147
+ """Treat successful file uploads as bot participation in a thread."""
1148
+ if not thread_ts:
1149
+ return
1150
+ self._bot_message_ts.add(thread_ts)
1151
+ if len(self._bot_message_ts) > self._BOT_TS_MAX:
1152
+ excess = len(self._bot_message_ts) - self._BOT_TS_MAX // 2
1153
+ for old_ts in list(self._bot_message_ts)[:excess]:
1154
+ self._bot_message_ts.discard(old_ts)
1155
+
1156
+ def _is_retryable_upload_error(self, exc: Exception) -> bool:
1157
+ """Best-effort detection for transient Slack upload failures."""
1158
+ status_code = getattr(getattr(exc, "response", None), "status_code", None)
1159
+ if status_code is not None:
1160
+ return status_code == 429 or status_code >= 500
1161
+
1162
+ body = " ".join(
1163
+ str(part) for part in (
1164
+ exc,
1165
+ getattr(exc, "message", ""),
1166
+ getattr(exc, "response", None),
1167
+ ) if part
1168
+ ).lower()
1169
+ if "rate_limited" in body or "ratelimited" in body or "429" in body:
1170
+ return True
1171
+ if "connection reset" in body or "service unavailable" in body or "temporarily unavailable" in body:
1172
+ return True
1173
+ return self._is_retryable_error(body)
1174
+
1175
+ # ----- Markdown → mrkdwn conversion -----
1176
+
1177
+ def format_message(self, content: str) -> str:
1178
+ """Convert standard markdown to Slack mrkdwn format.
1179
+
1180
+ Protected regions (code blocks, inline code) are extracted first so
1181
+ their contents are never modified. Standard markdown constructs
1182
+ (headers, bold, italic, links) are translated to mrkdwn syntax.
1183
+ """
1184
+ if not content:
1185
+ return content
1186
+
1187
+ placeholders: dict = {}
1188
+ counter = [0]
1189
+
1190
+ def _ph(value: str) -> str:
1191
+ """Stash value behind a placeholder that survives later passes."""
1192
+ key = f"\x00SL{counter[0]}\x00"
1193
+ counter[0] += 1
1194
+ placeholders[key] = value
1195
+ return key
1196
+
1197
+ text = content
1198
+
1199
+ # 1) Protect fenced code blocks (``` ... ```)
1200
+ text = re.sub(
1201
+ r'(```(?:[^\n]*\n)?[\s\S]*?```)',
1202
+ lambda m: _ph(m.group(0)),
1203
+ text,
1204
+ )
1205
+
1206
+ # 2) Protect inline code (`...`)
1207
+ text = re.sub(r'(`[^`]+`)', lambda m: _ph(m.group(0)), text)
1208
+
1209
+ # 3) Convert markdown links [text](url) → <url|text>
1210
+ def _convert_markdown_link(m):
1211
+ label = m.group(1)
1212
+ url = m.group(2).strip()
1213
+ if url.startswith('<') and url.endswith('>'):
1214
+ url = url[1:-1].strip()
1215
+ return _ph(f'<{url}|{label}>')
1216
+
1217
+ text = re.sub(
1218
+ r'(?<!!)\[([^\]]+)\]\(([^()]*(?:\([^()]*\)[^()]*)*)\)',
1219
+ _convert_markdown_link,
1220
+ text,
1221
+ )
1222
+
1223
+ # 4) Protect existing Slack entities/manual links so escaping and later
1224
+ # formatting passes don't break them.
1225
+ text = re.sub(
1226
+ r'(<(?:[@#!]|(?:https?|mailto|tel):)[^>\n]+>)',
1227
+ lambda m: _ph(m.group(1)),
1228
+ text,
1229
+ )
1230
+
1231
+ # 5) Protect blockquote markers before escaping
1232
+ text = re.sub(r'^(>+\s)', lambda m: _ph(m.group(0)), text, flags=re.MULTILINE)
1233
+
1234
+ # 6) Escape Slack control characters in remaining plain text.
1235
+ # Unescape first so already-escaped input doesn't get double-escaped.
1236
+ text = text.replace('&amp;', '&').replace('&lt;', '<').replace('&gt;', '>')
1237
+ text = text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
1238
+
1239
+ # 7) Convert headers (## Title) → *Title* (bold)
1240
+ def _convert_header(m):
1241
+ inner = m.group(1).strip()
1242
+ # Strip redundant bold markers inside a header
1243
+ inner = re.sub(r'\*\*(.+?)\*\*', r'\1', inner)
1244
+ return _ph(f'*{inner}*')
1245
+
1246
+ text = re.sub(
1247
+ r'^#{1,6}\s+(.+)$', _convert_header, text, flags=re.MULTILINE
1248
+ )
1249
+
1250
+ # 8) Convert bold+italic: ***text*** → *_text_* (Slack bold wrapping italic)
1251
+ text = re.sub(
1252
+ r'\*\*\*(.+?)\*\*\*',
1253
+ lambda m: _ph(f'*_{m.group(1)}_*'),
1254
+ text,
1255
+ )
1256
+
1257
+ # 9) Convert bold: **text** → *text* (Slack bold)
1258
+ text = re.sub(
1259
+ r'\*\*(.+?)\*\*',
1260
+ lambda m: _ph(f'*{m.group(1)}*'),
1261
+ text,
1262
+ )
1263
+
1264
+ # 10) Convert italic: _text_ stays as _text_ (already Slack italic)
1265
+ # Single *text* → _text_ (Slack italic), but only when the
1266
+ # emphasized text touches non-whitespace on both sides so literal
1267
+ # delimiters like "a * b * c" are preserved.
1268
+ text = re.sub(
1269
+ r'(?<!\*)\*(\S(?:[^*\n]*?\S)?)\*(?!\*)',
1270
+ lambda m: _ph(f'_{m.group(1)}_'),
1271
+ text,
1272
+ )
1273
+
1274
+ # 11) Convert strikethrough: ~~text~~ → ~text~
1275
+ text = re.sub(
1276
+ r'~~(.+?)~~',
1277
+ lambda m: _ph(f'~{m.group(1)}~'),
1278
+ text,
1279
+ )
1280
+
1281
+ # 12) Blockquotes: > prefix is already protected by step 5 above.
1282
+
1283
+ # 13) Restore placeholders in reverse order
1284
+ for key in reversed(placeholders):
1285
+ text = text.replace(key, placeholders[key])
1286
+
1287
+ return text
1288
+
1289
+ # ----- Reactions -----
1290
+
1291
+ async def _add_reaction(
1292
+ self, channel: str, timestamp: str, emoji: str
1293
+ ) -> bool:
1294
+ """Add an emoji reaction to a message. Returns True on success."""
1295
+ if not self._app:
1296
+ return False
1297
+ try:
1298
+ await self._get_client(channel).reactions_add(
1299
+ channel=channel, timestamp=timestamp, name=emoji
1300
+ )
1301
+ return True
1302
+ except Exception as e:
1303
+ # Don't log as error — may fail if already reacted or missing scope
1304
+ logger.debug("[Slack] reactions.add failed (%s): %s", emoji, e)
1305
+ return False
1306
+
1307
+ async def _remove_reaction(
1308
+ self, channel: str, timestamp: str, emoji: str
1309
+ ) -> bool:
1310
+ """Remove an emoji reaction from a message. Returns True on success."""
1311
+ if not self._app:
1312
+ return False
1313
+ try:
1314
+ await self._get_client(channel).reactions_remove(
1315
+ channel=channel, timestamp=timestamp, name=emoji
1316
+ )
1317
+ return True
1318
+ except Exception as e:
1319
+ logger.debug("[Slack] reactions.remove failed (%s): %s", emoji, e)
1320
+ return False
1321
+
1322
+ def _reactions_enabled(self) -> bool:
1323
+ """Check if message reactions are enabled via config/env."""
1324
+ return os.getenv("SLACK_REACTIONS", "true").lower() not in {"false", "0", "no"}
1325
+
1326
+ async def on_processing_start(self, event: MessageEvent) -> None:
1327
+ """Add an in-progress reaction when message processing begins."""
1328
+ if not self._reactions_enabled():
1329
+ return
1330
+ ts = getattr(event, "message_id", None)
1331
+ if not ts or ts not in self._reacting_message_ids:
1332
+ return
1333
+ channel_id = getattr(event.source, "chat_id", None)
1334
+ if channel_id:
1335
+ await self._add_reaction(channel_id, ts, "eyes")
1336
+
1337
+ async def on_processing_complete(self, event: MessageEvent, outcome: ProcessingOutcome) -> None:
1338
+ """Swap the in-progress reaction for a final success/failure reaction."""
1339
+ if not self._reactions_enabled():
1340
+ return
1341
+ ts = getattr(event, "message_id", None)
1342
+ if not ts or ts not in self._reacting_message_ids:
1343
+ return
1344
+ self._reacting_message_ids.discard(ts)
1345
+ channel_id = getattr(event.source, "chat_id", None)
1346
+ if not channel_id:
1347
+ return
1348
+ await self._remove_reaction(channel_id, ts, "eyes")
1349
+ if outcome == ProcessingOutcome.SUCCESS:
1350
+ await self._add_reaction(channel_id, ts, "white_check_mark")
1351
+ elif outcome == ProcessingOutcome.FAILURE:
1352
+ await self._add_reaction(channel_id, ts, "x")
1353
+
1354
+ # ----- User identity resolution -----
1355
+
1356
+ async def _resolve_user_name(self, user_id: str, chat_id: str = "") -> str:
1357
+ """Resolve a Slack user ID to a display name, with caching."""
1358
+ if not user_id:
1359
+ return ""
1360
+ if user_id in self._user_name_cache:
1361
+ return self._user_name_cache[user_id]
1362
+
1363
+ if not self._app:
1364
+ return user_id
1365
+
1366
+ try:
1367
+ client = self._get_client(chat_id) if chat_id else self._app.client
1368
+ result = await client.users_info(user=user_id)
1369
+ user = result.get("user", {})
1370
+ # Prefer display_name → real_name → user_id
1371
+ profile = user.get("profile", {})
1372
+ name = (
1373
+ profile.get("display_name")
1374
+ or profile.get("real_name")
1375
+ or user.get("real_name")
1376
+ or user.get("name")
1377
+ or user_id
1378
+ )
1379
+ self._user_name_cache[user_id] = name
1380
+ return name
1381
+ except Exception as e:
1382
+ logger.debug("[Slack] users.info failed for %s: %s", user_id, e)
1383
+ self._user_name_cache[user_id] = user_id
1384
+ return user_id
1385
+
1386
+ async def send_image_file(
1387
+ self,
1388
+ chat_id: str,
1389
+ image_path: str,
1390
+ caption: Optional[str] = None,
1391
+ reply_to: Optional[str] = None,
1392
+ metadata: Optional[Dict[str, Any]] = None,
1393
+ ) -> SendResult:
1394
+ """Send a local image file to Slack by uploading it."""
1395
+ try:
1396
+ return await self._upload_file(chat_id, image_path, caption, reply_to, metadata)
1397
+ except FileNotFoundError:
1398
+ return SendResult(success=False, error=f"Image file not found: {image_path}")
1399
+ except Exception as e: # pragma: no cover - defensive logging
1400
+ logger.error(
1401
+ "[%s] Failed to send local Slack image %s: %s",
1402
+ self.name,
1403
+ image_path,
1404
+ e,
1405
+ exc_info=True,
1406
+ )
1407
+ text = f"🖼️ Image: {image_path}"
1408
+ if caption:
1409
+ text = f"{caption}\n{text}"
1410
+ return await self.send(chat_id, text, reply_to=reply_to, metadata=metadata)
1411
+
1412
+ async def send_image(
1413
+ self,
1414
+ chat_id: str,
1415
+ image_url: str,
1416
+ caption: Optional[str] = None,
1417
+ reply_to: Optional[str] = None,
1418
+ metadata: Optional[Dict[str, Any]] = None,
1419
+ ) -> SendResult:
1420
+ """Send an image to Slack by uploading the URL as a file."""
1421
+ if not self._app:
1422
+ return SendResult(success=False, error="Not connected")
1423
+
1424
+ from tools.url_safety import is_safe_url
1425
+ if not is_safe_url(image_url):
1426
+ logger.warning("[Slack] Blocked unsafe image URL (SSRF protection)")
1427
+ return await super().send_image(chat_id, image_url, caption, reply_to, metadata=metadata)
1428
+
1429
+ try:
1430
+ import httpx
1431
+
1432
+ async def _ssrf_redirect_guard(response):
1433
+ """Re-check redirect targets so public URLs cannot bounce into private IPs."""
1434
+ if response.is_redirect and response.next_request:
1435
+ redirect_url = str(response.next_request.url)
1436
+ if not is_safe_url(redirect_url):
1437
+ raise ValueError("Blocked redirect to private/internal address")
1438
+
1439
+ # Download the image first
1440
+ async with httpx.AsyncClient(
1441
+ timeout=30.0,
1442
+ follow_redirects=True,
1443
+ event_hooks={"response": [_ssrf_redirect_guard]},
1444
+ ) as client:
1445
+ response = await client.get(image_url)
1446
+ response.raise_for_status()
1447
+
1448
+ thread_ts = self._resolve_thread_ts(reply_to, metadata)
1449
+ result = await self._get_client(chat_id).files_upload_v2(
1450
+ channel=chat_id,
1451
+ content=response.content,
1452
+ filename="image.png",
1453
+ initial_comment=caption or "",
1454
+ thread_ts=thread_ts,
1455
+ )
1456
+ self._record_uploaded_file_thread(chat_id, thread_ts)
1457
+
1458
+ return SendResult(success=True, raw_response=result)
1459
+
1460
+ except Exception as e: # pragma: no cover - defensive logging
1461
+ logger.warning(
1462
+ "[Slack] Failed to upload image from URL %s, falling back to text: %s",
1463
+ safe_url_for_log(image_url),
1464
+ e,
1465
+ exc_info=True,
1466
+ )
1467
+ # Fall back to sending the URL as text
1468
+ text = f"{caption}\n{image_url}" if caption else image_url
1469
+ return await self.send(
1470
+ chat_id=chat_id,
1471
+ content=text,
1472
+ reply_to=reply_to,
1473
+ metadata=metadata,
1474
+ )
1475
+
1476
+ async def send_voice(
1477
+ self,
1478
+ chat_id: str,
1479
+ audio_path: str,
1480
+ caption: Optional[str] = None,
1481
+ reply_to: Optional[str] = None,
1482
+ metadata: Optional[Dict[str, Any]] = None,
1483
+ **kwargs,
1484
+ ) -> SendResult:
1485
+ """Send an audio file to Slack."""
1486
+ try:
1487
+ return await self._upload_file(chat_id, audio_path, caption, reply_to, metadata)
1488
+ except FileNotFoundError:
1489
+ return SendResult(success=False, error=f"Audio file not found: {audio_path}")
1490
+ except Exception as e: # pragma: no cover - defensive logging
1491
+ logger.error(
1492
+ "[Slack] Failed to send audio file %s: %s",
1493
+ audio_path,
1494
+ e,
1495
+ exc_info=True,
1496
+ )
1497
+ return SendResult(success=False, error=str(e))
1498
+
1499
+ async def send_video(
1500
+ self,
1501
+ chat_id: str,
1502
+ video_path: str,
1503
+ caption: Optional[str] = None,
1504
+ reply_to: Optional[str] = None,
1505
+ metadata: Optional[Dict[str, Any]] = None,
1506
+ ) -> SendResult:
1507
+ """Send a video file to Slack."""
1508
+ if not self._app:
1509
+ return SendResult(success=False, error="Not connected")
1510
+
1511
+ if not os.path.exists(video_path):
1512
+ return SendResult(success=False, error=f"Video file not found: {video_path}")
1513
+
1514
+ try:
1515
+ thread_ts = self._resolve_thread_ts(reply_to, metadata)
1516
+ last_exc = None
1517
+ for attempt in range(3):
1518
+ try:
1519
+ result = await self._get_client(chat_id).files_upload_v2(
1520
+ channel=chat_id,
1521
+ file=video_path,
1522
+ filename=os.path.basename(video_path),
1523
+ initial_comment=caption or "",
1524
+ thread_ts=thread_ts,
1525
+ )
1526
+ self._record_uploaded_file_thread(chat_id, thread_ts)
1527
+ return SendResult(success=True, raw_response=result)
1528
+ except Exception as exc:
1529
+ last_exc = exc
1530
+ if not self._is_retryable_upload_error(exc) or attempt >= 2:
1531
+ raise
1532
+ logger.debug(
1533
+ "[Slack] Video upload retry %d/2 for %s: %s",
1534
+ attempt + 1,
1535
+ video_path,
1536
+ exc,
1537
+ )
1538
+ await asyncio.sleep(1.5 * (attempt + 1))
1539
+
1540
+ raise last_exc
1541
+
1542
+ except Exception as e: # pragma: no cover - defensive logging
1543
+ logger.error(
1544
+ "[%s] Failed to send video %s: %s",
1545
+ self.name,
1546
+ video_path,
1547
+ e,
1548
+ exc_info=True,
1549
+ )
1550
+ text = f"🎬 Video: {video_path}"
1551
+ if caption:
1552
+ text = f"{caption}\n{text}"
1553
+ return await self.send(chat_id, text, reply_to=reply_to, metadata=metadata)
1554
+
1555
+ async def send_document(
1556
+ self,
1557
+ chat_id: str,
1558
+ file_path: str,
1559
+ caption: Optional[str] = None,
1560
+ file_name: Optional[str] = None,
1561
+ reply_to: Optional[str] = None,
1562
+ metadata: Optional[Dict[str, Any]] = None,
1563
+ ) -> SendResult:
1564
+ """Send a document/file attachment to Slack."""
1565
+ if not self._app:
1566
+ return SendResult(success=False, error="Not connected")
1567
+
1568
+ if not os.path.exists(file_path):
1569
+ return SendResult(success=False, error=f"File not found: {file_path}")
1570
+
1571
+ display_name = file_name or os.path.basename(file_path)
1572
+ thread_ts = self._resolve_thread_ts(reply_to, metadata)
1573
+
1574
+ try:
1575
+ last_exc = None
1576
+ for attempt in range(3):
1577
+ try:
1578
+ result = await self._get_client(chat_id).files_upload_v2(
1579
+ channel=chat_id,
1580
+ file=file_path,
1581
+ filename=display_name,
1582
+ initial_comment=caption or "",
1583
+ thread_ts=thread_ts,
1584
+ )
1585
+ self._record_uploaded_file_thread(chat_id, thread_ts)
1586
+ return SendResult(success=True, raw_response=result)
1587
+ except Exception as exc:
1588
+ last_exc = exc
1589
+ if not self._is_retryable_upload_error(exc) or attempt >= 2:
1590
+ raise
1591
+ logger.debug(
1592
+ "[Slack] Document upload retry %d/2 for %s: %s",
1593
+ attempt + 1,
1594
+ file_path,
1595
+ exc,
1596
+ )
1597
+ await asyncio.sleep(1.5 * (attempt + 1))
1598
+
1599
+ raise last_exc
1600
+
1601
+ except Exception as e: # pragma: no cover - defensive logging
1602
+ logger.error(
1603
+ "[%s] Failed to send document %s: %s",
1604
+ self.name,
1605
+ file_path,
1606
+ e,
1607
+ exc_info=True,
1608
+ )
1609
+ text = f"📎 File: {file_path}"
1610
+ if caption:
1611
+ text = f"{caption}\n{text}"
1612
+ return await self.send(chat_id, text, reply_to=reply_to, metadata=metadata)
1613
+
1614
+ async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
1615
+ """Get information about a Slack channel."""
1616
+ if not self._app:
1617
+ return {"name": chat_id, "type": "unknown"}
1618
+
1619
+ try:
1620
+ result = await self._get_client(chat_id).conversations_info(channel=chat_id)
1621
+ channel = result.get("channel", {})
1622
+ is_dm = channel.get("is_im", False)
1623
+ return {
1624
+ "name": channel.get("name", chat_id),
1625
+ "type": "dm" if is_dm else "group",
1626
+ }
1627
+ except Exception as e: # pragma: no cover - defensive logging
1628
+ logger.error(
1629
+ "[Slack] Failed to fetch chat info for %s: %s",
1630
+ chat_id,
1631
+ e,
1632
+ exc_info=True,
1633
+ )
1634
+ return {"name": chat_id, "type": "unknown"}
1635
+
1636
+ # ----- Internal handlers -----
1637
+
1638
+ def _assistant_thread_key(self, channel_id: str, thread_ts: str) -> Optional[Tuple[str, str]]:
1639
+ """Return a stable cache key for Slack assistant thread metadata."""
1640
+ if not channel_id or not thread_ts:
1641
+ return None
1642
+ return (str(channel_id), str(thread_ts))
1643
+
1644
+ def _extract_assistant_thread_metadata(self, event: dict) -> Dict[str, str]:
1645
+ """Extract Slack Assistant thread identity data from an event payload."""
1646
+ assistant_thread = event.get("assistant_thread") or {}
1647
+ context = assistant_thread.get("context") or event.get("context") or {}
1648
+
1649
+ channel_id = (
1650
+ assistant_thread.get("channel_id")
1651
+ or event.get("channel")
1652
+ or context.get("channel_id")
1653
+ or ""
1654
+ )
1655
+ thread_ts = (
1656
+ assistant_thread.get("thread_ts")
1657
+ or event.get("thread_ts")
1658
+ or event.get("message_ts")
1659
+ or ""
1660
+ )
1661
+ user_id = (
1662
+ assistant_thread.get("user_id")
1663
+ or event.get("user")
1664
+ or context.get("user_id")
1665
+ or ""
1666
+ )
1667
+ team_id = (
1668
+ event.get("team")
1669
+ or event.get("team_id")
1670
+ or assistant_thread.get("team_id")
1671
+ or ""
1672
+ )
1673
+ context_channel_id = context.get("channel_id") or ""
1674
+
1675
+ return {
1676
+ "channel_id": str(channel_id) if channel_id else "",
1677
+ "thread_ts": str(thread_ts) if thread_ts else "",
1678
+ "user_id": str(user_id) if user_id else "",
1679
+ "team_id": str(team_id) if team_id else "",
1680
+ "context_channel_id": str(context_channel_id) if context_channel_id else "",
1681
+ }
1682
+
1683
+ def _cache_assistant_thread_metadata(self, metadata: Dict[str, str]) -> None:
1684
+ """Remember assistant thread identity data for later message events."""
1685
+ channel_id = metadata.get("channel_id", "")
1686
+ thread_ts = metadata.get("thread_ts", "")
1687
+ key = self._assistant_thread_key(channel_id, thread_ts)
1688
+ if not key:
1689
+ return
1690
+
1691
+ existing = self._assistant_threads.get(key, {})
1692
+ merged = dict(existing)
1693
+ merged.update({k: v for k, v in metadata.items() if v})
1694
+ self._assistant_threads[key] = merged
1695
+
1696
+ # Evict oldest entries when the cache exceeds the limit
1697
+ if len(self._assistant_threads) > self._ASSISTANT_THREADS_MAX:
1698
+ excess = len(self._assistant_threads) - self._ASSISTANT_THREADS_MAX // 2
1699
+ for old_key in list(self._assistant_threads)[:excess]:
1700
+ del self._assistant_threads[old_key]
1701
+
1702
+ team_id = merged.get("team_id", "")
1703
+ if team_id and channel_id:
1704
+ self._channel_team[channel_id] = team_id
1705
+
1706
+ def _lookup_assistant_thread_metadata(
1707
+ self,
1708
+ event: dict,
1709
+ channel_id: str = "",
1710
+ thread_ts: str = "",
1711
+ ) -> Dict[str, str]:
1712
+ """Load cached assistant-thread metadata that matches the current event."""
1713
+ metadata = self._extract_assistant_thread_metadata(event)
1714
+ if channel_id and not metadata.get("channel_id"):
1715
+ metadata["channel_id"] = channel_id
1716
+ if thread_ts and not metadata.get("thread_ts"):
1717
+ metadata["thread_ts"] = thread_ts
1718
+
1719
+ key = self._assistant_thread_key(
1720
+ metadata.get("channel_id", ""),
1721
+ metadata.get("thread_ts", ""),
1722
+ )
1723
+ cached = self._assistant_threads.get(key, {}) if key else {}
1724
+ if cached:
1725
+ merged = dict(cached)
1726
+ merged.update({k: v for k, v in metadata.items() if v})
1727
+ return merged
1728
+ return metadata
1729
+
1730
+ def _seed_assistant_thread_session(self, metadata: Dict[str, str]) -> None:
1731
+ """Prime the session store so assistant threads get stable user scoping."""
1732
+ session_store = getattr(self, "_session_store", None)
1733
+ if not session_store:
1734
+ return
1735
+
1736
+ channel_id = metadata.get("channel_id", "")
1737
+ thread_ts = metadata.get("thread_ts", "")
1738
+ user_id = metadata.get("user_id", "")
1739
+ if not channel_id or not thread_ts or not user_id:
1740
+ return
1741
+
1742
+ source = self.build_source(
1743
+ chat_id=channel_id,
1744
+ chat_name=channel_id,
1745
+ chat_type="dm",
1746
+ user_id=user_id,
1747
+ thread_id=thread_ts,
1748
+ chat_topic=metadata.get("context_channel_id") or None,
1749
+ )
1750
+
1751
+ try:
1752
+ session_store.get_or_create_session(source)
1753
+ except Exception:
1754
+ logger.debug(
1755
+ "[Slack] Failed to seed assistant thread session for %s/%s",
1756
+ channel_id,
1757
+ thread_ts,
1758
+ exc_info=True,
1759
+ )
1760
+
1761
+ async def _handle_assistant_thread_lifecycle_event(self, event: dict) -> None:
1762
+ """Handle Slack Assistant lifecycle events that carry user/thread identity."""
1763
+ metadata = self._extract_assistant_thread_metadata(event)
1764
+ self._cache_assistant_thread_metadata(metadata)
1765
+ self._seed_assistant_thread_session(metadata)
1766
+
1767
+ async def _handle_slack_message(self, event: dict) -> None:
1768
+ """Handle an incoming Slack message event."""
1769
+ # Dedup: Slack Socket Mode can redeliver events after reconnects (#4777)
1770
+ event_ts = event.get("ts", "")
1771
+ if event_ts and self._dedup.is_duplicate(event_ts):
1772
+ return
1773
+
1774
+ # Bot message filtering (SLACK_ALLOW_BOTS / config allow_bots):
1775
+ # "none" — ignore all bot messages (default, backward-compatible)
1776
+ # "mentions" — accept bot messages only when they @mention us
1777
+ # "all" — accept all bot messages (except our own)
1778
+ if event.get("bot_id") or event.get("subtype") == "bot_message":
1779
+ allow_bots = self.config.extra.get("allow_bots", "")
1780
+ if not allow_bots:
1781
+ allow_bots = os.getenv("SLACK_ALLOW_BOTS", "none")
1782
+ allow_bots = str(allow_bots).lower().strip()
1783
+ if allow_bots == "none":
1784
+ return
1785
+ elif allow_bots == "mentions":
1786
+ text_check = event.get("text", "")
1787
+ if self._bot_user_id and f"<@{self._bot_user_id}>" not in text_check:
1788
+ return
1789
+ # "all" falls through to process the message
1790
+ # Always ignore our own messages to prevent echo loops
1791
+ msg_user = event.get("user", "")
1792
+ if msg_user and self._bot_user_id and msg_user == self._bot_user_id:
1793
+ return
1794
+
1795
+ # Ignore message edits and deletions
1796
+ subtype = event.get("subtype")
1797
+ if subtype in {"message_changed", "message_deleted"}:
1798
+ return
1799
+
1800
+ original_text = event.get("text", "")
1801
+
1802
+ # Slack blocks native slash commands inside threads ("/queue is not
1803
+ # supported in threads. Sorry!"). As a workaround, recognise a
1804
+ # leading ``!`` as an alternate command prefix and rewrite it to
1805
+ # ``/`` so the rest of the pipeline (MessageType.COMMAND tagging,
1806
+ # gateway dispatcher) handles it like a normal slash command. Only
1807
+ # rewrite when the first token resolves to a known gateway command
1808
+ # so casual messages like "!nice work" pass through unchanged.
1809
+ if original_text.startswith("!"):
1810
+ try:
1811
+ from hermes_cli.commands import is_gateway_known_command
1812
+ first_token = original_text[1:].split(maxsplit=1)[0]
1813
+ # Strip "@suffix" the same way get_command() does, so
1814
+ # forms like ``!stop@hermes`` still resolve.
1815
+ cmd_name = first_token.split("@", 1)[0].lower()
1816
+ if cmd_name and "/" not in cmd_name and is_gateway_known_command(cmd_name):
1817
+ original_text = "/" + original_text[1:]
1818
+ except Exception: # pragma: no cover - defensive
1819
+ pass
1820
+
1821
+ text = original_text
1822
+
1823
+ # Extract quoted/forwarded content from Slack blocks.
1824
+ # Slack's modern composer embeds forwarded messages in the ``blocks``
1825
+ # array as ``rich_text_quote`` elements, which are NOT reflected in
1826
+ # the plain ``text`` field. Merge block text so the agent sees the
1827
+ # full message content.
1828
+ blocks = event.get("blocks")
1829
+ if blocks:
1830
+ blocks_text = _extract_text_from_slack_blocks(blocks)
1831
+ if blocks_text:
1832
+ # Only append if the blocks contain text not already present
1833
+ # in the plain text field (avoids duplication).
1834
+ stripped_blocks = blocks_text.strip()
1835
+ if stripped_blocks and stripped_blocks not in text.strip():
1836
+ logger.debug(
1837
+ "Slack: extracted additional text from blocks "
1838
+ "(likely quoted/forwarded content): %s",
1839
+ stripped_blocks[:300],
1840
+ )
1841
+ text = (text.strip() + "\n" + stripped_blocks).strip()
1842
+
1843
+ blocks_payload = _serialize_slack_blocks_for_agent(blocks)
1844
+ if blocks_payload:
1845
+ text = (text.strip() + "\n\n" + blocks_payload).strip()
1846
+
1847
+ # Extract link unfurls / rich attachments (e.g. Notion previews).
1848
+ # Slack places unfurled link previews in the ``attachments`` array with
1849
+ # fields like title, title_link/from_url, text, footer, and fallback.
1850
+ # Without reading these, the agent never sees shared link previews.
1851
+ slack_attachments = event.get("attachments") or []
1852
+ if slack_attachments:
1853
+ att_parts: list[str] = []
1854
+ for att in slack_attachments:
1855
+ att_title = att.get("title", "")
1856
+ att_url = att.get("title_link", "") or att.get("from_url", "")
1857
+ att_text = att.get("text", "")
1858
+ att_footer = att.get("footer", "")
1859
+ att_fallback = att.get("fallback", "")
1860
+
1861
+ # Skip message-type attachments (e.g. Slack bot messages with
1862
+ # is_msg_unfurl) to avoid echoing our own content.
1863
+ if att.get("is_msg_unfurl"):
1864
+ continue
1865
+
1866
+ # Build a readable representation.
1867
+ if att_title and att_url:
1868
+ header = f"📎 [{att_title}]({att_url})"
1869
+ elif att_title:
1870
+ header = f"📎 {att_title}"
1871
+ elif att_url:
1872
+ header = f"📎 {att_url}"
1873
+ else:
1874
+ header = None
1875
+
1876
+ # Prefer preview text, fall back to fallback description.
1877
+ body = att_text or att_fallback or ""
1878
+ if body:
1879
+ body = body.strip()
1880
+ if len(body) > 500:
1881
+ body = body[:497] + "..."
1882
+
1883
+ if header and body:
1884
+ section = f"{header}\n {body}"
1885
+ elif header:
1886
+ section = header
1887
+ elif body:
1888
+ section = f"📎 {body}"
1889
+ else:
1890
+ continue
1891
+
1892
+ # Deduplicate only when the fully rendered section is already
1893
+ # present. The shared URL often already appears in the user's
1894
+ # message text, and skipping on URL/title alone would hide the
1895
+ # preview body we actually want the agent to see.
1896
+ if section in text:
1897
+ continue
1898
+
1899
+ if att_footer:
1900
+ section = f"{section}\n _{att_footer}_"
1901
+
1902
+ att_parts.append(section)
1903
+
1904
+ if att_parts:
1905
+ attachment_text = "\n\n".join(att_parts)
1906
+ text = (text.strip() + "\n\n" + attachment_text).strip()
1907
+ logger.debug(
1908
+ "Slack: appended %d link unfurl(s) to message text",
1909
+ len(att_parts),
1910
+ )
1911
+
1912
+ channel_id = event.get("channel", "")
1913
+ ts = event.get("ts", "")
1914
+ assistant_meta = self._lookup_assistant_thread_metadata(
1915
+ event,
1916
+ channel_id=channel_id,
1917
+ thread_ts=event.get("thread_ts", ""),
1918
+ )
1919
+ user_id = event.get("user") or assistant_meta.get("user_id", "")
1920
+ if not channel_id:
1921
+ channel_id = assistant_meta.get("channel_id", "")
1922
+ team_id = (
1923
+ event.get("team")
1924
+ or event.get("team_id")
1925
+ or assistant_meta.get("team_id", "")
1926
+ )
1927
+
1928
+ # Track which workspace owns this channel
1929
+ if team_id and channel_id:
1930
+ self._channel_team[channel_id] = team_id
1931
+
1932
+ # Determine if this is a DM or channel message
1933
+ channel_type = event.get("channel_type", "")
1934
+ if not channel_type and channel_id.startswith("D"):
1935
+ channel_type = "im"
1936
+ is_dm = channel_type in {"im", "mpim"} # Both 1:1 and group DMs
1937
+
1938
+ # Build thread_ts for session keying.
1939
+ # In channels: fall back to ts so each top-level @mention starts a
1940
+ # new thread/session (the bot always replies in a thread).
1941
+ # In DMs: fall back to ts so each top-level DM reply thread gets
1942
+ # its own session key (matching channel behavior). Set
1943
+ # dm_top_level_threads_as_sessions: false in config to revert to
1944
+ # legacy single-session-per-DM-channel behavior.
1945
+ if is_dm:
1946
+ thread_ts = event.get("thread_ts") or assistant_meta.get("thread_ts")
1947
+ if not thread_ts and self._dm_top_level_threads_as_sessions():
1948
+ thread_ts = ts
1949
+ else:
1950
+ thread_ts = event.get("thread_ts") or ts # ts fallback for channels
1951
+
1952
+ # In channels, respond if:
1953
+ # 0. Channel is in free_response_channels, OR require_mention is
1954
+ # disabled — always process regardless of mention.
1955
+ # 1. The bot is @mentioned in this message, OR
1956
+ # 2. The message is a reply in a thread the bot started/participated in, OR
1957
+ # 3. The message is in a thread where the bot was previously @mentioned, OR
1958
+ # 4. There's an existing session for this thread (survives restarts)
1959
+ bot_uid = self._team_bot_user_ids.get(team_id, self._bot_user_id)
1960
+ routing_text = original_text or ""
1961
+ is_mentioned = bot_uid and f"<@{bot_uid}>" in routing_text
1962
+ event_thread_ts = event.get("thread_ts")
1963
+ is_thread_reply = bool(event_thread_ts and event_thread_ts != ts)
1964
+
1965
+ if not is_dm and bot_uid:
1966
+ # Check allowed channels — if set, only respond in these channels (whitelist)
1967
+ allowed_channels = self._slack_allowed_channels()
1968
+ if allowed_channels and channel_id not in allowed_channels:
1969
+ logger.debug("[Slack] Ignoring message in non-allowed channel: %s", channel_id)
1970
+ return
1971
+
1972
+ if channel_id in self._slack_free_response_channels():
1973
+ pass # Free-response channel — always process
1974
+ elif not self._slack_require_mention():
1975
+ pass # Mention requirement disabled globally for Slack
1976
+ elif self._slack_strict_mention() and not is_mentioned:
1977
+ return # Strict mode: ignore until @-mentioned again
1978
+ elif not is_mentioned:
1979
+ reply_to_bot_thread = (
1980
+ is_thread_reply and event_thread_ts in self._bot_message_ts
1981
+ )
1982
+ in_mentioned_thread = (
1983
+ event_thread_ts is not None
1984
+ and event_thread_ts in self._mentioned_threads
1985
+ )
1986
+ has_session = (
1987
+ is_thread_reply
1988
+ and self._has_active_session_for_thread(
1989
+ channel_id=channel_id,
1990
+ thread_ts=event_thread_ts,
1991
+ user_id=user_id,
1992
+ )
1993
+ )
1994
+ if not reply_to_bot_thread and not in_mentioned_thread and not has_session:
1995
+ return
1996
+
1997
+ if is_mentioned:
1998
+ # Strip the bot mention from the text
1999
+ text = text.replace(f"<@{bot_uid}>", "").strip()
2000
+ # Register this thread so all future messages auto-trigger the bot.
2001
+ # Skipped in strict mode: strict_mention=true bots must be
2002
+ # re-mentioned every turn, so remembering the thread would
2003
+ # defeat the feature (and re-enable agent-to-agent ack loops).
2004
+ if event_thread_ts and not self._slack_strict_mention():
2005
+ self._mentioned_threads.add(event_thread_ts)
2006
+ if len(self._mentioned_threads) > self._MENTIONED_THREADS_MAX:
2007
+ to_remove = list(self._mentioned_threads)[:self._MENTIONED_THREADS_MAX // 2]
2008
+ for t in to_remove:
2009
+ self._mentioned_threads.discard(t)
2010
+
2011
+ # When entering a thread for the first time (no existing session),
2012
+ # fetch thread context so the agent understands the conversation.
2013
+ if is_thread_reply and not self._has_active_session_for_thread(
2014
+ channel_id=channel_id,
2015
+ thread_ts=event_thread_ts,
2016
+ user_id=user_id,
2017
+ ):
2018
+ thread_context = await self._fetch_thread_context(
2019
+ channel_id=channel_id,
2020
+ thread_ts=event_thread_ts,
2021
+ current_ts=ts,
2022
+ team_id=team_id,
2023
+ )
2024
+ if thread_context:
2025
+ text = thread_context + text
2026
+
2027
+ # Determine message type
2028
+ msg_type = MessageType.TEXT
2029
+ if (original_text or "").startswith("/"):
2030
+ msg_type = MessageType.COMMAND
2031
+
2032
+ # Handle file attachments
2033
+ media_urls = []
2034
+ media_types = []
2035
+ attachment_notices: List[str] = []
2036
+ files = event.get("files", [])
2037
+ for f in files:
2038
+ # Slack Connect channels return stub file objects with
2039
+ # file_access="check_file_info" and no URL fields. We must
2040
+ # call files.info to retrieve the full object (including url_private_download)
2041
+ # before we can download it.
2042
+ # https://docs.slack.dev/reference/objects/file-object/#slack_connect_files
2043
+ if f.get("file_access") == "check_file_info":
2044
+ file_id = f.get("id")
2045
+ if not file_id:
2046
+ continue
2047
+ try:
2048
+ info_resp = await self._get_client(channel_id).files_info(file=file_id)
2049
+ if info_resp.get("ok"):
2050
+ f = info_resp["file"]
2051
+ else:
2052
+ detail = self._describe_slack_api_error(info_resp, file_obj=f)
2053
+ if detail:
2054
+ attachment_notices.append(detail)
2055
+ logger.warning("[Slack] %s", detail)
2056
+ else:
2057
+ logger.warning(
2058
+ "[Slack] files.info failed for %s: %s",
2059
+ file_id, info_resp.get("error"),
2060
+ )
2061
+ continue
2062
+ except Exception as e:
2063
+ response = getattr(e, "response", None)
2064
+ detail = self._describe_slack_api_error(response, file_obj=f)
2065
+ if detail:
2066
+ attachment_notices.append(detail)
2067
+ logger.warning("[Slack] %s", detail)
2068
+ else:
2069
+ logger.warning("[Slack] files.info error for %s: %s", file_id, e, exc_info=True)
2070
+ continue
2071
+
2072
+ mimetype = f.get("mimetype", "unknown")
2073
+ url = f.get("url_private_download") or f.get("url_private", "")
2074
+ if mimetype.startswith("image/") and url:
2075
+ try:
2076
+ ext = "." + mimetype.split("/")[-1].split(";")[0]
2077
+ if ext not in {".jpg", ".jpeg", ".png", ".gif", ".webp"}:
2078
+ ext = ".jpg"
2079
+ # Slack private URLs require the bot token as auth header
2080
+ cached = await self._download_slack_file(url, ext, team_id=team_id)
2081
+ media_urls.append(cached)
2082
+ media_types.append(mimetype)
2083
+ except Exception as e: # pragma: no cover - defensive logging
2084
+ detail = self._describe_slack_download_failure(e, file_obj=f)
2085
+ if detail:
2086
+ attachment_notices.append(detail)
2087
+ logger.warning("[Slack] %s", detail)
2088
+ else:
2089
+ logger.warning("[Slack] Failed to cache image from %s: %s", url, e, exc_info=True)
2090
+ elif mimetype.startswith("audio/") and url:
2091
+ try:
2092
+ ext = "." + mimetype.split("/")[-1].split(";")[0]
2093
+ if ext not in {".ogg", ".mp3", ".wav", ".webm", ".m4a"}:
2094
+ ext = ".ogg"
2095
+ cached = await self._download_slack_file(url, ext, audio=True, team_id=team_id)
2096
+ media_urls.append(cached)
2097
+ media_types.append(mimetype)
2098
+ except Exception as e: # pragma: no cover - defensive logging
2099
+ detail = self._describe_slack_download_failure(e, file_obj=f)
2100
+ if detail:
2101
+ attachment_notices.append(detail)
2102
+ logger.warning("[Slack] %s", detail)
2103
+ else:
2104
+ logger.warning("[Slack] Failed to cache audio from %s: %s", url, e, exc_info=True)
2105
+ elif url:
2106
+ # Try to handle as a document attachment
2107
+ try:
2108
+ original_filename = f.get("name", "")
2109
+ ext = ""
2110
+ if original_filename:
2111
+ _, ext = os.path.splitext(original_filename)
2112
+ ext = ext.lower()
2113
+
2114
+ # Fallback: reverse-lookup from MIME type
2115
+ if not ext and mimetype:
2116
+ mime_to_ext = {v: k for k, v in SUPPORTED_DOCUMENT_TYPES.items()}
2117
+ ext = mime_to_ext.get(mimetype, "")
2118
+
2119
+ if ext not in SUPPORTED_DOCUMENT_TYPES:
2120
+ continue # Skip unsupported file types silently
2121
+
2122
+ # Check file size (Slack limit: 20 MB for bots)
2123
+ file_size = f.get("size", 0)
2124
+ MAX_DOC_BYTES = 20 * 1024 * 1024
2125
+ if not file_size or file_size > MAX_DOC_BYTES:
2126
+ logger.warning("[Slack] Document too large or unknown size: %s", file_size)
2127
+ continue
2128
+
2129
+ # Download and cache
2130
+ raw_bytes = await self._download_slack_file_bytes(url, team_id=team_id)
2131
+ cached_path = cache_document_from_bytes(
2132
+ raw_bytes, original_filename or f"document{ext}"
2133
+ )
2134
+ doc_mime = SUPPORTED_DOCUMENT_TYPES[ext]
2135
+ media_urls.append(cached_path)
2136
+ media_types.append(doc_mime)
2137
+ logger.debug("[Slack] Cached user document: %s", cached_path)
2138
+
2139
+ # Inject small text-ish files directly into the prompt so
2140
+ # snippets like JSON/YAML/configs are actually visible to the agent.
2141
+ MAX_TEXT_INJECT_BYTES = 100 * 1024
2142
+ TEXT_INJECT_EXTENSIONS = {
2143
+ ".md", ".txt", ".csv", ".log", ".json", ".xml",
2144
+ ".yaml", ".yml", ".toml", ".ini", ".cfg",
2145
+ }
2146
+ if ext in TEXT_INJECT_EXTENSIONS and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES:
2147
+ try:
2148
+ text_content = raw_bytes.decode("utf-8")
2149
+ display_name = original_filename or f"document{ext}"
2150
+ display_name = re.sub(r'[^\w.\- ]', '_', display_name)
2151
+ injection = f"[Content of {display_name}]:\n{text_content}"
2152
+ if text:
2153
+ text = f"{injection}\n\n{text}"
2154
+ else:
2155
+ text = injection
2156
+ except UnicodeDecodeError:
2157
+ pass # Binary content, skip injection
2158
+
2159
+ except Exception as e: # pragma: no cover - defensive logging
2160
+ detail = self._describe_slack_download_failure(e, file_obj=f)
2161
+ if detail:
2162
+ attachment_notices.append(detail)
2163
+ logger.warning("[Slack] %s", detail)
2164
+ else:
2165
+ logger.warning("[Slack] Failed to cache document from %s: %s", url, e, exc_info=True)
2166
+
2167
+ if attachment_notices:
2168
+ notice_block = "[Slack attachment notice]\n" + "\n".join(f"- {n}" for n in attachment_notices)
2169
+ text = f"{notice_block}\n\n{text}" if text else notice_block
2170
+
2171
+ if msg_type != MessageType.COMMAND and media_types:
2172
+ if any(m.startswith("image/") for m in media_types):
2173
+ msg_type = MessageType.PHOTO
2174
+ elif any(m.startswith("audio/") for m in media_types):
2175
+ msg_type = MessageType.VOICE
2176
+ else:
2177
+ msg_type = MessageType.DOCUMENT
2178
+
2179
+ # Resolve user display name (cached after first lookup)
2180
+ user_name = await self._resolve_user_name(user_id, chat_id=channel_id)
2181
+
2182
+ # Build source
2183
+ source = self.build_source(
2184
+ chat_id=channel_id,
2185
+ chat_name=channel_id, # Will be resolved later if needed
2186
+ chat_type="dm" if is_dm else "group",
2187
+ user_id=user_id,
2188
+ user_name=user_name,
2189
+ thread_id=thread_ts,
2190
+ )
2191
+
2192
+ # Per-channel ephemeral prompt
2193
+ from gateway.platforms.base import resolve_channel_prompt, resolve_channel_skills
2194
+ _channel_prompt = resolve_channel_prompt(
2195
+ self.config.extra, channel_id, None,
2196
+ )
2197
+ _auto_skill = resolve_channel_skills(
2198
+ self.config.extra, channel_id, None,
2199
+ )
2200
+
2201
+ # Extract reply context if this message is a thread reply.
2202
+ # Mirrors the Telegram/Discord implementations so that gateway.run
2203
+ # can inject a `[Replying to: "..."]` prefix when the parent is not
2204
+ # already in the session history. Uses the thread-context cache when
2205
+ # available to avoid redundant conversations.replies calls.
2206
+ reply_to_text = None
2207
+ if thread_ts and thread_ts != ts:
2208
+ try:
2209
+ reply_to_text = await self._fetch_thread_parent_text(
2210
+ channel_id=channel_id,
2211
+ thread_ts=thread_ts,
2212
+ team_id=team_id,
2213
+ ) or None
2214
+ except Exception: # pragma: no cover - defensive
2215
+ reply_to_text = None
2216
+
2217
+ msg_event = MessageEvent(
2218
+ text=text,
2219
+ message_type=msg_type,
2220
+ source=source,
2221
+ raw_message=event,
2222
+ message_id=ts,
2223
+ media_urls=media_urls,
2224
+ media_types=media_types,
2225
+ reply_to_message_id=thread_ts if thread_ts != ts else None,
2226
+ channel_prompt=_channel_prompt,
2227
+ reply_to_text=reply_to_text,
2228
+ auto_skill=_auto_skill,
2229
+ )
2230
+
2231
+ # Only react when bot is directly addressed (DM or @mention).
2232
+ # In listen-all channels (require_mention=false), reacting to every
2233
+ # casual message would be noisy.
2234
+ _should_react = (is_dm or is_mentioned) and self._reactions_enabled()
2235
+ if _should_react:
2236
+ self._reacting_message_ids.add(ts)
2237
+
2238
+ await self.handle_message(msg_event)
2239
+
2240
+ # ----- Approval button support (Block Kit) -----
2241
+
2242
+ async def send_exec_approval(
2243
+ self, chat_id: str, command: str, session_key: str,
2244
+ description: str = "dangerous command",
2245
+ metadata: Optional[Dict[str, Any]] = None,
2246
+ ) -> SendResult:
2247
+ """Send a Block Kit approval prompt with interactive buttons.
2248
+
2249
+ The buttons call ``resolve_gateway_approval()`` to unblock the waiting
2250
+ agent thread — same mechanism as the text ``/approve`` flow.
2251
+ """
2252
+ if not self._app:
2253
+ return SendResult(success=False, error="Not connected")
2254
+
2255
+ try:
2256
+ cmd_preview = command[:2900] + "..." if len(command) > 2900 else command
2257
+ thread_ts = self._resolve_thread_ts(None, metadata)
2258
+
2259
+ blocks = [
2260
+ {
2261
+ "type": "section",
2262
+ "text": {
2263
+ "type": "mrkdwn",
2264
+ "text": (
2265
+ f":warning: *Command Approval Required*\n"
2266
+ f"```{cmd_preview}```\n"
2267
+ f"Reason: {description}"
2268
+ ),
2269
+ },
2270
+ },
2271
+ {
2272
+ "type": "actions",
2273
+ "elements": [
2274
+ {
2275
+ "type": "button",
2276
+ "text": {"type": "plain_text", "text": "Allow Once"},
2277
+ "style": "primary",
2278
+ "action_id": "hermes_approve_once",
2279
+ "value": session_key,
2280
+ },
2281
+ {
2282
+ "type": "button",
2283
+ "text": {"type": "plain_text", "text": "Allow Session"},
2284
+ "action_id": "hermes_approve_session",
2285
+ "value": session_key,
2286
+ },
2287
+ {
2288
+ "type": "button",
2289
+ "text": {"type": "plain_text", "text": "Always Allow"},
2290
+ "action_id": "hermes_approve_always",
2291
+ "value": session_key,
2292
+ },
2293
+ {
2294
+ "type": "button",
2295
+ "text": {"type": "plain_text", "text": "Deny"},
2296
+ "style": "danger",
2297
+ "action_id": "hermes_deny",
2298
+ "value": session_key,
2299
+ },
2300
+ ],
2301
+ },
2302
+ ]
2303
+
2304
+ kwargs: Dict[str, Any] = {
2305
+ "channel": chat_id,
2306
+ "text": f"⚠️ Command approval required: {cmd_preview[:100]}",
2307
+ "blocks": blocks,
2308
+ }
2309
+ if thread_ts:
2310
+ kwargs["thread_ts"] = thread_ts
2311
+
2312
+ result = await self._get_client(chat_id).chat_postMessage(**kwargs)
2313
+ msg_ts = result.get("ts", "")
2314
+ if msg_ts:
2315
+ self._approval_resolved[msg_ts] = False
2316
+
2317
+ return SendResult(success=True, message_id=msg_ts, raw_response=result)
2318
+ except Exception as e:
2319
+ logger.error("[Slack] send_exec_approval failed: %s", e, exc_info=True)
2320
+ return SendResult(success=False, error=str(e))
2321
+
2322
+ async def send_slash_confirm(
2323
+ self, chat_id: str, title: str, message: str, session_key: str,
2324
+ confirm_id: str, metadata: Optional[Dict[str, Any]] = None,
2325
+ ) -> SendResult:
2326
+ """Send a Block Kit three-option slash-command confirmation prompt."""
2327
+ if not self._app:
2328
+ return SendResult(success=False, error="Not connected")
2329
+
2330
+ try:
2331
+ body = message[:2900] + "..." if len(message) > 2900 else message
2332
+ thread_ts = self._resolve_thread_ts(None, metadata)
2333
+ # Encode session_key and confirm_id into the button value so the
2334
+ # callback handler can resolve without extra bookkeeping.
2335
+ value = f"{session_key}|{confirm_id}"
2336
+
2337
+ blocks = [
2338
+ {
2339
+ "type": "section",
2340
+ "text": {
2341
+ "type": "mrkdwn",
2342
+ "text": f"*{title or 'Confirm'}*\n\n{body}",
2343
+ },
2344
+ },
2345
+ {
2346
+ "type": "actions",
2347
+ "elements": [
2348
+ {
2349
+ "type": "button",
2350
+ "text": {"type": "plain_text", "text": "Approve Once"},
2351
+ "style": "primary",
2352
+ "action_id": "hermes_confirm_once",
2353
+ "value": value,
2354
+ },
2355
+ {
2356
+ "type": "button",
2357
+ "text": {"type": "plain_text", "text": "Always Approve"},
2358
+ "action_id": "hermes_confirm_always",
2359
+ "value": value,
2360
+ },
2361
+ {
2362
+ "type": "button",
2363
+ "text": {"type": "plain_text", "text": "Cancel"},
2364
+ "style": "danger",
2365
+ "action_id": "hermes_confirm_cancel",
2366
+ "value": value,
2367
+ },
2368
+ ],
2369
+ },
2370
+ ]
2371
+
2372
+ kwargs: Dict[str, Any] = {
2373
+ "channel": chat_id,
2374
+ "text": f"{title or 'Confirm'}: {body[:100]}",
2375
+ "blocks": blocks,
2376
+ }
2377
+ if thread_ts:
2378
+ kwargs["thread_ts"] = thread_ts
2379
+
2380
+ result = await self._get_client(chat_id).chat_postMessage(**kwargs)
2381
+ return SendResult(success=True, message_id=result.get("ts", ""), raw_response=result)
2382
+ except Exception as e:
2383
+ logger.error("[Slack] send_slash_confirm failed: %s", e, exc_info=True)
2384
+ return SendResult(success=False, error=str(e))
2385
+
2386
+ async def _handle_slash_confirm_action(self, ack, body, action) -> None:
2387
+ """Handle a slash-confirm button click from Block Kit."""
2388
+ await ack()
2389
+
2390
+ action_id = action.get("action_id", "")
2391
+ value = action.get("value", "")
2392
+ message = body.get("message", {})
2393
+ msg_ts = message.get("ts", "")
2394
+ channel_id = body.get("channel", {}).get("id", "")
2395
+ user_name = body.get("user", {}).get("name", "unknown")
2396
+ user_id = body.get("user", {}).get("id", "")
2397
+
2398
+ # Authorization — reuse the exec-approval allowlist.
2399
+ allowed_csv = os.getenv("SLACK_ALLOWED_USERS", "").strip()
2400
+ if allowed_csv:
2401
+ allowed_ids = {uid.strip() for uid in allowed_csv.split(",") if uid.strip()}
2402
+ if "*" not in allowed_ids and user_id not in allowed_ids:
2403
+ logger.warning(
2404
+ "[Slack] Unauthorized slash-confirm click by %s (%s) — ignoring",
2405
+ user_name, user_id,
2406
+ )
2407
+ return
2408
+
2409
+ # Parse session_key|confirm_id back out
2410
+ if "|" not in value:
2411
+ logger.warning("[Slack] Malformed slash-confirm value: %s", value)
2412
+ return
2413
+ session_key, confirm_id = value.split("|", 1)
2414
+
2415
+ choice_map = {
2416
+ "hermes_confirm_once": "once",
2417
+ "hermes_confirm_always": "always",
2418
+ "hermes_confirm_cancel": "cancel",
2419
+ }
2420
+ choice = choice_map.get(action_id, "cancel")
2421
+
2422
+ label_map = {
2423
+ "once": f"✅ Approved once by {user_name}",
2424
+ "always": f"🔒 Always approved by {user_name}",
2425
+ "cancel": f"❌ Cancelled by {user_name}",
2426
+ }
2427
+ decision_text = label_map.get(choice, f"Resolved by {user_name}")
2428
+
2429
+ # Pull original prompt body out of the section block so we can show
2430
+ # the decision inline without losing context.
2431
+ original_text = ""
2432
+ for block in message.get("blocks", []):
2433
+ if block.get("type") == "section":
2434
+ original_text = block.get("text", {}).get("text", "")
2435
+ break
2436
+
2437
+ updated_blocks = [
2438
+ {
2439
+ "type": "section",
2440
+ "text": {
2441
+ "type": "mrkdwn",
2442
+ "text": original_text or "Confirmation prompt",
2443
+ },
2444
+ },
2445
+ {
2446
+ "type": "context",
2447
+ "elements": [
2448
+ {"type": "mrkdwn", "text": decision_text},
2449
+ ],
2450
+ },
2451
+ ]
2452
+
2453
+ try:
2454
+ await self._get_client(channel_id).chat_update(
2455
+ channel=channel_id,
2456
+ ts=msg_ts,
2457
+ text=decision_text,
2458
+ blocks=updated_blocks,
2459
+ )
2460
+ except Exception as e:
2461
+ logger.warning("[Slack] Failed to update slash-confirm message: %s", e)
2462
+
2463
+ # Resolve via the module-level primitive and post any follow-up.
2464
+ try:
2465
+ from tools import slash_confirm as _slash_confirm_mod
2466
+ result_text = await _slash_confirm_mod.resolve(session_key, confirm_id, choice)
2467
+ if result_text:
2468
+ post_kwargs: Dict[str, Any] = {
2469
+ "channel": channel_id,
2470
+ "text": result_text,
2471
+ }
2472
+ # Inherit the thread so the reply stays in the same place.
2473
+ thread_ts = message.get("thread_ts") or msg_ts
2474
+ if thread_ts:
2475
+ post_kwargs["thread_ts"] = thread_ts
2476
+ await self._get_client(channel_id).chat_postMessage(**post_kwargs)
2477
+ logger.info(
2478
+ "Slack button resolved slash-confirm for session %s (choice=%s, user=%s)",
2479
+ session_key, choice, user_name,
2480
+ )
2481
+ except Exception as exc:
2482
+ logger.error("Failed to resolve slash-confirm from Slack button: %s", exc, exc_info=True)
2483
+
2484
+ async def _handle_approval_action(self, ack, body, action) -> None:
2485
+ """Handle an approval button click from Block Kit."""
2486
+ await ack()
2487
+
2488
+ action_id = action.get("action_id", "")
2489
+ session_key = action.get("value", "")
2490
+ message = body.get("message", {})
2491
+ msg_ts = message.get("ts", "")
2492
+ channel_id = body.get("channel", {}).get("id", "")
2493
+ user_name = body.get("user", {}).get("name", "unknown")
2494
+ user_id = body.get("user", {}).get("id", "")
2495
+
2496
+ # Only authorized users may click approval buttons. Button clicks
2497
+ # bypass the normal message auth flow in gateway/run.py, so we must
2498
+ # check here as well.
2499
+ allowed_csv = os.getenv("SLACK_ALLOWED_USERS", "").strip()
2500
+ if allowed_csv:
2501
+ allowed_ids = {uid.strip() for uid in allowed_csv.split(",") if uid.strip()}
2502
+ if "*" not in allowed_ids and user_id not in allowed_ids:
2503
+ logger.warning(
2504
+ "[Slack] Unauthorized approval click by %s (%s) — ignoring",
2505
+ user_name, user_id,
2506
+ )
2507
+ return
2508
+
2509
+ # Map action_id to approval choice
2510
+ choice_map = {
2511
+ "hermes_approve_once": "once",
2512
+ "hermes_approve_session": "session",
2513
+ "hermes_approve_always": "always",
2514
+ "hermes_deny": "deny",
2515
+ }
2516
+ choice = choice_map.get(action_id, "deny")
2517
+
2518
+ # Prevent double-clicks — atomic pop; first caller gets False, others get True (default)
2519
+ if self._approval_resolved.pop(msg_ts, True):
2520
+ return
2521
+
2522
+ # Update the message to show the decision and remove buttons
2523
+ label_map = {
2524
+ "once": f"✅ Approved once by {user_name}",
2525
+ "session": f"✅ Approved for session by {user_name}",
2526
+ "always": f"✅ Approved permanently by {user_name}",
2527
+ "deny": f"❌ Denied by {user_name}",
2528
+ }
2529
+ decision_text = label_map.get(choice, f"Resolved by {user_name}")
2530
+
2531
+ # Get original text from the section block
2532
+ original_text = ""
2533
+ for block in message.get("blocks", []):
2534
+ if block.get("type") == "section":
2535
+ original_text = block.get("text", {}).get("text", "")
2536
+ break
2537
+
2538
+ updated_blocks = [
2539
+ {
2540
+ "type": "section",
2541
+ "text": {
2542
+ "type": "mrkdwn",
2543
+ "text": original_text or "Command approval request",
2544
+ },
2545
+ },
2546
+ {
2547
+ "type": "context",
2548
+ "elements": [
2549
+ {"type": "mrkdwn", "text": decision_text},
2550
+ ],
2551
+ },
2552
+ ]
2553
+
2554
+ try:
2555
+ await self._get_client(channel_id).chat_update(
2556
+ channel=channel_id,
2557
+ ts=msg_ts,
2558
+ text=decision_text,
2559
+ blocks=updated_blocks,
2560
+ )
2561
+ except Exception as e:
2562
+ logger.warning("[Slack] Failed to update approval message: %s", e)
2563
+
2564
+ # Resolve the approval — this unblocks the agent thread
2565
+ try:
2566
+ from tools.approval import resolve_gateway_approval
2567
+ count = resolve_gateway_approval(session_key, choice)
2568
+ logger.info(
2569
+ "Slack button resolved %d approval(s) for session %s (choice=%s, user=%s)",
2570
+ count, session_key, choice, user_name,
2571
+ )
2572
+ except Exception as exc:
2573
+ logger.error("Failed to resolve gateway approval from Slack button: %s", exc)
2574
+
2575
+ # (approval state already consumed by atomic pop above)
2576
+
2577
+ # ----- Thread context fetching -----
2578
+
2579
+ async def _fetch_thread_context(
2580
+ self, channel_id: str, thread_ts: str, current_ts: str,
2581
+ team_id: str = "", limit: int = 30,
2582
+ ) -> str:
2583
+ """Fetch recent thread messages to provide context when the bot is
2584
+ mentioned mid-thread for the first time.
2585
+
2586
+ This method is only called when there is NO active session for the
2587
+ thread (guarded at the call site by _has_active_session_for_thread).
2588
+ That guard ensures thread messages are prepended only on the very
2589
+ first turn — after that the session history already holds them, so
2590
+ there is no duplication across subsequent turns.
2591
+
2592
+ Results are cached for _THREAD_CACHE_TTL seconds per thread to avoid
2593
+ hammering conversations.replies (Tier 3, ~50 req/min).
2594
+
2595
+ Returns a formatted string with prior thread history, or empty string
2596
+ on failure or if the thread has no prior messages.
2597
+ """
2598
+ cache_key = f"{channel_id}:{thread_ts}:{team_id}"
2599
+ now = time.monotonic()
2600
+ cached = self._thread_context_cache.get(cache_key)
2601
+ if cached and (now - cached.fetched_at) < self._THREAD_CACHE_TTL:
2602
+ return cached.content
2603
+
2604
+ try:
2605
+ client = self._get_client(channel_id)
2606
+
2607
+ # Retry with exponential backoff for Tier-3 rate limits (429).
2608
+ result = None
2609
+ for attempt in range(3):
2610
+ try:
2611
+ result = await client.conversations_replies(
2612
+ channel=channel_id,
2613
+ ts=thread_ts,
2614
+ limit=limit + 1, # +1 because it includes the current message
2615
+ inclusive=True,
2616
+ )
2617
+ break
2618
+ except Exception as exc:
2619
+ # Check for rate-limit error from slack_sdk
2620
+ err_str = str(exc).lower()
2621
+ is_rate_limit = (
2622
+ "ratelimited" in err_str
2623
+ or "429" in err_str
2624
+ or "rate_limited" in err_str
2625
+ )
2626
+ if is_rate_limit and attempt < 2:
2627
+ retry_after = 1.0 * (2 ** attempt) # 1s, 2s
2628
+ logger.warning(
2629
+ "[Slack] conversations.replies rate limited; retrying in %.1fs (attempt %d/3)",
2630
+ retry_after, attempt + 1,
2631
+ )
2632
+ await asyncio.sleep(retry_after)
2633
+ continue
2634
+ raise
2635
+
2636
+ if result is None:
2637
+ return ""
2638
+
2639
+ messages = result.get("messages", [])
2640
+ if not messages:
2641
+ return ""
2642
+
2643
+ bot_uid = self._team_bot_user_ids.get(team_id, self._bot_user_id)
2644
+ context_parts = []
2645
+ parent_text = ""
2646
+ for msg in messages:
2647
+ msg_ts = msg.get("ts", "")
2648
+ # Exclude the current triggering message — it will be delivered
2649
+ # as the user message itself, so including it here would duplicate it.
2650
+ if msg_ts == current_ts:
2651
+ continue
2652
+
2653
+ is_parent = msg_ts == thread_ts
2654
+ is_bot = bool(msg.get("bot_id")) or msg.get("subtype") == "bot_message"
2655
+ msg_user = msg.get("user", "")
2656
+
2657
+ # Identify "our own" bot for this workspace (multi-workspace safe).
2658
+ msg_team = msg.get("team") or team_id
2659
+ self_bot_uid = (
2660
+ self._team_bot_user_ids.get(msg_team)
2661
+ if msg_team
2662
+ else None
2663
+ ) or self._bot_user_id
2664
+
2665
+ # Exclude only our own prior bot replies (circular context).
2666
+ # Keep:
2667
+ # - the thread parent even if it was posted by a bot
2668
+ # (e.g. a cron job summary we are now replying to);
2669
+ # - other bots' child messages (useful third-party context).
2670
+ if (
2671
+ is_bot
2672
+ and not is_parent
2673
+ and self_bot_uid
2674
+ and msg_user == self_bot_uid
2675
+ ):
2676
+ continue
2677
+
2678
+ msg_text = msg.get("text", "").strip()
2679
+ if not msg_text:
2680
+ continue
2681
+
2682
+ # Strip bot mentions from context messages
2683
+ if bot_uid:
2684
+ msg_text = msg_text.replace(f"<@{bot_uid}>", "").strip()
2685
+
2686
+ prefix = "[thread parent] " if is_parent else ""
2687
+ display_user = msg_user or "unknown"
2688
+ # Prefer the bot's own name when the message is a bot post.
2689
+ if is_bot and not display_user:
2690
+ display_user = msg.get("username") or "bot"
2691
+ name = await self._resolve_user_name(display_user, chat_id=channel_id)
2692
+ context_parts.append(f"{prefix}{name}: {msg_text}")
2693
+ if is_parent:
2694
+ parent_text = msg_text
2695
+
2696
+ content = ""
2697
+ if context_parts:
2698
+ content = (
2699
+ "[Thread context — prior messages in this thread (not yet in conversation history):]\n"
2700
+ + "\n".join(context_parts)
2701
+ + "\n[End of thread context]\n\n"
2702
+ )
2703
+
2704
+ self._thread_context_cache[cache_key] = _ThreadContextCache(
2705
+ content=content,
2706
+ fetched_at=now,
2707
+ message_count=len(context_parts),
2708
+ parent_text=parent_text,
2709
+ )
2710
+ return content
2711
+
2712
+ except Exception as e:
2713
+ logger.warning("[Slack] Failed to fetch thread context: %s", e)
2714
+ return ""
2715
+
2716
+ async def _fetch_thread_parent_text(
2717
+ self, channel_id: str, thread_ts: str, team_id: str = "",
2718
+ ) -> str:
2719
+ """Return the raw text of the thread parent message (for reply_to_text).
2720
+
2721
+ Uses the same per-thread cache as :meth:`_fetch_thread_context` to avoid
2722
+ hitting ``conversations.replies`` twice. Falls back to a cheap single-
2723
+ message fetch (``limit=1, inclusive=True``) when the cache is cold.
2724
+
2725
+ Returns empty string on any failure — callers should treat an empty
2726
+ return as "no parent context to inject".
2727
+ """
2728
+ cache_key = f"{channel_id}:{thread_ts}:{team_id}"
2729
+ now = time.monotonic()
2730
+ cached = self._thread_context_cache.get(cache_key)
2731
+ if cached and (now - cached.fetched_at) < self._THREAD_CACHE_TTL:
2732
+ return cached.parent_text
2733
+
2734
+ try:
2735
+ client = self._get_client(channel_id)
2736
+ result = await client.conversations_replies(
2737
+ channel=channel_id,
2738
+ ts=thread_ts,
2739
+ limit=1,
2740
+ inclusive=True,
2741
+ )
2742
+ messages = result.get("messages", []) if result else []
2743
+ if not messages:
2744
+ return ""
2745
+ parent = messages[0]
2746
+ if parent.get("ts", "") != thread_ts:
2747
+ return ""
2748
+ bot_uid = self._team_bot_user_ids.get(team_id, self._bot_user_id)
2749
+ text = (parent.get("text") or "").strip()
2750
+ if bot_uid:
2751
+ text = text.replace(f"<@{bot_uid}>", "").strip()
2752
+ return text
2753
+ except Exception as exc: # pragma: no cover - defensive
2754
+ logger.debug("[Slack] Failed to fetch thread parent text: %s", exc)
2755
+ return ""
2756
+
2757
+ async def _handle_slash_command(self, command: dict) -> None:
2758
+ """Handle Slack slash commands.
2759
+
2760
+ Every gateway command in COMMAND_REGISTRY is registered as a native
2761
+ Slack slash (``/btw``, ``/stop``, ``/model``, etc.), matching the
2762
+ Discord and Telegram model. The slash name itself is the command;
2763
+ any text after it is the argument list.
2764
+
2765
+ The legacy ``/hermes <subcommand> [args]`` form is preserved for
2766
+ backward compatibility with older workspace manifests and for users
2767
+ who want a single entry point for free-form questions (``/hermes
2768
+ what's the weather`` — non-slash text is treated as a regular
2769
+ message).
2770
+ """
2771
+ slash_name = (command.get("command") or "").lstrip("/").strip()
2772
+ text = command.get("text", "").strip()
2773
+ user_id = command.get("user_id", "")
2774
+ channel_id = command.get("channel_id", "")
2775
+ team_id = command.get("team_id", "")
2776
+
2777
+ # Track which workspace owns this channel
2778
+ if team_id and channel_id:
2779
+ self._channel_team[channel_id] = team_id
2780
+
2781
+ if slash_name in {"hermes", ""}:
2782
+ # Legacy /hermes <subcommand> [args] routing + free-form questions.
2783
+ # Empty slash_name falls into this branch for backward compat
2784
+ # with any caller that didn't populate command["command"].
2785
+ from hermes_cli.commands import slack_subcommand_map
2786
+ subcommand_map = slack_subcommand_map()
2787
+ subcommand_map["compact"] = "/compress"
2788
+ # Guard against whitespace-only text where ``text`` is truthy but
2789
+ # ``text.split()`` returns ``[]`` (e.g. user sends ``/hermes ``).
2790
+ parts = text.split() if text else []
2791
+ first_word = parts[0] if parts else ""
2792
+ if first_word in subcommand_map:
2793
+ rest = text[len(first_word):].strip()
2794
+ text = f"{subcommand_map[first_word]} {rest}".strip() if rest else subcommand_map[first_word]
2795
+ elif text:
2796
+ pass # Treat as a regular question
2797
+ else:
2798
+ text = "/help"
2799
+ else:
2800
+ # Native slash — /<slash_name> [args]. Route directly through the
2801
+ # gateway command dispatcher by prepending the slash.
2802
+ text = f"/{slash_name} {text}".strip()
2803
+
2804
+ # Slack slash commands can originate from DMs or shared channels.
2805
+ # Preserve DM semantics only for DM channel IDs; shared channels must
2806
+ # keep group semantics so different users do not collide into one
2807
+ # session key.
2808
+ is_dm = str(channel_id).startswith("D")
2809
+ source = self.build_source(
2810
+ chat_id=channel_id,
2811
+ chat_type="dm" if is_dm else "group",
2812
+ user_id=user_id,
2813
+ )
2814
+
2815
+ event = MessageEvent(
2816
+ text=text,
2817
+ message_type=MessageType.COMMAND if text.startswith("/") else MessageType.TEXT,
2818
+ source=source,
2819
+ raw_message=command,
2820
+ )
2821
+
2822
+ # Stash the Slack response_url so the first reply for this
2823
+ # channel+user can be routed ephemerally (replaces the initial
2824
+ # "Running /cmd…" ack shown by handle_hermes_command).
2825
+ # Only stash for COMMAND events (text starts with "/") — free-form
2826
+ # questions via "/hermes <question>" must produce public replies so
2827
+ # the whole channel can see the agent's answer.
2828
+ response_url = command.get("response_url", "")
2829
+ if response_url and user_id and channel_id and text.startswith("/"):
2830
+ self._slash_command_contexts[(channel_id, user_id)] = {
2831
+ "response_url": response_url,
2832
+ "ts": time.monotonic(),
2833
+ }
2834
+
2835
+ # Set the ContextVar so send() can match the correct stashed
2836
+ # response_url even when multiple users slash concurrently.
2837
+ _slash_user_id_token = _slash_user_id.set(user_id or None)
2838
+ try:
2839
+ await self.handle_message(event)
2840
+ finally:
2841
+ _slash_user_id.reset(_slash_user_id_token)
2842
+
2843
+ def _has_active_session_for_thread(
2844
+ self,
2845
+ channel_id: str,
2846
+ thread_ts: str,
2847
+ user_id: str,
2848
+ ) -> bool:
2849
+ """Check if there's an active session for a thread.
2850
+
2851
+ Used to determine if thread replies without @mentions should be
2852
+ processed (they should if there's an active session).
2853
+
2854
+ Uses ``build_session_key()`` as the single source of truth for key
2855
+ construction — avoids the bug where manual key building didn't
2856
+ respect ``thread_sessions_per_user`` and ``group_sessions_per_user``
2857
+ settings correctly.
2858
+ """
2859
+ session_store = getattr(self, "_session_store", None)
2860
+ if not session_store:
2861
+ return False
2862
+
2863
+ try:
2864
+ from gateway.session import SessionSource, build_session_key
2865
+
2866
+ source = SessionSource(
2867
+ platform=Platform.SLACK,
2868
+ chat_id=channel_id,
2869
+ chat_type="group",
2870
+ user_id=user_id,
2871
+ thread_id=thread_ts,
2872
+ )
2873
+
2874
+ # Read session isolation settings from the store's config
2875
+ store_cfg = getattr(session_store, "config", None)
2876
+ gspu = getattr(store_cfg, "group_sessions_per_user", True) if store_cfg else True
2877
+ tspu = getattr(store_cfg, "thread_sessions_per_user", False) if store_cfg else False
2878
+
2879
+ session_key = build_session_key(
2880
+ source,
2881
+ group_sessions_per_user=gspu,
2882
+ thread_sessions_per_user=tspu,
2883
+ )
2884
+
2885
+ session_store._ensure_loaded()
2886
+ return session_key in session_store._entries
2887
+ except Exception:
2888
+ return False
2889
+
2890
+ async def _download_slack_file(self, url: str, ext: str, audio: bool = False, team_id: str = "") -> str:
2891
+ """Download a Slack file using the bot token for auth, with retry."""
2892
+ import httpx
2893
+
2894
+ bot_token = self._team_clients[team_id].token if team_id and team_id in self._team_clients else self.config.token
2895
+
2896
+ async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
2897
+ for attempt in range(3):
2898
+ try:
2899
+ response = await client.get(
2900
+ url,
2901
+ headers={"Authorization": f"Bearer {bot_token}"},
2902
+ )
2903
+ response.raise_for_status()
2904
+
2905
+ # Slack may return an HTML sign-in/redirect page
2906
+ # instead of actual media bytes (e.g. expired token,
2907
+ # restricted file access). Detect this early so we
2908
+ # don't cache bogus data and confuse downstream tools.
2909
+ ct = response.headers.get("content-type", "")
2910
+ if "text/html" in ct:
2911
+ raise ValueError(
2912
+ "Slack returned HTML instead of media "
2913
+ f"(content-type: {ct}); "
2914
+ "check bot token scopes and file permissions"
2915
+ )
2916
+
2917
+ if audio:
2918
+ from gateway.platforms.base import cache_audio_from_bytes
2919
+ return cache_audio_from_bytes(response.content, ext)
2920
+ else:
2921
+ from gateway.platforms.base import cache_image_from_bytes
2922
+ return cache_image_from_bytes(response.content, ext)
2923
+ except (httpx.TimeoutException, httpx.HTTPStatusError) as exc:
2924
+ if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code < 429:
2925
+ raise
2926
+ if attempt < 2:
2927
+ logger.debug("Slack file download retry %d/2 for %s: %s",
2928
+ attempt + 1, url[:80], exc)
2929
+ await asyncio.sleep(1.5 * (attempt + 1))
2930
+ continue
2931
+ raise
2932
+
2933
+ async def _download_slack_file_bytes(self, url: str, team_id: str = "") -> bytes:
2934
+ """Download a Slack file and return raw bytes, with retry."""
2935
+ import httpx
2936
+
2937
+ bot_token = self._team_clients[team_id].token if team_id and team_id in self._team_clients else self.config.token
2938
+
2939
+ async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
2940
+ for attempt in range(3):
2941
+ try:
2942
+ response = await client.get(
2943
+ url,
2944
+ headers={"Authorization": f"Bearer {bot_token}"},
2945
+ )
2946
+ response.raise_for_status()
2947
+ ct = response.headers.get("content-type", "")
2948
+ if "text/html" in ct:
2949
+ raise ValueError(
2950
+ "Slack returned HTML instead of file bytes "
2951
+ f"(content-type: {ct}); "
2952
+ "check bot token scopes and file permissions"
2953
+ )
2954
+ return response.content
2955
+ except (httpx.TimeoutException, httpx.HTTPStatusError, ValueError) as exc:
2956
+ if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code < 429:
2957
+ raise
2958
+ if isinstance(exc, ValueError):
2959
+ raise
2960
+ if attempt < 2:
2961
+ logger.debug("Slack file download retry %d/2 for %s: %s",
2962
+ attempt + 1, url[:80], exc)
2963
+ await asyncio.sleep(1.5 * (attempt + 1))
2964
+ continue
2965
+ raise
2966
+
2967
+ # ── Channel mention gating ─────────────────────────────────────────────
2968
+
2969
+ def _slack_require_mention(self) -> bool:
2970
+ """Return whether channel messages require an explicit bot mention.
2971
+
2972
+ Uses explicit-false parsing (like Discord/Matrix) rather than
2973
+ truthy parsing, since the safe default is True (gating on).
2974
+ Unrecognised or empty values keep gating enabled.
2975
+ """
2976
+ configured = self.config.extra.get("require_mention")
2977
+ if configured is not None:
2978
+ if isinstance(configured, str):
2979
+ return configured.lower() not in {"false", "0", "no", "off"}
2980
+ return bool(configured)
2981
+ return os.getenv("SLACK_REQUIRE_MENTION", "true").lower() not in {"false", "0", "no", "off"}
2982
+
2983
+ def _slack_strict_mention(self) -> bool:
2984
+ """When true, channel threads require an explicit @-mention on every
2985
+ message. Disables all auto-triggers (mentioned-thread memory,
2986
+ bot-message follow-up, session-presence). Defaults to False.
2987
+ """
2988
+ configured = self.config.extra.get("strict_mention")
2989
+ if configured is not None:
2990
+ if isinstance(configured, str):
2991
+ return configured.lower() in {"true", "1", "yes", "on"}
2992
+ return bool(configured)
2993
+ return os.getenv("SLACK_STRICT_MENTION", "false").lower() in {"true", "1", "yes", "on"}
2994
+
2995
+ def _slack_free_response_channels(self) -> set:
2996
+ """Return channel IDs where no @mention is required."""
2997
+ raw = self.config.extra.get("free_response_channels")
2998
+ if raw is None:
2999
+ raw = os.getenv("SLACK_FREE_RESPONSE_CHANNELS", "")
3000
+ if isinstance(raw, list):
3001
+ return {str(part).strip() for part in raw if str(part).strip()}
3002
+ # Coerce non-list scalars (str/int/float) to str before splitting.
3003
+ # A bare numeric YAML value (`free_response_channels: 1234567890`) is
3004
+ # loaded as int and was previously falling through the isinstance(str)
3005
+ # branch to return an empty set. str() here accepts whatever scalar
3006
+ # the YAML loader hands us without changing existing string/CSV
3007
+ # semantics.
3008
+ s = str(raw).strip() if raw is not None else ""
3009
+ if s:
3010
+ return {part.strip() for part in s.split(",") if part.strip()}
3011
+ return set()
3012
+
3013
+ def _slack_allowed_channels(self) -> set:
3014
+ """Return the whitelist of channel IDs the bot will respond in.
3015
+
3016
+ When non-empty, messages from channels NOT in this set are silently
3017
+ ignored — even if the bot is @mentioned. DMs are never filtered.
3018
+ Empty set means no restriction (fully backward compatible).
3019
+ """
3020
+ raw = self.config.extra.get("allowed_channels")
3021
+ if raw is None:
3022
+ raw = os.getenv("SLACK_ALLOWED_CHANNELS", "")
3023
+ if isinstance(raw, list):
3024
+ return {str(part).strip() for part in raw if str(part).strip()}
3025
+ if isinstance(raw, str) and raw.strip():
3026
+ return {part.strip() for part in raw.split(",") if part.strip()}
3027
+ return set()
3028
+