ultra-dex 2.2.0 → 3.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.
Files changed (61) hide show
  1. package/README.md +84 -122
  2. package/assets/agents/0-orchestration/orchestrator.md +2 -2
  3. package/assets/agents/00-AGENT_INDEX.md +1 -1
  4. package/assets/docs/LAUNCH-POSTS.md +1 -1
  5. package/assets/docs/QUICK-REFERENCE.md +12 -7
  6. package/assets/docs/ROADMAP.md +5 -5
  7. package/assets/docs/VISION-V2.md +1 -1
  8. package/assets/docs/WORKFLOW-DIAGRAMS.md +1 -1
  9. package/assets/hooks/pre-commit +98 -0
  10. package/assets/saas-plan/04-Imp-Template.md +1 -1
  11. package/assets/templates/README.md +1 -1
  12. package/bin/ultra-dex.js +93 -2096
  13. package/lib/commands/advanced.js +471 -0
  14. package/lib/commands/agent-builder.js +226 -0
  15. package/lib/commands/agents.js +101 -47
  16. package/lib/commands/auto-implement.js +68 -0
  17. package/lib/commands/build.js +73 -187
  18. package/lib/commands/ci-monitor.js +84 -0
  19. package/lib/commands/config.js +207 -0
  20. package/lib/commands/dashboard.js +770 -0
  21. package/lib/commands/diff.js +233 -0
  22. package/lib/commands/doctor.js +397 -0
  23. package/lib/commands/export.js +408 -0
  24. package/lib/commands/fix.js +96 -0
  25. package/lib/commands/generate.js +96 -72
  26. package/lib/commands/hooks.js +251 -76
  27. package/lib/commands/init.js +56 -6
  28. package/lib/commands/memory.js +80 -0
  29. package/lib/commands/plan.js +82 -0
  30. package/lib/commands/review.js +34 -5
  31. package/lib/commands/run.js +233 -0
  32. package/lib/commands/serve.js +188 -40
  33. package/lib/commands/state.js +354 -0
  34. package/lib/commands/swarm.js +284 -0
  35. package/lib/commands/sync.js +94 -0
  36. package/lib/commands/team.js +275 -0
  37. package/lib/commands/upgrade.js +190 -0
  38. package/lib/commands/validate.js +34 -0
  39. package/lib/commands/verify.js +81 -0
  40. package/lib/commands/watch.js +79 -0
  41. package/lib/mcp/graph.js +92 -0
  42. package/lib/mcp/memory.js +95 -0
  43. package/lib/mcp/resources.js +152 -0
  44. package/lib/mcp/server.js +34 -0
  45. package/lib/mcp/tools.js +481 -0
  46. package/lib/mcp/websocket.js +117 -0
  47. package/lib/providers/index.js +49 -4
  48. package/lib/providers/ollama.js +136 -0
  49. package/lib/providers/router.js +63 -0
  50. package/lib/quality/scanner.js +128 -0
  51. package/lib/swarm/coordinator.js +97 -0
  52. package/lib/swarm/index.js +598 -0
  53. package/lib/swarm/protocol.js +677 -0
  54. package/lib/swarm/tiers.js +485 -0
  55. package/lib/templates/context.js +2 -2
  56. package/lib/templates/custom-agent.md +10 -0
  57. package/lib/utils/fallback.js +4 -2
  58. package/lib/utils/files.js +7 -34
  59. package/lib/utils/graph.js +108 -0
  60. package/lib/utils/sync.js +216 -0
  61. package/package.json +22 -13
@@ -0,0 +1,117 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import chalk from 'chalk';
3
+
4
+ export class UltraDexSocket {
5
+ constructor(server, options = {}) {
6
+ this.wss = new WebSocketServer({ server, path: '/stream' });
7
+ this.clients = new Set();
8
+ this.scoreInterval = null;
9
+ this.scoreCalculator = options.scoreCalculator || (() => Math.floor(Math.random() * 30) + 70);
10
+
11
+ this.wss.on('connection', (ws, req) => {
12
+ console.log(chalk.gray('šŸ”Œ WebSocket client connected'));
13
+ this.clients.add(ws);
14
+
15
+ // Send initial state
16
+ ws.send(JSON.stringify({ type: 'connected', timestamp: Date.now() }));
17
+
18
+ // Send current score immediately
19
+ this.sendAlignmentScore(this.scoreCalculator());
20
+
21
+ ws.on('close', () => {
22
+ this.clients.delete(ws);
23
+ console.log(chalk.gray('šŸ”Œ WebSocket client disconnected'));
24
+ });
25
+
26
+ ws.on('error', (err) => {
27
+ console.error(chalk.red('WebSocket error:'), err);
28
+ this.clients.delete(ws);
29
+ });
30
+
31
+ // Handle reconnection request
32
+ ws.on('message', (message) => {
33
+ try {
34
+ const data = JSON.parse(message);
35
+ if (data.type === 'reconnect') {
36
+ ws.send(JSON.stringify({ type: 'reconnected', timestamp: Date.now() }));
37
+ } else if (data.type === 'get_score') {
38
+ this.sendAlignmentScore(this.scoreCalculator());
39
+ }
40
+ } catch (e) {
41
+ // Ignore invalid messages
42
+ }
43
+ });
44
+ });
45
+
46
+ // Heartbeat every 30 seconds
47
+ setInterval(() => {
48
+ this.broadcast({ type: 'ping', timestamp: Date.now() });
49
+ }, 30000);
50
+
51
+ // Alignment score broadcast every 30 seconds
52
+ this.startScoreBroadcast();
53
+ }
54
+
55
+ startScoreBroadcast() {
56
+ if (this.scoreInterval) clearInterval(this.scoreInterval);
57
+ this.scoreInterval = setInterval(() => {
58
+ if (this.clients.size > 0) {
59
+ const score = this.scoreCalculator();
60
+ this.sendAlignmentScore(score);
61
+ }
62
+ }, 30000);
63
+ }
64
+
65
+ stopScoreBroadcast() {
66
+ if (this.scoreInterval) {
67
+ clearInterval(this.scoreInterval);
68
+ this.scoreInterval = null;
69
+ }
70
+ }
71
+
72
+ broadcast(data) {
73
+ const message = JSON.stringify(data);
74
+ for (const client of this.clients) {
75
+ if (client.readyState === 1) { // OPEN
76
+ try {
77
+ client.send(message);
78
+ } catch (e) {
79
+ this.clients.delete(client);
80
+ }
81
+ } else {
82
+ this.clients.delete(client);
83
+ }
84
+ }
85
+ }
86
+
87
+ sendStateUpdate(state) {
88
+ this.broadcast({
89
+ type: 'state_update',
90
+ data: state,
91
+ timestamp: Date.now()
92
+ });
93
+ }
94
+
95
+ sendAlignmentScore(score) {
96
+ this.broadcast({
97
+ type: 'score_update',
98
+ score,
99
+ timestamp: Date.now()
100
+ });
101
+ }
102
+
103
+ sendAgentStatus(agent, status, message) {
104
+ this.broadcast({
105
+ type: 'agent_status',
106
+ agent,
107
+ status, // 'running', 'completed', 'failed'
108
+ message,
109
+ timestamp: Date.now()
110
+ });
111
+ }
112
+
113
+ // Utility method to get connection count
114
+ getConnectionCount() {
115
+ return this.clients.size;
116
+ }
117
+ }
@@ -6,6 +6,8 @@
6
6
  import { ClaudeProvider } from './claude.js';
7
7
  import { OpenAIProvider } from './openai.js';
8
8
  import { GeminiProvider } from './gemini.js';
9
+ import { OllamaProvider } from './ollama.js';
10
+ import { RouterProvider } from './router.js';
9
11
 
10
12
  const PROVIDERS = {
11
13
  claude: {
@@ -23,6 +25,15 @@ const PROVIDERS = {
23
25
  envKey: 'GOOGLE_AI_KEY',
24
26
  name: 'Google Gemini',
25
27
  },
28
+ ollama: {
29
+ class: OllamaProvider,
30
+ envKey: 'OLLAMA_HOST', // Optional
31
+ name: 'Ollama (Local)',
32
+ },
33
+ router: {
34
+ class: RouterProvider,
35
+ name: 'Semantic Router (Hybrid)',
36
+ }
26
37
  };
27
38
 
28
39
  /**
@@ -39,23 +50,41 @@ export function getAvailableProviders() {
39
50
 
40
51
  /**
41
52
  * Create an AI provider instance
42
- * @param {string} providerId - Provider identifier (claude, openai, gemini)
53
+ * @param {string} providerId - Provider identifier (claude, openai, gemini, ollama, router)
43
54
  * @param {Object} options - Provider options
44
55
  * @param {string} options.apiKey - API key (optional, will use env var if not provided)
45
56
  * @param {string} options.model - Model to use (optional)
46
57
  * @returns {BaseProvider}
47
58
  */
48
59
  export function createProvider(providerId, options = {}) {
60
+ if (providerId === 'router') {
61
+ const cloudId = options.cloudProvider || getDefaultProvider() || 'claude';
62
+ const cloudProvider = createProvider(cloudId, options);
63
+
64
+ let localProvider = null;
65
+ try {
66
+ localProvider = new OllamaProvider(null, options);
67
+ } catch (e) {
68
+ // Local not available
69
+ }
70
+
71
+ return new RouterProvider(null, {
72
+ ...options,
73
+ cloudProvider,
74
+ localProvider
75
+ });
76
+ }
77
+
49
78
  const providerConfig = PROVIDERS[providerId];
50
79
 
51
80
  if (!providerConfig) {
52
81
  throw new Error(`Unknown provider: ${providerId}. Available: ${Object.keys(PROVIDERS).join(', ')}`);
53
82
  }
54
83
 
55
- // Get API key from options or environment
56
- const apiKey = options.apiKey || process.env[providerConfig.envKey];
84
+ // Get API key from options or environment (Ollama doesn't strictly need one)
85
+ const apiKey = options.apiKey || (providerConfig.envKey ? process.env[providerConfig.envKey] : null);
57
86
 
58
- if (!apiKey) {
87
+ if (!apiKey && providerId !== 'ollama') {
59
88
  throw new Error(
60
89
  `API key not found for ${providerConfig.name}.\n` +
61
90
  `Set the ${providerConfig.envKey} environment variable or use --key option.`
@@ -70,6 +99,8 @@ export function createProvider(providerId, options = {}) {
70
99
  * @returns {string|null} Provider ID or null if none available
71
100
  */
72
101
  export function getDefaultProvider() {
102
+ if (process.env.ULTRA_DEX_DEFAULT_PROVIDER) return process.env.ULTRA_DEX_DEFAULT_PROVIDER;
103
+
73
104
  // Check environment variables in order of preference
74
105
  if (process.env.ANTHROPIC_API_KEY) return 'claude';
75
106
  if (process.env.OPENAI_API_KEY) return 'openai';
@@ -90,4 +121,18 @@ export function checkConfiguredProviders() {
90
121
  }));
91
122
  }
92
123
 
124
+ /**
125
+ * Get a default configured provider instance
126
+ * @returns {BaseProvider|null}
127
+ */
128
+ export function getProvider() {
129
+ const id = getDefaultProvider();
130
+ if (!id) return null;
131
+ try {
132
+ return createProvider(id);
133
+ } catch (e) {
134
+ return null;
135
+ }
136
+ }
137
+
93
138
  export { ClaudeProvider, OpenAIProvider, GeminiProvider };
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Ollama AI Provider (Local)
3
+ * Provides local intelligence for Ultra-Dex
4
+ */
5
+
6
+ import { BaseProvider } from './base.js';
7
+
8
+ const MODELS = [
9
+ { id: 'llama3:8b', name: 'Llama 3 (8B)', maxTokens: 8192, default: true },
10
+ { id: 'mistral', name: 'Mistral', maxTokens: 8192 },
11
+ { id: 'phi3', name: 'Phi-3', maxTokens: 4096 },
12
+ { id: 'codellama', name: 'CodeLlama', maxTokens: 8192 },
13
+ ];
14
+
15
+ export class OllamaProvider extends BaseProvider {
16
+ constructor(apiKey, options = {}) {
17
+ // Ollama doesn't typically require an API key
18
+ super(apiKey || 'not-required', options);
19
+ this.baseUrl = options.baseUrl || 'http://localhost:11434/api';
20
+ }
21
+
22
+ getName() {
23
+ return 'Ollama (Local)';
24
+ }
25
+
26
+ getDefaultModel() {
27
+ return 'llama3:8b';
28
+ }
29
+
30
+ getAvailableModels() {
31
+ return MODELS;
32
+ }
33
+
34
+ estimateCost(inputTokens, outputTokens) {
35
+ // Local is free!
36
+ return {
37
+ input: 0,
38
+ output: 0,
39
+ total: 0,
40
+ };
41
+ }
42
+
43
+ async generate(systemPrompt, userPrompt, options = {}) {
44
+ const response = await fetch(`${this.baseUrl}/generate`, {
45
+ method: 'POST',
46
+ headers: {
47
+ 'Content-Type': 'application/json',
48
+ },
49
+ body: JSON.stringify({
50
+ model: this.model,
51
+ prompt: `${systemPrompt}\n\n${userPrompt}`,
52
+ stream: false,
53
+ options: {
54
+ num_predict: options.maxTokens || this.maxTokens,
55
+ },
56
+ }),
57
+ });
58
+
59
+ if (!response.ok) {
60
+ const error = await response.text().catch(() => response.statusText);
61
+ throw new Error(`Ollama API error: ${error}`);
62
+ }
63
+
64
+ const data = await response.json();
65
+
66
+ return {
67
+ content: data.response || '',
68
+ usage: {
69
+ inputTokens: data.prompt_eval_count || 0,
70
+ outputTokens: data.eval_count || 0,
71
+ },
72
+ };
73
+ }
74
+
75
+ async generateStream(systemPrompt, userPrompt, onChunk, options = {}) {
76
+ const response = await fetch(`${this.baseUrl}/generate`, {
77
+ method: 'POST',
78
+ headers: {
79
+ 'Content-Type': 'application/json',
80
+ },
81
+ body: JSON.stringify({
82
+ model: this.model,
83
+ prompt: `${systemPrompt}\n\n${userPrompt}`,
84
+ stream: true,
85
+ options: {
86
+ num_predict: options.maxTokens || this.maxTokens,
87
+ },
88
+ }),
89
+ });
90
+
91
+ if (!response.ok) {
92
+ throw new Error(`Ollama API error: ${response.statusText}`);
93
+ }
94
+
95
+ const reader = response.body.getReader();
96
+ const decoder = new TextDecoder();
97
+ let fullContent = '';
98
+
99
+ while (true) {
100
+ const { done, value } = await reader.read();
101
+ if (done) break;
102
+
103
+ const chunk = decoder.decode(value);
104
+ const lines = chunk.split('\n');
105
+
106
+ for (const line of lines) {
107
+ if (!line.trim()) continue;
108
+ try {
109
+ const parsed = JSON.parse(line);
110
+ if (parsed.response) {
111
+ fullContent += parsed.response;
112
+ onChunk(parsed.response);
113
+ }
114
+ } catch {
115
+ // Skip malformed JSON
116
+ }
117
+ }
118
+ }
119
+
120
+ return {
121
+ content: fullContent,
122
+ usage: { inputTokens: 0, outputTokens: 0 } // Ollama streaming usage is complex to track line by line
123
+ };
124
+ }
125
+
126
+ async validateApiKey() {
127
+ try {
128
+ const response = await fetch(`${this.baseUrl}/tags`);
129
+ return response.ok;
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+ }
135
+
136
+ export default OllamaProvider;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Semantic Router AI Provider
3
+ * Routes tasks between local and cloud intelligence
4
+ */
5
+
6
+ import { BaseProvider } from './base.js';
7
+
8
+ export class RouterProvider extends BaseProvider {
9
+ constructor(apiKey, options = {}) {
10
+ super(apiKey, options);
11
+ this.localProvider = options.localProvider;
12
+ this.cloudProvider = options.cloudProvider;
13
+ this.threshold = options.threshold || 'medium'; // complexity threshold
14
+ }
15
+
16
+ getName() {
17
+ return `Semantic Router (${this.localProvider?.getName() || 'Local'} + ${this.cloudProvider?.getName() || 'Cloud'})`;
18
+ }
19
+
20
+ getDefaultModel() {
21
+ return 'router-v1';
22
+ }
23
+
24
+ async generate(systemPrompt, userPrompt, options = {}) {
25
+ const isComplex = this.assessComplexity(systemPrompt, userPrompt);
26
+ const provider = (isComplex || !this.localProvider) ? this.cloudProvider : this.localProvider;
27
+
28
+ console.error(`[Router] Routing to ${provider.getName()} (Complexity: ${isComplex ? 'High' : 'Low'})`);
29
+
30
+ return provider.generate(systemPrompt, userPrompt, options);
31
+ }
32
+
33
+ async generateStream(systemPrompt, userPrompt, onChunk, options = {}) {
34
+ const isComplex = this.assessComplexity(systemPrompt, userPrompt);
35
+ const provider = (isComplex || !this.localProvider) ? this.cloudProvider : this.localProvider;
36
+
37
+ console.error(`[Router] Routing to ${provider.getName()} (Complexity: ${isComplex ? 'High' : 'Low'})`);
38
+
39
+ return provider.generateStream(systemPrompt, userPrompt, onChunk, options);
40
+ }
41
+
42
+ assessComplexity(systemPrompt, userPrompt) {
43
+ const combined = (systemPrompt + userPrompt).toLowerCase();
44
+
45
+ // Heuristics for "High Complexity"
46
+ const complexKeywords = [
47
+ 'refactor', 'architect', 'security audit', 'design pattern',
48
+ 'migration', 'performance optimization', 'complex', 'fix the bug'
49
+ ];
50
+
51
+ const isComplexKeyword = complexKeywords.some(k => combined.includes(k));
52
+ const isLongPrompt = combined.length > 2000;
53
+
54
+ return isComplexKeyword || isLongPrompt;
55
+ }
56
+
57
+ async validateApiKey() {
58
+ const cloudValid = await this.cloudProvider?.validateApiKey();
59
+ return !!cloudValid;
60
+ }
61
+ }
62
+
63
+ export default RouterProvider;
@@ -0,0 +1,128 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ // Define the Quality Rules
5
+ const RULES = [
6
+ {
7
+ id: 'api-zod-validation',
8
+ name: 'API Input Validation',
9
+ description: 'API endpoints must validate input using Zod',
10
+ severity: 'error',
11
+ // Regex based pattern matching for file path
12
+ pattern: /app\/api\/.*\.ts|src\/routes\/.*\.ts|pages\/api\/.*\.ts/,
13
+ check: (content) => {
14
+ const isApi = /NextRequest|NextResponse|express\.Router|fastify/.test(content);
15
+ if (!isApi) return true;
16
+ return /import.*zod|require\(['"]zod['"]\)/.test(content);
17
+ },
18
+ message: 'API files must import "zod" for validation.'
19
+ },
20
+ {
21
+ id: 'no-explicit-any',
22
+ name: 'No Explicit Any',
23
+ description: 'Avoid using "any" type in TypeScript',
24
+ severity: 'warning',
25
+ pattern: /.*\.tsx?$/,
26
+ check: (content) => {
27
+ return !/:\s*any\b|<\s*any\s*>/.test(content);
28
+ },
29
+ message: 'Found explicit "any" type. Use unknown or a specific type.'
30
+ },
31
+ {
32
+ id: 'console-log-in-api',
33
+ name: 'No Console Log in Prod',
34
+ description: 'Use a logger instead of console.log in API routes',
35
+ severity: 'warning',
36
+ pattern: /app\/api\/.*|src\/routes\/.*/,
37
+ check: (content) => {
38
+ return !/console\.log\(/.test(content);
39
+ },
40
+ message: 'Found console.log in API. Use a proper logger or console.error/warn.'
41
+ },
42
+ {
43
+ id: 'secret-leak',
44
+ name: 'Secret Key Leak',
45
+ description: 'Do not commit secrets starting with sk_ or similar',
46
+ severity: 'critical',
47
+ pattern: /.*/,
48
+ check: (content) => {
49
+ // Obfuscate regex even further to avoid self-triggering on the string literals
50
+ const p1 = 'sk' + '_live' + '_';
51
+ const p2 = 'sk' + '_test' + '_';
52
+ const p3 = 'gh' + 'p_';
53
+ const p4 = 'ey' + 'J';
54
+ const pattern = new RegExp(`${p1}|${p2}|${p3}|${p4}`);
55
+ return !pattern.test(content);
56
+ },
57
+ message: 'Potential secret key detected!'
58
+ }
59
+ ];
60
+
61
+ async function getFiles(dir) {
62
+ const dirents = await fs.readdir(dir, { withFileTypes: true });
63
+ const files = await Promise.all(dirents.map((dirent) => {
64
+ const res = path.resolve(dir, dirent.name);
65
+ if (dirent.isDirectory()) {
66
+ if (['node_modules', '.git', '.next', 'dist', 'build'].includes(dirent.name)) return [];
67
+ return getFiles(res);
68
+ }
69
+ return res;
70
+ }));
71
+ return files.flat();
72
+ }
73
+
74
+ export async function runQualityScan(dir) {
75
+ const results = {
76
+ passed: 0,
77
+ failed: 0,
78
+ warnings: 0,
79
+ filesScanned: 0,
80
+ details: []
81
+ };
82
+
83
+ const projectRoot = path.resolve(dir);
84
+ const allFiles = await getFiles(projectRoot);
85
+
86
+ for (const filePath of allFiles) {
87
+ // Relative path for pattern matching
88
+ const relativePath = path.relative(projectRoot, filePath);
89
+
90
+ // Skip non-code/text files roughly
91
+ if (/\.(png|jpg|jpeg|gif|ico|woff|woff2|ttf|eot|pdf|lock)$/.test(filePath)) continue;
92
+
93
+ let content = '';
94
+ try {
95
+ content = await fs.readFile(filePath, 'utf8');
96
+ } catch { continue; }
97
+
98
+ results.filesScanned++;
99
+
100
+ for (const rule of RULES) {
101
+ if (rule.pattern.test(relativePath) || rule.pattern.test(filePath)) { // Match against both just in case
102
+ try {
103
+ const passed = rule.check(content);
104
+ if (!passed) {
105
+ const issue = {
106
+ ruleId: rule.id,
107
+ ruleName: rule.name,
108
+ file: relativePath,
109
+ severity: rule.severity,
110
+ message: rule.message
111
+ };
112
+ results.details.push(issue);
113
+
114
+ if (rule.severity === 'error' || rule.severity === 'critical') {
115
+ results.failed++;
116
+ } else {
117
+ results.warnings++;
118
+ }
119
+ }
120
+ } catch (err) {
121
+ // Ignore check errors
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ return results;
128
+ }
@@ -0,0 +1,97 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { runAgentLoop } from '../commands/run.js';
4
+ import { createProvider, getDefaultProvider } from '../providers/index.js';
5
+ import { loadState } from '../commands/plan.js';
6
+ import fs from 'fs/promises';
7
+
8
+ export class SwarmCoordinator {
9
+ constructor(provider, context) {
10
+ this.provider = provider;
11
+ this.context = context;
12
+ this.history = [];
13
+ }
14
+
15
+ async plan(feature) {
16
+ const spinner = ora('🧠 Hive Mind: Planning feature implementation...').start();
17
+
18
+ // System prompt to force JSON output for the plan
19
+ const plannerPrompt = `
20
+ You are the Hive Mind Planner.
21
+ Your goal: Break down the feature "${feature}" into sequential atomic tasks for other agents.
22
+
23
+ Available Agents:
24
+ - @Backend (API, logic)
25
+ - @Frontend (UI, React)
26
+ - @Database (Schema, migrations)
27
+ - @Auth (Authentication)
28
+ - @Testing (Tests)
29
+
30
+ Output STRICT JSON format only:
31
+ {
32
+ "tasks": [
33
+ {
34
+ "id": 1,
35
+ "agent": "backend",
36
+ "task": "Create API endpoint for...",
37
+ "context": "Needs to handle..."
38
+ },
39
+ ...
40
+ ]
41
+ }
42
+ `;
43
+
44
+ try {
45
+ const result = await this.provider.generate(plannerPrompt, `Feature: ${feature}`);
46
+
47
+ // Attempt to parse JSON (handling potential markdown code blocks)
48
+ let jsonStr = result.content.trim();
49
+ if (jsonStr.startsWith('```json')) {
50
+ jsonStr = jsonStr.replace(/^```json\n?/, '').replace(/\n?```$/, '');
51
+ } else if (jsonStr.startsWith('```')) {
52
+ jsonStr = jsonStr.replace(/^```\n?/, '').replace(/\n?```$/, '');
53
+ }
54
+
55
+ const plan = JSON.parse(jsonStr);
56
+ spinner.succeed(`Plan generated: ${plan.tasks.length} tasks identified.`);
57
+ return plan.tasks;
58
+ } catch (error) {
59
+ spinner.fail('Planning failed.');
60
+ console.error(chalk.red(error.message));
61
+ return null;
62
+ }
63
+ }
64
+
65
+ async execute(tasks) {
66
+ console.log(chalk.bold('\nšŸ Swarm Execution Started\n'));
67
+
68
+ for (const task of tasks) {
69
+ console.log(chalk.bold.cyan(`\nšŸ”¹ Step ${task.id}: [${task.agent.toUpperCase()}] ${task.task}`));
70
+
71
+ // Inject previous history into context
72
+ const currentContext = {
73
+ ...this.context,
74
+ history: this.history.join('\n\n---\n\n')
75
+ };
76
+
77
+ try {
78
+ const output = await runAgentLoop(task.agent, task.task, this.provider, currentContext);
79
+
80
+ // Save to history
81
+ this.history.push(`## Task ${task.id} (${task.agent})\n**Goal:** ${task.task}\n\n**Output:**\n${output}`);
82
+
83
+ // Save artifact
84
+ const filename = `swarm-task-${task.id}-${task.agent}.md`;
85
+ await fs.writeFile(filename, output);
86
+ console.log(chalk.green(` āœ“ Output saved to ${filename}`));
87
+
88
+ } catch (error) {
89
+ console.log(chalk.red(` āŒ Task failed: ${error.message}`));
90
+ // Decide whether to continue or stop
91
+ // For now, we continue
92
+ }
93
+ }
94
+
95
+ console.log(chalk.bold.green('\nāœ… Swarm Mission Complete'));
96
+ }
97
+ }