codepp 0.0.437__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 (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,453 @@
1
+ """PTY Manager for terminal emulation with cross-platform support.
2
+
3
+ Provides pseudo-terminal (PTY) functionality for interactive shell sessions
4
+ via WebSocket connections. Supports Unix (pty module) and Windows (pywinpty).
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ import os
10
+ import signal
11
+ import struct
12
+ import sys
13
+ from dataclasses import dataclass, field
14
+ from typing import Any, Callable, Optional
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Platform detection
19
+ IS_WINDOWS = sys.platform == "win32"
20
+
21
+ # Conditional imports based on platform
22
+ if IS_WINDOWS:
23
+ try:
24
+ import winpty # type: ignore
25
+
26
+ HAS_WINPTY = True
27
+ except ImportError:
28
+ HAS_WINPTY = False
29
+ winpty = None
30
+ else:
31
+ import fcntl
32
+ import pty
33
+ import termios
34
+
35
+ HAS_WINPTY = False
36
+
37
+
38
+ @dataclass
39
+ class PTYSession:
40
+ """Represents an active PTY session."""
41
+
42
+ session_id: str
43
+ master_fd: Optional[int] = None # Unix only
44
+ slave_fd: Optional[int] = None # Unix only
45
+ pid: Optional[int] = None # Unix only
46
+ winpty_process: Any = None # Windows only
47
+ cols: int = 80
48
+ rows: int = 24
49
+ on_output: Optional[Callable[[bytes], None]] = None
50
+ _reader_task: Optional[asyncio.Task] = None # type: ignore
51
+ _running: bool = field(default=False, init=False)
52
+
53
+ def is_alive(self) -> bool:
54
+ """Check if the PTY session is still active."""
55
+ if IS_WINDOWS:
56
+ return self.winpty_process is not None and self.winpty_process.isalive()
57
+ else:
58
+ if self.pid is None:
59
+ return False
60
+ try:
61
+ pid_result, status = os.waitpid(self.pid, os.WNOHANG)
62
+ if pid_result == 0:
63
+ return True # Still running
64
+ return False # Exited
65
+ except ChildProcessError:
66
+ return False # Already reaped
67
+
68
+
69
+ class PTYManager:
70
+ """Manages PTY sessions for terminal emulation.
71
+
72
+ Provides cross-platform terminal emulation with support for:
73
+ - Unix systems via the pty module
74
+ - Windows via pywinpty (optional dependency)
75
+
76
+ Example:
77
+ manager = PTYManager()
78
+ session = await manager.create_session(
79
+ session_id="my-terminal",
80
+ on_output=lambda data: print(data.decode())
81
+ )
82
+ await manager.write(session.session_id, b"ls -la\n")
83
+ await manager.close_session(session.session_id)
84
+ """
85
+
86
+ def __init__(self) -> None:
87
+ self._sessions: dict[str, PTYSession] = {}
88
+ self._lock = asyncio.Lock()
89
+
90
+ @property
91
+ def sessions(self) -> dict[str, PTYSession]:
92
+ """Get all active sessions."""
93
+ return self._sessions.copy()
94
+
95
+ async def create_session(
96
+ self,
97
+ session_id: str,
98
+ cols: int = 80,
99
+ rows: int = 24,
100
+ on_output: Optional[Callable[[bytes], None]] = None,
101
+ shell: Optional[str] = None,
102
+ ) -> PTYSession:
103
+ """Create a new PTY session.
104
+
105
+ Args:
106
+ session_id: Unique identifier for the session
107
+ cols: Terminal width in columns
108
+ rows: Terminal height in rows
109
+ on_output: Callback for terminal output
110
+ shell: Shell to spawn (defaults to user's shell or /bin/bash)
111
+
112
+ Returns:
113
+ PTYSession: The created session
114
+
115
+ Raises:
116
+ RuntimeError: If session creation fails
117
+ """
118
+ async with self._lock:
119
+ if session_id in self._sessions:
120
+ logger.warning(f"Session {session_id} already exists, closing old one")
121
+ await self._close_session_internal(session_id)
122
+
123
+ if IS_WINDOWS:
124
+ session = await self._create_windows_session(
125
+ session_id, cols, rows, on_output, shell
126
+ )
127
+ else:
128
+ session = await self._create_unix_session(
129
+ session_id, cols, rows, on_output, shell
130
+ )
131
+
132
+ self._sessions[session_id] = session
133
+ logger.info(f"Created PTY session: {session_id}")
134
+ return session
135
+
136
+ async def _create_unix_session(
137
+ self,
138
+ session_id: str,
139
+ cols: int,
140
+ rows: int,
141
+ on_output: Optional[Callable[[bytes], None]],
142
+ shell: Optional[str],
143
+ ) -> PTYSession:
144
+ """Create a PTY session on Unix systems."""
145
+ shell = shell or os.environ.get("SHELL", "/bin/bash")
146
+
147
+ # Fork a new process with a PTY
148
+ pid, master_fd = pty.fork()
149
+
150
+ if pid == 0:
151
+ # Child process - exec the shell
152
+ os.execlp(shell, shell, "-i") # noqa: S606
153
+ else:
154
+ # Parent process
155
+ # Set terminal size
156
+ self._set_unix_winsize(master_fd, rows, cols)
157
+
158
+ # Make master_fd non-blocking
159
+ flags = fcntl.fcntl(master_fd, fcntl.F_GETFL)
160
+ fcntl.fcntl(master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
161
+
162
+ session = PTYSession(
163
+ session_id=session_id,
164
+ master_fd=master_fd,
165
+ pid=pid,
166
+ cols=cols,
167
+ rows=rows,
168
+ on_output=on_output,
169
+ )
170
+ session._running = True
171
+
172
+ # Start reader task
173
+ session._reader_task = asyncio.create_task(self._unix_reader_loop(session))
174
+
175
+ return session
176
+
177
+ async def _create_windows_session(
178
+ self,
179
+ session_id: str,
180
+ cols: int,
181
+ rows: int,
182
+ on_output: Optional[Callable[[bytes], None]],
183
+ shell: Optional[str],
184
+ ) -> PTYSession:
185
+ """Create a PTY session on Windows systems."""
186
+ if not HAS_WINPTY:
187
+ raise RuntimeError(
188
+ "pywinpty is required for Windows terminal support. "
189
+ "Install it with: pip install pywinpty"
190
+ )
191
+
192
+ shell = shell or os.environ.get("COMSPEC", "cmd.exe")
193
+
194
+ # Create winpty process
195
+ winpty_process = winpty.PtyProcess.spawn(
196
+ shell,
197
+ dimensions=(rows, cols),
198
+ )
199
+
200
+ session = PTYSession(
201
+ session_id=session_id,
202
+ winpty_process=winpty_process,
203
+ cols=cols,
204
+ rows=rows,
205
+ on_output=on_output,
206
+ )
207
+ session._running = True
208
+
209
+ # Start reader task
210
+ session._reader_task = asyncio.create_task(self._windows_reader_loop(session))
211
+
212
+ return session
213
+
214
+ def _set_unix_winsize(self, fd: int, rows: int, cols: int) -> None:
215
+ """Set the terminal window size on Unix."""
216
+ winsize = struct.pack("HHHH", rows, cols, 0, 0)
217
+ fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
218
+
219
+ async def _unix_reader_loop(self, session: PTYSession) -> None:
220
+ """Read output from Unix PTY and forward to callback."""
221
+ loop = asyncio.get_running_loop()
222
+
223
+ try:
224
+ while session._running and session.master_fd is not None:
225
+ try:
226
+ data = await loop.run_in_executor(
227
+ None, self._read_unix_pty, session.master_fd
228
+ )
229
+
230
+ if data is None:
231
+ # No data available, wait a bit
232
+ await asyncio.sleep(0.01)
233
+ continue
234
+ elif data == b"":
235
+ # EOF - process terminated
236
+ break
237
+ elif session.on_output:
238
+ session.on_output(data)
239
+
240
+ except asyncio.CancelledError:
241
+ break
242
+
243
+ except Exception as e:
244
+ logger.error(f"Unix reader loop error: {e}")
245
+ finally:
246
+ session._running = False
247
+
248
+ def _read_unix_pty(self, fd: int) -> bytes | None:
249
+ """Read from Unix PTY file descriptor.
250
+
251
+ Returns:
252
+ bytes: Data read from PTY
253
+ None: No data available (would block)
254
+ b'': EOF (process terminated)
255
+ """
256
+ try:
257
+ data = os.read(fd, 4096)
258
+ return data
259
+ except BlockingIOError:
260
+ return None
261
+ except OSError:
262
+ return b""
263
+
264
+ async def _windows_reader_loop(self, session: PTYSession) -> None:
265
+ """Read output from Windows PTY and forward to callback."""
266
+ loop = asyncio.get_running_loop()
267
+
268
+ try:
269
+ while (
270
+ session._running
271
+ and session.winpty_process is not None
272
+ and session.winpty_process.isalive()
273
+ ):
274
+ try:
275
+ data = await loop.run_in_executor(
276
+ None, session.winpty_process.read, 4096
277
+ )
278
+ if data and session.on_output:
279
+ session.on_output(
280
+ data.encode() if isinstance(data, str) else data
281
+ )
282
+ except EOFError:
283
+ break
284
+ except asyncio.CancelledError:
285
+ break
286
+
287
+ await asyncio.sleep(0.01)
288
+
289
+ except Exception as e:
290
+ logger.error(f"Windows reader loop error: {e}")
291
+ finally:
292
+ session._running = False
293
+
294
+ async def write(self, session_id: str, data: bytes) -> bool:
295
+ """Write data to a PTY session.
296
+
297
+ Args:
298
+ session_id: The session to write to
299
+ data: Data to write
300
+
301
+ Returns:
302
+ bool: True if write succeeded
303
+ """
304
+ session = self._sessions.get(session_id)
305
+ if not session:
306
+ logger.warning(f"Session {session_id} not found")
307
+ return False
308
+
309
+ try:
310
+ if IS_WINDOWS:
311
+ if session.winpty_process:
312
+ session.winpty_process.write(
313
+ data.decode() if isinstance(data, bytes) else data
314
+ )
315
+ return True
316
+ else:
317
+ if session.master_fd is not None:
318
+ os.write(session.master_fd, data)
319
+ return True
320
+ except Exception as e:
321
+ logger.error(f"Write error for session {session_id}: {e}")
322
+
323
+ return False
324
+
325
+ async def resize(self, session_id: str, cols: int, rows: int) -> bool:
326
+ """Resize a PTY session.
327
+
328
+ Args:
329
+ session_id: The session to resize
330
+ cols: New width in columns
331
+ rows: New height in rows
332
+
333
+ Returns:
334
+ bool: True if resize succeeded
335
+ """
336
+ session = self._sessions.get(session_id)
337
+ if not session:
338
+ logger.warning(f"Session {session_id} not found")
339
+ return False
340
+
341
+ try:
342
+ if IS_WINDOWS:
343
+ if session.winpty_process:
344
+ session.winpty_process.setwinsize(rows, cols)
345
+ else:
346
+ if session.master_fd is not None:
347
+ self._set_unix_winsize(session.master_fd, rows, cols)
348
+
349
+ session.cols = cols
350
+ session.rows = rows
351
+ logger.debug(f"Resized session {session_id} to {cols}x{rows}")
352
+ return True
353
+
354
+ except Exception as e:
355
+ logger.error(f"Resize error for session {session_id}: {e}")
356
+ return False
357
+
358
+ async def close_session(self, session_id: str) -> bool:
359
+ """Close a PTY session.
360
+
361
+ Args:
362
+ session_id: The session to close
363
+
364
+ Returns:
365
+ bool: True if session was closed
366
+ """
367
+ async with self._lock:
368
+ return await self._close_session_internal(session_id)
369
+
370
+ async def _close_session_internal(self, session_id: str) -> bool:
371
+ """Internal session close without lock."""
372
+ session = self._sessions.pop(session_id, None)
373
+ if not session:
374
+ return False
375
+
376
+ session._running = False
377
+
378
+ # Cancel reader task
379
+ if session._reader_task:
380
+ session._reader_task.cancel()
381
+ try:
382
+ await session._reader_task
383
+ except asyncio.CancelledError:
384
+ pass
385
+
386
+ # Clean up platform-specific resources
387
+ if IS_WINDOWS:
388
+ if session.winpty_process:
389
+ try:
390
+ session.winpty_process.terminate()
391
+ except Exception as e:
392
+ logger.debug(f"Error terminating winpty: {e}")
393
+ else:
394
+ # Close file descriptors
395
+ if session.master_fd is not None:
396
+ try:
397
+ os.close(session.master_fd)
398
+ except OSError:
399
+ pass
400
+
401
+ # Terminate child process
402
+ if session.pid is not None:
403
+ try:
404
+ os.kill(session.pid, signal.SIGTERM)
405
+ # Use WNOHANG to avoid blocking the event loop
406
+ try:
407
+ os.waitpid(session.pid, os.WNOHANG)
408
+ except ChildProcessError:
409
+ pass
410
+ except (OSError, ChildProcessError):
411
+ pass
412
+
413
+ logger.info(f"Closed PTY session: {session_id}")
414
+ return True
415
+
416
+ async def close_all(self) -> None:
417
+ """Close all PTY sessions."""
418
+ async with self._lock:
419
+ session_ids = list(self._sessions.keys())
420
+ for session_id in session_ids:
421
+ await self._close_session_internal(session_id)
422
+ logger.info("Closed all PTY sessions")
423
+
424
+ def get_session(self, session_id: str) -> Optional[PTYSession]:
425
+ """Get a session by ID.
426
+
427
+ Args:
428
+ session_id: The session ID
429
+
430
+ Returns:
431
+ PTYSession or None if not found
432
+ """
433
+ return self._sessions.get(session_id)
434
+
435
+ def list_sessions(self) -> list[str]:
436
+ """List all active session IDs.
437
+
438
+ Returns:
439
+ List of session IDs
440
+ """
441
+ return list(self._sessions.keys())
442
+
443
+
444
+ # Global PTY manager instance
445
+ _pty_manager: Optional[PTYManager] = None
446
+
447
+
448
+ def get_pty_manager() -> PTYManager:
449
+ """Get or create the global PTY manager instance."""
450
+ global _pty_manager
451
+ if _pty_manager is None:
452
+ _pty_manager = PTYManager()
453
+ return _pty_manager
@@ -0,0 +1,12 @@
1
+ """API routers for Code Puppy REST endpoints.
2
+
3
+ This package contains the FastAPI router modules for different API domains:
4
+ - config: Configuration management endpoints
5
+ - commands: Command execution endpoints
6
+ - sessions: Session management endpoints
7
+ - agents: Agent-related endpoints
8
+ """
9
+
10
+ from code_puppy.api.routers import agents, commands, config, sessions
11
+
12
+ __all__ = ["config", "commands", "sessions", "agents"]
@@ -0,0 +1,36 @@
1
+ """Agents API endpoints for agent management.
2
+
3
+ This router provides REST endpoints for:
4
+ - Listing all available agents with their metadata
5
+ """
6
+
7
+ from typing import Any, Dict, List
8
+
9
+ from fastapi import APIRouter
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ @router.get("/")
15
+ async def list_agents() -> List[Dict[str, Any]]:
16
+ """List all available agents.
17
+
18
+ Returns a list of all agents registered in the system,
19
+ including their name, display name, and description.
20
+
21
+ Returns:
22
+ List[Dict[str, Any]]: List of agent information dictionaries.
23
+ """
24
+ from code_puppy.agents import get_agent_descriptions, get_available_agents
25
+
26
+ agents_dict = get_available_agents()
27
+ descriptions = get_agent_descriptions()
28
+
29
+ return [
30
+ {
31
+ "name": name,
32
+ "display_name": display_name,
33
+ "description": descriptions.get(name, "No description"),
34
+ }
35
+ for name, display_name in agents_dict.items()
36
+ ]