@hanzo/dev 2.0.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/lib/editor.ts CHANGED
@@ -29,6 +29,10 @@ export class FileEditor {
29
29
  private editHistory: Map<string, string[]> = new Map();
30
30
  private currentFile: string | null = null;
31
31
 
32
+ constructor(workingDir: string = process.cwd()) {
33
+ // No need to instantiate ChunkLocalizer as it has static methods
34
+ }
35
+
32
36
  async execute(command: EditCommand): Promise<EditResult> {
33
37
  switch (command.command) {
34
38
  case 'view':
@@ -290,6 +294,29 @@ export class FileEditor {
290
294
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
291
295
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
292
296
  }
297
+
298
+ async getRelevantChunks(filePath: string, query: string): Promise<any[]> {
299
+ try {
300
+ if (!fs.existsSync(filePath)) {
301
+ return [];
302
+ }
303
+
304
+ const content = fs.readFileSync(filePath, 'utf-8');
305
+ const chunk = ChunkLocalizer.findRelevantChunk(content, query);
306
+
307
+ if (!chunk) {
308
+ return [];
309
+ }
310
+
311
+ return [{
312
+ startLine: chunk.startLine,
313
+ endLine: chunk.endLine,
314
+ content: chunk.content
315
+ }];
316
+ } catch (error) {
317
+ return [];
318
+ }
319
+ }
293
320
  }
294
321
 
295
322
  // Chunk localizer for finding relevant code sections
@@ -0,0 +1,379 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { glob } from 'glob';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { spawn, ChildProcess } from 'child_process';
7
+ import { EventEmitter } from 'events';
8
+
9
+ export interface SwarmOptions {
10
+ provider: 'claude' | 'openai' | 'gemini' | 'grok' | 'local';
11
+ count: number;
12
+ prompt: string;
13
+ cwd?: string;
14
+ pattern?: string;
15
+ autoLogin?: boolean;
16
+ }
17
+
18
+ export interface SwarmAgent {
19
+ id: string;
20
+ process?: ChildProcess;
21
+ file?: string;
22
+ status: 'idle' | 'busy' | 'done' | 'error';
23
+ result?: string;
24
+ error?: string;
25
+ }
26
+
27
+ export class SwarmRunner extends EventEmitter {
28
+ private agents: Map<string, SwarmAgent> = new Map();
29
+ private fileQueue: string[] = [];
30
+ private options: SwarmOptions;
31
+ private activeCount: number = 0;
32
+
33
+ constructor(options: SwarmOptions) {
34
+ super();
35
+ this.options = {
36
+ cwd: process.cwd(),
37
+ pattern: '**/*',
38
+ autoLogin: true,
39
+ ...options
40
+ };
41
+ }
42
+
43
+ async run(): Promise<void> {
44
+ const spinner = ora(`Initializing swarm with ${this.options.count} agents...`).start();
45
+
46
+ try {
47
+ // Find files to process
48
+ this.fileQueue = await this.findFiles();
49
+ spinner.succeed(`Found ${this.fileQueue.length} files to process`);
50
+
51
+ if (this.fileQueue.length === 0) {
52
+ console.log(chalk.yellow('No files found matching pattern'));
53
+ return;
54
+ }
55
+
56
+ // Initialize agent pool
57
+ const agentCount = Math.min(this.options.count, this.fileQueue.length);
58
+ spinner.start(`Spawning ${agentCount} agents...`);
59
+
60
+ for (let i = 0; i < agentCount; i++) {
61
+ const agent: SwarmAgent = {
62
+ id: `agent-${i}`,
63
+ status: 'idle'
64
+ };
65
+ this.agents.set(agent.id, agent);
66
+ }
67
+
68
+ spinner.succeed(`Spawned ${agentCount} agents`);
69
+
70
+ // Process files in parallel
71
+ spinner.start('Processing files...');
72
+ const startTime = Date.now();
73
+
74
+ // Start processing
75
+ await this.processFiles();
76
+
77
+ const duration = (Date.now() - startTime) / 1000;
78
+ spinner.succeed(`Completed in ${duration.toFixed(1)}s`);
79
+
80
+ // Show results
81
+ this.showResults();
82
+
83
+ } catch (error) {
84
+ spinner.fail(`Swarm error: ${error}`);
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ private async findFiles(): Promise<string[]> {
90
+ return new Promise((resolve, reject) => {
91
+ const options = {
92
+ cwd: this.options.cwd,
93
+ nodir: true,
94
+ ignore: [
95
+ '**/node_modules/**',
96
+ '**/.git/**',
97
+ '**/dist/**',
98
+ '**/build/**',
99
+ '**/*.min.js',
100
+ '**/*.map'
101
+ ]
102
+ };
103
+
104
+ glob(this.options.pattern || '**/*', options, (err, files) => {
105
+ if (err) {
106
+ reject(err);
107
+ } else {
108
+ // Filter to only editable files
109
+ const editableFiles = files.filter(file => {
110
+ const ext = path.extname(file);
111
+ return ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cpp', '.c', '.h', '.go', '.rs', '.rb', '.php', '.swift', '.kt', '.scala', '.r', '.m', '.mm', '.md', '.txt', '.json', '.xml', '.yaml', '.yml', '.toml', '.ini', '.conf', '.sh', '.bash', '.zsh', '.fish', '.ps1', '.bat', '.cmd'].includes(ext);
112
+ });
113
+ resolve(editableFiles);
114
+ }
115
+ });
116
+ });
117
+ }
118
+
119
+ private async processFiles(): Promise<void> {
120
+ const promises: Promise<void>[] = [];
121
+
122
+ // Start initial batch of work
123
+ for (const [id, agent] of this.agents) {
124
+ if (this.fileQueue.length > 0) {
125
+ promises.push(this.processNextFile(agent));
126
+ }
127
+ }
128
+
129
+ // Wait for all agents to complete
130
+ await Promise.all(promises);
131
+ }
132
+
133
+ private async processNextFile(agent: SwarmAgent): Promise<void> {
134
+ while (this.fileQueue.length > 0) {
135
+ const file = this.fileQueue.shift();
136
+ if (!file) break;
137
+
138
+ agent.file = file;
139
+ agent.status = 'busy';
140
+ this.activeCount++;
141
+
142
+ try {
143
+ await this.processFile(agent, file);
144
+ agent.status = 'done';
145
+ } catch (error) {
146
+ agent.status = 'error';
147
+ agent.error = error instanceof Error ? error.message : String(error);
148
+ } finally {
149
+ this.activeCount--;
150
+ }
151
+ }
152
+ }
153
+
154
+ private async processFile(agent: SwarmAgent, file: string): Promise<void> {
155
+ const fullPath = path.join(this.options.cwd!, file);
156
+
157
+ // Build command based on provider
158
+ const command = this.buildCommand(file);
159
+
160
+ return new Promise((resolve, reject) => {
161
+ const child = spawn(command.cmd, command.args, {
162
+ cwd: this.options.cwd,
163
+ env: {
164
+ ...process.env,
165
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
166
+ OPENAI_API_KEY: process.env.OPENAI_API_KEY,
167
+ GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
168
+ GROK_API_KEY: process.env.GROK_API_KEY,
169
+ // Auto-accept edits for non-interactive mode
170
+ CLAUDE_CODE_PERMISSION_MODE: 'acceptEdits'
171
+ },
172
+ shell: true
173
+ });
174
+
175
+ agent.process = child;
176
+
177
+ let output = '';
178
+ let error = '';
179
+
180
+ child.stdout?.on('data', (data) => {
181
+ output += data.toString();
182
+ });
183
+
184
+ child.stderr?.on('data', (data) => {
185
+ error += data.toString();
186
+ });
187
+
188
+ child.on('close', (code) => {
189
+ if (code === 0) {
190
+ agent.result = output;
191
+ resolve();
192
+ } else {
193
+ reject(new Error(`Process exited with code ${code}: ${error}`));
194
+ }
195
+ });
196
+
197
+ child.on('error', (err) => {
198
+ reject(err);
199
+ });
200
+ });
201
+ }
202
+
203
+ private buildCommand(file: string): { cmd: string, args: string[] } {
204
+ const fullPath = path.join(this.options.cwd!, file);
205
+ const filePrompt = `${this.options.prompt}\n\nFile: ${file}`;
206
+
207
+ switch (this.options.provider) {
208
+ case 'claude':
209
+ return {
210
+ cmd: 'claude',
211
+ args: [
212
+ '-p',
213
+ filePrompt,
214
+ '--max-turns', '5',
215
+ '--allowedTools', 'Read,Write,Edit',
216
+ '--permission-mode', 'acceptEdits'
217
+ ]
218
+ };
219
+
220
+ case 'openai':
221
+ return {
222
+ cmd: 'openai',
223
+ args: [
224
+ 'chat',
225
+ '--prompt', filePrompt,
226
+ '--file', fullPath,
227
+ '--edit'
228
+ ]
229
+ };
230
+
231
+ case 'gemini':
232
+ return {
233
+ cmd: 'gemini',
234
+ args: [
235
+ 'edit',
236
+ fullPath,
237
+ '--prompt', filePrompt
238
+ ]
239
+ };
240
+
241
+ case 'grok':
242
+ return {
243
+ cmd: 'grok',
244
+ args: [
245
+ '--edit',
246
+ fullPath,
247
+ '--prompt', filePrompt
248
+ ]
249
+ };
250
+
251
+ case 'local':
252
+ return {
253
+ cmd: 'dev',
254
+ args: [
255
+ 'agent',
256
+ filePrompt
257
+ ]
258
+ };
259
+
260
+ default:
261
+ throw new Error(`Unknown provider: ${this.options.provider}`);
262
+ }
263
+ }
264
+
265
+ private showResults(): void {
266
+ console.log(chalk.bold.cyan('\n📊 Swarm Results\n'));
267
+
268
+ let successful = 0;
269
+ let failed = 0;
270
+
271
+ for (const [id, agent] of this.agents) {
272
+ if (agent.status === 'done') {
273
+ successful++;
274
+ console.log(chalk.green(`✓ ${agent.file || id}`));
275
+ } else if (agent.status === 'error') {
276
+ failed++;
277
+ console.log(chalk.red(`✗ ${agent.file || id}: ${agent.error}`));
278
+ }
279
+ }
280
+
281
+ console.log(chalk.gray('\n─────────────────'));
282
+ console.log(chalk.white('Total files:'), this.fileQueue.length + successful + failed);
283
+ console.log(chalk.green('Successful:'), successful);
284
+ if (failed > 0) {
285
+ console.log(chalk.red('Failed:'), failed);
286
+ }
287
+ }
288
+
289
+ async ensureProviderAuth(): Promise<boolean> {
290
+ switch (this.options.provider) {
291
+ case 'claude':
292
+ return this.ensureClaudeAuth();
293
+ case 'openai':
294
+ return !!process.env.OPENAI_API_KEY;
295
+ case 'gemini':
296
+ return !!process.env.GOOGLE_API_KEY || !!process.env.GEMINI_API_KEY;
297
+ case 'grok':
298
+ return !!process.env.GROK_API_KEY;
299
+ case 'local':
300
+ return true;
301
+ default:
302
+ return false;
303
+ }
304
+ }
305
+
306
+ private async ensureClaudeAuth(): Promise<boolean> {
307
+ // Check if already authenticated
308
+ try {
309
+ const testResult = await new Promise<boolean>((resolve) => {
310
+ const child = spawn('claude', ['-p', 'test', '--max-turns', '1'], {
311
+ env: process.env,
312
+ shell: true
313
+ });
314
+
315
+ let hasError = false;
316
+ let resolved = false;
317
+
318
+ const cleanup = () => {
319
+ if (!resolved) {
320
+ resolved = true;
321
+ clearTimeout(timeout);
322
+ child.kill();
323
+ }
324
+ };
325
+
326
+ child.stderr?.on('data', (data) => {
327
+ const output = data.toString();
328
+ if (output.includes('not authenticated') || output.includes('API key')) {
329
+ hasError = true;
330
+ }
331
+ });
332
+
333
+ child.on('close', () => {
334
+ cleanup();
335
+ resolve(!hasError);
336
+ });
337
+
338
+ // Timeout after 5 seconds
339
+ const timeout = setTimeout(() => {
340
+ cleanup();
341
+ resolve(!hasError);
342
+ }, 5000);
343
+ });
344
+
345
+ if (testResult) {
346
+ return true;
347
+ }
348
+
349
+ // Try to login automatically if we have API key
350
+ if (process.env.ANTHROPIC_API_KEY && this.options.autoLogin) {
351
+ console.log(chalk.yellow('Attempting automatic Claude login...'));
352
+
353
+ const loginResult = await new Promise<boolean>((resolve) => {
354
+ const child = spawn('claude', ['login'], {
355
+ env: {
356
+ ...process.env,
357
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY
358
+ },
359
+ shell: true,
360
+ stdio: 'inherit'
361
+ });
362
+
363
+ child.on('close', (code) => {
364
+ resolve(code === 0);
365
+ });
366
+ });
367
+
368
+ if (loginResult) {
369
+ console.log(chalk.green('✓ Claude login successful'));
370
+ return true;
371
+ }
372
+ }
373
+
374
+ return false;
375
+ } catch (error) {
376
+ return false;
377
+ }
378
+ }
379
+ }
@@ -0,0 +1,6 @@
1
+ // Sample JavaScript file
2
+ function calculateTotal(items) {
3
+ return items.reduce((sum, item) => sum + item.price, 0);
4
+ }
5
+
6
+ module.exports = { calculateTotal };
@@ -0,0 +1,12 @@
1
+ // TypeScript utility functions
2
+ export function formatDate(date: Date): string {
3
+ return date.toISOString().split('T')[0];
4
+ }
5
+
6
+ export function parseJSON<T>(json: string): T | null {
7
+ try {
8
+ return JSON.parse(json);
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
@@ -0,0 +1,15 @@
1
+ # Python data processing
2
+ import json
3
+ from typing import List, Dict
4
+
5
+ def process_data(items: List[Dict]) -> Dict:
6
+ """Process a list of items and return summary statistics."""
7
+ total = sum(item.get('value', 0) for item in items)
8
+ count = len(items)
9
+ average = total / count if count > 0 else 0
10
+
11
+ return {
12
+ 'total': total,
13
+ 'count': count,
14
+ 'average': average
15
+ }
@@ -0,0 +1,13 @@
1
+ # Documentation
2
+
3
+ This is a sample markdown file for testing the swarm functionality.
4
+
5
+ ## Features
6
+
7
+ - Parallel processing
8
+ - Multiple file types
9
+ - Automatic edits
10
+
11
+ ## Usage
12
+
13
+ Run the swarm command to process multiple files at once.
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "test-project",
3
+ "version": "1.0.0",
4
+ "description": "Test project for swarm processing",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": ["test", "swarm"],
10
+ "author": "",
11
+ "license": "ISC"
12
+ }
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+
3
+ echo "🐝 Hanzo Dev Swarm Demo"
4
+ echo "======================"
5
+ echo ""
6
+ echo "This demo will add copyright headers to 5 test files in parallel"
7
+ echo ""
8
+ echo "Files before:"
9
+ echo "-------------"
10
+ head -n 1 test-swarm/*.{js,ts,py,md,json} 2>/dev/null
11
+
12
+ echo ""
13
+ echo "Running swarm with 5 agents..."
14
+ echo ""
15
+
16
+ # Run the swarm command
17
+ node dist/cli/dev.js --claude --swarm 5 -p "Add this copyright header at the very top of each file: '// Copyright 2025 Hanzo Industries Inc.' (use # for Python, // for JS/TS/JSON)"
18
+
19
+ echo ""
20
+ echo "Files after:"
21
+ echo "------------"
22
+ head -n 2 test-swarm/*.{js,ts,py,md,json} 2>/dev/null
@@ -1,11 +1,11 @@
1
- import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
1
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import * as os from 'os';
5
- import { Editor, EditCommand } from '../src/lib/editor';
5
+ import { FileEditor, EditCommand } from '../src/lib/editor';
6
6
 
7
7
  describe('Editor', () => {
8
- let editor: Editor;
8
+ let editor: FileEditor;
9
9
  let testDir: string;
10
10
  let testFile: string;
11
11
 
@@ -13,7 +13,7 @@ describe('Editor', () => {
13
13
  // Create temporary test directory
14
14
  testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hanzo-dev-test-'));
15
15
  testFile = path.join(testDir, 'test.txt');
16
- editor = new Editor(testDir);
16
+ editor = new FileEditor(testDir);
17
17
  });
18
18
 
19
19
  afterEach(() => {
@@ -46,7 +46,7 @@ describe('Editor', () => {
46
46
 
47
47
  const result = await editor.execute(command);
48
48
  expect(result.success).toBe(false);
49
- expect(result.error).toContain('already exists');
49
+ expect(result.error).toBe('FILE_EXISTS');
50
50
  });
51
51
  });
52
52
 
@@ -121,7 +121,7 @@ describe('Editor', () => {
121
121
 
122
122
  const result = await editor.execute(command);
123
123
  expect(result.success).toBe(false);
124
- expect(result.error).toContain('not found');
124
+ expect(result.error).toBe('STRING_NOT_FOUND');
125
125
  });
126
126
 
127
127
  test('should fail when old string appears multiple times', async () => {
@@ -137,7 +137,7 @@ describe('Editor', () => {
137
137
 
138
138
  const result = await editor.execute(command);
139
139
  expect(result.success).toBe(false);
140
- expect(result.error).toContain('multiple');
140
+ expect(result.error).toBe('STRING_NOT_UNIQUE');
141
141
  });
142
142
  });
143
143
 
@@ -0,0 +1,13 @@
1
+ // Sample JavaScript code for testing
2
+ function calculateSum(numbers) {
3
+ return numbers.reduce((sum, num) => sum + num, 0);
4
+ }
5
+
6
+ function findMax(numbers) {
7
+ return Math.max(...numbers);
8
+ }
9
+
10
+ module.exports = {
11
+ calculateSum,
12
+ findMax
13
+ };
@@ -0,0 +1,28 @@
1
+ # Sample Python code for testing
2
+ import json
3
+ from typing import List, Dict, Optional
4
+
5
+ class DataProcessor:
6
+ """Process and analyze data."""
7
+
8
+ def __init__(self):
9
+ self.data = []
10
+
11
+ def add_item(self, item: Dict) -> None:
12
+ """Add an item to the dataset."""
13
+ self.data.append(item)
14
+
15
+ def get_summary(self) -> Dict:
16
+ """Get summary statistics."""
17
+ if not self.data:
18
+ return {"count": 0, "total": 0, "average": 0}
19
+
20
+ values = [item.get("value", 0) for item in self.data]
21
+ total = sum(values)
22
+ count = len(values)
23
+
24
+ return {
25
+ "count": count,
26
+ "total": total,
27
+ "average": total / count if count > 0 else 0
28
+ }
@@ -0,0 +1,22 @@
1
+ // Sample TypeScript code for testing
2
+ interface User {
3
+ id: number;
4
+ name: string;
5
+ email: string;
6
+ }
7
+
8
+ export class UserService {
9
+ private users: User[] = [];
10
+
11
+ addUser(user: User): void {
12
+ this.users.push(user);
13
+ }
14
+
15
+ getUser(id: number): User | undefined {
16
+ return this.users.find(user => user.id === id);
17
+ }
18
+
19
+ getAllUsers(): User[] {
20
+ return [...this.users];
21
+ }
22
+ }
@@ -1,10 +1,10 @@
1
- import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals';
1
+ import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import { MCPClient, MCPSession, MCPServerConfig } from '../src/lib/mcp-client';
3
3
  import { EventEmitter } from 'events';
4
4
  import * as child_process from 'child_process';
5
5
 
6
6
  // Mock child_process
7
- jest.mock('child_process');
7
+ vi.mock('child_process');
8
8
 
9
9
  describe('MCPClient', () => {
10
10
  let client: MCPClient;
@@ -15,16 +15,16 @@ describe('MCPClient', () => {
15
15
 
16
16
  // Mock spawn to return a fake process
17
17
  mockProcess = new EventEmitter();
18
- mockProcess.stdin = { write: jest.fn() };
18
+ mockProcess.stdin = { write: vi.fn() };
19
19
  mockProcess.stdout = new EventEmitter();
20
20
  mockProcess.stderr = new EventEmitter();
21
- mockProcess.kill = jest.fn();
21
+ mockProcess.kill = vi.fn();
22
22
 
23
- (child_process.spawn as jest.Mock).mockReturnValue(mockProcess);
23
+ vi.mocked(child_process.spawn).mockReturnValue(mockProcess as any);
24
24
  });
25
25
 
26
26
  afterEach(() => {
27
- jest.clearAllMocks();
27
+ vi.clearAllMocks();
28
28
  });
29
29
 
30
30
  describe('stdio transport', () => {