@hanzo/dev 1.2.0 ā 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +25 -0
- package/dist/cli/dev.js +8202 -553
- package/jest.config.js +30 -0
- package/package.json +13 -1
- package/src/cli/dev.ts +456 -106
- package/src/lib/agent-loop.ts +552 -0
- package/src/lib/code-act-agent.ts +378 -0
- package/src/lib/config.ts +163 -0
- package/src/lib/editor.ts +368 -0
- package/src/lib/function-calling.ts +318 -0
- package/src/lib/mcp-client.ts +259 -0
- package/src/lib/peer-agent-network.ts +584 -0
- package/src/lib/unified-workspace.ts +435 -0
- package/tests/browser-integration.test.ts +242 -0
- package/tests/code-act-agent.test.ts +305 -0
- package/tests/editor.test.ts +223 -0
- package/tests/mcp-client.test.ts +238 -0
- package/tests/peer-agent-network.test.ts +340 -0
- package/tests/setup.ts +25 -0
- package/tests/swe-bench.test.ts +357 -0
- package/tsconfig.json +13 -15
package/src/cli/dev.ts
CHANGED
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
|
-
import { spawn } from 'child_process';
|
|
5
|
+
import { spawn, execSync } from 'child_process';
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import * as os from 'os';
|
|
9
|
+
import { FileEditor } from '../lib/editor';
|
|
10
|
+
import { MCPClient, DEFAULT_MCP_SERVERS } from '../lib/mcp-client';
|
|
11
|
+
import { FunctionCallingSystem } from '../lib/function-calling';
|
|
12
|
+
import { ConfigManager } from '../lib/config';
|
|
13
|
+
import { CodeActAgent } from '../lib/code-act-agent';
|
|
14
|
+
import { UnifiedWorkspace, WorkspaceSession } from '../lib/unified-workspace';
|
|
15
|
+
import { PeerAgentNetwork } from '../lib/peer-agent-network';
|
|
9
16
|
|
|
10
17
|
const program = new Command();
|
|
11
18
|
|
|
@@ -23,7 +30,6 @@ function loadEnvFiles(): void {
|
|
|
23
30
|
if (match) {
|
|
24
31
|
const key = match[1].trim();
|
|
25
32
|
const value = match[2].trim();
|
|
26
|
-
// Only set if not already in environment
|
|
27
33
|
if (!process.env[key]) {
|
|
28
34
|
process.env[key] = value;
|
|
29
35
|
}
|
|
@@ -36,8 +42,28 @@ function loadEnvFiles(): void {
|
|
|
36
42
|
// Load env files on startup
|
|
37
43
|
loadEnvFiles();
|
|
38
44
|
|
|
39
|
-
//
|
|
45
|
+
// Check if uvx is available
|
|
46
|
+
function hasUvx(): boolean {
|
|
47
|
+
try {
|
|
48
|
+
execSync('which uvx', { stdio: 'ignore' });
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Available tools configuration
|
|
40
56
|
const TOOLS = {
|
|
57
|
+
'hanzo-dev': {
|
|
58
|
+
name: 'Hanzo Dev (OpenHands)',
|
|
59
|
+
command: hasUvx() ? 'uvx hanzo-dev' : 'hanzo-dev',
|
|
60
|
+
checkCommand: hasUvx() ? 'which uvx' : 'which hanzo-dev',
|
|
61
|
+
description: 'Hanzo AI software development agent - Full featured dev environment',
|
|
62
|
+
color: chalk.magenta,
|
|
63
|
+
apiKeys: ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'LLM_API_KEY', 'HANZO_API_KEY'],
|
|
64
|
+
priority: 1,
|
|
65
|
+
isDefault: true
|
|
66
|
+
},
|
|
41
67
|
claude: {
|
|
42
68
|
name: 'Claude (Anthropic)',
|
|
43
69
|
command: 'claude-code',
|
|
@@ -45,7 +71,7 @@ const TOOLS = {
|
|
|
45
71
|
description: 'Claude Code - AI coding assistant',
|
|
46
72
|
color: chalk.blue,
|
|
47
73
|
apiKeys: ['ANTHROPIC_API_KEY', 'CLAUDE_API_KEY'],
|
|
48
|
-
priority:
|
|
74
|
+
priority: 2
|
|
49
75
|
},
|
|
50
76
|
aider: {
|
|
51
77
|
name: 'Aider',
|
|
@@ -54,15 +80,6 @@ const TOOLS = {
|
|
|
54
80
|
description: 'AI pair programming in your terminal',
|
|
55
81
|
color: chalk.green,
|
|
56
82
|
apiKeys: ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'CLAUDE_API_KEY'],
|
|
57
|
-
priority: 2
|
|
58
|
-
},
|
|
59
|
-
openhands: {
|
|
60
|
-
name: 'OpenHands',
|
|
61
|
-
command: 'openhands',
|
|
62
|
-
checkCommand: 'which openhands',
|
|
63
|
-
description: 'AI software development agent',
|
|
64
|
-
color: chalk.magenta,
|
|
65
|
-
apiKeys: ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'LLM_API_KEY'],
|
|
66
83
|
priority: 3
|
|
67
84
|
},
|
|
68
85
|
gemini: {
|
|
@@ -104,7 +121,7 @@ async function isToolInstalled(tool: string): Promise<boolean> {
|
|
|
104
121
|
});
|
|
105
122
|
}
|
|
106
123
|
|
|
107
|
-
// Get list of available tools
|
|
124
|
+
// Get list of available tools
|
|
108
125
|
async function getAvailableTools(): Promise<string[]> {
|
|
109
126
|
const available: string[] = [];
|
|
110
127
|
for (const toolKey of Object.keys(TOOLS)) {
|
|
@@ -114,7 +131,6 @@ async function getAvailableTools(): Promise<string[]> {
|
|
|
114
131
|
available.push(toolKey);
|
|
115
132
|
}
|
|
116
133
|
}
|
|
117
|
-
// Sort by priority
|
|
118
134
|
return available.sort((a, b) => {
|
|
119
135
|
const priorityA = TOOLS[a as keyof typeof TOOLS].priority;
|
|
120
136
|
const priorityB = TOOLS[b as keyof typeof TOOLS].priority;
|
|
@@ -122,23 +138,25 @@ async function getAvailableTools(): Promise<string[]> {
|
|
|
122
138
|
});
|
|
123
139
|
}
|
|
124
140
|
|
|
125
|
-
// Get default tool
|
|
141
|
+
// Get default tool
|
|
126
142
|
async function getDefaultTool(): Promise<string | null> {
|
|
127
143
|
const availableTools = await getAvailableTools();
|
|
128
144
|
if (availableTools.length === 0) return null;
|
|
129
145
|
|
|
130
|
-
|
|
146
|
+
if (availableTools.includes('hanzo-dev')) {
|
|
147
|
+
return 'hanzo-dev';
|
|
148
|
+
}
|
|
149
|
+
|
|
131
150
|
for (const tool of availableTools) {
|
|
132
151
|
if (await isToolInstalled(tool) && hasApiKey(tool)) {
|
|
133
152
|
return tool;
|
|
134
153
|
}
|
|
135
154
|
}
|
|
136
155
|
|
|
137
|
-
// Otherwise return first available
|
|
138
156
|
return availableTools[0];
|
|
139
157
|
}
|
|
140
158
|
|
|
141
|
-
// Run a tool
|
|
159
|
+
// Run a tool
|
|
142
160
|
function runTool(tool: string, args: string[] = []): void {
|
|
143
161
|
const toolConfig = TOOLS[tool as keyof typeof TOOLS];
|
|
144
162
|
if (!toolConfig) {
|
|
@@ -148,19 +166,24 @@ function runTool(tool: string, args: string[] = []): void {
|
|
|
148
166
|
|
|
149
167
|
console.log(toolConfig.color(`\nš Launching ${toolConfig.name}...\n`));
|
|
150
168
|
|
|
169
|
+
if (tool === 'hanzo-dev' && hasUvx()) {
|
|
170
|
+
console.log(chalk.gray('Using uvx to run hanzo-dev...'));
|
|
171
|
+
}
|
|
172
|
+
|
|
151
173
|
const child = spawn(toolConfig.command, args, {
|
|
152
174
|
stdio: 'inherit',
|
|
153
175
|
shell: true,
|
|
154
|
-
env: process.env
|
|
176
|
+
env: process.env
|
|
155
177
|
});
|
|
156
178
|
|
|
157
179
|
child.on('error', (error) => {
|
|
158
180
|
console.error(chalk.red(`Failed to start ${toolConfig.name}: ${error.message}`));
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
181
|
+
|
|
182
|
+
if (tool === 'hanzo-dev') {
|
|
183
|
+
console.log(chalk.yellow('\nTo install hanzo-dev:'));
|
|
184
|
+
console.log(chalk.gray(' pip install hanzo-dev'));
|
|
185
|
+
console.log(chalk.gray(' # or'));
|
|
186
|
+
console.log(chalk.gray(' uvx hanzo-dev # (if you have uv installed)'));
|
|
164
187
|
}
|
|
165
188
|
process.exit(1);
|
|
166
189
|
});
|
|
@@ -173,11 +196,220 @@ function runTool(tool: string, args: string[] = []): void {
|
|
|
173
196
|
});
|
|
174
197
|
}
|
|
175
198
|
|
|
176
|
-
// Interactive mode
|
|
199
|
+
// Interactive editing mode using built-in editor
|
|
200
|
+
async function interactiveEditMode(): Promise<void> {
|
|
201
|
+
const editor = new FileEditor();
|
|
202
|
+
const functionCalling = new FunctionCallingSystem();
|
|
203
|
+
const mcpClient = new MCPClient();
|
|
204
|
+
|
|
205
|
+
console.log(chalk.bold.cyan('\nš Hanzo Dev Editor - Interactive Mode\n'));
|
|
206
|
+
console.log(chalk.gray('Commands: view, create, str_replace, insert, undo_edit, run, list, search, mcp, help, exit\n'));
|
|
207
|
+
|
|
208
|
+
// Connect to default MCP servers if available
|
|
209
|
+
for (const serverConfig of DEFAULT_MCP_SERVERS) {
|
|
210
|
+
try {
|
|
211
|
+
console.log(chalk.gray(`Connecting to MCP server: ${serverConfig.name}...`));
|
|
212
|
+
const session = await mcpClient.connect(serverConfig);
|
|
213
|
+
await functionCalling.registerMCPServer(serverConfig.name, session);
|
|
214
|
+
console.log(chalk.green(`ā Connected to ${serverConfig.name}`));
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.log(chalk.yellow(`ā Could not connect to ${serverConfig.name}`));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
while (true) {
|
|
221
|
+
const { command } = await inquirer.prompt([{
|
|
222
|
+
type: 'input',
|
|
223
|
+
name: 'command',
|
|
224
|
+
message: chalk.green('editor>'),
|
|
225
|
+
prefix: ''
|
|
226
|
+
}]);
|
|
227
|
+
|
|
228
|
+
if (command === 'exit' || command === 'quit') {
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (command === 'help') {
|
|
233
|
+
console.log(chalk.cyan('\nAvailable commands:'));
|
|
234
|
+
console.log(' view <file> [start] [end] - View file contents');
|
|
235
|
+
console.log(' create <file> - Create new file');
|
|
236
|
+
console.log(' str_replace <file> - Replace string in file');
|
|
237
|
+
console.log(' insert <file> <line> - Insert line in file');
|
|
238
|
+
console.log(' undo_edit <file> - Undo last edit');
|
|
239
|
+
console.log(' run <command> - Run shell command');
|
|
240
|
+
console.log(' list <directory> - List directory contents');
|
|
241
|
+
console.log(' search <pattern> [path] - Search for files');
|
|
242
|
+
console.log(' mcp - List MCP tools');
|
|
243
|
+
console.log(' tools - List all available tools');
|
|
244
|
+
console.log(' help - Show this help');
|
|
245
|
+
console.log(' exit - Exit editor\n');
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (command === 'tools') {
|
|
250
|
+
const tools = functionCalling.getAvailableTools();
|
|
251
|
+
console.log(chalk.cyan('\nAvailable tools:'));
|
|
252
|
+
tools.forEach(tool => {
|
|
253
|
+
console.log(` ${chalk.yellow(tool.name)} - ${tool.description}`);
|
|
254
|
+
});
|
|
255
|
+
console.log();
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (command === 'mcp') {
|
|
260
|
+
const sessions = mcpClient.getAllSessions();
|
|
261
|
+
console.log(chalk.cyan('\nMCP Sessions:'));
|
|
262
|
+
sessions.forEach(session => {
|
|
263
|
+
console.log(` ${chalk.yellow(session.id)}:`);
|
|
264
|
+
session.tools.forEach(tool => {
|
|
265
|
+
console.log(` - ${tool.name}: ${tool.description}`);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
console.log();
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Parse and execute commands
|
|
273
|
+
const parts = command.split(' ');
|
|
274
|
+
const cmd = parts[0];
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
let result;
|
|
278
|
+
|
|
279
|
+
switch (cmd) {
|
|
280
|
+
case 'view':
|
|
281
|
+
result = await editor.execute({
|
|
282
|
+
command: 'view',
|
|
283
|
+
path: parts[1],
|
|
284
|
+
startLine: parts[2] ? parseInt(parts[2]) : undefined,
|
|
285
|
+
endLine: parts[3] ? parseInt(parts[3]) : undefined
|
|
286
|
+
});
|
|
287
|
+
break;
|
|
288
|
+
|
|
289
|
+
case 'create':
|
|
290
|
+
const { content } = await inquirer.prompt([{
|
|
291
|
+
type: 'editor',
|
|
292
|
+
name: 'content',
|
|
293
|
+
message: 'Enter file content:'
|
|
294
|
+
}]);
|
|
295
|
+
result = await editor.execute({
|
|
296
|
+
command: 'create',
|
|
297
|
+
path: parts[1],
|
|
298
|
+
content
|
|
299
|
+
});
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
case 'str_replace':
|
|
303
|
+
const { oldStr } = await inquirer.prompt([{
|
|
304
|
+
type: 'input',
|
|
305
|
+
name: 'oldStr',
|
|
306
|
+
message: 'String to replace:'
|
|
307
|
+
}]);
|
|
308
|
+
const { newStr } = await inquirer.prompt([{
|
|
309
|
+
type: 'input',
|
|
310
|
+
name: 'newStr',
|
|
311
|
+
message: 'Replacement string:'
|
|
312
|
+
}]);
|
|
313
|
+
result = await editor.execute({
|
|
314
|
+
command: 'str_replace',
|
|
315
|
+
path: parts[1],
|
|
316
|
+
oldStr,
|
|
317
|
+
newStr
|
|
318
|
+
});
|
|
319
|
+
break;
|
|
320
|
+
|
|
321
|
+
case 'insert':
|
|
322
|
+
const { lineContent } = await inquirer.prompt([{
|
|
323
|
+
type: 'input',
|
|
324
|
+
name: 'lineContent',
|
|
325
|
+
message: 'Line content:'
|
|
326
|
+
}]);
|
|
327
|
+
result = await editor.execute({
|
|
328
|
+
command: 'insert',
|
|
329
|
+
path: parts[1],
|
|
330
|
+
lineNumber: parseInt(parts[2]),
|
|
331
|
+
content: lineContent
|
|
332
|
+
});
|
|
333
|
+
break;
|
|
334
|
+
|
|
335
|
+
case 'undo_edit':
|
|
336
|
+
result = await editor.execute({
|
|
337
|
+
command: 'undo_edit',
|
|
338
|
+
path: parts[1]
|
|
339
|
+
});
|
|
340
|
+
break;
|
|
341
|
+
|
|
342
|
+
case 'run':
|
|
343
|
+
const runCommand = parts.slice(1).join(' ');
|
|
344
|
+
result = await functionCalling.callFunction({
|
|
345
|
+
id: Date.now().toString(),
|
|
346
|
+
name: 'run_command',
|
|
347
|
+
arguments: { command: runCommand }
|
|
348
|
+
});
|
|
349
|
+
break;
|
|
350
|
+
|
|
351
|
+
case 'list':
|
|
352
|
+
result = await functionCalling.callFunction({
|
|
353
|
+
id: Date.now().toString(),
|
|
354
|
+
name: 'list_directory',
|
|
355
|
+
arguments: { path: parts[1] || '.' }
|
|
356
|
+
});
|
|
357
|
+
break;
|
|
358
|
+
|
|
359
|
+
case 'search':
|
|
360
|
+
result = await functionCalling.callFunction({
|
|
361
|
+
id: Date.now().toString(),
|
|
362
|
+
name: 'search_files',
|
|
363
|
+
arguments: {
|
|
364
|
+
pattern: parts[1],
|
|
365
|
+
path: parts[2] || '.',
|
|
366
|
+
regex: false
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
break;
|
|
370
|
+
|
|
371
|
+
default:
|
|
372
|
+
console.log(chalk.red(`Unknown command: ${cmd}`));
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (result) {
|
|
377
|
+
if (result.success || result.result?.success) {
|
|
378
|
+
console.log(chalk.green('ā'), result.message || 'Success');
|
|
379
|
+
if (result.content || result.result?.stdout) {
|
|
380
|
+
console.log(result.content || result.result.stdout);
|
|
381
|
+
}
|
|
382
|
+
if (result.result?.files) {
|
|
383
|
+
result.result.files.forEach((file: any) => {
|
|
384
|
+
const icon = file.type === 'directory' ? 'š' : 'š';
|
|
385
|
+
console.log(` ${icon} ${file.name}`);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (result.result?.matches) {
|
|
389
|
+
console.log(`Found ${result.result.total} matches:`);
|
|
390
|
+
result.result.matches.forEach((match: string) => {
|
|
391
|
+
console.log(` š ${match}`);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
console.log(chalk.red('ā'), result.message || result.error || 'Error');
|
|
396
|
+
if (result.result?.stderr) {
|
|
397
|
+
console.log(chalk.red(result.result.stderr));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.log(chalk.red('Error:'), error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
console.log(chalk.gray('\nExiting editor mode...'));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Interactive mode for tool selection
|
|
177
410
|
async function interactiveMode(): Promise<void> {
|
|
178
411
|
console.log(chalk.bold.cyan('\nš¤ Hanzo Dev - AI Development Assistant\n'));
|
|
179
412
|
|
|
180
|
-
// Show detected environment files
|
|
181
413
|
const envFiles = ['.env', '.env.local', '.env.development', '.env.production'];
|
|
182
414
|
const detectedEnvFiles = envFiles.filter(file => fs.existsSync(path.join(process.cwd(), file)));
|
|
183
415
|
if (detectedEnvFiles.length > 0) {
|
|
@@ -191,41 +423,34 @@ async function interactiveMode(): Promise<void> {
|
|
|
191
423
|
const availableTools = await getAvailableTools();
|
|
192
424
|
const defaultTool = await getDefaultTool();
|
|
193
425
|
|
|
194
|
-
if (availableTools.length === 0) {
|
|
426
|
+
if (availableTools.length === 0 && !hasApiKey('hanzo-dev')) {
|
|
195
427
|
console.log(chalk.yellow('No AI tools available. Please either:'));
|
|
196
|
-
console.log(chalk.yellow('\n1. Install
|
|
428
|
+
console.log(chalk.yellow('\n1. Install hanzo-dev (recommended):'));
|
|
429
|
+
console.log(chalk.gray(' pip install hanzo-dev'));
|
|
430
|
+
console.log(chalk.gray(' # or'));
|
|
431
|
+
console.log(chalk.gray(' uvx hanzo-dev'));
|
|
432
|
+
|
|
433
|
+
console.log(chalk.yellow('\n2. Install other tools:'));
|
|
197
434
|
console.log(chalk.gray(' npm install -g @hanzo/claude-code'));
|
|
198
435
|
console.log(chalk.gray(' pip install aider-chat'));
|
|
199
|
-
console.log(chalk.gray(' pip install openhands'));
|
|
200
436
|
|
|
201
|
-
console.log(chalk.yellow('\
|
|
437
|
+
console.log(chalk.yellow('\n3. Or configure API keys in your .env file:'));
|
|
202
438
|
console.log(chalk.gray(' ANTHROPIC_API_KEY=sk-ant-...'));
|
|
203
439
|
console.log(chalk.gray(' OPENAI_API_KEY=sk-...'));
|
|
204
|
-
console.log(chalk.gray(' GOOGLE_API_KEY=...'));
|
|
205
440
|
process.exit(1);
|
|
206
441
|
}
|
|
207
442
|
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
let hasAnyKey = false;
|
|
216
|
-
allApiKeys.forEach(key => {
|
|
217
|
-
if (process.env[key]) {
|
|
218
|
-
console.log(chalk.green(` ā ${key}`));
|
|
219
|
-
hasAnyKey = true;
|
|
443
|
+
// Add built-in editor option
|
|
444
|
+
const choices = [
|
|
445
|
+
{
|
|
446
|
+
name: chalk.bold.yellow('š§ Built-in Editor - Interactive file editing and MCP tools'),
|
|
447
|
+
value: 'builtin-editor',
|
|
448
|
+
short: 'Built-in Editor'
|
|
220
449
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (!hasAnyKey) {
|
|
224
|
-
console.log(chalk.gray(' (none detected)'));
|
|
225
|
-
}
|
|
226
|
-
console.log();
|
|
450
|
+
];
|
|
227
451
|
|
|
228
|
-
|
|
452
|
+
// Add external tools
|
|
453
|
+
for (const tool of availableTools) {
|
|
229
454
|
const toolConfig = TOOLS[tool as keyof typeof TOOLS];
|
|
230
455
|
const isInstalled = await isToolInstalled(tool);
|
|
231
456
|
const hasKey = hasApiKey(tool);
|
|
@@ -239,24 +464,32 @@ async function interactiveMode(): Promise<void> {
|
|
|
239
464
|
status = chalk.cyan(' [API Key Only]');
|
|
240
465
|
}
|
|
241
466
|
|
|
242
|
-
|
|
467
|
+
if (toolConfig.isDefault) {
|
|
468
|
+
status += chalk.bold.magenta(' ā
DEFAULT');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
choices.push({
|
|
243
472
|
name: `${toolConfig.name} - ${toolConfig.description}${status}`,
|
|
244
473
|
value: tool,
|
|
245
474
|
short: toolConfig.name
|
|
246
|
-
};
|
|
247
|
-
}
|
|
475
|
+
});
|
|
476
|
+
}
|
|
248
477
|
|
|
249
478
|
const { selectedTool } = await inquirer.prompt([
|
|
250
479
|
{
|
|
251
480
|
type: 'list',
|
|
252
481
|
name: 'selectedTool',
|
|
253
|
-
message: 'Select
|
|
482
|
+
message: 'Select a tool to launch:',
|
|
254
483
|
choices: choices,
|
|
255
|
-
default: defaultTool
|
|
484
|
+
default: defaultTool || 'builtin-editor'
|
|
256
485
|
}
|
|
257
486
|
]);
|
|
258
487
|
|
|
259
|
-
|
|
488
|
+
if (selectedTool === 'builtin-editor') {
|
|
489
|
+
await interactiveEditMode();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
260
493
|
const { passDirectory } = await inquirer.prompt([
|
|
261
494
|
{
|
|
262
495
|
type: 'confirm',
|
|
@@ -270,23 +503,53 @@ async function interactiveMode(): Promise<void> {
|
|
|
270
503
|
runTool(selectedTool, args);
|
|
271
504
|
}
|
|
272
505
|
|
|
273
|
-
// Setup version
|
|
506
|
+
// Setup version
|
|
274
507
|
const packagePath = path.join(__dirname, '../../package.json');
|
|
275
|
-
let version = '
|
|
508
|
+
let version = '2.0.0';
|
|
276
509
|
try {
|
|
277
510
|
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
|
|
278
511
|
version = packageJson.version;
|
|
279
512
|
} catch (error) {
|
|
280
|
-
// Use default version
|
|
513
|
+
// Use default version
|
|
281
514
|
}
|
|
282
515
|
|
|
283
516
|
program
|
|
284
517
|
.name('dev')
|
|
285
|
-
.description('Hanzo Dev - Meta AI development CLI
|
|
518
|
+
.description('Hanzo Dev - Meta AI development CLI with built-in editor and tool orchestration')
|
|
286
519
|
.version(version);
|
|
287
520
|
|
|
288
|
-
//
|
|
521
|
+
// Built-in editor command
|
|
522
|
+
program
|
|
523
|
+
.command('edit [path]')
|
|
524
|
+
.description('Launch built-in editor with file editing and MCP tools')
|
|
525
|
+
.action(async (path) => {
|
|
526
|
+
if (path && fs.existsSync(path)) {
|
|
527
|
+
process.chdir(path);
|
|
528
|
+
}
|
|
529
|
+
await interactiveEditMode();
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// External tool commands
|
|
289
533
|
Object.entries(TOOLS).forEach(([toolKey, toolConfig]) => {
|
|
534
|
+
if (toolKey === 'hanzo-dev') {
|
|
535
|
+
// Special alias for hanzo-dev
|
|
536
|
+
program
|
|
537
|
+
.command('python [args...]')
|
|
538
|
+
.description('Launch Python hanzo-dev (OpenHands)')
|
|
539
|
+
.action(async (args) => {
|
|
540
|
+
const isInstalled = await isToolInstalled('hanzo-dev');
|
|
541
|
+
if (!isInstalled && !hasUvx()) {
|
|
542
|
+
console.error(chalk.red('Hanzo Dev is not installed.'));
|
|
543
|
+
console.log(chalk.yellow('\nTo install:'));
|
|
544
|
+
console.log(chalk.gray(' pip install hanzo-dev'));
|
|
545
|
+
console.log(chalk.gray(' # or'));
|
|
546
|
+
console.log(chalk.gray(' pip install uv && uvx hanzo-dev'));
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
runTool('hanzo-dev', args);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
290
553
|
program
|
|
291
554
|
.command(`${toolKey} [args...]`)
|
|
292
555
|
.description(`Launch ${toolConfig.name}`)
|
|
@@ -296,25 +559,14 @@ Object.entries(TOOLS).forEach(([toolKey, toolConfig]) => {
|
|
|
296
559
|
|
|
297
560
|
if (!isInstalled && !hasKey) {
|
|
298
561
|
console.error(chalk.red(`${toolConfig.name} is not available.`));
|
|
299
|
-
console.log(chalk.yellow(`\nTo use ${toolConfig.name}, you need to either:`));
|
|
300
|
-
console.log(chalk.yellow('1. Install it:'));
|
|
301
|
-
console.log(chalk.gray(` Follow installation instructions for ${toolConfig.name}`));
|
|
302
|
-
console.log(chalk.yellow('\n2. Configure API keys:'));
|
|
303
|
-
toolConfig.apiKeys?.forEach(key => {
|
|
304
|
-
console.log(chalk.gray(` ${key}=your-api-key`));
|
|
305
|
-
});
|
|
306
562
|
process.exit(1);
|
|
307
563
|
}
|
|
308
564
|
|
|
309
|
-
if (!isInstalled) {
|
|
310
|
-
console.log(chalk.yellow(`Note: ${toolConfig.name} is not installed locally, using API mode.`));
|
|
311
|
-
}
|
|
312
|
-
|
|
313
565
|
runTool(toolKey, args);
|
|
314
566
|
});
|
|
315
567
|
});
|
|
316
568
|
|
|
317
|
-
// List
|
|
569
|
+
// List command
|
|
318
570
|
program
|
|
319
571
|
.command('list')
|
|
320
572
|
.alias('ls')
|
|
@@ -322,7 +574,6 @@ program
|
|
|
322
574
|
.action(async () => {
|
|
323
575
|
console.log(chalk.bold.cyan('\nš AI Tools Status:\n'));
|
|
324
576
|
|
|
325
|
-
// Show environment files
|
|
326
577
|
const envFiles = ['.env', '.env.local', '.env.development', '.env.production'];
|
|
327
578
|
const detectedEnvFiles = envFiles.filter(file => fs.existsSync(path.join(process.cwd(), file)));
|
|
328
579
|
if (detectedEnvFiles.length > 0) {
|
|
@@ -333,34 +584,27 @@ program
|
|
|
333
584
|
console.log();
|
|
334
585
|
}
|
|
335
586
|
|
|
336
|
-
|
|
337
|
-
console.log(chalk.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
let hasAnyKey = false;
|
|
344
|
-
allApiKeys.forEach(key => {
|
|
345
|
-
if (process.env[key]) {
|
|
346
|
-
console.log(chalk.green(` ā ${key} = ${process.env[key]?.substring(0, 10)}...`));
|
|
347
|
-
hasAnyKey = true;
|
|
348
|
-
}
|
|
349
|
-
});
|
|
587
|
+
console.log(chalk.bold('Built-in Features:'));
|
|
588
|
+
console.log(chalk.green(' ā Interactive Editor') + chalk.gray(' - File editing with view, create, str_replace'));
|
|
589
|
+
console.log(chalk.green(' ā MCP Client') + chalk.gray(' - Model Context Protocol tool integration'));
|
|
590
|
+
console.log(chalk.green(' ā Function Calling') + chalk.gray(' - Unified tool interface'));
|
|
591
|
+
console.log();
|
|
350
592
|
|
|
351
|
-
if (
|
|
352
|
-
console.log(chalk.
|
|
593
|
+
if (hasUvx()) {
|
|
594
|
+
console.log(chalk.bold('Package Manager:'));
|
|
595
|
+
console.log(chalk.green(' ā uvx available (can run Python tools without installation)'));
|
|
596
|
+
console.log();
|
|
353
597
|
}
|
|
354
|
-
console.log();
|
|
355
598
|
|
|
356
|
-
|
|
357
|
-
console.log(chalk.bold('Tools:'));
|
|
599
|
+
console.log(chalk.bold('External Tools:'));
|
|
358
600
|
for (const [toolKey, toolConfig] of Object.entries(TOOLS)) {
|
|
359
601
|
const isInstalled = await isToolInstalled(toolKey);
|
|
360
602
|
const hasKey = hasApiKey(toolKey);
|
|
361
603
|
|
|
362
604
|
let status = chalk.red('ā Not Available');
|
|
363
|
-
if (
|
|
605
|
+
if (toolKey === 'hanzo-dev' && hasUvx()) {
|
|
606
|
+
status = chalk.green('ā Ready (via uvx)');
|
|
607
|
+
} else if (isInstalled && hasKey) {
|
|
364
608
|
status = chalk.green('ā Ready (Installed + API Key)');
|
|
365
609
|
} else if (isInstalled) {
|
|
366
610
|
status = chalk.yellow('ā Installed (No API Key)');
|
|
@@ -368,11 +612,13 @@ program
|
|
|
368
612
|
status = chalk.cyan('ā API Mode (Not Installed)');
|
|
369
613
|
}
|
|
370
614
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
console.log(chalk.gray(` Requires: ${toolConfig.apiKeys.join(' or ')}`));
|
|
615
|
+
let displayName = toolConfig.color(toolConfig.name);
|
|
616
|
+
if (toolConfig.isDefault) {
|
|
617
|
+
displayName += chalk.bold.magenta(' ā
');
|
|
375
618
|
}
|
|
619
|
+
|
|
620
|
+
console.log(` ${status} ${displayName}`);
|
|
621
|
+
console.log(chalk.gray(` ${toolConfig.description}`));
|
|
376
622
|
}
|
|
377
623
|
});
|
|
378
624
|
|
|
@@ -381,13 +627,19 @@ program
|
|
|
381
627
|
.command('status')
|
|
382
628
|
.description('Show current working directory and environment')
|
|
383
629
|
.action(() => {
|
|
630
|
+
const config = new ConfigManager();
|
|
631
|
+
|
|
384
632
|
console.log(chalk.bold.cyan('\nš Hanzo Dev Status\n'));
|
|
385
633
|
console.log(`Current Directory: ${chalk.green(process.cwd())}`);
|
|
386
634
|
console.log(`User: ${chalk.green(os.userInfo().username)}`);
|
|
387
635
|
console.log(`Node Version: ${chalk.green(process.version)}`);
|
|
388
636
|
console.log(`Platform: ${chalk.green(os.platform())}`);
|
|
637
|
+
console.log(`Dev Version: ${chalk.green(version)}`);
|
|
638
|
+
|
|
639
|
+
if (hasUvx()) {
|
|
640
|
+
console.log(`UV/UVX: ${chalk.green('ā Available')}`);
|
|
641
|
+
}
|
|
389
642
|
|
|
390
|
-
// Show environment files
|
|
391
643
|
const envFiles = ['.env', '.env.local', '.env.development', '.env.production'];
|
|
392
644
|
const detectedEnvFiles = envFiles.filter(file => fs.existsSync(path.join(process.cwd(), file)));
|
|
393
645
|
if (detectedEnvFiles.length > 0) {
|
|
@@ -396,18 +648,116 @@ program
|
|
|
396
648
|
console.log(chalk.green(` ā ${file}`));
|
|
397
649
|
});
|
|
398
650
|
}
|
|
651
|
+
|
|
652
|
+
console.log(`\nConfiguration:`);
|
|
653
|
+
const cfg = config.getConfig();
|
|
654
|
+
console.log(` Default Agent: ${chalk.yellow(cfg.defaultAgent)}`);
|
|
655
|
+
console.log(` Runtime: ${chalk.yellow(cfg.runtime || 'cli')}`);
|
|
656
|
+
console.log(` Confirmation Mode: ${chalk.yellow(cfg.security.confirmationMode ? 'On' : 'Off')}`);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// Workspace command - unified workspace with shell, editor, browser, planner
|
|
660
|
+
program
|
|
661
|
+
.command('workspace')
|
|
662
|
+
.alias('ws')
|
|
663
|
+
.description('Launch unified workspace with shell, editor, browser, and planner')
|
|
664
|
+
.action(async () => {
|
|
665
|
+
const session = new WorkspaceSession();
|
|
666
|
+
await session.start();
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// Swarm command - spawn multiple agents for parallel work
|
|
670
|
+
program
|
|
671
|
+
.command('swarm [path]')
|
|
672
|
+
.description('Spawn agent swarm for codebase (one agent per file/directory)')
|
|
673
|
+
.option('-t, --type <type>', 'Agent type (claude-code, aider, openhands)', 'claude-code')
|
|
674
|
+
.option('-s, --strategy <strategy>', 'Assignment strategy (one-per-file, one-per-directory, by-complexity)', 'one-per-file')
|
|
675
|
+
.action(async (path, options) => {
|
|
676
|
+
const targetPath = path || process.cwd();
|
|
677
|
+
const network = new PeerAgentNetwork();
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
await network.spawnAgentsForCodebase(targetPath, options.type, options.strategy);
|
|
681
|
+
|
|
682
|
+
// Show network status
|
|
683
|
+
const status = network.getNetworkStatus();
|
|
684
|
+
console.log(chalk.cyan('\nš Network Status:'));
|
|
685
|
+
console.log(` Agents: ${status.totalAgents} (${status.activeAgents} active)`);
|
|
686
|
+
console.log(` Connections: ${status.totalConnections}`);
|
|
687
|
+
|
|
688
|
+
// Interactive swarm control
|
|
689
|
+
const { action } = await inquirer.prompt([{
|
|
690
|
+
type: 'list',
|
|
691
|
+
name: 'action',
|
|
692
|
+
message: 'What would you like to do?',
|
|
693
|
+
choices: [
|
|
694
|
+
{ name: 'Execute task on swarm', value: 'task' },
|
|
695
|
+
{ name: 'Run parallel tasks', value: 'parallel' },
|
|
696
|
+
{ name: 'Show network status', value: 'status' },
|
|
697
|
+
{ name: 'Exit', value: 'exit' }
|
|
698
|
+
]
|
|
699
|
+
}]);
|
|
700
|
+
|
|
701
|
+
if (action === 'task') {
|
|
702
|
+
const { task } = await inquirer.prompt([{
|
|
703
|
+
type: 'input',
|
|
704
|
+
name: 'task',
|
|
705
|
+
message: 'Enter task for swarm:'
|
|
706
|
+
}]);
|
|
707
|
+
|
|
708
|
+
await network.coordinateSwarm(task);
|
|
709
|
+
} else if (action === 'parallel') {
|
|
710
|
+
console.log(chalk.yellow('Enter tasks (one per line, empty line to finish):'));
|
|
711
|
+
const tasks: Array<{task: string}> = [];
|
|
712
|
+
|
|
713
|
+
while (true) {
|
|
714
|
+
const { task } = await inquirer.prompt([{
|
|
715
|
+
type: 'input',
|
|
716
|
+
name: 'task',
|
|
717
|
+
message: '>'
|
|
718
|
+
}]);
|
|
719
|
+
|
|
720
|
+
if (!task) break;
|
|
721
|
+
tasks.push({ task });
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (tasks.length > 0) {
|
|
725
|
+
await network.executeParallelTasks(tasks);
|
|
726
|
+
}
|
|
727
|
+
} else if (action === 'status') {
|
|
728
|
+
const status = network.getNetworkStatus();
|
|
729
|
+
console.log(chalk.cyan('\nš Detailed Network Status:'));
|
|
730
|
+
console.log(JSON.stringify(status, null, 2));
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
await network.cleanup();
|
|
734
|
+
} catch (error) {
|
|
735
|
+
console.error(chalk.red(`Swarm error: ${error}`));
|
|
736
|
+
await network.cleanup();
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
// Agent command - run a task with CodeAct agent
|
|
741
|
+
program
|
|
742
|
+
.command('agent <task>')
|
|
743
|
+
.description('Execute a task using CodeAct agent with automatic planning and error correction')
|
|
744
|
+
.action(async (task) => {
|
|
745
|
+
const agent = new CodeActAgent();
|
|
746
|
+
try {
|
|
747
|
+
await agent.executeTask(task);
|
|
748
|
+
} catch (error) {
|
|
749
|
+
console.error(chalk.red(`Agent error: ${error}`));
|
|
750
|
+
}
|
|
399
751
|
});
|
|
400
752
|
|
|
401
|
-
// Default action
|
|
753
|
+
// Default action
|
|
402
754
|
program
|
|
403
755
|
.action(async () => {
|
|
404
756
|
const defaultTool = await getDefaultTool();
|
|
405
757
|
if (defaultTool && process.argv.length === 2) {
|
|
406
|
-
|
|
407
|
-
console.log(chalk.gray(`Auto-selecting ${TOOLS[defaultTool as keyof typeof TOOLS].name} based on available API keys...`));
|
|
758
|
+
console.log(chalk.gray(`Auto-launching ${TOOLS[defaultTool as keyof typeof TOOLS].name}...`));
|
|
408
759
|
runTool(defaultTool, ['.']);
|
|
409
760
|
} else {
|
|
410
|
-
// Otherwise show interactive mode
|
|
411
761
|
interactiveMode();
|
|
412
762
|
}
|
|
413
763
|
});
|
|
@@ -415,12 +765,12 @@ program
|
|
|
415
765
|
// Parse arguments
|
|
416
766
|
program.parse();
|
|
417
767
|
|
|
418
|
-
// If no arguments
|
|
768
|
+
// If no arguments, run interactive mode
|
|
419
769
|
if (process.argv.length === 2) {
|
|
420
770
|
(async () => {
|
|
421
771
|
const defaultTool = await getDefaultTool();
|
|
422
772
|
if (defaultTool) {
|
|
423
|
-
console.log(chalk.gray(`Auto-
|
|
773
|
+
console.log(chalk.gray(`Auto-launching ${TOOLS[defaultTool as keyof typeof TOOLS].name}...`));
|
|
424
774
|
runTool(defaultTool, ['.']);
|
|
425
775
|
} else {
|
|
426
776
|
interactiveMode();
|