code-puppy 0.0.202__py3-none-any.whl → 0.0.204__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.
@@ -51,21 +51,50 @@ def get_terminal_session_id() -> str:
51
51
 
52
52
 
53
53
  def _is_process_alive(pid: int) -> bool:
54
- """Check if a process with the given PID is still alive.
54
+ """Check if a process with the given PID is still alive, cross-platform.
55
55
 
56
56
  Args:
57
57
  pid: Process ID to check
58
58
 
59
59
  Returns:
60
- bool: True if process exists, False otherwise
60
+ bool: True if process likely exists, False otherwise
61
61
  """
62
62
  try:
63
- # On Unix: os.kill(pid, 0) raises OSError if process doesn't exist
64
- # On Windows: This also works with signal 0
65
- os.kill(pid, 0)
63
+ if os.name == "nt":
64
+ # Windows: use OpenProcess to probe liveness safely
65
+ import ctypes
66
+ from ctypes import wintypes
67
+
68
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
69
+ kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
70
+ kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
71
+ kernel32.OpenProcess.restype = wintypes.HANDLE
72
+ handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, int(pid))
73
+ if handle:
74
+ kernel32.CloseHandle(handle)
75
+ return True
76
+ # If access denied, process likely exists but we can't query it
77
+ last_error = kernel32.GetLastError()
78
+ # ERROR_ACCESS_DENIED = 5
79
+ if last_error == 5:
80
+ return True
81
+ return False
82
+ else:
83
+ # Unix-like: signal 0 does not deliver a signal but checks existence
84
+ os.kill(int(pid), 0)
85
+ return True
86
+ except PermissionError:
87
+ # No permission to signal -> process exists
66
88
  return True
67
89
  except (OSError, ProcessLookupError):
90
+ # Process does not exist
91
+ return False
92
+ except ValueError:
93
+ # Invalid signal or pid format
68
94
  return False
95
+ except Exception:
96
+ # Be conservative – don't crash session cleanup due to platform quirks
97
+ return True
69
98
 
70
99
 
71
100
  def _cleanup_dead_sessions(sessions: dict[str, str]) -> dict[str, str]:
@@ -13,7 +13,7 @@ import pydantic
13
13
  import pydantic_ai.models
14
14
  from pydantic_ai import Agent as PydanticAgent
15
15
  from pydantic_ai import BinaryContent, DocumentUrl, ImageUrl
16
- from pydantic_ai import RunContext, UsageLimitExceeded
16
+ from pydantic_ai import RunContext, UsageLimitExceeded, UsageLimits
17
17
  from pydantic_ai.messages import (
18
18
  ModelMessage,
19
19
  ModelRequest,
@@ -950,7 +950,7 @@ class BaseAgent(ABC):
950
950
  self.set_message_history(
951
951
  self.prune_interrupted_tool_calls(self.get_message_history())
952
952
  )
953
- usage_limits = pydantic_ai.agent._usage.UsageLimits(request_limit=get_message_limit())
953
+ usage_limits = UsageLimits(request_limit=get_message_limit())
954
954
  result_ = await pydantic_agent.run(
955
955
  prompt_payload,
956
956
  message_history=self.get_message_history(),
code_puppy/main.py CHANGED
@@ -421,7 +421,13 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
421
421
 
422
422
  # Handle / commands based on cleaned prompt (after stripping attachments)
423
423
  if cleaned_for_commands.startswith("/"):
424
- command_result = handle_command(cleaned_for_commands)
424
+ try:
425
+ command_result = handle_command(cleaned_for_commands)
426
+ except Exception as e:
427
+ from code_puppy.messaging import emit_error
428
+ emit_error(f"Command error: {e}")
429
+ # Continue interactive loop instead of exiting
430
+ continue
425
431
  if command_result is True:
426
432
  continue
427
433
  elif isinstance(command_result, str):
@@ -1,6 +1,7 @@
1
1
  """Camoufox browser manager - privacy-focused Firefox automation."""
2
2
 
3
- from typing import Optional
3
+ from pathlib import Path
4
+ from typing import Optional, TypeAlias
4
5
 
5
6
  import camoufox
6
7
  from camoufox.addons import DefaultAddons
@@ -9,6 +10,8 @@ from camoufox.locale import ALLOW_GEOIP, download_mmdb
9
10
  from camoufox.pkgman import CamoufoxFetcher, camoufox_path
10
11
  from playwright.async_api import Browser, BrowserContext, Page
11
12
 
13
+ _MIN_VIEWPORT_DIMENSION = 640
14
+
12
15
  from code_puppy.messaging import emit_info
13
16
 
14
17
 
@@ -38,6 +41,9 @@ class CamoufoxManager:
38
41
  self.block_webrtc = True # Block WebRTC for privacy
39
42
  self.humanize = True # Add human-like behavior
40
43
 
44
+ # Persistent profile directory for consistent browser state across runs
45
+ self.profile_dir = self._get_profile_directory()
46
+
41
47
  @classmethod
42
48
  def get_instance(cls) -> "CamoufoxManager":
43
49
  """Get the singleton instance."""
@@ -45,6 +51,16 @@ class CamoufoxManager:
45
51
  cls._instance = cls()
46
52
  return cls._instance
47
53
 
54
+ def _get_profile_directory(self) -> Path:
55
+ """Get or create the persistent profile directory.
56
+
57
+ Returns a Path object pointing to ~/.code_puppy/camoufox_profile
58
+ where browser data (cookies, history, bookmarks, etc.) will be stored.
59
+ """
60
+ profile_path = Path.home() / ".code_puppy" / "camoufox_profile"
61
+ profile_path.mkdir(parents=True, exist_ok=True)
62
+ return profile_path
63
+
48
64
  async def async_initialize(self) -> None:
49
65
  """Initialize Camoufox browser."""
50
66
  if self._initialized:
@@ -68,30 +84,40 @@ class CamoufoxManager:
68
84
 
69
85
  async def _initialize_camoufox(self) -> None:
70
86
  """Try to start Camoufox with the configured privacy settings."""
87
+ emit_info(f"[cyan]📁 Using persistent profile: {self.profile_dir}[/cyan]")
88
+
71
89
  camoufox_instance = camoufox.AsyncCamoufox(
72
90
  headless=self.headless,
73
91
  block_webrtc=self.block_webrtc,
74
92
  humanize=self.humanize,
75
93
  exclude_addons=list(DefaultAddons),
94
+ persistent_context=True,
95
+ user_data_dir=str(self.profile_dir),
76
96
  addons=[],
77
97
  )
78
- self._browser = await camoufox_instance.start()
79
- self._context = await self._browser.new_context(
80
- viewport={"width": 1920, "height": 1080},
81
- ignore_https_errors=True,
82
- )
83
- page = await self._context.new_page()
84
- await page.goto(self.homepage)
98
+
99
+ self._browser = camoufox_instance.browser
100
+ # Use persistent storage directory for browser context
101
+ # This ensures cookies, localStorage, history, etc. persist across runs
102
+ if not self._initialized:
103
+ self._context = await camoufox_instance.start()
104
+ self._initialized = True
105
+ # Do not auto-open a page here to avoid duplicate windows/tabs.
85
106
 
86
107
  async def get_current_page(self) -> Optional[Page]:
87
- """Get the currently active page."""
108
+ """Get the currently active page. Lazily creates one if none exist."""
88
109
  if not self._initialized or not self._context:
89
110
  await self.async_initialize()
90
111
 
91
- if self._context:
92
- pages = self._context.pages
93
- return pages[0] if pages else None
94
- return None
112
+ if not self._context:
113
+ return None
114
+
115
+ pages = self._context.pages
116
+ if pages:
117
+ return pages[0]
118
+
119
+ # Lazily create a new blank page without navigation
120
+ return await self._context.new_page()
95
121
 
96
122
  async def new_page(self, url: Optional[str] = None) -> Page:
97
123
  """Create a new page and optionally navigate to URL."""
@@ -140,9 +166,21 @@ class CamoufoxManager:
140
166
  return self._context.pages
141
167
 
142
168
  async def _cleanup(self) -> None:
143
- """Clean up browser resources."""
169
+ """Clean up browser resources and save persistent state."""
144
170
  try:
171
+ # Save browser state before closing (cookies, localStorage, etc.)
145
172
  if self._context:
173
+ try:
174
+ storage_state_path = self.profile_dir / "storage_state.json"
175
+ await self._context.storage_state(path=str(storage_state_path))
176
+ emit_info(
177
+ f"[green]💾 Browser state saved to {storage_state_path}[/green]"
178
+ )
179
+ except Exception as e:
180
+ emit_info(
181
+ f"[yellow]Warning: Could not save storage state: {e}[/yellow]"
182
+ )
183
+
146
184
  await self._context.close()
147
185
  self._context = None
148
186
  if self._browser:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.202
3
+ Version: 0.0.204
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -26,7 +26,7 @@ Requires-Dist: openai>=1.99.1
26
26
  Requires-Dist: pathspec>=0.11.0
27
27
  Requires-Dist: playwright>=1.40.0
28
28
  Requires-Dist: prompt-toolkit>=3.0.52
29
- Requires-Dist: pydantic-ai==1.0.6
29
+ Requires-Dist: pydantic-ai==1.0.5
30
30
  Requires-Dist: pydantic>=2.4.0
31
31
  Requires-Dist: pyjwt>=2.8.0
32
32
  Requires-Dist: pytest-cov>=6.1.1
@@ -3,7 +3,7 @@ code_puppy/__main__.py,sha256=pDVssJOWP8A83iFkxMLY9YteHYat0EyWDQqMkKHpWp4,203
3
3
  code_puppy/callbacks.py,sha256=ukSgVFaEO68o6J09qFwDrnmNanrVv3toTLQhS504Meo,6162
4
4
  code_puppy/config.py,sha256=xT-nU1U4n7u8pyzJPG18-cJZBKv5OZI2CtHLt9DGRzU,26065
5
5
  code_puppy/http_utils.py,sha256=YLd8Y16idbI32JGeBXG8n5rT4o4X_zxk9FgUvK9XFo8,8248
6
- code_puppy/main.py,sha256=WqlOivWMzm0ijLB9qBHK5Q_adJurzkD_ywGEUVD16RA,23770
6
+ code_puppy/main.py,sha256=SUh2UNbbEwVWSQwDkz-xBp80Q8qenX7tItsEEopcZfI,24024
7
7
  code_puppy/model_factory.py,sha256=ZbIAJWMNKNdTCEMQK8Ig6TDDZlVNyGO9hOLHoLLPMYw,15397
8
8
  code_puppy/models.json,sha256=dClUciCo2RlVDs0ZAQCIur8MOavZUEAXHEecn0uPa-4,1629
9
9
  code_puppy/reopenable_async_client.py,sha256=4UJRaMp5np8cbef9F0zKQ7TPKOfyf5U-Kv-0zYUWDho,8274
@@ -21,13 +21,13 @@ code_puppy/agents/agent_cpp_reviewer.py,sha256=H4INgJo2OJ84QT7bfTkw4s1Ml7luwokhA
21
21
  code_puppy/agents/agent_creator_agent.py,sha256=IiwVirB6uoIeGOmtetut9eDv6o055ykND3V-fvyA8Lw,23042
22
22
  code_puppy/agents/agent_golang_reviewer.py,sha256=-OMuT8hkapVf2Oox46Ck9SRHlsfd8ab8uefbVfdW72M,3348
23
23
  code_puppy/agents/agent_javascript_reviewer.py,sha256=5YC4kRSvorcNgObjHjD2Rrgnvf8jlKhPqWdjOMjU9A0,3636
24
- code_puppy/agents/agent_manager.py,sha256=D5l72Xk3XVeb07FZHKxIMNfhOjxAAzC-8min-gv11mY,11568
24
+ code_puppy/agents/agent_manager.py,sha256=-q1p3_xHGTguXhDtHvVBWAscvX3ZrSNbXsl382GBeC4,12790
25
25
  code_puppy/agents/agent_python_reviewer.py,sha256=D0M3VA12QKdsyg2zIBI2FECxz0IP2fSIfg24xGzDhw0,3837
26
26
  code_puppy/agents/agent_qa_expert.py,sha256=wCGXzuAVElT5c-QigQVb8JX9Gw0JmViCUQQnADMSbVc,3796
27
27
  code_puppy/agents/agent_qa_kitten.py,sha256=5PeFFSwCFlTUvP6h5bGntx0xv5NmRwBiw0HnMqY8nLI,9107
28
28
  code_puppy/agents/agent_security_auditor.py,sha256=ADafi2x4gqXw6m-Nch5vjiKjO0Urcbj0x4zxHti3gDw,3712
29
29
  code_puppy/agents/agent_typescript_reviewer.py,sha256=EDY1mFkVpuJ1BPXsJFu2wQ2pfAV-90ipc_8w9ymrKPg,4054
30
- code_puppy/agents/base_agent.py,sha256=dsnoFEHXVhXgK-WIOskCU1Ccx6uz5onfiycWAMxBhaw,41421
30
+ code_puppy/agents/base_agent.py,sha256=4hwqOCXQwL2F6luQsYn_IAGjboz1MTU2YIfHG6IYKnU,41409
31
31
  code_puppy/agents/json_agent.py,sha256=lhopDJDoiSGHvD8A6t50hi9ZBoNRKgUywfxd0Po_Dzc,4886
32
32
  code_puppy/command_line/__init__.py,sha256=y7WeRemfYppk8KVbCGeAIiTuiOszIURCDjOMZv_YRmU,45
33
33
  code_puppy/command_line/attachments.py,sha256=rqBUR52wJj4cP-iTkN4KdoHZ9Oovc6tBp6ayifi9UF0,12022
@@ -99,7 +99,7 @@ code_puppy/tools/browser/browser_navigation.py,sha256=Tj_fNcM3KGpkM2UTKcGQX8BpI3
99
99
  code_puppy/tools/browser/browser_screenshot.py,sha256=YU4olUqxhqyK3_pBC0BtU6A7_EEtiRlh6saj93nkKAg,8258
100
100
  code_puppy/tools/browser/browser_scripts.py,sha256=MMO5KRjdrhuLOoJGoKGG1jm6UAqhFhUznz02aWqhMAE,15065
101
101
  code_puppy/tools/browser/browser_workflows.py,sha256=jplJ1T60W3G4-dhVJX-CXkm9sskUH_Qzp0Dj-oubvrE,6142
102
- code_puppy/tools/browser/camoufox_manager.py,sha256=RYvLcs0iAoVNtpLjrrA1uu6a5k9tAdBbmhWFGSWjX_A,6106
102
+ code_puppy/tools/browser/camoufox_manager.py,sha256=bzeerqgQa3CBKVe0UGPpbVR7pKpOVcHlvun7ASRD-KA,7770
103
103
  code_puppy/tools/browser/vqa_agent.py,sha256=0GMDgJAK728rIuSQxAVytFSNagjo0LCjCUxBTm3w9Po,1952
104
104
  code_puppy/tui/__init__.py,sha256=XesAxIn32zLPOmvpR2wIDxDAnnJr81a5pBJB4cZp1Xs,321
105
105
  code_puppy/tui/app.py,sha256=D-8qHzxYbe-bVgrkBLl2lLBw7HRbUoVqDTRKy1gaE-E,44279
@@ -123,9 +123,9 @@ code_puppy/tui/screens/help.py,sha256=eJuPaOOCp7ZSUlecearqsuX6caxWv7NQszUh0tZJjB
123
123
  code_puppy/tui/screens/mcp_install_wizard.py,sha256=vObpQwLbXjQsxmSg-WCasoev1usEi0pollKnL0SHu9U,27693
124
124
  code_puppy/tui/screens/settings.py,sha256=EoMxiguyeF0srwV1bj4_MG9rrxkNthh6TdTNsxnXLfE,11460
125
125
  code_puppy/tui/screens/tools.py,sha256=3pr2Xkpa9Js6Yhf1A3_wQVRzFOui-KDB82LwrsdBtyk,1715
126
- code_puppy-0.0.202.data/data/code_puppy/models.json,sha256=dClUciCo2RlVDs0ZAQCIur8MOavZUEAXHEecn0uPa-4,1629
127
- code_puppy-0.0.202.dist-info/METADATA,sha256=1zqxAnwukbylgLJ9rPwn2DNQ8zMCT7bNFl667rXJ4es,20759
128
- code_puppy-0.0.202.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
129
- code_puppy-0.0.202.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
130
- code_puppy-0.0.202.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
131
- code_puppy-0.0.202.dist-info/RECORD,,
126
+ code_puppy-0.0.204.data/data/code_puppy/models.json,sha256=dClUciCo2RlVDs0ZAQCIur8MOavZUEAXHEecn0uPa-4,1629
127
+ code_puppy-0.0.204.dist-info/METADATA,sha256=Fcbi95ybiGcekCuITFxE1_3YGaFJjdXoICpjpw4FNPQ,20759
128
+ code_puppy-0.0.204.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
129
+ code_puppy-0.0.204.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
130
+ code_puppy-0.0.204.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
131
+ code_puppy-0.0.204.dist-info/RECORD,,