mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-26 11:49:36 +00:00
feat: add REPL, IDE tools and ZAP protocol
- hanzo-tools-repl: Multi-language REPL with Jupyter kernel backend - Supports Python, Node.js/TypeScript, Bash, Ruby, Go, Rust - Persistent sessions for stateful evaluation - History and completion support - hanzo-tools-ide: IDE integration for VS Code, Cursor, JetBrains - Full editor control (open, edit, navigate, refactor) - Terminal integration - Diagnostics and quick fixes - WebSocket bridge to IDE extensions - ZAP Protocol (proto/zap/): - MCP superset for agentic operations - Streaming, REPL sessions, IDE integration - Browser DevTools support - Extension bridge specification - hanzo-mcp v0.11.7: - Added [repl], [ide], [interactive] extras
This commit is contained in:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "hanzo-mcp"
|
||||
version = "0.11.6"
|
||||
version = "0.11.7"
|
||||
description = "The Zen of Hanzo MCP: One server to rule them all. The ultimate MCP that orchestrates all others."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
@@ -68,6 +68,25 @@ memory = [
|
||||
"fastembed>=0.4.0",
|
||||
]
|
||||
|
||||
# Interactive REPL with multi-language Jupyter kernels
|
||||
repl = [
|
||||
"hanzo-tools-repl>=0.1.0",
|
||||
"jupyter-client>=8.6.0",
|
||||
"ipykernel>=6.29.0",
|
||||
]
|
||||
|
||||
# IDE integration (VS Code, Cursor, JetBrains)
|
||||
ide = [
|
||||
"hanzo-tools-ide>=0.1.0",
|
||||
"websockets>=12.0",
|
||||
]
|
||||
|
||||
# Full interactive suite (REPL + IDE + Browser)
|
||||
interactive = [
|
||||
"hanzo-mcp[repl,ide]",
|
||||
"hanzo-tools-browser>=0.2.1",
|
||||
]
|
||||
|
||||
# Development
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# hanzo-tools-ide
|
||||
|
||||
IDE integration tool for VS Code, Cursor, Windsurf, JetBrains, and LSP-compatible editors.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-IDE support**: VS Code, Cursor, Windsurf, JetBrains, Neovim
|
||||
- **Full editor control**: Open, edit, navigate, refactor
|
||||
- **Terminal integration**: Create and send commands to terminals
|
||||
- **Diagnostics**: Access errors, warnings, and quick fixes
|
||||
- **Agent-friendly**: Designed for AI agent workflows
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install hanzo-tools-ide
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
Install the Hanzo extension in your IDE:
|
||||
- **VS Code/Cursor/Windsurf**: Install "Hanzo AI" extension
|
||||
- **JetBrains**: Install "Hanzo AI" plugin
|
||||
- **Neovim**: Install hanzo.nvim plugin
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from hanzo_tools.ide import IdeTool
|
||||
|
||||
ide = IdeTool()
|
||||
|
||||
# Connect to IDE (auto-detects)
|
||||
await ide.call(action="connect")
|
||||
|
||||
# Open file
|
||||
await ide.call(action="open", path="/src/main.py", line=42)
|
||||
|
||||
# Insert text
|
||||
await ide.call(action="insert", text="# TODO: fix", line=10)
|
||||
|
||||
# Go to definition
|
||||
await ide.call(action="go_to_definition", line=15, column=8)
|
||||
|
||||
# Rename symbol
|
||||
await ide.call(action="rename", new_name="betterName", line=10, column=5)
|
||||
|
||||
# Run in terminal
|
||||
await ide.call(action="terminal", command="npm test")
|
||||
|
||||
# Get diagnostics
|
||||
await ide.call(action="diagnostics", severity="error")
|
||||
|
||||
# Execute VS Code command
|
||||
await ide.call(action="command", command="editor.action.formatDocument")
|
||||
```
|
||||
|
||||
## Actions
|
||||
|
||||
| Action | Description | Parameters |
|
||||
|--------|-------------|------------|
|
||||
| connect | Connect to IDE | ide?: vscode\|cursor\|jetbrains |
|
||||
| status | Check connection | - |
|
||||
| open | Open file | path, line? |
|
||||
| close | Close file | path? |
|
||||
| save | Save file | path? |
|
||||
| files | List open files | - |
|
||||
| select | Set selection | line, column, end_line?, end_column? |
|
||||
| insert | Insert text | text, line, column? |
|
||||
| replace | Replace text | text, line, column, end_line, end_column |
|
||||
| get_text | Get text | line?, end_line? |
|
||||
| go_to_definition | Navigate | line, column |
|
||||
| find_references | Find refs | line, column |
|
||||
| rename | Rename symbol | new_name, line, column |
|
||||
| format | Format doc | - |
|
||||
| diagnostics | Get errors | severity? |
|
||||
| quick_fix | Apply fix | line, column, index? |
|
||||
| command | Execute cmd | command, args? |
|
||||
| terminal | Terminal | command?, name? |
|
||||
| search | Search | query, include?, exclude? |
|
||||
|
||||
## License
|
||||
|
||||
MIT - Hanzo Industries Inc
|
||||
@@ -0,0 +1,3 @@
|
||||
# Namespace package - see PEP 420
|
||||
from pkgutil import extend_path
|
||||
__path__ = extend_path(__path__, __name__)
|
||||
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
hanzo-tools-ide: IDE integration for VS Code, JetBrains, and LSP editors.
|
||||
|
||||
Provides AI agents with direct control over IDEs:
|
||||
- File operations (open, save, close)
|
||||
- Editor operations (select, insert, replace)
|
||||
- Navigation (go to definition, find references)
|
||||
- Refactoring (rename, extract, format)
|
||||
- Terminal control (create, send commands)
|
||||
- Diagnostics (errors, warnings, quick fixes)
|
||||
|
||||
Usage:
|
||||
ide(action="open", path="/path/to/file.py")
|
||||
ide(action="insert", text="# comment", line=10)
|
||||
ide(action="go_to_definition", line=15, column=8)
|
||||
ide(action="rename", new_name="better_name", line=10, column=5)
|
||||
ide(action="terminal", command="npm test")
|
||||
"""
|
||||
|
||||
from .ide_tool import IdeTool, IdeConnection
|
||||
|
||||
TOOLS = [IdeTool()]
|
||||
|
||||
__all__ = ["IdeTool", "IdeConnection", "TOOLS"]
|
||||
@@ -0,0 +1,515 @@
|
||||
"""
|
||||
IDE Integration Tool for VS Code, JetBrains, and LSP-compatible editors.
|
||||
|
||||
Architecture:
|
||||
- VS Code: Connect via Hanzo extension (WebSocket on port 9225)
|
||||
- JetBrains: Connect via Gateway protocol
|
||||
- Neovim: Connect via RPC socket
|
||||
- Generic: Connect via LSP
|
||||
|
||||
VS Code Extension API surfaces used:
|
||||
- vscode.window: Active editor, terminals, notifications
|
||||
- vscode.workspace: Files, folders, configuration
|
||||
- vscode.commands: Execute any VS Code command
|
||||
- vscode.languages: Diagnostics, completion, hover
|
||||
|
||||
The Hanzo VS Code extension acts as a bridge, exposing these APIs
|
||||
via WebSocket for external AI agent control.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from enum import Enum
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
AIOHTTP_AVAILABLE = True
|
||||
except ImportError:
|
||||
AIOHTTP_AVAILABLE = False
|
||||
|
||||
try:
|
||||
import websockets
|
||||
WEBSOCKETS_AVAILABLE = True
|
||||
except ImportError:
|
||||
WEBSOCKETS_AVAILABLE = False
|
||||
|
||||
from hanzo_tools.core import BaseTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IdeType(str, Enum):
|
||||
"""Supported IDE types."""
|
||||
VSCODE = "vscode"
|
||||
JETBRAINS = "jetbrains"
|
||||
NEOVIM = "neovim"
|
||||
CURSOR = "cursor"
|
||||
WINDSURF = "windsurf"
|
||||
GENERIC = "generic"
|
||||
|
||||
|
||||
@dataclass
|
||||
class IdeConnection:
|
||||
"""Connection state for an IDE."""
|
||||
ide_type: IdeType
|
||||
endpoint: str
|
||||
websocket: Any = None
|
||||
connected: bool = False
|
||||
workspace_path: str | None = None
|
||||
active_file: str | None = None
|
||||
pending_requests: dict[str, asyncio.Future] = field(default_factory=dict)
|
||||
|
||||
async def connect(self) -> bool:
|
||||
"""Establish connection to IDE."""
|
||||
if not WEBSOCKETS_AVAILABLE:
|
||||
logger.warning("websockets not available")
|
||||
return False
|
||||
|
||||
try:
|
||||
self.websocket = await websockets.connect(
|
||||
self.endpoint,
|
||||
ping_interval=30,
|
||||
ping_timeout=10,
|
||||
)
|
||||
self.connected = True
|
||||
|
||||
# Start message receiver
|
||||
asyncio.create_task(self._receive_loop())
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to connect to {self.ide_type}: {e}")
|
||||
return False
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
"""Close connection."""
|
||||
if self.websocket:
|
||||
await self.websocket.close()
|
||||
self.connected = False
|
||||
|
||||
async def send(self, action: str, **params: Any) -> dict[str, Any]:
|
||||
"""Send request and wait for response."""
|
||||
if not self.connected or not self.websocket:
|
||||
return {"error": "Not connected to IDE"}
|
||||
|
||||
request_id = str(uuid.uuid4())
|
||||
|
||||
message = {
|
||||
"id": request_id,
|
||||
"action": action,
|
||||
"params": params,
|
||||
}
|
||||
|
||||
future: asyncio.Future[dict] = asyncio.get_event_loop().create_future()
|
||||
self.pending_requests[request_id] = future
|
||||
|
||||
try:
|
||||
await self.websocket.send(json.dumps(message))
|
||||
result = await asyncio.wait_for(future, timeout=30.0)
|
||||
return result
|
||||
except asyncio.TimeoutError:
|
||||
return {"error": "Request timed out"}
|
||||
finally:
|
||||
self.pending_requests.pop(request_id, None)
|
||||
|
||||
async def _receive_loop(self) -> None:
|
||||
"""Receive messages from IDE."""
|
||||
try:
|
||||
async for message in self.websocket:
|
||||
data = json.loads(message)
|
||||
request_id = data.get("id")
|
||||
|
||||
if request_id and request_id in self.pending_requests:
|
||||
self.pending_requests[request_id].set_result(data)
|
||||
else:
|
||||
# Event notification
|
||||
await self._handle_event(data)
|
||||
except Exception as e:
|
||||
logger.debug(f"Receive loop error: {e}")
|
||||
self.connected = False
|
||||
|
||||
async def _handle_event(self, data: dict) -> None:
|
||||
"""Handle event notification from IDE."""
|
||||
event_type = data.get("event")
|
||||
if event_type == "activeEditorChanged":
|
||||
self.active_file = data.get("path")
|
||||
elif event_type == "workspaceChanged":
|
||||
self.workspace_path = data.get("path")
|
||||
|
||||
|
||||
class IdeManager:
|
||||
"""Manages IDE connections."""
|
||||
|
||||
_instance: IdeManager | None = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.connections: dict[IdeType, IdeConnection] = {}
|
||||
self.default_ide: IdeType | None = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> IdeManager:
|
||||
"""Get singleton instance."""
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def connect(self, ide_type: IdeType = IdeType.VSCODE) -> IdeConnection:
|
||||
"""Connect to an IDE."""
|
||||
if ide_type in self.connections and self.connections[ide_type].connected:
|
||||
return self.connections[ide_type]
|
||||
|
||||
# Default endpoints for each IDE type
|
||||
endpoints = {
|
||||
IdeType.VSCODE: "ws://localhost:9225",
|
||||
IdeType.CURSOR: "ws://localhost:9226",
|
||||
IdeType.WINDSURF: "ws://localhost:9227",
|
||||
IdeType.JETBRAINS: "ws://localhost:63342/api/hanzo",
|
||||
IdeType.NEOVIM: "ws://localhost:9228",
|
||||
}
|
||||
|
||||
endpoint = endpoints.get(ide_type, endpoints[IdeType.VSCODE])
|
||||
|
||||
conn = IdeConnection(ide_type=ide_type, endpoint=endpoint)
|
||||
if await conn.connect():
|
||||
self.connections[ide_type] = conn
|
||||
if self.default_ide is None:
|
||||
self.default_ide = ide_type
|
||||
return conn
|
||||
|
||||
return conn
|
||||
|
||||
async def auto_connect(self) -> IdeConnection | None:
|
||||
"""Auto-detect and connect to available IDE."""
|
||||
for ide_type in [IdeType.VSCODE, IdeType.CURSOR, IdeType.WINDSURF]:
|
||||
conn = await self.connect(ide_type)
|
||||
if conn.connected:
|
||||
return conn
|
||||
return None
|
||||
|
||||
def get_connection(self, ide_type: IdeType | None = None) -> IdeConnection | None:
|
||||
"""Get existing connection."""
|
||||
ide_type = ide_type or self.default_ide
|
||||
if ide_type:
|
||||
return self.connections.get(ide_type)
|
||||
return None
|
||||
|
||||
|
||||
class IdeTool(BaseTool):
|
||||
"""
|
||||
IDE integration tool for controlling VS Code, JetBrains, and other editors.
|
||||
|
||||
Connects to the Hanzo IDE extension (VS Code, Cursor, Windsurf) via WebSocket.
|
||||
Provides full control over editor operations for AI agents.
|
||||
|
||||
Actions:
|
||||
- connect: Connect to IDE (auto-detects VS Code, Cursor, Windsurf)
|
||||
- status: Check connection status
|
||||
- open: Open file in editor
|
||||
- close: Close file
|
||||
- save: Save current file
|
||||
- files: List open files
|
||||
- select: Set selection in editor
|
||||
- insert: Insert text at position
|
||||
- replace: Replace text in range
|
||||
- get_text: Get text from file/selection
|
||||
- go_to_definition: Navigate to symbol definition
|
||||
- find_references: Find all references to symbol
|
||||
- rename: Rename symbol across workspace
|
||||
- format: Format document
|
||||
- diagnostics: Get errors and warnings
|
||||
- quick_fix: Apply quick fix
|
||||
- command: Execute VS Code command
|
||||
- terminal: Create/send to terminal
|
||||
- search: Search in workspace
|
||||
"""
|
||||
|
||||
name = "ide"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return """IDE control for VS Code, Cursor, Windsurf, and JetBrains.
|
||||
|
||||
ACTIONS:
|
||||
- connect: Connect to IDE (ide?: vscode|cursor|windsurf|jetbrains)
|
||||
- status: Check connection status
|
||||
- open: Open file (path: string, line?: int)
|
||||
- close: Close file (path?: string)
|
||||
- save: Save file (path?: string)
|
||||
- files: List open files
|
||||
- select: Set selection (line: int, column: int, end_line?: int, end_column?: int)
|
||||
- insert: Insert text (text: string, line: int, column?: int)
|
||||
- replace: Replace text (text: string, line: int, column: int, end_line: int, end_column: int)
|
||||
- get_text: Get text (line?: int, end_line?: int)
|
||||
- go_to_definition: Go to definition (line: int, column: int)
|
||||
- find_references: Find references (line: int, column: int)
|
||||
- rename: Rename symbol (new_name: string, line: int, column: int)
|
||||
- format: Format document
|
||||
- diagnostics: Get diagnostics (severity?: error|warning|info)
|
||||
- quick_fix: Apply quick fix (line: int, column: int, index?: int)
|
||||
- command: Execute command (command: string, args?: list)
|
||||
- terminal: Terminal (command?: string, name?: string)
|
||||
- search: Search workspace (query: string, include?: string, exclude?: string)
|
||||
|
||||
EXAMPLES:
|
||||
ide(action="connect")
|
||||
ide(action="open", path="/src/main.py", line=42)
|
||||
ide(action="insert", text="# TODO: fix this", line=10)
|
||||
ide(action="rename", new_name="better_name", line=15, column=8)
|
||||
ide(action="terminal", command="npm test")
|
||||
ide(action="command", command="workbench.action.toggleSidebarVisibility")"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.manager = IdeManager.get_instance()
|
||||
|
||||
async def call(
|
||||
self,
|
||||
action: str = "status",
|
||||
ide: str | None = None,
|
||||
path: str | None = None,
|
||||
line: int | None = None,
|
||||
column: int | None = None,
|
||||
end_line: int | None = None,
|
||||
end_column: int | None = None,
|
||||
text: str | None = None,
|
||||
new_name: str | None = None,
|
||||
command: str | None = None,
|
||||
args: list[Any] | None = None,
|
||||
query: str | None = None,
|
||||
include: str | None = None,
|
||||
exclude: str | None = None,
|
||||
name: str | None = None,
|
||||
severity: str | None = None,
|
||||
index: int | None = None,
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
"""Execute IDE action."""
|
||||
|
||||
action = action.lower()
|
||||
|
||||
# Connection management
|
||||
if action == "connect":
|
||||
return await self._connect(ide)
|
||||
elif action == "status":
|
||||
return self._status()
|
||||
|
||||
# Get connection
|
||||
ide_type = IdeType(ide) if ide else None
|
||||
conn = self.manager.get_connection(ide_type)
|
||||
|
||||
if not conn or not conn.connected:
|
||||
# Try auto-connect
|
||||
conn = await self.manager.auto_connect()
|
||||
if not conn or not conn.connected:
|
||||
return "Not connected to IDE. Use ide(action='connect') first or install Hanzo IDE extension."
|
||||
|
||||
# File operations
|
||||
if action == "open":
|
||||
if not path:
|
||||
return "Error: path required for open action"
|
||||
result = await conn.send("openFile", path=path, line=line)
|
||||
return self._format_result(result, f"Opened {path}")
|
||||
|
||||
elif action == "close":
|
||||
result = await conn.send("closeFile", path=path)
|
||||
return self._format_result(result, "File closed")
|
||||
|
||||
elif action == "save":
|
||||
result = await conn.send("saveFile", path=path)
|
||||
return self._format_result(result, "File saved")
|
||||
|
||||
elif action == "files":
|
||||
result = await conn.send("listOpenFiles")
|
||||
return self._format_result(result)
|
||||
|
||||
# Editor operations
|
||||
elif action == "select":
|
||||
if line is None:
|
||||
return "Error: line required for select action"
|
||||
result = await conn.send(
|
||||
"setSelection",
|
||||
line=line,
|
||||
column=column or 0,
|
||||
endLine=end_line,
|
||||
endColumn=end_column,
|
||||
)
|
||||
return self._format_result(result, "Selection set")
|
||||
|
||||
elif action == "insert":
|
||||
if text is None or line is None:
|
||||
return "Error: text and line required for insert action"
|
||||
result = await conn.send(
|
||||
"insertText",
|
||||
text=text,
|
||||
line=line,
|
||||
column=column or 0,
|
||||
)
|
||||
return self._format_result(result, "Text inserted")
|
||||
|
||||
elif action == "replace":
|
||||
if text is None:
|
||||
return "Error: text required for replace action"
|
||||
result = await conn.send(
|
||||
"replaceText",
|
||||
text=text,
|
||||
line=line,
|
||||
column=column,
|
||||
endLine=end_line,
|
||||
endColumn=end_column,
|
||||
)
|
||||
return self._format_result(result, "Text replaced")
|
||||
|
||||
elif action in ("get_text", "text"):
|
||||
result = await conn.send(
|
||||
"getText",
|
||||
line=line,
|
||||
endLine=end_line,
|
||||
)
|
||||
return self._format_result(result)
|
||||
|
||||
# Navigation
|
||||
elif action in ("go_to_definition", "definition"):
|
||||
if line is None or column is None:
|
||||
return "Error: line and column required"
|
||||
result = await conn.send(
|
||||
"goToDefinition",
|
||||
line=line,
|
||||
column=column,
|
||||
)
|
||||
return self._format_result(result)
|
||||
|
||||
elif action in ("find_references", "references"):
|
||||
if line is None or column is None:
|
||||
return "Error: line and column required"
|
||||
result = await conn.send(
|
||||
"findReferences",
|
||||
line=line,
|
||||
column=column,
|
||||
)
|
||||
return self._format_result(result)
|
||||
|
||||
# Refactoring
|
||||
elif action == "rename":
|
||||
if new_name is None or line is None or column is None:
|
||||
return "Error: new_name, line, and column required"
|
||||
result = await conn.send(
|
||||
"rename",
|
||||
newName=new_name,
|
||||
line=line,
|
||||
column=column,
|
||||
)
|
||||
return self._format_result(result, f"Renamed to {new_name}")
|
||||
|
||||
elif action == "format":
|
||||
result = await conn.send("formatDocument")
|
||||
return self._format_result(result, "Document formatted")
|
||||
|
||||
# Diagnostics
|
||||
elif action == "diagnostics":
|
||||
result = await conn.send("getDiagnostics", severity=severity)
|
||||
return self._format_result(result)
|
||||
|
||||
elif action == "quick_fix":
|
||||
if line is None or column is None:
|
||||
return "Error: line and column required"
|
||||
result = await conn.send(
|
||||
"applyQuickFix",
|
||||
line=line,
|
||||
column=column,
|
||||
index=index or 0,
|
||||
)
|
||||
return self._format_result(result, "Quick fix applied")
|
||||
|
||||
# Commands
|
||||
elif action == "command":
|
||||
if not command:
|
||||
return "Error: command required"
|
||||
result = await conn.send(
|
||||
"executeCommand",
|
||||
command=command,
|
||||
args=args or [],
|
||||
)
|
||||
return self._format_result(result)
|
||||
|
||||
# Terminal
|
||||
elif action == "terminal":
|
||||
if command:
|
||||
result = await conn.send(
|
||||
"sendToTerminal",
|
||||
command=command,
|
||||
name=name,
|
||||
)
|
||||
else:
|
||||
result = await conn.send(
|
||||
"createTerminal",
|
||||
name=name,
|
||||
)
|
||||
return self._format_result(result)
|
||||
|
||||
# Search
|
||||
elif action == "search":
|
||||
if not query:
|
||||
return "Error: query required"
|
||||
result = await conn.send(
|
||||
"searchWorkspace",
|
||||
query=query,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
)
|
||||
return self._format_result(result)
|
||||
|
||||
else:
|
||||
return f"Unknown action: {action}"
|
||||
|
||||
async def _connect(self, ide: str | None) -> str:
|
||||
"""Connect to IDE."""
|
||||
ide_type = IdeType(ide) if ide else None
|
||||
|
||||
if ide_type:
|
||||
conn = await self.manager.connect(ide_type)
|
||||
else:
|
||||
conn = await self.manager.auto_connect()
|
||||
|
||||
if conn and conn.connected:
|
||||
return f"Connected to {conn.ide_type.value}"
|
||||
return "Failed to connect. Ensure Hanzo IDE extension is installed and running."
|
||||
|
||||
def _status(self) -> str:
|
||||
"""Get connection status."""
|
||||
if not self.manager.connections:
|
||||
return "No IDE connections"
|
||||
|
||||
lines = ["IDE Connections:"]
|
||||
for ide_type, conn in self.manager.connections.items():
|
||||
status = "connected" if conn.connected else "disconnected"
|
||||
default = " (default)" if ide_type == self.manager.default_ide else ""
|
||||
lines.append(f" {ide_type.value}: {status}{default}")
|
||||
if conn.workspace_path:
|
||||
lines.append(f" Workspace: {conn.workspace_path}")
|
||||
if conn.active_file:
|
||||
lines.append(f" Active: {conn.active_file}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _format_result(self, result: dict, success_msg: str | None = None) -> str:
|
||||
"""Format result from IDE."""
|
||||
if "error" in result:
|
||||
return f"Error: {result['error']}"
|
||||
|
||||
if success_msg and result.get("success", True):
|
||||
return success_msg
|
||||
|
||||
# Format result data
|
||||
data = result.get("data") or result.get("result")
|
||||
if data:
|
||||
if isinstance(data, str):
|
||||
return data
|
||||
return json.dumps(data, indent=2)
|
||||
|
||||
return str(result)
|
||||
@@ -0,0 +1,43 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "hanzo-tools-ide"
|
||||
version = "0.1.0"
|
||||
description = "IDE integration tool for VS Code, JetBrains, and LSP-compatible editors"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
license = { text = "MIT" }
|
||||
authors = [{ name = "Hanzo Industries Inc", email = "[email protected]" }]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
keywords = ["mcp", "ide", "vscode", "jetbrains", "lsp", "editor"]
|
||||
dependencies = [
|
||||
"hanzo-tools-core>=0.1.0",
|
||||
"aiohttp>=3.9.0",
|
||||
"websockets>=12.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
lsp = ["pygls>=1.3.0"]
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
"pytest-asyncio>=0.23.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/hanzoai/python-sdk"
|
||||
|
||||
[project.entry-points."hanzo.tools"]
|
||||
ide = "hanzo_tools.ide:TOOLS"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["hanzo_tools*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
hanzo_tools = ["py.typed"]
|
||||
@@ -0,0 +1,62 @@
|
||||
# hanzo-tools-repl
|
||||
|
||||
Multi-language REPL tool with Jupyter kernel backend for interactive code evaluation.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-language support**: Python, Node.js/TypeScript, Bash, Ruby, Go, Rust
|
||||
- **Persistent sessions**: Maintain state across evaluations
|
||||
- **Jupyter kernels**: Uses standard Jupyter infrastructure
|
||||
- **Agent-friendly**: Designed for AI agent workflows
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install hanzo-tools-repl
|
||||
|
||||
# With all kernels
|
||||
pip install hanzo-tools-repl[full]
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from hanzo_tools.repl import ReplTool
|
||||
|
||||
repl = ReplTool()
|
||||
|
||||
# Start a Python session
|
||||
await repl.call(action="start", language="python")
|
||||
|
||||
# Evaluate code
|
||||
result = await repl.call(action="eval", code="x = 1 + 1\nprint(x)")
|
||||
# Output: [1] 2
|
||||
|
||||
# Node.js/TypeScript
|
||||
await repl.call(action="start", language="node")
|
||||
result = await repl.call(action="eval", code="const arr = [1,2,3].map(n => n*2)")
|
||||
|
||||
# List sessions
|
||||
await repl.call(action="list")
|
||||
|
||||
# Get history
|
||||
await repl.call(action="history", limit=10)
|
||||
|
||||
# Stop session
|
||||
await repl.call(action="stop")
|
||||
```
|
||||
|
||||
## Supported Languages
|
||||
|
||||
| Language | Kernel | Install |
|
||||
|----------|--------|---------|
|
||||
| Python | ipykernel | `pip install ipykernel` |
|
||||
| Node.js/TypeScript | tslab | `npm install -g tslab && tslab install` |
|
||||
| Bash | bash_kernel | `pip install bash_kernel` |
|
||||
| Ruby | iruby | `gem install iruby` |
|
||||
| Go | gophernotes | See gophernotes docs |
|
||||
| Rust | evcxr | `cargo install evcxr_jupyter` |
|
||||
|
||||
## License
|
||||
|
||||
MIT - Hanzo Industries Inc
|
||||
@@ -0,0 +1,3 @@
|
||||
# Namespace package - see PEP 420
|
||||
from pkgutil import extend_path
|
||||
__path__ = extend_path(__path__, __name__)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
hanzo-tools-repl: Multi-language REPL with Jupyter kernel backend.
|
||||
|
||||
Provides interactive code evaluation for agents across multiple languages:
|
||||
- Python (ipykernel)
|
||||
- Node.js/TypeScript (tslab)
|
||||
- Bash (bash_kernel)
|
||||
- And any language with a Jupyter kernel
|
||||
|
||||
Usage:
|
||||
repl(action="start", language="python") # Start kernel
|
||||
repl(action="eval", code="print('hello')") # Evaluate code
|
||||
repl(action="eval", code="const x = 1", language="typescript")
|
||||
repl(action="history") # Get history
|
||||
repl(action="stop") # Stop kernel
|
||||
repl(action="list") # List kernels
|
||||
"""
|
||||
|
||||
from .repl_tool import ReplTool, KernelManager
|
||||
|
||||
TOOLS = [ReplTool()]
|
||||
|
||||
__all__ = ["ReplTool", "KernelManager", "TOOLS"]
|
||||
@@ -0,0 +1,495 @@
|
||||
"""
|
||||
Unified REPL Tool with Multi-Language Jupyter Kernel Support.
|
||||
|
||||
Provides persistent interactive sessions for code evaluation across languages.
|
||||
Uses Jupyter kernels as backend for consistent cross-language experience.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
# Jupyter client imports
|
||||
try:
|
||||
from jupyter_client import KernelManager as JupyterKernelManager
|
||||
from jupyter_client.asynchronous import AsyncKernelClient
|
||||
JUPYTER_AVAILABLE = True
|
||||
except ImportError:
|
||||
JUPYTER_AVAILABLE = False
|
||||
JupyterKernelManager = None
|
||||
AsyncKernelClient = None
|
||||
|
||||
from hanzo_tools.core import BaseTool
|
||||
|
||||
|
||||
# Language to kernel mapping
|
||||
LANGUAGE_KERNELS: dict[str, str] = {
|
||||
# Python variants
|
||||
"python": "python3",
|
||||
"python3": "python3",
|
||||
"py": "python3",
|
||||
"ipython": "python3",
|
||||
|
||||
# JavaScript/TypeScript
|
||||
"javascript": "tslab",
|
||||
"js": "tslab",
|
||||
"typescript": "tslab",
|
||||
"ts": "tslab",
|
||||
"node": "tslab",
|
||||
|
||||
# Shell
|
||||
"bash": "bash",
|
||||
"sh": "bash",
|
||||
"shell": "bash",
|
||||
"zsh": "bash",
|
||||
|
||||
# Other languages (require kernel installation)
|
||||
"ruby": "ruby",
|
||||
"go": "gophernotes",
|
||||
"rust": "evcxr",
|
||||
"julia": "julia-1.10",
|
||||
"r": "ir",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutionResult:
|
||||
"""Result of code execution."""
|
||||
success: bool
|
||||
output: str
|
||||
error: str | None = None
|
||||
execution_count: int = 0
|
||||
data: dict[str, Any] = field(default_factory=dict)
|
||||
duration_ms: float = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class KernelSession:
|
||||
"""Represents an active kernel session."""
|
||||
id: str
|
||||
language: str
|
||||
kernel_name: str
|
||||
manager: Any # JupyterKernelManager
|
||||
client: Any # AsyncKernelClient
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
execution_count: int = 0
|
||||
history: list[tuple[str, ExecutionResult]] = field(default_factory=list)
|
||||
|
||||
|
||||
class KernelManager:
|
||||
"""Manages multiple Jupyter kernel sessions."""
|
||||
|
||||
_instance: KernelManager | None = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.sessions: dict[str, KernelSession] = {}
|
||||
self.default_session: str | None = None
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> KernelManager:
|
||||
"""Get singleton instance."""
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def start_kernel(
|
||||
self,
|
||||
language: str = "python",
|
||||
session_id: str | None = None,
|
||||
) -> KernelSession:
|
||||
"""Start a new kernel session."""
|
||||
if not JUPYTER_AVAILABLE:
|
||||
raise RuntimeError(
|
||||
"jupyter-client not installed. "
|
||||
"Install with: pip install hanzo-tools-repl[full]"
|
||||
)
|
||||
|
||||
kernel_name = LANGUAGE_KERNELS.get(language.lower(), language)
|
||||
session_id = session_id or f"{language}_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
async with self._lock:
|
||||
if session_id in self.sessions:
|
||||
return self.sessions[session_id]
|
||||
|
||||
# Create kernel manager
|
||||
km = JupyterKernelManager(kernel_name=kernel_name)
|
||||
km.start_kernel()
|
||||
|
||||
# Get async client
|
||||
client = km.client()
|
||||
client.start_channels()
|
||||
|
||||
# Wait for kernel ready
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
session = KernelSession(
|
||||
id=session_id,
|
||||
language=language,
|
||||
kernel_name=kernel_name,
|
||||
manager=km,
|
||||
client=client,
|
||||
)
|
||||
|
||||
self.sessions[session_id] = session
|
||||
|
||||
if self.default_session is None:
|
||||
self.default_session = session_id
|
||||
|
||||
return session
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
code: str,
|
||||
session_id: str | None = None,
|
||||
timeout: float = 30.0,
|
||||
) -> ExecutionResult:
|
||||
"""Execute code in a kernel session."""
|
||||
session_id = session_id or self.default_session
|
||||
|
||||
if not session_id or session_id not in self.sessions:
|
||||
# Auto-start default Python kernel
|
||||
session = await self.start_kernel("python")
|
||||
session_id = session.id
|
||||
|
||||
session = self.sessions[session_id]
|
||||
start_time = datetime.now()
|
||||
|
||||
try:
|
||||
# Execute code
|
||||
msg_id = session.client.execute(code)
|
||||
|
||||
output_parts: list[str] = []
|
||||
error_output: str | None = None
|
||||
data: dict[str, Any] = {}
|
||||
|
||||
# Collect results
|
||||
while True:
|
||||
try:
|
||||
msg = await asyncio.wait_for(
|
||||
asyncio.to_thread(
|
||||
session.client.get_iopub_msg,
|
||||
timeout=timeout
|
||||
),
|
||||
timeout=timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
break
|
||||
|
||||
msg_type = msg.get("msg_type", "")
|
||||
content = msg.get("content", {})
|
||||
|
||||
if msg_type == "stream":
|
||||
output_parts.append(content.get("text", ""))
|
||||
|
||||
elif msg_type == "execute_result":
|
||||
data = content.get("data", {})
|
||||
if "text/plain" in data:
|
||||
output_parts.append(data["text/plain"])
|
||||
|
||||
elif msg_type == "display_data":
|
||||
data.update(content.get("data", {}))
|
||||
if "text/plain" in data:
|
||||
output_parts.append(data["text/plain"])
|
||||
|
||||
elif msg_type == "error":
|
||||
error_output = "\n".join(content.get("traceback", []))
|
||||
|
||||
elif msg_type == "status":
|
||||
if content.get("execution_state") == "idle":
|
||||
break
|
||||
|
||||
duration = (datetime.now() - start_time).total_seconds() * 1000
|
||||
session.execution_count += 1
|
||||
|
||||
result = ExecutionResult(
|
||||
success=error_output is None,
|
||||
output="".join(output_parts),
|
||||
error=error_output,
|
||||
execution_count=session.execution_count,
|
||||
data=data,
|
||||
duration_ms=duration,
|
||||
)
|
||||
|
||||
# Add to history
|
||||
session.history.append((code, result))
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
return ExecutionResult(
|
||||
success=False,
|
||||
output="",
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
async def stop_kernel(self, session_id: str | None = None) -> bool:
|
||||
"""Stop a kernel session."""
|
||||
session_id = session_id or self.default_session
|
||||
|
||||
if not session_id or session_id not in self.sessions:
|
||||
return False
|
||||
|
||||
async with self._lock:
|
||||
session = self.sessions.pop(session_id)
|
||||
session.client.stop_channels()
|
||||
session.manager.shutdown_kernel()
|
||||
|
||||
if self.default_session == session_id:
|
||||
self.default_session = next(iter(self.sessions), None)
|
||||
|
||||
return True
|
||||
|
||||
async def stop_all(self) -> int:
|
||||
"""Stop all kernel sessions."""
|
||||
count = len(self.sessions)
|
||||
for session_id in list(self.sessions.keys()):
|
||||
await self.stop_kernel(session_id)
|
||||
return count
|
||||
|
||||
def list_sessions(self) -> list[dict[str, Any]]:
|
||||
"""List all active sessions."""
|
||||
return [
|
||||
{
|
||||
"id": s.id,
|
||||
"language": s.language,
|
||||
"kernel": s.kernel_name,
|
||||
"created": s.created_at.isoformat(),
|
||||
"executions": s.execution_count,
|
||||
"is_default": s.id == self.default_session,
|
||||
}
|
||||
for s in self.sessions.values()
|
||||
]
|
||||
|
||||
def get_history(
|
||||
self,
|
||||
session_id: str | None = None,
|
||||
limit: int = 20,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get execution history for a session."""
|
||||
session_id = session_id or self.default_session
|
||||
|
||||
if not session_id or session_id not in self.sessions:
|
||||
return []
|
||||
|
||||
session = self.sessions[session_id]
|
||||
history = session.history[-limit:]
|
||||
|
||||
return [
|
||||
{
|
||||
"index": i + 1,
|
||||
"code": code,
|
||||
"success": result.success,
|
||||
"output": result.output[:500] if result.output else None,
|
||||
"error": result.error[:200] if result.error else None,
|
||||
}
|
||||
for i, (code, result) in enumerate(history)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def list_available_kernels() -> list[dict[str, str]]:
|
||||
"""List available Jupyter kernels."""
|
||||
if not JUPYTER_AVAILABLE:
|
||||
return []
|
||||
|
||||
try:
|
||||
from jupyter_client.kernelspec import find_kernel_specs
|
||||
specs = find_kernel_specs()
|
||||
return [
|
||||
{"name": name, "path": path}
|
||||
for name, path in specs.items()
|
||||
]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
class ReplTool(BaseTool):
|
||||
"""
|
||||
Multi-language REPL with Jupyter kernel backend.
|
||||
|
||||
Provides interactive code evaluation for agents across languages:
|
||||
- Python, Node.js/TypeScript, Bash, and any Jupyter kernel
|
||||
|
||||
Actions:
|
||||
- start: Start a new kernel session
|
||||
- eval: Execute code in a session
|
||||
- stop: Stop a kernel session
|
||||
- list: List active sessions
|
||||
- history: Get execution history
|
||||
- kernels: List available kernels
|
||||
|
||||
Examples:
|
||||
repl(action="start", language="python")
|
||||
repl(action="eval", code="x = 1 + 1; print(x)")
|
||||
repl(action="eval", code="console.log('hello')", language="node")
|
||||
repl(action="history", limit=10)
|
||||
repl(action="stop")
|
||||
"""
|
||||
|
||||
name = "repl"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return """Multi-language REPL with persistent Jupyter kernel sessions.
|
||||
|
||||
ACTIONS:
|
||||
- start: Start kernel (language: python|node|typescript|bash|ruby|go|rust)
|
||||
- eval: Execute code (code: string, language?: string, session?: string)
|
||||
- stop: Stop kernel (session?: string)
|
||||
- list: List active sessions
|
||||
- history: Get execution history (session?: string, limit?: int)
|
||||
- kernels: List available Jupyter kernels
|
||||
|
||||
LANGUAGES: python, node/javascript/typescript, bash/shell, ruby, go, rust
|
||||
|
||||
EXAMPLES:
|
||||
repl(action="start", language="python")
|
||||
repl(action="eval", code="print('hello')")
|
||||
repl(action="eval", code="const x = [1,2,3].map(n => n*2)", language="node")
|
||||
repl(action="history")
|
||||
repl(action="stop")"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.manager = KernelManager.get_instance()
|
||||
|
||||
async def call(
|
||||
self,
|
||||
action: str = "eval",
|
||||
code: str | None = None,
|
||||
language: str = "python",
|
||||
session: str | None = None,
|
||||
limit: int = 20,
|
||||
timeout: float = 30.0,
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
"""Execute REPL action."""
|
||||
|
||||
action = action.lower()
|
||||
|
||||
if action == "start":
|
||||
return await self._start(language, session)
|
||||
|
||||
elif action in ("eval", "execute", "run"):
|
||||
if not code:
|
||||
return "Error: code parameter required for eval action"
|
||||
return await self._eval(code, language, session, timeout)
|
||||
|
||||
elif action == "stop":
|
||||
return await self._stop(session)
|
||||
|
||||
elif action == "list":
|
||||
return self._list()
|
||||
|
||||
elif action == "history":
|
||||
return self._history(session, limit)
|
||||
|
||||
elif action == "kernels":
|
||||
return self._kernels()
|
||||
|
||||
else:
|
||||
return f"Unknown action: {action}. Use: start, eval, stop, list, history, kernels"
|
||||
|
||||
async def _start(self, language: str, session_id: str | None) -> str:
|
||||
"""Start a new kernel session."""
|
||||
try:
|
||||
session = await self.manager.start_kernel(language, session_id)
|
||||
return f"""Kernel started:
|
||||
Session: {session.id}
|
||||
Language: {session.language}
|
||||
Kernel: {session.kernel_name}
|
||||
|
||||
Use repl(action="eval", code="...") to execute code."""
|
||||
except Exception as e:
|
||||
return f"Error starting kernel: {e}"
|
||||
|
||||
async def _eval(
|
||||
self,
|
||||
code: str,
|
||||
language: str,
|
||||
session_id: str | None,
|
||||
timeout: float,
|
||||
) -> str:
|
||||
"""Execute code in a session."""
|
||||
# Auto-start kernel if needed
|
||||
if not self.manager.sessions:
|
||||
await self.manager.start_kernel(language)
|
||||
elif session_id is None and language != "python":
|
||||
# Start language-specific kernel if not exists
|
||||
lang_sessions = [
|
||||
s for s in self.manager.sessions.values()
|
||||
if s.language.lower() == language.lower()
|
||||
]
|
||||
if not lang_sessions:
|
||||
await self.manager.start_kernel(language)
|
||||
|
||||
result = await self.manager.execute(code, session_id, timeout)
|
||||
|
||||
if result.success:
|
||||
output = result.output or "(no output)"
|
||||
return f"""[{result.execution_count}] {output}"""
|
||||
else:
|
||||
return f"""[{result.execution_count}] Error:
|
||||
{result.error}"""
|
||||
|
||||
async def _stop(self, session_id: str | None) -> str:
|
||||
"""Stop a kernel session."""
|
||||
if session_id == "all":
|
||||
count = await self.manager.stop_all()
|
||||
return f"Stopped {count} kernel(s)"
|
||||
|
||||
success = await self.manager.stop_kernel(session_id)
|
||||
if success:
|
||||
return f"Kernel stopped: {session_id or 'default'}"
|
||||
return "No active kernel to stop"
|
||||
|
||||
def _list(self) -> str:
|
||||
"""List active sessions."""
|
||||
sessions = self.manager.list_sessions()
|
||||
if not sessions:
|
||||
return "No active REPL sessions"
|
||||
|
||||
lines = ["Active REPL sessions:"]
|
||||
for s in sessions:
|
||||
default = " (default)" if s["is_default"] else ""
|
||||
lines.append(
|
||||
f" {s['id']}: {s['language']} ({s['kernel']}) "
|
||||
f"- {s['executions']} executions{default}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
def _history(self, session_id: str | None, limit: int) -> str:
|
||||
"""Get execution history."""
|
||||
history = self.manager.get_history(session_id, limit)
|
||||
if not history:
|
||||
return "No execution history"
|
||||
|
||||
lines = ["Execution history:"]
|
||||
for h in history:
|
||||
status = "OK" if h["success"] else "ERR"
|
||||
code_preview = h["code"][:50].replace("\n", "\\n")
|
||||
if len(h["code"]) > 50:
|
||||
code_preview += "..."
|
||||
lines.append(f" [{h['index']}] {status}: {code_preview}")
|
||||
return "\n".join(lines)
|
||||
|
||||
def _kernels(self) -> str:
|
||||
"""List available kernels."""
|
||||
kernels = self.manager.list_available_kernels()
|
||||
if not kernels:
|
||||
return "No Jupyter kernels found. Install with: pip install ipykernel"
|
||||
|
||||
lines = ["Available Jupyter kernels:"]
|
||||
for k in kernels:
|
||||
lines.append(f" {k['name']}")
|
||||
|
||||
lines.append("\nSupported language aliases:")
|
||||
for lang, kernel in sorted(set((v, k) for k, v in LANGUAGE_KERNELS.items())):
|
||||
aliases = [k for k, v in LANGUAGE_KERNELS.items() if v == lang]
|
||||
lines.append(f" {lang}: {', '.join(aliases)}")
|
||||
|
||||
return "\n".join(lines)
|
||||
@@ -0,0 +1,51 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "hanzo-tools-repl"
|
||||
version = "0.1.0"
|
||||
description = "Multi-language REPL tool with Jupyter kernel backend for interactive code evaluation"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
license = { text = "MIT" }
|
||||
authors = [{ name = "Hanzo Industries Inc", email = "[email protected]" }]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
keywords = ["mcp", "repl", "jupyter", "interactive", "code", "evaluation"]
|
||||
dependencies = [
|
||||
"hanzo-tools-core>=0.1.0",
|
||||
"jupyter-client>=8.6.0",
|
||||
"jupyter-core>=5.7.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
kernels = [
|
||||
"ipykernel>=6.29.0", # Python kernel
|
||||
"bash_kernel>=0.9.0", # Bash kernel
|
||||
"typescript-kernel>=0.1.0", # TypeScript (via ts-node)
|
||||
]
|
||||
node = ["tslab>=1.0.0"] # Node.js/TypeScript Jupyter kernel
|
||||
full = [
|
||||
"hanzo-tools-repl[kernels,node]",
|
||||
]
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
"pytest-asyncio>=0.23.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/hanzoai/python-sdk"
|
||||
|
||||
[project.entry-points."hanzo.tools"]
|
||||
repl = "hanzo_tools.repl:TOOLS"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["hanzo_tools*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
hanzo_tools = ["py.typed"]
|
||||
@@ -0,0 +1,164 @@
|
||||
# ZAP Protocol: MCP Superset for Agentic Operations
|
||||
|
||||
ZAP (Zero-latency Agent Protocol) extends MCP with capabilities for:
|
||||
- **Interactive Sessions**: REPL/notebook execution across languages
|
||||
- **Streaming**: Real-time output for long-running operations
|
||||
- **IDE Integration**: Full editor control (VS Code, JetBrains, Neovim)
|
||||
- **Browser DevTools**: Console, network, debugger control
|
||||
- **Extension Bridge**: Browser/IDE extension communication
|
||||
|
||||
## Protocol Versions
|
||||
|
||||
| Version | MCP Compat | Features |
|
||||
|---------|------------|----------|
|
||||
| 1.0.0 | 2025-06-18 | Full MCP + REPL + Browser + IDE |
|
||||
|
||||
## Wire Formats
|
||||
|
||||
- **Primary**: Cap'n Proto (binary, zero-copy)
|
||||
- **Fallback**: JSON-RPC 2.0 (MCP compatible)
|
||||
|
||||
## Message Types
|
||||
|
||||
### Tool Execution (MCP Compatible)
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "read",
|
||||
"arguments": {"file_path": "/path/to/file"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### REPL Session
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"method": "repl/eval",
|
||||
"params": {
|
||||
"session_id": "py_abc123",
|
||||
"language": "python",
|
||||
"code": "x = 1 + 1\nprint(x)",
|
||||
"stream_output": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Browser Console
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"method": "browser/console",
|
||||
"params": {
|
||||
"action": "evaluate",
|
||||
"code": "document.title"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IDE Control
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"method": "ide/command",
|
||||
"params": {
|
||||
"action": "rename",
|
||||
"line": 10,
|
||||
"column": 5,
|
||||
"new_name": "betterName"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"method": "stream/start",
|
||||
"params": {
|
||||
"type": "repl_output",
|
||||
"session_id": "py_abc123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Capabilities
|
||||
|
||||
### REPL Languages
|
||||
- Python (ipykernel)
|
||||
- Node.js/TypeScript (tslab)
|
||||
- Bash (bash_kernel)
|
||||
- Ruby, Go, Rust, Julia (with kernels)
|
||||
|
||||
### Browser Actions
|
||||
- JavaScript evaluation
|
||||
- Console message capture
|
||||
- Network interception
|
||||
- DOM manipulation
|
||||
- Debugger control
|
||||
|
||||
### IDE Actions
|
||||
- File operations (open, save, close)
|
||||
- Editor operations (select, insert, replace)
|
||||
- Navigation (go to definition, find references)
|
||||
- Refactoring (rename, format, extract)
|
||||
- Terminal control
|
||||
- Diagnostics and quick fixes
|
||||
|
||||
## Extension Bridge
|
||||
|
||||
ZAP communicates with browser/IDE extensions via WebSocket:
|
||||
|
||||
| Extension | Port | Protocol |
|
||||
|-----------|------|----------|
|
||||
| Browser | 9224 | HTTP/WS |
|
||||
| VS Code | 9225 | WebSocket |
|
||||
| Cursor | 9226 | WebSocket |
|
||||
| Windsurf | 9227 | WebSocket |
|
||||
| Neovim | 9228 | WebSocket |
|
||||
|
||||
## Proto Files
|
||||
|
||||
- `zap.proto` - Main protocol definition
|
||||
- Generated code available for:
|
||||
- Python: `pip install hanzo-zap`
|
||||
- TypeScript: `npm install @hanzo/zap`
|
||||
- Go: `go get github.com/hanzoai/zap`
|
||||
- Rust: `cargo add hanzo-zap`
|
||||
|
||||
## Usage with hanzo-mcp
|
||||
|
||||
ZAP is fully integrated into hanzo-mcp tools:
|
||||
|
||||
```python
|
||||
# REPL evaluation
|
||||
repl(action="eval", code="print('hello')", language="python")
|
||||
|
||||
# Browser console
|
||||
browser(action="console")
|
||||
browser(action="evaluate", code="document.title")
|
||||
|
||||
# IDE control
|
||||
ide(action="open", path="/src/main.py")
|
||||
ide(action="rename", new_name="newFunc", line=10, column=5)
|
||||
```
|
||||
|
||||
## Compared to MCP
|
||||
|
||||
| Feature | MCP | ZAP |
|
||||
|---------|-----|-----|
|
||||
| Tool calls | ✓ | ✓ |
|
||||
| Resources | ✓ | ✓ |
|
||||
| Prompts | ✓ | ✓ |
|
||||
| Streaming | ✗ | ✓ |
|
||||
| REPL sessions | ✗ | ✓ |
|
||||
| IDE integration | ✗ | ✓ |
|
||||
| Browser DevTools | ✗ | ✓ |
|
||||
| Binary protocol | ✗ | ✓ (Cap'n Proto) |
|
||||
| Extension bridge | ✗ | ✓ |
|
||||
|
||||
## License
|
||||
|
||||
MIT - Hanzo Industries Inc
|
||||
@@ -0,0 +1,258 @@
|
||||
# ZAP Extension Bridge Protocol
|
||||
|
||||
The ZAP Extension Bridge enables AI agents to control browsers and IDEs through native extensions.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ WebSocket ┌──────────────────┐
|
||||
│ hanzo-mcp │ ◄──────────────────► │ Browser Extension │
|
||||
│ (AI Agent) │ Port 9224 │ (Chrome/Firefox) │
|
||||
└─────────────────┘ └──────────────────┘
|
||||
│
|
||||
│ WebSocket
|
||||
│ ◄──────────────────────────► ┌──────────────────┐
|
||||
│ Port 9225 │ VS Code Extension │
|
||||
│ └──────────────────┘
|
||||
│
|
||||
│ WebSocket
|
||||
└──────────────────────────────► ┌──────────────────┐
|
||||
Port 9228 │ Neovim Plugin │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
## Browser Extension
|
||||
|
||||
### Installation
|
||||
- Chrome: [Chrome Web Store - Hanzo AI](https://chrome.google.com/webstore/detail/hanzo-ai)
|
||||
- Firefox: [Firefox Add-ons - Hanzo AI](https://addons.mozilla.org/firefox/addon/hanzo-ai)
|
||||
|
||||
### Exposed APIs
|
||||
|
||||
#### Console
|
||||
```typescript
|
||||
// Subscribe to console messages
|
||||
{ action: "console.subscribe" }
|
||||
|
||||
// Evaluate in console
|
||||
{ action: "console.eval", code: "1 + 1" }
|
||||
|
||||
// Get console history
|
||||
{ action: "console.history", level?: "error" | "warn" | "log" }
|
||||
```
|
||||
|
||||
#### DOM
|
||||
```typescript
|
||||
// Get accessibility snapshot
|
||||
{ action: "dom.snapshot" }
|
||||
|
||||
// Query selector
|
||||
{ action: "dom.query", selector: ".button" }
|
||||
|
||||
// Click element
|
||||
{ action: "dom.click", selector: "#submit" }
|
||||
```
|
||||
|
||||
#### Network
|
||||
```typescript
|
||||
// Subscribe to network events
|
||||
{ action: "network.subscribe" }
|
||||
|
||||
// Intercept requests
|
||||
{ action: "network.intercept", pattern: "*/api/*" }
|
||||
|
||||
// Mock response
|
||||
{ action: "network.mock", url: "/api/user", response: { name: "Test" } }
|
||||
```
|
||||
|
||||
#### DevTools
|
||||
```typescript
|
||||
// Set breakpoint
|
||||
{ action: "debugger.breakpoint", url: "app.js", line: 42 }
|
||||
|
||||
// Step through code
|
||||
{ action: "debugger.step" }
|
||||
|
||||
// Get call stack
|
||||
{ action: "debugger.stack" }
|
||||
```
|
||||
|
||||
## VS Code Extension
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
code --install-extension hanzo.hanzo-ai
|
||||
```
|
||||
|
||||
### Exposed APIs
|
||||
|
||||
#### Files
|
||||
```typescript
|
||||
// Open file
|
||||
{ action: "file.open", path: "/src/main.ts", line?: 42 }
|
||||
|
||||
// Save file
|
||||
{ action: "file.save", path?: "/src/main.ts" }
|
||||
|
||||
// Close file
|
||||
{ action: "file.close", path?: "/src/main.ts" }
|
||||
|
||||
// List open files
|
||||
{ action: "file.list" }
|
||||
```
|
||||
|
||||
#### Editor
|
||||
```typescript
|
||||
// Get selection
|
||||
{ action: "editor.selection" }
|
||||
|
||||
// Set selection
|
||||
{ action: "editor.select", line: 10, column: 5, endLine: 10, endColumn: 15 }
|
||||
|
||||
// Insert text
|
||||
{ action: "editor.insert", text: "// TODO", line: 10 }
|
||||
|
||||
// Replace text
|
||||
{ action: "editor.replace", text: "newCode", line: 10, column: 0, endLine: 12, endColumn: 0 }
|
||||
```
|
||||
|
||||
#### Navigation
|
||||
```typescript
|
||||
// Go to definition
|
||||
{ action: "nav.definition", line: 10, column: 5 }
|
||||
|
||||
// Find references
|
||||
{ action: "nav.references", line: 10, column: 5 }
|
||||
|
||||
// Go to symbol
|
||||
{ action: "nav.symbol", query: "MyClass" }
|
||||
```
|
||||
|
||||
#### Refactoring
|
||||
```typescript
|
||||
// Rename symbol
|
||||
{ action: "refactor.rename", line: 10, column: 5, newName: "betterName" }
|
||||
|
||||
// Format document
|
||||
{ action: "refactor.format" }
|
||||
|
||||
// Organize imports
|
||||
{ action: "refactor.imports" }
|
||||
|
||||
// Extract function
|
||||
{ action: "refactor.extract", type: "function", line: 10, endLine: 20 }
|
||||
```
|
||||
|
||||
#### Diagnostics
|
||||
```typescript
|
||||
// Get diagnostics
|
||||
{ action: "diagnostics.get", severity?: "error" | "warning" }
|
||||
|
||||
// Apply quick fix
|
||||
{ action: "diagnostics.fix", line: 10, column: 5, index?: 0 }
|
||||
```
|
||||
|
||||
#### Terminal
|
||||
```typescript
|
||||
// Create terminal
|
||||
{ action: "terminal.create", name?: "Build" }
|
||||
|
||||
// Send to terminal
|
||||
{ action: "terminal.send", command: "npm test", name?: "Build" }
|
||||
|
||||
// Get terminal output
|
||||
{ action: "terminal.output", name?: "Build" }
|
||||
```
|
||||
|
||||
#### Commands
|
||||
```typescript
|
||||
// Execute VS Code command
|
||||
{ action: "command.execute", command: "workbench.action.togglePanel" }
|
||||
|
||||
// List available commands
|
||||
{ action: "command.list", filter?: "workbench" }
|
||||
```
|
||||
|
||||
## Message Format
|
||||
|
||||
All messages use JSON-RPC 2.0:
|
||||
|
||||
### Request
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "unique-id",
|
||||
"method": "action",
|
||||
"params": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "unique-id",
|
||||
"result": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Error
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "unique-id",
|
||||
"error": {
|
||||
"code": -32600,
|
||||
"message": "Invalid Request"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Event (no id)
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "event",
|
||||
"params": {
|
||||
"type": "console.message",
|
||||
"data": { "level": "log", "text": "Hello" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- Extensions only accept connections from localhost
|
||||
- Optional authentication via shared secret
|
||||
- Sandboxed execution (no arbitrary code without user consent)
|
||||
- Rate limiting on sensitive operations
|
||||
- User must enable "AI Control" in extension settings
|
||||
|
||||
## Development
|
||||
|
||||
### Building Browser Extension
|
||||
```bash
|
||||
cd extensions/browser
|
||||
npm install
|
||||
npm run build
|
||||
# Load unpacked extension from dist/
|
||||
```
|
||||
|
||||
### Building VS Code Extension
|
||||
```bash
|
||||
cd extensions/vscode
|
||||
npm install
|
||||
npm run build
|
||||
code --install-extension hanzo-ai-*.vsix
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Run integration tests
|
||||
npm test
|
||||
|
||||
# Manual testing
|
||||
curl -X POST http://localhost:9224 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"action": "status"}'
|
||||
```
|
||||
@@ -0,0 +1,652 @@
|
||||
// ZAP Protocol: MCP Superset for Agentic Operations
|
||||
// Version: 1.0.0
|
||||
//
|
||||
// ZAP extends MCP (Model Context Protocol) with:
|
||||
// - Streaming capabilities for real-time output
|
||||
// - Interactive REPL sessions across languages
|
||||
// - IDE/Editor integration primitives
|
||||
// - Browser DevTools integration
|
||||
// - Extension messaging protocol
|
||||
//
|
||||
// Wire format: Cap'n Proto (primary), JSON-RPC (fallback)
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package zap.v1;
|
||||
|
||||
option go_package = "github.com/hanzoai/zap/proto/zap/v1";
|
||||
|
||||
// ============================================================================
|
||||
// Core Message Types
|
||||
// ============================================================================
|
||||
|
||||
// Base request envelope
|
||||
message ZapRequest {
|
||||
string id = 1;
|
||||
string method = 2;
|
||||
oneof params {
|
||||
ToolCallParams tool_call = 10;
|
||||
ReplParams repl = 11;
|
||||
BrowserParams browser = 12;
|
||||
IdeParams ide = 13;
|
||||
ExtensionParams extension = 14;
|
||||
StreamParams stream = 15;
|
||||
}
|
||||
map<string, string> metadata = 100;
|
||||
}
|
||||
|
||||
// Base response envelope
|
||||
message ZapResponse {
|
||||
string id = 1;
|
||||
oneof result {
|
||||
ToolResult tool_result = 10;
|
||||
ReplResult repl_result = 11;
|
||||
BrowserResult browser_result = 12;
|
||||
IdeResult ide_result = 13;
|
||||
ExtensionResult extension_result = 14;
|
||||
StreamChunk stream_chunk = 15;
|
||||
ErrorResult error = 99;
|
||||
}
|
||||
map<string, string> metadata = 100;
|
||||
}
|
||||
|
||||
message ErrorResult {
|
||||
int32 code = 1;
|
||||
string message = 2;
|
||||
string data = 3;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tool Execution (MCP Compatible)
|
||||
// ============================================================================
|
||||
|
||||
message ToolCallParams {
|
||||
string name = 1;
|
||||
map<string, Value> arguments = 2;
|
||||
}
|
||||
|
||||
message ToolResult {
|
||||
bool success = 1;
|
||||
repeated ContentBlock content = 2;
|
||||
bool is_error = 3;
|
||||
}
|
||||
|
||||
message ContentBlock {
|
||||
oneof content {
|
||||
TextContent text = 1;
|
||||
ImageContent image = 2;
|
||||
ResourceContent resource = 3;
|
||||
BinaryContent binary = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message TextContent {
|
||||
string text = 1;
|
||||
string mime_type = 2; // text/plain, text/markdown, application/json
|
||||
}
|
||||
|
||||
message ImageContent {
|
||||
bytes data = 1;
|
||||
string mime_type = 2; // image/png, image/jpeg
|
||||
string alt_text = 3;
|
||||
}
|
||||
|
||||
message ResourceContent {
|
||||
string uri = 1;
|
||||
string mime_type = 2;
|
||||
bytes data = 3;
|
||||
}
|
||||
|
||||
message BinaryContent {
|
||||
bytes data = 1;
|
||||
string mime_type = 2;
|
||||
}
|
||||
|
||||
// Generic value for arguments
|
||||
message Value {
|
||||
oneof value {
|
||||
string string_value = 1;
|
||||
int64 int_value = 2;
|
||||
double double_value = 3;
|
||||
bool bool_value = 4;
|
||||
ListValue list_value = 5;
|
||||
MapValue map_value = 6;
|
||||
bytes bytes_value = 7;
|
||||
}
|
||||
}
|
||||
|
||||
message ListValue {
|
||||
repeated Value values = 1;
|
||||
}
|
||||
|
||||
message MapValue {
|
||||
map<string, Value> fields = 1;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// REPL / Interactive Sessions
|
||||
// ============================================================================
|
||||
|
||||
message ReplParams {
|
||||
ReplAction action = 1;
|
||||
string session_id = 2;
|
||||
string language = 3;
|
||||
string code = 4;
|
||||
int32 timeout_ms = 5;
|
||||
bool stream_output = 6;
|
||||
}
|
||||
|
||||
enum ReplAction {
|
||||
REPL_ACTION_UNSPECIFIED = 0;
|
||||
REPL_ACTION_START = 1; // Start new kernel session
|
||||
REPL_ACTION_EVAL = 2; // Execute code
|
||||
REPL_ACTION_STOP = 3; // Stop session
|
||||
REPL_ACTION_INTERRUPT = 4; // Interrupt execution
|
||||
REPL_ACTION_COMPLETE = 5; // Tab completion
|
||||
REPL_ACTION_INSPECT = 6; // Inspect object
|
||||
REPL_ACTION_HISTORY = 7; // Get history
|
||||
REPL_ACTION_LIST = 8; // List sessions
|
||||
}
|
||||
|
||||
message ReplResult {
|
||||
string session_id = 1;
|
||||
bool success = 2;
|
||||
int32 execution_count = 3;
|
||||
string output = 4;
|
||||
string error = 5;
|
||||
repeated ReplOutput outputs = 6;
|
||||
repeated CompletionItem completions = 7;
|
||||
InspectResult inspect = 8;
|
||||
}
|
||||
|
||||
message ReplOutput {
|
||||
ReplOutputType type = 1;
|
||||
string text = 2;
|
||||
bytes data = 3;
|
||||
string mime_type = 4;
|
||||
}
|
||||
|
||||
enum ReplOutputType {
|
||||
REPL_OUTPUT_UNSPECIFIED = 0;
|
||||
REPL_OUTPUT_STDOUT = 1;
|
||||
REPL_OUTPUT_STDERR = 2;
|
||||
REPL_OUTPUT_RESULT = 3;
|
||||
REPL_OUTPUT_DISPLAY = 4;
|
||||
REPL_OUTPUT_ERROR = 5;
|
||||
}
|
||||
|
||||
message CompletionItem {
|
||||
string text = 1;
|
||||
string type = 2; // function, variable, module, etc.
|
||||
string signature = 3;
|
||||
string docstring = 4;
|
||||
}
|
||||
|
||||
message InspectResult {
|
||||
string name = 1;
|
||||
string type = 2;
|
||||
string signature = 3;
|
||||
string docstring = 4;
|
||||
string source = 5;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Browser / DevTools Integration
|
||||
// ============================================================================
|
||||
|
||||
message BrowserParams {
|
||||
BrowserAction action = 1;
|
||||
string context_id = 2;
|
||||
string page_id = 3;
|
||||
|
||||
// Action-specific params
|
||||
string url = 10;
|
||||
string selector = 11;
|
||||
string code = 12; // JavaScript to evaluate
|
||||
string expression = 13; // Console expression
|
||||
|
||||
// DevTools specific
|
||||
bool enable_console = 20;
|
||||
bool enable_network = 21;
|
||||
bool enable_dom = 22;
|
||||
bool enable_debugger = 23;
|
||||
}
|
||||
|
||||
enum BrowserAction {
|
||||
BROWSER_ACTION_UNSPECIFIED = 0;
|
||||
|
||||
// Navigation
|
||||
BROWSER_ACTION_NAVIGATE = 1;
|
||||
BROWSER_ACTION_RELOAD = 2;
|
||||
BROWSER_ACTION_BACK = 3;
|
||||
BROWSER_ACTION_FORWARD = 4;
|
||||
|
||||
// DOM interaction
|
||||
BROWSER_ACTION_CLICK = 10;
|
||||
BROWSER_ACTION_TYPE = 11;
|
||||
BROWSER_ACTION_SELECT = 12;
|
||||
BROWSER_ACTION_SCROLL = 13;
|
||||
|
||||
// JavaScript execution
|
||||
BROWSER_ACTION_EVALUATE = 20;
|
||||
BROWSER_ACTION_CONSOLE_EVAL = 21;
|
||||
|
||||
// DevTools
|
||||
BROWSER_ACTION_CONSOLE_SUBSCRIBE = 30;
|
||||
BROWSER_ACTION_NETWORK_SUBSCRIBE = 31;
|
||||
BROWSER_ACTION_DOM_SNAPSHOT = 32;
|
||||
BROWSER_ACTION_SET_BREAKPOINT = 33;
|
||||
BROWSER_ACTION_REMOVE_BREAKPOINT = 34;
|
||||
BROWSER_ACTION_PAUSE = 35;
|
||||
BROWSER_ACTION_RESUME = 36;
|
||||
BROWSER_ACTION_STEP = 37;
|
||||
|
||||
// Context management
|
||||
BROWSER_ACTION_NEW_CONTEXT = 40;
|
||||
BROWSER_ACTION_CLOSE_CONTEXT = 41;
|
||||
BROWSER_ACTION_NEW_PAGE = 42;
|
||||
BROWSER_ACTION_CLOSE_PAGE = 43;
|
||||
BROWSER_ACTION_LIST_PAGES = 44;
|
||||
|
||||
// Screenshots/capture
|
||||
BROWSER_ACTION_SCREENSHOT = 50;
|
||||
BROWSER_ACTION_PDF = 51;
|
||||
BROWSER_ACTION_ACCESSIBILITY_SNAPSHOT = 52;
|
||||
}
|
||||
|
||||
message BrowserResult {
|
||||
bool success = 1;
|
||||
string page_id = 2;
|
||||
string context_id = 3;
|
||||
|
||||
oneof result {
|
||||
EvaluateResult evaluate = 10;
|
||||
ConsoleMessage console_message = 11;
|
||||
NetworkEvent network_event = 12;
|
||||
DomSnapshot dom_snapshot = 13;
|
||||
DebuggerEvent debugger_event = 14;
|
||||
bytes screenshot = 15;
|
||||
PageInfo page_info = 16;
|
||||
repeated PageInfo pages = 17;
|
||||
}
|
||||
}
|
||||
|
||||
message EvaluateResult {
|
||||
string type = 1; // undefined, string, number, object, etc.
|
||||
string value = 2; // Serialized value
|
||||
string preview = 3; // Short preview for objects
|
||||
string class_name = 4;
|
||||
string description = 5;
|
||||
}
|
||||
|
||||
message ConsoleMessage {
|
||||
ConsoleLevel level = 1;
|
||||
string text = 2;
|
||||
string url = 3;
|
||||
int32 line = 4;
|
||||
int32 column = 5;
|
||||
int64 timestamp = 6;
|
||||
repeated string args = 7;
|
||||
}
|
||||
|
||||
enum ConsoleLevel {
|
||||
CONSOLE_LEVEL_UNSPECIFIED = 0;
|
||||
CONSOLE_LEVEL_LOG = 1;
|
||||
CONSOLE_LEVEL_DEBUG = 2;
|
||||
CONSOLE_LEVEL_INFO = 3;
|
||||
CONSOLE_LEVEL_WARNING = 4;
|
||||
CONSOLE_LEVEL_ERROR = 5;
|
||||
}
|
||||
|
||||
message NetworkEvent {
|
||||
NetworkEventType type = 1;
|
||||
string request_id = 2;
|
||||
string url = 3;
|
||||
string method = 4;
|
||||
int32 status = 5;
|
||||
map<string, string> headers = 6;
|
||||
bytes body = 7;
|
||||
int64 timestamp = 8;
|
||||
}
|
||||
|
||||
enum NetworkEventType {
|
||||
NETWORK_EVENT_UNSPECIFIED = 0;
|
||||
NETWORK_EVENT_REQUEST = 1;
|
||||
NETWORK_EVENT_RESPONSE = 2;
|
||||
NETWORK_EVENT_FAILED = 3;
|
||||
}
|
||||
|
||||
message DomSnapshot {
|
||||
string html = 1;
|
||||
string accessibility_tree = 2;
|
||||
repeated DomNode nodes = 3;
|
||||
}
|
||||
|
||||
message DomNode {
|
||||
int32 node_id = 1;
|
||||
string tag = 2;
|
||||
map<string, string> attributes = 3;
|
||||
string text = 4;
|
||||
repeated int32 children = 5;
|
||||
BoundingBox bounds = 6;
|
||||
}
|
||||
|
||||
message BoundingBox {
|
||||
float x = 1;
|
||||
float y = 2;
|
||||
float width = 3;
|
||||
float height = 4;
|
||||
}
|
||||
|
||||
message DebuggerEvent {
|
||||
DebuggerEventType type = 1;
|
||||
string script_id = 2;
|
||||
string url = 3;
|
||||
int32 line = 4;
|
||||
int32 column = 5;
|
||||
repeated StackFrame call_frames = 6;
|
||||
string reason = 7;
|
||||
}
|
||||
|
||||
enum DebuggerEventType {
|
||||
DEBUGGER_EVENT_UNSPECIFIED = 0;
|
||||
DEBUGGER_EVENT_PAUSED = 1;
|
||||
DEBUGGER_EVENT_RESUMED = 2;
|
||||
DEBUGGER_EVENT_SCRIPT_PARSED = 3;
|
||||
DEBUGGER_EVENT_BREAKPOINT_HIT = 4;
|
||||
}
|
||||
|
||||
message StackFrame {
|
||||
string function_name = 1;
|
||||
string script_id = 2;
|
||||
string url = 3;
|
||||
int32 line = 4;
|
||||
int32 column = 5;
|
||||
repeated Scope scopes = 6;
|
||||
}
|
||||
|
||||
message Scope {
|
||||
string type = 1; // global, local, closure, etc.
|
||||
map<string, string> variables = 2;
|
||||
}
|
||||
|
||||
message PageInfo {
|
||||
string page_id = 1;
|
||||
string context_id = 2;
|
||||
string url = 3;
|
||||
string title = 4;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// IDE / Editor Integration
|
||||
// ============================================================================
|
||||
|
||||
message IdeParams {
|
||||
IdeAction action = 1;
|
||||
string workspace_path = 2;
|
||||
string file_path = 3;
|
||||
|
||||
// Position
|
||||
int32 line = 10;
|
||||
int32 column = 11;
|
||||
int32 end_line = 12;
|
||||
int32 end_column = 13;
|
||||
|
||||
// Content
|
||||
string text = 20;
|
||||
string new_name = 21;
|
||||
|
||||
// Diagnostics
|
||||
DiagnosticSeverity min_severity = 30;
|
||||
}
|
||||
|
||||
enum IdeAction {
|
||||
IDE_ACTION_UNSPECIFIED = 0;
|
||||
|
||||
// File operations
|
||||
IDE_ACTION_OPEN_FILE = 1;
|
||||
IDE_ACTION_CLOSE_FILE = 2;
|
||||
IDE_ACTION_SAVE_FILE = 3;
|
||||
IDE_ACTION_GET_ACTIVE_FILE = 4;
|
||||
IDE_ACTION_LIST_OPEN_FILES = 5;
|
||||
|
||||
// Editor operations
|
||||
IDE_ACTION_GET_SELECTION = 10;
|
||||
IDE_ACTION_SET_SELECTION = 11;
|
||||
IDE_ACTION_INSERT_TEXT = 12;
|
||||
IDE_ACTION_REPLACE_TEXT = 13;
|
||||
IDE_ACTION_GET_LINE = 14;
|
||||
IDE_ACTION_GET_RANGE = 15;
|
||||
|
||||
// Navigation
|
||||
IDE_ACTION_GO_TO_DEFINITION = 20;
|
||||
IDE_ACTION_FIND_REFERENCES = 21;
|
||||
IDE_ACTION_GO_TO_LINE = 22;
|
||||
IDE_ACTION_REVEAL_RANGE = 23;
|
||||
|
||||
// Refactoring
|
||||
IDE_ACTION_RENAME = 30;
|
||||
IDE_ACTION_FORMAT_DOCUMENT = 31;
|
||||
IDE_ACTION_ORGANIZE_IMPORTS = 32;
|
||||
IDE_ACTION_EXTRACT_FUNCTION = 33;
|
||||
IDE_ACTION_EXTRACT_VARIABLE = 34;
|
||||
|
||||
// Diagnostics
|
||||
IDE_ACTION_GET_DIAGNOSTICS = 40;
|
||||
IDE_ACTION_QUICK_FIX = 41;
|
||||
|
||||
// Commands
|
||||
IDE_ACTION_EXECUTE_COMMAND = 50;
|
||||
IDE_ACTION_LIST_COMMANDS = 51;
|
||||
|
||||
// Workspace
|
||||
IDE_ACTION_GET_WORKSPACE_FOLDERS = 60;
|
||||
IDE_ACTION_FIND_FILES = 61;
|
||||
IDE_ACTION_SEARCH_TEXT = 62;
|
||||
|
||||
// Terminal
|
||||
IDE_ACTION_CREATE_TERMINAL = 70;
|
||||
IDE_ACTION_SEND_TO_TERMINAL = 71;
|
||||
IDE_ACTION_GET_TERMINAL_OUTPUT = 72;
|
||||
}
|
||||
|
||||
message IdeResult {
|
||||
bool success = 1;
|
||||
|
||||
oneof result {
|
||||
FileInfo file_info = 10;
|
||||
repeated FileInfo files = 11;
|
||||
TextRange selection = 12;
|
||||
string text = 13;
|
||||
repeated Location locations = 14;
|
||||
repeated Diagnostic diagnostics = 15;
|
||||
repeated CodeAction code_actions = 16;
|
||||
repeated string commands = 17;
|
||||
WorkspaceInfo workspace = 18;
|
||||
TerminalInfo terminal = 19;
|
||||
}
|
||||
}
|
||||
|
||||
message FileInfo {
|
||||
string path = 1;
|
||||
string uri = 2;
|
||||
string language_id = 3;
|
||||
bool is_dirty = 4;
|
||||
int32 line_count = 5;
|
||||
}
|
||||
|
||||
message TextRange {
|
||||
int32 start_line = 1;
|
||||
int32 start_column = 2;
|
||||
int32 end_line = 3;
|
||||
int32 end_column = 4;
|
||||
string text = 5;
|
||||
}
|
||||
|
||||
message Location {
|
||||
string uri = 1;
|
||||
TextRange range = 2;
|
||||
}
|
||||
|
||||
message Diagnostic {
|
||||
DiagnosticSeverity severity = 1;
|
||||
TextRange range = 2;
|
||||
string message = 3;
|
||||
string source = 4;
|
||||
string code = 5;
|
||||
}
|
||||
|
||||
enum DiagnosticSeverity {
|
||||
DIAGNOSTIC_SEVERITY_UNSPECIFIED = 0;
|
||||
DIAGNOSTIC_SEVERITY_ERROR = 1;
|
||||
DIAGNOSTIC_SEVERITY_WARNING = 2;
|
||||
DIAGNOSTIC_SEVERITY_INFO = 3;
|
||||
DIAGNOSTIC_SEVERITY_HINT = 4;
|
||||
}
|
||||
|
||||
message CodeAction {
|
||||
string title = 1;
|
||||
string kind = 2; // quickfix, refactor, source, etc.
|
||||
bool is_preferred = 3;
|
||||
repeated TextEdit edits = 4;
|
||||
string command = 5;
|
||||
}
|
||||
|
||||
message TextEdit {
|
||||
TextRange range = 1;
|
||||
string new_text = 2;
|
||||
}
|
||||
|
||||
message WorkspaceInfo {
|
||||
repeated WorkspaceFolder folders = 1;
|
||||
}
|
||||
|
||||
message WorkspaceFolder {
|
||||
string uri = 1;
|
||||
string name = 2;
|
||||
}
|
||||
|
||||
message TerminalInfo {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
int32 pid = 3;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Extension / Plugin Messaging
|
||||
// ============================================================================
|
||||
|
||||
message ExtensionParams {
|
||||
string extension_id = 1;
|
||||
ExtensionAction action = 2;
|
||||
string channel = 3;
|
||||
bytes payload = 4;
|
||||
map<string, string> headers = 5;
|
||||
}
|
||||
|
||||
enum ExtensionAction {
|
||||
EXTENSION_ACTION_UNSPECIFIED = 0;
|
||||
EXTENSION_ACTION_SEND = 1; // Send message to extension
|
||||
EXTENSION_ACTION_BROADCAST = 2; // Broadcast to all extensions
|
||||
EXTENSION_ACTION_SUBSCRIBE = 3; // Subscribe to channel
|
||||
EXTENSION_ACTION_UNSUBSCRIBE = 4;
|
||||
EXTENSION_ACTION_LIST = 5; // List connected extensions
|
||||
EXTENSION_ACTION_PING = 6; // Health check
|
||||
}
|
||||
|
||||
message ExtensionResult {
|
||||
bool success = 1;
|
||||
string extension_id = 2;
|
||||
bytes payload = 3;
|
||||
repeated ExtensionInfo extensions = 4;
|
||||
}
|
||||
|
||||
message ExtensionInfo {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
string version = 3;
|
||||
ExtensionType type = 4;
|
||||
repeated string capabilities = 5;
|
||||
bool connected = 6;
|
||||
}
|
||||
|
||||
enum ExtensionType {
|
||||
EXTENSION_TYPE_UNSPECIFIED = 0;
|
||||
EXTENSION_TYPE_BROWSER = 1; // Browser extension
|
||||
EXTENSION_TYPE_IDE = 2; // IDE plugin (VS Code, JetBrains)
|
||||
EXTENSION_TYPE_TERMINAL = 3; // Terminal integration
|
||||
EXTENSION_TYPE_MOBILE = 4; // Mobile app
|
||||
EXTENSION_TYPE_DESKTOP = 5; // Desktop app
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Streaming
|
||||
// ============================================================================
|
||||
|
||||
message StreamParams {
|
||||
StreamAction action = 1;
|
||||
string stream_id = 2;
|
||||
StreamType type = 3;
|
||||
map<string, string> filters = 4;
|
||||
}
|
||||
|
||||
enum StreamAction {
|
||||
STREAM_ACTION_UNSPECIFIED = 0;
|
||||
STREAM_ACTION_START = 1;
|
||||
STREAM_ACTION_STOP = 2;
|
||||
STREAM_ACTION_PAUSE = 3;
|
||||
STREAM_ACTION_RESUME = 4;
|
||||
}
|
||||
|
||||
enum StreamType {
|
||||
STREAM_TYPE_UNSPECIFIED = 0;
|
||||
STREAM_TYPE_REPL_OUTPUT = 1;
|
||||
STREAM_TYPE_CONSOLE_MESSAGES = 2;
|
||||
STREAM_TYPE_NETWORK_EVENTS = 3;
|
||||
STREAM_TYPE_DIAGNOSTICS = 4;
|
||||
STREAM_TYPE_FILE_CHANGES = 5;
|
||||
STREAM_TYPE_DEBUGGER = 6;
|
||||
}
|
||||
|
||||
message StreamChunk {
|
||||
string stream_id = 1;
|
||||
int64 sequence = 2;
|
||||
int64 timestamp = 3;
|
||||
|
||||
oneof data {
|
||||
ReplOutput repl_output = 10;
|
||||
ConsoleMessage console_message = 11;
|
||||
NetworkEvent network_event = 12;
|
||||
Diagnostic diagnostic = 13;
|
||||
FileChangeEvent file_change = 14;
|
||||
DebuggerEvent debugger_event = 15;
|
||||
bytes raw = 99;
|
||||
}
|
||||
}
|
||||
|
||||
message FileChangeEvent {
|
||||
FileChangeType type = 1;
|
||||
string uri = 2;
|
||||
}
|
||||
|
||||
enum FileChangeType {
|
||||
FILE_CHANGE_UNSPECIFIED = 0;
|
||||
FILE_CHANGE_CREATED = 1;
|
||||
FILE_CHANGE_CHANGED = 2;
|
||||
FILE_CHANGE_DELETED = 3;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Service Definition
|
||||
// ============================================================================
|
||||
|
||||
service ZapService {
|
||||
// Unary RPCs (MCP compatible)
|
||||
rpc Call(ZapRequest) returns (ZapResponse);
|
||||
|
||||
// Server streaming (real-time output)
|
||||
rpc Stream(StreamParams) returns (stream StreamChunk);
|
||||
|
||||
// Bidirectional streaming (interactive sessions)
|
||||
rpc Interactive(stream ZapRequest) returns (stream ZapResponse);
|
||||
}
|
||||
Reference in New Issue
Block a user