bimmo-cli 2.1.4 → 2.2.1

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.1.4",
4
- "description": "🌿 Plataforma de IA universal com Modo Normal, Agentes e Swarms. Suporte a Diffs coloridos, Auto-Edit (Auto/Manual) e Contexto Inteligente.",
3
+ "version": "2.2.1",
4
+ "description": "🌿 Plataforma de IA universal com Modo Normal, Agentes e Swarms. Suporte a Autocomplete de arquivos, Diffs coloridos e Contexto Inteligente.",
5
5
  "bin": {
6
6
  "bimmo": "bin/bimmo"
7
7
  },
@@ -18,8 +18,7 @@
18
18
  "zai",
19
19
  "agent",
20
20
  "swarm",
21
- "diff",
22
- "multimodal",
21
+ "autocomplete",
23
22
  "terminal"
24
23
  ],
25
24
  "author": "Judah",
@@ -42,7 +41,9 @@
42
41
  "conf": "^13.0.0",
43
42
  "diff": "^7.0.0",
44
43
  "figlet": "^1.7.0",
44
+ "fuzzy": "^0.1.3",
45
45
  "inquirer": "^10.1.0",
46
+ "inquirer-autocomplete-prompt": "^3.0.1",
46
47
  "marked": "^14.0.0",
47
48
  "marked-terminal": "^7.0.0",
48
49
  "mime-types": "^2.1.35",
package/src/config.js CHANGED
@@ -30,6 +30,7 @@ export async function configure() {
30
30
  { name: 'Criar novo perfil de IA', value: 'create' },
31
31
  { name: 'Selecionar perfil ativo', value: 'select' },
32
32
  { name: 'Gerenciar Agentes Especialistas', value: 'agents' },
33
+ { name: 'Configurar Idioma', value: 'language' },
33
34
  { name: 'Configurar chave Tavily', value: 'tavily' },
34
35
  { name: 'Sair', value: 'exit' }
35
36
  ]
@@ -38,6 +39,22 @@ export async function configure() {
38
39
 
39
40
  if (action === 'exit') return;
40
41
 
42
+ if (action === 'language') {
43
+ const { lang } = await inquirer.prompt([{
44
+ type: 'list',
45
+ name: 'lang',
46
+ message: 'Escolha o idioma do sistema:',
47
+ choices: [
48
+ { name: 'Português (Brasil)', value: 'pt-BR' },
49
+ { name: 'English', value: 'en-US' }
50
+ ],
51
+ default: config.get('language') || 'pt-BR'
52
+ }]);
53
+ config.set('language', lang);
54
+ console.log(chalk.green(`✓ Idioma definido para: ${lang}`));
55
+ return;
56
+ }
57
+
41
58
  if (action === 'agents') return configureAgents();
42
59
 
43
60
  if (action === 'tavily') {
package/src/interface.js CHANGED
@@ -1,6 +1,8 @@
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';
4
6
  import { marked } from 'marked';
5
7
  import TerminalRenderer from 'marked-terminal';
6
8
  import ora from 'ora';
@@ -16,23 +18,19 @@ import { getProjectContext } from './project-context.js';
16
18
  import { SwarmOrchestrator } from './orchestrator.js';
17
19
  import { editState } from './agent.js';
18
20
 
21
+ // Registrar plugin de autocomplete
22
+ inquirer.registerPrompt('autocomplete', autocompletePrompt);
23
+
19
24
  const __filename = fileURLToPath(import.meta.url);
20
25
  const __dirname = path.dirname(__filename);
21
26
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
22
27
  const version = pkg.version;
23
28
 
24
- // Configuração do renderizador - Forçamos o escape de HTML para garantir que tags não apareçam
25
29
  marked.use(new TerminalRenderer({
26
30
  heading: chalk.hex('#c084fc').bold,
27
31
  code: chalk.hex('#00ff9d'),
28
32
  }));
29
33
 
30
- marked.setOptions({
31
- sanitize: true, // Depreciado mas ajuda em versões antigas
32
- headerIds: false,
33
- mangle: false
34
- });
35
-
36
34
  const green = chalk.hex('#00ff9d');
37
35
  const lavender = chalk.hex('#c084fc');
38
36
  const gray = chalk.gray;
@@ -44,6 +42,28 @@ let activePersona = null;
44
42
  let exitCounter = 0;
45
43
  let exitTimer = null;
46
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
+
47
67
  async function processInput(input) {
48
68
  const parts = input.split(' ');
49
69
  const processedContent = [];
@@ -103,28 +123,18 @@ function getModeStyle() {
103
123
  }
104
124
  }
105
125
 
106
- /**
107
- * LIMPEZA ABSOLUTA DE HTML
108
- * Remove tags HTML antes da renderização Markdown.
109
- */
110
126
  function cleanAIResponse(text) {
111
127
  if (!text) return "";
112
-
113
- let cleaned = text
128
+ return text
114
129
  .replace(/<br\s*\/?>/gi, '\n')
115
130
  .replace(/<\/p>/gi, '\n\n')
116
131
  .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();
132
+ .replace(/<[^>]*>?/gm, '')
133
+ .replace(/&nbsp;/g, ' ')
134
+ .replace(/&lt;/g, '<')
135
+ .replace(/&gt;/g, '>')
136
+ .replace(/&amp;/g, '&')
137
+ .trim();
128
138
  }
129
139
 
130
140
  export async function startInteractive() {
@@ -158,6 +168,7 @@ export async function startInteractive() {
158
168
  console.log(lavender(` v${version} `.padStart(60, '─')));
159
169
  console.log(green(` Perfil: ${bold(config.activeProfile || 'Padrão')} • IA: ${bold(config.provider.toUpperCase())}`));
160
170
  console.log(green(` Modelo: ${bold(config.model)}`));
171
+ console.log(gray(` 📁 ${process.cwd()}`));
161
172
  console.log(gray(' /chat | /plan | /edit | /swarm | /use [agente] | /help'));
162
173
  console.log(lavender('─'.repeat(60)) + '\n');
163
174
 
@@ -182,21 +193,49 @@ export async function startInteractive() {
182
193
  while (true) {
183
194
  const modeIndicator = getModeStyle();
184
195
  let input;
196
+
185
197
  try {
186
- const answers = await inquirer.prompt([{ type: 'input', name: 'input', message: modeIndicator + green('Você'), prefix: '→' }]);
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
+ ]);
187
227
  input = answers.input;
188
228
  } catch (e) { continue; }
189
229
 
230
+ // Diretório constante
190
231
  console.log(gray(` 📁 ${process.cwd()}`));
232
+
191
233
  const rawInput = input.trim();
192
234
  const cmd = rawInput.toLowerCase();
193
235
 
194
- // COMANDOS CLI (Tratados antes de enviar para IA)
195
236
  if (cmd === '/exit' || cmd === 'exit' || cmd === 'sair') { process.exit(0); }
196
-
197
237
  if (cmd === '/chat') { currentMode = 'chat'; console.log(lavender('✓ Modo CHAT.\n')); continue; }
198
238
  if (cmd === '/plan') { currentMode = 'plan'; console.log(yellow('✓ Modo PLAN.\n')); continue; }
199
-
200
239
  if (cmd === '/edit') { currentMode = 'edit'; console.log(chalk.red(`⚠️ Modo EDIT ativado.\n`)); continue; }
201
240
  if (cmd === '/edit auto') { currentMode = 'edit'; editState.autoAccept = true; console.log(chalk.red('⚠️ Modo EDIT (AUTO) ativado.\n')); continue; }
202
241
  if (cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false; console.log(chalk.red('⚠️ Modo EDIT (MANUAL) ativado.\n')); continue; }
@@ -207,11 +246,7 @@ export async function startInteractive() {
207
246
  const { overwrite } = await inquirer.prompt([{ type: 'confirm', name: 'overwrite', message: 'O arquivo .bimmorc.json já existe. Sobrescrever?', default: false }]);
208
247
  if (!overwrite) continue;
209
248
  }
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
- };
249
+ const initialConfig = { projectName: path.basename(process.cwd()), rules: ["Siga as convenções."], ignorePatterns: ["node_modules", ".git"] };
215
250
  fs.writeFileSync(bimmoRcPath, JSON.stringify(initialConfig, null, 2));
216
251
  console.log(green(`\n✅ .bimmorc.json criado com sucesso.\n`));
217
252
  resetMessages();
@@ -254,7 +289,6 @@ export async function startInteractive() {
254
289
 
255
290
  if (rawInput === '') continue;
256
291
 
257
- // Enviar para a IA
258
292
  const controller = new AbortController();
259
293
  const localInterruptHandler = () => controller.abort();
260
294
  process.removeListener('SIGINT', globalSigIntHandler);
@@ -272,22 +306,16 @@ export async function startInteractive() {
272
306
  try {
273
307
  let responseText = await provider.sendMessage(messages, { signal: controller.signal });
274
308
  spinner.stop();
275
-
276
309
  const cleanedText = cleanAIResponse(responseText);
277
310
  messages.push({ role: 'assistant', content: responseText });
278
-
279
311
  console.log('\n' + lavender('bimmo ') + getModeStyle());
280
312
  console.log(lavender('─'.repeat(50)));
281
313
  console.log(marked(cleanedText));
282
314
  console.log(gray('─'.repeat(50)) + '\n');
283
315
  } catch (err) {
284
316
  spinner.stop();
285
- if (controller.signal.aborted || err.name === 'AbortError') {
286
- console.log(yellow('\n⚠️ Interrompido.\n'));
287
- messages.pop();
288
- } else {
289
- console.error(chalk.red('\n✖ Erro:') + ' ' + err.message + '\n');
290
- }
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'); }
291
319
  } finally {
292
320
  process.removeListener('SIGINT', localInterruptHandler);
293
321
  process.on('SIGINT', globalSigIntHandler);