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,156 @@
1
+ """Theme configuration for ask_user_question TUI.
2
+
3
+ This module provides theming support that integrates with Muse's
4
+ color configuration system. It allows the TUI to inherit colors from
5
+ the global configuration.
6
+ """
7
+
8
+ from collections.abc import Mapping
9
+ from typing import TYPE_CHECKING, NamedTuple, TypeVar
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Callable
13
+
14
+ __all__ = ["TUIColors", "RichColors", "get_tui_colors", "get_rich_colors"]
15
+
16
+ # Cached config getter to avoid repeated imports
17
+ _config_getter: Callable[[str], str | None] | None = None
18
+
19
+
20
+ def _get_config_value(key: str) -> str | None:
21
+ """Safely get a config value, caching the import for performance."""
22
+ global _config_getter
23
+ if _config_getter is None:
24
+ try:
25
+ from code_muse.config import get_value
26
+
27
+ _config_getter = get_value
28
+ except ImportError:
29
+ _config_getter = lambda _: None # noqa: E731
30
+ return _config_getter(key)
31
+
32
+
33
+ _T = TypeVar("_T", bound=NamedTuple)
34
+
35
+
36
+ def _apply_config_overrides[T: NamedTuple](
37
+ default: _T, config_map: Mapping[str, str]
38
+ ) -> _T:
39
+ """Apply config overrides to a color scheme.
40
+
41
+ Args:
42
+ default: Default NamedTuple instance
43
+ config_map: Mapping of field names to config keys
44
+
45
+ Returns:
46
+ New NamedTuple with overrides applied
47
+ """
48
+ overrides = {}
49
+ for field, config_key in config_map.items():
50
+ value = _get_config_value(config_key)
51
+ if value:
52
+ overrides[field] = value
53
+ return default._replace(**overrides) if overrides else default
54
+
55
+
56
+ class TUIColors(NamedTuple):
57
+ """Color scheme for the ask_user_question TUI."""
58
+
59
+ # Header and title colors
60
+ header_bold: str = "bold cyan"
61
+ header_dim: str = "fg:ansicyan dim"
62
+
63
+ # Cursor and selection colors
64
+ cursor_active: str = "fg:ansigreen bold"
65
+ cursor_inactive: str = "fg:ansiwhite"
66
+ selected: str = "fg:ansicyan"
67
+ selected_check: str = "fg:ansigreen"
68
+
69
+ # Text colors
70
+ text_normal: str = ""
71
+ text_dim: str = "fg:ansiwhite dim"
72
+ text_warning: str = "fg:ansiyellow bold"
73
+
74
+ # Help text colors
75
+ help_key: str = "fg:ansigreen"
76
+ help_text: str = "fg:ansiwhite dim"
77
+
78
+ # Error colors
79
+ error: str = "fg:ansired"
80
+
81
+
82
+ # Create defaults after class definitions
83
+ _DEFAULT_TUI = TUIColors()
84
+
85
+ # Mapping of configurable TUI color fields to config keys
86
+ _TUI_CONFIG_MAP: dict[str, str] = {
87
+ "header_bold": "tui_header_color",
88
+ "cursor_active": "tui_cursor_color",
89
+ "selected": "tui_selected_color",
90
+ }
91
+
92
+
93
+ def get_tui_colors() -> TUIColors:
94
+ """Get the current TUI color scheme.
95
+
96
+ Loads colors from Muse's configuration system for custom theming.
97
+ Falls back to defaults for any missing config values.
98
+
99
+ Returns:
100
+ TUIColors instance with the current theme.
101
+ """
102
+ return _apply_config_overrides(_DEFAULT_TUI, _TUI_CONFIG_MAP)
103
+
104
+
105
+ # Rich console color mappings for the right panel
106
+ class RichColors(NamedTuple):
107
+ """Rich markup colors for the question panel."""
108
+
109
+ # Header colors (Rich markup format)
110
+ header: str = "bold cyan"
111
+ progress: str = "dim"
112
+
113
+ # Question text
114
+ question: str = "bold"
115
+ question_hint: str = "dim"
116
+
117
+ # Option colors
118
+ cursor: str = "green bold"
119
+ selected: str = "cyan"
120
+ normal: str = ""
121
+ description: str = "dim"
122
+
123
+ # Input field
124
+ input_label: str = "bold yellow"
125
+ input_text: str = "green"
126
+ input_hint: str = "dim"
127
+
128
+ # Help overlay
129
+ help_border: str = "bold cyan"
130
+ help_title: str = "bold cyan"
131
+ help_section: str = "bold"
132
+ help_key: str = "green"
133
+ help_close: str = "dim"
134
+
135
+ # Timeout warning
136
+ timeout_warning: str = "bold yellow"
137
+
138
+
139
+ _DEFAULT_RICH = RichColors()
140
+
141
+ # Mapping of configurable Rich color fields to config keys
142
+ _RICH_CONFIG_MAP: dict[str, str] = {
143
+ "header": "tui_rich_header_color",
144
+ "cursor": "tui_rich_cursor_color",
145
+ }
146
+
147
+
148
+ def get_rich_colors() -> RichColors:
149
+ """Get Rich console colors for the question panel.
150
+
151
+ Falls back to defaults for any missing config values.
152
+
153
+ Returns:
154
+ RichColors instance with current theme.
155
+ """
156
+ return _apply_config_overrides(_DEFAULT_RICH, _RICH_CONFIG_MAP)
@@ -0,0 +1,422 @@
1
+ """TUI loop and keyboard handlers for ask_user_question.
2
+
3
+ This module contains the main TUI application loop and all keyboard bindings.
4
+ Separated from terminal_ui.py to keep files under 600 lines.
5
+ """
6
+
7
+ import asyncio
8
+ import shutil
9
+ import sys
10
+ from collections.abc import Callable
11
+ from dataclasses import dataclass
12
+ from typing import TYPE_CHECKING
13
+
14
+ from prompt_toolkit import Application
15
+ from prompt_toolkit.application import run_in_terminal
16
+ from prompt_toolkit.formatted_text import ANSI, FormattedText
17
+ from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
18
+ from prompt_toolkit.layout import Layout, VSplit, Window
19
+ from prompt_toolkit.layout.controls import FormattedTextControl
20
+ from prompt_toolkit.layout.dimension import Dimension
21
+ from prompt_toolkit.output import create_output
22
+ from prompt_toolkit.output.color_depth import ColorDepth
23
+ from prompt_toolkit.widgets import Frame
24
+
25
+ from .constants import (
26
+ ARROW_DOWN,
27
+ ARROW_LEFT,
28
+ ARROW_RIGHT,
29
+ ARROW_UP,
30
+ CHECK_MARK,
31
+ CURSOR_TRIANGLE,
32
+ )
33
+ from .renderers import render_question_panel
34
+ from .theme import get_rich_colors, get_tui_colors
35
+
36
+ if TYPE_CHECKING:
37
+ from .models import QuestionAnswer
38
+ from .terminal_ui import QuestionUIState
39
+
40
+
41
+ def _wait_for_keypress() -> None:
42
+ """Block until any key is pressed, reading directly from the terminal.
43
+
44
+ On Unix: switches to raw mode so a single keypress returns immediately.
45
+ On Windows: uses msvcrt.getch() which already reads a single key.
46
+ Called inside run_in_terminal's cooked-mode context.
47
+ """
48
+ try:
49
+ # Windows
50
+ import msvcrt
51
+
52
+ msvcrt.getch()
53
+ except ImportError:
54
+ # Unix / macOS
55
+ import select
56
+ import termios
57
+ import tty
58
+
59
+ fd = sys.__stdin__.fileno()
60
+ old_settings = termios.tcgetattr(fd)
61
+ try:
62
+ tty.setraw(fd)
63
+ ch = sys.__stdin__.read(1)
64
+ # Arrow/F-keys send multi-byte escape sequences (e.g. \x1b[A).
65
+ # Drain trailing bytes so they don't leak into prompt_toolkit.
66
+ if ch == "\x1b":
67
+ while select.select([sys.__stdin__], [], [], 0.01)[0]:
68
+ sys.__stdin__.read(1)
69
+ finally:
70
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
71
+
72
+
73
+ @dataclass(slots=True)
74
+ class TUIResult:
75
+ """Result holder for the TUI interaction."""
76
+
77
+ cancelled: bool = False
78
+ confirmed: bool = False
79
+
80
+
81
+ async def run_question_tui(
82
+ state: QuestionUIState,
83
+ ) -> tuple[list[QuestionAnswer], bool, bool]:
84
+ """Run the main question TUI loop.
85
+
86
+ Returns:
87
+ Tuple of (answers, cancelled, timed_out)
88
+ """
89
+ result = TUIResult()
90
+ timed_out = False
91
+ kb = KeyBindings()
92
+
93
+ # --- Factory for dual-mode handlers (vim keys that type in text mode) ---
94
+ def make_dual_mode_handler(
95
+ char: str, action: Callable[[], None]
96
+ ) -> Callable[[KeyPressEvent], None]:
97
+ """Create handler that types char in text mode, calls action otherwise."""
98
+
99
+ def handler(event: KeyPressEvent) -> None:
100
+ state.reset_activity_timer()
101
+ if state.entering_other_text:
102
+ state.other_text_buffer += char
103
+ else:
104
+ action()
105
+ event.app.invalidate()
106
+
107
+ return handler
108
+
109
+ # --- Factory for arrow key navigation (don't type in text mode) ---
110
+ def make_arrow_handler(
111
+ action: Callable[[], None],
112
+ ) -> Callable[[KeyPressEvent], None]:
113
+ """Create handler that only fires when not in text input mode."""
114
+
115
+ def handler(event: KeyPressEvent) -> None:
116
+ state.reset_activity_timer()
117
+ if not state.entering_other_text:
118
+ action()
119
+ event.app.invalidate()
120
+
121
+ return handler
122
+
123
+ kb.add("up")(make_arrow_handler(state.move_cursor_up))
124
+ kb.add("down")(make_arrow_handler(state.move_cursor_down))
125
+ kb.add("left")(make_arrow_handler(state.prev_question))
126
+ kb.add("right")(make_arrow_handler(state.next_question))
127
+
128
+ # --- Vim-style navigation (types letter in text mode) ---
129
+ kb.add("k")(make_dual_mode_handler("k", state.move_cursor_up))
130
+ kb.add("j")(make_dual_mode_handler("j", state.move_cursor_down))
131
+ kb.add("h")(make_dual_mode_handler("h", state.prev_question))
132
+ kb.add("l")(make_dual_mode_handler("l", state.next_question))
133
+ kb.add("g")(make_dual_mode_handler("g", state.jump_to_first))
134
+ kb.add("G")(make_dual_mode_handler("G", state.jump_to_last))
135
+
136
+ # --- Selection controls (also dual-mode) ---
137
+ def _toggle_help() -> None:
138
+ state.show_help = not state.show_help
139
+
140
+ kb.add("a")(make_dual_mode_handler("a", state.select_all_options))
141
+ kb.add("n")(make_dual_mode_handler("n", state.select_no_options))
142
+ kb.add("?")(make_dual_mode_handler("?", _toggle_help))
143
+
144
+ @kb.add("space")
145
+ def toggle_option(event: KeyPressEvent) -> None:
146
+ """Toggle/select the current option.
147
+
148
+ For multi-select: toggles the checkbox
149
+ For single-select: selects the radio button (without advancing)
150
+ """
151
+ state.reset_activity_timer()
152
+ if state.entering_other_text:
153
+ state.other_text_buffer += " "
154
+ event.app.invalidate()
155
+ return
156
+
157
+ # Check if current option is "Other"
158
+ if state.is_other_option(state.current_cursor):
159
+ state.enter_other_text_mode()
160
+ event.app.invalidate()
161
+ return
162
+
163
+ if state.current_question.multi_select:
164
+ # Toggle checkbox
165
+ state.toggle_current_option()
166
+ else:
167
+ # Select radio button (doesn't advance)
168
+ state.select_current_option()
169
+ event.app.invalidate()
170
+
171
+ @kb.add("enter")
172
+ def advance_question(event: KeyPressEvent) -> None:
173
+ """Select current option and advance, or submit if confirming selection.
174
+
175
+ Behavior:
176
+ - Selects the current option (for single-select) or enters Other mode
177
+ - Advances to next question if not on last
178
+ - On last question: only submits if cursor is on an already-selected option
179
+ (i.e., user is confirming their choice by pressing Enter on it again)
180
+ """
181
+ state.reset_activity_timer()
182
+ if state.entering_other_text:
183
+ # Confirm the "Other" text using centralized method
184
+ state.commit_other_text()
185
+ event.app.invalidate()
186
+ return
187
+
188
+ # Check if current option is "Other"
189
+ if state.is_other_option(state.current_cursor):
190
+ state.enter_other_text_mode()
191
+ event.app.invalidate()
192
+ return
193
+
194
+ is_last_question = state.current_question_index == len(state.questions) - 1
195
+ cursor_is_on_selected = state.is_option_selected(state.current_cursor)
196
+
197
+ # For single-select, select the current option when pressing Enter
198
+ if not state.current_question.multi_select:
199
+ state.select_current_option()
200
+
201
+ # Advance to next question if not on the last one
202
+ if not is_last_question:
203
+ state.next_question()
204
+ event.app.invalidate()
205
+ else:
206
+ # On the last question:
207
+ # Only submit if cursor was already on the selected option (confirming)
208
+ # This prevents accidental submission when browsing options
209
+ if cursor_is_on_selected:
210
+ result.confirmed = True
211
+ event.app.exit()
212
+ else:
213
+ # Just selected a new option, update display but don't submit
214
+ # User needs to press Enter again to confirm
215
+ event.app.invalidate()
216
+
217
+ @kb.add("c-s")
218
+ def submit_all(event: KeyPressEvent) -> None:
219
+ """Ctrl+S submits all answers immediately from any question."""
220
+ state.reset_activity_timer()
221
+ # If entering other text, save it first before submitting
222
+ if state.entering_other_text:
223
+ state.commit_other_text()
224
+ result.confirmed = True
225
+ event.app.exit()
226
+
227
+ @kb.add("escape")
228
+ def cancel(event: KeyPressEvent) -> None:
229
+ state.reset_activity_timer()
230
+ if state.entering_other_text:
231
+ state.entering_other_text = False
232
+ state.other_text_buffer = ""
233
+ event.app.invalidate()
234
+ return
235
+ result.cancelled = True
236
+ event.app.exit()
237
+
238
+ @kb.add("c-c")
239
+ def ctrl_c_cancel(event: KeyPressEvent) -> None:
240
+ result.cancelled = True
241
+ event.app.exit()
242
+
243
+ @kb.add("tab")
244
+ def toggle_peek(event: KeyPressEvent) -> None:
245
+ """Peek behind the TUI to see terminal output.
246
+
247
+ Uses prompt_toolkit's run_in_terminal to properly suspend rendering,
248
+ exit alt screen, wait for a keypress, then restore everything with
249
+ a full repaint. This prevents resize events from clobbering the
250
+ main screen during peek and ensures borders render correctly on return.
251
+ """
252
+ state.reset_activity_timer()
253
+
254
+ def _peek() -> None:
255
+ sys.__stdout__.write(
256
+ "\n \033[2mPress any key to return to questions...\033[0m\n"
257
+ )
258
+ sys.__stdout__.flush()
259
+ _wait_for_keypress()
260
+ state.reset_activity_timer()
261
+
262
+ run_in_terminal(_peek, in_executor=True)
263
+
264
+ @kb.add("<any>")
265
+ def handle_text_input(event: KeyPressEvent) -> None:
266
+ state.reset_activity_timer()
267
+ if state.entering_other_text:
268
+ char = event.data
269
+ if char and len(char) == 1 and ord(char) >= 32:
270
+ state.other_text_buffer += char
271
+ event.app.invalidate()
272
+
273
+ @kb.add("backspace")
274
+ def handle_backspace(event: KeyPressEvent) -> None:
275
+ if state.entering_other_text and state.other_text_buffer:
276
+ state.other_text_buffer = state.other_text_buffer[:-1]
277
+ event.app.invalidate()
278
+
279
+ # --- Panel rendering ---
280
+ # Cache colors once per session to avoid repeated config lookups
281
+ tui_colors = get_tui_colors()
282
+ rich_colors = get_rich_colors()
283
+
284
+ def get_left_panel_text() -> FormattedText:
285
+ """Generate the left panel with question headers."""
286
+ pad = " "
287
+ lines: list[tuple[str, str]] = [
288
+ ("", pad),
289
+ (tui_colors.header_bold, "Questions"),
290
+ ("", "\n\n"),
291
+ ]
292
+
293
+ for i, question in enumerate(state.questions):
294
+ is_current = i == state.current_question_index
295
+ is_answered = state.is_question_answered(i)
296
+ cursor = f"{CURSOR_TRIANGLE} " if is_current else " "
297
+ status = f"{CHECK_MARK} " if is_answered else " "
298
+
299
+ # Determine styles based on state
300
+ cursor_style = (
301
+ tui_colors.cursor_active if is_current else tui_colors.cursor_inactive
302
+ )
303
+ content_style = (
304
+ tui_colors.selected_check
305
+ if is_answered
306
+ else tui_colors.cursor_active
307
+ if is_current
308
+ else tui_colors.text_dim
309
+ )
310
+
311
+ lines.append(("", pad))
312
+ if is_answered:
313
+ # Answered: cursor and status+header use different styles
314
+ lines.append((cursor_style, cursor))
315
+ lines.append((content_style, status + question.header))
316
+ else:
317
+ # Not answered: cursor+status+header all use same style
318
+ lines.append((content_style, cursor + status + question.header))
319
+ lines.append(("", "\n"))
320
+
321
+ # Footer with keyboard shortcuts
322
+ lines.extend(
323
+ [
324
+ ("", "\n"),
325
+ ("", pad),
326
+ (tui_colors.header_dim, f"{ARROW_LEFT}{ARROW_RIGHT} Switch question"),
327
+ ("", "\n"),
328
+ ("", pad),
329
+ (tui_colors.header_dim, f"{ARROW_UP}{ARROW_DOWN} Navigate options"),
330
+ ("", "\n"),
331
+ ("", "\n"),
332
+ ("", pad),
333
+ (tui_colors.help_key, "Ctrl+S"),
334
+ (tui_colors.header_dim, " Submit"),
335
+ ("", "\n"),
336
+ ("", pad),
337
+ (tui_colors.help_key, "Tab"),
338
+ (tui_colors.header_dim, " Peek behind"),
339
+ ]
340
+ )
341
+
342
+ return FormattedText(lines)
343
+
344
+ def get_right_panel_text() -> ANSI:
345
+ """Generate the right panel with current question and options."""
346
+ # Calculate available width: terminal minus left panel, minus frame borders (4 chars)
347
+ term_width = shutil.get_terminal_size().columns
348
+ available = term_width - left_panel_width - 4
349
+ return render_question_panel(
350
+ state, colors=rich_colors, available_width=available
351
+ )
352
+
353
+ # --- Layout ---
354
+ # Calculate dynamic left panel width based on longest header
355
+ left_panel_width = state.get_left_panel_width()
356
+
357
+ left_panel = Window(
358
+ content=FormattedTextControl(lambda: get_left_panel_text()),
359
+ width=Dimension(preferred=left_panel_width, max=left_panel_width),
360
+ )
361
+
362
+ right_panel = Window(
363
+ content=FormattedTextControl(lambda: get_right_panel_text()),
364
+ wrap_lines=True,
365
+ # Right panel takes remaining space
366
+ )
367
+
368
+ root_container = VSplit(
369
+ [
370
+ Frame(left_panel, title=""),
371
+ Frame(right_panel, title=""),
372
+ ]
373
+ )
374
+
375
+ layout = Layout(root_container)
376
+
377
+ # Create output that writes to the real terminal, bypassing any stdout capture
378
+ output = create_output(stdout=sys.__stdout__)
379
+
380
+ app = Application(
381
+ layout=layout,
382
+ key_bindings=kb,
383
+ full_screen=True,
384
+ mouse_support=False,
385
+ color_depth=ColorDepth.DEPTH_24_BIT,
386
+ output=output,
387
+ )
388
+
389
+ # Timeout checker background task
390
+ async def timeout_checker() -> None:
391
+ nonlocal timed_out
392
+ while True:
393
+ await asyncio.sleep(1)
394
+ if state.is_timed_out():
395
+ timed_out = True
396
+ app.exit()
397
+ return
398
+ app.invalidate()
399
+
400
+ timeout_task = asyncio.create_task(timeout_checker())
401
+ app_exception: BaseException | None = None
402
+
403
+ try:
404
+ await app.run_async()
405
+ except BaseException as e:
406
+ app_exception = e
407
+ finally:
408
+ timeout_task.cancel()
409
+ # NOTE: asyncio.gather for already-running task; TaskGroup cannot adopt existing tasks.
410
+ await asyncio.gather(timeout_task, return_exceptions=True)
411
+
412
+ # Re-raise any exception from app.run_async() after cleanup
413
+ if app_exception is not None:
414
+ raise app_exception
415
+
416
+ if timed_out:
417
+ return ([], False, True)
418
+
419
+ if result.cancelled:
420
+ return ([], True, False)
421
+
422
+ return (state.build_answers(), False, False)
@@ -0,0 +1,99 @@
1
+ """Minimal background job registry with bounded log access.
2
+
3
+ Provides safe tracking of detached shell processes started via
4
+ ``run_shell_command(background=True)``. Jobs are stored in a
5
+ process-wide dict keyed by PID. Log tailing is bounded to avoid
6
+ unbounded reads.
7
+ """
8
+
9
+ import contextlib
10
+ import os
11
+ from collections import deque
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+
15
+
16
+ @dataclass
17
+ class BackgroundJob:
18
+ pid: int
19
+ command: str
20
+ cwd: str | None
21
+ log_file: str
22
+ start_time: float = field(default_factory=lambda: os.times()[0])
23
+
24
+
25
+ # In-memory registry of background jobs keyed by PID
26
+ _BACKGROUND_JOBS: dict[int, BackgroundJob] = {}
27
+
28
+ # Maximum bytes to read when tailing a background log
29
+ _MAX_TAIL_BYTES = 32_768
30
+
31
+ # Maximum lines to return from a tail
32
+ _MAX_TAIL_LINES = 256
33
+
34
+
35
+ def register_background_job(
36
+ pid: int, command: str, cwd: str | None, log_file: str
37
+ ) -> None:
38
+ """Register a new background job."""
39
+ _BACKGROUND_JOBS[pid] = BackgroundJob(
40
+ pid=pid,
41
+ command=command,
42
+ cwd=cwd,
43
+ log_file=log_file,
44
+ )
45
+
46
+
47
+ def list_background_jobs() -> list[BackgroundJob]:
48
+ """Return a list of registered background jobs."""
49
+ # Prune jobs whose log files no longer exist (rough heuristic)
50
+ stale: list[int] = []
51
+ for pid, job in list(_BACKGROUND_JOBS.items()):
52
+ try:
53
+ if not Path(job.log_file).exists():
54
+ stale.append(pid)
55
+ except OSError:
56
+ stale.append(pid)
57
+ for pid in stale:
58
+ _BACKGROUND_JOBS.pop(pid, None)
59
+ return list(_BACKGROUND_JOBS.values())
60
+
61
+
62
+ def stop_background_job(pid: int) -> bool:
63
+ """Attempt to terminate a background job by PID.
64
+
65
+ Returns True if the job was known and a signal was sent.
66
+ """
67
+ import signal
68
+
69
+ job = _BACKGROUND_JOBS.pop(pid, None)
70
+ if job is None:
71
+ return False
72
+ with contextlib.suppress(OSError, ProcessLookupError):
73
+ os.kill(pid, signal.SIGTERM)
74
+ return True
75
+
76
+
77
+ def tail_background_job_log(pid: int, max_lines: int = _MAX_TAIL_LINES) -> str:
78
+ """Return the tail of a background job's log file.
79
+
80
+ Reads at most ``_MAX_TAIL_BYTES`` from the end of the log to avoid
81
+ loading massive files into memory.
82
+ """
83
+ job = _BACKGROUND_JOBS.get(pid)
84
+ if job is None:
85
+ return ""
86
+ try:
87
+ path = Path(job.log_file)
88
+ if not path.exists():
89
+ return ""
90
+ size = path.stat().st_size
91
+ read_size = min(_MAX_TAIL_BYTES, size)
92
+ with open(path, "rb") as f:
93
+ f.seek(max(0, size - read_size))
94
+ raw = f.read()
95
+ text = raw.decode("utf-8", errors="replace")
96
+ lines = deque(text.splitlines(), maxlen=max_lines)
97
+ return "\n".join(lines)
98
+ except OSError, ValueError:
99
+ return ""