vibecore 0.2.0a1__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.

Files changed (63) hide show
  1. vibecore/__init__.py +0 -0
  2. vibecore/agents/default.py +79 -0
  3. vibecore/agents/prompts.py +12 -0
  4. vibecore/agents/task_agent.py +66 -0
  5. vibecore/cli.py +131 -0
  6. vibecore/context.py +24 -0
  7. vibecore/handlers/__init__.py +5 -0
  8. vibecore/handlers/stream_handler.py +231 -0
  9. vibecore/main.py +506 -0
  10. vibecore/main.tcss +0 -0
  11. vibecore/mcp/__init__.py +6 -0
  12. vibecore/mcp/manager.py +167 -0
  13. vibecore/mcp/server_wrapper.py +109 -0
  14. vibecore/models/__init__.py +5 -0
  15. vibecore/models/anthropic.py +239 -0
  16. vibecore/prompts/common_system_prompt.txt +64 -0
  17. vibecore/py.typed +0 -0
  18. vibecore/session/__init__.py +5 -0
  19. vibecore/session/file_lock.py +127 -0
  20. vibecore/session/jsonl_session.py +236 -0
  21. vibecore/session/loader.py +193 -0
  22. vibecore/session/path_utils.py +81 -0
  23. vibecore/settings.py +161 -0
  24. vibecore/tools/__init__.py +1 -0
  25. vibecore/tools/base.py +27 -0
  26. vibecore/tools/file/__init__.py +5 -0
  27. vibecore/tools/file/executor.py +282 -0
  28. vibecore/tools/file/tools.py +184 -0
  29. vibecore/tools/file/utils.py +78 -0
  30. vibecore/tools/python/__init__.py +1 -0
  31. vibecore/tools/python/backends/__init__.py +1 -0
  32. vibecore/tools/python/backends/terminal_backend.py +58 -0
  33. vibecore/tools/python/helpers.py +80 -0
  34. vibecore/tools/python/manager.py +208 -0
  35. vibecore/tools/python/tools.py +27 -0
  36. vibecore/tools/shell/__init__.py +5 -0
  37. vibecore/tools/shell/executor.py +223 -0
  38. vibecore/tools/shell/tools.py +156 -0
  39. vibecore/tools/task/__init__.py +5 -0
  40. vibecore/tools/task/executor.py +51 -0
  41. vibecore/tools/task/tools.py +51 -0
  42. vibecore/tools/todo/__init__.py +1 -0
  43. vibecore/tools/todo/manager.py +31 -0
  44. vibecore/tools/todo/models.py +36 -0
  45. vibecore/tools/todo/tools.py +111 -0
  46. vibecore/utils/__init__.py +5 -0
  47. vibecore/utils/text.py +28 -0
  48. vibecore/widgets/core.py +332 -0
  49. vibecore/widgets/core.tcss +63 -0
  50. vibecore/widgets/expandable.py +121 -0
  51. vibecore/widgets/expandable.tcss +69 -0
  52. vibecore/widgets/info.py +25 -0
  53. vibecore/widgets/info.tcss +17 -0
  54. vibecore/widgets/messages.py +232 -0
  55. vibecore/widgets/messages.tcss +85 -0
  56. vibecore/widgets/tool_message_factory.py +121 -0
  57. vibecore/widgets/tool_messages.py +483 -0
  58. vibecore/widgets/tool_messages.tcss +289 -0
  59. vibecore-0.2.0a1.dist-info/METADATA +407 -0
  60. vibecore-0.2.0a1.dist-info/RECORD +63 -0
  61. vibecore-0.2.0a1.dist-info/WHEEL +4 -0
  62. vibecore-0.2.0a1.dist-info/entry_points.txt +2 -0
  63. vibecore-0.2.0a1.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![Plot {i + 1}](file://{temp_path})")
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,5 @@
1
+ """Shell and system tools for Vibecore agents."""
2
+
3
+ from .tools import bash, glob, grep, ls
4
+
5
+ __all__ = ["bash", "glob", "grep", "ls"]
@@ -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}"]