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,73 @@
1
+ """Constants for the ask_user_question tool."""
2
+
3
+ from typing import Final
4
+
5
+ # Question constraints
6
+ MAX_QUESTIONS_PER_CALL: Final[int] = 10 # Reasonable limit for a single TUI interaction
7
+ MIN_OPTIONS_PER_QUESTION: Final[int] = 2
8
+ MAX_OPTIONS_PER_QUESTION: Final[int] = 6
9
+ MAX_HEADER_LENGTH: Final[int] = 60
10
+ MAX_LABEL_LENGTH: Final[int] = 50
11
+ MAX_DESCRIPTION_LENGTH: Final[int] = 200
12
+ MAX_QUESTION_LENGTH: Final[int] = 500
13
+ MAX_OTHER_TEXT_LENGTH: Final[int] = 500
14
+
15
+ # UI settings
16
+ DEFAULT_TIMEOUT_SECONDS: Final[int] = 300 # 5 minutes
17
+ TIMEOUT_WARNING_SECONDS: Final[int] = 60 # Show warning at 60s remaining
18
+ AUTO_ADD_OTHER_OPTION: Final[bool] = True
19
+
20
+ # Other option configuration
21
+ OTHER_OPTION_LABEL: Final[str] = "Other"
22
+ OTHER_OPTION_DESCRIPTION: Final[str] = "Enter a custom option"
23
+
24
+ # Left panel width magic numbers (extracted for clarity)
25
+ LEFT_PANEL_PADDING: Final[int] = (
26
+ 14 # left(2) + cursor(2) + checkmark(2) + right(2) + buffer(6)
27
+ )
28
+ MIN_LEFT_PANEL_WIDTH: Final[int] = 21
29
+ MAX_LEFT_PANEL_WIDTH: Final[int] = 36
30
+
31
+ # Horizontal padding for panel content (matches left panel's " " prefix)
32
+ PANEL_CONTENT_PADDING: Final[str] = " "
33
+
34
+ # CI environment variables to check for non-interactive detection
35
+ # Use tuple for true immutability (Final only prevents reassignment, not mutation)
36
+ CI_ENV_VARS: Final[tuple[str, ...]] = (
37
+ "CI",
38
+ "GITHUB_ACTIONS",
39
+ "GITLAB_CI",
40
+ "JENKINS_URL",
41
+ "TRAVIS",
42
+ "CIRCLECI",
43
+ "BUILDKITE",
44
+ "AZURE_PIPELINES",
45
+ "TEAMCITY_VERSION",
46
+ )
47
+
48
+ # Terminal escape sequences for alternate screen buffer
49
+ ENTER_ALT_SCREEN: Final[str] = "\033[?1049h"
50
+ EXIT_ALT_SCREEN: Final[str] = "\033[?1049l"
51
+ CLEAR_AND_HOME: Final[str] = "\033[2J\033[H"
52
+
53
+ # Unicode symbols for TUI rendering
54
+ CURSOR_POINTER: Final[str] = "\u276f" # ❯
55
+ CURSOR_TRIANGLE: Final[str] = "\u25b6" # ▶
56
+ CHECK_MARK: Final[str] = "\u2713" # ✓
57
+ RADIO_FILLED: Final[str] = "\u25cf" # ●
58
+ BORDER_DOUBLE: Final[str] = "\u2550" # ═
59
+ ARROW_LEFT: Final[str] = "\u2190" # ←
60
+ ARROW_RIGHT: Final[str] = "\u2192" # →
61
+ ARROW_UP: Final[str] = "\u2191" # ↑
62
+ ARROW_DOWN: Final[str] = "\u2193" # ↓
63
+ PIPE_SEPARATOR: Final[str] = "\u2502" # │
64
+
65
+ # Panel rendering
66
+ MAX_READABLE_WIDTH: Final[int] = 120
67
+ HELP_BORDER_WIDTH: Final[int] = 50
68
+
69
+ # Error formatting
70
+ MAX_VALIDATION_ERRORS_SHOWN: Final[int] = 3
71
+
72
+ # Terminal synchronization delay (seconds)
73
+ TERMINAL_SYNC_DELAY: Final[float] = 0.05
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env python
2
+ """Manual demo script for the ask_user_question TUI.
3
+
4
+ This is NOT an automated test - it's for interactive visual testing.
5
+ Run this script directly to demo the TUI:
6
+ python -m code_muse.tools.ask_user_question.demo_tui
7
+ """
8
+
9
+ from .handler import ask_user_question
10
+
11
+
12
+ def main():
13
+ """Run a test of the ask_user_question TUI."""
14
+ print("Testing ask_user_question TUI...")
15
+ print("=" * 50)
16
+
17
+ # Test single question, single select
18
+ result = ask_user_question(
19
+ [
20
+ {
21
+ "question": "Which database should we use for this project?",
22
+ "header": "Database",
23
+ "multi_select": False,
24
+ "options": [
25
+ {
26
+ "label": "PostgreSQL",
27
+ "description": "Relational database, ACID compliant, great for complex queries",
28
+ },
29
+ {
30
+ "label": "MongoDB",
31
+ "description": "Document store, flexible schema, good for rapid iteration",
32
+ },
33
+ {
34
+ "label": "Redis",
35
+ "description": "In-memory store, ultra-fast, best for caching",
36
+ },
37
+ {
38
+ "label": "SQLite",
39
+ "description": "Lightweight, file-based, perfect for local development",
40
+ },
41
+ ],
42
+ }
43
+ ]
44
+ )
45
+
46
+ print("\n" + "=" * 50)
47
+ print("Result:")
48
+ print(f" Answers: {result.answers}")
49
+ print(f" Cancelled: {result.cancelled}")
50
+ print(f" Error: {result.error}")
51
+ print(f" Timed out: {result.timed_out}")
52
+
53
+
54
+ if __name__ == "__main__":
55
+ main()
@@ -0,0 +1,232 @@
1
+ """Main handler for ask_user_question tool."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import os
6
+ import sys
7
+ from typing import Any
8
+
9
+ from pydantic import ValidationError
10
+
11
+ from code_muse.command_line.wiggum_state import is_wiggum_active
12
+ from code_muse.tools.subagent_context import is_subagent
13
+
14
+ from .constants import CI_ENV_VARS, DEFAULT_TIMEOUT_SECONDS, MAX_VALIDATION_ERRORS_SHOWN
15
+ from .models import (
16
+ AskUserQuestionInput,
17
+ AskUserQuestionOutput,
18
+ Question,
19
+ QuestionAnswer,
20
+ )
21
+ from .terminal_ui import CancelledException, interactive_question_picker
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class AsyncContextError(RuntimeError):
27
+ """Raised when TUI is called from async context without await."""
28
+
29
+ pass
30
+
31
+
32
+ def _cancelled_response() -> AskUserQuestionOutput:
33
+ """Create a standardized cancelled response.
34
+
35
+ Note: cancelled=True means intentional user action, not an error.
36
+ The error field is left None since cancellation is expected behavior.
37
+ """
38
+ return AskUserQuestionOutput.cancelled_response()
39
+
40
+
41
+ def is_interactive() -> bool:
42
+ """
43
+ Check if we're running in an interactive terminal.
44
+
45
+ Returns:
46
+ True if stdin is a TTY and we're not in a CI environment.
47
+ """
48
+ # stdin might be replaced with a non-file object in some embedding scenarios
49
+ # (e.g., Jupyter, pytest capture, or custom wrappers), so we catch AttributeError
50
+ try:
51
+ if not sys.stdin.isatty():
52
+ return False
53
+ except AttributeError, OSError:
54
+ return False
55
+
56
+ return not any(os.environ.get(var) for var in CI_ENV_VARS)
57
+
58
+
59
+ def ask_user_question(
60
+ questions: list[Question | dict[str, Any]],
61
+ timeout: int = DEFAULT_TIMEOUT_SECONDS,
62
+ ) -> AskUserQuestionOutput:
63
+ """
64
+ Ask the user one or more interactive multiple-choice questions.
65
+
66
+ This tool displays questions in a split-panel terminal TUI and captures
67
+ user responses through keyboard navigation and selection.
68
+
69
+ Args:
70
+ questions: List of question objects, each containing:
71
+ - question (str): The full question text
72
+ - header (str): Short label (max 60 chars)
73
+ - multi_select (bool, optional): Allow multiple selections
74
+ - options (list): 2-6 options, each with label and optional description
75
+ timeout: Inactivity timeout in seconds (default: 300)
76
+
77
+ Returns:
78
+ AskUserQuestionOutput containing:
79
+ - answers (list): List of answer objects for each question
80
+ - cancelled (bool): True if user cancelled
81
+ - error (str | None): Error message if failed
82
+ - timed_out (bool): True if timed out
83
+
84
+ Example:
85
+ >>> result = ask_user_question([{
86
+ ... "question": "Which database?",
87
+ ... "header": "Database",
88
+ ... "options": [
89
+ ... {"label": "PostgreSQL", "description": "Relational DB"},
90
+ ... {"label": "MongoDB", "description": "Document store"}
91
+ ... ]
92
+ ... }])
93
+ >>> print(result.answers[0].selected_options)
94
+ ['PostgreSQL']
95
+ """
96
+ logger.info("ask_user_question called with %d questions", len(questions))
97
+
98
+ # Block interactive tools in sub-agent context
99
+ if is_subagent():
100
+ logger.warning("ask_user_question called from sub-agent context - disabled")
101
+ return AskUserQuestionOutput.error_response(
102
+ "Interactive tools are disabled for sub-agents. "
103
+ "Sub-agents should make reasonable decisions or return to the parent agent "
104
+ "if user input is required."
105
+ )
106
+
107
+ # Block interactive tools in wiggum (autonomous loop) mode
108
+ if is_wiggum_active():
109
+ logger.warning("ask_user_question called during wiggum mode - disabled")
110
+ return AskUserQuestionOutput.error_response(
111
+ "Interactive tools are disabled during /wiggum mode. "
112
+ "The agent is running autonomously in a loop. "
113
+ "Make a reasonable decision to proceed, or stop and wait for user input "
114
+ "by completing the current task."
115
+ )
116
+
117
+ # Check for interactive environment
118
+ if not is_interactive():
119
+ logger.warning("Non-interactive environment detected")
120
+ return AskUserQuestionOutput.error_response(
121
+ "Cannot ask questions: not running in an interactive terminal. "
122
+ "Please provide configuration through arguments or config files."
123
+ )
124
+
125
+ # Validate input
126
+ try:
127
+ validated_input = _validate_input(questions)
128
+ except ValidationError as e:
129
+ error_msg = _format_validation_error(e)
130
+ logger.warning("Validation error: %s", error_msg)
131
+ return AskUserQuestionOutput.error_response(error_msg)
132
+ except (TypeError, ValueError) as e:
133
+ logger.error("Unexpected validation error: %s", e, exc_info=True)
134
+ return AskUserQuestionOutput.error_response(f"Validation error: {e!s}")
135
+
136
+ # Run the interactive TUI
137
+ try:
138
+ answers, cancelled, timed_out = _run_interactive_picker(
139
+ validated_input.questions, timeout
140
+ )
141
+
142
+ if timed_out:
143
+ logger.info("Interaction timed out after %d seconds", timeout)
144
+ return AskUserQuestionOutput.timeout_response(timeout)
145
+
146
+ if cancelled:
147
+ logger.info("User cancelled the interaction")
148
+ return _cancelled_response()
149
+
150
+ logger.info("Successfully collected %d answers", len(answers))
151
+ return AskUserQuestionOutput(answers=answers)
152
+
153
+ except CancelledException, KeyboardInterrupt:
154
+ logger.info("User cancelled the interaction")
155
+ return _cancelled_response()
156
+
157
+ except OSError as e:
158
+ logger.error("Unexpected error during interaction: %s", e)
159
+ return AskUserQuestionOutput.error_response(f"Interaction error: {e!s}")
160
+
161
+
162
+ def _run_interactive_picker(
163
+ questions: list[Question], timeout: int
164
+ ) -> tuple[list[QuestionAnswer], bool, bool]:
165
+ """Run the interactive TUI, handling async context detection.
166
+
167
+ If called from an async context, raises AsyncContextError with guidance.
168
+ For async callers, use `await interactive_question_picker()` directly.
169
+ """
170
+ # Check for async context BEFORE creating the coroutine to avoid
171
+ # "coroutine was never awaited" warnings on the error path.
172
+ try:
173
+ asyncio.get_running_loop()
174
+ # Already in async context - fail fast with helpful message
175
+ # Note: We avoid nest_asyncio.apply() as it globally patches the event loop,
176
+ # which can break other async code in the process and is not thread-safe.
177
+ raise AsyncContextError(
178
+ "Cannot run interactive TUI from within an async context. "
179
+ "Either call from synchronous code, or use "
180
+ "'await interactive_question_picker()' directly for async callers."
181
+ )
182
+ except RuntimeError:
183
+ # No running loop - safe to proceed with asyncio.run()
184
+ pass
185
+
186
+ return asyncio.run(interactive_question_picker(questions, timeout_seconds=timeout))
187
+
188
+
189
+ def _validate_input(
190
+ questions: list[Question | dict[str, Any]],
191
+ ) -> AskUserQuestionInput:
192
+ """
193
+ Validate and convert input dictionaries to Pydantic models.
194
+
195
+ Args:
196
+ questions: Raw question dictionaries or validated Question models
197
+
198
+ Returns:
199
+ Validated AskUserQuestionInput model
200
+
201
+ Raises:
202
+ ValidationError: If input doesn't match schema
203
+ """
204
+ # Single-pass validation - Pydantic handles nested dict->model conversion
205
+ return AskUserQuestionInput.model_validate({"questions": questions})
206
+
207
+
208
+ def _format_validation_error(error: ValidationError) -> str:
209
+ """
210
+ Format a Pydantic ValidationError into a readable string.
211
+
212
+ Args:
213
+ error: The Pydantic ValidationError
214
+
215
+ Returns:
216
+ Human-readable error message
217
+ """
218
+ errors = error.errors()
219
+ if not errors:
220
+ return "Validation error"
221
+
222
+ messages = []
223
+ for err in errors[:MAX_VALIDATION_ERRORS_SHOWN]:
224
+ loc = ".".join(str(x) for x in err["loc"])
225
+ msg = err["msg"]
226
+ messages.append(f"{loc}: {msg}")
227
+
228
+ result = "Validation error: " + "; ".join(messages)
229
+ if len(errors) > MAX_VALIDATION_ERRORS_SHOWN:
230
+ result += f" (and {len(errors) - MAX_VALIDATION_ERRORS_SHOWN} more)"
231
+
232
+ return result
@@ -0,0 +1,302 @@
1
+ """Pydantic models for the ask_user_question tool."""
2
+
3
+ import re
4
+ from typing import TYPE_CHECKING, Annotated, Any
5
+
6
+ from pydantic import BaseModel, BeforeValidator, Field, model_validator
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Callable
10
+
11
+ from .constants import (
12
+ MAX_DESCRIPTION_LENGTH,
13
+ MAX_HEADER_LENGTH,
14
+ MAX_LABEL_LENGTH,
15
+ MAX_OPTIONS_PER_QUESTION,
16
+ MAX_OTHER_TEXT_LENGTH,
17
+ MAX_QUESTION_LENGTH,
18
+ MAX_QUESTIONS_PER_CALL,
19
+ MIN_OPTIONS_PER_QUESTION,
20
+ )
21
+
22
+ __all__ = [
23
+ "AskUserQuestionInput",
24
+ "AskUserQuestionOutput",
25
+ "Question",
26
+ "QuestionAnswer",
27
+ "QuestionOption",
28
+ "sanitize_text",
29
+ ]
30
+
31
+ # Regex to match ANSI escape codes
32
+ ANSI_ESCAPE_PATTERN = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
33
+
34
+
35
+ def sanitize_text(text: str) -> str:
36
+ """Remove ANSI escape codes and strip whitespace."""
37
+ return ANSI_ESCAPE_PATTERN.sub("", text).strip()
38
+
39
+
40
+ def _make_sanitizer(
41
+ *, allow_none: bool = False, default: str = ""
42
+ ) -> Callable[[Any], str]:
43
+ """Create a sanitizer with configurable None handling.
44
+
45
+ Args:
46
+ allow_none: If True, None returns default. If False, raises ValueError.
47
+ default: Value to return when allow_none=True and input is None.
48
+
49
+ Returns:
50
+ A sanitizer function for use with BeforeValidator.
51
+ """
52
+
53
+ def sanitize(v: Any) -> str:
54
+ if v is None:
55
+ if allow_none:
56
+ return default
57
+ raise ValueError("Value cannot be None")
58
+ return sanitize_text(str(v))
59
+
60
+ return sanitize
61
+
62
+
63
+ # Pre-built sanitizers for common cases
64
+ _sanitize_required = _make_sanitizer(allow_none=False)
65
+ _sanitize_optional = _make_sanitizer(allow_none=True, default="")
66
+
67
+
68
+ def _sanitize_header(v: Any) -> str:
69
+ """Sanitize header: remove ANSI, strip, replace spaces with hyphens."""
70
+ return _sanitize_required(v).replace(" ", "-")
71
+
72
+
73
+ def _check_unique(items: list[str], field_name: str) -> None:
74
+ """Raise ValueError if items has duplicates (case-insensitive)."""
75
+ lowered = [i.lower() for i in items]
76
+ if len(lowered) != len(set(lowered)):
77
+ raise ValueError(f"{field_name} must be unique")
78
+
79
+
80
+ class QuestionOption(BaseModel):
81
+ """
82
+ A single selectable option for a question.
83
+
84
+ Attributes:
85
+ label: Short, descriptive name for the option (1-5 words recommended)
86
+ description: Longer explanation of what selecting this option means
87
+ """
88
+
89
+ label: Annotated[
90
+ str,
91
+ BeforeValidator(_sanitize_required),
92
+ Field(
93
+ min_length=1,
94
+ max_length=MAX_LABEL_LENGTH,
95
+ description="Short option name (1-5 words)",
96
+ ),
97
+ ]
98
+ description: Annotated[
99
+ str,
100
+ BeforeValidator(_sanitize_optional),
101
+ Field(
102
+ default="",
103
+ max_length=MAX_DESCRIPTION_LENGTH,
104
+ description="Explanation of what this option means",
105
+ ),
106
+ ]
107
+
108
+
109
+ class Question(BaseModel):
110
+ """
111
+ A single question with multiple-choice options.
112
+
113
+ Attributes:
114
+ question: The full question text displayed to the user
115
+ header: Short label used for compact display and response mapping
116
+ multi_select: Whether user can select multiple options
117
+ options: List of 2-6 selectable options
118
+ """
119
+
120
+ question: Annotated[
121
+ str,
122
+ BeforeValidator(_sanitize_required),
123
+ Field(
124
+ min_length=1,
125
+ max_length=MAX_QUESTION_LENGTH,
126
+ description="The full question text to display",
127
+ ),
128
+ ]
129
+ header: Annotated[
130
+ str,
131
+ BeforeValidator(_sanitize_header),
132
+ Field(
133
+ min_length=1,
134
+ max_length=MAX_HEADER_LENGTH,
135
+ description="Short label for compact display (max 60 chars)",
136
+ ),
137
+ ]
138
+ multi_select: Annotated[
139
+ bool,
140
+ Field(
141
+ default=False,
142
+ description="If true, user can select multiple options",
143
+ ),
144
+ ]
145
+ options: Annotated[
146
+ list[QuestionOption],
147
+ Field(
148
+ min_length=MIN_OPTIONS_PER_QUESTION,
149
+ max_length=MAX_OPTIONS_PER_QUESTION,
150
+ description="Array of 2-6 selectable options",
151
+ ),
152
+ ]
153
+
154
+ @model_validator(mode="after")
155
+ def validate_unique_labels(self) -> Question:
156
+ """Ensure all option labels are unique within a question."""
157
+ _check_unique([opt.label for opt in self.options], "Option labels")
158
+ return self
159
+
160
+
161
+ class AskUserQuestionInput(BaseModel):
162
+ """
163
+ Input schema for the ask_user_question tool.
164
+
165
+ Attributes:
166
+ questions: List of 1-10 questions to ask the user
167
+ """
168
+
169
+ questions: Annotated[
170
+ list[Question],
171
+ Field(
172
+ min_length=1,
173
+ max_length=MAX_QUESTIONS_PER_CALL,
174
+ description="Array of 1-10 questions to ask",
175
+ ),
176
+ ]
177
+
178
+ @model_validator(mode="after")
179
+ def validate_unique_headers(self) -> AskUserQuestionInput:
180
+ """Ensure all question headers are unique."""
181
+ _check_unique([q.header for q in self.questions], "Question headers")
182
+ return self
183
+
184
+
185
+ class QuestionAnswer(BaseModel):
186
+ """
187
+ Answer to a single question.
188
+
189
+ Attributes:
190
+ question_header: The header of the question being answered
191
+ selected_options: List of labels for selected options
192
+ other_text: Custom text if user selected "Other" option
193
+ """
194
+
195
+ question_header: Annotated[
196
+ str,
197
+ Field(description="Header of the answered question"),
198
+ ]
199
+ selected_options: Annotated[
200
+ list[str],
201
+ Field(
202
+ default_factory=list,
203
+ description="Labels of selected options",
204
+ ),
205
+ ]
206
+ other_text: Annotated[
207
+ str | None,
208
+ Field(
209
+ default=None,
210
+ max_length=MAX_OTHER_TEXT_LENGTH,
211
+ description="Custom text if 'Other' was selected",
212
+ ),
213
+ ]
214
+
215
+ @property
216
+ def has_other(self) -> bool:
217
+ """Check if user provided custom 'Other' input."""
218
+ return self.other_text is not None
219
+
220
+ @property
221
+ def is_empty(self) -> bool:
222
+ """Check if no options were selected."""
223
+ return not self.selected_options and self.other_text is None
224
+
225
+
226
+ class AskUserQuestionOutput(BaseModel):
227
+ """
228
+ Output schema for the ask_user_question tool.
229
+
230
+ Attributes:
231
+ answers: List of answers to all questions
232
+ cancelled: Whether user cancelled the interaction
233
+ error: Error message if something went wrong
234
+ timed_out: Whether the interaction timed out
235
+ """
236
+
237
+ answers: Annotated[
238
+ list[QuestionAnswer],
239
+ Field(
240
+ default_factory=list,
241
+ description="Answers to all questions",
242
+ ),
243
+ ]
244
+ cancelled: Annotated[
245
+ bool,
246
+ Field(
247
+ default=False,
248
+ description="True if user cancelled (Esc/Ctrl+C)",
249
+ ),
250
+ ]
251
+ error: Annotated[
252
+ str | None,
253
+ Field(
254
+ default=None,
255
+ description="Error message if interaction failed",
256
+ ),
257
+ ]
258
+ timed_out: Annotated[
259
+ bool,
260
+ Field(
261
+ default=False,
262
+ description="True if interaction timed out",
263
+ ),
264
+ ]
265
+
266
+ @property
267
+ def success(self) -> bool:
268
+ """Check if interaction completed successfully."""
269
+ return not self.cancelled and self.error is None and not self.timed_out
270
+
271
+ @classmethod
272
+ def error_response(cls, error: str) -> AskUserQuestionOutput:
273
+ """Create an error response."""
274
+ return cls(error=error)
275
+
276
+ @classmethod
277
+ def cancelled_response(cls) -> AskUserQuestionOutput:
278
+ """Create a cancelled response (intentional user action, not an error)."""
279
+ return cls(answers=[], cancelled=True, error=None)
280
+
281
+ @classmethod
282
+ def timeout_response(cls, timeout: int) -> AskUserQuestionOutput:
283
+ """Create a timeout response."""
284
+ return cls(
285
+ answers=[],
286
+ cancelled=False,
287
+ timed_out=True,
288
+ error=f"Interaction timed out after {timeout} seconds of inactivity",
289
+ )
290
+
291
+ def get_answer(self, header: str) -> QuestionAnswer | None:
292
+ """Get answer by question header (case-insensitive)."""
293
+ header_lower = header.lower()
294
+ return next(
295
+ (a for a in self.answers if a.question_header.lower() == header_lower),
296
+ None,
297
+ )
298
+
299
+ def get_selected(self, header: str) -> list[str]:
300
+ """Get selected options for a question by header."""
301
+ answer = self.get_answer(header)
302
+ return answer.selected_options if answer else []
@@ -0,0 +1,37 @@
1
+ """Tool registration for ask_user_question."""
2
+
3
+ from typing import TYPE_CHECKING, Annotated, Any
4
+
5
+ from pydantic import Field
6
+ from pydantic_ai import RunContext
7
+
8
+ from .handler import ask_user_question as _ask_user_question_impl
9
+ from .models import AskUserQuestionOutput
10
+
11
+ if TYPE_CHECKING:
12
+ from pydantic_ai import Agent
13
+
14
+
15
+ def register_ask_user_question(agent: Agent) -> None:
16
+ """Register the ask_user_question tool with the given agent."""
17
+
18
+ @agent.tool
19
+ def ask_user_question(
20
+ context: RunContext, # noqa: ARG001 - Required by framework
21
+ questions: Annotated[
22
+ list[dict[str, Any]],
23
+ Field(
24
+ description=(
25
+ "Array of question objects. Each question should include: "
26
+ "'question' (string), 'header' (short string), "
27
+ "optional 'multi_select' (boolean), and 'options' "
28
+ "(array of option objects with 'label' and optional "
29
+ "'description')."
30
+ )
31
+ ),
32
+ ],
33
+ ) -> AskUserQuestionOutput:
34
+ """Ask the user multiple related questions in an interactive TUI."""
35
+ # Keep the external tool schema simple for provider compatibility.
36
+ # The handler performs the real nested validation and normalization.
37
+ return _ask_user_question_impl(questions)