castrel-proxy 0.1.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.
- castrel_proxy/__init__.py +22 -0
- castrel_proxy/cli/__init__.py +5 -0
- castrel_proxy/cli/commands.py +608 -0
- castrel_proxy/core/__init__.py +18 -0
- castrel_proxy/core/client_id.py +94 -0
- castrel_proxy/core/config.py +158 -0
- castrel_proxy/core/daemon.py +206 -0
- castrel_proxy/core/executor.py +166 -0
- castrel_proxy/data/__init__.py +1 -0
- castrel_proxy/data/default_whitelist.txt +229 -0
- castrel_proxy/mcp/__init__.py +8 -0
- castrel_proxy/mcp/manager.py +278 -0
- castrel_proxy/network/__init__.py +13 -0
- castrel_proxy/network/api_client.py +284 -0
- castrel_proxy/network/websocket_client.py +1148 -0
- castrel_proxy/operations/__init__.py +17 -0
- castrel_proxy/operations/document.py +343 -0
- castrel_proxy/security/__init__.py +17 -0
- castrel_proxy/security/whitelist.py +403 -0
- castrel_proxy-0.1.0.dist-info/METADATA +302 -0
- castrel_proxy-0.1.0.dist-info/RECORD +24 -0
- castrel_proxy-0.1.0.dist-info/WHEEL +4 -0
- castrel_proxy-0.1.0.dist-info/entry_points.txt +2 -0
- castrel_proxy-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Castrel Bridge Proxy - Remote command execution bridge client"""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.5a1"
|
|
4
|
+
__author__ = "Castrel Team"
|
|
5
|
+
__license__ = "MIT"
|
|
6
|
+
|
|
7
|
+
from .core.client_id import get_client_id, get_machine_metadata
|
|
8
|
+
from .core.config import Config, ConfigError, get_config
|
|
9
|
+
from .network.api_client import APIClient, APIError, NetworkError, PairingError
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"__version__",
|
|
13
|
+
"get_client_id",
|
|
14
|
+
"get_machine_metadata",
|
|
15
|
+
"Config",
|
|
16
|
+
"ConfigError",
|
|
17
|
+
"get_config",
|
|
18
|
+
"APIClient",
|
|
19
|
+
"APIError",
|
|
20
|
+
"NetworkError",
|
|
21
|
+
"PairingError",
|
|
22
|
+
]
|
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI Commands Module
|
|
3
|
+
|
|
4
|
+
Defines all command-line interface commands for Castrel Bridge Proxy
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import base64
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any, Dict
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
|
|
16
|
+
from ..core.client_id import get_client_id
|
|
17
|
+
from ..core.config import ConfigError, get_config
|
|
18
|
+
from ..core.daemon import get_daemon_manager
|
|
19
|
+
from ..mcp.manager import get_mcp_manager
|
|
20
|
+
from ..network.api_client import APIError, NetworkError, PairingError, get_api_client
|
|
21
|
+
from ..network.websocket_client import WebSocketClient
|
|
22
|
+
from ..security.whitelist import init_whitelist_file
|
|
23
|
+
|
|
24
|
+
# Configure logging
|
|
25
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
26
|
+
|
|
27
|
+
app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def decode_verification_code(verification_code: str) -> Dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Decode verification code, extracting timestamp, workspace_id, and random code
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
verification_code: Encoded verification code
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Dict[str, Any]: Dictionary containing ts (timestamp), wid (workspace_id), rand (random_code)
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: Invalid verification code format
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
# Add possibly missing padding characters
|
|
45
|
+
padding = 4 - (len(verification_code) % 4)
|
|
46
|
+
if padding != 4:
|
|
47
|
+
verification_code += "=" * padding
|
|
48
|
+
|
|
49
|
+
# Base64 decode
|
|
50
|
+
decoded_bytes = base64.urlsafe_b64decode(verification_code)
|
|
51
|
+
json_str = decoded_bytes.decode("utf-8")
|
|
52
|
+
|
|
53
|
+
# Parse JSON
|
|
54
|
+
code_data = json.loads(json_str)
|
|
55
|
+
|
|
56
|
+
# Validate required fields
|
|
57
|
+
if not all(key in code_data for key in ["ts", "wid", "rand"]):
|
|
58
|
+
raise ValueError("Missing required fields in verification code")
|
|
59
|
+
|
|
60
|
+
return code_data
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
raise ValueError(f"Invalid verification code format: {str(e)}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.command()
|
|
67
|
+
def pair(
|
|
68
|
+
code: str = typer.Argument(..., help="Verification code provided by server"),
|
|
69
|
+
server_url: str = typer.Argument(..., help="Server URL address"),
|
|
70
|
+
):
|
|
71
|
+
"""
|
|
72
|
+
Pair with server
|
|
73
|
+
|
|
74
|
+
Pair local bridge with server using verification code. The code contains workspace ID and other
|
|
75
|
+
information without needing manual input.
|
|
76
|
+
|
|
77
|
+
Usage:
|
|
78
|
+
castrel-proxy pair <verification_code> <server_url>
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
castrel-proxy pair eyJ0cyI6MTczNTA4ODQwMCwid2lkIjoiZGVmYXVsdCIsInJhbmQiOiIxMjM0NTYifQ https://server.example.com
|
|
82
|
+
"""
|
|
83
|
+
config = get_config()
|
|
84
|
+
api_client = get_api_client()
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# Decode verification code to get workspace_id
|
|
88
|
+
typer.echo("Parsing verification code...")
|
|
89
|
+
try:
|
|
90
|
+
code_info = decode_verification_code(code)
|
|
91
|
+
workspace_id = code_info["wid"]
|
|
92
|
+
|
|
93
|
+
typer.secho("✓ Verification code parsed successfully", fg=typer.colors.GREEN)
|
|
94
|
+
typer.echo(f" Workspace ID: {workspace_id}")
|
|
95
|
+
except ValueError as e:
|
|
96
|
+
typer.secho(f"✗ Invalid verification code format: {e}", fg=typer.colors.RED, err=True)
|
|
97
|
+
typer.echo("Hint: Please ensure you use the complete verification code from the server", err=True)
|
|
98
|
+
raise typer.Exit(1)
|
|
99
|
+
|
|
100
|
+
# Generate client ID
|
|
101
|
+
typer.echo("\nGenerating client identifier...")
|
|
102
|
+
client_id = get_client_id()
|
|
103
|
+
typer.echo(f"Client ID: {client_id}")
|
|
104
|
+
|
|
105
|
+
# Connect to server and verify
|
|
106
|
+
typer.echo(f"\nConnecting to server: {server_url}")
|
|
107
|
+
typer.echo(f"Using verification code: {code}")
|
|
108
|
+
typer.echo(f"Workspace ID: {workspace_id}")
|
|
109
|
+
|
|
110
|
+
# Call server verification endpoint
|
|
111
|
+
api_client.verify_pairing(server_url, code, client_id, workspace_id)
|
|
112
|
+
|
|
113
|
+
# Verification successful, save configuration
|
|
114
|
+
config.save(server_url, code, client_id, workspace_id)
|
|
115
|
+
|
|
116
|
+
# Initialize whitelist configuration file
|
|
117
|
+
whitelist_path = init_whitelist_file()
|
|
118
|
+
typer.echo(f"Whitelist configuration initialized: {whitelist_path}")
|
|
119
|
+
|
|
120
|
+
typer.secho("✓ Pairing successful!", fg=typer.colors.GREEN)
|
|
121
|
+
typer.echo(f"Configuration saved to: {config.config_file}")
|
|
122
|
+
|
|
123
|
+
# Try to load and send MCP tools information
|
|
124
|
+
typer.echo("\nLoading MCP services...")
|
|
125
|
+
try:
|
|
126
|
+
mcp_manager = get_mcp_manager()
|
|
127
|
+
|
|
128
|
+
# Asynchronously connect to MCP and get tools
|
|
129
|
+
async def sync_mcp_tools():
|
|
130
|
+
# Connect all MCP services
|
|
131
|
+
count = await mcp_manager.connect_all()
|
|
132
|
+
if count == 0:
|
|
133
|
+
typer.echo("No MCP services configured, not registering MCP info")
|
|
134
|
+
await api_client._send_client_info(server_url, client_id, code, workspace_id, {})
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
typer.echo(f"Connected to {count} MCP service(s)")
|
|
138
|
+
|
|
139
|
+
# Get all tools
|
|
140
|
+
tools = await mcp_manager.get_all_tools()
|
|
141
|
+
typer.echo(f"Retrieved {len(tools)} tool(s)")
|
|
142
|
+
|
|
143
|
+
# Send to server
|
|
144
|
+
if tools:
|
|
145
|
+
typer.echo("Sending MCP tools information to server...")
|
|
146
|
+
await api_client._send_client_info(server_url, client_id, code, workspace_id, tools)
|
|
147
|
+
typer.secho("✓ MCP tools information synchronized", fg=typer.colors.GREEN)
|
|
148
|
+
|
|
149
|
+
# Disconnect MCP connections
|
|
150
|
+
await mcp_manager.disconnect_all()
|
|
151
|
+
|
|
152
|
+
asyncio.run(sync_mcp_tools())
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
typer.secho(f"⚠ MCP synchronization failed: {e}", fg=typer.colors.YELLOW)
|
|
156
|
+
typer.echo("Hint: You can manually synchronize MCP information later")
|
|
157
|
+
|
|
158
|
+
typer.echo("\nHint: Use 'castrel-proxy start' to start bridge service")
|
|
159
|
+
|
|
160
|
+
except PairingError as e:
|
|
161
|
+
typer.secho(f"✗ Pairing failed: {e}", fg=typer.colors.RED, err=True)
|
|
162
|
+
raise typer.Exit(1)
|
|
163
|
+
except NetworkError as e:
|
|
164
|
+
typer.secho(f"✗ Network error: {e}", fg=typer.colors.RED, err=True)
|
|
165
|
+
typer.echo("Please check if server address is correct and network connection is normal", err=True)
|
|
166
|
+
raise typer.Exit(1)
|
|
167
|
+
except ConfigError as e:
|
|
168
|
+
typer.secho(f"✗ Configuration error: {e}", fg=typer.colors.RED, err=True)
|
|
169
|
+
raise typer.Exit(1)
|
|
170
|
+
except APIError as e:
|
|
171
|
+
typer.secho(f"✗ API error: {e}", fg=typer.colors.RED, err=True)
|
|
172
|
+
raise typer.Exit(1)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
typer.secho(f"✗ Unknown error: {e}", fg=typer.colors.RED, err=True)
|
|
175
|
+
raise typer.Exit(1)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@app.command()
|
|
179
|
+
def start(
|
|
180
|
+
daemon: bool = typer.Option(
|
|
181
|
+
True, "--daemon/--foreground", "-d/-f", help="Run in background (default) or foreground"
|
|
182
|
+
),
|
|
183
|
+
):
|
|
184
|
+
"""
|
|
185
|
+
Start bridge service
|
|
186
|
+
|
|
187
|
+
Start bridge and connect to paired server.
|
|
188
|
+
|
|
189
|
+
Run in background (default):
|
|
190
|
+
castrel-proxy start
|
|
191
|
+
castrel-proxy start --daemon
|
|
192
|
+
castrel-proxy start -d
|
|
193
|
+
|
|
194
|
+
Run in foreground:
|
|
195
|
+
castrel-proxy start --foreground
|
|
196
|
+
castrel-proxy start -f
|
|
197
|
+
"""
|
|
198
|
+
config = get_config()
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
# Load configuration
|
|
202
|
+
config_data = config.load()
|
|
203
|
+
server_url = config_data["server_url"]
|
|
204
|
+
client_id = config_data["client_id"]
|
|
205
|
+
verification_code = config_data["verification_code"]
|
|
206
|
+
workspace_id = config_data["workspace_id"]
|
|
207
|
+
|
|
208
|
+
typer.secho("=== Starting Bridge Service ===", bold=True)
|
|
209
|
+
typer.echo(f"Server: {server_url}")
|
|
210
|
+
typer.echo(f"Client ID: {client_id}")
|
|
211
|
+
typer.echo(f"Workspace ID: {workspace_id}")
|
|
212
|
+
|
|
213
|
+
if daemon:
|
|
214
|
+
# Check if Windows
|
|
215
|
+
if sys.platform == "win32":
|
|
216
|
+
typer.secho("✗ Background mode is not supported on Windows", fg=typer.colors.YELLOW)
|
|
217
|
+
typer.echo("Hint: Use '--foreground' or '-f' flag to run in foreground mode")
|
|
218
|
+
raise typer.Exit(1)
|
|
219
|
+
|
|
220
|
+
# Get daemon manager
|
|
221
|
+
daemon_mgr = get_daemon_manager()
|
|
222
|
+
|
|
223
|
+
# Check if already running
|
|
224
|
+
if daemon_mgr.is_running():
|
|
225
|
+
pid = daemon_mgr.get_pid()
|
|
226
|
+
typer.secho(f"✗ Bridge is already running with PID {pid}", fg=typer.colors.YELLOW)
|
|
227
|
+
typer.echo("Hint: Use 'castrel-proxy stop' to stop it first")
|
|
228
|
+
raise typer.Exit(1)
|
|
229
|
+
|
|
230
|
+
typer.echo("\nStarting bridge in background...")
|
|
231
|
+
typer.echo(f"PID file: {daemon_mgr.pid_file}")
|
|
232
|
+
typer.echo(f"Log file: {daemon_mgr.log_file}")
|
|
233
|
+
|
|
234
|
+
# Daemonize the process
|
|
235
|
+
try:
|
|
236
|
+
daemon_mgr.daemonize()
|
|
237
|
+
|
|
238
|
+
# Configure logging to file
|
|
239
|
+
logging.basicConfig(
|
|
240
|
+
level=logging.INFO,
|
|
241
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
242
|
+
handlers=[logging.FileHandler(daemon_mgr.log_file), logging.StreamHandler()],
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
logger = logging.getLogger(__name__)
|
|
246
|
+
logger.info("=== Bridge Service Started in Background ===")
|
|
247
|
+
logger.info(f"Server: {server_url}")
|
|
248
|
+
logger.info(f"Client ID: {client_id}")
|
|
249
|
+
logger.info(f"Workspace ID: {workspace_id}")
|
|
250
|
+
|
|
251
|
+
# Create and run WebSocket client
|
|
252
|
+
ws_client = WebSocketClient(
|
|
253
|
+
server_url=server_url,
|
|
254
|
+
client_id=client_id,
|
|
255
|
+
verification_code=verification_code,
|
|
256
|
+
workspace_id=workspace_id,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
asyncio.run(ws_client.run())
|
|
260
|
+
|
|
261
|
+
except RuntimeError as e:
|
|
262
|
+
# Already running error
|
|
263
|
+
typer.secho(f"✗ {e}", fg=typer.colors.RED, err=True)
|
|
264
|
+
raise typer.Exit(1)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
if daemon_mgr.get_pid() is not None:
|
|
267
|
+
# Log error in daemon mode
|
|
268
|
+
logging.error(f"Runtime error: {e}", exc_info=True)
|
|
269
|
+
raise typer.Exit(1)
|
|
270
|
+
|
|
271
|
+
# This line is only reached by parent process before exit
|
|
272
|
+
typer.secho("✓ Bridge started in background", fg=typer.colors.GREEN)
|
|
273
|
+
typer.echo(f"PID: {daemon_mgr.get_pid()}")
|
|
274
|
+
typer.echo("Hint: Use 'castrel-proxy logs -f' to follow logs")
|
|
275
|
+
typer.echo("Hint: Use 'castrel-proxy stop' to stop service")
|
|
276
|
+
|
|
277
|
+
else:
|
|
278
|
+
typer.echo("\nRunning in foreground mode...")
|
|
279
|
+
typer.echo("Connecting to server...")
|
|
280
|
+
typer.echo("Hint: Press Ctrl+C to stop service\n")
|
|
281
|
+
|
|
282
|
+
# Create WebSocket client
|
|
283
|
+
ws_client = WebSocketClient(
|
|
284
|
+
server_url=server_url,
|
|
285
|
+
client_id=client_id,
|
|
286
|
+
verification_code=verification_code,
|
|
287
|
+
workspace_id=workspace_id,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Run client
|
|
291
|
+
try:
|
|
292
|
+
asyncio.run(ws_client.run())
|
|
293
|
+
except KeyboardInterrupt:
|
|
294
|
+
typer.echo("\nStopping bridge...")
|
|
295
|
+
typer.secho("✓ Bridge stopped", fg=typer.colors.GREEN)
|
|
296
|
+
except Exception as e:
|
|
297
|
+
typer.secho(f"\n✗ Runtime error: {e}", fg=typer.colors.RED, err=True)
|
|
298
|
+
raise typer.Exit(1)
|
|
299
|
+
|
|
300
|
+
except ConfigError as e:
|
|
301
|
+
typer.secho(f"✗ {e}", fg=typer.colors.RED, err=True)
|
|
302
|
+
typer.echo("Hint: Please pair first using 'castrel-proxy pair' command", err=True)
|
|
303
|
+
raise typer.Exit(1)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@app.command()
|
|
307
|
+
def config():
|
|
308
|
+
"""
|
|
309
|
+
View configuration information
|
|
310
|
+
"""
|
|
311
|
+
config_obj = get_config()
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
# Load configuration
|
|
315
|
+
config_data = config_obj.load()
|
|
316
|
+
|
|
317
|
+
typer.secho("=== Configuration Information ===", bold=True)
|
|
318
|
+
typer.echo(f"Config file: {config_obj.config_file}")
|
|
319
|
+
typer.echo(f"Server URL: {config_data['server_url']}")
|
|
320
|
+
typer.echo(f"Verification code: {config_data['verification_code']}")
|
|
321
|
+
typer.echo(f"Client ID: {config_data['client_id']}")
|
|
322
|
+
typer.echo(f"Workspace ID: {config_data['workspace_id']}")
|
|
323
|
+
|
|
324
|
+
if "paired_at" in config_data:
|
|
325
|
+
typer.echo(f"Paired at: {config_data['paired_at']}")
|
|
326
|
+
|
|
327
|
+
except ConfigError as e:
|
|
328
|
+
typer.secho(f"✗ {e}", fg=typer.colors.RED, err=True)
|
|
329
|
+
raise typer.Exit(1)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@app.command()
|
|
333
|
+
def status():
|
|
334
|
+
"""
|
|
335
|
+
View bridge running status
|
|
336
|
+
"""
|
|
337
|
+
config = get_config()
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
# Load configuration
|
|
341
|
+
config_data = config.load()
|
|
342
|
+
|
|
343
|
+
typer.secho("=== Bridge Status ===", bold=True)
|
|
344
|
+
typer.echo("Pairing status: ", nl=False)
|
|
345
|
+
typer.secho("Paired", fg=typer.colors.GREEN)
|
|
346
|
+
typer.echo(f"Server: {config_data['server_url']}")
|
|
347
|
+
typer.echo(f"Client ID: {config_data['client_id']}")
|
|
348
|
+
typer.echo(f"Workspace ID: {config_data['workspace_id']}")
|
|
349
|
+
|
|
350
|
+
if "paired_at" in config_data:
|
|
351
|
+
typer.echo(f"Paired at: {config_data['paired_at']}")
|
|
352
|
+
|
|
353
|
+
# Check running status
|
|
354
|
+
daemon_mgr = get_daemon_manager()
|
|
355
|
+
typer.echo("Running status: ", nl=False)
|
|
356
|
+
|
|
357
|
+
if daemon_mgr.is_running():
|
|
358
|
+
pid = daemon_mgr.get_pid()
|
|
359
|
+
typer.secho(f"Running (PID: {pid})", fg=typer.colors.GREEN)
|
|
360
|
+
typer.echo(f"PID file: {daemon_mgr.pid_file}")
|
|
361
|
+
typer.echo(f"Log file: {daemon_mgr.log_file}")
|
|
362
|
+
typer.echo("Hint: Use 'castrel-proxy logs -f' to follow logs")
|
|
363
|
+
else:
|
|
364
|
+
typer.secho("Not running", fg=typer.colors.YELLOW)
|
|
365
|
+
typer.echo("Hint: Use 'castrel-proxy start' to start service")
|
|
366
|
+
|
|
367
|
+
except ConfigError:
|
|
368
|
+
typer.secho("=== Bridge Status ===", bold=True)
|
|
369
|
+
typer.echo("Pairing status: ", nl=False)
|
|
370
|
+
typer.secho("Not paired", fg=typer.colors.YELLOW)
|
|
371
|
+
typer.echo("Hint: Use 'castrel-proxy pair' command to pair")
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@app.command()
|
|
375
|
+
def stop():
|
|
376
|
+
"""
|
|
377
|
+
Stop bridge service
|
|
378
|
+
|
|
379
|
+
Stop the background daemon process if running.
|
|
380
|
+
"""
|
|
381
|
+
daemon_mgr = get_daemon_manager()
|
|
382
|
+
|
|
383
|
+
# Check if running
|
|
384
|
+
if not daemon_mgr.is_running():
|
|
385
|
+
typer.secho("✗ Bridge is not running", fg=typer.colors.YELLOW)
|
|
386
|
+
# Clean up stale PID file if exists
|
|
387
|
+
if daemon_mgr.pid_file.exists():
|
|
388
|
+
daemon_mgr.pid_file.unlink()
|
|
389
|
+
typer.echo("Cleaned up stale PID file")
|
|
390
|
+
raise typer.Exit(0)
|
|
391
|
+
|
|
392
|
+
pid = daemon_mgr.get_pid()
|
|
393
|
+
typer.echo(f"Stopping bridge (PID: {pid})...")
|
|
394
|
+
|
|
395
|
+
# Stop the daemon
|
|
396
|
+
if daemon_mgr.stop():
|
|
397
|
+
typer.secho("✓ Bridge stopped", fg=typer.colors.GREEN)
|
|
398
|
+
else:
|
|
399
|
+
typer.secho("✗ Failed to stop bridge", fg=typer.colors.RED, err=True)
|
|
400
|
+
typer.echo(f"Hint: Try manually killing process {pid}")
|
|
401
|
+
raise typer.Exit(1)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@app.command()
|
|
405
|
+
def unpair():
|
|
406
|
+
"""
|
|
407
|
+
Unpair from server
|
|
408
|
+
"""
|
|
409
|
+
config = get_config()
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
# Check if configuration exists
|
|
413
|
+
if not config.exists():
|
|
414
|
+
typer.secho("✗ Pairing configuration not found", fg=typer.colors.YELLOW)
|
|
415
|
+
raise typer.Exit(0)
|
|
416
|
+
|
|
417
|
+
# Display current configuration information
|
|
418
|
+
config_data = config.load()
|
|
419
|
+
typer.echo(f"Currently paired server: {config_data['server_url']}")
|
|
420
|
+
typer.echo(f"Client ID: {config_data['client_id']}")
|
|
421
|
+
typer.echo(f"Workspace ID: {config_data['workspace_id']}")
|
|
422
|
+
|
|
423
|
+
# Confirm deletion
|
|
424
|
+
confirm = typer.confirm("Are you sure you want to unpair?")
|
|
425
|
+
if confirm:
|
|
426
|
+
typer.echo("Unpairing...")
|
|
427
|
+
config.delete()
|
|
428
|
+
typer.secho("✓ Unpaired", fg=typer.colors.GREEN)
|
|
429
|
+
else:
|
|
430
|
+
typer.echo("Cancelled")
|
|
431
|
+
|
|
432
|
+
except ConfigError as e:
|
|
433
|
+
typer.secho(f"✗ {e}", fg=typer.colors.RED, err=True)
|
|
434
|
+
raise typer.Exit(1)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@app.command()
|
|
438
|
+
def logs(
|
|
439
|
+
lines: int = typer.Option(50, "--lines", "-n", help="Display last N lines of logs"),
|
|
440
|
+
follow: bool = typer.Option(False, "--follow", "-f", help="Follow logs in real-time"),
|
|
441
|
+
):
|
|
442
|
+
"""
|
|
443
|
+
View bridge logs
|
|
444
|
+
|
|
445
|
+
Display logs from the background daemon process.
|
|
446
|
+
"""
|
|
447
|
+
daemon_mgr = get_daemon_manager()
|
|
448
|
+
|
|
449
|
+
# Check if log file exists
|
|
450
|
+
if not daemon_mgr.log_file.exists():
|
|
451
|
+
typer.secho("✗ Log file not found", fg=typer.colors.YELLOW)
|
|
452
|
+
typer.echo(f"Expected location: {daemon_mgr.log_file}")
|
|
453
|
+
typer.echo("Hint: Start the bridge first using 'castrel-proxy start'")
|
|
454
|
+
raise typer.Exit(1)
|
|
455
|
+
|
|
456
|
+
if follow:
|
|
457
|
+
# Follow logs in real-time
|
|
458
|
+
typer.echo(f"Following logs from {daemon_mgr.log_file}... (Ctrl+C to exit)\n")
|
|
459
|
+
try:
|
|
460
|
+
import subprocess
|
|
461
|
+
|
|
462
|
+
# Use tail -f to follow logs
|
|
463
|
+
subprocess.run(["tail", "-f", str(daemon_mgr.log_file)])
|
|
464
|
+
except KeyboardInterrupt:
|
|
465
|
+
typer.echo("\nStopped following logs")
|
|
466
|
+
except FileNotFoundError:
|
|
467
|
+
# tail command not available, fallback to Python implementation
|
|
468
|
+
typer.secho("✗ 'tail' command not found", fg=typer.colors.YELLOW)
|
|
469
|
+
typer.echo("Hint: Install coreutils or use '--lines' without '--follow'")
|
|
470
|
+
raise typer.Exit(1)
|
|
471
|
+
else:
|
|
472
|
+
# Display last N lines
|
|
473
|
+
typer.echo(f"Last {lines} lines from {daemon_mgr.log_file}:\n")
|
|
474
|
+
try:
|
|
475
|
+
with open(daemon_mgr.log_file, "r") as f:
|
|
476
|
+
all_lines = f.readlines()
|
|
477
|
+
last_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
|
|
478
|
+
for line in last_lines:
|
|
479
|
+
typer.echo(line.rstrip())
|
|
480
|
+
except Exception as e:
|
|
481
|
+
typer.secho(f"✗ Failed to read log file: {e}", fg=typer.colors.RED, err=True)
|
|
482
|
+
raise typer.Exit(1)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
@app.command()
|
|
486
|
+
def mcp_list():
|
|
487
|
+
"""
|
|
488
|
+
List configured MCP services
|
|
489
|
+
"""
|
|
490
|
+
mcp_manager = get_mcp_manager()
|
|
491
|
+
servers = mcp_manager.get_server_list()
|
|
492
|
+
|
|
493
|
+
if not servers:
|
|
494
|
+
typer.echo("No MCP services configured")
|
|
495
|
+
typer.echo(f"Config file: {mcp_manager.config_file}")
|
|
496
|
+
typer.echo("Hint: Refer to mcp.json.example to create configuration file")
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
typer.secho("=== MCP Service List ===", bold=True)
|
|
500
|
+
typer.echo(f"Config file: {mcp_manager.config_file}")
|
|
501
|
+
typer.echo(f"Total {len(servers)} service(s):\n")
|
|
502
|
+
|
|
503
|
+
for server in servers:
|
|
504
|
+
name = server["name"]
|
|
505
|
+
transport = server["transport"]
|
|
506
|
+
|
|
507
|
+
typer.echo(f"📦 {name}")
|
|
508
|
+
typer.echo(f" Transport: {transport}")
|
|
509
|
+
|
|
510
|
+
if transport == "stdio":
|
|
511
|
+
command = server["command"]
|
|
512
|
+
args = server["args"]
|
|
513
|
+
typer.echo(f" Command: {command} {' '.join(args)}")
|
|
514
|
+
if server["env"]:
|
|
515
|
+
typer.echo(f" Environment variables: {len(server['env'])} var(s)")
|
|
516
|
+
elif transport == "http":
|
|
517
|
+
typer.echo(f" URL: {server['url']}")
|
|
518
|
+
|
|
519
|
+
typer.echo()
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@app.command()
|
|
523
|
+
def mcp_sync():
|
|
524
|
+
"""
|
|
525
|
+
Synchronize MCP tools information to server
|
|
526
|
+
"""
|
|
527
|
+
config = get_config()
|
|
528
|
+
|
|
529
|
+
try:
|
|
530
|
+
# Load configuration
|
|
531
|
+
config_data = config.load()
|
|
532
|
+
server_url = config_data["server_url"]
|
|
533
|
+
client_id = config_data["client_id"]
|
|
534
|
+
verification_code = config_data["verification_code"]
|
|
535
|
+
workspace_id = config_data["workspace_id"]
|
|
536
|
+
|
|
537
|
+
typer.secho("=== Synchronizing MCP Tools ===", bold=True)
|
|
538
|
+
typer.echo(f"Server: {server_url}")
|
|
539
|
+
typer.echo(f"Client ID: {client_id}")
|
|
540
|
+
typer.echo(f"Workspace ID: {workspace_id}\n")
|
|
541
|
+
|
|
542
|
+
mcp_manager = get_mcp_manager()
|
|
543
|
+
api_client = get_api_client()
|
|
544
|
+
|
|
545
|
+
# Asynchronously synchronize MCP tools
|
|
546
|
+
async def sync_mcp_tools():
|
|
547
|
+
# Connect all MCP services
|
|
548
|
+
typer.echo("Connecting to MCP services...")
|
|
549
|
+
count = await mcp_manager.connect_all()
|
|
550
|
+
|
|
551
|
+
if count == 0:
|
|
552
|
+
typer.secho("✗ No available MCP services", fg=typer.colors.YELLOW)
|
|
553
|
+
typer.echo(f"Config file: {mcp_manager.config_file}")
|
|
554
|
+
typer.echo("Hint: Use 'castrel-proxy mcp-list' to view configuration")
|
|
555
|
+
return
|
|
556
|
+
|
|
557
|
+
typer.secho(f"✓ Connected to {count} MCP service(s)", fg=typer.colors.GREEN)
|
|
558
|
+
|
|
559
|
+
# Get all tools
|
|
560
|
+
typer.echo("\nRetrieving tools information...")
|
|
561
|
+
tools = await mcp_manager.get_all_tools()
|
|
562
|
+
|
|
563
|
+
# Calculate total tool count
|
|
564
|
+
total_tools = sum(len(tool_list) for tool_list in tools.values())
|
|
565
|
+
typer.secho(f"✓ Retrieved {total_tools} tool(s)", fg=typer.colors.GREEN)
|
|
566
|
+
|
|
567
|
+
# Display tools overview
|
|
568
|
+
if tools:
|
|
569
|
+
typer.echo("\nTools overview:")
|
|
570
|
+
for server, tool_list in tools.items():
|
|
571
|
+
typer.echo(f" {server}: {len(tool_list)} tool(s)")
|
|
572
|
+
for tool in tool_list[:3]: # Only display first 3
|
|
573
|
+
typer.echo(f" - {tool['name']}")
|
|
574
|
+
if len(tool_list) > 3:
|
|
575
|
+
typer.echo(f" ... and {len(tool_list) - 3} more")
|
|
576
|
+
|
|
577
|
+
# Send to server
|
|
578
|
+
if tools:
|
|
579
|
+
typer.echo("\nSending to server...")
|
|
580
|
+
await api_client._send_client_info(server_url, client_id, verification_code, workspace_id, tools)
|
|
581
|
+
typer.secho("✓ MCP tools information synchronized", fg=typer.colors.GREEN)
|
|
582
|
+
else:
|
|
583
|
+
typer.secho("⚠ No tools to synchronize", fg=typer.colors.YELLOW)
|
|
584
|
+
|
|
585
|
+
# Disconnect MCP connections
|
|
586
|
+
await mcp_manager.disconnect_all()
|
|
587
|
+
|
|
588
|
+
asyncio.run(sync_mcp_tools())
|
|
589
|
+
|
|
590
|
+
except ConfigError as e:
|
|
591
|
+
typer.secho(f"✗ {e}", fg=typer.colors.RED, err=True)
|
|
592
|
+
typer.echo("Hint: Please pair first using 'castrel-proxy pair' command", err=True)
|
|
593
|
+
raise typer.Exit(1)
|
|
594
|
+
except (NetworkError, APIError) as e:
|
|
595
|
+
typer.secho(f"✗ Synchronization failed: {e}", fg=typer.colors.RED, err=True)
|
|
596
|
+
raise typer.Exit(1)
|
|
597
|
+
except Exception as e:
|
|
598
|
+
typer.secho(f"✗ Unknown error: {e}", fg=typer.colors.RED, err=True)
|
|
599
|
+
raise typer.Exit(1)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def run():
|
|
603
|
+
"""Entry point for the CLI application"""
|
|
604
|
+
app()
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
if __name__ == "__main__":
|
|
608
|
+
run()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Core functionality for Castrel Bridge Proxy"""
|
|
2
|
+
|
|
3
|
+
from .client_id import get_client_id, get_machine_metadata
|
|
4
|
+
from .config import Config, ConfigError, get_config
|
|
5
|
+
from .daemon import DaemonManager, get_daemon_manager
|
|
6
|
+
from .executor import CommandExecutor, ExecutionResult
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"get_client_id",
|
|
10
|
+
"get_machine_metadata",
|
|
11
|
+
"Config",
|
|
12
|
+
"ConfigError",
|
|
13
|
+
"get_config",
|
|
14
|
+
"DaemonManager",
|
|
15
|
+
"get_daemon_manager",
|
|
16
|
+
"CommandExecutor",
|
|
17
|
+
"ExecutionResult",
|
|
18
|
+
]
|