navcli 0.2.1__py3-none-any.whl → 0.2.3__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.
- navcli/server/__init__.py +16 -3
- navcli/server/app.py +25 -1
- navcli/server/browser.py +4 -1
- navcli/server/routes/control.py +9 -0
- navcli/server/routes/explore.py +39 -0
- navcli/server/routes/interaction.py +40 -0
- navcli/server/routes/navigation.py +22 -0
- navcli/server/routes/query.py +40 -0
- navcli/server/routes/session.py +20 -0
- {navcli-0.2.1.dist-info → navcli-0.2.3.dist-info}/METADATA +1 -1
- {navcli-0.2.1.dist-info → navcli-0.2.3.dist-info}/RECORD +14 -14
- {navcli-0.2.1.dist-info → navcli-0.2.3.dist-info}/WHEEL +0 -0
- {navcli-0.2.1.dist-info → navcli-0.2.3.dist-info}/entry_points.txt +0 -0
- {navcli-0.2.1.dist-info → navcli-0.2.3.dist-info}/top_level.txt +0 -0
navcli/server/__init__.py
CHANGED
|
@@ -22,13 +22,15 @@ _NAVCLI_SESSIONS_DIR = os.path.join(_NAVCLI_HOME, "sessions")
|
|
|
22
22
|
_server: Optional[uvicorn.Server] = None
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
async def start_server(host: str = "127.0.0.1", port: int = 8765, headless: bool = True
|
|
25
|
+
async def start_server(host: str = "127.0.0.1", port: int = 8765, headless: bool = True,
|
|
26
|
+
browser_args: Optional[list] = None) -> uvicorn.Server:
|
|
26
27
|
"""Start the browser server.
|
|
27
28
|
|
|
28
29
|
Args:
|
|
29
30
|
host: Host to bind
|
|
30
31
|
port: Port to listen
|
|
31
32
|
headless: Run browser in headless mode (default: True)
|
|
33
|
+
browser_args: Additional browser launch arguments
|
|
32
34
|
|
|
33
35
|
Returns:
|
|
34
36
|
Uvicorn server instance
|
|
@@ -36,7 +38,7 @@ async def start_server(host: str = "127.0.0.1", port: int = 8765, headless: bool
|
|
|
36
38
|
global _server
|
|
37
39
|
|
|
38
40
|
# Start browser
|
|
39
|
-
await start_browser(headless=headless)
|
|
41
|
+
await start_browser(headless=headless, args=browser_args)
|
|
40
42
|
|
|
41
43
|
# Create app
|
|
42
44
|
app = create_app()
|
|
@@ -80,9 +82,20 @@ def main():
|
|
|
80
82
|
help="Run in headless mode (default)")
|
|
81
83
|
parser.add_argument("--no-headless", dest="headless", action="store_false",
|
|
82
84
|
help="Run with GUI (non-headless)")
|
|
85
|
+
parser.add_argument("--memory-limit", type=int, default=None,
|
|
86
|
+
help="Browser memory limit in MB (e.g., --memory-limit 512)")
|
|
83
87
|
|
|
84
88
|
args = parser.parse_args()
|
|
85
89
|
|
|
90
|
+
# Build browser args for memory limit
|
|
91
|
+
browser_args = None
|
|
92
|
+
if args.memory_limit:
|
|
93
|
+
browser_args = [
|
|
94
|
+
f"--js-flags=--max-old-space-size={args.memory_limit}",
|
|
95
|
+
"--disable-dev-shm-usage",
|
|
96
|
+
]
|
|
97
|
+
print(f"Memory limit: {args.memory_limit}MB")
|
|
98
|
+
|
|
86
99
|
print(f"Starting NavCLI Browser Server on {args.host}:{args.port}...")
|
|
87
100
|
print(f"Headless: {args.headless}")
|
|
88
101
|
print("-" * 60)
|
|
@@ -117,7 +130,7 @@ def main():
|
|
|
117
130
|
print("-" * 60)
|
|
118
131
|
|
|
119
132
|
try:
|
|
120
|
-
asyncio.run(start_server(args.host, args.port, headless=args.headless))
|
|
133
|
+
asyncio.run(start_server(args.host, args.port, headless=args.headless, browser_args=browser_args))
|
|
121
134
|
except KeyboardInterrupt:
|
|
122
135
|
print("\nShutting down...")
|
|
123
136
|
sys.exit(0)
|
navcli/server/app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""FastAPI application for NavCLI Browser Server."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import time
|
|
4
|
+
from fastapi import FastAPI, Request
|
|
4
5
|
from fastapi.middleware.cors import CORSMiddleware
|
|
5
6
|
|
|
6
7
|
from navcli.server.routes import navigation, interaction, query, explore, control, session
|
|
@@ -23,6 +24,29 @@ def create_app() -> FastAPI:
|
|
|
23
24
|
allow_headers=["*"],
|
|
24
25
|
)
|
|
25
26
|
|
|
27
|
+
# Request logging middleware
|
|
28
|
+
@app.middleware("http")
|
|
29
|
+
async def log_requests(request: Request, call_next):
|
|
30
|
+
start_time = time.time()
|
|
31
|
+
|
|
32
|
+
# Log request
|
|
33
|
+
method = request.method
|
|
34
|
+
path = request.url.path
|
|
35
|
+
query_str = request.url.query
|
|
36
|
+
if query_str:
|
|
37
|
+
print(f"[CMD] {method} {path}?{query_str}")
|
|
38
|
+
else:
|
|
39
|
+
print(f"[CMD] {method} {path}")
|
|
40
|
+
|
|
41
|
+
# Process request
|
|
42
|
+
response = await call_next(request)
|
|
43
|
+
|
|
44
|
+
# Log response time
|
|
45
|
+
process_time = time.time() - start_time
|
|
46
|
+
print(f"[DONE] {method} {path} - {response.status_code} ({process_time:.2f}s)")
|
|
47
|
+
|
|
48
|
+
return response
|
|
49
|
+
|
|
26
50
|
# Include routes
|
|
27
51
|
app.include_router(navigation.router, prefix="/cmd")
|
|
28
52
|
app.include_router(interaction.router, prefix="/cmd")
|
navcli/server/browser.py
CHANGED
|
@@ -25,11 +25,12 @@ _NAVCLI_SESSIONS_DIR = os.path.join(_NAVCLI_HOME, "sessions")
|
|
|
25
25
|
_storage_state_path: str = os.path.join(_NAVCLI_SESSIONS_DIR, "session.json") # Default session file
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
async def start_browser(headless: bool = True):
|
|
28
|
+
async def start_browser(headless: bool = True, args: Optional[list] = None):
|
|
29
29
|
"""Start the browser.
|
|
30
30
|
|
|
31
31
|
Args:
|
|
32
32
|
headless: Whether to run in headless mode (default: True)
|
|
33
|
+
args: Additional browser launch arguments
|
|
33
34
|
"""
|
|
34
35
|
global _browser, _context, _page
|
|
35
36
|
|
|
@@ -43,6 +44,8 @@ async def start_browser(headless: bool = True):
|
|
|
43
44
|
user_data_dir = os.environ.get("NAVCLI_USER_DATA_DIR")
|
|
44
45
|
|
|
45
46
|
launch_kwargs = {"headless": headless}
|
|
47
|
+
if args:
|
|
48
|
+
launch_kwargs["args"] = args
|
|
46
49
|
if executable_path:
|
|
47
50
|
launch_kwargs["executable_path"] = executable_path
|
|
48
51
|
|
navcli/server/routes/control.py
CHANGED
|
@@ -8,10 +8,16 @@ from navcli.server.browser import close_browser, get_current_state, start_browse
|
|
|
8
8
|
router = APIRouter()
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
def log_progress(message: str):
|
|
12
|
+
"""Print progress message."""
|
|
13
|
+
print(f"[PROGRESS] {message}")
|
|
14
|
+
|
|
15
|
+
|
|
11
16
|
@router.post("/quit")
|
|
12
17
|
async def quit() -> CommandResult:
|
|
13
18
|
"""Quit and close the browser server."""
|
|
14
19
|
try:
|
|
20
|
+
log_progress("Quitting browser...")
|
|
15
21
|
# Get final state before closing
|
|
16
22
|
try:
|
|
17
23
|
state = await get_current_state()
|
|
@@ -20,6 +26,7 @@ async def quit() -> CommandResult:
|
|
|
20
26
|
|
|
21
27
|
# Close browser
|
|
22
28
|
await close_browser()
|
|
29
|
+
log_progress("Browser closed")
|
|
23
30
|
|
|
24
31
|
return CommandResult(
|
|
25
32
|
success=True,
|
|
@@ -31,6 +38,7 @@ async def quit() -> CommandResult:
|
|
|
31
38
|
),
|
|
32
39
|
)
|
|
33
40
|
except Exception as e:
|
|
41
|
+
log_progress(f"Error: {e}")
|
|
34
42
|
return CommandResult(
|
|
35
43
|
success=False,
|
|
36
44
|
command="quit",
|
|
@@ -41,4 +49,5 @@ async def quit() -> CommandResult:
|
|
|
41
49
|
@router.post("/shutdown")
|
|
42
50
|
async def shutdown() -> CommandResult:
|
|
43
51
|
"""Shutdown the server (same as quit)."""
|
|
52
|
+
log_progress("Shutting down server...")
|
|
44
53
|
return await quit()
|
navcli/server/routes/explore.py
CHANGED
|
@@ -12,6 +12,11 @@ from navcli.utils import wait_for_condition
|
|
|
12
12
|
router = APIRouter(prefix="/explore")
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def log_progress(message: str):
|
|
16
|
+
"""Print progress message."""
|
|
17
|
+
print(f"[PROGRESS] {message}")
|
|
18
|
+
|
|
19
|
+
|
|
15
20
|
class FindRequest(BaseModel):
|
|
16
21
|
text: str
|
|
17
22
|
|
|
@@ -23,6 +28,8 @@ async def find(text: str = Query(...)) -> CommandResult:
|
|
|
23
28
|
if not text:
|
|
24
29
|
raise HTTPException(status_code=400, detail="text is required")
|
|
25
30
|
|
|
31
|
+
log_progress(f"Finding element with text: {text}")
|
|
32
|
+
|
|
26
33
|
await start_browser()
|
|
27
34
|
page = get_page()
|
|
28
35
|
|
|
@@ -34,6 +41,7 @@ async def find(text: str = Query(...)) -> CommandResult:
|
|
|
34
41
|
# Get element info
|
|
35
42
|
tag = await locator.evaluate_handle("el => el.tagName")
|
|
36
43
|
element_text = await locator.inner_text()
|
|
44
|
+
log_progress(f"Found: <{tag.lower()}> {element_text[:50]}...")
|
|
37
45
|
|
|
38
46
|
element = Element(
|
|
39
47
|
selector=f"text={text}",
|
|
@@ -56,6 +64,7 @@ async def find(text: str = Query(...)) -> CommandResult:
|
|
|
56
64
|
),
|
|
57
65
|
)
|
|
58
66
|
else:
|
|
67
|
+
log_progress(f"No element found with text: {text}")
|
|
59
68
|
return CommandResult(
|
|
60
69
|
success=False,
|
|
61
70
|
command="find",
|
|
@@ -63,9 +72,11 @@ async def find(text: str = Query(...)) -> CommandResult:
|
|
|
63
72
|
)
|
|
64
73
|
except Exception:
|
|
65
74
|
# Fallback: search all elements
|
|
75
|
+
log_progress(f"Fallback: searching elements for text: {text}")
|
|
66
76
|
state = await get_current_state()
|
|
67
77
|
for elem in state.dom.elements:
|
|
68
78
|
if text.lower() in elem.text.lower():
|
|
79
|
+
log_progress(f"Found: {elem.selector}")
|
|
69
80
|
return CommandResult(
|
|
70
81
|
success=True,
|
|
71
82
|
command="find",
|
|
@@ -76,12 +87,14 @@ async def find(text: str = Query(...)) -> CommandResult:
|
|
|
76
87
|
),
|
|
77
88
|
)
|
|
78
89
|
|
|
90
|
+
log_progress(f"No elements found with text: {text}")
|
|
79
91
|
return CommandResult(
|
|
80
92
|
success=False,
|
|
81
93
|
command="find",
|
|
82
94
|
error=f"no element found with text: {text}",
|
|
83
95
|
)
|
|
84
96
|
except Exception as e:
|
|
97
|
+
log_progress(f"Error: {e}")
|
|
85
98
|
return CommandResult(
|
|
86
99
|
success=False,
|
|
87
100
|
command="find",
|
|
@@ -100,12 +113,15 @@ async def findall(text: str = Query(...)) -> CommandResult:
|
|
|
100
113
|
if not text:
|
|
101
114
|
raise HTTPException(status_code=400, detail="text is required")
|
|
102
115
|
|
|
116
|
+
log_progress(f"Finding all elements with text: {text}")
|
|
117
|
+
|
|
103
118
|
await start_browser()
|
|
104
119
|
state = await get_current_state()
|
|
105
120
|
|
|
106
121
|
matching = [e for e in state.dom.elements if text.lower() in e.text.lower()]
|
|
107
122
|
|
|
108
123
|
if matching:
|
|
124
|
+
log_progress(f"Found {len(matching)} elements")
|
|
109
125
|
state.dom.elements = matching
|
|
110
126
|
return CommandResult(
|
|
111
127
|
success=True,
|
|
@@ -117,12 +133,14 @@ async def findall(text: str = Query(...)) -> CommandResult:
|
|
|
117
133
|
),
|
|
118
134
|
)
|
|
119
135
|
else:
|
|
136
|
+
log_progress(f"No elements found with text: {text}")
|
|
120
137
|
return CommandResult(
|
|
121
138
|
success=False,
|
|
122
139
|
command="findall",
|
|
123
140
|
error=f"no elements found with text: {text}",
|
|
124
141
|
)
|
|
125
142
|
except Exception as e:
|
|
143
|
+
log_progress(f"Error: {e}")
|
|
126
144
|
return CommandResult(
|
|
127
145
|
success=False,
|
|
128
146
|
command="findall",
|
|
@@ -141,6 +159,8 @@ async def inspect(selector: str = Query(...)) -> CommandResult:
|
|
|
141
159
|
if not selector:
|
|
142
160
|
raise HTTPException(status_code=400, detail="selector is required")
|
|
143
161
|
|
|
162
|
+
log_progress(f"Inspecting element: {selector}")
|
|
163
|
+
|
|
144
164
|
await start_browser()
|
|
145
165
|
page = get_page()
|
|
146
166
|
|
|
@@ -148,6 +168,7 @@ async def inspect(selector: str = Query(...)) -> CommandResult:
|
|
|
148
168
|
count = locator.count()
|
|
149
169
|
|
|
150
170
|
if count == 0:
|
|
171
|
+
log_progress(f"Element not found: {selector}")
|
|
151
172
|
return CommandResult(
|
|
152
173
|
success=False,
|
|
153
174
|
command="inspect",
|
|
@@ -189,6 +210,7 @@ async def inspect(selector: str = Query(...)) -> CommandResult:
|
|
|
189
210
|
|
|
190
211
|
state = await get_current_state()
|
|
191
212
|
state.dom.elements = [element]
|
|
213
|
+
log_progress(f"Inspected: <{tag.lower()}> {text[:30]}...")
|
|
192
214
|
|
|
193
215
|
return CommandResult(
|
|
194
216
|
success=True,
|
|
@@ -200,6 +222,7 @@ async def inspect(selector: str = Query(...)) -> CommandResult:
|
|
|
200
222
|
),
|
|
201
223
|
)
|
|
202
224
|
except Exception as e:
|
|
225
|
+
log_progress(f"Error: {e}")
|
|
203
226
|
return CommandResult(
|
|
204
227
|
success=False,
|
|
205
228
|
command="inspect",
|
|
@@ -221,9 +244,11 @@ async def wait(req: WaitRequest) -> CommandResult:
|
|
|
221
244
|
|
|
222
245
|
if req.selector:
|
|
223
246
|
# Wait for selector
|
|
247
|
+
log_progress(f"Waiting for selector: {req.selector}")
|
|
224
248
|
try:
|
|
225
249
|
await page.wait_for_selector(req.selector, timeout=10000)
|
|
226
250
|
state = await get_current_state()
|
|
251
|
+
log_progress(f"Selector appeared: {req.selector}")
|
|
227
252
|
return CommandResult(
|
|
228
253
|
success=True,
|
|
229
254
|
command="wait",
|
|
@@ -234,6 +259,7 @@ async def wait(req: WaitRequest) -> CommandResult:
|
|
|
234
259
|
),
|
|
235
260
|
)
|
|
236
261
|
except asyncio.TimeoutError:
|
|
262
|
+
log_progress(f"Timeout waiting for: {req.selector}")
|
|
237
263
|
return CommandResult(
|
|
238
264
|
success=False,
|
|
239
265
|
command="wait",
|
|
@@ -241,6 +267,7 @@ async def wait(req: WaitRequest) -> CommandResult:
|
|
|
241
267
|
)
|
|
242
268
|
elif req.seconds:
|
|
243
269
|
# Wait for seconds
|
|
270
|
+
log_progress(f"Waiting {req.seconds}s...")
|
|
244
271
|
await asyncio.sleep(req.seconds)
|
|
245
272
|
state = await get_current_state()
|
|
246
273
|
return CommandResult(
|
|
@@ -254,8 +281,10 @@ async def wait(req: WaitRequest) -> CommandResult:
|
|
|
254
281
|
)
|
|
255
282
|
else:
|
|
256
283
|
# Default: wait for network idle
|
|
284
|
+
log_progress("Waiting for network idle...")
|
|
257
285
|
await wait_for_network_idle(3.0)
|
|
258
286
|
state = await get_current_state()
|
|
287
|
+
log_progress("Network idle")
|
|
259
288
|
return CommandResult(
|
|
260
289
|
success=True,
|
|
261
290
|
command="wait",
|
|
@@ -266,6 +295,7 @@ async def wait(req: WaitRequest) -> CommandResult:
|
|
|
266
295
|
),
|
|
267
296
|
)
|
|
268
297
|
except Exception as e:
|
|
298
|
+
log_progress(f"Error: {e}")
|
|
269
299
|
return CommandResult(
|
|
270
300
|
success=False,
|
|
271
301
|
command="wait",
|
|
@@ -277,10 +307,12 @@ async def wait(req: WaitRequest) -> CommandResult:
|
|
|
277
307
|
async def wait_idle(timeout: float = 3.0) -> CommandResult:
|
|
278
308
|
"""Wait for network idle."""
|
|
279
309
|
try:
|
|
310
|
+
log_progress(f"Waiting for network idle (timeout: {timeout}s)...")
|
|
280
311
|
await start_browser()
|
|
281
312
|
idle = await wait_for_network_idle(timeout)
|
|
282
313
|
|
|
283
314
|
state = await get_current_state()
|
|
315
|
+
log_progress(f"Network idle: {idle}")
|
|
284
316
|
|
|
285
317
|
return CommandResult(
|
|
286
318
|
success=True,
|
|
@@ -292,6 +324,7 @@ async def wait_idle(timeout: float = 3.0) -> CommandResult:
|
|
|
292
324
|
),
|
|
293
325
|
)
|
|
294
326
|
except Exception as e:
|
|
327
|
+
log_progress(f"Error: {e}")
|
|
295
328
|
return CommandResult(
|
|
296
329
|
success=False,
|
|
297
330
|
command="wait_idle",
|
|
@@ -321,9 +354,11 @@ async def scroll(req: ScrollRequest) -> CommandResult:
|
|
|
321
354
|
|
|
322
355
|
if req.selector:
|
|
323
356
|
# Scroll element into view
|
|
357
|
+
log_progress(f"Scrolling to element: {req.selector}")
|
|
324
358
|
locator = page.locator(req.selector)
|
|
325
359
|
count = locator.count()
|
|
326
360
|
if count == 0:
|
|
361
|
+
log_progress(f"Element not found: {req.selector}")
|
|
327
362
|
return CommandResult(
|
|
328
363
|
success=False,
|
|
329
364
|
command="scroll",
|
|
@@ -333,6 +368,7 @@ async def scroll(req: ScrollRequest) -> CommandResult:
|
|
|
333
368
|
action = f"scrolled to {req.selector}"
|
|
334
369
|
elif req.direction:
|
|
335
370
|
# Directional scroll
|
|
371
|
+
log_progress(f"Scrolling {req.direction}...")
|
|
336
372
|
if req.direction == "top":
|
|
337
373
|
await page.evaluate("window.scrollTo(0, 0)")
|
|
338
374
|
action = "scrolled to top"
|
|
@@ -353,6 +389,7 @@ async def scroll(req: ScrollRequest) -> CommandResult:
|
|
|
353
389
|
)
|
|
354
390
|
elif req.x is not None and req.y is not None:
|
|
355
391
|
# Coordinate scroll
|
|
392
|
+
log_progress(f"Scrolling to ({req.x}, {req.y})")
|
|
356
393
|
await page.evaluate(f"window.scrollTo({req.x}, {req.y})")
|
|
357
394
|
action = f"scrolled to ({req.x}, {req.y})"
|
|
358
395
|
else:
|
|
@@ -364,6 +401,7 @@ async def scroll(req: ScrollRequest) -> CommandResult:
|
|
|
364
401
|
|
|
365
402
|
await asyncio.sleep(0.3) # Brief wait for scroll to complete
|
|
366
403
|
state = await get_current_state()
|
|
404
|
+
log_progress(action)
|
|
367
405
|
|
|
368
406
|
return CommandResult(
|
|
369
407
|
success=True,
|
|
@@ -375,6 +413,7 @@ async def scroll(req: ScrollRequest) -> CommandResult:
|
|
|
375
413
|
),
|
|
376
414
|
)
|
|
377
415
|
except Exception as e:
|
|
416
|
+
log_progress(f"Error: {e}")
|
|
378
417
|
return CommandResult(
|
|
379
418
|
success=False,
|
|
380
419
|
command="scroll",
|
|
@@ -9,6 +9,11 @@ from navcli.server.browser import get_page, get_current_state, start_browser, wa
|
|
|
9
9
|
router = APIRouter()
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
def log_progress(message: str):
|
|
13
|
+
"""Print progress message."""
|
|
14
|
+
print(f"[PROGRESS] {message}")
|
|
15
|
+
|
|
16
|
+
|
|
12
17
|
@router.post("/click")
|
|
13
18
|
async def click(
|
|
14
19
|
selector: str = Query(..., description="CSS selector"),
|
|
@@ -19,6 +24,8 @@ async def click(
|
|
|
19
24
|
if not selector:
|
|
20
25
|
raise HTTPException(status_code=400, detail="selector is required")
|
|
21
26
|
|
|
27
|
+
log_progress(f"Clicking '{selector}'...")
|
|
28
|
+
|
|
22
29
|
await start_browser()
|
|
23
30
|
page = get_page()
|
|
24
31
|
|
|
@@ -27,6 +34,7 @@ async def click(
|
|
|
27
34
|
# Check if element exists
|
|
28
35
|
count = locator.count()
|
|
29
36
|
if count == 0:
|
|
37
|
+
log_progress(f"Element not found: {selector}")
|
|
30
38
|
return CommandResult(
|
|
31
39
|
success=False,
|
|
32
40
|
command="click",
|
|
@@ -34,6 +42,7 @@ async def click(
|
|
|
34
42
|
)
|
|
35
43
|
|
|
36
44
|
if count > 1:
|
|
45
|
+
log_progress(f"Multiple elements found: {selector}")
|
|
37
46
|
return CommandResult(
|
|
38
47
|
success=False,
|
|
39
48
|
command="click",
|
|
@@ -43,8 +52,10 @@ async def click(
|
|
|
43
52
|
# Click
|
|
44
53
|
if force:
|
|
45
54
|
await locator.click(force=True)
|
|
55
|
+
log_progress(f"Force clicked '{selector}'")
|
|
46
56
|
else:
|
|
47
57
|
await locator.click()
|
|
58
|
+
log_progress(f"Clicked '{selector}'")
|
|
48
59
|
|
|
49
60
|
# Wait for network idle
|
|
50
61
|
await wait_for_network_idle(3.0)
|
|
@@ -61,6 +72,7 @@ async def click(
|
|
|
61
72
|
),
|
|
62
73
|
)
|
|
63
74
|
except Exception as e:
|
|
75
|
+
log_progress(f"Error: {e}")
|
|
64
76
|
return CommandResult(
|
|
65
77
|
success=False,
|
|
66
78
|
command="click",
|
|
@@ -80,6 +92,8 @@ async def type_text(
|
|
|
80
92
|
if not text:
|
|
81
93
|
raise HTTPException(status_code=400, detail="text is required")
|
|
82
94
|
|
|
95
|
+
log_progress(f"Typing into '{selector}': {text[:30]}...")
|
|
96
|
+
|
|
83
97
|
await start_browser()
|
|
84
98
|
page = get_page()
|
|
85
99
|
|
|
@@ -88,6 +102,7 @@ async def type_text(
|
|
|
88
102
|
# Check if element exists
|
|
89
103
|
count = locator.count()
|
|
90
104
|
if count == 0:
|
|
105
|
+
log_progress(f"Element not found: {selector}")
|
|
91
106
|
return CommandResult(
|
|
92
107
|
success=False,
|
|
93
108
|
command="type",
|
|
@@ -97,6 +112,7 @@ async def type_text(
|
|
|
97
112
|
# Clear and type
|
|
98
113
|
await locator.clear()
|
|
99
114
|
await locator.fill(text)
|
|
115
|
+
log_progress(f"Typed into '{selector}'")
|
|
100
116
|
|
|
101
117
|
state = await get_current_state()
|
|
102
118
|
|
|
@@ -110,6 +126,7 @@ async def type_text(
|
|
|
110
126
|
),
|
|
111
127
|
)
|
|
112
128
|
except Exception as e:
|
|
129
|
+
log_progress(f"Error: {e}")
|
|
113
130
|
return CommandResult(
|
|
114
131
|
success=False,
|
|
115
132
|
command="type",
|
|
@@ -126,6 +143,8 @@ async def clear_input(
|
|
|
126
143
|
if not selector:
|
|
127
144
|
raise HTTPException(status_code=400, detail="selector is required")
|
|
128
145
|
|
|
146
|
+
log_progress(f"Clearing '{selector}'...")
|
|
147
|
+
|
|
129
148
|
await start_browser()
|
|
130
149
|
page = get_page()
|
|
131
150
|
|
|
@@ -134,6 +153,7 @@ async def clear_input(
|
|
|
134
153
|
# Check if element exists
|
|
135
154
|
count = locator.count()
|
|
136
155
|
if count == 0:
|
|
156
|
+
log_progress(f"Element not found: {selector}")
|
|
137
157
|
return CommandResult(
|
|
138
158
|
success=False,
|
|
139
159
|
command="clear",
|
|
@@ -142,6 +162,7 @@ async def clear_input(
|
|
|
142
162
|
|
|
143
163
|
# Clear
|
|
144
164
|
await locator.clear()
|
|
165
|
+
log_progress(f"Cleared '{selector}'")
|
|
145
166
|
|
|
146
167
|
state = await get_current_state()
|
|
147
168
|
|
|
@@ -155,6 +176,7 @@ async def clear_input(
|
|
|
155
176
|
),
|
|
156
177
|
)
|
|
157
178
|
except Exception as e:
|
|
179
|
+
log_progress(f"Error: {e}")
|
|
158
180
|
return CommandResult(
|
|
159
181
|
success=False,
|
|
160
182
|
command="clear",
|
|
@@ -174,9 +196,12 @@ async def upload_file(
|
|
|
174
196
|
if not file_path:
|
|
175
197
|
raise HTTPException(status_code=400, detail="file_path is required")
|
|
176
198
|
|
|
199
|
+
log_progress(f"Uploading '{file_path}' to '{selector}'...")
|
|
200
|
+
|
|
177
201
|
# Check if file exists
|
|
178
202
|
import os
|
|
179
203
|
if not os.path.exists(file_path):
|
|
204
|
+
log_progress(f"File not found: {file_path}")
|
|
180
205
|
return CommandResult(
|
|
181
206
|
success=False,
|
|
182
207
|
command="upload",
|
|
@@ -191,6 +216,7 @@ async def upload_file(
|
|
|
191
216
|
# Check if element exists
|
|
192
217
|
count = locator.count()
|
|
193
218
|
if count == 0:
|
|
219
|
+
log_progress(f"Element not found: {selector}")
|
|
194
220
|
return CommandResult(
|
|
195
221
|
success=False,
|
|
196
222
|
command="upload",
|
|
@@ -199,6 +225,7 @@ async def upload_file(
|
|
|
199
225
|
|
|
200
226
|
# Upload file
|
|
201
227
|
await locator.set_input_files(file_path)
|
|
228
|
+
log_progress(f"Uploaded '{file_path}'")
|
|
202
229
|
|
|
203
230
|
state = await get_current_state()
|
|
204
231
|
|
|
@@ -212,6 +239,7 @@ async def upload_file(
|
|
|
212
239
|
),
|
|
213
240
|
)
|
|
214
241
|
except Exception as e:
|
|
242
|
+
log_progress(f"Error: {e}")
|
|
215
243
|
return CommandResult(
|
|
216
244
|
success=False,
|
|
217
245
|
command="upload",
|
|
@@ -228,18 +256,22 @@ async def dblclick(
|
|
|
228
256
|
if not selector:
|
|
229
257
|
raise HTTPException(status_code=400, detail="selector is required")
|
|
230
258
|
|
|
259
|
+
log_progress(f"Double-clicking '{selector}'...")
|
|
260
|
+
|
|
231
261
|
await start_browser()
|
|
232
262
|
page = get_page()
|
|
233
263
|
|
|
234
264
|
locator = page.locator(selector)
|
|
235
265
|
count = locator.count()
|
|
236
266
|
if count == 0:
|
|
267
|
+
log_progress(f"Element not found: {selector}")
|
|
237
268
|
return CommandResult(
|
|
238
269
|
success=False,
|
|
239
270
|
command="dblclick",
|
|
240
271
|
error=f"element not found: {selector}",
|
|
241
272
|
)
|
|
242
273
|
if count > 1:
|
|
274
|
+
log_progress(f"Multiple elements found: {selector}")
|
|
243
275
|
return CommandResult(
|
|
244
276
|
success=False,
|
|
245
277
|
command="dblclick",
|
|
@@ -247,6 +279,7 @@ async def dblclick(
|
|
|
247
279
|
)
|
|
248
280
|
|
|
249
281
|
await locator.dblclick()
|
|
282
|
+
log_progress(f"Double-clicked '{selector}'")
|
|
250
283
|
|
|
251
284
|
await wait_for_network_idle(3.0)
|
|
252
285
|
state = await get_current_state()
|
|
@@ -261,6 +294,7 @@ async def dblclick(
|
|
|
261
294
|
),
|
|
262
295
|
)
|
|
263
296
|
except Exception as e:
|
|
297
|
+
log_progress(f"Error: {e}")
|
|
264
298
|
return CommandResult(
|
|
265
299
|
success=False,
|
|
266
300
|
command="dblclick",
|
|
@@ -277,18 +311,22 @@ async def rightclick(
|
|
|
277
311
|
if not selector:
|
|
278
312
|
raise HTTPException(status_code=400, detail="selector is required")
|
|
279
313
|
|
|
314
|
+
log_progress(f"Right-clicking '{selector}'...")
|
|
315
|
+
|
|
280
316
|
await start_browser()
|
|
281
317
|
page = get_page()
|
|
282
318
|
|
|
283
319
|
locator = page.locator(selector)
|
|
284
320
|
count = locator.count()
|
|
285
321
|
if count == 0:
|
|
322
|
+
log_progress(f"Element not found: {selector}")
|
|
286
323
|
return CommandResult(
|
|
287
324
|
success=False,
|
|
288
325
|
command="rightclick",
|
|
289
326
|
error=f"element not found: {selector}",
|
|
290
327
|
)
|
|
291
328
|
if count > 1:
|
|
329
|
+
log_progress(f"Multiple elements found: {selector}")
|
|
292
330
|
return CommandResult(
|
|
293
331
|
success=False,
|
|
294
332
|
command="rightclick",
|
|
@@ -296,6 +334,7 @@ async def rightclick(
|
|
|
296
334
|
)
|
|
297
335
|
|
|
298
336
|
await locator.click(button="right")
|
|
337
|
+
log_progress(f"Right-clicked '{selector}'")
|
|
299
338
|
|
|
300
339
|
await wait_for_network_idle(3.0)
|
|
301
340
|
state = await get_current_state()
|
|
@@ -310,6 +349,7 @@ async def rightclick(
|
|
|
310
349
|
),
|
|
311
350
|
)
|
|
312
351
|
except Exception as e:
|
|
352
|
+
log_progress(f"Error: {e}")
|
|
313
353
|
return CommandResult(
|
|
314
354
|
success=False,
|
|
315
355
|
command="rightclick",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Navigation routes: goto, back, forward, reload."""
|
|
2
2
|
|
|
3
|
+
import time
|
|
3
4
|
from typing import Optional
|
|
4
5
|
from fastapi import APIRouter, HTTPException, Query
|
|
5
6
|
|
|
@@ -10,6 +11,11 @@ from navcli.utils import normalize_url
|
|
|
10
11
|
router = APIRouter()
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
def log_progress(message: str):
|
|
15
|
+
"""Print progress message."""
|
|
16
|
+
print(f"[PROGRESS] {message}")
|
|
17
|
+
|
|
18
|
+
|
|
13
19
|
@router.post("/goto")
|
|
14
20
|
async def goto(
|
|
15
21
|
url: str = Query(..., description="URL to navigate to"),
|
|
@@ -21,6 +27,7 @@ async def goto(
|
|
|
21
27
|
raise HTTPException(status_code=400, detail="url is required")
|
|
22
28
|
|
|
23
29
|
normalized_url = normalize_url(url)
|
|
30
|
+
log_progress(f"Navigating to {normalized_url}...")
|
|
24
31
|
|
|
25
32
|
# Start browser if not started
|
|
26
33
|
await start_browser()
|
|
@@ -28,12 +35,17 @@ async def goto(
|
|
|
28
35
|
|
|
29
36
|
# Navigate
|
|
30
37
|
await page.goto(normalized_url, wait_until=wait_until)
|
|
38
|
+
log_progress(f"Page loaded, waiting for network idle...")
|
|
31
39
|
|
|
32
40
|
# Wait for network idle
|
|
41
|
+
start_wait = time.time()
|
|
33
42
|
await wait_for_network_idle(3.0)
|
|
43
|
+
wait_time = time.time() - start_wait
|
|
44
|
+
log_progress(f"Network idle after {wait_time:.1f}s")
|
|
34
45
|
|
|
35
46
|
# Get new state
|
|
36
47
|
state = await get_current_state()
|
|
48
|
+
log_progress(f"Page title: {state.title}")
|
|
37
49
|
|
|
38
50
|
return CommandResult(
|
|
39
51
|
success=True,
|
|
@@ -45,6 +57,7 @@ async def goto(
|
|
|
45
57
|
),
|
|
46
58
|
)
|
|
47
59
|
except Exception as e:
|
|
60
|
+
log_progress(f"Error: {e}")
|
|
48
61
|
return CommandResult(
|
|
49
62
|
success=False,
|
|
50
63
|
command="goto",
|
|
@@ -56,11 +69,13 @@ async def goto(
|
|
|
56
69
|
async def back() -> CommandResult:
|
|
57
70
|
"""Go back in history."""
|
|
58
71
|
try:
|
|
72
|
+
log_progress("Going back...")
|
|
59
73
|
page = get_page()
|
|
60
74
|
await page.go_back()
|
|
61
75
|
await wait_for_network_idle(3.0)
|
|
62
76
|
|
|
63
77
|
state = await get_current_state()
|
|
78
|
+
log_progress(f"Back to: {state.url}")
|
|
64
79
|
|
|
65
80
|
return CommandResult(
|
|
66
81
|
success=True,
|
|
@@ -72,6 +87,7 @@ async def back() -> CommandResult:
|
|
|
72
87
|
),
|
|
73
88
|
)
|
|
74
89
|
except Exception as e:
|
|
90
|
+
log_progress(f"Error: {e}")
|
|
75
91
|
return CommandResult(
|
|
76
92
|
success=False,
|
|
77
93
|
command="back",
|
|
@@ -83,11 +99,13 @@ async def back() -> CommandResult:
|
|
|
83
99
|
async def forward() -> CommandResult:
|
|
84
100
|
"""Go forward in history."""
|
|
85
101
|
try:
|
|
102
|
+
log_progress("Going forward...")
|
|
86
103
|
page = get_page()
|
|
87
104
|
await page.go_forward()
|
|
88
105
|
await wait_for_network_idle(3.0)
|
|
89
106
|
|
|
90
107
|
state = await get_current_state()
|
|
108
|
+
log_progress(f"Forward to: {state.url}")
|
|
91
109
|
|
|
92
110
|
return CommandResult(
|
|
93
111
|
success=True,
|
|
@@ -99,6 +117,7 @@ async def forward() -> CommandResult:
|
|
|
99
117
|
),
|
|
100
118
|
)
|
|
101
119
|
except Exception as e:
|
|
120
|
+
log_progress(f"Error: {e}")
|
|
102
121
|
return CommandResult(
|
|
103
122
|
success=False,
|
|
104
123
|
command="forward",
|
|
@@ -110,11 +129,13 @@ async def forward() -> CommandResult:
|
|
|
110
129
|
async def reload() -> CommandResult:
|
|
111
130
|
"""Reload the current page."""
|
|
112
131
|
try:
|
|
132
|
+
log_progress("Reloading page...")
|
|
113
133
|
page = get_page()
|
|
114
134
|
await page.reload()
|
|
115
135
|
await wait_for_network_idle(3.0)
|
|
116
136
|
|
|
117
137
|
state = await get_current_state()
|
|
138
|
+
log_progress(f"Reloaded: {state.url}")
|
|
118
139
|
|
|
119
140
|
return CommandResult(
|
|
120
141
|
success=True,
|
|
@@ -126,6 +147,7 @@ async def reload() -> CommandResult:
|
|
|
126
147
|
),
|
|
127
148
|
)
|
|
128
149
|
except Exception as e:
|
|
150
|
+
log_progress(f"Error: {e}")
|
|
129
151
|
return CommandResult(
|
|
130
152
|
success=False,
|
|
131
153
|
command="reload",
|
navcli/server/routes/query.py
CHANGED
|
@@ -9,12 +9,19 @@ from navcli.utils import encode_screenshot
|
|
|
9
9
|
router = APIRouter(prefix="/query")
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
def log_progress(message: str):
|
|
13
|
+
"""Print progress message."""
|
|
14
|
+
print(f"[PROGRESS] {message}")
|
|
15
|
+
|
|
16
|
+
|
|
12
17
|
@router.get("/elements")
|
|
13
18
|
async def get_elements() -> CommandResult:
|
|
14
19
|
"""Get interactive elements on the page."""
|
|
15
20
|
try:
|
|
21
|
+
log_progress("Getting elements...")
|
|
16
22
|
await start_browser()
|
|
17
23
|
state = await get_current_state()
|
|
24
|
+
log_progress(f"Found {len(state.dom.elements)} elements")
|
|
18
25
|
|
|
19
26
|
return CommandResult(
|
|
20
27
|
success=True,
|
|
@@ -26,6 +33,7 @@ async def get_elements() -> CommandResult:
|
|
|
26
33
|
),
|
|
27
34
|
)
|
|
28
35
|
except Exception as e:
|
|
36
|
+
log_progress(f"Error: {e}")
|
|
29
37
|
return CommandResult(
|
|
30
38
|
success=False,
|
|
31
39
|
command="elements",
|
|
@@ -37,8 +45,10 @@ async def get_elements() -> CommandResult:
|
|
|
37
45
|
async def get_text() -> CommandResult:
|
|
38
46
|
"""Get page text content."""
|
|
39
47
|
try:
|
|
48
|
+
log_progress("Getting text content...")
|
|
40
49
|
page = get_page()
|
|
41
50
|
text = await page.inner_text("body")
|
|
51
|
+
log_progress(f"Text length: {len(text)} chars")
|
|
42
52
|
|
|
43
53
|
await start_browser()
|
|
44
54
|
state = await get_current_state()
|
|
@@ -53,6 +63,7 @@ async def get_text() -> CommandResult:
|
|
|
53
63
|
),
|
|
54
64
|
)
|
|
55
65
|
except Exception as e:
|
|
66
|
+
log_progress(f"Error: {e}")
|
|
56
67
|
return CommandResult(
|
|
57
68
|
success=False,
|
|
58
69
|
command="text",
|
|
@@ -73,6 +84,7 @@ async def get_paragraphs(min_length: int = 200) -> CommandResult:
|
|
|
73
84
|
List of paragraphs with their text, length, and CSS selector
|
|
74
85
|
"""
|
|
75
86
|
try:
|
|
87
|
+
log_progress(f"Extracting paragraphs (min {min_length} chars)...")
|
|
76
88
|
page = get_page()
|
|
77
89
|
|
|
78
90
|
# Extract paragraphs and large text blocks using JavaScript
|
|
@@ -151,6 +163,7 @@ async def get_paragraphs(min_length: int = 200) -> CommandResult:
|
|
|
151
163
|
paragraphs = [
|
|
152
164
|
Paragraph(**p) for p in paragraphs_data[:50] # Limit to 50 paragraphs
|
|
153
165
|
]
|
|
166
|
+
log_progress(f"Found {len(paragraphs)} paragraphs")
|
|
154
167
|
|
|
155
168
|
await start_browser()
|
|
156
169
|
state = await get_current_state()
|
|
@@ -167,6 +180,7 @@ async def get_paragraphs(min_length: int = 200) -> CommandResult:
|
|
|
167
180
|
),
|
|
168
181
|
)
|
|
169
182
|
except Exception as e:
|
|
183
|
+
log_progress(f"Error: {e}")
|
|
170
184
|
return CommandResult(
|
|
171
185
|
success=False,
|
|
172
186
|
command="paragraphs",
|
|
@@ -178,8 +192,10 @@ async def get_paragraphs(min_length: int = 200) -> CommandResult:
|
|
|
178
192
|
async def get_html() -> CommandResult:
|
|
179
193
|
"""Get page HTML content."""
|
|
180
194
|
try:
|
|
195
|
+
log_progress("Getting HTML content...")
|
|
181
196
|
page = get_page()
|
|
182
197
|
html = await page.content()
|
|
198
|
+
log_progress(f"HTML length: {len(html)} chars")
|
|
183
199
|
|
|
184
200
|
await start_browser()
|
|
185
201
|
state = await get_current_state()
|
|
@@ -194,6 +210,7 @@ async def get_html() -> CommandResult:
|
|
|
194
210
|
),
|
|
195
211
|
)
|
|
196
212
|
except Exception as e:
|
|
213
|
+
log_progress(f"Error: {e}")
|
|
197
214
|
return CommandResult(
|
|
198
215
|
success=False,
|
|
199
216
|
command="html",
|
|
@@ -205,9 +222,11 @@ async def get_html() -> CommandResult:
|
|
|
205
222
|
async def get_screenshot() -> CommandResult:
|
|
206
223
|
"""Get page screenshot as base64."""
|
|
207
224
|
try:
|
|
225
|
+
log_progress("Taking screenshot...")
|
|
208
226
|
page = get_page()
|
|
209
227
|
screenshot_bytes = await page.screenshot()
|
|
210
228
|
screenshot_base64 = encode_screenshot(screenshot_bytes)
|
|
229
|
+
log_progress(f"Screenshot size: {len(screenshot_bytes)} bytes")
|
|
211
230
|
|
|
212
231
|
await start_browser()
|
|
213
232
|
state = await get_current_state()
|
|
@@ -222,6 +241,7 @@ async def get_screenshot() -> CommandResult:
|
|
|
222
241
|
),
|
|
223
242
|
)
|
|
224
243
|
except Exception as e:
|
|
244
|
+
log_progress(f"Error: {e}")
|
|
225
245
|
return CommandResult(
|
|
226
246
|
success=False,
|
|
227
247
|
command="screenshot",
|
|
@@ -233,8 +253,10 @@ async def get_screenshot() -> CommandResult:
|
|
|
233
253
|
async def get_state() -> CommandResult:
|
|
234
254
|
"""Get full page state."""
|
|
235
255
|
try:
|
|
256
|
+
log_progress("Getting page state...")
|
|
236
257
|
await start_browser()
|
|
237
258
|
state = await get_current_state()
|
|
259
|
+
log_progress(f"Page: {state.title} ({state.url})")
|
|
238
260
|
|
|
239
261
|
return CommandResult(
|
|
240
262
|
success=True,
|
|
@@ -246,6 +268,7 @@ async def get_state() -> CommandResult:
|
|
|
246
268
|
),
|
|
247
269
|
)
|
|
248
270
|
except Exception as e:
|
|
271
|
+
log_progress(f"Error: {e}")
|
|
249
272
|
return CommandResult(
|
|
250
273
|
success=False,
|
|
251
274
|
command="state",
|
|
@@ -257,8 +280,10 @@ async def get_state() -> CommandResult:
|
|
|
257
280
|
async def get_url() -> CommandResult:
|
|
258
281
|
"""Get current URL."""
|
|
259
282
|
try:
|
|
283
|
+
log_progress("Getting URL...")
|
|
260
284
|
page = get_page()
|
|
261
285
|
url = page.url
|
|
286
|
+
log_progress(f"URL: {url}")
|
|
262
287
|
|
|
263
288
|
await start_browser()
|
|
264
289
|
state = await get_current_state()
|
|
@@ -273,6 +298,7 @@ async def get_url() -> CommandResult:
|
|
|
273
298
|
),
|
|
274
299
|
)
|
|
275
300
|
except Exception as e:
|
|
301
|
+
log_progress(f"Error: {e}")
|
|
276
302
|
return CommandResult(
|
|
277
303
|
success=False,
|
|
278
304
|
command="url",
|
|
@@ -284,8 +310,10 @@ async def get_url() -> CommandResult:
|
|
|
284
310
|
async def get_title() -> CommandResult:
|
|
285
311
|
"""Get page title."""
|
|
286
312
|
try:
|
|
313
|
+
log_progress("Getting title...")
|
|
287
314
|
page = get_page()
|
|
288
315
|
title = await page.title()
|
|
316
|
+
log_progress(f"Title: {title}")
|
|
289
317
|
|
|
290
318
|
await start_browser()
|
|
291
319
|
state = await get_current_state()
|
|
@@ -300,6 +328,7 @@ async def get_title() -> CommandResult:
|
|
|
300
328
|
),
|
|
301
329
|
)
|
|
302
330
|
except Exception as e:
|
|
331
|
+
log_progress(f"Error: {e}")
|
|
303
332
|
return CommandResult(
|
|
304
333
|
success=False,
|
|
305
334
|
command="title",
|
|
@@ -314,6 +343,7 @@ async def evaluate(expr: str) -> CommandResult:
|
|
|
314
343
|
Usage: /query/evaluate?expr=<js_expression>
|
|
315
344
|
"""
|
|
316
345
|
try:
|
|
346
|
+
log_progress(f"Evaluating JS: {expr[:50]}...")
|
|
317
347
|
await start_browser()
|
|
318
348
|
page = get_page()
|
|
319
349
|
|
|
@@ -331,6 +361,7 @@ async def evaluate(expr: str) -> CommandResult:
|
|
|
331
361
|
),
|
|
332
362
|
)
|
|
333
363
|
except Exception as e:
|
|
364
|
+
log_progress(f"Error: {e}")
|
|
334
365
|
return CommandResult(
|
|
335
366
|
success=False,
|
|
336
367
|
command="evaluate",
|
|
@@ -342,6 +373,7 @@ async def evaluate(expr: str) -> CommandResult:
|
|
|
342
373
|
async def get_links() -> CommandResult:
|
|
343
374
|
"""Get all links on the page."""
|
|
344
375
|
try:
|
|
376
|
+
log_progress("Extracting links...")
|
|
345
377
|
await start_browser()
|
|
346
378
|
page = get_page()
|
|
347
379
|
|
|
@@ -352,6 +384,7 @@ async def get_links() -> CommandResult:
|
|
|
352
384
|
visible: a.offsetParent !== null
|
|
353
385
|
}))
|
|
354
386
|
""")
|
|
387
|
+
log_progress(f"Found {len(links)} links")
|
|
355
388
|
|
|
356
389
|
state = await get_current_state()
|
|
357
390
|
|
|
@@ -365,6 +398,7 @@ async def get_links() -> CommandResult:
|
|
|
365
398
|
),
|
|
366
399
|
)
|
|
367
400
|
except Exception as e:
|
|
401
|
+
log_progress(f"Error: {e}")
|
|
368
402
|
return CommandResult(
|
|
369
403
|
success=False,
|
|
370
404
|
command="links",
|
|
@@ -376,6 +410,7 @@ async def get_links() -> CommandResult:
|
|
|
376
410
|
async def get_forms() -> CommandResult:
|
|
377
411
|
"""Get all forms on the page."""
|
|
378
412
|
try:
|
|
413
|
+
log_progress("Extracting forms...")
|
|
379
414
|
await start_browser()
|
|
380
415
|
page = get_page()
|
|
381
416
|
|
|
@@ -391,6 +426,7 @@ async def get_forms() -> CommandResult:
|
|
|
391
426
|
}))
|
|
392
427
|
}))
|
|
393
428
|
""")
|
|
429
|
+
log_progress(f"Found {len(forms)} forms")
|
|
394
430
|
|
|
395
431
|
state = await get_current_state()
|
|
396
432
|
|
|
@@ -404,6 +440,7 @@ async def get_forms() -> CommandResult:
|
|
|
404
440
|
),
|
|
405
441
|
)
|
|
406
442
|
except Exception as e:
|
|
443
|
+
log_progress(f"Error: {e}")
|
|
407
444
|
return CommandResult(
|
|
408
445
|
success=False,
|
|
409
446
|
command="forms",
|
|
@@ -415,6 +452,7 @@ async def get_forms() -> CommandResult:
|
|
|
415
452
|
async def get_tables() -> CommandResult:
|
|
416
453
|
"""Get all tables on the page with their data."""
|
|
417
454
|
try:
|
|
455
|
+
log_progress("Extracting tables...")
|
|
418
456
|
await start_browser()
|
|
419
457
|
page = get_page()
|
|
420
458
|
|
|
@@ -444,6 +482,7 @@ async def get_tables() -> CommandResult:
|
|
|
444
482
|
total_rows = sum(t['rowCount'] for t in tables)
|
|
445
483
|
# Add tables to state
|
|
446
484
|
state.tables = tables
|
|
485
|
+
log_progress(f"Found {len(tables)} tables, {total_rows} rows total")
|
|
447
486
|
|
|
448
487
|
return CommandResult(
|
|
449
488
|
success=True,
|
|
@@ -455,6 +494,7 @@ async def get_tables() -> CommandResult:
|
|
|
455
494
|
),
|
|
456
495
|
)
|
|
457
496
|
except Exception as e:
|
|
497
|
+
log_progress(f"Error: {e}")
|
|
458
498
|
return CommandResult(
|
|
459
499
|
success=False,
|
|
460
500
|
command="tables",
|
navcli/server/routes/session.py
CHANGED
|
@@ -21,6 +21,11 @@ _NAVCLI_SESSIONS_DIR = os.path.join(_NAVCLI_HOME, "sessions")
|
|
|
21
21
|
router = APIRouter()
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def log_progress(message: str):
|
|
25
|
+
"""Print progress message."""
|
|
26
|
+
print(f"[PROGRESS] {message}")
|
|
27
|
+
|
|
28
|
+
|
|
24
29
|
class CookieSetRequest(BaseModel):
|
|
25
30
|
"""Request body for setting a cookie."""
|
|
26
31
|
name: str
|
|
@@ -40,7 +45,9 @@ async def query_cookies() -> CommandResult:
|
|
|
40
45
|
Returns list of cookies with name, value, domain, path, etc.
|
|
41
46
|
"""
|
|
42
47
|
try:
|
|
48
|
+
log_progress("Getting cookies...")
|
|
43
49
|
cookies = await get_cookies()
|
|
50
|
+
log_progress(f"Found {len(cookies)} cookies")
|
|
44
51
|
|
|
45
52
|
return CommandResult(
|
|
46
53
|
success=True,
|
|
@@ -51,6 +58,7 @@ async def query_cookies() -> CommandResult:
|
|
|
51
58
|
),
|
|
52
59
|
)
|
|
53
60
|
except Exception as e:
|
|
61
|
+
log_progress(f"Error: {e}")
|
|
54
62
|
return CommandResult(
|
|
55
63
|
success=False,
|
|
56
64
|
command="cookies",
|
|
@@ -65,6 +73,7 @@ async def set_cookie_route(request: CookieSetRequest) -> CommandResult:
|
|
|
65
73
|
Usage: POST /cmd/cookies/set with JSON body
|
|
66
74
|
"""
|
|
67
75
|
try:
|
|
76
|
+
log_progress(f"Setting cookie: {request.name}")
|
|
68
77
|
await set_cookie(
|
|
69
78
|
name=request.name,
|
|
70
79
|
value=request.value,
|
|
@@ -85,6 +94,7 @@ async def set_cookie_route(request: CookieSetRequest) -> CommandResult:
|
|
|
85
94
|
),
|
|
86
95
|
)
|
|
87
96
|
except Exception as e:
|
|
97
|
+
log_progress(f"Error: {e}")
|
|
88
98
|
return CommandResult(
|
|
89
99
|
success=False,
|
|
90
100
|
command="set_cookie",
|
|
@@ -99,7 +109,9 @@ async def clear_cookies_route() -> CommandResult:
|
|
|
99
109
|
Returns the number of cookies that were cleared.
|
|
100
110
|
"""
|
|
101
111
|
try:
|
|
112
|
+
log_progress("Clearing cookies...")
|
|
102
113
|
count = await clear_cookies()
|
|
114
|
+
log_progress(f"Cleared {count} cookies")
|
|
103
115
|
|
|
104
116
|
return CommandResult(
|
|
105
117
|
success=True,
|
|
@@ -110,6 +122,7 @@ async def clear_cookies_route() -> CommandResult:
|
|
|
110
122
|
),
|
|
111
123
|
)
|
|
112
124
|
except Exception as e:
|
|
125
|
+
log_progress(f"Error: {e}")
|
|
113
126
|
return CommandResult(
|
|
114
127
|
success=False,
|
|
115
128
|
command="clear_cookies",
|
|
@@ -139,10 +152,12 @@ async def save_session_route(
|
|
|
139
152
|
else:
|
|
140
153
|
save_path = os.path.join(_NAVCLI_SESSIONS_DIR, "session.json")
|
|
141
154
|
|
|
155
|
+
log_progress(f"Saving session to {save_path}...")
|
|
142
156
|
storage_state = await save_session(save_path)
|
|
143
157
|
|
|
144
158
|
cookie_count = len(storage_state.get("cookies", []))
|
|
145
159
|
origin_count = len(storage_state.get("origins", []))
|
|
160
|
+
log_progress(f"Saved: {cookie_count} cookies, {origin_count} origins")
|
|
146
161
|
|
|
147
162
|
return CommandResult(
|
|
148
163
|
success=True,
|
|
@@ -153,6 +168,7 @@ async def save_session_route(
|
|
|
153
168
|
),
|
|
154
169
|
)
|
|
155
170
|
except Exception as e:
|
|
171
|
+
log_progress(f"Error: {e}")
|
|
156
172
|
return CommandResult(
|
|
157
173
|
success=False,
|
|
158
174
|
command="save_session",
|
|
@@ -182,7 +198,9 @@ async def load_session_route(
|
|
|
182
198
|
load_path = os.path.join(_NAVCLI_SESSIONS_DIR, "session.json")
|
|
183
199
|
|
|
184
200
|
try:
|
|
201
|
+
log_progress(f"Loading session from {load_path}...")
|
|
185
202
|
await load_session(load_path)
|
|
203
|
+
log_progress("Session loaded")
|
|
186
204
|
|
|
187
205
|
return CommandResult(
|
|
188
206
|
success=True,
|
|
@@ -193,12 +211,14 @@ async def load_session_route(
|
|
|
193
211
|
),
|
|
194
212
|
)
|
|
195
213
|
except FileNotFoundError:
|
|
214
|
+
log_progress(f"Session file not found: {load_path}")
|
|
196
215
|
return CommandResult(
|
|
197
216
|
success=False,
|
|
198
217
|
command="load_session",
|
|
199
218
|
error=f"Session file not found: {load_path}",
|
|
200
219
|
)
|
|
201
220
|
except Exception as e:
|
|
221
|
+
log_progress(f"Error: {e}")
|
|
202
222
|
return CommandResult(
|
|
203
223
|
success=False,
|
|
204
224
|
command="load_session",
|
|
@@ -14,17 +14,17 @@ navcli/core/models/dom.py,sha256=vZzEpzLEOlCnUJWBWx0-gmHj8WBxorsnPsZJgUW3UOE,116
|
|
|
14
14
|
navcli/core/models/element.py,sha256=6L8PnY7h_-_MFweq6NU6gdG1g5_r_AHlW1bYTuUBNoM,747
|
|
15
15
|
navcli/core/models/feedback.py,sha256=K67nc7fCiAAfeYKDppNEnHtM41fSdF-Ai7IMhvAv7PY,689
|
|
16
16
|
navcli/core/models/state.py,sha256=6TBh6zMX8uhBcFICNLFgJhPKLRbET47fjZTjvt3n0Hw,1002
|
|
17
|
-
navcli/server/__init__.py,sha256=
|
|
18
|
-
navcli/server/app.py,sha256=
|
|
19
|
-
navcli/server/browser.py,sha256=
|
|
17
|
+
navcli/server/__init__.py,sha256=eyUU2kDdtwRwHqhBTJ3vpqp86WAMpBpsnhbniALSa3E,4299
|
|
18
|
+
navcli/server/app.py,sha256=lMMXuJzB-7FOGfCXJosoMRV0mP51O2pF24KZDdC4p20,2015
|
|
19
|
+
navcli/server/browser.py,sha256=OneOVyd3yQnVSGiY0EU6WZ2RrR0QJa8FbZlWAF6eTF8,12244
|
|
20
20
|
navcli/server/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
21
|
navcli/server/routes/__init__.py,sha256=LCd1lCcBCqJmFchr9OTvKJvCTQNLETAljdPaou-fd3I,180
|
|
22
|
-
navcli/server/routes/control.py,sha256=
|
|
23
|
-
navcli/server/routes/explore.py,sha256=
|
|
24
|
-
navcli/server/routes/interaction.py,sha256=
|
|
25
|
-
navcli/server/routes/navigation.py,sha256=
|
|
26
|
-
navcli/server/routes/query.py,sha256=
|
|
27
|
-
navcli/server/routes/session.py,sha256=
|
|
22
|
+
navcli/server/routes/control.py,sha256=Hn1giy5H-PdpiwY-AiKsFWDNfIYk0erDxbMeksBGqEY,1337
|
|
23
|
+
navcli/server/routes/explore.py,sha256=3OwNp8vx2fwTdcAXeI2NqBXJ1R9fxQWqoLYPQawS-3Y,13734
|
|
24
|
+
navcli/server/routes/interaction.py,sha256=8FYGA5yqsJ045oMtsVCJpCsj4ANlcOCTs0Xs8oM4pvA,10194
|
|
25
|
+
navcli/server/routes/navigation.py,sha256=U23EtgUFy6FWacwWovrXgwb1PRhp5u1txn1Mt0o7o6I,4277
|
|
26
|
+
navcli/server/routes/query.py,sha256=icjQoBFGvsbM4PHfejYT15or4YdxlSR0IBYlWHcD9K8,15291
|
|
27
|
+
navcli/server/routes/session.py,sha256=GgJOem4p-YFqDiITWFC89t1pq9SQ0guY6AI9lMFBFZs,6307
|
|
28
28
|
navcli/utils/__init__.py,sha256=g3CIjq_xybpmanja9A7oyQMtf9emS6qdHJIJ6BJx6t0,521
|
|
29
29
|
navcli/utils/image.py,sha256=e30zNrOd3wi9UWesiQZJ5n62wIOfrGZnJFCMFKnFOD0,681
|
|
30
30
|
navcli/utils/js.py,sha256=-OenTrj4Nh4s-zjUvjJnN3XSncX8b03tNrgY_nhPRxo,334
|
|
@@ -32,8 +32,8 @@ navcli/utils/selector.py,sha256=E_8E5bjjL1gDn524l3bH3XzK3egn9IyoYEb7zXsTwTQ,2146
|
|
|
32
32
|
navcli/utils/text.py,sha256=wCFCKCn6Ibi8GjAeGXBHT5CfS8BCs7X9qVxDq4eR1tE,389
|
|
33
33
|
navcli/utils/time.py,sha256=bAjJ59rp02MkIfoCT1TzmFd_oGaGlOI-5RPiKSqT-kI,1181
|
|
34
34
|
navcli/utils/url.py,sha256=UG71w8ArYJlqjl2XslwAN-Kk9GJOPK3k5fg6CxXE5js,301
|
|
35
|
-
navcli-0.2.
|
|
36
|
-
navcli-0.2.
|
|
37
|
-
navcli-0.2.
|
|
38
|
-
navcli-0.2.
|
|
39
|
-
navcli-0.2.
|
|
35
|
+
navcli-0.2.3.dist-info/METADATA,sha256=2P48jfzjZt4soPHSXEpsp4EZTyiyaxwvnLsA-wlTVPo,2837
|
|
36
|
+
navcli-0.2.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
37
|
+
navcli-0.2.3.dist-info/entry_points.txt,sha256=J26RwbEYUO5UbuhH0aXF_tvQFCOCatAR80blaSn5oWk,72
|
|
38
|
+
navcli-0.2.3.dist-info/top_level.txt,sha256=dwTN5Lw7STNP3zhL2-RDElX3EslzM2sbYYLfuszQSO4,7
|
|
39
|
+
navcli-0.2.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|