bimmo-cli 2.2.8 → 2.2.10

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 +69 -130
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.10",
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
@@ -20,16 +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 - Ignora HTML e foca no Terminal
24
- const terminalRenderer = new TerminalRenderer({
23
+ marked.use(new TerminalRenderer({
25
24
  heading: chalk.hex('#c084fc').bold,
26
25
  code: chalk.hex('#00ff9d'),
27
26
  strong: chalk.bold,
28
27
  em: chalk.italic,
29
- html: () => '', // DROP TOTAL DE QUALQUER TAG HTML
30
- });
31
-
32
- marked.setOptions({ renderer: terminalRenderer });
28
+ html: () => '',
29
+ }));
33
30
 
34
31
  const green = chalk.hex('#00ff9d');
35
32
  const lavender = chalk.hex('#c084fc');
@@ -42,47 +39,33 @@ let activePersona = null;
42
39
  let exitCounter = 0;
43
40
  let exitTimer = null;
44
41
 
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
42
  /**
69
- * Coleta arquivos para preview e completion
43
+ * Coleta arquivos para preview e completion (Nível Gemini-CLI)
70
44
  */
71
45
  function getFilesForPreview(partialPath) {
72
46
  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);
76
-
77
- const searchDir = path.resolve(process.cwd(), dir);
78
- if (!fs.existsSync(searchDir)) return [];
47
+ let p = partialPath.startsWith('@') ? partialPath.slice(1) : partialPath;
48
+ let searchDir = '.';
49
+ let filter = '';
50
+
51
+ if (p.includes('/')) {
52
+ const lastSlash = p.lastIndexOf('/');
53
+ searchDir = p.substring(0, lastSlash) || '.';
54
+ filter = p.substring(lastSlash + 1);
55
+ } else {
56
+ searchDir = '.';
57
+ filter = p;
58
+ }
59
+
60
+ const absoluteSearchDir = path.resolve(process.cwd(), searchDir);
61
+ if (!fs.existsSync(absoluteSearchDir)) return [];
79
62
 
80
- const files = fs.readdirSync(searchDir);
63
+ const files = fs.readdirSync(absoluteSearchDir);
81
64
  return files
82
- .filter(f => (base === '' || f.startsWith(base)) && !f.startsWith('.') && f !== 'node_modules')
65
+ .filter(f => f.startsWith(filter) && !f.startsWith('.') && f !== 'node_modules')
83
66
  .map(f => {
84
- const rel = path.join(dir === '.' ? '' : dir, f);
85
- const isDir = fs.statSync(path.join(searchDir, f)).isDirectory();
67
+ const rel = path.join(searchDir === '.' ? '' : searchDir, f);
68
+ const isDir = fs.statSync(path.join(absoluteSearchDir, f)).isDirectory();
86
69
  return { name: rel, isDir };
87
70
  });
88
71
  } catch (e) { return []; }
@@ -100,13 +83,9 @@ function cleanAIResponse(text) {
100
83
 
101
84
  export async function startInteractive() {
102
85
  let config = getConfig();
103
- const lang = config.language || 'pt-BR';
104
- const t = i18n[lang] || i18n['pt-BR'];
105
-
106
86
  if (!config.provider || !config.apiKey) {
107
87
  console.log(lavender(figlet.textSync('bimmo')));
108
- await configure();
109
- return startInteractive();
88
+ await configure(); return startInteractive();
110
89
  }
111
90
 
112
91
  let provider = createProvider(config);
@@ -114,14 +93,12 @@ export async function startInteractive() {
114
93
  let messages = [];
115
94
 
116
95
  const resetMessages = () => {
117
- messages = [];
118
- messages.push({ role: 'system', content: getProjectContext() });
96
+ messages = [{ role: 'system', content: getProjectContext() }];
119
97
  if (activePersona) {
120
98
  const agent = (config.agents || {})[activePersona];
121
99
  if (agent) messages.push({ role: 'system', content: `Persona: ${agent.name}. Task: ${agent.role}` });
122
100
  }
123
101
  };
124
-
125
102
  resetMessages();
126
103
 
127
104
  console.clear();
@@ -131,13 +108,10 @@ export async function startInteractive() {
131
108
  console.log(green(` Modelo: ${bold(config.model)}`));
132
109
  console.log(lavender('─'.repeat(60)) + '\n');
133
110
 
134
- console.log(lavender(`👋 ${t.welcome}\n`));
135
-
136
111
  const rl = readline.createInterface({
137
112
  input: process.stdin,
138
113
  output: process.stdout,
139
114
  terminal: true,
140
- historySize: 100,
141
115
  completer: (line) => {
142
116
  const words = line.split(' ');
143
117
  const lastWord = words[words.length - 1];
@@ -152,52 +126,41 @@ export async function startInteractive() {
152
126
  let currentPreviewLines = 0;
153
127
 
154
128
  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
129
  if (currentPreviewLines > 0) {
130
+ // Move para baixo, limpa cada linha e volta
131
+ for (let i = 0; i < currentPreviewLines; i++) {
132
+ process.stdout.write('\n');
133
+ readline.clearLine(process.stdout, 0);
134
+ }
160
135
  readline.moveCursor(process.stdout, 0, -currentPreviewLines);
136
+ currentPreviewLines = 0;
161
137
  }
162
- currentPreviewLines = 0;
163
138
  };
164
139
 
165
- const showPreview = (line) => {
140
+ const showPreview = () => {
166
141
  clearPreview();
167
- const words = line.split(' ');
142
+ const words = rl.line.split(' ');
168
143
  const lastWord = words[words.length - 1];
169
144
 
170
145
  if (lastWord.startsWith('@')) {
171
146
  const files = getFilesForPreview(lastWord);
172
147
  if (files.length > 0) {
148
+ // Salva a posição do cursor
149
+ process.stdout.write('\u001b[s');
150
+
173
151
  process.stdout.write('\n');
174
- files.slice(0, 10).forEach(f => {
152
+ const displayFiles = files.slice(0, 10);
153
+ displayFiles.forEach(f => {
175
154
  process.stdout.write(gray(` ${f.isDir ? '📁' : '📄'} ${f.name}\n`));
176
155
  });
177
- currentPreviewLines = Math.min(files.length, 10) + 1;
178
- readline.moveCursor(process.stdout, 0, -currentPreviewLines);
179
- readline.cursorTo(process.stdout, rl.line.length + rl.getPrompt().length);
156
+ currentPreviewLines = displayFiles.length + 1;
157
+
158
+ // Restaura a posição do cursor
159
+ process.stdout.write('\u001b[u');
180
160
  }
181
161
  }
182
162
  };
183
163
 
184
- // Monitora digitação em tempo real
185
- process.stdin.on('keypress', (s, key) => {
186
- // Pequeno delay para o readline atualizar a linha interna
187
- setImmediate(() => showPreview(rl.line));
188
- });
189
-
190
- rl.on('SIGINT', () => {
191
- if (exitCounter === 0) {
192
- exitCounter++;
193
- console.log(`\n${gray(t.exitHint)}`);
194
- exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
195
- displayPrompt();
196
- } else {
197
- process.exit(0);
198
- }
199
- });
200
-
201
164
  const displayPrompt = () => {
202
165
  const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]` : '';
203
166
  let modeLabel = `[${currentMode.toUpperCase()}]`;
@@ -208,37 +171,45 @@ export async function startInteractive() {
208
171
  rl.prompt();
209
172
  };
210
173
 
174
+ process.stdin.on('keypress', (s, key) => {
175
+ if (key && (key.name === 'return' || key.name === 'enter')) return;
176
+ setImmediate(() => showPreview());
177
+ });
178
+
179
+ rl.on('SIGINT', () => {
180
+ if (exitCounter === 0) {
181
+ exitCounter++;
182
+ process.stdout.write(`\n${gray('(Pressione Ctrl+C novamente para sair)')}\n`);
183
+ exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
184
+ displayPrompt();
185
+ } else { process.exit(0); }
186
+ });
187
+
211
188
  displayPrompt();
212
189
 
213
190
  rl.on('line', async (input) => {
214
191
  clearPreview();
215
192
  const rawInput = input.trim();
216
- const cmd = rawInput.toLowerCase();
217
-
218
193
  if (rawInput === '') { displayPrompt(); return; }
219
194
 
195
+ const cmd = rawInput.toLowerCase();
220
196
  if (cmd === '/exit' || cmd === 'sair') process.exit(0);
221
197
  if (cmd === '/chat') { currentMode = 'chat'; displayPrompt(); return; }
222
198
  if (cmd === '/plan') { currentMode = 'plan'; displayPrompt(); return; }
223
199
  if (cmd === '/edit' || cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false; displayPrompt(); return; }
224
200
  if (cmd === '/edit auto') { currentMode = 'edit'; editState.autoAccept = true; displayPrompt(); return; }
225
201
  if (cmd === '/clear') { resetMessages(); console.clear(); displayPrompt(); return; }
226
- if (cmd === '/help') { console.log(gray(t.help)); displayPrompt(); return; }
227
-
202
+
228
203
  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();
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();
234
207
  try {
235
- const res = await provider.sendMessage([...messages, { role: 'user', content: initPrompt }], { signal: controller.signal });
208
+ const res = await provider.sendMessage([...messages, { role: 'user', content: initPrompt }]);
236
209
  spinner.stop();
237
210
  console.log(marked.parse(cleanAIResponse(res)));
238
211
  } catch (e) { spinner.stop(); console.error(chalk.red(e.message)); }
239
- resetMessages();
240
- displayPrompt();
241
- return;
212
+ resetMessages(); displayPrompt(); return;
242
213
  }
243
214
 
244
215
  if (cmd === '/config') {
@@ -246,36 +217,10 @@ export async function startInteractive() {
246
217
  displayPrompt(); return;
247
218
  }
248
219
 
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;
256
- }
257
-
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
- }
272
-
273
220
  // PROCESSAMENTO IA
274
221
  const controller = new AbortController();
275
222
  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');
223
+ process.removeListener('SIGINT', () => {});
279
224
  process.on('SIGINT', abortHandler);
280
225
 
281
226
  let modeInstr = "";
@@ -288,34 +233,28 @@ export async function startInteractive() {
288
233
  if (word.startsWith('@')) {
289
234
  const filePath = word.slice(1);
290
235
  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` });
236
+ processedContent.push({ type: 'text', text: `\n[ARQUIVO: ${filePath}]\n${fs.readFileSync(filePath, 'utf-8')}\n` });
293
237
  } else { processedContent.push({ type: 'text', text: word }); }
294
238
  } else { processedContent.push({ type: 'text', text: word }); }
295
239
  }
296
240
 
297
241
  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();
242
+ const spinner = ora({ text: lavender(`bimmo pensando...`), color: currentMode === 'edit' ? 'red' : 'magenta' }).start();
299
243
 
300
244
  try {
301
245
  let responseText = await provider.sendMessage(messages, { signal: controller.signal });
302
246
  spinner.stop();
303
- const cleanedText = cleanAIResponse(responseText);
304
- messages.push({ role: 'assistant', content: responseText });
305
247
  console.log(`\n${lavender('bimmo ')}${currentMode.toUpperCase()}`);
306
248
  console.log(lavender('─'.repeat(50)));
307
- process.stdout.write(marked.parse(cleanedText));
249
+ process.stdout.write(marked.parse(cleanAIResponse(responseText)));
308
250
  console.log(gray('\n' + '─'.repeat(50)));
251
+ messages.push({ role: 'assistant', content: responseText });
309
252
  } catch (err) {
310
253
  spinner.stop();
311
- if (controller.signal.aborted) { console.log(yellow(`\n⚠️ ${t.interrupted}`)); messages.pop(); }
254
+ if (controller.signal.aborted) { console.log(yellow(`\n⚠️ Interrompido.`)); messages.pop(); }
312
255
  else { console.error(chalk.red(`\n✖ Erro: ${err.message}`)); }
313
256
  } finally {
314
257
  process.removeListener('SIGINT', abortHandler);
315
- // Reativa o listener de preview
316
- process.stdin.on('keypress', (s, key) => {
317
- setImmediate(() => showPreview(rl.line));
318
- });
319
258
  displayPrompt();
320
259
  }
321
260
  });