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,75 @@
1
+ import logging
2
+ from fnmatch import fnmatch
3
+
4
+ from code_muse.plugins.policy_engine.policy_toml_schema import Decision, ToolRule
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def _tool_name_matches(tool_name: str, rule_tool_name: str) -> bool:
10
+ if rule_tool_name == "*":
11
+ return True
12
+ # Support fnmatch wildcards (e.g., "agent_run_*")
13
+ return fnmatch(tool_name, rule_tool_name)
14
+
15
+
16
+ def evaluate_policy(
17
+ tool_name: str,
18
+ command: str | None,
19
+ rules: list[ToolRule],
20
+ ) -> tuple[Decision, ToolRule | None]:
21
+ matched: list[ToolRule] = []
22
+ for rule in rules:
23
+ if not _tool_name_matches(tool_name, rule.tool_name):
24
+ continue
25
+ if rule.command_prefix is not None:
26
+ if command is None:
27
+ # command_prefix specified but no command provided — skip
28
+ continue
29
+ if not command.startswith(rule.command_prefix):
30
+ continue
31
+ matched.append(rule)
32
+
33
+ if not matched:
34
+ logger.debug(
35
+ "Policy: no rules matched for tool=%s command=%s — default ALLOW",
36
+ tool_name,
37
+ command,
38
+ )
39
+ return (Decision.ALLOW, None)
40
+
41
+ # Sort by priority descending
42
+ matched.sort(key=lambda r: r.priority, reverse=True)
43
+ highest_priority = matched[0].priority
44
+ top_rules = [r for r in matched if r.priority == highest_priority]
45
+
46
+ if len(top_rules) > 1:
47
+ decisions = {r.decision for r in top_rules}
48
+ if len(decisions) > 1:
49
+ logger.warning(
50
+ "Policy conflict: %d rules at priority %d with different decisions "
51
+ "for tool=%s command=%s. Using first registered: %s",
52
+ len(top_rules),
53
+ highest_priority,
54
+ tool_name,
55
+ command,
56
+ top_rules[0],
57
+ )
58
+
59
+ chosen = top_rules[0]
60
+ logger.debug(
61
+ "Policy: matched rule for tool=%s command=%s → %s (priority=%d, source=%s)",
62
+ tool_name,
63
+ command,
64
+ chosen.decision.value,
65
+ chosen.priority,
66
+ chosen.source,
67
+ )
68
+ return (chosen.decision, chosen)
69
+
70
+
71
+ def evaluate_tool_policy(
72
+ tool_name: str,
73
+ rules: list[ToolRule],
74
+ ) -> tuple[Decision, ToolRule | None]:
75
+ return evaluate_policy(tool_name, None, rules)
@@ -0,0 +1,90 @@
1
+ import logging
2
+ import os
3
+ from pathlib import Path
4
+
5
+ from code_muse.plugins.policy_engine.policy_toml_schema import (
6
+ ToolRule,
7
+ parse_policy_toml,
8
+ )
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # Simple cache: list of rules + dict of file mtimes
13
+ _rule_cache: list[ToolRule] | None = None
14
+ _file_mtimes: dict[str, float] = {}
15
+
16
+
17
+ def _get_user_policies_dir() -> Path:
18
+ return Path.home() / ".muse" / "policies"
19
+
20
+
21
+ def _get_project_policies_dir() -> Path:
22
+ return Path(os.getcwd()) / ".muse" / "policies"
23
+
24
+
25
+ def discover_policy_files() -> list[Path]:
26
+ files: list[Path] = []
27
+ for directory in (_get_user_policies_dir(), _get_project_policies_dir()):
28
+ if not directory.exists():
29
+ continue
30
+ try:
31
+ toml_files = sorted(directory.glob("*.toml"))
32
+ except OSError as exc:
33
+ logger.warning("Cannot scan policy directory %s: %s", directory, exc)
34
+ continue
35
+ for f in toml_files:
36
+ if not f.is_file():
37
+ continue
38
+ try:
39
+ # Check readability
40
+ _ = f.stat()
41
+ except OSError as exc:
42
+ logger.warning("Unreadable policy file %s: %s", f, exc)
43
+ continue
44
+ files.append(f)
45
+ return files
46
+
47
+
48
+ def _files_changed(files: list[Path]) -> bool:
49
+ global _file_mtimes
50
+ current: dict[str, float] = {}
51
+ for f in files:
52
+ try:
53
+ current[str(f)] = f.stat().st_mtime
54
+ except OSError:
55
+ return True
56
+ return current != _file_mtimes
57
+
58
+
59
+ def load_all_policies(force_reload: bool = False) -> list[ToolRule]:
60
+ global _rule_cache, _file_mtimes
61
+
62
+ files = discover_policy_files()
63
+
64
+ if not force_reload and _rule_cache is not None and not _files_changed(files):
65
+ return _rule_cache
66
+
67
+ all_rules: list[ToolRule] = []
68
+ new_mtimes: dict[str, float] = {}
69
+
70
+ for f in files:
71
+ try:
72
+ rules = parse_policy_toml(f)
73
+ all_rules.extend(rules)
74
+ new_mtimes[str(f)] = f.stat().st_mtime
75
+ except ValueError as exc:
76
+ logger.warning("Skipping invalid policy file %s: %s", f, exc)
77
+ except OSError as exc:
78
+ logger.warning("Cannot read policy file %s: %s", f, exc)
79
+
80
+ _rule_cache = all_rules
81
+ _file_mtimes = new_mtimes
82
+ logger.info("Loaded %d policy rules from %d file(s)", len(all_rules), len(files))
83
+ return all_rules
84
+
85
+
86
+ def clear_policy_cache() -> None:
87
+ global _rule_cache, _file_mtimes
88
+ _rule_cache = None
89
+ _file_mtimes = {}
90
+ logger.info("Policy cache cleared")
@@ -0,0 +1,115 @@
1
+ import logging
2
+ from dataclasses import dataclass, field
3
+ from enum import Enum
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class Decision(Enum):
11
+ ALLOW = "allow"
12
+ DENY = "deny"
13
+ ASK_USER = "ask_user"
14
+
15
+
16
+ @dataclass
17
+ class ToolRule:
18
+ tool_name: str
19
+ decision: Decision
20
+ command_prefix: str | None = None
21
+ priority: int = 0
22
+ description: str = ""
23
+ source: str = field(default="", repr=False)
24
+
25
+
26
+ def _warn_unknown_fields(rule_table: dict[str, Any], path: str | Path) -> None:
27
+ known = {"toolName", "commandPrefix", "decision", "priority", "description"}
28
+ unknown = set(rule_table.keys()) - known
29
+ if unknown:
30
+ logger.warning("Unknown fields in rule from %s: %s", path, ", ".join(unknown))
31
+
32
+
33
+ def parse_policy_toml(path: str | Path) -> list[ToolRule]:
34
+ path = Path(path)
35
+ import tomllib
36
+
37
+ raw = path.read_text(encoding="utf-8")
38
+ data = tomllib.loads(raw)
39
+
40
+ schema_version = data.get("schema_version")
41
+ if schema_version is not None and str(schema_version) != "1":
42
+ logger.warning(
43
+ "Policy file %s has schema_version %s; expected 1. "
44
+ "Forward compatibility assumed.",
45
+ path,
46
+ schema_version,
47
+ )
48
+
49
+ rules: list[ToolRule] = []
50
+ rule_tables = data.get("rule", [])
51
+ if isinstance(rule_tables, dict):
52
+ # Single [[rule]] can be parsed as dict by some TOML libs
53
+ rule_tables = [rule_tables]
54
+
55
+ for idx, rule_table in enumerate(rule_tables):
56
+ if not isinstance(rule_table, dict):
57
+ raise ValueError(
58
+ f"Invalid rule entry at index {idx} in {path}: expected table, got {type(rule_table).__name__}"
59
+ )
60
+
61
+ _warn_unknown_fields(rule_table, path)
62
+
63
+ tool_name = rule_table.get("toolName")
64
+ if tool_name is None or not isinstance(tool_name, str) or not tool_name.strip():
65
+ raise ValueError(f"Rule {idx + 1} in {path}: missing or invalid 'toolName'")
66
+
67
+ decision_str = rule_table.get("decision")
68
+ if decision_str is None or not isinstance(decision_str, str):
69
+ raise ValueError(f"Rule {idx + 1} in {path}: missing or invalid 'decision'")
70
+ try:
71
+ decision = Decision(decision_str)
72
+ except ValueError as exc:
73
+ raise ValueError(
74
+ f"Rule {idx + 1} in {path}: invalid decision '{decision_str}'. "
75
+ f"Must be one of: {', '.join(d.value for d in Decision)}"
76
+ ) from exc
77
+
78
+ command_prefix = rule_table.get("commandPrefix")
79
+ if command_prefix is not None and not isinstance(command_prefix, str):
80
+ raise ValueError(
81
+ f"Rule {idx + 1} in {path}: 'commandPrefix' must be a string"
82
+ )
83
+
84
+ priority = rule_table.get("priority", 0)
85
+ if not isinstance(priority, int):
86
+ raise ValueError(f"Rule {idx + 1} in {path}: 'priority' must be an integer")
87
+
88
+ description = rule_table.get("description", "")
89
+ if description is not None and not isinstance(description, str):
90
+ raise ValueError(
91
+ f"Rule {idx + 1} in {path}: 'description' must be a string"
92
+ )
93
+
94
+ rules.append(
95
+ ToolRule(
96
+ tool_name=tool_name,
97
+ decision=decision,
98
+ command_prefix=command_prefix if command_prefix else None,
99
+ priority=priority,
100
+ description=description,
101
+ source=str(path),
102
+ )
103
+ )
104
+
105
+ return rules
106
+
107
+
108
+ def validate_rules(rules: list[ToolRule]) -> None:
109
+ for rule in rules:
110
+ if not rule.tool_name or not rule.tool_name.strip():
111
+ raise ValueError(f"Invalid rule: tool_name is empty (from {rule.source})")
112
+ if rule.decision == Decision.DENY and not rule.tool_name.strip():
113
+ raise ValueError(
114
+ f"Invalid rule: deny with empty tool_name (from {rule.source})"
115
+ )
@@ -0,0 +1,112 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ from code_muse.callbacks import register_callback
5
+ from code_muse.messaging import emit_info
6
+ from code_muse.plugins.policy_engine.approval_flow_integration import (
7
+ integrate_policy_check,
8
+ )
9
+ from code_muse.plugins.policy_engine.policy_file_discovery import (
10
+ clear_policy_cache,
11
+ load_all_policies,
12
+ )
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Load policies at import time so they're available immediately.
17
+ # This is safe because load_all_policies caches results and logs warnings
18
+ # for invalid files rather than raising.
19
+ _INITIALIZED_RULES = load_all_policies()
20
+ logger.info(
21
+ "Policy engine initialized with %d rule(s)",
22
+ len(_INITIALIZED_RULES),
23
+ )
24
+
25
+
26
+ async def _on_run_shell_command(
27
+ context: Any,
28
+ command: str,
29
+ cwd: str | None = None,
30
+ timeout: int = 60,
31
+ ) -> dict[str, Any | None]:
32
+ """Policy check for shell commands.
33
+
34
+ Returns:
35
+ - {"auto_approve": True} if policy says ALLOW
36
+ - {"blocked": True, "error_message": "..."} if policy says DENY
37
+ - None for ASK_USER or no match (normal confirmation flow)
38
+ """
39
+ return integrate_policy_check("agent_run_shell_command", command)
40
+
41
+
42
+ async def _on_pre_tool_call(
43
+ tool_name: str,
44
+ tool_args: dict,
45
+ context: Any = None,
46
+ ) -> dict[str, Any | None]:
47
+ """Policy check for all tool calls.
48
+
49
+ Returns:
50
+ - {"blocked": True, "error_message": "..."} if policy says DENY
51
+ - None otherwise (ALLOW, ASK_USER, or no match)
52
+ """
53
+ result = integrate_policy_check(tool_name, None)
54
+ if result and result.get("blocked"):
55
+ return result
56
+ return None
57
+
58
+
59
+ def _on_custom_command(command: str, name: str) -> Any:
60
+ """Handle /policies and /policies reload commands."""
61
+ if name == "policies":
62
+ parts = command.split(maxsplit=1)
63
+ subcommand = parts[1].strip().lower() if len(parts) > 1 else ""
64
+
65
+ if subcommand == "reload":
66
+ clear_policy_cache()
67
+ rules = load_all_policies(force_reload=True)
68
+ emit_info(f"[Run] Policy rules reloaded: {len(rules)} active rule(s)")
69
+ return True
70
+
71
+ # Default /policies — list loaded rules
72
+ rules = load_all_policies()
73
+ if not rules:
74
+ emit_info("[Warn] No policy rules loaded.")
75
+ return True
76
+
77
+ lines: list[str] = ["[Run] Active rules:"]
78
+ seen_sources: set[str] = set()
79
+ for rule in rules:
80
+ prefix = f" [{rule.priority}] {rule.tool_name}"
81
+ if rule.command_prefix:
82
+ prefix += f" (prefix='{rule.command_prefix}')"
83
+ prefix += f" → {rule.decision.value}"
84
+ if rule.description:
85
+ prefix += f" — {rule.description}"
86
+ lines.append(prefix)
87
+ if rule.source:
88
+ seen_sources.add(rule.source)
89
+
90
+ if seen_sources:
91
+ lines.append("")
92
+ lines.append("Sources:")
93
+ for src in sorted(seen_sources):
94
+ lines.append(f" • {src}")
95
+
96
+ emit_info("\n".join(lines))
97
+ return True
98
+
99
+ return None
100
+
101
+
102
+ def _on_custom_command_help() -> list[tuple[str, str]]:
103
+ return [
104
+ ("policies", "List loaded policy rules"),
105
+ ("policies reload", "Reload policy rules from disk"),
106
+ ]
107
+
108
+
109
+ register_callback("run_shell_command", _on_run_shell_command, priority=50)
110
+ register_callback("pre_tool_call", _on_pre_tool_call, priority=50)
111
+ register_callback("custom_command", _on_custom_command)
112
+ register_callback("custom_command_help", _on_custom_command_help)
@@ -0,0 +1 @@
1
+ """/pop command plugin."""
@@ -0,0 +1,189 @@
1
+ """Plugin that adds /pop for trimming recent conversation history."""
2
+
3
+ from typing import Any
4
+
5
+ from code_muse.callbacks import register_callback
6
+
7
+
8
+ def emit_error(message: Any) -> None:
9
+ from code_muse.messaging import emit_error as _emit_error
10
+
11
+ _emit_error(message)
12
+
13
+
14
+ def emit_info(message: Any) -> None:
15
+ from code_muse.messaging import emit_info as _emit_info
16
+
17
+ _emit_info(message)
18
+
19
+
20
+ def emit_success(message: Any) -> None:
21
+ from code_muse.messaging import emit_success as _emit_success
22
+
23
+ _emit_success(message)
24
+
25
+
26
+ def emit_warning(message: Any) -> None:
27
+ from code_muse.messaging import emit_warning as _emit_warning
28
+
29
+ _emit_warning(message)
30
+
31
+
32
+ def _custom_help() -> list[tuple[str, str]]:
33
+ return [
34
+ (
35
+ "pop",
36
+ "Delete the N most-recent messages and prune broken tool-call fragments",
37
+ )
38
+ ]
39
+
40
+
41
+ def _has_only_tool_returns(message: Any) -> bool:
42
+ """Return True when a request message contains only tool-return parts."""
43
+ try:
44
+ from pydantic_ai.messages import ModelRequest, ToolReturnPart
45
+
46
+ if not isinstance(message, ModelRequest):
47
+ return False
48
+
49
+ parts = getattr(message, "parts", []) or []
50
+ return bool(parts) and all(isinstance(part, ToolReturnPart) for part in parts)
51
+ except Exception:
52
+ return False
53
+
54
+
55
+ def _has_unresolved_tool_calls(message: Any) -> bool:
56
+ """Return True when a response message still contains unresolved tool calls."""
57
+ try:
58
+ from pydantic_ai.messages import ModelResponse, ToolCallPart
59
+
60
+ if not isinstance(message, ModelResponse):
61
+ return False
62
+
63
+ parts = getattr(message, "parts", []) or []
64
+ return any(isinstance(part, ToolCallPart) for part in parts)
65
+ except Exception:
66
+ return False
67
+
68
+
69
+ def _prune_dangling_tool_fragments(history: list[Any]) -> tuple[list[Any], int]:
70
+ """Strip incomplete tool-call sequences from the tail of history."""
71
+ pruned = 0
72
+
73
+ while history:
74
+ tail = history[-1]
75
+ if _has_only_tool_returns(tail):
76
+ history.pop()
77
+ pruned += 1
78
+ continue
79
+
80
+ if _has_unresolved_tool_calls(tail):
81
+ history.pop()
82
+ pruned += 1
83
+ continue
84
+
85
+ break
86
+
87
+ return history, pruned
88
+
89
+
90
+ def _parse_pop_count(command: str) -> int | None:
91
+ tokens = command.split()
92
+ if len(tokens) < 2:
93
+ return 1
94
+
95
+ try:
96
+ count = int(tokens[1])
97
+ except ValueError:
98
+ emit_error(f"/pop: '{tokens[1]}' is not a valid integer – usage: /pop [N]")
99
+ return None
100
+
101
+ if count < 1:
102
+ emit_error("/pop: N must be a positive integer")
103
+ return None
104
+
105
+ return count
106
+
107
+
108
+ def _handle_pop_command(command: str) -> bool:
109
+ from code_muse.agents.agent_manager import get_current_agent
110
+
111
+ count = _parse_pop_count(command)
112
+ if count is None:
113
+ return True
114
+
115
+ try:
116
+ agent = get_current_agent()
117
+ except Exception as exc:
118
+ emit_error(f"/pop: could not get current agent – {exc}")
119
+ return True
120
+
121
+ history: list[Any] = list(agent.get_message_history())
122
+ if not history:
123
+ emit_warning("/pop: conversation history is empty – nothing to remove")
124
+ return True
125
+
126
+ poppable = len(history) - 1
127
+ if poppable <= 0:
128
+ emit_warning("/pop: only the system prompt is in history – nothing to remove")
129
+ return True
130
+
131
+ if count > poppable:
132
+ emit_warning(
133
+ f"/pop: requested {count} but only {poppable} message(s) can be removed "
134
+ f"(the system prompt is always preserved). Removing {poppable}."
135
+ )
136
+ count = poppable
137
+
138
+ before_count = len(history)
139
+ history = history[: before_count - count]
140
+ history, extra_pruned = _prune_dangling_tool_fragments(history)
141
+ after_count = len(history)
142
+ total_removed = before_count - after_count
143
+
144
+ try:
145
+ agent.set_message_history(history)
146
+ except Exception as exc:
147
+ emit_error(f"/pop: failed to update message history – {exc}")
148
+ return True
149
+
150
+ summary_parts = [f":scissors: Popped {count} message(s)"]
151
+ if extra_pruned:
152
+ summary_parts.append(
153
+ f"and pruned {extra_pruned} extra incomplete tool-call fragment(s)"
154
+ )
155
+
156
+ remaining = max(after_count - 1, 0)
157
+ emit_success(
158
+ " ".join(summary_parts)
159
+ + ".\n"
160
+ + f":scroll: History: {before_count - 1} → {remaining} message(s) "
161
+ + f"(excluding system prompt, removed {total_removed} total)"
162
+ )
163
+
164
+ if after_count <= 1:
165
+ emit_info(":bulb: History is now empty (system prompt only). Starting fresh!")
166
+
167
+ return True
168
+
169
+
170
+ def _handle_custom_command(command: str, name: str) -> bool | None:
171
+ if name != "pop":
172
+ return None
173
+
174
+ return _handle_pop_command(command)
175
+
176
+
177
+ register_callback("custom_command_help", _custom_help)
178
+ register_callback("custom_command", _handle_custom_command)
179
+
180
+
181
+ __all__ = [
182
+ "_custom_help",
183
+ "_handle_custom_command",
184
+ "_handle_pop_command",
185
+ "_parse_pop_count",
186
+ "_prune_dangling_tool_fragments",
187
+ "_has_only_tool_returns",
188
+ "_has_unresolved_tool_calls",
189
+ ]
@@ -0,0 +1,13 @@
1
+ """Plugin: drop user input onto a fresh line below the prompt chrome.
2
+
3
+ When enabled, transforms
4
+
5
+ [Run] prompt [agent] [model] (~/very/long/cwd) >>> typed text
6
+
7
+ into
8
+
9
+ [Run] prompt [agent] [model] (~/very/long/cwd) >>>
10
+ typed text
11
+
12
+ Toggle at runtime with ``/prompt_newline [on|off]``. Persisted in muse.cfg.
13
+ """
@@ -0,0 +1,19 @@
1
+ """Plugin-local config for the prompt_newline plugin."""
2
+
3
+ from code_muse.config import get_value, set_config_value
4
+
5
+ _CONFIG_KEY = "prompt_newline"
6
+ _TRUTHY = ("true", "1", "yes", "on")
7
+
8
+
9
+ def is_enabled() -> bool:
10
+ """Return True if the prompt_newline hack is enabled. Default: False."""
11
+ cfg_val = get_value(_CONFIG_KEY)
12
+ if cfg_val is None:
13
+ return False
14
+ return str(cfg_val).strip().lower() in _TRUTHY
15
+
16
+
17
+ def set_enabled(enabled: bool) -> None:
18
+ """Persist the on/off switch to muse.cfg."""
19
+ set_config_value(_CONFIG_KEY, "true" if enabled else "false")