bimmo-cli 2.2.12 → 3.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/bin/bimmo +10 -3
- package/package.json +26 -3
- package/src/interface.jsx +304 -0
- package/src/interface.js +0 -310
package/bin/bimmo
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env -S node --import=tsx
|
|
2
2
|
|
|
3
3
|
// Silencia o aviso de depreciação do punycode que polui o terminal em versões novas do Node
|
|
4
4
|
process.removeAllListeners('warning');
|
|
5
5
|
|
|
6
6
|
import { program } from 'commander';
|
|
7
|
-
import { startInteractive } from '../src/interface.
|
|
7
|
+
import { startInteractive } from '../src/interface.jsx';
|
|
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,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bimmo-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "🌿 Plataforma de IA universal profissional com
|
|
3
|
+
"version": "3.1.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",
|
|
10
|
+
"ai",
|
|
11
|
+
"cli",
|
|
12
|
+
"ink",
|
|
13
|
+
"react",
|
|
14
|
+
"openai",
|
|
15
|
+
"anthropic",
|
|
16
|
+
"gemini",
|
|
17
|
+
"agent",
|
|
18
|
+
"swarm",
|
|
19
|
+
"terminal"
|
|
11
20
|
],
|
|
12
21
|
"author": "Judah",
|
|
13
22
|
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/JudahAragao/bimmo-cli.git"
|
|
26
|
+
},
|
|
14
27
|
"files": [
|
|
15
28
|
"bin/",
|
|
16
29
|
"src/",
|
|
@@ -21,6 +34,7 @@
|
|
|
21
34
|
"access": "public"
|
|
22
35
|
},
|
|
23
36
|
"dependencies": {
|
|
37
|
+
"tsx": "^4.19.2",
|
|
24
38
|
"@anthropic-ai/sdk": "^0.36.3",
|
|
25
39
|
"@google/generative-ai": "^0.21.0",
|
|
26
40
|
"@tavily/core": "^0.0.2",
|
|
@@ -30,13 +44,22 @@
|
|
|
30
44
|
"diff": "^7.0.0",
|
|
31
45
|
"figlet": "^1.7.0",
|
|
32
46
|
"fuzzy": "^0.1.3",
|
|
47
|
+
"ink": "^5.1.0",
|
|
48
|
+
"ink-divider": "^3.0.0",
|
|
49
|
+
"ink-spinner": "^5.0.0",
|
|
50
|
+
"ink-text-input": "^6.0.0",
|
|
33
51
|
"inquirer": "^9.3.8",
|
|
52
|
+
"jiti": "^2.6.1",
|
|
34
53
|
"marked": "^14.0.0",
|
|
35
54
|
"marked-terminal": "^7.0.0",
|
|
36
55
|
"mime-types": "^2.1.35",
|
|
37
56
|
"ollama": "^0.5.12",
|
|
38
57
|
"openai": "^4.82.0",
|
|
39
58
|
"ora": "^8.1.1",
|
|
59
|
+
"react": "^18.2.0",
|
|
40
60
|
"zod": "^3.24.1"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"tsx": "^4.21.0"
|
|
41
64
|
}
|
|
42
65
|
}
|
|
@@ -0,0 +1,304 @@
|
|
|
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';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import figlet from 'figlet';
|
|
7
|
+
import { marked } from 'marked';
|
|
8
|
+
import TerminalRenderer from 'marked-terminal';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
import { getConfig, updateActiveModel, switchProfile } from './config.js';
|
|
14
|
+
import { createProvider } from './providers/factory.js';
|
|
15
|
+
import { getProjectContext } from './project-context.js';
|
|
16
|
+
import { editState } from './agent.js';
|
|
17
|
+
import { SwarmOrchestrator } from './orchestrator.js';
|
|
18
|
+
|
|
19
|
+
// --- CONFIGURAÇÃO VISUAL ---
|
|
20
|
+
const green = '#00ff9d';
|
|
21
|
+
const lavender = '#c084fc';
|
|
22
|
+
const gray = '#6272a4';
|
|
23
|
+
const yellow = '#f1fa8c';
|
|
24
|
+
const red = '#ff5555';
|
|
25
|
+
const cyan = '#8be9fd';
|
|
26
|
+
|
|
27
|
+
marked.use(new TerminalRenderer({
|
|
28
|
+
heading: chalk.hex(lavender).bold,
|
|
29
|
+
code: chalk.hex(green),
|
|
30
|
+
strong: chalk.bold,
|
|
31
|
+
em: chalk.italic,
|
|
32
|
+
html: () => '',
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
36
|
+
const __dirname = path.dirname(__filename);
|
|
37
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
|
|
38
|
+
const version = pkg.version;
|
|
39
|
+
|
|
40
|
+
// --- COMPONENTES ---
|
|
41
|
+
|
|
42
|
+
const Header = ({ config }) => (
|
|
43
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
44
|
+
<Text color={lavender}>{figlet.textSync('bimmo', { font: 'small' })}</Text>
|
|
45
|
+
<Box borderStyle="single" borderColor={lavender} paddingX={1} justifyContent="space-between">
|
|
46
|
+
<Text color={green} bold>v{version}</Text>
|
|
47
|
+
<Box>
|
|
48
|
+
<Text color={gray}>{config.activeProfile || 'Default'} </Text>
|
|
49
|
+
<Text color={lavender}>•</Text>
|
|
50
|
+
<Text color={gray}> {config.model}</Text>
|
|
51
|
+
</Box>
|
|
52
|
+
</Box>
|
|
53
|
+
</Box>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const MessageList = ({ messages }) => (
|
|
57
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
58
|
+
{messages.filter(m => m.role !== 'system').slice(-10).map((m, i) => (
|
|
59
|
+
<Box key={i} flexDirection="column" marginBottom={1}>
|
|
60
|
+
<Box>
|
|
61
|
+
<Text bold color={m.role === 'user' ? green : lavender}>
|
|
62
|
+
{m.role === 'user' ? '› Você' : '› bimmo'}
|
|
63
|
+
</Text>
|
|
64
|
+
{m.role === 'system' && <Text color={yellow}> [SISTEMA]</Text>}
|
|
65
|
+
</Box>
|
|
66
|
+
<Box paddingLeft={2}>
|
|
67
|
+
<Text>
|
|
68
|
+
{m.role === 'assistant'
|
|
69
|
+
? marked.parse(m.content).trim()
|
|
70
|
+
: (m.displayContent || m.content)}
|
|
71
|
+
</Text>
|
|
72
|
+
</Box>
|
|
73
|
+
</Box>
|
|
74
|
+
))}
|
|
75
|
+
</Box>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const Autocomplete = ({ suggestions }) => (
|
|
79
|
+
<Box flexDirection="column" borderStyle="round" borderColor={gray} paddingX={1} marginBottom={1}>
|
|
80
|
+
<Text color={gray} dimColor italic>Sugestões (TAB para completar):</Text>
|
|
81
|
+
{suggestions.map((f, i) => (
|
|
82
|
+
<Text key={i} color={i === 0 ? green : gray}>
|
|
83
|
+
{f.isDir ? '📁' : '📄'} {f.rel}{f.isDir ? '/' : ''}
|
|
84
|
+
</Text>
|
|
85
|
+
))}
|
|
86
|
+
</Box>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const Footer = ({ exitCounter }) => (
|
|
90
|
+
<Box marginTop={1} justifyContent="space-between" paddingX={1}>
|
|
91
|
+
<Text color={gray} dimColor>📁 {path.relative(process.env.HOME || '', process.cwd())}</Text>
|
|
92
|
+
{exitCounter === 1 && <Text color={yellow} bold> Pressione Ctrl+C novamente para sair</Text>}
|
|
93
|
+
<Box>
|
|
94
|
+
<Text color={gray} dimColor italic>↑↓ para histórico • /help para comandos</Text>
|
|
95
|
+
</Box>
|
|
96
|
+
</Box>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const BimmoApp = ({ initialConfig }) => {
|
|
100
|
+
const { exit } = useApp();
|
|
101
|
+
const [config, setConfig] = useState(initialConfig);
|
|
102
|
+
const [mode, setMode] = useState('chat');
|
|
103
|
+
const [activePersona, setActivePersona] = useState(null);
|
|
104
|
+
const [messages, setMessages] = useState([]);
|
|
105
|
+
const [input, setInput] = useState('');
|
|
106
|
+
const [isThinking, setIsThinking] = useState(false);
|
|
107
|
+
const [thinkingMessage, setThinkingMessage] = useState('bimmo pensando...');
|
|
108
|
+
const [exitCounter, setExitCounter] = useState(0);
|
|
109
|
+
const [provider, setProvider] = useState(() => createProvider(initialConfig));
|
|
110
|
+
|
|
111
|
+
// Inicializa contexto
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const ctx = getProjectContext();
|
|
114
|
+
setMessages([
|
|
115
|
+
{ role: 'system', content: ctx },
|
|
116
|
+
{ role: 'assistant', content: `Olá! Sou o **bimmo v${version}**. Como posso ajudar hoje?\n\nDigite \`/help\` para ver os comandos.` }
|
|
117
|
+
]);
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
const filePreview = useMemo(() => {
|
|
121
|
+
if (!input.includes('@')) return [];
|
|
122
|
+
const words = input.split(' ');
|
|
123
|
+
const lastWord = words[words.length - 1];
|
|
124
|
+
if (!lastWord.startsWith('@')) return [];
|
|
125
|
+
try {
|
|
126
|
+
const p = lastWord.slice(1);
|
|
127
|
+
const dir = p.includes('/') ? p.substring(0, p.lastIndexOf('/')) : '.';
|
|
128
|
+
const filter = p.includes('/') ? p.substring(p.lastIndexOf('/') + 1) : p;
|
|
129
|
+
const absDir = path.resolve(process.cwd(), dir);
|
|
130
|
+
if (!fs.existsSync(absDir)) return [];
|
|
131
|
+
return fs.readdirSync(absDir)
|
|
132
|
+
.filter(f => f.startsWith(filter) && !f.startsWith('.') && f !== 'node_modules')
|
|
133
|
+
.slice(0, 5)
|
|
134
|
+
.map(f => ({
|
|
135
|
+
name: f,
|
|
136
|
+
isDir: fs.statSync(path.join(absDir, f)).isDirectory(),
|
|
137
|
+
rel: path.join(dir === '.' ? '' : dir, f)
|
|
138
|
+
}));
|
|
139
|
+
} catch (e) { return []; }
|
|
140
|
+
}, [input]);
|
|
141
|
+
|
|
142
|
+
const handleSubmit = async (val) => {
|
|
143
|
+
const rawInput = val.trim();
|
|
144
|
+
if (!rawInput) return;
|
|
145
|
+
setInput('');
|
|
146
|
+
const parts = rawInput.split(' ');
|
|
147
|
+
const cmd = parts[0].toLowerCase();
|
|
148
|
+
|
|
149
|
+
// Comandos de Sistema
|
|
150
|
+
if (cmd === '/exit') exit();
|
|
151
|
+
if (cmd === '/clear') {
|
|
152
|
+
setMessages([{ role: 'system', content: getProjectContext() }, { role: 'assistant', content: 'Chat limpo.' }]);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (['/chat', '/plan', '/edit'].includes(cmd)) {
|
|
156
|
+
setMode(cmd.slice(1));
|
|
157
|
+
if (cmd === '/edit') editState.autoAccept = parts[1] === 'auto';
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (cmd === '/model') {
|
|
162
|
+
const newModel = parts[1];
|
|
163
|
+
if (newModel) {
|
|
164
|
+
updateActiveModel(newModel);
|
|
165
|
+
const newCfg = getConfig();
|
|
166
|
+
setConfig(newCfg);
|
|
167
|
+
setProvider(createProvider(newCfg));
|
|
168
|
+
setMessages(prev => [...prev, { role: 'system', content: `Modelo alterado para: ${newModel}` }]);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (cmd === '/switch') {
|
|
174
|
+
const profile = parts[1];
|
|
175
|
+
if (switchProfile(profile)) {
|
|
176
|
+
const newCfg = getConfig();
|
|
177
|
+
setConfig(newCfg);
|
|
178
|
+
setProvider(createProvider(newCfg));
|
|
179
|
+
setMessages(prev => [...prev, { role: 'system', content: `Perfil alterado para: ${profile}` }]);
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (cmd === '/use') {
|
|
185
|
+
const agentName = parts[1];
|
|
186
|
+
const agents = config.agents || {};
|
|
187
|
+
if (agentName === 'normal') {
|
|
188
|
+
setActivePersona(null);
|
|
189
|
+
} else if (agents[agentName]) {
|
|
190
|
+
setActivePersona(agentName);
|
|
191
|
+
setMode(agents[agentName].mode || 'chat');
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (cmd === '/swarm') {
|
|
197
|
+
const orchestrator = new SwarmOrchestrator(config);
|
|
198
|
+
setIsThinking(true);
|
|
199
|
+
setThinkingMessage('Enxame em ação...');
|
|
200
|
+
try {
|
|
201
|
+
let response;
|
|
202
|
+
if (parts[1] === 'seq') response = await orchestrator.runSequential(parts[2].split(','), parts.slice(3).join(' '));
|
|
203
|
+
if (parts[1] === 'run') response = await orchestrator.runHierarchical(parts[2], parts[3].split(','), parts.slice(4).join(' '));
|
|
204
|
+
setMessages(prev => [...prev, { role: 'user', content: rawInput }, { role: 'assistant', content: response }]);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
setMessages(prev => [...prev, { role: 'system', content: `Erro no enxame: ${err.message}` }]);
|
|
207
|
+
} finally {
|
|
208
|
+
setIsThinking(false);
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (cmd === '/help') {
|
|
214
|
+
setMessages(prev => [...prev, { role: 'assistant', content: `**Comandos:** /chat, /plan, /edit, /switch, /model, /use, /swarm, /clear, /exit, @arquivo` }]);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Processamento de arquivos @
|
|
219
|
+
setIsThinking(true);
|
|
220
|
+
let processedInput = rawInput;
|
|
221
|
+
const fileMatches = rawInput.match(/@[\w\.\-\/]+/g);
|
|
222
|
+
if (fileMatches) {
|
|
223
|
+
for (const match of fileMatches) {
|
|
224
|
+
const filePath = match.slice(1);
|
|
225
|
+
try {
|
|
226
|
+
if (fs.existsSync(filePath)) {
|
|
227
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
228
|
+
processedInput = processedInput.replace(match, `\n\n[Arquivo: ${filePath}]\n\`\`\`\n${content}\n\`\`\`\n`);
|
|
229
|
+
}
|
|
230
|
+
} catch (e) {}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const userMsg = { role: 'user', content: processedInput, displayContent: rawInput };
|
|
235
|
+
const newMessages = [...messages, userMsg];
|
|
236
|
+
setMessages(newMessages);
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
let finalMessages = newMessages;
|
|
240
|
+
if (activePersona && config.agents[activePersona]) {
|
|
241
|
+
const agent = config.agents[activePersona];
|
|
242
|
+
finalMessages = [{ role: 'system', content: `Sua tarefa: ${agent.role}\n\n${getProjectContext()}` }, ...newMessages.filter(m => m.role !== 'system')];
|
|
243
|
+
}
|
|
244
|
+
const response = await provider.sendMessage(finalMessages);
|
|
245
|
+
setMessages(prev => [...prev, { role: 'assistant', content: response }]);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
setMessages(prev => [...prev, { role: 'system', content: `Erro: ${err.message}` }]);
|
|
248
|
+
} finally {
|
|
249
|
+
setIsThinking(false);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
useInput((input, key) => {
|
|
254
|
+
if (key.ctrl && input === 'c') {
|
|
255
|
+
if (isThinking) setIsThinking(false);
|
|
256
|
+
else {
|
|
257
|
+
if (exitCounter === 0) { setExitCounter(1); setTimeout(() => setExitCounter(0), 2000); }
|
|
258
|
+
else exit();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (key.tab && filePreview.length > 0) {
|
|
262
|
+
const words = input.split(' ');
|
|
263
|
+
words[words.length - 1] = `@${filePreview[0].rel}${filePreview[0].isDir ? '/' : ''}`;
|
|
264
|
+
setInput(words.join(' '));
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<Box flexDirection="column" paddingX={1} minHeight={10}>
|
|
270
|
+
<Header config={config} />
|
|
271
|
+
<MessageList messages={messages} />
|
|
272
|
+
|
|
273
|
+
{isThinking && (
|
|
274
|
+
<Box marginBottom={1}>
|
|
275
|
+
<Text color={lavender}>
|
|
276
|
+
<Spinner type="dots" /> <Text italic>{thinkingMessage}</Text>
|
|
277
|
+
</Text>
|
|
278
|
+
</Box>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
{filePreview.length > 0 && <Autocomplete suggestions={filePreview} />}
|
|
282
|
+
|
|
283
|
+
<Box borderStyle="round" borderColor={isThinking ? gray : lavender} paddingX={1}>
|
|
284
|
+
<Text bold color={mode === 'edit' ? red : mode === 'plan' ? cyan : lavender}>
|
|
285
|
+
{activePersona ? `[${activePersona.toUpperCase()}] ` : ''}
|
|
286
|
+
[{mode.toUpperCase()}] ›{' '}
|
|
287
|
+
</Text>
|
|
288
|
+
<TextInput value={input} onChange={setInput} onSubmit={handleSubmit} placeholder="Como posso ajudar hoje?" />
|
|
289
|
+
</Box>
|
|
290
|
+
|
|
291
|
+
<Footer exitCounter={exitCounter} />
|
|
292
|
+
</Box>
|
|
293
|
+
);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
export async function startInteractive() {
|
|
297
|
+
const config = getConfig();
|
|
298
|
+
if (!config.provider || !config.apiKey) {
|
|
299
|
+
console.log(chalk.yellow('Provedor não configurado. Execute "bimmo config" primeiro.'));
|
|
300
|
+
process.exit(0);
|
|
301
|
+
}
|
|
302
|
+
process.stdout.write('\x1Bc');
|
|
303
|
+
render(<BimmoApp initialConfig={config} />);
|
|
304
|
+
}
|
package/src/interface.js
DELETED
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import figlet from 'figlet';
|
|
3
|
-
import { marked } from 'marked';
|
|
4
|
-
import TerminalRenderer from 'marked-terminal';
|
|
5
|
-
import ora from 'ora';
|
|
6
|
-
import fs from 'fs';
|
|
7
|
-
import path from 'path';
|
|
8
|
-
import mime from 'mime-types';
|
|
9
|
-
import readline from 'readline';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
|
-
|
|
12
|
-
import { getConfig, configure, updateActiveModel, switchProfile } from './config.js';
|
|
13
|
-
import { createProvider } from './providers/factory.js';
|
|
14
|
-
import { getProjectContext } from './project-context.js';
|
|
15
|
-
import { SwarmOrchestrator } from './orchestrator.js';
|
|
16
|
-
import { editState } from './agent.js';
|
|
17
|
-
|
|
18
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
-
const __dirname = path.dirname(__filename);
|
|
20
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
|
|
21
|
-
const version = pkg.version;
|
|
22
|
-
|
|
23
|
-
// CONFIGURAÇÃO DO RENDERIZADOR - ELIMINA HTML E FORMATA TERMINAL
|
|
24
|
-
marked.use(new TerminalRenderer({
|
|
25
|
-
heading: chalk.hex('#c084fc').bold,
|
|
26
|
-
code: chalk.hex('#00ff9d'),
|
|
27
|
-
strong: chalk.bold,
|
|
28
|
-
em: chalk.italic,
|
|
29
|
-
html: () => '', // DROP TOTAL DE HTML
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
const green = chalk.hex('#00ff9d');
|
|
33
|
-
const lavender = chalk.hex('#c084fc');
|
|
34
|
-
const gray = chalk.gray;
|
|
35
|
-
const bold = chalk.bold;
|
|
36
|
-
const yellow = chalk.yellow;
|
|
37
|
-
|
|
38
|
-
let currentMode = 'chat';
|
|
39
|
-
let activePersona = null;
|
|
40
|
-
let exitCounter = 0;
|
|
41
|
-
let exitTimer = null;
|
|
42
|
-
let currentPreviewLines = 0;
|
|
43
|
-
|
|
44
|
-
const i18n = {
|
|
45
|
-
'pt-BR': {
|
|
46
|
-
welcome: 'Olá! Sou o bimmo. Como posso ajudar?',
|
|
47
|
-
thinking: 'bimmo pensando...',
|
|
48
|
-
interrupted: 'Interrompido.',
|
|
49
|
-
exitHint: '(Pressione Ctrl+C novamente para sair)',
|
|
50
|
-
switchOk: 'Perfil ativo:',
|
|
51
|
-
agentOk: 'Agente ativo:',
|
|
52
|
-
help: '\nComandos:\n /chat | /plan | /edit | /init\n /switch [perfil] | /model [modelo]\n /use [agente] | /use normal\n /config | /clear | @arquivo\n'
|
|
53
|
-
},
|
|
54
|
-
'en-US': {
|
|
55
|
-
welcome: 'Hello! I am bimmo. How can I help?',
|
|
56
|
-
thinking: 'bimmo thinking...',
|
|
57
|
-
interrupted: 'Interrupted.',
|
|
58
|
-
exitHint: '(Press Ctrl+C again to exit)',
|
|
59
|
-
switchOk: 'Profile active:',
|
|
60
|
-
agentOk: 'Agent active:',
|
|
61
|
-
help: '\nCommands:\n /chat | /plan | /edit | /init\n /switch [profile] | /model [model]\n /use [agent] | /use normal\n /config | /clear | @file\n'
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Coleta arquivos e diretórios para o preview (Lógica Gemini-CLI)
|
|
67
|
-
*/
|
|
68
|
-
function getFilesForPreview(partialPath) {
|
|
69
|
-
try {
|
|
70
|
-
let p = partialPath.startsWith('@') ? partialPath.slice(1) : partialPath;
|
|
71
|
-
let searchDir = '.';
|
|
72
|
-
let filter = '';
|
|
73
|
-
|
|
74
|
-
if (p.endsWith('/')) {
|
|
75
|
-
searchDir = p;
|
|
76
|
-
filter = '';
|
|
77
|
-
} else if (p.includes('/')) {
|
|
78
|
-
const lastSlash = p.lastIndexOf('/');
|
|
79
|
-
searchDir = p.substring(0, lastSlash);
|
|
80
|
-
filter = p.substring(lastSlash + 1);
|
|
81
|
-
} else {
|
|
82
|
-
filter = p;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const absoluteSearchDir = path.resolve(process.cwd(), searchDir);
|
|
86
|
-
if (!fs.existsSync(absoluteSearchDir)) return [];
|
|
87
|
-
|
|
88
|
-
return fs.readdirSync(absoluteSearchDir)
|
|
89
|
-
.filter(f => f.startsWith(filter) && !f.startsWith('.') && f !== 'node_modules')
|
|
90
|
-
.map(f => {
|
|
91
|
-
const fullPath = path.join(absoluteSearchDir, f);
|
|
92
|
-
const isDir = fs.statSync(fullPath).isDirectory();
|
|
93
|
-
return { name: f, rel: path.join(searchDir, f), isDir };
|
|
94
|
-
});
|
|
95
|
-
} catch (e) { return []; }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function cleanAIResponse(text) {
|
|
99
|
-
if (!text) return "";
|
|
100
|
-
return text
|
|
101
|
-
.replace(/<br\s*\/?>/gi, '\n')
|
|
102
|
-
.replace(/<p>/gi, '')
|
|
103
|
-
.replace(/<\/p>/gi, '\n\n')
|
|
104
|
-
.replace(/<[^>]*>?/gm, '')
|
|
105
|
-
.trim();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export async function startInteractive() {
|
|
109
|
-
let config = getConfig();
|
|
110
|
-
const lang = config.language || 'pt-BR';
|
|
111
|
-
const t = i18n[lang] || i18n['pt-BR'];
|
|
112
|
-
|
|
113
|
-
if (!config.provider || !config.apiKey) {
|
|
114
|
-
console.log(lavender(figlet.textSync('bimmo')));
|
|
115
|
-
await configure(); return startInteractive();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let provider = createProvider(config);
|
|
119
|
-
const orchestrator = new SwarmOrchestrator(config);
|
|
120
|
-
let messages = [];
|
|
121
|
-
|
|
122
|
-
const resetMessages = () => {
|
|
123
|
-
messages = [{ role: 'system', content: getProjectContext() }];
|
|
124
|
-
if (activePersona) {
|
|
125
|
-
const agent = (config.agents || {})[activePersona];
|
|
126
|
-
if (agent) messages.push({ role: 'system', content: `Persona: ${agent.name}. Task: ${agent.role}` });
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
resetMessages();
|
|
130
|
-
|
|
131
|
-
console.clear();
|
|
132
|
-
console.log(lavender(figlet.textSync('bimmo')));
|
|
133
|
-
console.log(lavender(` v${version} `.padStart(60, '─')));
|
|
134
|
-
console.log(green(` Perfil: ${bold(config.activeProfile || 'Padrão')} • Modelo: ${bold(config.model)}`));
|
|
135
|
-
console.log(lavender('─'.repeat(60)) + '\n');
|
|
136
|
-
console.log(lavender(`👋 ${t.welcome}\n`));
|
|
137
|
-
|
|
138
|
-
const rl = readline.createInterface({
|
|
139
|
-
input: process.stdin,
|
|
140
|
-
output: process.stdout,
|
|
141
|
-
terminal: true,
|
|
142
|
-
completer: (line) => {
|
|
143
|
-
const words = line.split(' ');
|
|
144
|
-
const lastWord = words[words.length - 1];
|
|
145
|
-
if (lastWord.startsWith('@')) {
|
|
146
|
-
const hits = getFilesForPreview(lastWord).map(h => `@${h.rel}${h.isDir ? '/' : ''}`);
|
|
147
|
-
return [hits, lastWord];
|
|
148
|
-
}
|
|
149
|
-
return [[], line];
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const clearPreview = () => {
|
|
154
|
-
if (currentPreviewLines > 0) {
|
|
155
|
-
readline.cursorTo(process.stdout, 0);
|
|
156
|
-
readline.moveCursor(process.stdout, 0, 1);
|
|
157
|
-
for (let i = 0; i < currentPreviewLines; i++) {
|
|
158
|
-
readline.clearLine(process.stdout, 0);
|
|
159
|
-
readline.moveCursor(process.stdout, 0, 1);
|
|
160
|
-
}
|
|
161
|
-
readline.moveCursor(process.stdout, 0, -(currentPreviewLines + 1));
|
|
162
|
-
currentPreviewLines = 0;
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const showPreview = () => {
|
|
167
|
-
const words = rl.line.split(' ');
|
|
168
|
-
const lastWord = words[words.length - 1];
|
|
169
|
-
|
|
170
|
-
if (lastWord.startsWith('@')) {
|
|
171
|
-
const files = getFilesForPreview(lastWord);
|
|
172
|
-
if (files.length > 0) {
|
|
173
|
-
clearPreview();
|
|
174
|
-
process.stdout.write('\n');
|
|
175
|
-
const display = files.slice(0, 8);
|
|
176
|
-
display.forEach(f => {
|
|
177
|
-
process.stdout.write(gray(` ${f.isDir ? '📁' : '📄'} ${f.rel}${f.isDir ? '/' : ''}\n`));
|
|
178
|
-
});
|
|
179
|
-
currentPreviewLines = display.length;
|
|
180
|
-
readline.moveCursor(process.stdout, 0, -(currentPreviewLines + 1));
|
|
181
|
-
// Reposiciona o cursor de digitação
|
|
182
|
-
const promptLen = (activePersona ? activePersona.length + 2 : 0) + (currentMode.length + 8) + 3;
|
|
183
|
-
readline.cursorTo(process.stdout, (promptLen + rl.line.length) % process.stdout.columns);
|
|
184
|
-
} else { clearPreview(); }
|
|
185
|
-
} else { clearPreview(); }
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
const displayPrompt = () => {
|
|
189
|
-
const personaLabel = activePersona ? `[${activePersona.toUpperCase()}]` : '';
|
|
190
|
-
let modeLabel = `[${currentMode.toUpperCase()}]`;
|
|
191
|
-
if (currentMode === 'edit') modeLabel = editState.autoAccept ? '[EDIT(AUTO)]' : '[EDIT(MANUAL)]';
|
|
192
|
-
|
|
193
|
-
console.log(`${gray(`📁 ${process.cwd()}`)}`);
|
|
194
|
-
rl.setPrompt(lavender.bold(personaLabel) + (currentMode === 'edit' ? chalk.red.bold(modeLabel) : lavender.bold(modeLabel)) + green(' > '));
|
|
195
|
-
rl.prompt();
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
// MONITORAMENTO DE TECLAS (REAL-TIME @)
|
|
199
|
-
process.stdin.on('keypress', (s, key) => {
|
|
200
|
-
if (key && (key.name === 'return' || key.name === 'enter')) return;
|
|
201
|
-
setImmediate(() => showPreview());
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
rl.on('SIGINT', () => {
|
|
205
|
-
if (exitCounter === 0) {
|
|
206
|
-
exitCounter++;
|
|
207
|
-
process.stdout.write(`\n${gray(t.exitHint)}\n`);
|
|
208
|
-
exitTimer = setTimeout(() => { exitCounter = 0; }, 2000);
|
|
209
|
-
displayPrompt();
|
|
210
|
-
} else { process.exit(0); }
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
displayPrompt();
|
|
214
|
-
|
|
215
|
-
rl.on('line', async (input) => {
|
|
216
|
-
clearPreview();
|
|
217
|
-
const rawInput = input.trim();
|
|
218
|
-
if (rawInput === '') { displayPrompt(); return; }
|
|
219
|
-
|
|
220
|
-
const cmd = rawInput.toLowerCase();
|
|
221
|
-
if (cmd === '/exit' || cmd === 'sair') process.exit(0);
|
|
222
|
-
if (cmd === '/chat') { currentMode = 'chat'; displayPrompt(); return; }
|
|
223
|
-
if (cmd === '/plan') { currentMode = 'plan'; displayPrompt(); return; }
|
|
224
|
-
if (cmd === '/edit' || cmd === '/edit manual') { currentMode = 'edit'; editState.autoAccept = false; displayPrompt(); return; }
|
|
225
|
-
if (cmd === '/edit auto') { currentMode = 'edit'; editState.autoAccept = true; displayPrompt(); return; }
|
|
226
|
-
if (cmd === '/clear') { resetMessages(); console.clear(); displayPrompt(); return; }
|
|
227
|
-
if (cmd === '/help') { console.log(gray(t.help)); displayPrompt(); return; }
|
|
228
|
-
|
|
229
|
-
if (cmd === '/init') {
|
|
230
|
-
console.log(chalk.cyan('\n🚀 Mapeando projeto para gerar .bimmorc.json...\n'));
|
|
231
|
-
const initPrompt = `Analise a estrutura deste projeto e gere um .bimmorc.json com regras de código, stack e arquitetura. Use write_file.`;
|
|
232
|
-
const spinner = ora({ text: lavender(`${t.thinking}`), color: 'red' }).start();
|
|
233
|
-
try {
|
|
234
|
-
const res = await provider.sendMessage([...messages, { role: 'user', content: initPrompt }]);
|
|
235
|
-
spinner.stop(); console.log(marked.parse(cleanAIResponse(res)));
|
|
236
|
-
} catch (e) { spinner.stop(); console.error(chalk.red(e.message)); }
|
|
237
|
-
resetMessages(); displayPrompt(); return;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (cmd === '/config') {
|
|
241
|
-
rl.pause(); await configure(); config = getConfig(); provider = createProvider(config); rl.resume();
|
|
242
|
-
displayPrompt(); return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (cmd.startsWith('/switch ')) {
|
|
246
|
-
const pName = rawInput.split(' ')[1];
|
|
247
|
-
if (switchProfile(pName)) {
|
|
248
|
-
config = getConfig(); provider = createProvider(config);
|
|
249
|
-
console.log(green(`\n✓ ${t.switchOk} ${pName}`));
|
|
250
|
-
} else { console.log(chalk.red(`\n✖ Perfil não encontrado.`)); }
|
|
251
|
-
displayPrompt(); return;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (cmd.startsWith('/use ')) {
|
|
255
|
-
const aName = rawInput.split(' ')[1];
|
|
256
|
-
if (aName === 'normal') { activePersona = null; resetMessages(); displayPrompt(); return; }
|
|
257
|
-
const agents = config.agents || {};
|
|
258
|
-
if (agents[aName]) {
|
|
259
|
-
activePersona = aName;
|
|
260
|
-
const agent = agents[aName];
|
|
261
|
-
if (switchProfile(agent.profile)) { config = getConfig(); provider = createProvider(config); }
|
|
262
|
-
currentMode = agent.mode || 'chat';
|
|
263
|
-
console.log(green(`\n✓ ${t.agentOk} ${bold(aName)}`));
|
|
264
|
-
resetMessages();
|
|
265
|
-
} else { console.log(chalk.red(`\n✖ Agente não encontrado.`)); }
|
|
266
|
-
displayPrompt(); return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// PROCESSAMENTO IA
|
|
270
|
-
const controller = new AbortController();
|
|
271
|
-
const abortHandler = () => controller.abort();
|
|
272
|
-
process.removeListener('SIGINT', () => {});
|
|
273
|
-
process.on('SIGINT', abortHandler);
|
|
274
|
-
|
|
275
|
-
let modeInstr = "";
|
|
276
|
-
if (currentMode === 'plan') modeInstr = "\n[MODO PLAN] Apenas analise.";
|
|
277
|
-
else if (currentMode === 'edit') modeInstr = `\n[MODO EDIT] Auto-Accept: ${editState.autoAccept ? 'ON' : 'OFF'}`;
|
|
278
|
-
|
|
279
|
-
const processedContent = [];
|
|
280
|
-
const words = rawInput.split(' ');
|
|
281
|
-
for (const word of words) {
|
|
282
|
-
if (word.startsWith('@')) {
|
|
283
|
-
const filePath = word.slice(1);
|
|
284
|
-
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
285
|
-
processedContent.push({ type: 'text', text: `\n[ARQUIVO: ${filePath}]\n${fs.readFileSync(filePath, 'utf-8')}\n` });
|
|
286
|
-
} else { processedContent.push({ type: 'text', text: word }); }
|
|
287
|
-
} else { processedContent.push({ type: 'text', text: word }); }
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
messages.push({ role: 'user', content: [...processedContent, { type: 'text', text: modeInstr }] });
|
|
291
|
-
const spinner = ora({ text: lavender(`${t.thinking} (Ctrl+C para parar)`), color: currentMode === 'edit' ? 'red' : 'magenta' }).start();
|
|
292
|
-
|
|
293
|
-
try {
|
|
294
|
-
let responseText = await provider.sendMessage(messages, { signal: controller.signal });
|
|
295
|
-
spinner.stop();
|
|
296
|
-
console.log(`\n${lavender('bimmo ')}${currentMode.toUpperCase()}`);
|
|
297
|
-
console.log(lavender('─'.repeat(50)));
|
|
298
|
-
process.stdout.write(marked.parse(cleanAIResponse(responseText)));
|
|
299
|
-
console.log(gray('\n' + '─'.repeat(50)));
|
|
300
|
-
messages.push({ role: 'assistant', content: responseText });
|
|
301
|
-
} catch (err) {
|
|
302
|
-
spinner.stop();
|
|
303
|
-
if (controller.signal.aborted) { console.log(yellow(`\n⚠️ ${t.interrupted}`)); messages.pop(); }
|
|
304
|
-
else { console.error(chalk.red(`\n✖ Erro: ${err.message}`)); }
|
|
305
|
-
} finally {
|
|
306
|
-
process.removeListener('SIGINT', abortHandler);
|
|
307
|
-
displayPrompt();
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
}
|