webtap-tool 0.1.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.

@@ -0,0 +1,153 @@
1
+ # WebTap Command Documentation
2
+
3
+ ## Libraries
4
+ All commands have these pre-imported (no imports needed!):
5
+ - **Web:** bs4/BeautifulSoup, lxml, ElementTree/ET
6
+ - **Data:** json, yaml, msgpack, protobuf_json/protobuf_text
7
+ - **Security:** jwt, base64, hashlib, cryptography
8
+ - **HTTP:** httpx, urllib
9
+ - **Text:** re, difflib, textwrap, html
10
+ - **Utils:** datetime, collections, itertools, pprint, ast
11
+
12
+ ## Commands
13
+
14
+ ### body
15
+ Fetch and analyze HTTP response bodies with Python expressions.
16
+
17
+ #### Examples
18
+ ```python
19
+ body(123) # Get body
20
+ body(123, "json.loads(body)") # Parse JSON
21
+ body(123, "bs4(body, 'html.parser').find('title').text") # HTML title
22
+ body(123, "jwt.decode(body, options={'verify_signature': False})") # Decode JWT
23
+ body(123, "re.findall(r'/api/[^\"\\s]+', body)[:10]") # Find API endpoints
24
+ body(123, "httpx.get(json.loads(body)['next_url']).json()") # Chain requests
25
+ body(123, "msgpack.unpackb(body)") # Binary formats
26
+ ```
27
+
28
+ #### Tips
29
+ - **Chain requests:** `body({id}, "httpx.get(json.loads(body)['next_url']).text[:100]")`
30
+ - **Parse XML:** `body({id}, "ElementTree.fromstring(body).find('.//title').text")`
31
+ - **Extract forms:** `body({id}, "[f['action'] for f in bs4(body, 'html.parser').find_all('form')]")`
32
+ - **Decode protobuf:** `body({id}, "protobuf_json.Parse(body, YourMessage())")`
33
+ - **Find related:** `events({'requestId': request_id})` - related events
34
+
35
+ ### inspect
36
+ Inspect CDP events with full Python debugging.
37
+
38
+ Available objects: 'data' (when inspecting event), 'cdp' and 'state' (when no event).
39
+
40
+ #### Examples
41
+ ```python
42
+ inspect(456) # Full event
43
+ inspect(456, "data['method']") # Event type
44
+ inspect(456, "list(data.keys())") # Top-level keys
45
+ inspect(456, "data.get('params', {}).get('response', {}).get('status')")
46
+ inspect(456, "re.findall(r'session=(\\w+)', str(data))") # Extract patterns
47
+ inspect(456, "base64.b64decode(data['params']['response']['body'])")
48
+ inspect(456, "jwt.decode(auth.replace('Bearer ', ''), options={'verify_signature': False})")
49
+ inspect(expr="len(cdp.events)") # Direct CDP access
50
+ inspect(expr="[e for e in cdp.events if 'error' in str(e).lower()][:3]")
51
+ ```
52
+
53
+ #### Tips
54
+ - **Find related:** `events({'requestId': data.get('params', {}).get('requestId')})`
55
+ - **Compare events:** `inspect(other_id, "data.get('method')")`
56
+ - **Extract timing:** `inspect({id}, "data['params']['timing']")`
57
+ - **Decode cookies:** `inspect({id}, "[c.split('=') for c in data.get('params', {}).get('cookies', '').split(';')]")`
58
+ - **Get body:** `body({id})` - if this is a response event
59
+
60
+ ### network
61
+ Show network requests with full data.
62
+
63
+ #### Tips
64
+ - **Analyze responses:** `body({id})` - fetch response body
65
+ - **Parse HTML:** `body({id}, "bs4(body, 'html.parser').find('title').text")`
66
+ - **Extract JSON:** `body({id}, "json.loads(body)['data']")`
67
+ - **Find patterns:** `body({id}, "re.findall(r'/api/\\w+', body)")`
68
+ - **Decode JWT:** `body({id}, "jwt.decode(body, options={'verify_signature': False})")`
69
+ - **Search events:** `events({'url': '*api*'})` - find all API calls
70
+ - **Intercept traffic:** `fetch('enable')` then `requests()` - pause and modify
71
+
72
+ ### console
73
+ Show console messages with full data.
74
+
75
+ #### Tips
76
+ - **Inspect error:** `inspect({id})` - view full stack trace
77
+ - **Find all errors:** `events({'level': 'error'})` - filter console errors
78
+ - **Extract stack:** `inspect({id}, "data.get('stackTrace', {})")`
79
+ - **Search messages:** `events({'message': '*failed*'})` - pattern match
80
+ - **Check network:** `network()` - may show failed requests causing errors
81
+
82
+ ### events
83
+ Query CDP events by field values with automatic discovery.
84
+
85
+ Searches across ALL event types - network, console, page, etc.
86
+ Field names are discovered automatically and case-insensitive.
87
+
88
+ #### Examples
89
+ ```python
90
+ events() # Recent 20 events
91
+ events({"method": "Runtime.*"}) # Runtime events
92
+ events({"requestId": "123"}, limit=100) # Specific request
93
+ events({"url": "*api*"}) # Find all API calls
94
+ events({"status": 200}) # Successful responses
95
+ events({"level": "error"}) # Console errors
96
+ ```
97
+
98
+ #### Tips
99
+ - **Inspect full event:** `inspect({id})` - view complete CDP data
100
+ - **Extract nested data:** `inspect({id}, "data['params']['response']['headers']")`
101
+ - **Find patterns:** `inspect({id}, "re.findall(r'token=(\\w+)', str(data))")`
102
+ - **Get response body:** `body({id})` - if this is a network response
103
+ - **Decode data:** `inspect({id}, "base64.b64decode(data.get('params', {}).get('body', ''))")`
104
+
105
+ ### js
106
+ Execute JavaScript in the browser with optional promise handling.
107
+
108
+ #### Examples
109
+ ```python
110
+ js("document.title") # Get page title
111
+ js("document.body.innerText.length") # Get text length
112
+ js("[...document.links].map(a => a.href)") # Get all links
113
+ js("fetch('/api').then(r => r.json())", await_promise=True) # Async
114
+ js("document.querySelectorAll('.ad').forEach(e => e.remove())", wait_return=False)
115
+ js("window.fetch = new Proxy(window.fetch, {get: (t, p) => console.log(p)})", wait_return=False)
116
+ ```
117
+
118
+ #### Tips
119
+ - **Extract all links:** `js("[...document.links].map(a => a.href)")`
120
+ - **Get page text:** `js("document.body.innerText")`
121
+ - **Find data attributes:** `js("[...document.querySelectorAll('[data-id]')].map(e => e.dataset)")`
122
+ - **Monitor DOM:** `js("new MutationObserver(console.log).observe(document, {childList: true, subtree: true})", wait_return=False)`
123
+ - **Hook fetch:** `js("window.fetch = new Proxy(fetch, {apply: (t, _, a) => {console.log(a); return t(...a)}})", wait_return=False)`
124
+ - **Check console:** `console()` - see logged messages from JS execution
125
+
126
+ ### fetch
127
+ Control request interception for debugging and modification.
128
+
129
+ #### Examples
130
+ ```python
131
+ fetch("status") # Check status
132
+ fetch("enable") # Enable request stage
133
+ fetch("enable", {"response": true}) # Both stages
134
+ fetch("disable") # Disable
135
+ ```
136
+
137
+ #### Tips
138
+ - **View paused:** `requests()` - see all intercepted requests
139
+ - **Inspect request:** `inspect({id})` - view full CDP event data
140
+ - **Analyze body:** `body({id})` - fetch and examine response body
141
+ - **Resume request:** `resume({id})` - continue the request
142
+ - **Modify request:** `resume({id}, modifications={'url': '...'})`
143
+ - **Block request:** `fail({id}, 'BlockedByClient')` - reject the request
144
+
145
+ ### requests
146
+ Show paused requests and responses.
147
+
148
+ #### Tips
149
+ - **Inspect request:** `inspect({id})` - view full CDP event data
150
+ - **Analyze body:** `body({id})` - fetch and examine response body
151
+ - **Resume request:** `resume({id})` - continue the request
152
+ - **Modify request:** `resume({id}, modifications={'url': '...'})`
153
+ - **Fail request:** `fail({id}, 'BlockedByClient')` - block the request
@@ -0,0 +1,7 @@
1
+ """WebTap command modules for browser automation.
2
+
3
+ Commands are imported directly by app.py to register with the ReplKit2 app.
4
+ In CLI mode, only CLI-compatible commands are imported to avoid Typer issues.
5
+ """
6
+
7
+ # No imports needed here - app.py imports commands directly
@@ -0,0 +1,127 @@
1
+ """Response builders using ReplKit2 v0.10.0+ markdown elements."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ def table_response(
7
+ title: str | None = None,
8
+ headers: list[str] | None = None,
9
+ rows: list[dict] | None = None,
10
+ summary: str | None = None,
11
+ warnings: list[str] | None = None,
12
+ tips: list[str] | None = None,
13
+ ) -> dict:
14
+ """Build table response with full data.
15
+
16
+ Args:
17
+ title: Optional table title
18
+ headers: Column headers
19
+ rows: Data rows with FULL data
20
+ summary: Optional summary text
21
+ warnings: Optional warning messages
22
+ tips: Optional developer tips/guidance
23
+ """
24
+ elements = []
25
+
26
+ if title:
27
+ elements.append({"type": "heading", "content": title, "level": 2})
28
+
29
+ if warnings:
30
+ for warning in warnings:
31
+ elements.append({"type": "alert", "message": warning, "level": "warning"})
32
+
33
+ if headers and rows:
34
+ elements.append({"type": "table", "headers": headers, "rows": rows})
35
+ elif rows: # Headers can be inferred from row keys
36
+ elements.append({"type": "table", "rows": rows})
37
+ else:
38
+ elements.append({"type": "text", "content": "_No data available_"})
39
+
40
+ if summary:
41
+ elements.append({"type": "text", "content": f"_{summary}_"})
42
+
43
+ if tips:
44
+ elements.append({"type": "heading", "content": "Next Steps", "level": 3})
45
+ elements.append({"type": "list", "items": tips})
46
+
47
+ return {"elements": elements}
48
+
49
+
50
+ def info_response(
51
+ title: str | None = None,
52
+ fields: dict | None = None,
53
+ extra: str | None = None,
54
+ ) -> dict:
55
+ """Build info display with key-value pairs.
56
+
57
+ Args:
58
+ title: Optional info title
59
+ fields: Dict of field names to values
60
+ extra: Optional extra content (raw markdown)
61
+ """
62
+ elements = []
63
+
64
+ if title:
65
+ elements.append({"type": "heading", "content": title, "level": 2})
66
+
67
+ if fields:
68
+ for key, value in fields.items():
69
+ if value is not None:
70
+ elements.append({"type": "text", "content": f"**{key}:** {value}"})
71
+
72
+ if extra:
73
+ elements.append({"type": "raw", "content": extra})
74
+
75
+ if not elements:
76
+ elements.append({"type": "text", "content": "_No information available_"})
77
+
78
+ return {"elements": elements}
79
+
80
+
81
+ def error_response(message: str, suggestions: list[str] | None = None) -> dict:
82
+ """Build error response with optional suggestions.
83
+
84
+ Args:
85
+ message: Error message
86
+ suggestions: Optional list of suggestions
87
+ """
88
+ elements: list[dict[str, Any]] = [{"type": "alert", "message": message, "level": "error"}]
89
+
90
+ if suggestions:
91
+ elements.append({"type": "text", "content": "**Try:**"})
92
+ elements.append({"type": "list", "items": suggestions})
93
+
94
+ return {"elements": elements}
95
+
96
+
97
+ def success_response(message: str, details: dict | None = None) -> dict:
98
+ """Build success response with optional details.
99
+
100
+ Args:
101
+ message: Success message
102
+ details: Optional dict of additional details
103
+ """
104
+ elements = [{"type": "alert", "message": message, "level": "success"}]
105
+
106
+ if details:
107
+ for key, value in details.items():
108
+ if value is not None:
109
+ elements.append({"type": "text", "content": f"**{key}:** {value}"})
110
+
111
+ return {"elements": elements}
112
+
113
+
114
+ def warning_response(message: str, suggestions: list[str] | None = None) -> dict:
115
+ """Build warning response with optional suggestions.
116
+
117
+ Args:
118
+ message: Warning message
119
+ suggestions: Optional list of suggestions
120
+ """
121
+ elements: list[dict[str, Any]] = [{"type": "alert", "message": message, "level": "warning"}]
122
+
123
+ if suggestions:
124
+ elements.append({"type": "text", "content": "**Try:**"})
125
+ elements.append({"type": "list", "items": suggestions})
126
+
127
+ return {"elements": elements}
@@ -0,0 +1,108 @@
1
+ """Unified error handling for WebTap commands."""
2
+
3
+ from typing import Optional
4
+ from replkit2.textkit import markdown
5
+
6
+
7
+ ERRORS = {
8
+ "not_connected": {
9
+ "message": "Not connected to Chrome",
10
+ "details": "Use `connect()` to connect to a page",
11
+ "help": [
12
+ "Run `pages()` to see available tabs",
13
+ "Use `connect(0)` to connect to first tab",
14
+ "Or `connect(page_id='...')` for specific tab",
15
+ ],
16
+ },
17
+ "fetch_disabled": {
18
+ "message": "Fetch interception not enabled",
19
+ "details": "Enable with `fetch(enable=True)` to pause requests",
20
+ },
21
+ "no_data": {"message": "No data available", "details": "The requested data is not available"},
22
+ }
23
+
24
+
25
+ def check_connection(state) -> Optional[dict]:
26
+ """Check CDP connection and return error response if not connected.
27
+
28
+ Args:
29
+ state: Application state containing CDP session.
30
+ """
31
+ if not (state.cdp and state.cdp.is_connected):
32
+ return error_response("not_connected")
33
+ return None
34
+
35
+
36
+ def error_response(error_key: str, custom_message: str | None = None, **kwargs) -> dict:
37
+ """Build consistent error response in markdown.
38
+
39
+ Args:
40
+ error_key: Key from error templates or custom identifier.
41
+ custom_message: Override default message.
42
+ **kwargs: Additional context to add to error response.
43
+ """
44
+ error_info = ERRORS.get(error_key, {})
45
+ message = custom_message or error_info.get("message", "Error occurred")
46
+
47
+ builder = markdown().element("alert", message=message, level="error")
48
+
49
+ if details := error_info.get("details"):
50
+ builder.text(details)
51
+
52
+ if help_items := error_info.get("help"):
53
+ builder.text("**How to fix:**")
54
+ builder.list_(help_items)
55
+
56
+ for key, value in kwargs.items():
57
+ if value:
58
+ builder.text(f"_{key}: {value}_")
59
+
60
+ return builder.build()
61
+
62
+
63
+ def warning_response(message: str, details: str | None = None) -> dict:
64
+ """Build warning response for non-fatal issues.
65
+
66
+ Args:
67
+ message: Warning message text.
68
+ details: Additional details.
69
+ """
70
+ builder = markdown().element("alert", message=message, level="warning")
71
+ if details:
72
+ builder.text(details)
73
+ return builder.build()
74
+
75
+
76
+ def invalid_options_error(message: str, expected: dict = None) -> dict: # pyright: ignore[reportArgumentType]
77
+ """Create error response for invalid options dict.
78
+
79
+ Args:
80
+ message: Error message describing the issue.
81
+ expected: Optional example of expected format.
82
+ """
83
+ builder = markdown().element("alert", message=message, level="error")
84
+
85
+ if expected:
86
+ builder.text("**Expected format:**")
87
+ import json
88
+
89
+ builder.code_block(json.dumps(expected, indent=2), language="json")
90
+
91
+ return builder.build()
92
+
93
+
94
+ def missing_param_error(param: str, command: str = None) -> dict: # pyright: ignore[reportArgumentType]
95
+ """Create error response for missing required parameter.
96
+
97
+ Args:
98
+ param: Name of the missing parameter.
99
+ command: Optional command name for context.
100
+ """
101
+ message = f"Required parameter '{param}' not provided"
102
+ if command:
103
+ message = f"{command}: {message}"
104
+
105
+ builder = markdown().element("alert", message=message, level="error")
106
+ builder.text(f"The '{param}' parameter is required for this operation")
107
+
108
+ return builder.build()
@@ -0,0 +1,147 @@
1
+ """Parser for TIPS.md documentation.
2
+
3
+ This module reads TIPS.md and provides:
4
+ - MCP descriptions for commands
5
+ - Developer tips for command responses
6
+ - Pre-imported libraries documentation
7
+ """
8
+
9
+ import re
10
+ from pathlib import Path
11
+ from typing import Dict, List, Optional
12
+
13
+
14
+ class TipsParser:
15
+ """Parse TIPS.md for command documentation."""
16
+
17
+ def __init__(self):
18
+ # TIPS.md is in the same directory as this file
19
+ self.tips_path = Path(__file__).parent / "TIPS.md"
20
+ self.content = self.tips_path.read_text() if self.tips_path.exists() else ""
21
+ self._cache = {}
22
+
23
+ def _get_libraries(self) -> str:
24
+ """Extract the libraries section."""
25
+ if "libraries" not in self._cache:
26
+ match = re.search(r"## Libraries\n(.*?)(?=\n##)", self.content, re.DOTALL)
27
+ self._cache["libraries"] = match.group(1).strip() if match else ""
28
+ return self._cache["libraries"]
29
+
30
+ def _get_command_section(self, command: str) -> Optional[str]:
31
+ """Get the full section for a command."""
32
+ # Simple and explicit - look for the exact command name
33
+ # Use negative lookahead to ensure we match ### but not ####
34
+ pattern = rf"### {re.escape(command)}\n(.*?)(?=\n###(?!#)|\Z)"
35
+ match = re.search(pattern, self.content, re.DOTALL)
36
+ return match.group(1).strip() if match else None
37
+
38
+ def _get_description(self, command: str) -> Optional[str]:
39
+ """Get command description (text before #### sections)."""
40
+ section = self._get_command_section(command)
41
+ if not section:
42
+ return None
43
+
44
+ # Extract text before first #### heading
45
+ match = re.match(r"(.*?)(?=\n####|\Z)", section, re.DOTALL)
46
+ return match.group(1).strip() if match else ""
47
+
48
+ def _get_examples(self, command: str) -> Optional[str]:
49
+ """Get examples section for a command."""
50
+ section = self._get_command_section(command)
51
+ if not section:
52
+ return None
53
+
54
+ # Extract Examples section
55
+ match = re.search(r"#### Examples\n```python\n(.*?)\n```", section, re.DOTALL)
56
+ return match.group(1).strip() if match else None
57
+
58
+ def _get_tips(self, command: str, context: Optional[Dict] = None) -> Optional[List[str]]:
59
+ """Get tips list for a command."""
60
+ section = self._get_command_section(command)
61
+ if not section:
62
+ return None
63
+
64
+ # Extract Tips section
65
+ match = re.search(r"#### Tips\n(.*?)(?=\n###|\n##|\Z)", section, re.DOTALL)
66
+ if not match:
67
+ return None
68
+
69
+ tips_text = match.group(1)
70
+ # Parse bullet points
71
+ tips = re.findall(r"^- (.+)$", tips_text, re.MULTILINE)
72
+
73
+ # Apply context substitutions
74
+ if context and tips:
75
+ formatted_tips = []
76
+ for tip in tips:
77
+ for key, value in context.items():
78
+ tip = tip.replace(f"{{{key}}}", str(value))
79
+ formatted_tips.append(tip)
80
+ return formatted_tips
81
+
82
+ return tips
83
+
84
+ def _get_mcp_description(self, command: str) -> Optional[str]:
85
+ """Build MCP description from markdown."""
86
+ description = self._get_description(command)
87
+ if not description:
88
+ return None
89
+
90
+ # Build complete MCP description
91
+ parts = [description]
92
+
93
+ # Add libraries section for certain commands
94
+ if command in ["body", "inspect"]:
95
+ parts.append("")
96
+ parts.append(self._get_libraries())
97
+
98
+ # Add examples if available
99
+ examples = self._get_examples(command)
100
+ if examples:
101
+ parts.append("")
102
+ parts.append("Examples:")
103
+ # Indent examples
104
+ for line in examples.split("\n"):
105
+ parts.append(f" {line}" if line else "")
106
+
107
+ return "\n".join(parts)
108
+
109
+
110
+ # Global parser instance
111
+ parser = TipsParser()
112
+
113
+
114
+ # Public API
115
+ def get_mcp_description(command: str) -> Optional[str]:
116
+ """Get MCP description for a command from TIPS.md.
117
+
118
+ Args:
119
+ command: Name of the command.
120
+ """
121
+ return parser._get_mcp_description(command)
122
+
123
+
124
+ def get_tips(command: str, context: Optional[Dict] = None) -> Optional[List[str]]:
125
+ """Get developer tips for a command from TIPS.md.
126
+
127
+ Args:
128
+ command: Name of the command.
129
+ context: Optional context for variable substitution.
130
+ """
131
+ return parser._get_tips(command, context)
132
+
133
+
134
+ def get_all_tips() -> Dict[str, List[str]]:
135
+ """Get all available tips from TIPS.md."""
136
+ all_tips = {}
137
+
138
+ # Find all command sections
139
+ pattern = r"### ([^\n]+)\n"
140
+ matches = re.findall(pattern, parser.content)
141
+
142
+ for command in matches:
143
+ tips = parser._get_tips(command)
144
+ if tips:
145
+ all_tips[command] = tips
146
+
147
+ return all_tips