loom-harness 0.1.0__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 (82) hide show
  1. loom_harness-0.1.0/LICENSE +21 -0
  2. loom_harness-0.1.0/PKG-INFO +230 -0
  3. loom_harness-0.1.0/README.md +191 -0
  4. loom_harness-0.1.0/pyproject.toml +108 -0
  5. loom_harness-0.1.0/src/loom/__init__.py +5 -0
  6. loom_harness-0.1.0/src/loom/__main__.py +4 -0
  7. loom_harness-0.1.0/src/loom/auth.py +183 -0
  8. loom_harness-0.1.0/src/loom/batch.py +113 -0
  9. loom_harness-0.1.0/src/loom/browser.py +608 -0
  10. loom_harness-0.1.0/src/loom/canvas.py +163 -0
  11. loom_harness-0.1.0/src/loom/cli/__init__.py +250 -0
  12. loom_harness-0.1.0/src/loom/cli/auth.py +64 -0
  13. loom_harness-0.1.0/src/loom/cli/batch.py +45 -0
  14. loom_harness-0.1.0/src/loom/cli/browser.py +176 -0
  15. loom_harness-0.1.0/src/loom/cli/canvas.py +65 -0
  16. loom_harness-0.1.0/src/loom/cli/clipboard.py +39 -0
  17. loom_harness-0.1.0/src/loom/cli/contexts.py +76 -0
  18. loom_harness-0.1.0/src/loom/cli/cookies.py +105 -0
  19. loom_harness-0.1.0/src/loom/cli/daemon.py +167 -0
  20. loom_harness-0.1.0/src/loom/cli/dashboard.py +597 -0
  21. loom_harness-0.1.0/src/loom/cli/dialog.py +28 -0
  22. loom_harness-0.1.0/src/loom/cli/dom.py +396 -0
  23. loom_harness-0.1.0/src/loom/cli/download.py +24 -0
  24. loom_harness-0.1.0/src/loom/cli/eval.py +31 -0
  25. loom_harness-0.1.0/src/loom/cli/events.py +169 -0
  26. loom_harness-0.1.0/src/loom/cli/frames.py +94 -0
  27. loom_harness-0.1.0/src/loom/cli/input.py +220 -0
  28. loom_harness-0.1.0/src/loom/cli/network.py +270 -0
  29. loom_harness-0.1.0/src/loom/cli/observe.py +50 -0
  30. loom_harness-0.1.0/src/loom/cli/page.py +661 -0
  31. loom_harness-0.1.0/src/loom/cli/storage.py +216 -0
  32. loom_harness-0.1.0/src/loom/cli/workers.py +42 -0
  33. loom_harness-0.1.0/src/loom/client.py +315 -0
  34. loom_harness-0.1.0/src/loom/clipboard.py +68 -0
  35. loom_harness-0.1.0/src/loom/connection.py +214 -0
  36. loom_harness-0.1.0/src/loom/console.py +83 -0
  37. loom_harness-0.1.0/src/loom/contexts.py +143 -0
  38. loom_harness-0.1.0/src/loom/daemon/__init__.py +1 -0
  39. loom_harness-0.1.0/src/loom/daemon/__main__.py +44 -0
  40. loom_harness-0.1.0/src/loom/daemon/cache.py +303 -0
  41. loom_harness-0.1.0/src/loom/daemon/events.py +201 -0
  42. loom_harness-0.1.0/src/loom/daemon/pool.py +156 -0
  43. loom_harness-0.1.0/src/loom/daemon/server.py +671 -0
  44. loom_harness-0.1.0/src/loom/dialog.py +67 -0
  45. loom_harness-0.1.0/src/loom/dom.py +919 -0
  46. loom_harness-0.1.0/src/loom/download.py +42 -0
  47. loom_harness-0.1.0/src/loom/errors.py +137 -0
  48. loom_harness-0.1.0/src/loom/evaluator.py +55 -0
  49. loom_harness-0.1.0/src/loom/expect.py +87 -0
  50. loom_harness-0.1.0/src/loom/form.py +95 -0
  51. loom_harness-0.1.0/src/loom/frames.py +214 -0
  52. loom_harness-0.1.0/src/loom/idb.py +220 -0
  53. loom_harness-0.1.0/src/loom/input.py +366 -0
  54. loom_harness-0.1.0/src/loom/introspect.py +67 -0
  55. loom_harness-0.1.0/src/loom/launch_server.js +49 -0
  56. loom_harness-0.1.0/src/loom/locators.py +151 -0
  57. loom_harness-0.1.0/src/loom/mcp/__init__.py +1 -0
  58. loom_harness-0.1.0/src/loom/mcp/schema.py +128 -0
  59. loom_harness-0.1.0/src/loom/mcp/server.py +94 -0
  60. loom_harness-0.1.0/src/loom/metrics.py +99 -0
  61. loom_harness-0.1.0/src/loom/network/__init__.py +0 -0
  62. loom_harness-0.1.0/src/loom/network/kinds.py +73 -0
  63. loom_harness-0.1.0/src/loom/network/query.py +104 -0
  64. loom_harness-0.1.0/src/loom/network/routes.py +650 -0
  65. loom_harness-0.1.0/src/loom/network/sink.py +369 -0
  66. loom_harness-0.1.0/src/loom/network/waits.py +194 -0
  67. loom_harness-0.1.0/src/loom/observe.py +213 -0
  68. loom_harness-0.1.0/src/loom/output.py +70 -0
  69. loom_harness-0.1.0/src/loom/pages.py +729 -0
  70. loom_harness-0.1.0/src/loom/paths.py +57 -0
  71. loom_harness-0.1.0/src/loom/pattern.py +41 -0
  72. loom_harness-0.1.0/src/loom/py.typed +0 -0
  73. loom_harness-0.1.0/src/loom/recording.py +249 -0
  74. loom_harness-0.1.0/src/loom/rpc/__init__.py +53 -0
  75. loom_harness-0.1.0/src/loom/rpc/codec.py +236 -0
  76. loom_harness-0.1.0/src/loom/rpc/registry.py +573 -0
  77. loom_harness-0.1.0/src/loom/screenshot.py +88 -0
  78. loom_harness-0.1.0/src/loom/session.py +130 -0
  79. loom_harness-0.1.0/src/loom/snap.py +530 -0
  80. loom_harness-0.1.0/src/loom/state.py +258 -0
  81. loom_harness-0.1.0/src/loom/waits.py +70 -0
  82. loom_harness-0.1.0/src/loom/workers.py +105 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thomas Caron
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,230 @@
1
+ Metadata-Version: 2.4
2
+ Name: loom-harness
3
+ Version: 0.1.0
4
+ Summary: Stealth browser harness for autonomous LLM agents — Camoufox/Playwright wrapped behind a CLI, JSON-RPC daemon, and MCP server.
5
+ Keywords: browser,automation,playwright,camoufox,agent,llm,mcp,scraping,stealth
6
+ Author: Thomas Caron
7
+ Author-email: Thomas Caron <thomas.caron00@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Software Development :: Testing
20
+ Classifier: Typing :: Typed
21
+ Requires-Dist: playwright>=1.49
22
+ Requires-Dist: camoufox[geoip]>=0.4
23
+ Requires-Dist: pydantic>=2.9
24
+ Requires-Dist: pyyaml>=6.0
25
+ Requires-Dist: typer>=0.12
26
+ Requires-Dist: websockets>=12
27
+ Requires-Dist: anyio>=4
28
+ Requires-Dist: mcp>=1.0
29
+ Requires-Dist: httpx>=0.27
30
+ Requires-Dist: pillow>=10
31
+ Requires-Dist: orjson>=3.11.8
32
+ Requires-Dist: pytesseract>=0.3 ; extra == 'ocr'
33
+ Requires-Python: >=3.12
34
+ Project-URL: Homepage, https://github.com/tomtom103/loom
35
+ Project-URL: Repository, https://github.com/tomtom103/loom
36
+ Project-URL: Issues, https://github.com/tomtom103/loom/issues
37
+ Provides-Extra: ocr
38
+ Description-Content-Type: text/markdown
39
+
40
+ # Loom
41
+
42
+ **Stealth browser harness for autonomous LLM agents.**
43
+
44
+ Loom gives an agent a fast, observable handle on a real browser — open a tab,
45
+ click things, read the DOM, watch the network, run JS, replay APIs, persist
46
+ auth — exposed three ways:
47
+
48
+ - A CLI: `loom <namespace> <verb>` (JSONL output, scriptable, debuggable).
49
+ - A long-lived JSON-RPC daemon: `loomd` (Unix socket at `~/.loom/loomd.sock`,
50
+ auto-spawned on the first command).
51
+ - An MCP server: `loom mcp` (stdio transport, ~50 tools auto-derived from the
52
+ RPC registry).
53
+
54
+ All three share the same daemon, so a session opened from the CLI is drivable
55
+ from MCP and vice versa.
56
+
57
+ The default engine is **[Camoufox](https://github.com/daijro/camoufox)**, a
58
+ stealth-patched Firefox build with BrowserForge fingerprints. Chromium,
59
+ Firefox, and WebKit are also supported by passing `--engine`.
60
+
61
+ ## Why
62
+
63
+ Agent automation against real websites is a pile of small problems: bot
64
+ detection, flaky locators, capturing network traffic, persisting login state,
65
+ juggling multi-tab work, snapshotting state for the model to reason over.
66
+ Loom packages those into one fast, predictable harness so the agent can spend
67
+ its tokens on the actual task.
68
+
69
+ ## Status
70
+
71
+ Beta. ~625 unit tests, 81% line coverage. The public RPC surface is stable;
72
+ internal layout still moves between releases.
73
+
74
+ ## Install
75
+
76
+ ```bash
77
+ pip install loom-harness
78
+ playwright install firefox # for chromium/firefox/webkit engines
79
+ camoufox fetch # for the camoufox stealth engine
80
+ ```
81
+
82
+ On headless Linux, Loom can run any engine **headed** against a virtual
83
+ display for better stealth. Install Xvfb once:
84
+
85
+ ```bash
86
+ sudo apt-get install xvfb # Debian/Ubuntu
87
+ ```
88
+
89
+ Loom auto-detects Xvfb on `PATH` and uses it when `--display auto` is set
90
+ (the default).
91
+
92
+ ## Quickstart
93
+
94
+ ```bash
95
+ # Daemon starts itself the first time the CLI talks to it.
96
+ loom browser open --session s1 --display virtual --viewport 1280x720
97
+ loom page goto https://example.com -s s1
98
+ loom page snap -s s1 --max 30 | jq '.elements[0:5]'
99
+ loom dom click 'role:link[name="More information..."]' -s s1
100
+ loom page info -s s1
101
+ loom browser close -s s1
102
+ ```
103
+
104
+ `page snap` is the agent's primary "look" call — one payload combining a
105
+ screenshot, an element index, and stable, semantic locator strings (the same
106
+ grammar accepted by `dom.click`, `dom.type`, etc).
107
+
108
+ ## What's in the box
109
+
110
+ | Namespace | Highlights |
111
+ |-------------|------------|
112
+ | `browser` | open / close / list; multi-engine (camoufox \| chromium \| firefox \| webkit); persistent profiles; emulation knobs (viewport, locale, timezone, geolocation, color scheme, proxy, UA); Camoufox stealth knobs (OS fingerprint, geoip, humanize, WebRTC/WebGL blocks). |
113
+ | `page` | goto / reload / back / forward; new / close / switch / list / info; snap (annotated screenshot + element index); screenshot / pdf / metrics / console; wait-{selector, text, function, url, idle, request, response}; trace start/stop. |
114
+ | `dom` | click / type / press / hover / select / check / scroll / upload / text / html / query — all locator-grammar aware. |
115
+ | `frame` | list / info / switch / reset — every primitive after `frame switch` runs inside the iframe. Shadow DOM is pierced automatically by Playwright locators. |
116
+ | `network` | query / block / unblock / mock / unmock / offline / cache-clear / export / set-extra-headers / execute (replay a captured request via httpx with the live cookies). |
117
+ | `cookies`, `storage`, `storage idb`, `clipboard` | full CRUD on cookies, localStorage / sessionStorage, IndexedDB, and the page clipboard. |
118
+ | `auth` | interactive (block while operator logs in, snapshot `storage_state`); list / status / delete; replay via `--auth-profile` on subsequent sessions. |
119
+ | `context` | list / new / switch / close — multiple isolated cookie/storage worlds inside one browser. |
120
+ | `mouse`, `keyboard` | coordinate-based mouse + per-key keyboard control. |
121
+ | `dialog`, `download`, `eval` | handle native dialogs, wait for downloads, evaluate JS in the page. |
122
+ | `worker`, `coverage` | service / dedicated worker control; JS+CSS coverage (chromium only). |
123
+ | `observe`, `batch` | snap-act-snap-diff helper; multi-step RPC chains in one daemon dispatch. |
124
+ | `daemon` | start / stop / status. |
125
+
126
+ Full agent-facing reference: [`AGENTS.md`](./AGENTS.md). A drop-in skill for
127
+ Claude Code lives at [`.claude/skills/loom-browser-harness/SKILL.md`](./.claude/skills/loom-browser-harness/SKILL.md).
128
+
129
+ ## Live dashboard
130
+
131
+ ```bash
132
+ loom dashboard [-s SESSION] [--port 9876]
133
+ ```
134
+
135
+ Spins up a single-page web viewer at `http://127.0.0.1:9876/` showing a
136
+ live screenshot of the active page plus a scrolling timeline of every
137
+ RPC the daemon dispatches. Useful for demos and for watching long
138
+ autonomous runs from the browser instead of the CLI.
139
+
140
+ The same data is also available as a raw stream:
141
+
142
+ ```bash
143
+ loom events tail [-s SESSION] [--frames] [--save DIR]
144
+ ```
145
+
146
+ `--frames` opts in to base64 JPEG captures (~5fps per session, gated on
147
+ subscriber demand). `--save DIR` drops each frame to disk for replay.
148
+
149
+ ## MCP
150
+
151
+ ```bash
152
+ loom mcp # stdio transport — point any MCP client at this command
153
+ ```
154
+
155
+ Each RPC method is auto-exposed as one MCP tool with a JSON Schema derived
156
+ from its Python type hints. State is shared with the CLI through the daemon,
157
+ so an agent can mix MCP tool calls with shell `loom …` invocations freely.
158
+
159
+ ## Layout
160
+
161
+ ```
162
+ src/loom/
163
+ browser.py browser.open / close / list, engine launching
164
+ pages.py page.* primitives
165
+ dom.py dom.* primitives
166
+ snap.py page.snap — annotated screenshot + element index
167
+ locators.py semantic locator grammar (testid:, role:, text:, …)
168
+ frames.py frame.* primitives
169
+ network/ capture sink, route mocking, replay, waits
170
+ auth.py auth profile lifecycle
171
+ contexts.py multi-context per session
172
+ observe.py snap-act-snap-diff helper
173
+ batch.py multi-step RPC chains
174
+ daemon/ loomd: server, per-session worker pool, connection cache
175
+ rpc/ JSON-RPC codec + method registry
176
+ mcp/ stdio MCP server, schema generator
177
+ cli/ Typer entry points (one module per namespace)
178
+ client.py CLI ↔ daemon Unix-socket client (auto-spawn)
179
+ session.py session file CRUD (fcntl-locked updates)
180
+ paths.py ~/.loom layout (LOOM_HOME override)
181
+ errors.py structured error codes
182
+ ```
183
+
184
+ ## Development
185
+
186
+ ```bash
187
+ git clone https://github.com/tomtom103/loom
188
+ cd loom
189
+ make install # uv sync
190
+ make lint # ruff check
191
+ make type-check # mypy --strict
192
+ make test # pytest (mocked, fast)
193
+ make test-all # add the integration tests (real browsers)
194
+ ```
195
+
196
+ Requirements: Python 3.12+, [uv](https://docs.astral.sh/uv/) for the dev
197
+ setup, and one of the supported browser engines installed.
198
+
199
+ ## Files Loom owns
200
+
201
+ ```
202
+ ~/.loom/loomd.sock daemon Unix socket
203
+ ~/.loom/loomd.pid daemon pidfile (flock-guarded)
204
+ ~/.loom/loomd.log daemon log
205
+ ~/.loom/sessions/<name>.json session state (engine, ws_url, tabs, contexts, ...)
206
+ ~/.loom/captures/<name>/ per-session network + console capture
207
+ ~/.loom/downloads/<name>/ files saved via the page's download flow
208
+ ~/.loom/profiles/<name>/ saved auth profiles (storage_state.json)
209
+ ```
210
+
211
+ Override the root with `LOOM_HOME`.
212
+
213
+ ## Opting out of the daemon
214
+
215
+ ```bash
216
+ loom --no-daemon page goto https://example.com # in-process
217
+ LOOM_NO_DAEMON=1 loom page goto https://example.com # same, env-scoped
218
+ ```
219
+
220
+ Useful for tests, debugging, or scripts that don't want a daemon on disk.
221
+ You lose persistence between calls — the browser disconnects when the CLI
222
+ returns — but every primitive still works.
223
+
224
+ ## Releases
225
+
226
+ See [CHANGELOG.md](./CHANGELOG.md) for the history of public changes.
227
+
228
+ ## License
229
+
230
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,191 @@
1
+ # Loom
2
+
3
+ **Stealth browser harness for autonomous LLM agents.**
4
+
5
+ Loom gives an agent a fast, observable handle on a real browser — open a tab,
6
+ click things, read the DOM, watch the network, run JS, replay APIs, persist
7
+ auth — exposed three ways:
8
+
9
+ - A CLI: `loom <namespace> <verb>` (JSONL output, scriptable, debuggable).
10
+ - A long-lived JSON-RPC daemon: `loomd` (Unix socket at `~/.loom/loomd.sock`,
11
+ auto-spawned on the first command).
12
+ - An MCP server: `loom mcp` (stdio transport, ~50 tools auto-derived from the
13
+ RPC registry).
14
+
15
+ All three share the same daemon, so a session opened from the CLI is drivable
16
+ from MCP and vice versa.
17
+
18
+ The default engine is **[Camoufox](https://github.com/daijro/camoufox)**, a
19
+ stealth-patched Firefox build with BrowserForge fingerprints. Chromium,
20
+ Firefox, and WebKit are also supported by passing `--engine`.
21
+
22
+ ## Why
23
+
24
+ Agent automation against real websites is a pile of small problems: bot
25
+ detection, flaky locators, capturing network traffic, persisting login state,
26
+ juggling multi-tab work, snapshotting state for the model to reason over.
27
+ Loom packages those into one fast, predictable harness so the agent can spend
28
+ its tokens on the actual task.
29
+
30
+ ## Status
31
+
32
+ Beta. ~625 unit tests, 81% line coverage. The public RPC surface is stable;
33
+ internal layout still moves between releases.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install loom-harness
39
+ playwright install firefox # for chromium/firefox/webkit engines
40
+ camoufox fetch # for the camoufox stealth engine
41
+ ```
42
+
43
+ On headless Linux, Loom can run any engine **headed** against a virtual
44
+ display for better stealth. Install Xvfb once:
45
+
46
+ ```bash
47
+ sudo apt-get install xvfb # Debian/Ubuntu
48
+ ```
49
+
50
+ Loom auto-detects Xvfb on `PATH` and uses it when `--display auto` is set
51
+ (the default).
52
+
53
+ ## Quickstart
54
+
55
+ ```bash
56
+ # Daemon starts itself the first time the CLI talks to it.
57
+ loom browser open --session s1 --display virtual --viewport 1280x720
58
+ loom page goto https://example.com -s s1
59
+ loom page snap -s s1 --max 30 | jq '.elements[0:5]'
60
+ loom dom click 'role:link[name="More information..."]' -s s1
61
+ loom page info -s s1
62
+ loom browser close -s s1
63
+ ```
64
+
65
+ `page snap` is the agent's primary "look" call — one payload combining a
66
+ screenshot, an element index, and stable, semantic locator strings (the same
67
+ grammar accepted by `dom.click`, `dom.type`, etc).
68
+
69
+ ## What's in the box
70
+
71
+ | Namespace | Highlights |
72
+ |-------------|------------|
73
+ | `browser` | open / close / list; multi-engine (camoufox \| chromium \| firefox \| webkit); persistent profiles; emulation knobs (viewport, locale, timezone, geolocation, color scheme, proxy, UA); Camoufox stealth knobs (OS fingerprint, geoip, humanize, WebRTC/WebGL blocks). |
74
+ | `page` | goto / reload / back / forward; new / close / switch / list / info; snap (annotated screenshot + element index); screenshot / pdf / metrics / console; wait-{selector, text, function, url, idle, request, response}; trace start/stop. |
75
+ | `dom` | click / type / press / hover / select / check / scroll / upload / text / html / query — all locator-grammar aware. |
76
+ | `frame` | list / info / switch / reset — every primitive after `frame switch` runs inside the iframe. Shadow DOM is pierced automatically by Playwright locators. |
77
+ | `network` | query / block / unblock / mock / unmock / offline / cache-clear / export / set-extra-headers / execute (replay a captured request via httpx with the live cookies). |
78
+ | `cookies`, `storage`, `storage idb`, `clipboard` | full CRUD on cookies, localStorage / sessionStorage, IndexedDB, and the page clipboard. |
79
+ | `auth` | interactive (block while operator logs in, snapshot `storage_state`); list / status / delete; replay via `--auth-profile` on subsequent sessions. |
80
+ | `context` | list / new / switch / close — multiple isolated cookie/storage worlds inside one browser. |
81
+ | `mouse`, `keyboard` | coordinate-based mouse + per-key keyboard control. |
82
+ | `dialog`, `download`, `eval` | handle native dialogs, wait for downloads, evaluate JS in the page. |
83
+ | `worker`, `coverage` | service / dedicated worker control; JS+CSS coverage (chromium only). |
84
+ | `observe`, `batch` | snap-act-snap-diff helper; multi-step RPC chains in one daemon dispatch. |
85
+ | `daemon` | start / stop / status. |
86
+
87
+ Full agent-facing reference: [`AGENTS.md`](./AGENTS.md). A drop-in skill for
88
+ Claude Code lives at [`.claude/skills/loom-browser-harness/SKILL.md`](./.claude/skills/loom-browser-harness/SKILL.md).
89
+
90
+ ## Live dashboard
91
+
92
+ ```bash
93
+ loom dashboard [-s SESSION] [--port 9876]
94
+ ```
95
+
96
+ Spins up a single-page web viewer at `http://127.0.0.1:9876/` showing a
97
+ live screenshot of the active page plus a scrolling timeline of every
98
+ RPC the daemon dispatches. Useful for demos and for watching long
99
+ autonomous runs from the browser instead of the CLI.
100
+
101
+ The same data is also available as a raw stream:
102
+
103
+ ```bash
104
+ loom events tail [-s SESSION] [--frames] [--save DIR]
105
+ ```
106
+
107
+ `--frames` opts in to base64 JPEG captures (~5fps per session, gated on
108
+ subscriber demand). `--save DIR` drops each frame to disk for replay.
109
+
110
+ ## MCP
111
+
112
+ ```bash
113
+ loom mcp # stdio transport — point any MCP client at this command
114
+ ```
115
+
116
+ Each RPC method is auto-exposed as one MCP tool with a JSON Schema derived
117
+ from its Python type hints. State is shared with the CLI through the daemon,
118
+ so an agent can mix MCP tool calls with shell `loom …` invocations freely.
119
+
120
+ ## Layout
121
+
122
+ ```
123
+ src/loom/
124
+ browser.py browser.open / close / list, engine launching
125
+ pages.py page.* primitives
126
+ dom.py dom.* primitives
127
+ snap.py page.snap — annotated screenshot + element index
128
+ locators.py semantic locator grammar (testid:, role:, text:, …)
129
+ frames.py frame.* primitives
130
+ network/ capture sink, route mocking, replay, waits
131
+ auth.py auth profile lifecycle
132
+ contexts.py multi-context per session
133
+ observe.py snap-act-snap-diff helper
134
+ batch.py multi-step RPC chains
135
+ daemon/ loomd: server, per-session worker pool, connection cache
136
+ rpc/ JSON-RPC codec + method registry
137
+ mcp/ stdio MCP server, schema generator
138
+ cli/ Typer entry points (one module per namespace)
139
+ client.py CLI ↔ daemon Unix-socket client (auto-spawn)
140
+ session.py session file CRUD (fcntl-locked updates)
141
+ paths.py ~/.loom layout (LOOM_HOME override)
142
+ errors.py structured error codes
143
+ ```
144
+
145
+ ## Development
146
+
147
+ ```bash
148
+ git clone https://github.com/tomtom103/loom
149
+ cd loom
150
+ make install # uv sync
151
+ make lint # ruff check
152
+ make type-check # mypy --strict
153
+ make test # pytest (mocked, fast)
154
+ make test-all # add the integration tests (real browsers)
155
+ ```
156
+
157
+ Requirements: Python 3.12+, [uv](https://docs.astral.sh/uv/) for the dev
158
+ setup, and one of the supported browser engines installed.
159
+
160
+ ## Files Loom owns
161
+
162
+ ```
163
+ ~/.loom/loomd.sock daemon Unix socket
164
+ ~/.loom/loomd.pid daemon pidfile (flock-guarded)
165
+ ~/.loom/loomd.log daemon log
166
+ ~/.loom/sessions/<name>.json session state (engine, ws_url, tabs, contexts, ...)
167
+ ~/.loom/captures/<name>/ per-session network + console capture
168
+ ~/.loom/downloads/<name>/ files saved via the page's download flow
169
+ ~/.loom/profiles/<name>/ saved auth profiles (storage_state.json)
170
+ ```
171
+
172
+ Override the root with `LOOM_HOME`.
173
+
174
+ ## Opting out of the daemon
175
+
176
+ ```bash
177
+ loom --no-daemon page goto https://example.com # in-process
178
+ LOOM_NO_DAEMON=1 loom page goto https://example.com # same, env-scoped
179
+ ```
180
+
181
+ Useful for tests, debugging, or scripts that don't want a daemon on disk.
182
+ You lose persistence between calls — the browser disconnects when the CLI
183
+ returns — but every primitive still works.
184
+
185
+ ## Releases
186
+
187
+ See [CHANGELOG.md](./CHANGELOG.md) for the history of public changes.
188
+
189
+ ## License
190
+
191
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,108 @@
1
+ [project]
2
+ name = "loom-harness"
3
+ version = "0.1.0"
4
+ description = "Stealth browser harness for autonomous LLM agents — Camoufox/Playwright wrapped behind a CLI, JSON-RPC daemon, and MCP server."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "Thomas Caron", email = "thomas.caron00@gmail.com" }
10
+ ]
11
+ requires-python = ">=3.12"
12
+ keywords = [
13
+ "browser",
14
+ "automation",
15
+ "playwright",
16
+ "camoufox",
17
+ "agent",
18
+ "llm",
19
+ "mcp",
20
+ "scraping",
21
+ "stealth",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Intended Audience :: Developers",
26
+ "Operating System :: POSIX :: Linux",
27
+ "Operating System :: MacOS",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3 :: Only",
31
+ "Topic :: Internet :: WWW/HTTP :: Browsers",
32
+ "Topic :: Software Development :: Libraries :: Python Modules",
33
+ "Topic :: Software Development :: Testing",
34
+ "Typing :: Typed",
35
+ ]
36
+ dependencies = [
37
+ "playwright>=1.49",
38
+ "camoufox[geoip]>=0.4",
39
+ "pydantic>=2.9",
40
+ "pyyaml>=6.0",
41
+ "typer>=0.12",
42
+ "websockets>=12",
43
+ "anyio>=4",
44
+ "mcp>=1.0",
45
+ "httpx>=0.27",
46
+ "pillow>=10",
47
+ "orjson>=3.11.8",
48
+ ]
49
+
50
+ [project.urls]
51
+ Homepage = "https://github.com/tomtom103/loom"
52
+ Repository = "https://github.com/tomtom103/loom"
53
+ Issues = "https://github.com/tomtom103/loom/issues"
54
+
55
+ [project.scripts]
56
+ loom = "loom.cli:main"
57
+
58
+ [project.optional-dependencies]
59
+ ocr = ["pytesseract>=0.3"]
60
+
61
+ [dependency-groups]
62
+ dev = [
63
+ "pytest>=8",
64
+ "ruff>=0.11",
65
+ "mypy>=1.15",
66
+ "types-pyyaml>=6.0",
67
+ "pytest-asyncio>=0.24",
68
+ "pytest-cov>=5",
69
+ ]
70
+
71
+ [build-system]
72
+ requires = ["uv_build>=0.10.10,<0.11.0"]
73
+ build-backend = "uv_build"
74
+
75
+ [tool.uv.build-backend]
76
+ module-name = "loom"
77
+ module-root = "src"
78
+
79
+ [tool.ruff]
80
+ target-version = "py312"
81
+ line-length = 100
82
+
83
+ [tool.ruff.lint]
84
+ select = ["E", "F", "I", "UP", "B", "SIM"]
85
+
86
+ [tool.ruff.lint.per-file-ignores]
87
+ # Inline HTML/CSS/JS for the dashboard ships as a single string — line-length
88
+ # wrapping would corrupt the rendered output, so allow long lines here.
89
+ "src/loom/cli/dashboard.py" = ["E501"]
90
+ "tests/test_dashboard.py" = ["E501"]
91
+
92
+ [tool.ruff.lint.flake8-bugbear]
93
+ extend-immutable-calls = ["typer.Option", "typer.Argument"]
94
+
95
+ [tool.mypy]
96
+ python_version = "3.12"
97
+ strict = true
98
+
99
+ [[tool.mypy.overrides]]
100
+ module = ["pytesseract"]
101
+ ignore_missing_imports = true
102
+
103
+ [tool.pytest.ini_options]
104
+ testpaths = ["tests"]
105
+ asyncio_mode = "auto"
106
+ markers = [
107
+ "integration: tests that launch a real browser (slow; opt-in).",
108
+ ]
@@ -0,0 +1,5 @@
1
+ """Loom — browser automation toolkit for agents and scheduled scrapers."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,4 @@
1
+ from loom.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()