code-puppy 0.0.348__py3-none-any.whl → 0.0.372__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 (87) hide show
  1. code_puppy/agents/__init__.py +8 -0
  2. code_puppy/agents/agent_manager.py +272 -1
  3. code_puppy/agents/agent_pack_leader.py +383 -0
  4. code_puppy/agents/agent_qa_kitten.py +12 -7
  5. code_puppy/agents/agent_terminal_qa.py +323 -0
  6. code_puppy/agents/base_agent.py +11 -8
  7. code_puppy/agents/event_stream_handler.py +101 -8
  8. code_puppy/agents/pack/__init__.py +34 -0
  9. code_puppy/agents/pack/bloodhound.py +304 -0
  10. code_puppy/agents/pack/husky.py +321 -0
  11. code_puppy/agents/pack/retriever.py +393 -0
  12. code_puppy/agents/pack/shepherd.py +348 -0
  13. code_puppy/agents/pack/terrier.py +287 -0
  14. code_puppy/agents/pack/watchdog.py +367 -0
  15. code_puppy/agents/subagent_stream_handler.py +276 -0
  16. code_puppy/api/__init__.py +13 -0
  17. code_puppy/api/app.py +169 -0
  18. code_puppy/api/main.py +21 -0
  19. code_puppy/api/pty_manager.py +446 -0
  20. code_puppy/api/routers/__init__.py +12 -0
  21. code_puppy/api/routers/agents.py +36 -0
  22. code_puppy/api/routers/commands.py +217 -0
  23. code_puppy/api/routers/config.py +74 -0
  24. code_puppy/api/routers/sessions.py +232 -0
  25. code_puppy/api/templates/terminal.html +361 -0
  26. code_puppy/api/websocket.py +154 -0
  27. code_puppy/callbacks.py +73 -0
  28. code_puppy/chatgpt_codex_client.py +53 -0
  29. code_puppy/claude_cache_client.py +294 -41
  30. code_puppy/command_line/add_model_menu.py +13 -4
  31. code_puppy/command_line/agent_menu.py +662 -0
  32. code_puppy/command_line/core_commands.py +89 -112
  33. code_puppy/command_line/model_picker_completion.py +3 -20
  34. code_puppy/command_line/model_settings_menu.py +21 -3
  35. code_puppy/config.py +145 -70
  36. code_puppy/gemini_model.py +706 -0
  37. code_puppy/http_utils.py +6 -3
  38. code_puppy/messaging/__init__.py +15 -0
  39. code_puppy/messaging/messages.py +27 -0
  40. code_puppy/messaging/queue_console.py +1 -1
  41. code_puppy/messaging/rich_renderer.py +36 -1
  42. code_puppy/messaging/spinner/__init__.py +20 -2
  43. code_puppy/messaging/subagent_console.py +461 -0
  44. code_puppy/model_factory.py +50 -16
  45. code_puppy/model_switching.py +63 -0
  46. code_puppy/model_utils.py +27 -24
  47. code_puppy/models.json +12 -12
  48. code_puppy/plugins/antigravity_oauth/antigravity_model.py +206 -172
  49. code_puppy/plugins/antigravity_oauth/register_callbacks.py +15 -8
  50. code_puppy/plugins/antigravity_oauth/transport.py +236 -45
  51. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -2
  52. code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -30
  53. code_puppy/plugins/claude_code_oauth/utils.py +4 -1
  54. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  55. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  56. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  57. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  58. code_puppy/pydantic_patches.py +52 -0
  59. code_puppy/status_display.py +6 -2
  60. code_puppy/tools/__init__.py +37 -1
  61. code_puppy/tools/agent_tools.py +83 -33
  62. code_puppy/tools/browser/__init__.py +37 -0
  63. code_puppy/tools/browser/browser_control.py +6 -6
  64. code_puppy/tools/browser/browser_interactions.py +21 -20
  65. code_puppy/tools/browser/browser_locators.py +9 -9
  66. code_puppy/tools/browser/browser_manager.py +316 -0
  67. code_puppy/tools/browser/browser_navigation.py +7 -7
  68. code_puppy/tools/browser/browser_screenshot.py +78 -140
  69. code_puppy/tools/browser/browser_scripts.py +15 -13
  70. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  71. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  72. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  73. code_puppy/tools/browser/terminal_tools.py +525 -0
  74. code_puppy/tools/command_runner.py +292 -101
  75. code_puppy/tools/common.py +176 -1
  76. code_puppy/tools/display.py +84 -0
  77. code_puppy/tools/subagent_context.py +158 -0
  78. {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models.json +12 -12
  79. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/METADATA +17 -16
  80. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/RECORD +84 -51
  81. code_puppy/prompts/codex_system_prompt.md +0 -310
  82. code_puppy/tools/browser/camoufox_manager.py +0 -235
  83. code_puppy/tools/browser/vqa_agent.py +0 -90
  84. {code_puppy-0.0.348.data → code_puppy-0.0.372.data}/data/code_puppy/models_dev_api.json +0 -0
  85. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/WHEEL +0 -0
  86. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/entry_points.txt +0 -0
  87. {code_puppy-0.0.348.dist-info → code_puppy-0.0.372.dist-info}/licenses/LICENSE +0 -0
@@ -1,235 +0,0 @@
1
- """Camoufox browser manager - privacy-focused Firefox automation."""
2
-
3
- import os
4
- from pathlib import Path
5
- from typing import Optional
6
-
7
- from playwright.async_api import Browser, BrowserContext, Page
8
-
9
- from code_puppy import config
10
- from code_puppy.messaging import emit_info, emit_success, emit_warning
11
-
12
-
13
- class CamoufoxManager:
14
- """Singleton browser manager for Camoufox (privacy-focused Firefox) automation."""
15
-
16
- _instance: Optional["CamoufoxManager"] = None
17
- _browser: Optional[Browser] = None
18
- _context: Optional[BrowserContext] = None
19
- _initialized: bool = False
20
-
21
- def __new__(cls):
22
- if cls._instance is None:
23
- cls._instance = super().__new__(cls)
24
- return cls._instance
25
-
26
- def __init__(self):
27
- # Only initialize once
28
- if hasattr(self, "_init_done"):
29
- return
30
- self._init_done = True
31
-
32
- # Default to headless=True (no browser spam during tests)
33
- # Override with BROWSER_HEADLESS=false to see the browser
34
- self.headless = os.getenv("BROWSER_HEADLESS", "true").lower() != "false"
35
- self.homepage = "https://www.google.com"
36
- # Camoufox-specific settings
37
- self.geoip = True # Enable GeoIP spoofing
38
- self.block_webrtc = True # Block WebRTC for privacy
39
- self.humanize = True # Add human-like behavior
40
-
41
- # Persistent profile directory for consistent browser state across runs
42
- self.profile_dir = self._get_profile_directory()
43
-
44
- @classmethod
45
- def get_instance(cls) -> "CamoufoxManager":
46
- """Get the singleton instance."""
47
- if cls._instance is None:
48
- cls._instance = cls()
49
- return cls._instance
50
-
51
- def _get_profile_directory(self) -> Path:
52
- """Get or create the persistent profile directory (uses XDG_CACHE_HOME).
53
-
54
- Returns a Path object pointing to XDG_CACHE_HOME/code_puppy/camoufox_profile
55
- where browser data (cookies, history, bookmarks, etc.) will be stored.
56
- """
57
- cache_dir = Path(config.CACHE_DIR)
58
- profile_path = cache_dir / "camoufox_profile"
59
- profile_path.mkdir(parents=True, exist_ok=True, mode=0o700)
60
- return profile_path
61
-
62
- async def async_initialize(self) -> None:
63
- """Initialize Camoufox browser."""
64
- if self._initialized:
65
- return
66
-
67
- try:
68
- emit_info("Initializing Camoufox (privacy Firefox)...")
69
-
70
- # Ensure Camoufox binary and dependencies are fetched before launching
71
- await self._prefetch_camoufox()
72
-
73
- await self._initialize_camoufox()
74
- # emit_info(
75
- # "[green]✅ Camoufox initialized successfully (privacy-focused Firefox)[/green]"
76
- # ) # Removed to reduce console spam
77
- self._initialized = True
78
-
79
- except Exception:
80
- await self._cleanup()
81
- raise
82
-
83
- async def _initialize_camoufox(self) -> None:
84
- """Try to start Camoufox with the configured privacy settings."""
85
- emit_info(f"Using persistent profile: {self.profile_dir}")
86
- # Lazy import camoufox to avoid triggering heavy optional deps at import time
87
- try:
88
- import camoufox
89
- from camoufox.addons import DefaultAddons
90
-
91
- camoufox_instance = camoufox.AsyncCamoufox(
92
- headless=self.headless,
93
- block_webrtc=self.block_webrtc,
94
- humanize=self.humanize,
95
- exclude_addons=list(DefaultAddons),
96
- persistent_context=True,
97
- user_data_dir=str(self.profile_dir),
98
- addons=[],
99
- )
100
-
101
- self._browser = camoufox_instance.browser
102
- if not self._initialized:
103
- self._context = await camoufox_instance.start()
104
- self._initialized = True
105
- except Exception:
106
- from playwright.async_api import async_playwright
107
-
108
- emit_warning(
109
- "Camoufox no disponible. Usando Playwright (Chromium) como alternativa."
110
- )
111
- pw = await async_playwright().start()
112
- # Use persistent context directory for Chromium to emulate previous behavior
113
- context = await pw.chromium.launch_persistent_context(
114
- user_data_dir=str(self.profile_dir), headless=self.headless
115
- )
116
- self._context = context
117
- self._browser = context.browser
118
- self._initialized = True
119
-
120
- async def get_current_page(self) -> Optional[Page]:
121
- """Get the currently active page. Lazily creates one if none exist."""
122
- if not self._initialized or not self._context:
123
- await self.async_initialize()
124
-
125
- if not self._context:
126
- return None
127
-
128
- pages = self._context.pages
129
- if pages:
130
- return pages[0]
131
-
132
- # Lazily create a new blank page without navigation
133
- return await self._context.new_page()
134
-
135
- async def new_page(self, url: Optional[str] = None) -> Page:
136
- """Create a new page and optionally navigate to URL."""
137
- if not self._initialized:
138
- await self.async_initialize()
139
-
140
- page = await self._context.new_page()
141
- if url:
142
- await page.goto(url)
143
- return page
144
-
145
- async def _prefetch_camoufox(self) -> None:
146
- """Prefetch Camoufox binary and dependencies."""
147
- emit_info("Ensuring Camoufox binary and dependencies are up-to-date...")
148
-
149
- # Lazy import camoufox utilities to avoid side effects during module import
150
- try:
151
- from camoufox.exceptions import CamoufoxNotInstalled, UnsupportedVersion
152
- from camoufox.locale import ALLOW_GEOIP, download_mmdb
153
- from camoufox.pkgman import CamoufoxFetcher, camoufox_path
154
- except Exception:
155
- emit_warning(
156
- "Camoufox no disponible. Omitiendo prefetch y preparándose para usar Playwright."
157
- )
158
- return
159
-
160
- needs_install = False
161
- try:
162
- camoufox_path(download_if_missing=False)
163
- emit_info("Using cached Camoufox installation")
164
- except (CamoufoxNotInstalled, FileNotFoundError):
165
- emit_info("Camoufox not found, installing fresh copy")
166
- needs_install = True
167
- except UnsupportedVersion:
168
- emit_info("Camoufox update required, reinstalling")
169
- needs_install = True
170
-
171
- if needs_install:
172
- CamoufoxFetcher().install()
173
-
174
- # Fetch GeoIP database if enabled
175
- if ALLOW_GEOIP:
176
- download_mmdb()
177
-
178
- emit_info("Camoufox dependencies ready")
179
-
180
- async def close_page(self, page: Page) -> None:
181
- """Close a specific page."""
182
- await page.close()
183
-
184
- async def get_all_pages(self) -> list[Page]:
185
- """Get all open pages."""
186
- if not self._context:
187
- return []
188
- return self._context.pages
189
-
190
- async def _cleanup(self) -> None:
191
- """Clean up browser resources and save persistent state."""
192
- try:
193
- # Save browser state before closing (cookies, localStorage, etc.)
194
- if self._context:
195
- try:
196
- storage_state_path = self.profile_dir / "storage_state.json"
197
- await self._context.storage_state(path=str(storage_state_path))
198
- emit_success(f"Browser state saved to {storage_state_path}")
199
- except Exception as e:
200
- emit_warning(f"Could not save storage state: {e}")
201
-
202
- await self._context.close()
203
- self._context = None
204
- if self._browser:
205
- await self._browser.close()
206
- self._browser = None
207
- self._initialized = False
208
- except Exception as e:
209
- emit_warning(f"Warning during cleanup: {e}")
210
-
211
- async def close(self) -> None:
212
- """Close the browser and clean up resources."""
213
- await self._cleanup()
214
- emit_info("Camoufox browser closed")
215
-
216
- def __del__(self):
217
- """Ensure cleanup on object destruction."""
218
- # Note: Can't use async in __del__, so this is just a fallback
219
- if self._initialized:
220
- import asyncio
221
-
222
- try:
223
- loop = asyncio.get_event_loop()
224
- if loop.is_running():
225
- loop.create_task(self._cleanup())
226
- else:
227
- loop.run_until_complete(self._cleanup())
228
- except Exception:
229
- pass # Best effort cleanup
230
-
231
-
232
- # Convenience function for getting the singleton instance
233
- def get_camoufox_manager() -> CamoufoxManager:
234
- """Get the singleton CamoufoxManager instance."""
235
- return CamoufoxManager.get_instance()
@@ -1,90 +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
- def _get_vqa_instructions() -> str:
23
- """Get the system instructions for the VQA agent."""
24
- return (
25
- "You are a visual analysis specialist. Answer the user's question about the provided image. "
26
- "Always respond using the structured schema: answer, confidence (0-1 float), observations. "
27
- "Confidence reflects how certain you are about the answer. Observations should include useful, concise context."
28
- )
29
-
30
-
31
- @lru_cache(maxsize=1)
32
- def _load_vqa_agent(model_name: str) -> Agent[None, VisualAnalysisResult]:
33
- """Create a cached agent instance for visual analysis."""
34
- from code_puppy.model_utils import prepare_prompt_for_model
35
-
36
- models_config = ModelFactory.load_config()
37
- model = ModelFactory.get_model(model_name, models_config)
38
-
39
- # Handle claude-code models: swap instructions (prompt prepending happens in run_vqa_analysis)
40
- instructions = _get_vqa_instructions()
41
- prepared = prepare_prompt_for_model(
42
- model_name, instructions, "", prepend_system_to_user=False
43
- )
44
- instructions = prepared.instructions
45
-
46
- vqa_agent = Agent(
47
- model=model,
48
- instructions=instructions,
49
- output_type=VisualAnalysisResult,
50
- retries=2,
51
- )
52
-
53
- if get_use_dbos():
54
- from pydantic_ai.durable_exec.dbos import DBOSAgent
55
-
56
- dbos_agent = DBOSAgent(vqa_agent, name="vqa-agent")
57
- return dbos_agent
58
-
59
- return vqa_agent
60
-
61
-
62
- def _get_vqa_agent() -> Agent[None, VisualAnalysisResult]:
63
- """Return a cached VQA agent configured with the current model."""
64
- model_name = get_vqa_model_name()
65
- # lru_cache keyed by model_name ensures refresh when configuration changes
66
- return _load_vqa_agent(model_name)
67
-
68
-
69
- def run_vqa_analysis(
70
- question: str,
71
- image_bytes: bytes,
72
- media_type: str = "image/png",
73
- ) -> VisualAnalysisResult:
74
- """Execute the VQA agent synchronously against screenshot bytes."""
75
- from code_puppy.model_utils import prepare_prompt_for_model
76
-
77
- agent = _get_vqa_agent()
78
-
79
- # Handle claude-code models: prepend system prompt to user question
80
- model_name = get_vqa_model_name()
81
- prepared = prepare_prompt_for_model(model_name, _get_vqa_instructions(), question)
82
- question = prepared.user_prompt
83
-
84
- result = agent.run_sync(
85
- [
86
- question,
87
- BinaryContent(data=image_bytes, media_type=media_type),
88
- ]
89
- )
90
- return result.output