bimmo-cli 2.0.3 → 2.1.1

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.3",
4
- "description": "🌿 Plataforma de IA universal com modo Normal, Agentes Especialistas e Enxames (Swarms). Suporte a Auto-Edit, Contexto Inteligente e interrupção inteligente.",
3
+ "version": "2.1.1",
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,
@@ -96,10 +97,12 @@ async function processInput(input) {
96
97
  }
97
98
 
98
99
  function getModeStyle() {
99
- const personaLabel = activePersona ? `[${activePersona.toUpperCase()}] ` : '';
100
+ const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]` : '';
100
101
  switch (currentMode) {
101
102
  case 'plan': return yellow.bold(`${personaLabel}[PLAN] `);
102
- case 'edit': return chalk.red.bold(`${personaLabel}[EDIT] `);
103
+ case 'edit':
104
+ const editSubMode = editState.autoAccept ? '(AUTO)' : '(MANUAL)';
105
+ return chalk.red.bold(`${personaLabel}[EDIT${editSubMode}] `);
103
106
  default: return lavender.bold(`${personaLabel}[CHAT] `);
104
107
  }
105
108
  }
@@ -142,18 +145,19 @@ export async function startInteractive() {
142
145
 
143
146
  console.log(lavender('👋 Olá! Estou pronto. No que posso ajudar?\n'));
144
147
 
145
- // Handler Global de SIGINT para o modo ocioso (Idle)
146
- process.on('SIGINT', () => {
148
+ const globalSigIntHandler = () => {
147
149
  exitCounter++;
148
150
  if (exitCounter === 1) {
149
- console.log(gray('\n(Pressione Ctrl+C novamente para sair)'));
151
+ process.stdout.write(gray('\n(Pressione Ctrl+C novamente para sair)\n'));
150
152
  if (exitTimer) clearTimeout(exitTimer);
151
153
  exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
152
154
  } else {
153
- console.log(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
155
+ process.stdout.write(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
154
156
  process.exit(0);
155
157
  }
156
- });
158
+ };
159
+
160
+ process.on('SIGINT', globalSigIntHandler);
157
161
 
158
162
  readline.emitKeypressEvents(process.stdin);
159
163
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
@@ -173,7 +177,6 @@ export async function startInteractive() {
173
177
  ]);
174
178
  input = answers.input;
175
179
  } catch (e) {
176
- // Inquirer joga erro no Ctrl+C se não for tratado
177
180
  continue;
178
181
  }
179
182
 
@@ -187,7 +190,24 @@ export async function startInteractive() {
187
190
 
188
191
  if (cmd === '/chat') { currentMode = 'chat'; console.log(lavender('✓ Modo CHAT.\n')); continue; }
189
192
  if (cmd === '/plan') { currentMode = 'plan'; console.log(yellow('✓ Modo PLAN.\n')); continue; }
190
- if (cmd === '/edit') { currentMode = 'edit'; console.log(chalk.red('⚠️ Modo EDIT.\n')); continue; }
193
+
194
+ if (cmd === '/edit') {
195
+ currentMode = 'edit';
196
+ console.log(chalk.red(`⚠️ Modo EDIT ativado (Sub-modo atual: ${editState.autoAccept ? 'AUTO' : 'MANUAL'}).\n`));
197
+ continue;
198
+ }
199
+ if (cmd === '/edit auto') {
200
+ currentMode = 'edit';
201
+ editState.autoAccept = true;
202
+ console.log(chalk.red('⚠️ Modo EDIT (AUTO) ativado. Mudanças serão aplicadas sem perguntar.\n'));
203
+ continue;
204
+ }
205
+ if (cmd === '/edit manual') {
206
+ currentMode = 'edit';
207
+ editState.autoAccept = false;
208
+ console.log(chalk.red('⚠️ Modo EDIT (MANUAL) ativado. Pedirei permissão para cada mudança.\n'));
209
+ continue;
210
+ }
191
211
 
192
212
  if (cmd.startsWith('/switch ')) {
193
213
  const profileName = rawInput.split(' ')[1];
@@ -237,7 +257,9 @@ export async function startInteractive() {
237
257
  if (cmd === '/help') {
238
258
  console.log(gray(`
239
259
  Comandos de Modo:
240
- /chat /plan /edit Mudar modo de operação
260
+ /chat Modo conversa
261
+ /plan → Modo planejamento
262
+ /edit [auto/manual] → Modo edição (padrão manual)
241
263
  /use [agente] → Usar um Agente Especialista
242
264
  /use normal → Voltar para o chat normal
243
265
  /swarm → Rodar fluxos complexos
@@ -254,6 +276,30 @@ Gerenciamento:
254
276
 
255
277
  if (cmd === '/config') { await configure(); config = getConfig(); provider = createProvider(config); continue; }
256
278
 
279
+ if (cmd === '/init') {
280
+ const bimmoRcPath = path.join(process.cwd(), '.bimmorc.json');
281
+ if (fs.existsSync(bimmoRcPath)) {
282
+ const { overwrite } = await inquirer.prompt([{
283
+ type: 'confirm',
284
+ name: 'overwrite',
285
+ message: 'O arquivo .bimmorc.json já existe. Deseja sobrescrever?',
286
+ default: false
287
+ }]);
288
+ if (!overwrite) continue;
289
+ }
290
+ const initialConfig = {
291
+ projectName: path.basename(process.cwd()),
292
+ rules: ["Siga as convenções existentes.", "Prefira código modular."],
293
+ preferredTech: [],
294
+ ignorePatterns: ["node_modules", ".git"]
295
+ };
296
+ fs.writeFileSync(bimmoRcPath, JSON.stringify(initialConfig, null, 2));
297
+ console.log(green(`\n✅ .bimmorc.json criado com sucesso.\n`));
298
+
299
+ resetMessages();
300
+ continue;
301
+ }
302
+
257
303
  if (cmd === '/swarm') {
258
304
  const agents = config.agents || {};
259
305
  const agentList = Object.keys(agents);
@@ -290,19 +336,15 @@ Gerenciamento:
290
336
  if (rawInput === '') continue;
291
337
 
292
338
  const controller = new AbortController();
339
+ const localInterruptHandler = () => controller.abort();
293
340
 
294
- // Handler local para SIGINT durante o processamento da IA
295
- const localInterruptHandler = () => {
296
- controller.abort();
297
- };
298
-
299
- // Remove temporariamente o handler global de saída
300
- process.removeAllListeners('SIGINT');
341
+ // Switch de SIGINT para modo processamento
342
+ process.removeListener('SIGINT', globalSigIntHandler);
301
343
  process.on('SIGINT', localInterruptHandler);
302
344
 
303
345
  let modeInstr = "";
304
346
  if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Apenas analise.";
305
- else if (currentMode === 'edit') modeInstr = "\n[MODO EDIT] Aplique as mudanças agora.";
347
+ else if (currentMode === 'edit') modeInstr = `\n[MODO EDIT] Você tem permissão para usar ferramentas. (Auto-Accept: ${editState.autoAccept ? 'ON' : 'OFF'})`;
306
348
 
307
349
  const content = await processInput(rawInput);
308
350
  messages.push({
@@ -318,11 +360,18 @@ Gerenciamento:
318
360
  try {
319
361
  let responseText = await provider.sendMessage(messages, { signal: controller.signal });
320
362
  spinner.stop();
321
- const cleanedText = responseText.replace(/<\/?[^>]+(>|$)/g, "");
363
+
364
+ // LIMPEZA AGRESSIVA DE HTML
365
+ const cleanedText = responseText
366
+ .replace(/<br\s*\/?>/gi, '\n') // Converte <br> em newline real
367
+ .replace(/<p>/gi, '') // Remove tags <p> iniciais
368
+ .replace(/<\/p>/gi, '\n\n') // Converte </p> em double newline
369
+ .replace(/<\/?[^>]+(>|$)/g, ""); // Remove QUALQUER outra tag residual
370
+
322
371
  messages.push({ role: 'assistant', content: responseText });
323
- console.log('\n' + lavender('bimmo') + getModeStyle());
372
+ console.log('\n' + lavender('bimmo ') + getModeStyle());
324
373
  console.log(lavender('─'.repeat(50)));
325
- console.log(marked(cleanedText));
374
+ console.log(marked(cleanedText.trim()));
326
375
  console.log(gray('─'.repeat(50)) + '\n');
327
376
  } catch (err) {
328
377
  spinner.stop();
@@ -333,19 +382,9 @@ Gerenciamento:
333
382
  console.error(chalk.red('\n✖ Erro:') + ' ' + err.message + '\n');
334
383
  }
335
384
  } finally {
336
- // Restaura o handler global de saída
385
+ // Restaura o modo global de saída
337
386
  process.removeListener('SIGINT', localInterruptHandler);
338
- process.on('SIGINT', () => {
339
- exitCounter++;
340
- if (exitCounter === 1) {
341
- console.log(gray('\n(Pressione Ctrl+C novamente para sair)'));
342
- if (exitTimer) clearTimeout(exitTimer);
343
- exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
344
- } else {
345
- console.log(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
346
- process.exit(0);
347
- }
348
- });
387
+ process.on('SIGINT', globalSigIntHandler);
349
388
  }
350
389
  }
351
390
  }
@@ -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