primocode 1.0.0 → 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,11 +1,33 @@
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
+
5
+ ## Sistema Multi-Agente + RAG
6
+
7
+ ### Pipeline de 5 Agentes
8
+
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
14
+
15
+ ### RAG (Retrieval Augmented Generation)
16
+
17
+ - **Indexacao automatica** do projeto a cada hora
18
+ - **Contexto enriquecido** com arquivos relevantes
19
+ - **Busca semantica** via TF-IDF + cosine similarity
20
+
21
+ ### Memoria de Longo Prazo
22
+
23
+ - **Aprende preferencias**: stack tecnologico, estilo UI, complexidade
24
+ - **Contexto do projeto**: tecnologias, estrutura de arquivos
25
+ - **Historico indexado**: codigo gerado disponivel para consulta
4
26
 
5
27
  ## Modos
6
28
 
7
- - **Conecta Primo AI** — Engenharia completa: codigo, arquivos, terminal, navegador
8
- - **Chat** — Conversa direta e objetiva
29
+ - **Conecta Primo AI** — Pipeline multi-agente completo com RAG
30
+ - **Chat** — Conversa direta com modelo unico
9
31
  - **JARVIS** — Interacao por voz e visao
10
32
 
11
33
  ## Instalacao
@@ -14,49 +36,31 @@ Assistente de engenharia com inteligencia artificial.
14
36
  npm install -g primocode
15
37
  ```
16
38
 
17
- Ou via script:
18
-
19
- ```bash
20
- # Windows
21
- iwr -useb https://gist.githubusercontent.com/ConectaPrimoAI/primocode-install/raw/install.cmd | iex
22
-
23
- # Linux / macOS / WSL / Termux
24
- curl -fsSL https://gist.githubusercontent.com/ConectaPrimoAI/primocode-install/raw/install.sh | bash
25
- ```
26
-
27
39
  ## Uso
28
40
 
29
41
  ```bash
30
- primocode # Modo interativo
31
- primocode "crie um site" # Tarefa direta
42
+ primocode # Modo interativo (Multi-Agente + RAG)
43
+ primocode "crie um site completo" # Pipeline completo com contexto
32
44
  primocode --chat "explique REST" # Modo conversa
33
45
  primocode --jarvis # Voz e visao
34
- primocode -f app.js "analise isto" # Anexa arquivo
35
46
  ```
36
47
 
37
48
  ## Comandos
38
49
 
39
50
  | Comando | Funcao |
40
51
  |---------|--------|
41
- | `/mode` | Alterna entre Conecta Primo AI, Chat, JARVIS |
52
+ | `/mode` | Alterna entre modos |
42
53
  | `/attach` | Anexa arquivo ao contexto |
43
- | `/clear` | Limpa historico da conversa |
54
+ | `/clear` | Limpa historico |
55
+ | `/status` | Mostra status do servidor, RAG e memoria |
44
56
  | `/uninstall` | Remove PrimoCode |
45
- | `/help` | Exibe ajuda |
57
+ | `/help` | Ajuda |
46
58
  | `/exit` | Encerra |
47
59
 
48
- ## Configuracao
49
-
50
- Defina variaveis de ambiente:
51
-
52
- ```bash
53
- export PRIMOCODE_SERVER=https://seu-servidor.com
54
- export PRIMOCODE_MODEL=main
55
- ```
56
-
57
60
  ## Requisitos
58
61
 
59
62
  - Node.js 18.17+
63
+ - Servidor PrimoCode com API Groq
60
64
 
61
65
  ---
62
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,15 +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
- const { runAct } = require('../lib/act');
22
19
 
23
- // ---- Estado ----
24
20
  const state = {
25
21
  config: cfg.loadConfig(),
26
22
  history: [],
@@ -29,7 +25,6 @@ const state = {
29
25
  lastResponse: '',
30
26
  };
31
27
 
32
- // ---- Ajuda ----
33
28
  const HELP = `
34
29
  ${c.bold('Comandos')}
35
30
  /mode <nome> Conecta Primo AI | Chat | JARVIS
@@ -41,7 +36,7 @@ ${c.bold('Comandos')}
41
36
  /exit sair
42
37
 
43
38
  ${c.bold('Modos')}
44
- ${c.brand('Conecta Primo AI')} Engenharia completa
39
+ ${c.brand('Conecta Primo AI')} Engenharia completa (5 agentes)
45
40
  ${c.brand('Chat')} Conversa direta
46
41
  ${c.brand('JARVIS')} Voz e visao
47
42
  `;
@@ -55,34 +50,90 @@ function makePrompt() {
55
50
  }
56
51
  function refreshPrompt() { if (rl) rl.setPrompt(makePrompt()); }
57
52
 
58
- // ---- Envio ----
59
- async function sendPrompt(userText) {
60
- let promptWithAttach = userText;
61
- if (state.attachedFiles.length) {
62
- const parts = state.attachedFiles.map(f => `Arquivo: ${f.path}\n\`\`\`\n${f.content}\n\`\`\``);
63
- promptWithAttach = parts.join('\n\n') + '\n\n' + userText;
64
- }
53
+ // ---- Multi-Agent Pipeline ----
54
+ async function runMultiAgent(userText) {
55
+ const sp = new Spinner('Iniciando pipeline multi-agente').start();
65
56
 
66
- if (state.mode === 'Conecta Primo AI') {
67
- const r = await runAct(promptWithAttach, {
68
- server: state.config.server,
69
- model: state.config.model,
70
- 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
+ }),
71
68
  });
72
- state.history.push({ role: 'user', content: userText });
73
- state.history.push({ role: 'assistant', content: r.assistantMessage.content });
74
- state.lastResponse = r.assistantMessage.content;
75
- cfg.appendHistory({ mode: 'Conecta Primo AI', prompt: userText, response: r.assistantMessage.content });
76
- } else {
77
- await runChat(promptWithAttach, userText);
78
- }
79
69
 
80
- state.attachedFiles = [];
81
- refreshPrompt();
82
- }
70
+ sp.stop();
71
+
72
+ if (!response.ok) {
73
+ throw new Error(`HTTP ${response.status}`);
74
+ }
83
75
 
84
- // Chat
85
- async function runChat(promptForModel, userTextForMemory) {
76
+ const result = await response.json();
77
+
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) {
86
137
  const sp = new Spinner('Processando').start();
87
138
  let started = false;
88
139
  let text = '';
@@ -90,7 +141,7 @@ async function runChat(promptForModel, userTextForMemory) {
90
141
  await api.requestStream(state.config.server, '/api/normal/stream', {
91
142
  prompt: promptForModel,
92
143
  history: state.history.slice(-20),
93
- model: state.config.model,
144
+ model: cfg.resolveModel(state.mode),
94
145
  }, (evt) => {
95
146
  if (evt.erro) {
96
147
  if (!started) sp.stop();
@@ -117,7 +168,25 @@ async function runChat(promptForModel, userTextForMemory) {
117
168
  cfg.appendHistory({ mode: 'Chat', prompt: userTextForMemory, response: text });
118
169
  }
119
170
 
120
- // ---- 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 ----
121
190
  async function handleSlash(line) {
122
191
  const [cmd, ...rest] = line.trim().split(/\s+/);
123
192
  const arg = rest.join(' ').trim();
@@ -132,22 +201,30 @@ async function handleSlash(line) {
132
201
  console.log(c.muted(' Conversa limpa'));
133
202
  return;
134
203
  case '/version':
135
- console.log(c.muted(` PrimoCode`));
204
+ console.log(c.muted(` PrimoCode Multi-Agent v3.0`));
136
205
  console.log(c.muted(` Servidor: ${state.config.server}`));
137
206
  return;
138
- case '/status': {
207
+ case '/status': {
139
208
  const sp = new Spinner('Verificando').start();
140
209
  try {
141
- const h = await api.getHealth(state.config.server);
210
+ const res = await fetch(state.config.server + '/health');
211
+ const data = await res.json();
142
212
  sp.stop();
143
- 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
+ }
144
222
  } catch (e) {
145
223
  sp.stop();
146
224
  console.log(c.err(' Indisponivel'));
147
225
  }
148
226
  return;
149
- }
150
- case '/jarvis': {
227
+ }case '/jarvis': {
151
228
  try {
152
229
  const jarvis = require('../lib/jarvis');
153
230
  console.log(c.muted(' Iniciando JARVIS...'));
@@ -158,117 +235,44 @@ async function handleSlash(line) {
158
235
  return;
159
236
  }
160
237
  case '/mode': {
161
- const valid = ['Conecta Primo AI', 'Chat', 'JARVIS', 'primocode', 'chat'];
238
+ const valid = ['Conecta Primo AI', 'Chat', 'JARVIS'];
162
239
  let target = arg;
163
- if (target.toLowerCase() === 'act' || target.toLowerCase() === 'agent' || target.toLowerCase() === 'primocode') {
164
- console.log(c.muted(' Modo: Conecta Primo AI'));
165
- target = 'Conecta Primo AI';
240
+ if (!valid.includes(target)) {
241
+ console.log(c.warn(' /mode Conecta Primo AI | Chat | JARVIS'));
242
+ return;
166
243
  }
167
- if (!valid.includes(target)) { console.log(c.warn(' /mode Conecta Primo AI | Chat | JARVIS')); return; }
168
244
  state.mode = target;
245
+ state.config.model = cfg.resolveModel(target);
169
246
  console.log(c.muted(` Modo: ${target}`));
170
247
  refreshPrompt();
171
248
  return;
172
249
  }
173
- case '/model':
174
- if (!arg) { console.log(c.muted(` Modelo: ${state.config.model}`)); return; }
175
- state.config.model = arg;
176
- cfg.saveConfig({ model: arg });
177
- console.log(c.muted(` Modelo: ${arg}`));
178
- return;
179
250
  case '/attach': {
180
251
  if (!arg) { console.log(c.warn(' /attach <arquivo>')); return; }
181
252
  try {
182
253
  const content = tools.readFileSafe(arg);
183
254
  state.attachedFiles.push({ path: arg, content });
184
- console.log(c.muted(` Anexado: ${arg} (${tools.fmtBytes(Buffer.byteLength(content, 'utf8'))})`));
255
+ console.log(c.muted(` Anexado: ${arg}`));
185
256
  refreshPrompt();
186
257
  } catch (e) { console.log(c.err(` ${e.message}`)); }
187
258
  return;
188
259
  }
189
260
  case '/detach':
190
261
  state.attachedFiles = []; console.log(c.muted(' Anexos removidos')); refreshPrompt(); return;
191
- case '/files':
192
- if (!state.attachedFiles.length) console.log(c.muted(' (Nenhum)'));
193
- else for (const f of state.attachedFiles) console.log(c.muted(` ${f.path} (${tools.fmtBytes(Buffer.byteLength(f.content, 'utf8'))})`));
194
- return;
195
- case '/tree': {
196
- const d = parseInt(arg, 10) || 2;
197
- const r = tools.listDir('.', d);
198
- console.log(r.ok ? c.muted(r.tree) : c.err(` ${r.error}`));
199
- return;
200
- }
201
- case '/read': {
202
- if (!arg) { console.log(c.warn(' /read <arquivo>')); return; }
203
- try {
204
- const content = tools.readFileSafe(arg);
205
- for (const line of content.split('\n')) console.log(c.muted(' ') + c.white(line));
206
- } catch (e) { console.log(c.err(` ${e.message}`)); }
207
- return;
208
- }
209
- case '/write': {
210
- if (!arg) { console.log(c.warn(' /write <arquivo>')); return; }
211
- if (!state.lastResponse) { console.log(c.warn(' Sem resposta anterior')); return; }
212
- const blocks = tools.extractCodeBlocks(state.lastResponse);
213
- const content = blocks.length ? blocks[0].content : state.lastResponse;
214
- const r = tools.writeFile(arg, content);
215
- if (r.ok) console.log(c.muted(` ${r.created ? 'Criado' : 'Atualizado'} · ${r.path} · ${tools.fmtBytes(r.bytes)}`));
216
- else console.log(c.err(` ${r.error}`));
217
- return;
218
- }
219
- case '/apply': {
220
- if (!state.lastResponse) { console.log(c.warn(' Sem resposta anterior')); return; }
221
- const blocks = tools.extractCodeBlocks(state.lastResponse);
222
- const withFile = blocks.filter(b => b.filename);
223
- if (!withFile.length) { console.log(c.warn(' Nenhum bloco com // filename:')); return; }
224
- for (const b of withFile) {
225
- const r = tools.writeFile(b.filename, b.content);
226
- if (r.ok) console.log(c.muted(` ${r.created ? 'Criado' : 'Atualizado'} · ${r.path} · ${tools.fmtBytes(r.bytes)}`));
227
- else console.log(c.err(` ${b.filename}: ${r.error}`));
228
- }
229
- return;
230
- }
231
- case '/run': {
232
- if (!arg) { console.log(c.warn(' /run <cmd>')); return; }
233
- console.log(c.muted(` $ ${arg}`));
234
- const r = tools.runShellSync(arg);
235
- if (r.stdout) process.stdout.write(r.stdout);
236
- if (r.stderr) process.stdout.write(c.err(r.stderr));
237
- console.log(r.ok ? c.muted(' Concluido') : c.err(` Falhou`));
238
- return;
239
- }
240
- case '/open': {
241
- if (!arg) { console.log(c.warn(' /open <arquivo|url>')); return; }
242
- const r = tools.openInBrowser(arg);
243
- console.log(r.ok ? c.muted(` ${r.url}`) : c.err(` ${r.error}`));
244
- return;
245
- }
246
262
  case '/uninstall': {
247
263
  console.log(c.muted(' Removendo PrimoCode...'));
248
- const installDir = path.join(os.homedir(), '.primocode');
249
- const globalLink = path.join(os.homedir(), '.local/bin/primocode');
250
264
  try {
265
+ const installDir = path.join(os.homedir(), '.primocode');
266
+ const globalLink = path.join(os.homedir(), '.local/bin/primocode');
251
267
  if (fs.existsSync(globalLink)) fs.unlinkSync(globalLink);
252
- if (fs.existsSync(installDir)) {
253
- fs.rmSync(installDir, { recursive: true, force: true });
254
- }
268
+ if (fs.existsSync(installDir)) fs.rmSync(installDir, { recursive: true, force: true });
255
269
  console.log(c.ok(' PrimoCode removido'));
256
- console.log(c.muted(' Para reinstalar: npm install -g primocode'));
257
- } catch (e) {
258
- console.log(c.err(` Erro: ${e.message}`));
259
- }
270
+ } catch (e) { console.log(c.err(` Erro: ${e.message}`)); }
260
271
  return;
261
272
  }
262
- case '/act': {
263
- if (!arg) { console.log(c.warn(' /act <tarefa>')); return; }
264
- const r = await runAct(arg, {
265
- server: state.config.server,
266
- model: state.config.model,
267
- history: state.history,
268
- });
269
- state.history.push({ role: 'user', content: arg });
270
- state.history.push({ role: 'assistant', content: r.assistantMessage.content });
271
- 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...'));
272
276
  return;
273
277
  }
274
278
  default:
@@ -290,9 +294,8 @@ async function runOneShot(argv) {
290
294
  else if (a === '--jarvis' || a === '-j') mode = 'JARVIS';
291
295
  else if (a === '--attach' || a === '-f') attach.push(args[++i]);
292
296
  else if (a === '--help' || a === '-h') { printCliHelp(); process.exit(0); }
293
- 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); }
294
298
  else if (a === '--server') state.config.server = args[++i];
295
- else if (a === '--model') state.config.model = args[++i];
296
299
  else prompt.push(a);
297
300
  }
298
301
  if (!prompt.length) return false;
@@ -308,11 +311,10 @@ async function runOneShot(argv) {
308
311
  function printCliHelp() {
309
312
  console.log(welcomeBox(state.config));
310
313
  console.log(`${c.bold('Uso')}`);
311
- console.log(` primocode Interativo (Conecta Primo AI)`);
312
- console.log(` primocode "crie um site" Executa tarefa`);
314
+ console.log(` primocode Interativo (Multi-Agente)`);
315
+ console.log(` primocode "crie um site" Pipeline completo`);
313
316
  console.log(` primocode --chat "duvida" Modo conversa`);
314
317
  console.log(` primocode --jarvis Voz e visao`);
315
- console.log(` primocode -f app.js "analise" Anexa arquivo`);
316
318
  console.log(HELP);
317
319
  }
318
320
 
@@ -367,7 +369,7 @@ function startInteractive() {
367
369
  const versionFlag = flags.some(a => a === '-v' || a === '--version');
368
370
  const jarvisFlag = flags.some(a => a === '--jarvis' || a === '-j');
369
371
  if (helpFlag) { printCliHelp(); process.exit(0); }
370
- if (versionFlag) { console.log('PrimoCode'); process.exit(0); }
372
+ if (versionFlag) { console.log('PrimoCode Multi-Agent v3.0'); process.exit(0); }
371
373
  if (jarvisFlag) {
372
374
  try {
373
375
  const jarvis = require('../lib/jarvis');
package/lib/config.js CHANGED
@@ -15,13 +15,20 @@ function ensureDir() {
15
15
  try { fs.mkdirSync(CONFIG_DIR, { recursive: true }); } catch {}
16
16
  }
17
17
 
18
- // Mapeamento de nomes de produto para modelos do servidor
18
+ // Mapeamento de modos para modelos do servidor
19
19
  const MODEL_MAP = {
20
20
  'Conecta Primo AI': 'main',
21
21
  'Chat': 'fast',
22
22
  'JARVIS': 'main',
23
+ 'primocode': 'main',
24
+ 'chat': 'fast',
23
25
  };
24
26
 
27
+ // Resolve o modelo correto baseado no modo
28
+ function resolveModel(mode) {
29
+ return MODEL_MAP[mode] || MODEL_MAP['Conecta Primo AI'];
30
+ }
31
+
25
32
  function loadConfig() {
26
33
  ensureDir();
27
34
  let file = {};
@@ -30,11 +37,12 @@ function loadConfig() {
30
37
  } catch { file = {}; }
31
38
 
32
39
  const mode = process.env.PRIMOCODE_MODE || file.mode || 'Conecta Primo AI';
33
- const modelKey = MODEL_MAP[mode] || 'main';
40
+ // Modelo SEMPRE derivado do modo — nunca usa file.model diretamente
41
+ const model = resolveModel(mode);
34
42
 
35
43
  return {
36
44
  server: process.env.PRIMOCODE_SERVER || file.server || 'https://primocode-server.onrender.com',
37
- model: process.env.PRIMOCODE_MODEL || file.model || modelKey,
45
+ model: model,
38
46
  mode: mode,
39
47
  autoApply: file.autoApply === true,
40
48
  _file: file,
@@ -45,6 +53,10 @@ function saveConfig(partial) {
45
53
  ensureDir();
46
54
  const cur = (() => { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; } })();
47
55
  const next = { ...cur, ...partial };
56
+ // Se salvou um modo novo, atualiza o modelo tambem
57
+ if (partial.mode && MODEL_MAP[partial.mode]) {
58
+ next.model = MODEL_MAP[partial.mode];
59
+ }
48
60
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8');
49
61
  return next;
50
62
  }
@@ -56,4 +68,4 @@ function appendHistory(entry) {
56
68
  } catch {}
57
69
  }
58
70
 
59
- module.exports = { loadConfig, saveConfig, appendHistory, CONFIG_DIR, CONFIG_FILE, HISTORY_FILE, MODEL_MAP };
71
+ module.exports = { loadConfig, saveConfig, appendHistory, resolveModel, CONFIG_DIR, CONFIG_FILE, HISTORY_FILE, MODEL_MAP };
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,10 +1,10 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "1.0.0",
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
- "primocode": "./bin/primocode.js"
7
+ "primocode": "bin/primocode.js"
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node 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": {
@@ -23,7 +29,6 @@
23
29
  "url": "https://github.com/ConectaPrimoAI/primocode/issues"
24
30
  },
25
31
  "homepage": "https://github.com/ConectaPrimoAI/primocode#readme",
26
- "dependencies": {},
27
32
  "files": [
28
33
  "bin/",
29
34
  "lib/",