bimmo-cli 2.0.3 → 2.1.0
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 +52 -15
- 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.0
|
|
4
|
-
"description": "🌿 Plataforma de IA universal com
|
|
3
|
+
"version": "2.1.0",
|
|
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,
|
|
@@ -99,7 +100,9 @@ function getModeStyle() {
|
|
|
99
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,7 +145,6 @@ export async function startInteractive() {
|
|
|
142
145
|
|
|
143
146
|
console.log(lavender('👋 Olá! Estou pronto. No que posso ajudar?\n'));
|
|
144
147
|
|
|
145
|
-
// Handler Global de SIGINT para o modo ocioso (Idle)
|
|
146
148
|
process.on('SIGINT', () => {
|
|
147
149
|
exitCounter++;
|
|
148
150
|
if (exitCounter === 1) {
|
|
@@ -173,7 +175,6 @@ export async function startInteractive() {
|
|
|
173
175
|
]);
|
|
174
176
|
input = answers.input;
|
|
175
177
|
} catch (e) {
|
|
176
|
-
// Inquirer joga erro no Ctrl+C se não for tratado
|
|
177
178
|
continue;
|
|
178
179
|
}
|
|
179
180
|
|
|
@@ -187,7 +188,24 @@ export async function startInteractive() {
|
|
|
187
188
|
|
|
188
189
|
if (cmd === '/chat') { currentMode = 'chat'; console.log(lavender('✓ Modo CHAT.\n')); continue; }
|
|
189
190
|
if (cmd === '/plan') { currentMode = 'plan'; console.log(yellow('✓ Modo PLAN.\n')); continue; }
|
|
190
|
-
|
|
191
|
+
|
|
192
|
+
if (cmd === '/edit') {
|
|
193
|
+
currentMode = 'edit';
|
|
194
|
+
console.log(chalk.red(`⚠️ Modo EDIT ativado (Sub-modo atual: ${editState.autoAccept ? 'AUTO' : 'MANUAL'}).\n`));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (cmd === '/edit auto') {
|
|
198
|
+
currentMode = 'edit';
|
|
199
|
+
editState.autoAccept = true;
|
|
200
|
+
console.log(chalk.red('⚠️ Modo EDIT (AUTO) ativado. Mudanças serão aplicadas sem perguntar.\n'));
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (cmd === '/edit manual') {
|
|
204
|
+
currentMode = 'edit';
|
|
205
|
+
editState.autoAccept = false;
|
|
206
|
+
console.log(chalk.red('⚠️ Modo EDIT (MANUAL) ativado. Pedirei permissão para cada mudança.\n'));
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
191
209
|
|
|
192
210
|
if (cmd.startsWith('/switch ')) {
|
|
193
211
|
const profileName = rawInput.split(' ')[1];
|
|
@@ -237,7 +255,9 @@ export async function startInteractive() {
|
|
|
237
255
|
if (cmd === '/help') {
|
|
238
256
|
console.log(gray(`
|
|
239
257
|
Comandos de Modo:
|
|
240
|
-
/chat
|
|
258
|
+
/chat → Modo conversa
|
|
259
|
+
/plan → Modo planejamento
|
|
260
|
+
/edit [auto/manual] → Modo edição (padrão manual)
|
|
241
261
|
/use [agente] → Usar um Agente Especialista
|
|
242
262
|
/use normal → Voltar para o chat normal
|
|
243
263
|
/swarm → Rodar fluxos complexos
|
|
@@ -254,6 +274,30 @@ Gerenciamento:
|
|
|
254
274
|
|
|
255
275
|
if (cmd === '/config') { await configure(); config = getConfig(); provider = createProvider(config); continue; }
|
|
256
276
|
|
|
277
|
+
if (cmd === '/init') {
|
|
278
|
+
const bimmoRcPath = path.join(process.cwd(), '.bimmorc.json');
|
|
279
|
+
if (fs.existsSync(bimmoRcPath)) {
|
|
280
|
+
const { overwrite } = await inquirer.prompt([{
|
|
281
|
+
type: 'confirm',
|
|
282
|
+
name: 'overwrite',
|
|
283
|
+
message: 'O arquivo .bimmorc.json já existe. Deseja sobrescrever?',
|
|
284
|
+
default: false
|
|
285
|
+
}]);
|
|
286
|
+
if (!overwrite) continue;
|
|
287
|
+
}
|
|
288
|
+
const initialConfig = {
|
|
289
|
+
projectName: path.basename(process.cwd()),
|
|
290
|
+
rules: ["Siga as convenções existentes.", "Prefira código modular."],
|
|
291
|
+
preferredTech: [],
|
|
292
|
+
ignorePatterns: ["node_modules", ".git"]
|
|
293
|
+
};
|
|
294
|
+
fs.writeFileSync(bimmoRcPath, JSON.stringify(initialConfig, null, 2));
|
|
295
|
+
console.log(green(`\n✅ .bimmorc.json criado com sucesso.\n`));
|
|
296
|
+
|
|
297
|
+
resetMessages();
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
257
301
|
if (cmd === '/swarm') {
|
|
258
302
|
const agents = config.agents || {};
|
|
259
303
|
const agentList = Object.keys(agents);
|
|
@@ -290,19 +334,13 @@ Gerenciamento:
|
|
|
290
334
|
if (rawInput === '') continue;
|
|
291
335
|
|
|
292
336
|
const controller = new AbortController();
|
|
293
|
-
|
|
294
|
-
// Handler local para SIGINT durante o processamento da IA
|
|
295
|
-
const localInterruptHandler = () => {
|
|
296
|
-
controller.abort();
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
// Remove temporariamente o handler global de saída
|
|
337
|
+
const localInterruptHandler = () => controller.abort();
|
|
300
338
|
process.removeAllListeners('SIGINT');
|
|
301
339
|
process.on('SIGINT', localInterruptHandler);
|
|
302
340
|
|
|
303
341
|
let modeInstr = "";
|
|
304
342
|
if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Apenas analise.";
|
|
305
|
-
else if (currentMode === 'edit') modeInstr =
|
|
343
|
+
else if (currentMode === 'edit') modeInstr = `\n[MODO EDIT] Você tem permissão para usar ferramentas. (Auto-Accept: ${editState.autoAccept ? 'ON' : 'OFF'})`;
|
|
306
344
|
|
|
307
345
|
const content = await processInput(rawInput);
|
|
308
346
|
messages.push({
|
|
@@ -327,13 +365,12 @@ Gerenciamento:
|
|
|
327
365
|
} catch (err) {
|
|
328
366
|
spinner.stop();
|
|
329
367
|
if (controller.signal.aborted || err.name === 'AbortError') {
|
|
330
|
-
console.log(yellow('\n\n⚠️
|
|
368
|
+
console.log(yellow('\n\n⚠️ Interrompido.\n'));
|
|
331
369
|
messages.pop();
|
|
332
370
|
} else {
|
|
333
371
|
console.error(chalk.red('\n✖ Erro:') + ' ' + err.message + '\n');
|
|
334
372
|
}
|
|
335
373
|
} finally {
|
|
336
|
-
// Restaura o handler global de saída
|
|
337
374
|
process.removeListener('SIGINT', localInterruptHandler);
|
|
338
375
|
process.on('SIGINT', () => {
|
|
339
376
|
exitCounter++;
|
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
|
|