bimmo-cli 2.2.12 → 3.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.
Files changed (3) hide show
  1. package/bin/bimmo +8 -1
  2. package/package.json +12 -3
  3. package/src/interface.js +313 -263
package/bin/bimmo CHANGED
@@ -6,11 +6,18 @@ process.removeAllListeners('warning');
6
6
  import { program } from 'commander';
7
7
  import { startInteractive } from '../src/interface.js';
8
8
  import { configure } from '../src/config.js';
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
9
16
 
10
17
  program
11
18
  .name('bimmo')
12
19
  .description('bimmo — Sua IA universal no terminal (verde & lavanda)')
13
- .version('1.2.4');
20
+ .version(pkg.version);
14
21
 
15
22
  program
16
23
  .command('config')
package/package.json CHANGED
@@ -1,16 +1,20 @@
1
1
  {
2
2
  "name": "bimmo-cli",
3
- "version": "2.2.12",
4
- "description": "🌿 Plataforma de IA universal profissional com Agentes e Swarms. Suporte a Autocomplete real-time sólido, Diffs e Contexto Inteligente.",
3
+ "version": "3.0.0",
4
+ "description": "🌿 Plataforma de IA universal profissional com interface React (Ink). Agentes, Swarms, Autocomplete real-time e Contexto Inteligente.",
5
5
  "bin": {
6
6
  "bimmo": "bin/bimmo"
7
7
  },
8
8
  "type": "module",
9
9
  "keywords": [
10
- "ai", "cli", "openai", "anthropic", "gemini", "grok", "deepseek", "openrouter", "zai", "agent", "swarm", "terminal"
10
+ "ai", "cli", "ink", "react", "openai", "anthropic", "gemini", "agent", "swarm", "terminal"
11
11
  ],
12
12
  "author": "Judah",
13
13
  "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/JudahAragao/bimmo-cli.git"
17
+ },
14
18
  "files": [
15
19
  "bin/",
16
20
  "src/",
@@ -30,6 +34,10 @@
30
34
  "diff": "^7.0.0",
31
35
  "figlet": "^1.7.0",
32
36
  "fuzzy": "^0.1.3",
37
+ "ink": "^5.1.0",
38
+ "ink-divider": "^3.0.0",
39
+ "ink-spinner": "^5.0.0",
40
+ "ink-text-input": "^6.0.0",
33
41
  "inquirer": "^9.3.8",
34
42
  "marked": "^14.0.0",
35
43
  "marked-terminal": "^7.0.0",
@@ -37,6 +45,7 @@
37
45
  "ollama": "^0.5.12",
38
46
  "openai": "^4.82.0",
39
47
  "ora": "^8.1.1",
48
+ "react": "^18.2.0",
40
49
  "zod": "^3.24.1"
41
50
  }
42
51
  }
package/src/interface.js CHANGED
@@ -1,310 +1,360 @@
1
+ import React, { useState, useEffect, useMemo, useRef } from 'react';
2
+ import { render, Box, Text, useInput, useApp } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import Spinner from 'ink-spinner';
1
5
  import chalk from 'chalk';
2
6
  import figlet from 'figlet';
3
7
  import { marked } from 'marked';
4
8
  import TerminalRenderer from 'marked-terminal';
5
- import ora from 'ora';
6
9
  import fs from 'fs';
7
10
  import path from 'path';
8
- import mime from 'mime-types';
9
- import readline from 'readline';
10
11
  import { fileURLToPath } from 'url';
11
12
 
12
- import { getConfig, configure, updateActiveModel, switchProfile } from './config.js';
13
+ import { getConfig, updateActiveModel, switchProfile } from './config.js';
13
14
  import { createProvider } from './providers/factory.js';
14
15
  import { getProjectContext } from './project-context.js';
15
- import { SwarmOrchestrator } from './orchestrator.js';
16
16
  import { editState } from './agent.js';
17
+ import { SwarmOrchestrator } from './orchestrator.js';
17
18
 
18
- const __filename = fileURLToPath(import.meta.url);
19
- const __dirname = path.dirname(__filename);
20
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
21
- const version = pkg.version;
22
-
23
- // CONFIGURAÇÃO DO RENDERIZADOR - ELIMINA HTML E FORMATA TERMINAL
19
+ // Configuração do renderizador Markdown para o terminal
24
20
  marked.use(new TerminalRenderer({
25
21
  heading: chalk.hex('#c084fc').bold,
26
22
  code: chalk.hex('#00ff9d'),
27
23
  strong: chalk.bold,
28
24
  em: chalk.italic,
29
- html: () => '', // DROP TOTAL DE HTML
25
+ html: () => '',
30
26
  }));
31
27
 
32
- const green = chalk.hex('#00ff9d');
33
- const lavender = chalk.hex('#c084fc');
34
- const gray = chalk.gray;
35
- const bold = chalk.bold;
36
- const yellow = chalk.yellow;
37
-
38
- let currentMode = 'chat';
39
- let activePersona = null;
40
- let exitCounter = 0;
41
- let exitTimer = null;
42
- let currentPreviewLines = 0;
43
-
44
- const i18n = {
45
- 'pt-BR': {
46
- welcome: 'Olá! Sou o bimmo. Como posso ajudar?',
47
- thinking: 'bimmo pensando...',
48
- interrupted: 'Interrompido.',
49
- exitHint: '(Pressione Ctrl+C novamente para sair)',
50
- switchOk: 'Perfil ativo:',
51
- agentOk: 'Agente ativo:',
52
- help: '\nComandos:\n /chat | /plan | /edit | /init\n /switch [perfil] | /model [modelo]\n /use [agente] | /use normal\n /config | /clear | @arquivo\n'
53
- },
54
- 'en-US': {
55
- welcome: 'Hello! I am bimmo. How can I help?',
56
- thinking: 'bimmo thinking...',
57
- interrupted: 'Interrupted.',
58
- exitHint: '(Press Ctrl+C again to exit)',
59
- switchOk: 'Profile active:',
60
- agentOk: 'Agent active:',
61
- help: '\nCommands:\n /chat | /plan | /edit | /init\n /switch [profile] | /model [model]\n /use [agent] | /use normal\n /config | /clear | @file\n'
62
- }
63
- };
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = path.dirname(__filename);
30
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
31
+ const version = pkg.version;
64
32
 
65
- /**
66
- * Coleta arquivos e diretórios para o preview (Lógica Gemini-CLI)
67
- */
68
- function getFilesForPreview(partialPath) {
69
- try {
70
- let p = partialPath.startsWith('@') ? partialPath.slice(1) : partialPath;
71
- let searchDir = '.';
72
- let filter = '';
33
+ const green = '#00ff9d';
34
+ const lavender = '#c084fc';
35
+ const gray = '#6272a4';
36
+ const yellow = '#f1fa8c';
37
+ const red = '#ff5555';
38
+ const cyan = '#8be9fd';
39
+
40
+ const BimmoApp = ({ initialConfig }) => {
41
+ const { exit } = useApp();
42
+ const [config, setConfig] = useState(initialConfig);
43
+ const [mode, setMode] = useState('chat');
44
+ const [activePersona, setActivePersona] = useState(null);
45
+ const [messages, setMessages] = useState([]);
46
+ const [input, setInput] = useState('');
47
+ const [isThinking, setIsThinking] = useState(false);
48
+ const [thinkingMessage, setThinkingMessage] = useState('bimmo pensando...');
49
+ const [exitCounter, setExitCounter] = useState(0);
50
+ const [provider, setProvider] = useState(() => createProvider(initialConfig));
51
+
52
+ // Inicializa com o contexto do projeto
53
+ useEffect(() => {
54
+ const ctx = getProjectContext();
55
+ setMessages([{ role: 'system', content: ctx }]);
56
+
57
+ // Welcome message
58
+ setMessages(prev => [...prev, {
59
+ role: 'assistant',
60
+ content: `Olá! Sou o **bimmo v${version}**. Como posso ajudar hoje?\n\nDigite \`/help\` para ver os comandos disponíveis.`
61
+ }]);
62
+ }, []);
63
+
64
+ // Lógica de Autocomplete em tempo real para @arquivos
65
+ const filePreview = useMemo(() => {
66
+ if (!input.includes('@')) return [];
67
+ const words = input.split(' ');
68
+ const lastWord = words[words.length - 1];
69
+ if (!lastWord.startsWith('@')) return [];
73
70
 
74
- if (p.endsWith('/')) {
75
- searchDir = p;
76
- filter = '';
77
- } else if (p.includes('/')) {
78
- const lastSlash = p.lastIndexOf('/');
79
- searchDir = p.substring(0, lastSlash);
80
- filter = p.substring(lastSlash + 1);
81
- } else {
82
- filter = p;
71
+ try {
72
+ const p = lastWord.slice(1);
73
+ const dir = p.includes('/') ? p.substring(0, p.lastIndexOf('/')) : '.';
74
+ const filter = p.includes('/') ? p.substring(p.lastIndexOf('/') + 1) : p;
75
+ const absDir = path.resolve(process.cwd(), dir);
76
+ if (!fs.existsSync(absDir)) return [];
77
+
78
+ return fs.readdirSync(absDir)
79
+ .filter(f => f.startsWith(filter) && !f.startsWith('.') && f !== 'node_modules')
80
+ .slice(0, 5)
81
+ .map(f => ({
82
+ name: f,
83
+ isDir: fs.statSync(path.join(absDir, f)).isDirectory(),
84
+ rel: path.join(dir === '.' ? '' : dir, f)
85
+ }));
86
+ } catch (e) { return []; }
87
+ }, [input]);
88
+
89
+ const handleSubmit = async (val) => {
90
+ const rawInput = val.trim();
91
+ if (!rawInput) return;
92
+ setInput('');
93
+
94
+ const lowerInput = rawInput.toLowerCase();
95
+ const parts = rawInput.split(' ');
96
+ const cmd = parts[0].toLowerCase();
97
+
98
+ // COMANDOS INTERNOS
99
+ if (cmd === '/exit' || cmd === 'sair') exit();
100
+
101
+ if (cmd === '/clear') {
102
+ const ctx = getProjectContext();
103
+ setMessages([{ role: 'system', content: ctx }, { role: 'assistant', content: 'Chat limpo.' }]);
104
+ return;
83
105
  }
84
106
 
85
- const absoluteSearchDir = path.resolve(process.cwd(), searchDir);
86
- if (!fs.existsSync(absoluteSearchDir)) return [];
87
-
88
- return fs.readdirSync(absoluteSearchDir)
89
- .filter(f => f.startsWith(filter) && !f.startsWith('.') && f !== 'node_modules')
90
- .map(f => {
91
- const fullPath = path.join(absoluteSearchDir, f);
92
- const isDir = fs.statSync(fullPath).isDirectory();
93
- return { name: f, rel: path.join(searchDir, f), isDir };
94
- });
95
- } catch (e) { return []; }
96
- }
97
-
98
- function cleanAIResponse(text) {
99
- if (!text) return "";
100
- return text
101
- .replace(/<br\s*\/?>/gi, '\n')
102
- .replace(/<p>/gi, '')
103
- .replace(/<\/p>/gi, '\n\n')
104
- .replace(/<[^>]*>?/gm, '')
105
- .trim();
106
- }
107
-
108
- export async function startInteractive() {
109
- let config = getConfig();
110
- const lang = config.language || 'pt-BR';
111
- const t = i18n[lang] || i18n['pt-BR'];
112
-
113
- if (!config.provider || !config.apiKey) {
114
- console.log(lavender(figlet.textSync('bimmo')));
115
- await configure(); return startInteractive();
116
- }
117
-
118
- let provider = createProvider(config);
119
- const orchestrator = new SwarmOrchestrator(config);
120
- let messages = [];
121
-
122
- const resetMessages = () => {
123
- messages = [{ role: 'system', content: getProjectContext() }];
124
- if (activePersona) {
125
- const agent = (config.agents || {})[activePersona];
126
- if (agent) messages.push({ role: 'system', content: `Persona: ${agent.name}. Task: ${agent.role}` });
107
+ if (cmd === '/chat') { setMode('chat'); return; }
108
+ if (cmd === '/plan') { setMode('plan'); return; }
109
+ if (cmd === '/edit') {
110
+ setMode('edit');
111
+ editState.autoAccept = parts[1] === 'auto';
112
+ return;
127
113
  }
128
- };
129
- resetMessages();
130
114
 
131
- console.clear();
132
- console.log(lavender(figlet.textSync('bimmo')));
133
- console.log(lavender(` v${version} `.padStart(60, '─')));
134
- console.log(green(` Perfil: ${bold(config.activeProfile || 'Padrão')} • Modelo: ${bold(config.model)}`));
135
- console.log(lavender('─'.repeat(60)) + '\n');
136
- console.log(lavender(`👋 ${t.welcome}\n`));
137
-
138
- const rl = readline.createInterface({
139
- input: process.stdin,
140
- output: process.stdout,
141
- terminal: true,
142
- completer: (line) => {
143
- const words = line.split(' ');
144
- const lastWord = words[words.length - 1];
145
- if (lastWord.startsWith('@')) {
146
- const hits = getFilesForPreview(lastWord).map(h => `@${h.rel}${h.isDir ? '/' : ''}`);
147
- return [hits, lastWord];
115
+ if (cmd === '/model') {
116
+ const newModel = parts[1];
117
+ if (newModel) {
118
+ updateActiveModel(newModel);
119
+ const newCfg = getConfig();
120
+ setConfig(newCfg);
121
+ setProvider(createProvider(newCfg));
122
+ setMessages(prev => [...prev, { role: 'system', content: `Modelo alterado para: ${newModel}` }]);
148
123
  }
149
- return [[], line];
124
+ return;
150
125
  }
151
- });
152
126
 
153
- const clearPreview = () => {
154
- if (currentPreviewLines > 0) {
155
- readline.cursorTo(process.stdout, 0);
156
- readline.moveCursor(process.stdout, 0, 1);
157
- for (let i = 0; i < currentPreviewLines; i++) {
158
- readline.clearLine(process.stdout, 0);
159
- readline.moveCursor(process.stdout, 0, 1);
127
+ if (cmd === '/switch') {
128
+ const profile = parts[1];
129
+ if (switchProfile(profile)) {
130
+ const newCfg = getConfig();
131
+ setConfig(newCfg);
132
+ setProvider(createProvider(newCfg));
133
+ setMessages(prev => [...prev, { role: 'system', content: `Perfil alterado para: ${profile}` }]);
134
+ } else {
135
+ setMessages(prev => [...prev, { role: 'system', content: `Perfil "${profile}" não encontrado.` }]);
160
136
  }
161
- readline.moveCursor(process.stdout, 0, -(currentPreviewLines + 1));
162
- currentPreviewLines = 0;
137
+ return;
163
138
  }
164
- };
165
139
 
166
- const showPreview = () => {
167
- const words = rl.line.split(' ');
168
- const lastWord = words[words.length - 1];
169
-
170
- if (lastWord.startsWith('@')) {
171
- const files = getFilesForPreview(lastWord);
172
- if (files.length > 0) {
173
- clearPreview();
174
- process.stdout.write('\n');
175
- const display = files.slice(0, 8);
176
- display.forEach(f => {
177
- process.stdout.write(gray(` ${f.isDir ? '📁' : '📄'} ${f.rel}${f.isDir ? '/' : ''}\n`));
178
- });
179
- currentPreviewLines = display.length;
180
- readline.moveCursor(process.stdout, 0, -(currentPreviewLines + 1));
181
- // Reposiciona o cursor de digitação
182
- const promptLen = (activePersona ? activePersona.length + 2 : 0) + (currentMode.length + 8) + 3;
183
- readline.cursorTo(process.stdout, (promptLen + rl.line.length) % process.stdout.columns);
184
- } else { clearPreview(); }
185
- } else { clearPreview(); }
186
- };
187
-
188
- const displayPrompt = () => {
189
- const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]` : '';
190
- let modeLabel = `[${currentMode.toUpperCase()}]`;
191
- if (currentMode === 'edit') modeLabel = editState.autoAccept ? '[EDIT(AUTO)]' : '[EDIT(MANUAL)]';
192
-
193
- console.log(`${gray(`📁 ${process.cwd()}`)}`);
194
- rl.setPrompt(lavender.bold(personaLabel) + (currentMode === 'edit' ? chalk.red.bold(modeLabel) : lavender.bold(modeLabel)) + green(' > '));
195
- rl.prompt();
196
- };
197
-
198
- // MONITORAMENTO DE TECLAS (REAL-TIME @)
199
- process.stdin.on('keypress', (s, key) => {
200
- if (key && (key.name === 'return' || key.name === 'enter')) return;
201
- setImmediate(() => showPreview());
202
- });
203
-
204
- rl.on('SIGINT', () => {
205
- if (exitCounter === 0) {
206
- exitCounter++;
207
- process.stdout.write(`\n${gray(t.exitHint)}\n`);
208
- exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
209
- displayPrompt();
210
- } else { process.exit(0); }
211
- });
212
-
213
- displayPrompt();
214
-
215
- rl.on('line', async (input) => {
216
- clearPreview();
217
- const rawInput = input.trim();
218
- if (rawInput === '') { displayPrompt(); return; }
219
-
220
- const cmd = rawInput.toLowerCase();
221
- if (cmd === '/exit' || cmd === 'sair') process.exit(0);
222
- if (cmd === '/chat') { currentMode = 'chat'; displayPrompt(); return; }
223
- if (cmd === '/plan') { currentMode = 'plan'; displayPrompt(); return; }
224
- if (cmd === '/edit' || cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false; displayPrompt(); return; }
225
- if (cmd === '/edit auto') { currentMode = 'edit'; editState.autoAccept = true; displayPrompt(); return; }
226
- if (cmd === '/clear') { resetMessages(); console.clear(); displayPrompt(); return; }
227
- if (cmd === '/help') { console.log(gray(t.help)); displayPrompt(); return; }
228
-
229
- if (cmd === '/init') {
230
- console.log(chalk.cyan('\n🚀 Mapeando projeto para gerar .bimmorc.json...\n'));
231
- const initPrompt = `Analise a estrutura deste projeto e gere um .bimmorc.json com regras de código, stack e arquitetura. Use write_file.`;
232
- const spinner = ora({ text: lavender(`${t.thinking}`), color: 'red' }).start();
233
- try {
234
- const res = await provider.sendMessage([...messages, { role: 'user', content: initPrompt }]);
235
- spinner.stop(); console.log(marked.parse(cleanAIResponse(res)));
236
- } catch (e) { spinner.stop(); console.error(chalk.red(e.message)); }
237
- resetMessages(); displayPrompt(); return;
140
+ if (cmd === '/use') {
141
+ const agentName = parts[1];
142
+ const agents = config.agents || {};
143
+ if (agentName === 'normal') {
144
+ setActivePersona(null);
145
+ setMessages(prev => [...prev, { role: 'system', content: 'Modo normal ativado.' }]);
146
+ } else if (agents[agentName]) {
147
+ const agent = agents[agentName];
148
+ setActivePersona(agentName);
149
+ setMode(agent.mode || 'chat');
150
+ if (agent.profile && agent.profile !== config.activeProfile) {
151
+ switchProfile(agent.profile);
152
+ const newCfg = getConfig();
153
+ setConfig(newCfg);
154
+ setProvider(createProvider(newCfg));
155
+ }
156
+ setMessages(prev => [...prev, { role: 'system', content: `Agente "${agentName}" ativo.` }]);
157
+ } else {
158
+ setMessages(prev => [...prev, { role: 'system', content: `Agente "${agentName}" não encontrado.` }]);
159
+ }
160
+ return;
238
161
  }
239
162
 
240
- if (cmd === '/config') {
241
- rl.pause(); await configure(); config = getConfig(); provider = createProvider(config); rl.resume();
242
- displayPrompt(); return;
243
- }
163
+ if (cmd === '/swarm') {
164
+ const swarmType = parts[1];
165
+ const orchestrator = new SwarmOrchestrator(config);
166
+ setIsThinking(true);
167
+ setThinkingMessage('Enxame em ação...');
244
168
 
245
- if (cmd.startsWith('/switch ')) {
246
- const pName = rawInput.split(' ')[1];
247
- if (switchProfile(pName)) {
248
- config = getConfig(); provider = createProvider(config);
249
- console.log(green(`\n✓ ${t.switchOk} ${pName}`));
250
- } else { console.log(chalk.red(`\n✖ Perfil não encontrado.`)); }
251
- displayPrompt(); return;
169
+ try {
170
+ let response;
171
+ if (swarmType === 'seq') {
172
+ const agents = parts[2].split(',');
173
+ const goal = parts.slice(3).join(' ');
174
+ response = await orchestrator.runSequential(agents, goal);
175
+ } else if (swarmType === 'run') {
176
+ const manager = parts[2];
177
+ const workers = parts[3].split(',');
178
+ const goal = parts.slice(4).join(' ');
179
+ response = await orchestrator.runHierarchical(manager, workers, goal);
180
+ }
181
+ setMessages(prev => [...prev, { role: 'user', content: rawInput }, { role: 'assistant', content: response }]);
182
+ } catch (err) {
183
+ setMessages(prev => [...prev, { role: 'system', content: `Erro no enxame: ${err.message}` }]);
184
+ } finally {
185
+ setIsThinking(false);
186
+ setThinkingMessage('bimmo pensando...');
187
+ }
188
+ return;
252
189
  }
253
190
 
254
- if (cmd.startsWith('/use ')) {
255
- const aName = rawInput.split(' ')[1];
256
- if (aName === 'normal') { activePersona = null; resetMessages(); displayPrompt(); return; }
257
- const agents = config.agents || {};
258
- if (agents[aName]) {
259
- activePersona = aName;
260
- const agent = agents[aName];
261
- if (switchProfile(agent.profile)) { config = getConfig(); provider = createProvider(config); }
262
- currentMode = agent.mode || 'chat';
263
- console.log(green(`\n✓ ${t.agentOk} ${bold(aName)}`));
264
- resetMessages();
265
- } else { console.log(chalk.red(`\n✖ Agente não encontrado.`)); }
266
- displayPrompt(); return;
191
+ if (cmd === '/help') {
192
+ const helpText = `
193
+ **Comandos Disponíveis:**
194
+ \`/chat\` | \`/plan\` | \`/edit [auto|manual]\` - Muda o modo
195
+ \`/switch [perfil]\` - Alterna perfis
196
+ \`/model [modelo]\` - Altera o modelo
197
+ \`/use [agente|normal]\` - Ativa um agente
198
+ \`/swarm seq [agente1,agente2] [objetivo]\` - Enxame sequencial
199
+ \`/swarm run [líder] [worker1,worker2] [objetivo]\` - Enxame hierárquico
200
+ \`/clear\` - Limpa o chat
201
+ \`/exit\` - Encerra o bimmo
202
+ \`@arquivo\` - Inclui conteúdo de arquivo
203
+ `;
204
+ setMessages(prev => [...prev, { role: 'assistant', content: helpText }]);
205
+ return;
267
206
  }
268
207
 
269
- // PROCESSAMENTO IA
270
- const controller = new AbortController();
271
- const abortHandler = () => controller.abort();
272
- process.removeListener('SIGINT', () => {});
273
- process.on('SIGINT', abortHandler);
274
-
275
- let modeInstr = "";
276
- if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Apenas analise.";
277
- else if (currentMode === 'edit') modeInstr = `\n[MODO EDIT] Auto-Accept: ${editState.autoAccept ? 'ON' : 'OFF'}`;
278
-
279
- const processedContent = [];
280
- const words = rawInput.split(' ');
281
- for (const word of words) {
282
- if (word.startsWith('@')) {
283
- const filePath = word.slice(1);
284
- if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
285
- processedContent.push({ type: 'text', text: `\n[ARQUIVO: ${filePath}]\n${fs.readFileSync(filePath, 'utf-8')}\n` });
286
- } else { processedContent.push({ type: 'text', text: word }); }
287
- } else { processedContent.push({ type: 'text', text: word }); }
208
+ // ENVIO PARA IA
209
+ setIsThinking(true);
210
+
211
+ let processedInput = rawInput;
212
+ const fileMatches = rawInput.match(/@[\w\.\-\/]+/g);
213
+ if (fileMatches) {
214
+ for (const match of fileMatches) {
215
+ const filePath = match.slice(1);
216
+ try {
217
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
218
+ const content = fs.readFileSync(filePath, 'utf-8');
219
+ processedInput = processedInput.replace(match, `\n\n[Arquivo: ${filePath}]\n\`\`\`\n${content}\n\`\`\`\n`);
220
+ }
221
+ } catch (e) {}
222
+ }
288
223
  }
289
224
 
290
- messages.push({ role: 'user', content: [...processedContent, { type: 'text', text: modeInstr }] });
291
- const spinner = ora({ text: lavender(`${t.thinking} (Ctrl+C para parar)`), color: currentMode === 'edit' ? 'red' : 'magenta' }).start();
225
+ const userMsg = { role: 'user', content: processedInput, displayContent: rawInput };
226
+ const newMessages = [...messages, userMsg];
227
+ setMessages(newMessages);
292
228
 
293
229
  try {
294
- let responseText = await provider.sendMessage(messages, { signal: controller.signal });
295
- spinner.stop();
296
- console.log(`\n${lavender('bimmo ')}${currentMode.toUpperCase()}`);
297
- console.log(lavender('─'.repeat(50)));
298
- process.stdout.write(marked.parse(cleanAIResponse(responseText)));
299
- console.log(gray('\n' + ''.repeat(50)));
300
- messages.push({ role: 'assistant', content: responseText });
230
+ let finalMessages = newMessages;
231
+ if (activePersona && config.agents[activePersona]) {
232
+ const agent = config.agents[activePersona];
233
+ finalMessages = [
234
+ { role: 'system', content: `Sua tarefa: ${agent.role}\n\n${getProjectContext()}` },
235
+ ...newMessages.filter(m => m.role !== 'system')
236
+ ];
237
+ }
238
+
239
+ const response = await provider.sendMessage(finalMessages);
240
+ setMessages(prev => [...prev, { role: 'assistant', content: response }]);
301
241
  } catch (err) {
302
- spinner.stop();
303
- if (controller.signal.aborted) { console.log(yellow(`\n⚠️ ${t.interrupted}`)); messages.pop(); }
304
- else { console.error(chalk.red(`\n✖ Erro: ${err.message}`)); }
242
+ setMessages(prev => [...prev, { role: 'system', content: `Erro: ${err.message}` }]);
305
243
  } finally {
306
- process.removeListener('SIGINT', abortHandler);
307
- displayPrompt();
244
+ setIsThinking(false);
245
+ }
246
+ };
247
+
248
+ useInput((input, key) => {
249
+ if (key.ctrl && input === 'c') {
250
+ if (isThinking) {
251
+ setIsThinking(false);
252
+ } else {
253
+ if (exitCounter === 0) {
254
+ setExitCounter(1);
255
+ setTimeout(() => setExitCounter(0), 2000);
256
+ } else {
257
+ exit();
258
+ }
259
+ }
260
+ }
261
+
262
+ if (key.tab && filePreview.length > 0) {
263
+ const words = input.split(' ');
264
+ words[words.length - 1] = `@${filePreview[0].rel}${filePreview[0].isDir ? '/' : ''}`;
265
+ setInput(words.join(' '));
308
266
  }
309
267
  });
268
+
269
+ return (
270
+ <Box flexDirection="column" paddingX={1} minHeight={10}>
271
+ {/* HEADER */}
272
+ <Box flexDirection="column" marginBottom={1}>
273
+ <Text color={lavender}>{figlet.textSync('bimmo', { font: 'small' })}</Text>
274
+ <Box borderStyle="single" borderColor={lavender} paddingX={1} justifyContent="space-between">
275
+ <Text color={green} bold>v{version}</Text>
276
+ <Box>
277
+ <Text color={gray}>{config.activeProfile || 'Default'} </Text>
278
+ <Text color={lavender}>•</Text>
279
+ <Text color={gray}> {config.model}</Text>
280
+ </Box>
281
+ </Box>
282
+ </Box>
283
+
284
+ {/* MENSAGENS */}
285
+ <Box flexDirection="column" flexGrow={1}>
286
+ {messages.filter(m => m.role !== 'system').slice(-10).map((m, i) => (
287
+ <Box key={i} flexDirection="column" marginBottom={1}>
288
+ <Box>
289
+ <Text bold color={m.role === 'user' ? green : lavender}>
290
+ {m.role === 'user' ? '› Você' : '› bimmo'}
291
+ </Text>
292
+ {m.role === 'system' && <Text color={yellow}> [SISTEMA]</Text>}
293
+ </Box>
294
+ <Box paddingLeft={2}>
295
+ <Text>
296
+ {m.role === 'assistant'
297
+ ? marked.parse(m.content).trim()
298
+ : (m.displayContent || m.content)}
299
+ </Text>
300
+ </Box>
301
+ </Box>
302
+ ))}
303
+ </Box>
304
+
305
+ {/* STATUS / THINKING */}
306
+ {isThinking && (
307
+ <Box marginBottom={1}>
308
+ <Text color={lavender}>
309
+ <Spinner type="dots" /> <Text italic>{thinkingMessage}</Text>
310
+ </Text>
311
+ </Box>
312
+ )}
313
+
314
+ {/* AUTOCOMPLETE PREVIEW */}
315
+ {filePreview.length > 0 && (
316
+ <Box flexDirection="column" borderStyle="round" borderColor={gray} paddingX={1} marginBottom={1}>
317
+ <Text color={gray} dimColor italic>Sugestões (TAB para completar):</Text>
318
+ {filePreview.map((f, i) => (
319
+ <Text key={i} color={i === 0 ? green : gray}>
320
+ {f.isDir ? '📁' : '📄'} {f.rel}{f.isDir ? '/' : ''}
321
+ </Text>
322
+ ))}
323
+ </Box>
324
+ )}
325
+
326
+ {/* PROMPT */}
327
+ <Box borderStyle="round" borderColor={isThinking ? gray : lavender} paddingX={1}>
328
+ <Text bold color={mode === 'edit' ? red : mode === 'plan' ? cyan : lavender}>
329
+ {activePersona ? `[${activePersona.toUpperCase()}] ` : ''}
330
+ [{mode.toUpperCase()}] ›{' '}
331
+ </Text>
332
+ <TextInput
333
+ value={input}
334
+ onChange={setInput}
335
+ onSubmit={handleSubmit}
336
+ placeholder="Como posso ajudar hoje?"
337
+ />
338
+ </Box>
339
+
340
+ {/* FOOTER */}
341
+ <Box marginTop={1} justifyContent="space-between" paddingX={1}>
342
+ <Text color={gray} dimColor>📁 {path.relative(process.env.HOME || '', process.cwd())}</Text>
343
+ {exitCounter === 1 && <Text color={yellow} bold> Pressione Ctrl+C novamente para sair</Text>}
344
+ <Box>
345
+ <Text color={gray} dimColor italic>↑↓ para histórico (em breve) • /help para comandos</Text>
346
+ </Box>
347
+ </Box>
348
+ </Box>
349
+ );
350
+ };
351
+
352
+ export async function startInteractive() {
353
+ const config = getConfig();
354
+ if (!config.provider || !config.apiKey) {
355
+ console.log(chalk.yellow('Provedor não configurado. Execute "bimmo config" primeiro.'));
356
+ process.exit(0);
357
+ }
358
+ process.stdout.write('\x1Bc');
359
+ render(<BimmoApp initialConfig={config} />);
310
360
  }