kolega-code 0.1.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.
Files changed (171) hide show
  1. kolega_code/__init__.py +151 -0
  2. kolega_code/agent/__init__.py +42 -0
  3. kolega_code/agent/baseagent.py +998 -0
  4. kolega_code/agent/browseragent.py +123 -0
  5. kolega_code/agent/coder.py +157 -0
  6. kolega_code/agent/common.py +41 -0
  7. kolega_code/agent/compression.py +81 -0
  8. kolega_code/agent/context.py +112 -0
  9. kolega_code/agent/conversation.py +408 -0
  10. kolega_code/agent/generalagent.py +146 -0
  11. kolega_code/agent/investigationagent.py +123 -0
  12. kolega_code/agent/planningagent.py +187 -0
  13. kolega_code/agent/prompt_provider.py +196 -0
  14. kolega_code/agent/prompt_templates/agents/browser.j2 +102 -0
  15. kolega_code/agent/prompt_templates/agents/coder_cli_mode.j2 +127 -0
  16. kolega_code/agent/prompt_templates/agents/general.j2 +68 -0
  17. kolega_code/agent/prompt_templates/agents/investigation.j2 +72 -0
  18. kolega_code/agent/prompt_templates/common/frontend_guidance.md +36 -0
  19. kolega_code/agent/prompt_templates/common/kolega_md_instructions.md +14 -0
  20. kolega_code/agent/prompt_templates/environment_variables/workspace_env_vars.md +11 -0
  21. kolega_code/agent/prompt_templates/template_guidance/expo-template.md +379 -0
  22. kolega_code/agent/prompt_templates/template_guidance/html-website-template.md +3 -0
  23. kolega_code/agent/prompt_templates/template_guidance/mern-stack-template.md +3 -0
  24. kolega_code/agent/prompt_templates/template_guidance/react-vite-shadcdn-template.md +182 -0
  25. kolega_code/agent/prompts.py +192 -0
  26. kolega_code/agent/tests/__init__.py +0 -0
  27. kolega_code/agent/tests/llm/__init__.py +0 -0
  28. kolega_code/agent/tests/llm/test_anthropic_token_counting.py +633 -0
  29. kolega_code/agent/tests/llm/test_billing_openai_cache.py +74 -0
  30. kolega_code/agent/tests/llm/test_client.py +773 -0
  31. kolega_code/agent/tests/llm/test_dashscope_mapping.py +32 -0
  32. kolega_code/agent/tests/llm/test_error_boundary.py +322 -0
  33. kolega_code/agent/tests/llm/test_exceptions.py +249 -0
  34. kolega_code/agent/tests/llm/test_instrumented_client.py +536 -0
  35. kolega_code/agent/tests/llm/test_instrumented_client_integration.py +547 -0
  36. kolega_code/agent/tests/llm/test_langfuse_normalization.py +39 -0
  37. kolega_code/agent/tests/llm/test_model_specs.py +17 -0
  38. kolega_code/agent/tests/llm/test_openai_cached_tokens.py +58 -0
  39. kolega_code/agent/tests/llm/test_openai_cached_tokens_stream.py +74 -0
  40. kolega_code/agent/tests/llm/test_openai_message_conversion.py +30 -0
  41. kolega_code/agent/tests/llm/test_openai_token_counting.py +687 -0
  42. kolega_code/agent/tests/llm/test_tool_execution_ids.py +193 -0
  43. kolega_code/agent/tests/services/__init__.py +1 -0
  44. kolega_code/agent/tests/services/test_browser.py +447 -0
  45. kolega_code/agent/tests/services/test_browser_parity.py +353 -0
  46. kolega_code/agent/tests/services/test_file_system.py +699 -0
  47. kolega_code/agent/tests/services/test_sandbox_terminal_input.py +98 -0
  48. kolega_code/agent/tests/services/test_terminal.py +154 -0
  49. kolega_code/agent/tests/services/test_terminal_command_tracking.py +385 -0
  50. kolega_code/agent/tests/services/test_terminal_state_serializer.py +262 -0
  51. kolega_code/agent/tests/test_agent_tools_inventory.py +267 -0
  52. kolega_code/agent/tests/test_base_agent.py +1942 -0
  53. kolega_code/agent/tests/test_coder_attachments.py +330 -0
  54. kolega_code/agent/tests/test_coder_prompt_extensions.py +61 -0
  55. kolega_code/agent/tests/test_commands.py +179 -0
  56. kolega_code/agent/tests/test_duplicate_tool_results.py +556 -0
  57. kolega_code/agent/tests/test_empty_message_handling.py +48 -0
  58. kolega_code/agent/tests/test_general_agent.py +242 -0
  59. kolega_code/agent/tests/test_html.py +320 -0
  60. kolega_code/agent/tests/test_parallel_tool_calls.py +291 -0
  61. kolega_code/agent/tests/test_planning_agent.py +227 -0
  62. kolega_code/agent/tests/test_prompt_provider.py +271 -0
  63. kolega_code/agent/tests/test_tool_registry.py +102 -0
  64. kolega_code/agent/tests/test_tools.py +549 -0
  65. kolega_code/agent/tests/tool_backend/__init__.py +0 -0
  66. kolega_code/agent/tests/tool_backend/test_agent_tool.py +356 -0
  67. kolega_code/agent/tests/tool_backend/test_base_tool.py +147 -0
  68. kolega_code/agent/tests/tool_backend/test_browser_tool.py +335 -0
  69. kolega_code/agent/tests/tool_backend/test_build_tool.py +93 -0
  70. kolega_code/agent/tests/tool_backend/test_create_file_tool.py +115 -0
  71. kolega_code/agent/tests/tool_backend/test_glob_tool.py +196 -0
  72. kolega_code/agent/tests/tool_backend/test_glob_tool_sandbox_parity.py +230 -0
  73. kolega_code/agent/tests/tool_backend/test_list_directory_tool.py +292 -0
  74. kolega_code/agent/tests/tool_backend/test_read_file_tool.py +173 -0
  75. kolega_code/agent/tests/tool_backend/test_replace_entire_file_tool.py +115 -0
  76. kolega_code/agent/tests/tool_backend/test_replace_lines_tool.py +141 -0
  77. kolega_code/agent/tests/tool_backend/test_search_and_replace_tool.py +174 -0
  78. kolega_code/agent/tests/tool_backend/test_search_codebase_tool.py +228 -0
  79. kolega_code/agent/tests/tool_backend/test_terminal_tool.py +482 -0
  80. kolega_code/agent/tests/tool_backend/test_think_hard_integration.py +189 -0
  81. kolega_code/agent/tests/tool_backend/test_think_hard_streaming.py +445 -0
  82. kolega_code/agent/tests/tool_backend/test_web_fetch_tool.py +194 -0
  83. kolega_code/agent/tool_backend/agent_tool.py +414 -0
  84. kolega_code/agent/tool_backend/apply_edit_tool.py +98 -0
  85. kolega_code/agent/tool_backend/apply_patch_tool.py +514 -0
  86. kolega_code/agent/tool_backend/base_tool.py +217 -0
  87. kolega_code/agent/tool_backend/browser_tool.py +271 -0
  88. kolega_code/agent/tool_backend/build_tool.py +93 -0
  89. kolega_code/agent/tool_backend/create_file_tool.py +52 -0
  90. kolega_code/agent/tool_backend/glob_tool.py +323 -0
  91. kolega_code/agent/tool_backend/list_directory_tool.py +300 -0
  92. kolega_code/agent/tool_backend/memory_tool.py +79 -0
  93. kolega_code/agent/tool_backend/read_file_tool.py +119 -0
  94. kolega_code/agent/tool_backend/replace_entire_file_tool.py +40 -0
  95. kolega_code/agent/tool_backend/replace_lines_tool.py +97 -0
  96. kolega_code/agent/tool_backend/search_and_replace_tool.py +146 -0
  97. kolega_code/agent/tool_backend/search_codebase_tool.py +377 -0
  98. kolega_code/agent/tool_backend/streaming_tool.py +47 -0
  99. kolega_code/agent/tool_backend/terminal_tool.py +643 -0
  100. kolega_code/agent/tool_backend/think_hard_tool.py +211 -0
  101. kolega_code/agent/tool_backend/web_fetch_tool.py +205 -0
  102. kolega_code/agent/tools.py +1704 -0
  103. kolega_code/agent/utils/commands.py +94 -0
  104. kolega_code/cli/__init__.py +1 -0
  105. kolega_code/cli/app.py +2756 -0
  106. kolega_code/cli/config.py +280 -0
  107. kolega_code/cli/connection.py +49 -0
  108. kolega_code/cli/file_index.py +147 -0
  109. kolega_code/cli/main.py +564 -0
  110. kolega_code/cli/mentions.py +155 -0
  111. kolega_code/cli/messages.py +89 -0
  112. kolega_code/cli/provider_registry.py +96 -0
  113. kolega_code/cli/session_store.py +207 -0
  114. kolega_code/cli/settings.py +87 -0
  115. kolega_code/cli/skills.py +409 -0
  116. kolega_code/cli/slash_commands.py +108 -0
  117. kolega_code/cli/tests/__init__.py +1 -0
  118. kolega_code/cli/tests/test_app.py +4251 -0
  119. kolega_code/cli/tests/test_cli_config.py +171 -0
  120. kolega_code/cli/tests/test_connection.py +26 -0
  121. kolega_code/cli/tests/test_file_index.py +103 -0
  122. kolega_code/cli/tests/test_main.py +455 -0
  123. kolega_code/cli/tests/test_mentions.py +108 -0
  124. kolega_code/cli/tests/test_session_store.py +67 -0
  125. kolega_code/cli/tests/test_settings.py +62 -0
  126. kolega_code/cli/tests/test_skills.py +157 -0
  127. kolega_code/cli/tests/test_slash_commands.py +88 -0
  128. kolega_code/cli/theme.py +180 -0
  129. kolega_code/config.py +154 -0
  130. kolega_code/events.py +202 -0
  131. kolega_code/llm/client.py +300 -0
  132. kolega_code/llm/exceptions.py +285 -0
  133. kolega_code/llm/instrumented_client.py +520 -0
  134. kolega_code/llm/models.py +1368 -0
  135. kolega_code/llm/providers/__init__.py +0 -0
  136. kolega_code/llm/providers/anthropic.py +387 -0
  137. kolega_code/llm/providers/base.py +71 -0
  138. kolega_code/llm/providers/google.py +157 -0
  139. kolega_code/llm/providers/models.py +37 -0
  140. kolega_code/llm/providers/openai.py +363 -0
  141. kolega_code/llm/ratelimit.py +40 -0
  142. kolega_code/llm/specs.py +67 -0
  143. kolega_code/llm/tool_execution_ids.py +18 -0
  144. kolega_code/models/__init__.py +9 -0
  145. kolega_code/models/sandbox_terminal_state.py +47 -0
  146. kolega_code/runtime.py +50 -0
  147. kolega_code/sandbox/README.md +200 -0
  148. kolega_code/sandbox/__init__.py +21 -0
  149. kolega_code/sandbox/async_filesystem.py +475 -0
  150. kolega_code/sandbox/base.py +297 -0
  151. kolega_code/sandbox/browser.py +25 -0
  152. kolega_code/sandbox/event_loop.py +43 -0
  153. kolega_code/sandbox/filesystem.py +341 -0
  154. kolega_code/sandbox/local.py +118 -0
  155. kolega_code/sandbox/serializer.py +175 -0
  156. kolega_code/sandbox/terminal.py +868 -0
  157. kolega_code/sandbox/utils.py +216 -0
  158. kolega_code/services/base.py +255 -0
  159. kolega_code/services/browser.py +444 -0
  160. kolega_code/services/file_system.py +749 -0
  161. kolega_code/services/html.py +221 -0
  162. kolega_code/services/terminal.py +903 -0
  163. kolega_code/tools/__init__.py +22 -0
  164. kolega_code/tools/core.py +33 -0
  165. kolega_code/tools/definitions.py +81 -0
  166. kolega_code/tools/registry.py +73 -0
  167. kolega_code-0.1.0.dist-info/METADATA +157 -0
  168. kolega_code-0.1.0.dist-info/RECORD +171 -0
  169. kolega_code-0.1.0.dist-info/WHEEL +4 -0
  170. kolega_code-0.1.0.dist-info/entry_points.txt +2 -0
  171. kolega_code-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,200 @@
1
+ # Cloud Sandbox Implementation
2
+
3
+ This directory contains the cloud sandbox implementation for running AI agents in isolated environments using E2B.
4
+
5
+ ## Architecture Overview
6
+
7
+ The sandbox system provides a pluggable architecture that allows agents to run in cloud-based isolated environments instead of locally. The implementation uses E2B as the sandbox provider but is designed to support other providers in the future.
8
+
9
+ ### Key Components
10
+
11
+ 1. **Base Interfaces** (`base.py`)
12
+ - `SandboxConfig`: Configuration for sandbox creation (git URL, branch, environment variables)
13
+ - `ProjectManifest`: Project configuration for dependencies and setup
14
+ - `SandboxManager`: Abstract base class for sandbox management
15
+
16
+ 2. **E2B Implementation** (`sandbox_e2b.py`)
17
+ - `E2BSandboxManager`: Concrete implementation using E2B sandboxes
18
+ - Handles Git authentication using GitLab group access tokens
19
+ - Manages sandbox lifecycle (create, destroy, commit, push)
20
+ - Mounts workspace files from S3 for agent access
21
+
22
+ 3. **Sandbox-Aware Services**
23
+ - `SandboxFileSystem`: FileSystem implementation that operates within sandbox
24
+ - `SandboxTerminalManager`: Terminal manager for executing commands in sandbox
25
+ - `SandboxBrowserManager`: Browser manager placeholder (not yet implemented)
26
+
27
+ 4. **Utilities** (`utils.py`)
28
+ - Helper functions for common sandbox operations
29
+ - Git status parsing and diff generation
30
+ - Project test execution
31
+
32
+ ## How It Works
33
+
34
+ 1. **Host Job Integration**: When a host application job starts and sandbox mode is enabled:
35
+ - Creates an E2B sandbox with the workspace's Git repository
36
+ - Clones the repository using GitLab group access token
37
+ - Mounts workspace files from S3 as read-only
38
+ - Installs dependencies if a manifest file exists
39
+ - Injects sandbox-aware services into the agent
40
+
41
+ 2. **Agent Execution**: The agent runs with:
42
+ - Filesystem operations redirected to sandbox
43
+ - Terminal commands executed in sandbox
44
+ - Access to uploaded workspace files at `/home/user/workspace/kolega-project-files`
45
+ - All changes isolated from local environment
46
+
47
+ 3. **Git Workflow**: After agent completion:
48
+ - Detects modified files using `git status`
49
+ - Commits changes with descriptive message
50
+ - Pushes changes back to GitLab repository
51
+
52
+ 4. **Workspace Files**: Users can upload files through the UI which are:
53
+ - Stored in S3 at `<bucket>/<workspace_id>/<filename>`
54
+ - Automatically mounted in sandboxes at `/home/user/workspace/kolega-project-files`
55
+ - Available as read-only to prevent accidental modifications
56
+ - Excluded from git tracking via `.gitignore`
57
+
58
+ ## Configuration
59
+
60
+ ### Environment Variables
61
+
62
+ ```bash
63
+ # Enable sandbox mode
64
+ USE_SANDBOX=true
65
+
66
+ # Sandbox provider (currently only 'e2b' supported)
67
+ SANDBOX_PROVIDER=e2b
68
+
69
+ # E2B API key from https://e2b.dev
70
+ E2B_API_KEY=your_e2b_api_key
71
+
72
+ # E2B template (must use custom template with s3fs)
73
+ E2B_TEMPLATE=your_custom_template_id
74
+
75
+ # GitLab group access token with read/write permissions
76
+ GITLAB_GROUP_ACCESS_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
77
+
78
+ # S3 Configuration for workspace files
79
+ S3_BUCKET_NAME=your-kolega-files-bucket
80
+ AWS_ACCESS_KEY_ID=your-access-key
81
+ AWS_SECRET_ACCESS_KEY=your-secret-key
82
+ AWS_REGION=us-east-1 # Optional, defaults to us-east-1
83
+ ```
84
+
85
+ ### E2B Template Requirements
86
+
87
+ The E2B template must have s3fs installed. See `backend/e2b-template/` for the custom template that includes:
88
+ - Base E2B code interpreter
89
+ - s3fs for mounting S3 buckets
90
+ - Proper permissions setup
91
+
92
+ Build and deploy the template:
93
+ ```bash
94
+ cd backend/e2b-template
95
+ e2b template build
96
+ # Update E2B_TEMPLATE env var with the new template ID
97
+ ```
98
+
99
+ ### Project Manifest
100
+
101
+ Projects can include a `.kolega-manifest.yaml` file in the repository root:
102
+
103
+ ```yaml
104
+ name: my-project
105
+ runtime: node:18 # or python:3.11, etc.
106
+
107
+ # Optional: Commands to install dependencies
108
+ install_commands:
109
+ - npm install
110
+
111
+ # Optional: Commands to run before install
112
+ environment_setup:
113
+ - npm config set registry https://registry.npmjs.org/
114
+
115
+ # Optional: Development server command (runs automatically in background)
116
+ dev_server_command: npm run dev
117
+
118
+ # Optional: Test commands
119
+ test_commands:
120
+ - npm test
121
+
122
+ # Optional: Build command
123
+ build_command: npm run build
124
+ ```
125
+
126
+ #### Dev Server Auto-Start
127
+
128
+ When a `dev_server_command` is specified in the manifest, it will automatically start in the background after dependencies are installed. This is useful for:
129
+
130
+ - React/Vue/Angular development servers
131
+ - API servers (Express, FastAPI, etc.)
132
+ - Documentation servers
133
+ - Any long-running development process
134
+
135
+ Example configurations:
136
+
137
+ **React App:**
138
+ ```yaml
139
+ name: my-react-app
140
+ runtime: node:18
141
+ install_commands:
142
+ - npm install
143
+ dev_server_command: npm start -- --port 9001
144
+
145
+ ```
146
+
147
+ **Python FastAPI:**
148
+ ```yaml
149
+ name: my-api
150
+ runtime: python:3.11
151
+ install_commands:
152
+ - pip install -r requirements.txt
153
+ dev_server_command: uvicorn main:app --reload --host 0.0.0.0 --port 9001
154
+
155
+ ```
156
+
157
+ **Next.js App:**
158
+ ```yaml
159
+ name: my-nextjs-app
160
+ runtime: node:18
161
+ install_commands:
162
+ - npm install
163
+ dev_server_command: npm run dev -- -p 9001
164
+ ```
165
+
166
+ **Minimal Project (no dependencies):**
167
+ ```yaml
168
+ name: simple-static-site
169
+ runtime: node:18
170
+ # No install_commands needed for a simple static site
171
+ dev_server_command: python -m http.server 9001
172
+ ```
173
+
174
+ ## Usage
175
+
176
+ The sandbox is automatically used when:
177
+ 1. `USE_SANDBOX=true` in environment
178
+ 2. Workspace has a GitLab repository (`gitlab_project_url` is set)
179
+ 3. Agent job is started by the host application
180
+
181
+ No code changes are required in agents - they continue using the standard FileSystem and TerminalManager interfaces.
182
+
183
+ ### Accessing Workspace Files
184
+
185
+ Agents can access uploaded workspace files at:
186
+ ```python
187
+ workspace_files = "/home/user/workspace/kolega-project-files"
188
+
189
+ # List files
190
+ import os
191
+ files = os.listdir(workspace_files)
192
+
193
+ # Read a file
194
+ with open(f"{workspace_files}/document.pdf", "rb") as f:
195
+ content = f.read()
196
+ ```
197
+
198
+ ## Testing
199
+
200
+ The implementation includes comprehensive tests:
@@ -0,0 +1,21 @@
1
+ """Sandbox services for cloud-based agent execution."""
2
+
3
+ from .base import SandboxConfig, ProjectManifest, SandboxManager
4
+ from .filesystem import SandboxFileSystem
5
+ from .async_filesystem import AsyncSandboxFileSystem
6
+ from .terminal import SandboxTerminalManager
7
+ from .browser import SandboxBrowserManager
8
+ from .local import LocalSandboxManager
9
+ from .utils import get_modified_files_from_sandbox
10
+
11
+ __all__ = [
12
+ "SandboxConfig",
13
+ "ProjectManifest",
14
+ "SandboxManager",
15
+ "SandboxFileSystem",
16
+ "AsyncSandboxFileSystem",
17
+ "SandboxTerminalManager",
18
+ "SandboxBrowserManager",
19
+ "LocalSandboxManager",
20
+ "get_modified_files_from_sandbox",
21
+ ]
@@ -0,0 +1,475 @@
1
+ """Async FileSystem implementation for sandbox environments."""
2
+
3
+ import os
4
+ import base64
5
+ from pathlib import Path
6
+ from typing import Any, Dict, Iterator, List, Optional, Union, BinaryIO
7
+ from contextlib import contextmanager
8
+ from datetime import datetime
9
+ import asyncio
10
+ import nest_asyncio
11
+
12
+ from ..services.file_system import FileSystem
13
+
14
+
15
+ class AsyncSandboxFileSystem(FileSystem):
16
+ """FileSystem implementation that operates within an async sandbox.
17
+
18
+ This class provides a synchronous interface to an async sandbox by using
19
+ nest_asyncio to allow running async operations in sync contexts.
20
+ """
21
+
22
+ def __init__(self, sandbox: Any, root_path: str = "/home/user/workspace"):
23
+ """
24
+ Initialize async sandbox filesystem.
25
+
26
+ Args:
27
+ sandbox: The async sandbox instance (e.g., AsyncE2B Sandbox)
28
+ root_path: Root path within the sandbox
29
+ """
30
+ self.sandbox = sandbox
31
+ self.root_path = root_path
32
+ self._nest_asyncio_applied = False
33
+
34
+ def _run_async(self, coro):
35
+ """Run an async coroutine in a sync context using nest_asyncio."""
36
+ try:
37
+ # Try to get the current running loop
38
+ loop = asyncio.get_running_loop()
39
+
40
+ # Apply nest_asyncio only once and only when needed
41
+ if not self._nest_asyncio_applied:
42
+ try:
43
+ # Only apply if we're not in a uvloop context
44
+ if not hasattr(loop, "__module__") or "uvloop" not in loop.__module__:
45
+ nest_asyncio.apply()
46
+ self._nest_asyncio_applied = True
47
+ except:
48
+ # If patching fails, we'll try alternative approaches
49
+ pass
50
+
51
+ except RuntimeError:
52
+ # No running loop, create a new one
53
+ loop = asyncio.new_event_loop()
54
+ asyncio.set_event_loop(loop)
55
+ # Apply nest_asyncio for the new loop
56
+ if not self._nest_asyncio_applied:
57
+ nest_asyncio.apply()
58
+ self._nest_asyncio_applied = True
59
+
60
+ # Use nest_asyncio to run the coroutine if it was applied
61
+ if self._nest_asyncio_applied:
62
+ return loop.run_until_complete(coro)
63
+ else:
64
+ # If we couldn't apply nest_asyncio (e.g., uvloop),
65
+ # we need to use a different approach
66
+ import concurrent.futures
67
+ import threading
68
+
69
+ # Create a new event loop in a separate thread
70
+ def run_in_new_loop():
71
+ # Lazy import to avoid circular dependency
72
+ from kolega_code.sandbox.event_loop import cleanup_event_loop
73
+
74
+ new_loop = asyncio.new_event_loop()
75
+ asyncio.set_event_loop(new_loop)
76
+ try:
77
+ return new_loop.run_until_complete(coro)
78
+ finally:
79
+ # Proper cleanup following asyncio.Runner pattern
80
+ cleanup_event_loop(new_loop)
81
+
82
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
83
+ future = executor.submit(run_in_new_loop)
84
+ return future.result()
85
+
86
+ def _resolve_path(self, path: str) -> str:
87
+ """Resolve path relative to root."""
88
+ if os.path.isabs(path):
89
+ return path
90
+ if path == ".":
91
+ return self.root_path
92
+ return os.path.join(self.root_path, path)
93
+
94
+ # Synchronous methods wrapping async E2B API
95
+ def open(self, path: str, mode: str = "r", encoding: Optional[str] = None) -> Any:
96
+ """Open is not directly supported in sandbox - use read/write methods instead."""
97
+ raise NotImplementedError("Direct file handles not supported in sandbox. Use read_text/write_text instead.")
98
+
99
+ def read_text(self, path: str, encoding: str = "utf-8") -> str:
100
+ """Read text from file."""
101
+ full_path = self._resolve_path(path)
102
+ try:
103
+
104
+ async def _read():
105
+ return await self.sandbox.files.read(full_path)
106
+
107
+ content = self._run_async(_read())
108
+ if isinstance(content, bytes):
109
+ return content.decode(encoding)
110
+ return content
111
+ except Exception as e:
112
+ raise FileNotFoundError(f"Could not read file {path}: {e}")
113
+
114
+ def read_bytes(self, path: str) -> bytes:
115
+ """Read bytes from file."""
116
+ full_path = self._resolve_path(path)
117
+ try:
118
+ # For binary data, use base64 encoding via shell commands
119
+ # to avoid E2B files API text encoding issues
120
+ async def _read_bytes():
121
+ result = await self.sandbox.commands.run(f"base64 {full_path}")
122
+ if result.exit_code != 0:
123
+ raise FileNotFoundError(f"Could not read file {path}")
124
+ # Decode the base64 content
125
+ return base64.b64decode(result.stdout.strip())
126
+
127
+ return self._run_async(_read_bytes())
128
+
129
+ except Exception as e:
130
+ raise FileNotFoundError(f"Could not read file {path}: {e}")
131
+
132
+ def write_text(self, path: str, content: str, encoding: str = "utf-8") -> None:
133
+ """Write text to file."""
134
+ full_path = self._resolve_path(path)
135
+ try:
136
+
137
+ async def _write():
138
+ # Ensure parent directory exists
139
+ parent_dir = os.path.dirname(full_path)
140
+ if parent_dir != self.root_path:
141
+ await self.sandbox.commands.run(f"mkdir -p {parent_dir}")
142
+
143
+ await self.sandbox.files.write(full_path, content)
144
+
145
+ self._run_async(_write())
146
+ except Exception as e:
147
+ raise OSError(f"Could not write file {path}: {e}")
148
+
149
+ def write_bytes(self, path: str, content: bytes) -> None:
150
+ """Write bytes to file."""
151
+ full_path = self._resolve_path(path)
152
+ try:
153
+
154
+ async def _write_bytes():
155
+ # Ensure parent directory exists
156
+ parent_dir = os.path.dirname(full_path)
157
+ if parent_dir != self.root_path:
158
+ await self.sandbox.commands.run(f"mkdir -p {parent_dir}")
159
+
160
+ # For binary data, use base64 encoding and write via shell commands
161
+ # to avoid E2B files API text encoding issues
162
+ encoded_content = base64.b64encode(content).decode("ascii")
163
+
164
+ # Write the base64 encoded content and decode it
165
+ result = await self.sandbox.commands.run(f"echo '{encoded_content}' | base64 -d > {full_path}")
166
+
167
+ if result.exit_code != 0:
168
+ raise OSError(f"Failed to write binary file {path}: {result.stderr}")
169
+
170
+ self._run_async(_write_bytes())
171
+
172
+ except Exception as e:
173
+ raise OSError(f"Could not write file {path}: {e}")
174
+
175
+ def exists(self, path: str) -> bool:
176
+ """Check if path exists."""
177
+ full_path = self._resolve_path(path)
178
+ try:
179
+
180
+ async def _exists():
181
+ result = await self.sandbox.commands.run(f"test -e {full_path}")
182
+ return result.exit_code == 0
183
+
184
+ return self._run_async(_exists())
185
+ except:
186
+ return False
187
+
188
+ def is_file(self, path: str) -> bool:
189
+ """Check if path is a file."""
190
+ full_path = self._resolve_path(path)
191
+ try:
192
+
193
+ async def _is_file():
194
+ result = await self.sandbox.commands.run(f"test -f {full_path}")
195
+ return result.exit_code == 0
196
+
197
+ return self._run_async(_is_file())
198
+ except:
199
+ return False
200
+
201
+ def is_dir(self, path: str) -> bool:
202
+ """Check if path is a directory."""
203
+ full_path = self._resolve_path(path)
204
+ try:
205
+
206
+ async def _is_dir():
207
+ result = await self.sandbox.commands.run(f"test -d {full_path}")
208
+ return result.exit_code == 0
209
+
210
+ return self._run_async(_is_dir())
211
+ except:
212
+ return False
213
+
214
+ def stat(self, path: str) -> Dict[str, Any]:
215
+ """Get file statistics."""
216
+ full_path = self._resolve_path(path)
217
+ try:
218
+
219
+ async def _stat():
220
+ # Use stat command to get file info
221
+ result = await self.sandbox.commands.run(f"stat -c '%s %Y %Z' {full_path}")
222
+ if result.exit_code != 0:
223
+ raise FileNotFoundError(f"File not found: {path}")
224
+
225
+ size, mtime, ctime = result.stdout.strip().split()
226
+
227
+ return {
228
+ "size": int(size),
229
+ "modified_time": int(mtime),
230
+ "created_time": int(ctime),
231
+ "is_file": await self._async_is_file(full_path),
232
+ "is_directory": await self._async_is_dir(full_path),
233
+ }
234
+
235
+ return self._run_async(_stat())
236
+ except Exception as e:
237
+ raise OSError(f"Could not stat {path}: {e}")
238
+
239
+ async def _async_is_file(self, full_path: str) -> bool:
240
+ """Async helper to check if path is a file."""
241
+ try:
242
+ result = await self.sandbox.commands.run(f"test -f {full_path}")
243
+ return result.exit_code == 0
244
+ except Exception:
245
+ # E2B throws exception on non-zero exit codes
246
+ return False
247
+
248
+ async def _async_is_dir(self, full_path: str) -> bool:
249
+ """Async helper to check if path is a directory."""
250
+ try:
251
+ result = await self.sandbox.commands.run(f"test -d {full_path}")
252
+ return result.exit_code == 0
253
+ except Exception:
254
+ # E2B throws exception on non-zero exit codes
255
+ return False
256
+
257
+ def mkdir(self, path: str, parents: bool = False, exist_ok: bool = False) -> None:
258
+ """Create directory."""
259
+ full_path = self._resolve_path(path)
260
+
261
+ # Check if directory already exists
262
+ if self.exists(path):
263
+ if not exist_ok:
264
+ raise FileExistsError(f"Directory already exists: {path}")
265
+ return
266
+
267
+ try:
268
+
269
+ async def _mkdir():
270
+ if parents:
271
+ result = await self.sandbox.commands.run(f"mkdir -p {full_path}")
272
+ else:
273
+ result = await self.sandbox.commands.run(f"mkdir {full_path}")
274
+
275
+ if result.exit_code != 0:
276
+ raise OSError(f"Failed to create directory {path}: {result.stderr}")
277
+
278
+ self._run_async(_mkdir())
279
+ except Exception as e:
280
+ raise OSError(f"Could not create directory {path}: {e}")
281
+
282
+ def remove(self, path: str, missing_ok: bool = False) -> None:
283
+ """Remove file."""
284
+ full_path = self._resolve_path(path)
285
+
286
+ if not self.exists(path):
287
+ if not missing_ok:
288
+ raise FileNotFoundError(f"File not found: {path}")
289
+ return
290
+
291
+ try:
292
+
293
+ async def _remove():
294
+ result = await self.sandbox.commands.run(f"rm -f {full_path}")
295
+ if result.exit_code != 0:
296
+ raise OSError(f"Failed to remove file {path}: {result.stderr}")
297
+
298
+ self._run_async(_remove())
299
+ except Exception as e:
300
+ raise OSError(f"Could not remove file {path}: {e}")
301
+
302
+ def rmdir(self, path: str) -> None:
303
+ """Remove empty directory."""
304
+ full_path = self._resolve_path(path)
305
+ try:
306
+
307
+ async def _rmdir():
308
+ result = await self.sandbox.commands.run(f"rmdir {full_path}")
309
+ if result.exit_code != 0:
310
+ raise OSError(f"Failed to remove directory {path}: {result.stderr}")
311
+
312
+ self._run_async(_rmdir())
313
+ except Exception as e:
314
+ raise OSError(f"Could not remove directory {path}: {e}")
315
+
316
+ def rmtree(self, path: str) -> None:
317
+ """Remove directory tree."""
318
+ full_path = self._resolve_path(path)
319
+ try:
320
+
321
+ async def _rmtree():
322
+ result = await self.sandbox.commands.run(f"rm -rf {full_path}")
323
+ if result.exit_code != 0:
324
+ raise OSError(f"Failed to remove directory tree {path}: {result.stderr}")
325
+
326
+ self._run_async(_rmtree())
327
+ except Exception as e:
328
+ raise OSError(f"Could not remove directory tree {path}: {e}")
329
+
330
+ def listdir(self, path: str) -> List[str]:
331
+ """List directory contents."""
332
+ full_path = self._resolve_path(path)
333
+
334
+ if not self.is_dir(path):
335
+ raise NotADirectoryError(f"Not a directory: {path}")
336
+
337
+ try:
338
+
339
+ async def _listdir():
340
+ result = await self.sandbox.commands.run(f"ls -1 {full_path}")
341
+ if result.exit_code != 0:
342
+ raise FileNotFoundError(f"Directory not found: {path}")
343
+
344
+ if not result.stdout.strip():
345
+ return []
346
+
347
+ return [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
348
+
349
+ return self._run_async(_listdir())
350
+ except Exception as e:
351
+ raise OSError(f"Could not list directory {path}: {e}")
352
+
353
+ def iterdir(self, path: str) -> Iterator[str]:
354
+ """Iterate directory contents."""
355
+ return iter(self.listdir(path))
356
+
357
+ def glob(self, pattern: str) -> List[str]:
358
+ """Find paths matching pattern."""
359
+ try:
360
+
361
+ async def _glob():
362
+ # Handle different types of glob patterns
363
+ if pattern.startswith("**/"):
364
+ # Recursive pattern like **/*.py or **/*
365
+ remaining_pattern = pattern[3:] # Remove '**/'
366
+
367
+ if remaining_pattern == "*":
368
+ # Pattern is **/* - find all files and directories recursively
369
+ result = await self.sandbox.commands.run(
370
+ f"cd {self.root_path} && find . -mindepth 1 2>/dev/null | sed 's|^./||' | sort"
371
+ )
372
+ else:
373
+ # Pattern like **/*.py - find files matching pattern recursively
374
+ if "*" in remaining_pattern or "?" in remaining_pattern:
375
+ # Use find with -name for pattern matching
376
+ result = await self.sandbox.commands.run(
377
+ f"cd {self.root_path} && find . -name '{remaining_pattern}' 2>/dev/null | sed 's|^./||' | sort"
378
+ )
379
+ else:
380
+ # Exact filename search recursively
381
+ result = await self.sandbox.commands.run(
382
+ f"cd {self.root_path} && find . -name '{remaining_pattern}' 2>/dev/null | sed 's|^./||' | sort"
383
+ )
384
+
385
+ elif "*" in pattern or "?" in pattern:
386
+ # Simple glob pattern like *.py or test*.txt
387
+ if "/" in pattern:
388
+ # Pattern has directory component like subdir/*.txt
389
+ parent_dir = os.path.dirname(pattern)
390
+ filename_pattern = os.path.basename(pattern)
391
+ result = await self.sandbox.commands.run(
392
+ f"cd {self.root_path} && find {parent_dir} -maxdepth 1 -name '{filename_pattern}' 2>/dev/null | sort"
393
+ )
394
+ else:
395
+ # Simple pattern like *.py in current directory
396
+ result = await self.sandbox.commands.run(
397
+ f"cd {self.root_path} && find . -maxdepth 1 -name '{pattern}' 2>/dev/null | sed 's|^./||' | sort"
398
+ )
399
+
400
+ else:
401
+ # No wildcards - check if exact path exists
402
+ if self.exists(pattern):
403
+ return [pattern]
404
+ else:
405
+ return []
406
+
407
+ # Process the result
408
+ if result.exit_code == 0 and result.stdout.strip():
409
+ paths = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
410
+ # Filter out empty strings and current directory
411
+ return [path for path in paths if path and path != "."]
412
+ else:
413
+ return []
414
+
415
+ return self._run_async(_glob())
416
+
417
+ except Exception as e:
418
+ # If any error occurs, return empty list to match expected behavior
419
+ return []
420
+
421
+ def is_binary_file(self, path: str) -> bool:
422
+ """Check if file is binary."""
423
+ full_path = self._resolve_path(path)
424
+ try:
425
+
426
+ async def _is_binary():
427
+ # Use file command to detect binary
428
+ result = await self.sandbox.commands.run(f"file -b --mime {full_path}")
429
+ return result.exit_code == 0 and "charset=binary" in result.stdout
430
+
431
+ return self._run_async(_is_binary())
432
+ except:
433
+ return False
434
+
435
+ def get_name(self, path: str) -> str:
436
+ """Get basename of path."""
437
+ return os.path.basename(path)
438
+
439
+ def get_suffix(self, path: str) -> str:
440
+ """Get file extension."""
441
+ return os.path.splitext(path)[1]
442
+
443
+ def get_parent(self, path: str) -> str:
444
+ """Get parent directory."""
445
+ parent = os.path.dirname(path)
446
+ # Return "." for root-level files to match LocalFileSystem behavior
447
+ return parent if parent else "."
448
+
449
+ def get_parents(self, path: str) -> List[str]:
450
+ """Get all parent directories."""
451
+ parents = []
452
+ current = os.path.dirname(path)
453
+ while current and current != "/":
454
+ parents.append(current)
455
+ current = os.path.dirname(current)
456
+ return parents
457
+
458
+ def relative_to(self, path: str, other: str) -> str:
459
+ """Get relative path."""
460
+ return os.path.relpath(path, other)
461
+
462
+ def join_path(self, *parts: str) -> str:
463
+ """Join path components."""
464
+ return os.path.join(*parts)
465
+
466
+ def is_absolute(self, path: str) -> bool:
467
+ """Check if path is absolute."""
468
+ return os.path.isabs(path)
469
+
470
+ def get_path(self, path: str) -> Path:
471
+ """Get a Path object for the given path."""
472
+ resolved = self._resolve_path(path)
473
+ # Since _resolve_path returns a string for sandbox filesystem,
474
+ # we need to create a Path object from it
475
+ return Path(resolved)