webtap-tool 0.1.2__tar.gz → 0.1.4__tar.gz

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.

Files changed (50) hide show
  1. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/CHANGELOG.md +46 -0
  2. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/PKG-INFO +1 -1
  3. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/extension/popup.html +9 -0
  4. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/extension/popup.js +50 -11
  5. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/pyproject.toml +1 -1
  6. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/__init__.py +11 -3
  7. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/api.py +106 -31
  8. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/app.py +18 -0
  9. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/cdp/query.py +2 -1
  10. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/launch.py +3 -17
  11. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/services/body.py +1 -1
  12. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/services/console.py +1 -1
  13. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/services/fetch.py +1 -1
  14. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/services/network.py +2 -2
  15. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/services/setup.py +20 -10
  16. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/.gitignore +0 -0
  17. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/ARCHITECTURE.md +0 -0
  18. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/README.md +0 -0
  19. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/data/filters.json +0 -0
  20. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/extension/manifest.json +0 -0
  21. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/llms.txt +0 -0
  22. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/VISION.md +0 -0
  23. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/cdp/README.md +0 -0
  24. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/cdp/__init__.py +0 -0
  25. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/cdp/schema/README.md +0 -0
  26. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/cdp/schema/cdp_protocol.json +0 -0
  27. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/cdp/schema/cdp_version.json +0 -0
  28. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/cdp/session.py +0 -0
  29. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/DEVELOPER_GUIDE.md +0 -0
  30. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/TIPS.md +0 -0
  31. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/__init__.py +0 -0
  32. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/_builders.py +0 -0
  33. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/_errors.py +0 -0
  34. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/_tips.py +0 -0
  35. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/_utils.py +0 -0
  36. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/body.py +0 -0
  37. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/connection.py +0 -0
  38. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/console.py +0 -0
  39. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/events.py +0 -0
  40. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/fetch.py +0 -0
  41. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/filters.py +0 -0
  42. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/inspect.py +0 -0
  43. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/javascript.py +0 -0
  44. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/navigation.py +0 -0
  45. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/network.py +0 -0
  46. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/commands/setup.py +0 -0
  47. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/filters.py +0 -0
  48. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/services/README.md +0 -0
  49. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/services/__init__.py +0 -0
  50. {webtap_tool-0.1.2 → webtap_tool-0.1.4}/src/webtap/services/main.py +0 -0
@@ -15,6 +15,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
15
 
16
16
  ### Removed
17
17
 
18
+ ## [0.1.4] - 2025-09-08
19
+
20
+ ### Added
21
+ - Request timeout handling (3 seconds) in Chrome extension for better error detection
22
+ - Comprehensive `cleanup()` method for proper resource management on exit
23
+ - `atexit` registration to ensure cleanup happens even on unexpected termination
24
+
25
+ ### Changed
26
+ - **API Consolidation**: Merged `/pages` and `/instance` endpoints into single `/info` endpoint
27
+ - Enhanced `/status` endpoint to include fetch details inline (eliminates separate API call)
28
+ - Improved error messages in extension to distinguish between timeout, server not running, and other failures
29
+ - API server shutdown now uses `asyncio.run()` for proper task cleanup
30
+
31
+ ### Fixed
32
+ - Graceful shutdown issue that left zombie tasks with "Task was destroyed but it is pending" error
33
+ - API server cleanup on application exit now properly cancels all async tasks
34
+ - Extension error handling now provides specific feedback for different failure modes
35
+
36
+ ### Removed
37
+ - `/instance` endpoint (functionality merged into `/info`)
38
+ - `/fetch/paused` endpoint (data now included in `/status` response)
39
+
40
+ ## [0.1.3] - 2025-09-05
41
+
42
+ ### Added
43
+ - Multi-instance WebTap support with first-come-first-serve port management
44
+ - `/instance` endpoint to show WebTap instance information (PID, connected page, events)
45
+ - `/release` endpoint for graceful API port handoff between instances
46
+ - Chrome extension "Switch WebTap Instance" button for managing multiple instances
47
+ - Instance status display in extension popup showing PID and event count
48
+ - Automatic reconnection in extension after instance switching
49
+
50
+ ### Changed
51
+ - API server now checks port availability before starting (port 8765)
52
+ - Chrome profile launch strategy uses bindfs mounting instead of symlinks
53
+ - Service docstrings simplified (removed redundant "Internal service for" prefixes)
54
+ - Added `api_thread` tracking to WebTapState for proper thread lifecycle management
55
+
56
+ ### Fixed
57
+ - SQL numeric comparisons in query builder now use string comparison instead of CAST
58
+ - Type annotations improved with proper union types (`threading.Thread | None`)
59
+ - Network service HTTP status filtering uses string comparison to prevent SQL errors
60
+ - Extension connection handling during instance transitions
61
+
62
+ ### Removed
63
+
18
64
  ## [0.1.2] - 2025-09-05
19
65
 
20
66
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webtap-tool
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Terminal-based web page inspector for AI debugging sessions
5
5
  Author-email: Fredrik Angelsen <fredrikangelsen@gmail.com>
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -150,6 +150,15 @@
150
150
 
151
151
  <div class="divider"></div>
152
152
 
153
+ <button
154
+ id="switchInstance"
155
+ style="width: 100%; font-size: 11px; margin-bottom: 8px"
156
+ >
157
+ Switch WebTap Instance
158
+ </button>
159
+
160
+ <div class="divider"></div>
161
+
153
162
  <div class="filter-section">
154
163
  <div
155
164
  style="
@@ -1,14 +1,28 @@
1
1
  // API helper - communicate with WebTap service
2
2
  async function api(endpoint, method = "GET", body = null) {
3
3
  try {
4
- const opts = { method };
4
+ const opts = {
5
+ method,
6
+ // Add timeout to detect unresponsive server faster
7
+ signal: AbortSignal.timeout(3000)
8
+ };
5
9
  if (body) {
6
10
  opts.headers = { "Content-Type": "application/json" };
7
11
  opts.body = JSON.stringify(body);
8
12
  }
9
13
  const resp = await fetch(`http://localhost:8765${endpoint}`, opts);
14
+ if (!resp.ok) {
15
+ return { error: `HTTP ${resp.status}: ${resp.statusText}` };
16
+ }
10
17
  return await resp.json();
11
18
  } catch (e) {
19
+ // Better error messages
20
+ if (e.name === 'AbortError') {
21
+ return { error: "WebTap not responding (timeout)" };
22
+ }
23
+ if (e.message.includes('Failed to fetch')) {
24
+ return { error: "WebTap not running" };
25
+ }
12
26
  return { error: e.message };
13
27
  }
14
28
  }
@@ -17,15 +31,20 @@ async function api(endpoint, method = "GET", body = null) {
17
31
 
18
32
  // Load available pages from WebTap
19
33
  async function loadPages() {
20
- const result = await api("/pages");
21
-
22
- if (result.error) {
34
+ // Use combined /info endpoint for single round trip
35
+ const info = await api("/info");
36
+
37
+ if (info.error) {
23
38
  document.getElementById("pageList").innerHTML =
24
- "<option disabled>WebTap not running</option>";
39
+ `<option disabled>${info.error === "WebTap not initialized" ? "WebTap not running" : "Error loading pages"}</option>`;
25
40
  return;
26
41
  }
27
42
 
28
- const pages = result.pages || [];
43
+ // Update instance info tooltip
44
+ document.getElementById("switchInstance").title =
45
+ `PID: ${info.pid} | Events: ${info.events}`;
46
+
47
+ const pages = info.pages || [];
29
48
  const select = document.getElementById("pageList");
30
49
 
31
50
  select.innerHTML = "";
@@ -247,6 +266,27 @@ document.getElementById("disableAllFilters").onclick = async () => {
247
266
  setTimeout(updateFilters, 100);
248
267
  };
249
268
 
269
+ // Switch to a different WebTap instance
270
+ document.getElementById("switchInstance").onclick = async () => {
271
+ const result = await api("/release", "POST");
272
+ if (!result.error) {
273
+ document.getElementById("status").innerHTML =
274
+ '<span style="color: #666">Port released. Start new WebTap.</span>';
275
+ // Disable controls until reconnected
276
+ document.getElementById("connect").disabled = true;
277
+ document.getElementById("disconnect").disabled = true;
278
+ document.getElementById("fetchToggle").disabled = true;
279
+ // Try to reconnect after delay
280
+ setTimeout(() => {
281
+ loadPages();
282
+ updateStatus();
283
+ }, 2000);
284
+ } else {
285
+ document.getElementById("status").innerHTML =
286
+ `<span class="error">Error: ${result.error}</span>`;
287
+ }
288
+ };
289
+
250
290
  // Update all status from server - single source of truth
251
291
  async function updateStatus() {
252
292
  const status = await api("/status");
@@ -268,13 +308,12 @@ async function updateStatus() {
268
308
  document.getElementById("status").innerHTML =
269
309
  `<span class="connected">Connected</span> - Events: ${status.events}`;
270
310
 
271
- // Get fetch details if enabled
272
- if (status.fetch_enabled) {
273
- const fetchDetails = await api("/fetch/paused");
311
+ // Use fetch details from enhanced status (no extra API call needed)
312
+ if (status.fetch_enabled && status.fetch_details) {
274
313
  updateFetchStatus(
275
314
  true,
276
- status.paused_requests || 0,
277
- fetchDetails.response_stage || false,
315
+ status.fetch_details.paused_count || 0,
316
+ status.fetch_details.response_stage || false,
278
317
  );
279
318
  } else {
280
319
  updateFetchStatus(false);
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "webtap-tool"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  description = "Terminal-based web page inspector for AI debugging sessions"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -9,6 +9,7 @@ PUBLIC API:
9
9
  - main: Entry point function for CLI
10
10
  """
11
11
 
12
+ import atexit
12
13
  import sys
13
14
  import logging
14
15
 
@@ -45,10 +46,17 @@ def main():
45
46
 
46
47
 
47
48
  def _start_api_server_safely():
48
- """Start API server with error handling."""
49
+ """Start API server with error handling and cleanup registration."""
49
50
  try:
50
- start_api_server(app.state)
51
- logger.info("API server started on http://localhost:8765")
51
+ thread = start_api_server(app.state)
52
+ if thread and app.state:
53
+ app.state.api_thread = thread
54
+ logger.info("API server started on port 8765")
55
+
56
+ # Register cleanup to shut down API server on exit
57
+ atexit.register(lambda: app.state.cleanup() if app.state else None)
58
+ else:
59
+ logger.info("Port 8765 in use by another instance")
52
60
  except Exception as e:
53
61
  logger.warning(f"Failed to start API server: {e}")
54
62
 
@@ -5,6 +5,8 @@ PUBLIC API:
5
5
  """
6
6
 
7
7
  import logging
8
+ import os
9
+ import socket
8
10
  import threading
9
11
  from typing import Any, Dict
10
12
 
@@ -48,22 +50,54 @@ api.add_middleware(
48
50
  app_state = None
49
51
 
50
52
 
51
- @api.get("/pages")
52
- async def list_pages() -> Dict[str, Any]:
53
- """List available Chrome pages for extension selection."""
53
+ @api.get("/health")
54
+ async def health_check() -> Dict[str, Any]:
55
+ """Quick health check endpoint for extension."""
56
+ return {"status": "ok", "pid": os.getpid()}
57
+
58
+
59
+ @api.get("/info")
60
+ async def get_info() -> Dict[str, Any]:
61
+ """Combined endpoint for pages and instance info - reduces round trips."""
54
62
  if not app_state:
55
- return {"error": "WebTap not initialized", "pages": []}
63
+ return {"error": "WebTap not initialized", "pages": [], "pid": os.getpid()}
56
64
 
57
- return app_state.service.list_pages()
65
+ # Get pages
66
+ pages_data = app_state.service.list_pages()
67
+
68
+ # Get instance info
69
+ connected_to = None
70
+ if app_state.cdp.is_connected and app_state.cdp.page_info:
71
+ connected_to = app_state.cdp.page_info.get("title", "Untitled")
72
+
73
+ return {
74
+ "pid": os.getpid(),
75
+ "connected_to": connected_to,
76
+ "events": app_state.service.event_count,
77
+ "pages": pages_data.get("pages", []),
78
+ "error": pages_data.get("error"),
79
+ }
58
80
 
59
81
 
60
82
  @api.get("/status")
61
83
  async def get_status() -> Dict[str, Any]:
62
- """Get current connection status and event count."""
84
+ """Get comprehensive status including connection, events, and fetch details."""
63
85
  if not app_state:
64
86
  return {"connected": False, "error": "WebTap not initialized", "events": 0}
65
87
 
66
- return app_state.service.get_status()
88
+ status = app_state.service.get_status()
89
+
90
+ # Add fetch details if fetch is enabled
91
+ if status.get("fetch_enabled"):
92
+ fetch_service = app_state.service.fetch
93
+ paused_list = fetch_service.get_paused_list()
94
+ status["fetch_details"] = {
95
+ "paused_requests": paused_list,
96
+ "paused_count": len(paused_list),
97
+ "response_stage": fetch_service.enable_response_stage,
98
+ }
99
+
100
+ return status
67
101
 
68
102
 
69
103
  @api.post("/connect")
@@ -106,25 +140,6 @@ async def set_fetch_interception(request: FetchRequest) -> Dict[str, Any]:
106
140
  return result
107
141
 
108
142
 
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
143
  @api.get("/filters/status")
129
144
  async def get_filter_status() -> Dict[str, Any]:
130
145
  """Get current filter configuration and enabled categories."""
@@ -184,7 +199,26 @@ async def disable_all_filters() -> Dict[str, Any]:
184
199
  return {"enabled": [], "total": 0}
185
200
 
186
201
 
187
- def start_api_server(state, host: str = "127.0.0.1", port: int = 8765):
202
+ @api.post("/release")
203
+ async def release_port() -> Dict[str, Any]:
204
+ """Release API port for another WebTap instance."""
205
+ logger.info("Releasing API port for another instance")
206
+
207
+ # Schedule graceful shutdown after response
208
+ def shutdown():
209
+ # Just set the flag to stop uvicorn, don't kill the whole process
210
+ global _shutdown_requested
211
+ _shutdown_requested = True
212
+
213
+ threading.Timer(0.5, shutdown).start()
214
+ return {"message": "Releasing port 8765"}
215
+
216
+
217
+ # Flag to signal shutdown
218
+ _shutdown_requested = False
219
+
220
+
221
+ def start_api_server(state, host: str = "127.0.0.1", port: int = 8765) -> threading.Thread | None:
188
222
  """Start the API server in a background thread.
189
223
 
190
224
  Args:
@@ -193,10 +227,19 @@ def start_api_server(state, host: str = "127.0.0.1", port: int = 8765):
193
227
  port: Port to bind to. Defaults to 8765.
194
228
 
195
229
  Returns:
196
- Thread instance running the server.
230
+ Thread instance running the server, or None if port is in use.
197
231
  """
198
- global app_state
232
+ # Check port availability first
233
+ try:
234
+ with socket.socket() as s:
235
+ s.bind((host, port))
236
+ except OSError:
237
+ logger.info(f"Port {port} already in use")
238
+ return None
239
+
240
+ global app_state, _shutdown_requested
199
241
  app_state = state
242
+ _shutdown_requested = False # Reset flag for new instance
200
243
 
201
244
  thread = threading.Thread(target=run_server, args=(host, port), daemon=True)
202
245
  thread.start()
@@ -207,14 +250,46 @@ def start_api_server(state, host: str = "127.0.0.1", port: int = 8765):
207
250
 
208
251
  def run_server(host: str, port: int):
209
252
  """Run the FastAPI server in a thread."""
210
- try:
211
- uvicorn.run(
253
+ import asyncio
254
+
255
+ async def run():
256
+ """Run server with proper shutdown handling."""
257
+ config = uvicorn.Config(
212
258
  api,
213
259
  host=host,
214
260
  port=port,
215
261
  log_level="error",
216
262
  access_log=False,
217
263
  )
264
+ server = uvicorn.Server(config)
265
+
266
+ # Start server in background task
267
+ serve_task = asyncio.create_task(server.serve())
268
+
269
+ # Wait for shutdown signal
270
+ while not _shutdown_requested:
271
+ await asyncio.sleep(0.1)
272
+ if serve_task.done():
273
+ break
274
+
275
+ # Trigger shutdown
276
+ if not serve_task.done():
277
+ logger.info("API server shutting down")
278
+ server.should_exit = True
279
+ # Wait a bit for graceful shutdown
280
+ try:
281
+ await asyncio.wait_for(serve_task, timeout=1.0)
282
+ except asyncio.TimeoutError:
283
+ logger.debug("Server task timeout - cancelling")
284
+ serve_task.cancel()
285
+ try:
286
+ await serve_task
287
+ except asyncio.CancelledError:
288
+ pass
289
+
290
+ try:
291
+ # Use asyncio.run() which properly cleans up
292
+ asyncio.run(run())
218
293
  except Exception as e:
219
294
  logger.error(f"API server failed: {e}")
220
295
 
@@ -6,6 +6,7 @@ PUBLIC API:
6
6
  """
7
7
 
8
8
  import sys
9
+ import threading
9
10
  from dataclasses import dataclass, field
10
11
 
11
12
  from replkit2 import App
@@ -24,15 +25,32 @@ class WebTapState:
24
25
  Attributes:
25
26
  cdp: Chrome DevTools Protocol session instance.
26
27
  service: WebTapService orchestrating all domain services.
28
+ api_thread: Thread running the FastAPI server (if this instance owns the port).
27
29
  """
28
30
 
29
31
  cdp: CDPSession = field(default_factory=CDPSession)
30
32
  service: WebTapService = field(init=False)
33
+ api_thread: threading.Thread | None = None
31
34
 
32
35
  def __post_init__(self):
33
36
  """Initialize service with self reference after dataclass init."""
34
37
  self.service = WebTapService(self)
35
38
 
39
+ def cleanup(self):
40
+ """Cleanup resources on exit."""
41
+ # Disconnect CDP if connected
42
+ if self.cdp.is_connected:
43
+ self.cdp.disconnect()
44
+
45
+ # Stop API server if we own it
46
+ if self.api_thread and self.api_thread.is_alive():
47
+ # Import here to avoid circular dependency
48
+ import webtap.api
49
+
50
+ webtap.api._shutdown_requested = True
51
+ # Wait up to 1 second for graceful shutdown
52
+ self.api_thread.join(timeout=1.0)
53
+
36
54
 
37
55
  # Must be created before command imports for decorator registration
38
56
  app = App(
@@ -74,7 +74,8 @@ def build_query(
74
74
  pattern = value
75
75
  path_conditions.append(f"json_extract_string(event, '{json_path}') LIKE '{pattern}'")
76
76
  elif isinstance(value, (int, float)):
77
- path_conditions.append(f"CAST(json_extract_string(event, '{json_path}') AS NUMERIC) = {value}")
77
+ # Use string comparison for numeric values to avoid type conversion errors
78
+ path_conditions.append(f"json_extract_string(event, '{json_path}') = '{value}'")
78
79
  elif isinstance(value, bool):
79
80
  path_conditions.append(f"json_extract_string(event, '{json_path}') = '{str(value).lower()}'")
80
81
  elif value is None:
@@ -46,23 +46,9 @@ def run_chrome(state, detach: bool = True, port: int = 9222) -> dict:
46
46
  ],
47
47
  )
48
48
 
49
- # Setup temp profile with symlinks to real profile
49
+ # Simple: use clean temp profile for debugging
50
50
  temp_config = Path("/tmp/webtap-chrome-debug")
51
- real_config = Path.home() / ".config" / "google-chrome"
52
-
53
- if not temp_config.exists():
54
- temp_config.mkdir(parents=True)
55
-
56
- # Symlink Default profile
57
- default_profile = real_config / "Default"
58
- if default_profile.exists():
59
- (temp_config / "Default").symlink_to(default_profile)
60
-
61
- # Copy essential files
62
- for file in ["Local State", "First Run"]:
63
- src = real_config / file
64
- if src.exists():
65
- (temp_config / file).write_text(src.read_text())
51
+ temp_config.mkdir(parents=True, exist_ok=True)
66
52
 
67
53
  # Launch Chrome
68
54
  cmd = [chrome_exe, f"--remote-debugging-port={port}", "--remote-allow-origins=*", f"--user-data-dir={temp_config}"]
@@ -74,7 +60,7 @@ def run_chrome(state, detach: bool = True, port: int = 9222) -> dict:
74
60
  details={
75
61
  "Port": str(port),
76
62
  "Mode": "Background (detached)",
77
- "Profile": str(temp_config),
63
+ "Profile": "Temporary (clean)",
78
64
  "Next step": "Run connect() to attach WebTap",
79
65
  },
80
66
  )
@@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
12
12
 
13
13
 
14
14
  class BodyService:
15
- """Internal service for response body fetching and caching."""
15
+ """Response body fetching and caching."""
16
16
 
17
17
  def __init__(self):
18
18
  """Initialize body service."""
@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
10
10
 
11
11
 
12
12
  class ConsoleService:
13
- """Internal service for console event queries and monitoring."""
13
+ """Console event queries and monitoring."""
14
14
 
15
15
  def __init__(self):
16
16
  """Initialize console service."""
@@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
13
13
 
14
14
 
15
15
  class FetchService:
16
- """Internal service for fetch interception with explicit actions."""
16
+ """Fetch interception with explicit actions."""
17
17
 
18
18
  def __init__(self):
19
19
  """Initialize fetch service."""
@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
10
10
 
11
11
 
12
12
  class NetworkService:
13
- """Internal service for network event queries and monitoring."""
13
+ """Network event queries and monitoring."""
14
14
 
15
15
  def __init__(self):
16
16
  """Initialize network service."""
@@ -81,7 +81,7 @@ class NetworkService:
81
81
  json_extract_string(event, '$.params.response.statusText') as StatusText
82
82
  FROM events
83
83
  WHERE json_extract_string(event, '$.method') = 'Network.responseReceived'
84
- AND CAST(json_extract_string(event, '$.params.response.status') AS INTEGER) >= 400
84
+ AND json_extract_string(event, '$.params.response.status') >= '400'
85
85
  ORDER BY rowid DESC LIMIT {limit}
86
86
  """
87
87
 
@@ -177,22 +177,32 @@ class SetupService:
177
177
  }
178
178
 
179
179
  wrapper_script = """#!/bin/bash
180
- # Chrome wrapper to always enable debugging
180
+ # Chrome wrapper using bindfs for perfect state sync with debug port
181
181
 
182
- REAL_CONFIG="$HOME/.config/google-chrome"
183
- DEBUG_CONFIG="/tmp/chrome-debug-profile"
182
+ DEBUG_DIR="$HOME/.config/google-chrome-debug"
183
+ REAL_DIR="$HOME/.config/google-chrome"
184
184
 
185
- if [ ! -d "$DEBUG_CONFIG" ]; then
186
- mkdir -p "$DEBUG_CONFIG"
187
- ln -sf "$REAL_CONFIG/Default" "$DEBUG_CONFIG/Default"
188
- cp "$REAL_CONFIG/Local State" "$DEBUG_CONFIG/" 2>/dev/null || true
189
- cp "$REAL_CONFIG/First Run" "$DEBUG_CONFIG/" 2>/dev/null || true
185
+ # Check if bindfs is installed
186
+ if ! command -v bindfs &>/dev/null; then
187
+ echo "Error: bindfs not installed. Install with: yay -S bindfs" >&2
188
+ exit 1
190
189
  fi
191
190
 
191
+ # Mount real profile via bindfs if not already mounted
192
+ if ! mountpoint -q "$DEBUG_DIR" 2>/dev/null; then
193
+ mkdir -p "$DEBUG_DIR"
194
+ if ! bindfs --no-allow-other "$REAL_DIR" "$DEBUG_DIR"; then
195
+ echo "Error: Failed to mount Chrome profile via bindfs" >&2
196
+ exit 1
197
+ fi
198
+ echo "Chrome debug profile mounted. To unmount: fusermount -u $DEBUG_DIR" >&2
199
+ fi
200
+
201
+ # Launch Chrome with debugging on bindfs mount
192
202
  exec /usr/bin/google-chrome-stable \\
193
203
  --remote-debugging-port=9222 \\
194
- --remote-allow-origins=* \\
195
- --user-data-dir="$DEBUG_CONFIG" \\
204
+ --remote-allow-origins='*' \\
205
+ --user-data-dir="$DEBUG_DIR" \\
196
206
  "$@"
197
207
  """
198
208
 
File without changes
File without changes
File without changes
File without changes