@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/.eslintrc.json +24 -0
- package/README.md +359 -0
- package/dist/cli/dev.js +21724 -602
- package/package.json +19 -4
- package/src/cli/dev.ts +623 -106
- package/src/lib/agent-loop.ts +552 -0
- package/src/lib/benchmark-runner.ts +431 -0
- package/src/lib/code-act-agent.ts +378 -0
- package/src/lib/config.ts +163 -0
- package/src/lib/editor.ts +395 -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/swarm-runner.ts +379 -0
- package/src/lib/unified-workspace.ts +435 -0
- package/test-swarm/file1.js +6 -0
- package/test-swarm/file2.ts +12 -0
- package/test-swarm/file3.py +15 -0
- package/test-swarm/file4.md +13 -0
- package/test-swarm/file5.json +12 -0
- package/test-swarm-demo.sh +22 -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/fixtures/sample-code.js +13 -0
- package/tests/fixtures/sample-code.py +28 -0
- package/tests/fixtures/sample-code.ts +22 -0
- package/tests/mcp-client.test.ts +238 -0
- package/tests/peer-agent-network.test.ts +340 -0
- package/tests/swarm-runner.test.ts +301 -0
- package/tests/swe-bench.test.ts +357 -0
- package/tsconfig.json +13 -15
- package/vitest.config.ts +37 -0
|
@@ -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
|
+
}
|