hanzo 0.3.26__py3-none-any.whl → 0.3.28__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.
- hanzo/__init__.py +1 -1
- hanzo/interactive/enhanced_repl.py +226 -1
- hanzo/interactive/todo_manager.py +457 -0
- hanzo/tools/detector.py +127 -31
- {hanzo-0.3.26.dist-info → hanzo-0.3.28.dist-info}/METADATA +1 -1
- {hanzo-0.3.26.dist-info → hanzo-0.3.28.dist-info}/RECORD +8 -7
- {hanzo-0.3.26.dist-info → hanzo-0.3.28.dist-info}/WHEEL +0 -0
- {hanzo-0.3.26.dist-info → hanzo-0.3.28.dist-info}/entry_points.txt +0 -0
hanzo/__init__.py
CHANGED
|
@@ -32,6 +32,11 @@ except ImportError:
|
|
|
32
32
|
QuickModelSelector = None
|
|
33
33
|
BackgroundTaskManager = None
|
|
34
34
|
|
|
35
|
+
try:
|
|
36
|
+
from .todo_manager import TodoManager
|
|
37
|
+
except ImportError:
|
|
38
|
+
TodoManager = None
|
|
39
|
+
|
|
35
40
|
|
|
36
41
|
class EnhancedHanzoREPL:
|
|
37
42
|
"""Enhanced REPL with model selection and authentication."""
|
|
@@ -99,6 +104,9 @@ class EnhancedHanzoREPL:
|
|
|
99
104
|
# Initialize background task manager
|
|
100
105
|
self.task_manager = BackgroundTaskManager(console) if BackgroundTaskManager else None
|
|
101
106
|
|
|
107
|
+
# Initialize todo manager
|
|
108
|
+
self.todo_manager = TodoManager(console) if TodoManager else None
|
|
109
|
+
|
|
102
110
|
# Detect available tools and set default
|
|
103
111
|
if self.tool_detector:
|
|
104
112
|
self.detected_tools = self.tool_detector.detect_all()
|
|
@@ -139,6 +147,8 @@ class EnhancedHanzoREPL:
|
|
|
139
147
|
"tasks": self.show_tasks,
|
|
140
148
|
"kill": self.kill_task,
|
|
141
149
|
"quick": self.quick_model_select,
|
|
150
|
+
"todo": self.manage_todos,
|
|
151
|
+
"todos": self.manage_todos, # Alias
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
self.running = False
|
|
@@ -295,6 +305,9 @@ class EnhancedHanzoREPL:
|
|
|
295
305
|
"models": "models",
|
|
296
306
|
"login": "login",
|
|
297
307
|
"logout": "logout",
|
|
308
|
+
"todo": "todo",
|
|
309
|
+
"todos": "todos",
|
|
310
|
+
"t": "todo", # Shortcut for todo
|
|
298
311
|
}
|
|
299
312
|
|
|
300
313
|
mapped_cmd = slash_map.get(cmd, cmd)
|
|
@@ -550,6 +563,7 @@ class EnhancedHanzoREPL:
|
|
|
550
563
|
# Hanzo Enhanced REPL
|
|
551
564
|
|
|
552
565
|
## Slash Commands:
|
|
566
|
+
- `/todo [cmd]` - Manage todos (see `/todo help`)
|
|
553
567
|
- `/model [name]` - Change AI model (or `/m`)
|
|
554
568
|
- `/models` - List available models
|
|
555
569
|
- `/tools` - List available AI tools
|
|
@@ -685,4 +699,215 @@ class EnhancedHanzoREPL:
|
|
|
685
699
|
else:
|
|
686
700
|
self.task_manager.kill_task(task_id)
|
|
687
701
|
except (KeyboardInterrupt, EOFError):
|
|
688
|
-
pass
|
|
702
|
+
pass
|
|
703
|
+
|
|
704
|
+
async def manage_todos(self, args: str = ""):
|
|
705
|
+
"""Manage todos."""
|
|
706
|
+
if not self.todo_manager:
|
|
707
|
+
self.console.print("[yellow]Todo manager not available[/yellow]")
|
|
708
|
+
return
|
|
709
|
+
|
|
710
|
+
# Parse command
|
|
711
|
+
parts = args.strip().split(maxsplit=1)
|
|
712
|
+
|
|
713
|
+
if not parts:
|
|
714
|
+
# Show todos
|
|
715
|
+
self.todo_manager.display_todos()
|
|
716
|
+
return
|
|
717
|
+
|
|
718
|
+
subcommand = parts[0].lower()
|
|
719
|
+
rest = parts[1] if len(parts) > 1 else ""
|
|
720
|
+
|
|
721
|
+
# Handle subcommands
|
|
722
|
+
if subcommand in ["add", "a", "+"]:
|
|
723
|
+
# Add todo
|
|
724
|
+
if rest:
|
|
725
|
+
# Quick add
|
|
726
|
+
try:
|
|
727
|
+
todo = self.todo_manager.quick_add(rest)
|
|
728
|
+
self.console.print(f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]")
|
|
729
|
+
except ValueError as e:
|
|
730
|
+
self.console.print(f"[red]Error: {e}[/red]")
|
|
731
|
+
else:
|
|
732
|
+
# Interactive add
|
|
733
|
+
await self.add_todo_interactive()
|
|
734
|
+
|
|
735
|
+
elif subcommand in ["list", "ls", "l"]:
|
|
736
|
+
# List todos with optional filter
|
|
737
|
+
filter_parts = rest.split()
|
|
738
|
+
status = None
|
|
739
|
+
priority = None
|
|
740
|
+
tag = None
|
|
741
|
+
|
|
742
|
+
for i in range(0, len(filter_parts), 2):
|
|
743
|
+
if i + 1 < len(filter_parts):
|
|
744
|
+
key = filter_parts[i]
|
|
745
|
+
value = filter_parts[i + 1]
|
|
746
|
+
|
|
747
|
+
if key in ["status", "s"]:
|
|
748
|
+
status = value
|
|
749
|
+
elif key in ["priority", "p"]:
|
|
750
|
+
priority = value
|
|
751
|
+
elif key in ["tag", "t"]:
|
|
752
|
+
tag = value
|
|
753
|
+
|
|
754
|
+
todos = self.todo_manager.list_todos(status=status, priority=priority, tag=tag)
|
|
755
|
+
title = "Filtered Todos" if (status or priority or tag) else "All Todos"
|
|
756
|
+
self.todo_manager.display_todos(todos, title)
|
|
757
|
+
|
|
758
|
+
elif subcommand in ["done", "d", "complete", "finish"]:
|
|
759
|
+
# Mark as done
|
|
760
|
+
if rest:
|
|
761
|
+
todo = self.todo_manager.update_todo(rest, status="done")
|
|
762
|
+
if todo:
|
|
763
|
+
self.console.print(f"[green]✅ Marked as done: {todo.title}[/green]")
|
|
764
|
+
else:
|
|
765
|
+
self.console.print(f"[red]Todo not found: {rest}[/red]")
|
|
766
|
+
else:
|
|
767
|
+
self.console.print("[yellow]Usage: /todo done <id>[/yellow]")
|
|
768
|
+
|
|
769
|
+
elif subcommand in ["start", "begin", "progress"]:
|
|
770
|
+
# Mark as in progress
|
|
771
|
+
if rest:
|
|
772
|
+
todo = self.todo_manager.update_todo(rest, status="in_progress")
|
|
773
|
+
if todo:
|
|
774
|
+
self.console.print(f"[cyan]🔄 Started: {todo.title}[/cyan]")
|
|
775
|
+
else:
|
|
776
|
+
self.console.print(f"[red]Todo not found: {rest}[/red]")
|
|
777
|
+
else:
|
|
778
|
+
self.console.print("[yellow]Usage: /todo start <id>[/yellow]")
|
|
779
|
+
|
|
780
|
+
elif subcommand in ["cancel", "x"]:
|
|
781
|
+
# Cancel todo
|
|
782
|
+
if rest:
|
|
783
|
+
todo = self.todo_manager.update_todo(rest, status="cancelled")
|
|
784
|
+
if todo:
|
|
785
|
+
self.console.print(f"[red]❌ Cancelled: {todo.title}[/red]")
|
|
786
|
+
else:
|
|
787
|
+
self.console.print(f"[red]Todo not found: {rest}[/red]")
|
|
788
|
+
else:
|
|
789
|
+
self.console.print("[yellow]Usage: /todo cancel <id>[/yellow]")
|
|
790
|
+
|
|
791
|
+
elif subcommand in ["delete", "del", "rm", "remove"]:
|
|
792
|
+
# Delete todo
|
|
793
|
+
if rest:
|
|
794
|
+
if self.todo_manager.delete_todo(rest):
|
|
795
|
+
self.console.print(f"[green]✅ Deleted todo: {rest}[/green]")
|
|
796
|
+
else:
|
|
797
|
+
self.console.print(f"[red]Todo not found: {rest}[/red]")
|
|
798
|
+
else:
|
|
799
|
+
self.console.print("[yellow]Usage: /todo delete <id>[/yellow]")
|
|
800
|
+
|
|
801
|
+
elif subcommand in ["view", "show", "detail"]:
|
|
802
|
+
# View todo detail
|
|
803
|
+
if rest:
|
|
804
|
+
todo = self.todo_manager.get_todo(rest)
|
|
805
|
+
if todo:
|
|
806
|
+
self.todo_manager.display_todo_detail(todo)
|
|
807
|
+
else:
|
|
808
|
+
self.console.print(f"[red]Todo not found: {rest}[/red]")
|
|
809
|
+
else:
|
|
810
|
+
self.console.print("[yellow]Usage: /todo view <id>[/yellow]")
|
|
811
|
+
|
|
812
|
+
elif subcommand in ["stats", "statistics"]:
|
|
813
|
+
# Show statistics
|
|
814
|
+
self.todo_manager.display_statistics()
|
|
815
|
+
|
|
816
|
+
elif subcommand in ["clear", "reset"]:
|
|
817
|
+
# Clear all todos (with confirmation)
|
|
818
|
+
try:
|
|
819
|
+
confirm = await self.session.prompt_async("Are you sure you want to delete ALL todos? (yes/no): ")
|
|
820
|
+
if confirm.lower() in ["yes", "y"]:
|
|
821
|
+
self.todo_manager.todos = []
|
|
822
|
+
self.todo_manager.save_todos()
|
|
823
|
+
self.console.print("[green]✅ All todos cleared[/green]")
|
|
824
|
+
else:
|
|
825
|
+
self.console.print("[yellow]Cancelled[/yellow]")
|
|
826
|
+
except (KeyboardInterrupt, EOFError):
|
|
827
|
+
self.console.print("[yellow]Cancelled[/yellow]")
|
|
828
|
+
|
|
829
|
+
elif subcommand in ["help", "h", "?"]:
|
|
830
|
+
# Show todo help
|
|
831
|
+
self.show_todo_help()
|
|
832
|
+
|
|
833
|
+
else:
|
|
834
|
+
# Unknown subcommand, treat as quick add
|
|
835
|
+
try:
|
|
836
|
+
todo = self.todo_manager.quick_add(args)
|
|
837
|
+
self.console.print(f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]")
|
|
838
|
+
except ValueError:
|
|
839
|
+
self.console.print(f"[yellow]Unknown todo command: {subcommand}[/yellow]")
|
|
840
|
+
self.console.print("[dim]Use /todo help for available commands[/dim]")
|
|
841
|
+
|
|
842
|
+
async def add_todo_interactive(self):
|
|
843
|
+
"""Add todo interactively."""
|
|
844
|
+
try:
|
|
845
|
+
# Get title
|
|
846
|
+
title = await self.session.prompt_async("Title: ")
|
|
847
|
+
if not title:
|
|
848
|
+
self.console.print("[yellow]Cancelled[/yellow]")
|
|
849
|
+
return
|
|
850
|
+
|
|
851
|
+
# Get description
|
|
852
|
+
description = await self.session.prompt_async("Description (optional): ")
|
|
853
|
+
|
|
854
|
+
# Get priority
|
|
855
|
+
priority = await self.session.prompt_async("Priority (low/medium/high/urgent) [medium]: ")
|
|
856
|
+
if not priority:
|
|
857
|
+
priority = "medium"
|
|
858
|
+
|
|
859
|
+
# Get tags
|
|
860
|
+
tags_input = await self.session.prompt_async("Tags (comma-separated, optional): ")
|
|
861
|
+
tags = [t.strip() for t in tags_input.split(",") if t.strip()] if tags_input else []
|
|
862
|
+
|
|
863
|
+
# Get due date
|
|
864
|
+
due_date = await self.session.prompt_async("Due date (optional): ")
|
|
865
|
+
|
|
866
|
+
# Add todo
|
|
867
|
+
todo = self.todo_manager.add_todo(
|
|
868
|
+
title=title,
|
|
869
|
+
description=description,
|
|
870
|
+
priority=priority,
|
|
871
|
+
tags=tags,
|
|
872
|
+
due_date=due_date if due_date else None
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
self.console.print(f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]")
|
|
876
|
+
|
|
877
|
+
except (KeyboardInterrupt, EOFError):
|
|
878
|
+
self.console.print("[yellow]Cancelled[/yellow]")
|
|
879
|
+
|
|
880
|
+
def show_todo_help(self):
|
|
881
|
+
"""Show todo help."""
|
|
882
|
+
help_text = """
|
|
883
|
+
[bold cyan]Todo Management[/bold cyan]
|
|
884
|
+
|
|
885
|
+
[bold]Quick Add:[/bold]
|
|
886
|
+
/todo Buy milk #shopping !high @tomorrow
|
|
887
|
+
Format: title #tag1 #tag2 !priority @due_date
|
|
888
|
+
|
|
889
|
+
[bold]Commands:[/bold]
|
|
890
|
+
/todo - List all todos
|
|
891
|
+
/todo add <text> - Quick add todo
|
|
892
|
+
/todo list [filters] - List with filters
|
|
893
|
+
/todo done <id> - Mark as done
|
|
894
|
+
/todo start <id> - Mark as in progress
|
|
895
|
+
/todo cancel <id> - Cancel todo
|
|
896
|
+
/todo delete <id> - Delete todo
|
|
897
|
+
/todo view <id> - View todo details
|
|
898
|
+
/todo stats - Show statistics
|
|
899
|
+
/todo clear - Clear all todos
|
|
900
|
+
/todo help - Show this help
|
|
901
|
+
|
|
902
|
+
[bold]List Filters:[/bold]
|
|
903
|
+
/todo list status todo
|
|
904
|
+
/todo list priority high
|
|
905
|
+
/todo list tag work
|
|
906
|
+
|
|
907
|
+
[bold]Shortcuts:[/bold]
|
|
908
|
+
/todo a = add
|
|
909
|
+
/todo ls = list
|
|
910
|
+
/todo d = done
|
|
911
|
+
/todo rm = delete
|
|
912
|
+
"""
|
|
913
|
+
self.console.print(help_text)
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"""Native todo management for Hanzo REPL."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Dict, Optional, Any
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
from rich import box
|
|
15
|
+
from rich.prompt import Prompt, Confirm
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TodoPriority(Enum):
|
|
19
|
+
"""Todo priority levels."""
|
|
20
|
+
LOW = "low"
|
|
21
|
+
MEDIUM = "medium"
|
|
22
|
+
HIGH = "high"
|
|
23
|
+
URGENT = "urgent"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TodoStatus(Enum):
|
|
27
|
+
"""Todo status."""
|
|
28
|
+
TODO = "todo"
|
|
29
|
+
IN_PROGRESS = "in_progress"
|
|
30
|
+
DONE = "done"
|
|
31
|
+
CANCELLED = "cancelled"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Todo:
|
|
35
|
+
"""Single todo item."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
title: str,
|
|
40
|
+
description: str = "",
|
|
41
|
+
priority: TodoPriority = TodoPriority.MEDIUM,
|
|
42
|
+
status: TodoStatus = TodoStatus.TODO,
|
|
43
|
+
tags: List[str] = None,
|
|
44
|
+
due_date: Optional[str] = None,
|
|
45
|
+
id: Optional[str] = None,
|
|
46
|
+
created_at: Optional[str] = None,
|
|
47
|
+
updated_at: Optional[str] = None,
|
|
48
|
+
completed_at: Optional[str] = None
|
|
49
|
+
):
|
|
50
|
+
self.id = id or str(uuid.uuid4())[:8]
|
|
51
|
+
self.title = title
|
|
52
|
+
self.description = description
|
|
53
|
+
self.priority = priority
|
|
54
|
+
self.status = status
|
|
55
|
+
self.tags = tags or []
|
|
56
|
+
self.due_date = due_date
|
|
57
|
+
self.created_at = created_at or datetime.now().isoformat()
|
|
58
|
+
self.updated_at = updated_at or datetime.now().isoformat()
|
|
59
|
+
self.completed_at = completed_at
|
|
60
|
+
|
|
61
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
62
|
+
"""Convert to dictionary."""
|
|
63
|
+
return {
|
|
64
|
+
"id": self.id,
|
|
65
|
+
"title": self.title,
|
|
66
|
+
"description": self.description,
|
|
67
|
+
"priority": self.priority.value,
|
|
68
|
+
"status": self.status.value,
|
|
69
|
+
"tags": self.tags,
|
|
70
|
+
"due_date": self.due_date,
|
|
71
|
+
"created_at": self.created_at,
|
|
72
|
+
"updated_at": self.updated_at,
|
|
73
|
+
"completed_at": self.completed_at
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Todo":
|
|
78
|
+
"""Create from dictionary."""
|
|
79
|
+
return cls(
|
|
80
|
+
id=data.get("id"),
|
|
81
|
+
title=data["title"],
|
|
82
|
+
description=data.get("description", ""),
|
|
83
|
+
priority=TodoPriority(data.get("priority", "medium")),
|
|
84
|
+
status=TodoStatus(data.get("status", "todo")),
|
|
85
|
+
tags=data.get("tags", []),
|
|
86
|
+
due_date=data.get("due_date"),
|
|
87
|
+
created_at=data.get("created_at"),
|
|
88
|
+
updated_at=data.get("updated_at"),
|
|
89
|
+
completed_at=data.get("completed_at")
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TodoManager:
|
|
94
|
+
"""Manage todos with persistent storage."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, console: Optional[Console] = None):
|
|
97
|
+
self.console = console or Console()
|
|
98
|
+
self.config_dir = Path.home() / ".hanzo"
|
|
99
|
+
self.todos_file = self.config_dir / "todos.json"
|
|
100
|
+
self.todos: List[Todo] = []
|
|
101
|
+
self.load_todos()
|
|
102
|
+
|
|
103
|
+
def load_todos(self):
|
|
104
|
+
"""Load todos from file."""
|
|
105
|
+
if self.todos_file.exists():
|
|
106
|
+
try:
|
|
107
|
+
data = json.loads(self.todos_file.read_text())
|
|
108
|
+
self.todos = [Todo.from_dict(t) for t in data.get("todos", [])]
|
|
109
|
+
except Exception as e:
|
|
110
|
+
self.console.print(f"[red]Error loading todos: {e}[/red]")
|
|
111
|
+
self.todos = []
|
|
112
|
+
else:
|
|
113
|
+
self.todos = []
|
|
114
|
+
|
|
115
|
+
def save_todos(self):
|
|
116
|
+
"""Save todos to file."""
|
|
117
|
+
self.config_dir.mkdir(exist_ok=True)
|
|
118
|
+
data = {
|
|
119
|
+
"todos": [t.to_dict() for t in self.todos],
|
|
120
|
+
"last_updated": datetime.now().isoformat()
|
|
121
|
+
}
|
|
122
|
+
self.todos_file.write_text(json.dumps(data, indent=2))
|
|
123
|
+
|
|
124
|
+
def add_todo(
|
|
125
|
+
self,
|
|
126
|
+
title: str,
|
|
127
|
+
description: str = "",
|
|
128
|
+
priority: str = "medium",
|
|
129
|
+
tags: List[str] = None,
|
|
130
|
+
due_date: Optional[str] = None
|
|
131
|
+
) -> Todo:
|
|
132
|
+
"""Add a new todo."""
|
|
133
|
+
try:
|
|
134
|
+
priority_enum = TodoPriority(priority.lower())
|
|
135
|
+
except ValueError:
|
|
136
|
+
priority_enum = TodoPriority.MEDIUM
|
|
137
|
+
|
|
138
|
+
todo = Todo(
|
|
139
|
+
title=title,
|
|
140
|
+
description=description,
|
|
141
|
+
priority=priority_enum,
|
|
142
|
+
tags=tags or [],
|
|
143
|
+
due_date=due_date
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
self.todos.append(todo)
|
|
147
|
+
self.save_todos()
|
|
148
|
+
|
|
149
|
+
return todo
|
|
150
|
+
|
|
151
|
+
def update_todo(self, todo_id: str, **kwargs) -> Optional[Todo]:
|
|
152
|
+
"""Update a todo."""
|
|
153
|
+
todo = self.get_todo(todo_id)
|
|
154
|
+
if not todo:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
# Update fields
|
|
158
|
+
if "title" in kwargs:
|
|
159
|
+
todo.title = kwargs["title"]
|
|
160
|
+
if "description" in kwargs:
|
|
161
|
+
todo.description = kwargs["description"]
|
|
162
|
+
if "priority" in kwargs:
|
|
163
|
+
try:
|
|
164
|
+
todo.priority = TodoPriority(kwargs["priority"].lower())
|
|
165
|
+
except ValueError:
|
|
166
|
+
pass
|
|
167
|
+
if "status" in kwargs:
|
|
168
|
+
try:
|
|
169
|
+
new_status = TodoStatus(kwargs["status"].lower())
|
|
170
|
+
todo.status = new_status
|
|
171
|
+
|
|
172
|
+
# Update completed timestamp
|
|
173
|
+
if new_status == TodoStatus.DONE:
|
|
174
|
+
todo.completed_at = datetime.now().isoformat()
|
|
175
|
+
elif todo.status == TodoStatus.DONE and new_status != TodoStatus.DONE:
|
|
176
|
+
todo.completed_at = None
|
|
177
|
+
except ValueError:
|
|
178
|
+
pass
|
|
179
|
+
if "tags" in kwargs:
|
|
180
|
+
todo.tags = kwargs["tags"]
|
|
181
|
+
if "due_date" in kwargs:
|
|
182
|
+
todo.due_date = kwargs["due_date"]
|
|
183
|
+
|
|
184
|
+
todo.updated_at = datetime.now().isoformat()
|
|
185
|
+
self.save_todos()
|
|
186
|
+
|
|
187
|
+
return todo
|
|
188
|
+
|
|
189
|
+
def delete_todo(self, todo_id: str) -> bool:
|
|
190
|
+
"""Delete a todo."""
|
|
191
|
+
todo = self.get_todo(todo_id)
|
|
192
|
+
if todo:
|
|
193
|
+
self.todos.remove(todo)
|
|
194
|
+
self.save_todos()
|
|
195
|
+
return True
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
def get_todo(self, todo_id: str) -> Optional[Todo]:
|
|
199
|
+
"""Get a todo by ID."""
|
|
200
|
+
for todo in self.todos:
|
|
201
|
+
if todo.id == todo_id:
|
|
202
|
+
return todo
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
def list_todos(
|
|
206
|
+
self,
|
|
207
|
+
status: Optional[str] = None,
|
|
208
|
+
priority: Optional[str] = None,
|
|
209
|
+
tag: Optional[str] = None
|
|
210
|
+
) -> List[Todo]:
|
|
211
|
+
"""List todos with optional filters."""
|
|
212
|
+
filtered = self.todos
|
|
213
|
+
|
|
214
|
+
# Filter by status
|
|
215
|
+
if status:
|
|
216
|
+
try:
|
|
217
|
+
status_enum = TodoStatus(status.lower())
|
|
218
|
+
filtered = [t for t in filtered if t.status == status_enum]
|
|
219
|
+
except ValueError:
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
# Filter by priority
|
|
223
|
+
if priority:
|
|
224
|
+
try:
|
|
225
|
+
priority_enum = TodoPriority(priority.lower())
|
|
226
|
+
filtered = [t for t in filtered if t.priority == priority_enum]
|
|
227
|
+
except ValueError:
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
# Filter by tag
|
|
231
|
+
if tag:
|
|
232
|
+
filtered = [t for t in filtered if tag in t.tags]
|
|
233
|
+
|
|
234
|
+
# Sort by priority and status
|
|
235
|
+
priority_order = {
|
|
236
|
+
TodoPriority.URGENT: 0,
|
|
237
|
+
TodoPriority.HIGH: 1,
|
|
238
|
+
TodoPriority.MEDIUM: 2,
|
|
239
|
+
TodoPriority.LOW: 3
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
status_order = {
|
|
243
|
+
TodoStatus.IN_PROGRESS: 0,
|
|
244
|
+
TodoStatus.TODO: 1,
|
|
245
|
+
TodoStatus.DONE: 2,
|
|
246
|
+
TodoStatus.CANCELLED: 3
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
filtered.sort(key=lambda t: (
|
|
250
|
+
status_order.get(t.status, 999),
|
|
251
|
+
priority_order.get(t.priority, 999),
|
|
252
|
+
t.created_at
|
|
253
|
+
))
|
|
254
|
+
|
|
255
|
+
return filtered
|
|
256
|
+
|
|
257
|
+
def display_todos(
|
|
258
|
+
self,
|
|
259
|
+
todos: Optional[List[Todo]] = None,
|
|
260
|
+
title: str = "Todos"
|
|
261
|
+
):
|
|
262
|
+
"""Display todos in a nice table."""
|
|
263
|
+
if todos is None:
|
|
264
|
+
todos = self.list_todos()
|
|
265
|
+
|
|
266
|
+
if not todos:
|
|
267
|
+
self.console.print("[yellow]No todos found[/yellow]")
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
# Create table
|
|
271
|
+
table = Table(title=title, box=box.ROUNDED)
|
|
272
|
+
table.add_column("ID", style="cyan", width=8)
|
|
273
|
+
table.add_column("Status", width=12)
|
|
274
|
+
table.add_column("Priority", width=8)
|
|
275
|
+
table.add_column("Title", style="white")
|
|
276
|
+
table.add_column("Tags", style="dim")
|
|
277
|
+
table.add_column("Due", style="yellow")
|
|
278
|
+
|
|
279
|
+
for todo in todos:
|
|
280
|
+
# Status emoji
|
|
281
|
+
status_display = {
|
|
282
|
+
TodoStatus.TODO: "⭕ Todo",
|
|
283
|
+
TodoStatus.IN_PROGRESS: "🔄 In Progress",
|
|
284
|
+
TodoStatus.DONE: "✅ Done",
|
|
285
|
+
TodoStatus.CANCELLED: "❌ Cancelled"
|
|
286
|
+
}.get(todo.status, todo.status.value)
|
|
287
|
+
|
|
288
|
+
# Priority color
|
|
289
|
+
priority_color = {
|
|
290
|
+
TodoPriority.URGENT: "red bold",
|
|
291
|
+
TodoPriority.HIGH: "red",
|
|
292
|
+
TodoPriority.MEDIUM: "yellow",
|
|
293
|
+
TodoPriority.LOW: "green"
|
|
294
|
+
}.get(todo.priority, "white")
|
|
295
|
+
|
|
296
|
+
priority_display = f"[{priority_color}]{todo.priority.value.upper()}[/{priority_color}]"
|
|
297
|
+
|
|
298
|
+
# Tags
|
|
299
|
+
tags_display = ", ".join(todo.tags) if todo.tags else "-"
|
|
300
|
+
|
|
301
|
+
# Due date
|
|
302
|
+
due_display = todo.due_date if todo.due_date else "-"
|
|
303
|
+
|
|
304
|
+
table.add_row(
|
|
305
|
+
todo.id,
|
|
306
|
+
status_display,
|
|
307
|
+
priority_display,
|
|
308
|
+
todo.title,
|
|
309
|
+
tags_display,
|
|
310
|
+
due_display
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
self.console.print(table)
|
|
314
|
+
|
|
315
|
+
# Summary
|
|
316
|
+
total = len(todos)
|
|
317
|
+
done = len([t for t in todos if t.status == TodoStatus.DONE])
|
|
318
|
+
in_progress = len([t for t in todos if t.status == TodoStatus.IN_PROGRESS])
|
|
319
|
+
todo_count = len([t for t in todos if t.status == TodoStatus.TODO])
|
|
320
|
+
|
|
321
|
+
summary = f"Total: {total} | Todo: {todo_count} | In Progress: {in_progress} | Done: {done}"
|
|
322
|
+
self.console.print(f"\n[dim]{summary}[/dim]")
|
|
323
|
+
|
|
324
|
+
def display_todo_detail(self, todo: Todo):
|
|
325
|
+
"""Display detailed view of a todo."""
|
|
326
|
+
# Status color
|
|
327
|
+
status_color = {
|
|
328
|
+
TodoStatus.TODO: "yellow",
|
|
329
|
+
TodoStatus.IN_PROGRESS: "cyan",
|
|
330
|
+
TodoStatus.DONE: "green",
|
|
331
|
+
TodoStatus.CANCELLED: "red"
|
|
332
|
+
}.get(todo.status, "white")
|
|
333
|
+
|
|
334
|
+
# Priority color
|
|
335
|
+
priority_color = {
|
|
336
|
+
TodoPriority.URGENT: "red bold",
|
|
337
|
+
TodoPriority.HIGH: "red",
|
|
338
|
+
TodoPriority.MEDIUM: "yellow",
|
|
339
|
+
TodoPriority.LOW: "green"
|
|
340
|
+
}.get(todo.priority, "white")
|
|
341
|
+
|
|
342
|
+
# Build content
|
|
343
|
+
content = f"""
|
|
344
|
+
[bold]{todo.title}[/bold]
|
|
345
|
+
|
|
346
|
+
[dim]ID:[/dim] {todo.id}
|
|
347
|
+
[dim]Status:[/dim] [{status_color}]{todo.status.value.replace('_', ' ').title()}[/{status_color}]
|
|
348
|
+
[dim]Priority:[/dim] [{priority_color}]{todo.priority.value.upper()}[/{priority_color}]
|
|
349
|
+
[dim]Tags:[/dim] {', '.join(todo.tags) if todo.tags else 'None'}
|
|
350
|
+
[dim]Due Date:[/dim] {todo.due_date if todo.due_date else 'Not set'}
|
|
351
|
+
|
|
352
|
+
[dim]Description:[/dim]
|
|
353
|
+
{todo.description if todo.description else 'No description'}
|
|
354
|
+
|
|
355
|
+
[dim]Created:[/dim] {todo.created_at}
|
|
356
|
+
[dim]Updated:[/dim] {todo.updated_at}
|
|
357
|
+
[dim]Completed:[/dim] {todo.completed_at if todo.completed_at else 'Not completed'}
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
panel = Panel(content.strip(), title=f"Todo: {todo.title}", box=box.ROUNDED)
|
|
361
|
+
self.console.print(panel)
|
|
362
|
+
|
|
363
|
+
def quick_add(self, text: str) -> Todo:
|
|
364
|
+
"""Quick add todo from text.
|
|
365
|
+
|
|
366
|
+
Format: title #tag1 #tag2 !priority @due_date
|
|
367
|
+
"""
|
|
368
|
+
import re
|
|
369
|
+
|
|
370
|
+
# Extract tags (words starting with #)
|
|
371
|
+
tags = re.findall(r'#(\w+)', text)
|
|
372
|
+
text = re.sub(r'#\w+', '', text)
|
|
373
|
+
|
|
374
|
+
# Extract priority (word after !)
|
|
375
|
+
priority_match = re.search(r'!(\w+)', text)
|
|
376
|
+
priority = priority_match.group(1) if priority_match else "medium"
|
|
377
|
+
text = re.sub(r'!\w+', '', text)
|
|
378
|
+
|
|
379
|
+
# Extract due date (text after @)
|
|
380
|
+
due_match = re.search(r'@([^\s]+)', text)
|
|
381
|
+
due_date = due_match.group(1) if due_match else None
|
|
382
|
+
text = re.sub(r'@[^\s]+', '', text)
|
|
383
|
+
|
|
384
|
+
# Clean up title
|
|
385
|
+
title = text.strip()
|
|
386
|
+
|
|
387
|
+
if not title:
|
|
388
|
+
raise ValueError("Todo title cannot be empty")
|
|
389
|
+
|
|
390
|
+
return self.add_todo(
|
|
391
|
+
title=title,
|
|
392
|
+
priority=priority,
|
|
393
|
+
tags=tags,
|
|
394
|
+
due_date=due_date
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
398
|
+
"""Get todo statistics."""
|
|
399
|
+
total = len(self.todos)
|
|
400
|
+
|
|
401
|
+
# Status counts
|
|
402
|
+
status_counts = {}
|
|
403
|
+
for status in TodoStatus:
|
|
404
|
+
count = len([t for t in self.todos if t.status == status])
|
|
405
|
+
status_counts[status.value] = count
|
|
406
|
+
|
|
407
|
+
# Priority counts
|
|
408
|
+
priority_counts = {}
|
|
409
|
+
for priority in TodoPriority:
|
|
410
|
+
count = len([t for t in self.todos if t.priority == priority])
|
|
411
|
+
priority_counts[priority.value] = count
|
|
412
|
+
|
|
413
|
+
# Tags
|
|
414
|
+
all_tags = set()
|
|
415
|
+
for todo in self.todos:
|
|
416
|
+
all_tags.update(todo.tags)
|
|
417
|
+
|
|
418
|
+
# Completion rate
|
|
419
|
+
done = status_counts.get("done", 0)
|
|
420
|
+
completion_rate = (done / total * 100) if total > 0 else 0
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
"total": total,
|
|
424
|
+
"status": status_counts,
|
|
425
|
+
"priority": priority_counts,
|
|
426
|
+
"tags": list(all_tags),
|
|
427
|
+
"completion_rate": completion_rate
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
def display_statistics(self):
|
|
431
|
+
"""Display todo statistics."""
|
|
432
|
+
stats = self.get_statistics()
|
|
433
|
+
|
|
434
|
+
# Create stats panel
|
|
435
|
+
content = f"""
|
|
436
|
+
[bold cyan]Todo Statistics[/bold cyan]
|
|
437
|
+
|
|
438
|
+
[bold]Total Todos:[/bold] {stats['total']}
|
|
439
|
+
[bold]Completion Rate:[/bold] {stats['completion_rate']:.1f}%
|
|
440
|
+
|
|
441
|
+
[bold]By Status:[/bold]
|
|
442
|
+
⭕ Todo: {stats['status'].get('todo', 0)}
|
|
443
|
+
🔄 In Progress: {stats['status'].get('in_progress', 0)}
|
|
444
|
+
✅ Done: {stats['status'].get('done', 0)}
|
|
445
|
+
❌ Cancelled: {stats['status'].get('cancelled', 0)}
|
|
446
|
+
|
|
447
|
+
[bold]By Priority:[/bold]
|
|
448
|
+
🔴 Urgent: {stats['priority'].get('urgent', 0)}
|
|
449
|
+
🟠 High: {stats['priority'].get('high', 0)}
|
|
450
|
+
🟡 Medium: {stats['priority'].get('medium', 0)}
|
|
451
|
+
🟢 Low: {stats['priority'].get('low', 0)}
|
|
452
|
+
|
|
453
|
+
[bold]Tags:[/bold] {', '.join(stats['tags']) if stats['tags'] else 'None'}
|
|
454
|
+
"""
|
|
455
|
+
|
|
456
|
+
panel = Panel(content.strip(), title="📊 Statistics", box=box.ROUNDED)
|
|
457
|
+
self.console.print(panel)
|
hanzo/tools/detector.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import shutil
|
|
5
5
|
import subprocess
|
|
6
|
+
import httpx
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Dict, List, Optional, Tuple
|
|
8
9
|
from dataclasses import dataclass
|
|
@@ -33,12 +34,33 @@ class ToolDetector:
|
|
|
33
34
|
|
|
34
35
|
# Define available tools with priority order
|
|
35
36
|
TOOLS = [
|
|
37
|
+
# Hanzo Local Node - highest priority for privacy and local control
|
|
38
|
+
AITool(
|
|
39
|
+
name="hanzod",
|
|
40
|
+
command="hanzo node",
|
|
41
|
+
display_name="Hanzo Node (Local Private AI)",
|
|
42
|
+
provider="hanzo-local",
|
|
43
|
+
priority=0, # Highest priority - local and private
|
|
44
|
+
check_command=None, # Check via API endpoint
|
|
45
|
+
api_endpoint="http://localhost:8000/health",
|
|
46
|
+
env_var=None
|
|
47
|
+
),
|
|
48
|
+
AITool(
|
|
49
|
+
name="hanzo-router",
|
|
50
|
+
command="hanzo router",
|
|
51
|
+
display_name="Hanzo Router (LLM Proxy)",
|
|
52
|
+
provider="hanzo-router",
|
|
53
|
+
priority=1,
|
|
54
|
+
check_command=None,
|
|
55
|
+
api_endpoint="http://localhost:4000/health",
|
|
56
|
+
env_var=None
|
|
57
|
+
),
|
|
36
58
|
AITool(
|
|
37
59
|
name="claude-code",
|
|
38
60
|
command="claude",
|
|
39
61
|
display_name="Claude Code",
|
|
40
62
|
provider="anthropic",
|
|
41
|
-
priority=
|
|
63
|
+
priority=2,
|
|
42
64
|
check_command="claude --version",
|
|
43
65
|
env_var="ANTHROPIC_API_KEY"
|
|
44
66
|
),
|
|
@@ -47,7 +69,7 @@ class ToolDetector:
|
|
|
47
69
|
command="hanzo dev",
|
|
48
70
|
display_name="Hanzo Dev (Native)",
|
|
49
71
|
provider="hanzo",
|
|
50
|
-
priority=
|
|
72
|
+
priority=3,
|
|
51
73
|
check_command="hanzo --version",
|
|
52
74
|
env_var="HANZO_API_KEY"
|
|
53
75
|
),
|
|
@@ -56,7 +78,7 @@ class ToolDetector:
|
|
|
56
78
|
command="openai",
|
|
57
79
|
display_name="OpenAI Codex",
|
|
58
80
|
provider="openai",
|
|
59
|
-
priority=
|
|
81
|
+
priority=4,
|
|
60
82
|
check_command="openai --version",
|
|
61
83
|
env_var="OPENAI_API_KEY"
|
|
62
84
|
),
|
|
@@ -65,7 +87,7 @@ class ToolDetector:
|
|
|
65
87
|
command="gemini",
|
|
66
88
|
display_name="Gemini CLI",
|
|
67
89
|
provider="google",
|
|
68
|
-
priority=
|
|
90
|
+
priority=5,
|
|
69
91
|
check_command="gemini --version",
|
|
70
92
|
env_var="GEMINI_API_KEY"
|
|
71
93
|
),
|
|
@@ -74,7 +96,7 @@ class ToolDetector:
|
|
|
74
96
|
command="grok",
|
|
75
97
|
display_name="Grok CLI",
|
|
76
98
|
provider="xai",
|
|
77
|
-
priority=
|
|
99
|
+
priority=6,
|
|
78
100
|
check_command="grok --version",
|
|
79
101
|
env_var="GROK_API_KEY"
|
|
80
102
|
),
|
|
@@ -83,7 +105,7 @@ class ToolDetector:
|
|
|
83
105
|
command="openhands",
|
|
84
106
|
display_name="OpenHands CLI",
|
|
85
107
|
provider="openhands",
|
|
86
|
-
priority=
|
|
108
|
+
priority=7,
|
|
87
109
|
check_command="openhands --version",
|
|
88
110
|
env_var=None
|
|
89
111
|
),
|
|
@@ -92,7 +114,7 @@ class ToolDetector:
|
|
|
92
114
|
command="cursor",
|
|
93
115
|
display_name="Cursor AI",
|
|
94
116
|
provider="cursor",
|
|
95
|
-
priority=
|
|
117
|
+
priority=8,
|
|
96
118
|
check_command="cursor --version",
|
|
97
119
|
env_var=None
|
|
98
120
|
),
|
|
@@ -101,7 +123,7 @@ class ToolDetector:
|
|
|
101
123
|
command="codeium",
|
|
102
124
|
display_name="Codeium",
|
|
103
125
|
provider="codeium",
|
|
104
|
-
priority=
|
|
126
|
+
priority=9,
|
|
105
127
|
check_command="codeium --version",
|
|
106
128
|
env_var="CODEIUM_API_KEY"
|
|
107
129
|
),
|
|
@@ -110,7 +132,7 @@ class ToolDetector:
|
|
|
110
132
|
command="aider",
|
|
111
133
|
display_name="Aider",
|
|
112
134
|
provider="aider",
|
|
113
|
-
priority=
|
|
135
|
+
priority=10,
|
|
114
136
|
check_command="aider --version",
|
|
115
137
|
env_var=None
|
|
116
138
|
),
|
|
@@ -119,7 +141,7 @@ class ToolDetector:
|
|
|
119
141
|
command="continue",
|
|
120
142
|
display_name="Continue Dev",
|
|
121
143
|
provider="continue",
|
|
122
|
-
priority=
|
|
144
|
+
priority=11,
|
|
123
145
|
check_command="continue --version",
|
|
124
146
|
env_var=None
|
|
125
147
|
)
|
|
@@ -143,26 +165,51 @@ class ToolDetector:
|
|
|
143
165
|
|
|
144
166
|
def detect_tool(self, tool: AITool) -> bool:
|
|
145
167
|
"""Detect if a specific tool is available."""
|
|
168
|
+
# Check API endpoint first (for services like hanzod)
|
|
169
|
+
if tool.api_endpoint:
|
|
170
|
+
try:
|
|
171
|
+
response = httpx.get(tool.api_endpoint, timeout=1.0)
|
|
172
|
+
if response.status_code == 200:
|
|
173
|
+
tool.detected = True
|
|
174
|
+
tool.version = "Running"
|
|
175
|
+
|
|
176
|
+
# Special handling for Hanzo services
|
|
177
|
+
if tool.name == "hanzod":
|
|
178
|
+
# Check if models are loaded
|
|
179
|
+
try:
|
|
180
|
+
models_response = httpx.get("http://localhost:8000/models", timeout=1.0)
|
|
181
|
+
if models_response.status_code == 200:
|
|
182
|
+
models = models_response.json()
|
|
183
|
+
if models:
|
|
184
|
+
tool.version = f"Running ({len(models)} models)"
|
|
185
|
+
except:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
return True
|
|
189
|
+
except:
|
|
190
|
+
pass
|
|
191
|
+
|
|
146
192
|
# Check if command exists
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
tool.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
193
|
+
if tool.command:
|
|
194
|
+
tool.path = shutil.which(tool.command.split()[0])
|
|
195
|
+
if tool.path:
|
|
196
|
+
tool.detected = True
|
|
197
|
+
|
|
198
|
+
# Try to get version
|
|
199
|
+
if tool.check_command:
|
|
200
|
+
try:
|
|
201
|
+
result = subprocess.run(
|
|
202
|
+
tool.check_command.split(),
|
|
203
|
+
capture_output=True,
|
|
204
|
+
text=True,
|
|
205
|
+
timeout=2
|
|
206
|
+
)
|
|
207
|
+
if result.returncode == 0:
|
|
208
|
+
tool.version = result.stdout.strip().split()[-1]
|
|
209
|
+
except:
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
return True
|
|
166
213
|
|
|
167
214
|
# Check environment variable as fallback
|
|
168
215
|
if tool.env_var and os.getenv(tool.env_var):
|
|
@@ -229,13 +276,25 @@ class ToolDetector:
|
|
|
229
276
|
if self.detected_tools:
|
|
230
277
|
default = self.detected_tools[0]
|
|
231
278
|
self.console.print(f"\n[green]Default tool: {default.display_name}[/green]")
|
|
279
|
+
|
|
280
|
+
# Special message for Hanzo Node
|
|
281
|
+
if default.name == "hanzod":
|
|
282
|
+
self.console.print("[cyan]🔒 Using local private AI - your data stays on your machine[/cyan]")
|
|
283
|
+
self.console.print("[dim]Manage models with: hanzo node models[/dim]")
|
|
232
284
|
else:
|
|
233
285
|
self.console.print("\n[yellow]No AI coding tools detected.[/yellow]")
|
|
234
|
-
self.console.print("[dim]
|
|
286
|
+
self.console.print("[dim]Start Hanzo Node for local AI: hanzo node start[/dim]")
|
|
287
|
+
self.console.print("[dim]Or install Claude Code, OpenAI CLI, etc.[/dim]")
|
|
235
288
|
|
|
236
289
|
def get_tool_command(self, tool: AITool, prompt: str) -> List[str]:
|
|
237
290
|
"""Get the command to execute for a tool with a prompt."""
|
|
238
|
-
if tool.name == "
|
|
291
|
+
if tool.name == "hanzod":
|
|
292
|
+
# Use the local Hanzo node API
|
|
293
|
+
return ["hanzo", "ask", "--local", prompt]
|
|
294
|
+
elif tool.name == "hanzo-router":
|
|
295
|
+
# Use the router proxy
|
|
296
|
+
return ["hanzo", "ask", "--router", prompt]
|
|
297
|
+
elif tool.name == "claude-code":
|
|
239
298
|
return ["claude", prompt]
|
|
240
299
|
elif tool.name == "hanzo-dev":
|
|
241
300
|
return ["hanzo", "dev", "--prompt", prompt]
|
|
@@ -257,6 +316,43 @@ class ToolDetector:
|
|
|
257
316
|
def execute_with_tool(self, tool: AITool, prompt: str) -> Tuple[bool, str]:
|
|
258
317
|
"""Execute a prompt with a specific tool."""
|
|
259
318
|
try:
|
|
319
|
+
# Special handling for Hanzo services
|
|
320
|
+
if tool.name == "hanzod":
|
|
321
|
+
# Use the local API directly
|
|
322
|
+
try:
|
|
323
|
+
response = httpx.post(
|
|
324
|
+
"http://localhost:8000/chat/completions",
|
|
325
|
+
json={
|
|
326
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
327
|
+
"stream": False
|
|
328
|
+
},
|
|
329
|
+
timeout=30.0
|
|
330
|
+
)
|
|
331
|
+
if response.status_code == 200:
|
|
332
|
+
result = response.json()
|
|
333
|
+
return True, result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
334
|
+
except Exception as e:
|
|
335
|
+
return False, f"Hanzo Node error: {e}"
|
|
336
|
+
|
|
337
|
+
elif tool.name == "hanzo-router":
|
|
338
|
+
# Use the router API
|
|
339
|
+
try:
|
|
340
|
+
response = httpx.post(
|
|
341
|
+
"http://localhost:4000/chat/completions",
|
|
342
|
+
json={
|
|
343
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
344
|
+
"model": "gpt-3.5-turbo", # Router will route to best available
|
|
345
|
+
"stream": False
|
|
346
|
+
},
|
|
347
|
+
timeout=30.0
|
|
348
|
+
)
|
|
349
|
+
if response.status_code == 200:
|
|
350
|
+
result = response.json()
|
|
351
|
+
return True, result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
352
|
+
except Exception as e:
|
|
353
|
+
return False, f"Router error: {e}"
|
|
354
|
+
|
|
355
|
+
# Default command execution
|
|
260
356
|
command = self.get_tool_command(tool, prompt)
|
|
261
357
|
result = subprocess.run(
|
|
262
358
|
command,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hanzo
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.28
|
|
4
4
|
Summary: Hanzo AI - Complete AI Infrastructure Platform with CLI, Router, MCP, and Agent Runtime
|
|
5
5
|
Project-URL: Homepage, https://hanzo.ai
|
|
6
6
|
Project-URL: Repository, https://github.com/hanzoai/python-sdk
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
hanzo/__init__.py,sha256=
|
|
1
|
+
hanzo/__init__.py,sha256=g4XELSN8aLffy0YoVIjALuubohGyKAN9lejacSqkEOs,185
|
|
2
2
|
hanzo/__main__.py,sha256=F3Vz0Ty3bdAj_8oxyETMIqxlmNRnJOAFB1XPxbyfouI,105
|
|
3
3
|
hanzo/base_agent.py,sha256=ojPaSgFETYl7iARWnNpg8eyAt7sg8eKhn9xZThyvxRA,15324
|
|
4
4
|
hanzo/batch_orchestrator.py,sha256=vn6n5i9gTfZ4DtowFDd5iWgYKjgNTioIomkffKbipSM,35827
|
|
@@ -27,12 +27,13 @@ hanzo/commands/router.py,sha256=kB8snUM82cFk3znjFvs3jOJGqv5giKn8DiTkdbXnWYU,5332
|
|
|
27
27
|
hanzo/commands/tools.py,sha256=fG27wRweVmaFJowBpmwp5PgkRUtIF8bIlu_hGWr69Ss,10393
|
|
28
28
|
hanzo/interactive/__init__.py,sha256=ENHkGOqu-JYI05lqoOKDczJGl96oq6nM476EPhflAbI,74
|
|
29
29
|
hanzo/interactive/dashboard.py,sha256=XB5H_PMlReriCip-wW9iuUiJQOAtSATFG8EyhhFhItU,3842
|
|
30
|
-
hanzo/interactive/enhanced_repl.py,sha256=
|
|
30
|
+
hanzo/interactive/enhanced_repl.py,sha256=G_r1MEg3F0Hq9PQiDuJfiOYA2KT5A6_f-xerJWvTSo0,34235
|
|
31
31
|
hanzo/interactive/model_selector.py,sha256=4HcXvr8AI8Y5IttMH7Dhb8M0vqzP5y3S5kQrQmopYuw,5519
|
|
32
32
|
hanzo/interactive/repl.py,sha256=PXpRw1Cfqdqy1pQsKLqz9AwKJBFZ_Y758MpDlJIb9ao,6938
|
|
33
|
+
hanzo/interactive/todo_manager.py,sha256=tKsmfN4snILLkgwnjQcStP4CxYfNCXS5_pvQfmxkIjw,14640
|
|
33
34
|
hanzo/router/__init__.py,sha256=_cRG9nHC_wwq17iVYZSUNBYiJDdByfLDVEuIQn5-ePM,978
|
|
34
35
|
hanzo/tools/__init__.py,sha256=SsgmDvw5rO--NF4vKL9tV3O4WCNEl9aAIuqyTGSZ4RQ,122
|
|
35
|
-
hanzo/tools/detector.py,sha256=
|
|
36
|
+
hanzo/tools/detector.py,sha256=qwVc1fIDt2lDuqFqjhTVCnToRka91n125mpOpsPCfTU,14054
|
|
36
37
|
hanzo/ui/__init__.py,sha256=Ea22ereOm5Y0DDfyonA6qsO9Qkzofzd1CUE-VGW2lqw,241
|
|
37
38
|
hanzo/ui/inline_startup.py,sha256=7Y5dwqzt-L1J0F9peyqJ8XZgjHSua2nkItDTrLlBnhU,4265
|
|
38
39
|
hanzo/ui/startup.py,sha256=s7gP1QleQEIoCS1K0XBY7d6aufnwhicRLZDL7ej8ZZY,12235
|
|
@@ -40,7 +41,7 @@ hanzo/utils/__init__.py,sha256=5RRwKI852vp8smr4xCRgeKfn7dLEnHbdXGfVYTZ5jDQ,69
|
|
|
40
41
|
hanzo/utils/config.py,sha256=FD_LoBpcoF5dgJ7WL4o6LDp2pdOy8kS-dJ6iRO2GcGM,4728
|
|
41
42
|
hanzo/utils/net_check.py,sha256=YFbJ65SzfDYHkHLZe3n51VhId1VI3zhyx8p6BM-l6jE,3017
|
|
42
43
|
hanzo/utils/output.py,sha256=W0j3psF07vJiX4s02gbN4zYWfbKNsb8TSIoagBSf5vA,2704
|
|
43
|
-
hanzo-0.3.
|
|
44
|
-
hanzo-0.3.
|
|
45
|
-
hanzo-0.3.
|
|
46
|
-
hanzo-0.3.
|
|
44
|
+
hanzo-0.3.28.dist-info/METADATA,sha256=19BVN0yCDTn0FUruIXi1Mnh3VyqvlINVgySZ_HfYJ4o,6061
|
|
45
|
+
hanzo-0.3.28.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
46
|
+
hanzo-0.3.28.dist-info/entry_points.txt,sha256=pQLPMdqOXU_2BfTcMDhkqTCDNk_H6ApvYuSaWcuQOOw,171
|
|
47
|
+
hanzo-0.3.28.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|