feat(agent): agentic proxy with Claude Code integration

Enhanced agent tool architecture:
- 8 CLI agents: claude, codex, gemini, grok, qwen, vibe, code, dev
- Auto-detects Claude Code environment for seamless auth
- Shares hanzo-mcp config with spawned agents
- Lightweight - no heavy litellm dependency

Essential tools reorganization:
- Added agent, think, critic to essential tools
- Removed llm/consensus from essential (heavy deps, opt-in)
- HEAVY_TOOLS constant for tracking opt-in tools

Config sharing:
- HANZO_AGENT_PARENT=true passed to child agents
- MCP config env vars propagated
- API keys shared when appropriate
This commit is contained in:
2025-12-26 12:25:16 -08:00
parent da791ef8f4
commit d364e8a36f
50 changed files with 1138 additions and 1131 deletions
@@ -11,10 +11,12 @@ from hanzo_mcp.tools.common.mode import (
# Essential system tools that are ALWAYS enabled regardless of mode
# These are core infrastructure tools that should never be disabled
# NOTE: llm and consensus are NOT essential (heavy litellm dependency)
ESSENTIAL_SYSTEM_TOOLS: Set[str] = {
# Core LLM infrastructure
"llm", # Primary LLM interaction
"consensus", # Multi-model consensus
# Reasoning (lightweight)
"think", # Structured thinking
"critic", # Critical analysis
"agent", # Lightweight agent spawning
# Configuration and mode management
"config", # System configuration
"mode", # Mode switching
@@ -27,6 +29,13 @@ ESSENTIAL_SYSTEM_TOOLS: Set[str] = {
"memory", # Unified memory operations
}
# Heavy tools that are disabled by default (opt-in)
# These require large dependencies like litellm
HEAVY_TOOLS: Set[str] = {
"llm", # Requires litellm (~100MB+ deps)
"consensus", # Requires litellm
}
class ModeLoader:
"""Loads and manages tool modes for dynamic configuration."""
@@ -99,6 +99,7 @@ class PersonalityRegistry:
# Essential tools that are always available in every mode
# NOTE: llm/consensus are NOT essential (heavy litellm dependency - opt-in only)
ESSENTIAL_TOOLS = [
# File operations
"read",
@@ -112,12 +113,10 @@ ESSENTIAL_TOOLS = [
"open",
# Memory
"memory",
# Reasoning
# Reasoning (lightweight)
"think",
"critic",
# LLM infrastructure (always available)
"llm",
"consensus",
"agent", # Lightweight agent spawning (claude, codex, etc.)
# Configuration
"config",
"mode",
@@ -125,6 +124,13 @@ ESSENTIAL_TOOLS = [
"tool",
]
# Heavy tools (require large dependencies like litellm)
# These are opt-in only, not included by default
HEAVY_TOOLS = [
"llm", # Requires litellm (~100MB deps)
"consensus", # Requires litellm
]
# Common tool sets for reuse
UNIX_TOOLS = ["search", "find", "dag", "ps", "zsh"]
BUILD_TOOLS = ["dag", "npx", "uvx", "ps"]
+33 -2
View File
@@ -4,11 +4,10 @@ from pathlib import Path
from unittest.mock import Mock
import pytest
from hanzo_tools.agent import TOOLS, register_tools
from hanzo_tools.agent.unified_agent_tool import UnifiedAgentTool
from hanzo_tools.agent.iching_tool import IChingTool
from hanzo_tools.agent.review_tool import ReviewTool
from hanzo_tools.agent.unified_agent_tool import UnifiedAgentTool
@pytest.fixture
@@ -39,10 +38,18 @@ class TestUnifiedAgentTools:
def test_unified_agent_tool_agents(self):
"""Test UnifiedAgentTool has expected agents."""
tool = UnifiedAgentTool()
# Core agents
assert "claude" in tool.AGENTS
assert "codex" in tool.AGENTS
assert "gemini" in tool.AGENTS
assert "grok" in tool.AGENTS
# Extended agents
assert "qwen" in tool.AGENTS
assert "vibe" in tool.AGENTS
assert "code" in tool.AGENTS
assert "dev" in tool.AGENTS
# Total 8 agents
assert len(tool.AGENTS) == 8
def test_unified_agent_tool_list_agents(self):
"""Test agent listing."""
@@ -88,6 +95,30 @@ class TestUnifiedAgentTools:
assert iching.name == "iching"
assert review.name == "review"
def test_agent_config_sharing(self):
"""Test agent shares MCP config with spawned agents."""
tool = UnifiedAgentTool()
config = tool._mcp_config
# Config is a dict
assert isinstance(config, dict)
def test_claude_env_detection(self):
"""Test Claude Code environment detection."""
tool = UnifiedAgentTool()
env = tool._claude_env
# Should have expected keys
assert "running_in_claude" in env
assert "session_id" in env
assert "auth_token" in env
assert "api_key" in env
def test_default_agent_selection(self):
"""Test default agent selection logic."""
tool = UnifiedAgentTool()
default = tool._get_default_agent()
# Should return a valid agent name
assert default in tool.AGENTS
if __name__ == "__main__":
pytest.main([__file__, "-v"])
@@ -21,24 +21,24 @@ import logging
logger = logging.getLogger(__name__)
# Core consolidated tools
from .unified_agent_tool import UnifiedAgentTool
from .critic_tool import CriticTool # Legacy - use reasoning.critic instead
from .iching_tool import IChingTool
from .review_tool import ReviewTool
from .grok_cli_tool import GrokCLITool
# Legacy imports for backwards compatibility
from .cli_agent_base import CLIAgentBase
from .claude_cli_tool import ClaudeCLITool
from .codex_cli_tool import CodexCLITool
from .claude_cli_tool import ClaudeCLITool
from .gemini_cli_tool import GeminiCLITool
from .grok_cli_tool import GrokCLITool
from .critic_tool import CriticTool # Legacy - use reasoning.critic instead
from .unified_agent_tool import UnifiedAgentTool
# Export list for tool discovery - consolidated to 3 tools
# Note: critic is from hanzo-tools-reasoning, not here
TOOLS = [
UnifiedAgentTool, # Unified agent runner
IChingTool, # I Ching wisdom
ReviewTool, # Code review
IChingTool, # I Ching wisdom
ReviewTool, # Code review
]
__all__ = [
@@ -29,15 +29,16 @@ with warnings.catch_warnings():
from pydantic import Field
from openai.types.chat import ChatCompletionMessageParam
from mcp.server.fastmcp import Context as MCPContext
from hanzo_tools.jupyter import get_read_only_jupyter_tools
from hanzo_tools.filesystem import get_read_only_filesystem_tools
from hanzo_tools.core import (
BaseTool,
PermissionManager,
ToolContext,
create_tool_context,
PermissionManager,
auto_timeout,
create_tool_context,
)
from hanzo_tools.jupyter import get_read_only_jupyter_tools
from hanzo_tools.filesystem import get_read_only_filesystem_tools
from .prompt import (
get_default_model,
@@ -89,9 +89,9 @@ except ImportError:
self.metadata = metadata or {}
from hanzo_tools.core import BaseTool, PermissionManager, create_tool_context
from hanzo_tools.jupyter import get_read_only_jupyter_tools
from hanzo_tools.filesystem import Edit, get_read_only_filesystem_tools
from hanzo_tools.core import BaseTool, PermissionManager, create_tool_context
from .critic_tool import CriticTool
from .iching_tool import IChingTool
@@ -54,7 +54,8 @@ class ClaudeDesktopAuth:
# Check if claude command is available
try:
process = await asyncio.create_subprocess_exec(
"which", "claude",
"which",
"claude",
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
@@ -267,12 +267,13 @@ class CodeAuthManager:
# Check if claude command exists
try:
process = await asyncio.create_subprocess_exec(
"which", "claude",
"which",
"claude",
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
await asyncio.wait_for(process.wait(), timeout=5)
if process.returncode == 0:
# Claude command exists, update its config
claude_config = Path.home() / ".claude" / "config.json"
@@ -296,9 +297,10 @@ class CodeAuthManager:
"""Update claude command configuration if needed (sync wrapper)."""
# Use sync check for existence, skip if not found
import shutil
if not shutil.which("claude"):
return
claude_config = Path.home() / ".claude" / "config.json"
if claude_config.exists():
try:
@@ -44,10 +44,7 @@ except ImportError:
"""Metaclass that raises ImportError when stub class is instantiated."""
def __call__(cls, *args, **kwargs):
raise ImportError(
f"{cls.__name__} requires hanzo-agents package. "
"Install with: pip install hanzo-agents"
)
raise ImportError(f"{cls.__name__} requires hanzo-agents package. Install with: pip install hanzo-agents")
class Agent(metaclass=_StubMeta):
"""Stub - requires hanzo-agents."""
@@ -104,10 +101,7 @@ def _stub_factory(name: str):
"""Create a stub function that raises ImportError."""
def stub(*args, **kwargs):
raise ImportError(
f"{name} requires hanzo-agents package. "
"Install with: pip install hanzo-agents"
)
raise ImportError(f"{name} requires hanzo-agents package. Install with: pip install hanzo-agents")
stub.__name__ = name
return stub
@@ -160,9 +154,9 @@ except ImportError:
"""Stub - requires hanzo-agents."""
from hanzo_tools.core import BaseTool, PermissionManager, create_tool_context
from hanzo_tools.jupyter import get_read_only_jupyter_tools
from hanzo_tools.filesystem import Edit, get_read_only_filesystem_tools
from hanzo_tools.core import BaseTool, PermissionManager, create_tool_context
from .agent_tool import MCPAgent
@@ -1,10 +1,26 @@
"""Unified agent tool - run any agent by name.
"""Unified agent tool - lightweight agent spawning.
Single tool that dispatches to installed CLI agents (claude, codex, gemini, grok)
and orchestrates multi-agent workflows.
Single tool that dispatches to installed CLI agents and supports:
- claude: Claude Code CLI (default when running in Claude Code)
- codex: OpenAI Codex CLI
- gemini: Google Gemini CLI
- grok: xAI Grok CLI
- qwen: Alibaba Qwen CLI
- vibe: Vibe coding agent
- code: Hanzo Code agent
- dev: Hanzo Dev agent (default)
Key features:
- Auto-detects Claude Code environment and uses same auth
- Shares hanzo-mcp config with spawned agents
- Lightweight - no heavy dependencies
"""
from typing import Optional, List, Dict, Any, Literal, final, override, Annotated
import os
import json
import asyncio
from typing import List, Literal, Optional, Annotated, final, override
from pathlib import Path
from pydantic import Field
from mcp.server import FastMCP
@@ -12,22 +28,85 @@ from mcp.server.fastmcp import Context as MCPContext
from hanzo_tools.core import BaseTool, auto_timeout, create_tool_context
Action = Annotated[
Literal[
"run", # Run a specific agent
"list", # List available agents
"status", # Check agent status
"run", # Run a specific agent
"list", # List available agents
"status", # Check agent status
"config", # Show/set agent config
],
Field(description="Agent action to perform"),
]
def detect_claude_code_env() -> dict:
"""Detect if running inside Claude Code and get auth info.
Returns dict with:
running_in_claude: bool
session_id: Optional[str]
auth_token: Optional[str]
api_key: Optional[str]
"""
result = {
"running_in_claude": False,
"session_id": None,
"auth_token": None,
"api_key": None,
}
# Check Claude Code environment markers
if os.environ.get("CLAUDE_CODE") or os.environ.get("CLAUDE_SESSION_ID"):
result["running_in_claude"] = True
result["session_id"] = os.environ.get("CLAUDE_SESSION_ID")
# Check for Claude auth
if os.environ.get("ANTHROPIC_API_KEY"):
result["api_key"] = os.environ.get("ANTHROPIC_API_KEY")
# Check Claude Desktop config for OAuth tokens
claude_config_path = Path.home() / "Library/Application Support/Claude/claude_desktop_config.json"
if claude_config_path.exists():
try:
with open(claude_config_path) as f:
config = json.load(f)
if "mcpServers" in config and "hanzo-mcp" in config["mcpServers"]:
result["running_in_claude"] = True
except Exception:
pass
return result
def get_mcp_config() -> dict:
"""Get current hanzo-mcp config to share with spawned agents."""
config = {}
# Get relevant environment variables
mcp_env_vars = [
"HANZO_MCP_MODE",
"HANZO_MCP_ALLOWED_PATHS",
"HANZO_MCP_ENABLED_TOOLS",
"HANZO_MCP_PERSONA",
"ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
]
for var in mcp_env_vars:
if os.environ.get(var):
config[var] = os.environ.get(var)
return config
@final
class UnifiedAgentTool(BaseTool):
"""Unified agent tool for running CLI agents.
Dispatches to: claude, codex, gemini, grok or custom agents.
Lightweight agent spawning that:
- Auto-detects Claude Code environment
- Shares hanzo-mcp config with child agents
- Supports multiple agent backends
"""
name = "agent"
@@ -36,56 +115,123 @@ class UnifiedAgentTool(BaseTool):
AGENTS = {
"claude": {
"command": "claude",
"description": "Anthropic Claude Code CLI",
"args": ["-p"], # Use -p for print mode (non-interactive)
"description": "Anthropic Claude Code CLI (recommended when in Claude)",
"check": ["claude", "--version"],
"env_key": "ANTHROPIC_API_KEY",
"priority": 1, # Highest priority when in Claude env
},
"codex": {
"command": "codex",
"args": [],
"description": "OpenAI Codex CLI",
"check": ["codex", "--version"],
"env_key": "OPENAI_API_KEY",
"priority": 2,
},
"gemini": {
"command": "gemini",
"args": [],
"description": "Google Gemini CLI",
"check": ["gemini", "--version"],
"env_key": "GOOGLE_API_KEY",
"priority": 3,
},
"grok": {
"command": "grok",
"args": [],
"description": "xAI Grok CLI",
"check": ["grok", "--version"],
"env_key": "XAI_API_KEY",
"priority": 4,
},
"qwen": {
"command": "qwen",
"args": [],
"description": "Alibaba Qwen CLI",
"check": ["qwen", "--version"],
"env_key": "DASHSCOPE_API_KEY",
"priority": 5,
},
"vibe": {
"command": "vibe",
"args": [],
"description": "Vibe coding agent",
"check": ["vibe", "--version"],
"priority": 6,
},
"code": {
"command": "hanzo-code",
"args": [],
"description": "Hanzo Code agent",
"check": ["hanzo-code", "--version"],
"priority": 7,
},
"dev": {
"command": "hanzo-dev",
"args": [],
"description": "Hanzo Dev agent (full development assistant)",
"check": ["hanzo-dev", "--version"],
"priority": 8,
},
}
def __init__(self):
super().__init__()
self._claude_env = detect_claude_code_env()
self._mcp_config = get_mcp_config()
@property
@override
def description(self) -> str:
return """Run AI agents by name.
default_agent = self._get_default_agent()
return f"""Run AI agents by name. Lightweight agent spawning.
Actions:
- run: Execute an agent with a prompt
- run: Execute an agent with a prompt (default: {default_agent})
- list: List available agents
- status: Check if an agent is available
- status: Check agent availability
- config: Show/set agent configuration
Agents: claude, codex, gemini, grok
Agents: claude, codex, gemini, grok, qwen, vibe, code, dev
Examples:
agent run --name claude --prompt "Explain this code"
agent run --name codex --prompt "Write a function" --cwd /path/to/project
agent run --prompt "Explain this code" # Uses default agent
agent run --name claude --prompt "Review this PR"
agent run --name dev --prompt "Fix the build" --cwd /project
agent list
agent status --name claude
agent status
{"⚡ Running in Claude Code - claude agent uses same auth" if self._claude_env["running_in_claude"] else ""}
"""
def _get_default_agent(self) -> str:
"""Get the default agent based on environment."""
# If running in Claude Code, prefer claude
if self._claude_env.get("running_in_claude"):
return "claude"
# Otherwise check which agents are configured
for name, config in sorted(self.AGENTS.items(), key=lambda x: x[1].get("priority", 99)):
env_key = config.get("env_key")
if env_key and os.environ.get(env_key):
return name
# Default fallback
return "dev"
@override
@auto_timeout("agent")
async def call(
self,
ctx: MCPContext,
action: str = "list",
action: str = "run",
name: Optional[str] = None,
prompt: Optional[str] = None,
cwd: Optional[str] = None,
args: Optional[List[str]] = None,
timeout: int = 300,
share_config: bool = True,
**kwargs,
) -> str:
"""Execute agent operation."""
@@ -96,31 +242,75 @@ Examples:
return self._list_agents()
elif action == "status":
return await self._check_status(name)
elif action == "config":
return self._show_config()
elif action == "run":
return await self._run_agent(name, prompt, cwd, args, timeout)
# Use default agent if not specified
agent_name = name or self._get_default_agent()
return await self._run_agent(agent_name, prompt, cwd, args, timeout, share_config)
else:
return f"Unknown action: {action}. Use: run, list, status"
return f"Unknown action: {action}. Use: run, list, status, config"
def _list_agents(self) -> str:
"""List available agents."""
default = self._get_default_agent()
lines = ["Available agents:"]
for name, config in self.AGENTS.items():
lines.append(f" • {name}: {config['description']}")
for name, config in sorted(self.AGENTS.items(), key=lambda x: x[1].get("priority", 99)):
marker = " (default)" if name == default else ""
lines.append(f" • {name}: {config['description']}{marker}")
lines.append("")
lines.append("Usage: agent run --name <agent> --prompt 'your prompt'")
lines.append("Usage: agent run --prompt 'your prompt'")
lines.append(f" agent run --name <agent> --prompt 'prompt'")
if self._claude_env.get("running_in_claude"):
lines.append("")
lines.append("⚡ Running in Claude Code - using same authentication")
return "\n".join(lines)
def _show_config(self) -> str:
"""Show current agent configuration."""
lines = ["Agent Configuration:"]
lines.append(f" Default agent: {self._get_default_agent()}")
lines.append(f" Running in Claude: {self._claude_env.get('running_in_claude', False)}")
if self._claude_env.get("session_id"):
lines.append(f" Claude session: {self._claude_env['session_id'][:8]}...")
lines.append("")
lines.append("MCP Config (shared with spawned agents):")
for key, value in self._mcp_config.items():
# Mask sensitive values
if "KEY" in key or "TOKEN" in key:
display = value[:8] + "..." if len(value) > 8 else "***"
else:
display = value
lines.append(f" {key}: {display}")
return "\n".join(lines)
async def _check_status(self, name: Optional[str]) -> str:
"""Check if an agent is available."""
import asyncio
"""Check if agents are available."""
if not name:
# Check all agents
results = []
for agent_name, config in self.AGENTS.items():
for agent_name, config in sorted(self.AGENTS.items(), key=lambda x: x[1].get("priority", 99)):
available = await self._is_available(config["check"])
status = "✓ available" if available else "✗ not found"
# Check for API key
env_key = config.get("env_key")
has_key = bool(env_key and os.environ.get(env_key))
if available:
key_status = "✓ key" if has_key else "○ no key"
status = f"✓ installed ({key_status})"
else:
status = "✗ not found"
results.append(f" {agent_name}: {status}")
return "Agent status:\n" + "\n".join(results)
if name not in self.AGENTS:
@@ -134,8 +324,6 @@ Examples:
async def _is_available(self, check_cmd: List[str]) -> bool:
"""Check if a command is available."""
import asyncio
try:
proc = await asyncio.create_subprocess_exec(
*check_cmd,
@@ -149,18 +337,14 @@ Examples:
async def _run_agent(
self,
name: Optional[str],
name: str,
prompt: Optional[str],
cwd: Optional[str],
args: Optional[List[str]],
timeout: int,
share_config: bool,
) -> str:
"""Run an agent with a prompt."""
import asyncio
import os
if not name:
return "Error: name required for run action"
if not prompt:
return "Error: prompt required for run action"
@@ -169,18 +353,28 @@ Examples:
config = self.AGENTS[name]
command = config["command"]
default_args = config.get("args", [])
# Build command
cmd_args = [command, prompt]
cmd_args = [command] + default_args + [prompt]
if args:
cmd_args.extend(args)
# Build environment with shared MCP config
env = os.environ.copy()
if share_config:
env.update(self._mcp_config)
# Mark that this is a child agent
env["HANZO_AGENT_PARENT"] = "true"
env["HANZO_AGENT_NAME"] = name
try:
proc = await asyncio.create_subprocess_exec(
*cmd_args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=cwd or os.getcwd(),
env=env,
)
try:
@@ -202,7 +396,17 @@ Examples:
return f"Agent {name} timed out after {timeout}s"
except FileNotFoundError:
return f"Agent {name} not found. Install it or check PATH."
# Provide helpful installation instructions
install_hints = {
"claude": "npm install -g @anthropic-ai/claude-code",
"codex": "npm install -g @openai/codex",
"gemini": "pip install google-generativeai",
"grok": "pip install xai-grok",
"dev": "pip install hanzo-dev",
"code": "pip install hanzo-code",
}
hint = install_hints.get(name, f"Install the {name} CLI")
return f"Agent {name} not found.\n\nTo install: {hint}"
except Exception as e:
return f"Error running {name}: {e}"
@@ -212,15 +416,22 @@ Examples:
@mcp_server.tool()
async def agent(
action: Action = "list",
name: Annotated[Optional[str], Field(description="Agent name: claude, codex, gemini, grok")] = None,
action: Action = "run",
name: Annotated[
Optional[str], Field(description="Agent: claude, codex, gemini, grok, qwen, vibe, code, dev")
] = None,
prompt: Annotated[Optional[str], Field(description="Prompt for the agent")] = None,
cwd: Annotated[Optional[str], Field(description="Working directory")] = None,
args: Annotated[Optional[List[str]], Field(description="Additional arguments")] = None,
timeout: Annotated[int, Field(description="Timeout in seconds")] = 300,
share_config: Annotated[bool, Field(description="Share hanzo-mcp config with agent")] = True,
ctx: MCPContext = None,
) -> str:
"""Run AI agents: claude, codex, gemini, grok."""
"""Run AI agents: claude, codex, gemini, grok, qwen, vibe, code, dev.
Lightweight agent spawning with shared MCP config.
Auto-detects Claude Code environment for seamless auth.
"""
return await tool_instance.call(
ctx,
action=action,
@@ -229,4 +440,5 @@ Examples:
cwd=cwd,
args=args,
timeout=timeout,
share_config=share_config,
)
File diff suppressed because it is too large Load Diff
@@ -11,22 +11,22 @@ Performance optimizations:
- Async-first design with no blocking in event loop
"""
import asyncio
import base64
import functools
import io
import json
import re
import subprocess
import sys
import json
import time
from concurrent.futures import ThreadPoolExecutor
import base64
import asyncio
import functools
import subprocess
from typing import Any, Literal, Optional, Annotated, final, override
from pathlib import Path
from typing import Annotated, Any, Literal, Optional, final, override
from concurrent.futures import ThreadPoolExecutor
from pydantic import Field
from mcp.server import FastMCP
from mcp.server.fastmcp import Context as MCPContext
from pydantic import Field
from hanzo_tools.core import BaseTool, PermissionManager, auto_timeout
@@ -37,28 +37,56 @@ _EXECUTOR = ThreadPoolExecutor(max_workers=4, thread_name_prefix="computer_")
# All supported actions
Action = Literal[
# Mouse actions
"click", "double_click", "right_click", "middle_click",
"move", "move_relative", "drag", "drag_relative", "scroll",
"click",
"double_click",
"right_click",
"middle_click",
"move",
"move_relative",
"drag",
"drag_relative",
"scroll",
# Keyboard actions
"type", "write", "press", "key_down", "key_up", "hotkey",
"type",
"write",
"press",
"key_down",
"key_up",
"hotkey",
# Screen actions
"screenshot", "screenshot_region",
"screenshot",
"screenshot_region",
# Image location
"locate", "locate_all", "locate_center", "wait_for_image", "wait_while_image",
"locate",
"locate_all",
"locate_center",
"wait_for_image",
"wait_while_image",
# Pixel operations
"pixel", "pixel_matches",
"pixel",
"pixel_matches",
# Window management
"get_active_window", "list_windows", "focus_window",
"get_active_window",
"list_windows",
"focus_window",
# Screen management
"get_screens", "screen_size", "current_screen",
"get_screens",
"screen_size",
"current_screen",
# Region helpers
"define_region", "region_screenshot", "region_locate",
"define_region",
"region_screenshot",
"region_locate",
# Timing/flow
"sleep", "countdown", "set_pause", "set_failsafe",
"sleep",
"countdown",
"set_pause",
"set_failsafe",
# Batch operations
"batch",
# Info
"info", "position",
"info",
"position",
]
@@ -568,11 +596,13 @@ Examples:
displays = []
for gpu in data.get("SPDisplaysDataType", []):
for disp in gpu.get("spdisplays_ndrvs", []):
displays.append({
"name": disp.get("_name", "Unknown"),
"resolution": disp.get("_spdisplays_resolution", "Unknown"),
"main": disp.get("spdisplays_main") == "spdisplays_yes",
})
displays.append(
{
"name": disp.get("_name", "Unknown"),
"resolution": disp.get("_spdisplays_resolution", "Unknown"),
"main": disp.get("spdisplays_main") == "spdisplays_yes",
}
)
return json.dumps(displays, indent=2)
except Exception as e:
pass
@@ -611,16 +641,16 @@ Examples:
pg = self._ensure_pyautogui()
pos = pg.position()
size = pg.size()
return json.dumps({
"size": {"width": size.width, "height": size.height},
"mouse": {"x": pos.x, "y": pos.y},
})
return json.dumps(
{
"size": {"width": size.width, "height": size.height},
"mouse": {"x": pos.x, "y": pos.y},
}
)
# ========== Image Location Methods ==========
def _locate(
self, image_path: str, confidence: float, region: tuple | None
) -> str:
def _locate(self, image_path: str, confidence: float, region: tuple | None) -> str:
pg = self._ensure_pyautogui()
path = Path(image_path)
if not path.exists():
@@ -636,12 +666,18 @@ Examples:
location = pg.locateOnScreen(str(path), **kwargs)
if location:
center = pg.center(location)
return json.dumps({
"found": True,
"center": {"x": center.x, "y": center.y},
"box": {"left": location.left, "top": location.top,
"width": location.width, "height": location.height},
})
return json.dumps(
{
"found": True,
"center": {"x": center.x, "y": center.y},
"box": {
"left": location.left,
"top": location.top,
"width": location.width,
"height": location.height,
},
}
)
else:
return json.dumps({"found": False, "message": "Image not found on screen"})
except Exception as e:
@@ -662,11 +698,12 @@ Examples:
results = []
for loc in locations:
center = pg.center(loc)
results.append({
"center": {"x": center.x, "y": center.y},
"box": {"left": loc.left, "top": loc.top,
"width": loc.width, "height": loc.height},
})
results.append(
{
"center": {"x": center.x, "y": center.y},
"box": {"left": loc.left, "top": loc.top, "width": loc.width, "height": loc.height},
}
)
return json.dumps({"found": len(results), "locations": results})
except Exception as e:
return f"Error locating images: {str(e)}"
@@ -690,9 +727,7 @@ Examples:
except Exception as e:
return f"Error: {str(e)}"
async def _wait_for_image(
self, image_path: str, timeout: float, confidence: float, run
) -> str:
async def _wait_for_image(self, image_path: str, timeout: float, confidence: float, run) -> str:
"""Wait until image appears on screen."""
path = Path(image_path)
if not path.exists():
@@ -704,19 +739,19 @@ Examples:
data = json.loads(result) if result.startswith("{") else {}
if data.get("found"):
elapsed = time.time() - start
return json.dumps({
"found": True,
"x": data["x"],
"y": data["y"],
"elapsed": round(elapsed, 2),
})
return json.dumps(
{
"found": True,
"x": data["x"],
"y": data["y"],
"elapsed": round(elapsed, 2),
}
)
await asyncio.sleep(0.1) # Faster polling
return json.dumps({"found": False, "timeout": timeout})
async def _wait_while_image(
self, image_path: str, timeout: float, confidence: float, run
) -> str:
async def _wait_while_image(self, image_path: str, timeout: float, confidence: float, run) -> str:
"""Wait while image is visible on screen."""
path = Path(image_path)
if not path.exists():
@@ -744,22 +779,20 @@ Examples:
except Exception as e:
return f"Error: {str(e)}"
def _pixel_matches(
self, x: int, y: int, color: tuple[int, int, int], tolerance: int
) -> str:
def _pixel_matches(self, x: int, y: int, color: tuple[int, int, int], tolerance: int) -> str:
pg = self._ensure_pyautogui()
try:
screenshot = pg.screenshot(region=(x, y, 1, 1))
pixel = screenshot.getpixel((0, 0))
matches = all(
abs(pixel[i] - color[i]) <= tolerance for i in range(3)
matches = all(abs(pixel[i] - color[i]) <= tolerance for i in range(3))
return json.dumps(
{
"matches": matches,
"expected": {"r": color[0], "g": color[1], "b": color[2]},
"actual": {"r": pixel[0], "g": pixel[1], "b": pixel[2]},
"tolerance": tolerance,
}
)
return json.dumps({
"matches": matches,
"expected": {"r": color[0], "g": color[1], "b": color[2]},
"actual": {"r": pixel[0], "g": pixel[1], "b": pixel[2]},
"tolerance": tolerance,
})
except Exception as e:
return f"Error: {str(e)}"
@@ -768,7 +801,7 @@ Examples:
def _get_active_window(self) -> str:
"""Get frontmost window info using AppleScript."""
try:
script = '''
script = """
tell application "System Events"
set frontApp to first application process whose frontmost is true
set appName to name of frontApp
@@ -782,7 +815,7 @@ Examples:
return appName & "|||" & "" & "|||0,0|||0,0"
end try
end tell
'''
"""
result = subprocess.run(
["osascript", "-e", script],
capture_output=True,
@@ -794,12 +827,14 @@ Examples:
if len(parts) >= 4:
pos = parts[2].split(",")
size = parts[3].split(",")
return json.dumps({
"app": parts[0],
"title": parts[1],
"position": {"x": int(pos[0]), "y": int(pos[1])},
"size": {"width": int(size[0]), "height": int(size[1])},
})
return json.dumps(
{
"app": parts[0],
"title": parts[1],
"position": {"x": int(pos[0]), "y": int(pos[1])},
"size": {"width": int(size[0]), "height": int(size[1])},
}
)
return json.dumps({"error": "Could not get active window"})
except Exception as e:
return json.dumps({"error": str(e)})
@@ -807,7 +842,7 @@ Examples:
def _list_windows(self) -> str:
"""List all windows using AppleScript."""
try:
script = '''
script = """
set windowList to ""
tell application "System Events"
set allProcesses to application processes whose visible is true
@@ -825,7 +860,7 @@ Examples:
end repeat
end tell
return windowList
'''
"""
result = subprocess.run(
["osascript", "-e", script],
capture_output=True,
@@ -840,12 +875,14 @@ Examples:
if len(parts) >= 4:
pos = parts[2].split(",")
size = parts[3].split(",")
windows.append({
"app": parts[0],
"title": parts[1],
"position": {"x": int(pos[0]), "y": int(pos[1])},
"size": {"width": int(size[0]), "height": int(size[1])},
})
windows.append(
{
"app": parts[0],
"title": parts[1],
"position": {"x": int(pos[0]), "y": int(pos[1])},
"size": {"width": int(size[0]), "height": int(size[1])},
}
)
return json.dumps({"windows": windows, "count": len(windows)})
return json.dumps({"error": "Could not list windows", "windows": []})
except Exception as e:
@@ -950,7 +987,9 @@ Examples:
elif action == "type":
return await run(self._type_text, params.get("text", ""), params.get("interval", 0.02))
elif action == "write":
return await run(self._write_text, params.get("text", ""), params.get("clear", False), params.get("interval", 0.02))
return await run(
self._write_text, params.get("text", ""), params.get("clear", False), params.get("interval", 0.02)
)
elif action == "press":
return await run(self._press_key, params.get("key", ""))
elif action == "key_down":
@@ -978,13 +1017,15 @@ Examples:
pg = self._ensure_pyautogui()
size = pg.size()
pos = pg.position()
return json.dumps({
"screen": {"width": size.width, "height": size.height},
"mouse": {"x": pos.x, "y": pos.y},
"pause": self._pause,
"failsafe": self._failsafe,
"regions": list(self._defined_regions.keys()),
})
return json.dumps(
{
"screen": {"width": size.width, "height": size.height},
"mouse": {"x": pos.x, "y": pos.y},
"pause": self._pause,
"failsafe": self._failsafe,
"regions": list(self._defined_regions.keys()),
}
)
def _get_position(self) -> str:
pg = self._ensure_pyautogui()
@@ -1028,14 +1069,28 @@ Examples:
return await tool_self.call(
ctx,
action=action,
x=x, y=y, dx=dx, dy=dy,
text=text, key=key, keys=keys,
amount=amount, duration=duration, interval=interval,
region=region, clear=clear,
image_path=image_path, confidence=confidence, timeout=timeout,
color=tuple(color) if color else None, tolerance=tolerance,
title=title, use_regex=use_regex,
name=name, width=width, height=height,
x=x,
y=y,
dx=dx,
dy=dy,
text=text,
key=key,
keys=keys,
amount=amount,
duration=duration,
interval=interval,
region=region,
clear=clear,
image_path=image_path,
confidence=confidence,
timeout=timeout,
color=tuple(color) if color else None,
tolerance=tolerance,
title=title,
use_regex=use_regex,
name=name,
width=width,
height=height,
value=value,
actions=actions,
)
@@ -154,6 +154,7 @@ class FileSystemTool(BaseTool, ABC):
"""
if permission_manager is None:
from hanzo_tools.core.permissions import PermissionManager
permission_manager = PermissionManager()
self.permission_manager = permission_manager
@@ -80,17 +80,17 @@ __all__ = [
def get_read_only_filesystem_tools(permission_manager) -> list:
"""Get read-only filesystem tools for sandboxed agents.
Returns tools that can only read files, not modify them:
- read: Read file contents
- tree: View directory structure
- find: Find files by pattern
- search: Search file contents
- ast: Code structure analysis
Args:
permission_manager: PermissionManager instance
Returns:
List of instantiated read-only tools
"""
@@ -1,6 +1,6 @@
"""Edit tool - find and replace in files."""
from typing import Optional, Annotated
from typing import Optional, Annotated
from pathlib import Path
from pydantic import Field
@@ -1,6 +1,6 @@
"""Read tool - read file contents."""
from typing import Optional, Any, Annotated
from typing import Any, Optional, Annotated
from pathlib import Path
from pydantic import Field
@@ -6,7 +6,6 @@ from typing import Optional, Annotated
from pathlib import Path
import aiofiles
from pydantic import Field
from mcp.server import FastMCP
from mcp.server.fastmcp import Context as MCPContext
@@ -1,6 +1,6 @@
"""Write tool - write/create files."""
from typing import Optional, Annotated
from typing import Optional, Annotated
from pathlib import Path
from pydantic import Field
@@ -34,13 +34,13 @@ __all__ = [
def get_read_only_jupyter_tools(permission_manager) -> list:
"""Get read-only jupyter tools for sandboxed agents.
Returns tools that can read jupyter notebooks:
- jupyter: Read and analyze notebook contents
Args:
permission_manager: PermissionManager instance
Returns:
List of instantiated read-only tools
"""
@@ -15,7 +15,6 @@ Provides a unified `memory` tool for all memory and knowledge operations:
from mcp.server import FastMCP
from hanzo_tools.core import BaseTool, ToolRegistry
from hanzo_tools.memory.unified_memory_tool import UnifiedMemoryTool
# Legacy imports for backwards compatibility
from hanzo_tools.memory.memory_tools import (
@@ -33,6 +32,7 @@ from hanzo_tools.memory.knowledge_tools import (
SummarizeToMemoryTool,
ManageKnowledgeBasesTool,
)
from hanzo_tools.memory.unified_memory_tool import UnifiedMemoryTool
__all__ = [
# Primary unified tool
@@ -3,7 +3,7 @@
Single tool with action parameter replaces 9 separate memory tools.
"""
from typing import TYPE_CHECKING, Optional, List, Dict, Any, Literal, final, override, Annotated
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Annotated, final, override
from pydantic import Field
from mcp.server import FastMCP
@@ -25,6 +25,7 @@ def _check_memory_available() -> bool:
if MEMORY_AVAILABLE is None:
try:
import hanzo_memory # noqa: F401
MEMORY_AVAILABLE = True
except ImportError:
MEMORY_AVAILABLE = False
@@ -38,22 +39,23 @@ def _get_lazy_memory_service() -> "MemoryService":
if not _check_memory_available():
raise ImportError("hanzo-memory required. Install: pip install hanzo-memory")
from hanzo_memory.services.memory import get_memory_service
_memory_service = get_memory_service()
return _memory_service
Action = Annotated[
Literal[
"recall", # Search memories
"create", # Store new memories
"update", # Update existing memories
"delete", # Delete memories
"manage", # Atomic create/update/delete
"facts", # Recall facts from knowledge bases
"store", # Store facts in knowledge bases
"summarize", # Summarize and store
"kb", # Manage knowledge bases
"list", # List memories/knowledge bases
"recall", # Search memories
"create", # Store new memories
"update", # Update existing memories
"delete", # Delete memories
"manage", # Atomic create/update/delete
"facts", # Recall facts from knowledge bases
"store", # Store facts in knowledge bases
"summarize", # Summarize and store
"kb", # Manage knowledge bases
"list", # List memories/knowledge bases
],
Field(description="Memory action to perform"),
]
@@ -296,6 +298,7 @@ Examples:
try:
from hanzo_memory.services.knowledge import get_knowledge_service
ks = get_knowledge_service()
results = []
@@ -326,6 +329,7 @@ Examples:
try:
from hanzo_memory.services.knowledge import get_knowledge_service
ks = get_knowledge_service()
stored = []
@@ -366,6 +370,7 @@ Examples:
"""Manage knowledge bases."""
try:
from hanzo_memory.services.knowledge import get_knowledge_service
ks = get_knowledge_service()
if action == "list":
+4 -14
View File
@@ -125,9 +125,7 @@ class BaseAgent(ABC, Generic[TContext, TResult]):
# Log start
if context and hasattr(context, "log"):
await context.log(
f"Starting {self.name} with model {self.config.model}"
)
await context.log(f"Starting {self.name} with model {self.config.model}")
# Execute with retries
result = await self._execute_with_retries(prompt, context, env, **kwargs)
@@ -145,11 +143,7 @@ class BaseAgent(ABC, Generic[TContext, TResult]):
except Exception as e:
self._end_time = datetime.now()
duration = (
(self._end_time - self._start_time).total_seconds()
if self._start_time
else None
)
duration = (self._end_time - self._start_time).total_seconds() if self._start_time else None
logger.error(f"Agent {self.name} failed: {e}")
@@ -231,9 +225,7 @@ class BaseAgent(ABC, Generic[TContext, TResult]):
if attempt < self.config.max_retries - 1:
await asyncio.sleep(2**attempt)
raise Exception(
f"All {self.config.max_retries} attempts failed. Last error: {last_error}"
)
raise Exception(f"All {self.config.max_retries} attempts failed. Last error: {last_error}")
@abstractmethod
async def _execute_impl(
@@ -326,9 +318,7 @@ class CLIAgent(BaseAgent[TContext, str]):
)
except asyncio.TimeoutError:
process.kill()
raise asyncio.TimeoutError(
f"Command timed out after {self.config.timeout} seconds"
)
raise asyncio.TimeoutError(f"Command timed out after {self.config.timeout} seconds")
# Check for errors
if process.returncode != 0:
+7 -25
View File
@@ -55,11 +55,7 @@ class BatchTask:
@property
def success(self) -> bool:
"""Check if task succeeded."""
return (
self.status == "completed"
and self.result
and getattr(self.result, "success", True)
)
return self.status == "completed" and self.result and getattr(self.result, "success", True)
def get_error(self) -> Optional[str]:
"""Get error message from direct error or result."""
@@ -161,9 +157,7 @@ class BatchConfig:
if not config.consensus_mode and not config.critic_mode:
agent_match = re.search(r"agent:(\w+)", command)
if agent_match:
config.agent_model = agent_match.group(
1
) # Will be resolved in __post_init__
config.agent_model = agent_match.group(1) # Will be resolved in __post_init__
# Parse worktree option
worktree_match = re.search(r"worktree:(true|false)", command)
@@ -220,9 +214,7 @@ class BatchOrchestrator:
self._task_counter += 1
return f"task_{self._task_counter:04d}"
async def _setup_worktree(
self, task_id: str, config: BatchConfig
) -> Optional[Path]:
async def _setup_worktree(self, task_id: str, config: BatchConfig) -> Optional[Path]:
"""Setup git worktree for parallel editing.
Args:
@@ -536,9 +528,7 @@ class BatchOrchestrator:
# If consensus reached, combine insights
if consensus_result["consensus_reached"] and successful_responses:
combined = "\n\n".join(
[f"[{r['model']}]: {r['response']}" for r in successful_responses]
)
combined = "\n\n".join([f"[{r['model']}]: {r['response']}" for r in successful_responses])
consensus_result["combined_response"] = combined
return consensus_result
@@ -627,9 +617,7 @@ Provide your critical analysis:"""
return {
"critic_chain": reviews,
"final_review": (
reviews[-1]["review"] if reviews and "review" in reviews[-1] else None
),
"final_review": (reviews[-1]["review"] if reviews and "review" in reviews[-1] else None),
"models_used": models,
"chain_length": len(reviews),
}
@@ -706,11 +694,7 @@ Provide your critical analysis:"""
if "review" in review:
console.print(
Panel(
(
review["review"][:500] + "..."
if len(review["review"]) > 500
else review["review"]
),
(review["review"][:500] + "..." if len(review["review"]) > 500 else review["review"]),
title=f"[cyan]Critic {review['iteration']}: {review['model']}[/cyan]",
)
)
@@ -785,9 +769,7 @@ Provide your critical analysis:"""
del self.active_tasks[task.id]
# Generate summary
total_duration = sum(
t.duration() or 0 for t in self.completed_tasks + self.failed_tasks
)
total_duration = sum(t.duration() or 0 for t in self.completed_tasks + self.failed_tasks)
summary = {
"total_tasks": len(tasks),
+14 -42
View File
@@ -115,15 +115,9 @@ def serve(ctx, name: str, port: int):
@cli.command()
@click.option("--name", "-n", help="Node name (auto-generated if not provided)")
@click.option(
"--port", "-p", default=52415, help="Node port (default: 52415 for hanzo/net)"
)
@click.option(
"--network", default="local", help="Network to join (mainnet/testnet/local)"
)
@click.option(
"--models", "-m", multiple=True, help="Models to serve (e.g., llama-3.2-3b)"
)
@click.option("--port", "-p", default=52415, help="Node port (default: 52415 for hanzo/net)")
@click.option("--network", default="local", help="Network to join (mainnet/testnet/local)")
@click.option("--models", "-m", multiple=True, help="Models to serve (e.g., llama-3.2-3b)")
@click.option("--max-jobs", type=int, default=10, help="Max concurrent jobs")
@click.pass_context
def net(ctx, name: str, port: int, network: str, models: tuple, max_jobs: int):
@@ -137,15 +131,9 @@ def net(ctx, name: str, port: int, network: str, models: tuple, max_jobs: int):
@cli.command()
@click.option("--name", "-n", help="Node name (auto-generated if not provided)")
@click.option(
"--port", "-p", default=52415, help="Node port (default: 52415 for hanzo/net)"
)
@click.option(
"--network", default="local", help="Network to join (mainnet/testnet/local)"
)
@click.option(
"--models", "-m", multiple=True, help="Models to serve (e.g., llama-3.2-3b)"
)
@click.option("--port", "-p", default=52415, help="Node port (default: 52415 for hanzo/net)")
@click.option("--network", default="local", help="Network to join (mainnet/testnet/local)")
@click.option("--models", "-m", multiple=True, help="Models to serve (e.g., llama-3.2-3b)")
@click.option("--max-jobs", type=int, default=10, help="Max concurrent jobs")
@click.pass_context
def node(ctx, name: str, port: int, network: str, models: tuple, max_jobs: int):
@@ -180,15 +168,9 @@ def node(ctx, name: str, port: int, network: str, models: tuple, max_jobs: int):
@click.option("--repl", is_flag=True, help="Start REPL interface (default)")
@click.option("--instances", type=int, default=2, help="Number of worker agents")
@click.option("--mcp-tools", is_flag=True, default=True, help="Enable all MCP tools")
@click.option(
"--network-mode", is_flag=True, default=True, help="Network agents together"
)
@click.option(
"--guardrails", is_flag=True, default=True, help="Enable code quality guardrails"
)
@click.option(
"--use-network/--no-network", default=True, help="Use hanzo-network if available"
)
@click.option("--network-mode", is_flag=True, default=True, help="Network agents together")
@click.option("--guardrails", is_flag=True, default=True, help="Enable code quality guardrails")
@click.option("--use-network/--no-network", default=True, help="Use hanzo-network if available")
@click.option(
"--use-hanzo-net",
is_flag=True,
@@ -266,9 +248,7 @@ def dev(
console.print(f" Router Endpoint: {orch_config.router.endpoint}")
if orch_config.codex:
console.print(f" Codex Model: {orch_config.codex.model}")
console.print(
f" Cost Optimization: {'Enabled' if orch_config.enable_cost_optimization else 'Disabled'}"
)
console.print(f" Cost Optimization: {'Enabled' if orch_config.enable_cost_optimization else 'Disabled'}")
console.print()
asyncio.run(
@@ -351,9 +331,7 @@ async def start_compute_node(
console.print(f"\n[green]✓[/green] Node initialized")
console.print(f" Port: {port}")
console.print(
f" Models: {', '.join(models) if models else 'auto-detect'}"
)
console.print(f" Models: {', '.join(models) if models else 'auto-detect'}")
console.print("\n[bold green]Hanzo Net is running![/bold green]")
console.print("WebUI: http://localhost:52415")
console.print("API: http://localhost:52415/v1/chat/completions")
@@ -375,9 +353,7 @@ async def start_compute_node(
stop_task = asyncio.create_task(stop_event.wait())
# Wait for either net to complete or stop signal
done, pending = await asyncio.wait(
[net_task, stop_task], return_when=asyncio.FIRST_COMPLETED
)
done, pending = await asyncio.wait([net_task, stop_task], return_when=asyncio.FIRST_COMPLETED)
# Cancel pending tasks
for task in pending:
@@ -413,15 +389,11 @@ async def start_compute_node(
env["NET_MODELS"] = ",".join(models)
if name:
env["NET_NODE_NAME"] = name
env["PYTHONPATH"] = (
os.path.join(net_path, "src") + ":" + env.get("PYTHONPATH", "")
)
env["PYTHONPATH"] = os.path.join(net_path, "src") + ":" + env.get("PYTHONPATH", "")
console.print(f"\n[green]✓[/green] Starting net node")
console.print(f" Port: {port}")
console.print(
f" Models: {', '.join(models) if models else 'auto-detect'}"
)
console.print(f" Models: {', '.join(models) if models else 'auto-detect'}")
console.print("\n[bold green]Hanzo Net is running![/bold green]")
console.print("WebUI: http://localhost:52415")
console.print("API: http://localhost:52415/v1/chat/completions")
+1 -5
View File
@@ -88,11 +88,7 @@ async def run(ctx, agents: tuple, task: str, parallel: bool, timeout: Optional[i
network = create_network(agents=agent_list)
# Run task
result = (
await asyncio.wait_for(network.run(task), timeout=timeout)
if timeout
else await network.run(task)
)
result = await asyncio.wait_for(network.run(task), timeout=timeout) if timeout else await network.run(task)
console.print("[green]Task completed![/green]")
console.print(result)
+2 -6
View File
@@ -127,9 +127,7 @@ def login(ctx, email: str, password: str, api_key: str, sso: bool):
hanzo_auth = HanzoAuth()
# Email auth not implemented yet
console.print(
"[yellow]Email authentication not yet implemented[/yellow]"
)
console.print("[yellow]Email authentication not yet implemented[/yellow]")
console.print("[dim]Saving credentials locally for development[/dim]")
auth = {
@@ -249,9 +247,7 @@ def whoami():
content = "\n".join(lines) if lines else "[dim]No user information available[/dim]"
console.print(
Panel(content, title="[bold cyan]User Information[/bold cyan]", box=box.ROUNDED)
)
console.print(Panel(content, title="[bold cyan]User Information[/bold cyan]", box=box.ROUNDED))
@auth_group.command(name="set-key")
+3 -9
View File
@@ -18,9 +18,7 @@ from ..utils.output import console
@click.option("--local/--cloud", default=True, help="Use local or cloud model")
@click.option("--once", is_flag=True, help="Single question mode")
@click.option("--system", "-s", help="System prompt")
@click.option(
"--repl", is_flag=True, help="Start full REPL interface (like Claude Code)"
)
@click.option("--repl", is_flag=True, help="Start full REPL interface (like Claude Code)")
@click.option("--ipython", is_flag=True, help="Use IPython REPL interface")
@click.option("--tui", is_flag=True, help="Use beautiful TUI interface")
@click.argument("prompt", nargs=-1)
@@ -80,9 +78,7 @@ def chat_command(
asyncio.run(interactive_chat(ctx, model, local, system))
async def ask_once(
ctx, prompt: str, model: str, local: bool, system: Optional[str] = None
):
async def ask_once(ctx, prompt: str, model: str, local: bool, system: Optional[str] = None):
"""Ask a single question."""
messages = []
if system:
@@ -172,9 +168,7 @@ async def interactive_chat(ctx, model: str, local: bool, system: Optional[str]):
from prompt_toolkit import PromptSession
from prompt_toolkit.history import FileHistory
console.print(
f"[cyan]Chat session started[/cyan] (model: {model}, mode: {'local' if local else 'cloud'})"
)
console.print(f"[cyan]Chat session started[/cyan] (model: {model}, mode: {'local' if local else 'cloud'})")
console.print("Type 'exit' or Ctrl+D to quit\n")
session = PromptSession(history=FileHistory(".hanzo_chat_history"))
+1 -3
View File
@@ -115,9 +115,7 @@ def set(ctx, key: str, value: str, is_global: bool, is_local: bool):
# Save config
save_config(config_path, config)
console.print(
f"[green]✓[/green] Set {key} = {parsed_value} in {config_name} config"
)
console.print(f"[green]✓[/green] Set {key} = {parsed_value} in {config_name} config")
@config_group.command()
+2 -6
View File
@@ -96,9 +96,7 @@ async def tools(ctx, category: str):
# Display tools
for cat, tools in sorted(categories.items()):
table = Table(
title=f"{cat.title()} Tools" if len(categories) > 1 else "MCP Tools"
)
table = Table(title=f"{cat.title()} Tools" if len(categories) > 1 else "MCP Tools")
table.add_column("Name", style="cyan", no_wrap=True)
table.add_column("Description")
@@ -241,9 +239,7 @@ def install(ctx, path: str):
existing["mcpServers"]["hanzo-mcp"] = config["mcpServers"]["hanzo-mcp"]
config = existing
except Exception as e:
console.print(
f"[yellow]Warning: Could not read existing config: {e}[/yellow]"
)
console.print(f"[yellow]Warning: Could not read existing config: {e}[/yellow]")
# Write config
config_path.parent.mkdir(parents=True, exist_ok=True)
+7 -21
View File
@@ -50,9 +50,7 @@ async def start(
if not click.confirm("Continue without wallet?"):
return
miner = HanzoMiner(
name=name, wallet=wallet, device=device, network=network, min_stake=min_stake
)
miner = HanzoMiner(name=name, wallet=wallet, device=device, network=network, min_stake=min_stake)
with Progress(
SpinnerColumn(),
@@ -94,9 +92,7 @@ async def start(
if event["type"] == "log":
console.print(event["message"], end="")
elif event["type"] == "job":
console.print(
f"[green]Job completed:[/green] {event['job_id']} (+{event['tokens']} tokens)"
)
console.print(f"[green]Job completed:[/green] {event['job_id']} (+{event['tokens']} tokens)")
elif event["type"] == "error":
console.print(f"[red]Error:[/red] {event['message']}")
except KeyboardInterrupt:
@@ -109,9 +105,7 @@ async def start(
console.print("\n[cyan]Session Summary:[/cyan]")
console.print(f" Jobs completed: {final_stats.get('jobs_completed', 0)}")
console.print(f" Tokens earned: {final_stats.get('tokens_earned', 0)}")
console.print(
f" Average job time: {final_stats.get('avg_job_time', 'N/A')}"
)
console.print(f" Average job time: {final_stats.get('avg_job_time', 'N/A')}")
console.print("[green]✓[/green] Miner stopped")
@@ -207,15 +201,11 @@ async def status(ctx, name: str, detailed: bool):
console.print(f" Network: {stats.get('network', 'unknown')}")
console.print(f" Wallet: {stats.get('wallet', 'Not set')}")
console.print(f" Models: {', '.join(stats.get('models', []))}")
console.print(
f" Memory: {stats.get('memory_used', 0)} / {stats.get('memory_total', 0)} MB"
)
console.print(f" Memory: {stats.get('memory_used', 0)} / {stats.get('memory_total', 0)} MB")
console.print(f" CPU: {stats.get('cpu_percent', 0)}%")
if gpu := stats.get("gpu"):
console.print(
f" GPU: {gpu['name']} ({gpu['memory_used']} / {gpu['memory_total']} MB)"
)
console.print(f" GPU: {gpu['name']} ({gpu['memory_used']} / {gpu['memory_total']} MB)")
console.print(f"\n[cyan]Performance:[/cyan]")
console.print(f" Average job time: {stats.get('avg_job_time', 'N/A')}")
@@ -292,9 +282,7 @@ async def earnings(ctx, wallet: str, network: str):
if history := data.get("recent_jobs"):
console.print("\n[cyan]Recent Jobs:[/cyan]")
for job in history[:5]:
console.print(
f" • {job['timestamp']}: +{job['tokens']} tokens ({job['model']})"
)
console.print(f" • {job['timestamp']}: +{job['tokens']} tokens ({job['model']})")
@miner_group.command()
@@ -323,9 +311,7 @@ async def withdraw(ctx, amount: float, wallet: str, to: str, network: str):
with console.status("Processing withdrawal..."):
try:
result = await withdraw_earnings(
wallet=wallet, amount=amount, destination=to, network=network
)
result = await withdraw_earnings(wallet=wallet, amount=amount, destination=to, network=network)
console.print(f"[green]✓[/green] Withdrawal successful")
console.print(f" Transaction: {result['tx_hash']}")
+8 -25
View File
@@ -26,9 +26,7 @@ def network_group():
@click.option("--consensus", is_flag=True, help="Require consensus")
@click.option("--timeout", "-t", type=int, default=300, help="Timeout in seconds")
@click.pass_context
async def dispatch(
ctx, prompt: str, agents: int, model: str, mode: str, consensus: bool, timeout: int
):
async def dispatch(ctx, prompt: str, agents: int, model: str, mode: str, consensus: bool, timeout: int):
"""Dispatch work to agent network."""
try:
from hanzo_network import NetworkDispatcher
@@ -391,13 +389,9 @@ def topology(ctx, json: bool):
)
if gpu_available:
console.print(
"[green]✓[/green] GPU/Accelerator detected and available for AI workloads"
)
console.print("[green]✓[/green] GPU/Accelerator detected and available for AI workloads")
if "APPLE M" in caps.chip.upper():
console.print(
" [dim]Apple Silicon Neural Engine available for on-device AI[/dim]"
)
console.print(" [dim]Apple Silicon Neural Engine available for on-device AI[/dim]")
else:
console.print("[yellow]⚠[/yellow] No GPU detected - using CPU only")
console.print(" To enable GPU support:")
@@ -547,9 +541,7 @@ def topology_add(
try:
import qrcode
except ImportError:
console.print(
"\n[yellow]QR code generation requires qrcode library[/yellow]"
)
console.print("\n[yellow]QR code generation requires qrcode library[/yellow]")
console.print("Install with: pip install qrcode[pil]")
return
@@ -700,10 +692,7 @@ def models(ctx, endpoint: str):
models_list = [
model
for model in all_models
if not any(
keyword in model.get("id", "").lower()
for keyword in embedding_keywords
)
if not any(keyword in model.get("id", "").lower() for keyword in embedding_keywords)
]
if models_list:
@@ -741,16 +730,12 @@ def models(ctx, endpoint: str):
table.add_row(str(i), model_id, tier, provider)
console.print(table)
console.print(
f"\n[dim]Total LLMs: {len(models_list)} (embedding models filtered out)[/dim]"
)
console.print(f"\n[dim]Total LLMs: {len(models_list)} (embedding models filtered out)[/dim]")
else:
console.print("[yellow]No models available on gateway[/yellow]")
else:
# Show static list of known models
console.print(
"[yellow]Could not fetch live models, showing known models:[/yellow]\n"
)
console.print("[yellow]Could not fetch live models, showing known models:[/yellow]\n")
_show_static_models()
except Exception as e:
console.print(f"[yellow]Error fetching models: {e}[/yellow]\n")
@@ -758,9 +743,7 @@ def models(ctx, endpoint: str):
console.print("\n[dim]To use a model:[/dim]")
console.print(" [cyan]hanzo dev --model llama3-8b-instruct[/cyan]")
console.print(
"\n[dim cyan]Free tier:[/dim cyan] [green]Most models available without login![/green]"
)
console.print("\n[dim cyan]Free tier:[/dim cyan] [green]Most models available without login![/green]")
console.print(
"[dim cyan]Premium models:[/dim cyan] [yellow]gpt-4, claude-3-opus, o1-preview[/yellow] - [cyan]hanzo auth login[/cyan]"
)
+6 -18
View File
@@ -31,9 +31,7 @@ async def start(ctx, name: str, port: int, models: tuple, device: str):
await start_node(ctx, name, port, list(models) if models else None, device)
async def start_node(
ctx, name: str, port: int, models: Optional[List[str]] = None, device: str = "auto"
):
async def start_node(ctx, name: str, port: int, models: Optional[List[str]] = None, device: str = "auto"):
"""Start a local node via hanzo-cluster."""
try:
from hanzo_cluster import HanzoCluster
@@ -140,9 +138,7 @@ async def status(ctx, name: str):
if workers := status.get("node_details", []):
console.print("\n[cyan]Workers:[/cyan]")
for worker in workers:
console.print(
f" • {worker.get('name', 'unknown')} ({worker.get('state', 'unknown')})"
)
console.print(f" • {worker.get('name', 'unknown')} ({worker.get('state', 'unknown')})")
if device := worker.get("device"):
console.print(f" Device: {device}")
else:
@@ -256,9 +252,7 @@ def worker_group():
default="auto",
help="Device to use",
)
@click.option(
"--port", "-p", type=int, help="Worker port (auto-assigned if not specified)"
)
@click.option("--port", "-p", type=int, help="Worker port (auto-assigned if not specified)")
@click.option("--blockchain", is_flag=True, help="Enable blockchain features")
@click.option("--network", is_flag=True, help="Enable network discovery")
@click.pass_context
@@ -431,9 +425,7 @@ async def worker_info(ctx, name: str):
if resources := info.get("resources"):
console.print("\n[cyan]Resources:[/cyan]")
console.print(f" CPU: {resources.get('cpu_percent', 'N/A')}%")
console.print(
f" Memory: {resources.get('memory_used', 'N/A')} / {resources.get('memory_total', 'N/A')}"
)
console.print(f" Memory: {resources.get('memory_used', 'N/A')} / {resources.get('memory_total', 'N/A')}")
if gpu := resources.get("gpu"):
console.print(
f" GPU: {gpu.get('name', 'N/A')} ({gpu.get('memory_used', 'N/A')} / {gpu.get('memory_total', 'N/A')})"
@@ -446,12 +438,8 @@ async def worker_info(ctx, name: str):
if network := info.get("network"):
console.print("\n[cyan]Network:[/cyan]")
console.print(
f" Blockchain: {'enabled' if network.get('blockchain') else 'disabled'}"
)
console.print(
f" Discovery: {'enabled' if network.get('discovery') else 'disabled'}"
)
console.print(f" Blockchain: {'enabled' if network.get('blockchain') else 'disabled'}")
console.print(f" Discovery: {'enabled' if network.get('discovery') else 'disabled'}")
if peers := network.get("peers"):
console.print(f" Peers: {len(peers)}")
+3 -9
View File
@@ -105,18 +105,14 @@ def info(ctx):
if not providers:
console.print("\n[yellow]No LLM providers configured[/yellow]")
console.print(
"Set one of: OPENAI_API_KEY, ANTHROPIC_API_KEY, HANZO_API_KEY"
)
console.print("Set one of: OPENAI_API_KEY, ANTHROPIC_API_KEY, HANZO_API_KEY")
except ImportError:
console.print("[red]Error:[/red] hanzo-repl not installed")
@repl_group.command()
@click.option(
"--interface", type=click.Choice(["all", "ipython", "tui", "voice"]), default="all"
)
@click.option("--interface", type=click.Choice(["all", "ipython", "tui", "voice"]), default="all")
@click.pass_context
def install_extras(ctx, interface: str):
"""Install optional REPL components."""
@@ -169,9 +165,7 @@ def exec(ctx, command: tuple, model: str):
except ImportError as e:
console.print(f"[red]Import Error:[/red] {e}")
console.print(
"[yellow]Note:[/yellow] hanzo-repl may not be installed correctly"
)
console.print("[yellow]Note:[/yellow] hanzo-repl may not be installed correctly")
except Exception as e:
console.print(f"[red]Error: {e}[/red]")
+6 -20
View File
@@ -40,9 +40,7 @@ def start_router(ctx, port: int, config: Optional[str], detach: bool):
if not router_dir:
console.print("[red]Error:[/red] Hanzo router not found")
console.print("\nPlease clone the router:")
console.print(
" git clone https://github.com/hanzoai/router.git ~/work/hanzo/router"
)
console.print(" git clone https://github.com/hanzoai/router.git ~/work/hanzo/router")
return
console.print(f"[green]✓[/green] Found router at {router_dir}")
@@ -91,9 +89,7 @@ def start_router(ctx, port: int, config: Optional[str], detach: bool):
stderr=subprocess.DEVNULL,
start_new_session=True,
)
console.print(
f"[green]✓[/green] Router started in background (PID: {process.pid})"
)
console.print(f"[green]✓[/green] Router started in background (PID: {process.pid})")
console.print(f"Check status: curl http://localhost:{port}/health")
else:
# Run in foreground
@@ -116,14 +112,8 @@ def stop_router(port: int):
for proc in psutil.process_iter(["pid", "cmdline"]):
try:
cmdline = proc.info["cmdline"]
if (
cmdline
and "proxy_server" in " ".join(cmdline)
and str(port) in " ".join(cmdline)
):
console.print(
f"[yellow]Stopping router (PID: {proc.info['pid']})[/yellow]"
)
if cmdline and "proxy_server" in " ".join(cmdline) and str(port) in " ".join(cmdline):
console.print(f"[yellow]Stopping router (PID: {proc.info['pid']})[/yellow]")
proc.send_signal(signal.SIGTERM)
proc.wait(timeout=5)
found = True
@@ -148,9 +138,7 @@ def router_status(port: int):
# Try to get models
try:
models_response = httpx.get(
f"http://localhost:{port}/models", timeout=2.0
)
models_response = httpx.get(f"http://localhost:{port}/models", timeout=2.0)
if models_response.status_code == 200:
data = models_response.json()
if "data" in data:
@@ -158,9 +146,7 @@ def router_status(port: int):
except Exception:
pass
else:
console.print(
f"[yellow]Router responding but unhealthy (status: {response.status_code})[/yellow]"
)
console.print(f"[yellow]Router responding but unhealthy (status: {response.status_code})[/yellow]")
except httpx.ConnectError:
console.print(f"[red]Router not running on port {port}[/red]")
console.print("\nStart with: hanzo router start")
+2 -6
View File
@@ -30,9 +30,7 @@ async def list_tools(ctx, category: str, installed: bool):
with console.status("Loading tools..."):
try:
tools = await registry.list_tools(
category=category, installed_only=installed
)
tools = await registry.list_tools(category=category, installed_only=installed)
except Exception as e:
console.print(f"[red]Failed to load tools: {e}[/red]")
return
@@ -88,9 +86,7 @@ async def install(ctx, tool_name: str, version: str):
try:
result = await registry.install_tool(name=tool_name, version=version)
console.print(
f"[green]✓[/green] Installed {tool_name} v{result['version']}"
)
console.print(f"[green]✓[/green] Installed {tool_name} v{result['version']}")
if deps := result.get("dependencies_installed"):
console.print(f" Dependencies: {', '.join(deps)}")
+69 -215
View File
@@ -221,9 +221,7 @@ class HanzoDevOrchestrator:
grpo_dir = self.workspace_dir / "grpo"
grpo_dir.mkdir(exist_ok=True)
self.experience_manager = ExperienceManager(
str(grpo_dir / "experience_library.json")
)
self.experience_manager = ExperienceManager(str(grpo_dir / "experience_library.json"))
# Initialize with DeepSeek if API key available
deepseek_key = os.getenv("DEEPSEEK_API_KEY")
@@ -254,9 +252,7 @@ class HanzoDevOrchestrator:
for path in possible_paths:
expanded = Path(path).expanduser()
if expanded.exists() or (
path == "claude" and os.system(f"which {path} >/dev/null 2>&1") == 0
):
if expanded.exists() or (path == "claude" and os.system(f"which {path} >/dev/null 2>&1") == 0):
return str(expanded) if expanded.exists() else path
raise RuntimeError("Claude Code not found. Please specify path.")
@@ -343,9 +339,7 @@ class HanzoDevOrchestrator:
else:
return "Execute standard workflow with monitoring"
def _assess_risks(
self, problem: str, alternatives: List[str], context: Dict[str, Any]
) -> List[str]:
def _assess_risks(self, problem: str, alternatives: List[str], context: Dict[str, Any]) -> List[str]:
"""Assess risks of different approaches."""
risks = []
@@ -369,9 +363,7 @@ class HanzoDevOrchestrator:
) -> str:
"""Synthesize a decision from analysis."""
if len(risks) > 2:
return (
"Proceed cautiously with incremental approach and rollback capability"
)
return "Proceed cautiously with incremental approach and rollback capability"
elif alternatives:
return f"Execute primary approach: {alternatives[0]}"
else:
@@ -416,9 +408,7 @@ class HanzoDevOrchestrator:
if resume:
checkpoint_file = self._get_latest_checkpoint()
if checkpoint_file:
console.print(
f"[green]Resuming from checkpoint: {checkpoint_file.name}[/green]"
)
console.print(f"[green]Resuming from checkpoint: {checkpoint_file.name}[/green]")
# Prepare command
cmd = [self.claude_code_path]
@@ -536,12 +526,8 @@ class HanzoDevOrchestrator:
"timestamp": datetime.now().isoformat(),
"agent_state": self.agent_state.value,
"runtime_health": asdict(self.runtime_health),
"current_context": (
asdict(self.current_context) if self.current_context else None
),
"thinking_history": [
asdict(t) for t in self.thinking_history[-10:]
], # Last 10
"current_context": (asdict(self.current_context) if self.current_context else None),
"thinking_history": [asdict(t) for t in self.thinking_history[-10:]], # Last 10
}
with open(checkpoint_file, "w") as f:
@@ -559,9 +545,7 @@ class HanzoDevOrchestrator:
self.agent_state = AgentState(data["agent_state"])
# Restore other state as needed
console.print(
f"[green]✓ Restored from checkpoint: {checkpoint_file.name}[/green]"
)
console.print(f"[green]✓ Restored from checkpoint: {checkpoint_file.name}[/green]")
return True
except Exception as e:
console.print(f"[red]Failed to restore checkpoint: {e}[/red]")
@@ -577,9 +561,7 @@ class HanzoDevOrchestrator:
healthy = await self.health_check()
if not healthy:
console.print(
f"[yellow]Health check failed. State: {self.runtime_health.state.value}[/yellow]"
)
console.print(f"[yellow]Health check failed. State: {self.runtime_health.state.value}[/yellow]")
# Use System 2 thinking to decide what to do
thinking_result = await self.think(
@@ -588,9 +570,7 @@ class HanzoDevOrchestrator:
)
console.print(f"[cyan]Decision: {thinking_result.decision}[/cyan]")
console.print(
f"[cyan]Confidence: {thinking_result.confidence:.2f}[/cyan]"
)
console.print(f"[cyan]Confidence: {thinking_result.confidence:.2f}[/cyan]")
# Execute decision
if thinking_result.confidence > 0.6:
@@ -744,14 +724,10 @@ class HanzoDevOrchestrator:
experiences_after = len(self.experience_manager.experiences)
# Save updated library
self.experience_manager.save(
str(self.workspace_dir / "grpo" / "experience_library.json")
)
self.experience_manager.save(str(self.workspace_dir / "grpo" / "experience_library.json"))
learned_count = experiences_after - experiences_before
console.print(
f"[green]✓ Learned {learned_count} new experiences (total: {experiences_after})[/green]"
)
console.print(f"[green]✓ Learned {learned_count} new experiences (total: {experiences_after})[/green]")
return learned_count
@@ -854,9 +830,7 @@ class HanzoDevREPL:
handler = FallbackHandler()
if not handler.fallback_order:
console.print("[yellow]⚠️ No API keys detected[/yellow]")
console.print(
"[dim]Set OPENAI_API_KEY or ANTHROPIC_API_KEY to enable AI[/dim]"
)
console.print("[dim]Set OPENAI_API_KEY or ANTHROPIC_API_KEY to enable AI[/dim]")
console.print()
else:
primary = handler.fallback_order[0][1]
@@ -902,13 +876,9 @@ class HanzoDevREPL:
# Handle memory/context commands
from .memory_manager import handle_memory_command
handled = handle_memory_command(
user_input, self.memory_manager, console
)
handled = handle_memory_command(user_input, self.memory_manager, console)
if not handled:
console.print(
"[yellow]Unknown memory command. Use #memory help[/yellow]"
)
console.print("[yellow]Unknown memory command. Use #memory help[/yellow]")
else:
# Natural chat - send directly to AI agents
@@ -951,17 +921,13 @@ class HanzoDevREPL:
table.add_row("Agent State", self.orchestrator.agent_state.value)
table.add_row("Runtime State", self.orchestrator.runtime_health.state.value)
table.add_row(
"Last Response", str(self.orchestrator.runtime_health.last_response)
)
table.add_row("Last Response", str(self.orchestrator.runtime_health.last_response))
table.add_row(
"Response Time",
f"{self.orchestrator.runtime_health.response_time_ms:.2f}ms",
)
table.add_row("Error Count", str(self.orchestrator.runtime_health.error_count))
table.add_row(
"Restart Count", str(self.orchestrator.runtime_health.restart_count)
)
table.add_row("Restart Count", str(self.orchestrator.runtime_health.restart_count))
console.print(table)
@@ -1008,9 +974,7 @@ class HanzoDevREPL:
"""Restore from checkpoint."""
if not args:
# Show available checkpoints
checkpoints = list(
self.orchestrator.checkpoint_dir.glob("checkpoint_*.json")
)
checkpoints = list(self.orchestrator.checkpoint_dir.glob("checkpoint_*.json"))
if checkpoints:
console.print("[cyan]Available checkpoints:[/cyan]")
for cp in checkpoints:
@@ -1087,10 +1051,7 @@ Examples:
enhanced_message = message
# Try smart fallback if no specific model configured
if (
not hasattr(self.orchestrator, "orchestrator_model")
or self.orchestrator.orchestrator_model == "auto"
):
if not hasattr(self.orchestrator, "orchestrator_model") or self.orchestrator.orchestrator_model == "auto":
# Use streaming if available
from .streaming import stream_with_fallback
@@ -1102,9 +1063,7 @@ Examples:
# Response already displayed by streaming handler
return
else:
console.print(
"[red]No AI options available. Please configure API keys or install tools.[/red]"
)
console.print("[red]No AI options available. Please configure API keys or install tools.[/red]")
return
# For codex and other CLI tools, go straight to direct API chat
@@ -1345,26 +1304,14 @@ Examples:
# No API keys available
console.print("[red]No AI API keys configured![/red]")
console.print(
"[yellow]Try these options that don't need your API key:[/yellow]"
)
console.print("[yellow]Try these options that don't need your API key:[/yellow]")
console.print("\n[bold]CLI Tools (use existing tools):[/bold]")
console.print(
" • hanzo dev --orchestrator codex # OpenAI CLI (if installed)"
)
console.print(
" • hanzo dev --orchestrator claude # Claude Desktop (if installed)"
)
console.print(
" • hanzo dev --orchestrator gemini # Gemini CLI (if installed)"
)
console.print(
" • hanzo dev --orchestrator hanzo-ide # Hanzo IDE from ~/work/hanzo/ide"
)
console.print(" • hanzo dev --orchestrator codex # OpenAI CLI (if installed)")
console.print(" • hanzo dev --orchestrator claude # Claude Desktop (if installed)")
console.print(" • hanzo dev --orchestrator gemini # Gemini CLI (if installed)")
console.print(" • hanzo dev --orchestrator hanzo-ide # Hanzo IDE from ~/work/hanzo/ide")
console.print("\n[bold]Free APIs (rate limited):[/bold]")
console.print(
" • hanzo dev --orchestrator codestral # Free Mistral Codestral"
)
console.print(" • hanzo dev --orchestrator codestral # Free Mistral Codestral")
console.print(" • hanzo dev --orchestrator starcoder # Free StarCoder")
console.print("\n[bold]Local Models (unlimited):[/bold]")
console.print(" • hanzo dev --orchestrator local:llama3.2 # Via Ollama")
@@ -1407,16 +1354,10 @@ Examples:
if response.status_code == 200:
data = response.json()
if data.get("choices"):
console.print(
f"[cyan]Codestral:[/cyan] {data['choices'][0]['message']['content']}"
)
console.print(f"[cyan]Codestral:[/cyan] {data['choices'][0]['message']['content']}")
else:
console.print(
"[yellow]Free tier limit reached. Try local models instead:[/yellow]"
)
console.print(
" • Install Ollama: curl -fsSL https://ollama.com/install.sh | sh"
)
console.print("[yellow]Free tier limit reached. Try local models instead:[/yellow]")
console.print(" • Install Ollama: curl -fsSL https://ollama.com/install.sh | sh")
console.print(" • Run: ollama pull codellama")
console.print(" • Use: hanzo dev --orchestrator local:codellama")
@@ -1453,13 +1394,9 @@ Examples:
if response.status_code == 200:
data = response.json()
if isinstance(data, list) and data:
console.print(
f"[cyan]StarCoder:[/cyan] {data[0].get('generated_text', '')}"
)
console.print(f"[cyan]StarCoder:[/cyan] {data[0].get('generated_text', '')}")
else:
console.print(
"[yellow]API limit reached. Install local models:[/yellow]"
)
console.print("[yellow]API limit reached. Install local models:[/yellow]")
console.print(" • brew install ollama")
console.print(" • ollama pull starcoder2")
console.print(" • hanzo dev --orchestrator local:starcoder2")
@@ -1496,9 +1433,7 @@ Examples:
message,
]
process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
stdout, stderr = process.communicate(timeout=30)
@@ -1530,11 +1465,7 @@ Examples:
claude_path = None
for path in claude_paths:
if (
os.path.exists(path)
or subprocess.run(["which", path], capture_output=True).returncode
== 0
):
if os.path.exists(path) or subprocess.run(["which", path], capture_output=True).returncode == 0:
claude_path = path
break
@@ -1563,9 +1494,7 @@ Examples:
"""
subprocess.run(["osascript", "-e", script])
console.print(
"[cyan]Sent to Claude Desktop. Check the app for response.[/cyan]"
)
console.print("[cyan]Sent to Claude Desktop. Check the app for response.[/cyan]")
else:
# Try direct CLI invocation
process = subprocess.Popen(
@@ -1604,9 +1533,7 @@ Examples:
# Use gemini CLI
cmd = ["gemini", "chat", message]
process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
stdout, stderr = process.communicate(timeout=30)
@@ -1634,9 +1561,7 @@ Examples:
console.print("[red]Hanzo Dev IDE not found![/red]")
console.print("[yellow]Expected location: ~/work/hanzo/ide[/yellow]")
console.print("To set up:")
console.print(
" • git clone https://github.com/hanzoai/ide ~/work/hanzo/ide"
)
console.print(" • git clone https://github.com/hanzoai/ide ~/work/hanzo/ide")
console.print(" • cd ~/work/hanzo/ide && npm install")
return
@@ -1672,9 +1597,7 @@ Examples:
cmd = [cli_path, "chat", message]
cwd = None
process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=cwd
)
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=cwd)
stdout, stderr = process.communicate(timeout=30)
@@ -1721,9 +1644,7 @@ Examples:
if response.status_code == 200:
data = response.json()
if data.get("message"):
console.print(
f"[cyan]{model_name}:[/cyan] {data['message']['content']}"
)
console.print(f"[cyan]{model_name}:[/cyan] {data['message']['content']}")
return
except Exception:
@@ -1754,9 +1675,7 @@ Examples:
if response.status_code == 200:
data = response.json()
if data.get("choices"):
console.print(
f"[cyan]{model_name}:[/cyan] {data['choices'][0]['message']['content']}"
)
console.print(f"[cyan]{model_name}:[/cyan] {data['choices'][0]['message']['content']}")
return
except Exception:
@@ -1834,9 +1753,7 @@ async def run_dev_orchestrator(**kwargs):
# Check if we should use network mode
# For now, disable network mode since hanzo-network isn't available
if False and use_network and NETWORK_AVAILABLE:
console_obj.print(
f"[cyan]Mode: Network Orchestration with hanzo-network[/cyan]"
)
console_obj.print(f"[cyan]Mode: Network Orchestration with hanzo-network[/cyan]")
console_obj.print(f"Orchestrator: {orchestrator_model}")
console_obj.print(f"Workers: {instances} agents")
console_obj.print(f"Critics: {max(1, instances // 2)} agents")
@@ -1942,25 +1859,19 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
# Check if we can use hanzo-network
if not NETWORK_AVAILABLE:
self.console.print(
"[yellow]Warning: hanzo-network not available, falling back to basic mode[/yellow]"
)
self.console.print("[yellow]Warning: hanzo-network not available, falling back to basic mode[/yellow]")
async def initialize(self):
"""Initialize the agent network with orchestrator and workers."""
if not NETWORK_AVAILABLE:
self.console.print(
"[red]Cannot initialize network mode without hanzo-network[/red]"
)
self.console.print("[red]Cannot initialize network mode without hanzo-network[/red]")
return False
# Start hanzo net if requested for local orchestration
if self.use_hanzo_net or self.orchestrator_model.startswith("local:"):
await self._start_hanzo_net()
self.console.print(
f"[cyan]Initializing agent network with {self.orchestrator_model} orchestrator...[/cyan]"
)
self.console.print(f"[cyan]Initializing agent network with {self.orchestrator_model} orchestrator...[/cyan]")
# Create orchestrator agent (GPT-5, local, or other model)
self.orchestrator_agent = await self._create_orchestrator_agent()
@@ -1977,9 +1888,7 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
for i in range(num_local_workers):
local_worker = await self._create_local_worker_agent(i)
self.worker_agents.append(local_worker)
self.console.print(
f"[green]Added {num_local_workers} local workers for cost optimization[/green]"
)
self.console.print(f"[green]Added {num_local_workers} local workers for cost optimization[/green]")
# Create critic agents for System 2 thinking
if self.enable_guardrails:
@@ -2002,21 +1911,15 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
self.agent_network = create_network(
agents=all_agents,
router=router,
default_agent=(
self.orchestrator_agent.name if self.orchestrator_agent else None
),
default_agent=(self.orchestrator_agent.name if self.orchestrator_agent else None),
)
self.console.print(
f"[green]✓ Agent network initialized with {len(all_agents)} agents[/green]"
)
self.console.print(f"[green]✓ Agent network initialized with {len(all_agents)} agents[/green]")
return True
async def _start_hanzo_net(self):
"""Start hanzo net for local AI orchestration."""
self.console.print(
"[cyan]Starting hanzo/net for local AI orchestration...[/cyan]"
)
self.console.print("[cyan]Starting hanzo/net for local AI orchestration...[/cyan]")
# Check if hanzo net is already running
import socket
@@ -2026,9 +1929,7 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
sock.close()
if result == 0:
self.console.print(
f"[yellow]hanzo/net already running on port {self.hanzo_net_port}[/yellow]"
)
self.console.print(f"[yellow]hanzo/net already running on port {self.hanzo_net_port}[/yellow]")
return
# Start hanzo net
@@ -2089,9 +1990,7 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
tools=[],
)
self.console.print(
f"[green]✓ Created local {model_name} orchestrator via hanzo/net[/green]"
)
self.console.print(f"[green]✓ Created local {model_name} orchestrator via hanzo/net[/green]")
return orchestrator
# Parse model string to get provider and model
@@ -2147,9 +2046,7 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
tools=[], # Orchestrator tools will be added
)
self.console.print(
f"[green]✓ Created {self.orchestrator_model} orchestrator[/green]"
)
self.console.print(f"[green]✓ Created {self.orchestrator_model} orchestrator[/green]")
return orchestrator
def _get_orchestrator_system_prompt(self) -> str:
@@ -2282,10 +2179,7 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
"count",
"find",
]
if (
any(keyword in prompt_lower for keyword in simple_keywords)
and self.local_workers
):
if any(keyword in prompt_lower for keyword in simple_keywords) and self.local_workers:
return self.local_workers[0].name
# Complex implementation → API workers (Claude)
@@ -2297,10 +2191,7 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
"design",
"architect",
]
if (
any(keyword in prompt_lower for keyword in complex_keywords)
and self.api_workers
):
if any(keyword in prompt_lower for keyword in complex_keywords) and self.api_workers:
return self.api_workers[0].name
# Review tasks → Critics
@@ -2311,10 +2202,7 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
"improve",
"validate code",
]
if (
any(keyword in prompt_lower for keyword in review_keywords)
and self.critics
):
if any(keyword in prompt_lower for keyword in review_keywords) and self.critics:
return self.critics[0].name
# Strategic decisions → Orchestrator
@@ -2335,20 +2223,12 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
return self.local_workers[0].name
# Fall back to API workers for complex tasks
return (
self.api_workers[0].name
if self.api_workers
else self.orchestrator.name
)
return self.api_workers[0].name if self.api_workers else self.orchestrator.name
# Create the cost-optimized router
router = CostOptimizedRouter(
self.orchestrator_agent, self.worker_agents, self.critic_agents
)
router = CostOptimizedRouter(self.orchestrator_agent, self.worker_agents, self.critic_agents)
self.console.print(
"[green]✓ Created cost-optimized router (local models preferred)[/green]"
)
self.console.print("[green]✓ Created cost-optimized router (local models preferred)[/green]")
return router
async def _create_intelligent_router(self) -> Router:
@@ -2383,9 +2263,7 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
return router
async def execute_with_network(
self, task: str, context: Optional[Dict] = None
) -> Dict:
async def execute_with_network(self, task: str, context: Optional[Dict] = None) -> Dict:
"""Execute a task using the agent network.
Args:
@@ -2523,9 +2401,7 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
config = await self._create_instance_config(i, role)
self.instance_configs.append(config)
self.console.print(
f" [{i + 1}/{self.num_instances}] {role} instance configured"
)
self.console.print(f" [{i + 1}/{self.num_instances}] {role} instance configured")
# If networking enabled, configure MCP connections between instances
if self.enable_networking:
@@ -2587,9 +2463,7 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
async def _setup_mcp_networking(self):
"""Set up MCP networking between Claude instances."""
self.console.print(
"[cyan]Setting up MCP networking between instances...[/cyan]"
)
self.console.print("[cyan]Setting up MCP networking between instances...[/cyan]")
# Each instance gets MCP servers for all other instances
for i, config in enumerate(self.instance_configs):
@@ -2732,14 +2606,10 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
# Validate improvement didn't degrade quality
if await self._validate_improvement(result, improved):
self.console.print(
"[green]✓ Solution improved with System 2 feedback[/green]"
)
self.console.print("[green]✓ Solution improved with System 2 feedback[/green]")
return improved
else:
self.console.print(
"[yellow]⚠ Keeping original solution (improvement validation failed)[/yellow]"
)
self.console.print("[yellow]⚠ Keeping original solution (improvement validation failed)[/yellow]")
return result
@@ -2816,9 +2686,7 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
try:
import subprocess
result = subprocess.run(
["gemini", "chat", prompt], capture_output=True, text=True, timeout=30
)
result = subprocess.run(["gemini", "chat", prompt], capture_output=True, text=True, timeout=30)
if result.returncode == 0 and result.stdout:
return {"output": result.stdout.strip(), "success": True}
except Exception as e:
@@ -2882,9 +2750,7 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
logger.error(f"OpenAI API error: {e}")
# Try Anthropic
anthropic_key = os.environ.get("ANTHROPIC_API_KEY") or os.getenv(
"ANTHROPIC_API_KEY"
)
anthropic_key = os.environ.get("ANTHROPIC_API_KEY") or os.getenv("ANTHROPIC_API_KEY")
if anthropic_key:
try:
from anthropic import AsyncAnthropic
@@ -2903,9 +2769,7 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
# Try fallback handler as last resort
from .fallback_handler import smart_chat
response = await smart_chat(
prompt, console=None
) # No console to avoid duplicate messages
response = await smart_chat(prompt, console=None) # No console to avoid duplicate messages
if response:
return {"output": response, "success": True}
@@ -2948,25 +2812,15 @@ async def main():
"""Main entry point for hanzo-dev."""
import argparse
parser = argparse.ArgumentParser(
description="Hanzo Dev - System 2 Meta-AI Orchestrator"
)
parser.add_argument(
"--workspace", default="~/.hanzo/dev", help="Workspace directory"
)
parser = argparse.ArgumentParser(description="Hanzo Dev - System 2 Meta-AI Orchestrator")
parser.add_argument("--workspace", default="~/.hanzo/dev", help="Workspace directory")
parser.add_argument("--claude-path", help="Path to Claude Code executable")
parser.add_argument("--monitor", action="store_true", help="Start in monitor mode")
parser.add_argument("--repl", action="store_true", help="Start REPL interface")
parser.add_argument(
"--instances", type=int, default=2, help="Number of Claude instances"
)
parser.add_argument("--instances", type=int, default=2, help="Number of Claude instances")
parser.add_argument("--no-mcp", action="store_true", help="Disable MCP tools")
parser.add_argument(
"--no-network", action="store_true", help="Disable instance networking"
)
parser.add_argument(
"--no-guardrails", action="store_true", help="Disable guardrails"
)
parser.add_argument("--no-network", action="store_true", help="Disable instance networking")
parser.add_argument("--no-guardrails", action="store_true", help="Disable guardrails")
args = parser.parse_args()
+7 -21
View File
@@ -23,9 +23,7 @@ class FallbackHandler:
"deepseek_api": bool(os.getenv("DEEPSEEK_API_KEY")), # Added DeepSeek
"openai_api": bool(os.getenv("OPENAI_API_KEY")),
"anthropic_api": bool(os.getenv("ANTHROPIC_API_KEY")),
"google_api": bool(
os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
),
"google_api": bool(os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")),
"openai_cli": shutil.which("openai") is not None,
"claude_cli": shutil.which("claude") is not None,
"gemini_cli": shutil.which("gemini") is not None,
@@ -103,9 +101,7 @@ class FallbackHandler:
suggestions = []
if not self.available_options["deepseek_api"]:
suggestions.append(
"• Set DEEPSEEK_API_KEY for cost-effective DeepSeek access ($0.14/M tokens)"
)
suggestions.append("• Set DEEPSEEK_API_KEY for cost-effective DeepSeek access ($0.14/M tokens)")
if not self.available_options["openai_api"]:
suggestions.append("• Set OPENAI_API_KEY for GPT-4/GPT-5 access")
@@ -114,30 +110,22 @@ class FallbackHandler:
suggestions.append("• Set ANTHROPIC_API_KEY for Claude access")
if not self.available_options["ollama"]:
suggestions.append(
"• Install Ollama: curl -fsSL https://ollama.com/install.sh | sh"
)
suggestions.append("• Install Ollama: curl -fsSL https://ollama.com/install.sh | sh")
suggestions.append(" Then run: ollama pull llama3.2")
if not self.available_options["openai_cli"]:
suggestions.append("• Install OpenAI CLI: pip install openai-cli")
if not self.available_options["claude_cli"]:
suggestions.append(
"• Install Claude Desktop from https://claude.ai/download"
)
suggestions.append("• Install Claude Desktop from https://claude.ai/download")
return (
"\n".join(suggestions) if suggestions else "All AI options are available!"
)
return "\n".join(suggestions) if suggestions else "All AI options are available!"
def print_status(self, console):
"""Print the current status of available AI options."""
from rich.table import Table
table = Table(
title="Available AI Options", show_header=True, header_style="bold magenta"
)
table = Table(title="Available AI Options", show_header=True, header_style="bold magenta")
table.add_column("Option", style="cyan", width=20)
table.add_column("Status", width=10)
table.add_column("Model", width=20)
@@ -164,9 +152,7 @@ class FallbackHandler:
console.print(table)
if self.fallback_order:
console.print(
f"\n[green]Primary option: {self.fallback_order[0][1]}[/green]"
)
console.print(f"\n[green]Primary option: {self.fallback_order[0][1]}[/green]")
if len(self.fallback_order) > 1:
fallbacks = ", ".join([opt[1] for opt in self.fallback_order[1:]])
console.print(f"[yellow]Fallback options: {fallbacks}[/yellow]")
+34 -112
View File
@@ -98,9 +98,7 @@ class EnhancedHanzoREPL:
self.failed_tools = set() # Track tools that have failed this session
# Initialize background task manager
self.task_manager = (
BackgroundTaskManager(console) if BackgroundTaskManager else None
)
self.task_manager = BackgroundTaskManager(console) if BackgroundTaskManager else None
# Initialize todo manager
self.todo_manager = TodoManager(console) if TodoManager else None
@@ -114,9 +112,7 @@ class EnhancedHanzoREPL:
if default_tool:
self.current_model = f"tool:{default_tool.name}"
self.current_tool = default_tool
self.console.print(
f"[green]✓ Detected {default_tool.display_name} as default AI assistant[/green]"
)
self.console.print(f"[green]✓ Detected {default_tool.display_name} as default AI assistant[/green]")
else:
# Fallback to regular models
self.current_model = self.config.get("default_model", "gpt-3.5-turbo")
@@ -355,9 +351,7 @@ class EnhancedHanzoREPL:
# Router status
try:
response = httpx.get("http://localhost:4000/health", timeout=1)
router_status = (
"✅ Running" if response.status_code == 200 else "⚠️ Unhealthy"
)
router_status = "✅ Running" if response.status_code == 200 else "⚠️ Unhealthy"
router_details = "Port 4000"
except Exception:
router_status = "❌ Offline"
@@ -410,11 +404,7 @@ class EnhancedHanzoREPL:
args = f"tool:{tool.name}"
else:
# It's a model selection
model_idx = (
num - len(self.detected_tools) - 1
if self.detected_tools
else num - 1
)
model_idx = num - len(self.detected_tools) - 1 if self.detected_tools else num - 1
models_list = list(self.MODELS.keys())
if 0 <= model_idx < len(models_list):
args = models_list[model_idx]
@@ -427,11 +417,7 @@ class EnhancedHanzoREPL:
return
# Check if it's a tool
if (
args.startswith("tool:") or args in [t.name for t in self.detected_tools]
if self.detected_tools
else False
):
if args.startswith("tool:") or args in [t.name for t in self.detected_tools] if self.detected_tools else False:
# Handle tool selection
tool_name = args.replace("tool:", "") if args.startswith("tool:") else args
@@ -465,9 +451,7 @@ class EnhancedHanzoREPL:
# Show hint if it's a gateway model (not in our predefined list)
if args not in self.MODELS and not args.startswith("local:"):
self.console.print(
"[dim]Using gateway model - all models free! Use /models to see full list.[/dim]"
)
self.console.print("[dim]Using gateway model - all models free! Use /models to see full list.[/dim]")
async def list_tools(self, args: str = ""):
"""List available AI tools."""
@@ -480,14 +464,10 @@ class EnhancedHanzoREPL:
"""List available models from gateway.hanzo.ai."""
# Show tools first if available
if self.detected_tools:
self.console.print(
"[bold cyan]AI Coding Assistants (Detected):[/bold cyan]"
)
self.console.print("[bold cyan]AI Coding Assistants (Detected):[/bold cyan]")
for i, tool in enumerate(self.detected_tools, 1):
marker = "→" if self.current_model == f"tool:{tool.name}" else " "
self.console.print(
f" {marker} {i}. {tool.display_name} ({tool.provider})"
)
self.console.print(f" {marker} {i}. {tool.display_name} ({tool.provider})")
self.console.print()
# Try to fetch live models from gateway
@@ -507,20 +487,14 @@ class EnhancedHanzoREPL:
models_list = [
model
for model in all_models
if not any(
keyword in model.get("id", "").lower()
for keyword in embedding_keywords
)
if not any(keyword in model.get("id", "").lower() for keyword in embedding_keywords)
]
except Exception:
pass
# Fallback to static models if gateway fetch fails
if not models_list:
models_list = [
{"id": model_id, "name": model_name}
for model_id, model_name in self.MODELS.items()
]
models_list = [{"id": model_id, "name": model_name} for model_id, model_name in self.MODELS.items()]
# Create table
table = Table(title="Gateway LLMs (Chat Models)", box=box.ROUNDED)
@@ -559,22 +533,16 @@ class EnhancedHanzoREPL:
# Highlight current model
if model_id == self.current_model:
table.add_row(
str(i), f"[bold green]→ {model_id}[/bold green]", tier, provider
)
table.add_row(str(i), f"[bold green]→ {model_id}[/bold green]", tier, provider)
else:
table.add_row(str(i), model_id, tier, provider)
self.console.print(table)
self.console.print(
"\n[dim cyan]Free tier:[/dim cyan] [green]Most models available without login![/green]"
)
self.console.print("\n[dim cyan]Free tier:[/dim cyan] [green]Most models available without login![/green]")
self.console.print(
"[dim cyan]Premium models:[/dim cyan] [yellow]gpt-4, claude-3-opus, o1-preview[/yellow] - [cyan]hanzo auth login[/cyan]"
)
self.console.print(
"\n[dim]Use /model <name> or /model <number> to switch[/dim]"
)
self.console.print("\n[dim]Use /model <name> or /model <number> to switch[/dim]")
async def login(self, args: str = ""):
"""Login to Hanzo."""
@@ -697,9 +665,7 @@ class EnhancedHanzoREPL:
self.console.print(f"[dim]Executing: {full_cmd}[/dim]")
try:
result = subprocess.run(
full_cmd, shell=True, capture_output=True, text=True
)
result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True)
if result.stdout:
self.console.print(result.stdout)
@@ -720,19 +686,13 @@ class EnhancedHanzoREPL:
if tool.name not in self.failed_tools:
self.current_tool = tool
self.current_model = f"tool:{tool.name}"
self.console.print(
f"[yellow]Switched to {tool.display_name}[/yellow]"
)
self.console.print(f"[yellow]Switched to {tool.display_name}[/yellow]")
break
# Try the current tool
if self.current_tool.name not in self.failed_tools:
self.console.print(
f"[dim]Using {self.current_tool.display_name}...[/dim]"
)
success, output = self.tool_detector.execute_with_tool(
self.current_tool, message
)
self.console.print(f"[dim]Using {self.current_tool.display_name}...[/dim]")
success, output = self.tool_detector.execute_with_tool(self.current_tool, message)
if success:
self.console.print(output)
@@ -747,12 +707,8 @@ class EnhancedHanzoREPL:
if self.tool_detector and self.tool_detector.detected_tools:
for fallback_tool in self.tool_detector.detected_tools:
if fallback_tool.name not in self.failed_tools:
self.console.print(
f"[yellow]Trying {fallback_tool.display_name}...[/yellow]"
)
success, output = self.tool_detector.execute_with_tool(
fallback_tool, message
)
self.console.print(f"[yellow]Trying {fallback_tool.display_name}...[/yellow]")
success, output = self.tool_detector.execute_with_tool(fallback_tool, message)
if success:
self.console.print(output)
# Automatically switch to this working tool
@@ -768,21 +724,15 @@ class EnhancedHanzoREPL:
else:
# Mark as failed
self.failed_tools.add(fallback_tool.name)
self.console.print(
f"[red]{fallback_tool.display_name} also failed[/red]"
)
self.console.print(f"[red]{fallback_tool.display_name} also failed[/red]")
if not found_working:
# Final fallback to cloud model
self.console.print(f"[yellow]Falling back to cloud model...[/yellow]")
await self.execute_command(
"ask", f"--cloud --model gpt-3.5-turbo {message}"
)
await self.execute_command("ask", f"--cloud --model gpt-3.5-turbo {message}")
else:
# Use regular model through hanzo ask
await self.execute_command(
"ask", f"--cloud --model {self.current_model} {message}"
)
await self.execute_command("ask", f"--cloud --model {self.current_model} {message}")
async def quick_model_select(self, args: str = ""):
"""Quick model selector with arrow keys."""
@@ -791,11 +741,7 @@ class EnhancedHanzoREPL:
return
# Prepare tools and models
tools = (
[(f"tool:{t.name}", t.display_name) for t in self.detected_tools]
if self.detected_tools
else []
)
tools = [(f"tool:{t.name}", t.display_name) for t in self.detected_tools] if self.detected_tools else []
models = list(self.MODELS.items())
selector = QuickModelSelector(models, tools, self.current_model)
@@ -826,9 +772,7 @@ class EnhancedHanzoREPL:
else:
# Show tasks and prompt for selection
self.task_manager.list_tasks()
self.console.print(
"\n[cyan]Enter task ID to kill (or 'all' for all tasks):[/cyan]"
)
self.console.print("\n[cyan]Enter task ID to kill (or 'all' for all tasks):[/cyan]")
try:
task_id = await self.session.prompt_async("> ")
if task_id:
@@ -863,9 +807,7 @@ class EnhancedHanzoREPL:
# Quick add
try:
todo = self.todo_manager.quick_add(rest)
self.console.print(
f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]"
)
self.console.print(f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]")
except ValueError as e:
self.console.print(f"[red]Error: {e}[/red]")
else:
@@ -891,9 +833,7 @@ class EnhancedHanzoREPL:
elif key in ["tag", "t"]:
tag = value
todos = self.todo_manager.list_todos(
status=status, priority=priority, tag=tag
)
todos = self.todo_manager.list_todos(status=status, priority=priority, tag=tag)
title = "Filtered Todos" if (status or priority or tag) else "All Todos"
self.todo_manager.display_todos(todos, title)
@@ -902,9 +842,7 @@ class EnhancedHanzoREPL:
if rest:
todo = self.todo_manager.update_todo(rest, status="done")
if todo:
self.console.print(
f"[green]✅ Marked as done: {todo.title}[/green]"
)
self.console.print(f"[green]✅ Marked as done: {todo.title}[/green]")
else:
self.console.print(f"[red]Todo not found: {rest}[/red]")
else:
@@ -960,9 +898,7 @@ class EnhancedHanzoREPL:
elif subcommand in ["clear", "reset"]:
# Clear all todos (with confirmation)
try:
confirm = await self.session.prompt_async(
"Are you sure you want to delete ALL todos? (yes/no): "
)
confirm = await self.session.prompt_async("Are you sure you want to delete ALL todos? (yes/no): ")
if confirm.lower() in ["yes", "y"]:
self.todo_manager.todos = []
self.todo_manager.save_todos()
@@ -980,13 +916,9 @@ class EnhancedHanzoREPL:
# Unknown subcommand, treat as quick add
try:
todo = self.todo_manager.quick_add(args)
self.console.print(
f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]"
)
self.console.print(f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]")
except ValueError:
self.console.print(
f"[yellow]Unknown todo command: {subcommand}[/yellow]"
)
self.console.print(f"[yellow]Unknown todo command: {subcommand}[/yellow]")
self.console.print("[dim]Use /todo help for available commands[/dim]")
async def add_todo_interactive(self):
@@ -1002,21 +934,13 @@ class EnhancedHanzoREPL:
description = await self.session.prompt_async("Description (optional): ")
# Get priority
priority = await self.session.prompt_async(
"Priority (low/medium/high/urgent) [medium]: "
)
priority = await self.session.prompt_async("Priority (low/medium/high/urgent) [medium]: ")
if not priority:
priority = "medium"
# Get tags
tags_input = await self.session.prompt_async(
"Tags (comma-separated, optional): "
)
tags = (
[t.strip() for t in tags_input.split(",") if t.strip()]
if tags_input
else []
)
tags_input = await self.session.prompt_async("Tags (comma-separated, optional): ")
tags = [t.strip() for t in tags_input.split(",") if t.strip()] if tags_input else []
# Get due date
due_date = await self.session.prompt_async("Due date (optional): ")
@@ -1030,9 +954,7 @@ class EnhancedHanzoREPL:
due_date=due_date if due_date else None,
)
self.console.print(
f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]"
)
self.console.print(f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]")
except (KeyboardInterrupt, EOFError):
self.console.print("[yellow]Cancelled[/yellow]")
@@ -14,9 +14,7 @@ from prompt_toolkit.layout.containers import HSplit, Window
class QuickModelSelector:
"""Quick model selector with arrow navigation."""
def __init__(
self, models: List[Tuple[str, str]], tools: List[Tuple[str, str]], current: str
):
def __init__(self, models: List[Tuple[str, str]], tools: List[Tuple[str, str]], current: str):
self.models = models
self.tools = tools
self.current = current
@@ -99,9 +97,7 @@ class QuickModelSelector:
layout = Layout(Window(FormattedTextControl(get_text), wrap_lines=False))
app = Application(
layout=layout, key_bindings=kb, full_screen=False, mouse_support=True
)
app = Application(layout=layout, key_bindings=kb, full_screen=False, mouse_support=True)
return await app.run_async()
@@ -138,9 +134,7 @@ class BackgroundTaskManager:
task = self.tasks[task_id]
if task["process"].poll() is None:
task["process"].terminate()
self.console.print(
f"[yellow]Terminated {task_id}: {task['name']}[/yellow]"
)
self.console.print(f"[yellow]Terminated {task_id}: {task['name']}[/yellow]")
else:
self.console.print(f"[dim]Task {task_id} already stopped[/dim]")
del self.tasks[task_id]
+1 -3
View File
@@ -196,9 +196,7 @@ hanzo> mcp run read_file --arg path=README.md
except subprocess.TimeoutExpired:
self.console.print("[red]Command timed out[/red]")
except FileNotFoundError:
self.console.print(
"[red]Command not found. Make sure 'hanzo' is installed.[/red]"
)
self.console.print("[red]Command not found. Make sure 'hanzo' is installed.[/red]")
except Exception as e:
self.console.print(f"[red]Command error: {e}[/red]")
@@ -293,9 +293,7 @@ class TodoManager:
TodoPriority.LOW: "green",
}.get(todo.priority, "white")
priority_display = (
f"[{priority_color}]{todo.priority.value.upper()}[/{priority_color}]"
)
priority_display = f"[{priority_color}]{todo.priority.value.upper()}[/{priority_color}]"
# Tags
tags_display = ", ".join(todo.tags) if todo.tags else "-"
@@ -389,9 +387,7 @@ class TodoManager:
if not title:
raise ValueError("Todo title cannot be empty")
return self.add_todo(
title=title, priority=priority, tags=tags, due_date=due_date
)
return self.add_todo(title=title, priority=priority, tags=tags, due_date=due_date)
def get_statistics(self) -> Dict[str, Any]:
"""Get todo statistics."""
+3 -9
View File
@@ -57,9 +57,7 @@ class MemoryManager:
try:
with open(self.memory_file, "r") as f:
data = json.load(f)
self.memories = [
MemoryItem.from_dict(item) for item in data.get("memories", [])
]
self.memories = [MemoryItem.from_dict(item) for item in data.get("memories", [])]
except Exception as e:
print(f"Error loading memories: {e}")
self.memories = []
@@ -180,9 +178,7 @@ class MemoryManager:
self.memories = []
self.save_memories()
def get_memories(
self, type: str = None, tags: List[str] = None
) -> List[MemoryItem]:
def get_memories(self, type: str = None, tags: List[str] = None) -> List[MemoryItem]:
"""Get memories filtered by type or tags."""
result = self.memories
@@ -335,9 +331,7 @@ def handle_memory_command(command: str, memory_manager: MemoryManager, console)
console.print("[yellow]No memories stored.[/yellow]")
return True
table = Table(
title="Current Memories", show_header=True, header_style="bold magenta"
)
table = Table(title="Current Memories", show_header=True, header_style="bold magenta")
table.add_column("ID", style="cyan", width=10)
table.add_column("Type", width=12)
table.add_column("Content", width=50)
+1 -3
View File
@@ -16,9 +16,7 @@ from typing import Any, Dict, List, Optional
from dataclasses import field, dataclass
def check_local_node(
host: str = "localhost", port: int = 4000, timeout: float = 0.5
) -> bool:
def check_local_node(host: str = "localhost", port: int = 4000, timeout: float = 0.5) -> bool:
"""Check if hanzo-node is running locally.
Args:
+6 -19
View File
@@ -275,18 +275,10 @@ class SmartRateLimiter:
# Default configs for known APIs
self.configs = {
"openai": RateLimitConfig(
requests_per_minute=60, requests_per_hour=1000, burst_size=10
),
"anthropic": RateLimitConfig(
requests_per_minute=50, requests_per_hour=1000, burst_size=5
),
"local": RateLimitConfig(
requests_per_minute=100, requests_per_hour=10000, burst_size=20
),
"free": RateLimitConfig(
requests_per_minute=10, requests_per_hour=100, burst_size=2
),
"openai": RateLimitConfig(requests_per_minute=60, requests_per_hour=1000, burst_size=10),
"anthropic": RateLimitConfig(requests_per_minute=50, requests_per_hour=1000, burst_size=5),
"local": RateLimitConfig(requests_per_minute=100, requests_per_hour=10000, burst_size=20),
"free": RateLimitConfig(requests_per_minute=10, requests_per_hour=100, burst_size=2),
}
def get_limiter(self, api_type: str) -> RateLimiter:
@@ -296,9 +288,7 @@ class SmartRateLimiter:
self.limiters[api_type] = RateLimiter(config)
return self.limiters[api_type]
async def execute_with_limit(
self, api_type: str, func: Callable, *args, **kwargs
) -> Any:
async def execute_with_limit(self, api_type: str, func: Callable, *args, **kwargs) -> Any:
"""Execute function with appropriate rate limiting."""
limiter = self.get_limiter(api_type)
recovery = ErrorRecovery(limiter)
@@ -307,10 +297,7 @@ class SmartRateLimiter:
def get_all_status(self) -> Dict[str, Dict[str, Any]]:
"""Get status of all limiters."""
return {
api_type: limiter.get_status()
for api_type, limiter in self.limiters.items()
}
return {api_type: limiter.get_status() for api_type, limiter in self.limiters.items()}
# Global instance for easy use
+7 -21
View File
@@ -25,9 +25,7 @@ class StreamingHandler:
async def stream_openai(self, client, messages: list, model: str = "gpt-4") -> str:
"""Stream response from OpenAI API."""
try:
stream = await client.chat.completions.create(
model=model, messages=messages, stream=True, max_tokens=1000
)
stream = await client.chat.completions.create(model=model, messages=messages, stream=True, max_tokens=1000)
self.current_response = ""
self.is_streaming = True
@@ -63,9 +61,7 @@ class StreamingHandler:
self.is_streaming = False
return None
async def stream_anthropic(
self, client, messages: list, model: str = "claude-3-5-sonnet-20241022"
) -> str:
async def stream_anthropic(self, client, messages: list, model: str = "claude-3-5-sonnet-20241022") -> str:
"""Stream response from Anthropic API."""
try:
self.current_response = ""
@@ -81,9 +77,7 @@ class StreamingHandler:
console=self.console,
refresh_per_second=10,
) as live:
async with client.messages.stream(
model=model, messages=messages, max_tokens=1000
) as stream:
async with client.messages.stream(model=model, messages=messages, max_tokens=1000) as stream:
async for text in stream.text_stream:
self.current_response += text
live.update(
@@ -224,15 +218,11 @@ class TypewriterEffect:
for line in lines:
for char in line:
current_code += char
syntax = Syntax(
current_code, language, theme="monokai", line_numbers=True
)
syntax = Syntax(current_code, language, theme="monokai", line_numbers=True)
live.update(syntax)
await asyncio.sleep(speed)
current_code += "\n"
syntax = Syntax(
current_code, language, theme="monokai", line_numbers=True
)
syntax = Syntax(current_code, language, theme="monokai", line_numbers=True)
live.update(syntax)
@@ -250,9 +240,7 @@ async def stream_with_fallback(message: str, console: Console = None) -> Optiona
from openai import AsyncOpenAI
client = AsyncOpenAI()
return await handler.stream_openai(
client, [{"role": "user", "content": message}]
)
return await handler.stream_openai(client, [{"role": "user", "content": message}])
except Exception as e:
if console:
console.print(f"[yellow]OpenAI streaming failed: {e}[/yellow]")
@@ -263,9 +251,7 @@ async def stream_with_fallback(message: str, console: Console = None) -> Optiona
from anthropic import AsyncAnthropic
client = AsyncAnthropic()
return await handler.stream_anthropic(
client, [{"role": "user", "content": message}]
)
return await handler.stream_anthropic(client, [{"role": "user", "content": message}])
except Exception as e:
if console:
console.print(f"[yellow]Anthropic streaming failed: {e}[/yellow]")
+7 -23
View File
@@ -217,9 +217,7 @@ class ToolDetector:
]: # 400/422 means endpoint exists but params wrong
tool.detected = True
tool.version = f"Running (Port {port})"
tool.api_endpoint = (
f"http://localhost:{port}/health" # Update port
)
tool.api_endpoint = f"http://localhost:{port}/health" # Update port
# Try to get model info
try:
@@ -316,11 +314,7 @@ class ToolDetector:
version = tool.version or "Unknown" if tool.detected else "-"
# Highlight the default tool
if (
tool.detected and tool == self.detected_tools[0]
if self.detected_tools
else False
):
if tool.detected and tool == self.detected_tools[0] if self.detected_tools else False:
table.add_row(
str(i),
f"[bold green]→ {tool.display_name}[/bold green]",
@@ -347,15 +341,11 @@ class ToolDetector:
# Special message for Hanzo Node
if default.name == "hanzod":
self.console.print(
"[cyan]🔒 Using local private AI - your data stays on your machine[/cyan]"
)
self.console.print("[cyan]🔒 Using local private AI - your data stays on your machine[/cyan]")
self.console.print("[dim]Manage models with: hanzo node models[/dim]")
else:
self.console.print("\n[yellow]No AI coding tools detected.[/yellow]")
self.console.print(
"[dim]Start Hanzo Node for local AI: hanzo node start[/dim]"
)
self.console.print("[dim]Start Hanzo Node for local AI: hanzo node start[/dim]")
self.console.print("[dim]Or install Claude Code, OpenAI CLI, etc.[/dim]")
def get_tool_command(self, tool: AITool, prompt: str) -> List[str]:
@@ -417,9 +407,7 @@ class ToolDetector:
)
if response.status_code == 200:
result = response.json()
return True, result.get("choices", [{}])[0].get(
"message", {}
).get("content", "")
return True, result.get("choices", [{}])[0].get("message", {}).get("content", "")
else:
return (
False,
@@ -443,9 +431,7 @@ class ToolDetector:
)
if response.status_code == 200:
result = response.json()
return True, result.get("choices", [{}])[0].get(
"message", {}
).get("content", "")
return True, result.get("choices", [{}])[0].get("message", {}).get("content", "")
except Exception as e:
return False, f"Router error: {e}"
@@ -474,8 +460,6 @@ class ToolDetector:
if success:
return True, output, tool
else:
self.console.print(
f"[yellow]{tool.display_name} failed: {output}[/yellow]"
)
self.console.print(f"[yellow]{tool.display_name} failed: {output}[/yellow]")
return False, "No available tools could handle the request", None
+2 -6
View File
@@ -33,9 +33,7 @@ class InlineStartup:
# Check last shown time
if self.last_shown_file.exists():
try:
last_shown = datetime.fromisoformat(
self.last_shown_file.read_text().strip()
)
last_shown = datetime.fromisoformat(self.last_shown_file.read_text().strip())
if datetime.now() - last_shown < self.show_interval:
return False
except Exception:
@@ -65,9 +63,7 @@ class InlineStartup:
message.append("Router management, improved docs", style="yellow dim")
# Show panel
console.print(
Panel(message, box=box.MINIMAL, border_style="cyan", padding=(0, 1))
)
console.print(Panel(message, box=box.MINIMAL, border_style="cyan", padding=(0, 1)))
self.mark_shown()
+3 -9
View File
@@ -232,9 +232,7 @@ class StartupUI:
# Check router status
try:
response = httpx.get("http://localhost:4000/health", timeout=1)
router_status = (
"🟢 Running" if response.status_code == 200 else "🔴 Offline"
)
router_status = "🟢 Running" if response.status_code == 200 else "🔴 Offline"
except Exception:
router_status = "⚫ Offline"
@@ -258,9 +256,7 @@ class StartupUI:
status.append("API: ", style="bold")
status.append(api_status, style="white")
return Panel(
Align.center(status), box=box.ROUNDED, border_style="blue", padding=(0, 1)
)
return Panel(Align.center(status), box=box.ROUNDED, border_style="blue", padding=(0, 1))
def _create_qr_panel(self) -> Panel:
"""Create compact QR code panel for device connection."""
@@ -303,9 +299,7 @@ class StartupUI:
gpu_info = caps.chip[:20]
except Exception:
# Fallback - basic connection info
connection_data = json.dumps(
{"id": socket.gethostname(), "host": local_ip, "type": "hanzo-node"}
)
connection_data = json.dumps({"id": socket.gethostname(), "host": local_ip, "type": "hanzo-node"})
gpu_info = "CPU"
# Generate smallest possible QR code
+3 -11
View File
@@ -14,11 +14,7 @@ def get_config_paths() -> Dict[str, Path]:
# System config
if os.name == "nt": # Windows
paths["system"] = (
Path(os.environ.get("PROGRAMDATA", "C:\\ProgramData"))
/ "hanzo"
/ "config.yaml"
)
paths["system"] = Path(os.environ.get("PROGRAMDATA", "C:\\ProgramData")) / "hanzo" / "config.yaml"
else: # Unix-like
paths["system"] = Path("/etc/hanzo/config.yaml")
@@ -142,9 +138,7 @@ def init_config() -> Dict[str, Path]:
def get_default_model() -> str:
"""Get default model from config or environment."""
return os.environ.get("HANZO_DEFAULT_MODEL") or get_config_value(
"default_model", "llama-3.2-3b"
)
return os.environ.get("HANZO_DEFAULT_MODEL") or get_config_value("default_model", "llama-3.2-3b")
def get_api_key(provider: str) -> Optional[str]:
@@ -167,6 +161,4 @@ def get_api_key(provider: str) -> Optional[str]:
def is_local_preferred() -> bool:
"""Check if local execution is preferred."""
return os.environ.get("HANZO_USE_LOCAL", "").lower() == "true" or get_config_value(
"prefer_local", False
)
return os.environ.get("HANZO_USE_LOCAL", "").lower() == "true" or get_config_value("prefer_local", False)
+2 -6
View File
@@ -44,9 +44,7 @@ def check_net_installation() -> Tuple[bool, Optional[str], Optional[str]]:
return False, str(net_path), str(venv_python)
# No venv, check system Python
result = subprocess.run(
[sys.executable, "-c", "import scapy"], capture_output=True, text=True
)
result = subprocess.run([sys.executable, "-c", "import scapy"], capture_output=True, text=True)
if result.returncode == 0:
return True, str(net_path), sys.executable
@@ -104,9 +102,7 @@ def get_missing_dependencies(python_exe: str = None) -> list:
missing = []
for package in required_packages:
result = subprocess.run(
[python_exe, "-c", f"import {package}"], capture_output=True, text=True
)
result = subprocess.run([python_exe, "-c", f"import {package}"], capture_output=True, text=True)
if result.returncode != 0:
missing.append(package)