webtap-tool 0.4.0__py3-none-any.whl → 0.5.1__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.
Potentially problematic release.
This version of webtap-tool might be problematic. Click here for more details.
- webtap/api.py +318 -9
- webtap/app.py +12 -5
- webtap/cdp/session.py +101 -1
- webtap/commands/DEVELOPER_GUIDE.md +108 -22
- webtap/commands/TIPS.md +24 -1
- webtap/commands/_builders.py +139 -1
- webtap/commands/body.py +1 -2
- webtap/commands/connection.py +1 -2
- webtap/commands/console.py +1 -2
- webtap/commands/events.py +1 -2
- webtap/commands/fetch.py +1 -2
- webtap/commands/inspect.py +1 -2
- webtap/commands/javascript.py +61 -28
- webtap/commands/navigation.py +1 -2
- webtap/commands/network.py +17 -35
- webtap/commands/selections.py +129 -0
- webtap/commands/server.py +1 -0
- webtap/services/dom.py +512 -0
- webtap/services/main.py +14 -0
- {webtap_tool-0.4.0.dist-info → webtap_tool-0.5.1.dist-info}/METADATA +1 -1
- {webtap_tool-0.4.0.dist-info → webtap_tool-0.5.1.dist-info}/RECORD +23 -22
- webtap/commands/_errors.py +0 -108
- {webtap_tool-0.4.0.dist-info → webtap_tool-0.5.1.dist-info}/WHEEL +0 -0
- {webtap_tool-0.4.0.dist-info → webtap_tool-0.5.1.dist-info}/entry_points.txt +0 -0
|
@@ -173,7 +173,7 @@ def command(state, param: int = 0)
|
|
|
173
173
|
@app.command(display="markdown", fastmcp={"type": "resource", "mime_type": "text/markdown"})
|
|
174
174
|
def pages(state) -> dict:
|
|
175
175
|
"""List available pages."""
|
|
176
|
-
return
|
|
176
|
+
return table_response(
|
|
177
177
|
title="Chrome Pages",
|
|
178
178
|
headers=["Index", "Title", "URL"],
|
|
179
179
|
rows=rows,
|
|
@@ -187,7 +187,7 @@ def pages(state) -> dict:
|
|
|
187
187
|
def navigate(state, url: str) -> dict:
|
|
188
188
|
"""Navigate to URL."""
|
|
189
189
|
# Perform action
|
|
190
|
-
return
|
|
190
|
+
return info_response(
|
|
191
191
|
title="Navigation Complete",
|
|
192
192
|
fields={"URL": url, "Status": "Success"}
|
|
193
193
|
)
|
|
@@ -195,37 +195,75 @@ def navigate(state, url: str) -> dict:
|
|
|
195
195
|
|
|
196
196
|
## Error Handling
|
|
197
197
|
|
|
198
|
-
Always use the error utilities from `
|
|
198
|
+
Always use the error utilities from `_builders.py`:
|
|
199
199
|
|
|
200
200
|
```python
|
|
201
|
-
from webtap.commands.
|
|
201
|
+
from webtap.commands._builders import check_connection, error_response
|
|
202
202
|
|
|
203
203
|
def my_command(state, ...):
|
|
204
204
|
# Check connection first for commands that need it
|
|
205
205
|
if error := check_connection(state):
|
|
206
206
|
return error
|
|
207
|
-
|
|
207
|
+
|
|
208
208
|
# Validate parameters
|
|
209
209
|
if not valid:
|
|
210
|
-
return error_response("
|
|
211
|
-
|
|
212
|
-
#
|
|
213
|
-
return error_response("
|
|
210
|
+
return error_response("Parameter X must be Y", suggestions=["Try this", "Or that"])
|
|
211
|
+
|
|
212
|
+
# Simple errors
|
|
213
|
+
return error_response("Specific error message")
|
|
214
214
|
```
|
|
215
215
|
|
|
216
|
-
##
|
|
216
|
+
## Response Builders
|
|
217
217
|
|
|
218
|
-
Use
|
|
218
|
+
Use builders from `_builders.py`:
|
|
219
219
|
|
|
220
220
|
```python
|
|
221
|
-
from webtap.commands.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
221
|
+
from webtap.commands._builders import (
|
|
222
|
+
# Table responses
|
|
223
|
+
table_response, # Tables with headers, warnings, tips
|
|
224
|
+
|
|
225
|
+
# Info displays
|
|
226
|
+
info_response, # Key-value pairs with optional heading
|
|
227
|
+
|
|
228
|
+
# Status responses
|
|
229
|
+
error_response, # Errors with suggestions
|
|
230
|
+
success_response, # Success messages with details
|
|
231
|
+
warning_response, # Warnings with suggestions
|
|
232
|
+
|
|
233
|
+
# Code results
|
|
234
|
+
code_result_response, # Code execution with result display
|
|
235
|
+
code_response, # Simple code block display
|
|
236
|
+
|
|
237
|
+
# Connection helpers
|
|
238
|
+
check_connection, # Helper for CDP connection validation
|
|
239
|
+
check_fetch_enabled, # Helper for fetch interception validation
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Usage Examples
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
# Table with tips
|
|
247
|
+
return table_response(
|
|
248
|
+
title="Network Requests",
|
|
249
|
+
headers=["ID", "URL", "Status"],
|
|
250
|
+
rows=rows,
|
|
251
|
+
summary=f"{len(rows)} requests",
|
|
252
|
+
tips=["Use body(ID) to fetch response body"]
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Code execution result
|
|
256
|
+
return code_result_response(
|
|
257
|
+
"JavaScript Result",
|
|
258
|
+
code="2 + 2",
|
|
259
|
+
language="javascript",
|
|
260
|
+
result=4
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Error with suggestions
|
|
264
|
+
return error_response(
|
|
265
|
+
"Not connected to Chrome",
|
|
266
|
+
suggestions=["Run pages()", "Use connect(0)"]
|
|
229
267
|
)
|
|
230
268
|
```
|
|
231
269
|
|
|
@@ -300,15 +338,63 @@ Use explicit text instead of symbols for clarity:
|
|
|
300
338
|
4. **MCP mode**: Test with `webtap --mcp` command
|
|
301
339
|
5. **Markdown rendering**: Verify output displays correctly
|
|
302
340
|
|
|
341
|
+
## TIPS.md Integration
|
|
342
|
+
|
|
343
|
+
All commands should be documented in `TIPS.md` for consistent help and MCP descriptions:
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
from webtap.commands._tips import get_tips, get_mcp_description
|
|
347
|
+
|
|
348
|
+
# Get MCP description (for fastmcp metadata)
|
|
349
|
+
mcp_desc = get_mcp_description("mycommand")
|
|
350
|
+
|
|
351
|
+
@app.command(
|
|
352
|
+
display="markdown",
|
|
353
|
+
fastmcp={"type": "tool", "description": mcp_desc} if mcp_desc else {"type": "tool"}
|
|
354
|
+
)
|
|
355
|
+
def mycommand(state, param: str) -> dict:
|
|
356
|
+
"""My command description."""
|
|
357
|
+
# ... implementation ...
|
|
358
|
+
|
|
359
|
+
# Include tips in response
|
|
360
|
+
tips = get_tips("mycommand", context={"id": some_id})
|
|
361
|
+
return table_response(
|
|
362
|
+
headers=headers,
|
|
363
|
+
rows=rows,
|
|
364
|
+
tips=tips # Automatically shown as "Next Steps"
|
|
365
|
+
)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Adding to TIPS.md
|
|
369
|
+
|
|
370
|
+
Add a section for your command:
|
|
371
|
+
|
|
372
|
+
```markdown
|
|
373
|
+
### mycommand
|
|
374
|
+
Brief description of what the command does.
|
|
375
|
+
|
|
376
|
+
#### Examples
|
|
377
|
+
\```python
|
|
378
|
+
mycommand("param1") # Basic usage
|
|
379
|
+
mycommand("param2", flag=True) # With options
|
|
380
|
+
\```
|
|
381
|
+
|
|
382
|
+
#### Tips
|
|
383
|
+
- **Related command:** `other_command()` - does related thing
|
|
384
|
+
- **Advanced usage:** Use with `yet_another()` for X
|
|
385
|
+
- **Context aware:** Tips support {id} placeholders from context dict
|
|
386
|
+
```
|
|
387
|
+
|
|
303
388
|
## Checklist for New Commands
|
|
304
389
|
|
|
305
390
|
- [ ] Use `@app.command()` decorator with `display="markdown"`
|
|
306
391
|
- [ ] Add `fastmcp` metadata (type: "resource" or "tool")
|
|
307
392
|
- [ ] Use simple types only (no unions, no Optional)
|
|
308
393
|
- [ ] Add `# pyright: ignore[reportArgumentType]` for `dict = None`
|
|
309
|
-
- [ ] Import
|
|
310
|
-
- [ ] Use `
|
|
394
|
+
- [ ] Import builders from `_builders.py`
|
|
395
|
+
- [ ] Use `table_response()`, `info_response()`, or `code_result_response()`
|
|
311
396
|
- [ ] Check connection with `check_connection()` if needed
|
|
397
|
+
- [ ] Add command section to `TIPS.md` with examples and tips
|
|
398
|
+
- [ ] Use `get_tips()` to show tips in response
|
|
312
399
|
- [ ] Document parameters clearly in docstring
|
|
313
|
-
- [ ] Provide usage examples in docstring
|
|
314
400
|
- [ ] Test in both REPL and MCP modes
|
webtap/commands/TIPS.md
CHANGED
|
@@ -150,4 +150,27 @@ Show paused requests and responses.
|
|
|
150
150
|
- **Analyze body:** `body({id})` - fetch and examine response body
|
|
151
151
|
- **Resume request:** `resume({id})` - continue the request
|
|
152
152
|
- **Modify request:** `resume({id}, modifications={'url': '...'})`
|
|
153
|
-
- **Fail request:** `fail({id}, 'BlockedByClient')` - block the request
|
|
153
|
+
- **Fail request:** `fail({id}, 'BlockedByClient')` - block the request
|
|
154
|
+
|
|
155
|
+
### selections
|
|
156
|
+
Browser element selections with prompt and analysis.
|
|
157
|
+
|
|
158
|
+
Access selected DOM elements and their properties via Python expressions. Elements are selected using the Chrome extension's selection mode.
|
|
159
|
+
|
|
160
|
+
#### Examples
|
|
161
|
+
```python
|
|
162
|
+
selections() # View all selections
|
|
163
|
+
selections(expr="data['prompt']") # Get prompt text
|
|
164
|
+
selections(expr="data['selections']['1']") # Get element #1 data
|
|
165
|
+
selections(expr="data['selections']['1']['styles']") # Get styles
|
|
166
|
+
selections(expr="len(data['selections'])") # Count selections
|
|
167
|
+
selections(expr="{k: v['selector'] for k, v in data['selections'].items()}") # All selectors
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### Tips
|
|
171
|
+
- **Extract HTML:** `selections(expr="data['selections']['1']['outerHTML']")` - get element HTML
|
|
172
|
+
- **Get CSS selector:** `selections(expr="data['selections']['1']['selector']")` - unique selector
|
|
173
|
+
- **Use with js():** `js("element.offsetWidth", selection=1)` - integrate with JavaScript execution
|
|
174
|
+
- **Access styles:** `selections(expr="data['selections']['1']['styles']['display']")` - computed CSS
|
|
175
|
+
- **Get attributes:** `selections(expr="data['selections']['1']['preview']")` - tag, id, classes
|
|
176
|
+
- **Inspect in prompts:** Use `@webtap:webtap://selections` resource in Claude Code for AI analysis
|
webtap/commands/_builders.py
CHANGED
|
@@ -1,4 +1,36 @@
|
|
|
1
|
-
"""Response builders using ReplKit2 v0.10.0+ markdown elements.
|
|
1
|
+
"""Response builders using ReplKit2 v0.10.0+ markdown elements.
|
|
2
|
+
|
|
3
|
+
USAGE GUIDELINES:
|
|
4
|
+
|
|
5
|
+
Use builders for:
|
|
6
|
+
✅ Simple responses (error, info, success, warning)
|
|
7
|
+
✅ Tables with standard format
|
|
8
|
+
✅ Code execution results
|
|
9
|
+
✅ Repeated patterns across commands
|
|
10
|
+
|
|
11
|
+
Use manual building for:
|
|
12
|
+
❌ Complex multi-section layouts (>20 lines)
|
|
13
|
+
❌ Conditional sections with deep nesting
|
|
14
|
+
❌ Custom workflows (wizards, dashboards)
|
|
15
|
+
❌ Dual-mode resource views with tutorials
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
- network() - Simple table → Use table_response()
|
|
19
|
+
- javascript() - Code result → Use code_result_response()
|
|
20
|
+
- server() - Custom dashboard → Manual OK
|
|
21
|
+
- selections() resource mode - Tutorial layout → Manual OK
|
|
22
|
+
|
|
23
|
+
Available builders:
|
|
24
|
+
- table_response() - Tables with headers, warnings, tips
|
|
25
|
+
- info_response() - Key-value pairs with optional heading
|
|
26
|
+
- error_response() - Errors with suggestions
|
|
27
|
+
- success_response() - Success messages with details
|
|
28
|
+
- warning_response() - Warnings with suggestions
|
|
29
|
+
- check_connection() - Helper for CDP connection validation
|
|
30
|
+
- check_fetch_enabled() - Helper for fetch interception validation
|
|
31
|
+
- code_result_response() - Code execution with result display
|
|
32
|
+
- code_response() - Simple code block display
|
|
33
|
+
"""
|
|
2
34
|
|
|
3
35
|
from typing import Any
|
|
4
36
|
|
|
@@ -125,3 +157,109 @@ def warning_response(message: str, suggestions: list[str] | None = None) -> dict
|
|
|
125
157
|
elements.append({"type": "list", "items": suggestions})
|
|
126
158
|
|
|
127
159
|
return {"elements": elements}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# Connection helpers
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def check_connection(state):
|
|
166
|
+
"""Check CDP connection, return error response if not connected.
|
|
167
|
+
|
|
168
|
+
Returns error_response() if not connected, None otherwise.
|
|
169
|
+
Use pattern: `if error := check_connection(state): return error`
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
state: Application state containing CDP session.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Error response dict if not connected, None if connected.
|
|
176
|
+
"""
|
|
177
|
+
if not (state.cdp and state.cdp.is_connected):
|
|
178
|
+
return error_response(
|
|
179
|
+
"Not connected to Chrome",
|
|
180
|
+
suggestions=[
|
|
181
|
+
"Run `pages()` to see available tabs",
|
|
182
|
+
"Use `connect(0)` to connect to first tab",
|
|
183
|
+
"Or `connect(page_id='...')` for specific tab",
|
|
184
|
+
],
|
|
185
|
+
)
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def check_fetch_enabled(state):
|
|
190
|
+
"""Check fetch interception, return error response if not enabled.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
state: Application state containing fetch service.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Error response dict if not enabled, None if enabled.
|
|
197
|
+
"""
|
|
198
|
+
if not state.service.fetch.enabled:
|
|
199
|
+
return error_response(
|
|
200
|
+
"Fetch interception not enabled", suggestions=["Enable with `fetch('enable')` to pause requests"]
|
|
201
|
+
)
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# Code result builders
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def code_result_response(
|
|
209
|
+
title: str,
|
|
210
|
+
code: str,
|
|
211
|
+
language: str,
|
|
212
|
+
result: Any = None,
|
|
213
|
+
) -> dict:
|
|
214
|
+
"""Build code execution result display.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
title: Result heading (e.g. "JavaScript Result")
|
|
218
|
+
code: Source code executed
|
|
219
|
+
language: Syntax highlighting language
|
|
220
|
+
result: Execution result (supports dict/list/str/None)
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Markdown response with code and result
|
|
224
|
+
"""
|
|
225
|
+
import json
|
|
226
|
+
|
|
227
|
+
elements = [
|
|
228
|
+
{"type": "heading", "content": title, "level": 2},
|
|
229
|
+
{"type": "code_block", "content": code, "language": language},
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
if result is not None:
|
|
233
|
+
if isinstance(result, (dict, list)):
|
|
234
|
+
elements.append({"type": "code_block", "content": json.dumps(result, indent=2), "language": "json"})
|
|
235
|
+
else:
|
|
236
|
+
elements.append({"type": "text", "content": f"**Result:** `{result}`"})
|
|
237
|
+
else:
|
|
238
|
+
elements.append({"type": "text", "content": "**Result:** _(no return value)_"})
|
|
239
|
+
|
|
240
|
+
return {"elements": elements}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def code_response(
|
|
244
|
+
content: str,
|
|
245
|
+
language: str = "",
|
|
246
|
+
title: str | None = None,
|
|
247
|
+
) -> dict:
|
|
248
|
+
"""Build simple code block response.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
content: Code content to display
|
|
252
|
+
language: Syntax highlighting language
|
|
253
|
+
title: Optional heading above code block
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Markdown response with code block
|
|
257
|
+
"""
|
|
258
|
+
elements = []
|
|
259
|
+
|
|
260
|
+
if title:
|
|
261
|
+
elements.append({"type": "heading", "content": title, "level": 2})
|
|
262
|
+
|
|
263
|
+
elements.append({"type": "code_block", "content": content, "language": language})
|
|
264
|
+
|
|
265
|
+
return {"elements": elements}
|
webtap/commands/body.py
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
import json
|
|
4
4
|
from webtap.app import app
|
|
5
5
|
from webtap.commands._utils import evaluate_expression, format_expression_result
|
|
6
|
-
from webtap.commands.
|
|
7
|
-
from webtap.commands._builders import info_response, error_response
|
|
6
|
+
from webtap.commands._builders import check_connection, info_response, error_response
|
|
8
7
|
from webtap.commands._tips import get_mcp_description
|
|
9
8
|
|
|
10
9
|
|
webtap/commands/connection.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""Chrome browser connection management commands."""
|
|
2
2
|
|
|
3
3
|
from webtap.app import app
|
|
4
|
-
from webtap.commands.
|
|
5
|
-
from webtap.commands._builders import info_response, table_response, error_response
|
|
4
|
+
from webtap.commands._builders import check_connection, info_response, table_response, error_response
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
@app.command(display="markdown", fastmcp={"type": "tool"})
|
webtap/commands/console.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""Browser console message monitoring and display commands."""
|
|
2
2
|
|
|
3
3
|
from webtap.app import app
|
|
4
|
-
from webtap.commands._builders import table_response
|
|
5
|
-
from webtap.commands._errors import check_connection
|
|
4
|
+
from webtap.commands._builders import check_connection, table_response
|
|
6
5
|
from webtap.commands._tips import get_tips
|
|
7
6
|
|
|
8
7
|
|
webtap/commands/events.py
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from webtap.app import app
|
|
4
4
|
from webtap.cdp import build_query
|
|
5
|
-
from webtap.commands.
|
|
6
|
-
from webtap.commands._builders import table_response
|
|
5
|
+
from webtap.commands._builders import check_connection, table_response
|
|
7
6
|
from webtap.commands._tips import get_tips, get_mcp_description
|
|
8
7
|
|
|
9
8
|
|
webtap/commands/fetch.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""HTTP fetch request interception and debugging commands."""
|
|
2
2
|
|
|
3
3
|
from webtap.app import app
|
|
4
|
-
from webtap.commands.
|
|
5
|
-
from webtap.commands._builders import error_response, info_response, table_response
|
|
4
|
+
from webtap.commands._builders import check_connection, error_response, info_response, table_response
|
|
6
5
|
from webtap.commands._tips import get_tips
|
|
7
6
|
|
|
8
7
|
|
webtap/commands/inspect.py
CHANGED
|
@@ -4,8 +4,7 @@ import json
|
|
|
4
4
|
|
|
5
5
|
from webtap.app import app
|
|
6
6
|
from webtap.commands._utils import evaluate_expression, format_expression_result
|
|
7
|
-
from webtap.commands.
|
|
8
|
-
from webtap.commands._builders import error_response
|
|
7
|
+
from webtap.commands._builders import check_connection, error_response
|
|
9
8
|
from webtap.commands._tips import get_mcp_description
|
|
10
9
|
|
|
11
10
|
|
webtap/commands/javascript.py
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"""JavaScript code execution in browser context."""
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
3
|
from webtap.app import app
|
|
5
|
-
from webtap.commands.
|
|
6
|
-
from webtap.commands._builders import info_response, error_response
|
|
4
|
+
from webtap.commands._builders import check_connection, info_response, error_response, code_result_response
|
|
7
5
|
from webtap.commands._tips import get_mcp_description
|
|
8
6
|
|
|
9
7
|
|
|
@@ -17,35 +15,86 @@ mcp_desc = get_mcp_description("js")
|
|
|
17
15
|
},
|
|
18
16
|
fastmcp={"type": "tool", "description": mcp_desc} if mcp_desc else {"type": "tool"},
|
|
19
17
|
)
|
|
20
|
-
def js(
|
|
21
|
-
|
|
18
|
+
def js(
|
|
19
|
+
state,
|
|
20
|
+
code: str,
|
|
21
|
+
selection: int = None,
|
|
22
|
+
persist: bool = False,
|
|
23
|
+
wait_return: bool = True,
|
|
24
|
+
await_promise: bool = False,
|
|
25
|
+
) -> dict: # pyright: ignore[reportArgumentType]
|
|
26
|
+
"""Execute JavaScript in the browser with optional element selection.
|
|
22
27
|
|
|
23
28
|
Args:
|
|
24
|
-
code: JavaScript code to execute
|
|
29
|
+
code: JavaScript code to execute (use 'element' variable if selection provided)
|
|
30
|
+
selection: Browser element selection number (e.g., 1 for #1) - makes element available
|
|
31
|
+
persist: Keep variables in global scope across calls (default: False, uses fresh scope)
|
|
25
32
|
wait_return: Wait for and return result (default: True)
|
|
26
33
|
await_promise: Await promises before returning (default: False)
|
|
27
34
|
|
|
28
35
|
Examples:
|
|
36
|
+
# Default: fresh scope (no redeclaration errors)
|
|
37
|
+
js("const x = 1") # ✓ x isolated
|
|
38
|
+
js("const x = 2") # ✓ No error, fresh scope
|
|
29
39
|
js("document.title") # Get page title
|
|
30
|
-
js("document.body.innerText.length") # Get text length
|
|
31
|
-
js("console.log('test')", wait_return=False) # Fire and forget
|
|
32
40
|
js("[...document.links].map(a => a.href)") # Get all links
|
|
33
41
|
|
|
42
|
+
# With browser element selection (always fresh scope)
|
|
43
|
+
js("element.offsetWidth", selection=1) # Use element #1
|
|
44
|
+
js("element.classList", selection=2) # Use element #2
|
|
45
|
+
js("element.getBoundingClientRect()", selection=1)
|
|
46
|
+
|
|
47
|
+
# Persistent scope: keep variables across calls
|
|
48
|
+
js("var data = fetch('/api')", persist=True) # data persists
|
|
49
|
+
js("data.json()", persist=True) # Access data from previous call
|
|
50
|
+
|
|
34
51
|
# Async operations
|
|
35
52
|
js("fetch('/api').then(r => r.json())", await_promise=True)
|
|
36
53
|
|
|
37
54
|
# DOM manipulation (no return needed)
|
|
38
55
|
js("document.querySelectorAll('.ad').forEach(e => e.remove())", wait_return=False)
|
|
39
56
|
|
|
40
|
-
# Install interceptors
|
|
41
|
-
js("window.fetch = new Proxy(window.fetch, {get: (t, p) => console.log(p)})", wait_return=False)
|
|
42
|
-
|
|
43
57
|
Returns:
|
|
44
58
|
The evaluated result if wait_return=True, otherwise execution status
|
|
45
59
|
"""
|
|
46
60
|
if error := check_connection(state):
|
|
47
61
|
return error
|
|
48
62
|
|
|
63
|
+
# Handle browser element selection
|
|
64
|
+
if selection is not None:
|
|
65
|
+
# Check if browser data exists
|
|
66
|
+
if not hasattr(state, "browser_data") or not state.browser_data:
|
|
67
|
+
return error_response(
|
|
68
|
+
"No browser selections available",
|
|
69
|
+
suggestions=[
|
|
70
|
+
"Use browser() to select elements first",
|
|
71
|
+
"Or omit the selection parameter to run code directly",
|
|
72
|
+
],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Get the jsPath for the selected element
|
|
76
|
+
selections = state.browser_data.get("selections", {})
|
|
77
|
+
sel_key = str(selection)
|
|
78
|
+
|
|
79
|
+
if sel_key not in selections:
|
|
80
|
+
available = ", ".join(selections.keys()) if selections else "none"
|
|
81
|
+
return error_response(
|
|
82
|
+
f"Selection #{selection} not found",
|
|
83
|
+
suggestions=[f"Available selections: {available}", "Use browser() to see all selections"],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
js_path = selections[sel_key].get("jsPath")
|
|
87
|
+
if not js_path:
|
|
88
|
+
return error_response(f"Selection #{selection} has no jsPath")
|
|
89
|
+
|
|
90
|
+
# Wrap code with element variable in fresh scope (IIFE)
|
|
91
|
+
# Selection always uses fresh scope to avoid element redeclaration errors
|
|
92
|
+
code = f"(() => {{ const element = {js_path}; return ({code}); }})()"
|
|
93
|
+
elif not persist:
|
|
94
|
+
# Default: wrap in IIFE for fresh scope (avoids const/let redeclaration errors)
|
|
95
|
+
code = f"(() => {{ return ({code}); }})()"
|
|
96
|
+
# else: persist=True, use code as-is (global scope)
|
|
97
|
+
|
|
49
98
|
result = state.cdp.execute(
|
|
50
99
|
"Runtime.evaluate", {"expression": code, "returnByValue": wait_return, "awaitPromise": await_promise}
|
|
51
100
|
)
|
|
@@ -60,23 +109,7 @@ def js(state, code: str, wait_return: bool = True, await_promise: bool = False)
|
|
|
60
109
|
# Return based on wait_return flag
|
|
61
110
|
if wait_return:
|
|
62
111
|
value = result.get("result", {}).get("value")
|
|
63
|
-
|
|
64
|
-
# Format the result in markdown
|
|
65
|
-
elements = [
|
|
66
|
-
{"type": "heading", "content": "JavaScript Result", "level": 2},
|
|
67
|
-
{"type": "code_block", "content": code, "language": "javascript"}, # Full code
|
|
68
|
-
]
|
|
69
|
-
|
|
70
|
-
# Add the result
|
|
71
|
-
if value is not None:
|
|
72
|
-
if isinstance(value, (dict, list)):
|
|
73
|
-
elements.append({"type": "code_block", "content": json.dumps(value, indent=2), "language": "json"})
|
|
74
|
-
else:
|
|
75
|
-
elements.append({"type": "text", "content": f"**Result:** `{value}`"})
|
|
76
|
-
else:
|
|
77
|
-
elements.append({"type": "text", "content": "**Result:** _(no return value)_"})
|
|
78
|
-
|
|
79
|
-
return {"elements": elements}
|
|
112
|
+
return code_result_response("JavaScript Result", code, "javascript", result=value)
|
|
80
113
|
else:
|
|
81
114
|
return info_response(
|
|
82
115
|
title="JavaScript Execution",
|
webtap/commands/navigation.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""Browser page navigation and history commands."""
|
|
2
2
|
|
|
3
3
|
from webtap.app import app
|
|
4
|
-
from webtap.commands.
|
|
5
|
-
from webtap.commands._builders import info_response, table_response, error_response
|
|
4
|
+
from webtap.commands._builders import check_connection, info_response, table_response, error_response
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
@app.command(display="markdown", fastmcp={"type": "tool"})
|
webtap/commands/network.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from typing import List
|
|
4
4
|
|
|
5
5
|
from webtap.app import app
|
|
6
|
-
from webtap.commands.
|
|
6
|
+
from webtap.commands._builders import check_connection, table_response
|
|
7
7
|
from webtap.commands._tips import get_tips
|
|
8
8
|
|
|
9
9
|
|
|
@@ -68,40 +68,22 @@ def network(state, limit: int = 20, filters: List[str] = None, no_filters: bool
|
|
|
68
68
|
if limit and len(results) == limit:
|
|
69
69
|
warnings.append(f"Showing first {limit} results (use limit parameter to see more)")
|
|
70
70
|
|
|
71
|
-
# Get tips from TIPS.md with context
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
tips = get_tips("network", context={"id": example_id})
|
|
76
|
-
|
|
77
|
-
# Build response elements manually to add filter tip
|
|
78
|
-
elements = []
|
|
79
|
-
elements.append({"type": "heading", "content": "Network Requests", "level": 2})
|
|
80
|
-
|
|
81
|
-
for warning in warnings or []:
|
|
82
|
-
elements.append({"type": "alert", "message": warning, "level": "warning"})
|
|
71
|
+
# Get tips from TIPS.md with context, and add filter guidance
|
|
72
|
+
combined_tips = [
|
|
73
|
+
"Reduce noise with `filters()` - filter by type (XHR, Fetch) or domain (*/api/*)",
|
|
74
|
+
]
|
|
83
75
|
|
|
84
76
|
if rows:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"type": "alert",
|
|
98
|
-
"message": "Tip: Reduce noise with filters by type or domain. Example: `filters('update', {'category': 'api-only', 'mode': 'include', 'types': ['XHR', 'Fetch'], 'domains': ['*/api/*', '*/graphql/*']})`",
|
|
99
|
-
"level": "info",
|
|
100
|
-
}
|
|
77
|
+
example_id = rows[0]["ID"]
|
|
78
|
+
context_tips = get_tips("network", context={"id": example_id})
|
|
79
|
+
if context_tips:
|
|
80
|
+
combined_tips.extend(context_tips)
|
|
81
|
+
|
|
82
|
+
return table_response(
|
|
83
|
+
title="Network Requests",
|
|
84
|
+
headers=["ID", "ReqID", "Method", "Status", "URL", "Type", "Size"],
|
|
85
|
+
rows=rows,
|
|
86
|
+
summary=f"{len(rows)} requests" if rows else None,
|
|
87
|
+
warnings=warnings,
|
|
88
|
+
tips=combined_tips,
|
|
101
89
|
)
|
|
102
|
-
|
|
103
|
-
if tips:
|
|
104
|
-
elements.append({"type": "heading", "content": "Next Steps", "level": 3})
|
|
105
|
-
elements.append({"type": "list", "items": tips})
|
|
106
|
-
|
|
107
|
-
return {"elements": elements}
|