navcli 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.
- navcli-0.1.0/PKG-INFO +79 -0
- navcli-0.1.0/docs/README.md +46 -0
- navcli-0.1.0/navcli/cli/__init__.py +5 -0
- navcli-0.1.0/navcli/cli/app.py +84 -0
- navcli-0.1.0/navcli/cli/client.py +271 -0
- navcli-0.1.0/navcli/cli/commands/__init__.py +0 -0
- navcli-0.1.0/navcli/cli/commands/base.py +46 -0
- navcli-0.1.0/navcli/cli/commands/control.py +119 -0
- navcli-0.1.0/navcli/cli/commands/explore.py +125 -0
- navcli-0.1.0/navcli/cli/commands/interaction.py +129 -0
- navcli-0.1.0/navcli/cli/commands/navigation.py +90 -0
- navcli-0.1.0/navcli/cli/commands/query.py +171 -0
- navcli-0.1.0/navcli/core/__init__.py +0 -0
- navcli-0.1.0/navcli/core/models/__init__.py +15 -0
- navcli-0.1.0/navcli/core/models/dom.py +33 -0
- navcli-0.1.0/navcli/core/models/element.py +22 -0
- navcli-0.1.0/navcli/core/models/feedback.py +24 -0
- navcli-0.1.0/navcli/core/models/state.py +19 -0
- navcli-0.1.0/navcli/server/__init__.py +86 -0
- navcli-0.1.0/navcli/server/app.py +48 -0
- navcli-0.1.0/navcli/server/browser.py +373 -0
- navcli-0.1.0/navcli/server/middleware/__init__.py +0 -0
- navcli-0.1.0/navcli/server/routes/__init__.py +11 -0
- navcli-0.1.0/navcli/server/routes/control.py +44 -0
- navcli-0.1.0/navcli/server/routes/explore.py +382 -0
- navcli-0.1.0/navcli/server/routes/interaction.py +317 -0
- navcli-0.1.0/navcli/server/routes/navigation.py +133 -0
- navcli-0.1.0/navcli/server/routes/query.py +303 -0
- navcli-0.1.0/navcli/server/routes/session.py +177 -0
- navcli-0.1.0/navcli/utils/__init__.py +20 -0
- navcli-0.1.0/navcli/utils/image.py +30 -0
- navcli-0.1.0/navcli/utils/js.py +13 -0
- navcli-0.1.0/navcli/utils/selector.py +88 -0
- navcli-0.1.0/navcli/utils/text.py +17 -0
- navcli-0.1.0/navcli/utils/time.py +46 -0
- navcli-0.1.0/navcli/utils/url.py +16 -0
- navcli-0.1.0/navcli.egg-info/PKG-INFO +79 -0
- navcli-0.1.0/navcli.egg-info/SOURCES.txt +42 -0
- navcli-0.1.0/navcli.egg-info/dependency_links.txt +1 -0
- navcli-0.1.0/navcli.egg-info/entry_points.txt +3 -0
- navcli-0.1.0/navcli.egg-info/requires.txt +13 -0
- navcli-0.1.0/navcli.egg-info/top_level.txt +1 -0
- navcli-0.1.0/pyproject.toml +73 -0
- navcli-0.1.0/setup.cfg +4 -0
navcli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: navcli
|
|
3
|
+
Version: 0.1.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 - 目标与愿景
|
|
35
|
+
|
|
36
|
+
## 核心目标
|
|
37
|
+
|
|
38
|
+
**让 AI Agent 能够像人类一样浏览网页。**
|
|
39
|
+
|
|
40
|
+
现有方案(HTTP API、无头浏览器脚本、Playwright MCP)存在局限:
|
|
41
|
+
- 不支持 JS 渲染的 SPA
|
|
42
|
+
- 无会话状态持久化
|
|
43
|
+
- 缺乏交互式探索能力
|
|
44
|
+
|
|
45
|
+
NavCLI 的定位:**可交互、可探索的浏览器命令行工具**
|
|
46
|
+
|
|
47
|
+
## 核心价值
|
|
48
|
+
|
|
49
|
+
| 特性 | NavCLI 解决的问题 |
|
|
50
|
+
|------|------------------|
|
|
51
|
+
| JS 渲染支持 | 完整支持 SPA 应用 |
|
|
52
|
+
| 会话持久化 | cookies、session 保持 |
|
|
53
|
+
| 交互式 CLI | Agent 可边探索边操作 |
|
|
54
|
+
| Token 优化 | 轻量 elements + 按需 text/html |
|
|
55
|
+
|
|
56
|
+
## 典型工作流
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
> g https://example.com # 导航
|
|
60
|
+
> elements # 查看可操作元素
|
|
61
|
+
> c .btn-login # 点击登录
|
|
62
|
+
> t #email "test@example.com" # 输入邮箱
|
|
63
|
+
> t #password "123456" # 输入密码
|
|
64
|
+
> c button[type="submit"] # 提交
|
|
65
|
+
> text # 确认结果
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Agent 可以:**导航 → 观察 → 交互 → 反馈 → 继续**
|
|
69
|
+
|
|
70
|
+
## 愿景
|
|
71
|
+
|
|
72
|
+
成为 AI Agent 的**标准浏览器交互层**,让任何 Agent 都能通过命令式界面控制浏览器,完成:
|
|
73
|
+
- 表单填写、登录认证
|
|
74
|
+
- 信息抓取、内容探索
|
|
75
|
+
- 复杂多步业务流程
|
|
76
|
+
|
|
77
|
+
## 相关文档
|
|
78
|
+
|
|
79
|
+
- [PRD 产品需求文档](./NAVCLI_PRD.md)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# NavCLI - 目标与愿景
|
|
2
|
+
|
|
3
|
+
## 核心目标
|
|
4
|
+
|
|
5
|
+
**让 AI Agent 能够像人类一样浏览网页。**
|
|
6
|
+
|
|
7
|
+
现有方案(HTTP API、无头浏览器脚本、Playwright MCP)存在局限:
|
|
8
|
+
- 不支持 JS 渲染的 SPA
|
|
9
|
+
- 无会话状态持久化
|
|
10
|
+
- 缺乏交互式探索能力
|
|
11
|
+
|
|
12
|
+
NavCLI 的定位:**可交互、可探索的浏览器命令行工具**
|
|
13
|
+
|
|
14
|
+
## 核心价值
|
|
15
|
+
|
|
16
|
+
| 特性 | NavCLI 解决的问题 |
|
|
17
|
+
|------|------------------|
|
|
18
|
+
| JS 渲染支持 | 完整支持 SPA 应用 |
|
|
19
|
+
| 会话持久化 | cookies、session 保持 |
|
|
20
|
+
| 交互式 CLI | Agent 可边探索边操作 |
|
|
21
|
+
| Token 优化 | 轻量 elements + 按需 text/html |
|
|
22
|
+
|
|
23
|
+
## 典型工作流
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
> g https://example.com # 导航
|
|
27
|
+
> elements # 查看可操作元素
|
|
28
|
+
> c .btn-login # 点击登录
|
|
29
|
+
> t #email "test@example.com" # 输入邮箱
|
|
30
|
+
> t #password "123456" # 输入密码
|
|
31
|
+
> c button[type="submit"] # 提交
|
|
32
|
+
> text # 确认结果
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Agent 可以:**导航 → 观察 → 交互 → 反馈 → 继续**
|
|
36
|
+
|
|
37
|
+
## 愿景
|
|
38
|
+
|
|
39
|
+
成为 AI Agent 的**标准浏览器交互层**,让任何 Agent 都能通过命令式界面控制浏览器,完成:
|
|
40
|
+
- 表单填写、登录认证
|
|
41
|
+
- 信息抓取、内容探索
|
|
42
|
+
- 复杂多步业务流程
|
|
43
|
+
|
|
44
|
+
## 相关文档
|
|
45
|
+
|
|
46
|
+
- [PRD 产品需求文档](./NAVCLI_PRD.md)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""NavCLI - Browser automation command line interface."""
|
|
2
|
+
|
|
3
|
+
import cmd2
|
|
4
|
+
from navcli.cli.client import NavClient
|
|
5
|
+
from navcli.cli.commands.navigation import NavigationCommands
|
|
6
|
+
from navcli.cli.commands.interaction import InteractionCommands
|
|
7
|
+
from navcli.cli.commands.query import QueryCommands
|
|
8
|
+
from navcli.cli.commands.explore import ExploreCommands
|
|
9
|
+
from navcli.cli.commands.control import ControlCommands
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NavCLI(
|
|
13
|
+
NavigationCommands,
|
|
14
|
+
InteractionCommands,
|
|
15
|
+
QueryCommands,
|
|
16
|
+
ExploreCommands,
|
|
17
|
+
ControlCommands,
|
|
18
|
+
cmd2.Cmd
|
|
19
|
+
):
|
|
20
|
+
"""NavCLI main application class."""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
super().__init__(client=NavClient())
|
|
24
|
+
self.prompt = "(navcli) "
|
|
25
|
+
self.doc_header = "Available commands"
|
|
26
|
+
|
|
27
|
+
def do_help(self, args):
|
|
28
|
+
"""Show help or help for specific command.
|
|
29
|
+
|
|
30
|
+
Usage: help [command]
|
|
31
|
+
"""
|
|
32
|
+
if args:
|
|
33
|
+
# Show help for specific command
|
|
34
|
+
super().do_help(args)
|
|
35
|
+
else:
|
|
36
|
+
# Show categorized help
|
|
37
|
+
self.poutput("\nNavigation Commands:")
|
|
38
|
+
self.poutput(" goto <url> Navigate to URL")
|
|
39
|
+
self.poutput(" back Go back in history")
|
|
40
|
+
self.poutput(" forward Go forward in history")
|
|
41
|
+
self.poutput(" reload Reload current page")
|
|
42
|
+
|
|
43
|
+
self.poutput("\nInteraction Commands:")
|
|
44
|
+
self.poutput(" click <sel> [--force] Click element")
|
|
45
|
+
self.poutput(" type <sel> <text> Type text into input")
|
|
46
|
+
self.poutput(" clear <sel> Clear input field")
|
|
47
|
+
self.poutput(" upload <sel> <path> Upload file")
|
|
48
|
+
|
|
49
|
+
self.poutput("\nQuery Commands:")
|
|
50
|
+
self.poutput(" elements Get interactive elements")
|
|
51
|
+
self.poutput(" text Get page text content")
|
|
52
|
+
self.poutput(" html Get page HTML content")
|
|
53
|
+
self.poutput(" screenshot Take page screenshot")
|
|
54
|
+
self.poutput(" state Get full page state")
|
|
55
|
+
self.poutput(" url Get current URL")
|
|
56
|
+
self.poutput(" title Get page title")
|
|
57
|
+
|
|
58
|
+
self.poutput("\nExplore Commands:")
|
|
59
|
+
self.poutput(" find <text> Find element by text")
|
|
60
|
+
self.poutput(" findall <text> Find all elements by text")
|
|
61
|
+
self.poutput(" inspect <sel> Inspect element details")
|
|
62
|
+
self.poutput(" wait [--sel/--sec] Wait for selector or timeout")
|
|
63
|
+
self.poutput(" wait_idle [sec] Wait for network idle")
|
|
64
|
+
|
|
65
|
+
self.poutput("\nControl Commands:")
|
|
66
|
+
self.poutput(" quit Close browser")
|
|
67
|
+
self.poutput(" shutdown Shutdown server")
|
|
68
|
+
self.poutput(" exit Exit CLI")
|
|
69
|
+
self.poutput("")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main():
|
|
73
|
+
"""Entry point for navcli command."""
|
|
74
|
+
import sys
|
|
75
|
+
cli = NavCLI()
|
|
76
|
+
# Run command if provided as argument
|
|
77
|
+
if len(sys.argv) > 1:
|
|
78
|
+
cli.onecmd(" ".join(sys.argv[1:]))
|
|
79
|
+
else:
|
|
80
|
+
cli.cmdloop()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
main()
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""HTTP client for NavCLI server communication."""
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from urllib.parse import urljoin
|
|
6
|
+
|
|
7
|
+
DEFAULT_BASE_URL = "http://127.0.0.1:8765"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NavClient:
|
|
11
|
+
"""Async HTTP client for NavCLI server."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, base_url: str = DEFAULT_BASE_URL):
|
|
14
|
+
self.base_url = base_url.rstrip("/")
|
|
15
|
+
self.session: Optional[aiohttp.ClientSession] = None
|
|
16
|
+
|
|
17
|
+
async def connect(self):
|
|
18
|
+
"""Create aiohttp session."""
|
|
19
|
+
if self.session is None:
|
|
20
|
+
self.session = aiohttp.ClientSession()
|
|
21
|
+
|
|
22
|
+
async def close(self):
|
|
23
|
+
"""Close aiohttp session."""
|
|
24
|
+
if self.session:
|
|
25
|
+
await self.session.close()
|
|
26
|
+
self.session = None
|
|
27
|
+
|
|
28
|
+
async def _request(self, method: str, path: str, **kwargs):
|
|
29
|
+
"""Make HTTP request to server."""
|
|
30
|
+
if self.session is None:
|
|
31
|
+
await self.connect()
|
|
32
|
+
|
|
33
|
+
url = urljoin(self.base_url + "/", path.lstrip("/"))
|
|
34
|
+
async with self.session.request(method, url, **kwargs) as response:
|
|
35
|
+
return await response.json()
|
|
36
|
+
|
|
37
|
+
# Navigation commands
|
|
38
|
+
async def goto(self, url: str, wait_until: str = "networkidle"):
|
|
39
|
+
"""Navigate to URL."""
|
|
40
|
+
return await self._request("POST", f"/cmd/goto?url={url}&wait_until={wait_until}")
|
|
41
|
+
|
|
42
|
+
async def back(self):
|
|
43
|
+
"""Go back in history."""
|
|
44
|
+
return await self._request("POST", "/cmd/back")
|
|
45
|
+
|
|
46
|
+
async def forward(self):
|
|
47
|
+
"""Go forward in history."""
|
|
48
|
+
return await self._request("POST", "/cmd/forward")
|
|
49
|
+
|
|
50
|
+
async def reload(self):
|
|
51
|
+
"""Reload current page."""
|
|
52
|
+
return await self._request("POST", "/cmd/reload")
|
|
53
|
+
|
|
54
|
+
# Interaction commands
|
|
55
|
+
async def click(self, selector: str, force: bool = False):
|
|
56
|
+
"""Click an element."""
|
|
57
|
+
return await self._request("POST", f"/cmd/click?selector={selector}&force={force}")
|
|
58
|
+
|
|
59
|
+
async def type(self, selector: str, text: str):
|
|
60
|
+
"""Type text into input."""
|
|
61
|
+
import urllib.parse
|
|
62
|
+
encoded_text = urllib.parse.quote(text)
|
|
63
|
+
return await self._request("POST", f"/cmd/type?selector={selector}&text={encoded_text}")
|
|
64
|
+
|
|
65
|
+
async def clear(self, selector: str):
|
|
66
|
+
"""Clear input field."""
|
|
67
|
+
return await self._request("POST", f"/cmd/clear?selector={selector}")
|
|
68
|
+
|
|
69
|
+
async def upload(self, selector: str, file_path: str):
|
|
70
|
+
"""Upload file to input."""
|
|
71
|
+
import urllib.parse
|
|
72
|
+
encoded_path = urllib.parse.quote(file_path)
|
|
73
|
+
return await self._request("POST", f"/cmd/upload?selector={selector}&file_path={encoded_path}")
|
|
74
|
+
|
|
75
|
+
async def dblclick(self, selector: str):
|
|
76
|
+
"""Double-click an element."""
|
|
77
|
+
return await self._request("POST", f"/cmd/dblclick?selector={selector}")
|
|
78
|
+
|
|
79
|
+
async def rightclick(self, selector: str):
|
|
80
|
+
"""Right-click an element."""
|
|
81
|
+
return await self._request("POST", f"/cmd/rightclick?selector={selector}")
|
|
82
|
+
|
|
83
|
+
# Query commands
|
|
84
|
+
async def get_elements(self):
|
|
85
|
+
"""Get interactive elements."""
|
|
86
|
+
return await self._request("GET", "/query/elements")
|
|
87
|
+
|
|
88
|
+
async def get_text(self):
|
|
89
|
+
"""Get page text."""
|
|
90
|
+
return await self._request("GET", "/query/text")
|
|
91
|
+
|
|
92
|
+
async def get_html(self):
|
|
93
|
+
"""Get page HTML."""
|
|
94
|
+
return await self._request("GET", "/query/html")
|
|
95
|
+
|
|
96
|
+
async def get_screenshot(self):
|
|
97
|
+
"""Get page screenshot."""
|
|
98
|
+
return await self._request("GET", "/query/screenshot")
|
|
99
|
+
|
|
100
|
+
async def get_state(self):
|
|
101
|
+
"""Get page state."""
|
|
102
|
+
return await self._request("GET", "/query/state")
|
|
103
|
+
|
|
104
|
+
async def get_url(self):
|
|
105
|
+
"""Get current URL."""
|
|
106
|
+
return await self._request("GET", "/query/url")
|
|
107
|
+
|
|
108
|
+
async def get_title(self):
|
|
109
|
+
"""Get page title."""
|
|
110
|
+
return await self._request("GET", "/query/title")
|
|
111
|
+
|
|
112
|
+
async def evaluate(self, expr: str):
|
|
113
|
+
"""Evaluate JavaScript expression."""
|
|
114
|
+
import urllib.parse
|
|
115
|
+
encoded = urllib.parse.quote(expr)
|
|
116
|
+
return await self._request("GET", f"/query/evaluate?expr={encoded}")
|
|
117
|
+
|
|
118
|
+
async def get_links(self):
|
|
119
|
+
"""Get all links on the page."""
|
|
120
|
+
return await self._request("GET", "/query/links")
|
|
121
|
+
|
|
122
|
+
async def get_forms(self):
|
|
123
|
+
"""Get all forms on the page."""
|
|
124
|
+
return await self._request("GET", "/query/forms")
|
|
125
|
+
|
|
126
|
+
# Explore commands
|
|
127
|
+
async def find(self, text: str):
|
|
128
|
+
"""Find element by text."""
|
|
129
|
+
import urllib.parse
|
|
130
|
+
encoded_text = urllib.parse.quote(text)
|
|
131
|
+
return await self._request("GET", f"/explore/find?text={encoded_text}")
|
|
132
|
+
|
|
133
|
+
async def findall(self, text: str):
|
|
134
|
+
"""Find all elements by text."""
|
|
135
|
+
import urllib.parse
|
|
136
|
+
encoded_text = urllib.parse.quote(text)
|
|
137
|
+
return await self._request("GET", f"/explore/findall?text={encoded_text}")
|
|
138
|
+
|
|
139
|
+
async def inspect(self, selector: str):
|
|
140
|
+
"""Inspect element details."""
|
|
141
|
+
import urllib.parse
|
|
142
|
+
encoded_selector = urllib.parse.quote(selector)
|
|
143
|
+
return await self._request("GET", f"/explore/inspect?selector={encoded_selector}")
|
|
144
|
+
|
|
145
|
+
async def wait(self, selector: str = None, seconds: float = None):
|
|
146
|
+
"""Wait for selector or timeout."""
|
|
147
|
+
if selector:
|
|
148
|
+
import urllib.parse
|
|
149
|
+
encoded = urllib.parse.quote(selector)
|
|
150
|
+
return await self._request("POST", f"/explore/wait?selector={encoded}")
|
|
151
|
+
elif seconds is not None:
|
|
152
|
+
return await self._request("POST", f"/explore/wait?seconds={seconds}")
|
|
153
|
+
else:
|
|
154
|
+
return await self._request("POST", "/explore/wait")
|
|
155
|
+
|
|
156
|
+
async def wait_idle(self, timeout: float = 3.0):
|
|
157
|
+
"""Wait for network idle."""
|
|
158
|
+
return await self._request("POST", f"/explore/wait/idle?timeout={timeout}")
|
|
159
|
+
|
|
160
|
+
async def scroll(self, direction: str = None, x: int = None, y: int = None, selector: str = None):
|
|
161
|
+
"""Scroll the page.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
direction: top, bottom, up, down
|
|
165
|
+
x, y: coordinates
|
|
166
|
+
selector: element selector to scroll into view
|
|
167
|
+
"""
|
|
168
|
+
import urllib.parse
|
|
169
|
+
|
|
170
|
+
params = []
|
|
171
|
+
if direction:
|
|
172
|
+
params.append(f"direction={direction}")
|
|
173
|
+
elif x is not None and y is not None:
|
|
174
|
+
params.append(f"x={x}&y={y}")
|
|
175
|
+
elif selector:
|
|
176
|
+
encoded = urllib.parse.quote(selector)
|
|
177
|
+
params.append(f"selector={encoded}")
|
|
178
|
+
|
|
179
|
+
if params:
|
|
180
|
+
return await self._request("POST", f"/explore/scroll?{'&'.join(params)}")
|
|
181
|
+
else:
|
|
182
|
+
return await self._request("POST", "/explore/scroll?direction=down")
|
|
183
|
+
|
|
184
|
+
# Control commands
|
|
185
|
+
async def quit(self):
|
|
186
|
+
"""Quit browser."""
|
|
187
|
+
return await self._request("POST", "/cmd/quit")
|
|
188
|
+
|
|
189
|
+
async def shutdown(self):
|
|
190
|
+
"""Shutdown server."""
|
|
191
|
+
return await self._request("POST", "/cmd/shutdown")
|
|
192
|
+
|
|
193
|
+
# Session commands
|
|
194
|
+
async def get_cookies(self):
|
|
195
|
+
"""Get all cookies."""
|
|
196
|
+
return await self._request("GET", "/cmd/cookies")
|
|
197
|
+
|
|
198
|
+
async def clear_cookies(self):
|
|
199
|
+
"""Clear all cookies."""
|
|
200
|
+
return await self._request("DELETE", "/cmd/cookies/clear")
|
|
201
|
+
|
|
202
|
+
async def set_cookie(self, name: str, value: str, domain: str, path: str = "/",
|
|
203
|
+
expires: int = None, http_only: bool = False,
|
|
204
|
+
secure: bool = False, same_site: str = None):
|
|
205
|
+
"""Set a cookie.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
name: Cookie name
|
|
209
|
+
value: Cookie value
|
|
210
|
+
domain: Domain for cookie
|
|
211
|
+
path: Cookie path (default: "/")
|
|
212
|
+
expires: Expiration timestamp
|
|
213
|
+
http_only: HttpOnly flag
|
|
214
|
+
secure: Secure flag
|
|
215
|
+
same_site: SameSite attribute
|
|
216
|
+
"""
|
|
217
|
+
import urllib.parse
|
|
218
|
+
import json
|
|
219
|
+
|
|
220
|
+
body = {
|
|
221
|
+
"name": name,
|
|
222
|
+
"value": value,
|
|
223
|
+
"domain": domain,
|
|
224
|
+
"path": path,
|
|
225
|
+
}
|
|
226
|
+
if expires is not None:
|
|
227
|
+
body["expires"] = expires
|
|
228
|
+
if http_only:
|
|
229
|
+
body["httpOnly"] = http_only
|
|
230
|
+
if secure:
|
|
231
|
+
body["secure"] = secure
|
|
232
|
+
if same_site:
|
|
233
|
+
body["sameSite"] = same_site
|
|
234
|
+
|
|
235
|
+
return await self._request("POST", "/cmd/cookies/set", json=body)
|
|
236
|
+
|
|
237
|
+
async def save_session(self, path: str = ".navcli_session.json"):
|
|
238
|
+
"""Save session to file.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
path: File path to save session
|
|
242
|
+
"""
|
|
243
|
+
import urllib.parse
|
|
244
|
+
encoded_path = urllib.parse.quote(path)
|
|
245
|
+
return await self._request("POST", f"/cmd/session/save?path={encoded_path}")
|
|
246
|
+
|
|
247
|
+
async def load_session(self, path: str = ".navcli_session.json"):
|
|
248
|
+
"""Load session from file.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
path: File path to load session from
|
|
252
|
+
"""
|
|
253
|
+
import urllib.parse
|
|
254
|
+
encoded_path = urllib.parse.quote(path)
|
|
255
|
+
return await self._request("POST", f"/cmd/session/load?path={encoded_path}")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def main():
|
|
259
|
+
"""Test client connection."""
|
|
260
|
+
client = NavClient()
|
|
261
|
+
try:
|
|
262
|
+
await client.connect()
|
|
263
|
+
result = await client.get_state()
|
|
264
|
+
print(result)
|
|
265
|
+
finally:
|
|
266
|
+
await client.close()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
if __name__ == "__main__":
|
|
270
|
+
import asyncio
|
|
271
|
+
asyncio.run(main())
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Base command class for NavCLI commands."""
|
|
2
|
+
|
|
3
|
+
import cmd2
|
|
4
|
+
from navcli.cli.client import NavClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseBrowserCommand(cmd2.Cmd):
|
|
8
|
+
"""Base class for browser commands with client connection."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, client: NavClient):
|
|
11
|
+
super().__init__()
|
|
12
|
+
self.client = client
|
|
13
|
+
self.prompt = "(browser) "
|
|
14
|
+
|
|
15
|
+
def connect_client(self):
|
|
16
|
+
"""Ensure client is connected."""
|
|
17
|
+
return self.client.connect()
|
|
18
|
+
|
|
19
|
+
async def run_async_client(self, coro):
|
|
20
|
+
"""Run async client method."""
|
|
21
|
+
await self.connect_client()
|
|
22
|
+
try:
|
|
23
|
+
return await coro
|
|
24
|
+
finally:
|
|
25
|
+
await self.client.close()
|
|
26
|
+
|
|
27
|
+
def pretty_result(self, result: dict):
|
|
28
|
+
"""Format command result for display."""
|
|
29
|
+
if result.get("success"):
|
|
30
|
+
feedback = result.get("feedback", {})
|
|
31
|
+
self.poutput(f"[OK] {feedback.get('action', 'done')}")
|
|
32
|
+
if feedback.get("result"):
|
|
33
|
+
self.poutput(f" Result: {feedback.get('result')}")
|
|
34
|
+
state = result.get("state", {})
|
|
35
|
+
if state.get("url"):
|
|
36
|
+
self.poutput(f" URL: {state.get('url')}")
|
|
37
|
+
if state.get("title"):
|
|
38
|
+
self.poutput(f" Title: {state.get('title')}")
|
|
39
|
+
else:
|
|
40
|
+
error = result.get("error", "Unknown error")
|
|
41
|
+
self.perror(f"[FAIL] {error}")
|
|
42
|
+
|
|
43
|
+
def complete_url(self, text, line, begidx, endidx):
|
|
44
|
+
"""Complete URL with common prefixes."""
|
|
45
|
+
urls = ["http://", "https://", "http://www.", "https://www."]
|
|
46
|
+
return [u for u in urls if u.startswith(text)]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Control commands: quit, shutdown, save_session, load_session, cookies."""
|
|
2
|
+
|
|
3
|
+
import cmd2
|
|
4
|
+
from navcli.cli.commands.base import BaseBrowserCommand
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ControlCommands(BaseBrowserCommand):
|
|
8
|
+
"""Control command set."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, client):
|
|
11
|
+
super().__init__(client)
|
|
12
|
+
|
|
13
|
+
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
14
|
+
def do_quit(self, _):
|
|
15
|
+
"""Close the browser.
|
|
16
|
+
|
|
17
|
+
Usage: quit
|
|
18
|
+
"""
|
|
19
|
+
import asyncio
|
|
20
|
+
|
|
21
|
+
async def _quit():
|
|
22
|
+
return await self.client.quit()
|
|
23
|
+
|
|
24
|
+
result = asyncio.run(self.run_async_client(_quit))
|
|
25
|
+
self.pretty_result(result)
|
|
26
|
+
|
|
27
|
+
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
28
|
+
def do_shutdown(self, _):
|
|
29
|
+
"""Shutdown the server.
|
|
30
|
+
|
|
31
|
+
Usage: shutdown
|
|
32
|
+
"""
|
|
33
|
+
import asyncio
|
|
34
|
+
|
|
35
|
+
async def _shutdown():
|
|
36
|
+
return await self.client.shutdown()
|
|
37
|
+
|
|
38
|
+
result = asyncio.run(self.run_async_client(_shutdown))
|
|
39
|
+
self.pretty_result(result)
|
|
40
|
+
|
|
41
|
+
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
42
|
+
def do_exit(self, _):
|
|
43
|
+
"""Exit the CLI.
|
|
44
|
+
|
|
45
|
+
Usage: exit
|
|
46
|
+
"""
|
|
47
|
+
self.poutput("Goodbye!")
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
# Session commands
|
|
51
|
+
|
|
52
|
+
save_parser = cmd2.Cmd2ArgumentParser()
|
|
53
|
+
save_parser.add_argument("path", nargs="?", default=".navcli_session.json",
|
|
54
|
+
help="Path to save session file")
|
|
55
|
+
|
|
56
|
+
@cmd2.with_argparser(save_parser)
|
|
57
|
+
def do_save_session(self, args):
|
|
58
|
+
"""Save session (cookies, storage) to file.
|
|
59
|
+
|
|
60
|
+
Usage: save_session [path]
|
|
61
|
+
Example: save_session my_session.json
|
|
62
|
+
"""
|
|
63
|
+
import asyncio
|
|
64
|
+
|
|
65
|
+
async def _save():
|
|
66
|
+
return await self.client.save_session(args.path)
|
|
67
|
+
|
|
68
|
+
result = asyncio.run(self.run_async_client(_save))
|
|
69
|
+
self.pretty_result(result)
|
|
70
|
+
|
|
71
|
+
load_parser = cmd2.Cmd2ArgumentParser()
|
|
72
|
+
load_parser.add_argument("path", nargs="?", default=".navcli_session.json",
|
|
73
|
+
help="Path to load session file from")
|
|
74
|
+
|
|
75
|
+
@cmd2.with_argparser(load_parser)
|
|
76
|
+
def do_load_session(self, args):
|
|
77
|
+
"""Load session (cookies, storage) from file.
|
|
78
|
+
|
|
79
|
+
Usage: load_session [path]
|
|
80
|
+
Example: load_session my_session.json
|
|
81
|
+
"""
|
|
82
|
+
import asyncio
|
|
83
|
+
|
|
84
|
+
async def _load():
|
|
85
|
+
return await self.client.load_session(args.path)
|
|
86
|
+
|
|
87
|
+
result = asyncio.run(self.run_async_client(_load))
|
|
88
|
+
self.pretty_result(result)
|
|
89
|
+
|
|
90
|
+
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
91
|
+
def do_cookies(self, _):
|
|
92
|
+
"""Show current cookies.
|
|
93
|
+
|
|
94
|
+
Usage: cookies
|
|
95
|
+
"""
|
|
96
|
+
import asyncio
|
|
97
|
+
|
|
98
|
+
async def _get():
|
|
99
|
+
return await self.client.get_cookies()
|
|
100
|
+
|
|
101
|
+
result = asyncio.run(self.run_async_client(_get))
|
|
102
|
+
if result.get("success"):
|
|
103
|
+
self.poutput("[OK] got cookies")
|
|
104
|
+
else:
|
|
105
|
+
self.perror(f"[FAIL] {result.get('error')}")
|
|
106
|
+
|
|
107
|
+
@cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
|
|
108
|
+
def do_clear_cookies(self, _):
|
|
109
|
+
"""Clear all cookies.
|
|
110
|
+
|
|
111
|
+
Usage: clear_cookies
|
|
112
|
+
"""
|
|
113
|
+
import asyncio
|
|
114
|
+
|
|
115
|
+
async def _clear():
|
|
116
|
+
return await self.client.clear_cookies()
|
|
117
|
+
|
|
118
|
+
result = asyncio.run(self.run_async_client(_clear))
|
|
119
|
+
self.pretty_result(result)
|