hanzo 0.3.23__py3-none-any.whl → 0.3.25__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.

Potentially problematic release.


This version of hanzo might be problematic. Click here for more details.

@@ -0,0 +1,152 @@
1
+ """Router command for starting Hanzo router proxy."""
2
+
3
+ import os
4
+ import sys
5
+ import subprocess
6
+ from typing import Optional
7
+ from pathlib import Path
8
+
9
+ import click
10
+
11
+ from ..utils.output import console
12
+
13
+
14
+ @click.group(name="router")
15
+ def router_group():
16
+ """Manage Hanzo router (LLM proxy)."""
17
+ pass
18
+
19
+
20
+ @router_group.command(name="start")
21
+ @click.option("--port", "-p", default=4000, help="Port to run router on")
22
+ @click.option("--config", "-c", help="Config file path")
23
+ @click.option("--detach", "-d", is_flag=True, help="Run in background")
24
+ @click.pass_context
25
+ def start_router(ctx, port: int, config: Optional[str], detach: bool):
26
+ """Start the Hanzo router proxy server."""
27
+ # Find router directory
28
+ router_paths = [
29
+ Path.home() / "work" / "hanzo" / "router",
30
+ Path.home() / "hanzo" / "router",
31
+ Path.cwd().parent / "router",
32
+ ]
33
+
34
+ router_dir = None
35
+ for path in router_paths:
36
+ if path.exists() and (path / "litellm" / "proxy" / "proxy_server.py").exists():
37
+ router_dir = path
38
+ break
39
+
40
+ if not router_dir:
41
+ console.print("[red]Error:[/red] Hanzo router not found")
42
+ console.print("\nPlease clone the router:")
43
+ console.print(" git clone https://github.com/hanzoai/router.git ~/work/hanzo/router")
44
+ return
45
+
46
+ console.print(f"[green]✓[/green] Found router at {router_dir}")
47
+
48
+ # Prepare environment
49
+ env = os.environ.copy()
50
+ env["PYTHONPATH"] = str(router_dir) + ":" + env.get("PYTHONPATH", "")
51
+
52
+ # Build command
53
+ cmd = [
54
+ sys.executable,
55
+ "-m", "litellm.proxy.proxy_server",
56
+ "--port", str(port),
57
+ ]
58
+
59
+ if config:
60
+ # Use provided config
61
+ config_path = Path(config)
62
+ if not config_path.exists():
63
+ console.print(f"[red]Error:[/red] Config file not found: {config}")
64
+ return
65
+ cmd.extend(["--config", str(config_path)])
66
+ else:
67
+ # Check for default config
68
+ default_config = router_dir / "config.yaml"
69
+ if default_config.exists():
70
+ cmd.extend(["--config", str(default_config)])
71
+ console.print(f"[dim]Using config: {default_config}[/dim]")
72
+
73
+ console.print(f"\n[bold cyan]Starting Hanzo Router on port {port}[/bold cyan]")
74
+ console.print(f"API endpoint: http://localhost:{port}/v1")
75
+ console.print("\nPress Ctrl+C to stop\n")
76
+
77
+ try:
78
+ # Change to router directory and run
79
+ os.chdir(router_dir)
80
+
81
+ if detach:
82
+ # Run in background
83
+ process = subprocess.Popen(
84
+ cmd,
85
+ env=env,
86
+ stdout=subprocess.DEVNULL,
87
+ stderr=subprocess.DEVNULL,
88
+ start_new_session=True
89
+ )
90
+ console.print(f"[green]✓[/green] Router started in background (PID: {process.pid})")
91
+ console.print(f"Check status: curl http://localhost:{port}/health")
92
+ else:
93
+ # Run in foreground
94
+ subprocess.run(cmd, env=env)
95
+ except KeyboardInterrupt:
96
+ console.print("\n[yellow]Router stopped[/yellow]")
97
+ except Exception as e:
98
+ console.print(f"[red]Error starting router: {e}[/red]")
99
+
100
+
101
+ @router_group.command(name="stop")
102
+ @click.option("--port", "-p", default=4000, help="Port router is running on")
103
+ def stop_router(port: int):
104
+ """Stop the router."""
105
+ import signal
106
+
107
+ import psutil
108
+
109
+ found = False
110
+ for proc in psutil.process_iter(['pid', 'cmdline']):
111
+ try:
112
+ cmdline = proc.info['cmdline']
113
+ if cmdline and 'proxy_server' in ' '.join(cmdline) and str(port) in ' '.join(cmdline):
114
+ console.print(f"[yellow]Stopping router (PID: {proc.info['pid']})[/yellow]")
115
+ proc.send_signal(signal.SIGTERM)
116
+ proc.wait(timeout=5)
117
+ found = True
118
+ console.print("[green]✓[/green] Router stopped")
119
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.TimeoutExpired):
120
+ continue
121
+
122
+ if not found:
123
+ console.print(f"[yellow]No router found on port {port}[/yellow]")
124
+
125
+
126
+ @router_group.command(name="status")
127
+ @click.option("--port", "-p", default=4000, help="Port to check")
128
+ def router_status(port: int):
129
+ """Check router status."""
130
+ import httpx
131
+
132
+ try:
133
+ response = httpx.get(f"http://localhost:{port}/health", timeout=2.0)
134
+ if response.status_code == 200:
135
+ console.print(f"[green]✓[/green] Router is running on port {port}")
136
+
137
+ # Try to get models
138
+ try:
139
+ models_response = httpx.get(f"http://localhost:{port}/models", timeout=2.0)
140
+ if models_response.status_code == 200:
141
+ data = models_response.json()
142
+ if "data" in data:
143
+ console.print(f"Available models: {len(data['data'])}")
144
+ except Exception:
145
+ pass
146
+ else:
147
+ console.print(f"[yellow]Router responding but unhealthy (status: {response.status_code})[/yellow]")
148
+ except httpx.ConnectError:
149
+ console.print(f"[red]Router not running on port {port}[/red]")
150
+ console.print("\nStart with: hanzo router start")
151
+ except Exception as e:
152
+ console.print(f"[red]Error checking router: {e}[/red]")
hanzo/dev.py CHANGED
@@ -2359,7 +2359,7 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
2359
2359
  claude_available = True
2360
2360
  elif shutil.which("claude"):
2361
2361
  claude_available = True
2362
- except:
2362
+ except Exception:
2363
2363
  pass
2364
2364
 
2365
2365
  if not claude_available:
hanzo/fallback_handler.py CHANGED
@@ -44,7 +44,7 @@ class FallbackHandler:
44
44
  if response.status_code == 200:
45
45
  data = response.json()
46
46
  return len(data.get("models", [])) > 0
47
- except:
47
+ except Exception:
48
48
  pass
49
49
  return False
50
50
 
@@ -0,0 +1,513 @@
1
+ """Enhanced REPL with model selection and authentication."""
2
+
3
+ import os
4
+ import json
5
+ import httpx
6
+ import asyncio
7
+ from typing import Optional, Dict, Any
8
+ from pathlib import Path
9
+ from datetime import datetime
10
+
11
+ from rich.console import Console
12
+ from rich.markdown import Markdown
13
+ from rich.panel import Panel
14
+ from rich.table import Table
15
+ from rich.text import Text
16
+ from rich import box
17
+ from prompt_toolkit import PromptSession
18
+ from prompt_toolkit.history import FileHistory
19
+ from prompt_toolkit.completion import WordCompleter
20
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
21
+ from prompt_toolkit.formatted_text import HTML
22
+
23
+
24
+ class EnhancedHanzoREPL:
25
+ """Enhanced REPL with model selection and authentication."""
26
+
27
+ # Available models
28
+ MODELS = {
29
+ # OpenAI
30
+ "gpt-4": "OpenAI GPT-4",
31
+ "gpt-4-turbo": "OpenAI GPT-4 Turbo",
32
+ "gpt-3.5-turbo": "OpenAI GPT-3.5 Turbo",
33
+
34
+ # Anthropic
35
+ "claude-3-opus": "Anthropic Claude 3 Opus",
36
+ "claude-3-sonnet": "Anthropic Claude 3 Sonnet",
37
+ "claude-3-haiku": "Anthropic Claude 3 Haiku",
38
+ "claude-2.1": "Anthropic Claude 2.1",
39
+
40
+ # Google
41
+ "gemini-pro": "Google Gemini Pro",
42
+ "gemini-pro-vision": "Google Gemini Pro Vision",
43
+
44
+ # Meta
45
+ "llama2-70b": "Meta Llama 2 70B",
46
+ "llama2-13b": "Meta Llama 2 13B",
47
+ "llama2-7b": "Meta Llama 2 7B",
48
+ "codellama-34b": "Meta Code Llama 34B",
49
+
50
+ # Mistral
51
+ "mistral-medium": "Mistral Medium",
52
+ "mistral-small": "Mistral Small",
53
+ "mixtral-8x7b": "Mixtral 8x7B",
54
+
55
+ # Local models
56
+ "local:llama2": "Local Llama 2",
57
+ "local:mistral": "Local Mistral",
58
+ "local:phi-2": "Local Phi-2",
59
+ }
60
+
61
+ def __init__(self, console: Optional[Console] = None):
62
+ self.console = console or Console()
63
+ self.config_dir = Path.home() / ".hanzo"
64
+ self.config_file = self.config_dir / "config.json"
65
+ self.auth_file = self.config_dir / "auth.json"
66
+
67
+ # Load configuration
68
+ self.config = self.load_config()
69
+ self.auth = self.load_auth()
70
+
71
+ # Current model
72
+ self.current_model = self.config.get("default_model", "gpt-3.5-turbo")
73
+
74
+ # Setup session
75
+ self.session = PromptSession(
76
+ history=FileHistory(str(self.config_dir / ".repl_history")),
77
+ auto_suggest=AutoSuggestFromHistory(),
78
+ )
79
+
80
+ # Commands
81
+ self.commands = {
82
+ "help": self.show_help,
83
+ "exit": self.exit_repl,
84
+ "quit": self.exit_repl,
85
+ "clear": self.clear_screen,
86
+ "status": self.show_status,
87
+ "model": self.change_model,
88
+ "models": self.list_models,
89
+ "login": self.login,
90
+ "logout": self.logout,
91
+ "config": self.show_config,
92
+ }
93
+
94
+ self.running = False
95
+
96
+ def load_config(self) -> Dict[str, Any]:
97
+ """Load configuration from file."""
98
+ if self.config_file.exists():
99
+ try:
100
+ return json.loads(self.config_file.read_text())
101
+ except:
102
+ pass
103
+ return {}
104
+
105
+ def save_config(self):
106
+ """Save configuration to file."""
107
+ self.config_dir.mkdir(exist_ok=True)
108
+ self.config_file.write_text(json.dumps(self.config, indent=2))
109
+
110
+ def load_auth(self) -> Dict[str, Any]:
111
+ """Load authentication data."""
112
+ if self.auth_file.exists():
113
+ try:
114
+ return json.loads(self.auth_file.read_text())
115
+ except:
116
+ pass
117
+ return {}
118
+
119
+ def save_auth(self):
120
+ """Save authentication data."""
121
+ self.config_dir.mkdir(exist_ok=True)
122
+ self.auth_file.write_text(json.dumps(self.auth, indent=2))
123
+
124
+ def get_prompt(self) -> str:
125
+ """Get the simple prompt."""
126
+ # We'll use a simple > prompt, the box is handled by prompt_toolkit
127
+ return "> "
128
+
129
+ def is_authenticated(self) -> bool:
130
+ """Check if user is authenticated."""
131
+ # Check for API key
132
+ if os.getenv("HANZO_API_KEY"):
133
+ return True
134
+
135
+ # Check auth file
136
+ if self.auth.get("api_key"):
137
+ return True
138
+
139
+ # Check if logged in
140
+ if self.auth.get("logged_in"):
141
+ return True
142
+
143
+ return False
144
+
145
+ def get_model_info(self):
146
+ """Get current model info string."""
147
+ # Determine provider from model name
148
+ model = self.current_model
149
+ if model.startswith("gpt"):
150
+ provider = "openai"
151
+ elif model.startswith("claude"):
152
+ provider = "anthropic"
153
+ elif model.startswith("gemini"):
154
+ provider = "google"
155
+ elif model.startswith("llama") or model.startswith("codellama"):
156
+ provider = "meta"
157
+ elif model.startswith("mistral") or model.startswith("mixtral"):
158
+ provider = "mistral"
159
+ elif model.startswith("local:"):
160
+ provider = "local"
161
+ else:
162
+ provider = "unknown"
163
+
164
+ # Auth status
165
+ auth_status = "🔓" if self.is_authenticated() else "🔒"
166
+
167
+ return f"[dim]model: {provider}/{model} {auth_status}[/dim]"
168
+
169
+ async def run(self):
170
+ """Run the enhanced REPL."""
171
+ self.running = True
172
+
173
+ # Setup completer
174
+ commands = list(self.commands.keys())
175
+ models = list(self.MODELS.keys())
176
+ cli_commands = ["chat", "ask", "agent", "node", "mcp", "network",
177
+ "auth", "config", "tools", "miner", "serve", "net",
178
+ "dev", "router"]
179
+
180
+ completer = WordCompleter(
181
+ commands + models + cli_commands,
182
+ ignore_case=True,
183
+ )
184
+
185
+ while self.running:
186
+ try:
187
+ # Show model info above prompt
188
+ self.console.print(self.get_model_info())
189
+
190
+ # Get input with simple prompt
191
+ command = await self.session.prompt_async(
192
+ self.get_prompt(),
193
+ completer=completer
194
+ )
195
+
196
+ if not command.strip():
197
+ continue
198
+
199
+ # Handle slash commands
200
+ if command.startswith("/"):
201
+ await self.handle_slash_command(command[1:])
202
+ continue
203
+
204
+ # Parse command
205
+ parts = command.strip().split(maxsplit=1)
206
+ cmd = parts[0].lower()
207
+ args = parts[1] if len(parts) > 1 else ""
208
+
209
+ # Execute command
210
+ if cmd in self.commands:
211
+ await self.commands[cmd](args)
212
+ elif cmd in cli_commands:
213
+ await self.execute_command(cmd, args)
214
+ else:
215
+ # Treat as chat message
216
+ await self.chat_with_ai(command)
217
+
218
+ except KeyboardInterrupt:
219
+ continue
220
+ except EOFError:
221
+ break
222
+ except Exception as e:
223
+ self.console.print(f"[red]Error: {e}[/red]")
224
+
225
+ async def handle_slash_command(self, command: str):
226
+ """Handle slash commands like /model, /status, etc."""
227
+ parts = command.strip().split(maxsplit=1)
228
+ cmd = parts[0].lower()
229
+ args = parts[1] if len(parts) > 1 else ""
230
+
231
+ # Map slash commands to regular commands
232
+ slash_map = {
233
+ "m": "model",
234
+ "s": "status",
235
+ "h": "help",
236
+ "q": "quit",
237
+ "c": "clear",
238
+ "models": "models",
239
+ "login": "login",
240
+ "logout": "logout",
241
+ }
242
+
243
+ mapped_cmd = slash_map.get(cmd, cmd)
244
+
245
+ if mapped_cmd in self.commands:
246
+ await self.commands[mapped_cmd](args)
247
+ else:
248
+ self.console.print(f"[yellow]Unknown command: /{cmd}[/yellow]")
249
+ self.console.print("[dim]Type /help for available commands[/dim]")
250
+
251
+ async def show_status(self, args: str = ""):
252
+ """Show comprehensive status."""
253
+ # Create status table
254
+ table = Table(title="System Status", box=box.ROUNDED)
255
+ table.add_column("Component", style="cyan")
256
+ table.add_column("Status", style="green")
257
+ table.add_column("Details", style="dim")
258
+
259
+ # Authentication status
260
+ if self.is_authenticated():
261
+ auth_status = "✅ Authenticated"
262
+ auth_details = self.auth.get("email", "API Key configured")
263
+ else:
264
+ auth_status = "❌ Not authenticated"
265
+ auth_details = "Run /login to authenticate"
266
+ table.add_row("Authentication", auth_status, auth_details)
267
+
268
+ # Current model
269
+ model_name = self.MODELS.get(self.current_model, self.current_model)
270
+ table.add_row("Current Model", f"🤖 {self.current_model}", model_name)
271
+
272
+ # Router status
273
+ try:
274
+ response = httpx.get("http://localhost:4000/health", timeout=1)
275
+ router_status = "✅ Running" if response.status_code == 200 else "⚠️ Unhealthy"
276
+ router_details = "Port 4000"
277
+ except:
278
+ router_status = "❌ Offline"
279
+ router_details = "Run 'hanzo router start'"
280
+ table.add_row("Router", router_status, router_details)
281
+
282
+ # Node status
283
+ try:
284
+ response = httpx.get("http://localhost:8000/health", timeout=1)
285
+ node_status = "✅ Running" if response.status_code == 200 else "⚠️ Unhealthy"
286
+ node_details = "Port 8000"
287
+ except:
288
+ node_status = "❌ Offline"
289
+ node_details = "Run 'hanzo node start'"
290
+ table.add_row("Node", node_status, node_details)
291
+
292
+ # API endpoints
293
+ if os.getenv("HANZO_API_KEY"):
294
+ api_status = "✅ Configured"
295
+ api_details = "Using Hanzo Cloud API"
296
+ else:
297
+ api_status = "⚠️ Not configured"
298
+ api_details = "Set HANZO_API_KEY environment variable"
299
+ table.add_row("Cloud API", api_status, api_details)
300
+
301
+ self.console.print(table)
302
+
303
+ # Show additional info
304
+ if self.auth.get("last_login"):
305
+ self.console.print(f"\n[dim]Last login: {self.auth['last_login']}[/dim]")
306
+
307
+ async def change_model(self, args: str = ""):
308
+ """Change the current model."""
309
+ if not args:
310
+ # Show model selection menu
311
+ await self.list_models("")
312
+ self.console.print("\n[cyan]Enter model name or number:[/cyan]")
313
+
314
+ # Get selection
315
+ try:
316
+ selection = await self.session.prompt_async("> ")
317
+
318
+ # Handle numeric selection
319
+ if selection.isdigit():
320
+ models_list = list(self.MODELS.keys())
321
+ idx = int(selection) - 1
322
+ if 0 <= idx < len(models_list):
323
+ args = models_list[idx]
324
+ else:
325
+ self.console.print("[red]Invalid selection[/red]")
326
+ return
327
+ else:
328
+ args = selection
329
+ except (KeyboardInterrupt, EOFError):
330
+ return
331
+
332
+ # Validate model
333
+ if args not in self.MODELS and not args.startswith("local:"):
334
+ self.console.print(f"[red]Unknown model: {args}[/red]")
335
+ self.console.print("[dim]Use /models to see available models[/dim]")
336
+ return
337
+
338
+ # Change model
339
+ self.current_model = args
340
+ self.config["default_model"] = args
341
+ self.save_config()
342
+
343
+ model_name = self.MODELS.get(args, args)
344
+ self.console.print(f"[green]✅ Switched to {model_name}[/green]")
345
+
346
+ async def list_models(self, args: str = ""):
347
+ """List available models."""
348
+ table = Table(title="Available Models", box=box.ROUNDED)
349
+ table.add_column("#", style="dim")
350
+ table.add_column("Model ID", style="cyan")
351
+ table.add_column("Name", style="white")
352
+ table.add_column("Provider", style="yellow")
353
+
354
+ for i, (model_id, model_name) in enumerate(self.MODELS.items(), 1):
355
+ # Extract provider
356
+ if model_id.startswith("gpt"):
357
+ provider = "OpenAI"
358
+ elif model_id.startswith("claude"):
359
+ provider = "Anthropic"
360
+ elif model_id.startswith("gemini"):
361
+ provider = "Google"
362
+ elif model_id.startswith("llama") or model_id.startswith("codellama"):
363
+ provider = "Meta"
364
+ elif model_id.startswith("mistral") or model_id.startswith("mixtral"):
365
+ provider = "Mistral"
366
+ elif model_id.startswith("local:"):
367
+ provider = "Local"
368
+ else:
369
+ provider = "Other"
370
+
371
+ # Highlight current model
372
+ if model_id == self.current_model:
373
+ table.add_row(
374
+ str(i),
375
+ f"[bold green]→ {model_id}[/bold green]",
376
+ f"[bold]{model_name}[/bold]",
377
+ provider
378
+ )
379
+ else:
380
+ table.add_row(str(i), model_id, model_name, provider)
381
+
382
+ self.console.print(table)
383
+ self.console.print("\n[dim]Use /model <name> or /model <number> to switch[/dim]")
384
+
385
+ async def login(self, args: str = ""):
386
+ """Login to Hanzo."""
387
+ self.console.print("[cyan]Hanzo Authentication[/cyan]\n")
388
+
389
+ # Check if already logged in
390
+ if self.is_authenticated():
391
+ self.console.print("[yellow]Already authenticated[/yellow]")
392
+ if self.auth.get("email"):
393
+ self.console.print(f"Logged in as: {self.auth['email']}")
394
+ return
395
+
396
+ # Get credentials
397
+ try:
398
+ # Email
399
+ email = await self.session.prompt_async("Email: ")
400
+
401
+ # Password (hidden)
402
+ from prompt_toolkit import prompt
403
+ password = prompt("Password: ", is_password=True)
404
+
405
+ # Attempt login
406
+ self.console.print("\n[dim]Authenticating...[/dim]")
407
+
408
+ # TODO: Implement actual authentication
409
+ # For now, simulate successful login
410
+ await asyncio.sleep(1)
411
+
412
+ # Save auth
413
+ self.auth["email"] = email
414
+ self.auth["logged_in"] = True
415
+ self.auth["last_login"] = datetime.now().isoformat()
416
+ self.save_auth()
417
+
418
+ self.console.print("[green]✅ Successfully logged in![/green]")
419
+
420
+ except (KeyboardInterrupt, EOFError):
421
+ self.console.print("\n[yellow]Login cancelled[/yellow]")
422
+
423
+ async def logout(self, args: str = ""):
424
+ """Logout from Hanzo."""
425
+ if not self.is_authenticated():
426
+ self.console.print("[yellow]Not logged in[/yellow]")
427
+ return
428
+
429
+ # Clear auth
430
+ self.auth = {}
431
+ self.save_auth()
432
+
433
+ # Clear environment variable if set
434
+ if "HANZO_API_KEY" in os.environ:
435
+ del os.environ["HANZO_API_KEY"]
436
+
437
+ self.console.print("[green]✅ Successfully logged out[/green]")
438
+
439
+ async def show_config(self, args: str = ""):
440
+ """Show current configuration."""
441
+ config_text = json.dumps(self.config, indent=2)
442
+ self.console.print(Panel(config_text, title="Configuration", box=box.ROUNDED))
443
+
444
+ async def show_help(self, args: str = ""):
445
+ """Show enhanced help."""
446
+ help_text = """
447
+ # Hanzo Enhanced REPL
448
+
449
+ ## Slash Commands:
450
+ - `/model [name]` - Change AI model (or `/m`)
451
+ - `/models` - List available models
452
+ - `/status` - Show system status (or `/s`)
453
+ - `/login` - Login to Hanzo Cloud
454
+ - `/logout` - Logout from Hanzo
455
+ - `/config` - Show configuration
456
+ - `/help` - Show this help (or `/h`)
457
+ - `/clear` - Clear screen (or `/c`)
458
+ - `/quit` - Exit REPL (or `/q`)
459
+
460
+ ## Model Selection:
461
+ - Use `/model gpt-4` to switch to GPT-4
462
+ - Use `/model 3` to select model by number
463
+ - Current model shown in prompt: `hanzo [gpt] >`
464
+
465
+ ## Authentication:
466
+ - 🔓 = Authenticated (logged in or API key set)
467
+ - 🔒 = Not authenticated
468
+ - Use `/login` to authenticate with Hanzo Cloud
469
+
470
+ ## Tips:
471
+ - Type any message to chat with current model
472
+ - Use Tab for command completion
473
+ - Use Up/Down arrows for history
474
+ """
475
+ self.console.print(Markdown(help_text))
476
+
477
+ async def clear_screen(self, args: str = ""):
478
+ """Clear the screen."""
479
+ self.console.clear()
480
+
481
+ async def exit_repl(self, args: str = ""):
482
+ """Exit the REPL."""
483
+ self.running = False
484
+ self.console.print("[yellow]Goodbye! 👋[/yellow]")
485
+
486
+ async def execute_command(self, cmd: str, args: str):
487
+ """Execute a CLI command."""
488
+ # Import here to avoid circular imports
489
+ import subprocess
490
+
491
+ full_cmd = f"hanzo {cmd} {args}".strip()
492
+ self.console.print(f"[dim]Executing: {full_cmd}[/dim]")
493
+
494
+ try:
495
+ result = subprocess.run(
496
+ full_cmd,
497
+ shell=True,
498
+ capture_output=True,
499
+ text=True
500
+ )
501
+
502
+ if result.stdout:
503
+ self.console.print(result.stdout)
504
+ if result.stderr:
505
+ self.console.print(f"[red]{result.stderr}[/red]")
506
+
507
+ except Exception as e:
508
+ self.console.print(f"[red]Error executing command: {e}[/red]")
509
+
510
+ async def chat_with_ai(self, message: str):
511
+ """Chat with AI using current model."""
512
+ # Default to cloud mode to avoid needing local server
513
+ await self.execute_command("ask", f"--cloud --model {self.current_model} {message}")