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.
@@ -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