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 +31 -52
- package/bin/primocode.js +135 -137
- package/lib/jarvis.js +51 -649
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -1,87 +1,66 @@
|
|
|
1
|
-
# PrimoCode
|
|
1
|
+
# PrimoCode v3.1
|
|
2
2
|
|
|
3
|
-
Assistente de engenharia com
|
|
3
|
+
Assistente de engenharia com IA multi-agente, RAG e memoria de longo prazo.
|
|
4
4
|
|
|
5
|
-
##
|
|
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
|
-
###
|
|
7
|
+
### Pipeline de 5 Agentes
|
|
14
8
|
|
|
15
|
-
|
|
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
|
-
###
|
|
15
|
+
### RAG (Retrieval Augmented Generation)
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
npm install -g primocode
|
|
25
|
-
```
|
|
21
|
+
### Memoria de Longo Prazo
|
|
26
22
|
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
# Script automatico
|
|
31
|
-
curl -fsSL https://raw.githubusercontent.com/ConectaPrimoAI/primocode/main/install.sh | bash
|
|
27
|
+
## Modos
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
|
52
|
+
| `/mode` | Alterna entre modos |
|
|
63
53
|
| `/attach` | Anexa arquivo ao contexto |
|
|
64
|
-
| `/clear` | Limpa historico
|
|
54
|
+
| `/clear` | Limpa historico |
|
|
55
|
+
| `/status` | Mostra status do servidor, RAG e memoria |
|
|
65
56
|
| `/uninstall` | Remove PrimoCode |
|
|
66
|
-
| `/help` |
|
|
57
|
+
| `/help` | Ajuda |
|
|
67
58
|
| `/exit` | Encerra |
|
|
68
59
|
|
|
69
|
-
##
|
|
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
|
-
|
|
81
|
-
-
|
|
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
|
|
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
|
-
// ----
|
|
62
|
-
async function
|
|
63
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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:
|
|
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
|
-
// ----
|
|
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
|
-
|
|
207
|
+
case '/status': {
|
|
142
208
|
const sp = new Spinner('Verificando').start();
|
|
143
209
|
try {
|
|
144
|
-
const
|
|
210
|
+
const res = await fetch(state.config.server + '/health');
|
|
211
|
+
const data = await res.json();
|
|
145
212
|
sp.stop();
|
|
146
|
-
|
|
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'
|
|
238
|
+
const valid = ['Conecta Primo AI', 'Chat', 'JARVIS'];
|
|
165
239
|
let target = arg;
|
|
166
|
-
if (
|
|
167
|
-
console.log(c.
|
|
168
|
-
|
|
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 =
|
|
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}
|
|
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
|
-
|
|
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 '/
|
|
267
|
-
if (!
|
|
268
|
-
|
|
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 (
|
|
316
|
-
console.log(` primocode "crie um site"
|
|
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
|
-
//
|
|
2
|
-
//
|
|
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
|
|
6
|
-
const
|
|
7
|
+
const WebSocket = require('ws');
|
|
8
|
+
const open = require('open');
|
|
9
|
+
const { execSync } = require('child_process');
|
|
7
10
|
|
|
8
|
-
const PORT =
|
|
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
|
|
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 => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[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
|
|
538
|
-
return
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
|
4
|
-
"description": "PrimoCode — assistente de engenharia com IA.
|
|
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": [
|
|
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": {
|