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,441 @@
1
+ """File Permission Handler Plugin.
2
+
3
+ This plugin handles user permission prompts for file operations,
4
+ providing a consistent and extensible permission system.
5
+ """
6
+
7
+ import contextlib
8
+ import difflib
9
+ import threading
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from rich.text import Text as RichText
14
+
15
+ from code_muse.callbacks import register_callback
16
+ from code_muse.config import get_diff_context_lines, get_yolo_mode
17
+ from code_muse.messaging import emit_warning
18
+ from code_muse.tools.common import (
19
+ _find_best_window,
20
+ get_user_approval,
21
+ )
22
+
23
+ # Lock for preventing multiple simultaneous permission prompts.
24
+ # FREE-THREADED: Sync-only file confirmation; keep threading.Lock.
25
+ _FILE_CONFIRMATION_LOCK = threading.Lock()
26
+
27
+ # Thread-local storage for user feedback from permission prompts
28
+ _thread_local = threading.local()
29
+
30
+
31
+ def get_last_user_feedback() -> str | None:
32
+ """Get the last user feedback from a permission prompt in this thread.
33
+
34
+ Returns:
35
+ The user feedback string, or None if no feedback was provided.
36
+ """
37
+ return getattr(_thread_local, "last_user_feedback", None)
38
+
39
+
40
+ def _set_user_feedback(feedback: str | None) -> None:
41
+ """Store user feedback in thread-local storage."""
42
+ _thread_local.last_user_feedback = feedback
43
+
44
+
45
+ def clear_user_feedback() -> None:
46
+ """Clear any stored user feedback."""
47
+ _thread_local.last_user_feedback = None
48
+
49
+
50
+ def set_diff_already_shown(shown: bool = True) -> None:
51
+ """Mark that a diff preview was already shown during permission prompt."""
52
+ _thread_local.diff_already_shown = shown
53
+
54
+
55
+ def was_diff_already_shown() -> bool:
56
+ """Check if a diff was already shown during the permission prompt.
57
+
58
+ Returns:
59
+ True if diff was shown, False otherwise
60
+ """
61
+ return getattr(_thread_local, "diff_already_shown", False)
62
+
63
+
64
+ def clear_diff_shown_flag() -> None:
65
+ """Clear the diff-already-shown flag."""
66
+ _thread_local.diff_already_shown = False
67
+
68
+
69
+ # Diff formatting is now handled by common.format_diff_with_colors()
70
+ # Arrow selector and approval UI now handled by common.get_user_approval()
71
+
72
+
73
+ def _preview_delete_snippet(file_path: str, snippet: str) -> str | None:
74
+ """Generate a preview diff for deleting a snippet without modifying the file."""
75
+ try:
76
+ file_path = Path(file_path).resolve()
77
+ if not file_path.exists() or not file_path.is_file():
78
+ return None
79
+
80
+ with open(file_path, encoding="utf-8", errors="surrogateescape") as f:
81
+ original = f.read()
82
+
83
+ # Sanitize any surrogate characters
84
+ with contextlib.suppress(UnicodeEncodeError, UnicodeDecodeError):
85
+ original = original.encode("utf-8", errors="surrogatepass").decode(
86
+ "utf-8", errors="replace"
87
+ )
88
+
89
+ if snippet not in original:
90
+ return None
91
+
92
+ modified = original.replace(snippet, "")
93
+ diff_text = "".join(
94
+ difflib.unified_diff(
95
+ original.splitlines(keepends=True),
96
+ modified.splitlines(keepends=True),
97
+ fromfile=f"a/{file_path.name}",
98
+ tofile=f"b/{file_path.name}",
99
+ n=get_diff_context_lines(),
100
+ )
101
+ )
102
+ return diff_text
103
+ except Exception:
104
+ return None
105
+
106
+
107
+ def _preview_write_to_file(
108
+ file_path: str, content: str, overwrite: bool = False
109
+ ) -> str | None:
110
+ """Generate a preview diff for writing to a file without modifying it."""
111
+ try:
112
+ file_path = Path(file_path).resolve()
113
+ exists = file_path.exists()
114
+
115
+ if exists and not overwrite:
116
+ return None
117
+
118
+ diff_lines = difflib.unified_diff(
119
+ [] if not exists else [""],
120
+ content.splitlines(keepends=True),
121
+ fromfile="/dev/null" if not exists else f"a/{file_path.name}",
122
+ tofile=f"b/{file_path.name}",
123
+ n=get_diff_context_lines(),
124
+ )
125
+ return "".join(diff_lines)
126
+ except Exception:
127
+ return None
128
+
129
+
130
+ def _preview_replace_in_file(
131
+ file_path: str, replacements: list[dict[str, str]]
132
+ ) -> str | None:
133
+ """Generate a preview diff for replacing text in a file without modifying the file."""
134
+ try:
135
+ file_path = Path(file_path).resolve()
136
+
137
+ with open(file_path, encoding="utf-8", errors="surrogateescape") as f:
138
+ original = f.read()
139
+
140
+ # Sanitize any surrogate characters
141
+ with contextlib.suppress(UnicodeEncodeError, UnicodeDecodeError):
142
+ original = original.encode("utf-8", errors="surrogatepass").decode(
143
+ "utf-8", errors="replace"
144
+ )
145
+
146
+ modified = original
147
+ for rep in replacements:
148
+ old_snippet = rep.get("old_str", "")
149
+ new_snippet = rep.get("new_str", "")
150
+
151
+ if old_snippet and old_snippet in modified:
152
+ modified = modified.replace(old_snippet, new_snippet)
153
+ continue
154
+
155
+ # Use the same logic as file_modifications for fuzzy matching
156
+ orig_lines = modified.splitlines()
157
+ loc, score = _find_best_window(orig_lines, old_snippet)
158
+
159
+ if score < 0.95 or loc is None:
160
+ return None
161
+
162
+ start, end = loc
163
+ had_trailing_newline = modified.endswith("\n")
164
+ prefix = "\n".join(orig_lines[:start])
165
+ suffix = "\n".join(orig_lines[end:])
166
+ parts = []
167
+ if prefix:
168
+ parts.append(prefix)
169
+ parts.append(new_snippet.rstrip("\n"))
170
+ if suffix:
171
+ parts.append(suffix)
172
+ modified = "\n".join(parts)
173
+ if had_trailing_newline and not modified.endswith("\n"):
174
+ modified += "\n"
175
+
176
+ if modified == original:
177
+ return None
178
+
179
+ diff_text = "".join(
180
+ difflib.unified_diff(
181
+ original.splitlines(keepends=True),
182
+ modified.splitlines(keepends=True),
183
+ fromfile=f"a/{file_path.name}",
184
+ tofile=f"b/{file_path.name}",
185
+ n=get_diff_context_lines(),
186
+ )
187
+ )
188
+ return diff_text
189
+ except Exception:
190
+ return None
191
+
192
+
193
+ def _preview_delete_file(file_path: str) -> str | None:
194
+ """Whole-file deletes intentionally do not show content diffs."""
195
+ return None
196
+
197
+
198
+ def prompt_for_file_permission(
199
+ file_path: str,
200
+ operation: str,
201
+ preview: str | None = None,
202
+ message_group: str | None = None,
203
+ ) -> tuple[bool, str | None]:
204
+ """Prompt the user for permission to perform a file operation.
205
+
206
+ This function provides a unified permission prompt system for all file operations.
207
+
208
+ Args:
209
+ file_path: Path to the file being modified.
210
+ operation: Description of the operation (e.g., "edit", "delete", "create").
211
+ preview: Optional preview of changes (diff or content preview).
212
+ message_group: Optional message group for organizing output.
213
+
214
+ Returns:
215
+ Tuple of (confirmed: bool, user_feedback: str | None)
216
+ - confirmed: True if permission is granted, False otherwise
217
+ - user_feedback: Optional feedback message from user to send back to the model
218
+ """
219
+ yolo_mode = get_yolo_mode()
220
+
221
+ # Skip confirmation only if in yolo mode (removed TTY check for better compatibility)
222
+ if yolo_mode:
223
+ return True, None
224
+
225
+ # Try to acquire the lock to prevent multiple simultaneous prompts
226
+ confirmation_lock_acquired = _FILE_CONFIRMATION_LOCK.acquire(blocking=False)
227
+ if not confirmation_lock_acquired:
228
+ emit_warning(
229
+ "Another file operation is currently awaiting confirmation",
230
+ message_group=message_group,
231
+ )
232
+ return False, None
233
+
234
+ try:
235
+ # Build panel content
236
+ panel_content = RichText()
237
+ panel_content.append("🔒 Requesting permission to ", style="bold yellow")
238
+ panel_content.append(operation, style="bold cyan")
239
+ panel_content.append(":\n", style="bold yellow")
240
+ panel_content.append("📄 ", style="dim")
241
+ panel_content.append(file_path, style="bold white")
242
+
243
+ # Use the common approval function
244
+ confirmed, user_feedback = get_user_approval(
245
+ title="File Operation",
246
+ content=panel_content,
247
+ preview=preview,
248
+ border_style="dim white",
249
+ )
250
+
251
+ return confirmed, user_feedback
252
+
253
+ finally:
254
+ if confirmation_lock_acquired:
255
+ _FILE_CONFIRMATION_LOCK.release()
256
+
257
+
258
+ def handle_edit_file_permission(
259
+ context: Any,
260
+ file_path: str,
261
+ operation_type: str,
262
+ operation_data: Any,
263
+ message_group: str | None = None,
264
+ ) -> bool:
265
+ """Handle permission for edit_file operations with automatic preview generation.
266
+
267
+ Args:
268
+ context: The operation context
269
+ file_path: Path to the file being operated on
270
+ operation_type: Type of edit operation ('write', 'replace', 'delete_snippet')
271
+ operation_data: Operation-specific data (content, replacements, snippet, etc.)
272
+ message_group: Optional message group
273
+
274
+ Returns:
275
+ True if permission granted, False if denied
276
+ """
277
+ preview = None
278
+
279
+ if operation_type == "write":
280
+ content = operation_data.get("content", "")
281
+ overwrite = operation_data.get("overwrite", False)
282
+ preview = _preview_write_to_file(file_path, content, overwrite)
283
+ operation_desc = "write to"
284
+ elif operation_type == "replace":
285
+ replacements = operation_data.get("replacements", [])
286
+ preview = _preview_replace_in_file(file_path, replacements)
287
+ operation_desc = "replace text in"
288
+ elif operation_type == "delete_snippet":
289
+ snippet = operation_data.get("delete_snippet", "")
290
+ preview = _preview_delete_snippet(file_path, snippet)
291
+ operation_desc = "delete snippet from"
292
+ else:
293
+ operation_desc = f"perform {operation_type} operation on"
294
+
295
+ confirmed, user_feedback = prompt_for_file_permission(
296
+ file_path, operation_desc, preview, message_group
297
+ )
298
+ # Store feedback in thread-local storage so the tool can access it
299
+ _set_user_feedback(user_feedback)
300
+ return confirmed
301
+
302
+
303
+ def handle_delete_file_permission(
304
+ context: Any,
305
+ file_path: str,
306
+ message_group: str | None = None,
307
+ ) -> bool:
308
+ """Handle permission for delete_file operations with automatic preview generation.
309
+
310
+ Args:
311
+ context: The operation context
312
+ file_path: Path to the file being deleted
313
+ message_group: Optional message group
314
+
315
+ Returns:
316
+ True if permission granted, False if denied
317
+ """
318
+ preview = _preview_delete_file(file_path)
319
+ confirmed, user_feedback = prompt_for_file_permission(
320
+ file_path, "delete", preview, message_group
321
+ )
322
+ # Store feedback in thread-local storage so the tool can access it
323
+ _set_user_feedback(user_feedback)
324
+ return confirmed
325
+
326
+
327
+ def handle_file_permission(
328
+ context: Any,
329
+ file_path: str,
330
+ operation: str,
331
+ preview: str | None = None,
332
+ message_group: str | None = None,
333
+ operation_data: Any = None,
334
+ ) -> bool:
335
+ """Callback handler for file permission checks.
336
+
337
+ This function is called by file operations to check for user permission.
338
+ It returns True if the operation should proceed, False if it should be cancelled.
339
+
340
+ Args:
341
+ context: The operation context
342
+ file_path: Path to the file being operated on
343
+ operation: Description of the operation
344
+ preview: Optional preview of changes (deprecated - use operation_data instead)
345
+ message_group: Optional message group
346
+ operation_data: Operation-specific data for preview generation
347
+
348
+ Returns:
349
+ True if permission granted, False if denied
350
+ """
351
+ # Generate preview from operation_data if provided
352
+ if operation_data is not None:
353
+ preview = _generate_preview_from_operation_data(
354
+ file_path, operation, operation_data
355
+ )
356
+
357
+ confirmed, user_feedback = prompt_for_file_permission(
358
+ file_path, operation, preview, message_group
359
+ )
360
+ # Store feedback in thread-local storage so the tool can access it
361
+ _set_user_feedback(user_feedback)
362
+ return confirmed
363
+
364
+
365
+ def _generate_preview_from_operation_data(
366
+ file_path: str, operation: str, operation_data: Any
367
+ ) -> str | None:
368
+ """Generate preview diff from operation data.
369
+
370
+ Args:
371
+ file_path: Path to the file
372
+ operation: Type of operation
373
+ operation_data: Operation-specific data
374
+
375
+ Returns:
376
+ Preview diff or None if generation fails
377
+ """
378
+ try:
379
+ if operation == "delete":
380
+ return _preview_delete_file(file_path)
381
+ elif operation == "write":
382
+ content = operation_data.get("content", "")
383
+ overwrite = operation_data.get("overwrite", False)
384
+ return _preview_write_to_file(file_path, content, overwrite)
385
+ elif operation == "delete snippet from":
386
+ snippet = operation_data.get("snippet", "")
387
+ return _preview_delete_snippet(file_path, snippet)
388
+ elif operation == "replace text in":
389
+ replacements = operation_data.get("replacements", [])
390
+ return _preview_replace_in_file(file_path, replacements)
391
+ elif operation == "edit_file":
392
+ # Handle edit_file operations
393
+ if "delete_snippet" in operation_data:
394
+ return _preview_delete_snippet(
395
+ file_path, operation_data["delete_snippet"]
396
+ )
397
+ elif "replacements" in operation_data:
398
+ return _preview_replace_in_file(
399
+ file_path, operation_data["replacements"]
400
+ )
401
+ elif "content" in operation_data:
402
+ content = operation_data.get("content", "")
403
+ overwrite = operation_data.get("overwrite", False)
404
+ return _preview_write_to_file(file_path, content, overwrite)
405
+
406
+ return None
407
+ except Exception:
408
+ return None
409
+
410
+
411
+ def get_permission_handler_help() -> str:
412
+ """Return help information for the file permission handler."""
413
+ return """File Permission Handler Plugin:
414
+ - Unified permission prompts for all file operations
415
+ - YOLO mode support for automatic approval
416
+ - Thread-safe confirmation system
417
+ - Consistent user experience across file operations
418
+ - Detailed preview support with diff highlighting
419
+ - Automatic preview generation from operation data"""
420
+
421
+
422
+ def get_file_permission_prompt_additions() -> str:
423
+ """Return file permission handling prompt additions for agents."""
424
+ if get_yolo_mode():
425
+ return ""
426
+
427
+ return """
428
+ ## User Approval System
429
+
430
+ When file operations are rejected, the response includes a `user_feedback` field:
431
+ - If `user_feedback` has text: implement their suggestion and retry the operation.
432
+ - If `user_feedback` is empty: stop and ask the user what they want instead.
433
+ - Never retry the exact same rejected operation without changes.
434
+ """
435
+
436
+
437
+ # Register the callback for file permission handling
438
+ register_callback("file_permission", handle_file_permission)
439
+
440
+ # Register the prompt hook for file permission instructions
441
+ register_callback("load_prompt", get_file_permission_prompt_additions)
@@ -0,0 +1,30 @@
1
+ """Filter Engine plugin for Muse.
2
+
3
+ Intercepts shell commands, classifies them, applies compression strategies,
4
+ and returns compact results to reduce token usage.
5
+ """
6
+
7
+ from code_muse.plugins.filter_engine.classifier import CommandClassifier
8
+ from code_muse.plugins.filter_engine.dispatcher import FilterDispatcher
9
+ from code_muse.plugins.filter_engine.register_callbacks import (
10
+ filter_engine_callback,
11
+ )
12
+ from code_muse.plugins.filter_engine.registry import StrategyRegistry
13
+
14
+ # Import strategies so they self-register with the strategy registry
15
+ from code_muse.plugins.filter_engine.strategies import ( # noqa: F401
16
+ code,
17
+ git,
18
+ lint,
19
+ test,
20
+ )
21
+ from code_muse.plugins.filter_engine.verbosity import VerbosityLevel, get_verbosity
22
+
23
+ __all__ = [
24
+ "CommandClassifier",
25
+ "FilterDispatcher",
26
+ "StrategyRegistry",
27
+ "VerbosityLevel",
28
+ "filter_engine_callback",
29
+ "get_verbosity",
30
+ ]
@@ -0,0 +1,153 @@
1
+ """Command classifier for the filter engine.
2
+
3
+ Uses pre-compiled regex patterns to classify shell commands into categories
4
+ that determine which compression strategy is applied.
5
+ """
6
+
7
+ import re
8
+ from typing import ClassVar
9
+
10
+
11
+ class CommandClassifier:
12
+ """Classify shell commands into categories using regex patterns."""
13
+
14
+ # Pre-compiled pattern sets for each category
15
+ PATTERNS: ClassVar[dict[str, list[re.Pattern[str]]]] = {
16
+ "git": [
17
+ re.compile(r"^\s*git\s+status"),
18
+ re.compile(r"^\s*git\s+log"),
19
+ re.compile(r"^\s*git\s+diff"),
20
+ re.compile(r"^\s*git\s+add"),
21
+ re.compile(r"^\s*git\s+commit"),
22
+ re.compile(r"^\s*git\s+push"),
23
+ re.compile(r"^\s*git\s+pull"),
24
+ re.compile(r"^\s*git\s+fetch"),
25
+ re.compile(r"^\s*git\s+branch"),
26
+ re.compile(r"^\s*git\s+checkout"),
27
+ re.compile(r"^\s*git\s+merge"),
28
+ re.compile(r"^\s*git\s+rebase"),
29
+ re.compile(r"^\s*git\s+show"),
30
+ re.compile(r"^\s*git\s+blame"),
31
+ re.compile(r"^\s*git\s+stash"),
32
+ re.compile(r"^\s*git\s+reset"),
33
+ re.compile(r"^\s*git\s+tag"),
34
+ re.compile(r"^\s*git\s+clone"),
35
+ re.compile(r"^\s*git\s+init"),
36
+ re.compile(r"^\s*git\s+remote"),
37
+ re.compile(r"^\s*git\s+config"),
38
+ re.compile(r"^\s*git\s+\S"), # catch-all for any other git command
39
+ ],
40
+ "test": [
41
+ re.compile(r"^\s*pytest\b"),
42
+ re.compile(r"^\s*python\s+-m\s+pytest\b"),
43
+ re.compile(r"^\s*vitest\b"),
44
+ re.compile(r"^\s*jest\b"),
45
+ re.compile(r"^\s*cargo\s+test\b"),
46
+ re.compile(r"^\s*rspec\b"),
47
+ re.compile(r"^\s*go\s+test\b"),
48
+ re.compile(r"^\s*npm\s+test\b"),
49
+ re.compile(r"^\s*yarn\s+test\b"),
50
+ re.compile(r"^\s*npx\s+jest\b"),
51
+ re.compile(r"^\s*npx\s+vitest\b"),
52
+ re.compile(r"^\s*python\s+-m\s+unittest\b"),
53
+ re.compile(r"^\s*mvn\s+test\b"),
54
+ re.compile(r"^\s*gradle\s+test\b"),
55
+ re.compile(r"^\s*tox\b"),
56
+ re.compile(r"^\s*nox\b"),
57
+ ],
58
+ "lint": [
59
+ re.compile(r"^\s*ruff\b"),
60
+ re.compile(r"^\s*eslint\b"),
61
+ re.compile(r"^\s*tsc\b"),
62
+ re.compile(r"^\s*golangci-lint\b"),
63
+ re.compile(r"^\s*rubocop\b"),
64
+ re.compile(r"^\s*flake8\b"),
65
+ re.compile(r"^\s*mypy\b"),
66
+ re.compile(r"^\s*pyright\b"),
67
+ re.compile(r"^\s*cargo\s+clippy\b"),
68
+ re.compile(r"^\s*clippy\b"),
69
+ re.compile(r"^\s*shellcheck\b"),
70
+ re.compile(r"^\s*markdownlint\b"),
71
+ re.compile(r"^\s*pylint\b"),
72
+ re.compile(r"^\s*black\s+--check\b"),
73
+ re.compile(r"^\s*isort\s+--check\b"),
74
+ re.compile(r"^\s*prettier\s+--check\b"),
75
+ ],
76
+ "code": [
77
+ re.compile(r"^\s*cat\b"),
78
+ re.compile(r"^\s*head\b"),
79
+ re.compile(r"^\s*tail\b"),
80
+ re.compile(r"^\s*less\b"),
81
+ re.compile(r"^\s*bat\b"),
82
+ re.compile(r"^\s*nl\b"),
83
+ re.compile(r"^\s*sed\b"),
84
+ re.compile(r"^\s*awk\b"),
85
+ re.compile(r"^\s*grep\b"),
86
+ re.compile(r"^\s*rg\b"),
87
+ re.compile(r"^\s*find\b"),
88
+ re.compile(r"^\s*ls\b"),
89
+ re.compile(r"^\s*tree\b"),
90
+ re.compile(r"^\s*wc\b"),
91
+ re.compile(r"^\s*sort\b"),
92
+ re.compile(r"^\s*uniq\b"),
93
+ re.compile(r"^\s*xargs\b"),
94
+ re.compile(r"^\s*cut\b"),
95
+ re.compile(r"^\s*tr\b"),
96
+ re.compile(r"^\s*dd\b"),
97
+ re.compile(r"^\s*diff\b"),
98
+ re.compile(r"^\s*cmp\b"),
99
+ ],
100
+ "read": [
101
+ re.compile(r"^\s*cat\b"),
102
+ re.compile(r"^\s*head\b"),
103
+ re.compile(r"^\s*tail\b"),
104
+ re.compile(r"^\s*less\b"),
105
+ re.compile(r"^\s*bat\b"),
106
+ ],
107
+ }
108
+
109
+ @classmethod
110
+ def classify(cls, command: str) -> str:
111
+ """Classify a shell command into a category.
112
+
113
+ Categories are checked in priority order: git, test, lint, code, read.
114
+ The ``read`` category is a subset of ``code``; if a command matches both,
115
+ it is classified as ``read`` because read is more specific.
116
+
117
+ Args:
118
+ command: The raw shell command string.
119
+
120
+ Returns:
121
+ One of ``git``, ``test``, ``lint``, ``code``, ``read``, or ``unknown``.
122
+ """
123
+ if not command or not command.strip():
124
+ return "unknown"
125
+
126
+ stripped = command.strip()
127
+
128
+ # Check git first (most specific)
129
+ for pattern in cls.PATTERNS["git"]:
130
+ if pattern.search(stripped):
131
+ return "git"
132
+
133
+ # Check test
134
+ for pattern in cls.PATTERNS["test"]:
135
+ if pattern.search(stripped):
136
+ return "test"
137
+
138
+ # Check lint
139
+ for pattern in cls.PATTERNS["lint"]:
140
+ if pattern.search(stripped):
141
+ return "lint"
142
+
143
+ # Check read (subset of code, checked before code)
144
+ for pattern in cls.PATTERNS["read"]:
145
+ if pattern.search(stripped):
146
+ return "read"
147
+
148
+ # Check code
149
+ for pattern in cls.PATTERNS["code"]:
150
+ if pattern.search(stripped):
151
+ return "code"
152
+
153
+ return "unknown"