bimmo-cli 2.2.11 → 3.0.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/bin/bimmo +8 -1
- package/package.json +12 -3
- package/src/interface.js +300 -207
package/bin/bimmo
CHANGED
|
@@ -6,11 +6,18 @@ process.removeAllListeners('warning');
|
|
|
6
6
|
import { program } from 'commander';
|
|
7
7
|
import { startInteractive } from '../src/interface.js';
|
|
8
8
|
import { configure } from '../src/config.js';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
|
|
9
16
|
|
|
10
17
|
program
|
|
11
18
|
.name('bimmo')
|
|
12
19
|
.description('bimmo — Sua IA universal no terminal (verde & lavanda)')
|
|
13
|
-
.version(
|
|
20
|
+
.version(pkg.version);
|
|
14
21
|
|
|
15
22
|
program
|
|
16
23
|
.command('config')
|
package/package.json
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bimmo-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "🌿 Plataforma de IA universal profissional com
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "🌿 Plataforma de IA universal profissional com interface React (Ink). Agentes, Swarms, Autocomplete real-time e Contexto Inteligente.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"bimmo": "bin/bimmo"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"keywords": [
|
|
10
|
-
"ai", "cli", "
|
|
10
|
+
"ai", "cli", "ink", "react", "openai", "anthropic", "gemini", "agent", "swarm", "terminal"
|
|
11
11
|
],
|
|
12
12
|
"author": "Judah",
|
|
13
13
|
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/JudahAragao/bimmo-cli.git"
|
|
17
|
+
},
|
|
14
18
|
"files": [
|
|
15
19
|
"bin/",
|
|
16
20
|
"src/",
|
|
@@ -30,6 +34,10 @@
|
|
|
30
34
|
"diff": "^7.0.0",
|
|
31
35
|
"figlet": "^1.7.0",
|
|
32
36
|
"fuzzy": "^0.1.3",
|
|
37
|
+
"ink": "^5.1.0",
|
|
38
|
+
"ink-divider": "^3.0.0",
|
|
39
|
+
"ink-spinner": "^5.0.0",
|
|
40
|
+
"ink-text-input": "^6.0.0",
|
|
33
41
|
"inquirer": "^9.3.8",
|
|
34
42
|
"marked": "^14.0.0",
|
|
35
43
|
"marked-terminal": "^7.0.0",
|
|
@@ -37,6 +45,7 @@
|
|
|
37
45
|
"ollama": "^0.5.12",
|
|
38
46
|
"openai": "^4.82.0",
|
|
39
47
|
"ora": "^8.1.1",
|
|
48
|
+
"react": "^18.2.0",
|
|
40
49
|
"zod": "^3.24.1"
|
|
41
50
|
}
|
|
42
51
|
}
|
package/src/interface.js
CHANGED
|
@@ -1,26 +1,22 @@
|
|
|
1
|
+
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { render, Box, Text, useInput, useApp } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
1
5
|
import chalk from 'chalk';
|
|
2
6
|
import figlet from 'figlet';
|
|
3
7
|
import { marked } from 'marked';
|
|
4
8
|
import TerminalRenderer from 'marked-terminal';
|
|
5
|
-
import ora from 'ora';
|
|
6
9
|
import fs from 'fs';
|
|
7
10
|
import path from 'path';
|
|
8
|
-
import mime from 'mime-types';
|
|
9
|
-
import readline from 'readline';
|
|
10
11
|
import { fileURLToPath } from 'url';
|
|
11
|
-
import { stripVTControlCharacters } from 'util';
|
|
12
12
|
|
|
13
|
-
import { getConfig,
|
|
13
|
+
import { getConfig, updateActiveModel, switchProfile } from './config.js';
|
|
14
14
|
import { createProvider } from './providers/factory.js';
|
|
15
15
|
import { getProjectContext } from './project-context.js';
|
|
16
|
-
import { SwarmOrchestrator } from './orchestrator.js';
|
|
17
16
|
import { editState } from './agent.js';
|
|
17
|
+
import { SwarmOrchestrator } from './orchestrator.js';
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
const __dirname = path.dirname(__filename);
|
|
21
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
|
|
22
|
-
const version = pkg.version;
|
|
23
|
-
|
|
19
|
+
// Configuração do renderizador Markdown para o terminal
|
|
24
20
|
marked.use(new TerminalRenderer({
|
|
25
21
|
heading: chalk.hex('#c084fc').bold,
|
|
26
22
|
code: chalk.hex('#00ff9d'),
|
|
@@ -29,239 +25,336 @@ marked.use(new TerminalRenderer({
|
|
|
29
25
|
html: () => '',
|
|
30
26
|
}));
|
|
31
27
|
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const yellow = chalk.yellow;
|
|
28
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
29
|
+
const __dirname = path.dirname(__filename);
|
|
30
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
|
|
31
|
+
const version = pkg.version;
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
const green = '#00ff9d';
|
|
34
|
+
const lavender = '#c084fc';
|
|
35
|
+
const gray = '#6272a4';
|
|
36
|
+
const yellow = '#f1fa8c';
|
|
37
|
+
const red = '#ff5555';
|
|
38
|
+
const cyan = '#8be9fd';
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
const BimmoApp = ({ initialConfig }) => {
|
|
41
|
+
const { exit } = useApp();
|
|
42
|
+
const [config, setConfig] = useState(initialConfig);
|
|
43
|
+
const [mode, setMode] = useState('chat');
|
|
44
|
+
const [activePersona, setActivePersona] = useState(null);
|
|
45
|
+
const [messages, setMessages] = useState([]);
|
|
46
|
+
const [input, setInput] = useState('');
|
|
47
|
+
const [isThinking, setIsThinking] = useState(false);
|
|
48
|
+
const [thinkingMessage, setThinkingMessage] = useState('bimmo pensando...');
|
|
49
|
+
const [exitCounter, setExitCounter] = useState(0);
|
|
50
|
+
const [provider, setProvider] = useState(() => createProvider(initialConfig));
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
// Inicializa com o contexto do projeto
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const ctx = getProjectContext();
|
|
55
|
+
setMessages([{ role: 'system', content: ctx }]);
|
|
56
|
+
|
|
57
|
+
// Welcome message
|
|
58
|
+
setMessages(prev => [...prev, {
|
|
59
|
+
role: 'assistant',
|
|
60
|
+
content: `Olá! Sou o **bimmo v${version}**. Como posso ajudar hoje?\n\nDigite \`/help\` para ver os comandos disponíveis.`
|
|
61
|
+
}]);
|
|
62
|
+
}, []);
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
// Lógica de Autocomplete em tempo real para @arquivos
|
|
65
|
+
const filePreview = useMemo(() => {
|
|
66
|
+
if (!input.includes('@')) return [];
|
|
67
|
+
const words = input.split(' ');
|
|
68
|
+
const lastWord = words[words.length - 1];
|
|
69
|
+
if (!lastWord.startsWith('@')) return [];
|
|
60
70
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return { name: rel, isDir };
|
|
68
|
-
});
|
|
69
|
-
} catch (e) { return []; }
|
|
70
|
-
}
|
|
71
|
+
try {
|
|
72
|
+
const p = lastWord.slice(1);
|
|
73
|
+
const dir = p.includes('/') ? p.substring(0, p.lastIndexOf('/')) : '.';
|
|
74
|
+
const filter = p.includes('/') ? p.substring(p.lastIndexOf('/') + 1) : p;
|
|
75
|
+
const absDir = path.resolve(process.cwd(), dir);
|
|
76
|
+
if (!fs.existsSync(absDir)) return [];
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
78
|
+
return fs.readdirSync(absDir)
|
|
79
|
+
.filter(f => f.startsWith(filter) && !f.startsWith('.') && f !== 'node_modules')
|
|
80
|
+
.slice(0, 5)
|
|
81
|
+
.map(f => ({
|
|
82
|
+
name: f,
|
|
83
|
+
isDir: fs.statSync(path.join(absDir, f)).isDirectory(),
|
|
84
|
+
rel: path.join(dir === '.' ? '' : dir, f)
|
|
85
|
+
}));
|
|
86
|
+
} catch (e) { return []; }
|
|
87
|
+
}, [input]);
|
|
81
88
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
await configure(); return startInteractive();
|
|
87
|
-
}
|
|
89
|
+
const handleSubmit = async (val) => {
|
|
90
|
+
const rawInput = val.trim();
|
|
91
|
+
if (!rawInput) return;
|
|
92
|
+
setInput('');
|
|
88
93
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
const lowerInput = rawInput.toLowerCase();
|
|
95
|
+
const parts = rawInput.split(' ');
|
|
96
|
+
const cmd = parts[0].toLowerCase();
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
// COMANDOS INTERNOS
|
|
99
|
+
if (cmd === '/exit' || cmd === 'sair') exit();
|
|
100
|
+
|
|
101
|
+
if (cmd === '/clear') {
|
|
102
|
+
const ctx = getProjectContext();
|
|
103
|
+
setMessages([{ role: 'system', content: ctx }, { role: 'assistant', content: 'Chat limpo.' }]);
|
|
104
|
+
return;
|
|
98
105
|
}
|
|
99
|
-
};
|
|
100
|
-
resetMessages();
|
|
101
106
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
if (cmd === '/chat') { setMode('chat'); return; }
|
|
108
|
+
if (cmd === '/plan') { setMode('plan'); return; }
|
|
109
|
+
if (cmd === '/edit') {
|
|
110
|
+
setMode('edit');
|
|
111
|
+
editState.autoAccept = parts[1] === 'auto';
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
108
114
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const hits = getFilesForPreview(lastWord).map(h => `@${h.name}`);
|
|
118
|
-
return [hits, lastWord];
|
|
115
|
+
if (cmd === '/model') {
|
|
116
|
+
const newModel = parts[1];
|
|
117
|
+
if (newModel) {
|
|
118
|
+
updateActiveModel(newModel);
|
|
119
|
+
const newCfg = getConfig();
|
|
120
|
+
setConfig(newCfg);
|
|
121
|
+
setProvider(createProvider(newCfg));
|
|
122
|
+
setMessages(prev => [...prev, { role: 'system', content: `Modelo alterado para: ${newModel}` }]);
|
|
119
123
|
}
|
|
120
|
-
return
|
|
124
|
+
return;
|
|
121
125
|
}
|
|
122
|
-
});
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
if (cmd === '/switch') {
|
|
128
|
+
const profile = parts[1];
|
|
129
|
+
if (switchProfile(profile)) {
|
|
130
|
+
const newCfg = getConfig();
|
|
131
|
+
setConfig(newCfg);
|
|
132
|
+
setProvider(createProvider(newCfg));
|
|
133
|
+
setMessages(prev => [...prev, { role: 'system', content: `Perfil alterado para: ${profile}` }]);
|
|
134
|
+
} else {
|
|
135
|
+
setMessages(prev => [...prev, { role: 'system', content: `Perfil "${profile}" não encontrado.` }]);
|
|
133
136
|
}
|
|
134
|
-
|
|
135
|
-
readline.moveCursor(process.stdout, 0, -(currentPreviewLines + 1));
|
|
136
|
-
currentPreviewLines = 0;
|
|
137
|
+
return;
|
|
137
138
|
}
|
|
138
|
-
};
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// Retorna o cursor para a posição exata de escrita
|
|
158
|
-
readline.moveCursor(process.stdout, 0, -(currentPreviewLines + 1));
|
|
159
|
-
const promptWidth = stripVTControlCharacters(rl.getPrompt()).length;
|
|
160
|
-
readline.cursorTo(process.stdout, (promptWidth + rl.line.length) % (process.stdout.columns || 80));
|
|
140
|
+
if (cmd === '/use') {
|
|
141
|
+
const agentName = parts[1];
|
|
142
|
+
const agents = config.agents || {};
|
|
143
|
+
if (agentName === 'normal') {
|
|
144
|
+
setActivePersona(null);
|
|
145
|
+
setMessages(prev => [...prev, { role: 'system', content: 'Modo normal ativado.' }]);
|
|
146
|
+
} else if (agents[agentName]) {
|
|
147
|
+
const agent = agents[agentName];
|
|
148
|
+
setActivePersona(agentName);
|
|
149
|
+
setMode(agent.mode || 'chat');
|
|
150
|
+
if (agent.profile && agent.profile !== config.activeProfile) {
|
|
151
|
+
switchProfile(agent.profile);
|
|
152
|
+
const newCfg = getConfig();
|
|
153
|
+
setConfig(newCfg);
|
|
154
|
+
setProvider(createProvider(newCfg));
|
|
155
|
+
}
|
|
156
|
+
setMessages(prev => [...prev, { role: 'system', content: `Agente "${agentName}" ativo.` }]);
|
|
161
157
|
} else {
|
|
162
|
-
|
|
158
|
+
setMessages(prev => [...prev, { role: 'system', content: `Agente "${agentName}" não encontrado.` }]);
|
|
163
159
|
}
|
|
164
|
-
|
|
165
|
-
clearPreview();
|
|
160
|
+
return;
|
|
166
161
|
}
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const displayPrompt = () => {
|
|
170
|
-
const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]` : '';
|
|
171
|
-
let modeLabel = `[${currentMode.toUpperCase()}]`;
|
|
172
|
-
if (currentMode === 'edit') modeLabel = editState.autoAccept ? '[EDIT(AUTO)]' : '[EDIT(MANUAL)]';
|
|
173
|
-
|
|
174
|
-
console.log(`${gray(`📁 ${process.cwd()}`)}`);
|
|
175
|
-
rl.setPrompt(lavender.bold(personaLabel) + (currentMode === 'edit' ? chalk.red.bold(modeLabel) : lavender.bold(modeLabel)) + green(' > '));
|
|
176
|
-
rl.prompt();
|
|
177
|
-
};
|
|
178
162
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
163
|
+
if (cmd === '/swarm') {
|
|
164
|
+
const swarmType = parts[1];
|
|
165
|
+
const orchestrator = new SwarmOrchestrator(config);
|
|
166
|
+
setIsThinking(true);
|
|
167
|
+
setThinkingMessage('Enxame em ação...');
|
|
184
168
|
|
|
185
|
-
const cmd = rawInput.toLowerCase();
|
|
186
|
-
if (cmd === '/exit' || cmd === 'sair') process.exit(0);
|
|
187
|
-
if (cmd === '/chat') { currentMode = 'chat'; displayPrompt(); return; }
|
|
188
|
-
if (cmd === '/plan') { currentMode = 'plan'; displayPrompt(); return; }
|
|
189
|
-
if (cmd === '/edit' || cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false; displayPrompt(); return; }
|
|
190
|
-
if (cmd === '/edit auto') { currentMode = 'edit'; editState.autoAccept = true; displayPrompt(); return; }
|
|
191
|
-
if (cmd === '/clear') { resetMessages(); console.clear(); displayPrompt(); return; }
|
|
192
|
-
|
|
193
|
-
if (cmd === '/init') {
|
|
194
|
-
console.log(chalk.cyan('\n🚀 Gerando .bimmorc.json...\n'));
|
|
195
|
-
const initPrompt = `Analise o projeto e crie o .bimmorc.json estruturado. Use write_file.`;
|
|
196
|
-
const spinner = ora({ text: lavender(`bimmo pensando...`), color: 'red' }).start();
|
|
197
169
|
try {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
170
|
+
let response;
|
|
171
|
+
if (swarmType === 'seq') {
|
|
172
|
+
const agents = parts[2].split(',');
|
|
173
|
+
const goal = parts.slice(3).join(' ');
|
|
174
|
+
response = await orchestrator.runSequential(agents, goal);
|
|
175
|
+
} else if (swarmType === 'run') {
|
|
176
|
+
const manager = parts[2];
|
|
177
|
+
const workers = parts[3].split(',');
|
|
178
|
+
const goal = parts.slice(4).join(' ');
|
|
179
|
+
response = await orchestrator.runHierarchical(manager, workers, goal);
|
|
180
|
+
}
|
|
181
|
+
setMessages(prev => [...prev, { role: 'user', content: rawInput }, { role: 'assistant', content: response }]);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
setMessages(prev => [...prev, { role: 'system', content: `Erro no enxame: ${err.message}` }]);
|
|
184
|
+
} finally {
|
|
185
|
+
setIsThinking(false);
|
|
186
|
+
setThinkingMessage('bimmo pensando...');
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
203
189
|
}
|
|
204
190
|
|
|
205
|
-
if (cmd === '/
|
|
206
|
-
|
|
207
|
-
|
|
191
|
+
if (cmd === '/help') {
|
|
192
|
+
const helpText = `
|
|
193
|
+
**Comandos Disponíveis:**
|
|
194
|
+
\`/chat\` | \`/plan\` | \`/edit [auto|manual]\` - Muda o modo
|
|
195
|
+
\`/switch [perfil]\` - Alterna perfis
|
|
196
|
+
\`/model [modelo]\` - Altera o modelo
|
|
197
|
+
\`/use [agente|normal]\` - Ativa um agente
|
|
198
|
+
\`/swarm seq [agente1,agente2] [objetivo]\` - Enxame sequencial
|
|
199
|
+
\`/swarm run [líder] [worker1,worker2] [objetivo]\` - Enxame hierárquico
|
|
200
|
+
\`/clear\` - Limpa o chat
|
|
201
|
+
\`/exit\` - Encerra o bimmo
|
|
202
|
+
\`@arquivo\` - Inclui conteúdo de arquivo
|
|
203
|
+
`;
|
|
204
|
+
setMessages(prev => [...prev, { role: 'assistant', content: helpText }]);
|
|
205
|
+
return;
|
|
208
206
|
}
|
|
209
207
|
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
processedContent.push({ type: 'text', text: `\n[ARQUIVO: ${filePath}]\n${fs.readFileSync(filePath, 'utf-8')}\n` });
|
|
226
|
-
} else { processedContent.push({ type: 'text', text: word }); }
|
|
227
|
-
} else { processedContent.push({ type: 'text', text: word }); }
|
|
208
|
+
// ENVIO PARA IA
|
|
209
|
+
setIsThinking(true);
|
|
210
|
+
|
|
211
|
+
let processedInput = rawInput;
|
|
212
|
+
const fileMatches = rawInput.match(/@[\w\.\-\/]+/g);
|
|
213
|
+
if (fileMatches) {
|
|
214
|
+
for (const match of fileMatches) {
|
|
215
|
+
const filePath = match.slice(1);
|
|
216
|
+
try {
|
|
217
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
218
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
219
|
+
processedInput = processedInput.replace(match, `\n\n[Arquivo: ${filePath}]\n\`\`\`\n${content}\n\`\`\`\n`);
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {}
|
|
222
|
+
}
|
|
228
223
|
}
|
|
229
224
|
|
|
230
|
-
|
|
231
|
-
const
|
|
225
|
+
const userMsg = { role: 'user', content: processedInput, displayContent: rawInput };
|
|
226
|
+
const newMessages = [...messages, userMsg];
|
|
227
|
+
setMessages(newMessages);
|
|
232
228
|
|
|
233
229
|
try {
|
|
234
|
-
let
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
230
|
+
let finalMessages = newMessages;
|
|
231
|
+
if (activePersona && config.agents[activePersona]) {
|
|
232
|
+
const agent = config.agents[activePersona];
|
|
233
|
+
finalMessages = [
|
|
234
|
+
{ role: 'system', content: `Sua tarefa: ${agent.role}\n\n${getProjectContext()}` },
|
|
235
|
+
...newMessages.filter(m => m.role !== 'system')
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const response = await provider.sendMessage(finalMessages);
|
|
240
|
+
setMessages(prev => [...prev, { role: 'assistant', content: response }]);
|
|
241
241
|
} catch (err) {
|
|
242
|
-
|
|
243
|
-
if (controller.signal.aborted) { console.log(yellow(`\n⚠️ Interrompido.`)); messages.pop(); }
|
|
244
|
-
else { console.error(chalk.red(`\n✖ Erro: ${err.message}`)); }
|
|
242
|
+
setMessages(prev => [...prev, { role: 'system', content: `Erro: ${err.message}` }]);
|
|
245
243
|
} finally {
|
|
246
|
-
|
|
247
|
-
displayPrompt();
|
|
244
|
+
setIsThinking(false);
|
|
248
245
|
}
|
|
249
|
-
}
|
|
246
|
+
};
|
|
250
247
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
248
|
+
useInput((input, key) => {
|
|
249
|
+
if (key.ctrl && input === 'c') {
|
|
250
|
+
if (isThinking) {
|
|
251
|
+
setIsThinking(false);
|
|
252
|
+
} else {
|
|
253
|
+
if (exitCounter === 0) {
|
|
254
|
+
setExitCounter(1);
|
|
255
|
+
setTimeout(() => setExitCounter(0), 2000);
|
|
256
|
+
} else {
|
|
257
|
+
exit();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (key.tab && filePreview.length > 0) {
|
|
263
|
+
const words = input.split(' ');
|
|
264
|
+
words[words.length - 1] = `@${filePreview[0].rel}${filePreview[0].isDir ? '/' : ''}`;
|
|
265
|
+
setInput(words.join(' '));
|
|
266
|
+
}
|
|
255
267
|
});
|
|
256
268
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
269
|
+
return (
|
|
270
|
+
<Box flexDirection="column" paddingX={1} minHeight={10}>
|
|
271
|
+
{/* HEADER */}
|
|
272
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
273
|
+
<Text color={lavender}>{figlet.textSync('bimmo', { font: 'small' })}</Text>
|
|
274
|
+
<Box borderStyle="single" borderColor={lavender} paddingX={1} justifyContent="space-between">
|
|
275
|
+
<Text color={green} bold>v{version}</Text>
|
|
276
|
+
<Box>
|
|
277
|
+
<Text color={gray}>{config.activeProfile || 'Default'} </Text>
|
|
278
|
+
<Text color={lavender}>•</Text>
|
|
279
|
+
<Text color={gray}> {config.model}</Text>
|
|
280
|
+
</Box>
|
|
281
|
+
</Box>
|
|
282
|
+
</Box>
|
|
283
|
+
|
|
284
|
+
{/* MENSAGENS */}
|
|
285
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
286
|
+
{messages.filter(m => m.role !== 'system').slice(-10).map((m, i) => (
|
|
287
|
+
<Box key={i} flexDirection="column" marginBottom={1}>
|
|
288
|
+
<Box>
|
|
289
|
+
<Text bold color={m.role === 'user' ? green : lavender}>
|
|
290
|
+
{m.role === 'user' ? '› Você' : '› bimmo'}
|
|
291
|
+
</Text>
|
|
292
|
+
{m.role === 'system' && <Text color={yellow}> [SISTEMA]</Text>}
|
|
293
|
+
</Box>
|
|
294
|
+
<Box paddingLeft={2}>
|
|
295
|
+
<Text>
|
|
296
|
+
{m.role === 'assistant'
|
|
297
|
+
? marked.parse(m.content).trim()
|
|
298
|
+
: (m.displayContent || m.content)}
|
|
299
|
+
</Text>
|
|
300
|
+
</Box>
|
|
301
|
+
</Box>
|
|
302
|
+
))}
|
|
303
|
+
</Box>
|
|
265
304
|
|
|
266
|
-
|
|
305
|
+
{/* STATUS / THINKING */}
|
|
306
|
+
{isThinking && (
|
|
307
|
+
<Box marginBottom={1}>
|
|
308
|
+
<Text color={lavender}>
|
|
309
|
+
<Spinner type="dots" /> <Text italic>{thinkingMessage}</Text>
|
|
310
|
+
</Text>
|
|
311
|
+
</Box>
|
|
312
|
+
)}
|
|
313
|
+
|
|
314
|
+
{/* AUTOCOMPLETE PREVIEW */}
|
|
315
|
+
{filePreview.length > 0 && (
|
|
316
|
+
<Box flexDirection="column" borderStyle="round" borderColor={gray} paddingX={1} marginBottom={1}>
|
|
317
|
+
<Text color={gray} dimColor italic>Sugestões (TAB para completar):</Text>
|
|
318
|
+
{filePreview.map((f, i) => (
|
|
319
|
+
<Text key={i} color={i === 0 ? green : gray}>
|
|
320
|
+
{f.isDir ? '📁' : '📄'} {f.rel}{f.isDir ? '/' : ''}
|
|
321
|
+
</Text>
|
|
322
|
+
))}
|
|
323
|
+
</Box>
|
|
324
|
+
)}
|
|
325
|
+
|
|
326
|
+
{/* PROMPT */}
|
|
327
|
+
<Box borderStyle="round" borderColor={isThinking ? gray : lavender} paddingX={1}>
|
|
328
|
+
<Text bold color={mode === 'edit' ? red : mode === 'plan' ? cyan : lavender}>
|
|
329
|
+
{activePersona ? `[${activePersona.toUpperCase()}] ` : ''}
|
|
330
|
+
[{mode.toUpperCase()}] ›{' '}
|
|
331
|
+
</Text>
|
|
332
|
+
<TextInput
|
|
333
|
+
value={input}
|
|
334
|
+
onChange={setInput}
|
|
335
|
+
onSubmit={handleSubmit}
|
|
336
|
+
placeholder="Como posso ajudar hoje?"
|
|
337
|
+
/>
|
|
338
|
+
</Box>
|
|
339
|
+
|
|
340
|
+
{/* FOOTER */}
|
|
341
|
+
<Box marginTop={1} justifyContent="space-between" paddingX={1}>
|
|
342
|
+
<Text color={gray} dimColor>📁 {path.relative(process.env.HOME || '', process.cwd())}</Text>
|
|
343
|
+
{exitCounter === 1 && <Text color={yellow} bold> Pressione Ctrl+C novamente para sair</Text>}
|
|
344
|
+
<Box>
|
|
345
|
+
<Text color={gray} dimColor italic>↑↓ para histórico (em breve) • /help para comandos</Text>
|
|
346
|
+
</Box>
|
|
347
|
+
</Box>
|
|
348
|
+
</Box>
|
|
349
|
+
);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
export async function startInteractive() {
|
|
353
|
+
const config = getConfig();
|
|
354
|
+
if (!config.provider || !config.apiKey) {
|
|
355
|
+
console.log(chalk.yellow('Provedor não configurado. Execute "bimmo config" primeiro.'));
|
|
356
|
+
process.exit(0);
|
|
357
|
+
}
|
|
358
|
+
process.stdout.write('\x1Bc');
|
|
359
|
+
render(<BimmoApp initialConfig={config} />);
|
|
267
360
|
}
|