webtap-tool 0.11.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.
- webtap/VISION.md +246 -0
- webtap/__init__.py +84 -0
- webtap/__main__.py +6 -0
- webtap/api/__init__.py +9 -0
- webtap/api/app.py +26 -0
- webtap/api/models.py +69 -0
- webtap/api/server.py +111 -0
- webtap/api/sse.py +182 -0
- webtap/api/state.py +89 -0
- webtap/app.py +79 -0
- webtap/cdp/README.md +275 -0
- webtap/cdp/__init__.py +12 -0
- webtap/cdp/har.py +302 -0
- webtap/cdp/schema/README.md +41 -0
- webtap/cdp/schema/cdp_protocol.json +32785 -0
- webtap/cdp/schema/cdp_version.json +8 -0
- webtap/cdp/session.py +667 -0
- webtap/client.py +81 -0
- webtap/commands/DEVELOPER_GUIDE.md +401 -0
- webtap/commands/TIPS.md +269 -0
- webtap/commands/__init__.py +29 -0
- webtap/commands/_builders.py +331 -0
- webtap/commands/_code_generation.py +110 -0
- webtap/commands/_tips.py +147 -0
- webtap/commands/_utils.py +273 -0
- webtap/commands/connection.py +220 -0
- webtap/commands/console.py +87 -0
- webtap/commands/fetch.py +310 -0
- webtap/commands/filters.py +116 -0
- webtap/commands/javascript.py +73 -0
- webtap/commands/js_export.py +73 -0
- webtap/commands/launch.py +72 -0
- webtap/commands/navigation.py +197 -0
- webtap/commands/network.py +136 -0
- webtap/commands/quicktype.py +306 -0
- webtap/commands/request.py +93 -0
- webtap/commands/selections.py +138 -0
- webtap/commands/setup.py +219 -0
- webtap/commands/to_model.py +163 -0
- webtap/daemon.py +185 -0
- webtap/daemon_state.py +53 -0
- webtap/filters.py +219 -0
- webtap/rpc/__init__.py +14 -0
- webtap/rpc/errors.py +49 -0
- webtap/rpc/framework.py +223 -0
- webtap/rpc/handlers.py +625 -0
- webtap/rpc/machine.py +84 -0
- webtap/services/README.md +83 -0
- webtap/services/__init__.py +15 -0
- webtap/services/console.py +124 -0
- webtap/services/dom.py +547 -0
- webtap/services/fetch.py +415 -0
- webtap/services/main.py +392 -0
- webtap/services/network.py +401 -0
- webtap/services/setup/__init__.py +185 -0
- webtap/services/setup/chrome.py +233 -0
- webtap/services/setup/desktop.py +255 -0
- webtap/services/setup/extension.py +147 -0
- webtap/services/setup/platform.py +162 -0
- webtap/services/state_snapshot.py +86 -0
- webtap_tool-0.11.0.dist-info/METADATA +535 -0
- webtap_tool-0.11.0.dist-info/RECORD +64 -0
- webtap_tool-0.11.0.dist-info/WHEEL +4 -0
- webtap_tool-0.11.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Generate types and schemas from HTTP request/response bodies using quicktype."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from webtap.app import app
|
|
9
|
+
from webtap.commands._builders import success_response, error_response
|
|
10
|
+
from webtap.commands._code_generation import (
|
|
11
|
+
ensure_output_directory,
|
|
12
|
+
parse_json,
|
|
13
|
+
extract_json_path,
|
|
14
|
+
validate_generation_data,
|
|
15
|
+
)
|
|
16
|
+
from webtap.commands._utils import evaluate_expression, fetch_body_content
|
|
17
|
+
from webtap.commands._tips import get_mcp_description
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
mcp_desc = get_mcp_description("quicktype")
|
|
21
|
+
|
|
22
|
+
# Header template for generated files
|
|
23
|
+
HEADER_TEMPLATE = """Generated by WebTap from request {row_id}
|
|
24
|
+
Source: {url}
|
|
25
|
+
Method: {method}
|
|
26
|
+
Generated: {timestamp}
|
|
27
|
+
|
|
28
|
+
Do not edit manually."""
|
|
29
|
+
|
|
30
|
+
# Comment syntax per language
|
|
31
|
+
COMMENT_STYLES = {
|
|
32
|
+
"TypeScript": "//",
|
|
33
|
+
"Python": "#",
|
|
34
|
+
"Go": "//",
|
|
35
|
+
"Rust": "//",
|
|
36
|
+
"Java": "//",
|
|
37
|
+
"Kotlin": "//",
|
|
38
|
+
"Swift": "//",
|
|
39
|
+
"C#": "//",
|
|
40
|
+
"C++": "//",
|
|
41
|
+
"Dart": "//",
|
|
42
|
+
"Ruby": "#",
|
|
43
|
+
"JSON Schema": None,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _run_quicktype(
|
|
48
|
+
json_data: dict | list,
|
|
49
|
+
output: str,
|
|
50
|
+
type_name: str = None, # pyright: ignore[reportArgumentType]
|
|
51
|
+
just_types: bool = True,
|
|
52
|
+
prefer_types: bool = True,
|
|
53
|
+
options: dict = None, # pyright: ignore[reportArgumentType]
|
|
54
|
+
) -> tuple[bool, str]:
|
|
55
|
+
"""Run quicktype CLI with given parameters.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Tuple of (success: bool, error_message: str)
|
|
59
|
+
"""
|
|
60
|
+
if not shutil.which("quicktype"):
|
|
61
|
+
return False, "quicktype CLI not found. Install with: npm install -g quicktype"
|
|
62
|
+
|
|
63
|
+
output_path = Path(output)
|
|
64
|
+
ext = output_path.suffix.lower()
|
|
65
|
+
|
|
66
|
+
cmd = ["quicktype", "-o", str(output), "--src-lang", "json", "--top-level", type_name]
|
|
67
|
+
|
|
68
|
+
if just_types:
|
|
69
|
+
cmd.append("--just-types")
|
|
70
|
+
|
|
71
|
+
if ext in {".ts", ".tsx"} and prefer_types:
|
|
72
|
+
cmd.append("--prefer-types")
|
|
73
|
+
|
|
74
|
+
for key, val in (options or {}).items():
|
|
75
|
+
flag = f"--{key.replace('_', '-')}"
|
|
76
|
+
if val is True:
|
|
77
|
+
cmd.append(flag)
|
|
78
|
+
elif val is not False and val is not None:
|
|
79
|
+
cmd.extend([flag, str(val)])
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
subprocess.run(
|
|
83
|
+
cmd,
|
|
84
|
+
input=json.dumps(json_data, indent=2),
|
|
85
|
+
capture_output=True,
|
|
86
|
+
text=True,
|
|
87
|
+
check=True,
|
|
88
|
+
timeout=30,
|
|
89
|
+
)
|
|
90
|
+
return True, ""
|
|
91
|
+
except subprocess.CalledProcessError as e:
|
|
92
|
+
return False, f"quicktype failed: {e.stderr.strip() if e.stderr else str(e)}"
|
|
93
|
+
except subprocess.TimeoutExpired:
|
|
94
|
+
return False, "quicktype timed out (>30s)"
|
|
95
|
+
except Exception as e:
|
|
96
|
+
return False, f"Unexpected error: {str(e)}"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _insert_header(row_id: int, har_entry: dict, output_path: Path, language: str) -> None:
|
|
100
|
+
"""Insert language-aware header comment into generated file.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
row_id: HAR row ID for reference.
|
|
104
|
+
har_entry: HAR entry with request metadata.
|
|
105
|
+
output_path: Path to generated file.
|
|
106
|
+
language: Target language (e.g., "TypeScript", "Python").
|
|
107
|
+
"""
|
|
108
|
+
if not HEADER_TEMPLATE or language not in COMMENT_STYLES:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
comment_prefix = COMMENT_STYLES[language]
|
|
112
|
+
if not comment_prefix:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
request = har_entry.get("request", {})
|
|
117
|
+
metadata = {
|
|
118
|
+
"row_id": row_id,
|
|
119
|
+
"url": request.get("url", "N/A"),
|
|
120
|
+
"method": request.get("method", "N/A"),
|
|
121
|
+
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
header_text = HEADER_TEMPLATE.format(**metadata)
|
|
125
|
+
header_lines = [
|
|
126
|
+
f"{comment_prefix} {line}" if line.strip() else comment_prefix for line in header_text.split("\n")
|
|
127
|
+
]
|
|
128
|
+
header = "\n".join(header_lines)
|
|
129
|
+
|
|
130
|
+
content = output_path.read_text()
|
|
131
|
+
output_path.write_text(header + "\n\n" + content)
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@app.command(display="markdown", fastmcp={"type": "tool", "mime_type": "text/markdown", "description": mcp_desc or ""})
|
|
137
|
+
def quicktype(
|
|
138
|
+
state,
|
|
139
|
+
id: int,
|
|
140
|
+
output: str,
|
|
141
|
+
type_name: str,
|
|
142
|
+
field: str = "response.content",
|
|
143
|
+
json_path: str = None, # pyright: ignore[reportArgumentType]
|
|
144
|
+
expr: str = None, # pyright: ignore[reportArgumentType]
|
|
145
|
+
just_types: bool = True,
|
|
146
|
+
prefer_types: bool = True,
|
|
147
|
+
options: dict = None, # pyright: ignore[reportArgumentType]
|
|
148
|
+
) -> dict: # pyright: ignore[reportArgumentType]
|
|
149
|
+
"""Generate types/schemas from request or response body using quicktype CLI.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
id: Row ID from network() output
|
|
153
|
+
output: Output file path (extension determines language: .ts, .py, .go, etc.)
|
|
154
|
+
type_name: Top-level type name (e.g., "User", "ApiResponse")
|
|
155
|
+
field: Body to use - "response.content" (default) or "request.postData"
|
|
156
|
+
json_path: Optional JSON path to extract nested data (e.g., "data[0]")
|
|
157
|
+
expr: Optional Python expression to transform data (has 'body' variable)
|
|
158
|
+
just_types: Generate only types, no serializers (default: True)
|
|
159
|
+
prefer_types: Use 'type' instead of 'interface' for TypeScript (default: True)
|
|
160
|
+
options: Additional quicktype flags as dict
|
|
161
|
+
|
|
162
|
+
Examples:
|
|
163
|
+
quicktype(5, "types/User.ts", "User")
|
|
164
|
+
quicktype(5, "models/customer.py", "Customer")
|
|
165
|
+
quicktype(5, "api.go", "ApiResponse")
|
|
166
|
+
quicktype(5, "types.ts", "User", json_path="data[0]")
|
|
167
|
+
quicktype(5, "types/Form.ts", "Form", field="request.postData")
|
|
168
|
+
quicktype(5, "types.ts", "User", options={"readonly": True})
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Success message with generation details
|
|
172
|
+
"""
|
|
173
|
+
# Get HAR entry via RPC - need full entry with request_id for body fetch
|
|
174
|
+
try:
|
|
175
|
+
result = state.client.call("request", id=id, fields=["*"])
|
|
176
|
+
har_entry = result.get("entry")
|
|
177
|
+
except Exception as e:
|
|
178
|
+
return error_response(f"Failed to get request: {e}")
|
|
179
|
+
|
|
180
|
+
if not har_entry:
|
|
181
|
+
return error_response(f"Request {id} not found")
|
|
182
|
+
|
|
183
|
+
# Fetch body content
|
|
184
|
+
body_content, err = fetch_body_content(state, har_entry, field)
|
|
185
|
+
if err or body_content is None:
|
|
186
|
+
return error_response(
|
|
187
|
+
err or "Failed to fetch body",
|
|
188
|
+
suggestions=[
|
|
189
|
+
f"Field '{field}' could not be fetched",
|
|
190
|
+
"For response body: field='response.content'",
|
|
191
|
+
"For POST data: field='request.postData'",
|
|
192
|
+
],
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Transform via expression or parse as JSON
|
|
196
|
+
if expr:
|
|
197
|
+
try:
|
|
198
|
+
namespace = {"body": body_content}
|
|
199
|
+
data, _ = evaluate_expression(expr, namespace)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
return error_response(
|
|
202
|
+
f"Expression failed: {e}",
|
|
203
|
+
suggestions=[
|
|
204
|
+
"Variable available: 'body' (str)",
|
|
205
|
+
"Example: json.loads(body)['data'][0]",
|
|
206
|
+
"Example: dict(urllib.parse.parse_qsl(body))",
|
|
207
|
+
],
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
if not body_content.strip():
|
|
211
|
+
return error_response("Body is empty")
|
|
212
|
+
|
|
213
|
+
data, parse_err = parse_json(body_content)
|
|
214
|
+
if parse_err:
|
|
215
|
+
return error_response(
|
|
216
|
+
parse_err,
|
|
217
|
+
suggestions=[
|
|
218
|
+
"Body must be valid JSON, or use expr to transform it",
|
|
219
|
+
'For form data: expr="dict(urllib.parse.parse_qsl(body))"',
|
|
220
|
+
],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Extract nested path if specified
|
|
224
|
+
if json_path:
|
|
225
|
+
data, err = extract_json_path(data, json_path)
|
|
226
|
+
if err:
|
|
227
|
+
return error_response(
|
|
228
|
+
err,
|
|
229
|
+
suggestions=[
|
|
230
|
+
f"Path '{json_path}' not found in body",
|
|
231
|
+
'Try a simpler path like "data" or "data[0]"',
|
|
232
|
+
],
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Validate structure
|
|
236
|
+
is_valid, validation_err = validate_generation_data(data)
|
|
237
|
+
if not is_valid:
|
|
238
|
+
return error_response(
|
|
239
|
+
validation_err or "Invalid data structure",
|
|
240
|
+
suggestions=[
|
|
241
|
+
"Code generation requires dict or list structure",
|
|
242
|
+
"Use json_path or expr to extract a complex object",
|
|
243
|
+
],
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Ensure output directory exists
|
|
247
|
+
output_path = ensure_output_directory(output)
|
|
248
|
+
|
|
249
|
+
# Run quicktype
|
|
250
|
+
success, error_msg = _run_quicktype(
|
|
251
|
+
json_data=data,
|
|
252
|
+
output=str(output_path),
|
|
253
|
+
type_name=type_name,
|
|
254
|
+
just_types=just_types,
|
|
255
|
+
prefer_types=prefer_types,
|
|
256
|
+
options=options,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if not success:
|
|
260
|
+
return error_response(
|
|
261
|
+
error_msg,
|
|
262
|
+
suggestions=[
|
|
263
|
+
"Check that quicktype is installed: npm install -g quicktype",
|
|
264
|
+
"Verify the JSON structure is valid",
|
|
265
|
+
"Try simplifying with json_path",
|
|
266
|
+
],
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Detect language and insert header
|
|
270
|
+
ext = output_path.suffix.lower()
|
|
271
|
+
lang_map = {
|
|
272
|
+
".ts": "TypeScript",
|
|
273
|
+
".tsx": "TypeScript",
|
|
274
|
+
".py": "Python",
|
|
275
|
+
".go": "Go",
|
|
276
|
+
".rs": "Rust",
|
|
277
|
+
".java": "Java",
|
|
278
|
+
".kt": "Kotlin",
|
|
279
|
+
".swift": "Swift",
|
|
280
|
+
".cs": "C#",
|
|
281
|
+
".cpp": "C++",
|
|
282
|
+
".dart": "Dart",
|
|
283
|
+
".rb": "Ruby",
|
|
284
|
+
".json": "JSON Schema",
|
|
285
|
+
}
|
|
286
|
+
language = lang_map.get(ext, "Unknown")
|
|
287
|
+
|
|
288
|
+
_insert_header(id, har_entry, output_path, language)
|
|
289
|
+
|
|
290
|
+
# Count lines
|
|
291
|
+
try:
|
|
292
|
+
file_content = output_path.read_text()
|
|
293
|
+
line_count = len(file_content.splitlines())
|
|
294
|
+
except Exception:
|
|
295
|
+
line_count = "unknown"
|
|
296
|
+
|
|
297
|
+
return success_response(
|
|
298
|
+
"Types generated successfully",
|
|
299
|
+
details={
|
|
300
|
+
"Output": str(output_path),
|
|
301
|
+
"Language": language,
|
|
302
|
+
"Type Name": type_name,
|
|
303
|
+
"Lines": line_count,
|
|
304
|
+
"Size": f"{output_path.stat().st_size} bytes",
|
|
305
|
+
},
|
|
306
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Request details command with ES-style field selection."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from webtap.app import app
|
|
6
|
+
from webtap.client import RPCError
|
|
7
|
+
from webtap.commands._builders import error_response
|
|
8
|
+
from webtap.commands._tips import get_mcp_description
|
|
9
|
+
from webtap.commands._utils import evaluate_expression, format_expression_result
|
|
10
|
+
|
|
11
|
+
_mcp_desc = get_mcp_description("request")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command(
|
|
15
|
+
display="markdown",
|
|
16
|
+
fastmcp={"type": "tool", "mime_type": "text/markdown", "description": _mcp_desc or ""},
|
|
17
|
+
)
|
|
18
|
+
def request(
|
|
19
|
+
state,
|
|
20
|
+
id: int,
|
|
21
|
+
fields: list = None, # pyright: ignore[reportArgumentType]
|
|
22
|
+
expr: str = None, # pyright: ignore[reportArgumentType]
|
|
23
|
+
) -> dict:
|
|
24
|
+
"""Get HAR request details with field selection.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
id: Row ID from network() output
|
|
28
|
+
fields: ES-style field patterns (HAR structure)
|
|
29
|
+
- None: minimal (method, url, status, time, state)
|
|
30
|
+
- ["*"]: all fields
|
|
31
|
+
- ["request.*"]: all request fields
|
|
32
|
+
- ["request.headers.*"]: all request headers
|
|
33
|
+
- ["request.postData"]: request body
|
|
34
|
+
- ["response.headers.*"]: all response headers
|
|
35
|
+
- ["response.content"]: fetch response body on-demand
|
|
36
|
+
expr: Python expression with 'data' variable containing selected fields
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
request(123) # Minimal
|
|
40
|
+
request(123, ["*"]) # Everything
|
|
41
|
+
request(123, ["request.headers.*"]) # Request headers
|
|
42
|
+
request(123, ["response.content"]) # Fetch response body
|
|
43
|
+
request(123, ["request.postData", "response.content"]) # Both bodies
|
|
44
|
+
request(123, ["response.content"], expr="json.loads(data['response']['content']['text'])")
|
|
45
|
+
"""
|
|
46
|
+
# Get pre-selected HAR entry from daemon via RPC
|
|
47
|
+
# Field selection (including body fetch) happens server-side
|
|
48
|
+
try:
|
|
49
|
+
result = state.client.call("request", id=id, fields=fields)
|
|
50
|
+
selected = result.get("entry")
|
|
51
|
+
except RPCError as e:
|
|
52
|
+
return error_response(e.message)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
return error_response(str(e))
|
|
55
|
+
|
|
56
|
+
if not selected:
|
|
57
|
+
return error_response(f"Request {id} not found")
|
|
58
|
+
|
|
59
|
+
# If expr provided, evaluate it with data available
|
|
60
|
+
if expr:
|
|
61
|
+
try:
|
|
62
|
+
namespace = {"data": selected}
|
|
63
|
+
eval_result, output = evaluate_expression(expr, namespace)
|
|
64
|
+
formatted = format_expression_result(eval_result, output)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"elements": [
|
|
68
|
+
{"type": "heading", "content": "Expression Result", "level": 2},
|
|
69
|
+
{"type": "code_block", "content": expr, "language": "python"},
|
|
70
|
+
{"type": "text", "content": "**Result:**"},
|
|
71
|
+
{"type": "code_block", "content": formatted, "language": ""},
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
except Exception as e:
|
|
75
|
+
return error_response(
|
|
76
|
+
f"{type(e).__name__}: {e}",
|
|
77
|
+
suggestions=[
|
|
78
|
+
"The selected fields are available as 'data' variable",
|
|
79
|
+
"Common libraries are pre-imported: re, json, bs4, jwt, httpx",
|
|
80
|
+
"Example: json.loads(data['response']['content']['text'])",
|
|
81
|
+
],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Build markdown response
|
|
85
|
+
elements = [
|
|
86
|
+
{"type": "heading", "content": f"Request {id}", "level": 2},
|
|
87
|
+
{"type": "code_block", "content": json.dumps(selected, indent=2, default=str), "language": "json"},
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
return {"elements": elements}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
__all__ = ["request"]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Browser element selection and prompt analysis commands.
|
|
2
|
+
|
|
3
|
+
PUBLIC API:
|
|
4
|
+
- browser: Analyze browser element selections with prompt
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from webtap.app import app
|
|
8
|
+
from webtap.client import RPCError
|
|
9
|
+
from webtap.commands._utils import evaluate_expression, format_expression_result
|
|
10
|
+
from webtap.commands._builders import error_response
|
|
11
|
+
from webtap.commands._tips import get_tips
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command(
|
|
15
|
+
display="markdown",
|
|
16
|
+
fastmcp=[{"type": "resource", "mime_type": "text/markdown"}, {"type": "tool", "mime_type": "text/markdown"}],
|
|
17
|
+
)
|
|
18
|
+
def selections(state, expr: str = None) -> dict: # pyright: ignore[reportArgumentType]
|
|
19
|
+
"""Browser element selections with prompt and analysis.
|
|
20
|
+
|
|
21
|
+
As Resource (no parameters):
|
|
22
|
+
browser # Returns current prompt and all selections
|
|
23
|
+
|
|
24
|
+
As Tool (with parameters):
|
|
25
|
+
browser(expr="data['prompt']") # Get prompt text
|
|
26
|
+
browser(expr="data['selections']['1']['styles']") # Get styles for #1
|
|
27
|
+
browser(expr="len(data['selections'])") # Count selections
|
|
28
|
+
browser(expr="{k: v['selector'] for k, v in data['selections'].items()}") # All selectors
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
expr: Python expression with 'data' variable containing prompt and selections
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Formatted browser data or expression result
|
|
35
|
+
"""
|
|
36
|
+
# Fetch browser data from daemon via RPC
|
|
37
|
+
try:
|
|
38
|
+
daemon_status = state.client.call("status")
|
|
39
|
+
except RPCError as e:
|
|
40
|
+
return error_response(e.message)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
return error_response(str(e))
|
|
43
|
+
|
|
44
|
+
browser = daemon_status.get("browser", {})
|
|
45
|
+
if not browser.get("selections"):
|
|
46
|
+
return error_response(
|
|
47
|
+
"No browser selections available",
|
|
48
|
+
suggestions=[
|
|
49
|
+
"Use the Chrome extension to select elements",
|
|
50
|
+
"Click 'Start Selection Mode' in the extension side panel",
|
|
51
|
+
"Select elements on the page",
|
|
52
|
+
],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
data = {"prompt": browser.get("prompt", ""), "selections": browser.get("selections", {})}
|
|
56
|
+
|
|
57
|
+
# No expression - RESOURCE MODE: Return formatted view
|
|
58
|
+
if not expr:
|
|
59
|
+
return _format_browser_data(data)
|
|
60
|
+
|
|
61
|
+
# TOOL MODE: Evaluate expression
|
|
62
|
+
try:
|
|
63
|
+
namespace = {"data": data}
|
|
64
|
+
result, output = evaluate_expression(expr, namespace)
|
|
65
|
+
formatted_result = format_expression_result(result, output)
|
|
66
|
+
|
|
67
|
+
# Build markdown response
|
|
68
|
+
return {
|
|
69
|
+
"elements": [
|
|
70
|
+
{"type": "heading", "content": "Expression Result", "level": 2},
|
|
71
|
+
{"type": "code_block", "content": expr, "language": "python"},
|
|
72
|
+
{"type": "text", "content": "**Result:**"},
|
|
73
|
+
{"type": "code_block", "content": formatted_result, "language": ""},
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
except Exception as e:
|
|
77
|
+
# Provide helpful suggestions
|
|
78
|
+
suggestions = [
|
|
79
|
+
"The data is available as 'data' variable",
|
|
80
|
+
"Access prompt: data['prompt']",
|
|
81
|
+
"Access selections: data['selections']",
|
|
82
|
+
"Access specific element: data['selections']['1']",
|
|
83
|
+
"Available fields: outerHTML, selector, jsPath, styles, xpath, fullXpath, preview",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
if "KeyError" in str(type(e).__name__):
|
|
87
|
+
suggestions.extend(
|
|
88
|
+
[
|
|
89
|
+
"Check available selection IDs: list(data['selections'].keys())",
|
|
90
|
+
"Check available fields: data['selections']['1'].keys()",
|
|
91
|
+
]
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return error_response(f"{type(e).__name__}: {e}", suggestions=suggestions)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _format_browser_data(data: dict) -> dict:
|
|
98
|
+
"""Format browser data as markdown for resource view."""
|
|
99
|
+
elements = []
|
|
100
|
+
|
|
101
|
+
# Show prompt
|
|
102
|
+
elements.append({"type": "heading", "content": "Browser Prompt", "level": 2})
|
|
103
|
+
elements.append({"type": "text", "content": data.get("prompt", "")})
|
|
104
|
+
|
|
105
|
+
# Show selection count
|
|
106
|
+
selection_count = len(data.get("selections", {}))
|
|
107
|
+
elements.append({"type": "text", "content": f"\n**Selected Elements:** {selection_count}"})
|
|
108
|
+
|
|
109
|
+
# Show each selection with preview
|
|
110
|
+
if selection_count > 0:
|
|
111
|
+
elements.append({"type": "heading", "content": "Element Selections", "level": 3})
|
|
112
|
+
|
|
113
|
+
for sel_id in sorted(data["selections"].keys(), key=lambda x: int(x)):
|
|
114
|
+
sel = data["selections"][sel_id]
|
|
115
|
+
preview = sel.get("preview", {})
|
|
116
|
+
|
|
117
|
+
# Build preview line
|
|
118
|
+
preview_parts = [f"**#{sel_id}:**", preview.get("tag", "unknown")]
|
|
119
|
+
if preview.get("id"):
|
|
120
|
+
preview_parts.append(f"#{preview['id']}")
|
|
121
|
+
if preview.get("classes"):
|
|
122
|
+
preview_parts.append(f".{preview['classes'][0]}")
|
|
123
|
+
|
|
124
|
+
elements.append({"type": "text", "content": " ".join(preview_parts)})
|
|
125
|
+
|
|
126
|
+
# Show selector
|
|
127
|
+
elements.append({"type": "code_block", "content": sel.get("selector", ""), "language": "css"})
|
|
128
|
+
|
|
129
|
+
# Show usage tips from TIPS.md
|
|
130
|
+
tips = get_tips("selections")
|
|
131
|
+
if tips:
|
|
132
|
+
elements.append({"type": "heading", "content": "Next Steps", "level": 3})
|
|
133
|
+
elements.append({"type": "list", "items": tips})
|
|
134
|
+
|
|
135
|
+
return {"elements": elements}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
__all__ = ["selections"]
|