bimmo-cli 1.2.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.
@@ -0,0 +1,26 @@
1
+ # BIMMO-CLI: Protocolo de Operação e Contexto
2
+
3
+ Você é o **bimmo**, uma IA universal operando via terminal com uma interface verde e lavanda. Seu objetivo é auxiliar o usuário em tarefas de codificação, pesquisa e automação com máxima precisão.
4
+
5
+ ## 🛡️ Diretrizes de Comportamento
6
+ 1. **Identidade:** Você é o bimmo. Mantenha um tom profissional, técnico e direto.
7
+ 2. **Modos de Operação:**
8
+ - **CHAT:** Apenas conversa. Não altere arquivos.
9
+ - **PLAN:** Analise e descreva mudanças. Não as execute.
10
+ - **EDIT:** Você tem permissão para usar `write_file` e `run_command`. Aplique as mudanças solicitadas com precisão cirúrgica.
11
+ 3. **Padrões de Código:** Siga sempre as convenções existentes no projeto do usuário. Prefira código limpo, tipado (quando aplicável) e bem documentado.
12
+
13
+ ## 🛠️ Ferramentas (Tools)
14
+ Você tem acesso a:
15
+ - `search_internet`: Use para buscar informações atualizadas.
16
+ - `read_file`: Use para ler o contexto de arquivos locais.
17
+ - `write_file`: Use para criar ou modificar arquivos no modo EDIT.
18
+ - `run_command`: Use para executar comandos shell (build, tests, install).
19
+
20
+ ## 📂 Contexto do Projeto
21
+ Sempre que o usuário utilizar `@caminho/do/arquivo`, o conteúdo desse arquivo será anexado à mensagem dele. Use isso para entender a estrutura e a lógica antes de sugerir ou aplicar mudanças.
22
+
23
+ ## ⚠️ Regras Críticas
24
+ - Nunca apague arquivos sem confirmação explícita.
25
+ - No modo EDIT, valide se o código gerado é funcional.
26
+ - Se não tiver certeza de algo, peça clarificação usando o modo CHAT ou PLAN.
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # 🌿 bimmo-cli
2
+
3
+ **bimmo** é um assistente de IA universal para o seu terminal, com uma interface elegante em tons de **Verde & Lavanda**. Ele funciona como um agente autônomo e multimodal que gerencia o contexto do seu projeto e executa tarefas complexas.
4
+
5
+ ---
6
+
7
+ ## 📦 Instalação Global
8
+
9
+ Para ter o **bimmo** sempre disponível no seu terminal como um comando do sistema:
10
+
11
+ ```bash
12
+ npm install -g bimmo-cli
13
+ ```
14
+
15
+ Após a instalação, basta digitar `bimmo` em qualquer pasta:
16
+
17
+ ```bash
18
+ bimmo
19
+ ```
20
+
21
+ ---
22
+
23
+ ## ✨ Principais Funcionalidades
24
+
25
+ ### 🤖 Agente Autônomo e Modos de Operação
26
+ - **/chat**: Conversa normal para tirar dúvidas.
27
+ - **/plan**: Analisa o código e descreve o plano de ação sem alterar nada.
28
+ - **/edit**: **Modo Auto-Edit**. A IA tem permissão para ler, criar, editar arquivos e rodar comandos shell para resolver problemas.
29
+
30
+ ### 📁 Contexto Inteligente de Projeto
31
+ - **Auto-Indexação**: O bimmo mapeia a estrutura do seu projeto na inicialização.
32
+ - **Herança de Regras**: Detecta automaticamente arquivos `.bimmorc.json`, `CLAUDE.md` e `INSTRUCTIONS.md`.
33
+ - **Anexos com `@`**: Digite `@caminho/arquivo` para anexar códigos ou imagens à conversa.
34
+
35
+ ### 🌐 Busca e Multi-Provedor
36
+ - **Busca na Web**: Integrado com Tavily API para pesquisas em tempo real.
37
+ - **Perfis**: Salve múltiplos perfis (OpenAI, Anthropic, Gemini, DeepSeek, Grok, Ollama, OpenRouter, Z.ai) e alterne instantaneamente com `/switch`.
38
+
39
+ ---
40
+
41
+ ## 🛠️ Comandos Rápidos no Chat
42
+
43
+ | Comando | Função |
44
+ | :--- | :--- |
45
+ | `/chat` | Modo conversa. |
46
+ | `/plan` | Modo planejamento (seguro). |
47
+ | `/edit` | Modo edição automática (agente). |
48
+ | `/init` | Cria o arquivo `.bimmorc.json` no projeto. |
49
+ | `/switch [nome]` | Troca de perfil (IA/Chave/Modelo). |
50
+ | `/model [nome]` | Troca apenas o modelo da IA atual. |
51
+ | `/config` | Gerenciar seus perfis e chaves de API. |
52
+ | `@arquivo` | Lê um arquivo ou imagem para o contexto. |
53
+
54
+ ---
55
+
56
+ ## 🚀 Publicando via GitHub (Automação)
57
+
58
+ Para publicar novas versões no NPM automaticamente usando o GitHub:
59
+
60
+ 1. Suba seu código para um repositório no GitHub.
61
+ 2. No NPM, gere um **Access Token (Automation)**.
62
+ 3. No GitHub, vá em **Settings > Secrets > Actions** e adicione o secret `NPM_TOKEN`.
63
+ 4. Sempre que você criar uma **"New Release"** no GitHub, o projeto será publicado no NPM e ficará disponível para `npm install -g`.
64
+
65
+ ---
66
+
67
+ Feito com 💜 por Judah.
package/bin/bimmo ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from 'commander';
4
+ import { startInteractive } from '../src/interface.js';
5
+ import { configure } from '../src/config.js';
6
+
7
+ program
8
+ .name('bimmo')
9
+ .description('bimmo — Sua IA universal no terminal (verde & lavanda)')
10
+ .version('1.1.0');
11
+
12
+ program
13
+ .command('config')
14
+ .description('Configurar provedor e chave de API')
15
+ .action(configure);
16
+
17
+ program.action(startInteractive); // ao digitar só "bimmo" entra no chat
18
+
19
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "bimmo-cli",
3
+ "version": "1.2.0",
4
+ "description": "🌿 CLI universal para IAs com interface verde/lavanda, modo agente (Auto-Edit) e contexto inteligente de projeto.",
5
+ "bin": {
6
+ "bimmo": "./bin/bimmo"
7
+ },
8
+ "type": "module",
9
+ "keywords": [
10
+ "ai",
11
+ "cli",
12
+ "openai",
13
+ "anthropic",
14
+ "gemini",
15
+ "grok",
16
+ "deepseek",
17
+ "openrouter",
18
+ "agent",
19
+ "multimodal",
20
+ "terminal"
21
+ ],
22
+ "author": "Judah",
23
+ "license": "MIT",
24
+ "files": [
25
+ "bin/",
26
+ "src/",
27
+ "README.md",
28
+ ".bimmo-context.md"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "@anthropic-ai/sdk": "^0.36.3",
35
+ "@google/generative-ai": "^0.21.0",
36
+ "@tavily/core": "^0.0.2",
37
+ "chalk": "^5.3.0",
38
+ "commander": "^12.1.0",
39
+ "conf": "^13.0.0",
40
+ "figlet": "^1.7.0",
41
+ "inquirer": "^10.1.0",
42
+ "marked": "^14.0.0",
43
+ "marked-terminal": "^7.0.0",
44
+ "mime-types": "^2.1.35",
45
+ "ollama": "^0.5.12",
46
+ "openai": "^4.82.0",
47
+ "ora": "^8.1.1",
48
+ "zod": "^3.24.1"
49
+ }
50
+ }
package/src/agent.js ADDED
@@ -0,0 +1,110 @@
1
+ import { tavily } from '@tavily/core';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { execSync } from 'child_process';
5
+ import { getConfig } from './config.js';
6
+
7
+ const config = getConfig();
8
+ const tvly = config.tavilyKey ? tavily({ apiKey: config.tavilyKey }) : null;
9
+
10
+ export const tools = [
11
+ {
12
+ name: 'search_internet',
13
+ description: 'Pesquisa informações atualizadas na internet sobre qualquer assunto.',
14
+ parameters: {
15
+ type: 'object',
16
+ properties: {
17
+ query: { type: 'string', description: 'O termo de busca' }
18
+ },
19
+ required: ['query']
20
+ },
21
+ execute: async ({ query }) => {
22
+ if (!tvly) return 'Erro: Chave de API da Tavily não configurada. Use /config para configurar.';
23
+ const searchResponse = await tvly.search(query, {
24
+ searchDepth: 'advanced',
25
+ maxResults: 5
26
+ });
27
+ return JSON.stringify(searchResponse.results.map(r => ({
28
+ title: r.title,
29
+ url: r.url,
30
+ content: r.content
31
+ })));
32
+ }
33
+ },
34
+ {
35
+ name: 'read_file',
36
+ description: 'Lê o conteúdo de um arquivo no sistema.',
37
+ parameters: {
38
+ type: 'object',
39
+ properties: {
40
+ path: { type: 'string', description: 'Caminho do arquivo' }
41
+ },
42
+ required: ['path']
43
+ },
44
+ execute: async ({ path: filePath }) => {
45
+ try {
46
+ return fs.readFileSync(filePath, 'utf-8');
47
+ } catch (err) {
48
+ return `Erro ao ler arquivo: ${err.message}`;
49
+ }
50
+ }
51
+ },
52
+ {
53
+ name: 'write_file',
54
+ description: 'Cria ou sobrescreve um arquivo com um conteúdo específico.',
55
+ parameters: {
56
+ type: 'object',
57
+ properties: {
58
+ path: { type: 'string', description: 'Camin de destino' },
59
+ content: { type: 'string', description: 'Conteúdo do arquivo' }
60
+ },
61
+ required: ['path', 'content']
62
+ },
63
+ execute: async ({ path: filePath, content }) => {
64
+ try {
65
+ const dir = path.dirname(filePath);
66
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
67
+ fs.writeFileSync(filePath, content);
68
+ return `Arquivo ${filePath} criado com sucesso.`;
69
+ } catch (err) {
70
+ return `Erro ao escrever arquivo: ${err.message}`;
71
+ }
72
+ }
73
+ },
74
+ {
75
+ name: 'run_command',
76
+ description: 'Executa um comando shell no sistema.',
77
+ parameters: {
78
+ type: 'object',
79
+ properties: {
80
+ command: { type: 'string', description: 'Comando shell a ser executado' }
81
+ },
82
+ required: ['command']
83
+ },
84
+ execute: async ({ command }) => {
85
+ try {
86
+ const output = execSync(command, { encoding: 'utf-8', timeout: 30000 });
87
+ return output || 'Comando executado sem retorno visual.';
88
+ } catch (err) {
89
+ return `Erro ao executar comando: ${err.stderr || err.message}`;
90
+ }
91
+ }
92
+ }
93
+ ];
94
+
95
+ export async function handleToolCalls(toolCalls) {
96
+ const results = [];
97
+ for (const call of toolCalls) {
98
+ const tool = tools.find(t => t.name === call.name);
99
+ if (tool) {
100
+ console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
101
+ const result = await tool.execute(call.args);
102
+ results.push({
103
+ callId: call.id,
104
+ name: call.name,
105
+ result
106
+ });
107
+ }
108
+ }
109
+ return results;
110
+ }
package/src/config.js ADDED
@@ -0,0 +1,151 @@
1
+ import Conf from 'conf';
2
+ import inquirer from 'inquirer';
3
+ import chalk from 'chalk';
4
+
5
+ const config = new Conf({ projectName: 'bimmo-cli' });
6
+
7
+ const providers = {
8
+ openai: { baseURL: 'https://api.openai.com/v1', defaultModel: 'gpt-4o' },
9
+ anthropic: { baseURL: 'https://api.anthropic.com/v1', defaultModel: 'claude-3-5-sonnet-20240620' },
10
+ grok: { baseURL: 'https://api.x.ai/v1', defaultModel: 'grok-2-1212' },
11
+ gemini: { baseURL: 'https://generativelanguage.googleapis.com/v1beta', defaultModel: 'gemini-2.0-flash' },
12
+ ollama: { baseURL: 'http://localhost:11434/api', defaultModel: 'llama3.2' },
13
+ openrouter: { baseURL: 'https://openrouter.ai/api/v1', defaultModel: 'google/gemini-2.0-flash-exp:free' },
14
+ deepseek: { baseURL: 'https://api.deepseek.com', defaultModel: 'deepseek-chat' },
15
+ zai: { baseURL: 'https://api.z.ai/v1', defaultModel: 'glm-4' }
16
+ };
17
+
18
+ export async function configure() {
19
+ console.log(chalk.cyan('🔧 Configuração do bimmo-cli\n'));
20
+
21
+ const profiles = config.get('profiles') || {};
22
+ const profileList = Object.keys(profiles);
23
+
24
+ const { action } = await inquirer.prompt([
25
+ {
26
+ type: 'list',
27
+ name: 'action',
28
+ message: 'O que deseja fazer?',
29
+ choices: [
30
+ { name: 'Criar novo perfil de IA', value: 'create' },
31
+ { name: 'Selecionar perfil ativo', value: 'select' },
32
+ { name: 'Configurar chave Tavily', value: 'tavily' },
33
+ { name: 'Sair', value: 'exit' }
34
+ ]
35
+ }
36
+ ]);
37
+
38
+ if (action === 'exit') return;
39
+
40
+ if (action === 'tavily') {
41
+ const { tavilyKey } = await inquirer.prompt([{
42
+ type: 'input',
43
+ name: 'tavilyKey',
44
+ message: 'Chave de API Tavily:',
45
+ default: config.get('tavilyKey')
46
+ }]);
47
+ config.set('tavilyKey', tavilyKey);
48
+ console.log(chalk.green('✓ Chave Tavily salva.'));
49
+ return;
50
+ }
51
+
52
+ if (action === 'select') {
53
+ if (profileList.length === 0) {
54
+ console.log(chalk.yellow('Nenhum perfil encontrado. Crie um primeiro.'));
55
+ return configure();
56
+ }
57
+ const { selected } = await inquirer.prompt([{
58
+ type: 'list',
59
+ name: 'selected',
60
+ message: 'Escolha o perfil para ativar:',
61
+ choices: profileList
62
+ }]);
63
+ const p = profiles[selected];
64
+ config.set('provider', p.provider);
65
+ config.set('apiKey', p.apiKey);
66
+ config.set('model', p.model);
67
+ config.set('baseURL', p.baseURL);
68
+ config.set('activeProfile', selected);
69
+ console.log(chalk.green(`✓ Perfil "${selected}" ativado!`));
70
+ return;
71
+ }
72
+
73
+ const answers = await inquirer.prompt([
74
+ {
75
+ type: 'input',
76
+ name: 'profileName',
77
+ message: 'Dê um nome para este perfil (ex: MeuGPT, ClaudeTrabalho):',
78
+ validate: i => i.length > 0 || 'Nome obrigatório'
79
+ },
80
+ {
81
+ type: 'list',
82
+ name: 'provider',
83
+ message: 'Qual provedor?',
84
+ choices: Object.keys(providers)
85
+ },
86
+ {
87
+ type: 'input',
88
+ name: 'apiKey',
89
+ message: 'API Key:',
90
+ validate: i => i.length > 5 || 'Chave inválida'
91
+ },
92
+ {
93
+ type: 'input',
94
+ name: 'model',
95
+ message: 'Modelo padrão (vazio para default):'
96
+ },
97
+ {
98
+ type: 'input',
99
+ name: 'customBaseURL',
100
+ message: 'URL customizada (vazio para default):'
101
+ }
102
+ ]);
103
+
104
+ const newProfile = {
105
+ provider: answers.provider,
106
+ apiKey: answers.apiKey,
107
+ model: answers.model || providers[answers.provider].defaultModel,
108
+ baseURL: answers.customBaseURL || providers[answers.provider].baseURL
109
+ };
110
+
111
+ profiles[answers.profileName] = newProfile;
112
+ config.set('profiles', profiles);
113
+
114
+ // Define como ativo imediatamente
115
+ config.set('provider', newProfile.provider);
116
+ config.set('apiKey', newProfile.apiKey);
117
+ config.set('model', newProfile.model);
118
+ config.set('baseURL', newProfile.baseURL);
119
+ config.set('activeProfile', answers.profileName);
120
+
121
+ console.log(chalk.green(`\n✅ Perfil "${answers.profileName}" criado e ativado!`));
122
+ }
123
+
124
+ export function getConfig() {
125
+ return config.store;
126
+ }
127
+
128
+ export function updateActiveModel(newModel) {
129
+ config.set('model', newModel);
130
+ // Também atualiza no perfil se houver um ativo
131
+ const active = config.get('activeProfile');
132
+ if (active) {
133
+ const profiles = config.get('profiles');
134
+ profiles[active].model = newModel;
135
+ config.set('profiles', profiles);
136
+ }
137
+ }
138
+
139
+ export function switchProfile(name) {
140
+ const profiles = config.get('profiles') || {};
141
+ if (profiles[name]) {
142
+ const p = profiles[name];
143
+ config.set('provider', p.provider);
144
+ config.set('apiKey', p.apiKey);
145
+ config.set('model', p.model);
146
+ config.set('baseURL', p.baseURL);
147
+ config.set('activeProfile', name);
148
+ return true;
149
+ }
150
+ return false;
151
+ }
@@ -0,0 +1,271 @@
1
+ import chalk from 'chalk';
2
+ import figlet from 'figlet';
3
+ import inquirer from 'inquirer';
4
+ import { marked } from 'marked';
5
+ import TerminalRenderer from 'marked-terminal';
6
+ import ora from 'ora';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import mime from 'mime-types';
10
+
11
+ import { getConfig, configure, updateActiveModel, switchProfile } from './config.js';
12
+ import { createProvider } from './providers/factory.js';
13
+ import { getProjectContext } from './project-context.js';
14
+
15
+ marked.use(new TerminalRenderer({
16
+ heading: chalk.hex('#c084fc').bold,
17
+ code: chalk.hex('#00ff9d'),
18
+ }));
19
+
20
+ const green = chalk.hex('#00ff9d');
21
+ const lavender = chalk.hex('#c084fc');
22
+ const gray = chalk.gray;
23
+ const bold = chalk.bold;
24
+ const yellow = chalk.yellow;
25
+
26
+ let currentMode = 'chat'; // 'chat', 'plan', 'edit'
27
+
28
+ async function processInput(input) {
29
+ const parts = input.split(' ');
30
+ const processedContent = [];
31
+
32
+ for (const part of parts) {
33
+ if (part.startsWith('@')) {
34
+ const filePath = part.slice(1);
35
+ try {
36
+ if (fs.existsSync(filePath)) {
37
+ const stats = fs.statSync(filePath);
38
+ if (stats.isFile()) {
39
+ const mimeType = mime.lookup(filePath) || 'application/octet-stream';
40
+
41
+ if (mimeType.startsWith('image/')) {
42
+ const base64Image = fs.readFileSync(filePath, { encoding: 'base64' });
43
+ processedContent.push({
44
+ type: 'image',
45
+ mimeType,
46
+ data: base64Image,
47
+ fileName: path.basename(filePath)
48
+ });
49
+ } else {
50
+ const textContent = fs.readFileSync(filePath, 'utf-8');
51
+ processedContent.push({
52
+ type: 'text',
53
+ text: `\n--- Arquivo: ${path.basename(filePath)} ---\n${textContent}\n--- Fim do arquivo ---\n`
54
+ });
55
+ }
56
+ }
57
+ } else {
58
+ processedContent.push({ type: 'text', text: part });
59
+ }
60
+ } catch (err) {
61
+ processedContent.push({ type: 'text', text: part });
62
+ }
63
+ } else {
64
+ processedContent.push({ type: 'text', text: part });
65
+ }
66
+ }
67
+
68
+ const hasImage = processedContent.some(c => c.type === 'image');
69
+
70
+ if (!hasImage) {
71
+ return processedContent.map(c => c.text).join(' ');
72
+ }
73
+
74
+ const finalContent = [];
75
+ let currentText = "";
76
+
77
+ for (const item of processedContent) {
78
+ if (item.type === 'text') {
79
+ currentText += (currentText ? " " : "") + item.text;
80
+ } else {
81
+ if (currentText) {
82
+ finalContent.push({ type: 'text', text: currentText });
83
+ currentText = "";
84
+ }
85
+ finalContent.push(item);
86
+ }
87
+ }
88
+ if (currentText) finalContent.push({ type: 'text', text: currentText });
89
+
90
+ return finalContent;
91
+ }
92
+
93
+ function getModeStyle() {
94
+ switch (currentMode) {
95
+ case 'plan': return yellow.bold(' [PLAN] ');
96
+ case 'edit': return chalk.red.bold(' [EDIT] ');
97
+ default: return lavender.bold(' [CHAT] ');
98
+ }
99
+ }
100
+
101
+ export async function startInteractive() {
102
+ let config = getConfig();
103
+
104
+ if (!config.provider || !config.apiKey) {
105
+ console.log(lavender(figlet.textSync('bimmo', { font: 'slant' })));
106
+ console.log(gray('\nBem-vindo! Vamos configurar seus perfis de IA.\n'));
107
+ await configure();
108
+ return startInteractive();
109
+ }
110
+
111
+ let provider = createProvider(config);
112
+ const messages = [];
113
+
114
+ // 1. Injeta o sistema de contexto inteligente do projeto
115
+ const projectContext = getProjectContext();
116
+ messages.push({
117
+ role: 'system',
118
+ content: projectContext
119
+ });
120
+
121
+ console.clear();
122
+ console.log(lavender(figlet.textSync('bimmo', { font: 'small' })));
123
+ console.log(lavender('─'.repeat(60)));
124
+ console.log(green(` Perfil Ativo: ${bold(config.activeProfile || 'Padrão')} (${config.provider.toUpperCase()})`));
125
+ console.log(green(` Modelo: ${bold(config.model)}`));
126
+ console.log(gray(' /chat | /plan | /edit | /switch [perfil] | /model [novo] | /help'));
127
+ console.log(lavender('─'.repeat(60)) + '\n');
128
+
129
+ console.log(lavender('👋 Olá! Sou seu agente BIMMO. No que posso atuar?\n'));
130
+
131
+ while (true) {
132
+ const modeIndicator = getModeStyle();
133
+ const { input } = await inquirer.prompt([
134
+ {
135
+ type: 'input',
136
+ name: 'input',
137
+ message: modeIndicator + green('Você'),
138
+ prefix: '→',
139
+ }
140
+ ]);
141
+
142
+ const rawInput = input.trim();
143
+ const cmd = rawInput.toLowerCase();
144
+
145
+ if (cmd === '/exit' || cmd === 'exit' || cmd === 'sair') {
146
+ console.log(lavender('\n👋 BIMMO encerrando sessão. Até logo!\n'));
147
+ break;
148
+ }
149
+
150
+ if (cmd === '/chat') { currentMode = 'chat'; console.log(lavender('✓ Modo CHAT.\n')); continue; }
151
+ if (cmd === '/plan') { currentMode = 'plan'; console.log(yellow('✓ Modo PLAN.\n')); continue; }
152
+ if (cmd === '/edit') { currentMode = 'edit'; console.log(chalk.red('⚠️ Modo EDIT.\n')); continue; }
153
+
154
+ // /switch [perfil] -> Troca Perfil + Chave + Provedor + Modelo instantaneamente
155
+ if (cmd.startsWith('/switch ')) {
156
+ const profileName = rawInput.split(' ')[1];
157
+ if (profileName && switchProfile(profileName)) {
158
+ config = getConfig(); // Atualiza config local
159
+ provider = createProvider(config); // Recria provedor com nova chave/url
160
+ console.log(green(`\n✓ Trocado para o perfil "${bold(profileName)}"!`));
161
+ console.log(gray(` IA: ${config.provider.toUpperCase()} | Modelo: ${config.model}\n`));
162
+ } else {
163
+ console.log(chalk.red(`\n✖ Perfil "${profileName}" não encontrado.\n`));
164
+ }
165
+ continue;
166
+ }
167
+
168
+ // /model [modelo] -> Troca apenas o modelo do Perfil atual
169
+ if (cmd.startsWith('/model ')) {
170
+ const newModel = rawInput.split(' ')[1];
171
+ if (newModel) {
172
+ updateActiveModel(newModel);
173
+ config.model = newModel;
174
+ provider = createProvider(config);
175
+ console.log(green(`\n✓ Modelo atualizado para: ${bold(newModel)}\n`));
176
+ }
177
+ continue;
178
+ }
179
+
180
+ if (cmd === '/clear') {
181
+ messages.length = 0;
182
+ messages.push({ role: 'system', content: getProjectContext() });
183
+ console.clear();
184
+ console.log(lavender('✓ Histórico limpo, contexto preservado.\n'));
185
+ continue;
186
+ }
187
+
188
+ if (cmd === '/help') {
189
+ console.log(gray(`
190
+ Comandos Disponíveis:
191
+ /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)
197
+ @caminho → Anexar arquivos ou imagens
198
+ `));
199
+ continue;
200
+ }
201
+
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 já existe. Deseja sobrescrever?',
209
+ default: false
210
+ }]);
211
+ if (!overwrite) continue;
212
+ }
213
+
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
+ });
234
+ continue;
235
+ }
236
+
237
+ if (cmd === '/config') { await configure(); config = getConfig(); provider = createProvider(config); continue; }
238
+
239
+ if (rawInput === '') continue;
240
+
241
+ // Injeção dinâmica de instruções de modo
242
+ let modeInstr = "";
243
+ if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Descreva e analise, mas NÃO altere arquivos.";
244
+ else if (currentMode === 'edit') modeInstr = "\n[MODO EDIT] Você tem permissão para usar write_file e run_command AGORA.";
245
+
246
+ const content = await processInput(rawInput);
247
+ messages.push({
248
+ role: 'user',
249
+ content: typeof content === 'string' ? content + modeInstr : [...content, { type: 'text', text: modeInstr }]
250
+ });
251
+
252
+ const spinner = ora({
253
+ text: lavender(`bimmo (${currentMode}) pensando...`),
254
+ color: currentMode === 'edit' ? 'red' : 'magenta'
255
+ }).start();
256
+
257
+ try {
258
+ const responseText = await provider.sendMessage(messages);
259
+ spinner.stop();
260
+ messages.push({ role: 'assistant', content: responseText });
261
+
262
+ console.log('\n' + lavender('bimmo') + getModeStyle());
263
+ console.log(lavender('─'.repeat(50)));
264
+ console.log(marked(responseText));
265
+ console.log(gray('─'.repeat(50)) + '\n');
266
+ } catch (err) {
267
+ spinner.stop();
268
+ console.error(chalk.red('✖ Erro Crítico:') + ' ' + err.message + '\n');
269
+ }
270
+ }
271
+ }
@@ -0,0 +1,40 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { execSync } from 'child_process';
4
+
5
+ /**
6
+ * Coleta o contexto completo do projeto para a IA.
7
+ * Inclui estrutura de arquivos, arquivos de instrução e configurações do .bimmorc
8
+ */
9
+ export function getProjectContext() {
10
+ const cwd = process.cwd();
11
+ let context = "=== CONTEXTO DO PROJETO ===\n";
12
+
13
+ // 1. Tentar ler .bimmorc.json para regras customizadas
14
+ const bimmoRcPath = path.join(cwd, '.bimmorc.json');
15
+ if (fs.existsSync(bimmoRcPath)) {
16
+ try {
17
+ const rc = JSON.parse(fs.readFileSync(bimmoRcPath, 'utf-8'));
18
+ context += `Regras de Projeto (.bimmorc):\n${JSON.stringify(rc, null, 2)}\n\n`;
19
+ } catch (e) {}
20
+ }
21
+
22
+ // 2. Tentar ler arquivos de instruções comuns (Claude, Gemini, etc)
23
+ const instructionFiles = ['CLAUDE.md', 'INSTRUCTIONS.md', '.bimmo-context.md', 'CONTRIBUTING.md'];
24
+ for (const file of instructionFiles) {
25
+ const p = path.join(cwd, file);
26
+ if (fs.existsSync(p)) {
27
+ context += `Instruções de ${file}:\n${fs.readFileSync(p, 'utf-8')}\n\n`;
28
+ }
29
+ }
30
+
31
+ // 3. Adicionar estrutura de diretórios (Quantizada/Resumida)
32
+ try {
33
+ const tree = execSync('find . -maxdepth 2 -not -path "*/.*" -not -path "./node_modules*"', { encoding: 'utf-8' });
34
+ context += `Estrutura de Arquivos (Resumo):\n${tree}\n`;
35
+ } catch (e) {
36
+ context += "Estrutura de arquivos indisponível.\n";
37
+ }
38
+
39
+ return context;
40
+ }
@@ -0,0 +1,77 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { BaseProvider } from './base.js';
3
+ import { tools } from '../agent.js';
4
+
5
+ export class AnthropicProvider extends BaseProvider {
6
+ constructor(config) {
7
+ super(config);
8
+ this.client = new Anthropic({
9
+ apiKey: this.config.apiKey
10
+ });
11
+ }
12
+
13
+ formatContent(content) {
14
+ if (typeof content === 'string') return content;
15
+ return content.map(part => {
16
+ if (part.type === 'text') return { type: 'text', text: part.text };
17
+ if (part.type === 'image') return {
18
+ type: 'image',
19
+ source: { type: 'base64', media_type: part.mimeType, data: part.data }
20
+ };
21
+ });
22
+ }
23
+
24
+ async sendMessage(messages) {
25
+ const systemMessage = messages.find(m => m.role === 'system');
26
+ const userMessages = messages
27
+ .filter(m => m.role !== 'system')
28
+ .map(m => ({
29
+ role: m.role,
30
+ content: this.formatContent(m.content)
31
+ }));
32
+
33
+ // Converte tools do agent.js para o formato da Anthropic
34
+ const anthropicTools = tools.map(t => ({
35
+ name: t.name,
36
+ description: t.description,
37
+ input_schema: t.parameters
38
+ }));
39
+
40
+ const response = await this.client.messages.create({
41
+ model: this.config.model,
42
+ max_tokens: 4096,
43
+ system: systemMessage ? systemMessage.content : undefined,
44
+ messages: userMessages,
45
+ tools: anthropicTools,
46
+ temperature: 0.7
47
+ });
48
+
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) {
54
+ console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
55
+ const result = await tool.execute(toolUse.input);
56
+
57
+ // Adiciona a resposta da IA e o resultado da tool ao histórico
58
+ const nextMessages = [
59
+ ...messages,
60
+ { role: 'assistant', content: response.content },
61
+ {
62
+ role: 'user',
63
+ content: [{
64
+ type: 'tool_result',
65
+ tool_use_id: toolUse.id,
66
+ content: String(result)
67
+ }]
68
+ }
69
+ ];
70
+
71
+ return this.sendMessage(nextMessages);
72
+ }
73
+ }
74
+
75
+ return response.content[0].text;
76
+ }
77
+ }
@@ -0,0 +1,9 @@
1
+ export class BaseProvider {
2
+ constructor(config) {
3
+ this.config = config;
4
+ }
5
+
6
+ async sendMessage(messages) {
7
+ throw new Error('Método sendMessage deve ser implementado');
8
+ }
9
+ }
@@ -0,0 +1,20 @@
1
+ import { OpenAIProvider } from './openai.js';
2
+ import { AnthropicProvider } from './anthropic.js';
3
+ import { GrokProvider } from './grok.js';
4
+ import { GeminiProvider } from './gemini.js';
5
+ import { OllamaProvider } from './ollama.js';
6
+
7
+ export function createProvider(config) {
8
+ switch (config.provider) {
9
+ case 'openai': return new OpenAIProvider(config);
10
+ case 'anthropic': return new AnthropicProvider(config);
11
+ case 'grok': return new GrokProvider(config);
12
+ case 'gemini': return new GeminiProvider(config);
13
+ case 'ollama': return new OllamaProvider(config);
14
+ case 'openrouter': return new OpenAIProvider(config);
15
+ case 'deepseek': return new OpenAIProvider(config);
16
+ case 'zai': return new OpenAIProvider(config);
17
+ default:
18
+ throw new Error(`Provider ${config.provider} não suportado ainda.`);
19
+ }
20
+ }
@@ -0,0 +1,77 @@
1
+ import { GoogleGenerativeAI } from '@google/generative-ai';
2
+ import { BaseProvider } from './base.js';
3
+ import { tools } from '../agent.js';
4
+
5
+ export class GeminiProvider extends BaseProvider {
6
+ constructor(config) {
7
+ super(config);
8
+ this.genAI = new GoogleGenerativeAI(this.config.apiKey);
9
+ }
10
+
11
+ formatContent(content) {
12
+ if (typeof content === 'string') return [{ text: content }];
13
+ return content.map(part => {
14
+ if (part.type === 'text') return { text: part.text };
15
+ if (part.type === 'image') return {
16
+ inlineData: { mimeType: part.mimeType, data: part.data }
17
+ };
18
+ });
19
+ }
20
+
21
+ async sendMessage(messages) {
22
+ const systemPrompt = messages.find(m => m.role === 'system')?.content;
23
+ const history = messages
24
+ .filter(m => m.role !== 'system')
25
+ .slice(0, -1)
26
+ .map(msg => ({
27
+ role: msg.role === 'user' ? 'user' : 'model',
28
+ parts: this.formatContent(msg.content)
29
+ }));
30
+
31
+ const lastMessageContent = this.formatContent(messages[messages.length - 1].content);
32
+
33
+ // Converte tools do agent.js para o formato do Gemini
34
+ const geminiTools = tools.map(t => ({
35
+ functionDeclarations: [{
36
+ name: t.name,
37
+ description: t.description,
38
+ parameters: t.parameters
39
+ }]
40
+ }));
41
+
42
+ const model = this.genAI.getGenerativeModel({
43
+ model: this.config.model,
44
+ systemInstruction: systemPrompt,
45
+ tools: geminiTools
46
+ });
47
+
48
+ const chat = model.startChat({
49
+ history: history,
50
+ generationConfig: { temperature: 0.7, maxOutputTokens: 8192 },
51
+ });
52
+
53
+ const result = await chat.sendMessage(lastMessageContent);
54
+ const response = await result.response;
55
+
56
+ // Processamento de Tool Calls no Gemini
57
+ const call = response.candidates[0].content.parts.find(p => p.functionCall);
58
+ if (call) {
59
+ const tool = tools.find(t => t.name === call.functionCall.name);
60
+ if (tool) {
61
+ console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
62
+ const result = await tool.execute(call.functionCall.args);
63
+
64
+ // No Gemini, enviamos o resultado de volta para o chat
65
+ const resultResponse = await chat.sendMessage([{
66
+ functionResponse: {
67
+ name: call.functionCall.name,
68
+ response: { content: result }
69
+ }
70
+ }]);
71
+ return resultResponse.response.text();
72
+ }
73
+ }
74
+
75
+ return response.text();
76
+ }
77
+ }
@@ -0,0 +1,47 @@
1
+ import OpenAI from 'openai';
2
+ import { BaseProvider } from './base.js';
3
+
4
+ export class GrokProvider extends BaseProvider {
5
+ constructor(config) {
6
+ super(config);
7
+ this.client = new OpenAI({
8
+ apiKey: this.config.apiKey,
9
+ baseURL: this.config.baseURL || 'https://api.x.ai/v1'
10
+ });
11
+ }
12
+
13
+ formatMessages(messages) {
14
+ return messages.map(msg => {
15
+ if (typeof msg.content === 'string') {
16
+ return msg;
17
+ }
18
+
19
+ const content = msg.content.map(part => {
20
+ if (part.type === 'text') {
21
+ return { type: 'text', text: part.text };
22
+ } else if (part.type === 'image') {
23
+ return {
24
+ type: 'image_url',
25
+ image_url: {
26
+ url: `data:${part.mimeType};base64,${part.data}`
27
+ }
28
+ };
29
+ }
30
+ });
31
+
32
+ return { ...msg, content };
33
+ });
34
+ }
35
+
36
+ async sendMessage(messages) {
37
+ const formattedMessages = this.formatMessages(messages);
38
+
39
+ const response = await this.client.chat.completions.create({
40
+ model: this.config.model,
41
+ messages: formattedMessages,
42
+ temperature: 0.7
43
+ });
44
+
45
+ return response.choices[0].message.content;
46
+ }
47
+ }
@@ -0,0 +1,51 @@
1
+ import ollama from 'ollama';
2
+ import { BaseProvider } from './base.js';
3
+
4
+ export class OllamaProvider extends BaseProvider {
5
+ constructor(config) {
6
+ super(config);
7
+ this.client = ollama;
8
+ }
9
+
10
+ formatMessages(messages) {
11
+ return messages.map(msg => {
12
+ if (typeof msg.content === 'string') return msg;
13
+
14
+ const images = msg.content
15
+ .filter(part => part.type === 'image')
16
+ .map(part => part.data); // Ollama espera apenas a string base64
17
+
18
+ const text = msg.content
19
+ .filter(part => part.type === 'text')
20
+ .map(part => part.text)
21
+ .join(' ');
22
+
23
+ return {
24
+ role: msg.role,
25
+ content: text,
26
+ images: images.length > 0 ? images : undefined
27
+ };
28
+ });
29
+ }
30
+
31
+ async sendMessage(messages) {
32
+ const formattedMessages = this.formatMessages(messages);
33
+
34
+ const response = await this.client.chat({
35
+ model: this.config.model,
36
+ messages: formattedMessages,
37
+ stream: false,
38
+ options: {
39
+ temperature: 0.7,
40
+ }
41
+ });
42
+
43
+ const text = response.message?.content;
44
+
45
+ if (!text) {
46
+ throw new Error('Resposta inválida do Ollama');
47
+ }
48
+
49
+ return text;
50
+ }
51
+ }
@@ -0,0 +1,77 @@
1
+ import OpenAI from 'openai';
2
+ import { BaseProvider } from './base.js';
3
+ import { tools } from '../agent.js';
4
+
5
+ export class OpenAIProvider extends BaseProvider {
6
+ constructor(config) {
7
+ super(config);
8
+ this.client = new OpenAI({
9
+ apiKey: this.config.apiKey,
10
+ baseURL: this.config.baseURL
11
+ });
12
+ }
13
+
14
+ formatMessages(messages) {
15
+ return messages.map(msg => {
16
+ if (typeof msg.content === 'string') return msg;
17
+
18
+ const content = msg.content.map(part => {
19
+ if (part.type === 'text') return { type: 'text', text: part.text };
20
+ if (part.type === 'image') return {
21
+ type: 'image_url',
22
+ image_url: { url: `data:${part.mimeType};base64,${part.data}` }
23
+ };
24
+ });
25
+ return { ...msg, content };
26
+ });
27
+ }
28
+
29
+ async sendMessage(messages) {
30
+ const formattedMessages = this.formatMessages(messages);
31
+
32
+ // Converte tools do agent.js para o formato da OpenAI
33
+ const openAiTools = tools.map(t => ({
34
+ type: 'function',
35
+ function: {
36
+ name: t.name,
37
+ description: t.description,
38
+ parameters: t.parameters
39
+ }
40
+ }));
41
+
42
+ const response = await this.client.chat.completions.create({
43
+ model: this.config.model,
44
+ messages: formattedMessages,
45
+ tools: openAiTools,
46
+ tool_choice: 'auto'
47
+ });
48
+
49
+ const message = response.choices[0].message;
50
+
51
+ if (message.tool_calls) {
52
+ const toolResults = [];
53
+ for (const toolCall of message.tool_calls) {
54
+ const tool = tools.find(t => t.name === toolCall.function.name);
55
+ if (tool) {
56
+ console.log(`\n ${tool.name === 'search_internet' ? '🌐' : '🛠️'} Executando: ${tool.name}...`);
57
+ const args = JSON.parse(toolCall.function.arguments);
58
+ const result = await tool.execute(args);
59
+
60
+ toolResults.push({
61
+ role: 'tool',
62
+ tool_call_id: toolCall.id,
63
+ content: String(result)
64
+ });
65
+ }
66
+ }
67
+
68
+ // Adiciona a chamada da tool e o resultado ao histórico
69
+ const nextMessages = [...formattedMessages, message, ...toolResults];
70
+
71
+ // Chamada recursiva para processar a resposta final da IA com o resultado da tool
72
+ return this.sendMessage(nextMessages);
73
+ }
74
+
75
+ return message.content;
76
+ }
77
+ }