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.
Files changed (26) hide show
  1. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/PKG-INFO +2 -1
  2. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/__init__.py +1 -1
  3. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/cli.py +190 -34
  4. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/services/source.py +23 -9
  5. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger.egg-info/PKG-INFO +2 -1
  6. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger.egg-info/requires.txt +1 -0
  7. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/pyproject.toml +2 -1
  8. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/tests/test_cli.py +53 -0
  9. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/README.md +0 -0
  10. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/__main__.py +0 -0
  11. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/debugger.py +0 -0
  12. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/services/__init__.py +0 -0
  13. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/services/execute.py +0 -0
  14. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/services/filesystem.py +0 -0
  15. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/services/info.py +0 -0
  16. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/services/inspect_vars.py +0 -0
  17. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/services/shell.py +0 -0
  18. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/utils/__init__.py +0 -0
  19. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger/utils/env.py +0 -0
  20. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger.egg-info/SOURCES.txt +0 -0
  21. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger.egg-info/dependency_links.txt +0 -0
  22. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger.egg-info/entry_points.txt +0 -0
  23. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/hypha_debugger.egg-info/top_level.txt +0 -0
  24. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/setup.cfg +0 -0
  25. {hypha_debugger-0.2.0 → hypha_debugger-0.2.2}/tests/test_services.py +0 -0
  26. {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.0
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"
@@ -12,5 +12,5 @@ Usage (sync):
12
12
 
13
13
  from hypha_debugger.debugger import start_debugger, start_debugger_sync, DebugSession
14
14
 
15
- __version__ = "0.2.0"
15
+ __version__ = "0.2.2"
16
16
  __all__ = ["start_debugger", "start_debugger_sync", "DebugSession", "__version__"]
@@ -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 cmd_sh(args: list, bare: bool = False) -> int:
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
- _name, profile = _resolve_profile(opts["profile"])
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> [--token T] [--workspace W] [--use]")
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}' -> {url} (now active)")
400
+ _emit_state({"HYD_PROFILE": name}, human=f"profile '{name}' -> {label} (now active)")
263
401
  else:
264
- # No current-state change just confirm (eval-safe comment).
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
- print(f"profile: {name} cwd: {_env_cwd() or '(remote default)'} (HYD_PROFILE/HYD_CWD)")
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
- info = _call_remote(profile, "get_process_info", {})
338
- print(f"connected: pid={info.get('pid')} host={info.get('hostname')} py={info.get('python_version')}")
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 debugger CLI (run remote shell commands with minimal overhead)
504
+ """hyd — Hypha remote CLI: drive a terminal (Python process) OR a browser with minimal overhead
363
505
 
364
- USAGE
365
- hyd sh '<command>' Run a shell command on the active profile (cwd via HYD_CWD)
366
- hyd '<command>' Same (bare form; anything not a subcommand runs as a command)
367
- hyd py '<python>' Run Python via the debugger's execute_code
368
- hyd -p <profile> sh '...' Run against a specific profile (overrides HYD_PROFILE)
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> [--token T] [--workspace W] [--use]
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 ("sh", "run", "exec"):
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 is a shell command.
417
- return cmd_sh(argv, bare=True)
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 execute code, read/write files, inspect variables, and install packages.
93
+ to run shell + Python, read/write files, inspect variables, and install packages.
94
94
 
95
- **Recommended approach**: Use `execute_code` as your primary tool it's a
96
- persistent Python REPL where variables, imports, and functions survive across
97
- calls. The other endpoints are convenience shortcuts.
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 (recommended for agents)
103
+ ## Fast path — the `hyd` CLI (do this first)
100
104
 
101
- For repeated shell work, don't hand-write authenticated curl calls install the
102
- `hyd` client CLI once and run remote commands with almost no per-call overhead.
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.0
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"
@@ -1,5 +1,6 @@
1
1
  hypha-rpc>=0.20.0
2
2
  pydantic>=2.0
3
+ certifi
3
4
 
4
5
  [dev]
5
6
  pytest>=7.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hypha-debugger"
7
- version = "0.2.0"
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