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,1401 @@
1
+ import asyncio
2
+ import atexit
3
+ import ctypes
4
+ import os
5
+ import select
6
+ import signal
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ import threading
11
+ import time
12
+ import traceback
13
+ from collections import deque
14
+ from collections.abc import Callable
15
+
16
+ # FREE-THREADED: ThreadPoolExecutor is compatible with free-threaded Python 3.14 —
17
+ # no GIL contention for I/O-bound shell command work.
18
+ from concurrent.futures import ThreadPoolExecutor
19
+ from contextlib import contextmanager, suppress
20
+ from functools import partial
21
+ from typing import Literal
22
+
23
+ from pydantic import BaseModel
24
+ from pydantic_ai import RunContext
25
+ from rich.text import Text
26
+
27
+ from code_muse.messaging import ( # Structured messaging types
28
+ AgentReasoningMessage,
29
+ ShellOutputMessage,
30
+ ShellStartMessage,
31
+ emit_error,
32
+ emit_info,
33
+ emit_shell_line,
34
+ emit_warning,
35
+ get_message_bus,
36
+ )
37
+ from code_muse.tools.background_jobs import register_background_job
38
+ from code_muse.tools.common import generate_group_id, get_user_approval_async
39
+ from code_muse.tools.subagent_context import is_subagent
40
+
41
+ # Maximum line length for shell command output to prevent massive token usage
42
+ # This helps avoid exceeding model context limits when commands produce very long lines
43
+ MAX_LINE_LENGTH = 256
44
+
45
+
46
+ def _truncate_line(line: str) -> str:
47
+ """Truncate a line to MAX_LINE_LENGTH if it exceeds the limit."""
48
+ if len(line) > MAX_LINE_LENGTH:
49
+ return line[:MAX_LINE_LENGTH] + "... [truncated]"
50
+ return line
51
+
52
+
53
+ # Windows-specific: Check if pipe has data available without blocking
54
+ # This is needed because select() doesn't work on pipes on Windows
55
+ if sys.platform.startswith("win"):
56
+ import msvcrt
57
+
58
+ # Load kernel32 for PeekNamedPipe
59
+ _kernel32 = ctypes.windll.kernel32
60
+
61
+ def _win32_pipe_has_data(pipe) -> bool:
62
+ """Check if a Windows pipe has data available without blocking.
63
+
64
+ Uses PeekNamedPipe from kernel32.dll to check if there's data
65
+ in the pipe buffer without actually reading it.
66
+
67
+ Args:
68
+ pipe: A file object with a fileno() method (e.g., process.stdout)
69
+
70
+ Returns:
71
+ True if data is available, False otherwise (including on error)
72
+ """
73
+ try:
74
+ # Get the Windows handle from the file descriptor
75
+ handle = msvcrt.get_osfhandle(pipe.fileno())
76
+
77
+ # PeekNamedPipe parameters:
78
+ # - hNamedPipe: handle to the pipe
79
+ # - lpBuffer: buffer to receive data (NULL = don't read)
80
+ # - nBufferSize: size of buffer (0 = don't read)
81
+ # - lpBytesRead: receives bytes read (NULL)
82
+ # - lpTotalBytesAvail: receives total bytes available
83
+ # - lpBytesLeftThisMessage: receives bytes left (NULL)
84
+ bytes_available = ctypes.c_ulong(0)
85
+
86
+ result = _kernel32.PeekNamedPipe(
87
+ handle,
88
+ None, # Don't read data
89
+ 0, # Buffer size 0
90
+ None, # Don't care about bytes read
91
+ ctypes.byref(bytes_available), # Get bytes available
92
+ None, # Don't care about bytes left in message
93
+ )
94
+
95
+ if result:
96
+ return bytes_available.value > 0
97
+ return False
98
+ except ValueError, OSError, ctypes.ArgumentError:
99
+ # Handle closed, invalid, or other errors
100
+ return False
101
+ else:
102
+ # POSIX stub - not used, but keeps the code clean
103
+ def _win32_pipe_has_data(pipe) -> bool:
104
+ return False
105
+
106
+
107
+ _AWAITING_USER_INPUT = threading.Event()
108
+
109
+ # FREE-THREADED: _CONFIRMATION_LOCK guards sync shell confirmation prompts.
110
+ _CONFIRMATION_LOCK = threading.Lock()
111
+
112
+ # Track running shell processes so we can kill them on Ctrl-C from the UI
113
+ _RUNNING_PROCESSES: set[subprocess.Popen] = set()
114
+ # FREE-THREADED: _RUNNING_PROCESSES_LOCK guards sync process set mutations.
115
+ _RUNNING_PROCESSES_LOCK = threading.Lock()
116
+ _USER_KILLED_PROCESSES = set()
117
+
118
+ # Global state for shell command keyboard handling
119
+ _SHELL_CTRL_X_STOP_EVENT: threading.Event | None = None
120
+ _SHELL_CTRL_X_THREAD: threading.Thread | None = None
121
+ _ORIGINAL_SIGINT_HANDLER = None
122
+
123
+ # Reference-counted keyboard context - stays active while ANY command is running
124
+ _KEYBOARD_CONTEXT_REFCOUNT = 0
125
+ # FREE-THREADED: _KEYBOARD_CONTEXT_LOCK guards sync keyboard refcount mutations.
126
+ _KEYBOARD_CONTEXT_LOCK = threading.Lock()
127
+
128
+ # Thread-safe registry of active stop events for concurrent shell commands
129
+ _ACTIVE_STOP_EVENTS: set[threading.Event] = set()
130
+ # FREE-THREADED: _ACTIVE_STOP_EVENTS_LOCK guards sync stop-event registry.
131
+ _ACTIVE_STOP_EVENTS_LOCK = threading.Lock()
132
+
133
+ # Thread pool for running blocking shell commands without blocking the event loop.
134
+ # FREE-THREADED: ThreadPoolExecutor is compatible with free-threaded Python 3.14 —
135
+ # no GIL contention for I/O-bound subprocess work. This allows multiple
136
+ # sub-agents to run shell commands in parallel.
137
+ _SHELL_EXECUTOR = ThreadPoolExecutor(max_workers=16, thread_name_prefix="shell_cmd_")
138
+
139
+
140
+ def _register_process(proc: subprocess.Popen) -> None:
141
+ with _RUNNING_PROCESSES_LOCK:
142
+ _RUNNING_PROCESSES.add(proc)
143
+
144
+
145
+ def _unregister_process(proc: subprocess.Popen) -> None:
146
+ with _RUNNING_PROCESSES_LOCK:
147
+ _RUNNING_PROCESSES.discard(proc)
148
+
149
+
150
+ def _kill_process_group(proc: subprocess.Popen) -> None:
151
+ """Attempt to aggressively terminate a process and its group.
152
+
153
+ Cross-platform best-effort. On POSIX, uses process groups. On Windows, tries taskkill with /T flag for tree kill.
154
+ """
155
+ try:
156
+ if sys.platform.startswith("win"):
157
+ # On Windows, use taskkill to kill the process tree
158
+ # /F = force, /T = kill tree (children), /PID = process ID
159
+ try:
160
+ import subprocess as sp
161
+
162
+ # Try taskkill first - more reliable on Windows
163
+ sp.run(
164
+ ["taskkill", "/F", "/T", "/PID", str(proc.pid)],
165
+ capture_output=True,
166
+ timeout=2,
167
+ check=False,
168
+ )
169
+ time.sleep(0.3)
170
+ except Exception:
171
+ # Fallback to Python's built-in methods
172
+ pass
173
+
174
+ # Double-check it's dead, if not use proc.kill()
175
+ if proc.poll() is None:
176
+ try:
177
+ proc.kill()
178
+ time.sleep(0.3)
179
+ except Exception:
180
+ pass
181
+ return
182
+
183
+ # POSIX
184
+ pid = proc.pid
185
+ try:
186
+ pgid = os.getpgid(pid)
187
+ os.killpg(pgid, signal.SIGTERM)
188
+ time.sleep(1.0)
189
+ if proc.poll() is None:
190
+ os.killpg(pgid, signal.SIGINT)
191
+ time.sleep(0.6)
192
+ if proc.poll() is None:
193
+ os.killpg(pgid, signal.SIGKILL)
194
+ time.sleep(0.5)
195
+ except OSError:
196
+ # Fall back to direct kill of the process
197
+ try:
198
+ if proc.poll() is None:
199
+ proc.kill()
200
+ except OSError:
201
+ pass
202
+
203
+ if proc.poll() is None:
204
+ # Last ditch attempt; may be unkillable zombie
205
+ try:
206
+ for _ in range(3):
207
+ os.kill(proc.pid, signal.SIGKILL)
208
+ time.sleep(0.2)
209
+ if proc.poll() is not None:
210
+ break
211
+ except Exception:
212
+ pass
213
+ except Exception as e:
214
+ emit_error(f"Kill process error: {e}")
215
+
216
+
217
+ def kill_all_running_shell_processes() -> int:
218
+ """Kill all currently tracked running shell processes and stop reader threads.
219
+
220
+ Returns the number of processes signaled.
221
+ """
222
+ # Signal all active reader threads to stop
223
+ with _ACTIVE_STOP_EVENTS_LOCK:
224
+ for evt in _ACTIVE_STOP_EVENTS:
225
+ evt.set()
226
+
227
+ procs: list[subprocess.Popen]
228
+ with _RUNNING_PROCESSES_LOCK:
229
+ procs = list(_RUNNING_PROCESSES)
230
+ count = 0
231
+ for p in procs:
232
+ try:
233
+ # Close pipes first to unblock readline()
234
+ try:
235
+ if p.stdout and not p.stdout.closed:
236
+ p.stdout.close()
237
+ if p.stderr and not p.stderr.closed:
238
+ p.stderr.close()
239
+ if p.stdin and not p.stdin.closed:
240
+ p.stdin.close()
241
+ except OSError, ValueError:
242
+ pass
243
+
244
+ if p.poll() is None:
245
+ _kill_process_group(p)
246
+ count += 1
247
+ _USER_KILLED_PROCESSES.add(p.pid)
248
+ finally:
249
+ _unregister_process(p)
250
+ return count
251
+
252
+
253
+ def shutdown() -> None:
254
+ """Gracefully shut down the command runner subsystem.
255
+
256
+ Kills any still-running shell processes and shuts down the internal
257
+ ThreadPoolExecutor. Called automatically at interpreter exit via
258
+ ``atexit``; callers may also invoke it explicitly during orderly
259
+ shutdown (e.g. from ``cli_runner.main_entry()``).
260
+ """
261
+ try:
262
+ killed = kill_all_running_shell_processes()
263
+ if killed:
264
+ emit_info(f"Shutdown killed {killed} running shell process(es)")
265
+ except Exception as exc:
266
+ emit_warning(f"Process cleanup during shutdown: {exc}")
267
+
268
+ try:
269
+ _SHELL_EXECUTOR.shutdown(wait=True, cancel_futures=True)
270
+ emit_info("Shell command executor shut down cleanly")
271
+ except Exception as exc:
272
+ emit_warning(f"Executor shutdown error: {exc}")
273
+
274
+
275
+ atexit.register(shutdown)
276
+
277
+
278
+ def get_running_shell_process_count() -> int:
279
+ """Return the number of currently-active shell processes being tracked."""
280
+ with _RUNNING_PROCESSES_LOCK:
281
+ alive = 0
282
+ stale: set[subprocess.Popen] = set()
283
+ for proc in _RUNNING_PROCESSES:
284
+ if proc.poll() is None:
285
+ alive += 1
286
+ else:
287
+ stale.add(proc)
288
+ for proc in stale:
289
+ _RUNNING_PROCESSES.discard(proc)
290
+ return alive
291
+
292
+
293
+ # Function to check if user input is awaited
294
+ def is_awaiting_user_input():
295
+ """Check if command_runner is waiting for user input."""
296
+ return _AWAITING_USER_INPUT.is_set()
297
+
298
+
299
+ # Function to set user input flag
300
+ def set_awaiting_user_input(awaiting=True):
301
+ """Set the flag indicating if user input is awaited."""
302
+ if awaiting:
303
+ _AWAITING_USER_INPUT.set()
304
+ else:
305
+ _AWAITING_USER_INPUT.clear()
306
+
307
+ # When we're setting this flag, also pause/resume all active spinners
308
+ if awaiting:
309
+ # Pause all active spinners (imported here to avoid circular imports)
310
+ try:
311
+ from code_muse.messaging.spinner import pause_all_spinners
312
+
313
+ pause_all_spinners()
314
+ except ImportError:
315
+ pass # Spinner functionality not available
316
+ else:
317
+ # Resume all active spinners
318
+ try:
319
+ from code_muse.messaging.spinner import resume_all_spinners
320
+
321
+ resume_all_spinners()
322
+ except ImportError:
323
+ pass # Spinner functionality not available
324
+
325
+
326
+ class ShellCommandOutput(BaseModel):
327
+ success: bool
328
+ command: str | None
329
+ error: str | None = ""
330
+ stdout: str | None
331
+ stderr: str | None
332
+ exit_code: int | None
333
+ execution_time: float | None
334
+ timeout: bool | None = False
335
+ user_interrupted: bool | None = False
336
+ user_feedback: str | None = None # User feedback when command is rejected
337
+ background: bool = False # True if command was run in background mode
338
+ log_file: str | None = None # Path to temp log file for background commands
339
+ pid: int | None = None # Process ID for background commands
340
+
341
+
342
+ class ShellSafetyAssessment(BaseModel):
343
+ """Assessment of shell command safety risks.
344
+
345
+ This model represents the structured output from the shell safety checker agent.
346
+ It provides a risk level classification and reasoning for that assessment.
347
+
348
+ Attributes:
349
+ risk: Risk level classification. Can be one of:
350
+ 'none' (completely safe), 'low' (minimal risk), 'medium' (moderate risk),
351
+ 'high' (significant risk), 'critical' (severe/destructive risk).
352
+ reasoning: Brief explanation (max 1-2 sentences) of why this risk level
353
+ was assigned. Should be concise and actionable.
354
+ is_fallback: Whether this assessment is a fallback due to parsing failure.
355
+ Fallback assessments are not cached to allow retry with fresh LLM responses.
356
+ """
357
+
358
+ risk: Literal["none", "low", "medium", "high", "critical"]
359
+ reasoning: str
360
+ is_fallback: bool = False
361
+
362
+
363
+ def _listen_for_ctrl_x_windows(
364
+ stop_event: threading.Event,
365
+ on_escape: Callable[[], None],
366
+ ) -> None:
367
+ """Windows-specific Ctrl-X listener."""
368
+ import msvcrt
369
+ import time
370
+
371
+ while not stop_event.is_set():
372
+ try:
373
+ if msvcrt.kbhit():
374
+ try:
375
+ # Try to read a character
376
+ # Note: msvcrt.getwch() returns unicode string on Windows
377
+ key = msvcrt.getwch()
378
+
379
+ # Check for Ctrl+X (\x18) or other interrupt keys
380
+ # Some terminals might not send \x18, so also check for 'x' with modifier
381
+ if key == "\x18": # Standard Ctrl+X
382
+ try:
383
+ on_escape()
384
+ except Exception:
385
+ emit_warning(
386
+ "Ctrl+X handler raised unexpectedly; Ctrl+C still works."
387
+ )
388
+ # Note: In some Windows terminals, Ctrl+X might not be captured
389
+ # Users can use Ctrl+C as alternative, which is handled by signal handler
390
+ except OSError, ValueError:
391
+ # kbhit/getwch can fail on Windows in certain terminal states
392
+ # Just continue, user can use Ctrl+C
393
+ pass
394
+ except Exception:
395
+ # Be silent about Windows listener errors - they're common
396
+ # User can use Ctrl+C as fallback
397
+ pass
398
+ time.sleep(0.05)
399
+
400
+
401
+ def _listen_for_ctrl_x_posix(
402
+ stop_event: threading.Event,
403
+ on_escape: Callable[[], None],
404
+ ) -> None:
405
+ """POSIX-specific Ctrl-X listener."""
406
+ import select
407
+ import sys
408
+ import termios
409
+ import tty
410
+
411
+ stdin = sys.stdin
412
+ try:
413
+ fd = stdin.fileno()
414
+ except AttributeError, ValueError, OSError:
415
+ return
416
+ try:
417
+ original_attrs = termios.tcgetattr(fd)
418
+ except Exception:
419
+ return
420
+
421
+ try:
422
+ tty.setcbreak(fd)
423
+ while not stop_event.is_set():
424
+ try:
425
+ read_ready, _, _ = select.select([stdin], [], [], 0.05)
426
+ except Exception:
427
+ break
428
+ if not read_ready:
429
+ continue
430
+ data = stdin.read(1)
431
+ if not data:
432
+ break
433
+ if data == "\x18": # Ctrl+X
434
+ try:
435
+ on_escape()
436
+ except Exception:
437
+ emit_warning(
438
+ "Ctrl+X handler raised unexpectedly; Ctrl+C still works."
439
+ )
440
+ finally:
441
+ termios.tcsetattr(fd, termios.TCSADRAIN, original_attrs)
442
+
443
+
444
+ def _spawn_ctrl_x_key_listener(
445
+ stop_event: threading.Event,
446
+ on_escape: Callable[[], None],
447
+ ) -> threading.Thread | None:
448
+ """Start a Ctrl+X key listener thread for CLI sessions."""
449
+ try:
450
+ import sys
451
+ except ImportError:
452
+ return None
453
+
454
+ stdin = getattr(sys, "stdin", None)
455
+ if stdin is None or not hasattr(stdin, "isatty"):
456
+ return None
457
+ try:
458
+ if not stdin.isatty():
459
+ return None
460
+ except Exception:
461
+ return None
462
+
463
+ def listener() -> None:
464
+ try:
465
+ if sys.platform.startswith("win"):
466
+ _listen_for_ctrl_x_windows(stop_event, on_escape)
467
+ else:
468
+ _listen_for_ctrl_x_posix(stop_event, on_escape)
469
+ except Exception:
470
+ emit_warning(
471
+ "Ctrl+X key listener stopped unexpectedly; press Ctrl+C to cancel."
472
+ )
473
+
474
+ thread = threading.Thread(
475
+ target=listener, name="shell-command-ctrl-x-listener", daemon=True
476
+ )
477
+ thread.start()
478
+ return thread
479
+
480
+
481
+ @contextmanager
482
+ def _shell_command_keyboard_context():
483
+ """Context manager to handle keyboard interrupts during shell command execution.
484
+
485
+ This context manager:
486
+ 1. Disables the agent's Ctrl-C handler (so it doesn't cancel the agent)
487
+ 2. Enables a Ctrl-X listener to kill the running shell process
488
+ 3. Restores the original Ctrl-C handler when done
489
+ """
490
+ global _SHELL_CTRL_X_STOP_EVENT, _SHELL_CTRL_X_THREAD, _ORIGINAL_SIGINT_HANDLER
491
+
492
+ # Handler for Ctrl-X: kill all running shell processes
493
+ def handle_ctrl_x_press() -> None:
494
+ emit_warning("\n🛑 Ctrl-X detected! Interrupting shell command...")
495
+ kill_all_running_shell_processes()
496
+
497
+ # Handler for Ctrl-C during shell execution: just kill the shell process, don't cancel agent
498
+ def shell_sigint_handler(_sig, _frame):
499
+ """During shell execution, Ctrl-C kills the shell but doesn't cancel the agent."""
500
+ emit_warning("\n🛑 Ctrl-C detected! Interrupting shell command...")
501
+ kill_all_running_shell_processes()
502
+
503
+ # Set up Ctrl-X listener
504
+ _SHELL_CTRL_X_STOP_EVENT = threading.Event()
505
+ _SHELL_CTRL_X_THREAD = _spawn_ctrl_x_key_listener(
506
+ _SHELL_CTRL_X_STOP_EVENT,
507
+ handle_ctrl_x_press,
508
+ )
509
+
510
+ # Replace SIGINT handler temporarily
511
+ try:
512
+ _ORIGINAL_SIGINT_HANDLER = signal.signal(signal.SIGINT, shell_sigint_handler)
513
+ except ValueError, OSError:
514
+ # Can't set signal handler (maybe not main thread?)
515
+ _ORIGINAL_SIGINT_HANDLER = None
516
+
517
+ try:
518
+ yield
519
+ finally:
520
+ # Clean up: stop Ctrl-X listener
521
+ if _SHELL_CTRL_X_STOP_EVENT:
522
+ _SHELL_CTRL_X_STOP_EVENT.set()
523
+
524
+ if _SHELL_CTRL_X_THREAD and _SHELL_CTRL_X_THREAD.is_alive():
525
+ with suppress(Exception):
526
+ _SHELL_CTRL_X_THREAD.join(timeout=0.2)
527
+
528
+ # Restore original SIGINT handler
529
+ if _ORIGINAL_SIGINT_HANDLER is not None:
530
+ with suppress(ValueError, OSError):
531
+ signal.signal(signal.SIGINT, _ORIGINAL_SIGINT_HANDLER)
532
+
533
+ # Clean up global state
534
+ _SHELL_CTRL_X_STOP_EVENT = None
535
+ _SHELL_CTRL_X_THREAD = None
536
+ _ORIGINAL_SIGINT_HANDLER = None
537
+
538
+
539
+ def _handle_ctrl_x_press() -> None:
540
+ """Handler for Ctrl-X: kill all running shell processes."""
541
+ emit_warning("\n🛑 Ctrl-X detected! Interrupting all shell commands...")
542
+ kill_all_running_shell_processes()
543
+
544
+
545
+ def _shell_sigint_handler(_sig, _frame):
546
+ """During shell execution, Ctrl-C kills all shells but doesn't cancel agent."""
547
+ emit_warning("\n🛑 Ctrl-C detected! Interrupting all shell commands...")
548
+ kill_all_running_shell_processes()
549
+
550
+
551
+ def _start_keyboard_listener() -> None:
552
+ """Start the Ctrl-X listener and install SIGINT handler.
553
+
554
+ Called when the first shell command starts.
555
+ """
556
+ global _SHELL_CTRL_X_STOP_EVENT, _SHELL_CTRL_X_THREAD, _ORIGINAL_SIGINT_HANDLER
557
+
558
+ # Set up Ctrl-X listener
559
+ _SHELL_CTRL_X_STOP_EVENT = threading.Event()
560
+ _SHELL_CTRL_X_THREAD = _spawn_ctrl_x_key_listener(
561
+ _SHELL_CTRL_X_STOP_EVENT,
562
+ _handle_ctrl_x_press,
563
+ )
564
+
565
+ # Replace SIGINT handler temporarily
566
+ try:
567
+ _ORIGINAL_SIGINT_HANDLER = signal.signal(signal.SIGINT, _shell_sigint_handler)
568
+ except ValueError, OSError:
569
+ # Can't set signal handler (maybe not main thread?)
570
+ _ORIGINAL_SIGINT_HANDLER = None
571
+
572
+
573
+ def _stop_keyboard_listener() -> None:
574
+ """Stop the Ctrl-X listener and restore SIGINT handler.
575
+
576
+ Called when the last shell command finishes.
577
+ """
578
+ global _SHELL_CTRL_X_STOP_EVENT, _SHELL_CTRL_X_THREAD, _ORIGINAL_SIGINT_HANDLER
579
+
580
+ # Clean up: stop Ctrl-X listener
581
+ if _SHELL_CTRL_X_STOP_EVENT:
582
+ _SHELL_CTRL_X_STOP_EVENT.set()
583
+
584
+ if _SHELL_CTRL_X_THREAD and _SHELL_CTRL_X_THREAD.is_alive():
585
+ with suppress(Exception):
586
+ _SHELL_CTRL_X_THREAD.join(timeout=0.2)
587
+
588
+ # Restore original SIGINT handler
589
+ if _ORIGINAL_SIGINT_HANDLER is not None:
590
+ with suppress(ValueError, OSError):
591
+ signal.signal(signal.SIGINT, _ORIGINAL_SIGINT_HANDLER)
592
+
593
+ # Clean up global state
594
+ _SHELL_CTRL_X_STOP_EVENT = None
595
+ _SHELL_CTRL_X_THREAD = None
596
+ _ORIGINAL_SIGINT_HANDLER = None
597
+
598
+
599
+ def _acquire_keyboard_context() -> None:
600
+ """Acquire the shared keyboard context (reference counted).
601
+
602
+ Starts the Ctrl-X listener when the first command starts.
603
+ Safe to call from any thread.
604
+ """
605
+ global _KEYBOARD_CONTEXT_REFCOUNT
606
+
607
+ should_start = False
608
+ with _KEYBOARD_CONTEXT_LOCK:
609
+ _KEYBOARD_CONTEXT_REFCOUNT += 1
610
+ if _KEYBOARD_CONTEXT_REFCOUNT == 1:
611
+ should_start = True
612
+
613
+ # Start listener OUTSIDE the lock to avoid blocking other commands
614
+ if should_start:
615
+ _start_keyboard_listener()
616
+
617
+
618
+ def _release_keyboard_context() -> None:
619
+ """Release the shared keyboard context (reference counted).
620
+
621
+ Stops the Ctrl-X listener when the last command finishes.
622
+ Safe to call from any thread.
623
+ """
624
+ global _KEYBOARD_CONTEXT_REFCOUNT
625
+
626
+ should_stop = False
627
+ with _KEYBOARD_CONTEXT_LOCK:
628
+ _KEYBOARD_CONTEXT_REFCOUNT -= 1
629
+ if _KEYBOARD_CONTEXT_REFCOUNT <= 0:
630
+ _KEYBOARD_CONTEXT_REFCOUNT = 0 # Safety clamp
631
+ should_stop = True
632
+
633
+ # Stop listener OUTSIDE the lock to avoid blocking other commands
634
+ if should_stop:
635
+ _stop_keyboard_listener()
636
+
637
+
638
+ def run_shell_command_streaming(
639
+ process: subprocess.Popen,
640
+ timeout: int = 60,
641
+ command: str = "",
642
+ group_id: str = None,
643
+ silent: bool = False,
644
+ ):
645
+ stop_event = threading.Event()
646
+ with _ACTIVE_STOP_EVENTS_LOCK:
647
+ _ACTIVE_STOP_EVENTS.add(stop_event)
648
+
649
+ start_time = time.time()
650
+ last_output_time = [start_time]
651
+
652
+ ABSOLUTE_TIMEOUT_SECONDS = 270
653
+
654
+ stdout_lines: deque[str] = deque(maxlen=256)
655
+ stderr_lines: deque[str] = deque(maxlen=256)
656
+
657
+ stdout_thread = None
658
+ stderr_thread = None
659
+
660
+ def read_stdout():
661
+ try:
662
+ fd = process.stdout.fileno()
663
+ except ValueError, OSError:
664
+ return
665
+
666
+ try:
667
+ while True:
668
+ # Check stop event first
669
+ if stop_event.is_set():
670
+ break
671
+
672
+ # Use select to check if data is available (with timeout)
673
+ if sys.platform.startswith("win"):
674
+ # Windows doesn't support select on pipes
675
+ # Use PeekNamedPipe via _win32_pipe_has_data() to check
676
+ # if data is available without blocking
677
+ try:
678
+ if _win32_pipe_has_data(process.stdout):
679
+ line = process.stdout.readline()
680
+ if not line: # EOF
681
+ break
682
+ line = line.rstrip("\n")
683
+ line = _truncate_line(line)
684
+ stdout_lines.append(line)
685
+ if not silent:
686
+ emit_shell_line(line, stream="stdout")
687
+ last_output_time[0] = time.time()
688
+ else:
689
+ # No data available, check if process has exited
690
+ if process.poll() is not None:
691
+ # Process exited, do one final drain
692
+ try:
693
+ remaining = process.stdout.read()
694
+ if remaining:
695
+ for line in remaining.split("\n"):
696
+ line = _truncate_line(line)
697
+ stdout_lines.append(line)
698
+ if not silent:
699
+ emit_shell_line(line, stream="stdout")
700
+ except ValueError, OSError:
701
+ pass
702
+ break
703
+ # Sleep briefly to avoid busy-waiting (100ms like POSIX)
704
+ time.sleep(0.1)
705
+ except ValueError, OSError:
706
+ break
707
+ else:
708
+ # POSIX: use select with timeout
709
+ try:
710
+ ready, _, _ = select.select([fd], [], [], 0.1) # 100ms timeout
711
+ except ValueError, OSError:
712
+ break
713
+
714
+ if ready:
715
+ line = process.stdout.readline()
716
+ if not line: # EOF
717
+ break
718
+ line = line.rstrip("\n")
719
+ line = _truncate_line(line)
720
+ stdout_lines.append(line)
721
+ if not silent:
722
+ emit_shell_line(line, stream="stdout")
723
+ last_output_time[0] = time.time()
724
+ # If not ready, loop continues and checks stop event again
725
+ except ValueError, OSError:
726
+ pass
727
+ except Exception:
728
+ pass
729
+
730
+ def read_stderr():
731
+ try:
732
+ fd = process.stderr.fileno()
733
+ except ValueError, OSError:
734
+ return
735
+
736
+ try:
737
+ while True:
738
+ # Check stop event first
739
+ if stop_event.is_set():
740
+ break
741
+
742
+ if sys.platform.startswith("win"):
743
+ # Windows doesn't support select on pipes
744
+ # Use PeekNamedPipe via _win32_pipe_has_data() to check
745
+ # if data is available without blocking
746
+ try:
747
+ if _win32_pipe_has_data(process.stderr):
748
+ line = process.stderr.readline()
749
+ if not line: # EOF
750
+ break
751
+ line = line.rstrip("\n")
752
+ line = _truncate_line(line)
753
+ stderr_lines.append(line)
754
+ if not silent:
755
+ emit_shell_line(line, stream="stderr")
756
+ last_output_time[0] = time.time()
757
+ else:
758
+ # No data available, check if process has exited
759
+ if process.poll() is not None:
760
+ # Process exited, do one final drain
761
+ try:
762
+ remaining = process.stderr.read()
763
+ if remaining:
764
+ for line in remaining.split("\n"):
765
+ line = _truncate_line(line)
766
+ stderr_lines.append(line)
767
+ if not silent:
768
+ emit_shell_line(line, stream="stderr")
769
+ except ValueError, OSError:
770
+ pass
771
+ break
772
+ # Sleep briefly to avoid busy-waiting (100ms like POSIX)
773
+ time.sleep(0.1)
774
+ except ValueError, OSError:
775
+ break
776
+ else:
777
+ try:
778
+ ready, _, _ = select.select([fd], [], [], 0.1)
779
+ except ValueError, OSError:
780
+ break
781
+
782
+ if ready:
783
+ line = process.stderr.readline()
784
+ if not line: # EOF
785
+ break
786
+ line = line.rstrip("\n")
787
+ line = _truncate_line(line)
788
+ stderr_lines.append(line)
789
+ if not silent:
790
+ emit_shell_line(line, stream="stderr")
791
+ last_output_time[0] = time.time()
792
+ except ValueError, OSError:
793
+ pass
794
+ except Exception:
795
+ pass
796
+
797
+ def cleanup_process_and_threads(timeout_type: str = "unknown"):
798
+ nonlocal stdout_thread, stderr_thread
799
+
800
+ def nuclear_kill(proc):
801
+ _kill_process_group(proc)
802
+
803
+ try:
804
+ # Signal reader threads to stop first
805
+ stop_event.set()
806
+
807
+ if process.poll() is None:
808
+ nuclear_kill(process)
809
+
810
+ try:
811
+ if process.stdout and not process.stdout.closed:
812
+ process.stdout.close()
813
+ if process.stderr and not process.stderr.closed:
814
+ process.stderr.close()
815
+ if process.stdin and not process.stdin.closed:
816
+ process.stdin.close()
817
+ except OSError, ValueError:
818
+ pass
819
+
820
+ # Unregister once we're done cleaning up
821
+ _unregister_process(process)
822
+
823
+ if stdout_thread and stdout_thread.is_alive():
824
+ stdout_thread.join(timeout=3)
825
+ if stdout_thread.is_alive() and not silent:
826
+ emit_warning(
827
+ f"stdout reader thread failed to terminate after {timeout_type} timeout",
828
+ message_group=group_id,
829
+ )
830
+
831
+ if stderr_thread and stderr_thread.is_alive():
832
+ stderr_thread.join(timeout=3)
833
+ if stderr_thread.is_alive() and not silent:
834
+ emit_warning(
835
+ f"stderr reader thread failed to terminate after {timeout_type} timeout",
836
+ message_group=group_id,
837
+ )
838
+
839
+ except Exception as e:
840
+ if not silent:
841
+ emit_warning(
842
+ f"Error during process cleanup: {e}", message_group=group_id
843
+ )
844
+
845
+ execution_time = time.time() - start_time
846
+ return ShellCommandOutput(
847
+ **{
848
+ "success": False,
849
+ "command": command,
850
+ "stdout": "\n".join(stdout_lines),
851
+ "stderr": "\n".join(stderr_lines),
852
+ "exit_code": -9,
853
+ "execution_time": execution_time,
854
+ "timeout": True,
855
+ "error": f"Command timed out after {timeout} seconds",
856
+ }
857
+ )
858
+
859
+ try:
860
+ stdout_thread = threading.Thread(target=read_stdout, daemon=True)
861
+ stderr_thread = threading.Thread(target=read_stderr, daemon=True)
862
+
863
+ stdout_thread.start()
864
+ stderr_thread.start()
865
+
866
+ while process.poll() is None:
867
+ current_time = time.time()
868
+
869
+ if current_time - start_time > ABSOLUTE_TIMEOUT_SECONDS:
870
+ if not silent:
871
+ emit_error(
872
+ "Process killed: absolute timeout reached",
873
+ message_group=group_id,
874
+ )
875
+ return cleanup_process_and_threads("absolute")
876
+
877
+ if current_time - last_output_time[0] > timeout:
878
+ if not silent:
879
+ emit_error(
880
+ "Process killed: inactivity timeout reached",
881
+ message_group=group_id,
882
+ )
883
+ return cleanup_process_and_threads("inactivity")
884
+
885
+ time.sleep(0.1)
886
+
887
+ if stdout_thread:
888
+ stdout_thread.join(timeout=5)
889
+ if stderr_thread:
890
+ stderr_thread.join(timeout=5)
891
+
892
+ exit_code = process.returncode
893
+ execution_time = time.time() - start_time
894
+
895
+ try:
896
+ if process.stdout and not process.stdout.closed:
897
+ process.stdout.close()
898
+ if process.stderr and not process.stderr.closed:
899
+ process.stderr.close()
900
+ if process.stdin and not process.stdin.closed:
901
+ process.stdin.close()
902
+ except OSError, ValueError:
903
+ pass
904
+
905
+ _unregister_process(process)
906
+
907
+ # Apply line length limits to stdout/stderr before returning
908
+ truncated_stdout = list(stdout_lines)
909
+ truncated_stderr = list(stderr_lines)
910
+
911
+ # Emit structured ShellOutputMessage for the UI (skip for silent sub-agents)
912
+ if not silent:
913
+ shell_output_msg = ShellOutputMessage(
914
+ command=command,
915
+ stdout="\n".join(truncated_stdout),
916
+ stderr="\n".join(truncated_stderr),
917
+ exit_code=exit_code,
918
+ duration_seconds=execution_time,
919
+ )
920
+ get_message_bus().emit(shell_output_msg)
921
+
922
+ with _ACTIVE_STOP_EVENTS_LOCK:
923
+ _ACTIVE_STOP_EVENTS.discard(stop_event)
924
+
925
+ if exit_code != 0:
926
+ return ShellCommandOutput(
927
+ success=False,
928
+ command=command,
929
+ error="""The process didn't exit cleanly! If the user_interrupted flag is true,
930
+ please stop all execution and ask the user for clarification!""",
931
+ stdout="\n".join(truncated_stdout),
932
+ stderr="\n".join(truncated_stderr),
933
+ exit_code=exit_code,
934
+ execution_time=execution_time,
935
+ timeout=False,
936
+ user_interrupted=process.pid in _USER_KILLED_PROCESSES,
937
+ )
938
+
939
+ return ShellCommandOutput(
940
+ success=True,
941
+ command=command,
942
+ stdout="\n".join(truncated_stdout),
943
+ stderr="\n".join(truncated_stderr),
944
+ exit_code=exit_code,
945
+ execution_time=execution_time,
946
+ timeout=False,
947
+ )
948
+
949
+ except Exception as e:
950
+ with _ACTIVE_STOP_EVENTS_LOCK:
951
+ _ACTIVE_STOP_EVENTS.discard(stop_event)
952
+ return ShellCommandOutput(
953
+ success=False,
954
+ command=command,
955
+ error=f"Error during streaming execution: {str(e)}",
956
+ stdout="\n".join(stdout_lines),
957
+ stderr="\n".join(stderr_lines),
958
+ exit_code=-1,
959
+ timeout=False,
960
+ )
961
+
962
+
963
+ async def run_shell_command(
964
+ context: RunContext,
965
+ command: str,
966
+ cwd: str = None,
967
+ timeout: int = 60,
968
+ background: bool = False,
969
+ ) -> ShellCommandOutput:
970
+ # Generate unique group_id for this command execution
971
+ group_id = generate_group_id("shell_command", command)
972
+
973
+ # Invoke safety check callbacks (only active in yolo_mode)
974
+ # This allows plugins to intercept and assess commands before execution
975
+ from code_muse.callbacks import on_run_shell_command
976
+
977
+ callback_results = await on_run_shell_command(context, command, cwd, timeout)
978
+
979
+ # Check if any callback blocked the command
980
+ # Callbacks can return None (allow) or a dict with blocked=True (reject)
981
+ _auto_approved = False
982
+ for result in callback_results:
983
+ if result and isinstance(result, dict):
984
+ if result.get("blocked"):
985
+ return ShellCommandOutput(
986
+ success=False,
987
+ command=command,
988
+ error=result.get(
989
+ "error_message", "Command blocked by safety check"
990
+ ),
991
+ user_feedback=result.get("reasoning", ""),
992
+ stdout=None,
993
+ stderr=None,
994
+ exit_code=None,
995
+ execution_time=None,
996
+ )
997
+ if result.get("pre_executed") and "output" in result:
998
+ return result["output"]
999
+ if result.get("auto_approve"):
1000
+ _auto_approved = True
1001
+
1002
+ if not command or not command.strip():
1003
+ emit_error("Command cannot be empty", message_group=group_id)
1004
+ return ShellCommandOutput(
1005
+ **{"success": False, "error": "Command cannot be empty"}
1006
+ )
1007
+
1008
+ from code_muse.config import get_yolo_mode
1009
+
1010
+ yolo_mode = get_yolo_mode()
1011
+
1012
+ # Check if we're running as a sub-agent (skip confirmation and run silently)
1013
+ running_as_subagent = is_subagent()
1014
+
1015
+ confirmation_lock_acquired = False
1016
+
1017
+ # Only ask for confirmation if we're in an interactive TTY, not in yolo mode,
1018
+ # and NOT running as a sub-agent (sub-agents run without user interaction)
1019
+ # and NOT auto-approved by a policy engine callback
1020
+ if (
1021
+ not yolo_mode
1022
+ and not running_as_subagent
1023
+ and not _auto_approved
1024
+ and sys.stdin.isatty()
1025
+ ):
1026
+ confirmation_lock_acquired = _CONFIRMATION_LOCK.acquire(blocking=False)
1027
+ if not confirmation_lock_acquired:
1028
+ return ShellCommandOutput(
1029
+ success=False,
1030
+ command=command,
1031
+ error="Another command is currently awaiting confirmation",
1032
+ )
1033
+
1034
+ # Get agent_name for personalized messages
1035
+ from code_muse.config import get_agent_name
1036
+
1037
+ agent_name = get_agent_name().title()
1038
+
1039
+ # Build panel content
1040
+ panel_content = Text()
1041
+ panel_content.append("⚡ Requesting permission to run:\n", style="bold yellow")
1042
+ panel_content.append("$ ", style="bold green")
1043
+ panel_content.append(command, style="bold white")
1044
+
1045
+ if cwd:
1046
+ panel_content.append("\n\n", style="")
1047
+ panel_content.append("📂 Working directory: ", style="dim")
1048
+ panel_content.append(cwd, style="dim cyan")
1049
+
1050
+ if background:
1051
+ panel_content.append("\n\n", style="")
1052
+ panel_content.append(
1053
+ "⏱️ This command will run in the background with no timeout.\n",
1054
+ style="bold red",
1055
+ )
1056
+ panel_content.append(
1057
+ "The process will continue until it completes or is manually stopped.",
1058
+ style="dim",
1059
+ )
1060
+
1061
+ # Use the common approval function (async version)
1062
+ confirmed, user_feedback = await get_user_approval_async(
1063
+ title="Shell Command",
1064
+ content=panel_content,
1065
+ preview=None,
1066
+ border_style="dim white",
1067
+ agent_name=agent_name,
1068
+ )
1069
+
1070
+ # Release lock after approval
1071
+ if confirmation_lock_acquired:
1072
+ _CONFIRMATION_LOCK.release()
1073
+
1074
+ if not confirmed:
1075
+ if user_feedback:
1076
+ result = ShellCommandOutput(
1077
+ success=False,
1078
+ command=command,
1079
+ error=f"USER REJECTED: {user_feedback}",
1080
+ user_feedback=user_feedback,
1081
+ stdout=None,
1082
+ stderr=None,
1083
+ exit_code=None,
1084
+ execution_time=None,
1085
+ )
1086
+ else:
1087
+ result = ShellCommandOutput(
1088
+ success=False,
1089
+ command=command,
1090
+ error="User rejected the command!",
1091
+ stdout=None,
1092
+ stderr=None,
1093
+ exit_code=None,
1094
+ execution_time=None,
1095
+ )
1096
+ return result
1097
+
1098
+ # Handle background execution - runs command detached and returns immediately.
1099
+ # Approval has already been obtained above (or skipped in yolo/sub-agent mode).
1100
+ if background:
1101
+ # Create temp log file for output
1102
+ log_file = tempfile.NamedTemporaryFile( # noqa: SIM115
1103
+ mode="w",
1104
+ prefix="shell_bg_",
1105
+ suffix=".log",
1106
+ delete=False, # Keep file so agent can read it later
1107
+ )
1108
+ log_file_path = log_file.name
1109
+
1110
+ try:
1111
+ # Platform-specific process detachment
1112
+ if sys.platform.startswith("win"):
1113
+ creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
1114
+ process = subprocess.Popen(
1115
+ command,
1116
+ shell=True,
1117
+ stdout=log_file,
1118
+ stderr=subprocess.STDOUT,
1119
+ stdin=subprocess.DEVNULL,
1120
+ cwd=cwd,
1121
+ creationflags=creationflags,
1122
+ )
1123
+ else:
1124
+ process = subprocess.Popen(
1125
+ command,
1126
+ shell=True,
1127
+ stdout=log_file,
1128
+ stderr=subprocess.STDOUT,
1129
+ stdin=subprocess.DEVNULL,
1130
+ cwd=cwd,
1131
+ start_new_session=True, # Fully detach on POSIX
1132
+ )
1133
+
1134
+ log_file.close() # Close our handle, process keeps writing
1135
+
1136
+ # Emit UI messages so user sees what happened
1137
+ bus = get_message_bus()
1138
+ bus.emit(
1139
+ ShellStartMessage(
1140
+ command=command,
1141
+ cwd=cwd,
1142
+ timeout=0, # No timeout for background processes
1143
+ background=True,
1144
+ )
1145
+ )
1146
+
1147
+ # Register the job so it can be listed/stopped later
1148
+ register_background_job(
1149
+ pid=process.pid,
1150
+ command=command,
1151
+ cwd=cwd,
1152
+ log_file=log_file.name,
1153
+ )
1154
+
1155
+ # Emit info about background execution
1156
+ emit_info(
1157
+ f"🚀 Background process started (PID: {process.pid}) - no timeout, runs until complete"
1158
+ )
1159
+ emit_info(f"📄 Output logging to: {log_file.name}")
1160
+
1161
+ # Return immediately - don't wait, don't block
1162
+ return ShellCommandOutput(
1163
+ success=True,
1164
+ command=command,
1165
+ stdout=None,
1166
+ stderr=None,
1167
+ exit_code=None,
1168
+ execution_time=0.0,
1169
+ background=True,
1170
+ log_file=log_file.name,
1171
+ pid=process.pid,
1172
+ )
1173
+ except Exception as e:
1174
+ with suppress(Exception):
1175
+ log_file.close()
1176
+ # Clean up the temp file on error since no process will write to it
1177
+ with suppress(OSError):
1178
+ os.unlink(log_file_path)
1179
+ # Emit error message so user sees what happened
1180
+ emit_error(f"❌ Failed to start background process: {e}")
1181
+ return ShellCommandOutput(
1182
+ success=False,
1183
+ command=command,
1184
+ error=f"Failed to start background process: {e}",
1185
+ stdout=None,
1186
+ stderr=None,
1187
+ exit_code=None,
1188
+ execution_time=None,
1189
+ background=True,
1190
+ )
1191
+
1192
+ # Execute the command - sub-agents run silently without keyboard context
1193
+ return await _execute_shell_command(
1194
+ command=command,
1195
+ cwd=cwd,
1196
+ timeout=timeout,
1197
+ group_id=group_id,
1198
+ silent=running_as_subagent,
1199
+ )
1200
+
1201
+
1202
+ async def _execute_shell_command(
1203
+ command: str,
1204
+ cwd: str | None,
1205
+ timeout: int,
1206
+ group_id: str,
1207
+ silent: bool = False,
1208
+ ) -> ShellCommandOutput:
1209
+ """Internal helper to execute a shell command.
1210
+
1211
+ Args:
1212
+ command: The shell command to execute
1213
+ cwd: Working directory for command execution
1214
+ timeout: Inactivity timeout in seconds
1215
+ group_id: Unique group ID for message grouping
1216
+ silent: If True, suppress streaming output (for sub-agents)
1217
+
1218
+ Returns:
1219
+ ShellCommandOutput with execution results
1220
+ """
1221
+ # Always emit the ShellStartMessage banner (even for sub-agents)
1222
+ bus = get_message_bus()
1223
+ bus.emit(
1224
+ ShellStartMessage(
1225
+ command=command,
1226
+ cwd=cwd,
1227
+ timeout=timeout,
1228
+ )
1229
+ )
1230
+
1231
+ # Pause spinner during shell command so \r output can work properly
1232
+ from code_muse.messaging.spinner import pause_all_spinners, resume_all_spinners
1233
+
1234
+ pause_all_spinners()
1235
+
1236
+ # Acquire shared keyboard context - Ctrl-X/Ctrl-C will kill ALL running commands
1237
+ # This is reference-counted: listener starts on first command, stops on last
1238
+ _acquire_keyboard_context()
1239
+ try:
1240
+ return await _run_command_inner(command, cwd, timeout, group_id, silent=silent)
1241
+ finally:
1242
+ _release_keyboard_context()
1243
+ resume_all_spinners()
1244
+
1245
+
1246
+ def _run_command_sync(
1247
+ command: str,
1248
+ cwd: str | None,
1249
+ timeout: int,
1250
+ group_id: str,
1251
+ silent: bool = False,
1252
+ ) -> ShellCommandOutput:
1253
+ """Synchronous command execution - runs in thread pool."""
1254
+ creationflags = 0
1255
+ preexec_fn = None
1256
+ if sys.platform.startswith("win"):
1257
+ try:
1258
+ creationflags = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined]
1259
+ except Exception:
1260
+ creationflags = 0
1261
+ else:
1262
+ preexec_fn = os.setsid if hasattr(os, "setsid") else None
1263
+
1264
+ import io
1265
+
1266
+ process = subprocess.Popen(
1267
+ command,
1268
+ shell=True,
1269
+ stdout=subprocess.PIPE,
1270
+ stderr=subprocess.PIPE,
1271
+ cwd=cwd,
1272
+ bufsize=0, # Unbuffered for real-time output
1273
+ preexec_fn=preexec_fn,
1274
+ creationflags=creationflags,
1275
+ )
1276
+
1277
+ # Wrap pipes with TextIOWrapper that preserves \r (newline='' disables translation)
1278
+ process.stdout = io.TextIOWrapper(
1279
+ process.stdout, newline="", encoding="utf-8", errors="replace"
1280
+ )
1281
+ process.stderr = io.TextIOWrapper(
1282
+ process.stderr, newline="", encoding="utf-8", errors="replace"
1283
+ )
1284
+ _register_process(process)
1285
+ try:
1286
+ return run_shell_command_streaming(
1287
+ process, timeout=timeout, command=command, group_id=group_id, silent=silent
1288
+ )
1289
+ finally:
1290
+ # Ensure unregistration in case streaming returned early or raised
1291
+ _unregister_process(process)
1292
+
1293
+
1294
+ async def _run_command_inner(
1295
+ command: str,
1296
+ cwd: str | None,
1297
+ timeout: int,
1298
+ group_id: str,
1299
+ silent: bool = False,
1300
+ ) -> ShellCommandOutput:
1301
+ """Inner command execution logic - runs blocking code in thread pool."""
1302
+ loop = asyncio.get_running_loop()
1303
+ try:
1304
+ # Run the blocking shell command in a thread pool to avoid blocking the event loop
1305
+ # This allows multiple sub-agents to run shell commands in parallel
1306
+ return await loop.run_in_executor(
1307
+ _SHELL_EXECUTOR,
1308
+ partial(_run_command_sync, command, cwd, timeout, group_id, silent),
1309
+ )
1310
+ except Exception as e:
1311
+ if not silent:
1312
+ emit_error(traceback.format_exc(), message_group=group_id)
1313
+ if "stdout" not in locals():
1314
+ stdout = None
1315
+ if "stderr" not in locals():
1316
+ stderr = None
1317
+
1318
+ # Apply line length limits to stdout/stderr if they exist
1319
+ truncated_stdout = None
1320
+ if stdout:
1321
+ stdout_lines = stdout.split("\n")
1322
+ truncated_stdout = "\n".join(
1323
+ [_truncate_line(line) for line in stdout_lines[-256:]]
1324
+ )
1325
+
1326
+ truncated_stderr = None
1327
+ if stderr:
1328
+ stderr_lines = stderr.split("\n")
1329
+ truncated_stderr = "\n".join(
1330
+ [_truncate_line(line) for line in stderr_lines[-256:]]
1331
+ )
1332
+
1333
+ return ShellCommandOutput(
1334
+ success=False,
1335
+ command=command,
1336
+ error=f"Error executing command {str(e)}",
1337
+ stdout=truncated_stdout,
1338
+ stderr=truncated_stderr,
1339
+ exit_code=-1,
1340
+ timeout=False,
1341
+ )
1342
+
1343
+
1344
+ class ReasoningOutput(BaseModel):
1345
+ success: bool = True
1346
+
1347
+
1348
+ def share_your_reasoning(
1349
+ context: RunContext, reasoning: str, next_steps: str | list[str] | None = None
1350
+ ) -> ReasoningOutput:
1351
+ # Handle list of next steps by formatting them
1352
+ formatted_next_steps = next_steps
1353
+ if isinstance(next_steps, list):
1354
+ formatted_next_steps = "\n".join(
1355
+ [f"{i + 1}. {step}" for i, step in enumerate(next_steps)]
1356
+ )
1357
+
1358
+ # Emit structured AgentReasoningMessage for the UI
1359
+ reasoning_msg = AgentReasoningMessage(
1360
+ reasoning=reasoning,
1361
+ next_steps=formatted_next_steps
1362
+ if formatted_next_steps and formatted_next_steps.strip()
1363
+ else None,
1364
+ )
1365
+ get_message_bus().emit(reasoning_msg)
1366
+
1367
+ return ReasoningOutput(success=True)
1368
+
1369
+
1370
+ def register_agent_run_shell_command(agent):
1371
+ """Register only the agent_run_shell_command tool."""
1372
+
1373
+ @agent.tool
1374
+ async def agent_run_shell_command(
1375
+ context: RunContext,
1376
+ command: str = "",
1377
+ cwd: str = None,
1378
+ timeout: int = 60,
1379
+ background: bool = False,
1380
+ ) -> ShellCommandOutput:
1381
+ """Execute a shell command with comprehensive monitoring and safety features.
1382
+
1383
+ Supports streaming output, timeout handling, and background execution.
1384
+ """
1385
+ return await run_shell_command(context, command, cwd, timeout, background)
1386
+
1387
+
1388
+ def register_agent_share_your_reasoning(agent):
1389
+ """Register only the agent_share_your_reasoning tool."""
1390
+
1391
+ @agent.tool
1392
+ def agent_share_your_reasoning(
1393
+ context: RunContext,
1394
+ reasoning: str = "",
1395
+ next_steps: str | list[str] | None = None,
1396
+ ) -> ReasoningOutput:
1397
+ """Share the agent's current reasoning and planned next steps with the user.
1398
+
1399
+ Displays reasoning and upcoming actions in a formatted panel for transparency.
1400
+ """
1401
+ return share_your_reasoning(context, reasoning, next_steps)