ultra-dex 2.2.1 → 3.2.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 (87) hide show
  1. package/README.md +112 -151
  2. package/assets/agents/00-AGENT_INDEX.md +1 -1
  3. package/assets/code-patterns/clerk-middleware.ts +138 -0
  4. package/assets/code-patterns/prisma-schema.prisma +224 -0
  5. package/assets/code-patterns/rls-policies.sql +246 -0
  6. package/assets/code-patterns/server-actions.ts +191 -0
  7. package/assets/code-patterns/trpc-router.ts +258 -0
  8. package/assets/cursor-rules/13-ai-integration.mdc +155 -0
  9. package/assets/cursor-rules/14-server-components.mdc +81 -0
  10. package/assets/cursor-rules/15-server-actions.mdc +102 -0
  11. package/assets/cursor-rules/16-edge-middleware.mdc +105 -0
  12. package/assets/cursor-rules/17-streaming-ssr.mdc +138 -0
  13. package/assets/docs/LAUNCH-POSTS.md +1 -1
  14. package/assets/docs/QUICK-REFERENCE.md +9 -4
  15. package/assets/docs/VISION-V2.md +1 -1
  16. package/assets/hooks/pre-commit +98 -0
  17. package/assets/saas-plan/04-Imp-Template.md +1 -1
  18. package/bin/ultra-dex.js +132 -4
  19. package/lib/commands/advanced.js +471 -0
  20. package/lib/commands/agent-builder.js +226 -0
  21. package/lib/commands/agents.js +102 -42
  22. package/lib/commands/auto-implement.js +68 -0
  23. package/lib/commands/banner.js +43 -21
  24. package/lib/commands/build.js +78 -183
  25. package/lib/commands/ci-monitor.js +84 -0
  26. package/lib/commands/config.js +207 -0
  27. package/lib/commands/dashboard.js +770 -0
  28. package/lib/commands/diff.js +233 -0
  29. package/lib/commands/doctor.js +416 -0
  30. package/lib/commands/export.js +408 -0
  31. package/lib/commands/fix.js +96 -0
  32. package/lib/commands/generate.js +105 -78
  33. package/lib/commands/hooks.js +251 -76
  34. package/lib/commands/init.js +102 -54
  35. package/lib/commands/memory.js +80 -0
  36. package/lib/commands/plan.js +82 -0
  37. package/lib/commands/review.js +34 -5
  38. package/lib/commands/run.js +233 -0
  39. package/lib/commands/scaffold.js +151 -0
  40. package/lib/commands/serve.js +179 -146
  41. package/lib/commands/state.js +327 -0
  42. package/lib/commands/swarm.js +306 -0
  43. package/lib/commands/sync.js +82 -23
  44. package/lib/commands/team.js +275 -0
  45. package/lib/commands/upgrade.js +190 -0
  46. package/lib/commands/validate.js +34 -0
  47. package/lib/commands/verify.js +81 -0
  48. package/lib/commands/watch.js +79 -0
  49. package/lib/config/theme.js +47 -0
  50. package/lib/mcp/graph.js +92 -0
  51. package/lib/mcp/memory.js +95 -0
  52. package/lib/mcp/resources.js +152 -0
  53. package/lib/mcp/server.js +34 -0
  54. package/lib/mcp/tools.js +481 -0
  55. package/lib/mcp/websocket.js +117 -0
  56. package/lib/providers/index.js +49 -4
  57. package/lib/providers/ollama.js +136 -0
  58. package/lib/providers/router.js +63 -0
  59. package/lib/quality/scanner.js +128 -0
  60. package/lib/swarm/coordinator.js +97 -0
  61. package/lib/swarm/index.js +598 -0
  62. package/lib/swarm/protocol.js +677 -0
  63. package/lib/swarm/tiers.js +485 -0
  64. package/lib/templates/code/clerk-middleware.ts +138 -0
  65. package/lib/templates/code/prisma-schema.prisma +224 -0
  66. package/lib/templates/code/rls-policies.sql +246 -0
  67. package/lib/templates/code/server-actions.ts +191 -0
  68. package/lib/templates/code/trpc-router.ts +258 -0
  69. package/lib/templates/custom-agent.md +10 -0
  70. package/lib/themes/doomsday.js +229 -0
  71. package/lib/ui/index.js +5 -0
  72. package/lib/ui/interface.js +241 -0
  73. package/lib/ui/spinners.js +116 -0
  74. package/lib/ui/theme.js +183 -0
  75. package/lib/utils/agents.js +32 -0
  76. package/lib/utils/files.js +14 -0
  77. package/lib/utils/graph.js +108 -0
  78. package/lib/utils/help.js +64 -0
  79. package/lib/utils/messages.js +35 -0
  80. package/lib/utils/progress.js +24 -0
  81. package/lib/utils/prompts.js +47 -0
  82. package/lib/utils/spinners.js +46 -0
  83. package/lib/utils/status.js +31 -0
  84. package/lib/utils/tables.js +41 -0
  85. package/lib/utils/theme-state.js +9 -0
  86. package/lib/utils/version-display.js +32 -0
  87. package/package.json +31 -13
@@ -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
+ }
@@ -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 };