bimmo-cli 1.2.3 → 2.0.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/bin/bimmo CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Silencia o aviso de depreciação do punycode que polui o terminal em versões novas do Node
4
+ process.removeAllListeners('warning');
5
+
3
6
  import { program } from 'commander';
4
7
  import { startInteractive } from '../src/interface.js';
5
8
  import { configure } from '../src/config.js';
@@ -7,13 +10,13 @@ import { configure } from '../src/config.js';
7
10
  program
8
11
  .name('bimmo')
9
12
  .description('bimmo — Sua IA universal no terminal (verde & lavanda)')
10
- .version('1.1.0');
13
+ .version('1.2.4');
11
14
 
12
15
  program
13
16
  .command('config')
14
17
  .description('Configurar provedor e chave de API')
15
18
  .action(configure);
16
19
 
17
- program.action(startInteractive); // ao digitar só "bimmo" entra no chat
20
+ program.action(startInteractive);
18
21
 
19
- program.parse();
22
+ program.parse();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bimmo-cli",
3
- "version": "1.2.3",
4
- "description": "🌿 CLI universal para IAs com interface verde/lavanda, modo agente (Auto-Edit) e contexto inteligente de projeto.",
3
+ "version": "2.0.1",
4
+ "description": "🌿 Plataforma de Orquestração de Agentes IA (Swarms) com interface verde/lavanda, modo Auto-Edit e contexto inteligente.",
5
5
  "bin": {
6
6
  "bimmo": "bin/bimmo"
7
7
  },
@@ -15,7 +15,10 @@
15
15
  "grok",
16
16
  "deepseek",
17
17
  "openrouter",
18
+ "zai",
18
19
  "agent",
20
+ "swarm",
21
+ "orchestration",
19
22
  "multimodal",
20
23
  "terminal"
21
24
  ],
package/src/config.js CHANGED
@@ -29,6 +29,7 @@ export async function configure() {
29
29
  choices: [
30
30
  { name: 'Criar novo perfil de IA', value: 'create' },
31
31
  { name: 'Selecionar perfil ativo', value: 'select' },
32
+ { name: 'Gerenciar Agentes Especialistas', value: 'agents' },
32
33
  { name: 'Configurar chave Tavily', value: 'tavily' },
33
34
  { name: 'Sair', value: 'exit' }
34
35
  ]
@@ -37,6 +38,8 @@ export async function configure() {
37
38
 
38
39
  if (action === 'exit') return;
39
40
 
41
+ if (action === 'agents') return configureAgents();
42
+
40
43
  if (action === 'tavily') {
41
44
  const { tavilyKey } = await inquirer.prompt([{
42
45
  type: 'input',
@@ -74,7 +77,7 @@ export async function configure() {
74
77
  {
75
78
  type: 'input',
76
79
  name: 'profileName',
77
- message: 'Dê um nome para este perfil (ex: MeuGPT, ClaudeTrabalho):',
80
+ message: 'Dê um nome para este perfil:',
78
81
  validate: i => i.length > 0 || 'Nome obrigatório'
79
82
  },
80
83
  {
@@ -120,6 +123,50 @@ export async function configure() {
120
123
  console.log(chalk.green(`\n✅ Perfil "${answers.profileName}" criado e ativado!`));
121
124
  }
122
125
 
126
+ async function configureAgents() {
127
+ const agents = config.get('agents') || {};
128
+ const profiles = config.get('profiles') || {};
129
+ const profileList = Object.keys(profiles);
130
+
131
+ const { action } = await inquirer.prompt([{
132
+ type: 'list',
133
+ name: 'action',
134
+ message: 'Gerenciar Agentes:',
135
+ choices: ['Criar Agente', 'Listar Agentes', 'Remover Agente', 'Voltar']
136
+ }]);
137
+
138
+ if (action === 'Voltar') return configure();
139
+
140
+ if (action === 'Criar Agente') {
141
+ if (profileList.length === 0) {
142
+ console.log(chalk.red('Crie um perfil de IA primeiro.'));
143
+ return configure();
144
+ }
145
+
146
+ const answers = await inquirer.prompt([
147
+ { type: 'input', name: 'name', message: 'Nome do Agente (ex: Arquiteto, Revisor):' },
148
+ { type: 'list', name: 'profile', message: 'Qual perfil este agente usará?', choices: profileList },
149
+ { type: 'input', name: 'modelOverride', message: 'Sobrescrever modelo (vazio para manter o do perfil):' },
150
+ { type: 'list', name: 'mode', message: 'Modo padrão:', choices: ['chat', 'plan', 'edit'] },
151
+ { type: 'editor', name: 'role', message: 'Descreva a Task/Papel deste agente (System Prompt):' }
152
+ ]);
153
+
154
+ agents[answers.name] = answers;
155
+ config.set('agents', agents);
156
+ console.log(chalk.green(`✓ Agente "${answers.name}" criado!`));
157
+ }
158
+
159
+ if (action === 'Listar Agentes') {
160
+ console.log(chalk.blue('\nAgentes Configurados:'));
161
+ Object.keys(agents).forEach(name => {
162
+ console.log(`- ${chalk.bold(name)} [${agents[name].profile}] (Modo: ${agents[name].mode})`);
163
+ });
164
+ console.log('');
165
+ }
166
+
167
+ return configureAgents();
168
+ }
169
+
123
170
  export function getConfig() {
124
171
  return config.store;
125
172
  }
package/src/interface.js CHANGED
@@ -7,10 +7,12 @@ import ora from 'ora';
7
7
  import fs from 'fs';
8
8
  import path from 'path';
9
9
  import mime from 'mime-types';
10
+ import readline from 'readline';
10
11
 
11
12
  import { getConfig, configure, updateActiveModel, switchProfile } from './config.js';
12
13
  import { createProvider } from './providers/factory.js';
13
14
  import { getProjectContext } from './project-context.js';
15
+ import { SwarmOrchestrator } from './orchestrator.js';
14
16
 
15
17
  marked.use(new TerminalRenderer({
16
18
  heading: chalk.hex('#c084fc').bold,
@@ -109,9 +111,9 @@ export async function startInteractive() {
109
111
  }
110
112
 
111
113
  let provider = createProvider(config);
114
+ const orchestrator = new SwarmOrchestrator(config);
112
115
  const messages = [];
113
116
 
114
- // 1. Injeta o sistema de contexto inteligente do projeto
115
117
  const projectContext = getProjectContext();
116
118
  messages.push({
117
119
  role: 'system',
@@ -123,11 +125,14 @@ export async function startInteractive() {
123
125
  console.log(lavender('─'.repeat(60)));
124
126
  console.log(green(` Perfil Ativo: ${bold(config.activeProfile || 'Padrão')} (${config.provider.toUpperCase()})`));
125
127
  console.log(green(` Modelo: ${bold(config.model)}`));
126
- console.log(gray(' /chat | /plan | /edit | /switch [perfil] | /model [novo] | /help'));
128
+ console.log(gray(' /chat | /plan | /edit | /swarm | /switch [perfil] | /model [novo] | /help'));
127
129
  console.log(lavender('─'.repeat(60)) + '\n');
128
130
 
129
131
  console.log(lavender('👋 Olá! Sou seu agente BIMMO. No que posso atuar?\n'));
130
132
 
133
+ readline.emitKeypressEvents(process.stdin);
134
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
135
+
131
136
  while (true) {
132
137
  const modeIndicator = getModeStyle();
133
138
  const { input } = await inquirer.prompt([
@@ -144,19 +149,18 @@ export async function startInteractive() {
144
149
 
145
150
  if (cmd === '/exit' || cmd === 'exit' || cmd === 'sair') {
146
151
  console.log(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
147
- break;
152
+ process.exit(0);
148
153
  }
149
154
 
150
155
  if (cmd === '/chat') { currentMode = 'chat'; console.log(lavender('✓ Modo CHAT.\n')); continue; }
151
156
  if (cmd === '/plan') { currentMode = 'plan'; console.log(yellow('✓ Modo PLAN.\n')); continue; }
152
157
  if (cmd === '/edit') { currentMode = 'edit'; console.log(chalk.red('⚠️ Modo EDIT.\n')); continue; }
153
158
 
154
- // /switch [perfil] -> Troca Perfil + Chave + Provedor + Modelo instantaneamente
155
159
  if (cmd.startsWith('/switch ')) {
156
160
  const profileName = rawInput.split(' ')[1];
157
161
  if (profileName && switchProfile(profileName)) {
158
- config = getConfig(); // Atualiza config local
159
- provider = createProvider(config); // Recria provedor com nova chave/url
162
+ config = getConfig();
163
+ provider = createProvider(config);
160
164
  console.log(green(`\n✓ Trocado para o perfil "${bold(profileName)}"!`));
161
165
  console.log(gray(` IA: ${config.provider.toUpperCase()} | Modelo: ${config.model}\n`));
162
166
  } else {
@@ -165,7 +169,6 @@ export async function startInteractive() {
165
169
  continue;
166
170
  }
167
171
 
168
- // /model [modelo] -> Troca apenas o modelo do Perfil atual
169
172
  if (cmd.startsWith('/model ')) {
170
173
  const newModel = rawInput.split(' ')[1];
171
174
  if (newModel) {
@@ -189,56 +192,83 @@ export async function startInteractive() {
189
192
  console.log(gray(`
190
193
  Comandos Disponíveis:
191
194
  /chat /plan /edit → Mudar modo de operação
192
- /switch [nome] → Mudar PERFIL (Troca Chave/API/IA completa)
193
- /model [nome] → Mudar apenas o MODELO da IA atual
194
- /init Inicializar .bimmorc.json neste projeto
195
- /config → Gerenciar perfis e chaves
196
- /clear Resetar conversa (mantém contexto base)
195
+ /switch [nome] → Mudar PERFIL (IA completa)
196
+ /model [nome] → Mudar apenas o MODELO atual
197
+ /swarm Configurar e rodar enxames de agentes
198
+ /config → Gerenciar perfis e agentes
199
+ /init Inicializar .bimmorc.json
197
200
  @caminho → Anexar arquivos ou imagens
198
201
  `));
199
202
  continue;
200
203
  }
201
204
 
202
- if (cmd === '/init') {
203
- const bimmoRcPath = path.join(process.cwd(), '.bimmorc.json');
204
- if (fs.existsSync(bimmoRcPath)) {
205
- const { overwrite } = await inquirer.prompt([{
206
- type: 'confirm',
207
- name: 'overwrite',
208
- message: 'O arquivo .bimmorc.json existe. Deseja sobrescrever?',
209
- default: false
205
+ if (cmd === '/config') { await configure(); config = getConfig(); provider = createProvider(config); continue; }
206
+
207
+ if (cmd === '/swarm') {
208
+ const agents = config.agents || {};
209
+ const agentList = Object.keys(agents);
210
+
211
+ if (agentList.length < 2) {
212
+ console.log(chalk.yellow('\nVocê precisa de pelo menos 2 Agentes configurados para rodar um Enxame.\nUse /config para criar Agentes.\n'));
213
+ continue;
214
+ }
215
+
216
+ const { swarmAction } = await inquirer.prompt([{
217
+ type: 'list',
218
+ name: 'swarmAction',
219
+ message: 'Ação de Enxame:',
220
+ choices: ['Rodar Enxame Sequencial', 'Rodar Enxame Hierárquico', 'Voltar']
221
+ }]);
222
+
223
+ if (swarmAction === 'Voltar') continue;
224
+
225
+ const { goal } = await inquirer.prompt([{ type: 'input', name: 'goal', message: 'Qual o objetivo final deste enxame?' }]);
226
+
227
+ if (swarmAction === 'Rodar Enxame Sequencial') {
228
+ const { selectedAgents } = await inquirer.prompt([{
229
+ type: 'checkbox',
230
+ name: 'selectedAgents',
231
+ message: 'Selecione os agentes e a ordem (mínimo 2):',
232
+ choices: agentList
210
233
  }]);
211
- if (!overwrite) continue;
234
+
235
+ if (selectedAgents.length < 2) {
236
+ console.log(chalk.red('\nSelecione pelo menos 2 agentes.\n'));
237
+ continue;
238
+ }
239
+
240
+ try {
241
+ const finalResult = await orchestrator.runSequential(selectedAgents, goal);
242
+ console.log(lavender('\n=== RESULTADO FINAL DO ENXAME ===\n'));
243
+ console.log(marked(finalResult));
244
+ } catch (e) {
245
+ console.error(chalk.red(`\nErro no Enxame: ${e.message}`));
246
+ }
212
247
  }
213
248
 
214
- const initialConfig = {
215
- projectName: path.basename(process.cwd()),
216
- rules: [
217
- "Siga as convenções de código existentes.",
218
- "Prefira código limpo e modular.",
219
- "Sempre valide mudanças antes de aplicar no modo EDIT."
220
- ],
221
- preferredTech: [],
222
- architecture: "Não especificada",
223
- ignorePatterns: ["node_modules", "dist", ".git"]
224
- };
225
-
226
- fs.writeFileSync(bimmoRcPath, JSON.stringify(initialConfig, null, 2));
227
- console.log(green(`\n✅ Arquivo .bimmorc.json criado com sucesso em: ${bold(bimmoRcPath)}\n`));
228
-
229
- // Recarrega o contexto para a conversa atual
230
- messages.push({
231
- role: 'system',
232
- content: `Novo contexto inicializado via /init:\n${JSON.stringify(initialConfig, null, 2)}`
233
- });
249
+ if (swarmAction === 'Rodar Enxame Hierárquico') {
250
+ const { manager } = await inquirer.prompt([{ type: 'list', name: 'manager', message: 'Selecione o Agente Líder (Manager):', choices: agentList }]);
251
+ const { workers } = await inquirer.prompt([{ type: 'checkbox', name: 'workers', message: 'Selecione os Workers:', choices: agentList.filter(a => a !== manager) }]);
252
+
253
+ try {
254
+ const finalResult = await orchestrator.runHierarchical(manager, workers, goal);
255
+ console.log(lavender('\n=== RESULTADO FINAL DO ENXAME ===\n'));
256
+ console.log(marked(finalResult));
257
+ } catch (e) {
258
+ console.error(chalk.red(`\nErro no Enxame: ${e.message}`));
259
+ }
260
+ }
234
261
  continue;
235
262
  }
236
263
 
237
- if (cmd === '/config') { await configure(); config = getConfig(); provider = createProvider(config); continue; }
238
-
239
264
  if (rawInput === '') continue;
240
265
 
241
- // Injeção dinâmica de instruções de modo
266
+ const controller = new AbortController();
267
+ const interruptHandler = () => controller.abort();
268
+ const keypressHandler = (str, key) => { if (key.name === 'escape' || (key.ctrl && key.name === 'c')) interruptHandler(); };
269
+ process.on('SIGINT', interruptHandler);
270
+ process.stdin.on('keypress', keypressHandler);
271
+
242
272
  let modeInstr = "";
243
273
  if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Descreva e analise, mas NÃO altere arquivos.";
244
274
  else if (currentMode === 'edit') modeInstr = "\n[MODO EDIT] Você tem permissão para usar write_file e run_command AGORA.";
@@ -250,22 +280,30 @@ Comandos Disponíveis:
250
280
  });
251
281
 
252
282
  const spinner = ora({
253
- text: lavender(`bimmo (${currentMode}) pensando...`),
283
+ text: lavender(`bimmo (${currentMode}) pensando... (Ctrl+C para interromper)`),
254
284
  color: currentMode === 'edit' ? 'red' : 'magenta'
255
285
  }).start();
256
286
 
257
287
  try {
258
- const responseText = await provider.sendMessage(messages);
288
+ let responseText = await provider.sendMessage(messages, { signal: controller.signal });
259
289
  spinner.stop();
290
+ const cleanedText = responseText.replace(/<\/?[^>]+(>|$)/g, "");
260
291
  messages.push({ role: 'assistant', content: responseText });
261
-
262
292
  console.log('\n' + lavender('bimmo') + getModeStyle());
263
293
  console.log(lavender('─'.repeat(50)));
264
- console.log(marked(responseText));
294
+ console.log(marked(cleanedText));
265
295
  console.log(gray('─'.repeat(50)) + '\n');
266
296
  } catch (err) {
267
297
  spinner.stop();
268
- console.error(chalk.red('✖ Erro Crítico:') + ' ' + err.message + '\n');
298
+ if (controller.signal.aborted || err.name === 'AbortError') {
299
+ console.log(yellow('\n\n⚠️ Operação interrompida pelo usuário.\n'));
300
+ messages.pop();
301
+ } else {
302
+ console.error(chalk.red('\n✖ Erro Crítico:') + ' ' + err.message + '\n');
303
+ }
304
+ } finally {
305
+ process.off('SIGINT', interruptHandler);
306
+ process.stdin.off('keypress', keypressHandler);
269
307
  }
270
308
  }
271
309
  }
@@ -0,0 +1,84 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { createProvider } from './providers/factory.js';
4
+ import { getProjectContext } from './project-context.js';
5
+
6
+ export class SwarmOrchestrator {
7
+ constructor(config) {
8
+ this.config = config;
9
+ this.agents = config.agents || {};
10
+ this.profiles = config.profiles || {};
11
+ }
12
+
13
+ /**
14
+ * Executa uma tarefa sequencialmente através de uma lista de agentes.
15
+ * O output de um agente vira o contexto do próximo.
16
+ */
17
+ async runSequential(agentNames, goal, options = {}) {
18
+ let currentContext = goal;
19
+ const results = [];
20
+
21
+ console.log(chalk.cyan(`\n🚀 Iniciando Enxame Sequencial: ${agentNames.join(' → ')}\n`));
22
+
23
+ for (const name of agentNames) {
24
+ const agent = this.agents[name];
25
+ if (!agent) throw new Error(`Agente ${name} não encontrado.`);
26
+
27
+ const profile = this.profiles[agent.profile];
28
+ if (!profile) throw new Error(`Perfil ${agent.profile} do agente ${name} não encontrado.`);
29
+
30
+ const agentConfig = {
31
+ ...profile,
32
+ model: agent.modelOverride || profile.model
33
+ };
34
+
35
+ const provider = createProvider(agentConfig);
36
+ const spinner = ora({
37
+ text: chalk.magenta(`Agente [${name}] trabalhando...`),
38
+ color: agent.mode === 'edit' ? 'red' : 'magenta'
39
+ }).start();
40
+
41
+ const messages = [
42
+ { role: 'system', content: `Você é o agente ${name}. Sua tarefa específica é: ${agent.role}\n\nMODO ATUAL: ${agent.mode.toUpperCase()}\n${getProjectContext()}` },
43
+ { role: 'user', content: `CONTEXTO ATUAL:\n${currentContext}\n\nOBJETIVO DO ENXAME:\n${goal}\n\nPor favor, execute sua parte e retorne o resultado final processado.` }
44
+ ];
45
+
46
+ try {
47
+ const response = await provider.sendMessage(messages, { signal: options.signal });
48
+ spinner.succeed(chalk.green(`Agente [${name}] concluído.`));
49
+
50
+ currentContext = response;
51
+ results.push({ agent: name, output: response });
52
+ } catch (err) {
53
+ spinner.fail(chalk.red(`Agente [${name}] falhou: ${err.message}`));
54
+ throw err;
55
+ }
56
+ }
57
+
58
+ return currentContext;
59
+ }
60
+
61
+ /**
62
+ * Executa uma tarefa hierárquica.
63
+ * Um Manager recebe o objetivo, define o que cada Worker deve fazer, e consolida.
64
+ */
65
+ async runHierarchical(managerName, workerNames, goal, options = {}) {
66
+ console.log(chalk.cyan(`\n👑 Iniciando Enxame Hierárquico (Líder: ${managerName})\n`));
67
+
68
+ // Passo 1: Manager analisa e delega
69
+ const managerOutput = await this.runSequential([managerName], `Analise o objetivo abaixo e descreva o que cada um dos seguintes agentes deve fazer: ${workerNames.join(', ')}.\n\nOBJETIVO: ${goal}`);
70
+
71
+ // Passo 2: Workers executam baseado na delegação do Manager (em paralelo para velocidade)
72
+ console.log(chalk.blue(`\n👷 Workers entrando em ação...\n`));
73
+ const workerPromises = workerNames.map(name =>
74
+ this.runSequential([name], `Baseado no plano do Manager:\n${managerOutput}\n\nExecute sua tarefa para o objetivo: ${goal}`, options)
75
+ );
76
+
77
+ const workerResults = await Promise.all(workerPromises);
78
+
79
+ // Passo 3: Manager consolida tudo
80
+ const finalResult = await this.runSequential([managerName], `Aqui estão os resultados dos workers:\n${workerResults.join('\n---\n')}\n\nPor favor, consolide tudo em um resultado final perfeito para o objetivo: ${goal}`, options);
81
+
82
+ return finalResult;
83
+ }
84
+ }
@@ -21,7 +21,7 @@ export class AnthropicProvider extends BaseProvider {
21
21
  });
22
22
  }
23
23
 
24
- async sendMessage(messages) {
24
+ async sendMessage(messages, options = {}) {
25
25
  const systemMessage = messages.find(m => m.role === 'system');
26
26
  const userMessages = messages
27
27
  .filter(m => m.role !== 'system')
@@ -30,7 +30,6 @@ export class AnthropicProvider extends BaseProvider {
30
30
  content: this.formatContent(m.content)
31
31
  }));
32
32
 
33
- // Converte tools do agent.js para o formato da Anthropic
34
33
  const anthropicTools = tools.map(t => ({
35
34
  name: t.name,
36
35
  description: t.description,
@@ -44,17 +43,18 @@ export class AnthropicProvider extends BaseProvider {
44
43
  messages: userMessages,
45
44
  tools: anthropicTools,
46
45
  temperature: 0.7
47
- });
46
+ }, { signal: options.signal });
48
47
 
49
48
  if (response.stop_reason === 'tool_use') {
50
49
  const toolUse = response.content.find(p => p.type === 'tool_use');
51
50
  const tool = tools.find(t => t.name === toolUse.name);
52
51
 
53
52
  if (tool) {
53
+ if (options.signal?.aborted) throw new Error('Abortado pelo usuário');
54
+
54
55
  console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
55
56
  const result = await tool.execute(toolUse.input);
56
57
 
57
- // Adiciona a resposta da IA e o resultado da tool ao histórico
58
58
  const nextMessages = [
59
59
  ...messages,
60
60
  { role: 'assistant', content: response.content },
@@ -68,10 +68,10 @@ export class AnthropicProvider extends BaseProvider {
68
68
  }
69
69
  ];
70
70
 
71
- return this.sendMessage(nextMessages);
71
+ return this.sendMessage(nextMessages, options);
72
72
  }
73
73
  }
74
74
 
75
75
  return response.content[0].text;
76
76
  }
77
- }
77
+ }
@@ -3,7 +3,7 @@ export class BaseProvider {
3
3
  this.config = config;
4
4
  }
5
5
 
6
- async sendMessage(messages) {
6
+ async sendMessage(messages, options = {}) {
7
7
  throw new Error('Método sendMessage deve ser implementado');
8
8
  }
9
- }
9
+ }
@@ -18,7 +18,7 @@ export class GeminiProvider extends BaseProvider {
18
18
  });
19
19
  }
20
20
 
21
- async sendMessage(messages) {
21
+ async sendMessage(messages, options = {}) {
22
22
  const systemPrompt = messages.find(m => m.role === 'system')?.content;
23
23
  const history = messages
24
24
  .filter(m => m.role !== 'system')
@@ -30,7 +30,6 @@ export class GeminiProvider extends BaseProvider {
30
30
 
31
31
  const lastMessageContent = this.formatContent(messages[messages.length - 1].content);
32
32
 
33
- // Converte tools do agent.js para o formato do Gemini
34
33
  const geminiTools = tools.map(t => ({
35
34
  functionDeclarations: [{
36
35
  name: t.name,
@@ -53,19 +52,19 @@ export class GeminiProvider extends BaseProvider {
53
52
  const result = await chat.sendMessage(lastMessageContent);
54
53
  const response = await result.response;
55
54
 
56
- // Processamento de Tool Calls no Gemini
57
55
  const call = response.candidates[0].content.parts.find(p => p.functionCall);
58
56
  if (call) {
57
+ if (options.signal?.aborted) throw new Error('Abortado pelo usuário');
58
+
59
59
  const tool = tools.find(t => t.name === call.functionCall.name);
60
60
  if (tool) {
61
61
  console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
62
- const result = await tool.execute(call.functionCall.args);
62
+ const toolResult = await tool.execute(call.functionCall.args);
63
63
 
64
- // No Gemini, enviamos o resultado de volta para o chat
65
64
  const resultResponse = await chat.sendMessage([{
66
65
  functionResponse: {
67
66
  name: call.functionCall.name,
68
- response: { content: result }
67
+ response: { content: toolResult }
69
68
  }
70
69
  }]);
71
70
  return resultResponse.response.text();
@@ -74,4 +73,4 @@ export class GeminiProvider extends BaseProvider {
74
73
 
75
74
  return response.text();
76
75
  }
77
- }
76
+ }
@@ -1,5 +1,6 @@
1
1
  import OpenAI from 'openai';
2
2
  import { BaseProvider } from './base.js';
3
+ import { tools } from '../agent.js';
3
4
 
4
5
  export class GrokProvider extends BaseProvider {
5
6
  constructor(config) {
@@ -33,15 +34,15 @@ export class GrokProvider extends BaseProvider {
33
34
  });
34
35
  }
35
36
 
36
- async sendMessage(messages) {
37
+ async sendMessage(messages, options = {}) {
37
38
  const formattedMessages = this.formatMessages(messages);
38
39
 
39
40
  const response = await this.client.chat.completions.create({
40
41
  model: this.config.model,
41
42
  messages: formattedMessages,
42
43
  temperature: 0.7
43
- });
44
+ }, { signal: options.signal });
44
45
 
45
46
  return response.choices[0].message.content;
46
47
  }
47
- }
48
+ }
@@ -13,7 +13,7 @@ export class OllamaProvider extends BaseProvider {
13
13
 
14
14
  const images = msg.content
15
15
  .filter(part => part.type === 'image')
16
- .map(part => part.data); // Ollama espera apenas a string base64
16
+ .map(part => part.data);
17
17
 
18
18
  const text = msg.content
19
19
  .filter(part => part.type === 'text')
@@ -28,7 +28,7 @@ export class OllamaProvider extends BaseProvider {
28
28
  });
29
29
  }
30
30
 
31
- async sendMessage(messages) {
31
+ async sendMessage(messages, options = {}) {
32
32
  const formattedMessages = this.formatMessages(messages);
33
33
 
34
34
  const response = await this.client.chat({
@@ -38,7 +38,7 @@ export class OllamaProvider extends BaseProvider {
38
38
  options: {
39
39
  temperature: 0.7,
40
40
  }
41
- });
41
+ }, { signal: options.signal });
42
42
 
43
43
  const text = response.message?.content;
44
44
 
@@ -48,4 +48,4 @@ export class OllamaProvider extends BaseProvider {
48
48
 
49
49
  return text;
50
50
  }
51
- }
51
+ }
@@ -34,7 +34,7 @@ export class OpenAIProvider extends BaseProvider {
34
34
  });
35
35
  }
36
36
 
37
- async sendMessage(messages) {
37
+ async sendMessage(messages, options = {}) {
38
38
  const formattedMessages = this.formatMessages(messages);
39
39
 
40
40
  const openAiTools = tools.map(t => ({
@@ -46,25 +46,26 @@ export class OpenAIProvider extends BaseProvider {
46
46
  }
47
47
  }));
48
48
 
49
- const options = {
49
+ const requestOptions = {
50
50
  model: this.config.model,
51
51
  messages: formattedMessages,
52
52
  temperature: 0.7
53
53
  };
54
54
 
55
- // Só envia tools se houver alguma definida para evitar erros em alguns provedores
56
55
  if (openAiTools.length > 0) {
57
- options.tools = openAiTools;
58
- options.tool_choice = 'auto';
56
+ requestOptions.tools = openAiTools;
57
+ requestOptions.tool_choice = 'auto';
59
58
  }
60
59
 
61
- const response = await this.client.chat.completions.create(options);
60
+ const response = await this.client.chat.completions.create(requestOptions, { signal: options.signal });
62
61
 
63
62
  const message = response.choices[0].message;
64
63
 
65
64
  if (message.tool_calls) {
66
65
  const toolResults = [];
67
66
  for (const toolCall of message.tool_calls) {
67
+ if (options.signal?.aborted) throw new Error('Abortado pelo usuário');
68
+
68
69
  const tool = tools.find(t => t.name === toolCall.function.name);
69
70
  if (tool) {
70
71
  console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
@@ -80,9 +81,9 @@ export class OpenAIProvider extends BaseProvider {
80
81
  }
81
82
 
82
83
  const nextMessages = [...formattedMessages, message, ...toolResults];
83
- return this.sendMessage(nextMessages);
84
+ return this.sendMessage(nextMessages, options);
84
85
  }
85
86
 
86
87
  return message.content;
87
88
  }
88
- }
89
+ }