vutler-nexus 1.0.0 ā 2.0.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/bin/cli.js +101 -30
- package/lib/config.js +7 -0
- package/lib/orchestrator-cloud.js +236 -0
- package/lib/vutler-client.js +137 -0
- package/lib/web-server.js +118 -46
- package/package.json +5 -4
package/bin/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ const { NexusTunnel } = require('../lib/tunnel');
|
|
|
14
14
|
const { AgentRuntime } = require('../lib/agent-runtime');
|
|
15
15
|
const { WebServer } = require('../lib/web-server');
|
|
16
16
|
const { listProviders } = require('../lib/llm-providers');
|
|
17
|
+
const NexusCloudOrchestrator = require('../lib/orchestrator-cloud');
|
|
17
18
|
const https = require('https');
|
|
18
19
|
const http = require('http');
|
|
19
20
|
const fs = require('fs');
|
|
@@ -246,7 +247,43 @@ program
|
|
|
246
247
|
const workspace = await ask('Workspace path', path.join(process.env.HOME || process.cwd(), '.vutler', 'workspace'));
|
|
247
248
|
const webPort = parseInt(await ask('Web interface port', '3939')) || 3939;
|
|
248
249
|
|
|
249
|
-
//
|
|
250
|
+
// Runtime Mode Selection (Local vs Cloud)
|
|
251
|
+
console.log(chalk.blue('\nš Execution Mode:'));
|
|
252
|
+
const modes = [
|
|
253
|
+
{ key: 'local', name: 'Local (Claude CLI)', desc: 'Run agents locally using Claude CLI' },
|
|
254
|
+
{ key: 'cloud', name: 'Cloud (Vutler API)', desc: 'Route tasks to Vutler cloud agents' }
|
|
255
|
+
];
|
|
256
|
+
modes.forEach((m, i) => {
|
|
257
|
+
console.log(` ${i + 1}. ${chalk.cyan(m.name)} - ${m.desc}`);
|
|
258
|
+
});
|
|
259
|
+
const modeIndex = parseInt(await ask('Select execution mode (number)', '1')) || 1;
|
|
260
|
+
const selectedMode = modes[modeIndex - 1];
|
|
261
|
+
|
|
262
|
+
if (!selectedMode) {
|
|
263
|
+
error('Invalid mode selection');
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let cloudConfig = null;
|
|
268
|
+
if (selectedMode.key === 'cloud') {
|
|
269
|
+
console.log(chalk.green(`\nš” Cloud Mode Configuration:`));
|
|
270
|
+
const vutlerUrl = await ask('Vutler API URL', 'https://app.vutler.ai');
|
|
271
|
+
const apiKey = await askPassword('VUTLER_API_KEY');
|
|
272
|
+
|
|
273
|
+
if (!apiKey) {
|
|
274
|
+
warn('ā ļø No API key provided. Cloud mode requires VUTLER_API_KEY');
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
cloudConfig = {
|
|
279
|
+
mode: 'cloud',
|
|
280
|
+
vutlerUrl,
|
|
281
|
+
vutlerApiKey: apiKey
|
|
282
|
+
};
|
|
283
|
+
console.log(chalk.green('ā
Cloud mode configured'));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// LLM Provider Selection (only for local mode)
|
|
250
287
|
console.log(chalk.blue('\nš§ LLM Provider Selection:'));
|
|
251
288
|
|
|
252
289
|
// Check available providers
|
|
@@ -354,11 +391,14 @@ program
|
|
|
354
391
|
|
|
355
392
|
// Build configuration
|
|
356
393
|
const newConfig = {
|
|
394
|
+
mode: cloudConfig?.mode || 'local',
|
|
395
|
+
vutlerUrl: cloudConfig?.vutlerUrl,
|
|
396
|
+
vutlerApiKey: cloudConfig?.vutlerApiKey,
|
|
357
397
|
cloudUrl,
|
|
358
398
|
token: token || null,
|
|
359
399
|
workspace,
|
|
360
400
|
webPort,
|
|
361
|
-
llm: llmConfig,
|
|
401
|
+
llm: cloudConfig ? { provider: 'cloud' } : llmConfig,
|
|
362
402
|
auth: authResult || undefined,
|
|
363
403
|
agent: {
|
|
364
404
|
name: agentName,
|
|
@@ -396,7 +436,12 @@ program
|
|
|
396
436
|
|
|
397
437
|
console.log(chalk.green(`\nā
Configuration saved to ${config.CONFIG_FILE}`));
|
|
398
438
|
console.log(` Agent: ${chalk.cyan(agentName)}`);
|
|
399
|
-
console.log(`
|
|
439
|
+
console.log(` Mode: ${chalk.cyan(selectedMode.name)}`);
|
|
440
|
+
if (cloudConfig) {
|
|
441
|
+
console.log(` Vutler API: ${chalk.cyan(cloudConfig.vutlerUrl)}`);
|
|
442
|
+
} else {
|
|
443
|
+
console.log(` LLM: ${chalk.cyan(providerData.name)}`);
|
|
444
|
+
}
|
|
400
445
|
console.log(` Workspace: ${chalk.cyan(workspace)}`);
|
|
401
446
|
console.log(` Web UI: ${chalk.cyan(`http://localhost:${webPort}`)}`);
|
|
402
447
|
if (token) {
|
|
@@ -424,8 +469,18 @@ program
|
|
|
424
469
|
log('š Starting Vutler Nexus Agent Runtime...\n');
|
|
425
470
|
|
|
426
471
|
try {
|
|
472
|
+
// Check mode
|
|
473
|
+
if (cfg.mode === 'cloud') {
|
|
474
|
+
// Set environment variable for cloud orchestrator
|
|
475
|
+
if (cfg.vutlerApiKey) {
|
|
476
|
+
process.env.VUTLER_API_KEY = cfg.vutlerApiKey;
|
|
477
|
+
}
|
|
478
|
+
log(`āļø Cloud Mode - Vutler API: ${cfg.vutlerUrl || 'https://app.vutler.ai'}`);
|
|
479
|
+
}
|
|
480
|
+
|
|
427
481
|
// Initialize agent runtime
|
|
428
|
-
|
|
482
|
+
const providerName = cfg.mode === 'cloud' ? 'Cloud (Vutler API)' : (cfg.llm?.provider || 'unknown');
|
|
483
|
+
log(`š§ Initializing Agent Runtime (${providerName})...`);
|
|
429
484
|
agentRuntime = new AgentRuntime(cfg);
|
|
430
485
|
success('ā
Agent Runtime ready');
|
|
431
486
|
|
|
@@ -492,8 +547,10 @@ program
|
|
|
492
547
|
console.log(chalk.blue(` Cloud: ${cfg.cloudUrl}`));
|
|
493
548
|
}
|
|
494
549
|
|
|
495
|
-
|
|
496
|
-
|
|
550
|
+
if (cfg.mode !== 'cloud') {
|
|
551
|
+
const llmProviderName = providerInfo[cfg.llm?.provider]?.name || cfg.llm?.provider || 'Unknown';
|
|
552
|
+
console.log(chalk.blue(` LLM: ${llmProviderName}`));
|
|
553
|
+
}
|
|
497
554
|
console.log(chalk.gray('\n Press Ctrl+C to stop'));
|
|
498
555
|
|
|
499
556
|
} catch (error) {
|
|
@@ -561,36 +618,50 @@ program
|
|
|
561
618
|
console.log(` Web Port: ${cfg.webPort || 'not set'}`);
|
|
562
619
|
|
|
563
620
|
// LLM status
|
|
621
|
+
// Determine execution mode
|
|
622
|
+
const mode = cfg.mode || 'local';
|
|
623
|
+
console.log(chalk.blue('\nš Execution Mode:'));
|
|
624
|
+
console.log(` Mode: ${mode === 'cloud' ? 'Cloud (Vutler API)' : 'Local'}`);
|
|
625
|
+
|
|
626
|
+
if (mode === 'cloud') {
|
|
627
|
+
console.log(` Vutler API: ${cfg.vutlerUrl || 'https://app.vutler.ai'}`);
|
|
628
|
+
console.log(` API Key: ${cfg.vutlerApiKey ? cfg.vutlerApiKey.slice(0, 8) + '...' : 'not set'}`);
|
|
629
|
+
}
|
|
630
|
+
|
|
564
631
|
console.log(chalk.blue('\nš§ LLM Configuration:'));
|
|
565
|
-
const provider = cfg.llm?.provider || 'not set';
|
|
566
|
-
const providerName = providerInfo[provider]?.name || provider;
|
|
632
|
+
const provider = mode === 'cloud' ? 'cloud' : (cfg.llm?.provider || 'not set');
|
|
633
|
+
const providerName = mode === 'cloud' ? 'Cloud Agents' : (providerInfo[provider]?.name || provider);
|
|
567
634
|
console.log(` Provider: ${providerName}`);
|
|
568
635
|
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
636
|
+
if (mode !== 'cloud') {
|
|
637
|
+
if (provider === 'claude-code') {
|
|
638
|
+
const claudeStatus = await checkClaudeCLI();
|
|
639
|
+
if (claudeStatus.available) {
|
|
640
|
+
console.log(` Claude CLI: ā
${claudeStatus.version}`);
|
|
641
|
+
} else {
|
|
642
|
+
console.log(` Claude CLI: ā not found (install with: pip install claude-cli)`);
|
|
643
|
+
}
|
|
644
|
+
console.log(` Cost: $0 (uses Max subscription)`);
|
|
645
|
+
} else if (provider === 'ollama') {
|
|
646
|
+
const ollamaStatus = await checkOllama();
|
|
647
|
+
if (ollamaStatus.available) {
|
|
648
|
+
console.log(` Ollama: ā
running (${ollamaStatus.models.length} models)`);
|
|
649
|
+
} else {
|
|
650
|
+
console.log(` Ollama: ā not running (start with: ollama serve)`);
|
|
651
|
+
}
|
|
652
|
+
console.log(` Cost: $0 (local models)`);
|
|
653
|
+
} else if (providerInfo[provider]?.requiresApiKey) {
|
|
654
|
+
console.log(` API Key: ${cfg.llm?.apiKey ? cfg.llm.apiKey.slice(0, 8) + '...' : 'not set'}`);
|
|
655
|
+
console.log(` Cost: per token usage`);
|
|
583
656
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
console.log(`
|
|
587
|
-
console.log(`
|
|
657
|
+
|
|
658
|
+
console.log(` Model: ${cfg.llm?.model || 'not set'}`);
|
|
659
|
+
console.log(` Max Tokens: ${cfg.llm?.maxTokens || 'not set'}`);
|
|
660
|
+
console.log(` Temperature: ${cfg.llm?.temperature || 'not set'}`);
|
|
661
|
+
} else {
|
|
662
|
+
console.log(` Cost: per cloud agent usage`);
|
|
588
663
|
}
|
|
589
664
|
|
|
590
|
-
console.log(` Model: ${cfg.llm?.model || 'not set'}`);
|
|
591
|
-
console.log(` Max Tokens: ${cfg.llm?.maxTokens || 'not set'}`);
|
|
592
|
-
console.log(` Temperature: ${cfg.llm?.temperature || 'not set'}`);
|
|
593
|
-
|
|
594
665
|
// Cloud status
|
|
595
666
|
console.log(chalk.blue('\nāļø Cloud Integration:'));
|
|
596
667
|
console.log(` Cloud URL: ${cfg.cloudUrl || 'not set'}`);
|
package/lib/config.js
CHANGED
|
@@ -139,6 +139,13 @@ function canRunOffline() {
|
|
|
139
139
|
|
|
140
140
|
function canUseLLM() {
|
|
141
141
|
const config = read();
|
|
142
|
+
|
|
143
|
+
// Cloud mode
|
|
144
|
+
if (config.mode === 'cloud') {
|
|
145
|
+
return !!(config.vutlerApiKey);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Local mode
|
|
142
149
|
if (config.llm?.provider === 'claude-code') {
|
|
143
150
|
return true; // Assume claude CLI is available
|
|
144
151
|
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
const VutlerClient = require('./vutler-client');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Nexus Cloud Orchestrator
|
|
8
|
+
*
|
|
9
|
+
* Routes tasks to Vutler cloud agents instead of local execution
|
|
10
|
+
*/
|
|
11
|
+
class NexusCloudOrchestrator {
|
|
12
|
+
constructor(configPath = null) {
|
|
13
|
+
// Initialize with default config if no path provided
|
|
14
|
+
if (configPath) {
|
|
15
|
+
this.configPath = configPath.replace('~', os.homedir());
|
|
16
|
+
this.config = this.loadConfig();
|
|
17
|
+
} else {
|
|
18
|
+
// Use default minimal config when running from CLI
|
|
19
|
+
this.config = this.getDefaultConfig();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.client = new VutlerClient({
|
|
23
|
+
baseUrl: this.config.vutlerUrl,
|
|
24
|
+
apiKey: process.env.VUTLER_API_KEY
|
|
25
|
+
});
|
|
26
|
+
this.stats = {
|
|
27
|
+
tasksCompleted: 0,
|
|
28
|
+
totalCost: 0,
|
|
29
|
+
agentUsage: {}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get default cloud configuration
|
|
35
|
+
*/
|
|
36
|
+
getDefaultConfig() {
|
|
37
|
+
return {
|
|
38
|
+
vutlerUrl: process.env.VUTLER_URL || 'https://app.vutler.ai',
|
|
39
|
+
agents: [
|
|
40
|
+
{ id: 'default', name: 'Default Cloud Agent', enabled: true, location: 'Vutler Cloud' }
|
|
41
|
+
],
|
|
42
|
+
routing: {
|
|
43
|
+
default: 'default'
|
|
44
|
+
},
|
|
45
|
+
limits: {
|
|
46
|
+
timeoutSeconds: 300
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load agent configuration from file
|
|
53
|
+
*/
|
|
54
|
+
loadConfig() {
|
|
55
|
+
if (!this.configPath || !fs.existsSync(this.configPath)) {
|
|
56
|
+
// Fall back to default if file doesn't exist
|
|
57
|
+
return this.getDefaultConfig();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const raw = fs.readFileSync(this.configPath, 'utf8');
|
|
62
|
+
const config = JSON.parse(raw);
|
|
63
|
+
|
|
64
|
+
// Validate
|
|
65
|
+
if (!config.agents || config.agents.length === 0) {
|
|
66
|
+
console.warn('No agents defined in config, using default');
|
|
67
|
+
return this.getDefaultConfig();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!config.routing || !config.routing.default) {
|
|
71
|
+
console.warn('Routing config missing, using default');
|
|
72
|
+
return this.getDefaultConfig();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return config;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.warn(`Failed to load config: ${error.message}, using default`);
|
|
78
|
+
return this.getDefaultConfig();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Route a task to the best cloud agent
|
|
84
|
+
*/
|
|
85
|
+
routeTask(task, options = {}) {
|
|
86
|
+
// Force specific agent if requested
|
|
87
|
+
if (options.forceAgent) {
|
|
88
|
+
const agent = this.config.agents.find(a => a.id === options.forceAgent);
|
|
89
|
+
if (agent && agent.enabled) {
|
|
90
|
+
return options.forceAgent;
|
|
91
|
+
}
|
|
92
|
+
console.warn(`Forced agent ${options.forceAgent} not available, falling back`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check keywords
|
|
96
|
+
const taskLower = task.toLowerCase();
|
|
97
|
+
|
|
98
|
+
for (const [keyword, agentIds] of Object.entries(this.config.routing.keywords || {})) {
|
|
99
|
+
if (taskLower.includes(keyword)) {
|
|
100
|
+
// Pick first enabled agent for this keyword
|
|
101
|
+
for (const agentId of agentIds) {
|
|
102
|
+
const agent = this.config.agents.find(a => a.id === agentId);
|
|
103
|
+
if (agent && agent.enabled) {
|
|
104
|
+
console.log(`šÆ Routing to ${agent.name} (keyword: "${keyword}")`);
|
|
105
|
+
return agentId;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Default agent
|
|
112
|
+
console.log(`š Routing to default agent: ${this.config.routing.default}`);
|
|
113
|
+
return this.config.routing.default;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Execute a task via cloud agent
|
|
118
|
+
*/
|
|
119
|
+
async executeTask(task, options = {}) {
|
|
120
|
+
const agentId = options.agentId || this.routeTask(task, options);
|
|
121
|
+
const agent = this.config.agents.find(a => a.id === agentId);
|
|
122
|
+
|
|
123
|
+
if (!agent) {
|
|
124
|
+
throw new Error(`Agent not found: ${agentId}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!agent.enabled) {
|
|
128
|
+
console.warn(`Agent ${agentId} disabled, falling back to default`);
|
|
129
|
+
return this.executeTask(task, { ...options, agentId: this.config.routing.default, noFallback: true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(`\nāļø ${agent.name} (cloud)`);
|
|
133
|
+
console.log(` Location: ${agent.location || 'Vutler Cloud'}`);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const result = await this.client.executeTask(agentId, task, {
|
|
137
|
+
context: options.context,
|
|
138
|
+
timeout: options.timeout || this.config.limits?.timeoutSeconds * 1000,
|
|
139
|
+
userId: options.userId
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Track stats
|
|
143
|
+
this.stats.tasksCompleted++;
|
|
144
|
+
this.stats.totalCost += result.cost;
|
|
145
|
+
this.stats.agentUsage[agentId] = (this.stats.agentUsage[agentId] || 0) + 1;
|
|
146
|
+
|
|
147
|
+
// Log to tracking file if enabled
|
|
148
|
+
if (this.config.tracking?.enabled) {
|
|
149
|
+
this.logTask(task, result);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(`\nā Cloud execution failed: ${error.message}`);
|
|
156
|
+
|
|
157
|
+
// Fallback to default agent if not already using it
|
|
158
|
+
if (agentId !== this.config.routing.default && !options.noFallback) {
|
|
159
|
+
console.log(`\nš Falling back to ${this.config.routing.default}...`);
|
|
160
|
+
return this.executeTask(task, { ...options, agentId: this.config.routing.default, noFallback: true });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Log task execution to JSONL file
|
|
169
|
+
*/
|
|
170
|
+
logTask(task, result) {
|
|
171
|
+
const logPath = this.config.tracking.logPath.replace('~', os.homedir());
|
|
172
|
+
const logDir = path.dirname(logPath);
|
|
173
|
+
|
|
174
|
+
if (!fs.existsSync(logDir)) {
|
|
175
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const entry = JSON.stringify({
|
|
179
|
+
timestamp: result.timestamp,
|
|
180
|
+
task: task.substring(0, 200),
|
|
181
|
+
agentId: result.agentId,
|
|
182
|
+
agentName: result.agentName,
|
|
183
|
+
source: result.source,
|
|
184
|
+
duration: result.duration,
|
|
185
|
+
cost: result.cost,
|
|
186
|
+
usage: result.usage
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
fs.appendFileSync(logPath, entry + '\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get orchestrator stats
|
|
194
|
+
*/
|
|
195
|
+
getStats() {
|
|
196
|
+
return {
|
|
197
|
+
...this.stats,
|
|
198
|
+
averageCost: this.stats.tasksCompleted > 0
|
|
199
|
+
? this.stats.totalCost / this.stats.tasksCompleted
|
|
200
|
+
: 0
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Test cloud connectivity
|
|
206
|
+
*/
|
|
207
|
+
async testConnection() {
|
|
208
|
+
try {
|
|
209
|
+
const ok = await this.client.ping();
|
|
210
|
+
if (ok) {
|
|
211
|
+
console.log('ā
Connected to Vutler Cloud');
|
|
212
|
+
return true;
|
|
213
|
+
} else {
|
|
214
|
+
console.log('ā Vutler Cloud unreachable');
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.log(`ā Connection failed: ${error.message}`);
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* List available cloud agents
|
|
225
|
+
*/
|
|
226
|
+
async listCloudAgents() {
|
|
227
|
+
try {
|
|
228
|
+
return await this.client.listAgents();
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error(`Failed to list cloud agents: ${error.message}`);
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = NexusCloudOrchestrator;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vutler Cloud Client
|
|
3
|
+
*
|
|
4
|
+
* Executes tasks via Vutler cloud agents instead of local processes
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class VutlerClient {
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
this.baseUrl = config.baseUrl || process.env.VUTLER_API_URL || 'https://app.vutler.ai';
|
|
10
|
+
this.apiKey = config.apiKey || process.env.VUTLER_API_KEY;
|
|
11
|
+
|
|
12
|
+
if (!this.apiKey) {
|
|
13
|
+
throw new Error('VUTLER_API_KEY required for cloud agent execution');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Execute a task with a cloud agent
|
|
19
|
+
*/
|
|
20
|
+
async executeTask(agentId, task, options = {}) {
|
|
21
|
+
const startTime = Date.now();
|
|
22
|
+
|
|
23
|
+
console.log(`āļø Executing task via cloud agent: ${agentId}`);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(`${this.baseUrl}/api/v1/agents/${agentId}/execute`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
31
|
+
'X-User-Id': options.userId || 'default'
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
task,
|
|
35
|
+
context: options.context || {},
|
|
36
|
+
timeout: options.timeout || 300000,
|
|
37
|
+
streaming: false
|
|
38
|
+
})
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
const error = await response.text();
|
|
43
|
+
throw new Error(`Cloud agent error (${response.status}): ${error}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const result = await response.json();
|
|
47
|
+
const duration = Date.now() - startTime;
|
|
48
|
+
|
|
49
|
+
const usage = result.usage || { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
50
|
+
if (!usage.totalTokens) {
|
|
51
|
+
usage.totalTokens = usage.inputTokens + usage.outputTokens;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
agentId,
|
|
56
|
+
agentName: result.agent?.name || agentId,
|
|
57
|
+
result: result.output || result.result,
|
|
58
|
+
duration,
|
|
59
|
+
cost: result.cost || 0,
|
|
60
|
+
usage,
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
62
|
+
source: 'cloud'
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(`ā Cloud execution failed: ${error.message}`);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* List available cloud agents
|
|
73
|
+
*/
|
|
74
|
+
async listAgents() {
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(`${this.baseUrl}/api/v1/agents`, {
|
|
77
|
+
headers: {
|
|
78
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new Error(`Failed to list agents: ${response.status}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const data = await response.json();
|
|
87
|
+
return data.agents || [];
|
|
88
|
+
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error(`Failed to list cloud agents: ${error.message}`);
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get agent details
|
|
97
|
+
*/
|
|
98
|
+
async getAgent(agentId) {
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(`${this.baseUrl}/api/v1/agents/${agentId}`, {
|
|
101
|
+
headers: {
|
|
102
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const data = await response.json();
|
|
111
|
+
return data.agent;
|
|
112
|
+
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error(`Failed to get agent ${agentId}: ${error.message}`);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Health check
|
|
121
|
+
*/
|
|
122
|
+
async ping() {
|
|
123
|
+
try {
|
|
124
|
+
const response = await fetch(`${this.baseUrl}/api/v1/health`, {
|
|
125
|
+
headers: {
|
|
126
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return response.ok;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = VutlerClient;
|
package/lib/web-server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @vutler/nexus ā Local Web Server
|
|
3
3
|
* Express server providing web interface for chat and configuration
|
|
4
|
+
* Supports both local (Claude CLI) and cloud (Vutler API) modes
|
|
4
5
|
*/
|
|
5
6
|
const express = require('express');
|
|
6
7
|
const path = require('path');
|
|
@@ -8,16 +9,28 @@ const { EventEmitter } = require('events');
|
|
|
8
9
|
|
|
9
10
|
const multer = require('multer');
|
|
10
11
|
const fs = require('fs');
|
|
12
|
+
const NexusCloudOrchestrator = require('./orchestrator-cloud');
|
|
11
13
|
|
|
12
14
|
class WebServer extends EventEmitter {
|
|
13
15
|
constructor(config, agentRuntime) {
|
|
14
16
|
super();
|
|
15
17
|
this.config = config;
|
|
16
18
|
this.agentRuntime = agentRuntime;
|
|
19
|
+
this.cloudOrchestrator = null;
|
|
17
20
|
this.app = express();
|
|
18
21
|
this.server = null;
|
|
19
22
|
this.activeConnections = new Set();
|
|
20
23
|
|
|
24
|
+
// Initialize cloud orchestrator if in cloud mode
|
|
25
|
+
if (config.mode === 'cloud' && config.vutlerApiKey) {
|
|
26
|
+
try {
|
|
27
|
+
process.env.VUTLER_API_KEY = config.vutlerApiKey;
|
|
28
|
+
this.cloudOrchestrator = new NexusCloudOrchestrator();
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('[web] Failed to initialize cloud orchestrator:', error.message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
this.setupMiddleware();
|
|
22
35
|
this.setupRoutes();
|
|
23
36
|
}
|
|
@@ -51,6 +64,8 @@ class WebServer extends EventEmitter {
|
|
|
51
64
|
this.app.get('/api/status', (req, res) => {
|
|
52
65
|
try {
|
|
53
66
|
const runtimeStatus = this.agentRuntime ? this.agentRuntime.getStatus() : null;
|
|
67
|
+
const isCloud = this.config.mode === 'cloud' && this.cloudOrchestrator;
|
|
68
|
+
|
|
54
69
|
res.json({
|
|
55
70
|
server: 'running',
|
|
56
71
|
agent: runtimeStatus,
|
|
@@ -58,11 +73,14 @@ class WebServer extends EventEmitter {
|
|
|
58
73
|
workspace: this.config.workspace,
|
|
59
74
|
webPort: this.config.webPort,
|
|
60
75
|
agentName: this.config.agent?.name,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
mode: this.config.mode || 'local',
|
|
77
|
+
provider: isCloud ? 'cloud' : this.config.llm?.provider,
|
|
78
|
+
model: isCloud ? 'Vutler Cloud' : this.config.llm?.model,
|
|
79
|
+
hasApiKey: !!(this.config.llm?.apiKey || this.config.vutlerApiKey),
|
|
80
|
+
vutlerUrl: isCloud ? this.config.vutlerUrl : undefined
|
|
64
81
|
},
|
|
65
|
-
connections: this.activeConnections.size
|
|
82
|
+
connections: this.activeConnections.size,
|
|
83
|
+
cloudReady: isCloud
|
|
66
84
|
});
|
|
67
85
|
} catch (error) {
|
|
68
86
|
res.status(500).json({ error: error.message });
|
|
@@ -125,7 +143,7 @@ class WebServer extends EventEmitter {
|
|
|
125
143
|
}
|
|
126
144
|
});
|
|
127
145
|
|
|
128
|
-
// Chat endpoint
|
|
146
|
+
// Chat endpoint - supports both local and cloud modes
|
|
129
147
|
this.app.post('/api/chat', async (req, res) => {
|
|
130
148
|
try {
|
|
131
149
|
const { message, stream = true, attachments = [] } = req.body;
|
|
@@ -134,55 +152,109 @@ class WebServer extends EventEmitter {
|
|
|
134
152
|
return res.status(400).json({ error: 'Message is required' });
|
|
135
153
|
}
|
|
136
154
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (!stream) {
|
|
142
|
-
// Non-streaming response
|
|
143
|
-
const response = await this.agentRuntime.processMessage(message, { attachments });
|
|
144
|
-
res.json(response);
|
|
145
|
-
} else {
|
|
146
|
-
// Streaming response using Server-Sent Events
|
|
147
|
-
res.writeHead(200, {
|
|
148
|
-
'Content-Type': 'text/event-stream',
|
|
149
|
-
'Cache-Control': 'no-cache',
|
|
150
|
-
'Connection': 'keep-alive',
|
|
151
|
-
'Access-Control-Allow-Origin': '*',
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// Track connection
|
|
155
|
-
this.activeConnections.add(res);
|
|
156
|
-
|
|
157
|
-
// Handle client disconnect
|
|
158
|
-
res.on('close', () => {
|
|
159
|
-
this.activeConnections.delete(res);
|
|
160
|
-
});
|
|
161
|
-
|
|
155
|
+
// Route based on mode
|
|
156
|
+
if (this.config.mode === 'cloud' && this.cloudOrchestrator) {
|
|
157
|
+
// Cloud mode - route to Vutler cloud agents
|
|
162
158
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
159
|
+
console.log('[web] āļø Cloud mode - executing via Vutler');
|
|
160
|
+
const result = await this.cloudOrchestrator.executeTask(message, {
|
|
161
|
+
userId: 'web-ui',
|
|
162
|
+
stream: false
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (!stream) {
|
|
166
|
+
res.json({
|
|
167
|
+
type: 'text',
|
|
168
|
+
content: result.result || result.output || 'No response',
|
|
169
|
+
source: 'cloud',
|
|
170
|
+
duration: result.duration,
|
|
171
|
+
agent: result.agentName
|
|
172
|
+
});
|
|
173
|
+
} else {
|
|
174
|
+
// Streaming cloud response
|
|
175
|
+
res.writeHead(200, {
|
|
176
|
+
'Content-Type': 'text/event-stream',
|
|
177
|
+
'Cache-Control': 'no-cache',
|
|
178
|
+
'Connection': 'keep-alive',
|
|
179
|
+
'Access-Control-Allow-Origin': '*',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
this.activeConnections.add(res);
|
|
183
|
+
res.on('close', () => {
|
|
184
|
+
this.activeConnections.delete(res);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Send result chunks
|
|
188
|
+
const chunks = (result.result || '').split(' ');
|
|
189
|
+
for (const chunk of chunks) {
|
|
167
190
|
if (!res.destroyed) {
|
|
168
|
-
res.write(`data: ${JSON.stringify(chunk)}\\n\\n`);
|
|
191
|
+
res.write(`data: ${JSON.stringify({ type: 'text', content: chunk + ' ' })}\\n\\n`);
|
|
169
192
|
}
|
|
170
193
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
194
|
+
|
|
195
|
+
if (!res.destroyed) {
|
|
196
|
+
res.write(`data: ${JSON.stringify({ type: 'complete', agent: result.agentName, duration: result.duration })}\\n\\n`);
|
|
197
|
+
res.end();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.activeConnections.delete(res);
|
|
177
201
|
}
|
|
178
202
|
} catch (error) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
res.
|
|
203
|
+
console.error('[web] Cloud execution error:', error);
|
|
204
|
+
if (!res.headersSent) {
|
|
205
|
+
res.status(500).json({ error: error.message });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else if (this.agentRuntime) {
|
|
209
|
+
// Local mode - use agent runtime
|
|
210
|
+
if (!stream) {
|
|
211
|
+
// Non-streaming response
|
|
212
|
+
const response = await this.agentRuntime.processMessage(message, { attachments });
|
|
213
|
+
res.json(response);
|
|
214
|
+
} else {
|
|
215
|
+
// Streaming response using Server-Sent Events
|
|
216
|
+
res.writeHead(200, {
|
|
217
|
+
'Content-Type': 'text/event-stream',
|
|
218
|
+
'Cache-Control': 'no-cache',
|
|
219
|
+
'Connection': 'keep-alive',
|
|
220
|
+
'Access-Control-Allow-Origin': '*',
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Track connection
|
|
224
|
+
this.activeConnections.add(res);
|
|
225
|
+
|
|
226
|
+
// Handle client disconnect
|
|
227
|
+
res.on('close', () => {
|
|
228
|
+
this.activeConnections.delete(res);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
await this.agentRuntime.processMessage(message, {
|
|
233
|
+
stream: true,
|
|
234
|
+
attachments,
|
|
235
|
+
onChunk: (chunk) => {
|
|
236
|
+
if (!res.destroyed) {
|
|
237
|
+
res.write(`data: ${JSON.stringify(chunk)}\\n\\n`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Send completion marker
|
|
243
|
+
if (!res.destroyed) {
|
|
244
|
+
res.write('data: {"type":"complete"}\\n\\n');
|
|
245
|
+
res.end();
|
|
246
|
+
}
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (!res.destroyed) {
|
|
249
|
+
res.write(`data: ${JSON.stringify({ type: 'error', error: error.message })}\\n\\n`);
|
|
250
|
+
res.end();
|
|
251
|
+
}
|
|
182
252
|
}
|
|
253
|
+
|
|
254
|
+
this.activeConnections.delete(res);
|
|
183
255
|
}
|
|
184
|
-
|
|
185
|
-
|
|
256
|
+
} else {
|
|
257
|
+
return res.status(500).json({ error: 'No execution runtime available' });
|
|
186
258
|
}
|
|
187
259
|
|
|
188
260
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vutler-nexus",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Vutler Nexus
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Vutler Nexus ā Complete agent runtime with Local (Claude CLI) or Cloud (Vutler API) support",
|
|
5
5
|
"main": "lib/tunnel.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"vutler-nexus": "./bin/cli.js"
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"multi-provider",
|
|
34
34
|
"claude",
|
|
35
35
|
"openai",
|
|
36
|
-
"
|
|
36
|
+
"cloud",
|
|
37
|
+
"vutler-api"
|
|
37
38
|
],
|
|
38
39
|
"license": "UNLICENSED",
|
|
39
40
|
"files": [
|
|
@@ -44,4 +45,4 @@
|
|
|
44
45
|
"engines": {
|
|
45
46
|
"node": ">=16.0.0"
|
|
46
47
|
}
|
|
47
|
-
}
|
|
48
|
+
}
|