bimmo-cli 2.2.3 → 2.2.5

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 +3 -17
  2. package/src/interface.js +143 -193
package/package.json CHANGED
@@ -1,25 +1,13 @@
1
1
  {
2
2
  "name": "bimmo-cli",
3
- "version": "2.2.3",
4
- "description": "🌿 Plataforma de IA universal com Modo Normal, Agentes e Swarms. Suporte a Autocomplete de arquivos, Diffs coloridos e Contexto Inteligente.",
3
+ "version": "2.2.5",
4
+ "description": "🌿 Plataforma de IA universal profissional com Agentes e Swarms. Suporte a Autocomplete, Diffs e Contexto Inteligente.",
5
5
  "bin": {
6
6
  "bimmo": "bin/bimmo"
7
7
  },
8
8
  "type": "module",
9
9
  "keywords": [
10
- "ai",
11
- "cli",
12
- "openai",
13
- "anthropic",
14
- "gemini",
15
- "grok",
16
- "deepseek",
17
- "openrouter",
18
- "zai",
19
- "agent",
20
- "swarm",
21
- "autocomplete",
22
- "terminal"
10
+ "ai", "cli", "openai", "anthropic", "gemini", "grok", "deepseek", "openrouter", "zai", "agent", "swarm", "terminal"
23
11
  ],
24
12
  "author": "Judah",
25
13
  "license": "MIT",
@@ -41,9 +29,7 @@
41
29
  "conf": "^13.0.0",
42
30
  "diff": "^7.0.0",
43
31
  "figlet": "^1.7.0",
44
- "fuzzy": "^0.1.3",
45
32
  "inquirer": "^9.3.8",
46
- "inquirer-autocomplete-prompt": "^3.0.1",
47
33
  "marked": "^14.0.0",
48
34
  "marked-terminal": "^7.0.0",
49
35
  "mime-types": "^2.1.35",
package/src/interface.js CHANGED
@@ -1,8 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import figlet from 'figlet';
3
3
  import inquirer from 'inquirer';
4
- import autocompletePrompt from 'inquirer-autocomplete-prompt';
5
- import fuzzy from 'fuzzy';
6
4
  import { marked } from 'marked';
7
5
  import TerminalRenderer from 'marked-terminal';
8
6
  import ora from 'ora';
@@ -18,18 +16,20 @@ import { getProjectContext } from './project-context.js';
18
16
  import { SwarmOrchestrator } from './orchestrator.js';
19
17
  import { editState } from './agent.js';
20
18
 
21
- // Registrar plugin de autocomplete
22
- inquirer.registerPrompt('autocomplete', autocompletePrompt);
23
-
24
19
  const __filename = fileURLToPath(import.meta.url);
25
20
  const __dirname = path.dirname(__filename);
26
21
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
27
22
  const version = pkg.version;
28
23
 
29
- marked.use(new TerminalRenderer({
24
+ // CONFIGURAÇÃO DO RENDERIZADOR (CORREÇÃO DEFINITIVA DO <P>)
25
+ const terminalRenderer = new TerminalRenderer({
30
26
  heading: chalk.hex('#c084fc').bold,
31
27
  code: chalk.hex('#00ff9d'),
32
- }));
28
+ strong: chalk.bold,
29
+ em: chalk.italic,
30
+ });
31
+
32
+ marked.setOptions({ renderer: terminalRenderer });
33
33
 
34
34
  const green = chalk.hex('#00ff9d');
35
35
  const lavender = chalk.hex('#c084fc');
@@ -42,84 +42,39 @@ let activePersona = null;
42
42
  let exitCounter = 0;
43
43
  let exitTimer = null;
44
44
 
45
- /**
46
- * Procura arquivos e diretórios recursivamente para o autocomplete do @
47
- */
48
- function getFiles(dir, filter = '') {
49
- const results = [];
50
- try {
51
- const list = fs.readdirSync(dir);
52
- for (const file of list) {
53
- if (file === 'node_modules' || file === '.git') continue;
54
- const fullPath = path.join(dir, file);
55
- const relPath = path.relative(process.cwd(), fullPath);
56
- if (relPath.toLowerCase().includes(filter.toLowerCase())) {
57
- results.push(relPath);
58
- }
59
- if (fs.statSync(fullPath).isDirectory()) {
60
- // Limitamos profundidade para performance se necessário
61
- }
62
- }
63
- } catch (e) {}
64
- return results;
65
- }
66
-
67
- async function processInput(input) {
68
- const parts = input.split(' ');
69
- const processedContent = [];
70
-
71
- for (const part of parts) {
72
- if (part.startsWith('@')) {
73
- const filePath = part.slice(1);
74
- try {
75
- if (fs.existsSync(filePath)) {
76
- const stats = fs.statSync(filePath);
77
- if (stats.isFile()) {
78
- const mimeType = mime.lookup(filePath) || 'application/octet-stream';
79
- if (mimeType.startsWith('image/')) {
80
- const base64Image = fs.readFileSync(filePath, { encoding: 'base64' });
81
- processedContent.push({ type: 'image', mimeType, data: base64Image, fileName: path.basename(filePath) });
82
- } else {
83
- const textContent = fs.readFileSync(filePath, 'utf-8');
84
- processedContent.push({ type: 'text', text: `\n--- Arquivo: ${path.basename(filePath)} ---\n${textContent}\n--- Fim do arquivo ---\n` });
85
- }
86
- }
87
- } else {
88
- processedContent.push({ type: 'text', text: part });
89
- }
90
- } catch (err) {
91
- processedContent.push({ type: 'text', text: part });
92
- }
93
- } else {
94
- processedContent.push({ type: 'text', text: part });
95
- }
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'
96
65
  }
66
+ };
97
67
 
98
- const hasImage = processedContent.some(c => c.type === 'image');
99
- if (!hasImage) return processedContent.map(c => c.text).join(' ');
100
-
101
- const finalContent = [];
102
- let currentText = "";
103
- for (const item of processedContent) {
104
- if (item.type === 'text') {
105
- currentText += (currentText ? " " : "") + item.text;
106
- } else {
107
- if (currentText) { finalContent.push({ type: 'text', text: currentText }); currentText = ""; }
108
- finalContent.push(item);
109
- }
110
- }
111
- if (currentText) finalContent.push({ type: 'text', text: currentText });
112
- return finalContent;
113
- }
114
-
115
- function getModeStyle() {
116
- const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]` : '';
117
- switch (currentMode) {
118
- case 'plan': return yellow.bold(`${personaLabel}[PLAN] `);
119
- case 'edit':
120
- const editSubMode = editState.autoAccept ? '(AUTO)' : '(MANUAL)';
121
- return chalk.red.bold(`${personaLabel}[EDIT${editSubMode}] `);
122
- default: return lavender.bold(`${personaLabel}[CHAT] `);
68
+ function getFilesForCompletion(partialPath) {
69
+ 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));
73
+ 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 [];
123
78
  }
124
79
  }
125
80
 
@@ -127,22 +82,19 @@ function cleanAIResponse(text) {
127
82
  if (!text) return "";
128
83
  return text
129
84
  .replace(/<br\s*\/?>/gi, '\n')
85
+ .replace(/<p>/gi, '')
130
86
  .replace(/<\/p>/gi, '\n\n')
131
- .replace(/<\/div>/gi, '\n')
132
87
  .replace(/<[^>]*>?/gm, '')
133
- .replace(/&nbsp;/g, ' ')
134
- .replace(/&lt;/g, '<')
135
- .replace(/&gt;/g, '>')
136
- .replace(/&amp;/g, '&')
137
88
  .trim();
138
89
  }
139
90
 
140
91
  export async function startInteractive() {
141
92
  let config = getConfig();
93
+ const lang = config.language || 'pt-BR';
94
+ const t = i18n[lang] || i18n['pt-BR'];
142
95
 
143
96
  if (!config.provider || !config.apiKey) {
144
97
  console.log(lavender(figlet.textSync('bimmo')));
145
- console.log(gray('\nBem-vindo! Vamos configurar seus perfis de IA.\n'));
146
98
  await configure();
147
99
  return startInteractive();
148
100
  }
@@ -153,11 +105,10 @@ export async function startInteractive() {
153
105
 
154
106
  const resetMessages = () => {
155
107
  messages = [];
156
- const projectContext = getProjectContext();
157
- messages.push({ role: 'system', content: projectContext });
108
+ messages.push({ role: 'system', content: getProjectContext() });
158
109
  if (activePersona) {
159
110
  const agent = (config.agents || {})[activePersona];
160
- if (agent) messages.push({ role: 'system', content: `Sua persona atual é: ${agent.name}. Sua tarefa: ${agent.role}` });
111
+ if (agent) messages.push({ role: 'system', content: `Persona: ${agent.name}. Task: ${agent.role}` });
161
112
  }
162
113
  };
163
114
 
@@ -168,157 +119,156 @@ export async function startInteractive() {
168
119
  console.log(lavender(` v${version} `.padStart(60, '─')));
169
120
  console.log(green(` Perfil: ${bold(config.activeProfile || 'Padrão')} • IA: ${bold(config.provider.toUpperCase())}`));
170
121
  console.log(green(` Modelo: ${bold(config.model)}`));
171
- console.log(gray(` 📁 ${process.cwd()}`));
172
- console.log(gray(' /chat | /plan | /edit | /swarm | /use [agente] | /help'));
173
122
  console.log(lavender('─'.repeat(60)) + '\n');
174
123
 
175
- console.log(lavender('👋 Olá! Estou pronto. No que posso ajudar?\n'));
124
+ console.log(lavender(`👋 ${t.welcome}\n`));
125
+
126
+ const rl = readline.createInterface({
127
+ input: process.stdin,
128
+ output: process.stdout,
129
+ terminal: true,
130
+ historySize: 100,
131
+ completer: (line) => {
132
+ const words = line.split(' ');
133
+ const lastWord = words[words.length - 1];
134
+ if (lastWord.startsWith('@')) {
135
+ const hits = getFilesForCompletion(lastWord);
136
+ return [hits.map(h => `@${h}`), lastWord];
137
+ }
138
+ return [[], line];
139
+ }
140
+ });
176
141
 
177
- const globalSigIntHandler = () => {
142
+ // Handler de saída
143
+ rl.on('SIGINT', () => {
178
144
  exitCounter++;
179
145
  if (exitCounter === 1) {
180
- process.stdout.write(gray('\n(Pressione Ctrl+C novamente para sair)\n'));
146
+ console.log(`\n${gray(t.exitHint)}`);
181
147
  if (exitTimer) clearTimeout(exitTimer);
182
148
  exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
149
+ displayPrompt();
183
150
  } else {
184
- process.stdout.write(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
151
+ console.log(lavender('\n👋 BIMMO encerrando sessão.\n'));
185
152
  process.exit(0);
186
153
  }
187
- };
154
+ });
188
155
 
189
- process.on('SIGINT', globalSigIntHandler);
190
- readline.emitKeypressEvents(process.stdin);
191
- if (process.stdin.isTTY) process.stdin.setRawMode(true);
192
-
193
- while (true) {
194
- const modeIndicator = getModeStyle();
195
- let input;
156
+ const displayPrompt = () => {
157
+ const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]` : '';
158
+ let modeLabel = `[${currentMode.toUpperCase()}]`;
159
+ if (currentMode === 'edit') modeLabel = editState.autoAccept ? '[EDIT(AUTO)]' : '[EDIT(MANUAL)]';
196
160
 
197
- try {
198
- // Usamos autocomplete para permitir navegação de arquivos com @
199
- const answers = await inquirer.prompt([
200
- {
201
- type: 'autocomplete',
202
- name: 'input',
203
- message: modeIndicator + green('>'),
204
- prefix: '',
205
- source: async (answersSoFar, inputSearch) => {
206
- const currentInput = inputSearch || '';
207
-
208
- // Se o usuário digitar @, oferecemos arquivos
209
- if (currentInput.includes('@')) {
210
- const lastWord = currentInput.split(' ').pop();
211
- if (lastWord.startsWith('@')) {
212
- const searchPath = lastWord.slice(1);
213
- const files = getFiles(process.cwd(), searchPath);
214
- return files.map(f => ({
215
- name: `@${f} ${fs.statSync(f).isDirectory() ? '(DIR)' : '(FILE)'}`,
216
- value: currentInput.substring(0, currentInput.lastIndexOf('@')) + '@' + f
217
- }));
218
- }
219
- }
220
-
221
- // Caso contrário, apenas retorna o que ele está digitando como única opção
222
- // para não atrapalhar o chat normal
223
- return [currentInput];
224
- }
225
- }
226
- ]);
227
- input = answers.input;
228
- } catch (e) { continue; }
229
-
230
- // Diretório constante
231
- console.log(gray(` 📁 ${process.cwd()}`));
161
+ console.log(`\n${gray(`📁 ${process.cwd()}`)}`);
162
+ rl.setPrompt(lavender.bold(personaLabel) + (currentMode === 'edit' ? chalk.red.bold(modeLabel) : lavender.bold(modeLabel)) + green(' > '));
163
+ rl.prompt();
164
+ };
232
165
 
166
+ displayPrompt();
167
+
168
+ rl.on('line', async (input) => {
233
169
  const rawInput = input.trim();
234
170
  const cmd = rawInput.toLowerCase();
235
171
 
236
- if (cmd === '/exit' || cmd === 'exit' || cmd === 'sair') { process.exit(0); }
237
- if (cmd === '/chat') { currentMode = 'chat'; console.log(lavender('✓ Modo CHAT.\n')); continue; }
238
- if (cmd === '/plan') { currentMode = 'plan'; console.log(yellow('✓ Modo PLAN.\n')); continue; }
239
- if (cmd === '/edit') { currentMode = 'edit'; console.log(chalk.red(`⚠️ Modo EDIT ativado.\n`)); continue; }
240
- if (cmd === '/edit auto') { currentMode = 'edit'; editState.autoAccept = true; console.log(chalk.red('⚠️ Modo EDIT (AUTO) ativado.\n')); continue; }
241
- if (cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false; console.log(chalk.red('⚠️ Modo EDIT (MANUAL) ativado.\n')); continue; }
172
+ if (rawInput === '') { displayPrompt(); return; }
242
173
 
174
+ // COMANDOS INTERNOS
175
+ if (cmd === '/exit' || cmd === 'sair') process.exit(0);
176
+ if (cmd === '/chat') { currentMode = 'chat'; displayPrompt(); return; }
177
+ if (cmd === '/plan') { currentMode = 'plan'; displayPrompt(); return; }
178
+ if (cmd === '/edit' || cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false; displayPrompt(); return; }
179
+ if (cmd === '/edit auto') { currentMode = 'edit'; editState.autoAccept = true; displayPrompt(); return; }
180
+
181
+ if (cmd === '/clear') { resetMessages(); console.clear(); displayPrompt(); return; }
182
+ if (cmd === '/help') { console.log(gray(t.help)); displayPrompt(); return; }
183
+
243
184
  if (cmd === '/init') {
244
185
  const bimmoRcPath = path.join(process.cwd(), '.bimmorc.json');
245
- if (fs.existsSync(bimmoRcPath)) {
246
- const { overwrite } = await inquirer.prompt([{ type: 'confirm', name: 'overwrite', message: 'O arquivo .bimmorc.json já existe. Sobrescrever?', default: false }]);
247
- if (!overwrite) continue;
248
- }
249
- const initialConfig = { projectName: path.basename(process.cwd()), rules: ["Siga as convenções."], ignorePatterns: ["node_modules", ".git"] };
186
+ const initialConfig = { projectName: path.basename(process.cwd()), rules: ["Clean code"], ignorePatterns: ["node_modules"] };
250
187
  fs.writeFileSync(bimmoRcPath, JSON.stringify(initialConfig, null, 2));
251
- console.log(green(`\n✅ .bimmorc.json criado com sucesso.\n`));
188
+ console.log(green(`\n✅ .bimmorc.json criado.`));
252
189
  resetMessages();
253
- continue;
190
+ displayPrompt();
191
+ return;
192
+ }
193
+
194
+ if (cmd === '/config') {
195
+ rl.pause();
196
+ await configure();
197
+ config = getConfig();
198
+ provider = createProvider(config);
199
+ rl.resume();
200
+ displayPrompt();
201
+ return;
254
202
  }
255
203
 
256
204
  if (cmd.startsWith('/switch ')) {
257
- const profileName = rawInput.split(' ')[1];
258
- if (profileName && switchProfile(profileName)) {
205
+ const pName = rawInput.split(' ')[1];
206
+ if (switchProfile(pName)) {
259
207
  config = getConfig(); provider = createProvider(config);
260
- console.log(green(`\n✓ Perfil "${bold(profileName)}" ativado!`));
261
- continue;
262
- }
263
- console.log(chalk.red(`\n✖ Perfil não encontrado.\n`)); continue;
208
+ console.log(green(`\n✓ ${t.switchOk}`));
209
+ } else { console.log(chalk.red(`\n✖ Perfil não encontrado.`)); }
210
+ displayPrompt(); return;
264
211
  }
265
212
 
266
213
  if (cmd.startsWith('/use ')) {
267
- const agentName = rawInput.split(' ')[1];
214
+ const aName = rawInput.split(' ')[1];
215
+ if (aName === 'normal') { activePersona = null; resetMessages(); displayPrompt(); return; }
268
216
  const agents = config.agents || {};
269
- if (agentName === 'normal' || agentName === 'default') { activePersona = null; resetMessages(); continue; }
270
- if (agents[agentName]) {
271
- activePersona = agentName;
272
- const agent = agents[agentName];
217
+ if (agents[aName]) {
218
+ activePersona = aName;
219
+ const agent = agents[aName];
273
220
  if (switchProfile(agent.profile)) { config = getConfig(); provider = createProvider(config); }
274
221
  currentMode = agent.mode || 'chat';
275
- console.log(green(`\n✓ Ativado Agente: ${bold(agentName)}`));
222
+ console.log(green(`\n✓ ${t.agentOk} ${bold(aName)}`));
276
223
  resetMessages();
277
- } else { console.log(chalk.red(`\n✖ Agente não encontrado.\n`)); }
278
- continue;
224
+ } else { console.log(chalk.red(`\n✖ Agente não encontrado.`)); }
225
+ displayPrompt(); return;
279
226
  }
280
227
 
281
- if (cmd === '/clear') { resetMessages(); console.clear(); continue; }
282
-
283
- if (cmd === '/help') {
284
- console.log(gray(`\nComandos:\n /chat | /plan | /edit [auto/manual] | /init\n /switch [nome] | /model [nome] | /use [agente]\n /config | /clear | @arquivo\n`));
285
- continue;
286
- }
287
-
288
- if (cmd === '/config') { await configure(); config = getConfig(); provider = createProvider(config); continue; }
289
-
290
- if (rawInput === '') continue;
291
-
228
+ // PROCESSAMENTO IA
292
229
  const controller = new AbortController();
293
- const localInterruptHandler = () => controller.abort();
294
- process.removeListener('SIGINT', globalSigIntHandler);
295
- process.on('SIGINT', localInterruptHandler);
230
+ const abortHandler = () => controller.abort();
231
+ process.on('SIGINT', abortHandler);
296
232
 
297
233
  let modeInstr = "";
298
234
  if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Apenas analise.";
299
235
  else if (currentMode === 'edit') modeInstr = `\n[MODO EDIT] Auto-Accept: ${editState.autoAccept ? 'ON' : 'OFF'}`;
300
236
 
301
- const content = await processInput(rawInput);
302
- messages.push({ role: 'user', content: typeof content === 'string' ? content + modeInstr : [...content, { type: 'text', text: modeInstr }] });
237
+ // Processar anexos @
238
+ const processedContent = [];
239
+ const words = rawInput.split(' ');
240
+ for (const word of words) {
241
+ if (word.startsWith('@')) {
242
+ const filePath = word.slice(1);
243
+ 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` });
246
+ } else { processedContent.push({ type: 'text', text: word }); }
247
+ } else { processedContent.push({ type: 'text', text: word }); }
248
+ }
249
+
250
+ messages.push({ role: 'user', content: [...processedContent, { type: 'text', text: modeInstr }] });
303
251
 
304
- const spinner = ora({ text: lavender(`bimmo pensando... (Ctrl+C para interromper)`), color: currentMode === 'edit' ? 'red' : 'magenta' }).start();
252
+ const spinner = ora({ text: lavender(`${t.thinking} (Ctrl+C para parar)`), color: currentMode === 'edit' ? 'red' : 'magenta' }).start();
305
253
 
306
254
  try {
307
255
  let responseText = await provider.sendMessage(messages, { signal: controller.signal });
308
256
  spinner.stop();
257
+
309
258
  const cleanedText = cleanAIResponse(responseText);
310
259
  messages.push({ role: 'assistant', content: responseText });
311
- console.log('\n' + lavender('bimmo ') + getModeStyle());
260
+
261
+ console.log(`\n${lavender('bimmo ')}${currentMode.toUpperCase()}`);
312
262
  console.log(lavender('─'.repeat(50)));
313
- console.log(marked(cleanedText));
314
- console.log(gray('─'.repeat(50)) + '\n');
263
+ console.log(marked.parse(cleanedText)); // Usamos parse para garantir o renderer terminal
264
+ console.log(gray('─'.repeat(50)));
315
265
  } catch (err) {
316
266
  spinner.stop();
317
- if (controller.signal.aborted || err.name === 'AbortError') { console.log(yellow('\n⚠️ Interrompido.\n')); messages.pop(); }
318
- else { console.error(chalk.red('\n✖ Erro:') + ' ' + err.message + '\n'); }
267
+ if (controller.signal.aborted) { console.log(yellow(`\n⚠️ ${t.interrupted}`)); messages.pop(); }
268
+ else { console.error(chalk.red(`\n✖ Erro: ${err.message}`)); }
319
269
  } finally {
320
- process.removeListener('SIGINT', localInterruptHandler);
321
- process.on('SIGINT', globalSigIntHandler);
270
+ process.removeListener('SIGINT', abortHandler);
271
+ displayPrompt();
322
272
  }
323
- }
273
+ });
324
274
  }