hanzo 0.3.24__py3-none-any.whl → 0.3.26__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,166 @@
1
+ """Quick model selector with arrow key navigation."""
2
+
3
+ from typing import List, Optional, Tuple
4
+ from prompt_toolkit import Application
5
+ from prompt_toolkit.key_binding import KeyBindings
6
+ from prompt_toolkit.layout.containers import HSplit, Window
7
+ from prompt_toolkit.layout.controls import FormattedTextControl
8
+ from prompt_toolkit.layout.layout import Layout
9
+ from prompt_toolkit.widgets import Label
10
+ from rich.console import Console
11
+
12
+
13
+ class QuickModelSelector:
14
+ """Quick model selector with arrow navigation."""
15
+
16
+ def __init__(self, models: List[Tuple[str, str]], tools: List[Tuple[str, str]], current: str):
17
+ self.models = models
18
+ self.tools = tools
19
+ self.current = current
20
+ self.all_items = tools + models # Tools first, then models
21
+ self.selected_index = 0
22
+
23
+ # Find current selection
24
+ for i, (item_id, _) in enumerate(self.all_items):
25
+ if item_id == current:
26
+ self.selected_index = i
27
+ break
28
+
29
+ def get_display_lines(self) -> List[str]:
30
+ """Get display lines for the selector."""
31
+ lines = []
32
+
33
+ if self.tools:
34
+ lines.append("AI Coding Assistants:")
35
+ for i, (tool_id, tool_name) in enumerate(self.tools):
36
+ marker = "→ " if i == self.selected_index else " "
37
+ lines.append(f"{marker}{tool_name}")
38
+
39
+ if self.models:
40
+ if self.tools:
41
+ lines.append("") # Empty line
42
+ lines.append("Language Models:")
43
+
44
+ tool_count = len(self.tools)
45
+ for i, (model_id, model_name) in enumerate(self.models):
46
+ actual_idx = tool_count + i
47
+ marker = "→ " if actual_idx == self.selected_index else " "
48
+ lines.append(f"{marker}{model_name}")
49
+
50
+ return lines
51
+
52
+ def move_up(self):
53
+ """Move selection up."""
54
+ if self.selected_index > 0:
55
+ self.selected_index -= 1
56
+
57
+ def move_down(self):
58
+ """Move selection down."""
59
+ if self.selected_index < len(self.all_items) - 1:
60
+ self.selected_index += 1
61
+
62
+ def get_selected(self) -> Tuple[str, str]:
63
+ """Get the selected item."""
64
+ if 0 <= self.selected_index < len(self.all_items):
65
+ return self.all_items[self.selected_index]
66
+ return None, None
67
+
68
+ async def run(self) -> Optional[str]:
69
+ """Run the selector and return selected model/tool ID."""
70
+ kb = KeyBindings()
71
+
72
+ @kb.add('up')
73
+ def _(event):
74
+ self.move_up()
75
+ event.app.invalidate()
76
+
77
+ @kb.add('down')
78
+ def _(event):
79
+ self.move_down()
80
+ event.app.invalidate()
81
+
82
+ @kb.add('enter')
83
+ def _(event):
84
+ event.app.exit(result=self.get_selected()[0])
85
+
86
+ @kb.add('c-c')
87
+ @kb.add('escape')
88
+ def _(event):
89
+ event.app.exit(result=None)
90
+
91
+ def get_text():
92
+ lines = self.get_display_lines()
93
+ lines.append("")
94
+ lines.append("↑/↓: Navigate Enter: Select Esc: Cancel")
95
+ return "\n".join(lines)
96
+
97
+ layout = Layout(
98
+ Window(
99
+ FormattedTextControl(get_text),
100
+ wrap_lines=False
101
+ )
102
+ )
103
+
104
+ app = Application(
105
+ layout=layout,
106
+ key_bindings=kb,
107
+ full_screen=False,
108
+ mouse_support=True
109
+ )
110
+
111
+ return await app.run_async()
112
+
113
+
114
+ class BackgroundTaskManager:
115
+ """Manage background tasks."""
116
+
117
+ def __init__(self, console: Optional[Console] = None):
118
+ self.console = console or Console()
119
+ self.tasks = {} # task_id -> process
120
+ self.next_id = 1
121
+
122
+ def add_task(self, name: str, process):
123
+ """Add a background task."""
124
+ task_id = f"task_{self.next_id}"
125
+ self.next_id += 1
126
+ self.tasks[task_id] = {
127
+ "name": name,
128
+ "process": process,
129
+ "started": True
130
+ }
131
+ return task_id
132
+
133
+ def list_tasks(self):
134
+ """List all background tasks."""
135
+ if not self.tasks:
136
+ self.console.print("[dim]No background tasks running[/dim]")
137
+ return
138
+
139
+ self.console.print("[bold]Background Tasks:[/bold]")
140
+ for task_id, task in self.tasks.items():
141
+ status = "🟢 Running" if task["process"].poll() is None else "🔴 Stopped"
142
+ self.console.print(f" {task_id}: {task['name']} - {status}")
143
+
144
+ def kill_task(self, task_id: str):
145
+ """Kill a background task."""
146
+ if task_id in self.tasks:
147
+ task = self.tasks[task_id]
148
+ if task["process"].poll() is None:
149
+ task["process"].terminate()
150
+ self.console.print(f"[yellow]Terminated {task_id}: {task['name']}[/yellow]")
151
+ else:
152
+ self.console.print(f"[dim]Task {task_id} already stopped[/dim]")
153
+ del self.tasks[task_id]
154
+ else:
155
+ self.console.print(f"[red]Task {task_id} not found[/red]")
156
+
157
+ def kill_all(self):
158
+ """Kill all background tasks."""
159
+ if not self.tasks:
160
+ self.console.print("[dim]No tasks to kill[/dim]")
161
+ return
162
+
163
+ for task_id in list(self.tasks.keys()):
164
+ self.kill_task(task_id)
165
+
166
+ self.console.print("[green]All tasks terminated[/green]")
hanzo/interactive/repl.py CHANGED
@@ -45,9 +45,9 @@ class HanzoREPL:
45
45
 
46
46
  while self.running:
47
47
  try:
48
- # Get input
48
+ # Get input with simple prompt
49
49
  command = await self.session.prompt_async(
50
- "hanzo> ", completer=completer
50
+ "> ", completer=completer
51
51
  )
52
52
 
53
53
  if not command.strip():
@@ -0,0 +1,5 @@
1
+ """AI tools detection and management."""
2
+
3
+ from .detector import ToolDetector, AITool
4
+
5
+ __all__ = ["ToolDetector", "AITool"]
@@ -0,0 +1,291 @@
1
+ """Detect available AI coding tools and assistants."""
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import Dict, List, Optional, Tuple
8
+ from dataclasses import dataclass
9
+
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+ from rich import box
13
+
14
+
15
+ @dataclass
16
+ class AITool:
17
+ """Represents an AI coding tool."""
18
+ name: str
19
+ command: str
20
+ display_name: str
21
+ provider: str
22
+ priority: int # Lower is higher priority
23
+ check_command: Optional[str] = None
24
+ env_var: Optional[str] = None
25
+ api_endpoint: Optional[str] = None
26
+ detected: bool = False
27
+ version: Optional[str] = None
28
+ path: Optional[str] = None
29
+
30
+
31
+ class ToolDetector:
32
+ """Detect and manage available AI coding tools."""
33
+
34
+ # Define available tools with priority order
35
+ TOOLS = [
36
+ AITool(
37
+ name="claude-code",
38
+ command="claude",
39
+ display_name="Claude Code",
40
+ provider="anthropic",
41
+ priority=1,
42
+ check_command="claude --version",
43
+ env_var="ANTHROPIC_API_KEY"
44
+ ),
45
+ AITool(
46
+ name="hanzo-dev",
47
+ command="hanzo dev",
48
+ display_name="Hanzo Dev (Native)",
49
+ provider="hanzo",
50
+ priority=2,
51
+ check_command="hanzo --version",
52
+ env_var="HANZO_API_KEY"
53
+ ),
54
+ AITool(
55
+ name="openai-codex",
56
+ command="openai",
57
+ display_name="OpenAI Codex",
58
+ provider="openai",
59
+ priority=3,
60
+ check_command="openai --version",
61
+ env_var="OPENAI_API_KEY"
62
+ ),
63
+ AITool(
64
+ name="gemini-cli",
65
+ command="gemini",
66
+ display_name="Gemini CLI",
67
+ provider="google",
68
+ priority=4,
69
+ check_command="gemini --version",
70
+ env_var="GEMINI_API_KEY"
71
+ ),
72
+ AITool(
73
+ name="grok-cli",
74
+ command="grok",
75
+ display_name="Grok CLI",
76
+ provider="xai",
77
+ priority=5,
78
+ check_command="grok --version",
79
+ env_var="GROK_API_KEY"
80
+ ),
81
+ AITool(
82
+ name="openhands",
83
+ command="openhands",
84
+ display_name="OpenHands CLI",
85
+ provider="openhands",
86
+ priority=6,
87
+ check_command="openhands --version",
88
+ env_var=None
89
+ ),
90
+ AITool(
91
+ name="cursor",
92
+ command="cursor",
93
+ display_name="Cursor AI",
94
+ provider="cursor",
95
+ priority=7,
96
+ check_command="cursor --version",
97
+ env_var=None
98
+ ),
99
+ AITool(
100
+ name="codeium",
101
+ command="codeium",
102
+ display_name="Codeium",
103
+ provider="codeium",
104
+ priority=8,
105
+ check_command="codeium --version",
106
+ env_var="CODEIUM_API_KEY"
107
+ ),
108
+ AITool(
109
+ name="aider",
110
+ command="aider",
111
+ display_name="Aider",
112
+ provider="aider",
113
+ priority=9,
114
+ check_command="aider --version",
115
+ env_var=None
116
+ ),
117
+ AITool(
118
+ name="continue",
119
+ command="continue",
120
+ display_name="Continue Dev",
121
+ provider="continue",
122
+ priority=10,
123
+ check_command="continue --version",
124
+ env_var=None
125
+ )
126
+ ]
127
+
128
+ def __init__(self, console: Optional[Console] = None):
129
+ self.console = console or Console()
130
+ self.detected_tools: List[AITool] = []
131
+
132
+ def detect_all(self) -> List[AITool]:
133
+ """Detect all available AI tools."""
134
+ self.detected_tools = []
135
+
136
+ for tool in self.TOOLS:
137
+ if self.detect_tool(tool):
138
+ self.detected_tools.append(tool)
139
+
140
+ # Sort by priority
141
+ self.detected_tools.sort(key=lambda t: t.priority)
142
+ return self.detected_tools
143
+
144
+ def detect_tool(self, tool: AITool) -> bool:
145
+ """Detect if a specific tool is available."""
146
+ # Check if command exists
147
+ tool.path = shutil.which(tool.command.split()[0])
148
+ if tool.path:
149
+ tool.detected = True
150
+
151
+ # Try to get version
152
+ if tool.check_command:
153
+ try:
154
+ result = subprocess.run(
155
+ tool.check_command.split(),
156
+ capture_output=True,
157
+ text=True,
158
+ timeout=2
159
+ )
160
+ if result.returncode == 0:
161
+ tool.version = result.stdout.strip().split()[-1]
162
+ except:
163
+ pass
164
+
165
+ return True
166
+
167
+ # Check environment variable as fallback
168
+ if tool.env_var and os.getenv(tool.env_var):
169
+ tool.detected = True
170
+ return True
171
+
172
+ return False
173
+
174
+ def get_default_tool(self) -> Optional[AITool]:
175
+ """Get the default tool based on priority and availability."""
176
+ if not self.detected_tools:
177
+ self.detect_all()
178
+
179
+ if self.detected_tools:
180
+ return self.detected_tools[0]
181
+ return None
182
+
183
+ def get_tool_by_name(self, name: str) -> Optional[AITool]:
184
+ """Get a specific tool by name."""
185
+ for tool in self.TOOLS:
186
+ if tool.name == name or tool.display_name.lower() == name.lower():
187
+ if self.detect_tool(tool):
188
+ return tool
189
+ return None
190
+
191
+ def show_available_tools(self):
192
+ """Display available tools in a table."""
193
+ self.detect_all()
194
+
195
+ table = Table(title="Available AI Coding Tools", box=box.ROUNDED)
196
+ table.add_column("#", style="dim")
197
+ table.add_column("Tool", style="cyan")
198
+ table.add_column("Provider", style="yellow")
199
+ table.add_column("Status", style="green")
200
+ table.add_column("Version", style="blue")
201
+ table.add_column("Priority", style="magenta")
202
+
203
+ for i, tool in enumerate(self.TOOLS, 1):
204
+ status = "✅ Available" if tool.detected else "❌ Not Found"
205
+ version = tool.version or "Unknown" if tool.detected else "-"
206
+
207
+ # Highlight the default tool
208
+ if tool.detected and tool == self.detected_tools[0] if self.detected_tools else False:
209
+ table.add_row(
210
+ str(i),
211
+ f"[bold green]→ {tool.display_name}[/bold green]",
212
+ tool.provider,
213
+ status,
214
+ version,
215
+ str(tool.priority)
216
+ )
217
+ else:
218
+ table.add_row(
219
+ str(i),
220
+ tool.display_name,
221
+ tool.provider,
222
+ status,
223
+ version,
224
+ str(tool.priority)
225
+ )
226
+
227
+ self.console.print(table)
228
+
229
+ if self.detected_tools:
230
+ default = self.detected_tools[0]
231
+ self.console.print(f"\n[green]Default tool: {default.display_name}[/green]")
232
+ else:
233
+ self.console.print("\n[yellow]No AI coding tools detected.[/yellow]")
234
+ self.console.print("[dim]Install Claude Code, OpenAI CLI, or other tools to enable AI features.[/dim]")
235
+
236
+ def get_tool_command(self, tool: AITool, prompt: str) -> List[str]:
237
+ """Get the command to execute for a tool with a prompt."""
238
+ if tool.name == "claude-code":
239
+ return ["claude", prompt]
240
+ elif tool.name == "hanzo-dev":
241
+ return ["hanzo", "dev", "--prompt", prompt]
242
+ elif tool.name == "openai-codex":
243
+ return ["openai", "api", "completions.create", "-m", "code-davinci-002", "-p", prompt]
244
+ elif tool.name == "gemini-cli":
245
+ return ["gemini", "generate", "--prompt", prompt]
246
+ elif tool.name == "grok-cli":
247
+ return ["grok", "complete", prompt]
248
+ elif tool.name == "openhands":
249
+ return ["openhands", "run", prompt]
250
+ elif tool.name == "cursor":
251
+ return ["cursor", "--prompt", prompt]
252
+ elif tool.name == "aider":
253
+ return ["aider", "--message", prompt]
254
+ else:
255
+ return [tool.command, prompt]
256
+
257
+ def execute_with_tool(self, tool: AITool, prompt: str) -> Tuple[bool, str]:
258
+ """Execute a prompt with a specific tool."""
259
+ try:
260
+ command = self.get_tool_command(tool, prompt)
261
+ result = subprocess.run(
262
+ command,
263
+ capture_output=True,
264
+ text=True,
265
+ timeout=30
266
+ )
267
+
268
+ if result.returncode == 0:
269
+ return True, result.stdout
270
+ else:
271
+ return False, result.stderr or "Command failed"
272
+ except subprocess.TimeoutExpired:
273
+ return False, "Command timed out"
274
+ except Exception as e:
275
+ return False, str(e)
276
+
277
+ def execute_with_fallback(self, prompt: str) -> Tuple[bool, str, AITool]:
278
+ """Execute with fallback through available tools."""
279
+ if not self.detected_tools:
280
+ self.detect_all()
281
+
282
+ for tool in self.detected_tools:
283
+ self.console.print(f"[dim]Trying {tool.display_name}...[/dim]")
284
+ success, output = self.execute_with_tool(tool, prompt)
285
+
286
+ if success:
287
+ return True, output, tool
288
+ else:
289
+ self.console.print(f"[yellow]{tool.display_name} failed: {output}[/yellow]")
290
+
291
+ return False, "No available tools could handle the request", None
hanzo/ui/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ Hanzo UI components for CLI.
3
+ """
4
+
5
+ from .startup import show_startup, StartupUI
6
+ from .inline_startup import show_inline_startup, show_status
7
+
8
+ __all__ = [
9
+ "show_startup",
10
+ "StartupUI",
11
+ "show_inline_startup",
12
+ "show_status"
13
+ ]
@@ -0,0 +1,136 @@
1
+ """
2
+ Inline startup notifications for Hanzo commands.
3
+ """
4
+
5
+ import os
6
+ import json
7
+ from pathlib import Path
8
+ from datetime import datetime, timedelta
9
+ from typing import Optional
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.text import Text
13
+ from rich import box
14
+
15
+ console = Console()
16
+
17
+
18
+ class InlineStartup:
19
+ """Lightweight inline startup notifications."""
20
+
21
+ def __init__(self):
22
+ self.config_dir = Path.home() / ".hanzo"
23
+ self.last_shown_file = self.config_dir / ".last_inline_shown"
24
+ self.show_interval = timedelta(hours=24) # Show once per day
25
+
26
+ def should_show(self) -> bool:
27
+ """Check if we should show inline startup."""
28
+ # Check environment variable
29
+ if os.environ.get("HANZO_NO_STARTUP") == "1":
30
+ return False
31
+
32
+ # Check last shown time
33
+ if self.last_shown_file.exists():
34
+ try:
35
+ last_shown = datetime.fromisoformat(
36
+ self.last_shown_file.read_text().strip()
37
+ )
38
+ if datetime.now() - last_shown < self.show_interval:
39
+ return False
40
+ except:
41
+ pass
42
+
43
+ return True
44
+
45
+ def mark_shown(self):
46
+ """Mark inline startup as shown."""
47
+ self.config_dir.mkdir(exist_ok=True)
48
+ self.last_shown_file.write_text(datetime.now().isoformat())
49
+
50
+ def show_mini(self, command: str = None):
51
+ """Show mini inline startup."""
52
+ if not self.should_show():
53
+ return
54
+
55
+ # Build message
56
+ message = Text()
57
+ message.append("✨ ", style="yellow")
58
+ message.append("Hanzo AI ", style="bold cyan")
59
+ message.append("v0.3.23", style="green")
60
+
61
+ # Add what's new teaser
62
+ message.append(" • ", style="dim")
63
+ message.append("What's new: ", style="dim")
64
+ message.append("Router management, improved docs", style="yellow dim")
65
+
66
+ # Show panel
67
+ console.print(
68
+ Panel(
69
+ message,
70
+ box=box.MINIMAL,
71
+ border_style="cyan",
72
+ padding=(0, 1)
73
+ )
74
+ )
75
+
76
+ self.mark_shown()
77
+
78
+ def show_command_hint(self, command: str):
79
+ """Show command-specific hints."""
80
+ hints = {
81
+ "chat": "💡 Tip: Use --model to change AI model, --router for local proxy",
82
+ "node": "💡 Tip: Run 'hanzo node start' to enable local AI inference",
83
+ "router": "💡 Tip: Router provides unified access to 100+ LLM providers",
84
+ "repl": "💡 Tip: REPL combines Python with AI assistance",
85
+ "agent": "💡 Tip: Agents can work in parallel with 'hanzo agent swarm'"
86
+ }
87
+
88
+ hint = hints.get(command)
89
+ if hint and os.environ.get("HANZO_SHOW_HINTS") != "0":
90
+ console.print(f"[dim]{hint}[/dim]")
91
+
92
+ def show_status_bar(self):
93
+ """Show a compact status bar."""
94
+ items = []
95
+
96
+ # Check router
97
+ try:
98
+ import httpx
99
+ response = httpx.get("http://localhost:4000/health", timeout=0.5)
100
+ if response.status_code == 200:
101
+ items.append("[green]Router ✓[/green]")
102
+ except:
103
+ pass
104
+
105
+ # Check node
106
+ try:
107
+ import httpx
108
+ response = httpx.get("http://localhost:8000/health", timeout=0.5)
109
+ if response.status_code == 200:
110
+ items.append("[green]Node ✓[/green]")
111
+ except:
112
+ pass
113
+
114
+ # Check API key
115
+ if os.environ.get("HANZO_API_KEY"):
116
+ items.append("[green]API ✓[/green]")
117
+ else:
118
+ items.append("[yellow]API ⚠[/yellow]")
119
+
120
+ if items:
121
+ status = " • ".join(items)
122
+ console.print(f"[dim]Status: {status}[/dim]")
123
+
124
+
125
+ def show_inline_startup(command: str = None):
126
+ """Show inline startup notification."""
127
+ startup = InlineStartup()
128
+ startup.show_mini(command)
129
+ if command:
130
+ startup.show_command_hint(command)
131
+
132
+
133
+ def show_status():
134
+ """Show compact status bar."""
135
+ startup = InlineStartup()
136
+ startup.show_status_bar()