bimmo-cli 2.2.8 → 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.
Files changed (2) hide show
  1. package/package.json +3 -2
  2. package/src/interface.js +54 -120
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bimmo-cli",
3
- "version": "2.2.8",
4
- "description": "🌿 Plataforma de IA universal profissional com Agentes e Swarms. Suporte a Autocomplete real-time, Diffs coloridos 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/interface.js CHANGED
@@ -8,6 +8,7 @@ import path from 'path';
8
8
  import mime from 'mime-types';
9
9
  import readline from 'readline';
10
10
  import { fileURLToPath } from 'url';
11
+ import { stripVTControlCharacters } from 'util';
11
12
 
12
13
  import { getConfig, configure, updateActiveModel, switchProfile } from './config.js';
13
14
  import { createProvider } from './providers/factory.js';
@@ -20,13 +21,13 @@ const __dirname = path.dirname(__filename);
20
21
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
21
22
  const version = pkg.version;
22
23
 
23
- // CONFIGURAÇÃO DO RENDERIZADOR - Ignora HTML e foca no Terminal
24
+ // Configuração do renderizador - DROP TOTAL DE HTML
24
25
  const terminalRenderer = new TerminalRenderer({
25
26
  heading: chalk.hex('#c084fc').bold,
26
27
  code: chalk.hex('#00ff9d'),
27
28
  strong: chalk.bold,
28
29
  em: chalk.italic,
29
- html: () => '', // DROP TOTAL DE QUALQUER TAG HTML
30
+ html: (html) => '', // Remove qualquer tag HTML que sobrar
30
31
  });
31
32
 
32
33
  marked.setOptions({ renderer: terminalRenderer });
@@ -42,37 +43,11 @@ 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
- /**
69
- * Coleta arquivos para preview e completion
70
- */
71
46
  function getFilesForPreview(partialPath) {
72
47
  try {
73
- const cleanPartial = partialPath.startsWith('@') ? partialPath.slice(1) : partialPath;
74
- const dir = path.dirname(cleanPartial) === '.' && !cleanPartial.includes('/') ? '.' : path.dirname(cleanPartial);
75
- const base = path.basename(cleanPartial);
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);
76
51
 
77
52
  const searchDir = path.resolve(process.cwd(), dir);
78
53
  if (!fs.existsSync(searchDir)) return [];
@@ -100,9 +75,6 @@ function cleanAIResponse(text) {
100
75
 
101
76
  export async function startInteractive() {
102
77
  let config = getConfig();
103
- const lang = config.language || 'pt-BR';
104
- const t = i18n[lang] || i18n['pt-BR'];
105
-
106
78
  if (!config.provider || !config.apiKey) {
107
79
  console.log(lavender(figlet.textSync('bimmo')));
108
80
  await configure();
@@ -131,13 +103,10 @@ export async function startInteractive() {
131
103
  console.log(green(` Modelo: ${bold(config.model)}`));
132
104
  console.log(lavender('─'.repeat(60)) + '\n');
133
105
 
134
- console.log(lavender(`👋 ${t.welcome}\n`));
135
-
136
106
  const rl = readline.createInterface({
137
107
  input: process.stdin,
138
108
  output: process.stdout,
139
109
  terminal: true,
140
- historySize: 100,
141
110
  completer: (line) => {
142
111
  const words = line.split(' ');
143
112
  const lastWord = words[words.length - 1];
@@ -152,130 +121,101 @@ export async function startInteractive() {
152
121
  let currentPreviewLines = 0;
153
122
 
154
123
  const clearPreview = () => {
155
- for (let i = 0; i < currentPreviewLines; i++) {
156
- readline.moveCursor(process.stdout, 0, 1);
157
- readline.clearLine(process.stdout, 0);
158
- }
159
124
  if (currentPreviewLines > 0) {
160
- readline.moveCursor(process.stdout, 0, -currentPreviewLines);
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;
161
132
  }
162
- currentPreviewLines = 0;
163
133
  };
164
134
 
165
- const showPreview = (line) => {
135
+ const showPreview = () => {
166
136
  clearPreview();
167
- const words = line.split(' ');
137
+ const words = rl.line.split(' ');
168
138
  const lastWord = words[words.length - 1];
169
139
 
170
140
  if (lastWord.startsWith('@')) {
171
141
  const files = getFilesForPreview(lastWord);
172
142
  if (files.length > 0) {
173
143
  process.stdout.write('\n');
174
- files.slice(0, 10).forEach(f => {
144
+ const displayFiles = files.slice(0, 8);
145
+ displayFiles.forEach(f => {
175
146
  process.stdout.write(gray(` ${f.isDir ? '📁' : '📄'} ${f.name}\n`));
176
147
  });
177
- currentPreviewLines = Math.min(files.length, 10) + 1;
148
+ currentPreviewLines = displayFiles.length + 1;
149
+ // Volta o cursor para a linha do prompt de forma segura
178
150
  readline.moveCursor(process.stdout, 0, -currentPreviewLines);
179
- readline.cursorTo(process.stdout, rl.line.length + rl.getPrompt().length);
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);
180
157
  }
181
158
  }
182
159
  };
183
160
 
184
- // Monitora digitação em tempo real
161
+ const displayPrompt = () => {
162
+ const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]` : '';
163
+ let modeLabel = `[${currentMode.toUpperCase()}]`;
164
+ if (currentMode === 'edit') modeLabel = editState.autoAccept ? '[EDIT(AUTO)]' : '[EDIT(MANUAL)]';
165
+
166
+ console.log(`\n${gray(`📁 ${process.cwd()}`)}`);
167
+ rl.setPrompt(lavender.bold(personaLabel) + (currentMode === 'edit' ? chalk.red.bold(modeLabel) : lavender.bold(modeLabel)) + green(' > '));
168
+ rl.prompt();
169
+ };
170
+
171
+ // Escuta teclas para o preview em tempo real
185
172
  process.stdin.on('keypress', (s, key) => {
186
- // Pequeno delay para o readline atualizar a linha interna
187
- setImmediate(() => showPreview(rl.line));
173
+ if (key && (key.name === 'return' || key.name === 'enter')) return;
174
+ setImmediate(() => showPreview());
188
175
  });
189
176
 
190
177
  rl.on('SIGINT', () => {
191
178
  if (exitCounter === 0) {
192
179
  exitCounter++;
193
- console.log(`\n${gray(t.exitHint)}`);
180
+ process.stdout.write(`\n${gray('(Pressione novamente para sair)')}\n`);
194
181
  exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
195
182
  displayPrompt();
196
- } else {
197
- process.exit(0);
198
- }
183
+ } else { process.exit(0); }
199
184
  });
200
185
 
201
- const displayPrompt = () => {
202
- const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]` : '';
203
- let modeLabel = `[${currentMode.toUpperCase()}]`;
204
- if (currentMode === 'edit') modeLabel = editState.autoAccept ? '[EDIT(AUTO)]' : '[EDIT(MANUAL)]';
205
-
206
- console.log(`${gray(`📁 ${process.cwd()}`)}`);
207
- rl.setPrompt(lavender.bold(personaLabel) + (currentMode === 'edit' ? chalk.red.bold(modeLabel) : lavender.bold(modeLabel)) + green(' > '));
208
- rl.prompt();
209
- };
210
-
211
186
  displayPrompt();
212
187
 
213
188
  rl.on('line', async (input) => {
214
189
  clearPreview();
215
190
  const rawInput = input.trim();
216
- const cmd = rawInput.toLowerCase();
217
-
218
191
  if (rawInput === '') { displayPrompt(); return; }
219
192
 
193
+ const cmd = rawInput.toLowerCase();
220
194
  if (cmd === '/exit' || cmd === 'sair') process.exit(0);
221
195
  if (cmd === '/chat') { currentMode = 'chat'; displayPrompt(); return; }
222
196
  if (cmd === '/plan') { currentMode = 'plan'; displayPrompt(); return; }
223
197
  if (cmd === '/edit' || cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false; displayPrompt(); return; }
224
198
  if (cmd === '/edit auto') { currentMode = 'edit'; editState.autoAccept = true; displayPrompt(); return; }
225
199
  if (cmd === '/clear') { resetMessages(); console.clear(); displayPrompt(); return; }
226
- if (cmd === '/help') { console.log(gray(t.help)); displayPrompt(); return; }
227
-
200
+
228
201
  if (cmd === '/init') {
229
- console.log(chalk.cyan('\n🚀 Analisando projeto para gerar .bimmorc.json inteligente...\n'));
230
- const initPrompt = `Analise a estrutura atual deste projeto e crie um arquivo chamado .bimmorc.json na raiz com nome, regras, stack e arquitetura. Use write_file.`;
231
-
232
- const controller = new AbortController();
233
- const spinner = ora({ text: lavender(`${t.thinking}`), color: 'red' }).start();
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();
234
205
  try {
235
- const res = await provider.sendMessage([...messages, { role: 'user', content: initPrompt }], { signal: controller.signal });
206
+ const res = await provider.sendMessage([...messages, { role: 'user', content: initPrompt }]);
236
207
  spinner.stop();
237
208
  console.log(marked.parse(cleanAIResponse(res)));
238
209
  } catch (e) { spinner.stop(); console.error(chalk.red(e.message)); }
239
- resetMessages();
240
- displayPrompt();
241
- return;
242
- }
243
-
244
- if (cmd === '/config') {
245
- rl.pause(); await configure(); config = getConfig(); provider = createProvider(config); rl.resume();
246
- displayPrompt(); return;
247
- }
248
-
249
- if (cmd.startsWith('/switch ')) {
250
- const pName = rawInput.split(' ')[1];
251
- if (switchProfile(pName)) {
252
- config = getConfig(); provider = createProvider(config);
253
- console.log(green(`\n✓ ${t.switchOk}`));
254
- } else { console.log(chalk.red(`\n✖ Perfil não encontrado.`)); }
255
- displayPrompt(); return;
210
+ resetMessages(); displayPrompt(); return;
256
211
  }
257
212
 
258
- if (cmd.startsWith('/use ')) {
259
- const aName = rawInput.split(' ')[1];
260
- if (aName === 'normal') { activePersona = null; resetMessages(); displayPrompt(); return; }
261
- const agents = config.agents || {};
262
- if (agents[aName]) {
263
- activePersona = aName;
264
- const agent = agents[aName];
265
- if (switchProfile(agent.profile)) { config = getConfig(); provider = createProvider(config); }
266
- currentMode = agent.mode || 'chat';
267
- console.log(green(`\n✓ ${t.agentOk} ${bold(aName)}`));
268
- resetMessages();
269
- } else { console.log(chalk.red(`\n✖ Agente não encontrado.`)); }
270
- displayPrompt(); return;
271
- }
213
+ if (cmd === '/config') { rl.pause(); await configure(); config = getConfig(); provider = createProvider(config); rl.resume(); displayPrompt(); return; }
272
214
 
273
215
  // PROCESSAMENTO IA
274
216
  const controller = new AbortController();
275
217
  const abortHandler = () => controller.abort();
276
-
277
- // Remove temporariamente o listener de preview para não conflitar com a saída da IA
278
- process.stdin.removeAllListeners('keypress');
218
+ process.removeListener('SIGINT', () => {}); // Limpa handlers antigos
279
219
  process.on('SIGINT', abortHandler);
280
220
 
281
221
  let modeInstr = "";
@@ -288,34 +228,28 @@ export async function startInteractive() {
288
228
  if (word.startsWith('@')) {
289
229
  const filePath = word.slice(1);
290
230
  if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
291
- const content = fs.readFileSync(filePath, 'utf-8');
292
- 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` });
293
232
  } else { processedContent.push({ type: 'text', text: word }); }
294
233
  } else { processedContent.push({ type: 'text', text: word }); }
295
234
  }
296
235
 
297
236
  messages.push({ role: 'user', content: [...processedContent, { type: 'text', text: modeInstr }] });
298
- 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();
299
238
 
300
239
  try {
301
240
  let responseText = await provider.sendMessage(messages, { signal: controller.signal });
302
241
  spinner.stop();
303
- const cleanedText = cleanAIResponse(responseText);
304
- messages.push({ role: 'assistant', content: responseText });
305
242
  console.log(`\n${lavender('bimmo ')}${currentMode.toUpperCase()}`);
306
243
  console.log(lavender('─'.repeat(50)));
307
- process.stdout.write(marked.parse(cleanedText));
244
+ process.stdout.write(marked.parse(cleanAIResponse(responseText)));
308
245
  console.log(gray('\n' + '─'.repeat(50)));
246
+ messages.push({ role: 'assistant', content: responseText });
309
247
  } catch (err) {
310
248
  spinner.stop();
311
- if (controller.signal.aborted) { console.log(yellow(`\n⚠️ ${t.interrupted}`)); messages.pop(); }
249
+ if (controller.signal.aborted) { console.log(yellow(`\n⚠️ Interrompido.`)); messages.pop(); }
312
250
  else { console.error(chalk.red(`\n✖ Erro: ${err.message}`)); }
313
251
  } finally {
314
252
  process.removeListener('SIGINT', abortHandler);
315
- // Reativa o listener de preview
316
- process.stdin.on('keypress', (s, key) => {
317
- setImmediate(() => showPreview(rl.line));
318
- });
319
253
  displayPrompt();
320
254
  }
321
255
  });