hypha-debugger 0.1.8__tar.gz → 0.1.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/PKG-INFO +1 -1
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/__init__.py +1 -1
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/services/filesystem.py +22 -34
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/services/source.py +65 -79
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger.egg-info/PKG-INFO +1 -1
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/pyproject.toml +1 -1
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/tests/test_services.py +28 -8
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/README.md +0 -0
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/__main__.py +0 -0
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/debugger.py +0 -0
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/services/__init__.py +0 -0
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/services/execute.py +0 -0
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/services/info.py +0 -0
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/services/inspect_vars.py +0 -0
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/utils/__init__.py +0 -0
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger/utils/env.py +0 -0
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger.egg-info/SOURCES.txt +0 -0
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger.egg-info/dependency_links.txt +0 -0
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger.egg-info/entry_points.txt +0 -0
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger.egg-info/requires.txt +0 -0
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/hypha_debugger.egg-info/top_level.txt +0 -0
- {hypha_debugger-0.1.8 → hypha_debugger-0.1.10}/setup.cfg +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""File system browsing and writing service
|
|
1
|
+
"""File system browsing and writing service."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
|
|
@@ -10,19 +10,19 @@ try:
|
|
|
10
10
|
def list_files(
|
|
11
11
|
path: str = Field(
|
|
12
12
|
default=".",
|
|
13
|
-
description='Directory path relative to CWD. Default: "."
|
|
13
|
+
description='Directory path (absolute or relative to CWD). Default: ".".',
|
|
14
14
|
),
|
|
15
15
|
pattern: str = Field(
|
|
16
16
|
default="",
|
|
17
17
|
description='Glob pattern to filter files, e.g. "*.py".',
|
|
18
18
|
),
|
|
19
19
|
) -> dict:
|
|
20
|
-
"""List files and directories
|
|
20
|
+
"""List files and directories at the given path."""
|
|
21
21
|
return _list_files_impl(path, pattern)
|
|
22
22
|
|
|
23
23
|
@schema_function
|
|
24
24
|
def read_file(
|
|
25
|
-
path: str = Field(..., description="File path relative to CWD."),
|
|
25
|
+
path: str = Field(..., description="File path (absolute or relative to CWD)."),
|
|
26
26
|
max_lines: int = Field(
|
|
27
27
|
default=500,
|
|
28
28
|
description="Maximum number of lines to read. Default: 500.",
|
|
@@ -36,7 +36,7 @@ try:
|
|
|
36
36
|
description="File encoding. Default: utf-8.",
|
|
37
37
|
),
|
|
38
38
|
) -> dict:
|
|
39
|
-
"""Read a file
|
|
39
|
+
"""Read a file. Returns the content as a string.
|
|
40
40
|
|
|
41
41
|
Use offset and max_lines to paginate through large files.
|
|
42
42
|
"""
|
|
@@ -44,7 +44,7 @@ try:
|
|
|
44
44
|
|
|
45
45
|
@schema_function
|
|
46
46
|
def write_file(
|
|
47
|
-
path: str = Field(..., description="File path relative to CWD."),
|
|
47
|
+
path: str = Field(..., description="File path (absolute or relative to CWD)."),
|
|
48
48
|
content: str = Field(..., description="Content to write to the file."),
|
|
49
49
|
mode: str = Field(
|
|
50
50
|
default="w",
|
|
@@ -59,47 +59,39 @@ try:
|
|
|
59
59
|
description="File encoding. Default: utf-8.",
|
|
60
60
|
),
|
|
61
61
|
) -> dict:
|
|
62
|
-
"""Write content to a file
|
|
62
|
+
"""Write content to a file.
|
|
63
63
|
|
|
64
64
|
Creates parent directories automatically. Use mode="a" to append.
|
|
65
|
-
Sandboxed: cannot write outside the process CWD.
|
|
66
65
|
"""
|
|
67
66
|
return _write_file_impl(path, content, mode, create_dirs, encoding)
|
|
68
67
|
|
|
69
68
|
except ImportError:
|
|
70
69
|
|
|
71
70
|
def list_files(path: str = ".", pattern: str = "") -> dict:
|
|
72
|
-
"""List files and directories
|
|
71
|
+
"""List files and directories at the given path."""
|
|
73
72
|
return _list_files_impl(path, pattern)
|
|
74
73
|
|
|
75
74
|
def read_file(
|
|
76
75
|
path: str = "", max_lines: int = 500, offset: int = 0, encoding: str = "utf-8"
|
|
77
76
|
) -> dict:
|
|
78
|
-
"""Read a file
|
|
77
|
+
"""Read a file."""
|
|
79
78
|
return _read_file_impl(path, max_lines, offset, encoding)
|
|
80
79
|
|
|
81
80
|
def write_file(
|
|
82
81
|
path: str = "", content: str = "", mode: str = "w",
|
|
83
82
|
create_dirs: bool = True, encoding: str = "utf-8"
|
|
84
83
|
) -> dict:
|
|
85
|
-
"""Write content to a file
|
|
84
|
+
"""Write content to a file."""
|
|
86
85
|
return _write_file_impl(path, content, mode, create_dirs, encoding)
|
|
87
86
|
|
|
88
87
|
|
|
89
|
-
def
|
|
90
|
-
"""Resolve path
|
|
91
|
-
|
|
92
|
-
resolved = os.path.realpath(os.path.join(cwd, path))
|
|
93
|
-
if not resolved.startswith(cwd):
|
|
94
|
-
raise PermissionError(f"Access denied: path escapes CWD ({path})")
|
|
95
|
-
return resolved
|
|
88
|
+
def _resolve(path: str) -> str:
|
|
89
|
+
"""Resolve path to absolute."""
|
|
90
|
+
return os.path.realpath(os.path.expanduser(path))
|
|
96
91
|
|
|
97
92
|
|
|
98
93
|
def _list_files_impl(path: str, pattern: str) -> dict:
|
|
99
|
-
|
|
100
|
-
resolved = _resolve_safe(path)
|
|
101
|
-
except PermissionError as e:
|
|
102
|
-
return {"error": str(e)}
|
|
94
|
+
resolved = _resolve(path)
|
|
103
95
|
|
|
104
96
|
if not os.path.isdir(resolved):
|
|
105
97
|
return {"error": f"Not a directory: {path}"}
|
|
@@ -123,7 +115,7 @@ def _list_files_impl(path: str, pattern: str) -> dict:
|
|
|
123
115
|
return {"error": f"Permission denied: {path}"}
|
|
124
116
|
|
|
125
117
|
return {
|
|
126
|
-
"path":
|
|
118
|
+
"path": resolved,
|
|
127
119
|
"entries": entries[:500],
|
|
128
120
|
"total": len(entries),
|
|
129
121
|
}
|
|
@@ -144,10 +136,7 @@ def _entry_info(name: str, full_path: str) -> dict:
|
|
|
144
136
|
|
|
145
137
|
|
|
146
138
|
def _read_file_impl(path: str, max_lines: int, offset: int, encoding: str) -> dict:
|
|
147
|
-
|
|
148
|
-
resolved = _resolve_safe(path)
|
|
149
|
-
except PermissionError as e:
|
|
150
|
-
return {"error": str(e)}
|
|
139
|
+
resolved = _resolve(path)
|
|
151
140
|
|
|
152
141
|
if not os.path.isfile(resolved):
|
|
153
142
|
return {"error": f"Not a file: {path}"}
|
|
@@ -163,7 +152,7 @@ def _read_file_impl(path: str, max_lines: int, offset: int, encoding: str) -> di
|
|
|
163
152
|
lines.append(line)
|
|
164
153
|
content = "".join(lines)
|
|
165
154
|
return {
|
|
166
|
-
"path":
|
|
155
|
+
"path": resolved,
|
|
167
156
|
"content": content,
|
|
168
157
|
"lines_read": len(lines),
|
|
169
158
|
"offset": offset,
|
|
@@ -179,20 +168,19 @@ def _write_file_impl(
|
|
|
179
168
|
if mode not in ("w", "a"):
|
|
180
169
|
return {"error": f'Invalid mode: {mode}. Use "w" or "a".'}
|
|
181
170
|
|
|
182
|
-
|
|
183
|
-
resolved = _resolve_safe(path)
|
|
184
|
-
except PermissionError as e:
|
|
185
|
-
return {"error": str(e)}
|
|
171
|
+
resolved = _resolve(path)
|
|
186
172
|
|
|
187
173
|
try:
|
|
188
174
|
if create_dirs:
|
|
189
|
-
os.
|
|
175
|
+
parent = os.path.dirname(resolved)
|
|
176
|
+
if parent:
|
|
177
|
+
os.makedirs(parent, exist_ok=True)
|
|
190
178
|
|
|
191
179
|
with open(resolved, mode, encoding=encoding) as f:
|
|
192
180
|
f.write(content)
|
|
193
181
|
|
|
194
182
|
return {
|
|
195
|
-
"path":
|
|
183
|
+
"path": resolved,
|
|
196
184
|
"bytes_written": len(content.encode(encoding)),
|
|
197
185
|
"mode": mode,
|
|
198
186
|
}
|
|
@@ -87,104 +87,90 @@ def _get_skill_md_impl() -> str:
|
|
|
87
87
|
return """# Hypha Remote Debugger — Python Process API
|
|
88
88
|
|
|
89
89
|
## Overview
|
|
90
|
-
A debugger is attached to a running Python process. You
|
|
91
|
-
read/write files, inspect variables, and
|
|
90
|
+
A remote debugger is attached to a running Python process. You have full access
|
|
91
|
+
to execute code, read/write files, inspect variables, and install packages.
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
**Recommended approach**: Use `execute_code` as your primary tool — it's a
|
|
94
|
+
persistent Python REPL where variables, imports, and functions survive across
|
|
95
|
+
calls. The other endpoints are convenience shortcuts.
|
|
94
96
|
|
|
95
|
-
|
|
96
|
-
Get information about the current Python process.
|
|
97
|
-
- **Returns**: `{pid, cwd, python_version, hostname, platform, memory_mb, cpu_count, ...}`
|
|
98
|
-
- **Example**: `curl "$SERVICE_URL/get_process_info"`
|
|
97
|
+
## Quick Start
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
across calls in the default REPL namespace.
|
|
104
|
-
- **code** (string, required): Python code to execute.
|
|
105
|
-
- **namespace** (string, default `""`): Namespace. Empty = persistent REPL. `"__main__"` = main module.
|
|
106
|
-
- **timeout** (int, default `30`): Timeout in seconds. 0 for no timeout.
|
|
107
|
-
- **Returns**: `{ok, stdout, stderr, result, result_repr, result_type, error?, error_type?, error_message?, traceback?, timed_out?}`
|
|
108
|
-
- The last expression in the code block is automatically captured as `result`.
|
|
109
|
-
E.g. `"x = 1\\nx + 1"` returns `result=2`.
|
|
110
|
-
- **Example**:
|
|
111
|
-
```bash
|
|
112
|
-
curl -X POST "$SERVICE_URL/execute_code" \\
|
|
113
|
-
-H "Content-Type: application/json" \\
|
|
114
|
-
-d '{"code": "import sys; sys.version"}'
|
|
115
|
-
```
|
|
99
|
+
```bash
|
|
100
|
+
# Set the service URL (provided when debugger starts)
|
|
101
|
+
SERVICE_URL="<your-service-url>"
|
|
116
102
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
-
|
|
120
|
-
-
|
|
121
|
-
- **Returns**: `{name, type, repr, length?, shape?, dtype?, keys?}`
|
|
103
|
+
# Run Python code (persistent REPL — variables survive across calls):
|
|
104
|
+
curl -X POST "$SERVICE_URL/execute_code" \\
|
|
105
|
+
-H "Content-Type: application/json" \\
|
|
106
|
+
-d '{"code": "import os; os.listdir(\\".\\")"}'
|
|
122
107
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
- **include_private** (bool, default `false`): Include `_`-prefixed names.
|
|
128
|
-
- **Returns**: List of `{name, type, repr}`.
|
|
108
|
+
# Install a package:
|
|
109
|
+
curl -X POST "$SERVICE_URL/execute_code" \\
|
|
110
|
+
-H "Content-Type: application/json" \\
|
|
111
|
+
-d '{"code": "import subprocess; subprocess.check_output([\\\"pip\\\", \\\"install\\\", \\\"requests\\\"])"}'
|
|
129
112
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
-
|
|
133
|
-
-
|
|
113
|
+
# Write a file via execute_code (avoids JSON escaping issues):
|
|
114
|
+
curl -X POST "$SERVICE_URL/execute_code" \\
|
|
115
|
+
-H "Content-Type: application/json" \\
|
|
116
|
+
-d '{"code": "with open(\\\"hello.py\\\", \\\"w\\\") as f: f.write(\\\"print(42)\\\\n\\\")"}'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Functions
|
|
120
|
+
|
|
121
|
+
### execute_code(code, namespace?, timeout?) — PRIMARY
|
|
122
|
+
Execute Python code in a persistent REPL. The last expression is automatically
|
|
123
|
+
captured as the return value via AST parsing.
|
|
124
|
+
- **code** (str, required): Python code to execute.
|
|
125
|
+
- **namespace** (str, default `""`): Empty = persistent REPL, `"__main__"` = main module.
|
|
126
|
+
- **timeout** (int, default `30`): Seconds. 0 = no timeout.
|
|
127
|
+
- **Returns**: `{ok, stdout, stderr, result, result_repr, result_type, error?, error_type?, error_message?, traceback?, timed_out?}`
|
|
128
|
+
- Variables, functions, and imports persist across calls.
|
|
129
|
+
- Example: `"x = 1\\nx + 1"` returns `{ok: true, result: 2}`.
|
|
130
|
+
|
|
131
|
+
### get_process_info()
|
|
132
|
+
PID, CWD, Python version, hostname, platform, memory usage, CPU count.
|
|
133
|
+
- **Example**: `curl "$SERVICE_URL/get_process_info"`
|
|
134
134
|
|
|
135
135
|
### list_files(path?, pattern?)
|
|
136
|
-
List
|
|
137
|
-
- **path** (
|
|
138
|
-
- **pattern** (string): Glob filter, e.g. `"*.py"`.
|
|
136
|
+
List directory contents. Accepts absolute or relative paths.
|
|
137
|
+
- **path** (str, default `"."`), **pattern** (str): glob filter e.g. `"*.py"`.
|
|
139
138
|
- **Returns**: `{path, entries: [{name, type, size?}], total}`
|
|
140
|
-
- **Example**: `curl "$SERVICE_URL/list_files"`
|
|
141
139
|
|
|
142
140
|
### read_file(path, max_lines?, offset?, encoding?)
|
|
143
|
-
Read a file
|
|
144
|
-
- **path** (
|
|
145
|
-
- **max_lines** (int, default `500`): Max lines to read.
|
|
146
|
-
- **offset** (int, default `0`): Lines to skip from beginning.
|
|
141
|
+
Read a file. Accepts absolute or relative paths.
|
|
142
|
+
- **path** (str, required), **max_lines** (int, default `500`), **offset** (int, default `0`).
|
|
147
143
|
- **Returns**: `{path, content, lines_read, offset, truncated}`
|
|
148
|
-
- **Example**:
|
|
149
|
-
```bash
|
|
150
|
-
curl -X POST "$SERVICE_URL/read_file" \\
|
|
151
|
-
-H "Content-Type: application/json" \\
|
|
152
|
-
-d '{"path": "main.py"}'
|
|
153
|
-
```
|
|
154
144
|
|
|
155
145
|
### write_file(path, content, mode?, create_dirs?, encoding?)
|
|
156
|
-
Write
|
|
157
|
-
- **path** (
|
|
158
|
-
- **content** (string, required): Content to write.
|
|
159
|
-
- **mode** (string, default `"w"`): `"w"` to overwrite, `"a"` to append.
|
|
160
|
-
- **create_dirs** (bool, default `true`): Create parent directories.
|
|
146
|
+
Write/append to a file. Auto-creates parent dirs. Accepts absolute or relative paths.
|
|
147
|
+
- **path** (str), **content** (str), **mode** (`"w"` or `"a"`).
|
|
161
148
|
- **Returns**: `{path, bytes_written, mode}`
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
149
|
+
|
|
150
|
+
### get_variable(name, namespace?)
|
|
151
|
+
Inspect a variable by name in a module namespace.
|
|
152
|
+
- **Returns**: `{name, type, repr, length?, shape?, dtype?, keys?}`
|
|
153
|
+
|
|
154
|
+
### list_variables(namespace?, filter?, include_private?)
|
|
155
|
+
List variables in a namespace. Filters by substring.
|
|
156
|
+
|
|
157
|
+
### get_stack_trace()
|
|
158
|
+
Stack traces of all threads. Useful for debugging hangs.
|
|
168
159
|
|
|
169
160
|
### get_installed_packages(filter?)
|
|
170
|
-
List
|
|
171
|
-
- **filter** (string): Substring filter for package names.
|
|
172
|
-
- **Returns**: List of `{name, version}`.
|
|
173
|
-
- **Example**: `curl "$SERVICE_URL/get_installed_packages"`
|
|
161
|
+
List pip packages. Optional name substring filter.
|
|
174
162
|
|
|
175
163
|
### get_source(module?)
|
|
176
|
-
Get
|
|
177
|
-
- **module** (string): Module path, e.g. `"services.execute"`. Empty to list modules.
|
|
178
|
-
- **Returns**: `{module, source, lines}` or `{modules, hint}`.
|
|
164
|
+
Get debugger source code. Empty = list modules, e.g. `"services.execute"`.
|
|
179
165
|
|
|
180
166
|
### get_skill_md()
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
-
|
|
187
|
-
- POST endpoints accept JSON body
|
|
188
|
-
-
|
|
189
|
-
-
|
|
167
|
+
Returns this document.
|
|
168
|
+
|
|
169
|
+
## Tips
|
|
170
|
+
- **Use `execute_code` for file writes** when content has special characters —
|
|
171
|
+
it avoids JSON/curl escaping issues that affect `write_file` via HTTP.
|
|
172
|
+
- File operations accept absolute paths or paths relative to the process CWD.
|
|
173
|
+
- POST endpoints accept JSON body. GET endpoints take no body.
|
|
174
|
+
- Code execution has a 30s default timeout to prevent hangs.
|
|
175
|
+
- The REPL namespace is independent from `__main__` by default.
|
|
190
176
|
"""
|
|
@@ -185,10 +185,13 @@ def test_read_file_with_offset():
|
|
|
185
185
|
assert result["lines_read"] <= 2
|
|
186
186
|
|
|
187
187
|
|
|
188
|
-
def
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
def test_read_file_absolute():
|
|
189
|
+
"""Can read files by absolute path."""
|
|
190
|
+
import pathlib
|
|
191
|
+
toml_path = str(pathlib.Path("pyproject.toml").resolve())
|
|
192
|
+
result = read_file(toml_path)
|
|
193
|
+
assert result.get("error") is None
|
|
194
|
+
assert "hypha-debugger" in result["content"]
|
|
192
195
|
|
|
193
196
|
|
|
194
197
|
def test_write_file():
|
|
@@ -234,10 +237,27 @@ def test_write_file_create_dirs():
|
|
|
234
237
|
os.chdir(old_cwd)
|
|
235
238
|
|
|
236
239
|
|
|
237
|
-
def
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
240
|
+
def test_write_file_absolute():
|
|
241
|
+
"""Can write files by absolute path."""
|
|
242
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
243
|
+
abs_path = os.path.join(tmpdir, "abs_test.txt")
|
|
244
|
+
result = write_file(abs_path, "absolute write")
|
|
245
|
+
assert result.get("error") is None
|
|
246
|
+
assert result["bytes_written"] == 14
|
|
247
|
+
read_result = read_file(abs_path)
|
|
248
|
+
assert read_result["content"] == "absolute write"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_list_files_absolute():
|
|
252
|
+
"""Can list files by absolute path."""
|
|
253
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
254
|
+
# Create a test file
|
|
255
|
+
with open(os.path.join(tmpdir, "hello.txt"), "w") as f:
|
|
256
|
+
f.write("hi")
|
|
257
|
+
result = list_files(tmpdir)
|
|
258
|
+
assert result.get("error") is None
|
|
259
|
+
names = [e["name"] for e in result["entries"]]
|
|
260
|
+
assert "hello.txt" in names
|
|
241
261
|
|
|
242
262
|
|
|
243
263
|
def test_write_file_invalid_mode():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|