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,410 @@
1
+ """Monkey patches for pydantic-ai.
2
+
3
+ This module contains all monkey patches needed to customize pydantic-ai behavior.
4
+ These patches MUST be applied before any other pydantic-ai imports to work correctly.
5
+
6
+ Usage:
7
+ from code_muse.pydantic_patches import apply_all_patches
8
+ apply_all_patches()
9
+ """
10
+
11
+ import contextlib
12
+ import importlib.metadata
13
+ from typing import Any
14
+
15
+
16
+ def _get_muse_version() -> str:
17
+ """Get the current Muse version."""
18
+ try:
19
+ return importlib.metadata.version("code-muse")
20
+ except Exception:
21
+ return "0.0.0-dev"
22
+
23
+
24
+ def patch_user_agent() -> None:
25
+ """Patch pydantic-ai's User-Agent to use Muse's version.
26
+
27
+ pydantic-ai sets its own User-Agent ('pydantic-ai/x.x.x') via a @cache-decorated
28
+ function. We replace it with a dynamic function that returns:
29
+ - 'KimiCLI/0.63' for Kimi models
30
+ - 'Muse/{version}' for all other models
31
+
32
+ This MUST be called before any pydantic-ai models are created.
33
+ """
34
+ try:
35
+ import pydantic_ai.models as pydantic_models
36
+
37
+ version = _get_muse_version()
38
+
39
+ # Clear cache if already called
40
+ if hasattr(pydantic_models.get_user_agent, "cache_clear"):
41
+ pydantic_models.get_user_agent.cache_clear()
42
+
43
+ def _get_dynamic_user_agent() -> str:
44
+ """Return User-Agent based on current model selection."""
45
+ try:
46
+ from code_muse.config import get_global_model_name
47
+
48
+ model_name = get_global_model_name()
49
+ if model_name and "kimi" in model_name.lower():
50
+ return "KimiCLI/0.63"
51
+ except Exception:
52
+ pass
53
+ return f"Muse/{version}"
54
+
55
+ pydantic_models.get_user_agent = _get_dynamic_user_agent
56
+ except Exception:
57
+ pass # Don't crash on patch failure
58
+
59
+
60
+ def patch_message_history_cleaning() -> None:
61
+ """Disable overly strict message history cleaning in pydantic-ai."""
62
+ try:
63
+ from pydantic_ai import _agent_graph
64
+
65
+ _agent_graph._clean_message_history = lambda messages: messages
66
+ except Exception:
67
+ pass
68
+
69
+
70
+ def patch_process_message_history() -> None:
71
+ """Patch _process_message_history to skip strict ModelRequest validation.
72
+
73
+ Pydantic AI added a validation that history must end with ModelRequest,
74
+ but this breaks valid conversation flows. We patch it to skip that validation.
75
+
76
+ In newer pydantic-ai versions the function was removed, so this becomes a no-op.
77
+ """
78
+ try:
79
+ from pydantic_ai import _agent_graph
80
+
81
+ if not hasattr(_agent_graph, "_process_message_history"):
82
+ return
83
+
84
+ async def _patched_process_message_history(messages, processors, run_context):
85
+ """Patched version that doesn't enforce ModelRequest at end."""
86
+ from pydantic_ai._agent_graph import (
87
+ cast,
88
+ exceptions,
89
+ is_async_callable,
90
+ is_takes_ctx,
91
+ run_in_executor,
92
+ )
93
+
94
+ for processor in processors:
95
+ takes_ctx = is_takes_ctx(processor)
96
+
97
+ if is_async_callable(processor):
98
+ if takes_ctx:
99
+ messages = await processor(run_context, messages)
100
+ else:
101
+ messages = await processor(messages)
102
+ else:
103
+ if takes_ctx:
104
+ sync_processor_with_ctx = cast(
105
+ "_HistoryProcessorSyncWithCtx", processor
106
+ )
107
+ messages = await run_in_executor(
108
+ sync_processor_with_ctx, run_context, messages
109
+ )
110
+ else:
111
+ sync_processor = cast("_HistoryProcessorSync", processor)
112
+ messages = await run_in_executor(sync_processor, messages)
113
+
114
+ if len(messages) == 0:
115
+ raise exceptions.UserError("Processed history cannot be empty.")
116
+
117
+ # NOTE: We intentionally skip the "must end with ModelRequest" validation
118
+ # that was added in newer Pydantic AI versions.
119
+
120
+ return messages
121
+
122
+ _agent_graph._process_message_history = _patched_process_message_history
123
+ except Exception:
124
+ pass
125
+
126
+
127
+ def patch_tool_call_json_repair() -> None:
128
+ """Patch pydantic-ai's _call_tool to auto-repair malformed JSON arguments.
129
+
130
+ LLMs sometimes produce slightly broken JSON in tool calls (trailing commas,
131
+ missing quotes, etc.). This patch intercepts tool calls and runs json_repair
132
+ on the arguments before validation, preventing unnecessary retries.
133
+ """
134
+ try:
135
+ import json_repair
136
+ from pydantic_ai._tool_manager import ToolManager
137
+
138
+ # Store the original method
139
+ _original_call_tool = ToolManager._call_tool
140
+
141
+ async def _patched_call_tool(
142
+ self,
143
+ call,
144
+ *,
145
+ allow_partial: bool,
146
+ wrap_validation_errors: bool,
147
+ approved: bool,
148
+ metadata: Any = None,
149
+ ):
150
+ """Patched _call_tool that repairs malformed JSON before validation."""
151
+ # Only attempt repair if args is a string (JSON)
152
+ if isinstance(call.args, str) and call.args:
153
+ try:
154
+ repaired = json_repair.repair_json(call.args)
155
+ if repaired != call.args:
156
+ # Update the call args with repaired JSON
157
+ call.args = repaired
158
+ except Exception:
159
+ pass # If repair fails, let original validation handle it
160
+
161
+ # Call the original method
162
+ return await _original_call_tool(
163
+ self,
164
+ call,
165
+ allow_partial=allow_partial,
166
+ wrap_validation_errors=wrap_validation_errors,
167
+ approved=approved,
168
+ metadata=metadata,
169
+ )
170
+
171
+ # Apply the patch
172
+ ToolManager._call_tool = _patched_call_tool
173
+
174
+ except ImportError:
175
+ pass # json_repair or pydantic_ai not available
176
+ except Exception:
177
+ pass # Don't crash on patch failure
178
+
179
+
180
+ def patch_tool_call_callbacks() -> None:
181
+ """Patch pydantic-ai tool handling to support callbacks and Claude Code tool names.
182
+
183
+ Claude Code OAuth prefixes tool names with ``cp_`` on the wire. pydantic-ai
184
+ classifies tool calls *before* ``_call_tool`` runs, so unprefixing only in
185
+ ``_call_tool`` is too late: prefixed tools get marked as ``unknown`` and can
186
+ burn through result retries, eventually raising ``UnexpectedModelBehavior``.
187
+
188
+ This patch normalizes Claude Code tool names early (during lookup/dispatch)
189
+ and wraps ``_call_tool`` so every tool invocation also triggers the
190
+ ``pre_tool_call`` and ``post_tool_call`` callbacks defined in
191
+ ``code_muse.callbacks``.
192
+ """
193
+ import time
194
+
195
+ try:
196
+ from pydantic_ai._tool_manager import ToolManager
197
+
198
+ _original_call_tool = ToolManager._call_tool
199
+ _original_get_tool_def = ToolManager.get_tool_def
200
+ _original_handle_call = ToolManager.handle_call
201
+
202
+ # Tool name prefix used by Claude Code OAuth - tools are prefixed on
203
+ # outgoing requests, so we need to unprefix them when they come back.
204
+ TOOL_PREFIX = "cp_"
205
+
206
+ def _normalize_tool_name(name: Any) -> Any:
207
+ """Strip the ``cp_`` prefix if present."""
208
+ if isinstance(name, str) and name.startswith(TOOL_PREFIX):
209
+ return name[len(TOOL_PREFIX) :]
210
+ return name
211
+
212
+ def _normalize_call_tool_name(call: Any) -> tuple[Any, Any]:
213
+ """Normalize the tool_name on a call object in-place."""
214
+ tool_name = getattr(call, "tool_name", None)
215
+ normalized_name = _normalize_tool_name(tool_name)
216
+ if normalized_name != tool_name:
217
+ with contextlib.suppress(AttributeError, TypeError):
218
+ call.tool_name = normalized_name
219
+ return normalized_name, call
220
+
221
+ # -- Early normalization patches -----------------------------------------
222
+ # These run *before* pydantic-ai classifies the tool as function/output/
223
+ # unknown, so prefixed names resolve correctly.
224
+
225
+ def _patched_get_tool_def(self, name: str):
226
+ return _original_get_tool_def(self, _normalize_tool_name(name))
227
+
228
+ async def _patched_handle_call(
229
+ self,
230
+ call,
231
+ allow_partial: bool = False,
232
+ wrap_validation_errors: bool = True,
233
+ *,
234
+ approved: bool = False,
235
+ metadata: Any = None,
236
+ ):
237
+ _normalize_call_tool_name(call)
238
+ return await _original_handle_call(
239
+ self,
240
+ call,
241
+ allow_partial=allow_partial,
242
+ wrap_validation_errors=wrap_validation_errors,
243
+ approved=approved,
244
+ metadata=metadata,
245
+ )
246
+
247
+ # -- _call_tool wrapper with callbacks -----------------------------------
248
+
249
+ async def _patched_call_tool(
250
+ self,
251
+ call,
252
+ *,
253
+ allow_partial: bool,
254
+ wrap_validation_errors: bool,
255
+ approved: bool,
256
+ metadata: Any = None,
257
+ ):
258
+ tool_name, call = _normalize_call_tool_name(call)
259
+
260
+ # Normalise args to a dict for the callback contract
261
+ tool_args: dict = {}
262
+ if isinstance(call.args, dict):
263
+ tool_args = call.args
264
+ elif isinstance(call.args, str):
265
+ try:
266
+ import json
267
+
268
+ tool_args = json.loads(call.args)
269
+ except Exception:
270
+ tool_args = {"raw": call.args}
271
+
272
+ # --- pre_tool_call (with blocking support) ---
273
+ # Returns a string tool-result on block so pydantic-ai sees a clean
274
+ # "BLOCKED: ..." message and the agent can react gracefully, without
275
+ # triggering UnexpectedModelBehavior crashes.
276
+ try:
277
+ from code_muse import callbacks
278
+ from code_muse.messaging import emit_warning
279
+
280
+ callback_results = await callbacks.on_pre_tool_call(
281
+ tool_name, tool_args
282
+ )
283
+
284
+ for callback_result in callback_results:
285
+ if (
286
+ callback_result
287
+ and isinstance(callback_result, dict)
288
+ and callback_result.get("blocked")
289
+ ):
290
+ raw_reason = (
291
+ callback_result.get("error_message")
292
+ or callback_result.get("reason")
293
+ or ""
294
+ )
295
+ if "[BLOCKED]" in raw_reason:
296
+ clean_reason = raw_reason[
297
+ raw_reason.index("[BLOCKED]") :
298
+ ].strip()
299
+ else:
300
+ clean_reason = (
301
+ raw_reason.strip() or "Tool execution blocked by hook"
302
+ )
303
+ block_msg = f"🚫 Hook blocked this tool call: {clean_reason}"
304
+ emit_warning(block_msg)
305
+ return f"ERROR: {block_msg}\n\nThe hook policy prevented this tool from running. Please inform the user and do not retry this specific command."
306
+ except Exception:
307
+ pass # other errors don't block tool execution
308
+
309
+ start = time.perf_counter()
310
+ error: Exception | None = None
311
+ result = None
312
+ try:
313
+ result = await _original_call_tool(
314
+ self,
315
+ call,
316
+ allow_partial=allow_partial,
317
+ wrap_validation_errors=wrap_validation_errors,
318
+ approved=approved,
319
+ metadata=metadata,
320
+ )
321
+ return result
322
+ except Exception as exc:
323
+ error = exc
324
+ raise
325
+ finally:
326
+ duration_ms = (time.perf_counter() - start) * 1000
327
+ final_result = result if error is None else {"error": str(error)}
328
+ try:
329
+ from code_muse import callbacks
330
+
331
+ await callbacks.on_post_tool_call(
332
+ tool_name, tool_args, final_result, duration_ms
333
+ )
334
+ except Exception:
335
+ pass # never block tool execution
336
+
337
+ ToolManager.get_tool_def = _patched_get_tool_def
338
+ ToolManager.handle_call = _patched_handle_call
339
+ ToolManager._call_tool = _patched_call_tool
340
+
341
+ except ImportError:
342
+ pass
343
+ except Exception:
344
+ pass
345
+
346
+
347
+ def patch_prompt_toolkit_emoji_width() -> None:
348
+ """Patch prompt_toolkit's character width calculation for emojis.
349
+
350
+ Modern terminals render most emojis as 2 cells wide, but wcwidth often
351
+ returns 1 for many emoji codepoints. This causes cursor misalignment.
352
+
353
+ This patch:
354
+ 1. Returns 0 for variation selectors (zero-width modifiers)
355
+ 2. Returns 2 for emoji codepoints (terminals render them wide)
356
+ 3. Falls back to wcwidth for non-emoji characters
357
+ """
358
+ try:
359
+ import wcwidth
360
+ from prompt_toolkit import utils as pt_utils
361
+
362
+ _original_get_cwidth = pt_utils.get_cwidth
363
+
364
+ def _patched_get_cwidth(char: str) -> int:
365
+ """Get character width with better emoji support."""
366
+ code = ord(char)
367
+
368
+ # Variation selectors are zero-width
369
+ if 0xFE00 <= code <= 0xFE0F: # VS1-VS16
370
+ return 0
371
+
372
+ # Emoji codepoints - terminals render these as 2 cells wide
373
+ # even when wcwidth says 1
374
+ if (
375
+ 0x1F300 <= code <= 0x1F9FF # Misc Symbols/Pictographs, Emoticons
376
+ or 0x1F600 <= code <= 0x1F64F # Emoticons
377
+ or 0x1F680 <= code <= 0x1F6FF # Transport/Map symbols
378
+ or 0x1FA00 <= code <= 0x1FAFF # Symbols/Pictographs Extended-A
379
+ or 0x2600 <= code <= 0x26FF # Misc Symbols (☀️, ⚡, etc)
380
+ or 0x2700 <= code <= 0x27BF # Dingbats (✂️, ✈️, etc)
381
+ or 0x1F1E0 <= code <= 0x1F1FF # Regional indicators (flags)
382
+ ):
383
+ return 2
384
+
385
+ # Use wcwidth for non-emoji
386
+ w = wcwidth.wcwidth(char)
387
+ if w >= 0:
388
+ return w
389
+
390
+ return _original_get_cwidth(char)
391
+
392
+ pt_utils.get_cwidth = _patched_get_cwidth
393
+
394
+ except ImportError:
395
+ pass # wcwidth or prompt_toolkit not available
396
+ except Exception:
397
+ pass # Don't crash on patch failure
398
+
399
+
400
+ def apply_all_patches() -> None:
401
+ """Apply all pydantic-ai monkey patches.
402
+
403
+ Call this at the very top of main.py, before any other imports.
404
+ """
405
+ patch_user_agent()
406
+ patch_message_history_cleaning()
407
+ patch_process_message_history()
408
+ patch_tool_call_json_repair()
409
+ patch_tool_call_callbacks()
410
+ patch_prompt_toolkit_emoji_width()
@@ -0,0 +1,233 @@
1
+ """
2
+ ReopenableAsyncClient - A reopenable httpx.AsyncClient wrapper.
3
+
4
+ This module provides a ReopenableAsyncClient class that extends httpx.AsyncClient
5
+ to support reopening after being closed, which the standard httpx.AsyncClient
6
+ doesn't support.
7
+ """
8
+
9
+ import asyncio
10
+ import threading
11
+
12
+ import httpx
13
+
14
+
15
+ class ReopenableAsyncClient:
16
+ """
17
+ A wrapper around httpx.AsyncClient that can be reopened after being closed.
18
+
19
+ Standard httpx.AsyncClient becomes unusable after calling aclose().
20
+ This class allows you to reopen the client and continue using it.
21
+
22
+ Example:
23
+ >>> client = ReopenableAsyncClient(timeout=30.0)
24
+ >>> await client.get("https://httpbin.org/get")
25
+ >>> await client.aclose()
26
+ >>> # Client is now closed, but can be reopened
27
+ >>> await client.reopen()
28
+ >>> await client.get("https://httpbin.org/get") # Works!
29
+
30
+ The client preserves all original configuration when reopening.
31
+ """
32
+
33
+ class _StreamWrapper:
34
+ """Async context manager wrapper for streaming responses."""
35
+
36
+ def __init__(
37
+ self,
38
+ parent_client: ReopenableAsyncClient,
39
+ method: str,
40
+ url: str | httpx.URL,
41
+ **kwargs,
42
+ ):
43
+ self.parent_client = parent_client
44
+ self.method = method
45
+ self.url = url
46
+ self.kwargs = kwargs
47
+ self._stream_context = None
48
+
49
+ async def __aenter__(self):
50
+ client = await self.parent_client._ensure_client_open()
51
+ self._stream_context = client.stream(self.method, self.url, **self.kwargs)
52
+ return await self._stream_context.__aenter__()
53
+
54
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
55
+ if self._stream_context:
56
+ return await self._stream_context.__aexit__(exc_type, exc_val, exc_tb)
57
+
58
+ def __init__(self, client_class=None, **kwargs):
59
+ """
60
+ Initialize the ReopenableAsyncClient.
61
+
62
+ Args:
63
+ client_class: Class to use for creating the internal client (defaults to httpx.AsyncClient)
64
+ **kwargs: All arguments that would be passed to the client constructor
65
+ """
66
+ self._client_class = client_class or httpx.AsyncClient
67
+ self._client_kwargs = kwargs.copy()
68
+ self._client: httpx.AsyncClient | None = None
69
+ self._is_closed = True
70
+ self._lock = asyncio.Lock()
71
+ # FREE-THREADED: _sync_lock guards build_request() which is a sync method
72
+ # that creates a temporary sync httpx.Client. It must remain threading.Lock.
73
+ self._sync_lock = threading.Lock()
74
+
75
+ async def _ensure_client_open(self) -> httpx.AsyncClient:
76
+ """
77
+ Ensure the underlying client is open and ready to use.
78
+
79
+ Returns:
80
+ The active client instance
81
+
82
+ Raises:
83
+ RuntimeError: If client cannot be opened
84
+ """
85
+ async with self._lock:
86
+ if self._is_closed or self._client is None:
87
+ await self._create_client()
88
+ return self._client
89
+
90
+ async def _create_client(self) -> None:
91
+ """Create a new client with the stored configuration."""
92
+ if self._client is not None and not self._is_closed:
93
+ # Close existing client first
94
+ await self._client.aclose()
95
+
96
+ self._client = self._client_class(**self._client_kwargs)
97
+ self._is_closed = False
98
+
99
+ async def reopen(self) -> None:
100
+ """
101
+ Explicitly reopen the client after it has been closed.
102
+
103
+ This is useful when you want to reuse a client that was previously closed.
104
+ """
105
+ async with self._lock:
106
+ await self._create_client()
107
+
108
+ async def aclose(self) -> None:
109
+ """
110
+ Close the underlying httpx.AsyncClient.
111
+
112
+ After calling this, the client can be reopened using reopen() or
113
+ automatically when making the next request.
114
+ """
115
+ async with self._lock:
116
+ if self._client is not None and not self._is_closed:
117
+ await self._client.aclose()
118
+ self._is_closed = True
119
+
120
+ @property
121
+ def is_closed(self) -> bool:
122
+ """Check if the client is currently closed."""
123
+ return self._is_closed or self._client is None
124
+
125
+ # Delegate all httpx.AsyncClient methods to the underlying client
126
+
127
+ async def get(self, url: str | httpx.URL, **kwargs) -> httpx.Response:
128
+ """Make a GET request."""
129
+ client = await self._ensure_client_open()
130
+ return await client.get(url, **kwargs)
131
+
132
+ async def post(self, url: str | httpx.URL, **kwargs) -> httpx.Response:
133
+ """Make a POST request."""
134
+ client = await self._ensure_client_open()
135
+ return await client.post(url, **kwargs)
136
+
137
+ async def put(self, url: str | httpx.URL, **kwargs) -> httpx.Response:
138
+ """Make a PUT request."""
139
+ client = await self._ensure_client_open()
140
+ return await client.put(url, **kwargs)
141
+
142
+ async def patch(self, url: str | httpx.URL, **kwargs) -> httpx.Response:
143
+ """Make a PATCH request."""
144
+ client = await self._ensure_client_open()
145
+ return await client.patch(url, **kwargs)
146
+
147
+ async def delete(self, url: str | httpx.URL, **kwargs) -> httpx.Response:
148
+ """Make a DELETE request."""
149
+ client = await self._ensure_client_open()
150
+ return await client.delete(url, **kwargs)
151
+
152
+ async def head(self, url: str | httpx.URL, **kwargs) -> httpx.Response:
153
+ """Make a HEAD request."""
154
+ client = await self._ensure_client_open()
155
+ return await client.head(url, **kwargs)
156
+
157
+ async def options(self, url: str | httpx.URL, **kwargs) -> httpx.Response:
158
+ """Make an OPTIONS request."""
159
+ client = await self._ensure_client_open()
160
+ return await client.options(url, **kwargs)
161
+
162
+ async def request(
163
+ self, method: str, url: str | httpx.URL, **kwargs
164
+ ) -> httpx.Response:
165
+ """Make a request with the specified HTTP method."""
166
+ client = await self._ensure_client_open()
167
+ return await client.request(method, url, **kwargs)
168
+
169
+ async def send(self, request: httpx.Request, **kwargs) -> httpx.Response:
170
+ """Send a pre-built request."""
171
+ client = await self._ensure_client_open()
172
+ return await client.send(request, **kwargs)
173
+
174
+ def build_request(
175
+ self, method: str, url: str | httpx.URL, **kwargs
176
+ ) -> httpx.Request:
177
+ """
178
+ Build a request without sending it.
179
+
180
+ Note: This creates a temporary client if none exists, but doesn't keep it open.
181
+ """
182
+ with self._sync_lock:
183
+ if self._client is None or self._is_closed:
184
+ # Create temporary sync client for building request only
185
+ # Use httpx.Client (sync) so we can properly close it
186
+ temp_client = httpx.Client(**self._client_kwargs)
187
+ try:
188
+ return temp_client.build_request(method, url, **kwargs)
189
+ finally:
190
+ temp_client.close()
191
+ return self._client.build_request(method, url, **kwargs)
192
+
193
+ def stream(self, method: str, url: str | httpx.URL, **kwargs):
194
+ """Stream a request. Returns an async context manager."""
195
+ return self._StreamWrapper(self, method, url, **kwargs)
196
+
197
+ # Context manager support
198
+ async def __aenter__(self):
199
+ """Async context manager entry."""
200
+ await self._ensure_client_open()
201
+ return self
202
+
203
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
204
+ """Async context manager exit."""
205
+ await self.aclose()
206
+
207
+ # Properties that don't require an active client
208
+ @property
209
+ def timeout(self) -> httpx.Timeout | None:
210
+ """Get the configured timeout."""
211
+ return self._client_kwargs.get("timeout")
212
+
213
+ @property
214
+ def headers(self) -> httpx.Headers:
215
+ """Get the configured headers."""
216
+ if self._client is not None:
217
+ return self._client.headers
218
+ # Return headers from kwargs if client doesn't exist
219
+ headers = self._client_kwargs.get("headers", {})
220
+ return httpx.Headers(headers)
221
+
222
+ @property
223
+ def cookies(self) -> httpx.Cookies:
224
+ """Get the current cookies."""
225
+ if self._client is not None and not self._is_closed:
226
+ return self._client.cookies
227
+ # Return empty cookies if client doesn't exist or is closed
228
+ return httpx.Cookies()
229
+
230
+ def __repr__(self) -> str:
231
+ """String representation of the client."""
232
+ status = "closed" if self.is_closed else "open"
233
+ return f"<ReopenableAsyncClient [{status}]>"