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
webtap/client.py ADDED
@@ -0,0 +1,81 @@
1
+ """JSON-RPC 2.0 client for WebTap daemon communication."""
2
+
3
+ import logging
4
+ import uuid
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class RPCError(Exception):
14
+ """RPC error with code, message, and optional data."""
15
+
16
+ def __init__(self, code: str, message: str, data: dict | None = None):
17
+ super().__init__(message)
18
+ self.code = code
19
+ self.message = message
20
+ self.data = data
21
+
22
+
23
+ class RPCClient:
24
+ """Simple JSON-RPC 2.0 client for WebTap daemon.
25
+
26
+ All daemon communication goes through `call()`:
27
+
28
+ client.call("connect", page=0)
29
+ client.call("network", limit=50, type="xhr")
30
+ client.call("status")
31
+
32
+ The client tracks epoch for stale request detection.
33
+ """
34
+
35
+ def __init__(self, base_url: str = "http://localhost:8765", timeout: float = 30.0):
36
+ self.base_url = base_url
37
+ self.epoch = 0
38
+ self._client = httpx.Client(timeout=timeout)
39
+
40
+ def call(self, method: str, **params) -> dict[str, Any]:
41
+ """Call RPC method. Raises RPCError on error."""
42
+ request: dict[str, Any] = {
43
+ "jsonrpc": "2.0",
44
+ "method": method,
45
+ "params": params,
46
+ "id": str(uuid.uuid4()),
47
+ }
48
+
49
+ # Only send epoch if we've synced with server (non-zero)
50
+ # Server validates epoch only if provided, so first call syncs without validation
51
+ if self.epoch > 0:
52
+ request["epoch"] = self.epoch
53
+
54
+ try:
55
+ response = self._client.post(f"{self.base_url}/rpc", json=request)
56
+ response.raise_for_status()
57
+ data = response.json()
58
+ except httpx.ConnectError as e:
59
+ logger.error(f"Failed to connect to daemon at {self.base_url}: {e}")
60
+ raise RuntimeError("Cannot connect to daemon. Is it running? Try 'webtap --daemon' to start it.") from e
61
+ except httpx.HTTPError as e:
62
+ logger.error(f"HTTP error from daemon: {e}")
63
+ raise
64
+
65
+ # Update epoch from server response (always sync)
66
+ if "epoch" in data:
67
+ self.epoch = data["epoch"]
68
+
69
+ # Check for RPC error
70
+ if "error" in data:
71
+ err = data["error"]
72
+ raise RPCError(err.get("code", "UNKNOWN"), err.get("message", "Unknown error"), err.get("data"))
73
+
74
+ return data.get("result", {})
75
+
76
+ def close(self):
77
+ """Close the HTTP client."""
78
+ self._client.close()
79
+
80
+
81
+ __all__ = ["RPCClient", "RPCError"]
@@ -0,0 +1,401 @@
1
+ # WebTap Commands Developer Guide
2
+
3
+ This guide documents the patterns and conventions for developing WebTap commands with MCP compatibility.
4
+
5
+ ## Command Patterns (Post-Refinement)
6
+
7
+ ### 1. Simple Commands (No Parameters)
8
+ ```python
9
+ @app.command(display="markdown", fastmcp={"type": "tool"})
10
+ def disconnect(state) -> dict:
11
+ """Disconnect from Chrome."""
12
+ # Implementation
13
+ ```
14
+
15
+ ### 2. Single Required Parameter
16
+ ```python
17
+ @app.command(display="markdown", fastmcp={"type": "tool"})
18
+ def navigate(state, url: str) -> dict:
19
+ """Navigate to URL."""
20
+ # Implementation
21
+ ```
22
+
23
+ ### 3. Optional Boolean/Simple Parameters (Direct)
24
+ ```python
25
+ @app.command(display="markdown", fastmcp={"type": "tool"})
26
+ def reload(state, ignore_cache: bool = False) -> dict:
27
+ """Reload current page."""
28
+ # Implementation
29
+
30
+ # Multiple boolean flags
31
+ @app.command(display="markdown", fastmcp={"type": "tool"})
32
+ def clear(state, events: bool = True, console: bool = False) -> dict:
33
+ """Clear various data stores."""
34
+ # Implementation
35
+ ```
36
+
37
+ ### 4. Mutually Exclusive Parameters (Direct)
38
+ Use direct parameters when you have different ways to identify the same thing:
39
+
40
+ ```python
41
+ @app.command(display="markdown", fastmcp={"type": "tool"})
42
+ def connect(state, page: int = None, page_id: str = None) -> dict:
43
+ """Connect to Chrome page.
44
+
45
+ Args:
46
+ page: Connect by page index (0-based)
47
+ page_id: Connect by page ID
48
+
49
+ Note: Cannot specify both page and page_id.
50
+ """
51
+ if page is not None and page_id is not None:
52
+ return error_response("invalid_parameters",
53
+ "Cannot specify both 'page' and 'page_id'")
54
+ # Implementation
55
+ ```
56
+
57
+ ### 5. Multiple Optional Parameters (Direct)
58
+ Use direct parameters for cleaner API when parameters are well-defined:
59
+
60
+ ```python
61
+ @app.command(display="markdown", fastmcp={"type": "tool"})
62
+ def network(state, limit: int = 20, filters: list = None, no_filters: bool = False) -> dict:
63
+ """Show network requests.
64
+
65
+ Args:
66
+ limit: Maximum results to show
67
+ filters: Specific filter categories to apply
68
+ no_filters: Show everything unfiltered
69
+ """
70
+ # Implementation
71
+
72
+ # With field selection and expression evaluation
73
+ @app.command(display="markdown", fastmcp={"type": "tool"})
74
+ def request(state, id: int, fields: list = None, expr: str = None) -> dict:
75
+ """Get HAR request details with field selection."""
76
+ # Implementation
77
+ ```
78
+
79
+ ### 6. Mixed Parameters (Direct + Dict)
80
+ Use dict only for complex/variable configurations:
81
+
82
+ ```python
83
+ @app.command(display="markdown", fastmcp={"type": "tool"})
84
+ def resume(state, request: int, wait: float = 0.5, modifications: dict = None) -> dict:
85
+ """Resume a paused request.
86
+
87
+ Args:
88
+ request: Request row ID
89
+ wait: Wait time for next event
90
+ modifications: Request/response modifications
91
+ - {"url": "..."} - Change URL
92
+ - {"method": "POST"} - Change method
93
+ """
94
+ mods = modifications or {}
95
+ # Implementation
96
+ ```
97
+
98
+ ### 7. Field Selection Pattern
99
+ Use list for selecting fields from structured data:
100
+
101
+ ```python
102
+ @app.command(display="markdown", fastmcp={"type": "tool"})
103
+ def request(state, id: int, fields: list = None, expr: str = None) -> dict:
104
+ """Get HAR request details with field selection.
105
+
106
+ Args:
107
+ id: HAR row ID from network() output
108
+ fields: Field patterns to include
109
+ - ["*"] - Everything
110
+ - ["request.*"] - Request data
111
+ - ["response.content"] - Response body
112
+ expr: Python expression to evaluate on data
113
+ """
114
+ # Fields are selected from HAR entry structure
115
+ ```
116
+
117
+ ### 8. Action + Config Pattern (Complex Operations)
118
+ Keep for commands with varied operations:
119
+
120
+ ```python
121
+ @app.command(display="markdown", fastmcp={"type": "tool"})
122
+ def filters(state, action: str = "list", config: dict = None) -> dict:
123
+ """Manage filters.
124
+
125
+ Args:
126
+ action: Operation to perform
127
+ - "list" - Show all filters
128
+ - "add" - Add filter
129
+ - "remove" - Remove filter
130
+ config: Action-specific configuration
131
+ - For add: {"category": "ads", "patterns": ["*ad*"]}
132
+ - For remove: {"patterns": ["*ad*"]}
133
+ """
134
+ cfg = config or {}
135
+
136
+ if action == "add":
137
+ category = cfg.get("category", "custom")
138
+ patterns = cfg.get("patterns", [])
139
+ # Implementation
140
+ ```
141
+
142
+ ## MCP Type Requirements
143
+
144
+ ### ❌ Avoid These (Not MCP Compatible)
145
+ ```python
146
+ # Union types
147
+ def command(state, param: str | None = None)
148
+
149
+ # Optional types
150
+ from typing import Optional
151
+ def command(state, param: Optional[str] = None)
152
+
153
+ # Complex types
154
+ from typing import Dict, List
155
+ def command(state, data: Dict[str, List[str]])
156
+
157
+ # **kwargs
158
+ def command(state, **fields)
159
+ ```
160
+
161
+ ### ✅ Use These Instead
162
+ ```python
163
+ # Simple defaults
164
+ def command(state, param: str = "default")
165
+ def command(state, param: dict = None) # pyright: ignore[reportArgumentType]
166
+ def command(state, param: list = None) # pyright: ignore[reportArgumentType]
167
+ def command(state, param: bool = False)
168
+ def command(state, param: int = 0)
169
+ ```
170
+
171
+ ## Response Patterns
172
+
173
+ ### Resources (Read-Only Data)
174
+ ```python
175
+ @app.command(display="markdown", fastmcp={"type": "resource", "mime_type": "text/markdown"})
176
+ def pages(state) -> dict:
177
+ """List available pages."""
178
+ return table_response(
179
+ title="Chrome Pages",
180
+ headers=["Index", "Title", "URL"],
181
+ rows=rows,
182
+ summary=f"{len(rows)} pages"
183
+ )
184
+ ```
185
+
186
+ ### Tools (Actions with Side Effects)
187
+ ```python
188
+ @app.command(display="markdown", fastmcp={"type": "tool"})
189
+ def navigate(state, url: str) -> dict:
190
+ """Navigate to URL."""
191
+ # Perform action
192
+ return info_response(
193
+ title="Navigation Complete",
194
+ fields={"URL": url, "Status": "Success"}
195
+ )
196
+ ```
197
+
198
+ ## Error Handling
199
+
200
+ Always use the error utilities from `_builders.py`:
201
+
202
+ ```python
203
+ from webtap.commands._builders import check_connection, error_response
204
+
205
+ def my_command(state, ...):
206
+ # Check connection first for commands that need it
207
+ if error := check_connection(state):
208
+ return error
209
+
210
+ # Validate parameters
211
+ if not valid:
212
+ return error_response("Parameter X must be Y", suggestions=["Try this", "Or that"])
213
+
214
+ # Simple errors
215
+ return error_response("Specific error message")
216
+ ```
217
+
218
+ ## Response Builders
219
+
220
+ Use builders from `_builders.py`:
221
+
222
+ ```python
223
+ from webtap.commands._builders import (
224
+ # Table responses
225
+ table_response, # Tables with headers, warnings, tips
226
+
227
+ # Info displays
228
+ info_response, # Key-value pairs with optional heading
229
+
230
+ # Status responses
231
+ error_response, # Errors with suggestions
232
+ success_response, # Success messages with details
233
+ warning_response, # Warnings with suggestions
234
+
235
+ # Code results
236
+ code_result_response, # Code execution with result display
237
+ code_response, # Simple code block display
238
+
239
+ # Connection helpers
240
+ check_connection, # Helper for CDP connection validation
241
+ check_fetch_enabled, # Helper for fetch interception validation
242
+ )
243
+ ```
244
+
245
+ ### Usage Examples
246
+
247
+ ```python
248
+ # Table with tips
249
+ return table_response(
250
+ title="Network Requests",
251
+ headers=["ID", "URL", "Status"],
252
+ rows=rows,
253
+ summary=f"{len(rows)} requests",
254
+ tips=["Use request(ID, ['response.content']) to fetch response body"]
255
+ )
256
+
257
+ # Code execution result
258
+ return code_result_response(
259
+ "JavaScript Result",
260
+ code="2 + 2",
261
+ language="javascript",
262
+ result=4
263
+ )
264
+
265
+ # Error with suggestions
266
+ return error_response(
267
+ "Not connected to Chrome",
268
+ suggestions=["Run pages()", "Use connect(0)"]
269
+ )
270
+ ```
271
+
272
+ ## Text Over Symbols
273
+
274
+ Use explicit text instead of symbols for clarity:
275
+
276
+ ```python
277
+ # Status text
278
+ "Connected" / "Disconnected"
279
+ "Enabled" / "Disabled"
280
+ "Yes" / "No"
281
+
282
+ # For empty values
283
+ "-" or "None" or ""
284
+
285
+ # Descriptive status
286
+ "3 requests paused" instead of symbols
287
+ "Request Failed" instead of error symbols
288
+ ```
289
+
290
+ ## Decision Tree for Parameter Patterns (Updated)
291
+
292
+ 1. **No parameters?** → Simple command
293
+ 2. **One required param?** → Single parameter
294
+ 3. **Few well-defined params?** → Direct parameters with defaults
295
+ 4. **Multiple ways to identify same thing?** → Direct mutually exclusive params
296
+ 5. **Dynamic/unknown field names?** → Dict for filters
297
+ 6. **Complex variable config?** → Dict for modifications
298
+ 7. **Different operations based on input?** → Action + config pattern
299
+
300
+ ### When to Use Direct Parameters
301
+ - Parameters are well-defined and limited (< 5)
302
+ - Parameters are commonly used
303
+ - Makes the API more intuitive
304
+ - Boolean flags or simple types
305
+
306
+ ### When to Keep Dict Parameters
307
+ - Field names are dynamic (like CDP event fields)
308
+ - Configuration varies significantly by action
309
+ - Many optional parameters rarely used together
310
+ - Complex nested structures needed
311
+
312
+ ## Examples by Category (Current Implementation)
313
+
314
+ ### Navigation Commands
315
+ - `navigate(url: str)` - Single required parameter
316
+ - `reload(ignore_cache: bool = False)` - Optional boolean
317
+ - `back()`, `forward()` - No parameters
318
+
319
+ ### Query Commands
320
+ - `network(limit: int = 20, ...)` - Direct params with filters
321
+ - `request(id: int, fields: list = None, expr: str = None)` - Field selection + expression
322
+ - `console(limit: int = 50, level: str = None)` - Direct optional params
323
+
324
+ ### Management Commands
325
+ - `connect(page: int = None, page_id: str = None)` - Mutually exclusive direct params
326
+ - `clear(events: bool = True, console: bool = False)` - Boolean flags
327
+ - `filters(add: str = None, remove: str = None, ...)` - Inline filter management
328
+
329
+ ### JavaScript & Fetch Commands
330
+ - `js(code: str, wait_return: bool = True, await_promise: bool = False)` - Direct params
331
+ - `fetch(action: str, options: dict = None)` - Action pattern
332
+ - `resume(request: int, wait: float = 0.5, modifications: dict = None)` - Direct + dict
333
+
334
+ ## Testing Your Command
335
+
336
+ 1. **Type checking**: Run `basedpyright` to ensure types are correct
337
+ 2. **Linting**: Run `ruff check` for code style
338
+ 3. **REPL mode**: Test with `webtap` command
339
+ 4. **MCP mode**: Test with `webtap --mcp` command
340
+ 5. **Markdown rendering**: Verify output displays correctly
341
+
342
+ ## TIPS.md Integration
343
+
344
+ All commands should be documented in `TIPS.md` for consistent help and MCP descriptions:
345
+
346
+ ```python
347
+ from webtap.commands._tips import get_tips, get_mcp_description
348
+
349
+ # Get MCP description (for fastmcp metadata)
350
+ mcp_desc = get_mcp_description("mycommand")
351
+
352
+ @app.command(
353
+ display="markdown",
354
+ fastmcp={"type": "tool", "description": mcp_desc} if mcp_desc else {"type": "tool"}
355
+ )
356
+ def mycommand(state, param: str) -> dict:
357
+ """My command description."""
358
+ # ... implementation ...
359
+
360
+ # Include tips in response
361
+ tips = get_tips("mycommand", context={"id": some_id})
362
+ return table_response(
363
+ headers=headers,
364
+ rows=rows,
365
+ tips=tips # Automatically shown as "Next Steps"
366
+ )
367
+ ```
368
+
369
+ ### Adding to TIPS.md
370
+
371
+ Add a section for your command:
372
+
373
+ ```markdown
374
+ ### mycommand
375
+ Brief description of what the command does.
376
+
377
+ #### Examples
378
+ \```python
379
+ mycommand("param1") # Basic usage
380
+ mycommand("param2", flag=True) # With options
381
+ \```
382
+
383
+ #### Tips
384
+ - **Related command:** `other_command()` - does related thing
385
+ - **Advanced usage:** Use with `yet_another()` for X
386
+ - **Context aware:** Tips support {id} placeholders from context dict
387
+ ```
388
+
389
+ ## Checklist for New Commands
390
+
391
+ - [ ] Use `@app.command()` decorator with `display="markdown"`
392
+ - [ ] Add `fastmcp` metadata (type: "resource" or "tool")
393
+ - [ ] Use simple types only (no unions, no Optional)
394
+ - [ ] Add `# pyright: ignore[reportArgumentType]` for `dict = None`
395
+ - [ ] Import builders from `_builders.py`
396
+ - [ ] Use `table_response()`, `info_response()`, or `code_result_response()`
397
+ - [ ] Check connection with `check_connection()` if needed
398
+ - [ ] Add command section to `TIPS.md` with examples and tips
399
+ - [ ] Use `get_tips()` to show tips in response
400
+ - [ ] Document parameters clearly in docstring
401
+ - [ ] Test in both REPL and MCP modes