patchpal 0.3.1__tar.gz → 0.3.2__tar.gz
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.
- {patchpal-0.3.1/patchpal.egg-info → patchpal-0.3.2}/PKG-INFO +1 -1
- {patchpal-0.3.1 → patchpal-0.3.2}/patchpal/__init__.py +1 -1
- {patchpal-0.3.1 → patchpal-0.3.2}/patchpal/agent.py +1 -1
- {patchpal-0.3.1 → patchpal-0.3.2}/patchpal/cli.py +49 -1
- {patchpal-0.3.1 → patchpal-0.3.2}/patchpal/tools.py +225 -22
- {patchpal-0.3.1 → patchpal-0.3.2/patchpal.egg-info}/PKG-INFO +1 -1
- {patchpal-0.3.1 → patchpal-0.3.2}/LICENSE +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/MANIFEST.in +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/README.md +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/patchpal/context.py +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/patchpal/permissions.py +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/patchpal/skills.py +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/patchpal/system_prompt.md +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/patchpal.egg-info/SOURCES.txt +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/patchpal.egg-info/dependency_links.txt +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/patchpal.egg-info/entry_points.txt +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/patchpal.egg-info/requires.txt +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/patchpal.egg-info/top_level.txt +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/pyproject.toml +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/setup.cfg +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/tests/test_agent.py +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/tests/test_cli.py +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/tests/test_context.py +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/tests/test_guardrails.py +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/tests/test_operational_safety.py +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/tests/test_skills.py +0 -0
- {patchpal-0.3.1 → patchpal-0.3.2}/tests/test_tools.py +0 -0
|
@@ -541,7 +541,7 @@ TOOLS = [
|
|
|
541
541
|
"type": "function",
|
|
542
542
|
"function": {
|
|
543
543
|
"name": "run_shell",
|
|
544
|
-
"description": "Run a safe shell command in the repository. Privilege escalation (sudo, su) blocked by default unless PATCHPAL_ALLOW_SUDO=true.",
|
|
544
|
+
"description": "Run a safe shell command in the repository. Commands execute from repository root automatically (no need for 'cd'). Privilege escalation (sudo, su) blocked by default unless PATCHPAL_ALLOW_SUDO=true.",
|
|
545
545
|
"parameters": {
|
|
546
546
|
"type": "object",
|
|
547
547
|
"properties": {
|
|
@@ -248,7 +248,9 @@ Supported models: Any LiteLLM-supported model
|
|
|
248
248
|
print(f"\033[1;36m🔧 Using custom system prompt: {custom_prompt_path}\033[0m")
|
|
249
249
|
|
|
250
250
|
print("\nType 'exit' to quit.")
|
|
251
|
-
print(
|
|
251
|
+
print(
|
|
252
|
+
"Use '/status' to check context window usage, '/compact' to manually compact, '/clear' to start fresh."
|
|
253
|
+
)
|
|
252
254
|
print("Use 'list skills' to see available skills or /skillname to invoke skills.")
|
|
253
255
|
print("Press Ctrl-C during agent execution to interrupt the agent.\n")
|
|
254
256
|
|
|
@@ -363,6 +365,52 @@ Supported models: Any LiteLLM-supported model
|
|
|
363
365
|
print("=" * 70 + "\n")
|
|
364
366
|
continue
|
|
365
367
|
|
|
368
|
+
# Handle /clear command - clear conversation history
|
|
369
|
+
if user_input.lower() in ["clear", "/clear"]:
|
|
370
|
+
print("\n" + "=" * 70)
|
|
371
|
+
print("\033[1;36mClear Context\033[0m")
|
|
372
|
+
print("=" * 70)
|
|
373
|
+
|
|
374
|
+
if not agent.messages:
|
|
375
|
+
print("\033[1;33m Context is already empty.\033[0m")
|
|
376
|
+
print("=" * 70 + "\n")
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
# Show current status
|
|
380
|
+
stats = agent.context_manager.get_usage_stats(agent.messages)
|
|
381
|
+
print(
|
|
382
|
+
f" Current: {len(agent.messages)} messages, {stats['total_tokens']:,} tokens"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Confirm before clearing
|
|
386
|
+
try:
|
|
387
|
+
confirm = pt_prompt(
|
|
388
|
+
FormattedText(
|
|
389
|
+
[
|
|
390
|
+
("ansiyellow", " Clear all context and start fresh? (y/n): "),
|
|
391
|
+
("", ""),
|
|
392
|
+
]
|
|
393
|
+
)
|
|
394
|
+
).strip()
|
|
395
|
+
if confirm.lower() not in ["y", "yes"]:
|
|
396
|
+
print(" Cancelled.")
|
|
397
|
+
print("=" * 70 + "\n")
|
|
398
|
+
continue
|
|
399
|
+
except KeyboardInterrupt:
|
|
400
|
+
print("\n Cancelled.")
|
|
401
|
+
print("=" * 70 + "\n")
|
|
402
|
+
continue
|
|
403
|
+
|
|
404
|
+
# Clear conversation history
|
|
405
|
+
agent.messages = []
|
|
406
|
+
agent._last_compaction_message_count = 0
|
|
407
|
+
|
|
408
|
+
print("\n\033[1;32m✓ Context cleared successfully!\033[0m")
|
|
409
|
+
print(" Starting fresh with empty conversation history.")
|
|
410
|
+
print(" All previous context has been removed - ready for a new task.")
|
|
411
|
+
print("=" * 70 + "\n")
|
|
412
|
+
continue
|
|
413
|
+
|
|
366
414
|
# Handle /compact command - manually trigger compaction
|
|
367
415
|
if user_input.lower() in ["compact", "/compact"]:
|
|
368
416
|
print("\n" + "=" * 70)
|
|
@@ -100,6 +100,10 @@ WEB_USER_AGENT = f"PatchPal/{__version__} (AI Code Assistant)"
|
|
|
100
100
|
# Shell command configuration
|
|
101
101
|
SHELL_TIMEOUT = int(os.getenv("PATCHPAL_SHELL_TIMEOUT", 30)) # 30 seconds default
|
|
102
102
|
|
|
103
|
+
# Output filtering configuration - reduce token usage from verbose commands
|
|
104
|
+
ENABLE_OUTPUT_FILTERING = os.getenv("PATCHPAL_FILTER_OUTPUTS", "true").lower() == "true"
|
|
105
|
+
MAX_OUTPUT_LINES = int(os.getenv("PATCHPAL_MAX_OUTPUT_LINES", 500)) # Max lines of output
|
|
106
|
+
|
|
103
107
|
# Global flag for requiring permission on ALL operations (including reads)
|
|
104
108
|
# Set via CLI flag --require-permission-for-all
|
|
105
109
|
_REQUIRE_PERMISSION_FOR_ALL = False
|
|
@@ -195,10 +199,194 @@ class OperationLimiter:
|
|
|
195
199
|
audit_logger.info(f"Operation {self.operations}/{self.max_operations}: {operation}")
|
|
196
200
|
|
|
197
201
|
def reset(self):
|
|
198
|
-
"""Reset operation counter."""
|
|
202
|
+
"""Reset the operation counter (used in tests)."""
|
|
199
203
|
self.operations = 0
|
|
200
204
|
|
|
201
205
|
|
|
206
|
+
class OutputFilter:
|
|
207
|
+
"""Filter verbose command outputs to reduce token usage.
|
|
208
|
+
|
|
209
|
+
This class implements Claude Code's strategy of filtering verbose outputs
|
|
210
|
+
to show only relevant information (e.g., test failures, error messages).
|
|
211
|
+
Can save 75% or more on output tokens for verbose commands.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def should_filter(cmd: str) -> bool:
|
|
216
|
+
"""Check if a command should have its output filtered.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
cmd: The shell command
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
True if filtering should be applied
|
|
223
|
+
"""
|
|
224
|
+
if not ENABLE_OUTPUT_FILTERING:
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
# Test runners - show only failures
|
|
228
|
+
test_patterns = [
|
|
229
|
+
"pytest",
|
|
230
|
+
"npm test",
|
|
231
|
+
"npm run test",
|
|
232
|
+
"yarn test",
|
|
233
|
+
"go test",
|
|
234
|
+
"cargo test",
|
|
235
|
+
"mvn test",
|
|
236
|
+
"gradle test",
|
|
237
|
+
"ruby -I test",
|
|
238
|
+
"rspec",
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
# Version control - limit log output
|
|
242
|
+
vcs_patterns = [
|
|
243
|
+
"git log",
|
|
244
|
+
"git reflog",
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
# Package managers - show only important info
|
|
248
|
+
pkg_patterns = [
|
|
249
|
+
"npm install",
|
|
250
|
+
"pip install",
|
|
251
|
+
"cargo build",
|
|
252
|
+
"go build",
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
all_patterns = test_patterns + vcs_patterns + pkg_patterns
|
|
256
|
+
return any(pattern in cmd for pattern in all_patterns)
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
def filter_output(cmd: str, output: str) -> str:
|
|
260
|
+
"""Filter command output to reduce token usage.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
cmd: The shell command
|
|
264
|
+
output: The raw command output
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Filtered output with only relevant information
|
|
268
|
+
"""
|
|
269
|
+
if not output or not ENABLE_OUTPUT_FILTERING:
|
|
270
|
+
return output
|
|
271
|
+
|
|
272
|
+
lines = output.split("\n")
|
|
273
|
+
original_lines = len(lines)
|
|
274
|
+
|
|
275
|
+
# Test output - show only failures and summary
|
|
276
|
+
if any(
|
|
277
|
+
pattern in cmd
|
|
278
|
+
for pattern in ["pytest", "npm test", "yarn test", "go test", "cargo test", "rspec"]
|
|
279
|
+
):
|
|
280
|
+
filtered_lines = []
|
|
281
|
+
in_failure = False
|
|
282
|
+
failure_context = []
|
|
283
|
+
|
|
284
|
+
for line in lines:
|
|
285
|
+
# Capture failure indicators
|
|
286
|
+
if any(
|
|
287
|
+
keyword in line.upper()
|
|
288
|
+
for keyword in ["FAIL", "ERROR", "FAILED", "✗", "✖", "FAILURE"]
|
|
289
|
+
):
|
|
290
|
+
in_failure = True
|
|
291
|
+
failure_context = [line]
|
|
292
|
+
elif in_failure:
|
|
293
|
+
# Capture context after failure (up to 10 lines or until next test/blank line)
|
|
294
|
+
failure_context.append(line)
|
|
295
|
+
# End failure context on: blank line, next test case, or 10 lines
|
|
296
|
+
if (
|
|
297
|
+
not line.strip()
|
|
298
|
+
or "::" in line
|
|
299
|
+
or line.startswith("=")
|
|
300
|
+
or len(failure_context) >= 10
|
|
301
|
+
):
|
|
302
|
+
filtered_lines.extend(failure_context)
|
|
303
|
+
in_failure = False
|
|
304
|
+
failure_context = []
|
|
305
|
+
# Always capture summary lines
|
|
306
|
+
elif any(
|
|
307
|
+
keyword in line.lower()
|
|
308
|
+
for keyword in ["passed", "failed", "error", "summary", "total"]
|
|
309
|
+
):
|
|
310
|
+
filtered_lines.append(line)
|
|
311
|
+
|
|
312
|
+
# Add remaining failure context
|
|
313
|
+
if failure_context:
|
|
314
|
+
filtered_lines.extend(failure_context)
|
|
315
|
+
|
|
316
|
+
# If we filtered significantly, add header
|
|
317
|
+
if filtered_lines and len(filtered_lines) < original_lines * 0.5:
|
|
318
|
+
header = f"[Filtered test output - showing failures only ({len(filtered_lines)}/{original_lines} lines)]"
|
|
319
|
+
return header + "\n" + "\n".join(filtered_lines)
|
|
320
|
+
else:
|
|
321
|
+
# Not much to filter, return original but truncated if too long
|
|
322
|
+
return OutputFilter._truncate_output(output, lines, original_lines)
|
|
323
|
+
|
|
324
|
+
# Git log - limit to reasonable number of commits
|
|
325
|
+
elif "git log" in cmd or "git reflog" in cmd:
|
|
326
|
+
# Take first 50 lines (typically ~5-10 commits with details)
|
|
327
|
+
if len(lines) > 50:
|
|
328
|
+
truncated = "\n".join(lines[:50])
|
|
329
|
+
footer = f"\n[Output truncated: showing first 50/{original_lines} lines. Use --max-count to limit commits]"
|
|
330
|
+
return truncated + footer
|
|
331
|
+
return output
|
|
332
|
+
|
|
333
|
+
# Build/install output - show only errors and final status
|
|
334
|
+
elif any(
|
|
335
|
+
pattern in cmd for pattern in ["npm install", "pip install", "cargo build", "go build"]
|
|
336
|
+
):
|
|
337
|
+
filtered_lines = []
|
|
338
|
+
|
|
339
|
+
for line in lines:
|
|
340
|
+
# Keep error/warning lines
|
|
341
|
+
if any(
|
|
342
|
+
keyword in line.upper()
|
|
343
|
+
for keyword in ["ERROR", "WARN", "FAIL", "SUCCESSFULLY", "COMPLETE"]
|
|
344
|
+
):
|
|
345
|
+
filtered_lines.append(line)
|
|
346
|
+
# Keep final summary lines
|
|
347
|
+
elif any(
|
|
348
|
+
keyword in line.lower()
|
|
349
|
+
for keyword in ["installed", "built", "compiled", "finished"]
|
|
350
|
+
):
|
|
351
|
+
filtered_lines.append(line)
|
|
352
|
+
|
|
353
|
+
if filtered_lines and len(filtered_lines) < original_lines * 0.3:
|
|
354
|
+
header = f"[Filtered build output - showing errors and summary only ({len(filtered_lines)}/{original_lines} lines)]"
|
|
355
|
+
return header + "\n" + "\n".join(filtered_lines)
|
|
356
|
+
else:
|
|
357
|
+
return OutputFilter._truncate_output(output, lines, original_lines)
|
|
358
|
+
|
|
359
|
+
# Default: truncate if too long
|
|
360
|
+
return OutputFilter._truncate_output(output, lines, original_lines)
|
|
361
|
+
|
|
362
|
+
@staticmethod
|
|
363
|
+
def _truncate_output(output: str, lines: list, original_lines: int) -> str:
|
|
364
|
+
"""Truncate output if it exceeds maximum lines.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
output: Original output string
|
|
368
|
+
lines: Split lines
|
|
369
|
+
original_lines: Count of original lines
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Truncated output if necessary
|
|
373
|
+
"""
|
|
374
|
+
if original_lines > MAX_OUTPUT_LINES:
|
|
375
|
+
# Show first and last portions
|
|
376
|
+
keep_start = MAX_OUTPUT_LINES // 2
|
|
377
|
+
keep_end = MAX_OUTPUT_LINES // 2
|
|
378
|
+
|
|
379
|
+
truncated_lines = (
|
|
380
|
+
lines[:keep_start]
|
|
381
|
+
+ ["", f"... [truncated {original_lines - MAX_OUTPUT_LINES} lines] ...", ""]
|
|
382
|
+
+ lines[-keep_end:]
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
return "\n".join(truncated_lines)
|
|
386
|
+
|
|
387
|
+
return output
|
|
388
|
+
|
|
389
|
+
|
|
202
390
|
# Global operation limiter
|
|
203
391
|
_operation_limiter = OperationLimiter()
|
|
204
392
|
|
|
@@ -1738,26 +1926,7 @@ def edit_file(path: str, old_string: str, new_string: str) -> str:
|
|
|
1738
1926
|
f"💡 Tip: Use read_lines() to see the exact context, or use apply_patch() for multiple changes."
|
|
1739
1927
|
)
|
|
1740
1928
|
|
|
1741
|
-
#
|
|
1742
|
-
permission_manager = _get_permission_manager()
|
|
1743
|
-
|
|
1744
|
-
# Format colored diff for permission prompt (use the matched string for accurate diff)
|
|
1745
|
-
diff_display = _format_colored_diff(matched_string, new_string, file_path=path)
|
|
1746
|
-
|
|
1747
|
-
# Add warning if writing outside repository
|
|
1748
|
-
outside_repo_warning = ""
|
|
1749
|
-
if not _is_inside_repo(p):
|
|
1750
|
-
outside_repo_warning = "\n ⚠️ WARNING: Writing file outside repository\n"
|
|
1751
|
-
|
|
1752
|
-
description = f" ● Update({path}){outside_repo_warning}\n{diff_display}"
|
|
1753
|
-
|
|
1754
|
-
if not permission_manager.request_permission("edit_file", description, pattern=path):
|
|
1755
|
-
return "Operation cancelled by user."
|
|
1756
|
-
|
|
1757
|
-
# Backup if enabled
|
|
1758
|
-
backup_path = _backup_file(p)
|
|
1759
|
-
|
|
1760
|
-
# Perform replacement using the matched string
|
|
1929
|
+
# Perform indentation adjustment and trailing newline preservation BEFORE showing diff
|
|
1761
1930
|
# Important: Adjust indentation and preserve trailing newlines to maintain file structure
|
|
1762
1931
|
adjusted_new_string = new_string
|
|
1763
1932
|
|
|
@@ -1803,6 +1972,25 @@ def edit_file(path: str, old_string: str, new_string: str) -> str:
|
|
|
1803
1972
|
trailing_newlines = len(matched_string) - len(matched_string.rstrip("\n"))
|
|
1804
1973
|
adjusted_new_string = adjusted_new_string + ("\n" * trailing_newlines)
|
|
1805
1974
|
|
|
1975
|
+
# Check permission before proceeding (use adjusted_new_string for accurate diff display)
|
|
1976
|
+
permission_manager = _get_permission_manager()
|
|
1977
|
+
|
|
1978
|
+
# Format colored diff for permission prompt (use adjusted_new_string so user sees what will actually be written)
|
|
1979
|
+
diff_display = _format_colored_diff(matched_string, adjusted_new_string, file_path=path)
|
|
1980
|
+
|
|
1981
|
+
# Add warning if writing outside repository
|
|
1982
|
+
outside_repo_warning = ""
|
|
1983
|
+
if not _is_inside_repo(p):
|
|
1984
|
+
outside_repo_warning = "\n ⚠️ WARNING: Writing file outside repository\n"
|
|
1985
|
+
|
|
1986
|
+
description = f" ● Update({path}){outside_repo_warning}\n{diff_display}"
|
|
1987
|
+
|
|
1988
|
+
if not permission_manager.request_permission("edit_file", description, pattern=path):
|
|
1989
|
+
return "Operation cancelled by user."
|
|
1990
|
+
|
|
1991
|
+
# Backup if enabled
|
|
1992
|
+
backup_path = _backup_file(p)
|
|
1993
|
+
|
|
1806
1994
|
new_content = content.replace(matched_string, adjusted_new_string)
|
|
1807
1995
|
|
|
1808
1996
|
# Write the new content
|
|
@@ -2359,4 +2547,19 @@ def run_shell(cmd: str) -> str:
|
|
|
2359
2547
|
stdout = result.stdout.decode("utf-8", errors="replace") if result.stdout else ""
|
|
2360
2548
|
stderr = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
|
|
2361
2549
|
|
|
2362
|
-
|
|
2550
|
+
output = stdout + stderr
|
|
2551
|
+
|
|
2552
|
+
# Apply output filtering to reduce token usage
|
|
2553
|
+
if OutputFilter.should_filter(cmd):
|
|
2554
|
+
filtered_output = OutputFilter.filter_output(cmd, output)
|
|
2555
|
+
# Log if we filtered significantly
|
|
2556
|
+
original_lines = len(output.split("\n"))
|
|
2557
|
+
filtered_lines = len(filtered_output.split("\n"))
|
|
2558
|
+
if filtered_lines < original_lines * 0.5:
|
|
2559
|
+
audit_logger.info(
|
|
2560
|
+
f"SHELL_FILTER: Reduced output from {original_lines} to {filtered_lines} lines "
|
|
2561
|
+
f"(~{int((1 - filtered_lines / original_lines) * 100)}% reduction)"
|
|
2562
|
+
)
|
|
2563
|
+
return filtered_output
|
|
2564
|
+
|
|
2565
|
+
return output
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|