navcli 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,129 @@
1
+ """Interaction commands: click, type, clear, upload."""
2
+
3
+ import cmd2
4
+ from navcli.cli.commands.base import BaseBrowserCommand
5
+
6
+
7
+ # Parsers for aliased commands
8
+ _click_parser = cmd2.Cmd2ArgumentParser()
9
+ _click_parser.add_argument("selector", help="CSS selector")
10
+ _click_parser.add_argument("--force", action="store_true", help="Force click without waiting")
11
+
12
+ _type_parser = cmd2.Cmd2ArgumentParser()
13
+ _type_parser.add_argument("selector", help="CSS selector")
14
+ _type_parser.add_argument("text", help="Text to type")
15
+
16
+
17
+ class InteractionCommands(BaseBrowserCommand):
18
+ """Interaction command set."""
19
+
20
+ def __init__(self, client):
21
+ super().__init__(client)
22
+
23
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
24
+ def do_click(self, args):
25
+ """Click an element.
26
+
27
+ Usage: click <selector> [--force]
28
+ Example: click #submit-button
29
+ Example: click .login-btn --force
30
+ """
31
+ import asyncio
32
+
33
+ async def _click():
34
+ return await self.client.click(args.selector, force=args.force)
35
+
36
+ result = asyncio.run(self.run_async_client(_click))
37
+ self.pretty_result(result)
38
+
39
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
40
+ def do_type(self, args):
41
+ """Type text into an input field.
42
+
43
+ Usage: type <selector> <text>
44
+ Example: type #search-input hello world
45
+ """
46
+ import asyncio
47
+
48
+ async def _type():
49
+ return await self.client.type(args.selector, args.text)
50
+
51
+ result = asyncio.run(self.run_async_client(_type))
52
+ self.pretty_result(result)
53
+
54
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
55
+ def do_dblclick(self, args):
56
+ """Double-click an element.
57
+
58
+ Usage: dblclick <selector>
59
+ Example: dblclick #submit-button
60
+ """
61
+ import asyncio
62
+
63
+ async def _dblclick():
64
+ return await self.client.dblclick(args.selector)
65
+
66
+ result = asyncio.run(self.run_async_client(_dblclick))
67
+ self.pretty_result(result)
68
+
69
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
70
+ def do_rightclick(self, args):
71
+ """Right-click an element.
72
+
73
+ Usage: rightclick <selector>
74
+ Example: rightclick .context-menu
75
+ """
76
+ import asyncio
77
+
78
+ async def _rightclick():
79
+ return await self.client.rightclick(args.selector)
80
+
81
+ result = asyncio.run(self.run_async_client(_rightclick))
82
+ self.pretty_result(result)
83
+
84
+ # Aliases
85
+ @cmd2.with_argparser(_click_parser)
86
+ def do_c(self, args):
87
+ """Click an element (shortcut).
88
+
89
+ Usage: c <selector> [--force]
90
+ """
91
+ return self.do_click(args)
92
+
93
+ @cmd2.with_argparser(_type_parser)
94
+ def do_t(self, args):
95
+ """Type text into an input (shortcut).
96
+
97
+ Usage: t <selector> <text>
98
+ """
99
+ return self.do_type(args)
100
+
101
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
102
+ def do_clear(self, args):
103
+ """Clear an input field.
104
+
105
+ Usage: clear <selector>
106
+ Example: clear #search-input
107
+ """
108
+ import asyncio
109
+
110
+ async def _clear():
111
+ return await self.client.clear(args.selector)
112
+
113
+ result = asyncio.run(self.run_async_client(_clear))
114
+ self.pretty_result(result)
115
+
116
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
117
+ def do_upload(self, args):
118
+ """Upload a file to an input.
119
+
120
+ Usage: upload <selector> <file_path>
121
+ Example: upload input[type=file] /path/to/file.txt
122
+ """
123
+ import asyncio
124
+
125
+ async def _upload():
126
+ return await self.client.upload(args.selector, args.file_path)
127
+
128
+ result = asyncio.run(self.run_async_client(_upload))
129
+ self.pretty_result(result)
@@ -0,0 +1,90 @@
1
+ """Navigation commands: goto, back, forward, reload."""
2
+
3
+ import cmd2
4
+ from navcli.cli.commands.base import BaseBrowserCommand
5
+
6
+
7
+ # Alias parser for goto -> g
8
+ _goto_parser = cmd2.Cmd2ArgumentParser()
9
+ _goto_parser.add_argument("url", help="URL to navigate to")
10
+
11
+
12
+ class NavigationCommands(BaseBrowserCommand):
13
+ """Navigation command set."""
14
+
15
+ def __init__(self, client):
16
+ super().__init__(client)
17
+
18
+ @cmd2.with_argparser(_goto_parser)
19
+ def do_goto(self, args):
20
+ """Navigate to a URL.
21
+
22
+ Usage: goto <url>
23
+ Example: goto https://example.com
24
+ """
25
+ import asyncio
26
+ url = args.url
27
+ if not url.startswith(("http://", "https://")):
28
+ url = "https://" + url
29
+
30
+ async def _goto():
31
+ return await self.client.goto(url)
32
+
33
+ result = asyncio.run(self.run_async_client(_goto))
34
+ self.pretty_result(result)
35
+
36
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
37
+ def do_back(self, _):
38
+ """Go back in history.
39
+
40
+ Usage: back
41
+ """
42
+ import asyncio
43
+
44
+ async def _back():
45
+ return await self.client.back()
46
+
47
+ result = asyncio.run(self.run_async_client(_back))
48
+ self.pretty_result(result)
49
+
50
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
51
+ def do_forward(self, _):
52
+ """Go forward in history.
53
+
54
+ Usage: forward
55
+ """
56
+ import asyncio
57
+
58
+ async def _forward():
59
+ return await self.client.forward()
60
+
61
+ result = asyncio.run(self.run_async_client(_forward))
62
+ self.pretty_result(result)
63
+
64
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
65
+ def do_reload(self, _):
66
+ """Reload the current page.
67
+
68
+ Usage: reload
69
+ """
70
+ import asyncio
71
+
72
+ async def _reload():
73
+ return await self.client.reload()
74
+
75
+ result = asyncio.run(self.run_async_client(_reload))
76
+ self.pretty_result(result)
77
+
78
+ # Aliases
79
+ do_b = do_back # back -> b
80
+ do_f = do_forward # forward -> f
81
+ do_r = do_reload # reload -> r
82
+
83
+ # goto -> g (requires separate parser)
84
+ @cmd2.with_argparser(_goto_parser)
85
+ def do_g(self, args):
86
+ """Navigate to a URL (shortcut).
87
+
88
+ Usage: g <url>
89
+ """
90
+ return self.do_goto(args)
@@ -0,0 +1,171 @@
1
+ """Query commands: elements, text, html, screenshot, state, url, title."""
2
+
3
+ import cmd2
4
+ import base64
5
+ from navcli.cli.commands.base import BaseBrowserCommand
6
+
7
+
8
+ # Parser for evaluate command
9
+ _evaluate_parser = cmd2.Cmd2ArgumentParser()
10
+ _evaluate_parser.add_argument("expression", help="JavaScript expression")
11
+
12
+
13
+ class QueryCommands(BaseBrowserCommand):
14
+ """Query command set."""
15
+
16
+ def __init__(self, client):
17
+ super().__init__(client)
18
+
19
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
20
+ def do_elements(self, _):
21
+ """Get interactive elements on the page.
22
+
23
+ Usage: elements
24
+ """
25
+ import asyncio
26
+
27
+ async def _elements():
28
+ return await self.client.get_elements()
29
+
30
+ result = asyncio.run(self.run_async_client(_elements))
31
+ self.pretty_result(result)
32
+ if result.get("success"):
33
+ state = result.get("state", {})
34
+ elements = state.get("dom", {}).get("elements", [])
35
+ for elem in elements[:20]:
36
+ self.poutput(f" {elem.get('selector')} [{elem.get('tag')}] {elem.get('text', '')[:50]}")
37
+ if len(elements) > 20:
38
+ self.poutput(f" ... and {len(elements) - 20} more")
39
+
40
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
41
+ def do_text(self, _):
42
+ """Get page text content.
43
+
44
+ Usage: text
45
+ """
46
+ import asyncio
47
+
48
+ async def _text():
49
+ return await self.client.get_text()
50
+
51
+ result = asyncio.run(self.run_async_client(_text))
52
+ self.pretty_result(result)
53
+
54
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
55
+ def do_html(self, _):
56
+ """Get page HTML content.
57
+
58
+ Usage: html
59
+ """
60
+ import asyncio
61
+
62
+ async def _html():
63
+ return await self.client.get_html()
64
+
65
+ result = asyncio.run(self.run_async_client(_html))
66
+ self.pretty_result(result)
67
+
68
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
69
+ def do_screenshot(self, _):
70
+ """Take a screenshot of the page.
71
+
72
+ Usage: screenshot [output_file]
73
+ Example: screenshot
74
+ Example: screenshot page.png
75
+ """
76
+ import asyncio
77
+ import os
78
+
79
+ async def _screenshot():
80
+ return await self.client.get_screenshot()
81
+
82
+ result = asyncio.run(self.run_async_client(_screenshot))
83
+ self.pretty_result(result)
84
+
85
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
86
+ def do_state(self, _):
87
+ """Get full page state.
88
+
89
+ Usage: state
90
+ """
91
+ import asyncio
92
+
93
+ async def _state():
94
+ return await self.client.get_state()
95
+
96
+ result = asyncio.run(self.run_async_client(_state))
97
+ self.pretty_result(result)
98
+
99
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
100
+ def do_url(self, _):
101
+ """Get current URL.
102
+
103
+ Usage: url
104
+ """
105
+ import asyncio
106
+
107
+ async def _url():
108
+ return await self.client.get_url()
109
+
110
+ result = asyncio.run(self.run_async_client(_url))
111
+ self.pretty_result(result)
112
+
113
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
114
+ def do_title(self, _):
115
+ """Get page title.
116
+
117
+ Usage: title
118
+ """
119
+ import asyncio
120
+
121
+ async def _title():
122
+ return await self.client.get_title()
123
+
124
+ result = asyncio.run(self.run_async_client(_title))
125
+ self.pretty_result(result)
126
+
127
+ @cmd2.with_argparser(_evaluate_parser)
128
+ def do_evaluate(self, args):
129
+ """Evaluate JavaScript expression.
130
+
131
+ Usage: evaluate <js_expression>
132
+ Example: evaluate document.title
133
+ Example: evaluate window.innerHeight
134
+ """
135
+ import asyncio
136
+
137
+ async def _evaluate():
138
+ return await self.client.evaluate(args.expression)
139
+
140
+ result = asyncio.run(self.run_async_client(_evaluate))
141
+ self.pretty_result(result)
142
+
143
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
144
+ def do_links(self, _):
145
+ """Get all links on the page.
146
+
147
+ Usage: links
148
+ """
149
+ import asyncio
150
+
151
+ async def _links():
152
+ return await self.client.get_links()
153
+
154
+ result = asyncio.run(self.run_async_client(_links))
155
+ self.pretty_result(result)
156
+ if result.get("success"):
157
+ state = result.get("state", {})
158
+
159
+ @cmd2.with_argparser(cmd2.Cmd2ArgumentParser())
160
+ def do_forms(self, _):
161
+ """Get all forms on the page.
162
+
163
+ Usage: forms
164
+ """
165
+ import asyncio
166
+
167
+ async def _forms():
168
+ return await self.client.get_forms()
169
+
170
+ result = asyncio.run(self.run_async_client(_forms))
171
+ self.pretty_result(result)
File without changes
@@ -0,0 +1,15 @@
1
+ """Core models for NavCLI."""
2
+
3
+ from .element import Element
4
+ from .dom import DOMTree, DOMChange
5
+ from .state import StateSnapshot
6
+ from .feedback import CommandResult, Feedback
7
+
8
+ __all__ = [
9
+ 'Element',
10
+ 'DOMTree',
11
+ 'DOMChange',
12
+ 'StateSnapshot',
13
+ 'CommandResult',
14
+ 'Feedback',
15
+ ]
@@ -0,0 +1,33 @@
1
+ """DOM Tree model for page DOM representation."""
2
+
3
+ from typing import Optional
4
+ from pydantic import BaseModel, Field
5
+
6
+ from .element import Element
7
+
8
+
9
+ class DOMChange(BaseModel):
10
+ """Represents a SPA DOM change."""
11
+
12
+ type: str # 'added', 'removed', 'modified'
13
+ selector: str # CSS selector of changed element
14
+ html: Optional[str] = None # new HTML (for added/modified)
15
+ text: Optional[str] = None # new text (for modified)
16
+
17
+
18
+ class DOMTree(BaseModel):
19
+ """Represents the page DOM tree.
20
+
21
+ SPA Diff Strategy:
22
+ - First load returns full DOM
23
+ - Subsequent operations return incremental changes
24
+ - Fallback to full DOM if changes > 50%
25
+ """
26
+
27
+ version: int = 0 # incremental version number
28
+ full: bool = True # is full DOM (vs incremental)
29
+ html: str = "" # full or incremental HTML
30
+ changes: list[DOMChange] = Field(default_factory=list) # SPA changes
31
+ elements: list[Element] = Field(default_factory=list) # interactive elements
32
+ async_loaded: bool = False # contains async loaded content
33
+ loading: bool = False # currently loading
@@ -0,0 +1,22 @@
1
+ """Element model for interactive elements."""
2
+
3
+ from typing import Optional
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class Element(BaseModel):
8
+ """Represents an interactive element on the page.
9
+
10
+ Only returns "actionable + meaningful" elements:
11
+ - button, a (with href), input, select, textarea
12
+ - h1-h3, img (with alt), nav, menu
13
+ """
14
+
15
+ selector: str # CSS selector
16
+ tag: str # tag name
17
+ text: str # element text content
18
+ clickable: bool # is clickable
19
+ input: bool # is inputtable
20
+ type: Optional[str] = None # input type (for input elements)
21
+ href: Optional[str] = None # href (for anchor elements)
22
+ alt: Optional[str] = None # alt text (for img elements)
@@ -0,0 +1,24 @@
1
+ """Feedback and CommandResult models."""
2
+
3
+ from typing import Optional
4
+ from pydantic import BaseModel, Field
5
+
6
+ from .state import StateSnapshot
7
+
8
+
9
+ class Feedback(BaseModel):
10
+ """Represents the result of a command execution."""
11
+
12
+ action: str = "" # action performed
13
+ result: str = "" # result description
14
+ changes: list = Field(default_factory=list) # DOM changes
15
+
16
+
17
+ class CommandResult(BaseModel):
18
+ """Standard result returned by all commands."""
19
+
20
+ success: bool = False
21
+ command: str = ""
22
+ state: StateSnapshot = Field(default_factory=StateSnapshot)
23
+ feedback: Feedback = Field(default_factory=Feedback)
24
+ error: Optional[str] = None
@@ -0,0 +1,19 @@
1
+ """StateSnapshot model for page state."""
2
+
3
+ from typing import Optional
4
+ from pydantic import BaseModel, Field
5
+
6
+ from .dom import DOMTree
7
+
8
+
9
+ class StateSnapshot(BaseModel):
10
+ """Represents the current page state."""
11
+
12
+ url: str = "" # current URL
13
+ title: str = "" # page title
14
+ dom: DOMTree = Field(default_factory=DOMTree) # DOM tree
15
+ variables: dict = Field(default_factory=dict) # page variables
16
+ loading: bool = False # is still loading
17
+ network_idle: bool = True # network is idle
18
+ dom_stable: bool = True # DOM is stable
19
+ pending_requests: int = 0 # pending request count
@@ -0,0 +1,86 @@
1
+ """NavCLI Browser Server."""
2
+
3
+ import asyncio
4
+ import signal
5
+ import sys
6
+ from typing import Optional
7
+
8
+ import uvicorn
9
+ from uvicorn.config import Config
10
+
11
+ from navcli.server.app import create_app
12
+ from navcli.server.browser import start_browser, close_browser
13
+
14
+ __version__ = "0.1.0"
15
+
16
+ # Server instance
17
+ _server: Optional[uvicorn.Server] = None
18
+
19
+
20
+ async def start_server(host: str = "127.0.0.1", port: int = 8765) -> uvicorn.Server:
21
+ """Start the browser server.
22
+
23
+ Args:
24
+ host: Host to bind
25
+ port: Port to listen
26
+
27
+ Returns:
28
+ Uvicorn server instance
29
+ """
30
+ global _server
31
+
32
+ # Start browser
33
+ await start_browser()
34
+
35
+ # Create app
36
+ app = create_app()
37
+
38
+ # Configure uvicorn
39
+ config = Config(app, host=host, port=port, log_level="info")
40
+ _server = uvicorn.Server(config)
41
+
42
+ # Register shutdown handler
43
+ def signal_handler():
44
+ asyncio.create_task(shutdown())
45
+
46
+ config.lifespan = "on"
47
+
48
+ # Start server
49
+ await _server.serve()
50
+
51
+ return _server
52
+
53
+
54
+ async def shutdown():
55
+ """Shutdown the server."""
56
+ global _server
57
+
58
+ if _server:
59
+ _server.should_exit = True
60
+
61
+ # Close browser
62
+ await close_browser()
63
+
64
+
65
+ def main():
66
+ """Main entry point for nav-server command."""
67
+ import argparse
68
+
69
+ parser = argparse.ArgumentParser(description="NavCLI Browser Server")
70
+ parser.add_argument("--host", default="127.0.0.1", help="Host to bind")
71
+ parser.add_argument("--port", type=int, default=8765, help="Port to listen")
72
+
73
+ args = parser.parse_args()
74
+
75
+ print(f"Starting NavCLI Browser Server on {args.host}:{args.port}...")
76
+
77
+ try:
78
+ asyncio.run(start_server(args.host, args.port))
79
+ except KeyboardInterrupt:
80
+ print("\nShutting down...")
81
+ sys.exit(0)
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
86
+
navcli/server/app.py ADDED
@@ -0,0 +1,48 @@
1
+ """FastAPI application for NavCLI Browser Server."""
2
+
3
+ from fastapi import FastAPI
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+
6
+ from navcli.server.routes import navigation, interaction, query, explore, control, session
7
+
8
+
9
+ def create_app() -> FastAPI:
10
+ """Create and configure the FastAPI application."""
11
+ app = FastAPI(
12
+ title="NavCLI Browser Server",
13
+ description="HTTP API for controlling Playwright browser",
14
+ version="0.1.0",
15
+ )
16
+
17
+ # CORS
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=["*"],
21
+ allow_credentials=True,
22
+ allow_methods=["*"],
23
+ allow_headers=["*"],
24
+ )
25
+
26
+ # Include routes
27
+ app.include_router(navigation.router, prefix="/cmd")
28
+ app.include_router(interaction.router, prefix="/cmd")
29
+ app.include_router(query.router)
30
+ app.include_router(explore.router)
31
+ app.include_router(control.router, prefix="/cmd")
32
+ app.include_router(session.router, prefix="/cmd")
33
+
34
+ @app.get("/")
35
+ async def root():
36
+ """Root endpoint."""
37
+ return {
38
+ "name": "NavCLI Browser Server",
39
+ "version": "0.1.0",
40
+ "status": "running",
41
+ }
42
+
43
+ @app.get("/health")
44
+ async def health():
45
+ """Health check endpoint."""
46
+ return {"status": "healthy"}
47
+
48
+ return app