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,59 @@
1
+ """Shared helpers for switching models and reloading agents safely."""
2
+
3
+ from code_muse.config import set_model_name
4
+
5
+
6
+ def _get_effective_agent_model(agent) -> str | None:
7
+ """Safely fetch the effective model name for an agent."""
8
+ try:
9
+ return agent.get_model_name()
10
+ except Exception:
11
+ return None
12
+
13
+
14
+ def set_model_and_reload_agent(
15
+ model_name: str,
16
+ *,
17
+ warn_on_pinned_mismatch: bool = True,
18
+ ) -> None:
19
+ """Set the global model and reload the active agent.
20
+
21
+ This keeps model switching consistent across commands while avoiding
22
+ direct imports that can trigger circular dependencies.
23
+ """
24
+ from code_muse.messaging import emit_info, emit_warning
25
+
26
+ set_model_name(model_name)
27
+
28
+ try:
29
+ from code_muse.agents import get_current_agent
30
+
31
+ current_agent = get_current_agent()
32
+ if current_agent is None:
33
+ emit_warning("Model changed but no active agent was found to reload")
34
+ return
35
+
36
+ # JSON agents may need to refresh their config before reload
37
+ if hasattr(current_agent, "refresh_config"):
38
+ try:
39
+ current_agent.refresh_config()
40
+ except Exception:
41
+ # Non-fatal, continue to reload
42
+ ...
43
+
44
+ if warn_on_pinned_mismatch:
45
+ effective_model = _get_effective_agent_model(current_agent)
46
+ if effective_model and effective_model != model_name:
47
+ display_name = getattr(
48
+ current_agent, "display_name", current_agent.name
49
+ )
50
+ emit_warning(
51
+ "Active agent "
52
+ f"'{display_name}' is pinned to '{effective_model}', "
53
+ f"so '{model_name}' will not take effect until unpinned."
54
+ )
55
+
56
+ current_agent.reload_code_generation_agent()
57
+ emit_info("Active agent reloaded")
58
+ except Exception as exc:
59
+ emit_warning(f"Model changed but agent reload failed: {exc}")
@@ -0,0 +1,156 @@
1
+ """Model-related utilities shared across agents and tools.
2
+
3
+ This module is intentionally model-agnostic. Anything model-family-specific
4
+ (e.g. claude-code OAuth prompt handling) lives in its own plugin and hooks
5
+ into the ``prepare_model_prompt`` or ``get_model_system_prompt`` callbacks.
6
+
7
+ Plugins can register:
8
+
9
+ - ``prepare_model_prompt``: fully take over prompt prep for a model family.
10
+ - ``get_model_system_prompt``: augment/override the system prompt for a model.
11
+ """
12
+
13
+ from dataclasses import dataclass
14
+
15
+
16
+ @dataclass
17
+ class PreparedPrompt:
18
+ """Result of preparing a prompt for a specific model.
19
+
20
+ Attributes:
21
+ instructions: The system instructions to use for the agent
22
+ user_prompt: The user prompt (possibly modified)
23
+ is_claude_code: Whether this is a claude-code model (set by the
24
+ claude_code_oauth plugin via the ``prepare_model_prompt`` hook).
25
+ """
26
+
27
+ instructions: str
28
+ user_prompt: str
29
+ is_claude_code: bool
30
+
31
+
32
+ def prepare_prompt_for_model(
33
+ model_name: str,
34
+ system_prompt: str,
35
+ user_prompt: str,
36
+ prepend_system_to_user: bool = True,
37
+ ) -> PreparedPrompt:
38
+ """Prepare instructions and prompt for a specific model.
39
+
40
+ Core fires two hooks to let plugins customize prompt prep:
41
+
42
+ 1. ``prepare_model_prompt`` — first winner with ``handled=True`` takes
43
+ over entirely (used by the claude_code_oauth plugin).
44
+ 2. ``get_model_system_prompt`` — legacy per-model system-prompt hook;
45
+ still fired for compatibility with plugins (e.g. agent_skills) that
46
+ rely on it.
47
+
48
+ If no plugin handles the model, we return the original system/user prompt
49
+ unchanged.
50
+
51
+ Args:
52
+ model_name: The name of the model being used.
53
+ system_prompt: The default system prompt from the agent.
54
+ user_prompt: The user's prompt/message.
55
+ prepend_system_to_user: Whether to prepend the system prompt to the
56
+ user prompt (only meaningful for plugins that opt into it).
57
+
58
+ Returns:
59
+ PreparedPrompt ready for the model.
60
+ """
61
+ from code_muse import callbacks
62
+
63
+ # 1) Give the dedicated prepare_model_prompt hook first crack. First
64
+ # plugin to claim ``handled=True`` wins.
65
+ for result in callbacks.on_prepare_model_prompt(
66
+ model_name, system_prompt, user_prompt, prepend_system_to_user
67
+ ):
68
+ if result and isinstance(result, dict) and result.get("handled"):
69
+ return PreparedPrompt(
70
+ instructions=result.get("instructions", system_prompt),
71
+ user_prompt=result.get("user_prompt", user_prompt),
72
+ is_claude_code=bool(result.get("is_claude_code", False)),
73
+ )
74
+
75
+ # 2) Fall back to the legacy per-model system-prompt hook for plugins
76
+ # that still register there.
77
+ for result in callbacks.on_get_model_system_prompt(
78
+ model_name, system_prompt, user_prompt
79
+ ):
80
+ if result and isinstance(result, dict) and result.get("handled"):
81
+ return PreparedPrompt(
82
+ instructions=result.get("instructions", system_prompt),
83
+ user_prompt=result.get("user_prompt", user_prompt),
84
+ is_claude_code=bool(result.get("is_claude_code", False)),
85
+ )
86
+
87
+ # 3) No plugin handled it — return the caller's prompts unchanged.
88
+ return PreparedPrompt(
89
+ instructions=system_prompt,
90
+ user_prompt=user_prompt,
91
+ is_claude_code=False,
92
+ )
93
+
94
+
95
+ def supports_adaptive_thinking(
96
+ model_name: str, actual_model_id: str | None = None
97
+ ) -> bool:
98
+ """Return whether a model should default to adaptive thinking.
99
+
100
+ Opus 4-6, Opus 4-7, and Sonnet 4-6 models support adaptive thinking.
101
+ Checks both the alias/key and the real model ID to handle Bedrock-style
102
+ names like ``us.anthropic.claude-opus-4-7``.
103
+
104
+ Args:
105
+ model_name: The model alias/key (e.g. ``"bedrock-opus-4-7"``).
106
+ actual_model_id: The real model ID from config (e.g.
107
+ ``"us.anthropic.claude-opus-4-7"``).
108
+ """
109
+ candidates = [model_name.lower()]
110
+ if actual_model_id:
111
+ candidates.append(actual_model_id.lower())
112
+
113
+ _ADAPTIVE_TAGS = (
114
+ "opus-4-6",
115
+ "4-6-opus",
116
+ "opus-4-7",
117
+ "4-7-opus",
118
+ "sonnet-4-6",
119
+ "4-6-sonnet",
120
+ )
121
+ return any(tag in c for c in candidates for tag in _ADAPTIVE_TAGS)
122
+
123
+
124
+ def get_default_extended_thinking(
125
+ model_name: str, actual_model_id: str | None = None
126
+ ) -> str:
127
+ """Return the default extended_thinking mode for an Anthropic model.
128
+
129
+ Opus 4-6, Opus 4-7, and Sonnet 4-6 models default to ``"adaptive"``
130
+ thinking; all other Anthropic models default to ``"enabled"``.
131
+
132
+ Args:
133
+ model_name: The model alias/key (e.g. ``"bedrock-opus-4-7"``).
134
+ actual_model_id: The real model ID from config (e.g.
135
+ ``"us.anthropic.claude-opus-4-7"``).
136
+
137
+ Returns:
138
+ ``"adaptive"`` for supported variants, ``"enabled"`` otherwise.
139
+ """
140
+ if supports_adaptive_thinking(model_name, actual_model_id):
141
+ return "adaptive"
142
+ return "enabled"
143
+
144
+
145
+ def should_use_anthropic_thinking_summary(
146
+ model_name: str, actual_model_id: str | None = None
147
+ ) -> bool:
148
+ """Return whether Anthropic adaptive thinking should request summary display.
149
+
150
+ Anthropic's newer Opus 4.7 models require ``display: \"summarized\"`` alongside
151
+ ``thinking={"type": "adaptive"}``.
152
+ """
153
+ candidates = [model_name.lower()]
154
+ if actual_model_id:
155
+ candidates.append(actual_model_id.lower())
156
+ return any("opus-4-7" in c or "4-7-opus" in c for c in candidates)
code_muse/models.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "wafer.ai-glm-5.1": {
3
+ "type": "custom_openai",
4
+ "provider": "wafer",
5
+ "name": "GLM-5.1",
6
+ "custom_endpoint": {
7
+ "url": "https://pass.wafer.ai/v1",
8
+ "api_key": "$WAFER_API_KEY"
9
+ },
10
+ "context_length": 200000,
11
+ "supported_settings": ["temperature", "seed", "top_p", "max_tokens"]
12
+ },
13
+ "wafer.ai-Qwen3.5-397B-A17B": {
14
+ "type": "custom_openai",
15
+ "provider": "wafer",
16
+ "name": "Qwen3.5-397B-A17B",
17
+ "custom_endpoint": {
18
+ "url": "https://pass.wafer.ai/v1",
19
+ "api_key": "$WAFER_API_KEY"
20
+ },
21
+ "context_length": 262144,
22
+ "supported_settings": ["temperature", "seed", "top_p", "max_tokens"]
23
+ },
24
+ "wafer-DeepSeek-V4-Pro": {
25
+ "type": "custom_openai",
26
+ "provider": "wafer",
27
+ "name": "DeepSeek-V4-Pro",
28
+ "custom_endpoint": {
29
+ "url": "https://pass.wafer.ai/v1",
30
+ "api_key": "$WAFER_API_KEY"
31
+ },
32
+ "context_length": 1000000,
33
+ "supported_settings": ["temperature", "seed", "top_p", "max_tokens"]
34
+ },
35
+ "wafer-MiniMax-M2.7": {
36
+ "type": "custom_openai",
37
+ "provider": "wafer",
38
+ "name": "MiniMax-M2.7",
39
+ "custom_endpoint": {
40
+ "url": "https://pass.wafer.ai/v1",
41
+ "api_key": "$WAFER_API_KEY"
42
+ },
43
+ "context_length": 204800,
44
+ "supported_settings": ["temperature", "seed", "top_p", "max_tokens"]
45
+ },
46
+ "crof-kimi-k2.5-lightning": {
47
+ "type": "custom_openai",
48
+ "provider": "crof",
49
+ "name": "kimi-k2.5-lightning",
50
+ "custom_endpoint": {
51
+ "url": "https://crof.ai/v1",
52
+ "api_key": "$CROF_API_KEY"
53
+ },
54
+ "context_length": 131072,
55
+ "streaming": false,
56
+ "strict_tools": false,
57
+ "supported_settings": ["temperature", "seed", "top_p"]
58
+ },
59
+ "zai-glm-5-coding": {
60
+ "type": "zai_coding",
61
+ "provider": "zai",
62
+ "name": "glm-5",
63
+ "context_length": 200000,
64
+ "supported_settings": ["temperature", "top_p"]
65
+ }
66
+ }
@@ -0,0 +1,26 @@
1
+ """Models Cache + LRU module for Muse.
2
+
3
+ Ports Codex's models_cache.json pattern and Oh-My-Pi's fs_cache LRU
4
+ to Python with thread-safe generics.
5
+ """
6
+
7
+ from code_muse.models_cache.blocking_lru_cache import BlockingLruCache
8
+ from code_muse.models_cache.cache_writer import write_models_cache
9
+ from code_muse.models_cache.sha256_hash import sha256_digest, sha256_digest_file
10
+ from code_muse.models_cache.startup_integration import (
11
+ CACHE_TTL,
12
+ MODELS_CACHE_PATH,
13
+ load_cached_models,
14
+ refresh_models_cache,
15
+ )
16
+
17
+ __all__ = [
18
+ "BlockingLruCache",
19
+ "sha256_digest",
20
+ "sha256_digest_file",
21
+ "write_models_cache",
22
+ "load_cached_models",
23
+ "refresh_models_cache",
24
+ "CACHE_TTL",
25
+ "MODELS_CACHE_PATH",
26
+ ]
@@ -0,0 +1,98 @@
1
+ """Thread-safe generic LRU cache using OrderedDict and threading.Lock.
2
+
3
+ FREE-THREADED: This cache uses threading.Lock because it may be accessed
4
+ from both sync and async contexts. If all callers become fully async,
5
+ consider switching to asyncio.Lock.
6
+ """
7
+
8
+ import threading
9
+ from collections import OrderedDict
10
+ from collections.abc import Callable
11
+ from typing import TypeVar
12
+
13
+ K = TypeVar("K")
14
+ V = TypeVar("V")
15
+
16
+
17
+ class BlockingLruCache[K, V]:
18
+ """Thread-safe LRU cache with get_or_insert_with pattern.
19
+
20
+ Uses OrderedDict for O(1) LRU eviction and threading.Lock for
21
+ thread safety. Capacity must be a positive integer (> 0).
22
+ Gracefully works outside asyncio/tokio runtimes.
23
+ """
24
+
25
+ def __init__(self, capacity: int) -> None:
26
+ if capacity < 1:
27
+ raise ValueError("capacity must be positive")
28
+ self._capacity = capacity
29
+ # FREE-THREADED: Generic cache — may be accessed from sync or async code.
30
+ # Keep threading.Lock; migrate to asyncio.Lock only if all callers are async.
31
+ self._lock = threading.Lock()
32
+ self._cache: OrderedDict[K, V] = OrderedDict()
33
+
34
+ def get(self, key: K) -> V | None:
35
+ """Return cached value or None. Promotes to MRU on hit."""
36
+ with self._lock:
37
+ if key in self._cache:
38
+ self._cache.move_to_end(key)
39
+ return self._cache[key]
40
+ return None
41
+
42
+ def get_or_insert_with(self, key: K, factory: Callable[[], V]) -> V:
43
+ """Return cached value, or call factory on miss and cache it.
44
+
45
+ The factory is called OUTSIDE the lock to avoid holding it during
46
+ expensive computation. A double-check is performed under lock.
47
+ """
48
+ # Fast path: check cache under lock
49
+ with self._lock:
50
+ if key in self._cache:
51
+ self._cache.move_to_end(key)
52
+ return self._cache[key]
53
+
54
+ # Slow path: compute outside lock
55
+ value = factory()
56
+
57
+ # Insert under lock with eviction check
58
+ with self._lock:
59
+ # Double-check: another thread may have inserted while we computed
60
+ if key in self._cache:
61
+ self._cache.move_to_end(key)
62
+ return self._cache[key]
63
+
64
+ # Evict oldest if at capacity
65
+ while len(self._cache) >= self._capacity:
66
+ self._cache.popitem(last=False)
67
+
68
+ self._cache[key] = value
69
+ return value
70
+
71
+ def insert(self, key: K, value: V) -> None:
72
+ """Insert a value, evicting oldest if at capacity."""
73
+ with self._lock:
74
+ if key in self._cache:
75
+ self._cache.move_to_end(key)
76
+ self._cache[key] = value
77
+ return
78
+ while len(self._cache) >= self._capacity:
79
+ self._cache.popitem(last=False)
80
+ self._cache[key] = value
81
+
82
+ def remove(self, key: K) -> V | None:
83
+ """Remove and return the value for key, or None."""
84
+ with self._lock:
85
+ return self._cache.pop(key, None)
86
+
87
+ def clear(self) -> None:
88
+ """Remove all entries."""
89
+ with self._lock:
90
+ self._cache.clear()
91
+
92
+ def __len__(self) -> int:
93
+ with self._lock:
94
+ return len(self._cache)
95
+
96
+ @property
97
+ def capacity(self) -> int:
98
+ return self._capacity
@@ -0,0 +1,86 @@
1
+ """Write models_cache.json to the data directory.
2
+
3
+ Ports Codex's models_cache.json pattern: a snapshot of the full model
4
+ catalog bundled with the client, refreshed on startup.
5
+ """
6
+
7
+ import dataclasses
8
+ import json
9
+ import logging
10
+ import os
11
+ from datetime import UTC, datetime
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from code_muse.config import DATA_DIR
16
+ from code_muse.models_dev_parser import (
17
+ BUNDLED_JSON_FILENAME,
18
+ ModelInfo,
19
+ ModelsDevRegistry,
20
+ )
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def _client_version() -> str:
26
+ """Return the installed code-muse version."""
27
+ try:
28
+ from importlib.metadata import version
29
+
30
+ return version("code-muse")
31
+ except Exception:
32
+ return "unknown"
33
+
34
+
35
+ def _model_to_dict(model: ModelInfo, priority: int) -> dict[str, Any]:
36
+ """Serialize a ModelInfo into the cache entry format."""
37
+ base = {
38
+ "slug": model.full_id,
39
+ "display_name": model.name,
40
+ "description": "",
41
+ "visibility": "List",
42
+ "priority": priority,
43
+ }
44
+ # Include all dataclass fields for completeness
45
+ fields = dataclasses.asdict(model)
46
+ # Avoid duplicating keys we already set explicitly
47
+ for key, value in fields.items():
48
+ if key not in base:
49
+ base[key] = value
50
+ return base
51
+
52
+
53
+ def write_models_cache(models: list[ModelInfo] | None = None) -> Path | None:
54
+ """Write the models cache file.
55
+
56
+ Args:
57
+ models: Optional explicit list of ModelInfo objects. If None,
58
+ loads from the bundled models_dev_api.json.
59
+
60
+ Returns:
61
+ Path to the written file, or None if writing failed.
62
+ """
63
+ try:
64
+ if models is None:
65
+ bundled_path = Path(__file__).parent.parent / BUNDLED_JSON_FILENAME
66
+ registry = ModelsDevRegistry(bundled_path)
67
+ models = list(registry.models.values())
68
+
69
+ cache_data: dict[str, Any] = {
70
+ "fetched_at": datetime.now(UTC).isoformat(),
71
+ "etag": None,
72
+ "client_version": _client_version(),
73
+ "models": [_model_to_dict(m, priority=idx) for idx, m in enumerate(models)],
74
+ }
75
+
76
+ cache_path = Path(DATA_DIR) / "models_cache.json"
77
+ os.makedirs(cache_path.parent, exist_ok=True)
78
+ with open(cache_path, "w", encoding="utf-8") as f:
79
+ json.dump(cache_data, f, indent=2, default=str)
80
+
81
+ logger.info(f"Wrote models cache ({len(models)} models) to {cache_path}")
82
+ return cache_path
83
+
84
+ except (OSError, ValueError, TypeError) as exc:
85
+ logger.warning(f"Failed to write models cache: {exc}")
86
+ return None
@@ -0,0 +1,34 @@
1
+ # cython: language_level=3
2
+ """SHA-256 content hash utility for content-addressed cache keys."""
3
+
4
+ import hashlib
5
+ from pathlib import Path
6
+
7
+
8
+ def sha256_digest(data: bytes) -> str:
9
+ """Return the SHA-256 hex digests of the given bytes.
10
+
11
+ Returns a 64-character lowercase hex string.
12
+ """
13
+ return hashlib.sha256(data).hexdigest()
14
+
15
+
16
+ def sha256_digest_file(path: Path) -> str:
17
+ """Return the SHA-256 hex digests of a file's contents.
18
+
19
+ Streams the file in 64KB chunks to handle large files efficiently.
20
+ The chunk-reading loop uses typed locals and bound methods to avoid
21
+ Python-level attribute lookups.
22
+ """
23
+ cdef object hasher = hashlib.sha256()
24
+ cdef bytes chunk
25
+ cdef object update = hasher.update
26
+ cdef object read
27
+ with open(path, "rb") as f:
28
+ read = f.read
29
+ while True:
30
+ chunk = read(65536)
31
+ if len(chunk) == 0:
32
+ break
33
+ update(chunk)
34
+ return hasher.hexdigest()
@@ -0,0 +1,75 @@
1
+ """Startup cache integration — load cached models on session start."""
2
+
3
+ import json
4
+ import logging
5
+ from datetime import UTC, datetime, timedelta
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from code_muse.config import MODELS_CACHE_FILE
10
+ from code_muse.models_cache.cache_writer import write_models_cache
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ MODELS_CACHE_PATH = Path(MODELS_CACHE_FILE)
15
+ CACHE_TTL = timedelta(hours=24)
16
+
17
+
18
+ def load_cached_models() -> list[dict[str, Any]] | None:
19
+ """Load models from cache if fresh (< 24h old).
20
+
21
+ Returns:
22
+ List of model dicts if cache is fresh, None if stale or missing.
23
+ Never raises — logs errors and returns None.
24
+ """
25
+ try:
26
+ if not MODELS_CACHE_PATH.exists():
27
+ logger.debug("models_cache.json not found")
28
+ return None
29
+
30
+ with open(MODELS_CACHE_PATH, encoding="utf-8") as f:
31
+ cache = json.load(f)
32
+
33
+ fetched_at_str = cache.get("fetched_at")
34
+ if not fetched_at_str:
35
+ logger.warning("models_cache.json missing fetched_at")
36
+ return None
37
+
38
+ fetched_at = datetime.fromisoformat(fetched_at_str)
39
+ age = datetime.now(UTC) - fetched_at
40
+
41
+ if age > CACHE_TTL:
42
+ logger.debug(f"models_cache.json is {age} old, needs refresh")
43
+ return None
44
+
45
+ models = cache.get("models")
46
+ if not isinstance(models, list):
47
+ logger.warning("models_cache.json models is not a list")
48
+ return None
49
+
50
+ logger.info(f"Using cached models ({len(models)} models, {age} old)")
51
+ return models
52
+
53
+ except (OSError, ValueError) as exc:
54
+ logger.warning(f"Failed to load models_cache.json: {exc}")
55
+ return None
56
+
57
+
58
+ def refresh_models_cache() -> list[dict[str, Any]]:
59
+ """Fetch fresh models and update cache.
60
+
61
+ Returns:
62
+ Fresh list of model dicts.
63
+ """
64
+ write_models_cache()
65
+ cached = load_cached_models()
66
+ if cached is not None:
67
+ return cached
68
+ # If cache write failed, load from bundled as ultimate fallback
69
+ from code_muse.models_dev_parser import ModelsDevRegistry
70
+
71
+ bundled_path = Path(__file__).parent.parent / "models_dev_api.json"
72
+ registry = ModelsDevRegistry(bundled_path)
73
+ return [
74
+ {"slug": m.full_id, "display_name": m.name} for m in registry.models.values()
75
+ ]