webtap-tool 0.11.0__py3-none-any.whl

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