repld-tool 0.0.1__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Fredrik Angelsen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,221 @@
1
+ Metadata-Version: 2.4
2
+ Name: repld-tool
3
+ Version: 0.0.1
4
+ Summary: Persistent Python runtime with MCP channel push. Dev shell and autonomous-agent substrate in one package.
5
+ Keywords: repl,mcp,agent,browser
6
+ Author: Fredrik Angelsen
7
+ Author-email: Fredrik Angelsen <fredrikangelsen@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Dist: duckdb>=1.5.2 ; extra == 'browser'
13
+ Requires-Dist: websockets>=16.0 ; extra == 'browser'
14
+ Requires-Dist: rich>=15.0.0 ; extra == 'pretty'
15
+ Requires-Python: >=3.12
16
+ Provides-Extra: browser
17
+ Provides-Extra: pretty
18
+ Description-Content-Type: text/markdown
19
+
20
+ # repld
21
+
22
+ Persistent Python runtime with MCP channel push. Dev shell and autonomous-agent substrate in one package.
23
+
24
+ ```bash
25
+ uv tool install repld-tool
26
+ ```
27
+
28
+ ## What it does
29
+
30
+ - **Stateful kernel** — auth once, hold the client, query across turns. State persists across cells.
31
+ - **Async-native** — top-level `await`, `defer()` for fire-and-forget, `@every()` for periodic tasks. Long jobs never block the turn.
32
+ - **Channel push** — task completion, webhooks, file changes, and timers arrive as `<channel>` injections. The agent reacts; it doesn't poll.
33
+ - **Shared namespace** — human and agent operate on the same `__main__`. Stage data in one, use it in the other.
34
+ - **Browser integration** — attach to your logged-in Chrome tabs via CDP. No API keys, no OAuth dance. The agent discovers the API surface from your traffic.
35
+ - **Gists** — reusable Python modules that wrap any web app's API. The browser supplies auth; the gist captures the pattern.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ # install globally
41
+ uv tool install repld-tool
42
+
43
+ # in any project:
44
+ cd path/to/project
45
+ repld init # writes .mcp.json + updates .gitignore
46
+ repld # starts the kernel
47
+ ```
48
+
49
+ Project-local alternative: `uv add --dev repld-tool`, then point `.mcp.json` at `uv run repld bridge`.
50
+
51
+ `repld init` produces this `.mcp.json`:
52
+
53
+ ```json
54
+ {
55
+ "mcpServers": {
56
+ "repld": { "type": "stdio", "command": "repld", "args": ["bridge"] }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## Quick example
62
+
63
+ The agent calls `exec` to run Python in the kernel:
64
+
65
+ ```python
66
+ # runs inline — result returned immediately
67
+ import httpx
68
+ httpx.get("https://api.example.com/status").json()
69
+
70
+ # long-running — returns task_id, pushes channel notification on completion
71
+ await asyncio.sleep(30)
72
+ notify("done", kind="migration")
73
+ ```
74
+
75
+ Autonomous worker — five lines:
76
+
77
+ ```python
78
+ @every(300)
79
+ async def check_overdue():
80
+ for inv in await po.get_overdue():
81
+ notify(f"Overdue: {inv.customer} {inv.amount} NOK",
82
+ kind="overdue", invoice_id=inv.id)
83
+ ```
84
+
85
+ The kernel runs the watcher; the agent reacts to each `<channel>` injection.
86
+
87
+ ## With an existing app
88
+
89
+ `repld` inherits your project's environment. A `repl.py` at the project root:
90
+
91
+ ```python
92
+ from myapp.main import app
93
+ from myapp.db import async_session_maker
94
+ import asyncio, uvicorn
95
+
96
+ asyncio.create_task(uvicorn.Server(
97
+ uvicorn.Config(app, host="127.0.0.1", port=8000, log_level="warning")
98
+ ).serve())
99
+
100
+ session = async_session_maker()
101
+ print("FastAPI on :8000, db session ready")
102
+ ```
103
+
104
+ ```bash
105
+ repld --init repl.py
106
+ ```
107
+
108
+ The agent now has a live handle on your running app: inspect routes, query the ORM, call handlers bypassing HTTP.
109
+
110
+ ## Tools
111
+
112
+ **Core:**
113
+
114
+ | Tool | What it does |
115
+ |------|-------------|
116
+ | `exec` | Execute Python. Returns inline within timeout (default 2s); otherwise returns `task_id` and pushes channel on completion. |
117
+ | `get_task` | Status + head/tail preview of a running task's output. |
118
+ | `cancel` | Cancel a running task by id. |
119
+
120
+ **Browser** (requires `uv tool install repld-tool[browser]`):
121
+
122
+ | Tool | What it does |
123
+ |------|-------------|
124
+ | `browser_attach` | Watch URL pattern, auto-attach matching tabs. |
125
+ | `browser_tabs` | List attached tabs. |
126
+ | `browser_pages` | List all Chrome targets. |
127
+ | `browser_js` | Evaluate JavaScript in a tab. |
128
+ | `browser_network` | Query captured traffic (HAR-style, DuckDB). |
129
+ | `browser_body` | Response body for a captured request. |
130
+ | `browser_request` | Request headers/postData for a captured request. |
131
+ | `browser_fetch` | In-page fetch (inherits auth/cookies). |
132
+ | `browser_click` | Click element (trusted dispatch). |
133
+ | `browser_type` | Type into element (trusted dispatch). |
134
+ | `browser_key` | Send key press (Enter, Escape, etc). |
135
+ | `browser_navigate` | Navigate tab to URL. |
136
+ | `browser_open` | Open new tab. |
137
+ | `browser_console` | Query console logs and exceptions. |
138
+ | `browser_screenshot` | Capture page screenshot. |
139
+ | `browser_cdp` | Raw CDP passthrough. |
140
+ | `browser_clear` | Reset captured network/console. |
141
+ | `browser_detach` | Remove watch pattern, detach tabs. |
142
+
143
+ Output from every cell spills to `$XDG_RUNTIME_DIR/repld/` — the inline response carries a head/tail preview plus the spill path. Use standard `Read`/`Grep` tools for full output.
144
+
145
+ ## Helpers
146
+
147
+ Available in the kernel namespace:
148
+
149
+ ```python
150
+ notify(content, **meta) # channel push to the agent
151
+ ask(prompt) # block on free-form human input
152
+ confirm(prompt) # block on yes/no
153
+ choose(prompt, options) # block on pick-one
154
+ defer(coro, label=None) # fire-and-forget, channel push on completion
155
+ @every(seconds) # periodic ticker, fn.cancel() to stop
156
+ ```
157
+
158
+ Browser builtins (when `repld[browser]` is installed):
159
+
160
+ ```python
161
+ tab = await browser.get("*example.com*") # find tab by URL glob
162
+ tab = await browser.open("https://...") # open new tab
163
+ await browser.watch("*pattern*") # auto-attach matching tabs
164
+
165
+ await tab.js("document.title") # eval JS
166
+ await tab.fetch("/api/data") # in-page fetch (inherits session)
167
+ await tab.click("#submit") # trusted click
168
+ await tab.type_text("#search", "query") # trusted typing
169
+ tab.network(url="*api*") # query captured traffic
170
+ ```
171
+
172
+ ## Gists
173
+
174
+ Gists are Python modules in `./gists/` (project) or `~/.repld/gists/` (global) that wrap anything into a callable API — web apps via the browser, databases, graph stores, embedding indexes, internal services.
175
+
176
+ ```python
177
+ # gists/myapp.py
178
+ """MyApp — accounts and transactions."""
179
+
180
+ class MyApp:
181
+ def __init__(self, tab): self._tab = tab
182
+
183
+ @classmethod
184
+ async def connect(cls):
185
+ from __main__ import browser
186
+ tab = await browser.get("*myapp.com*")
187
+ return cls(tab)
188
+
189
+ async def accounts(self):
190
+ return (await self._tab.fetch("/api/accounts"))["body"]
191
+ ```
192
+
193
+ ```python
194
+ import myapp
195
+ app = await myapp.MyApp.connect()
196
+ await app.accounts()
197
+ ```
198
+
199
+ Re-importing after edits auto-reloads. Gists can register MCP tools via `__repld_tools__` — scaffold with `repld gist <name>`. Run `repld help gists` for details.
200
+
201
+ ## Browser
202
+
203
+ `repld[browser]` attaches to Chrome via CDP (`--remote-debugging-port=9222`). You log in normally; the agent sees your traffic, discovers the API surface, and works with your authenticated sessions.
204
+
205
+ ```python
206
+ tab = await browser.get("*salesforce*")
207
+ reqs = tab.network(url="*/api/*") # discover API calls
208
+ auth = reqs[0].request_headers["Authorization"] # extract auth
209
+ ```
210
+
211
+ Body capture via Fetch interception means login flows, redirects, and CSRF exchanges are never lost. See [docs/browser.md](docs/browser.md) for the full design.
212
+
213
+ ## Scope
214
+
215
+ `repld` executes arbitrary Python in your project environment. It is a **dev-time tool** — never a runtime dependency. The IPC socket is localhost-only with user-only permissions.
216
+
217
+ Channels are a research-preview feature of Claude Code. The current integration uses `--dangerously-load-development-channels server:repld`.
218
+
219
+ ## License
220
+
221
+ [MIT](LICENSE)
@@ -0,0 +1,202 @@
1
+ # repld
2
+
3
+ Persistent Python runtime with MCP channel push. Dev shell and autonomous-agent substrate in one package.
4
+
5
+ ```bash
6
+ uv tool install repld-tool
7
+ ```
8
+
9
+ ## What it does
10
+
11
+ - **Stateful kernel** — auth once, hold the client, query across turns. State persists across cells.
12
+ - **Async-native** — top-level `await`, `defer()` for fire-and-forget, `@every()` for periodic tasks. Long jobs never block the turn.
13
+ - **Channel push** — task completion, webhooks, file changes, and timers arrive as `<channel>` injections. The agent reacts; it doesn't poll.
14
+ - **Shared namespace** — human and agent operate on the same `__main__`. Stage data in one, use it in the other.
15
+ - **Browser integration** — attach to your logged-in Chrome tabs via CDP. No API keys, no OAuth dance. The agent discovers the API surface from your traffic.
16
+ - **Gists** — reusable Python modules that wrap any web app's API. The browser supplies auth; the gist captures the pattern.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ # install globally
22
+ uv tool install repld-tool
23
+
24
+ # in any project:
25
+ cd path/to/project
26
+ repld init # writes .mcp.json + updates .gitignore
27
+ repld # starts the kernel
28
+ ```
29
+
30
+ Project-local alternative: `uv add --dev repld-tool`, then point `.mcp.json` at `uv run repld bridge`.
31
+
32
+ `repld init` produces this `.mcp.json`:
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "repld": { "type": "stdio", "command": "repld", "args": ["bridge"] }
38
+ }
39
+ }
40
+ ```
41
+
42
+ ## Quick example
43
+
44
+ The agent calls `exec` to run Python in the kernel:
45
+
46
+ ```python
47
+ # runs inline — result returned immediately
48
+ import httpx
49
+ httpx.get("https://api.example.com/status").json()
50
+
51
+ # long-running — returns task_id, pushes channel notification on completion
52
+ await asyncio.sleep(30)
53
+ notify("done", kind="migration")
54
+ ```
55
+
56
+ Autonomous worker — five lines:
57
+
58
+ ```python
59
+ @every(300)
60
+ async def check_overdue():
61
+ for inv in await po.get_overdue():
62
+ notify(f"Overdue: {inv.customer} {inv.amount} NOK",
63
+ kind="overdue", invoice_id=inv.id)
64
+ ```
65
+
66
+ The kernel runs the watcher; the agent reacts to each `<channel>` injection.
67
+
68
+ ## With an existing app
69
+
70
+ `repld` inherits your project's environment. A `repl.py` at the project root:
71
+
72
+ ```python
73
+ from myapp.main import app
74
+ from myapp.db import async_session_maker
75
+ import asyncio, uvicorn
76
+
77
+ asyncio.create_task(uvicorn.Server(
78
+ uvicorn.Config(app, host="127.0.0.1", port=8000, log_level="warning")
79
+ ).serve())
80
+
81
+ session = async_session_maker()
82
+ print("FastAPI on :8000, db session ready")
83
+ ```
84
+
85
+ ```bash
86
+ repld --init repl.py
87
+ ```
88
+
89
+ The agent now has a live handle on your running app: inspect routes, query the ORM, call handlers bypassing HTTP.
90
+
91
+ ## Tools
92
+
93
+ **Core:**
94
+
95
+ | Tool | What it does |
96
+ |------|-------------|
97
+ | `exec` | Execute Python. Returns inline within timeout (default 2s); otherwise returns `task_id` and pushes channel on completion. |
98
+ | `get_task` | Status + head/tail preview of a running task's output. |
99
+ | `cancel` | Cancel a running task by id. |
100
+
101
+ **Browser** (requires `uv tool install repld-tool[browser]`):
102
+
103
+ | Tool | What it does |
104
+ |------|-------------|
105
+ | `browser_attach` | Watch URL pattern, auto-attach matching tabs. |
106
+ | `browser_tabs` | List attached tabs. |
107
+ | `browser_pages` | List all Chrome targets. |
108
+ | `browser_js` | Evaluate JavaScript in a tab. |
109
+ | `browser_network` | Query captured traffic (HAR-style, DuckDB). |
110
+ | `browser_body` | Response body for a captured request. |
111
+ | `browser_request` | Request headers/postData for a captured request. |
112
+ | `browser_fetch` | In-page fetch (inherits auth/cookies). |
113
+ | `browser_click` | Click element (trusted dispatch). |
114
+ | `browser_type` | Type into element (trusted dispatch). |
115
+ | `browser_key` | Send key press (Enter, Escape, etc). |
116
+ | `browser_navigate` | Navigate tab to URL. |
117
+ | `browser_open` | Open new tab. |
118
+ | `browser_console` | Query console logs and exceptions. |
119
+ | `browser_screenshot` | Capture page screenshot. |
120
+ | `browser_cdp` | Raw CDP passthrough. |
121
+ | `browser_clear` | Reset captured network/console. |
122
+ | `browser_detach` | Remove watch pattern, detach tabs. |
123
+
124
+ Output from every cell spills to `$XDG_RUNTIME_DIR/repld/` — the inline response carries a head/tail preview plus the spill path. Use standard `Read`/`Grep` tools for full output.
125
+
126
+ ## Helpers
127
+
128
+ Available in the kernel namespace:
129
+
130
+ ```python
131
+ notify(content, **meta) # channel push to the agent
132
+ ask(prompt) # block on free-form human input
133
+ confirm(prompt) # block on yes/no
134
+ choose(prompt, options) # block on pick-one
135
+ defer(coro, label=None) # fire-and-forget, channel push on completion
136
+ @every(seconds) # periodic ticker, fn.cancel() to stop
137
+ ```
138
+
139
+ Browser builtins (when `repld[browser]` is installed):
140
+
141
+ ```python
142
+ tab = await browser.get("*example.com*") # find tab by URL glob
143
+ tab = await browser.open("https://...") # open new tab
144
+ await browser.watch("*pattern*") # auto-attach matching tabs
145
+
146
+ await tab.js("document.title") # eval JS
147
+ await tab.fetch("/api/data") # in-page fetch (inherits session)
148
+ await tab.click("#submit") # trusted click
149
+ await tab.type_text("#search", "query") # trusted typing
150
+ tab.network(url="*api*") # query captured traffic
151
+ ```
152
+
153
+ ## Gists
154
+
155
+ Gists are Python modules in `./gists/` (project) or `~/.repld/gists/` (global) that wrap anything into a callable API — web apps via the browser, databases, graph stores, embedding indexes, internal services.
156
+
157
+ ```python
158
+ # gists/myapp.py
159
+ """MyApp — accounts and transactions."""
160
+
161
+ class MyApp:
162
+ def __init__(self, tab): self._tab = tab
163
+
164
+ @classmethod
165
+ async def connect(cls):
166
+ from __main__ import browser
167
+ tab = await browser.get("*myapp.com*")
168
+ return cls(tab)
169
+
170
+ async def accounts(self):
171
+ return (await self._tab.fetch("/api/accounts"))["body"]
172
+ ```
173
+
174
+ ```python
175
+ import myapp
176
+ app = await myapp.MyApp.connect()
177
+ await app.accounts()
178
+ ```
179
+
180
+ Re-importing after edits auto-reloads. Gists can register MCP tools via `__repld_tools__` — scaffold with `repld gist <name>`. Run `repld help gists` for details.
181
+
182
+ ## Browser
183
+
184
+ `repld[browser]` attaches to Chrome via CDP (`--remote-debugging-port=9222`). You log in normally; the agent sees your traffic, discovers the API surface, and works with your authenticated sessions.
185
+
186
+ ```python
187
+ tab = await browser.get("*salesforce*")
188
+ reqs = tab.network(url="*/api/*") # discover API calls
189
+ auth = reqs[0].request_headers["Authorization"] # extract auth
190
+ ```
191
+
192
+ Body capture via Fetch interception means login flows, redirects, and CSRF exchanges are never lost. See [docs/browser.md](docs/browser.md) for the full design.
193
+
194
+ ## Scope
195
+
196
+ `repld` executes arbitrary Python in your project environment. It is a **dev-time tool** — never a runtime dependency. The IPC socket is localhost-only with user-only permissions.
197
+
198
+ Channels are a research-preview feature of Claude Code. The current integration uses `--dangerously-load-development-channels server:repld`.
199
+
200
+ ## License
201
+
202
+ [MIT](LICENSE)
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "repld-tool"
3
+ version = "0.0.1"
4
+ description = "Persistent Python runtime with MCP channel push. Dev shell and autonomous-agent substrate in one package."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Fredrik Angelsen", email = "fredrikangelsen@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Programming Language :: Python :: 3",
15
+ ]
16
+ keywords = ["repl", "mcp", "agent", "browser"]
17
+ dependencies = []
18
+
19
+ [project.scripts]
20
+ repld = "repld:main"
21
+
22
+ [project.optional-dependencies]
23
+ pretty = [
24
+ "rich>=15.0.0",
25
+ ]
26
+ browser = [
27
+ "duckdb>=1.5.2",
28
+ "websockets>=16.0",
29
+ ]
30
+
31
+ [tool.uv.build-backend]
32
+ module-name = "repld"
33
+
34
+ [build-system]
35
+ requires = ["uv_build>=0.11.6,<0.12.0"]
36
+ build-backend = "uv_build"
37
+
38
+ [tool.basedpyright]
39
+ typeCheckingMode = "standard"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ __all__ = ["main"]
@@ -0,0 +1,70 @@
1
+ """Stdio MCP ↔ unix-socket bridge.
2
+
3
+ Dumb bidirectional byte-pipe. Does not parse MCP. Reads the kernel's socket
4
+ path from ./.pyrepl.lock, connects, then:
5
+
6
+ stdin → socket (thread 1)
7
+ socket → stdout (thread 2)
8
+
9
+ Exits on EOF from either side. One bridge = one MCP client session.
10
+ """
11
+
12
+ import socket
13
+ import sys
14
+ import threading
15
+ from pathlib import Path
16
+
17
+ from .ipc import connect_to_kernel
18
+
19
+ LOCK_PATH = Path.cwd() / ".pyrepl.lock"
20
+
21
+
22
+ def _err(msg: str) -> None:
23
+ print(f"repld bridge: {msg}", file=sys.stderr, flush=True)
24
+
25
+
26
+ def run_bridge(argv: list[str]) -> int:
27
+ result = connect_to_kernel(LOCK_PATH)
28
+ if isinstance(result, str):
29
+ _err(result)
30
+ return 1
31
+ sock, _lock = result
32
+
33
+ stop = threading.Event()
34
+
35
+ def stdin_to_sock() -> None:
36
+ try:
37
+ for line in sys.stdin:
38
+ if not line.endswith("\n"):
39
+ line = line + "\n"
40
+ sock.sendall(line.encode("utf-8"))
41
+ except (BrokenPipeError, OSError):
42
+ stop.set()
43
+ finally:
44
+ # Half-close write side so the kernel sees EOF and drains/closes.
45
+ # DO NOT set stop here: in-flight responses may still be inbound.
46
+ try:
47
+ sock.shutdown(socket.SHUT_WR)
48
+ except OSError:
49
+ pass
50
+
51
+ def sock_to_stdout() -> None:
52
+ try:
53
+ rfile = sock.makefile("r", encoding="utf-8")
54
+ for line in rfile:
55
+ sys.stdout.write(line)
56
+ sys.stdout.flush()
57
+ except (BrokenPipeError, OSError):
58
+ pass
59
+ finally:
60
+ # Socket-side EOF drives shutdown.
61
+ stop.set()
62
+
63
+ threading.Thread(target=stdin_to_sock, daemon=True, name="bridge-stdin").start()
64
+ threading.Thread(target=sock_to_stdout, daemon=True, name="bridge-stdout").start()
65
+ stop.wait()
66
+ try:
67
+ sock.close()
68
+ except OSError:
69
+ pass
70
+ return 0