onetool-mcp 1.0.0b1__py3-none-any.whl → 1.0.0rc2__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 (81) hide show
  1. onetool/cli.py +63 -4
  2. onetool_mcp-1.0.0rc2.dist-info/METADATA +266 -0
  3. onetool_mcp-1.0.0rc2.dist-info/RECORD +129 -0
  4. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/LICENSE.txt +1 -1
  5. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/NOTICE.txt +54 -64
  6. ot/__main__.py +6 -6
  7. ot/config/__init__.py +48 -46
  8. ot/config/global_templates/__init__.py +2 -2
  9. ot/config/{defaults → global_templates}/diagram-templates/api-flow.mmd +33 -33
  10. ot/config/{defaults → global_templates}/diagram-templates/c4-context.puml +30 -30
  11. ot/config/{defaults → global_templates}/diagram-templates/class-diagram.mmd +87 -87
  12. ot/config/{defaults → global_templates}/diagram-templates/feature-mindmap.mmd +70 -70
  13. ot/config/{defaults → global_templates}/diagram-templates/microservices.d2 +81 -81
  14. ot/config/{defaults → global_templates}/diagram-templates/project-gantt.mmd +37 -37
  15. ot/config/{defaults → global_templates}/diagram-templates/state-machine.mmd +42 -42
  16. ot/config/global_templates/diagram.yaml +167 -0
  17. ot/config/global_templates/onetool.yaml +3 -1
  18. ot/config/{defaults → global_templates}/prompts.yaml +102 -97
  19. ot/config/global_templates/security.yaml +31 -0
  20. ot/config/global_templates/servers.yaml +93 -12
  21. ot/config/global_templates/snippets.yaml +5 -26
  22. ot/config/{defaults → global_templates}/tool_templates/__init__.py +7 -7
  23. ot/config/loader.py +221 -105
  24. ot/config/mcp.py +5 -1
  25. ot/config/secrets.py +192 -190
  26. ot/decorators.py +116 -116
  27. ot/executor/__init__.py +35 -35
  28. ot/executor/base.py +16 -16
  29. ot/executor/fence_processor.py +83 -83
  30. ot/executor/linter.py +142 -142
  31. ot/executor/pep723.py +288 -288
  32. ot/executor/runner.py +20 -6
  33. ot/executor/simple.py +163 -163
  34. ot/executor/validator.py +603 -164
  35. ot/http_client.py +145 -145
  36. ot/logging/__init__.py +37 -37
  37. ot/logging/entry.py +213 -213
  38. ot/logging/format.py +191 -188
  39. ot/logging/span.py +349 -349
  40. ot/meta.py +236 -14
  41. ot/paths.py +32 -49
  42. ot/prompts.py +218 -218
  43. ot/proxy/manager.py +14 -2
  44. ot/registry/__init__.py +189 -189
  45. ot/registry/parser.py +269 -269
  46. ot/server.py +330 -315
  47. ot/shortcuts/__init__.py +15 -15
  48. ot/shortcuts/aliases.py +87 -87
  49. ot/shortcuts/snippets.py +258 -258
  50. ot/stats/__init__.py +35 -35
  51. ot/stats/html.py +2 -2
  52. ot/stats/reader.py +354 -354
  53. ot/stats/timing.py +57 -57
  54. ot/support.py +63 -63
  55. ot/tools.py +1 -1
  56. ot/utils/batch.py +161 -161
  57. ot/utils/cache.py +120 -120
  58. ot/utils/exceptions.py +23 -23
  59. ot/utils/factory.py +178 -179
  60. ot/utils/format.py +65 -65
  61. ot/utils/http.py +202 -202
  62. ot/utils/platform.py +45 -45
  63. ot/utils/truncate.py +69 -69
  64. ot_tools/__init__.py +4 -4
  65. ot_tools/_convert/__init__.py +12 -12
  66. ot_tools/_convert/pdf.py +254 -254
  67. ot_tools/diagram.yaml +167 -167
  68. ot_tools/scaffold.py +2 -2
  69. ot_tools/transform.py +124 -19
  70. ot_tools/web_fetch.py +94 -43
  71. onetool_mcp-1.0.0b1.dist-info/METADATA +0 -163
  72. onetool_mcp-1.0.0b1.dist-info/RECORD +0 -132
  73. ot/config/defaults/bench.yaml +0 -4
  74. ot/config/defaults/onetool.yaml +0 -25
  75. ot/config/defaults/servers.yaml +0 -7
  76. ot/config/defaults/snippets.yaml +0 -4
  77. ot_tools/firecrawl.py +0 -732
  78. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/WHEEL +0 -0
  79. {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/entry_points.txt +0 -0
  80. /ot/config/{defaults → global_templates}/tool_templates/extension.py +0 -0
  81. /ot/config/{defaults → global_templates}/tool_templates/isolated.py +0 -0
ot/utils/format.py CHANGED
@@ -1,65 +1,65 @@
1
- """Result serialization utilities for MCP responses."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- from typing import Any, Literal
7
-
8
- import yaml
9
-
10
- __all__ = ["FormatMode", "serialize_result"]
11
-
12
- FormatMode = Literal["json", "json_h", "yml", "yml_h", "raw"]
13
-
14
-
15
- def serialize_result(result: Any, fmt: FormatMode = "json") -> str:
16
- """Serialize tool result to string for MCP response.
17
-
18
- Tools return native Python types (dict, list, str). This function
19
- serializes them to a string suitable for MCP text content.
20
-
21
- Format modes:
22
- - json: Compact JSON (default, no spaces)
23
- - json_h: Human-readable JSON (2-space indent)
24
- - yml: YAML flow style (compact)
25
- - yml_h: YAML block style (human-readable)
26
- - raw: str() conversion
27
-
28
- Args:
29
- result: Tool result (dict, list, str, or other)
30
- fmt: Output format mode (default: "json")
31
-
32
- Returns:
33
- String representation suitable for MCP response
34
- """
35
- # Strings pass through unchanged for all formats except raw
36
- if isinstance(result, str) and fmt != "raw":
37
- return result
38
-
39
- if fmt == "raw":
40
- return str(result)
41
-
42
- if fmt == "json":
43
- if isinstance(result, (dict, list)):
44
- return json.dumps(result, ensure_ascii=False, separators=(",", ":"))
45
- return str(result)
46
-
47
- if fmt == "json_h":
48
- if isinstance(result, (dict, list)):
49
- return json.dumps(result, ensure_ascii=False, indent=2)
50
- return str(result)
51
-
52
- if fmt == "yml":
53
- if isinstance(result, (dict, list)):
54
- return yaml.dump(result, default_flow_style=True, allow_unicode=True, sort_keys=False).rstrip()
55
- return str(result)
56
-
57
- if fmt == "yml_h":
58
- if isinstance(result, (dict, list)):
59
- return yaml.dump(result, default_flow_style=False, allow_unicode=True, sort_keys=False).rstrip()
60
- return str(result)
61
-
62
- # Unknown format, fall back to compact JSON
63
- if isinstance(result, (dict, list)):
64
- return json.dumps(result, ensure_ascii=False, separators=(",", ":"))
65
- return str(result)
1
+ """Result serialization utilities for MCP responses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, Literal
7
+
8
+ import yaml
9
+
10
+ __all__ = ["FormatMode", "serialize_result"]
11
+
12
+ FormatMode = Literal["json", "json_h", "yml", "yml_h", "raw"]
13
+
14
+
15
+ def serialize_result(result: Any, fmt: FormatMode = "json") -> str:
16
+ """Serialize tool result to string for MCP response.
17
+
18
+ Tools return native Python types (dict, list, str). This function
19
+ serializes them to a string suitable for MCP text content.
20
+
21
+ Format modes:
22
+ - json: Compact JSON (default, no spaces)
23
+ - json_h: Human-readable JSON (2-space indent)
24
+ - yml: YAML flow style (compact)
25
+ - yml_h: YAML block style (human-readable)
26
+ - raw: str() conversion
27
+
28
+ Args:
29
+ result: Tool result (dict, list, str, or other)
30
+ fmt: Output format mode (default: "json")
31
+
32
+ Returns:
33
+ String representation suitable for MCP response
34
+ """
35
+ # Strings pass through unchanged for all formats except raw
36
+ if isinstance(result, str) and fmt != "raw":
37
+ return result
38
+
39
+ if fmt == "raw":
40
+ return str(result)
41
+
42
+ if fmt == "json":
43
+ if isinstance(result, (dict, list)):
44
+ return json.dumps(result, ensure_ascii=False, separators=(",", ":"))
45
+ return str(result)
46
+
47
+ if fmt == "json_h":
48
+ if isinstance(result, (dict, list)):
49
+ return json.dumps(result, ensure_ascii=False, indent=2)
50
+ return str(result)
51
+
52
+ if fmt == "yml":
53
+ if isinstance(result, (dict, list)):
54
+ return yaml.dump(result, default_flow_style=True, allow_unicode=True, sort_keys=False).rstrip()
55
+ return str(result)
56
+
57
+ if fmt == "yml_h":
58
+ if isinstance(result, (dict, list)):
59
+ return yaml.dump(result, default_flow_style=False, allow_unicode=True, sort_keys=False).rstrip()
60
+ return str(result)
61
+
62
+ # Unknown format, fall back to compact JSON
63
+ if isinstance(result, (dict, list)):
64
+ return json.dumps(result, ensure_ascii=False, separators=(",", ":"))
65
+ return str(result)
ot/utils/http.py CHANGED
@@ -1,202 +1,202 @@
1
- """HTTP request utilities for OneTool.
2
-
3
- Provides standardized HTTP error handling and header construction
4
- for API-based tools.
5
-
6
- Example:
7
- from ot.utils import safe_request, api_headers
8
-
9
- # Build API headers with authentication
10
- headers = api_headers("MY_API_KEY", header_name="Authorization", prefix="Bearer")
11
-
12
- # Make a request with standardized error handling
13
- success, result = safe_request(
14
- lambda: client.get("/endpoint", headers=headers)
15
- )
16
- if success:
17
- data = result # Parsed JSON dict
18
- else:
19
- error_msg = result # Error message string
20
- """
21
-
22
- from __future__ import annotations
23
-
24
- from typing import TYPE_CHECKING, Any, TypeVar
25
-
26
- from ot.config.secrets import get_secret
27
-
28
- if TYPE_CHECKING:
29
- from collections.abc import Callable
30
-
31
- __all__ = ["api_headers", "check_api_key", "safe_request"]
32
-
33
- T = TypeVar("T")
34
-
35
-
36
- def api_headers(
37
- secret_name: str,
38
- *,
39
- header_name: str = "Authorization",
40
- prefix: str = "Bearer",
41
- extra: dict[str, str] | None = None,
42
- ) -> dict[str, str]:
43
- """Build API request headers with authentication.
44
-
45
- Retrieves a secret and constructs the appropriate authorization header.
46
- Commonly used patterns:
47
- - Bearer token: prefix="Bearer" (default)
48
- - API key header: header_name="X-API-Key", prefix=""
49
-
50
- Args:
51
- secret_name: Name of secret in secrets.yaml (e.g., "MY_API_KEY")
52
- header_name: Header name for the auth token (default: "Authorization")
53
- prefix: Prefix before the token value (default: "Bearer")
54
- extra: Additional headers to include
55
-
56
- Returns:
57
- Dict of headers ready for HTTP requests
58
-
59
- Raises:
60
- ValueError: If the secret is not configured
61
-
62
- Example:
63
- # Bearer token auth
64
- headers = api_headers("OPENAI_API_KEY")
65
- # {"Authorization": "Bearer sk-..."}
66
-
67
- # Custom API key header
68
- headers = api_headers(
69
- "BRAVE_API_KEY", header_name="X-Subscription-Token", prefix=""
70
- )
71
- # {"X-Subscription-Token": "BSA..."}
72
-
73
- # With extra headers
74
- headers = api_headers("API_KEY", extra={"Accept": "application/json"})
75
- """
76
- api_key = get_secret(secret_name)
77
- if not api_key:
78
- raise ValueError(f"{secret_name} secret not configured in secrets.yaml")
79
-
80
- headers: dict[str, str] = {}
81
-
82
- # Build auth header
83
- if prefix:
84
- headers[header_name] = f"{prefix} {api_key}"
85
- else:
86
- headers[header_name] = api_key
87
-
88
- # Add extra headers
89
- if extra:
90
- headers.update(extra)
91
-
92
- return headers
93
-
94
-
95
- def safe_request(
96
- request_fn: Callable[[], T],
97
- *,
98
- parse_json: bool = True,
99
- ) -> tuple[bool, T | dict[str, Any] | str]:
100
- """Execute an HTTP request with standardized error handling.
101
-
102
- Wraps an HTTP request call and handles common error patterns:
103
- - Connection errors
104
- - HTTP status errors
105
- - JSON parsing errors
106
- - Timeout errors
107
-
108
- Args:
109
- request_fn: Callable that makes the HTTP request and returns response
110
- parse_json: If True (default), parse response as JSON
111
-
112
- Returns:
113
- Tuple of (success: bool, result).
114
- On success: (True, parsed_json_dict or response)
115
- On failure: (False, error_message_string)
116
-
117
- Example:
118
- # With httpx client
119
- success, result = safe_request(
120
- lambda: client.get("/api/data", params={"q": "test"})
121
- )
122
-
123
- if success:
124
- data = result["data"]
125
- else:
126
- return f"Error: {result}"
127
-
128
- # Without JSON parsing
129
- success, result = safe_request(
130
- lambda: client.get("/raw/text"),
131
- parse_json=False,
132
- )
133
- """
134
- try:
135
- response = request_fn()
136
-
137
- # Check for raise_for_status method (httpx/requests response)
138
- if hasattr(response, "raise_for_status"):
139
- response.raise_for_status()
140
-
141
- if parse_json and hasattr(response, "json"):
142
- return True, response.json()
143
-
144
- if hasattr(response, "text"):
145
- return True, response.text
146
-
147
- return True, response
148
-
149
- except Exception as e:
150
- return False, _format_http_error(e)
151
-
152
-
153
- def _format_http_error(error: Exception) -> str:
154
- """Format an HTTP error into a user-friendly message.
155
-
156
- Extracts status code and response text when available.
157
-
158
- Args:
159
- error: The exception from the HTTP request
160
-
161
- Returns:
162
- Formatted error message string
163
- """
164
- error_type = type(error).__name__
165
-
166
- # Check for response attribute (HTTPStatusError, etc.)
167
- if hasattr(error, "response"):
168
- response = error.response
169
- status = getattr(response, "status_code", "unknown")
170
- text = getattr(response, "text", "")[:200]
171
- return f"HTTP error ({status}): {text}" if text else f"HTTP error ({status})"
172
-
173
- # Check for common error types
174
- error_str = str(error)
175
- if "timeout" in error_str.lower():
176
- return f"Request timeout: {error}"
177
- if "connection" in error_str.lower():
178
- return f"Connection error: {error}"
179
-
180
- return f"Request failed ({error_type}): {error}"
181
-
182
-
183
- def check_api_key(secret_name: str) -> str | None:
184
- """Check if an API key is configured and return error message if not.
185
-
186
- Convenience function for early validation in tools.
187
-
188
- Args:
189
- secret_name: Name of secret to check
190
-
191
- Returns:
192
- None if configured, error message string if not
193
-
194
- Example:
195
- if error := check_api_key("BRAVE_API_KEY"):
196
- return error
197
- # Proceed with API calls
198
- """
199
- api_key = get_secret(secret_name)
200
- if not api_key:
201
- return f"Error: {secret_name} secret not configured"
202
- return None
1
+ """HTTP request utilities for OneTool.
2
+
3
+ Provides standardized HTTP error handling and header construction
4
+ for API-based tools.
5
+
6
+ Example:
7
+ from ot.utils import safe_request, api_headers
8
+
9
+ # Build API headers with authentication
10
+ headers = api_headers("MY_API_KEY", header_name="Authorization", prefix="Bearer")
11
+
12
+ # Make a request with standardized error handling
13
+ success, result = safe_request(
14
+ lambda: client.get("/endpoint", headers=headers)
15
+ )
16
+ if success:
17
+ data = result # Parsed JSON dict
18
+ else:
19
+ error_msg = result # Error message string
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import TYPE_CHECKING, Any, TypeVar
25
+
26
+ from ot.config.secrets import get_secret
27
+
28
+ if TYPE_CHECKING:
29
+ from collections.abc import Callable
30
+
31
+ __all__ = ["api_headers", "check_api_key", "safe_request"]
32
+
33
+ T = TypeVar("T")
34
+
35
+
36
+ def api_headers(
37
+ secret_name: str,
38
+ *,
39
+ header_name: str = "Authorization",
40
+ prefix: str = "Bearer",
41
+ extra: dict[str, str] | None = None,
42
+ ) -> dict[str, str]:
43
+ """Build API request headers with authentication.
44
+
45
+ Retrieves a secret and constructs the appropriate authorization header.
46
+ Commonly used patterns:
47
+ - Bearer token: prefix="Bearer" (default)
48
+ - API key header: header_name="X-API-Key", prefix=""
49
+
50
+ Args:
51
+ secret_name: Name of secret in secrets.yaml (e.g., "MY_API_KEY")
52
+ header_name: Header name for the auth token (default: "Authorization")
53
+ prefix: Prefix before the token value (default: "Bearer")
54
+ extra: Additional headers to include
55
+
56
+ Returns:
57
+ Dict of headers ready for HTTP requests
58
+
59
+ Raises:
60
+ ValueError: If the secret is not configured
61
+
62
+ Example:
63
+ # Bearer token auth
64
+ headers = api_headers("OPENAI_API_KEY")
65
+ # {"Authorization": "Bearer sk-..."}
66
+
67
+ # Custom API key header
68
+ headers = api_headers(
69
+ "BRAVE_API_KEY", header_name="X-Subscription-Token", prefix=""
70
+ )
71
+ # {"X-Subscription-Token": "BSA..."}
72
+
73
+ # With extra headers
74
+ headers = api_headers("API_KEY", extra={"Accept": "application/json"})
75
+ """
76
+ api_key = get_secret(secret_name)
77
+ if not api_key:
78
+ raise ValueError(f"{secret_name} secret not configured in secrets.yaml")
79
+
80
+ headers: dict[str, str] = {}
81
+
82
+ # Build auth header
83
+ if prefix:
84
+ headers[header_name] = f"{prefix} {api_key}"
85
+ else:
86
+ headers[header_name] = api_key
87
+
88
+ # Add extra headers
89
+ if extra:
90
+ headers.update(extra)
91
+
92
+ return headers
93
+
94
+
95
+ def safe_request(
96
+ request_fn: Callable[[], T],
97
+ *,
98
+ parse_json: bool = True,
99
+ ) -> tuple[bool, T | dict[str, Any] | str]:
100
+ """Execute an HTTP request with standardized error handling.
101
+
102
+ Wraps an HTTP request call and handles common error patterns:
103
+ - Connection errors
104
+ - HTTP status errors
105
+ - JSON parsing errors
106
+ - Timeout errors
107
+
108
+ Args:
109
+ request_fn: Callable that makes the HTTP request and returns response
110
+ parse_json: If True (default), parse response as JSON
111
+
112
+ Returns:
113
+ Tuple of (success: bool, result).
114
+ On success: (True, parsed_json_dict or response)
115
+ On failure: (False, error_message_string)
116
+
117
+ Example:
118
+ # With httpx client
119
+ success, result = safe_request(
120
+ lambda: client.get("/api/data", params={"q": "test"})
121
+ )
122
+
123
+ if success:
124
+ data = result["data"]
125
+ else:
126
+ return f"Error: {result}"
127
+
128
+ # Without JSON parsing
129
+ success, result = safe_request(
130
+ lambda: client.get("/raw/text"),
131
+ parse_json=False,
132
+ )
133
+ """
134
+ try:
135
+ response = request_fn()
136
+
137
+ # Check for raise_for_status method (httpx/requests response)
138
+ if hasattr(response, "raise_for_status"):
139
+ response.raise_for_status()
140
+
141
+ if parse_json and hasattr(response, "json"):
142
+ return True, response.json()
143
+
144
+ if hasattr(response, "text"):
145
+ return True, response.text
146
+
147
+ return True, response
148
+
149
+ except Exception as e:
150
+ return False, _format_http_error(e)
151
+
152
+
153
+ def _format_http_error(error: Exception) -> str:
154
+ """Format an HTTP error into a user-friendly message.
155
+
156
+ Extracts status code and response text when available.
157
+
158
+ Args:
159
+ error: The exception from the HTTP request
160
+
161
+ Returns:
162
+ Formatted error message string
163
+ """
164
+ error_type = type(error).__name__
165
+
166
+ # Check for response attribute (HTTPStatusError, etc.)
167
+ if hasattr(error, "response"):
168
+ response = error.response
169
+ status = getattr(response, "status_code", "unknown")
170
+ text = getattr(response, "text", "")[:200]
171
+ return f"HTTP error ({status}): {text}" if text else f"HTTP error ({status})"
172
+
173
+ # Check for common error types
174
+ error_str = str(error)
175
+ if "timeout" in error_str.lower():
176
+ return f"Request timeout: {error}"
177
+ if "connection" in error_str.lower():
178
+ return f"Connection error: {error}"
179
+
180
+ return f"Request failed ({error_type}): {error}"
181
+
182
+
183
+ def check_api_key(secret_name: str) -> str | None:
184
+ """Check if an API key is configured and return error message if not.
185
+
186
+ Convenience function for early validation in tools.
187
+
188
+ Args:
189
+ secret_name: Name of secret to check
190
+
191
+ Returns:
192
+ None if configured, error message string if not
193
+
194
+ Example:
195
+ if error := check_api_key("BRAVE_API_KEY"):
196
+ return error
197
+ # Proceed with API calls
198
+ """
199
+ api_key = get_secret(secret_name)
200
+ if not api_key:
201
+ return f"Error: {secret_name} secret not configured"
202
+ return None
ot/utils/platform.py CHANGED
@@ -1,45 +1,45 @@
1
- """Platform detection utilities for cross-platform support."""
2
-
3
- from __future__ import annotations
4
-
5
- import sys
6
-
7
- # Platform-specific install commands for external dependencies
8
- INSTALL_COMMANDS: dict[str, dict[str, str]] = {
9
- "rg": {
10
- "darwin": "brew install ripgrep",
11
- "linux": "apt install ripgrep # or: snap install ripgrep",
12
- "win32": "winget install BurntSushi.ripgrep.MSVC # or: scoop install ripgrep",
13
- },
14
- "playwright": {
15
- "darwin": "pip install playwright && playwright install",
16
- "linux": "pip install playwright && playwright install",
17
- "win32": "pip install playwright && playwright install",
18
- },
19
- }
20
-
21
-
22
- def get_install_hint(tool: str) -> str:
23
- """Get platform-appropriate install command for a tool.
24
-
25
- Args:
26
- tool: Tool name (e.g., "rg", "playwright")
27
-
28
- Returns:
29
- Install command for the current platform, or generic message if unknown.
30
-
31
- Example:
32
- >>> get_install_hint("rg") # On macOS
33
- 'brew install ripgrep'
34
- >>> get_install_hint("rg") # On Linux
35
- 'apt install ripgrep # or: snap install ripgrep'
36
- >>> get_install_hint("rg") # On Windows
37
- 'winget install BurntSushi.ripgrep.MSVC # or: scoop install ripgrep'
38
- """
39
- platform = sys.platform
40
- # Normalize Linux variants (linux2, etc.)
41
- if platform.startswith("linux"):
42
- platform = "linux"
43
-
44
- commands = INSTALL_COMMANDS.get(tool, {})
45
- return commands.get(platform, f"Install {tool} for your platform")
1
+ """Platform detection utilities for cross-platform support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ # Platform-specific install commands for external dependencies
8
+ INSTALL_COMMANDS: dict[str, dict[str, str]] = {
9
+ "rg": {
10
+ "darwin": "brew install ripgrep",
11
+ "linux": "apt install ripgrep # or: snap install ripgrep",
12
+ "win32": "winget install BurntSushi.ripgrep.MSVC # or: scoop install ripgrep",
13
+ },
14
+ "playwright": {
15
+ "darwin": "pip install playwright && playwright install",
16
+ "linux": "pip install playwright && playwright install",
17
+ "win32": "pip install playwright && playwright install",
18
+ },
19
+ }
20
+
21
+
22
+ def get_install_hint(tool: str) -> str:
23
+ """Get platform-appropriate install command for a tool.
24
+
25
+ Args:
26
+ tool: Tool name (e.g., "rg", "playwright")
27
+
28
+ Returns:
29
+ Install command for the current platform, or generic message if unknown.
30
+
31
+ Example:
32
+ >>> get_install_hint("rg") # On macOS
33
+ 'brew install ripgrep'
34
+ >>> get_install_hint("rg") # On Linux
35
+ 'apt install ripgrep # or: snap install ripgrep'
36
+ >>> get_install_hint("rg") # On Windows
37
+ 'winget install BurntSushi.ripgrep.MSVC # or: scoop install ripgrep'
38
+ """
39
+ platform = sys.platform
40
+ # Normalize Linux variants (linux2, etc.)
41
+ if platform.startswith("linux"):
42
+ platform = "linux"
43
+
44
+ commands = INSTALL_COMMANDS.get(tool, {})
45
+ return commands.get(platform, f"Install {tool} for your platform")