arch-ops-server 3.0.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.
- arch_ops_server/__init__.py +176 -0
- arch_ops_server/aur.py +1190 -0
- arch_ops_server/config.py +361 -0
- arch_ops_server/http_server.py +829 -0
- arch_ops_server/logs.py +345 -0
- arch_ops_server/mirrors.py +397 -0
- arch_ops_server/news.py +288 -0
- arch_ops_server/pacman.py +1305 -0
- arch_ops_server/py.typed +0 -0
- arch_ops_server/server.py +1869 -0
- arch_ops_server/system.py +307 -0
- arch_ops_server/utils.py +313 -0
- arch_ops_server/wiki.py +245 -0
- arch_ops_server-3.0.1.dist-info/METADATA +253 -0
- arch_ops_server-3.0.1.dist-info/RECORD +17 -0
- arch_ops_server-3.0.1.dist-info/WHEEL +4 -0
- arch_ops_server-3.0.1.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-only OR MIT
|
|
2
|
+
"""
|
|
3
|
+
System diagnostics and information module.
|
|
4
|
+
Provides system health checks, disk space monitoring, and service status.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, Any, List
|
|
12
|
+
|
|
13
|
+
from .utils import (
|
|
14
|
+
IS_ARCH,
|
|
15
|
+
run_command,
|
|
16
|
+
create_error_response,
|
|
17
|
+
check_command_exists
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def get_system_info() -> Dict[str, Any]:
|
|
24
|
+
"""
|
|
25
|
+
Get core system information.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Dict with kernel, architecture, hostname, uptime, memory info
|
|
29
|
+
"""
|
|
30
|
+
logger.info("Gathering system information")
|
|
31
|
+
|
|
32
|
+
info = {}
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
# Kernel version
|
|
36
|
+
exit_code, stdout, _ = await run_command(["uname", "-r"], timeout=5, check=False)
|
|
37
|
+
if exit_code == 0:
|
|
38
|
+
info["kernel"] = stdout.strip()
|
|
39
|
+
|
|
40
|
+
# Architecture
|
|
41
|
+
exit_code, stdout, _ = await run_command(["uname", "-m"], timeout=5, check=False)
|
|
42
|
+
if exit_code == 0:
|
|
43
|
+
info["architecture"] = stdout.strip()
|
|
44
|
+
|
|
45
|
+
# Hostname
|
|
46
|
+
exit_code, stdout, _ = await run_command(["hostname"], timeout=5, check=False)
|
|
47
|
+
if exit_code == 0:
|
|
48
|
+
info["hostname"] = stdout.strip()
|
|
49
|
+
|
|
50
|
+
# Uptime
|
|
51
|
+
exit_code, stdout, _ = await run_command(["uptime", "-p"], timeout=5, check=False)
|
|
52
|
+
if exit_code == 0:
|
|
53
|
+
info["uptime"] = stdout.strip()
|
|
54
|
+
|
|
55
|
+
# Memory info from /proc/meminfo
|
|
56
|
+
try:
|
|
57
|
+
meminfo_path = Path("/proc/meminfo")
|
|
58
|
+
if meminfo_path.exists():
|
|
59
|
+
with open(meminfo_path, "r") as f:
|
|
60
|
+
meminfo = f.read()
|
|
61
|
+
|
|
62
|
+
# Parse memory values
|
|
63
|
+
mem_total_match = re.search(r"MemTotal:\s+(\d+)", meminfo)
|
|
64
|
+
mem_available_match = re.search(r"MemAvailable:\s+(\d+)", meminfo)
|
|
65
|
+
|
|
66
|
+
if mem_total_match:
|
|
67
|
+
info["memory_total_kb"] = int(mem_total_match.group(1))
|
|
68
|
+
info["memory_total_mb"] = int(mem_total_match.group(1)) // 1024
|
|
69
|
+
|
|
70
|
+
if mem_available_match:
|
|
71
|
+
info["memory_available_kb"] = int(mem_available_match.group(1))
|
|
72
|
+
info["memory_available_mb"] = int(mem_available_match.group(1)) // 1024
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.warning(f"Failed to read memory info: {e}")
|
|
75
|
+
|
|
76
|
+
info["is_arch_linux"] = IS_ARCH
|
|
77
|
+
|
|
78
|
+
logger.info("Successfully gathered system information")
|
|
79
|
+
return info
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error(f"Failed to gather system info: {e}")
|
|
83
|
+
return create_error_response(
|
|
84
|
+
"SystemInfoError",
|
|
85
|
+
f"Failed to gather system information: {str(e)}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def check_disk_space() -> Dict[str, Any]:
|
|
90
|
+
"""
|
|
91
|
+
Check disk space for critical paths.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Dict with disk usage for /, /home, /var, /var/cache/pacman/pkg
|
|
95
|
+
"""
|
|
96
|
+
logger.info("Checking disk space")
|
|
97
|
+
|
|
98
|
+
paths_to_check = ["/", "/home", "/var"]
|
|
99
|
+
|
|
100
|
+
if IS_ARCH:
|
|
101
|
+
paths_to_check.append("/var/cache/pacman/pkg")
|
|
102
|
+
|
|
103
|
+
disk_info = {}
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
for path in paths_to_check:
|
|
107
|
+
if not Path(path).exists():
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
exit_code, stdout, _ = await run_command(
|
|
111
|
+
["df", "-h", path],
|
|
112
|
+
timeout=5,
|
|
113
|
+
check=False
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if exit_code == 0:
|
|
117
|
+
lines = stdout.strip().split('\n')
|
|
118
|
+
if len(lines) >= 2:
|
|
119
|
+
# Parse df output
|
|
120
|
+
parts = lines[1].split()
|
|
121
|
+
if len(parts) >= 5:
|
|
122
|
+
disk_info[path] = {
|
|
123
|
+
"size": parts[1],
|
|
124
|
+
"used": parts[2],
|
|
125
|
+
"available": parts[3],
|
|
126
|
+
"use_percent": parts[4],
|
|
127
|
+
"mounted_on": parts[5] if len(parts) > 5 else path
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Check if space is critically low
|
|
131
|
+
use_pct = int(parts[4].rstrip('%'))
|
|
132
|
+
if use_pct > 90:
|
|
133
|
+
disk_info[path]["warning"] = "Critical: Less than 10% free"
|
|
134
|
+
elif use_pct > 80:
|
|
135
|
+
disk_info[path]["warning"] = "Low: Less than 20% free"
|
|
136
|
+
|
|
137
|
+
logger.info(f"Checked disk space for {len(disk_info)} paths")
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
"disk_usage": disk_info,
|
|
141
|
+
"paths_checked": len(disk_info)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.error(f"Failed to check disk space: {e}")
|
|
146
|
+
return create_error_response(
|
|
147
|
+
"DiskCheckError",
|
|
148
|
+
f"Failed to check disk space: {str(e)}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def get_pacman_cache_stats() -> Dict[str, Any]:
|
|
153
|
+
"""
|
|
154
|
+
Analyze pacman package cache.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Dict with cache size, package count, statistics
|
|
158
|
+
"""
|
|
159
|
+
if not IS_ARCH:
|
|
160
|
+
return create_error_response(
|
|
161
|
+
"NotSupported",
|
|
162
|
+
"Pacman cache analysis is only available on Arch Linux"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
logger.info("Analyzing pacman cache")
|
|
166
|
+
|
|
167
|
+
cache_dir = Path("/var/cache/pacman/pkg")
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
if not cache_dir.exists():
|
|
171
|
+
return create_error_response(
|
|
172
|
+
"NotFound",
|
|
173
|
+
"Pacman cache directory not found"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Count packages
|
|
177
|
+
pkg_files = list(cache_dir.glob("*.pkg.tar.*"))
|
|
178
|
+
pkg_count = len(pkg_files)
|
|
179
|
+
|
|
180
|
+
# Calculate total size
|
|
181
|
+
total_size = sum(f.stat().st_size for f in pkg_files)
|
|
182
|
+
total_size_mb = total_size / (1024 * 1024)
|
|
183
|
+
total_size_gb = total_size_mb / 1024
|
|
184
|
+
|
|
185
|
+
logger.info(f"Cache: {pkg_count} packages, {total_size_gb:.2f} GB")
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
"cache_dir": str(cache_dir),
|
|
189
|
+
"package_count": pkg_count,
|
|
190
|
+
"total_size_bytes": total_size,
|
|
191
|
+
"total_size_mb": round(total_size_mb, 2),
|
|
192
|
+
"total_size_gb": round(total_size_gb, 2)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.error(f"Failed to analyze cache: {e}")
|
|
197
|
+
return create_error_response(
|
|
198
|
+
"CacheAnalysisError",
|
|
199
|
+
f"Failed to analyze pacman cache: {str(e)}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def check_failed_services() -> Dict[str, Any]:
|
|
204
|
+
"""
|
|
205
|
+
Detect failed systemd services.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Dict with list of failed services
|
|
209
|
+
"""
|
|
210
|
+
if not check_command_exists("systemctl"):
|
|
211
|
+
return create_error_response(
|
|
212
|
+
"NotSupported",
|
|
213
|
+
"systemctl not available (systemd-based system required)"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
logger.info("Checking for failed services")
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
exit_code, stdout, _ = await run_command(
|
|
220
|
+
["systemctl", "--failed", "--no-pager"],
|
|
221
|
+
timeout=10,
|
|
222
|
+
check=False
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Parse output
|
|
226
|
+
failed_services = []
|
|
227
|
+
lines = stdout.strip().split('\n')
|
|
228
|
+
|
|
229
|
+
for line in lines:
|
|
230
|
+
# Skip header and footer lines
|
|
231
|
+
if line.startswith('●') or line.startswith('UNIT'):
|
|
232
|
+
continue
|
|
233
|
+
if 'loaded units listed' in line.lower():
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
# Parse service line
|
|
237
|
+
parts = line.split()
|
|
238
|
+
if parts and parts[0].endswith('.service'):
|
|
239
|
+
failed_services.append({
|
|
240
|
+
"unit": parts[0],
|
|
241
|
+
"load": parts[1] if len(parts) > 1 else "",
|
|
242
|
+
"active": parts[2] if len(parts) > 2 else "",
|
|
243
|
+
"sub": parts[3] if len(parts) > 3 else "",
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
logger.info(f"Found {len(failed_services)} failed services")
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
"failed_count": len(failed_services),
|
|
250
|
+
"failed_services": failed_services,
|
|
251
|
+
"all_ok": len(failed_services) == 0
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.error(f"Failed to check services: {e}")
|
|
256
|
+
return create_error_response(
|
|
257
|
+
"ServiceCheckError",
|
|
258
|
+
f"Failed to check failed services: {str(e)}"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def get_boot_logs(lines: int = 100) -> Dict[str, Any]:
|
|
263
|
+
"""
|
|
264
|
+
Retrieve recent boot logs.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
lines: Number of lines to retrieve
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Dict with boot log contents
|
|
271
|
+
"""
|
|
272
|
+
if not check_command_exists("journalctl"):
|
|
273
|
+
return create_error_response(
|
|
274
|
+
"NotSupported",
|
|
275
|
+
"journalctl not available (systemd-based system required)"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
logger.info(f"Retrieving {lines} lines of boot logs")
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
exit_code, stdout, stderr = await run_command(
|
|
282
|
+
["journalctl", "-b", "-n", str(lines), "--no-pager"],
|
|
283
|
+
timeout=15,
|
|
284
|
+
check=False
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if exit_code != 0:
|
|
288
|
+
return create_error_response(
|
|
289
|
+
"CommandError",
|
|
290
|
+
f"Failed to retrieve boot logs: {stderr}"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
log_lines = stdout.strip().split('\n')
|
|
294
|
+
|
|
295
|
+
logger.info(f"Retrieved {len(log_lines)} lines of boot logs")
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
"line_count": len(log_lines),
|
|
299
|
+
"logs": log_lines
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
except Exception as e:
|
|
303
|
+
logger.error(f"Failed to get boot logs: {e}")
|
|
304
|
+
return create_error_response(
|
|
305
|
+
"LogRetrievalError",
|
|
306
|
+
f"Failed to retrieve boot logs: {str(e)}"
|
|
307
|
+
)
|
arch_ops_server/utils.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-only OR MIT
|
|
2
|
+
"""
|
|
3
|
+
Utility functions for Arch Linux MCP Server.
|
|
4
|
+
Provides platform detection, subprocess execution, and error handling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import platform
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional, Dict, Any
|
|
13
|
+
|
|
14
|
+
# Configure logging to stderr (STDIO server requirement)
|
|
15
|
+
logging.basicConfig(
|
|
16
|
+
level=logging.INFO,
|
|
17
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
18
|
+
handlers=[logging.StreamHandler()]
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_arch_linux() -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Detect if the current system is Arch Linux.
|
|
27
|
+
|
|
28
|
+
Checks for:
|
|
29
|
+
1. /etc/arch-release file existence
|
|
30
|
+
2. Platform identification
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
bool: True if running on Arch Linux, False otherwise
|
|
34
|
+
"""
|
|
35
|
+
# Check for Arch release file
|
|
36
|
+
if Path("/etc/arch-release").exists():
|
|
37
|
+
logger.info("Detected Arch Linux via /etc/arch-release")
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
# Fallback check via platform info
|
|
41
|
+
try:
|
|
42
|
+
with open("/etc/os-release", "r") as f:
|
|
43
|
+
content = f.read()
|
|
44
|
+
if "Arch Linux" in content or "ID=arch" in content:
|
|
45
|
+
logger.info("Detected Arch Linux via /etc/os-release")
|
|
46
|
+
return True
|
|
47
|
+
except FileNotFoundError:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
logger.info("Not running on Arch Linux")
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Cache the result since it won't change during runtime
|
|
55
|
+
IS_ARCH = is_arch_linux()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def run_command(
|
|
59
|
+
cmd: list[str],
|
|
60
|
+
timeout: int = 10,
|
|
61
|
+
check: bool = True,
|
|
62
|
+
skip_sudo_check: bool = False
|
|
63
|
+
) -> tuple[int, str, str]:
|
|
64
|
+
"""
|
|
65
|
+
Execute a command asynchronously with timeout protection.
|
|
66
|
+
|
|
67
|
+
Note: For sudo commands, stdin is properly connected to allow password input
|
|
68
|
+
if passwordless sudo is not configured.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
cmd: Command and arguments as list
|
|
72
|
+
timeout: Timeout in seconds (default: 10)
|
|
73
|
+
check: If True, raise exception on non-zero exit code
|
|
74
|
+
skip_sudo_check: If True, skip the early sudo password check (for testing)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Tuple of (exit_code, stdout, stderr)
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
asyncio.TimeoutError: If command exceeds timeout
|
|
81
|
+
RuntimeError: If check=True and command fails
|
|
82
|
+
"""
|
|
83
|
+
logger.debug(f"Executing command: {' '.join(cmd)}")
|
|
84
|
+
|
|
85
|
+
# Check if this is a sudo command and if password is cached
|
|
86
|
+
is_sudo_command = cmd and cmd[0] == "sudo"
|
|
87
|
+
if is_sudo_command and not skip_sudo_check:
|
|
88
|
+
# Test if sudo password is cached (non-interactive mode)
|
|
89
|
+
test_cmd = ["sudo", "-n", "true"]
|
|
90
|
+
try:
|
|
91
|
+
test_process = await asyncio.create_subprocess_exec(
|
|
92
|
+
*test_cmd,
|
|
93
|
+
stdout=asyncio.subprocess.PIPE,
|
|
94
|
+
stderr=asyncio.subprocess.PIPE
|
|
95
|
+
)
|
|
96
|
+
await test_process.communicate()
|
|
97
|
+
password_cached = test_process.returncode == 0
|
|
98
|
+
logger.debug(f"Sudo password cached: {password_cached}")
|
|
99
|
+
|
|
100
|
+
if not password_cached:
|
|
101
|
+
logger.warning("Sudo password is required but not cached. "
|
|
102
|
+
"Please run 'sudo pacman -S <package>' manually in the terminal.")
|
|
103
|
+
return (
|
|
104
|
+
1,
|
|
105
|
+
"",
|
|
106
|
+
"Sudo password required. Please configure passwordless sudo for pacman/paru, "
|
|
107
|
+
"or run the installation command manually in your terminal."
|
|
108
|
+
)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.warning(f"Could not check sudo status: {e}")
|
|
111
|
+
password_cached = False
|
|
112
|
+
else:
|
|
113
|
+
password_cached = True
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
# Attach stdin to subprocess for commands that might need input
|
|
117
|
+
# Use asyncio.subprocess.PIPE to allow stdin interaction
|
|
118
|
+
process = await asyncio.create_subprocess_exec(
|
|
119
|
+
*cmd,
|
|
120
|
+
stdout=asyncio.subprocess.PIPE,
|
|
121
|
+
stderr=asyncio.subprocess.PIPE,
|
|
122
|
+
stdin=asyncio.subprocess.PIPE if is_sudo_command else None
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Communicate with the process
|
|
126
|
+
# For sudo commands, this allows password input if needed
|
|
127
|
+
stdout, stderr = await asyncio.wait_for(
|
|
128
|
+
process.communicate(),
|
|
129
|
+
timeout=timeout
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
exit_code = process.returncode
|
|
133
|
+
stdout_str = stdout.decode('utf-8', errors='replace') if stdout else ""
|
|
134
|
+
stderr_str = stderr.decode('utf-8', errors='replace') if stderr else ""
|
|
135
|
+
|
|
136
|
+
logger.debug(f"Command exit code: {exit_code}")
|
|
137
|
+
|
|
138
|
+
if check and exit_code != 0:
|
|
139
|
+
raise RuntimeError(
|
|
140
|
+
f"Command failed with exit code {exit_code}: {stderr_str}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return exit_code, stdout_str, stderr_str
|
|
144
|
+
|
|
145
|
+
except asyncio.TimeoutError:
|
|
146
|
+
logger.error(f"Command timed out after {timeout}s: {' '.join(cmd)}")
|
|
147
|
+
raise
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.error(f"Command execution failed: {e}")
|
|
150
|
+
raise
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def add_aur_warning(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
154
|
+
"""
|
|
155
|
+
Wrap AUR data with prominent safety warning.
|
|
156
|
+
|
|
157
|
+
The AUR contains user-produced content that may be outdated,
|
|
158
|
+
broken, or malicious. Always inspect PKGBUILDs before installation.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
data: Original AUR response data
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Dict with added warning metadata
|
|
165
|
+
"""
|
|
166
|
+
return {
|
|
167
|
+
"warning": (
|
|
168
|
+
"⚠️ AUR PACKAGE WARNING ⚠️\n"
|
|
169
|
+
"AUR packages are USER-PRODUCED content and are not officially supported.\n"
|
|
170
|
+
"These packages may be outdated, broken, or even malicious.\n"
|
|
171
|
+
"ALWAYS review the PKGBUILD and other files before installing.\n"
|
|
172
|
+
"Use at your own risk."
|
|
173
|
+
),
|
|
174
|
+
"data": data
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def create_error_response(
|
|
179
|
+
error_type: str,
|
|
180
|
+
message: str,
|
|
181
|
+
details: Optional[str] = None,
|
|
182
|
+
suggest_wiki_search: bool = True
|
|
183
|
+
) -> Dict[str, Any]:
|
|
184
|
+
"""
|
|
185
|
+
Create a structured error response with Wiki suggestions.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
error_type: Type of error (e.g., "NetworkError", "NotFound")
|
|
189
|
+
message: Human-readable error message
|
|
190
|
+
details: Optional additional details
|
|
191
|
+
suggest_wiki_search: Whether to suggest related Wiki searches (default: True)
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Structured error dict with Wiki suggestions
|
|
195
|
+
"""
|
|
196
|
+
response = {
|
|
197
|
+
"error": True,
|
|
198
|
+
"type": error_type,
|
|
199
|
+
"message": message
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if details:
|
|
203
|
+
response["details"] = details
|
|
204
|
+
|
|
205
|
+
# Add Wiki suggestions for common error types
|
|
206
|
+
if suggest_wiki_search:
|
|
207
|
+
wiki_suggestions = _get_wiki_suggestions_for_error(error_type, message)
|
|
208
|
+
if wiki_suggestions:
|
|
209
|
+
response["wiki_suggestions"] = wiki_suggestions
|
|
210
|
+
response["help_text"] = (
|
|
211
|
+
"💡 Search the Arch Wiki for these topics to find solutions. "
|
|
212
|
+
"Use the search_archwiki tool with these keywords."
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
logger.error(f"{error_type}: {message}")
|
|
216
|
+
|
|
217
|
+
return response
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _get_wiki_suggestions_for_error(error_type: str, message: str) -> list[str]:
|
|
221
|
+
"""
|
|
222
|
+
Generate relevant Arch Wiki search suggestions based on error type.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
error_type: Type of error
|
|
226
|
+
message: Error message
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
List of suggested Wiki search terms
|
|
230
|
+
"""
|
|
231
|
+
suggestions = []
|
|
232
|
+
message_lower = message.lower()
|
|
233
|
+
|
|
234
|
+
# Map error types to Wiki topics
|
|
235
|
+
error_wiki_map = {
|
|
236
|
+
"NotFound": ["Package management", "AUR"],
|
|
237
|
+
"TimeoutError": ["Network configuration", "Mirrors"],
|
|
238
|
+
"HTTPError": ["Network configuration", "Proxy"],
|
|
239
|
+
"CommandNotFound": ["Pacman", "System maintenance"],
|
|
240
|
+
"NotSupported": ["Installation guide", "System requirements"],
|
|
241
|
+
"RateLimitError": ["AUR", "Mirror"],
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# Add general suggestions based on error type
|
|
245
|
+
if error_type in error_wiki_map:
|
|
246
|
+
suggestions.extend(error_wiki_map[error_type])
|
|
247
|
+
|
|
248
|
+
# Add context-specific suggestions based on message keywords
|
|
249
|
+
keyword_map = {
|
|
250
|
+
"pacman": ["Pacman", "Pacman/Rosetta"],
|
|
251
|
+
"package": ["Package management", "Official repositories"],
|
|
252
|
+
"dependency": ["Dependency", "Package management"],
|
|
253
|
+
"mirror": ["Mirrors", "Reflector"],
|
|
254
|
+
"network": ["Network configuration", "Systemd-networkd"],
|
|
255
|
+
"update": ["System maintenance", "Pacman#Upgrading packages"],
|
|
256
|
+
"gpg": ["Pacman/Package signing", "GnuPG"],
|
|
257
|
+
"disk": ["File systems", "Partitioning"],
|
|
258
|
+
"boot": ["Boot process", "Arch boot process"],
|
|
259
|
+
"kernel": ["Kernel", "Kernel modules"],
|
|
260
|
+
"driver": ["Kernel modules", "Xorg"],
|
|
261
|
+
"graphics": ["Xorg", "NVIDIA", "AMD"],
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for keyword, topics in keyword_map.items():
|
|
265
|
+
if keyword in message_lower:
|
|
266
|
+
suggestions.extend(topics)
|
|
267
|
+
|
|
268
|
+
# Remove duplicates while preserving order
|
|
269
|
+
seen = set()
|
|
270
|
+
unique_suggestions = []
|
|
271
|
+
for suggestion in suggestions:
|
|
272
|
+
if suggestion not in seen:
|
|
273
|
+
seen.add(suggestion)
|
|
274
|
+
unique_suggestions.append(suggestion)
|
|
275
|
+
|
|
276
|
+
return unique_suggestions[:5] # Limit to top 5 suggestions
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def check_command_exists(command: str) -> bool:
|
|
280
|
+
"""
|
|
281
|
+
Check if a command exists in the system PATH.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
command: Command name to check
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
bool: True if command exists, False otherwise
|
|
288
|
+
"""
|
|
289
|
+
try:
|
|
290
|
+
result = os.system(f"which {command} > /dev/null 2>&1")
|
|
291
|
+
return result == 0
|
|
292
|
+
except Exception:
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def get_aur_helper() -> Optional[str]:
|
|
297
|
+
"""
|
|
298
|
+
Detect available AUR helper with priority: paru > yay.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
str: Name of available AUR helper ('paru' or 'yay'), or None if neither exists
|
|
302
|
+
"""
|
|
303
|
+
# Check in priority order
|
|
304
|
+
if check_command_exists("paru"):
|
|
305
|
+
logger.info("Found AUR helper: paru")
|
|
306
|
+
return "paru"
|
|
307
|
+
elif check_command_exists("yay"):
|
|
308
|
+
logger.info("Found AUR helper: yay")
|
|
309
|
+
return "yay"
|
|
310
|
+
else:
|
|
311
|
+
logger.warning("No AUR helper found (paru or yay)")
|
|
312
|
+
return None
|
|
313
|
+
|