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.
- webtap/VISION.md +234 -0
- webtap/__init__.py +56 -0
- webtap/api.py +222 -0
- webtap/app.py +76 -0
- webtap/cdp/README.md +268 -0
- webtap/cdp/__init__.py +14 -0
- webtap/cdp/query.py +107 -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 +365 -0
- webtap/commands/DEVELOPER_GUIDE.md +314 -0
- webtap/commands/TIPS.md +153 -0
- webtap/commands/__init__.py +7 -0
- webtap/commands/_builders.py +127 -0
- webtap/commands/_errors.py +108 -0
- webtap/commands/_tips.py +147 -0
- webtap/commands/_utils.py +227 -0
- webtap/commands/body.py +161 -0
- webtap/commands/connection.py +168 -0
- webtap/commands/console.py +69 -0
- webtap/commands/events.py +109 -0
- webtap/commands/fetch.py +219 -0
- webtap/commands/filters.py +224 -0
- webtap/commands/inspect.py +146 -0
- webtap/commands/javascript.py +87 -0
- webtap/commands/launch.py +86 -0
- webtap/commands/navigation.py +199 -0
- webtap/commands/network.py +85 -0
- webtap/commands/setup.py +127 -0
- webtap/filters.py +289 -0
- webtap/services/README.md +83 -0
- webtap/services/__init__.py +15 -0
- webtap/services/body.py +113 -0
- webtap/services/console.py +116 -0
- webtap/services/fetch.py +397 -0
- webtap/services/main.py +175 -0
- webtap/services/network.py +105 -0
- webtap/services/setup.py +219 -0
- webtap_tool-0.1.1.dist-info/METADATA +427 -0
- webtap_tool-0.1.1.dist-info/RECORD +43 -0
- webtap_tool-0.1.1.dist-info/WHEEL +4 -0
- webtap_tool-0.1.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Shared utilities for WebTap command modules."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from typing import Any, Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def evaluate_expression(expr: str, namespace: dict) -> Tuple[Any, str]:
|
|
11
|
+
"""Execute Python code and capture both stdout and the last expression result.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
expr: Python code to execute.
|
|
15
|
+
namespace: Dict of variables available to the code.
|
|
16
|
+
"""
|
|
17
|
+
# Standard libraries - always available
|
|
18
|
+
import re
|
|
19
|
+
import base64
|
|
20
|
+
import hashlib
|
|
21
|
+
import html
|
|
22
|
+
import urllib.parse
|
|
23
|
+
import datetime
|
|
24
|
+
import collections
|
|
25
|
+
import itertools
|
|
26
|
+
import pprint
|
|
27
|
+
import textwrap
|
|
28
|
+
import difflib
|
|
29
|
+
import xml.etree.ElementTree as ElementTree
|
|
30
|
+
|
|
31
|
+
# Web scraping & parsing
|
|
32
|
+
from bs4 import BeautifulSoup
|
|
33
|
+
import lxml.etree
|
|
34
|
+
import lxml.html
|
|
35
|
+
|
|
36
|
+
# Reverse engineering essentials
|
|
37
|
+
import jwt
|
|
38
|
+
import yaml
|
|
39
|
+
import httpx
|
|
40
|
+
import cryptography.fernet
|
|
41
|
+
import cryptography.hazmat
|
|
42
|
+
from google.protobuf import json_format as protobuf_json
|
|
43
|
+
from google.protobuf import text_format as protobuf_text
|
|
44
|
+
import msgpack
|
|
45
|
+
|
|
46
|
+
# Update namespace with ALL libraries
|
|
47
|
+
namespace.update(
|
|
48
|
+
{
|
|
49
|
+
# Standard
|
|
50
|
+
"re": re,
|
|
51
|
+
"json": json, # Already imported at module level
|
|
52
|
+
"base64": base64,
|
|
53
|
+
"hashlib": hashlib,
|
|
54
|
+
"html": html,
|
|
55
|
+
"urllib": urllib,
|
|
56
|
+
"datetime": datetime,
|
|
57
|
+
"collections": collections,
|
|
58
|
+
"itertools": itertools,
|
|
59
|
+
"pprint": pprint,
|
|
60
|
+
"textwrap": textwrap,
|
|
61
|
+
"difflib": difflib,
|
|
62
|
+
"ast": ast, # Already imported at module level
|
|
63
|
+
"ElementTree": ElementTree,
|
|
64
|
+
"ET": ElementTree, # Common alias
|
|
65
|
+
# Web scraping
|
|
66
|
+
"BeautifulSoup": BeautifulSoup,
|
|
67
|
+
"bs4": BeautifulSoup, # Alias
|
|
68
|
+
"lxml": lxml,
|
|
69
|
+
# Reverse engineering
|
|
70
|
+
"jwt": jwt,
|
|
71
|
+
"yaml": yaml,
|
|
72
|
+
"httpx": httpx,
|
|
73
|
+
"cryptography": cryptography,
|
|
74
|
+
"protobuf_json": protobuf_json,
|
|
75
|
+
"protobuf_text": protobuf_text,
|
|
76
|
+
"msgpack": msgpack,
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Capture stdout
|
|
81
|
+
old_stdout = sys.stdout
|
|
82
|
+
sys.stdout = captured_output = StringIO()
|
|
83
|
+
result = None
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
# Parse the code to find if last node is an expression
|
|
87
|
+
tree = ast.parse(expr)
|
|
88
|
+
if tree.body:
|
|
89
|
+
# If last node is an Expression, evaluate it separately
|
|
90
|
+
if isinstance(tree.body[-1], ast.Expr):
|
|
91
|
+
# Execute all but the last node
|
|
92
|
+
if len(tree.body) > 1:
|
|
93
|
+
exec_tree = ast.Module(body=tree.body[:-1], type_ignores=[])
|
|
94
|
+
exec(compile(exec_tree, "<string>", "exec"), namespace)
|
|
95
|
+
# Evaluate the last expression
|
|
96
|
+
result = eval(compile(ast.Expression(body=tree.body[-1].value), "<string>", "eval"), namespace)
|
|
97
|
+
else:
|
|
98
|
+
# All statements, just exec everything
|
|
99
|
+
exec(compile(tree, "<string>", "exec"), namespace)
|
|
100
|
+
|
|
101
|
+
except SyntaxError:
|
|
102
|
+
# Fallback to simple exec if parsing fails
|
|
103
|
+
exec(expr, namespace)
|
|
104
|
+
finally:
|
|
105
|
+
# Always restore stdout
|
|
106
|
+
sys.stdout = old_stdout
|
|
107
|
+
output = captured_output.getvalue()
|
|
108
|
+
|
|
109
|
+
return result, output
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def format_expression_result(result: Any, output: str, max_length: int = 2000) -> str:
|
|
113
|
+
"""Format the result of an expression evaluation for display.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
result: The evaluation result.
|
|
117
|
+
output: Any stdout output captured.
|
|
118
|
+
max_length: Maximum length before truncation.
|
|
119
|
+
"""
|
|
120
|
+
parts = []
|
|
121
|
+
|
|
122
|
+
if output:
|
|
123
|
+
parts.append(output.rstrip())
|
|
124
|
+
|
|
125
|
+
if result is not None:
|
|
126
|
+
if isinstance(result, (dict, list)):
|
|
127
|
+
formatted = json.dumps(result, indent=2)
|
|
128
|
+
if len(formatted) > max_length:
|
|
129
|
+
parts.append(formatted[:max_length] + f"\n... [truncated, {len(formatted)} chars total]")
|
|
130
|
+
else:
|
|
131
|
+
parts.append(formatted)
|
|
132
|
+
elif isinstance(result, str) and len(result) > max_length:
|
|
133
|
+
parts.append(result[:max_length] + f"\n... [truncated, {len(result)} chars total]")
|
|
134
|
+
else:
|
|
135
|
+
parts.append(str(result))
|
|
136
|
+
|
|
137
|
+
return "\n".join(parts) if parts else "(no output)"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ============= MCP Dict Parameter Utilities =============
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ============= MCP Dict Parameter Utilities =============
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def parse_options(options: dict = None, defaults: dict = None) -> dict: # pyright: ignore[reportArgumentType]
|
|
147
|
+
"""Parse options dict with defaults.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
options: User-provided options dict.
|
|
151
|
+
defaults: Default values dict.
|
|
152
|
+
"""
|
|
153
|
+
if defaults is None:
|
|
154
|
+
defaults = {}
|
|
155
|
+
if options is None:
|
|
156
|
+
return defaults.copy()
|
|
157
|
+
|
|
158
|
+
result = defaults.copy()
|
|
159
|
+
result.update(options)
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def extract_option(options: dict, key: str, default=None, required: bool = False):
|
|
164
|
+
"""Extract single option from dict with validation.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
options: Options dict to extract from.
|
|
168
|
+
key: Key to extract.
|
|
169
|
+
default: Default value if not found.
|
|
170
|
+
required: Whether the key is required.
|
|
171
|
+
"""
|
|
172
|
+
if options is None:
|
|
173
|
+
if required:
|
|
174
|
+
raise ValueError(f"Required option '{key}' not provided")
|
|
175
|
+
return default
|
|
176
|
+
|
|
177
|
+
if required and key not in options:
|
|
178
|
+
raise ValueError(f"Required option '{key}' not provided")
|
|
179
|
+
|
|
180
|
+
return options.get(key, default)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def validate_dict_keys(options: dict, allowed: set, required: set = None) -> dict: # pyright: ignore[reportArgumentType]
|
|
184
|
+
"""Validate dict has only allowed keys and all required keys.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
options: Dict to validate.
|
|
188
|
+
allowed: Set of allowed keys.
|
|
189
|
+
required: Optional set of required keys.
|
|
190
|
+
"""
|
|
191
|
+
if options is None:
|
|
192
|
+
options = {}
|
|
193
|
+
|
|
194
|
+
# Check for unknown keys
|
|
195
|
+
unknown = set(options.keys()) - allowed
|
|
196
|
+
if unknown:
|
|
197
|
+
raise ValueError(f"Unknown options: {', '.join(sorted(unknown))}")
|
|
198
|
+
|
|
199
|
+
# Check for required keys
|
|
200
|
+
if required:
|
|
201
|
+
missing = required - set(options.keys())
|
|
202
|
+
if missing:
|
|
203
|
+
raise ValueError(f"Missing required options: {', '.join(sorted(missing))}")
|
|
204
|
+
|
|
205
|
+
return options
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def extract_nested(options: dict, path: str, default=None):
|
|
209
|
+
"""Extract nested value from dict using dot notation.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
options: Dict to extract from.
|
|
213
|
+
path: Dot-separated path.
|
|
214
|
+
default: Default value if path not found.
|
|
215
|
+
"""
|
|
216
|
+
if options is None:
|
|
217
|
+
return default
|
|
218
|
+
|
|
219
|
+
current = options
|
|
220
|
+
for key in path.split("."):
|
|
221
|
+
if not isinstance(current, dict):
|
|
222
|
+
return default
|
|
223
|
+
current = current.get(key)
|
|
224
|
+
if current is None:
|
|
225
|
+
return default
|
|
226
|
+
|
|
227
|
+
return current
|
webtap/commands/body.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""HTTP response body inspection and analysis commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from webtap.app import app
|
|
5
|
+
from webtap.commands._utils import evaluate_expression, format_expression_result
|
|
6
|
+
from webtap.commands._errors import check_connection
|
|
7
|
+
from webtap.commands._builders import info_response, error_response
|
|
8
|
+
from webtap.commands._tips import get_mcp_description
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
mcp_desc = get_mcp_description("body")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command(display="markdown", fastmcp={"type": "tool", "description": mcp_desc} if mcp_desc else {"type": "tool"})
|
|
15
|
+
def body(state, response: int, expr: str = None, decode: bool = True, cache: bool = True) -> dict: # pyright: ignore[reportArgumentType]
|
|
16
|
+
"""Fetch and analyze response body with Python expressions.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
response: Response row ID from network() or requests()
|
|
20
|
+
expr: Optional Python expression with 'body' variable
|
|
21
|
+
decode: Auto-decode base64 (default: True)
|
|
22
|
+
cache: Use cached body (default: True)
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Body content or expression result
|
|
26
|
+
"""
|
|
27
|
+
if error := check_connection(state):
|
|
28
|
+
return error
|
|
29
|
+
|
|
30
|
+
# Get body from service (with optional caching)
|
|
31
|
+
body_service = state.service.body
|
|
32
|
+
result = body_service.get_response_body(response, use_cache=cache)
|
|
33
|
+
|
|
34
|
+
if "error" in result:
|
|
35
|
+
return error_response(result["error"])
|
|
36
|
+
|
|
37
|
+
body_content = result.get("body", "")
|
|
38
|
+
is_base64 = result.get("base64Encoded", False)
|
|
39
|
+
|
|
40
|
+
# Handle base64 decoding if requested
|
|
41
|
+
if is_base64 and decode:
|
|
42
|
+
decoded = body_service.decode_body(body_content, is_base64)
|
|
43
|
+
if isinstance(decoded, bytes):
|
|
44
|
+
# Binary content - can't show directly
|
|
45
|
+
if not expr:
|
|
46
|
+
return info_response(
|
|
47
|
+
title="Response Body",
|
|
48
|
+
fields={
|
|
49
|
+
"Type": "Binary content",
|
|
50
|
+
"Size (base64)": f"{len(body_content)} bytes",
|
|
51
|
+
"Size (decoded)": f"{len(decoded)} bytes",
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
# For expressions, provide the bytes
|
|
55
|
+
body_content = decoded
|
|
56
|
+
else:
|
|
57
|
+
# Successfully decoded to text
|
|
58
|
+
body_content = decoded
|
|
59
|
+
|
|
60
|
+
# No expression - return the body directly
|
|
61
|
+
if not expr:
|
|
62
|
+
if isinstance(body_content, bytes):
|
|
63
|
+
return info_response(
|
|
64
|
+
title="Response Body", fields={"Type": "Binary content", "Size": f"{len(body_content)} bytes"}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Build markdown response with body in code block
|
|
68
|
+
# DATA-LEVEL TRUNCATION for memory/performance (as per refactor plan)
|
|
69
|
+
MAX_BODY_SIZE = 5000 # Keep data-level truncation for large bodies
|
|
70
|
+
elements = [{"type": "heading", "content": "Response Body", "level": 2}]
|
|
71
|
+
|
|
72
|
+
# Try to detect content type and format appropriately
|
|
73
|
+
content_preview = body_content[:100]
|
|
74
|
+
if content_preview.strip().startswith("{") or content_preview.strip().startswith("["):
|
|
75
|
+
# Likely JSON
|
|
76
|
+
try:
|
|
77
|
+
parsed = json.loads(body_content)
|
|
78
|
+
formatted = json.dumps(parsed, indent=2)
|
|
79
|
+
if len(formatted) > MAX_BODY_SIZE:
|
|
80
|
+
elements.append({"type": "code_block", "content": formatted[:MAX_BODY_SIZE], "language": "json"})
|
|
81
|
+
elements.append(
|
|
82
|
+
{"type": "text", "content": f"_[truncated at {MAX_BODY_SIZE} chars, {len(formatted)} total]_"}
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
elements.append({"type": "code_block", "content": formatted, "language": "json"})
|
|
86
|
+
except (json.JSONDecodeError, ValueError):
|
|
87
|
+
# Not valid JSON, show as text
|
|
88
|
+
if len(body_content) > MAX_BODY_SIZE:
|
|
89
|
+
elements.append({"type": "code_block", "content": body_content[:MAX_BODY_SIZE], "language": ""})
|
|
90
|
+
elements.append(
|
|
91
|
+
{
|
|
92
|
+
"type": "text",
|
|
93
|
+
"content": f"_[truncated at {MAX_BODY_SIZE} chars, {len(body_content)} total]_",
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
elements.append({"type": "code_block", "content": body_content, "language": ""})
|
|
98
|
+
elif content_preview.strip().startswith("<"):
|
|
99
|
+
# Likely HTML/XML
|
|
100
|
+
if len(body_content) > MAX_BODY_SIZE:
|
|
101
|
+
elements.append({"type": "code_block", "content": body_content[:MAX_BODY_SIZE], "language": "html"})
|
|
102
|
+
elements.append(
|
|
103
|
+
{"type": "text", "content": f"_[truncated at {MAX_BODY_SIZE} chars, {len(body_content)} total]_"}
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
elements.append({"type": "code_block", "content": body_content, "language": "html"})
|
|
107
|
+
else:
|
|
108
|
+
# Plain text or unknown
|
|
109
|
+
if len(body_content) > MAX_BODY_SIZE:
|
|
110
|
+
elements.append({"type": "code_block", "content": body_content[:MAX_BODY_SIZE], "language": ""})
|
|
111
|
+
elements.append(
|
|
112
|
+
{"type": "text", "content": f"_[truncated at {MAX_BODY_SIZE} chars, {len(body_content)} total]_"}
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
elements.append({"type": "code_block", "content": body_content, "language": ""})
|
|
116
|
+
|
|
117
|
+
elements.append({"type": "text", "content": f"\n**Size:** {len(body_content)} characters"})
|
|
118
|
+
return {"elements": elements}
|
|
119
|
+
|
|
120
|
+
# Evaluate expression with body available
|
|
121
|
+
try:
|
|
122
|
+
namespace = {"body": body_content}
|
|
123
|
+
result, output = evaluate_expression(expr, namespace)
|
|
124
|
+
formatted_result = format_expression_result(result, output)
|
|
125
|
+
|
|
126
|
+
# Build markdown response
|
|
127
|
+
return {
|
|
128
|
+
"elements": [
|
|
129
|
+
{"type": "heading", "content": "Expression Result", "level": 2},
|
|
130
|
+
{"type": "code_block", "content": expr, "language": "python"},
|
|
131
|
+
{"type": "text", "content": "**Result:**"},
|
|
132
|
+
{"type": "code_block", "content": formatted_result, "language": ""},
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
except Exception as e:
|
|
136
|
+
# Provide helpful suggestions based on the error type
|
|
137
|
+
suggestions = ["The body is available as 'body' variable"]
|
|
138
|
+
|
|
139
|
+
if "NameError" in str(type(e).__name__):
|
|
140
|
+
suggestions.extend(
|
|
141
|
+
[
|
|
142
|
+
"Common libraries are pre-imported: re, json, bs4, jwt, httpx",
|
|
143
|
+
"Example: bs4(body, 'html.parser').find('title')",
|
|
144
|
+
]
|
|
145
|
+
)
|
|
146
|
+
elif "JSONDecodeError" in str(e):
|
|
147
|
+
suggestions.extend(
|
|
148
|
+
[
|
|
149
|
+
"Body might not be valid JSON. Try: type(body) to check",
|
|
150
|
+
"For HTML, use: bs4(body, 'html.parser')",
|
|
151
|
+
]
|
|
152
|
+
)
|
|
153
|
+
elif "KeyError" in str(e):
|
|
154
|
+
suggestions.extend(
|
|
155
|
+
[
|
|
156
|
+
"Key not found. Try: json.loads(body).keys() to see available keys",
|
|
157
|
+
"Use .get() for safe access: data.get('key', 'default')",
|
|
158
|
+
]
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return error_response(f"{type(e).__name__}: {e}", suggestions=suggestions)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Chrome browser connection management commands."""
|
|
2
|
+
|
|
3
|
+
from webtap.app import app
|
|
4
|
+
from webtap.commands._errors import check_connection
|
|
5
|
+
from webtap.commands._builders import info_response, table_response, error_response
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
9
|
+
def connect(state, page: int = None, page_id: str = None) -> dict: # pyright: ignore[reportArgumentType]
|
|
10
|
+
"""Connect to Chrome page and enable all required domains.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
page: Connect by page index (0-based)
|
|
14
|
+
page_id: Connect by page ID
|
|
15
|
+
|
|
16
|
+
Note: If neither is specified, connects to first available page.
|
|
17
|
+
Cannot specify both page and page_id.
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
connect() # First page
|
|
21
|
+
connect(page=2) # Third page (0-indexed)
|
|
22
|
+
connect(page_id="xyz") # Specific page ID
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Connection status in markdown
|
|
26
|
+
"""
|
|
27
|
+
if page is not None and page_id is not None:
|
|
28
|
+
return error_response("Cannot specify both 'page' and 'page_id'. Use one or the other.")
|
|
29
|
+
|
|
30
|
+
result = state.service.connect_to_page(page_index=page, page_id=page_id)
|
|
31
|
+
|
|
32
|
+
if "error" in result:
|
|
33
|
+
return error_response(result["error"])
|
|
34
|
+
|
|
35
|
+
# Success - return formatted info with full URL
|
|
36
|
+
return info_response(
|
|
37
|
+
title="Connection Established",
|
|
38
|
+
fields={"Page": result["title"], "URL": result["url"]}, # Full URL
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
43
|
+
def disconnect(state) -> dict:
|
|
44
|
+
"""Disconnect from Chrome."""
|
|
45
|
+
result = state.service.disconnect()
|
|
46
|
+
|
|
47
|
+
if not result["was_connected"]:
|
|
48
|
+
return info_response(title="Disconnect Status", fields={"Status": "Not connected"})
|
|
49
|
+
|
|
50
|
+
return info_response(title="Disconnect Status", fields={"Status": "Disconnected"})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command(display="markdown", fastmcp={"type": "tool"})
|
|
54
|
+
def clear(state, events: bool = True, console: bool = False, cache: bool = False) -> dict:
|
|
55
|
+
"""Clear various data stores.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
events: Clear CDP events (default: True)
|
|
59
|
+
console: Clear console messages (default: False)
|
|
60
|
+
cache: Clear body cache (default: False)
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
clear() # Clear events only
|
|
64
|
+
clear(events=True, console=True) # Clear events and console
|
|
65
|
+
clear(cache=True) # Clear cache only
|
|
66
|
+
clear(events=False, console=True) # Console only
|
|
67
|
+
clear(events=True, console=True, cache=True) # Clear everything
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Summary of what was cleared
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
cleared = []
|
|
74
|
+
|
|
75
|
+
# Clear CDP events
|
|
76
|
+
if events:
|
|
77
|
+
state.service.clear_events()
|
|
78
|
+
cleared.append("events")
|
|
79
|
+
|
|
80
|
+
# Clear browser console
|
|
81
|
+
if console:
|
|
82
|
+
if state.cdp and state.cdp.is_connected:
|
|
83
|
+
if state.service.console.clear_browser_console():
|
|
84
|
+
cleared.append("console")
|
|
85
|
+
else:
|
|
86
|
+
cleared.append("console (not connected)")
|
|
87
|
+
|
|
88
|
+
# Clear body cache
|
|
89
|
+
if cache:
|
|
90
|
+
if hasattr(state.service, "body") and state.service.body:
|
|
91
|
+
count = state.service.body.clear_cache()
|
|
92
|
+
cleared.append(f"cache ({count} bodies)")
|
|
93
|
+
else:
|
|
94
|
+
cleared.append("cache (0 bodies)")
|
|
95
|
+
|
|
96
|
+
# Return summary
|
|
97
|
+
if not cleared:
|
|
98
|
+
return info_response(
|
|
99
|
+
title="Clear Status",
|
|
100
|
+
fields={"Result": "Nothing to clear (specify events=True, console=True, or cache=True)"},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return info_response(title="Clear Status", fields={"Cleared": ", ".join(cleared)})
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@app.command(
|
|
107
|
+
display="markdown",
|
|
108
|
+
truncate={
|
|
109
|
+
"Title": {"max": 20, "mode": "end"},
|
|
110
|
+
"URL": {"max": 30, "mode": "middle"},
|
|
111
|
+
"ID": {"max": 6, "mode": "end"},
|
|
112
|
+
},
|
|
113
|
+
fastmcp={"type": "resource", "mime_type": "application/json"},
|
|
114
|
+
)
|
|
115
|
+
def pages(state) -> dict:
|
|
116
|
+
"""List available Chrome pages.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Table of available pages in markdown
|
|
120
|
+
"""
|
|
121
|
+
result = state.service.list_pages()
|
|
122
|
+
pages_list = result.get("pages", [])
|
|
123
|
+
|
|
124
|
+
# Format rows for table with FULL data
|
|
125
|
+
rows = [
|
|
126
|
+
{
|
|
127
|
+
"Index": str(i),
|
|
128
|
+
"Title": p.get("title", "Untitled"), # Full title
|
|
129
|
+
"URL": p.get("url", ""), # Full URL
|
|
130
|
+
"ID": p.get("id", ""), # Full ID
|
|
131
|
+
"Connected": "Yes" if p.get("is_connected") else "No",
|
|
132
|
+
}
|
|
133
|
+
for i, p in enumerate(pages_list)
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
# Build markdown response
|
|
137
|
+
return table_response(
|
|
138
|
+
title="Chrome Pages",
|
|
139
|
+
headers=["Index", "Title", "URL", "ID", "Connected"],
|
|
140
|
+
rows=rows,
|
|
141
|
+
summary=f"{len(pages_list)} page{'s' if len(pages_list) != 1 else ''} available",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@app.command(display="markdown", fastmcp={"type": "resource", "mime_type": "application/json"})
|
|
146
|
+
def status(state) -> dict:
|
|
147
|
+
"""Get connection status.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Status information in markdown
|
|
151
|
+
"""
|
|
152
|
+
# Check connection - return error dict if not connected
|
|
153
|
+
if error := check_connection(state):
|
|
154
|
+
return error
|
|
155
|
+
|
|
156
|
+
status = state.service.get_status()
|
|
157
|
+
|
|
158
|
+
# Build formatted response with full URL
|
|
159
|
+
return info_response(
|
|
160
|
+
title="Connection Status",
|
|
161
|
+
fields={
|
|
162
|
+
"Page": status.get("title", "Unknown"),
|
|
163
|
+
"URL": status.get("url", ""), # Full URL
|
|
164
|
+
"Events": f"{status['events']} stored",
|
|
165
|
+
"Fetch": "Enabled" if status["fetch_enabled"] else "Disabled",
|
|
166
|
+
"Domains": ", ".join(status["enabled_domains"]),
|
|
167
|
+
},
|
|
168
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Browser console message monitoring and display commands."""
|
|
2
|
+
|
|
3
|
+
from webtap.app import app
|
|
4
|
+
from webtap.commands._builders import table_response
|
|
5
|
+
from webtap.commands._errors import check_connection
|
|
6
|
+
from webtap.commands._tips import get_tips
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@app.command(
|
|
10
|
+
display="markdown",
|
|
11
|
+
truncate={"Message": {"max": 80, "mode": "end"}},
|
|
12
|
+
transforms={"Time": "format_timestamp"},
|
|
13
|
+
fastmcp={"type": "resource", "mime_type": "application/json"},
|
|
14
|
+
)
|
|
15
|
+
def console(state, limit: int = 50) -> dict:
|
|
16
|
+
"""Show console messages with full data.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
limit: Max results (default: 50)
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
console() # Recent console messages
|
|
23
|
+
console(limit=100) # Show more messages
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Table of console messages with full data
|
|
27
|
+
"""
|
|
28
|
+
# Check connection
|
|
29
|
+
if error := check_connection(state):
|
|
30
|
+
return error
|
|
31
|
+
|
|
32
|
+
# Get data from service
|
|
33
|
+
results = state.service.console.get_recent_messages(limit=limit)
|
|
34
|
+
|
|
35
|
+
# Build rows with FULL data
|
|
36
|
+
rows = []
|
|
37
|
+
for row in results:
|
|
38
|
+
rowid, level, source, message, timestamp = row
|
|
39
|
+
rows.append(
|
|
40
|
+
{
|
|
41
|
+
"ID": str(rowid),
|
|
42
|
+
"Level": (level or "LOG").upper(),
|
|
43
|
+
"Source": source or "console",
|
|
44
|
+
"Message": message or "", # Full message
|
|
45
|
+
"Time": timestamp or 0, # Raw timestamp for transform
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Build response
|
|
50
|
+
warnings = []
|
|
51
|
+
if limit and len(results) == limit:
|
|
52
|
+
warnings.append(f"Showing first {limit} messages (use limit parameter to see more)")
|
|
53
|
+
|
|
54
|
+
# Get contextual tips from TIPS.md
|
|
55
|
+
tips = None
|
|
56
|
+
if rows:
|
|
57
|
+
# Focus on error/warning messages for debugging
|
|
58
|
+
error_rows = [r for r in rows if r.get("Level", "").upper() in ["ERROR", "WARN", "WARNING"]]
|
|
59
|
+
example_id = error_rows[0]["ID"] if error_rows else rows[0]["ID"]
|
|
60
|
+
tips = get_tips("console", context={"id": example_id})
|
|
61
|
+
|
|
62
|
+
return table_response(
|
|
63
|
+
title="Console Messages",
|
|
64
|
+
headers=["ID", "Level", "Source", "Message", "Time"],
|
|
65
|
+
rows=rows,
|
|
66
|
+
summary=f"{len(rows)} messages",
|
|
67
|
+
warnings=warnings,
|
|
68
|
+
tips=tips,
|
|
69
|
+
)
|