code-muse 0.0.1__py3-none-any.whl

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 (394) hide show
  1. code_muse/__init__.py +26 -0
  2. code_muse/__main__.py +10 -0
  3. code_muse/agents/__init__.py +31 -0
  4. code_muse/agents/_builder.py +214 -0
  5. code_muse/agents/_compaction.py +506 -0
  6. code_muse/agents/_diagnostics.py +171 -0
  7. code_muse/agents/_history.py +382 -0
  8. code_muse/agents/_key_listeners.py +148 -0
  9. code_muse/agents/_non_streaming_render.py +148 -0
  10. code_muse/agents/_runtime.py +596 -0
  11. code_muse/agents/agent_creator_agent.py +603 -0
  12. code_muse/agents/agent_helios.py +47 -0
  13. code_muse/agents/agent_manager.py +740 -0
  14. code_muse/agents/agent_muse.py +78 -0
  15. code_muse/agents/agent_planning.py +44 -0
  16. code_muse/agents/agent_qa_melpomene.py +207 -0
  17. code_muse/agents/base_agent.py +194 -0
  18. code_muse/agents/event_stream_handler.py +361 -0
  19. code_muse/agents/json_agent.py +201 -0
  20. code_muse/agents/prompt_v3.py +521 -0
  21. code_muse/agents/subagent_stream_handler.py +273 -0
  22. code_muse/callbacks.py +941 -0
  23. code_muse/chatgpt_codex_client.py +333 -0
  24. code_muse/claude_cache_client.py +853 -0
  25. code_muse/cli_runner/__init__.py +319 -0
  26. code_muse/cli_runner/args.py +63 -0
  27. code_muse/cli_runner/loop.py +510 -0
  28. code_muse/cli_runner/resume.py +72 -0
  29. code_muse/cli_runner/runner.py +161 -0
  30. code_muse/command_line/__init__.py +1 -0
  31. code_muse/command_line/add_model_menu.py +1331 -0
  32. code_muse/command_line/agent_menu.py +674 -0
  33. code_muse/command_line/attachments.py +397 -0
  34. code_muse/command_line/autosave_menu.py +709 -0
  35. code_muse/command_line/clipboard.py +528 -0
  36. code_muse/command_line/colors_menu.py +530 -0
  37. code_muse/command_line/command_handler.py +262 -0
  38. code_muse/command_line/command_registry.py +150 -0
  39. code_muse/command_line/config_commands.py +711 -0
  40. code_muse/command_line/core_commands.py +740 -0
  41. code_muse/command_line/diff_menu.py +865 -0
  42. code_muse/command_line/file_path_completion.py +73 -0
  43. code_muse/command_line/load_context_completion.py +57 -0
  44. code_muse/command_line/model_picker_completion.py +512 -0
  45. code_muse/command_line/model_settings_menu.py +983 -0
  46. code_muse/command_line/onboarding_slides.py +162 -0
  47. code_muse/command_line/onboarding_wizard.py +337 -0
  48. code_muse/command_line/pagination.py +41 -0
  49. code_muse/command_line/pin_command_completion.py +329 -0
  50. code_muse/command_line/prompt_toolkit_completion.py +886 -0
  51. code_muse/command_line/session_commands.py +304 -0
  52. code_muse/command_line/shell_passthrough.py +145 -0
  53. code_muse/command_line/skills_completion.py +158 -0
  54. code_muse/command_line/types.py +18 -0
  55. code_muse/command_line/uc_menu.py +908 -0
  56. code_muse/command_line/utils.py +105 -0
  57. code_muse/command_line/wiggum_state.py +77 -0
  58. code_muse/config.py +1138 -0
  59. code_muse/config_agent.py +168 -0
  60. code_muse/config_appearance.py +241 -0
  61. code_muse/config_model.py +357 -0
  62. code_muse/config_security.py +73 -0
  63. code_muse/error_logging.py +132 -0
  64. code_muse/evals/__init__.py +35 -0
  65. code_muse/evals/eval_helpers.py +81 -0
  66. code_muse/evals/eval_runner.py +299 -0
  67. code_muse/evals/sample_evals/__init__.py +1 -0
  68. code_muse/evals/sample_evals/eval_frugal_reads.py +59 -0
  69. code_muse/evals/sample_evals/eval_memory_planning.py +31 -0
  70. code_muse/evals/sample_evals/eval_shell_efficiency.py +39 -0
  71. code_muse/evals/sample_evals/eval_tool_masking.py +33 -0
  72. code_muse/fs_scan_cache/__init__.py +31 -0
  73. code_muse/fs_scan_cache/invalidation_hooks.py +89 -0
  74. code_muse/fs_scan_cache/scan_cache_core.cpython-314-darwin.so +0 -0
  75. code_muse/fs_scan_cache/scan_cache_core.pyx +203 -0
  76. code_muse/fs_scan_cache/tool_integration.py +309 -0
  77. code_muse/fs_scan_cache/ttl_policy.py +44 -0
  78. code_muse/gemini_code_assist.py +383 -0
  79. code_muse/gemini_model.py +838 -0
  80. code_muse/hook_engine/README.md +105 -0
  81. code_muse/hook_engine/__init__.py +21 -0
  82. code_muse/hook_engine/aliases.py +153 -0
  83. code_muse/hook_engine/engine.py +221 -0
  84. code_muse/hook_engine/executor.py +347 -0
  85. code_muse/hook_engine/matcher.py +154 -0
  86. code_muse/hook_engine/models.py +245 -0
  87. code_muse/hook_engine/registry.py +114 -0
  88. code_muse/hook_engine/trust.py +268 -0
  89. code_muse/hook_engine/validator.py +144 -0
  90. code_muse/http_utils.py +360 -0
  91. code_muse/keymap.py +128 -0
  92. code_muse/list_filtering.py +26 -0
  93. code_muse/main.py +10 -0
  94. code_muse/messaging/__init__.py +259 -0
  95. code_muse/messaging/bus.py +621 -0
  96. code_muse/messaging/commands.py +166 -0
  97. code_muse/messaging/markdown_patches.py +57 -0
  98. code_muse/messaging/message_queue.py +397 -0
  99. code_muse/messaging/messages.py +591 -0
  100. code_muse/messaging/queue_console.py +269 -0
  101. code_muse/messaging/renderers.py +308 -0
  102. code_muse/messaging/rich_renderer.py +1158 -0
  103. code_muse/messaging/shimmer.py +154 -0
  104. code_muse/messaging/spinner/__init__.py +87 -0
  105. code_muse/messaging/spinner/console_spinner.py +250 -0
  106. code_muse/messaging/spinner/spinner_base.py +82 -0
  107. code_muse/messaging/subagent_console.py +458 -0
  108. code_muse/model_factory.py +1203 -0
  109. code_muse/model_switching.py +59 -0
  110. code_muse/model_utils.py +156 -0
  111. code_muse/models.json +66 -0
  112. code_muse/models_cache/__init__.py +26 -0
  113. code_muse/models_cache/blocking_lru_cache.py +98 -0
  114. code_muse/models_cache/cache_writer.py +86 -0
  115. code_muse/models_cache/sha256_hash.cpython-314-darwin.so +0 -0
  116. code_muse/models_cache/sha256_hash.pyx +34 -0
  117. code_muse/models_cache/startup_integration.py +75 -0
  118. code_muse/models_dev_api.json +1 -0
  119. code_muse/models_dev_parser.py +590 -0
  120. code_muse/motion.py +126 -0
  121. code_muse/plugins/__init__.py +471 -0
  122. code_muse/plugins/agent_skills/__init__.py +32 -0
  123. code_muse/plugins/agent_skills/config.py +176 -0
  124. code_muse/plugins/agent_skills/discovery.py +309 -0
  125. code_muse/plugins/agent_skills/downloader.py +389 -0
  126. code_muse/plugins/agent_skills/installer.py +19 -0
  127. code_muse/plugins/agent_skills/metadata.py +293 -0
  128. code_muse/plugins/agent_skills/prompt_builder.py +66 -0
  129. code_muse/plugins/agent_skills/register_callbacks.py +298 -0
  130. code_muse/plugins/agent_skills/remote_catalog.py +320 -0
  131. code_muse/plugins/agent_skills/skill_catalog.py +254 -0
  132. code_muse/plugins/agent_skills/skills_install_menu.py +690 -0
  133. code_muse/plugins/agent_skills/skills_menu.py +791 -0
  134. code_muse/plugins/autonomous_memory/__init__.py +39 -0
  135. code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-darwin.so +0 -0
  136. code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-x86_64-linux-gnu.so +0 -0
  137. code_muse/plugins/autonomous_memory/bm25_scorer.pyx +291 -0
  138. code_muse/plugins/autonomous_memory/consolidation.py +82 -0
  139. code_muse/plugins/autonomous_memory/extraction.py +382 -0
  140. code_muse/plugins/autonomous_memory/lease_lock.py +105 -0
  141. code_muse/plugins/autonomous_memory/memory_injection.py +59 -0
  142. code_muse/plugins/autonomous_memory/register_callbacks.py +268 -0
  143. code_muse/plugins/autonomous_memory/secret_scanner.py +62 -0
  144. code_muse/plugins/autonomous_memory/session_scanner.py +163 -0
  145. code_muse/plugins/aws_bedrock/__init__.py +14 -0
  146. code_muse/plugins/aws_bedrock/config.py +99 -0
  147. code_muse/plugins/aws_bedrock/register_callbacks.py +241 -0
  148. code_muse/plugins/aws_bedrock/utils.py +153 -0
  149. code_muse/plugins/azure_foundry/README.md +238 -0
  150. code_muse/plugins/azure_foundry/__init__.py +15 -0
  151. code_muse/plugins/azure_foundry/config.py +125 -0
  152. code_muse/plugins/azure_foundry/discovery.py +187 -0
  153. code_muse/plugins/azure_foundry/register_callbacks.py +495 -0
  154. code_muse/plugins/azure_foundry/token.py +180 -0
  155. code_muse/plugins/azure_foundry/utils.py +345 -0
  156. code_muse/plugins/build_filter/__init__.py +1 -0
  157. code_muse/plugins/build_filter/register_callbacks.py +201 -0
  158. code_muse/plugins/build_filter/strategies/__init__.py +1 -0
  159. code_muse/plugins/build_filter/strategies/build.py +397 -0
  160. code_muse/plugins/chatgpt_oauth/__init__.py +6 -0
  161. code_muse/plugins/chatgpt_oauth/config.py +52 -0
  162. code_muse/plugins/chatgpt_oauth/oauth_flow.py +338 -0
  163. code_muse/plugins/chatgpt_oauth/register_callbacks.py +172 -0
  164. code_muse/plugins/chatgpt_oauth/test_plugin.py +301 -0
  165. code_muse/plugins/chatgpt_oauth/utils.py +538 -0
  166. code_muse/plugins/checkpointing/__init__.py +29 -0
  167. code_muse/plugins/checkpointing/checkpoint_hook.py +51 -0
  168. code_muse/plugins/checkpointing/conversation_snapshots.py +117 -0
  169. code_muse/plugins/checkpointing/register_callbacks.py +51 -0
  170. code_muse/plugins/checkpointing/restore_command.py +263 -0
  171. code_muse/plugins/checkpointing/rewind_shortcut.py +88 -0
  172. code_muse/plugins/checkpointing/shadow_git.py +90 -0
  173. code_muse/plugins/claude_code_hooks/__init__.py +1 -0
  174. code_muse/plugins/claude_code_hooks/config.py +188 -0
  175. code_muse/plugins/claude_code_hooks/register_callbacks.py +208 -0
  176. code_muse/plugins/claude_code_oauth/README.md +167 -0
  177. code_muse/plugins/claude_code_oauth/SETUP.md +93 -0
  178. code_muse/plugins/claude_code_oauth/__init__.py +25 -0
  179. code_muse/plugins/claude_code_oauth/config.py +52 -0
  180. code_muse/plugins/claude_code_oauth/fast_mode.py +124 -0
  181. code_muse/plugins/claude_code_oauth/prompt_handler.py +63 -0
  182. code_muse/plugins/claude_code_oauth/register_callbacks.py +547 -0
  183. code_muse/plugins/claude_code_oauth/test_fast_mode.py +165 -0
  184. code_muse/plugins/claude_code_oauth/test_plugin.py +283 -0
  185. code_muse/plugins/claude_code_oauth/token_refresh_heartbeat.py +237 -0
  186. code_muse/plugins/claude_code_oauth/utils.py +664 -0
  187. code_muse/plugins/copilot_auth/__init__.py +11 -0
  188. code_muse/plugins/copilot_auth/config.py +91 -0
  189. code_muse/plugins/copilot_auth/reasoning_client.py +409 -0
  190. code_muse/plugins/copilot_auth/register_callbacks.py +461 -0
  191. code_muse/plugins/copilot_auth/utils.py +584 -0
  192. code_muse/plugins/custom_commands/__init__.py +14 -0
  193. code_muse/plugins/custom_commands/args_injection.py +82 -0
  194. code_muse/plugins/custom_commands/command_discovery.py +89 -0
  195. code_muse/plugins/custom_commands/command_toml_schema.py +71 -0
  196. code_muse/plugins/custom_commands/register_callbacks.py +176 -0
  197. code_muse/plugins/customizable_commands/__init__.py +0 -0
  198. code_muse/plugins/customizable_commands/register_callbacks.py +136 -0
  199. code_muse/plugins/destructive_command_guard/__init__.py +14 -0
  200. code_muse/plugins/destructive_command_guard/detector.py +375 -0
  201. code_muse/plugins/destructive_command_guard/register_callbacks.py +148 -0
  202. code_muse/plugins/example_custom_command/README.md +280 -0
  203. code_muse/plugins/example_custom_command/register_callbacks.py +51 -0
  204. code_muse/plugins/file_permission_handler/__init__.py +4 -0
  205. code_muse/plugins/file_permission_handler/register_callbacks.py +441 -0
  206. code_muse/plugins/filter_engine/__init__.py +30 -0
  207. code_muse/plugins/filter_engine/classifier.py +153 -0
  208. code_muse/plugins/filter_engine/content_detector.py +184 -0
  209. code_muse/plugins/filter_engine/dispatcher.py +244 -0
  210. code_muse/plugins/filter_engine/register_callbacks.py +188 -0
  211. code_muse/plugins/filter_engine/registry.py +279 -0
  212. code_muse/plugins/filter_engine/strategies/__init__.py +8 -0
  213. code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-darwin.so +0 -0
  214. code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-x86_64-linux-gnu.so +0 -0
  215. code_muse/plugins/filter_engine/strategies/ast_compressor.pyx +348 -0
  216. code_muse/plugins/filter_engine/strategies/ast_parser.py +167 -0
  217. code_muse/plugins/filter_engine/strategies/code.cpython-314-darwin.so +0 -0
  218. code_muse/plugins/filter_engine/strategies/code.cpython-314-x86_64-linux-gnu.so +0 -0
  219. code_muse/plugins/filter_engine/strategies/code.pyx +584 -0
  220. code_muse/plugins/filter_engine/strategies/git.cpython-314-darwin.so +0 -0
  221. code_muse/plugins/filter_engine/strategies/git.cpython-314-x86_64-linux-gnu.so +0 -0
  222. code_muse/plugins/filter_engine/strategies/git.pyx +438 -0
  223. code_muse/plugins/filter_engine/strategies/json_compressor.cpython-314-darwin.so +0 -0
  224. code_muse/plugins/filter_engine/strategies/json_compressor.pyx +253 -0
  225. code_muse/plugins/filter_engine/strategies/json_patterns.cpython-314-darwin.so +0 -0
  226. code_muse/plugins/filter_engine/strategies/json_patterns.pyx +178 -0
  227. code_muse/plugins/filter_engine/strategies/lint.cpython-314-darwin.so +0 -0
  228. code_muse/plugins/filter_engine/strategies/lint.cpython-314-x86_64-linux-gnu.so +0 -0
  229. code_muse/plugins/filter_engine/strategies/lint.pyx +626 -0
  230. code_muse/plugins/filter_engine/strategies/test.cpython-314-darwin.so +0 -0
  231. code_muse/plugins/filter_engine/strategies/test.cpython-314-x86_64-linux-gnu.so +0 -0
  232. code_muse/plugins/filter_engine/strategies/test.pyx +431 -0
  233. code_muse/plugins/filter_engine/verbosity.py +63 -0
  234. code_muse/plugins/force_push_guard/__init__.py +5 -0
  235. code_muse/plugins/force_push_guard/detector.py +96 -0
  236. code_muse/plugins/force_push_guard/register_callbacks.py +144 -0
  237. code_muse/plugins/force_push_guard/test_detector.py +143 -0
  238. code_muse/plugins/frontend_emitter/__init__.py +25 -0
  239. code_muse/plugins/frontend_emitter/emitter.py +121 -0
  240. code_muse/plugins/frontend_emitter/register_callbacks.py +259 -0
  241. code_muse/plugins/gac/__init__.py +4 -0
  242. code_muse/plugins/gac/git_ops.py +136 -0
  243. code_muse/plugins/gac/prompt.py +191 -0
  244. code_muse/plugins/gac/register_callbacks.py +82 -0
  245. code_muse/plugins/hook_creator/__init__.py +1 -0
  246. code_muse/plugins/hook_creator/register_callbacks.py +34 -0
  247. code_muse/plugins/hook_manager/__init__.py +1 -0
  248. code_muse/plugins/hook_manager/config.py +289 -0
  249. code_muse/plugins/hook_manager/hooks_menu.py +563 -0
  250. code_muse/plugins/hook_manager/register_callbacks.py +227 -0
  251. code_muse/plugins/hook_monitor/register_callbacks.py +36 -0
  252. code_muse/plugins/mindpack/__init__.py +0 -0
  253. code_muse/plugins/mindpack/factory.py +930 -0
  254. code_muse/plugins/mindpack/judge.py +573 -0
  255. code_muse/plugins/mindpack/memory.py +100 -0
  256. code_muse/plugins/mindpack/mindpack_menu.py +1552 -0
  257. code_muse/plugins/mindpack/orchestration.py +605 -0
  258. code_muse/plugins/mindpack/register_callbacks.py +175 -0
  259. code_muse/plugins/mindpack/schemas.py +358 -0
  260. code_muse/plugins/mindpack/tools.py +387 -0
  261. code_muse/plugins/oauth_muse_html.py +226 -0
  262. code_muse/plugins/ollama_setup/__init__.py +5 -0
  263. code_muse/plugins/ollama_setup/completer.py +36 -0
  264. code_muse/plugins/ollama_setup/register_callbacks.py +410 -0
  265. code_muse/plugins/plan_command/__init__.py +0 -0
  266. code_muse/plugins/plan_command/register_callbacks.py +206 -0
  267. code_muse/plugins/plan_mode/__init__.py +37 -0
  268. code_muse/plugins/plan_mode/mode_cycling.py +40 -0
  269. code_muse/plugins/plan_mode/plan_generation.py +68 -0
  270. code_muse/plugins/plan_mode/plan_hooks.py +74 -0
  271. code_muse/plugins/plan_mode/plan_mode_tools.py +138 -0
  272. code_muse/plugins/plan_mode/register_callbacks.py +121 -0
  273. code_muse/plugins/plugin_trust/register_callbacks.py +140 -0
  274. code_muse/plugins/policy_engine/__init__.py +46 -0
  275. code_muse/plugins/policy_engine/approval_flow_integration.py +59 -0
  276. code_muse/plugins/policy_engine/policy_evaluator.py +75 -0
  277. code_muse/plugins/policy_engine/policy_file_discovery.py +90 -0
  278. code_muse/plugins/policy_engine/policy_toml_schema.py +115 -0
  279. code_muse/plugins/policy_engine/register_callbacks.py +112 -0
  280. code_muse/plugins/pop_command/__init__.py +1 -0
  281. code_muse/plugins/pop_command/register_callbacks.py +189 -0
  282. code_muse/plugins/prompt_newline/__init__.py +13 -0
  283. code_muse/plugins/prompt_newline/config.py +19 -0
  284. code_muse/plugins/prompt_newline/register_callbacks.py +159 -0
  285. code_muse/plugins/safety_status/__init__.py +0 -0
  286. code_muse/plugins/safety_status/register_callbacks.py +113 -0
  287. code_muse/plugins/semantic_compression/__init__.py +6 -0
  288. code_muse/plugins/semantic_compression/compressor.py +295 -0
  289. code_muse/plugins/semantic_compression/config.py +123 -0
  290. code_muse/plugins/semantic_compression/register_callbacks.py +320 -0
  291. code_muse/plugins/shell_minimizer/__init__.py +50 -0
  292. code_muse/plugins/shell_minimizer/builtin_filters.toml +393 -0
  293. code_muse/plugins/shell_minimizer/pipeline.py +556 -0
  294. code_muse/plugins/shell_minimizer/primitives.py +482 -0
  295. code_muse/plugins/shell_minimizer/register_callbacks.py +276 -0
  296. code_muse/plugins/shell_safety/__init__.py +6 -0
  297. code_muse/plugins/shell_safety/agent_shell_safety.py +69 -0
  298. code_muse/plugins/shell_safety/command_cache.py +149 -0
  299. code_muse/plugins/shell_safety/register_callbacks.py +202 -0
  300. code_muse/plugins/synthetic_status/__init__.py +1 -0
  301. code_muse/plugins/synthetic_status/register_callbacks.py +128 -0
  302. code_muse/plugins/synthetic_status/status_api.py +145 -0
  303. code_muse/plugins/token_caching/__init__.py +21 -0
  304. code_muse/plugins/token_caching/cache_hit_tracking.py +128 -0
  305. code_muse/plugins/token_caching/cacheable_prefix_detection.py +28 -0
  306. code_muse/plugins/token_caching/register_callbacks.py +54 -0
  307. code_muse/plugins/token_caching/stats_display.py +35 -0
  308. code_muse/plugins/token_tracking/__init__.py +26 -0
  309. code_muse/plugins/token_tracking/database.py +381 -0
  310. code_muse/plugins/token_tracking/edit_analyzer.py +97 -0
  311. code_muse/plugins/token_tracking/record.py +55 -0
  312. code_muse/plugins/token_tracking/register_callbacks.py +277 -0
  313. code_muse/plugins/token_tracking/reports.py +329 -0
  314. code_muse/plugins/universal_constructor/__init__.py +13 -0
  315. code_muse/plugins/universal_constructor/models.py +136 -0
  316. code_muse/plugins/universal_constructor/register_callbacks.py +47 -0
  317. code_muse/plugins/universal_constructor/registry.py +390 -0
  318. code_muse/plugins/universal_constructor/runner.py +474 -0
  319. code_muse/plugins/universal_constructor/safety.py +440 -0
  320. code_muse/plugins/universal_constructor/sandbox.py +584 -0
  321. code_muse/provider_identity.py +105 -0
  322. code_muse/pydantic_patches.py +410 -0
  323. code_muse/reopenable_async_client.py +233 -0
  324. code_muse/round_robin_model.py +151 -0
  325. code_muse/secret_storage.py +74 -0
  326. code_muse/security/__init__.py +1 -0
  327. code_muse/security/redaction.cpython-314-darwin.so +0 -0
  328. code_muse/security/redaction.cpython-314-x86_64-linux-gnu.so +0 -0
  329. code_muse/security/redaction.pyx +135 -0
  330. code_muse/session_storage.py +565 -0
  331. code_muse/status_display.py +261 -0
  332. code_muse/stream_parser/__init__.py +76 -0
  333. code_muse/stream_parser/assistant_text_parser.py +90 -0
  334. code_muse/stream_parser/citation_parser.py +76 -0
  335. code_muse/stream_parser/inline_hidden_tag_parser.py +236 -0
  336. code_muse/stream_parser/proposed_plan_parser.py +158 -0
  337. code_muse/stream_parser/stream_text_chunk.py +23 -0
  338. code_muse/stream_parser/stream_text_parser.py +27 -0
  339. code_muse/stream_parser/tagged_line_parser.cpython-314-darwin.so +0 -0
  340. code_muse/stream_parser/tagged_line_parser.pyx +251 -0
  341. code_muse/stream_parser/utf8_stream_parser.cpython-314-darwin.so +0 -0
  342. code_muse/stream_parser/utf8_stream_parser.pyx +206 -0
  343. code_muse/summarization_agent.py +308 -0
  344. code_muse/terminal_utils.cpython-314-darwin.so +0 -0
  345. code_muse/terminal_utils.cpython-314-x86_64-linux-gnu.so +0 -0
  346. code_muse/terminal_utils.pyx +483 -0
  347. code_muse/tools/__init__.py +459 -0
  348. code_muse/tools/agent_tools.py +613 -0
  349. code_muse/tools/ask_user_question/__init__.py +26 -0
  350. code_muse/tools/ask_user_question/constants.py +73 -0
  351. code_muse/tools/ask_user_question/demo_tui.py +55 -0
  352. code_muse/tools/ask_user_question/handler.py +232 -0
  353. code_muse/tools/ask_user_question/models.py +302 -0
  354. code_muse/tools/ask_user_question/registration.py +37 -0
  355. code_muse/tools/ask_user_question/renderers.py +336 -0
  356. code_muse/tools/ask_user_question/terminal_ui.py +327 -0
  357. code_muse/tools/ask_user_question/theme.py +156 -0
  358. code_muse/tools/ask_user_question/tui_loop.py +422 -0
  359. code_muse/tools/background_jobs.py +99 -0
  360. code_muse/tools/browser/__init__.py +37 -0
  361. code_muse/tools/browser/browser_control.py +289 -0
  362. code_muse/tools/browser/browser_interactions.py +545 -0
  363. code_muse/tools/browser/browser_locators.py +640 -0
  364. code_muse/tools/browser/browser_manager.py +376 -0
  365. code_muse/tools/browser/browser_navigation.py +251 -0
  366. code_muse/tools/browser/browser_screenshot.py +180 -0
  367. code_muse/tools/browser/browser_scripts.py +462 -0
  368. code_muse/tools/browser/browser_workflows.py +222 -0
  369. code_muse/tools/chrome_cdp/__init__.py +1070 -0
  370. code_muse/tools/chrome_cdp/register_callbacks.py +61 -0
  371. code_muse/tools/command_runner.py +1401 -0
  372. code_muse/tools/common.py +1407 -0
  373. code_muse/tools/display.py +87 -0
  374. code_muse/tools/file_modifications.py +1099 -0
  375. code_muse/tools/file_operations.py +860 -0
  376. code_muse/tools/image_tools.py +185 -0
  377. code_muse/tools/meetin_proxy/__init__.py +243 -0
  378. code_muse/tools/meetin_proxy/capture_addon.py +82 -0
  379. code_muse/tools/meetin_proxy/proxy_manager.py +326 -0
  380. code_muse/tools/meetin_proxy/register_callbacks.py +45 -0
  381. code_muse/tools/path_policy.py +219 -0
  382. code_muse/tools/skills_tools.py +586 -0
  383. code_muse/tools/subagent_context.py +158 -0
  384. code_muse/tools/tools_content.py +50 -0
  385. code_muse/tools/universal_constructor.py +965 -0
  386. code_muse/uvx_detection.py +241 -0
  387. code_muse/version_checker.py +86 -0
  388. code_muse-0.0.1.data/data/code_muse/models.json +66 -0
  389. code_muse-0.0.1.data/data/code_muse/models_dev_api.json +1 -0
  390. code_muse-0.0.1.dist-info/METADATA +845 -0
  391. code_muse-0.0.1.dist-info/RECORD +394 -0
  392. code_muse-0.0.1.dist-info/WHEEL +4 -0
  393. code_muse-0.0.1.dist-info/entry_points.txt +2 -0
  394. code_muse-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1070 @@
1
+ """Chrome CDP — Chrome DevTools Protocol inspection tool.
2
+
3
+ Pure-Python CDP client using websockets. No Node.js dependency.
4
+ Connects directly to Chrome's remote-debugging WebSocket endpoint.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import json
11
+ import logging
12
+ import os
13
+ import platform
14
+ import tempfile
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from pydantic import BaseModel
19
+ from pydantic_ai import RunContext
20
+
21
+ from code_muse.messaging import emit_info, emit_success
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Module-level caches
27
+ # ---------------------------------------------------------------------------
28
+ _CHROME_WS: str | None = None
29
+ _PERSISTENT_SESSIONS: dict[str, CdpSession] = {}
30
+ _ACTIVE_TABS_CACHE: dict[str, str] = {} # prefix -> targetId
31
+ _PAGES_CACHE: list[dict[str, Any]] = []
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Pydantic models
35
+ # ---------------------------------------------------------------------------
36
+
37
+
38
+ class ChromeCdpResult(BaseModel):
39
+ """Result of a chrome_cdp command."""
40
+
41
+ success: bool = True
42
+ output: str = ""
43
+ screenshot_file: str = ""
44
+ page_list: list[dict[str, Any]] = []
45
+ error: str = ""
46
+ dpr: float = 1.0
47
+
48
+
49
+ class ChromeCdpInfo(BaseModel):
50
+ """Status/info about Chrome CDP availability."""
51
+
52
+ available: bool = False
53
+ ws_url: str = ""
54
+ version: str = ""
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Port / WS discovery
59
+ # ---------------------------------------------------------------------------
60
+
61
+
62
+ def _get_devtools_port() -> int:
63
+ """Read Chrome's DevToolsActivePort file from common locations."""
64
+ env_path = os.environ.get("CDP_PORT_FILE")
65
+ if env_path:
66
+ paths = [Path(env_path)]
67
+ else:
68
+ home = Path.home()
69
+ system = platform.system()
70
+ if system == "Darwin":
71
+ base = home / "Library/Application Support"
72
+ else:
73
+ base = home / ".config"
74
+
75
+ profiles = [
76
+ "Google/Chrome",
77
+ "google-chrome",
78
+ "BraveSoftware/Brave-Browser",
79
+ "brave-browser",
80
+ "microsoft-edge",
81
+ "chromium",
82
+ "vivaldi",
83
+ ]
84
+
85
+ paths: list[Path] = []
86
+ for profile in profiles:
87
+ paths.append(base / profile / "DevToolsActivePort")
88
+ # Flatpak / snap variants
89
+ flatpak_path = (
90
+ ".var/app/com.google.Chrome/config/google-chrome/DevToolsActivePort"
91
+ )
92
+ paths.append(home / flatpak_path)
93
+ paths.append(home / "snap/chromium/common/chromium/DevToolsActivePort")
94
+
95
+ for p in paths:
96
+ try:
97
+ text = p.read_text(encoding="utf-8").strip()
98
+ first_line = text.splitlines()[0]
99
+ return int(first_line)
100
+ except Exception:
101
+ continue
102
+
103
+ raise FileNotFoundError(
104
+ "Could not find Chrome DevToolsActivePort. "
105
+ "Enable remote debugging (chrome://inspect/#devices) or set CDP_PORT_FILE."
106
+ )
107
+
108
+
109
+ def _get_browser_ws_url(port: int) -> str:
110
+ """Fetch the browser WebSocket URL from Chrome's JSON/version endpoint."""
111
+ import urllib.request
112
+
113
+ url = f"http://localhost:{port}/json/version"
114
+ with urllib.request.urlopen(url, timeout=5) as resp: # noqa: S310
115
+ data = json.loads(resp.read().decode("utf-8"))
116
+ ws_url = data.get("webSocketDebuggerUrl", "")
117
+ if not ws_url:
118
+ raise RuntimeError("No webSocketDebuggerUrl in /json/version")
119
+ return ws_url
120
+
121
+
122
+ def _discover_browser_ws() -> str:
123
+ """Discover and cache the browser WebSocket URL."""
124
+ global _CHROME_WS
125
+ if _CHROME_WS:
126
+ return _CHROME_WS
127
+ port = _get_devtools_port()
128
+ ws_url = _get_browser_ws_url(port)
129
+ _CHROME_WS = ws_url
130
+ return ws_url
131
+
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # HTTP helpers for page listing
135
+ # ---------------------------------------------------------------------------
136
+
137
+
138
+ def _fetch_json_list(port: int) -> list[dict[str, Any]]:
139
+ """Fetch the list of open pages from Chrome's JSON/list endpoint."""
140
+ import urllib.request
141
+
142
+ url = f"http://localhost:{port}/json/list"
143
+ with urllib.request.urlopen(url, timeout=5) as resp: # noqa: S310
144
+ return json.loads(resp.read().decode("utf-8"))
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # WebSocket helpers
149
+ # ---------------------------------------------------------------------------
150
+
151
+
152
+ def _is_ws_closed(ws: Any) -> bool:
153
+ """Check whether a websockets connection is closed.
154
+ Compatible with both old and new websockets APIs.
155
+ """
156
+ if hasattr(ws, "closed"):
157
+ return bool(ws.closed)
158
+ import websockets
159
+
160
+ return ws.state == websockets.State.CLOSED
161
+
162
+
163
+ async def _ws_connect(ws_url: str) -> Any:
164
+ """Open a WebSocket connection to the given URL."""
165
+ try:
166
+ import websockets
167
+ except ImportError as exc:
168
+ raise RuntimeError(
169
+ "websockets library not installed. Run: pip install websockets"
170
+ ) from exc
171
+
172
+ return await websockets.connect(ws_url, open_timeout=5, close_timeout=5)
173
+
174
+
175
+ async def _send_cdp(
176
+ ws,
177
+ method: str,
178
+ params: dict[str, Any] | None = None,
179
+ session_id: str | None = None,
180
+ timeout: float = 10.0,
181
+ ) -> Any:
182
+ """Send a CDP command and wait for the matching response."""
183
+ msg_id = _next_msg_id()
184
+ payload: dict[str, Any] = {"id": msg_id, "method": method}
185
+ if params is not None:
186
+ payload["params"] = params
187
+ if session_id is not None:
188
+ payload["sessionId"] = session_id
189
+
190
+ future: asyncio.Future[Any] = asyncio.get_running_loop().create_future()
191
+ _PENDING[msg_id] = future
192
+
193
+ try:
194
+ await ws.send(json.dumps(payload))
195
+ msg = await asyncio.wait_for(future, timeout=timeout)
196
+ finally:
197
+ _PENDING.pop(msg_id, None)
198
+
199
+ if "error" in msg:
200
+ err_msg = msg["error"].get("message", str(msg["error"]))
201
+ raise RuntimeError(f"CDP error: {err_msg}")
202
+ return msg.get("result", {})
203
+
204
+
205
+ _MSG_COUNTER = 0
206
+ _PENDING: dict[int, asyncio.Future[Any]] = {}
207
+
208
+
209
+ def _next_msg_id() -> int:
210
+ global _MSG_COUNTER
211
+ _MSG_COUNTER += 1
212
+ return _MSG_COUNTER
213
+
214
+
215
+ async def _read_ws_loop(ws, event_buffers: dict[str, list] | None = None) -> None:
216
+ """Background task: reads WS messages, resolves futures, buffers events."""
217
+ import websockets
218
+
219
+ try:
220
+ async for raw in ws:
221
+ try:
222
+ msg = json.loads(raw)
223
+ except json.JSONDecodeError:
224
+ continue
225
+ msg_id = msg.get("id")
226
+ method = msg.get("method")
227
+ params = msg.get("params", {})
228
+
229
+ if msg_id is not None and msg_id in _PENDING:
230
+ future = _PENDING.pop(msg_id)
231
+ if not future.done():
232
+ future.set_result(msg)
233
+
234
+ # Route events to buffers (console logs, network events, etc.)
235
+ if method and event_buffers is not None:
236
+ if method in event_buffers:
237
+ event_buffers[method].append(params)
238
+ # Also store under a wildcard for generic "all events"
239
+ if "*" in event_buffers:
240
+ event_buffers["*"].append({"method": method, "params": params})
241
+ except websockets.exceptions.ConnectionClosed:
242
+ pass
243
+ except Exception:
244
+ logger.exception("chrome_cdp WS read loop error")
245
+ finally:
246
+ for fut in list(_PENDING.values()):
247
+ if not fut.done():
248
+ fut.cancel()
249
+ _PENDING.clear()
250
+
251
+
252
+ # ---------------------------------------------------------------------------
253
+ # CdpSession
254
+ # ---------------------------------------------------------------------------
255
+
256
+
257
+ class CdpSession:
258
+ """Persistent CDP session for a single target tab."""
259
+
260
+ def __init__(self, target_id: str):
261
+ self.target_id = target_id
262
+ self.ws: Any | None = None
263
+ self.session_id: str | None = None
264
+ self._read_task: asyncio.Task[Any] | None = None
265
+ # Event buffers for monitoring
266
+ self._event_buffers: dict[str, list[dict]] = {}
267
+ self._console_capturing = False
268
+ self._network_capturing = False
269
+
270
+ async def enable_console_capture(self) -> None:
271
+ """Start capturing console logs from the page."""
272
+ if self._console_capturing:
273
+ return
274
+ self._event_buffers["Runtime.consoleAPICalled"] = []
275
+ self._event_buffers["Runtime.exceptionThrown"] = []
276
+ await self.send("Runtime.enable")
277
+ self._console_capturing = True
278
+
279
+ async def get_console_logs(self) -> list[dict]:
280
+ """Get captured console logs and clear the buffer."""
281
+ logs = []
282
+ for entry in self._event_buffers.get("Runtime.consoleAPICalled", []):
283
+ args = entry.get("args", [])
284
+ text = " ".join(
285
+ a.get("value", str(a)) if isinstance(a, dict) else str(a) for a in args
286
+ )
287
+ level = entry.get("type", "log")
288
+ logs.append({"level": level, "text": text})
289
+ self._event_buffers["Runtime.consoleAPICalled"] = []
290
+
291
+ exceptions = []
292
+ for exc in self._event_buffers.get("Runtime.exceptionThrown", []):
293
+ details = exc.get("exceptionDetails", {})
294
+ text = details.get("text", "") or str(
295
+ details.get("exception", {}).get("description", "")
296
+ )
297
+ exceptions.append({"level": "exception", "text": text})
298
+ self._event_buffers["Runtime.exceptionThrown"] = []
299
+
300
+ return logs + exceptions
301
+
302
+ async def enable_network_capture(self) -> None:
303
+ """Start capturing network requests."""
304
+ if self._network_capturing:
305
+ return
306
+ self._event_buffers["Network.requestWillBeSent"] = []
307
+ self._event_buffers["Network.responseReceived"] = []
308
+ self._event_buffers["Network.loadingFailed"] = []
309
+ await self.send("Network.enable")
310
+ self._network_capturing = True
311
+
312
+ async def disable_network_capture(self) -> None:
313
+ """Stop capturing network requests."""
314
+ if not self._network_capturing:
315
+ return
316
+ import contextlib
317
+
318
+ with contextlib.suppress(Exception):
319
+ await self.send("Network.disable")
320
+ self._network_capturing = False
321
+
322
+ async def get_network_activity(self) -> list[dict]:
323
+ """Get captured network activity and clear the buffer."""
324
+ # Combine requests and responses by requestId
325
+ requests = {
326
+ r["requestId"]: {
327
+ "url": r.get("request", {}).get("url", ""),
328
+ "method": r.get("request", {}).get("method", ""),
329
+ "type": r.get("type", ""),
330
+ "timestamp": r.get("timestamp", 0),
331
+ }
332
+ for r in self._event_buffers.get("Network.requestWillBeSent", [])
333
+ }
334
+ responses = {
335
+ r["requestId"]: {
336
+ "status": r.get("response", {}).get("status", 0),
337
+ "status_text": r.get("response", {}).get("statusText", ""),
338
+ "content_type": r.get("response", {}).get("mimeType", ""),
339
+ "size": r.get("response", {}).get("transferSize", 0),
340
+ }
341
+ for r in self._event_buffers.get("Network.responseReceived", [])
342
+ }
343
+ failures = {
344
+ f["requestId"]: f.get("errorText", "Failed")
345
+ for f in self._event_buffers.get("Network.loadingFailed", [])
346
+ }
347
+
348
+ # Merge
349
+ all_ids = set(requests) | set(responses) | set(failures)
350
+ merged = []
351
+ for rid in all_ids:
352
+ entry = {"request_id": rid}
353
+ entry.update(requests.get(rid, {}))
354
+ entry.update(responses.get(rid, {}))
355
+ if rid in failures:
356
+ entry["error"] = failures[rid]
357
+ merged.append(entry)
358
+
359
+ # Clear buffers
360
+ for key in (
361
+ "Network.requestWillBeSent",
362
+ "Network.responseReceived",
363
+ "Network.loadingFailed",
364
+ ):
365
+ self._event_buffers[key] = []
366
+
367
+ return merged
368
+
369
+ async def get_source(self, url: str) -> str:
370
+ """Get the source code of a script or style by URL."""
371
+ # Try as script source first
372
+ try:
373
+ result = await self.send("Debugger.getScriptSource", {"scriptId": url})
374
+ return result.get("scriptSource", "")
375
+ except Exception:
376
+ pass
377
+ # Try getting source from page via eval (fetch the URL content)
378
+ result = await self.eval(f"fetch({json.dumps(url)}).then(r => r.text())")
379
+ return result
380
+
381
+ async def ensure_connected(self) -> None:
382
+ """Connect to the browser WS, attach to target, and store sessionId."""
383
+ if self.ws is not None and not _is_ws_closed(self.ws):
384
+ return
385
+
386
+ browser_ws = _discover_browser_ws()
387
+ self.ws = await _ws_connect(browser_ws)
388
+ self._read_task = asyncio.create_task(
389
+ _read_ws_loop(self.ws, self._event_buffers)
390
+ )
391
+
392
+ result = await _send_cdp(
393
+ self.ws,
394
+ "Target.attachToTarget",
395
+ {"targetId": self.target_id, "flatten": True},
396
+ )
397
+ self.session_id = result.get("sessionId")
398
+ if not self.session_id:
399
+ raise RuntimeError("No sessionId returned from attachToTarget")
400
+
401
+ async def send(
402
+ self,
403
+ method: str,
404
+ params: dict[str, Any] | None = None,
405
+ timeout: float = 10.0,
406
+ ) -> Any:
407
+ """Send a CDP command via the target session."""
408
+ if self.ws is None or _is_ws_closed(self.ws):
409
+ await self.ensure_connected()
410
+ return await _send_cdp(
411
+ self.ws, method, params, session_id=self.session_id, timeout=timeout
412
+ )
413
+
414
+ async def disconnect(self) -> None:
415
+ """Close the WebSocket and clean up."""
416
+ if self._read_task is not None:
417
+ self._read_task.cancel()
418
+ self._read_task = None
419
+ if self.ws is not None:
420
+ import contextlib
421
+
422
+ with contextlib.suppress(Exception):
423
+ await self.ws.close()
424
+ self.ws = None
425
+ self.session_id = None
426
+
427
+ async def eval(self, expression: str) -> str:
428
+ """Evaluate a JS expression and return the string result."""
429
+ result = await self.send(
430
+ "Runtime.evaluate",
431
+ {"expression": expression, "returnByValue": True},
432
+ )
433
+ res = result.get("result", {})
434
+ if res.get("subtype") == "error":
435
+ desc = res.get("description", res.get("value", "JS error"))
436
+ raise RuntimeError(desc)
437
+ val = res.get("value")
438
+ return str(val) if val is not None else ""
439
+
440
+ async def get_screenshot(self) -> tuple[bytes, float]:
441
+ """Capture a screenshot and return (bytes, dpr)."""
442
+ result = await self.send("Page.captureScreenshot", {"format": "png"})
443
+ b64 = result.get("data", "")
444
+ if not b64:
445
+ raise RuntimeError("Screenshot data empty")
446
+ import base64
447
+
448
+ raw = base64.b64decode(b64)
449
+
450
+ # Try to fetch DPR
451
+ dpr = 1.0
452
+ try:
453
+ metrics = await self.send("Page.getLayoutMetrics")
454
+ dpr = metrics.get("visualViewport", {}).get("scale", 1.0)
455
+ except Exception:
456
+ pass
457
+ return raw, float(dpr)
458
+
459
+ async def get_snapshot(self) -> str:
460
+ """Fetch accessibility tree and return formatted text."""
461
+ result = await self.send("Accessibility.getFullAXTree")
462
+ nodes = result.get("nodes", [])
463
+ if not nodes:
464
+ return "(empty accessibility tree)"
465
+ lines: list[str] = []
466
+ for node in nodes:
467
+ role = node.get("role", {}).get("value", "")
468
+ name = node.get("name", {}).get("value", "")
469
+ if role or name:
470
+ lines.append(f"[{role}] {name}")
471
+ return "\n".join(lines) if lines else "(empty accessibility tree)"
472
+
473
+ async def navigate(self, url: str) -> None:
474
+ """Navigate to a URL and wait for load."""
475
+ # Enable Page events first
476
+ await self.send("Page.enable")
477
+ result = await self.send("Page.navigate", {"url": url})
478
+ frame_id = result.get("frameId")
479
+ if not frame_id:
480
+ raise RuntimeError("Navigation failed: no frameId")
481
+
482
+ # Wait for loadEventFired via event — but our simple loop only resolves
483
+ # responses, not events. Poll document.readyState instead.
484
+ for _ in range(50):
485
+ ready = await self.eval("document.readyState")
486
+ if ready == "complete":
487
+ return
488
+ await asyncio.sleep(0.2)
489
+ raise RuntimeError("Navigation timeout: page did not reach readyState=complete")
490
+
491
+ async def click_selector(self, selector: str) -> None:
492
+ """Click an element via JS."""
493
+ expression = (
494
+ "(function(){"
495
+ "var el=document.querySelector('" + selector.replace("'", "\\'") + "');"
496
+ "if(!el) throw new Error('"
497
+ "Element not found: " + selector.replace("'", "\\'") + "');"
498
+ "el.click();"
499
+ "return true;"
500
+ "})()"
501
+ )
502
+ await self.eval(expression)
503
+
504
+ async def click_xy(self, x: int, y: int) -> None:
505
+ """Dispatch a mouse click at (x, y)."""
506
+ await self.send(
507
+ "Input.dispatchMouseEvent",
508
+ {
509
+ "type": "mousePressed",
510
+ "x": x,
511
+ "y": y,
512
+ "button": "left",
513
+ "clickCount": 1,
514
+ },
515
+ )
516
+ await self.send(
517
+ "Input.dispatchMouseEvent",
518
+ {
519
+ "type": "mouseReleased",
520
+ "x": x,
521
+ "y": y,
522
+ "button": "left",
523
+ "clickCount": 1,
524
+ },
525
+ )
526
+
527
+ async def type_text(self, text: str) -> None:
528
+ """Insert text into the focused element."""
529
+ await self.send("Input.insertText", {"text": text})
530
+
531
+ async def get_html(self, selector: str | None = None) -> str:
532
+ """Get outerHTML of the page or a matched element."""
533
+ if selector:
534
+ expr = (
535
+ "(function(){"
536
+ "var el=document.querySelector('" + selector.replace("'", "\\'") + "');"
537
+ "if(!el) throw new Error('"
538
+ "Element not found: " + selector.replace("'", "\\'") + "');"
539
+ "return el.outerHTML;"
540
+ "})()"
541
+ )
542
+ return await self.eval(expr)
543
+ return await self.eval("document.documentElement.outerHTML")
544
+
545
+ async def get_network_entries(self) -> list[dict[str, Any]]:
546
+ """Return performance resource timing entries."""
547
+ raw = await self.eval(
548
+ "JSON.stringify(performance.getEntriesByType('resource').map(e=>{"
549
+ "return {name:e.name,initiatorType:e.initiatorType,duration:e.duration};"
550
+ "}))"
551
+ )
552
+ try:
553
+ return json.loads(raw)
554
+ except json.JSONDecodeError:
555
+ return []
556
+
557
+
558
+ # ---------------------------------------------------------------------------
559
+ # Page cache helpers
560
+ # ---------------------------------------------------------------------------
561
+
562
+
563
+ def _refresh_page_cache() -> list[dict[str, Any]]:
564
+ """Fetch all pages from Chrome and update caches."""
565
+ global _PAGES_CACHE, _ACTIVE_TABS_CACHE
566
+ port = _get_devtools_port()
567
+ pages = _fetch_json_list(port)
568
+ _PAGES_CACHE = [p for p in pages if p.get("type") == "page"]
569
+ _ACTIVE_TABS_CACHE.clear()
570
+ for idx, page in enumerate(_PAGES_CACHE, start=1):
571
+ target_id = page.get("id", "")
572
+ prefix = f"{idx}"
573
+ _ACTIVE_TABS_CACHE[prefix] = target_id
574
+ return _PAGES_CACHE
575
+
576
+
577
+ def _resolve_target(prefix: str) -> str:
578
+ """Resolve a short prefix to a full targetId."""
579
+ if prefix in _ACTIVE_TABS_CACHE:
580
+ return _ACTIVE_TABS_CACHE[prefix]
581
+ # Try refreshing
582
+ _refresh_page_cache()
583
+ if prefix in _ACTIVE_TABS_CACHE:
584
+ return _ACTIVE_TABS_CACHE[prefix]
585
+ raise ValueError(f"Unknown target prefix '{prefix}'. Run list first.")
586
+
587
+
588
+ def _get_or_create_session(target_id: str) -> CdpSession:
589
+ """Return an existing CdpSession or create a new one."""
590
+ if target_id not in _PERSISTENT_SESSIONS:
591
+ _PERSISTENT_SESSIONS[target_id] = CdpSession(target_id)
592
+ return _PERSISTENT_SESSIONS[target_id]
593
+
594
+
595
+ # ---------------------------------------------------------------------------
596
+ # Tool registration
597
+ # ---------------------------------------------------------------------------
598
+
599
+
600
+ def register_chrome_cdp(agent):
601
+ """Register the chrome_cdp unified tool on the agent."""
602
+
603
+ @agent.tool
604
+ async def chrome_cdp(
605
+ context: RunContext,
606
+ command: str,
607
+ target: str = "",
608
+ expression: str = "",
609
+ selector: str = "",
610
+ url: str = "",
611
+ x: int = 0,
612
+ y: int = 0,
613
+ text: str = "",
614
+ ) -> ChromeCdpResult | ChromeCdpInfo | dict[str, Any]:
615
+ """Inspect, debug, and interact with web pages via Chrome DevTools Protocol.
616
+
617
+ Connects to your running Chrome browser — like having the DevTools
618
+ inspector programmatically. Use it to see what's happening on any page,
619
+ debug rendering issues, catch console errors, monitor network requests,
620
+ and interact with the page.
621
+
622
+ ─── QUICK START ───
623
+
624
+ 1. Check Chrome: chrome_cdp(command="status")
625
+ 2. List tabs: chrome_cdp(command="list")
626
+ 3. Snapshot: chrome_cdp(command="snap", target="1")
627
+ 4. Screenshot: chrome_cdp(command="shot", target="1")
628
+ 5. Console: chrome_cdp(command="console", target="1")
629
+ 6. Evaluate JS: chrome_cdp(command="eval", target="1", expression="document.title")
630
+
631
+ ─── FULL CAPABILITY GUIDE ───
632
+
633
+ Call `chrome_cdp(command="guide")` at any time to see the complete
634
+ capability catalog with examples. Or read the command breakdown below.
635
+
636
+ 📄 PAGE INSPECTION
637
+ list / ls → List all open tabs with numeric prefixes
638
+ shot / screenshot → Save a PNG screenshot; returns file path + DPR
639
+ snap / snapshot → Accessibility tree (how screen readers see the page)
640
+ html → Get full page HTML or a specific element's outerHTML
641
+ source → Get JS/CSS source code by URL or fetch-page path
642
+
643
+ 💻 JAVASCRIPT RUNTIME
644
+ eval → Execute any JavaScript: read state, call functions,
645
+ inspect localStorage, cookies, component props, etc.
646
+ Example: eval "JSON.stringify(window.__INITIAL_STATE__)"
647
+ Example: eval "document.querySelector('h1').textContent"
648
+ console / logs → Capture console.log/warn/error/info output from the page.
649
+ Also catches unhandled JS exceptions. Use AFTER page
650
+ interactions to see if anything broke.
651
+
652
+ 🌐 NETWORK
653
+ net / network → Performance API resource entries (duration, size, type)
654
+ network_start → Begin REAL-TIME capture of all HTTP requests/responses
655
+ network_captured→ Show captured requests with method, status, URL, size, errors
656
+ network_stop → Stop network capture
657
+
658
+ 🖱️ PAGE INTERACTION
659
+ nav / navigate → Navigate to a URL and wait for page load
660
+ click → Click an element by CSS selector
661
+ clickxy → Click at CSS pixel coordinates (x, y)
662
+ type → Type text into the focused element (works in cross-origin iframes!)
663
+
664
+ 🔧 DEBUGGING
665
+ status → Check if Chrome remote debugging is available
666
+ guide → Show this complete capability guide with examples
667
+
668
+ ─── ADVANCED WORKFLOWS ───
669
+
670
+ Debug a dev server:
671
+ chrome_cdp(command="nav", target="1", url="http://localhost:5173")
672
+ chrome_cdp(command="shot", target="1")
673
+ chrome_cdp(command="console", target="1")
674
+
675
+ Debug a failed API call:
676
+ chrome_cdp(command="network_start", target="1")
677
+ chrome_cdp(command="click", target="1", selector="button[type=submit]")
678
+ chrome_cdp(command="network_captured", target="1")
679
+ chrome_cdp(command="network_stop", target="1")
680
+
681
+ Check page state:
682
+ chrome_cdp(command="eval", target="1", expression="navigator.userAgent")
683
+ chrome_cdp(command="eval", target="1", expression="JSON.stringify(localStorage)")
684
+ chrome_cdp(command="eval", target="1", expression="document.cookie")
685
+
686
+ Args:
687
+ command: What to do — see guide above for full list.
688
+ target: Tab prefix from `list` output (e.g. "1", "2").
689
+ expression: JavaScript expression for `eval`.
690
+ selector: CSS selector for `click` or scoped `html`.
691
+ url: URL for `navigate` or `source`.
692
+ x: X coordinate in CSS pixels for `clickxy`.
693
+ y: Y coordinate in CSS pixels for `clickxy`.
694
+ text: Text to type for `type`.
695
+
696
+ Returns:
697
+ ChromeCdpResult with success/error status and output.
698
+ """
699
+ cmd = command.lower().strip()
700
+
701
+ # --- status ---
702
+ if cmd in ("status",):
703
+ try:
704
+ ws_url = _discover_browser_ws()
705
+ import urllib.request
706
+
707
+ port = _get_devtools_port()
708
+ version_url = f"http://localhost:{port}/json/version"
709
+ with urllib.request.urlopen(version_url, timeout=3) as resp: # noqa: S310
710
+ data = json.loads(resp.read().decode("utf-8"))
711
+ version = data.get("Browser", "")
712
+ emit_info(f"chrome_cdp: Chrome available — {version}")
713
+ return ChromeCdpInfo(available=True, ws_url=ws_url, version=version)
714
+ except Exception as exc:
715
+ logger.exception("chrome_cdp status check failed")
716
+ return ChromeCdpInfo(
717
+ available=False, ws_url="", version=f"Not available: {exc}"
718
+ )
719
+
720
+ # --- guide ---
721
+ if cmd in ("guide", "help", "commands"):
722
+ guide_text = (
723
+ "╔══════════════════════════════════════════════════════════╗\n"
724
+ "║ chrome_cdp — Complete Capability Guide ║\n"
725
+ "╚══════════════════════════════════════════════════════════╝\n"
726
+ "\n"
727
+ "📄 PAGE INSPECTION\n"
728
+ ' chrome_cdp(command="list") — List open tabs\n'
729
+ ' chrome_cdp(command="shot", target="1") — Screenshot (temp file)\n'
730
+ ' chrome_cdp(command="snap", target="1") — Accessibility tree\n'
731
+ ' chrome_cdp(command="html", target="1") — Full page HTML\n'
732
+ ' chrome_cdp(command="html", target="1", selector="#main") — Element HTML\n'
733
+ ' chrome_cdp(command="source", target="1", url="https://...") — JS/CSS source\n'
734
+ "\n"
735
+ "💻 JAVASCRIPT RUNTIME\n"
736
+ ' chrome_cdp(command="eval", target="1", expression="document.title")\n'
737
+ ' chrome_cdp(command="eval", target="1", expression="JSON.stringify(localStorage)")\n'
738
+ ' chrome_cdp(command="eval", target="1", expression="document.cookie")\n'
739
+ ' chrome_cdp(command="eval", target="1", expression="navigator.userAgent")\n'
740
+ ' chrome_cdp(command="eval", target="1", expression="window.innerWidth") — viewport size\n'
741
+ ' chrome_cdp(command="console", target="1") — Capture console logs/errors\n'
742
+ "\n"
743
+ "🌐 NETWORK DIAGNOSTICS\n"
744
+ ' chrome_cdp(command="net", target="1") — Resource timing entries\n'
745
+ ' chrome_cdp(command="network_start", target="1") — Begin HTTP monitoring\n'
746
+ ' chrome_cdp(command="network_captured", target="1") — View captured requests\n'
747
+ ' chrome_cdp(command="network_stop", target="1") — Stop monitoring\n'
748
+ "\n"
749
+ "🖱️ PAGE INTERACTION\n"
750
+ ' chrome_cdp(command="nav", target="1", url="https://...") — Navigate\n'
751
+ ' chrome_cdp(command="click", target="1", selector="button") — Click element\n'
752
+ ' chrome_cdp(command="clickxy", target="1", x=100, y=200) — Click at coords\n'
753
+ ' chrome_cdp(command="type", target="1", text="hello") — Type text\n'
754
+ "\n"
755
+ "🔧 SYSTEM\n"
756
+ ' chrome_cdp(command="status") — Check Chrome availability\n'
757
+ ' chrome_cdp(command="guide") — Show this guide\n'
758
+ "\n"
759
+ "─── TYPICAL DEBUGGING WORKFLOW ───\n"
760
+ " # After starting a dev server:\n"
761
+ ' 1. chrome_cdp(command="status")\n'
762
+ ' 2. chrome_cdp(command="list")\n'
763
+ ' 3. chrome_cdp(command="nav", target="1", url="http://localhost:3000")\n'
764
+ ' 4. chrome_cdp(command="shot", target="1")\n'
765
+ ' 5. chrome_cdp(command="console", target="1")\n'
766
+ ' 6. chrome_cdp(command="snap", target="1")\n'
767
+ "\n"
768
+ " ─ or debug a failing API call ─\n"
769
+ ' 1. chrome_cdp(command="network_start", target="1")\n'
770
+ " 2. [interact with the page]\n"
771
+ ' 3. chrome_cdp(command="network_captured", target="1")\n'
772
+ ' 4. chrome_cdp(command="network_stop", target="1")\n'
773
+ "\n"
774
+ "─── TIPS ───\n"
775
+ " • The first connection to a tab may show 'Allow debugging?' — approve it once.\n"
776
+ " • Screenshots are saved to temp files; the path is returned in output.\n"
777
+ " • DPR (device pixel ratio) affects coordinates: CSS px = screenshot px / DPR.\n"
778
+ " • Use `console` AFTER interactions to catch post-action errors.\n"
779
+ " • Network capture starts fresh when you call network_start.\n"
780
+ )
781
+ return ChromeCdpResult(success=True, output=guide_text)
782
+
783
+ # --- list / ls ---
784
+ if cmd in ("list", "ls"):
785
+ try:
786
+ pages = _refresh_page_cache()
787
+ page_list: list[dict[str, Any]] = []
788
+ for idx, page in enumerate(pages, start=1):
789
+ title = page.get("title", "")
790
+ url = page.get("url", "")
791
+ target_id = page.get("id", "")
792
+ page_list.append(
793
+ {
794
+ "prefix": str(idx),
795
+ "title": title,
796
+ "url": url,
797
+ "target_id": target_id,
798
+ }
799
+ )
800
+ return ChromeCdpResult(
801
+ success=True,
802
+ output=f"{len(page_list)} tab(s) open",
803
+ page_list=page_list,
804
+ )
805
+ except Exception as exc:
806
+ logger.exception("chrome_cdp list failed")
807
+ return ChromeCdpResult(success=False, error=str(exc))
808
+
809
+ # All remaining commands need a target
810
+ if not target:
811
+ return ChromeCdpResult(
812
+ success=False,
813
+ error="Missing 'target' parameter. Run list first to get a prefix.",
814
+ )
815
+
816
+ try:
817
+ target_id = _resolve_target(target)
818
+ session = _get_or_create_session(target_id)
819
+ await session.ensure_connected()
820
+ except Exception as exc:
821
+ logger.exception("chrome_cdp connect failed for target=%s", target)
822
+ return ChromeCdpResult(success=False, error=str(exc))
823
+
824
+ # --- shot / screenshot ---
825
+ if cmd in ("shot", "screenshot"):
826
+ try:
827
+ screenshot_bytes, dpr = await session.get_screenshot()
828
+ fd, path = tempfile.mkstemp(prefix="chrome_cdp_shot_", suffix=".png")
829
+ with open(fd, "wb") as f:
830
+ f.write(screenshot_bytes)
831
+ emit_success(f"chrome_cdp screenshot saved: {path}")
832
+ return ChromeCdpResult(
833
+ success=True,
834
+ output=f"Screenshot saved to {path} (DPR={dpr})",
835
+ screenshot_file=path,
836
+ dpr=dpr,
837
+ )
838
+ except Exception as exc:
839
+ logger.exception("chrome_cdp screenshot failed")
840
+ return ChromeCdpResult(success=False, error=str(exc))
841
+
842
+ # --- snap / snapshot ---
843
+ if cmd in ("snap", "snapshot"):
844
+ try:
845
+ tree = await session.get_snapshot()
846
+ return ChromeCdpResult(success=True, output=tree)
847
+ except Exception as exc:
848
+ logger.exception("chrome_cdp snapshot failed")
849
+ return ChromeCdpResult(success=False, error=str(exc))
850
+
851
+ # --- eval ---
852
+ if cmd in ("eval",):
853
+ if not expression:
854
+ return ChromeCdpResult(
855
+ success=False, error="Missing 'expression' for eval command."
856
+ )
857
+ try:
858
+ result = await session.eval(expression)
859
+ return ChromeCdpResult(success=True, output=result)
860
+ except Exception as exc:
861
+ logger.exception("chrome_cdp eval failed")
862
+ return ChromeCdpResult(success=False, error=str(exc))
863
+
864
+ # --- html ---
865
+ if cmd in ("html",):
866
+ try:
867
+ html = await session.get_html(selector if selector else None)
868
+ # Truncate very large HTML
869
+ if len(html) > 100_000:
870
+ html = html[:100_000] + "\n... (truncated)"
871
+ return ChromeCdpResult(success=True, output=html)
872
+ except Exception as exc:
873
+ logger.exception("chrome_cdp html failed")
874
+ return ChromeCdpResult(success=False, error=str(exc))
875
+
876
+ # --- nav / navigate ---
877
+ if cmd in ("nav", "navigate"):
878
+ if not url:
879
+ return ChromeCdpResult(
880
+ success=False, error="Missing 'url' for navigate command."
881
+ )
882
+ try:
883
+ await session.navigate(url)
884
+ return ChromeCdpResult(success=True, output=f"Navigated to {url}")
885
+ except Exception as exc:
886
+ logger.exception("chrome_cdp navigate failed")
887
+ return ChromeCdpResult(success=False, error=str(exc))
888
+
889
+ # --- net / network ---
890
+ if cmd in ("net", "network"):
891
+ try:
892
+ entries = await session.get_network_entries()
893
+ lines = [
894
+ f"{e.get('name', '')} [{e.get('initiatorType', '')}]"
895
+ f" {e.get('duration', 0):.1f}ms"
896
+ for e in entries
897
+ ]
898
+ return ChromeCdpResult(
899
+ success=True,
900
+ output=f"{len(entries)} resource(s)\n" + "\n".join(lines[:200]),
901
+ )
902
+ except Exception as exc:
903
+ logger.exception("chrome_cdp network failed")
904
+ return ChromeCdpResult(success=False, error=str(exc))
905
+
906
+ # --- console ---
907
+ if cmd in ("console", "logs"):
908
+ try:
909
+ await session.enable_console_capture()
910
+ # Wait a moment for any existing console events
911
+ await asyncio.sleep(0.5)
912
+ logs = await session.get_console_logs()
913
+ if not logs:
914
+ return ChromeCdpResult(
915
+ success=True, output="(no console output captured)"
916
+ )
917
+ lines = []
918
+ for entry in logs:
919
+ level = entry.get("level", "log")
920
+ text = entry.get("text", "")
921
+ lines.append(f"[{level}] {text}")
922
+ return ChromeCdpResult(success=True, output="\n".join(lines))
923
+ except Exception as exc:
924
+ logger.exception("chrome_cdp console failed")
925
+ return ChromeCdpResult(success=False, error=str(exc))
926
+
927
+ # --- network_start ---
928
+ if cmd in ("network_start",):
929
+ try:
930
+ await session.enable_network_capture()
931
+ return ChromeCdpResult(success=True, output="Network capture started")
932
+ except Exception as exc:
933
+ logger.exception("chrome_cdp network_start failed")
934
+ return ChromeCdpResult(success=False, error=str(exc))
935
+
936
+ # --- network_stop ---
937
+ if cmd in ("network_stop",):
938
+ try:
939
+ await session.disable_network_capture()
940
+ return ChromeCdpResult(success=True, output="Network capture stopped")
941
+ except Exception as exc:
942
+ return ChromeCdpResult(success=False, error=str(exc))
943
+
944
+ # --- network_captured ---
945
+ if cmd in ("network_captured",):
946
+ try:
947
+ activity = await session.get_network_activity()
948
+ if not activity:
949
+ return ChromeCdpResult(
950
+ success=True, output="(no network activity captured)"
951
+ )
952
+ lines = []
953
+ for entry in activity[:100]: # cap at 100 entries
954
+ url = entry.get("url", "")[:80]
955
+ method = entry.get("method", "?")
956
+ status = entry.get("status", "?")
957
+ size = entry.get("size", "?")
958
+ error = entry.get("error", "")
959
+ if error:
960
+ lines.append(f"{method} {status} {size}B FAIL:{error} {url}")
961
+ else:
962
+ lines.append(f"{method} {status} {size}B {url}")
963
+ return ChromeCdpResult(success=True, output="\n".join(lines))
964
+ except Exception as exc:
965
+ logger.exception("chrome_cdp network_captured failed")
966
+ return ChromeCdpResult(success=False, error=str(exc))
967
+
968
+ # --- source ---
969
+ if cmd in ("source",):
970
+ if not url:
971
+ return ChromeCdpResult(
972
+ success=False,
973
+ error="Missing 'url' for source command. "
974
+ "Pass the script URL or src path.",
975
+ )
976
+ try:
977
+ src = await session.get_source(url)
978
+ return ChromeCdpResult(success=True, output=src)
979
+ except Exception as exc:
980
+ logger.exception("chrome_cdp source failed")
981
+ return ChromeCdpResult(success=False, error=str(exc))
982
+
983
+ # --- click ---
984
+ if cmd in ("click",):
985
+ if not selector:
986
+ return ChromeCdpResult(
987
+ success=False, error="Missing 'selector' for click command."
988
+ )
989
+ try:
990
+ await session.click_selector(selector)
991
+ return ChromeCdpResult(
992
+ success=True, output=f"Clicked element: {selector}"
993
+ )
994
+ except Exception as exc:
995
+ logger.exception("chrome_cdp click failed")
996
+ return ChromeCdpResult(success=False, error=str(exc))
997
+
998
+ # --- clickxy ---
999
+ if cmd in ("clickxy",):
1000
+ try:
1001
+ await session.click_xy(x, y)
1002
+ return ChromeCdpResult(success=True, output=f"Clicked at ({x}, {y})")
1003
+ except Exception as exc:
1004
+ logger.exception("chrome_cdp clickxy failed")
1005
+ return ChromeCdpResult(success=False, error=str(exc))
1006
+
1007
+ # --- type ---
1008
+ if cmd in ("type",):
1009
+ if not text:
1010
+ return ChromeCdpResult(
1011
+ success=False, error="Missing 'text' for type command."
1012
+ )
1013
+ try:
1014
+ await session.type_text(text)
1015
+ return ChromeCdpResult(
1016
+ success=True, output=f"Typed text ({len(text)} chars)"
1017
+ )
1018
+ except Exception as exc:
1019
+ logger.exception("chrome_cdp type failed")
1020
+ return ChromeCdpResult(success=False, error=str(exc))
1021
+
1022
+ # Unknown command
1023
+ return ChromeCdpResult(
1024
+ success=False,
1025
+ error=(
1026
+ f"Unknown command '{command}'. "
1027
+ "Use: status, list, shot, snap, eval, "
1028
+ "html, nav, net, console, network_start, network_captured, "
1029
+ "network_stop, source, click, clickxy, type."
1030
+ ),
1031
+ )
1032
+
1033
+ return chrome_cdp
1034
+
1035
+
1036
+ # ---------------------------------------------------------------------------
1037
+ # Cleanup
1038
+ # ---------------------------------------------------------------------------
1039
+
1040
+
1041
+ async def cleanup_all_sessions() -> None:
1042
+ """Disconnect all persistent CDP sessions. Called on shutdown."""
1043
+ global _CHROME_WS
1044
+ for session in list(_PERSISTENT_SESSIONS.values()):
1045
+ await session.disconnect()
1046
+ _PERSISTENT_SESSIONS.clear()
1047
+ _ACTIVE_TABS_CACHE.clear()
1048
+ _PAGES_CACHE.clear()
1049
+ _CHROME_WS = None
1050
+
1051
+
1052
+ def _sync_cleanup() -> None:
1053
+ """Synchronous cleanup wrapper for atexit/shutdown."""
1054
+ try:
1055
+ loop = asyncio.get_running_loop()
1056
+ loop.create_task(cleanup_all_sessions())
1057
+ except RuntimeError:
1058
+ pass # No running loop, connections will be cleaned on process exit
1059
+
1060
+
1061
+ # Register startup callback for availability check
1062
+ from code_muse.callbacks import register_callback # noqa: E402
1063
+ from code_muse.tools.chrome_cdp.register_callbacks import ( # noqa: E402
1064
+ register as _register_chrome_cdp_callbacks,
1065
+ )
1066
+
1067
+ _register_chrome_cdp_callbacks()
1068
+
1069
+ # Register shutdown cleanup
1070
+ register_callback("shutdown", _sync_cleanup)