@hanzo/dev 1.2.0 → 2.1.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/src/cli/dev.ts CHANGED
@@ -2,10 +2,20 @@
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';
16
+ import { BenchmarkRunner, BenchmarkConfig } from '../lib/benchmark-runner';
17
+ import { ConfigurableAgentLoop, LLMProvider } from '../lib/agent-loop';
18
+ import { SwarmRunner, SwarmOptions } from '../lib/swarm-runner';
9
19
 
10
20
  const program = new Command();
11
21
 
@@ -23,7 +33,6 @@ function loadEnvFiles(): void {
23
33
  if (match) {
24
34
  const key = match[1].trim();
25
35
  const value = match[2].trim();
26
- // Only set if not already in environment
27
36
  if (!process.env[key]) {
28
37
  process.env[key] = value;
29
38
  }
@@ -36,8 +45,28 @@ function loadEnvFiles(): void {
36
45
  // Load env files on startup
37
46
  loadEnvFiles();
38
47
 
39
- // Available tools configuration with API key detection
48
+ // Check if uvx is available
49
+ function hasUvx(): boolean {
50
+ try {
51
+ execSync('which uvx', { stdio: 'ignore' });
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ // Available tools configuration
40
59
  const TOOLS = {
60
+ 'hanzo-dev': {
61
+ name: 'Hanzo Dev (OpenHands)',
62
+ command: hasUvx() ? 'uvx hanzo-dev' : 'hanzo-dev',
63
+ checkCommand: hasUvx() ? 'which uvx' : 'which hanzo-dev',
64
+ description: 'Hanzo AI software development agent - Full featured dev environment',
65
+ color: chalk.magenta,
66
+ apiKeys: ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'LLM_API_KEY', 'HANZO_API_KEY'],
67
+ priority: 1,
68
+ isDefault: true
69
+ },
41
70
  claude: {
42
71
  name: 'Claude (Anthropic)',
43
72
  command: 'claude-code',
@@ -45,7 +74,7 @@ const TOOLS = {
45
74
  description: 'Claude Code - AI coding assistant',
46
75
  color: chalk.blue,
47
76
  apiKeys: ['ANTHROPIC_API_KEY', 'CLAUDE_API_KEY'],
48
- priority: 1
77
+ priority: 2
49
78
  },
50
79
  aider: {
51
80
  name: 'Aider',
@@ -54,15 +83,6 @@ const TOOLS = {
54
83
  description: 'AI pair programming in your terminal',
55
84
  color: chalk.green,
56
85
  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
86
  priority: 3
67
87
  },
68
88
  gemini: {
@@ -104,7 +124,7 @@ async function isToolInstalled(tool: string): Promise<boolean> {
104
124
  });
105
125
  }
106
126
 
107
- // Get list of available tools (installed OR has API key)
127
+ // Get list of available tools
108
128
  async function getAvailableTools(): Promise<string[]> {
109
129
  const available: string[] = [];
110
130
  for (const toolKey of Object.keys(TOOLS)) {
@@ -114,7 +134,6 @@ async function getAvailableTools(): Promise<string[]> {
114
134
  available.push(toolKey);
115
135
  }
116
136
  }
117
- // Sort by priority
118
137
  return available.sort((a, b) => {
119
138
  const priorityA = TOOLS[a as keyof typeof TOOLS].priority;
120
139
  const priorityB = TOOLS[b as keyof typeof TOOLS].priority;
@@ -122,23 +141,25 @@ async function getAvailableTools(): Promise<string[]> {
122
141
  });
123
142
  }
124
143
 
125
- // Get default tool based on API keys and installation
144
+ // Get default tool
126
145
  async function getDefaultTool(): Promise<string | null> {
127
146
  const availableTools = await getAvailableTools();
128
147
  if (availableTools.length === 0) return null;
129
148
 
130
- // Prefer tools that have both API key and are installed
149
+ if (availableTools.includes('hanzo-dev')) {
150
+ return 'hanzo-dev';
151
+ }
152
+
131
153
  for (const tool of availableTools) {
132
154
  if (await isToolInstalled(tool) && hasApiKey(tool)) {
133
155
  return tool;
134
156
  }
135
157
  }
136
158
 
137
- // Otherwise return first available
138
159
  return availableTools[0];
139
160
  }
140
161
 
141
- // Run a tool with optional arguments
162
+ // Run a tool
142
163
  function runTool(tool: string, args: string[] = []): void {
143
164
  const toolConfig = TOOLS[tool as keyof typeof TOOLS];
144
165
  if (!toolConfig) {
@@ -148,19 +169,24 @@ function runTool(tool: string, args: string[] = []): void {
148
169
 
149
170
  console.log(toolConfig.color(`\n🚀 Launching ${toolConfig.name}...\n`));
150
171
 
172
+ if (tool === 'hanzo-dev' && hasUvx()) {
173
+ console.log(chalk.gray('Using uvx to run hanzo-dev...'));
174
+ }
175
+
151
176
  const child = spawn(toolConfig.command, args, {
152
177
  stdio: 'inherit',
153
178
  shell: true,
154
- env: process.env // Pass through all environment variables
179
+ env: process.env
155
180
  });
156
181
 
157
182
  child.on('error', (error) => {
158
183
  console.error(chalk.red(`Failed to start ${toolConfig.name}: ${error.message}`));
159
- if (!hasApiKey(tool)) {
160
- console.log(chalk.yellow(`\nMake sure you have one of these API keys configured:`));
161
- toolConfig.apiKeys?.forEach(key => {
162
- console.log(chalk.gray(` - ${key}`));
163
- });
184
+
185
+ if (tool === 'hanzo-dev') {
186
+ console.log(chalk.yellow('\nTo install hanzo-dev:'));
187
+ console.log(chalk.gray(' pip install hanzo-dev'));
188
+ console.log(chalk.gray(' # or'));
189
+ console.log(chalk.gray(' uvx hanzo-dev # (if you have uv installed)'));
164
190
  }
165
191
  process.exit(1);
166
192
  });
@@ -173,11 +199,220 @@ function runTool(tool: string, args: string[] = []): void {
173
199
  });
174
200
  }
175
201
 
176
- // Interactive mode
202
+ // Interactive editing mode using built-in editor
203
+ async function interactiveEditMode(): Promise<void> {
204
+ const editor = new FileEditor();
205
+ const functionCalling = new FunctionCallingSystem();
206
+ const mcpClient = new MCPClient();
207
+
208
+ console.log(chalk.bold.cyan('\n📝 Hanzo Dev Editor - Interactive Mode\n'));
209
+ console.log(chalk.gray('Commands: view, create, str_replace, insert, undo_edit, run, list, search, mcp, help, exit\n'));
210
+
211
+ // Connect to default MCP servers if available
212
+ for (const serverConfig of DEFAULT_MCP_SERVERS) {
213
+ try {
214
+ console.log(chalk.gray(`Connecting to MCP server: ${serverConfig.name}...`));
215
+ const session = await mcpClient.connect(serverConfig);
216
+ await functionCalling.registerMCPServer(serverConfig.name, session);
217
+ console.log(chalk.green(`✓ Connected to ${serverConfig.name}`));
218
+ } catch (error) {
219
+ console.log(chalk.yellow(`⚠ Could not connect to ${serverConfig.name}`));
220
+ }
221
+ }
222
+
223
+ while (true) {
224
+ const { command } = await inquirer.prompt([{
225
+ type: 'input',
226
+ name: 'command',
227
+ message: chalk.green('editor>'),
228
+ prefix: ''
229
+ }]);
230
+
231
+ if (command === 'exit' || command === 'quit') {
232
+ break;
233
+ }
234
+
235
+ if (command === 'help') {
236
+ console.log(chalk.cyan('\nAvailable commands:'));
237
+ console.log(' view <file> [start] [end] - View file contents');
238
+ console.log(' create <file> - Create new file');
239
+ console.log(' str_replace <file> - Replace string in file');
240
+ console.log(' insert <file> <line> - Insert line in file');
241
+ console.log(' undo_edit <file> - Undo last edit');
242
+ console.log(' run <command> - Run shell command');
243
+ console.log(' list <directory> - List directory contents');
244
+ console.log(' search <pattern> [path] - Search for files');
245
+ console.log(' mcp - List MCP tools');
246
+ console.log(' tools - List all available tools');
247
+ console.log(' help - Show this help');
248
+ console.log(' exit - Exit editor\n');
249
+ continue;
250
+ }
251
+
252
+ if (command === 'tools') {
253
+ const tools = functionCalling.getAvailableTools();
254
+ console.log(chalk.cyan('\nAvailable tools:'));
255
+ tools.forEach(tool => {
256
+ console.log(` ${chalk.yellow(tool.name)} - ${tool.description}`);
257
+ });
258
+ console.log();
259
+ continue;
260
+ }
261
+
262
+ if (command === 'mcp') {
263
+ const sessions = mcpClient.getAllSessions();
264
+ console.log(chalk.cyan('\nMCP Sessions:'));
265
+ sessions.forEach(session => {
266
+ console.log(` ${chalk.yellow(session.id)}:`);
267
+ session.tools.forEach(tool => {
268
+ console.log(` - ${tool.name}: ${tool.description}`);
269
+ });
270
+ });
271
+ console.log();
272
+ continue;
273
+ }
274
+
275
+ // Parse and execute commands
276
+ const parts = command.split(' ');
277
+ const cmd = parts[0];
278
+
279
+ try {
280
+ let result;
281
+
282
+ switch (cmd) {
283
+ case 'view':
284
+ result = await editor.execute({
285
+ command: 'view',
286
+ path: parts[1],
287
+ startLine: parts[2] ? parseInt(parts[2]) : undefined,
288
+ endLine: parts[3] ? parseInt(parts[3]) : undefined
289
+ });
290
+ break;
291
+
292
+ case 'create':
293
+ const { content } = await inquirer.prompt([{
294
+ type: 'editor',
295
+ name: 'content',
296
+ message: 'Enter file content:'
297
+ }]);
298
+ result = await editor.execute({
299
+ command: 'create',
300
+ path: parts[1],
301
+ content
302
+ });
303
+ break;
304
+
305
+ case 'str_replace':
306
+ const { oldStr } = await inquirer.prompt([{
307
+ type: 'input',
308
+ name: 'oldStr',
309
+ message: 'String to replace:'
310
+ }]);
311
+ const { newStr } = await inquirer.prompt([{
312
+ type: 'input',
313
+ name: 'newStr',
314
+ message: 'Replacement string:'
315
+ }]);
316
+ result = await editor.execute({
317
+ command: 'str_replace',
318
+ path: parts[1],
319
+ oldStr,
320
+ newStr
321
+ });
322
+ break;
323
+
324
+ case 'insert':
325
+ const { lineContent } = await inquirer.prompt([{
326
+ type: 'input',
327
+ name: 'lineContent',
328
+ message: 'Line content:'
329
+ }]);
330
+ result = await editor.execute({
331
+ command: 'insert',
332
+ path: parts[1],
333
+ lineNumber: parseInt(parts[2]),
334
+ content: lineContent
335
+ });
336
+ break;
337
+
338
+ case 'undo_edit':
339
+ result = await editor.execute({
340
+ command: 'undo_edit',
341
+ path: parts[1]
342
+ });
343
+ break;
344
+
345
+ case 'run':
346
+ const runCommand = parts.slice(1).join(' ');
347
+ result = await functionCalling.callFunction({
348
+ id: Date.now().toString(),
349
+ name: 'run_command',
350
+ arguments: { command: runCommand }
351
+ });
352
+ break;
353
+
354
+ case 'list':
355
+ result = await functionCalling.callFunction({
356
+ id: Date.now().toString(),
357
+ name: 'list_directory',
358
+ arguments: { path: parts[1] || '.' }
359
+ });
360
+ break;
361
+
362
+ case 'search':
363
+ result = await functionCalling.callFunction({
364
+ id: Date.now().toString(),
365
+ name: 'search_files',
366
+ arguments: {
367
+ pattern: parts[1],
368
+ path: parts[2] || '.',
369
+ regex: false
370
+ }
371
+ });
372
+ break;
373
+
374
+ default:
375
+ console.log(chalk.red(`Unknown command: ${cmd}`));
376
+ continue;
377
+ }
378
+
379
+ if (result) {
380
+ if (result.success || result.result?.success) {
381
+ console.log(chalk.green('✓'), result.message || 'Success');
382
+ if (result.content || result.result?.stdout) {
383
+ console.log(result.content || result.result.stdout);
384
+ }
385
+ if (result.result?.files) {
386
+ result.result.files.forEach((file: any) => {
387
+ const icon = file.type === 'directory' ? '📁' : '📄';
388
+ console.log(` ${icon} ${file.name}`);
389
+ });
390
+ }
391
+ if (result.result?.matches) {
392
+ console.log(`Found ${result.result.total} matches:`);
393
+ result.result.matches.forEach((match: string) => {
394
+ console.log(` 📄 ${match}`);
395
+ });
396
+ }
397
+ } else {
398
+ console.log(chalk.red('✗'), result.message || result.error || 'Error');
399
+ if (result.result?.stderr) {
400
+ console.log(chalk.red(result.result.stderr));
401
+ }
402
+ }
403
+ }
404
+ } catch (error) {
405
+ console.log(chalk.red('Error:'), error);
406
+ }
407
+ }
408
+
409
+ console.log(chalk.gray('\nExiting editor mode...'));
410
+ }
411
+
412
+ // Interactive mode for tool selection
177
413
  async function interactiveMode(): Promise<void> {
178
414
  console.log(chalk.bold.cyan('\n🤖 Hanzo Dev - AI Development Assistant\n'));
179
415
 
180
- // Show detected environment files
181
416
  const envFiles = ['.env', '.env.local', '.env.development', '.env.production'];
182
417
  const detectedEnvFiles = envFiles.filter(file => fs.existsSync(path.join(process.cwd(), file)));
183
418
  if (detectedEnvFiles.length > 0) {
@@ -191,41 +426,34 @@ async function interactiveMode(): Promise<void> {
191
426
  const availableTools = await getAvailableTools();
192
427
  const defaultTool = await getDefaultTool();
193
428
 
194
- if (availableTools.length === 0) {
429
+ if (availableTools.length === 0 && !hasApiKey('hanzo-dev')) {
195
430
  console.log(chalk.yellow('No AI tools available. Please either:'));
196
- console.log(chalk.yellow('\n1. Install a tool:'));
431
+ console.log(chalk.yellow('\n1. Install hanzo-dev (recommended):'));
432
+ console.log(chalk.gray(' pip install hanzo-dev'));
433
+ console.log(chalk.gray(' # or'));
434
+ console.log(chalk.gray(' uvx hanzo-dev'));
435
+
436
+ console.log(chalk.yellow('\n2. Install other tools:'));
197
437
  console.log(chalk.gray(' npm install -g @hanzo/claude-code'));
198
438
  console.log(chalk.gray(' pip install aider-chat'));
199
- console.log(chalk.gray(' pip install openhands'));
200
439
 
201
- console.log(chalk.yellow('\n2. Or configure API keys in your .env file:'));
440
+ console.log(chalk.yellow('\n3. Or configure API keys in your .env file:'));
202
441
  console.log(chalk.gray(' ANTHROPIC_API_KEY=sk-ant-...'));
203
442
  console.log(chalk.gray(' OPENAI_API_KEY=sk-...'));
204
- console.log(chalk.gray(' GOOGLE_API_KEY=...'));
205
443
  process.exit(1);
206
444
  }
207
445
 
208
- // Show available API keys
209
- console.log(chalk.gray('🔑 Detected API keys:'));
210
- const allApiKeys = new Set<string>();
211
- Object.values(TOOLS).forEach(tool => {
212
- tool.apiKeys?.forEach(key => allApiKeys.add(key));
213
- });
214
-
215
- let hasAnyKey = false;
216
- allApiKeys.forEach(key => {
217
- if (process.env[key]) {
218
- console.log(chalk.green(` ✓ ${key}`));
219
- hasAnyKey = true;
446
+ // Add built-in editor option
447
+ const choices = [
448
+ {
449
+ name: chalk.bold.yellow('🔧 Built-in Editor - Interactive file editing and MCP tools'),
450
+ value: 'builtin-editor',
451
+ short: 'Built-in Editor'
220
452
  }
221
- });
222
-
223
- if (!hasAnyKey) {
224
- console.log(chalk.gray(' (none detected)'));
225
- }
226
- console.log();
453
+ ];
227
454
 
228
- const choices = await Promise.all(availableTools.map(async tool => {
455
+ // Add external tools
456
+ for (const tool of availableTools) {
229
457
  const toolConfig = TOOLS[tool as keyof typeof TOOLS];
230
458
  const isInstalled = await isToolInstalled(tool);
231
459
  const hasKey = hasApiKey(tool);
@@ -239,24 +467,32 @@ async function interactiveMode(): Promise<void> {
239
467
  status = chalk.cyan(' [API Key Only]');
240
468
  }
241
469
 
242
- return {
470
+ if (toolConfig.isDefault) {
471
+ status += chalk.bold.magenta(' ★ DEFAULT');
472
+ }
473
+
474
+ choices.push({
243
475
  name: `${toolConfig.name} - ${toolConfig.description}${status}`,
244
476
  value: tool,
245
477
  short: toolConfig.name
246
- };
247
- }));
478
+ });
479
+ }
248
480
 
249
481
  const { selectedTool } = await inquirer.prompt([
250
482
  {
251
483
  type: 'list',
252
484
  name: 'selectedTool',
253
- message: 'Select an AI tool to launch:',
485
+ message: 'Select a tool to launch:',
254
486
  choices: choices,
255
- default: defaultTool
487
+ default: defaultTool || 'builtin-editor'
256
488
  }
257
489
  ]);
258
490
 
259
- // Check if we should pass the current directory
491
+ if (selectedTool === 'builtin-editor') {
492
+ await interactiveEditMode();
493
+ return;
494
+ }
495
+
260
496
  const { passDirectory } = await inquirer.prompt([
261
497
  {
262
498
  type: 'confirm',
@@ -270,23 +506,53 @@ async function interactiveMode(): Promise<void> {
270
506
  runTool(selectedTool, args);
271
507
  }
272
508
 
273
- // Setup version from package.json
509
+ // Setup version
274
510
  const packagePath = path.join(__dirname, '../../package.json');
275
- let version = '1.2.0';
511
+ let version = '2.0.0';
276
512
  try {
277
513
  const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
278
514
  version = packageJson.version;
279
515
  } catch (error) {
280
- // Use default version if package.json not found
516
+ // Use default version
281
517
  }
282
518
 
283
519
  program
284
520
  .name('dev')
285
- .description('Hanzo Dev - Meta AI development CLI that manages and runs all AI coding assistants')
521
+ .description('Hanzo Dev - Meta AI development CLI with built-in editor and tool orchestration')
286
522
  .version(version);
287
523
 
288
- // Direct tool commands
524
+ // Built-in editor command
525
+ program
526
+ .command('edit [path]')
527
+ .description('Launch built-in editor with file editing and MCP tools')
528
+ .action(async (path) => {
529
+ if (path && fs.existsSync(path)) {
530
+ process.chdir(path);
531
+ }
532
+ await interactiveEditMode();
533
+ });
534
+
535
+ // External tool commands
289
536
  Object.entries(TOOLS).forEach(([toolKey, toolConfig]) => {
537
+ if (toolKey === 'hanzo-dev') {
538
+ // Special alias for hanzo-dev
539
+ program
540
+ .command('python [args...]')
541
+ .description('Launch Python hanzo-dev (OpenHands)')
542
+ .action(async (args) => {
543
+ const isInstalled = await isToolInstalled('hanzo-dev');
544
+ if (!isInstalled && !hasUvx()) {
545
+ console.error(chalk.red('Hanzo Dev is not installed.'));
546
+ console.log(chalk.yellow('\nTo install:'));
547
+ console.log(chalk.gray(' pip install hanzo-dev'));
548
+ console.log(chalk.gray(' # or'));
549
+ console.log(chalk.gray(' pip install uv && uvx hanzo-dev'));
550
+ process.exit(1);
551
+ }
552
+ runTool('hanzo-dev', args);
553
+ });
554
+ }
555
+
290
556
  program
291
557
  .command(`${toolKey} [args...]`)
292
558
  .description(`Launch ${toolConfig.name}`)
@@ -296,25 +562,14 @@ Object.entries(TOOLS).forEach(([toolKey, toolConfig]) => {
296
562
 
297
563
  if (!isInstalled && !hasKey) {
298
564
  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
565
  process.exit(1);
307
566
  }
308
567
 
309
- if (!isInstalled) {
310
- console.log(chalk.yellow(`Note: ${toolConfig.name} is not installed locally, using API mode.`));
311
- }
312
-
313
568
  runTool(toolKey, args);
314
569
  });
315
570
  });
316
571
 
317
- // List installed tools
572
+ // List command
318
573
  program
319
574
  .command('list')
320
575
  .alias('ls')
@@ -322,7 +577,6 @@ program
322
577
  .action(async () => {
323
578
  console.log(chalk.bold.cyan('\n📋 AI Tools Status:\n'));
324
579
 
325
- // Show environment files
326
580
  const envFiles = ['.env', '.env.local', '.env.development', '.env.production'];
327
581
  const detectedEnvFiles = envFiles.filter(file => fs.existsSync(path.join(process.cwd(), file)));
328
582
  if (detectedEnvFiles.length > 0) {
@@ -333,34 +587,27 @@ program
333
587
  console.log();
334
588
  }
335
589
 
336
- // Show API keys
337
- console.log(chalk.bold('API Keys:'));
338
- const allApiKeys = new Set<string>();
339
- Object.values(TOOLS).forEach(tool => {
340
- tool.apiKeys?.forEach(key => allApiKeys.add(key));
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
- });
590
+ console.log(chalk.bold('Built-in Features:'));
591
+ console.log(chalk.green(' Interactive Editor') + chalk.gray(' - File editing with view, create, str_replace'));
592
+ console.log(chalk.green(' ✓ MCP Client') + chalk.gray(' - Model Context Protocol tool integration'));
593
+ console.log(chalk.green(' ✓ Function Calling') + chalk.gray(' - Unified tool interface'));
594
+ console.log();
350
595
 
351
- if (!hasAnyKey) {
352
- console.log(chalk.gray(' (no API keys detected)'));
596
+ if (hasUvx()) {
597
+ console.log(chalk.bold('Package Manager:'));
598
+ console.log(chalk.green(' ✓ uvx available (can run Python tools without installation)'));
599
+ console.log();
353
600
  }
354
- console.log();
355
601
 
356
- // Show tools
357
- console.log(chalk.bold('Tools:'));
602
+ console.log(chalk.bold('External Tools:'));
358
603
  for (const [toolKey, toolConfig] of Object.entries(TOOLS)) {
359
604
  const isInstalled = await isToolInstalled(toolKey);
360
605
  const hasKey = hasApiKey(toolKey);
361
606
 
362
607
  let status = chalk.red('✗ Not Available');
363
- if (isInstalled && hasKey) {
608
+ if (toolKey === 'hanzo-dev' && hasUvx()) {
609
+ status = chalk.green('✓ Ready (via uvx)');
610
+ } else if (isInstalled && hasKey) {
364
611
  status = chalk.green('✓ Ready (Installed + API Key)');
365
612
  } else if (isInstalled) {
366
613
  status = chalk.yellow('⚠ Installed (No API Key)');
@@ -368,11 +615,13 @@ program
368
615
  status = chalk.cyan('☁ API Mode (Not Installed)');
369
616
  }
370
617
 
371
- console.log(` ${status} ${toolConfig.color(toolConfig.name)}`);
372
- console.log(chalk.gray(` ${toolConfig.description}`));
373
- if (!hasKey && toolConfig.apiKeys) {
374
- console.log(chalk.gray(` Requires: ${toolConfig.apiKeys.join(' or ')}`));
618
+ let displayName = toolConfig.color(toolConfig.name);
619
+ if (toolConfig.isDefault) {
620
+ displayName += chalk.bold.magenta(' ★');
375
621
  }
622
+
623
+ console.log(` ${status} ${displayName}`);
624
+ console.log(chalk.gray(` ${toolConfig.description}`));
376
625
  }
377
626
  });
378
627
 
@@ -381,13 +630,19 @@ program
381
630
  .command('status')
382
631
  .description('Show current working directory and environment')
383
632
  .action(() => {
633
+ const config = new ConfigManager();
634
+
384
635
  console.log(chalk.bold.cyan('\n📊 Hanzo Dev Status\n'));
385
636
  console.log(`Current Directory: ${chalk.green(process.cwd())}`);
386
637
  console.log(`User: ${chalk.green(os.userInfo().username)}`);
387
638
  console.log(`Node Version: ${chalk.green(process.version)}`);
388
639
  console.log(`Platform: ${chalk.green(os.platform())}`);
640
+ console.log(`Dev Version: ${chalk.green(version)}`);
641
+
642
+ if (hasUvx()) {
643
+ console.log(`UV/UVX: ${chalk.green('✓ Available')}`);
644
+ }
389
645
 
390
- // Show environment files
391
646
  const envFiles = ['.env', '.env.local', '.env.development', '.env.production'];
392
647
  const detectedEnvFiles = envFiles.filter(file => fs.existsSync(path.join(process.cwd(), file)));
393
648
  if (detectedEnvFiles.length > 0) {
@@ -396,18 +651,280 @@ program
396
651
  console.log(chalk.green(` ✓ ${file}`));
397
652
  });
398
653
  }
654
+
655
+ console.log(`\nConfiguration:`);
656
+ const cfg = config.getConfig();
657
+ console.log(` Default Agent: ${chalk.yellow(cfg.defaultAgent)}`);
658
+ console.log(` Runtime: ${chalk.yellow(cfg.runtime || 'cli')}`);
659
+ console.log(` Confirmation Mode: ${chalk.yellow(cfg.security.confirmationMode ? 'On' : 'Off')}`);
399
660
  });
400
661
 
401
- // Default action - run default tool or interactive mode
662
+ // Workspace command - unified workspace with shell, editor, browser, planner
402
663
  program
664
+ .command('workspace')
665
+ .alias('ws')
666
+ .description('Launch unified workspace with shell, editor, browser, and planner')
403
667
  .action(async () => {
668
+ const session = new WorkspaceSession();
669
+ await session.start();
670
+ });
671
+
672
+ // Swarm command - spawn multiple agents for parallel work
673
+ program
674
+ .command('swarm [path]')
675
+ .description('Spawn agent swarm for codebase (one agent per file/directory)')
676
+ .option('-t, --type <type>', 'Agent type (claude-code, aider, openhands)', 'claude-code')
677
+ .option('-s, --strategy <strategy>', 'Assignment strategy (one-per-file, one-per-directory, by-complexity)', 'one-per-file')
678
+ .action(async (path, options) => {
679
+ const targetPath = path || process.cwd();
680
+ const network = new PeerAgentNetwork();
681
+
682
+ try {
683
+ await network.spawnAgentsForCodebase(targetPath, options.type, options.strategy);
684
+
685
+ // Show network status
686
+ const status = network.getNetworkStatus();
687
+ console.log(chalk.cyan('\n📊 Network Status:'));
688
+ console.log(` Agents: ${status.totalAgents} (${status.activeAgents} active)`);
689
+ console.log(` Connections: ${status.totalConnections}`);
690
+
691
+ // Interactive swarm control
692
+ const { action } = await inquirer.prompt([{
693
+ type: 'list',
694
+ name: 'action',
695
+ message: 'What would you like to do?',
696
+ choices: [
697
+ { name: 'Execute task on swarm', value: 'task' },
698
+ { name: 'Run parallel tasks', value: 'parallel' },
699
+ { name: 'Show network status', value: 'status' },
700
+ { name: 'Exit', value: 'exit' }
701
+ ]
702
+ }]);
703
+
704
+ if (action === 'task') {
705
+ const { task } = await inquirer.prompt([{
706
+ type: 'input',
707
+ name: 'task',
708
+ message: 'Enter task for swarm:'
709
+ }]);
710
+
711
+ await network.coordinateSwarm(task);
712
+ } else if (action === 'parallel') {
713
+ console.log(chalk.yellow('Enter tasks (one per line, empty line to finish):'));
714
+ const tasks: Array<{task: string}> = [];
715
+
716
+ while (true) {
717
+ const { task } = await inquirer.prompt([{
718
+ type: 'input',
719
+ name: 'task',
720
+ message: '>'
721
+ }]);
722
+
723
+ if (!task) break;
724
+ tasks.push({ task });
725
+ }
726
+
727
+ if (tasks.length > 0) {
728
+ await network.executeParallelTasks(tasks);
729
+ }
730
+ } else if (action === 'status') {
731
+ const status = network.getNetworkStatus();
732
+ console.log(chalk.cyan('\n📊 Detailed Network Status:'));
733
+ console.log(JSON.stringify(status, null, 2));
734
+ }
735
+
736
+ await network.cleanup();
737
+ } catch (error) {
738
+ console.error(chalk.red(`Swarm error: ${error}`));
739
+ await network.cleanup();
740
+ }
741
+ });
742
+
743
+ // Agent command - run a task with CodeAct agent
744
+ program
745
+ .command('agent <task>')
746
+ .description('Execute a task using CodeAct agent with automatic planning and error correction')
747
+ .action(async (task) => {
748
+ const agent = new CodeActAgent();
749
+ try {
750
+ await agent.executeTask(task);
751
+ } catch (error) {
752
+ console.error(chalk.red(`Agent error: ${error}`));
753
+ }
754
+ });
755
+
756
+ // Benchmark command - run SWE-bench evaluation
757
+ program
758
+ .command('benchmark')
759
+ .alias('bench')
760
+ .description('Run SWE-bench evaluation to measure performance')
761
+ .option('-d, --dataset <dataset>', 'Dataset to use (swe-bench, swe-bench-lite, custom)', 'swe-bench-lite')
762
+ .option('-a, --agents <number>', 'Number of agents for parallel execution', '5')
763
+ .option('-p, --parallel', 'Run tasks in parallel', true)
764
+ .option('-t, --timeout <ms>', 'Timeout per task in milliseconds', '300000')
765
+ .option('-o, --output <file>', 'Output file for results', 'benchmark-results.json')
766
+ .option('--provider <provider>', 'LLM provider (claude, openai, gemini, local)')
767
+ .option('--max-tasks <number>', 'Maximum number of tasks to run')
768
+ .action(async (options) => {
769
+ console.log(chalk.bold.cyan('\n🏃 Starting Hanzo Dev Benchmark\n'));
770
+
771
+ // Parse options
772
+ const config: BenchmarkConfig = {
773
+ dataset: options.dataset as any,
774
+ agents: parseInt(options.agents),
775
+ parallel: options.parallel !== 'false',
776
+ timeout: parseInt(options.timeout),
777
+ output: options.output,
778
+ maxTasks: options.maxTasks ? parseInt(options.maxTasks) : undefined
779
+ };
780
+
781
+ // Set provider if specified
782
+ if (options.provider) {
783
+ const providers = ConfigurableAgentLoop.getAvailableProviders();
784
+ const provider = providers.find(p =>
785
+ p.type === options.provider ||
786
+ p.name.toLowerCase().includes(options.provider.toLowerCase())
787
+ );
788
+
789
+ if (provider) {
790
+ config.provider = provider;
791
+ } else {
792
+ console.error(chalk.red(`Provider '${options.provider}' not found or not configured`));
793
+ console.log(chalk.yellow('\nAvailable providers:'));
794
+ providers.forEach(p => {
795
+ console.log(` - ${p.name} (${p.type})`);
796
+ });
797
+ process.exit(1);
798
+ }
799
+ }
800
+
801
+ // Run benchmark
802
+ const runner = new BenchmarkRunner(config);
803
+
804
+ try {
805
+ await runner.run();
806
+ console.log(chalk.green('\n✅ Benchmark completed successfully'));
807
+ } catch (error) {
808
+ console.error(chalk.red(`\n❌ Benchmark failed: ${error}`));
809
+ process.exit(1);
810
+ }
811
+ });
812
+
813
+ // Add global options for provider and swarm
814
+ program
815
+ .option('--claude', 'Use Claude AI provider')
816
+ .option('--openai', 'Use OpenAI provider')
817
+ .option('--gemini', 'Use Gemini provider')
818
+ .option('--grok', 'Use Grok provider')
819
+ .option('--local', 'Use local AI provider')
820
+ .option('--swarm <count>', 'Launch swarm of agents (up to 100)')
821
+ .option('-p, --prompt <prompt>', 'Task prompt for agents');
822
+
823
+ // Swarm mode function
824
+ async function runSwarmMode(options: any): Promise<void> {
825
+ // Determine provider
826
+ let provider: SwarmOptions['provider'] = 'claude';
827
+ if (options.claude) provider = 'claude';
828
+ else if (options.openai) provider = 'openai';
829
+ else if (options.gemini) provider = 'gemini';
830
+ else if (options.grok) provider = 'grok';
831
+ else if (options.local) provider = 'local';
832
+
833
+ // Parse swarm count
834
+ const count = Math.min(parseInt(options.swarm) || 5, 100);
835
+
836
+ if (!options.prompt) {
837
+ console.error(chalk.red('Error: --prompt is required when using --swarm'));
838
+ process.exit(1);
839
+ }
840
+
841
+ const swarmOptions: SwarmOptions = {
842
+ provider,
843
+ count,
844
+ prompt: options.prompt,
845
+ cwd: process.cwd(),
846
+ autoLogin: true
847
+ };
848
+
849
+ console.log(chalk.bold.cyan(`\n🐝 Hanzo Dev Swarm Mode\n`));
850
+ console.log(chalk.gray(`Provider: ${provider}`));
851
+ console.log(chalk.gray(`Agents: ${count}`));
852
+ console.log(chalk.gray(`Prompt: ${options.prompt}\n`));
853
+
854
+ const runner = new SwarmRunner(swarmOptions);
855
+
856
+ // Check authentication
857
+ const hasAuth = await runner.ensureProviderAuth();
858
+ if (!hasAuth) {
859
+ console.error(chalk.red(`\nError: ${provider} is not authenticated`));
860
+ console.log(chalk.yellow('\nTo authenticate:'));
861
+
862
+ switch (provider) {
863
+ case 'claude':
864
+ console.log(chalk.gray(' 1. Set ANTHROPIC_API_KEY environment variable'));
865
+ console.log(chalk.gray(' 2. Run: claude login'));
866
+ break;
867
+ case 'openai':
868
+ console.log(chalk.gray(' Set OPENAI_API_KEY environment variable'));
869
+ break;
870
+ case 'gemini':
871
+ console.log(chalk.gray(' Set GOOGLE_API_KEY or GEMINI_API_KEY environment variable'));
872
+ break;
873
+ case 'grok':
874
+ console.log(chalk.gray(' Set GROK_API_KEY environment variable'));
875
+ break;
876
+ }
877
+ process.exit(1);
878
+ }
879
+
880
+ try {
881
+ await runner.run();
882
+ } catch (error) {
883
+ console.error(chalk.red(`\nSwarm error: ${error}`));
884
+ process.exit(1);
885
+ }
886
+ }
887
+
888
+ // Default action
889
+ program
890
+ .action(async (options) => {
891
+ // Check if swarm mode is requested
892
+ if (options.swarm) {
893
+ await runSwarmMode(options);
894
+ return;
895
+ }
896
+
897
+ // Check if a specific provider is requested
898
+ if (options.claude || options.openai || options.gemini || options.grok || options.local) {
899
+ let provider = 'claude';
900
+ if (options.claude) provider = 'claude';
901
+ else if (options.openai) provider = 'openai';
902
+ else if (options.gemini) provider = 'gemini';
903
+ else if (options.grok) provider = 'grok';
904
+ else if (options.local) provider = 'local';
905
+
906
+ // Map provider to tool name
907
+ const toolMap: Record<string, string> = {
908
+ claude: 'claude',
909
+ openai: 'codex',
910
+ gemini: 'gemini',
911
+ grok: 'grok',
912
+ local: 'hanzo-dev'
913
+ };
914
+
915
+ const toolName = toolMap[provider];
916
+ if (toolName && TOOLS[toolName as keyof typeof TOOLS]) {
917
+ console.log(chalk.gray(`Launching ${TOOLS[toolName as keyof typeof TOOLS].name}...`));
918
+ runTool(toolName, options.prompt ? [options.prompt] : ['.']);
919
+ return;
920
+ }
921
+ }
922
+
404
923
  const defaultTool = await getDefaultTool();
405
924
  if (defaultTool && process.argv.length === 2) {
406
- // If we have a default tool and no arguments, run it directly
407
- console.log(chalk.gray(`Auto-selecting ${TOOLS[defaultTool as keyof typeof TOOLS].name} based on available API keys...`));
925
+ console.log(chalk.gray(`Auto-launching ${TOOLS[defaultTool as keyof typeof TOOLS].name}...`));
408
926
  runTool(defaultTool, ['.']);
409
927
  } else {
410
- // Otherwise show interactive mode
411
928
  interactiveMode();
412
929
  }
413
930
  });
@@ -415,12 +932,12 @@ program
415
932
  // Parse arguments
416
933
  program.parse();
417
934
 
418
- // If no arguments provided, check for default tool
935
+ // If no arguments, run interactive mode
419
936
  if (process.argv.length === 2) {
420
937
  (async () => {
421
938
  const defaultTool = await getDefaultTool();
422
939
  if (defaultTool) {
423
- console.log(chalk.gray(`Auto-selecting ${TOOLS[defaultTool as keyof typeof TOOLS].name} based on available API keys...`));
940
+ console.log(chalk.gray(`Auto-launching ${TOOLS[defaultTool as keyof typeof TOOLS].name}...`));
424
941
  runTool(defaultTool, ['.']);
425
942
  } else {
426
943
  interactiveMode();