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
code_muse/config.py ADDED
@@ -0,0 +1,1138 @@
1
+ import configparser
2
+ import contextlib
3
+ import datetime
4
+ import os
5
+ from pathlib import Path
6
+
7
+ from code_muse.session_storage import save_session
8
+
9
+
10
+ def _get_xdg_dir(env_var: str, fallback: str) -> Path:
11
+ """
12
+ Get directory for code_muse files, defaulting to ~/.muse.
13
+
14
+ XDG paths are only used when the corresponding environment variable
15
+ is explicitly set by the user. Otherwise, we use the legacy ~/.muse
16
+ directory for all file types (config, data, cache, state).
17
+
18
+ Args:
19
+ env_var: XDG environment variable name (e.g., "XDG_CONFIG_HOME")
20
+ fallback: Fallback path relative to home (e.g., ".config") - unused unless XDG var is set
21
+
22
+ Returns:
23
+ Path to the directory for code_muse files
24
+ """
25
+ # Use XDG directory ONLY if environment variable is explicitly set
26
+ xdg_base = os.getenv(env_var)
27
+ if xdg_base:
28
+ return Path(xdg_base) / "muse"
29
+
30
+ # Default to legacy ~/.muse for all file types
31
+ return Path.home() / ".muse"
32
+
33
+
34
+ # XDG Base Directory paths
35
+ CONFIG_DIR = _get_xdg_dir("XDG_CONFIG_HOME", ".config")
36
+ DATA_DIR = _get_xdg_dir("XDG_DATA_HOME", ".local/share")
37
+ CACHE_DIR = _get_xdg_dir("XDG_CACHE_HOME", ".cache")
38
+ STATE_DIR = _get_xdg_dir("XDG_STATE_HOME", ".local/state")
39
+
40
+ # Configuration files (XDG_CONFIG_HOME)
41
+ CONFIG_FILE = CONFIG_DIR / "muse.cfg"
42
+
43
+ # Config cache to avoid repeated file reads
44
+ _config_cache: tuple[str, float, configparser.ConfigParser | None] = None
45
+ _config_cache_lock = None # Will be initialized lazily; FREE-THREADED: sync-only cache
46
+
47
+
48
+ def _get_cached_config() -> configparser.ConfigParser:
49
+ """Return a cached ConfigParser, re-reading only when the file changes."""
50
+ global _config_cache, _config_cache_lock
51
+ import threading
52
+
53
+ if _config_cache_lock is None:
54
+ _config_cache_lock = threading.Lock()
55
+
56
+ cache_key = str(CONFIG_FILE)
57
+ try:
58
+ mtime = CONFIG_FILE.stat().st_mtime
59
+ except OSError:
60
+ mtime = 0.0
61
+
62
+ with _config_cache_lock:
63
+ if _config_cache is not None and _config_cache[0] == cache_key and _config_cache[1] == mtime:
64
+ return _config_cache[2]
65
+ # Cache miss or file changed — reload
66
+ config = configparser.ConfigParser()
67
+ config.read(CONFIG_FILE)
68
+ _config_cache = (cache_key, mtime, config)
69
+ return config
70
+
71
+
72
+ # Data files (XDG_DATA_HOME)
73
+ MODELS_FILE = DATA_DIR / "models.json"
74
+ EXTRA_MODELS_FILE = DATA_DIR / "extra_models.json"
75
+ MODELS_CACHE_FILE = DATA_DIR / "models_cache.json"
76
+ AGENTS_DIR = DATA_DIR / "agents"
77
+ SKILLS_DIR = DATA_DIR / "skills"
78
+ CONTEXTS_DIR = DATA_DIR / "contexts"
79
+
80
+ # OAuth plugin model files (XDG_DATA_HOME)
81
+ GEMINI_MODELS_FILE = DATA_DIR / "gemini_models.json"
82
+ CHATGPT_MODELS_FILE = DATA_DIR / "chatgpt_models.json"
83
+ CLAUDE_MODELS_FILE = DATA_DIR / "claude_models.json"
84
+ COPILOT_MODELS_FILE = DATA_DIR / "copilot_models.json"
85
+
86
+ # Cache files (XDG_CACHE_HOME)
87
+ AUTOSAVE_DIR = CACHE_DIR / "autosaves"
88
+
89
+ # State files (XDG_STATE_HOME)
90
+ COMMAND_HISTORY_FILE = STATE_DIR / "command_history.txt"
91
+
92
+
93
+ def get_subagent_verbose() -> bool:
94
+ """Return True if sub-agent verbose output is enabled (default False).
95
+
96
+ When False (default), sub-agents produce quiet, sparse output suitable
97
+ for parallel execution. When True, sub-agents produce full verbose output
98
+ like the main agent (useful for debugging).
99
+ """
100
+ cfg_val = get_value("subagent_verbose")
101
+ if cfg_val is None:
102
+ return False
103
+ return str(cfg_val).strip().lower() in {"1", "true", "yes", "on"}
104
+
105
+
106
+ # Pack agents - the specialized sub-agents coordinated by Pack Leader
107
+
108
+ # Agents that require Universal Constructor to be enabled
109
+
110
+
111
+ def get_max_hook_retries() -> int:
112
+ """Return the maximum number of plugin hook retries after an agent run.
113
+
114
+ When a plugin hook returns ``{"retry": True, ...}`` the agent re-runs.
115
+ This caps how many times that can happen to prevent runaway loops.
116
+ Defaults to 3.
117
+ """
118
+ val = get_value("max_hook_retries")
119
+ if val is None:
120
+ return 3
121
+ try:
122
+ n = int(val)
123
+ return max(1, n) # At least 1 to avoid nonsensical values
124
+ except ValueError, TypeError:
125
+ return 3
126
+
127
+
128
+ def get_enable_streaming() -> bool:
129
+ """
130
+ Get the enable_streaming configuration value.
131
+ Controls whether streaming (SSE) is used for model responses.
132
+ Returns True if streaming is enabled, False otherwise.
133
+ Defaults to True.
134
+ """
135
+ val = get_value("enable_streaming")
136
+ if val is None:
137
+ return True # Default to True for better UX
138
+ return str(val).lower() in ("1", "true", "yes", "on")
139
+
140
+
141
+ def get_auto_approve() -> bool:
142
+ """
143
+ Get the auto_approve configuration value.
144
+ When True, all user approval prompts are automatically approved
145
+ without showing the interactive menu.
146
+ Defaults to True for a smoother UX.
147
+ """
148
+ val = get_value("auto_approve")
149
+ if val is None:
150
+ return True
151
+ return str(val).lower() in ("1", "true", "yes", "on")
152
+
153
+
154
+ DEFAULT_SECTION = "muse"
155
+ REQUIRED_KEYS = ["agent_name", "owner_name"]
156
+
157
+ # Runtime-only autosave session ID (per-process)
158
+ _CURRENT_AUTOSAVE_ID: str | None = None
159
+
160
+ # Session-local model name (initialized from file on first access, then cached)
161
+ _SESSION_MODEL: str | None = None
162
+
163
+ # Cache containers for model validation and defaults
164
+ _model_validation_cache = {}
165
+ _default_model_cache = None
166
+ _default_vision_model_cache = None
167
+
168
+
169
+ def ensure_config_exists():
170
+ """
171
+ Ensure that XDG directories and muse.cfg exist, prompting if needed.
172
+ Returns configparser.ConfigParser for reading.
173
+ """
174
+ # Create all XDG directories with 0700 permissions per XDG spec
175
+ for directory in [CONFIG_DIR, DATA_DIR, CACHE_DIR, STATE_DIR, SKILLS_DIR]:
176
+ if not directory.exists():
177
+ directory.mkdir(parents=True, mode=0o700, exist_ok=True)
178
+ exists = CONFIG_FILE.is_file()
179
+ config = configparser.ConfigParser()
180
+ if exists:
181
+ config.read(CONFIG_FILE)
182
+ missing = []
183
+ if DEFAULT_SECTION not in config:
184
+ config[DEFAULT_SECTION] = {}
185
+ for key in REQUIRED_KEYS:
186
+ if not config[DEFAULT_SECTION].get(key):
187
+ missing.append(key)
188
+ if missing:
189
+ # Note: Using sys.stdout here for initial setup before messaging system is available
190
+ import sys
191
+
192
+ sys.stdout.write("[Run] Let's get your agent ready.\n")
193
+ sys.stdout.flush()
194
+ for key in missing:
195
+ if key == "agent_name":
196
+ val = input("What should we name the agent? ").strip()
197
+ elif key == "owner_name":
198
+ val = input(
199
+ "What's your name (so Muse knows its owner)? "
200
+ ).strip()
201
+ else:
202
+ val = input(f"Enter {key}: ").strip()
203
+ config[DEFAULT_SECTION][key] = val
204
+
205
+ # Set default values for important config keys if they don't exist
206
+ if not config[DEFAULT_SECTION].get("auto_save_session"):
207
+ config[DEFAULT_SECTION]["auto_save_session"] = "true"
208
+ if not config[DEFAULT_SECTION].get("animations_enabled"):
209
+ config[DEFAULT_SECTION]["animations_enabled"] = "true"
210
+
211
+ # Write the config if we made any changes
212
+ if missing or not exists:
213
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
214
+ config.write(f)
215
+ return config
216
+
217
+
218
+ def get_value(key: str):
219
+ config = _get_cached_config()
220
+ val = config.get(DEFAULT_SECTION, key, fallback=None)
221
+ return val
222
+
223
+
224
+ def get_agent_name():
225
+ return get_value("agent_name") or "Muse"
226
+
227
+
228
+ def get_owner_name():
229
+ return get_value("owner_name") or "Creator"
230
+
231
+
232
+ def get_animations_enabled() -> bool:
233
+ """Return whether terminal animations are enabled.
234
+
235
+ Defaults to True if not configured.
236
+ """
237
+ val = get_value("animations_enabled")
238
+ if val is None:
239
+ return True
240
+ return val.lower() in ("true", "1", "yes", "on")
241
+
242
+
243
+ # Legacy function removed - message history limit is no longer used
244
+ # Message history is now managed by token-based compaction system
245
+ # using get_protected_token_count() and get_summarization_threshold()
246
+
247
+
248
+ def get_allow_recursion() -> bool:
249
+ """
250
+ Get the allow_recursion configuration value.
251
+ Returns True if recursion is allowed, False otherwise.
252
+ """
253
+ val = get_value("allow_recursion")
254
+ if val is None:
255
+ return True # Default to True to allow recursion unless explicitly disabled
256
+ return str(val).lower() in ("1", "true", "yes", "on")
257
+
258
+
259
+ def get_model_context_length() -> int:
260
+ """
261
+ Get the context length for the currently configured model from models.json
262
+ """
263
+ try:
264
+ from code_muse.model_factory import ModelFactory
265
+
266
+ model_configs = ModelFactory.load_config()
267
+ model_name = get_global_model_name()
268
+
269
+ # Get context length from model config
270
+ model_config = model_configs.get(model_name, {})
271
+ context_length = model_config.get("context_length", 128000) # Default value
272
+
273
+ return int(context_length)
274
+ except Exception:
275
+ # Fallback to default context length if anything goes wrong
276
+ return 128000
277
+
278
+
279
+ # --- CONFIG SETTER STARTS HERE ---
280
+ def get_config_keys():
281
+ """
282
+ Returns the list of all config keys currently in muse.cfg,
283
+ plus certain preset expected keys (e.g. "yolo_mode", "model", "compaction_strategy", "message_limit", "allow_recursion").
284
+ """
285
+ default_keys = [
286
+ "yolo_mode",
287
+ "model",
288
+ "compaction_strategy",
289
+ "protected_token_count",
290
+ "compaction_threshold",
291
+ "summarization_model",
292
+ "message_limit",
293
+ "allow_recursion",
294
+ "openai_reasoning_effort",
295
+ "openai_reasoning_summary",
296
+ "openai_verbosity",
297
+ "auto_save_session",
298
+ "max_saved_sessions",
299
+ "http2",
300
+ "diff_context_lines",
301
+ "default_agent",
302
+ "temperature",
303
+ "frontend_emitter_enabled",
304
+ "frontend_emitter_max_recent_events",
305
+ "frontend_emitter_queue_size",
306
+ "auto_approve",
307
+ ]
308
+ # Add pack agents control key
309
+ default_keys.append("enable_pack_agents")
310
+ # Add universal constructor control key
311
+ default_keys.append("enable_universal_constructor")
312
+ # Add hook retry limit key
313
+ default_keys.append("max_hook_retries")
314
+ # Add streaming control key
315
+ default_keys.append("enable_streaming")
316
+ # Add cancel agent key configuration
317
+ default_keys.append("cancel_agent_key")
318
+ # Add banner color keys
319
+ for banner_name in DEFAULT_BANNER_COLORS:
320
+ default_keys.append(f"banner_color_{banner_name}")
321
+ # Add resume message count configuration
322
+ default_keys.append("resume_message_count")
323
+
324
+ config = configparser.ConfigParser()
325
+ config.read(CONFIG_FILE)
326
+ keys = set(config[DEFAULT_SECTION].keys()) if DEFAULT_SECTION in config else set()
327
+ keys.update(default_keys)
328
+ return sorted(keys)
329
+
330
+
331
+ def set_config_value(key: str, value: str):
332
+ """
333
+ Sets a config value in the persistent config file.
334
+ """
335
+ global _config_cache
336
+ config = configparser.ConfigParser()
337
+ config.read(CONFIG_FILE)
338
+ if DEFAULT_SECTION not in config:
339
+ config[DEFAULT_SECTION] = {}
340
+ config[DEFAULT_SECTION][key] = value
341
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
342
+ config.write(f)
343
+ # Invalidate cache so subsequent reads pick up the change immediately
344
+ _config_cache = None
345
+
346
+
347
+ # Alias for API compatibility
348
+ def set_value(key: str, value: str) -> None:
349
+ """Set a config value. Alias for set_config_value."""
350
+ set_config_value(key, value)
351
+
352
+
353
+ def reset_value(key: str) -> None:
354
+ """Remove a key from the config file, resetting it to default."""
355
+ global _config_cache
356
+ config = configparser.ConfigParser()
357
+ config.read(CONFIG_FILE)
358
+ if DEFAULT_SECTION in config and key in config[DEFAULT_SECTION]:
359
+ del config[DEFAULT_SECTION][key]
360
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
361
+ config.write(f)
362
+ # Invalidate cache so subsequent reads pick up the change immediately
363
+ _config_cache = None
364
+
365
+
366
+ # --- MODEL STICKY EXTENSION STARTS HERE ---
367
+
368
+
369
+ def _default_model_from_models_json():
370
+ """Load the default model name from models.json.
371
+
372
+ Returns the first model in models.json as the default.
373
+ Falls back to ``gpt-5`` if the file cannot be read.
374
+ """
375
+ global _default_model_cache
376
+
377
+ if _default_model_cache is not None:
378
+ return _default_model_cache
379
+
380
+ try:
381
+ from code_muse.model_factory import ModelFactory
382
+
383
+ models_config = ModelFactory.load_config()
384
+ if models_config:
385
+ # Use first model in models.json as default
386
+ first_key = next(iter(models_config))
387
+ _default_model_cache = first_key
388
+ return first_key
389
+ _default_model_cache = "gpt-5"
390
+ return "gpt-5"
391
+ except Exception:
392
+ _default_model_cache = "gpt-5"
393
+ return "gpt-5"
394
+
395
+
396
+ def _default_vision_model_from_models_json() -> str:
397
+ """Select a default vision-capable model from models.json with caching."""
398
+ global _default_vision_model_cache
399
+
400
+ if _default_vision_model_cache is not None:
401
+ return _default_vision_model_cache
402
+
403
+ try:
404
+ from code_muse.model_factory import ModelFactory
405
+
406
+ models_config = ModelFactory.load_config()
407
+ if models_config:
408
+ # Prefer explicitly tagged vision models
409
+ for name, config in models_config.items():
410
+ if config.get("supports_vision"):
411
+ _default_vision_model_cache = name
412
+ return name
413
+
414
+ # Fallback heuristic: common multimodal models
415
+ preferred_candidates = (
416
+ "gpt-4.1",
417
+ "gpt-4.1-mini",
418
+ "gpt-4.1-nano",
419
+ "claude-4-0-sonnet",
420
+ "gemini-2.5-flash-preview-05-20",
421
+ )
422
+ for candidate in preferred_candidates:
423
+ if candidate in models_config:
424
+ _default_vision_model_cache = candidate
425
+ return candidate
426
+
427
+ # Last resort: use the general default model
428
+ _default_vision_model_cache = _default_model_from_models_json()
429
+ return _default_vision_model_cache
430
+
431
+ _default_vision_model_cache = "gpt-4.1"
432
+ return "gpt-4.1"
433
+ except Exception:
434
+ _default_vision_model_cache = "gpt-4.1"
435
+ return "gpt-4.1"
436
+
437
+
438
+ def _validate_model_exists(model_name: str) -> bool:
439
+ """Check if a model exists in models.json with caching to avoid redundant calls."""
440
+ global _model_validation_cache
441
+
442
+ # Check cache first
443
+ if model_name in _model_validation_cache:
444
+ return _model_validation_cache[model_name]
445
+
446
+ try:
447
+ from code_muse.model_factory import ModelFactory
448
+
449
+ models_config = ModelFactory.load_config()
450
+ exists = model_name in models_config
451
+
452
+ # Cache the result
453
+ _model_validation_cache[model_name] = exists
454
+ return exists
455
+ except Exception:
456
+ # If we can't validate, assume it exists to avoid breaking things
457
+ _model_validation_cache[model_name] = True
458
+ return True
459
+
460
+
461
+ def clear_model_cache():
462
+ """Clear the model validation cache. Call this when models.json changes."""
463
+ global _model_validation_cache, _default_model_cache, _default_vision_model_cache
464
+ _model_validation_cache.clear()
465
+ _default_model_cache = None
466
+ _default_vision_model_cache = None
467
+
468
+
469
+ def reset_session_model():
470
+ """Reset the session-local model cache.
471
+
472
+ This is primarily for testing purposes. In normal operation, the session
473
+ model is set once at startup and only changes via set_model_name().
474
+ """
475
+ global _SESSION_MODEL
476
+ _SESSION_MODEL = None
477
+
478
+
479
+ def model_supports_setting(model_name: str, setting: str) -> bool:
480
+ """Check if a model supports a particular setting (e.g., 'temperature', 'seed').
481
+
482
+ Args:
483
+ model_name: The name of the model to check.
484
+ setting: The setting name to check for (e.g., 'temperature', 'seed', 'top_p').
485
+
486
+ Returns:
487
+ True if the model supports the setting, False otherwise.
488
+ Defaults to True for backwards compatibility if model config doesn't specify.
489
+ """
490
+ # GLM-4.7 and GLM-5 models always support clear_thinking setting
491
+ if setting == "clear_thinking" and (
492
+ "glm-4.7" in model_name.lower() or "glm-5" in model_name.lower()
493
+ ):
494
+ return True
495
+
496
+ try:
497
+ from code_muse.model_factory import ModelFactory
498
+
499
+ models_config = ModelFactory.load_config()
500
+ model_config = models_config.get(model_name, {})
501
+
502
+ # Get supported_settings list, default to supporting common settings
503
+ supported_settings = model_config.get("supported_settings")
504
+
505
+ if supported_settings is None:
506
+ # Default: assume common settings are supported for backwards compatibility
507
+ # For Anthropic/Claude models, include extended thinking settings
508
+ if model_name.startswith("claude-") or model_name.startswith("anthropic-"):
509
+ base = ["temperature", "extended_thinking", "budget_tokens"]
510
+ from code_muse.model_utils import supports_adaptive_thinking
511
+
512
+ if supports_adaptive_thinking(model_name):
513
+ base.append("effort")
514
+ return setting in base
515
+ return setting in ["temperature", "seed"]
516
+
517
+ return setting in supported_settings
518
+ except Exception:
519
+ # If we can't check, assume supported for safety
520
+ return True
521
+
522
+
523
+ def get_global_model_name():
524
+ """Return a valid model name for Muse to use.
525
+
526
+ Uses session-local caching so that model changes in other terminals
527
+ don't affect this running instance. The file is only read once at startup.
528
+
529
+ 1. If _SESSION_MODEL is set, return it (session cache)
530
+ 2. Otherwise, look at ``model`` in *muse.cfg*
531
+ 3. If that value exists **and** is present in *models.json*, use it
532
+ 4. Otherwise return the first model listed in *models.json*
533
+ 5. As a last resort fall back to ``claude-4-0-sonnet``
534
+
535
+ The result is cached in _SESSION_MODEL for subsequent calls.
536
+ """
537
+ global _SESSION_MODEL
538
+
539
+ # Return cached session model if already initialized
540
+ if _SESSION_MODEL is not None:
541
+ return _SESSION_MODEL
542
+
543
+ # First access - initialize from file
544
+ stored_model = get_value("model")
545
+
546
+ if stored_model:
547
+ # Use cached validation to avoid hitting ModelFactory every time
548
+ if _validate_model_exists(stored_model):
549
+ _SESSION_MODEL = stored_model
550
+ return _SESSION_MODEL
551
+
552
+ # Either no stored model or it's not valid – choose default from models.json
553
+ _SESSION_MODEL = _default_model_from_models_json()
554
+ return _SESSION_MODEL
555
+
556
+
557
+ def set_model_name(model: str):
558
+ """Sets the model name in both the session cache and persistent config file.
559
+
560
+ Updates _SESSION_MODEL immediately for this process, and writes to the
561
+ config file so new terminals will pick up this model as their default.
562
+ """
563
+ global _SESSION_MODEL
564
+
565
+ # Update session cache immediately
566
+ _SESSION_MODEL = model
567
+
568
+ # Also persist to file for new terminal sessions
569
+ config = configparser.ConfigParser()
570
+ config.read(CONFIG_FILE)
571
+ if DEFAULT_SECTION not in config:
572
+ config[DEFAULT_SECTION] = {}
573
+ config[DEFAULT_SECTION]["model"] = model or ""
574
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
575
+ config.write(f)
576
+
577
+ # Clear model cache when switching models to ensure fresh validation
578
+ clear_model_cache()
579
+
580
+
581
+ # --- PER-MODEL SETTINGS ---
582
+
583
+
584
+ # Legacy functions for backward compatibility
585
+
586
+
587
+ def normalize_command_history():
588
+ """
589
+ Normalize the command history file by converting old format timestamps to the new format.
590
+
591
+ Old format example:
592
+ - "# 2025-08-04 12:44:45.469829"
593
+
594
+ New format example:
595
+ - "# 2025-08-05T10:35:33" (ISO)
596
+ """
597
+ import os
598
+ import re
599
+
600
+ # Skip implementation during tests
601
+ import sys
602
+
603
+ if "pytest" in sys.modules:
604
+ return
605
+
606
+ # Skip normalization if file doesn't exist
607
+ command_history_exists = COMMAND_HISTORY_FILE.is_file()
608
+ if not command_history_exists:
609
+ return
610
+
611
+ try:
612
+ # Read the entire file with encoding error handling for Windows
613
+ with open(
614
+ COMMAND_HISTORY_FILE, encoding="utf-8", errors="surrogateescape"
615
+ ) as f:
616
+ content = f.read()
617
+
618
+ # Sanitize any surrogate characters that might have slipped in
619
+ try:
620
+ content = content.encode("utf-8", errors="surrogatepass").decode(
621
+ "utf-8", errors="replace"
622
+ )
623
+ except UnicodeEncodeError, UnicodeDecodeError:
624
+ pass # Keep original if sanitization fails
625
+
626
+ # Skip empty files
627
+ if not content.strip():
628
+ return
629
+
630
+ # Define regex pattern for old timestamp format
631
+ # Format: "# YYYY-MM-DD HH:MM:SS.ffffff"
632
+ old_timestamp_pattern = r"# (\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})\.(\d+)"
633
+
634
+ # Function to convert matched timestamp to ISO format
635
+ def convert_to_iso(match):
636
+ date = match.group(1)
637
+ time = match.group(2)
638
+ # Create ISO format (YYYY-MM-DDThh:mm:ss)
639
+ return f"# {date}T{time}"
640
+
641
+ # Replace all occurrences of the old timestamp format with the new ISO format
642
+ updated_content = re.sub(old_timestamp_pattern, convert_to_iso, content)
643
+
644
+ # Write the updated content back to the file only if changes were made
645
+ if content != updated_content:
646
+ import tempfile
647
+
648
+ fd, tmp_path = tempfile.mkstemp(
649
+ dir=str(COMMAND_HISTORY_FILE.parent), suffix=".tmp"
650
+ )
651
+ try:
652
+ with os.fdopen(
653
+ fd, "w", encoding="utf-8", errors="surrogateescape"
654
+ ) as f:
655
+ f.write(updated_content)
656
+ os.replace(tmp_path, COMMAND_HISTORY_FILE)
657
+ except BaseException:
658
+ with contextlib.suppress(OSError):
659
+ os.unlink(tmp_path)
660
+ raise
661
+ except Exception as e:
662
+ from code_muse.messaging import emit_error
663
+
664
+ emit_error(
665
+ f"An unexpected error occurred while normalizing command history: {str(e)}"
666
+ )
667
+
668
+
669
+ def initialize_command_history_file():
670
+ """Create the command history file if it doesn't exist.
671
+ Handles migration from the old history file location for backward compatibility.
672
+ Also normalizes the command history format if needed.
673
+ """
674
+ from pathlib import Path
675
+
676
+ # Ensure the state directory exists before trying to create the history file
677
+ if not STATE_DIR.exists():
678
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
679
+
680
+ command_history_exists = COMMAND_HISTORY_FILE.is_file()
681
+ if not command_history_exists:
682
+ try:
683
+ COMMAND_HISTORY_FILE.touch()
684
+
685
+ # For backwards compatibility, copy the old history file, then remove it
686
+ old_history_file = Path.home() / ".muse_history.txt"
687
+ old_history_exists = old_history_file.is_file()
688
+ if old_history_exists:
689
+ import shutil
690
+
691
+ shutil.copy2(old_history_file, COMMAND_HISTORY_FILE)
692
+ old_history_file.unlink(missing_ok=True)
693
+
694
+ # Normalize the command history format if needed
695
+ normalize_command_history()
696
+ except Exception as e:
697
+ from code_muse.messaging import emit_error
698
+
699
+ emit_error(
700
+ f"An unexpected error occurred while trying to initialize history file: {str(e)}"
701
+ )
702
+
703
+
704
+ def get_yolo_mode():
705
+ """
706
+ Checks muse.cfg for 'yolo_mode' (case-insensitive in value only).
707
+ Defaults to False (safe mode) if not set.
708
+ Allowed values for ON: 1, '1', 'true', 'yes', 'on' (all case-insensitive for value).
709
+ """
710
+ true_vals = {"1", "true", "yes", "on"}
711
+ cfg_val = get_value("yolo_mode")
712
+ if cfg_val is not None:
713
+ return str(cfg_val).strip().lower() in true_vals
714
+ return False
715
+
716
+
717
+ def get_safety_permission_level():
718
+ """
719
+ Checks muse.cfg for 'safety_permission_level' (case-insensitive in value only).
720
+ Defaults to 'medium' if not set.
721
+ Allowed values: 'none', 'low', 'medium', 'high', 'critical' (all case-insensitive for value).
722
+ Returns the normalized lowercase string.
723
+ """
724
+ valid_levels = {"none", "low", "medium", "high", "critical"}
725
+ cfg_val = get_value("safety_permission_level")
726
+ if cfg_val is not None:
727
+ normalized = str(cfg_val).strip().lower()
728
+ if normalized in valid_levels:
729
+ return normalized
730
+ return "medium" # Default to medium risk threshold
731
+
732
+
733
+ def get_grep_output_verbose():
734
+ """
735
+ Checks muse.cfg for 'grep_output_verbose' (case-insensitive in value only).
736
+ Defaults to False (concise output) if not set.
737
+ Allowed values for ON: 1, '1', 'true', 'yes', 'on' (all case-insensitive for value).
738
+
739
+ When False (default): Shows only file names with match counts
740
+ When True: Shows full output with line numbers and content
741
+ """
742
+ true_vals = {"1", "true", "yes", "on"}
743
+ cfg_val = get_value("grep_output_verbose")
744
+ if cfg_val is not None:
745
+ return str(cfg_val).strip().lower() in true_vals
746
+ return False
747
+
748
+
749
+ def get_protected_token_count():
750
+ """
751
+ Returns the user-configured protected token count for message history compaction.
752
+ This is the number of tokens in recent messages that won't be summarized.
753
+ Defaults to 50000 if unset or misconfigured.
754
+ Configurable by 'protected_token_count' key.
755
+ Enforces that protected tokens don't exceed 75% of model context length.
756
+ """
757
+ val = get_value("protected_token_count")
758
+ try:
759
+ # Get the model context length to enforce the 75% limit
760
+ model_context_length = get_model_context_length()
761
+ max_protected_tokens = int(model_context_length * 0.75)
762
+
763
+ # Parse the configured value
764
+ configured_value = int(val) if val else 50000
765
+
766
+ # Apply constraints: minimum 1000, maximum 75% of context length
767
+ return max(1000, min(configured_value, max_protected_tokens))
768
+ except ValueError, TypeError:
769
+ # If parsing fails, return a reasonable default that respects the 75% limit
770
+ model_context_length = get_model_context_length()
771
+ max_protected_tokens = int(model_context_length * 0.75)
772
+ return min(50000, max_protected_tokens)
773
+
774
+
775
+ def get_resume_message_count() -> int:
776
+ """
777
+ Returns the number of messages to display when resuming a session.
778
+ Defaults to 50 if unset or misconfigured.
779
+ Configurable by 'resume_message_count' key via /set command.
780
+
781
+ Example: /set resume_message_count=30
782
+ """
783
+ val = get_value("resume_message_count")
784
+ try:
785
+ configured_value = int(val) if val else 50
786
+ # Enforce reasonable bounds: minimum 1, maximum 100
787
+ return max(1, min(configured_value, 100))
788
+ except ValueError, TypeError:
789
+ return 50
790
+
791
+
792
+ def get_compaction_threshold():
793
+ """
794
+ Returns the user-configured compaction threshold as a float between 0.0 and 1.0.
795
+ This is the proportion of model context that triggers compaction.
796
+ Defaults to 0.85 (85%) if unset or misconfigured.
797
+ Configurable by 'compaction_threshold' key.
798
+ """
799
+ val = get_value("compaction_threshold")
800
+ try:
801
+ threshold = float(val) if val else 0.85
802
+ # Clamp between reasonable bounds
803
+ return max(0.5, min(0.95, threshold))
804
+ except ValueError, TypeError:
805
+ return 0.85
806
+
807
+
808
+ def get_compaction_strategy() -> str:
809
+ """
810
+ Returns the user-configured compaction strategy.
811
+ Options are 'summarization' or 'truncation'.
812
+ Defaults to 'summarization' if not set or misconfigured.
813
+ Configurable by 'compaction_strategy' key.
814
+ """
815
+ val = get_value("compaction_strategy")
816
+ if val and val.lower() in ["summarization", "truncation"]:
817
+ return val.lower()
818
+ # Default to summarization
819
+ return "truncation"
820
+
821
+
822
+ def get_http2() -> bool:
823
+ """
824
+ Get the http2 configuration value.
825
+ Returns False if not set (default).
826
+ """
827
+ val = get_value("http2")
828
+ if val is None:
829
+ return False
830
+ return str(val).lower() in ("1", "true", "yes", "on")
831
+
832
+
833
+ def set_http2(enabled: bool) -> None:
834
+ """
835
+ Sets the http2 configuration value.
836
+
837
+ Args:
838
+ enabled: Whether to enable HTTP/2 for httpx clients
839
+ """
840
+ set_config_value("http2", "true" if enabled else "false")
841
+
842
+
843
+ def get_message_limit(default: int = 1000) -> int:
844
+ """
845
+ Returns the user-configured message/request limit for the agent.
846
+ This controls how many steps/requests the agent can take.
847
+ Defaults to 1000 if unset or misconfigured.
848
+ Configurable by 'message_limit' key.
849
+ """
850
+ val = get_value("message_limit")
851
+ try:
852
+ return int(val) if val else default
853
+ except ValueError, TypeError:
854
+ return default
855
+
856
+
857
+ def save_command_to_history(command: str):
858
+ """Save a command to the history file with an ISO format timestamp.
859
+
860
+ Args:
861
+ command: The command to save
862
+ """
863
+ import datetime
864
+
865
+ try:
866
+ timestamp = datetime.datetime.now().isoformat(timespec="seconds")
867
+
868
+ # Sanitize command to remove any invalid surrogate characters
869
+ # that could cause encoding errors on Windows
870
+ try:
871
+ command = command.encode("utf-8", errors="surrogatepass").decode(
872
+ "utf-8", errors="replace"
873
+ )
874
+ except UnicodeEncodeError, UnicodeDecodeError:
875
+ # If that fails, do a more aggressive cleanup
876
+ command = "".join(
877
+ char if ord(char) < 0xD800 or ord(char) > 0xDFFF else "\ufffd"
878
+ for char in command
879
+ )
880
+
881
+ with open(
882
+ COMMAND_HISTORY_FILE, "a", encoding="utf-8", errors="surrogateescape"
883
+ ) as f:
884
+ f.write(f"\n# {timestamp}\n{command}\n")
885
+ except Exception as e:
886
+ from code_muse.messaging import emit_error
887
+
888
+ emit_error(
889
+ f"An unexpected error occurred while saving command history: {str(e)}"
890
+ )
891
+
892
+
893
+ def get_auto_save_session() -> bool:
894
+ """
895
+ Checks muse.cfg for 'auto_save_session' (case-insensitive in value only).
896
+ Defaults to True if not set.
897
+ Allowed values for ON: 1, '1', 'true', 'yes', 'on' (all case-insensitive for value).
898
+ """
899
+ true_vals = {"1", "true", "yes", "on"}
900
+ cfg_val = get_value("auto_save_session")
901
+ if cfg_val is not None:
902
+ return str(cfg_val).strip().lower() in true_vals
903
+ return True
904
+
905
+
906
+ def set_auto_save_session(enabled: bool):
907
+ """Sets the auto_save_session configuration value.
908
+
909
+ Args:
910
+ enabled: Whether to enable auto-saving of sessions
911
+ """
912
+ set_config_value("auto_save_session", "true" if enabled else "false")
913
+
914
+
915
+ def get_max_saved_sessions() -> int:
916
+ """
917
+ Gets the maximum number of sessions to keep.
918
+ Defaults to 20 if not set.
919
+ """
920
+ cfg_val = get_value("max_saved_sessions")
921
+ if cfg_val is not None:
922
+ try:
923
+ val = int(cfg_val)
924
+ return max(0, val) # Ensure non-negative
925
+ except ValueError, TypeError:
926
+ pass
927
+ return 20
928
+
929
+
930
+ def set_max_saved_sessions(max_sessions: int):
931
+ """Sets the max_saved_sessions configuration value.
932
+
933
+ Args:
934
+ max_sessions: Maximum number of sessions to keep (0 for unlimited)
935
+ """
936
+ set_config_value("max_saved_sessions", str(max_sessions))
937
+
938
+
939
+ # Defaults for diff highlight colors — single source of truth.
940
+
941
+
942
+ # =============================================================================
943
+ # Banner Color Configuration
944
+ # =============================================================================
945
+
946
+ # Default banner colors (Rich color names)
947
+ # A beautiful jewel-tone palette with semantic meaning:
948
+ # - Blues/Teals: Reading & navigation (calm, informational)
949
+ # - Warm tones: Actions & changes (edits, shell commands)
950
+ # - Purples: AI thinking & reasoning (the "brain" colors)
951
+ # - Greens: Completions & success
952
+ # - Neutrals: Search & listings
953
+
954
+
955
+ def get_current_autosave_id() -> str:
956
+ """Get or create the current autosave session ID for this process."""
957
+ global _CURRENT_AUTOSAVE_ID
958
+ if not _CURRENT_AUTOSAVE_ID:
959
+ # Use a full timestamp so tests and UX can predict the name if needed
960
+ _CURRENT_AUTOSAVE_ID = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
961
+ return _CURRENT_AUTOSAVE_ID
962
+
963
+
964
+ def rotate_autosave_id() -> str:
965
+ """Force a new autosave session ID and return it."""
966
+ global _CURRENT_AUTOSAVE_ID
967
+ _CURRENT_AUTOSAVE_ID = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
968
+ return _CURRENT_AUTOSAVE_ID
969
+
970
+
971
+ def get_current_autosave_session_name() -> str:
972
+ """Return the full session name used for autosaves (no file extension)."""
973
+ return f"auto_session_{get_current_autosave_id()}"
974
+
975
+
976
+ def set_current_autosave_from_session_name(session_name: str) -> str:
977
+ """Set the current autosave ID based on a full session name.
978
+
979
+ Accepts names like 'auto_session_YYYYMMDD_HHMMSS' and extracts the ID part.
980
+ Returns the ID that was set.
981
+ """
982
+ global _CURRENT_AUTOSAVE_ID
983
+ prefix = "auto_session_"
984
+ if session_name.startswith(prefix):
985
+ _CURRENT_AUTOSAVE_ID = session_name[len(prefix) :]
986
+ else:
987
+ _CURRENT_AUTOSAVE_ID = session_name
988
+ return _CURRENT_AUTOSAVE_ID
989
+
990
+
991
+ def auto_save_session_if_enabled() -> bool:
992
+ """Automatically save the current session if auto_save_session is enabled."""
993
+ if not get_auto_save_session():
994
+ return False
995
+
996
+ try:
997
+ import pathlib
998
+
999
+ from code_muse.agents.agent_manager import get_current_agent
1000
+ from code_muse.messaging import emit_info
1001
+
1002
+ current_agent = get_current_agent()
1003
+ history = current_agent.get_message_history()
1004
+ if not history:
1005
+ return False
1006
+
1007
+ now = datetime.datetime.now()
1008
+ session_name = get_current_autosave_session_name()
1009
+ autosave_dir = pathlib.Path(AUTOSAVE_DIR)
1010
+
1011
+ metadata = save_session(
1012
+ history=history,
1013
+ session_name=session_name,
1014
+ base_dir=autosave_dir,
1015
+ timestamp=now.isoformat(),
1016
+ token_estimator=current_agent.estimate_tokens_for_message,
1017
+ auto_saved=True,
1018
+ )
1019
+
1020
+ emit_info(
1021
+ f"[Done] Auto-saved session: {metadata.message_count} messages ({metadata.total_tokens} tokens)"
1022
+ )
1023
+
1024
+ return True
1025
+
1026
+ except Exception as exc: # pragma: no cover - defensive logging
1027
+ from code_muse.messaging import emit_error
1028
+
1029
+ emit_error(f"Failed to auto-save session: {exc}")
1030
+ return False
1031
+
1032
+
1033
+ def finalize_autosave_session() -> str:
1034
+ """Persist the current autosave snapshot and rotate to a fresh session."""
1035
+ auto_save_session_if_enabled()
1036
+ return rotate_autosave_id()
1037
+
1038
+
1039
+ # API Key management functions
1040
+
1041
+
1042
+ # --- FRONTEND EMITTER CONFIGURATION ---
1043
+ def get_frontend_emitter_enabled() -> bool:
1044
+ """Check if frontend emitter is enabled."""
1045
+ val = get_value("frontend_emitter_enabled")
1046
+ if val is None:
1047
+ return True # Enabled by default
1048
+ return str(val).lower() in ("1", "true", "yes", "on")
1049
+
1050
+
1051
+ def get_frontend_emitter_max_recent_events() -> int:
1052
+ """Get max number of recent events to buffer."""
1053
+ val = get_value("frontend_emitter_max_recent_events")
1054
+ if val is None:
1055
+ return 100
1056
+ try:
1057
+ return int(val)
1058
+ except ValueError:
1059
+ return 100
1060
+
1061
+
1062
+ def get_frontend_emitter_queue_size() -> int:
1063
+ """Get max subscriber queue size."""
1064
+ val = get_value("frontend_emitter_queue_size")
1065
+ if val is None:
1066
+ return 100
1067
+ try:
1068
+ return int(val)
1069
+ except ValueError:
1070
+ return 100
1071
+
1072
+
1073
+ # Re-exports from config submodules (kept at bottom to avoid circular imports)
1074
+ from code_muse.config_agent import ( # noqa: E402,F401
1075
+ PACK_AGENT_NAMES,
1076
+ UC_AGENT_NAMES,
1077
+ clear_agent_pinned_model,
1078
+ get_agent_pinned_model,
1079
+ get_agents_pinned_to_model,
1080
+ get_all_agent_pinned_models,
1081
+ get_default_agent,
1082
+ get_pack_agents_enabled,
1083
+ get_project_agents_directory,
1084
+ get_universal_constructor_enabled,
1085
+ get_user_agents_directory,
1086
+ set_agent_pinned_model,
1087
+ set_default_agent,
1088
+ set_universal_constructor_enabled,
1089
+ )
1090
+ from code_muse.config_appearance import ( # noqa: E402,F401
1091
+ _DEFAULT_DIFF_ADDITION_HEX,
1092
+ _DEFAULT_DIFF_DELETION_HEX,
1093
+ DEFAULT_BANNER_COLORS,
1094
+ _coerce_to_hex,
1095
+ get_all_banner_colors,
1096
+ get_banner_color,
1097
+ get_diff_addition_color,
1098
+ get_diff_context_lines,
1099
+ get_diff_deletion_color,
1100
+ get_suppress_informational_messages,
1101
+ get_suppress_thinking_messages,
1102
+ reset_all_banner_colors,
1103
+ reset_banner_color,
1104
+ set_banner_color,
1105
+ set_diff_addition_color,
1106
+ set_diff_deletion_color,
1107
+ set_diff_highlight_style,
1108
+ set_suppress_informational_messages,
1109
+ set_suppress_thinking_messages,
1110
+ )
1111
+ from code_muse.config_model import ( # noqa: E402,F401
1112
+ _sanitize_model_name_for_key,
1113
+ clear_model_settings,
1114
+ get_all_model_settings,
1115
+ get_effective_model_settings,
1116
+ get_effective_seed,
1117
+ get_effective_temperature,
1118
+ get_effective_top_p,
1119
+ get_model_setting,
1120
+ get_openai_reasoning_effort,
1121
+ get_openai_reasoning_summary,
1122
+ get_openai_verbosity,
1123
+ get_muse_token,
1124
+ get_summarization_model_name,
1125
+ get_temperature,
1126
+ set_model_setting,
1127
+ set_openai_reasoning_effort,
1128
+ set_openai_reasoning_summary,
1129
+ set_openai_verbosity,
1130
+ set_muse_token,
1131
+ set_summarization_model_name,
1132
+ set_temperature,
1133
+ )
1134
+ from code_muse.config_security import ( # noqa: E402,F401
1135
+ get_api_key,
1136
+ load_api_keys_to_environment,
1137
+ set_api_key,
1138
+ )