lyceum-cli 1.0.14__py3-none-any.whl → 1.0.18__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.
lyceum/__init__.py ADDED
File without changes
File without changes
File without changes
@@ -0,0 +1,462 @@
1
+ """
2
+ Authentication commands: login, logout, status
3
+ """
4
+
5
+ from typing import Optional
6
+ import typer
7
+ from rich.console import Console
8
+ import webbrowser
9
+ import threading
10
+ import time
11
+ from http.server import HTTPServer, BaseHTTPRequestHandler
12
+ from urllib.parse import urlparse, parse_qs
13
+ import socket
14
+
15
+ from ...shared.config import config
16
+
17
+ console = Console()
18
+
19
+ auth_app = typer.Typer(name="auth", help="Authentication commands")
20
+
21
+ # Global variables for callback server
22
+ callback_result = {"token": None, "error": None, "received": False}
23
+
24
+
25
+ class CallbackHandler(BaseHTTPRequestHandler):
26
+ """HTTP handler for OAuth callback"""
27
+
28
+ def log_message(self, format, *args):
29
+ # Suppress HTTP server logs
30
+ pass
31
+
32
+ def do_GET(self):
33
+ global callback_result
34
+
35
+ try:
36
+ # Parse the callback URL
37
+ parsed_url = urlparse(self.path)
38
+ query_params = parse_qs(parsed_url.query)
39
+
40
+ if parsed_url.path == "/callback":
41
+ # Extract token from query parameters
42
+ if "token" in query_params:
43
+ token = query_params["token"][0]
44
+ user_info = query_params.get("user", [None])[0]
45
+ refresh_token = query_params.get("refresh_token", [None])[0]
46
+
47
+ callback_result["token"] = token
48
+ callback_result["user"] = user_info
49
+ if refresh_token:
50
+ callback_result["refresh_token"] = refresh_token
51
+ callback_result["received"] = True
52
+
53
+ # Send success response
54
+ self.send_response(200)
55
+ self.send_header("Content-type", "text/html")
56
+ self.end_headers()
57
+
58
+ success_html = """
59
+ <!DOCTYPE html>
60
+ <html lang="en">
61
+ <head>
62
+ <meta charset="UTF-8">
63
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
64
+ <title>Lyceum CLI - Authentication Success</title>
65
+ <script src="https://cdn.tailwindcss.com"></script>
66
+ </head>
67
+ <body class="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
68
+ <div class="max-w-md w-full space-y-8">
69
+ <div class="bg-white rounded-lg shadow-md p-8 text-center">
70
+ <!-- Success Icon -->
71
+ <div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-green-100 mb-6">
72
+ <svg class="h-8 w-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
73
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
74
+ </svg>
75
+ </div>
76
+
77
+ <!-- Header -->
78
+ <div class="mb-6">
79
+ <h1 class="text-2xl font-bold text-gray-900 mb-2">Authentication Successful!</h1>
80
+ <p class="text-gray-600 text-lg">Welcome to Lyceum</p>
81
+ </div>
82
+
83
+ <!-- Instructions -->
84
+ <div class="space-y-3 mb-8">
85
+ <p class="text-gray-700">You can now close this browser tab and return to the CLI.</p>
86
+ <p class="text-sm text-gray-500">Your Lyceum CLI has been authenticated successfully and is ready to use.</p>
87
+ </div>
88
+
89
+ <!-- Close Button -->
90
+ <button
91
+ onclick="window.close()"
92
+ class="w-full py-2 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 transition-colors"
93
+ >
94
+ Close Tab
95
+ </button>
96
+
97
+ <!-- Lyceum Branding -->
98
+ <div class="mt-8 pt-6 border-t border-gray-200">
99
+ <p class="text-xs text-gray-400">Powered by Lyceum Technology</p>
100
+ </div>
101
+ </div>
102
+ </div>
103
+
104
+ <!-- Auto-close script -->
105
+ <script>
106
+ // Auto-close after 10 seconds
107
+ setTimeout(() => {
108
+ window.close();
109
+ }, 10000);
110
+ </script>
111
+ </body>
112
+ </html>
113
+ """
114
+ try:
115
+ self.wfile.write(success_html.encode())
116
+ except (BrokenPipeError, ConnectionResetError):
117
+ # Browser closed connection (expected when tab auto-closes)
118
+ pass
119
+
120
+ elif "error" in query_params:
121
+ error = query_params["error"][0]
122
+ callback_result["error"] = error
123
+ callback_result["received"] = True
124
+
125
+ # Send error response
126
+ self.send_response(400)
127
+ self.send_header("Content-type", "text/html")
128
+ self.end_headers()
129
+
130
+ error_html = f"""
131
+ <!DOCTYPE html>
132
+ <html lang="en">
133
+ <head>
134
+ <meta charset="UTF-8">
135
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
136
+ <title>Lyceum CLI - Authentication Error</title>
137
+ <script src="https://cdn.tailwindcss.com"></script>
138
+ </head>
139
+ <body class="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
140
+ <div class="max-w-md w-full space-y-8">
141
+ <div class="bg-white rounded-lg shadow-md p-8 text-center">
142
+ <!-- Error Icon -->
143
+ <div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-red-100 mb-6">
144
+ <svg class="h-8 w-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
145
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
146
+ </svg>
147
+ </div>
148
+
149
+ <!-- Header -->
150
+ <div class="mb-6">
151
+ <h1 class="text-2xl font-bold text-gray-900 mb-2">Authentication Failed</h1>
152
+ <p class="text-red-600 text-lg">Something went wrong</p>
153
+ </div>
154
+
155
+ <!-- Error Details -->
156
+ <div class="space-y-3 mb-8">
157
+ <div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md">
158
+ <p class="text-sm">{error}</p>
159
+ </div>
160
+ <p class="text-gray-600">Please try again or contact support if the issue persists.</p>
161
+ </div>
162
+
163
+ <!-- Close Button -->
164
+ <button
165
+ onclick="window.close()"
166
+ class="w-full py-2 px-4 bg-gray-600 text-white rounded-md hover:bg-gray-700 focus:ring-2 focus:ring-gray-500 transition-colors"
167
+ >
168
+ Close Tab
169
+ </button>
170
+
171
+ <!-- Lyceum Branding -->
172
+ <div class="mt-8 pt-6 border-t border-gray-200">
173
+ <p class="text-xs text-gray-400">Powered by Lyceum Technology</p>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </body>
178
+ </html>
179
+ """
180
+ try:
181
+ self.wfile.write(error_html.encode())
182
+ except (BrokenPipeError, ConnectionResetError):
183
+ # Browser closed connection
184
+ pass
185
+ else:
186
+ # Missing parameters
187
+ callback_result["error"] = "Missing token or error parameter"
188
+ callback_result["received"] = True
189
+
190
+ self.send_response(400)
191
+ self.send_header("Content-type", "text/html")
192
+ self.end_headers()
193
+ invalid_html = """
194
+ <!DOCTYPE html>
195
+ <html lang="en">
196
+ <head>
197
+ <meta charset="UTF-8">
198
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
199
+ <title>Lyceum CLI - Invalid Parameters</title>
200
+ <script src="https://cdn.tailwindcss.com"></script>
201
+ </head>
202
+ <body class="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
203
+ <div class="max-w-md w-full space-y-8">
204
+ <div class="bg-white rounded-lg shadow-md p-8 text-center">
205
+ <div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-yellow-100 mb-6">
206
+ <svg class="h-8 w-8 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
207
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.268 16c-.77.833.192 2.5 1.732 2.5z"></path>
208
+ </svg>
209
+ </div>
210
+ <h1 class="text-2xl font-bold text-gray-900 mb-2">Invalid Parameters</h1>
211
+ <p class="text-gray-600 mb-8">The authentication callback received invalid parameters.</p>
212
+ <button onclick="window.close()" class="w-full py-2 px-4 bg-gray-600 text-white rounded-md hover:bg-gray-700 focus:ring-2 focus:ring-gray-500 transition-colors">Close Tab</button>
213
+ </div>
214
+ </div>
215
+ </body>
216
+ </html>
217
+ """
218
+ try:
219
+ self.wfile.write(invalid_html.encode())
220
+ except (BrokenPipeError, ConnectionResetError):
221
+ # Browser closed connection
222
+ pass
223
+ else:
224
+ # Invalid path
225
+ self.send_response(404)
226
+ self.send_header("Content-type", "text/html")
227
+ self.end_headers()
228
+ notfound_html = """
229
+ <!DOCTYPE html>
230
+ <html lang="en">
231
+ <head>
232
+ <meta charset="UTF-8">
233
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
234
+ <title>Lyceum CLI - Page Not Found</title>
235
+ <script src="https://cdn.tailwindcss.com"></script>
236
+ </head>
237
+ <body class="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
238
+ <div class="max-w-md w-full space-y-8">
239
+ <div class="bg-white rounded-lg shadow-md p-8 text-center">
240
+ <div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-gray-100 mb-6">
241
+ <svg class="h-8 w-8 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
242
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.419-1.007-5.866-2.609C6.107 12.398 6 12.202 6 12s.107-.398.134-.609C7.581 10.007 9.66 9 12 9s4.419 1.007 5.866 2.609c.027.203.134.4.134.609s-.107.406-.134.609A7.962 7.962 0 0117 15z"></path>
243
+ </svg>
244
+ </div>
245
+ <h1 class="text-2xl font-bold text-gray-900 mb-2">Page Not Found</h1>
246
+ <p class="text-gray-600 mb-8">The requested page could not be found.</p>
247
+ <button onclick="window.close()" class="w-full py-2 px-4 bg-gray-600 text-white rounded-md hover:bg-gray-700 focus:ring-2 focus:ring-gray-500 transition-colors">Close Tab</button>
248
+ </div>
249
+ </div>
250
+ </body>
251
+ </html>
252
+ """
253
+ try:
254
+ self.wfile.write(notfound_html.encode())
255
+ except (BrokenPipeError, ConnectionResetError):
256
+ # Browser closed connection
257
+ pass
258
+
259
+ except Exception as e:
260
+ callback_result["error"] = str(e)
261
+ callback_result["received"] = True
262
+
263
+ self.send_response(500)
264
+ self.send_header("Content-type", "text/html")
265
+ self.end_headers()
266
+ server_error_html = f"""
267
+ <!DOCTYPE html>
268
+ <html lang="en">
269
+ <head>
270
+ <meta charset="UTF-8">
271
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
272
+ <title>Lyceum CLI - Server Error</title>
273
+ <script src="https://cdn.tailwindcss.com"></script>
274
+ </head>
275
+ <body class="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
276
+ <div class="max-w-md w-full space-y-8">
277
+ <div class="bg-white rounded-lg shadow-md p-8 text-center">
278
+ <div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-red-100 mb-6">
279
+ <svg class="h-8 w-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
280
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
281
+ </svg>
282
+ </div>
283
+ <h1 class="text-2xl font-bold text-gray-900 mb-2">Server Error</h1>
284
+ <p class="text-red-600 mb-4">An unexpected error occurred</p>
285
+ <div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md mb-8">
286
+ <p class="text-sm">{e}</p>
287
+ </div>
288
+ <button onclick="window.close()" class="w-full py-2 px-4 bg-gray-600 text-white rounded-md hover:bg-gray-700 focus:ring-2 focus:ring-gray-500 transition-colors">Close Tab</button>
289
+ </div>
290
+ </div>
291
+ </body>
292
+ </html>
293
+ """
294
+ try:
295
+ self.wfile.write(server_error_html.encode())
296
+ except (BrokenPipeError, ConnectionResetError):
297
+ # Browser closed connection
298
+ pass
299
+
300
+
301
+ def get_available_port():
302
+ """Find an available port for the callback server"""
303
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
304
+ s.bind(('', 0))
305
+ s.listen(1)
306
+ port = s.getsockname()[1]
307
+ return port
308
+
309
+
310
+ @auth_app.command("login")
311
+ def login(
312
+ base_url: Optional[str] = typer.Option(None, "--url", help="API base URL (for development)"),
313
+ dashboard_url: Optional[str] = typer.Option("https://dashboard.lyceum.technology", "--dashboard", help="Dashboard URL"),
314
+ manual: bool = typer.Option(False, "--manual", help="Use manual API key login instead of browser"),
315
+ api_key: Optional[str] = typer.Option(None, "--api-key", "-k", help="API key for manual login"),
316
+ ):
317
+ """Login to Lyceum via browser authentication"""
318
+ global callback_result
319
+
320
+ if manual:
321
+ # Legacy manual login
322
+ if not api_key:
323
+ api_key = typer.prompt("Enter your Lyceum API key", hide_input=True)
324
+
325
+ config.api_key = api_key
326
+ if base_url:
327
+ config.base_url = base_url
328
+ config.save()
329
+
330
+ # Test the connection
331
+ try:
332
+ import httpx
333
+ headers = {"Authorization": f"Bearer {config.api_key}"}
334
+ response = httpx.get(f"{config.base_url}/api/v2/external/machine-types", headers=headers, timeout=10.0)
335
+ if response.status_code == 200:
336
+ console.print("[green]✅ Successfully authenticated![/green]")
337
+ else:
338
+ console.print(f"[red]❌ Authentication failed: HTTP {response.status_code}[/red]")
339
+ raise typer.Exit(1)
340
+ except Exception as e:
341
+ console.print(f"[red]❌ Authentication failed: {e}[/red]")
342
+ raise typer.Exit(1)
343
+ return
344
+
345
+ # OAuth-style browser login
346
+ try:
347
+ if base_url:
348
+ config.base_url = base_url
349
+
350
+ # Reset callback result
351
+ callback_result = {"token": None, "error": None, "received": False}
352
+
353
+ # Start callback server
354
+ callback_port = get_available_port()
355
+ callback_server = HTTPServer(('localhost', callback_port), CallbackHandler)
356
+
357
+ console.print(f"[dim]Starting callback server on port {callback_port}...[/dim]")
358
+
359
+ # Start server in background thread
360
+ server_thread = threading.Thread(target=callback_server.serve_forever, daemon=True)
361
+ server_thread.start()
362
+
363
+ # Construct login URL
364
+ callback_url = f"http://localhost:{callback_port}/callback"
365
+ login_url = f"{dashboard_url}/cli-login?callback={callback_url}"
366
+
367
+ console.print("[cyan]🌐 Opening browser for authentication...[/cyan]")
368
+ console.print(f"[dim]If browser doesn't open, visit: {login_url}[/dim]")
369
+
370
+ # Open browser
371
+ if not webbrowser.open(login_url):
372
+ console.print("[yellow]⚠️ Could not open browser automatically[/yellow]")
373
+ console.print(f"[yellow]Please manually open: {login_url}[/yellow]")
374
+
375
+ console.print("[dim]Waiting for authentication... (timeout: 120 seconds)[/dim]")
376
+
377
+ # Wait for callback with timeout
378
+ timeout = 120 # 2 minutes
379
+ start_time = time.time()
380
+
381
+ while not callback_result["received"] and (time.time() - start_time) < timeout:
382
+ time.sleep(0.5)
383
+
384
+ # Stop server
385
+ callback_server.shutdown()
386
+ callback_server.server_close()
387
+
388
+ if callback_result["received"]:
389
+ if callback_result["token"]:
390
+ # Save token and test connection
391
+ config.api_key = callback_result["token"]
392
+ # Also save refresh_token if provided
393
+ if callback_result.get("refresh_token"):
394
+ config.refresh_token = callback_result["refresh_token"]
395
+ config.save()
396
+
397
+ console.print("[green]✅ Authentication token received![/green]")
398
+
399
+ # Test the connection using health endpoint
400
+ try:
401
+ import httpx
402
+ headers = {"Authorization": f"Bearer {config.api_key}"}
403
+ response = httpx.get(f"{config.base_url}/api/v2/external/machine-types", headers=headers, timeout=10.0)
404
+ if response.status_code == 200:
405
+ console.print("[green]✅ Successfully authenticated![/green]")
406
+ if callback_result.get("user"):
407
+ console.print(f"[dim]Logged in as: {callback_result['user']}[/dim]")
408
+ else:
409
+ console.print(f"[red]❌ Token validation failed: HTTP {response.status_code}[/red]")
410
+ raise typer.Exit(1)
411
+ except Exception as e:
412
+ console.print(f"[red]❌ Token validation failed: {e}[/red]")
413
+ raise typer.Exit(1)
414
+
415
+ elif callback_result["error"]:
416
+ console.print(f"[red]❌ Authentication failed: {callback_result['error']}[/red]")
417
+ raise typer.Exit(1)
418
+ else:
419
+ console.print("[red]❌ Authentication timed out. Please try again.[/red]")
420
+ raise typer.Exit(1)
421
+
422
+ except KeyboardInterrupt:
423
+ console.print("\n[yellow]Authentication cancelled by user.[/yellow]")
424
+ raise typer.Exit(1)
425
+ except Exception as e:
426
+ console.print(f"[red]❌ Authentication error: {e}[/red]")
427
+ raise typer.Exit(1)
428
+
429
+
430
+ @auth_app.command("logout")
431
+ def logout():
432
+ """Logout and remove stored credentials"""
433
+ from ...shared.config import CONFIG_FILE
434
+ if CONFIG_FILE.exists():
435
+ CONFIG_FILE.unlink()
436
+ console.print("[green]✅ Logged out successfully![/green]")
437
+
438
+
439
+ @auth_app.command("status")
440
+ def status():
441
+ """Show current configuration and authentication status"""
442
+ from ...shared.config import CONFIG_FILE
443
+ console.print(f"[dim]Config file: {CONFIG_FILE}[/dim]")
444
+ console.print(f"[dim]Base URL: {config.base_url}[/dim]")
445
+
446
+ if config.api_key:
447
+ console.print(f"[green]✅ Authenticated[/green]")
448
+ console.print(f"[dim]API Key: {config.api_key[:8]}...[/dim]")
449
+
450
+ # Test connection
451
+ try:
452
+ import httpx
453
+ headers = {"Authorization": f"Bearer {config.api_key}"}
454
+ response = httpx.get(f"{config.base_url}/api/v2/external/machine-types", headers=headers, timeout=10.0)
455
+ if response.status_code == 200:
456
+ console.print("[green]✅ API connection working[/green]")
457
+ else:
458
+ console.print(f"[yellow]⚠️ API connection issues: HTTP {response.status_code}[/yellow]")
459
+ except Exception as e:
460
+ console.print(f"[red]❌ API connection failed: {e}[/red]")
461
+ else:
462
+ console.print("[red]❌ Not authenticated. Run 'lyceum login' first.[/red]")
File without changes
File without changes
@@ -0,0 +1,99 @@
1
+ """
2
+ Python execution commands
3
+ """
4
+
5
+ from pathlib import Path
6
+ from typing import Optional
7
+ import typer
8
+ from rich.console import Console
9
+ import httpx
10
+
11
+ from ....shared.config import config
12
+ from ....shared.streaming import stream_execution_output
13
+
14
+ console = Console()
15
+
16
+ python_app = typer.Typer(name="python", help="Python execution commands")
17
+
18
+
19
+ @python_app.command("run")
20
+ def run_python(
21
+ code_or_file: str = typer.Argument(..., help="Python code to execute or path to Python file"),
22
+ machine_type: str = typer.Option("cpu", "--machine", "-m", help="Machine type (cpu, a100, h100, etc.)"),
23
+ timeout: int = typer.Option(60, "--timeout", "-t", help="Execution timeout in seconds"),
24
+ file_name: Optional[str] = typer.Option(None, "--file-name", "-f", help="Name for the execution"),
25
+ requirements: Optional[str] = typer.Option(None, "--requirements", "-r", help="Requirements file path or pip requirements string"),
26
+ imports: Optional[list[str]] = typer.Option(None, "--import", help="Pre-import modules (can be used multiple times)"),
27
+ ):
28
+ """Execute Python code or file on Lyceum Cloud"""
29
+ try:
30
+ # Check if it's a file path
31
+ code_to_execute = code_or_file
32
+ if Path(code_or_file).exists():
33
+ console.print(f"[dim]Reading code from file: {code_or_file}[/dim]")
34
+ with open(code_or_file, 'r') as f:
35
+ code_to_execute = f.read()
36
+ # Use filename as execution name if not provided
37
+ if not file_name:
38
+ file_name = Path(code_or_file).name
39
+
40
+ # Handle requirements
41
+ requirements_content = None
42
+ if requirements:
43
+ # Check if it's a file path
44
+ if Path(requirements).exists():
45
+ console.print(f"[dim]Reading requirements from file: {requirements}[/dim]")
46
+ with open(requirements, 'r') as f:
47
+ requirements_content = f.read()
48
+ else:
49
+ # Treat as direct pip requirements string
50
+ requirements_content = requirements
51
+
52
+ # Create execution request payload
53
+ payload = {
54
+ "code": code_to_execute,
55
+ "nbcode": 0,
56
+ "execution_type": machine_type,
57
+ "timeout": timeout,
58
+ }
59
+
60
+ if file_name:
61
+ payload["file_name"] = file_name
62
+ if requirements_content:
63
+ payload["requirements_content"] = requirements_content
64
+ if imports:
65
+ payload["prior_imports"] = imports
66
+
67
+ # Make API request
68
+ response = httpx.post(
69
+ f"{config.base_url}/api/v2/external/execution/streaming/start",
70
+ headers={"Authorization": f"Bearer {config.api_key}"},
71
+ json=payload,
72
+ timeout=30.0
73
+ )
74
+
75
+ if response.status_code != 200:
76
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
77
+ console.print(f"[red]{response.content.decode()}[/red]")
78
+ raise typer.Exit(1)
79
+
80
+ data = response.json()
81
+ execution_id = data['execution_id']
82
+ streaming_url = data.get('streaming_url')
83
+
84
+ console.print(f"[green]✅ Execution started![/green]")
85
+ console.print(f"[dim]Execution ID: {execution_id}[/dim]")
86
+
87
+ if 'pythia_decision' in data:
88
+ console.print(f"[dim]Pythia recommendation: {data['pythia_decision']}[/dim]")
89
+
90
+ # Stream the execution output
91
+ success = stream_execution_output(execution_id, streaming_url)
92
+
93
+ if not success:
94
+ console.print(f"[yellow]💡 You can check the execution later with: lyceum status[/yellow]")
95
+ raise typer.Exit(1)
96
+
97
+ except Exception as e:
98
+ console.print(f"[red]Error: {e}[/red]")
99
+ raise typer.Exit(1)
@@ -0,0 +1,2 @@
1
+ # Empty init - import modules directly in main.py to avoid circular imports
2
+ __all__ = []