bimmo-cli 4.0.5 → 4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimmo-cli",
3
- "version": "4.0.5",
3
+ "version": "4.1.0",
4
4
  "description": "🌿 Plataforma de IA universal profissional com interface React (Ink) pura.",
5
5
  "bin": {
6
6
  "bimmo": "bin/bimmo"
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. Use /config para configurar.';
31
- console.log(chalk.blue(`\n 🌐 Pesquisando na web: ${chalk.bold(query)}...`));
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
- console.log(chalk.blue(`\n 📖 Lendo arquivo: ${chalk.bold(filePath)}...`));
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 com um conteúdo específico.',
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
- process.stdout.write(color(`${prefix} ${line}\n`));
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
- 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`)));
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
- // Lógica de Aprovação
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
- if (approve === 'no') return "Alteração rejeitada pelo usuário.";
128
- if (approve === 'all') editState.autoAccept = true;
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 a ser executado' }
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
- console.log(chalk.yellow(`\n ⚡ Comando proposto: ${chalk.bold(command)}`));
131
+ onStatus?.({ type: 'command', message: `Executando: ${command}` });
154
132
 
155
133
  if (!editState.autoAccept) {
156
- const { approve } = await inquirer.prompt([{
157
- type: 'list',
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
- return output || 'Comando executado com sucesso (sem retorno).';
139
+ onStatus?.({ type: 'command_output', message: 'Resultado do comando', output });
140
+ return output || 'Comando executado com sucesso.';
173
141
  } catch (err) {
174
- return `Erro ao executar comando: ${err.stderr || err.message}`;
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,129 +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)) return [];
156
- return fs.readdirSync(absDir)
157
- .filter(f => f.startsWith(filter) && !f.startsWith('.') && f !== 'node_modules')
158
- .slice(0, 5)
159
- .map(f => ({
160
- name: f,
161
- isDir: fs.statSync(path.join(absDir, f)).isDirectory(),
162
- rel: path.join(dir === '.' ? '' : dir, f)
163
- }));
164
- } catch (e) { return []; }
165
- }, [input]);
166
-
167
- useEffect(() => {
168
- setSelectedIndex(0);
169
- }, [filePreview.length]);
170
180
 
171
181
  const handleSubmit = async (val) => {
172
- const rawInput = val.trim();
173
- if (!rawInput) return;
174
- setInput('');
175
- const parts = rawInput.split(' ');
176
- const cmd = parts[0].toLowerCase();
177
-
178
- // Comandos de Sistema
179
- if (cmd === '/exit') exit();
180
- if (cmd === '/clear') {
181
- setStaticMessages([]);
182
- setMessages([{ role: 'system', content: getProjectContext() }, { role: 'assistant', content: 'Chat limpo.' }]);
183
- return;
184
- }
185
- if (['/chat', '/plan', '/edit'].includes(cmd)) {
186
- setMode(cmd.slice(1));
187
- if (cmd === '/edit') editState.autoAccept = parts[1] === 'auto';
188
- return;
189
- }
190
-
191
- if (cmd === '/model') {
192
- const newModel = parts[1];
193
- if (newModel) {
194
- updateActiveModel(newModel);
195
- const newCfg = getConfig();
196
- setConfig(newCfg);
197
- setProvider(createProvider(newCfg));
198
- setMessages(prev => [...prev, { role: 'system', content: `Modelo alterado para: ${newModel}` }]);
199
- }
200
- return;
201
- }
202
-
203
- if (cmd === '/switch') {
204
- const profile = parts[1];
205
- if (switchProfile(profile)) {
206
- const newCfg = getConfig();
207
- setConfig(newCfg);
208
- setProvider(createProvider(newCfg));
209
- setMessages(prev => [...prev, { role: 'system', content: `Perfil alterado para: ${profile}` }]);
210
- }
211
- return;
212
- }
213
-
214
- if (cmd === '/use') {
215
- const agentName = parts[1];
216
- const agents = config.agents || {};
217
- if (agentName === 'normal') {
218
- setActivePersona(null);
219
- } else if (agents[agentName]) {
220
- setActivePersona(agentName);
221
- setMode(agents[agentName].mode || 'chat');
222
- }
223
- return;
224
- }
225
-
226
- if (cmd === '/help') {
227
- 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` }]);
228
- return;
229
- }
230
-
231
- // Processamento de arquivos
232
- setIsThinking(true);
233
- let processedInput = rawInput;
234
- const fileMatches = rawInput.match(/@[\w\.\-\/]+/g);
235
- if (fileMatches) {
236
- for (const match of fileMatches) {
237
- const filePath = match.slice(1);
238
- try {
239
- if (fs.existsSync(filePath)) {
240
- const content = fs.readFileSync(filePath, 'utf-8');
241
- processedInput = processedInput.replace(match, `\n\n[Arquivo: ${filePath}]\n\`\`\`\n${content}\n\`\`\`\n`);
242
- }
243
- } catch (e) {}
244
- }
245
- }
246
-
247
- const userMsg = { role: 'user', content: processedInput, displayContent: rawInput };
248
-
249
- // Move mensagens antigas para static para performance
250
- if (messages.length > 5) {
251
- setStaticMessages(prev => [...prev, ...messages.slice(0, -5)]);
252
- setMessages(prev => [...prev.slice(-5), userMsg]);
253
- } else {
254
- setMessages(prev => [...prev, userMsg]);
255
- }
256
-
182
+ // ... (unchanged code)
257
183
  try {
258
184
  let finalMessages = [...staticMessages, ...messages, userMsg];
259
185
  if (activePersona && config.agents[activePersona]) {
@@ -261,19 +187,49 @@ const BimmoApp = ({ initialConfig }) => {
261
187
  finalMessages = [{ role: 'system', content: `Sua tarefa: ${agent.role}\n\n${getProjectContext()}` }, ...finalMessages.filter(m => m.role !== 'system')];
262
188
  }
263
189
 
264
- const response = await provider.sendMessage(finalMessages);
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);
265
203
  setMessages(prev => [...prev, { role: 'assistant', content: response }]);
266
204
  } catch (err) {
267
205
  setMessages(prev => [...prev, { role: 'system', content: `Erro: ${err.message}` }]);
268
206
  } finally {
269
207
  setIsThinking(false);
208
+ setToolStatus(null);
209
+ setConfirmation(null);
210
+ setThinkingMessage('bimmo pensando...');
270
211
  }
271
212
  };
272
213
 
273
- useInput((input, key) => {
274
- if (key.ctrl && input === 'c') {
275
- if (isThinking) setIsThinking(false);
276
- else {
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
+ // ... rest of useInput
228
+
229
+ if (isThinking) {
230
+ setIsThinking(false);
231
+ setMessages(prev => [...prev, { role: 'system', content: 'Interrompido pelo usuário.' }]);
232
+ } else {
277
233
  if (exitCounter === 0) {
278
234
  setExitCounter(1);
279
235
  setTimeout(() => setExitCounter(0), 2000);
@@ -281,6 +237,7 @@ const BimmoApp = ({ initialConfig }) => {
281
237
  exit();
282
238
  }
283
239
  }
240
+ return;
284
241
  }
285
242
  if (key.tab && filePreview.length > 0) {
286
243
  const selected = filePreview[selectedIndex] || filePreview[0];
@@ -308,11 +265,15 @@ const BimmoApp = ({ initialConfig }) => {
308
265
  messages.filter(m => m.role !== 'system').map((m, i) => h(Message, { key: i, ...m }))
309
266
  ),
310
267
 
311
- isThinking && h(Box, { marginBottom: 1 },
312
- h(Text, { color: THEME.lavender },
313
- h(Spinner, { type: 'dots' }),
314
- h(Text, { italic: true }, ` ${thinkingMessage}`)
315
- )
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 })
316
277
  ),
317
278
 
318
279
  filePreview.length > 0 && h(AutocompleteSuggestions, { suggestions: filePreview, selectedIndex }),
@@ -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 toolUse = response.content.find(p => p.type === 'tool_use');
51
- const tool = tools.find(t => t.name === toolUse.name);
52
-
53
- if (tool) {
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 result = await tool.execute(toolUse.input);
56
-
57
- const nextMessages = [
58
- ...messages,
59
- { role: 'assistant', content: response.content },
60
- {
61
- role: 'user',
62
- content: [{
63
- type: 'tool_result',
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[0].text;
78
+ return response.content.find(p => p.type === 'text')?.text || "";
75
79
  }
76
80
  }
@@ -3,7 +3,7 @@ export class BaseProvider {
3
3
  this.config = config;
4
4
  }
5
5
 
6
- async sendMessage(messages, options = {}) {
6
+ async sendMessage(messages, options = { onStatus: null, onConfirm: null }) {
7
7
  throw new Error('Método sendMessage deve ser implementado');
8
8
  }
9
9
  }
@@ -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 call = response.candidates[0].content.parts.find(p => p.functionCall);
57
- if (call) {
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 tool = tools.find(t => t.name === call.functionCall.name);
61
- if (tool) {
62
- const toolResult = await tool.execute(call.functionCall.args);
63
-
64
- const resultResponse = await chat.sendMessage([{
65
- functionResponse: {
66
- name: call.functionCall.name,
67
- response: { content: toolResult }
68
- }
69
- }]);
70
- return resultResponse.response.text();
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();
@@ -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 response = await this.client.chat.completions.create({
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
- }, { signal: options.signal });
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 response.choices[0].message.content;
85
+ return message.content;
46
86
  }
47
87
  }
@@ -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',