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
webtap/commands/TIPS.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
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
|
+
### request
|
|
15
|
+
Get HAR request details with field selection and Python expressions.
|
|
16
|
+
|
|
17
|
+
#### Examples
|
|
18
|
+
```python
|
|
19
|
+
request(123) # Minimal (method, url, status)
|
|
20
|
+
request(123, ["*"]) # Everything
|
|
21
|
+
request(123, ["request.headers.*"]) # Request headers
|
|
22
|
+
request(123, ["response.content"]) # Fetch response body
|
|
23
|
+
request(123, ["request.postData", "response.content"]) # Both bodies
|
|
24
|
+
request(123, ["response.content"], expr="json.loads(data['response']['content']['text'])") # Parse JSON
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
#### Tips
|
|
28
|
+
- **Field patterns:** `["request.*"]`, `["response.headers.*"]`, `["response.content"]`
|
|
29
|
+
- **Expression:** `expr` has access to selected data as `data` variable
|
|
30
|
+
- **Parse JSON:** `expr="json.loads(data['response']['content']['text'])"`
|
|
31
|
+
- **Generate models:** `to_model(id, "models/model.py", "Model")` - create Pydantic from body
|
|
32
|
+
- **Generate types:** `quicktype(id, "types.ts", "Type")` - TypeScript/other languages
|
|
33
|
+
|
|
34
|
+
### to_model
|
|
35
|
+
Generate Pydantic v2 models from request or response bodies.
|
|
36
|
+
|
|
37
|
+
Uses HAR row ID from `network()` output with explicit field selection.
|
|
38
|
+
|
|
39
|
+
#### Examples
|
|
40
|
+
```python
|
|
41
|
+
# Response bodies (default)
|
|
42
|
+
to_model(5, "models/product.py", "Product")
|
|
43
|
+
to_model(5, "models/user.py", "User", json_path="data[0]")
|
|
44
|
+
|
|
45
|
+
# Request bodies (POST/PUT/PATCH)
|
|
46
|
+
to_model(5, "models/form.py", "Form", field="request.postData")
|
|
47
|
+
to_model(5, "models/form.py", "Form", field="request.postData", expr="dict(urllib.parse.parse_qsl(body))")
|
|
48
|
+
|
|
49
|
+
# Advanced transformations
|
|
50
|
+
to_model(5, "models/clean.py", "Clean", expr="{k: v for k, v in json.loads(body).items() if k != 'meta'}")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### Tips
|
|
54
|
+
- **Preview body:** `request(id, ["response.content"])` - check structure before generating
|
|
55
|
+
- **Find requests:** `network(url="*api*")` - locate API calls
|
|
56
|
+
- **Form data:** `field="request.postData"` with `expr="dict(urllib.parse.parse_qsl(body))"`
|
|
57
|
+
- **Nested extraction:** `json_path="data[0]"` for JSON with wrapper objects
|
|
58
|
+
- **Custom transforms:** `expr` has `body` (str) variable available
|
|
59
|
+
- **Organization:** Paths like `"models/customers/group.py"` create directory structure automatically
|
|
60
|
+
|
|
61
|
+
### quicktype
|
|
62
|
+
Generate types from request or response bodies. Supports TypeScript, Go, Rust, Python, and 10+ other languages.
|
|
63
|
+
|
|
64
|
+
Uses HAR row ID from `network()` output with explicit field selection.
|
|
65
|
+
|
|
66
|
+
#### Examples
|
|
67
|
+
```python
|
|
68
|
+
# Response bodies (default)
|
|
69
|
+
quicktype(5, "types/User.ts", "User")
|
|
70
|
+
quicktype(5, "api.go", "ApiResponse")
|
|
71
|
+
quicktype(5, "schema.json", "Schema")
|
|
72
|
+
quicktype(5, "types.ts", "User", json_path="data[0]")
|
|
73
|
+
|
|
74
|
+
# Request bodies (POST/PUT/PATCH)
|
|
75
|
+
quicktype(5, "types/Form.ts", "Form", field="request.postData")
|
|
76
|
+
quicktype(5, "types/Form.ts", "Form", field="request.postData", expr="dict(urllib.parse.parse_qsl(body))")
|
|
77
|
+
|
|
78
|
+
# Advanced options
|
|
79
|
+
quicktype(5, "types.ts", "User", options={"readonly": True})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### Tips
|
|
83
|
+
- **Preview body:** `request(id, ["response.content"])` - check structure before generating
|
|
84
|
+
- **Find requests:** `network(url="*api*")` - locate API calls
|
|
85
|
+
- **Form data:** `field="request.postData"` with `expr="dict(urllib.parse.parse_qsl(body))"`
|
|
86
|
+
- **Nested extraction:** `json_path="data[0]"` for JSON with wrapper objects
|
|
87
|
+
- **Languages:** .ts/.go/.rs/.java/.kt/.swift/.cs/.cpp/.dart/.rb/.json extensions set language
|
|
88
|
+
- **Options:** Dict keys map to CLI flags: `{"readonly": True}` → `--readonly`
|
|
89
|
+
- **Install:** `npm install -g quicktype` if command not found
|
|
90
|
+
- **Pydantic:** Use `to_model(id, "models/model.py", "Model")` for Pydantic v2 instead
|
|
91
|
+
|
|
92
|
+
### network
|
|
93
|
+
Show network requests with full data. Use `req_state="paused"` to filter paused requests.
|
|
94
|
+
|
|
95
|
+
#### Tips
|
|
96
|
+
- **Analyze responses:** `request({id}, ["response.content"])` - fetch response body
|
|
97
|
+
- **Generate models:** `to_model({id}, "models/model.py", "Model")` - create Pydantic models from JSON
|
|
98
|
+
- **Parse HTML:** `request({id}, ["response.content"], expr="bs4(data['response']['content']['text'], 'html.parser').find('title').text")`
|
|
99
|
+
- **Extract JSON:** `request({id}, ["response.content"], expr="json.loads(data['response']['content']['text'])['data']")`
|
|
100
|
+
- **Find patterns:** `network(url="*api*")` - filter by URL pattern
|
|
101
|
+
- **View paused only:** `network(req_state="paused")` - show only paused requests
|
|
102
|
+
- **Intercept traffic:** `fetch('enable')` then `resume({id})` or `fail({id})` to control
|
|
103
|
+
|
|
104
|
+
### console
|
|
105
|
+
Show console messages with full data.
|
|
106
|
+
|
|
107
|
+
#### Tips
|
|
108
|
+
- **Filter errors:** `console(level="error")` - show only errors
|
|
109
|
+
- **Check network:** `network()` - may show failed requests causing errors
|
|
110
|
+
- **Debug with js:** `js("console.log('debug:', myVar)")` - add console output
|
|
111
|
+
|
|
112
|
+
### js
|
|
113
|
+
Execute JavaScript in the browser. Uses fresh scope by default to avoid redeclaration errors.
|
|
114
|
+
|
|
115
|
+
#### Expression Mode (Important!)
|
|
116
|
+
Default mode wraps code as `return (code)`, so **use single expressions only**:
|
|
117
|
+
```python
|
|
118
|
+
js("document.title") # ✓ Single expression
|
|
119
|
+
js("1 + 2 + 3") # ✓ Single expression
|
|
120
|
+
js("[...document.links].map(a=>a.href)") # ✓ Single expression (chained)
|
|
121
|
+
js("const x = 1; x + 1") # ✗ Multi-statement fails (semicolon)
|
|
122
|
+
js("let y = 2; return y") # ✗ Fails - already wrapped in return
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
For multi-statement code, use `persist=True` (runs in global scope, not wrapped):
|
|
126
|
+
```python
|
|
127
|
+
js("var x = 1; x + 1", persist=True) # ✓ Multi-statement works
|
|
128
|
+
js("const arr = [1,2,3]; arr.map(x => x*2)", persist=True) # ✓ Works
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### Scope Behavior
|
|
132
|
+
**Default (fresh scope)** - Each call runs in isolation via IIFE wrapper:
|
|
133
|
+
```python
|
|
134
|
+
js("document.title") # ✓ Returns title
|
|
135
|
+
js("document.title") # ✓ No redeclaration issues
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Persistent scope** - Variables survive across calls (global scope):
|
|
139
|
+
```python
|
|
140
|
+
js("var data = {count: 0}", persist=True) # data persists
|
|
141
|
+
js("data.count++", persist=True) # Modifies data
|
|
142
|
+
js("data.count", persist=True) # Returns 1
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**With browser element** - Always fresh scope (element injected):
|
|
146
|
+
```python
|
|
147
|
+
js("element.offsetWidth", selection=1) # Use element #1
|
|
148
|
+
js("element.classList", selection=2) # Use element #2
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### Examples
|
|
152
|
+
```python
|
|
153
|
+
# Basic queries (single expressions)
|
|
154
|
+
js("document.title") # Get page title
|
|
155
|
+
js("[...document.links].map(a => a.href)") # Get all links
|
|
156
|
+
js("document.body.innerText.length") # Text length
|
|
157
|
+
|
|
158
|
+
# Async operations
|
|
159
|
+
js("fetch('/api').then(r => r.json())", await_promise=True)
|
|
160
|
+
|
|
161
|
+
# DOM manipulation (no return)
|
|
162
|
+
js("document.querySelectorAll('.ad').forEach(e => e.remove())", wait_return=False)
|
|
163
|
+
|
|
164
|
+
# Multi-statement operations (use persist=True)
|
|
165
|
+
js("var apiData = null", persist=True)
|
|
166
|
+
js("fetch('/api').then(r => r.json()).then(d => apiData = d)", persist=True, await_promise=True)
|
|
167
|
+
js("apiData.users.length", persist=True)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### Tips
|
|
171
|
+
- **Expression mode:** Default wraps as `return (code)` - use single expressions or `persist=True`
|
|
172
|
+
- **Fresh scope:** Default behavior prevents const/let redeclaration errors
|
|
173
|
+
- **Persistent state:** Use `persist=True` for multi-step operations, global hooks, or multi-statement code
|
|
174
|
+
- **No return needed:** Set `wait_return=False` for DOM manipulation or hooks
|
|
175
|
+
- **Browser selections:** Use `selection=N` with browser() to operate on selected elements
|
|
176
|
+
- **Check console:** `console()` - see logged messages from JS execution
|
|
177
|
+
- **Hook fetch:** `js("window.fetch = new Proxy(fetch, {apply: (t, _, a) => {console.log(a); return t(...a)}})", persist=True, wait_return=False)`
|
|
178
|
+
|
|
179
|
+
### fetch
|
|
180
|
+
Control request interception for debugging and modification.
|
|
181
|
+
|
|
182
|
+
#### Examples
|
|
183
|
+
```python
|
|
184
|
+
fetch("status") # Check status
|
|
185
|
+
fetch("enable") # Enable request stage
|
|
186
|
+
fetch("enable", {"response": true}) # Both stages
|
|
187
|
+
fetch("disable") # Disable
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### Tips
|
|
191
|
+
- **View paused:** `network(req_state="paused")` or `requests()` - see intercepted requests
|
|
192
|
+
- **View details:** `request({id})` - view request/response data
|
|
193
|
+
- **Resume request:** `resume({id})` - continue the request
|
|
194
|
+
- **Modify request:** `resume({id}, modifications={'url': '...'})`
|
|
195
|
+
- **Block request:** `fail({id}, 'BlockedByClient')` - reject the request
|
|
196
|
+
- **Mock response:** `fulfill({id}, body='{"ok":true}')` - return custom response without server
|
|
197
|
+
|
|
198
|
+
### requests
|
|
199
|
+
Show paused requests. Equivalent to `network(req_state="paused")`.
|
|
200
|
+
|
|
201
|
+
#### Tips
|
|
202
|
+
- **View details:** `request({id})` - view request/response data
|
|
203
|
+
- **Resume request:** `resume({id})` - continue the request
|
|
204
|
+
- **Modify request:** `resume({id}, modifications={'url': '...'})`
|
|
205
|
+
- **Fail request:** `fail({id}, 'BlockedByClient')` - block the request
|
|
206
|
+
- **Mock response:** `fulfill({id}, body='...')` - return custom response
|
|
207
|
+
|
|
208
|
+
### fulfill
|
|
209
|
+
Fulfill a paused request with a custom response without hitting the server.
|
|
210
|
+
|
|
211
|
+
#### Examples
|
|
212
|
+
```python
|
|
213
|
+
fulfill(583) # Empty 200 response
|
|
214
|
+
fulfill(583, body='{"ok": true}') # JSON response
|
|
215
|
+
fulfill(583, body="Not Found", status=404) # Error response
|
|
216
|
+
fulfill(583, headers=[{"name": "Content-Type", "value": "application/json"}])
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### Tips
|
|
220
|
+
- **Mock APIs:** Return fake JSON during development without backend
|
|
221
|
+
- **Test errors:** `fulfill({id}, body="Error", status=500)` - test error handling
|
|
222
|
+
- **Set headers:** Use `headers=[{"name": "...", "value": "..."}]` for content-type etc.
|
|
223
|
+
- **View paused:** `network(req_state="paused")` - find requests to fulfill
|
|
224
|
+
|
|
225
|
+
### page
|
|
226
|
+
Get current page information and navigate.
|
|
227
|
+
|
|
228
|
+
#### Tips
|
|
229
|
+
- **Navigate:** `navigate("https://example.com")` - go to URL
|
|
230
|
+
- **Reload:** `reload()` or `reload(ignore_cache=True)` - refresh page
|
|
231
|
+
- **History:** `back()`, `forward()`, `history()` - navigate history
|
|
232
|
+
- **Execute JS:** `js("document.title")` - run JavaScript in page
|
|
233
|
+
- **Monitor traffic:** `network()` - see requests, `console()` - see messages
|
|
234
|
+
- **Switch page:** `pages()` then `connect(page=N)` - change to another tab
|
|
235
|
+
- **Full status:** `status()` - connection details and event count
|
|
236
|
+
|
|
237
|
+
### pages
|
|
238
|
+
List available Chrome pages and manage connections.
|
|
239
|
+
|
|
240
|
+
#### Tips
|
|
241
|
+
- **Connect to page:** `connect(page={index})` - connect by index number
|
|
242
|
+
- **Connect by ID:** `connect(page_id="{page_id}")` - stable across tab reordering
|
|
243
|
+
- **Switch pages:** Just call `connect()` again - no need to disconnect first
|
|
244
|
+
- **Check status:** `status()` - see current connection and event count
|
|
245
|
+
- **Reconnect:** If connection lost, select page and `connect()` again
|
|
246
|
+
- **Find page:** Look for title/URL in table - index stays consistent
|
|
247
|
+
|
|
248
|
+
### selections
|
|
249
|
+
Browser element selections with prompt and analysis.
|
|
250
|
+
|
|
251
|
+
Access selected DOM elements and their properties via Python expressions. Elements are selected using the Chrome extension's selection mode.
|
|
252
|
+
|
|
253
|
+
#### Examples
|
|
254
|
+
```python
|
|
255
|
+
selections() # View all selections
|
|
256
|
+
selections(expr="data['prompt']") # Get prompt text
|
|
257
|
+
selections(expr="data['selections']['1']") # Get element #1 data
|
|
258
|
+
selections(expr="data['selections']['1']['styles']") # Get styles
|
|
259
|
+
selections(expr="len(data['selections'])") # Count selections
|
|
260
|
+
selections(expr="{k: v['selector'] for k, v in data['selections'].items()}") # All selectors
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
#### Tips
|
|
264
|
+
- **Extract HTML:** `selections(expr="data['selections']['1']['outerHTML']")` - get element HTML
|
|
265
|
+
- **Get CSS selector:** `selections(expr="data['selections']['1']['selector']")` - unique selector
|
|
266
|
+
- **Use with js():** `js("element.offsetWidth", selection=1)` - integrate with JavaScript execution
|
|
267
|
+
- **Access styles:** `selections(expr="data['selections']['1']['styles']['display']")` - computed CSS
|
|
268
|
+
- **Get attributes:** `selections(expr="data['selections']['1']['preview']")` - tag, id, classes
|
|
269
|
+
- **Inspect in prompts:** Use `@webtap:webtap://selections` resource in Claude Code for AI analysis
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
Module Organization:
|
|
7
|
+
- _builders.py: Response builders using ReplKit2 markdown elements
|
|
8
|
+
- _tips.py: Parser for TIPS.md documentation
|
|
9
|
+
- _utils.py: Shared utilities for command modules
|
|
10
|
+
- _code_generation.py: Code generation utilities for HTTP bodies
|
|
11
|
+
- connection.py: Chrome browser connection management
|
|
12
|
+
- navigation.py: Browser navigation commands
|
|
13
|
+
- network.py: Network request monitoring
|
|
14
|
+
- console.py: Console message display
|
|
15
|
+
- filters.py: Filter management
|
|
16
|
+
- fetch.py: Request interception
|
|
17
|
+
- javascript.py: JavaScript execution
|
|
18
|
+
- request.py: Request data extraction with Python expressions
|
|
19
|
+
- to_model.py: Generate Pydantic models from responses
|
|
20
|
+
- quicktype.py: Generate type definitions via quicktype
|
|
21
|
+
- js_export.py: Export JS evaluation results to local files
|
|
22
|
+
- selections.py: DOM element selection and inspection
|
|
23
|
+
- setup.py: Component installation
|
|
24
|
+
- launch.py: Browser launch helpers
|
|
25
|
+
|
|
26
|
+
Note: Files prefixed with underscore are internal utilities not exposed as commands.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# No imports needed here - app.py imports commands directly
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Response builders using ReplKit2 v0.10.0+ markdown elements.
|
|
2
|
+
|
|
3
|
+
USAGE GUIDELINES:
|
|
4
|
+
|
|
5
|
+
Use builders for:
|
|
6
|
+
✅ Simple responses (error, info, success, warning)
|
|
7
|
+
✅ Tables with standard format
|
|
8
|
+
✅ Code execution results
|
|
9
|
+
✅ Repeated patterns across commands
|
|
10
|
+
|
|
11
|
+
Use manual building for:
|
|
12
|
+
❌ Complex multi-section layouts (>20 lines)
|
|
13
|
+
❌ Conditional sections with deep nesting
|
|
14
|
+
❌ Custom workflows (wizards, dashboards)
|
|
15
|
+
❌ Dual-mode resource views with tutorials
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
- network() - Simple table → Use table_response()
|
|
19
|
+
- javascript() - Code result → Use code_result_response()
|
|
20
|
+
- server() - Custom dashboard → Manual OK
|
|
21
|
+
- selections() resource mode - Tutorial layout → Manual OK
|
|
22
|
+
|
|
23
|
+
Available builders:
|
|
24
|
+
- table_response() - Tables with headers, warnings, tips
|
|
25
|
+
- info_response() - Key-value pairs with optional heading and tips
|
|
26
|
+
- error_response() - Errors with suggestions
|
|
27
|
+
- success_response() - Success messages with details
|
|
28
|
+
- warning_response() - Warnings with suggestions
|
|
29
|
+
- check_connection() - Helper for CDP connection validation
|
|
30
|
+
- check_fetch_enabled() - Helper for fetch interception validation
|
|
31
|
+
- code_result_response() - Code execution with result display
|
|
32
|
+
- code_response() - Simple code block display
|
|
33
|
+
|
|
34
|
+
Format helpers (for REPL display):
|
|
35
|
+
- format_size() - Convert bytes to human-readable (e.g., "1.5M")
|
|
36
|
+
- format_timestamp() - Convert epoch ms to time string (e.g., "12:34:56")
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from datetime import datetime
|
|
40
|
+
from typing import Any
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def format_size(size_bytes: int | None) -> str:
|
|
44
|
+
"""Format bytes as human-readable size string.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
size_bytes: Size in bytes, or None/0
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Human-readable string like "1.5M", "234K", "56B", or "-" for None/0
|
|
51
|
+
"""
|
|
52
|
+
if not size_bytes:
|
|
53
|
+
return "-"
|
|
54
|
+
|
|
55
|
+
if size_bytes >= 1_000_000:
|
|
56
|
+
return f"{size_bytes / 1_000_000:.1f}M"
|
|
57
|
+
elif size_bytes >= 1_000:
|
|
58
|
+
return f"{size_bytes / 1_000:.1f}K"
|
|
59
|
+
else:
|
|
60
|
+
return f"{size_bytes}B"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def format_timestamp(epoch_ms: float | None) -> str:
|
|
64
|
+
"""Format epoch milliseconds as time string.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
epoch_ms: Unix timestamp in milliseconds, or None/0
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Time string like "12:34:56", or "-" for None/0
|
|
71
|
+
"""
|
|
72
|
+
if not epoch_ms:
|
|
73
|
+
return "-"
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
dt = datetime.fromtimestamp(epoch_ms / 1000)
|
|
77
|
+
return dt.strftime("%H:%M:%S")
|
|
78
|
+
except (ValueError, OSError):
|
|
79
|
+
return "-"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def table_response(
|
|
83
|
+
title: str | None = None,
|
|
84
|
+
headers: list[str] | None = None,
|
|
85
|
+
rows: list[dict] | None = None,
|
|
86
|
+
summary: str | None = None,
|
|
87
|
+
warnings: list[str] | None = None,
|
|
88
|
+
tips: list[str] | None = None,
|
|
89
|
+
truncate: dict | None = None,
|
|
90
|
+
) -> dict:
|
|
91
|
+
"""Build table response with full data.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
title: Optional table title
|
|
95
|
+
headers: Column headers
|
|
96
|
+
rows: Data rows with FULL data
|
|
97
|
+
summary: Optional summary text
|
|
98
|
+
warnings: Optional warning messages
|
|
99
|
+
tips: Optional developer tips/guidance
|
|
100
|
+
truncate: Element-level truncation config (e.g., {"URL": {"max": 60, "mode": "middle"}})
|
|
101
|
+
"""
|
|
102
|
+
elements = []
|
|
103
|
+
|
|
104
|
+
if title:
|
|
105
|
+
elements.append({"type": "heading", "content": title, "level": 2})
|
|
106
|
+
|
|
107
|
+
if warnings:
|
|
108
|
+
for warning in warnings:
|
|
109
|
+
elements.append({"type": "alert", "message": warning, "level": "warning"})
|
|
110
|
+
|
|
111
|
+
if headers and rows:
|
|
112
|
+
table_element: dict[str, Any] = {"type": "table", "headers": headers, "rows": rows}
|
|
113
|
+
if truncate:
|
|
114
|
+
table_element["truncate"] = truncate
|
|
115
|
+
elements.append(table_element)
|
|
116
|
+
elif rows: # Headers can be inferred from row keys
|
|
117
|
+
table_element = {"type": "table", "rows": rows}
|
|
118
|
+
if truncate:
|
|
119
|
+
table_element["truncate"] = truncate
|
|
120
|
+
elements.append(table_element)
|
|
121
|
+
else:
|
|
122
|
+
elements.append({"type": "text", "content": "_No data available_"})
|
|
123
|
+
|
|
124
|
+
if summary:
|
|
125
|
+
elements.append({"type": "text", "content": f"_{summary}_"})
|
|
126
|
+
|
|
127
|
+
if tips:
|
|
128
|
+
elements.append({"type": "heading", "content": "Next Steps", "level": 3})
|
|
129
|
+
elements.append({"type": "list", "items": tips})
|
|
130
|
+
|
|
131
|
+
return {"elements": elements}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def info_response(
|
|
135
|
+
title: str | None = None,
|
|
136
|
+
fields: dict | None = None,
|
|
137
|
+
extra: str | None = None,
|
|
138
|
+
tips: list[str] | None = None,
|
|
139
|
+
) -> dict:
|
|
140
|
+
"""Build info display with key-value pairs.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
title: Optional info title
|
|
144
|
+
fields: Dict of field names to values
|
|
145
|
+
extra: Optional extra content (raw markdown)
|
|
146
|
+
tips: Optional developer tips/guidance
|
|
147
|
+
"""
|
|
148
|
+
elements = []
|
|
149
|
+
|
|
150
|
+
if title:
|
|
151
|
+
elements.append({"type": "heading", "content": title, "level": 2})
|
|
152
|
+
|
|
153
|
+
if fields:
|
|
154
|
+
for key, value in fields.items():
|
|
155
|
+
if value is not None:
|
|
156
|
+
elements.append({"type": "text", "content": f"**{key}:** {value}"})
|
|
157
|
+
|
|
158
|
+
if extra:
|
|
159
|
+
elements.append({"type": "raw", "content": extra})
|
|
160
|
+
|
|
161
|
+
if not elements:
|
|
162
|
+
elements.append({"type": "text", "content": "_No information available_"})
|
|
163
|
+
|
|
164
|
+
if tips:
|
|
165
|
+
elements.append({"type": "heading", "content": "Next Steps", "level": 3})
|
|
166
|
+
elements.append({"type": "list", "items": tips})
|
|
167
|
+
|
|
168
|
+
return {"elements": elements}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def error_response(message: str, suggestions: list[str] | None = None) -> dict:
|
|
172
|
+
"""Build error response with optional suggestions.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
message: Error message
|
|
176
|
+
suggestions: Optional list of suggestions
|
|
177
|
+
"""
|
|
178
|
+
elements: list[dict[str, Any]] = [{"type": "alert", "message": message, "level": "error"}]
|
|
179
|
+
|
|
180
|
+
if suggestions:
|
|
181
|
+
elements.append({"type": "text", "content": "**Try:**"})
|
|
182
|
+
elements.append({"type": "list", "items": suggestions})
|
|
183
|
+
|
|
184
|
+
return {"elements": elements}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def success_response(message: str, details: dict | None = None) -> dict:
|
|
188
|
+
"""Build success response with optional details.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
message: Success message
|
|
192
|
+
details: Optional dict of additional details
|
|
193
|
+
"""
|
|
194
|
+
elements = [{"type": "alert", "message": message, "level": "success"}]
|
|
195
|
+
|
|
196
|
+
if details:
|
|
197
|
+
for key, value in details.items():
|
|
198
|
+
if value is not None:
|
|
199
|
+
elements.append({"type": "text", "content": f"**{key}:** {value}"})
|
|
200
|
+
|
|
201
|
+
return {"elements": elements}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def warning_response(message: str, suggestions: list[str] | None = None) -> dict:
|
|
205
|
+
"""Build warning response with optional suggestions.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
message: Warning message
|
|
209
|
+
suggestions: Optional list of suggestions
|
|
210
|
+
"""
|
|
211
|
+
elements: list[dict[str, Any]] = [{"type": "alert", "message": message, "level": "warning"}]
|
|
212
|
+
|
|
213
|
+
if suggestions:
|
|
214
|
+
elements.append({"type": "text", "content": "**Try:**"})
|
|
215
|
+
elements.append({"type": "list", "items": suggestions})
|
|
216
|
+
|
|
217
|
+
return {"elements": elements}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# Connection helpers
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def check_connection(state):
|
|
224
|
+
"""Check CDP connection via daemon, return error response if not connected.
|
|
225
|
+
|
|
226
|
+
Returns error_response() if not connected, None otherwise.
|
|
227
|
+
Use pattern: `if error := check_connection(state): return error`
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
state: Application state containing RPC client.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Error response dict if not connected, None if connected.
|
|
234
|
+
"""
|
|
235
|
+
try:
|
|
236
|
+
status = state.client.call("status")
|
|
237
|
+
if not status.get("connected"):
|
|
238
|
+
return error_response(
|
|
239
|
+
"Not connected to Chrome",
|
|
240
|
+
suggestions=[
|
|
241
|
+
"Run `pages()` to see available tabs",
|
|
242
|
+
"Use `connect(0)` to connect to first tab",
|
|
243
|
+
"Or `connect(page_id='...')` for specific tab",
|
|
244
|
+
],
|
|
245
|
+
)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
return error_response(f"Failed to check connection: {e}")
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def check_fetch_enabled(state):
|
|
252
|
+
"""Check fetch interception via daemon, return error response if not enabled.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
state: Application state containing RPC client.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Error response dict if not enabled, None if enabled.
|
|
259
|
+
"""
|
|
260
|
+
try:
|
|
261
|
+
status = state.client.call("status")
|
|
262
|
+
if not status.get("fetch", {}).get("enabled"):
|
|
263
|
+
return error_response(
|
|
264
|
+
"Fetch interception not enabled", suggestions=["Enable with `fetch('enable')` to pause requests"]
|
|
265
|
+
)
|
|
266
|
+
except Exception as e:
|
|
267
|
+
return error_response(f"Failed to check fetch status: {e}")
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# Code result builders
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def code_result_response(
|
|
275
|
+
title: str,
|
|
276
|
+
code: str,
|
|
277
|
+
language: str,
|
|
278
|
+
result: Any = None,
|
|
279
|
+
) -> dict:
|
|
280
|
+
"""Build code execution result display.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
title: Result heading (e.g. "JavaScript Result")
|
|
284
|
+
code: Source code executed
|
|
285
|
+
language: Syntax highlighting language
|
|
286
|
+
result: Execution result (supports dict/list/str/None)
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Markdown response with code and result
|
|
290
|
+
"""
|
|
291
|
+
import json
|
|
292
|
+
|
|
293
|
+
elements = [
|
|
294
|
+
{"type": "heading", "content": title, "level": 2},
|
|
295
|
+
{"type": "code_block", "content": code, "language": language},
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
if result is not None:
|
|
299
|
+
if isinstance(result, (dict, list)):
|
|
300
|
+
elements.append({"type": "code_block", "content": json.dumps(result, indent=2), "language": "json"})
|
|
301
|
+
else:
|
|
302
|
+
elements.append({"type": "text", "content": f"**Result:** `{result}`"})
|
|
303
|
+
else:
|
|
304
|
+
elements.append({"type": "text", "content": "**Result:** _(no return value)_"})
|
|
305
|
+
|
|
306
|
+
return {"elements": elements}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def code_response(
|
|
310
|
+
content: str,
|
|
311
|
+
language: str = "",
|
|
312
|
+
title: str | None = None,
|
|
313
|
+
) -> dict:
|
|
314
|
+
"""Build simple code block response.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
content: Code content to display
|
|
318
|
+
language: Syntax highlighting language
|
|
319
|
+
title: Optional heading above code block
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Markdown response with code block
|
|
323
|
+
"""
|
|
324
|
+
elements = []
|
|
325
|
+
|
|
326
|
+
if title:
|
|
327
|
+
elements.append({"type": "heading", "content": title, "level": 2})
|
|
328
|
+
|
|
329
|
+
elements.append({"type": "code_block", "content": content, "language": language})
|
|
330
|
+
|
|
331
|
+
return {"elements": elements}
|