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 +234 -0
- webtap/__init__.py +56 -0
- webtap/api.py +222 -0
- webtap/app.py +76 -0
- webtap/cdp/README.md +268 -0
- webtap/cdp/__init__.py +14 -0
- webtap/cdp/query.py +107 -0
- webtap/cdp/schema/README.md +41 -0
- webtap/cdp/schema/cdp_protocol.json +32785 -0
- webtap/cdp/schema/cdp_version.json +8 -0
- webtap/cdp/session.py +365 -0
- webtap/commands/DEVELOPER_GUIDE.md +314 -0
- webtap/commands/TIPS.md +153 -0
- webtap/commands/__init__.py +7 -0
- webtap/commands/_builders.py +127 -0
- webtap/commands/_errors.py +108 -0
- webtap/commands/_tips.py +147 -0
- webtap/commands/_utils.py +227 -0
- webtap/commands/body.py +161 -0
- webtap/commands/connection.py +168 -0
- webtap/commands/console.py +69 -0
- webtap/commands/events.py +109 -0
- webtap/commands/fetch.py +219 -0
- webtap/commands/filters.py +224 -0
- webtap/commands/inspect.py +146 -0
- webtap/commands/javascript.py +87 -0
- webtap/commands/launch.py +86 -0
- webtap/commands/navigation.py +199 -0
- webtap/commands/network.py +85 -0
- webtap/commands/setup.py +127 -0
- webtap/filters.py +289 -0
- webtap/services/README.md +83 -0
- webtap/services/__init__.py +15 -0
- webtap/services/body.py +113 -0
- webtap/services/console.py +116 -0
- webtap/services/fetch.py +397 -0
- webtap/services/main.py +175 -0
- webtap/services/network.py +105 -0
- webtap/services/setup.py +219 -0
- webtap_tool-0.1.1.dist-info/METADATA +427 -0
- webtap_tool-0.1.1.dist-info/RECORD +43 -0
- webtap_tool-0.1.1.dist-info/WHEEL +4 -0
- webtap_tool-0.1.1.dist-info/entry_points.txt +2 -0
webtap/commands/TIPS.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
### body
|
|
15
|
+
Fetch and analyze HTTP response bodies with Python expressions.
|
|
16
|
+
|
|
17
|
+
#### Examples
|
|
18
|
+
```python
|
|
19
|
+
body(123) # Get body
|
|
20
|
+
body(123, "json.loads(body)") # Parse JSON
|
|
21
|
+
body(123, "bs4(body, 'html.parser').find('title').text") # HTML title
|
|
22
|
+
body(123, "jwt.decode(body, options={'verify_signature': False})") # Decode JWT
|
|
23
|
+
body(123, "re.findall(r'/api/[^\"\\s]+', body)[:10]") # Find API endpoints
|
|
24
|
+
body(123, "httpx.get(json.loads(body)['next_url']).json()") # Chain requests
|
|
25
|
+
body(123, "msgpack.unpackb(body)") # Binary formats
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
#### Tips
|
|
29
|
+
- **Chain requests:** `body({id}, "httpx.get(json.loads(body)['next_url']).text[:100]")`
|
|
30
|
+
- **Parse XML:** `body({id}, "ElementTree.fromstring(body).find('.//title').text")`
|
|
31
|
+
- **Extract forms:** `body({id}, "[f['action'] for f in bs4(body, 'html.parser').find_all('form')]")`
|
|
32
|
+
- **Decode protobuf:** `body({id}, "protobuf_json.Parse(body, YourMessage())")`
|
|
33
|
+
- **Find related:** `events({'requestId': request_id})` - related events
|
|
34
|
+
|
|
35
|
+
### inspect
|
|
36
|
+
Inspect CDP events with full Python debugging.
|
|
37
|
+
|
|
38
|
+
Available objects: 'data' (when inspecting event), 'cdp' and 'state' (when no event).
|
|
39
|
+
|
|
40
|
+
#### Examples
|
|
41
|
+
```python
|
|
42
|
+
inspect(456) # Full event
|
|
43
|
+
inspect(456, "data['method']") # Event type
|
|
44
|
+
inspect(456, "list(data.keys())") # Top-level keys
|
|
45
|
+
inspect(456, "data.get('params', {}).get('response', {}).get('status')")
|
|
46
|
+
inspect(456, "re.findall(r'session=(\\w+)', str(data))") # Extract patterns
|
|
47
|
+
inspect(456, "base64.b64decode(data['params']['response']['body'])")
|
|
48
|
+
inspect(456, "jwt.decode(auth.replace('Bearer ', ''), options={'verify_signature': False})")
|
|
49
|
+
inspect(expr="len(cdp.events)") # Direct CDP access
|
|
50
|
+
inspect(expr="[e for e in cdp.events if 'error' in str(e).lower()][:3]")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### Tips
|
|
54
|
+
- **Find related:** `events({'requestId': data.get('params', {}).get('requestId')})`
|
|
55
|
+
- **Compare events:** `inspect(other_id, "data.get('method')")`
|
|
56
|
+
- **Extract timing:** `inspect({id}, "data['params']['timing']")`
|
|
57
|
+
- **Decode cookies:** `inspect({id}, "[c.split('=') for c in data.get('params', {}).get('cookies', '').split(';')]")`
|
|
58
|
+
- **Get body:** `body({id})` - if this is a response event
|
|
59
|
+
|
|
60
|
+
### network
|
|
61
|
+
Show network requests with full data.
|
|
62
|
+
|
|
63
|
+
#### Tips
|
|
64
|
+
- **Analyze responses:** `body({id})` - fetch response body
|
|
65
|
+
- **Parse HTML:** `body({id}, "bs4(body, 'html.parser').find('title').text")`
|
|
66
|
+
- **Extract JSON:** `body({id}, "json.loads(body)['data']")`
|
|
67
|
+
- **Find patterns:** `body({id}, "re.findall(r'/api/\\w+', body)")`
|
|
68
|
+
- **Decode JWT:** `body({id}, "jwt.decode(body, options={'verify_signature': False})")`
|
|
69
|
+
- **Search events:** `events({'url': '*api*'})` - find all API calls
|
|
70
|
+
- **Intercept traffic:** `fetch('enable')` then `requests()` - pause and modify
|
|
71
|
+
|
|
72
|
+
### console
|
|
73
|
+
Show console messages with full data.
|
|
74
|
+
|
|
75
|
+
#### Tips
|
|
76
|
+
- **Inspect error:** `inspect({id})` - view full stack trace
|
|
77
|
+
- **Find all errors:** `events({'level': 'error'})` - filter console errors
|
|
78
|
+
- **Extract stack:** `inspect({id}, "data.get('stackTrace', {})")`
|
|
79
|
+
- **Search messages:** `events({'message': '*failed*'})` - pattern match
|
|
80
|
+
- **Check network:** `network()` - may show failed requests causing errors
|
|
81
|
+
|
|
82
|
+
### events
|
|
83
|
+
Query CDP events by field values with automatic discovery.
|
|
84
|
+
|
|
85
|
+
Searches across ALL event types - network, console, page, etc.
|
|
86
|
+
Field names are discovered automatically and case-insensitive.
|
|
87
|
+
|
|
88
|
+
#### Examples
|
|
89
|
+
```python
|
|
90
|
+
events() # Recent 20 events
|
|
91
|
+
events({"method": "Runtime.*"}) # Runtime events
|
|
92
|
+
events({"requestId": "123"}, limit=100) # Specific request
|
|
93
|
+
events({"url": "*api*"}) # Find all API calls
|
|
94
|
+
events({"status": 200}) # Successful responses
|
|
95
|
+
events({"level": "error"}) # Console errors
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### Tips
|
|
99
|
+
- **Inspect full event:** `inspect({id})` - view complete CDP data
|
|
100
|
+
- **Extract nested data:** `inspect({id}, "data['params']['response']['headers']")`
|
|
101
|
+
- **Find patterns:** `inspect({id}, "re.findall(r'token=(\\w+)', str(data))")`
|
|
102
|
+
- **Get response body:** `body({id})` - if this is a network response
|
|
103
|
+
- **Decode data:** `inspect({id}, "base64.b64decode(data.get('params', {}).get('body', ''))")`
|
|
104
|
+
|
|
105
|
+
### js
|
|
106
|
+
Execute JavaScript in the browser with optional promise handling.
|
|
107
|
+
|
|
108
|
+
#### Examples
|
|
109
|
+
```python
|
|
110
|
+
js("document.title") # Get page title
|
|
111
|
+
js("document.body.innerText.length") # Get text length
|
|
112
|
+
js("[...document.links].map(a => a.href)") # Get all links
|
|
113
|
+
js("fetch('/api').then(r => r.json())", await_promise=True) # Async
|
|
114
|
+
js("document.querySelectorAll('.ad').forEach(e => e.remove())", wait_return=False)
|
|
115
|
+
js("window.fetch = new Proxy(window.fetch, {get: (t, p) => console.log(p)})", wait_return=False)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### Tips
|
|
119
|
+
- **Extract all links:** `js("[...document.links].map(a => a.href)")`
|
|
120
|
+
- **Get page text:** `js("document.body.innerText")`
|
|
121
|
+
- **Find data attributes:** `js("[...document.querySelectorAll('[data-id]')].map(e => e.dataset)")`
|
|
122
|
+
- **Monitor DOM:** `js("new MutationObserver(console.log).observe(document, {childList: true, subtree: true})", wait_return=False)`
|
|
123
|
+
- **Hook fetch:** `js("window.fetch = new Proxy(fetch, {apply: (t, _, a) => {console.log(a); return t(...a)}})", wait_return=False)`
|
|
124
|
+
- **Check console:** `console()` - see logged messages from JS execution
|
|
125
|
+
|
|
126
|
+
### fetch
|
|
127
|
+
Control request interception for debugging and modification.
|
|
128
|
+
|
|
129
|
+
#### Examples
|
|
130
|
+
```python
|
|
131
|
+
fetch("status") # Check status
|
|
132
|
+
fetch("enable") # Enable request stage
|
|
133
|
+
fetch("enable", {"response": true}) # Both stages
|
|
134
|
+
fetch("disable") # Disable
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### Tips
|
|
138
|
+
- **View paused:** `requests()` - see all intercepted requests
|
|
139
|
+
- **Inspect request:** `inspect({id})` - view full CDP event data
|
|
140
|
+
- **Analyze body:** `body({id})` - fetch and examine response body
|
|
141
|
+
- **Resume request:** `resume({id})` - continue the request
|
|
142
|
+
- **Modify request:** `resume({id}, modifications={'url': '...'})`
|
|
143
|
+
- **Block request:** `fail({id}, 'BlockedByClient')` - reject the request
|
|
144
|
+
|
|
145
|
+
### requests
|
|
146
|
+
Show paused requests and responses.
|
|
147
|
+
|
|
148
|
+
#### Tips
|
|
149
|
+
- **Inspect request:** `inspect({id})` - view full CDP event data
|
|
150
|
+
- **Analyze body:** `body({id})` - fetch and examine response body
|
|
151
|
+
- **Resume request:** `resume({id})` - continue the request
|
|
152
|
+
- **Modify request:** `resume({id}, modifications={'url': '...'})`
|
|
153
|
+
- **Fail request:** `fail({id}, 'BlockedByClient')` - block the request
|
|
@@ -0,0 +1,7 @@
|
|
|
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
|
+
|
|
7
|
+
# No imports needed here - app.py imports commands directly
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Response builders using ReplKit2 v0.10.0+ markdown elements."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def table_response(
|
|
7
|
+
title: str | None = None,
|
|
8
|
+
headers: list[str] | None = None,
|
|
9
|
+
rows: list[dict] | None = None,
|
|
10
|
+
summary: str | None = None,
|
|
11
|
+
warnings: list[str] | None = None,
|
|
12
|
+
tips: list[str] | None = None,
|
|
13
|
+
) -> dict:
|
|
14
|
+
"""Build table response with full data.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
title: Optional table title
|
|
18
|
+
headers: Column headers
|
|
19
|
+
rows: Data rows with FULL data
|
|
20
|
+
summary: Optional summary text
|
|
21
|
+
warnings: Optional warning messages
|
|
22
|
+
tips: Optional developer tips/guidance
|
|
23
|
+
"""
|
|
24
|
+
elements = []
|
|
25
|
+
|
|
26
|
+
if title:
|
|
27
|
+
elements.append({"type": "heading", "content": title, "level": 2})
|
|
28
|
+
|
|
29
|
+
if warnings:
|
|
30
|
+
for warning in warnings:
|
|
31
|
+
elements.append({"type": "alert", "message": warning, "level": "warning"})
|
|
32
|
+
|
|
33
|
+
if headers and rows:
|
|
34
|
+
elements.append({"type": "table", "headers": headers, "rows": rows})
|
|
35
|
+
elif rows: # Headers can be inferred from row keys
|
|
36
|
+
elements.append({"type": "table", "rows": rows})
|
|
37
|
+
else:
|
|
38
|
+
elements.append({"type": "text", "content": "_No data available_"})
|
|
39
|
+
|
|
40
|
+
if summary:
|
|
41
|
+
elements.append({"type": "text", "content": f"_{summary}_"})
|
|
42
|
+
|
|
43
|
+
if tips:
|
|
44
|
+
elements.append({"type": "heading", "content": "Next Steps", "level": 3})
|
|
45
|
+
elements.append({"type": "list", "items": tips})
|
|
46
|
+
|
|
47
|
+
return {"elements": elements}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def info_response(
|
|
51
|
+
title: str | None = None,
|
|
52
|
+
fields: dict | None = None,
|
|
53
|
+
extra: str | None = None,
|
|
54
|
+
) -> dict:
|
|
55
|
+
"""Build info display with key-value pairs.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
title: Optional info title
|
|
59
|
+
fields: Dict of field names to values
|
|
60
|
+
extra: Optional extra content (raw markdown)
|
|
61
|
+
"""
|
|
62
|
+
elements = []
|
|
63
|
+
|
|
64
|
+
if title:
|
|
65
|
+
elements.append({"type": "heading", "content": title, "level": 2})
|
|
66
|
+
|
|
67
|
+
if fields:
|
|
68
|
+
for key, value in fields.items():
|
|
69
|
+
if value is not None:
|
|
70
|
+
elements.append({"type": "text", "content": f"**{key}:** {value}"})
|
|
71
|
+
|
|
72
|
+
if extra:
|
|
73
|
+
elements.append({"type": "raw", "content": extra})
|
|
74
|
+
|
|
75
|
+
if not elements:
|
|
76
|
+
elements.append({"type": "text", "content": "_No information available_"})
|
|
77
|
+
|
|
78
|
+
return {"elements": elements}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def error_response(message: str, suggestions: list[str] | None = None) -> dict:
|
|
82
|
+
"""Build error response with optional suggestions.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
message: Error message
|
|
86
|
+
suggestions: Optional list of suggestions
|
|
87
|
+
"""
|
|
88
|
+
elements: list[dict[str, Any]] = [{"type": "alert", "message": message, "level": "error"}]
|
|
89
|
+
|
|
90
|
+
if suggestions:
|
|
91
|
+
elements.append({"type": "text", "content": "**Try:**"})
|
|
92
|
+
elements.append({"type": "list", "items": suggestions})
|
|
93
|
+
|
|
94
|
+
return {"elements": elements}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def success_response(message: str, details: dict | None = None) -> dict:
|
|
98
|
+
"""Build success response with optional details.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
message: Success message
|
|
102
|
+
details: Optional dict of additional details
|
|
103
|
+
"""
|
|
104
|
+
elements = [{"type": "alert", "message": message, "level": "success"}]
|
|
105
|
+
|
|
106
|
+
if details:
|
|
107
|
+
for key, value in details.items():
|
|
108
|
+
if value is not None:
|
|
109
|
+
elements.append({"type": "text", "content": f"**{key}:** {value}"})
|
|
110
|
+
|
|
111
|
+
return {"elements": elements}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def warning_response(message: str, suggestions: list[str] | None = None) -> dict:
|
|
115
|
+
"""Build warning response with optional suggestions.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
message: Warning message
|
|
119
|
+
suggestions: Optional list of suggestions
|
|
120
|
+
"""
|
|
121
|
+
elements: list[dict[str, Any]] = [{"type": "alert", "message": message, "level": "warning"}]
|
|
122
|
+
|
|
123
|
+
if suggestions:
|
|
124
|
+
elements.append({"type": "text", "content": "**Try:**"})
|
|
125
|
+
elements.append({"type": "list", "items": suggestions})
|
|
126
|
+
|
|
127
|
+
return {"elements": elements}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Unified error handling for WebTap commands."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from replkit2.textkit import markdown
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
ERRORS = {
|
|
8
|
+
"not_connected": {
|
|
9
|
+
"message": "Not connected to Chrome",
|
|
10
|
+
"details": "Use `connect()` to connect to a page",
|
|
11
|
+
"help": [
|
|
12
|
+
"Run `pages()` to see available tabs",
|
|
13
|
+
"Use `connect(0)` to connect to first tab",
|
|
14
|
+
"Or `connect(page_id='...')` for specific tab",
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
"fetch_disabled": {
|
|
18
|
+
"message": "Fetch interception not enabled",
|
|
19
|
+
"details": "Enable with `fetch(enable=True)` to pause requests",
|
|
20
|
+
},
|
|
21
|
+
"no_data": {"message": "No data available", "details": "The requested data is not available"},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def check_connection(state) -> Optional[dict]:
|
|
26
|
+
"""Check CDP connection and return error response if not connected.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
state: Application state containing CDP session.
|
|
30
|
+
"""
|
|
31
|
+
if not (state.cdp and state.cdp.is_connected):
|
|
32
|
+
return error_response("not_connected")
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def error_response(error_key: str, custom_message: str | None = None, **kwargs) -> dict:
|
|
37
|
+
"""Build consistent error response in markdown.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
error_key: Key from error templates or custom identifier.
|
|
41
|
+
custom_message: Override default message.
|
|
42
|
+
**kwargs: Additional context to add to error response.
|
|
43
|
+
"""
|
|
44
|
+
error_info = ERRORS.get(error_key, {})
|
|
45
|
+
message = custom_message or error_info.get("message", "Error occurred")
|
|
46
|
+
|
|
47
|
+
builder = markdown().element("alert", message=message, level="error")
|
|
48
|
+
|
|
49
|
+
if details := error_info.get("details"):
|
|
50
|
+
builder.text(details)
|
|
51
|
+
|
|
52
|
+
if help_items := error_info.get("help"):
|
|
53
|
+
builder.text("**How to fix:**")
|
|
54
|
+
builder.list_(help_items)
|
|
55
|
+
|
|
56
|
+
for key, value in kwargs.items():
|
|
57
|
+
if value:
|
|
58
|
+
builder.text(f"_{key}: {value}_")
|
|
59
|
+
|
|
60
|
+
return builder.build()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def warning_response(message: str, details: str | None = None) -> dict:
|
|
64
|
+
"""Build warning response for non-fatal issues.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
message: Warning message text.
|
|
68
|
+
details: Additional details.
|
|
69
|
+
"""
|
|
70
|
+
builder = markdown().element("alert", message=message, level="warning")
|
|
71
|
+
if details:
|
|
72
|
+
builder.text(details)
|
|
73
|
+
return builder.build()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def invalid_options_error(message: str, expected: dict = None) -> dict: # pyright: ignore[reportArgumentType]
|
|
77
|
+
"""Create error response for invalid options dict.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
message: Error message describing the issue.
|
|
81
|
+
expected: Optional example of expected format.
|
|
82
|
+
"""
|
|
83
|
+
builder = markdown().element("alert", message=message, level="error")
|
|
84
|
+
|
|
85
|
+
if expected:
|
|
86
|
+
builder.text("**Expected format:**")
|
|
87
|
+
import json
|
|
88
|
+
|
|
89
|
+
builder.code_block(json.dumps(expected, indent=2), language="json")
|
|
90
|
+
|
|
91
|
+
return builder.build()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def missing_param_error(param: str, command: str = None) -> dict: # pyright: ignore[reportArgumentType]
|
|
95
|
+
"""Create error response for missing required parameter.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
param: Name of the missing parameter.
|
|
99
|
+
command: Optional command name for context.
|
|
100
|
+
"""
|
|
101
|
+
message = f"Required parameter '{param}' not provided"
|
|
102
|
+
if command:
|
|
103
|
+
message = f"{command}: {message}"
|
|
104
|
+
|
|
105
|
+
builder = markdown().element("alert", message=message, level="error")
|
|
106
|
+
builder.text(f"The '{param}' parameter is required for this operation")
|
|
107
|
+
|
|
108
|
+
return builder.build()
|
webtap/commands/_tips.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Parser for TIPS.md documentation.
|
|
2
|
+
|
|
3
|
+
This module reads TIPS.md and provides:
|
|
4
|
+
- MCP descriptions for commands
|
|
5
|
+
- Developer tips for command responses
|
|
6
|
+
- Pre-imported libraries documentation
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TipsParser:
|
|
15
|
+
"""Parse TIPS.md for command documentation."""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
# TIPS.md is in the same directory as this file
|
|
19
|
+
self.tips_path = Path(__file__).parent / "TIPS.md"
|
|
20
|
+
self.content = self.tips_path.read_text() if self.tips_path.exists() else ""
|
|
21
|
+
self._cache = {}
|
|
22
|
+
|
|
23
|
+
def _get_libraries(self) -> str:
|
|
24
|
+
"""Extract the libraries section."""
|
|
25
|
+
if "libraries" not in self._cache:
|
|
26
|
+
match = re.search(r"## Libraries\n(.*?)(?=\n##)", self.content, re.DOTALL)
|
|
27
|
+
self._cache["libraries"] = match.group(1).strip() if match else ""
|
|
28
|
+
return self._cache["libraries"]
|
|
29
|
+
|
|
30
|
+
def _get_command_section(self, command: str) -> Optional[str]:
|
|
31
|
+
"""Get the full section for a command."""
|
|
32
|
+
# Simple and explicit - look for the exact command name
|
|
33
|
+
# Use negative lookahead to ensure we match ### but not ####
|
|
34
|
+
pattern = rf"### {re.escape(command)}\n(.*?)(?=\n###(?!#)|\Z)"
|
|
35
|
+
match = re.search(pattern, self.content, re.DOTALL)
|
|
36
|
+
return match.group(1).strip() if match else None
|
|
37
|
+
|
|
38
|
+
def _get_description(self, command: str) -> Optional[str]:
|
|
39
|
+
"""Get command description (text before #### sections)."""
|
|
40
|
+
section = self._get_command_section(command)
|
|
41
|
+
if not section:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
# Extract text before first #### heading
|
|
45
|
+
match = re.match(r"(.*?)(?=\n####|\Z)", section, re.DOTALL)
|
|
46
|
+
return match.group(1).strip() if match else ""
|
|
47
|
+
|
|
48
|
+
def _get_examples(self, command: str) -> Optional[str]:
|
|
49
|
+
"""Get examples section for a command."""
|
|
50
|
+
section = self._get_command_section(command)
|
|
51
|
+
if not section:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
# Extract Examples section
|
|
55
|
+
match = re.search(r"#### Examples\n```python\n(.*?)\n```", section, re.DOTALL)
|
|
56
|
+
return match.group(1).strip() if match else None
|
|
57
|
+
|
|
58
|
+
def _get_tips(self, command: str, context: Optional[Dict] = None) -> Optional[List[str]]:
|
|
59
|
+
"""Get tips list for a command."""
|
|
60
|
+
section = self._get_command_section(command)
|
|
61
|
+
if not section:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
# Extract Tips section
|
|
65
|
+
match = re.search(r"#### Tips\n(.*?)(?=\n###|\n##|\Z)", section, re.DOTALL)
|
|
66
|
+
if not match:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
tips_text = match.group(1)
|
|
70
|
+
# Parse bullet points
|
|
71
|
+
tips = re.findall(r"^- (.+)$", tips_text, re.MULTILINE)
|
|
72
|
+
|
|
73
|
+
# Apply context substitutions
|
|
74
|
+
if context and tips:
|
|
75
|
+
formatted_tips = []
|
|
76
|
+
for tip in tips:
|
|
77
|
+
for key, value in context.items():
|
|
78
|
+
tip = tip.replace(f"{{{key}}}", str(value))
|
|
79
|
+
formatted_tips.append(tip)
|
|
80
|
+
return formatted_tips
|
|
81
|
+
|
|
82
|
+
return tips
|
|
83
|
+
|
|
84
|
+
def _get_mcp_description(self, command: str) -> Optional[str]:
|
|
85
|
+
"""Build MCP description from markdown."""
|
|
86
|
+
description = self._get_description(command)
|
|
87
|
+
if not description:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
# Build complete MCP description
|
|
91
|
+
parts = [description]
|
|
92
|
+
|
|
93
|
+
# Add libraries section for certain commands
|
|
94
|
+
if command in ["body", "inspect"]:
|
|
95
|
+
parts.append("")
|
|
96
|
+
parts.append(self._get_libraries())
|
|
97
|
+
|
|
98
|
+
# Add examples if available
|
|
99
|
+
examples = self._get_examples(command)
|
|
100
|
+
if examples:
|
|
101
|
+
parts.append("")
|
|
102
|
+
parts.append("Examples:")
|
|
103
|
+
# Indent examples
|
|
104
|
+
for line in examples.split("\n"):
|
|
105
|
+
parts.append(f" {line}" if line else "")
|
|
106
|
+
|
|
107
|
+
return "\n".join(parts)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Global parser instance
|
|
111
|
+
parser = TipsParser()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# Public API
|
|
115
|
+
def get_mcp_description(command: str) -> Optional[str]:
|
|
116
|
+
"""Get MCP description for a command from TIPS.md.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
command: Name of the command.
|
|
120
|
+
"""
|
|
121
|
+
return parser._get_mcp_description(command)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_tips(command: str, context: Optional[Dict] = None) -> Optional[List[str]]:
|
|
125
|
+
"""Get developer tips for a command from TIPS.md.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
command: Name of the command.
|
|
129
|
+
context: Optional context for variable substitution.
|
|
130
|
+
"""
|
|
131
|
+
return parser._get_tips(command, context)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_all_tips() -> Dict[str, List[str]]:
|
|
135
|
+
"""Get all available tips from TIPS.md."""
|
|
136
|
+
all_tips = {}
|
|
137
|
+
|
|
138
|
+
# Find all command sections
|
|
139
|
+
pattern = r"### ([^\n]+)\n"
|
|
140
|
+
matches = re.findall(pattern, parser.content)
|
|
141
|
+
|
|
142
|
+
for command in matches:
|
|
143
|
+
tips = parser._get_tips(command)
|
|
144
|
+
if tips:
|
|
145
|
+
all_tips[command] = tips
|
|
146
|
+
|
|
147
|
+
return all_tips
|