bimmo-cli 4.0.0 → 4.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.
Files changed (3) hide show
  1. package/bin/bimmo +22 -3
  2. package/package.json +10 -1
  3. package/src/interface.js +112 -80
package/bin/bimmo CHANGED
@@ -4,14 +4,33 @@
4
4
  process.removeAllListeners('warning');
5
5
 
6
6
  import { program } from 'commander';
7
- import { startInteractive } from '../src/interface.js';
8
- import { configure } from '../src/config.js';
9
7
  import fs from 'fs';
10
8
  import path from 'path';
11
- import { fileURLToPath } from 'url';
9
+ import { fileURLToPath, pathToFileURL } from 'url';
12
10
 
13
11
  const __filename = fileURLToPath(import.meta.url);
14
12
  const __dirname = path.dirname(__filename);
13
+
14
+ // Tenta carregar do dist primeiro (produção), depois do src (desenvolvimento)
15
+ let interfaceModule;
16
+ const distPath = path.join(__dirname, '../dist/interface.js');
17
+ const srcPath = path.join(__dirname, '../src/interface.js');
18
+
19
+ if (fs.existsSync(distPath)) {
20
+ interfaceModule = await import(pathToFileURL(distPath).href);
21
+ } else {
22
+ interfaceModule = await import(pathToFileURL(srcPath).href);
23
+ }
24
+
25
+ const { startInteractive } = interfaceModule;
26
+
27
+ const configPath = fs.existsSync(path.join(__dirname, '../dist/config.js'))
28
+ ? path.join(__dirname, '../dist/config.js')
29
+ : path.join(__dirname, '../src/config.js');
30
+
31
+ const configModule = await import(pathToFileURL(configPath).href);
32
+
33
+ const { configure } = configModule;
15
34
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
16
35
 
17
36
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimmo-cli",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "🌿 Plataforma de IA universal profissional com interface React (Ink) pura.",
5
5
  "bin": {
6
6
  "bimmo": "bin/bimmo"
@@ -24,6 +24,15 @@
24
24
  "publishConfig": {
25
25
  "access": "public"
26
26
  },
27
+ "scripts": {
28
+ "build": "rimraf dist && esbuild src/*.js src/**/*.js --format=esm --outdir=dist --platform=node --target=node20",
29
+ "dev": "node bin/bimmo",
30
+ "start": "npm run build && node bin/bimmo"
31
+ },
32
+ "devDependencies": {
33
+ "esbuild": "^0.24.0",
34
+ "rimraf": "^6.0.1"
35
+ },
27
36
  "dependencies": {
28
37
  "@anthropic-ai/sdk": "^0.36.3",
29
38
  "@google/generative-ai": "^0.21.0",
package/src/interface.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import React, { useState, useEffect, useMemo } from 'react';
2
- import { render, Box, Text, useInput, useApp } from 'ink';
2
+ import { render, Box, Text, useInput, useApp, Static } from 'ink';
3
3
  import TextInput from 'ink-text-input';
4
4
  import Spinner from 'ink-spinner';
5
+ import Divider from 'ink-divider';
5
6
  import chalk from 'chalk';
6
7
  import figlet from 'figlet';
7
8
  import { marked } from 'marked';
@@ -16,16 +17,20 @@ import { getProjectContext } from './project-context.js';
16
17
  import { editState } from './agent.js';
17
18
  import { SwarmOrchestrator } from './orchestrator.js';
18
19
 
19
- const green = '#00ff9d';
20
- const lavender = '#c084fc';
21
- const gray = '#6272a4';
22
- const yellow = '#f1fa8c';
23
- const red = '#ff5555';
24
- const cyan = '#8be9fd';
20
+ // Cores e Temas
21
+ const THEME = {
22
+ green: '#00ff9d',
23
+ lavender: '#c084fc',
24
+ gray: '#6272a4',
25
+ yellow: '#f1fa8c',
26
+ red: '#ff5555',
27
+ cyan: '#8be9fd',
28
+ border: '#44475a'
29
+ };
25
30
 
26
31
  marked.use(new TerminalRenderer({
27
- heading: chalk.hex(lavender).bold,
28
- code: chalk.hex(green),
32
+ heading: chalk.hex(THEME.lavender).bold,
33
+ code: chalk.hex(THEME.green),
29
34
  strong: chalk.bold,
30
35
  em: chalk.italic,
31
36
  html: () => '',
@@ -33,74 +38,88 @@ marked.use(new TerminalRenderer({
33
38
 
34
39
  const __filename = fileURLToPath(import.meta.url);
35
40
  const __dirname = path.dirname(__filename);
36
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
41
+ // A localização do package.json depende de onde este arquivo está (src ou dist)
42
+ const pkgPath = fs.existsSync(path.join(__dirname, '../package.json'))
43
+ ? path.join(__dirname, '../package.json')
44
+ : path.join(__dirname, '../../package.json');
45
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
37
46
  const version = pkg.version;
38
47
 
39
48
  const h = React.createElement;
40
49
 
50
+ // --- COMPONENTES ---
51
+
41
52
  const Header = ({ config }) => (
42
53
  h(Box, { flexDirection: 'column', marginBottom: 1 },
43
- h(Text, { color: lavender }, figlet.textSync('bimmo')),
44
- h(Box, { borderStyle: 'single', borderColor: lavender, paddingX: 1, justifyContent: 'space-between' },
45
- h(Text, { color: green, bold: true }, `v${version}`),
54
+ h(Text, { color: THEME.lavender, bold: true }, figlet.textSync('bimmo', { font: 'Small' })),
55
+ h(Box, { borderStyle: 'round', borderColor: THEME.border, paddingX: 1, justifyContent: 'space-between' },
56
+ h(Text, { color: THEME.green }, `v${version}`),
46
57
  h(Box, null,
47
- h(Text, { color: gray }, `${config.activeProfile || 'Default'} `),
48
- h(Text, { color: lavender }, '•'),
49
- h(Text, { color: gray }, ` ${config.model}`)
58
+ h(Text, { color: THEME.gray }, `${config.activeProfile || 'Default'} `),
59
+ h(Text, { color: THEME.lavender }, '•'),
60
+ h(Text, { color: THEME.cyan }, ` ${config.model}`)
50
61
  )
51
62
  )
52
63
  )
53
64
  );
54
65
 
55
- const MessageList = ({ messages }) => (
56
- h(Box, { flexDirection: 'column', flexGrow: 1 },
57
- messages.filter(m => m.role !== 'system').slice(-10).map((m, i) => (
58
- h(Box, { key: i, flexDirection: 'column', marginBottom: 1 },
59
- h(Box, null,
60
- h(Text, { bold: true, color: m.role === 'user' ? green : lavender },
61
- m.role === 'user' ? '› Você' : '› bimmo'
62
- ),
63
- m.role === 'system' && h(Text, { color: yellow }, ' [SISTEMA]')
64
- ),
65
- h(Box, { paddingLeft: 2 },
66
- h(Text, null,
67
- m.role === 'assistant'
68
- ? marked.parse(m.content).trim()
69
- : (m.displayContent || m.content)
70
- )
71
- )
66
+ const Message = ({ role, content, displayContent }) => {
67
+ const isUser = role === 'user';
68
+ const color = isUser ? THEME.green : THEME.lavender;
69
+ const label = isUser ? '› VOCÊ' : '› bimmo';
70
+
71
+ return h(Box, { flexDirection: 'column', marginBottom: 1 },
72
+ h(Box, null,
73
+ h(Text, { color, bold: true }, label),
74
+ role === 'system' && h(Text, { color: THEME.yellow }, ' [SISTEMA]')
75
+ ),
76
+ h(Box, { paddingLeft: 2 },
77
+ h(Text, null,
78
+ role === 'assistant'
79
+ ? marked.parse(content).trim()
80
+ : (displayContent || content)
72
81
  )
73
- ))
74
- )
75
- );
82
+ )
83
+ );
84
+ };
76
85
 
77
- const Autocomplete = ({ suggestions }) => (
78
- h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: gray, paddingX: 1, marginBottom: 1 },
79
- h(Text, { color: gray, dimColor: true, italic: true }, 'Sugestões (TAB para completar):'),
86
+ const AutocompleteSuggestions = ({ suggestions }) => (
87
+ h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: THEME.border, paddingX: 1, marginBottom: 1 },
88
+ h(Text, { color: THEME.gray, dimColor: true, italic: true }, 'Sugestões (TAB):'),
80
89
  suggestions.map((f, i) => (
81
- h(Text, { key: i, color: i === 0 ? green : gray },
90
+ h(Text, { key: i, color: i === 0 ? THEME.green : THEME.gray },
82
91
  `${f.isDir ? '📁' : '📄'} ${f.rel}${f.isDir ? '/' : ''}`
83
92
  )
84
93
  ))
85
94
  )
86
95
  );
87
96
 
88
- const Footer = ({ exitCounter }) => (
89
- h(Box, { marginTop: 1, justifyContent: 'space-between', paddingX: 1 },
90
- h(Text, { color: gray, dimColor: true }, `📁 ${path.relative(process.env.HOME || '', process.cwd())}`),
91
- exitCounter === 1 && h(Text, { color: yellow, bold: true }, ' Pressione Ctrl+C novamente para sair'),
92
- h(Box, null,
93
- h(Text, { color: gray, dimColor: true, italic: true }, '↑↓ para histórico • /help para comandos')
97
+ const FooterStatus = ({ mode, activePersona, exitCounter }) => (
98
+ h(Box, { marginTop: 1, flexDirection: 'column' },
99
+ h(Divider, { borderColor: THEME.border }),
100
+ h(Box, { justifyContent: 'space-between', paddingX: 1 },
101
+ h(Box, null,
102
+ h(Text, { color: THEME.gray }, `Modo: `),
103
+ h(Text, { color: mode === 'edit' ? THEME.red : mode === 'plan' ? THEME.cyan : THEME.lavender, bold: true }, mode.toUpperCase()),
104
+ activePersona && h(Text, { color: THEME.yellow }, ` (${activePersona})`)
105
+ ),
106
+ h(Text, { color: THEME.gray, dimColor: true }, `📁 ${path.basename(process.cwd())}`),
107
+ exitCounter === 1
108
+ ? h(Text, { color: THEME.yellow, bold: true }, ' Pressione Ctrl+C novamente para sair ')
109
+ : h(Text, { color: THEME.gray, italic: true }, ' /help para comandos ')
94
110
  )
95
111
  )
96
112
  );
97
113
 
114
+ // --- APP PRINCIPAL ---
115
+
98
116
  const BimmoApp = ({ initialConfig }) => {
99
117
  const { exit } = useApp();
100
118
  const [config, setConfig] = useState(initialConfig);
101
119
  const [mode, setMode] = useState('chat');
102
120
  const [activePersona, setActivePersona] = useState(null);
103
121
  const [messages, setMessages] = useState([]);
122
+ const [staticMessages, setStaticMessages] = useState([]); // Para mensagens antigas
104
123
  const [input, setInput] = useState('');
105
124
  const [isThinking, setIsThinking] = useState(false);
106
125
  const [thinkingMessage, setThinkingMessage] = useState('bimmo pensando...');
@@ -144,8 +163,10 @@ const BimmoApp = ({ initialConfig }) => {
144
163
  const parts = rawInput.split(' ');
145
164
  const cmd = parts[0].toLowerCase();
146
165
 
166
+ // Comandos de Sistema
147
167
  if (cmd === '/exit') exit();
148
168
  if (cmd === '/clear') {
169
+ setStaticMessages([]);
149
170
  setMessages([{ role: 'system', content: getProjectContext() }, { role: 'assistant', content: 'Chat limpo.' }]);
150
171
  return;
151
172
  }
@@ -190,28 +211,12 @@ const BimmoApp = ({ initialConfig }) => {
190
211
  return;
191
212
  }
192
213
 
193
- if (cmd === '/swarm') {
194
- const orchestrator = new SwarmOrchestrator(config);
195
- setIsThinking(true);
196
- setThinkingMessage('Enxame em ação...');
197
- try {
198
- let response;
199
- if (parts[1] === 'seq') response = await orchestrator.runSequential(parts[2].split(','), parts.slice(3).join(' '));
200
- if (parts[1] === 'run') response = await orchestrator.runHierarchical(parts[2], parts[3].split(','), parts.slice(4).join(' '));
201
- setMessages(prev => [...prev, { role: 'user', content: rawInput }, { role: 'assistant', content: response }]);
202
- } catch (err) {
203
- setMessages(prev => [...prev, { role: 'system', content: `Erro no enxame: ${err.message}` }]);
204
- } finally {
205
- setIsThinking(false);
206
- }
207
- return;
208
- }
209
-
210
214
  if (cmd === '/help') {
211
- setMessages(prev => [...prev, { role: 'assistant', content: `**Comandos:** /chat, /plan, /edit, /switch, /model, /use, /swarm, /clear, /exit, @arquivo` }]);
215
+ setMessages(prev => [...prev, { role: 'assistant', content: `**Comandos Disponíveis:**\n\n- \`/chat\`, \`/plan\`, \`/edit\`: Alternar modos\n- \`/model <nome>\`: Trocar modelo atual\n- \`/switch <perfil>\`: Trocar perfil de API\n- \`/use <agente>\`: Usar agente especializado\n- \`/clear\`: Limpar histórico\n- \`/exit\`: Sair do bimmo\n- \`@arquivo\`: Referenciar arquivo local` }]);
212
216
  return;
213
217
  }
214
218
 
219
+ // Processamento de arquivos
215
220
  setIsThinking(true);
216
221
  let processedInput = rawInput;
217
222
  const fileMatches = rawInput.match(/@[\w\.\-\/]+/g);
@@ -228,15 +233,22 @@ const BimmoApp = ({ initialConfig }) => {
228
233
  }
229
234
 
230
235
  const userMsg = { role: 'user', content: processedInput, displayContent: rawInput };
231
- const newMessages = [...messages, userMsg];
232
- setMessages(newMessages);
236
+
237
+ // Move mensagens antigas para static para performance
238
+ if (messages.length > 5) {
239
+ setStaticMessages(prev => [...prev, ...messages.slice(0, -5)]);
240
+ setMessages(prev => [...prev.slice(-5), userMsg]);
241
+ } else {
242
+ setMessages(prev => [...prev, userMsg]);
243
+ }
233
244
 
234
245
  try {
235
- let finalMessages = newMessages;
246
+ let finalMessages = [...staticMessages, ...messages, userMsg];
236
247
  if (activePersona && config.agents[activePersona]) {
237
248
  const agent = config.agents[activePersona];
238
- finalMessages = [{ role: 'system', content: `Sua tarefa: ${agent.role}\n\n${getProjectContext()}` }, ...newMessages.filter(m => m.role !== 'system')];
249
+ finalMessages = [{ role: 'system', content: `Sua tarefa: ${agent.role}\n\n${getProjectContext()}` }, ...finalMessages.filter(m => m.role !== 'system')];
239
250
  }
251
+
240
252
  const response = await provider.sendMessage(finalMessages);
241
253
  setMessages(prev => [...prev, { role: 'assistant', content: response }]);
242
254
  } catch (err) {
@@ -250,8 +262,12 @@ const BimmoApp = ({ initialConfig }) => {
250
262
  if (key.ctrl && input === 'c') {
251
263
  if (isThinking) setIsThinking(false);
252
264
  else {
253
- if (exitCounter === 0) { setExitCounter(1); setTimeout(() => setExitCounter(0), 2000); }
254
- else exit();
265
+ if (exitCounter === 0) {
266
+ setExitCounter(1);
267
+ setTimeout(() => setExitCounter(0), 2000);
268
+ } else {
269
+ exit();
270
+ }
255
271
  }
256
272
  }
257
273
  if (key.tab && filePreview.length > 0) {
@@ -262,23 +278,36 @@ const BimmoApp = ({ initialConfig }) => {
262
278
  });
263
279
 
264
280
  return (
265
- h(Box, { flexDirection: 'column', paddingX: 1, minHeight: 10 },
281
+ h(Box, { flexDirection: 'column', paddingX: 2, paddingY: 1, minHeight: 15 },
266
282
  h(Header, { config }),
267
- h(MessageList, { messages }),
283
+
284
+ h(Box, { flexDirection: 'column', flexGrow: 1, marginBottom: 1 },
285
+ h(Static, { items: staticMessages }, (m, i) => h(Message, { key: `static-${i}`, ...m })),
286
+ messages.filter(m => m.role !== 'system').map((m, i) => h(Message, { key: i, ...m }))
287
+ ),
288
+
268
289
  isThinking && h(Box, { marginBottom: 1 },
269
- h(Text, { color: lavender },
290
+ h(Text, { color: THEME.lavender },
270
291
  h(Spinner, { type: 'dots' }),
271
292
  h(Text, { italic: true }, ` ${thinkingMessage}`)
272
293
  )
273
294
  ),
274
- filePreview.length > 0 && h(Autocomplete, { suggestions: filePreview }),
275
- h(Box, { borderStyle: 'round', borderColor: isThinking ? gray : lavender, paddingX: 1 },
276
- h(Text, { bold: true, color: mode === 'edit' ? red : mode === 'plan' ? cyan : lavender },
277
- `${activePersona ? `[${activePersona.toUpperCase()}] ` : ''}[${mode.toUpperCase()}] `
295
+
296
+ filePreview.length > 0 && h(AutocompleteSuggestions, { suggestions: filePreview }),
297
+
298
+ h(Box, { borderStyle: 'round', borderColor: isThinking ? THEME.gray : THEME.lavender, paddingX: 1 },
299
+ h(Text, { bold: true, color: mode === 'edit' ? THEME.red : mode === 'plan' ? THEME.cyan : THEME.lavender },
300
+ `${activePersona ? `[${activePersona.toUpperCase()}] ` : ''}› `
278
301
  ),
279
- h(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, placeholder: 'Como posso ajudar hoje?' })
302
+ h(TextInput, {
303
+ value: input,
304
+ onChange: setInput,
305
+ onSubmit: handleSubmit,
306
+ placeholder: 'Diga algo ou use / para comandos...'
307
+ })
280
308
  ),
281
- h(Footer, { exitCounter })
309
+
310
+ h(FooterStatus, { mode, activePersona, exitCounter })
282
311
  )
283
312
  );
284
313
  };
@@ -286,9 +315,12 @@ const BimmoApp = ({ initialConfig }) => {
286
315
  export async function startInteractive() {
287
316
  const config = getConfig();
288
317
  if (!config.provider || !config.apiKey) {
289
- console.log(chalk.yellow('Provedor não configurado. Execute "bimmo config" primeiro.'));
318
+ console.log(chalk.yellow('\n⚠️ Provedor não configurado.'));
319
+ console.log(chalk.gray('Execute "bimmo config" para configurar sua chave de API.\n'));
290
320
  process.exit(0);
291
321
  }
322
+
323
+ // Limpa tela e inicia
292
324
  process.stdout.write('\x1Bc');
293
325
  render(h(BimmoApp, { initialConfig: config }));
294
326
  }