navcli 0.2.2__tar.gz → 0.2.3__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 (45) hide show
  1. {navcli-0.2.2 → navcli-0.2.3}/PKG-INFO +1 -1
  2. {navcli-0.2.2 → navcli-0.2.3}/navcli/server/app.py +25 -1
  3. {navcli-0.2.2 → navcli-0.2.3}/navcli/server/routes/control.py +9 -0
  4. {navcli-0.2.2 → navcli-0.2.3}/navcli/server/routes/explore.py +39 -0
  5. {navcli-0.2.2 → navcli-0.2.3}/navcli/server/routes/interaction.py +40 -0
  6. {navcli-0.2.2 → navcli-0.2.3}/navcli/server/routes/navigation.py +22 -0
  7. {navcli-0.2.2 → navcli-0.2.3}/navcli/server/routes/query.py +40 -0
  8. {navcli-0.2.2 → navcli-0.2.3}/navcli/server/routes/session.py +20 -0
  9. {navcli-0.2.2 → navcli-0.2.3}/navcli.egg-info/PKG-INFO +1 -1
  10. {navcli-0.2.2 → navcli-0.2.3}/pyproject.toml +1 -1
  11. {navcli-0.2.2 → navcli-0.2.3}/README.md +0 -0
  12. {navcli-0.2.2 → navcli-0.2.3}/docs/README.md +0 -0
  13. {navcli-0.2.2 → navcli-0.2.3}/navcli/cli/__init__.py +0 -0
  14. {navcli-0.2.2 → navcli-0.2.3}/navcli/cli/app.py +0 -0
  15. {navcli-0.2.2 → navcli-0.2.3}/navcli/cli/client.py +0 -0
  16. {navcli-0.2.2 → navcli-0.2.3}/navcli/cli/commands/__init__.py +0 -0
  17. {navcli-0.2.2 → navcli-0.2.3}/navcli/cli/commands/base.py +0 -0
  18. {navcli-0.2.2 → navcli-0.2.3}/navcli/cli/commands/control.py +0 -0
  19. {navcli-0.2.2 → navcli-0.2.3}/navcli/cli/commands/explore.py +0 -0
  20. {navcli-0.2.2 → navcli-0.2.3}/navcli/cli/commands/interaction.py +0 -0
  21. {navcli-0.2.2 → navcli-0.2.3}/navcli/cli/commands/navigation.py +0 -0
  22. {navcli-0.2.2 → navcli-0.2.3}/navcli/cli/commands/query.py +0 -0
  23. {navcli-0.2.2 → navcli-0.2.3}/navcli/core/__init__.py +0 -0
  24. {navcli-0.2.2 → navcli-0.2.3}/navcli/core/models/__init__.py +0 -0
  25. {navcli-0.2.2 → navcli-0.2.3}/navcli/core/models/dom.py +0 -0
  26. {navcli-0.2.2 → navcli-0.2.3}/navcli/core/models/element.py +0 -0
  27. {navcli-0.2.2 → navcli-0.2.3}/navcli/core/models/feedback.py +0 -0
  28. {navcli-0.2.2 → navcli-0.2.3}/navcli/core/models/state.py +0 -0
  29. {navcli-0.2.2 → navcli-0.2.3}/navcli/server/__init__.py +0 -0
  30. {navcli-0.2.2 → navcli-0.2.3}/navcli/server/browser.py +0 -0
  31. {navcli-0.2.2 → navcli-0.2.3}/navcli/server/middleware/__init__.py +0 -0
  32. {navcli-0.2.2 → navcli-0.2.3}/navcli/server/routes/__init__.py +0 -0
  33. {navcli-0.2.2 → navcli-0.2.3}/navcli/utils/__init__.py +0 -0
  34. {navcli-0.2.2 → navcli-0.2.3}/navcli/utils/image.py +0 -0
  35. {navcli-0.2.2 → navcli-0.2.3}/navcli/utils/js.py +0 -0
  36. {navcli-0.2.2 → navcli-0.2.3}/navcli/utils/selector.py +0 -0
  37. {navcli-0.2.2 → navcli-0.2.3}/navcli/utils/text.py +0 -0
  38. {navcli-0.2.2 → navcli-0.2.3}/navcli/utils/time.py +0 -0
  39. {navcli-0.2.2 → navcli-0.2.3}/navcli/utils/url.py +0 -0
  40. {navcli-0.2.2 → navcli-0.2.3}/navcli.egg-info/SOURCES.txt +0 -0
  41. {navcli-0.2.2 → navcli-0.2.3}/navcli.egg-info/dependency_links.txt +0 -0
  42. {navcli-0.2.2 → navcli-0.2.3}/navcli.egg-info/entry_points.txt +0 -0
  43. {navcli-0.2.2 → navcli-0.2.3}/navcli.egg-info/requires.txt +0 -0
  44. {navcli-0.2.2 → navcli-0.2.3}/navcli.egg-info/top_level.txt +0 -0
  45. {navcli-0.2.2 → navcli-0.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: navcli
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: 可交互、可探索的浏览器命令行工具,专为 AI Agent 设计
5
5
  Author: NavCLI Team
6
6
  License: MIT
@@ -1,6 +1,7 @@
1
1
  """FastAPI application for NavCLI Browser Server."""
2
2
 
3
- from fastapi import FastAPI
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")
@@ -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()
@@ -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",
@@ -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",
@@ -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",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: navcli
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: 可交互、可探索的浏览器命令行工具,专为 AI Agent 设计
5
5
  Author: NavCLI Team
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "navcli"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "可交互、可探索的浏览器命令行工具,专为 AI Agent 设计"
9
9
  readme = "docs/README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes