webtap-tool 0.7.0__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of webtap-tool might be problematic. Click here for more details.
- webtap/__init__.py +4 -0
- webtap/api.py +50 -57
- webtap/app.py +5 -0
- webtap/cdp/session.py +166 -27
- webtap/commands/TIPS.md +125 -22
- webtap/commands/_builders.py +7 -1
- webtap/commands/_code_generation.py +110 -0
- webtap/commands/body.py +9 -5
- webtap/commands/connection.py +21 -0
- webtap/commands/javascript.py +13 -25
- webtap/commands/navigation.py +5 -0
- webtap/commands/quicktype.py +268 -0
- webtap/commands/to_model.py +23 -75
- webtap/services/body.py +209 -24
- webtap/services/dom.py +19 -12
- webtap/services/fetch.py +19 -0
- webtap/services/main.py +194 -2
- webtap/services/state_snapshot.py +88 -0
- {webtap_tool-0.7.0.dist-info → webtap_tool-0.8.0.dist-info}/METADATA +1 -1
- {webtap_tool-0.7.0.dist-info → webtap_tool-0.8.0.dist-info}/RECORD +22 -19
- {webtap_tool-0.7.0.dist-info → webtap_tool-0.8.0.dist-info}/WHEEL +0 -0
- {webtap_tool-0.7.0.dist-info → webtap_tool-0.8.0.dist-info}/entry_points.txt +0 -0
webtap/commands/TIPS.md
CHANGED
|
@@ -12,13 +12,21 @@ All commands have these pre-imported (no imports needed!):
|
|
|
12
12
|
## Commands
|
|
13
13
|
|
|
14
14
|
### body
|
|
15
|
-
Fetch and analyze HTTP response bodies with Python expressions.
|
|
15
|
+
Fetch and analyze HTTP request or response bodies with Python expressions. Automatically detects event type.
|
|
16
16
|
|
|
17
17
|
#### Examples
|
|
18
18
|
```python
|
|
19
|
-
|
|
20
|
-
body(123
|
|
19
|
+
# Response bodies
|
|
20
|
+
body(123) # Get response body
|
|
21
|
+
body(123, "json.loads(body)") # Parse JSON response
|
|
21
22
|
body(123, "bs4(body, 'html.parser').find('title').text") # HTML title
|
|
23
|
+
|
|
24
|
+
# Request bodies (POST/PUT/PATCH)
|
|
25
|
+
body(456) # Get request POST data
|
|
26
|
+
body(456, "json.loads(body)") # Parse JSON request body
|
|
27
|
+
body(456, "json.loads(body)['customerId']") # Extract request field
|
|
28
|
+
|
|
29
|
+
# Analysis
|
|
22
30
|
body(123, "jwt.decode(body, options={'verify_signature': False})") # Decode JWT
|
|
23
31
|
body(123, "re.findall(r'/api/[^\"\\s]+', body)[:10]") # Find API endpoints
|
|
24
32
|
body(123, "httpx.get(json.loads(body)['next_url']).json()") # Chain requests
|
|
@@ -26,7 +34,10 @@ body(123, "msgpack.unpackb(body)") # Binary formats
|
|
|
26
34
|
```
|
|
27
35
|
|
|
28
36
|
#### Tips
|
|
37
|
+
- **Auto-detect type:** Command automatically detects request vs response events
|
|
38
|
+
- **Find request events:** `events({"method": "Network.requestWillBeSent", "url": "*WsJobCard/Post*"})` - POST requests
|
|
29
39
|
- **Generate models:** `to_model({id}, "models/model.py", "Model")` - create Pydantic models from JSON
|
|
40
|
+
- **Generate types:** `quicktype({id}, "types.ts", "Type")` - TypeScript/other languages
|
|
30
41
|
- **Chain requests:** `body({id}, "httpx.get(json.loads(body)['next_url']).text[:100]")`
|
|
31
42
|
- **Parse XML:** `body({id}, "ElementTree.fromstring(body).find('.//title').text")`
|
|
32
43
|
- **Extract forms:** `body({id}, "[f['action'] for f in bs4(body, 'html.parser').find_all('form')]")`
|
|
@@ -34,22 +45,62 @@ body(123, "msgpack.unpackb(body)") # Binary formats
|
|
|
34
45
|
- **Find related:** `events({'requestId': request_id})` - related events
|
|
35
46
|
|
|
36
47
|
### to_model
|
|
37
|
-
Generate Pydantic v2 models from
|
|
48
|
+
Generate Pydantic v2 models from request or response bodies.
|
|
38
49
|
|
|
39
50
|
#### Examples
|
|
40
51
|
```python
|
|
41
|
-
|
|
42
|
-
to_model(123, "models/
|
|
43
|
-
to_model(123, "/
|
|
52
|
+
# Response bodies
|
|
53
|
+
to_model(123, "models/product.py", "Product") # Full response
|
|
54
|
+
to_model(123, "models/customers/group.py", "CustomerGroup", json_path="data[0]") # Extract nested
|
|
55
|
+
|
|
56
|
+
# Request bodies (POST/PUT/PATCH)
|
|
57
|
+
to_model(172, "models/form.py", "JobCardForm", expr="dict(urllib.parse.parse_qsl(body))") # Form data
|
|
58
|
+
to_model(180, "models/request.py", "CreateOrder") # JSON POST body
|
|
59
|
+
|
|
60
|
+
# Advanced transformations
|
|
61
|
+
to_model(123, "models/clean.py", "Clean", expr="{k: v for k, v in json.loads(body).items() if k != 'meta'}")
|
|
62
|
+
to_model(123, "models/merged.py", "Merged", expr="{**json.loads(body), 'url': event['params']['response']['url']}")
|
|
44
63
|
```
|
|
45
64
|
|
|
46
65
|
#### Tips
|
|
47
|
-
- **Check structure
|
|
48
|
-
- **
|
|
49
|
-
- **
|
|
50
|
-
- **
|
|
51
|
-
- **
|
|
52
|
-
- **
|
|
66
|
+
- **Check structure:** `body({id})` - preview body before generating
|
|
67
|
+
- **Find requests:** `events({"method": "Network.requestWillBeSent", "url": "*api/orders*"})` - locate POST events
|
|
68
|
+
- **Form data:** `expr="dict(urllib.parse.parse_qsl(body))"` for application/x-www-form-urlencoded
|
|
69
|
+
- **Nested extraction:** `json_path="data[0]"` for JSON with wrapper objects
|
|
70
|
+
- **Custom transforms:** `expr` has `body` (str) and `event` (dict) variables available
|
|
71
|
+
- **Organization:** Paths like `"models/customers/group.py"` create directory structure automatically
|
|
72
|
+
- **Field mapping:** Add `Field(alias="...")` after generation for API field names
|
|
73
|
+
|
|
74
|
+
### quicktype
|
|
75
|
+
Generate types from request or response bodies. Supports TypeScript, Go, Rust, Python, and 10+ other languages.
|
|
76
|
+
|
|
77
|
+
#### Examples
|
|
78
|
+
```python
|
|
79
|
+
# Response bodies
|
|
80
|
+
quicktype(123, "types/User.ts", "User") # TypeScript
|
|
81
|
+
quicktype(123, "api.go", "ApiResponse") # Go struct
|
|
82
|
+
quicktype(123, "schema.json", "Schema") # JSON Schema
|
|
83
|
+
quicktype(123, "types.ts", "User", json_path="data[0]") # Extract nested
|
|
84
|
+
|
|
85
|
+
# Request bodies (POST/PUT/PATCH)
|
|
86
|
+
quicktype(172, "types/JobCard.ts", "JobCardForm", expr="dict(urllib.parse.parse_qsl(body))") # Form data
|
|
87
|
+
quicktype(180, "types/CreateOrder.ts", "CreateOrderRequest") # JSON POST body
|
|
88
|
+
|
|
89
|
+
# Advanced options
|
|
90
|
+
quicktype(123, "types.ts", "User", options={"readonly": True}) # TypeScript readonly
|
|
91
|
+
quicktype(123, "types.ts", "Clean", expr="{k: v for k, v in json.loads(body).items() if k != 'meta'}")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### Tips
|
|
95
|
+
- **Check structure:** `body({id})` - preview body before generating
|
|
96
|
+
- **Find requests:** `events({"method": "Network.requestWillBeSent", "url": "*api*"})` - locate POST events
|
|
97
|
+
- **Form data:** `expr="dict(urllib.parse.parse_qsl(body))"` for application/x-www-form-urlencoded
|
|
98
|
+
- **Nested extraction:** `json_path="data[0]"` for JSON with wrapper objects
|
|
99
|
+
- **Languages:** .ts/.go/.rs/.java/.kt/.swift/.cs/.cpp/.dart/.rb/.json extensions set language
|
|
100
|
+
- **Options:** Dict keys map to CLI flags: `{"readonly": True}` → `--readonly`, `{"nice_property_names": True}` → `--nice-property-names`. See `quicktype --help` for language-specific flags
|
|
101
|
+
- **Common options:** TypeScript: `{"readonly": True, "prefer_types": True}`, Go: `{"omit_empty": True}`, Python: `{"pydantic_base_model": True}`
|
|
102
|
+
- **Install:** `npm install -g quicktype` if command not found
|
|
103
|
+
- **Pydantic models:** Use `to_model({id}, "models/model.py", "Model")` for Pydantic v2 instead
|
|
53
104
|
|
|
54
105
|
### inspect
|
|
55
106
|
Inspect CDP events with full Python debugging.
|
|
@@ -123,25 +174,54 @@ events({"level": "error"}) # Console errors
|
|
|
123
174
|
- **Decode data:** `inspect({id}, "base64.b64decode(data.get('params', {}).get('body', ''))")`
|
|
124
175
|
|
|
125
176
|
### js
|
|
126
|
-
Execute JavaScript in the browser
|
|
177
|
+
Execute JavaScript in the browser. Uses fresh scope by default to avoid redeclaration errors.
|
|
178
|
+
|
|
179
|
+
#### Scope Behavior
|
|
180
|
+
**Default (fresh scope)** - Each call runs in isolation:
|
|
181
|
+
```python
|
|
182
|
+
js("const x = 1") # ✓ x isolated
|
|
183
|
+
js("const x = 2") # ✓ No error, fresh scope
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Persistent scope** - Variables survive across calls:
|
|
187
|
+
```python
|
|
188
|
+
js("var data = {count: 0}", persist=True) # data persists
|
|
189
|
+
js("data.count++", persist=True) # Modifies data
|
|
190
|
+
js("data.count", persist=True) # Returns 1
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**With browser element** - Always fresh scope:
|
|
194
|
+
```python
|
|
195
|
+
js("element.offsetWidth", selection=1) # Use element #1
|
|
196
|
+
js("element.classList", selection=2) # Use element #2
|
|
197
|
+
```
|
|
127
198
|
|
|
128
199
|
#### Examples
|
|
129
200
|
```python
|
|
201
|
+
# Basic queries
|
|
130
202
|
js("document.title") # Get page title
|
|
131
|
-
js("document.body.innerText.length") # Get text length
|
|
132
203
|
js("[...document.links].map(a => a.href)") # Get all links
|
|
133
|
-
js("
|
|
204
|
+
js("document.body.innerText.length") # Text length
|
|
205
|
+
|
|
206
|
+
# Async operations
|
|
207
|
+
js("fetch('/api').then(r => r.json())", await_promise=True)
|
|
208
|
+
|
|
209
|
+
# DOM manipulation (no return)
|
|
134
210
|
js("document.querySelectorAll('.ad').forEach(e => e.remove())", wait_return=False)
|
|
135
|
-
|
|
211
|
+
|
|
212
|
+
# Persistent state for multi-step operations
|
|
213
|
+
js("var apiData = null", persist=True)
|
|
214
|
+
js("fetch('/api').then(r => r.json()).then(d => apiData = d)", persist=True, await_promise=True)
|
|
215
|
+
js("apiData.users.length", persist=True)
|
|
136
216
|
```
|
|
137
217
|
|
|
138
218
|
#### Tips
|
|
139
|
-
- **
|
|
140
|
-
- **
|
|
141
|
-
- **
|
|
142
|
-
- **
|
|
143
|
-
- **Hook fetch:** `js("window.fetch = new Proxy(fetch, {apply: (t, _, a) => {console.log(a); return t(...a)}})", wait_return=False)`
|
|
219
|
+
- **Fresh scope:** Default behavior prevents const/let redeclaration errors
|
|
220
|
+
- **Persistent state:** Use `persist=True` for multi-step operations or global hooks
|
|
221
|
+
- **No return needed:** Set `wait_return=False` for DOM manipulation or hooks
|
|
222
|
+
- **Browser selections:** Use `selection=N` with browser() to operate on selected elements
|
|
144
223
|
- **Check console:** `console()` - see logged messages from JS execution
|
|
224
|
+
- **Hook fetch:** `js("window.fetch = new Proxy(fetch, {apply: (t, _, a) => {console.log(a); return t(...a)}})", persist=True, wait_return=False)`
|
|
145
225
|
|
|
146
226
|
### fetch
|
|
147
227
|
Control request interception for debugging and modification.
|
|
@@ -172,6 +252,29 @@ Show paused requests and responses.
|
|
|
172
252
|
- **Modify request:** `resume({id}, modifications={'url': '...'})`
|
|
173
253
|
- **Fail request:** `fail({id}, 'BlockedByClient')` - block the request
|
|
174
254
|
|
|
255
|
+
### page
|
|
256
|
+
Get current page information and navigate.
|
|
257
|
+
|
|
258
|
+
#### Tips
|
|
259
|
+
- **Navigate:** `navigate("https://example.com")` - go to URL
|
|
260
|
+
- **Reload:** `reload()` or `reload(ignore_cache=True)` - refresh page
|
|
261
|
+
- **History:** `back()`, `forward()`, `history()` - navigate history
|
|
262
|
+
- **Execute JS:** `js("document.title")` - run JavaScript in page
|
|
263
|
+
- **Monitor traffic:** `network()` - see requests, `console()` - see messages
|
|
264
|
+
- **Switch page:** `pages()` then `connect(page=N)` - change to another tab
|
|
265
|
+
- **Full status:** `status()` - connection details and event count
|
|
266
|
+
|
|
267
|
+
### pages
|
|
268
|
+
List available Chrome pages and manage connections.
|
|
269
|
+
|
|
270
|
+
#### Tips
|
|
271
|
+
- **Connect to page:** `connect(page={index})` - connect by index number
|
|
272
|
+
- **Connect by ID:** `connect(page_id="{page_id}")` - stable across tab reordering
|
|
273
|
+
- **Switch pages:** Just call `connect()` again - no need to disconnect first
|
|
274
|
+
- **Check status:** `status()` - see current connection and event count
|
|
275
|
+
- **Reconnect:** If connection lost, select page and `connect()` again
|
|
276
|
+
- **Find page:** Look for title/URL in table - index stays consistent
|
|
277
|
+
|
|
175
278
|
### selections
|
|
176
279
|
Browser element selections with prompt and analysis.
|
|
177
280
|
|
webtap/commands/_builders.py
CHANGED
|
@@ -22,7 +22,7 @@ Examples:
|
|
|
22
22
|
|
|
23
23
|
Available builders:
|
|
24
24
|
- table_response() - Tables with headers, warnings, tips
|
|
25
|
-
- info_response() - Key-value pairs with optional heading
|
|
25
|
+
- info_response() - Key-value pairs with optional heading and tips
|
|
26
26
|
- error_response() - Errors with suggestions
|
|
27
27
|
- success_response() - Success messages with details
|
|
28
28
|
- warning_response() - Warnings with suggestions
|
|
@@ -83,6 +83,7 @@ def info_response(
|
|
|
83
83
|
title: str | None = None,
|
|
84
84
|
fields: dict | None = None,
|
|
85
85
|
extra: str | None = None,
|
|
86
|
+
tips: list[str] | None = None,
|
|
86
87
|
) -> dict:
|
|
87
88
|
"""Build info display with key-value pairs.
|
|
88
89
|
|
|
@@ -90,6 +91,7 @@ def info_response(
|
|
|
90
91
|
title: Optional info title
|
|
91
92
|
fields: Dict of field names to values
|
|
92
93
|
extra: Optional extra content (raw markdown)
|
|
94
|
+
tips: Optional developer tips/guidance
|
|
93
95
|
"""
|
|
94
96
|
elements = []
|
|
95
97
|
|
|
@@ -107,6 +109,10 @@ def info_response(
|
|
|
107
109
|
if not elements:
|
|
108
110
|
elements.append({"type": "text", "content": "_No information available_"})
|
|
109
111
|
|
|
112
|
+
if tips:
|
|
113
|
+
elements.append({"type": "heading", "content": "Next Steps", "level": 3})
|
|
114
|
+
elements.append({"type": "list", "items": tips})
|
|
115
|
+
|
|
110
116
|
return {"elements": elements}
|
|
111
117
|
|
|
112
118
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Code generation utilities for transforming HTTP bodies into code.
|
|
2
|
+
|
|
3
|
+
Pure transformation functions with no dependencies on services or state.
|
|
4
|
+
Used by to_model(), quicktype(), and future code generation commands.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_json(content: str) -> tuple[Any, str | None]:
|
|
13
|
+
"""Parse JSON string into Python object.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
content: JSON string to parse.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Tuple of (parsed_data, error_message).
|
|
20
|
+
On success: (data, None)
|
|
21
|
+
On failure: (None, error_string)
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
data, error = parse_json('{"key": "value"}')
|
|
25
|
+
if error:
|
|
26
|
+
return error_response(error)
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
return json.loads(content), None
|
|
30
|
+
except json.JSONDecodeError as e:
|
|
31
|
+
return None, f"Invalid JSON: {e}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def extract_json_path(data: Any, path: str) -> tuple[Any, str | None]:
|
|
35
|
+
"""Extract nested data using simple bracket notation.
|
|
36
|
+
|
|
37
|
+
Supports paths like "data[0]", "results.users", or "data[0].items".
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
data: Dict or list to extract from.
|
|
41
|
+
path: Path using dot and bracket notation.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Tuple of (extracted_data, error_message).
|
|
45
|
+
On success: (data, None)
|
|
46
|
+
On failure: (None, error_string)
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
result, err = extract_json_path({"data": [1,2,3]}, "data[0]")
|
|
50
|
+
# result = 1, err = None
|
|
51
|
+
|
|
52
|
+
result, err = extract_json_path({"user": {"name": "Bob"}}, "user.name")
|
|
53
|
+
# result = "Bob", err = None
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
parts = path.replace("[", ".").replace("]", "").split(".")
|
|
57
|
+
result = data
|
|
58
|
+
for part in parts:
|
|
59
|
+
if part:
|
|
60
|
+
if part.isdigit():
|
|
61
|
+
result = result[int(part)]
|
|
62
|
+
else:
|
|
63
|
+
result = result[part]
|
|
64
|
+
return result, None
|
|
65
|
+
except (KeyError, IndexError, TypeError) as e:
|
|
66
|
+
return None, f"JSON path '{path}' not found: {e}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def validate_generation_data(data: Any) -> tuple[bool, str | None]:
|
|
70
|
+
"""Validate data structure for code generation.
|
|
71
|
+
|
|
72
|
+
Code generators (Pydantic, quicktype) require dict or list structures.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
data: Data to validate.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Tuple of (is_valid, error_message).
|
|
79
|
+
On success: (True, None)
|
|
80
|
+
On failure: (False, error_string)
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
is_valid, error = validate_generation_data({"key": "value"})
|
|
84
|
+
# is_valid = True, error = None
|
|
85
|
+
|
|
86
|
+
is_valid, error = validate_generation_data("string")
|
|
87
|
+
# is_valid = False, error = "Data is str, not dict or list"
|
|
88
|
+
"""
|
|
89
|
+
if not isinstance(data, (dict, list)):
|
|
90
|
+
return False, f"Data is {type(data).__name__}, not dict or list"
|
|
91
|
+
return True, None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def ensure_output_directory(output: str) -> Path:
|
|
95
|
+
"""Create output directory if needed, return resolved path.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
output: Output file path (can be relative, use ~, etc.).
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Resolved absolute Path object.
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
path = ensure_output_directory("~/models/user.py")
|
|
105
|
+
# Creates ~/models/ if it doesn't exist
|
|
106
|
+
# Returns Path("/home/user/models/user.py")
|
|
107
|
+
"""
|
|
108
|
+
output_path = Path(output).expanduser().resolve()
|
|
109
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
return output_path
|
webtap/commands/body.py
CHANGED
|
@@ -11,11 +11,15 @@ mcp_desc = get_mcp_description("body")
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
@app.command(display="markdown", fastmcp={"type": "tool", "description": mcp_desc} if mcp_desc else {"type": "tool"})
|
|
14
|
-
def body(state,
|
|
15
|
-
"""Fetch and analyze response body with Python expressions.
|
|
14
|
+
def body(state, event: int, expr: str = None, decode: bool = True, cache: bool = True) -> dict: # pyright: ignore[reportArgumentType]
|
|
15
|
+
"""Fetch and analyze request or response body with Python expressions.
|
|
16
|
+
|
|
17
|
+
Automatically detects event type and fetches appropriate body:
|
|
18
|
+
- Request events (Network.requestWillBeSent): POST/PUT/PATCH request body
|
|
19
|
+
- Response events (Network.responseReceived): response body
|
|
16
20
|
|
|
17
21
|
Args:
|
|
18
|
-
|
|
22
|
+
event: Event row ID from network(), events(), or requests()
|
|
19
23
|
expr: Optional Python expression with 'body' variable
|
|
20
24
|
decode: Auto-decode base64 (default: True)
|
|
21
25
|
cache: Use cached body (default: True)
|
|
@@ -28,7 +32,7 @@ def body(state, response: int, expr: str = None, decode: bool = True, cache: boo
|
|
|
28
32
|
|
|
29
33
|
# Get body from service (with optional caching)
|
|
30
34
|
body_service = state.service.body
|
|
31
|
-
result = body_service.
|
|
35
|
+
result = body_service.get_body(event, use_cache=cache)
|
|
32
36
|
|
|
33
37
|
if "error" in result:
|
|
34
38
|
return error_response(result["error"])
|
|
@@ -66,7 +70,7 @@ def body(state, response: int, expr: str = None, decode: bool = True, cache: boo
|
|
|
66
70
|
# Build markdown response with body in code block
|
|
67
71
|
# DATA-LEVEL TRUNCATION for memory/performance (as per refactor plan)
|
|
68
72
|
MAX_BODY_SIZE = 5000 # Keep data-level truncation for large bodies
|
|
69
|
-
elements = [{"type": "heading", "content": "
|
|
73
|
+
elements = [{"type": "heading", "content": "Body", "level": 2}]
|
|
70
74
|
|
|
71
75
|
# Try to detect content type and format appropriately
|
|
72
76
|
content_preview = body_content[:100]
|
webtap/commands/connection.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from webtap.app import app
|
|
4
4
|
from webtap.commands._builders import check_connection, info_response, table_response, error_response
|
|
5
|
+
from webtap.commands._tips import get_tips
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
@@ -132,12 +133,32 @@ def pages(state) -> dict:
|
|
|
132
133
|
for i, p in enumerate(pages_list)
|
|
133
134
|
]
|
|
134
135
|
|
|
136
|
+
# Get contextual tips
|
|
137
|
+
tips = None
|
|
138
|
+
if rows:
|
|
139
|
+
# Find connected page or first page
|
|
140
|
+
connected_row = next((r for r in rows if r["Connected"] == "Yes"), rows[0])
|
|
141
|
+
page_index = connected_row["Index"]
|
|
142
|
+
|
|
143
|
+
# Get page_id for the example page
|
|
144
|
+
connected_page = next((p for p in pages_list if str(pages_list.index(p)) == page_index), None)
|
|
145
|
+
page_id = connected_page.get("id", "")[:6] if connected_page else ""
|
|
146
|
+
|
|
147
|
+
tips = get_tips("pages", context={"index": page_index, "page_id": page_id})
|
|
148
|
+
|
|
149
|
+
# Build contextual warnings
|
|
150
|
+
warnings = []
|
|
151
|
+
if any(r["Connected"] == "Yes" for r in rows):
|
|
152
|
+
warnings.append("Already connected - call connect(page=N) to switch pages")
|
|
153
|
+
|
|
135
154
|
# Build markdown response
|
|
136
155
|
return table_response(
|
|
137
156
|
title="Chrome Pages",
|
|
138
157
|
headers=["Index", "Title", "URL", "ID", "Connected"],
|
|
139
158
|
rows=rows,
|
|
140
159
|
summary=f"{len(pages_list)} page{'s' if len(pages_list) != 1 else ''} available",
|
|
160
|
+
warnings=warnings if warnings else None,
|
|
161
|
+
tips=tips,
|
|
141
162
|
)
|
|
142
163
|
|
|
143
164
|
|
webtap/commands/javascript.py
CHANGED
|
@@ -23,39 +23,27 @@ def js(
|
|
|
23
23
|
wait_return: bool = True,
|
|
24
24
|
await_promise: bool = False,
|
|
25
25
|
) -> dict:
|
|
26
|
-
"""Execute JavaScript in the browser
|
|
26
|
+
"""Execute JavaScript in the browser.
|
|
27
|
+
|
|
28
|
+
Uses fresh scope by default to avoid redeclaration errors. Set persist=True
|
|
29
|
+
to keep variables across calls. Use selection=N to operate on browser elements.
|
|
27
30
|
|
|
28
31
|
Args:
|
|
29
|
-
code: JavaScript code to execute
|
|
30
|
-
selection: Browser element selection number
|
|
31
|
-
persist: Keep variables in global scope across calls (default: False
|
|
32
|
+
code: JavaScript code to execute
|
|
33
|
+
selection: Browser element selection number - makes 'element' variable available
|
|
34
|
+
persist: Keep variables in global scope across calls (default: False)
|
|
32
35
|
wait_return: Wait for and return result (default: True)
|
|
33
36
|
await_promise: Await promises before returning (default: False)
|
|
34
37
|
|
|
35
38
|
Examples:
|
|
36
|
-
#
|
|
37
|
-
js("
|
|
38
|
-
js("
|
|
39
|
-
js("
|
|
40
|
-
js("
|
|
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
|
-
|
|
51
|
-
# Async operations
|
|
52
|
-
js("fetch('/api').then(r => r.json())", await_promise=True)
|
|
53
|
-
|
|
54
|
-
# DOM manipulation (no return needed)
|
|
55
|
-
js("document.querySelectorAll('.ad').forEach(e => e.remove())", wait_return=False)
|
|
39
|
+
js("document.title") # Fresh scope (default)
|
|
40
|
+
js("var data = {count: 0}", persist=True) # Persistent state
|
|
41
|
+
js("element.offsetWidth", selection=1) # With browser element
|
|
42
|
+
js("fetch('/api')", await_promise=True) # Async operation
|
|
43
|
+
js("element.remove()", selection=1, wait_return=False) # No return needed
|
|
56
44
|
|
|
57
45
|
Returns:
|
|
58
|
-
|
|
46
|
+
Evaluated result if wait_return=True, otherwise execution status
|
|
59
47
|
"""
|
|
60
48
|
if error := check_connection(state):
|
|
61
49
|
return error
|
webtap/commands/navigation.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from webtap.app import app
|
|
4
4
|
from webtap.commands._builders import check_connection, info_response, table_response, error_response
|
|
5
|
+
from webtap.commands._tips import get_tips
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
@@ -146,6 +147,9 @@ def page(state) -> dict:
|
|
|
146
147
|
except Exception:
|
|
147
148
|
title = current.get("title", "")
|
|
148
149
|
|
|
150
|
+
# Get tips from TIPS.md
|
|
151
|
+
tips = get_tips("page")
|
|
152
|
+
|
|
149
153
|
# Build formatted response
|
|
150
154
|
return info_response(
|
|
151
155
|
title=title or "Untitled Page",
|
|
@@ -154,6 +158,7 @@ def page(state) -> dict:
|
|
|
154
158
|
"ID": current.get("id", ""),
|
|
155
159
|
"Type": current.get("transitionType", ""),
|
|
156
160
|
},
|
|
161
|
+
tips=tips,
|
|
157
162
|
)
|
|
158
163
|
|
|
159
164
|
return error_response("No navigation history available")
|