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 ADDED
@@ -0,0 +1,234 @@
1
+ # WebTap Vision: Work WITH Chrome DevTools Protocol
2
+
3
+ ## Core Philosophy
4
+
5
+ **Store CDP events as-is. Transform minimally. Query on-demand.**
6
+
7
+ Instead of transforming CDP's complex nested structures into our own models, we embrace CDP's native format. We store events mostly unchanged, extract minimal data for tables, and query additional data on-demand.
8
+
9
+ ## The Problem We're Solving
10
+
11
+ CDP sends rich, nested event data. Previous approaches tried to:
12
+ 1. Transform everything into flat models
13
+ 2. Create abstraction layers over CDP
14
+ 3. Build complex query engines
15
+ 4. Format data for display
16
+
17
+ This led to:
18
+ - Loss of CDP's rich information
19
+ - Complex transformation logic
20
+ - Over-engineered abstractions
21
+ - Unnecessary memory usage
22
+
23
+ ## The Solution: Native CDP Storage
24
+
25
+ ### 1. Store Events As-Is
26
+
27
+ ```python
28
+ # CDP gives us this - we keep it!
29
+ {
30
+ "method": "Network.responseReceived",
31
+ "params": {
32
+ "requestId": "123.456",
33
+ "response": {
34
+ "status": 200,
35
+ "headers": {...},
36
+ "mimeType": "application/json",
37
+ "timing": {...}
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ ### 2. Minimal Summaries for Tables
44
+
45
+ Extract only what's needed for table display:
46
+ ```python
47
+ NetworkSummary(
48
+ id="123.456",
49
+ method="GET",
50
+ status=200,
51
+ url="https://api.example.com/data",
52
+ type="json",
53
+ size=1234
54
+ )
55
+ ```
56
+
57
+ Keep the full CDP events attached for detail views.
58
+
59
+ ### 3. On-Demand Queries
60
+
61
+ Some data isn't in the event stream:
62
+ - Response bodies: `Network.getResponseBody`
63
+ - Cookies: `Storage.getCookies`
64
+ - LocalStorage: `DOMStorage.getDOMStorageItems`
65
+ - JavaScript evaluation: `Runtime.evaluate`
66
+
67
+ Query these when needed, not preemptively.
68
+
69
+ ## Architecture
70
+
71
+ ```
72
+ ┌─────────────────┐
73
+ │ Chrome Tab │
74
+ └────────┬────────┘
75
+ │ CDP Events
76
+ ┌────────▼────────┐
77
+ │ WebSocket │
78
+ │ (WebSocketApp) │
79
+ └────────┬────────┘
80
+ │ Raw Events
81
+ ┌────────▼────────┐
82
+ │ DuckDB Storage │
83
+ │ (events table) │
84
+ └────────┬────────┘
85
+ │ SQL Queries
86
+ ┌────────────┼────────────┐
87
+ │ │ │
88
+ ┌───────▼──────┐ ┌───▼───┐ ┌──────▼──────┐
89
+ │ Commands │ │ Tables│ │Detail Views │
90
+ │network() │ │ │ │ │
91
+ │console() │ │Minimal│ │Full CDP Data│
92
+ │storage() │ │Summary│ │+ On-Demand │
93
+ └──────────────┘ └───────┘ └─────────────┘
94
+ ```
95
+
96
+ ## Data Flow Examples
97
+
98
+ ### Network Request Lifecycle
99
+
100
+ ```python
101
+ # 1. Request sent - store as-is in DuckDB
102
+ db.execute("INSERT INTO events VALUES (?)", [json.dumps({
103
+ "method": "Network.requestWillBeSent",
104
+ "params": {...} # Full CDP data
105
+ })])
106
+
107
+ # 2. Response received - store as-is
108
+ db.execute("INSERT INTO events VALUES (?)", [json.dumps({
109
+ "method": "Network.responseReceived",
110
+ "params": {...} # Full CDP data
111
+ })])
112
+
113
+ # 3. Query for table view - SQL on JSON
114
+ db.execute("""
115
+ SELECT
116
+ json_extract_string(event, '$.params.requestId') as id,
117
+ json_extract_string(event, '$.params.response.status') as status,
118
+ json_extract_string(event, '$.params.response.url') as url
119
+ FROM events
120
+ WHERE json_extract_string(event, '$.method') = 'Network.responseReceived'
121
+ """)
122
+
123
+ # 4. Detail view - get all events for request
124
+ db.execute("""
125
+ SELECT event FROM events
126
+ WHERE json_extract_string(event, '$.params.requestId') = ?
127
+ """, [request_id])
128
+
129
+ # 5. Body fetch - on-demand CDP call
130
+ cdp.execute("Network.getResponseBody", {"requestId": "123.456"})
131
+ ```
132
+
133
+ ### Console Message
134
+
135
+ ```python
136
+ # 1. Store as-is
137
+ console_events.append({
138
+ "method": "Runtime.consoleAPICalled",
139
+ "params": {
140
+ "type": "error",
141
+ "args": [{"type": "string", "value": "Failed to fetch"}],
142
+ "stackTrace": {...}
143
+ }
144
+ })
145
+
146
+ # 2. Table view - minimal summary
147
+ ConsoleSummary(
148
+ id="console-123",
149
+ level="error",
150
+ message="Failed to fetch",
151
+ source="console"
152
+ )
153
+
154
+ # 3. Detail view - full CDP data
155
+ {
156
+ "summary": summary,
157
+ "raw": console_events[i], # Full CDP event with stack trace
158
+ }
159
+ ```
160
+
161
+ ## Benefits
162
+
163
+ 1. **No Information Loss** - Full CDP data always available
164
+ 2. **Minimal Memory** - Only store what CDP sends
165
+ 3. **Simple Code** - No complex transformations
166
+ 4. **Fast Tables** - Minimal summaries render quickly
167
+ 5. **Rich Details** - Full CDP data for debugging
168
+ 6. **On-Demand Loading** - Expensive operations only when needed
169
+ 7. **Future Proof** - New CDP features automatically available
170
+
171
+ ## Implementation Principles
172
+
173
+ ### DO:
174
+ - Store CDP events as-is
175
+ - Build minimal summaries for tables
176
+ - Query additional data on-demand
177
+ - Group events by correlation ID (requestId)
178
+ - Let Replkit2 handle display
179
+
180
+ ### DON'T:
181
+ - Transform CDP structure unnecessarily
182
+ - Fetch data preemptively
183
+ - Create abstraction layers
184
+ - Build complex query engines
185
+ - Format data for display
186
+
187
+ ## File Structure
188
+
189
+ ```
190
+ webtap/
191
+ ├── VISION.md # This file
192
+ ├── __init__.py # Module initialization + API server startup
193
+ ├── app.py # REPL app with WebTapState
194
+ ├── api.py # FastAPI server for Chrome extension
195
+ ├── filters.py # Filter management system
196
+ ├── cdp/
197
+ │ ├── __init__.py
198
+ │ ├── session.py # CDPSession with DuckDB storage
199
+ │ ├── query.py # Dynamic query builder
200
+ │ └── schema/ # CDP protocol reference
201
+ │ ├── README.md
202
+ │ └── cdp_version.json
203
+ ├── services/ # Service layer (business logic)
204
+ │ ├── __init__.py
205
+ │ ├── main.py # WebTapService orchestrator
206
+ │ ├── network.py # Network request handling
207
+ │ ├── console.py # Console message handling
208
+ │ ├── fetch.py # Request interception
209
+ │ └── body.py # Response body caching
210
+ └── commands/ # Thin command wrappers
211
+ ├── __init__.py
212
+ ├── _errors.py # Unified error handling
213
+ ├── _markdown.py # Markdown elements
214
+ ├── _symbols.py # ASCII symbol registry
215
+ ├── _utils.py # Shared utilities
216
+ ├── connection.py # connect, disconnect, pages
217
+ ├── navigation.py # navigate, reload, back, forward
218
+ ├── network.py # network() command
219
+ ├── console.py # console() command
220
+ ├── events.py # events() dynamic querying
221
+ ├── inspect.py # inspect() event details
222
+ ├── javascript.py # js() execution
223
+ ├── body.py # body() response inspection
224
+ ├── fetch.py # fetch(), requests(), resume()
225
+ └── filters.py # filters() management
226
+ ```
227
+
228
+ ## Success Metrics
229
+
230
+ - **Lines of Code**: < 500 (excluding commands)
231
+ - **Transformation Logic**: < 100 lines
232
+ - **Memory Usage**: Only what CDP sends
233
+ - **Response Time**: Instant for tables, < 100ms for details
234
+ - **CDP Coverage**: 100% of CDP data accessible
webtap/__init__.py ADDED
@@ -0,0 +1,56 @@
1
+ """WebTap - Chrome DevTools Protocol REPL.
2
+
3
+ Main entry point for WebTap browser debugging tool. Provides both REPL and MCP
4
+ functionality for Chrome DevTools Protocol interaction with native CDP event
5
+ storage and on-demand querying.
6
+
7
+ PUBLIC API:
8
+ - app: Main ReplKit2 App instance
9
+ - main: Entry point function for CLI
10
+ """
11
+
12
+ import sys
13
+ import logging
14
+
15
+ from webtap.app import app
16
+ from webtap.api import start_api_server
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def main():
22
+ """Entry point for the WebTap REPL.
23
+
24
+ Starts in one of three modes:
25
+ - CLI mode (with --cli flag) for command-line interface
26
+ - MCP mode (with --mcp flag) for Model Context Protocol server
27
+ - REPL mode (default) for interactive shell
28
+
29
+ In REPL and MCP modes, the API server is started for Chrome extension
30
+ integration. The API server runs in background to handle extension requests.
31
+ """
32
+ # Start API server for Chrome extension (except in CLI mode)
33
+ if "--cli" not in sys.argv:
34
+ _start_api_server_safely()
35
+
36
+ if "--mcp" in sys.argv:
37
+ app.mcp.run()
38
+ elif "--cli" in sys.argv:
39
+ # Remove --cli from argv before passing to Typer
40
+ sys.argv.remove("--cli")
41
+ app.cli() # Run CLI mode via Typer
42
+ else:
43
+ # Run REPL
44
+ app.run(title="WebTap - Chrome DevTools Protocol REPL")
45
+
46
+
47
+ def _start_api_server_safely():
48
+ """Start API server with error handling."""
49
+ try:
50
+ start_api_server(app.state)
51
+ logger.info("API server started on http://localhost:8765")
52
+ except Exception as e:
53
+ logger.warning(f"Failed to start API server: {e}")
54
+
55
+
56
+ __all__ = ["app", "main"]
webtap/api.py ADDED
@@ -0,0 +1,222 @@
1
+ """FastAPI endpoints for WebTap browser extension.
2
+
3
+ PUBLIC API:
4
+ - start_api_server: Start API server in background thread
5
+ """
6
+
7
+ import logging
8
+ import threading
9
+ from typing import Any, Dict
10
+
11
+ from fastapi import FastAPI
12
+ from fastapi.middleware.cors import CORSMiddleware
13
+ from pydantic import BaseModel
14
+ import uvicorn
15
+
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ # Request models
21
+ class ConnectRequest(BaseModel):
22
+ """Request model for connecting to a Chrome page."""
23
+
24
+ page_id: str
25
+
26
+
27
+ class FetchRequest(BaseModel):
28
+ """Request model for enabling/disabling fetch interception."""
29
+
30
+ enabled: bool
31
+ response_stage: bool = False # Optional: also pause at Response stage
32
+
33
+
34
+ # Create FastAPI app
35
+ api = FastAPI(title="WebTap API", version="0.1.0")
36
+
37
+ # Enable CORS for extension
38
+ api.add_middleware(
39
+ CORSMiddleware,
40
+ allow_origins=["*"], # Chrome extensions have unique origins
41
+ allow_credentials=True,
42
+ allow_methods=["*"],
43
+ allow_headers=["*"],
44
+ )
45
+
46
+
47
+ # Global reference to WebTap state (set by start_api_server)
48
+ app_state = None
49
+
50
+
51
+ @api.get("/pages")
52
+ async def list_pages() -> Dict[str, Any]:
53
+ """List available Chrome pages for extension selection."""
54
+ if not app_state:
55
+ return {"error": "WebTap not initialized", "pages": []}
56
+
57
+ return app_state.service.list_pages()
58
+
59
+
60
+ @api.get("/status")
61
+ async def get_status() -> Dict[str, Any]:
62
+ """Get current connection status and event count."""
63
+ if not app_state:
64
+ return {"connected": False, "error": "WebTap not initialized", "events": 0}
65
+
66
+ return app_state.service.get_status()
67
+
68
+
69
+ @api.post("/connect")
70
+ async def connect(request: ConnectRequest) -> Dict[str, Any]:
71
+ """Connect to a Chrome page by stable page ID."""
72
+ if not app_state:
73
+ return {"error": "WebTap not initialized"}
74
+
75
+ return app_state.service.connect_to_page(page_id=request.page_id)
76
+
77
+
78
+ @api.post("/disconnect")
79
+ async def disconnect() -> Dict[str, Any]:
80
+ """Disconnect from currently connected page."""
81
+ if not app_state:
82
+ return {"error": "WebTap not initialized"}
83
+
84
+ return app_state.service.disconnect()
85
+
86
+
87
+ @api.post("/clear")
88
+ async def clear_events() -> Dict[str, Any]:
89
+ """Clear all stored events from DuckDB."""
90
+ if not app_state:
91
+ return {"error": "WebTap not initialized"}
92
+
93
+ return app_state.service.clear_events()
94
+
95
+
96
+ @api.post("/fetch")
97
+ async def set_fetch_interception(request: FetchRequest) -> Dict[str, Any]:
98
+ """Enable or disable fetch request interception."""
99
+ if not app_state:
100
+ return {"error": "WebTap not initialized"}
101
+
102
+ if request.enabled:
103
+ result = app_state.service.fetch.enable(app_state.service.cdp, response_stage=request.response_stage)
104
+ else:
105
+ result = app_state.service.fetch.disable()
106
+ return result
107
+
108
+
109
+ @api.get("/fetch/paused")
110
+ async def get_paused_requests() -> Dict[str, Any]:
111
+ """Get list of currently paused fetch requests."""
112
+ if not app_state:
113
+ return {"error": "WebTap not initialized", "requests": []}
114
+
115
+ fetch_service = app_state.service.fetch
116
+ if not fetch_service.enabled:
117
+ return {"enabled": False, "requests": []}
118
+
119
+ paused_list = fetch_service.get_paused_list()
120
+ return {
121
+ "enabled": True,
122
+ "requests": paused_list,
123
+ "count": len(paused_list),
124
+ "response_stage": fetch_service.enable_response_stage,
125
+ }
126
+
127
+
128
+ @api.get("/filters/status")
129
+ async def get_filter_status() -> Dict[str, Any]:
130
+ """Get current filter configuration and enabled categories."""
131
+ if not app_state:
132
+ return {"error": "WebTap not initialized", "filters": {}, "enabled": []}
133
+
134
+ fm = app_state.service.filters
135
+ return {"filters": fm.filters, "enabled": list(fm.enabled_categories), "path": str(fm.filter_path)}
136
+
137
+
138
+ @api.post("/filters/toggle/{category}")
139
+ async def toggle_filter_category(category: str) -> Dict[str, Any]:
140
+ """Toggle a specific filter category on or off."""
141
+ if not app_state:
142
+ return {"error": "WebTap not initialized"}
143
+
144
+ fm = app_state.service.filters
145
+
146
+ if category not in fm.filters:
147
+ return {"error": f"Category '{category}' not found"}
148
+
149
+ if category in fm.enabled_categories:
150
+ fm.enabled_categories.discard(category)
151
+ enabled = False
152
+ else:
153
+ fm.enabled_categories.add(category)
154
+ enabled = True
155
+
156
+ fm.save()
157
+
158
+ return {"category": category, "enabled": enabled, "total_enabled": len(fm.enabled_categories)}
159
+
160
+
161
+ @api.post("/filters/enable-all")
162
+ async def enable_all_filters() -> Dict[str, Any]:
163
+ """Enable all available filter categories."""
164
+ if not app_state:
165
+ return {"error": "WebTap not initialized"}
166
+
167
+ fm = app_state.service.filters
168
+ fm.set_enabled_categories(None)
169
+ fm.save()
170
+
171
+ return {"enabled": list(fm.enabled_categories), "total": len(fm.enabled_categories)}
172
+
173
+
174
+ @api.post("/filters/disable-all")
175
+ async def disable_all_filters() -> Dict[str, Any]:
176
+ """Disable all filter categories."""
177
+ if not app_state:
178
+ return {"error": "WebTap not initialized"}
179
+
180
+ fm = app_state.service.filters
181
+ fm.set_enabled_categories([])
182
+ fm.save()
183
+
184
+ return {"enabled": [], "total": 0}
185
+
186
+
187
+ def start_api_server(state, host: str = "127.0.0.1", port: int = 8765):
188
+ """Start the API server in a background thread.
189
+
190
+ Args:
191
+ state: WebTapState instance from the main app.
192
+ host: Host to bind to. Defaults to 127.0.0.1.
193
+ port: Port to bind to. Defaults to 8765.
194
+
195
+ Returns:
196
+ Thread instance running the server.
197
+ """
198
+ global app_state
199
+ app_state = state
200
+
201
+ thread = threading.Thread(target=run_server, args=(host, port), daemon=True)
202
+ thread.start()
203
+
204
+ logger.info(f"API server started on http://{host}:{port}")
205
+ return thread
206
+
207
+
208
+ def run_server(host: str, port: int):
209
+ """Run the FastAPI server in a thread."""
210
+ try:
211
+ uvicorn.run(
212
+ api,
213
+ host=host,
214
+ port=port,
215
+ log_level="error",
216
+ access_log=False,
217
+ )
218
+ except Exception as e:
219
+ logger.error(f"API server failed: {e}")
220
+
221
+
222
+ __all__ = ["start_api_server"]
webtap/app.py ADDED
@@ -0,0 +1,76 @@
1
+ """Main application entry point for WebTap browser debugger.
2
+
3
+ PUBLIC API:
4
+ - WebTapState: Application state class with CDP session and service
5
+ - app: Main ReplKit2 App instance (imported by commands and __init__)
6
+ """
7
+
8
+ import sys
9
+ from dataclasses import dataclass, field
10
+
11
+ from replkit2 import App
12
+
13
+ from webtap.cdp import CDPSession
14
+ from webtap.services import WebTapService
15
+
16
+
17
+ @dataclass
18
+ class WebTapState:
19
+ """Application state for WebTap browser debugging.
20
+
21
+ Maintains CDP session and connection state for browser interaction.
22
+ All data is stored in DuckDB via the CDP session - no caching needed.
23
+
24
+ Attributes:
25
+ cdp: Chrome DevTools Protocol session instance.
26
+ service: WebTapService orchestrating all domain services.
27
+ """
28
+
29
+ cdp: CDPSession = field(default_factory=CDPSession)
30
+ service: WebTapService = field(init=False)
31
+
32
+ def __post_init__(self):
33
+ """Initialize service with self reference after dataclass init."""
34
+ self.service = WebTapService(self)
35
+
36
+
37
+ # Must be created before command imports for decorator registration
38
+ app = App(
39
+ "webtap",
40
+ WebTapState,
41
+ uri_scheme="webtap",
42
+ fastmcp={
43
+ "description": "Chrome DevTools Protocol debugger",
44
+ "tags": {"browser", "debugging", "chrome", "cdp"},
45
+ },
46
+ typer_config={
47
+ "add_completion": False, # Hide shell completion options
48
+ "help": "WebTap - Chrome DevTools Protocol CLI",
49
+ },
50
+ )
51
+
52
+ # Command imports trigger @app.command decorator registration
53
+ if "--cli" in sys.argv:
54
+ # Only import CLI-compatible commands (no dict/list parameters)
55
+ from webtap.commands import setup # noqa: E402, F401
56
+ from webtap.commands import launch # noqa: E402, F401
57
+ else:
58
+ # Import all commands for REPL/MCP mode
59
+ from webtap.commands import connection # noqa: E402, F401
60
+ from webtap.commands import navigation # noqa: E402, F401
61
+ from webtap.commands import javascript # noqa: E402, F401
62
+ from webtap.commands import network # noqa: E402, F401
63
+ from webtap.commands import console # noqa: E402, F401
64
+ from webtap.commands import events # noqa: E402, F401
65
+ from webtap.commands import filters # noqa: E402, F401
66
+ from webtap.commands import inspect # noqa: E402, F401
67
+ from webtap.commands import fetch # noqa: E402, F401
68
+ from webtap.commands import body # noqa: E402, F401
69
+ from webtap.commands import setup # noqa: E402, F401
70
+ from webtap.commands import launch # noqa: E402, F401
71
+
72
+
73
+ # Entry point is in __init__.py:main() as specified in pyproject.toml
74
+
75
+
76
+ __all__ = ["WebTapState", "app"]