hypha-debugger 0.2.0__tar.gz → 0.2.2__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.2.0 → hypha_debugger-0.2.2}/PKG-INFO +2 -1
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/__init__.py +1 -1
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/cli.py +190 -34
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/services/source.py +23 -9
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger.egg-info/PKG-INFO +2 -1
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger.egg-info/requires.txt +1 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/pyproject.toml +2 -1
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/tests/test_cli.py +53 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/README.md +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/__main__.py +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/debugger.py +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/services/__init__.py +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/services/execute.py +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/services/filesystem.py +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/services/info.py +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/services/inspect_vars.py +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/services/shell.py +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/utils/__init__.py +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/utils/env.py +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger.egg-info/SOURCES.txt +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger.egg-info/dependency_links.txt +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger.egg-info/entry_points.txt +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger.egg-info/top_level.txt +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/setup.cfg +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/tests/test_services.py +0 -0
- {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/tests/test_shell.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hypha-debugger
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Injectable debugger for Python processes and AI agents, powered by Hypha RPC
|
|
5
5
|
Author: Amun AI AB
|
|
6
6
|
License: MIT
|
|
@@ -20,6 +20,7 @@ Requires-Python: >=3.9
|
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
Requires-Dist: hypha-rpc>=0.20.0
|
|
22
22
|
Requires-Dist: pydantic>=2.0
|
|
23
|
+
Requires-Dist: certifi
|
|
23
24
|
Provides-Extra: dev
|
|
24
25
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
26
|
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
@@ -22,6 +22,7 @@ after every ``hyd sh``. See any debugger's ``GET <url>/get_skill_md``.
|
|
|
22
22
|
import json
|
|
23
23
|
import os
|
|
24
24
|
import shlex
|
|
25
|
+
import ssl
|
|
25
26
|
import sys
|
|
26
27
|
import urllib.error
|
|
27
28
|
import urllib.request
|
|
@@ -29,6 +30,24 @@ import urllib.request
|
|
|
29
30
|
__all__ = ["main"]
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
def _ssl_context() -> ssl.SSLContext:
|
|
34
|
+
"""A TLS context that verifies certs using certifi's CA bundle when available.
|
|
35
|
+
|
|
36
|
+
Python's default context uses the system/OpenSSL trust store, which is often
|
|
37
|
+
missing or unconfigured (macOS framework Python, minimal containers, fresh
|
|
38
|
+
venvs) — causing CERTIFICATE_VERIFY_FAILED on HTTPS service URLs even though
|
|
39
|
+
`curl` works. certifi (a transitive dep of hypha-rpc) fixes that portably.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
import certifi
|
|
43
|
+
return ssl.create_default_context(cafile=certifi.where())
|
|
44
|
+
except Exception:
|
|
45
|
+
return ssl.create_default_context()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_SSL_CTX = _ssl_context()
|
|
49
|
+
|
|
50
|
+
|
|
32
51
|
class CliError(Exception):
|
|
33
52
|
pass
|
|
34
53
|
|
|
@@ -114,6 +133,29 @@ def _normalize_url(url: str) -> str:
|
|
|
114
133
|
return url.rstrip("/")
|
|
115
134
|
|
|
116
135
|
|
|
136
|
+
# ---- profile type (terminal vs browser) ----------------------------------
|
|
137
|
+
def _profile_type(profile: dict) -> str:
|
|
138
|
+
return profile.get("type", "terminal")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _infer_type(url: str) -> str:
|
|
142
|
+
"""Guess the profile type from the service id in the URL."""
|
|
143
|
+
u = url.lower()
|
|
144
|
+
if "web-navigator" in u or "web-debugger" in u or "navigator" in u:
|
|
145
|
+
return "browser"
|
|
146
|
+
return "terminal"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _print_value(value) -> None:
|
|
150
|
+
"""Print a service result: strings verbatim, everything else as JSON."""
|
|
151
|
+
if value is None:
|
|
152
|
+
return
|
|
153
|
+
if isinstance(value, str):
|
|
154
|
+
sys.stdout.write(value if value.endswith("\n") else value + "\n")
|
|
155
|
+
else:
|
|
156
|
+
print(json.dumps(value, indent=2, default=str))
|
|
157
|
+
|
|
158
|
+
|
|
117
159
|
def _resolve_profile(explicit: str = "") -> tuple:
|
|
118
160
|
"""Return (name, profile_dict). Priority: -p flag > $HYD_PROFILE > sole profile."""
|
|
119
161
|
profiles = _load_profiles()
|
|
@@ -141,7 +183,7 @@ def _call_remote(profile: dict, fn: str, params: dict, http_timeout: int = 75) -
|
|
|
141
183
|
if profile.get("token"):
|
|
142
184
|
req.add_header("Authorization", "Bearer " + profile["token"])
|
|
143
185
|
try:
|
|
144
|
-
with urllib.request.urlopen(req, timeout=http_timeout) as resp:
|
|
186
|
+
with urllib.request.urlopen(req, timeout=http_timeout, context=_SSL_CTX) as resp:
|
|
145
187
|
raw = resp.read().decode("utf-8", "replace")
|
|
146
188
|
except urllib.error.HTTPError as e:
|
|
147
189
|
body = e.read().decode("utf-8", "replace") if e.fp else ""
|
|
@@ -175,17 +217,7 @@ def _extract_opts(args: list) -> tuple:
|
|
|
175
217
|
return opts, rest
|
|
176
218
|
|
|
177
219
|
|
|
178
|
-
def
|
|
179
|
-
if bare:
|
|
180
|
-
opts, command_parts = {"profile": "", "timeout": None, "cwd": ""}, args
|
|
181
|
-
else:
|
|
182
|
-
opts, command_parts = _extract_opts(args)
|
|
183
|
-
command = " ".join(command_parts).strip()
|
|
184
|
-
if not command:
|
|
185
|
-
raise CliError("nothing to run. Usage: hyd sh '<command>'")
|
|
186
|
-
_name, profile = _resolve_profile(opts["profile"])
|
|
187
|
-
cwd = opts["cwd"] or _env_cwd()
|
|
188
|
-
timeout = opts["timeout"] if opts["timeout"] is not None else 30
|
|
220
|
+
def _run_shell(profile: dict, command: str, cwd: str, timeout: int) -> int:
|
|
189
221
|
data = _call_remote(
|
|
190
222
|
profile, "execute_bash",
|
|
191
223
|
{"command": command, "cwd": cwd, "timeout": timeout},
|
|
@@ -200,12 +232,113 @@ def cmd_sh(args: list, bare: bool = False) -> int:
|
|
|
200
232
|
return int(data.get("exit_code", 0) or 0)
|
|
201
233
|
|
|
202
234
|
|
|
235
|
+
def _run_js(profile: dict, code: str, timeout: int) -> int:
|
|
236
|
+
data = _call_remote(profile, "execute_script", {"code": code}, http_timeout=(timeout or 60) + 15)
|
|
237
|
+
if isinstance(data, dict):
|
|
238
|
+
if data.get("error"):
|
|
239
|
+
sys.stderr.write(f"hyd: {data['error']}\n")
|
|
240
|
+
return 1
|
|
241
|
+
# execute_script returns {result, type, ...}; print just the result.
|
|
242
|
+
_print_value(data["result"] if "result" in data else data)
|
|
243
|
+
else:
|
|
244
|
+
_print_value(data)
|
|
245
|
+
return 0
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def cmd_run(args: list, bare: bool = False) -> int:
|
|
249
|
+
"""Type-adaptive run: shell on a terminal profile, JavaScript on a browser one."""
|
|
250
|
+
if bare:
|
|
251
|
+
opts, parts = {"profile": "", "timeout": None, "cwd": ""}, args
|
|
252
|
+
else:
|
|
253
|
+
opts, parts = _extract_opts(args)
|
|
254
|
+
payload = " ".join(parts).strip()
|
|
255
|
+
if not payload:
|
|
256
|
+
raise CliError("nothing to run. Usage: hyd '<shell command or JS>'")
|
|
257
|
+
name, profile = _resolve_profile(opts["profile"])
|
|
258
|
+
timeout = opts["timeout"] if opts["timeout"] is not None else 30
|
|
259
|
+
if _profile_type(profile) == "browser":
|
|
260
|
+
return _run_js(profile, payload, timeout)
|
|
261
|
+
return _run_shell(profile, payload, opts["cwd"] or _env_cwd(), timeout)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def cmd_sh(args: list) -> int:
|
|
265
|
+
opts, parts = _extract_opts(args)
|
|
266
|
+
command = " ".join(parts).strip()
|
|
267
|
+
if not command:
|
|
268
|
+
raise CliError("nothing to run. Usage: hyd sh '<command>'")
|
|
269
|
+
name, profile = _resolve_profile(opts["profile"])
|
|
270
|
+
if _profile_type(profile) == "browser":
|
|
271
|
+
raise CliError(f"profile '{name}' is a browser profile — use `hyd js '<code>'` (or bare `hyd '<code>'`).")
|
|
272
|
+
timeout = opts["timeout"] if opts["timeout"] is not None else 30
|
|
273
|
+
return _run_shell(profile, command, opts["cwd"] or _env_cwd(), timeout)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def cmd_js(args: list) -> int:
|
|
277
|
+
opts, parts = _extract_opts(args)
|
|
278
|
+
code = " ".join(parts).strip()
|
|
279
|
+
if not code:
|
|
280
|
+
raise CliError("nothing to run. Usage: hyd js '<javascript>'")
|
|
281
|
+
name, profile = _resolve_profile(opts["profile"])
|
|
282
|
+
if _profile_type(profile) != "browser":
|
|
283
|
+
raise CliError(f"profile '{name}' is a terminal profile — use `hyd sh '<command>'`.")
|
|
284
|
+
timeout = opts["timeout"] if opts["timeout"] is not None else 30
|
|
285
|
+
return _run_js(profile, code, timeout)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def cmd_call(args: list) -> int:
|
|
289
|
+
"""Generic passthrough: call any service function. `hyd call <fn> [--json '{...}'] [k=v ...]`."""
|
|
290
|
+
opts, rest = _extract_opts(args)
|
|
291
|
+
if not rest:
|
|
292
|
+
raise CliError("usage: hyd call <function> [--json '{...}'] [key=value ...]")
|
|
293
|
+
fn, params = rest[0], {}
|
|
294
|
+
i = 1
|
|
295
|
+
while i < len(rest):
|
|
296
|
+
a = rest[i]
|
|
297
|
+
if a == "--json" and i + 1 < len(rest):
|
|
298
|
+
params.update(json.loads(rest[i + 1])); i += 2; continue
|
|
299
|
+
if "=" in a:
|
|
300
|
+
k, v = a.split("=", 1)
|
|
301
|
+
params[k] = v; i += 1; continue
|
|
302
|
+
i += 1
|
|
303
|
+
_name, profile = _resolve_profile(opts["profile"])
|
|
304
|
+
_print_value(_call_remote(profile, fn, params))
|
|
305
|
+
return 0
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def cmd_nav(args: list) -> int:
|
|
309
|
+
if not args:
|
|
310
|
+
raise CliError("usage: hyd nav <url>")
|
|
311
|
+
name, profile = _resolve_profile()
|
|
312
|
+
if _profile_type(profile) != "browser":
|
|
313
|
+
raise CliError(f"profile '{name}' is not a browser profile.")
|
|
314
|
+
_print_value(_call_remote(profile, "navigate", {"url": args[0]}))
|
|
315
|
+
return 0
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def cmd_shot(args: list) -> int:
|
|
319
|
+
import base64
|
|
320
|
+
name, profile = _resolve_profile()
|
|
321
|
+
if _profile_type(profile) != "browser":
|
|
322
|
+
raise CliError(f"profile '{name}' is not a browser profile.")
|
|
323
|
+
data = _call_remote(profile, "take_screenshot", {})
|
|
324
|
+
b64 = data.get("base64") or (data.get("data_url", "").split(",", 1)[-1] if data.get("data_url") else "")
|
|
325
|
+
if not b64:
|
|
326
|
+
raise CliError(f"no screenshot data: {data.get('error', data)}")
|
|
327
|
+
path = args[0] if args else "screenshot.png"
|
|
328
|
+
with open(path, "wb") as f:
|
|
329
|
+
f.write(base64.b64decode(b64))
|
|
330
|
+
print(path)
|
|
331
|
+
return 0
|
|
332
|
+
|
|
333
|
+
|
|
203
334
|
def cmd_py(args: list) -> int:
|
|
204
335
|
opts, code_parts = _extract_opts(args)
|
|
205
336
|
code = " ".join(code_parts).strip()
|
|
206
337
|
if not code:
|
|
207
338
|
raise CliError("nothing to run. Usage: hyd py '<python code>'")
|
|
208
|
-
|
|
339
|
+
name, profile = _resolve_profile(opts["profile"])
|
|
340
|
+
if _profile_type(profile) == "browser":
|
|
341
|
+
raise CliError(f"profile '{name}' is a browser profile — use `hyd js '<code>'`.")
|
|
209
342
|
data = _call_remote(profile, "execute_code", {"code": code})
|
|
210
343
|
if isinstance(data, dict):
|
|
211
344
|
if data.get("stdout"):
|
|
@@ -243,26 +376,30 @@ def cmd_profile_add(args: list) -> int:
|
|
|
243
376
|
i = 0
|
|
244
377
|
while i < len(args):
|
|
245
378
|
a = args[i]
|
|
246
|
-
if a in ("--token", "--workspace") and i + 1 < len(args):
|
|
379
|
+
if a in ("--token", "--workspace", "--type") and i + 1 < len(args):
|
|
247
380
|
opts[a.lstrip("-")] = args[i + 1]; i += 2; continue
|
|
248
381
|
pos.append(a); i += 1
|
|
249
382
|
if len(pos) < 2:
|
|
250
|
-
raise CliError("usage: hyd profile add <name> <service_url> [--
|
|
383
|
+
raise CliError("usage: hyd profile add <name> <service_url> [--type terminal|browser] [--token T] [--use]")
|
|
251
384
|
name, url = pos[0], _normalize_url(pos[1])
|
|
385
|
+
ptype = opts.get("type")
|
|
386
|
+
if ptype and ptype not in ("terminal", "browser"):
|
|
387
|
+
raise CliError("--type must be 'terminal' or 'browser'")
|
|
252
388
|
profiles = _load_profiles()
|
|
253
389
|
entry = profiles.get(name, {})
|
|
254
390
|
entry["service_url"] = url
|
|
391
|
+
entry["type"] = ptype or entry.get("type") or _infer_type(url)
|
|
255
392
|
if opts.get("token"):
|
|
256
393
|
entry["token"] = opts["token"]
|
|
257
394
|
if opts.get("workspace"):
|
|
258
395
|
entry["workspace"] = opts["workspace"]
|
|
259
396
|
profiles[name] = entry
|
|
260
397
|
_save_profiles(profiles)
|
|
398
|
+
label = f"{url} [{entry['type']}]"
|
|
261
399
|
if use:
|
|
262
|
-
_emit_state({"HYD_PROFILE": name}, human=f"profile '{name}' -> {
|
|
400
|
+
_emit_state({"HYD_PROFILE": name}, human=f"profile '{name}' -> {label} (now active)")
|
|
263
401
|
else:
|
|
264
|
-
#
|
|
265
|
-
print(f"# profile '{name}' -> {url} (activate: export HYD_PROFILE={name})")
|
|
402
|
+
print(f"# profile '{name}' -> {label} (activate: export HYD_PROFILE={name})")
|
|
266
403
|
return 0
|
|
267
404
|
|
|
268
405
|
|
|
@@ -275,7 +412,7 @@ def cmd_profile_list(_args: list) -> int:
|
|
|
275
412
|
for name, p in profiles.items():
|
|
276
413
|
mark = "*" if name == active else " "
|
|
277
414
|
tok = " (token)" if p.get("token") else ""
|
|
278
|
-
print(f"{mark} {name} {p.get('service_url', '')}{tok}")
|
|
415
|
+
print(f"{mark} {name} [{_profile_type(p)}] {p.get('service_url', '')}{tok}")
|
|
279
416
|
return 0
|
|
280
417
|
|
|
281
418
|
|
|
@@ -331,11 +468,16 @@ def cmd_status(_args: list) -> int:
|
|
|
331
468
|
name, profile = _resolve_profile()
|
|
332
469
|
except CliError as e:
|
|
333
470
|
print(e); return 1
|
|
334
|
-
|
|
471
|
+
ptype = _profile_type(profile)
|
|
472
|
+
print(f"profile: {name} [{ptype}] cwd: {_env_cwd() or '(remote default)'} (HYD_PROFILE/HYD_CWD)")
|
|
335
473
|
print(f"url: {_normalize_url(profile['service_url'])}")
|
|
336
474
|
try:
|
|
337
|
-
|
|
338
|
-
|
|
475
|
+
if ptype == "browser":
|
|
476
|
+
info = _call_remote(profile, "get_page_info", {})
|
|
477
|
+
print(f"connected: url={info.get('url')} title={info.get('title')}")
|
|
478
|
+
else:
|
|
479
|
+
info = _call_remote(profile, "get_process_info", {})
|
|
480
|
+
print(f"connected: pid={info.get('pid')} host={info.get('hostname')} py={info.get('python_version')}")
|
|
339
481
|
except CliError as e:
|
|
340
482
|
print(f"UNREACHABLE: {e}"); return 1
|
|
341
483
|
return 0
|
|
@@ -359,17 +501,20 @@ def cmd_shell_init(_args: list) -> int:
|
|
|
359
501
|
|
|
360
502
|
def _print_help() -> int:
|
|
361
503
|
print(
|
|
362
|
-
"""hyd — Hypha remote
|
|
504
|
+
"""hyd — Hypha remote CLI: drive a terminal (Python process) OR a browser with minimal overhead
|
|
363
505
|
|
|
364
|
-
USAGE
|
|
365
|
-
hyd
|
|
366
|
-
hyd '<command>'
|
|
367
|
-
hyd
|
|
368
|
-
hyd
|
|
506
|
+
USAGE (adapts to the active profile's type)
|
|
507
|
+
hyd '<x>' Bare: runs <x> as SHELL on a terminal profile, JAVASCRIPT on a browser one
|
|
508
|
+
hyd sh '<command>' Force a shell command (terminal profile; cwd via HYD_CWD)
|
|
509
|
+
hyd js '<javascript>' Force JavaScript (browser profile) — returns the result
|
|
510
|
+
hyd py '<python>' Run Python via execute_code (terminal profile)
|
|
511
|
+
hyd call <fn> [--json '{…}'] [k=v …] Call ANY service function (either type)
|
|
512
|
+
hyd nav <url> | shot [file] Browser: navigate / save a screenshot (PNG)
|
|
513
|
+
hyd -p <profile> … Run against a specific profile (overrides HYD_PROFILE)
|
|
369
514
|
|
|
370
|
-
PROFILES (machines — stored on disk)
|
|
371
|
-
hyd profile add <name> <service_url> [--
|
|
372
|
-
hyd profile list | show <name> | rm <name>
|
|
515
|
+
PROFILES (machines — stored on disk; each has a type: terminal|browser)
|
|
516
|
+
hyd profile add <name> <service_url> [--type terminal|browser] [--token T] [--use]
|
|
517
|
+
hyd profile list | show <name> | rm <name> (type inferred from the URL if omitted)
|
|
373
518
|
|
|
374
519
|
CURRENT STATE (env vars, per-terminal — NOT on disk)
|
|
375
520
|
export HYD_PROFILE=<name> Select the active profile for this shell
|
|
@@ -397,10 +542,20 @@ def main(argv=None) -> int:
|
|
|
397
542
|
from hypha_debugger import __version__
|
|
398
543
|
print(f"hyd (hypha-debugger) {__version__}")
|
|
399
544
|
return 0
|
|
400
|
-
if cmd in ("
|
|
545
|
+
if cmd in ("run", "exec"):
|
|
546
|
+
return cmd_run(argv[1:])
|
|
547
|
+
if cmd == "sh":
|
|
401
548
|
return cmd_sh(argv[1:])
|
|
549
|
+
if cmd == "js":
|
|
550
|
+
return cmd_js(argv[1:])
|
|
402
551
|
if cmd == "py":
|
|
403
552
|
return cmd_py(argv[1:])
|
|
553
|
+
if cmd == "call":
|
|
554
|
+
return cmd_call(argv[1:])
|
|
555
|
+
if cmd == "nav":
|
|
556
|
+
return cmd_nav(argv[1:])
|
|
557
|
+
if cmd == "shot":
|
|
558
|
+
return cmd_shot(argv[1:])
|
|
404
559
|
if cmd in ("profile", "profiles"):
|
|
405
560
|
return cmd_profile(argv[1:] if cmd == "profile" else ["list"] + argv[1:])
|
|
406
561
|
if cmd == "use":
|
|
@@ -413,8 +568,9 @@ def main(argv=None) -> int:
|
|
|
413
568
|
return cmd_status(argv[1:])
|
|
414
569
|
if cmd == "shell-init":
|
|
415
570
|
return cmd_shell_init(argv[1:])
|
|
416
|
-
# Bare fallback: the whole argv
|
|
417
|
-
|
|
571
|
+
# Bare fallback: run the whole argv, dispatching by profile type
|
|
572
|
+
# (shell on a terminal profile, JavaScript on a browser one).
|
|
573
|
+
return cmd_run(argv, bare=True)
|
|
418
574
|
except CliError as e:
|
|
419
575
|
sys.stderr.write(f"hyd: {e}\n")
|
|
420
576
|
return 2
|
|
@@ -90,18 +90,20 @@ def _get_skill_md_impl() -> str:
|
|
|
90
90
|
|
|
91
91
|
## Overview
|
|
92
92
|
A remote debugger is attached to a running Python process. You have full access
|
|
93
|
-
to
|
|
93
|
+
to run shell + Python, read/write files, inspect variables, and install packages.
|
|
94
94
|
|
|
95
|
-
**
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
**START HERE → install the `hyd` CLI (next section).** Decision rule: if you'll run
|
|
96
|
+
more than ~2 commands here (i.e. almost any real task), `hyd` is the intended
|
|
97
|
+
interface — one `pip install`, then every command is a short `hyd sh '…'` /
|
|
98
|
+
`hyd py '…'` with the connection + working directory remembered for you, far fewer
|
|
99
|
+
tokens than re-sending an authenticated curl each time, and it behaves like a local
|
|
100
|
+
shell. Only for a single throwaway call is raw curl (further down) simpler. The raw
|
|
101
|
+
HTTP API is the low-level fallback and powers the CLI.
|
|
98
102
|
|
|
99
|
-
## Fast path — the `hyd` CLI (
|
|
103
|
+
## Fast path — the `hyd` CLI (do this first)
|
|
100
104
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
It stores this connection as a named **profile** (URL + token) on disk, so later
|
|
104
|
-
calls carry no auth or JSON boilerplate. Bootstrap yourself:
|
|
105
|
+
Bootstrap yourself in three lines (THIS_SERVICE_URL = the URL you fetched this doc
|
|
106
|
+
from, without the trailing `/get_skill_md`):
|
|
105
107
|
|
|
106
108
|
```bash
|
|
107
109
|
pip install hypha-debugger # ships the `hyd` command
|
|
@@ -111,8 +113,13 @@ export HYD_PROFILE=main # select it (env var — per-terminal, not
|
|
|
111
113
|
hyd sh 'uname -a && pwd' # run a remote shell command
|
|
112
114
|
hyd 'ls -la' # bare form: anything not a subcommand runs as a command
|
|
113
115
|
hyd py 'import sys; sys.version' # run Python via execute_code
|
|
116
|
+
hyd status # confirm it's connected
|
|
114
117
|
```
|
|
115
118
|
|
|
119
|
+
If the `hyd` command isn't on your PATH after install (e.g. a `--user` install),
|
|
120
|
+
either add the install bin dir to PATH or just use `python -m hypha_debugger.cli`
|
|
121
|
+
in place of `hyd` — identical arguments (`python -m hypha_debugger.cli sh 'pwd'`).
|
|
122
|
+
|
|
116
123
|
Key ideas:
|
|
117
124
|
- **The remote is stateless.** The "current directory" and "current profile" live
|
|
118
125
|
in the environment variables `HYD_CWD` and `HYD_PROFILE` (per shell), so different
|
|
@@ -122,6 +129,13 @@ Key ideas:
|
|
|
122
129
|
`hyd sh`.
|
|
123
130
|
- **Multiple machines**: add more profiles (`hyd profile add other "<url>" --token …`)
|
|
124
131
|
and switch with `export HYD_PROFILE=other` or a one-off `hyd -p other sh '…'`.
|
|
132
|
+
- **Terminal *or* browser**: each profile has a `type` (inferred from the URL). A
|
|
133
|
+
terminal profile (this Python debugger) runs shell/Python; a `browser` profile (a
|
|
134
|
+
Hypha Navigator web service) runs JavaScript. The SAME bare `hyd '<x>'` adapts —
|
|
135
|
+
shell on a terminal profile, JS on a browser one — or force it with `hyd sh` /
|
|
136
|
+
`hyd js`. `hyd call <fn> [--json '{…}']` calls any function; `hyd nav <url>` /
|
|
137
|
+
`hyd shot [file]` are browser shortcuts. Example:
|
|
138
|
+
`hyd profile add web "<navigator-url>" --type browser --use && hyd 'document.title'`.
|
|
125
139
|
- `hyd sh` streams stdout/stderr and exits with the remote command's exit code, so
|
|
126
140
|
it behaves like a local shell. Run `hyd` (no args) for full help.
|
|
127
141
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hypha-debugger
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Injectable debugger for Python processes and AI agents, powered by Hypha RPC
|
|
5
5
|
Author: Amun AI AB
|
|
6
6
|
License: MIT
|
|
@@ -20,6 +20,7 @@ Requires-Python: >=3.9
|
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
Requires-Dist: hypha-rpc>=0.20.0
|
|
22
22
|
Requires-Dist: pydantic>=2.0
|
|
23
|
+
Requires-Dist: certifi
|
|
23
24
|
Provides-Extra: dev
|
|
24
25
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
26
|
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hypha-debugger"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Injectable debugger for Python processes and AI agents, powered by Hypha RPC"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -25,6 +25,7 @@ classifiers = [
|
|
|
25
25
|
dependencies = [
|
|
26
26
|
"hypha-rpc>=0.20.0",
|
|
27
27
|
"pydantic>=2.0",
|
|
28
|
+
"certifi",
|
|
28
29
|
]
|
|
29
30
|
|
|
30
31
|
[project.urls]
|
|
@@ -116,6 +116,59 @@ def test_bare_fallback_runs_as_command(cfg, monkeypatch):
|
|
|
116
116
|
assert seen["command"] == "ls -la"
|
|
117
117
|
|
|
118
118
|
|
|
119
|
+
def test_type_inference_browser_and_terminal(cfg):
|
|
120
|
+
cli.main(["profile", "add", "web", "https://h.io/services/web-navigator-abc"])
|
|
121
|
+
cli.main(["profile", "add", "box", "https://h.io/services/py-debugger-xyz"])
|
|
122
|
+
profs = cli._load_profiles()
|
|
123
|
+
assert profs["web"]["type"] == "browser"
|
|
124
|
+
assert profs["box"]["type"] == "terminal"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_type_explicit_override(cfg):
|
|
128
|
+
cli.main(["profile", "add", "x", "https://h.io/services/anything", "--type", "browser"])
|
|
129
|
+
assert cli._load_profiles()["x"]["type"] == "browser"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_bare_dispatches_by_type(cfg, monkeypatch):
|
|
133
|
+
cli.main(["profile", "add", "web", "https://h.io/services/web-navigator-abc"])
|
|
134
|
+
monkeypatch.setenv("HYD_PROFILE", "web")
|
|
135
|
+
seen = {}
|
|
136
|
+
monkeypatch.setattr(cli, "_call_remote", lambda profile, fn, params, http_timeout=75: seen.update(fn=fn, params=params) or {"result": "My Page", "type": "string"})
|
|
137
|
+
rc = cli.main(["document.title"]) # bare, browser profile -> execute_script
|
|
138
|
+
assert rc == 0 and seen["fn"] == "execute_script" and seen["params"]["code"] == "document.title"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_js_result_printed(cfg, monkeypatch, capsys):
|
|
142
|
+
cli.main(["profile", "add", "web", "https://h.io/services/web-navigator-abc"])
|
|
143
|
+
monkeypatch.setenv("HYD_PROFILE", "web")
|
|
144
|
+
monkeypatch.setattr(cli, "_call_remote", lambda *a, **k: {"result": {"a": 1}, "type": "object"})
|
|
145
|
+
cli.main(["js", "({a:1})"])
|
|
146
|
+
assert '"a": 1' in capsys.readouterr().out
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_sh_rejects_browser_profile(cfg, monkeypatch, capsys):
|
|
150
|
+
cli.main(["profile", "add", "web", "https://h.io/services/web-navigator-abc"])
|
|
151
|
+
monkeypatch.setenv("HYD_PROFILE", "web")
|
|
152
|
+
rc = cli.main(["sh", "ls"])
|
|
153
|
+
assert rc == 2 and "browser profile" in capsys.readouterr().err
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_js_rejects_terminal_profile(cfg, monkeypatch, capsys):
|
|
157
|
+
cli.main(["profile", "add", "box", "https://h.io/services/py-debugger-xyz"])
|
|
158
|
+
monkeypatch.setenv("HYD_PROFILE", "box")
|
|
159
|
+
rc = cli.main(["js", "1+1"])
|
|
160
|
+
assert rc == 2 and "terminal profile" in capsys.readouterr().err
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_call_generic_passthrough(cfg, monkeypatch):
|
|
164
|
+
cli.main(["profile", "add", "web", "https://h.io/services/web-navigator-abc"])
|
|
165
|
+
monkeypatch.setenv("HYD_PROFILE", "web")
|
|
166
|
+
seen = {}
|
|
167
|
+
monkeypatch.setattr(cli, "_call_remote", lambda profile, fn, params, http_timeout=75: seen.update(fn=fn, params=params) or {"ok": True})
|
|
168
|
+
cli.main(["call", "navigate", "--json", '{"url":"https://x.com"}'])
|
|
169
|
+
assert seen["fn"] == "navigate" and seen["params"]["url"] == "https://x.com"
|
|
170
|
+
|
|
171
|
+
|
|
119
172
|
def test_p_flag_selects_profile(cfg, monkeypatch):
|
|
120
173
|
cli.main(["profile", "add", "a", "https://a.io/s/1"])
|
|
121
174
|
cli.main(["profile", "add", "b", "https://b.io/s/2"])
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|