New dev, same as old dev

This commit is contained in:
Hanzo Dev
2025-07-05 17:48:06 -04:00
parent 3bd618ade6
commit 6e261ded83
20 changed files with 4618 additions and 61 deletions
+104
View File
@@ -0,0 +1,104 @@
.PHONY: help setup dev build test clean install-local login
# Colors
GREEN := $(shell tput -Txterm setaf 2)
YELLOW := $(shell tput -Txterm setaf 3)
WHITE := $(shell tput -Txterm setaf 7)
CYAN := $(shell tput -Txterm setaf 6)
RESET := $(shell tput -Txterm sgr0)
# Default target
help:
@echo ""
@echo "${GREEN}Hanzo Dev - Local Development${RESET}"
@echo ""
@echo "${YELLOW}Available commands:${RESET}"
@echo " ${CYAN}make setup${RESET} - Set up for local development"
@echo " ${CYAN}make dev${RESET} - Start development mode"
@echo " ${CYAN}make build${RESET} - Build all packages"
@echo " ${CYAN}make install-local${RESET} - Install hanzo-dev locally"
@echo " ${CYAN}make login${RESET} - Login to Hanzo AI"
@echo " ${CYAN}make test${RESET} - Run tests"
@echo " ${CYAN}make clean${RESET} - Clean build artifacts"
@echo ""
# Setup for local development
setup:
@echo "${GREEN}Setting up Hanzo Dev...${RESET}"
@npm install
@make build
@make install-local
@echo "${GREEN}✓ Setup complete!${RESET}"
@echo ""
@echo "${YELLOW}Next steps:${RESET}"
@echo "1. Run: ${CYAN}make login${RESET}"
@echo "2. Run: ${CYAN}hanzo-dev init${RESET}"
@echo ""
# Development mode
dev:
@echo "${GREEN}Starting development mode...${RESET}"
@npm run watch &
@cd packages/dev && npm run dev &
@cd packages/mcp && npm run dev &
@echo "${GREEN}Development servers started!${RESET}"
@echo "Press Ctrl+C to stop"
@wait
# Build all packages
build:
@echo "${GREEN}Building VS Code extension...${RESET}"
@npm run compile
@echo "${GREEN}Building CLI (@hanzo/dev)...${RESET}"
@cd packages/dev && npm install && npm run build
@echo "${GREEN}Building MCP (@hanzo/mcp)...${RESET}"
@cd packages/mcp && npm install && npm run build
@echo "${GREEN}✓ Build complete!${RESET}"
# Install hanzo-dev command locally
install-local:
@echo "${GREEN}Installing hanzo-dev locally...${RESET}"
@cd packages/dev && npm link
@echo "${GREEN}✓ hanzo-dev installed!${RESET}"
@echo "Run '${CYAN}hanzo-dev --help${RESET}' to get started"
# Login to Hanzo AI
login:
@echo "${GREEN}Logging in to Hanzo AI...${RESET}"
@hanzo-dev login
# Run tests
test:
@echo "${GREEN}Running tests...${RESET}"
@npm test
@cd packages/dev && npm test
@cd packages/mcp && npm test
# Clean build artifacts
clean:
@echo "${YELLOW}Cleaning build artifacts...${RESET}"
@rm -rf out dist
@rm -rf packages/dev/dist
@rm -rf packages/mcp/dist
@rm -rf node_modules
@rm -rf packages/dev/node_modules
@rm -rf packages/mcp/node_modules
@echo "${GREEN}✓ Clean complete!${RESET}"
# Quick commands for development
run-claude:
@hanzo-dev run claude "$(TASK)"
run-aider:
@hanzo-dev run aider "$(TASK)" --auto-commit
run-openhands:
@hanzo-dev run openhands "$(TASK)" --worktree
compare:
@hanzo-dev compare "$(TASK)"
# Example usage:
# make run-claude TASK="implement a REST API"
# make run-aider TASK="fix the failing tests"
# make compare TASK="optimize this database query"
+133 -57
View File
@@ -1,9 +1,9 @@
# Hanzo AI
# Dev - Meta AI Development Platform 🚀
[![VS Code Extension CI/CD](https://github.com/hanzoai/extension/workflows/VS%20Code%20Extension%20CI%2FCD/badge.svg)](https://github.com/hanzoai/extension/actions/workflows/vscode-extension.yml)
[![JetBrains Plugin CI/CD](https://github.com/hanzoai/extension/workflows/JetBrains%20Plugin%20CI%2FCD/badge.svg)](https://github.com/hanzoai/extension/actions/workflows/jetbrains-plugin.yml)
[![VS Code Extension CI/CD](https://github.com/hanzoai/dev/workflows/VS%20Code%20Extension%20CI%2FCD/badge.svg)](https://github.com/hanzoai/dev/actions/workflows/vscode-extension.yml)
[![JetBrains Plugin CI/CD](https://github.com/hanzoai/dev/workflows/JetBrains%20Plugin%20CI%2FCD/badge.svg)](https://github.com/hanzoai/dev/actions/workflows/jetbrains-plugin.yml)
The ultimate toolkit for AI engineers.
The ultimate meta AI development platform. Manage and run ALL AI tools (Claude, Codex, Gemini, OpenHands, Aider) in one unified interface with authentication, API management, and parallel execution.
## What You Get
@@ -16,76 +16,144 @@ The ultimate toolkit for AI engineers.
- **Browser Automation** - Built-in Playwright for web tasks
- **Team Collaboration** - Shared context and credits
## Quick Start
## 🚀 Quick Start - Get Running in 2 Minutes
```bash
# VS Code / Cursor / Windsurf
Install hanzoai-*.vsix
# Clone and setup
git clone https://github.com/hanzoai/dev.git
cd dev
make setup
# Claude Code
Drag hanzoai-*.dxt
# Login to Hanzo AI (opens browser)
make login
# OR use existing API key
export HANZO_API_KEY=hzo_... # from iam.hanzo.ai
# JetBrains IDEs (IntelliJ, PyCharm, WebStorm, etc.)
Install hanzo-ai-plugin.zip via Settings → Plugins → Install from Disk
# You're ready! Run any AI tool:
dev run claude "implement a REST API"
dev run aider "fix the failing tests" --auto-commit
dev run openhands "analyze this codebase" --worktree
```
# Terminal / Neovim
## 📦 Installation Options
### Option 1: CLI Tool (@hanzo/dev) - Recommended for Quick Start
```bash
# Install globally
npm install -g @hanzo/dev
# Login with your Hanzo account
dev login
# Initialize in your project
dev init
```
### Option 2: VS Code Extension (Hanzo AI)
```bash
# Install from marketplace
code --install-extension hanzo-ai.hanzo-ai
# Or install .vsix locally
code --install-extension hanzo-ai-*.vsix
```
### Option 3: MCP Server (@hanzo/mcp)
```bash
# For Claude Desktop
npx @hanzo/mcp@latest
```
## Use It
## 🎯 Core Features
### 🔐 Unified Authentication & API Management
```bash
# Login to Hanzo AI
@hanzo login # Opens iam.hanzo.ai in browser
# Login once, use everywhere
dev login
# Or set API key directly
export HANZO_API_KEY=hzo_... # from iam.hanzo.ai
# Talk to any model
@hanzo agent --model o3-pro solve this algorithm
@hanzo agent --model claude-4 review my code
# Activate legendary modes
@hanzo mode carmack # Optimize like a game engine
@hanzo mode norvig # AI implementation mastery
# Control browsers
@hanzo browser navigate https://example.com
@hanzo browser screenshot
# Search everything
@hanzo search "auth flow"
# Symbol search across projects
@hanzo symbols "class UserController"
@hanzo symbols "function authenticate"
# Install any MCP server
@hanzo mcp --action install --package @modelcontextprotocol/server-github
@hanzo mcp --action call --tool github_search --args '{"query": "MCP"}'
# All your API keys are synced and encrypted locally
# - OpenAI/Codex API keys
# - Anthropic/Claude API keys
# - Google/Gemini API keys
# - Auto-passthrough to all tools
```
## Development
### VS Code Extension
### 🤖 Run Any AI Tool
```bash
# Claude - Advanced reasoning and coding
dev run claude "refactor this authentication system"
# Aider - Git-aware pair programming
dev run aider "add test coverage" --auto-commit
# OpenHands - Autonomous software engineering
dev run openhands "implement user management" --worktree
# Codex - Code generation
dev run codex "generate a REST API client"
# Gemini - Multimodal AI
dev run gemini "analyze this architecture diagram"
```
### ⚡ Async Long-Running Tasks
```bash
# Start task in background (auto-quits after 5min idle)
dev run claude "migrate database to PostgreSQL" --async
# Output: Job ID: abc123...
# Check status
dev status abc123
# Keep alive and send more instructions
dev input abc123 "also add connection pooling"
```
### 🌳 Parallel Development with Git Worktrees
```bash
# Spawn multiple AI agents working in parallel
dev run claude "implement auth" --worktree
dev run aider "add tests" --worktree
dev run openhands "write docs" --worktree
# Each runs in its own branch and directory
dev worktree list
```
### 🔄 Compare AI Tools
```bash
# See how different AIs approach the same problem
dev compare "optimize this database query"
# Output shows results from all tools side-by-side
```
## 🛠️ Local Development
```bash
# Quick setup with Make
make setup # Install everything
make dev # Start dev mode
make test # Run tests
# Manual setup
npm install
npm run compile
npm test
vsce package # Build VSIX
cd packages/dev && npm install && npm link
cd packages/mcp && npm install
# Run locally
dev --help
```
### JetBrains Plugin
### Build from Source
```bash
cd jetbrains-plugin
./gradlew build
# Or with Docker:
./build-plugin-simple.sh
```
# Build everything
make build
### Claude Code Extension
```bash
npm run build:dxt
# Or individually:
npm run compile # VS Code extension
cd packages/dev && npm run build # CLI tool
cd packages/mcp && npm run build # MCP server
```
## CI/CD
@@ -95,10 +163,18 @@ All extensions are automatically built and tested on push:
- JetBrains plugin tests run on Java 17
- Releases are created automatically for tagged versions
## Links
## 🏗️ Architecture
- **Dev CLI** (`@hanzo/dev`) - Command-line interface for all AI tools
- **Hanzo AI Extension** - VS Code/JetBrains integration
- **MCP Server** (`@hanzo/mcp`) - Model Context Protocol tools
- **Platform Sync** - Universal context and bi-directional file sync
- **Async Wrapper** - Long-running task management with idle detection
## 🔗 Links
🚀 **[Login to Hanzo AI](https://iam.hanzo.ai)** | 🌐 **[Hanzo AI](https://hanzo.ai)** | 📖 **[Docs](https://docs.hanzo.ai)** | 💬 **[Discord](https://discord.gg/hanzoai)**
---
Built for engineers who ship.
Built with ❤️ for engineers who ship fast.
+4 -4
View File
@@ -1,14 +1,14 @@
{
"name": "hanzoai",
"displayName": "Hanzo AI Context Manager",
"description": "The ultimate toolkit for AI engineers. Access 200+ LLMs (OpenRouter/LiteLLM compatible), 4000+ MCP servers, and powerful development tools. Features unified API, shared context, intelligent routing, and VS Code native integration.",
"name": "hanzo-ai",
"displayName": "Hanzo AI",
"description": "The ultimate meta AI development platform. Manage and run all LLMs and CLI tools (Claude, Codex, Gemini, OpenHands, Aider) in one unified interface. Features MCP integration, async execution, git worktrees, and universal context sync.",
"version": "1.5.4",
"publisher": "hanzo-ai",
"private": false,
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/hanzoai/extension.git"
"url": "https://github.com/hanzoai/dev.git"
},
"engines": {
"vscode": "^1.85.0"
+58
View File
@@ -0,0 +1,58 @@
{
"name": "@hanzo/dev",
"version": "1.0.0",
"description": "Hanzo Dev - Meta AI development CLI that manages and runs all LLMs and CLI tools",
"main": "dist/index.js",
"bin": {
"dev": "./dist/cli/dev.js",
"hanzo-dev": "./dist/cli/dev.js"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "jest",
"prepublishOnly": "npm run build"
},
"keywords": [
"ai",
"llm",
"cli",
"claude",
"openai",
"gemini",
"aider",
"openhands",
"development",
"tools"
],
"author": "Hanzo AI",
"license": "MIT",
"dependencies": {
"chalk": "^5.3.0",
"commander": "^11.1.0",
"inquirer": "^9.2.12",
"ora": "^7.0.1",
"uuid": "^9.0.1",
"ws": "^8.16.0"
},
"devDependencies": {
"@types/inquirer": "^9.0.7",
"@types/node": "^20.10.5",
"@types/uuid": "^9.0.7",
"@types/ws": "^8.5.10",
"jest": "^29.7.0",
"typescript": "^5.3.3"
},
"engines": {
"node": ">=16.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/hanzoai/dev.git",
"directory": "packages/dev"
},
"homepage": "https://hanzo.ai",
"bugs": {
"url": "https://github.com/hanzoai/dev/issues"
}
}
+37
View File
@@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "../../src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": false,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"../../src/cli/**/*",
"../../src/cli-tools/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
]
}
+46
View File
@@ -0,0 +1,46 @@
{
"name": "@hanzo/mcp",
"version": "1.0.0",
"description": "Hanzo MCP Server - Model Context Protocol tools for AI development",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "jest",
"prepublishOnly": "npm run build"
},
"keywords": [
"mcp",
"model-context-protocol",
"ai",
"claude",
"tools",
"development"
],
"author": "Hanzo AI",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^0.6.2",
"glob": "^10.3.10",
"minimatch": "^9.0.3"
},
"devDependencies": {
"@types/glob": "^8.1.0",
"@types/minimatch": "^5.1.2",
"@types/node": "^20.10.5",
"jest": "^29.7.0",
"typescript": "^5.3.3"
},
"engines": {
"node": ">=16.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/hanzoai/dev.git",
"directory": "packages/mcp"
},
"homepage": "https://hanzo.ai",
"bugs": {
"url": "https://github.com/hanzoai/dev/issues"
}
}
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
# Hanzo Dev Local Setup Script
set -e
echo "🚀 Setting up Hanzo Dev for local development..."
# Colors
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Check Node.js
if ! command -v node &> /dev/null; then
echo -e "${RED}❌ Node.js is required but not installed.${NC}"
exit 1
fi
echo -e "${GREEN}✓ Node.js found: $(node --version)${NC}"
# Install dependencies
echo -e "\n${BLUE}📦 Installing dependencies...${NC}"
npm install
# Build the project
echo -e "\n${BLUE}🔨 Building Hanzo Dev...${NC}"
# Build VS Code extension
echo "Building VS Code extension..."
npm run compile
# Build CLI tool
echo "Building CLI tool..."
cd packages/dev
npm install
npm run build
cd ../..
# Build MCP server
echo "Building MCP server..."
cd packages/mcp
npm install
npm run build
cd ../..
# Create symlink for local development
echo -e "\n${BLUE}🔗 Creating local development symlink...${NC}"
npm link packages/dev
# Create hanzo-dev command
echo -e "\n${BLUE}🎯 Setting up hanzo-dev command...${NC}"
# Create a wrapper script
cat > /tmp/hanzo-dev-wrapper.js << 'EOF'
#!/usr/bin/env node
require('./packages/dev/dist/cli/hanzo-dev.js');
EOF
chmod +x /tmp/hanzo-dev-wrapper.js
sudo ln -sf "$(pwd)/tmp/hanzo-dev-wrapper.js" /usr/local/bin/hanzo-dev
echo -e "\n${GREEN}✅ Setup complete!${NC}"
echo -e "\n${YELLOW}Quick Start:${NC}"
echo "1. Login to Hanzo AI:"
echo -e " ${BLUE}hanzo-dev login${NC}"
echo ""
echo "2. Initialize in your project:"
echo -e " ${BLUE}hanzo-dev init${NC}"
echo ""
echo "3. Run any AI tool:"
echo -e " ${BLUE}hanzo-dev run claude "your task"${NC}"
echo -e " ${BLUE}hanzo-dev run aider "fix the tests" --auto-commit${NC}"
echo -e " ${BLUE}hanzo-dev run openhands "analyze codebase" --worktree${NC}"
echo ""
echo "4. Compare tools:"
echo -e " ${BLUE}hanzo-dev compare "optimize this function"${NC}"
echo ""
echo "5. Run in background:"
echo -e " ${BLUE}hanzo-dev run claude "big refactoring task" --async${NC}"
echo ""
echo -e "${GREEN}🎉 Happy coding with Hanzo Dev!${NC}"
+273
View File
@@ -0,0 +1,273 @@
import { BaseCLITool, CLIToolConfig } from '../common/base-cli';
import * as vscode from 'vscode';
import { execSync } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
export class AiderCLI extends BaseCLITool {
constructor() {
super({
name: 'Aider CLI',
command: 'aider',
args: [],
env: {},
model: 'gpt-4-turbo-preview',
apiKeyEnvVar: 'OPENAI_API_KEY',
maxTokens: 4096,
temperature: 0.7
});
}
async install(): Promise<boolean> {
try {
// Install via pip
const hasPip = await this.commandExists('pip3');
if (!hasPip) {
throw new Error('Python pip is required to install Aider');
}
await this.runCommand('pip3', ['install', 'aider-chat']);
// Set up git if needed
const hasGit = await this.commandExists('git');
if (!hasGit) {
console.warn('Git is recommended for Aider to work optimally');
}
return true;
} catch (error) {
console.error('Failed to install Aider:', error);
return false;
}
}
async checkInstallation(): Promise<boolean> {
return this.commandExists('aider');
}
generatePrompt(task: string, context?: any): string {
// Aider takes the task as a direct message
return task;
}
protected getApiKey(): string {
const config = vscode.workspace.getConfiguration('hanzo');
const openaiKey = config.get<string>('openaiApiKey');
if (openaiKey) return openaiKey;
const anthropicKey = config.get<string>('anthropicApiKey');
if (anthropicKey && this.config.model?.includes('claude')) {
return anthropicKey;
}
return process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || '';
}
private async commandExists(command: string): Promise<boolean> {
try {
execSync(`which ${command}`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
// Aider-specific methods
async startInteractiveSession(files: string[], options?: any): Promise<string> {
const args = ['--yes']; // Auto-confirm prompts
// Add model configuration
if (options?.model) {
args.push('--model', options.model);
} else if (this.config.model) {
args.push('--model', this.config.model);
}
// Add files to edit
files.forEach(file => args.push(file));
// Additional options
if (options?.readOnly) {
args.push('--read-only');
}
if (options?.autoCommit !== false) {
args.push('--auto-commits');
}
if (options?.noPretty) {
args.push('--no-pretty');
}
if (options?.showDiffs !== false) {
args.push('--show-diffs');
}
const result = await this.execute('', {
env: {
...process.env,
AIDER_ARGS: args.join(' ')
}
});
return result.output || '';
}
async editFiles(files: string[], instruction: string, options?: any): Promise<string> {
const args = [
'--yes',
'--message', instruction
];
// Add model
if (options?.model || this.config.model) {
args.push('--model', options?.model || this.config.model);
}
// Add files
files.forEach(file => args.push(file));
// Auto-commit by default
if (options?.autoCommit !== false) {
args.push('--auto-commits');
}
const result = await this.execute('', {
env: {
...process.env,
AIDER_COMMAND: instruction,
AIDER_ARGS: args.join(' ')
}
});
return result.output || '';
}
async addFiles(files: string[], readOnly: boolean = false): Promise<string> {
const args = ['--add'];
if (readOnly) {
args.push('--read-only');
}
files.forEach(file => args.push(file));
const result = await this.execute('', {
env: {
...process.env,
AIDER_ARGS: args.join(' ')
}
});
return result.output || '';
}
async runWithGitRepo(task: string, repoPath: string, options?: any): Promise<string> {
const args = [
'--yes',
'--message', task
];
if (options?.model) {
args.push('--model', options.model);
}
if (options?.darkMode) {
args.push('--dark-mode');
}
if (options?.stream !== false) {
args.push('--stream');
}
const result = await this.execute('', {
cwd: repoPath,
env: {
...process.env,
AIDER_ARGS: args.join(' ')
}
});
return result.output || '';
}
async askQuestion(question: string, context?: string[]): Promise<string> {
const args = [
'--yes',
'--message', question,
'--no-auto-commits' // Don't commit for questions
];
// Add context files as read-only
if (context && context.length > 0) {
args.push('--read-only');
context.forEach(file => args.push(file));
}
const result = await this.execute('', {
env: {
...process.env,
AIDER_ARGS: args.join(' ')
}
});
return result.output || '';
}
async useAlternativeModel(model: string, task: string, files?: string[]): Promise<string> {
const args = [
'--yes',
'--model', model,
'--message', task
];
if (files) {
files.forEach(file => args.push(file));
}
const result = await this.execute('', {
env: {
...process.env,
AIDER_ARGS: args.join(' ')
}
});
return result.output || '';
}
async showCostEstimate(files: string[]): Promise<string> {
const args = ['--show-cost', '--dry-run'];
files.forEach(file => args.push(file));
const result = await this.execute('', {
env: {
...process.env,
AIDER_ARGS: args.join(' ')
}
});
return result.output || '';
}
async applyChangesFromUrl(url: string, files?: string[]): Promise<string> {
const task = `Apply the changes described in this URL: ${url}`;
const args = [
'--yes',
'--message', task
];
if (files) {
files.forEach(file => args.push(file));
}
const result = await this.execute('', {
env: {
...process.env,
AIDER_ARGS: args.join(' ')
}
});
return result.output || '';
}
}
+478
View File
@@ -0,0 +1,478 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { execSync, spawn } from 'child_process';
import { EventEmitter } from 'events';
import * as http from 'http';
import * as crypto from 'crypto';
export interface HanzoAuthConfig {
apiUrl: string;
iamUrl: string;
clientId: string;
scope: string;
configPath?: string;
}
export interface HanzoCredentials {
apiKey?: string;
accessToken?: string;
refreshToken?: string;
expiresAt?: number;
userId?: string;
email?: string;
settings?: Record<string, any>;
}
export interface APIKeyInfo {
name: string;
key: string;
provider: string;
enabled: boolean;
masked?: string;
}
export class HanzoAuth extends EventEmitter {
private config: HanzoAuthConfig;
private credentialsPath: string;
private settingsPath: string;
private credentials: HanzoCredentials | null = null;
private server?: http.Server;
constructor(config: Partial<HanzoAuthConfig> = {}) {
super();
this.config = {
apiUrl: config.apiUrl || 'https://api.hanzo.ai',
iamUrl: config.iamUrl || 'https://iam.hanzo.ai',
clientId: config.clientId || 'hanzo-dev-cli',
scope: config.scope || 'api:access tools:manage',
configPath: config.configPath || path.join(os.homedir(), '.hanzo')
};
// Ensure config directory exists
fs.mkdirSync(this.config.configPath!, { recursive: true });
this.credentialsPath = path.join(this.config.configPath!, 'credentials.json');
this.settingsPath = path.join(this.config.configPath!, 'settings.json');
// Load existing credentials
this.loadCredentials();
}
private loadCredentials(): void {
try {
if (fs.existsSync(this.credentialsPath)) {
const data = fs.readFileSync(this.credentialsPath, 'utf-8');
this.credentials = JSON.parse(data);
// Check if token is expired
if (this.credentials?.expiresAt && this.credentials.expiresAt < Date.now()) {
this.emit('token:expired');
}
}
} catch (error) {
console.error('Failed to load credentials:', error);
}
}
private saveCredentials(): void {
try {
fs.writeFileSync(
this.credentialsPath,
JSON.stringify(this.credentials, null, 2),
{ mode: 0o600 } // Secure file permissions
);
} catch (error) {
console.error('Failed to save credentials:', error);
}
}
async login(): Promise<boolean> {
return new Promise((resolve, reject) => {
// Generate PKCE challenge
const codeVerifier = this.generateCodeVerifier();
const codeChallenge = this.generateCodeChallenge(codeVerifier);
const state = crypto.randomBytes(16).toString('hex');
// Start local server for OAuth callback
const port = 51234;
this.server = http.createServer(async (req, res) => {
const url = new URL(req.url!, `http://localhost:${port}`);
if (url.pathname === '/callback') {
const code = url.searchParams.get('code');
const returnedState = url.searchParams.get('state');
if (returnedState !== state) {
res.writeHead(400);
res.end('Invalid state parameter');
this.server?.close();
reject(new Error('Invalid state parameter'));
return;
}
if (code) {
// Exchange code for tokens
try {
await this.exchangeCodeForTokens(code, codeVerifier);
// Success response
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<html>
<head>
<title>Hanzo Dev - Login Successful</title>
<style>
body { font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #1a1a1a; color: white; }
.container { text-align: center; }
.success { color: #4ade80; font-size: 48px; margin-bottom: 20px; }
</style>
</head>
<body>
<div class="container">
<div class="success">✓</div>
<h1>Login Successful!</h1>
<p>You can now close this window and return to your terminal.</p>
</div>
<script>setTimeout(() => window.close(), 3000);</script>
</body>
</html>
`);
this.server?.close();
resolve(true);
} catch (error) {
res.writeHead(500);
res.end('Failed to exchange code for tokens');
this.server?.close();
reject(error);
}
} else {
res.writeHead(400);
res.end('No authorization code received');
this.server?.close();
reject(new Error('No authorization code received'));
}
}
});
this.server.listen(port, () => {
// Build authorization URL
const authUrl = new URL('/oauth/authorize', this.config.iamUrl);
authUrl.searchParams.set('client_id', this.config.clientId);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('redirect_uri', `http://localhost:${port}/callback`);
authUrl.searchParams.set('scope', this.config.scope);
authUrl.searchParams.set('state', state);
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
// Open browser
this.openBrowser(authUrl.toString());
console.log('Opening browser for authentication...');
console.log('If browser doesn\'t open, visit:', authUrl.toString());
});
// Timeout after 5 minutes
setTimeout(() => {
if (this.server) {
this.server.close();
reject(new Error('Login timeout'));
}
}, 5 * 60 * 1000);
});
}
private async exchangeCodeForTokens(code: string, codeVerifier: string): Promise<void> {
const response = await fetch(`${this.config.iamUrl}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
grant_type: 'authorization_code',
code,
client_id: this.config.clientId,
code_verifier: codeVerifier,
redirect_uri: 'http://localhost:51234/callback'
})
});
if (!response.ok) {
throw new Error(`Failed to exchange code: ${response.statusText}`);
}
const data = await response.json();
this.credentials = {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresAt: Date.now() + (data.expires_in * 1000),
...this.credentials // Preserve existing settings
};
// Fetch user info
await this.fetchUserInfo();
// Fetch and sync API keys
await this.syncAPIKeys();
this.saveCredentials();
this.emit('login:success', this.credentials);
}
private async fetchUserInfo(): Promise<void> {
if (!this.credentials?.accessToken) return;
const response = await fetch(`${this.config.iamUrl}/api/user`, {
headers: {
'Authorization': `Bearer ${this.credentials.accessToken}`
}
});
if (response.ok) {
const user = await response.json();
this.credentials.userId = user.id;
this.credentials.email = user.email;
}
}
async syncAPIKeys(): Promise<APIKeyInfo[]> {
if (!this.credentials?.accessToken) {
throw new Error('Not authenticated');
}
const response = await fetch(`${this.config.apiUrl}/v1/api-keys`, {
headers: {
'Authorization': `Bearer ${this.credentials.accessToken}`
}
});
if (!response.ok) {
throw new Error(`Failed to fetch API keys: ${response.statusText}`);
}
const apiKeys = await response.json();
// Store API keys in settings (encrypted)
const settings = this.loadSettings();
settings.apiKeys = apiKeys.map((key: any) => ({
name: key.name,
provider: key.provider,
enabled: key.enabled,
// Store encrypted version locally
encryptedKey: this.encryptData(key.key),
masked: key.key.substring(0, 8) + '...' + key.key.substring(key.key.length - 4)
}));
this.saveSettings(settings);
this.emit('apikeys:synced', apiKeys.length);
return apiKeys;
}
getAPIKey(provider: string): string | null {
const settings = this.loadSettings();
const keyInfo = settings.apiKeys?.find((k: any) =>
k.provider === provider && k.enabled
);
if (!keyInfo?.encryptedKey) {
// Try environment variable as fallback
const envKey = `${provider.toUpperCase()}_API_KEY`;
return process.env[envKey] || null;
}
return this.decryptData(keyInfo.encryptedKey);
}
async refreshToken(): Promise<boolean> {
if (!this.credentials?.refreshToken) {
return false;
}
try {
const response = await fetch(`${this.config.iamUrl}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: this.credentials.refreshToken,
client_id: this.config.clientId
})
});
if (!response.ok) {
throw new Error(`Failed to refresh token: ${response.statusText}`);
}
const data = await response.json();
this.credentials = {
...this.credentials,
accessToken: data.access_token,
refreshToken: data.refresh_token || this.credentials.refreshToken,
expiresAt: Date.now() + (data.expires_in * 1000)
};
this.saveCredentials();
this.emit('token:refreshed');
return true;
} catch (error) {
this.emit('token:refresh:failed', error);
return false;
}
}
async logout(): Promise<void> {
if (this.credentials?.accessToken) {
try {
await fetch(`${this.config.iamUrl}/oauth/revoke`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.credentials.accessToken}`
},
body: JSON.stringify({
token: this.credentials.refreshToken || this.credentials.accessToken
})
});
} catch (error) {
console.error('Failed to revoke token:', error);
}
}
// Clear credentials but keep settings
this.credentials = null;
if (fs.existsSync(this.credentialsPath)) {
fs.unlinkSync(this.credentialsPath);
}
this.emit('logout');
}
isAuthenticated(): boolean {
if (!this.credentials?.accessToken) {
return false;
}
// Check if token is expired
if (this.credentials.expiresAt && this.credentials.expiresAt < Date.now()) {
return false;
}
return true;
}
getCredentials(): HanzoCredentials | null {
return this.credentials;
}
async makeAuthenticatedRequest(url: string, options: RequestInit = {}): Promise<Response> {
if (!this.isAuthenticated()) {
// Try to refresh token
if (this.credentials?.refreshToken) {
const refreshed = await this.refreshToken();
if (!refreshed) {
throw new Error('Not authenticated');
}
} else {
throw new Error('Not authenticated');
}
}
const headers = {
...options.headers,
'Authorization': `Bearer ${this.credentials!.accessToken}`
};
return fetch(url, { ...options, headers });
}
private loadSettings(): Record<string, any> {
try {
if (fs.existsSync(this.settingsPath)) {
return JSON.parse(fs.readFileSync(this.settingsPath, 'utf-8'));
}
} catch (error) {
console.error('Failed to load settings:', error);
}
return {};
}
private saveSettings(settings: Record<string, any>): void {
try {
fs.writeFileSync(
this.settingsPath,
JSON.stringify(settings, null, 2),
{ mode: 0o600 }
);
} catch (error) {
console.error('Failed to save settings:', error);
}
}
private encryptData(data: string): string {
// Use machine ID as encryption key
const key = crypto.scryptSync(this.getMachineId(), 'salt', 32);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
let encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
private decryptData(encryptedData: string): string {
const [ivHex, encrypted] = encryptedData.split(':');
const key = crypto.scryptSync(this.getMachineId(), 'salt', 32);
const iv = Buffer.from(ivHex, 'hex');
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
private getMachineId(): string {
// Simple machine ID based on hostname and platform
return crypto.createHash('sha256')
.update(os.hostname())
.update(os.platform())
.update(os.homedir())
.digest('hex')
.substring(0, 32);
}
private generateCodeVerifier(): string {
return crypto.randomBytes(32).toString('base64url');
}
private generateCodeChallenge(verifier: string): string {
return crypto.createHash('sha256')
.update(verifier)
.digest('base64url');
}
private openBrowser(url: string): void {
const platform = os.platform();
try {
if (platform === 'darwin') {
spawn('open', [url], { detached: true });
} else if (platform === 'win32') {
spawn('start', ['', url], { shell: true, detached: true });
} else {
spawn('xdg-open', [url], { detached: true });
}
} catch (error) {
console.error('Failed to open browser:', error);
}
}
}
+107
View File
@@ -0,0 +1,107 @@
import { BaseCLITool, CLIToolConfig } from '../common/base-cli';
import * as vscode from 'vscode';
import { execSync } from 'child_process';
export class ClaudeCLI extends BaseCLITool {
constructor() {
super({
name: 'Claude CLI',
command: 'claude',
args: [],
env: {},
model: 'claude-3-opus-20240229',
apiKeyEnvVar: 'ANTHROPIC_API_KEY',
maxTokens: 4096,
temperature: 0.7
});
}
async install(): Promise<boolean> {
try {
// Check if pip/pipx is available
const hasPipx = await this.commandExists('pipx');
const hasPip = await this.commandExists('pip');
if (hasPipx) {
await this.runCommand('pipx', ['install', 'anthropic']);
} else if (hasPip) {
await this.runCommand('pip', ['install', '--user', 'anthropic']);
} else {
throw new Error('Neither pip nor pipx found. Please install Python first.');
}
return true;
} catch (error) {
console.error('Failed to install Claude CLI:', error);
return false;
}
}
async checkInstallation(): Promise<boolean> {
return this.commandExists('claude');
}
generatePrompt(task: string, context?: any): string {
const systemPrompt = context?.systemPrompt || 'You are a helpful AI assistant.';
const prompt = `
System: ${systemPrompt}
Task: ${task}
Please provide a detailed response.
`;
return prompt;
}
protected getApiKey(): string {
// First check VS Code settings
const config = vscode.workspace.getConfiguration('hanzo');
const anthropicKey = config.get<string>('anthropicApiKey');
if (anthropicKey) return anthropicKey;
// Fall back to environment variable
return process.env.ANTHROPIC_API_KEY || '';
}
private async commandExists(command: string): Promise<boolean> {
try {
execSync(`which ${command}`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
// Claude-specific methods
async chat(message: string, conversationId?: string): Promise<string> {
const args = ['chat'];
if (conversationId) {
args.push('--conversation', conversationId);
}
args.push('--message', message);
const result = await this.execute('', {
env: { ...process.env, CLAUDE_ARGS: args.join(' ') }
});
return result.output || '';
}
async completeCode(code: string, language: string): Promise<string> {
const prompt = `Complete this ${language} code:\n\n${code}`;
const result = await this.execute(prompt);
return result.output || '';
}
async reviewCode(code: string, language: string): Promise<string> {
const prompt = `Review this ${language} code for bugs, security issues, and improvements:\n\n${code}`;
const result = await this.execute(prompt);
return result.output || '';
}
async generateTests(code: string, language: string, framework?: string): Promise<string> {
const prompt = `Generate comprehensive unit tests for this ${language} code${framework ? ` using ${framework}` : ''}:\n\n${code}`;
const result = await this.execute(prompt);
return result.output || '';
}
}
+383
View File
@@ -0,0 +1,383 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { BaseCLITool } from './common/base-cli';
import { ClaudeCLI } from './claude/claude-cli';
import { CodexCLI } from './codex/codex-cli';
import { GeminiCLI } from './gemini/gemini-cli';
import { OpenHandsCLI } from './openhands/openhands-cli';
import { AiderCLI } from './aider/aider-cli';
import { AsyncToolWrapper, AsyncToolResult } from './platform/async-tool-wrapper';
import { HanzoAuth } from '../auth/hanzo-auth';
export type CLIToolType = 'claude' | 'codex' | 'gemini' | 'openhands' | 'aider';
export interface CLIToolTask {
tool: CLIToolType;
task: string;
context?: any;
directory?: string;
files?: string[];
}
export class CLIToolManager {
private tools: Map<CLIToolType, BaseCLITool> = new Map();
private asyncWrapper: AsyncToolWrapper;
private outputChannel: vscode.OutputChannel;
private initialized: boolean = false;
private asyncJobs: Map<string, { toolType: CLIToolType; task: string }> = new Map();
private auth: HanzoAuth;
constructor(auth?: HanzoAuth) {
this.outputChannel = vscode.window.createOutputChannel('Hanzo Dev Tools');
this.asyncWrapper = new AsyncToolWrapper({
idleTimeout: 5 * 60 * 1000, // 5 minutes
maxRuntime: 30 * 60 * 1000, // 30 minutes
persistResults: true,
resultPath: path.join(process.cwd(), '.hanzo-dev', 'async-results')
});
this.auth = auth || new HanzoAuth();
this.setupAsyncHandlers();
this.setupAPIKeys();
}
async initialize(): Promise<void> {
if (this.initialized) return;
this.outputChannel.appendLine('Initializing CLI tools...');
// Initialize all CLI tools
const claudeCLI = new ClaudeCLI();
const codexCLI = new CodexCLI();
const geminiCLI = new GeminiCLI();
const openhandsCLI = new OpenHandsCLI();
const aiderCLI = new AiderCLI();
// Set up event listeners
[claudeCLI, codexCLI, geminiCLI, openhandsCLI, aiderCLI].forEach(tool => {
tool.on('output', (data) => {
this.outputChannel.append(`[${tool.getConfig().name}] ${data}`);
});
tool.on('error', (error) => {
this.outputChannel.appendLine(`[${tool.getConfig().name}] ERROR: ${error}`);
});
});
// Initialize tools
await Promise.all([
claudeCLI.initialize(),
codexCLI.initialize(),
geminiCLI.initialize(),
openhandsCLI.initialize(),
aiderCLI.initialize()
]);
this.tools.set('claude', claudeCLI);
this.tools.set('codex', codexCLI);
this.tools.set('gemini', geminiCLI);
this.tools.set('openhands', openhandsCLI);
this.tools.set('aider', aiderCLI);
this.initialized = true;
this.outputChannel.appendLine('CLI tools initialized successfully!');
}
private async setupAPIKeys(): Promise<void> {
// Set up API keys from Hanzo Auth
if (this.auth.isAuthenticated()) {
// Claude/Anthropic
const anthropicKey = this.auth.getAPIKey('anthropic');
if (anthropicKey) {
process.env.ANTHROPIC_API_KEY = anthropicKey;
}
// OpenAI/Codex
const openaiKey = this.auth.getAPIKey('openai');
if (openaiKey) {
process.env.OPENAI_API_KEY = openaiKey;
}
// Google/Gemini
const googleKey = this.auth.getAPIKey('google');
if (googleKey) {
process.env.GOOGLE_API_KEY = googleKey;
}
this.outputChannel.appendLine('API keys loaded from Hanzo Auth');
}
}
async executeTool(toolType: CLIToolType, task: string, options?: any): Promise<any> {
await this.initialize();
const tool = this.tools.get(toolType);
if (!tool) {
throw new Error(`Unknown tool type: ${toolType}`);
}
if (!tool.isReady()) {
throw new Error(`${toolType} is not ready. Please check installation.`);
}
this.outputChannel.appendLine(`\n[${toolType}] Executing: ${task}`);
const result = await tool.execute(task, options);
if (result.success) {
this.outputChannel.appendLine(`[${toolType}] Completed successfully in ${result.duration}ms`);
} else {
this.outputChannel.appendLine(`[${toolType}] Failed: ${result.error}`);
}
return result;
}
async executeParallel(tasks: CLIToolTask[]): Promise<Map<string, any>> {
await this.initialize();
const results = new Map<string, any>();
const promises = tasks.map(async (taskConfig, index) => {
const tool = this.tools.get(taskConfig.tool);
if (!tool) {
results.set(`task-${index}`, {
error: `Unknown tool: ${taskConfig.tool}`
});
return;
}
try {
const result = await tool.execute(taskConfig.task, {
cwd: taskConfig.directory
});
results.set(`task-${index}`, result);
} catch (error) {
results.set(`task-${index}`, {
error: error.message
});
}
});
await Promise.all(promises);
return results;
}
async compareResults(task: string, tools: CLIToolType[] = ['claude', 'codex', 'gemini']): Promise<Map<CLIToolType, any>> {
await this.initialize();
const results = new Map<CLIToolType, any>();
this.outputChannel.appendLine(`\n=== Comparing results for: ${task} ===`);
for (const toolType of tools) {
const tool = this.tools.get(toolType);
if (!tool || !tool.isReady()) {
results.set(toolType, {
error: `${toolType} not available`
});
continue;
}
try {
const result = await tool.execute(task);
results.set(toolType, result);
this.outputChannel.appendLine(`\n[${toolType}] Result:\n${result.output}`);
} catch (error) {
results.set(toolType, {
error: error.message
});
}
}
return results;
}
// Specialized methods for common tasks
async generateCode(description: string, language: string, preferredTool?: CLIToolType): Promise<string> {
const tool = preferredTool || 'codex';
const codexCLI = this.tools.get(tool) as CodexCLI;
if (codexCLI && codexCLI.generateCode) {
return codexCLI.generateCode(description, language);
}
// Fallback to general execution
const task = `Generate ${language} code for: ${description}`;
const result = await this.executeTool(tool, task);
return result.output || '';
}
async reviewCode(code: string, language: string, preferredTool?: CLIToolType): Promise<string> {
const tool = preferredTool || 'claude';
const claudeCLI = this.tools.get(tool) as ClaudeCLI;
if (claudeCLI && claudeCLI.reviewCode) {
return claudeCLI.reviewCode(code, language);
}
// Fallback
const task = `Review this ${language} code:\n${code}`;
const result = await this.executeTool(tool, task);
return result.output || '';
}
async debugCode(code: string, error: string, language: string, preferredTool?: CLIToolType): Promise<string> {
const tool = preferredTool || 'gemini';
const geminiCLI = this.tools.get(tool) as GeminiCLI;
if (geminiCLI && geminiCLI.debugCode) {
return geminiCLI.debugCode(code, error, language);
}
// Fallback
const task = `Debug this ${language} code with error: ${error}\n\nCode:\n${code}`;
const result = await this.executeTool(tool, task);
return result.output || '';
}
getAvailableTools(): CLIToolType[] {
const available: CLIToolType[] = [];
for (const [type, tool] of this.tools) {
if (tool.isReady()) {
available.push(type);
}
}
return available;
}
showOutput(): void {
this.outputChannel.show();
}
private setupAsyncHandlers(): void {
this.asyncWrapper.on('job:created', (job) => {
this.outputChannel.appendLine(`[Async] Job created: ${job.id} (${job.toolName})`);
});
this.asyncWrapper.on('job:completed', (job) => {
this.outputChannel.appendLine(`[Async] Job completed: ${job.id} (${job.toolName})`);
const jobInfo = this.asyncJobs.get(job.id);
if (jobInfo) {
vscode.window.showInformationMessage(
`Hanzo Dev: ${jobInfo.toolType} task completed`
);
}
});
this.asyncWrapper.on('job:failed', (job) => {
this.outputChannel.appendLine(`[Async] Job failed: ${job.id} (${job.toolName})`);
});
this.asyncWrapper.on('job:idle', (job) => {
this.outputChannel.appendLine(`[Async] Job idle: ${job.id} (${job.toolName})`);
});
}
// Async execution methods
async executeToolAsync(toolType: CLIToolType, task: string, options?: any): Promise<string> {
await this.initialize();
const tool = this.tools.get(toolType);
if (!tool) {
throw new Error(`Unknown tool type: ${toolType}`);
}
if (!tool.isReady()) {
throw new Error(`${toolType} is not ready. Please check installation.`);
}
const command = tool.getConfig().command;
const args = [...(tool.getConfig().args || []), tool.generatePrompt(task, options)];
const jobId = await this.asyncWrapper.createAsyncTool(
toolType,
command,
args,
{ toolType, task, options }
);
this.asyncJobs.set(jobId, { toolType, task });
this.outputChannel.appendLine(`\n[${toolType}] Started async job: ${jobId}`);
vscode.window.showInformationMessage(
`Hanzo Dev: Started ${toolType} task (Job ID: ${jobId.substring(0, 8)}...)`
);
return jobId;
}
async queryAsyncJob(jobId: string): Promise<AsyncToolResult | null> {
return this.asyncWrapper.queryJob(jobId);
}
async waitForAsyncJob(jobId: string, timeout?: number): Promise<AsyncToolResult | null> {
return this.asyncWrapper.waitForJob(jobId, timeout);
}
keepAsyncJobAlive(jobId: string): void {
this.asyncWrapper.keepAlive(jobId);
}
sendInputToAsyncJob(jobId: string, input: string): boolean {
return this.asyncWrapper.sendInput(jobId, input);
}
cancelAsyncJob(jobId: string): void {
this.asyncWrapper.cancelJob(jobId);
this.asyncJobs.delete(jobId);
}
getActiveAsyncJobs(): Array<{ jobId: string; toolType: CLIToolType; task: string }> {
const activeJobs = this.asyncWrapper.getActiveJobs();
return activeJobs.map(job => {
const jobInfo = this.asyncJobs.get(job.id);
return {
jobId: job.id,
toolType: jobInfo?.toolType || 'unknown' as CLIToolType,
task: jobInfo?.task || 'Unknown task'
};
});
}
// Execute multiple tools asynchronously and compare results
async executeParallelAsync(
tasks: CLIToolTask[]
): Promise<Map<string, string>> {
await this.initialize();
const jobIds = new Map<string, string>();
for (const taskConfig of tasks) {
try {
const jobId = await this.executeToolAsync(
taskConfig.tool,
taskConfig.task,
{
cwd: taskConfig.directory,
...taskConfig.context
}
);
jobIds.set(`${taskConfig.tool}-${Date.now()}`, jobId);
} catch (error) {
this.outputChannel.appendLine(`Failed to start ${taskConfig.tool}: ${error.message}`);
}
}
return jobIds;
}
dispose(): void {
// Cancel all async jobs
for (const jobId of this.asyncJobs.keys()) {
this.asyncWrapper.cancelJob(jobId);
}
for (const tool of this.tools.values()) {
tool.stop();
}
this.asyncWrapper.dispose();
this.outputChannel.dispose();
}
}
+120
View File
@@ -0,0 +1,120 @@
import { BaseCLITool, CLIToolConfig } from '../common/base-cli';
import * as vscode from 'vscode';
import { execSync } from 'child_process';
export class CodexCLI extends BaseCLITool {
constructor() {
super({
name: 'Codex CLI',
command: 'openai',
args: ['api', 'completions.create'],
env: {},
model: 'code-davinci-002',
apiKeyEnvVar: 'OPENAI_API_KEY',
maxTokens: 2048,
temperature: 0.3
});
}
async install(): Promise<boolean> {
try {
// Install OpenAI CLI
const hasNpm = await this.commandExists('npm');
const hasYarn = await this.commandExists('yarn');
if (hasNpm) {
await this.runCommand('npm', ['install', '-g', 'openai-cli']);
} else if (hasYarn) {
await this.runCommand('yarn', ['global', 'add', 'openai-cli']);
} else {
// Try Python installation
await this.runCommand('pip', ['install', '--user', 'openai']);
}
return true;
} catch (error) {
console.error('Failed to install Codex CLI:', error);
return false;
}
}
async checkInstallation(): Promise<boolean> {
return this.commandExists('openai') || this.commandExists('python');
}
generatePrompt(task: string, context?: any): string {
const language = context?.language || 'typescript';
const purpose = context?.purpose || 'general';
const prompt = `
Language: ${language}
Purpose: ${purpose}
Task: ${task}
Generate high-quality, production-ready code with comments.
`;
return prompt;
}
protected getApiKey(): string {
const config = vscode.workspace.getConfiguration('hanzo');
const openaiKey = config.get<string>('openaiApiKey');
if (openaiKey) return openaiKey;
return process.env.OPENAI_API_KEY || '';
}
private async commandExists(command: string): Promise<boolean> {
try {
execSync(`which ${command}`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
// Codex-specific methods
async generateCode(description: string, language: string): Promise<string> {
const prompt = `# ${language}\n# ${description}\n\n`;
const result = await this.execute(prompt, {
env: {
...process.env,
OPENAI_MODEL: 'code-davinci-002',
OPENAI_MAX_TOKENS: '2048',
OPENAI_TEMPERATURE: '0.3'
}
});
return result.output || '';
}
async explainCode(code: string, language: string): Promise<string> {
const prompt = `Explain this ${language} code in detail:\n\n${code}\n\nExplanation:`;
const result = await this.execute(prompt);
return result.output || '';
}
async optimizeCode(code: string, language: string): Promise<string> {
const prompt = `Optimize this ${language} code for performance and readability:\n\n${code}\n\nOptimized version:`;
const result = await this.execute(prompt);
return result.output || '';
}
async convertCode(code: string, fromLang: string, toLang: string): Promise<string> {
const prompt = `Convert this ${fromLang} code to ${toLang}:\n\n${code}\n\n${toLang} version:`;
const result = await this.execute(prompt);
return result.output || '';
}
async generateDocumentation(code: string, language: string): Promise<string> {
const prompt = `Generate comprehensive documentation for this ${language} code:\n\n${code}\n\nDocumentation:`;
const result = await this.execute(prompt);
return result.output || '';
}
async findBugs(code: string, language: string): Promise<string> {
const prompt = `Find potential bugs and issues in this ${language} code:\n\n${code}\n\nBugs found:`;
const result = await this.execute(prompt);
return result.output || '';
}
}
+150
View File
@@ -0,0 +1,150 @@
import { spawn, ChildProcess, SpawnOptions } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import { EventEmitter } from 'events';
export interface CLIToolConfig {
name: string;
command: string;
args?: string[];
env?: Record<string, string>;
model?: string;
apiKeyEnvVar?: string;
maxTokens?: number;
temperature?: number;
}
export interface CLIToolResponse {
success: boolean;
output?: string;
error?: string;
exitCode?: number;
duration?: number;
}
export abstract class BaseCLITool extends EventEmitter {
protected config: CLIToolConfig;
protected process?: ChildProcess;
protected isInstalled: boolean = false;
protected output: string[] = [];
protected startTime?: number;
constructor(config: CLIToolConfig) {
super();
this.config = config;
}
abstract install(): Promise<boolean>;
abstract checkInstallation(): Promise<boolean>;
abstract generatePrompt(task: string, context?: any): string;
async initialize(): Promise<void> {
this.isInstalled = await this.checkInstallation();
if (!this.isInstalled) {
console.log(`${this.config.name} not installed. Installing...`);
this.isInstalled = await this.install();
}
}
async execute(task: string, options: SpawnOptions = {}): Promise<CLIToolResponse> {
if (!this.isInstalled) {
await this.initialize();
}
this.startTime = Date.now();
this.output = [];
return new Promise((resolve) => {
const prompt = this.generatePrompt(task);
const args = [...(this.config.args || []), prompt];
const env = {
...process.env,
...this.config.env,
[this.config.apiKeyEnvVar || 'API_KEY']: this.getApiKey()
};
this.process = spawn(this.config.command, args, {
...options,
env
});
this.process.stdout?.on('data', (data) => {
const output = data.toString();
this.output.push(output);
this.emit('output', output);
});
this.process.stderr?.on('data', (data) => {
const error = data.toString();
this.emit('error', error);
});
this.process.on('exit', (code) => {
const duration = Date.now() - this.startTime!;
resolve({
success: code === 0,
output: this.output.join(''),
exitCode: code || 0,
duration
});
});
this.process.on('error', (err) => {
resolve({
success: false,
error: err.message,
duration: Date.now() - this.startTime!
});
});
});
}
async executeWithFile(task: string, filePath: string): Promise<CLIToolResponse> {
const fileContent = fs.readFileSync(filePath, 'utf-8');
const enhancedTask = `${task}\n\nFile: ${filePath}\nContent:\n${fileContent}`;
return this.execute(enhancedTask);
}
async executeInDirectory(task: string, directory: string): Promise<CLIToolResponse> {
return this.execute(task, { cwd: directory });
}
stop(): void {
if (this.process) {
this.process.kill();
}
}
protected getApiKey(): string {
// Override in subclasses to get API key from appropriate source
return process.env[this.config.apiKeyEnvVar || 'API_KEY'] || '';
}
protected async runCommand(command: string, args: string[] = []): Promise<string> {
return new Promise((resolve, reject) => {
const proc = spawn(command, args, { shell: true });
let output = '';
proc.stdout?.on('data', (data) => {
output += data.toString();
});
proc.on('exit', (code) => {
if (code === 0) {
resolve(output.trim());
} else {
reject(new Error(`Command failed with code ${code}`));
}
});
});
}
getConfig(): CLIToolConfig {
return this.config;
}
isReady(): boolean {
return this.isInstalled;
}
}
+201
View File
@@ -0,0 +1,201 @@
import { BaseCLITool, CLIToolConfig } from '../common/base-cli';
import * as vscode from 'vscode';
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
export class GeminiCLI extends BaseCLITool {
constructor() {
super({
name: 'Gemini CLI',
command: 'gemini',
args: [],
env: {},
model: 'gemini-pro',
apiKeyEnvVar: 'GOOGLE_API_KEY',
maxTokens: 8192,
temperature: 0.7
});
}
async install(): Promise<boolean> {
try {
// Install Google AI Python SDK or Node.js package
const hasNpm = await this.commandExists('npm');
const hasPython = await this.commandExists('python3');
if (hasNpm) {
// Install Node.js Gemini CLI wrapper
await this.runCommand('npm', ['install', '-g', '@google/generative-ai-cli']);
} else if (hasPython) {
// Install Python SDK
await this.runCommand('pip', ['install', '--user', 'google-generativeai']);
// Create a wrapper script
const wrapperScript = `#!/usr/bin/env python3
import sys
import google.generativeai as genai
import os
genai.configure(api_key=os.environ.get('GOOGLE_API_KEY'))
model = genai.GenerativeModel('gemini-pro')
if len(sys.argv) > 1:
prompt = ' '.join(sys.argv[1:])
response = model.generate_content(prompt)
print(response.text)
`;
const wrapperPath = path.join(process.env.HOME || '', '.local', 'bin', 'gemini');
fs.mkdirSync(path.dirname(wrapperPath), { recursive: true });
fs.writeFileSync(wrapperPath, wrapperScript);
fs.chmodSync(wrapperPath, '755');
}
return true;
} catch (error) {
console.error('Failed to install Gemini CLI:', error);
return false;
}
}
async checkInstallation(): Promise<boolean> {
return this.commandExists('gemini') ||
(await this.commandExists('python3') && await this.pythonPackageExists('google-generativeai'));
}
generatePrompt(task: string, context?: any): string {
const role = context?.role || 'You are an expert AI assistant specialized in coding and development.';
const constraints = context?.constraints || [];
let prompt = `${role}\n\n`;
if (constraints.length > 0) {
prompt += `Constraints:\n${constraints.map(c => `- ${c}`).join('\n')}\n\n`;
}
prompt += `Task: ${task}\n\nPlease provide a comprehensive response.`;
return prompt;
}
protected getApiKey(): string {
const config = vscode.workspace.getConfiguration('hanzo');
const googleKey = config.get<string>('googleApiKey');
if (googleKey) return googleKey;
return process.env.GOOGLE_API_KEY || '';
}
private async commandExists(command: string): Promise<boolean> {
try {
execSync(`which ${command}`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
private async pythonPackageExists(packageName: string): Promise<boolean> {
try {
execSync(`python3 -c "import ${packageName.replace('-', '_')}"`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
// Gemini-specific methods
async analyzeCode(code: string, language: string): Promise<string> {
const prompt = `Analyze this ${language} code and provide insights on:
1. Code quality
2. Performance characteristics
3. Security considerations
4. Best practices adherence
5. Potential improvements
Code:
${code}`;
const result = await this.execute(prompt);
return result.output || '';
}
async generateProject(description: string, stack: string[]): Promise<string> {
const prompt = `Generate a complete project structure for:
Description: ${description}
Tech Stack: ${stack.join(', ')}
Include:
- Directory structure
- Key files with starter code
- Configuration files
- README with setup instructions`;
const result = await this.execute(prompt);
return result.output || '';
}
async refactorCode(code: string, language: string, goals: string[]): Promise<string> {
const prompt = `Refactor this ${language} code to achieve:
${goals.map((g, i) => `${i + 1}. ${g}`).join('\n')}
Original code:
${code}
Provide the refactored code with explanations for changes.`;
const result = await this.execute(prompt);
return result.output || '';
}
async generateAPI(specification: string): Promise<string> {
const prompt = `Generate a REST API based on this specification:
${specification}
Include:
- API endpoints
- Request/response schemas
- Authentication details
- Example implementations
- OpenAPI/Swagger documentation`;
const result = await this.execute(prompt);
return result.output || '';
}
async debugCode(code: string, error: string, language: string): Promise<string> {
const prompt = `Debug this ${language} code that's producing an error:
Error:
${error}
Code:
${code}
Provide:
1. Root cause analysis
2. Fixed code
3. Explanation of the fix
4. Prevention tips`;
const result = await this.execute(prompt);
return result.output || '';
}
async compareApproaches(task: string, approaches: string[]): Promise<string> {
const prompt = `Compare these approaches for: ${task}
Approaches:
${approaches.map((a, i) => `${i + 1}. ${a}`).join('\n')}
Analyze:
- Pros and cons of each
- Performance implications
- Maintainability
- Best use cases
- Recommendation with justification`;
const result = await this.execute(prompt);
return result.output || '';
}
}
+181
View File
@@ -0,0 +1,181 @@
import { BaseCLITool, CLIToolConfig } from '../common/base-cli';
import * as vscode from 'vscode';
import { execSync } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
export class OpenHandsCLI extends BaseCLITool {
constructor() {
super({
name: 'OpenHands CLI',
command: 'openhands',
args: [],
env: {
OPENAI_API_KEY: process.env.OPENAI_API_KEY || '',
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || ''
},
model: 'gpt-4',
apiKeyEnvVar: 'OPENAI_API_KEY',
maxTokens: 8192,
temperature: 0.7
});
}
async install(): Promise<boolean> {
try {
// Check if Docker is installed (required for OpenHands)
const hasDocker = await this.commandExists('docker');
if (!hasDocker) {
throw new Error('Docker is required for OpenHands. Please install Docker first.');
}
// Install via pip
const hasPip = await this.commandExists('pip3');
if (hasPip) {
await this.runCommand('pip3', ['install', 'openhands']);
} else {
// Clone and install from source
const installDir = path.join(process.env.HOME || '', '.hanzo-dev', 'tools');
fs.mkdirSync(installDir, { recursive: true });
await this.runCommand('git', [
'clone',
'https://github.com/All-Hands-AI/OpenHands.git',
path.join(installDir, 'openhands')
]);
await this.runCommand('pip3', ['install', '-e', '.'], {
cwd: path.join(installDir, 'openhands')
});
}
return true;
} catch (error) {
console.error('Failed to install OpenHands:', error);
return false;
}
}
async checkInstallation(): Promise<boolean> {
return this.commandExists('openhands') ||
fs.existsSync(path.join(process.env.HOME || '', '.hanzo-dev', 'tools', 'openhands'));
}
generatePrompt(task: string, context?: any): string {
const workspace = context?.workspace || process.cwd();
const model = context?.model || this.config.model;
// OpenHands uses a different prompt format
return `--task "${task}" --workspace "${workspace}" --model ${model}`;
}
protected getApiKey(): string {
const config = vscode.workspace.getConfiguration('hanzo');
// OpenHands can use multiple API keys
const openaiKey = config.get<string>('openaiApiKey') || process.env.OPENAI_API_KEY;
const anthropicKey = config.get<string>('anthropicApiKey') || process.env.ANTHROPIC_API_KEY;
// Return the appropriate key based on model
if (this.config.model?.includes('claude')) {
return anthropicKey || '';
}
return openaiKey || '';
}
private async commandExists(command: string): Promise<boolean> {
try {
execSync(`which ${command}`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
// OpenHands-specific methods
async createAgent(task: string, workspace: string, config?: any): Promise<string> {
const args = [
'--task', task,
'--workspace', workspace,
'--model', config?.model || this.config.model
];
if (config?.maxIterations) {
args.push('--max-iterations', config.maxIterations);
}
if (config?.agentClass) {
args.push('--agent-class', config.agentClass);
}
const result = await this.execute('', {
env: {
...process.env,
OPENHANDS_ARGS: args.join(' ')
}
});
return result.output || '';
}
async runInDocker(task: string, workspace: string): Promise<string> {
const dockerArgs = [
'run', '-it', '--rm',
'-v', `${workspace}:/workspace`,
'-e', `OPENAI_API_KEY=${this.getApiKey()}`,
'ghcr.io/all-hands-ai/openhands:latest',
'--task', task
];
const result = await this.execute('', {
env: {
...process.env,
DOCKER_COMMAND: 'docker',
DOCKER_ARGS: dockerArgs.join(' ')
}
});
return result.output || '';
}
async executeCodeAction(action: string, code: string, language: string): Promise<string> {
const validActions = ['implement', 'debug', 'refactor', 'test', 'document'];
if (!validActions.includes(action)) {
throw new Error(`Invalid action: ${action}. Must be one of: ${validActions.join(', ')}`);
}
const task = `${action} this ${language} code:\n\n${code}`;
const result = await this.execute(task);
return result.output || '';
}
async generateTests(code: string, language: string, framework?: string): Promise<string> {
const task = `Generate comprehensive unit tests for this ${language} code${framework ? ` using ${framework}` : ''}:\n\n${code}`;
const result = await this.execute(task);
return result.output || '';
}
async reviewPullRequest(prUrl: string): Promise<string> {
const task = `Review this pull request and provide feedback: ${prUrl}`;
const result = await this.execute(task);
return result.output || '';
}
async explainCodebase(directory: string, focus?: string): Promise<string> {
const task = focus
? `Explain the ${focus} functionality in the codebase at ${directory}`
: `Provide an overview of the codebase architecture at ${directory}`;
const result = await this.execute(task, { cwd: directory });
return result.output || '';
}
async implementFeature(description: string, targetFile?: string): Promise<string> {
const task = targetFile
? `Implement this feature in ${targetFile}: ${description}`
: `Implement this feature: ${description}`;
const result = await this.execute(task);
return result.output || '';
}
}
@@ -0,0 +1,551 @@
import { EventEmitter } from 'events';
import { spawn, ChildProcess } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { performance } from 'perf_hooks';
export interface AsyncToolConfig {
idleTimeout: number; // Time in ms before auto-quit (default: 5 minutes)
maxRuntime: number; // Maximum runtime in ms (default: 30 minutes)
checkInterval: number; // How often to check status in ms (default: 1 second)
outputBufferSize: number; // Max output to keep in memory (default: 10MB)
persistResults: boolean; // Save results to disk (default: true)
resultPath: string; // Where to save results
}
export interface AsyncToolJob {
id: string;
toolName: string;
command: string;
args: string[];
status: 'queued' | 'running' | 'completed' | 'failed' | 'cancelled' | 'idle';
startTime?: Date;
endTime?: Date;
lastActivity?: Date;
output: string[];
error: string[];
result?: any;
metadata: Record<string, any>;
process?: ChildProcess;
idleTimer?: NodeJS.Timeout;
runtimeTimer?: NodeJS.Timeout;
}
export interface AsyncToolResult {
jobId: string;
status: AsyncToolJob['status'];
output: string;
error: string;
result?: any;
duration?: number;
metadata: Record<string, any>;
}
export class AsyncToolWrapper extends EventEmitter {
private jobs: Map<string, AsyncToolJob> = new Map();
private config: AsyncToolConfig;
private checkTimer?: NodeJS.Timer;
private outputStreams: Map<string, fs.WriteStream> = new Map();
constructor(config: Partial<AsyncToolConfig> = {}) {
super();
this.config = {
idleTimeout: config.idleTimeout || 5 * 60 * 1000, // 5 minutes
maxRuntime: config.maxRuntime || 30 * 60 * 1000, // 30 minutes
checkInterval: config.checkInterval || 1000, // 1 second
outputBufferSize: config.outputBufferSize || 10 * 1024 * 1024, // 10MB
persistResults: config.persistResults ?? true,
resultPath: config.resultPath || path.join(process.cwd(), '.hanzo-dev', 'async-results')
};
if (this.config.persistResults) {
fs.mkdirSync(this.config.resultPath, { recursive: true });
}
this.startStatusChecker();
}
/**
* Create an async version of any tool/command
*/
async createAsyncTool(
toolName: string,
command: string,
args: string[] = [],
metadata: Record<string, any> = {}
): Promise<string> {
const jobId = uuidv4();
const job: AsyncToolJob = {
id: jobId,
toolName,
command,
args,
status: 'queued',
output: [],
error: [],
metadata
};
this.jobs.set(jobId, job);
this.emit('job:created', job);
// Start the job asynchronously
setImmediate(() => this.startJob(job));
return jobId;
}
/**
* Create async versions of MCP tools
*/
async createAsyncMCPTool(toolName: string, args: any, metadata: Record<string, any> = {}): Promise<string> {
// Convert MCP tool call to async
const mcpCommand = this.getMCPCommand(toolName);
const mcpArgs = this.serializeMCPArgs(args);
return this.createAsyncTool(`mcp_${toolName}`, mcpCommand, mcpArgs, {
...metadata,
mcpTool: true,
originalArgs: args
});
}
private getMCPCommand(toolName: string): string {
// Map tool names to their executable commands
const toolMap: Record<string, string> = {
'search': 'mcp-search',
'grep': 'mcp-grep',
'read': 'mcp-read',
'edit': 'mcp-edit',
'bash': 'bash',
'webfetch': 'mcp-webfetch',
// Add more as needed
};
return toolMap[toolName.toLowerCase()] || toolName;
}
private serializeMCPArgs(args: any): string[] {
// Convert MCP args to command line arguments
const result: string[] = [];
for (const [key, value] of Object.entries(args)) {
if (value !== null && value !== undefined) {
result.push(`--${key}`);
if (typeof value === 'boolean') {
// Boolean flags don't need values
} else if (Array.isArray(value)) {
result.push(value.join(','));
} else {
result.push(String(value));
}
}
}
return result;
}
private async startJob(job: AsyncToolJob): Promise<void> {
try {
job.status = 'running';
job.startTime = new Date();
job.lastActivity = new Date();
// Create output stream if persisting
if (this.config.persistResults) {
const outputPath = path.join(this.config.resultPath, `${job.id}.log`);
const stream = fs.createWriteStream(outputPath);
this.outputStreams.set(job.id, stream);
}
// Spawn the process
job.process = spawn(job.command, job.args, {
shell: true,
env: {
...process.env,
HANZO_DEV_ASYNC: 'true',
HANZO_DEV_JOB_ID: job.id
}
});
// Handle stdout
job.process.stdout?.on('data', (data) => {
const output = data.toString();
this.handleOutput(job, output, 'stdout');
});
// Handle stderr
job.process.stderr?.on('data', (data) => {
const error = data.toString();
this.handleOutput(job, error, 'stderr');
});
// Handle process exit
job.process.on('exit', (code, signal) => {
this.handleJobComplete(job, code, signal);
});
// Handle process error
job.process.on('error', (error) => {
job.status = 'failed';
job.error.push(error.message);
this.handleJobComplete(job, 1);
});
// Set up idle timeout
this.resetIdleTimer(job);
// Set up max runtime timeout
job.runtimeTimer = setTimeout(() => {
if (job.status === 'running') {
this.cancelJob(job.id, 'Max runtime exceeded');
}
}, this.config.maxRuntime);
this.emit('job:started', job);
} catch (error) {
job.status = 'failed';
job.error.push(error.message);
this.emit('job:failed', job);
}
}
private handleOutput(job: AsyncToolJob, data: string, type: 'stdout' | 'stderr'): void {
job.lastActivity = new Date();
// Add to appropriate buffer
const buffer = type === 'stdout' ? job.output : job.error;
buffer.push(data);
// Trim buffer if too large
const totalSize = buffer.reduce((sum, str) => sum + str.length, 0);
if (totalSize > this.config.outputBufferSize) {
// Remove oldest entries until under limit
while (buffer.length > 0 &&
buffer.reduce((sum, str) => sum + str.length, 0) > this.config.outputBufferSize) {
buffer.shift();
}
}
// Write to file if persisting
const stream = this.outputStreams.get(job.id);
if (stream) {
stream.write(`[${type}] ${new Date().toISOString()} ${data}`);
}
// Reset idle timer
this.resetIdleTimer(job);
// Emit output event
this.emit('job:output', {
jobId: job.id,
type,
data
});
// Check for completion patterns
this.checkForCompletion(job, data);
}
private checkForCompletion(job: AsyncToolJob, output: string): void {
// Look for common completion patterns
const completionPatterns = [
/task completed/i,
/finished successfully/i,
/done\./i,
/completed in \d+/i,
/\[done\]/i
];
if (completionPatterns.some(pattern => pattern.test(output))) {
// Mark as idle instead of immediately completing
job.status = 'idle';
this.emit('job:idle', job);
}
}
private resetIdleTimer(job: AsyncToolJob): void {
// Clear existing timer
if (job.idleTimer) {
clearTimeout(job.idleTimer);
}
// Set new timer
job.idleTimer = setTimeout(() => {
if (job.status === 'running' || job.status === 'idle') {
this.handleIdleTimeout(job);
}
}, this.config.idleTimeout);
}
private handleIdleTimeout(job: AsyncToolJob): void {
if (job.status === 'idle') {
// If already idle, complete the job
this.completeJob(job.id);
} else {
// Mark as idle
job.status = 'idle';
this.emit('job:idle', job);
// Give it one more idle period before completing
this.resetIdleTimer(job);
}
}
private handleJobComplete(job: AsyncToolJob, code?: number | null, signal?: string | null): void {
job.endTime = new Date();
job.status = code === 0 ? 'completed' : 'failed';
if (job.idleTimer) {
clearTimeout(job.idleTimer);
}
if (job.runtimeTimer) {
clearTimeout(job.runtimeTimer);
}
// Close output stream
const stream = this.outputStreams.get(job.id);
if (stream) {
stream.end();
this.outputStreams.delete(job.id);
}
// Parse result if possible
try {
const output = job.output.join('');
if (output.trim().startsWith('{') || output.trim().startsWith('[')) {
job.result = JSON.parse(output);
}
} catch (error) {
// Not JSON, keep as string
}
// Save result if persisting
if (this.config.persistResults) {
this.saveJobResult(job);
}
this.emit('job:completed', job);
}
private saveJobResult(job: AsyncToolJob): void {
const resultPath = path.join(this.config.resultPath, `${job.id}.json`);
const result: AsyncToolResult = {
jobId: job.id,
status: job.status,
output: job.output.join(''),
error: job.error.join(''),
result: job.result,
duration: job.startTime && job.endTime ?
job.endTime.getTime() - job.startTime.getTime() : undefined,
metadata: job.metadata
};
fs.writeFileSync(resultPath, JSON.stringify(result, null, 2));
}
/**
* Query the status of an async job
*/
async queryJob(jobId: string): Promise<AsyncToolResult | null> {
const job = this.jobs.get(jobId);
if (!job) {
// Try to load from disk
if (this.config.persistResults) {
const resultPath = path.join(this.config.resultPath, `${jobId}.json`);
if (fs.existsSync(resultPath)) {
return JSON.parse(fs.readFileSync(resultPath, 'utf-8'));
}
}
return null;
}
return {
jobId: job.id,
status: job.status,
output: job.output.join(''),
error: job.error.join(''),
result: job.result,
duration: job.startTime ?
(job.endTime ? job.endTime.getTime() : Date.now()) - job.startTime.getTime() : undefined,
metadata: job.metadata
};
}
/**
* Wait for a job to complete
*/
async waitForJob(jobId: string, timeout?: number): Promise<AsyncToolResult | null> {
const startTime = Date.now();
const maxWait = timeout || this.config.maxRuntime;
while (Date.now() - startTime < maxWait) {
const result = await this.queryJob(jobId);
if (!result) {
throw new Error(`Job ${jobId} not found`);
}
if (['completed', 'failed', 'cancelled'].includes(result.status)) {
return result;
}
// Wait before checking again
await new Promise(resolve => setTimeout(resolve, this.config.checkInterval));
}
throw new Error(`Timeout waiting for job ${jobId}`);
}
/**
* Keep a job alive (prevent idle timeout)
*/
keepAlive(jobId: string): void {
const job = this.jobs.get(jobId);
if (job && (job.status === 'running' || job.status === 'idle')) {
job.lastActivity = new Date();
this.resetIdleTimer(job);
if (job.status === 'idle') {
job.status = 'running';
this.emit('job:resumed', job);
}
}
}
/**
* Send input to a running job
*/
sendInput(jobId: string, input: string): boolean {
const job = this.jobs.get(jobId);
if (!job || !job.process || job.status !== 'running') {
return false;
}
job.process.stdin?.write(input + '\n');
job.lastActivity = new Date();
this.resetIdleTimer(job);
return true;
}
/**
* Complete a job manually
*/
completeJob(jobId: string, result?: any): void {
const job = this.jobs.get(jobId);
if (!job || ['completed', 'failed', 'cancelled'].includes(job.status)) {
return;
}
if (result) {
job.result = result;
}
if (job.process && !job.process.killed) {
job.process.kill('SIGTERM');
} else {
this.handleJobComplete(job, 0);
}
}
/**
* Cancel a running job
*/
cancelJob(jobId: string, reason?: string): void {
const job = this.jobs.get(jobId);
if (!job || ['completed', 'failed', 'cancelled'].includes(job.status)) {
return;
}
job.status = 'cancelled';
if (reason) {
job.error.push(`Cancelled: ${reason}`);
}
if (job.process && !job.process.killed) {
job.process.kill('SIGKILL');
}
this.handleJobComplete(job, -1);
}
/**
* Get all active jobs
*/
getActiveJobs(): AsyncToolJob[] {
return Array.from(this.jobs.values()).filter(
job => ['running', 'idle', 'queued'].includes(job.status)
);
}
/**
* Get all jobs
*/
getAllJobs(): AsyncToolJob[] {
return Array.from(this.jobs.values());
}
/**
* Clean up completed jobs
*/
cleanupCompletedJobs(olderThan?: number): number {
const cutoff = Date.now() - (olderThan || 24 * 60 * 60 * 1000); // Default 24 hours
let cleaned = 0;
for (const [jobId, job] of this.jobs) {
if (['completed', 'failed', 'cancelled'].includes(job.status) &&
job.endTime && job.endTime.getTime() < cutoff) {
this.jobs.delete(jobId);
cleaned++;
}
}
return cleaned;
}
private startStatusChecker(): void {
this.checkTimer = setInterval(() => {
// Emit status for monitoring
const stats = {
total: this.jobs.size,
running: 0,
idle: 0,
completed: 0,
failed: 0
};
for (const job of this.jobs.values()) {
stats[job.status]++;
}
this.emit('status', stats);
}, 10000); // Every 10 seconds
}
dispose(): void {
// Cancel all running jobs
for (const job of this.getActiveJobs()) {
this.cancelJob(job.id, 'System shutdown');
}
// Clear timers
if (this.checkTimer) {
clearInterval(this.checkTimer);
}
// Close all streams
for (const stream of this.outputStreams.values()) {
stream.end();
}
this.removeAllListeners();
}
}
+450
View File
@@ -0,0 +1,450 @@
import * as vscode from 'vscode';
import { EventEmitter } from 'events';
import { spawn, ChildProcess } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import { execSync } from 'child_process';
import { CLIToolManager, CLIToolType } from '../cli-tool-manager';
import { DevMonitor } from './dev-monitor';
import { PlatformSyncService } from './sync-service';
export interface DevToolInstance {
id: string;
type: CLIToolType | 'openhands' | 'aider' | 'custom';
sessionId: string;
process?: ChildProcess;
worktreePath?: string;
branch?: string;
status: 'initializing' | 'running' | 'stopped' | 'error';
config: DevToolConfig;
startTime: Date;
lastActivity?: Date;
}
export interface DevToolConfig {
name: string;
command: string;
args?: string[];
env?: Record<string, string>;
workingDirectory: string;
useWorktree?: boolean;
worktreePrefix?: string;
autoSync?: boolean;
sessionTimeout?: number;
}
export interface LauncherConfig {
maxInstances: number;
defaultTimeout: number;
gitRoot: string;
workspacePath: string;
enableSync: boolean;
syncConfig?: any;
}
export class DevLauncher extends EventEmitter {
private instances: Map<string, DevToolInstance> = new Map();
private cliManager: CLIToolManager;
private monitor: DevMonitor;
private syncService?: PlatformSyncService;
private config: LauncherConfig;
private outputChannel: vscode.OutputChannel;
constructor(config: LauncherConfig) {
super();
this.config = config;
this.cliManager = new CLIToolManager();
this.monitor = new DevMonitor({
logPath: path.join(config.workspacePath, '.hanzo-dev', 'logs'),
maxLogSize: 10 * 1024 * 1024, // 10MB
rotateInterval: 24 * 60 * 60 * 1000, // Daily
metricsInterval: 5000, // 5 seconds
enablePerfMonitoring: true
});
this.outputChannel = vscode.window.createOutputChannel('Hanzo Dev Launcher');
if (config.enableSync && config.syncConfig) {
this.syncService = new PlatformSyncService(config.syncConfig);
this.setupSyncHandlers();
}
}
async initialize(): Promise<void> {
await this.cliManager.initialize();
if (this.syncService) {
await this.syncService.connect();
}
this.outputChannel.appendLine('Hanzo Dev Launcher initialized');
}
private setupSyncHandlers(): void {
if (!this.syncService) return;
this.syncService.on('command_request', (request) => {
this.handleRemoteCommand(request);
});
this.syncService.on('file_synced', (data) => {
this.outputChannel.appendLine(`File synced: ${data.filePath}`);
});
}
async launchTool(
type: DevToolInstance['type'],
task: string,
options: Partial<DevToolConfig> = {}
): Promise<string> {
if (this.instances.size >= this.config.maxInstances) {
throw new Error(`Maximum instances (${this.config.maxInstances}) reached`);
}
const instanceId = `${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const sessionId = this.monitor.startSession(type, options.workingDirectory || this.config.workspacePath, options.name || 'main');
const config: DevToolConfig = {
name: options.name || `${type}-session`,
command: this.getToolCommand(type),
args: options.args || [],
env: options.env || {},
workingDirectory: options.workingDirectory || this.config.workspacePath,
useWorktree: options.useWorktree ?? true,
worktreePrefix: options.worktreePrefix || `ai/${type}`,
autoSync: options.autoSync ?? true,
sessionTimeout: options.sessionTimeout || this.config.defaultTimeout
};
const instance: DevToolInstance = {
id: instanceId,
type,
sessionId,
status: 'initializing',
config,
startTime: new Date()
};
this.instances.set(instanceId, instance);
try {
// Create worktree if requested
if (config.useWorktree && this.isGitRepo()) {
await this.createWorktree(instance);
}
// Launch the tool
await this.startToolProcess(instance, task);
// Register with sync service
if (this.syncService && config.autoSync) {
this.syncService.registerDevTool(instanceId, {
sessionId,
workingDirectory: instance.worktreePath || config.workingDirectory,
branch: instance.branch || 'main',
files: [],
lastActivity: new Date(),
metadata: { type, task }
});
}
this.emit('tool:launched', instance);
this.outputChannel.appendLine(`Launched ${type} instance: ${instanceId}`);
return instanceId;
} catch (error) {
instance.status = 'error';
this.monitor.endSession(sessionId, 'failed');
this.monitor.log('error', 'launcher', `Failed to launch ${type}: ${error.message}`);
throw error;
}
}
private getToolCommand(type: DevToolInstance['type']): string {
switch (type) {
case 'claude':
return 'claude';
case 'codex':
return 'openai';
case 'gemini':
return 'gemini';
case 'openhands':
return 'openhands';
case 'aider':
return 'aider';
default:
return type;
}
}
private isGitRepo(): boolean {
try {
execSync('git rev-parse --git-dir', {
cwd: this.config.gitRoot,
stdio: 'ignore'
});
return true;
} catch {
return false;
}
}
private async createWorktree(instance: DevToolInstance): Promise<void> {
const { worktreePrefix } = instance.config;
const branchName = `${worktreePrefix}/${instance.id}`;
const worktreePath = path.join(this.config.gitRoot, '.worktrees', instance.id);
try {
// Create worktree with new branch
execSync(`git worktree add -b "${branchName}" "${worktreePath}"`, {
cwd: this.config.gitRoot,
stdio: 'pipe'
});
instance.branch = branchName;
instance.worktreePath = worktreePath;
instance.config.workingDirectory = worktreePath;
this.outputChannel.appendLine(`Created worktree: ${worktreePath} (branch: ${branchName})`);
this.monitor.log('info', 'launcher', `Created worktree for ${instance.type}`);
} catch (error) {
this.monitor.log('error', 'launcher', `Failed to create worktree: ${error.message}`);
throw new Error(`Failed to create worktree: ${error.message}`);
}
}
private async startToolProcess(instance: DevToolInstance, task: string): Promise<void> {
const { command, args = [], env = {}, workingDirectory } = instance.config;
// For known CLI tools, use the CLIToolManager
if (['claude', 'codex', 'gemini'].includes(instance.type)) {
const result = await this.cliManager.executeTool(
instance.type as CLIToolType,
task,
{ cwd: workingDirectory }
);
instance.status = result.success ? 'running' : 'error';
this.monitor.recordCommand(instance.sessionId, command, [task]);
this.monitor.recordCommandComplete(instance.sessionId, result.success);
if (result.output) {
this.monitor.log('info', instance.type, result.output);
}
return;
}
// For other tools, spawn directly
const processEnv = {
...process.env,
...env,
HANZO_DEV_SESSION: instance.sessionId,
HANZO_DEV_INSTANCE: instance.id
};
// Special handling for specific tools
let fullCommand = command;
let fullArgs = [...args];
switch (instance.type) {
case 'aider':
// Aider-specific setup
fullArgs = ['--yes', '--auto-commits', ...args, task];
break;
case 'openhands':
// OpenHands-specific setup
fullArgs = ['--workspace', workingDirectory, '--task', task, ...args];
break;
}
instance.process = spawn(fullCommand, fullArgs, {
cwd: workingDirectory,
env: processEnv,
shell: true
});
instance.status = 'running';
instance.lastActivity = new Date();
// Handle process output
instance.process.stdout?.on('data', (data) => {
const output = data.toString();
this.monitor.log('info', instance.type, output);
if (this.syncService) {
this.syncService.logOutput(instance.id, output);
}
instance.lastActivity = new Date();
this.emit('tool:output', { instanceId: instance.id, output });
});
instance.process.stderr?.on('data', (data) => {
const error = data.toString();
this.monitor.log('error', instance.type, error);
if (this.syncService) {
this.syncService.logError(instance.id, error);
}
this.emit('tool:error', { instanceId: instance.id, error });
});
instance.process.on('exit', (code) => {
instance.status = 'stopped';
this.monitor.endSession(instance.sessionId, code === 0 ? 'completed' : 'failed');
if (this.syncService) {
this.syncService.updateStatus(instance.id, 'stopped', { exitCode: code });
}
this.emit('tool:stopped', { instanceId: instance.id, exitCode: code });
// Clean up worktree after a delay
if (instance.worktreePath) {
setTimeout(() => this.cleanupWorktree(instance), 60000);
}
});
// Set up session timeout
if (instance.config.sessionTimeout) {
setTimeout(() => {
if (instance.status === 'running') {
this.stopTool(instance.id);
}
}, instance.config.sessionTimeout);
}
this.monitor.recordCommand(instance.sessionId, fullCommand, fullArgs);
}
async stopTool(instanceId: string): Promise<void> {
const instance = this.instances.get(instanceId);
if (!instance) return;
if (instance.process) {
instance.process.kill('SIGTERM');
// Force kill after 5 seconds
setTimeout(() => {
if (instance.process && !instance.process.killed) {
instance.process.kill('SIGKILL');
}
}, 5000);
}
instance.status = 'stopped';
this.monitor.endSession(instance.sessionId);
if (this.syncService) {
this.syncService.updateStatus(instanceId, 'stopped');
}
this.emit('tool:stopped', { instanceId });
}
async stopAllTools(): Promise<void> {
const promises = Array.from(this.instances.keys()).map(
id => this.stopTool(id)
);
await Promise.all(promises);
}
private async cleanupWorktree(instance: DevToolInstance): Promise<void> {
if (!instance.worktreePath || !instance.branch) return;
try {
// Remove worktree
execSync(`git worktree remove --force "${instance.worktreePath}"`, {
cwd: this.config.gitRoot,
stdio: 'pipe'
});
// Delete branch
execSync(`git branch -D "${instance.branch}"`, {
cwd: this.config.gitRoot,
stdio: 'pipe'
});
this.outputChannel.appendLine(`Cleaned up worktree: ${instance.worktreePath}`);
} catch (error) {
this.monitor.log('warn', 'launcher', `Failed to cleanup worktree: ${error.message}`);
}
// Remove instance from map
this.instances.delete(instance.id);
}
private async handleRemoteCommand(request: any): Promise<void> {
const { toolId, command, args } = request.data;
const instance = this.instances.get(toolId);
if (!instance || !instance.process) {
this.syncService?.logError(toolId, 'Instance not found or not running');
return;
}
// Send command to process stdin
if (instance.process.stdin) {
instance.process.stdin.write(`${command} ${args.join(' ')}\n`);
this.monitor.recordCommand(instance.sessionId, command, args);
}
}
async launchParallel(
tasks: Array<{ type: DevToolInstance['type']; task: string; options?: Partial<DevToolConfig> }>
): Promise<string[]> {
const results = await Promise.allSettled(
tasks.map(t => this.launchTool(t.type, t.task, t.options))
);
const instanceIds: string[] = [];
for (const result of results) {
if (result.status === 'fulfilled') {
instanceIds.push(result.value);
} else {
this.monitor.log('error', 'launcher', `Failed to launch tool: ${result.reason}`);
}
}
return instanceIds;
}
getInstance(instanceId: string): DevToolInstance | undefined {
return this.instances.get(instanceId);
}
getActiveInstances(): DevToolInstance[] {
return Array.from(this.instances.values()).filter(
i => i.status === 'running'
);
}
getAllInstances(): DevToolInstance[] {
return Array.from(this.instances.values());
}
getInstancesByType(type: DevToolInstance['type']): DevToolInstance[] {
return Array.from(this.instances.values()).filter(
i => i.type === type
);
}
showLauncherView(): void {
this.outputChannel.show();
}
async dispose(): Promise<void> {
await this.stopAllTools();
if (this.syncService) {
await this.syncService.disconnect();
}
this.monitor.dispose();
this.cliManager.dispose();
this.outputChannel.dispose();
}
}
+346
View File
@@ -0,0 +1,346 @@
import * as vscode from 'vscode';
import { EventEmitter } from 'events';
import * as fs from 'fs';
import * as path from 'path';
import { performance } from 'perf_hooks';
export interface DevToolSession {
id: string;
toolType: string;
startTime: Date;
endTime?: Date;
status: 'running' | 'completed' | 'failed' | 'paused';
workingDirectory: string;
branch: string;
metrics: SessionMetrics;
logs: LogEntry[];
}
export interface SessionMetrics {
commandsExecuted: number;
filesModified: number;
linesAdded: number;
linesRemoved: number;
tokensUsed: number;
apiCalls: number;
errors: number;
cpuUsage?: number;
memoryUsage?: number;
}
export interface LogEntry {
timestamp: Date;
level: 'info' | 'warn' | 'error' | 'debug';
source: string;
message: string;
data?: any;
}
export interface MonitorConfig {
logPath: string;
maxLogSize: number;
rotateInterval: number;
metricsInterval: number;
enablePerfMonitoring: boolean;
}
export class DevMonitor extends EventEmitter {
private sessions: Map<string, DevToolSession> = new Map();
private config: MonitorConfig;
private logStream?: fs.WriteStream;
private metricsTimer?: NodeJS.Timer;
private outputChannel: vscode.OutputChannel;
private statusBar: vscode.StatusBarItem;
private perfObserver?: PerformanceObserver;
constructor(config: MonitorConfig) {
super();
this.config = config;
this.outputChannel = vscode.window.createOutputChannel('Hanzo Dev Monitor');
this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
this.statusBar.command = 'hanzo-dev.showMonitor';
this.initializeLogging();
this.startMetricsCollection();
}
private initializeLogging(): void {
// Create log directory if it doesn't exist
fs.mkdirSync(this.config.logPath, { recursive: true });
// Create log file with rotation
const logFile = path.join(this.config.logPath, `hanzo-dev-${Date.now()}.log`);
this.logStream = fs.createWriteStream(logFile, { flags: 'a' });
// Set up log rotation
setInterval(() => this.rotateLog(), this.config.rotateInterval);
}
private rotateLog(): void {
if (!this.logStream) return;
const stats = fs.statSync(this.logStream.path.toString());
if (stats.size > this.config.maxLogSize) {
this.logStream.end();
const newLogFile = path.join(this.config.logPath, `hanzo-dev-${Date.now()}.log`);
this.logStream = fs.createWriteStream(newLogFile, { flags: 'a' });
}
}
private startMetricsCollection(): void {
this.metricsTimer = setInterval(() => {
this.collectMetrics();
this.updateStatusBar();
}, this.config.metricsInterval);
if (this.config.enablePerfMonitoring) {
this.setupPerformanceMonitoring();
}
}
private setupPerformanceMonitoring(): void {
try {
const { PerformanceObserver } = require('perf_hooks');
this.perfObserver = new PerformanceObserver((items) => {
for (const entry of items.getEntries()) {
this.log('debug', 'performance', `${entry.name}: ${entry.duration}ms`);
}
});
this.perfObserver.observe({ entryTypes: ['measure'] });
} catch (error) {
this.log('warn', 'monitor', 'Performance monitoring not available');
}
}
startSession(toolType: string, workingDirectory: string, branch: string): string {
const sessionId = `${toolType}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const session: DevToolSession = {
id: sessionId,
toolType,
startTime: new Date(),
status: 'running',
workingDirectory,
branch,
metrics: {
commandsExecuted: 0,
filesModified: 0,
linesAdded: 0,
linesRemoved: 0,
tokensUsed: 0,
apiCalls: 0,
errors: 0
},
logs: []
};
this.sessions.set(sessionId, session);
this.emit('session:started', session);
this.log('info', 'monitor', `Started ${toolType} session: ${sessionId}`);
return sessionId;
}
endSession(sessionId: string, status: 'completed' | 'failed' = 'completed'): void {
const session = this.sessions.get(sessionId);
if (!session) return;
session.endTime = new Date();
session.status = status;
this.emit('session:ended', session);
this.log('info', 'monitor', `Ended session ${sessionId}: ${status}`);
// Archive session after a delay
setTimeout(() => this.archiveSession(sessionId), 60000);
}
private archiveSession(sessionId: string): void {
const session = this.sessions.get(sessionId);
if (!session) return;
// Save session data to file
const archivePath = path.join(this.config.logPath, 'sessions', `${sessionId}.json`);
fs.mkdirSync(path.dirname(archivePath), { recursive: true });
fs.writeFileSync(archivePath, JSON.stringify(session, null, 2));
// Remove from active sessions
this.sessions.delete(sessionId);
}
log(level: LogEntry['level'], source: string, message: string, data?: any): void {
const entry: LogEntry = {
timestamp: new Date(),
level,
source,
message,
data
};
// Add to all active sessions
for (const session of this.sessions.values()) {
if (session.status === 'running') {
session.logs.push(entry);
}
}
// Write to log file
if (this.logStream) {
this.logStream.write(JSON.stringify(entry) + '\n');
}
// Show in output channel
const logMessage = `[${entry.timestamp.toISOString()}] [${entry.level.toUpperCase()}] [${entry.source}] ${entry.message}`;
this.outputChannel.appendLine(logMessage);
// Show errors in UI
if (level === 'error') {
vscode.window.showErrorMessage(`Hanzo Dev: ${message}`);
}
}
incrementMetric(sessionId: string, metric: keyof SessionMetrics, value: number = 1): void {
const session = this.sessions.get(sessionId);
if (!session) return;
(session.metrics[metric] as number) += value;
this.emit('metrics:updated', { sessionId, metric, value });
}
recordCommand(sessionId: string, command: string, args: string[]): void {
const session = this.sessions.get(sessionId);
if (!session) return;
this.incrementMetric(sessionId, 'commandsExecuted');
this.log('info', session.toolType, `Command: ${command} ${args.join(' ')}`);
performance.mark(`cmd-start-${sessionId}`);
}
recordCommandComplete(sessionId: string, success: boolean): void {
performance.mark(`cmd-end-${sessionId}`);
try {
performance.measure(
`command-duration-${sessionId}`,
`cmd-start-${sessionId}`,
`cmd-end-${sessionId}`
);
} catch (error) {
// Ignore if marks don't exist
}
if (!success) {
this.incrementMetric(sessionId, 'errors');
}
}
recordFileChange(sessionId: string, filePath: string, linesAdded: number, linesRemoved: number): void {
const session = this.sessions.get(sessionId);
if (!session) return;
this.incrementMetric(sessionId, 'filesModified');
this.incrementMetric(sessionId, 'linesAdded', linesAdded);
this.incrementMetric(sessionId, 'linesRemoved', linesRemoved);
this.log('debug', session.toolType, `File modified: ${filePath} (+${linesAdded}/-${linesRemoved})`);
}
recordAPICall(sessionId: string, endpoint: string, tokensUsed: number): void {
const session = this.sessions.get(sessionId);
if (!session) return;
this.incrementMetric(sessionId, 'apiCalls');
this.incrementMetric(sessionId, 'tokensUsed', tokensUsed);
this.log('debug', session.toolType, `API call: ${endpoint} (${tokensUsed} tokens)`);
}
private collectMetrics(): void {
if (process.platform === 'darwin' || process.platform === 'linux') {
// Collect system metrics for active sessions
for (const session of this.sessions.values()) {
if (session.status === 'running') {
// Basic process metrics (would need proper implementation)
session.metrics.cpuUsage = process.cpuUsage().user / 1000000;
session.metrics.memoryUsage = process.memoryUsage().heapUsed / 1024 / 1024;
}
}
}
}
private updateStatusBar(): void {
const activeSessions = Array.from(this.sessions.values()).filter(
s => s.status === 'running'
);
if (activeSessions.length === 0) {
this.statusBar.hide();
return;
}
const totalCommands = activeSessions.reduce(
(sum, s) => sum + s.metrics.commandsExecuted, 0
);
const totalTokens = activeSessions.reduce(
(sum, s) => sum + s.metrics.tokensUsed, 0
);
this.statusBar.text = `$(terminal) Hanzo Dev: ${activeSessions.length} active | ${totalCommands} cmds | ${totalTokens} tokens`;
this.statusBar.tooltip = activeSessions.map(
s => `${s.toolType}: ${s.metrics.commandsExecuted} commands`
).join('\n');
this.statusBar.show();
}
getSession(sessionId: string): DevToolSession | undefined {
return this.sessions.get(sessionId);
}
getActiveSessions(): DevToolSession[] {
return Array.from(this.sessions.values()).filter(
s => s.status === 'running'
);
}
getAllSessions(): DevToolSession[] {
return Array.from(this.sessions.values());
}
pauseSession(sessionId: string): void {
const session = this.sessions.get(sessionId);
if (session && session.status === 'running') {
session.status = 'paused';
this.emit('session:paused', session);
}
}
resumeSession(sessionId: string): void {
const session = this.sessions.get(sessionId);
if (session && session.status === 'paused') {
session.status = 'running';
this.emit('session:resumed', session);
}
}
showMonitorView(): void {
this.outputChannel.show();
}
dispose(): void {
if (this.metricsTimer) {
clearInterval(this.metricsTimer);
}
if (this.perfObserver) {
this.perfObserver.disconnect();
}
if (this.logStream) {
this.logStream.end();
}
this.statusBar.dispose();
this.outputChannel.dispose();
}
}
+318
View File
@@ -0,0 +1,318 @@
import * as vscode from 'vscode';
import { EventEmitter } from 'events';
import * as fs from 'fs';
import * as path from 'path';
import { createHash } from 'crypto';
import WebSocket from 'ws';
export interface SyncConfig {
platformUrl: string;
apiKey: string;
projectId: string;
syncInterval: number;
enableBidirectional: boolean;
}
export interface DevToolContext {
toolId: string;
sessionId: string;
workingDirectory: string;
branch: string;
files: string[];
lastActivity: Date;
metadata: Record<string, any>;
}
export interface SyncEvent {
type: 'file_change' | 'command' | 'output' | 'error' | 'status';
timestamp: Date;
toolId: string;
data: any;
}
export class PlatformSyncService extends EventEmitter {
private config: SyncConfig;
private ws?: WebSocket;
private contexts: Map<string, DevToolContext> = new Map();
private syncQueue: SyncEvent[] = [];
private connected: boolean = false;
private syncTimer?: NodeJS.Timer;
private fileWatchers: Map<string, vscode.FileSystemWatcher> = new Map();
constructor(config: SyncConfig) {
super();
this.config = config;
}
async connect(): Promise<void> {
return new Promise((resolve, reject) => {
const wsUrl = `${this.config.platformUrl.replace('http', 'ws')}/sync`;
this.ws = new WebSocket(wsUrl, {
headers: {
'Authorization': `Bearer ${this.config.apiKey}`,
'X-Project-ID': this.config.projectId
}
});
this.ws.on('open', () => {
this.connected = true;
this.emit('connected');
this.startSync();
resolve();
});
this.ws.on('message', (data) => {
this.handlePlatformMessage(JSON.parse(data.toString()));
});
this.ws.on('error', (error) => {
this.emit('error', error);
reject(error);
});
this.ws.on('close', () => {
this.connected = false;
this.emit('disconnected');
this.stopSync();
// Auto-reconnect after 5 seconds
setTimeout(() => this.connect(), 5000);
});
});
}
registerDevTool(toolId: string, context: Omit<DevToolContext, 'toolId'>): void {
const fullContext: DevToolContext = {
toolId,
...context
};
this.contexts.set(toolId, fullContext);
// Set up file watching for bi-directional sync
if (this.config.enableBidirectional) {
this.setupFileWatching(toolId, context.workingDirectory);
}
// Send registration event
this.queueEvent({
type: 'status',
timestamp: new Date(),
toolId,
data: { status: 'registered', context: fullContext }
});
}
private setupFileWatching(toolId: string, directory: string): void {
const pattern = new vscode.RelativePattern(directory, '**/*');
const watcher = vscode.workspace.createFileSystemWatcher(pattern);
watcher.onDidChange((uri) => {
this.handleFileChange(toolId, uri, 'changed');
});
watcher.onDidCreate((uri) => {
this.handleFileChange(toolId, uri, 'created');
});
watcher.onDidDelete((uri) => {
this.handleFileChange(toolId, uri, 'deleted');
});
this.fileWatchers.set(toolId, watcher);
}
private handleFileChange(toolId: string, uri: vscode.Uri, changeType: string): void {
const context = this.contexts.get(toolId);
if (!context) return;
const relativePath = path.relative(context.workingDirectory, uri.fsPath);
// Don't sync hidden files or node_modules
if (relativePath.includes('node_modules') || relativePath.startsWith('.')) {
return;
}
this.queueEvent({
type: 'file_change',
timestamp: new Date(),
toolId,
data: {
path: relativePath,
changeType,
content: changeType !== 'deleted' ? fs.readFileSync(uri.fsPath, 'utf-8') : null,
hash: changeType !== 'deleted' ? this.hashFile(uri.fsPath) : null
}
});
}
private hashFile(filePath: string): string {
const content = fs.readFileSync(filePath);
return createHash('sha256').update(content).digest('hex');
}
logCommand(toolId: string, command: string, args: string[]): void {
this.queueEvent({
type: 'command',
timestamp: new Date(),
toolId,
data: { command, args }
});
}
logOutput(toolId: string, output: string): void {
this.queueEvent({
type: 'output',
timestamp: new Date(),
toolId,
data: { output }
});
}
logError(toolId: string, error: string): void {
this.queueEvent({
type: 'error',
timestamp: new Date(),
toolId,
data: { error }
});
}
updateStatus(toolId: string, status: string, metadata?: any): void {
const context = this.contexts.get(toolId);
if (context) {
context.lastActivity = new Date();
if (metadata) {
context.metadata = { ...context.metadata, ...metadata };
}
}
this.queueEvent({
type: 'status',
timestamp: new Date(),
toolId,
data: { status, metadata }
});
}
private queueEvent(event: SyncEvent): void {
this.syncQueue.push(event);
// If connected, sync immediately
if (this.connected) {
this.syncEvents();
}
}
private startSync(): void {
// Sync queued events immediately
this.syncEvents();
// Set up periodic sync
this.syncTimer = setInterval(() => {
this.syncEvents();
this.syncContexts();
}, this.config.syncInterval);
}
private stopSync(): void {
if (this.syncTimer) {
clearInterval(this.syncTimer);
this.syncTimer = undefined;
}
}
private syncEvents(): void {
if (!this.connected || !this.ws || this.syncQueue.length === 0) {
return;
}
const events = [...this.syncQueue];
this.syncQueue = [];
this.ws.send(JSON.stringify({
type: 'sync_events',
events
}));
}
private syncContexts(): void {
if (!this.connected || !this.ws) {
return;
}
const contexts = Array.from(this.contexts.values());
this.ws.send(JSON.stringify({
type: 'sync_contexts',
contexts
}));
}
private handlePlatformMessage(message: any): void {
switch (message.type) {
case 'file_update':
this.handleRemoteFileUpdate(message);
break;
case 'command_request':
this.emit('command_request', message);
break;
case 'context_update':
this.handleContextUpdate(message);
break;
}
}
private handleRemoteFileUpdate(message: any): void {
if (!this.config.enableBidirectional) return;
const { toolId, filePath, content } = message.data;
const context = this.contexts.get(toolId);
if (!context) return;
const fullPath = path.join(context.workingDirectory, filePath);
// Create directory if it doesn't exist
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
// Write the file
fs.writeFileSync(fullPath, content);
this.emit('file_synced', { toolId, filePath, fullPath });
}
private handleContextUpdate(message: any): void {
const { toolId, updates } = message.data;
const context = this.contexts.get(toolId);
if (!context) return;
Object.assign(context, updates);
this.emit('context_updated', { toolId, updates });
}
getContext(toolId: string): DevToolContext | undefined {
return this.contexts.get(toolId);
}
getAllContexts(): DevToolContext[] {
return Array.from(this.contexts.values());
}
async disconnect(): Promise<void> {
this.stopSync();
// Clean up file watchers
for (const watcher of this.fileWatchers.values()) {
watcher.dispose();
}
this.fileWatchers.clear();
if (this.ws) {
this.ws.close();
this.ws = undefined;
}
this.connected = false;
}
}
+594
View File
@@ -0,0 +1,594 @@
#!/usr/bin/env node
import { Command } from 'commander';
import * as path from 'path';
import * as fs from 'fs';
import { execSync } from 'child_process';
import chalk from 'chalk';
import ora from 'ora';
import inquirer from 'inquirer';
import { DevLauncher } from '../cli-tools/platform/dev-launcher';
import { CLIToolManager } from '../cli-tools/cli-tool-manager';
import { AsyncToolWrapper } from '../cli-tools/platform/async-tool-wrapper';
import { HanzoAuth } from '../cli-tools/auth/hanzo-auth';
const program = new Command();
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'));
program
.name('dev')
.description('Dev - Meta AI development tool that manages and runs all LLMs and CLI tools')
.version(packageJson.version);
// Global auth instance
const auth = new HanzoAuth();
// Login command
program
.command('login')
.description('Login to Hanzo AI platform')
.action(async () => {
const spinner = ora('Logging in to Hanzo AI...').start();
try {
const success = await auth.login();
if (success) {
spinner.succeed('Successfully logged in to Hanzo AI!');
const creds = auth.getCredentials();
if (creds?.email) {
console.log(chalk.gray(`Logged in as: ${creds.email}`));
}
// Sync API keys
console.log(chalk.gray('Syncing API keys...'));
const apiKeys = await auth.syncAPIKeys();
console.log(chalk.green(`✓ Synced ${apiKeys.length} API keys`));
} else {
spinner.fail('Login failed');
}
} catch (error) {
spinner.fail(`Login failed: ${error.message}`);
process.exit(1);
}
});
// Logout command
program
.command('logout')
.description('Logout from Hanzo AI platform')
.action(async () => {
await auth.logout();
console.log(chalk.green('Successfully logged out'));
});
// Status command
program
.command('auth-status')
.description('Check authentication status')
.action(async () => {
if (auth.isAuthenticated()) {
const creds = auth.getCredentials();
console.log(chalk.green('✓ Authenticated'));
if (creds?.email) {
console.log(`Email: ${creds.email}`);
}
if (creds?.userId) {
console.log(`User ID: ${creds.userId}`);
}
} else {
console.log(chalk.red('✗ Not authenticated'));
console.log(chalk.gray('Run "dev login" to authenticate'));
}
});
// Initialize command
program
.command('init')
.description('Initialize Hanzo Dev in the current directory')
.option('-f, --force', 'Force initialization even if already initialized')
.action(async (options) => {
const spinner = ora('Initializing Hanzo Dev...').start();
try {
const configPath = path.join(process.cwd(), '.hanzo-dev');
if (fs.existsSync(configPath) && !options.force) {
spinner.fail('Hanzo Dev already initialized. Use --force to reinitialize.');
return;
}
// Create directory structure
fs.mkdirSync(path.join(configPath, 'logs'), { recursive: true });
fs.mkdirSync(path.join(configPath, 'async-results'), { recursive: true });
fs.mkdirSync(path.join(configPath, 'worktrees'), { recursive: true });
// Create default config
const defaultConfig = {
version: packageJson.version,
tools: {
claude: { enabled: true },
codex: { enabled: true },
gemini: { enabled: true },
openhands: { enabled: true },
aider: { enabled: true }
},
sync: {
enabled: false,
platformUrl: 'https://platform.hanzo.ai',
projectId: ''
},
defaults: {
idleTimeout: 300000, // 5 minutes
maxRuntime: 1800000, // 30 minutes
autoCommit: true
}
};
fs.writeFileSync(
path.join(configPath, 'config.json'),
JSON.stringify(defaultConfig, null, 2)
);
spinner.succeed('Hanzo Dev initialized successfully!');
console.log(chalk.gray(`Configuration saved to ${path.join(configPath, 'config.json')}}`));
} catch (error) {
spinner.fail(`Failed to initialize: ${error.message}`);
process.exit(1);
}
});
// Run command - launch a tool
program
.command('run <tool> [task...]')
.description('Run a specific AI tool (claude, codex, gemini, openhands, aider)')
.option('-a, --async', 'Run in async mode (long-running background process)')
.option('-w, --worktree', 'Create a git worktree for this session')
.option('-m, --model <model>', 'Specify the model to use')
.option('-f, --files <files...>', 'Files to include in the context')
.option('-d, --directory <dir>', 'Working directory for the tool')
.option('--no-sync', 'Disable platform sync for this session')
.action(async (tool, taskParts, options) => {
const task = taskParts.join(' ');
const spinner = ora(`Starting ${tool}...`).start();
try {
const config = loadConfig();
if (!config.tools[tool]?.enabled) {
spinner.fail(`Tool '${tool}' is not enabled or not recognized.`);
console.log(chalk.gray('Available tools: claude, codex, gemini, openhands, aider'));
return;
}
if (options.async) {
// Use async wrapper for long-running processes
const asyncWrapper = new AsyncToolWrapper({
idleTimeout: config.defaults.idleTimeout,
maxRuntime: config.defaults.maxRuntime,
persistResults: true,
resultPath: path.join(process.cwd(), '.hanzo-dev', 'async-results')
});
const jobId = await asyncWrapper.createAsyncTool(
tool,
getToolCommand(tool),
getToolArgs(tool, task, options),
{ tool, task, options }
);
spinner.succeed(`Started ${tool} in async mode`);
console.log(chalk.green(`Job ID: ${jobId}`));
console.log(chalk.gray(`Query status with: dev status ${jobId}`));
console.log(chalk.gray(`Send input with: dev input ${jobId} "your input"`));
} else {
// Use regular launcher for interactive sessions
const launcher = new DevLauncher({
maxInstances: 10,
defaultTimeout: config.defaults.maxRuntime,
gitRoot: findGitRoot() || process.cwd(),
workspacePath: options.directory || process.cwd(),
enableSync: config.sync.enabled && options.sync !== false,
syncConfig: config.sync
});
await launcher.initialize();
const instanceId = await launcher.launchTool(
tool,
task,
{
workingDirectory: options.directory,
useWorktree: options.worktree,
model: options.model,
files: options.files
}
);
spinner.succeed(`Launched ${tool}`);
console.log(chalk.green(`Instance ID: ${instanceId}`));
// Keep process alive for interactive session
process.on('SIGINT', async () => {
console.log(chalk.yellow('\nShutting down...'));
await launcher.stopTool(instanceId);
process.exit(0);
});
// Wait for tool to complete
await new Promise(() => {}); // Keep alive
}
} catch (error) {
spinner.fail(`Failed to run ${tool}: ${error.message}`);
process.exit(1);
}
});
// Compare command - run multiple tools and compare results
program
.command('compare <task>')
.description('Run the same task on multiple AI tools and compare results')
.option('-t, --tools <tools...>', 'Tools to compare (default: all)')
.option('-d, --directory <dir>', 'Working directory')
.action(async (task, options) => {
const spinner = ora('Setting up comparison...').start();
try {
const tools = options.tools || ['claude', 'codex', 'gemini', 'openhands', 'aider'];
const manager = new CLIToolManager();
await manager.initialize();
spinner.text = 'Running tools in parallel...';
const results = await manager.compareResults(task, tools);
spinner.succeed('Comparison complete!');
// Display results
console.log(chalk.bold('\nComparison Results:'));
console.log(chalk.gray('='.repeat(80)));
for (const [tool, result of results) {
console.log(chalk.bold.blue(`\n${tool.toUpperCase()}:`));
if (result.error) {
console.log(chalk.red(`Error: ${result.error}`));
} else {
console.log(result.output || 'No output');
}
console.log(chalk.gray('-'.repeat(80)));
}
} catch (error) {
spinner.fail(`Comparison failed: ${error.message}`);
process.exit(1);
}
});
// Status command - check async job status
program
.command('status [jobId]')
.description('Check status of async jobs')
.action(async (jobId) => {
try {
const asyncWrapper = new AsyncToolWrapper({
resultPath: path.join(process.cwd(), '.hanzo-dev', 'async-results')
});
if (jobId) {
// Query specific job
const result = await asyncWrapper.queryJob(jobId);
if (!result) {
console.log(chalk.red(`Job ${jobId} not found`));
return;
}
console.log(chalk.bold(`Job ${jobId}:`));
console.log(`Status: ${getStatusColor(result.status)(result.status)}`);
if (result.duration) {
console.log(`Duration: ${formatDuration(result.duration)}`);
}
console.log(`\nOutput:\n${result.output || 'No output yet'}`);
if (result.error) {
console.log(chalk.red(`\nErrors:\n${result.error}`));
}
} else {
// Show all active jobs
const activeJobs = asyncWrapper.getActiveJobs();
if (activeJobs.length === 0) {
console.log(chalk.gray('No active jobs'));
return;
}
console.log(chalk.bold('Active Jobs:'));
for (const job of activeJobs) {
console.log(`${chalk.cyan(job.id)} - ${job.toolName} (${getStatusColor(job.status)(job.status)})`);
}
}
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
process.exit(1);
}
});
// Input command - send input to async job
program
.command('input <jobId> <input>')
.description('Send input to a running async job')
.action(async (jobId, input) => {
try {
const asyncWrapper = new AsyncToolWrapper({
resultPath: path.join(process.cwd(), '.hanzo-dev', 'async-results')
});
const success = asyncWrapper.sendInput(jobId, input);
if (success) {
console.log(chalk.green('Input sent successfully'));
} else {
console.log(chalk.red('Failed to send input. Job may not be running.'));
}
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
process.exit(1);
}
});
// Install command - install CLI tools
program
.command('install [tools...]')
.description('Install AI CLI tools')
.option('-a, --all', 'Install all supported tools')
.action(async (tools, options) => {
const allTools = ['claude', 'codex', 'gemini', 'openhands', 'aider'];
const toolsToInstall = options.all ? allTools : (tools.length > 0 ? tools : allTools);
console.log(chalk.bold('Installing Hanzo Dev tools...\n'));
for (const tool of toolsToInstall) {
const spinner = ora(`Installing ${tool}...`).start();
try {
const manager = new CLIToolManager();
await manager.initialize();
spinner.succeed(`${tool} installed successfully`);
} catch (error) {
spinner.fail(`Failed to install ${tool}: ${error.message}`);
}
}
console.log(chalk.green('\n✨ Installation complete!'));
});
// Worktree command - manage git worktrees
program
.command('worktree')
.description('Manage git worktrees for parallel development')
.addCommand(
new Command('list')
.description('List all worktrees')
.action(() => {
try {
const output = execSync('git worktree list', { encoding: 'utf-8' });
console.log(output);
} catch (error) {
console.error(chalk.red('Error listing worktrees. Make sure you are in a git repository.'));
}
})
)
.addCommand(
new Command('clean')
.description('Clean up Hanzo Dev worktrees')
.action(() => {
try {
const output = execSync('git worktree list --porcelain', { encoding: 'utf-8' });
const worktrees = output.split('\n\n').filter(Boolean);
let cleaned = 0;
for (const worktree of worktrees) {
if (worktree.includes('.worktrees/')) {
const path = worktree.split('\n')[0].replace('worktree ', '');
execSync(`git worktree remove --force "${path}"`);
cleaned++;
}
}
console.log(chalk.green(`Cleaned up ${cleaned} worktrees`));
} catch (error) {
console.error(chalk.red(`Error cleaning worktrees: ${error.message}`));
}
})
);
// Interactive mode
program
.command('interactive')
.description('Start interactive mode')
.action(async () => {
console.log(chalk.bold('Welcome to Hanzo Dev Interactive Mode!\n'));
while (true) {
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{ name: 'Run a tool', value: 'run' },
{ name: 'Compare tools', value: 'compare' },
{ name: 'Check job status', value: 'status' },
{ name: 'Manage worktrees', value: 'worktree' },
{ name: 'Exit', value: 'exit' }
]
}
]);
if (action === 'exit') {
console.log(chalk.gray('Goodbye!'));
break;
}
// Handle different actions
switch (action) {
case 'run':
await interactiveRun();
break;
case 'compare':
await interactiveCompare();
break;
case 'status':
await program.parseAsync(['node', 'hanzo-dev', 'status']);
break;
case 'worktree':
await program.parseAsync(['node', 'hanzo-dev', 'worktree', 'list']);
break;
}
console.log(''); // Add spacing
}
});
// Helper functions
function loadConfig(): any {
const configPath = path.join(process.cwd(), '.hanzo-dev', 'config.json');
if (!fs.existsSync(configPath)) {
console.log(chalk.yellow('No config found. Run "hanzo-dev init" first.'));
process.exit(1);
}
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
}
function findGitRoot(): string | null {
try {
const root = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
return root;
} catch {
return null;
}
}
function getToolCommand(tool: string): string {
const commands = {
claude: 'claude',
codex: 'openai',
gemini: 'gemini',
openhands: 'openhands',
aider: 'aider'
};
return commands[tool] || tool;
}
function getToolArgs(tool: string, task: string, options: any): string[] {
const args = [];
if (options.model) {
args.push('--model', options.model);
}
if (options.files) {
options.files.forEach(file => args.push(file));
}
args.push(task);
return args;
}
function getStatusColor(status: string): (text: string) => string {
switch (status) {
case 'running':
return chalk.green;
case 'completed':
return chalk.blue;
case 'failed':
return chalk.red;
case 'idle':
return chalk.yellow;
default:
return chalk.gray;
}
}
function formatDuration(ms: number): string {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
} else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
} else {
return `${seconds}s`;
}
}
async function interactiveRun(): Promise<void> {
const answers = await inquirer.prompt([
{
type: 'list',
name: 'tool',
message: 'Which tool would you like to use?',
choices: ['claude', 'codex', 'gemini', 'openhands', 'aider']
},
{
type: 'input',
name: 'task',
message: 'What task would you like to perform?'
},
{
type: 'confirm',
name: 'async',
message: 'Run in background (async mode)?',
default: false
},
{
type: 'confirm',
name: 'worktree',
message: 'Create a separate git worktree?',
default: false
}
]);
const args = ['node', 'hanzo-dev', 'run', answers.tool, answers.task];
if (answers.async) args.push('--async');
if (answers.worktree) args.push('--worktree');
await program.parseAsync(args);
}
async function interactiveCompare(): Promise<void> {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'task',
message: 'What task would you like to compare?'
},
{
type: 'checkbox',
name: 'tools',
message: 'Which tools to compare?',
choices: ['claude', 'codex', 'gemini', 'openhands', 'aider'],
default: ['claude', 'codex', 'gemini']
}
]);
const args = ['node', 'hanzo-dev', 'compare', answers.task];
if (answers.tools.length > 0) {
args.push('--tools', ...answers.tools);
}
await program.parseAsync(args);
}
// Parse arguments
program.parse(process.argv);
// Show help if no command provided
if (!process.argv.slice(2).length) {
program.outputHelp();
}