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,245 @@
1
+ """
2
+ Data models for the hook engine.
3
+
4
+ Defines all data structures used throughout the hook engine with full type
5
+ safety and validation.
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Literal
10
+
11
+
12
+ @dataclass
13
+ class HookConfig:
14
+ """
15
+ Configuration for a single hook.
16
+
17
+ Attributes:
18
+ matcher: Pattern to match against events (e.g., "Edit && .py")
19
+ type: Type of hook action ("command" or "prompt")
20
+ command: Command or prompt text to execute
21
+ timeout: Maximum execution time in milliseconds (default: 5000)
22
+ once: Execute only once per session (default: False)
23
+ enabled: Whether this hook is enabled (default: True)
24
+ id: Optional unique identifier for this hook
25
+ source: Origin of the hook config ("global" or "project")
26
+ trusted: Whether this hook has been explicitly trusted. For global
27
+ hooks this defaults to True. Project hooks default to False.
28
+ """
29
+
30
+ matcher: str
31
+ type: Literal["command", "prompt"]
32
+ command: str
33
+ timeout: int = 5000
34
+ once: bool = False
35
+ enabled: bool = True
36
+ id: str | None = None
37
+ source: Literal["global", "project"] = "global"
38
+ trusted: bool = True
39
+
40
+ def __post_init__(self):
41
+ """Validate hook configuration after initialization."""
42
+ if not self.matcher:
43
+ raise ValueError("Hook matcher cannot be empty")
44
+
45
+ if self.type not in ("command", "prompt"):
46
+ raise ValueError(
47
+ f"Hook type must be 'command' or 'prompt', got: {self.type}"
48
+ )
49
+
50
+ if not self.command:
51
+ raise ValueError("Hook command cannot be empty")
52
+
53
+ if self.timeout < 100:
54
+ raise ValueError(f"Hook timeout must be >= 100ms, got: {self.timeout}")
55
+
56
+ if self.id is None:
57
+ import hashlib
58
+
59
+ content = f"{self.matcher}:{self.type}:{self.command}"
60
+ self.id = hashlib.sha256(content.encode()).hexdigest()[:12]
61
+
62
+
63
+ @dataclass
64
+ class EventData:
65
+ """
66
+ Input data for hook processing.
67
+
68
+ Attributes:
69
+ event_type: Type of event (PreToolUse, PostToolUse, etc.)
70
+ tool_name: Name of the tool being called
71
+ tool_args: Arguments passed to the tool
72
+ context: Optional context metadata (result, duration, etc.)
73
+ """
74
+
75
+ event_type: str
76
+ tool_name: str
77
+ tool_args: dict[str, Any] = field(default_factory=dict)
78
+ context: dict[str, Any] = field(default_factory=dict)
79
+
80
+ def __post_init__(self):
81
+ if not self.event_type:
82
+ raise ValueError("Event type cannot be empty")
83
+ if not self.tool_name:
84
+ raise ValueError("Tool name cannot be empty")
85
+
86
+
87
+ @dataclass
88
+ class ExecutionResult:
89
+ """
90
+ Result from executing a hook.
91
+
92
+ Attributes:
93
+ blocked: Whether the hook blocked the operation
94
+ hook_command: The command that was executed
95
+ stdout: Standard output from command
96
+ stderr: Standard error from command
97
+ exit_code: Exit code from command execution
98
+ duration_ms: Execution duration in milliseconds
99
+ error: Error message if execution failed
100
+ hook_id: ID of the hook that was executed
101
+ """
102
+
103
+ blocked: bool
104
+ hook_command: str
105
+ stdout: str = ""
106
+ stderr: str = ""
107
+ exit_code: int = 0
108
+ duration_ms: float = 0.0
109
+ error: str | None = None
110
+ hook_id: str | None = None
111
+
112
+ @property
113
+ def success(self) -> bool:
114
+ """Whether the hook executed successfully (exit code 0)."""
115
+ return self.exit_code == 0 and self.error is None
116
+
117
+ @property
118
+ def output(self) -> str:
119
+ """Combined stdout and stderr."""
120
+ parts = []
121
+ if self.stdout:
122
+ parts.append(self.stdout)
123
+ if self.stderr:
124
+ parts.append(self.stderr)
125
+ return "\n".join(parts)
126
+
127
+
128
+ @dataclass
129
+ class HookGroup:
130
+ """A group of hooks that share the same matcher."""
131
+
132
+ matcher: str
133
+ hooks: list[HookConfig] = field(default_factory=list)
134
+
135
+ def __post_init__(self):
136
+ if not self.matcher:
137
+ raise ValueError("Hook group matcher cannot be empty")
138
+
139
+
140
+ @dataclass
141
+ class HookRegistry:
142
+ """Registry of all hooks organized by event type."""
143
+
144
+ pre_tool_use: list[HookConfig] = field(default_factory=list)
145
+ post_tool_use: list[HookConfig] = field(default_factory=list)
146
+ session_start: list[HookConfig] = field(default_factory=list)
147
+ session_end: list[HookConfig] = field(default_factory=list)
148
+ pre_compact: list[HookConfig] = field(default_factory=list)
149
+ user_prompt_submit: list[HookConfig] = field(default_factory=list)
150
+ notification: list[HookConfig] = field(default_factory=list)
151
+ stop: list[HookConfig] = field(default_factory=list)
152
+ subagent_stop: list[HookConfig] = field(default_factory=list)
153
+
154
+ _executed_once_hooks: set = field(default_factory=set, repr=False)
155
+
156
+ def get_hooks_for_event(self, event_type: str) -> list[HookConfig]:
157
+ attr_name = self._normalize_event_type(event_type)
158
+ if not hasattr(self, attr_name):
159
+ return []
160
+ all_hooks = getattr(self, attr_name)
161
+ enabled_hooks = []
162
+ for hook in all_hooks:
163
+ if not hook.enabled:
164
+ continue
165
+ if hook.once and hook.id in self._executed_once_hooks:
166
+ continue
167
+ enabled_hooks.append(hook)
168
+ return enabled_hooks
169
+
170
+ def mark_hook_executed(self, hook_id: str) -> None:
171
+ self._executed_once_hooks.add(hook_id)
172
+
173
+ def reset_once_hooks(self) -> None:
174
+ self._executed_once_hooks.clear()
175
+
176
+ @staticmethod
177
+ def _normalize_event_type(event_type: str) -> str:
178
+ import re
179
+
180
+ s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", event_type)
181
+ return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
182
+
183
+ def add_hook(self, event_type: str, hook: HookConfig) -> None:
184
+ attr_name = self._normalize_event_type(event_type)
185
+ if not hasattr(self, attr_name):
186
+ raise ValueError(f"Unknown event type: {event_type}")
187
+ getattr(self, attr_name).append(hook)
188
+
189
+ def remove_hook(self, event_type: str, hook_id: str) -> bool:
190
+ attr_name = self._normalize_event_type(event_type)
191
+ if not hasattr(self, attr_name):
192
+ return False
193
+ hooks_list = getattr(self, attr_name)
194
+ for i, hook in enumerate(hooks_list):
195
+ if hook.id == hook_id:
196
+ hooks_list.pop(i)
197
+ return True
198
+ return False
199
+
200
+ def count_hooks(self, event_type: str | None = None) -> int:
201
+ if event_type is None:
202
+ total = 0
203
+ for attr in [
204
+ "pre_tool_use",
205
+ "post_tool_use",
206
+ "session_start",
207
+ "session_end",
208
+ "pre_compact",
209
+ "user_prompt_submit",
210
+ "notification",
211
+ "stop",
212
+ "subagent_stop",
213
+ ]:
214
+ total += len(getattr(self, attr))
215
+ return total
216
+ attr_name = self._normalize_event_type(event_type)
217
+ if not hasattr(self, attr_name):
218
+ return 0
219
+ return len(getattr(self, attr_name))
220
+
221
+
222
+ @dataclass
223
+ class ProcessEventResult:
224
+ """Result from processing an event through the hook engine."""
225
+
226
+ blocked: bool
227
+ executed_hooks: int
228
+ results: list[ExecutionResult]
229
+ blocking_reason: str | None = None
230
+ total_duration_ms: float = 0.0
231
+
232
+ @property
233
+ def all_successful(self) -> bool:
234
+ return all(result.success for result in self.results)
235
+
236
+ @property
237
+ def failed_hooks(self) -> list[ExecutionResult]:
238
+ return [result for result in self.results if not result.success]
239
+
240
+ def get_combined_output(self) -> str:
241
+ outputs = []
242
+ for result in self.results:
243
+ if result.output:
244
+ outputs.append(f"[{result.hook_command}]\n{result.output}")
245
+ return "\n\n".join(outputs)
@@ -0,0 +1,114 @@
1
+ """
2
+ Registry management for hooks.
3
+
4
+ Builds and manages the HookRegistry from configuration dictionaries.
5
+ """
6
+
7
+ import logging
8
+ import re
9
+ from typing import Any
10
+
11
+ from .models import HookConfig, HookRegistry
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Supported event types
16
+ SUPPORTED_EVENT_TYPES = [
17
+ "PreToolUse",
18
+ "PostToolUse",
19
+ "SessionStart",
20
+ "SessionEnd",
21
+ "PreCompact",
22
+ "UserPromptSubmit",
23
+ "Notification",
24
+ "Stop",
25
+ "SubagentStop",
26
+ ]
27
+
28
+
29
+ def build_registry_from_config(
30
+ config: dict[str, Any],
31
+ source: str = "global",
32
+ trusted: bool = True,
33
+ ) -> HookRegistry:
34
+ """
35
+ Build a HookRegistry from a configuration dictionary.
36
+
37
+ Args:
38
+ config: Hook configuration dictionary.
39
+ source: Origin of this config ("global" or "project").
40
+ trusted: Whether hooks from this config are pre-trusted.
41
+
42
+ Returns:
43
+ Populated HookRegistry
44
+ """
45
+ registry = HookRegistry()
46
+
47
+ for event_type, hook_groups in config.items():
48
+ if event_type.startswith("_"):
49
+ continue # skip comment keys
50
+
51
+ if not isinstance(hook_groups, list):
52
+ logger.warning(f"Hook groups for '{event_type}' must be a list, skipping")
53
+ continue
54
+
55
+ for group in hook_groups:
56
+ if not isinstance(group, dict):
57
+ continue
58
+
59
+ matcher = group.get("matcher", "*")
60
+ hooks_data = group.get("hooks", [])
61
+
62
+ for hook_data in hooks_data:
63
+ if not isinstance(hook_data, dict):
64
+ continue
65
+ if hook_data.get("type") == "command" and not hook_data.get("command"):
66
+ continue
67
+
68
+ try:
69
+ hook = HookConfig(
70
+ matcher=matcher,
71
+ type=hook_data.get("type", "command"),
72
+ command=hook_data.get("command", hook_data.get("prompt", "")),
73
+ timeout=hook_data.get("timeout", 5000),
74
+ once=hook_data.get("once", False),
75
+ enabled=hook_data.get("enabled", True),
76
+ id=hook_data.get("id"),
77
+ source=source, # type: ignore[arg-type]
78
+ trusted=trusted,
79
+ )
80
+ registry.add_hook(event_type, hook)
81
+ except (ValueError, KeyError) as e:
82
+ logger.warning(f"Skipping invalid hook in {event_type}: {e}")
83
+
84
+ return registry
85
+
86
+
87
+ def get_registry_stats(registry: HookRegistry) -> dict[str, Any]:
88
+ """Get statistics about a registry."""
89
+ stats: dict[str, Any] = {
90
+ "total_hooks": registry.count_hooks(),
91
+ "enabled_hooks": 0,
92
+ "disabled_hooks": 0,
93
+ "by_event": {},
94
+ }
95
+
96
+ def _to_attr(event_type: str) -> str:
97
+ s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", event_type)
98
+ return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
99
+
100
+ for event_type in SUPPORTED_EVENT_TYPES:
101
+ attr = _to_attr(event_type)
102
+ hooks = getattr(registry, attr, [])
103
+ enabled = sum(1 for h in hooks if h.enabled)
104
+ disabled = len(hooks) - enabled
105
+ stats["enabled_hooks"] += enabled
106
+ stats["disabled_hooks"] += disabled
107
+ if hooks:
108
+ stats["by_event"][event_type] = {
109
+ "total": len(hooks),
110
+ "enabled": enabled,
111
+ "disabled": disabled,
112
+ }
113
+
114
+ return stats
@@ -0,0 +1,268 @@
1
+ """Hook trust engine for project-level hooks.
2
+
3
+ Requires explicit user trust before executing repository-controlled hooks.
4
+ Trust decisions are stored privately in XDG state and keyed by:
5
+ - project root directory
6
+ - hooks file path
7
+ - content hash (SHA-256 of hooks file)
8
+
9
+ This prevents repo-controlled hooks from executing automatically after
10
+ cloning or when content changes.
11
+ """
12
+
13
+ import contextlib
14
+ import hashlib
15
+ import json
16
+ import logging
17
+ import os
18
+ from pathlib import Path
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # XDG state directory for private trust storage
23
+ _TRUST_DIR = (
24
+ Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state"))
25
+ / "code_muse"
26
+ )
27
+ _TRUST_FILE = _TRUST_DIR / "hook_trust.json"
28
+
29
+ # Minimal env vars allowed for project hooks
30
+ _SAFE_ENV_ALLOWLIST = {
31
+ "PATH",
32
+ "HOME",
33
+ "SHELL",
34
+ "PWD",
35
+ "TERM",
36
+ "LANG",
37
+ "LC_ALL",
38
+ "USER",
39
+ "LOGNAME",
40
+ "CLAUDE_PROJECT_DIR",
41
+ "CLAUDE_TOOL_INPUT",
42
+ "CLAUDE_TOOL_NAME",
43
+ "CLAUDE_HOOK_EVENT",
44
+ "CLAUDE_CODE_HOOK",
45
+ "CLAUDE_FILE_PATH",
46
+ }
47
+
48
+ # Patterns that suggest a secret-bearing env var
49
+ _SECRET_ENV_PATTERNS = (
50
+ "token",
51
+ "secret",
52
+ "password",
53
+ "passwd",
54
+ "api_key",
55
+ "apikey",
56
+ "auth",
57
+ "credential",
58
+ "private_key",
59
+ "ssh_key",
60
+ "aws_access",
61
+ "aws_secret",
62
+ "bearer",
63
+ "jwt",
64
+ "client_secret",
65
+ "access_token",
66
+ "refresh_token",
67
+ "id_token",
68
+ )
69
+
70
+
71
+ def _ensure_private_dir(path: Path) -> Path:
72
+ """Ensure directory exists with 0o700 permissions."""
73
+ path.mkdir(parents=True, exist_ok=True)
74
+ with contextlib.suppress(OSError):
75
+ os.chmod(path, 0o700)
76
+ return path
77
+
78
+
79
+ def _atomic_write_private_json(file_path: Path, data: dict) -> None:
80
+ """Atomically write JSON with 0o600 permissions."""
81
+ tmp_path = file_path.with_suffix(".tmp")
82
+ try:
83
+ fd = os.open(
84
+ str(tmp_path),
85
+ os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
86
+ 0o600,
87
+ )
88
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
89
+ json.dump(data, f, indent=2)
90
+ f.flush()
91
+ os.fsync(f.fileno())
92
+ os.replace(str(tmp_path), str(file_path))
93
+ with contextlib.suppress(OSError):
94
+ os.chmod(file_path, 0o600)
95
+ except Exception:
96
+ with contextlib.suppress(OSError):
97
+ tmp_path.unlink(missing_ok=True)
98
+ raise
99
+
100
+
101
+ def _load_trust_db() -> dict[str, dict]:
102
+ """Load the trust database from private storage."""
103
+ _ensure_private_dir(_TRUST_DIR)
104
+ if not _TRUST_FILE.exists():
105
+ return {}
106
+ try:
107
+ with open(_TRUST_FILE, encoding="utf-8") as f:
108
+ data = json.load(f)
109
+ if isinstance(data, dict):
110
+ return data
111
+ except json.JSONDecodeError, OSError:
112
+ pass
113
+ return {}
114
+
115
+
116
+ def _save_trust_db(db: dict[str, dict]) -> None:
117
+ """Save the trust database to private storage."""
118
+ _ensure_private_dir(_TRUST_DIR)
119
+ _atomic_write_private_json(_TRUST_FILE, db)
120
+
121
+
122
+ def _compute_trust_key(
123
+ project_root: str | Path,
124
+ hooks_file_path: str | Path,
125
+ content_hash: str,
126
+ ) -> str:
127
+ """Compute a deterministic trust key."""
128
+ raw = f"{project_root}\n{hooks_file_path}\n{content_hash}"
129
+ return hashlib.sha256(raw.encode("utf-8")).hexdigest()
130
+
131
+
132
+ def compute_content_hash(content: str) -> str:
133
+ """Compute SHA-256 hash of hook file content."""
134
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()
135
+
136
+
137
+ def is_hook_trusted(
138
+ project_root: str | Path,
139
+ hooks_file_path: str | Path,
140
+ content_hash: str,
141
+ ) -> bool:
142
+ """Check whether a project hook file is explicitly trusted.
143
+
144
+ Args:
145
+ project_root: Absolute path to the project root directory.
146
+ hooks_file_path: Absolute path to the hooks configuration file.
147
+ content_hash: SHA-256 hash of the current hooks file content.
148
+
149
+ Returns:
150
+ True if the user has previously approved this exact content.
151
+ """
152
+ db = _load_trust_db()
153
+ key = _compute_trust_key(project_root, hooks_file_path, content_hash)
154
+ entry = db.get(key)
155
+ if entry is None:
156
+ return False
157
+ stored_hash = entry.get("content_hash")
158
+ if stored_hash != content_hash:
159
+ return False
160
+ return entry.get("trusted", False)
161
+
162
+
163
+ def approve_hook(
164
+ project_root: str,
165
+ hooks_file_path: str,
166
+ content_hash: str,
167
+ ) -> None:
168
+ """Explicitly mark a project hook file as trusted.
169
+
170
+ Args:
171
+ project_root: Absolute path to the project root directory.
172
+ hooks_file_path: Absolute path to the hooks configuration file.
173
+ content_hash: SHA-256 hash of the current hooks file content.
174
+ """
175
+ db = _load_trust_db()
176
+ key = _compute_trust_key(project_root, hooks_file_path, content_hash)
177
+ db[key] = {
178
+ "project_root": project_root,
179
+ "hooks_file_path": hooks_file_path,
180
+ "content_hash": content_hash,
181
+ "trusted": True,
182
+ }
183
+ _save_trust_db(db)
184
+ logger.info(f"Hook trusted: {hooks_file_path} (hash={content_hash[:16]}...)")
185
+
186
+
187
+ def revoke_hook_trust(
188
+ project_root: str,
189
+ hooks_file_path: str,
190
+ ) -> None:
191
+ """Revoke trust for a project hook file regardless of content hash.
192
+
193
+ Args:
194
+ project_root: Absolute path to the project root directory.
195
+ hooks_file_path: Absolute path to the hooks configuration file.
196
+ """
197
+ db = _load_trust_db()
198
+ keys_to_remove = []
199
+ for key, entry in db.items():
200
+ if (
201
+ entry.get("project_root") == project_root
202
+ and entry.get("hooks_file_path") == hooks_file_path
203
+ ):
204
+ keys_to_remove.append(key)
205
+ for key in keys_to_remove:
206
+ del db[key]
207
+ if keys_to_remove:
208
+ _save_trust_db(db)
209
+ logger.info(f"Hook trust revoked: {hooks_file_path}")
210
+
211
+
212
+ def build_minimal_hook_env(base_env: dict[str, str | None] = None) -> dict[str, str]:
213
+ """Build a minimal environment dict for project hooks.
214
+
215
+ Strips secret-like environment variables while preserving safe
216
+ operational variables and any hook-specific vars.
217
+
218
+ Args:
219
+ base_env: Optional base environment to filter. Defaults to os.environ.
220
+
221
+ Returns:
222
+ Filtered environment dictionary.
223
+ """
224
+ env = base_env if base_env is not None else os.environ.copy()
225
+ filtered: dict[str, str] = {}
226
+ for key, value in env.items():
227
+ upper_key = key.upper()
228
+ # Always allow safe vars
229
+ if upper_key in _SAFE_ENV_ALLOWLIST:
230
+ filtered[key] = value
231
+ continue
232
+ # Block anything that looks secret-bearing
233
+ lower_key = key.lower()
234
+ if any(pattern in lower_key for pattern in _SECRET_ENV_PATTERNS):
235
+ continue
236
+ # Block very long values that might be tokens/keys
237
+ if len(value) > 4096 and upper_key not in _SAFE_ENV_ALLOWLIST:
238
+ continue
239
+ filtered[key] = value
240
+ return filtered
241
+
242
+
243
+ def cap_hook_output(text: str, max_chars: int = 4096, max_lines: int = 256) -> str:
244
+ """Cap hook stdout/stderr to prevent unbounded output in model context.
245
+
246
+ Args:
247
+ text: Raw output string.
248
+ max_chars: Maximum total characters to retain.
249
+ max_lines: Maximum number of lines to retain.
250
+
251
+ Returns:
252
+ Capped output with a truncation marker if trimmed.
253
+ """
254
+ if not text:
255
+ return text
256
+ lines = text.splitlines()
257
+ if len(lines) > max_lines:
258
+ lines = lines[:max_lines]
259
+ truncated = True
260
+ else:
261
+ truncated = False
262
+ result = "\n".join(lines)
263
+ if len(result) > max_chars:
264
+ result = result[:max_chars]
265
+ truncated = True
266
+ if truncated:
267
+ result += "\n... [output truncated]"
268
+ return result