code-puppy 0.0.83__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 +1 -0
- code_puppy/agent.py +8 -35
- code_puppy/agent_prompts.py +1 -3
- code_puppy/command_line/motd.py +1 -1
- code_puppy/main.py +24 -13
- code_puppy/message_history_processor.py +128 -36
- code_puppy/model_factory.py +20 -14
- code_puppy/state_management.py +9 -5
- code_puppy/summarization_agent.py +2 -4
- code_puppy/tools/__init__.py +4 -1
- code_puppy/tools/command_runner.py +355 -116
- code_puppy/tools/file_modifications.py +3 -1
- code_puppy/tools/file_operations.py +30 -23
- {code_puppy-0.0.83.dist-info → code_puppy-0.0.85.dist-info}/METADATA +1 -1
- code_puppy-0.0.85.dist-info/RECORD +30 -0
- code_puppy/session_memory.py +0 -83
- code_puppy-0.0.83.dist-info/RECORD +0 -31
- {code_puppy-0.0.83.data → code_puppy-0.0.85.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.83.dist-info → code_puppy-0.0.85.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.83.dist-info → code_puppy-0.0.85.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.83.dist-info → code_puppy-0.0.85.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,14 +1,117 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import signal
|
|
1
3
|
import subprocess
|
|
4
|
+
import threading
|
|
2
5
|
import time
|
|
3
|
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
})
|
|
49
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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(
|
|
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
|
|
4
|
+
from typing import List
|
|
5
5
|
|
|
6
|
-
from pydantic import BaseModel
|
|
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(
|
|
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(
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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()}"
|
|
@@ -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,,
|