bimmo-cli 2.2.11 → 2.2.12

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 +2 -2
  2. package/src/interface.js +102 -59
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bimmo-cli",
3
- "version": "2.2.11",
4
- "description": "🌿 Plataforma de IA universal profissional com Agentes e Swarms. Suporte a Autocomplete real-time robusto, Diffs e Contexto Inteligente.",
3
+ "version": "2.2.12",
4
+ "description": "🌿 Plataforma de IA universal profissional com Agentes e Swarms. Suporte a Autocomplete real-time sólido, Diffs e Contexto Inteligente.",
5
5
  "bin": {
6
6
  "bimmo": "bin/bimmo"
7
7
  },
package/src/interface.js CHANGED
@@ -8,7 +8,6 @@ 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';
12
11
 
13
12
  import { getConfig, configure, updateActiveModel, switchProfile } from './config.js';
14
13
  import { createProvider } from './providers/factory.js';
@@ -21,12 +20,13 @@ const __dirname = path.dirname(__filename);
21
20
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
22
21
  const version = pkg.version;
23
22
 
23
+ // CONFIGURAÇÃO DO RENDERIZADOR - ELIMINA HTML E FORMATA TERMINAL
24
24
  marked.use(new TerminalRenderer({
25
25
  heading: chalk.hex('#c084fc').bold,
26
26
  code: chalk.hex('#00ff9d'),
27
27
  strong: chalk.bold,
28
28
  em: chalk.italic,
29
- html: () => '',
29
+ html: () => '', // DROP TOTAL DE HTML
30
30
  }));
31
31
 
32
32
  const green = chalk.hex('#00ff9d');
@@ -39,32 +39,58 @@ let currentMode = 'chat';
39
39
  let activePersona = null;
40
40
  let exitCounter = 0;
41
41
  let exitTimer = null;
42
+ let currentPreviewLines = 0;
42
43
 
44
+ const i18n = {
45
+ 'pt-BR': {
46
+ welcome: 'Olá! Sou o bimmo. Como posso ajudar?',
47
+ thinking: 'bimmo pensando...',
48
+ interrupted: 'Interrompido.',
49
+ exitHint: '(Pressione Ctrl+C novamente para sair)',
50
+ switchOk: 'Perfil ativo:',
51
+ agentOk: 'Agente ativo:',
52
+ help: '\nComandos:\n /chat | /plan | /edit | /init\n /switch [perfil] | /model [modelo]\n /use [agente] | /use normal\n /config | /clear | @arquivo\n'
53
+ },
54
+ 'en-US': {
55
+ welcome: 'Hello! I am bimmo. How can I help?',
56
+ thinking: 'bimmo thinking...',
57
+ interrupted: 'Interrupted.',
58
+ exitHint: '(Press Ctrl+C again to exit)',
59
+ switchOk: 'Profile active:',
60
+ agentOk: 'Agent active:',
61
+ help: '\nCommands:\n /chat | /plan | /edit | /init\n /switch [profile] | /model [model]\n /use [agent] | /use normal\n /config | /clear | @file\n'
62
+ }
63
+ };
64
+
65
+ /**
66
+ * Coleta arquivos e diretórios para o preview (Lógica Gemini-CLI)
67
+ */
43
68
  function getFilesForPreview(partialPath) {
44
69
  try {
45
70
  let p = partialPath.startsWith('@') ? partialPath.slice(1) : partialPath;
46
71
  let searchDir = '.';
47
72
  let filter = '';
48
73
 
49
- if (p.includes('/')) {
74
+ if (p.endsWith('/')) {
75
+ searchDir = p;
76
+ filter = '';
77
+ } else if (p.includes('/')) {
50
78
  const lastSlash = p.lastIndexOf('/');
51
- searchDir = p.substring(0, lastSlash) || '.';
79
+ searchDir = p.substring(0, lastSlash);
52
80
  filter = p.substring(lastSlash + 1);
53
81
  } else {
54
- searchDir = '.';
55
82
  filter = p;
56
83
  }
57
84
 
58
85
  const absoluteSearchDir = path.resolve(process.cwd(), searchDir);
59
86
  if (!fs.existsSync(absoluteSearchDir)) return [];
60
87
 
61
- const files = fs.readdirSync(absoluteSearchDir);
62
- return files
88
+ return fs.readdirSync(absoluteSearchDir)
63
89
  .filter(f => f.startsWith(filter) && !f.startsWith('.') && f !== 'node_modules')
64
90
  .map(f => {
65
- const rel = path.join(searchDir === '.' ? '' : searchDir, f);
66
- const isDir = fs.statSync(path.join(absoluteSearchDir, f)).isDirectory();
67
- return { name: rel, isDir };
91
+ const fullPath = path.join(absoluteSearchDir, f);
92
+ const isDir = fs.statSync(fullPath).isDirectory();
93
+ return { name: f, rel: path.join(searchDir, f), isDir };
68
94
  });
69
95
  } catch (e) { return []; }
70
96
  }
@@ -81,6 +107,9 @@ function cleanAIResponse(text) {
81
107
 
82
108
  export async function startInteractive() {
83
109
  let config = getConfig();
110
+ const lang = config.language || 'pt-BR';
111
+ const t = i18n[lang] || i18n['pt-BR'];
112
+
84
113
  if (!config.provider || !config.apiKey) {
85
114
  console.log(lavender(figlet.textSync('bimmo')));
86
115
  await configure(); return startInteractive();
@@ -102,9 +131,9 @@ export async function startInteractive() {
102
131
  console.clear();
103
132
  console.log(lavender(figlet.textSync('bimmo')));
104
133
  console.log(lavender(` v${version} `.padStart(60, '─')));
105
- console.log(green(` Perfil: ${bold(config.activeProfile || 'Padrão')} • IA: ${bold(config.provider.toUpperCase())}`));
106
- console.log(green(` Modelo: ${bold(config.model)}`));
134
+ console.log(green(` Perfil: ${bold(config.activeProfile || 'Padrão')} • Modelo: ${bold(config.model)}`));
107
135
  console.log(lavender('─'.repeat(60)) + '\n');
136
+ console.log(lavender(`👋 ${t.welcome}\n`));
108
137
 
109
138
  const rl = readline.createInterface({
110
139
  input: process.stdin,
@@ -114,24 +143,21 @@ export async function startInteractive() {
114
143
  const words = line.split(' ');
115
144
  const lastWord = words[words.length - 1];
116
145
  if (lastWord.startsWith('@')) {
117
- const hits = getFilesForPreview(lastWord).map(h => `@${h.name}`);
146
+ const hits = getFilesForPreview(lastWord).map(h => `@${h.rel}${h.isDir ? '/' : ''}`);
118
147
  return [hits, lastWord];
119
148
  }
120
149
  return [[], line];
121
150
  }
122
151
  });
123
152
 
124
- let currentPreviewLines = 0;
125
-
126
153
  const clearPreview = () => {
127
154
  if (currentPreviewLines > 0) {
128
- // Move o cursor para o início da primeira linha de preview
155
+ readline.cursorTo(process.stdout, 0);
129
156
  readline.moveCursor(process.stdout, 0, 1);
130
157
  for (let i = 0; i < currentPreviewLines; i++) {
131
158
  readline.clearLine(process.stdout, 0);
132
159
  readline.moveCursor(process.stdout, 0, 1);
133
160
  }
134
- // Volta o cursor para a posição original
135
161
  readline.moveCursor(process.stdout, 0, -(currentPreviewLines + 1));
136
162
  currentPreviewLines = 0;
137
163
  }
@@ -145,25 +171,18 @@ export async function startInteractive() {
145
171
  const files = getFilesForPreview(lastWord);
146
172
  if (files.length > 0) {
147
173
  clearPreview();
148
- const displayFiles = files.slice(0, 10);
149
-
150
- // Move cursor para o final da linha atual e pula linha
151
174
  process.stdout.write('\n');
152
- displayFiles.forEach(f => {
153
- process.stdout.write(gray(` ${f.isDir ? '📁' : '📄'} ${f.name}\n`));
175
+ const display = files.slice(0, 8);
176
+ display.forEach(f => {
177
+ process.stdout.write(gray(` ${f.isDir ? '📁' : '📄'} ${f.rel}${f.isDir ? '/' : ''}\n`));
154
178
  });
155
- currentPreviewLines = displayFiles.length;
156
-
157
- // Retorna o cursor para a posição exata de escrita
179
+ currentPreviewLines = display.length;
158
180
  readline.moveCursor(process.stdout, 0, -(currentPreviewLines + 1));
159
- const promptWidth = stripVTControlCharacters(rl.getPrompt()).length;
160
- readline.cursorTo(process.stdout, (promptWidth + rl.line.length) % (process.stdout.columns || 80));
161
- } else {
162
- clearPreview();
163
- }
164
- } else {
165
- clearPreview();
166
- }
181
+ // Reposiciona o cursor de digitação
182
+ const promptLen = (activePersona ? activePersona.length + 2 : 0) + (currentMode.length + 8) + 3;
183
+ readline.cursorTo(process.stdout, (promptLen + rl.line.length) % process.stdout.columns);
184
+ } else { clearPreview(); }
185
+ } else { clearPreview(); }
167
186
  };
168
187
 
169
188
  const displayPrompt = () => {
@@ -176,7 +195,23 @@ export async function startInteractive() {
176
195
  rl.prompt();
177
196
  };
178
197
 
179
- // Escuta teclas de forma segura
198
+ // MONITORAMENTO DE TECLAS (REAL-TIME @)
199
+ process.stdin.on('keypress', (s, key) => {
200
+ if (key && (key.name === 'return' || key.name === 'enter')) return;
201
+ setImmediate(() => showPreview());
202
+ });
203
+
204
+ rl.on('SIGINT', () => {
205
+ if (exitCounter === 0) {
206
+ exitCounter++;
207
+ process.stdout.write(`\n${gray(t.exitHint)}\n`);
208
+ exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
209
+ displayPrompt();
210
+ } else { process.exit(0); }
211
+ });
212
+
213
+ displayPrompt();
214
+
180
215
  rl.on('line', async (input) => {
181
216
  clearPreview();
182
217
  const rawInput = input.trim();
@@ -189,15 +224,15 @@ export async function startInteractive() {
189
224
  if (cmd === '/edit' || cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false; displayPrompt(); return; }
190
225
  if (cmd === '/edit auto') { currentMode = 'edit'; editState.autoAccept = true; displayPrompt(); return; }
191
226
  if (cmd === '/clear') { resetMessages(); console.clear(); displayPrompt(); return; }
192
-
227
+ if (cmd === '/help') { console.log(gray(t.help)); displayPrompt(); return; }
228
+
193
229
  if (cmd === '/init') {
194
- console.log(chalk.cyan('\n🚀 Gerando .bimmorc.json...\n'));
195
- const initPrompt = `Analise o projeto e crie o .bimmorc.json estruturado. Use write_file.`;
196
- const spinner = ora({ text: lavender(`bimmo pensando...`), color: 'red' }).start();
230
+ console.log(chalk.cyan('\n🚀 Mapeando projeto para gerar .bimmorc.json...\n'));
231
+ const initPrompt = `Analise a estrutura deste projeto e gere um .bimmorc.json com regras de código, stack e arquitetura. Use write_file.`;
232
+ const spinner = ora({ text: lavender(`${t.thinking}`), color: 'red' }).start();
197
233
  try {
198
234
  const res = await provider.sendMessage([...messages, { role: 'user', content: initPrompt }]);
199
- spinner.stop();
200
- console.log(marked.parse(cleanAIResponse(res)));
235
+ spinner.stop(); console.log(marked.parse(cleanAIResponse(res)));
201
236
  } catch (e) { spinner.stop(); console.error(chalk.red(e.message)); }
202
237
  resetMessages(); displayPrompt(); return;
203
238
  }
@@ -207,9 +242,34 @@ export async function startInteractive() {
207
242
  displayPrompt(); return;
208
243
  }
209
244
 
245
+ if (cmd.startsWith('/switch ')) {
246
+ const pName = rawInput.split(' ')[1];
247
+ if (switchProfile(pName)) {
248
+ config = getConfig(); provider = createProvider(config);
249
+ console.log(green(`\n✓ ${t.switchOk} ${pName}`));
250
+ } else { console.log(chalk.red(`\n✖ Perfil não encontrado.`)); }
251
+ displayPrompt(); return;
252
+ }
253
+
254
+ if (cmd.startsWith('/use ')) {
255
+ const aName = rawInput.split(' ')[1];
256
+ if (aName === 'normal') { activePersona = null; resetMessages(); displayPrompt(); return; }
257
+ const agents = config.agents || {};
258
+ if (agents[aName]) {
259
+ activePersona = aName;
260
+ const agent = agents[aName];
261
+ if (switchProfile(agent.profile)) { config = getConfig(); provider = createProvider(config); }
262
+ currentMode = agent.mode || 'chat';
263
+ console.log(green(`\n✓ ${t.agentOk} ${bold(aName)}`));
264
+ resetMessages();
265
+ } else { console.log(chalk.red(`\n✖ Agente não encontrado.`)); }
266
+ displayPrompt(); return;
267
+ }
268
+
210
269
  // PROCESSAMENTO IA
211
270
  const controller = new AbortController();
212
271
  const abortHandler = () => controller.abort();
272
+ process.removeListener('SIGINT', () => {});
213
273
  process.on('SIGINT', abortHandler);
214
274
 
215
275
  let modeInstr = "";
@@ -228,7 +288,7 @@ export async function startInteractive() {
228
288
  }
229
289
 
230
290
  messages.push({ role: 'user', content: [...processedContent, { type: 'text', text: modeInstr }] });
231
- const spinner = ora({ text: lavender(`bimmo pensando...`), color: currentMode === 'edit' ? 'red' : 'magenta' }).start();
291
+ const spinner = ora({ text: lavender(`${t.thinking} (Ctrl+C para parar)`), color: currentMode === 'edit' ? 'red' : 'magenta' }).start();
232
292
 
233
293
  try {
234
294
  let responseText = await provider.sendMessage(messages, { signal: controller.signal });
@@ -240,28 +300,11 @@ export async function startInteractive() {
240
300
  messages.push({ role: 'assistant', content: responseText });
241
301
  } catch (err) {
242
302
  spinner.stop();
243
- if (controller.signal.aborted) { console.log(yellow(`\n⚠️ Interrompido.`)); messages.pop(); }
303
+ if (controller.signal.aborted) { console.log(yellow(`\n⚠️ ${t.interrupted}`)); messages.pop(); }
244
304
  else { console.error(chalk.red(`\n✖ Erro: ${err.message}`)); }
245
305
  } finally {
246
306
  process.removeListener('SIGINT', abortHandler);
247
307
  displayPrompt();
248
308
  }
249
309
  });
250
-
251
- // Listener de tecla para o preview
252
- process.stdin.on('keypress', (s, key) => {
253
- if (key && (key.name === 'return' || key.name === 'enter')) return;
254
- setImmediate(() => showPreview());
255
- });
256
-
257
- rl.on('SIGINT', () => {
258
- if (exitCounter === 0) {
259
- exitCounter++;
260
- process.stdout.write(`\n${gray('(Pressione novamente para sair)')}\n`);
261
- exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
262
- displayPrompt();
263
- } else { process.exit(0); }
264
- });
265
-
266
- displayPrompt();
267
310
  }