samarthya-bot 1.1.4 → 2.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.
@@ -6,422 +6,815 @@ const readline = require('readline');
6
6
 
7
7
  const args = process.argv.slice(2);
8
8
  const command = args[0];
9
-
10
9
  const backendDir = path.join(__dirname, '..');
11
10
 
12
- // Helper to check if server is already running on port 5000
11
+ // ╔══════════════════════════════════════════════════════════════╗
12
+ // ║ 🎨 TERMINAL COLORS ║
13
+ // ╚══════════════════════════════════════════════════════════════╝
14
+ const c = {
15
+ reset: '\x1b[0m',
16
+ bold: '\x1b[1m',
17
+ dim: '\x1b[2m',
18
+ italic: '\x1b[3m',
19
+ under: '\x1b[4m',
20
+ // Indian Flag Colors
21
+ saffron: '\x1b[38;2;255;153;51m',
22
+ white: '\x1b[38;2;255;255;255m',
23
+ green: '\x1b[38;2;19;136;8m',
24
+ navy: '\x1b[38;2;0;0;128m',
25
+ // UI Colors
26
+ red: '\x1b[38;2;239;68;68m',
27
+ cyan: '\x1b[38;2;34;211;238m',
28
+ yellow: '\x1b[38;2;250;204;21m',
29
+ purple: '\x1b[38;2;168;85;247m',
30
+ pink: '\x1b[38;2;244;114;182m',
31
+ lime: '\x1b[38;2;74;222;128m',
32
+ gray: '\x1b[38;2;120;120;120m',
33
+ dimWhite: '\x1b[38;2;180;180,180m',
34
+ // Backgrounds
35
+ bgSaffron: '\x1b[48;2;255;153;51m',
36
+ bgGreen: '\x1b[48;2;19;136;8m',
37
+ bgNavy: '\x1b[48;2;0;0;128m',
38
+ bgDark: '\x1b[48;2;15;15;25m',
39
+ };
40
+
41
+ // ╔══════════════════════════════════════════════════════════════╗
42
+ // ║ 🏳️ ASCII ART BANNER ║
43
+ // ╚══════════════════════════════════════════════════════════════╝
44
+ const BANNER = `
45
+ ${c.saffron}${c.bold} ███████╗ █████╗ ███╗ ███╗ █████╗ ██████╗ ████████╗██╗ ██╗██╗ ██╗ █████╗
46
+ ██╔════╝██╔══██╗████╗ ████║██╔══██╗██╔══██╗╚══██╔══╝██║ ██║╚██╗ ██╔╝██╔══██╗
47
+ ███████╗███████║██╔████╔██║███████║██████╔╝ ██║ ███████║ ╚████╔╝ ███████║
48
+ ${c.white} ╚════██║██╔══██║██║╚██╔╝██║██╔══██║██╔══██╗ ██║ ██╔══██║ ╚██╔╝ ██╔══██║
49
+ ███████║██║ ██║██║ ╚═╝ ██║██║ ██║██║ ██║ ██║ ██║ ██║ ██║ ██║ ██║
50
+ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
51
+ ${c.green} ██████╗ ██████╗ ████████╗
52
+ ██╔══██╗██╔═══██╗╚══██╔══╝
53
+ ██████╔╝██║ ██║ ██║
54
+ ${c.green} ██╔══██╗██║ ██║ ██║
55
+ ██████╔╝╚██████╔╝ ██║
56
+ ╚═════╝ ╚═════╝ ╚═╝ ${c.reset}
57
+
58
+ ${c.gray} ─────────────────────────────────────────────────────────────────────────────${c.reset}
59
+ ${c.dim} 🇮🇳 Privacy-First Local Agentic OS • Made in India 🇮🇳${c.reset}
60
+ ${c.gray} ─────────────────────────────────────────────────────────────────────────────${c.reset}
61
+ `;
62
+
63
+ const MINI_BANNER = `
64
+ ${c.saffron}${c.bold} ╔═══╗╔═══╗╔╗╔╗╔═══╗╔═══╗╔════╗╔╗ ╔╗╔╗ ╔╗╔═══╗ ╔══╗╔═══╗╔════╗${c.reset}
65
+ ${c.saffron} ║╔═╗║║╔═╗║║║║║║╔═╗║║╔═╗║║╔╗╔╗║║║ ║║╚╝ ╚╝║╔═╗║ ║╔╗║║╔═╗║║╔╗╔╗║${c.reset}
66
+ ${c.white}${c.bold} ║╚══╗║║ ║║║╚╝║║║ ║║║╚═╝║╚╝║║╚╝║╚═╝║ ║║ ║║ ║╚╝╚╗║║ ║║╚╝║║╚╝${c.reset}
67
+ ${c.white} ╚══╗║║╚═╝║║╔╗║║╚═╝║║╔╗╔╝ ║║ ║╔═╗║ ║╚═╝║ ║╔═╗║║║ ║║ ║║ ${c.reset}
68
+ ${c.green}${c.bold} ║╚═╝║║╔═╗║║║║║║╔═╗║║║║║ ║║ ║║ ║║ ║╔═╗║ ║╚═╝║║╚═╝║ ║║ ${c.reset}
69
+ ${c.green} ╚═══╝╚╝ ╚╝╚╝╚╝╚╝ ╚╝╚╝╚╝ ╚╝ ╚╝ ╚╝ ╚╝ ╚╝ ╚═══╝╚═══╝ ╚╝ ${c.reset}
70
+ `;
71
+
72
+ // ╔══════════════════════════════════════════════════════════════╗
73
+ // ║ 🛠 HELPER UTILITIES ║
74
+ // ╚══════════════════════════════════════════════════════════════╝
75
+
76
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
77
+
78
+ // Animated spinner
79
+ class Spinner {
80
+ constructor(text) {
81
+ this.text = text;
82
+ this.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
83
+ this.i = 0;
84
+ this.timer = null;
85
+ }
86
+ start() {
87
+ this.timer = setInterval(() => {
88
+ process.stdout.write(`\r${c.cyan}${this.frames[this.i++ % this.frames.length]}${c.reset} ${this.text}`);
89
+ }, 80);
90
+ }
91
+ stop(successText) {
92
+ clearInterval(this.timer);
93
+ process.stdout.write(`\r${c.lime}✔${c.reset} ${successText || this.text}\n`);
94
+ }
95
+ fail(failText) {
96
+ clearInterval(this.timer);
97
+ process.stdout.write(`\r${c.red}✖${c.reset} ${failText || this.text}\n`);
98
+ }
99
+ }
100
+
101
+ // Progress bar
102
+ const progressBar = (current, total, width = 30) => {
103
+ const filled = Math.round((current / total) * width);
104
+ const empty = width - filled;
105
+ const bar = `${c.saffron}${'█'.repeat(filled)}${c.gray}${'░'.repeat(empty)}${c.reset}`;
106
+ return `${bar} ${c.dim}${current}/${total}${c.reset}`;
107
+ };
108
+
109
+ // Step header
110
+ const stepHeader = (num, total, text) => {
111
+ console.log(`\n${c.saffron}${c.bold} [${num}/${total}]${c.reset} ${c.white}${c.bold}${text}${c.reset}`);
112
+ console.log(`${c.gray} ${'─'.repeat(60)}${c.reset}`);
113
+ };
114
+
115
+ // Labeled print
116
+ const info = (text) => console.log(` ${c.cyan}ℹ${c.reset} ${text}`);
117
+ const success = (text) => console.log(` ${c.lime}✔${c.reset} ${text}`);
118
+ const warn = (text) => console.log(` ${c.yellow}⚠${c.reset} ${text}`);
119
+ const error = (text) => console.log(` ${c.red}✖${c.reset} ${text}`);
120
+
121
+ // Create readline interface
122
+ const createRL = () => readline.createInterface({ input: process.stdin, output: process.stdout });
123
+ const ask = (rl, query) => new Promise(resolve => rl.question(query, resolve));
124
+
125
+ // Check if server is running
13
126
  const isServerRunning = () => {
14
127
  try {
15
128
  if (process.platform === 'win32') {
16
129
  const output = execSync('netstat -ano | findstr :5000', { encoding: 'utf-8' });
17
130
  return output.includes('LISTENING');
18
131
  } else {
19
- // macOS and Linux
20
- // Using lsof as it's more standard across unix than fuser
21
132
  execSync('lsof -i:5000 -t 2>/dev/null');
22
133
  return true;
23
134
  }
24
- } catch {
25
- return false;
135
+ } catch { return false; }
136
+ };
137
+
138
+ // Read existing .env
139
+ const readEnv = () => {
140
+ const envPath = path.join(backendDir, '.env');
141
+ const envVars = {};
142
+ if (fs.existsSync(envPath)) {
143
+ fs.readFileSync(envPath, 'utf8').split('\n').forEach(line => {
144
+ const eqIndex = line.indexOf('=');
145
+ if (eqIndex > 0) {
146
+ const k = line.substring(0, eqIndex).trim();
147
+ const v = line.substring(eqIndex + 1).trim();
148
+ if (k) envVars[k] = v;
149
+ }
150
+ });
26
151
  }
27
- }
28
-
152
+ return envVars;
153
+ };
154
+
155
+ // Write .env
156
+ const writeEnv = (envVars) => {
157
+ const envPath = path.join(backendDir, '.env');
158
+ const content = Object.keys(envVars).map(k => `${k}=${envVars[k]}`).join('\n');
159
+ fs.writeFileSync(envPath, content);
160
+ };
161
+
162
+ // Mask API key for display
163
+ const maskKey = (key) => {
164
+ if (!key || key.length < 8) return '****';
165
+ return key.substring(0, 4) + '•'.repeat(Math.min(key.length - 8, 20)) + key.substring(key.length - 4);
166
+ };
167
+
168
+ // ╔══════════════════════════════════════════════════════════════╗
169
+ // ║ PROVIDER & MODEL DEFINITIONS ║
170
+ // ╚══════════════════════════════════════════════════════════════╝
171
+
172
+ const PROVIDERS = [
173
+ { id: 'gemini', name: 'Google Gemini', tag: 'Free Tier', color: c.cyan, envKey: 'GEMINI_API_KEY' },
174
+ { id: 'groq', name: 'Groq', tag: 'Fastest', color: c.yellow, envKey: 'GROQ_API_KEY' },
175
+ { id: 'anthropic', name: 'Anthropic Claude', tag: 'Smartest', color: c.purple, envKey: 'ANTHROPIC_API_KEY' },
176
+ { id: 'openai', name: 'OpenAI', tag: 'GPT-5', color: c.lime, envKey: 'OPENAI_API_KEY' },
177
+ { id: 'deepseek', name: 'DeepSeek', tag: 'Budget', color: c.cyan, envKey: 'DEEPSEEK_API_KEY' },
178
+ { id: 'qwen', name: 'Qwen', tag: 'Alibaba', color: c.pink, envKey: 'QWEN_API_KEY' },
179
+ { id: 'openrouter', name: 'OpenRouter', tag: '100+ Models', color: c.saffron, envKey: 'OPENROUTER_API_KEY' },
180
+ { id: 'ollama', name: 'Ollama', tag: 'Offline', color: c.green, envKey: null },
181
+ { id: 'mistral', name: 'Mistral AI', tag: 'EU Privacy', color: c.white, envKey: 'MISTRAL_API_KEY' },
182
+ ];
183
+
184
+ const MODELS = {
185
+ gemini: [
186
+ { id: 'gemini-2.5-flash', desc: 'Fastest reasoning, great pricing' },
187
+ { id: 'gemini-2.5-pro', desc: 'Advanced multi-step reasoning' },
188
+ { id: 'gemini-2.5-flash-lite', desc: 'Budget friendly' },
189
+ { id: 'gemini-2.0-flash', desc: 'Older 1M context' },
190
+ ],
191
+ groq: [
192
+ { id: 'llama-3.3-70b-versatile', desc: 'Best overall Llama' },
193
+ { id: 'llama-3.1-8b-instant', desc: 'Extreme fast' },
194
+ { id: 'qwen/qwen3-32b', desc: 'Powerful 32b reasoning' },
195
+ ],
196
+ anthropic: [
197
+ { id: 'claude-3-5-sonnet-latest', desc: 'Best balance' },
198
+ { id: 'claude-3-opus-latest', desc: 'Most capable' },
199
+ ],
200
+ openai: [
201
+ { id: 'gpt-5.2', desc: 'Best for coding & agentic' },
202
+ { id: 'gpt-5-mini', desc: 'Faster, cost-efficient' },
203
+ { id: 'gpt-4o', desc: 'Fast, intelligent' },
204
+ { id: 'o3-mini', desc: 'Small reasoning' },
205
+ ],
206
+ deepseek: [
207
+ { id: 'deepseek-chat', desc: 'General purpose chat' },
208
+ { id: 'deepseek-coder', desc: 'Code specialized' },
209
+ ],
210
+ qwen: [
211
+ { id: 'qwen-max', desc: 'Most capable Qwen' },
212
+ { id: 'qwen-turbo', desc: 'Fast and efficient' },
213
+ ],
214
+ openrouter: [
215
+ { id: 'auto', desc: 'Auto-select best model' },
216
+ { id: 'anthropic/claude-3.5-sonnet', desc: 'Claude via OpenRouter' },
217
+ { id: 'google/gemini-2.5-flash', desc: 'Gemini via OpenRouter' },
218
+ ],
219
+ ollama: [
220
+ { id: 'dolphin3:8b-llama3.1-q4_K_M', desc: 'Default local' },
221
+ { id: 'llama3:8b', desc: 'Meta Llama 3' },
222
+ { id: 'mistral', desc: 'Mistral local' },
223
+ ],
224
+ mistral: [
225
+ { id: 'mistral-large-3', desc: 'General-purpose multimodal' },
226
+ { id: 'ministral-3-8b', desc: 'Powerful & efficient' },
227
+ { id: 'devstral-2', desc: 'Best for code & agents' },
228
+ ],
229
+ };
230
+
231
+ // ╔══════════════════════════════════════════════════════════════╗
232
+ // ║ 🚀 CLI COMMANDS ║
233
+ // ╚══════════════════════════════════════════════════════════════╝
234
+
235
+ // ─── Show help ───
29
236
  if (!command) {
30
- console.log(`
31
- 🤖 SamarthyaBot Local AI Agent
32
- Usage:
33
- samarthya onboard - Setup your local environment
34
- samarthya model - Change your active AI provider/model
35
- samarthya gateway - Start the local server
36
- samarthya status - Check if the agent is running
37
- samarthya tunnel - Expose to internet & setup webhooks
38
- samarthya stop - Stop the running gateway
39
- samarthya restart - Restart the gateway
40
- `);
237
+ console.log(BANNER);
238
+ console.log(`${c.bold} COMMANDS:${c.reset}`);
239
+ console.log(` ${c.saffron}samarthya onboard${c.reset} ${c.dim}Setup + Start everything (single command!)${c.reset}`);
240
+ console.log(` ${c.saffron}samarthya gateway${c.reset} ${c.dim}Start the local server${c.reset}`);
241
+ console.log(` ${c.saffron}samarthya model${c.reset} ${c.dim}Change AI provider/model${c.reset}`);
242
+ console.log(` ${c.saffron}samarthya telegram${c.reset} ${c.dim}Configure Telegram bot${c.reset}`);
243
+ console.log(` ${c.saffron}samarthya discord${c.reset} ${c.dim}Configure Discord bot${c.reset}`);
244
+ console.log(` ${c.saffron}samarthya config${c.reset} ${c.dim}View current configuration${c.reset}`);
245
+ console.log(` ${c.saffron}samarthya tunnel${c.reset} ${c.dim}Expose to internet & setup webhooks${c.reset}`);
246
+ console.log(` ${c.saffron}samarthya status${c.reset} ${c.dim}Check if agent is running${c.reset}`);
247
+ console.log(` ${c.saffron}samarthya stop${c.reset} ${c.dim}Stop the gateway${c.reset}`);
248
+ console.log(` ${c.saffron}samarthya restart${c.reset} ${c.dim}Restart the gateway${c.reset}\n`);
41
249
  process.exit(0);
42
250
  }
43
251
 
44
252
  switch (command) {
45
- case 'onboard':
46
- console.log('🚀 Running SamarthyaBot Setup Wizard...');
47
253
 
48
- const rl = readline.createInterface({
49
- input: process.stdin,
50
- output: process.stdout
51
- });
254
+ // ╔══════════════════════════════════════════════════════════════╗
255
+ // ║ 📦 ONBOARD WIZARD ║
256
+ // ╚══════════════════════════════════════════════════════════════╝
257
+ case 'onboard': {
258
+ console.log(BANNER);
259
+ console.log(` ${c.saffron}${c.bold}🚀 Setup Wizard${c.reset}${c.dim} — Let's get your AI agent ready!\n${c.reset}`);
52
260
 
53
- const question = (query) => new Promise(resolve => rl.question(query, resolve));
261
+ const rl = createRL();
262
+ const q = (query) => ask(rl, ` ${c.white}${query}${c.reset}`);
54
263
 
55
264
  (async () => {
56
- console.log("\n🌐 Select your primary AI Provider:");
57
- console.log(" 1) Google Gemini (Default)");
58
- console.log(" 2) Anthropic Claude");
59
- console.log(" 3) Groq (Fastest)");
60
- console.log(" 4) OpenAI");
61
- console.log(" 5) Local Ollama (Offline)");
62
- console.log(" 6) Mistral AI\n");
63
-
64
- let providerRaw = await question("Enter choice (1-6, default 1): ");
65
- let activeProvider = 'gemini';
66
- let useOllama = 'false';
67
-
68
- switch (providerRaw.trim()) {
69
- case '2': activeProvider = 'anthropic'; break;
70
- case '3': activeProvider = 'groq'; break;
71
- case '4': activeProvider = 'openai'; break;
72
- case '5':
73
- activeProvider = 'ollama';
74
- useOllama = 'true';
75
- break;
76
- case '6': activeProvider = 'mistral'; break;
77
- case '1':
78
- default:
79
- activeProvider = 'gemini';
80
- break;
265
+ const TOTAL_STEPS = 5;
266
+
267
+ // ── Step 1: Select AI Provider ──
268
+ stepHeader(1, TOTAL_STEPS, 'Select AI Provider');
269
+ console.log();
270
+ PROVIDERS.forEach((p, i) => {
271
+ const num = `${i + 1}`.padStart(2);
272
+ console.log(` ${c.dim}${num})${c.reset} ${p.color}${c.bold}${p.name}${c.reset} ${c.gray}(${p.tag})${c.reset}`);
273
+ });
274
+ console.log();
275
+
276
+ let providerRaw = await q(` Enter choice [${c.saffron}1-9${c.reset}, default ${c.saffron}1${c.reset}]: `);
277
+ const provIdx = parseInt(providerRaw.trim()) - 1;
278
+ const provider = PROVIDERS[provIdx >= 0 && provIdx < PROVIDERS.length ? provIdx : 0];
279
+ const activeProvider = provider.id;
280
+ const useOllama = activeProvider === 'ollama' ? 'true' : 'false';
281
+
282
+ console.log(`\n ${c.lime}✔${c.reset} Selected: ${provider.color}${c.bold}${provider.name}${c.reset}\n`);
283
+
284
+ // ── Step 2: API Key for selected provider ──
285
+ stepHeader(2, TOTAL_STEPS, 'API Key Configuration');
286
+ let apiKey = '';
287
+ if (provider.envKey) {
288
+ apiKey = await q(` 🔑 Enter ${provider.name} API Key: `);
289
+ if (apiKey.trim()) {
290
+ success(`Key saved: ${c.dim}${maskKey(apiKey.trim())}${c.reset}`);
291
+ } else {
292
+ info(`Skipped — will use existing key if available`);
293
+ }
294
+ } else {
295
+ info(`${provider.name} runs locally — no API key needed`);
296
+ }
297
+ console.log();
298
+
299
+ // ── Step 3: Telegram configuration ──
300
+ stepHeader(3, TOTAL_STEPS, 'Channel Integrations');
301
+ let telegramToken = '';
302
+ let discordToken = '';
303
+
304
+ const enableTg = await q(` 📱 Enable Telegram Bot? (${c.lime}y${c.reset}/${c.red}n${c.reset}): `);
305
+ if (enableTg.trim().toLowerCase() === 'y' || enableTg.trim().toLowerCase() === 'yes') {
306
+ telegramToken = await q(` 🤖 Enter Telegram Bot Token: `);
307
+ if (telegramToken.trim()) {
308
+ success(`Telegram configured: ${c.dim}${maskKey(telegramToken.trim())}${c.reset}`);
309
+ }
310
+ } else {
311
+ info(`Telegram skipped`);
312
+ }
313
+ console.log();
314
+
315
+ const enableDc = await q(` 🟣 Enable Discord Bot? (${c.lime}y${c.reset}/${c.red}n${c.reset}): `);
316
+ if (enableDc.trim().toLowerCase() === 'y' || enableDc.trim().toLowerCase() === 'yes') {
317
+ discordToken = await q(` 🟣 Enter Discord Bot Token: `);
318
+ if (discordToken.trim()) {
319
+ success(`Discord configured: ${c.dim}${maskKey(discordToken.trim())}${c.reset}`);
320
+ }
321
+ } else {
322
+ info(`Discord skipped`);
81
323
  }
324
+ console.log();
82
325
 
83
- console.log(`\n👉 Selected Provider: ${activeProvider.toUpperCase()}\n`);
326
+ // ── Step 4: Save configuration ──
327
+ stepHeader(4, TOTAL_STEPS, 'Saving Configuration');
84
328
 
85
- const geminiKey = await question('🔑 Enter Google Gemini API Key (or press Enter to skip if already set): ');
86
- const anthropicKey = await question('🔑 Enter Anthropic (Claude) API Key (or press Enter to skip): ');
87
- const groqKey = await question('🔑 Enter Groq API Key (or press Enter to skip): ');
88
- const openAiKey = await question('🔑 Enter OpenAI API Key (or press Enter to skip): ');
89
- const telegramToken = await question('🤖 Enter Telegram Bot Token (or press Enter to skip): ');
329
+ const spin1 = new Spinner('Writing .env configuration...');
330
+ spin1.start();
331
+ await sleep(600);
90
332
 
91
333
  const envPath = path.join(backendDir, '.env');
92
- let envVars = {};
93
-
94
- // Read existing if present to keep other configs
95
- if (fs.existsSync(envPath)) {
96
- const currentEnv = fs.readFileSync(envPath, 'utf8');
97
- currentEnv.split('\n').forEach(line => {
98
- const [k, v] = line.split('=');
99
- if (k && v) envVars[k.trim()] = v.trim();
100
- });
101
- } else {
102
- envVars = {
334
+ let envVars = readEnv();
335
+
336
+ // Set defaults if fresh install
337
+ if (!envVars['PORT']) {
338
+ Object.assign(envVars, {
103
339
  PORT: '5000',
104
- MONGODB_URI: 'mongodb://localhost:27017/samarthya',
340
+ MONGO_URI: 'mongodb://localhost:27017/samarthya',
105
341
  JWT_SECRET: 'samarthya_secret_key_change_in_production',
106
342
  NODE_ENV: 'production',
107
343
  CORS_ORIGIN: 'http://localhost:5000',
108
- USE_OLLAMA: useOllama,
109
- ACTIVE_PROVIDER: activeProvider,
110
- ACTIVE_MODEL: activeProvider === 'gemini' ? 'gemini-2.5-flash' : '',
111
344
  OLLAMA_URL: 'http://localhost:11434',
112
- OLLAMA_MODEL: 'dolphin3:8b-llama3.1-q4_K_M'
113
- };
345
+ OLLAMA_MODEL: 'dolphin3:8b-llama3.1-q4_K_M',
346
+ RESTRICT_TO_WORKSPACE: 'true',
347
+ HEARTBEAT_INTERVAL: '30',
348
+ });
114
349
  }
115
350
 
116
- envVars['USE_OLLAMA'] = useOllama;
117
351
  envVars['ACTIVE_PROVIDER'] = activeProvider;
352
+ envVars['USE_OLLAMA'] = useOllama;
353
+ if (activeProvider === 'gemini' && !envVars['ACTIVE_MODEL']) envVars['ACTIVE_MODEL'] = 'gemini-2.5-flash';
118
354
 
119
- // Assign keys if provided
120
- if (geminiKey.trim()) envVars['GEMINI_API_KEY'] = geminiKey.trim();
121
- if (anthropicKey.trim()) envVars['ANTHROPIC_API_KEY'] = anthropicKey.trim();
122
- if (groqKey.trim()) envVars['GROQ_API_KEY'] = groqKey.trim();
123
- if (openAiKey.trim()) envVars['OPENAI_API_KEY'] = openAiKey.trim();
355
+ if (apiKey.trim() && provider.envKey) envVars[provider.envKey] = apiKey.trim();
124
356
  if (telegramToken.trim()) envVars['TELEGRAM_BOT_TOKEN'] = telegramToken.trim();
357
+ if (discordToken.trim()) envVars['DISCORD_BOT_TOKEN'] = discordToken.trim();
358
+
359
+ // Auto-generate encryption key
360
+ if (!envVars['MEMORY_ENCRYPTION_KEY']) {
361
+ const crypto = require('crypto');
362
+ envVars['MEMORY_ENCRYPTION_KEY'] = crypto.randomBytes(16).toString('hex');
363
+ }
125
364
 
126
365
  if (!envVars['GEMINI_API_KEY']) envVars['GEMINI_API_KEY'] = 'dummy';
127
366
 
128
- // Write back to .env
129
- const newEnvContent = Object.keys(envVars).map(k => `${k}=${envVars[k]}`).join('\n');
130
- fs.writeFileSync(envPath, newEnvContent);
131
- console.log('\n✅ Keys saved to .env file securely.');
367
+ writeEnv(envVars);
368
+ spin1.stop('Configuration saved');
132
369
 
133
- console.log('📦 Installing backend dependencies (this might take a few seconds)...');
370
+ const spin2 = new Spinner('Installing dependencies...');
371
+ spin2.start();
134
372
  try {
135
- execSync('npm install --production', { cwd: backendDir, stdio: 'ignore' });
136
- } catch (e) { /* ignore */ }
373
+ execSync('npm install --production 2>/dev/null', { cwd: backendDir, stdio: 'ignore' });
374
+ spin2.stop('Dependencies installed');
375
+ } catch {
376
+ spin2.fail('Dependencies install failed (non-critical)');
377
+ }
137
378
 
138
- console.log('\n✨ Onboarding complete! Run "samarthya gateway" to start your AI Operator.');
139
379
  rl.close();
140
- process.exit(0);
380
+
381
+ // ── Step 5: Boot Gateway ──
382
+ stepHeader(5, TOTAL_STEPS, 'Starting SamarthyaBot');
383
+
384
+ console.log();
385
+ const bootSteps = [
386
+ 'Loading AI modules',
387
+ 'Connecting database',
388
+ 'Starting security sandbox',
389
+ 'Initializing heartbeat service',
390
+ 'Registering tool packs',
391
+ 'Booting web server'
392
+ ];
393
+
394
+ for (let i = 0; i < bootSteps.length; i++) {
395
+ const s = new Spinner(bootSteps[i] + '...');
396
+ s.start();
397
+ await sleep(400 + Math.random() * 300);
398
+ s.stop(bootSteps[i]);
399
+ }
400
+
401
+ console.log(`\n${c.gray} ─────────────────────────────────────────────────────────────${c.reset}`);
402
+ console.log(` ${c.lime}${c.bold}🚀 SamarthyaBot is READY!${c.reset}`);
403
+ console.log(` ${c.dim}Dashboard: ${c.cyan}${c.under}http://localhost:5000${c.reset}`);
404
+ console.log(` ${c.dim}Provider: ${provider.color}${provider.name}${c.reset}`);
405
+ if (telegramToken.trim()) console.log(` ${c.dim}Telegram: ${c.lime}Connected${c.reset}`);
406
+ if (discordToken.trim()) console.log(` ${c.dim}Discord: ${c.purple}Connected${c.reset}`);
407
+ console.log(`${c.gray} ─────────────────────────────────────────────────────────────${c.reset}\n`);
408
+
409
+ // Load .env and spawn the server
410
+ try { require('dotenv').config({ path: envPath }); } catch (e) { }
411
+
412
+ const gatewayChild = spawn('node', ['server.js'], {
413
+ cwd: backendDir,
414
+ stdio: 'inherit'
415
+ });
416
+
417
+ gatewayChild.on('close', (code) => {
418
+ console.log(`\n${c.red}Gateway exited with code ${code}${c.reset}`);
419
+ });
420
+
421
+ // Auto-start tunnel if Telegram configured
422
+ setTimeout(() => {
423
+ if (envVars['TELEGRAM_BOT_TOKEN'] && envVars['TELEGRAM_BOT_TOKEN'] !== 'dummy') {
424
+ info('Auto-starting tunnel for Telegram webhook...');
425
+ const isWin = process.platform === 'win32';
426
+ const tunnelChild = spawn('npm', ['exec', 'localtunnel', '--', '--port', '5000'], {
427
+ stdio: 'pipe', shell: isWin
428
+ });
429
+ tunnelChild.stdout.on('data', async (data) => {
430
+ const output = data.toString();
431
+ const match = output.match(/your url is: (https:\/\/.+)/);
432
+ if (match && match[1]) {
433
+ const publicUrl = match[1];
434
+ success(`Public URL: ${c.cyan}${c.under}${publicUrl}${c.reset}`);
435
+ if (envVars['TELEGRAM_BOT_TOKEN']) {
436
+ try {
437
+ const tgUrl = `https://api.telegram.org/bot${envVars['TELEGRAM_BOT_TOKEN']}/setWebhook?url=${publicUrl}/api/telegram/webhook`;
438
+ const res = await fetch(tgUrl);
439
+ const result = await res.json();
440
+ if (result.ok) success('Telegram Webhook set!');
441
+ else warn('Telegram Webhook failed: ' + result.description);
442
+ } catch (err) { warn('Webhook error: ' + err.message); }
443
+ }
444
+ }
445
+ });
446
+ tunnelChild.stderr.on('data', (data) => warn('Tunnel: ' + data.toString().trim()));
447
+ }
448
+ }, 3000);
141
449
  })();
142
450
  break;
451
+ }
452
+
453
+ // ╔══════════════════════════════════════════════════════════════╗
454
+ // ║ 🧠 MODEL SWITCH ║
455
+ // ╚══════════════════════════════════════════════════════════════╝
456
+ case 'model': {
457
+ console.log(`\n${c.saffron}${c.bold} 🧠 Model Selector${c.reset}\n`);
143
458
 
144
- case 'model':
145
- console.log('🔄 Changing SamarthyaBot Active AI Model...');
146
- const rlModel = readline.createInterface({ input: process.stdin, output: process.stdout });
147
- const qModel = (query) => new Promise(res => rlModel.question(query, res));
459
+ const rl = createRL();
460
+ const q = (query) => ask(rl, ` ${c.white}${query}${c.reset}`);
148
461
 
149
462
  (async () => {
150
- console.log("\n🌐 Select your primary AI Provider:");
151
- console.log(" 1) Google Gemini (Default)");
152
- console.log(" 2) Anthropic Claude");
153
- console.log(" 3) Groq (Fastest)");
154
- console.log(" 4) OpenAI");
155
- console.log(" 5) Local Ollama (Offline)");
156
- console.log(" 6) Mistral AI\n");
157
-
158
- let pRaw = await qModel("Enter choice (1-6, default 1): ");
159
- let aProv = 'gemini';
160
- let uOll = 'false';
161
-
162
- switch (pRaw.trim()) {
163
- case '2': aProv = 'anthropic'; break;
164
- case '3': aProv = 'groq'; break;
165
- case '4': aProv = 'openai'; break;
166
- case '5': aProv = 'ollama'; uOll = 'true'; break;
167
- case '6': aProv = 'mistral'; break;
168
- default: aProv = 'gemini'; break;
463
+ // Step 1: Provider
464
+ console.log(` ${c.bold}Select Provider:${c.reset}`);
465
+ PROVIDERS.forEach((p, i) => {
466
+ console.log(` ${c.dim}${i + 1})${c.reset} ${p.color}${p.name}${c.reset} ${c.gray}(${p.tag})${c.reset}`);
467
+ });
468
+ console.log();
469
+
470
+ let pRaw = await q(` Choice [1-9]: `);
471
+ const pIdx = parseInt(pRaw.trim()) - 1;
472
+ const prov = PROVIDERS[pIdx >= 0 && pIdx < PROVIDERS.length ? pIdx : 0];
473
+
474
+ console.log(`\n ${c.lime}✔${c.reset} Provider: ${prov.color}${c.bold}${prov.name}${c.reset}\n`);
475
+
476
+ // Step 2: Model
477
+ const models = MODELS[prov.id] || [];
478
+ if (models.length > 0) {
479
+ console.log(` ${c.bold}Select Model:${c.reset}`);
480
+ models.forEach((m, i) => {
481
+ console.log(` ${c.dim}${i + 1})${c.reset} ${c.white}${m.id}${c.reset} ${c.gray}— ${m.desc}${c.reset}`);
482
+ });
483
+ console.log();
169
484
  }
170
485
 
171
- let aMod = '';
172
- console.log(`\n📌 Sub-Models for ${aProv.toUpperCase()}:`);
173
- if (aProv === 'gemini') {
174
- console.log(" 1) gemini-2.5-flash (Fastest reasoning, great pricing)");
175
- console.log(" 2) gemini-2.5-pro (Advanced complex multi-step reasoning)");
176
- console.log(" 3) gemini-2.5-flash-lite (Fastest, budget friendly)");
177
- console.log(" 4) gemini-2.0-flash (Older 1M context version)");
178
- const mSel = await qModel("Enter model choice (or type custom model ID directly): ");
179
- if (mSel.trim() === '1' || mSel.trim() === '') aMod = 'gemini-2.5-flash';
180
- else if (mSel.trim() === '2') aMod = 'gemini-2.5-pro';
181
- else if (mSel.trim() === '3') aMod = 'gemini-2.5-flash-lite';
182
- else if (mSel.trim() === '4') aMod = 'gemini-2.0-flash';
183
- else aMod = mSel.trim();
184
- } else if (aProv === 'openai') {
185
- console.log(" 1) gpt-5.2 (Best for coding & agentic tasks)");
186
- console.log(" 2) gpt-5-mini (Faster, cost-efficient GPT-5)");
187
- console.log(" 3) gpt-4o (Fast, intelligent, flexible)");
188
- console.log(" 4) o3-mini (Small reasoning alternative)");
189
- const mSel = await qModel("Enter model choice (or type custom ID): ");
190
- if (mSel.trim() === '1') aMod = 'gpt-5.2';
191
- else if (mSel.trim() === '2') aMod = 'gpt-5-mini';
192
- else if (mSel.trim() === '3') aMod = 'gpt-4o';
193
- else if (mSel.trim() === '4') aMod = 'o3-mini';
194
- else aMod = mSel.trim();
195
- } else if (aProv === 'groq') {
196
- console.log(" 1) llama-3.3-70b-versatile (Best overall Llama)");
197
- console.log(" 2) llama-3.1-8b-instant (Extreme Fast Llama)");
198
- console.log(" 3) qwen/qwen3-32b (Powerful 32b reasoning)");
199
- const mSel = await qModel("Enter model choice (or type custom ID): ");
200
- if (mSel.trim() === '1' || mSel.trim() === '') aMod = 'llama-3.3-70b-versatile';
201
- else if (mSel.trim() === '2') aMod = 'llama-3.1-8b-instant';
202
- else if (mSel.trim() === '3') aMod = 'qwen/qwen3-32b';
203
- else aMod = mSel.trim();
204
- } else if (aProv === 'mistral') {
205
- console.log(" 1) mistral-large-3 (General-purpose multimodal)");
206
- console.log(" 2) ministral-3-8b (Powerful & efficient)");
207
- console.log(" 3) mistral-small-3.2 (Small latest model)");
208
- console.log(" 4) devstral-2 (Best for code & agents)");
209
- const mSel = await qModel("Enter model choice (or type custom ID): ");
210
- if (mSel.trim() === '1' || mSel.trim() === '') aMod = 'mistral-large-3';
211
- else if (mSel.trim() === '2') aMod = 'ministral-3-8b';
212
- else if (mSel.trim() === '3') aMod = 'mistral-small-3.2';
213
- else if (mSel.trim() === '4') aMod = 'devstral-2';
214
- else aMod = mSel.trim();
215
- } else if (aProv === 'anthropic') {
216
- console.log(" 1) claude-3-5-sonnet-latest");
217
- console.log(" 2) claude-3-opus-latest");
218
- const mSel = await qModel("Enter model choice (or type custom ID): ");
219
- if (mSel.trim() === '1' || mSel.trim() === '') aMod = 'claude-3-5-sonnet-latest';
220
- else if (mSel.trim() === '2') aMod = 'claude-3-opus-latest';
221
- else aMod = mSel.trim();
222
- } else if (aProv === 'ollama') {
223
- console.log(" Your models list usually includes:");
224
- console.log(" 1) dolphin3:8b-llama3.1-q4_K_M (Default)");
225
- console.log(" 2) llama3:8b");
226
- console.log(" 3) mistral");
227
- const mSel = await qModel("Enter model choice (or type custom Ollama tag): ");
228
- if (mSel.trim() === '1' || mSel.trim() === '') aMod = 'dolphin3:8b-llama3.1-q4_K_M';
229
- else if (mSel.trim() === '2') aMod = 'llama3:8b';
230
- else if (mSel.trim() === '3') aMod = 'mistral';
231
- else aMod = mSel.trim();
486
+ let mRaw = await q(` Choice (or type custom ID): `);
487
+ const mIdx = parseInt(mRaw.trim()) - 1;
488
+ let modelId = '';
489
+ if (mIdx >= 0 && mIdx < models.length) modelId = models[mIdx].id;
490
+ else if (mRaw.trim()) modelId = mRaw.trim();
491
+ else modelId = models[0]?.id || '';
492
+
493
+ console.log(`\n ${c.lime}✔${c.reset} Model: ${c.cyan}${c.bold}${modelId}${c.reset}\n`);
494
+
495
+ // Save
496
+ const spin = new Spinner('Updating configuration...');
497
+ spin.start();
498
+ await sleep(400);
499
+
500
+ const envVars = readEnv();
501
+ envVars['ACTIVE_PROVIDER'] = prov.id;
502
+ envVars['ACTIVE_MODEL'] = modelId;
503
+ envVars['USE_OLLAMA'] = prov.id === 'ollama' ? 'true' : 'false';
504
+ if (prov.id === 'ollama') envVars['OLLAMA_MODEL'] = modelId;
505
+ writeEnv(envVars);
506
+
507
+ spin.stop('Configuration updated');
508
+ warn('Restart the gateway for changes to take effect: ' + c.saffron + 'samarthya restart' + c.reset);
509
+
510
+ rl.close();
511
+ })();
512
+ break;
513
+ }
514
+
515
+ // ╔══════════════════════════════════════════════════════════════╗
516
+ // ║ 📱 TELEGRAM CONFIG ║
517
+ // ╚══════════════════════════════════════════════════════════════╝
518
+ case 'telegram': {
519
+ console.log(`\n${c.saffron}${c.bold} 📱 Telegram Configuration${c.reset}\n`);
520
+
521
+ const rl = createRL();
522
+ const q = (query) => ask(rl, ` ${c.white}${query}${c.reset}`);
523
+
524
+ (async () => {
525
+ const envVars = readEnv();
526
+ const existing = envVars['TELEGRAM_BOT_TOKEN'];
527
+
528
+ if (existing && existing !== 'dummy') {
529
+ info(`Current token: ${c.dim}${maskKey(existing)}${c.reset}`);
232
530
  }
233
531
 
234
- const ePath = path.join(backendDir, '.env');
235
- if (fs.existsSync(ePath)) {
236
- let currentEnv = fs.readFileSync(ePath, 'utf8');
237
- let lines = currentEnv.split('\n');
238
- let updated = false;
239
- let mUpdated = false;
240
-
241
- lines = lines.map(line => {
242
- if (line.startsWith('ACTIVE_PROVIDER=')) { updated = true; return `ACTIVE_PROVIDER=${aProv}`; }
243
- if (line.startsWith('ACTIVE_MODEL=')) { mUpdated = true; return `ACTIVE_MODEL=${aMod}`; }
244
- if (line.startsWith('USE_OLLAMA=')) { uUpdated = true; return `USE_OLLAMA=${uOll}`; }
245
- if (aProv === 'ollama' && line.startsWith('OLLAMA_MODEL=')) { return `OLLAMA_MODEL=${aMod}`; }
246
- return line;
247
- });
532
+ const token = await q(` 🤖 Enter Telegram Bot Token (or Enter to keep): `);
533
+ if (token.trim()) {
534
+ const spin = new Spinner('Saving Telegram token...');
535
+ spin.start();
536
+ await sleep(400);
537
+ envVars['TELEGRAM_BOT_TOKEN'] = token.trim();
538
+ writeEnv(envVars);
539
+ spin.stop('Telegram token saved');
540
+ info(`Run ${c.saffron}samarthya tunnel${c.reset} to set webhook automatically`);
541
+ } else {
542
+ info('No changes made');
543
+ }
544
+ rl.close();
545
+ })();
546
+ break;
547
+ }
548
+
549
+ // ╔══════════════════════════════════════════════════════════════╗
550
+ // ║ 🟣 DISCORD CONFIG ║
551
+ // ╚══════════════════════════════════════════════════════════════╝
552
+ case 'discord': {
553
+ console.log(`\n${c.purple}${c.bold} 🟣 Discord Configuration${c.reset}\n`);
554
+
555
+ const rl = createRL();
556
+ const q = (query) => ask(rl, ` ${c.white}${query}${c.reset}`);
248
557
 
249
- if (!updated) lines.push(`ACTIVE_PROVIDER=${aProv}`);
250
- if (!mUpdated) lines.push(`ACTIVE_MODEL=${aMod}`);
251
- if (!uUpdated) lines.push(`USE_OLLAMA=${uOll}`);
558
+ (async () => {
559
+ const envVars = readEnv();
560
+ const existing = envVars['DISCORD_BOT_TOKEN'];
561
+
562
+ if (existing) {
563
+ info(`Current token: ${c.dim}${maskKey(existing)}${c.reset}`);
564
+ }
252
565
 
253
- fs.writeFileSync(ePath, lines.join('\n'));
254
- console.log(`\n✅ Model successfully switched to: ${aProv.toUpperCase()}!`);
255
- console.log('🔄 Please restart the gateway if it is currently running.');
566
+ const token = await q(` 🟣 Enter Discord Bot Token (or Enter to keep): `);
567
+ if (token.trim()) {
568
+ const spin = new Spinner('Saving Discord token...');
569
+ spin.start();
570
+ await sleep(400);
571
+ envVars['DISCORD_BOT_TOKEN'] = token.trim();
572
+ writeEnv(envVars);
573
+ spin.stop('Discord token saved');
574
+ info(`Restart gateway to connect: ${c.saffron}samarthya restart${c.reset}`);
256
575
  } else {
257
- console.log(' Error: .env file not found. Please run "samarthya onboard" first.');
576
+ info('No changes made');
258
577
  }
259
578
 
260
- rlModel.close();
579
+ // Allow-list config
580
+ const allowFrom = await q(` 👤 Discord user IDs to allow (comma-separated, or Enter to skip): `);
581
+ if (allowFrom.trim()) {
582
+ envVars['DISCORD_ALLOW_FROM'] = allowFrom.trim();
583
+ writeEnv(envVars);
584
+ success('Allow-list updated');
585
+ }
586
+
587
+ rl.close();
261
588
  })();
262
589
  break;
590
+ }
263
591
 
264
- case 'gateway':
592
+ // ╔══════════════════════════════════════════════════════════════╗
593
+ // ║ ⚙️ CONFIG VIEWER ║
594
+ // ╚══════════════════════════════════════════════════════════════╝
595
+ case 'config': {
596
+ console.log(`\n${c.saffron}${c.bold} ⚙️ Current Configuration${c.reset}\n`);
597
+ const envVars = readEnv();
598
+
599
+ const configItems = [
600
+ { label: 'AI Provider', key: 'ACTIVE_PROVIDER', color: c.cyan },
601
+ { label: 'AI Model', key: 'ACTIVE_MODEL', color: c.cyan },
602
+ { label: 'Port', key: 'PORT', color: c.white },
603
+ { label: 'MongoDB', key: 'MONGO_URI', color: c.green },
604
+ { label: 'Gemini Key', key: 'GEMINI_API_KEY', color: c.cyan, mask: true },
605
+ { label: 'Groq Key', key: 'GROQ_API_KEY', color: c.yellow, mask: true },
606
+ { label: 'Anthropic Key', key: 'ANTHROPIC_API_KEY', color: c.purple, mask: true },
607
+ { label: 'OpenAI Key', key: 'OPENAI_API_KEY', color: c.lime, mask: true },
608
+ { label: 'DeepSeek Key', key: 'DEEPSEEK_API_KEY', color: c.cyan, mask: true },
609
+ { label: 'OpenRouter Key', key: 'OPENROUTER_API_KEY', color: c.saffron, mask: true },
610
+ { label: 'Telegram Token', key: 'TELEGRAM_BOT_TOKEN', color: c.cyan, mask: true },
611
+ { label: 'Discord Token', key: 'DISCORD_BOT_TOKEN', color: c.purple, mask: true },
612
+ { label: 'Encryption Key', key: 'MEMORY_ENCRYPTION_KEY', color: c.red, mask: true },
613
+ { label: 'Sandbox', key: 'RESTRICT_TO_WORKSPACE', color: c.green },
614
+ { label: 'Heartbeat (min)', key: 'HEARTBEAT_INTERVAL', color: c.pink },
615
+ ];
616
+
617
+ const maxLabel = Math.max(...configItems.map(i => i.label.length));
618
+
619
+ configItems.forEach(item => {
620
+ const val = envVars[item.key];
621
+ const label = item.label.padEnd(maxLabel + 2);
622
+ if (val && val !== 'dummy') {
623
+ const display = item.mask ? maskKey(val) : val;
624
+ console.log(` ${c.dim}${label}${c.reset} ${item.color}${display}${c.reset}`);
625
+ } else {
626
+ console.log(` ${c.dim}${label}${c.reset} ${c.gray}not set${c.reset}`);
627
+ }
628
+ });
629
+
630
+ console.log(`\n ${c.dim}File: ${path.join(backendDir, '.env')}${c.reset}\n`);
631
+ break;
632
+ }
633
+
634
+ // ╔══════════════════════════════════════════════════════════════╗
635
+ // ║ 🌐 GATEWAY ║
636
+ // ╚══════════════════════════════════════════════════════════════╝
637
+ case 'gateway': {
265
638
  if (isServerRunning()) {
266
- console.log('⚠️ Gateway is already running on port 5000!');
267
- console.log('🌐 Access the dashboard at http://localhost:5000');
639
+ warn('Gateway is already running on port 5000!');
640
+ info(`Dashboard: ${c.cyan}${c.under}http://localhost:5000${c.reset}`);
268
641
  process.exit(0);
269
642
  }
270
643
 
271
- try { require('dotenv').config({ path: path.join(backendDir, '.env') }); } catch (e) { /* ignore if not installed */ }
644
+ console.log(MINI_BANNER);
272
645
 
273
- const activeProvider = process.env.ACTIVE_PROVIDER ? process.env.ACTIVE_PROVIDER.toUpperCase() : 'GEMINI';
274
- const activeModel = process.env.ACTIVE_MODEL || 'gemini-2.5-flash';
646
+ try { require('dotenv').config({ path: path.join(backendDir, '.env') }); } catch (e) { }
647
+
648
+ const prov = process.env.ACTIVE_PROVIDER?.toUpperCase() || 'GEMINI';
649
+ const mod = process.env.ACTIVE_MODEL || 'gemini-2.5-flash';
275
650
 
276
- console.log('🚀 Starting SamarthyaBot Gateway in the background...');
277
- console.log(`🧠 Local Config: Using ${activeProvider} (${activeModel})`);
278
- console.log('💡 Tip: Run "samarthya model" to change your AI provider/model at any time.');
279
- console.log('🌐 You can access your agent dashboard at http://localhost:5000\n');
651
+ console.log(` ${c.dim}Provider: ${c.saffron}${prov}${c.reset} ${c.dim}(${mod})${c.reset}`);
652
+ console.log(` ${c.dim}Dashboard: ${c.cyan}${c.under}http://localhost:5000${c.reset}\n`);
280
653
 
281
- // We use spawn to run the server
282
654
  const child = spawn('node', ['server.js'], {
283
655
  cwd: backendDir,
284
656
  stdio: 'inherit'
285
657
  });
286
658
 
287
659
  child.on('close', (code) => {
288
- console.log(`Gateway exited with code ${code}`);
660
+ console.log(`\n${c.red}Gateway exited with code ${code}${c.reset}`);
289
661
  });
290
662
  break;
663
+ }
291
664
 
292
- case 'status':
665
+ // ╔══════════════════════════════════════════════════════════════╗
666
+ // ║ 📊 STATUS ║
667
+ // ╚══════════════════════════════════════════════════════════════╝
668
+ case 'status': {
669
+ console.log();
293
670
  if (isServerRunning()) {
294
- console.log('🟢 SamarthyaBot Gateway is actively running on port 5000.');
295
- console.log('🌐 Dashboard: http://localhost:5000');
671
+ success(`${c.bold}SamarthyaBot is ${c.lime}ONLINE${c.reset}`);
672
+ info(`Dashboard: ${c.cyan}${c.under}http://localhost:5000${c.reset}`);
673
+
674
+ const envVars = readEnv();
675
+ if (envVars['ACTIVE_PROVIDER']) {
676
+ info(`Provider: ${c.saffron}${envVars['ACTIVE_PROVIDER'].toUpperCase()}${c.reset}`);
677
+ }
296
678
  } else {
297
- console.log('🔴 SamarthyaBot Gateway is offline. Run "samarthya gateway" to start.');
679
+ error(`${c.bold}SamarthyaBot is ${c.red}OFFLINE${c.reset}`);
680
+ info(`Start with: ${c.saffron}samarthya gateway${c.reset}`);
298
681
  }
682
+ console.log();
299
683
  break;
684
+ }
685
+
686
+ // ╔══════════════════════════════════════════════════════════════╗
687
+ // ║ 🚇 TUNNEL ║
688
+ // ╚══════════════════════════════════════════════════════════════╝
689
+ case 'tunnel': {
690
+ console.log(`\n${c.saffron}${c.bold} 🚇 LocalTunnel${c.reset}\n`);
300
691
 
301
- case 'tunnel':
302
- console.log('🚇 Starting LocalTunnel to expose port 5000 to the internet...');
303
692
  if (!isServerRunning()) {
304
- console.log('⚠️ Warning: SamarthyaBot Gateway is not running! Run "samarthya gateway" in another terminal first.');
693
+ warn('Gateway is not running! Start it first: ' + c.saffron + 'samarthya gateway' + c.reset);
305
694
  }
306
695
 
307
696
  try { require('dotenv').config({ path: path.join(backendDir, '.env') }); } catch (e) { }
308
697
 
309
- const isWindows = process.platform === 'win32';
698
+ const spin = new Spinner('Connecting to tunnel service...');
699
+ spin.start();
700
+
701
+ const isWin = process.platform === 'win32';
310
702
  const tunnelProcess = spawn('npm', ['exec', 'localtunnel', '--', '--port', '5000'], {
311
- stdio: 'pipe',
312
- shell: isWindows
703
+ stdio: 'pipe', shell: isWin
313
704
  });
314
705
 
315
706
  tunnelProcess.stdout.on('data', async (data) => {
316
707
  const output = data.toString();
317
- console.log(output.trim());
318
708
  const match = output.match(/your url is: (https:\/\/.+)/);
319
709
  if (match && match[1]) {
710
+ spin.stop('Tunnel connected');
320
711
  const publicUrl = match[1];
321
- console.log(`\n✅ Public Gateway URL: ${publicUrl}`);
712
+ success(`Public URL: ${c.cyan}${c.under}${publicUrl}${c.reset}`);
713
+ info(`Telegram webhook: ${publicUrl}/api/telegram/webhook`);
714
+ info(`WhatsApp webhook: ${publicUrl}/api/whatsapp/webhook`);
322
715
 
323
- // Set Telegram Webhook Automatically
324
716
  if (process.env.TELEGRAM_BOT_TOKEN) {
325
- console.log('🔗 Setting Telegram Webhook...');
717
+ const spin2 = new Spinner('Setting Telegram webhook...');
718
+ spin2.start();
326
719
  try {
327
720
  const tgUrl = `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/setWebhook?url=${publicUrl}/api/telegram/webhook`;
328
721
  const res = await fetch(tgUrl);
329
722
  const result = await res.json();
330
- if (result.ok) {
331
- console.log('🟢 Telegram Webhook Set Successfully!');
332
- } else {
333
- console.log('🔴 Failed to set Telegram Webhook:', result.description);
334
- }
723
+ if (result.ok) spin2.stop('Telegram webhook set!');
724
+ else spin2.fail('Webhook failed: ' + result.description);
335
725
  } catch (err) {
336
- console.log('🔴 Error setting Telegram Webhook:', err.message);
726
+ spin2.fail('Webhook error: ' + err.message);
337
727
  }
338
- } else {
339
- console.log('ℹ️ TELEGRAM_BOT_TOKEN not found in .env. Skipping Telegram Webhook setup.');
340
728
  }
341
729
 
342
- console.log('\n📱 Put this URL in your Meta WhatsApp App Dashboard:');
343
- console.log(` ${publicUrl}/api/whatsapp/webhook`);
344
- console.log('\n(Leave this terminal running to keep the tunnel open natively)');
730
+ console.log(`\n ${c.dim}Keep this terminal open to maintain the tunnel${c.reset}\n`);
345
731
  }
346
732
  });
347
733
 
348
734
  tunnelProcess.stderr.on('data', (data) => {
349
- console.error('Tunnel Error:', data.toString());
735
+ const msg = data.toString().trim();
736
+ if (msg) warn('Tunnel: ' + msg);
350
737
  });
351
738
 
352
739
  tunnelProcess.on('close', (code) => {
353
- console.log(`Tunnel closed with code ${code}`);
740
+ console.log(`\n${c.dim}Tunnel closed with code ${code}${c.reset}`);
354
741
  });
355
-
356
742
  break;
743
+ }
357
744
 
358
- case 'stop':
745
+ // ╔══════════════════════════════════════════════════════════════╗
746
+ // ║ 🛑 STOP ║
747
+ // ╚══════════════════════════════════════════════════════════════╝
748
+ case 'stop': {
749
+ console.log();
359
750
  if (isServerRunning()) {
360
- console.log('🛑 Stopping SamarthyaBot Gateway...');
751
+ const spin = new Spinner('Stopping SamarthyaBot...');
752
+ spin.start();
361
753
  try {
362
754
  if (process.platform === 'win32') {
363
- // Find PID of process listening on port 5000 and kill it
364
755
  const netstatOut = execSync('netstat -ano | findstr :5000', { encoding: 'utf-8' });
365
- const lines = netstatOut.split('\\n');
366
- for (const line of lines) {
756
+ netstatOut.split('\n').forEach(line => {
367
757
  if (line.includes('LISTENING')) {
368
- const parts = line.trim().split(/\\s+/);
758
+ const parts = line.trim().split(/\s+/);
369
759
  const pid = parts[parts.length - 1];
370
- if (pid && pid !== '0') {
371
- execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
372
- }
760
+ if (pid && pid !== '0') execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
373
761
  }
374
- }
762
+ });
375
763
  } else {
376
- // macOS and Linux
377
764
  execSync('lsof -t -i:5000 | xargs kill -9 2>/dev/null || fuser -k 5000/tcp 2>/dev/null');
378
765
  }
379
- console.log('Gateway stopped successfully.');
380
- } catch (e) {
381
- console.log('Failed to stop gateway gracefully. Process might already be dead.');
766
+ spin.stop('Gateway stopped');
767
+ } catch {
768
+ spin.fail('Failed to stop gateway');
382
769
  }
383
770
  } else {
384
- console.log('⚠️ Gateway is not currently running.');
771
+ info('Gateway is not running');
385
772
  }
773
+ console.log();
386
774
  break;
775
+ }
776
+
777
+ // ╔══════════════════════════════════════════════════════════════╗
778
+ // ║ 🔄 RESTART ║
779
+ // ╚══════════════════════════════════════════════════════════════╝
780
+ case 'restart': {
781
+ console.log();
782
+ const spin = new Spinner('Restarting SamarthyaBot...');
783
+ spin.start();
387
784
 
388
- case 'restart':
389
- console.log('🔄 Restarting SamarthyaBot Gateway...');
390
785
  if (isServerRunning()) {
391
786
  try {
392
787
  if (process.platform === 'win32') {
393
788
  const netstatOut = execSync('netstat -ano | findstr :5000', { encoding: 'utf-8' });
394
- const lines = netstatOut.split('\\n');
395
- for (const line of lines) {
789
+ netstatOut.split('\n').forEach(line => {
396
790
  if (line.includes('LISTENING')) {
397
- const parts = line.trim().split(/\\s+/);
791
+ const parts = line.trim().split(/\s+/);
398
792
  const pid = parts[parts.length - 1];
399
- if (pid && pid !== '0') {
400
- execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
401
- }
793
+ if (pid && pid !== '0') execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
402
794
  }
403
- }
795
+ });
404
796
  } else {
405
797
  execSync('lsof -t -i:5000 | xargs kill -9 2>/dev/null || fuser -k 5000/tcp 2>/dev/null');
406
798
  }
407
- } catch (e) { /* ignore */ }
799
+ } catch { }
408
800
  }
409
- // Give it a moment to free the port
801
+
410
802
  setTimeout(() => {
803
+ spin.stop('Restarted');
411
804
  const restartChild = spawn('node', ['server.js'], {
412
805
  cwd: backendDir,
413
806
  stdio: 'inherit'
414
807
  });
415
808
  restartChild.on('close', (code) => {
416
- console.log(`Gateway exited with code ${code}`);
809
+ console.log(`\n${c.red}Gateway exited with code ${code}${c.reset}`);
417
810
  });
418
- console.log('✅ Gateway restarted successfully!');
419
- console.log('🌐 Dashboard: http://localhost:5000\n');
420
- }, 1000);
811
+ info(`Dashboard: ${c.cyan}${c.under}http://localhost:5000${c.reset}\n`);
812
+ }, 1200);
421
813
  break;
814
+ }
422
815
 
423
816
  default:
424
- console.log(`❌ Unknown command: ${command}`);
425
- console.log('Try "samarthya onboard" or "samarthya gateway"');
817
+ error(`Unknown command: ${c.bold}${command}${c.reset}`);
818
+ info(`Run ${c.saffron}samarthya${c.reset} to see all commands`);
426
819
  process.exit(1);
427
820
  }