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,853 @@
1
+ """Cache helpers for Claude Code / Anthropic.
2
+
3
+ ClaudeCacheAsyncClient: httpx client that tries to patch /v1/messages bodies.
4
+
5
+ We now also expose `patch_anthropic_client_messages` which monkey-patches
6
+ AsyncAnthropic.messages.create() so we can inject cache_control BEFORE
7
+ serialization, avoiding httpx/Pydantic internals.
8
+
9
+ This module also handles:
10
+ - Tool name prefixing/unprefixing for Claude Code OAuth compatibility
11
+ - Header transformations (anthropic-beta, user-agent)
12
+ - URL modifications (adding ?beta=true query param)
13
+ """
14
+
15
+ import asyncio
16
+ import base64
17
+ import json
18
+ import logging
19
+ import time
20
+ from collections.abc import Callable, MutableMapping
21
+ from typing import Any
22
+ from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
23
+
24
+ import httpx
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Refresh token if it's older than the configured max age (seconds)
29
+ TOKEN_MAX_AGE_SECONDS = 3600
30
+
31
+ # Retry configuration
32
+ RETRY_STATUS_CODES = (429, 500, 502, 503, 504)
33
+ MAX_RETRIES = 5
34
+
35
+ # Tool name prefix for Claude Code OAuth compatibility
36
+ # Tools are prefixed on outgoing requests and unprefixed on incoming responses
37
+ TOOL_PREFIX = "cp_"
38
+
39
+ # User-Agent to send with Claude Code OAuth requests
40
+ CLAUDE_CLI_USER_AGENT = "claude-cli/2.1.2 (external, cli)"
41
+
42
+
43
+ def _model_requires_thinking_summary(model_name):
44
+ # Anthropic's Opus 4.7 family rejects adaptive-thinking requests unless a
45
+ # 'display: summary' field is present alongside 'type: adaptive'. We check
46
+ # both naming conventions (opus-4-7 and 4-7-opus).
47
+ if not model_name:
48
+ return False
49
+ lower = model_name.lower()
50
+ return "opus-4-7" in lower or "4-7-opus" in lower
51
+
52
+
53
+ def _enforce_thinking_display_summary(payload):
54
+ # Belt-and-suspenders wire-level enforcement of thinking.display='summary'
55
+ # for Opus 4.7 payloads. Mutates payload in place; returns True if a
56
+ # change was made. No-ops on non-matching models or payloads without a
57
+ # thinking dict.
58
+ if not isinstance(payload, dict):
59
+ return False
60
+ if not _model_requires_thinking_summary(payload.get("model")):
61
+ return False
62
+ thinking = payload.get("thinking")
63
+ if not isinstance(thinking, dict):
64
+ return False
65
+ if thinking.get("display") == "summarized":
66
+ return False
67
+ thinking["display"] = "summarized"
68
+ return True
69
+
70
+
71
+ try:
72
+ from anthropic import AsyncAnthropic
73
+ except ImportError: # pragma: no cover - optional dep
74
+ AsyncAnthropic = None # type: ignore
75
+
76
+
77
+ class ClaudeCacheAsyncClient(httpx.AsyncClient):
78
+ """Async HTTP client with Claude Code OAuth transformations.
79
+
80
+ Handles:
81
+ - Cache control injection for prompt caching
82
+ - Tool name prefixing on outgoing requests
83
+ - Tool name unprefixing on incoming streaming responses
84
+ - Header transformations (anthropic-beta, user-agent)
85
+ - URL modifications (adding ?beta=true)
86
+ - Proactive token refresh
87
+ """
88
+
89
+ def __init__(
90
+ self,
91
+ *args: Any,
92
+ oauth_reauthentication_callback: Callable[[], str | None] | None = None,
93
+ token_update_callback: Callable[[str], None] | None = None,
94
+ **kwargs: Any,
95
+ ) -> None:
96
+ super().__init__(*args, **kwargs)
97
+ self._oauth_reauthentication_callback = oauth_reauthentication_callback
98
+ self._token_update_callback = token_update_callback
99
+
100
+ def set_token_update_callback(self, callback: Callable[[str], None] | None) -> None:
101
+ self._token_update_callback = callback
102
+
103
+ def _notify_token_recovered(self, access_token: str) -> None:
104
+ if not self._token_update_callback:
105
+ return
106
+ try:
107
+ self._token_update_callback(access_token)
108
+ except Exception as exc:
109
+ logger.debug("Token update callback failed: %s", exc)
110
+
111
+ def _get_jwt_age_seconds(self, token: str | None) -> float | None:
112
+ """Decode a JWT and return its age in seconds.
113
+
114
+ Returns None if the token can't be decoded or has no timestamp claims.
115
+ Uses 'iat' (issued at) if available, otherwise calculates from 'exp'.
116
+ """
117
+ if not token:
118
+ return None
119
+
120
+ try:
121
+ # JWT format: header.payload.signature
122
+ # We only need the payload (second part)
123
+ parts = token.split(".")
124
+ if len(parts) != 3:
125
+ return None
126
+
127
+ # Decode the payload (base64url encoded)
128
+ payload_b64 = parts[1]
129
+ # Add padding if needed (base64url doesn't require padding)
130
+ padding = 4 - len(payload_b64) % 4
131
+ if padding != 4:
132
+ payload_b64 += "=" * padding
133
+
134
+ payload_bytes = base64.urlsafe_b64decode(payload_b64)
135
+ payload = json.loads(payload_bytes.decode("utf-8"))
136
+
137
+ now = time.time()
138
+
139
+ # Prefer 'iat' (issued at) claim if available
140
+ if "iat" in payload:
141
+ iat = float(payload["iat"])
142
+ age = now - iat
143
+ return age
144
+
145
+ # Fall back to calculating from 'exp' claim
146
+ # Assume tokens are typically valid for TOKEN_MAX_AGE_SECONDS
147
+ if "exp" in payload:
148
+ exp = float(payload["exp"])
149
+ # If exp is in the future, calculate how long until expiry
150
+ # and assume the token was issued TOKEN_MAX_AGE_SECONDS before expiry
151
+ time_until_exp = exp - now
152
+ # If token has less than TOKEN_MAX_AGE_SECONDS left, it's "old"
153
+ age = TOKEN_MAX_AGE_SECONDS - time_until_exp
154
+ return max(0, age)
155
+
156
+ return None
157
+ except Exception as exc:
158
+ logger.debug("Failed to decode JWT age: %s", exc)
159
+ return None
160
+
161
+ def _extract_bearer_token(self, request: httpx.Request) -> str | None:
162
+ """Extract the bearer token from request headers."""
163
+ auth_header = request.headers.get("Authorization") or request.headers.get(
164
+ "authorization"
165
+ )
166
+ if auth_header and auth_header.lower().startswith("bearer "):
167
+ return auth_header[7:] # Strip "Bearer " prefix
168
+ return None
169
+
170
+ def _should_refresh_token(self, request: httpx.Request) -> bool:
171
+ """Check if the token should be refreshed (within the max-age window).
172
+
173
+ Uses two strategies:
174
+ 1. Decode JWT to check token age (if possible)
175
+ 2. Fall back to stored expires_at from token file
176
+
177
+ Returns True if token expires within TOKEN_MAX_AGE_SECONDS.
178
+ """
179
+ token = self._extract_bearer_token(request)
180
+ if not token:
181
+ return False
182
+
183
+ # Strategy 1: Try to decode JWT age
184
+ age = self._get_jwt_age_seconds(token)
185
+ if age is not None:
186
+ should_refresh = age >= TOKEN_MAX_AGE_SECONDS
187
+ if should_refresh:
188
+ logger.info(
189
+ "JWT token is %.1f seconds old (>= %d), will refresh proactively",
190
+ age,
191
+ TOKEN_MAX_AGE_SECONDS,
192
+ )
193
+ return should_refresh
194
+
195
+ # Strategy 2: Fall back to stored expires_at from token file
196
+ should_refresh = self._check_stored_token_expiry()
197
+ if should_refresh:
198
+ logger.info(
199
+ "Stored token expires within %d seconds, will refresh proactively",
200
+ TOKEN_MAX_AGE_SECONDS,
201
+ )
202
+ return should_refresh
203
+
204
+ @staticmethod
205
+ def _check_stored_token_expiry() -> bool:
206
+ """Check if the stored token expires within TOKEN_MAX_AGE_SECONDS.
207
+
208
+ This is a fallback for when JWT decoding fails or isn't available.
209
+ Uses the expires_at timestamp from the stored token file.
210
+ """
211
+ try:
212
+ from code_muse.plugins.claude_code_oauth.utils import (
213
+ is_token_expired,
214
+ load_stored_tokens,
215
+ )
216
+
217
+ tokens = load_stored_tokens()
218
+ if not tokens:
219
+ return False
220
+
221
+ # is_token_expired already uses the configured refresh buffer window
222
+ return is_token_expired(tokens)
223
+ except Exception as exc:
224
+ logger.debug("Error checking stored token expiry: %s", exc)
225
+ return False
226
+
227
+ @staticmethod
228
+ def _prefix_tool_names(body: bytes) -> bytes | None:
229
+ """Prefix all tool names in the request body with TOOL_PREFIX.
230
+
231
+ This is required for Claude Code OAuth compatibility - tools must be
232
+ prefixed on outgoing requests and unprefixed on incoming responses.
233
+ """
234
+ try:
235
+ data = json.loads(body.decode("utf-8"))
236
+ except Exception:
237
+ return None
238
+
239
+ if not isinstance(data, dict):
240
+ return None
241
+
242
+ tools = data.get("tools")
243
+ if not isinstance(tools, list) or not tools:
244
+ return None
245
+
246
+ modified = False
247
+ for tool in tools:
248
+ if isinstance(tool, dict) and "name" in tool:
249
+ name = tool["name"]
250
+ if name and not name.startswith(TOOL_PREFIX):
251
+ tool["name"] = f"{TOOL_PREFIX}{name}"
252
+ modified = True
253
+
254
+ if not modified:
255
+ return None
256
+
257
+ return json.dumps(data).encode("utf-8")
258
+
259
+ @staticmethod
260
+ def _transform_headers_for_claude_code(
261
+ headers: MutableMapping[str, str],
262
+ ) -> None:
263
+ """Transform headers for Claude Code OAuth compatibility.
264
+
265
+ - Sets user-agent to claude-cli
266
+ - Merges anthropic-beta headers appropriately
267
+ - Removes x-api-key (using Bearer auth instead)
268
+ """
269
+ # Set user-agent
270
+ headers["user-agent"] = CLAUDE_CLI_USER_AGENT
271
+
272
+ # Handle anthropic-beta header — merge required betas with any
273
+ # extras already present (e.g. context-1m-2025-08-07).
274
+ incoming_beta = headers.get("anthropic-beta", "")
275
+ incoming_betas = [b.strip() for b in incoming_beta.split(",") if b.strip()]
276
+
277
+ # Always-required betas for Claude Code OAuth
278
+ required_betas = [
279
+ "oauth-2025-04-20",
280
+ "interleaved-thinking-2025-05-14",
281
+ ]
282
+ if "claude-code-20250219" in incoming_betas:
283
+ required_betas.append("claude-code-20250219")
284
+
285
+ # Merge: start with required, then append any extras from the
286
+ # incoming headers that aren't already in the required set.
287
+ merged = list(required_betas)
288
+ required_set = set(required_betas)
289
+ for beta in incoming_betas:
290
+ if beta not in required_set:
291
+ merged.append(beta)
292
+
293
+ headers["anthropic-beta"] = ",".join(merged)
294
+
295
+ # Remove x-api-key if present (we use Bearer auth)
296
+ for key in ["x-api-key", "X-API-Key", "X-Api-Key"]:
297
+ if key in headers:
298
+ del headers[key]
299
+
300
+ @staticmethod
301
+ def _add_beta_query_param(url: httpx.URL) -> httpx.URL:
302
+ """Add ?beta=true query parameter to the URL if not already present."""
303
+ # Parse the URL
304
+ parsed = urlparse(str(url))
305
+ query_params = parse_qs(parsed.query)
306
+
307
+ # Only add if not already present
308
+ if "beta" not in query_params:
309
+ query_params["beta"] = ["true"]
310
+ # Rebuild query string
311
+ new_query = urlencode(query_params, doseq=True)
312
+ # Rebuild URL
313
+ new_parsed = parsed._replace(query=new_query)
314
+ return httpx.URL(urlunparse(new_parsed))
315
+
316
+ return url
317
+
318
+ async def send(
319
+ self, request: httpx.Request, *args: Any, **kwargs: Any
320
+ ) -> httpx.Response: # type: ignore[override]
321
+ is_messages_endpoint = request.url.path.endswith("/v1/messages")
322
+
323
+ # Proactive token refresh: check JWT age before every request
324
+ if not request.extensions.get("claude_oauth_proactive_refresh_attempted"):
325
+ try:
326
+ if self._should_refresh_token(request):
327
+ # TODO: PEP 734 async bridge — convert refresh_access_token to async
328
+ refreshed_token = await asyncio.to_thread(
329
+ self._refresh_claude_oauth_token
330
+ )
331
+ if refreshed_token:
332
+ logger.info("Proactively refreshed token before request")
333
+ # Rebuild request with new token
334
+ headers = dict(request.headers)
335
+ self._update_auth_headers(headers, refreshed_token)
336
+ body_bytes = self._extract_body_bytes(request)
337
+ request = self.build_request(
338
+ method=request.method,
339
+ url=request.url,
340
+ headers=headers,
341
+ content=body_bytes,
342
+ )
343
+ request.extensions[
344
+ "claude_oauth_proactive_refresh_attempted"
345
+ ] = True
346
+ except Exception as exc:
347
+ logger.debug("Error during proactive token refresh check: %s", exc)
348
+
349
+ # Apply Claude Code OAuth transformations for /v1/messages
350
+ if is_messages_endpoint:
351
+ try:
352
+ body_bytes = self._extract_body_bytes(request)
353
+ headers = dict(request.headers)
354
+ url = request.url
355
+ body_modified = False
356
+ headers_modified = False
357
+
358
+ # 1. Transform headers for Claude Code OAuth
359
+ self._transform_headers_for_claude_code(headers)
360
+ headers_modified = True
361
+
362
+ # 2. Add ?beta=true query param
363
+ url = self._add_beta_query_param(url)
364
+
365
+ # 3. Prefix tool names in request body
366
+ if body_bytes:
367
+ prefixed_body = self._prefix_tool_names(body_bytes)
368
+ if prefixed_body is not None:
369
+ body_bytes = prefixed_body
370
+ body_modified = True
371
+
372
+ # 4. Inject cache_control
373
+ cached_body = self._inject_cache_control(body_bytes)
374
+ if cached_body is not None:
375
+ body_bytes = cached_body
376
+ body_modified = True
377
+
378
+ # Rebuild request if anything changed
379
+ if body_modified or headers_modified or url != request.url:
380
+ try:
381
+ rebuilt = self.build_request(
382
+ method=request.method,
383
+ url=url,
384
+ headers=headers,
385
+ content=body_bytes,
386
+ )
387
+
388
+ # Copy core internals so httpx uses the modified body/stream
389
+ if hasattr(rebuilt, "_content"):
390
+ request._content = rebuilt._content # type: ignore[attr-defined]
391
+ if hasattr(rebuilt, "stream"):
392
+ request.stream = rebuilt.stream
393
+ if hasattr(rebuilt, "extensions"):
394
+ # Preserve caller-owned flags (notably oauth retry guards)
395
+ # when httpx gives the rebuilt request fresh extensions.
396
+ request.extensions = {
397
+ **rebuilt.extensions,
398
+ **request.extensions,
399
+ }
400
+
401
+ # Update URL
402
+ request.url = url
403
+
404
+ # Update headers
405
+ for key, value in headers.items():
406
+ request.headers[key] = value
407
+
408
+ # Ensure Content-Length matches the new body
409
+ if body_bytes:
410
+ request.headers["Content-Length"] = str(len(body_bytes))
411
+
412
+ except Exception as exc:
413
+ logger.debug("Error rebuilding request: %s", exc)
414
+
415
+ except Exception as exc:
416
+ logger.debug("Error in Claude Code transformations: %s", exc)
417
+
418
+ # Send the request with retry logic for transient errors
419
+ response = await self._send_with_retries(request, *args, **kwargs)
420
+
421
+ # Extract cache usage for token tracking (best-effort, never blocks)
422
+ if is_messages_endpoint and response.status_code == 200:
423
+ try:
424
+ from code_muse.plugins.token_caching.cache_hit_tracking import (
425
+ _session_stats,
426
+ extract_cache_usage,
427
+ )
428
+
429
+ await response.aread()
430
+ data = response.json()
431
+ usage = extract_cache_usage(data)
432
+ if usage:
433
+ _session_stats.record_usage(usage)
434
+ except Exception:
435
+ pass
436
+
437
+ # NOTE: Tool name unprefixing is now handled at the pydantic-ai level
438
+ # in pydantic_patches.py rather than wrapping the HTTP response stream.
439
+ # The response wrapper caused zlib decompression errors due to httpx
440
+ # response lifecycle issues.
441
+
442
+ # Handle auth errors with token refresh
443
+ try:
444
+ if response.status_code in (400, 401, 403) and not request.extensions.get(
445
+ "claude_oauth_refresh_attempted"
446
+ ):
447
+ is_auth_error = response.status_code in (401, 403)
448
+
449
+ if response.status_code == 400:
450
+ is_auth_error = await self._is_cloudflare_html_error(response)
451
+ if is_auth_error:
452
+ logger.info(
453
+ "Detected Cloudflare 400 error (likely auth-related), attempting token refresh"
454
+ )
455
+
456
+ if is_auth_error:
457
+ # TODO: PEP 734 async bridge — convert reauthentication to async
458
+ recovered_token = await asyncio.to_thread(
459
+ self._recover_claude_oauth_token_after_auth_error
460
+ )
461
+ if recovered_token:
462
+ logger.info("Token recovered successfully, retrying request")
463
+ await response.aclose()
464
+ body_bytes = self._extract_body_bytes(request)
465
+ headers = dict(request.headers)
466
+ self._update_auth_headers(headers, recovered_token)
467
+ retry_request = self.build_request(
468
+ method=request.method,
469
+ url=request.url,
470
+ headers=headers,
471
+ content=body_bytes,
472
+ )
473
+ retry_request.extensions["claude_oauth_refresh_attempted"] = (
474
+ True
475
+ )
476
+ return await self._send_with_retries(
477
+ retry_request, *args, **kwargs
478
+ )
479
+ else:
480
+ logger.warning(
481
+ "Token recovery failed, returning original error"
482
+ )
483
+ except Exception as exc:
484
+ logger.debug("Error during token refresh attempt: %s", exc)
485
+
486
+ return response
487
+
488
+ async def _send_with_retries(
489
+ self, request: httpx.Request, *args: Any, **kwargs: Any
490
+ ) -> httpx.Response:
491
+ """Send request with automatic retries for rate limits and server errors.
492
+
493
+ Retries on:
494
+ - 429 (rate limit) - respects Retry-After header
495
+ - 500, 502, 503, 504 (server errors) - exponential backoff
496
+ - Connection errors (ConnectError, ReadTimeout, PoolTimeout)
497
+ """
498
+ last_response = None
499
+ last_exception = None
500
+
501
+ for attempt in range(MAX_RETRIES + 1):
502
+ try:
503
+ response = await super().send(request, *args, **kwargs)
504
+ last_response = response
505
+
506
+ # Check for retryable status
507
+ if response.status_code not in RETRY_STATUS_CODES:
508
+ return response
509
+
510
+ # Don't retry if this is the last attempt
511
+ if attempt >= MAX_RETRIES:
512
+ return response
513
+
514
+ # Close response before retrying
515
+ await response.aclose()
516
+
517
+ # Calculate wait time with exponential backoff
518
+ wait_time = 1.0 * (2**attempt) # 1s, 2s, 4s, 8s, 16s
519
+
520
+ # For 429, respect Retry-After header if present
521
+ if response.status_code == 429:
522
+ retry_after = response.headers.get("Retry-After")
523
+ if retry_after:
524
+ try:
525
+ wait_time = float(retry_after)
526
+ except ValueError:
527
+ # Try parsing http-date format
528
+ try:
529
+ from email.utils import parsedate_to_datetime
530
+
531
+ date = parsedate_to_datetime(retry_after)
532
+ wait_time = max(0, date.timestamp() - time.time())
533
+ except Exception:
534
+ pass
535
+
536
+ # Cap wait time between 0.5s and 60s
537
+ wait_time = max(0.5, min(wait_time, 60.0))
538
+
539
+ logger.info(
540
+ "HTTP %d received, retrying in %.1fs (attempt %d/%d)",
541
+ response.status_code,
542
+ wait_time,
543
+ attempt + 1,
544
+ MAX_RETRIES,
545
+ )
546
+ await asyncio.sleep(wait_time)
547
+
548
+ except (httpx.ConnectError, httpx.ReadTimeout, httpx.PoolTimeout) as exc:
549
+ last_exception = exc
550
+
551
+ # Don't retry if this is the last attempt
552
+ if attempt >= MAX_RETRIES:
553
+ raise
554
+
555
+ wait_time = 1.0 * (2**attempt)
556
+ wait_time = max(0.5, min(wait_time, 60.0))
557
+
558
+ logger.warning(
559
+ "HTTP connection error: %s. Retrying in %.1fs (attempt %d/%d)",
560
+ exc,
561
+ wait_time,
562
+ attempt + 1,
563
+ MAX_RETRIES,
564
+ )
565
+ await asyncio.sleep(wait_time)
566
+
567
+ except Exception:
568
+ # Don't retry on other exceptions (e.g., validation errors)
569
+ raise
570
+
571
+ # Return last response if we have one
572
+ if last_response is not None:
573
+ return last_response
574
+
575
+ # Re-raise last exception if we have one
576
+ if last_exception is not None:
577
+ raise last_exception
578
+
579
+ # This shouldn't happen, but just in case
580
+ raise RuntimeError("Retry loop completed without response or exception")
581
+
582
+ @staticmethod
583
+ def _extract_body_bytes(request: httpx.Request) -> bytes | None:
584
+ # Try public content first
585
+ try:
586
+ content = request.content
587
+ if content:
588
+ return content
589
+ except Exception:
590
+ pass
591
+
592
+ # Fallback to private attr if necessary
593
+ try:
594
+ content = getattr(request, "_content", None)
595
+ if content:
596
+ return content
597
+ except Exception:
598
+ pass
599
+
600
+ return None
601
+
602
+ @staticmethod
603
+ def _update_auth_headers(
604
+ headers: MutableMapping[str, str], access_token: str
605
+ ) -> None:
606
+ bearer_value = f"Bearer {access_token}"
607
+ if "Authorization" in headers or "authorization" in headers:
608
+ headers["Authorization"] = bearer_value
609
+ elif "x-api-key" in headers or "X-API-Key" in headers:
610
+ headers["x-api-key"] = access_token
611
+ else:
612
+ headers["Authorization"] = bearer_value
613
+
614
+ @staticmethod
615
+ async def _is_cloudflare_html_error(response: httpx.Response) -> bool:
616
+ """Check if this is a Cloudflare HTML error response.
617
+
618
+ Cloudflare often returns HTML error pages with status 400 when
619
+ there are authentication issues.
620
+ """
621
+ # Check content type
622
+ content_type = response.headers.get("content-type", "")
623
+ if "text/html" not in content_type.lower():
624
+ return False
625
+
626
+ # Check if body contains Cloudflare markers
627
+ try:
628
+ # For async httpx, we need to read the body first
629
+ if not hasattr(response, "_content") or not response._content:
630
+ try:
631
+ await response.aread()
632
+ except Exception as read_exc:
633
+ logger.debug("Failed to read response body: %s", read_exc)
634
+ return False
635
+
636
+ # Now we can safely access the content
637
+ if hasattr(response, "_content") and response._content:
638
+ body = response._content.decode("utf-8", errors="ignore")
639
+ else:
640
+ # Fallback to text property (should work after aread)
641
+ try:
642
+ body = response.text
643
+ except Exception:
644
+ return False
645
+
646
+ # Look for Cloudflare and 400 Bad Request markers
647
+ body_lower = body.lower()
648
+ return "cloudflare" in body_lower and "400 bad request" in body_lower
649
+ except Exception as exc:
650
+ logger.debug("Error checking for Cloudflare error: %s", exc)
651
+ return False
652
+
653
+ def _recover_claude_oauth_token_after_auth_error(self) -> str | None:
654
+ """Recover an OAuth token after the API rejected the current one.
655
+
656
+ First tries a refresh-token exchange. If that fails, an optional
657
+ provider-specific callback may run a full interactive OAuth flow.
658
+ """
659
+ refreshed_token = self._refresh_claude_oauth_token()
660
+ if refreshed_token:
661
+ return refreshed_token
662
+
663
+ if not self._oauth_reauthentication_callback:
664
+ return None
665
+
666
+ try:
667
+ reauthenticated_token = self._oauth_reauthentication_callback()
668
+ except Exception as exc:
669
+ logger.error("Exception during OAuth reauthentication: %s", exc)
670
+ return None
671
+
672
+ if not reauthenticated_token:
673
+ logger.warning("OAuth reauthentication returned no token")
674
+ return None
675
+
676
+ self._update_auth_headers(self.headers, reauthenticated_token)
677
+ self._notify_token_recovered(reauthenticated_token)
678
+ return reauthenticated_token
679
+
680
+ def _refresh_claude_oauth_token(self) -> str | None:
681
+ try:
682
+ from code_muse.plugins.claude_code_oauth.utils import refresh_access_token
683
+
684
+ logger.info("Attempting to refresh Claude Code OAuth token...")
685
+ refreshed_token = refresh_access_token(force=True)
686
+ if refreshed_token:
687
+ self._update_auth_headers(self.headers, refreshed_token)
688
+ self._notify_token_recovered(refreshed_token)
689
+ logger.info("Successfully refreshed Claude Code OAuth token")
690
+ else:
691
+ logger.warning("Token refresh returned None")
692
+ return refreshed_token
693
+ except Exception as exc:
694
+ logger.error("Exception during token refresh: %s", exc)
695
+ return None
696
+
697
+ @staticmethod
698
+ def _inject_cache_control(body: bytes) -> bytes | None:
699
+ try:
700
+ data = json.loads(body.decode("utf-8"))
701
+ except Exception:
702
+ return None
703
+
704
+ if not isinstance(data, dict):
705
+ return None
706
+
707
+ modified = False
708
+
709
+ # Anthropic supports up to 4 cache breakpoints. We place them on
710
+ # the three most impactful, stable prefixes so that content which
711
+ # doesn't change between turns is independently cached:
712
+ # 1. System prompt – static across the whole session
713
+ # 2. Tool definitions – static across the whole session
714
+ # 3. Last message – caches the growing conversation prefix
715
+
716
+ # 1. System prompt
717
+ system = data.get("system")
718
+ if isinstance(system, list) and system:
719
+ last_sys = system[-1]
720
+ if isinstance(last_sys, dict) and "cache_control" not in last_sys:
721
+ last_sys["cache_control"] = {"type": "ephemeral"}
722
+ modified = True
723
+ elif isinstance(system, str) and system:
724
+ # Convert bare string to content-block list so we can attach
725
+ # cache_control (the Anthropic API accepts both formats).
726
+ data["system"] = [
727
+ {"type": "text", "text": system, "cache_control": {"type": "ephemeral"}}
728
+ ]
729
+ modified = True
730
+
731
+ # 2. Tool definitions
732
+ tools = data.get("tools")
733
+ if isinstance(tools, list) and tools:
734
+ last_tool = tools[-1]
735
+ if isinstance(last_tool, dict) and "cache_control" not in last_tool:
736
+ last_tool["cache_control"] = {"type": "ephemeral"}
737
+ modified = True
738
+
739
+ # 3. Last message content block
740
+ messages = data.get("messages")
741
+ if isinstance(messages, list) and messages:
742
+ last = messages[-1]
743
+ if isinstance(last, dict):
744
+ content = last.get("content")
745
+ if isinstance(content, list) and content:
746
+ last_block = content[-1]
747
+ if (
748
+ isinstance(last_block, dict)
749
+ and "cache_control" not in last_block
750
+ ):
751
+ last_block["cache_control"] = {"type": "ephemeral"}
752
+ modified = True
753
+
754
+ # 4. Opus 4.7 adaptive-thinking requires display=summarized on the
755
+ # thinking dict. Enforce at the wire level so the request can't go
756
+ # out without it, regardless of upstream settings construction.
757
+ if _enforce_thinking_display_summary(data):
758
+ modified = True
759
+
760
+ if not modified:
761
+ return None
762
+
763
+ return json.dumps(data).encode("utf-8")
764
+
765
+
766
+ def _inject_cache_control_in_payload(payload: dict[str, Any]) -> None:
767
+ """In-place cache_control injection on Anthropic messages.create payload.
768
+
769
+ Places up to three cache breakpoints (Anthropic allows 4) on the most
770
+ valuable, stable prefixes:
771
+ 1. System prompt – never changes between turns
772
+ 2. Tool defs – never changes between turns
773
+ 3. Last message – caches the growing conversation prefix
774
+ """
775
+
776
+ # 1. System prompt
777
+ system = payload.get("system")
778
+ if isinstance(system, list) and system:
779
+ last_sys = system[-1]
780
+ if isinstance(last_sys, dict) and "cache_control" not in last_sys:
781
+ last_sys["cache_control"] = {"type": "ephemeral"}
782
+ elif isinstance(system, str) and system:
783
+ payload["system"] = [
784
+ {"type": "text", "text": system, "cache_control": {"type": "ephemeral"}}
785
+ ]
786
+
787
+ # 2. Tool definitions
788
+ tools = payload.get("tools")
789
+ if isinstance(tools, list) and tools:
790
+ last_tool = tools[-1]
791
+ if isinstance(last_tool, dict) and "cache_control" not in last_tool:
792
+ last_tool["cache_control"] = {"type": "ephemeral"}
793
+
794
+ # 3. Last message content block
795
+ messages = payload.get("messages")
796
+ if isinstance(messages, list) and messages:
797
+ last = messages[-1]
798
+ if isinstance(last, dict):
799
+ content = last.get("content")
800
+ if isinstance(content, list) and content:
801
+ last_block = content[-1]
802
+ if isinstance(last_block, dict) and "cache_control" not in last_block:
803
+ last_block["cache_control"] = {"type": "ephemeral"}
804
+
805
+ # 4. Opus 4.7 adaptive-thinking requires display=summarized on the
806
+ # thinking dict. Enforce here as well so the AsyncAnthropic client
807
+ # patch path matches the raw httpx path.
808
+ _enforce_thinking_display_summary(payload)
809
+
810
+
811
+ def _make_cache_wrapper(original_create: Callable[..., Any]) -> Callable[..., Any]:
812
+ """Create a wrapped version of messages.create that injects cache_control."""
813
+
814
+ async def wrapped_create(*args: Any, **kwargs: Any):
815
+ if kwargs:
816
+ _inject_cache_control_in_payload(kwargs)
817
+ elif args:
818
+ maybe_payload = args[-1]
819
+ if isinstance(maybe_payload, dict):
820
+ _inject_cache_control_in_payload(maybe_payload)
821
+
822
+ return await original_create(*args, **kwargs)
823
+
824
+ return wrapped_create
825
+
826
+
827
+ def patch_anthropic_client_messages(client: Any) -> None:
828
+ """Monkey-patch AsyncAnthropic messages.create to inject cache_control.
829
+
830
+ Patches both client.messages.create AND client.beta.messages.create
831
+ since pydantic-ai uses the beta endpoint.
832
+ """
833
+
834
+ if AsyncAnthropic is None or not isinstance(client, AsyncAnthropic): # type: ignore[arg-type]
835
+ return
836
+
837
+ # Patch client.messages.create
838
+ try:
839
+ messages_obj = getattr(client, "messages", None)
840
+ if messages_obj is not None:
841
+ messages_obj.create = _make_cache_wrapper(messages_obj.create) # type: ignore[assignment]
842
+ except Exception: # pragma: no cover - defensive
843
+ pass
844
+
845
+ # Patch client.beta.messages.create (used by pydantic-ai)
846
+ try:
847
+ beta_obj = getattr(client, "beta", None)
848
+ if beta_obj is not None:
849
+ beta_messages_obj = getattr(beta_obj, "messages", None)
850
+ if beta_messages_obj is not None:
851
+ beta_messages_obj.create = _make_cache_wrapper(beta_messages_obj.create) # type: ignore[assignment]
852
+ except Exception: # pragma: no cover - defensive
853
+ pass