hanzo 0.3.27__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-0.3.27.dist-info → hanzo-0.3.28.dist-info}/METADATA +1 -1
- {hanzo-0.3.27.dist-info → hanzo-0.3.28.dist-info}/RECORD +7 -6
- {hanzo-0.3.27.dist-info → hanzo-0.3.28.dist-info}/WHEEL +0 -0
- {hanzo-0.3.27.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)
|
|
@@ -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,9 +27,10 @@ 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
36
|
hanzo/tools/detector.py,sha256=qwVc1fIDt2lDuqFqjhTVCnToRka91n125mpOpsPCfTU,14054
|
|
@@ -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
|