patchpal 0.4.1__tar.gz → 0.4.3__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.
Files changed (27) hide show
  1. {patchpal-0.4.1/patchpal.egg-info → patchpal-0.4.3}/PKG-INFO +1 -1
  2. {patchpal-0.4.1 → patchpal-0.4.3}/patchpal/__init__.py +1 -1
  3. {patchpal-0.4.1 → patchpal-0.4.3}/patchpal/agent.py +43 -4
  4. {patchpal-0.4.1 → patchpal-0.4.3}/patchpal/tools.py +10 -6
  5. {patchpal-0.4.1 → patchpal-0.4.3/patchpal.egg-info}/PKG-INFO +1 -1
  6. {patchpal-0.4.1 → patchpal-0.4.3}/tests/test_agent.py +27 -14
  7. {patchpal-0.4.1 → patchpal-0.4.3}/LICENSE +0 -0
  8. {patchpal-0.4.1 → patchpal-0.4.3}/MANIFEST.in +0 -0
  9. {patchpal-0.4.1 → patchpal-0.4.3}/README.md +0 -0
  10. {patchpal-0.4.1 → patchpal-0.4.3}/patchpal/cli.py +0 -0
  11. {patchpal-0.4.1 → patchpal-0.4.3}/patchpal/context.py +0 -0
  12. {patchpal-0.4.1 → patchpal-0.4.3}/patchpal/permissions.py +0 -0
  13. {patchpal-0.4.1 → patchpal-0.4.3}/patchpal/skills.py +0 -0
  14. {patchpal-0.4.1 → patchpal-0.4.3}/patchpal/system_prompt.md +0 -0
  15. {patchpal-0.4.1 → patchpal-0.4.3}/patchpal.egg-info/SOURCES.txt +0 -0
  16. {patchpal-0.4.1 → patchpal-0.4.3}/patchpal.egg-info/dependency_links.txt +0 -0
  17. {patchpal-0.4.1 → patchpal-0.4.3}/patchpal.egg-info/entry_points.txt +0 -0
  18. {patchpal-0.4.1 → patchpal-0.4.3}/patchpal.egg-info/requires.txt +0 -0
  19. {patchpal-0.4.1 → patchpal-0.4.3}/patchpal.egg-info/top_level.txt +0 -0
  20. {patchpal-0.4.1 → patchpal-0.4.3}/pyproject.toml +0 -0
  21. {patchpal-0.4.1 → patchpal-0.4.3}/setup.cfg +0 -0
  22. {patchpal-0.4.1 → patchpal-0.4.3}/tests/test_cli.py +0 -0
  23. {patchpal-0.4.1 → patchpal-0.4.3}/tests/test_context.py +0 -0
  24. {patchpal-0.4.1 → patchpal-0.4.3}/tests/test_guardrails.py +0 -0
  25. {patchpal-0.4.1 → patchpal-0.4.3}/tests/test_operational_safety.py +0 -0
  26. {patchpal-0.4.1 → patchpal-0.4.3}/tests/test_skills.py +0 -0
  27. {patchpal-0.4.1 → patchpal-0.4.3}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchpal
3
- Version: 0.4.1
3
+ Version: 0.4.3
4
4
  Summary: A lean Claude Code clone in pure Python
5
5
  Author: PatchPal Contributors
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  """PatchPal - An open-source Claude Code clone implemented purely in Python."""
2
2
 
3
- __version__ = "0.4.1"
3
+ __version__ = "0.4.3"
4
4
 
5
5
  from patchpal.agent import create_agent
6
6
  from patchpal.tools import (
@@ -756,13 +756,52 @@ def _apply_prompt_caching(messages: List[Dict[str, Any]], model_id: str) -> List
756
756
 
757
757
  # Apply caching to system messages (first 2)
758
758
  for idx in system_messages[:2]:
759
- if "cache_control" not in messages[idx] and "cachePoint" not in messages[idx]:
760
- messages[idx] = {**messages[idx], **cache_marker}
759
+ msg = messages[idx]
760
+ # Skip if already has cache marker at content block level
761
+ if isinstance(msg.get("content"), list):
762
+ # Already structured - check if any block has cache_control/cachePoint
763
+ has_cache = any(
764
+ "cache_control" in block or "cachePoint" in block
765
+ for block in msg["content"]
766
+ if isinstance(block, dict)
767
+ )
768
+ if not has_cache and msg["content"]:
769
+ # Add cache marker to the last content block
770
+ last_block = msg["content"][-1]
771
+ if isinstance(last_block, dict):
772
+ last_block.update(cache_marker)
773
+ else:
774
+ # Convert simple string content to structured format with cache marker
775
+ content_text = msg.get("content", "")
776
+ messages[idx] = {
777
+ **msg,
778
+ "content": [{"type": "text", "text": content_text, **cache_marker}],
779
+ }
761
780
 
762
781
  # Apply caching to last 2 messages
763
782
  for idx in last_two_indices:
764
- if "cache_control" not in messages[idx] and "cachePoint" not in messages[idx]:
765
- messages[idx] = {**messages[idx], **cache_marker}
783
+ msg = messages[idx]
784
+ # Skip if already has cache marker at content block level
785
+ if isinstance(msg.get("content"), list):
786
+ # Already structured - check if any block has cache_control/cachePoint
787
+ has_cache = any(
788
+ "cache_control" in block or "cachePoint" in block
789
+ for block in msg["content"]
790
+ if isinstance(block, dict)
791
+ )
792
+ if not has_cache and msg["content"]:
793
+ # Add cache marker to the last content block
794
+ last_block = msg["content"][-1]
795
+ if isinstance(last_block, dict):
796
+ last_block.update(cache_marker)
797
+ else:
798
+ # Convert simple string content to structured format with cache marker
799
+ content_text = msg.get("content", "")
800
+ if content_text: # Only convert non-empty content
801
+ messages[idx] = {
802
+ **msg,
803
+ "content": [{"type": "text", "text": content_text, **cache_marker}],
804
+ }
766
805
 
767
806
  return messages
768
807
 
@@ -665,7 +665,7 @@ def _check_path(path: str, must_exist: bool = True) -> Path:
665
665
  Validate and resolve a path.
666
666
 
667
667
  Args:
668
- path: Path to validate (relative or absolute)
668
+ path: Path to validate (relative, absolute, or with ~ for home directory)
669
669
  must_exist: Whether the file must exist
670
670
 
671
671
  Returns:
@@ -678,12 +678,15 @@ def _check_path(path: str, must_exist: bool = True) -> Path:
678
678
  Can access files anywhere on the system (repository or outside).
679
679
  Sensitive files (.env, credentials) are always blocked for safety.
680
680
  """
681
+ # Expand ~ for home directory first
682
+ expanded_path = os.path.expanduser(path)
683
+
681
684
  # Resolve path (handle both absolute and relative paths)
682
- path_obj = Path(path)
685
+ path_obj = Path(expanded_path)
683
686
  if path_obj.is_absolute():
684
687
  p = path_obj.resolve()
685
688
  else:
686
- p = (REPO_ROOT / path).resolve()
689
+ p = (REPO_ROOT / expanded_path).resolve()
687
690
 
688
691
  # Check if file exists when required
689
692
  if must_exist and not p.is_file():
@@ -1074,12 +1077,13 @@ def tree(path: str = ".", max_depth: int = 3, show_hidden: bool = False) -> str:
1074
1077
  # Limit max_depth
1075
1078
  max_depth = min(max_depth, 10)
1076
1079
 
1077
- # Resolve path (handle both absolute and relative paths)
1078
- path_obj = Path(path)
1080
+ # Expand ~ for home directory and resolve path (handle both absolute and relative paths)
1081
+ expanded_path = os.path.expanduser(path)
1082
+ path_obj = Path(expanded_path)
1079
1083
  if path_obj.is_absolute():
1080
1084
  start_path = path_obj.resolve()
1081
1085
  else:
1082
- start_path = (REPO_ROOT / path).resolve()
1086
+ start_path = (REPO_ROOT / expanded_path).resolve()
1083
1087
 
1084
1088
  # Check if path exists and is a directory
1085
1089
  if not start_path.exists():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchpal
3
- Version: 0.4.1
3
+ Version: 0.4.3
4
4
  Summary: A lean Claude Code clone in pure Python
5
5
  Author: PatchPal Contributors
6
6
  License-Expression: Apache-2.0
@@ -441,13 +441,17 @@ def test_prompt_caching_application_anthropic():
441
441
  # Test with direct Anthropic API
442
442
  cached_messages = _apply_prompt_caching(messages.copy(), "anthropic/claude-sonnet-4-5")
443
443
 
444
- # System message should have cache_control
445
- assert "cache_control" in cached_messages[0]
446
- assert cached_messages[0]["cache_control"] == {"type": "ephemeral"}
444
+ # System message should have cache_control inside content block
445
+ assert isinstance(cached_messages[0]["content"], list)
446
+ assert cached_messages[0]["content"][0]["type"] == "text"
447
+ assert "cache_control" in cached_messages[0]["content"][0]
448
+ assert cached_messages[0]["content"][0]["cache_control"] == {"type": "ephemeral"}
447
449
 
448
- # Last 2 messages should have cache_control
449
- assert "cache_control" in cached_messages[-1] # Last user message
450
- assert "cache_control" in cached_messages[-2] # Last assistant message
450
+ # Last 2 messages should have cache_control inside content blocks
451
+ assert isinstance(cached_messages[-1]["content"], list) # Last user message
452
+ assert "cache_control" in cached_messages[-1]["content"][0]
453
+ assert isinstance(cached_messages[-2]["content"], list) # Last assistant message
454
+ assert "cache_control" in cached_messages[-2]["content"][0]
451
455
 
452
456
 
453
457
  def test_prompt_caching_application_bedrock():
@@ -466,13 +470,17 @@ def test_prompt_caching_application_bedrock():
466
470
  messages.copy(), "bedrock/anthropic.claude-sonnet-4-5-v1:0"
467
471
  )
468
472
 
469
- # System message should have cachePoint (Bedrock format)
470
- assert "cachePoint" in cached_messages[0]
471
- assert cached_messages[0]["cachePoint"] == {"type": "ephemeral"}
473
+ # System message should have cachePoint inside content block (Bedrock format)
474
+ assert isinstance(cached_messages[0]["content"], list)
475
+ assert cached_messages[0]["content"][0]["type"] == "text"
476
+ assert "cachePoint" in cached_messages[0]["content"][0]
477
+ assert cached_messages[0]["content"][0]["cachePoint"] == {"type": "ephemeral"}
472
478
 
473
- # Last 2 messages should have cachePoint
474
- assert "cachePoint" in cached_messages[-1]
475
- assert "cachePoint" in cached_messages[-2]
479
+ # Last 2 messages should have cachePoint inside content blocks
480
+ assert isinstance(cached_messages[-1]["content"], list)
481
+ assert "cachePoint" in cached_messages[-1]["content"][0]
482
+ assert isinstance(cached_messages[-2]["content"], list)
483
+ assert "cachePoint" in cached_messages[-2]["content"][0]
476
484
 
477
485
 
478
486
  def test_prompt_caching_no_modification_for_unsupported():
@@ -506,5 +514,10 @@ def test_prompt_caching_idempotent():
506
514
  cached_once = _apply_prompt_caching(messages.copy(), "anthropic/claude-sonnet-4-5")
507
515
  cached_twice = _apply_prompt_caching(cached_once.copy(), "anthropic/claude-sonnet-4-5")
508
516
 
509
- # Should be the same after second application
510
- assert cached_once == cached_twice
517
+ # Should have the same structure after second application
518
+ assert cached_once[0]["content"][0] == cached_twice[0]["content"][0]
519
+ assert cached_once[1]["content"][0] == cached_twice[1]["content"][0]
520
+
521
+ # Should only have one cache_control marker per message
522
+ assert len([k for k in cached_twice[0]["content"][0].keys() if "cache" in k]) == 1
523
+ assert len([k for k in cached_twice[1]["content"][0].keys() if "cache" in k]) == 1
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