apexbot 1.0.8 → 1.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.
package/README.md CHANGED
@@ -39,20 +39,32 @@
39
39
 
40
40
  ### 🧠 Brain System
41
41
  - Personality profiles (SOUL files)
42
- - Long-term memory with vector store
42
+ - **Long-term memory** with LanceDB vector store
43
+ - **Auto-recall** - relevant memories injected into context
44
+ - **Auto-capture** - preferences and decisions saved automatically
43
45
  - Context-aware responses
44
46
  - Lobster Engine for reasoning
45
47
 
48
+ ### 🔄 Model Fallback Chain
49
+ - **Automatic failover** between AI providers
50
+ - Tries: Ollama → Google → Anthropic
51
+ - No manual intervention needed
52
+
46
53
  ### 🛠️ 30+ Skills
47
54
 
48
55
  | Category | Skills |
49
56
  |----------|--------|
50
57
  | **Communication** | Telegram, WhatsApp, Discord, Slack, Signal, iMessage, Matrix, Twitter, Gmail |
51
- | **Productivity** | Obsidian, Notion, Trello, GitHub, Calendar, Reminders, Apple Notes |
58
+ | **Productivity** | Obsidian, Notion, Trello, GitHub, Calendar, Reminders, Apple Notes, **Memory** |
52
59
  | **Media** | Spotify, Sonos, Voice/TTS, Image Gen, GIF Search |
53
60
  | **Smart Home** | Home Assistant, Philips Hue |
54
61
  | **Automation** | Browser, Cron, 1Password, Weather, Search, System |
55
62
 
63
+ ### ⚡ Moltbot-Style Enhancements
64
+ - **Rate limiting** via `@grammyjs/transformer-throttler`
65
+ - **Typing indicators** - shows "typing..." while processing
66
+ - **Memory tools** - `memory_recall`, `memory_store`, `memory_forget`
67
+
56
68
  ### ⌨️ Claude Code Style CLI
57
69
 
58
70
  **Input Prefixes:**
@@ -165,12 +177,23 @@ Config file: `~/.apexbot/config.json`
165
177
 
166
178
  | Provider | Cost | Notes |
167
179
  |----------|------|-------|
168
- | **Ollama** | Free | Local, recommended |
169
- | Google Gemini | Free tier | Cloud API |
170
- | Anthropic Claude | Paid | Cloud API |
180
+ | **Ollama** | Free | Local, recommended, default |
181
+ | Google Gemini | Free tier | Cloud API, fallback #1 |
182
+ | Anthropic Claude | Paid | Cloud API, fallback #2 |
171
183
  | OpenAI GPT | Paid | Cloud API |
172
184
  | Kimi | Free tier | Cloud API |
173
185
 
186
+ ### Memory Plugin (Optional)
187
+
188
+ ```env
189
+ # Required for memory_recall/store/forget
190
+ OPENAI_API_KEY=sk-...
191
+
192
+ # Optional
193
+ MEMORY_EMBEDDING_MODEL=text-embedding-3-small
194
+ MEMORY_DB_PATH=~/.apexbot/memory/lancedb
195
+ ```
196
+
174
197
  ---
175
198
 
176
199
  ## 🔧 CLI Commands
@@ -253,6 +276,9 @@ MIT License. See [LICENSE](LICENSE).
253
276
 
254
277
  ## 🙏 Acknowledgments
255
278
 
256
- - Inspired by [Clawdbot/Moltbot](https://clawd.bot)
279
+ - **Ported features from** [Clawdbot/Moltbot](https://clawd.bot)
280
+ - Model fallback chain
281
+ - Long-term memory with LanceDB
282
+ - Rate limiting & typing indicators
257
283
  - Powered by [Ollama](https://ollama.com)
258
284
  - Built with TypeScript, grammy, discord.js
@@ -12,10 +12,18 @@ const generative_ai_1 = require("@google/generative-ai");
12
12
  const tools_1 = require("../tools");
13
13
  const brain_1 = require("../brain");
14
14
  const toolExecutor_1 = require("./toolExecutor");
15
+ const debug_1 = require("../core/debug");
15
16
  class AgentManager {
16
17
  config = null;
17
18
  googleClient = null;
18
19
  brain = null;
20
+ // Moltbot-style model fallback chain
21
+ fallbackModels = [
22
+ { provider: 'ollama', model: 'qwen2.5:14b' },
23
+ { provider: 'google', model: 'gemini-2.0-flash' },
24
+ { provider: 'anthropic', model: 'claude-sonnet-4-20250514' },
25
+ ];
26
+ fallbackEnabled = true;
19
27
  defaultSystemPrompt = `You are ApexBot, an autonomous AI assistant like Claude Code. You can execute real actions on the user's computer.
20
28
 
21
29
  ## CORE PRINCIPLES
@@ -58,7 +66,7 @@ You are running locally on the user's machine. No data leaves their computer. Yo
58
66
  */
59
67
  setBrain(brain) {
60
68
  this.brain = brain;
61
- console.log('[Agent] Brain connected');
69
+ (0, debug_1.debug)('Agent', 'Brain connected');
62
70
  }
63
71
  configure(config) {
64
72
  this.config = {
@@ -84,7 +92,7 @@ You are running locally on the user's machine. No data leaves their computer. Yo
84
92
  if (config.provider === 'kimi') {
85
93
  // nothing to initialize here; processWithKimi will use fetch + config.apiUrl
86
94
  }
87
- console.log(`[Agent] Configured with ${config.provider} (${config.model || 'default model'})`);
95
+ (0, debug_1.debug)('Agent', `Configured with ${config.provider} (${config.model || 'default model'})`);
88
96
  }
89
97
  async process(session, message) {
90
98
  if (!this.config) {
@@ -92,7 +100,7 @@ You are running locally on the user's machine. No data leaves their computer. Yo
92
100
  return { text: 'Agent not configured. Please set up an AI provider.' };
93
101
  }
94
102
  const userText = message.text || '';
95
- console.log(`[Agent] Processing message: ${userText.slice(0, 50)}...`);
103
+ (0, debug_1.debug)('Agent', `Processing message: ${userText.slice(0, 50)}...`);
96
104
  // Handle slash commands
97
105
  if (userText.startsWith('/')) {
98
106
  return this.handleCommand(session, userText);
@@ -101,30 +109,44 @@ You are running locally on the user's machine. No data leaves their computer. Yo
101
109
  const history = this.buildHistory(session, userText);
102
110
  try {
103
111
  let response;
104
- switch (this.config.provider) {
105
- case 'ollama':
106
- response = await this.processWithOllama(history);
107
- break;
108
- case 'google':
109
- response = await this.processWithGemini(history);
110
- break;
111
- case 'anthropic':
112
- response = await this.processWithClaude(history);
113
- break;
114
- case 'openai':
115
- response = await this.processWithOpenAI(history);
116
- break;
117
- case 'kimi':
118
- response = await this.processWithKimi(history);
119
- break;
120
- default:
121
- response = { text: 'Unknown AI provider' };
112
+ let lastError = null;
113
+ // Try primary provider first
114
+ try {
115
+ response = await this.processWithProvider(this.config.provider, this.config.model || '', history);
122
116
  }
117
+ catch (primaryError) {
118
+ console.warn(`[Agent] Primary provider ${this.config.provider} failed: ${primaryError.message}`);
119
+ lastError = primaryError;
120
+ // Try fallback providers (Moltbot-style)
121
+ if (this.fallbackEnabled && this.fallbackModels.length > 0) {
122
+ for (const fallback of this.fallbackModels) {
123
+ // Skip if same as primary
124
+ if (fallback.provider === this.config.provider)
125
+ continue;
126
+ try {
127
+ (0, debug_1.debug)('Agent', `Trying fallback: ${fallback.provider}/${fallback.model}`);
128
+ response = await this.processWithProvider(fallback.provider, fallback.model, history);
129
+ (0, debug_1.debug)('Agent', `Fallback ${fallback.provider} succeeded!`);
130
+ lastError = null;
131
+ break;
132
+ }
133
+ catch (fallbackError) {
134
+ console.warn(`[Agent] Fallback ${fallback.provider} failed: ${fallbackError.message}`);
135
+ lastError = fallbackError;
136
+ }
137
+ }
138
+ }
139
+ // If all providers failed
140
+ if (lastError) {
141
+ throw lastError;
142
+ }
143
+ }
144
+ response = response;
123
145
  // Check for tool calls in response
124
146
  if (this.config.enableTools) {
125
147
  const toolCalls = (0, toolExecutor_1.parseToolCalls)(response.text);
126
148
  if (toolCalls.length > 0) {
127
- console.log(`[Agent] Found ${toolCalls.length} tool calls:`, toolCalls.map(t => t.name));
149
+ (0, debug_1.debug)('Agent', `Found ${toolCalls.length} tool calls:`, toolCalls.map(t => t.name));
128
150
  // Build tool context
129
151
  const context = (0, toolExecutor_1.buildToolContext)(session.id, message.userId || message.from || 'unknown', message.channel, { workspaceDir: process.cwd() });
130
152
  // Execute tools
@@ -132,9 +154,9 @@ You are running locally on the user's machine. No data leaves their computer. Yo
132
154
  response.toolCalls = toolResults;
133
155
  // Log results
134
156
  for (const { call, result } of toolResults) {
135
- console.log(`[Agent] Tool ${call.name}: ${result.success ? 'SUCCESS' : 'FAILED'}`);
157
+ (0, debug_1.debug)('Agent', `Tool ${call.name}: ${result.success ? 'SUCCESS' : 'FAILED'}`);
136
158
  if (!result.success)
137
- console.log(`[Agent] Error: ${result.error}`);
159
+ (0, debug_1.debug)('Agent', `Error: ${result.error}`);
138
160
  }
139
161
  // Format results and continue conversation
140
162
  const resultsText = (0, toolExecutor_1.formatToolResults)(toolResults);
@@ -172,7 +194,7 @@ You are running locally on the user's machine. No data leaves their computer. Yo
172
194
  }
173
195
  }
174
196
  }
175
- console.log(`[Agent] Generated response: ${response.text.slice(0, 50)}...`);
197
+ (0, debug_1.debug)('Agent', `Generated response: ${response.text.slice(0, 50)}...`);
176
198
  // Save to session (initialize messages array if needed)
177
199
  if (!session.messages)
178
200
  session.messages = [];
@@ -247,6 +269,38 @@ You are running locally on the user's machine. No data leaves their computer. Yo
247
269
  }
248
270
  return parts.join('\n') || 'Done.';
249
271
  }
272
+ /**
273
+ * Route to the correct provider-specific method (Moltbot-style)
274
+ */
275
+ async processWithProvider(provider, model, history) {
276
+ // Temporarily override model if specified
277
+ const originalModel = this.config?.model;
278
+ if (model && this.config) {
279
+ this.config.model = model;
280
+ }
281
+ try {
282
+ switch (provider) {
283
+ case 'ollama':
284
+ return await this.processWithOllama(history);
285
+ case 'google':
286
+ return await this.processWithGemini(history);
287
+ case 'anthropic':
288
+ return await this.processWithClaude(history);
289
+ case 'openai':
290
+ return await this.processWithOpenAI(history);
291
+ case 'kimi':
292
+ return await this.processWithKimi(history);
293
+ default:
294
+ throw new Error(`Unknown provider: ${provider}`);
295
+ }
296
+ }
297
+ finally {
298
+ // Restore original model
299
+ if (originalModel && this.config) {
300
+ this.config.model = originalModel;
301
+ }
302
+ }
303
+ }
250
304
  async processWithKimi(history) {
251
305
  // Generic Kimi 2.5 integration wrapper. This implementation is intentionally
252
306
  // generic: it will POST to a user-provided `apiUrl` (in config or env) and
@@ -298,7 +352,7 @@ You are running locally on the user's machine. No data leaves their computer. Yo
298
352
  const apiUrl = cfg.apiUrl || 'http://localhost:11434';
299
353
  const model = cfg.model || 'llama3.2';
300
354
  const temperature = cfg.temperature ?? 0.7;
301
- console.log(`[Agent] Calling Ollama at ${apiUrl} with model ${model}...`);
355
+ (0, debug_1.debug)('Agent', `Calling Ollama at ${apiUrl} with model ${model}...`);
302
356
  // Build messages array for Ollama chat API
303
357
  const messages = history.map(m => ({
304
358
  role: m.role,
@@ -327,7 +381,7 @@ You are running locally on the user's machine. No data leaves their computer. Yo
327
381
  }
328
382
  const data = await res.json();
329
383
  const text = data.message?.content || '';
330
- console.log(`[Agent] Ollama response received: ${text.slice(0, 50)}...`);
384
+ (0, debug_1.debug)('Agent', `Ollama response received: ${text.slice(0, 50)}...`);
331
385
  return {
332
386
  text: String(text).trim() || 'No response from model',
333
387
  usage: {
@@ -379,7 +433,7 @@ You are running locally on the user's machine. No data leaves their computer. Yo
379
433
  try {
380
434
  // Build full system prompt from brain files
381
435
  systemPrompt = await this.brain.buildSystemPrompt(this.defaultSystemPrompt);
382
- console.log('[Agent] Using brain context for system prompt');
436
+ (0, debug_1.debug)('Agent', 'Using brain context for system prompt');
383
437
  }
384
438
  catch (e) {
385
439
  console.warn('[Agent] Failed to load brain context, using default');
@@ -22,6 +22,7 @@ const allowlist_1 = require("../safety/allowlist");
22
22
  const unifiedInbox_1 = require("../channels/unifiedInbox");
23
23
  const lobster_1 = require("../lobster");
24
24
  const agentManager_1 = require("./agentManager");
25
+ const debug_1 = require("../core/debug");
25
26
  const tools_1 = require("../tools");
26
27
  /**
27
28
  * Agent Orchestrator - Main control loop
@@ -94,7 +95,7 @@ class AgentOrchestrator extends events_1.EventEmitter {
94
95
  // ─────────────────────────────────────────
95
96
  // PHASE 1: INGEST
96
97
  // ─────────────────────────────────────────
97
- console.log('[Orchestrator] Phase 1: INGEST');
98
+ (0, debug_1.debugPhase)(1, 'INGEST', { message, sessionId });
98
99
  this.emit('phase:ingest', { message, sessionId });
99
100
  // Get or create session
100
101
  const session = await this.sessionStore.getOrCreate(sessionId, userId, channel);
@@ -107,7 +108,7 @@ class AgentOrchestrator extends events_1.EventEmitter {
107
108
  // ─────────────────────────────────────────
108
109
  // PHASE 2: CONTEXT
109
110
  // ─────────────────────────────────────────
110
- console.log('[Orchestrator] Phase 2: CONTEXT');
111
+ (0, debug_1.debugPhase)(2, 'CONTEXT', { sessionId });
111
112
  this.emit('phase:context', { sessionId });
112
113
  // Get brain context
113
114
  const brainContext = await this.brain.buildSystemPrompt();
@@ -118,7 +119,7 @@ class AgentOrchestrator extends events_1.EventEmitter {
118
119
  if (this.config.enableRAG) {
119
120
  const searchResults = await this.vectorStore.search(message, this.config.ragTopK, this.config.ragThreshold);
120
121
  memories = searchResults.map(r => r.entry.content);
121
- console.log(`[Orchestrator] Found ${memories.length} relevant memories`);
122
+ (0, debug_1.debug)('Orchestrator', `Found ${memories.length} relevant memories`);
122
123
  }
123
124
  const context = {
124
125
  sessionId,
@@ -134,7 +135,7 @@ class AgentOrchestrator extends events_1.EventEmitter {
134
135
  // ─────────────────────────────────────────
135
136
  // PHASE 3: INFER
136
137
  // ─────────────────────────────────────────
137
- console.log('[Orchestrator] Phase 3: INFER');
138
+ (0, debug_1.debugPhase)(3, 'INFER', null);
138
139
  this.emit('phase:infer', { context });
139
140
  // Build enhanced prompt with memories
140
141
  let enhancedMessage = message;
@@ -172,7 +173,7 @@ class AgentOrchestrator extends events_1.EventEmitter {
172
173
  // ─────────────────────────────────────────
173
174
  // PHASE 4: EXECUTE (handled in agent)
174
175
  // ─────────────────────────────────────────
175
- console.log('[Orchestrator] Phase 4: EXECUTE');
176
+ (0, debug_1.debugPhase)(4, 'EXECUTE', { toolsExecuted: result.toolsExecuted });
176
177
  this.emit('phase:execute', { toolsExecuted: result.toolsExecuted });
177
178
  // Check for workflow triggers
178
179
  const workflowMatch = message.match(/^\/(run|workflow)\s+(\w+)/i);
@@ -190,7 +191,7 @@ class AgentOrchestrator extends events_1.EventEmitter {
190
191
  // ─────────────────────────────────────────
191
192
  // PHASE 5: RESPOND
192
193
  // ─────────────────────────────────────────
193
- console.log('[Orchestrator] Phase 5: RESPOND');
194
+ (0, debug_1.debugPhase)(5, 'RESPOND', null);
194
195
  this.emit('phase:respond', { response: result.response });
195
196
  // Store assistant response in session
196
197
  await this.sessionStore.appendMessage(sessionId, {
@@ -94,6 +94,46 @@ function parseToolCalls(text) {
94
94
  }
95
95
  }
96
96
  }
97
+ // Pattern 4: Raw JSON tool calls (no code block) - common with Ollama
98
+ // Matches: {"tool": "name", "args": {...}}
99
+ if (calls.length === 0) {
100
+ const rawJsonRegex = /\{"(?:tool|function|name)"\s*:\s*"([^"]+)"[^}]*\}/g;
101
+ let rawMatch;
102
+ while ((rawMatch = rawJsonRegex.exec(text)) !== null) {
103
+ try {
104
+ // Try to parse the full JSON object
105
+ const startIdx = rawMatch.index;
106
+ let braceCount = 0;
107
+ let endIdx = startIdx;
108
+ for (let i = startIdx; i < text.length; i++) {
109
+ if (text[i] === '{')
110
+ braceCount++;
111
+ if (text[i] === '}')
112
+ braceCount--;
113
+ if (braceCount === 0) {
114
+ endIdx = i + 1;
115
+ break;
116
+ }
117
+ }
118
+ const jsonStr = text.slice(startIdx, endIdx);
119
+ const parsed = JSON.parse(jsonStr);
120
+ if (parsed.tool || parsed.function || parsed.name) {
121
+ const toolName = parsed.tool || parsed.function || parsed.name;
122
+ // Only add if it's a registered tool
123
+ if (tools_1.toolRegistry.has(toolName)) {
124
+ calls.push({
125
+ name: toolName,
126
+ arguments: parsed.args || parsed.arguments || parsed.parameters || {},
127
+ });
128
+ console.log(`[ToolExecutor] Parsed raw JSON tool call: ${toolName}`);
129
+ }
130
+ }
131
+ }
132
+ catch (e) {
133
+ // Failed to parse, try next match
134
+ }
135
+ }
136
+ }
97
137
  if (calls.length > 0) {
98
138
  console.log(`[ToolExecutor] Found ${calls.length} tool call(s): ${calls.map(c => c.name).join(', ')}`);
99
139
  }
@@ -140,47 +180,48 @@ function getToolsSystemPrompt() {
140
180
  return '';
141
181
  }
142
182
  const toolDescriptions = tools.map(t => {
143
- const params = t.parameters.map(p => ` - ${p.name} (${p.type}${p.required ? ', required' : ''}): ${p.description}`).join('\n');
144
- return `- **${t.name}**: ${t.description}`;
183
+ const params = t.parameters.map(p => ` - ${p.name} (${p.type}${p.required ? ', required' : ''}): ${p.description}`).join('\n');
184
+ return `- **${t.name}**: ${t.description}\n Parameters:\n${params}`;
145
185
  }).join('\n');
186
+ const toolNames = tools.map(t => t.name).join(', ');
146
187
  return `
147
- ## TOOLS - IMPORTANT!
188
+ ## TOOLS - CRITICAL INSTRUCTIONS
148
189
 
149
- You have tools to interact with the real world. **USE THEM AUTOMATICALLY** when the user asks for something that requires them.
190
+ You have access to specific tools listed below. Use them to perform real actions.
150
191
 
151
- **HOW TO USE A TOOL:**
152
- Include this EXACT format in your response (the system will execute it automatically):
192
+ **⚠️ CRITICAL RULE:**
193
+ You can ONLY use tools from this exact list: [${toolNames}]
194
+ DO NOT invent, guess, or fabricate tool names. If a tool doesn't exist in the list, say "I don't have a tool for that" instead of making one up.
153
195
 
196
+ **FORMAT (exact JSON required):**
154
197
  \`\`\`json
155
- {"tool": "tool_name", "args": {"param": "value"}}
198
+ {"tool": "exact_tool_name_from_list", "args": {"param": "value"}}
156
199
  \`\`\`
157
200
 
158
- **WHEN TO USE TOOLS (do it automatically, don't ask!):**
159
- - "what's the weather" → use \`weather\` tool
160
- - "set a reminder" → use \`reminder_set\` tool
161
- - "what time is it" → use \`datetime\` tool
162
- - "how much RAM" / system info → use \`system_info\` tool
163
- - "calculate X" / math → use \`math\` tool
164
- - "read file X" → use \`read_file\` tool
165
- - "run command X" → use \`shell\` tool
166
- - "search for X" → use \`web_search\` tool
167
- - "play music" / spotify → use \`spotify_*\` tools
168
- - "create playlist" → use \`spotify_create_playlist\` tool
169
- - "my notes" / obsidian → use \`obsidian_*\` tools
201
+ **WHEN TO USE TOOLS (do it automatically):**
202
+ - Weather request → use \`weather\`
203
+ - Time/date → use \`datetime\`
204
+ - Math → use \`math\`
205
+ - System info → use \`system_info\`
206
+ - Set reminder → use \`reminder_set\`
207
+ - Spotify controls → use \`spotify_*\` (only if listed)
208
+ - Notes/Obsidian → use \`obsidian_*\` (only if listed)
209
+ - Web search → use \`search\` or \`web_search\`
210
+ - Shell commands → use \`shell\`
211
+ - Read/write files → use \`read_file\`, \`write_file\`
170
212
 
171
- **AVAILABLE TOOLS:**
213
+ **AVAILABLE TOOLS (ONLY these exist):**
172
214
  ${toolDescriptions}
173
215
 
174
216
  **RULES:**
175
- 1. ALWAYS use a tool when the user's request matches a tool's capability
176
- 2. Do NOT explain how to use tools - just USE them
177
- 3. Do NOT show code examples - execute the tool directly
178
- 4. After tool execution, summarize the result in natural language
179
- 5. If a tool fails, explain why and suggest alternatives
217
+ 1. USE tools from the above list automatically
218
+ 2. NEVER invent tool names that aren't listed
219
+ 3. NEVER show code examples - execute tools directly
220
+ 4. If tool doesn't exist, say "I don't have that capability"
221
+ 5. After execution, explain results in natural language
180
222
 
181
223
  **EXAMPLE:**
182
224
  User: "What's the weather in Tokyo?"
183
- Your response should include:
184
225
  \`\`\`json
185
226
  {"tool": "weather", "args": {"location": "Tokyo"}}
186
227
  \`\`\`
@@ -5,6 +5,7 @@
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.TelegramChannel = void 0;
7
7
  const grammy_1 = require("grammy");
8
+ const transformer_throttler_1 = require("@grammyjs/transformer-throttler");
8
9
  const eventBus_1 = require("../core/eventBus");
9
10
  class TelegramChannel {
10
11
  name = 'telegram';
@@ -19,6 +20,8 @@ class TelegramChannel {
19
20
  ...config,
20
21
  };
21
22
  this.bot = new grammy_1.Bot(config.botToken);
23
+ // Apply rate limiting (Moltbot-style)
24
+ this.bot.api.config.use((0, transformer_throttler_1.apiThrottler)());
22
25
  this.setupHandlers();
23
26
  }
24
27
  setupHandlers() {
@@ -33,6 +36,13 @@ class TelegramChannel {
33
36
  const chatId = ctx.chat.id;
34
37
  const isGroup = ctx.chat.type === 'group' || ctx.chat.type === 'supergroup';
35
38
  const text = ctx.message.text;
39
+ // Send typing indicator immediately (Moltbot-style)
40
+ try {
41
+ await ctx.replyWithChatAction('typing');
42
+ }
43
+ catch (e) {
44
+ // Ignore typing errors
45
+ }
36
46
  // Skip slash commands - they are handled by bot.command()
37
47
  if (text.startsWith('/')) {
38
48
  return;
package/dist/cli/chat.js CHANGED
@@ -81,16 +81,13 @@ const FOX_THINKING = `${colors.orange}
81
81
  ${colors.orange} / - \\${colors.reset}
82
82
  `;
83
83
  const BANNER = `
84
- ${colors.rust}╔═══════════════════════════════════════════════════════════╗${colors.reset}
85
- ${colors.rust}║${colors.reset} ${colors.orange}█████╗ ██████╗ ███████╗██╗ ██╗${colors.amber} ██████╗ ██████╗ ████████╗${colors.reset} ${colors.rust}║${colors.reset}
86
- ${colors.rust}║${colors.reset} ${colors.orange}██╔══██╗██╔══██╗██╔════╝╚██╗██╔╝${colors.amber} ██╔══██╗██╔═══██╗╚══██╔══╝${colors.reset} ${colors.rust}║${colors.reset}
87
- ${colors.rust}║${colors.reset} ${colors.orange}███████║██████╔╝█████╗ ╚███╔╝${colors.amber} ██████╔╝██║ ██║ ██║${colors.reset} ${colors.rust}║${colors.reset}
88
- ${colors.rust}║${colors.reset} ${colors.orange}██╔══██║██╔═══╝ ██╔══╝ ██╔██╗${colors.amber} ██╔══██╗██║ ██║ ██║${colors.reset} ${colors.rust}║${colors.reset}
89
- ${colors.rust}║${colors.reset} ${colors.orange}██║ ██║██║ ███████╗██╔╝ ██╗${colors.amber} ██████╔╝╚██████╔╝ ██║${colors.reset} ${colors.rust}║${colors.reset}
90
- ${colors.rust}║${colors.reset} ${colors.orange}╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝${colors.amber} ╚═════╝ ╚═════╝ ╚═╝${colors.reset} ${colors.rust}║${colors.reset}
91
- ${colors.rust}╠═══════════════════════════════════════════════════════════╣${colors.reset}
92
- ${colors.rust}║${colors.reset} ${colors.gold}🦊 Free • Private • Local-First AI${colors.reset} ${colors.gray}v2026.1${colors.reset} ${colors.rust}║${colors.reset}
93
- ${colors.rust}╚═══════════════════════════════════════════════════════════╝${colors.reset}
84
+ ${colors.orange}${colors.bold} _ ____ _______ ______ ___ _____${colors.reset}
85
+ ${colors.orange}${colors.bold} / \\ | _ \\| ____\\ \\/ / __ ) / _ \\_ _|${colors.reset}
86
+ ${colors.amber}${colors.bold} / _ \\ | |_) | _| \\ /| _ \\| | | || |${colors.reset}
87
+ ${colors.amber}${colors.bold} / ___ \\| __/| |___ / \\| |_) | |_| || |${colors.reset}
88
+ ${colors.rust}${colors.bold} /_/ \\_\\_| |_____/_/\\_\\____/ \\___/ |_|${colors.reset}
89
+
90
+ ${colors.gold}🦊 Free • Private • Local-First AI${colors.reset} ${colors.gray}v2026.1${colors.reset}
94
91
  `;
95
92
  const SPINNER_FRAMES = ['🦊', '🔥', '✨', '💫', '⭐', '🌟', '✨', '🔥'];
96
93
  /**
@@ -270,6 +267,27 @@ ${colors.orange}${colors.bold}Input Prefixes${colors.reset} ${co
270
267
  case 'tools':
271
268
  this.emit('command:tools');
272
269
  break;
270
+ case 'model':
271
+ if (args[0]) {
272
+ this.emit('command:model', args[0]);
273
+ console.log(`${colors.green}✓ Model changed to: ${colors.gold}${args[0]}${colors.reset}`);
274
+ }
275
+ else {
276
+ this.emit('command:model:show');
277
+ }
278
+ break;
279
+ case 'provider':
280
+ if (args[0]) {
281
+ this.emit('command:provider', args[0]);
282
+ console.log(`${colors.green}✓ Provider changed to: ${colors.gold}${args[0]}${colors.reset}`);
283
+ }
284
+ else {
285
+ console.log(`${colors.amber}Usage: /provider <ollama|google|anthropic|openai|kimi>${colors.reset}`);
286
+ }
287
+ break;
288
+ case 'config':
289
+ this.emit('command:config');
290
+ break;
273
291
  case 'exit':
274
292
  case 'quit':
275
293
  case 'bye':
@@ -285,15 +303,19 @@ ${colors.orange}${colors.bold}Input Prefixes${colors.reset} ${co
285
303
  ${colors.orange}${colors.bold}🦊 ApexBot Commands${colors.reset}
286
304
  ${colors.gray}${'─'.repeat(40)}${colors.reset}
287
305
 
288
- ${colors.orange}/help${colors.reset} Show this help
289
- ${colors.orange}/clear${colors.reset} Clear screen
290
- ${colors.orange}/reset${colors.reset} Reset conversation
291
- ${colors.orange}/history${colors.reset} Show recent messages
292
- ${colors.orange}/status${colors.reset} Show bot status
293
- ${colors.orange}/skills${colors.reset} List enabled skills
294
- ${colors.orange}/tools${colors.reset} List available tools
295
- ${colors.orange}/fox${colors.reset} Show fox mascot 🦊
296
- ${colors.orange}/exit${colors.reset} Exit ApexBot
306
+ ${colors.orange}/help${colors.reset} Show this help
307
+ ${colors.orange}/clear${colors.reset} Clear screen
308
+ ${colors.orange}/reset${colors.reset} Reset conversation
309
+ ${colors.orange}/history${colors.reset} Show recent messages
310
+ ${colors.orange}/status${colors.reset} Show bot status
311
+ ${colors.orange}/skills${colors.reset} List enabled skills
312
+ ${colors.orange}/tools${colors.reset} List available tools
313
+ ${colors.orange}/model${colors.reset} Show/change model
314
+ ${colors.orange}/model${colors.reset} ${colors.gray}<name>${colors.reset} Change to specified model
315
+ ${colors.orange}/provider${colors.reset} Change AI provider
316
+ ${colors.orange}/config${colors.reset} Show configuration
317
+ ${colors.orange}/fox${colors.reset} Show fox mascot 🦊
318
+ ${colors.orange}/exit${colors.reset} Exit ApexBot
297
319
 
298
320
  ${colors.gray}Shortcuts: Ctrl+C (exit), Ctrl+L (clear), ↑↓ (history)${colors.reset}
299
321
  `);
package/dist/cli/index.js CHANGED
@@ -1105,6 +1105,43 @@ program
1105
1105
  const status = await orchestrator.getStatus();
1106
1106
  console.log(` Files: ${status.brain?.files?.join(', ') || 'none'}`);
1107
1107
  });
1108
+ // Model/provider/config commands
1109
+ chat.on('command:model', (model) => {
1110
+ const cfg = loadConfig();
1111
+ cfg.agent = cfg.agent || {};
1112
+ cfg.agent.model = model;
1113
+ saveConfig(cfg);
1114
+ console.log(chalk.green(` Model configured: ${model}`));
1115
+ });
1116
+ chat.on('command:model:show', () => {
1117
+ const cfg = loadConfig();
1118
+ const currentModel = cfg.agent?.model || 'llama3.2:latest';
1119
+ const provider = cfg.agent?.provider || 'ollama';
1120
+ console.log(`\n ${chalk.yellow('Current Model:')} ${chalk.cyan(currentModel)}`);
1121
+ console.log(` ${chalk.yellow('Provider:')} ${chalk.cyan(provider)}`);
1122
+ console.log(`\n ${chalk.gray('Usage: /model <name> (e.g. /model gemma2:9b)')}`);
1123
+ });
1124
+ chat.on('command:provider', (provider) => {
1125
+ const validProviders = ['ollama', 'google', 'anthropic', 'openai', 'kimi'];
1126
+ if (!validProviders.includes(provider.toLowerCase())) {
1127
+ console.log(chalk.red(` Invalid provider. Use: ${validProviders.join(', ')}`));
1128
+ return;
1129
+ }
1130
+ const cfg = loadConfig();
1131
+ cfg.agent = cfg.agent || {};
1132
+ cfg.agent.provider = provider.toLowerCase();
1133
+ saveConfig(cfg);
1134
+ console.log(chalk.green(` Provider configured: ${provider}`));
1135
+ });
1136
+ chat.on('command:config', () => {
1137
+ const cfg = loadConfig();
1138
+ console.log(`\n ${chalk.yellow('Current Configuration:')}`);
1139
+ console.log(` ${chalk.gray('─'.repeat(30))}`);
1140
+ console.log(` Provider: ${chalk.cyan(cfg.agent?.provider || 'ollama')}`);
1141
+ console.log(` Model: ${chalk.cyan(cfg.agent?.model || 'llama3.2:latest')}`);
1142
+ console.log(` API URL: ${chalk.cyan(cfg.agent?.apiUrl || 'http://localhost:11434')}`);
1143
+ console.log(`\n ${chalk.gray(`Config file: ${CONFIG_FILE}`)}`);
1144
+ });
1108
1145
  // Start chat
1109
1146
  await chat.start();
1110
1147
  });