bimmo-cli 2.0.3 → 2.1.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 +4 -3
- package/src/agent.js +84 -7
- package/src/interface.js +72 -33
- package/src/providers/grok.js +11 -12
- package/src/providers/openai.js +18 -9
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bimmo-cli",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "🌿 Plataforma de IA universal com
|
|
3
|
+
"version": "2.1.1",
|
|
4
|
+
"description": "🌿 Plataforma de IA universal com Modo Normal, Agentes e Swarms. Suporte a Diffs coloridos, Auto-Edit (Auto/Manual) e Contexto Inteligente.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"bimmo": "bin/bimmo"
|
|
7
7
|
},
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"zai",
|
|
19
19
|
"agent",
|
|
20
20
|
"swarm",
|
|
21
|
-
"
|
|
21
|
+
"diff",
|
|
22
22
|
"multimodal",
|
|
23
23
|
"terminal"
|
|
24
24
|
],
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"chalk": "^5.3.0",
|
|
41
41
|
"commander": "^12.1.0",
|
|
42
42
|
"conf": "^13.0.0",
|
|
43
|
+
"diff": "^7.0.0",
|
|
43
44
|
"figlet": "^1.7.0",
|
|
44
45
|
"inquirer": "^10.1.0",
|
|
45
46
|
"marked": "^14.0.0",
|
package/src/agent.js
CHANGED
|
@@ -3,10 +3,18 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
import { getConfig } from './config.js';
|
|
6
|
+
import * as diff from 'diff';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import inquirer from 'inquirer';
|
|
6
9
|
|
|
7
10
|
const config = getConfig();
|
|
8
11
|
const tvly = config.tavilyKey ? tavily({ apiKey: config.tavilyKey }) : null;
|
|
9
12
|
|
|
13
|
+
// Estado global para controle de edições (temporário na sessão)
|
|
14
|
+
export const editState = {
|
|
15
|
+
autoAccept: false
|
|
16
|
+
};
|
|
17
|
+
|
|
10
18
|
export const tools = [
|
|
11
19
|
{
|
|
12
20
|
name: 'search_internet',
|
|
@@ -55,17 +63,69 @@ export const tools = [
|
|
|
55
63
|
parameters: {
|
|
56
64
|
type: 'object',
|
|
57
65
|
properties: {
|
|
58
|
-
path: { type: 'string', description: '
|
|
66
|
+
path: { type: 'string', description: 'Caminho de destino' },
|
|
59
67
|
content: { type: 'string', description: 'Conteúdo do arquivo' }
|
|
60
68
|
},
|
|
61
69
|
required: ['path', 'content']
|
|
62
70
|
},
|
|
63
71
|
execute: async ({ path: filePath, content }) => {
|
|
64
72
|
try {
|
|
65
|
-
const
|
|
73
|
+
const absolutePath = path.resolve(filePath);
|
|
74
|
+
const oldContent = fs.existsSync(absolutePath) ? fs.readFileSync(absolutePath, 'utf-8') : "";
|
|
75
|
+
|
|
76
|
+
// Gerar Diff
|
|
77
|
+
const differences = diff.diffLines(oldContent, content);
|
|
78
|
+
|
|
79
|
+
console.log(`\n${chalk.cyan('📝 Alterações propostas em:')} ${chalk.bold(filePath)}`);
|
|
80
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
81
|
+
|
|
82
|
+
let hasChanges = false;
|
|
83
|
+
differences.forEach((part) => {
|
|
84
|
+
if (part.added || part.removed) hasChanges = true;
|
|
85
|
+
const color = part.added ? chalk.green : part.removed ? chalk.red : chalk.gray;
|
|
86
|
+
const prefix = part.added ? '+' : part.removed ? '-' : ' ';
|
|
87
|
+
|
|
88
|
+
// Mostra apenas linhas com mudanças ou um pouco de contexto
|
|
89
|
+
if (part.added || part.removed) {
|
|
90
|
+
process.stdout.write(color(`${prefix} ${part.value}`));
|
|
91
|
+
} else {
|
|
92
|
+
// Mostra apenas as primeiras e últimas linhas de blocos sem mudança para encurtar
|
|
93
|
+
const lines = part.value.split('\n');
|
|
94
|
+
if (lines.length > 4) {
|
|
95
|
+
process.stdout.write(color(` ${lines[0]}\n ...\n ${lines[lines.length-2]}\n`));
|
|
96
|
+
} else {
|
|
97
|
+
process.stdout.write(color(` ${part.value}`));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
console.log(chalk.gray('\n' + '─'.repeat(40)));
|
|
102
|
+
|
|
103
|
+
if (!hasChanges) {
|
|
104
|
+
return "Nenhuma mudança detectada no arquivo.";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Lógica de Aprovação
|
|
108
|
+
if (!editState.autoAccept) {
|
|
109
|
+
const { approve } = await inquirer.prompt([{
|
|
110
|
+
type: 'list',
|
|
111
|
+
name: 'approve',
|
|
112
|
+
message: 'Deseja aplicar estas alterações?',
|
|
113
|
+
choices: [
|
|
114
|
+
{ name: '✅ Sim', value: 'yes' },
|
|
115
|
+
{ name: '❌ Não', value: 'no' },
|
|
116
|
+
{ name: '⚡ Sim para tudo (Auto-Accept)', value: 'all' }
|
|
117
|
+
]
|
|
118
|
+
}]);
|
|
119
|
+
|
|
120
|
+
if (approve === 'no') return "Alteração rejeitada pelo usuário.";
|
|
121
|
+
if (approve === 'all') editState.autoAccept = true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const dir = path.dirname(absolutePath);
|
|
66
125
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
67
|
-
fs.writeFileSync(
|
|
68
|
-
|
|
126
|
+
fs.writeFileSync(absolutePath, content);
|
|
127
|
+
|
|
128
|
+
return `Arquivo ${filePath} atualizado com sucesso.`;
|
|
69
129
|
} catch (err) {
|
|
70
130
|
return `Erro ao escrever arquivo: ${err.message}`;
|
|
71
131
|
}
|
|
@@ -83,8 +143,26 @@ export const tools = [
|
|
|
83
143
|
},
|
|
84
144
|
execute: async ({ command }) => {
|
|
85
145
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
146
|
+
console.log(`\n${chalk.yellow('⚠️ Comando proposto:')} ${chalk.bold(command)}`);
|
|
147
|
+
|
|
148
|
+
if (!editState.autoAccept) {
|
|
149
|
+
const { approve } = await inquirer.prompt([{
|
|
150
|
+
type: 'list',
|
|
151
|
+
name: 'approve',
|
|
152
|
+
message: 'Executar este comando?',
|
|
153
|
+
choices: [
|
|
154
|
+
{ name: '✅ Sim', value: 'yes' },
|
|
155
|
+
{ name: '❌ Não', value: 'no' },
|
|
156
|
+
{ name: '⚡ Sim para tudo (Auto-Accept)', value: 'all' }
|
|
157
|
+
]
|
|
158
|
+
}]);
|
|
159
|
+
|
|
160
|
+
if (approve === 'no') return "Comando rejeitado pelo usuário.";
|
|
161
|
+
if (approve === 'all') editState.autoAccept = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const output = execSync(command, { encoding: 'utf-8', timeout: 60000 });
|
|
165
|
+
return output || 'Comando executado com sucesso (sem retorno).';
|
|
88
166
|
} catch (err) {
|
|
89
167
|
return `Erro ao executar comando: ${err.stderr || err.message}`;
|
|
90
168
|
}
|
|
@@ -97,7 +175,6 @@ export async function handleToolCalls(toolCalls) {
|
|
|
97
175
|
for (const call of toolCalls) {
|
|
98
176
|
const tool = tools.find(t => t.name === call.name);
|
|
99
177
|
if (tool) {
|
|
100
|
-
console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
|
|
101
178
|
const result = await tool.execute(call.args);
|
|
102
179
|
results.push({
|
|
103
180
|
callId: call.id,
|
package/src/interface.js
CHANGED
|
@@ -13,6 +13,7 @@ import { getConfig, configure, updateActiveModel, switchProfile } from './config
|
|
|
13
13
|
import { createProvider } from './providers/factory.js';
|
|
14
14
|
import { getProjectContext } from './project-context.js';
|
|
15
15
|
import { SwarmOrchestrator } from './orchestrator.js';
|
|
16
|
+
import { editState } from './agent.js';
|
|
16
17
|
|
|
17
18
|
marked.use(new TerminalRenderer({
|
|
18
19
|
heading: chalk.hex('#c084fc').bold,
|
|
@@ -96,10 +97,12 @@ async function processInput(input) {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
function getModeStyle() {
|
|
99
|
-
const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]
|
|
100
|
+
const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]` : '';
|
|
100
101
|
switch (currentMode) {
|
|
101
102
|
case 'plan': return yellow.bold(`${personaLabel}[PLAN] `);
|
|
102
|
-
case 'edit':
|
|
103
|
+
case 'edit':
|
|
104
|
+
const editSubMode = editState.autoAccept ? '(AUTO)' : '(MANUAL)';
|
|
105
|
+
return chalk.red.bold(`${personaLabel}[EDIT${editSubMode}] `);
|
|
103
106
|
default: return lavender.bold(`${personaLabel}[CHAT] `);
|
|
104
107
|
}
|
|
105
108
|
}
|
|
@@ -142,18 +145,19 @@ export async function startInteractive() {
|
|
|
142
145
|
|
|
143
146
|
console.log(lavender('👋 Olá! Estou pronto. No que posso ajudar?\n'));
|
|
144
147
|
|
|
145
|
-
|
|
146
|
-
process.on('SIGINT', () => {
|
|
148
|
+
const globalSigIntHandler = () => {
|
|
147
149
|
exitCounter++;
|
|
148
150
|
if (exitCounter === 1) {
|
|
149
|
-
|
|
151
|
+
process.stdout.write(gray('\n(Pressione Ctrl+C novamente para sair)\n'));
|
|
150
152
|
if (exitTimer) clearTimeout(exitTimer);
|
|
151
153
|
exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
|
|
152
154
|
} else {
|
|
153
|
-
|
|
155
|
+
process.stdout.write(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
|
|
154
156
|
process.exit(0);
|
|
155
157
|
}
|
|
156
|
-
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
process.on('SIGINT', globalSigIntHandler);
|
|
157
161
|
|
|
158
162
|
readline.emitKeypressEvents(process.stdin);
|
|
159
163
|
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
@@ -173,7 +177,6 @@ export async function startInteractive() {
|
|
|
173
177
|
]);
|
|
174
178
|
input = answers.input;
|
|
175
179
|
} catch (e) {
|
|
176
|
-
// Inquirer joga erro no Ctrl+C se não for tratado
|
|
177
180
|
continue;
|
|
178
181
|
}
|
|
179
182
|
|
|
@@ -187,7 +190,24 @@ export async function startInteractive() {
|
|
|
187
190
|
|
|
188
191
|
if (cmd === '/chat') { currentMode = 'chat'; console.log(lavender('✓ Modo CHAT.\n')); continue; }
|
|
189
192
|
if (cmd === '/plan') { currentMode = 'plan'; console.log(yellow('✓ Modo PLAN.\n')); continue; }
|
|
190
|
-
|
|
193
|
+
|
|
194
|
+
if (cmd === '/edit') {
|
|
195
|
+
currentMode = 'edit';
|
|
196
|
+
console.log(chalk.red(`⚠️ Modo EDIT ativado (Sub-modo atual: ${editState.autoAccept ? 'AUTO' : 'MANUAL'}).\n`));
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (cmd === '/edit auto') {
|
|
200
|
+
currentMode = 'edit';
|
|
201
|
+
editState.autoAccept = true;
|
|
202
|
+
console.log(chalk.red('⚠️ Modo EDIT (AUTO) ativado. Mudanças serão aplicadas sem perguntar.\n'));
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (cmd === '/edit manual') {
|
|
206
|
+
currentMode = 'edit';
|
|
207
|
+
editState.autoAccept = false;
|
|
208
|
+
console.log(chalk.red('⚠️ Modo EDIT (MANUAL) ativado. Pedirei permissão para cada mudança.\n'));
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
191
211
|
|
|
192
212
|
if (cmd.startsWith('/switch ')) {
|
|
193
213
|
const profileName = rawInput.split(' ')[1];
|
|
@@ -237,7 +257,9 @@ export async function startInteractive() {
|
|
|
237
257
|
if (cmd === '/help') {
|
|
238
258
|
console.log(gray(`
|
|
239
259
|
Comandos de Modo:
|
|
240
|
-
/chat
|
|
260
|
+
/chat → Modo conversa
|
|
261
|
+
/plan → Modo planejamento
|
|
262
|
+
/edit [auto/manual] → Modo edição (padrão manual)
|
|
241
263
|
/use [agente] → Usar um Agente Especialista
|
|
242
264
|
/use normal → Voltar para o chat normal
|
|
243
265
|
/swarm → Rodar fluxos complexos
|
|
@@ -254,6 +276,30 @@ Gerenciamento:
|
|
|
254
276
|
|
|
255
277
|
if (cmd === '/config') { await configure(); config = getConfig(); provider = createProvider(config); continue; }
|
|
256
278
|
|
|
279
|
+
if (cmd === '/init') {
|
|
280
|
+
const bimmoRcPath = path.join(process.cwd(), '.bimmorc.json');
|
|
281
|
+
if (fs.existsSync(bimmoRcPath)) {
|
|
282
|
+
const { overwrite } = await inquirer.prompt([{
|
|
283
|
+
type: 'confirm',
|
|
284
|
+
name: 'overwrite',
|
|
285
|
+
message: 'O arquivo .bimmorc.json já existe. Deseja sobrescrever?',
|
|
286
|
+
default: false
|
|
287
|
+
}]);
|
|
288
|
+
if (!overwrite) continue;
|
|
289
|
+
}
|
|
290
|
+
const initialConfig = {
|
|
291
|
+
projectName: path.basename(process.cwd()),
|
|
292
|
+
rules: ["Siga as convenções existentes.", "Prefira código modular."],
|
|
293
|
+
preferredTech: [],
|
|
294
|
+
ignorePatterns: ["node_modules", ".git"]
|
|
295
|
+
};
|
|
296
|
+
fs.writeFileSync(bimmoRcPath, JSON.stringify(initialConfig, null, 2));
|
|
297
|
+
console.log(green(`\n✅ .bimmorc.json criado com sucesso.\n`));
|
|
298
|
+
|
|
299
|
+
resetMessages();
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
257
303
|
if (cmd === '/swarm') {
|
|
258
304
|
const agents = config.agents || {};
|
|
259
305
|
const agentList = Object.keys(agents);
|
|
@@ -290,19 +336,15 @@ Gerenciamento:
|
|
|
290
336
|
if (rawInput === '') continue;
|
|
291
337
|
|
|
292
338
|
const controller = new AbortController();
|
|
339
|
+
const localInterruptHandler = () => controller.abort();
|
|
293
340
|
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
controller.abort();
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
// Remove temporariamente o handler global de saída
|
|
300
|
-
process.removeAllListeners('SIGINT');
|
|
341
|
+
// Switch de SIGINT para modo processamento
|
|
342
|
+
process.removeListener('SIGINT', globalSigIntHandler);
|
|
301
343
|
process.on('SIGINT', localInterruptHandler);
|
|
302
344
|
|
|
303
345
|
let modeInstr = "";
|
|
304
346
|
if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Apenas analise.";
|
|
305
|
-
else if (currentMode === 'edit') modeInstr =
|
|
347
|
+
else if (currentMode === 'edit') modeInstr = `\n[MODO EDIT] Você tem permissão para usar ferramentas. (Auto-Accept: ${editState.autoAccept ? 'ON' : 'OFF'})`;
|
|
306
348
|
|
|
307
349
|
const content = await processInput(rawInput);
|
|
308
350
|
messages.push({
|
|
@@ -318,11 +360,18 @@ Gerenciamento:
|
|
|
318
360
|
try {
|
|
319
361
|
let responseText = await provider.sendMessage(messages, { signal: controller.signal });
|
|
320
362
|
spinner.stop();
|
|
321
|
-
|
|
363
|
+
|
|
364
|
+
// LIMPEZA AGRESSIVA DE HTML
|
|
365
|
+
const cleanedText = responseText
|
|
366
|
+
.replace(/<br\s*\/?>/gi, '\n') // Converte <br> em newline real
|
|
367
|
+
.replace(/<p>/gi, '') // Remove tags <p> iniciais
|
|
368
|
+
.replace(/<\/p>/gi, '\n\n') // Converte </p> em double newline
|
|
369
|
+
.replace(/<\/?[^>]+(>|$)/g, ""); // Remove QUALQUER outra tag residual
|
|
370
|
+
|
|
322
371
|
messages.push({ role: 'assistant', content: responseText });
|
|
323
|
-
console.log('\n' + lavender('bimmo') + getModeStyle());
|
|
372
|
+
console.log('\n' + lavender('bimmo ') + getModeStyle());
|
|
324
373
|
console.log(lavender('─'.repeat(50)));
|
|
325
|
-
console.log(marked(cleanedText));
|
|
374
|
+
console.log(marked(cleanedText.trim()));
|
|
326
375
|
console.log(gray('─'.repeat(50)) + '\n');
|
|
327
376
|
} catch (err) {
|
|
328
377
|
spinner.stop();
|
|
@@ -333,19 +382,9 @@ Gerenciamento:
|
|
|
333
382
|
console.error(chalk.red('\n✖ Erro:') + ' ' + err.message + '\n');
|
|
334
383
|
}
|
|
335
384
|
} finally {
|
|
336
|
-
// Restaura o
|
|
385
|
+
// Restaura o modo global de saída
|
|
337
386
|
process.removeListener('SIGINT', localInterruptHandler);
|
|
338
|
-
process.on('SIGINT',
|
|
339
|
-
exitCounter++;
|
|
340
|
-
if (exitCounter === 1) {
|
|
341
|
-
console.log(gray('\n(Pressione Ctrl+C novamente para sair)'));
|
|
342
|
-
if (exitTimer) clearTimeout(exitTimer);
|
|
343
|
-
exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
|
|
344
|
-
} else {
|
|
345
|
-
console.log(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
|
|
346
|
-
process.exit(0);
|
|
347
|
-
}
|
|
348
|
-
});
|
|
387
|
+
process.on('SIGINT', globalSigIntHandler);
|
|
349
388
|
}
|
|
350
389
|
}
|
|
351
390
|
}
|
package/src/providers/grok.js
CHANGED
|
@@ -13,24 +13,23 @@ export class GrokProvider extends BaseProvider {
|
|
|
13
13
|
|
|
14
14
|
formatMessages(messages) {
|
|
15
15
|
return messages.map(msg => {
|
|
16
|
-
if (typeof msg.content === 'string') {
|
|
16
|
+
if (typeof msg.content === 'string' || msg.content === null) {
|
|
17
17
|
return msg;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return { type: 'text', text: part.text };
|
|
23
|
-
|
|
24
|
-
return {
|
|
20
|
+
if (Array.isArray(msg.content)) {
|
|
21
|
+
const content = msg.content.map(part => {
|
|
22
|
+
if (part.type === 'text') return { type: 'text', text: part.text };
|
|
23
|
+
if (part.type === 'image') return {
|
|
25
24
|
type: 'image_url',
|
|
26
|
-
image_url: {
|
|
27
|
-
url: `data:${part.mimeType};base64,${part.data}`
|
|
28
|
-
}
|
|
25
|
+
image_url: { url: `data:${part.mimeType};base64,${part.data}` }
|
|
29
26
|
};
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
return part;
|
|
28
|
+
});
|
|
29
|
+
return { ...msg, content };
|
|
30
|
+
}
|
|
32
31
|
|
|
33
|
-
return
|
|
32
|
+
return msg;
|
|
34
33
|
});
|
|
35
34
|
}
|
|
36
35
|
|
package/src/providers/openai.js
CHANGED
|
@@ -21,16 +21,25 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
21
21
|
|
|
22
22
|
formatMessages(messages) {
|
|
23
23
|
return messages.map(msg => {
|
|
24
|
-
|
|
24
|
+
// Se content for string ou null (comum em tool calls), retorna como está
|
|
25
|
+
if (typeof msg.content === 'string' || msg.content === null) {
|
|
26
|
+
return msg;
|
|
27
|
+
}
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
type: '
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
// Se for um array (multimodal), processa as partes
|
|
30
|
+
if (Array.isArray(msg.content)) {
|
|
31
|
+
const content = msg.content.map(part => {
|
|
32
|
+
if (part.type === 'text') return { type: 'text', text: part.text };
|
|
33
|
+
if (part.type === 'image') return {
|
|
34
|
+
type: 'image_url',
|
|
35
|
+
image_url: { url: `data:${part.mimeType};base64,${part.data}` }
|
|
36
|
+
};
|
|
37
|
+
return part; // Mantém outras partes (como tool_result se houver)
|
|
38
|
+
});
|
|
39
|
+
return { ...msg, content };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return msg;
|
|
34
43
|
});
|
|
35
44
|
}
|
|
36
45
|
|