code-puppy 0.0.84__py3-none-any.whl → 0.0.85__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 CHANGED
@@ -1,5 +1,6 @@
1
1
  try:
2
2
  import importlib.metadata
3
+
3
4
  __version__ = importlib.metadata.version("code-puppy")
4
5
  except importlib.metadata.PackageNotFoundError:
5
6
  __version__ = "0.0.1"
code_puppy/agent.py CHANGED
@@ -79,7 +79,7 @@ def reload_code_generation_agent():
79
79
  instructions=instructions,
80
80
  output_type=str,
81
81
  retries=3,
82
- history_processors=[message_history_accumulator]
82
+ history_processors=[message_history_accumulator],
83
83
  )
84
84
  register_all_tools(agent)
85
85
  _code_generation_agent = agent
@@ -101,9 +101,7 @@ Important rules:
101
101
 
102
102
  Your solutions should be production-ready, maintainable, and follow best practices for the chosen language.
103
103
 
104
- Return your final response as a structured output having the following fields:
105
- * output_message: The final output message to display to the user
106
- * awaiting_user_input: True if user input is needed to continue the task. If you get an error, you might consider asking the user for help.
104
+ Return your final response as a string output
107
105
  """
108
106
 
109
107
 
@@ -48,4 +48,4 @@ def print_motd(console, force: bool = False) -> bool:
48
48
  console.print(MOTD_MESSAGE)
49
49
  mark_motd_seen(MOTD_VERSION)
50
50
  return True
51
- return False
51
+ return False
code_puppy/main.py CHANGED
@@ -21,7 +21,8 @@ from code_puppy.state_management import get_message_history, set_message_history
21
21
  # Initialize rich console for pretty output
22
22
  from code_puppy.tools.common import console
23
23
  from code_puppy.version_checker import fetch_latest_version
24
- from code_puppy.message_history_processor import message_history_processor
24
+ from code_puppy.message_history_processor import message_history_processor, prune_interrupted_tool_calls
25
+
25
26
 
26
27
  # from code_puppy.tools import * # noqa: F403
27
28
 
@@ -193,13 +194,13 @@ async def interactive_mode(history_file_path: str) -> None:
193
194
  try:
194
195
  prettier_code_blocks()
195
196
  local_cancelled = False
197
+
196
198
  async def run_agent_task():
197
199
  try:
198
200
  agent = get_code_generation_agent()
199
201
  async with agent.run_mcp_servers():
200
202
  return await agent.run(
201
- task,
202
- message_history=get_message_history()
203
+ task, message_history=get_message_history()
203
204
  )
204
205
  except Exception as e:
205
206
  console.log("Task failed", e)
@@ -207,20 +208,30 @@ async def interactive_mode(history_file_path: str) -> None:
207
208
  agent_task = asyncio.create_task(run_agent_task())
208
209
 
209
210
  import signal
211
+ from code_puppy.tools import kill_all_running_shell_processes
210
212
 
211
213
  original_handler = None
212
214
 
215
+ # Ensure the interrupt handler only acts once per task
216
+ handled = False
213
217
  def keyboard_interrupt_handler(sig, frame):
214
218
  nonlocal local_cancelled
215
- if not agent_task.done():
216
- set_message_history(
217
- message_history_processor(
218
- get_message_history()
219
- )
220
- )
221
- agent_task.cancel()
222
- local_cancelled = True
223
-
219
+ nonlocal handled
220
+ if handled:
221
+ return
222
+ handled = True
223
+ # First, nuke any running shell processes triggered by tools
224
+ try:
225
+ killed = kill_all_running_shell_processes()
226
+ if killed:
227
+ console.print(f"[yellow]Cancelled {killed} running shell process(es).[/yellow]")
228
+ else:
229
+ # Then cancel the agent task
230
+ if not agent_task.done():
231
+ agent_task.cancel()
232
+ local_cancelled = True
233
+ except Exception as e:
234
+ console.print(f"[dim]Shell kill error: {e}[/dim]")
224
235
  try:
225
236
  original_handler = signal.getsignal(signal.SIGINT)
226
237
  signal.signal(signal.SIGINT, keyboard_interrupt_handler)
@@ -1,30 +1,40 @@
1
1
  import json
2
- import queue
3
- from typing import List
2
+ from typing import List, Set
4
3
  import os
5
4
  from pathlib import Path
6
5
 
7
6
  import pydantic
8
7
  import tiktoken
9
- from pydantic_ai.messages import ModelMessage, ToolCallPart, ToolReturnPart, UserPromptPart, TextPart, ModelRequest, ModelResponse
8
+ from pydantic_ai.messages import (
9
+ ModelMessage,
10
+ TextPart,
11
+ ModelResponse,
12
+ ModelRequest,
13
+ ToolCallPart,
14
+ )
10
15
 
11
- from code_puppy.config import get_message_history_limit
12
16
  from code_puppy.tools.common import console
13
17
  from code_puppy.model_factory import ModelFactory
14
18
  from code_puppy.config import get_model_name
15
19
 
16
20
  # Import summarization agent
17
21
  try:
18
- from code_puppy.summarization_agent import get_summarization_agent as _get_summarization_agent
22
+ from code_puppy.summarization_agent import (
23
+ get_summarization_agent as _get_summarization_agent,
24
+ )
25
+
19
26
  SUMMARIZATION_AVAILABLE = True
20
-
27
+
21
28
  # Make the function available in this module's namespace for mocking
22
29
  def get_summarization_agent():
23
30
  return _get_summarization_agent()
24
-
31
+
25
32
  except ImportError:
26
33
  SUMMARIZATION_AVAILABLE = False
27
- console.print("[yellow]Warning: Summarization agent not available. Message history will be truncated instead of summarized.[/yellow]")
34
+ console.print(
35
+ "[yellow]Warning: Summarization agent not available. Message history will be truncated instead of summarized.[/yellow]"
36
+ )
37
+
28
38
  def get_summarization_agent():
29
39
  return None
30
40
 
@@ -40,10 +50,10 @@ def get_tokenizer_for_model(model_name: str):
40
50
  def stringify_message_part(part) -> str:
41
51
  """
42
52
  Convert a message part to a string representation for token estimation or other uses.
43
-
53
+
44
54
  Args:
45
55
  part: A message part that may contain content or be a tool call
46
-
56
+
47
57
  Returns:
48
58
  String representation of the message part
49
59
  """
@@ -54,7 +64,7 @@ def stringify_message_part(part) -> str:
54
64
  result += str(type(part)) + ": "
55
65
 
56
66
  # Handle content
57
- if hasattr(part, 'content') and part.content:
67
+ if hasattr(part, "content") and part.content:
58
68
  # Handle different content types
59
69
  if isinstance(part.content, str):
60
70
  result = part.content
@@ -64,16 +74,16 @@ def stringify_message_part(part) -> str:
64
74
  result = json.dumps(part.content)
65
75
  else:
66
76
  result = str(part.content)
67
-
77
+
68
78
  # Handle tool calls which may have additional token costs
69
79
  # If part also has content, we'll process tool calls separately
70
- if hasattr(part, 'tool_name') and part.tool_name:
80
+ if hasattr(part, "tool_name") and part.tool_name:
71
81
  # Estimate tokens for tool name and parameters
72
82
  tool_text = part.tool_name
73
83
  if hasattr(part, "args"):
74
84
  tool_text += f" {str(part.args)}"
75
85
  result += tool_text
76
-
86
+
77
87
  return result
78
88
 
79
89
 
@@ -84,27 +94,22 @@ def estimate_tokens_for_message(message: ModelMessage) -> int:
84
94
  """
85
95
  tokenizer = get_tokenizer_for_model(get_model_name())
86
96
  total_tokens = 0
87
-
97
+
88
98
  for part in message.parts:
89
99
  part_str = stringify_message_part(part)
90
100
  if part_str:
91
101
  tokens = tokenizer.encode(part_str)
92
102
  total_tokens += len(tokens)
93
-
103
+
94
104
  return max(1, total_tokens)
95
105
 
96
106
 
97
107
  def summarize_messages(messages: List[ModelMessage]) -> ModelMessage:
98
-
99
- # Get the summarization agent
100
108
  summarization_agent = get_summarization_agent()
101
- message_strings = []
102
-
109
+ message_strings: List[str] = []
103
110
  for message in messages:
104
111
  for part in message.parts:
105
112
  message_strings.append(stringify_message_part(part))
106
-
107
-
108
113
  summary_string = "\n".join(message_strings)
109
114
  instructions = (
110
115
  "Above I've given you a log of Agentic AI steps that have been taken"
@@ -116,19 +121,53 @@ def summarize_messages(messages: List[ModelMessage]) -> ModelMessage:
116
121
  "\n Make sure your result is a bulleted list of all steps and interactions."
117
122
  )
118
123
  try:
119
- # Run the summarization agent
120
124
  result = summarization_agent.run_sync(f"{summary_string}\n{instructions}")
121
-
122
- # Create a new message with the summarized content
123
- summarized_parts = [TextPart(result.output)]
124
- summarized_message = ModelResponse(parts=summarized_parts)
125
- return summarized_message
125
+ return ModelResponse(parts=[TextPart(result.output)])
126
126
  except Exception as e:
127
127
  console.print(f"Summarization failed during compaction: {e}")
128
- # Return original message if summarization fails
129
128
  return None
130
129
 
131
130
 
131
+ # New: single-message summarization helper used by tests
132
+ # - If the message has a ToolCallPart, return original message (no summarization)
133
+ # - If the message has system/instructions, return original message
134
+ # - Otherwise, summarize and return a new ModelRequest with the summarized content
135
+ # - On any error, return the original message
136
+
137
+
138
+ def summarize_message(message: ModelMessage) -> ModelMessage:
139
+ if not SUMMARIZATION_AVAILABLE:
140
+ return message
141
+ try:
142
+ # If the message looks like a system/instructions message, skip summarization
143
+ instructions = getattr(message, "instructions", None)
144
+ if instructions:
145
+ return message
146
+ # If any part is a tool call, skip summarization
147
+ for part in message.parts:
148
+ if isinstance(part, ToolCallPart) or getattr(part, "tool_name", None):
149
+ return message
150
+ # Build prompt from textual content parts
151
+ content_bits: List[str] = []
152
+ for part in message.parts:
153
+ s = stringify_message_part(part)
154
+ if s:
155
+ content_bits.append(s)
156
+ if not content_bits:
157
+ return message
158
+ prompt = (
159
+ "Please summarize the following user message:\n"
160
+ + "\n".join(content_bits)
161
+ )
162
+ agent = get_summarization_agent()
163
+ result = agent.run_sync(prompt)
164
+ summarized = ModelRequest([TextPart(result.output)])
165
+ return summarized
166
+ except Exception as e:
167
+ console.print(f"Summarization failed: {e}")
168
+ return message
169
+
170
+
132
171
  def get_model_context_length() -> int:
133
172
  """
134
173
  Get the context length for the currently configured model from models.json
@@ -139,20 +178,69 @@ def get_model_context_length() -> int:
139
178
  models_path = Path(__file__).parent / "models.json"
140
179
  else:
141
180
  models_path = Path(models_path)
142
-
181
+
143
182
  model_configs = ModelFactory.load_config(str(models_path))
144
183
  model_name = get_model_name()
145
-
184
+
146
185
  # Get context length from model config
147
186
  model_config = model_configs.get(model_name, {})
148
187
  context_length = model_config.get("context_length", 128000) # Default value
149
-
188
+
150
189
  # Reserve 10% of context for response
151
190
  return int(context_length)
152
191
 
192
+ def prune_interrupted_tool_calls(messages: List[ModelMessage]) -> List[ModelMessage]:
193
+ """
194
+ Remove any messages that participate in mismatched tool call sequences.
195
+
196
+ A mismatched tool call id is one that appears in a ToolCall (model/tool request)
197
+ without a corresponding tool return, or vice versa. We preserve original order
198
+ and only drop messages that contain parts referencing mismatched tool_call_ids.
199
+ """
200
+ if not messages:
201
+ return messages
153
202
 
154
- def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage]:
203
+ tool_call_ids: Set[str] = set()
204
+ tool_return_ids: Set[str] = set()
205
+
206
+ # First pass: collect ids for calls vs returns
207
+ for msg in messages:
208
+ for part in getattr(msg, "parts", []) or []:
209
+ tool_call_id = getattr(part, "tool_call_id", None)
210
+ if not tool_call_id:
211
+ continue
212
+ # Heuristic: if it's an explicit ToolCallPart or has a tool_name/args,
213
+ # consider it a call; otherwise it's a return/result.
214
+ if part.part_kind == "tool-call":
215
+ tool_call_ids.add(tool_call_id)
216
+ else:
217
+ tool_return_ids.add(tool_call_id)
218
+
219
+ mismatched: Set[str] = tool_call_ids.symmetric_difference(tool_return_ids)
220
+ if not mismatched:
221
+ return messages
155
222
 
223
+ pruned: List[ModelMessage] = []
224
+ dropped_count = 0
225
+ for msg in messages:
226
+ has_mismatched = False
227
+ for part in getattr(msg, "parts", []) or []:
228
+ tcid = getattr(part, "tool_call_id", None)
229
+ if tcid and tcid in mismatched:
230
+ has_mismatched = True
231
+ break
232
+ if has_mismatched:
233
+ dropped_count += 1
234
+ continue
235
+ pruned.append(msg)
236
+
237
+ if dropped_count:
238
+ console.print(f"[yellow]Pruned {dropped_count} message(s) with mismatched tool_call_id pairs[/yellow]")
239
+ return pruned
240
+
241
+
242
+ def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage]:
243
+ # First, prune any interrupted/mismatched tool-call conversations
156
244
  total_current_tokens = sum(estimate_tokens_for_message(msg) for msg in messages)
157
245
 
158
246
  model_max = get_model_context_length()
@@ -165,7 +253,9 @@ def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage
165
253
  if proportion_used > 0.9:
166
254
  summary = summarize_messages(messages)
167
255
  result_messages = [messages[0], summary]
168
- final_token_count = sum(estimate_tokens_for_message(msg) for msg in result_messages)
256
+ final_token_count = sum(
257
+ estimate_tokens_for_message(msg) for msg in result_messages
258
+ )
169
259
  console.print(f"Final token count after processing: {final_token_count}")
170
260
  return result_messages
171
- return messages
261
+ return messages
@@ -42,15 +42,17 @@ def build_httpx_proxy(proxy):
42
42
  """Build an httpx.Proxy object from a proxy string in format ip:port:username:password"""
43
43
  proxy_tokens = proxy.split(":")
44
44
  if len(proxy_tokens) != 4:
45
- raise ValueError(f"Invalid proxy format: {proxy}. Expected format: ip:port:username:password")
46
-
45
+ raise ValueError(
46
+ f"Invalid proxy format: {proxy}. Expected format: ip:port:username:password"
47
+ )
48
+
47
49
  ip, port, username, password = proxy_tokens
48
50
  proxy_url = f"http://{ip}:{port}"
49
51
  proxy_auth = (username, password)
50
-
52
+
51
53
  # Log the proxy being used
52
54
  console.log(f"Using proxy: {proxy_url} with username: {username}")
53
-
55
+
54
56
  return httpx.Proxy(url=proxy_url, auth=proxy_auth)
55
57
 
56
58
 
@@ -58,18 +60,22 @@ def get_random_proxy_from_file(file_path):
58
60
  """Reads proxy file and returns a random proxy formatted for httpx.AsyncClient"""
59
61
  if not os.path.exists(file_path):
60
62
  raise ValueError(f"Proxy file '{file_path}' not found.")
61
-
63
+
62
64
  with open(file_path, "r") as f:
63
65
  proxies = [line.strip() for line in f.readlines() if line.strip()]
64
-
66
+
65
67
  if not proxies:
66
- raise ValueError(f"Proxy file '{file_path}' is empty or contains only whitespace.")
67
-
68
+ raise ValueError(
69
+ f"Proxy file '{file_path}' is empty or contains only whitespace."
70
+ )
71
+
68
72
  selected_proxy = random.choice(proxies)
69
73
  try:
70
74
  return build_httpx_proxy(selected_proxy)
71
- except ValueError as e:
72
- console.log(f"Warning: Malformed proxy '{selected_proxy}' found in file '{file_path}', ignoring and continuing without proxy.")
75
+ except ValueError:
76
+ console.log(
77
+ f"Warning: Malformed proxy '{selected_proxy}' found in file '{file_path}', ignoring and continuing without proxy."
78
+ )
73
79
  return None
74
80
 
75
81
 
@@ -147,13 +153,13 @@ class ModelFactory:
147
153
 
148
154
  elif model_type == "custom_anthropic":
149
155
  url, headers, ca_certs_path, api_key = get_custom_config(model_config)
150
-
156
+
151
157
  # Check for proxy configuration
152
158
  proxy_file_path = os.environ.get("CODE_PUPPY_PROXIES")
153
159
  proxy = None
154
160
  if proxy_file_path:
155
161
  proxy = get_random_proxy_from_file(proxy_file_path)
156
-
162
+
157
163
  # Only pass proxy to client if it's valid
158
164
  client_args = {"headers": headers, "verify": ca_certs_path}
159
165
  if proxy is not None:
@@ -223,13 +229,13 @@ class ModelFactory:
223
229
 
224
230
  elif model_type == "custom_openai":
225
231
  url, headers, ca_certs_path, api_key = get_custom_config(model_config)
226
-
232
+
227
233
  # Check for proxy configuration
228
234
  proxy_file_path = os.environ.get("CODE_PUPPY_PROXIES")
229
235
  proxy = None
230
236
  if proxy_file_path:
231
237
  proxy = get_random_proxy_from_file(proxy_file_path)
232
-
238
+
233
239
  # Only pass proxy to client if it's valid
234
240
  client_args = {"headers": headers, "verify": ca_certs_path}
235
241
  if proxy is not None:
@@ -1,24 +1,28 @@
1
1
  from typing import Any, List
2
2
 
3
- from code_puppy.tools.common import console
4
3
  from code_puppy.message_history_processor import message_history_processor
5
4
 
6
5
  _message_history: List[Any] = []
7
6
 
7
+
8
8
  def get_message_history() -> List[Any]:
9
9
  return _message_history
10
10
 
11
+
11
12
  def set_message_history(history: List[Any]) -> None:
12
13
  global _message_history
13
14
  _message_history = history
14
15
 
16
+
15
17
  def clear_message_history() -> None:
16
18
  global _message_history
17
19
  _message_history = []
18
20
 
21
+
19
22
  def append_to_message_history(message: Any) -> None:
20
23
  _message_history.append(message)
21
24
 
25
+
22
26
  def extend_message_history(history: List[Any]) -> None:
23
27
  _message_history.extend(history)
24
28
 
@@ -37,18 +41,18 @@ def hash_message(message):
37
41
 
38
42
  def message_history_accumulator(messages: List[Any]):
39
43
  global _message_history
40
-
44
+
41
45
  message_history_hashes = set([hash_message(m) for m in _message_history])
42
46
  for msg in messages:
43
47
  if hash_message(msg) not in message_history_hashes:
44
48
  _message_history.append(msg)
45
-
49
+
46
50
  # Apply message history trimming using the main processor
47
51
  # This ensures we maintain global state while still managing context limits
48
52
  trimmed_messages = message_history_processor(_message_history)
49
-
53
+
50
54
  # Update our global state with the trimmed version
51
55
  # This preserves the state but keeps us within token limits
52
56
  _message_history = trimmed_messages
53
-
57
+
54
58
  return _message_history
@@ -1,9 +1,7 @@
1
1
  import os
2
2
  from pathlib import Path
3
3
 
4
- import pydantic
5
4
  from pydantic_ai import Agent
6
- from pydantic_ai.mcp import MCPServerSSE
7
5
 
8
6
  from code_puppy.model_factory import ModelFactory
9
7
  from code_puppy.tools.common import console
@@ -33,7 +31,7 @@ def reload_summarization_agent():
33
31
  else Path(__file__).parent / "models.json"
34
32
  )
35
33
  model = ModelFactory.get_model(model_name, ModelFactory.load_config(models_path))
36
-
34
+
37
35
  # Specialized instructions for summarization
38
36
  instructions = """You are a message summarization expert. Your task is to summarize conversation messages
39
37
  while preserving important context and information. The summaries should be concise but capture the essential
@@ -51,7 +49,7 @@ When summarizing:
51
49
  model=model,
52
50
  instructions=instructions,
53
51
  output_type=str,
54
- retries=1 # Fewer retries for summarization
52
+ retries=1, # Fewer retries for summarization
55
53
  )
56
54
  _summarization_agent = agent
57
55
  _LAST_MODEL_NAME = model_name
@@ -1,4 +1,7 @@
1
- from code_puppy.tools.command_runner import register_command_runner_tools
1
+ from code_puppy.tools.command_runner import (
2
+ register_command_runner_tools,
3
+ kill_all_running_shell_processes,
4
+ )
2
5
  from code_puppy.tools.file_modifications import register_file_modifications_tools
3
6
  from code_puppy.tools.file_operations import register_file_operations_tools
4
7
 
@@ -1,14 +1,117 @@
1
+ import os
2
+ import signal
1
3
  import subprocess
4
+ import threading
2
5
  import time
3
- from typing import Any, Dict
6
+ import traceback
7
+ import sys
8
+ from typing import Set
4
9
 
5
10
  from pydantic import BaseModel
6
11
  from pydantic_ai import RunContext
7
12
  from rich.markdown import Markdown
8
- from rich.syntax import Syntax
13
+ from rich.text import Text
9
14
 
10
15
  from code_puppy.tools.common import console
11
16
 
17
+ _AWAITING_USER_INPUT = False
18
+
19
+ _CONFIRMATION_LOCK = threading.Lock()
20
+
21
+ # Track running shell processes so we can kill them on Ctrl-C from the UI
22
+ _RUNNING_PROCESSES: Set[subprocess.Popen] = set()
23
+ _RUNNING_PROCESSES_LOCK = threading.Lock()
24
+ _USER_KILLED_PROCESSES = set()
25
+
26
+ def _register_process(proc: subprocess.Popen) -> None:
27
+ with _RUNNING_PROCESSES_LOCK:
28
+ _RUNNING_PROCESSES.add(proc)
29
+
30
+
31
+ def _unregister_process(proc: subprocess.Popen) -> None:
32
+ with _RUNNING_PROCESSES_LOCK:
33
+ _RUNNING_PROCESSES.discard(proc)
34
+
35
+
36
+ def _kill_process_group(proc: subprocess.Popen) -> None:
37
+ """Attempt to aggressively terminate a process and its group.
38
+
39
+ Cross-platform best-effort. On POSIX, uses process groups. On Windows, tries CTRL_BREAK_EVENT, then terminate().
40
+ """
41
+ try:
42
+ if sys.platform.startswith("win"):
43
+ try:
44
+ # Try a soft break first if the group exists
45
+ proc.send_signal(signal.CTRL_BREAK_EVENT) # type: ignore[attr-defined]
46
+ time.sleep(0.8)
47
+ except Exception:
48
+ pass
49
+ if proc.poll() is None:
50
+ try:
51
+ proc.terminate()
52
+ time.sleep(0.8)
53
+ except Exception:
54
+ pass
55
+ if proc.poll() is None:
56
+ try:
57
+ proc.kill()
58
+ except Exception:
59
+ pass
60
+ return
61
+
62
+ # POSIX
63
+ pid = proc.pid
64
+ try:
65
+ pgid = os.getpgid(pid)
66
+ os.killpg(pgid, signal.SIGTERM)
67
+ time.sleep(1.0)
68
+ if proc.poll() is None:
69
+ os.killpg(pgid, signal.SIGINT)
70
+ time.sleep(0.6)
71
+ if proc.poll() is None:
72
+ os.killpg(pgid, signal.SIGKILL)
73
+ time.sleep(0.5)
74
+ except (OSError, ProcessLookupError):
75
+ # Fall back to direct kill of the process
76
+ try:
77
+ if proc.poll() is None:
78
+ proc.kill()
79
+ except (OSError, ProcessLookupError):
80
+ pass
81
+
82
+ if proc.poll() is None:
83
+ # Last ditch attempt; may be unkillable zombie
84
+ try:
85
+ for _ in range(3):
86
+ os.kill(proc.pid, signal.SIGKILL)
87
+ time.sleep(0.2)
88
+ if proc.poll() is not None:
89
+ break
90
+ except Exception:
91
+ pass
92
+ except Exception as e:
93
+ console.print(f"Kill process error: {e}")
94
+
95
+
96
+ def kill_all_running_shell_processes() -> int:
97
+ """Kill all currently tracked running shell processes.
98
+
99
+ Returns the number of processes signaled.
100
+ """
101
+ procs: list[subprocess.Popen]
102
+ with _RUNNING_PROCESSES_LOCK:
103
+ procs = list(_RUNNING_PROCESSES)
104
+ count = 0
105
+ for p in procs:
106
+ try:
107
+ if p.poll() is None:
108
+ _kill_process_group(p)
109
+ count += 1
110
+ _USER_KILLED_PROCESSES.add(p.pid)
111
+ finally:
112
+ _unregister_process(p)
113
+ return count
114
+
12
115
 
13
116
  class ShellCommandOutput(BaseModel):
14
117
  success: bool
@@ -19,35 +122,250 @@ class ShellCommandOutput(BaseModel):
19
122
  exit_code: int | None
20
123
  execution_time: float | None
21
124
  timeout: bool | None = False
125
+ user_interrupted: bool | None = False
126
+
127
+
128
+ def run_shell_command_streaming(
129
+ process: subprocess.Popen, timeout: int = 60, command: str = ""
130
+ ):
131
+ start_time = time.time()
132
+ last_output_time = [start_time]
133
+
134
+ ABSOLUTE_TIMEOUT_SECONDS = 270
135
+
136
+ stdout_lines = []
137
+ stderr_lines = []
138
+
139
+ stdout_thread = None
140
+ stderr_thread = None
141
+
142
+ def read_stdout():
143
+ try:
144
+ for line in iter(process.stdout.readline, ""):
145
+ if line:
146
+ line = line.rstrip("\n\r")
147
+ stdout_lines.append(line)
148
+ console.log(line)
149
+ last_output_time[0] = time.time()
150
+ except Exception:
151
+ pass
152
+
153
+ def read_stderr():
154
+ try:
155
+ for line in iter(process.stderr.readline, ""):
156
+ if line:
157
+ line = line.rstrip("\n\r")
158
+ stderr_lines.append(line)
159
+ console.log(line)
160
+ last_output_time[0] = time.time()
161
+ except Exception:
162
+ pass
163
+
164
+ def cleanup_process_and_threads(timeout_type: str = "unknown"):
165
+ nonlocal stdout_thread, stderr_thread
166
+
167
+ def nuclear_kill(proc):
168
+ _kill_process_group(proc)
169
+
170
+ try:
171
+ if process.poll() is None:
172
+ nuclear_kill(process)
173
+
174
+ try:
175
+ if process.stdout and not process.stdout.closed:
176
+ process.stdout.close()
177
+ if process.stderr and not process.stderr.closed:
178
+ process.stderr.close()
179
+ if process.stdin and not process.stdin.closed:
180
+ process.stdin.close()
181
+ except (OSError, ValueError):
182
+ pass
183
+
184
+ # Unregister once we're done cleaning up
185
+ _unregister_process(process)
186
+
187
+ if stdout_thread and stdout_thread.is_alive():
188
+ stdout_thread.join(timeout=3)
189
+ if stdout_thread.is_alive():
190
+ console.print(
191
+ f"stdout reader thread failed to terminate after {timeout_type} seconds"
192
+ )
193
+
194
+ if stderr_thread and stderr_thread.is_alive():
195
+ stderr_thread.join(timeout=3)
196
+ if stderr_thread.is_alive():
197
+ console.print(
198
+ f"stderr reader thread failed to terminate after {timeout_type} seconds"
199
+ )
200
+
201
+ except Exception as e:
202
+ console.log(f"Error during process cleanup {e}")
203
+
204
+ execution_time = time.time() - start_time
205
+ return ShellCommandOutput(
206
+ **{
207
+ "success": False,
208
+ "command": command,
209
+ "stdout": "\n".join(stdout_lines[-1000:]),
210
+ "stderr": "\n".join(stderr_lines[-1000:]),
211
+ "exit_code": -9,
212
+ "execution_time": execution_time,
213
+ "timeout": True,
214
+ "error": f"Command timed out after {timeout} seconds",
215
+ }
216
+ )
217
+
218
+ try:
219
+ stdout_thread = threading.Thread(target=read_stdout, daemon=True)
220
+ stderr_thread = threading.Thread(target=read_stderr, daemon=True)
221
+
222
+ stdout_thread.start()
223
+ stderr_thread.start()
224
+
225
+ while process.poll() is None:
226
+ current_time = time.time()
227
+
228
+ if current_time - start_time > ABSOLUTE_TIMEOUT_SECONDS:
229
+ error_msg = Text()
230
+ error_msg.append(
231
+ "Process killed: inactivity timeout reached", style="bold red"
232
+ )
233
+ console.print(error_msg)
234
+ return cleanup_process_and_threads("absolute")
235
+
236
+ if current_time - last_output_time[0] > timeout:
237
+ error_msg = Text()
238
+ error_msg.append(
239
+ "Process killed: inactivity timeout reached", style="bold red"
240
+ )
241
+ console.print(error_msg)
242
+ return cleanup_process_and_threads("inactivity")
243
+
244
+ time.sleep(0.1)
245
+
246
+ if stdout_thread:
247
+ stdout_thread.join(timeout=5)
248
+ if stderr_thread:
249
+ stderr_thread.join(timeout=5)
250
+
251
+ exit_code = process.returncode
252
+ execution_time = time.time() - start_time
253
+
254
+ try:
255
+ if process.stdout and not process.stdout.closed:
256
+ process.stdout.close()
257
+ if process.stderr and not process.stderr.closed:
258
+ process.stderr.close()
259
+ if process.stdin and not process.stdin.closed:
260
+ process.stdin.close()
261
+ except (OSError, ValueError):
262
+ pass
263
+
264
+ _unregister_process(process)
265
+
266
+ if exit_code != 0:
267
+ console.print(
268
+ f"Command failed with exit code {exit_code}", style="bold red"
269
+ )
270
+ console.print(f"Took {execution_time:.2f}s", style="dim")
271
+ time.sleep(1)
272
+ return ShellCommandOutput(
273
+ success=False,
274
+ command=command,
275
+ error="""The process didn't exit cleanly! If the user_interrupted flag is true,
276
+ please stop all execution and ask the user for clarification!""",
277
+ stdout="\n".join(stdout_lines[-1000:]),
278
+ stderr="\n".join(stderr_lines[-1000:]),
279
+ exit_code=exit_code,
280
+ execution_time=execution_time,
281
+ timeout=False,
282
+ user_interrupted=process.pid in _USER_KILLED_PROCESSES
283
+ )
284
+ return ShellCommandOutput(
285
+ success=exit_code == 0,
286
+ command=command,
287
+ stdout="\n".join(stdout_lines[-1000:]),
288
+ stderr="\n".join(stderr_lines[-1000:]),
289
+ exit_code=exit_code,
290
+ execution_time=execution_time,
291
+ timeout=False,
292
+ )
293
+
294
+ except Exception as e:
295
+ return ShellCommandOutput(
296
+ success=False,
297
+ command=command,
298
+ error=f"Error durign streaming execution {str(e)}",
299
+ stdout="\n".join(stdout_lines[-1000:]),
300
+ stderr="\n".join(stderr_lines[-1000:]),
301
+ exit_code=-1,
302
+ timeout=False,
303
+ )
304
+
22
305
 
23
306
  def run_shell_command(
24
307
  context: RunContext, command: str, cwd: str = None, timeout: int = 60
25
308
  ) -> ShellCommandOutput:
309
+ command_displayed = False
26
310
  if not command or not command.strip():
27
311
  console.print("[bold red]Error:[/bold red] Command cannot be empty")
28
- return ShellCommandOutput(**{"success": False, "error": "Command cannot be empty"})
312
+ return ShellCommandOutput(
313
+ **{"success": False, "error": "Command cannot be empty"}
314
+ )
29
315
  console.print(
30
316
  f"\n[bold white on blue] SHELL COMMAND [/bold white on blue] \U0001f4c2 [bold green]$ {command}[/bold green]"
31
317
  )
32
- if cwd:
33
- console.print(f"[dim]Working directory: {cwd}[/dim]")
34
- console.print("[dim]" + "-" * 60 + "[/dim]")
35
318
  from code_puppy.config import get_yolo_mode
36
319
 
37
320
  yolo_mode = get_yolo_mode()
38
- if not yolo_mode:
39
- user_input = input("Are you sure you want to run this command? (yes/no): ")
40
- if user_input.strip().lower() not in {"yes", "y"}:
41
- console.print(
42
- "[bold yellow]Command execution canceled by user.[/bold yellow]"
321
+
322
+ confirmation_lock_acquired = False
323
+
324
+ # Only ask for confirmation if we're in an interactive TTY and not in yolo mode.
325
+ if not yolo_mode and sys.stdin.isatty():
326
+ confirmation_lock_acquired = _CONFIRMATION_LOCK.acquire(blocking=False)
327
+ if not confirmation_lock_acquired:
328
+ return ShellCommandOutput(
329
+ success=False,
330
+ command=command,
331
+ error="Another command is currently awaiting confirmation",
43
332
  )
44
- return ShellCommandOutput(**{
45
- "success": False,
46
- "command": command,
47
- "error": "User canceled command execution",
48
- })
49
- try:
333
+
334
+ command_displayed = True
335
+
336
+ if cwd:
337
+ console.print(f"[dim] Working directory: {cwd} [/dim]")
338
+ time.sleep(0.2)
339
+ sys.stdout.write("Are you sure you want to run this command? (y(es)/n(o))\n")
340
+ sys.stdout.flush()
341
+
342
+ try:
343
+ user_input = input()
344
+ confirmed = user_input.strip().lower() in {"yes", "y"}
345
+ except (KeyboardInterrupt, EOFError):
346
+ console.print("\n Cancelled by user")
347
+ confirmed = False
348
+ finally:
349
+ if confirmation_lock_acquired:
350
+ _CONFIRMATION_LOCK.release()
351
+
352
+ if not confirmed:
353
+ result = ShellCommandOutput(
354
+ success=False, command=command, error="User rejected the command!"
355
+ )
356
+ return result
357
+ else:
50
358
  start_time = time.time()
359
+ try:
360
+ creationflags = 0
361
+ preexec_fn = None
362
+ if sys.platform.startswith("win"):
363
+ try:
364
+ creationflags = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined]
365
+ except Exception:
366
+ creationflags = 0
367
+ else:
368
+ preexec_fn = os.setsid if hasattr(os, "setsid") else None
51
369
  process = subprocess.Popen(
52
370
  command,
53
371
  shell=True,
@@ -55,112 +373,33 @@ def run_shell_command(
55
373
  stderr=subprocess.PIPE,
56
374
  text=True,
57
375
  cwd=cwd,
376
+ bufsize=1,
377
+ universal_newlines=True,
378
+ preexec_fn=preexec_fn,
379
+ creationflags=creationflags,
58
380
  )
381
+ _register_process(process)
59
382
  try:
60
- stdout, stderr = process.communicate(timeout=timeout)
61
- exit_code = process.returncode
62
- execution_time = time.time() - start_time
63
- if stdout.strip():
64
- console.print("[bold white]STDOUT:[/bold white]")
65
- console.print(
66
- Syntax(
67
- stdout.strip(),
68
- "bash",
69
- theme="monokai",
70
- background_color="default",
71
- )
72
- )
73
- else:
74
- console.print("[yellow]No STDOUT output[/yellow]")
75
- if stderr.strip():
76
- console.print("[bold yellow]STDERR:[/bold yellow]")
77
- console.print(
78
- Syntax(
79
- stderr.strip(),
80
- "bash",
81
- theme="monokai",
82
- background_color="default",
83
- )
84
- )
85
- if exit_code == 0:
86
- console.print(
87
- f"[bold green]✓ Command completed successfully[/bold green] [dim](took {execution_time:.2f}s)[/dim]"
88
- )
89
- else:
90
- console.print(
91
- f"[bold red]✗ Command failed with exit code {exit_code}[/bold red] [dim](took {execution_time:.2f}s)[/dim]"
92
- )
93
- if not stdout.strip() and not stderr.strip():
94
- console.print(
95
- "[bold yellow]This command produced no output at all![/bold yellow]"
96
- )
97
- console.print("[dim]" + "-" * 60 + "[/dim]\n")
98
- return ShellCommandOutput(**{
99
- "success": exit_code == 0,
100
- "command": command,
101
- "stdout": stdout,
102
- "stderr": stderr,
103
- "exit_code": exit_code,
104
- "execution_time": execution_time,
105
- "timeout": False,
106
- })
107
- except subprocess.TimeoutExpired:
108
- process.kill()
109
- stdout, stderr = process.communicate()
110
- execution_time = time.time() - start_time
111
- if stdout.strip():
112
- console.print(
113
- "[bold white]STDOUT (incomplete due to timeout):[/bold white]"
114
- )
115
- console.print(
116
- Syntax(
117
- stdout.strip(),
118
- "bash",
119
- theme="monokai",
120
- background_color="default",
121
- )
122
- )
123
- if stderr.strip():
124
- console.print("[bold yellow]STDERR:[/bold yellow]")
125
- console.print(
126
- Syntax(
127
- stderr.strip(),
128
- "bash",
129
- theme="monokai",
130
- background_color="default",
131
- )
132
- )
133
- console.print(
134
- f"[bold red]⏱ Command timed out after {timeout} seconds[/bold red] [dim](ran for {execution_time:.2f}s)[/dim]"
135
- )
136
- console.print("[dim]" + "-" * 60 + "[/dim]\n")
137
- return ShellCommandOutput(**{
138
- "success": False,
139
- "command": command,
140
- "stdout": stdout[-1000:],
141
- "stderr": stderr[-1000:],
142
- "exit_code": None,
143
- "execution_time": execution_time,
144
- "timeout": True,
145
- "error": f"Command timed out after {timeout} seconds",
146
- })
383
+ return run_shell_command_streaming(process, timeout=timeout, command=command)
384
+ finally:
385
+ # Ensure unregistration in case streaming returned early or raised
386
+ _unregister_process(process)
147
387
  except Exception as e:
148
- console.print_exception(show_locals=True)
149
- console.print("[dim]" + "-" * 60 + "[/dim]\n")
150
- # Ensure stdout and stderr are always defined
388
+ console.print(traceback.format_exc())
151
389
  if "stdout" not in locals():
152
390
  stdout = None
153
391
  if "stderr" not in locals():
154
392
  stderr = None
155
- return ShellCommandOutput(**{
156
- "success": False,
157
- "command": command,
158
- "error": f"Error executing command: {str(e)}",
159
- "stdout": stdout[-1000:] if stdout else None,
160
- "stderr": stderr[-1000:] if stderr else None,
161
- "exit_code": -1,
162
- "timeout": False,
163
- })
393
+ return ShellCommandOutput(
394
+ success=False,
395
+ command=command,
396
+ error=f"Error executing command {str(e)}",
397
+ stdout="\n".join(stdout[-1000:]) if stdout else None,
398
+ stderr="\n".join(stderr[-1000:]) if stderr else None,
399
+ exit_code=-1,
400
+ timeout=False,
401
+ )
402
+
164
403
 
165
404
  class ReasoningOutput(BaseModel):
166
405
  success: bool = True
@@ -378,7 +378,9 @@ def register_file_modifications_tools(agent):
378
378
  """Attach file-editing tools to *agent* with mandatory diff rendering."""
379
379
 
380
380
  @agent.tool(retries=5)
381
- def edit_file(context: RunContext, path: str = "", diff: str = "") -> EditFileOutput:
381
+ def edit_file(
382
+ context: RunContext, path: str = "", diff: str = ""
383
+ ) -> EditFileOutput:
382
384
  return EditFileOutput(**_edit_file(context, path, diff))
383
385
 
384
386
  @agent.tool(retries=5)
@@ -1,9 +1,9 @@
1
1
  # file_operations.py
2
2
 
3
3
  import os
4
- from typing import Any, Dict, List
4
+ from typing import List
5
5
 
6
- from pydantic import BaseModel, StrictStr, StrictInt
6
+ from pydantic import BaseModel
7
7
  from pydantic_ai import RunContext
8
8
 
9
9
  from code_puppy.tools.common import console
@@ -41,11 +41,15 @@ def _list_files(
41
41
  f"[bold red]Error:[/bold red] Directory '{directory}' does not exist"
42
42
  )
43
43
  console.print("[dim]" + "-" * 60 + "[/dim]\n")
44
- return ListFileOutput(files=[ListedFile(path=None, type=None, full_path=None, depth=None)])
44
+ return ListFileOutput(
45
+ files=[ListedFile(path=None, type=None, full_path=None, depth=None)]
46
+ )
45
47
  if not os.path.isdir(directory):
46
48
  console.print(f"[bold red]Error:[/bold red] '{directory}' is not a directory")
47
49
  console.print("[dim]" + "-" * 60 + "[/dim]\n")
48
- return ListFileOutput(files=[ListedFile(path=None, type=None, full_path=None, depth=None)])
50
+ return ListFileOutput(
51
+ files=[ListedFile(path=None, type=None, full_path=None, depth=None)]
52
+ )
49
53
  folder_structure = {}
50
54
  file_list = []
51
55
  for root, dirs, files in os.walk(directory):
@@ -57,13 +61,15 @@ def _list_files(
57
61
  if rel_path:
58
62
  dir_path = os.path.join(directory, rel_path)
59
63
  results.append(
60
- ListedFile(**{
61
- "path": rel_path,
62
- "type": "directory",
63
- "size": 0,
64
- "full_path": dir_path,
65
- "depth": depth,
66
- })
64
+ ListedFile(
65
+ **{
66
+ "path": rel_path,
67
+ "type": "directory",
68
+ "size": 0,
69
+ "full_path": dir_path,
70
+ "depth": depth,
71
+ }
72
+ )
67
73
  )
68
74
  folder_structure[rel_path] = {
69
75
  "path": rel_path,
@@ -131,9 +137,7 @@ def _list_files(
131
137
  return "\U0001f4c4"
132
138
 
133
139
  if results:
134
- files = sorted(
135
- [f for f in results if f.type == "file"], key=lambda x: x.path
136
- )
140
+ files = sorted([f for f in results if f.type == "file"], key=lambda x: x.path)
137
141
  console.print(
138
142
  f"\U0001f4c1 [bold blue]{os.path.basename(directory) or directory}[/bold blue]"
139
143
  )
@@ -177,6 +181,7 @@ def _list_files(
177
181
  class ReadFileOutput(BaseModel):
178
182
  content: str | None
179
183
 
184
+
180
185
  def _read_file(context: RunContext, file_path: str) -> ReadFileOutput:
181
186
  file_path = os.path.abspath(file_path)
182
187
  console.print(
@@ -191,7 +196,7 @@ def _read_file(context: RunContext, file_path: str) -> ReadFileOutput:
191
196
  with open(file_path, "r", encoding="utf-8") as f:
192
197
  content = f.read()
193
198
  return ReadFileOutput(content=content)
194
- except Exception as exc:
199
+ except Exception:
195
200
  return ReadFileOutput(content="FILE NOT FOUND")
196
201
 
197
202
 
@@ -200,12 +205,12 @@ class MatchInfo(BaseModel):
200
205
  line_number: int | None
201
206
  line_content: str | None
202
207
 
208
+
203
209
  class GrepOutput(BaseModel):
204
210
  matches: List[MatchInfo]
205
211
 
206
- def _grep(
207
- context: RunContext, search_string: str, directory: str = "."
208
- ) -> GrepOutput:
212
+
213
+ def _grep(context: RunContext, search_string: str, directory: str = ".") -> GrepOutput:
209
214
  matches: List[MatchInfo] = []
210
215
  directory = os.path.abspath(directory)
211
216
  console.print(
@@ -229,11 +234,13 @@ def _grep(
229
234
  with open(file_path, "r", encoding="utf-8", errors="ignore") as fh:
230
235
  for line_number, line_content in enumerate(fh, 1):
231
236
  if search_string in line_content:
232
- match_info = MatchInfo(**{
233
- "file_path": file_path,
234
- "line_number": line_number,
235
- "line_content": line_content.strip(),
236
- })
237
+ match_info = MatchInfo(
238
+ **{
239
+ "file_path": file_path,
240
+ "line_number": line_number,
241
+ "line_content": line_content.strip(),
242
+ }
243
+ )
237
244
  matches.append(match_info)
238
245
  # console.print(
239
246
  # f"[green]Match:[/green] {file_path}:{line_number} - {line_content.strip()}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.84
3
+ Version: 0.0.85
4
4
  Summary: Code generation agent
5
5
  Author: Michael Pfaffenberger
6
6
  License: MIT
@@ -0,0 +1,30 @@
1
+ code_puppy/__init__.py,sha256=CWH46ZAmJRmHAbOiAhG07OrWYEcEt4yvDTkZU341Wag,169
2
+ code_puppy/agent.py,sha256=7_1FpGPnw8U632OXP0hLmFIozfVvllF491q8gCpaa8c,3284
3
+ code_puppy/agent_prompts.py,sha256=wTah_TvakCMhkb_KwuWCsw4_UR1QsjTZeOT1I8at_nc,6593
4
+ code_puppy/config.py,sha256=r5nw5ChOP8xd_K5yo8U5OtO2gy2bFhARiyNtDp1JrwQ,5013
5
+ code_puppy/main.py,sha256=qtBokZZfPQRGF91KNl_n_ywutONwF2f-zTwmt_ROsEU,11080
6
+ code_puppy/message_history_processor.py,sha256=o5RNa-i7q_btYWJVaJFhRB2np58kObIDhQW2hZSRiKw,9244
7
+ code_puppy/model_factory.py,sha256=HXuFHNkVjkCcorAd3ScFmSvBILO932UTq6OmNAqisT8,10898
8
+ code_puppy/models.json,sha256=jr0-LW87aJS79GosVwoZdHeeq5eflPzgdPoMbcqpVA8,2728
9
+ code_puppy/state_management.py,sha256=JkTkmq6f9rl_RHPDoBqJvbAzgaMsIkJf-k38ragItIo,1692
10
+ code_puppy/summarization_agent.py,sha256=jHUQe6iYJsMT0ywEwO7CrhUIKEamO5imhAsDwvNuvow,2684
11
+ code_puppy/version_checker.py,sha256=aRGulzuY4C4CdFvU1rITduyL-1xTFsn4GiD1uSfOl_Y,396
12
+ code_puppy/command_line/__init__.py,sha256=y7WeRemfYppk8KVbCGeAIiTuiOszIURCDjOMZv_YRmU,45
13
+ code_puppy/command_line/file_path_completion.py,sha256=gw8NpIxa6GOpczUJRyh7VNZwoXKKn-yvCqit7h2y6Gg,2931
14
+ code_puppy/command_line/meta_command_handler.py,sha256=L7qP2g0Faz0V7bMH4YK3s03OWWuQFtK7Sh-Kt2zmmEQ,6182
15
+ code_puppy/command_line/model_picker_completion.py,sha256=NkyZZG7IhcVWSJ3ADytwCA5f8DpNeVs759Qtqs4fQtY,3733
16
+ code_puppy/command_line/motd.py,sha256=FoZsiVpXGF8WpAmEJX4O895W7MDuzCtNWvFAOShxUXY,1572
17
+ code_puppy/command_line/prompt_toolkit_completion.py,sha256=_gP0FIOgHDNHTTWLNL0XNzr6sO0ISe7Mec1uQNo9kcM,8337
18
+ code_puppy/command_line/utils.py,sha256=7eyxDHjPjPB9wGDJQQcXV_zOsGdYsFgI0SGCetVmTqE,1251
19
+ code_puppy/tools/__init__.py,sha256=WTHYIfRk2KMmk6o45TELpbB3GIiAm8s7GmfJ7Zy_tww,503
20
+ code_puppy/tools/command_runner.py,sha256=iYXAauCmntHgRcPOoMavslQ7oVFQhL0hYmjVUu6ezpk,14354
21
+ code_puppy/tools/common.py,sha256=M53zhiXZAmPdvi1Y_bzCxgvEmifOvRRJvYPARYRZqHw,2253
22
+ code_puppy/tools/file_modifications.py,sha256=BzQrGEacS2NZr2ru9N30x_Qd70JDudBKOAPO1XjBohg,13861
23
+ code_puppy/tools/file_operations.py,sha256=ypk4yL90LDSVRr0xyWafttzt956J_nXhhenCXhOOit8,11326
24
+ code_puppy/tools/ts_code_map.py,sha256=o-u8p5vsYwitfDtVEoPS-7MwWn2xHzwtIQLo1_WMhQs,17647
25
+ code_puppy-0.0.85.data/data/code_puppy/models.json,sha256=jr0-LW87aJS79GosVwoZdHeeq5eflPzgdPoMbcqpVA8,2728
26
+ code_puppy-0.0.85.dist-info/METADATA,sha256=mGczXN9Szot3pRImK59UT27eZq4apINNkrZxBTzUY6w,6351
27
+ code_puppy-0.0.85.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
28
+ code_puppy-0.0.85.dist-info/entry_points.txt,sha256=d8YkBvIUxF-dHNJAj-x4fPEqizbY5d_TwvYpc01U5kw,58
29
+ code_puppy-0.0.85.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
30
+ code_puppy-0.0.85.dist-info/RECORD,,
@@ -1,30 +0,0 @@
1
- code_puppy/__init__.py,sha256=oDE4GhaqOHsYi9XCGp6A2-PqhDqxJiYP_XmxmoKWoPU,168
2
- code_puppy/agent.py,sha256=7On9mo4RWRaoINJr4Rv4c7Ot751FdQfSnTg0grrxzp8,3283
3
- code_puppy/agent_prompts.py,sha256=13YIpTZa3R3lg60-fdkll7t7hgSBtQL0M53wcE1gzyQ,6834
4
- code_puppy/config.py,sha256=r5nw5ChOP8xd_K5yo8U5OtO2gy2bFhARiyNtDp1JrwQ,5013
5
- code_puppy/main.py,sha256=WL1EGw86u_yQJDrkvnZbabldRKcYgFMuWQPQDz8zUgY,10428
6
- code_puppy/message_history_processor.py,sha256=QOAQqxOJ2MSe6sTnZ6F4PMBqrtmV8RBQ0LmwQKh-o1A,6158
7
- code_puppy/model_factory.py,sha256=3j7AcJfZAHbx_plL9oOxjGJO0MMTRaQFThCErg8VpH8,10909
8
- code_puppy/models.json,sha256=jr0-LW87aJS79GosVwoZdHeeq5eflPzgdPoMbcqpVA8,2728
9
- code_puppy/state_management.py,sha256=1QycApDBbXjayxXsYRecJib8TQ-MYMTeYvN5P_1Ipdg,1747
10
- code_puppy/summarization_agent.py,sha256=N1UZg_R3wJFb7ZdVexDqx7L_8yxQ5m5nMOwGsLNfvKM,2744
11
- code_puppy/version_checker.py,sha256=aRGulzuY4C4CdFvU1rITduyL-1xTFsn4GiD1uSfOl_Y,396
12
- code_puppy/command_line/__init__.py,sha256=y7WeRemfYppk8KVbCGeAIiTuiOszIURCDjOMZv_YRmU,45
13
- code_puppy/command_line/file_path_completion.py,sha256=gw8NpIxa6GOpczUJRyh7VNZwoXKKn-yvCqit7h2y6Gg,2931
14
- code_puppy/command_line/meta_command_handler.py,sha256=L7qP2g0Faz0V7bMH4YK3s03OWWuQFtK7Sh-Kt2zmmEQ,6182
15
- code_puppy/command_line/model_picker_completion.py,sha256=NkyZZG7IhcVWSJ3ADytwCA5f8DpNeVs759Qtqs4fQtY,3733
16
- code_puppy/command_line/motd.py,sha256=7ICNgfL4EgSrmCAHIsCK72R19obSQXkK8l7XGJBkvrQ,1571
17
- code_puppy/command_line/prompt_toolkit_completion.py,sha256=_gP0FIOgHDNHTTWLNL0XNzr6sO0ISe7Mec1uQNo9kcM,8337
18
- code_puppy/command_line/utils.py,sha256=7eyxDHjPjPB9wGDJQQcXV_zOsGdYsFgI0SGCetVmTqE,1251
19
- code_puppy/tools/__init__.py,sha256=ozIGpLM7pKSjH4UeojkTodhfVYZeNzMsLtK_oyw41HA,456
20
- code_puppy/tools/command_runner.py,sha256=NFCL35x44McMzSUNHQyg5q4Zx7wkvqD-nH4_YAU8N2s,7229
21
- code_puppy/tools/common.py,sha256=M53zhiXZAmPdvi1Y_bzCxgvEmifOvRRJvYPARYRZqHw,2253
22
- code_puppy/tools/file_modifications.py,sha256=nGI8gRD6Vtkg8EzBkErsv3khE3VI-_M1z_PdQLvjfLo,13847
23
- code_puppy/tools/file_operations.py,sha256=eftkN-MxsRGQc8c1iIoNmN5r-Ppld5YJRT7a89kxpkM,11207
24
- code_puppy/tools/ts_code_map.py,sha256=o-u8p5vsYwitfDtVEoPS-7MwWn2xHzwtIQLo1_WMhQs,17647
25
- code_puppy-0.0.84.data/data/code_puppy/models.json,sha256=jr0-LW87aJS79GosVwoZdHeeq5eflPzgdPoMbcqpVA8,2728
26
- code_puppy-0.0.84.dist-info/METADATA,sha256=NWnTj01yQNciyZLXZDYgWbU8St84wCea4wE4fLn27lE,6351
27
- code_puppy-0.0.84.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
28
- code_puppy-0.0.84.dist-info/entry_points.txt,sha256=d8YkBvIUxF-dHNJAj-x4fPEqizbY5d_TwvYpc01U5kw,58
29
- code_puppy-0.0.84.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
30
- code_puppy-0.0.84.dist-info/RECORD,,