bimmo-cli 2.2.3 → 2.2.8
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 +3 -17
- package/src/agent.js +16 -9
- package/src/interface.js +197 -199
- package/src/providers/anthropic.js +1 -2
- package/src/providers/gemini.js +1 -1
- package/src/providers/openai.js +2 -8
package/package.json
CHANGED
|
@@ -1,25 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bimmo-cli",
|
|
3
|
-
"version": "2.2.
|
|
4
|
-
"description": "🌿 Plataforma de IA universal com
|
|
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.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"bimmo": "bin/bimmo"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"keywords": [
|
|
10
|
-
"ai",
|
|
11
|
-
"cli",
|
|
12
|
-
"openai",
|
|
13
|
-
"anthropic",
|
|
14
|
-
"gemini",
|
|
15
|
-
"grok",
|
|
16
|
-
"deepseek",
|
|
17
|
-
"openrouter",
|
|
18
|
-
"zai",
|
|
19
|
-
"agent",
|
|
20
|
-
"swarm",
|
|
21
|
-
"autocomplete",
|
|
22
|
-
"terminal"
|
|
10
|
+
"ai", "cli", "openai", "anthropic", "gemini", "grok", "deepseek", "openrouter", "zai", "agent", "swarm", "terminal"
|
|
23
11
|
],
|
|
24
12
|
"author": "Judah",
|
|
25
13
|
"license": "MIT",
|
|
@@ -41,9 +29,7 @@
|
|
|
41
29
|
"conf": "^13.0.0",
|
|
42
30
|
"diff": "^7.0.0",
|
|
43
31
|
"figlet": "^1.7.0",
|
|
44
|
-
"fuzzy": "^0.1.3",
|
|
45
32
|
"inquirer": "^9.3.8",
|
|
46
|
-
"inquirer-autocomplete-prompt": "^3.0.1",
|
|
47
33
|
"marked": "^14.0.0",
|
|
48
34
|
"marked-terminal": "^7.0.0",
|
|
49
35
|
"mime-types": "^2.1.35",
|
package/src/agent.js
CHANGED
|
@@ -28,6 +28,7 @@ export const tools = [
|
|
|
28
28
|
},
|
|
29
29
|
execute: async ({ query }) => {
|
|
30
30
|
if (!tvly) return 'Erro: Chave de API da Tavily não configurada. Use /config para configurar.';
|
|
31
|
+
console.log(chalk.blue(`\n 🌐 Pesquisando na web: ${chalk.bold(query)}...`));
|
|
31
32
|
const searchResponse = await tvly.search(query, {
|
|
32
33
|
searchDepth: 'advanced',
|
|
33
34
|
maxResults: 5
|
|
@@ -51,6 +52,7 @@ export const tools = [
|
|
|
51
52
|
},
|
|
52
53
|
execute: async ({ path: filePath }) => {
|
|
53
54
|
try {
|
|
55
|
+
console.log(chalk.blue(`\n 📖 Lendo arquivo: ${chalk.bold(filePath)}...`));
|
|
54
56
|
return fs.readFileSync(filePath, 'utf-8');
|
|
55
57
|
} catch (err) {
|
|
56
58
|
return `Erro ao ler arquivo: ${err.message}`;
|
|
@@ -77,7 +79,7 @@ export const tools = [
|
|
|
77
79
|
const differences = diff.diffLines(oldContent, content);
|
|
78
80
|
|
|
79
81
|
console.log(`\n${chalk.cyan('📝 Alterações propostas em:')} ${chalk.bold(filePath)}`);
|
|
80
|
-
console.log(chalk.gray('─'.repeat(
|
|
82
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
81
83
|
|
|
82
84
|
let hasChanges = false;
|
|
83
85
|
differences.forEach((part) => {
|
|
@@ -85,20 +87,25 @@ export const tools = [
|
|
|
85
87
|
const color = part.added ? chalk.green : part.removed ? chalk.red : chalk.gray;
|
|
86
88
|
const prefix = part.added ? '+' : part.removed ? '-' : ' ';
|
|
87
89
|
|
|
88
|
-
// Mostra apenas linhas com mudanças ou um pouco de contexto
|
|
89
90
|
if (part.added || part.removed) {
|
|
90
|
-
|
|
91
|
+
// Garante que cada linha tenha o prefixo
|
|
92
|
+
const lines = part.value.split('\n');
|
|
93
|
+
lines.forEach(line => {
|
|
94
|
+
if (line || part.value.endsWith('\n')) {
|
|
95
|
+
process.stdout.write(color(`${prefix} ${line}\n`));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
91
98
|
} else {
|
|
92
99
|
// Mostra apenas as primeiras e últimas linhas de blocos sem mudança para encurtar
|
|
93
|
-
const lines = part.value.split('\n');
|
|
100
|
+
const lines = part.value.split('\n').filter(l => l.trim() !== "");
|
|
94
101
|
if (lines.length > 4) {
|
|
95
|
-
process.stdout.write(color(` ${lines[0]}\n ...\n ${lines[lines.length-
|
|
96
|
-
} else {
|
|
97
|
-
process.stdout.write(color(` ${
|
|
102
|
+
process.stdout.write(color(` ${lines[0]}\n ...\n ${lines[lines.length-1]}\n`));
|
|
103
|
+
} else if (lines.length > 0) {
|
|
104
|
+
lines.forEach(line => process.stdout.write(color(` ${line}\n`)));
|
|
98
105
|
}
|
|
99
106
|
}
|
|
100
107
|
});
|
|
101
|
-
console.log(chalk.gray('
|
|
108
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
102
109
|
|
|
103
110
|
if (!hasChanges) {
|
|
104
111
|
return "Nenhuma mudança detectada no arquivo.";
|
|
@@ -143,7 +150,7 @@ export const tools = [
|
|
|
143
150
|
},
|
|
144
151
|
execute: async ({ command }) => {
|
|
145
152
|
try {
|
|
146
|
-
console.log(
|
|
153
|
+
console.log(chalk.yellow(`\n ⚡ Comando proposto: ${chalk.bold(command)}`));
|
|
147
154
|
|
|
148
155
|
if (!editState.autoAccept) {
|
|
149
156
|
const { approve } = await inquirer.prompt([{
|
package/src/interface.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import figlet from 'figlet';
|
|
3
|
-
import inquirer from 'inquirer';
|
|
4
|
-
import autocompletePrompt from 'inquirer-autocomplete-prompt';
|
|
5
|
-
import fuzzy from 'fuzzy';
|
|
6
3
|
import { marked } from 'marked';
|
|
7
4
|
import TerminalRenderer from 'marked-terminal';
|
|
8
5
|
import ora from 'ora';
|
|
@@ -18,18 +15,21 @@ import { getProjectContext } from './project-context.js';
|
|
|
18
15
|
import { SwarmOrchestrator } from './orchestrator.js';
|
|
19
16
|
import { editState } from './agent.js';
|
|
20
17
|
|
|
21
|
-
// Registrar plugin de autocomplete
|
|
22
|
-
inquirer.registerPrompt('autocomplete', autocompletePrompt);
|
|
23
|
-
|
|
24
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
19
|
const __dirname = path.dirname(__filename);
|
|
26
20
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
|
|
27
21
|
const version = pkg.version;
|
|
28
22
|
|
|
29
|
-
|
|
23
|
+
// CONFIGURAÇÃO DO RENDERIZADOR - Ignora HTML e foca no Terminal
|
|
24
|
+
const terminalRenderer = new TerminalRenderer({
|
|
30
25
|
heading: chalk.hex('#c084fc').bold,
|
|
31
26
|
code: chalk.hex('#00ff9d'),
|
|
32
|
-
|
|
27
|
+
strong: chalk.bold,
|
|
28
|
+
em: chalk.italic,
|
|
29
|
+
html: () => '', // DROP TOTAL DE QUALQUER TAG HTML
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
marked.setOptions({ renderer: terminalRenderer });
|
|
33
33
|
|
|
34
34
|
const green = chalk.hex('#00ff9d');
|
|
35
35
|
const lavender = chalk.hex('#c084fc');
|
|
@@ -42,107 +42,69 @@ let activePersona = null;
|
|
|
42
42
|
let exitCounter = 0;
|
|
43
43
|
let exitTimer = null;
|
|
44
44
|
|
|
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
|
+
|
|
45
68
|
/**
|
|
46
|
-
*
|
|
69
|
+
* Coleta arquivos para preview e completion
|
|
47
70
|
*/
|
|
48
|
-
function
|
|
49
|
-
const results = [];
|
|
71
|
+
function getFilesForPreview(partialPath) {
|
|
50
72
|
try {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function processInput(input) {
|
|
68
|
-
const parts = input.split(' ');
|
|
69
|
-
const processedContent = [];
|
|
70
|
-
|
|
71
|
-
for (const part of parts) {
|
|
72
|
-
if (part.startsWith('@')) {
|
|
73
|
-
const filePath = part.slice(1);
|
|
74
|
-
try {
|
|
75
|
-
if (fs.existsSync(filePath)) {
|
|
76
|
-
const stats = fs.statSync(filePath);
|
|
77
|
-
if (stats.isFile()) {
|
|
78
|
-
const mimeType = mime.lookup(filePath) || 'application/octet-stream';
|
|
79
|
-
if (mimeType.startsWith('image/')) {
|
|
80
|
-
const base64Image = fs.readFileSync(filePath, { encoding: 'base64' });
|
|
81
|
-
processedContent.push({ type: 'image', mimeType, data: base64Image, fileName: path.basename(filePath) });
|
|
82
|
-
} else {
|
|
83
|
-
const textContent = fs.readFileSync(filePath, 'utf-8');
|
|
84
|
-
processedContent.push({ type: 'text', text: `\n--- Arquivo: ${path.basename(filePath)} ---\n${textContent}\n--- Fim do arquivo ---\n` });
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
} else {
|
|
88
|
-
processedContent.push({ type: 'text', text: part });
|
|
89
|
-
}
|
|
90
|
-
} catch (err) {
|
|
91
|
-
processedContent.push({ type: 'text', text: part });
|
|
92
|
-
}
|
|
93
|
-
} else {
|
|
94
|
-
processedContent.push({ type: 'text', text: part });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const hasImage = processedContent.some(c => c.type === 'image');
|
|
99
|
-
if (!hasImage) return processedContent.map(c => c.text).join(' ');
|
|
100
|
-
|
|
101
|
-
const finalContent = [];
|
|
102
|
-
let currentText = "";
|
|
103
|
-
for (const item of processedContent) {
|
|
104
|
-
if (item.type === 'text') {
|
|
105
|
-
currentText += (currentText ? " " : "") + item.text;
|
|
106
|
-
} else {
|
|
107
|
-
if (currentText) { finalContent.push({ type: 'text', text: currentText }); currentText = ""; }
|
|
108
|
-
finalContent.push(item);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (currentText) finalContent.push({ type: 'text', text: currentText });
|
|
112
|
-
return finalContent;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function getModeStyle() {
|
|
116
|
-
const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]` : '';
|
|
117
|
-
switch (currentMode) {
|
|
118
|
-
case 'plan': return yellow.bold(`${personaLabel}[PLAN] `);
|
|
119
|
-
case 'edit':
|
|
120
|
-
const editSubMode = editState.autoAccept ? '(AUTO)' : '(MANUAL)';
|
|
121
|
-
return chalk.red.bold(`${personaLabel}[EDIT${editSubMode}] `);
|
|
122
|
-
default: return lavender.bold(`${personaLabel}[CHAT] `);
|
|
123
|
-
}
|
|
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 [];
|
|
79
|
+
|
|
80
|
+
const files = fs.readdirSync(searchDir);
|
|
81
|
+
return files
|
|
82
|
+
.filter(f => (base === '' || f.startsWith(base)) && !f.startsWith('.') && f !== 'node_modules')
|
|
83
|
+
.map(f => {
|
|
84
|
+
const rel = path.join(dir === '.' ? '' : dir, f);
|
|
85
|
+
const isDir = fs.statSync(path.join(searchDir, f)).isDirectory();
|
|
86
|
+
return { name: rel, isDir };
|
|
87
|
+
});
|
|
88
|
+
} catch (e) { return []; }
|
|
124
89
|
}
|
|
125
90
|
|
|
126
91
|
function cleanAIResponse(text) {
|
|
127
92
|
if (!text) return "";
|
|
128
93
|
return text
|
|
129
94
|
.replace(/<br\s*\/?>/gi, '\n')
|
|
95
|
+
.replace(/<p>/gi, '')
|
|
130
96
|
.replace(/<\/p>/gi, '\n\n')
|
|
131
|
-
.replace(/<\/div>/gi, '\n')
|
|
132
97
|
.replace(/<[^>]*>?/gm, '')
|
|
133
|
-
.replace(/ /g, ' ')
|
|
134
|
-
.replace(/</g, '<')
|
|
135
|
-
.replace(/>/g, '>')
|
|
136
|
-
.replace(/&/g, '&')
|
|
137
98
|
.trim();
|
|
138
99
|
}
|
|
139
100
|
|
|
140
101
|
export async function startInteractive() {
|
|
141
102
|
let config = getConfig();
|
|
103
|
+
const lang = config.language || 'pt-BR';
|
|
104
|
+
const t = i18n[lang] || i18n['pt-BR'];
|
|
142
105
|
|
|
143
106
|
if (!config.provider || !config.apiKey) {
|
|
144
107
|
console.log(lavender(figlet.textSync('bimmo')));
|
|
145
|
-
console.log(gray('\nBem-vindo! Vamos configurar seus perfis de IA.\n'));
|
|
146
108
|
await configure();
|
|
147
109
|
return startInteractive();
|
|
148
110
|
}
|
|
@@ -153,11 +115,10 @@ export async function startInteractive() {
|
|
|
153
115
|
|
|
154
116
|
const resetMessages = () => {
|
|
155
117
|
messages = [];
|
|
156
|
-
|
|
157
|
-
messages.push({ role: 'system', content: projectContext });
|
|
118
|
+
messages.push({ role: 'system', content: getProjectContext() });
|
|
158
119
|
if (activePersona) {
|
|
159
120
|
const agent = (config.agents || {})[activePersona];
|
|
160
|
-
if (agent) messages.push({ role: 'system', content: `
|
|
121
|
+
if (agent) messages.push({ role: 'system', content: `Persona: ${agent.name}. Task: ${agent.role}` });
|
|
161
122
|
}
|
|
162
123
|
};
|
|
163
124
|
|
|
@@ -168,157 +129,194 @@ export async function startInteractive() {
|
|
|
168
129
|
console.log(lavender(` v${version} `.padStart(60, '─')));
|
|
169
130
|
console.log(green(` Perfil: ${bold(config.activeProfile || 'Padrão')} • IA: ${bold(config.provider.toUpperCase())}`));
|
|
170
131
|
console.log(green(` Modelo: ${bold(config.model)}`));
|
|
171
|
-
console.log(gray(` 📁 ${process.cwd()}`));
|
|
172
|
-
console.log(gray(' /chat | /plan | /edit | /swarm | /use [agente] | /help'));
|
|
173
132
|
console.log(lavender('─'.repeat(60)) + '\n');
|
|
174
133
|
|
|
175
|
-
console.log(lavender(
|
|
134
|
+
console.log(lavender(`👋 ${t.welcome}\n`));
|
|
135
|
+
|
|
136
|
+
const rl = readline.createInterface({
|
|
137
|
+
input: process.stdin,
|
|
138
|
+
output: process.stdout,
|
|
139
|
+
terminal: true,
|
|
140
|
+
historySize: 100,
|
|
141
|
+
completer: (line) => {
|
|
142
|
+
const words = line.split(' ');
|
|
143
|
+
const lastWord = words[words.length - 1];
|
|
144
|
+
if (lastWord.startsWith('@')) {
|
|
145
|
+
const hits = getFilesForPreview(lastWord).map(h => `@${h.name}`);
|
|
146
|
+
return [hits, lastWord];
|
|
147
|
+
}
|
|
148
|
+
return [[], line];
|
|
149
|
+
}
|
|
150
|
+
});
|
|
176
151
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
152
|
+
let currentPreviewLines = 0;
|
|
153
|
+
|
|
154
|
+
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
|
+
if (currentPreviewLines > 0) {
|
|
160
|
+
readline.moveCursor(process.stdout, 0, -currentPreviewLines);
|
|
161
|
+
}
|
|
162
|
+
currentPreviewLines = 0;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const showPreview = (line) => {
|
|
166
|
+
clearPreview();
|
|
167
|
+
const words = line.split(' ');
|
|
168
|
+
const lastWord = words[words.length - 1];
|
|
169
|
+
|
|
170
|
+
if (lastWord.startsWith('@')) {
|
|
171
|
+
const files = getFilesForPreview(lastWord);
|
|
172
|
+
if (files.length > 0) {
|
|
173
|
+
process.stdout.write('\n');
|
|
174
|
+
files.slice(0, 10).forEach(f => {
|
|
175
|
+
process.stdout.write(gray(` ${f.isDir ? '📁' : '📄'} ${f.name}\n`));
|
|
176
|
+
});
|
|
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);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
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)}`);
|
|
182
194
|
exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
|
|
195
|
+
displayPrompt();
|
|
183
196
|
} else {
|
|
184
|
-
process.stdout.write(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
|
|
185
197
|
process.exit(0);
|
|
186
198
|
}
|
|
187
|
-
};
|
|
199
|
+
});
|
|
188
200
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
while (true) {
|
|
194
|
-
const modeIndicator = getModeStyle();
|
|
195
|
-
let input;
|
|
201
|
+
const displayPrompt = () => {
|
|
202
|
+
const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]` : '';
|
|
203
|
+
let modeLabel = `[${currentMode.toUpperCase()}]`;
|
|
204
|
+
if (currentMode === 'edit') modeLabel = editState.autoAccept ? '[EDIT(AUTO)]' : '[EDIT(MANUAL)]';
|
|
196
205
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
message: modeIndicator + green('>'),
|
|
204
|
-
prefix: '',
|
|
205
|
-
source: async (answersSoFar, inputSearch) => {
|
|
206
|
-
const currentInput = inputSearch || '';
|
|
207
|
-
|
|
208
|
-
// Se o usuário digitar @, oferecemos arquivos
|
|
209
|
-
if (currentInput.includes('@')) {
|
|
210
|
-
const lastWord = currentInput.split(' ').pop();
|
|
211
|
-
if (lastWord.startsWith('@')) {
|
|
212
|
-
const searchPath = lastWord.slice(1);
|
|
213
|
-
const files = getFiles(process.cwd(), searchPath);
|
|
214
|
-
return files.map(f => ({
|
|
215
|
-
name: `@${f} ${fs.statSync(f).isDirectory() ? '(DIR)' : '(FILE)'}`,
|
|
216
|
-
value: currentInput.substring(0, currentInput.lastIndexOf('@')) + '@' + f
|
|
217
|
-
}));
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Caso contrário, apenas retorna o que ele está digitando como única opção
|
|
222
|
-
// para não atrapalhar o chat normal
|
|
223
|
-
return [currentInput];
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
]);
|
|
227
|
-
input = answers.input;
|
|
228
|
-
} catch (e) { continue; }
|
|
229
|
-
|
|
230
|
-
// Diretório constante
|
|
231
|
-
console.log(gray(` 📁 ${process.cwd()}`));
|
|
206
|
+
console.log(`${gray(`📁 ${process.cwd()}`)}`);
|
|
207
|
+
rl.setPrompt(lavender.bold(personaLabel) + (currentMode === 'edit' ? chalk.red.bold(modeLabel) : lavender.bold(modeLabel)) + green(' > '));
|
|
208
|
+
rl.prompt();
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
displayPrompt();
|
|
232
212
|
|
|
213
|
+
rl.on('line', async (input) => {
|
|
214
|
+
clearPreview();
|
|
233
215
|
const rawInput = input.trim();
|
|
234
216
|
const cmd = rawInput.toLowerCase();
|
|
235
217
|
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
if (cmd === '/
|
|
239
|
-
if (cmd === '/
|
|
240
|
-
if (cmd === '/
|
|
241
|
-
if (cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false;
|
|
218
|
+
if (rawInput === '') { displayPrompt(); return; }
|
|
219
|
+
|
|
220
|
+
if (cmd === '/exit' || cmd === 'sair') process.exit(0);
|
|
221
|
+
if (cmd === '/chat') { currentMode = 'chat'; displayPrompt(); return; }
|
|
222
|
+
if (cmd === '/plan') { currentMode = 'plan'; displayPrompt(); return; }
|
|
223
|
+
if (cmd === '/edit' || cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false; displayPrompt(); return; }
|
|
224
|
+
if (cmd === '/edit auto') { currentMode = 'edit'; editState.autoAccept = true; displayPrompt(); return; }
|
|
225
|
+
if (cmd === '/clear') { resetMessages(); console.clear(); displayPrompt(); return; }
|
|
226
|
+
if (cmd === '/help') { console.log(gray(t.help)); displayPrompt(); return; }
|
|
242
227
|
|
|
243
228
|
if (cmd === '/init') {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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();
|
|
234
|
+
try {
|
|
235
|
+
const res = await provider.sendMessage([...messages, { role: 'user', content: initPrompt }], { signal: controller.signal });
|
|
236
|
+
spinner.stop();
|
|
237
|
+
console.log(marked.parse(cleanAIResponse(res)));
|
|
238
|
+
} catch (e) { spinner.stop(); console.error(chalk.red(e.message)); }
|
|
252
239
|
resetMessages();
|
|
253
|
-
|
|
240
|
+
displayPrompt();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (cmd === '/config') {
|
|
245
|
+
rl.pause(); await configure(); config = getConfig(); provider = createProvider(config); rl.resume();
|
|
246
|
+
displayPrompt(); return;
|
|
254
247
|
}
|
|
255
248
|
|
|
256
249
|
if (cmd.startsWith('/switch ')) {
|
|
257
|
-
const
|
|
258
|
-
if (
|
|
250
|
+
const pName = rawInput.split(' ')[1];
|
|
251
|
+
if (switchProfile(pName)) {
|
|
259
252
|
config = getConfig(); provider = createProvider(config);
|
|
260
|
-
console.log(green(`\n✓
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
console.log(chalk.red(`\n✖ Perfil não encontrado.\n`)); continue;
|
|
253
|
+
console.log(green(`\n✓ ${t.switchOk}`));
|
|
254
|
+
} else { console.log(chalk.red(`\n✖ Perfil não encontrado.`)); }
|
|
255
|
+
displayPrompt(); return;
|
|
264
256
|
}
|
|
265
257
|
|
|
266
258
|
if (cmd.startsWith('/use ')) {
|
|
267
|
-
const
|
|
259
|
+
const aName = rawInput.split(' ')[1];
|
|
260
|
+
if (aName === 'normal') { activePersona = null; resetMessages(); displayPrompt(); return; }
|
|
268
261
|
const agents = config.agents || {};
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const agent = agents[agentName];
|
|
262
|
+
if (agents[aName]) {
|
|
263
|
+
activePersona = aName;
|
|
264
|
+
const agent = agents[aName];
|
|
273
265
|
if (switchProfile(agent.profile)) { config = getConfig(); provider = createProvider(config); }
|
|
274
266
|
currentMode = agent.mode || 'chat';
|
|
275
|
-
console.log(green(`\n✓
|
|
267
|
+
console.log(green(`\n✓ ${t.agentOk} ${bold(aName)}`));
|
|
276
268
|
resetMessages();
|
|
277
|
-
} else { console.log(chalk.red(`\n✖ Agente não encontrado
|
|
278
|
-
|
|
269
|
+
} else { console.log(chalk.red(`\n✖ Agente não encontrado.`)); }
|
|
270
|
+
displayPrompt(); return;
|
|
279
271
|
}
|
|
280
272
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (cmd === '/help') {
|
|
284
|
-
console.log(gray(`\nComandos:\n /chat | /plan | /edit [auto/manual] | /init\n /switch [nome] | /model [nome] | /use [agente]\n /config | /clear | @arquivo\n`));
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (cmd === '/config') { await configure(); config = getConfig(); provider = createProvider(config); continue; }
|
|
289
|
-
|
|
290
|
-
if (rawInput === '') continue;
|
|
291
|
-
|
|
273
|
+
// PROCESSAMENTO IA
|
|
292
274
|
const controller = new AbortController();
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
275
|
+
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');
|
|
279
|
+
process.on('SIGINT', abortHandler);
|
|
296
280
|
|
|
297
281
|
let modeInstr = "";
|
|
298
282
|
if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Apenas analise.";
|
|
299
283
|
else if (currentMode === 'edit') modeInstr = `\n[MODO EDIT] Auto-Accept: ${editState.autoAccept ? 'ON' : 'OFF'}`;
|
|
300
284
|
|
|
301
|
-
const
|
|
302
|
-
|
|
285
|
+
const processedContent = [];
|
|
286
|
+
const words = rawInput.split(' ');
|
|
287
|
+
for (const word of words) {
|
|
288
|
+
if (word.startsWith('@')) {
|
|
289
|
+
const filePath = word.slice(1);
|
|
290
|
+
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` });
|
|
293
|
+
} else { processedContent.push({ type: 'text', text: word }); }
|
|
294
|
+
} else { processedContent.push({ type: 'text', text: word }); }
|
|
295
|
+
}
|
|
303
296
|
|
|
304
|
-
|
|
297
|
+
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();
|
|
305
299
|
|
|
306
300
|
try {
|
|
307
301
|
let responseText = await provider.sendMessage(messages, { signal: controller.signal });
|
|
308
302
|
spinner.stop();
|
|
309
303
|
const cleanedText = cleanAIResponse(responseText);
|
|
310
304
|
messages.push({ role: 'assistant', content: responseText });
|
|
311
|
-
console.log(
|
|
305
|
+
console.log(`\n${lavender('bimmo ')}${currentMode.toUpperCase()}`);
|
|
312
306
|
console.log(lavender('─'.repeat(50)));
|
|
313
|
-
|
|
314
|
-
console.log(gray('─'.repeat(50))
|
|
307
|
+
process.stdout.write(marked.parse(cleanedText));
|
|
308
|
+
console.log(gray('\n' + '─'.repeat(50)));
|
|
315
309
|
} catch (err) {
|
|
316
310
|
spinner.stop();
|
|
317
|
-
if (controller.signal.aborted
|
|
318
|
-
else { console.error(chalk.red(
|
|
311
|
+
if (controller.signal.aborted) { console.log(yellow(`\n⚠️ ${t.interrupted}`)); messages.pop(); }
|
|
312
|
+
else { console.error(chalk.red(`\n✖ Erro: ${err.message}`)); }
|
|
319
313
|
} finally {
|
|
320
|
-
process.removeListener('SIGINT',
|
|
321
|
-
|
|
314
|
+
process.removeListener('SIGINT', abortHandler);
|
|
315
|
+
// Reativa o listener de preview
|
|
316
|
+
process.stdin.on('keypress', (s, key) => {
|
|
317
|
+
setImmediate(() => showPreview(rl.line));
|
|
318
|
+
});
|
|
319
|
+
displayPrompt();
|
|
322
320
|
}
|
|
323
|
-
}
|
|
321
|
+
});
|
|
324
322
|
}
|
|
@@ -18,6 +18,7 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
18
18
|
type: 'image',
|
|
19
19
|
source: { type: 'base64', media_type: part.mimeType, data: part.data }
|
|
20
20
|
};
|
|
21
|
+
return part;
|
|
21
22
|
});
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -51,8 +52,6 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
51
52
|
|
|
52
53
|
if (tool) {
|
|
53
54
|
if (options.signal?.aborted) throw new Error('Abortado pelo usuário');
|
|
54
|
-
|
|
55
|
-
console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
|
|
56
55
|
const result = await tool.execute(toolUse.input);
|
|
57
56
|
|
|
58
57
|
const nextMessages = [
|
package/src/providers/gemini.js
CHANGED
|
@@ -15,6 +15,7 @@ export class GeminiProvider extends BaseProvider {
|
|
|
15
15
|
if (part.type === 'image') return {
|
|
16
16
|
inlineData: { mimeType: part.mimeType, data: part.data }
|
|
17
17
|
};
|
|
18
|
+
return part;
|
|
18
19
|
});
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -58,7 +59,6 @@ export class GeminiProvider extends BaseProvider {
|
|
|
58
59
|
|
|
59
60
|
const tool = tools.find(t => t.name === call.functionCall.name);
|
|
60
61
|
if (tool) {
|
|
61
|
-
console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
|
|
62
62
|
const toolResult = await tool.execute(call.functionCall.args);
|
|
63
63
|
|
|
64
64
|
const resultResponse = await chat.sendMessage([{
|
package/src/providers/openai.js
CHANGED
|
@@ -21,12 +21,8 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
21
21
|
|
|
22
22
|
formatMessages(messages) {
|
|
23
23
|
return messages.map(msg => {
|
|
24
|
-
|
|
25
|
-
if (typeof msg.content === 'string' || msg.content === null) {
|
|
26
|
-
return msg;
|
|
27
|
-
}
|
|
24
|
+
if (typeof msg.content === 'string' || msg.content === null) return msg;
|
|
28
25
|
|
|
29
|
-
// Se for um array (multimodal), processa as partes
|
|
30
26
|
if (Array.isArray(msg.content)) {
|
|
31
27
|
const content = msg.content.map(part => {
|
|
32
28
|
if (part.type === 'text') return { type: 'text', text: part.text };
|
|
@@ -34,11 +30,10 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
34
30
|
type: 'image_url',
|
|
35
31
|
image_url: { url: `data:${part.mimeType};base64,${part.data}` }
|
|
36
32
|
};
|
|
37
|
-
return part;
|
|
33
|
+
return part;
|
|
38
34
|
});
|
|
39
35
|
return { ...msg, content };
|
|
40
36
|
}
|
|
41
|
-
|
|
42
37
|
return msg;
|
|
43
38
|
});
|
|
44
39
|
}
|
|
@@ -77,7 +72,6 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
77
72
|
|
|
78
73
|
const tool = tools.find(t => t.name === toolCall.function.name);
|
|
79
74
|
if (tool) {
|
|
80
|
-
console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
|
|
81
75
|
const args = JSON.parse(toolCall.function.arguments);
|
|
82
76
|
const result = await tool.execute(args);
|
|
83
77
|
|