code-puppy 0.0.214__py3-none-any.whl → 0.0.366__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 +7 -1
- code_puppy/agents/__init__.py +2 -0
- code_puppy/agents/agent_c_reviewer.py +59 -6
- code_puppy/agents/agent_code_puppy.py +7 -1
- code_puppy/agents/agent_code_reviewer.py +12 -2
- code_puppy/agents/agent_cpp_reviewer.py +73 -6
- code_puppy/agents/agent_creator_agent.py +45 -4
- code_puppy/agents/agent_golang_reviewer.py +92 -3
- code_puppy/agents/agent_javascript_reviewer.py +101 -8
- code_puppy/agents/agent_manager.py +81 -4
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +28 -6
- code_puppy/agents/agent_qa_expert.py +98 -6
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_security_auditor.py +113 -3
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +106 -7
- code_puppy/agents/base_agent.py +802 -176
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +142 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +176 -738
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +0 -3
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +15 -26
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +2 -2
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +18 -6
- code_puppy/command_line/mcp/start_command.py +47 -25
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +7 -1
- code_puppy/command_line/mcp/stop_command.py +8 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +75 -25
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +463 -63
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +898 -112
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +210 -148
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -698
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/blocking_startup.py +70 -43
- code_puppy/mcp_/captured_stdio_server.py +2 -2
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +65 -38
- code_puppy/mcp_/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/mcp_/server_registry_catalog.py +24 -5
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +21 -5
- code_puppy/messaging/spinner/console_spinner.py +86 -51
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +634 -83
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +66 -68
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +9 -12
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +41 -13
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +536 -52
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +19 -23
- code_puppy/tools/browser/browser_interactions.py +41 -48
- code_puppy/tools/browser/browser_locators.py +36 -38
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +16 -16
- code_puppy/tools/browser/browser_screenshot.py +79 -143
- code_puppy/tools/browser/browser_scripts.py +32 -42
- code_puppy/tools/browser/browser_workflows.py +44 -27
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +930 -147
- code_puppy/tools/common.py +1113 -5
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +226 -154
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/messaging/spinner/textual_spinner.py +0 -106
- code_puppy/tools/browser/camoufox_manager.py +0 -216
- code_puppy/tools/browser/vqa_agent.py +0 -70
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -1105
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -551
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -185
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -17
- code_puppy/tui/screens/autosave_picker.py +0 -175
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -306
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
- code_puppy-0.0.214.dist-info/RECORD +0 -131
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# file_operations.py
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
4
6
|
import tempfile
|
|
5
7
|
from typing import List
|
|
6
8
|
|
|
@@ -10,15 +12,14 @@ from pydantic_ai import RunContext
|
|
|
10
12
|
# ---------------------------------------------------------------------------
|
|
11
13
|
# Module-level helper functions (exposed for unit tests _and_ used as tools)
|
|
12
14
|
# ---------------------------------------------------------------------------
|
|
13
|
-
from code_puppy.messaging import (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
from code_puppy.messaging import ( # New structured messaging types
|
|
16
|
+
FileContentMessage,
|
|
17
|
+
FileEntry,
|
|
18
|
+
FileListingMessage,
|
|
19
|
+
GrepMatch,
|
|
20
|
+
GrepResultMessage,
|
|
21
|
+
get_message_bus,
|
|
20
22
|
)
|
|
21
|
-
from code_puppy.tools.common import generate_group_id
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
# Pydantic models for tool return types
|
|
@@ -49,6 +50,7 @@ class MatchInfo(BaseModel):
|
|
|
49
50
|
|
|
50
51
|
class GrepOutput(BaseModel):
|
|
51
52
|
matches: List[MatchInfo]
|
|
53
|
+
error: str | None = None
|
|
52
54
|
|
|
53
55
|
|
|
54
56
|
def is_likely_home_directory(directory):
|
|
@@ -107,54 +109,71 @@ def is_project_directory(directory):
|
|
|
107
109
|
return False
|
|
108
110
|
|
|
109
111
|
|
|
112
|
+
def would_match_directory(pattern: str, directory: str) -> bool:
|
|
113
|
+
"""Check if a glob pattern would match the given directory path.
|
|
114
|
+
|
|
115
|
+
This is used to avoid adding ignore patterns that would inadvertently
|
|
116
|
+
exclude the directory we're actually trying to search in.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
pattern: A glob pattern like '**/tmp/**' or 'node_modules'
|
|
120
|
+
directory: The directory path to check against
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
True if the pattern would match the directory, False otherwise
|
|
124
|
+
"""
|
|
125
|
+
import fnmatch
|
|
126
|
+
|
|
127
|
+
# Normalize the directory path
|
|
128
|
+
abs_dir = os.path.abspath(directory)
|
|
129
|
+
dir_name = os.path.basename(abs_dir)
|
|
130
|
+
|
|
131
|
+
# Strip leading/trailing wildcards and slashes for simpler matching
|
|
132
|
+
clean_pattern = pattern.strip("*").strip("/")
|
|
133
|
+
|
|
134
|
+
# Check if the directory name matches the pattern
|
|
135
|
+
if fnmatch.fnmatch(dir_name, clean_pattern):
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
# Check if the full path contains the pattern
|
|
139
|
+
if fnmatch.fnmatch(abs_dir, pattern):
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
# Check if any part of the path matches
|
|
143
|
+
path_parts = abs_dir.split(os.sep)
|
|
144
|
+
for part in path_parts:
|
|
145
|
+
if fnmatch.fnmatch(part, clean_pattern):
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
|
|
110
151
|
def _list_files(
|
|
111
152
|
context: RunContext, directory: str = ".", recursive: bool = True
|
|
112
153
|
) -> ListFileOutput:
|
|
113
|
-
import shutil
|
|
114
|
-
import subprocess
|
|
115
154
|
import sys
|
|
116
155
|
|
|
117
156
|
results = []
|
|
118
157
|
directory = os.path.abspath(os.path.expanduser(directory))
|
|
119
158
|
|
|
120
|
-
#
|
|
159
|
+
# Plain text output for LLM consumption
|
|
121
160
|
output_lines = []
|
|
122
|
-
|
|
123
|
-
directory_listing_header = (
|
|
124
|
-
"\n[bold white on blue] DIRECTORY LISTING [/bold white on blue]"
|
|
125
|
-
)
|
|
126
|
-
output_lines.append(directory_listing_header)
|
|
127
|
-
|
|
128
|
-
directory_info = f"\U0001f4c2 [bold cyan]{directory}[/bold cyan] [dim](recursive={recursive})[/dim]\n"
|
|
129
|
-
output_lines.append(directory_info)
|
|
130
|
-
|
|
131
|
-
divider = "[dim]" + "─" * 100 + "\n" + "[/dim]"
|
|
132
|
-
output_lines.append(divider)
|
|
161
|
+
output_lines.append(f"DIRECTORY LISTING: {directory} (recursive={recursive})")
|
|
133
162
|
|
|
134
163
|
if not os.path.exists(directory):
|
|
135
|
-
error_msg =
|
|
136
|
-
|
|
137
|
-
)
|
|
138
|
-
output_lines.append(error_msg)
|
|
139
|
-
|
|
140
|
-
output_lines.append(divider)
|
|
141
|
-
return ListFileOutput(content="\n".join(output_lines))
|
|
164
|
+
error_msg = f"Error: Directory '{directory}' does not exist"
|
|
165
|
+
return ListFileOutput(content=error_msg, error=error_msg)
|
|
142
166
|
if not os.path.isdir(directory):
|
|
143
|
-
error_msg = f"
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
output_lines.append(divider)
|
|
147
|
-
return ListFileOutput(content="\n".join(output_lines))
|
|
167
|
+
error_msg = f"Error: '{directory}' is not a directory"
|
|
168
|
+
return ListFileOutput(content=error_msg, error=error_msg)
|
|
148
169
|
|
|
149
170
|
# Smart home directory detection - auto-limit recursion for performance
|
|
150
171
|
# But allow recursion in tests (when context=None) or when explicitly requested
|
|
151
172
|
if context is not None and is_likely_home_directory(directory) and recursive:
|
|
152
173
|
if not is_project_directory(directory):
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
info_msg = f"[dim]💡 To force recursive listing in home directory, use list_files('{directory}', recursive=True) explicitly[/dim]"
|
|
157
|
-
output_lines.append(info_msg)
|
|
174
|
+
output_lines.append(
|
|
175
|
+
"Warning: Detected home directory - limiting to non-recursive listing for performance"
|
|
176
|
+
)
|
|
158
177
|
recursive = False
|
|
159
178
|
|
|
160
179
|
# Create a temporary ignore file with our ignore patterns
|
|
@@ -178,10 +197,10 @@ def _list_files(
|
|
|
178
197
|
rg_path = venv_rg_exe_path
|
|
179
198
|
break
|
|
180
199
|
|
|
181
|
-
if not rg_path:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return ListFileOutput(content=
|
|
200
|
+
if not rg_path and recursive:
|
|
201
|
+
# Only need ripgrep for recursive listings
|
|
202
|
+
error_msg = "Error: ripgrep (rg) not found. Please install ripgrep to use this tool."
|
|
203
|
+
return ListFileOutput(content=error_msg, error=error_msg)
|
|
185
204
|
|
|
186
205
|
# Only use ripgrep for recursive listings
|
|
187
206
|
if recursive:
|
|
@@ -198,6 +217,10 @@ def _list_files(
|
|
|
198
217
|
) as f:
|
|
199
218
|
ignore_file = f.name
|
|
200
219
|
for pattern in DIR_IGNORE_PATTERNS:
|
|
220
|
+
# Skip patterns that would match the search directory itself
|
|
221
|
+
# For example, if searching in /tmp/test-dir, skip **/tmp/**
|
|
222
|
+
if would_match_directory(pattern, directory):
|
|
223
|
+
continue
|
|
201
224
|
f.write(f"{pattern}\n")
|
|
202
225
|
|
|
203
226
|
cmd.extend(["--ignore-file", ignore_file])
|
|
@@ -290,8 +313,6 @@ def _list_files(
|
|
|
290
313
|
# ripgrep's --files option only returns files; we add directories and files ourselves
|
|
291
314
|
if not recursive:
|
|
292
315
|
try:
|
|
293
|
-
from code_puppy.tools.common import should_ignore_dir_path
|
|
294
|
-
|
|
295
316
|
entries = os.listdir(directory)
|
|
296
317
|
for entry in sorted(entries):
|
|
297
318
|
full_entry_path = os.path.join(directory, entry)
|
|
@@ -299,8 +320,9 @@ def _list_files(
|
|
|
299
320
|
continue
|
|
300
321
|
|
|
301
322
|
if os.path.isdir(full_entry_path):
|
|
302
|
-
#
|
|
303
|
-
|
|
323
|
+
# In non-recursive mode, only skip obviously system/hidden directories
|
|
324
|
+
# Don't use the full should_ignore_dir_path which is too aggressive
|
|
325
|
+
if entry.startswith("."):
|
|
304
326
|
continue
|
|
305
327
|
results.append(
|
|
306
328
|
ListedFile(
|
|
@@ -330,17 +352,11 @@ def _list_files(
|
|
|
330
352
|
# Skip entries we can't access
|
|
331
353
|
pass
|
|
332
354
|
except subprocess.TimeoutExpired:
|
|
333
|
-
error_msg =
|
|
334
|
-
|
|
335
|
-
)
|
|
336
|
-
output_lines.append(error_msg)
|
|
337
|
-
return ListFileOutput(content="\n".join(output_lines))
|
|
355
|
+
error_msg = "Error: List files command timed out after 30 seconds"
|
|
356
|
+
return ListFileOutput(content=error_msg, error=error_msg)
|
|
338
357
|
except Exception as e:
|
|
339
|
-
error_msg =
|
|
340
|
-
|
|
341
|
-
)
|
|
342
|
-
output_lines.append(error_msg)
|
|
343
|
-
return ListFileOutput(content="\n".join(output_lines))
|
|
358
|
+
error_msg = f"Error: Error during list files operation: {e}"
|
|
359
|
+
return ListFileOutput(content=error_msg, error=error_msg)
|
|
344
360
|
finally:
|
|
345
361
|
# Clean up the temporary ignore file
|
|
346
362
|
if ignore_file and os.path.exists(ignore_file):
|
|
@@ -390,59 +406,48 @@ def _list_files(
|
|
|
390
406
|
file_count = sum(1 for item in results if item.type == "file")
|
|
391
407
|
total_size = sum(item.size for item in results if item.type == "file")
|
|
392
408
|
|
|
393
|
-
# Build
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
output_lines.append(dir_header)
|
|
397
|
-
|
|
398
|
-
# Sort all items by path for consistent display
|
|
399
|
-
all_items = sorted(results, key=lambda x: x.path)
|
|
400
|
-
|
|
401
|
-
# Build file and directory tree representation
|
|
402
|
-
parent_dirs_with_content = set()
|
|
403
|
-
for item in all_items:
|
|
404
|
-
# Skip root directory entries with no path
|
|
409
|
+
# Build structured FileEntry objects for the UI
|
|
410
|
+
file_entries = []
|
|
411
|
+
for item in sorted(results, key=lambda x: x.path):
|
|
405
412
|
if item.type == "directory" and not item.path:
|
|
406
413
|
continue
|
|
414
|
+
file_entries.append(
|
|
415
|
+
FileEntry(
|
|
416
|
+
path=item.path,
|
|
417
|
+
type="dir" if item.type == "directory" else "file",
|
|
418
|
+
size=item.size,
|
|
419
|
+
depth=item.depth or 0,
|
|
420
|
+
)
|
|
421
|
+
)
|
|
407
422
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
prefix += "\u2514\u2500\u2500 "
|
|
419
|
-
else:
|
|
420
|
-
prefix += " "
|
|
423
|
+
# Emit structured message for the UI
|
|
424
|
+
file_listing_msg = FileListingMessage(
|
|
425
|
+
directory=directory,
|
|
426
|
+
files=file_entries,
|
|
427
|
+
recursive=recursive,
|
|
428
|
+
total_size=total_size,
|
|
429
|
+
dir_count=dir_count,
|
|
430
|
+
file_count=file_count,
|
|
431
|
+
)
|
|
432
|
+
get_message_bus().emit(file_listing_msg)
|
|
421
433
|
|
|
422
|
-
|
|
434
|
+
# Build plain text output for LLM consumption
|
|
435
|
+
for item in sorted(results, key=lambda x: x.path):
|
|
436
|
+
if item.type == "directory" and not item.path:
|
|
437
|
+
continue
|
|
423
438
|
name = os.path.basename(item.path) or item.path
|
|
424
|
-
|
|
425
|
-
# Add directory or file line with appropriate formatting
|
|
439
|
+
indent = " " * (item.depth or 0)
|
|
426
440
|
if item.type == "directory":
|
|
427
|
-
|
|
428
|
-
output_lines.append(dir_line)
|
|
441
|
+
output_lines.append(f"{indent}{name}/")
|
|
429
442
|
else:
|
|
430
|
-
icon = get_file_icon(item.path)
|
|
431
443
|
size_str = format_size(item.size)
|
|
432
|
-
|
|
433
|
-
output_lines.append(file_line)
|
|
434
|
-
|
|
435
|
-
# Add summary information
|
|
436
|
-
summary_header = "\n[bold cyan]Summary:[/bold cyan]"
|
|
437
|
-
output_lines.append(summary_header)
|
|
438
|
-
|
|
439
|
-
summary_line = f"\U0001f4c1 [blue]{dir_count} directories[/blue], \U0001f4c4 [green]{file_count} files[/green] [dim]({format_size(total_size)} total)[/dim]"
|
|
440
|
-
output_lines.append(summary_line)
|
|
444
|
+
output_lines.append(f"{indent}{name} ({size_str})")
|
|
441
445
|
|
|
442
|
-
|
|
443
|
-
output_lines.append(
|
|
446
|
+
# Add summary
|
|
447
|
+
output_lines.append(
|
|
448
|
+
f"\nSummary: {dir_count} directories, {file_count} files ({format_size(total_size)} total)"
|
|
449
|
+
)
|
|
444
450
|
|
|
445
|
-
# Return the content string
|
|
446
451
|
return ListFileOutput(content="\n".join(output_lines))
|
|
447
452
|
|
|
448
453
|
|
|
@@ -454,16 +459,6 @@ def _read_file(
|
|
|
454
459
|
) -> ReadFileOutput:
|
|
455
460
|
file_path = os.path.abspath(os.path.expanduser(file_path))
|
|
456
461
|
|
|
457
|
-
# Generate group_id for this tool execution
|
|
458
|
-
group_id = generate_group_id("read_file", file_path)
|
|
459
|
-
|
|
460
|
-
# Build console message with optional parameters
|
|
461
|
-
console_msg = f"\n[bold white on blue] READ FILE [/bold white on blue] \U0001f4c2 [bold cyan]{file_path}[/bold cyan]"
|
|
462
|
-
if start_line is not None and num_lines is not None:
|
|
463
|
-
console_msg += f" [dim](lines {start_line}-{start_line + num_lines - 1})[/dim]"
|
|
464
|
-
emit_info(console_msg, message_group=group_id)
|
|
465
|
-
|
|
466
|
-
emit_divider(message_group=group_id)
|
|
467
462
|
if not os.path.exists(file_path):
|
|
468
463
|
error_msg = f"File {file_path} does not exist"
|
|
469
464
|
return ReadFileOutput(content=error_msg, num_tokens=0, error=error_msg)
|
|
@@ -471,12 +466,15 @@ def _read_file(
|
|
|
471
466
|
error_msg = f"{file_path} is not a file"
|
|
472
467
|
return ReadFileOutput(content=error_msg, num_tokens=0, error=error_msg)
|
|
473
468
|
try:
|
|
474
|
-
|
|
469
|
+
# Use errors="surrogateescape" to handle files with invalid UTF-8 sequences
|
|
470
|
+
# This is common on Windows when files contain emojis or were created by
|
|
471
|
+
# applications that don't properly encode Unicode
|
|
472
|
+
with open(file_path, "r", encoding="utf-8", errors="surrogateescape") as f:
|
|
475
473
|
if start_line is not None and num_lines is not None:
|
|
476
474
|
# Read only the specified lines
|
|
477
475
|
lines = f.readlines()
|
|
478
|
-
# Adjust for 1-based line numbering
|
|
479
|
-
start_idx = start_line - 1
|
|
476
|
+
# Adjust for 1-based line numbering and handle negative values
|
|
477
|
+
start_idx = start_line - 1 if start_line > 0 else 0
|
|
480
478
|
end_idx = start_idx + num_lines
|
|
481
479
|
# Ensure indices are within bounds
|
|
482
480
|
start_idx = max(0, start_idx)
|
|
@@ -486,6 +484,21 @@ def _read_file(
|
|
|
486
484
|
# Read the entire file
|
|
487
485
|
content = f.read()
|
|
488
486
|
|
|
487
|
+
# Sanitize the content to remove any surrogate characters that could
|
|
488
|
+
# cause issues when the content is later serialized or displayed
|
|
489
|
+
# This re-encodes with surrogatepass then decodes with replace to
|
|
490
|
+
# convert lone surrogates to replacement characters
|
|
491
|
+
try:
|
|
492
|
+
content = content.encode("utf-8", errors="surrogatepass").decode(
|
|
493
|
+
"utf-8", errors="replace"
|
|
494
|
+
)
|
|
495
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
496
|
+
# If that fails, do a more aggressive cleanup
|
|
497
|
+
content = "".join(
|
|
498
|
+
char if ord(char) < 0xD800 or ord(char) > 0xDFFF else "\ufffd"
|
|
499
|
+
for char in content
|
|
500
|
+
)
|
|
501
|
+
|
|
489
502
|
# Simple approximation: ~4 characters per token
|
|
490
503
|
num_tokens = len(content) // 4
|
|
491
504
|
if num_tokens > 10000:
|
|
@@ -494,6 +507,30 @@ def _read_file(
|
|
|
494
507
|
error="The file is massive, greater than 10,000 tokens which is dangerous to read entirely. Please read this file in chunks.",
|
|
495
508
|
num_tokens=0,
|
|
496
509
|
)
|
|
510
|
+
|
|
511
|
+
# Count total lines for the message
|
|
512
|
+
total_lines = content.count("\n") + (
|
|
513
|
+
1 if content and not content.endswith("\n") else 0
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# Emit structured message for the UI
|
|
517
|
+
# Only include start_line/num_lines if they are valid positive integers
|
|
518
|
+
emit_start_line = (
|
|
519
|
+
start_line if start_line is not None and start_line >= 1 else None
|
|
520
|
+
)
|
|
521
|
+
emit_num_lines = (
|
|
522
|
+
num_lines if num_lines is not None and num_lines >= 1 else None
|
|
523
|
+
)
|
|
524
|
+
file_content_msg = FileContentMessage(
|
|
525
|
+
path=file_path,
|
|
526
|
+
content=content,
|
|
527
|
+
start_line=emit_start_line,
|
|
528
|
+
num_lines=emit_num_lines,
|
|
529
|
+
total_lines=total_lines,
|
|
530
|
+
num_tokens=num_tokens,
|
|
531
|
+
)
|
|
532
|
+
get_message_bus().emit(file_content_msg)
|
|
533
|
+
|
|
497
534
|
return ReadFileOutput(content=content, num_tokens=num_tokens)
|
|
498
535
|
except (FileNotFoundError, PermissionError):
|
|
499
536
|
# For backward compatibility with tests, return "FILE NOT FOUND" for these specific errors
|
|
@@ -504,6 +541,33 @@ def _read_file(
|
|
|
504
541
|
return ReadFileOutput(content=message, num_tokens=0, error=message)
|
|
505
542
|
|
|
506
543
|
|
|
544
|
+
def _sanitize_string(text: str) -> str:
|
|
545
|
+
"""Sanitize a string to remove invalid Unicode surrogates.
|
|
546
|
+
|
|
547
|
+
This handles encoding issues common on Windows with copy-paste operations.
|
|
548
|
+
"""
|
|
549
|
+
if not text:
|
|
550
|
+
return text
|
|
551
|
+
try:
|
|
552
|
+
# Try encoding - if it works, string is clean
|
|
553
|
+
text.encode("utf-8")
|
|
554
|
+
return text
|
|
555
|
+
except UnicodeEncodeError:
|
|
556
|
+
pass
|
|
557
|
+
|
|
558
|
+
try:
|
|
559
|
+
# Encode allowing surrogates, then decode replacing them
|
|
560
|
+
return text.encode("utf-8", errors="surrogatepass").decode(
|
|
561
|
+
"utf-8", errors="replace"
|
|
562
|
+
)
|
|
563
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
564
|
+
# Last resort: filter out surrogate characters
|
|
565
|
+
return "".join(
|
|
566
|
+
char if ord(char) < 0xD800 or ord(char) > 0xDFFF else "\ufffd"
|
|
567
|
+
for char in text
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
|
|
507
571
|
def _grep(context: RunContext, search_string: str, directory: str = ".") -> GrepOutput:
|
|
508
572
|
import json
|
|
509
573
|
import os
|
|
@@ -511,17 +575,12 @@ def _grep(context: RunContext, search_string: str, directory: str = ".") -> Grep
|
|
|
511
575
|
import subprocess
|
|
512
576
|
import sys
|
|
513
577
|
|
|
578
|
+
# Sanitize search string to handle any surrogates from copy-paste
|
|
579
|
+
search_string = _sanitize_string(search_string)
|
|
580
|
+
|
|
514
581
|
directory = os.path.abspath(os.path.expanduser(directory))
|
|
515
582
|
matches: List[MatchInfo] = []
|
|
516
|
-
|
|
517
|
-
# Generate group_id for this tool execution
|
|
518
|
-
group_id = generate_group_id("grep", f"{directory}_{search_string}")
|
|
519
|
-
|
|
520
|
-
emit_info(
|
|
521
|
-
f"\n[bold white on blue] GREP [/bold white on blue] \U0001f4c2 [bold cyan]{directory}[/bold cyan] [dim]for '{search_string}'[/dim]",
|
|
522
|
-
message_group=group_id,
|
|
523
|
-
)
|
|
524
|
-
emit_divider(message_group=group_id)
|
|
583
|
+
error_message: str | None = None
|
|
525
584
|
|
|
526
585
|
# Create a temporary ignore file with our ignore patterns
|
|
527
586
|
ignore_file = None
|
|
@@ -553,11 +612,10 @@ def _grep(context: RunContext, search_string: str, directory: str = ".") -> Grep
|
|
|
553
612
|
break
|
|
554
613
|
|
|
555
614
|
if not rg_path:
|
|
556
|
-
|
|
557
|
-
"ripgrep (rg) not found. Please install ripgrep to use this tool."
|
|
558
|
-
message_group=group_id,
|
|
615
|
+
error_message = (
|
|
616
|
+
"ripgrep (rg) not found. Please install ripgrep to use this tool."
|
|
559
617
|
)
|
|
560
|
-
return GrepOutput(matches=[])
|
|
618
|
+
return GrepOutput(matches=[], error=error_message)
|
|
561
619
|
|
|
562
620
|
cmd = [
|
|
563
621
|
rg_path,
|
|
@@ -579,7 +637,15 @@ def _grep(context: RunContext, search_string: str, directory: str = ".") -> Grep
|
|
|
579
637
|
|
|
580
638
|
cmd.extend(["--ignore-file", ignore_file])
|
|
581
639
|
cmd.extend([search_string, directory])
|
|
582
|
-
|
|
640
|
+
# Use encoding with error handling to handle files with invalid UTF-8
|
|
641
|
+
result = subprocess.run(
|
|
642
|
+
cmd,
|
|
643
|
+
capture_output=True,
|
|
644
|
+
text=True,
|
|
645
|
+
timeout=30,
|
|
646
|
+
encoding="utf-8",
|
|
647
|
+
errors="replace", # Replace invalid chars instead of crashing
|
|
648
|
+
)
|
|
583
649
|
|
|
584
650
|
# Parse the JSON output from ripgrep
|
|
585
651
|
for line in result.stdout.strip().split("\n"):
|
|
@@ -603,49 +669,57 @@ def _grep(context: RunContext, search_string: str, directory: str = ".") -> Grep
|
|
|
603
669
|
if len(line_content.strip()) > 512:
|
|
604
670
|
line_content = line_content.strip()[0:512]
|
|
605
671
|
if file_path and line_number:
|
|
672
|
+
# Sanitize content to handle any remaining encoding issues
|
|
606
673
|
match_info = MatchInfo(
|
|
607
|
-
file_path=file_path,
|
|
674
|
+
file_path=_sanitize_string(file_path),
|
|
608
675
|
line_number=line_number,
|
|
609
|
-
line_content=line_content.strip(),
|
|
676
|
+
line_content=_sanitize_string(line_content.strip()),
|
|
610
677
|
)
|
|
611
678
|
matches.append(match_info)
|
|
612
679
|
# Limit to 50 matches total, same as original implementation
|
|
613
680
|
if len(matches) >= 50:
|
|
614
681
|
break
|
|
615
|
-
emit_system_message(
|
|
616
|
-
f"[green]Match:[/green] {file_path}:{line_number} - {line_content.strip()}",
|
|
617
|
-
message_group=group_id,
|
|
618
|
-
)
|
|
619
682
|
except json.JSONDecodeError:
|
|
620
683
|
# Skip lines that aren't valid JSON
|
|
621
684
|
continue
|
|
622
685
|
|
|
623
|
-
if not matches:
|
|
624
|
-
emit_warning(
|
|
625
|
-
f"No matches found for '{search_string}' in {directory}",
|
|
626
|
-
message_group=group_id,
|
|
627
|
-
)
|
|
628
|
-
else:
|
|
629
|
-
emit_success(
|
|
630
|
-
f"Found {len(matches)} match(es) for '{search_string}' in {directory}",
|
|
631
|
-
message_group=group_id,
|
|
632
|
-
)
|
|
633
|
-
|
|
634
686
|
except subprocess.TimeoutExpired:
|
|
635
|
-
|
|
687
|
+
error_message = "Grep command timed out after 30 seconds"
|
|
636
688
|
except FileNotFoundError:
|
|
637
|
-
|
|
638
|
-
"ripgrep (rg) not found. Please install ripgrep to use this tool."
|
|
639
|
-
message_group=group_id,
|
|
689
|
+
error_message = (
|
|
690
|
+
"ripgrep (rg) not found. Please install ripgrep to use this tool."
|
|
640
691
|
)
|
|
641
692
|
except Exception as e:
|
|
642
|
-
|
|
693
|
+
error_message = f"Error during grep operation: {e}"
|
|
643
694
|
finally:
|
|
644
695
|
# Clean up the temporary ignore file
|
|
645
696
|
if ignore_file and os.path.exists(ignore_file):
|
|
646
697
|
os.unlink(ignore_file)
|
|
647
698
|
|
|
648
|
-
|
|
699
|
+
# Build structured GrepMatch objects for the UI
|
|
700
|
+
grep_matches = [
|
|
701
|
+
GrepMatch(
|
|
702
|
+
file_path=m.file_path or "",
|
|
703
|
+
line_number=m.line_number or 1,
|
|
704
|
+
line_content=m.line_content or "",
|
|
705
|
+
)
|
|
706
|
+
for m in matches
|
|
707
|
+
]
|
|
708
|
+
|
|
709
|
+
# Count unique files searched (approximation based on matches)
|
|
710
|
+
unique_files = len(set(m.file_path for m in matches)) if matches else 0
|
|
711
|
+
|
|
712
|
+
# Emit structured message for the UI (only once, at the end)
|
|
713
|
+
grep_result_msg = GrepResultMessage(
|
|
714
|
+
search_term=search_string,
|
|
715
|
+
directory=directory,
|
|
716
|
+
matches=grep_matches,
|
|
717
|
+
total_matches=len(matches),
|
|
718
|
+
files_searched=unique_files,
|
|
719
|
+
)
|
|
720
|
+
get_message_bus().emit(grep_result_msg)
|
|
721
|
+
|
|
722
|
+
return GrepOutput(matches=matches, error=error_message)
|
|
649
723
|
|
|
650
724
|
|
|
651
725
|
def register_list_files(agent):
|
|
@@ -706,10 +780,8 @@ def register_list_files(agent):
|
|
|
706
780
|
recursive = False
|
|
707
781
|
result = _list_files(context, directory, recursive)
|
|
708
782
|
|
|
709
|
-
#
|
|
710
|
-
|
|
711
|
-
result.content, message_group=generate_group_id("list_files", directory)
|
|
712
|
-
)
|
|
783
|
+
# The structured FileListingMessage is already emitted by _list_files
|
|
784
|
+
# No need to emit again here
|
|
713
785
|
if warning:
|
|
714
786
|
result.error = warning
|
|
715
787
|
if (len(result.content)) > 200000:
|