auto-coder 1.0.0__py3-none-any.whl → 2.0.0__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.

Potentially problematic release.


This version of auto-coder might be problematic. Click here for more details.

Files changed (574) hide show
  1. auto_coder-2.0.0.dist-info/LICENSE +158 -0
  2. auto_coder-2.0.0.dist-info/METADATA +558 -0
  3. auto_coder-2.0.0.dist-info/RECORD +795 -0
  4. {auto_coder-1.0.0.dist-info → auto_coder-2.0.0.dist-info}/WHEEL +1 -1
  5. {auto_coder-1.0.0.dist-info → auto_coder-2.0.0.dist-info}/entry_points.txt +3 -3
  6. autocoder/__init__.py +31 -0
  7. autocoder/agent/auto_filegroup.py +32 -13
  8. autocoder/agent/auto_learn_from_commit.py +9 -1
  9. autocoder/agent/base_agentic/__init__.py +3 -0
  10. autocoder/agent/base_agentic/agent_hub.py +1 -1
  11. autocoder/agent/base_agentic/base_agent.py +235 -136
  12. autocoder/agent/base_agentic/default_tools.py +119 -118
  13. autocoder/agent/base_agentic/test_base_agent.py +1 -1
  14. autocoder/agent/base_agentic/tool_registry.py +32 -20
  15. autocoder/agent/base_agentic/tools/read_file_tool_resolver.py +24 -3
  16. autocoder/agent/base_agentic/tools/write_to_file_tool_resolver.py +24 -11
  17. autocoder/agent/base_agentic/types.py +42 -0
  18. autocoder/agent/entry_command_agent/chat.py +73 -59
  19. autocoder/auto_coder.py +31 -40
  20. autocoder/auto_coder_rag.py +11 -1084
  21. autocoder/auto_coder_runner.py +970 -2345
  22. autocoder/auto_coder_terminal.py +26 -0
  23. autocoder/auto_coder_terminal_v3.py +190 -0
  24. autocoder/chat/conf_command.py +224 -124
  25. autocoder/chat/models_command.py +361 -299
  26. autocoder/chat/rules_command.py +79 -31
  27. autocoder/chat_auto_coder.py +988 -398
  28. autocoder/chat_auto_coder_lang.py +23 -732
  29. autocoder/commands/auto_command.py +25 -8
  30. autocoder/commands/auto_web.py +1 -1
  31. autocoder/commands/tools.py +44 -44
  32. autocoder/common/__init__.py +150 -128
  33. autocoder/common/ac_style_command_parser/__init__.py +39 -2
  34. autocoder/common/ac_style_command_parser/config.py +422 -0
  35. autocoder/common/ac_style_command_parser/parser.py +292 -78
  36. autocoder/common/ac_style_command_parser/test_parser.py +241 -16
  37. autocoder/common/ac_style_command_parser/test_typed_parser.py +342 -0
  38. autocoder/common/ac_style_command_parser/typed_parser.py +653 -0
  39. autocoder/common/action_yml_file_manager.py +25 -13
  40. autocoder/common/agent_events/__init__.py +52 -0
  41. autocoder/common/agent_events/agent_event_emitter.py +193 -0
  42. autocoder/common/agent_events/event_factory.py +177 -0
  43. autocoder/common/agent_events/examples.py +307 -0
  44. autocoder/common/agent_events/types.py +113 -0
  45. autocoder/common/agent_events/utils.py +68 -0
  46. autocoder/common/agent_hooks/__init__.py +44 -0
  47. autocoder/common/agent_hooks/examples.py +582 -0
  48. autocoder/common/agent_hooks/hook_executor.py +217 -0
  49. autocoder/common/agent_hooks/hook_manager.py +288 -0
  50. autocoder/common/agent_hooks/types.py +133 -0
  51. autocoder/common/agent_hooks/utils.py +99 -0
  52. autocoder/common/agent_query_queue/queue_executor.py +324 -0
  53. autocoder/common/agent_query_queue/queue_manager.py +325 -0
  54. autocoder/common/agents/__init__.py +11 -0
  55. autocoder/common/agents/agent_manager.py +323 -0
  56. autocoder/common/agents/agent_parser.py +189 -0
  57. autocoder/common/agents/example_usage.py +344 -0
  58. autocoder/common/agents/integration_example.py +330 -0
  59. autocoder/common/agents/test_agent_parser.py +545 -0
  60. autocoder/common/async_utils.py +101 -0
  61. autocoder/common/auto_coder_lang.py +23 -972
  62. autocoder/common/autocoderargs_parser/__init__.py +14 -0
  63. autocoder/common/autocoderargs_parser/parser.py +184 -0
  64. autocoder/common/autocoderargs_parser/tests/__init__.py +1 -0
  65. autocoder/common/autocoderargs_parser/tests/test_args_parser.py +235 -0
  66. autocoder/common/autocoderargs_parser/tests/test_token_parser.py +195 -0
  67. autocoder/common/autocoderargs_parser/token_parser.py +290 -0
  68. autocoder/common/buildin_tokenizer.py +2 -4
  69. autocoder/common/code_auto_generate.py +149 -74
  70. autocoder/common/code_auto_generate_diff.py +163 -70
  71. autocoder/common/code_auto_generate_editblock.py +179 -89
  72. autocoder/common/code_auto_generate_strict_diff.py +167 -72
  73. autocoder/common/code_auto_merge_editblock.py +13 -6
  74. autocoder/common/code_modification_ranker.py +1 -1
  75. autocoder/common/command_completer.py +3 -3
  76. autocoder/common/command_file_manager/manager.py +183 -47
  77. autocoder/common/command_file_manager/test_command_file_manager.py +507 -0
  78. autocoder/common/command_templates.py +1 -1
  79. autocoder/common/conf_utils.py +2 -4
  80. autocoder/common/conversations/config.py +11 -3
  81. autocoder/common/conversations/get_conversation_manager.py +100 -2
  82. autocoder/common/conversations/llm_stats_models.py +264 -0
  83. autocoder/common/conversations/manager.py +112 -28
  84. autocoder/common/conversations/models.py +16 -2
  85. autocoder/common/conversations/storage/index_manager.py +134 -10
  86. autocoder/common/core_config/__init__.py +63 -0
  87. autocoder/common/core_config/agentic_mode_manager.py +109 -0
  88. autocoder/common/core_config/base_manager.py +123 -0
  89. autocoder/common/core_config/compatibility.py +151 -0
  90. autocoder/common/core_config/config_manager.py +156 -0
  91. autocoder/common/core_config/conversation_manager.py +31 -0
  92. autocoder/common/core_config/exclude_manager.py +72 -0
  93. autocoder/common/core_config/file_manager.py +177 -0
  94. autocoder/common/core_config/human_as_model_manager.py +129 -0
  95. autocoder/common/core_config/lib_manager.py +54 -0
  96. autocoder/common/core_config/main_manager.py +81 -0
  97. autocoder/common/core_config/mode_manager.py +126 -0
  98. autocoder/common/core_config/models.py +70 -0
  99. autocoder/common/core_config/test_memory_manager.py +1056 -0
  100. autocoder/common/env_manager.py +282 -0
  101. autocoder/common/env_manager_usage_example.py +211 -0
  102. autocoder/common/file_checkpoint/conversation_checkpoint.py +19 -19
  103. autocoder/common/file_checkpoint/manager.py +264 -48
  104. autocoder/common/file_checkpoint/test_backup.py +1 -18
  105. autocoder/common/file_checkpoint/test_manager.py +270 -1
  106. autocoder/common/file_checkpoint/test_store.py +1 -17
  107. autocoder/common/file_handler/__init__.py +23 -0
  108. autocoder/common/file_handler/active_context_handler.py +159 -0
  109. autocoder/common/file_handler/add_files_handler.py +409 -0
  110. autocoder/common/file_handler/chat_handler.py +180 -0
  111. autocoder/common/file_handler/coding_handler.py +401 -0
  112. autocoder/common/file_handler/commit_handler.py +200 -0
  113. autocoder/common/file_handler/lib_handler.py +156 -0
  114. autocoder/common/file_handler/list_files_handler.py +111 -0
  115. autocoder/common/file_handler/mcp_handler.py +268 -0
  116. autocoder/common/file_handler/models_handler.py +493 -0
  117. autocoder/common/file_handler/remove_files_handler.py +172 -0
  118. autocoder/common/git_utils.py +44 -8
  119. autocoder/common/global_cancel.py +15 -6
  120. autocoder/common/ignorefiles/test_ignore_file_utils.py +1 -1
  121. autocoder/common/international/__init__.py +31 -0
  122. autocoder/common/international/demo_international.py +92 -0
  123. autocoder/common/international/message_manager.py +157 -0
  124. autocoder/common/international/messages/__init__.py +56 -0
  125. autocoder/common/international/messages/async_command_messages.py +507 -0
  126. autocoder/common/international/messages/auto_coder_messages.py +2208 -0
  127. autocoder/common/international/messages/chat_auto_coder_messages.py +1547 -0
  128. autocoder/common/international/messages/command_help_messages.py +986 -0
  129. autocoder/common/international/messages/conversation_command_messages.py +191 -0
  130. autocoder/common/international/messages/git_helper_plugin_messages.py +159 -0
  131. autocoder/common/international/messages/queue_command_messages.py +751 -0
  132. autocoder/common/international/messages/rules_command_messages.py +77 -0
  133. autocoder/common/international/messages/sdk_messages.py +1707 -0
  134. autocoder/common/international/messages/token_helper_plugin_messages.py +361 -0
  135. autocoder/common/international/messages/tool_display_messages.py +1212 -0
  136. autocoder/common/international/messages/workflow_exception_messages.py +473 -0
  137. autocoder/common/international/test_international.py +612 -0
  138. autocoder/common/linter_core/__init__.py +28 -0
  139. autocoder/common/linter_core/base_linter.py +61 -0
  140. autocoder/common/linter_core/config_loader.py +271 -0
  141. autocoder/common/linter_core/formatters/__init__.py +0 -0
  142. autocoder/common/linter_core/formatters/base_formatter.py +38 -0
  143. autocoder/common/linter_core/formatters/raw_formatter.py +17 -0
  144. autocoder/common/linter_core/linter.py +166 -0
  145. autocoder/common/linter_core/linter_factory.py +216 -0
  146. autocoder/common/linter_core/linter_manager.py +333 -0
  147. autocoder/common/linter_core/linters/__init__.py +9 -0
  148. autocoder/common/linter_core/linters/java_linter.py +342 -0
  149. autocoder/common/linter_core/linters/python_linter.py +115 -0
  150. autocoder/common/linter_core/linters/typescript_linter.py +119 -0
  151. autocoder/common/linter_core/models/__init__.py +7 -0
  152. autocoder/common/linter_core/models/lint_result.py +91 -0
  153. autocoder/common/linter_core/models.py +33 -0
  154. autocoder/common/linter_core/tests/__init__.py +3 -0
  155. autocoder/common/linter_core/tests/test_config_loader.py +323 -0
  156. autocoder/common/linter_core/tests/test_config_loading.py +308 -0
  157. autocoder/common/linter_core/tests/test_factory_manager.py +234 -0
  158. autocoder/common/linter_core/tests/test_formatters.py +147 -0
  159. autocoder/common/linter_core/tests/test_integration.py +317 -0
  160. autocoder/common/linter_core/tests/test_java_linter.py +496 -0
  161. autocoder/common/linter_core/tests/test_linters.py +265 -0
  162. autocoder/common/linter_core/tests/test_models.py +81 -0
  163. autocoder/common/linter_core/tests/verify_config_loading.py +296 -0
  164. autocoder/common/linter_core/tests/verify_fixes.py +183 -0
  165. autocoder/common/llm_friendly_package/__init__.py +31 -0
  166. autocoder/common/llm_friendly_package/base_manager.py +102 -0
  167. autocoder/common/llm_friendly_package/docs_manager.py +121 -0
  168. autocoder/common/llm_friendly_package/library_manager.py +171 -0
  169. autocoder/common/{llm_friendly_package.py → llm_friendly_package/main_manager.py} +204 -231
  170. autocoder/common/llm_friendly_package/models.py +40 -0
  171. autocoder/common/llm_friendly_package/test_llm_friendly_package.py +536 -0
  172. autocoder/common/llms/__init__.py +15 -0
  173. autocoder/common/llms/demo_error_handling.py +85 -0
  174. autocoder/common/llms/factory.py +142 -0
  175. autocoder/common/llms/manager.py +264 -0
  176. autocoder/common/llms/pricing.py +121 -0
  177. autocoder/common/llms/registry.py +288 -0
  178. autocoder/common/llms/schema.py +77 -0
  179. autocoder/common/llms/simple_demo.py +45 -0
  180. autocoder/common/llms/test_quick_model.py +116 -0
  181. autocoder/common/llms/test_remove_functionality.py +182 -0
  182. autocoder/common/llms/tests/__init__.py +1 -0
  183. autocoder/common/llms/tests/test_manager.py +330 -0
  184. autocoder/common/llms/tests/test_registry.py +364 -0
  185. autocoder/common/mcp_tools/__init__.py +62 -0
  186. autocoder/common/{mcp_tools.py → mcp_tools/executor.py} +49 -40
  187. autocoder/common/{mcp_hub.py → mcp_tools/hub.py} +42 -68
  188. autocoder/common/{mcp_server_install.py → mcp_tools/installer.py} +16 -28
  189. autocoder/common/{mcp_server.py → mcp_tools/server.py} +176 -48
  190. autocoder/common/mcp_tools/test_keyboard_interrupt.py +93 -0
  191. autocoder/common/mcp_tools/test_mcp_tools.py +391 -0
  192. autocoder/common/{mcp_server_types.py → mcp_tools/types.py} +121 -48
  193. autocoder/common/mcp_tools/verify_functionality.py +202 -0
  194. autocoder/common/model_speed_tester.py +32 -26
  195. autocoder/common/priority_directory_finder/__init__.py +142 -0
  196. autocoder/common/priority_directory_finder/examples.py +230 -0
  197. autocoder/common/priority_directory_finder/finder.py +283 -0
  198. autocoder/common/priority_directory_finder/models.py +236 -0
  199. autocoder/common/priority_directory_finder/test_priority_directory_finder.py +431 -0
  200. autocoder/common/project_scanner/__init__.py +18 -0
  201. autocoder/common/project_scanner/compat.py +77 -0
  202. autocoder/common/project_scanner/scanner.py +436 -0
  203. autocoder/common/project_tracker/__init__.py +27 -0
  204. autocoder/common/project_tracker/api.py +228 -0
  205. autocoder/common/project_tracker/demo.py +272 -0
  206. autocoder/common/project_tracker/tracker.py +487 -0
  207. autocoder/common/project_tracker/types.py +53 -0
  208. autocoder/common/pruner/__init__.py +67 -0
  209. autocoder/common/pruner/agentic_conversation_pruner.py +651 -102
  210. autocoder/common/pruner/conversation_message_ids_api.py +386 -0
  211. autocoder/common/pruner/conversation_message_ids_manager.py +347 -0
  212. autocoder/common/pruner/conversation_message_ids_pruner.py +473 -0
  213. autocoder/common/pruner/conversation_normalizer.py +347 -0
  214. autocoder/common/pruner/conversation_pruner.py +26 -6
  215. autocoder/common/pruner/test_agentic_conversation_pruner.py +554 -112
  216. autocoder/common/pruner/test_conversation_normalizer.py +502 -0
  217. autocoder/common/pruner/test_tool_content_detector.py +324 -0
  218. autocoder/common/pruner/tool_content_detector.py +227 -0
  219. autocoder/common/pruner/tools/__init__.py +18 -0
  220. autocoder/common/pruner/tools/query_message_ids.py +264 -0
  221. autocoder/common/pruner/tools/test_agentic_pruning_logic.py +432 -0
  222. autocoder/common/pruner/tools/test_message_ids_pruning_only.py +192 -0
  223. autocoder/common/pull_requests/__init__.py +9 -1
  224. autocoder/common/pull_requests/utils.py +122 -1
  225. autocoder/common/rag_manager/rag_manager.py +36 -40
  226. autocoder/common/rulefiles/__init__.py +53 -1
  227. autocoder/common/rulefiles/api.py +250 -0
  228. autocoder/common/rulefiles/core/__init__.py +14 -0
  229. autocoder/common/rulefiles/core/manager.py +241 -0
  230. autocoder/common/rulefiles/core/selector.py +805 -0
  231. autocoder/common/rulefiles/models/__init__.py +20 -0
  232. autocoder/common/rulefiles/models/index.py +16 -0
  233. autocoder/common/rulefiles/models/init_rule.py +18 -0
  234. autocoder/common/rulefiles/models/rule_file.py +18 -0
  235. autocoder/common/rulefiles/models/rule_relevance.py +14 -0
  236. autocoder/common/rulefiles/models/summary.py +16 -0
  237. autocoder/common/rulefiles/test_rulefiles.py +776 -0
  238. autocoder/common/rulefiles/utils/__init__.py +34 -0
  239. autocoder/common/rulefiles/utils/monitor.py +86 -0
  240. autocoder/common/rulefiles/utils/parser.py +230 -0
  241. autocoder/common/save_formatted_log.py +67 -10
  242. autocoder/common/search_replace.py +8 -1
  243. autocoder/common/search_replace_patch/__init__.py +24 -0
  244. autocoder/common/search_replace_patch/base.py +115 -0
  245. autocoder/common/search_replace_patch/manager.py +248 -0
  246. autocoder/common/search_replace_patch/patch_replacer.py +304 -0
  247. autocoder/common/search_replace_patch/similarity_replacer.py +306 -0
  248. autocoder/common/search_replace_patch/string_replacer.py +181 -0
  249. autocoder/common/search_replace_patch/tests/__init__.py +3 -0
  250. autocoder/common/search_replace_patch/tests/run_tests.py +126 -0
  251. autocoder/common/search_replace_patch/tests/test_base.py +188 -0
  252. autocoder/common/search_replace_patch/tests/test_empty_line_insert.py +233 -0
  253. autocoder/common/search_replace_patch/tests/test_integration.py +389 -0
  254. autocoder/common/search_replace_patch/tests/test_manager.py +351 -0
  255. autocoder/common/search_replace_patch/tests/test_patch_replacer.py +316 -0
  256. autocoder/common/search_replace_patch/tests/test_regex_replacer.py +306 -0
  257. autocoder/common/search_replace_patch/tests/test_similarity_replacer.py +384 -0
  258. autocoder/common/shell_commands/__init__.py +197 -0
  259. autocoder/common/shell_commands/background_process_notifier.py +346 -0
  260. autocoder/common/shell_commands/command_executor.py +1127 -0
  261. autocoder/common/shell_commands/error_recovery.py +541 -0
  262. autocoder/common/shell_commands/exceptions.py +120 -0
  263. autocoder/common/shell_commands/interactive_executor.py +476 -0
  264. autocoder/common/shell_commands/interactive_pexpect_process.py +623 -0
  265. autocoder/common/shell_commands/interactive_process.py +744 -0
  266. autocoder/common/shell_commands/interactive_session_manager.py +1014 -0
  267. autocoder/common/shell_commands/monitoring.py +529 -0
  268. autocoder/common/shell_commands/process_cleanup.py +386 -0
  269. autocoder/common/shell_commands/process_manager.py +606 -0
  270. autocoder/common/shell_commands/test_interactive_pexpect_process.py +281 -0
  271. autocoder/common/shell_commands/tests/__init__.py +6 -0
  272. autocoder/common/shell_commands/tests/conftest.py +118 -0
  273. autocoder/common/shell_commands/tests/test_background_process_notifier.py +703 -0
  274. autocoder/common/shell_commands/tests/test_command_executor.py +448 -0
  275. autocoder/common/shell_commands/tests/test_error_recovery.py +305 -0
  276. autocoder/common/shell_commands/tests/test_exceptions.py +299 -0
  277. autocoder/common/shell_commands/tests/test_execute_batch.py +588 -0
  278. autocoder/common/shell_commands/tests/test_indented_batch_commands.py +244 -0
  279. autocoder/common/shell_commands/tests/test_integration.py +664 -0
  280. autocoder/common/shell_commands/tests/test_monitoring.py +546 -0
  281. autocoder/common/shell_commands/tests/test_performance.py +632 -0
  282. autocoder/common/shell_commands/tests/test_process_cleanup.py +397 -0
  283. autocoder/common/shell_commands/tests/test_process_manager.py +606 -0
  284. autocoder/common/shell_commands/tests/test_timeout_config.py +343 -0
  285. autocoder/common/shell_commands/tests/test_timeout_manager.py +520 -0
  286. autocoder/common/shell_commands/timeout_config.py +315 -0
  287. autocoder/common/shell_commands/timeout_manager.py +352 -0
  288. autocoder/common/terminal_paste/__init__.py +14 -0
  289. autocoder/common/terminal_paste/demo.py +145 -0
  290. autocoder/common/terminal_paste/demo_paste_functionality.py +95 -0
  291. autocoder/common/terminal_paste/paste_handler.py +200 -0
  292. autocoder/common/terminal_paste/paste_manager.py +118 -0
  293. autocoder/common/terminal_paste/tests/__init__.py +1 -0
  294. autocoder/common/terminal_paste/tests/test_paste_handler.py +182 -0
  295. autocoder/common/terminal_paste/tests/test_paste_manager.py +126 -0
  296. autocoder/common/terminal_paste/utils.py +163 -0
  297. autocoder/common/test_autocoder_args.py +232 -0
  298. autocoder/common/test_env_manager.py +173 -0
  299. autocoder/common/test_env_manager_integration.py +159 -0
  300. autocoder/common/text_similarity/__init__.py +9 -0
  301. autocoder/common/text_similarity/demo.py +216 -0
  302. autocoder/common/text_similarity/examples.py +266 -0
  303. autocoder/common/text_similarity/test_text_similarity.py +306 -0
  304. autocoder/common/text_similarity/text_similarity.py +194 -0
  305. autocoder/common/text_similarity/utils.py +125 -0
  306. autocoder/common/todos/__init__.py +61 -0
  307. autocoder/common/todos/cache/__init__.py +16 -0
  308. autocoder/common/todos/cache/base_cache.py +89 -0
  309. autocoder/common/todos/cache/cache_manager.py +228 -0
  310. autocoder/common/todos/cache/memory_cache.py +225 -0
  311. autocoder/common/todos/config.py +155 -0
  312. autocoder/common/todos/exceptions.py +35 -0
  313. autocoder/common/todos/get_todo_manager.py +161 -0
  314. autocoder/common/todos/manager.py +537 -0
  315. autocoder/common/todos/models.py +239 -0
  316. autocoder/common/todos/storage/__init__.py +14 -0
  317. autocoder/common/todos/storage/base_storage.py +76 -0
  318. autocoder/common/todos/storage/file_storage.py +278 -0
  319. autocoder/common/tokens/counter.py +24 -2
  320. autocoder/common/tools_manager/__init__.py +17 -0
  321. autocoder/common/tools_manager/examples.py +162 -0
  322. autocoder/common/tools_manager/manager.py +385 -0
  323. autocoder/common/tools_manager/models.py +39 -0
  324. autocoder/common/tools_manager/test_tools_manager.py +303 -0
  325. autocoder/common/tools_manager/utils.py +191 -0
  326. autocoder/common/v2/agent/agentic_callbacks.py +270 -0
  327. autocoder/common/v2/agent/agentic_edit.py +2699 -1856
  328. autocoder/common/v2/agent/agentic_edit_change_manager.py +474 -0
  329. autocoder/common/v2/agent/agentic_edit_tools/__init__.py +35 -1
  330. autocoder/common/v2/agent/agentic_edit_tools/ac_mod_list_tool_resolver.py +279 -0
  331. autocoder/common/v2/agent/agentic_edit_tools/ac_mod_write_tool_resolver.py +10 -1
  332. autocoder/common/v2/agent/agentic_edit_tools/background_task_tool_resolver.py +1167 -0
  333. autocoder/common/v2/agent/agentic_edit_tools/base_tool_resolver.py +2 -2
  334. autocoder/common/v2/agent/agentic_edit_tools/conversation_message_ids_read_tool_resolver.py +214 -0
  335. autocoder/common/v2/agent/agentic_edit_tools/conversation_message_ids_write_tool_resolver.py +299 -0
  336. autocoder/common/v2/agent/agentic_edit_tools/count_tokens_tool_resolver.py +290 -0
  337. autocoder/common/v2/agent/agentic_edit_tools/execute_command_tool_resolver.py +564 -29
  338. autocoder/common/v2/agent/agentic_edit_tools/execute_workflow_tool_resolver.py +485 -0
  339. autocoder/common/v2/agent/agentic_edit_tools/extract_to_text_tool_resolver.py +225 -0
  340. autocoder/common/v2/agent/agentic_edit_tools/lint_report.py +79 -0
  341. autocoder/common/v2/agent/agentic_edit_tools/linter_config_models.py +343 -0
  342. autocoder/common/v2/agent/agentic_edit_tools/linter_enabled_tool_resolver.py +189 -0
  343. autocoder/common/v2/agent/agentic_edit_tools/list_files_tool_resolver.py +169 -101
  344. autocoder/common/v2/agent/agentic_edit_tools/load_extra_document_tool_resolver.py +349 -0
  345. autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +243 -50
  346. autocoder/common/v2/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +667 -147
  347. autocoder/common/v2/agent/agentic_edit_tools/run_named_subagents_tool_resolver.py +691 -0
  348. autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +410 -86
  349. autocoder/common/v2/agent/agentic_edit_tools/session_interactive_tool_resolver.py +115 -0
  350. autocoder/common/v2/agent/agentic_edit_tools/session_start_tool_resolver.py +190 -0
  351. autocoder/common/v2/agent/agentic_edit_tools/session_stop_tool_resolver.py +76 -0
  352. autocoder/common/v2/agent/agentic_edit_tools/test_write_to_file_tool_resolver.py +207 -192
  353. autocoder/common/v2/agent/agentic_edit_tools/todo_read_tool_resolver.py +80 -63
  354. autocoder/common/v2/agent/agentic_edit_tools/todo_write_tool_resolver.py +237 -233
  355. autocoder/common/v2/agent/agentic_edit_tools/use_mcp_tool_resolver.py +2 -2
  356. autocoder/common/v2/agent/agentic_edit_tools/web_crawl_tool_resolver.py +557 -0
  357. autocoder/common/v2/agent/agentic_edit_tools/web_search_tool_resolver.py +600 -0
  358. autocoder/common/v2/agent/agentic_edit_tools/write_to_file_tool_resolver.py +56 -121
  359. autocoder/common/v2/agent/agentic_edit_types.py +343 -9
  360. autocoder/common/v2/agent/runner/__init__.py +3 -3
  361. autocoder/common/v2/agent/runner/base_runner.py +12 -26
  362. autocoder/common/v2/agent/runner/{event_runner.py → file_based_event_runner.py} +3 -2
  363. autocoder/common/v2/agent/runner/sdk_runner.py +150 -8
  364. autocoder/common/v2/agent/runner/terminal_runner.py +170 -57
  365. autocoder/common/v2/agent/runner/tool_display.py +557 -159
  366. autocoder/common/v2/agent/test_agentic_callbacks.py +265 -0
  367. autocoder/common/v2/agent/test_agentic_edit.py +194 -0
  368. autocoder/common/v2/agent/tool_caller/__init__.py +24 -0
  369. autocoder/common/v2/agent/tool_caller/default_tool_resolver_map.py +135 -0
  370. autocoder/common/v2/agent/tool_caller/integration_test.py +172 -0
  371. autocoder/common/v2/agent/tool_caller/plugins/__init__.py +14 -0
  372. autocoder/common/v2/agent/tool_caller/plugins/base_plugin.py +126 -0
  373. autocoder/common/v2/agent/tool_caller/plugins/examples/__init__.py +13 -0
  374. autocoder/common/v2/agent/tool_caller/plugins/examples/logging_plugin.py +164 -0
  375. autocoder/common/v2/agent/tool_caller/plugins/examples/security_filter_plugin.py +198 -0
  376. autocoder/common/v2/agent/tool_caller/plugins/plugin_interface.py +141 -0
  377. autocoder/common/v2/agent/tool_caller/test_tool_caller.py +278 -0
  378. autocoder/common/v2/agent/tool_caller/tool_call_plugin_manager.py +331 -0
  379. autocoder/common/v2/agent/tool_caller/tool_caller.py +337 -0
  380. autocoder/common/v2/agent/tool_caller/usage_example.py +193 -0
  381. autocoder/common/v2/code_agentic_editblock_manager.py +4 -4
  382. autocoder/common/v2/code_auto_generate.py +136 -78
  383. autocoder/common/v2/code_auto_generate_diff.py +135 -79
  384. autocoder/common/v2/code_auto_generate_editblock.py +174 -99
  385. autocoder/common/v2/code_auto_generate_strict_diff.py +151 -71
  386. autocoder/common/v2/code_auto_merge.py +1 -1
  387. autocoder/common/v2/code_auto_merge_editblock.py +13 -1
  388. autocoder/common/v2/code_diff_manager.py +3 -3
  389. autocoder/common/v2/code_editblock_manager.py +4 -14
  390. autocoder/common/v2/code_manager.py +1 -1
  391. autocoder/common/v2/code_strict_diff_manager.py +2 -2
  392. autocoder/common/wrap_llm_hint/__init__.py +10 -0
  393. autocoder/common/wrap_llm_hint/test_wrap_llm_hint.py +1067 -0
  394. autocoder/common/wrap_llm_hint/utils.py +432 -0
  395. autocoder/common/wrap_llm_hint/wrap_llm_hint.py +323 -0
  396. autocoder/completer/__init__.py +8 -0
  397. autocoder/completer/command_completer_v2.py +1051 -0
  398. autocoder/default_project/__init__.py +501 -0
  399. autocoder/dispacher/__init__.py +4 -12
  400. autocoder/dispacher/actions/action.py +165 -7
  401. autocoder/dispacher/actions/plugins/action_regex_project.py +2 -2
  402. autocoder/index/entry.py +116 -124
  403. autocoder/{agent → index/filter}/agentic_filter.py +322 -333
  404. autocoder/index/filter/normal_filter.py +5 -11
  405. autocoder/index/filter/quick_filter.py +1 -1
  406. autocoder/index/index.py +36 -9
  407. autocoder/index/tests/__init__.py +1 -0
  408. autocoder/index/tests/run_tests.py +195 -0
  409. autocoder/index/tests/test_entry.py +303 -0
  410. autocoder/index/tests/test_index_manager.py +314 -0
  411. autocoder/index/tests/test_module_integration.py +300 -0
  412. autocoder/index/tests/test_symbols_utils.py +183 -0
  413. autocoder/inner/__init__.py +4 -0
  414. autocoder/inner/agentic.py +932 -0
  415. autocoder/inner/async_command_handler.py +992 -0
  416. autocoder/inner/conversation_command_handlers.py +623 -0
  417. autocoder/inner/merge_command_handler.py +213 -0
  418. autocoder/inner/queue_command_handler.py +684 -0
  419. autocoder/models.py +95 -266
  420. autocoder/plugins/git_helper_plugin.py +31 -29
  421. autocoder/plugins/token_helper_plugin.py +65 -46
  422. autocoder/pyproject/__init__.py +32 -29
  423. autocoder/rag/agentic_rag.py +215 -75
  424. autocoder/rag/cache/simple_cache.py +1 -2
  425. autocoder/rag/loaders/image_loader.py +1 -1
  426. autocoder/rag/long_context_rag.py +42 -26
  427. autocoder/rag/qa_conversation_strategy.py +1 -1
  428. autocoder/rag/terminal/__init__.py +17 -0
  429. autocoder/rag/terminal/args.py +581 -0
  430. autocoder/rag/terminal/bootstrap.py +61 -0
  431. autocoder/rag/terminal/command_handlers.py +653 -0
  432. autocoder/rag/terminal/formatters/__init__.py +20 -0
  433. autocoder/rag/terminal/formatters/base.py +70 -0
  434. autocoder/rag/terminal/formatters/json_format.py +66 -0
  435. autocoder/rag/terminal/formatters/stream_json.py +95 -0
  436. autocoder/rag/terminal/formatters/text.py +28 -0
  437. autocoder/rag/terminal/init.py +120 -0
  438. autocoder/rag/terminal/utils.py +106 -0
  439. autocoder/rag/test_agentic_rag.py +389 -0
  440. autocoder/rag/test_doc_filter.py +3 -3
  441. autocoder/rag/test_long_context_rag.py +1 -1
  442. autocoder/rag/test_token_limiter.py +517 -10
  443. autocoder/rag/token_counter.py +3 -0
  444. autocoder/rag/token_limiter.py +19 -15
  445. autocoder/rag/tools/__init__.py +26 -2
  446. autocoder/rag/tools/bochaai_example.py +343 -0
  447. autocoder/rag/tools/bochaai_sdk.py +541 -0
  448. autocoder/rag/tools/metaso_example.py +268 -0
  449. autocoder/rag/tools/metaso_sdk.py +417 -0
  450. autocoder/rag/tools/recall_tool.py +28 -7
  451. autocoder/rag/tools/run_integration_tests.py +204 -0
  452. autocoder/rag/tools/test_all_providers.py +318 -0
  453. autocoder/rag/tools/test_bochaai_integration.py +482 -0
  454. autocoder/rag/tools/test_final_integration.py +215 -0
  455. autocoder/rag/tools/test_metaso_integration.py +424 -0
  456. autocoder/rag/tools/test_metaso_real.py +171 -0
  457. autocoder/rag/tools/test_web_crawl_tool.py +639 -0
  458. autocoder/rag/tools/test_web_search_tool.py +509 -0
  459. autocoder/rag/tools/todo_read_tool.py +202 -0
  460. autocoder/rag/tools/todo_write_tool.py +412 -0
  461. autocoder/rag/tools/web_crawl_tool.py +634 -0
  462. autocoder/rag/tools/web_search_tool.py +558 -0
  463. autocoder/rag/tools/web_tools_example.py +119 -0
  464. autocoder/rag/types.py +16 -0
  465. autocoder/rag/variable_holder.py +4 -2
  466. autocoder/rags.py +86 -79
  467. autocoder/regexproject/__init__.py +23 -21
  468. autocoder/sdk/__init__.py +46 -190
  469. autocoder/sdk/api.py +370 -0
  470. autocoder/sdk/async_runner/__init__.py +26 -0
  471. autocoder/sdk/async_runner/async_executor.py +650 -0
  472. autocoder/sdk/async_runner/async_handler.py +356 -0
  473. autocoder/sdk/async_runner/markdown_processor.py +595 -0
  474. autocoder/sdk/async_runner/task_metadata.py +284 -0
  475. autocoder/sdk/async_runner/worktree_manager.py +438 -0
  476. autocoder/sdk/cli/__init__.py +2 -5
  477. autocoder/sdk/cli/formatters.py +28 -204
  478. autocoder/sdk/cli/handlers.py +77 -44
  479. autocoder/sdk/cli/main.py +154 -171
  480. autocoder/sdk/cli/options.py +95 -22
  481. autocoder/sdk/constants.py +139 -51
  482. autocoder/sdk/core/auto_coder_core.py +484 -109
  483. autocoder/sdk/core/bridge.py +297 -115
  484. autocoder/sdk/exceptions.py +18 -12
  485. autocoder/sdk/formatters/__init__.py +19 -0
  486. autocoder/sdk/formatters/input.py +64 -0
  487. autocoder/sdk/formatters/output.py +247 -0
  488. autocoder/sdk/formatters/stream.py +54 -0
  489. autocoder/sdk/models/__init__.py +6 -5
  490. autocoder/sdk/models/options.py +55 -18
  491. autocoder/sdk/utils/formatters.py +27 -195
  492. autocoder/suffixproject/__init__.py +28 -25
  493. autocoder/terminal/__init__.py +14 -0
  494. autocoder/terminal/app.py +454 -0
  495. autocoder/terminal/args.py +32 -0
  496. autocoder/terminal/bootstrap.py +178 -0
  497. autocoder/terminal/command_processor.py +521 -0
  498. autocoder/terminal/command_registry.py +57 -0
  499. autocoder/terminal/help.py +97 -0
  500. autocoder/terminal/tasks/__init__.py +5 -0
  501. autocoder/terminal/tasks/background.py +77 -0
  502. autocoder/terminal/tasks/task_event.py +70 -0
  503. autocoder/terminal/ui/__init__.py +13 -0
  504. autocoder/terminal/ui/completer.py +268 -0
  505. autocoder/terminal/ui/keybindings.py +75 -0
  506. autocoder/terminal/ui/session.py +41 -0
  507. autocoder/terminal/ui/toolbar.py +64 -0
  508. autocoder/terminal/utils/__init__.py +13 -0
  509. autocoder/terminal/utils/errors.py +18 -0
  510. autocoder/terminal/utils/paths.py +19 -0
  511. autocoder/terminal/utils/shell.py +43 -0
  512. autocoder/terminal_v3/__init__.py +10 -0
  513. autocoder/terminal_v3/app.py +201 -0
  514. autocoder/terminal_v3/handlers/__init__.py +5 -0
  515. autocoder/terminal_v3/handlers/command_handler.py +131 -0
  516. autocoder/terminal_v3/models/__init__.py +6 -0
  517. autocoder/terminal_v3/models/conversation_buffer.py +214 -0
  518. autocoder/terminal_v3/models/message.py +50 -0
  519. autocoder/terminal_v3/models/tool_display.py +247 -0
  520. autocoder/terminal_v3/ui/__init__.py +7 -0
  521. autocoder/terminal_v3/ui/keybindings.py +56 -0
  522. autocoder/terminal_v3/ui/layout.py +141 -0
  523. autocoder/terminal_v3/ui/styles.py +43 -0
  524. autocoder/tsproject/__init__.py +23 -23
  525. autocoder/utils/auto_coder_utils/chat_stream_out.py +1 -1
  526. autocoder/utils/llms.py +88 -80
  527. autocoder/utils/math_utils.py +101 -0
  528. autocoder/utils/model_provider_selector.py +16 -4
  529. autocoder/utils/operate_config_api.py +33 -5
  530. autocoder/utils/thread_utils.py +2 -2
  531. autocoder/version.py +4 -2
  532. autocoder/workflow_agents/__init__.py +84 -0
  533. autocoder/workflow_agents/agent.py +143 -0
  534. autocoder/workflow_agents/exceptions.py +573 -0
  535. autocoder/workflow_agents/executor.py +489 -0
  536. autocoder/workflow_agents/loader.py +737 -0
  537. autocoder/workflow_agents/runner.py +267 -0
  538. autocoder/workflow_agents/types.py +172 -0
  539. autocoder/workflow_agents/utils.py +434 -0
  540. autocoder/workflow_agents/workflow_manager.py +211 -0
  541. auto_coder-1.0.0.dist-info/METADATA +0 -396
  542. auto_coder-1.0.0.dist-info/RECORD +0 -442
  543. auto_coder-1.0.0.dist-info/licenses/LICENSE +0 -201
  544. autocoder/auto_coder_server.py +0 -672
  545. autocoder/benchmark.py +0 -138
  546. autocoder/common/ac_style_command_parser/example.py +0 -7
  547. autocoder/common/cleaner.py +0 -31
  548. autocoder/common/command_completer_v2.py +0 -615
  549. autocoder/common/context_pruner.py +0 -477
  550. autocoder/common/conversation_pruner.py +0 -132
  551. autocoder/common/directory_cache/__init__.py +0 -1
  552. autocoder/common/directory_cache/cache.py +0 -192
  553. autocoder/common/directory_cache/test_cache.py +0 -190
  554. autocoder/common/file_checkpoint/examples.py +0 -217
  555. autocoder/common/llm_friendly_package_example.py +0 -138
  556. autocoder/common/llm_friendly_package_test.py +0 -63
  557. autocoder/common/pull_requests/test_module.py +0 -1
  558. autocoder/common/rulefiles/autocoderrules_utils.py +0 -484
  559. autocoder/common/text.py +0 -30
  560. autocoder/common/v2/agent/agentic_edit_tools/list_package_info_tool_resolver.py +0 -42
  561. autocoder/common/v2/agent/agentic_edit_tools/test_execute_command_tool_resolver.py +0 -70
  562. autocoder/common/v2/agent/agentic_edit_tools/test_search_files_tool_resolver.py +0 -163
  563. autocoder/common/v2/agent/agentic_tool_display.py +0 -183
  564. autocoder/plugins/dynamic_completion_example.py +0 -148
  565. autocoder/plugins/sample_plugin.py +0 -160
  566. autocoder/sdk/cli/__main__.py +0 -26
  567. autocoder/sdk/cli/completion_wrapper.py +0 -38
  568. autocoder/sdk/cli/install_completion.py +0 -301
  569. autocoder/sdk/models/messages.py +0 -209
  570. autocoder/sdk/session/__init__.py +0 -32
  571. autocoder/sdk/session/session.py +0 -106
  572. autocoder/sdk/session/session_manager.py +0 -56
  573. {auto_coder-1.0.0.dist-info → auto_coder-2.0.0.dist-info}/top_level.txt +0 -0
  574. /autocoder/{sdk/example.py → common/agent_query_queue/__init__.py} +0 -0
@@ -0,0 +1,1014 @@
1
+ """
2
+ Interactive session manager for handling multiple interactive command sessions.
3
+
4
+ This module provides a thread-safe singleton manager for handling multiple
5
+ interactive command sessions with support for both InteractiveProcess and
6
+ InteractivePexpectProcess backends, automatic cleanup and resource management.
7
+ """
8
+
9
+ import threading
10
+ import time
11
+ import uuid
12
+ import atexit
13
+ import re
14
+ import platform
15
+ from typing import Dict, Optional, Any, List, Union, Type
16
+ from loguru import logger
17
+
18
+ from .interactive_executor import InteractiveCommandExecutor, InteractiveSession
19
+ from .interactive_process import InteractiveProcess
20
+ from .interactive_pexpect_process import InteractivePexpectProcess, PEXPECT_AVAILABLE
21
+ from .exceptions import CommandExecutionError
22
+
23
+
24
+ def clean_terminal_output(text: str) -> str:
25
+ """
26
+ Clean terminal control characters and ANSI escape sequences from output.
27
+
28
+ Args:
29
+ text: Raw terminal output
30
+
31
+ Returns:
32
+ Cleaned text with control characters removed
33
+ """
34
+ if not text:
35
+ return text
36
+ # return text
37
+
38
+ # Remove ANSI escape sequences
39
+ ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
40
+ text = ansi_escape.sub('', text)
41
+
42
+ # Normalize Windows CRLF to LF first
43
+ text = text.replace('\r\n', '\n').replace('\r', '\n')
44
+
45
+ # Handle standalone carriage returns (\r) that are used to return cursor to
46
+ # the beginning of the line (common in interactive shells). We simulate the
47
+ # overwrite by keeping only the content AFTER the last \r in each logical
48
+ # line.
49
+ if '\r' in text:
50
+ processed_lines = []
51
+ for raw_line in text.split('\n'):
52
+ # Repeatedly process carriage returns within the line
53
+ while '\r' in raw_line:
54
+ raw_line = raw_line.split('\r')[-1]
55
+ processed_lines.append(raw_line)
56
+ text = '\n'.join(processed_lines)
57
+
58
+ # Remove other control characters but keep newlines and tabs
59
+ control_chars = re.compile(r'[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]')
60
+ text = control_chars.sub('', text)
61
+
62
+ # Clean up multiple consecutive newlines
63
+ text = re.sub(r'\n\s*\n\s*\n+', '\n\n', text)
64
+
65
+ # Strip leading/trailing whitespace
66
+ text = text.strip()
67
+
68
+ return text
69
+
70
+
71
+ class ProcessType:
72
+ """Constants for process types."""
73
+ STANDARD = "standard"
74
+ PEXPECT = "pexpect"
75
+
76
+
77
+ class ToolResult:
78
+ """Result object for tool operations."""
79
+
80
+ def __init__(self, success: bool, message: str, content: Optional[Union[Dict[str, Any], str]] = None):
81
+ self.success = success
82
+ self.message = message
83
+ self.content = content or {}
84
+
85
+
86
+ class EnhancedSessionHandle:
87
+ """Enhanced handle for managing an interactive session with process type support."""
88
+
89
+ def __init__(
90
+ self,
91
+ session: InteractiveSession,
92
+ command: str,
93
+ process_type: str,
94
+ cwd: Optional[str] = None
95
+ ):
96
+ self.session = session
97
+ self.command = command
98
+ self.process_type = process_type
99
+ self.cwd = cwd
100
+ self.created_at = time.time()
101
+ self.last_activity = time.time()
102
+
103
+ def update_activity(self):
104
+ """Update the last activity timestamp."""
105
+ self.last_activity = time.time()
106
+
107
+ def get_stats(self) -> Dict[str, Any]:
108
+ """Get session statistics."""
109
+ return {
110
+ 'session_id': self.session.session_id,
111
+ 'command': self.command,
112
+ 'process_type': self.process_type,
113
+ 'cwd': self.cwd,
114
+ 'pid': self.session.process.pid if self.session.process else None,
115
+ 'created_at': self.created_at,
116
+ 'last_activity': self.last_activity,
117
+ 'duration': time.time() - self.created_at,
118
+ 'idle_time': time.time() - self.last_activity,
119
+ 'is_alive': self.session.process.is_alive() if self.session.process else False
120
+ }
121
+
122
+
123
+ class InteractiveSessionManager:
124
+ """Thread-safe singleton manager for interactive command sessions with process type switching."""
125
+
126
+ _instance = None
127
+ _lock = threading.Lock()
128
+
129
+ def __new__(cls):
130
+ if cls._instance is None:
131
+ with cls._lock:
132
+ if cls._instance is None:
133
+ cls._instance = super().__new__(cls)
134
+ cls._instance._initialized = False
135
+ return cls._instance
136
+
137
+ def __init__(self):
138
+ if self._initialized:
139
+ return
140
+
141
+ self._initialized = True
142
+ self.sessions: Dict[str, EnhancedSessionHandle] = {}
143
+ self.executor = InteractiveCommandExecutor(verbose=False)
144
+ self._cleanup_lock = threading.Lock()
145
+ self._cleanup_thread = None
146
+ self._shutdown = False
147
+
148
+ # Register cleanup on exit
149
+ atexit.register(self.cleanup_all)
150
+
151
+ # Start cleanup thread
152
+ self._start_cleanup_thread()
153
+
154
+ logger.info("InteractiveSessionManager initialized with process type switching support")
155
+
156
+ def _start_cleanup_thread(self):
157
+ """Start the background cleanup thread."""
158
+ def cleanup_worker():
159
+ while not self._shutdown:
160
+ try:
161
+ self._cleanup_idle_sessions()
162
+ time.sleep(60) # Check every minute
163
+ except Exception as e:
164
+ logger.error(f"Error in cleanup thread: {e}")
165
+ time.sleep(60)
166
+
167
+ self._cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
168
+ self._cleanup_thread.start()
169
+
170
+ def _cleanup_idle_sessions(self):
171
+ """Clean up idle sessions (30 minutes of inactivity)."""
172
+ current_time = time.time()
173
+ idle_timeout = 30 * 60 # 30 minutes
174
+
175
+ with self._cleanup_lock:
176
+ to_remove = []
177
+ for session_id, handle in self.sessions.items():
178
+ if current_time - handle.last_activity > idle_timeout:
179
+ logger.info(f"Cleaning up idle session {session_id} (type: {handle.process_type})")
180
+ try:
181
+ handle.session.terminate()
182
+ except Exception as e:
183
+ logger.error(f"Error terminating idle session {session_id}: {e}")
184
+ to_remove.append(session_id)
185
+
186
+ for session_id in to_remove:
187
+ self.sessions.pop(session_id, None)
188
+
189
+ def _determine_process_type(
190
+ self,
191
+ command: str,
192
+ use_pexpect: Optional[bool] = None
193
+ ) -> str:
194
+ """
195
+ Determine the best process type for a command.
196
+
197
+ Args:
198
+ command: Command to execute
199
+ use_pexpect: Explicit process type selection
200
+
201
+ Returns:
202
+ Process type string
203
+ """
204
+ # Explicit selection
205
+ if use_pexpect is not None:
206
+ if use_pexpect:
207
+ if PEXPECT_AVAILABLE:
208
+ return ProcessType.PEXPECT
209
+ else:
210
+ logger.warning("pexpect requested but not available, falling back to standard process")
211
+ return ProcessType.STANDARD
212
+ else:
213
+ return ProcessType.STANDARD
214
+
215
+ # Automatic selection based on command and platform
216
+ if not PEXPECT_AVAILABLE:
217
+ return ProcessType.STANDARD
218
+
219
+ # Commands that benefit from pexpect
220
+ interactive_commands = [
221
+ 'python', 'python3', 'node', 'repl', 'irb', 'ghci',
222
+ 'mysql', 'psql', 'sqlite3', 'redis-cli', 'mongo',
223
+ 'ssh', 'telnet', 'ftp', 'sftp'
224
+ ]
225
+
226
+ cmd_lower = command.lower()
227
+ for interactive_cmd in interactive_commands:
228
+ if cmd_lower.startswith(interactive_cmd):
229
+ logger.debug(f"Auto-selecting pexpect for interactive command: {command}")
230
+ return ProcessType.PEXPECT
231
+
232
+ # Default to standard process
233
+ return ProcessType.STANDARD
234
+
235
+ def _create_process(
236
+ self,
237
+ command: str,
238
+ process_type: str,
239
+ cwd: Optional[str] = None,
240
+ env: Optional[Dict[str, str]] = None,
241
+ timeout: Optional[int] = None,
242
+ **kwargs
243
+ ) -> Union[InteractiveProcess, InteractivePexpectProcess]:
244
+ """
245
+ Create a process of the specified type.
246
+
247
+ Args:
248
+ command: Command to execute
249
+ process_type: Type of process to create
250
+ cwd: Working directory
251
+ env: Environment variables
252
+ timeout: Process timeout
253
+ **kwargs: Additional process arguments
254
+
255
+ Returns:
256
+ Process instance
257
+
258
+ Raises:
259
+ CommandExecutionError: If process creation fails
260
+ """
261
+ try:
262
+ if process_type == ProcessType.PEXPECT:
263
+ if not PEXPECT_AVAILABLE:
264
+ raise CommandExecutionError("pexpect not available on this platform")
265
+
266
+ process = InteractivePexpectProcess(
267
+ command=command,
268
+ cwd=cwd,
269
+ env=env,
270
+ timeout=timeout,
271
+ **kwargs
272
+ )
273
+ else:
274
+ process = InteractiveProcess(
275
+ command=command,
276
+ cwd=cwd,
277
+ env=env,
278
+ **kwargs
279
+ )
280
+
281
+ process.start()
282
+ return process
283
+
284
+ except Exception as e:
285
+ # Fallback to standard process if pexpect fails
286
+ if process_type == ProcessType.PEXPECT:
287
+ logger.warning(f"pexpect process creation failed, falling back to standard: {e}")
288
+ return self._create_process(
289
+ command, ProcessType.STANDARD, cwd, env, timeout, **kwargs
290
+ )
291
+ else:
292
+ raise CommandExecutionError(f"Failed to create process: {e}")
293
+
294
+ def create_session(
295
+ self,
296
+ command: str,
297
+ cwd: Optional[str] = None,
298
+ env: Optional[Dict[str, str]] = None,
299
+ timeout: Optional[int] = None,
300
+ use_pexpect: Optional[bool] = None,
301
+ session_id: Optional[str] = None,
302
+ **kwargs
303
+ ) -> ToolResult:
304
+ """
305
+ Create a new interactive session with automatic or manual process type selection.
306
+
307
+ Args:
308
+ command: Command to execute
309
+ cwd: Working directory
310
+ env: Environment variables
311
+ timeout: Session timeout in seconds
312
+ use_pexpect: Explicit process type selection (None=auto, True=pexpect, False=standard)
313
+ session_id: Custom session ID
314
+ **kwargs: Additional process arguments
315
+
316
+ Returns:
317
+ ToolResult with session information
318
+ """
319
+ try:
320
+ if session_id is None:
321
+ session_id = str(uuid.uuid4())
322
+
323
+ # Determine process type
324
+ process_type = self._determine_process_type(command, use_pexpect)
325
+
326
+ # Create process
327
+ process = self._create_process(
328
+ command=command,
329
+ process_type=process_type,
330
+ cwd=cwd,
331
+ env=env,
332
+ timeout=timeout,
333
+ **kwargs
334
+ )
335
+
336
+ # Create session wrapper
337
+ session = InteractiveSession(
338
+ process=process,
339
+ session_id=session_id,
340
+ timeout=timeout
341
+ )
342
+
343
+ # Create enhanced handle
344
+ handle = EnhancedSessionHandle(session, command, process_type, cwd)
345
+
346
+ # Store session
347
+ with self._cleanup_lock:
348
+ self.sessions[session_id] = handle
349
+
350
+ logger.info(f"Created interactive session {session_id} using {process_type} process for command: {command}")
351
+
352
+ return ToolResult(
353
+ success=True,
354
+ message=f"Interactive session started successfully using {process_type} process",
355
+ content={
356
+ 'session_id': session_id,
357
+ 'pid': process.pid if process else None,
358
+ 'command': command,
359
+ 'process_type': process_type,
360
+ 'cwd': cwd,
361
+ 'created_at': handle.created_at
362
+ }
363
+ )
364
+
365
+ except Exception as e:
366
+ logger.error(f"Failed to create interactive session: {e}")
367
+ return ToolResult(
368
+ success=False,
369
+ message=f"Failed to create interactive session: {str(e)}"
370
+ )
371
+
372
+ def send_input(
373
+ self,
374
+ session_id: str,
375
+ input_text: str,
376
+ read_timeout: Optional[int] = None,
377
+ max_bytes: Optional[int] = None,
378
+ expect_prompt: Optional[bool] = False,
379
+ prompt_regex: Optional[str] = r'>>> ?$'
380
+ ) -> ToolResult:
381
+ """
382
+ Send input to a session and read output with process-type-aware handling.
383
+
384
+ Args:
385
+ session_id: Session ID
386
+ input_text: Text to send to stdin
387
+ read_timeout: Timeout for reading output
388
+ max_bytes: Maximum bytes to read
389
+ expect_prompt: Whether to wait for prompt before returning
390
+ prompt_regex: Regular expression to match prompt
391
+
392
+ Returns:
393
+ ToolResult with output
394
+ """
395
+ with self._cleanup_lock:
396
+ handle = self.sessions.get(session_id)
397
+ if not handle:
398
+ return ToolResult(
399
+ success=False,
400
+ message=f"Session {session_id} not found"
401
+ )
402
+
403
+ # Update activity
404
+ handle.update_activity()
405
+
406
+ try:
407
+ # Check if session is still alive
408
+ if not handle.session.process.is_alive():
409
+ return ToolResult(
410
+ success=False,
411
+ message=f"Session {session_id} is no longer alive"
412
+ )
413
+
414
+ # Send input based on process type
415
+ if handle.process_type == ProcessType.PEXPECT:
416
+ # Use pexpect-specific methods
417
+ if hasattr(handle.session.process, 'sendline'):
418
+ # Remove trailing newline if present since sendline adds it
419
+ clean_input = input_text.rstrip('\n\r')
420
+ handle.session.process.sendline(clean_input)
421
+ else:
422
+ handle.session.process.write(input_text)
423
+ else:
424
+ # Use standard process write
425
+ handle.session.process.write(input_text)
426
+
427
+ # Read output based on expect_prompt setting and process type
428
+ if expect_prompt and handle.process_type == ProcessType.PEXPECT:
429
+ # Use pexpect expect functionality
430
+ raw_output = self._read_until_prompt_pexpect(
431
+ handle.session.process,
432
+ prompt_regex or r'>>> ?$',
433
+ read_timeout or 2
434
+ )
435
+ elif expect_prompt:
436
+ # Use standard process with prompt detection
437
+ raw_output = self._read_until_prompt(
438
+ handle.session.process,
439
+ prompt_regex or r'>>> ?$',
440
+ read_timeout or 2
441
+ )
442
+ else:
443
+ # Standard output reading
444
+ raw_output = handle.session.process.read_output(
445
+ timeout=read_timeout or 2
446
+ )
447
+
448
+ # Clean the output to remove terminal control characters
449
+ cleaned_output = clean_terminal_output(raw_output or '')
450
+
451
+ logger.debug(f"Session {session_id} ({handle.process_type}): sent input, got {len(raw_output or '')} bytes raw output, {len(cleaned_output)} chars cleaned, expect_prompt={expect_prompt}")
452
+
453
+ return ToolResult(
454
+ success=True,
455
+ message="Input sent and output received",
456
+ content={
457
+ 'session_id': session_id,
458
+ 'output': cleaned_output,
459
+ 'raw_output': raw_output or '',
460
+ 'input_sent': input_text,
461
+ 'bytes_read': len(raw_output or ''),
462
+ 'cleaned_length': len(cleaned_output),
463
+ 'process_type': handle.process_type
464
+ }
465
+ )
466
+
467
+ except Exception as e:
468
+ logger.error(f"Error interacting with session {session_id}: {e}")
469
+ return ToolResult(
470
+ success=False,
471
+ message=f"Error interacting with session: {str(e)}"
472
+ )
473
+
474
+ def read_output(
475
+ self,
476
+ session_id: str,
477
+ read_timeout: Optional[int] = None,
478
+ max_bytes: Optional[int] = None,
479
+ expect_prompt: Optional[bool] = False,
480
+ prompt_regex: Optional[str] = r'>>> ?$'
481
+ ) -> ToolResult:
482
+ """
483
+ Read output from a session without sending any input.
484
+
485
+ Args:
486
+ session_id: Session ID
487
+ read_timeout: Timeout for reading output
488
+ max_bytes: Maximum bytes to read
489
+ expect_prompt: Whether to wait for prompt before returning
490
+ prompt_regex: Regular expression to match prompt
491
+
492
+ Returns:
493
+ ToolResult with output
494
+ """
495
+ with self._cleanup_lock:
496
+ handle = self.sessions.get(session_id)
497
+ if not handle:
498
+ return ToolResult(
499
+ success=False,
500
+ message=f"Session {session_id} not found"
501
+ )
502
+
503
+ # Update activity
504
+ handle.update_activity()
505
+
506
+ try:
507
+ # Check if session is still alive
508
+ if not handle.session.process.is_alive():
509
+ return ToolResult(
510
+ success=False,
511
+ message=f"Session {session_id} is no longer alive"
512
+ )
513
+
514
+ # Read output based on process type and expect_prompt setting
515
+ if expect_prompt and handle.process_type == ProcessType.PEXPECT:
516
+ raw_output = self._read_until_prompt_pexpect(
517
+ handle.session.process,
518
+ prompt_regex or r'>>> ?$',
519
+ read_timeout or 2
520
+ )
521
+ elif expect_prompt:
522
+ raw_output = self._read_until_prompt(
523
+ handle.session.process,
524
+ prompt_regex or r'>>> ?$',
525
+ read_timeout or 2
526
+ )
527
+ else:
528
+ raw_output = handle.session.process.read_output(
529
+ timeout=read_timeout or 2
530
+ )
531
+
532
+ # Clean the output to remove terminal control characters
533
+ cleaned_output = clean_terminal_output(raw_output or '')
534
+
535
+ logger.debug(f"Session {session_id} ({handle.process_type}): read output only, got {len(raw_output or '')} bytes raw output, {len(cleaned_output)} chars cleaned, expect_prompt={expect_prompt}")
536
+
537
+ return ToolResult(
538
+ success=True,
539
+ message="Output read successfully",
540
+ content={
541
+ 'session_id': session_id,
542
+ 'output': cleaned_output,
543
+ 'raw_output': raw_output or '',
544
+ 'input_sent': None,
545
+ 'bytes_read': len(raw_output or ''),
546
+ 'cleaned_length': len(cleaned_output),
547
+ 'process_type': handle.process_type
548
+ }
549
+ )
550
+
551
+ except Exception as e:
552
+ logger.error(f"Error reading output from session {session_id}: {e}")
553
+ return ToolResult(
554
+ success=False,
555
+ message=f"Error reading output from session: {str(e)}"
556
+ )
557
+
558
+ def read_output_progressive(
559
+ self,
560
+ session_id: str,
561
+ read_timeout: Optional[int] = None,
562
+ max_bytes: Optional[int] = None,
563
+ expect_prompt: Optional[bool] = False,
564
+ prompt_regex: Optional[str] = r'>>> ?$'
565
+ ) -> ToolResult:
566
+ """
567
+ Progressively read output from a session with intelligent timeout handling.
568
+
569
+ This method waits for initial output, then continues reading as long as content
570
+ is available, stopping only after 5 seconds of no new content.
571
+
572
+ Args:
573
+ session_id: Session ID
574
+ read_timeout: Initial timeout for reading output (default: 3 seconds)
575
+ max_bytes: Maximum bytes to read in total
576
+ expect_prompt: Whether to wait for prompt before returning
577
+ prompt_regex: Regular expression to match prompt
578
+
579
+ Returns:
580
+ ToolResult with accumulated output
581
+ """
582
+ with self._cleanup_lock:
583
+ handle = self.sessions.get(session_id)
584
+ if not handle:
585
+ return ToolResult(
586
+ success=False,
587
+ message=f"Session {session_id} not found"
588
+ )
589
+
590
+ # Update activity
591
+ handle.update_activity()
592
+
593
+ try:
594
+ # Check if session is still alive
595
+ if not handle.session.process.is_alive():
596
+ return ToolResult(
597
+ success=False,
598
+ message=f"Session {session_id} is no longer alive"
599
+ )
600
+
601
+ # Progressive reading logic
602
+ initial_timeout = read_timeout or 3
603
+ accumulated_output = ""
604
+ total_bytes_read = 0
605
+ read_cycles = 0
606
+ no_content_duration = 0
607
+ last_read_time = time.time()
608
+
609
+ logger.debug(f"Session {session_id} ({handle.process_type}): Starting progressive read with initial timeout {initial_timeout}s")
610
+
611
+ while True:
612
+ read_cycles += 1
613
+ cycle_start_time = time.time()
614
+
615
+ # Read with current timeout
616
+ if expect_prompt and accumulated_output:
617
+ # If we already have some output and expect prompt, check if we found it
618
+ lines = accumulated_output.split('\n')
619
+ if lines and re.search(prompt_regex or r'>>> ?$', lines[-1]):
620
+ logger.debug(f"Session {session_id}: Found prompt pattern, stopping progressive read")
621
+ break
622
+
623
+ # Read a chunk
624
+ chunk = handle.session.process.read_output(timeout=initial_timeout)
625
+
626
+ if chunk and chunk.strip():
627
+ # Got content, reset no-content counter
628
+ accumulated_output += chunk
629
+ total_bytes_read += len(chunk)
630
+ no_content_duration = 0
631
+ last_read_time = time.time()
632
+
633
+ logger.debug(f"Session {session_id}: Read cycle {read_cycles}, got {len(chunk)} bytes, total: {total_bytes_read} bytes")
634
+
635
+ # Check max_bytes limit
636
+ if max_bytes and total_bytes_read >= max_bytes:
637
+ logger.debug(f"Session {session_id}: Reached max_bytes limit ({max_bytes}), stopping")
638
+ break
639
+
640
+ else:
641
+ # No content in this cycle
642
+ current_time = time.time()
643
+ no_content_duration += current_time - cycle_start_time
644
+
645
+ logger.debug(f"Session {session_id}: Read cycle {read_cycles}, no content for {no_content_duration:.1f}s")
646
+
647
+ # If we have some content and no new content for 5 seconds, stop
648
+ if accumulated_output and no_content_duration >= 5.0:
649
+ logger.debug(f"Session {session_id}: No new content for 5 seconds, stopping progressive read")
650
+ break
651
+
652
+ # If we have no content at all and waited initial timeout, stop
653
+ if not accumulated_output and no_content_duration >= initial_timeout:
654
+ logger.debug(f"Session {session_id}: No initial content after {initial_timeout}s, stopping")
655
+ break
656
+
657
+ # Brief pause between cycles to avoid excessive CPU usage
658
+ time.sleep(0.1)
659
+
660
+ # Clean the accumulated output
661
+ cleaned_output = clean_terminal_output(accumulated_output)
662
+
663
+ logger.debug(f"Session {session_id} ({handle.process_type}): Progressive read completed after {read_cycles} cycles, "
664
+ f"got {total_bytes_read} bytes raw output, {len(cleaned_output)} chars cleaned")
665
+
666
+ return ToolResult(
667
+ success=True,
668
+ message=f"Progressive output read completed ({read_cycles} cycles)",
669
+ content={
670
+ 'session_id': session_id,
671
+ 'output': cleaned_output,
672
+ 'raw_output': accumulated_output,
673
+ 'input_sent': None,
674
+ 'bytes_read': total_bytes_read,
675
+ 'cleaned_length': len(cleaned_output),
676
+ 'read_cycles': read_cycles,
677
+ 'total_duration': time.time() - last_read_time + no_content_duration,
678
+ 'process_type': handle.process_type
679
+ }
680
+ )
681
+
682
+ except Exception as e:
683
+ logger.error(f"Error in progressive read from session {session_id}: {e}")
684
+ return ToolResult(
685
+ success=False,
686
+ message=f"Error in progressive read from session: {str(e)}"
687
+ )
688
+
689
+ def send_input_then_get_progressive(
690
+ self,
691
+ session_id: str,
692
+ input_text: str,
693
+ read_timeout: Optional[int] = None,
694
+ max_bytes: Optional[int] = None,
695
+ expect_prompt: Optional[bool] = False,
696
+ prompt_regex: Optional[str] = r'>>> ?$'
697
+ ) -> ToolResult:
698
+ """
699
+ Send input to a session and then progressively read output with intelligent timeout handling.
700
+
701
+ This method sends input first, then waits for initial output and continues reading
702
+ as long as content is available, stopping only after 5 seconds of no new content.
703
+
704
+ Args:
705
+ session_id: Session ID
706
+ input_text: Text to send to stdin
707
+ read_timeout: Initial timeout for reading output (default: 3 seconds)
708
+ max_bytes: Maximum bytes to read in total
709
+ expect_prompt: Whether to wait for prompt before returning
710
+ prompt_regex: Regular expression to match prompt
711
+
712
+ Returns:
713
+ ToolResult with accumulated output
714
+ """
715
+ with self._cleanup_lock:
716
+ handle = self.sessions.get(session_id)
717
+ if not handle:
718
+ return ToolResult(
719
+ success=False,
720
+ message=f"Session {session_id} not found"
721
+ )
722
+
723
+ # Update activity
724
+ handle.update_activity()
725
+
726
+ try:
727
+ # Check if session is still alive
728
+ if not handle.session.process.is_alive():
729
+ return ToolResult(
730
+ success=False,
731
+ message=f"Session {session_id} is no longer alive"
732
+ )
733
+
734
+ # Send input first
735
+ logger.debug(f"Session {session_id} ({handle.process_type}): Sending input: {input_text[:50]}...")
736
+
737
+ if handle.process_type == ProcessType.PEXPECT:
738
+ # Use pexpect-specific methods
739
+ if hasattr(handle.session.process, 'sendline'):
740
+ clean_input = input_text.rstrip('\n\r')
741
+ handle.session.process.sendline(clean_input)
742
+ else:
743
+ handle.session.process.write(input_text)
744
+ else:
745
+ handle.session.process.write(input_text)
746
+
747
+ # Progressive reading logic (similar to read_output_progressive)
748
+ initial_timeout = read_timeout or 3
749
+ accumulated_output = ""
750
+ total_bytes_read = 0
751
+ read_cycles = 0
752
+ no_content_duration = 0
753
+ last_read_time = time.time()
754
+
755
+ logger.debug(f"Session {session_id} ({handle.process_type}): Starting progressive read after input with initial timeout {initial_timeout}s")
756
+
757
+ while True:
758
+ read_cycles += 1
759
+ cycle_start_time = time.time()
760
+
761
+ # Read with current timeout
762
+ if expect_prompt and accumulated_output:
763
+ # If we already have some output and expect prompt, check if we found it
764
+ lines = accumulated_output.split('\n')
765
+ if lines and re.search(prompt_regex or r'>>> ?$', lines[-1]):
766
+ logger.debug(f"Session {session_id}: Found prompt pattern, stopping progressive read")
767
+ break
768
+
769
+ # Read a chunk
770
+ chunk = handle.session.process.read_output(timeout=initial_timeout)
771
+
772
+ if chunk and chunk.strip():
773
+ # Got content, reset no-content counter
774
+ accumulated_output += chunk
775
+ total_bytes_read += len(chunk)
776
+ no_content_duration = 0
777
+ last_read_time = time.time()
778
+
779
+ logger.debug(f"Session {session_id}: Read cycle {read_cycles}, got {len(chunk)} bytes, total: {total_bytes_read} bytes")
780
+
781
+ # Check max_bytes limit
782
+ if max_bytes and total_bytes_read >= max_bytes:
783
+ logger.debug(f"Session {session_id}: Reached max_bytes limit ({max_bytes}), stopping")
784
+ break
785
+
786
+ else:
787
+ # No content in this cycle
788
+ current_time = time.time()
789
+ no_content_duration += current_time - cycle_start_time
790
+
791
+ logger.debug(f"Session {session_id}: Read cycle {read_cycles}, no content for {no_content_duration:.1f}s")
792
+
793
+ # If we have some content and no new content for 5 seconds, stop
794
+ if accumulated_output and no_content_duration >= 5.0:
795
+ logger.debug(f"Session {session_id}: No new content for 5 seconds, stopping progressive read")
796
+ break
797
+
798
+ # If we have no content at all and waited initial timeout, stop
799
+ if not accumulated_output and no_content_duration >= initial_timeout:
800
+ logger.debug(f"Session {session_id}: No initial content after {initial_timeout}s, stopping")
801
+ break
802
+
803
+ # Brief pause between cycles to avoid excessive CPU usage
804
+ time.sleep(0.1)
805
+
806
+ # Clean the accumulated output
807
+ cleaned_output = clean_terminal_output(accumulated_output)
808
+
809
+ logger.debug(f"Session {session_id} ({handle.process_type}): Progressive read after input completed after {read_cycles} cycles, "
810
+ f"got {total_bytes_read} bytes raw output, {len(cleaned_output)} chars cleaned")
811
+
812
+ return ToolResult(
813
+ success=True,
814
+ message=f"Input sent and progressive output read completed ({read_cycles} cycles)",
815
+ content={
816
+ 'session_id': session_id,
817
+ 'output': cleaned_output,
818
+ 'raw_output': accumulated_output,
819
+ 'input_sent': input_text,
820
+ 'bytes_read': total_bytes_read,
821
+ 'cleaned_length': len(cleaned_output),
822
+ 'read_cycles': read_cycles,
823
+ 'total_duration': time.time() - last_read_time + no_content_duration,
824
+ 'process_type': handle.process_type
825
+ }
826
+ )
827
+
828
+ except Exception as e:
829
+ logger.error(f"Error in send input then progressive read from session {session_id}: {e}")
830
+ return ToolResult(
831
+ success=False,
832
+ message=f"Error in send input then progressive read from session: {str(e)}"
833
+ )
834
+
835
+ def terminate_session(self, session_id: str, force: bool = False) -> ToolResult:
836
+ """
837
+ Terminate a session.
838
+
839
+ Args:
840
+ session_id: Session ID
841
+ force: Whether to force termination
842
+
843
+ Returns:
844
+ ToolResult indicating success
845
+ """
846
+ with self._cleanup_lock:
847
+ handle = self.sessions.pop(session_id, None)
848
+ if not handle:
849
+ return ToolResult(
850
+ success=False,
851
+ message=f"Session {session_id} not found"
852
+ )
853
+
854
+ try:
855
+ if force:
856
+ # Force termination - use terminate with grace_timeout=0
857
+ handle.session.process.terminate(grace_timeout=0.0)
858
+ else:
859
+ # Graceful termination
860
+ handle.session.terminate()
861
+
862
+ logger.info(f"Terminated session {session_id} ({handle.process_type}, force={force})")
863
+
864
+ return ToolResult(
865
+ success=True,
866
+ message=f"Session {session_id} terminated successfully",
867
+ content={
868
+ 'session_id': session_id,
869
+ 'process_type': handle.process_type,
870
+ 'terminated': True,
871
+ 'force': force
872
+ }
873
+ )
874
+
875
+ except Exception as e:
876
+ logger.error(f"Error terminating session {session_id}: {e}")
877
+ return ToolResult(
878
+ success=False,
879
+ message=f"Error terminating session: {str(e)}"
880
+ )
881
+
882
+ def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
883
+ """Get information about a session."""
884
+ with self._cleanup_lock:
885
+ handle = self.sessions.get(session_id)
886
+ if handle:
887
+ return handle.get_stats()
888
+ return None
889
+
890
+ def list_sessions(self) -> List[Dict[str, Any]]:
891
+ """List all active sessions with process type information."""
892
+ with self._cleanup_lock:
893
+ return [handle.get_stats() for handle in self.sessions.values()]
894
+
895
+ def get_process_type_stats(self) -> Dict[str, Any]:
896
+ """Get statistics about process types in use."""
897
+ with self._cleanup_lock:
898
+ stats = {
899
+ 'total_sessions': len(self.sessions),
900
+ 'by_process_type': {},
901
+ 'pexpect_available': PEXPECT_AVAILABLE,
902
+ 'platform': platform.system()
903
+ }
904
+
905
+ for handle in self.sessions.values():
906
+ process_type = handle.process_type
907
+ if process_type not in stats['by_process_type']:
908
+ stats['by_process_type'][process_type] = 0
909
+ stats['by_process_type'][process_type] += 1
910
+
911
+ return stats
912
+
913
+ def cleanup_all(self):
914
+ """Clean up all sessions."""
915
+ self._shutdown = True
916
+
917
+ with self._cleanup_lock:
918
+ for session_id, handle in list(self.sessions.items()):
919
+ try:
920
+ logger.info(f"Cleaning up session {session_id} ({handle.process_type})")
921
+ handle.session.terminate()
922
+ except Exception as e:
923
+ logger.error(f"Error terminating session {session_id} during cleanup: {e}")
924
+
925
+ self.sessions.clear()
926
+
927
+ # Clean up executor
928
+ try:
929
+ self.executor.cleanup()
930
+ except Exception as e:
931
+ logger.error(f"Error cleaning up executor: {e}")
932
+
933
+ logger.info("InteractiveSessionManager cleanup completed")
934
+
935
+ def _read_until_prompt(self, process, prompt_regex: str, timeout: int) -> str:
936
+ """
937
+ Read output until a prompt pattern is found or timeout occurs (standard process).
938
+
939
+ Args:
940
+ process: The interactive process
941
+ prompt_regex: Regular expression to match prompt
942
+ timeout: Total timeout in seconds
943
+
944
+ Returns:
945
+ Accumulated output string
946
+ """
947
+ import time
948
+ import re
949
+
950
+ start_time = time.time()
951
+ accumulated_output = ""
952
+ prompt_pattern = re.compile(prompt_regex, re.MULTILINE)
953
+
954
+ while time.time() - start_time < timeout:
955
+ # Read a chunk with short timeout
956
+ chunk = process.read_output(timeout=0.1)
957
+ if chunk:
958
+ accumulated_output += chunk
959
+
960
+ # Check if we found the prompt pattern
961
+ # Look for prompt at the end of the accumulated output
962
+ lines = accumulated_output.split('\n')
963
+ if len(lines) > 0:
964
+ last_line = lines[-1]
965
+ if prompt_pattern.search(last_line):
966
+ logger.debug(f"Found prompt pattern '{prompt_regex}' in: {repr(last_line)}")
967
+ break
968
+ else:
969
+ # No output available, sleep briefly
970
+ time.sleep(0.05)
971
+
972
+ return accumulated_output
973
+
974
+ def _read_until_prompt_pexpect(self, process, prompt_regex: str, timeout: int) -> str:
975
+ """
976
+ Read output until a prompt pattern is found using pexpect expect functionality.
977
+
978
+ Args:
979
+ process: The pexpect process
980
+ prompt_regex: Regular expression to match prompt
981
+ timeout: Total timeout in seconds
982
+
983
+ Returns:
984
+ Accumulated output string
985
+ """
986
+ try:
987
+ if hasattr(process, 'expect'):
988
+ # Use pexpect's built-in expect functionality
989
+ process.expect(prompt_regex, timeout=timeout)
990
+ # Return the output before the match
991
+ before = getattr(process, 'before', '')
992
+ after = getattr(process, 'after', '')
993
+ return (before or '') + (after or '')
994
+ else:
995
+ # Fallback to standard prompt reading
996
+ return self._read_until_prompt(process, prompt_regex, timeout)
997
+ except Exception as e:
998
+ logger.debug(f"pexpect expect failed, using fallback: {e}")
999
+ return self._read_until_prompt(process, prompt_regex, timeout)
1000
+
1001
+
1002
+ # Global instance
1003
+ _session_manager = None
1004
+ _session_manager_lock = threading.Lock()
1005
+
1006
+
1007
+ def get_session_manager() -> InteractiveSessionManager:
1008
+ """Get the global session manager instance."""
1009
+ global _session_manager
1010
+ if _session_manager is None:
1011
+ with _session_manager_lock:
1012
+ if _session_manager is None:
1013
+ _session_manager = InteractiveSessionManager()
1014
+ return _session_manager