vibecore 0.2.0__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.
Potentially problematic release.
This version of vibecore might be problematic. Click here for more details.
- vibecore/__init__.py +0 -0
- vibecore/agents/default.py +79 -0
- vibecore/agents/prompts.py +12 -0
- vibecore/agents/task_agent.py +66 -0
- vibecore/cli.py +150 -0
- vibecore/context.py +24 -0
- vibecore/handlers/__init__.py +5 -0
- vibecore/handlers/stream_handler.py +231 -0
- vibecore/main.py +506 -0
- vibecore/main.tcss +0 -0
- vibecore/mcp/__init__.py +6 -0
- vibecore/mcp/manager.py +167 -0
- vibecore/mcp/server_wrapper.py +109 -0
- vibecore/models/__init__.py +5 -0
- vibecore/models/anthropic.py +239 -0
- vibecore/prompts/common_system_prompt.txt +64 -0
- vibecore/py.typed +0 -0
- vibecore/session/__init__.py +5 -0
- vibecore/session/file_lock.py +127 -0
- vibecore/session/jsonl_session.py +236 -0
- vibecore/session/loader.py +193 -0
- vibecore/session/path_utils.py +81 -0
- vibecore/settings.py +161 -0
- vibecore/tools/__init__.py +1 -0
- vibecore/tools/base.py +27 -0
- vibecore/tools/file/__init__.py +5 -0
- vibecore/tools/file/executor.py +282 -0
- vibecore/tools/file/tools.py +184 -0
- vibecore/tools/file/utils.py +78 -0
- vibecore/tools/python/__init__.py +1 -0
- vibecore/tools/python/backends/__init__.py +1 -0
- vibecore/tools/python/backends/terminal_backend.py +58 -0
- vibecore/tools/python/helpers.py +80 -0
- vibecore/tools/python/manager.py +208 -0
- vibecore/tools/python/tools.py +27 -0
- vibecore/tools/shell/__init__.py +5 -0
- vibecore/tools/shell/executor.py +223 -0
- vibecore/tools/shell/tools.py +156 -0
- vibecore/tools/task/__init__.py +5 -0
- vibecore/tools/task/executor.py +51 -0
- vibecore/tools/task/tools.py +51 -0
- vibecore/tools/todo/__init__.py +1 -0
- vibecore/tools/todo/manager.py +31 -0
- vibecore/tools/todo/models.py +36 -0
- vibecore/tools/todo/tools.py +111 -0
- vibecore/utils/__init__.py +5 -0
- vibecore/utils/text.py +28 -0
- vibecore/widgets/core.py +332 -0
- vibecore/widgets/core.tcss +63 -0
- vibecore/widgets/expandable.py +121 -0
- vibecore/widgets/expandable.tcss +69 -0
- vibecore/widgets/info.py +25 -0
- vibecore/widgets/info.tcss +17 -0
- vibecore/widgets/messages.py +232 -0
- vibecore/widgets/messages.tcss +85 -0
- vibecore/widgets/tool_message_factory.py +121 -0
- vibecore/widgets/tool_messages.py +483 -0
- vibecore/widgets/tool_messages.tcss +289 -0
- vibecore-0.2.0.dist-info/METADATA +407 -0
- vibecore-0.2.0.dist-info/RECORD +63 -0
- vibecore-0.2.0.dist-info/WHEEL +4 -0
- vibecore-0.2.0.dist-info/entry_points.txt +2 -0
- vibecore-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Helper functions for Python execution tool."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
|
|
6
|
+
from agents import RunContextWrapper
|
|
7
|
+
|
|
8
|
+
from vibecore.context import VibecoreContext
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from PIL import Image # type: ignore[import-not-found]
|
|
12
|
+
from term_image.image import AutoImage # type: ignore[import-not-found]
|
|
13
|
+
|
|
14
|
+
TERM_IMAGE_AVAILABLE = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
TERM_IMAGE_AVAILABLE = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def execute_python_helper(ctx: RunContextWrapper[VibecoreContext], code: str) -> str:
|
|
20
|
+
"""Helper function to execute Python code.
|
|
21
|
+
|
|
22
|
+
This is the actual implementation extracted from the tool decorator.
|
|
23
|
+
"""
|
|
24
|
+
result = await ctx.context.python_manager.execute(code)
|
|
25
|
+
|
|
26
|
+
# Format the response
|
|
27
|
+
response_parts = []
|
|
28
|
+
|
|
29
|
+
if result.output:
|
|
30
|
+
response_parts.append(f"Output:\n```\n{result.output}```")
|
|
31
|
+
|
|
32
|
+
if result.error:
|
|
33
|
+
response_parts.append(f"Error:\n```\n{result.error}```")
|
|
34
|
+
|
|
35
|
+
if result.value is not None and not result.output:
|
|
36
|
+
# Only show the value if there was no print output
|
|
37
|
+
response_parts.append(f"Result: `{result.value}`")
|
|
38
|
+
|
|
39
|
+
# Display any captured matplotlib images
|
|
40
|
+
if result.images:
|
|
41
|
+
for i, image_data in enumerate(result.images):
|
|
42
|
+
try:
|
|
43
|
+
# Save image to temporary file
|
|
44
|
+
with tempfile.NamedTemporaryFile(mode="wb", suffix=".png", delete=False) as tmp_file:
|
|
45
|
+
tmp_file.write(image_data)
|
|
46
|
+
temp_path = tmp_file.name
|
|
47
|
+
|
|
48
|
+
# Add Markdown image reference to response
|
|
49
|
+
response_parts.append(f"\n")
|
|
50
|
+
|
|
51
|
+
# Display in terminal if term-image is available
|
|
52
|
+
if TERM_IMAGE_AVAILABLE:
|
|
53
|
+
# Load image from bytes for terminal display
|
|
54
|
+
buf = BytesIO(image_data)
|
|
55
|
+
pil_image = Image.open(buf) # type: ignore
|
|
56
|
+
|
|
57
|
+
# Use AutoImage for automatic terminal detection
|
|
58
|
+
term_image = AutoImage(pil_image, width=80) # type: ignore
|
|
59
|
+
|
|
60
|
+
# Display the image
|
|
61
|
+
term_image.draw(h_align="center", v_align="top", pad_width=1, pad_height=1) # type: ignore
|
|
62
|
+
|
|
63
|
+
# Close the image
|
|
64
|
+
pil_image.close()
|
|
65
|
+
buf.close()
|
|
66
|
+
|
|
67
|
+
# Note that an image was displayed
|
|
68
|
+
if i == 0:
|
|
69
|
+
response_parts.append("[Image displayed in terminal]")
|
|
70
|
+
else:
|
|
71
|
+
# Note that term-image is not available
|
|
72
|
+
if i == 0:
|
|
73
|
+
response_parts.append("[Matplotlib plots saved to temporary files]")
|
|
74
|
+
except Exception as e:
|
|
75
|
+
response_parts.append(f"\n[Error processing image {i + 1}: {e}]")
|
|
76
|
+
|
|
77
|
+
if not response_parts:
|
|
78
|
+
return "Code executed successfully (no output)."
|
|
79
|
+
|
|
80
|
+
return "\n\n".join(response_parts)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Python code execution manager."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import sys
|
|
5
|
+
import warnings
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from io import StringIO
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ExecutionResult:
|
|
13
|
+
"""Result of Python code execution."""
|
|
14
|
+
|
|
15
|
+
success: bool
|
|
16
|
+
output: str
|
|
17
|
+
error: str
|
|
18
|
+
value: Any = None
|
|
19
|
+
images: list[bytes] | None = None # Store captured matplotlib images
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TerminalImageCapture(StringIO):
|
|
23
|
+
"""Custom StringIO that captures term-image output."""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
super().__init__()
|
|
27
|
+
self.has_term_image_output = False
|
|
28
|
+
|
|
29
|
+
def write(self, s: str) -> int:
|
|
30
|
+
# Check if this is term-image output (usually contains escape sequences)
|
|
31
|
+
if "\033[" in s or "\x1b[" in s:
|
|
32
|
+
self.has_term_image_output = True
|
|
33
|
+
return super().write(s)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PythonExecutionManager:
|
|
37
|
+
"""Manages Python code execution with persistent context."""
|
|
38
|
+
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
"""Initialize the execution manager with empty context."""
|
|
41
|
+
self.globals: dict[str, Any] = {"__builtins__": __builtins__}
|
|
42
|
+
self.locals: dict[str, Any] = {}
|
|
43
|
+
self._setup_matplotlib_backend()
|
|
44
|
+
|
|
45
|
+
def _setup_matplotlib_backend(self) -> None:
|
|
46
|
+
"""Set up the terminal matplotlib backend."""
|
|
47
|
+
# Pre-configure matplotlib to use our custom backend
|
|
48
|
+
# This will take effect when matplotlib is imported
|
|
49
|
+
# import os
|
|
50
|
+
|
|
51
|
+
# # Set the backend environment variable
|
|
52
|
+
# os.environ["MPLBACKEND"] = "module://vibecore.tools.python.backends.terminal_backend"
|
|
53
|
+
|
|
54
|
+
# # Also set it in the execution globals
|
|
55
|
+
# self.globals["__matplotlib_backend__"] = "module://vibecore.tools.python.backends.terminal_backend"
|
|
56
|
+
|
|
57
|
+
# Add a helper function to set matplotlib backend programmatically
|
|
58
|
+
backend_setup_code = """
|
|
59
|
+
def _setup_matplotlib_terminal():
|
|
60
|
+
'''Helper to ensure matplotlib uses terminal backend.'''
|
|
61
|
+
try:
|
|
62
|
+
import matplotlib
|
|
63
|
+
matplotlib.use('module://vibecore.tools.python.backends.terminal_backend')
|
|
64
|
+
except ImportError:
|
|
65
|
+
pass
|
|
66
|
+
_setup_matplotlib_terminal()
|
|
67
|
+
"""
|
|
68
|
+
exec(backend_setup_code, self.globals, self.globals)
|
|
69
|
+
|
|
70
|
+
async def execute(self, code: str) -> ExecutionResult:
|
|
71
|
+
"""Execute Python code and return the result.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
code: Python code to execute.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
ExecutionResult with success status, output, errors, and return value.
|
|
78
|
+
"""
|
|
79
|
+
# Capture stdout and stderr
|
|
80
|
+
old_stdout = sys.stdout
|
|
81
|
+
old_stderr = sys.stderr
|
|
82
|
+
stdout_capture = TerminalImageCapture()
|
|
83
|
+
stderr_capture = StringIO()
|
|
84
|
+
|
|
85
|
+
# Capture warnings
|
|
86
|
+
old_showwarning = warnings.showwarning
|
|
87
|
+
|
|
88
|
+
def custom_showwarning(message, category, filename, lineno, file=None, line=None):
|
|
89
|
+
stderr_capture.write(warnings.formatwarning(message, category, filename, lineno, line))
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
sys.stdout = stdout_capture
|
|
93
|
+
sys.stderr = stderr_capture
|
|
94
|
+
warnings.showwarning = custom_showwarning
|
|
95
|
+
|
|
96
|
+
# Parse the code to handle async functions
|
|
97
|
+
tree = ast.parse(code, mode="exec")
|
|
98
|
+
|
|
99
|
+
# Check if we need to run in async context
|
|
100
|
+
has_await = self._has_await_at_module_level(tree)
|
|
101
|
+
|
|
102
|
+
if has_await:
|
|
103
|
+
# Create an event loop if needed and run async code
|
|
104
|
+
result_value = await self._execute_async(code)
|
|
105
|
+
else:
|
|
106
|
+
# Normal execution
|
|
107
|
+
result_value = None
|
|
108
|
+
|
|
109
|
+
# Check if last statement is an expression we should evaluate
|
|
110
|
+
last_is_expr = tree.body and isinstance(tree.body[-1], ast.Expr)
|
|
111
|
+
|
|
112
|
+
if last_is_expr:
|
|
113
|
+
# Execute all but the last statement
|
|
114
|
+
if len(tree.body) > 1:
|
|
115
|
+
exec(
|
|
116
|
+
compile(ast.Module(body=tree.body[:-1], type_ignores=[]), "<string>", "exec"),
|
|
117
|
+
self.globals,
|
|
118
|
+
self.globals,
|
|
119
|
+
)
|
|
120
|
+
# Evaluate the last expression
|
|
121
|
+
last_expr = tree.body[-1]
|
|
122
|
+
assert isinstance(last_expr, ast.Expr) # We already checked this
|
|
123
|
+
result_value = eval(
|
|
124
|
+
compile(ast.Expression(body=last_expr.value), "<string>", "eval"),
|
|
125
|
+
self.globals,
|
|
126
|
+
self.globals,
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
# Execute everything normally
|
|
130
|
+
exec(code, self.globals, self.globals)
|
|
131
|
+
|
|
132
|
+
# Check for captured matplotlib images
|
|
133
|
+
images = None
|
|
134
|
+
try:
|
|
135
|
+
# Import the backend module to access captured images
|
|
136
|
+
from vibecore.tools.python.backends import terminal_backend
|
|
137
|
+
|
|
138
|
+
captured_images = terminal_backend.get_captured_images()
|
|
139
|
+
if captured_images:
|
|
140
|
+
images = captured_images
|
|
141
|
+
terminal_backend.clear_captured_images()
|
|
142
|
+
except Exception:
|
|
143
|
+
# If we can't get images, just continue without them
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
return ExecutionResult(
|
|
147
|
+
success=True,
|
|
148
|
+
output=stdout_capture.getvalue(),
|
|
149
|
+
error=stderr_capture.getvalue(),
|
|
150
|
+
value=result_value,
|
|
151
|
+
images=images,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
except SyntaxError as e:
|
|
155
|
+
return ExecutionResult(
|
|
156
|
+
success=False,
|
|
157
|
+
output="",
|
|
158
|
+
error=f"SyntaxError: {e}",
|
|
159
|
+
value=None,
|
|
160
|
+
)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
return ExecutionResult(
|
|
163
|
+
success=False,
|
|
164
|
+
output=stdout_capture.getvalue(),
|
|
165
|
+
error=f"{type(e).__name__}: {e}",
|
|
166
|
+
value=None,
|
|
167
|
+
)
|
|
168
|
+
finally:
|
|
169
|
+
# Restore stdout, stderr, and warnings
|
|
170
|
+
sys.stdout = old_stdout
|
|
171
|
+
sys.stderr = old_stderr
|
|
172
|
+
warnings.showwarning = old_showwarning
|
|
173
|
+
|
|
174
|
+
def _has_await_at_module_level(self, tree: ast.Module) -> bool:
|
|
175
|
+
"""Check if there are await expressions at module level."""
|
|
176
|
+
for node in tree.body:
|
|
177
|
+
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Await):
|
|
178
|
+
return True
|
|
179
|
+
# Check for await in direct assignments
|
|
180
|
+
if isinstance(node, ast.Assign) and isinstance(node.value, ast.Await):
|
|
181
|
+
return True
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
async def _execute_async(self, code: str) -> Any:
|
|
185
|
+
"""Execute code containing await expressions."""
|
|
186
|
+
# Wrap the entire code in an async function
|
|
187
|
+
lines = code.splitlines()
|
|
188
|
+
indented_code = "\n".join(f" {line}" if line.strip() else "" for line in lines)
|
|
189
|
+
wrapped_code = f"async def __async_exec():\n{indented_code}\n return None"
|
|
190
|
+
|
|
191
|
+
# Define the async function
|
|
192
|
+
exec(wrapped_code, self.globals, self.globals)
|
|
193
|
+
|
|
194
|
+
# Run it
|
|
195
|
+
result = await self.globals["__async_exec"]()
|
|
196
|
+
|
|
197
|
+
# Clean up
|
|
198
|
+
del self.globals["__async_exec"]
|
|
199
|
+
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
def reset_context(self) -> None:
|
|
203
|
+
"""Reset the execution context."""
|
|
204
|
+
matplotlib_backend = self.globals.get("__matplotlib_backend__")
|
|
205
|
+
self.globals = {"__builtins__": __builtins__}
|
|
206
|
+
if matplotlib_backend is not None:
|
|
207
|
+
self.globals["__matplotlib_backend__"] = matplotlib_backend
|
|
208
|
+
self.locals = {}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Python code execution tool."""
|
|
2
|
+
|
|
3
|
+
from agents import RunContextWrapper, function_tool
|
|
4
|
+
|
|
5
|
+
from vibecore.context import VibecoreContext
|
|
6
|
+
|
|
7
|
+
from .helpers import execute_python_helper
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@function_tool
|
|
11
|
+
async def execute_python(ctx: RunContextWrapper[VibecoreContext], code: str) -> str:
|
|
12
|
+
"""Execute Python code with persistent context across the session.
|
|
13
|
+
|
|
14
|
+
The execution environment maintains state between calls, allowing you to:
|
|
15
|
+
- Define variables and functions that persist
|
|
16
|
+
- Import modules that remain available
|
|
17
|
+
- Build up complex computations step by step
|
|
18
|
+
- Make sure to define code as function so that we can use it later
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
ctx: The run context wrapper containing the Python execution manager.
|
|
22
|
+
code: Python code to execute.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A string containing the execution result, output, or error message.
|
|
26
|
+
"""
|
|
27
|
+
return await execute_python_helper(ctx, code)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Shell command execution logic."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import glob as glob_module
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from vibecore.tools.file.utils import PathValidationError, validate_file_path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def bash_executor(command: str, timeout: int | None = None) -> tuple[str, int]:
|
|
13
|
+
"""Execute a bash command asynchronously.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
command: The bash command to execute
|
|
17
|
+
timeout: Optional timeout in milliseconds (max 600000)
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Tuple of (output, exit_code)
|
|
21
|
+
"""
|
|
22
|
+
# Set default timeout if not provided
|
|
23
|
+
if timeout is None:
|
|
24
|
+
timeout = 120000 # 2 minutes default
|
|
25
|
+
|
|
26
|
+
# Validate timeout
|
|
27
|
+
if timeout < 0:
|
|
28
|
+
return "Error: Timeout must be positive", 1
|
|
29
|
+
if timeout > 600000:
|
|
30
|
+
return "Error: Timeout cannot exceed 600000ms (10 minutes)", 1
|
|
31
|
+
|
|
32
|
+
# Convert timeout to seconds
|
|
33
|
+
timeout_seconds = timeout / 1000.0
|
|
34
|
+
|
|
35
|
+
process = None
|
|
36
|
+
try:
|
|
37
|
+
# Create subprocess
|
|
38
|
+
process = await asyncio.create_subprocess_shell(
|
|
39
|
+
command,
|
|
40
|
+
stdout=subprocess.PIPE,
|
|
41
|
+
stderr=subprocess.STDOUT,
|
|
42
|
+
shell=True,
|
|
43
|
+
executable="/bin/bash",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Wait for completion with timeout
|
|
47
|
+
stdout, _ = await asyncio.wait_for(process.communicate(), timeout=timeout_seconds)
|
|
48
|
+
|
|
49
|
+
# Decode output
|
|
50
|
+
output = stdout.decode("utf-8", errors="replace")
|
|
51
|
+
|
|
52
|
+
# Truncate if too long
|
|
53
|
+
if len(output) > 30000:
|
|
54
|
+
output = output[:30000] + "\n... (output truncated)"
|
|
55
|
+
|
|
56
|
+
return output, process.returncode or 0
|
|
57
|
+
|
|
58
|
+
except TimeoutError:
|
|
59
|
+
# Kill the process if it times out
|
|
60
|
+
if process and process.returncode is None:
|
|
61
|
+
process.kill()
|
|
62
|
+
await process.wait()
|
|
63
|
+
return f"Error: Command timed out after {timeout}ms", 124
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
return f"Error executing command: {e}", 1
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def glob_files(pattern: str, path: str | None = None) -> list[str]:
|
|
70
|
+
"""Find files matching a glob pattern.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
pattern: The glob pattern to match
|
|
74
|
+
path: Optional directory to search in (defaults to CWD)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
List of matching file paths sorted by modification time
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
# Validate and resolve the path
|
|
81
|
+
search_path = Path.cwd() if path is None else validate_file_path(path)
|
|
82
|
+
|
|
83
|
+
# Validate path is a directory
|
|
84
|
+
if not search_path.is_dir():
|
|
85
|
+
return [f"Error: Path is not a directory: {search_path}"]
|
|
86
|
+
except PathValidationError as e:
|
|
87
|
+
return [f"Error: {e}"]
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# Convert to absolute path
|
|
91
|
+
search_path = search_path.resolve()
|
|
92
|
+
|
|
93
|
+
# Perform glob search
|
|
94
|
+
full_pattern = str(search_path / pattern)
|
|
95
|
+
matches = list(glob_module.glob(full_pattern, recursive=True))
|
|
96
|
+
|
|
97
|
+
# Filter to only files (not directories)
|
|
98
|
+
file_matches = [m for m in matches if Path(m).is_file()]
|
|
99
|
+
|
|
100
|
+
# Sort by modification time (newest first)
|
|
101
|
+
file_matches.sort(key=lambda x: Path(x).stat().st_mtime, reverse=True)
|
|
102
|
+
|
|
103
|
+
# Return relative paths if searching in CWD, absolute otherwise
|
|
104
|
+
if path is None:
|
|
105
|
+
file_matches = [str(Path(m).relative_to(Path.cwd())) for m in file_matches]
|
|
106
|
+
|
|
107
|
+
return file_matches
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
return [f"Error: {e}"]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async def grep_files(pattern: str, path: str | None = None, include: str | None = None) -> list[str]:
|
|
114
|
+
"""Search file contents using regular expressions.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
pattern: The regex pattern to search for
|
|
118
|
+
path: Directory to search in (defaults to CWD)
|
|
119
|
+
include: File pattern to include (e.g. "*.js")
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
List of file paths containing matches, sorted by modification time
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
# Validate and resolve the path
|
|
126
|
+
search_path = Path.cwd() if path is None else validate_file_path(path)
|
|
127
|
+
|
|
128
|
+
# Validate path is a directory
|
|
129
|
+
if not search_path.is_dir():
|
|
130
|
+
return [f"Error: Path is not a directory: {search_path}"]
|
|
131
|
+
except PathValidationError as e:
|
|
132
|
+
return [f"Error: {e}"]
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
# Compile regex pattern
|
|
136
|
+
regex = re.compile(pattern)
|
|
137
|
+
except re.error as e:
|
|
138
|
+
return [f"Error: Invalid regex pattern: {e}"]
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
# Get all files to search
|
|
142
|
+
if include:
|
|
143
|
+
# Use glob to find files matching the include pattern
|
|
144
|
+
search_pattern = "**/" + include
|
|
145
|
+
all_files = list(search_path.glob(search_pattern))
|
|
146
|
+
else:
|
|
147
|
+
# Search all files recursively
|
|
148
|
+
all_files = [f for f in search_path.rglob("*") if f.is_file()]
|
|
149
|
+
|
|
150
|
+
# Search for pattern in each file
|
|
151
|
+
matching_files = []
|
|
152
|
+
for file_path in all_files:
|
|
153
|
+
try:
|
|
154
|
+
# Skip binary files
|
|
155
|
+
with file_path.open("r", encoding="utf-8", errors="replace") as f:
|
|
156
|
+
content = f.read()
|
|
157
|
+
if regex.search(content):
|
|
158
|
+
matching_files.append(file_path)
|
|
159
|
+
except Exception:
|
|
160
|
+
# Skip files that can't be read
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
# Sort by modification time (newest first)
|
|
164
|
+
matching_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
|
165
|
+
|
|
166
|
+
# Return relative paths if searching in CWD, absolute otherwise
|
|
167
|
+
if path is None:
|
|
168
|
+
result = [str(f.relative_to(Path.cwd())) for f in matching_files]
|
|
169
|
+
else:
|
|
170
|
+
result = [str(f) for f in matching_files]
|
|
171
|
+
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
return [f"Error: {e}"]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def list_directory(path: str, ignore: list[str] | None = None) -> list[str]:
|
|
179
|
+
"""List files and directories in a given path.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
path: The absolute path to list
|
|
183
|
+
ignore: Optional list of glob patterns to ignore
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
List of entries in the directory
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
# Validate and resolve the path
|
|
190
|
+
dir_path = validate_file_path(path)
|
|
191
|
+
|
|
192
|
+
# Validate path is a directory
|
|
193
|
+
if not dir_path.is_dir():
|
|
194
|
+
return [f"Error: Path is not a directory: {dir_path}"]
|
|
195
|
+
except PathValidationError as e:
|
|
196
|
+
return [f"Error: {e}"]
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
# Get all entries
|
|
200
|
+
entries = []
|
|
201
|
+
for entry in sorted(dir_path.iterdir()):
|
|
202
|
+
# Skip if matches ignore pattern
|
|
203
|
+
if ignore:
|
|
204
|
+
skip = False
|
|
205
|
+
for pattern in ignore:
|
|
206
|
+
if entry.match(pattern):
|
|
207
|
+
skip = True
|
|
208
|
+
break
|
|
209
|
+
if skip:
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
# Format entry
|
|
213
|
+
if entry.is_dir():
|
|
214
|
+
entries.append(f"{entry.name}/")
|
|
215
|
+
else:
|
|
216
|
+
entries.append(entry.name)
|
|
217
|
+
|
|
218
|
+
return entries
|
|
219
|
+
|
|
220
|
+
except PermissionError:
|
|
221
|
+
return [f"Error: Permission denied accessing directory: {path}"]
|
|
222
|
+
except Exception as e:
|
|
223
|
+
return [f"Error: {e}"]
|