navcli 0.1.0__tar.gz → 0.2.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.
- navcli-0.2.0/PKG-INFO +79 -0
- navcli-0.2.0/docs/README.md +46 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/app.py +4 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/client.py +34 -8
- {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/commands/control.py +16 -16
- {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/commands/explore.py +6 -6
- {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/commands/interaction.py +6 -6
- {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/commands/navigation.py +31 -9
- {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/commands/query.py +54 -10
- {navcli-0.1.0 → navcli-0.2.0}/navcli/core/models/__init__.py +2 -1
- {navcli-0.1.0 → navcli-0.2.0}/navcli/core/models/state.py +14 -3
- navcli-0.2.0/navcli/server/__init__.py +128 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/server/browser.py +61 -22
- {navcli-0.1.0 → navcli-0.2.0}/navcli/server/routes/explore.py +1 -1
- {navcli-0.1.0 → navcli-0.2.0}/navcli/server/routes/query.py +166 -7
- {navcli-0.1.0 → navcli-0.2.0}/navcli/server/routes/session.py +38 -9
- navcli-0.2.0/navcli.egg-info/PKG-INFO +79 -0
- {navcli-0.1.0 → navcli-0.2.0}/pyproject.toml +1 -1
- navcli-0.1.0/PKG-INFO +0 -79
- navcli-0.1.0/docs/README.md +0 -46
- navcli-0.1.0/navcli/server/__init__.py +0 -86
- navcli-0.1.0/navcli.egg-info/PKG-INFO +0 -79
- {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/__init__.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/commands/__init__.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/commands/base.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/core/__init__.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/core/models/dom.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/core/models/element.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/core/models/feedback.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/server/app.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/server/middleware/__init__.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/server/routes/__init__.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/server/routes/control.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/server/routes/interaction.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/server/routes/navigation.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/utils/__init__.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/utils/image.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/utils/js.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/utils/selector.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/utils/text.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/utils/time.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli/utils/url.py +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli.egg-info/SOURCES.txt +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli.egg-info/dependency_links.txt +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli.egg-info/entry_points.txt +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli.egg-info/requires.txt +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/navcli.egg-info/top_level.txt +0 -0
- {navcli-0.1.0 → navcli-0.2.0}/setup.cfg +0 -0
navcli-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: navcli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: 可交互、可探索的浏览器命令行工具,专为 AI Agent 设计
|
|
5
|
+
Author: NavCLI Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/navcli/navcli
|
|
8
|
+
Project-URL: Repository, https://github.com/navcli/navcli
|
|
9
|
+
Project-URL: Issues, https://github.com/navcli/navcli/issues
|
|
10
|
+
Keywords: browser,cli,automation,ai-agent,playwright
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: playwright>=1.40.0
|
|
22
|
+
Requires-Dist: cmd2>=2.4.0
|
|
23
|
+
Requires-Dist: fastapi>=0.109.0
|
|
24
|
+
Requires-Dist: uvicorn<1.0.0,>=0.25.0
|
|
25
|
+
Requires-Dist: pydantic>=2.5.0
|
|
26
|
+
Requires-Dist: cssify>=1.0.0
|
|
27
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy>=1.8.0; extra == "dev"
|
|
33
|
+
|
|
34
|
+
# NavCLI - Goals & Vision
|
|
35
|
+
|
|
36
|
+
## Core Goal
|
|
37
|
+
|
|
38
|
+
**Enable AI Agents to browse the web like humans.**
|
|
39
|
+
|
|
40
|
+
Existing solutions (HTTP APIs, headless browser scripts, Playwright MCP) have limitations:
|
|
41
|
+
- No support for JS-rendered SPAs
|
|
42
|
+
- No session persistence
|
|
43
|
+
- Lack of interactive exploration
|
|
44
|
+
|
|
45
|
+
NavCLI's positioning: **An interactive, explorable browser CLI**
|
|
46
|
+
|
|
47
|
+
## Core Value
|
|
48
|
+
|
|
49
|
+
| Feature | What NavCLI Solves |
|
|
50
|
+
|---------|-------------------|
|
|
51
|
+
| JS Rendering | Full SPA support |
|
|
52
|
+
| Session Persistence | Cookies, session maintained |
|
|
53
|
+
| Interactive CLI | Agent can explore while operating |
|
|
54
|
+
| Token Optimization | Lightweight elements + on-demand text/html |
|
|
55
|
+
|
|
56
|
+
## Typical Workflow
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
> g https://example.com # Navigate
|
|
60
|
+
> elements # Observe interactive elements
|
|
61
|
+
> c .btn-login # Click login
|
|
62
|
+
> t #email "test@example.com" # Type email
|
|
63
|
+
> t #password "123456" # Type password
|
|
64
|
+
> c button[type="submit"] # Submit
|
|
65
|
+
> text # Confirm result
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Agent can: **Navigate → Observe → Interact → Feedback → Continue**
|
|
69
|
+
|
|
70
|
+
## Vision
|
|
71
|
+
|
|
72
|
+
Become the **standard browser interaction layer** for AI Agents, enabling any Agent to control browsers via command-line interface:
|
|
73
|
+
- Form filling, login authentication
|
|
74
|
+
- Information scraping, content exploration
|
|
75
|
+
- Complex multi-step business processes
|
|
76
|
+
|
|
77
|
+
## Related Documentation
|
|
78
|
+
|
|
79
|
+
- [PRD Product Requirements](./NAVCLI_PRD.md)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# NavCLI - Goals & Vision
|
|
2
|
+
|
|
3
|
+
## Core Goal
|
|
4
|
+
|
|
5
|
+
**Enable AI Agents to browse the web like humans.**
|
|
6
|
+
|
|
7
|
+
Existing solutions (HTTP APIs, headless browser scripts, Playwright MCP) have limitations:
|
|
8
|
+
- No support for JS-rendered SPAs
|
|
9
|
+
- No session persistence
|
|
10
|
+
- Lack of interactive exploration
|
|
11
|
+
|
|
12
|
+
NavCLI's positioning: **An interactive, explorable browser CLI**
|
|
13
|
+
|
|
14
|
+
## Core Value
|
|
15
|
+
|
|
16
|
+
| Feature | What NavCLI Solves |
|
|
17
|
+
|---------|-------------------|
|
|
18
|
+
| JS Rendering | Full SPA support |
|
|
19
|
+
| Session Persistence | Cookies, session maintained |
|
|
20
|
+
| Interactive CLI | Agent can explore while operating |
|
|
21
|
+
| Token Optimization | Lightweight elements + on-demand text/html |
|
|
22
|
+
|
|
23
|
+
## Typical Workflow
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
> g https://example.com # Navigate
|
|
27
|
+
> elements # Observe interactive elements
|
|
28
|
+
> c .btn-login # Click login
|
|
29
|
+
> t #email "test@example.com" # Type email
|
|
30
|
+
> t #password "123456" # Type password
|
|
31
|
+
> c button[type="submit"] # Submit
|
|
32
|
+
> text # Confirm result
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Agent can: **Navigate → Observe → Interact → Feedback → Continue**
|
|
36
|
+
|
|
37
|
+
## Vision
|
|
38
|
+
|
|
39
|
+
Become the **standard browser interaction layer** for AI Agents, enabling any Agent to control browsers via command-line interface:
|
|
40
|
+
- Form filling, login authentication
|
|
41
|
+
- Information scraping, content exploration
|
|
42
|
+
- Complex multi-step business processes
|
|
43
|
+
|
|
44
|
+
## Related Documentation
|
|
45
|
+
|
|
46
|
+
- [PRD Product Requirements](./NAVCLI_PRD.md)
|
|
@@ -54,6 +54,10 @@ class NavCLI(
|
|
|
54
54
|
self.poutput(" state Get full page state")
|
|
55
55
|
self.poutput(" url Get current URL")
|
|
56
56
|
self.poutput(" title Get page title")
|
|
57
|
+
self.poutput(" tables Get all tables with data")
|
|
58
|
+
self.poutput(" paragraphs Get large text paragraphs (200+ chars)")
|
|
59
|
+
self.poutput(" links Get all links")
|
|
60
|
+
self.poutput(" forms Get all forms")
|
|
57
61
|
|
|
58
62
|
self.poutput("\nExplore Commands:")
|
|
59
63
|
self.poutput(" find <text> Find element by text")
|
|
@@ -123,6 +123,18 @@ class NavClient:
|
|
|
123
123
|
"""Get all forms on the page."""
|
|
124
124
|
return await self._request("GET", "/query/forms")
|
|
125
125
|
|
|
126
|
+
async def get_tables(self):
|
|
127
|
+
"""Get all tables on the page with their data."""
|
|
128
|
+
return await self._request("GET", "/query/tables")
|
|
129
|
+
|
|
130
|
+
async def get_paragraphs(self, min_length: int = 200):
|
|
131
|
+
"""Get large text paragraphs (200+ chars) from the page.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
min_length: Minimum paragraph length (default: 200)
|
|
135
|
+
"""
|
|
136
|
+
return await self._request("GET", f"/query/paragraphs?min_length={min_length}")
|
|
137
|
+
|
|
126
138
|
# Explore commands
|
|
127
139
|
async def find(self, text: str):
|
|
128
140
|
"""Find element by text."""
|
|
@@ -234,25 +246,39 @@ class NavClient:
|
|
|
234
246
|
|
|
235
247
|
return await self._request("POST", "/cmd/cookies/set", json=body)
|
|
236
248
|
|
|
237
|
-
async def save_session(self, path: str =
|
|
249
|
+
async def save_session(self, name: str = None, path: str = None):
|
|
238
250
|
"""Save session to file.
|
|
239
251
|
|
|
240
252
|
Args:
|
|
241
|
-
|
|
253
|
+
name: Session name (saves to ~/.navcli/sessions/<name>.json)
|
|
254
|
+
path: Full file path (alternative to name)
|
|
242
255
|
"""
|
|
243
256
|
import urllib.parse
|
|
244
|
-
encoded_path = urllib.parse.quote(path)
|
|
245
|
-
return await self._request("POST", f"/cmd/session/save?path={encoded_path}")
|
|
246
257
|
|
|
247
|
-
|
|
258
|
+
if name:
|
|
259
|
+
return await self._request("POST", f"/cmd/session/save?name={name}")
|
|
260
|
+
elif path:
|
|
261
|
+
encoded_path = urllib.parse.quote(path)
|
|
262
|
+
return await self._request("POST", f"/cmd/session/save?path={encoded_path}")
|
|
263
|
+
else:
|
|
264
|
+
return await self._request("POST", "/cmd/session/save")
|
|
265
|
+
|
|
266
|
+
async def load_session(self, name: str = None, path: str = None):
|
|
248
267
|
"""Load session from file.
|
|
249
268
|
|
|
250
269
|
Args:
|
|
251
|
-
|
|
270
|
+
name: Session name (loads from ~/.navcli/sessions/<name>.json)
|
|
271
|
+
path: Full file path (alternative to name)
|
|
252
272
|
"""
|
|
253
273
|
import urllib.parse
|
|
254
|
-
|
|
255
|
-
|
|
274
|
+
|
|
275
|
+
if name:
|
|
276
|
+
return await self._request("POST", f"/cmd/session/load?name={name}")
|
|
277
|
+
elif path:
|
|
278
|
+
encoded_path = urllib.parse.quote(path)
|
|
279
|
+
return await self._request("POST", f"/cmd/session/load?path={encoded_path}")
|
|
280
|
+
else:
|
|
281
|
+
return await self._request("POST", "/cmd/session/load")
|
|
256
282
|
|
|
257
283
|
|
|
258
284
|
async def main():
|
|
@@ -21,7 +21,7 @@ class ControlCommands(BaseBrowserCommand):
|
|
|
21
21
|
async def _quit():
|
|
22
22
|
return await self.client.quit()
|
|
23
23
|
|
|
24
|
-
result = asyncio.run(self.run_async_client(_quit))
|
|
24
|
+
result = asyncio.run(self.run_async_client(_quit()))
|
|
25
25
|
self.pretty_result(result)
|
|
26
26
|
|
|
27
27
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -35,7 +35,7 @@ class ControlCommands(BaseBrowserCommand):
|
|
|
35
35
|
async def _shutdown():
|
|
36
36
|
return await self.client.shutdown()
|
|
37
37
|
|
|
38
|
-
result = asyncio.run(self.run_async_client(_shutdown))
|
|
38
|
+
result = asyncio.run(self.run_async_client(_shutdown()))
|
|
39
39
|
self.pretty_result(result)
|
|
40
40
|
|
|
41
41
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -50,41 +50,41 @@ class ControlCommands(BaseBrowserCommand):
|
|
|
50
50
|
# Session commands
|
|
51
51
|
|
|
52
52
|
save_parser = cmd2.Cmd2ArgumentParser()
|
|
53
|
-
save_parser.add_argument("
|
|
54
|
-
|
|
53
|
+
save_parser.add_argument("name", nargs="?", default=None,
|
|
54
|
+
help="Session name (saves to ~/.navcli/sessions/<name>.json)")
|
|
55
55
|
|
|
56
56
|
@cmd2.with_argparser(save_parser)
|
|
57
57
|
def do_save_session(self, args):
|
|
58
58
|
"""Save session (cookies, storage) to file.
|
|
59
59
|
|
|
60
|
-
Usage: save_session [
|
|
61
|
-
Example: save_session
|
|
60
|
+
Usage: save_session [name]
|
|
61
|
+
Example: save_session jisilu
|
|
62
62
|
"""
|
|
63
63
|
import asyncio
|
|
64
64
|
|
|
65
65
|
async def _save():
|
|
66
|
-
return await self.client.save_session(args.
|
|
66
|
+
return await self.client.save_session(name=args.name)
|
|
67
67
|
|
|
68
|
-
result = asyncio.run(self.run_async_client(_save))
|
|
68
|
+
result = asyncio.run(self.run_async_client(_save()))
|
|
69
69
|
self.pretty_result(result)
|
|
70
70
|
|
|
71
71
|
load_parser = cmd2.Cmd2ArgumentParser()
|
|
72
|
-
load_parser.add_argument("
|
|
73
|
-
|
|
72
|
+
load_parser.add_argument("name", nargs="?", default=None,
|
|
73
|
+
help="Session name (loads from ~/.navcli/sessions/<name>.json)")
|
|
74
74
|
|
|
75
75
|
@cmd2.with_argparser(load_parser)
|
|
76
76
|
def do_load_session(self, args):
|
|
77
77
|
"""Load session (cookies, storage) from file.
|
|
78
78
|
|
|
79
|
-
Usage: load_session [
|
|
80
|
-
Example: load_session
|
|
79
|
+
Usage: load_session [name]
|
|
80
|
+
Example: load_session jisilu
|
|
81
81
|
"""
|
|
82
82
|
import asyncio
|
|
83
83
|
|
|
84
84
|
async def _load():
|
|
85
|
-
return await self.client.load_session(args.
|
|
85
|
+
return await self.client.load_session(name=args.name)
|
|
86
86
|
|
|
87
|
-
result = asyncio.run(self.run_async_client(_load))
|
|
87
|
+
result = asyncio.run(self.run_async_client(_load()))
|
|
88
88
|
self.pretty_result(result)
|
|
89
89
|
|
|
90
90
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -98,7 +98,7 @@ class ControlCommands(BaseBrowserCommand):
|
|
|
98
98
|
async def _get():
|
|
99
99
|
return await self.client.get_cookies()
|
|
100
100
|
|
|
101
|
-
result = asyncio.run(self.run_async_client(_get))
|
|
101
|
+
result = asyncio.run(self.run_async_client(_get()))
|
|
102
102
|
if result.get("success"):
|
|
103
103
|
self.poutput("[OK] got cookies")
|
|
104
104
|
else:
|
|
@@ -115,5 +115,5 @@ class ControlCommands(BaseBrowserCommand):
|
|
|
115
115
|
async def _clear():
|
|
116
116
|
return await self.client.clear_cookies()
|
|
117
117
|
|
|
118
|
-
result = asyncio.run(self.run_async_client(_clear))
|
|
118
|
+
result = asyncio.run(self.run_async_client(_clear()))
|
|
119
119
|
self.pretty_result(result)
|
|
@@ -22,7 +22,7 @@ class ExploreCommands(BaseBrowserCommand):
|
|
|
22
22
|
async def _find():
|
|
23
23
|
return await self.client.find(args.text)
|
|
24
24
|
|
|
25
|
-
result = asyncio.run(self.run_async_client(_find))
|
|
25
|
+
result = asyncio.run(self.run_async_client(_find()))
|
|
26
26
|
self.pretty_result(result)
|
|
27
27
|
|
|
28
28
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -37,7 +37,7 @@ class ExploreCommands(BaseBrowserCommand):
|
|
|
37
37
|
async def _findall():
|
|
38
38
|
return await self.client.findall(args.text)
|
|
39
39
|
|
|
40
|
-
result = asyncio.run(self.run_async_client(_findall))
|
|
40
|
+
result = asyncio.run(self.run_async_client(_findall()))
|
|
41
41
|
self.pretty_result(result)
|
|
42
42
|
|
|
43
43
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -52,7 +52,7 @@ class ExploreCommands(BaseBrowserCommand):
|
|
|
52
52
|
async def _inspect():
|
|
53
53
|
return await self.client.inspect(args.selector)
|
|
54
54
|
|
|
55
|
-
result = asyncio.run(self.run_async_client(_inspect))
|
|
55
|
+
result = asyncio.run(self.run_async_client(_inspect()))
|
|
56
56
|
self.pretty_result(result)
|
|
57
57
|
|
|
58
58
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -73,7 +73,7 @@ class ExploreCommands(BaseBrowserCommand):
|
|
|
73
73
|
else:
|
|
74
74
|
return await self.client.wait()
|
|
75
75
|
|
|
76
|
-
result = asyncio.run(self.run_async_client(_wait))
|
|
76
|
+
result = asyncio.run(self.run_async_client(_wait()))
|
|
77
77
|
self.pretty_result(result)
|
|
78
78
|
|
|
79
79
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -89,7 +89,7 @@ class ExploreCommands(BaseBrowserCommand):
|
|
|
89
89
|
timeout = args.timeout if args.timeout else 3.0
|
|
90
90
|
return await self.client.wait_idle(timeout=timeout)
|
|
91
91
|
|
|
92
|
-
result = asyncio.run(self.run_async_client(_wait_idle))
|
|
92
|
+
result = asyncio.run(self.run_async_client(_wait_idle()))
|
|
93
93
|
self.pretty_result(result)
|
|
94
94
|
|
|
95
95
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -121,5 +121,5 @@ class ExploreCommands(BaseBrowserCommand):
|
|
|
121
121
|
else:
|
|
122
122
|
return await self.client.scroll(direction="down")
|
|
123
123
|
|
|
124
|
-
result = asyncio.run(self.run_async_client(_scroll))
|
|
124
|
+
result = asyncio.run(self.run_async_client(_scroll()))
|
|
125
125
|
self.pretty_result(result)
|
|
@@ -33,7 +33,7 @@ class InteractionCommands(BaseBrowserCommand):
|
|
|
33
33
|
async def _click():
|
|
34
34
|
return await self.client.click(args.selector, force=args.force)
|
|
35
35
|
|
|
36
|
-
result = asyncio.run(self.run_async_client(_click))
|
|
36
|
+
result = asyncio.run(self.run_async_client(_click()))
|
|
37
37
|
self.pretty_result(result)
|
|
38
38
|
|
|
39
39
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -48,7 +48,7 @@ class InteractionCommands(BaseBrowserCommand):
|
|
|
48
48
|
async def _type():
|
|
49
49
|
return await self.client.type(args.selector, args.text)
|
|
50
50
|
|
|
51
|
-
result = asyncio.run(self.run_async_client(_type))
|
|
51
|
+
result = asyncio.run(self.run_async_client(_type()))
|
|
52
52
|
self.pretty_result(result)
|
|
53
53
|
|
|
54
54
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -63,7 +63,7 @@ class InteractionCommands(BaseBrowserCommand):
|
|
|
63
63
|
async def _dblclick():
|
|
64
64
|
return await self.client.dblclick(args.selector)
|
|
65
65
|
|
|
66
|
-
result = asyncio.run(self.run_async_client(_dblclick))
|
|
66
|
+
result = asyncio.run(self.run_async_client(_dblclick()))
|
|
67
67
|
self.pretty_result(result)
|
|
68
68
|
|
|
69
69
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -78,7 +78,7 @@ class InteractionCommands(BaseBrowserCommand):
|
|
|
78
78
|
async def _rightclick():
|
|
79
79
|
return await self.client.rightclick(args.selector)
|
|
80
80
|
|
|
81
|
-
result = asyncio.run(self.run_async_client(_rightclick))
|
|
81
|
+
result = asyncio.run(self.run_async_client(_rightclick()))
|
|
82
82
|
self.pretty_result(result)
|
|
83
83
|
|
|
84
84
|
# Aliases
|
|
@@ -110,7 +110,7 @@ class InteractionCommands(BaseBrowserCommand):
|
|
|
110
110
|
async def _clear():
|
|
111
111
|
return await self.client.clear(args.selector)
|
|
112
112
|
|
|
113
|
-
result = asyncio.run(self.run_async_client(_clear))
|
|
113
|
+
result = asyncio.run(self.run_async_client(_clear()))
|
|
114
114
|
self.pretty_result(result)
|
|
115
115
|
|
|
116
116
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -125,5 +125,5 @@ class InteractionCommands(BaseBrowserCommand):
|
|
|
125
125
|
async def _upload():
|
|
126
126
|
return await self.client.upload(args.selector, args.file_path)
|
|
127
127
|
|
|
128
|
-
result = asyncio.run(self.run_async_client(_upload))
|
|
128
|
+
result = asyncio.run(self.run_async_client(_upload()))
|
|
129
129
|
self.pretty_result(result)
|
|
@@ -17,7 +17,7 @@ class NavigationCommands(BaseBrowserCommand):
|
|
|
17
17
|
|
|
18
18
|
@cmd2.with_argparser(_goto_parser)
|
|
19
19
|
def do_goto(self, args):
|
|
20
|
-
"""Navigate to a URL.
|
|
20
|
+
"""Navigate to a URL and show page summary.
|
|
21
21
|
|
|
22
22
|
Usage: goto <url>
|
|
23
23
|
Example: goto https://example.com
|
|
@@ -28,9 +28,17 @@ class NavigationCommands(BaseBrowserCommand):
|
|
|
28
28
|
url = "https://" + url
|
|
29
29
|
|
|
30
30
|
async def _goto():
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
# Navigate first
|
|
32
|
+
result = await self.client.goto(url)
|
|
33
|
+
# Then get paragraphs and merge into state
|
|
34
|
+
para_result = await self.client.get_paragraphs(min_length=200)
|
|
35
|
+
if para_result.get("success") and result.get("success"):
|
|
36
|
+
# Extract paragraphs from state and merge
|
|
37
|
+
para_data = para_result.get("state", {}).get("paragraphs", [])
|
|
38
|
+
result["state"]["paragraphs"] = para_data
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
result = asyncio.run(self.run_async_client(_goto()))
|
|
34
42
|
self.pretty_result(result)
|
|
35
43
|
|
|
36
44
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -44,7 +52,7 @@ class NavigationCommands(BaseBrowserCommand):
|
|
|
44
52
|
async def _back():
|
|
45
53
|
return await self.client.back()
|
|
46
54
|
|
|
47
|
-
result = asyncio.run(self.run_async_client(_back))
|
|
55
|
+
result = asyncio.run(self.run_async_client(_back()))
|
|
48
56
|
self.pretty_result(result)
|
|
49
57
|
|
|
50
58
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -58,7 +66,7 @@ class NavigationCommands(BaseBrowserCommand):
|
|
|
58
66
|
async def _forward():
|
|
59
67
|
return await self.client.forward()
|
|
60
68
|
|
|
61
|
-
result = asyncio.run(self.run_async_client(_forward))
|
|
69
|
+
result = asyncio.run(self.run_async_client(_forward()))
|
|
62
70
|
self.pretty_result(result)
|
|
63
71
|
|
|
64
72
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -72,7 +80,7 @@ class NavigationCommands(BaseBrowserCommand):
|
|
|
72
80
|
async def _reload():
|
|
73
81
|
return await self.client.reload()
|
|
74
82
|
|
|
75
|
-
result = asyncio.run(self.run_async_client(_reload))
|
|
83
|
+
result = asyncio.run(self.run_async_client(_reload()))
|
|
76
84
|
self.pretty_result(result)
|
|
77
85
|
|
|
78
86
|
# Aliases
|
|
@@ -80,11 +88,25 @@ class NavigationCommands(BaseBrowserCommand):
|
|
|
80
88
|
do_f = do_forward # forward -> f
|
|
81
89
|
do_r = do_reload # reload -> r
|
|
82
90
|
|
|
83
|
-
# goto -> g (
|
|
91
|
+
# goto -> g (shortcut with same logic as do_goto)
|
|
84
92
|
@cmd2.with_argparser(_goto_parser)
|
|
85
93
|
def do_g(self, args):
|
|
86
94
|
"""Navigate to a URL (shortcut).
|
|
87
95
|
|
|
88
96
|
Usage: g <url>
|
|
89
97
|
"""
|
|
90
|
-
|
|
98
|
+
import asyncio
|
|
99
|
+
url = args.url
|
|
100
|
+
if not url.startswith(("http://", "https://")):
|
|
101
|
+
url = "https://" + url
|
|
102
|
+
|
|
103
|
+
async def _goto():
|
|
104
|
+
result = await self.client.goto(url)
|
|
105
|
+
para_result = await self.client.get_paragraphs(min_length=200)
|
|
106
|
+
if para_result.get("success") and result.get("success"):
|
|
107
|
+
para_data = para_result.get("state", {}).get("paragraphs", [])
|
|
108
|
+
result["state"]["paragraphs"] = para_data
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
result = asyncio.run(self.run_async_client(_goto()))
|
|
112
|
+
self.pretty_result(result)
|
|
@@ -27,7 +27,7 @@ class QueryCommands(BaseBrowserCommand):
|
|
|
27
27
|
async def _elements():
|
|
28
28
|
return await self.client.get_elements()
|
|
29
29
|
|
|
30
|
-
result = asyncio.run(self.run_async_client(_elements))
|
|
30
|
+
result = asyncio.run(self.run_async_client(_elements()))
|
|
31
31
|
self.pretty_result(result)
|
|
32
32
|
if result.get("success"):
|
|
33
33
|
state = result.get("state", {})
|
|
@@ -48,7 +48,7 @@ class QueryCommands(BaseBrowserCommand):
|
|
|
48
48
|
async def _text():
|
|
49
49
|
return await self.client.get_text()
|
|
50
50
|
|
|
51
|
-
result = asyncio.run(self.run_async_client(_text))
|
|
51
|
+
result = asyncio.run(self.run_async_client(_text()))
|
|
52
52
|
self.pretty_result(result)
|
|
53
53
|
|
|
54
54
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -62,7 +62,7 @@ class QueryCommands(BaseBrowserCommand):
|
|
|
62
62
|
async def _html():
|
|
63
63
|
return await self.client.get_html()
|
|
64
64
|
|
|
65
|
-
result = asyncio.run(self.run_async_client(_html))
|
|
65
|
+
result = asyncio.run(self.run_async_client(_html()))
|
|
66
66
|
self.pretty_result(result)
|
|
67
67
|
|
|
68
68
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -79,7 +79,7 @@ class QueryCommands(BaseBrowserCommand):
|
|
|
79
79
|
async def _screenshot():
|
|
80
80
|
return await self.client.get_screenshot()
|
|
81
81
|
|
|
82
|
-
result = asyncio.run(self.run_async_client(_screenshot))
|
|
82
|
+
result = asyncio.run(self.run_async_client(_screenshot()))
|
|
83
83
|
self.pretty_result(result)
|
|
84
84
|
|
|
85
85
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -93,7 +93,7 @@ class QueryCommands(BaseBrowserCommand):
|
|
|
93
93
|
async def _state():
|
|
94
94
|
return await self.client.get_state()
|
|
95
95
|
|
|
96
|
-
result = asyncio.run(self.run_async_client(_state))
|
|
96
|
+
result = asyncio.run(self.run_async_client(_state()))
|
|
97
97
|
self.pretty_result(result)
|
|
98
98
|
|
|
99
99
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -107,7 +107,7 @@ class QueryCommands(BaseBrowserCommand):
|
|
|
107
107
|
async def _url():
|
|
108
108
|
return await self.client.get_url()
|
|
109
109
|
|
|
110
|
-
result = asyncio.run(self.run_async_client(_url))
|
|
110
|
+
result = asyncio.run(self.run_async_client(_url()))
|
|
111
111
|
self.pretty_result(result)
|
|
112
112
|
|
|
113
113
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -121,7 +121,7 @@ class QueryCommands(BaseBrowserCommand):
|
|
|
121
121
|
async def _title():
|
|
122
122
|
return await self.client.get_title()
|
|
123
123
|
|
|
124
|
-
result = asyncio.run(self.run_async_client(_title))
|
|
124
|
+
result = asyncio.run(self.run_async_client(_title()))
|
|
125
125
|
self.pretty_result(result)
|
|
126
126
|
|
|
127
127
|
@cmd2.with_argparser(_evaluate_parser)
|
|
@@ -137,7 +137,7 @@ class QueryCommands(BaseBrowserCommand):
|
|
|
137
137
|
async def _evaluate():
|
|
138
138
|
return await self.client.evaluate(args.expression)
|
|
139
139
|
|
|
140
|
-
result = asyncio.run(self.run_async_client(_evaluate))
|
|
140
|
+
result = asyncio.run(self.run_async_client(_evaluate()))
|
|
141
141
|
self.pretty_result(result)
|
|
142
142
|
|
|
143
143
|
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
@@ -151,7 +151,7 @@ class QueryCommands(BaseBrowserCommand):
|
|
|
151
151
|
async def _links():
|
|
152
152
|
return await self.client.get_links()
|
|
153
153
|
|
|
154
|
-
result = asyncio.run(self.run_async_client(_links))
|
|
154
|
+
result = asyncio.run(self.run_async_client(_links()))
|
|
155
155
|
self.pretty_result(result)
|
|
156
156
|
if result.get("success"):
|
|
157
157
|
state = result.get("state", {})
|
|
@@ -167,5 +167,49 @@ class QueryCommands(BaseBrowserCommand):
|
|
|
167
167
|
async def _forms():
|
|
168
168
|
return await self.client.get_forms()
|
|
169
169
|
|
|
170
|
-
result = asyncio.run(self.run_async_client(_forms))
|
|
170
|
+
result = asyncio.run(self.run_async_client(_forms()))
|
|
171
171
|
self.pretty_result(result)
|
|
172
|
+
|
|
173
|
+
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
174
|
+
def do_tables(self, _):
|
|
175
|
+
"""Get all tables on the page with their data.
|
|
176
|
+
|
|
177
|
+
Usage: tables
|
|
178
|
+
"""
|
|
179
|
+
import asyncio
|
|
180
|
+
|
|
181
|
+
async def _tables():
|
|
182
|
+
return await self.client.get_tables()
|
|
183
|
+
|
|
184
|
+
result = asyncio.run(self.run_async_client(_tables()))
|
|
185
|
+
self.pretty_result(result)
|
|
186
|
+
if result.get("success"):
|
|
187
|
+
state = result.get("state", {})
|
|
188
|
+
tables = state.get("tables", [])
|
|
189
|
+
for t in tables:
|
|
190
|
+
self.poutput(f" Table: {t.get('id', 'N/A')} - {t.get('rowCount', 0)} rows")
|
|
191
|
+
|
|
192
|
+
_paragraphs_parser = cmd2.Cmd2ArgumentParser()
|
|
193
|
+
_paragraphs_parser.add_argument("min_length", nargs="?", type=int, default=200,
|
|
194
|
+
help="Minimum paragraph length (default: 200)")
|
|
195
|
+
|
|
196
|
+
@cmd2.with_argparser(_paragraphs_parser)
|
|
197
|
+
def do_paragraphs(self, args):
|
|
198
|
+
"""Get large text paragraphs (200+ chars) from the page.
|
|
199
|
+
|
|
200
|
+
Useful for extracting article content, policy documents, etc.
|
|
201
|
+
|
|
202
|
+
Usage: paragraphs [min_length]
|
|
203
|
+
Example: paragraphs
|
|
204
|
+
Example: paragraphs 500
|
|
205
|
+
"""
|
|
206
|
+
import asyncio
|
|
207
|
+
|
|
208
|
+
async def _paragraphs():
|
|
209
|
+
return await self.client.get_paragraphs(min_length=args.min_length)
|
|
210
|
+
|
|
211
|
+
result = asyncio.run(self.run_async_client(_paragraphs()))
|
|
212
|
+
self.pretty_result(result)
|
|
213
|
+
if result.get("success"):
|
|
214
|
+
state = result.get("state", {})
|
|
215
|
+
# Paragraphs are included in the feedback.result info
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from .element import Element
|
|
4
4
|
from .dom import DOMTree, DOMChange
|
|
5
|
-
from .state import StateSnapshot
|
|
5
|
+
from .state import StateSnapshot, Paragraph
|
|
6
6
|
from .feedback import CommandResult, Feedback
|
|
7
7
|
|
|
8
8
|
__all__ = [
|
|
@@ -10,6 +10,7 @@ __all__ = [
|
|
|
10
10
|
'DOMTree',
|
|
11
11
|
'DOMChange',
|
|
12
12
|
'StateSnapshot',
|
|
13
|
+
'Paragraph',
|
|
13
14
|
'CommandResult',
|
|
14
15
|
'Feedback',
|
|
15
16
|
]
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
"""StateSnapshot model for page state."""
|
|
2
2
|
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import Optional, List, Dict, Any
|
|
4
4
|
from pydantic import BaseModel, Field
|
|
5
5
|
|
|
6
6
|
from .dom import DOMTree
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
class Paragraph(BaseModel):
|
|
10
|
+
"""Represents a large text paragraph."""
|
|
11
|
+
|
|
12
|
+
index: int = 0
|
|
13
|
+
text: str = ""
|
|
14
|
+
length: int = 0
|
|
15
|
+
selector: str = ""
|
|
16
|
+
|
|
17
|
+
|
|
9
18
|
class StateSnapshot(BaseModel):
|
|
10
19
|
"""Represents the current page state."""
|
|
11
20
|
|
|
@@ -15,5 +24,7 @@ class StateSnapshot(BaseModel):
|
|
|
15
24
|
variables: dict = Field(default_factory=dict) # page variables
|
|
16
25
|
loading: bool = False # is still loading
|
|
17
26
|
network_idle: bool = True # network is idle
|
|
18
|
-
dom_stable: bool = True
|
|
19
|
-
pending_requests: int = 0
|
|
27
|
+
dom_stable: bool = True # DOM is stable
|
|
28
|
+
pending_requests: int = 0 # pending request count
|
|
29
|
+
tables: List[Dict[str, Any]] = Field(default_factory=list) # extracted tables
|
|
30
|
+
paragraphs: List[Paragraph] = Field(default_factory=list) # extracted paragraphs
|