spagents 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 (43) hide show
  1. spagents-0.1.0/.github/workflows/publish.yml +27 -0
  2. spagents-0.1.0/.gitignore +10 -0
  3. spagents-0.1.0/.python-version +1 -0
  4. spagents-0.1.0/LICENSE.md +63 -0
  5. spagents-0.1.0/PKG-INFO +316 -0
  6. spagents-0.1.0/README.md +233 -0
  7. spagents-0.1.0/pyproject.toml +38 -0
  8. spagents-0.1.0/src/spagents/__init__.py +23 -0
  9. spagents-0.1.0/src/spagents/actions/__init__.py +0 -0
  10. spagents-0.1.0/src/spagents/actions/discovery.py +40 -0
  11. spagents-0.1.0/src/spagents/browser/__init__.py +0 -0
  12. spagents-0.1.0/src/spagents/browser/manager.py +61 -0
  13. spagents-0.1.0/src/spagents/browser/page.py +112 -0
  14. spagents-0.1.0/src/spagents/browser/session.py +73 -0
  15. spagents-0.1.0/src/spagents/cli/__init__.py +0 -0
  16. spagents-0.1.0/src/spagents/cli/main.py +326 -0
  17. spagents-0.1.0/src/spagents/detection/__init__.py +0 -0
  18. spagents-0.1.0/src/spagents/detection/ready.py +167 -0
  19. spagents-0.1.0/src/spagents/extraction/__init__.py +0 -0
  20. spagents-0.1.0/src/spagents/extraction/extractor.py +86 -0
  21. spagents-0.1.0/src/spagents/extraction/models.py +102 -0
  22. spagents-0.1.0/src/spagents/js/__init__.py +26 -0
  23. spagents-0.1.0/src/spagents/js/content_heuristic.js +41 -0
  24. spagents-0.1.0/src/spagents/js/discover_actions.js +305 -0
  25. spagents-0.1.0/src/spagents/js/extract_articles.js +210 -0
  26. spagents-0.1.0/src/spagents/js/extract_links.js +40 -0
  27. spagents-0.1.0/src/spagents/js/extract_main_text.js +6 -0
  28. spagents-0.1.0/src/spagents/js/extract_metadata.js +18 -0
  29. spagents-0.1.0/src/spagents/js/mutation_observer.js +20 -0
  30. spagents-0.1.0/src/spagents/mcp/__init__.py +0 -0
  31. spagents-0.1.0/src/spagents/mcp/server.py +236 -0
  32. spagents-0.1.0/src/spagents/py.typed +0 -0
  33. spagents-0.1.0/tests/__init__.py +0 -0
  34. spagents-0.1.0/tests/conftest.py +72 -0
  35. spagents-0.1.0/tests/test_actions.py +99 -0
  36. spagents-0.1.0/tests/test_detection.py +30 -0
  37. spagents-0.1.0/tests/test_extraction.py +81 -0
  38. spagents-0.1.0/tests/test_js_loader.py +45 -0
  39. spagents-0.1.0/tests/test_models.py +121 -0
  40. spagents-0.1.0/tests/test_page.py +117 -0
  41. spagents-0.1.0/tests/test_session.py +63 -0
  42. spagents-0.1.0/tests/test_spa/index.html +315 -0
  43. spagents-0.1.0/uv.lock +2025 -0
@@ -0,0 +1,27 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ jobs:
7
+ publish:
8
+ runs-on: ubuntu-latest
9
+ if: github.ref == 'refs/heads/main'
10
+ permissions:
11
+ id-token: write
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@v6
17
+
18
+ - name: Set up Python
19
+ uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.12"
22
+
23
+ - name: Build package
24
+ run: uv build
25
+
26
+ - name: Publish to PyPI
27
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.10
@@ -0,0 +1,63 @@
1
+ # spagents License
2
+
3
+ ## MIT License (Amended)
4
+
5
+ Copyright (c) 2026 Moses Wynn
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ ### Additional Condition: Community Contribution Requirement
18
+
19
+ Organizations with **more than 100 employees** that use this Software in
20
+ any capacity (internal tools, products, services, or development workflows)
21
+ must make a one-time donation of **$1.00 USD per employee** to one or more
22
+ of the following organizations:
23
+
24
+ - **ACLU (American Civil Liberties Union)** — [donate.aclu.org](https://donate.aclu.org/)
25
+ - **SPLC (Southern Poverty Law Center)** — [donate.splcenter.org](https://donate.splcenter.org/sslpage.aspx?pid=463)
26
+ - **DSA (Democratic Socialists of America)** — [dsausa.org/donate](https://www.dsausa.org/donate/)
27
+ - **Appalachian OUTReach** — [appalachianoutreach.org/donate](https://www.appalachianoutreach.org/donate)
28
+
29
+ The donation may be split across multiple organizations.
30
+
31
+ Upon making the donation, the organization must submit a **pull request** to
32
+ this repository adding their name to the [Approved Organizations](#approved-organizations)
33
+ section below, including proof of donation (receipt, confirmation number, or
34
+ screenshot with sensitive information redacted).
35
+
36
+ Organizations that believe they should be **exempt** from this requirement
37
+ (e.g., nonprofits, educational institutions, organizations with fewer
38
+ resources than their headcount suggests) may submit a pull request adding
39
+ their name to the Approved Organizations section with a written explanation
40
+ of why they believe an exemption is warranted. Exemptions are granted at the
41
+ sole discretion of the copyright holder.
42
+
43
+ **Use of this Software by qualifying organizations without compliance
44
+ constitutes a violation of this license.**
45
+
46
+ ### Approved Organizations
47
+
48
+ <!-- Add your organization below via pull request -->
49
+ <!-- Format: | Organization Name | Date | Donation or Exemption | -->
50
+
51
+ | Organization | Date | Status |
52
+ |---|---|---|
53
+ | | | |
54
+
55
+ ---
56
+
57
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
58
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
59
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
60
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
61
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
62
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
63
+ SOFTWARE.
@@ -0,0 +1,316 @@
1
+ Metadata-Version: 2.4
2
+ Name: spagents
3
+ Version: 0.1.0
4
+ Summary: SPA-aware browsing library for AI agents
5
+ Author-email: Moses Wynn <accounts@moseswynn.com>
6
+ License: # spagents License
7
+
8
+ ## MIT License (Amended)
9
+
10
+ Copyright (c) 2026 Moses Wynn
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ ### Additional Condition: Community Contribution Requirement
23
+
24
+ Organizations with **more than 100 employees** that use this Software in
25
+ any capacity (internal tools, products, services, or development workflows)
26
+ must make a one-time donation of **$1.00 USD per employee** to one or more
27
+ of the following organizations:
28
+
29
+ - **ACLU (American Civil Liberties Union)** — [donate.aclu.org](https://donate.aclu.org/)
30
+ - **SPLC (Southern Poverty Law Center)** — [donate.splcenter.org](https://donate.splcenter.org/sslpage.aspx?pid=463)
31
+ - **DSA (Democratic Socialists of America)** — [dsausa.org/donate](https://www.dsausa.org/donate/)
32
+ - **Appalachian OUTReach** — [appalachianoutreach.org/donate](https://www.appalachianoutreach.org/donate)
33
+
34
+ The donation may be split across multiple organizations.
35
+
36
+ Upon making the donation, the organization must submit a **pull request** to
37
+ this repository adding their name to the [Approved Organizations](#approved-organizations)
38
+ section below, including proof of donation (receipt, confirmation number, or
39
+ screenshot with sensitive information redacted).
40
+
41
+ Organizations that believe they should be **exempt** from this requirement
42
+ (e.g., nonprofits, educational institutions, organizations with fewer
43
+ resources than their headcount suggests) may submit a pull request adding
44
+ their name to the Approved Organizations section with a written explanation
45
+ of why they believe an exemption is warranted. Exemptions are granted at the
46
+ sole discretion of the copyright holder.
47
+
48
+ **Use of this Software by qualifying organizations without compliance
49
+ constitutes a violation of this license.**
50
+
51
+ ### Approved Organizations
52
+
53
+ <!-- Add your organization below via pull request -->
54
+ <!-- Format: | Organization Name | Date | Donation or Exemption | -->
55
+
56
+ | Organization | Date | Status |
57
+ |---|---|---|
58
+ | | | |
59
+
60
+ ---
61
+
62
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
63
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
64
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
65
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
66
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
67
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
68
+ SOFTWARE.
69
+ License-File: LICENSE.md
70
+ Requires-Python: >=3.10
71
+ Requires-Dist: beautifulsoup4>=4.12
72
+ Requires-Dist: fastmcp>=2.0
73
+ Requires-Dist: lxml>=5.0
74
+ Requires-Dist: playwright>=1.40
75
+ Requires-Dist: pydantic>=2.0
76
+ Requires-Dist: typer>=0.9
77
+ Provides-Extra: dev
78
+ Requires-Dist: mypy>=1.10; extra == 'dev'
79
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
80
+ Requires-Dist: pytest>=8.0; extra == 'dev'
81
+ Requires-Dist: ruff>=0.4; extra == 'dev'
82
+ Description-Content-Type: text/markdown
83
+
84
+ <p align="center">
85
+ <h1 align="center">spagents</h1>
86
+ <p align="center">
87
+ <strong>Give your AI agents eyes for the modern web.</strong>
88
+ </p>
89
+ <p align="center">
90
+ <a href="#installation">Installation</a> &middot;
91
+ <a href="#quick-start">Quick Start</a> &middot;
92
+ <a href="#mcp-server">MCP Server</a> &middot;
93
+ <a href="#python-sdk">Python SDK</a> &middot;
94
+ <a href="#how-it-works">How It Works</a>
95
+ </p>
96
+ </p>
97
+
98
+ ---
99
+
100
+ **AI agents are blind to Single Page Applications.** Tools like `fetch` and `requests` return empty HTML shells — no articles, no content, no interactive elements. SPAs render everything via JavaScript *after* the page loads.
101
+
102
+ **spagents** fixes this. It launches a real browser, intelligently waits for SPAs to finish rendering, and returns structured, agent-friendly data — articles, actions, inputs, navigation — ready for your agent to use.
103
+
104
+ ### The problem
105
+
106
+ ```
107
+ # What fetch/requests sees on a SPA:
108
+ <div id="app"></div>
109
+ <script src="bundle.js"></script>
110
+ ```
111
+
112
+ ### The spagents solution
113
+
114
+ ```bash
115
+ $ spagents browse "https://news.kagi.com" --format text
116
+
117
+ Title: World | Kagi News
118
+ Content Ready: True
119
+
120
+ === 12 Articles ===
121
+ MIDDLE EAST: Update: Iran rejects US truce plan, sets terms
122
+ TECH ACCOUNTABILITY: US jury holds Meta, YouTube liable in addiction case
123
+ ARCHAEOLOGY: Possible d'Artagnan remains found beneath Maastricht church
124
+ ...
125
+
126
+ === 45 Actions ===
127
+ [1] (navigate) Listitem: Technology
128
+ [2] (click) Button: Expand story (Iran rejects US truce plan...)
129
+ [3] (input) Input: Search
130
+ ...
131
+ ```
132
+
133
+ ### Key features
134
+
135
+ - **Smart content detection** — Knows when a SPA is done rendering (not just `sleep(5)`)
136
+ - **Structured extraction** — Returns articles, links, metadata as typed Pydantic models
137
+ - **Full interaction** — Click, type, scroll, press keys, navigate — like a real user
138
+ - **Action discovery** — Finds every interactive element: buttons, inputs, ARIA roles, custom components
139
+ - **Session persistence** — Cookies, localStorage, and auth state preserved across navigations
140
+ - **Three interfaces** — Python SDK, CLI, and MCP server for Claude Desktop / AI agents
141
+
142
+ ---
143
+
144
+ ## Installation
145
+
146
+ ```bash
147
+ pip install spagents
148
+ playwright install chromium
149
+ ```
150
+
151
+ ### From source
152
+
153
+ ```bash
154
+ git clone https://github.com/your-username/spagents.git
155
+ cd spagents
156
+ uv sync
157
+ uv run playwright install chromium
158
+ ```
159
+
160
+ ## Quick start
161
+
162
+ ### CLI
163
+
164
+ ```bash
165
+ # Structured JSON output (default)
166
+ spagents browse "https://news.kagi.com"
167
+
168
+ # Human-readable text
169
+ spagents browse "https://news.kagi.com" --format text
170
+
171
+ # Interactive REPL session
172
+ spagents interactive "https://news.kagi.com"
173
+ ```
174
+
175
+ ### Python SDK
176
+
177
+ ```python
178
+ import asyncio
179
+ from spagents import BrowserManager
180
+
181
+ async def main():
182
+ async with BrowserManager() as browser:
183
+ session = await browser.new_session()
184
+
185
+ # Browse a SPA — content is fully rendered
186
+ state = await session.navigate("https://news.kagi.com")
187
+ for article in state.content.articles:
188
+ print(f"{article.category}: {article.headline}")
189
+
190
+ # Interact with the page
191
+ for action in state.actions:
192
+ if "Technology" in action.description:
193
+ state = await session.click(action.selector)
194
+ break
195
+
196
+ # Type into inputs, press keys
197
+ state = await session.type_text("#search", "climate change")
198
+ state = await session.press_key("Enter")
199
+
200
+ await session.close()
201
+
202
+ asyncio.run(main())
203
+ ```
204
+
205
+ ### MCP Server
206
+
207
+ Connect spagents to Claude Desktop or any MCP-compatible AI agent:
208
+
209
+ ```bash
210
+ spagents mcp
211
+ ```
212
+
213
+ Add to your Claude Desktop config (`claude_desktop_config.json`):
214
+
215
+ ```json
216
+ {
217
+ "mcpServers": {
218
+ "spagents": {
219
+ "command": "spagents",
220
+ "args": ["mcp"]
221
+ }
222
+ }
223
+ }
224
+ ```
225
+
226
+ Now Claude can browse any SPA:
227
+
228
+ > **You:** "What's the top story on Kagi News today?"
229
+ >
230
+ > **Claude:** *uses `browse` tool* → *reads structured articles* → gives you the answer
231
+
232
+ #### MCP tools
233
+
234
+ | Tool | Description |
235
+ |---|---|
236
+ | `browse` | Navigate to a URL, return rendered content + interactive actions |
237
+ | `click` | Click an element by CSS selector |
238
+ | `type_text` | Type into an input field |
239
+ | `press_key` | Press a keyboard key (Enter, Tab, Escape, etc.) |
240
+ | `list_actions` | Discover all interactive elements on the page |
241
+ | `navigate` | Go to a new URL within an existing session |
242
+ | `scroll` | Scroll up or down, trigger infinite scroll |
243
+ | `extract_content` | Re-extract content from the current page |
244
+ | `close_session` | Close a browser session and free resources |
245
+
246
+ ## Interactive REPL commands
247
+
248
+ ```
249
+ click <n> Click action by number
250
+ click <selector> Click by CSS selector
251
+ type <n> <text> Type text into an input by action number
252
+ type "<selector>" <text> Type text into an input by CSS selector
253
+ press <key> Press a key (Enter, Escape, Tab, ArrowDown, etc.)
254
+ select <n> <value> Select dropdown option by action number
255
+ scroll [down|up] Scroll the page
256
+ actions List all interactive elements
257
+ extract Re-extract page content
258
+ navigate <url> Navigate to a new URL
259
+ json Dump current state as JSON
260
+ quit Exit the session
261
+ ```
262
+
263
+ ## How it works
264
+
265
+ spagents wraps [Playwright](https://playwright.dev/python/) and adds three intelligent layers:
266
+
267
+ ### 1. Content ready detection
268
+
269
+ A multi-signal detector that knows when a SPA has *actually* finished rendering:
270
+
271
+ | Signal | What it checks |
272
+ |---|---|
273
+ | **Network quiescence** | No pending XHR/fetch requests for 500ms (ignoring analytics noise) |
274
+ | **DOM stabilization** | MutationObserver sees no changes for 300ms after initial render |
275
+ | **Content heuristic** | Meaningful text exists, no loading spinners, real links present |
276
+
277
+ This replaces naive approaches like `sleep(5)` or Playwright's `networkidle` (which breaks on long-polling and WebSocket connections).
278
+
279
+ ### 2. Content extraction
280
+
281
+ Extracts structured data from the rendered DOM:
282
+
283
+ - **Articles** with headlines, summaries, sources, highlights, quotes, and sections
284
+ - **Links** with surrounding context (which heading or section they're under)
285
+ - **Metadata** from OG tags, meta descriptions, and page title
286
+
287
+ ### 3. Action discovery
288
+
289
+ Finds *every* interactive element on the page through four phases:
290
+
291
+ 1. **Semantic HTML** — `<a>`, `<button>`, `<input>`, `<select>`
292
+ 2. **ARIA roles** — `role="button"`, `role="tab"`, `role="listitem"`, etc.
293
+ 3. **Custom components** — `tabindex`, `onclick`, `cursor: pointer`
294
+ 4. **Disambiguation** — Duplicate labels get context from parent containers
295
+
296
+ ## CLI reference
297
+
298
+ ```
299
+ spagents browse <url> [OPTIONS]
300
+ --format, -f Output format: json (default) or text
301
+ --timeout, -t Content detection timeout in ms (default: 15000)
302
+ --no-headless Run browser with visible window
303
+
304
+ spagents interactive <url> [OPTIONS]
305
+ --timeout, -t Content detection timeout in ms (default: 15000)
306
+ --no-headless Run browser with visible window
307
+
308
+ spagents mcp [OPTIONS]
309
+ --transport Transport: stdio (default) or sse
310
+ --port, -p Port for SSE transport (default: 8000)
311
+ ```
312
+
313
+ ## License
314
+
315
+ MIT with an amended community contribution requirement for organizations with
316
+ more than 100 employees. See [LICENSE.md](LICENSE.md) for details.
@@ -0,0 +1,233 @@
1
+ <p align="center">
2
+ <h1 align="center">spagents</h1>
3
+ <p align="center">
4
+ <strong>Give your AI agents eyes for the modern web.</strong>
5
+ </p>
6
+ <p align="center">
7
+ <a href="#installation">Installation</a> &middot;
8
+ <a href="#quick-start">Quick Start</a> &middot;
9
+ <a href="#mcp-server">MCP Server</a> &middot;
10
+ <a href="#python-sdk">Python SDK</a> &middot;
11
+ <a href="#how-it-works">How It Works</a>
12
+ </p>
13
+ </p>
14
+
15
+ ---
16
+
17
+ **AI agents are blind to Single Page Applications.** Tools like `fetch` and `requests` return empty HTML shells — no articles, no content, no interactive elements. SPAs render everything via JavaScript *after* the page loads.
18
+
19
+ **spagents** fixes this. It launches a real browser, intelligently waits for SPAs to finish rendering, and returns structured, agent-friendly data — articles, actions, inputs, navigation — ready for your agent to use.
20
+
21
+ ### The problem
22
+
23
+ ```
24
+ # What fetch/requests sees on a SPA:
25
+ <div id="app"></div>
26
+ <script src="bundle.js"></script>
27
+ ```
28
+
29
+ ### The spagents solution
30
+
31
+ ```bash
32
+ $ spagents browse "https://news.kagi.com" --format text
33
+
34
+ Title: World | Kagi News
35
+ Content Ready: True
36
+
37
+ === 12 Articles ===
38
+ MIDDLE EAST: Update: Iran rejects US truce plan, sets terms
39
+ TECH ACCOUNTABILITY: US jury holds Meta, YouTube liable in addiction case
40
+ ARCHAEOLOGY: Possible d'Artagnan remains found beneath Maastricht church
41
+ ...
42
+
43
+ === 45 Actions ===
44
+ [1] (navigate) Listitem: Technology
45
+ [2] (click) Button: Expand story (Iran rejects US truce plan...)
46
+ [3] (input) Input: Search
47
+ ...
48
+ ```
49
+
50
+ ### Key features
51
+
52
+ - **Smart content detection** — Knows when a SPA is done rendering (not just `sleep(5)`)
53
+ - **Structured extraction** — Returns articles, links, metadata as typed Pydantic models
54
+ - **Full interaction** — Click, type, scroll, press keys, navigate — like a real user
55
+ - **Action discovery** — Finds every interactive element: buttons, inputs, ARIA roles, custom components
56
+ - **Session persistence** — Cookies, localStorage, and auth state preserved across navigations
57
+ - **Three interfaces** — Python SDK, CLI, and MCP server for Claude Desktop / AI agents
58
+
59
+ ---
60
+
61
+ ## Installation
62
+
63
+ ```bash
64
+ pip install spagents
65
+ playwright install chromium
66
+ ```
67
+
68
+ ### From source
69
+
70
+ ```bash
71
+ git clone https://github.com/your-username/spagents.git
72
+ cd spagents
73
+ uv sync
74
+ uv run playwright install chromium
75
+ ```
76
+
77
+ ## Quick start
78
+
79
+ ### CLI
80
+
81
+ ```bash
82
+ # Structured JSON output (default)
83
+ spagents browse "https://news.kagi.com"
84
+
85
+ # Human-readable text
86
+ spagents browse "https://news.kagi.com" --format text
87
+
88
+ # Interactive REPL session
89
+ spagents interactive "https://news.kagi.com"
90
+ ```
91
+
92
+ ### Python SDK
93
+
94
+ ```python
95
+ import asyncio
96
+ from spagents import BrowserManager
97
+
98
+ async def main():
99
+ async with BrowserManager() as browser:
100
+ session = await browser.new_session()
101
+
102
+ # Browse a SPA — content is fully rendered
103
+ state = await session.navigate("https://news.kagi.com")
104
+ for article in state.content.articles:
105
+ print(f"{article.category}: {article.headline}")
106
+
107
+ # Interact with the page
108
+ for action in state.actions:
109
+ if "Technology" in action.description:
110
+ state = await session.click(action.selector)
111
+ break
112
+
113
+ # Type into inputs, press keys
114
+ state = await session.type_text("#search", "climate change")
115
+ state = await session.press_key("Enter")
116
+
117
+ await session.close()
118
+
119
+ asyncio.run(main())
120
+ ```
121
+
122
+ ### MCP Server
123
+
124
+ Connect spagents to Claude Desktop or any MCP-compatible AI agent:
125
+
126
+ ```bash
127
+ spagents mcp
128
+ ```
129
+
130
+ Add to your Claude Desktop config (`claude_desktop_config.json`):
131
+
132
+ ```json
133
+ {
134
+ "mcpServers": {
135
+ "spagents": {
136
+ "command": "spagents",
137
+ "args": ["mcp"]
138
+ }
139
+ }
140
+ }
141
+ ```
142
+
143
+ Now Claude can browse any SPA:
144
+
145
+ > **You:** "What's the top story on Kagi News today?"
146
+ >
147
+ > **Claude:** *uses `browse` tool* → *reads structured articles* → gives you the answer
148
+
149
+ #### MCP tools
150
+
151
+ | Tool | Description |
152
+ |---|---|
153
+ | `browse` | Navigate to a URL, return rendered content + interactive actions |
154
+ | `click` | Click an element by CSS selector |
155
+ | `type_text` | Type into an input field |
156
+ | `press_key` | Press a keyboard key (Enter, Tab, Escape, etc.) |
157
+ | `list_actions` | Discover all interactive elements on the page |
158
+ | `navigate` | Go to a new URL within an existing session |
159
+ | `scroll` | Scroll up or down, trigger infinite scroll |
160
+ | `extract_content` | Re-extract content from the current page |
161
+ | `close_session` | Close a browser session and free resources |
162
+
163
+ ## Interactive REPL commands
164
+
165
+ ```
166
+ click <n> Click action by number
167
+ click <selector> Click by CSS selector
168
+ type <n> <text> Type text into an input by action number
169
+ type "<selector>" <text> Type text into an input by CSS selector
170
+ press <key> Press a key (Enter, Escape, Tab, ArrowDown, etc.)
171
+ select <n> <value> Select dropdown option by action number
172
+ scroll [down|up] Scroll the page
173
+ actions List all interactive elements
174
+ extract Re-extract page content
175
+ navigate <url> Navigate to a new URL
176
+ json Dump current state as JSON
177
+ quit Exit the session
178
+ ```
179
+
180
+ ## How it works
181
+
182
+ spagents wraps [Playwright](https://playwright.dev/python/) and adds three intelligent layers:
183
+
184
+ ### 1. Content ready detection
185
+
186
+ A multi-signal detector that knows when a SPA has *actually* finished rendering:
187
+
188
+ | Signal | What it checks |
189
+ |---|---|
190
+ | **Network quiescence** | No pending XHR/fetch requests for 500ms (ignoring analytics noise) |
191
+ | **DOM stabilization** | MutationObserver sees no changes for 300ms after initial render |
192
+ | **Content heuristic** | Meaningful text exists, no loading spinners, real links present |
193
+
194
+ This replaces naive approaches like `sleep(5)` or Playwright's `networkidle` (which breaks on long-polling and WebSocket connections).
195
+
196
+ ### 2. Content extraction
197
+
198
+ Extracts structured data from the rendered DOM:
199
+
200
+ - **Articles** with headlines, summaries, sources, highlights, quotes, and sections
201
+ - **Links** with surrounding context (which heading or section they're under)
202
+ - **Metadata** from OG tags, meta descriptions, and page title
203
+
204
+ ### 3. Action discovery
205
+
206
+ Finds *every* interactive element on the page through four phases:
207
+
208
+ 1. **Semantic HTML** — `<a>`, `<button>`, `<input>`, `<select>`
209
+ 2. **ARIA roles** — `role="button"`, `role="tab"`, `role="listitem"`, etc.
210
+ 3. **Custom components** — `tabindex`, `onclick`, `cursor: pointer`
211
+ 4. **Disambiguation** — Duplicate labels get context from parent containers
212
+
213
+ ## CLI reference
214
+
215
+ ```
216
+ spagents browse <url> [OPTIONS]
217
+ --format, -f Output format: json (default) or text
218
+ --timeout, -t Content detection timeout in ms (default: 15000)
219
+ --no-headless Run browser with visible window
220
+
221
+ spagents interactive <url> [OPTIONS]
222
+ --timeout, -t Content detection timeout in ms (default: 15000)
223
+ --no-headless Run browser with visible window
224
+
225
+ spagents mcp [OPTIONS]
226
+ --transport Transport: stdio (default) or sse
227
+ --port, -p Port for SSE transport (default: 8000)
228
+ ```
229
+
230
+ ## License
231
+
232
+ MIT with an amended community contribution requirement for organizations with
233
+ more than 100 employees. See [LICENSE.md](LICENSE.md) for details.
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "spagents"
3
+ version = "0.1.0"
4
+ description = "SPA-aware browsing library for AI agents"
5
+ readme = "README.md"
6
+ license = { file = "LICENSE.md" }
7
+ authors = [
8
+ { name = "Moses Wynn", email = "accounts@moseswynn.com" }
9
+ ]
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "playwright>=1.40",
13
+ "pydantic>=2.0",
14
+ "typer>=0.9",
15
+ "beautifulsoup4>=4.12",
16
+ "lxml>=5.0",
17
+ "fastmcp>=2.0",
18
+ ]
19
+
20
+ [project.scripts]
21
+ spagents = "spagents.cli.main:app"
22
+
23
+ [build-system]
24
+ requires = ["hatchling"]
25
+ build-backend = "hatchling.build"
26
+
27
+ [project.optional-dependencies]
28
+ dev = [
29
+ "pytest>=8.0",
30
+ "pytest-asyncio>=0.23",
31
+ "ruff>=0.4",
32
+ "mypy>=1.10",
33
+ ]
34
+
35
+ [tool.pytest.ini_options]
36
+ asyncio_mode = "auto"
37
+ asyncio_default_fixture_loop_scope = "function"
38
+ testpaths = ["tests"]