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.
- repld_tool-0.0.1/LICENSE +21 -0
- repld_tool-0.0.1/PKG-INFO +221 -0
- repld_tool-0.0.1/README.md +202 -0
- repld_tool-0.0.1/pyproject.toml +39 -0
- repld_tool-0.0.1/src/repld/__init__.py +3 -0
- repld_tool-0.0.1/src/repld/bridge.py +70 -0
- repld_tool-0.0.1/src/repld/browser/__init__.py +428 -0
- repld_tool-0.0.1/src/repld/browser/capture.py +206 -0
- repld_tool-0.0.1/src/repld/browser/cdp.py +269 -0
- repld_tool-0.0.1/src/repld/browser/har.py +558 -0
- repld_tool-0.0.1/src/repld/browser/observe.py +624 -0
- repld_tool-0.0.1/src/repld/browser/session.py +511 -0
- repld_tool-0.0.1/src/repld/browser/tab.py +1162 -0
- repld_tool-0.0.1/src/repld/cli.py +60 -0
- repld_tool-0.0.1/src/repld/display.py +408 -0
- repld_tool-0.0.1/src/repld/events.py +170 -0
- repld_tool-0.0.1/src/repld/exec_cmd.py +336 -0
- repld_tool-0.0.1/src/repld/gates.py +100 -0
- repld_tool-0.0.1/src/repld/gists.py +356 -0
- repld_tool-0.0.1/src/repld/help.py +529 -0
- repld_tool-0.0.1/src/repld/ipc.py +260 -0
- repld_tool-0.0.1/src/repld/kernel.py +689 -0
- repld_tool-0.0.1/src/repld/protocol.py +906 -0
- repld_tool-0.0.1/src/repld/runtime.py +131 -0
- repld_tool-0.0.1/src/repld/scaffold.py +193 -0
- repld_tool-0.0.1/src/repld/tasks.py +258 -0
repld_tool-0.0.1/LICENSE
ADDED
|
@@ -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,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
|