primocode 1.0.1 → 1.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/README.md CHANGED
@@ -1,87 +1,66 @@
1
- # PrimoCode
1
+ # PrimoCode v3.1
2
2
 
3
- Assistente de engenharia com inteligencia artificial.
3
+ Assistente de engenharia com IA multi-agente, RAG e memoria de longo prazo.
4
4
 
5
- ## Modos
6
-
7
- - **Conecta Primo AI** — Engenharia completa: codigo, arquivos, terminal, navegador
8
- - **Chat** — Conversa direta e objetiva
9
- - **JARVIS** — Interacao por voz e visao
10
-
11
- ## Instalacao
5
+ ## Sistema Multi-Agente + RAG
12
6
 
13
- ### Requisito
7
+ ### Pipeline de 5 Agentes
14
8
 
15
- Node.js 18.17+ (instalado automaticamente nos scripts abaixo)
9
+ 1. **Prompt Engineer** — Estrutura e otimiza o prompt do usuario
10
+ 2. **Construtor 1** — Gera prototipo funcional rapidamente
11
+ 3. **Construtor 2** — Refina e otimiza o codigo
12
+ 4. **Analisador** — Encontra bugs e vulnerabilidades
13
+ 5. **Revisor** — Code review final e aprovacao
16
14
 
17
- ### Windows
15
+ ### RAG (Retrieval Augmented Generation)
18
16
 
19
- ```powershell
20
- # PowerShell (como Administrador)
21
- iwr -useb https://raw.githubusercontent.com/ConectaPrimoAI/primocode/main/install.cmd | iex
17
+ - **Indexacao automatica** do projeto a cada hora
18
+ - **Contexto enriquecido** com arquivos relevantes
19
+ - **Busca semantica** via TF-IDF + cosine similarity
22
20
 
23
- # Ou via npm (Node.js precisa estar instalado)
24
- npm install -g primocode
25
- ```
21
+ ### Memoria de Longo Prazo
26
22
 
27
- ### Linux / macOS / WSL
23
+ - **Aprende preferencias**: stack tecnologico, estilo UI, complexidade
24
+ - **Contexto do projeto**: tecnologias, estrutura de arquivos
25
+ - **Historico indexado**: codigo gerado disponivel para consulta
28
26
 
29
- ```bash
30
- # Script automatico
31
- curl -fsSL https://raw.githubusercontent.com/ConectaPrimoAI/primocode/main/install.sh | bash
27
+ ## Modos
32
28
 
33
- # Ou via npm
34
- npm install -g primocode
35
- ```
29
+ - **Conecta Primo AI** — Pipeline multi-agente completo com RAG
30
+ - **Chat** — Conversa direta com modelo unico
31
+ - **JARVIS** — Interacao por voz e visao
36
32
 
37
- ### Termux (Android)
33
+ ## Instalacao
38
34
 
39
35
  ```bash
40
- # Script especifico para Termux
41
- curl -fsSL https://raw.githubusercontent.com/ConectaPrimoAI/primocode/main/termux-install.sh | bash
42
-
43
- # Ou manualmente
44
- pkg install -y nodejs git
45
36
  npm install -g primocode
46
37
  ```
47
38
 
48
39
  ## Uso
49
40
 
50
41
  ```bash
51
- primocode # Modo interativo
52
- primocode "crie um site" # Tarefa direta
42
+ primocode # Modo interativo (Multi-Agente + RAG)
43
+ primocode "crie um site completo" # Pipeline completo com contexto
53
44
  primocode --chat "explique REST" # Modo conversa
54
45
  primocode --jarvis # Voz e visao
55
- primocode -f app.js "analise isto" # Anexa arquivo
56
46
  ```
57
47
 
58
48
  ## Comandos
59
49
 
60
50
  | Comando | Funcao |
61
51
  |---------|--------|
62
- | `/mode` | Alterna entre Conecta Primo AI, Chat, JARVIS |
52
+ | `/mode` | Alterna entre modos |
63
53
  | `/attach` | Anexa arquivo ao contexto |
64
- | `/clear` | Limpa historico da conversa |
54
+ | `/clear` | Limpa historico |
55
+ | `/status` | Mostra status do servidor, RAG e memoria |
65
56
  | `/uninstall` | Remove PrimoCode |
66
- | `/help` | Exibe ajuda |
57
+ | `/help` | Ajuda |
67
58
  | `/exit` | Encerra |
68
59
 
69
- ## Configuracao
70
-
71
- Defina variaveis de ambiente:
72
-
73
- ```bash
74
- export PRIMOCODE_SERVER=https://seu-servidor.com
75
- export PRIMOCODE_MODEL=main
76
- ```
77
-
78
- ## Mobile (Termux)
60
+ ## Requisitos
79
61
 
80
- PrimoCode funciona perfeitamente no Termux:
81
- - Instale o Termux da F-Droid (versao atualizada)
82
- - Execute o script de instalacao acima
83
- - Use todos os modos: Conecta Primo AI, Chat e JARVIS
84
- - JARVIS abre no navegador do dispositivo
62
+ - Node.js 18.17+
63
+ - Servidor PrimoCode com API Groq
85
64
 
86
65
  ---
87
66
 
package/bin/primocode.js CHANGED
@@ -1,9 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * PrimoCode
4
- * Conecta Primo AI → engenharia completa: código, arquivos, terminal, navegador
5
- * Chat → conversa direta
6
- * JARVIS → voz e visão
3
+ * PrimoCode v3.0 — Cliente Multi-Agente
4
+ * Conecta Primo AI: Engenharia completa com pipeline de 5 agentes
7
5
  */
8
6
 
9
7
  'use strict';
@@ -12,18 +10,13 @@ const readline = require('readline');
12
10
  const path = require('path');
13
11
  const fs = require('fs');
14
12
  const os = require('os');
15
- const { spawn } = require('child_process');
13
+ const { spawn, execSync } = require('child_process');
16
14
 
17
15
  const { c, welcomeBox, Spinner } = require('../lib/ui');
18
16
  const api = require('../lib/api');
19
17
  const tools = require('../lib/tools');
20
18
  const cfg = require('../lib/config');
21
19
 
22
- // Garante modelo correto baseado no modo atual
23
- function getModel() { return cfg.resolveModel(state.mode); }
24
- const { runAct } = require('../lib/act');
25
-
26
- // ---- Estado ----
27
20
  const state = {
28
21
  config: cfg.loadConfig(),
29
22
  history: [],
@@ -32,7 +25,6 @@ const state = {
32
25
  lastResponse: '',
33
26
  };
34
27
 
35
- // ---- Ajuda ----
36
28
  const HELP = `
37
29
  ${c.bold('Comandos')}
38
30
  /mode <nome> Conecta Primo AI | Chat | JARVIS
@@ -44,7 +36,7 @@ ${c.bold('Comandos')}
44
36
  /exit sair
45
37
 
46
38
  ${c.bold('Modos')}
47
- ${c.brand('Conecta Primo AI')} Engenharia completa
39
+ ${c.brand('Conecta Primo AI')} Engenharia completa (5 agentes)
48
40
  ${c.brand('Chat')} Conversa direta
49
41
  ${c.brand('JARVIS')} Voz e visao
50
42
  `;
@@ -58,34 +50,90 @@ function makePrompt() {
58
50
  }
59
51
  function refreshPrompt() { if (rl) rl.setPrompt(makePrompt()); }
60
52
 
61
- // ---- Envio ----
62
- async function sendPrompt(userText) {
63
- let promptWithAttach = userText;
64
- if (state.attachedFiles.length) {
65
- const parts = state.attachedFiles.map(f => `Arquivo: ${f.path}\n\`\`\`\n${f.content}\n\`\`\``);
66
- promptWithAttach = parts.join('\n\n') + '\n\n' + userText;
67
- }
53
+ // ---- Multi-Agent Pipeline ----
54
+ async function runMultiAgent(userText) {
55
+ const sp = new Spinner('Iniciando pipeline multi-agente').start();
68
56
 
69
- if (state.mode === 'Conecta Primo AI') {
70
- const r = await runAct(promptWithAttach, {
71
- server: state.config.server,
72
- model: getModel(),
73
- history: state.history,
57
+ try {
58
+ sp.update('Analisando contexto do projeto...');
59
+
60
+ const response = await fetch(state.config.server + '/api/multi-agent', {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({
64
+ prompt: userText,
65
+ maxIterations: 3,
66
+ projectPath: process.cwd(),
67
+ }),
74
68
  });
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: 'Conecta Primo AI', prompt: userText, response: r.assistantMessage.content });
79
- } else {
80
- await runChat(promptWithAttach, userText);
81
- }
82
69
 
83
- state.attachedFiles = [];
84
- refreshPrompt();
85
- }
70
+ sp.stop();
71
+
72
+ if (!response.ok) {
73
+ throw new Error(`HTTP ${response.status}`);
74
+ }
75
+
76
+ const result = await response.json();
86
77
 
87
- // Chat
88
- async function runChat(promptForModel, userTextForMemory) {
78
+ if (result.error) {
79
+ console.log(c.err(`\nErro: ${result.error}`));
80
+ return;
81
+ }
82
+
83
+ // Mostrar contexto RAG
84
+ if (result.ragContext) {
85
+ console.log(c.muted('\n ── Contexto RAG ──'));
86
+ console.log(c.muted(` ${result.ragContext.substring(0, 120)}...`));
87
+ }
88
+
89
+ // Mostrar progresso dos agentes
90
+ console.log(c.muted('\n ── Pipeline Multi-Agente ──'));
91
+ for (const phase of result.phases) {
92
+ const icon = phase.agent.includes('Revisor') ? '✓' :
93
+ phase.agent.includes('Analisador') ? '🔍' :
94
+ phase.agent.includes('Correcao') ? '🔧' : '●';
95
+ console.log(c.muted(` ${icon} ${phase.agent}`));
96
+ }
97
+
98
+ // Mostrar resultado do revisor
99
+ const reviewPhase = result.phases.find(p => p.agent === 'Revisor Final');
100
+ if (reviewPhase) {
101
+ const approved = reviewPhase.output.includes('APROVADO');
102
+ const score = reviewPhase.output.match(/nota\s*(\d+)/)?.[1] || '?';
103
+ console.log(c.muted(` ──`));
104
+ console.log(approved ? c.ok(` ✓ Aprovado (nota ${score}/10)`) : c.warn(` ⚠ Revisado (nota ${score}/10)`));
105
+ }
106
+
107
+ // Extrair e mostrar codigo
108
+ const code = result.finalCode;
109
+ state.lastResponse = code;
110
+
111
+ // Mostrar arquivos gerados
112
+ if (result.files && result.files.length) {
113
+ console.log(c.muted(`\n Arquivos gerados:`));
114
+ for (const f of result.files) {
115
+ console.log(c.muted(` 📄 ${f.path}`));
116
+ }
117
+ }
118
+
119
+ // Mostrar codigo (primeiros 2000 chars)
120
+ console.log('\n' + c.white(code.substring(0, 2000)));
121
+ if (code.length > 2000) {
122
+ console.log(c.muted(`\n ... +${code.length - 2000} caracteres`));
123
+ }
124
+
125
+ // Perguntar se quer deploy
126
+ console.log(c.muted(`\n Use /deploy para abrir no navegador ou instalar dependencias`));
127
+
128
+ cfg.appendHistory({ mode: 'Conecta Primo AI', prompt: userText, response: code });
129
+
130
+ } catch (err) {
131
+ sp.stop();
132
+ console.log(c.err(`\nFalha no pipeline: ${err.message}`));
133
+ console.log(c.muted(' Tentando modo normal...'));
134
+ await runChat(userText, userText);
135
+ }
136
+ }async function runChat(promptForModel, userTextForMemory) {
89
137
  const sp = new Spinner('Processando').start();
90
138
  let started = false;
91
139
  let text = '';
@@ -93,7 +141,7 @@ async function runChat(promptForModel, userTextForMemory) {
93
141
  await api.requestStream(state.config.server, '/api/normal/stream', {
94
142
  prompt: promptForModel,
95
143
  history: state.history.slice(-20),
96
- model: getModel(),
144
+ model: cfg.resolveModel(state.mode),
97
145
  }, (evt) => {
98
146
  if (evt.erro) {
99
147
  if (!started) sp.stop();
@@ -120,7 +168,25 @@ async function runChat(promptForModel, userTextForMemory) {
120
168
  cfg.appendHistory({ mode: 'Chat', prompt: userTextForMemory, response: text });
121
169
  }
122
170
 
123
- // ---- Slash commands ----
171
+ // ---- Envio Principal ----
172
+ async function sendPrompt(userText) {
173
+ let promptWithAttach = userText;
174
+ if (state.attachedFiles.length) {
175
+ const parts = state.attachedFiles.map(f => `Arquivo: ${f.path}\n\`\`\`\n${f.content}\n\`\`\``);
176
+ promptWithAttach = parts.join('\n\n') + '\n\n' + userText;
177
+ }
178
+
179
+ if (state.mode === 'Conecta Primo AI') {
180
+ await runMultiAgent(promptWithAttach);
181
+ } else {
182
+ await runChat(promptWithAttach, userText);
183
+ }
184
+
185
+ state.attachedFiles = [];
186
+ refreshPrompt();
187
+ }
188
+
189
+ // ---- Slash Commands ----
124
190
  async function handleSlash(line) {
125
191
  const [cmd, ...rest] = line.trim().split(/\s+/);
126
192
  const arg = rest.join(' ').trim();
@@ -135,22 +201,30 @@ async function handleSlash(line) {
135
201
  console.log(c.muted(' Conversa limpa'));
136
202
  return;
137
203
  case '/version':
138
- console.log(c.muted(` PrimoCode`));
204
+ console.log(c.muted(` PrimoCode Multi-Agent v3.0`));
139
205
  console.log(c.muted(` Servidor: ${state.config.server}`));
140
206
  return;
141
- case '/status': {
207
+ case '/status': {
142
208
  const sp = new Spinner('Verificando').start();
143
209
  try {
144
- const h = await api.getHealth(state.config.server);
210
+ const res = await fetch(state.config.server + '/health');
211
+ const data = await res.json();
145
212
  sp.stop();
146
- console.log(h.ok ? c.ok(' Online') + c.muted(` ${state.config.server}`) : c.err(' Indisponivel'));
213
+ if (data.status === 'ok') {
214
+ console.log(c.ok(' Online') + c.muted(` ${state.config.server}`));
215
+ console.log(c.muted(` Sistema: ${data.system} v${data.version}`));
216
+ console.log(c.muted(` Agentes: ${data.agents?.join(', ')}`));
217
+ console.log(c.muted(` RAG: Indexacao automatica ativa`));
218
+ console.log(c.muted(` Memory: Aprendizado de padroes ativo`));
219
+ } else {
220
+ console.log(c.err(' Indisponivel'));
221
+ }
147
222
  } catch (e) {
148
223
  sp.stop();
149
224
  console.log(c.err(' Indisponivel'));
150
225
  }
151
226
  return;
152
- }
153
- case '/jarvis': {
227
+ }case '/jarvis': {
154
228
  try {
155
229
  const jarvis = require('../lib/jarvis');
156
230
  console.log(c.muted(' Iniciando JARVIS...'));
@@ -161,118 +235,44 @@ async function handleSlash(line) {
161
235
  return;
162
236
  }
163
237
  case '/mode': {
164
- const valid = ['Conecta Primo AI', 'Chat', 'JARVIS', 'primocode', 'chat'];
238
+ const valid = ['Conecta Primo AI', 'Chat', 'JARVIS'];
165
239
  let target = arg;
166
- if (target.toLowerCase() === 'act' || target.toLowerCase() === 'agent' || target.toLowerCase() === 'primocode') {
167
- console.log(c.muted(' Modo: Conecta Primo AI'));
168
- target = 'Conecta Primo AI';
240
+ if (!valid.includes(target)) {
241
+ console.log(c.warn(' /mode Conecta Primo AI | Chat | JARVIS'));
242
+ return;
169
243
  }
170
- if (!valid.includes(target)) { console.log(c.warn(' /mode Conecta Primo AI | Chat | JARVIS')); return; }
171
244
  state.mode = target;
172
- state.config.model = getModel();
245
+ state.config.model = cfg.resolveModel(target);
173
246
  console.log(c.muted(` Modo: ${target}`));
174
247
  refreshPrompt();
175
248
  return;
176
249
  }
177
- case '/model':
178
- if (!arg) { console.log(c.muted(` Modelo: ${state.config.model}`)); return; }
179
- state.config.model = arg;
180
- cfg.saveConfig({ model: arg });
181
- console.log(c.muted(` Modelo: ${arg}`));
182
- return;
183
250
  case '/attach': {
184
251
  if (!arg) { console.log(c.warn(' /attach <arquivo>')); return; }
185
252
  try {
186
253
  const content = tools.readFileSafe(arg);
187
254
  state.attachedFiles.push({ path: arg, content });
188
- console.log(c.muted(` Anexado: ${arg} (${tools.fmtBytes(Buffer.byteLength(content, 'utf8'))})`));
255
+ console.log(c.muted(` Anexado: ${arg}`));
189
256
  refreshPrompt();
190
257
  } catch (e) { console.log(c.err(` ${e.message}`)); }
191
258
  return;
192
259
  }
193
260
  case '/detach':
194
261
  state.attachedFiles = []; console.log(c.muted(' Anexos removidos')); refreshPrompt(); return;
195
- case '/files':
196
- if (!state.attachedFiles.length) console.log(c.muted(' (Nenhum)'));
197
- else for (const f of state.attachedFiles) console.log(c.muted(` ${f.path} (${tools.fmtBytes(Buffer.byteLength(f.content, 'utf8'))})`));
198
- return;
199
- case '/tree': {
200
- const d = parseInt(arg, 10) || 2;
201
- const r = tools.listDir('.', d);
202
- console.log(r.ok ? c.muted(r.tree) : c.err(` ${r.error}`));
203
- return;
204
- }
205
- case '/read': {
206
- if (!arg) { console.log(c.warn(' /read <arquivo>')); return; }
207
- try {
208
- const content = tools.readFileSafe(arg);
209
- for (const line of content.split('\n')) console.log(c.muted(' ') + c.white(line));
210
- } catch (e) { console.log(c.err(` ${e.message}`)); }
211
- return;
212
- }
213
- case '/write': {
214
- if (!arg) { console.log(c.warn(' /write <arquivo>')); return; }
215
- if (!state.lastResponse) { console.log(c.warn(' Sem resposta anterior')); return; }
216
- const blocks = tools.extractCodeBlocks(state.lastResponse);
217
- const content = blocks.length ? blocks[0].content : state.lastResponse;
218
- const r = tools.writeFile(arg, content);
219
- if (r.ok) console.log(c.muted(` ${r.created ? 'Criado' : 'Atualizado'} · ${r.path} · ${tools.fmtBytes(r.bytes)}`));
220
- else console.log(c.err(` ${r.error}`));
221
- return;
222
- }
223
- case '/apply': {
224
- if (!state.lastResponse) { console.log(c.warn(' Sem resposta anterior')); return; }
225
- const blocks = tools.extractCodeBlocks(state.lastResponse);
226
- const withFile = blocks.filter(b => b.filename);
227
- if (!withFile.length) { console.log(c.warn(' Nenhum bloco com // filename:')); return; }
228
- for (const b of withFile) {
229
- const r = tools.writeFile(b.filename, b.content);
230
- if (r.ok) console.log(c.muted(` ${r.created ? 'Criado' : 'Atualizado'} · ${r.path} · ${tools.fmtBytes(r.bytes)}`));
231
- else console.log(c.err(` ${b.filename}: ${r.error}`));
232
- }
233
- return;
234
- }
235
- case '/run': {
236
- if (!arg) { console.log(c.warn(' /run <cmd>')); return; }
237
- console.log(c.muted(` $ ${arg}`));
238
- const r = tools.runShellSync(arg);
239
- if (r.stdout) process.stdout.write(r.stdout);
240
- if (r.stderr) process.stdout.write(c.err(r.stderr));
241
- console.log(r.ok ? c.muted(' Concluido') : c.err(` Falhou`));
242
- return;
243
- }
244
- case '/open': {
245
- if (!arg) { console.log(c.warn(' /open <arquivo|url>')); return; }
246
- const r = tools.openInBrowser(arg);
247
- console.log(r.ok ? c.muted(` ${r.url}`) : c.err(` ${r.error}`));
248
- return;
249
- }
250
262
  case '/uninstall': {
251
263
  console.log(c.muted(' Removendo PrimoCode...'));
252
- const installDir = path.join(os.homedir(), '.primocode');
253
- const globalLink = path.join(os.homedir(), '.local/bin/primocode');
254
264
  try {
265
+ const installDir = path.join(os.homedir(), '.primocode');
266
+ const globalLink = path.join(os.homedir(), '.local/bin/primocode');
255
267
  if (fs.existsSync(globalLink)) fs.unlinkSync(globalLink);
256
- if (fs.existsSync(installDir)) {
257
- fs.rmSync(installDir, { recursive: true, force: true });
258
- }
268
+ if (fs.existsSync(installDir)) fs.rmSync(installDir, { recursive: true, force: true });
259
269
  console.log(c.ok(' PrimoCode removido'));
260
- console.log(c.muted(' Para reinstalar: npm install -g primocode'));
261
- } catch (e) {
262
- console.log(c.err(` Erro: ${e.message}`));
263
- }
270
+ } catch (e) { console.log(c.err(` Erro: ${e.message}`)); }
264
271
  return;
265
272
  }
266
- case '/act': {
267
- if (!arg) { console.log(c.warn(' /act <tarefa>')); return; }
268
- const r = await runAct(arg, {
269
- server: state.config.server,
270
- model: getModel(),
271
- history: state.history,
272
- });
273
- state.history.push({ role: 'user', content: arg });
274
- state.history.push({ role: 'assistant', content: r.assistantMessage.content });
275
- state.lastResponse = r.assistantMessage.content;
273
+ case '/deploy': {
274
+ if (!state.lastResponse) { console.log(c.warn(' Sem codigo para deploy')); return; }
275
+ console.log(c.muted(' Deploy em desenvolvimento...'));
276
276
  return;
277
277
  }
278
278
  default:
@@ -294,9 +294,8 @@ async function runOneShot(argv) {
294
294
  else if (a === '--jarvis' || a === '-j') mode = 'JARVIS';
295
295
  else if (a === '--attach' || a === '-f') attach.push(args[++i]);
296
296
  else if (a === '--help' || a === '-h') { printCliHelp(); process.exit(0); }
297
- else if (a === '--version' || a === '-v') { console.log('PrimoCode'); process.exit(0); }
297
+ else if (a === '--version' || a === '-v') { console.log('PrimoCode Multi-Agent v3.0'); process.exit(0); }
298
298
  else if (a === '--server') state.config.server = args[++i];
299
- else if (a === '--model') state.config.model = args[++i];
300
299
  else prompt.push(a);
301
300
  }
302
301
  if (!prompt.length) return false;
@@ -312,11 +311,10 @@ async function runOneShot(argv) {
312
311
  function printCliHelp() {
313
312
  console.log(welcomeBox(state.config));
314
313
  console.log(`${c.bold('Uso')}`);
315
- console.log(` primocode Interativo (Conecta Primo AI)`);
316
- console.log(` primocode "crie um site" Executa tarefa`);
314
+ console.log(` primocode Interativo (Multi-Agente)`);
315
+ console.log(` primocode "crie um site" Pipeline completo`);
317
316
  console.log(` primocode --chat "duvida" Modo conversa`);
318
317
  console.log(` primocode --jarvis Voz e visao`);
319
- console.log(` primocode -f app.js "analise" Anexa arquivo`);
320
318
  console.log(HELP);
321
319
  }
322
320
 
@@ -371,7 +369,7 @@ function startInteractive() {
371
369
  const versionFlag = flags.some(a => a === '-v' || a === '--version');
372
370
  const jarvisFlag = flags.some(a => a === '--jarvis' || a === '-j');
373
371
  if (helpFlag) { printCliHelp(); process.exit(0); }
374
- if (versionFlag) { console.log('PrimoCode'); process.exit(0); }
372
+ if (versionFlag) { console.log('PrimoCode Multi-Agent v3.0'); process.exit(0); }
375
373
  if (jarvisFlag) {
376
374
  try {
377
375
  const jarvis = require('../lib/jarvis');
package/lib/jarvis.js CHANGED
@@ -1,665 +1,67 @@
1
- // PrimoCode Jarvis - servidor local (HTTP + WebSocket hand-rolled) + página browser
2
- // Zero deps: http + crypto nativos. Roda em localhost:8765.
1
+ // JARVIS — Interface de voz e visao do PrimoCode
2
+ // Servidor HTTP local + WebSocket para comunicacao com a CLI
3
+
4
+ 'use strict';
3
5
 
4
6
  const http = require('http');
5
- const crypto = require('crypto');
6
- const { c } = require('./ui');
7
+ const WebSocket = require('ws');
8
+ const open = require('open');
9
+ const { execSync } = require('child_process');
7
10
 
8
- const PORT = Number(process.env.PRIMOCODE_JARVIS_PORT) || 8765;
9
- const HOST = '127.0.0.1';
11
+ const PORT = 8765;
10
12
 
11
- // ---------- página frontend (vibe cmd, terminal preto/verde-teal) ----------
12
13
  function renderHtml(serverUrl) {
13
- return `<!doctype html>
14
- <html lang="pt-br">
15
- <head>
16
- <meta charset="utf-8" />
17
- <meta name="viewport" content="width=device-width,initial-scale=1" />
18
- <title>JARVIS · PrimoCode</title>
19
- <style>
20
- :root {
21
- --bg:#0a0a0f;
22
- --surface:#12121a;
23
- --surface-hover:#1a1a25;
24
- --border:#252535;
25
- --fg:#e8e8f0;
26
- --fg-dim:#6b6b80;
27
- --accent:#00d4aa;
28
- --accent-glow:rgba(0,212,170,0.15);
29
- --amber:#f0a030;
30
- --err:#e05060;
31
- --radius:14px;
32
- --mono:'SF Mono','JetBrains Mono','Fira Code',ui-monospace,Menlo,monospace;
33
- }
34
- * { box-sizing:border-box; margin:0; padding:0; }
35
- html, body {
36
- height:100%; background:var(--bg); color:var(--fg);
37
- font-family:var(--mono); font-size:14px; overflow:hidden;
38
- }
39
-
40
- body::before {
41
- content:''; position:fixed; inset:0; pointer-events:none; z-index:0;
42
- background:
43
- radial-gradient(ellipse 80% 50% at 50% 0%, rgba(0,212,170,0.06) 0%, transparent 60%),
44
- radial-gradient(ellipse 60% 40% at 80% 100%, rgba(240,160,48,0.04) 0%, transparent 50%);
45
- }
46
-
47
- .app {
48
- position:relative; z-index:1;
49
- display:grid;
50
- grid-template-columns: 1fr 360px;
51
- grid-template-rows: auto 1fr auto;
52
- height:100vh; gap:16px; padding:20px;
53
- }
54
-
55
- .header {
56
- grid-column:1/-1;
57
- display:flex; align-items:center; justify-content:space-between;
58
- padding:14px 20px;
59
- background:var(--surface);
60
- border:1px solid var(--border);
61
- border-radius:var(--radius);
62
- }
63
- .brand {
64
- display:flex; align-items:center; gap:12px;
65
- font-weight:600; font-size:15px; letter-spacing:0.04em;
66
- }
67
- .pulse {
68
- width:10px; height:10px; border-radius:50%;
69
- background:var(--accent);
70
- box-shadow:0 0 0 0 var(--accent-glow);
71
- animation:pulse 2s ease-in-out infinite;
72
- }
73
- @keyframes pulse {
74
- 0%,100% { opacity:1; box-shadow:0 0 0 0 var(--accent-glow); }
75
- 50% { opacity:0.6; box-shadow:0 0 0 8px transparent; }
76
- }
77
- .status {
78
- display:flex; gap:18px; align-items:center;
79
- font-size:12px; color:var(--fg-dim);
80
- }
81
- .status-item { display:flex; align-items:center; gap:6px; }
82
- .status-dot {
83
- width:7px; height:7px; border-radius:50%;
84
- background:var(--fg-dim); transition:all 0.3s;
85
- }
86
- .status-dot.on { background:var(--accent); box-shadow:0 0 8px var(--accent); }
87
- .status-dot.err { background:var(--err); box-shadow:0 0 8px var(--err); }
88
- .status-label { font-weight:500; color:var(--fg); }
89
-
90
- .chat {
91
- background:var(--surface);
92
- border:1px solid var(--border);
93
- border-radius:var(--radius);
94
- padding:20px;
95
- overflow-y:auto;
96
- display:flex; flex-direction:column; gap:12px;
97
- scrollbar-width:thin; scrollbar-color:var(--border) transparent;
98
- }
99
- .chat::-webkit-scrollbar { width:5px; }
100
- .chat::-webkit-scrollbar-thumb { background:var(--border); border-radius:10px; }
101
-
102
- .msg {
103
- max-width:85%; padding:12px 16px;
104
- border-radius:12px; line-height:1.6;
105
- animation:fadeIn 0.3s ease;
106
- }
107
- @keyframes fadeIn { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:translateY(0); } }
108
-
109
- .msg-user {
110
- align-self:flex-end;
111
- background:linear-gradient(135deg, rgba(0,212,170,0.12), rgba(0,212,170,0.06));
112
- border:1px solid rgba(0,212,170,0.2);
113
- color:var(--fg);
114
- }
115
- .msg-jarvis {
116
- align-self:flex-start;
117
- background:var(--surface-hover);
118
- border:1px solid var(--border);
119
- color:var(--fg);
120
- }
121
- .msg-system {
122
- align-self:center;
123
- font-size:12px; color:var(--fg-dim);
124
- padding:6px 14px;
125
- background:rgba(255,255,255,0.03);
126
- border-radius:20px;
127
- }
128
- .msg-tag {
129
- font-size:11px; font-weight:600; text-transform:uppercase;
130
- letter-spacing:0.08em; margin-bottom:4px; opacity:0.7;
131
- }
132
- .msg-user .msg-tag { color:var(--accent); }
133
- .msg-jarvis .msg-tag { color:var(--amber); }
134
-
135
- .sidebar {
136
- display:flex; flex-direction:column; gap:16px;
137
- }
138
- .card {
139
- background:var(--surface);
140
- border:1px solid var(--border);
141
- border-radius:var(--radius);
142
- padding:16px;
143
- transition:border-color 0.3s;
144
- }
145
- .card:hover { border-color:var(--border); }
146
- .card-title {
147
- font-size:11px; font-weight:600; text-transform:uppercase;
148
- letter-spacing:0.12em; color:var(--fg-dim); margin-bottom:12px;
149
- }
150
-
151
- .cam-box {
152
- aspect-ratio:16/10; background:#000;
153
- border-radius:10px; overflow:hidden; position:relative;
154
- border:1px solid var(--border);
155
- }
156
- .cam-box video { width:100%; height:100%; object-fit:cover; transform:scaleX(-1); }
157
- .cam-box.off::after {
158
- content:'camera off'; position:absolute; inset:0;
159
- display:flex; align-items:center; justify-content:center;
160
- color:var(--fg-dim); font-size:13px;
161
- }
162
- .cam-info {
163
- display:flex; justify-content:space-between;
164
- font-size:12px; color:var(--fg-dim); margin-top:8px;
165
- }
166
-
167
- .wave-box {
168
- height:64px; background:#08080f;
169
- border-radius:10px; border:1px solid var(--border);
170
- overflow:hidden; position:relative;
171
- }
172
- .wave-box canvas { width:100%; height:100%; display:block; }
173
- .volume-bar {
174
- height:5px; background:var(--border); border-radius:3px;
175
- overflow:hidden; margin-top:10px;
176
- }
177
- .volume-fill {
178
- height:100%; width:0%; background:var(--accent);
179
- border-radius:3px; transition:width 0.08s linear;
180
- box-shadow:0 0 10px rgba(0,212,170,0.3);
181
- }
182
- .audio-info {
183
- display:flex; justify-content:space-between;
184
- font-size:12px; color:var(--fg-dim); margin-top:8px;
185
- }
186
-
187
- .controls { display:flex; flex-wrap:wrap; gap:8px; }
188
- .btn {
189
- background:transparent; color:var(--fg); border:1px solid var(--border);
190
- padding:8px 14px; border-radius:8px; cursor:pointer;
191
- font-family:var(--mono); font-size:12px; font-weight:500;
192
- transition:all 0.2s; letter-spacing:0.02em;
193
- }
194
- .btn:hover { border-color:var(--accent); color:var(--accent); background:var(--accent-glow); }
195
- .btn.active { border-color:var(--accent); color:var(--accent); background:var(--accent-glow); }
196
- .btn-danger:hover { border-color:var(--err); color:var(--err); background:rgba(224,80,96,0.08); }
197
-
198
- .voice-select {
199
- width:100%; margin-top:8px;
200
- background:var(--bg); color:var(--fg);
201
- border:1px solid var(--border); border-radius:8px;
202
- padding:8px 12px; font-family:var(--mono); font-size:12px;
203
- cursor:pointer;
204
- }
205
- .voice-select:focus { outline:none; border-color:var(--accent); }
206
-
207
- .footer {
208
- grid-column:1/-1;
209
- display:flex; justify-content:space-between; align-items:center;
210
- padding:12px 20px;
211
- background:var(--surface);
212
- border:1px solid var(--border);
213
- border-radius:var(--radius);
214
- font-size:12px; color:var(--fg-dim);
215
- }
216
- .footer-hint b { color:var(--fg); font-weight:500; }
217
- .footer-server { font-family:var(--mono); }
218
-
219
- .typing {
220
- display:flex; gap:4px; align-items:center;
221
- padding:8px 0;
222
- }
223
- .typing-dot {
224
- width:6px; height:6px; border-radius:50%;
225
- background:var(--fg-dim);
226
- animation:typingBounce 1.4s ease-in-out infinite;
227
- }
228
- .typing-dot:nth-child(2) { animation-delay:0.2s; }
229
- .typing-dot:nth-child(3) { animation-delay:0.4s; }
230
- @keyframes typingBounce {
231
- 0%,60%,100% { transform:translateY(0); }
232
- 30% { transform:translateY(-4px); }
233
- }
234
-
235
- .scroll-btn {
236
- position:absolute; bottom:80px; right:380px;
237
- width:36px; height:36px; border-radius:50%;
238
- background:var(--surface); border:1px solid var(--border);
239
- color:var(--fg); cursor:pointer; display:none;
240
- align-items:center; justify-content:center;
241
- font-size:16px; transition:all 0.2s;
242
- }
243
- .scroll-btn:hover { border-color:var(--accent); color:var(--accent); }
244
- .scroll-btn.show { display:flex; }
245
- </style>
246
- </head>
247
- <body>
248
- <div class="app">
249
- <div class="header">
250
- <div class="brand"><span class="pulse"></span> JARVIS · PrimoCode</div>
251
- <div class="status">
252
- <div class="status-item"><span id="d-mic" class="status-dot"></span> mic <span id="s-mic" class="status-label">off</span></div>
253
- <div class="status-item"><span id="d-cam" class="status-dot"></span> cam <span id="s-cam" class="status-label">off</span></div>
254
- <div class="status-item"><span id="d-ws" class="status-dot"></span> cli <span id="s-ws" class="status-label">...</span></div>
255
- <div class="status-item"><span id="d-brain" class="status-dot"></span> brain <span id="s-brain" class="status-label">idle</span></div>
256
- </div>
257
- </div>
258
-
259
- <div class="chat" id="log"></div>
260
- <button class="scroll-btn" id="scrollBtn" onclick="scrollToBottom()">↓</button>
261
-
262
- <div class="sidebar">
263
- <div class="card">
264
- <div class="card-title">Camera</div>
265
- <div class="cam-box off" id="camWrap"><video id="video" autoplay playsinline muted></video></div>
266
- <div class="cam-info"><span id="camRes">—</span><span id="camState">off</span></div>
267
- </div>
268
-
269
- <div class="card">
270
- <div class="card-title">Audio</div>
271
- <div class="wave-box"><canvas id="wave"></canvas></div>
272
- <div class="volume-bar"><div class="volume-fill" id="meter"></div></div>
273
- <div class="audio-info"><span id="rms">RMS 0.00</span><span id="silence">silencio 0.0s</span></div>
274
- </div>
275
-
276
- <div class="card">
277
- <div class="card-title">Controles</div>
278
- <div class="controls">
279
- <button class="btn" id="bMic">ligar mic</button>
280
- <button class="btn" id="bCam">ligar cam</button>
281
- <button class="btn btn-danger" id="bStop">parar</button>
282
- </div>
283
- <div style="margin-top:14px;">
284
- <div class="card-title" style="margin-bottom:6px;">Voz</div>
285
- <select id="voiceSel" class="voice-select"></select>
286
- <button class="btn" id="bTest" style="margin-top:8px; width:100%;">testar voz</button>
287
- </div>
288
- <div class="audio-info" style="margin-top:10px;">
289
- <span>silencio auto <b>4.0s</b></span>
290
- <span id="voice">—</span>
291
- </div>
292
- </div>
293
- </div>
294
-
295
- <div class="footer">
296
- <div class="footer-hint">Fale normalmente · silencio de 4s envia · diga <b>"o que voce ve"</b> para usar a camera · <b>Esc</b> encerra</div>
297
- <div class="footer-server">server <b>${escapeHtml(serverUrl)}</b></div>
298
- </div>
299
- </div>
300
-
301
- <script>
302
- const SERVER = ${JSON.stringify(serverUrl)};
303
- const WS_URL = 'ws://' + location.host + '/ws';
304
- const SILENCE_MS = 4000;
305
- const VISION_TRIGGERS = /\b(v[eê]|olh[ae]|c[aâ]mera|camera|imagem|foto|tela|mostrando|est[aá] vendo|voc[eê] v[eê])\b/i;
306
-
307
- const $ = (id) => document.getElementById(id);
308
- const logEl = $('log');
309
-
310
- function log(kind, text) {
311
- const el = document.createElement('div');
312
- el.className = 'msg msg-' + kind;
313
- const tag = kind==='user' ? 'Voce' : kind==='jarvis' ? 'JARVIS' : kind==='error' ? 'Erro' : '·';
314
- el.innerHTML = '<div class="msg-tag">' + tag + '</div>' + escapeHtml(text);
315
- logEl.appendChild(el);
316
- logEl.scrollTop = logEl.scrollHeight;
317
- updateScrollBtn();
318
- }
319
-
320
- function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[m])); }
321
-
322
- function setStatus(id, on, text) {
323
- const d = $('d-'+id); const s = $('s-'+id);
324
- if (d) { d.classList.remove('on','err'); if (on===true) d.classList.add('on'); else if (on==='err') d.classList.add('err'); }
325
- if (s && text) s.textContent = text;
326
- }
327
-
328
- function scrollToBottom() { logEl.scrollTop = logEl.scrollHeight; }
329
- function updateScrollBtn() {
330
- const btn = $('scrollBtn');
331
- const nearBottom = logEl.scrollHeight - logEl.scrollTop - logEl.clientHeight < 100;
332
- btn.classList.toggle('show', !nearBottom);
333
- }
334
- logEl.addEventListener('scroll', updateScrollBtn);
335
-
336
- let ws = null;
337
- function connectWS() {
338
- try {
339
- ws = new WebSocket(WS_URL);
340
- ws.onopen = () => { setStatus('ws', true, 'ok'); log('system', 'conectado ao CLI'); };
341
- ws.onclose = () => { setStatus('ws', 'err', 'off'); };
342
- ws.onerror = () => { setStatus('ws', 'err', 'erro'); };
343
- ws.onmessage = (ev) => {
344
- try {
345
- const msg = JSON.parse(ev.data);
346
- if (msg.type === 'log') log(msg.kind || 'system', msg.text);
347
- } catch {}
348
- };
349
- } catch (e) { setStatus('ws', 'err', 'erro'); }
350
- }
351
- function wsSend(obj) { try { if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj)); } catch {} }
352
- connectWS();
353
-
354
- let recognition = null;
355
- let micStream = null;
356
- let audioCtx = null;
357
- let analyser = null;
358
- let waveRAF = null;
359
- let silenceTimer = null;
360
- let silenceStart = 0;
361
- let currentTranscript = '';
362
- let isSending = false;
363
-
364
- function initRecognition() {
365
- const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
366
- if (!SR) { log('error', 'esse navegador nao suporta reconhecimento de voz (use Chrome)'); return null; }
367
- const r = new SR();
368
- r.lang = 'pt-BR'; r.continuous = true; r.interimResults = true;
369
- r.onresult = (ev) => {
370
- let interim = '', final = '';
371
- for (let i = ev.resultIndex; i < ev.results.length; i++) {
372
- const res = ev.results[i];
373
- if (res.isFinal) final += res[0].transcript; else interim += res[0].transcript;
374
- }
375
- if (final) currentTranscript += final;
376
- if (interim || final) resetSilence();
377
- };
378
- r.onerror = (e) => { if (e.error !== 'no-speech' && e.error !== 'aborted') log('error', 'stt: ' + e.error); };
379
- r.onend = () => { if (micOn) { try { r.start(); } catch {} } };
380
- return r;
381
- }
382
-
383
- function resetSilence() { silenceStart = performance.now(); }
384
-
385
- function tickSilence() {
386
- if (!micOn || isSending) return;
387
- const elapsed = performance.now() - silenceStart;
388
- const sec = (elapsed / 1000).toFixed(1);
389
- $('silence').textContent = 'silencio ' + sec + 's';
390
- if (elapsed >= SILENCE_MS && currentTranscript.trim()) {
391
- sendTranscript();
392
- }
393
- silenceTimer = requestAnimationFrame(tickSilence);
394
- }
395
-
396
- function sendTranscript() {
397
- const text = currentTranscript.trim();
398
- if (!text) return;
399
- isSending = true;
400
- log('user', text);
401
- const useVision = VISION_TRIGGERS.test(text) && camOn;
402
- wsSend({ type: 'user', text, vision: useVision });
403
- currentTranscript = '';
404
- setTimeout(() => { isSending = false; resetSilence(); }, 800);
405
- }
406
-
407
- function startWave(stream) {
408
- audioCtx = new (window.AudioContext || window.webkitAudioContext)();
409
- const src = audioCtx.createMediaStreamSource(stream);
410
- analyser = audioCtx.createAnalyser();
411
- analyser.fftSize = 256;
412
- src.connect(analyser);
413
- const canvas = $('wave');
414
- const ctx = canvas.getContext('2d');
415
- const data = new Uint8Array(analyser.frequencyBinCount);
416
-
417
- function draw() {
418
- waveRAF = requestAnimationFrame(draw);
419
- analyser.getByteFrequencyData(data);
420
- const w = canvas.width = canvas.offsetWidth * window.devicePixelRatio;
421
- const h = canvas.height = canvas.offsetHeight * window.devicePixelRatio;
422
- ctx.clearRect(0, 0, w, h);
423
- const bars = 60; const barW = w / bars;
424
- let sum = 0;
425
- for (let i = 0; i < bars; i++) {
426
- const idx = Math.floor(i * data.length / bars);
427
- const val = data[idx] / 255;
428
- sum += val;
429
- const bh = val * h * 0.85;
430
- const x = i * barW;
431
- const y = (h - bh) / 2;
432
- const hue = 160 + val * 40;
433
- ctx.fillStyle = `hsla(${hue}, 80%, 55%, ${0.3 + val * 0.7})`;
434
- ctx.beginPath();
435
- ctx.roundRect(x + 1, y, barW - 2, bh, 3);
436
- ctx.fill();
437
- }
438
- const rms = sum / bars;
439
- $('rms').textContent = 'RMS ' + rms.toFixed(2);
440
- $('meter').style.width = Math.min(100, rms * 140) + '%';
441
- }
442
- draw();
443
- }
444
-
445
- function stopWave() {
446
- if (waveRAF) cancelAnimationFrame(waveRAF);
447
- if (audioCtx) audioCtx.close();
448
- waveRAF = null; audioCtx = null; analyser = null;
449
- $('rms').textContent = 'RMS 0.00';
450
- $('meter').style.width = '0%';
451
- }
452
-
453
- let camOn = false;
454
- async function toggleCam() {
455
- if (camOn) {
456
- const v = $('video'); v.pause(); v.srcObject = null;
457
- $('camWrap').classList.add('off'); $('camState').textContent = 'off';
458
- $('camRes').textContent = '—'; camOn = false; setStatus('cam', false, 'off');
459
- return;
460
- }
461
- try {
462
- const stream = await navigator.mediaDevices.getUserMedia({ video: { width:1280, height:720 } });
463
- const v = $('video'); v.srcObject = stream;
464
- await v.play();
465
- const t = stream.getVideoTracks()[0];
466
- const s = t.getSettings();
467
- $('camWrap').classList.remove('off'); $('camState').textContent = 'on';
468
- $('camRes').textContent = (s.width||'—') + '×' + (s.height||'—');
469
- camOn = true; setStatus('cam', true, 'on');
470
- } catch (e) { log('error', 'camera: ' + e.message); }
471
- }
472
-
473
- let micOn = false;
474
- async function toggleMic() {
475
- if (micOn) {
476
- if (recognition) { try { recognition.stop(); } catch {} }
477
- if (micStream) { micStream.getTracks().forEach(t => t.stop()); }
478
- stopWave();
479
- micOn = false; setStatus('mic', false, 'off'); $('bMic').classList.remove('active'); $('bMic').textContent = 'ligar mic';
480
- if (silenceTimer) cancelAnimationFrame(silenceTimer);
481
- return;
482
- }
483
- try {
484
- micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
485
- recognition = initRecognition();
486
- if (recognition) recognition.start();
487
- startWave(micStream);
488
- resetSilence();
489
- silenceTimer = requestAnimationFrame(tickSilence);
490
- micOn = true; setStatus('mic', true, 'on'); $('bMic').classList.add('active'); $('bMic').textContent = 'desligar mic';
491
- } catch (e) { log('error', 'mic: ' + e.message); }
492
- }
493
-
494
- let voices = [];
495
- function loadVoices() {
496
- voices = speechSynthesis.getVoices().filter(v => v.lang.startsWith('pt'));
497
- const sel = $('voiceSel');
498
- sel.innerHTML = '';
499
- voices.forEach((v, i) => {
500
- const o = document.createElement('option');
501
- o.value = i; o.textContent = v.name;
502
- sel.appendChild(o);
503
- });
504
- if (voices.length) $('voice').textContent = voices[0].name;
505
- }
506
- speechSynthesis.onvoiceschanged = loadVoices;
507
- loadVoices();
508
-
509
- function speak(text) {
510
- if (!text || !voices.length) return;
511
- const u = new SpeechSynthesisUtterance(text);
512
- const idx = parseInt($('voiceSel').value, 10) || 0;
513
- u.voice = voices[idx] || voices[0];
514
- u.rate = 1.05; u.pitch = 1.0;
515
- speechSynthesis.speak(u);
516
- }
517
-
518
- $('bMic').onclick = toggleMic;
519
- $('bCam').onclick = toggleCam;
520
- $('bStop').onclick = () => { if (micOn) toggleMic(); if (camOn) toggleCam(); };
521
- $('bTest').onclick = () => speak('JARVIS online. Pronto para ajudar.');
522
- $('voiceSel').onchange = () => { const v = voices[$('voiceSel').value]; if (v) $('voice').textContent = v.name; };
523
-
524
- document.addEventListener('keydown', (e) => {
525
- if (e.key === 'Escape') {
526
- wsSend({ type: 'shutdown' });
527
- window.close();
528
- }
529
- });
530
-
531
- log('system', 'JARVIS pronto. Clique em "ligar mic" para comecar.');
532
- </script>
533
- </body>
534
- </html>`;
14
+ return `' + new_jarvis_html.replace(/`/g, '\\`').replace(/\$/g, '\\$') + `';
535
15
  }
536
16
 
537
- function wsAccept(key) {
538
- return crypto.createHash('sha1').update(key + WS_GUID).digest('base64');
539
- }
540
- function wsFrame(payload) {
541
- const data = Buffer.from(payload, 'utf8');
542
- const len = data.length;
543
- let head;
544
- if (len < 126) {
545
- head = Buffer.alloc(2);
546
- head[0] = 0x81; // FIN + text frame
547
- head[1] = len;
548
- } else if (len < 65536) {
549
- head = Buffer.alloc(4);
550
- head[0] = 0x81; head[1] = 126;
551
- head.writeUInt16BE(len, 2);
552
- } else {
553
- head = Buffer.alloc(10);
554
- head[0] = 0x81; head[1] = 127;
555
- head.writeBigUInt64BE(BigInt(len), 2);
556
- }
557
- return Buffer.concat([head, data]);
558
- }
559
- function wsParse(buf) {
560
- // retorna { text, rest } ou null se incompleto
561
- if (buf.length < 2) return null;
562
- const b0 = buf[0], b1 = buf[1];
563
- const opcode = b0 & 0x0f;
564
- const masked = (b1 & 0x80) !== 0;
565
- let len = b1 & 0x7f;
566
- let offset = 2;
567
- if (len === 126) { if (buf.length < 4) return null; len = buf.readUInt16BE(2); offset = 4; }
568
- else if (len === 127) { if (buf.length < 10) return null; len = Number(buf.readBigUInt64BE(2)); offset = 10; }
569
- if (masked) { if (buf.length < offset + 4 + len) return null; }
570
- else { if (buf.length < offset + len) return null; }
571
- let mask = null;
572
- if (masked) { mask = buf.slice(offset, offset+4); offset += 4; }
573
- const payload = buf.slice(offset, offset+len);
574
- if (masked) for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i%4];
575
- const rest = buf.slice(offset+len);
576
- if (opcode === 0x8) return { close: true, rest };
577
- if (opcode === 0x1) return { text: payload.toString('utf8'), rest };
578
- return { rest }; // binário/outros: descarta
579
- }
580
-
581
- // ---------- launch(): sobe servidor local, retorna Promise que resolve quando browser fecha ----------
582
- function launch(config, opts = {}) {
583
- const serverUrl = (config && config.server) || 'https://primocode-server.onrender.com';
584
- let clients = new Set();
585
- let shutdownRequested = false;
17
+ function launch(config) {
18
+ return new Promise((resolve, reject) => {
19
+ const server = http.createServer((req, res) => {
20
+ if (req.url === '/') {
21
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
22
+ res.end(renderHtml(config.server));
23
+ } else {
24
+ res.writeHead(404);
25
+ res.end('Not found');
26
+ }
27
+ });
586
28
 
587
- const server = http.createServer((req, res) => {
588
- if (req.url === '/' || req.url === '/index.html') {
589
- const html = renderHtml(serverUrl);
590
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
591
- res.end(html);
592
- return;
593
- }
594
- if (req.url === '/health') {
595
- res.writeHead(200, { 'Content-Type': 'application/json' });
596
- res.end(JSON.stringify({ ok:true, server: serverUrl }));
597
- return;
598
- }
599
- res.writeHead(404); res.end('nao encontrado');
600
- });
29
+ const wss = new WebSocket.Server({ server });
30
+ wss.on('connection', (ws) => {
31
+ ws.on('message', (data) => {
32
+ try {
33
+ const msg = JSON.parse(data);
34
+ if (msg.type === 'user') {
35
+ // Encaminha para a CLI processar
36
+ process.stdout.write(`\\n[JARVIS] ${msg.text}\\n`);
37
+ }
38
+ if (msg.type === 'shutdown') {
39
+ server.close();
40
+ wss.close();
41
+ resolve();
42
+ }
43
+ } catch {}
44
+ });
45
+ });
601
46
 
602
- server.on('upgrade', (req, socket) => {
603
- if (req.url !== '/ws') { socket.destroy(); return; }
604
- const key = req.headers['sec-websocket-key'];
605
- if (!key) { socket.destroy(); return; }
606
- const accept = wsAccept(key);
607
- socket.write(
608
- 'HTTP/1.1 101 Switching Protocols\r\n' +
609
- 'Upgrade: websocket\r\n' +
610
- 'Connection: Upgrade\r\n' +
611
- `Sec-WebSocket-Accept: ${accept}\r\n\r\n`
612
- );
613
- clients.add(socket);
614
- let buf = Buffer.alloc(0);
615
- socket.on('data', (chunk) => {
616
- buf = Buffer.concat([buf, chunk]);
617
- while (true) {
618
- const f = wsParse(buf);
619
- if (!f) break;
620
- buf = f.rest;
621
- if (f.close) { try { socket.end(); } catch {} clients.delete(socket); break; }
622
- if (f.text) {
623
- try {
624
- const msg = JSON.parse(f.text);
625
- if (msg.type === 'user' && typeof msg.text === 'string') {
626
- process.stdout.write(`\n${c.brand('▶')} ${c.dim('voz:')} ${c.white(msg.text)}\n`);
627
- } else if (msg.type === 'jarvis' && typeof msg.text === 'string') {
628
- process.stdout.write(`${c.accent('◀')} ${c.dim('JARVIS:')} ${msg.text}\n`);
629
- } else if (msg.type === 'shutdown') {
630
- shutdownRequested = true;
631
- server.close(() => {});
632
- for (const s of clients) { try { s.end(); } catch {} }
633
- }
634
- } catch {}
635
- }
636
- }
47
+ server.listen(PORT, () => {
48
+ const url = `http://127.0.0.1:${PORT}`;
49
+ console.log(`JARVIS aberto em ${url}`);
50
+ open(url).catch(() => {
51
+ // Fallback para Windows
52
+ try { execSync(`start ${url}`); } catch {}
53
+ });
637
54
  });
638
- socket.on('error', () => { clients.delete(socket); });
639
- socket.on('close', () => { clients.delete(socket); });
640
- });
641
55
 
642
- return new Promise((resolve, reject) => {
643
- server.listen(PORT, HOST, async () => {
644
- const url = `http://${HOST}:${PORT}`;
645
- process.stdout.write(
646
- `\n${c.brand('●')} ${c.bold('Jarvis')} ativo em ${c.accent(url)}\n` +
647
- ` ${c.muted('abrindo o navegador…')} ${c.dim('feche a aba ou pressione Esc para encerrar')}\n\n`
648
- );
649
- // abre navegador do sistema
650
- try {
651
- const { openInBrowser } = require('./tools');
652
- openInBrowser(url);
653
- } catch {
654
- process.stdout.write(` ${c.muted('abra manualmente:')} ${url}\n`);
56
+ server.on('error', (err) => {
57
+ if (err.code === 'EADDRINUSE') {
58
+ console.log(`Porta ${PORT} em uso. Abrindo navegador...`);
59
+ open(`http://127.0.0.1:${PORT}`).catch(() => {});
60
+ resolve();
61
+ } else {
62
+ reject(err);
655
63
  }
656
- // handler pra Ctrl+C encerrar
657
- const onSig = () => { try { server.close(); } catch {} process.exit(0); };
658
- process.once('SIGINT', onSig);
659
- process.once('SIGTERM', onSig);
660
64
  });
661
- server.on('error', (e) => reject(e));
662
- server.on('close', () => resolve({ ok: true }));
663
65
  });
664
66
  }
665
67
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "1.0.1",
4
- "description": "PrimoCode — assistente de engenharia com IA. Conecta Primo AI para engenharia completa, Chat para conversa direta, JARVIS para voz e visao.",
3
+ "version": "1.1.0",
4
+ "description": "PrimoCode — assistente de engenharia com IA multi-agente. Pipeline: Prompt Engineer → Construtor 1 → Construtor 2 → Analisador → Revisor → Deploy.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
7
7
  "primocode": "bin/primocode.js"
@@ -12,7 +12,13 @@
12
12
  "engines": {
13
13
  "node": ">=18.17.0"
14
14
  },
15
- "keywords": ["cli", "ai", "coding-assistant", "agent", "primocode"],
15
+ "keywords": [
16
+ "cli",
17
+ "ai",
18
+ "coding-assistant",
19
+ "agent",
20
+ "primocode"
21
+ ],
16
22
  "author": "ConectaPrimoAI <conectaprimoai@gmail.com>",
17
23
  "license": "MIT",
18
24
  "repository": {