ultra-dex 2.2.1 → 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.
- package/README.md +84 -128
- package/assets/agents/00-AGENT_INDEX.md +1 -1
- package/assets/docs/LAUNCH-POSTS.md +1 -1
- package/assets/docs/QUICK-REFERENCE.md +9 -4
- package/assets/docs/VISION-V2.md +1 -1
- package/assets/hooks/pre-commit +98 -0
- package/assets/saas-plan/04-Imp-Template.md +1 -1
- package/bin/ultra-dex.js +95 -4
- package/lib/commands/advanced.js +471 -0
- package/lib/commands/agent-builder.js +226 -0
- package/lib/commands/agents.js +99 -42
- package/lib/commands/auto-implement.js +68 -0
- package/lib/commands/build.js +73 -187
- package/lib/commands/ci-monitor.js +84 -0
- package/lib/commands/config.js +207 -0
- package/lib/commands/dashboard.js +770 -0
- package/lib/commands/diff.js +233 -0
- package/lib/commands/doctor.js +397 -0
- package/lib/commands/export.js +408 -0
- package/lib/commands/fix.js +96 -0
- package/lib/commands/generate.js +96 -72
- package/lib/commands/hooks.js +251 -76
- package/lib/commands/init.js +53 -1
- package/lib/commands/memory.js +80 -0
- package/lib/commands/plan.js +82 -0
- package/lib/commands/review.js +34 -5
- package/lib/commands/run.js +233 -0
- package/lib/commands/serve.js +177 -146
- package/lib/commands/state.js +354 -0
- package/lib/commands/swarm.js +284 -0
- package/lib/commands/sync.js +82 -23
- package/lib/commands/team.js +275 -0
- package/lib/commands/upgrade.js +190 -0
- package/lib/commands/validate.js +34 -0
- package/lib/commands/verify.js +81 -0
- package/lib/commands/watch.js +79 -0
- package/lib/mcp/graph.js +92 -0
- package/lib/mcp/memory.js +95 -0
- package/lib/mcp/resources.js +152 -0
- package/lib/mcp/server.js +34 -0
- package/lib/mcp/tools.js +481 -0
- package/lib/mcp/websocket.js +117 -0
- package/lib/providers/index.js +49 -4
- package/lib/providers/ollama.js +136 -0
- package/lib/providers/router.js +63 -0
- package/lib/quality/scanner.js +128 -0
- package/lib/swarm/coordinator.js +97 -0
- package/lib/swarm/index.js +598 -0
- package/lib/swarm/protocol.js +677 -0
- package/lib/swarm/tiers.js +485 -0
- package/lib/templates/custom-agent.md +10 -0
- package/lib/utils/files.js +14 -0
- package/lib/utils/graph.js +108 -0
- package/package.json +22 -13
package/lib/mcp/tools.js
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { loadState, saveState, generateMarkdown } from '../commands/plan.js';
|
|
5
|
+
import { projectGraph } from './graph.js';
|
|
6
|
+
import { swarmCommand } from '../commands/swarm.js';
|
|
7
|
+
import { ultraMemory } from './memory.js';
|
|
8
|
+
import { glob } from 'glob';
|
|
9
|
+
|
|
10
|
+
export function registerTools(server) {
|
|
11
|
+
// Tool: Remember Fact
|
|
12
|
+
server.tool(
|
|
13
|
+
"remember",
|
|
14
|
+
"Save a fact, decision, or piece of context to persistent memory for future reference",
|
|
15
|
+
{
|
|
16
|
+
text: z.string().describe("The fact or information to remember"),
|
|
17
|
+
tags: z.array(z.string()).optional().describe("Tags to categorize the information"),
|
|
18
|
+
source: z.string().optional().default("agent").describe("Source of the information")
|
|
19
|
+
},
|
|
20
|
+
async ({ text, tags, source }) => {
|
|
21
|
+
try {
|
|
22
|
+
await ultraMemory.remember(text, tags, source);
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: "text", text: `✅ Remembered: "${text.slice(0, 50)}..."` }]
|
|
25
|
+
};
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: `Failed to remember: ${error.message}` }]
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Tool: Recall Context
|
|
35
|
+
server.tool(
|
|
36
|
+
"recall",
|
|
37
|
+
"Search persistent memory for relevant past context, decisions, or facts",
|
|
38
|
+
{
|
|
39
|
+
query: z.string().describe("Search query to find relevant memories"),
|
|
40
|
+
limit: z.number().optional().default(5).describe("Maximum number of memories to return")
|
|
41
|
+
},
|
|
42
|
+
async ({ query, limit }) => {
|
|
43
|
+
try {
|
|
44
|
+
const results = await ultraMemory.search(query, limit);
|
|
45
|
+
if (results.length === 0) {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: "No relevant memories found." }]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const formatted = results.map(r =>
|
|
52
|
+
`[${new Date(r.timestamp).toLocaleDateString()}] (${r.source}) ${r.tags?.length ? '#' + r.tags.join(' #') : ''}\n${r.text}`
|
|
53
|
+
).join('\n\n---\n\n');
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: "text", text: `Relevant memories found:\n\n${formatted}` }]
|
|
57
|
+
};
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: "text", text: `Recall failed: ${error.message}` }]
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Tool: Clear Memory
|
|
67
|
+
server.tool(
|
|
68
|
+
"clear_memory",
|
|
69
|
+
"Clear all or part of the persistent memory",
|
|
70
|
+
{
|
|
71
|
+
before: z.string().optional().describe("Clear memories older than this date (ISO format)")
|
|
72
|
+
},
|
|
73
|
+
async ({ before }) => {
|
|
74
|
+
try {
|
|
75
|
+
await ultraMemory.clear(before);
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: "text", text: `✅ Memory cleared${before ? ' before ' + before : ''}.` }]
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
content: [{ type: "text", text: `Failed to clear memory: ${error.message}` }]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Tool: Start Swarm
|
|
88
|
+
server.tool(
|
|
89
|
+
"start_swarm",
|
|
90
|
+
"Start a multi-agent swarm workflow for a specific feature",
|
|
91
|
+
{
|
|
92
|
+
feature: z.string().describe("The feature or task to implement"),
|
|
93
|
+
provider: z.string().optional().describe("AI provider (claude, openai, gemini)"),
|
|
94
|
+
key: z.string().optional().describe("API Key for the provider")
|
|
95
|
+
},
|
|
96
|
+
async ({ feature, provider, key }) => {
|
|
97
|
+
try {
|
|
98
|
+
console.error(`[MCP] Starting Swarm for: ${feature}`);
|
|
99
|
+
// Run swarm command (this logs to stdout/stderr which MCP captures)
|
|
100
|
+
// We capture the output by intercepting the console logs or just trust the side effects
|
|
101
|
+
// Since swarmCommand is designed for CLI, we might need to wrap it or modify it to return result.
|
|
102
|
+
// For now, we trigger it and return a success message indicating it started.
|
|
103
|
+
|
|
104
|
+
await swarmCommand(feature, { provider, key });
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: "text", text: `✅ Swarm started for feature: "${feature}". Check server logs for progress.` }]
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: "text", text: `Swarm failed to start: ${error.message}` }]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Tool: Update Task Status
|
|
118
|
+
server.tool(
|
|
119
|
+
"update_task_status",
|
|
120
|
+
"Update the status of a task in the project plan",
|
|
121
|
+
{
|
|
122
|
+
taskId: z.string().describe("The ID of the task (e.g., '1.1', '2.3')"),
|
|
123
|
+
status: z.enum(['pending', 'in_progress', 'completed']).describe("New status")
|
|
124
|
+
},
|
|
125
|
+
async ({ taskId, status }) => {
|
|
126
|
+
const state = await loadState();
|
|
127
|
+
if (!state) return { content: [{ type: "text", text: "Error: No state found." }] };
|
|
128
|
+
|
|
129
|
+
let taskFound = false;
|
|
130
|
+
let oldStatus = '';
|
|
131
|
+
|
|
132
|
+
for (const phase of state.phases) {
|
|
133
|
+
const step = phase.steps.find(s => s.id === taskId);
|
|
134
|
+
if (step) {
|
|
135
|
+
oldStatus = step.status;
|
|
136
|
+
step.status = status;
|
|
137
|
+
taskFound = true;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (taskFound) {
|
|
143
|
+
const success = await saveState(state);
|
|
144
|
+
if (success) {
|
|
145
|
+
// Also update the Markdown plan file
|
|
146
|
+
const md = generateMarkdown(state);
|
|
147
|
+
await fs.writeFile(path.resolve(process.cwd(), 'IMPLEMENTATION-PLAN.md'), md);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: `✅ Task ${taskId} updated: ${oldStatus} -> ${status}` }]
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return { content: [{ type: "text", text: "Error: Failed to save state." }] };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { content: [{ type: "text", text: `Error: Task ${taskId} not found.` }] };
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Tool: Query Codebase Graph
|
|
161
|
+
server.tool(
|
|
162
|
+
"query_codebase",
|
|
163
|
+
"Search the codebase structure and dependencies",
|
|
164
|
+
{
|
|
165
|
+
query: z.string().describe("Search term or file name"),
|
|
166
|
+
type: z.enum(['files', 'dependencies', 'reverse_deps']).default('files')
|
|
167
|
+
},
|
|
168
|
+
async ({ query, type }) => {
|
|
169
|
+
// Ensure graph is populated
|
|
170
|
+
if (projectGraph.nodes.size === 0) {
|
|
171
|
+
await projectGraph.scan();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const summary = projectGraph.getSummary();
|
|
175
|
+
|
|
176
|
+
if (type === 'files') {
|
|
177
|
+
const matches = summary.files.filter(f => f.toLowerCase().includes(query.toLowerCase()));
|
|
178
|
+
return {
|
|
179
|
+
content: [{ type: "text", text: `Found ${matches.length} files matching '${query}':\n${matches.slice(0, 20).join('\n')}${matches.length > 20 ? '\n...' : ''}` }]
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (type === 'dependencies') {
|
|
184
|
+
const deps = summary.dependencies.filter(e => e.from.includes(query));
|
|
185
|
+
return {
|
|
186
|
+
content: [{ type: "text", text: `Dependencies for files matching '${query}':\n${deps.map(d => `${d.from} -> ${d.to}`).slice(0, 20).join('\n')}` }]
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (type === 'reverse_deps') {
|
|
191
|
+
const refs = summary.dependencies.filter(e => e.to.includes(query));
|
|
192
|
+
return {
|
|
193
|
+
content: [{ type: "text", text: `Files depending on '${query}':\n${refs.map(d => `${d.from}`).slice(0, 20).join('\n')}` }]
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { content: [{ type: "text", text: "Invalid query type." }] };
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Tool: Verify Task
|
|
202
|
+
server.tool(
|
|
203
|
+
"verify_task",
|
|
204
|
+
"Run the 21-step verification framework for a specific task",
|
|
205
|
+
{
|
|
206
|
+
taskName: z.string().describe("The name or ID of the task to verify")
|
|
207
|
+
},
|
|
208
|
+
async ({ taskName }) => {
|
|
209
|
+
try {
|
|
210
|
+
const state = await loadState();
|
|
211
|
+
if (!state) {
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: "Error: No project state found. Run `ultra-dex init` first." }]
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let taskFound = null;
|
|
218
|
+
for (const phase of state.phases) {
|
|
219
|
+
if (phase.steps) {
|
|
220
|
+
const step = phase.steps.find(s => s.id === taskName || s.task.includes(taskName));
|
|
221
|
+
if (step) {
|
|
222
|
+
taskFound = step;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const checklist = [
|
|
229
|
+
"1. Atomic Scope Defined", "2. Context Loaded", "3. Architecture Alignment",
|
|
230
|
+
"4. Security Patterns Applied", "5. Type Safety Check", "6. Error Handling Strategy",
|
|
231
|
+
"7. API Documentation Updated", "8. Database Schema Verified", "9. Environment Variables Set",
|
|
232
|
+
"10. Implementation Complete", "11. Console Logs Removed", "12. Edge Cases Handled",
|
|
233
|
+
"13. Performance Check", "14. Accessibility (A11y) Check", "15. Cross-browser Check",
|
|
234
|
+
"16. Unit Tests Passed", "17. Integration Tests Passed", "18. Linting & Formatting",
|
|
235
|
+
"19. Code Review Approved", "20. Migration Scripts Ready", "21. Deployment Readiness"
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
const report = taskFound
|
|
239
|
+
? `Verification Report for '${taskFound.task}' (${taskFound.id}):\nStatus: ${taskFound.status}\n\n`
|
|
240
|
+
: `General Verification Report for '${taskName}':\n\n`;
|
|
241
|
+
|
|
242
|
+
const fullReport = report + checklist.map((step, i) => `[ ] ${step}`).join('\n');
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
content: [{
|
|
246
|
+
type: "text",
|
|
247
|
+
text: fullReport
|
|
248
|
+
}]
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return {
|
|
253
|
+
content: [{ type: "text", text: `Verification failed: ${error.message}` }]
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Tool: Read Code
|
|
260
|
+
server.tool(
|
|
261
|
+
"read_code",
|
|
262
|
+
"Read a file from the codebase",
|
|
263
|
+
{
|
|
264
|
+
filePath: z.string().describe("Path to the file relative to project root")
|
|
265
|
+
},
|
|
266
|
+
async ({ filePath }) => {
|
|
267
|
+
try {
|
|
268
|
+
const fullPath = path.resolve(process.cwd(), filePath);
|
|
269
|
+
// Security check: ensure path is within process.cwd()
|
|
270
|
+
if (!fullPath.startsWith(process.cwd())) {
|
|
271
|
+
throw new Error("Access denied: Path outside project root");
|
|
272
|
+
}
|
|
273
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
274
|
+
return {
|
|
275
|
+
content: [{ type: "text", text: content }]
|
|
276
|
+
};
|
|
277
|
+
} catch (error) {
|
|
278
|
+
return {
|
|
279
|
+
content: [{ type: "text", text: `Error reading file: ${error.message}` }]
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Tool: Write Code (God Mode)
|
|
286
|
+
server.tool(
|
|
287
|
+
"write_code",
|
|
288
|
+
"Write or update a file in the codebase",
|
|
289
|
+
{
|
|
290
|
+
filePath: z.string().describe("Path to the file relative to project root"),
|
|
291
|
+
content: z.string().describe("The new content for the file"),
|
|
292
|
+
description: z.string().optional().describe("Description of the change for audit logs")
|
|
293
|
+
},
|
|
294
|
+
async ({ filePath, content, description }) => {
|
|
295
|
+
try {
|
|
296
|
+
const fullPath = path.resolve(process.cwd(), filePath);
|
|
297
|
+
if (!fullPath.startsWith(process.cwd())) {
|
|
298
|
+
throw new Error("Access denied: Path outside project root");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Ensure directory exists
|
|
302
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
303
|
+
await fs.writeFile(fullPath, content, 'utf8');
|
|
304
|
+
|
|
305
|
+
// Log to stderr for server visibility
|
|
306
|
+
console.error(`[MCP] Write: ${filePath} - ${description || 'No description'}`);
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: "text", text: `Successfully wrote ${filePath}` }]
|
|
310
|
+
};
|
|
311
|
+
} catch (error) {
|
|
312
|
+
return {
|
|
313
|
+
content: [{ type: "text", text: `Error writing file: ${error.message}` }]
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// Tool: Search Code (Graph-Aware)
|
|
320
|
+
server.tool(
|
|
321
|
+
"search_code",
|
|
322
|
+
"Search for symbols, functions, or patterns using the Code Property Graph",
|
|
323
|
+
{
|
|
324
|
+
query: z.string().describe("The symbol or function name to search for"),
|
|
325
|
+
useGraph: z.boolean().default(true).describe("Use structural graph search instead of text grep")
|
|
326
|
+
},
|
|
327
|
+
async ({ query, useGraph }) => {
|
|
328
|
+
try {
|
|
329
|
+
const { buildGraph, queryGraph } = await import('../utils/graph.js');
|
|
330
|
+
const graph = await buildGraph();
|
|
331
|
+
|
|
332
|
+
if (useGraph) {
|
|
333
|
+
const nodes = queryGraph(graph, query);
|
|
334
|
+
if (nodes.length > 0) {
|
|
335
|
+
return {
|
|
336
|
+
content: [{ type: "text", text: `Found structural matches in graph:\n${JSON.stringify(nodes, null, 2)}` }]
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Fallback to basic text search
|
|
342
|
+
const files = await glob('**/*.{js,ts,jsx,tsx,md,json}', {
|
|
343
|
+
ignore: ['node_modules/**', '.git/**', 'dist/**'],
|
|
344
|
+
nodir: true
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const results = [];
|
|
348
|
+
for (const file of files) {
|
|
349
|
+
const content = await fs.readFile(file, 'utf8');
|
|
350
|
+
if (content.includes(query)) {
|
|
351
|
+
results.push(file);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
content: [{ type: "text", text: `Matches found in files:\n${results.join('\n')}` }]
|
|
357
|
+
};
|
|
358
|
+
} catch (error) {
|
|
359
|
+
return {
|
|
360
|
+
content: [{ type: "text", text: `Search failed: ${error.message}` }]
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// Tool: Analyze Impact
|
|
367
|
+
server.tool(
|
|
368
|
+
"analyze_impact",
|
|
369
|
+
"Determine which files or functions will be impacted by changing a specific file",
|
|
370
|
+
{
|
|
371
|
+
filePath: z.string().describe("The file path to analyze")
|
|
372
|
+
},
|
|
373
|
+
async ({ filePath }) => {
|
|
374
|
+
try {
|
|
375
|
+
const { buildGraph, getImpactAnalysis } = await import('../utils/graph.js');
|
|
376
|
+
const graph = await buildGraph();
|
|
377
|
+
const impacts = getImpactAnalysis(graph, filePath);
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
content: [{
|
|
381
|
+
type: "text",
|
|
382
|
+
text: impacts.length > 0
|
|
383
|
+
? `Changing ${filePath} may impact the following files:\n- ${impacts.join('\n- ')}`
|
|
384
|
+
: `No direct dependents found for ${filePath}.`
|
|
385
|
+
}]
|
|
386
|
+
};
|
|
387
|
+
} catch (error) {
|
|
388
|
+
return {
|
|
389
|
+
content: [{ type: "text", text: `Impact analysis failed: ${error.message}` }]
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
// Tool: Get Agent Prompt
|
|
396
|
+
server.tool(
|
|
397
|
+
"get_agent",
|
|
398
|
+
"Get the system prompt for a specialized agent",
|
|
399
|
+
{
|
|
400
|
+
agentName: z.string().describe("Name of the agent (e.g., 'backend', 'planner', 'cto')")
|
|
401
|
+
},
|
|
402
|
+
async ({ agentName }) => {
|
|
403
|
+
const lowerName = agentName.toLowerCase();
|
|
404
|
+
const potentialPaths = [
|
|
405
|
+
`agents/1-leadership/${lowerName}.md`,
|
|
406
|
+
`agents/2-development/${lowerName}.md`,
|
|
407
|
+
`agents/3-security/${lowerName}.md`,
|
|
408
|
+
`agents/4-devops/${lowerName}.md`,
|
|
409
|
+
`agents/5-quality/${lowerName}.md`,
|
|
410
|
+
`agents/6-specialist/${lowerName}.md`,
|
|
411
|
+
`agents/0-orchestration/${lowerName}.md`,
|
|
412
|
+
`agents/${lowerName}.md`
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
for (const p of potentialPaths) {
|
|
416
|
+
try {
|
|
417
|
+
const fullPath = path.resolve(process.cwd(), p);
|
|
418
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
419
|
+
return {
|
|
420
|
+
content: [{ type: "text", text: content }]
|
|
421
|
+
};
|
|
422
|
+
} catch (e) {
|
|
423
|
+
// continue
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
content: [{ type: "text", text: `Agent '${agentName}' not found. List of agents available in agents/00-AGENT_INDEX.md` }]
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
// Tool: Start Swarm (Agent Orchestration)
|
|
434
|
+
server.tool(
|
|
435
|
+
"start_swarm",
|
|
436
|
+
"Trigger a multi-agent swarm to plan and implement a feature",
|
|
437
|
+
{
|
|
438
|
+
feature: z.string().describe("Description of the feature to build"),
|
|
439
|
+
mode: z.enum(['plan_only', 'execute']).default('plan_only').describe("Whether to just plan or also execute")
|
|
440
|
+
},
|
|
441
|
+
async ({ feature, mode }) => {
|
|
442
|
+
try {
|
|
443
|
+
const { runAgentLoop } = await import('../commands/run.js');
|
|
444
|
+
const { createProvider, getDefaultProvider } = await import('../providers/index.js');
|
|
445
|
+
const { loadState } = await import('../commands/plan.js');
|
|
446
|
+
const { projectGraph } = await import('./graph.js');
|
|
447
|
+
|
|
448
|
+
// Setup Context
|
|
449
|
+
const state = await loadState();
|
|
450
|
+
const context = {
|
|
451
|
+
state,
|
|
452
|
+
plan: state ? generateMarkdown(state) : '',
|
|
453
|
+
graph: projectGraph.getSummary()
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const provider = createProvider(getDefaultProvider(), { maxTokens: 8000 });
|
|
457
|
+
|
|
458
|
+
// Step 1: Planning
|
|
459
|
+
const planOutput = await runAgentLoop('planner', feature, provider, context);
|
|
460
|
+
|
|
461
|
+
if (mode === 'plan_only') {
|
|
462
|
+
return {
|
|
463
|
+
content: [{ type: "text", text: `Swarm Planning Complete:\n\n${planOutput}` }]
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Step 2: Execution (Simplified for MCP - invoking CTO)
|
|
468
|
+
const execOutput = await runAgentLoop('cto', `Execute this plan:\n${planOutput}`, provider, context);
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
content: [{ type: "text", text: `Swarm Execution Complete:\n\n${execOutput}` }]
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
} catch (error) {
|
|
475
|
+
return {
|
|
476
|
+
content: [{ type: "text", text: `Swarm failed: ${error.message}` }]
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
}
|
|
@@ -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
|
+
}
|
package/lib/providers/index.js
CHANGED
|
@@ -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 };
|