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.
@@ -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,5 @@
1
+ """Command line interface for Castrel Bridge Proxy"""
2
+
3
+ from .commands import app, run
4
+
5
+ __all__ = ["app", "run"]
@@ -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
+ ]