apexbot 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,838 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * ApexBot CLI - Command line interface
5
+ * Inspired by Clawdbot's elegant CLI design
6
+ * 100% FREE with Ollama (local AI)
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ require("dotenv/config");
43
+ const commander_1 = require("commander");
44
+ const gateway_1 = require("../gateway");
45
+ const telegram_1 = require("../channels/telegram");
46
+ const fs = __importStar(require("fs"));
47
+ const path = __importStar(require("path"));
48
+ // Import CLI utilities (CommonJS compatible versions)
49
+ const chalk = require('chalk');
50
+ const boxen = require('boxen');
51
+ const ora = require('ora');
52
+ const inquirer = require('inquirer');
53
+ const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.apexbot');
54
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
55
+ const VERSION = '2026.1.27-1';
56
+ // ═══════════════════════════════════════════════════════════════════
57
+ // ASCII Art Mascot - Fox (Apex predator, clever, open-source friendly)
58
+ // ═══════════════════════════════════════════════════════════════════
59
+ const MASCOT = `
60
+ /\\ /\\
61
+ //\\\\_//\\\\ ____
62
+ \\_ _/ / /
63
+ / * * \\ /^^^]
64
+ \\_\\O/_/ [ ]
65
+ / \\_ [ /
66
+ \\ \\_ / /
67
+ [ [ / \\/ _/
68
+ _[ [ \\ /_/
69
+ `;
70
+ const LOGO = `
71
+ █████╗ ██████╗ ███████╗██╗ ██╗██████╗ ██████╗ ████████╗
72
+ ██╔══██╗██╔══██╗██╔════╝╚██╗██╔╝██╔══██╗██╔═══██╗╚══██╔══╝
73
+ ███████║██████╔╝█████╗ ╚███╔╝ ██████╔╝██║ ██║ ██║
74
+ ██╔══██║██╔═══╝ ██╔══╝ ██╔██╗ ██╔══██╗██║ ██║ ██║
75
+ ██║ ██║██║ ███████╗██╔╝ ██╗██████╔╝╚██████╔╝ ██║
76
+ ╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝
77
+ `;
78
+ const MINI_LOGO = `
79
+ _ ____ _______ ______ ___ _____
80
+ / \\ | _ \\| ____\\ \\/ / __ ) / _ \\_ _|
81
+ / _ \\ | |_) | _| \\ /| _ \\| | | || |
82
+ / ___ \\| __/| |___ / \\| |_) | |_| || |
83
+ /_/ \\_\\_| |_____/_/\\_\\____/ \\___/ |_|
84
+ `;
85
+ // ═══════════════════════════════════════════════════════════════════
86
+ // Helper Functions
87
+ // ═══════════════════════════════════════════════════════════════════
88
+ function showBanner() {
89
+ console.clear();
90
+ console.log(chalk.cyan(LOGO));
91
+ console.log(chalk.gray(` ψ ApexBot ${VERSION} — Your free, private AI assistant ψ`));
92
+ console.log('');
93
+ }
94
+ function showMascot() {
95
+ console.log(chalk.yellow(MASCOT));
96
+ }
97
+ function showSection(title, content) {
98
+ console.log('');
99
+ console.log(chalk.yellow(`● ${title}`));
100
+ console.log('');
101
+ console.log(content);
102
+ }
103
+ function showBox(content, title) {
104
+ console.log(boxen(content, {
105
+ padding: 1,
106
+ margin: 1,
107
+ borderStyle: 'round',
108
+ borderColor: 'cyan',
109
+ title: title,
110
+ titleAlignment: 'center',
111
+ }));
112
+ }
113
+ function loadConfig() {
114
+ try {
115
+ if (fs.existsSync(CONFIG_FILE)) {
116
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
117
+ }
118
+ }
119
+ catch (e) {
120
+ // ignore
121
+ }
122
+ return {};
123
+ }
124
+ function ensureConfigDir() {
125
+ if (!fs.existsSync(CONFIG_DIR)) {
126
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
127
+ }
128
+ }
129
+ function saveConfig(config) {
130
+ ensureConfigDir();
131
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
132
+ }
133
+ // ═══════════════════════════════════════════════════════════════════
134
+ // Ollama Detection
135
+ // ═══════════════════════════════════════════════════════════════════
136
+ async function checkOllama(url = 'http://localhost:11434') {
137
+ try {
138
+ const res = await fetch(`${url}/api/tags`);
139
+ if (res.ok) {
140
+ const data = await res.json();
141
+ const models = (data.models || []).map((m) => m.name);
142
+ return { running: true, models };
143
+ }
144
+ }
145
+ catch (e) {
146
+ // not running
147
+ }
148
+ return { running: false, models: [] };
149
+ }
150
+ // ═══════════════════════════════════════════════════════════════════
151
+ // Main CLI Program
152
+ // ═══════════════════════════════════════════════════════════════════
153
+ const program = new commander_1.Command();
154
+ program
155
+ .name('apexbot')
156
+ .description('ApexBot - Your free, private AI assistant powered by Ollama')
157
+ .version(VERSION);
158
+ // ─────────────────────────────────────────────────────────────────
159
+ // ONBOARD Command - Interactive Setup (like Clawdbot)
160
+ // ─────────────────────────────────────────────────────────────────
161
+ program
162
+ .command('onboard')
163
+ .alias('setup')
164
+ .alias('init')
165
+ .description('Interactive setup wizard')
166
+ .action(async () => {
167
+ showBanner();
168
+ showMascot();
169
+ console.log(chalk.green(' ψ FRESH DAILY ψ'));
170
+ console.log(chalk.cyan(' ApexBot onboarding'));
171
+ console.log('');
172
+ // ══════════════════════════════════════════════════════════════
173
+ // Security Section (like Clawdbot)
174
+ // ══════════════════════════════════════════════════════════════
175
+ showSection('Security', `${chalk.gray('Please read:')} ${chalk.blue('https://github.com/apexbot/docs/security')}
176
+
177
+ ApexBot agents can run commands, read/write files, and act through any tools you
178
+ enable. They can only send messages on channels you configure (for example, an account
179
+ you log in on this machine, or a bot account like Telegram/Discord).
180
+
181
+ ${chalk.yellow("If you're new to this, start with Ollama and least privilege.")} It helps limit what
182
+ an agent can do if it's tricked or makes a mistake.
183
+
184
+ ${chalk.cyan('✨ Good news:')} ApexBot uses ${chalk.green('Ollama (local AI)')} by default — your data never leaves
185
+ your computer. No cloud APIs, no tracking, 100% private and FREE.`);
186
+ console.log('');
187
+ const { continueSetup } = await inquirer.prompt([{
188
+ type: 'confirm',
189
+ name: 'continueSetup',
190
+ message: chalk.yellow('I understand this is powerful and inherently risky. Continue?'),
191
+ default: false,
192
+ }]);
193
+ if (!continueSetup) {
194
+ console.log('');
195
+ console.log(chalk.gray('Setup cancelled. Run `apexbot onboard` when ready.'));
196
+ process.exit(0);
197
+ }
198
+ console.log('');
199
+ console.log(chalk.gray('─'.repeat(60)));
200
+ // ══════════════════════════════════════════════════════════════
201
+ // Step 1: AI Provider (Ollama is default)
202
+ // ══════════════════════════════════════════════════════════════
203
+ showSection('Step 1: AI Provider', `${chalk.green('🌟 RECOMMENDED:')} Ollama (Local AI - 100% FREE, Private, No API Keys!)
204
+
205
+ Ollama runs AI models locally on your machine. Your conversations stay private.
206
+ No internet required after initial model download.`);
207
+ const { provider } = await inquirer.prompt([{
208
+ type: 'list',
209
+ name: 'provider',
210
+ message: 'Choose AI provider:',
211
+ choices: [
212
+ { name: `${chalk.green('⭐')} Ollama (LOCAL - 100% FREE) ${chalk.gray('[RECOMMENDED]')}`, value: 'ollama' },
213
+ new inquirer.Separator(),
214
+ { name: ` Kimi K2.5 (Cloud - requires API key)`, value: 'kimi' },
215
+ { name: ` Google Gemini (Cloud - requires API key)`, value: 'google' },
216
+ { name: ` Anthropic Claude (Cloud - requires API key)`, value: 'anthropic' },
217
+ { name: ` OpenAI GPT (Cloud - requires API key)`, value: 'openai' },
218
+ ],
219
+ default: 'ollama',
220
+ }]);
221
+ const config = { agent: { provider }, channels: {}, gateway: {} };
222
+ if (provider === 'ollama') {
223
+ // Check if Ollama is running
224
+ const spinner = ora('Checking Ollama connection...').start();
225
+ const ollamaUrl = 'http://localhost:11434';
226
+ const ollama = await checkOllama(ollamaUrl);
227
+ if (ollama.running) {
228
+ spinner.succeed(chalk.green('Ollama is running!'));
229
+ config.agent.apiUrl = ollamaUrl;
230
+ if (ollama.models.length > 0) {
231
+ console.log('');
232
+ console.log(chalk.cyan('Available models:'));
233
+ ollama.models.forEach((m, i) => console.log(chalk.gray(` ${i + 1}. ${m}`)));
234
+ console.log('');
235
+ const { model } = await inquirer.prompt([{
236
+ type: 'list',
237
+ name: 'model',
238
+ message: 'Choose model:',
239
+ choices: [
240
+ ...ollama.models.map(m => ({ name: m, value: m })),
241
+ new inquirer.Separator(),
242
+ { name: chalk.gray('Enter custom model name...'), value: '_custom' },
243
+ ],
244
+ }]);
245
+ if (model === '_custom') {
246
+ const { customModel } = await inquirer.prompt([{
247
+ type: 'input',
248
+ name: 'customModel',
249
+ message: 'Model name:',
250
+ default: 'llama3.2',
251
+ }]);
252
+ config.agent.model = customModel;
253
+ }
254
+ else {
255
+ config.agent.model = model;
256
+ }
257
+ }
258
+ else {
259
+ spinner.warn(chalk.yellow('No models found. You need to pull a model first.'));
260
+ console.log('');
261
+ console.log(chalk.cyan('Recommended models for ApexBot:'));
262
+ console.log(chalk.gray(' • llama3.2 - Fast, good for chat (3.2B params)'));
263
+ console.log(chalk.gray(' • llama3.1:8b - Better quality (8B params)'));
264
+ console.log(chalk.gray(' • qwen2.5:14b - Excellent multilingual (14B params)'));
265
+ console.log(chalk.gray(' • codellama - Best for coding tasks'));
266
+ console.log('');
267
+ console.log(chalk.yellow('Run: ollama pull llama3.2'));
268
+ console.log('');
269
+ const { model } = await inquirer.prompt([{
270
+ type: 'input',
271
+ name: 'model',
272
+ message: 'Model to use (will be pulled if needed):',
273
+ default: 'llama3.2',
274
+ }]);
275
+ config.agent.model = model;
276
+ }
277
+ }
278
+ else {
279
+ spinner.fail(chalk.red('Ollama is not running!'));
280
+ console.log('');
281
+ showBox(`${chalk.yellow('Ollama is not running or not installed.')}
282
+
283
+ To install Ollama:
284
+ ${chalk.cyan('Windows:')} Download from https://ollama.com
285
+ ${chalk.cyan('macOS:')} brew install ollama
286
+ ${chalk.cyan('Linux:')} curl -fsSL https://ollama.com/install.sh | sh
287
+
288
+ Then start Ollama:
289
+ ${chalk.green('ollama serve')}
290
+
291
+ And pull a model:
292
+ ${chalk.green('ollama pull llama3.2')}`, 'Installation Guide');
293
+ const { continueAnyway } = await inquirer.prompt([{
294
+ type: 'confirm',
295
+ name: 'continueAnyway',
296
+ message: 'Continue setup anyway? (You can start Ollama later)',
297
+ default: true,
298
+ }]);
299
+ if (!continueAnyway) {
300
+ console.log(chalk.gray('Run `apexbot onboard` after starting Ollama.'));
301
+ process.exit(0);
302
+ }
303
+ config.agent.apiUrl = ollamaUrl;
304
+ const { model } = await inquirer.prompt([{
305
+ type: 'input',
306
+ name: 'model',
307
+ message: 'Model to use:',
308
+ default: 'llama3.2',
309
+ }]);
310
+ config.agent.model = model;
311
+ }
312
+ }
313
+ else {
314
+ // Cloud provider - need API key
315
+ const { apiKey } = await inquirer.prompt([{
316
+ type: 'password',
317
+ name: 'apiKey',
318
+ message: `Enter ${provider.toUpperCase()} API key:`,
319
+ mask: '*',
320
+ }]);
321
+ config.agent.apiKey = apiKey;
322
+ if (provider === 'kimi') {
323
+ config.agent.apiUrl = 'https://api.moonshot.cn/v1';
324
+ config.agent.model = 'kimi-k2-5';
325
+ }
326
+ else if (provider === 'google') {
327
+ config.agent.model = 'gemini-2.0-flash';
328
+ }
329
+ }
330
+ console.log('');
331
+ console.log(chalk.gray('─'.repeat(60)));
332
+ // ══════════════════════════════════════════════════════════════
333
+ // Step 2: Channels
334
+ // ══════════════════════════════════════════════════════════════
335
+ showSection('Step 2: Channels', `ApexBot can connect to multiple messaging platforms.
336
+ Configure at least one channel to start chatting.`);
337
+ const { channels } = await inquirer.prompt([{
338
+ type: 'checkbox',
339
+ name: 'channels',
340
+ message: 'Select channels to configure (space to toggle, enter to confirm):',
341
+ choices: [
342
+ { name: '📱 Telegram', value: 'telegram' },
343
+ { name: '🎮 Discord', value: 'discord' },
344
+ { name: '🌐 WebChat (built-in)', value: 'webchat' },
345
+ ],
346
+ validate: (input) => {
347
+ if (input.length === 0) {
348
+ return 'Please select at least one channel (use spacebar to select)';
349
+ }
350
+ return true;
351
+ },
352
+ }]);
353
+ if (channels.includes('telegram')) {
354
+ console.log('');
355
+ console.log(chalk.cyan('Telegram Setup:'));
356
+ console.log(chalk.gray('Get a bot token from @BotFather on Telegram'));
357
+ console.log('');
358
+ const { telegramToken } = await inquirer.prompt([{
359
+ type: 'input',
360
+ name: 'telegramToken',
361
+ message: 'Telegram Bot Token:',
362
+ }]);
363
+ if (telegramToken) {
364
+ config.channels.telegram = { botToken: telegramToken };
365
+ const { allowedUsers } = await inquirer.prompt([{
366
+ type: 'input',
367
+ name: 'allowedUsers',
368
+ message: 'Allowed user IDs (comma-separated, or * for all):',
369
+ default: '*',
370
+ }]);
371
+ if (allowedUsers && allowedUsers !== '*') {
372
+ config.channels.telegram.allowedUsers = allowedUsers.split(',').map((s) => parseInt(s.trim(), 10));
373
+ }
374
+ }
375
+ }
376
+ if (channels.includes('discord')) {
377
+ console.log('');
378
+ console.log(chalk.cyan('Discord Setup:'));
379
+ console.log(chalk.gray('Get a bot token from Discord Developer Portal'));
380
+ console.log('');
381
+ const { discordToken } = await inquirer.prompt([{
382
+ type: 'input',
383
+ name: 'discordToken',
384
+ message: 'Discord Bot Token:',
385
+ }]);
386
+ if (discordToken) {
387
+ config.channels.discord = { botToken: discordToken };
388
+ }
389
+ }
390
+ console.log('');
391
+ console.log(chalk.gray('─'.repeat(60)));
392
+ // ══════════════════════════════════════════════════════════════
393
+ // Step 3: Gateway Settings
394
+ // ══════════════════════════════════════════════════════════════
395
+ showSection('Step 3: Gateway', `The gateway is the central hub for ApexBot.
396
+ WebChat UI will be available at http://localhost:<port>/chat`);
397
+ const { port } = await inquirer.prompt([{
398
+ type: 'input',
399
+ name: 'port',
400
+ message: 'Gateway port:',
401
+ default: '18789',
402
+ validate: (input) => {
403
+ const num = parseInt(input, 10);
404
+ return num > 0 && num < 65536 ? true : 'Please enter a valid port number';
405
+ },
406
+ }]);
407
+ config.gateway.port = parseInt(port, 10);
408
+ config.gateway.token = Math.random().toString(36).substring(2, 15);
409
+ // ══════════════════════════════════════════════════════════════
410
+ // Save Config
411
+ // ══════════════════════════════════════════════════════════════
412
+ saveConfig(config);
413
+ console.log('');
414
+ console.log(chalk.gray('─'.repeat(60)));
415
+ console.log('');
416
+ showBox(`${chalk.green('✅ Configuration saved!')}
417
+
418
+ Config file: ${chalk.cyan(CONFIG_FILE)}`, '🎉 Setup Complete!');
419
+ console.log('');
420
+ // Ask to start gateway immediately
421
+ const { startNow } = await inquirer.prompt([{
422
+ type: 'confirm',
423
+ name: 'startNow',
424
+ message: chalk.yellow('🚀 Start ApexBot gateway now?'),
425
+ default: true,
426
+ }]);
427
+ if (startNow) {
428
+ console.log('');
429
+ console.log(chalk.green('Starting gateway...'));
430
+ console.log('');
431
+ // Import and start gateway directly
432
+ await startGatewayServer(config, {});
433
+ }
434
+ else {
435
+ console.log('');
436
+ console.log(chalk.gray('To start later, run:'));
437
+ console.log(chalk.green(' npx ts-node src/cli/index.ts gateway'));
438
+ console.log('');
439
+ }
440
+ });
441
+ // ─────────────────────────────────────────────────────────────────
442
+ // Gateway Start Function (reusable)
443
+ // ─────────────────────────────────────────────────────────────────
444
+ async function startGatewayServer(config, options = {}) {
445
+ console.log('');
446
+ console.log(chalk.cyan(MINI_LOGO));
447
+ console.log(chalk.gray(` v${VERSION} — Starting gateway...`));
448
+ console.log('');
449
+ const port = parseInt(options.port || config.gateway?.port || '18789', 10);
450
+ const gateway = new gateway_1.Gateway({
451
+ port,
452
+ host: '127.0.0.1',
453
+ token: config.gateway?.token,
454
+ verbose: options.verbose,
455
+ });
456
+ // Configure agent
457
+ gateway.agents.configure(config.agent);
458
+ // Register channels
459
+ if (config.channels?.telegram?.botToken) {
460
+ const telegram = new telegram_1.TelegramChannel({
461
+ botToken: config.channels.telegram.botToken,
462
+ allowedUsers: config.channels.telegram.allowedUsers,
463
+ });
464
+ gateway.channels.register(telegram);
465
+ }
466
+ // Start gateway
467
+ await gateway.start();
468
+ await gateway.channels.connectAll();
469
+ console.log('');
470
+ console.log(chalk.green('✅ ApexBot is running!'));
471
+ console.log('');
472
+ console.log(` ${chalk.cyan('Dashboard:')} http://127.0.0.1:${port}/chat`);
473
+ console.log(` ${chalk.cyan('API:')} http://127.0.0.1:${port}`);
474
+ console.log(` ${chalk.cyan('WebSocket:')} ws://127.0.0.1:${port}`);
475
+ console.log('');
476
+ console.log(chalk.gray('Press Ctrl+C to stop.'));
477
+ console.log('');
478
+ // Handle shutdown
479
+ process.on('SIGINT', async () => {
480
+ console.log('');
481
+ console.log(chalk.yellow('🛑 Shutting down...'));
482
+ await gateway.stop();
483
+ process.exit(0);
484
+ });
485
+ }
486
+ // ─────────────────────────────────────────────────────────────────
487
+ // GATEWAY Command
488
+ // ─────────────────────────────────────────────────────────────────
489
+ program
490
+ .command('gateway')
491
+ .alias('start')
492
+ .alias('run')
493
+ .description('Start the ApexBot gateway')
494
+ .option('-p, --port <port>', 'Gateway port')
495
+ .option('-v, --verbose', 'Verbose logging')
496
+ .action(async (options) => {
497
+ const config = loadConfig();
498
+ if (!config.agent?.provider) {
499
+ console.log('');
500
+ console.log(chalk.yellow('⚠️ ApexBot is not configured yet.'));
501
+ console.log('');
502
+ console.log(`Run ${chalk.green('apexbot onboard')} to set up your bot.`);
503
+ console.log('');
504
+ process.exit(1);
505
+ }
506
+ await startGatewayServer(config, options);
507
+ });
508
+ // ─────────────────────────────────────────────────────────────────
509
+ // STATUS Command
510
+ // ─────────────────────────────────────────────────────────────────
511
+ program
512
+ .command('status')
513
+ .description('Show ApexBot status')
514
+ .action(async () => {
515
+ const config = loadConfig();
516
+ console.log('');
517
+ console.log(chalk.cyan(MINI_LOGO));
518
+ console.log('');
519
+ const spinner = ora('Checking status...').start();
520
+ // Check config
521
+ const hasConfig = fs.existsSync(CONFIG_FILE);
522
+ // Check Ollama
523
+ let ollamaStatus = '❌ Not checked';
524
+ if (config.agent?.provider === 'ollama') {
525
+ const ollama = await checkOllama(config.agent.apiUrl || 'http://localhost:11434');
526
+ ollamaStatus = ollama.running
527
+ ? chalk.green(`✅ Running (${ollama.models.length} models)`)
528
+ : chalk.red('❌ Not running');
529
+ }
530
+ // Check gateway
531
+ let gatewayStatus = chalk.gray('Not running');
532
+ try {
533
+ const port = config.gateway?.port || 18789;
534
+ const res = await fetch(`http://127.0.0.1:${port}/health`);
535
+ if (res.ok) {
536
+ const data = await res.json();
537
+ gatewayStatus = chalk.green(`✅ Running (${data.sessions} sessions)`);
538
+ }
539
+ }
540
+ catch (e) {
541
+ gatewayStatus = chalk.gray('Not running');
542
+ }
543
+ spinner.stop();
544
+ console.log(chalk.cyan('Status:'));
545
+ console.log('');
546
+ console.log(` Config: ${hasConfig ? chalk.green('✅ ' + CONFIG_FILE) : chalk.yellow('⚠️ Not configured')}`);
547
+ console.log(` Provider: ${config.agent?.provider || chalk.gray('None')}`);
548
+ console.log(` Model: ${config.agent?.model || chalk.gray('None')}`);
549
+ console.log(` Ollama: ${ollamaStatus}`);
550
+ console.log(` Gateway: ${gatewayStatus}`);
551
+ console.log(` Channels: ${Object.keys(config.channels || {}).join(', ') || chalk.gray('None')}`);
552
+ console.log('');
553
+ if (!hasConfig) {
554
+ console.log(chalk.yellow(`Run ${chalk.green('apexbot onboard')} to set up your bot.`));
555
+ console.log('');
556
+ }
557
+ });
558
+ // ─────────────────────────────────────────────────────────────────
559
+ // CONFIG Command
560
+ // ─────────────────────────────────────────────────────────────────
561
+ program
562
+ .command('config')
563
+ .description('Show or edit configuration')
564
+ .option('--show', 'Show current config')
565
+ .option('--reset', 'Reset configuration')
566
+ .action(async (options) => {
567
+ if (options.reset) {
568
+ const { confirm } = await inquirer.prompt([{
569
+ type: 'confirm',
570
+ name: 'confirm',
571
+ message: chalk.red('Are you sure you want to reset all configuration?'),
572
+ default: false,
573
+ }]);
574
+ if (confirm) {
575
+ if (fs.existsSync(CONFIG_FILE)) {
576
+ fs.unlinkSync(CONFIG_FILE);
577
+ }
578
+ console.log(chalk.green('✅ Configuration reset.'));
579
+ console.log(`Run ${chalk.cyan('apexbot onboard')} to set up again.`);
580
+ }
581
+ return;
582
+ }
583
+ const config = loadConfig();
584
+ console.log('');
585
+ console.log(chalk.cyan('Current configuration:'));
586
+ console.log('');
587
+ console.log(chalk.gray(CONFIG_FILE));
588
+ console.log('');
589
+ // Hide sensitive data
590
+ const safeConfig = JSON.parse(JSON.stringify(config));
591
+ if (safeConfig.agent?.apiKey) {
592
+ safeConfig.agent.apiKey = '***hidden***';
593
+ }
594
+ if (safeConfig.channels?.telegram?.botToken) {
595
+ safeConfig.channels.telegram.botToken = '***hidden***';
596
+ }
597
+ if (safeConfig.channels?.discord?.botToken) {
598
+ safeConfig.channels.discord.botToken = '***hidden***';
599
+ }
600
+ console.log(JSON.stringify(safeConfig, null, 2));
601
+ console.log('');
602
+ });
603
+ // ─────────────────────────────────────────────────────────────────
604
+ // MODELS Command - List/pull Ollama models
605
+ // ─────────────────────────────────────────────────────────────────
606
+ program
607
+ .command('models')
608
+ .description('List available Ollama models')
609
+ .option('--pull <model>', 'Pull a new model')
610
+ .action(async (options) => {
611
+ const config = loadConfig();
612
+ const ollamaUrl = config.agent?.apiUrl || 'http://localhost:11434';
613
+ if (options.pull) {
614
+ console.log('');
615
+ console.log(chalk.cyan(`Pulling model: ${options.pull}`));
616
+ console.log(chalk.gray('This may take a while...'));
617
+ console.log('');
618
+ // Use exec to run ollama pull
619
+ const { exec } = require('child_process');
620
+ exec(`ollama pull ${options.pull}`, (error, stdout, stderr) => {
621
+ if (error) {
622
+ console.log(chalk.red(`Error: ${error.message}`));
623
+ return;
624
+ }
625
+ console.log(stdout);
626
+ if (stderr)
627
+ console.log(stderr);
628
+ console.log(chalk.green('✅ Model pulled successfully!'));
629
+ });
630
+ return;
631
+ }
632
+ const spinner = ora('Fetching models...').start();
633
+ const ollama = await checkOllama(ollamaUrl);
634
+ spinner.stop();
635
+ if (!ollama.running) {
636
+ console.log('');
637
+ console.log(chalk.red('❌ Ollama is not running.'));
638
+ console.log(chalk.gray('Start it with: ollama serve'));
639
+ console.log('');
640
+ return;
641
+ }
642
+ console.log('');
643
+ console.log(chalk.cyan('Available models:'));
644
+ console.log('');
645
+ if (ollama.models.length === 0) {
646
+ console.log(chalk.gray(' No models installed.'));
647
+ console.log('');
648
+ console.log(chalk.yellow('Recommended models:'));
649
+ console.log(chalk.gray(' • ollama pull llama3.2 - Fast, good for chat'));
650
+ console.log(chalk.gray(' • ollama pull qwen2.5:14b - Excellent multilingual'));
651
+ console.log(chalk.gray(' • ollama pull codellama - Best for coding'));
652
+ }
653
+ else {
654
+ ollama.models.forEach((m, i) => {
655
+ const current = m === config.agent?.model ? chalk.green(' ← current') : '';
656
+ console.log(` ${i + 1}. ${m}${current}`);
657
+ });
658
+ }
659
+ console.log('');
660
+ });
661
+ // ─────────────────────────────────────────────────────────────────
662
+ // DAEMON Command - Manage ApexBot daemon (like clawdbot daemon)
663
+ // ─────────────────────────────────────────────────────────────────
664
+ program
665
+ .command('daemon')
666
+ .description('Manage ApexBot daemon')
667
+ .argument('<action>', 'start, stop, restart, or status')
668
+ .action(async (action) => {
669
+ const config = loadConfig();
670
+ const pidFile = path.join(CONFIG_DIR, 'daemon.pid');
671
+ const logFile = path.join(CONFIG_DIR, 'daemon.log');
672
+ switch (action) {
673
+ case 'start': {
674
+ // Check if already running
675
+ if (fs.existsSync(pidFile)) {
676
+ const oldPid = fs.readFileSync(pidFile, 'utf8').trim();
677
+ try {
678
+ process.kill(parseInt(oldPid), 0);
679
+ console.log('');
680
+ console.log(chalk.yellow(`⚠️ Daemon already running (PID: ${oldPid})`));
681
+ console.log(chalk.gray(`Use 'apexbot daemon restart' to restart.`));
682
+ console.log('');
683
+ return;
684
+ }
685
+ catch (e) {
686
+ // Process not running, clean up stale PID file
687
+ fs.unlinkSync(pidFile);
688
+ }
689
+ }
690
+ console.log('');
691
+ console.log(chalk.cyan('🚀 Starting ApexBot daemon...'));
692
+ // Spawn detached process (cross-platform)
693
+ const { spawn } = require('child_process');
694
+ const out = fs.openSync(logFile, 'a');
695
+ const err = fs.openSync(logFile, 'a');
696
+ const isWindows = process.platform === 'win32';
697
+ const child = spawn(isWindows ? 'cmd.exe' : 'npx', isWindows
698
+ ? ['/c', 'npx', 'ts-node', 'src/cli/index.ts', 'gateway']
699
+ : ['ts-node', 'src/cli/index.ts', 'gateway'], {
700
+ cwd: process.cwd(),
701
+ detached: true,
702
+ stdio: ['ignore', out, err],
703
+ shell: false,
704
+ });
705
+ fs.writeFileSync(pidFile, String(child.pid));
706
+ child.unref();
707
+ console.log(chalk.green(`✅ Daemon started (PID: ${child.pid})`));
708
+ console.log('');
709
+ console.log(` ${chalk.cyan('Dashboard:')} http://127.0.0.1:${config.gateway?.port || 18789}/chat`);
710
+ console.log(` ${chalk.cyan('Logs:')} ${logFile}`);
711
+ console.log('');
712
+ console.log(chalk.gray('Use "apexbot daemon status" to check status.'));
713
+ console.log(chalk.gray('Use "apexbot daemon stop" to stop.'));
714
+ console.log('');
715
+ break;
716
+ }
717
+ case 'stop': {
718
+ if (!fs.existsSync(pidFile)) {
719
+ console.log('');
720
+ console.log(chalk.yellow('⚠️ Daemon is not running.'));
721
+ console.log('');
722
+ return;
723
+ }
724
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim());
725
+ console.log('');
726
+ console.log(chalk.cyan(`🛑 Stopping daemon (PID: ${pid})...`));
727
+ try {
728
+ process.kill(pid, 'SIGTERM');
729
+ fs.unlinkSync(pidFile);
730
+ console.log(chalk.green('✅ Daemon stopped.'));
731
+ }
732
+ catch (e) {
733
+ if (e.code === 'ESRCH') {
734
+ fs.unlinkSync(pidFile);
735
+ console.log(chalk.yellow('⚠️ Daemon was not running (stale PID file removed).'));
736
+ }
737
+ else {
738
+ console.log(chalk.red(`❌ Failed to stop daemon: ${e.message}`));
739
+ }
740
+ }
741
+ console.log('');
742
+ break;
743
+ }
744
+ case 'restart': {
745
+ console.log('');
746
+ console.log(chalk.cyan('🔄 Restarting daemon...'));
747
+ // Stop first
748
+ if (fs.existsSync(pidFile)) {
749
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim());
750
+ try {
751
+ process.kill(pid, 'SIGTERM');
752
+ fs.unlinkSync(pidFile);
753
+ await new Promise(r => setTimeout(r, 1000));
754
+ }
755
+ catch (e) {
756
+ // Ignore if already dead
757
+ }
758
+ }
759
+ // Then start
760
+ const { spawn } = require('child_process');
761
+ const out = fs.openSync(logFile, 'a');
762
+ const err = fs.openSync(logFile, 'a');
763
+ const isWindows = process.platform === 'win32';
764
+ const child = spawn(isWindows ? 'cmd.exe' : 'npx', isWindows
765
+ ? ['/c', 'npx', 'ts-node', 'src/cli/index.ts', 'gateway']
766
+ : ['ts-node', 'src/cli/index.ts', 'gateway'], {
767
+ cwd: process.cwd(),
768
+ detached: true,
769
+ stdio: ['ignore', out, err],
770
+ shell: false,
771
+ });
772
+ fs.writeFileSync(pidFile, String(child.pid));
773
+ child.unref();
774
+ console.log(chalk.green(`✅ Daemon restarted (PID: ${child.pid})`));
775
+ console.log('');
776
+ break;
777
+ }
778
+ case 'status': {
779
+ console.log('');
780
+ console.log(chalk.cyan(MINI_LOGO));
781
+ console.log('');
782
+ if (!fs.existsSync(pidFile)) {
783
+ console.log(chalk.yellow('⚠️ Daemon is not running.'));
784
+ console.log('');
785
+ console.log(chalk.gray('Start with: apexbot daemon start'));
786
+ console.log('');
787
+ return;
788
+ }
789
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim());
790
+ let isRunning = false;
791
+ try {
792
+ process.kill(pid, 0);
793
+ isRunning = true;
794
+ }
795
+ catch (e) {
796
+ isRunning = false;
797
+ }
798
+ if (isRunning) {
799
+ console.log(chalk.green(`✅ Daemon is running (PID: ${pid})`));
800
+ console.log('');
801
+ // Try to get status from gateway
802
+ try {
803
+ const port = config.gateway?.port || 18789;
804
+ const res = await fetch(`http://127.0.0.1:${port}/health`);
805
+ const data = await res.json();
806
+ console.log(` ${chalk.cyan('Uptime:')} ${Math.floor(data.uptime)}s`);
807
+ console.log(` ${chalk.cyan('Sessions:')} ${data.sessions}`);
808
+ console.log(` ${chalk.cyan('Dashboard:')} http://127.0.0.1:${port}/chat`);
809
+ }
810
+ catch (e) {
811
+ console.log(chalk.gray(' (Gateway not responding yet)'));
812
+ }
813
+ }
814
+ else {
815
+ console.log(chalk.yellow('⚠️ Daemon process died (cleaning up PID file).'));
816
+ fs.unlinkSync(pidFile);
817
+ }
818
+ console.log('');
819
+ break;
820
+ }
821
+ default:
822
+ console.log('');
823
+ console.log(chalk.red(`Unknown action: ${action}`));
824
+ console.log('');
825
+ console.log('Usage: apexbot daemon <start|stop|restart|status>');
826
+ console.log('');
827
+ }
828
+ });
829
+ // ─────────────────────────────────────────────────────────────────
830
+ // Default action - show help with banner
831
+ // ─────────────────────────────────────────────────────────────────
832
+ program
833
+ .action(() => {
834
+ showBanner();
835
+ showMascot();
836
+ program.help();
837
+ });
838
+ program.parse();