bimmo-cli 2.0.3 → 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.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.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,
@@ -99,7 +100,9 @@ function getModeStyle() {
99
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,7 +145,6 @@ 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
148
  process.on('SIGINT', () => {
147
149
  exitCounter++;
148
150
  if (exitCounter === 1) {
@@ -173,7 +175,6 @@ export async function startInteractive() {
173
175
  ]);
174
176
  input = answers.input;
175
177
  } catch (e) {
176
- // Inquirer joga erro no Ctrl+C se não for tratado
177
178
  continue;
178
179
  }
179
180
 
@@ -187,7 +188,24 @@ export async function startInteractive() {
187
188
 
188
189
  if (cmd === '/chat') { currentMode = 'chat'; console.log(lavender('✓ Modo CHAT.\n')); continue; }
189
190
  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; }
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
+ }
191
209
 
192
210
  if (cmd.startsWith('/switch ')) {
193
211
  const profileName = rawInput.split(' ')[1];
@@ -237,7 +255,9 @@ export async function startInteractive() {
237
255
  if (cmd === '/help') {
238
256
  console.log(gray(`
239
257
  Comandos de Modo:
240
- /chat /plan /edit Mudar modo de operação
258
+ /chat Modo conversa
259
+ /plan → Modo planejamento
260
+ /edit [auto/manual] → Modo edição (padrão manual)
241
261
  /use [agente] → Usar um Agente Especialista
242
262
  /use normal → Voltar para o chat normal
243
263
  /swarm → Rodar fluxos complexos
@@ -254,6 +274,30 @@ Gerenciamento:
254
274
 
255
275
  if (cmd === '/config') { await configure(); config = getConfig(); provider = createProvider(config); continue; }
256
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
+
257
301
  if (cmd === '/swarm') {
258
302
  const agents = config.agents || {};
259
303
  const agentList = Object.keys(agents);
@@ -290,19 +334,13 @@ Gerenciamento:
290
334
  if (rawInput === '') continue;
291
335
 
292
336
  const controller = new AbortController();
293
-
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
337
+ const localInterruptHandler = () => controller.abort();
300
338
  process.removeAllListeners('SIGINT');
301
339
  process.on('SIGINT', localInterruptHandler);
302
340
 
303
341
  let modeInstr = "";
304
342
  if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Apenas analise.";
305
- else if (currentMode === 'edit') modeInstr = "\n[MODO EDIT] Aplique as mudanças agora.";
343
+ else if (currentMode === 'edit') modeInstr = `\n[MODO EDIT] Você tem permissão para usar ferramentas. (Auto-Accept: ${editState.autoAccept ? 'ON' : 'OFF'})`;
306
344
 
307
345
  const content = await processInput(rawInput);
308
346
  messages.push({
@@ -327,13 +365,12 @@ Gerenciamento:
327
365
  } catch (err) {
328
366
  spinner.stop();
329
367
  if (controller.signal.aborted || err.name === 'AbortError') {
330
- console.log(yellow('\n\n⚠️ Operação interrompida pelo usuário.\n'));
368
+ console.log(yellow('\n\n⚠️ Interrompido.\n'));
331
369
  messages.pop();
332
370
  } else {
333
371
  console.error(chalk.red('\n✖ Erro:') + ' ' + err.message + '\n');
334
372
  }
335
373
  } finally {
336
- // Restaura o handler global de saída
337
374
  process.removeListener('SIGINT', localInterruptHandler);
338
375
  process.on('SIGINT', () => {
339
376
  exitCounter++;
@@ -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