@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/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
- // Available tools configuration with API key detection
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: 1
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 (installed OR has API key)
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 based on API keys and installation
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
- // Prefer tools that have both API key and are installed
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 with optional arguments
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 // Pass through all environment variables
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
- 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
- });
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 a tool:'));
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('\n2. Or configure API keys in your .env file:'));
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
- // 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;
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
- const choices = await Promise.all(availableTools.map(async tool => {
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
- return {
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 an AI tool to launch:',
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
- // Check if we should pass the current directory
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 from package.json
506
+ // Setup version
274
507
  const packagePath = path.join(__dirname, '../../package.json');
275
- let version = '1.2.0';
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 if package.json not found
513
+ // Use default version
281
514
  }
282
515
 
283
516
  program
284
517
  .name('dev')
285
- .description('Hanzo Dev - Meta AI development CLI that manages and runs all AI coding assistants')
518
+ .description('Hanzo Dev - Meta AI development CLI with built-in editor and tool orchestration')
286
519
  .version(version);
287
520
 
288
- // Direct tool commands
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 installed tools
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
- // 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
- });
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 (!hasAnyKey) {
352
- console.log(chalk.gray(' (no API keys detected)'));
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
- // Show tools
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 (isInstalled && hasKey) {
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
- 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 ')}`));
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 - run default tool or interactive mode
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
- // 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...`));
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 provided, check for default tool
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-selecting ${TOOLS[defaultTool as keyof typeof TOOLS].name} based on available API keys...`));
773
+ console.log(chalk.gray(`Auto-launching ${TOOLS[defaultTool as keyof typeof TOOLS].name}...`));
424
774
  runTool(defaultTool, ['.']);
425
775
  } else {
426
776
  interactiveMode();