hanzo 0.3.27__py3-none-any.whl → 0.3.29__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.
- hanzo/__init__.py +1 -1
- hanzo/interactive/enhanced_repl.py +245 -3
- hanzo/interactive/todo_manager.py +457 -0
- hanzo/tools/detector.py +42 -15
- {hanzo-0.3.27.dist-info → hanzo-0.3.29.dist-info}/METADATA +1 -1
- {hanzo-0.3.27.dist-info → hanzo-0.3.29.dist-info}/RECORD +8 -7
- {hanzo-0.3.27.dist-info → hanzo-0.3.29.dist-info}/WHEEL +0 -0
- {hanzo-0.3.27.dist-info → hanzo-0.3.29.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
|
|
@@ -631,8 +645,25 @@ class EnhancedHanzoREPL:
|
|
|
631
645
|
if success:
|
|
632
646
|
self.console.print(output)
|
|
633
647
|
else:
|
|
634
|
-
#
|
|
635
|
-
self.console.print(f"[
|
|
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)
|
hanzo/tools/detector.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hanzo
|
|
3
|
-
Version: 0.3.
|
|
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,4 +1,4 @@
|
|
|
1
|
-
hanzo/__init__.py,sha256=
|
|
1
|
+
hanzo/__init__.py,sha256=UijC5dH9CJe4QLC_IgQEkfv59evBzIHDNeuOUnfjIRg,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=8tA7ch_DpHq_M1ZHx7_bYgTqxhyq4ORH9fcGCCGajGo,35325
|
|
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=-qPqOWF5C5YEpLsNnEnyggicdjhI8cUhsJWT49fwkkY,15664
|
|
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.29.dist-info/METADATA,sha256=Z4-d35SChxL0tnUE6TKgRZPoDVL42EiCQJcfyDJ6SA4,6061
|
|
45
|
+
hanzo-0.3.29.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
46
|
+
hanzo-0.3.29.dist-info/entry_points.txt,sha256=pQLPMdqOXU_2BfTcMDhkqTCDNk_H6ApvYuSaWcuQOOw,171
|
|
47
|
+
hanzo-0.3.29.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|