code-puppy 0.0.214__py3-none-any.whl → 0.0.366__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 (231) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +2 -0
  3. code_puppy/agents/agent_c_reviewer.py +59 -6
  4. code_puppy/agents/agent_code_puppy.py +7 -1
  5. code_puppy/agents/agent_code_reviewer.py +12 -2
  6. code_puppy/agents/agent_cpp_reviewer.py +73 -6
  7. code_puppy/agents/agent_creator_agent.py +45 -4
  8. code_puppy/agents/agent_golang_reviewer.py +92 -3
  9. code_puppy/agents/agent_javascript_reviewer.py +101 -8
  10. code_puppy/agents/agent_manager.py +81 -4
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +28 -6
  15. code_puppy/agents/agent_qa_expert.py +98 -6
  16. code_puppy/agents/agent_qa_kitten.py +12 -7
  17. code_puppy/agents/agent_security_auditor.py +113 -3
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +106 -7
  20. code_puppy/agents/base_agent.py +802 -176
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/pack/__init__.py +34 -0
  23. code_puppy/agents/pack/bloodhound.py +304 -0
  24. code_puppy/agents/pack/husky.py +321 -0
  25. code_puppy/agents/pack/retriever.py +393 -0
  26. code_puppy/agents/pack/shepherd.py +348 -0
  27. code_puppy/agents/pack/terrier.py +287 -0
  28. code_puppy/agents/pack/watchdog.py +367 -0
  29. code_puppy/agents/prompt_reviewer.py +145 -0
  30. code_puppy/agents/subagent_stream_handler.py +276 -0
  31. code_puppy/api/__init__.py +13 -0
  32. code_puppy/api/app.py +169 -0
  33. code_puppy/api/main.py +21 -0
  34. code_puppy/api/pty_manager.py +446 -0
  35. code_puppy/api/routers/__init__.py +12 -0
  36. code_puppy/api/routers/agents.py +36 -0
  37. code_puppy/api/routers/commands.py +217 -0
  38. code_puppy/api/routers/config.py +74 -0
  39. code_puppy/api/routers/sessions.py +232 -0
  40. code_puppy/api/templates/terminal.html +361 -0
  41. code_puppy/api/websocket.py +154 -0
  42. code_puppy/callbacks.py +142 -4
  43. code_puppy/chatgpt_codex_client.py +283 -0
  44. code_puppy/claude_cache_client.py +586 -0
  45. code_puppy/cli_runner.py +916 -0
  46. code_puppy/command_line/add_model_menu.py +1079 -0
  47. code_puppy/command_line/agent_menu.py +395 -0
  48. code_puppy/command_line/attachments.py +10 -5
  49. code_puppy/command_line/autosave_menu.py +605 -0
  50. code_puppy/command_line/clipboard.py +527 -0
  51. code_puppy/command_line/colors_menu.py +520 -0
  52. code_puppy/command_line/command_handler.py +176 -738
  53. code_puppy/command_line/command_registry.py +150 -0
  54. code_puppy/command_line/config_commands.py +715 -0
  55. code_puppy/command_line/core_commands.py +792 -0
  56. code_puppy/command_line/diff_menu.py +863 -0
  57. code_puppy/command_line/load_context_completion.py +15 -22
  58. code_puppy/command_line/mcp/base.py +0 -3
  59. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  60. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  61. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  62. code_puppy/command_line/mcp/edit_command.py +148 -0
  63. code_puppy/command_line/mcp/handler.py +9 -4
  64. code_puppy/command_line/mcp/help_command.py +6 -5
  65. code_puppy/command_line/mcp/install_command.py +15 -26
  66. code_puppy/command_line/mcp/install_menu.py +685 -0
  67. code_puppy/command_line/mcp/list_command.py +2 -2
  68. code_puppy/command_line/mcp/logs_command.py +174 -65
  69. code_puppy/command_line/mcp/remove_command.py +2 -2
  70. code_puppy/command_line/mcp/restart_command.py +12 -4
  71. code_puppy/command_line/mcp/search_command.py +16 -10
  72. code_puppy/command_line/mcp/start_all_command.py +18 -6
  73. code_puppy/command_line/mcp/start_command.py +47 -25
  74. code_puppy/command_line/mcp/status_command.py +4 -5
  75. code_puppy/command_line/mcp/stop_all_command.py +7 -1
  76. code_puppy/command_line/mcp/stop_command.py +8 -4
  77. code_puppy/command_line/mcp/test_command.py +2 -2
  78. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  79. code_puppy/command_line/mcp_completion.py +174 -0
  80. code_puppy/command_line/model_picker_completion.py +75 -25
  81. code_puppy/command_line/model_settings_menu.py +884 -0
  82. code_puppy/command_line/motd.py +14 -8
  83. code_puppy/command_line/onboarding_slides.py +179 -0
  84. code_puppy/command_line/onboarding_wizard.py +340 -0
  85. code_puppy/command_line/pin_command_completion.py +329 -0
  86. code_puppy/command_line/prompt_toolkit_completion.py +463 -63
  87. code_puppy/command_line/session_commands.py +296 -0
  88. code_puppy/command_line/utils.py +54 -0
  89. code_puppy/config.py +898 -112
  90. code_puppy/error_logging.py +118 -0
  91. code_puppy/gemini_code_assist.py +385 -0
  92. code_puppy/gemini_model.py +602 -0
  93. code_puppy/http_utils.py +210 -148
  94. code_puppy/keymap.py +128 -0
  95. code_puppy/main.py +5 -698
  96. code_puppy/mcp_/__init__.py +17 -0
  97. code_puppy/mcp_/async_lifecycle.py +35 -4
  98. code_puppy/mcp_/blocking_startup.py +70 -43
  99. code_puppy/mcp_/captured_stdio_server.py +2 -2
  100. code_puppy/mcp_/config_wizard.py +4 -4
  101. code_puppy/mcp_/dashboard.py +15 -6
  102. code_puppy/mcp_/managed_server.py +65 -38
  103. code_puppy/mcp_/manager.py +146 -52
  104. code_puppy/mcp_/mcp_logs.py +224 -0
  105. code_puppy/mcp_/registry.py +6 -6
  106. code_puppy/mcp_/server_registry_catalog.py +24 -5
  107. code_puppy/messaging/__init__.py +199 -2
  108. code_puppy/messaging/bus.py +610 -0
  109. code_puppy/messaging/commands.py +167 -0
  110. code_puppy/messaging/markdown_patches.py +57 -0
  111. code_puppy/messaging/message_queue.py +17 -48
  112. code_puppy/messaging/messages.py +500 -0
  113. code_puppy/messaging/queue_console.py +1 -24
  114. code_puppy/messaging/renderers.py +43 -146
  115. code_puppy/messaging/rich_renderer.py +1027 -0
  116. code_puppy/messaging/spinner/__init__.py +21 -5
  117. code_puppy/messaging/spinner/console_spinner.py +86 -51
  118. code_puppy/messaging/subagent_console.py +461 -0
  119. code_puppy/model_factory.py +634 -83
  120. code_puppy/model_utils.py +167 -0
  121. code_puppy/models.json +66 -68
  122. code_puppy/models_dev_api.json +1 -0
  123. code_puppy/models_dev_parser.py +592 -0
  124. code_puppy/plugins/__init__.py +164 -10
  125. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  126. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  127. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  128. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  129. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  130. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  131. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  132. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  133. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  134. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  135. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  136. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  137. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  138. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  139. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  140. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  141. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  142. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  143. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  144. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  145. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  146. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  147. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  148. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  149. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  150. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  151. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  152. code_puppy/plugins/example_custom_command/README.md +280 -0
  153. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  154. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  155. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  156. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  157. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  158. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  159. code_puppy/plugins/oauth_puppy_html.py +228 -0
  160. code_puppy/plugins/shell_safety/__init__.py +6 -0
  161. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  162. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  163. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  164. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  165. code_puppy/prompts/codex_system_prompt.md +310 -0
  166. code_puppy/pydantic_patches.py +131 -0
  167. code_puppy/reopenable_async_client.py +8 -8
  168. code_puppy/round_robin_model.py +9 -12
  169. code_puppy/session_storage.py +2 -1
  170. code_puppy/status_display.py +21 -4
  171. code_puppy/summarization_agent.py +41 -13
  172. code_puppy/terminal_utils.py +418 -0
  173. code_puppy/tools/__init__.py +37 -1
  174. code_puppy/tools/agent_tools.py +536 -52
  175. code_puppy/tools/browser/__init__.py +37 -0
  176. code_puppy/tools/browser/browser_control.py +19 -23
  177. code_puppy/tools/browser/browser_interactions.py +41 -48
  178. code_puppy/tools/browser/browser_locators.py +36 -38
  179. code_puppy/tools/browser/browser_manager.py +316 -0
  180. code_puppy/tools/browser/browser_navigation.py +16 -16
  181. code_puppy/tools/browser/browser_screenshot.py +79 -143
  182. code_puppy/tools/browser/browser_scripts.py +32 -42
  183. code_puppy/tools/browser/browser_workflows.py +44 -27
  184. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  185. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  186. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  187. code_puppy/tools/browser/terminal_tools.py +525 -0
  188. code_puppy/tools/command_runner.py +930 -147
  189. code_puppy/tools/common.py +1113 -5
  190. code_puppy/tools/display.py +84 -0
  191. code_puppy/tools/file_modifications.py +288 -89
  192. code_puppy/tools/file_operations.py +226 -154
  193. code_puppy/tools/subagent_context.py +158 -0
  194. code_puppy/uvx_detection.py +242 -0
  195. code_puppy/version_checker.py +30 -11
  196. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  197. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  198. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
  199. code_puppy-0.0.366.dist-info/RECORD +217 -0
  200. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  201. code_puppy/command_line/mcp/add_command.py +0 -183
  202. code_puppy/messaging/spinner/textual_spinner.py +0 -106
  203. code_puppy/tools/browser/camoufox_manager.py +0 -216
  204. code_puppy/tools/browser/vqa_agent.py +0 -70
  205. code_puppy/tui/__init__.py +0 -10
  206. code_puppy/tui/app.py +0 -1105
  207. code_puppy/tui/components/__init__.py +0 -21
  208. code_puppy/tui/components/chat_view.py +0 -551
  209. code_puppy/tui/components/command_history_modal.py +0 -218
  210. code_puppy/tui/components/copy_button.py +0 -139
  211. code_puppy/tui/components/custom_widgets.py +0 -63
  212. code_puppy/tui/components/human_input_modal.py +0 -175
  213. code_puppy/tui/components/input_area.py +0 -167
  214. code_puppy/tui/components/sidebar.py +0 -309
  215. code_puppy/tui/components/status_bar.py +0 -185
  216. code_puppy/tui/messages.py +0 -27
  217. code_puppy/tui/models/__init__.py +0 -8
  218. code_puppy/tui/models/chat_message.py +0 -25
  219. code_puppy/tui/models/command_history.py +0 -89
  220. code_puppy/tui/models/enums.py +0 -24
  221. code_puppy/tui/screens/__init__.py +0 -17
  222. code_puppy/tui/screens/autosave_picker.py +0 -175
  223. code_puppy/tui/screens/help.py +0 -130
  224. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  225. code_puppy/tui/screens/settings.py +0 -306
  226. code_puppy/tui/screens/tools.py +0 -74
  227. code_puppy/tui_state.py +0 -55
  228. code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
  229. code_puppy-0.0.214.dist-info/RECORD +0 -131
  230. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
  231. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -1,183 +0,0 @@
1
- """
2
- MCP Add Command - Adds new MCP servers from JSON configuration or wizard.
3
- """
4
-
5
- import json
6
- import logging
7
- import os
8
- from typing import List, Optional
9
-
10
- from code_puppy.messaging import emit_info
11
- from code_puppy.tui_state import is_tui_mode
12
-
13
- from .base import MCPCommandBase
14
- from .wizard_utils import run_interactive_install_wizard
15
-
16
- # Configure logging
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- class AddCommand(MCPCommandBase):
21
- """
22
- Command handler for adding MCP servers.
23
-
24
- Adds new MCP servers from JSON configuration or interactive wizard.
25
- """
26
-
27
- def execute(self, args: List[str], group_id: Optional[str] = None) -> None:
28
- """
29
- Add a new MCP server from JSON configuration or launch wizard.
30
-
31
- Usage:
32
- /mcp add - Launch interactive wizard
33
- /mcp add <json> - Add server from JSON config
34
-
35
- Example JSON:
36
- /mcp add {"name": "test", "type": "stdio", "command": "echo", "args": ["hello"]}
37
-
38
- Args:
39
- args: Command arguments - JSON config or empty for wizard
40
- group_id: Optional message group ID for grouping related messages
41
- """
42
- if group_id is None:
43
- group_id = self.generate_group_id()
44
-
45
- # Check if in TUI mode and guide user to use Ctrl+T instead
46
- if is_tui_mode() and not args:
47
- emit_info(
48
- "💡 In TUI mode, press Ctrl+T to open the MCP Install Wizard",
49
- message_group=group_id,
50
- )
51
- emit_info(
52
- " The wizard provides a better interface for browsing and installing MCP servers.",
53
- message_group=group_id,
54
- )
55
- return
56
-
57
- try:
58
- if args:
59
- # Parse JSON from arguments
60
- json_str = " ".join(args)
61
-
62
- try:
63
- config_dict = json.loads(json_str)
64
- except json.JSONDecodeError as e:
65
- emit_info(f"Invalid JSON: {e}", message_group=group_id)
66
- emit_info(
67
- "Usage: /mcp add <json> or /mcp add (for wizard)",
68
- message_group=group_id,
69
- )
70
- emit_info(
71
- 'Example: /mcp add {"name": "test", "type": "stdio", "command": "echo"}',
72
- message_group=group_id,
73
- )
74
- return
75
-
76
- # Validate required fields
77
- if "name" not in config_dict:
78
- emit_info("Missing required field: 'name'", message_group=group_id)
79
- return
80
- if "type" not in config_dict:
81
- emit_info("Missing required field: 'type'", message_group=group_id)
82
- return
83
-
84
- # Add the server
85
- success = self._add_server_from_json(config_dict, group_id)
86
-
87
- if success:
88
- # Reload MCP servers
89
- try:
90
- from code_puppy.agent import reload_mcp_servers
91
-
92
- reload_mcp_servers()
93
- except ImportError:
94
- pass
95
-
96
- emit_info(
97
- "Use '/mcp list' to see all servers", message_group=group_id
98
- )
99
-
100
- else:
101
- # No arguments - launch interactive wizard with server templates
102
- success = run_interactive_install_wizard(self.manager, group_id)
103
-
104
- if success:
105
- # Reload the agent to pick up new server
106
- try:
107
- from code_puppy.agent import reload_mcp_servers
108
-
109
- reload_mcp_servers()
110
- except ImportError:
111
- pass
112
-
113
- except ImportError as e:
114
- logger.error(f"Failed to import: {e}")
115
- emit_info("Required module not available", message_group=group_id)
116
- except Exception as e:
117
- logger.error(f"Error in add command: {e}")
118
- emit_info(f"[red]Error adding server: {e}[/red]", message_group=group_id)
119
-
120
- def _add_server_from_json(self, config_dict: dict, group_id: str) -> bool:
121
- """
122
- Add a server from JSON configuration.
123
-
124
- Args:
125
- config_dict: Server configuration dictionary
126
- group_id: Message group ID
127
-
128
- Returns:
129
- True if successful, False otherwise
130
- """
131
- try:
132
- from code_puppy.config import MCP_SERVERS_FILE
133
- from code_puppy.mcp_.managed_server import ServerConfig
134
-
135
- # Extract required fields
136
- name = config_dict.pop("name")
137
- server_type = config_dict.pop("type")
138
- enabled = config_dict.pop("enabled", True)
139
-
140
- # Everything else goes into config
141
- server_config = ServerConfig(
142
- id=f"{name}_{hash(name)}",
143
- name=name,
144
- type=server_type,
145
- enabled=enabled,
146
- config=config_dict, # Remaining fields are server-specific config
147
- )
148
-
149
- # Register the server
150
- server_id = self.manager.register_server(server_config)
151
-
152
- if not server_id:
153
- emit_info(f"Failed to add server '{name}'", message_group=group_id)
154
- return False
155
-
156
- emit_info(
157
- f"✅ Added server '{name}' (ID: {server_id})", message_group=group_id
158
- )
159
-
160
- # Save to mcp_servers.json for persistence
161
- if os.path.exists(MCP_SERVERS_FILE):
162
- with open(MCP_SERVERS_FILE, "r") as f:
163
- data = json.load(f)
164
- servers = data.get("mcp_servers", {})
165
- else:
166
- servers = {}
167
- data = {"mcp_servers": servers}
168
-
169
- # Add new server
170
- servers[name] = config_dict.copy()
171
- servers[name]["type"] = server_type
172
-
173
- # Save back
174
- os.makedirs(os.path.dirname(MCP_SERVERS_FILE), exist_ok=True)
175
- with open(MCP_SERVERS_FILE, "w") as f:
176
- json.dump(data, f, indent=2)
177
-
178
- return True
179
-
180
- except Exception as e:
181
- logger.error(f"Error adding server from JSON: {e}")
182
- emit_info(f"[red]Failed to add server: {e}[/red]", message_group=group_id)
183
- return False
@@ -1,106 +0,0 @@
1
- """
2
- Textual spinner implementation for TUI mode.
3
- """
4
-
5
- from textual.widgets import Static
6
-
7
- from .spinner_base import SpinnerBase
8
-
9
-
10
- class TextualSpinner(Static):
11
- """A textual spinner widget based on the SimpleSpinnerWidget."""
12
-
13
- # Use the frames from SpinnerBase
14
- FRAMES = SpinnerBase.FRAMES
15
-
16
- def __init__(self, **kwargs):
17
- """Initialize the textual spinner."""
18
- super().__init__("", **kwargs)
19
- self._frame_index = 0
20
- self._is_spinning = False
21
- self._timer = None
22
- self._paused = False
23
- self._previous_state = ""
24
-
25
- # Register this spinner for global management
26
- from . import register_spinner
27
-
28
- register_spinner(self)
29
-
30
- def start_spinning(self):
31
- """Start the spinner animation using Textual's timer system."""
32
- if not self._is_spinning:
33
- self._is_spinning = True
34
- self._frame_index = 0
35
- self.update_frame_display()
36
- # Start the animation timer using Textual's timer system
37
- self._timer = self.set_interval(0.10, self.update_frame_display)
38
-
39
- def stop_spinning(self):
40
- """Stop the spinner animation."""
41
- self._is_spinning = False
42
- if self._timer:
43
- self._timer.stop()
44
- self._timer = None
45
- self.update("")
46
-
47
- # Unregister this spinner from global management
48
- from . import unregister_spinner
49
-
50
- unregister_spinner(self)
51
-
52
- def update_frame(self):
53
- """Update to the next frame."""
54
- if self._is_spinning:
55
- self._frame_index = (self._frame_index + 1) % len(self.FRAMES)
56
-
57
- def update_frame_display(self):
58
- """Update the display with the current frame."""
59
- if self._is_spinning:
60
- self.update_frame()
61
- current_frame = self.FRAMES[self._frame_index]
62
-
63
- # Check if we're awaiting user input to determine which message to show
64
- from code_puppy.tools.command_runner import is_awaiting_user_input
65
-
66
- if is_awaiting_user_input():
67
- # Show waiting message when waiting for user input
68
- message = SpinnerBase.WAITING_MESSAGE
69
- else:
70
- # Show thinking message during normal processing
71
- message = SpinnerBase.THINKING_MESSAGE
72
-
73
- context_info = SpinnerBase.get_context_info()
74
- context_segment = (
75
- f" [bold white]{context_info}[/bold white]" if context_info else ""
76
- )
77
-
78
- self.update(
79
- f"[bold cyan]{message}[/bold cyan][bold cyan]{current_frame}[/bold cyan]{context_segment}"
80
- )
81
-
82
- def pause(self):
83
- """Pause the spinner animation temporarily."""
84
- if self._is_spinning and self._timer and not self._paused:
85
- self._paused = True
86
- self._timer.pause()
87
- # Store current state but don't clear it completely
88
- self._previous_state = self.renderable
89
- self.update("")
90
-
91
- def resume(self):
92
- """Resume a paused spinner animation."""
93
- # Check if we should show a spinner - don't resume if waiting for user input
94
- from code_puppy.tools.command_runner import is_awaiting_user_input
95
-
96
- if is_awaiting_user_input():
97
- return # Don't resume if waiting for user input
98
-
99
- if self._is_spinning and self._timer and self._paused:
100
- self._paused = False
101
- self._timer.resume()
102
- # Restore previous state instead of immediately updating display
103
- if self._previous_state:
104
- self.update(self._previous_state)
105
- else:
106
- self.update_frame_display()
@@ -1,216 +0,0 @@
1
- """Camoufox browser manager - privacy-focused Firefox automation."""
2
-
3
- from pathlib import Path
4
- from typing import Optional
5
-
6
- import camoufox
7
- from camoufox.addons import DefaultAddons
8
- from camoufox.exceptions import CamoufoxNotInstalled, UnsupportedVersion
9
- from camoufox.locale import ALLOW_GEOIP, download_mmdb
10
- from camoufox.pkgman import CamoufoxFetcher, camoufox_path
11
- from playwright.async_api import Browser, BrowserContext, Page
12
-
13
-
14
- from code_puppy.messaging import emit_info
15
-
16
-
17
- class CamoufoxManager:
18
- """Singleton browser manager for Camoufox (privacy-focused Firefox) automation."""
19
-
20
- _instance: Optional["CamoufoxManager"] = None
21
- _browser: Optional[Browser] = None
22
- _context: Optional[BrowserContext] = None
23
- _initialized: bool = False
24
-
25
- def __new__(cls):
26
- if cls._instance is None:
27
- cls._instance = super().__new__(cls)
28
- return cls._instance
29
-
30
- def __init__(self):
31
- # Only initialize once
32
- if hasattr(self, "_init_done"):
33
- return
34
- self._init_done = True
35
-
36
- self.headless = False
37
- self.homepage = "https://www.google.com"
38
- # Camoufox-specific settings
39
- self.geoip = True # Enable GeoIP spoofing
40
- self.block_webrtc = True # Block WebRTC for privacy
41
- self.humanize = True # Add human-like behavior
42
-
43
- # Persistent profile directory for consistent browser state across runs
44
- self.profile_dir = self._get_profile_directory()
45
-
46
- @classmethod
47
- def get_instance(cls) -> "CamoufoxManager":
48
- """Get the singleton instance."""
49
- if cls._instance is None:
50
- cls._instance = cls()
51
- return cls._instance
52
-
53
- def _get_profile_directory(self) -> Path:
54
- """Get or create the persistent profile directory.
55
-
56
- Returns a Path object pointing to ~/.code_puppy/camoufox_profile
57
- where browser data (cookies, history, bookmarks, etc.) will be stored.
58
- """
59
- profile_path = Path.home() / ".code_puppy" / "camoufox_profile"
60
- profile_path.mkdir(parents=True, exist_ok=True)
61
- return profile_path
62
-
63
- async def async_initialize(self) -> None:
64
- """Initialize Camoufox browser."""
65
- if self._initialized:
66
- return
67
-
68
- try:
69
- emit_info("[yellow]Initializing Camoufox (privacy Firefox)...[/yellow]")
70
-
71
- # Ensure Camoufox binary and dependencies are fetched before launching
72
- await self._prefetch_camoufox()
73
-
74
- await self._initialize_camoufox()
75
- emit_info(
76
- "[green]✅ Camoufox initialized successfully (privacy-focused Firefox)[/green]"
77
- )
78
- self._initialized = True
79
-
80
- except Exception:
81
- await self._cleanup()
82
- raise
83
-
84
- async def _initialize_camoufox(self) -> None:
85
- """Try to start Camoufox with the configured privacy settings."""
86
- emit_info(f"[cyan]📁 Using persistent profile: {self.profile_dir}[/cyan]")
87
-
88
- camoufox_instance = camoufox.AsyncCamoufox(
89
- headless=self.headless,
90
- block_webrtc=self.block_webrtc,
91
- humanize=self.humanize,
92
- exclude_addons=list(DefaultAddons),
93
- persistent_context=True,
94
- user_data_dir=str(self.profile_dir),
95
- addons=[],
96
- )
97
-
98
- self._browser = camoufox_instance.browser
99
- # Use persistent storage directory for browser context
100
- # This ensures cookies, localStorage, history, etc. persist across runs
101
- if not self._initialized:
102
- self._context = await camoufox_instance.start()
103
- self._initialized = True
104
- # Do not auto-open a page here to avoid duplicate windows/tabs.
105
-
106
- async def get_current_page(self) -> Optional[Page]:
107
- """Get the currently active page. Lazily creates one if none exist."""
108
- if not self._initialized or not self._context:
109
- await self.async_initialize()
110
-
111
- if not self._context:
112
- return None
113
-
114
- pages = self._context.pages
115
- if pages:
116
- return pages[0]
117
-
118
- # Lazily create a new blank page without navigation
119
- return await self._context.new_page()
120
-
121
- async def new_page(self, url: Optional[str] = None) -> Page:
122
- """Create a new page and optionally navigate to URL."""
123
- if not self._initialized:
124
- await self.async_initialize()
125
-
126
- page = await self._context.new_page()
127
- if url:
128
- await page.goto(url)
129
- return page
130
-
131
- async def _prefetch_camoufox(self) -> None:
132
- """Prefetch Camoufox binary and dependencies."""
133
- emit_info(
134
- "[cyan]🔍 Ensuring Camoufox binary and dependencies are up-to-date...[/cyan]"
135
- )
136
-
137
- needs_install = False
138
- try:
139
- camoufox_path(download_if_missing=False)
140
- emit_info("[cyan]🗃️ Using cached Camoufox installation[/cyan]")
141
- except (CamoufoxNotInstalled, FileNotFoundError):
142
- emit_info("[cyan]📥 Camoufox not found, installing fresh copy[/cyan]")
143
- needs_install = True
144
- except UnsupportedVersion:
145
- emit_info("[cyan]♻️ Camoufox update required, reinstalling[/cyan]")
146
- needs_install = True
147
-
148
- if needs_install:
149
- CamoufoxFetcher().install()
150
-
151
- # Fetch GeoIP database if enabled
152
- if ALLOW_GEOIP:
153
- download_mmdb()
154
-
155
- emit_info("[cyan]📦 Camoufox dependencies ready[/cyan]")
156
-
157
- async def close_page(self, page: Page) -> None:
158
- """Close a specific page."""
159
- await page.close()
160
-
161
- async def get_all_pages(self) -> list[Page]:
162
- """Get all open pages."""
163
- if not self._context:
164
- return []
165
- return self._context.pages
166
-
167
- async def _cleanup(self) -> None:
168
- """Clean up browser resources and save persistent state."""
169
- try:
170
- # Save browser state before closing (cookies, localStorage, etc.)
171
- if self._context:
172
- try:
173
- storage_state_path = self.profile_dir / "storage_state.json"
174
- await self._context.storage_state(path=str(storage_state_path))
175
- emit_info(
176
- f"[green]💾 Browser state saved to {storage_state_path}[/green]"
177
- )
178
- except Exception as e:
179
- emit_info(
180
- f"[yellow]Warning: Could not save storage state: {e}[/yellow]"
181
- )
182
-
183
- await self._context.close()
184
- self._context = None
185
- if self._browser:
186
- await self._browser.close()
187
- self._browser = None
188
- self._initialized = False
189
- except Exception as e:
190
- emit_info(f"[yellow]Warning during cleanup: {e}[/yellow]")
191
-
192
- async def close(self) -> None:
193
- """Close the browser and clean up resources."""
194
- await self._cleanup()
195
- emit_info("[yellow]Camoufox browser closed[/yellow]")
196
-
197
- def __del__(self):
198
- """Ensure cleanup on object destruction."""
199
- # Note: Can't use async in __del__, so this is just a fallback
200
- if self._initialized:
201
- import asyncio
202
-
203
- try:
204
- loop = asyncio.get_event_loop()
205
- if loop.is_running():
206
- loop.create_task(self._cleanup())
207
- else:
208
- loop.run_until_complete(self._cleanup())
209
- except Exception:
210
- pass # Best effort cleanup
211
-
212
-
213
- # Convenience function for getting the singleton instance
214
- def get_camoufox_manager() -> CamoufoxManager:
215
- """Get the singleton CamoufoxManager instance."""
216
- return CamoufoxManager.get_instance()
@@ -1,70 +0,0 @@
1
- """Utilities for running visual question-answering via pydantic-ai."""
2
-
3
- from __future__ import annotations
4
-
5
- from functools import lru_cache
6
-
7
- from pydantic import BaseModel, Field
8
- from pydantic_ai import Agent, BinaryContent
9
-
10
- from code_puppy.config import get_use_dbos, get_vqa_model_name
11
- from code_puppy.model_factory import ModelFactory
12
-
13
-
14
- class VisualAnalysisResult(BaseModel):
15
- """Structured response from the VQA agent."""
16
-
17
- answer: str
18
- confidence: float = Field(ge=0.0, le=1.0)
19
- observations: str
20
-
21
-
22
- @lru_cache(maxsize=1)
23
- def _load_vqa_agent(model_name: str) -> Agent[None, VisualAnalysisResult]:
24
- """Create a cached agent instance for visual analysis."""
25
- models_config = ModelFactory.load_config()
26
- model = ModelFactory.get_model(model_name, models_config)
27
-
28
- instructions = (
29
- "You are a visual analysis specialist. Answer the user's question about the provided image. "
30
- "Always respond using the structured schema: answer, confidence (0-1 float), observations. "
31
- "Confidence reflects how certain you are about the answer. Observations should include useful, concise context."
32
- )
33
-
34
- vqa_agent = Agent(
35
- model=model,
36
- instructions=instructions,
37
- output_type=VisualAnalysisResult,
38
- retries=2,
39
- )
40
-
41
- if get_use_dbos():
42
- from pydantic_ai.durable_exec.dbos import DBOSAgent
43
-
44
- dbos_agent = DBOSAgent(vqa_agent, name="vqa-agent")
45
- return dbos_agent
46
-
47
- return vqa_agent
48
-
49
-
50
- def _get_vqa_agent() -> Agent[None, VisualAnalysisResult]:
51
- """Return a cached VQA agent configured with the current model."""
52
- model_name = get_vqa_model_name()
53
- # lru_cache keyed by model_name ensures refresh when configuration changes
54
- return _load_vqa_agent(model_name)
55
-
56
-
57
- def run_vqa_analysis(
58
- question: str,
59
- image_bytes: bytes,
60
- media_type: str = "image/png",
61
- ) -> VisualAnalysisResult:
62
- """Execute the VQA agent synchronously against screenshot bytes."""
63
- agent = _get_vqa_agent()
64
- result = agent.run_sync(
65
- [
66
- question,
67
- BinaryContent(data=image_bytes, media_type=media_type),
68
- ]
69
- )
70
- return result.output
@@ -1,10 +0,0 @@
1
- """
2
- Code Puppy TUI package.
3
-
4
- This package provides a modern Text User Interface for Code Puppy using the Textual framework.
5
- It maintains compatibility with existing functionality while providing an enhanced user experience.
6
- """
7
-
8
- from .app import CodePuppyTUI, run_textual_ui
9
-
10
- __all__ = ["CodePuppyTUI", "run_textual_ui"]