hanzo 0.3.27__tar.gz → 0.3.29__tar.gz

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.

Files changed (47) hide show
  1. {hanzo-0.3.27 → hanzo-0.3.29}/PKG-INFO +1 -1
  2. {hanzo-0.3.27 → hanzo-0.3.29}/pyproject.toml +1 -1
  3. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/__init__.py +1 -1
  4. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/interactive/enhanced_repl.py +245 -3
  5. hanzo-0.3.29/src/hanzo/interactive/todo_manager.py +457 -0
  6. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/tools/detector.py +42 -15
  7. {hanzo-0.3.27 → hanzo-0.3.29}/.gitignore +0 -0
  8. {hanzo-0.3.27 → hanzo-0.3.29}/README.md +0 -0
  9. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/__main__.py +0 -0
  10. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/base_agent.py +0 -0
  11. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/batch_orchestrator.py +0 -0
  12. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/cli.py +0 -0
  13. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/commands/__init__.py +0 -0
  14. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/commands/agent.py +0 -0
  15. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/commands/auth.py +0 -0
  16. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/commands/auth_broken.py +0 -0
  17. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/commands/chat.py +0 -0
  18. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/commands/config.py +0 -0
  19. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/commands/mcp.py +0 -0
  20. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/commands/miner.py +0 -0
  21. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/commands/network.py +0 -0
  22. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/commands/node.py +0 -0
  23. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/commands/repl.py +0 -0
  24. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/commands/router.py +0 -0
  25. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/commands/tools.py +0 -0
  26. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/dev.py +0 -0
  27. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/fallback_handler.py +0 -0
  28. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/interactive/__init__.py +0 -0
  29. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/interactive/dashboard.py +0 -0
  30. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/interactive/model_selector.py +0 -0
  31. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/interactive/repl.py +0 -0
  32. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/mcp_server.py +0 -0
  33. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/memory_manager.py +0 -0
  34. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/model_registry.py +0 -0
  35. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/orchestrator_config.py +0 -0
  36. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/rate_limiter.py +0 -0
  37. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/repl.py +0 -0
  38. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/router/__init__.py +0 -0
  39. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/streaming.py +0 -0
  40. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/tools/__init__.py +0 -0
  41. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/ui/__init__.py +0 -0
  42. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/ui/inline_startup.py +0 -0
  43. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/ui/startup.py +0 -0
  44. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/utils/__init__.py +0 -0
  45. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/utils/config.py +0 -0
  46. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/utils/net_check.py +0 -0
  47. {hanzo-0.3.27 → hanzo-0.3.29}/src/hanzo/utils/output.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hanzo
3
- Version: 0.3.27
3
+ Version: 0.3.29
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "hanzo"
3
- version = "0.3.27"
3
+ version = "0.3.29"
4
4
  description = "Hanzo AI - Complete AI Infrastructure Platform with CLI, Router, MCP, and Agent Runtime"
5
5
  authors = [
6
6
  {name = "Hanzo AI", email = "dev@hanzo.ai"},
@@ -1,6 +1,6 @@
1
1
  """Hanzo - Complete AI Infrastructure Platform with CLI, Router, MCP, and Agent Runtime."""
2
2
 
3
- __version__ = "0.3.27"
3
+ __version__ = "0.3.29"
4
4
  __all__ = ["main", "cli", "__version__"]
5
5
 
6
6
  from .cli import cli, main
@@ -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
@@ -631,8 +645,25 @@ class EnhancedHanzoREPL:
631
645
  if success:
632
646
  self.console.print(output)
633
647
  else:
634
- # Fallback to regular model
635
- self.console.print(f"[yellow]{self.current_tool.display_name} failed, trying cloud model...[/yellow]")
648
+ # Show error and try fallback
649
+ self.console.print(f"[red]Error: {output}[/red]")
650
+
651
+ # Try to find next available tool
652
+ if self.tool_detector and self.tool_detector.detected_tools:
653
+ for fallback_tool in self.tool_detector.detected_tools:
654
+ if fallback_tool != self.current_tool:
655
+ self.console.print(f"[yellow]Trying {fallback_tool.display_name}...[/yellow]")
656
+ success, output = self.tool_detector.execute_with_tool(fallback_tool, message)
657
+ if success:
658
+ self.console.print(output)
659
+ # Suggest switching to working tool
660
+ self.console.print(f"\n[dim]Tip: Switch to {fallback_tool.display_name} with /model {fallback_tool.name}[/dim]")
661
+ return
662
+ else:
663
+ self.console.print(f"[red]{fallback_tool.display_name} also failed[/red]")
664
+
665
+ # Final fallback to cloud model
666
+ self.console.print(f"[yellow]Falling back to cloud model...[/yellow]")
636
667
  await self.execute_command("ask", f"--cloud --model gpt-3.5-turbo {message}")
637
668
  else:
638
669
  # Use regular model through hanzo ask
@@ -685,4 +716,215 @@ class EnhancedHanzoREPL:
685
716
  else:
686
717
  self.task_manager.kill_task(task_id)
687
718
  except (KeyboardInterrupt, EOFError):
688
- pass
719
+ pass
720
+
721
+ async def manage_todos(self, args: str = ""):
722
+ """Manage todos."""
723
+ if not self.todo_manager:
724
+ self.console.print("[yellow]Todo manager not available[/yellow]")
725
+ return
726
+
727
+ # Parse command
728
+ parts = args.strip().split(maxsplit=1)
729
+
730
+ if not parts:
731
+ # Show todos
732
+ self.todo_manager.display_todos()
733
+ return
734
+
735
+ subcommand = parts[0].lower()
736
+ rest = parts[1] if len(parts) > 1 else ""
737
+
738
+ # Handle subcommands
739
+ if subcommand in ["add", "a", "+"]:
740
+ # Add todo
741
+ if rest:
742
+ # Quick add
743
+ try:
744
+ todo = self.todo_manager.quick_add(rest)
745
+ self.console.print(f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]")
746
+ except ValueError as e:
747
+ self.console.print(f"[red]Error: {e}[/red]")
748
+ else:
749
+ # Interactive add
750
+ await self.add_todo_interactive()
751
+
752
+ elif subcommand in ["list", "ls", "l"]:
753
+ # List todos with optional filter
754
+ filter_parts = rest.split()
755
+ status = None
756
+ priority = None
757
+ tag = None
758
+
759
+ for i in range(0, len(filter_parts), 2):
760
+ if i + 1 < len(filter_parts):
761
+ key = filter_parts[i]
762
+ value = filter_parts[i + 1]
763
+
764
+ if key in ["status", "s"]:
765
+ status = value
766
+ elif key in ["priority", "p"]:
767
+ priority = value
768
+ elif key in ["tag", "t"]:
769
+ tag = value
770
+
771
+ todos = self.todo_manager.list_todos(status=status, priority=priority, tag=tag)
772
+ title = "Filtered Todos" if (status or priority or tag) else "All Todos"
773
+ self.todo_manager.display_todos(todos, title)
774
+
775
+ elif subcommand in ["done", "d", "complete", "finish"]:
776
+ # Mark as done
777
+ if rest:
778
+ todo = self.todo_manager.update_todo(rest, status="done")
779
+ if todo:
780
+ self.console.print(f"[green]✅ Marked as done: {todo.title}[/green]")
781
+ else:
782
+ self.console.print(f"[red]Todo not found: {rest}[/red]")
783
+ else:
784
+ self.console.print("[yellow]Usage: /todo done <id>[/yellow]")
785
+
786
+ elif subcommand in ["start", "begin", "progress"]:
787
+ # Mark as in progress
788
+ if rest:
789
+ todo = self.todo_manager.update_todo(rest, status="in_progress")
790
+ if todo:
791
+ self.console.print(f"[cyan]🔄 Started: {todo.title}[/cyan]")
792
+ else:
793
+ self.console.print(f"[red]Todo not found: {rest}[/red]")
794
+ else:
795
+ self.console.print("[yellow]Usage: /todo start <id>[/yellow]")
796
+
797
+ elif subcommand in ["cancel", "x"]:
798
+ # Cancel todo
799
+ if rest:
800
+ todo = self.todo_manager.update_todo(rest, status="cancelled")
801
+ if todo:
802
+ self.console.print(f"[red]❌ Cancelled: {todo.title}[/red]")
803
+ else:
804
+ self.console.print(f"[red]Todo not found: {rest}[/red]")
805
+ else:
806
+ self.console.print("[yellow]Usage: /todo cancel <id>[/yellow]")
807
+
808
+ elif subcommand in ["delete", "del", "rm", "remove"]:
809
+ # Delete todo
810
+ if rest:
811
+ if self.todo_manager.delete_todo(rest):
812
+ self.console.print(f"[green]✅ Deleted todo: {rest}[/green]")
813
+ else:
814
+ self.console.print(f"[red]Todo not found: {rest}[/red]")
815
+ else:
816
+ self.console.print("[yellow]Usage: /todo delete <id>[/yellow]")
817
+
818
+ elif subcommand in ["view", "show", "detail"]:
819
+ # View todo detail
820
+ if rest:
821
+ todo = self.todo_manager.get_todo(rest)
822
+ if todo:
823
+ self.todo_manager.display_todo_detail(todo)
824
+ else:
825
+ self.console.print(f"[red]Todo not found: {rest}[/red]")
826
+ else:
827
+ self.console.print("[yellow]Usage: /todo view <id>[/yellow]")
828
+
829
+ elif subcommand in ["stats", "statistics"]:
830
+ # Show statistics
831
+ self.todo_manager.display_statistics()
832
+
833
+ elif subcommand in ["clear", "reset"]:
834
+ # Clear all todos (with confirmation)
835
+ try:
836
+ confirm = await self.session.prompt_async("Are you sure you want to delete ALL todos? (yes/no): ")
837
+ if confirm.lower() in ["yes", "y"]:
838
+ self.todo_manager.todos = []
839
+ self.todo_manager.save_todos()
840
+ self.console.print("[green]✅ All todos cleared[/green]")
841
+ else:
842
+ self.console.print("[yellow]Cancelled[/yellow]")
843
+ except (KeyboardInterrupt, EOFError):
844
+ self.console.print("[yellow]Cancelled[/yellow]")
845
+
846
+ elif subcommand in ["help", "h", "?"]:
847
+ # Show todo help
848
+ self.show_todo_help()
849
+
850
+ else:
851
+ # Unknown subcommand, treat as quick add
852
+ try:
853
+ todo = self.todo_manager.quick_add(args)
854
+ self.console.print(f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]")
855
+ except ValueError:
856
+ self.console.print(f"[yellow]Unknown todo command: {subcommand}[/yellow]")
857
+ self.console.print("[dim]Use /todo help for available commands[/dim]")
858
+
859
+ async def add_todo_interactive(self):
860
+ """Add todo interactively."""
861
+ try:
862
+ # Get title
863
+ title = await self.session.prompt_async("Title: ")
864
+ if not title:
865
+ self.console.print("[yellow]Cancelled[/yellow]")
866
+ return
867
+
868
+ # Get description
869
+ description = await self.session.prompt_async("Description (optional): ")
870
+
871
+ # Get priority
872
+ priority = await self.session.prompt_async("Priority (low/medium/high/urgent) [medium]: ")
873
+ if not priority:
874
+ priority = "medium"
875
+
876
+ # Get tags
877
+ tags_input = await self.session.prompt_async("Tags (comma-separated, optional): ")
878
+ tags = [t.strip() for t in tags_input.split(",") if t.strip()] if tags_input else []
879
+
880
+ # Get due date
881
+ due_date = await self.session.prompt_async("Due date (optional): ")
882
+
883
+ # Add todo
884
+ todo = self.todo_manager.add_todo(
885
+ title=title,
886
+ description=description,
887
+ priority=priority,
888
+ tags=tags,
889
+ due_date=due_date if due_date else None
890
+ )
891
+
892
+ self.console.print(f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]")
893
+
894
+ except (KeyboardInterrupt, EOFError):
895
+ self.console.print("[yellow]Cancelled[/yellow]")
896
+
897
+ def show_todo_help(self):
898
+ """Show todo help."""
899
+ help_text = """
900
+ [bold cyan]Todo Management[/bold cyan]
901
+
902
+ [bold]Quick Add:[/bold]
903
+ /todo Buy milk #shopping !high @tomorrow
904
+ Format: title #tag1 #tag2 !priority @due_date
905
+
906
+ [bold]Commands:[/bold]
907
+ /todo - List all todos
908
+ /todo add <text> - Quick add todo
909
+ /todo list [filters] - List with filters
910
+ /todo done <id> - Mark as done
911
+ /todo start <id> - Mark as in progress
912
+ /todo cancel <id> - Cancel todo
913
+ /todo delete <id> - Delete todo
914
+ /todo view <id> - View todo details
915
+ /todo stats - Show statistics
916
+ /todo clear - Clear all todos
917
+ /todo help - Show this help
918
+
919
+ [bold]List Filters:[/bold]
920
+ /todo list status todo
921
+ /todo list priority high
922
+ /todo list tag work
923
+
924
+ [bold]Shortcuts:[/bold]
925
+ /todo a = add
926
+ /todo ls = list
927
+ /todo d = done
928
+ /todo rm = delete
929
+ """
930
+ 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)
@@ -170,22 +170,46 @@ class ToolDetector:
170
170
  try:
171
171
  response = httpx.get(tool.api_endpoint, timeout=1.0)
172
172
  if response.status_code == 200:
173
- tool.detected = True
174
- tool.version = "Running"
175
-
176
- # Special handling for Hanzo services
173
+ # For Hanzo Node, verify it can actually handle chat completions
177
174
  if tool.name == "hanzod":
178
- # Check if models are loaded
179
175
  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)"
176
+ # Check if the chat completions endpoint works
177
+ test_response = httpx.post(
178
+ "http://localhost:8000/v1/chat/completions",
179
+ json={
180
+ "messages": [{"role": "user", "content": "test"}],
181
+ "model": "test",
182
+ "max_tokens": 1
183
+ },
184
+ timeout=2.0
185
+ )
186
+ # Only mark as detected if we get a valid response or specific error
187
+ # 404 means the endpoint doesn't exist
188
+ if test_response.status_code == 404:
189
+ return False
190
+
191
+ tool.detected = True
192
+ tool.version = "Running (Local AI)"
193
+
194
+ # Try to get model info
195
+ try:
196
+ models_response = httpx.get("http://localhost:8000/v1/models", timeout=1.0)
197
+ if models_response.status_code == 200:
198
+ models = models_response.json().get("data", [])
199
+ if models:
200
+ tool.version = f"Running ({len(models)} models)"
201
+ except:
202
+ pass
203
+
204
+ return True
185
205
  except:
186
- pass
187
-
188
- return True
206
+ # If chat endpoint doesn't work, node isn't useful
207
+ return False
208
+ else:
209
+ # For other services, just check health endpoint
210
+ tool.detected = True
211
+ tool.version = "Running"
212
+ return True
189
213
  except:
190
214
  pass
191
215
 
@@ -318,12 +342,13 @@ class ToolDetector:
318
342
  try:
319
343
  # Special handling for Hanzo services
320
344
  if tool.name == "hanzod":
321
- # Use the local API directly
345
+ # Use the local API directly with correct endpoint
322
346
  try:
323
347
  response = httpx.post(
324
- "http://localhost:8000/chat/completions",
348
+ "http://localhost:8000/v1/chat/completions",
325
349
  json={
326
350
  "messages": [{"role": "user", "content": prompt}],
351
+ "model": "default", # Use default model
327
352
  "stream": False
328
353
  },
329
354
  timeout=30.0
@@ -331,6 +356,8 @@ class ToolDetector:
331
356
  if response.status_code == 200:
332
357
  result = response.json()
333
358
  return True, result.get("choices", [{}])[0].get("message", {}).get("content", "")
359
+ else:
360
+ return False, f"Hanzo Node returned {response.status_code}: {response.text}"
334
361
  except Exception as e:
335
362
  return False, f"Hanzo Node error: {e}"
336
363
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes