code-puppy 0.0.151__tar.gz → 0.0.153__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 (133) hide show
  1. {code_puppy-0.0.151 → code_puppy-0.0.153}/PKG-INFO +1 -1
  2. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/agents/agent_code_puppy.py +0 -5
  3. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/config.py +1 -1
  4. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/message_history_processor.py +2 -2
  5. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tools/file_operations.py +207 -129
  6. {code_puppy-0.0.151 → code_puppy-0.0.153}/pyproject.toml +1 -1
  7. {code_puppy-0.0.151 → code_puppy-0.0.153}/.gitignore +0 -0
  8. {code_puppy-0.0.151 → code_puppy-0.0.153}/LICENSE +0 -0
  9. {code_puppy-0.0.151 → code_puppy-0.0.153}/README.md +0 -0
  10. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/__init__.py +0 -0
  11. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/__main__.py +0 -0
  12. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/agent.py +0 -0
  13. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/agents/__init__.py +0 -0
  14. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/agents/agent_creator_agent.py +0 -0
  15. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/agents/agent_manager.py +0 -0
  16. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/agents/agent_orchestrator.json +0 -0
  17. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/agents/base_agent.py +0 -0
  18. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/agents/json_agent.py +0 -0
  19. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/agents/runtime_manager.py +0 -0
  20. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/callbacks.py +0 -0
  21. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/__init__.py +0 -0
  22. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/command_handler.py +0 -0
  23. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/file_path_completion.py +0 -0
  24. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/load_context_completion.py +0 -0
  25. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/__init__.py +0 -0
  26. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/add_command.py +0 -0
  27. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/base.py +0 -0
  28. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/handler.py +0 -0
  29. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/help_command.py +0 -0
  30. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/install_command.py +0 -0
  31. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/list_command.py +0 -0
  32. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/logs_command.py +0 -0
  33. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/remove_command.py +0 -0
  34. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/restart_command.py +0 -0
  35. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/search_command.py +0 -0
  36. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  37. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/start_command.py +0 -0
  38. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/status_command.py +0 -0
  39. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  40. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/stop_command.py +0 -0
  41. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/test_command.py +0 -0
  42. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/utils.py +0 -0
  43. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  44. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/meta_command_handler.py +0 -0
  45. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/model_picker_completion.py +0 -0
  46. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/motd.py +0 -0
  47. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  48. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/command_line/utils.py +0 -0
  49. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/http_utils.py +0 -0
  50. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/main.py +0 -0
  51. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/__init__.py +0 -0
  52. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/async_lifecycle.py +0 -0
  53. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/blocking_startup.py +0 -0
  54. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/captured_stdio_server.py +0 -0
  55. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/circuit_breaker.py +0 -0
  56. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/config_wizard.py +0 -0
  57. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/dashboard.py +0 -0
  58. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/error_isolation.py +0 -0
  59. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/examples/retry_example.py +0 -0
  60. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/health_monitor.py +0 -0
  61. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/managed_server.py +0 -0
  62. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/manager.py +0 -0
  63. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/registry.py +0 -0
  64. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/retry_manager.py +0 -0
  65. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/server_registry_catalog.py +0 -0
  66. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/status_tracker.py +0 -0
  67. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/mcp/system_tools.py +0 -0
  68. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/messaging/__init__.py +0 -0
  69. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/messaging/message_queue.py +0 -0
  70. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/messaging/queue_console.py +0 -0
  71. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/messaging/renderers.py +0 -0
  72. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/messaging/spinner/__init__.py +0 -0
  73. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/messaging/spinner/console_spinner.py +0 -0
  74. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/messaging/spinner/spinner_base.py +0 -0
  75. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/messaging/spinner/textual_spinner.py +0 -0
  76. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/model_factory.py +0 -0
  77. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/models.json +0 -0
  78. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/plugins/__init__.py +0 -0
  79. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/reopenable_async_client.py +0 -0
  80. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/round_robin_model.py +0 -0
  81. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/state_management.py +0 -0
  82. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/status_display.py +0 -0
  83. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/summarization_agent.py +0 -0
  84. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/token_utils.py +0 -0
  85. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tools/__init__.py +0 -0
  86. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tools/agent_tools.py +0 -0
  87. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tools/command_runner.py +0 -0
  88. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tools/common.py +0 -0
  89. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tools/file_modifications.py +0 -0
  90. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tools/token_check.py +0 -0
  91. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tools/tools_content.py +0 -0
  92. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/__init__.py +0 -0
  93. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/app.py +0 -0
  94. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/components/__init__.py +0 -0
  95. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/components/chat_view.py +0 -0
  96. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/components/command_history_modal.py +0 -0
  97. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/components/copy_button.py +0 -0
  98. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/components/custom_widgets.py +0 -0
  99. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/components/human_input_modal.py +0 -0
  100. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/components/input_area.py +0 -0
  101. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/components/sidebar.py +0 -0
  102. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/components/status_bar.py +0 -0
  103. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/messages.py +0 -0
  104. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/models/__init__.py +0 -0
  105. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/models/chat_message.py +0 -0
  106. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/models/command_history.py +0 -0
  107. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/models/enums.py +0 -0
  108. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/screens/__init__.py +0 -0
  109. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/screens/help.py +0 -0
  110. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/screens/mcp_install_wizard.py +0 -0
  111. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/screens/settings.py +0 -0
  112. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/screens/tools.py +0 -0
  113. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/__init__.py +0 -0
  114. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_agent_command.py +0 -0
  115. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_chat_message.py +0 -0
  116. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_chat_view.py +0 -0
  117. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_command_history.py +0 -0
  118. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_copy_button.py +0 -0
  119. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_custom_widgets.py +0 -0
  120. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_disclaimer.py +0 -0
  121. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_enums.py +0 -0
  122. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_file_browser.py +0 -0
  123. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_help.py +0 -0
  124. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_history_file_reader.py +0 -0
  125. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_input_area.py +0 -0
  126. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_settings.py +0 -0
  127. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_sidebar.py +0 -0
  128. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_sidebar_history.py +0 -0
  129. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_sidebar_history_navigation.py +0 -0
  130. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_status_bar.py +0 -0
  131. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_timestamped_history.py +0 -0
  132. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/tui/tests/test_tools.py +0 -0
  133. {code_puppy-0.0.151 → code_puppy-0.0.153}/code_puppy/version_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.151
3
+ Version: 0.0.153
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -99,17 +99,12 @@ edit_file(
99
99
  payload={{file_path="example.py", "delete_snippet": "# TODO: remove this line"}}
100
100
  )
101
101
  ```
102
-
103
- NEVER output an entire file – this is very expensive.
104
- You may not edit file extensions: [.ipynb]
105
-
106
102
  Best-practice guidelines for `edit_file`:
107
103
  • Keep each diff small – ideally between 100-300 lines.
108
104
  • Apply multiple sequential `edit_file` calls when you need to refactor large files instead of sending one massive diff.
109
105
  • Never paste an entire file inside `old_str`; target only the minimal snippet you want changed.
110
106
  • If the resulting file would grow beyond 600 lines, split logic into additional files and create them with separate `edit_file` calls.
111
107
 
112
-
113
108
  System Operations:
114
109
  - run_shell_command(command, cwd=None, timeout=60): Use this to execute commands, run tests, or start services
115
110
 
@@ -76,7 +76,7 @@ def get_allow_recursion() -> bool:
76
76
  """
77
77
  val = get_value("allow_recursion")
78
78
  if val is None:
79
- return False # Default to False for safety
79
+ return True # Default to False for safety
80
80
  return str(val).lower() in ("1", "true", "yes", "on")
81
81
 
82
82
 
@@ -31,7 +31,7 @@ def estimate_token_count(text: str) -> int:
31
31
  Simple token estimation using len(message) - 4.
32
32
  This replaces tiktoken with a much simpler approach.
33
33
  """
34
- return max(1, len(text) - 4)
34
+ return int(max(1, len(text)) / 4)
35
35
 
36
36
 
37
37
  def stringify_message_part(part) -> str:
@@ -86,7 +86,7 @@ def estimate_tokens_for_message(message: ModelMessage) -> int:
86
86
  if part_str:
87
87
  total_tokens += estimate_token_count(part_str)
88
88
 
89
- return max(1, total_tokens)
89
+ return int(max(1, total_tokens) / 4)
90
90
 
91
91
 
92
92
  def filter_huge_messages(messages: List[ModelMessage]) -> List[ModelMessage]:
@@ -1,6 +1,7 @@
1
1
  # file_operations.py
2
2
 
3
3
  import os
4
+ import tempfile
4
5
  from typing import List
5
6
 
6
7
  from pydantic import BaseModel, conint
@@ -46,7 +47,7 @@ class ListedFile(BaseModel):
46
47
 
47
48
 
48
49
  class ListFileOutput(BaseModel):
49
- files: List[ListedFile]
50
+ content: str
50
51
  error: str | None = None
51
52
 
52
53
 
@@ -125,116 +126,182 @@ def is_project_directory(directory):
125
126
  def _list_files(
126
127
  context: RunContext, directory: str = ".", recursive: bool = True
127
128
  ) -> ListFileOutput:
128
-
129
+ import subprocess
130
+ import tempfile
131
+ import shutil
132
+ import sys
133
+
129
134
  results = []
130
135
  directory = os.path.abspath(directory)
131
136
 
132
- # Generate group_id for this tool execution
133
- group_id = generate_group_id("list_files", directory)
134
-
135
- emit_info(
136
- "\n[bold white on blue] DIRECTORY LISTING [/bold white on blue]",
137
- message_group=group_id,
138
- )
139
- emit_info(
140
- f"\U0001f4c2 [bold cyan]{directory}[/bold cyan] [dim](recursive={recursive})[/dim]\n",
141
- message_group=group_id,
142
- )
143
- emit_divider(message_group=group_id)
137
+ # Build string representation
138
+ output_lines = []
139
+
140
+ directory_listing_header = "\n[bold white on blue] DIRECTORY LISTING [/bold white on blue]"
141
+ output_lines.append(directory_listing_header)
142
+
143
+ directory_info = f"\U0001f4c2 [bold cyan]{directory}[/bold cyan] [dim](recursive={recursive})[/dim]\n"
144
+ output_lines.append(directory_info)
145
+
146
+ divider = "[dim]" + "─" * 100 + "\n" + "[/dim]"
147
+ output_lines.append(divider)
148
+
144
149
  if not os.path.exists(directory):
145
- emit_error(f"Directory '{directory}' does not exist", message_group=group_id)
146
- emit_divider(message_group=group_id)
147
- return ListFileOutput(
148
- files=[ListedFile(path=None, type=None, full_path=None, depth=None)]
149
- )
150
+ error_msg = f"[red bold]Error:[/red bold] Directory '{directory}' does not exist"
151
+ output_lines.append(error_msg)
152
+
153
+ output_lines.append(divider)
154
+ return ListFileOutput(content="\n".join(output_lines))
150
155
  if not os.path.isdir(directory):
151
- emit_error(f"'{directory}' is not a directory", message_group=group_id)
152
- emit_divider(message_group=group_id)
153
- return ListFileOutput(
154
- files=[ListedFile(path=None, type=None, full_path=None, depth=None)]
155
- )
156
+ error_msg = f"[red bold]Error:[/red bold] '{directory}' is not a directory"
157
+ output_lines.append(error_msg)
158
+
159
+ output_lines.append(divider)
160
+ return ListFileOutput(content="\n".join(output_lines))
156
161
 
157
162
  # Smart home directory detection - auto-limit recursion for performance
158
163
  # But allow recursion in tests (when context=None) or when explicitly requested
159
164
  if context is not None and is_likely_home_directory(directory) and recursive:
160
165
  if not is_project_directory(directory):
161
- emit_warning(
162
- "🏠 Detected home directory - limiting to non-recursive listing for performance",
163
- message_group=group_id,
164
- )
165
- emit_info(
166
- f"💡 To force recursive listing in home directory, use list_files('{directory}', recursive=True) explicitly",
167
- message_group=group_id,
168
- )
166
+ warning_msg = "[yellow bold]Warning:[/yellow bold] 🏠 Detected home directory - limiting to non-recursive listing for performance"
167
+ output_lines.append(warning_msg)
168
+
169
+ info_msg = f"[dim]💡 To force recursive listing in home directory, use list_files('{directory}', recursive=True) explicitly[/dim]"
170
+ output_lines.append(info_msg)
169
171
  recursive = False
170
- folder_structure = {}
171
- file_list = []
172
- for root, dirs, files in os.walk(directory):
173
- # Filter out ignored directories
174
- dirs[:] = [d for d in dirs if not should_ignore_path(os.path.join(root, d))]
172
+
173
+ # Create a temporary ignore file with our ignore patterns
174
+ ignore_file = None
175
+ try:
176
+ # Find ripgrep executable - first check system PATH, then virtual environment
177
+ rg_path = shutil.which("rg")
178
+ if not rg_path:
179
+ # Try to find it in the virtual environment
180
+ # Use sys.executable to determine the Python environment path
181
+ python_dir = os.path.dirname(sys.executable)
182
+ # Check both 'bin' (Unix) and 'Scripts' (Windows) directories
183
+ for rg_dir in ["bin", "Scripts"]:
184
+ venv_rg_path = os.path.join(python_dir, "rg")
185
+ if os.path.exists(venv_rg_path):
186
+ rg_path = venv_rg_path
187
+ break
188
+ # Also check with .exe extension for Windows
189
+ venv_rg_exe_path = os.path.join(python_dir, "rg.exe")
190
+ if os.path.exists(venv_rg_exe_path):
191
+ rg_path = venv_rg_exe_path
192
+ break
175
193
 
176
- rel_path = os.path.relpath(root, directory)
177
- depth = 0 if rel_path == "." else rel_path.count(os.sep) + 1
178
- if rel_path == ".":
179
- rel_path = ""
194
+ if not rg_path:
195
+ error_msg = f"[red bold]Error:[/red bold] ripgrep (rg) not found. Please install ripgrep to use this tool."
196
+ output_lines.append(error_msg)
197
+ return ListFileOutput(content="\n".join(output_lines))
198
+
199
+ # Build command for ripgrep --files
200
+ cmd = [rg_path, "--files"]
180
201
 
181
- # Add directory entry for subdirectories (except root)
182
- if rel_path:
183
- dir_path = os.path.join(directory, rel_path)
184
- results.append(
185
- ListedFile(
186
- path=rel_path,
187
- type="directory",
188
- size=0,
189
- full_path=dir_path,
190
- depth=depth,
191
- )
192
- )
193
- folder_structure[rel_path] = {
194
- "path": rel_path,
195
- "depth": depth,
196
- "full_path": dir_path,
197
- }
198
- else: # Root directory - add both directories and files
199
- # Add directories
200
- for d in dirs:
201
- dir_path = os.path.join(root, d)
202
+ # For non-recursive mode, we'll limit depth after getting results
203
+ if not recursive:
204
+ cmd.extend(["--max-depth", "1"])
205
+
206
+ # Add ignore patterns to the command via a temporary file
207
+ from code_puppy.tools.common import IGNORE_PATTERNS
208
+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.ignore') as f:
209
+ ignore_file = f.name
210
+ for pattern in IGNORE_PATTERNS:
211
+ f.write(f"{pattern}\n")
212
+
213
+ cmd.extend(["--ignore-file", ignore_file])
214
+ cmd.append(directory)
215
+
216
+ # Run ripgrep to get file listing
217
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
218
+
219
+ # Process the output lines
220
+ files = result.stdout.strip().split('\n') if result.stdout.strip() else []
221
+
222
+ # Create ListedFile objects with metadata
223
+ for file_path in files:
224
+ if not file_path: # Skip empty lines
225
+ continue
226
+
227
+ full_path = os.path.join(directory, file_path)
228
+
229
+ # Skip if file doesn't exist (though it should)
230
+ if not os.path.exists(full_path):
231
+ continue
232
+
233
+ # For non-recursive mode, skip files in subdirectories
234
+ if not recursive and os.sep in file_path:
235
+ continue
236
+
237
+ # Check if path is a file or directory
238
+ if os.path.isfile(full_path):
239
+ entry_type = "file"
240
+ size = os.path.getsize(full_path)
241
+ elif os.path.isdir(full_path):
242
+ entry_type = "directory"
243
+ size = 0
244
+ else:
245
+ # Skip if it's neither a file nor directory
246
+ continue
247
+
248
+ try:
249
+ # Get stats for the entry
250
+ stat_info = os.stat(full_path)
251
+ actual_size = stat_info.st_size
252
+
253
+ # For files, we use the actual size; for directories, we keep size=0
254
+ if entry_type == "file":
255
+ size = actual_size
256
+
257
+ # Calculate depth
258
+ depth = file_path.count(os.sep)
259
+
260
+ # Add directory entries if needed for files
261
+ if entry_type == "file":
262
+ dir_path = os.path.dirname(file_path)
263
+ if dir_path:
264
+ # Add directory path components if they don't exist
265
+ path_parts = dir_path.split(os.sep)
266
+ for i in range(len(path_parts)):
267
+ partial_path = os.sep.join(path_parts[:i+1])
268
+ # Check if we already added this directory
269
+ if not any(f.path == partial_path and f.type == "directory" for f in results):
270
+ results.append(
271
+ ListedFile(
272
+ path=partial_path,
273
+ type="directory",
274
+ size=0,
275
+ full_path=os.path.join(directory, partial_path),
276
+ depth=partial_path.count(os.sep),
277
+ )
278
+ )
279
+
280
+ # Add the entry (file or directory)
202
281
  results.append(
203
282
  ListedFile(
204
- path=d,
205
- type="directory",
206
- size=0,
207
- full_path=dir_path,
283
+ path=file_path,
284
+ type=entry_type,
285
+ size=size,
286
+ full_path=full_path,
208
287
  depth=depth,
209
288
  )
210
289
  )
211
- folder_structure[d] = {
212
- "path": d,
213
- "depth": depth,
214
- "full_path": dir_path,
215
- }
216
-
217
- # Add files to results
218
- for file in files:
219
- file_path = os.path.join(root, file)
220
- if should_ignore_path(file_path):
290
+ except (FileNotFoundError, PermissionError, OSError):
291
+ # Skip files we can't access
221
292
  continue
222
- rel_file_path = os.path.join(rel_path, file) if rel_path else file
223
- try:
224
- size = os.path.getsize(file_path)
225
- file_info = {
226
- "path": rel_file_path,
227
- "type": "file",
228
- "size": size,
229
- "full_path": file_path,
230
- "depth": depth,
231
- }
232
- results.append(ListedFile(**file_info))
233
- file_list.append(file_info)
234
- except (FileNotFoundError, PermissionError):
235
- continue
236
- if not recursive:
237
- break
293
+ except subprocess.TimeoutExpired:
294
+ error_msg = f"[red bold]Error:[/red bold] List files command timed out after 30 seconds"
295
+ output_lines.append(error_msg)
296
+ return ListFileOutput(content="\n".join(output_lines))
297
+ except Exception as e:
298
+ error_msg = f"[red bold]Error:[/red bold] Error during list files operation: {e}"
299
+ output_lines.append(error_msg)
300
+ return ListFileOutput(content="\n".join(output_lines))
301
+ finally:
302
+ # Clean up the temporary ignore file
303
+ if ignore_file and os.path.exists(ignore_file):
304
+ os.unlink(ignore_file)
238
305
 
239
306
  def format_size(size_bytes):
240
307
  if size_bytes < 1024:
@@ -275,20 +342,31 @@ def _list_files(
275
342
  else:
276
343
  return "\U0001f4c4"
277
344
 
278
- if results:
279
- files = sorted([f for f in results if f.type == "file"], key=lambda x: x.path)
280
- emit_info(
281
- f"\U0001f4c1 [bold blue]{os.path.basename(directory) or directory}[/bold blue]",
282
- message_group=group_id,
283
- )
345
+ dir_count = sum(1 for item in results if item.type == "directory")
346
+ file_count = sum(1 for item in results if item.type == "file")
347
+ total_size = sum(item.size for item in results if item.type == "file")
348
+
349
+ # Build the directory header section
350
+ dir_name = os.path.basename(directory) or directory
351
+ dir_header = f"\U0001f4c1 [bold blue]{dir_name}[/bold blue]"
352
+ output_lines.append(dir_header)
353
+
354
+ # Sort all items by path for consistent display
284
355
  all_items = sorted(results, key=lambda x: x.path)
356
+
357
+ # Build file and directory tree representation
285
358
  parent_dirs_with_content = set()
286
- for i, item in enumerate(all_items):
359
+ for item in all_items:
360
+ # Skip root directory entries with no path
287
361
  if item.type == "directory" and not item.path:
288
362
  continue
363
+
364
+ # Track parent directories that contain files/dirs
289
365
  if os.sep in item.path:
290
366
  parent_path = os.path.dirname(item.path)
291
367
  parent_dirs_with_content.add(parent_path)
368
+
369
+ # Calculate indentation depth based on path separators
292
370
  depth = item.path.count(os.sep) + 1 if item.path else 0
293
371
  prefix = ""
294
372
  for d in range(depth):
@@ -296,29 +374,32 @@ def _list_files(
296
374
  prefix += "\u2514\u2500\u2500 "
297
375
  else:
298
376
  prefix += " "
377
+
378
+ # Get the display name (basename) of the item
299
379
  name = os.path.basename(item.path) or item.path
380
+
381
+ # Add directory or file line with appropriate formatting
300
382
  if item.type == "directory":
301
- emit_info(
302
- f"{prefix}\U0001f4c1 [bold blue]{name}/[/bold blue]",
303
- message_group=group_id,
304
- )
383
+ dir_line = f"{prefix}\U0001f4c1 [bold blue]{name}/[/bold blue]"
384
+ output_lines.append(dir_line)
305
385
  else:
306
386
  icon = get_file_icon(item.path)
307
387
  size_str = format_size(item.size)
308
- emit_info(
309
- f"{prefix}{icon} [green]{name}[/green] [dim]({size_str})[/dim]",
310
- message_group=group_id,
311
- )
312
- dir_count = sum(1 for item in results if item.type == "directory")
313
- file_count = sum(1 for item in results if item.type == "file")
314
- total_size = sum(item.size for item in results if item.type == "file")
315
- emit_info("\n[bold cyan]Summary:[/bold cyan]", message_group=group_id)
316
- emit_info(
317
- f"\U0001f4c1 [blue]{dir_count} directories[/blue], \U0001f4c4 [green]{file_count} files[/green] [dim]({format_size(total_size)} total)[/dim]",
318
- message_group=group_id,
319
- )
320
- emit_divider(message_group=group_id)
321
- return ListFileOutput(files=results)
388
+ file_line = f"{prefix}{icon} [green]{name}[/green] [dim]({size_str})[/dim]"
389
+ output_lines.append(file_line)
390
+
391
+ # Add summary information
392
+ summary_header = "\n[bold cyan]Summary:[/bold cyan]"
393
+ output_lines.append(summary_header)
394
+
395
+ summary_line = f"\U0001f4c1 [blue]{dir_count} directories[/blue], \U0001f4c4 [green]{file_count} files[/green] [dim]({format_size(total_size)} total)[/dim]"
396
+ output_lines.append(summary_line)
397
+
398
+ final_divider = "[dim]" + "─" * 100 + "\n" + "[/dim]"
399
+ output_lines.append(final_divider)
400
+
401
+ # Return both the content string and the list of ListedFile objects
402
+ return ListFileOutput(content="\n".join(output_lines), files=results)
322
403
 
323
404
 
324
405
  def _read_file(
@@ -530,25 +611,18 @@ def register_list_files(agent):
530
611
  Defaults to True.
531
612
 
532
613
  Returns:
533
- ListFileOutput: A structured response containing:
534
- - files (List[ListedFile]): List of files and directories found, where
535
- each ListedFile contains:
536
- - path (str | None): Relative path from the listing directory
537
- - type (str | None): "file" or "directory"
538
- - size (int): File size in bytes (0 for directories)
539
- - full_path (str | None): Absolute path to the item
540
- - depth (int | None): Nesting depth from the root directory
614
+ ListFileOutput: A response containing:
615
+ - content (str): String representation of the directory listing
541
616
  - error (str | None): Error message if listing failed
542
617
 
543
618
  Examples:
544
619
  >>> # List current directory
545
620
  >>> result = list_files(ctx)
546
- >>> for file in result.files:
547
- ... print(f"{file.type}: {file.path} ({file.size} bytes)")
621
+ >>> print(result.content)
548
622
 
549
623
  >>> # List specific directory non-recursively
550
624
  >>> result = list_files(ctx, "/path/to/project", recursive=False)
551
- >>> print(f"Found {len(result.files)} items")
625
+ >>> print(result.content)
552
626
 
553
627
  >>> # Handle potential errors
554
628
  >>> result = list_files(ctx, "/nonexistent/path")
@@ -566,9 +640,13 @@ def register_list_files(agent):
566
640
  warning = "Recursion disabled globally for list_files - returning non-recursive results"
567
641
  recursive = False
568
642
  result = _list_files(context, directory, recursive)
643
+
644
+ # Emit the content directly to ensure it's displayed to the user
645
+ emit_info(result.content, message_group=generate_group_id("list_files", directory))
646
+
569
647
  if warning:
570
648
  result.error = warning
571
- return result
649
+ return result
572
650
 
573
651
 
574
652
  def register_read_file(agent):
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-puppy"
7
- version = "0.0.151"
7
+ version = "0.0.153"
8
8
  description = "Code generation agent"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes
File without changes