bimmo-cli 2.2.10 → 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 +89 -40
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bimmo-cli",
3
- "version": "2.2.10",
4
- "description": "🌿 Plataforma de IA universal profissional com Agentes e Swarms. Suporte a Autocomplete real-time (estilo gemini-cli), 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
@@ -20,12 +20,13 @@ const __dirname = path.dirname(__filename);
20
20
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
21
21
  const version = pkg.version;
22
22
 
23
+ // CONFIGURAÇÃO DO RENDERIZADOR - ELIMINA HTML E FORMATA TERMINAL
23
24
  marked.use(new TerminalRenderer({
24
25
  heading: chalk.hex('#c084fc').bold,
25
26
  code: chalk.hex('#00ff9d'),
26
27
  strong: chalk.bold,
27
28
  em: chalk.italic,
28
- html: () => '',
29
+ html: () => '', // DROP TOTAL DE HTML
29
30
  }));
30
31
 
31
32
  const green = chalk.hex('#00ff9d');
@@ -38,9 +39,31 @@ let currentMode = 'chat';
38
39
  let activePersona = null;
39
40
  let exitCounter = 0;
40
41
  let exitTimer = null;
42
+ let currentPreviewLines = 0;
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
+ };
41
64
 
42
65
  /**
43
- * Coleta arquivos para preview e completion (Nível Gemini-CLI)
66
+ * Coleta arquivos e diretórios para o preview (Lógica Gemini-CLI)
44
67
  */
45
68
  function getFilesForPreview(partialPath) {
46
69
  try {
@@ -48,25 +71,26 @@ function getFilesForPreview(partialPath) {
48
71
  let searchDir = '.';
49
72
  let filter = '';
50
73
 
51
- if (p.includes('/')) {
74
+ if (p.endsWith('/')) {
75
+ searchDir = p;
76
+ filter = '';
77
+ } else if (p.includes('/')) {
52
78
  const lastSlash = p.lastIndexOf('/');
53
- searchDir = p.substring(0, lastSlash) || '.';
79
+ searchDir = p.substring(0, lastSlash);
54
80
  filter = p.substring(lastSlash + 1);
55
81
  } else {
56
- searchDir = '.';
57
82
  filter = p;
58
83
  }
59
84
 
60
85
  const absoluteSearchDir = path.resolve(process.cwd(), searchDir);
61
86
  if (!fs.existsSync(absoluteSearchDir)) return [];
62
87
 
63
- const files = fs.readdirSync(absoluteSearchDir);
64
- return files
88
+ return fs.readdirSync(absoluteSearchDir)
65
89
  .filter(f => f.startsWith(filter) && !f.startsWith('.') && f !== 'node_modules')
66
90
  .map(f => {
67
- const rel = path.join(searchDir === '.' ? '' : searchDir, f);
68
- const isDir = fs.statSync(path.join(absoluteSearchDir, f)).isDirectory();
69
- 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 };
70
94
  });
71
95
  } catch (e) { return []; }
72
96
  }
@@ -83,6 +107,9 @@ function cleanAIResponse(text) {
83
107
 
84
108
  export async function startInteractive() {
85
109
  let config = getConfig();
110
+ const lang = config.language || 'pt-BR';
111
+ const t = i18n[lang] || i18n['pt-BR'];
112
+
86
113
  if (!config.provider || !config.apiKey) {
87
114
  console.log(lavender(figlet.textSync('bimmo')));
88
115
  await configure(); return startInteractive();
@@ -104,9 +131,9 @@ export async function startInteractive() {
104
131
  console.clear();
105
132
  console.log(lavender(figlet.textSync('bimmo')));
106
133
  console.log(lavender(` v${version} `.padStart(60, '─')));
107
- console.log(green(` Perfil: ${bold(config.activeProfile || 'Padrão')} • IA: ${bold(config.provider.toUpperCase())}`));
108
- console.log(green(` Modelo: ${bold(config.model)}`));
134
+ console.log(green(` Perfil: ${bold(config.activeProfile || 'Padrão')} • Modelo: ${bold(config.model)}`));
109
135
  console.log(lavender('─'.repeat(60)) + '\n');
136
+ console.log(lavender(`👋 ${t.welcome}\n`));
110
137
 
111
138
  const rl = readline.createInterface({
112
139
  input: process.stdin,
@@ -116,49 +143,46 @@ export async function startInteractive() {
116
143
  const words = line.split(' ');
117
144
  const lastWord = words[words.length - 1];
118
145
  if (lastWord.startsWith('@')) {
119
- const hits = getFilesForPreview(lastWord).map(h => `@${h.name}`);
146
+ const hits = getFilesForPreview(lastWord).map(h => `@${h.rel}${h.isDir ? '/' : ''}`);
120
147
  return [hits, lastWord];
121
148
  }
122
149
  return [[], line];
123
150
  }
124
151
  });
125
152
 
126
- let currentPreviewLines = 0;
127
-
128
153
  const clearPreview = () => {
129
154
  if (currentPreviewLines > 0) {
130
- // Move para baixo, limpa cada linha e volta
155
+ readline.cursorTo(process.stdout, 0);
156
+ readline.moveCursor(process.stdout, 0, 1);
131
157
  for (let i = 0; i < currentPreviewLines; i++) {
132
- process.stdout.write('\n');
133
158
  readline.clearLine(process.stdout, 0);
159
+ readline.moveCursor(process.stdout, 0, 1);
134
160
  }
135
- readline.moveCursor(process.stdout, 0, -currentPreviewLines);
161
+ readline.moveCursor(process.stdout, 0, -(currentPreviewLines + 1));
136
162
  currentPreviewLines = 0;
137
163
  }
138
164
  };
139
165
 
140
166
  const showPreview = () => {
141
- clearPreview();
142
167
  const words = rl.line.split(' ');
143
168
  const lastWord = words[words.length - 1];
144
169
 
145
170
  if (lastWord.startsWith('@')) {
146
171
  const files = getFilesForPreview(lastWord);
147
172
  if (files.length > 0) {
148
- // Salva a posição do cursor
149
- process.stdout.write('\u001b[s');
150
-
173
+ clearPreview();
151
174
  process.stdout.write('\n');
152
- const displayFiles = files.slice(0, 10);
153
- displayFiles.forEach(f => {
154
- 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`));
155
178
  });
156
- currentPreviewLines = displayFiles.length + 1;
157
-
158
- // Restaura a posição do cursor
159
- process.stdout.write('\u001b[u');
160
- }
161
- }
179
+ currentPreviewLines = display.length;
180
+ readline.moveCursor(process.stdout, 0, -(currentPreviewLines + 1));
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(); }
162
186
  };
163
187
 
164
188
  const displayPrompt = () => {
@@ -171,6 +195,7 @@ export async function startInteractive() {
171
195
  rl.prompt();
172
196
  };
173
197
 
198
+ // MONITORAMENTO DE TECLAS (REAL-TIME @)
174
199
  process.stdin.on('keypress', (s, key) => {
175
200
  if (key && (key.name === 'return' || key.name === 'enter')) return;
176
201
  setImmediate(() => showPreview());
@@ -179,7 +204,7 @@ export async function startInteractive() {
179
204
  rl.on('SIGINT', () => {
180
205
  if (exitCounter === 0) {
181
206
  exitCounter++;
182
- process.stdout.write(`\n${gray('(Pressione Ctrl+C novamente para sair)')}\n`);
207
+ process.stdout.write(`\n${gray(t.exitHint)}\n`);
183
208
  exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
184
209
  displayPrompt();
185
210
  } else { process.exit(0); }
@@ -199,15 +224,15 @@ export async function startInteractive() {
199
224
  if (cmd === '/edit' || cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false; displayPrompt(); return; }
200
225
  if (cmd === '/edit auto') { currentMode = 'edit'; editState.autoAccept = true; displayPrompt(); return; }
201
226
  if (cmd === '/clear') { resetMessages(); console.clear(); displayPrompt(); return; }
202
-
227
+ if (cmd === '/help') { console.log(gray(t.help)); displayPrompt(); return; }
228
+
203
229
  if (cmd === '/init') {
204
- console.log(chalk.cyan('\n🚀 Gerando .bimmorc.json...\n'));
205
- const initPrompt = `Analise o projeto e crie o .bimmorc.json estruturado. Use write_file.`;
206
- 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();
207
233
  try {
208
234
  const res = await provider.sendMessage([...messages, { role: 'user', content: initPrompt }]);
209
- spinner.stop();
210
- console.log(marked.parse(cleanAIResponse(res)));
235
+ spinner.stop(); console.log(marked.parse(cleanAIResponse(res)));
211
236
  } catch (e) { spinner.stop(); console.error(chalk.red(e.message)); }
212
237
  resetMessages(); displayPrompt(); return;
213
238
  }
@@ -217,6 +242,30 @@ export async function startInteractive() {
217
242
  displayPrompt(); return;
218
243
  }
219
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
+
220
269
  // PROCESSAMENTO IA
221
270
  const controller = new AbortController();
222
271
  const abortHandler = () => controller.abort();
@@ -239,7 +288,7 @@ export async function startInteractive() {
239
288
  }
240
289
 
241
290
  messages.push({ role: 'user', content: [...processedContent, { type: 'text', text: modeInstr }] });
242
- 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();
243
292
 
244
293
  try {
245
294
  let responseText = await provider.sendMessage(messages, { signal: controller.signal });
@@ -251,7 +300,7 @@ export async function startInteractive() {
251
300
  messages.push({ role: 'assistant', content: responseText });
252
301
  } catch (err) {
253
302
  spinner.stop();
254
- if (controller.signal.aborted) { console.log(yellow(`\n⚠️ Interrompido.`)); messages.pop(); }
303
+ if (controller.signal.aborted) { console.log(yellow(`\n⚠️ ${t.interrupted}`)); messages.pop(); }
255
304
  else { console.error(chalk.red(`\n✖ Erro: ${err.message}`)); }
256
305
  } finally {
257
306
  process.removeListener('SIGINT', abortHandler);