samarthya-bot 2.0.0 → 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,171 +6,407 @@ 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 — Your Local Agentic OS
32
- Usage:
33
- samarthya onboard - Setup + Start everything (single command!)
34
- samarthya model - Change your active AI provider/model
35
- samarthya gateway - Start the local server only
36
- samarthya tunnel - Expose to internet & setup webhooks
37
- samarthya status - Check if the agent is running
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, Free tier)");
58
- console.log(" 2) Anthropic Claude (Smartest)");
59
- console.log(" 3) Groq (Fastest)");
60
- console.log(" 4) OpenAI (GPT-5)");
61
- console.log(" 5) DeepSeek (Budget-friendly)");
62
- console.log(" 6) Qwen (Alibaba)");
63
- console.log(" 7) OpenRouter (100+ models via 1 key)");
64
- console.log(" 8) Local Ollama (Offline, Private)");
65
- console.log(" 9) Mistral AI\n");
66
-
67
- let providerRaw = await question("Enter choice (1-9, default 1): ");
68
- let activeProvider = 'gemini';
69
- let useOllama = 'false';
70
-
71
- switch (providerRaw.trim()) {
72
- case '2': activeProvider = 'anthropic'; break;
73
- case '3': activeProvider = 'groq'; break;
74
- case '4': activeProvider = 'openai'; break;
75
- case '5': activeProvider = 'deepseek'; break;
76
- case '6': activeProvider = 'qwen'; break;
77
- case '7': activeProvider = 'openrouter'; break;
78
- case '8':
79
- activeProvider = 'ollama';
80
- useOllama = 'true';
81
- break;
82
- case '9': activeProvider = 'mistral'; break;
83
- case '1':
84
- default:
85
- activeProvider = 'gemini';
86
- 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`);
87
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();
88
314
 
89
- console.log(`\n👉 Selected Provider: ${activeProvider.toUpperCase()}\n`);
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`);
323
+ }
324
+ console.log();
325
+
326
+ // ── Step 4: Save configuration ──
327
+ stepHeader(4, TOTAL_STEPS, 'Saving Configuration');
90
328
 
91
- const geminiKey = await question('🔑 Enter Google Gemini API Key (or press Enter to skip if already set): ');
92
- const anthropicKey = await question('🔑 Enter Anthropic (Claude) API Key (or press Enter to skip): ');
93
- const groqKey = await question('🔑 Enter Groq API Key (or press Enter to skip): ');
94
- const openAiKey = await question('🔑 Enter OpenAI API Key (or press Enter to skip): ');
95
- const deepseekKey = await question('🔑 Enter DeepSeek API Key (or press Enter to skip): ');
96
- const qwenKey = await question('🔑 Enter Qwen API Key (or press Enter to skip): ');
97
- const openrouterKey = await question('🔑 Enter OpenRouter API Key (or press Enter to skip): ');
98
- const telegramToken = await question('🤖 Enter Telegram Bot Token (or press Enter to skip): ');
99
- const discordToken = await question('🟣 Enter Discord Bot Token (or press Enter to skip): ');
100
- const encKey = await question('🔐 Enter 32-char Encryption Key (or press Enter for auto-generate): ');
329
+ const spin1 = new Spinner('Writing .env configuration...');
330
+ spin1.start();
331
+ await sleep(600);
101
332
 
102
333
  const envPath = path.join(backendDir, '.env');
103
- let envVars = {};
104
-
105
- // Read existing if present to keep other configs
106
- if (fs.existsSync(envPath)) {
107
- const currentEnv = fs.readFileSync(envPath, 'utf8');
108
- currentEnv.split('\n').forEach(line => {
109
- const eqIndex = line.indexOf('=');
110
- if (eqIndex > 0) {
111
- const k = line.substring(0, eqIndex).trim();
112
- const v = line.substring(eqIndex + 1).trim();
113
- if (k) envVars[k] = v;
114
- }
115
- });
116
- } else {
117
- envVars = {
334
+ let envVars = readEnv();
335
+
336
+ // Set defaults if fresh install
337
+ if (!envVars['PORT']) {
338
+ Object.assign(envVars, {
118
339
  PORT: '5000',
119
340
  MONGO_URI: 'mongodb://localhost:27017/samarthya',
120
341
  JWT_SECRET: 'samarthya_secret_key_change_in_production',
121
342
  NODE_ENV: 'production',
122
343
  CORS_ORIGIN: 'http://localhost:5000',
123
- USE_OLLAMA: useOllama,
124
- ACTIVE_PROVIDER: activeProvider,
125
- ACTIVE_MODEL: activeProvider === 'gemini' ? 'gemini-2.5-flash' : '',
126
344
  OLLAMA_URL: 'http://localhost:11434',
127
345
  OLLAMA_MODEL: 'dolphin3:8b-llama3.1-q4_K_M',
128
346
  RESTRICT_TO_WORKSPACE: 'true',
129
- HEARTBEAT_INTERVAL: '30'
130
- };
347
+ HEARTBEAT_INTERVAL: '30',
348
+ });
131
349
  }
132
350
 
133
- envVars['USE_OLLAMA'] = useOllama;
134
351
  envVars['ACTIVE_PROVIDER'] = activeProvider;
352
+ envVars['USE_OLLAMA'] = useOllama;
353
+ if (activeProvider === 'gemini' && !envVars['ACTIVE_MODEL']) envVars['ACTIVE_MODEL'] = 'gemini-2.5-flash';
135
354
 
136
- // Assign keys if provided
137
- if (geminiKey.trim()) envVars['GEMINI_API_KEY'] = geminiKey.trim();
138
- if (anthropicKey.trim()) envVars['ANTHROPIC_API_KEY'] = anthropicKey.trim();
139
- if (groqKey.trim()) envVars['GROQ_API_KEY'] = groqKey.trim();
140
- if (openAiKey.trim()) envVars['OPENAI_API_KEY'] = openAiKey.trim();
141
- if (deepseekKey.trim()) envVars['DEEPSEEK_API_KEY'] = deepseekKey.trim();
142
- if (qwenKey.trim()) envVars['QWEN_API_KEY'] = qwenKey.trim();
143
- if (openrouterKey.trim()) envVars['OPENROUTER_API_KEY'] = openrouterKey.trim();
355
+ if (apiKey.trim() && provider.envKey) envVars[provider.envKey] = apiKey.trim();
144
356
  if (telegramToken.trim()) envVars['TELEGRAM_BOT_TOKEN'] = telegramToken.trim();
145
357
  if (discordToken.trim()) envVars['DISCORD_BOT_TOKEN'] = discordToken.trim();
146
358
 
147
- // Auto-generate encryption key if not provided
148
- if (encKey.trim()) {
149
- envVars['MEMORY_ENCRYPTION_KEY'] = encKey.trim();
150
- } else if (!envVars['MEMORY_ENCRYPTION_KEY']) {
359
+ // Auto-generate encryption key
360
+ if (!envVars['MEMORY_ENCRYPTION_KEY']) {
151
361
  const crypto = require('crypto');
152
362
  envVars['MEMORY_ENCRYPTION_KEY'] = crypto.randomBytes(16).toString('hex');
153
- console.log('🔐 Auto-generated encryption key.');
154
363
  }
155
364
 
156
365
  if (!envVars['GEMINI_API_KEY']) envVars['GEMINI_API_KEY'] = 'dummy';
157
366
 
158
- // Write back to .env
159
- const newEnvContent = Object.keys(envVars).map(k => `${k}=${envVars[k]}`).join('\n');
160
- fs.writeFileSync(envPath, newEnvContent);
161
- console.log('\n✅ Keys saved to .env file securely.');
367
+ writeEnv(envVars);
368
+ spin1.stop('Configuration saved');
162
369
 
163
- console.log('📦 Installing backend dependencies...');
370
+ const spin2 = new Spinner('Installing dependencies...');
371
+ spin2.start();
164
372
  try {
165
- execSync('npm install --production', { cwd: backendDir, stdio: 'ignore' });
166
- } 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
+ }
167
378
 
168
379
  rl.close();
169
380
 
170
- // ═══════ AUTO-START: Gateway + Tunnel in one command ═══════
171
- console.log('\n🚀 Starting SamarthyaBot Gateway...');
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`);
172
408
 
173
- // Load the .env we just wrote
409
+ // Load .env and spawn the server
174
410
  try { require('dotenv').config({ path: envPath }); } catch (e) { }
175
411
 
176
412
  const gatewayChild = spawn('node', ['server.js'], {
@@ -179,338 +415,406 @@ switch (command) {
179
415
  });
180
416
 
181
417
  gatewayChild.on('close', (code) => {
182
- console.log(`Gateway exited with code ${code}`);
418
+ console.log(`\n${c.red}Gateway exited with code ${code}${c.reset}`);
183
419
  });
184
420
 
185
- // Wait for server to boot, then start tunnel
421
+ // Auto-start tunnel if Telegram configured
186
422
  setTimeout(() => {
187
423
  if (envVars['TELEGRAM_BOT_TOKEN'] && envVars['TELEGRAM_BOT_TOKEN'] !== 'dummy') {
188
- console.log('\n🚇 Auto-starting tunnel for Telegram webhook...');
424
+ info('Auto-starting tunnel for Telegram webhook...');
189
425
  const isWin = process.platform === 'win32';
190
426
  const tunnelChild = spawn('npm', ['exec', 'localtunnel', '--', '--port', '5000'], {
191
- stdio: 'pipe',
192
- shell: isWin
427
+ stdio: 'pipe', shell: isWin
193
428
  });
194
-
195
429
  tunnelChild.stdout.on('data', async (data) => {
196
430
  const output = data.toString();
197
- console.log(output.trim());
198
431
  const match = output.match(/your url is: (https:\/\/.+)/);
199
432
  if (match && match[1]) {
200
433
  const publicUrl = match[1];
201
- console.log(`\n✅ Public Gateway URL: ${publicUrl}`);
434
+ success(`Public URL: ${c.cyan}${c.under}${publicUrl}${c.reset}`);
202
435
  if (envVars['TELEGRAM_BOT_TOKEN']) {
203
436
  try {
204
437
  const tgUrl = `https://api.telegram.org/bot${envVars['TELEGRAM_BOT_TOKEN']}/setWebhook?url=${publicUrl}/api/telegram/webhook`;
205
438
  const res = await fetch(tgUrl);
206
439
  const result = await res.json();
207
- if (result.ok) console.log('🟢 Telegram Webhook Set!');
208
- else console.log('🔴 Telegram Webhook failed:', result.description);
209
- } catch (err) {
210
- console.log('🔴 Webhook error:', err.message);
211
- }
440
+ if (result.ok) success('Telegram Webhook set!');
441
+ else warn('Telegram Webhook failed: ' + result.description);
442
+ } catch (err) { warn('Webhook error: ' + err.message); }
212
443
  }
213
444
  }
214
445
  });
215
-
216
- tunnelChild.stderr.on('data', (data) => console.error('Tunnel:', data.toString()));
217
- } else {
218
- console.log('\n✅ Gateway is running! Open http://localhost:5000');
219
- console.log('ℹ️ No Telegram token found — skipping tunnel. Run "samarthya tunnel" later if needed.');
446
+ tunnelChild.stderr.on('data', (data) => warn('Tunnel: ' + data.toString().trim()));
220
447
  }
221
448
  }, 3000);
449
+ })();
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`);
458
+
459
+ const rl = createRL();
460
+ const q = (query) => ask(rl, ` ${c.white}${query}${c.reset}`);
461
+
462
+ (async () => {
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();
484
+ }
485
+
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`);
222
494
 
223
- // Don't exit — keep process alive
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();
224
511
  })();
225
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`);
226
520
 
227
- case 'model':
228
- console.log('🔄 Changing SamarthyaBot Active AI Model...');
229
- const rlModel = readline.createInterface({ input: process.stdin, output: process.stdout });
230
- const qModel = (query) => new Promise(res => rlModel.question(query, res));
521
+ const rl = createRL();
522
+ const q = (query) => ask(rl, ` ${c.white}${query}${c.reset}`);
231
523
 
232
524
  (async () => {
233
- console.log("\n🌐 Select your primary AI Provider:");
234
- console.log(" 1) Google Gemini (Default)");
235
- console.log(" 2) Anthropic Claude");
236
- console.log(" 3) Groq (Fastest)");
237
- console.log(" 4) OpenAI");
238
- console.log(" 5) DeepSeek");
239
- console.log(" 6) Qwen (Alibaba)");
240
- console.log(" 7) OpenRouter (100+ models)");
241
- console.log(" 8) Local Ollama (Offline)");
242
- console.log(" 9) Mistral AI\n");
243
-
244
- let pRaw = await qModel("Enter choice (1-9, default 1): ");
245
- let aProv = 'gemini';
246
- let uOll = 'false';
247
-
248
- switch (pRaw.trim()) {
249
- case '2': aProv = 'anthropic'; break;
250
- case '3': aProv = 'groq'; break;
251
- case '4': aProv = 'openai'; break;
252
- case '5': aProv = 'deepseek'; break;
253
- case '6': aProv = 'qwen'; break;
254
- case '7': aProv = 'openrouter'; break;
255
- case '8': aProv = 'ollama'; uOll = 'true'; break;
256
- case '9': aProv = 'mistral'; break;
257
- default: aProv = 'gemini'; break;
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}`);
258
530
  }
259
531
 
260
- let aMod = '';
261
- console.log(`\n📌 Sub-Models for ${aProv.toUpperCase()}:`);
262
- if (aProv === 'gemini') {
263
- console.log(" 1) gemini-2.5-flash (Fastest reasoning, great pricing)");
264
- console.log(" 2) gemini-2.5-pro (Advanced complex multi-step reasoning)");
265
- console.log(" 3) gemini-2.5-flash-lite (Fastest, budget friendly)");
266
- console.log(" 4) gemini-2.0-flash (Older 1M context version)");
267
- const mSel = await qModel("Enter model choice (or type custom model ID directly): ");
268
- if (mSel.trim() === '1' || mSel.trim() === '') aMod = 'gemini-2.5-flash';
269
- else if (mSel.trim() === '2') aMod = 'gemini-2.5-pro';
270
- else if (mSel.trim() === '3') aMod = 'gemini-2.5-flash-lite';
271
- else if (mSel.trim() === '4') aMod = 'gemini-2.0-flash';
272
- else aMod = mSel.trim();
273
- } else if (aProv === 'openai') {
274
- console.log(" 1) gpt-5.2 (Best for coding & agentic tasks)");
275
- console.log(" 2) gpt-5-mini (Faster, cost-efficient GPT-5)");
276
- console.log(" 3) gpt-4o (Fast, intelligent, flexible)");
277
- console.log(" 4) o3-mini (Small reasoning alternative)");
278
- const mSel = await qModel("Enter model choice (or type custom ID): ");
279
- if (mSel.trim() === '1') aMod = 'gpt-5.2';
280
- else if (mSel.trim() === '2') aMod = 'gpt-5-mini';
281
- else if (mSel.trim() === '3') aMod = 'gpt-4o';
282
- else if (mSel.trim() === '4') aMod = 'o3-mini';
283
- else aMod = mSel.trim();
284
- } else if (aProv === 'groq') {
285
- console.log(" 1) llama-3.3-70b-versatile (Best overall Llama)");
286
- console.log(" 2) llama-3.1-8b-instant (Extreme Fast Llama)");
287
- console.log(" 3) qwen/qwen3-32b (Powerful 32b reasoning)");
288
- const mSel = await qModel("Enter model choice (or type custom ID): ");
289
- if (mSel.trim() === '1' || mSel.trim() === '') aMod = 'llama-3.3-70b-versatile';
290
- else if (mSel.trim() === '2') aMod = 'llama-3.1-8b-instant';
291
- else if (mSel.trim() === '3') aMod = 'qwen/qwen3-32b';
292
- else aMod = mSel.trim();
293
- } else if (aProv === 'mistral') {
294
- console.log(" 1) mistral-large-3 (General-purpose multimodal)");
295
- console.log(" 2) ministral-3-8b (Powerful & efficient)");
296
- console.log(" 3) mistral-small-3.2 (Small latest model)");
297
- console.log(" 4) devstral-2 (Best for code & agents)");
298
- const mSel = await qModel("Enter model choice (or type custom ID): ");
299
- if (mSel.trim() === '1' || mSel.trim() === '') aMod = 'mistral-large-3';
300
- else if (mSel.trim() === '2') aMod = 'ministral-3-8b';
301
- else if (mSel.trim() === '3') aMod = 'mistral-small-3.2';
302
- else if (mSel.trim() === '4') aMod = 'devstral-2';
303
- else aMod = mSel.trim();
304
- } else if (aProv === 'anthropic') {
305
- console.log(" 1) claude-3-5-sonnet-latest");
306
- console.log(" 2) claude-3-opus-latest");
307
- const mSel = await qModel("Enter model choice (or type custom ID): ");
308
- if (mSel.trim() === '1' || mSel.trim() === '') aMod = 'claude-3-5-sonnet-latest';
309
- else if (mSel.trim() === '2') aMod = 'claude-3-opus-latest';
310
- else aMod = mSel.trim();
311
- } else if (aProv === 'ollama') {
312
- console.log(" Your models list usually includes:");
313
- console.log(" 1) dolphin3:8b-llama3.1-q4_K_M (Default)");
314
- console.log(" 2) llama3:8b");
315
- console.log(" 3) mistral");
316
- const mSel = await qModel("Enter model choice (or type custom Ollama tag): ");
317
- if (mSel.trim() === '1' || mSel.trim() === '') aMod = 'dolphin3:8b-llama3.1-q4_K_M';
318
- else if (mSel.trim() === '2') aMod = 'llama3:8b';
319
- else if (mSel.trim() === '3') aMod = 'mistral';
320
- else aMod = mSel.trim();
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');
321
543
  }
544
+ rl.close();
545
+ })();
546
+ break;
547
+ }
322
548
 
323
- const ePath = path.join(backendDir, '.env');
324
- if (fs.existsSync(ePath)) {
325
- let currentEnv = fs.readFileSync(ePath, 'utf8');
326
- let lines = currentEnv.split('\n');
327
- let updated = false;
328
- let mUpdated = false;
329
-
330
- lines = lines.map(line => {
331
- if (line.startsWith('ACTIVE_PROVIDER=')) { updated = true; return `ACTIVE_PROVIDER=${aProv}`; }
332
- if (line.startsWith('ACTIVE_MODEL=')) { mUpdated = true; return `ACTIVE_MODEL=${aMod}`; }
333
- if (line.startsWith('USE_OLLAMA=')) { uUpdated = true; return `USE_OLLAMA=${uOll}`; }
334
- if (aProv === 'ollama' && line.startsWith('OLLAMA_MODEL=')) { return `OLLAMA_MODEL=${aMod}`; }
335
- return line;
336
- });
549
+ // ╔══════════════════════════════════════════════════════════════╗
550
+ // ║ 🟣 DISCORD CONFIG ║
551
+ // ╚══════════════════════════════════════════════════════════════╝
552
+ case 'discord': {
553
+ console.log(`\n${c.purple}${c.bold} 🟣 Discord Configuration${c.reset}\n`);
337
554
 
338
- if (!updated) lines.push(`ACTIVE_PROVIDER=${aProv}`);
339
- if (!mUpdated) lines.push(`ACTIVE_MODEL=${aMod}`);
340
- if (!uUpdated) lines.push(`USE_OLLAMA=${uOll}`);
555
+ const rl = createRL();
556
+ const q = (query) => ask(rl, ` ${c.white}${query}${c.reset}`);
341
557
 
342
- fs.writeFileSync(ePath, lines.join('\n'));
343
- console.log(`\n✅ Model successfully switched to: ${aProv.toUpperCase()}!`);
344
- console.log('🔄 Please restart the gateway if it is currently running.');
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
+ }
565
+
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}`);
345
575
  } else {
346
- console.log(' Error: .env file not found. Please run "samarthya onboard" first.');
576
+ info('No changes made');
577
+ }
578
+
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');
347
585
  }
348
586
 
349
- rlModel.close();
587
+ rl.close();
350
588
  })();
351
589
  break;
590
+ }
591
+
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
+ }
352
633
 
353
- case 'gateway':
634
+ // ╔══════════════════════════════════════════════════════════════╗
635
+ // ║ 🌐 GATEWAY ║
636
+ // ╚══════════════════════════════════════════════════════════════╝
637
+ case 'gateway': {
354
638
  if (isServerRunning()) {
355
- console.log('⚠️ Gateway is already running on port 5000!');
356
- 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}`);
357
641
  process.exit(0);
358
642
  }
359
643
 
360
- try { require('dotenv').config({ path: path.join(backendDir, '.env') }); } catch (e) { /* ignore if not installed */ }
644
+ console.log(MINI_BANNER);
361
645
 
362
- const activeProvider = process.env.ACTIVE_PROVIDER ? process.env.ACTIVE_PROVIDER.toUpperCase() : 'GEMINI';
363
- const activeModel = process.env.ACTIVE_MODEL || 'gemini-2.5-flash';
646
+ try { require('dotenv').config({ path: path.join(backendDir, '.env') }); } catch (e) { }
364
647
 
365
- console.log('🚀 Starting SamarthyaBot Gateway in the background...');
366
- console.log(`🧠 Local Config: Using ${activeProvider} (${activeModel})`);
367
- console.log('💡 Tip: Run "samarthya model" to change your AI provider/model at any time.');
368
- console.log('🌐 You can access your agent dashboard at http://localhost:5000\n');
648
+ const prov = process.env.ACTIVE_PROVIDER?.toUpperCase() || 'GEMINI';
649
+ const mod = process.env.ACTIVE_MODEL || 'gemini-2.5-flash';
650
+
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`);
369
653
 
370
- // We use spawn to run the server
371
654
  const child = spawn('node', ['server.js'], {
372
655
  cwd: backendDir,
373
656
  stdio: 'inherit'
374
657
  });
375
658
 
376
659
  child.on('close', (code) => {
377
- console.log(`Gateway exited with code ${code}`);
660
+ console.log(`\n${c.red}Gateway exited with code ${code}${c.reset}`);
378
661
  });
379
662
  break;
663
+ }
380
664
 
381
- case 'status':
665
+ // ╔══════════════════════════════════════════════════════════════╗
666
+ // ║ 📊 STATUS ║
667
+ // ╚══════════════════════════════════════════════════════════════╝
668
+ case 'status': {
669
+ console.log();
382
670
  if (isServerRunning()) {
383
- console.log('🟢 SamarthyaBot Gateway is actively running on port 5000.');
384
- 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
+ }
385
678
  } else {
386
- 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}`);
387
681
  }
682
+ console.log();
388
683
  break;
684
+ }
685
+
686
+ // ╔══════════════════════════════════════════════════════════════╗
687
+ // ║ 🚇 TUNNEL ║
688
+ // ╚══════════════════════════════════════════════════════════════╝
689
+ case 'tunnel': {
690
+ console.log(`\n${c.saffron}${c.bold} 🚇 LocalTunnel${c.reset}\n`);
389
691
 
390
- case 'tunnel':
391
- console.log('🚇 Starting LocalTunnel to expose port 5000 to the internet...');
392
692
  if (!isServerRunning()) {
393
- 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);
394
694
  }
395
695
 
396
696
  try { require('dotenv').config({ path: path.join(backendDir, '.env') }); } catch (e) { }
397
697
 
398
- const isWindows = process.platform === 'win32';
698
+ const spin = new Spinner('Connecting to tunnel service...');
699
+ spin.start();
700
+
701
+ const isWin = process.platform === 'win32';
399
702
  const tunnelProcess = spawn('npm', ['exec', 'localtunnel', '--', '--port', '5000'], {
400
- stdio: 'pipe',
401
- shell: isWindows
703
+ stdio: 'pipe', shell: isWin
402
704
  });
403
705
 
404
706
  tunnelProcess.stdout.on('data', async (data) => {
405
707
  const output = data.toString();
406
- console.log(output.trim());
407
708
  const match = output.match(/your url is: (https:\/\/.+)/);
408
709
  if (match && match[1]) {
710
+ spin.stop('Tunnel connected');
409
711
  const publicUrl = match[1];
410
- 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`);
411
715
 
412
- // Set Telegram Webhook Automatically
413
716
  if (process.env.TELEGRAM_BOT_TOKEN) {
414
- console.log('🔗 Setting Telegram Webhook...');
717
+ const spin2 = new Spinner('Setting Telegram webhook...');
718
+ spin2.start();
415
719
  try {
416
720
  const tgUrl = `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/setWebhook?url=${publicUrl}/api/telegram/webhook`;
417
721
  const res = await fetch(tgUrl);
418
722
  const result = await res.json();
419
- if (result.ok) {
420
- console.log('🟢 Telegram Webhook Set Successfully!');
421
- } else {
422
- console.log('🔴 Failed to set Telegram Webhook:', result.description);
423
- }
723
+ if (result.ok) spin2.stop('Telegram webhook set!');
724
+ else spin2.fail('Webhook failed: ' + result.description);
424
725
  } catch (err) {
425
- console.log('🔴 Error setting Telegram Webhook:', err.message);
726
+ spin2.fail('Webhook error: ' + err.message);
426
727
  }
427
- } else {
428
- console.log('ℹ️ TELEGRAM_BOT_TOKEN not found in .env. Skipping Telegram Webhook setup.');
429
728
  }
430
729
 
431
- console.log('\n📱 Put this URL in your Meta WhatsApp App Dashboard:');
432
- console.log(` ${publicUrl}/api/whatsapp/webhook`);
433
- 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`);
434
731
  }
435
732
  });
436
733
 
437
734
  tunnelProcess.stderr.on('data', (data) => {
438
- console.error('Tunnel Error:', data.toString());
735
+ const msg = data.toString().trim();
736
+ if (msg) warn('Tunnel: ' + msg);
439
737
  });
440
738
 
441
739
  tunnelProcess.on('close', (code) => {
442
- console.log(`Tunnel closed with code ${code}`);
740
+ console.log(`\n${c.dim}Tunnel closed with code ${code}${c.reset}`);
443
741
  });
444
-
445
742
  break;
743
+ }
446
744
 
447
- case 'stop':
745
+ // ╔══════════════════════════════════════════════════════════════╗
746
+ // ║ 🛑 STOP ║
747
+ // ╚══════════════════════════════════════════════════════════════╝
748
+ case 'stop': {
749
+ console.log();
448
750
  if (isServerRunning()) {
449
- console.log('🛑 Stopping SamarthyaBot Gateway...');
751
+ const spin = new Spinner('Stopping SamarthyaBot...');
752
+ spin.start();
450
753
  try {
451
754
  if (process.platform === 'win32') {
452
- // Find PID of process listening on port 5000 and kill it
453
755
  const netstatOut = execSync('netstat -ano | findstr :5000', { encoding: 'utf-8' });
454
- const lines = netstatOut.split('\\n');
455
- for (const line of lines) {
756
+ netstatOut.split('\n').forEach(line => {
456
757
  if (line.includes('LISTENING')) {
457
- const parts = line.trim().split(/\\s+/);
758
+ const parts = line.trim().split(/\s+/);
458
759
  const pid = parts[parts.length - 1];
459
- if (pid && pid !== '0') {
460
- execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
461
- }
760
+ if (pid && pid !== '0') execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
462
761
  }
463
- }
762
+ });
464
763
  } else {
465
- // macOS and Linux
466
764
  execSync('lsof -t -i:5000 | xargs kill -9 2>/dev/null || fuser -k 5000/tcp 2>/dev/null');
467
765
  }
468
- console.log('Gateway stopped successfully.');
469
- } catch (e) {
470
- 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');
471
769
  }
472
770
  } else {
473
- console.log('⚠️ Gateway is not currently running.');
771
+ info('Gateway is not running');
474
772
  }
773
+ console.log();
475
774
  break;
775
+ }
776
+
777
+ // ╔══════════════════════════════════════════════════════════════╗
778
+ // ║ 🔄 RESTART ║
779
+ // ╚══════════════════════════════════════════════════════════════╝
780
+ case 'restart': {
781
+ console.log();
782
+ const spin = new Spinner('Restarting SamarthyaBot...');
783
+ spin.start();
476
784
 
477
- case 'restart':
478
- console.log('🔄 Restarting SamarthyaBot Gateway...');
479
785
  if (isServerRunning()) {
480
786
  try {
481
787
  if (process.platform === 'win32') {
482
788
  const netstatOut = execSync('netstat -ano | findstr :5000', { encoding: 'utf-8' });
483
- const lines = netstatOut.split('\\n');
484
- for (const line of lines) {
789
+ netstatOut.split('\n').forEach(line => {
485
790
  if (line.includes('LISTENING')) {
486
- const parts = line.trim().split(/\\s+/);
791
+ const parts = line.trim().split(/\s+/);
487
792
  const pid = parts[parts.length - 1];
488
- if (pid && pid !== '0') {
489
- execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
490
- }
793
+ if (pid && pid !== '0') execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
491
794
  }
492
- }
795
+ });
493
796
  } else {
494
797
  execSync('lsof -t -i:5000 | xargs kill -9 2>/dev/null || fuser -k 5000/tcp 2>/dev/null');
495
798
  }
496
- } catch (e) { /* ignore */ }
799
+ } catch { }
497
800
  }
498
- // Give it a moment to free the port
801
+
499
802
  setTimeout(() => {
803
+ spin.stop('Restarted');
500
804
  const restartChild = spawn('node', ['server.js'], {
501
805
  cwd: backendDir,
502
806
  stdio: 'inherit'
503
807
  });
504
808
  restartChild.on('close', (code) => {
505
- console.log(`Gateway exited with code ${code}`);
809
+ console.log(`\n${c.red}Gateway exited with code ${code}${c.reset}`);
506
810
  });
507
- console.log('✅ Gateway restarted successfully!');
508
- console.log('🌐 Dashboard: http://localhost:5000\n');
509
- }, 1000);
811
+ info(`Dashboard: ${c.cyan}${c.under}http://localhost:5000${c.reset}\n`);
812
+ }, 1200);
510
813
  break;
814
+ }
511
815
 
512
816
  default:
513
- console.log(`❌ Unknown command: ${command}`);
514
- 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`);
515
819
  process.exit(1);
516
820
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "samarthya-agent",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "main": "server.js",
5
5
  "bin": {
6
6
  "samarthya": "./bin/samarthya.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "samarthya-bot",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Privacy-First Local Agentic OS & Command Center",
5
5
  "main": "backend/server.js",
6
6
  "bin": {