bimmo-cli 2.1.2 → 2.1.4

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/interface.js +75 -178
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimmo-cli",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
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"
package/src/interface.js CHANGED
@@ -21,11 +21,18 @@ 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 - Forçamos o escape de HTML para garantir que tags não apareçam
24
25
  marked.use(new TerminalRenderer({
25
26
  heading: chalk.hex('#c084fc').bold,
26
27
  code: chalk.hex('#00ff9d'),
27
28
  }));
28
29
 
30
+ marked.setOptions({
31
+ sanitize: true, // Depreciado mas ajuda em versões antigas
32
+ headerIds: false,
33
+ mangle: false
34
+ });
35
+
29
36
  const green = chalk.hex('#00ff9d');
30
37
  const lavender = chalk.hex('#c084fc');
31
38
  const gray = chalk.gray;
@@ -49,21 +56,12 @@ async function processInput(input) {
49
56
  const stats = fs.statSync(filePath);
50
57
  if (stats.isFile()) {
51
58
  const mimeType = mime.lookup(filePath) || 'application/octet-stream';
52
-
53
59
  if (mimeType.startsWith('image/')) {
54
60
  const base64Image = fs.readFileSync(filePath, { encoding: 'base64' });
55
- processedContent.push({
56
- type: 'image',
57
- mimeType,
58
- data: base64Image,
59
- fileName: path.basename(filePath)
60
- });
61
+ processedContent.push({ type: 'image', mimeType, data: base64Image, fileName: path.basename(filePath) });
61
62
  } else {
62
63
  const textContent = fs.readFileSync(filePath, 'utf-8');
63
- processedContent.push({
64
- type: 'text',
65
- text: `\n--- Arquivo: ${path.basename(filePath)} ---\n${textContent}\n--- Fim do arquivo ---\n`
66
- });
64
+ processedContent.push({ type: 'text', text: `\n--- Arquivo: ${path.basename(filePath)} ---\n${textContent}\n--- Fim do arquivo ---\n` });
67
65
  }
68
66
  }
69
67
  } else {
@@ -78,27 +76,19 @@ async function processInput(input) {
78
76
  }
79
77
 
80
78
  const hasImage = processedContent.some(c => c.type === 'image');
81
-
82
- if (!hasImage) {
83
- return processedContent.map(c => c.text).join(' ');
84
- }
79
+ if (!hasImage) return processedContent.map(c => c.text).join(' ');
85
80
 
86
81
  const finalContent = [];
87
82
  let currentText = "";
88
-
89
83
  for (const item of processedContent) {
90
84
  if (item.type === 'text') {
91
85
  currentText += (currentText ? " " : "") + item.text;
92
86
  } else {
93
- if (currentText) {
94
- finalContent.push({ type: 'text', text: currentText });
95
- currentText = "";
96
- }
87
+ if (currentText) { finalContent.push({ type: 'text', text: currentText }); currentText = ""; }
97
88
  finalContent.push(item);
98
89
  }
99
90
  }
100
91
  if (currentText) finalContent.push({ type: 'text', text: currentText });
101
-
102
92
  return finalContent;
103
93
  }
104
94
 
@@ -113,6 +103,30 @@ function getModeStyle() {
113
103
  }
114
104
  }
115
105
 
106
+ /**
107
+ * LIMPEZA ABSOLUTA DE HTML
108
+ * Remove tags HTML antes da renderização Markdown.
109
+ */
110
+ function cleanAIResponse(text) {
111
+ if (!text) return "";
112
+
113
+ let cleaned = text
114
+ .replace(/<br\s*\/?>/gi, '\n')
115
+ .replace(/<\/p>/gi, '\n\n')
116
+ .replace(/<\/div>/gi, '\n')
117
+ .replace(/<li>/gi, '* ')
118
+ .replace(/<\/li>/gi, '\n');
119
+
120
+ // Regex extremamente agressiva para remover qualquer tag restante
121
+ cleaned = cleaned.replace(/<[^>]*>?/gm, '');
122
+
123
+ // Decodifica entidades comuns
124
+ const entities = {
125
+ '&nbsp;': ' ', '&lt;': '<', '&gt;': '>', '&amp;': '&', '&quot;': '"', '&apos;': "'"
126
+ };
127
+ return cleaned.replace(/&[a-z0-9#]+;/gi, (match) => entities[match] || match).trim();
128
+ }
129
+
116
130
  export async function startInteractive() {
117
131
  let config = getConfig();
118
132
 
@@ -133,9 +147,7 @@ export async function startInteractive() {
133
147
  messages.push({ role: 'system', content: projectContext });
134
148
  if (activePersona) {
135
149
  const agent = (config.agents || {})[activePersona];
136
- if (agent) {
137
- messages.push({ role: 'system', content: `Sua persona atual é: ${agent.name}. Sua tarefa: ${agent.role}` });
138
- }
150
+ if (agent) messages.push({ role: 'system', content: `Sua persona atual é: ${agent.name}. Sua tarefa: ${agent.role}` });
139
151
  }
140
152
  };
141
153
 
@@ -164,234 +176,119 @@ export async function startInteractive() {
164
176
  };
165
177
 
166
178
  process.on('SIGINT', globalSigIntHandler);
167
-
168
179
  readline.emitKeypressEvents(process.stdin);
169
180
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
170
181
 
171
182
  while (true) {
172
183
  const modeIndicator = getModeStyle();
173
184
  let input;
174
-
175
185
  try {
176
- const answers = await inquirer.prompt([
177
- {
178
- type: 'input',
179
- name: 'input',
180
- message: modeIndicator + green('Você'),
181
- prefix: '→',
182
- }
183
- ]);
186
+ const answers = await inquirer.prompt([{ type: 'input', name: 'input', message: modeIndicator + green('Você'), prefix: '→' }]);
184
187
  input = answers.input;
185
- } catch (e) {
186
- continue;
187
- }
188
+ } catch (e) { continue; }
188
189
 
189
- // Mostra o diretório atual logo abaixo do input do usuário
190
190
  console.log(gray(` 📁 ${process.cwd()}`));
191
-
192
191
  const rawInput = input.trim();
193
192
  const cmd = rawInput.toLowerCase();
194
193
 
195
- if (cmd === '/exit' || cmd === 'exit' || cmd === 'sair') {
196
- console.log(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
197
- process.exit(0);
198
- }
199
-
194
+ // COMANDOS CLI (Tratados antes de enviar para IA)
195
+ if (cmd === '/exit' || cmd === 'exit' || cmd === 'sair') { process.exit(0); }
196
+
200
197
  if (cmd === '/chat') { currentMode = 'chat'; console.log(lavender('✓ Modo CHAT.\n')); continue; }
201
198
  if (cmd === '/plan') { currentMode = 'plan'; console.log(yellow('✓ Modo PLAN.\n')); continue; }
202
199
 
203
- if (cmd === '/edit') {
204
- currentMode = 'edit';
205
- console.log(chalk.red(`⚠️ Modo EDIT ativado (Sub-modo atual: ${editState.autoAccept ? 'AUTO' : 'MANUAL'}).\n`));
206
- continue;
207
- }
208
- if (cmd === '/edit auto') {
209
- currentMode = 'edit';
210
- editState.autoAccept = true;
211
- console.log(chalk.red('⚠️ Modo EDIT (AUTO) ativado. Mudanças serão aplicadas sem perguntar.\n'));
212
- continue;
213
- }
214
- if (cmd === '/edit manual') {
215
- currentMode = 'edit';
216
- editState.autoAccept = false;
217
- console.log(chalk.red('⚠️ Modo EDIT (MANUAL) ativado. Pedirei permissão para cada mudança.\n'));
200
+ if (cmd === '/edit') { currentMode = 'edit'; console.log(chalk.red(`⚠️ Modo EDIT ativado.\n`)); continue; }
201
+ if (cmd === '/edit auto') { currentMode = 'edit'; editState.autoAccept = true; console.log(chalk.red('⚠️ Modo EDIT (AUTO) ativado.\n')); continue; }
202
+ if (cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false; console.log(chalk.red('⚠️ Modo EDIT (MANUAL) ativado.\n')); continue; }
203
+
204
+ if (cmd === '/init') {
205
+ const bimmoRcPath = path.join(process.cwd(), '.bimmorc.json');
206
+ if (fs.existsSync(bimmoRcPath)) {
207
+ const { overwrite } = await inquirer.prompt([{ type: 'confirm', name: 'overwrite', message: 'O arquivo .bimmorc.json já existe. Sobrescrever?', default: false }]);
208
+ if (!overwrite) continue;
209
+ }
210
+ const initialConfig = {
211
+ projectName: path.basename(process.cwd()),
212
+ rules: ["Siga as convenções existentes.", "Prefira código modular."],
213
+ ignorePatterns: ["node_modules", ".git"]
214
+ };
215
+ fs.writeFileSync(bimmoRcPath, JSON.stringify(initialConfig, null, 2));
216
+ console.log(green(`\n✅ .bimmorc.json criado com sucesso.\n`));
217
+ resetMessages();
218
218
  continue;
219
219
  }
220
220
 
221
221
  if (cmd.startsWith('/switch ')) {
222
222
  const profileName = rawInput.split(' ')[1];
223
223
  if (profileName && switchProfile(profileName)) {
224
- config = getConfig();
225
- provider = createProvider(config);
224
+ config = getConfig(); provider = createProvider(config);
226
225
  console.log(green(`\n✓ Perfil "${bold(profileName)}" ativado!`));
227
226
  continue;
228
227
  }
229
- console.log(chalk.red(`\n✖ Perfil não encontrado.\n`));
230
- continue;
228
+ console.log(chalk.red(`\n✖ Perfil não encontrado.\n`)); continue;
231
229
  }
232
230
 
233
231
  if (cmd.startsWith('/use ')) {
234
232
  const agentName = rawInput.split(' ')[1];
235
233
  const agents = config.agents || {};
236
- if (agentName === 'normal' || agentName === 'default') {
237
- activePersona = null;
238
- console.log(lavender(`\n✓ Voltando para o Modo Normal.\n`));
239
- resetMessages();
240
- continue;
241
- }
234
+ if (agentName === 'normal' || agentName === 'default') { activePersona = null; resetMessages(); continue; }
242
235
  if (agents[agentName]) {
243
236
  activePersona = agentName;
244
237
  const agent = agents[agentName];
245
- if (switchProfile(agent.profile)) {
246
- config = getConfig();
247
- provider = createProvider(config);
248
- }
238
+ if (switchProfile(agent.profile)) { config = getConfig(); provider = createProvider(config); }
249
239
  currentMode = agent.mode || 'chat';
250
- console.log(green(`\n✓ Agora você está falando com o Agente: ${bold(agentName)}`));
251
- console.log(gray(` Task: ${agent.role.substring(0, 100)}...\n`));
240
+ console.log(green(`\n✓ Ativado Agente: ${bold(agentName)}`));
252
241
  resetMessages();
253
- } else {
254
- console.log(chalk.red(`\n✖ Agente "${agentName}" não encontrado.\n`));
255
- }
242
+ } else { console.log(chalk.red(`\n✖ Agente não encontrado.\n`)); }
256
243
  continue;
257
244
  }
258
245
 
259
- if (cmd === '/clear') {
260
- resetMessages();
261
- console.clear();
262
- console.log(lavender('✓ Histórico limpo, contexto preservado.\n'));
263
- continue;
264
- }
246
+ if (cmd === '/clear') { resetMessages(); console.clear(); continue; }
265
247
 
266
248
  if (cmd === '/help') {
267
- console.log(gray(`
268
- Comandos de Modo:
269
- /chat → Modo conversa
270
- /plan → Modo planejamento
271
- /edit [auto/manual] → Modo edição (padrão manual)
272
- /use [agente] → Usar um Agente Especialista
273
- /use normal → Voltar para o chat normal
274
- /swarm → Rodar fluxos complexos
275
-
276
- Gerenciamento:
277
- /switch [nome] → Mudar perfil de IA completo
278
- /model [nome] → Mudar modelo atual
279
- /config → Perfis e Agentes
280
- /init → Inicializar .bimmorc.json
281
- @arquivo → Ler arquivo ou imagem
282
- `));
249
+ console.log(gray(`\nComandos:\n /chat | /plan | /edit [auto/manual] | /init\n /switch [nome] | /model [nome] | /use [agente]\n /config | /clear | @arquivo\n`));
283
250
  continue;
284
251
  }
285
252
 
286
253
  if (cmd === '/config') { await configure(); config = getConfig(); provider = createProvider(config); continue; }
287
254
 
288
- if (cmd === '/init') {
289
- const bimmoRcPath = path.join(process.cwd(), '.bimmorc.json');
290
- if (fs.existsSync(bimmoRcPath)) {
291
- const { overwrite } = await inquirer.prompt([{
292
- type: 'confirm',
293
- name: 'overwrite',
294
- message: 'O arquivo .bimmorc.json já existe. Deseja sobrescrever?',
295
- default: false
296
- }]);
297
- if (!overwrite) continue;
298
- }
299
- const initialConfig = {
300
- projectName: path.basename(process.cwd()),
301
- rules: ["Siga as convenções existentes.", "Prefira código modular."],
302
- preferredTech: [],
303
- ignorePatterns: ["node_modules", ".git"]
304
- };
305
- fs.writeFileSync(bimmoRcPath, JSON.stringify(initialConfig, null, 2));
306
- console.log(green(`\n✅ .bimmorc.json criado com sucesso.\n`));
307
-
308
- resetMessages();
309
- continue;
310
- }
311
-
312
- if (cmd === '/swarm') {
313
- const agents = config.agents || {};
314
- const agentList = Object.keys(agents);
315
- if (agentList.length < 2) {
316
- console.log(chalk.yellow('\nCrie pelo menos 2 Agentes em /config primeiro.\n'));
317
- continue;
318
- }
319
- const { swarmAction } = await inquirer.prompt([{
320
- type: 'list',
321
- name: 'swarmAction',
322
- message: 'Tipo de Enxame:',
323
- choices: ['Sequencial (A → B)', 'Hierárquico (Líder + Workers)', 'Voltar']
324
- }]);
325
- if (swarmAction === 'Voltar') continue;
326
- const { goal } = await inquirer.prompt([{ type: 'input', name: 'goal', message: 'Objetivo do enxame:' }]);
327
-
328
- try {
329
- let result;
330
- if (swarmAction.includes('Sequencial')) {
331
- const { selectedAgents } = await inquirer.prompt([{ type: 'checkbox', name: 'selectedAgents', message: 'Ordem dos agentes:', choices: agentList }]);
332
- result = await orchestrator.runSequential(selectedAgents, goal);
333
- } else {
334
- const { manager } = await inquirer.prompt([{ type: 'list', name: 'manager', message: 'Líder:', choices: agentList }]);
335
- const { workers } = await inquirer.prompt([{ type: 'checkbox', name: 'workers', message: 'Workers:', choices: agentList.filter(a => a !== manager) }]);
336
- result = await orchestrator.runHierarchical(manager, workers, goal);
337
- }
338
- console.log(lavender('\n=== RESULTADO FINAL ===\n') + marked(result));
339
- } catch (e) {
340
- console.error(chalk.red(`\nErro: ${e.message}`));
341
- }
342
- continue;
343
- }
344
-
345
255
  if (rawInput === '') continue;
346
256
 
257
+ // Enviar para a IA
347
258
  const controller = new AbortController();
348
259
  const localInterruptHandler = () => controller.abort();
349
-
350
- // Switch de SIGINT para modo processamento
351
260
  process.removeListener('SIGINT', globalSigIntHandler);
352
261
  process.on('SIGINT', localInterruptHandler);
353
262
 
354
263
  let modeInstr = "";
355
264
  if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Apenas analise.";
356
- else if (currentMode === 'edit') modeInstr = `\n[MODO EDIT] Você tem permissão para usar ferramentas. (Auto-Accept: ${editState.autoAccept ? 'ON' : 'OFF'})`;
265
+ else if (currentMode === 'edit') modeInstr = `\n[MODO EDIT] Auto-Accept: ${editState.autoAccept ? 'ON' : 'OFF'}`;
357
266
 
358
267
  const content = await processInput(rawInput);
359
- messages.push({
360
- role: 'user',
361
- content: typeof content === 'string' ? content + modeInstr : [...content, { type: 'text', text: modeInstr }]
362
- });
268
+ messages.push({ role: 'user', content: typeof content === 'string' ? content + modeInstr : [...content, { type: 'text', text: modeInstr }] });
363
269
 
364
- const spinner = ora({
365
- text: lavender(`bimmo pensando... (Ctrl+C para interromper)`),
366
- color: currentMode === 'edit' ? 'red' : 'magenta'
367
- }).start();
270
+ const spinner = ora({ text: lavender(`bimmo pensando... (Ctrl+C para interromper)`), color: currentMode === 'edit' ? 'red' : 'magenta' }).start();
368
271
 
369
272
  try {
370
273
  let responseText = await provider.sendMessage(messages, { signal: controller.signal });
371
274
  spinner.stop();
372
275
 
373
- // LIMPEZA AGRESSIVA DE HTML
374
- const cleanedText = responseText
375
- .replace(/<br\s*\/?>/gi, '\n') // Converte <br> em newline real
376
- .replace(/<p>/gi, '') // Remove tags <p> iniciais
377
- .replace(/<\/p>/gi, '\n\n') // Converte </p> em double newline
378
- .replace(/<\/?[^>]+(>|$)/g, ""); // Remove QUALQUER outra tag residual
379
-
276
+ const cleanedText = cleanAIResponse(responseText);
380
277
  messages.push({ role: 'assistant', content: responseText });
278
+
381
279
  console.log('\n' + lavender('bimmo ') + getModeStyle());
382
280
  console.log(lavender('─'.repeat(50)));
383
- console.log(marked(cleanedText.trim()));
281
+ console.log(marked(cleanedText));
384
282
  console.log(gray('─'.repeat(50)) + '\n');
385
283
  } catch (err) {
386
284
  spinner.stop();
387
285
  if (controller.signal.aborted || err.name === 'AbortError') {
388
- console.log(yellow('\n\n⚠️ Operação interrompida pelo usuário.\n'));
286
+ console.log(yellow('\n⚠️ Interrompido.\n'));
389
287
  messages.pop();
390
288
  } else {
391
289
  console.error(chalk.red('\n✖ Erro:') + ' ' + err.message + '\n');
392
290
  }
393
291
  } finally {
394
- // Restaura o modo global de saída
395
292
  process.removeListener('SIGINT', localInterruptHandler);
396
293
  process.on('SIGINT', globalSigIntHandler);
397
294
  }