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.
Files changed (64) hide show
  1. webtap/VISION.md +246 -0
  2. webtap/__init__.py +84 -0
  3. webtap/__main__.py +6 -0
  4. webtap/api/__init__.py +9 -0
  5. webtap/api/app.py +26 -0
  6. webtap/api/models.py +69 -0
  7. webtap/api/server.py +111 -0
  8. webtap/api/sse.py +182 -0
  9. webtap/api/state.py +89 -0
  10. webtap/app.py +79 -0
  11. webtap/cdp/README.md +275 -0
  12. webtap/cdp/__init__.py +12 -0
  13. webtap/cdp/har.py +302 -0
  14. webtap/cdp/schema/README.md +41 -0
  15. webtap/cdp/schema/cdp_protocol.json +32785 -0
  16. webtap/cdp/schema/cdp_version.json +8 -0
  17. webtap/cdp/session.py +667 -0
  18. webtap/client.py +81 -0
  19. webtap/commands/DEVELOPER_GUIDE.md +401 -0
  20. webtap/commands/TIPS.md +269 -0
  21. webtap/commands/__init__.py +29 -0
  22. webtap/commands/_builders.py +331 -0
  23. webtap/commands/_code_generation.py +110 -0
  24. webtap/commands/_tips.py +147 -0
  25. webtap/commands/_utils.py +273 -0
  26. webtap/commands/connection.py +220 -0
  27. webtap/commands/console.py +87 -0
  28. webtap/commands/fetch.py +310 -0
  29. webtap/commands/filters.py +116 -0
  30. webtap/commands/javascript.py +73 -0
  31. webtap/commands/js_export.py +73 -0
  32. webtap/commands/launch.py +72 -0
  33. webtap/commands/navigation.py +197 -0
  34. webtap/commands/network.py +136 -0
  35. webtap/commands/quicktype.py +306 -0
  36. webtap/commands/request.py +93 -0
  37. webtap/commands/selections.py +138 -0
  38. webtap/commands/setup.py +219 -0
  39. webtap/commands/to_model.py +163 -0
  40. webtap/daemon.py +185 -0
  41. webtap/daemon_state.py +53 -0
  42. webtap/filters.py +219 -0
  43. webtap/rpc/__init__.py +14 -0
  44. webtap/rpc/errors.py +49 -0
  45. webtap/rpc/framework.py +223 -0
  46. webtap/rpc/handlers.py +625 -0
  47. webtap/rpc/machine.py +84 -0
  48. webtap/services/README.md +83 -0
  49. webtap/services/__init__.py +15 -0
  50. webtap/services/console.py +124 -0
  51. webtap/services/dom.py +547 -0
  52. webtap/services/fetch.py +415 -0
  53. webtap/services/main.py +392 -0
  54. webtap/services/network.py +401 -0
  55. webtap/services/setup/__init__.py +185 -0
  56. webtap/services/setup/chrome.py +233 -0
  57. webtap/services/setup/desktop.py +255 -0
  58. webtap/services/setup/extension.py +147 -0
  59. webtap/services/setup/platform.py +162 -0
  60. webtap/services/state_snapshot.py +86 -0
  61. webtap_tool-0.11.0.dist-info/METADATA +535 -0
  62. webtap_tool-0.11.0.dist-info/RECORD +64 -0
  63. webtap_tool-0.11.0.dist-info/WHEEL +4 -0
  64. 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"]