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 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
- // LLM Provider Selection
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(` LLM: ${chalk.cyan(providerData.name)}`);
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
- log(`🧠 Initializing Agent Runtime (${cfg.llm?.provider || 'unknown'})...`);
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
- const providerName = providerInfo[cfg.llm?.provider]?.name || cfg.llm?.provider || 'Unknown';
496
- console.log(chalk.blue(` LLM: ${providerName}`));
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 (provider === 'claude-code') {
570
- const claudeStatus = await checkClaudeCLI();
571
- if (claudeStatus.available) {
572
- console.log(` Claude CLI: āœ… ${claudeStatus.version}`);
573
- } else {
574
- console.log(` Claude CLI: āŒ not found (install with: pip install claude-cli)`);
575
- }
576
- console.log(` Cost: $0 (uses Max subscription)`);
577
- } else if (provider === 'ollama') {
578
- const ollamaStatus = await checkOllama();
579
- if (ollamaStatus.available) {
580
- console.log(` Ollama: āœ… running (${ollamaStatus.models.length} models)`);
581
- } else {
582
- console.log(` Ollama: āŒ not running (start with: ollama serve)`);
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
- console.log(` Cost: $0 (local models)`);
585
- } else if (providerInfo[provider]?.requiresApiKey) {
586
- console.log(` API Key: ${cfg.llm?.apiKey ? cfg.llm.apiKey.slice(0, 8) + '...' : 'not set'}`);
587
- console.log(` Cost: per token usage`);
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
- provider: this.config.llm?.provider,
62
- model: this.config.llm?.model,
63
- hasApiKey: !!(this.config.llm?.apiKey)
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
- if (!this.agentRuntime) {
138
- return res.status(500).json({ error: 'Agent runtime not initialized' });
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
- await this.agentRuntime.processMessage(message, {
164
- stream: true,
165
- attachments,
166
- onChunk: (chunk) => {
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
- // Send completion marker
174
- if (!res.destroyed) {
175
- res.write('data: {"type":"complete"}\\n\\n');
176
- res.end();
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
- if (!res.destroyed) {
180
- res.write(`data: ${JSON.stringify({ type: 'error', error: error.message })}\\n\\n`);
181
- res.end();
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
- this.activeConnections.delete(res);
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": "1.0.0",
4
- "description": "Vutler Nexus \u2014 Complete local agent runtime with multi-provider LLM support",
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
- "ollama"
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
+ }