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,116 @@
1
+ """Filter group management commands."""
2
+
3
+ from webtap.app import app
4
+ from webtap.client import RPCError
5
+ from webtap.commands._builders import info_response, error_response, table_response
6
+ from webtap.commands._tips import get_mcp_description
7
+
8
+ _filters_desc = get_mcp_description("filters")
9
+
10
+
11
+ @app.command(
12
+ display="markdown", fastmcp={"type": "tool", "mime_type": "text/markdown", "description": _filters_desc or ""}
13
+ )
14
+ def filters(
15
+ state,
16
+ add: str = None, # pyright: ignore[reportArgumentType]
17
+ remove: str = None, # pyright: ignore[reportArgumentType]
18
+ enable: str = None, # pyright: ignore[reportArgumentType]
19
+ disable: str = None, # pyright: ignore[reportArgumentType]
20
+ hide: dict = None, # pyright: ignore[reportArgumentType]
21
+ ) -> dict:
22
+ """Manage filter groups for noise reduction.
23
+
24
+ Args:
25
+ add: Create new group with this name (requires hide=)
26
+ remove: Delete group by name
27
+ enable: Enable group by name
28
+ disable: Disable group by name
29
+ hide: Filter config for add {"types": [...], "urls": [...]}
30
+
31
+ Examples:
32
+ filters() # Show all groups
33
+ filters(add="assets", hide={"types": ["Image"]}) # Create group
34
+ filters(enable="assets") # Enable group
35
+ filters(disable="assets") # Disable group
36
+ filters(remove="assets") # Delete group
37
+ """
38
+ try:
39
+ # Handle add - create new group
40
+ if add:
41
+ if not hide:
42
+ return error_response("hide= required when adding a group")
43
+
44
+ state.client.call("filters.add", name=add, hide=hide)
45
+ return info_response(
46
+ title="Group Created",
47
+ fields={
48
+ "Name": add,
49
+ "Types": ", ".join(hide.get("types", [])) or "-",
50
+ "URLs": ", ".join(hide.get("urls", [])) or "-",
51
+ },
52
+ )
53
+
54
+ # Handle remove - delete group
55
+ if remove:
56
+ result = state.client.call("filters.remove", name=remove)
57
+ if result.get("removed"):
58
+ return info_response(title="Group Removed", fields={"Name": remove})
59
+ return error_response(f"Group '{remove}' not found")
60
+
61
+ # Handle enable - toggle group on (in-memory)
62
+ if enable:
63
+ result = state.client.call("filters.enable", name=enable)
64
+ if result.get("enabled"):
65
+ return info_response(title="Group Enabled", fields={"Name": enable})
66
+ return error_response(f"Group '{enable}' not found")
67
+
68
+ # Handle disable - toggle group off (in-memory)
69
+ if disable:
70
+ result = state.client.call("filters.disable", name=disable)
71
+ if result.get("disabled"):
72
+ return info_response(title="Group Disabled", fields={"Name": disable})
73
+ return error_response(f"Group '{disable}' not found")
74
+
75
+ # Default: list all groups with status
76
+ status = state.client.call("filters.status")
77
+
78
+ if not status:
79
+ return {
80
+ "elements": [
81
+ {"type": "heading", "content": "Filter Groups", "level": 2},
82
+ {"type": "text", "content": "No filter groups configured."},
83
+ {
84
+ "type": "text",
85
+ "content": 'Create one: `filters(add="assets", hide={"types": ["Image", "Font"]})`',
86
+ },
87
+ ]
88
+ }
89
+
90
+ # Build table
91
+ rows = []
92
+ for name, group in status.items():
93
+ hide_cfg = group.get("hide", {})
94
+ rows.append(
95
+ {
96
+ "Group": name,
97
+ "Status": "enabled" if group.get("enabled") else "disabled",
98
+ "Types": ", ".join(hide_cfg.get("types", [])) or "-",
99
+ "URLs": ", ".join(hide_cfg.get("urls", [])) or "-",
100
+ }
101
+ )
102
+
103
+ return table_response(
104
+ title="Filter Groups",
105
+ headers=["Group", "Status", "Types", "URLs"],
106
+ rows=rows,
107
+ tips=["Enabled groups hide matching requests from network()"],
108
+ )
109
+
110
+ except RPCError as e:
111
+ return error_response(e.message)
112
+ except Exception as e:
113
+ return error_response(str(e))
114
+
115
+
116
+ __all__ = ["filters"]
@@ -0,0 +1,73 @@
1
+ """JavaScript code execution in browser context."""
2
+
3
+ from replkit2.types import ExecutionContext
4
+
5
+ from webtap.app import app
6
+ from webtap.client import RPCError
7
+ from webtap.commands._builders import error_response, code_result_response
8
+ from webtap.commands._tips import get_mcp_description
9
+
10
+ mcp_desc = get_mcp_description("js")
11
+
12
+
13
+ @app.command(
14
+ display="markdown",
15
+ fastmcp={"type": "tool", "mime_type": "text/markdown", "description": mcp_desc}
16
+ if mcp_desc
17
+ else {"type": "tool", "mime_type": "text/markdown"},
18
+ )
19
+ def js(
20
+ state,
21
+ code: str,
22
+ selection: int = None, # pyright: ignore[reportArgumentType]
23
+ persist: bool = False,
24
+ wait_return: bool = True,
25
+ await_promise: bool = False,
26
+ _ctx: ExecutionContext = None, # pyright: ignore[reportArgumentType]
27
+ ) -> dict:
28
+ """Execute JavaScript in the browser. Uses fresh scope by default to avoid redeclaration errors.
29
+
30
+ Args:
31
+ code: JavaScript code to execute (single expression by default, multi-statement with persist=True)
32
+ selection: Browser element selection number - makes 'element' variable available
33
+ persist: Keep variables in global scope across calls (default: False)
34
+ wait_return: Wait for and return result (default: True)
35
+ await_promise: Await promises before returning (default: False)
36
+
37
+ Examples:
38
+ js("document.title") # Fresh scope (default)
39
+ js("[...document.links].map(a => a.href)") # Single expression works
40
+ js("var x = 1; x + 1", persist=True) # Multi-statement needs persist=True
41
+ js("element.offsetWidth", selection=1) # With browser element
42
+ js("fetch('/api')", await_promise=True) # Async operation
43
+ js("element.remove()", selection=1, wait_return=False) # No return needed
44
+ """
45
+ try:
46
+ result = state.client.call(
47
+ "js",
48
+ code=code,
49
+ selection=selection,
50
+ persist=persist,
51
+ await_promise=await_promise,
52
+ return_value=wait_return,
53
+ )
54
+
55
+ if wait_return:
56
+ return code_result_response("JavaScript Result", code, "javascript", result=result.get("value"))
57
+ else:
58
+ # Truncate code for display
59
+ is_repl = _ctx and _ctx.is_repl()
60
+ max_len = 50 if is_repl else 200
61
+ display_code = code if len(code) <= max_len else code[:max_len] + "..."
62
+
63
+ return {
64
+ "elements": [
65
+ {"type": "heading", "content": "JavaScript Execution", "level": 2},
66
+ {"type": "text", "content": f"**Status:** Executed\n\n**Expression:** `{display_code}`"},
67
+ ]
68
+ }
69
+
70
+ except RPCError as e:
71
+ return error_response(e.message)
72
+ except Exception as e:
73
+ return error_response(str(e))
@@ -0,0 +1,73 @@
1
+ """Export JavaScript evaluation results to local files.
2
+
3
+ This module provides the js_export command for saving JS eval output.
4
+ """
5
+
6
+ from webtap.app import app
7
+ from webtap.commands._builders import error_response, success_response
8
+ from webtap.commands._code_generation import ensure_output_directory
9
+
10
+
11
+ @app.command(display="markdown", fastmcp={"type": "tool"})
12
+ def js_export(
13
+ state,
14
+ code: str,
15
+ output: str,
16
+ selection: int = None, # pyright: ignore[reportArgumentType]
17
+ persist: bool = False,
18
+ await_promise: bool = False,
19
+ ) -> dict:
20
+ """Export JavaScript evaluation result to a local file.
21
+
22
+ Args:
23
+ code: JavaScript expression to evaluate (result is written to file)
24
+ output: Output file path
25
+ selection: Browser selection number to bind to 'element' variable. Defaults to None.
26
+ persist: Keep variables in global scope. Defaults to False.
27
+ await_promise: Await promise results. Defaults to False.
28
+
29
+ Returns:
30
+ Success or error response with file details.
31
+
32
+ Examples:
33
+ js_export("setEquipment.toString()", "out/fn.js")
34
+ js_export("JSON.stringify(x2netvars, null, 2)", "out/vars.json")
35
+ """
36
+ # Execute JS via RPC
37
+ try:
38
+ result = state.client.call(
39
+ "js",
40
+ code=code,
41
+ selection=selection,
42
+ persist=persist,
43
+ await_promise=await_promise,
44
+ return_value=True,
45
+ )
46
+ except Exception as e:
47
+ return error_response(f"JavaScript execution failed: {e}")
48
+
49
+ if not result.get("executed"):
50
+ return error_response("JavaScript execution did not complete")
51
+
52
+ value = result.get("value")
53
+ if value is None:
54
+ return error_response("Expression returned null/undefined")
55
+
56
+ # Convert to string if needed
57
+ content = value if isinstance(value, str) else str(value)
58
+
59
+ # Write to file
60
+ output_path = ensure_output_directory(output)
61
+ try:
62
+ output_path.write_text(content)
63
+ except Exception as e:
64
+ return error_response(f"Failed to write file: {e}")
65
+
66
+ return success_response(
67
+ "Exported successfully",
68
+ details={
69
+ "Output": str(output_path),
70
+ "Size": f"{output_path.stat().st_size} bytes",
71
+ "Lines": len(content.splitlines()),
72
+ },
73
+ )
@@ -0,0 +1,72 @@
1
+ """Chrome launch commands for WebTap."""
2
+
3
+ import shutil
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ from webtap.app import app
8
+ from webtap.commands._builders import success_response, error_response
9
+
10
+
11
+ @app.command(
12
+ display="markdown",
13
+ typer={"name": "run-chrome", "help": "Launch Chrome with debugging enabled"},
14
+ fastmcp={"enabled": False},
15
+ )
16
+ def run_chrome(state, detach: bool = True, port: int = 9222) -> dict:
17
+ """Launch Chrome with debugging enabled for WebTap.
18
+
19
+ Args:
20
+ detach: Run Chrome in background (default: True)
21
+ port: Debugging port (default: 9222)
22
+
23
+ Returns:
24
+ Status message
25
+ """
26
+ # Find Chrome executable
27
+ chrome_paths = [
28
+ "google-chrome-stable",
29
+ "google-chrome",
30
+ "chromium-browser",
31
+ "chromium",
32
+ ]
33
+
34
+ chrome_exe = None
35
+ for path in chrome_paths:
36
+ if shutil.which(path):
37
+ chrome_exe = path
38
+ break
39
+
40
+ if not chrome_exe:
41
+ return error_response(
42
+ "Chrome not found",
43
+ suggestions=[
44
+ "Install google-chrome-stable: sudo apt install google-chrome-stable",
45
+ "Or install chromium: sudo apt install chromium-browser",
46
+ ],
47
+ )
48
+
49
+ # Simple: use clean temp profile for debugging
50
+ temp_config = Path("/tmp/webtap-chrome-debug")
51
+ temp_config.mkdir(parents=True, exist_ok=True)
52
+
53
+ # Launch Chrome
54
+ cmd = [chrome_exe, f"--remote-debugging-port={port}", "--remote-allow-origins=*", f"--user-data-dir={temp_config}"]
55
+
56
+ if detach:
57
+ subprocess.Popen(cmd, start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
58
+ return success_response(
59
+ f"Launched {chrome_exe}",
60
+ details={
61
+ "Port": str(port),
62
+ "Mode": "Background (detached)",
63
+ "Profile": "Temporary (clean)",
64
+ "Next step": "Run connect() to attach WebTap",
65
+ },
66
+ )
67
+ else:
68
+ result = subprocess.run(cmd)
69
+ if result.returncode == 0:
70
+ return success_response("Chrome closed normally")
71
+ else:
72
+ return error_response(f"Chrome exited with code {result.returncode}")
@@ -0,0 +1,197 @@
1
+ """Browser navigation commands."""
2
+
3
+ from webtap.app import app
4
+ from webtap.client import RPCError
5
+ from webtap.commands._builders import info_response, error_response, table_response
6
+ from webtap.commands._tips import get_tips
7
+
8
+
9
+ @app.command(
10
+ display="markdown",
11
+ fastmcp={"type": "tool", "mime_type": "text/markdown"},
12
+ )
13
+ def navigate(state, url: str) -> dict:
14
+ """Navigate to URL.
15
+
16
+ Args:
17
+ url: URL to navigate to
18
+
19
+ Returns:
20
+ Navigation result with frame and loader IDs
21
+ """
22
+ try:
23
+ result = state.client.call("navigate", url=url)
24
+
25
+ if result.get("error"):
26
+ return error_response(f"Navigation error: {result['error']}")
27
+
28
+ return info_response(
29
+ title="Navigation",
30
+ fields={
31
+ "URL": url,
32
+ "Frame ID": result.get("frame_id", ""),
33
+ "Loader ID": result.get("loader_id", ""),
34
+ },
35
+ )
36
+
37
+ except RPCError as e:
38
+ return error_response(e.message)
39
+ except Exception as e:
40
+ return error_response(f"Navigation failed: {e}")
41
+
42
+
43
+ @app.command(
44
+ display="markdown",
45
+ fastmcp={"type": "tool", "mime_type": "text/markdown"},
46
+ )
47
+ def reload(state, ignore_cache: bool = False) -> dict:
48
+ """Reload the current page.
49
+
50
+ Args:
51
+ ignore_cache: Force reload ignoring cache
52
+ """
53
+ try:
54
+ result = state.client.call("reload", ignore_cache=ignore_cache)
55
+
56
+ return info_response(
57
+ title="Page Reload",
58
+ fields={
59
+ "Status": "Page reloaded",
60
+ "Cache": "Ignored" if result.get("ignore_cache") else "Used",
61
+ },
62
+ )
63
+
64
+ except RPCError as e:
65
+ return error_response(e.message)
66
+ except Exception as e:
67
+ return error_response(f"Reload failed: {e}")
68
+
69
+
70
+ @app.command(
71
+ display="markdown",
72
+ fastmcp={"type": "tool", "mime_type": "text/markdown"},
73
+ )
74
+ def back(state) -> dict:
75
+ """Navigate back in history."""
76
+ try:
77
+ result = state.client.call("back")
78
+
79
+ if not result.get("navigated"):
80
+ return info_response(
81
+ title="Navigation Back",
82
+ fields={"Status": result.get("reason", "Cannot go back")},
83
+ )
84
+
85
+ return info_response(
86
+ title="Navigation Back",
87
+ fields={
88
+ "Status": "Navigated back",
89
+ "Page": result.get("title", ""),
90
+ "URL": result.get("url", ""),
91
+ "Index": f"{result.get('index', 0) + 1} of {result.get('total', 0)}",
92
+ },
93
+ )
94
+
95
+ except RPCError as e:
96
+ return error_response(e.message)
97
+ except Exception as e:
98
+ return error_response(f"Back navigation failed: {e}")
99
+
100
+
101
+ @app.command(
102
+ display="markdown",
103
+ fastmcp={"type": "tool", "mime_type": "text/markdown"},
104
+ )
105
+ def forward(state) -> dict:
106
+ """Navigate forward in history."""
107
+ try:
108
+ result = state.client.call("forward")
109
+
110
+ if not result.get("navigated"):
111
+ return info_response(
112
+ title="Navigation Forward",
113
+ fields={"Status": result.get("reason", "Cannot go forward")},
114
+ )
115
+
116
+ return info_response(
117
+ title="Navigation Forward",
118
+ fields={
119
+ "Status": "Navigated forward",
120
+ "Page": result.get("title", ""),
121
+ "URL": result.get("url", ""),
122
+ "Index": f"{result.get('index', 0) + 1} of {result.get('total', 0)}",
123
+ },
124
+ )
125
+
126
+ except RPCError as e:
127
+ return error_response(e.message)
128
+ except Exception as e:
129
+ return error_response(f"Forward navigation failed: {e}")
130
+
131
+
132
+ @app.command(
133
+ display="markdown",
134
+ fastmcp={"type": "resource", "mime_type": "text/markdown"},
135
+ )
136
+ def page(state) -> dict:
137
+ """Get current page information."""
138
+ try:
139
+ result = state.client.call("page")
140
+ tips = get_tips("page")
141
+
142
+ return info_response(
143
+ title=result.get("title", "Untitled Page"),
144
+ fields={
145
+ "URL": result.get("url", ""),
146
+ "ID": result.get("id", ""),
147
+ "Type": result.get("type", ""),
148
+ },
149
+ tips=tips,
150
+ )
151
+
152
+ except RPCError as e:
153
+ return error_response(e.message)
154
+ except Exception as e:
155
+ return error_response(f"Page info failed: {e}")
156
+
157
+
158
+ @app.command(
159
+ display="markdown",
160
+ fastmcp={"type": "resource", "mime_type": "text/markdown"},
161
+ )
162
+ def history(state) -> dict:
163
+ """Get navigation history."""
164
+ try:
165
+ result = state.client.call("history")
166
+ entries = result.get("entries", [])
167
+
168
+ if not entries:
169
+ return info_response(
170
+ title="Navigation History",
171
+ fields={"Status": "No history entries"},
172
+ )
173
+
174
+ # Format rows for table
175
+ rows = []
176
+ for e in entries:
177
+ marker = "→ " if e.get("current") else " "
178
+ rows.append(
179
+ {
180
+ "": marker,
181
+ "ID": e.get("id", ""),
182
+ "Title": e.get("title", "")[:40],
183
+ "URL": e.get("url", "")[:60],
184
+ "Type": e.get("type", ""),
185
+ }
186
+ )
187
+
188
+ return table_response(
189
+ title="Navigation History",
190
+ rows=rows,
191
+ summary=f"{len(entries)} entries",
192
+ )
193
+
194
+ except RPCError as e:
195
+ return error_response(e.message)
196
+ except Exception as e:
197
+ return error_response(f"History failed: {e}")
@@ -0,0 +1,136 @@
1
+ """Network request monitoring and display commands."""
2
+
3
+ from replkit2.types import ExecutionContext
4
+
5
+ from webtap.app import app
6
+ from webtap.client import RPCError
7
+ from webtap.commands._builders import table_response, error_response, format_size
8
+ from webtap.commands._tips import get_tips
9
+
10
+ # Truncation values for REPL mode (compact display)
11
+ _REPL_TRUNCATE = {
12
+ "ReqID": {"max": 12, "mode": "end"},
13
+ "URL": {"max": 60, "mode": "middle"},
14
+ }
15
+
16
+ # Truncation values for MCP mode (generous for LLM context)
17
+ _MCP_TRUNCATE = {
18
+ "ReqID": {"max": 50, "mode": "end"},
19
+ "URL": {"max": 200, "mode": "middle"},
20
+ }
21
+
22
+
23
+ @app.command(
24
+ display="markdown",
25
+ fastmcp=[{"type": "resource", "mime_type": "text/markdown"}, {"type": "tool", "mime_type": "text/markdown"}],
26
+ )
27
+ def network(
28
+ state,
29
+ status: int = None, # pyright: ignore[reportArgumentType]
30
+ method: str = None, # pyright: ignore[reportArgumentType]
31
+ resource_type: str = None, # pyright: ignore[reportArgumentType]
32
+ url: str = None, # pyright: ignore[reportArgumentType]
33
+ req_state: str = None, # pyright: ignore[reportArgumentType]
34
+ show_all: bool = False,
35
+ limit: int = 50,
36
+ _ctx: ExecutionContext = None, # pyright: ignore[reportArgumentType]
37
+ ) -> dict:
38
+ """List network requests with inline filters.
39
+
40
+ Args:
41
+ status: Filter by HTTP status code (e.g., 404, 500)
42
+ method: Filter by HTTP method (e.g., "POST", "GET")
43
+ resource_type: Filter by resource type (e.g., "xhr", "fetch", "websocket")
44
+ url: Filter by URL pattern (supports * wildcard)
45
+ req_state: Filter by state (pending, loading, complete, failed, paused)
46
+ show_all: Bypass noise filter groups
47
+ limit: Max results (default 50)
48
+
49
+ Examples:
50
+ network() # Default with noise filter
51
+ network(status=404) # Only 404s
52
+ network(method="POST") # Only POST requests
53
+ network(resource_type="websocket") # Only WebSocket
54
+ network(url="*api*") # URLs containing "api"
55
+ network(req_state="paused") # Only paused requests
56
+ network(show_all=True) # Show everything
57
+ """
58
+ # Build params, omitting None values
59
+ params = {"limit": limit, "show_all": show_all}
60
+ if status is not None:
61
+ params["status"] = status
62
+ if method is not None:
63
+ params["method"] = method
64
+ if resource_type is not None:
65
+ params["resource_type"] = resource_type
66
+ if url is not None:
67
+ params["url"] = url
68
+ if req_state is not None:
69
+ params["state"] = req_state
70
+
71
+ try:
72
+ result = state.client.call("network", **params)
73
+ requests = result.get("requests", [])
74
+ except RPCError as e:
75
+ return error_response(e.message)
76
+ except Exception as e:
77
+ return error_response(str(e))
78
+
79
+ # Mode-specific configuration
80
+ is_repl = _ctx and _ctx.is_repl()
81
+
82
+ # Check if any request has pause_stage (to show Pause column)
83
+ has_pause = any(r.get("pause_stage") for r in requests)
84
+
85
+ # Build rows with mode-specific formatting
86
+ rows = []
87
+ for r in requests:
88
+ row = {
89
+ "ID": str(r["id"]),
90
+ "ReqID": r["request_id"],
91
+ "Method": r["method"],
92
+ "Status": str(r["status"]) if r["status"] else "-",
93
+ "URL": r["url"],
94
+ "Type": r["type"] or "-",
95
+ # REPL: human-friendly format, MCP: raw bytes for LLM
96
+ "Size": format_size(r["size"]) if is_repl else (r["size"] or 0),
97
+ "State": r.get("state", "-"),
98
+ }
99
+ # Add Pause column if relevant
100
+ if has_pause:
101
+ row["Pause"] = r.get("pause_stage") or "-"
102
+ rows.append(row)
103
+
104
+ # Build response with developer guidance
105
+ warnings = []
106
+ if limit and len(requests) == limit:
107
+ warnings.append(f"Showing {limit} most recent (use limit parameter to see more)")
108
+
109
+ # Get tips from TIPS.md with context
110
+ combined_tips = []
111
+ if not show_all:
112
+ combined_tips.append("Use show_all=True to bypass filter groups")
113
+
114
+ if rows:
115
+ example_id = rows[0]["ID"]
116
+ context_tips = get_tips("network", context={"id": example_id})
117
+ if context_tips:
118
+ combined_tips.extend(context_tips)
119
+
120
+ # Use mode-specific truncation
121
+ truncate = _REPL_TRUNCATE if is_repl else _MCP_TRUNCATE
122
+
123
+ # Build headers dynamically
124
+ headers = ["ID", "ReqID", "Method", "Status", "URL", "Type", "Size", "State"]
125
+ if has_pause:
126
+ headers.append("Pause")
127
+
128
+ return table_response(
129
+ title="Network Requests",
130
+ headers=headers,
131
+ rows=rows,
132
+ summary=f"{len(rows)} requests" if rows else None,
133
+ warnings=warnings,
134
+ tips=combined_tips if combined_tips else None,
135
+ truncate=truncate,
136
+ )