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.
- code_puppy/__init__.py +7 -1
- code_puppy/agents/__init__.py +2 -0
- code_puppy/agents/agent_c_reviewer.py +59 -6
- code_puppy/agents/agent_code_puppy.py +7 -1
- code_puppy/agents/agent_code_reviewer.py +12 -2
- code_puppy/agents/agent_cpp_reviewer.py +73 -6
- code_puppy/agents/agent_creator_agent.py +45 -4
- code_puppy/agents/agent_golang_reviewer.py +92 -3
- code_puppy/agents/agent_javascript_reviewer.py +101 -8
- code_puppy/agents/agent_manager.py +81 -4
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +28 -6
- code_puppy/agents/agent_qa_expert.py +98 -6
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_security_auditor.py +113 -3
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +106 -7
- code_puppy/agents/base_agent.py +802 -176
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +142 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +176 -738
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +0 -3
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +15 -26
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +2 -2
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +18 -6
- code_puppy/command_line/mcp/start_command.py +47 -25
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +7 -1
- code_puppy/command_line/mcp/stop_command.py +8 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +75 -25
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +463 -63
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +898 -112
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +210 -148
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -698
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/blocking_startup.py +70 -43
- code_puppy/mcp_/captured_stdio_server.py +2 -2
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +65 -38
- code_puppy/mcp_/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/mcp_/server_registry_catalog.py +24 -5
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +21 -5
- code_puppy/messaging/spinner/console_spinner.py +86 -51
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +634 -83
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +66 -68
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +9 -12
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +41 -13
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +536 -52
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +19 -23
- code_puppy/tools/browser/browser_interactions.py +41 -48
- code_puppy/tools/browser/browser_locators.py +36 -38
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +16 -16
- code_puppy/tools/browser/browser_screenshot.py +79 -143
- code_puppy/tools/browser/browser_scripts.py +32 -42
- code_puppy/tools/browser/browser_workflows.py +44 -27
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +930 -147
- code_puppy/tools/common.py +1113 -5
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +226 -154
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/messaging/spinner/textual_spinner.py +0 -106
- code_puppy/tools/browser/camoufox_manager.py +0 -216
- code_puppy/tools/browser/vqa_agent.py +0 -70
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -1105
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -551
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -185
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -17
- code_puppy/tui/screens/autosave_picker.py +0 -175
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -306
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
- code_puppy-0.0.214.dist-info/RECORD +0 -131
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
- {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
|
code_puppy/tui/__init__.py
DELETED
|
@@ -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"]
|