ephem-debugger-py 0.3.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.
- ephem_debugger_py-0.3.2/PKG-INFO +89 -0
- ephem_debugger_py-0.3.2/README.md +72 -0
- ephem_debugger_py-0.3.2/pyproject.toml +23 -0
- ephem_debugger_py-0.3.2/src/ephem_debugger_py/__init__.py +48 -0
- ephem_debugger_py-0.3.2/src/ephem_debugger_py/bridge.py +175 -0
- ephem_debugger_py-0.3.2/src/ephem_debugger_py/browser.py +436 -0
- ephem_debugger_py-0.3.2/src/ephem_debugger_py/capture.py +37 -0
- ephem_debugger_py-0.3.2/src/ephem_debugger_py/middleware/__init__.py +0 -0
- ephem_debugger_py-0.3.2/src/ephem_debugger_py/middleware/django.py +118 -0
- ephem_debugger_py-0.3.2/src/ephem_debugger_py/middleware/fastapi.py +158 -0
- ephem_debugger_py-0.3.2/src/ephem_debugger_py/middleware/flask.py +159 -0
- ephem_debugger_py-0.3.2/src/ephem_debugger_py/protocol.py +113 -0
- ephem_debugger_py-0.3.2/src/ephem_debugger_py/py.typed +0 -0
- ephem_debugger_py-0.3.2/src/ephem_debugger_py/store.py +141 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: ephem-debugger-py
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Dev-only observability for AI agent debugging
|
|
5
|
+
Author: ephem-sh
|
|
6
|
+
Requires-Dist: django>=4.2 ; extra == 'django'
|
|
7
|
+
Requires-Dist: fastapi>=0.100.0 ; extra == 'fastapi'
|
|
8
|
+
Requires-Dist: starlette>=0.27.0 ; extra == 'fastapi'
|
|
9
|
+
Requires-Dist: flask>=2.3 ; extra == 'flask'
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Project-URL: Homepage, https://debugger.ephem.sh/
|
|
12
|
+
Project-URL: Repository, https://github.com/ephem-sh/debugger
|
|
13
|
+
Provides-Extra: django
|
|
14
|
+
Provides-Extra: fastapi
|
|
15
|
+
Provides-Extra: flask
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# ephem-debugger-py
|
|
19
|
+
|
|
20
|
+
Dev-only observability middleware for Python web frameworks. Part of the [debugger](https://github.com/ephem-sh/debugger) project.
|
|
21
|
+
|
|
22
|
+
Captures HTTP requests, console output, and browser-side data from your dev server. AI agents query this data through the `dbg` CLI.
|
|
23
|
+
|
|
24
|
+
> **Preview** — under active development.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install ephem-debugger-py
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## FastAPI
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from fastapi import FastAPI
|
|
36
|
+
from ephem_debugger_py.middleware.fastapi import instrument
|
|
37
|
+
|
|
38
|
+
app = FastAPI()
|
|
39
|
+
instrument(app, port=8000)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Flask
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from flask import Flask
|
|
46
|
+
from ephem_debugger_py.middleware.flask import init_debugger
|
|
47
|
+
|
|
48
|
+
app = Flask(__name__)
|
|
49
|
+
init_debugger(app, port=5000)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Django
|
|
53
|
+
|
|
54
|
+
Add to `settings.py`:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
MIDDLEWARE = [
|
|
58
|
+
'ephem_debugger_py.middleware.django.DebuggerMiddleware',
|
|
59
|
+
# ... other middleware
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
DEBUGGER_PORT = 8000
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Query with CLI
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx dbg status
|
|
69
|
+
npx dbg server console
|
|
70
|
+
npx dbg browser console
|
|
71
|
+
npx dbg browser network
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## How it works
|
|
75
|
+
|
|
76
|
+
1. Middleware captures HTTP request/response metadata and logging output
|
|
77
|
+
2. IPC bridge exposes data via Unix socket (or TCP on Windows)
|
|
78
|
+
3. Browser client script is auto-injected into HTML responses
|
|
79
|
+
4. `dbg` CLI queries the session — works the same across all languages
|
|
80
|
+
|
|
81
|
+
## Other languages
|
|
82
|
+
|
|
83
|
+
- **Node.js** — [`@ephem-sh/debugger`](https://www.npmjs.com/package/@ephem-sh/debugger)
|
|
84
|
+
- **Go** — [`ephem-debugger-go`](https://github.com/ephem-sh/debugger/tree/main/packages/debugger-go)
|
|
85
|
+
- **Rust** — [`ephem-debugger-rs`](https://crates.io/crates/ephem-debugger-rs)
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# ephem-debugger-py
|
|
2
|
+
|
|
3
|
+
Dev-only observability middleware for Python web frameworks. Part of the [debugger](https://github.com/ephem-sh/debugger) project.
|
|
4
|
+
|
|
5
|
+
Captures HTTP requests, console output, and browser-side data from your dev server. AI agents query this data through the `dbg` CLI.
|
|
6
|
+
|
|
7
|
+
> **Preview** — under active development.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install ephem-debugger-py
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## FastAPI
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from fastapi import FastAPI
|
|
19
|
+
from ephem_debugger_py.middleware.fastapi import instrument
|
|
20
|
+
|
|
21
|
+
app = FastAPI()
|
|
22
|
+
instrument(app, port=8000)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Flask
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from flask import Flask
|
|
29
|
+
from ephem_debugger_py.middleware.flask import init_debugger
|
|
30
|
+
|
|
31
|
+
app = Flask(__name__)
|
|
32
|
+
init_debugger(app, port=5000)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Django
|
|
36
|
+
|
|
37
|
+
Add to `settings.py`:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
MIDDLEWARE = [
|
|
41
|
+
'ephem_debugger_py.middleware.django.DebuggerMiddleware',
|
|
42
|
+
# ... other middleware
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
DEBUGGER_PORT = 8000
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Query with CLI
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx dbg status
|
|
52
|
+
npx dbg server console
|
|
53
|
+
npx dbg browser console
|
|
54
|
+
npx dbg browser network
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## How it works
|
|
58
|
+
|
|
59
|
+
1. Middleware captures HTTP request/response metadata and logging output
|
|
60
|
+
2. IPC bridge exposes data via Unix socket (or TCP on Windows)
|
|
61
|
+
3. Browser client script is auto-injected into HTML responses
|
|
62
|
+
4. `dbg` CLI queries the session — works the same across all languages
|
|
63
|
+
|
|
64
|
+
## Other languages
|
|
65
|
+
|
|
66
|
+
- **Node.js** — [`@ephem-sh/debugger`](https://www.npmjs.com/package/@ephem-sh/debugger)
|
|
67
|
+
- **Go** — [`ephem-debugger-go`](https://github.com/ephem-sh/debugger/tree/main/packages/debugger-go)
|
|
68
|
+
- **Rust** — [`ephem-debugger-rs`](https://crates.io/crates/ephem-debugger-rs)
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ephem-debugger-py"
|
|
3
|
+
version = "0.3.2"
|
|
4
|
+
description = "Dev-only observability for AI agent debugging"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "ephem-sh" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[project.urls]
|
|
13
|
+
Homepage = "https://debugger.ephem.sh/"
|
|
14
|
+
Repository = "https://github.com/ephem-sh/debugger"
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
fastapi = ["fastapi>=0.100.0", "starlette>=0.27.0"]
|
|
18
|
+
django = ["django>=4.2"]
|
|
19
|
+
flask = ["flask>=2.3"]
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["uv_build>=0.10.10,<0.11.0"]
|
|
23
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Dev-only observability for AI agent debugging.
|
|
2
|
+
|
|
3
|
+
Usage with FastAPI::
|
|
4
|
+
|
|
5
|
+
from ephem_debugger_py.middleware.fastapi import Middleware, logger, close
|
|
6
|
+
app.add_middleware(Middleware, port=8000)
|
|
7
|
+
|
|
8
|
+
Usage with Django (settings.py)::
|
|
9
|
+
|
|
10
|
+
MIDDLEWARE = ["ephem_debugger_py.middleware.django.DebuggerMiddleware", ...]
|
|
11
|
+
|
|
12
|
+
Usage with Flask::
|
|
13
|
+
|
|
14
|
+
from ephem_debugger_py.middleware.flask import init_debugger
|
|
15
|
+
init_debugger(app, port=5000)
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from .bridge import Bridge
|
|
20
|
+
from .capture import DebuggerHandler
|
|
21
|
+
from .protocol import SessionInfo, compute_socket_path, create_session
|
|
22
|
+
from .store import LogStore
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Debugger:
|
|
26
|
+
"""Main debugger instance. Creates store, bridge, and logging handler."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, framework: str, port: int) -> None:
|
|
29
|
+
self.session = create_session(framework, port)
|
|
30
|
+
self.store = LogStore(self.session)
|
|
31
|
+
self.bridge = Bridge(self.store, self.session.socket_path)
|
|
32
|
+
self.handler = DebuggerHandler(self.store)
|
|
33
|
+
self.bridge.start()
|
|
34
|
+
|
|
35
|
+
def close(self) -> None:
|
|
36
|
+
"""Stop the bridge and clean up resources."""
|
|
37
|
+
self.bridge.stop()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"Debugger",
|
|
42
|
+
"LogStore",
|
|
43
|
+
"Bridge",
|
|
44
|
+
"DebuggerHandler",
|
|
45
|
+
"SessionInfo",
|
|
46
|
+
"create_session",
|
|
47
|
+
"compute_socket_path",
|
|
48
|
+
]
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""IPC bridge -- NDJSON server for the dbg CLI."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import socket
|
|
7
|
+
import threading
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .store import LogStore
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Bridge:
|
|
15
|
+
"""NDJSON IPC server that the dbg CLI connects to.
|
|
16
|
+
|
|
17
|
+
On Windows, uses TCP on localhost with a .debugger/bridge.addr discovery
|
|
18
|
+
file. On Unix, uses a Unix domain socket.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, store: LogStore, socket_path: str) -> None:
|
|
22
|
+
self._store = store
|
|
23
|
+
self._socket_path = socket_path
|
|
24
|
+
self._server: socket.socket | None = None
|
|
25
|
+
self._thread: threading.Thread | None = None
|
|
26
|
+
self._running = False
|
|
27
|
+
|
|
28
|
+
def start(self) -> None:
|
|
29
|
+
"""Start listening for CLI connections."""
|
|
30
|
+
if os.name == "nt":
|
|
31
|
+
self._start_tcp()
|
|
32
|
+
else:
|
|
33
|
+
self._start_unix()
|
|
34
|
+
|
|
35
|
+
def _start_unix(self) -> None:
|
|
36
|
+
sock_dir = os.path.dirname(self._socket_path)
|
|
37
|
+
os.makedirs(sock_dir, exist_ok=True)
|
|
38
|
+
try:
|
|
39
|
+
os.unlink(self._socket_path)
|
|
40
|
+
except OSError:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
self._server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
44
|
+
self._server.bind(self._socket_path)
|
|
45
|
+
self._server.listen(5)
|
|
46
|
+
self._server.settimeout(1.0)
|
|
47
|
+
self._running = True
|
|
48
|
+
self._thread = threading.Thread(target=self._accept_loop, daemon=True)
|
|
49
|
+
self._thread.start()
|
|
50
|
+
self._write_session_file()
|
|
51
|
+
|
|
52
|
+
def _start_tcp(self) -> None:
|
|
53
|
+
self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
54
|
+
self._server.bind(("127.0.0.1", 0))
|
|
55
|
+
self._server.listen(5)
|
|
56
|
+
self._server.settimeout(1.0)
|
|
57
|
+
|
|
58
|
+
addr = self._server.getsockname()
|
|
59
|
+
addr_str = f"{addr[0]}:{addr[1]}"
|
|
60
|
+
|
|
61
|
+
# Write addr file so CLI can discover us
|
|
62
|
+
addr_dir = os.path.join(os.getcwd(), ".debugger")
|
|
63
|
+
os.makedirs(addr_dir, exist_ok=True)
|
|
64
|
+
addr_file = os.path.join(addr_dir, "bridge.addr")
|
|
65
|
+
with open(addr_file, "w") as f:
|
|
66
|
+
f.write(addr_str)
|
|
67
|
+
|
|
68
|
+
# Update session socket_path to actual TCP address for discovery
|
|
69
|
+
self._store.session.socket_path = addr_str
|
|
70
|
+
|
|
71
|
+
self._running = True
|
|
72
|
+
self._thread = threading.Thread(target=self._accept_loop, daemon=True)
|
|
73
|
+
self._thread.start()
|
|
74
|
+
self._write_session_file()
|
|
75
|
+
|
|
76
|
+
def _write_session_file(self) -> None:
|
|
77
|
+
"""Write session.json for multi-session discovery."""
|
|
78
|
+
session_dir = os.path.join(os.getcwd(), ".debugger")
|
|
79
|
+
os.makedirs(session_dir, exist_ok=True)
|
|
80
|
+
session_file = os.path.join(session_dir, "session.json")
|
|
81
|
+
with open(session_file, "w") as f:
|
|
82
|
+
json.dump(self._store.session.to_dict(), f)
|
|
83
|
+
f.write("\n")
|
|
84
|
+
|
|
85
|
+
def _accept_loop(self) -> None:
|
|
86
|
+
while self._running:
|
|
87
|
+
try:
|
|
88
|
+
if self._server is None:
|
|
89
|
+
break
|
|
90
|
+
conn, _ = self._server.accept()
|
|
91
|
+
threading.Thread(
|
|
92
|
+
target=self._handle_conn, args=(conn,), daemon=True
|
|
93
|
+
).start()
|
|
94
|
+
except socket.timeout:
|
|
95
|
+
continue
|
|
96
|
+
except OSError:
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
def _handle_conn(self, conn: socket.socket) -> None:
|
|
100
|
+
try:
|
|
101
|
+
data = b""
|
|
102
|
+
while True:
|
|
103
|
+
chunk = conn.recv(4096)
|
|
104
|
+
if not chunk:
|
|
105
|
+
break
|
|
106
|
+
data += chunk
|
|
107
|
+
while b"\n" in data:
|
|
108
|
+
line, data = data.split(b"\n", 1)
|
|
109
|
+
if not line.strip():
|
|
110
|
+
continue
|
|
111
|
+
self._handle_request(conn, line)
|
|
112
|
+
except OSError:
|
|
113
|
+
pass
|
|
114
|
+
finally:
|
|
115
|
+
conn.close()
|
|
116
|
+
|
|
117
|
+
def _handle_request(self, conn: socket.socket, line: bytes) -> None:
|
|
118
|
+
try:
|
|
119
|
+
req = json.loads(line)
|
|
120
|
+
except json.JSONDecodeError:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
req_id = req.get("id", "")
|
|
124
|
+
command = req.get("command", "")
|
|
125
|
+
|
|
126
|
+
if command == "push":
|
|
127
|
+
# Push entries from external sources
|
|
128
|
+
entries = req.get("entries", [])
|
|
129
|
+
if isinstance(entries, list):
|
|
130
|
+
for entry in entries:
|
|
131
|
+
self._store.push(entry)
|
|
132
|
+
resp = {"id": req_id, "ok": True, "data": []}
|
|
133
|
+
elif command == "status":
|
|
134
|
+
resp = {
|
|
135
|
+
"id": req_id,
|
|
136
|
+
"ok": True,
|
|
137
|
+
"data": [],
|
|
138
|
+
"session": self._store.session.to_dict(),
|
|
139
|
+
}
|
|
140
|
+
else:
|
|
141
|
+
filters = req.get("filters")
|
|
142
|
+
data = self._store.query(command, filters)
|
|
143
|
+
resp = {"id": req_id, "ok": True, "data": data}
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
conn.sendall(json.dumps(resp).encode() + b"\n")
|
|
147
|
+
except OSError:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
def stop(self) -> None:
|
|
151
|
+
"""Stop the bridge and clean up socket/addr files."""
|
|
152
|
+
self._running = False
|
|
153
|
+
if self._server:
|
|
154
|
+
self._server.close()
|
|
155
|
+
if self._thread:
|
|
156
|
+
self._thread.join(timeout=2)
|
|
157
|
+
|
|
158
|
+
# Remove session file
|
|
159
|
+
try:
|
|
160
|
+
os.unlink(os.path.join(os.getcwd(), ".debugger", "session.json"))
|
|
161
|
+
except OSError:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
# Cleanup
|
|
165
|
+
if os.name != "nt":
|
|
166
|
+
try:
|
|
167
|
+
os.unlink(self._socket_path)
|
|
168
|
+
except OSError:
|
|
169
|
+
pass
|
|
170
|
+
else:
|
|
171
|
+
try:
|
|
172
|
+
addr_file = os.path.join(os.getcwd(), ".debugger", "bridge.addr")
|
|
173
|
+
os.unlink(addr_file)
|
|
174
|
+
except OSError:
|
|
175
|
+
pass
|