hanzo 0.2.3__py3-none-any.whl → 0.2.6__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 +4 -93
- hanzo/__main__.py +6 -0
- hanzo/cli.py +221 -8
- hanzo/commands/__init__.py +3 -0
- hanzo/commands/agent.py +112 -0
- hanzo/commands/auth.py +324 -0
- hanzo/commands/chat.py +183 -0
- hanzo/commands/cluster.py +428 -0
- hanzo/commands/config.py +240 -0
- hanzo/commands/mcp.py +235 -0
- hanzo/commands/miner.py +323 -0
- hanzo/commands/network.py +333 -0
- hanzo/commands/repl.py +186 -0
- hanzo/commands/tools.py +335 -0
- hanzo/interactive/__init__.py +3 -0
- hanzo/interactive/dashboard.py +125 -0
- hanzo/interactive/repl.py +184 -0
- hanzo/router/__init__.py +13 -7
- hanzo/utils/__init__.py +3 -0
- hanzo/utils/config.py +170 -0
- hanzo/utils/net_check.py +107 -0
- hanzo/utils/output.py +103 -0
- {hanzo-0.2.3.dist-info → hanzo-0.2.6.dist-info}/METADATA +6 -3
- hanzo-0.2.6.dist-info/RECORD +28 -0
- hanzo-0.2.3.dist-info/RECORD +0 -9
- {hanzo-0.2.3.dist-info → hanzo-0.2.6.dist-info}/WHEEL +0 -0
- {hanzo-0.2.3.dist-info → hanzo-0.2.6.dist-info}/entry_points.txt +0 -0
hanzo/commands/mcp.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""MCP (Model Context Protocol) commands."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from typing import Dict, Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.syntax import Syntax
|
|
10
|
+
|
|
11
|
+
from ..utils.output import console, handle_errors
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group(name="mcp")
|
|
15
|
+
def mcp_group():
|
|
16
|
+
"""Manage MCP servers and tools."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@mcp_group.command()
|
|
21
|
+
@click.option("--name", "-n", default="hanzo-mcp", help="Server name")
|
|
22
|
+
@click.option("--transport", "-t", type=click.Choice(["stdio", "sse"]), default="stdio", help="Transport protocol")
|
|
23
|
+
@click.option("--allow-path", "-p", multiple=True, help="Allowed paths")
|
|
24
|
+
@click.option("--enable-agent", is_flag=True, help="Enable agent tools")
|
|
25
|
+
@click.option("--host", default="127.0.0.1", help="Host for SSE transport")
|
|
26
|
+
@click.option("--port", default=3000, type=int, help="Port for SSE transport")
|
|
27
|
+
@click.pass_context
|
|
28
|
+
def serve(ctx, name: str, transport: str, allow_path: tuple, enable_agent: bool, host: str, port: int):
|
|
29
|
+
"""Start MCP server."""
|
|
30
|
+
try:
|
|
31
|
+
from hanzoai.mcp import run_mcp_server
|
|
32
|
+
except ImportError:
|
|
33
|
+
console.print("[red]Error:[/red] hanzo-mcp not installed")
|
|
34
|
+
console.print("Install with: pip install hanzo[mcp]")
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
allowed_paths = list(allow_path) if allow_path else ["."]
|
|
38
|
+
|
|
39
|
+
console.print(f"[cyan]Starting MCP server[/cyan]")
|
|
40
|
+
console.print(f" Name: {name}")
|
|
41
|
+
console.print(f" Transport: {transport}")
|
|
42
|
+
console.print(f" Allowed paths: {', '.join(allowed_paths)}")
|
|
43
|
+
|
|
44
|
+
if transport == "sse":
|
|
45
|
+
console.print(f" Endpoint: http://{host}:{port}")
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
run_mcp_server(
|
|
49
|
+
name=name,
|
|
50
|
+
transport=transport,
|
|
51
|
+
allowed_paths=allowed_paths,
|
|
52
|
+
enable_agent_tool=enable_agent,
|
|
53
|
+
host=host,
|
|
54
|
+
port=port
|
|
55
|
+
)
|
|
56
|
+
except KeyboardInterrupt:
|
|
57
|
+
console.print("\n[yellow]Server stopped[/yellow]")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@mcp_group.command()
|
|
61
|
+
@click.option("--category", "-c", help="Filter by category")
|
|
62
|
+
@click.pass_context
|
|
63
|
+
async def tools(ctx, category: str):
|
|
64
|
+
"""List available MCP tools."""
|
|
65
|
+
try:
|
|
66
|
+
from hanzoai.mcp import create_server
|
|
67
|
+
except ImportError:
|
|
68
|
+
console.print("[red]Error:[/red] hanzo-mcp not installed")
|
|
69
|
+
console.print("Install with: pip install hanzo[mcp]")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
with console.status("Loading tools..."):
|
|
73
|
+
server = create_server(enable_all_tools=True)
|
|
74
|
+
tools_list = await server.mcp.list_tools()
|
|
75
|
+
|
|
76
|
+
# Group by category if available
|
|
77
|
+
categories = {}
|
|
78
|
+
for tool in tools_list:
|
|
79
|
+
cat = getattr(tool, 'category', 'general')
|
|
80
|
+
if category and cat != category:
|
|
81
|
+
continue
|
|
82
|
+
if cat not in categories:
|
|
83
|
+
categories[cat] = []
|
|
84
|
+
categories[cat].append(tool)
|
|
85
|
+
|
|
86
|
+
# Display tools
|
|
87
|
+
for cat, tools in sorted(categories.items()):
|
|
88
|
+
table = Table(title=f"{cat.title()} Tools" if len(categories) > 1 else "MCP Tools")
|
|
89
|
+
table.add_column("Name", style="cyan", no_wrap=True)
|
|
90
|
+
table.add_column("Description")
|
|
91
|
+
|
|
92
|
+
for tool in sorted(tools, key=lambda t: t.name):
|
|
93
|
+
table.add_row(tool.name, tool.description)
|
|
94
|
+
|
|
95
|
+
console.print(table)
|
|
96
|
+
if len(categories) > 1:
|
|
97
|
+
console.print()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@mcp_group.command()
|
|
101
|
+
@click.argument("tool")
|
|
102
|
+
@click.option("--arg", "-a", multiple=True, help="Tool arguments (key=value)")
|
|
103
|
+
@click.option("--json-args", "-j", help="JSON arguments")
|
|
104
|
+
@click.pass_context
|
|
105
|
+
async def run(ctx, tool: str, arg: tuple, json_args: str):
|
|
106
|
+
"""Run an MCP tool."""
|
|
107
|
+
try:
|
|
108
|
+
from hanzoai.mcp import create_server
|
|
109
|
+
except ImportError:
|
|
110
|
+
console.print("[red]Error:[/red] hanzo-mcp not installed")
|
|
111
|
+
console.print("Install with: pip install hanzo[mcp]")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# Parse arguments
|
|
115
|
+
args = {}
|
|
116
|
+
|
|
117
|
+
if json_args:
|
|
118
|
+
try:
|
|
119
|
+
args = json.loads(json_args)
|
|
120
|
+
except json.JSONDecodeError as e:
|
|
121
|
+
console.print(f"[red]Invalid JSON: {e}[/red]")
|
|
122
|
+
return
|
|
123
|
+
else:
|
|
124
|
+
for a in arg:
|
|
125
|
+
if "=" not in a:
|
|
126
|
+
console.print(f"[red]Invalid argument format: {a}[/red]")
|
|
127
|
+
console.print("Use: --arg key=value")
|
|
128
|
+
return
|
|
129
|
+
key, value = a.split("=", 1)
|
|
130
|
+
|
|
131
|
+
# Try to parse value as JSON first
|
|
132
|
+
try:
|
|
133
|
+
args[key] = json.loads(value)
|
|
134
|
+
except:
|
|
135
|
+
args[key] = value
|
|
136
|
+
|
|
137
|
+
# Create server and run tool
|
|
138
|
+
with console.status(f"Running tool '{tool}'..."):
|
|
139
|
+
server = create_server(enable_all_tools=True)
|
|
140
|
+
|
|
141
|
+
# Find tool
|
|
142
|
+
tools_list = await server.mcp.list_tools()
|
|
143
|
+
tool_obj = None
|
|
144
|
+
for t in tools_list:
|
|
145
|
+
if t.name == tool:
|
|
146
|
+
tool_obj = t
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
if not tool_obj:
|
|
150
|
+
console.print(f"[red]Tool not found: {tool}[/red]")
|
|
151
|
+
console.print("Use 'hanzo mcp tools' to list available tools")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
# Run tool
|
|
155
|
+
try:
|
|
156
|
+
# Mock context for now
|
|
157
|
+
from mcp.server.fastmcp import Context
|
|
158
|
+
context = Context()
|
|
159
|
+
|
|
160
|
+
# Get tool function
|
|
161
|
+
tool_func = server.mcp._tool_map.get(tool)
|
|
162
|
+
if tool_func:
|
|
163
|
+
result = await tool_func(**args)
|
|
164
|
+
else:
|
|
165
|
+
console.print(f"[red]Tool function not found: {tool}[/red]")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
except Exception as e:
|
|
169
|
+
console.print(f"[red]Tool error: {e}[/red]")
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
# Display result
|
|
173
|
+
if isinstance(result, str):
|
|
174
|
+
try:
|
|
175
|
+
# Try to parse as JSON for pretty printing
|
|
176
|
+
data = json.loads(result)
|
|
177
|
+
console.print_json(data=data)
|
|
178
|
+
except:
|
|
179
|
+
# Display as text
|
|
180
|
+
console.print(result)
|
|
181
|
+
else:
|
|
182
|
+
console.print(result)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@mcp_group.command()
|
|
186
|
+
@click.option("--path", "-p", default="~/.config/claude/claude_desktop_config.json", help="Config file path")
|
|
187
|
+
@click.pass_context
|
|
188
|
+
def install(ctx, path: str):
|
|
189
|
+
"""Install MCP server in Claude Desktop."""
|
|
190
|
+
try:
|
|
191
|
+
from hanzoai.mcp import create_server
|
|
192
|
+
except ImportError:
|
|
193
|
+
console.print("[red]Error:[/red] hanzo-mcp not installed")
|
|
194
|
+
console.print("Install with: pip install hanzo[mcp]")
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
import os
|
|
198
|
+
import json
|
|
199
|
+
from pathlib import Path
|
|
200
|
+
|
|
201
|
+
config_path = Path(os.path.expanduser(path))
|
|
202
|
+
|
|
203
|
+
# Create config
|
|
204
|
+
config = {
|
|
205
|
+
"mcpServers": {
|
|
206
|
+
"hanzo-mcp": {
|
|
207
|
+
"command": "hanzo",
|
|
208
|
+
"args": ["mcp", "serve", "--transport", "stdio"]
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# Check if file exists
|
|
214
|
+
if config_path.exists():
|
|
215
|
+
try:
|
|
216
|
+
with open(config_path, "r") as f:
|
|
217
|
+
existing = json.load(f)
|
|
218
|
+
|
|
219
|
+
if "mcpServers" not in existing:
|
|
220
|
+
existing["mcpServers"] = {}
|
|
221
|
+
|
|
222
|
+
existing["mcpServers"]["hanzo-mcp"] = config["mcpServers"]["hanzo-mcp"]
|
|
223
|
+
config = existing
|
|
224
|
+
except Exception as e:
|
|
225
|
+
console.print(f"[yellow]Warning: Could not read existing config: {e}[/yellow]")
|
|
226
|
+
|
|
227
|
+
# Write config
|
|
228
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
229
|
+
|
|
230
|
+
with open(config_path, "w") as f:
|
|
231
|
+
json.dump(config, f, indent=2)
|
|
232
|
+
|
|
233
|
+
console.print(f"[green]✓[/green] Installed hanzo-mcp in Claude Desktop")
|
|
234
|
+
console.print(f" Config: {config_path}")
|
|
235
|
+
console.print("\nRestart Claude Desktop for changes to take effect")
|
hanzo/commands/miner.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Mining commands for distributed AI compute."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Optional, List
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from ..utils.output import console, handle_errors
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group(name="miner")
|
|
14
|
+
def miner_group():
|
|
15
|
+
"""Manage Hanzo AI mining (contribute compute)."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@miner_group.command()
|
|
20
|
+
@click.option("--name", "-n", help="Miner name (auto-generated if not provided)")
|
|
21
|
+
@click.option("--wallet", "-w", help="Wallet address for rewards")
|
|
22
|
+
@click.option("--models", "-m", multiple=True, help="Models to support")
|
|
23
|
+
@click.option("--device", type=click.Choice(["cpu", "gpu", "auto"]), default="auto", help="Device to use")
|
|
24
|
+
@click.option("--network", default="mainnet", help="Network to join")
|
|
25
|
+
@click.option("--min-stake", type=float, help="Minimum stake requirement")
|
|
26
|
+
@click.pass_context
|
|
27
|
+
async def start(ctx, name: str, wallet: str, models: tuple, device: str, network: str, min_stake: float):
|
|
28
|
+
"""Start mining (contribute compute to network)."""
|
|
29
|
+
try:
|
|
30
|
+
from hanzo_miner import HanzoMiner
|
|
31
|
+
except ImportError:
|
|
32
|
+
console.print("[red]Error:[/red] hanzo-miner not installed")
|
|
33
|
+
console.print("Install with: pip install hanzo[miner]")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
# Check wallet
|
|
37
|
+
if not wallet:
|
|
38
|
+
console.print("[yellow]Warning:[/yellow] No wallet address provided")
|
|
39
|
+
console.print("You won't earn rewards without a wallet")
|
|
40
|
+
if not click.confirm("Continue without wallet?"):
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
miner = HanzoMiner(
|
|
44
|
+
name=name,
|
|
45
|
+
wallet=wallet,
|
|
46
|
+
device=device,
|
|
47
|
+
network=network,
|
|
48
|
+
min_stake=min_stake
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
with Progress(
|
|
52
|
+
SpinnerColumn(),
|
|
53
|
+
TextColumn("[progress.description]{task.description}"),
|
|
54
|
+
console=console,
|
|
55
|
+
) as progress:
|
|
56
|
+
task = progress.add_task("Starting miner...", total=None)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
# Start miner
|
|
60
|
+
await miner.start(models=list(models) if models else None)
|
|
61
|
+
progress.update(task, completed=True)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
progress.stop()
|
|
64
|
+
console.print(f"[red]Failed to start miner: {e}[/red]")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
console.print(f"[green]✓[/green] Miner started")
|
|
68
|
+
console.print(f" Name: {miner.name}")
|
|
69
|
+
console.print(f" Network: {network}")
|
|
70
|
+
console.print(f" Device: {device}")
|
|
71
|
+
if wallet:
|
|
72
|
+
console.print(f" Wallet: {wallet[:8]}...{wallet[-4:]}")
|
|
73
|
+
|
|
74
|
+
# Show initial stats
|
|
75
|
+
stats = await miner.get_stats()
|
|
76
|
+
if stats:
|
|
77
|
+
console.print("\n[cyan]Initial Stats:[/cyan]")
|
|
78
|
+
console.print(f" Jobs completed: {stats.get('jobs_completed', 0)}")
|
|
79
|
+
console.print(f" Tokens earned: {stats.get('tokens_earned', 0)}")
|
|
80
|
+
console.print(f" Uptime: {stats.get('uptime', '0s')}")
|
|
81
|
+
|
|
82
|
+
console.print("\nPress Ctrl+C to stop mining\n")
|
|
83
|
+
console.print("[dim]Logs:[/dim]")
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
# Stream logs and stats
|
|
87
|
+
async for event in miner.stream_events():
|
|
88
|
+
if event["type"] == "log":
|
|
89
|
+
console.print(event["message"], end="")
|
|
90
|
+
elif event["type"] == "job":
|
|
91
|
+
console.print(f"[green]Job completed:[/green] {event['job_id']} (+{event['tokens']} tokens)")
|
|
92
|
+
elif event["type"] == "error":
|
|
93
|
+
console.print(f"[red]Error:[/red] {event['message']}")
|
|
94
|
+
except KeyboardInterrupt:
|
|
95
|
+
console.print("\n[yellow]Stopping miner...[/yellow]")
|
|
96
|
+
await miner.stop()
|
|
97
|
+
|
|
98
|
+
# Show final stats
|
|
99
|
+
final_stats = await miner.get_stats()
|
|
100
|
+
if final_stats:
|
|
101
|
+
console.print("\n[cyan]Session Summary:[/cyan]")
|
|
102
|
+
console.print(f" Jobs completed: {final_stats.get('jobs_completed', 0)}")
|
|
103
|
+
console.print(f" Tokens earned: {final_stats.get('tokens_earned', 0)}")
|
|
104
|
+
console.print(f" Average job time: {final_stats.get('avg_job_time', 'N/A')}")
|
|
105
|
+
|
|
106
|
+
console.print("[green]✓[/green] Miner stopped")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@miner_group.command()
|
|
110
|
+
@click.option("--name", "-n", help="Miner name")
|
|
111
|
+
@click.pass_context
|
|
112
|
+
async def stop(ctx, name: str):
|
|
113
|
+
"""Stop mining."""
|
|
114
|
+
try:
|
|
115
|
+
from hanzo_miner import HanzoMiner
|
|
116
|
+
except ImportError:
|
|
117
|
+
console.print("[red]Error:[/red] hanzo-miner not installed")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
if name:
|
|
121
|
+
miner = HanzoMiner.get_by_name(name)
|
|
122
|
+
if miner:
|
|
123
|
+
console.print(f"[yellow]Stopping miner '{name}'...[/yellow]")
|
|
124
|
+
await miner.stop()
|
|
125
|
+
console.print(f"[green]✓[/green] Miner stopped")
|
|
126
|
+
else:
|
|
127
|
+
console.print(f"[red]Miner not found: {name}[/red]")
|
|
128
|
+
else:
|
|
129
|
+
# Stop all miners
|
|
130
|
+
miners = HanzoMiner.get_all()
|
|
131
|
+
if miners:
|
|
132
|
+
if click.confirm(f"Stop all {len(miners)} miners?"):
|
|
133
|
+
for miner in miners:
|
|
134
|
+
await miner.stop()
|
|
135
|
+
console.print(f"[green]✓[/green] Stopped {len(miners)} miners")
|
|
136
|
+
else:
|
|
137
|
+
console.print("[yellow]No miners running[/yellow]")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@miner_group.command()
|
|
141
|
+
@click.option("--name", "-n", help="Miner name")
|
|
142
|
+
@click.option("--detailed", "-d", is_flag=True, help="Show detailed stats")
|
|
143
|
+
@click.pass_context
|
|
144
|
+
async def status(ctx, name: str, detailed: bool):
|
|
145
|
+
"""Show mining status."""
|
|
146
|
+
try:
|
|
147
|
+
from hanzo_miner import HanzoMiner
|
|
148
|
+
except ImportError:
|
|
149
|
+
console.print("[red]Error:[/red] hanzo-miner not installed")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
if name:
|
|
153
|
+
# Show specific miner
|
|
154
|
+
miner = HanzoMiner.get_by_name(name)
|
|
155
|
+
if not miner:
|
|
156
|
+
console.print(f"[red]Miner not found: {name}[/red]")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
miners = [miner]
|
|
160
|
+
else:
|
|
161
|
+
# Show all miners
|
|
162
|
+
miners = HanzoMiner.get_all()
|
|
163
|
+
|
|
164
|
+
if not miners:
|
|
165
|
+
console.print("[yellow]No miners running[/yellow]")
|
|
166
|
+
console.print("Start mining with: hanzo miner start")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
# Create table
|
|
170
|
+
table = Table(title="Active Miners")
|
|
171
|
+
table.add_column("Name", style="cyan")
|
|
172
|
+
table.add_column("Status", style="green")
|
|
173
|
+
table.add_column("Device", style="yellow")
|
|
174
|
+
table.add_column("Jobs", style="blue")
|
|
175
|
+
table.add_column("Tokens", style="magenta")
|
|
176
|
+
table.add_column("Uptime", style="white")
|
|
177
|
+
|
|
178
|
+
for miner in miners:
|
|
179
|
+
stats = await miner.get_stats()
|
|
180
|
+
table.add_row(
|
|
181
|
+
miner.name,
|
|
182
|
+
stats.get("status", "unknown"),
|
|
183
|
+
stats.get("device", "unknown"),
|
|
184
|
+
str(stats.get("jobs_completed", 0)),
|
|
185
|
+
f"{stats.get('tokens_earned', 0):.2f}",
|
|
186
|
+
stats.get("uptime", "0s")
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
console.print(table)
|
|
190
|
+
|
|
191
|
+
if detailed and len(miners) == 1:
|
|
192
|
+
# Show detailed stats for single miner
|
|
193
|
+
miner = miners[0]
|
|
194
|
+
stats = await miner.get_stats()
|
|
195
|
+
|
|
196
|
+
console.print("\n[cyan]Detailed Statistics:[/cyan]")
|
|
197
|
+
console.print(f" Network: {stats.get('network', 'unknown')}")
|
|
198
|
+
console.print(f" Wallet: {stats.get('wallet', 'Not set')}")
|
|
199
|
+
console.print(f" Models: {', '.join(stats.get('models', []))}")
|
|
200
|
+
console.print(f" Memory: {stats.get('memory_used', 0)} / {stats.get('memory_total', 0)} MB")
|
|
201
|
+
console.print(f" CPU: {stats.get('cpu_percent', 0)}%")
|
|
202
|
+
|
|
203
|
+
if gpu := stats.get("gpu"):
|
|
204
|
+
console.print(f" GPU: {gpu['name']} ({gpu['memory_used']} / {gpu['memory_total']} MB)")
|
|
205
|
+
|
|
206
|
+
console.print(f"\n[cyan]Performance:[/cyan]")
|
|
207
|
+
console.print(f" Average job time: {stats.get('avg_job_time', 'N/A')}")
|
|
208
|
+
console.print(f" Success rate: {stats.get('success_rate', 0)}%")
|
|
209
|
+
console.print(f" Tokens/hour: {stats.get('tokens_per_hour', 0):.2f}")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@miner_group.command()
|
|
213
|
+
@click.option("--network", default="mainnet", help="Network to check")
|
|
214
|
+
@click.pass_context
|
|
215
|
+
async def leaderboard(ctx, network: str):
|
|
216
|
+
"""Show mining leaderboard."""
|
|
217
|
+
try:
|
|
218
|
+
from hanzo_miner import get_leaderboard
|
|
219
|
+
except ImportError:
|
|
220
|
+
console.print("[red]Error:[/red] hanzo-miner not installed")
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
with console.status("Loading leaderboard..."):
|
|
224
|
+
try:
|
|
225
|
+
leaders = await get_leaderboard(network=network)
|
|
226
|
+
except Exception as e:
|
|
227
|
+
console.print(f"[red]Failed to load leaderboard: {e}[/red]")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
if not leaders:
|
|
231
|
+
console.print("[yellow]No data available[/yellow]")
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
table = Table(title=f"Mining Leaderboard - {network}")
|
|
235
|
+
table.add_column("Rank", style="cyan")
|
|
236
|
+
table.add_column("Miner", style="green")
|
|
237
|
+
table.add_column("Jobs", style="yellow")
|
|
238
|
+
table.add_column("Tokens", style="magenta")
|
|
239
|
+
table.add_column("Success Rate", style="blue")
|
|
240
|
+
|
|
241
|
+
for i, leader in enumerate(leaders[:20], 1):
|
|
242
|
+
table.add_row(
|
|
243
|
+
str(i),
|
|
244
|
+
leader["name"],
|
|
245
|
+
str(leader["jobs"]),
|
|
246
|
+
f"{leader['tokens']:.2f}",
|
|
247
|
+
f"{leader['success_rate']}%"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
console.print(table)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@miner_group.command()
|
|
254
|
+
@click.option("--wallet", "-w", required=True, help="Wallet address")
|
|
255
|
+
@click.option("--network", default="mainnet", help="Network")
|
|
256
|
+
@click.pass_context
|
|
257
|
+
async def earnings(ctx, wallet: str, network: str):
|
|
258
|
+
"""Check mining earnings."""
|
|
259
|
+
try:
|
|
260
|
+
from hanzo_miner import check_earnings
|
|
261
|
+
except ImportError:
|
|
262
|
+
console.print("[red]Error:[/red] hanzo-miner not installed")
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
with console.status("Checking earnings..."):
|
|
266
|
+
try:
|
|
267
|
+
data = await check_earnings(wallet=wallet, network=network)
|
|
268
|
+
except Exception as e:
|
|
269
|
+
console.print(f"[red]Failed to check earnings: {e}[/red]")
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
console.print(f"[cyan]Earnings for {wallet[:8]}...{wallet[-4:]}[/cyan]")
|
|
273
|
+
console.print(f" Network: {network}")
|
|
274
|
+
console.print(f" Total earned: {data.get('total_earned', 0):.2f} tokens")
|
|
275
|
+
console.print(f" Available: {data.get('available', 0):.2f} tokens")
|
|
276
|
+
console.print(f" Pending: {data.get('pending', 0):.2f} tokens")
|
|
277
|
+
|
|
278
|
+
if history := data.get("recent_jobs"):
|
|
279
|
+
console.print("\n[cyan]Recent Jobs:[/cyan]")
|
|
280
|
+
for job in history[:5]:
|
|
281
|
+
console.print(f" • {job['timestamp']}: +{job['tokens']} tokens ({job['model']})")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@miner_group.command()
|
|
285
|
+
@click.argument("amount", type=float)
|
|
286
|
+
@click.option("--wallet", "-w", required=True, help="Wallet address")
|
|
287
|
+
@click.option("--to", required=True, help="Destination address")
|
|
288
|
+
@click.option("--network", default="mainnet", help="Network")
|
|
289
|
+
@click.pass_context
|
|
290
|
+
async def withdraw(ctx, amount: float, wallet: str, to: str, network: str):
|
|
291
|
+
"""Withdraw mining earnings."""
|
|
292
|
+
try:
|
|
293
|
+
from hanzo_miner import withdraw_earnings
|
|
294
|
+
except ImportError:
|
|
295
|
+
console.print("[red]Error:[/red] hanzo-miner not installed")
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
# Confirm withdrawal
|
|
299
|
+
console.print(f"[yellow]Withdrawal Request:[/yellow]")
|
|
300
|
+
console.print(f" Amount: {amount} tokens")
|
|
301
|
+
console.print(f" From: {wallet[:8]}...{wallet[-4:]}")
|
|
302
|
+
console.print(f" To: {to[:8]}...{to[-4:]}")
|
|
303
|
+
console.print(f" Network: {network}")
|
|
304
|
+
|
|
305
|
+
if not click.confirm("Proceed with withdrawal?"):
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
with console.status("Processing withdrawal..."):
|
|
309
|
+
try:
|
|
310
|
+
result = await withdraw_earnings(
|
|
311
|
+
wallet=wallet,
|
|
312
|
+
amount=amount,
|
|
313
|
+
destination=to,
|
|
314
|
+
network=network
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
console.print(f"[green]✓[/green] Withdrawal successful")
|
|
318
|
+
console.print(f" Transaction: {result['tx_hash']}")
|
|
319
|
+
console.print(f" Amount: {result['amount']} tokens")
|
|
320
|
+
console.print(f" Fee: {result['fee']} tokens")
|
|
321
|
+
|
|
322
|
+
except Exception as e:
|
|
323
|
+
console.print(f"[red]Withdrawal failed: {e}[/red]")
|