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.
Files changed (48) hide show
  1. navcli-0.2.0/PKG-INFO +79 -0
  2. navcli-0.2.0/docs/README.md +46 -0
  3. {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/app.py +4 -0
  4. {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/client.py +34 -8
  5. {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/commands/control.py +16 -16
  6. {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/commands/explore.py +6 -6
  7. {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/commands/interaction.py +6 -6
  8. {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/commands/navigation.py +31 -9
  9. {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/commands/query.py +54 -10
  10. {navcli-0.1.0 → navcli-0.2.0}/navcli/core/models/__init__.py +2 -1
  11. {navcli-0.1.0 → navcli-0.2.0}/navcli/core/models/state.py +14 -3
  12. navcli-0.2.0/navcli/server/__init__.py +128 -0
  13. {navcli-0.1.0 → navcli-0.2.0}/navcli/server/browser.py +61 -22
  14. {navcli-0.1.0 → navcli-0.2.0}/navcli/server/routes/explore.py +1 -1
  15. {navcli-0.1.0 → navcli-0.2.0}/navcli/server/routes/query.py +166 -7
  16. {navcli-0.1.0 → navcli-0.2.0}/navcli/server/routes/session.py +38 -9
  17. navcli-0.2.0/navcli.egg-info/PKG-INFO +79 -0
  18. {navcli-0.1.0 → navcli-0.2.0}/pyproject.toml +1 -1
  19. navcli-0.1.0/PKG-INFO +0 -79
  20. navcli-0.1.0/docs/README.md +0 -46
  21. navcli-0.1.0/navcli/server/__init__.py +0 -86
  22. navcli-0.1.0/navcli.egg-info/PKG-INFO +0 -79
  23. {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/__init__.py +0 -0
  24. {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/commands/__init__.py +0 -0
  25. {navcli-0.1.0 → navcli-0.2.0}/navcli/cli/commands/base.py +0 -0
  26. {navcli-0.1.0 → navcli-0.2.0}/navcli/core/__init__.py +0 -0
  27. {navcli-0.1.0 → navcli-0.2.0}/navcli/core/models/dom.py +0 -0
  28. {navcli-0.1.0 → navcli-0.2.0}/navcli/core/models/element.py +0 -0
  29. {navcli-0.1.0 → navcli-0.2.0}/navcli/core/models/feedback.py +0 -0
  30. {navcli-0.1.0 → navcli-0.2.0}/navcli/server/app.py +0 -0
  31. {navcli-0.1.0 → navcli-0.2.0}/navcli/server/middleware/__init__.py +0 -0
  32. {navcli-0.1.0 → navcli-0.2.0}/navcli/server/routes/__init__.py +0 -0
  33. {navcli-0.1.0 → navcli-0.2.0}/navcli/server/routes/control.py +0 -0
  34. {navcli-0.1.0 → navcli-0.2.0}/navcli/server/routes/interaction.py +0 -0
  35. {navcli-0.1.0 → navcli-0.2.0}/navcli/server/routes/navigation.py +0 -0
  36. {navcli-0.1.0 → navcli-0.2.0}/navcli/utils/__init__.py +0 -0
  37. {navcli-0.1.0 → navcli-0.2.0}/navcli/utils/image.py +0 -0
  38. {navcli-0.1.0 → navcli-0.2.0}/navcli/utils/js.py +0 -0
  39. {navcli-0.1.0 → navcli-0.2.0}/navcli/utils/selector.py +0 -0
  40. {navcli-0.1.0 → navcli-0.2.0}/navcli/utils/text.py +0 -0
  41. {navcli-0.1.0 → navcli-0.2.0}/navcli/utils/time.py +0 -0
  42. {navcli-0.1.0 → navcli-0.2.0}/navcli/utils/url.py +0 -0
  43. {navcli-0.1.0 → navcli-0.2.0}/navcli.egg-info/SOURCES.txt +0 -0
  44. {navcli-0.1.0 → navcli-0.2.0}/navcli.egg-info/dependency_links.txt +0 -0
  45. {navcli-0.1.0 → navcli-0.2.0}/navcli.egg-info/entry_points.txt +0 -0
  46. {navcli-0.1.0 → navcli-0.2.0}/navcli.egg-info/requires.txt +0 -0
  47. {navcli-0.1.0 → navcli-0.2.0}/navcli.egg-info/top_level.txt +0 -0
  48. {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 = ".navcli_session.json"):
249
+ async def save_session(self, name: str = None, path: str = None):
238
250
  """Save session to file.
239
251
 
240
252
  Args:
241
- path: File path to save session
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
- async def load_session(self, path: str = ".navcli_session.json"):
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
- path: File path to load session from
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
- encoded_path = urllib.parse.quote(path)
255
- return await self._request("POST", f"/cmd/session/load?path={encoded_path}")
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("path", nargs="?", default=".navcli_session.json",
54
- help="Path to save session file")
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 [path]
61
- Example: save_session my_session.json
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.path)
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("path", nargs="?", default=".navcli_session.json",
73
- help="Path to load session file from")
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 [path]
80
- Example: load_session my_session.json
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.path)
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
- return await self.client.goto(url)
32
-
33
- result = asyncio.run(self.run_async_client(_goto))
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 (requires separate parser)
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
- return self.do_goto(args)
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 # DOM is stable
19
- pending_requests: int = 0 # pending request count
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