bimmo-cli 2.2.5 → 2.2.9

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.2.5",
4
- "description": "🌿 Plataforma de IA universal profissional com Agentes e Swarms. Suporte a Autocomplete, Diffs e Contexto Inteligente.",
3
+ "version": "2.2.9",
4
+ "description": "🌿 Plataforma de IA universal profissional com Agentes e Swarms. Suporte a Autocomplete real-time (estilo gemini-cli), Diffs e Contexto Inteligente.",
5
5
  "bin": {
6
6
  "bimmo": "bin/bimmo"
7
7
  },
@@ -29,6 +29,7 @@
29
29
  "conf": "^13.0.0",
30
30
  "diff": "^7.0.0",
31
31
  "figlet": "^1.7.0",
32
+ "fuzzy": "^0.1.3",
32
33
  "inquirer": "^9.3.8",
33
34
  "marked": "^14.0.0",
34
35
  "marked-terminal": "^7.0.0",
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,6 +1,5 @@
1
1
  import chalk from 'chalk';
2
2
  import figlet from 'figlet';
3
- import inquirer from 'inquirer';
4
3
  import { marked } from 'marked';
5
4
  import TerminalRenderer from 'marked-terminal';
6
5
  import ora from 'ora';
@@ -9,6 +8,7 @@ import path from 'path';
9
8
  import mime from 'mime-types';
10
9
  import readline from 'readline';
11
10
  import { fileURLToPath } from 'url';
11
+ import { stripVTControlCharacters } from 'util';
12
12
 
13
13
  import { getConfig, configure, updateActiveModel, switchProfile } from './config.js';
14
14
  import { createProvider } from './providers/factory.js';
@@ -21,12 +21,13 @@ const __dirname = path.dirname(__filename);
21
21
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
22
22
  const version = pkg.version;
23
23
 
24
- // CONFIGURAÇÃO DO RENDERIZADOR (CORREÇÃO DEFINITIVA DO <P>)
24
+ // Configuração do renderizador - DROP TOTAL DE HTML
25
25
  const terminalRenderer = new TerminalRenderer({
26
26
  heading: chalk.hex('#c084fc').bold,
27
27
  code: chalk.hex('#00ff9d'),
28
28
  strong: chalk.bold,
29
29
  em: chalk.italic,
30
+ html: (html) => '', // Remove qualquer tag HTML que sobrar
30
31
  });
31
32
 
32
33
  marked.setOptions({ renderer: terminalRenderer });
@@ -42,40 +43,24 @@ let activePersona = null;
42
43
  let exitCounter = 0;
43
44
  let exitTimer = null;
44
45
 
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
-
68
- function getFilesForCompletion(partialPath) {
46
+ function getFilesForPreview(partialPath) {
69
47
  try {
70
- const dir = path.dirname(partialPath.startsWith('@') ? partialPath.slice(1) : partialPath) || '.';
71
- const base = path.basename(partialPath.startsWith('@') ? partialPath.slice(1) : partialPath);
72
- const files = fs.readdirSync(path.resolve(process.cwd(), dir));
48
+ const p = partialPath.startsWith('@') ? partialPath.slice(1) : partialPath;
49
+ const dir = path.dirname(p) === '.' && !p.includes('/') ? '.' : path.dirname(p);
50
+ const base = path.basename(p) === '.' ? '' : path.basename(p);
51
+
52
+ const searchDir = path.resolve(process.cwd(), dir);
53
+ if (!fs.existsSync(searchDir)) return [];
54
+
55
+ const files = fs.readdirSync(searchDir);
73
56
  return files
74
- .filter(f => f.startsWith(base) && !f.startsWith('.') && f !== 'node_modules')
75
- .map(f => path.join(dir, f));
76
- } catch (e) {
77
- return [];
78
- }
57
+ .filter(f => (base === '' || f.startsWith(base)) && !f.startsWith('.') && f !== 'node_modules')
58
+ .map(f => {
59
+ const rel = path.join(dir === '.' ? '' : dir, f);
60
+ const isDir = fs.statSync(path.join(searchDir, f)).isDirectory();
61
+ return { name: rel, isDir };
62
+ });
63
+ } catch (e) { return []; }
79
64
  }
80
65
 
81
66
  function cleanAIResponse(text) {
@@ -90,9 +75,6 @@ function cleanAIResponse(text) {
90
75
 
91
76
  export async function startInteractive() {
92
77
  let config = getConfig();
93
- const lang = config.language || 'pt-BR';
94
- const t = i18n[lang] || i18n['pt-BR'];
95
-
96
78
  if (!config.provider || !config.apiKey) {
97
79
  console.log(lavender(figlet.textSync('bimmo')));
98
80
  await configure();
@@ -121,37 +103,60 @@ export async function startInteractive() {
121
103
  console.log(green(` Modelo: ${bold(config.model)}`));
122
104
  console.log(lavender('─'.repeat(60)) + '\n');
123
105
 
124
- console.log(lavender(`👋 ${t.welcome}\n`));
125
-
126
106
  const rl = readline.createInterface({
127
107
  input: process.stdin,
128
108
  output: process.stdout,
129
109
  terminal: true,
130
- historySize: 100,
131
110
  completer: (line) => {
132
111
  const words = line.split(' ');
133
112
  const lastWord = words[words.length - 1];
134
113
  if (lastWord.startsWith('@')) {
135
- const hits = getFilesForCompletion(lastWord);
136
- return [hits.map(h => `@${h}`), lastWord];
114
+ const hits = getFilesForPreview(lastWord).map(h => `@${h.name}`);
115
+ return [hits, lastWord];
137
116
  }
138
117
  return [[], line];
139
118
  }
140
119
  });
141
120
 
142
- // Handler de saída
143
- rl.on('SIGINT', () => {
144
- exitCounter++;
145
- if (exitCounter === 1) {
146
- console.log(`\n${gray(t.exitHint)}`);
147
- if (exitTimer) clearTimeout(exitTimer);
148
- exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
149
- displayPrompt();
150
- } else {
151
- console.log(lavender('\n👋 BIMMO encerrando sessão.\n'));
152
- process.exit(0);
121
+ let currentPreviewLines = 0;
122
+
123
+ const clearPreview = () => {
124
+ if (currentPreviewLines > 0) {
125
+ // Move o cursor para o final das linhas de preview e apaga tudo
126
+ readline.moveCursor(process.stdout, 0, currentPreviewLines);
127
+ for (let i = 0; i < currentPreviewLines; i++) {
128
+ readline.moveCursor(process.stdout, 0, -1);
129
+ readline.clearLine(process.stdout, 0);
130
+ }
131
+ currentPreviewLines = 0;
153
132
  }
154
- });
133
+ };
134
+
135
+ const showPreview = () => {
136
+ clearPreview();
137
+ const words = rl.line.split(' ');
138
+ const lastWord = words[words.length - 1];
139
+
140
+ if (lastWord.startsWith('@')) {
141
+ const files = getFilesForPreview(lastWord);
142
+ if (files.length > 0) {
143
+ process.stdout.write('\n');
144
+ const displayFiles = files.slice(0, 8);
145
+ displayFiles.forEach(f => {
146
+ process.stdout.write(gray(` ${f.isDir ? '📁' : '📄'} ${f.name}\n`));
147
+ });
148
+ currentPreviewLines = displayFiles.length + 1;
149
+ // Volta o cursor para a linha do prompt de forma segura
150
+ readline.moveCursor(process.stdout, 0, -currentPreviewLines);
151
+ // Posiciona o cursor exatamente onde o usuário estava digitando
152
+ const promptText = rl.getPrompt();
153
+ const visualPromptLen = stripVTControlCharacters(promptText).length;
154
+ const totalLen = visualPromptLen + rl.line.length;
155
+ const cols = process.stdout.columns || 80;
156
+ readline.cursorTo(process.stdout, totalLen % cols);
157
+ }
158
+ }
159
+ };
155
160
 
156
161
  const displayPrompt = () => {
157
162
  const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]` : '';
@@ -163,108 +168,85 @@ export async function startInteractive() {
163
168
  rl.prompt();
164
169
  };
165
170
 
171
+ // Escuta teclas para o preview em tempo real
172
+ process.stdin.on('keypress', (s, key) => {
173
+ if (key && (key.name === 'return' || key.name === 'enter')) return;
174
+ setImmediate(() => showPreview());
175
+ });
176
+
177
+ rl.on('SIGINT', () => {
178
+ if (exitCounter === 0) {
179
+ exitCounter++;
180
+ process.stdout.write(`\n${gray('(Pressione novamente para sair)')}\n`);
181
+ exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
182
+ displayPrompt();
183
+ } else { process.exit(0); }
184
+ });
185
+
166
186
  displayPrompt();
167
187
 
168
188
  rl.on('line', async (input) => {
189
+ clearPreview();
169
190
  const rawInput = input.trim();
170
- const cmd = rawInput.toLowerCase();
171
-
172
191
  if (rawInput === '') { displayPrompt(); return; }
173
192
 
174
- // COMANDOS INTERNOS
193
+ const cmd = rawInput.toLowerCase();
175
194
  if (cmd === '/exit' || cmd === 'sair') process.exit(0);
176
195
  if (cmd === '/chat') { currentMode = 'chat'; displayPrompt(); return; }
177
196
  if (cmd === '/plan') { currentMode = 'plan'; displayPrompt(); return; }
178
197
  if (cmd === '/edit' || cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false; displayPrompt(); return; }
179
198
  if (cmd === '/edit auto') { currentMode = 'edit'; editState.autoAccept = true; displayPrompt(); return; }
180
-
181
199
  if (cmd === '/clear') { resetMessages(); console.clear(); displayPrompt(); return; }
182
- if (cmd === '/help') { console.log(gray(t.help)); displayPrompt(); return; }
183
200
 
184
201
  if (cmd === '/init') {
185
- const bimmoRcPath = path.join(process.cwd(), '.bimmorc.json');
186
- const initialConfig = { projectName: path.basename(process.cwd()), rules: ["Clean code"], ignorePatterns: ["node_modules"] };
187
- fs.writeFileSync(bimmoRcPath, JSON.stringify(initialConfig, null, 2));
188
- console.log(green(`\n✅ .bimmorc.json criado.`));
189
- resetMessages();
190
- displayPrompt();
191
- return;
202
+ console.log(chalk.cyan('\n🚀 Gerando .bimmorc.json...\n'));
203
+ const initPrompt = `Analise o projeto e crie o .bimmorc.json com regras e stack. Use write_file.`;
204
+ const spinner = ora({ text: lavender(`bimmo pensando...`), color: 'red' }).start();
205
+ try {
206
+ const res = await provider.sendMessage([...messages, { role: 'user', content: initPrompt }]);
207
+ spinner.stop();
208
+ console.log(marked.parse(cleanAIResponse(res)));
209
+ } catch (e) { spinner.stop(); console.error(chalk.red(e.message)); }
210
+ resetMessages(); displayPrompt(); return;
192
211
  }
193
212
 
194
- if (cmd === '/config') {
195
- rl.pause();
196
- await configure();
197
- config = getConfig();
198
- provider = createProvider(config);
199
- rl.resume();
200
- displayPrompt();
201
- return;
202
- }
203
-
204
- if (cmd.startsWith('/switch ')) {
205
- const pName = rawInput.split(' ')[1];
206
- if (switchProfile(pName)) {
207
- config = getConfig(); provider = createProvider(config);
208
- console.log(green(`\n✓ ${t.switchOk}`));
209
- } else { console.log(chalk.red(`\n✖ Perfil não encontrado.`)); }
210
- displayPrompt(); return;
211
- }
212
-
213
- if (cmd.startsWith('/use ')) {
214
- const aName = rawInput.split(' ')[1];
215
- if (aName === 'normal') { activePersona = null; resetMessages(); displayPrompt(); return; }
216
- const agents = config.agents || {};
217
- if (agents[aName]) {
218
- activePersona = aName;
219
- const agent = agents[aName];
220
- if (switchProfile(agent.profile)) { config = getConfig(); provider = createProvider(config); }
221
- currentMode = agent.mode || 'chat';
222
- console.log(green(`\n✓ ${t.agentOk} ${bold(aName)}`));
223
- resetMessages();
224
- } else { console.log(chalk.red(`\n✖ Agente não encontrado.`)); }
225
- displayPrompt(); return;
226
- }
213
+ if (cmd === '/config') { rl.pause(); await configure(); config = getConfig(); provider = createProvider(config); rl.resume(); displayPrompt(); return; }
227
214
 
228
215
  // PROCESSAMENTO IA
229
216
  const controller = new AbortController();
230
217
  const abortHandler = () => controller.abort();
218
+ process.removeListener('SIGINT', () => {}); // Limpa handlers antigos
231
219
  process.on('SIGINT', abortHandler);
232
220
 
233
221
  let modeInstr = "";
234
222
  if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Apenas analise.";
235
223
  else if (currentMode === 'edit') modeInstr = `\n[MODO EDIT] Auto-Accept: ${editState.autoAccept ? 'ON' : 'OFF'}`;
236
224
 
237
- // Processar anexos @
238
225
  const processedContent = [];
239
226
  const words = rawInput.split(' ');
240
227
  for (const word of words) {
241
228
  if (word.startsWith('@')) {
242
229
  const filePath = word.slice(1);
243
230
  if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
244
- const content = fs.readFileSync(filePath, 'utf-8');
245
- processedContent.push({ type: 'text', text: `\n[ARQUIVO: ${filePath}]\n${content}\n` });
231
+ processedContent.push({ type: 'text', text: `\n[ARQUIVO: ${filePath}]\n${fs.readFileSync(filePath, 'utf-8')}\n` });
246
232
  } else { processedContent.push({ type: 'text', text: word }); }
247
233
  } else { processedContent.push({ type: 'text', text: word }); }
248
234
  }
249
235
 
250
236
  messages.push({ role: 'user', content: [...processedContent, { type: 'text', text: modeInstr }] });
251
-
252
- const spinner = ora({ text: lavender(`${t.thinking} (Ctrl+C para parar)`), color: currentMode === 'edit' ? 'red' : 'magenta' }).start();
237
+ const spinner = ora({ text: lavender(`bimmo pensando... (Ctrl+C para parar)`), color: currentMode === 'edit' ? 'red' : 'magenta' }).start();
253
238
 
254
239
  try {
255
240
  let responseText = await provider.sendMessage(messages, { signal: controller.signal });
256
241
  spinner.stop();
257
-
258
- const cleanedText = cleanAIResponse(responseText);
259
- messages.push({ role: 'assistant', content: responseText });
260
-
261
242
  console.log(`\n${lavender('bimmo ')}${currentMode.toUpperCase()}`);
262
243
  console.log(lavender('─'.repeat(50)));
263
- console.log(marked.parse(cleanedText)); // Usamos parse para garantir o renderer terminal
264
- console.log(gray('─'.repeat(50)));
244
+ process.stdout.write(marked.parse(cleanAIResponse(responseText)));
245
+ console.log(gray('\n' + '─'.repeat(50)));
246
+ messages.push({ role: 'assistant', content: responseText });
265
247
  } catch (err) {
266
248
  spinner.stop();
267
- if (controller.signal.aborted) { console.log(yellow(`\n⚠️ ${t.interrupted}`)); messages.pop(); }
249
+ if (controller.signal.aborted) { console.log(yellow(`\n⚠️ Interrompido.`)); messages.pop(); }
268
250
  else { console.error(chalk.red(`\n✖ Erro: ${err.message}`)); }
269
251
  } finally {
270
252
  process.removeListener('SIGINT', abortHandler);
@@ -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