@rabts/cli 1.0.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rabts/cli",
3
- "version": "1.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Rabts Studio CLI",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -15,9 +15,13 @@
15
15
  "@google/genai": "^2.9.0",
16
16
  "axios": "^1.7.2",
17
17
  "chalk": "^5.3.0",
18
+ "cheerio": "^1.2.0",
18
19
  "commander": "^12.1.0",
19
20
  "dotenv": "^16.4.5",
20
21
  "inquirer": "^10.0.0",
21
- "openai": "^4.52.0"
22
+ "marked": "^15.0.12",
23
+ "marked-terminal": "^7.3.0",
24
+ "openai": "^4.52.0",
25
+ "ora": "^9.4.1"
22
26
  }
23
27
  }
package/src/agent.js CHANGED
@@ -4,12 +4,22 @@ import { exec } from 'child_process';
4
4
  import util from 'util';
5
5
  import chalk from 'chalk';
6
6
  import inquirer from 'inquirer';
7
+ import ora from 'ora';
8
+ import * as cheerio from 'cheerio';
9
+ import axios from 'axios';
10
+ import { Marked } from 'marked';
11
+ import { markedTerminal } from 'marked-terminal';
7
12
  import { sendMessage } from './llm.js';
13
+ import { loadSessionMemory, saveSessionMemory, clearSessionMemory } from './memory.js';
14
+
15
+ // Setup Markdown Renderer
16
+ const marked = new Marked();
17
+ marked.use(markedTerminal());
8
18
 
9
19
  const execPromise = util.promisify(exec);
10
20
 
11
21
  // Tema Rabts
12
- const themeColor = chalk.cyan; // Azul claro para padronizar
22
+ const themeColor = chalk.cyan;
13
23
  const infoColor = chalk.gray;
14
24
 
15
25
  const tools = [
@@ -51,14 +61,39 @@ const tools = [
51
61
  required: ["command"]
52
62
  }
53
63
  }
64
+ },
65
+ {
66
+ type: "function",
67
+ function: {
68
+ name: "search_code",
69
+ description: "Faz uma busca global por texto (Grep) nos arquivos do projeto.",
70
+ parameters: {
71
+ type: "object",
72
+ properties: { query: { type: "string", description: "O que pesquisar" } },
73
+ required: ["query"]
74
+ }
75
+ }
76
+ },
77
+ {
78
+ type: "function",
79
+ function: {
80
+ name: "read_url",
81
+ description: "Lê e extrai texto limpo de uma página web ou documentação.",
82
+ parameters: {
83
+ type: "object",
84
+ properties: { url: { type: "string" } },
85
+ required: ["url"]
86
+ }
87
+ }
54
88
  }
55
89
  ];
56
90
 
57
- async function executeToolCall(toolCall) {
91
+ async function executeToolCall(toolCall, spinner) {
58
92
  const args = JSON.parse(toolCall.function.arguments);
59
93
  const cwd = process.cwd();
60
94
 
61
95
  if (toolCall.function.name === 'read_file') {
96
+ spinner.text = `Lendo arquivo: ${args.filepath}`;
62
97
  try {
63
98
  const fullPath = path.resolve(cwd, args.filepath);
64
99
  return fs.readFileSync(fullPath, 'utf8');
@@ -68,11 +103,11 @@ async function executeToolCall(toolCall) {
68
103
  }
69
104
 
70
105
  if (toolCall.function.name === 'write_file') {
106
+ spinner.text = `Salvando arquivo: ${args.filepath}`;
71
107
  try {
72
108
  const fullPath = path.resolve(cwd, args.filepath);
73
109
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });
74
110
  fs.writeFileSync(fullPath, args.content, 'utf8');
75
- console.log(themeColor(` ↳ Arquivo salvo: `) + chalk.white(args.filepath));
76
111
  return `Arquivo salvo em ${fullPath}`;
77
112
  } catch (e) {
78
113
  return `Erro ao salvar arquivo: ${e.message}`;
@@ -80,6 +115,8 @@ async function executeToolCall(toolCall) {
80
115
  }
81
116
 
82
117
  if (toolCall.function.name === 'run_command') {
118
+ // Para perguntar ao usuário, precisamos pausar o spinner
119
+ spinner.stop();
83
120
  const { confirm } = await inquirer.prompt([{
84
121
  type: 'confirm',
85
122
  name: 'confirm',
@@ -87,10 +124,13 @@ async function executeToolCall(toolCall) {
87
124
  default: false
88
125
  }]);
89
126
 
90
- if (!confirm) return `Usuário negou o comando: ${args.command}`;
127
+ if (!confirm) {
128
+ spinner.start('Retomando...');
129
+ return `Usuário negou o comando: ${args.command}`;
130
+ }
91
131
 
132
+ spinner.start(`Executando comando: ${args.command}`);
92
133
  try {
93
- console.log(themeColor(` ↳ Executando: `) + chalk.white(args.command));
94
134
  const { stdout, stderr } = await execPromise(args.command, { cwd });
95
135
  return stdout || stderr || "Sucesso sem output.";
96
136
  } catch (e) {
@@ -98,19 +138,75 @@ async function executeToolCall(toolCall) {
98
138
  }
99
139
  }
100
140
 
141
+ if (toolCall.function.name === 'search_code') {
142
+ spinner.text = `Buscando globalmente por: ${args.query}`;
143
+ try {
144
+ // Uso do grep recursivo ignorando node_modules
145
+ const cmd = `grep -rn --exclude-dir=node_modules --exclude-dir=.git "${args.query}" . || true`;
146
+ const { stdout } = await execPromise(cmd, { cwd });
147
+ if (!stdout.trim()) return "Nenhum resultado encontrado.";
148
+ // Limita resultados para não estourar tokens
149
+ return stdout.substring(0, 10000);
150
+ } catch (e) {
151
+ return `Erro ao buscar: ${e.message}`;
152
+ }
153
+ }
154
+
155
+ if (toolCall.function.name === 'read_url') {
156
+ spinner.text = `Acessando URL: ${args.url}`;
157
+ try {
158
+ const response = await axios.get(args.url);
159
+ const $ = cheerio.load(response.data);
160
+ // Remove scripts, styles
161
+ $('script, style, noscript, svg, img, nav, footer').remove();
162
+ const text = $('body').text().replace(/\s+/g, ' ').trim();
163
+ return text.substring(0, 15000); // Retorna até 15k chars
164
+ } catch (e) {
165
+ return `Erro ao acessar URL: ${e.message}`;
166
+ }
167
+ }
168
+
101
169
  return "Função desconhecida.";
102
170
  }
103
171
 
172
+ function getProjectContext() {
173
+ const cwd = process.cwd();
174
+ let contextStr = `\nContexto do Diretório Atual:\n- Caminho: ${cwd}\n`;
175
+
176
+ try {
177
+ const files = fs.readdirSync(cwd).slice(0, 50);
178
+ contextStr += `- Arquivos na raiz: ${files.join(', ')}\n`;
179
+
180
+ if (fs.existsSync(path.join(cwd, 'package.json'))) {
181
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
182
+ contextStr += `- Projeto Node.js: ${pkg.name || 'Desconhecido'}\n`;
183
+ }
184
+
185
+ if (fs.existsSync(path.join(cwd, 'composer.json'))) {
186
+ const comp = JSON.parse(fs.readFileSync(path.join(cwd, 'composer.json'), 'utf8'));
187
+ contextStr += `- Projeto PHP (Composer): ${comp.name || 'Desconhecido'}\n`;
188
+ }
189
+ } catch (e) {}
190
+ return contextStr;
191
+ }
192
+
104
193
  export async function startInteractiveLoop(config) {
105
194
  console.log(infoColor(`\nConectado via ${config.provider} (Modelo: ${config.model})`));
106
- console.log(infoColor('Você pode conversar ou pedir para ler/criar arquivos. Digite "sair" para encerrar.\n'));
195
+ console.log(infoColor('Dicas: Digite "sair" para encerrar. Digite "/clear" para limpar a memória desta pasta.\n'));
107
196
 
108
- let messages = [
109
- {
110
- role: 'system',
111
- content: 'Você é o Rabts AI, um assistente de terminal. Você pode conversar com o usuário ou usar ferramentas para manipular arquivos e comandos quando requisitado.'
112
- }
113
- ];
197
+ let messages = loadSessionMemory();
198
+ const isNewSession = messages.length === 0;
199
+
200
+ if (isNewSession) {
201
+ let systemContent = 'Você é o Rabts AI, um desenvolvedor e assistente de terminal altamente inteligente capaz de navegar pela internet e gerenciar código autonomamente. Responda usando Markdown limpo.';
202
+ systemContent += getProjectContext();
203
+
204
+ messages = [
205
+ { role: 'system', content: systemContent }
206
+ ];
207
+ } else {
208
+ console.log(chalk.green('✓ Sessão anterior restaurada para este projeto.\n'));
209
+ }
114
210
 
115
211
  const supportsTools = (config.provider === 'rabts' || config.provider === 'openai');
116
212
 
@@ -123,28 +219,45 @@ export async function startInteractiveLoop(config) {
123
219
  }
124
220
  ]);
125
221
 
126
- if (userPrompt.toLowerCase() === 'sair' || userPrompt.toLowerCase() === 'exit') {
222
+ const input = userPrompt.trim();
223
+
224
+ if (input.toLowerCase() === 'sair' || input.toLowerCase() === 'exit') {
127
225
  console.log(infoColor('Até logo!'));
128
226
  break;
129
227
  }
228
+
229
+ if (input.toLowerCase() === '/clear') {
230
+ clearSessionMemory();
231
+ console.log(chalk.yellow('Memória limpa! O contexto do projeto será reiniciado na próxima mensagem.\n'));
232
+ messages = [];
233
+ continue;
234
+ }
130
235
 
131
- if (!userPrompt.trim()) continue;
236
+ if (!input) continue;
132
237
 
133
- messages.push({ role: 'user', content: userPrompt });
238
+ if (messages.length === 0) {
239
+ messages.push({
240
+ role: 'system',
241
+ content: 'Você é o Rabts AI, um desenvolvedor autônomo. ' + getProjectContext()
242
+ });
243
+ }
244
+
245
+ messages.push({ role: 'user', content: input });
134
246
 
135
247
  let isProcessingTools = true;
136
248
  let totalPromptTokens = 0;
137
249
  let totalCompletionTokens = 0;
138
250
  let startTime = Date.now();
139
251
 
252
+ const spinner = ora({
253
+ text: 'Pensando...',
254
+ color: 'cyan',
255
+ spinner: 'dots'
256
+ }).start();
257
+
140
258
  while (isProcessingTools) {
141
- process.stdout.write(infoColor(' Pensando...'));
142
-
143
259
  const responseMessage = await sendMessage(config, messages, supportsTools ? tools : null);
144
260
 
145
- process.stdout.clearLine(0);
146
- process.stdout.cursorTo(0);
147
-
148
261
  if (responseMessage.usage) {
149
262
  totalPromptTokens += responseMessage.usage.prompt_tokens || 0;
150
263
  totalCompletionTokens += responseMessage.usage.completion_tokens || 0;
@@ -158,8 +271,9 @@ export async function startInteractiveLoop(config) {
158
271
 
159
272
  if (responseMessage.tool_calls && responseMessage.tool_calls.length > 0) {
160
273
  for (const toolCall of responseMessage.tool_calls) {
161
- console.log(themeColor(` Usando ferramenta: `) + chalk.white(toolCall.function.name));
162
- const result = await executeToolCall(toolCall);
274
+ spinner.text = `Analisando ferramenta: ${toolCall.function.name}`;
275
+ const result = await executeToolCall(toolCall, spinner);
276
+
163
277
  messages.push({
164
278
  tool_call_id: toolCall.id,
165
279
  role: "tool",
@@ -167,15 +281,19 @@ export async function startInteractiveLoop(config) {
167
281
  content: result,
168
282
  });
169
283
  }
284
+ spinner.text = 'Processando resultados...';
170
285
  } else {
286
+ spinner.stop();
171
287
  const duration = ((Date.now() - startTime) / 1000).toFixed(1);
172
288
 
173
289
  if (responseMessage.content) {
174
290
  console.log(themeColor('\nRabts:'));
175
- console.log(chalk.white(responseMessage.content));
291
+ console.log(marked.parse(responseMessage.content).trim());
176
292
  }
177
293
 
178
- console.log(infoColor(`\n ↳ Finalizado em ${duration}s • ${totalPromptTokens + totalCompletionTokens} tokens (${totalPromptTokens} in, ${totalCompletionTokens} out)\n`));
294
+ console.log(infoColor(`\n ↳ Finalizado em ${duration}s • ${totalPromptTokens + totalCompletionTokens} tokens\n`));
295
+
296
+ saveSessionMemory(messages);
179
297
  isProcessingTools = false;
180
298
  }
181
299
  }
package/src/config.js CHANGED
@@ -6,7 +6,7 @@ import chalk from 'chalk';
6
6
 
7
7
  const CONFIG_FILE_PATH = path.join(os.homedir(), '.rabts-config.json');
8
8
 
9
- const RABTS_DEFAULT_KEY = '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';
9
+ // A chave padrão não fica mais no código-fonte por segurança
10
10
 
11
11
  export function getConfig() {
12
12
  if (fs.existsSync(CONFIG_FILE_PATH)) {
@@ -47,7 +47,8 @@ export async function configureApiKey() {
47
47
  let config = { provider };
48
48
 
49
49
  if (provider === 'rabts') {
50
- config.apiKey = RABTS_DEFAULT_KEY;
50
+ // A chave é injetada via proxy no servidor backend
51
+ config.apiKey = 'proxy_mode';
51
52
  const { model } = await inquirer.prompt([
52
53
  {
53
54
  type: 'list',
package/src/llm.js CHANGED
@@ -10,8 +10,8 @@ export function getClient(config) {
10
10
 
11
11
  if (config.provider === 'rabts') {
12
12
  clients[config.provider] = new OpenAI({
13
- baseURL: 'https://api.groq.com/openai/v1',
14
- apiKey: config.apiKey,
13
+ baseURL: 'https://dev.rabts.com/api',
14
+ apiKey: 'proxy_mode',
15
15
  });
16
16
  } else if (config.provider === 'openai') {
17
17
  clients[config.provider] = new OpenAI({
package/src/memory.js ADDED
@@ -0,0 +1,46 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export function getMemoryPath() {
5
+ const cwd = process.cwd();
6
+ return path.join(cwd, '.rabts', 'session.json');
7
+ }
8
+
9
+ export function loadSessionMemory() {
10
+ const memPath = getMemoryPath();
11
+ if (fs.existsSync(memPath)) {
12
+ try {
13
+ const data = fs.readFileSync(memPath, 'utf8');
14
+ return JSON.parse(data);
15
+ } catch (e) {
16
+ return [];
17
+ }
18
+ }
19
+ return [];
20
+ }
21
+
22
+ export function saveSessionMemory(messages) {
23
+ const memPath = getMemoryPath();
24
+ const dirPath = path.dirname(memPath);
25
+
26
+ if (!fs.existsSync(dirPath)) {
27
+ fs.mkdirSync(dirPath, { recursive: true });
28
+ }
29
+
30
+ // Ocultar a pasta no Windows/Linux de forma simples
31
+ // Adicionar um gitignore para que o usuário não commite acidentalmente o histórico
32
+ const gitignorePath = path.join(dirPath, '.gitignore');
33
+ if (!fs.existsSync(gitignorePath)) {
34
+ fs.writeFileSync(gitignorePath, "*\n");
35
+ }
36
+
37
+ // Filtrar o que salvar (não precisamos salvar todos os resultados longos de tools se não quisermos)
38
+ fs.writeFileSync(memPath, JSON.stringify(messages, null, 2), 'utf8');
39
+ }
40
+
41
+ export function clearSessionMemory() {
42
+ const memPath = getMemoryPath();
43
+ if (fs.existsSync(memPath)) {
44
+ fs.unlinkSync(memPath);
45
+ }
46
+ }