bimmo-cli 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bimmo-cli",
3
- "version": "2.0.1",
4
- "description": "🌿 Plataforma de Orquestração de Agentes IA (Swarms) com interface verde/lavanda, modo Auto-Edit e contexto inteligente.",
3
+ "version": "2.1.0",
4
+ "description": "🌿 Plataforma de IA universal com Modo Normal, Agentes e Swarms. Suporte a Diffs coloridos, Auto-Edit (Auto/Manual) e Contexto Inteligente.",
5
5
  "bin": {
6
6
  "bimmo": "bin/bimmo"
7
7
  },
@@ -18,7 +18,7 @@
18
18
  "zai",
19
19
  "agent",
20
20
  "swarm",
21
- "orchestration",
21
+ "diff",
22
22
  "multimodal",
23
23
  "terminal"
24
24
  ],
@@ -40,6 +40,7 @@
40
40
  "chalk": "^5.3.0",
41
41
  "commander": "^12.1.0",
42
42
  "conf": "^13.0.0",
43
+ "diff": "^7.0.0",
43
44
  "figlet": "^1.7.0",
44
45
  "inquirer": "^10.1.0",
45
46
  "marked": "^14.0.0",
package/src/agent.js CHANGED
@@ -3,10 +3,18 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { execSync } from 'child_process';
5
5
  import { getConfig } from './config.js';
6
+ import * as diff from 'diff';
7
+ import chalk from 'chalk';
8
+ import inquirer from 'inquirer';
6
9
 
7
10
  const config = getConfig();
8
11
  const tvly = config.tavilyKey ? tavily({ apiKey: config.tavilyKey }) : null;
9
12
 
13
+ // Estado global para controle de edições (temporário na sessão)
14
+ export const editState = {
15
+ autoAccept: false
16
+ };
17
+
10
18
  export const tools = [
11
19
  {
12
20
  name: 'search_internet',
@@ -55,17 +63,69 @@ export const tools = [
55
63
  parameters: {
56
64
  type: 'object',
57
65
  properties: {
58
- path: { type: 'string', description: 'Camin de destino' },
66
+ path: { type: 'string', description: 'Caminho de destino' },
59
67
  content: { type: 'string', description: 'Conteúdo do arquivo' }
60
68
  },
61
69
  required: ['path', 'content']
62
70
  },
63
71
  execute: async ({ path: filePath, content }) => {
64
72
  try {
65
- const dir = path.dirname(filePath);
73
+ const absolutePath = path.resolve(filePath);
74
+ const oldContent = fs.existsSync(absolutePath) ? fs.readFileSync(absolutePath, 'utf-8') : "";
75
+
76
+ // Gerar Diff
77
+ const differences = diff.diffLines(oldContent, content);
78
+
79
+ console.log(`\n${chalk.cyan('📝 Alterações propostas em:')} ${chalk.bold(filePath)}`);
80
+ console.log(chalk.gray('─'.repeat(40)));
81
+
82
+ let hasChanges = false;
83
+ differences.forEach((part) => {
84
+ if (part.added || part.removed) hasChanges = true;
85
+ const color = part.added ? chalk.green : part.removed ? chalk.red : chalk.gray;
86
+ const prefix = part.added ? '+' : part.removed ? '-' : ' ';
87
+
88
+ // Mostra apenas linhas com mudanças ou um pouco de contexto
89
+ if (part.added || part.removed) {
90
+ process.stdout.write(color(`${prefix} ${part.value}`));
91
+ } else {
92
+ // Mostra apenas as primeiras e últimas linhas de blocos sem mudança para encurtar
93
+ const lines = part.value.split('\n');
94
+ 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}`));
98
+ }
99
+ }
100
+ });
101
+ console.log(chalk.gray('\n' + '─'.repeat(40)));
102
+
103
+ if (!hasChanges) {
104
+ return "Nenhuma mudança detectada no arquivo.";
105
+ }
106
+
107
+ // Lógica de Aprovação
108
+ if (!editState.autoAccept) {
109
+ const { approve } = await inquirer.prompt([{
110
+ type: 'list',
111
+ name: 'approve',
112
+ message: 'Deseja aplicar estas alterações?',
113
+ choices: [
114
+ { name: '✅ Sim', value: 'yes' },
115
+ { name: '❌ Não', value: 'no' },
116
+ { name: '⚡ Sim para tudo (Auto-Accept)', value: 'all' }
117
+ ]
118
+ }]);
119
+
120
+ if (approve === 'no') return "Alteração rejeitada pelo usuário.";
121
+ if (approve === 'all') editState.autoAccept = true;
122
+ }
123
+
124
+ const dir = path.dirname(absolutePath);
66
125
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
67
- fs.writeFileSync(filePath, content);
68
- return `Arquivo ${filePath} criado com sucesso.`;
126
+ fs.writeFileSync(absolutePath, content);
127
+
128
+ return `Arquivo ${filePath} atualizado com sucesso.`;
69
129
  } catch (err) {
70
130
  return `Erro ao escrever arquivo: ${err.message}`;
71
131
  }
@@ -83,8 +143,26 @@ export const tools = [
83
143
  },
84
144
  execute: async ({ command }) => {
85
145
  try {
86
- const output = execSync(command, { encoding: 'utf-8', timeout: 30000 });
87
- return output || 'Comando executado sem retorno visual.';
146
+ console.log(`\n${chalk.yellow('⚠️ Comando proposto:')} ${chalk.bold(command)}`);
147
+
148
+ if (!editState.autoAccept) {
149
+ const { approve } = await inquirer.prompt([{
150
+ type: 'list',
151
+ name: 'approve',
152
+ message: 'Executar este comando?',
153
+ choices: [
154
+ { name: '✅ Sim', value: 'yes' },
155
+ { name: '❌ Não', value: 'no' },
156
+ { name: '⚡ Sim para tudo (Auto-Accept)', value: 'all' }
157
+ ]
158
+ }]);
159
+
160
+ if (approve === 'no') return "Comando rejeitado pelo usuário.";
161
+ if (approve === 'all') editState.autoAccept = true;
162
+ }
163
+
164
+ const output = execSync(command, { encoding: 'utf-8', timeout: 60000 });
165
+ return output || 'Comando executado com sucesso (sem retorno).';
88
166
  } catch (err) {
89
167
  return `Erro ao executar comando: ${err.stderr || err.message}`;
90
168
  }
@@ -97,7 +175,6 @@ export async function handleToolCalls(toolCalls) {
97
175
  for (const call of toolCalls) {
98
176
  const tool = tools.find(t => t.name === call.name);
99
177
  if (tool) {
100
- console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
101
178
  const result = await tool.execute(call.args);
102
179
  results.push({
103
180
  callId: call.id,
package/src/interface.js CHANGED
@@ -13,6 +13,7 @@ import { getConfig, configure, updateActiveModel, switchProfile } from './config
13
13
  import { createProvider } from './providers/factory.js';
14
14
  import { getProjectContext } from './project-context.js';
15
15
  import { SwarmOrchestrator } from './orchestrator.js';
16
+ import { editState } from './agent.js';
16
17
 
17
18
  marked.use(new TerminalRenderer({
18
19
  heading: chalk.hex('#c084fc').bold,
@@ -25,7 +26,10 @@ const gray = chalk.gray;
25
26
  const bold = chalk.bold;
26
27
  const yellow = chalk.yellow;
27
28
 
28
- let currentMode = 'chat'; // 'chat', 'plan', 'edit'
29
+ let currentMode = 'chat';
30
+ let activePersona = null;
31
+ let exitCounter = 0;
32
+ let exitTimer = null;
29
33
 
30
34
  async function processInput(input) {
31
35
  const parts = input.split(' ');
@@ -93,10 +97,13 @@ async function processInput(input) {
93
97
  }
94
98
 
95
99
  function getModeStyle() {
100
+ const personaLabel = activePersona ? `[${activePersona.toUpperCase()}] ` : '';
96
101
  switch (currentMode) {
97
- case 'plan': return yellow.bold(' [PLAN] ');
98
- case 'edit': return chalk.red.bold(' [EDIT] ');
99
- default: return lavender.bold(' [CHAT] ');
102
+ case 'plan': return yellow.bold(`${personaLabel}[PLAN] `);
103
+ case 'edit':
104
+ const editSubMode = editState.autoAccept ? '(AUTO)' : '(MANUAL)';
105
+ return chalk.red.bold(`${personaLabel}[EDIT${editSubMode}] `);
106
+ default: return lavender.bold(`${personaLabel}[CHAT] `);
100
107
  }
101
108
  }
102
109
 
@@ -112,37 +119,64 @@ export async function startInteractive() {
112
119
 
113
120
  let provider = createProvider(config);
114
121
  const orchestrator = new SwarmOrchestrator(config);
115
- const messages = [];
122
+ let messages = [];
123
+
124
+ const resetMessages = () => {
125
+ messages = [];
126
+ const projectContext = getProjectContext();
127
+ messages.push({ role: 'system', content: projectContext });
128
+ if (activePersona) {
129
+ const agent = (config.agents || {})[activePersona];
130
+ if (agent) {
131
+ messages.push({ role: 'system', content: `Sua persona atual é: ${agent.name}. Sua tarefa: ${agent.role}` });
132
+ }
133
+ }
134
+ };
116
135
 
117
- const projectContext = getProjectContext();
118
- messages.push({
119
- role: 'system',
120
- content: projectContext
121
- });
136
+ resetMessages();
122
137
 
123
138
  console.clear();
124
139
  console.log(lavender(figlet.textSync('bimmo')));
125
140
  console.log(lavender('─'.repeat(60)));
126
- console.log(green(` Perfil Ativo: ${bold(config.activeProfile || 'Padrão')} (${config.provider.toUpperCase()})`));
141
+ console.log(green(` Perfil: ${bold(config.activeProfile || 'Padrão')} • IA: ${bold(config.provider.toUpperCase())}`));
127
142
  console.log(green(` Modelo: ${bold(config.model)}`));
128
- console.log(gray(' /chat | /plan | /edit | /swarm | /switch [perfil] | /model [novo] | /help'));
143
+ console.log(gray(' /chat | /plan | /edit | /swarm | /use [agente] | /help'));
129
144
  console.log(lavender('─'.repeat(60)) + '\n');
130
145
 
131
- console.log(lavender('👋 Olá! Sou seu agente BIMMO. No que posso atuar?\n'));
146
+ console.log(lavender('👋 Olá! Estou pronto. No que posso ajudar?\n'));
147
+
148
+ process.on('SIGINT', () => {
149
+ exitCounter++;
150
+ if (exitCounter === 1) {
151
+ console.log(gray('\n(Pressione Ctrl+C novamente para sair)'));
152
+ if (exitTimer) clearTimeout(exitTimer);
153
+ exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
154
+ } else {
155
+ console.log(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
156
+ process.exit(0);
157
+ }
158
+ });
132
159
 
133
160
  readline.emitKeypressEvents(process.stdin);
134
161
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
135
162
 
136
163
  while (true) {
137
164
  const modeIndicator = getModeStyle();
138
- const { input } = await inquirer.prompt([
139
- {
140
- type: 'input',
141
- name: 'input',
142
- message: modeIndicator + green('Você'),
143
- prefix: '',
144
- }
145
- ]);
165
+ let input;
166
+
167
+ try {
168
+ const answers = await inquirer.prompt([
169
+ {
170
+ type: 'input',
171
+ name: 'input',
172
+ message: modeIndicator + green('Você'),
173
+ prefix: '→',
174
+ }
175
+ ]);
176
+ input = answers.input;
177
+ } catch (e) {
178
+ continue;
179
+ }
146
180
 
147
181
  const rawInput = input.trim();
148
182
  const cmd = rawInput.toLowerCase();
@@ -154,35 +188,65 @@ export async function startInteractive() {
154
188
 
155
189
  if (cmd === '/chat') { currentMode = 'chat'; console.log(lavender('✓ Modo CHAT.\n')); continue; }
156
190
  if (cmd === '/plan') { currentMode = 'plan'; console.log(yellow('✓ Modo PLAN.\n')); continue; }
157
- if (cmd === '/edit') { currentMode = 'edit'; console.log(chalk.red('⚠️ Modo EDIT.\n')); continue; }
191
+
192
+ if (cmd === '/edit') {
193
+ currentMode = 'edit';
194
+ console.log(chalk.red(`⚠️ Modo EDIT ativado (Sub-modo atual: ${editState.autoAccept ? 'AUTO' : 'MANUAL'}).\n`));
195
+ continue;
196
+ }
197
+ if (cmd === '/edit auto') {
198
+ currentMode = 'edit';
199
+ editState.autoAccept = true;
200
+ console.log(chalk.red('⚠️ Modo EDIT (AUTO) ativado. Mudanças serão aplicadas sem perguntar.\n'));
201
+ continue;
202
+ }
203
+ if (cmd === '/edit manual') {
204
+ currentMode = 'edit';
205
+ editState.autoAccept = false;
206
+ console.log(chalk.red('⚠️ Modo EDIT (MANUAL) ativado. Pedirei permissão para cada mudança.\n'));
207
+ continue;
208
+ }
158
209
 
159
210
  if (cmd.startsWith('/switch ')) {
160
211
  const profileName = rawInput.split(' ')[1];
161
212
  if (profileName && switchProfile(profileName)) {
162
213
  config = getConfig();
163
214
  provider = createProvider(config);
164
- console.log(green(`\n✓ Trocado para o perfil "${bold(profileName)}"!`));
165
- console.log(gray(` IA: ${config.provider.toUpperCase()} | Modelo: ${config.model}\n`));
166
- } else {
167
- console.log(chalk.red(`\n✖ Perfil "${profileName}" não encontrado.\n`));
215
+ console.log(green(`\n✓ Perfil "${bold(profileName)}" ativado!`));
216
+ continue;
168
217
  }
218
+ console.log(chalk.red(`\n✖ Perfil não encontrado.\n`));
169
219
  continue;
170
220
  }
171
221
 
172
- if (cmd.startsWith('/model ')) {
173
- const newModel = rawInput.split(' ')[1];
174
- if (newModel) {
175
- updateActiveModel(newModel);
176
- config.model = newModel;
177
- provider = createProvider(config);
178
- console.log(green(`\n✓ Modelo atualizado para: ${bold(newModel)}\n`));
222
+ if (cmd.startsWith('/use ')) {
223
+ const agentName = rawInput.split(' ')[1];
224
+ const agents = config.agents || {};
225
+ if (agentName === 'normal' || agentName === 'default') {
226
+ activePersona = null;
227
+ console.log(lavender(`\n✓ Voltando para o Modo Normal.\n`));
228
+ resetMessages();
229
+ continue;
230
+ }
231
+ if (agents[agentName]) {
232
+ activePersona = agentName;
233
+ const agent = agents[agentName];
234
+ if (switchProfile(agent.profile)) {
235
+ config = getConfig();
236
+ provider = createProvider(config);
237
+ }
238
+ currentMode = agent.mode || 'chat';
239
+ console.log(green(`\n✓ Agora você está falando com o Agente: ${bold(agentName)}`));
240
+ console.log(gray(` Task: ${agent.role.substring(0, 100)}...\n`));
241
+ resetMessages();
242
+ } else {
243
+ console.log(chalk.red(`\n✖ Agente "${agentName}" não encontrado.\n`));
179
244
  }
180
245
  continue;
181
246
  }
182
247
 
183
248
  if (cmd === '/clear') {
184
- messages.length = 0;
185
- messages.push({ role: 'system', content: getProjectContext() });
249
+ resetMessages();
186
250
  console.clear();
187
251
  console.log(lavender('✓ Histórico limpo, contexto preservado.\n'));
188
252
  continue;
@@ -190,73 +254,79 @@ export async function startInteractive() {
190
254
 
191
255
  if (cmd === '/help') {
192
256
  console.log(gray(`
193
- Comandos Disponíveis:
194
- /chat /plan /edit Mudar modo de operação
195
- /switch [nome] Mudar PERFIL (IA completa)
196
- /model [nome] Mudar apenas o MODELO atual
197
- /swarm Configurar e rodar enxames de agentes
198
- /config Gerenciar perfis e agentes
257
+ Comandos de Modo:
258
+ /chat Modo conversa
259
+ /plan Modo planejamento
260
+ /edit [auto/manual] Modo edição (padrão manual)
261
+ /use [agente] Usar um Agente Especialista
262
+ /use normal Voltar para o chat normal
263
+ /swarm → Rodar fluxos complexos
264
+
265
+ Gerenciamento:
266
+ /switch [nome] → Mudar perfil de IA completo
267
+ /model [nome] → Mudar modelo atual
268
+ /config → Perfis e Agentes
199
269
  /init → Inicializar .bimmorc.json
200
- @caminhoAnexar arquivos ou imagens
270
+ @arquivoLer arquivo ou imagem
201
271
  `));
202
272
  continue;
203
273
  }
204
274
 
205
275
  if (cmd === '/config') { await configure(); config = getConfig(); provider = createProvider(config); continue; }
206
276
 
277
+ if (cmd === '/init') {
278
+ const bimmoRcPath = path.join(process.cwd(), '.bimmorc.json');
279
+ if (fs.existsSync(bimmoRcPath)) {
280
+ const { overwrite } = await inquirer.prompt([{
281
+ type: 'confirm',
282
+ name: 'overwrite',
283
+ message: 'O arquivo .bimmorc.json já existe. Deseja sobrescrever?',
284
+ default: false
285
+ }]);
286
+ if (!overwrite) continue;
287
+ }
288
+ const initialConfig = {
289
+ projectName: path.basename(process.cwd()),
290
+ rules: ["Siga as convenções existentes.", "Prefira código modular."],
291
+ preferredTech: [],
292
+ ignorePatterns: ["node_modules", ".git"]
293
+ };
294
+ fs.writeFileSync(bimmoRcPath, JSON.stringify(initialConfig, null, 2));
295
+ console.log(green(`\n✅ .bimmorc.json criado com sucesso.\n`));
296
+
297
+ resetMessages();
298
+ continue;
299
+ }
300
+
207
301
  if (cmd === '/swarm') {
208
302
  const agents = config.agents || {};
209
303
  const agentList = Object.keys(agents);
210
-
211
304
  if (agentList.length < 2) {
212
- console.log(chalk.yellow('\nVocê precisa de pelo menos 2 Agentes configurados para rodar um Enxame.\nUse /config para criar Agentes.\n'));
305
+ console.log(chalk.yellow('\nCrie pelo menos 2 Agentes em /config primeiro.\n'));
213
306
  continue;
214
307
  }
215
-
216
308
  const { swarmAction } = await inquirer.prompt([{
217
309
  type: 'list',
218
310
  name: 'swarmAction',
219
- message: 'Ação de Enxame:',
220
- choices: ['Rodar Enxame Sequencial', 'Rodar Enxame Hierárquico', 'Voltar']
311
+ message: 'Tipo de Enxame:',
312
+ choices: ['Sequencial (A → B)', 'Hierárquico (Líder + Workers)', 'Voltar']
221
313
  }]);
222
-
223
314
  if (swarmAction === 'Voltar') continue;
224
-
225
- const { goal } = await inquirer.prompt([{ type: 'input', name: 'goal', message: 'Qual o objetivo final deste enxame?' }]);
226
-
227
- if (swarmAction === 'Rodar Enxame Sequencial') {
228
- const { selectedAgents } = await inquirer.prompt([{
229
- type: 'checkbox',
230
- name: 'selectedAgents',
231
- message: 'Selecione os agentes e a ordem (mínimo 2):',
232
- choices: agentList
233
- }]);
234
-
235
- if (selectedAgents.length < 2) {
236
- console.log(chalk.red('\nSelecione pelo menos 2 agentes.\n'));
237
- continue;
238
- }
239
-
240
- try {
241
- const finalResult = await orchestrator.runSequential(selectedAgents, goal);
242
- console.log(lavender('\n=== RESULTADO FINAL DO ENXAME ===\n'));
243
- console.log(marked(finalResult));
244
- } catch (e) {
245
- console.error(chalk.red(`\nErro no Enxame: ${e.message}`));
246
- }
247
- }
248
-
249
- if (swarmAction === 'Rodar Enxame Hierárquico') {
250
- const { manager } = await inquirer.prompt([{ type: 'list', name: 'manager', message: 'Selecione o Agente Líder (Manager):', choices: agentList }]);
251
- const { workers } = await inquirer.prompt([{ type: 'checkbox', name: 'workers', message: 'Selecione os Workers:', choices: agentList.filter(a => a !== manager) }]);
252
-
253
- try {
254
- const finalResult = await orchestrator.runHierarchical(manager, workers, goal);
255
- console.log(lavender('\n=== RESULTADO FINAL DO ENXAME ===\n'));
256
- console.log(marked(finalResult));
257
- } catch (e) {
258
- console.error(chalk.red(`\nErro no Enxame: ${e.message}`));
315
+ const { goal } = await inquirer.prompt([{ type: 'input', name: 'goal', message: 'Objetivo do enxame:' }]);
316
+
317
+ try {
318
+ let result;
319
+ if (swarmAction.includes('Sequencial')) {
320
+ const { selectedAgents } = await inquirer.prompt([{ type: 'checkbox', name: 'selectedAgents', message: 'Ordem dos agentes:', choices: agentList }]);
321
+ result = await orchestrator.runSequential(selectedAgents, goal);
322
+ } else {
323
+ const { manager } = await inquirer.prompt([{ type: 'list', name: 'manager', message: 'Líder:', choices: agentList }]);
324
+ const { workers } = await inquirer.prompt([{ type: 'checkbox', name: 'workers', message: 'Workers:', choices: agentList.filter(a => a !== manager) }]);
325
+ result = await orchestrator.runHierarchical(manager, workers, goal);
259
326
  }
327
+ console.log(lavender('\n=== RESULTADO FINAL ===\n') + marked(result));
328
+ } catch (e) {
329
+ console.error(chalk.red(`\nErro: ${e.message}`));
260
330
  }
261
331
  continue;
262
332
  }
@@ -264,14 +334,13 @@ Comandos Disponíveis:
264
334
  if (rawInput === '') continue;
265
335
 
266
336
  const controller = new AbortController();
267
- const interruptHandler = () => controller.abort();
268
- const keypressHandler = (str, key) => { if (key.name === 'escape' || (key.ctrl && key.name === 'c')) interruptHandler(); };
269
- process.on('SIGINT', interruptHandler);
270
- process.stdin.on('keypress', keypressHandler);
337
+ const localInterruptHandler = () => controller.abort();
338
+ process.removeAllListeners('SIGINT');
339
+ process.on('SIGINT', localInterruptHandler);
271
340
 
272
341
  let modeInstr = "";
273
- if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Descreva e analise, mas NÃO altere arquivos.";
274
- else if (currentMode === 'edit') modeInstr = "\n[MODO EDIT] Você tem permissão para usar write_file e run_command AGORA.";
342
+ if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Apenas analise.";
343
+ else if (currentMode === 'edit') modeInstr = `\n[MODO EDIT] Você tem permissão para usar ferramentas. (Auto-Accept: ${editState.autoAccept ? 'ON' : 'OFF'})`;
275
344
 
276
345
  const content = await processInput(rawInput);
277
346
  messages.push({
@@ -280,7 +349,7 @@ Comandos Disponíveis:
280
349
  });
281
350
 
282
351
  const spinner = ora({
283
- text: lavender(`bimmo (${currentMode}) pensando... (Ctrl+C para interromper)`),
352
+ text: lavender(`bimmo pensando... (Ctrl+C para interromper)`),
284
353
  color: currentMode === 'edit' ? 'red' : 'magenta'
285
354
  }).start();
286
355
 
@@ -296,14 +365,24 @@ Comandos Disponíveis:
296
365
  } catch (err) {
297
366
  spinner.stop();
298
367
  if (controller.signal.aborted || err.name === 'AbortError') {
299
- console.log(yellow('\n\n⚠️ Operação interrompida pelo usuário.\n'));
368
+ console.log(yellow('\n\n⚠️ Interrompido.\n'));
300
369
  messages.pop();
301
370
  } else {
302
- console.error(chalk.red('\n✖ Erro Crítico:') + ' ' + err.message + '\n');
371
+ console.error(chalk.red('\n✖ Erro:') + ' ' + err.message + '\n');
303
372
  }
304
373
  } finally {
305
- process.off('SIGINT', interruptHandler);
306
- process.stdin.off('keypress', keypressHandler);
374
+ process.removeListener('SIGINT', localInterruptHandler);
375
+ process.on('SIGINT', () => {
376
+ exitCounter++;
377
+ if (exitCounter === 1) {
378
+ console.log(gray('\n(Pressione Ctrl+C novamente para sair)'));
379
+ if (exitTimer) clearTimeout(exitTimer);
380
+ exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
381
+ } else {
382
+ console.log(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
383
+ process.exit(0);
384
+ }
385
+ });
307
386
  }
308
387
  }
309
- }
388
+ }
@@ -13,24 +13,23 @@ export class GrokProvider extends BaseProvider {
13
13
 
14
14
  formatMessages(messages) {
15
15
  return messages.map(msg => {
16
- if (typeof msg.content === 'string') {
16
+ if (typeof msg.content === 'string' || msg.content === null) {
17
17
  return msg;
18
18
  }
19
19
 
20
- const content = msg.content.map(part => {
21
- if (part.type === 'text') {
22
- return { type: 'text', text: part.text };
23
- } else if (part.type === 'image') {
24
- return {
20
+ if (Array.isArray(msg.content)) {
21
+ const content = msg.content.map(part => {
22
+ if (part.type === 'text') return { type: 'text', text: part.text };
23
+ if (part.type === 'image') return {
25
24
  type: 'image_url',
26
- image_url: {
27
- url: `data:${part.mimeType};base64,${part.data}`
28
- }
25
+ image_url: { url: `data:${part.mimeType};base64,${part.data}` }
29
26
  };
30
- }
31
- });
27
+ return part;
28
+ });
29
+ return { ...msg, content };
30
+ }
32
31
 
33
- return { ...msg, content };
32
+ return msg;
34
33
  });
35
34
  }
36
35
 
@@ -21,16 +21,25 @@ export class OpenAIProvider extends BaseProvider {
21
21
 
22
22
  formatMessages(messages) {
23
23
  return messages.map(msg => {
24
- if (typeof msg.content === 'string') return 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
+ }
25
28
 
26
- const content = msg.content.map(part => {
27
- if (part.type === 'text') return { type: 'text', text: part.text };
28
- if (part.type === 'image') return {
29
- type: 'image_url',
30
- image_url: { url: `data:${part.mimeType};base64,${part.data}` }
31
- };
32
- });
33
- return { ...msg, content };
29
+ // Se for um array (multimodal), processa as partes
30
+ if (Array.isArray(msg.content)) {
31
+ const content = msg.content.map(part => {
32
+ if (part.type === 'text') return { type: 'text', text: part.text };
33
+ if (part.type === 'image') return {
34
+ type: 'image_url',
35
+ image_url: { url: `data:${part.mimeType};base64,${part.data}` }
36
+ };
37
+ return part; // Mantém outras partes (como tool_result se houver)
38
+ });
39
+ return { ...msg, content };
40
+ }
41
+
42
+ return msg;
34
43
  });
35
44
  }
36
45