primocode 4.3.1 → 7.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/primocode.js CHANGED
@@ -1,14 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * PrimoCode v4.2 — Cliente Multi-Agente (Cross-Platform)
4
- * Primo Code: Engenharia completa com pipeline oculto de 5 agentes
5
- * Chat: Conversa direta
6
- *
7
- * REGRAS:
8
- * - IA NUNCA entrega código no CMD
9
- * - Sempre salva em arquivo e abre no navegador
10
- * - Memória de projetos em C:\\primocode\\ (ou ~/primocode/)
11
- * - Mouse virtual para controle completo da interface
3
+ * PrimoCode CLI v6.1 · ConectaPrimoAI
4
+ * Interface estilo Claude Code — minimalista, poderosa, elegante.
5
+ * Auto-instalação de Node.js com confirmação Y/n e barra de progresso.
6
+ * Suporte Termux (pkg install nodejs).
7
+ * Modos: primocode (agente completo, PADRÃO) · chat (conversa pura)
12
8
  */
13
9
 
14
10
  'use strict';
@@ -17,176 +13,89 @@ const readline = require('readline');
17
13
  const path = require('path');
18
14
  const fs = require('fs');
19
15
  const os = require('os');
20
- const { spawn, execSync } = require('child_process');
16
+ const { execSync, spawn } = require('child_process');
17
+ const pkg = require('../package.json');
21
18
 
22
- const { c, welcomeBox, Spinner } = require('../lib/ui');
19
+ const { c, welcomeBox, Spinner, ProgressBar, askYesNo } = require('../lib/ui');
23
20
  const api = require('../lib/api');
24
21
  const tools = require('../lib/tools');
25
22
  const cfg = require('../lib/config');
26
- const memory = require('../lib/memory');
27
- const mouse = require('../lib/mouse');
28
- const platform = require('../lib/platform');
23
+ const { runAct } = require('../lib/act');
24
+
25
+ const VERSION = pkg.version;
29
26
 
30
27
  const state = {
31
28
  config: cfg.loadConfig(),
32
29
  history: [],
33
- mode: 'Primo Code',
30
+ mode: 'primocode',
34
31
  attachedFiles: [],
35
32
  lastResponse: '',
36
- currentProject: null,
37
33
  };
38
34
 
39
35
  const HELP = `
40
36
  ${c.bold('Comandos')}
41
- /mode <nome> Primo Code | Chat
42
- /attach <arquivo> anexa arquivo
43
- /clear limpa conversa
44
- /cls limpa tela
45
- /uninstall remove PrimoCode
46
- /app <nome> abrir aplicativo
47
- /terminal abrir terminal
48
- /run <arquivo> executar arquivo (py, js, html)
49
- /screenshot capturar tela
50
- /mouse <x> <y> mover mouse para coordenadas
51
- /click <x> <y> clicar em coordenadas
52
- /search <termo> pesquisar na web
53
- /project mostrar projeto atual
54
- /projects listar projetos
55
- /help ajuda
56
- /exit sair
37
+ ${c.brand('/mode')} Primo Code / Chat
38
+ ${c.brand('/attach')} (Caminho do arquivo ou pasta) anexa arquivo
39
+ ${c.brand('/clear')} limpa conversa
40
+ ${c.brand('/cls')} limpa tela
41
+ ${c.brand('/uninstall')} remove PrimoCode
42
+ ${c.brand('/exit')} sair
57
43
 
58
44
  ${c.bold('Modos')}
59
- ${c.brand('Primo Code')} Engenharia completa (pipeline oculto)
60
- ${c.brand('Chat')} Conversa direta
45
+ ${c.magenta('Primo Code')} ${c.muted('Agente completo — cursor próprio, escreve código, controla desktop e navegador')}
46
+ ${c.brand('Chat')} ${c.muted('Conversa pura — sem tools, só diálogo')}
61
47
  `;
62
48
 
63
49
  let rl = null;
64
50
 
65
51
  function makePrompt() {
66
- const tag = state.mode === 'Primo Code' ? c.magenta('PrimoCode') : c.brand(state.mode);
52
+ const modeTag = state.mode === 'primocode'
53
+ ? c.magenta('Primo Code')
54
+ : c.brand('Chat');
67
55
  const att = state.attachedFiles.length ? c.muted(` +${state.attachedFiles.length}`) : '';
68
- const proj = state.currentProject ? c.muted(` [${state.currentProject}]`) : '';
69
- return `\n${c.muted(tag + att + proj)} ${c.brand('❯')} `;
56
+ return `\n${c.muted(modeTag + att)} ${c.brand('❯')} `;
70
57
  }
71
58
  function refreshPrompt() { if (rl) rl.setPrompt(makePrompt()); }
72
59
 
73
- // ---- Pesquisa Web (apenas comando /search) ----
74
- async function webSearch(query) {
75
- try {
76
- const result = platform.webSearch(query);
77
- await new Promise(r => setTimeout(r, 3000));
78
- return result;
79
- } catch (e) {
80
- return { ok: false, error: e.message };
60
+ async function sendPrompt(userText) {
61
+ let promptWithAttach = userText;
62
+ if (state.attachedFiles.length) {
63
+ const parts = state.attachedFiles.map(f => `Arquivo: ${f.path}\n\`\`\`\n${f.content}\n\`\`\``);
64
+ promptWithAttach = parts.join('\n\n') + '\n\n' + userText;
81
65
  }
82
- }
83
66
 
84
- // ---- Pipeline Multi-Agente Oculto ----
85
- async function runMultiAgent(userText) {
86
- console.log(c.muted(' 🚀 Gerando código...'));
87
- const sp = new Spinner('').start();
88
-
89
- try {
90
- // Pipeline multi-agente — SEM pesquisa web automática
91
- sp.update('Processando...');
92
-
93
- const result = await api.request(state.config.server, '/api/act', {
94
- prompt: userText,
95
- history: [],
96
- model: 'main',
97
- maxIterations: 3,
98
- projectPath: process.cwd(),
67
+ if (state.mode === 'agent') {
68
+ await runAgent(promptWithAttach, userText);
69
+ } else if (state.mode === 'act') {
70
+ const r = await runAct(promptWithAttach, {
71
+ server: state.config.server,
72
+ model: state.config.model,
73
+ history: state.history,
99
74
  });
100
-
101
- sp.stop();
102
-
103
- if (result.error) {
104
- // Silencia erro técnico, tenta modo chat
105
- await runChat(userText, userText);
106
- return;
107
- }
108
-
109
- const code = result.finalCode || result.response || '';
110
- state.lastResponse = code;
111
-
112
- // NUNCA mostrar código no CMD — sempre salvar em arquivo
113
- if (code && code.length > 0) {
114
- // Determinar nome do projeto
115
- let projectName = state.currentProject;
116
- if (!projectName) {
117
- const match = userText.match(/(?:crie|faça|desenvolva|build|make|create)\s+(?:um|uma|o|a)?\s*([\w\-]+)/i);
118
- projectName = match ? match[1] : `projeto-${Date.now()}`;
119
- state.currentProject = projectName;
120
- }
121
-
122
- // Criar projeto se não existir
123
- const project = memory.createProject(projectName);
124
-
125
- // Salvar código em arquivo
126
- const ext = code.includes('<!DOCTYPE html>') || code.includes('<html') ? '.html' :
127
- code.includes('import ') || code.includes('export ') ? '.js' :
128
- code.includes('def ') || code.includes('import ') ? '.py' : '.txt';
129
- const filename = `index${ext}`;
130
- const filePath = memory.saveProjectFile(projectName, filename, code);
131
-
132
- // Salvar também como output.html para fácil acesso
133
- const outputHtml = path.join(memory.PRIMOCODE_DIR, 'output.html');
134
- fs.writeFileSync(outputHtml, code, 'utf8');
135
-
136
- // Atualizar memória do projeto
137
- const projMem = memory.loadProjectMemory(projectName);
138
- const updatedMem = projMem + `\n## Código Gerado (${new Date().toLocaleString('pt-BR')})\n\`\`\`\n${code.substring(0, 500)}...\n\`\`\`\n`;
139
- memory.saveProjectMemory(projectName, updatedMem);
140
-
141
- console.log(c.ok(`\n ✓ Código salvo em: ${filePath}`));
142
-
143
- // Abrir no navegador se for HTML
144
- if (ext === '.html') {
145
- try {
146
- platform.openBrowser(filePath);
147
- console.log(c.muted(' 🌐 Abrindo no navegador...'));
148
- } catch (e) {
149
- // Silencia erro
150
- }
151
- } else {
152
- // Para outros arquivos, tenta executar
153
- try {
154
- if (ext === '.py') {
155
- execSync(`start cmd /k python "${filePath}"`, { stdio: 'ignore' });
156
- console.log(c.muted(' ▶ Executando Python...'));
157
- } else if (ext === '.js') {
158
- execSync(`start cmd /k node "${filePath}"`, { stdio: 'ignore' });
159
- console.log(c.muted(' ▶ Executando Node.js...'));
160
- }
161
- } catch (e) {
162
- console.log(c.muted(' 📄 Arquivo salvo (não executável automaticamente)'));
163
- }
164
- }
165
- }
166
-
167
- cfg.appendHistory({ mode: 'Primo Code', prompt: userText, response: code });
168
-
169
- } catch (err) {
170
- sp.stop();
171
- // Silencia erro de pipeline, tenta modo chat
172
- await runChat(userText, userText);
75
+ state.history.push({ role: 'user', content: userText });
76
+ state.history.push({ role: 'assistant', content: r.assistantMessage.content });
77
+ state.lastResponse = r.assistantMessage.content;
78
+ cfg.appendHistory({ mode: 'primocode', prompt: userText, response: r.assistantMessage.content });
79
+ } else {
80
+ await runChat(promptWithAttach, userText);
173
81
  }
82
+ state.attachedFiles = [];
83
+ refreshPrompt();
174
84
  }
175
85
 
176
86
  async function runChat(promptForModel, userTextForMemory) {
177
- const sp = new Spinner('').start();
178
-
87
+ const sp = new Spinner('pensando').start();
179
88
  let started = false;
180
89
  let text = '';
181
90
  try {
182
91
  await api.requestStream(state.config.server, '/api/normal/stream', {
183
92
  prompt: promptForModel,
184
93
  history: state.history.slice(-20),
185
- model: cfg.resolveModel(state.mode),
94
+ model: state.config.model,
186
95
  }, (evt) => {
187
96
  if (evt.erro) {
188
97
  if (!started) sp.stop();
189
- // Silencia erro de stream
98
+ console.log(c.err(`\nerro: ${evt.erro}`));
190
99
  return;
191
100
  }
192
101
  if (evt.delta) {
@@ -194,381 +103,267 @@ async function runChat(promptForModel, userTextForMemory) {
194
103
  text += evt.delta;
195
104
  process.stdout.write(c.white(evt.delta));
196
105
  }
197
- if (evt.done && started) process.stdout.write('\n');
106
+ if (evt.done) { if (!started) sp.stop(); process.stdout.write('\n'); }
198
107
  });
199
- } catch (err) {
108
+ } catch (e) {
200
109
  sp.stop();
201
- // Silencia erro de conexão completamente
202
- return;
203
- }
204
- if (!started) { sp.stop(); console.log(c.muted('\n ⚡ Resposta rápida indisponível. Use /mode Chat para conversar.')); return; }
205
-
206
- state.history.push({ role: 'user', content: userTextForMemory });
207
- state.history.push({ role: 'assistant', content: text });
208
- state.lastResponse = text;
209
- cfg.appendHistory({ mode: state.mode, prompt: userTextForMemory, response: text });
210
- }
211
-
212
- // ---- Detectar se é pedido de código ----
213
- function isCodeRequest(text) {
214
- const codeKeywords = [
215
- 'crie', 'criar', 'faça', 'fazer', 'desenvolva', 'desenvolver',
216
- 'build', 'make', 'create', 'generate', 'code', 'codigo',
217
- 'site', 'web', 'app', 'aplicativo', 'programa', 'script',
218
- 'calculadora', 'jogo', 'game', 'pagina', 'website',
219
- 'html', 'css', 'js', 'python', 'react', 'vue', 'angular',
220
- 'api', 'backend', 'frontend', 'database', 'banco de dados'
221
- ];
222
- const lower = text.toLowerCase();
223
- return codeKeywords.some(kw => lower.includes(kw));
224
- }
225
-
226
- // ---- Envio Principal ----
227
- async function sendPrompt(userText) {
228
- let promptWithAttach = userText;
229
- if (state.attachedFiles.length) {
230
- const parts = state.attachedFiles.map(f => `Arquivo: ${f.path}\n\`\`\`\n${f.content}\n\`\`\``);
231
- promptWithAttach = parts.join('\n\n') + '\n\n' + userText;
110
+ console.log(c.err(`\n${e.message || e}`));
232
111
  }
233
-
234
- // Adicionar contexto de memória
235
- const memContext = memory.loadMemory();
236
- if (memContext) {
237
- promptWithAttach = `[Memória do sistema: ${memContext.substring(0, 300)}]\n\n${promptWithAttach}`;
112
+ if (text) {
113
+ state.history.push({ role: 'user', content: userTextForMemory });
114
+ state.history.push({ role: 'assistant', content: text });
115
+ state.lastResponse = text;
116
+ cfg.appendHistory({ mode: 'chat', prompt: userTextForMemory, response: text });
238
117
  }
118
+ }
239
119
 
240
- if (state.mode === 'Primo Code') {
241
- // Se for conversa simples (oi, tudo bem, etc.), usar Chat direto
242
- if (!isCodeRequest(userText)) {
243
- await runChat(promptWithAttach, userText);
244
- } else {
245
- await runMultiAgent(promptWithAttach);
246
- }
247
- } else {
248
- await runChat(promptWithAttach, userText);
120
+ async function runAgent(promptForModel, userTextForMemory) {
121
+ const sp = new Spinner('agente pensando').start();
122
+ let step = '';
123
+ let text = '';
124
+ try {
125
+ await api.requestStream(state.config.server, '/api/agent/stream', {
126
+ prompt: promptForModel,
127
+ history: state.history.slice(-20),
128
+ }, (evt) => {
129
+ if (evt.erro) { sp.stop(); console.log(c.err(`\nerro: ${evt.erro}`)); return; }
130
+ if (evt.step && evt.event === 'start') {
131
+ if (!step) sp.stop();
132
+ step = evt.step;
133
+ const label = step === 'arquiteto' ? 'Arquiteto' : step === 'engenheiro' ? 'Engenheiro' : 'Revisor';
134
+ console.log(c.muted(`\n ${label}…`));
135
+ return;
136
+ }
137
+ if (evt.step === 'revisor' && evt.delta) {
138
+ if (!text) process.stdout.write('\n');
139
+ text += evt.delta;
140
+ process.stdout.write(c.white(evt.delta));
141
+ }
142
+ if (evt.done) process.stdout.write('\n');
143
+ });
144
+ } catch (e) { sp.stop(); console.log(c.err(`\n${e.message || e}`)); }
145
+ if (text) {
146
+ state.history.push({ role: 'user', content: userTextForMemory });
147
+ state.history.push({ role: 'assistant', content: text });
148
+ state.lastResponse = text;
149
+ cfg.appendHistory({ mode: 'agent', prompt: userTextForMemory, response: text });
249
150
  }
250
-
251
- state.attachedFiles = [];
252
- refreshPrompt();
253
151
  }
254
152
 
255
- // ---- Slash Commands ----
256
- async function handleSlash(line) {
257
- const [cmd, ...rest] = line.trim().split(/\s+/);
258
- const arg = rest.join(' ').trim();
153
+ function isSlash(text) { return text.trim().startsWith('/'); }
259
154
 
155
+ async function handleSlash(text) {
156
+ const [cmd, ...args] = text.trim().slice(1).split(/\s+/);
157
+ const rest = text.trim().slice(1 + cmd.length).trim();
260
158
  switch (cmd.toLowerCase()) {
261
- case '/help': process.stdout.write(HELP); return;
262
- case '/exit': case '/quit':
263
- console.log(c.muted('\nEncerrado.\n')); process.exit(0);
264
- case '/cls': console.clear(); process.stdout.write(welcomeBox(state.config)); return;
265
- case '/clear':
266
- state.history = [];
267
- console.log(c.muted(' Conversa limpa'));
268
- return;
269
- case '/version':
270
- console.log(c.muted(` PrimoCode v4.2 — Cross-Platform`));
271
- console.log(c.muted(` Servidor: ${state.config.server}`));
272
- return;
273
- case '/status': {
274
- const sp = new Spinner('Verificando').start();
275
- try {
276
- const res = await api.request(state.config.server, '/health', {});
277
- sp.stop();
278
- if (res.status === 'ok') {
279
- console.log(c.ok(' Online') + c.muted(` ${state.config.server}`));
280
- } else {
281
- console.log(c.muted(' Servidor offline'));
282
- }
283
- } catch (e) {
284
- sp.stop();
285
- // Silencia erro de status
286
- console.log(c.muted(' Servidor offline'));
287
- }
288
- return;
159
+ case 'help': console.log(HELP); break;
160
+ case 'mode': {
161
+ const m = rest.toLowerCase();
162
+ if (['act','agent','chat'].includes(m)) { state.mode = m; console.log(c.ok(`Modo: ${m}`)); }
163
+ else console.log(c.err('Uso: /mode chat|agent|act'));
164
+ break;
289
165
  }
290
- case '/mode': {
291
- const valid = ['Primo Code', 'Chat'];
292
- let target = arg;
293
- if (!valid.includes(target)) {
294
- console.log(c.warn(' /mode Primo Code | Chat'));
295
- return;
296
- }
297
- state.mode = target;
298
- state.config.model = cfg.resolveModel(target);
299
- console.log(c.muted(` Modo: ${target}`));
300
- refreshPrompt();
301
- return;
166
+ case 'model': {
167
+ const m = rest.toLowerCase();
168
+ if (['main','fast'].includes(m)) { state.config.model = m; cfg.saveConfig(state.config); console.log(c.ok(`Modelo: ${m}`)); }
169
+ else console.log(c.err('Uso: /model main|fast'));
170
+ break;
302
171
  }
303
- case '/attach': {
304
- if (!arg) { console.log(c.warn(' /attach <arquivo>')); return; }
305
- try {
306
- const content = tools.readFileSafe(arg);
307
- state.attachedFiles.push({ path: arg, content });
308
- console.log(c.muted(` Anexado: ${arg}`));
309
- refreshPrompt();
310
- } catch (e) { console.log(c.muted(` Erro ao anexar`)); }
311
- return;
172
+ case 'attach': {
173
+ if (!rest) { console.log(c.err('Uso: /attach <arquivo>')); break; }
174
+ try { const content = fs.readFileSync(path.resolve(rest), 'utf-8'); state.attachedFiles.push({ path: rest, content }); console.log(c.ok(`Anexado: ${rest} (${tools.fmtBytes(content.length)})`)); }
175
+ catch { console.log(c.err(`Não consegui ler: ${rest}`)); }
176
+ break;
312
177
  }
313
- case '/detach':
314
- state.attachedFiles = []; console.log(c.muted(' Anexos removidos')); refreshPrompt(); return;
315
- case '/project': {
316
- if (state.currentProject) {
317
- console.log(c.muted(` Projeto atual: ${state.currentProject}`));
318
- const projMem = memory.loadProjectMemory(state.currentProject);
319
- if (projMem) {
320
- console.log(c.muted(' Memória do projeto carregada'));
321
- }
322
- } else {
323
- console.log(c.muted(' Nenhum projeto ativo'));
324
- }
325
- return;
178
+ case 'detach': state.attachedFiles = []; console.log(c.ok('Anexos removidos.')); break;
179
+ case 'files':
180
+ if (!state.attachedFiles.length) console.log(c.muted('Nenhum arquivo anexado.'));
181
+ else state.attachedFiles.forEach(f => console.log(` ${c.brand(f.path)} ${c.muted(tools.fmtBytes(f.content.length))}`));
182
+ break;
183
+ case 'tree': { const depth = parseInt(rest,10)||2; console.log(tools.treeDir('.', depth)); break; }
184
+ case 'read': {
185
+ if (!rest) { console.log(c.err('Uso: /read <arquivo>')); break; }
186
+ try { const content = fs.readFileSync(path.resolve(rest), 'utf-8'); console.log(c.muted(`\n--- ${rest} ---\n`)); console.log(content.length>2000?content.slice(0,2000)+'\n...':content); }
187
+ catch { console.log(c.err(`Não consegui ler: ${rest}`)); }
188
+ break;
326
189
  }
327
- case '/projects': {
328
- const projects = memory.listProjects();
329
- if (projects.length) {
330
- console.log(c.muted(' Projetos existentes:'));
331
- for (const p of projects) {
332
- console.log(c.muted(` 📁 ${p}`));
333
- }
334
- } else {
335
- console.log(c.muted(' Nenhum projeto encontrado'));
336
- }
337
- return;
190
+ case 'write': {
191
+ if (!rest) { console.log(c.err('Uso: /write <arquivo>')); break; }
192
+ if (!state.lastResponse) { console.log(c.err('Nenhuma resposta para salvar.')); break; }
193
+ try { fs.writeFileSync(path.resolve(rest), state.lastResponse, 'utf-8'); console.log(c.ok(`Salvo em: ${rest}`)); }
194
+ catch (e) { console.log(c.err(`Erro: ${e.message}`)); }
195
+ break;
338
196
  }
339
- case '/app': {
340
- if (!arg) { console.log(c.warn(' /app <nome-do-aplicativo>')); return; }
341
- try {
342
- const result = await mouse.openApp(arg);
343
- console.log(result.ok ? c.ok(` ✓ Aplicativo aberto: ${arg}`) : c.muted(` Indisponível`));
344
- } catch (e) {
345
- // Silencia erro
346
- }
347
- return;
197
+ case 'apply': {
198
+ const applied = tools.applyCodeBlocks(state.lastResponse);
199
+ if (applied.length) applied.forEach(a => console.log(c.ok(`${a.created?'Criado':'Atualizado'}: ${a.path}`)));
200
+ else console.log(c.muted('Nenhum bloco com hint // filename: encontrado.'));
201
+ break;
348
202
  }
349
- case '/terminal': {
350
- try {
351
- const result = platform.openTerminal();
352
- console.log(result.ok ? c.ok(' ✓ Terminal aberto') : c.muted(` ${result.message || 'Terminal'}`));
353
- } catch (e) {
354
- // Silencia erro
355
- }
356
- return;
203
+ case 'run': {
204
+ if (!rest) { console.log(c.err('Uso: /run <comando>')); break; }
205
+ console.log(c.muted(`$ ${rest}`));
206
+ const result = await tools.runShell(rest);
207
+ if (result.stdout) console.log(result.stdout);
208
+ if (result.stderr) console.log(c.err(result.stderr));
209
+ if (result.code !== 0) console.log(c.err(`exit ${result.code}`));
210
+ break;
357
211
  }
358
- case '/run': {
359
- if (!arg) { console.log(c.warn(' /run <arquivo>')); return; }
360
- try {
361
- const ext = path.extname(arg).toLowerCase();
362
- if (ext === '.html' || ext === '.htm') {
363
- platform.openBrowser(arg);
364
- console.log(c.ok(` ✓ Aberto no navegador: ${arg}`));
365
- } else {
366
- const result = platform.openApp(arg);
367
- console.log(result.ok ? c.ok(` ✓ Executando: ${arg}`) : c.muted(` Indisponível`));
368
- }
369
- } catch (e) {
370
- // Silencia erro
371
- }
372
- return;
373
- }
374
- case '/screenshot': {
375
- try {
376
- const result = await mouse.screenshot();
377
- console.log(result.ok ? c.ok(` ✓ Screenshot salvo: ${result.path}`) : c.muted(` Indisponível`));
378
- } catch (e) {
379
- // Silencia erro
380
- }
381
- return;
212
+ case 'open': {
213
+ if (!rest) { console.log(c.err('Uso: /open <arquivo|url>')); break; }
214
+ await tools.openInBrowser(rest);
215
+ console.log(c.ok(`Aberto: ${rest}`));
216
+ break;
382
217
  }
383
- case '/mouse': {
384
- const [x, y] = arg.split(/\s+/).map(Number);
385
- if (isNaN(x) || isNaN(y)) { console.log(c.warn(' /mouse <x> <y>')); return; }
386
- try {
387
- const result = await mouse.moveTo(x, y);
388
- console.log(result.ok !== false ? c.ok(` ✓ Mouse movido para (${x}, ${y})`) : c.muted(` Indisponível`));
389
- } catch (e) {
390
- // Silencia erro
391
- }
392
- return;
218
+ case 'act': {
219
+ if (!rest) { console.log(c.err('Uso: /act <tarefa>')); break; }
220
+ const old = state.mode; state.mode = 'act'; await sendPrompt(rest); state.mode = old;
221
+ break;
393
222
  }
394
- case '/click': {
395
- const [cx, cy] = arg.split(/\s+/).map(Number);
396
- if (isNaN(cx) || isNaN(cy)) { console.log(c.warn(' /click <x> <y>')); return; }
397
- try {
398
- const result = await mouse.clickAt(cx, cy);
399
- console.log(result.ok ? c.ok(` ✓ Clicado em (${cx}, ${cy})`) : c.muted(` Indisponível`));
400
- } catch (e) {
401
- // Silencia erro
402
- }
403
- return;
223
+ case 'clear': state.history = []; console.log(c.ok('Memória limpa.')); break;
224
+ case 'cls': process.stdout.write('\x1Bc'); break;
225
+ case 'status': {
226
+ const sp2 = new Spinner('verificando').start();
227
+ try { const h = await api.getHealth(state.config.server); sp2.stop(); console.log(c.ok(`Servidor: ${h.status||'ok'}`)); console.log(c.muted(` Modelos: ${Object.values(h.models||{}).join(', ')}`)); console.log(c.muted(` Fallback: ${h.using_fallback?'ativo':'inativo'}`)); }
228
+ catch (e) { sp2.stop(); console.log(c.err(`Indisponível: ${e.message}`)); }
229
+ break;
404
230
  }
405
- case '/search': {
406
- if (!arg) { console.log(c.warn(' /search <termo>')); return; }
407
- try {
408
- const result = await webSearch(arg);
409
- console.log(result.ok ? c.ok(` ✓ Pesquisa iniciada: ${arg}`) : c.muted(` Pesquisa indisponível`));
410
- } catch (e) {
411
- // Silencia erro
412
- }
413
- return;
414
- }
415
- case '/uninstall': {
416
- console.log(c.muted(' Removendo PrimoCode...'));
417
- try {
418
- const installDir = path.join(os.homedir(), '.primocode');
419
- const globalLink = path.join(os.homedir(), '.local/bin/primocode');
420
- if (fs.existsSync(globalLink)) fs.unlinkSync(globalLink);
421
- if (fs.existsSync(installDir)) fs.rmSync(installDir, { recursive: true, force: true });
422
- console.log(c.ok(' PrimoCode removido'));
423
- } catch (e) {
424
- // Silencia erro
425
- }
426
- return;
427
- }
428
- default:
429
- console.log(c.warn(` Comando desconhecido: ${cmd}`));
430
- console.log(c.muted(' /help'));
231
+ case 'version': console.log(c.brand(`PrimoCode v${VERSION}`)); break;
232
+ case 'exit': case 'quit': console.log(c.muted('Até logo!')); process.exit(0);
233
+ default: console.log(c.err(`Comando desconhecido: /${cmd}. Digite /help.`));
431
234
  }
432
235
  }
433
236
 
434
- // ---- One-shot ----
435
- async function runOneShot(argv) {
436
- const args = argv.slice(2);
437
- let mode = 'Primo Code';
438
- let attach = [];
439
- const prompt = [];
440
- for (let i = 0; i < args.length; i++) {
441
- const a = args[i];
442
- if (a === '--agent' || a === '-a' || a === '--act' || a === '-x' || a === '--primocode' || a === '-p') mode = 'Primo Code';
443
- else if (a === '--chat' || a === '-c') mode = 'Chat';
444
- else if (a === '--attach' || a === '-f') attach.push(args[++i]);
445
- else if (a === '--help' || a === '-h') { printCliHelp(); process.exit(0); }
446
- else if (a === '--version' || a === '-v') { console.log('PrimoCode v4.2 — Windows | macOS | Linux | Termux'); process.exit(0); }
447
- else if (a === '--server') state.config.server = args[++i];
448
- else prompt.push(a);
237
+ // Detecta Termux
238
+ function isTermux() {
239
+ return !!process.env.TERMUX_VERSION || (process.env.PREFIX && process.env.PREFIX.includes('termux'));
240
+ }
241
+
242
+ // ---- Auto-instalação Node.js com barra de progresso ----
243
+ async function checkAndInstallNode() {
244
+ const nodeExists = () => { try { execSync('node --version', {stdio:'ignore'}); return true; } catch { return false; } };
245
+
246
+ if (nodeExists()) {
247
+ const v = execSync('node --version', {encoding:'utf-8'}).trim();
248
+ const major = parseInt(v.replace('v','').split('.')[0]);
249
+ if (major >= 18) { console.log(c.ok(`Node.js ${v} detectado.`)); return true; }
250
+ console.log(c.warn(`Node.js ${v} detectado, mas requer v18.17+.`));
251
+ } else {
252
+ console.log(c.err('Node.js não encontrado no sistema.'));
449
253
  }
450
- if (!prompt.length) return false;
451
- state.mode = mode;
452
- for (const p of attach) {
453
- try { state.attachedFiles.push({ path: p, content: tools.readFileSafe(p) }); }
454
- catch (e) {
455
- // Silencia erro de anexo
456
- }
254
+
255
+ // PERGUNTA antes de instalar — aguarda resposta do usuário
256
+ const answer = await askYesNo('Deseja instalar o Node.js automaticamente?');
257
+ if (!answer) {
258
+ console.log(c.err('Node.js é necessário para rodar o PrimoCode.'));
259
+ console.log(c.muted('Instale manualmente em: https://nodejs.org/'));
260
+ process.exit(1);
457
261
  }
458
- await sendPrompt(prompt.join(' '));
459
- return true;
460
- }
461
262
 
462
- function printCliHelp() {
463
- console.log(welcomeBox(state.config));
464
- console.log(`${c.bold('Uso')}`);
465
- console.log(` primocode Interativo (Multi-Agente)`);
466
- console.log(` primocode "crie um site" Pipeline completo`);
467
- console.log(` primocode --chat "duvida" Modo conversa`);
468
- console.log(HELP);
469
- console.log(`${c.bold('Comandos de Sistema')}`);
470
- console.log(` /app <nome> Abrir aplicativo`);
471
- console.log(` /terminal Abrir terminal`);
472
- console.log(` /run <arquivo> Executar arquivo (Python, JS, HTML)`);
473
- console.log(` /screenshot Capturar tela`);
474
- console.log(` /mouse <x> <y> Mover mouse`);
475
- console.log(` /click <x> <y> Clicar`);
476
- console.log(` /search <termo> Pesquisar na web`);
477
- console.log(` /project Ver projeto atual`);
478
- console.log(` /projects Listar projetos`);
479
- }
263
+ const platform = os.platform();
264
+ const pb = new ProgressBar('Instalando Node.js');
480
265
 
481
- // ---- Verificar Termux na inicialização ----
482
- async function checkTermux() {
483
- if (platform.isTermux()) {
484
- const setup = platform.checkTermuxSetup();
485
- if (!setup.ok) {
486
- console.log(c.warn('⚠ Termux detectado!'));
487
- console.log(c.muted(' Configurações necessárias:'));
488
- for (const issue of setup.issues) {
489
- console.log(c.muted(` • ${issue}`));
266
+ try {
267
+ if (isTermux()) {
268
+ // Termux — usa pkg
269
+ pb.start();
270
+ const child = spawn('pkg', ['install', '-y', 'nodejs'], { stdio: 'pipe' });
271
+ let stderr = '';
272
+ child.stderr.on('data', d => { stderr += d; pb.tick(); });
273
+ await new Promise((resolve, reject) => {
274
+ child.on('close', code => { pb.stop(); code === 0 ? resolve() : reject(new Error(stderr || 'pkg failed')); });
275
+ });
276
+ } else if (platform === 'darwin' || platform === 'linux') {
277
+ const hasNvm = () => { try { execSync('command -v nvm', {stdio:'ignore'}); return true; } catch { return false; } };
278
+ const hasFnm = () => { try { execSync('command -v fnm', {stdio:'ignore'}); return true; } catch { return false; } };
279
+
280
+ if (hasNvm() || fs.existsSync(path.join(os.homedir(), '.nvm'))) {
281
+ pb.start();
282
+ const nvmDir = path.join(os.homedir(), '.nvm');
283
+ const child = spawn('bash', ['-c', `export NVM_DIR="${nvmDir}" && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm install 20 && nvm use 20 && nvm alias default 20`], { stdio: 'pipe' });
284
+ let stderr = '';
285
+ child.stderr.on('data', d => { stderr += d; pb.tick(); });
286
+ await new Promise((resolve, reject) => {
287
+ child.on('close', code => { pb.stop(); code === 0 ? resolve() : reject(new Error(stderr || 'nvm failed')); });
288
+ });
289
+ } else if (hasFnm()) {
290
+ pb.start();
291
+ const child = spawn('bash', ['-c', 'fnm install 20 && fnm use 20 && fnm default 20'], { stdio: 'pipe' });
292
+ let stderr = '';
293
+ child.stderr.on('data', d => { stderr += d; pb.tick(); });
294
+ await new Promise((resolve, reject) => {
295
+ child.on('close', code => { pb.stop(); code === 0 ? resolve() : reject(new Error(stderr || 'fnm failed')); });
296
+ });
297
+ } else {
298
+ pb.start();
299
+ // Instala nvm primeiro
300
+ const child1 = spawn('bash', ['-c', 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash'], { stdio: 'pipe' });
301
+ let stderr1 = '';
302
+ child1.stderr.on('data', d => { stderr1 += d; pb.tick(); });
303
+ await new Promise((resolve, reject) => {
304
+ child1.on('close', code => { code === 0 ? resolve() : reject(new Error(stderr1)); });
305
+ });
306
+
307
+ // Instala Node via nvm
308
+ const nvmDir = path.join(os.homedir(), '.nvm');
309
+ const child2 = spawn('bash', ['-c', `export NVM_DIR="${nvmDir}" && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm install 20 && nvm use 20 && nvm alias default 20`], { stdio: 'pipe' });
310
+ let stderr2 = '';
311
+ child2.stderr.on('data', d => { stderr2 += d; pb.tick(); });
312
+ await new Promise((resolve, reject) => {
313
+ child2.on('close', code => { pb.stop(); code === 0 ? resolve() : reject(new Error(stderr2 || 'nvm install failed')); });
314
+ });
490
315
  }
491
- console.log(c.muted(`Projetos serão salvos em: /storage/shared/primocode/`));
492
- } else {
493
- console.log(c.ok(`✓ Termux configurado!`));
494
- console.log(c.muted(` Projetos em: /storage/shared/primocode/`));
316
+ } else if (platform === 'win32') {
317
+ pb.start();
318
+ const child = spawn('cmd', ['/c', 'winget install OpenJS.NodeJS.LTS'], { stdio: 'pipe' });
319
+ let stderr = '';
320
+ child.stderr.on('data', d => { stderr += d; pb.tick(); });
321
+ await new Promise((resolve, reject) => {
322
+ child.on('close', code => { pb.stop(); code === 0 ? resolve() : reject(new Error(stderr || 'winget failed')); });
323
+ });
495
324
  }
496
- }
497
- }
498
325
 
499
- // ---- Verificar projeto existente na inicialização ----
500
- async function checkExistingProject() {
501
- const projects = memory.listProjects();
502
- if (projects.length > 0) {
503
- console.log(c.muted(`\n 📁 Projetos existentes encontrados:`));
504
- for (const p of projects) {
505
- console.log(c.muted(` • ${p}`));
326
+ // Verifica instalação
327
+ if (!nodeExists()) {
328
+ throw new Error('Node.js não foi instalado corretamente');
506
329
  }
507
- console.log(c.muted(`\n Use /project <nome> para continuar ou comece um novo projeto.`));
330
+ const v = execSync('node --version', {encoding:'utf-8'}).trim();
331
+ console.log(c.ok(`Node.js ${v} instalado com sucesso!`));
332
+ return true;
333
+ } catch (e) {
334
+ pb.stop();
335
+ console.log(c.err(`Falha ao instalar Node.js: ${e.message}`));
336
+ console.log(c.muted('Por favor, instale manualmente: https://nodejs.org/'));
337
+ process.exit(1);
508
338
  }
509
339
  }
510
340
 
511
- // ---- Interactive ----
512
- function startInteractive() {
513
- process.stdout.write(welcomeBox(state.config));
514
-
515
- // Verificar Termux
516
- checkTermux();
517
- // Verificar projetos existentes
518
- checkExistingProject();
519
-
520
- rl = readline.createInterface({
521
- input: process.stdin,
522
- output: process.stdout,
523
- prompt: makePrompt(),
524
- historySize: 200,
525
- });
526
- rl.prompt();
527
-
528
- const queue = [];
529
- let busy = false;
530
- async function drain() {
531
- if (busy) return;
532
- busy = true;
533
- while (queue.length) {
534
- const input = queue.shift();
535
- try {
536
- if (input.startsWith('/')) await handleSlash(input);
537
- else await sendPrompt(input);
538
- } catch (e) {
539
- // Silencia erro geral
540
- }
341
+ async function main() {
342
+ await checkAndInstallNode();
343
+ const args = process.argv.slice(2);
344
+ if (args.length && !args[0].startsWith('-')) {
345
+ const prompt = args.join(' ');
346
+ const isAgent = args.includes('--agent');
347
+ const fileFlag = args.indexOf('-f');
348
+ let promptText = prompt;
349
+ if (fileFlag >= 0 && args[fileFlag+1]) {
350
+ try { const fc = fs.readFileSync(path.resolve(args[fileFlag+1]), 'utf-8'); promptText = `Arquivo: ${args[fileFlag+1]}\n\`\`\`\n${fc}\n\`\`\`\n\n${prompt}`; }
351
+ catch { console.log(c.err(`Não consegui ler: ${args[fileFlag+1]}`)); process.exit(1); }
541
352
  }
542
- busy = false;
543
- rl.prompt();
353
+ if (isAgent) await runAgent(promptText, prompt);
354
+ else { const r = await runAct(promptText, {server:state.config.server, model:state.config.model, history:[]}); console.log('\n'+c.white(r.assistantMessage.content)); }
355
+ process.exit(0);
544
356
  }
545
- rl.on('line', (line) => {
546
- const input = line.trim();
547
- if (!input) { if (!busy) rl.prompt(); return; }
548
- queue.push(input);
549
- drain();
550
- });
551
- rl.on('SIGINT', () => { console.log(c.muted('\nUse /exit para sair')); rl.prompt(); });
552
- rl.on('close', () => {
553
- const check = () => {
554
- if (!busy && queue.length === 0) { console.log(c.muted('\nEncerrado.\n')); process.exit(0); }
555
- setTimeout(check, 100);
556
- };
557
- check();
357
+ console.log(welcomeBox(VERSION, state.config.server));
358
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt() });
359
+ rl.prompt();
360
+ rl.on('line', async (line) => {
361
+ const text = line.trim();
362
+ if (!text) { rl.prompt(); return; }
363
+ if (isSlash(text)) await handleSlash(text); else await sendPrompt(text);
364
+ rl.prompt();
558
365
  });
366
+ rl.on('close', () => { console.log(c.muted('\nAté logo!')); process.exit(0); });
559
367
  }
560
368
 
561
- // ---- Entry ----
562
- (async () => {
563
- const flags = process.argv.slice(2);
564
- const hasPrompt = flags.some(a => !a.startsWith('-'));
565
- const helpFlag = flags.some(a => a === '-h' || a === '--help');
566
- const versionFlag = flags.some(a => a === '-v' || a === '--version');
567
- if (helpFlag) { printCliHelp(); process.exit(0); }
568
- if (versionFlag) { console.log('PrimoCode v4.2 — Windows | macOS | Linux | Termux'); process.exit(0); }
569
- if (hasPrompt) { await runOneShot(process.argv); process.exit(0); }
570
- startInteractive();
571
- })().catch((err) => {
572
- // Silencia erro fatal
573
- process.exit(1);
574
- });
369
+ main().catch(e => { console.error(c.err(e.message||e)); process.exit(1); });