bimmo-cli 2.2.3 → 2.2.8

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/package.json CHANGED
@@ -1,25 +1,13 @@
1
1
  {
2
2
  "name": "bimmo-cli",
3
- "version": "2.2.3",
4
- "description": "🌿 Plataforma de IA universal com Modo Normal, Agentes e Swarms. Suporte a Autocomplete de arquivos, Diffs coloridos e Contexto Inteligente.",
3
+ "version": "2.2.8",
4
+ "description": "🌿 Plataforma de IA universal profissional com Agentes e Swarms. Suporte a Autocomplete real-time, Diffs coloridos e Contexto Inteligente.",
5
5
  "bin": {
6
6
  "bimmo": "bin/bimmo"
7
7
  },
8
8
  "type": "module",
9
9
  "keywords": [
10
- "ai",
11
- "cli",
12
- "openai",
13
- "anthropic",
14
- "gemini",
15
- "grok",
16
- "deepseek",
17
- "openrouter",
18
- "zai",
19
- "agent",
20
- "swarm",
21
- "autocomplete",
22
- "terminal"
10
+ "ai", "cli", "openai", "anthropic", "gemini", "grok", "deepseek", "openrouter", "zai", "agent", "swarm", "terminal"
23
11
  ],
24
12
  "author": "Judah",
25
13
  "license": "MIT",
@@ -41,9 +29,7 @@
41
29
  "conf": "^13.0.0",
42
30
  "diff": "^7.0.0",
43
31
  "figlet": "^1.7.0",
44
- "fuzzy": "^0.1.3",
45
32
  "inquirer": "^9.3.8",
46
- "inquirer-autocomplete-prompt": "^3.0.1",
47
33
  "marked": "^14.0.0",
48
34
  "marked-terminal": "^7.0.0",
49
35
  "mime-types": "^2.1.35",
package/src/agent.js CHANGED
@@ -28,6 +28,7 @@ export const tools = [
28
28
  },
29
29
  execute: async ({ query }) => {
30
30
  if (!tvly) return 'Erro: Chave de API da Tavily não configurada. Use /config para configurar.';
31
+ console.log(chalk.blue(`\n 🌐 Pesquisando na web: ${chalk.bold(query)}...`));
31
32
  const searchResponse = await tvly.search(query, {
32
33
  searchDepth: 'advanced',
33
34
  maxResults: 5
@@ -51,6 +52,7 @@ export const tools = [
51
52
  },
52
53
  execute: async ({ path: filePath }) => {
53
54
  try {
55
+ console.log(chalk.blue(`\n 📖 Lendo arquivo: ${chalk.bold(filePath)}...`));
54
56
  return fs.readFileSync(filePath, 'utf-8');
55
57
  } catch (err) {
56
58
  return `Erro ao ler arquivo: ${err.message}`;
@@ -77,7 +79,7 @@ export const tools = [
77
79
  const differences = diff.diffLines(oldContent, content);
78
80
 
79
81
  console.log(`\n${chalk.cyan('📝 Alterações propostas em:')} ${chalk.bold(filePath)}`);
80
- console.log(chalk.gray('─'.repeat(40)));
82
+ console.log(chalk.gray('─'.repeat(50)));
81
83
 
82
84
  let hasChanges = false;
83
85
  differences.forEach((part) => {
@@ -85,20 +87,25 @@ export const tools = [
85
87
  const color = part.added ? chalk.green : part.removed ? chalk.red : chalk.gray;
86
88
  const prefix = part.added ? '+' : part.removed ? '-' : ' ';
87
89
 
88
- // Mostra apenas linhas com mudanças ou um pouco de contexto
89
90
  if (part.added || part.removed) {
90
- process.stdout.write(color(`${prefix} ${part.value}`));
91
+ // Garante que cada linha tenha o prefixo
92
+ const lines = part.value.split('\n');
93
+ lines.forEach(line => {
94
+ if (line || part.value.endsWith('\n')) {
95
+ process.stdout.write(color(`${prefix} ${line}\n`));
96
+ }
97
+ });
91
98
  } else {
92
99
  // Mostra apenas as primeiras e últimas linhas de blocos sem mudança para encurtar
93
- const lines = part.value.split('\n');
100
+ const lines = part.value.split('\n').filter(l => l.trim() !== "");
94
101
  if (lines.length > 4) {
95
- process.stdout.write(color(` ${lines[0]}\n ...\n ${lines[lines.length-2]}\n`));
96
- } else {
97
- process.stdout.write(color(` ${part.value}`));
102
+ process.stdout.write(color(` ${lines[0]}\n ...\n ${lines[lines.length-1]}\n`));
103
+ } else if (lines.length > 0) {
104
+ lines.forEach(line => process.stdout.write(color(` ${line}\n`)));
98
105
  }
99
106
  }
100
107
  });
101
- console.log(chalk.gray('\n' + '─'.repeat(40)));
108
+ console.log(chalk.gray('─'.repeat(50)));
102
109
 
103
110
  if (!hasChanges) {
104
111
  return "Nenhuma mudança detectada no arquivo.";
@@ -143,7 +150,7 @@ export const tools = [
143
150
  },
144
151
  execute: async ({ command }) => {
145
152
  try {
146
- console.log(`\n${chalk.yellow('⚠️ Comando proposto:')} ${chalk.bold(command)}`);
153
+ console.log(chalk.yellow(`\n Comando proposto: ${chalk.bold(command)}`));
147
154
 
148
155
  if (!editState.autoAccept) {
149
156
  const { approve } = await inquirer.prompt([{
package/src/interface.js CHANGED
@@ -1,8 +1,5 @@
1
1
  import chalk from 'chalk';
2
2
  import figlet from 'figlet';
3
- import inquirer from 'inquirer';
4
- import autocompletePrompt from 'inquirer-autocomplete-prompt';
5
- import fuzzy from 'fuzzy';
6
3
  import { marked } from 'marked';
7
4
  import TerminalRenderer from 'marked-terminal';
8
5
  import ora from 'ora';
@@ -18,18 +15,21 @@ import { getProjectContext } from './project-context.js';
18
15
  import { SwarmOrchestrator } from './orchestrator.js';
19
16
  import { editState } from './agent.js';
20
17
 
21
- // Registrar plugin de autocomplete
22
- inquirer.registerPrompt('autocomplete', autocompletePrompt);
23
-
24
18
  const __filename = fileURLToPath(import.meta.url);
25
19
  const __dirname = path.dirname(__filename);
26
20
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
27
21
  const version = pkg.version;
28
22
 
29
- marked.use(new TerminalRenderer({
23
+ // CONFIGURAÇÃO DO RENDERIZADOR - Ignora HTML e foca no Terminal
24
+ const terminalRenderer = new TerminalRenderer({
30
25
  heading: chalk.hex('#c084fc').bold,
31
26
  code: chalk.hex('#00ff9d'),
32
- }));
27
+ strong: chalk.bold,
28
+ em: chalk.italic,
29
+ html: () => '', // DROP TOTAL DE QUALQUER TAG HTML
30
+ });
31
+
32
+ marked.setOptions({ renderer: terminalRenderer });
33
33
 
34
34
  const green = chalk.hex('#00ff9d');
35
35
  const lavender = chalk.hex('#c084fc');
@@ -42,107 +42,69 @@ let activePersona = null;
42
42
  let exitCounter = 0;
43
43
  let exitTimer = null;
44
44
 
45
+ const i18n = {
46
+ 'pt-BR': {
47
+ welcome: 'Olá! Estou pronto. No que posso ajudar?',
48
+ thinking: 'bimmo pensando...',
49
+ interrupted: 'Operação interrompida.',
50
+ exitHint: '(Pressione Ctrl+C novamente para sair)',
51
+ switchOk: 'Perfil ativado!',
52
+ agentOk: 'Agente ativado:',
53
+ modeEdit: 'Modo EDIT ativado.',
54
+ help: '\nComandos:\n /chat | /plan | /edit | /init\n /switch [nome] | /model [nome]\n /use [agente] | /use normal\n /config | /clear | @arquivo\n'
55
+ },
56
+ 'en-US': {
57
+ welcome: 'Hello! I am ready. How can I help you?',
58
+ thinking: 'bimmo thinking...',
59
+ interrupted: 'Operation interrupted.',
60
+ exitHint: '(Press Ctrl+C again to exit)',
61
+ switchOk: 'Profile activated!',
62
+ agentOk: 'Agent activated:',
63
+ modeEdit: 'EDIT mode activated.',
64
+ help: '\nCommands:\n /chat | /plan | /edit | /init\n /switch [name] | /model [name]\n /use [agent] | /use normal\n /config | /clear | @file\n'
65
+ }
66
+ };
67
+
45
68
  /**
46
- * Procura arquivos e diretórios recursivamente para o autocomplete do @
69
+ * Coleta arquivos para preview e completion
47
70
  */
48
- function getFiles(dir, filter = '') {
49
- const results = [];
71
+ function getFilesForPreview(partialPath) {
50
72
  try {
51
- const list = fs.readdirSync(dir);
52
- for (const file of list) {
53
- if (file === 'node_modules' || file === '.git') continue;
54
- const fullPath = path.join(dir, file);
55
- const relPath = path.relative(process.cwd(), fullPath);
56
- if (relPath.toLowerCase().includes(filter.toLowerCase())) {
57
- results.push(relPath);
58
- }
59
- if (fs.statSync(fullPath).isDirectory()) {
60
- // Limitamos profundidade para performance se necessário
61
- }
62
- }
63
- } catch (e) {}
64
- return results;
65
- }
66
-
67
- async function processInput(input) {
68
- const parts = input.split(' ');
69
- const processedContent = [];
70
-
71
- for (const part of parts) {
72
- if (part.startsWith('@')) {
73
- const filePath = part.slice(1);
74
- try {
75
- if (fs.existsSync(filePath)) {
76
- const stats = fs.statSync(filePath);
77
- if (stats.isFile()) {
78
- const mimeType = mime.lookup(filePath) || 'application/octet-stream';
79
- if (mimeType.startsWith('image/')) {
80
- const base64Image = fs.readFileSync(filePath, { encoding: 'base64' });
81
- processedContent.push({ type: 'image', mimeType, data: base64Image, fileName: path.basename(filePath) });
82
- } else {
83
- const textContent = fs.readFileSync(filePath, 'utf-8');
84
- processedContent.push({ type: 'text', text: `\n--- Arquivo: ${path.basename(filePath)} ---\n${textContent}\n--- Fim do arquivo ---\n` });
85
- }
86
- }
87
- } else {
88
- processedContent.push({ type: 'text', text: part });
89
- }
90
- } catch (err) {
91
- processedContent.push({ type: 'text', text: part });
92
- }
93
- } else {
94
- processedContent.push({ type: 'text', text: part });
95
- }
96
- }
97
-
98
- const hasImage = processedContent.some(c => c.type === 'image');
99
- if (!hasImage) return processedContent.map(c => c.text).join(' ');
100
-
101
- const finalContent = [];
102
- let currentText = "";
103
- for (const item of processedContent) {
104
- if (item.type === 'text') {
105
- currentText += (currentText ? " " : "") + item.text;
106
- } else {
107
- if (currentText) { finalContent.push({ type: 'text', text: currentText }); currentText = ""; }
108
- finalContent.push(item);
109
- }
110
- }
111
- if (currentText) finalContent.push({ type: 'text', text: currentText });
112
- return finalContent;
113
- }
114
-
115
- function getModeStyle() {
116
- const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]` : '';
117
- switch (currentMode) {
118
- case 'plan': return yellow.bold(`${personaLabel}[PLAN] `);
119
- case 'edit':
120
- const editSubMode = editState.autoAccept ? '(AUTO)' : '(MANUAL)';
121
- return chalk.red.bold(`${personaLabel}[EDIT${editSubMode}] `);
122
- default: return lavender.bold(`${personaLabel}[CHAT] `);
123
- }
73
+ const cleanPartial = partialPath.startsWith('@') ? partialPath.slice(1) : partialPath;
74
+ const dir = path.dirname(cleanPartial) === '.' && !cleanPartial.includes('/') ? '.' : path.dirname(cleanPartial);
75
+ const base = path.basename(cleanPartial);
76
+
77
+ const searchDir = path.resolve(process.cwd(), dir);
78
+ if (!fs.existsSync(searchDir)) return [];
79
+
80
+ const files = fs.readdirSync(searchDir);
81
+ return files
82
+ .filter(f => (base === '' || f.startsWith(base)) && !f.startsWith('.') && f !== 'node_modules')
83
+ .map(f => {
84
+ const rel = path.join(dir === '.' ? '' : dir, f);
85
+ const isDir = fs.statSync(path.join(searchDir, f)).isDirectory();
86
+ return { name: rel, isDir };
87
+ });
88
+ } catch (e) { return []; }
124
89
  }
125
90
 
126
91
  function cleanAIResponse(text) {
127
92
  if (!text) return "";
128
93
  return text
129
94
  .replace(/<br\s*\/?>/gi, '\n')
95
+ .replace(/<p>/gi, '')
130
96
  .replace(/<\/p>/gi, '\n\n')
131
- .replace(/<\/div>/gi, '\n')
132
97
  .replace(/<[^>]*>?/gm, '')
133
- .replace(/&nbsp;/g, ' ')
134
- .replace(/&lt;/g, '<')
135
- .replace(/&gt;/g, '>')
136
- .replace(/&amp;/g, '&')
137
98
  .trim();
138
99
  }
139
100
 
140
101
  export async function startInteractive() {
141
102
  let config = getConfig();
103
+ const lang = config.language || 'pt-BR';
104
+ const t = i18n[lang] || i18n['pt-BR'];
142
105
 
143
106
  if (!config.provider || !config.apiKey) {
144
107
  console.log(lavender(figlet.textSync('bimmo')));
145
- console.log(gray('\nBem-vindo! Vamos configurar seus perfis de IA.\n'));
146
108
  await configure();
147
109
  return startInteractive();
148
110
  }
@@ -153,11 +115,10 @@ export async function startInteractive() {
153
115
 
154
116
  const resetMessages = () => {
155
117
  messages = [];
156
- const projectContext = getProjectContext();
157
- messages.push({ role: 'system', content: projectContext });
118
+ messages.push({ role: 'system', content: getProjectContext() });
158
119
  if (activePersona) {
159
120
  const agent = (config.agents || {})[activePersona];
160
- if (agent) messages.push({ role: 'system', content: `Sua persona atual é: ${agent.name}. Sua tarefa: ${agent.role}` });
121
+ if (agent) messages.push({ role: 'system', content: `Persona: ${agent.name}. Task: ${agent.role}` });
161
122
  }
162
123
  };
163
124
 
@@ -168,157 +129,194 @@ export async function startInteractive() {
168
129
  console.log(lavender(` v${version} `.padStart(60, '─')));
169
130
  console.log(green(` Perfil: ${bold(config.activeProfile || 'Padrão')} • IA: ${bold(config.provider.toUpperCase())}`));
170
131
  console.log(green(` Modelo: ${bold(config.model)}`));
171
- console.log(gray(` 📁 ${process.cwd()}`));
172
- console.log(gray(' /chat | /plan | /edit | /swarm | /use [agente] | /help'));
173
132
  console.log(lavender('─'.repeat(60)) + '\n');
174
133
 
175
- console.log(lavender('👋 Olá! Estou pronto. No que posso ajudar?\n'));
134
+ console.log(lavender(`👋 ${t.welcome}\n`));
135
+
136
+ const rl = readline.createInterface({
137
+ input: process.stdin,
138
+ output: process.stdout,
139
+ terminal: true,
140
+ historySize: 100,
141
+ completer: (line) => {
142
+ const words = line.split(' ');
143
+ const lastWord = words[words.length - 1];
144
+ if (lastWord.startsWith('@')) {
145
+ const hits = getFilesForPreview(lastWord).map(h => `@${h.name}`);
146
+ return [hits, lastWord];
147
+ }
148
+ return [[], line];
149
+ }
150
+ });
176
151
 
177
- const globalSigIntHandler = () => {
178
- exitCounter++;
179
- if (exitCounter === 1) {
180
- process.stdout.write(gray('\n(Pressione Ctrl+C novamente para sair)\n'));
181
- if (exitTimer) clearTimeout(exitTimer);
152
+ let currentPreviewLines = 0;
153
+
154
+ const clearPreview = () => {
155
+ for (let i = 0; i < currentPreviewLines; i++) {
156
+ readline.moveCursor(process.stdout, 0, 1);
157
+ readline.clearLine(process.stdout, 0);
158
+ }
159
+ if (currentPreviewLines > 0) {
160
+ readline.moveCursor(process.stdout, 0, -currentPreviewLines);
161
+ }
162
+ currentPreviewLines = 0;
163
+ };
164
+
165
+ const showPreview = (line) => {
166
+ clearPreview();
167
+ const words = 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
+ process.stdout.write('\n');
174
+ files.slice(0, 10).forEach(f => {
175
+ process.stdout.write(gray(` ${f.isDir ? '📁' : '📄'} ${f.name}\n`));
176
+ });
177
+ currentPreviewLines = Math.min(files.length, 10) + 1;
178
+ readline.moveCursor(process.stdout, 0, -currentPreviewLines);
179
+ readline.cursorTo(process.stdout, rl.line.length + rl.getPrompt().length);
180
+ }
181
+ }
182
+ };
183
+
184
+ // Monitora digitação em tempo real
185
+ process.stdin.on('keypress', (s, key) => {
186
+ // Pequeno delay para o readline atualizar a linha interna
187
+ setImmediate(() => showPreview(rl.line));
188
+ });
189
+
190
+ rl.on('SIGINT', () => {
191
+ if (exitCounter === 0) {
192
+ exitCounter++;
193
+ console.log(`\n${gray(t.exitHint)}`);
182
194
  exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
195
+ displayPrompt();
183
196
  } else {
184
- process.stdout.write(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
185
197
  process.exit(0);
186
198
  }
187
- };
199
+ });
188
200
 
189
- process.on('SIGINT', globalSigIntHandler);
190
- readline.emitKeypressEvents(process.stdin);
191
- if (process.stdin.isTTY) process.stdin.setRawMode(true);
192
-
193
- while (true) {
194
- const modeIndicator = getModeStyle();
195
- let input;
201
+ const displayPrompt = () => {
202
+ const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]` : '';
203
+ let modeLabel = `[${currentMode.toUpperCase()}]`;
204
+ if (currentMode === 'edit') modeLabel = editState.autoAccept ? '[EDIT(AUTO)]' : '[EDIT(MANUAL)]';
196
205
 
197
- try {
198
- // Usamos autocomplete para permitir navegação de arquivos com @
199
- const answers = await inquirer.prompt([
200
- {
201
- type: 'autocomplete',
202
- name: 'input',
203
- message: modeIndicator + green('>'),
204
- prefix: '',
205
- source: async (answersSoFar, inputSearch) => {
206
- const currentInput = inputSearch || '';
207
-
208
- // Se o usuário digitar @, oferecemos arquivos
209
- if (currentInput.includes('@')) {
210
- const lastWord = currentInput.split(' ').pop();
211
- if (lastWord.startsWith('@')) {
212
- const searchPath = lastWord.slice(1);
213
- const files = getFiles(process.cwd(), searchPath);
214
- return files.map(f => ({
215
- name: `@${f} ${fs.statSync(f).isDirectory() ? '(DIR)' : '(FILE)'}`,
216
- value: currentInput.substring(0, currentInput.lastIndexOf('@')) + '@' + f
217
- }));
218
- }
219
- }
220
-
221
- // Caso contrário, apenas retorna o que ele está digitando como única opção
222
- // para não atrapalhar o chat normal
223
- return [currentInput];
224
- }
225
- }
226
- ]);
227
- input = answers.input;
228
- } catch (e) { continue; }
229
-
230
- // Diretório constante
231
- console.log(gray(` 📁 ${process.cwd()}`));
206
+ console.log(`${gray(`📁 ${process.cwd()}`)}`);
207
+ rl.setPrompt(lavender.bold(personaLabel) + (currentMode === 'edit' ? chalk.red.bold(modeLabel) : lavender.bold(modeLabel)) + green(' > '));
208
+ rl.prompt();
209
+ };
210
+
211
+ displayPrompt();
232
212
 
213
+ rl.on('line', async (input) => {
214
+ clearPreview();
233
215
  const rawInput = input.trim();
234
216
  const cmd = rawInput.toLowerCase();
235
217
 
236
- if (cmd === '/exit' || cmd === 'exit' || cmd === 'sair') { process.exit(0); }
237
- if (cmd === '/chat') { currentMode = 'chat'; console.log(lavender('✓ Modo CHAT.\n')); continue; }
238
- if (cmd === '/plan') { currentMode = 'plan'; console.log(yellow('✓ Modo PLAN.\n')); continue; }
239
- if (cmd === '/edit') { currentMode = 'edit'; console.log(chalk.red(`⚠️ Modo EDIT ativado.\n`)); continue; }
240
- if (cmd === '/edit auto') { currentMode = 'edit'; editState.autoAccept = true; console.log(chalk.red('⚠️ Modo EDIT (AUTO) ativado.\n')); continue; }
241
- if (cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false; console.log(chalk.red('⚠️ Modo EDIT (MANUAL) ativado.\n')); continue; }
218
+ if (rawInput === '') { displayPrompt(); return; }
219
+
220
+ if (cmd === '/exit' || cmd === 'sair') process.exit(0);
221
+ if (cmd === '/chat') { currentMode = 'chat'; displayPrompt(); return; }
222
+ if (cmd === '/plan') { currentMode = 'plan'; displayPrompt(); return; }
223
+ if (cmd === '/edit' || cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false; displayPrompt(); return; }
224
+ if (cmd === '/edit auto') { currentMode = 'edit'; editState.autoAccept = true; displayPrompt(); return; }
225
+ if (cmd === '/clear') { resetMessages(); console.clear(); displayPrompt(); return; }
226
+ if (cmd === '/help') { console.log(gray(t.help)); displayPrompt(); return; }
242
227
 
243
228
  if (cmd === '/init') {
244
- const bimmoRcPath = path.join(process.cwd(), '.bimmorc.json');
245
- if (fs.existsSync(bimmoRcPath)) {
246
- const { overwrite } = await inquirer.prompt([{ type: 'confirm', name: 'overwrite', message: 'O arquivo .bimmorc.json já existe. Sobrescrever?', default: false }]);
247
- if (!overwrite) continue;
248
- }
249
- const initialConfig = { projectName: path.basename(process.cwd()), rules: ["Siga as convenções."], ignorePatterns: ["node_modules", ".git"] };
250
- fs.writeFileSync(bimmoRcPath, JSON.stringify(initialConfig, null, 2));
251
- console.log(green(`\n✅ .bimmorc.json criado com sucesso.\n`));
229
+ console.log(chalk.cyan('\n🚀 Analisando projeto para gerar .bimmorc.json inteligente...\n'));
230
+ const initPrompt = `Analise a estrutura atual deste projeto e crie um arquivo chamado .bimmorc.json na raiz com nome, regras, stack e arquitetura. Use write_file.`;
231
+
232
+ const controller = new AbortController();
233
+ const spinner = ora({ text: lavender(`${t.thinking}`), color: 'red' }).start();
234
+ try {
235
+ const res = await provider.sendMessage([...messages, { role: 'user', content: initPrompt }], { signal: controller.signal });
236
+ spinner.stop();
237
+ console.log(marked.parse(cleanAIResponse(res)));
238
+ } catch (e) { spinner.stop(); console.error(chalk.red(e.message)); }
252
239
  resetMessages();
253
- continue;
240
+ displayPrompt();
241
+ return;
242
+ }
243
+
244
+ if (cmd === '/config') {
245
+ rl.pause(); await configure(); config = getConfig(); provider = createProvider(config); rl.resume();
246
+ displayPrompt(); return;
254
247
  }
255
248
 
256
249
  if (cmd.startsWith('/switch ')) {
257
- const profileName = rawInput.split(' ')[1];
258
- if (profileName && switchProfile(profileName)) {
250
+ const pName = rawInput.split(' ')[1];
251
+ if (switchProfile(pName)) {
259
252
  config = getConfig(); provider = createProvider(config);
260
- console.log(green(`\n✓ Perfil "${bold(profileName)}" ativado!`));
261
- continue;
262
- }
263
- console.log(chalk.red(`\n✖ Perfil não encontrado.\n`)); continue;
253
+ console.log(green(`\n✓ ${t.switchOk}`));
254
+ } else { console.log(chalk.red(`\n✖ Perfil não encontrado.`)); }
255
+ displayPrompt(); return;
264
256
  }
265
257
 
266
258
  if (cmd.startsWith('/use ')) {
267
- const agentName = rawInput.split(' ')[1];
259
+ const aName = rawInput.split(' ')[1];
260
+ if (aName === 'normal') { activePersona = null; resetMessages(); displayPrompt(); return; }
268
261
  const agents = config.agents || {};
269
- if (agentName === 'normal' || agentName === 'default') { activePersona = null; resetMessages(); continue; }
270
- if (agents[agentName]) {
271
- activePersona = agentName;
272
- const agent = agents[agentName];
262
+ if (agents[aName]) {
263
+ activePersona = aName;
264
+ const agent = agents[aName];
273
265
  if (switchProfile(agent.profile)) { config = getConfig(); provider = createProvider(config); }
274
266
  currentMode = agent.mode || 'chat';
275
- console.log(green(`\n✓ Ativado Agente: ${bold(agentName)}`));
267
+ console.log(green(`\n✓ ${t.agentOk} ${bold(aName)}`));
276
268
  resetMessages();
277
- } else { console.log(chalk.red(`\n✖ Agente não encontrado.\n`)); }
278
- continue;
269
+ } else { console.log(chalk.red(`\n✖ Agente não encontrado.`)); }
270
+ displayPrompt(); return;
279
271
  }
280
272
 
281
- if (cmd === '/clear') { resetMessages(); console.clear(); continue; }
282
-
283
- if (cmd === '/help') {
284
- console.log(gray(`\nComandos:\n /chat | /plan | /edit [auto/manual] | /init\n /switch [nome] | /model [nome] | /use [agente]\n /config | /clear | @arquivo\n`));
285
- continue;
286
- }
287
-
288
- if (cmd === '/config') { await configure(); config = getConfig(); provider = createProvider(config); continue; }
289
-
290
- if (rawInput === '') continue;
291
-
273
+ // PROCESSAMENTO IA
292
274
  const controller = new AbortController();
293
- const localInterruptHandler = () => controller.abort();
294
- process.removeListener('SIGINT', globalSigIntHandler);
295
- process.on('SIGINT', localInterruptHandler);
275
+ const abortHandler = () => controller.abort();
276
+
277
+ // Remove temporariamente o listener de preview para não conflitar com a saída da IA
278
+ process.stdin.removeAllListeners('keypress');
279
+ process.on('SIGINT', abortHandler);
296
280
 
297
281
  let modeInstr = "";
298
282
  if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Apenas analise.";
299
283
  else if (currentMode === 'edit') modeInstr = `\n[MODO EDIT] Auto-Accept: ${editState.autoAccept ? 'ON' : 'OFF'}`;
300
284
 
301
- const content = await processInput(rawInput);
302
- messages.push({ role: 'user', content: typeof content === 'string' ? content + modeInstr : [...content, { type: 'text', text: modeInstr }] });
285
+ const processedContent = [];
286
+ const words = rawInput.split(' ');
287
+ for (const word of words) {
288
+ if (word.startsWith('@')) {
289
+ const filePath = word.slice(1);
290
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
291
+ const content = fs.readFileSync(filePath, 'utf-8');
292
+ processedContent.push({ type: 'text', text: `\n[ARQUIVO: ${filePath}]\n${content}\n` });
293
+ } else { processedContent.push({ type: 'text', text: word }); }
294
+ } else { processedContent.push({ type: 'text', text: word }); }
295
+ }
303
296
 
304
- const spinner = ora({ text: lavender(`bimmo pensando... (Ctrl+C para interromper)`), color: currentMode === 'edit' ? 'red' : 'magenta' }).start();
297
+ messages.push({ role: 'user', content: [...processedContent, { type: 'text', text: modeInstr }] });
298
+ const spinner = ora({ text: lavender(`${t.thinking} (Ctrl+C para parar)`), color: currentMode === 'edit' ? 'red' : 'magenta' }).start();
305
299
 
306
300
  try {
307
301
  let responseText = await provider.sendMessage(messages, { signal: controller.signal });
308
302
  spinner.stop();
309
303
  const cleanedText = cleanAIResponse(responseText);
310
304
  messages.push({ role: 'assistant', content: responseText });
311
- console.log('\n' + lavender('bimmo ') + getModeStyle());
305
+ console.log(`\n${lavender('bimmo ')}${currentMode.toUpperCase()}`);
312
306
  console.log(lavender('─'.repeat(50)));
313
- console.log(marked(cleanedText));
314
- console.log(gray('─'.repeat(50)) + '\n');
307
+ process.stdout.write(marked.parse(cleanedText));
308
+ console.log(gray('\n' + '─'.repeat(50)));
315
309
  } catch (err) {
316
310
  spinner.stop();
317
- if (controller.signal.aborted || err.name === 'AbortError') { console.log(yellow('\n⚠️ Interrompido.\n')); messages.pop(); }
318
- else { console.error(chalk.red('\n✖ Erro:') + ' ' + err.message + '\n'); }
311
+ if (controller.signal.aborted) { console.log(yellow(`\n⚠️ ${t.interrupted}`)); messages.pop(); }
312
+ else { console.error(chalk.red(`\n✖ Erro: ${err.message}`)); }
319
313
  } finally {
320
- process.removeListener('SIGINT', localInterruptHandler);
321
- process.on('SIGINT', globalSigIntHandler);
314
+ process.removeListener('SIGINT', abortHandler);
315
+ // Reativa o listener de preview
316
+ process.stdin.on('keypress', (s, key) => {
317
+ setImmediate(() => showPreview(rl.line));
318
+ });
319
+ displayPrompt();
322
320
  }
323
- }
321
+ });
324
322
  }
@@ -18,6 +18,7 @@ export class AnthropicProvider extends BaseProvider {
18
18
  type: 'image',
19
19
  source: { type: 'base64', media_type: part.mimeType, data: part.data }
20
20
  };
21
+ return part;
21
22
  });
22
23
  }
23
24
 
@@ -51,8 +52,6 @@ export class AnthropicProvider extends BaseProvider {
51
52
 
52
53
  if (tool) {
53
54
  if (options.signal?.aborted) throw new Error('Abortado pelo usuário');
54
-
55
- console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
56
55
  const result = await tool.execute(toolUse.input);
57
56
 
58
57
  const nextMessages = [
@@ -15,6 +15,7 @@ export class GeminiProvider extends BaseProvider {
15
15
  if (part.type === 'image') return {
16
16
  inlineData: { mimeType: part.mimeType, data: part.data }
17
17
  };
18
+ return part;
18
19
  });
19
20
  }
20
21
 
@@ -58,7 +59,6 @@ export class GeminiProvider extends BaseProvider {
58
59
 
59
60
  const tool = tools.find(t => t.name === call.functionCall.name);
60
61
  if (tool) {
61
- console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
62
62
  const toolResult = await tool.execute(call.functionCall.args);
63
63
 
64
64
  const resultResponse = await chat.sendMessage([{
@@ -21,12 +21,8 @@ export class OpenAIProvider extends BaseProvider {
21
21
 
22
22
  formatMessages(messages) {
23
23
  return messages.map(msg => {
24
- // Se content for string ou null (comum em tool calls), retorna como está
25
- if (typeof msg.content === 'string' || msg.content === null) {
26
- return msg;
27
- }
24
+ if (typeof msg.content === 'string' || msg.content === null) return msg;
28
25
 
29
- // Se for um array (multimodal), processa as partes
30
26
  if (Array.isArray(msg.content)) {
31
27
  const content = msg.content.map(part => {
32
28
  if (part.type === 'text') return { type: 'text', text: part.text };
@@ -34,11 +30,10 @@ export class OpenAIProvider extends BaseProvider {
34
30
  type: 'image_url',
35
31
  image_url: { url: `data:${part.mimeType};base64,${part.data}` }
36
32
  };
37
- return part; // Mantém outras partes (como tool_result se houver)
33
+ return part;
38
34
  });
39
35
  return { ...msg, content };
40
36
  }
41
-
42
37
  return msg;
43
38
  });
44
39
  }
@@ -77,7 +72,6 @@ export class OpenAIProvider extends BaseProvider {
77
72
 
78
73
  const tool = tools.find(t => t.name === toolCall.function.name);
79
74
  if (tool) {
80
- console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
81
75
  const args = JSON.parse(toolCall.function.arguments);
82
76
  const result = await tool.execute(args);
83
77