bimmo-cli 4.0.6 → 4.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 +1 -1
- package/src/agent.js +28 -60
- package/src/interface.js +92 -142
- package/src/project-context.js +11 -0
- package/src/providers/anthropic.js +25 -21
- package/src/providers/base.js +1 -1
- package/src/providers/gemini.js +45 -13
- package/src/providers/grok.js +43 -3
- package/src/providers/openai.js +1 -1
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -5,12 +5,10 @@ import { execSync } from 'child_process';
|
|
|
5
5
|
import { getConfig } from './config.js';
|
|
6
6
|
import * as diff from 'diff';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
|
-
import inquirer from 'inquirer';
|
|
9
8
|
|
|
10
9
|
const config = getConfig();
|
|
11
10
|
const tvly = config.tavilyKey ? tavily({ apiKey: config.tavilyKey }) : null;
|
|
12
11
|
|
|
13
|
-
// Estado global para controle de edições (temporário na sessão)
|
|
14
12
|
export const editState = {
|
|
15
13
|
autoAccept: false
|
|
16
14
|
};
|
|
@@ -26,9 +24,9 @@ export const tools = [
|
|
|
26
24
|
},
|
|
27
25
|
required: ['query']
|
|
28
26
|
},
|
|
29
|
-
execute: async ({ query }) => {
|
|
30
|
-
if (!tvly) return 'Erro: Chave de API da Tavily não configurada.
|
|
31
|
-
|
|
27
|
+
execute: async ({ query }, { onStatus }) => {
|
|
28
|
+
if (!tvly) return 'Erro: Chave de API da Tavily não configurada.';
|
|
29
|
+
onStatus?.({ type: 'search', message: `Pesquisando: ${query}` });
|
|
32
30
|
const searchResponse = await tvly.search(query, {
|
|
33
31
|
searchDepth: 'advanced',
|
|
34
32
|
maxResults: 5
|
|
@@ -50,9 +48,9 @@ export const tools = [
|
|
|
50
48
|
},
|
|
51
49
|
required: ['path']
|
|
52
50
|
},
|
|
53
|
-
execute: async ({ path: filePath }) => {
|
|
51
|
+
execute: async ({ path: filePath }, { onStatus }) => {
|
|
54
52
|
try {
|
|
55
|
-
|
|
53
|
+
onStatus?.({ type: 'read', message: `Lendo: ${filePath}` });
|
|
56
54
|
return fs.readFileSync(filePath, 'utf-8');
|
|
57
55
|
} catch (err) {
|
|
58
56
|
return `Erro ao ler arquivo: ${err.message}`;
|
|
@@ -61,71 +59,51 @@ export const tools = [
|
|
|
61
59
|
},
|
|
62
60
|
{
|
|
63
61
|
name: 'write_file',
|
|
64
|
-
description: 'Cria ou sobrescreve um arquivo
|
|
62
|
+
description: 'Cria ou sobrescreve um arquivo. SEMPRE mostre as mudanças antes de aplicar.',
|
|
65
63
|
parameters: {
|
|
66
64
|
type: 'object',
|
|
67
65
|
properties: {
|
|
68
66
|
path: { type: 'string', description: 'Caminho de destino' },
|
|
69
|
-
content: { type: 'string', description: 'Conteúdo do arquivo' }
|
|
67
|
+
content: { type: 'string', description: 'Conteúdo completo do arquivo' }
|
|
70
68
|
},
|
|
71
69
|
required: ['path', 'content']
|
|
72
70
|
},
|
|
73
|
-
execute: async ({ path: filePath, content }) => {
|
|
71
|
+
execute: async ({ path: filePath, content }, { onStatus, onConfirm }) => {
|
|
74
72
|
try {
|
|
75
73
|
const absolutePath = path.resolve(filePath);
|
|
76
74
|
const oldContent = fs.existsSync(absolutePath) ? fs.readFileSync(absolutePath, 'utf-8') : "";
|
|
77
75
|
|
|
78
|
-
// Gerar Diff
|
|
79
76
|
const differences = diff.diffLines(oldContent, content);
|
|
80
|
-
|
|
81
|
-
console.log(`\n${chalk.cyan('📝 Alterações propostas em:')} ${chalk.bold(filePath)}`);
|
|
82
|
-
console.log(chalk.gray('─'.repeat(50)));
|
|
83
|
-
|
|
77
|
+
let diffString = '';
|
|
84
78
|
let hasChanges = false;
|
|
79
|
+
|
|
85
80
|
differences.forEach((part) => {
|
|
86
81
|
if (part.added || part.removed) hasChanges = true;
|
|
87
|
-
const color = part.added ? chalk.green : part.removed ? chalk.red : chalk.gray;
|
|
88
82
|
const prefix = part.added ? '+' : part.removed ? '-' : ' ';
|
|
83
|
+
const lines = part.value.split('\n');
|
|
89
84
|
|
|
90
85
|
if (part.added || part.removed) {
|
|
91
|
-
// Garante que cada linha tenha o prefixo
|
|
92
|
-
const lines = part.value.split('\n');
|
|
93
86
|
lines.forEach(line => {
|
|
94
87
|
if (line || part.value.endsWith('\n')) {
|
|
95
|
-
|
|
88
|
+
diffString += `${prefix} ${line}\n`;
|
|
96
89
|
}
|
|
97
90
|
});
|
|
98
91
|
} else {
|
|
99
|
-
// Mostra apenas as primeiras e últimas linhas de blocos sem mudança para encurtar
|
|
100
|
-
const lines = part.value.split('\n').filter(l => l.trim() !== "");
|
|
101
92
|
if (lines.length > 4) {
|
|
102
|
-
|
|
103
|
-
} else
|
|
104
|
-
lines.forEach(line =>
|
|
93
|
+
diffString += ` ${lines[0]}\n ...\n ${lines[lines.length-2]}\n`;
|
|
94
|
+
} else {
|
|
95
|
+
lines.forEach(line => { if (line) diffString += ` ${line}\n` });
|
|
105
96
|
}
|
|
106
97
|
}
|
|
107
98
|
});
|
|
108
|
-
console.log(chalk.gray('─'.repeat(50)));
|
|
109
99
|
|
|
110
|
-
if (!hasChanges)
|
|
111
|
-
return "Nenhuma mudança detectada no arquivo.";
|
|
112
|
-
}
|
|
100
|
+
if (!hasChanges) return "Nenhuma mudança detectada.";
|
|
113
101
|
|
|
114
|
-
|
|
115
|
-
if (!editState.autoAccept) {
|
|
116
|
-
const { approve } = await inquirer.prompt([{
|
|
117
|
-
type: 'list',
|
|
118
|
-
name: 'approve',
|
|
119
|
-
message: 'Deseja aplicar estas alterações?',
|
|
120
|
-
choices: [
|
|
121
|
-
{ name: '✅ Sim', value: 'yes' },
|
|
122
|
-
{ name: '❌ Não', value: 'no' },
|
|
123
|
-
{ name: '⚡ Sim para tudo (Auto-Accept)', value: 'all' }
|
|
124
|
-
]
|
|
125
|
-
}]);
|
|
102
|
+
onStatus?.({ type: 'diff', message: `Alterando: ${filePath}`, diff: diffString });
|
|
126
103
|
|
|
127
|
-
|
|
128
|
-
|
|
104
|
+
if (!editState.autoAccept) {
|
|
105
|
+
const approved = await onConfirm?.(`Deseja aplicar as mudanças em ${filePath}?`);
|
|
106
|
+
if (!approved) return "Alteração rejeitada pelo usuário.";
|
|
129
107
|
}
|
|
130
108
|
|
|
131
109
|
const dir = path.dirname(absolutePath);
|
|
@@ -144,34 +122,24 @@ export const tools = [
|
|
|
144
122
|
parameters: {
|
|
145
123
|
type: 'object',
|
|
146
124
|
properties: {
|
|
147
|
-
command: { type: 'string', description: 'Comando shell
|
|
125
|
+
command: { type: 'string', description: 'Comando shell' }
|
|
148
126
|
},
|
|
149
127
|
required: ['command']
|
|
150
128
|
},
|
|
151
|
-
execute: async ({ command }) => {
|
|
129
|
+
execute: async ({ command }, { onStatus, onConfirm }) => {
|
|
152
130
|
try {
|
|
153
|
-
|
|
131
|
+
onStatus?.({ type: 'command', message: `Executando: ${command}` });
|
|
154
132
|
|
|
155
133
|
if (!editState.autoAccept) {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
name: 'approve',
|
|
159
|
-
message: 'Executar este comando?',
|
|
160
|
-
choices: [
|
|
161
|
-
{ name: '✅ Sim', value: 'yes' },
|
|
162
|
-
{ name: '❌ Não', value: 'no' },
|
|
163
|
-
{ name: '⚡ Sim para tudo (Auto-Accept)', value: 'all' }
|
|
164
|
-
]
|
|
165
|
-
}]);
|
|
166
|
-
|
|
167
|
-
if (approve === 'no') return "Comando rejeitado pelo usuário.";
|
|
168
|
-
if (approve === 'all') editState.autoAccept = true;
|
|
134
|
+
const approved = await onConfirm?.(`Executar comando: ${command}?`);
|
|
135
|
+
if (!approved) return "Comando rejeitado.";
|
|
169
136
|
}
|
|
170
137
|
|
|
171
138
|
const output = execSync(command, { encoding: 'utf-8', timeout: 60000 });
|
|
172
|
-
|
|
139
|
+
onStatus?.({ type: 'command_output', message: 'Resultado do comando', output });
|
|
140
|
+
return output || 'Comando executado com sucesso.';
|
|
173
141
|
} catch (err) {
|
|
174
|
-
return `Erro
|
|
142
|
+
return `Erro: ${err.stderr || err.message}`;
|
|
175
143
|
}
|
|
176
144
|
}
|
|
177
145
|
}
|
package/src/interface.js
CHANGED
|
@@ -74,6 +74,17 @@ const Message = ({ role, content, displayContent }) => {
|
|
|
74
74
|
const isUser = role === 'user';
|
|
75
75
|
const color = isUser ? THEME.green : THEME.lavender;
|
|
76
76
|
const label = isUser ? '› VOCÊ' : '› bimmo';
|
|
77
|
+
|
|
78
|
+
const renderUserContent = (text) => {
|
|
79
|
+
if (typeof text !== 'string') return text;
|
|
80
|
+
const parts = text.split(/(@[\w\.\-\/]+)/g);
|
|
81
|
+
return parts.map((part, i) => {
|
|
82
|
+
if (part.startsWith('@')) {
|
|
83
|
+
return h(Text, { key: i, color: THEME.cyan, bold: true }, part);
|
|
84
|
+
}
|
|
85
|
+
return h(Text, { key: i }, part);
|
|
86
|
+
});
|
|
87
|
+
};
|
|
77
88
|
|
|
78
89
|
return h(Box, { flexDirection: 'column', marginBottom: 1 },
|
|
79
90
|
h(Box, null,
|
|
@@ -84,7 +95,7 @@ const Message = ({ role, content, displayContent }) => {
|
|
|
84
95
|
h(Text, null,
|
|
85
96
|
role === 'assistant'
|
|
86
97
|
? marked.parse(content).trim()
|
|
87
|
-
: (displayContent || content)
|
|
98
|
+
: renderUserContent(displayContent || content)
|
|
88
99
|
)
|
|
89
100
|
)
|
|
90
101
|
);
|
|
@@ -118,6 +129,38 @@ const FooterStatus = ({ mode, activePersona, exitCounter }) => (
|
|
|
118
129
|
)
|
|
119
130
|
);
|
|
120
131
|
|
|
132
|
+
const ToolDisplay = ({ status }) => {
|
|
133
|
+
if (!status) return null;
|
|
134
|
+
const { type, message, diff, output } = status;
|
|
135
|
+
|
|
136
|
+
return h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: THEME.gray, paddingX: 1, marginBottom: 1 },
|
|
137
|
+
h(Box, null,
|
|
138
|
+
h(Text, { color: THEME.yellow, bold: true }, `[${type.toUpperCase()}] `),
|
|
139
|
+
h(Text, null, message)
|
|
140
|
+
),
|
|
141
|
+
diff && h(Box, { marginTop: 1, flexDirection: 'column' },
|
|
142
|
+
diff.split('\n').map((line, i) => {
|
|
143
|
+
let color = THEME.gray;
|
|
144
|
+
if (line.startsWith('+')) color = THEME.green;
|
|
145
|
+
if (line.startsWith('-')) color = THEME.red;
|
|
146
|
+
return h(Text, { key: i, color }, line);
|
|
147
|
+
})
|
|
148
|
+
),
|
|
149
|
+
output && h(Box, { marginTop: 1, maxHeight: 10 },
|
|
150
|
+
h(Text, { color: THEME.gray }, output)
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const ConfirmationPrompt = ({ confirmation }) => {
|
|
156
|
+
if (!confirmation) return null;
|
|
157
|
+
return h(Box, { borderStyle: 'bold', borderColor: THEME.yellow, paddingX: 1, marginBottom: 1 },
|
|
158
|
+
h(Text, { bold: true }, `${confirmation.message} `),
|
|
159
|
+
h(Text, { color: THEME.green }, '(Y) Sim '),
|
|
160
|
+
h(Text, { color: THEME.red }, '/ (N) Não')
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
121
164
|
// --- APP PRINCIPAL ---
|
|
122
165
|
|
|
123
166
|
const BimmoApp = ({ initialConfig }) => {
|
|
@@ -131,140 +174,12 @@ const BimmoApp = ({ initialConfig }) => {
|
|
|
131
174
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
132
175
|
const [isThinking, setIsThinking] = useState(false);
|
|
133
176
|
const [thinkingMessage, setThinkingMessage] = useState('bimmo pensando...');
|
|
177
|
+
const [toolStatus, setToolStatus] = useState(null); // { type, message, diff, output }
|
|
178
|
+
const [confirmation, setConfirmation] = useState(null); // { message, resolve }
|
|
134
179
|
const [exitCounter, setExitCounter] = useState(0);
|
|
135
|
-
const [provider, setProvider] = useState(() => createProvider(initialConfig));
|
|
136
|
-
|
|
137
|
-
useEffect(() => {
|
|
138
|
-
const ctx = getProjectContext();
|
|
139
|
-
setMessages([
|
|
140
|
-
{ role: 'system', content: ctx },
|
|
141
|
-
{ role: 'assistant', content: `Olá! Sou o **bimmo v${version}**. Como posso ajudar hoje?\n\nDigite \`/help\` para ver os comandos.` }
|
|
142
|
-
]);
|
|
143
|
-
}, []);
|
|
144
|
-
|
|
145
|
-
const filePreview = useMemo(() => {
|
|
146
|
-
if (!input.includes('@')) return [];
|
|
147
|
-
const words = input.split(' ');
|
|
148
|
-
const lastWord = words[words.length - 1];
|
|
149
|
-
if (!lastWord.startsWith('@')) return [];
|
|
150
|
-
try {
|
|
151
|
-
const p = lastWord.slice(1);
|
|
152
|
-
const dir = p.includes('/') ? p.substring(0, p.lastIndexOf('/')) : '.';
|
|
153
|
-
const filter = p.includes('/') ? p.substring(p.lastIndexOf('/') + 1) : p;
|
|
154
|
-
const absDir = path.resolve(process.cwd(), dir);
|
|
155
|
-
if (!fs.existsSync(absDir) || !fs.statSync(absDir).isDirectory()) return [];
|
|
156
|
-
|
|
157
|
-
const files = fs.readdirSync(absDir)
|
|
158
|
-
.filter(f => f.startsWith(filter) && !f.startsWith('.') && f !== 'node_modules')
|
|
159
|
-
.map(f => {
|
|
160
|
-
const fullPath = path.join(absDir, f);
|
|
161
|
-
const isDir = fs.statSync(fullPath).isDirectory();
|
|
162
|
-
return {
|
|
163
|
-
name: f,
|
|
164
|
-
isDir,
|
|
165
|
-
rel: path.join(dir === '.' ? '' : dir, f)
|
|
166
|
-
};
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Ordena: pastas primeiro, depois arquivos
|
|
170
|
-
return files.sort((a, b) => {
|
|
171
|
-
if (a.isDir && !b.isDir) return -1;
|
|
172
|
-
if (!a.isDir && b.isDir) return 1;
|
|
173
|
-
return a.name.localeCompare(b.name);
|
|
174
|
-
}).slice(0, 10);
|
|
175
|
-
} catch (e) { return []; }
|
|
176
|
-
}, [input]);
|
|
177
|
-
|
|
178
|
-
useEffect(() => {
|
|
179
|
-
setSelectedIndex(0);
|
|
180
|
-
}, [filePreview.length]);
|
|
181
180
|
|
|
182
181
|
const handleSubmit = async (val) => {
|
|
183
|
-
|
|
184
|
-
if (!rawInput) return;
|
|
185
|
-
setInput('');
|
|
186
|
-
const parts = rawInput.split(' ');
|
|
187
|
-
const cmd = parts[0].toLowerCase();
|
|
188
|
-
|
|
189
|
-
// Comandos de Sistema
|
|
190
|
-
if (cmd === '/exit') exit();
|
|
191
|
-
if (cmd === '/clear') {
|
|
192
|
-
setStaticMessages([]);
|
|
193
|
-
setMessages([{ role: 'system', content: getProjectContext() }, { role: 'assistant', content: 'Chat limpo.' }]);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
if (['/chat', '/plan', '/edit'].includes(cmd)) {
|
|
197
|
-
setMode(cmd.slice(1));
|
|
198
|
-
if (cmd === '/edit') editState.autoAccept = parts[1] === 'auto';
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (cmd === '/model') {
|
|
203
|
-
const newModel = parts[1];
|
|
204
|
-
if (newModel) {
|
|
205
|
-
updateActiveModel(newModel);
|
|
206
|
-
const newCfg = getConfig();
|
|
207
|
-
setConfig(newCfg);
|
|
208
|
-
setProvider(createProvider(newCfg));
|
|
209
|
-
setMessages(prev => [...prev, { role: 'system', content: `Modelo alterado para: ${newModel}` }]);
|
|
210
|
-
}
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (cmd === '/switch') {
|
|
215
|
-
const profile = parts[1];
|
|
216
|
-
if (switchProfile(profile)) {
|
|
217
|
-
const newCfg = getConfig();
|
|
218
|
-
setConfig(newCfg);
|
|
219
|
-
setProvider(createProvider(newCfg));
|
|
220
|
-
setMessages(prev => [...prev, { role: 'system', content: `Perfil alterado para: ${profile}` }]);
|
|
221
|
-
}
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (cmd === '/use') {
|
|
226
|
-
const agentName = parts[1];
|
|
227
|
-
const agents = config.agents || {};
|
|
228
|
-
if (agentName === 'normal') {
|
|
229
|
-
setActivePersona(null);
|
|
230
|
-
} else if (agents[agentName]) {
|
|
231
|
-
setActivePersona(agentName);
|
|
232
|
-
setMode(agents[agentName].mode || 'chat');
|
|
233
|
-
}
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (cmd === '/help') {
|
|
238
|
-
setMessages(prev => [...prev, { role: 'assistant', content: `**Comandos Disponíveis:**\n\n- \`/chat\`, \`/plan\`, \`/edit\`: Alternar modos\n- \`/model <nome>\`: Trocar modelo atual\n- \`/switch <perfil>\`: Trocar perfil de API\n- \`/use <agente>\`: Usar agente especializado\n- \`/clear\`: Limpar histórico\n- \`/exit\`: Sair do bimmo\n- \`@arquivo\`: Referenciar arquivo local` }]);
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Processamento de arquivos
|
|
243
|
-
setIsThinking(true);
|
|
244
|
-
let processedInput = rawInput;
|
|
245
|
-
const fileMatches = rawInput.match(/@[\w\.\-\/]+/g);
|
|
246
|
-
if (fileMatches) {
|
|
247
|
-
for (const match of fileMatches) {
|
|
248
|
-
const filePath = match.slice(1);
|
|
249
|
-
try {
|
|
250
|
-
if (fs.existsSync(filePath)) {
|
|
251
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
252
|
-
processedInput = processedInput.replace(match, `\n\n[Arquivo: ${filePath}]\n\`\`\`\n${content}\n\`\`\`\n`);
|
|
253
|
-
}
|
|
254
|
-
} catch (e) {}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const userMsg = { role: 'user', content: processedInput, displayContent: rawInput };
|
|
259
|
-
|
|
260
|
-
// Move mensagens antigas para static para performance
|
|
261
|
-
if (messages.length > 5) {
|
|
262
|
-
setStaticMessages(prev => [...prev, ...messages.slice(0, -5)]);
|
|
263
|
-
setMessages(prev => [...prev.slice(-5), userMsg]);
|
|
264
|
-
} else {
|
|
265
|
-
setMessages(prev => [...prev, userMsg]);
|
|
266
|
-
}
|
|
267
|
-
|
|
182
|
+
// ... (unchanged code)
|
|
268
183
|
try {
|
|
269
184
|
let finalMessages = [...staticMessages, ...messages, userMsg];
|
|
270
185
|
if (activePersona && config.agents[activePersona]) {
|
|
@@ -272,19 +187,49 @@ const BimmoApp = ({ initialConfig }) => {
|
|
|
272
187
|
finalMessages = [{ role: 'system', content: `Sua tarefa: ${agent.role}\n\n${getProjectContext()}` }, ...finalMessages.filter(m => m.role !== 'system')];
|
|
273
188
|
}
|
|
274
189
|
|
|
275
|
-
const
|
|
190
|
+
const options = {
|
|
191
|
+
onStatus: (status) => {
|
|
192
|
+
setToolStatus(status);
|
|
193
|
+
if (status.message) setThinkingMessage(status.message);
|
|
194
|
+
},
|
|
195
|
+
onConfirm: (message) => {
|
|
196
|
+
return new Promise((resolve) => {
|
|
197
|
+
setConfirmation({ message, resolve });
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const response = await provider.sendMessage(finalMessages, options);
|
|
276
203
|
setMessages(prev => [...prev, { role: 'assistant', content: response }]);
|
|
277
204
|
} catch (err) {
|
|
278
205
|
setMessages(prev => [...prev, { role: 'system', content: `Erro: ${err.message}` }]);
|
|
279
206
|
} finally {
|
|
280
207
|
setIsThinking(false);
|
|
208
|
+
setToolStatus(null);
|
|
209
|
+
setConfirmation(null);
|
|
210
|
+
setThinkingMessage('bimmo pensando...');
|
|
281
211
|
}
|
|
282
212
|
};
|
|
283
213
|
|
|
284
|
-
useInput((
|
|
285
|
-
if (
|
|
286
|
-
if (
|
|
287
|
-
|
|
214
|
+
useInput((char, key) => {
|
|
215
|
+
if (confirmation) {
|
|
216
|
+
if (char.toLowerCase() === 'y' || key.return) {
|
|
217
|
+
const resolve = confirmation.resolve;
|
|
218
|
+
setConfirmation(null);
|
|
219
|
+
resolve(true);
|
|
220
|
+
} else if (char.toLowerCase() === 'n' || key.escape) {
|
|
221
|
+
const resolve = confirmation.resolve;
|
|
222
|
+
setConfirmation(null);
|
|
223
|
+
resolve(false);
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (key.ctrl && char === 'c') {
|
|
229
|
+
if (isThinking) {
|
|
230
|
+
setIsThinking(false);
|
|
231
|
+
setMessages(prev => [...prev, { role: 'system', content: 'Interrompido pelo usuário.' }]);
|
|
232
|
+
} else {
|
|
288
233
|
if (exitCounter === 0) {
|
|
289
234
|
setExitCounter(1);
|
|
290
235
|
setTimeout(() => setExitCounter(0), 2000);
|
|
@@ -292,6 +237,7 @@ const BimmoApp = ({ initialConfig }) => {
|
|
|
292
237
|
exit();
|
|
293
238
|
}
|
|
294
239
|
}
|
|
240
|
+
return;
|
|
295
241
|
}
|
|
296
242
|
if (key.tab && filePreview.length > 0) {
|
|
297
243
|
const selected = filePreview[selectedIndex] || filePreview[0];
|
|
@@ -319,11 +265,15 @@ const BimmoApp = ({ initialConfig }) => {
|
|
|
319
265
|
messages.filter(m => m.role !== 'system').map((m, i) => h(Message, { key: i, ...m }))
|
|
320
266
|
),
|
|
321
267
|
|
|
322
|
-
isThinking && h(Box, { marginBottom: 1 },
|
|
323
|
-
h(
|
|
324
|
-
h(
|
|
325
|
-
|
|
326
|
-
|
|
268
|
+
isThinking && h(Box, { marginBottom: 1, flexDirection: 'column' },
|
|
269
|
+
h(Box, null,
|
|
270
|
+
h(Text, { color: THEME.lavender },
|
|
271
|
+
h(Spinner, { type: 'dots' }),
|
|
272
|
+
h(Text, { italic: true }, ` ${thinkingMessage}`)
|
|
273
|
+
)
|
|
274
|
+
),
|
|
275
|
+
h(ToolDisplay, { status: toolStatus }),
|
|
276
|
+
h(ConfirmationPrompt, { confirmation })
|
|
327
277
|
),
|
|
328
278
|
|
|
329
279
|
filePreview.length > 0 && h(AutocompleteSuggestions, { suggestions: filePreview, selectedIndex }),
|
package/src/project-context.js
CHANGED
|
@@ -36,5 +36,16 @@ export function getProjectContext() {
|
|
|
36
36
|
context += "Estrutura de arquivos indisponível.\n";
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
context += `\n=== INSTRUÇÕES DO SISTEMA ===
|
|
40
|
+
Você é o bimmo-cli, um assistente de desenvolvimento avançado.
|
|
41
|
+
Você possui ferramentas para interagir com o sistema e a internet:
|
|
42
|
+
1. read_file: Sempre use esta ferramenta para ler o conteúdo de um arquivo antes de editá-lo ou para entender o contexto do código.
|
|
43
|
+
2. write_file: Use para criar ou modificar arquivos. Mostre apenas as mudanças necessárias.
|
|
44
|
+
3. run_command: Use para executar comandos shell (npm test, build, lint, etc).
|
|
45
|
+
4. search_internet: Se o usuário pedir algo que você não sabe ou que requer dados atualizados, use esta ferramenta para pesquisar na web.
|
|
46
|
+
|
|
47
|
+
Sempre explique brevemente o que você vai fazer antes de chamar uma ferramenta.
|
|
48
|
+
Suas respostas devem ser em Markdown.\n`;
|
|
49
|
+
|
|
39
50
|
return context;
|
|
40
51
|
}
|
|
@@ -47,30 +47,34 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
47
47
|
}, { signal: options.signal });
|
|
48
48
|
|
|
49
49
|
if (response.stop_reason === 'tool_use') {
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
const toolUseParts = response.content.filter(p => p.type === 'tool_use');
|
|
51
|
+
const toolResults = [];
|
|
52
|
+
|
|
53
|
+
for (const toolUse of toolUseParts) {
|
|
54
54
|
if (options.signal?.aborted) throw new Error('Abortado pelo usuário');
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
tool_use_id: toolUse.id,
|
|
65
|
-
content: String(result)
|
|
66
|
-
}]
|
|
67
|
-
}
|
|
68
|
-
];
|
|
69
|
-
|
|
70
|
-
return this.sendMessage(nextMessages, options);
|
|
55
|
+
const tool = tools.find(t => t.name === toolUse.name);
|
|
56
|
+
if (tool) {
|
|
57
|
+
const result = await tool.execute(toolUse.input, options);
|
|
58
|
+
toolResults.push({
|
|
59
|
+
type: 'tool_result',
|
|
60
|
+
tool_use_id: toolUse.id,
|
|
61
|
+
content: String(result)
|
|
62
|
+
});
|
|
63
|
+
}
|
|
71
64
|
}
|
|
65
|
+
|
|
66
|
+
const nextMessages = [
|
|
67
|
+
...messages,
|
|
68
|
+
{ role: 'assistant', content: response.content },
|
|
69
|
+
{
|
|
70
|
+
role: 'user',
|
|
71
|
+
content: toolResults
|
|
72
|
+
}
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
return this.sendMessage(nextMessages, options);
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
return response.content
|
|
78
|
+
return response.content.find(p => p.type === 'text')?.text || "";
|
|
75
79
|
}
|
|
76
80
|
}
|
package/src/providers/base.js
CHANGED
package/src/providers/gemini.js
CHANGED
|
@@ -53,22 +53,54 @@ export class GeminiProvider extends BaseProvider {
|
|
|
53
53
|
const result = await chat.sendMessage(lastMessageContent);
|
|
54
54
|
const response = await result.response;
|
|
55
55
|
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
56
|
+
const toolCalls = response.candidates[0].content.parts.filter(p => p.functionCall);
|
|
57
|
+
if (toolCalls.length > 0) {
|
|
58
58
|
if (options.signal?.aborted) throw new Error('Abortado pelo usuário');
|
|
59
59
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
60
|
+
const functionResponses = [];
|
|
61
|
+
for (const call of toolCalls) {
|
|
62
|
+
const tool = tools.find(t => t.name === call.functionCall.name);
|
|
63
|
+
if (tool) {
|
|
64
|
+
const toolResult = await tool.execute(call.functionCall.args, options);
|
|
65
|
+
functionResponses.push({
|
|
66
|
+
functionResponse: {
|
|
67
|
+
name: call.functionCall.name,
|
|
68
|
+
response: { content: toolResult }
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
71
72
|
}
|
|
73
|
+
|
|
74
|
+
const resultResponse = await chat.sendMessage(functionResponses);
|
|
75
|
+
const nextResponse = await resultResponse.response;
|
|
76
|
+
|
|
77
|
+
// Se o próximo turno também tiver chamadas de função, precisamos de recursão.
|
|
78
|
+
// No entanto, a API do Gemini lida com o histórico dentro do chat.
|
|
79
|
+
// Vamos verificar se há mais chamadas.
|
|
80
|
+
const moreToolCalls = nextResponse.candidates[0].content.parts.filter(p => p.functionCall);
|
|
81
|
+
if (moreToolCalls.length > 0) {
|
|
82
|
+
// Para manter a simplicidade e recursão similar ao OpenAI:
|
|
83
|
+
// Mas o sendMessage do Gemini retorna a resposta final.
|
|
84
|
+
// Vamos tentar processar recursivamente se necessário.
|
|
85
|
+
// A forma mais robusta é um loop.
|
|
86
|
+
let currentResponse = nextResponse;
|
|
87
|
+
while (currentResponse.candidates[0].content.parts.some(p => p.functionCall)) {
|
|
88
|
+
const nextCalls = currentResponse.candidates[0].content.parts.filter(p => p.functionCall);
|
|
89
|
+
const nextResponses = [];
|
|
90
|
+
for (const call of nextCalls) {
|
|
91
|
+
const t = tools.find(tool => tool.name === call.functionCall.name);
|
|
92
|
+
if (t) {
|
|
93
|
+
const r = await t.execute(call.functionCall.args, options);
|
|
94
|
+
nextResponses.push({ functionResponse: { name: call.functionCall.name, response: { content: r } } });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const rRes = await chat.sendMessage(nextResponses);
|
|
98
|
+
currentResponse = await rRes.response;
|
|
99
|
+
}
|
|
100
|
+
return currentResponse.text();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return nextResponse.text();
|
|
72
104
|
}
|
|
73
105
|
|
|
74
106
|
return response.text();
|
package/src/providers/grok.js
CHANGED
|
@@ -36,12 +36,52 @@ export class GrokProvider extends BaseProvider {
|
|
|
36
36
|
async sendMessage(messages, options = {}) {
|
|
37
37
|
const formattedMessages = this.formatMessages(messages);
|
|
38
38
|
|
|
39
|
-
const
|
|
39
|
+
const openAiTools = tools.map(t => ({
|
|
40
|
+
type: 'function',
|
|
41
|
+
function: {
|
|
42
|
+
name: t.name,
|
|
43
|
+
description: t.description,
|
|
44
|
+
parameters: t.parameters
|
|
45
|
+
}
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
const requestOptions = {
|
|
40
49
|
model: this.config.model,
|
|
41
50
|
messages: formattedMessages,
|
|
42
51
|
temperature: 0.7
|
|
43
|
-
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (openAiTools.length > 0) {
|
|
55
|
+
requestOptions.tools = openAiTools;
|
|
56
|
+
requestOptions.tool_choice = 'auto';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const response = await this.client.chat.completions.create(requestOptions, { signal: options.signal });
|
|
60
|
+
|
|
61
|
+
const message = response.choices[0].message;
|
|
62
|
+
|
|
63
|
+
if (message.tool_calls) {
|
|
64
|
+
const toolResults = [];
|
|
65
|
+
for (const toolCall of message.tool_calls) {
|
|
66
|
+
if (options.signal?.aborted) throw new Error('Abortado pelo usuário');
|
|
67
|
+
|
|
68
|
+
const tool = tools.find(t => t.name === toolCall.function.name);
|
|
69
|
+
if (tool) {
|
|
70
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
71
|
+
const result = await tool.execute(args, options);
|
|
72
|
+
|
|
73
|
+
toolResults.push({
|
|
74
|
+
role: 'tool',
|
|
75
|
+
tool_call_id: toolCall.id,
|
|
76
|
+
content: String(result)
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const nextMessages = [...formattedMessages, message, ...toolResults];
|
|
82
|
+
return this.sendMessage(nextMessages, options);
|
|
83
|
+
}
|
|
44
84
|
|
|
45
|
-
return
|
|
85
|
+
return message.content;
|
|
46
86
|
}
|
|
47
87
|
}
|
package/src/providers/openai.js
CHANGED
|
@@ -73,7 +73,7 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
73
73
|
const tool = tools.find(t => t.name === toolCall.function.name);
|
|
74
74
|
if (tool) {
|
|
75
75
|
const args = JSON.parse(toolCall.function.arguments);
|
|
76
|
-
const result = await tool.execute(args);
|
|
76
|
+
const result = await tool.execute(args, options);
|
|
77
77
|
|
|
78
78
|
toolResults.push({
|
|
79
79
|
role: 'tool',
|