aleph-rlm 0.6.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.
- aleph/__init__.py +49 -0
- aleph/cache/__init__.py +6 -0
- aleph/cache/base.py +20 -0
- aleph/cache/memory.py +27 -0
- aleph/cli.py +1044 -0
- aleph/config.py +154 -0
- aleph/core.py +874 -0
- aleph/mcp/__init__.py +30 -0
- aleph/mcp/local_server.py +3527 -0
- aleph/mcp/server.py +20 -0
- aleph/prompts/__init__.py +5 -0
- aleph/prompts/system.py +45 -0
- aleph/providers/__init__.py +14 -0
- aleph/providers/anthropic.py +253 -0
- aleph/providers/base.py +59 -0
- aleph/providers/openai.py +224 -0
- aleph/providers/registry.py +22 -0
- aleph/repl/__init__.py +5 -0
- aleph/repl/helpers.py +1068 -0
- aleph/repl/sandbox.py +777 -0
- aleph/sub_query/__init__.py +166 -0
- aleph/sub_query/api_backend.py +166 -0
- aleph/sub_query/cli_backend.py +327 -0
- aleph/types.py +216 -0
- aleph/utils/__init__.py +6 -0
- aleph/utils/logging.py +79 -0
- aleph/utils/tokens.py +43 -0
- aleph_rlm-0.6.0.dist-info/METADATA +358 -0
- aleph_rlm-0.6.0.dist-info/RECORD +32 -0
- aleph_rlm-0.6.0.dist-info/WHEEL +4 -0
- aleph_rlm-0.6.0.dist-info/entry_points.txt +3 -0
- aleph_rlm-0.6.0.dist-info/licenses/LICENSE +21 -0
aleph/cli.py
ADDED
|
@@ -0,0 +1,1044 @@
|
|
|
1
|
+
"""CLI installer for Aleph MCP server.
|
|
2
|
+
|
|
3
|
+
Provides easy installation of Aleph into various MCP clients:
|
|
4
|
+
- Claude Desktop (macOS/Windows)
|
|
5
|
+
- Cursor (global/project)
|
|
6
|
+
- Windsurf
|
|
7
|
+
- Claude Code
|
|
8
|
+
- VSCode
|
|
9
|
+
- Codex CLI
|
|
10
|
+
- Gemini CLI
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
aleph-rlm install # Interactive mode, detects all clients
|
|
14
|
+
aleph-rlm install claude-desktop
|
|
15
|
+
aleph-rlm install cursor
|
|
16
|
+
aleph-rlm install windsurf
|
|
17
|
+
aleph-rlm install claude-code
|
|
18
|
+
aleph-rlm install codex
|
|
19
|
+
aleph-rlm install gemini
|
|
20
|
+
aleph-rlm install --all # Configure all detected clients
|
|
21
|
+
aleph-rlm uninstall <client>
|
|
22
|
+
aleph-rlm doctor # Verify installation
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import platform
|
|
30
|
+
import re
|
|
31
|
+
import shutil
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
from dataclasses import dataclass
|
|
35
|
+
from datetime import datetime
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Callable
|
|
38
|
+
|
|
39
|
+
__all__ = ["main"]
|
|
40
|
+
|
|
41
|
+
# Try to import rich for colored output, fall back to plain text
|
|
42
|
+
try:
|
|
43
|
+
from rich.console import Console
|
|
44
|
+
from rich.panel import Panel
|
|
45
|
+
from rich.table import Table
|
|
46
|
+
from rich import print as rprint
|
|
47
|
+
RICH_AVAILABLE = True
|
|
48
|
+
console = Console()
|
|
49
|
+
except ImportError:
|
|
50
|
+
RICH_AVAILABLE = False
|
|
51
|
+
console = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# =============================================================================
|
|
55
|
+
# Output helpers (with/without rich)
|
|
56
|
+
# =============================================================================
|
|
57
|
+
|
|
58
|
+
def print_success(msg: str) -> None:
|
|
59
|
+
"""Print success message in green."""
|
|
60
|
+
if RICH_AVAILABLE:
|
|
61
|
+
console.print(f"[green]{msg}[/green]")
|
|
62
|
+
else:
|
|
63
|
+
print(f"SUCCESS: {msg}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def print_error(msg: str) -> None:
|
|
67
|
+
"""Print error message in red."""
|
|
68
|
+
if RICH_AVAILABLE:
|
|
69
|
+
console.print(f"[red]{msg}[/red]")
|
|
70
|
+
else:
|
|
71
|
+
print(f"ERROR: {msg}", file=sys.stderr)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def print_warning(msg: str) -> None:
|
|
75
|
+
"""Print warning message in yellow."""
|
|
76
|
+
if RICH_AVAILABLE:
|
|
77
|
+
console.print(f"[yellow]{msg}[/yellow]")
|
|
78
|
+
else:
|
|
79
|
+
print(f"WARNING: {msg}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def print_info(msg: str) -> None:
|
|
83
|
+
"""Print info message in blue."""
|
|
84
|
+
if RICH_AVAILABLE:
|
|
85
|
+
console.print(f"[blue]{msg}[/blue]")
|
|
86
|
+
else:
|
|
87
|
+
print(msg)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def print_header(title: str) -> None:
|
|
91
|
+
"""Print a header/title."""
|
|
92
|
+
if RICH_AVAILABLE:
|
|
93
|
+
console.print(Panel(title, style="bold cyan"))
|
|
94
|
+
else:
|
|
95
|
+
print(f"\n{'=' * 50}")
|
|
96
|
+
print(f" {title}")
|
|
97
|
+
print(f"{'=' * 50}\n")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def print_table(title: str, rows: list[tuple[str, str, str]]) -> None:
|
|
101
|
+
"""Print a table with Client, Status, Path columns."""
|
|
102
|
+
if RICH_AVAILABLE:
|
|
103
|
+
table = Table(title=title)
|
|
104
|
+
table.add_column("Client", style="cyan")
|
|
105
|
+
table.add_column("Status", style="green")
|
|
106
|
+
table.add_column("Path")
|
|
107
|
+
for row in rows:
|
|
108
|
+
table.add_row(*row)
|
|
109
|
+
console.print(table)
|
|
110
|
+
else:
|
|
111
|
+
print(f"\n{title}")
|
|
112
|
+
print("-" * 70)
|
|
113
|
+
print(f"{'Client':<20} {'Status':<15} {'Path'}")
|
|
114
|
+
print("-" * 70)
|
|
115
|
+
for client, status, path in rows:
|
|
116
|
+
print(f"{client:<20} {status:<15} {path}")
|
|
117
|
+
print()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# =============================================================================
|
|
121
|
+
# Client configuration
|
|
122
|
+
# =============================================================================
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class ClientConfig:
|
|
126
|
+
"""Configuration for an MCP client."""
|
|
127
|
+
name: str
|
|
128
|
+
display_name: str
|
|
129
|
+
config_path: Callable[[], Path | None]
|
|
130
|
+
is_cli: bool = False # True for Claude Code which uses CLI commands
|
|
131
|
+
restart_instruction: str = ""
|
|
132
|
+
config_format: str = "json"
|
|
133
|
+
|
|
134
|
+
def get_path(self) -> Path | None:
|
|
135
|
+
"""Get the config path, returns None if not applicable."""
|
|
136
|
+
return self.config_path()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _get_claude_desktop_path() -> Path | None:
|
|
140
|
+
"""Get Claude Desktop config path based on platform."""
|
|
141
|
+
system = platform.system()
|
|
142
|
+
if system == "Darwin": # macOS
|
|
143
|
+
return Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
|
|
144
|
+
elif system == "Windows":
|
|
145
|
+
appdata = os.environ.get("APPDATA")
|
|
146
|
+
if appdata:
|
|
147
|
+
return Path(appdata) / "Claude" / "claude_desktop_config.json"
|
|
148
|
+
elif system == "Linux":
|
|
149
|
+
# XDG-compliant path
|
|
150
|
+
config_home = os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
|
|
151
|
+
return Path(config_home) / "Claude" / "claude_desktop_config.json"
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _get_cursor_global_path() -> Path | None:
|
|
156
|
+
"""Get Cursor global config path."""
|
|
157
|
+
return Path.home() / ".cursor" / "mcp.json"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _get_cursor_project_path() -> Path | None:
|
|
161
|
+
"""Get Cursor project-level config path (current directory)."""
|
|
162
|
+
return Path.cwd() / ".cursor" / "mcp.json"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _get_windsurf_path() -> Path | None:
|
|
166
|
+
"""Get Windsurf config path."""
|
|
167
|
+
return Path.home() / ".codeium" / "windsurf" / "mcp_config.json"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _get_vscode_path() -> Path | None:
|
|
171
|
+
"""Get VSCode project-level config path."""
|
|
172
|
+
return Path.cwd() / ".vscode" / "mcp.json"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _get_claude_code_path() -> Path | None:
|
|
176
|
+
"""Claude Code uses CLI, not a config file."""
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _get_codex_path() -> Path | None:
|
|
181
|
+
"""Get Codex CLI config path."""
|
|
182
|
+
return Path.home() / ".codex" / "config.toml"
|
|
183
|
+
|
|
184
|
+
def _get_gemini_path() -> Path | None:
|
|
185
|
+
"""Get Gemini CLI config path."""
|
|
186
|
+
return Path.home() / ".gemini" / "mcp.json"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# Define all supported clients
|
|
190
|
+
CLIENTS: dict[str, ClientConfig] = {
|
|
191
|
+
"claude-desktop": ClientConfig(
|
|
192
|
+
name="claude-desktop",
|
|
193
|
+
display_name="Claude Desktop",
|
|
194
|
+
config_path=_get_claude_desktop_path,
|
|
195
|
+
restart_instruction="Restart Claude Desktop to load Aleph",
|
|
196
|
+
),
|
|
197
|
+
"cursor": ClientConfig(
|
|
198
|
+
name="cursor",
|
|
199
|
+
display_name="Cursor (Global)",
|
|
200
|
+
config_path=_get_cursor_global_path,
|
|
201
|
+
restart_instruction="Restart Cursor to load Aleph",
|
|
202
|
+
),
|
|
203
|
+
"cursor-project": ClientConfig(
|
|
204
|
+
name="cursor-project",
|
|
205
|
+
display_name="Cursor (Project)",
|
|
206
|
+
config_path=_get_cursor_project_path,
|
|
207
|
+
restart_instruction="Restart Cursor to load Aleph",
|
|
208
|
+
),
|
|
209
|
+
"windsurf": ClientConfig(
|
|
210
|
+
name="windsurf",
|
|
211
|
+
display_name="Windsurf",
|
|
212
|
+
config_path=_get_windsurf_path,
|
|
213
|
+
restart_instruction="Restart Windsurf to load Aleph",
|
|
214
|
+
),
|
|
215
|
+
"vscode": ClientConfig(
|
|
216
|
+
name="vscode",
|
|
217
|
+
display_name="VSCode (Project)",
|
|
218
|
+
config_path=_get_vscode_path,
|
|
219
|
+
restart_instruction="Restart VSCode to load Aleph",
|
|
220
|
+
),
|
|
221
|
+
"claude-code": ClientConfig(
|
|
222
|
+
name="claude-code",
|
|
223
|
+
display_name="Claude Code",
|
|
224
|
+
config_path=_get_claude_code_path,
|
|
225
|
+
is_cli=True,
|
|
226
|
+
restart_instruction="Run 'claude' to use Aleph",
|
|
227
|
+
),
|
|
228
|
+
"codex": ClientConfig(
|
|
229
|
+
name="codex",
|
|
230
|
+
display_name="Codex CLI",
|
|
231
|
+
config_path=_get_codex_path,
|
|
232
|
+
restart_instruction="Restart Codex CLI to load Aleph",
|
|
233
|
+
config_format="toml",
|
|
234
|
+
),
|
|
235
|
+
"gemini": ClientConfig(
|
|
236
|
+
name="gemini",
|
|
237
|
+
display_name="Gemini CLI",
|
|
238
|
+
config_path=_get_gemini_path,
|
|
239
|
+
restart_instruction="Restart Gemini CLI to load Aleph",
|
|
240
|
+
),
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# The JSON configuration to inject
|
|
244
|
+
ALEPH_MCP_CONFIG = {
|
|
245
|
+
"command": "aleph",
|
|
246
|
+
"args": ["--enable-actions", "--workspace-mode", "any", "--tool-docs", "concise"],
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# =============================================================================
|
|
251
|
+
# Detection and installation logic
|
|
252
|
+
# =============================================================================
|
|
253
|
+
|
|
254
|
+
def _find_claude_cli() -> str | None:
|
|
255
|
+
"""Find the Claude Code CLI executable.
|
|
256
|
+
|
|
257
|
+
On Windows with NPM installation, the executable may be claude.cmd or claude.ps1.
|
|
258
|
+
Returns the executable name if found, None otherwise.
|
|
259
|
+
"""
|
|
260
|
+
# Try standard 'claude' first (works on macOS/Linux and some Windows setups)
|
|
261
|
+
if shutil.which("claude"):
|
|
262
|
+
return "claude"
|
|
263
|
+
|
|
264
|
+
# On Windows, NPM creates .cmd and .ps1 wrapper scripts
|
|
265
|
+
if platform.system() == "Windows":
|
|
266
|
+
for ext in (".cmd", ".ps1", ".exe"):
|
|
267
|
+
exe_name = f"claude{ext}"
|
|
268
|
+
if shutil.which(exe_name):
|
|
269
|
+
return exe_name
|
|
270
|
+
|
|
271
|
+
# Also check common npm global bin locations on Windows
|
|
272
|
+
npm_paths = []
|
|
273
|
+
appdata = os.environ.get("APPDATA")
|
|
274
|
+
if appdata:
|
|
275
|
+
npm_paths.append(Path(appdata) / "npm" / "claude.cmd")
|
|
276
|
+
npm_paths.append(Path(appdata) / "npm" / "claude.ps1")
|
|
277
|
+
|
|
278
|
+
localappdata = os.environ.get("LOCALAPPDATA")
|
|
279
|
+
if localappdata:
|
|
280
|
+
npm_paths.append(Path(localappdata) / "npm" / "claude.cmd")
|
|
281
|
+
npm_paths.append(Path(localappdata) / "npm" / "claude.ps1")
|
|
282
|
+
|
|
283
|
+
for npm_path in npm_paths:
|
|
284
|
+
if npm_path.exists():
|
|
285
|
+
return str(npm_path)
|
|
286
|
+
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def is_client_installed(client: ClientConfig) -> bool:
|
|
291
|
+
"""Check if a client appears to be installed."""
|
|
292
|
+
if client.is_cli:
|
|
293
|
+
# Check if claude CLI is available
|
|
294
|
+
return _find_claude_cli() is not None
|
|
295
|
+
|
|
296
|
+
path = client.get_path()
|
|
297
|
+
if path is None:
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
# Check if the config directory exists (client is likely installed)
|
|
301
|
+
# For Claude Desktop, check the parent directory
|
|
302
|
+
if client.name == "claude-desktop":
|
|
303
|
+
return path.parent.exists()
|
|
304
|
+
|
|
305
|
+
# For editors, we check if the global config dir exists
|
|
306
|
+
if client.name == "cursor":
|
|
307
|
+
return path.parent.exists()
|
|
308
|
+
|
|
309
|
+
if client.name == "windsurf":
|
|
310
|
+
return path.parent.exists()
|
|
311
|
+
|
|
312
|
+
if client.name == "codex":
|
|
313
|
+
return path.parent.exists()
|
|
314
|
+
|
|
315
|
+
if client.name == "gemini":
|
|
316
|
+
return path.parent.exists()
|
|
317
|
+
|
|
318
|
+
# For project-level configs, always return True (user may want to create)
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def is_aleph_configured(client: ClientConfig) -> bool:
|
|
323
|
+
"""Check if Aleph is already configured in a client."""
|
|
324
|
+
if client.is_cli:
|
|
325
|
+
# Check claude mcp list
|
|
326
|
+
claude_exe = _find_claude_cli()
|
|
327
|
+
if not claude_exe:
|
|
328
|
+
return False
|
|
329
|
+
try:
|
|
330
|
+
result = subprocess.run(
|
|
331
|
+
[claude_exe, "mcp", "list"],
|
|
332
|
+
capture_output=True,
|
|
333
|
+
text=True,
|
|
334
|
+
timeout=10,
|
|
335
|
+
shell=claude_exe.endswith((".cmd", ".ps1")), # Use shell for Windows scripts
|
|
336
|
+
)
|
|
337
|
+
return "aleph" in result.stdout.lower()
|
|
338
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
|
|
339
|
+
return False
|
|
340
|
+
|
|
341
|
+
if client.config_format == "toml":
|
|
342
|
+
return is_aleph_configured_toml(client)
|
|
343
|
+
|
|
344
|
+
path = client.get_path()
|
|
345
|
+
if path is None or not path.exists():
|
|
346
|
+
return False
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
350
|
+
config = json.load(f)
|
|
351
|
+
return "aleph" in config.get("mcpServers", {})
|
|
352
|
+
except (json.JSONDecodeError, OSError):
|
|
353
|
+
return False
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def backup_config(path: Path) -> Path | None:
|
|
357
|
+
"""Create a backup of the config file."""
|
|
358
|
+
if not path.exists():
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
362
|
+
backup_path = path.with_suffix(f".backup_{timestamp}.json")
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
shutil.copy2(path, backup_path)
|
|
366
|
+
return backup_path
|
|
367
|
+
except OSError as e:
|
|
368
|
+
print_warning(f"Could not create backup: {e}")
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def backup_config_toml(path: Path) -> Path | None:
|
|
373
|
+
"""Create a backup of a TOML config file."""
|
|
374
|
+
if not path.exists():
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
378
|
+
backup_path = path.with_suffix(f"{path.suffix}.backup_{timestamp}")
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
shutil.copy2(path, backup_path)
|
|
382
|
+
return backup_path
|
|
383
|
+
except OSError as e:
|
|
384
|
+
print_warning(f"Could not create backup: {e}")
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def validate_json(path: Path) -> bool:
|
|
389
|
+
"""Validate that a JSON file is well-formed."""
|
|
390
|
+
try:
|
|
391
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
392
|
+
json.load(f)
|
|
393
|
+
return True
|
|
394
|
+
except (json.JSONDecodeError, OSError):
|
|
395
|
+
return False
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def validate_toml(path: Path) -> bool:
|
|
399
|
+
"""Validate that a TOML file is well-formed when tomllib/tomli is available."""
|
|
400
|
+
try:
|
|
401
|
+
import tomllib # type: ignore[attr-defined]
|
|
402
|
+
except ImportError:
|
|
403
|
+
try:
|
|
404
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
405
|
+
except ImportError:
|
|
406
|
+
return True
|
|
407
|
+
try:
|
|
408
|
+
with open(path, "rb") as f:
|
|
409
|
+
tomllib.load(f)
|
|
410
|
+
return True
|
|
411
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
412
|
+
return False
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _toml_section_exists(config_text: str, section: str) -> bool:
|
|
416
|
+
pattern = rf"^\[{re.escape(section)}\]\s*$"
|
|
417
|
+
return re.search(pattern, config_text, flags=re.MULTILINE) is not None
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _remove_toml_section(config_text: str, section: str) -> str:
|
|
421
|
+
pattern = rf"(?ms)^\[{re.escape(section)}\]\n(?:.*\n)*?(?=^\[|\Z)"
|
|
422
|
+
return re.sub(pattern, "", config_text)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def is_aleph_configured_toml(client: ClientConfig) -> bool:
|
|
426
|
+
"""Check if Aleph is configured in a TOML config file (Codex)."""
|
|
427
|
+
path = client.get_path()
|
|
428
|
+
if path is None or not path.exists():
|
|
429
|
+
return False
|
|
430
|
+
try:
|
|
431
|
+
config_text = path.read_text(encoding="utf-8")
|
|
432
|
+
except OSError:
|
|
433
|
+
return False
|
|
434
|
+
return _toml_section_exists(config_text, "mcp_servers.aleph")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def install_to_toml_config(
|
|
438
|
+
client: ClientConfig,
|
|
439
|
+
dry_run: bool = False,
|
|
440
|
+
) -> bool:
|
|
441
|
+
"""Install Aleph to a TOML config file (Codex)."""
|
|
442
|
+
path = client.get_path()
|
|
443
|
+
if path is None:
|
|
444
|
+
print_error(f"Could not determine config path for {client.display_name}")
|
|
445
|
+
return False
|
|
446
|
+
|
|
447
|
+
if path.exists():
|
|
448
|
+
try:
|
|
449
|
+
config_text = path.read_text(encoding="utf-8")
|
|
450
|
+
except OSError as e:
|
|
451
|
+
print_error(f"Could not read {path}: {e}")
|
|
452
|
+
return False
|
|
453
|
+
else:
|
|
454
|
+
config_text = ""
|
|
455
|
+
|
|
456
|
+
if _toml_section_exists(config_text, "mcp_servers.aleph"):
|
|
457
|
+
print_warning(f"Aleph is already configured in {client.display_name}")
|
|
458
|
+
return True
|
|
459
|
+
|
|
460
|
+
block = (
|
|
461
|
+
"[mcp_servers.aleph]\n"
|
|
462
|
+
"command = \"aleph\"\n"
|
|
463
|
+
"args = [\"--enable-actions\", \"--workspace-mode\", \"any\", \"--tool-docs\", \"concise\"]\n"
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
if dry_run:
|
|
467
|
+
print_info(f"[DRY RUN] Would write to: {path}")
|
|
468
|
+
print_info(f"[DRY RUN] Would append:\n{block}")
|
|
469
|
+
return True
|
|
470
|
+
|
|
471
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
472
|
+
|
|
473
|
+
if path.exists():
|
|
474
|
+
backup = backup_config_toml(path)
|
|
475
|
+
if backup:
|
|
476
|
+
print_info(f"Backed up existing config to: {backup}")
|
|
477
|
+
|
|
478
|
+
new_text = config_text
|
|
479
|
+
if new_text and not new_text.endswith("\n"):
|
|
480
|
+
new_text += "\n"
|
|
481
|
+
if new_text and not new_text.endswith("\n\n"):
|
|
482
|
+
new_text += "\n"
|
|
483
|
+
new_text += block
|
|
484
|
+
|
|
485
|
+
try:
|
|
486
|
+
path.write_text(new_text, encoding="utf-8")
|
|
487
|
+
except OSError as e:
|
|
488
|
+
print_error(f"Could not write to {path}: {e}")
|
|
489
|
+
return False
|
|
490
|
+
|
|
491
|
+
if not validate_toml(path):
|
|
492
|
+
print_error(f"Written TOML may be invalid! Check {path}")
|
|
493
|
+
return False
|
|
494
|
+
|
|
495
|
+
print_success(f"Configured Aleph in {client.display_name}")
|
|
496
|
+
print_info(f"Config file: {path}")
|
|
497
|
+
if client.restart_instruction:
|
|
498
|
+
print_info(client.restart_instruction)
|
|
499
|
+
|
|
500
|
+
return True
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def uninstall_from_toml_config(
|
|
504
|
+
client: ClientConfig,
|
|
505
|
+
dry_run: bool = False,
|
|
506
|
+
) -> bool:
|
|
507
|
+
"""Remove Aleph from a TOML config file (Codex)."""
|
|
508
|
+
path = client.get_path()
|
|
509
|
+
if path is None:
|
|
510
|
+
print_error(f"Could not determine config path for {client.display_name}")
|
|
511
|
+
return False
|
|
512
|
+
|
|
513
|
+
if not path.exists():
|
|
514
|
+
print_warning(f"Config file does not exist: {path}")
|
|
515
|
+
return True
|
|
516
|
+
|
|
517
|
+
try:
|
|
518
|
+
config_text = path.read_text(encoding="utf-8")
|
|
519
|
+
except OSError as e:
|
|
520
|
+
print_error(f"Could not read {path}: {e}")
|
|
521
|
+
return False
|
|
522
|
+
|
|
523
|
+
has_main = _toml_section_exists(config_text, "mcp_servers.aleph")
|
|
524
|
+
has_env = _toml_section_exists(config_text, "mcp_servers.aleph.env")
|
|
525
|
+
if not has_main and not has_env:
|
|
526
|
+
print_warning(f"Aleph is not configured in {client.display_name}")
|
|
527
|
+
return True
|
|
528
|
+
|
|
529
|
+
if dry_run:
|
|
530
|
+
print_info(f"[DRY RUN] Would remove Aleph from: {path}")
|
|
531
|
+
return True
|
|
532
|
+
|
|
533
|
+
backup = backup_config_toml(path)
|
|
534
|
+
if backup:
|
|
535
|
+
print_info(f"Backed up existing config to: {backup}")
|
|
536
|
+
|
|
537
|
+
new_text = _remove_toml_section(config_text, "mcp_servers.aleph.env")
|
|
538
|
+
new_text = _remove_toml_section(new_text, "mcp_servers.aleph")
|
|
539
|
+
new_text = re.sub(r"\n{3,}", "\n\n", new_text).rstrip()
|
|
540
|
+
if new_text:
|
|
541
|
+
new_text += "\n"
|
|
542
|
+
|
|
543
|
+
try:
|
|
544
|
+
path.write_text(new_text, encoding="utf-8")
|
|
545
|
+
except OSError as e:
|
|
546
|
+
print_error(f"Could not write to {path}: {e}")
|
|
547
|
+
return False
|
|
548
|
+
|
|
549
|
+
print_success(f"Removed Aleph from {client.display_name}")
|
|
550
|
+
return True
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def install_to_config_file(
|
|
554
|
+
client: ClientConfig,
|
|
555
|
+
dry_run: bool = False,
|
|
556
|
+
) -> bool:
|
|
557
|
+
"""Install Aleph to a JSON config file."""
|
|
558
|
+
path = client.get_path()
|
|
559
|
+
if path is None:
|
|
560
|
+
print_error(f"Could not determine config path for {client.display_name}")
|
|
561
|
+
return False
|
|
562
|
+
|
|
563
|
+
# Load existing config or create new
|
|
564
|
+
if path.exists():
|
|
565
|
+
try:
|
|
566
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
567
|
+
config = json.load(f)
|
|
568
|
+
except json.JSONDecodeError as e:
|
|
569
|
+
print_error(f"Invalid JSON in {path}: {e}")
|
|
570
|
+
return False
|
|
571
|
+
except OSError as e:
|
|
572
|
+
print_error(f"Could not read {path}: {e}")
|
|
573
|
+
return False
|
|
574
|
+
else:
|
|
575
|
+
config = {}
|
|
576
|
+
|
|
577
|
+
# Ensure mcpServers exists
|
|
578
|
+
if "mcpServers" not in config:
|
|
579
|
+
config["mcpServers"] = {}
|
|
580
|
+
|
|
581
|
+
# Check if already configured
|
|
582
|
+
if "aleph" in config["mcpServers"]:
|
|
583
|
+
print_warning(f"Aleph is already configured in {client.display_name}")
|
|
584
|
+
return True
|
|
585
|
+
|
|
586
|
+
# Add Aleph config
|
|
587
|
+
config["mcpServers"]["aleph"] = ALEPH_MCP_CONFIG.copy()
|
|
588
|
+
|
|
589
|
+
if dry_run:
|
|
590
|
+
print_info(f"[DRY RUN] Would write to: {path}")
|
|
591
|
+
print_info(f"[DRY RUN] New config:\n{json.dumps(config, indent=2)}")
|
|
592
|
+
return True
|
|
593
|
+
|
|
594
|
+
# Create parent directory if needed
|
|
595
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
596
|
+
|
|
597
|
+
# Backup existing config
|
|
598
|
+
if path.exists():
|
|
599
|
+
backup = backup_config(path)
|
|
600
|
+
if backup:
|
|
601
|
+
print_info(f"Backed up existing config to: {backup}")
|
|
602
|
+
|
|
603
|
+
# Write new config
|
|
604
|
+
try:
|
|
605
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
606
|
+
json.dump(config, f, indent=2)
|
|
607
|
+
except OSError as e:
|
|
608
|
+
print_error(f"Could not write to {path}: {e}")
|
|
609
|
+
return False
|
|
610
|
+
|
|
611
|
+
# Validate
|
|
612
|
+
if not validate_json(path):
|
|
613
|
+
print_error(f"Written JSON is invalid! Check {path}")
|
|
614
|
+
return False
|
|
615
|
+
|
|
616
|
+
print_success(f"Configured Aleph in {client.display_name}")
|
|
617
|
+
print_info(f"Config file: {path}")
|
|
618
|
+
if client.restart_instruction:
|
|
619
|
+
print_info(client.restart_instruction)
|
|
620
|
+
|
|
621
|
+
return True
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def install_to_claude_code(dry_run: bool = False) -> bool:
|
|
625
|
+
"""Install Aleph to Claude Code using CLI."""
|
|
626
|
+
claude_exe = _find_claude_cli()
|
|
627
|
+
if not claude_exe:
|
|
628
|
+
print_error("Claude Code CLI not found. Install it first: https://claude.ai/code")
|
|
629
|
+
if platform.system() == "Windows":
|
|
630
|
+
print_info("If installed via NPM, ensure %APPDATA%\\npm is in your PATH")
|
|
631
|
+
return False
|
|
632
|
+
|
|
633
|
+
if dry_run:
|
|
634
|
+
print_info(
|
|
635
|
+
f"[DRY RUN] Would run: {claude_exe} mcp add aleph aleph -- --enable-actions --workspace-mode any --tool-docs concise"
|
|
636
|
+
)
|
|
637
|
+
return True
|
|
638
|
+
|
|
639
|
+
try:
|
|
640
|
+
result = subprocess.run(
|
|
641
|
+
[
|
|
642
|
+
claude_exe,
|
|
643
|
+
"mcp",
|
|
644
|
+
"add",
|
|
645
|
+
"aleph",
|
|
646
|
+
"aleph",
|
|
647
|
+
"--",
|
|
648
|
+
"--enable-actions",
|
|
649
|
+
"--workspace-mode",
|
|
650
|
+
"any",
|
|
651
|
+
"--tool-docs",
|
|
652
|
+
"concise",
|
|
653
|
+
],
|
|
654
|
+
capture_output=True,
|
|
655
|
+
text=True,
|
|
656
|
+
timeout=30,
|
|
657
|
+
shell=claude_exe.endswith((".cmd", ".ps1")), # Use shell for Windows scripts
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
if result.returncode == 0:
|
|
661
|
+
print_success("Configured Aleph in Claude Code")
|
|
662
|
+
print_info("Run 'claude' to use Aleph")
|
|
663
|
+
return True
|
|
664
|
+
else:
|
|
665
|
+
# Check if it's already installed
|
|
666
|
+
if "already exists" in result.stderr.lower():
|
|
667
|
+
print_warning("Aleph is already configured in Claude Code")
|
|
668
|
+
return True
|
|
669
|
+
print_error(f"Failed to add Aleph to Claude Code: {result.stderr}")
|
|
670
|
+
return False
|
|
671
|
+
except subprocess.TimeoutExpired:
|
|
672
|
+
print_error("Command timed out")
|
|
673
|
+
return False
|
|
674
|
+
except FileNotFoundError:
|
|
675
|
+
print_error("Claude Code CLI not found")
|
|
676
|
+
return False
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def uninstall_from_config_file(
|
|
680
|
+
client: ClientConfig,
|
|
681
|
+
dry_run: bool = False,
|
|
682
|
+
) -> bool:
|
|
683
|
+
"""Remove Aleph from a JSON config file."""
|
|
684
|
+
path = client.get_path()
|
|
685
|
+
if path is None:
|
|
686
|
+
print_error(f"Could not determine config path for {client.display_name}")
|
|
687
|
+
return False
|
|
688
|
+
|
|
689
|
+
if not path.exists():
|
|
690
|
+
print_warning(f"Config file does not exist: {path}")
|
|
691
|
+
return True
|
|
692
|
+
|
|
693
|
+
try:
|
|
694
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
695
|
+
config = json.load(f)
|
|
696
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
697
|
+
print_error(f"Could not read {path}: {e}")
|
|
698
|
+
return False
|
|
699
|
+
|
|
700
|
+
if "mcpServers" not in config or "aleph" not in config["mcpServers"]:
|
|
701
|
+
print_warning(f"Aleph is not configured in {client.display_name}")
|
|
702
|
+
return True
|
|
703
|
+
|
|
704
|
+
if dry_run:
|
|
705
|
+
print_info(f"[DRY RUN] Would remove 'aleph' from mcpServers in: {path}")
|
|
706
|
+
return True
|
|
707
|
+
|
|
708
|
+
# Backup before removing
|
|
709
|
+
backup = backup_config(path)
|
|
710
|
+
if backup:
|
|
711
|
+
print_info(f"Backed up existing config to: {backup}")
|
|
712
|
+
|
|
713
|
+
# Remove Aleph
|
|
714
|
+
del config["mcpServers"]["aleph"]
|
|
715
|
+
|
|
716
|
+
# Clean up empty mcpServers
|
|
717
|
+
if not config["mcpServers"]:
|
|
718
|
+
del config["mcpServers"]
|
|
719
|
+
|
|
720
|
+
try:
|
|
721
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
722
|
+
json.dump(config, f, indent=2)
|
|
723
|
+
except OSError as e:
|
|
724
|
+
print_error(f"Could not write to {path}: {e}")
|
|
725
|
+
return False
|
|
726
|
+
|
|
727
|
+
print_success(f"Removed Aleph from {client.display_name}")
|
|
728
|
+
return True
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def uninstall_from_claude_code(dry_run: bool = False) -> bool:
|
|
732
|
+
"""Remove Aleph from Claude Code using CLI."""
|
|
733
|
+
claude_exe = _find_claude_cli()
|
|
734
|
+
if not claude_exe:
|
|
735
|
+
print_error("Claude Code CLI not found")
|
|
736
|
+
return False
|
|
737
|
+
|
|
738
|
+
if dry_run:
|
|
739
|
+
print_info(f"[DRY RUN] Would run: {claude_exe} mcp remove aleph")
|
|
740
|
+
return True
|
|
741
|
+
|
|
742
|
+
try:
|
|
743
|
+
result = subprocess.run(
|
|
744
|
+
[claude_exe, "mcp", "remove", "aleph"],
|
|
745
|
+
capture_output=True,
|
|
746
|
+
text=True,
|
|
747
|
+
timeout=30,
|
|
748
|
+
shell=claude_exe.endswith((".cmd", ".ps1")), # Use shell for Windows scripts
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
if result.returncode == 0:
|
|
752
|
+
print_success("Removed Aleph from Claude Code")
|
|
753
|
+
return True
|
|
754
|
+
else:
|
|
755
|
+
if "not found" in result.stderr.lower():
|
|
756
|
+
print_warning("Aleph is not configured in Claude Code")
|
|
757
|
+
return True
|
|
758
|
+
print_error(f"Failed to remove Aleph from Claude Code: {result.stderr}")
|
|
759
|
+
return False
|
|
760
|
+
except subprocess.TimeoutExpired:
|
|
761
|
+
print_error("Command timed out")
|
|
762
|
+
return False
|
|
763
|
+
except FileNotFoundError:
|
|
764
|
+
print_error("Claude Code CLI not found")
|
|
765
|
+
return False
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def install_client(client: ClientConfig, dry_run: bool = False) -> bool:
|
|
769
|
+
"""Install Aleph to a specific client."""
|
|
770
|
+
if client.is_cli:
|
|
771
|
+
return install_to_claude_code(dry_run)
|
|
772
|
+
if client.config_format == "toml":
|
|
773
|
+
return install_to_toml_config(client, dry_run)
|
|
774
|
+
return install_to_config_file(client, dry_run)
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def uninstall_client(client: ClientConfig, dry_run: bool = False) -> bool:
|
|
778
|
+
"""Uninstall Aleph from a specific client."""
|
|
779
|
+
if client.is_cli:
|
|
780
|
+
return uninstall_from_claude_code(dry_run)
|
|
781
|
+
if client.config_format == "toml":
|
|
782
|
+
return uninstall_from_toml_config(client, dry_run)
|
|
783
|
+
return uninstall_from_config_file(client, dry_run)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
# =============================================================================
|
|
787
|
+
# Doctor command
|
|
788
|
+
# =============================================================================
|
|
789
|
+
|
|
790
|
+
def doctor() -> bool:
|
|
791
|
+
"""Verify Aleph installation and diagnose issues."""
|
|
792
|
+
print_header("Aleph Doctor")
|
|
793
|
+
|
|
794
|
+
all_ok = True
|
|
795
|
+
|
|
796
|
+
# Check if aleph is available
|
|
797
|
+
print_info("Checking aleph command...")
|
|
798
|
+
if shutil.which("aleph"):
|
|
799
|
+
print_success("aleph is in PATH")
|
|
800
|
+
else:
|
|
801
|
+
print_error("aleph not found in PATH")
|
|
802
|
+
print_info("Try reinstalling: pip install \"aleph-rlm[mcp]\"")
|
|
803
|
+
all_ok = False
|
|
804
|
+
|
|
805
|
+
# Check MCP dependency
|
|
806
|
+
print_info("\nChecking MCP dependency...")
|
|
807
|
+
try:
|
|
808
|
+
import mcp # noqa: F401
|
|
809
|
+
print_success("MCP package is installed")
|
|
810
|
+
except ImportError:
|
|
811
|
+
print_error("MCP package not installed")
|
|
812
|
+
print_info("Install with: pip install \"aleph-rlm[mcp]\"")
|
|
813
|
+
all_ok = False
|
|
814
|
+
|
|
815
|
+
# Check each client
|
|
816
|
+
print_info("\nChecking client configurations...")
|
|
817
|
+
rows = []
|
|
818
|
+
|
|
819
|
+
for name, client in CLIENTS.items():
|
|
820
|
+
if client.is_cli:
|
|
821
|
+
claude_exe = _find_claude_cli()
|
|
822
|
+
if claude_exe:
|
|
823
|
+
if is_aleph_configured(client):
|
|
824
|
+
status = "Configured"
|
|
825
|
+
else:
|
|
826
|
+
status = "Not configured"
|
|
827
|
+
path_str = f"(CLI: {claude_exe})"
|
|
828
|
+
else:
|
|
829
|
+
status = "Not installed"
|
|
830
|
+
path_str = "-"
|
|
831
|
+
else:
|
|
832
|
+
path = client.get_path()
|
|
833
|
+
if path is None:
|
|
834
|
+
status = "N/A"
|
|
835
|
+
path_str = "-"
|
|
836
|
+
elif not is_client_installed(client):
|
|
837
|
+
status = "Not installed"
|
|
838
|
+
path_str = str(path)
|
|
839
|
+
elif is_aleph_configured(client):
|
|
840
|
+
status = "Configured"
|
|
841
|
+
path_str = str(path)
|
|
842
|
+
else:
|
|
843
|
+
status = "Not configured"
|
|
844
|
+
path_str = str(path)
|
|
845
|
+
|
|
846
|
+
rows.append((client.display_name, status, path_str))
|
|
847
|
+
|
|
848
|
+
print_table("MCP Client Status", rows)
|
|
849
|
+
|
|
850
|
+
# Test MCP server startup
|
|
851
|
+
print_info("Testing MCP server startup...")
|
|
852
|
+
try:
|
|
853
|
+
from aleph.mcp.local_server import AlephMCPServerLocal # noqa: F401
|
|
854
|
+
print_success("Aleph MCP server module loads correctly")
|
|
855
|
+
except ImportError as e:
|
|
856
|
+
print_error(f"Failed to import MCP server: {e}")
|
|
857
|
+
all_ok = False
|
|
858
|
+
except RuntimeError as e:
|
|
859
|
+
if "mcp" in str(e).lower():
|
|
860
|
+
print_error(f"MCP dependency issue: {e}")
|
|
861
|
+
print_info("Install with: pip install \"aleph-rlm[mcp]\"")
|
|
862
|
+
else:
|
|
863
|
+
print_error(f"Server error: {e}")
|
|
864
|
+
all_ok = False
|
|
865
|
+
|
|
866
|
+
print()
|
|
867
|
+
if all_ok:
|
|
868
|
+
print_success("All checks passed!")
|
|
869
|
+
else:
|
|
870
|
+
print_error("Some checks failed. See above for details.")
|
|
871
|
+
|
|
872
|
+
return all_ok
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
# =============================================================================
|
|
876
|
+
# Interactive mode
|
|
877
|
+
# =============================================================================
|
|
878
|
+
|
|
879
|
+
def interactive_install(dry_run: bool = False) -> None:
|
|
880
|
+
"""Interactive installation mode."""
|
|
881
|
+
print_header("Aleph MCP Server Installer")
|
|
882
|
+
|
|
883
|
+
# Detect installed clients
|
|
884
|
+
detected = []
|
|
885
|
+
for name, client in CLIENTS.items():
|
|
886
|
+
if is_client_installed(client):
|
|
887
|
+
configured = is_aleph_configured(client)
|
|
888
|
+
detected.append((name, client, configured))
|
|
889
|
+
|
|
890
|
+
if not detected:
|
|
891
|
+
print_warning("No MCP clients detected!")
|
|
892
|
+
print_info("Supported clients: Claude Desktop, Cursor, Windsurf, VSCode, Claude Code, Codex CLI")
|
|
893
|
+
return
|
|
894
|
+
|
|
895
|
+
print_info(f"Detected {len(detected)} MCP client(s):\n")
|
|
896
|
+
|
|
897
|
+
rows = []
|
|
898
|
+
for name, client, configured in detected:
|
|
899
|
+
status = "Already configured" if configured else "Not configured"
|
|
900
|
+
path = client.get_path()
|
|
901
|
+
path_str = "(CLI)" if client.is_cli else str(path) if path else "-"
|
|
902
|
+
rows.append((client.display_name, status, path_str))
|
|
903
|
+
|
|
904
|
+
print_table("Detected Clients", rows)
|
|
905
|
+
|
|
906
|
+
# Ask user which to configure
|
|
907
|
+
to_configure = []
|
|
908
|
+
for name, client, configured in detected:
|
|
909
|
+
if configured:
|
|
910
|
+
print_info(f"{client.display_name}: Already configured, skipping")
|
|
911
|
+
continue
|
|
912
|
+
|
|
913
|
+
try:
|
|
914
|
+
response = input(f"Configure {client.display_name}? [Y/n]: ").strip().lower()
|
|
915
|
+
if response in ("", "y", "yes"):
|
|
916
|
+
to_configure.append(client)
|
|
917
|
+
except (EOFError, KeyboardInterrupt):
|
|
918
|
+
print("\nAborted.")
|
|
919
|
+
return
|
|
920
|
+
|
|
921
|
+
if not to_configure:
|
|
922
|
+
print_info("No clients to configure.")
|
|
923
|
+
return
|
|
924
|
+
|
|
925
|
+
print()
|
|
926
|
+
for client in to_configure:
|
|
927
|
+
install_client(client, dry_run)
|
|
928
|
+
print()
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def install_all(dry_run: bool = False) -> None:
|
|
932
|
+
"""Install Aleph to all detected clients."""
|
|
933
|
+
print_header("Installing Aleph to All Detected Clients")
|
|
934
|
+
|
|
935
|
+
for name, client in CLIENTS.items():
|
|
936
|
+
if not is_client_installed(client):
|
|
937
|
+
print_info(f"Skipping {client.display_name} (not installed)")
|
|
938
|
+
continue
|
|
939
|
+
|
|
940
|
+
if is_aleph_configured(client):
|
|
941
|
+
print_info(f"Skipping {client.display_name} (already configured)")
|
|
942
|
+
continue
|
|
943
|
+
|
|
944
|
+
install_client(client, dry_run)
|
|
945
|
+
print()
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
# =============================================================================
|
|
949
|
+
# CLI entry point
|
|
950
|
+
# =============================================================================
|
|
951
|
+
|
|
952
|
+
def print_usage() -> None:
|
|
953
|
+
"""Print CLI usage information."""
|
|
954
|
+
print("""
|
|
955
|
+
Aleph MCP Server Installer
|
|
956
|
+
|
|
957
|
+
Usage:
|
|
958
|
+
aleph-rlm install Interactive mode - detect and configure clients
|
|
959
|
+
aleph-rlm install <client> Configure a specific client
|
|
960
|
+
aleph-rlm install --all Configure all detected clients
|
|
961
|
+
aleph-rlm uninstall <client> Remove Aleph from a client
|
|
962
|
+
aleph-rlm doctor Verify installation
|
|
963
|
+
|
|
964
|
+
Clients:
|
|
965
|
+
claude-desktop Claude Desktop app
|
|
966
|
+
cursor Cursor editor (global config)
|
|
967
|
+
cursor-project Cursor editor (project config)
|
|
968
|
+
windsurf Windsurf editor
|
|
969
|
+
vscode VSCode (project config)
|
|
970
|
+
claude-code Claude Code CLI
|
|
971
|
+
codex Codex CLI
|
|
972
|
+
gemini Gemini CLI
|
|
973
|
+
|
|
974
|
+
Options:
|
|
975
|
+
--dry-run Preview changes without writing
|
|
976
|
+
--help, -h Show this help message
|
|
977
|
+
|
|
978
|
+
Examples:
|
|
979
|
+
aleph-rlm install # Interactive mode
|
|
980
|
+
aleph-rlm install claude-desktop # Configure Claude Desktop
|
|
981
|
+
aleph-rlm install codex # Configure Codex CLI
|
|
982
|
+
aleph-rlm install --all --dry-run # Preview all installations
|
|
983
|
+
aleph-rlm uninstall cursor # Remove from Cursor
|
|
984
|
+
aleph-rlm doctor # Check installation status
|
|
985
|
+
""")
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
def main() -> None:
|
|
989
|
+
"""CLI entry point."""
|
|
990
|
+
args = sys.argv[1:]
|
|
991
|
+
|
|
992
|
+
if not args or args[0] in ("--help", "-h", "help"):
|
|
993
|
+
print_usage()
|
|
994
|
+
return
|
|
995
|
+
|
|
996
|
+
dry_run = "--dry-run" in args
|
|
997
|
+
if dry_run:
|
|
998
|
+
args = [a for a in args if a != "--dry-run"]
|
|
999
|
+
|
|
1000
|
+
command = args[0] if args else ""
|
|
1001
|
+
|
|
1002
|
+
if command == "doctor":
|
|
1003
|
+
success = doctor()
|
|
1004
|
+
sys.exit(0 if success else 1)
|
|
1005
|
+
|
|
1006
|
+
elif command == "install":
|
|
1007
|
+
if len(args) == 1:
|
|
1008
|
+
# Interactive mode
|
|
1009
|
+
interactive_install(dry_run)
|
|
1010
|
+
elif args[1] == "--all":
|
|
1011
|
+
install_all(dry_run)
|
|
1012
|
+
elif args[1] in CLIENTS:
|
|
1013
|
+
client = CLIENTS[args[1]]
|
|
1014
|
+
success = install_client(client, dry_run)
|
|
1015
|
+
sys.exit(0 if success else 1)
|
|
1016
|
+
else:
|
|
1017
|
+
print_error(f"Unknown client: {args[1]}")
|
|
1018
|
+
print_info(f"Available clients: {', '.join(CLIENTS.keys())}")
|
|
1019
|
+
sys.exit(1)
|
|
1020
|
+
|
|
1021
|
+
elif command == "uninstall":
|
|
1022
|
+
if len(args) < 2:
|
|
1023
|
+
print_error("Please specify a client to uninstall from")
|
|
1024
|
+
print_info(f"Available clients: {', '.join(CLIENTS.keys())}")
|
|
1025
|
+
sys.exit(1)
|
|
1026
|
+
|
|
1027
|
+
client_name = args[1]
|
|
1028
|
+
if client_name not in CLIENTS:
|
|
1029
|
+
print_error(f"Unknown client: {client_name}")
|
|
1030
|
+
print_info(f"Available clients: {', '.join(CLIENTS.keys())}")
|
|
1031
|
+
sys.exit(1)
|
|
1032
|
+
|
|
1033
|
+
client = CLIENTS[client_name]
|
|
1034
|
+
success = uninstall_client(client, dry_run)
|
|
1035
|
+
sys.exit(0 if success else 1)
|
|
1036
|
+
|
|
1037
|
+
else:
|
|
1038
|
+
print_error(f"Unknown command: {command}")
|
|
1039
|
+
print_usage()
|
|
1040
|
+
sys.exit(1)
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
if __name__ == "__main__":
|
|
1044
|
+
main()
|