bimmo-cli 4.0.0 → 4.0.2

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