code-puppy 0.0.203__py3-none-any.whl → 0.0.205__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,6 +13,10 @@ from pydantic_ai import BinaryContent, DocumentUrl, ImageUrl
13
13
 
14
14
  SUPPORTED_INLINE_SCHEMES = {"http", "https"}
15
15
 
16
+ # Maximum path length to consider - conservative limit to avoid OS errors
17
+ # Most OS have limits around 4096, but we set lower to catch garbage early
18
+ MAX_PATH_LENGTH = 1024
19
+
16
20
  # Allow common extensions people drag in the terminal.
17
21
  DEFAULT_ACCEPTED_IMAGE_EXTENSIONS = {
18
22
  ".png",
@@ -61,6 +65,9 @@ def _is_probable_path(token: str) -> bool:
61
65
 
62
66
  if not token:
63
67
  return False
68
+ # Reject absurdly long tokens before any processing to avoid OS errors
69
+ if len(token) > MAX_PATH_LENGTH:
70
+ return False
64
71
  if token.startswith("#"):
65
72
  return False
66
73
  # Windows drive letters or Unix absolute/relative paths
@@ -69,7 +76,7 @@ def _is_probable_path(token: str) -> bool:
69
76
  if len(token) >= 2 and token[1] == ":":
70
77
  return True
71
78
  # Things like `path/to/file.png`
72
- return os.sep in token or "\"" in token
79
+ return os.sep in token or '"' in token
73
80
 
74
81
 
75
82
  def _unescape_dragged_path(token: str) -> str:
@@ -107,9 +114,13 @@ def _load_binary(path: Path) -> bytes:
107
114
  except FileNotFoundError as exc:
108
115
  raise AttachmentParsingError(f"Attachment not found: {path}") from exc
109
116
  except PermissionError as exc:
110
- raise AttachmentParsingError(f"Cannot read attachment (permission denied): {path}") from exc
117
+ raise AttachmentParsingError(
118
+ f"Cannot read attachment (permission denied): {path}"
119
+ ) from exc
111
120
  except OSError as exc:
112
- raise AttachmentParsingError(f"Failed to read attachment {path}: {exc}") from exc
121
+ raise AttachmentParsingError(
122
+ f"Failed to read attachment {path}: {exc}"
123
+ ) from exc
113
124
 
114
125
 
115
126
  def _tokenise(prompt: str) -> Iterable[str]:
@@ -147,7 +158,10 @@ def _candidate_paths(
147
158
 
148
159
  def _is_supported_extension(path: Path) -> bool:
149
160
  suffix = path.suffix.lower()
150
- return suffix in DEFAULT_ACCEPTED_IMAGE_EXTENSIONS | DEFAULT_ACCEPTED_DOCUMENT_EXTENSIONS
161
+ return (
162
+ suffix
163
+ in DEFAULT_ACCEPTED_IMAGE_EXTENSIONS | DEFAULT_ACCEPTED_DOCUMENT_EXTENSIONS
164
+ )
151
165
 
152
166
 
153
167
  def _parse_link(token: str) -> PromptLinkAttachment | None:
@@ -203,6 +217,11 @@ def _detect_path_tokens(prompt: str) -> tuple[list[_DetectedPath], list[str]]:
203
217
  index += 1
204
218
  continue
205
219
 
220
+ # Additional guard: skip if stripped token exceeds reasonable path length
221
+ if len(stripped_token) > MAX_PATH_LENGTH:
222
+ index += 1
223
+ continue
224
+
206
225
  start_index = index
207
226
  consumed_until = index + 1
208
227
  candidate_path_token = stripped_token
@@ -223,7 +242,16 @@ def _detect_path_tokens(prompt: str) -> tuple[list[_DetectedPath], list[str]]:
223
242
  index = consumed_until
224
243
  continue
225
244
 
226
- if not path.exists() or not path.is_file():
245
+ # Guard filesystem operations against OS errors (ENAMETOOLONG, etc.)
246
+ try:
247
+ path_exists = path.exists()
248
+ path_is_file = path.is_file() if path_exists else False
249
+ except OSError:
250
+ # Skip this token if filesystem check fails (path too long, etc.)
251
+ index = consumed_until
252
+ continue
253
+
254
+ if not path_exists or not path_is_file:
227
255
  found_span = False
228
256
  last_path = path
229
257
  for joined, end_index in _candidate_paths(tokens, index):
@@ -328,7 +356,8 @@ def parse_prompt_attachments(prompt: str) -> ProcessedPrompt:
328
356
  spans = [
329
357
  (d.start_index, d.consumed_until)
330
358
  for d in detections
331
- if (d.path is not None and not d.unsupported) or (d.link is not None and d.path is None)
359
+ if (d.path is not None and not d.unsupported)
360
+ or (d.link is not None and d.path is None)
332
361
  ]
333
362
  cleaned_parts: list[str] = []
334
363
  i = 0
@@ -360,4 +389,4 @@ __all__ = [
360
389
  "PromptLinkAttachment",
361
390
  "AttachmentParsingError",
362
391
  "parse_prompt_attachments",
363
- ]
392
+ ]
@@ -12,14 +12,20 @@ from typing import Optional
12
12
 
13
13
  from prompt_toolkit import PromptSession
14
14
  from prompt_toolkit.completion import Completer, Completion, merge_completers
15
+ from prompt_toolkit.filters import is_searching
15
16
  from prompt_toolkit.formatted_text import FormattedText
16
17
  from prompt_toolkit.history import FileHistory
17
- from prompt_toolkit.filters import is_searching
18
18
  from prompt_toolkit.key_binding import KeyBindings
19
19
  from prompt_toolkit.keys import Keys
20
20
  from prompt_toolkit.layout.processors import Processor, Transformation
21
21
  from prompt_toolkit.styles import Style
22
22
 
23
+ from code_puppy.command_line.attachments import (
24
+ DEFAULT_ACCEPTED_DOCUMENT_EXTENSIONS,
25
+ DEFAULT_ACCEPTED_IMAGE_EXTENSIONS,
26
+ _detect_path_tokens,
27
+ _tokenise,
28
+ )
23
29
  from code_puppy.command_line.file_path_completion import FilePathCompleter
24
30
  from code_puppy.command_line.load_context_completion import LoadContextCompleter
25
31
  from code_puppy.command_line.model_picker_completion import (
@@ -34,11 +40,6 @@ from code_puppy.config import (
34
40
  get_puppy_name,
35
41
  get_value,
36
42
  )
37
- from code_puppy.command_line.attachments import (
38
- DEFAULT_ACCEPTED_DOCUMENT_EXTENSIONS,
39
- DEFAULT_ACCEPTED_IMAGE_EXTENSIONS,
40
- _detect_path_tokens, _tokenise,
41
- )
42
43
 
43
44
 
44
45
  class SetCompleter(Completer):
@@ -108,6 +109,8 @@ class AttachmentPlaceholderProcessor(Processor):
108
109
  """Display friendly placeholders for recognised attachments."""
109
110
 
110
111
  _PLACEHOLDER_STYLE = "class:attachment-placeholder"
112
+ # Skip expensive path detection for very long input (likely pasted content)
113
+ _MAX_TEXT_LENGTH_FOR_REALTIME = 500
111
114
 
112
115
  def apply_transformation(self, transformation_input):
113
116
  document = transformation_input.document
@@ -115,6 +118,10 @@ class AttachmentPlaceholderProcessor(Processor):
115
118
  if not text:
116
119
  return Transformation(list(transformation_input.fragments))
117
120
 
121
+ # Skip real-time path detection for long text to avoid slowdown
122
+ if len(text) > self._MAX_TEXT_LENGTH_FOR_REALTIME:
123
+ return Transformation(list(transformation_input.fragments))
124
+
118
125
  detections, _warnings = _detect_path_tokens(text)
119
126
  replacements: list[tuple[int, int, str]] = []
120
127
  search_cursor = 0
@@ -138,7 +145,7 @@ class AttachmentPlaceholderProcessor(Processor):
138
145
  continue
139
146
 
140
147
  # Use token-span for robust lookup (handles escaped spaces)
141
- span_tokens = token_view[detection.start_index:detection.consumed_until]
148
+ span_tokens = token_view[detection.start_index : detection.consumed_until]
142
149
  raw_span = " ".join(span_tokens).replace(ESCAPE_MARKER, r"\ ")
143
150
  index = text.find(raw_span, search_cursor)
144
151
  span_len = len(raw_span)
@@ -188,7 +195,9 @@ class AttachmentPlaceholderProcessor(Processor):
188
195
  display_index += 1
189
196
 
190
197
  for _ in text[source_index:end]:
191
- source_to_display_map.append(placeholder_start if placeholder else display_index)
198
+ source_to_display_map.append(
199
+ placeholder_start if placeholder else display_index
200
+ )
192
201
  source_index += 1
193
202
 
194
203
  if source_index < len(text):
@@ -340,6 +349,7 @@ async def get_input_with_combined_completion(
340
349
 
341
350
  # Also allow Ctrl+Enter for newline (terminal-dependent)
342
351
  try:
352
+
343
353
  @bindings.add("c-enter", eager=True)
344
354
  def _(event):
345
355
  event.app.current_buffer.insert_text("\n")
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):
code_puppy/models.json CHANGED
@@ -50,6 +50,11 @@
50
50
  "name": "claude-sonnet-4-5-20250929",
51
51
  "context_length": 200000
52
52
  },
53
+ "claude-4-1-opus": {
54
+ "type": "anthropic",
55
+ "name": "claude-opus-4-1-20250805",
56
+ "context_length": 200000
57
+ },
53
58
  "glm-4.5-coding": {
54
59
  "type": "zai_coding",
55
60
  "name": "glm-4.5"
@@ -50,6 +50,11 @@
50
50
  "name": "claude-sonnet-4-5-20250929",
51
51
  "context_length": 200000
52
52
  },
53
+ "claude-4-1-opus": {
54
+ "type": "anthropic",
55
+ "name": "claude-opus-4-1-20250805",
56
+ "context_length": 200000
57
+ },
53
58
  "glm-4.5-coding": {
54
59
  "type": "zai_coding",
55
60
  "name": "glm-4.5"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.203
3
+ Version: 0.0.205
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
@@ -3,9 +3,9 @@ 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
- code_puppy/models.json,sha256=dClUciCo2RlVDs0ZAQCIur8MOavZUEAXHEecn0uPa-4,1629
8
+ code_puppy/models.json,sha256=dppxeQ43J5aDl-5Ytr9Rq31tiEpcQndgDVTuF1Csr_g,1751
9
9
  code_puppy/reopenable_async_client.py,sha256=4UJRaMp5np8cbef9F0zKQ7TPKOfyf5U-Kv-0zYUWDho,8274
10
10
  code_puppy/round_robin_model.py,sha256=UEfw-Ix7GpNRWSxxuJtA-EE4_A46KXjMgFRciprfLmg,5634
11
11
  code_puppy/session_storage.py,sha256=Pf5C-qyC6xLhZCTlbAdkPwOFyqlaDomVnj9Z4cZcZsE,9595
@@ -21,7 +21,7 @@ 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
@@ -30,13 +30,13 @@ code_puppy/agents/agent_typescript_reviewer.py,sha256=EDY1mFkVpuJ1BPXsJFu2wQ2pfA
30
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
- code_puppy/command_line/attachments.py,sha256=rqBUR52wJj4cP-iTkN4KdoHZ9Oovc6tBp6ayifi9UF0,12022
33
+ code_puppy/command_line/attachments.py,sha256=eOf0zqBWnoAgC1FhWOkOyLjx_es0HGEFQ6EV1ZZuc1c,12934
34
34
  code_puppy/command_line/command_handler.py,sha256=alxMe5v_4jq8Sm6HETsgfF-VoDtgExj9dVzxP77fwmY,31614
35
35
  code_puppy/command_line/file_path_completion.py,sha256=gw8NpIxa6GOpczUJRyh7VNZwoXKKn-yvCqit7h2y6Gg,2931
36
36
  code_puppy/command_line/load_context_completion.py,sha256=6eZxV6Bs-EFwZjN93V8ZDZUC-6RaWxvtZk-04Wtikyw,2240
37
37
  code_puppy/command_line/model_picker_completion.py,sha256=uqwpbMYnCcWUZZ10Y4pMBKBfW52wQ-KdML2PO4Xjwr0,4501
38
38
  code_puppy/command_line/motd.py,sha256=PEdkp3ZnydVfvd7mNJylm8YyFNUKg9jmY6uwkA1em8c,2152
39
- code_puppy/command_line/prompt_toolkit_completion.py,sha256=VZanuLcv1Py4sknTJWlJOQ6IqWqDjlHPoBcoeHEcPCA,15325
39
+ code_puppy/command_line/prompt_toolkit_completion.py,sha256=ce0VB2TeaqcbdXolRwk_kI19tKgX-1AROhJPBbfI10s,15694
40
40
  code_puppy/command_line/utils.py,sha256=7eyxDHjPjPB9wGDJQQcXV_zOsGdYsFgI0SGCetVmTqE,1251
41
41
  code_puppy/command_line/mcp/__init__.py,sha256=0-OQuwjq_pLiTVJ1_NrirVwdRerghyKs_MTZkwPC7YY,315
42
42
  code_puppy/command_line/mcp/add_command.py,sha256=lZ09RpFDIeghX1zhc2YIAqBASs5Ra52x5YAasUKvqJg,6409
@@ -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.203.data/data/code_puppy/models.json,sha256=dClUciCo2RlVDs0ZAQCIur8MOavZUEAXHEecn0uPa-4,1629
127
- code_puppy-0.0.203.dist-info/METADATA,sha256=vEcAaiXB9z01PmgZTX_mCX_u-9KrT5M19oawPnWEAAM,20759
128
- code_puppy-0.0.203.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
129
- code_puppy-0.0.203.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
130
- code_puppy-0.0.203.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
131
- code_puppy-0.0.203.dist-info/RECORD,,
126
+ code_puppy-0.0.205.data/data/code_puppy/models.json,sha256=dppxeQ43J5aDl-5Ytr9Rq31tiEpcQndgDVTuF1Csr_g,1751
127
+ code_puppy-0.0.205.dist-info/METADATA,sha256=k7rTavcaOz2Wc2VAf_Fy0dBaRuI0qQUXMAYK8Htumew,20759
128
+ code_puppy-0.0.205.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
129
+ code_puppy-0.0.205.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
130
+ code_puppy-0.0.205.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
131
+ code_puppy-0.0.205.dist-info/RECORD,,