primocode 9.8.0-beta.7 → 9.8.0-beta.9
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 +6 -5
- package/app/app.css +482 -281
- package/app/app.js +888 -315
- package/app/index.html +176 -101
- package/app/primo-marca.js +58 -1
- package/bin/primocode.js +99 -39
- package/lib/act.js +5 -0
- package/lib/app.js +297 -89
- package/lib/fala.js +95 -1
- package/lib/offline.js +168 -0
- package/lib/{ide.js → painel.js} +41 -87
- package/package.json +2 -3
- package/skills/design-de-verdade/SKILL.md +2 -0
- package/skills/motion-de-primeira/SKILL.md +1 -1
- package/skills/site-com-motion/SKILL.md +39 -0
- package/app/ide.css +0 -345
- package/app/ide.html +0 -183
- package/app/ide.js +0 -639
- package/kits/acolhedor/index.html +0 -130
- package/kits/acolhedor/script.js +0 -25
- package/kits/acolhedor/style.css +0 -146
- package/kits/editorial/index.html +0 -115
- package/kits/editorial/script.js +0 -19
- package/kits/editorial/style.css +0 -112
- package/kits/landing/index.html +0 -162
- package/kits/landing/script.js +0 -199
- package/kits/landing/style.css +0 -219
- package/kits/premium/index.html +0 -83
- package/kits/premium/script.js +0 -11
- package/kits/premium/style.css +0 -87
- package/kits/vitrine/index.html +0 -108
- package/kits/vitrine/script.js +0 -20
- package/kits/vitrine/style.css +0 -117
- package/lib/kits.js +0 -206
package/lib/fala.js
CHANGED
|
@@ -37,9 +37,95 @@
|
|
|
37
37
|
|
|
38
38
|
'use strict';
|
|
39
39
|
|
|
40
|
+
const { EventEmitter } = require('events');
|
|
40
41
|
const { c, actionHeader, friendlyError } = require('./ui');
|
|
41
42
|
const markdown = require('./markdown.js');
|
|
42
43
|
|
|
44
|
+
/* ── O CANAL ESTRUTURADO ─────────────────────────────────────────────────
|
|
45
|
+
* A janela do app (lib/app.js) mostra a conversa como CHAT: a fala do Primo
|
|
46
|
+
* em texto e cada ação dele numa linha cinza ("Leu index.html", "Rodou npm
|
|
47
|
+
* test ✓"). Adivinhar isso relendo a saída colorida do terminal seria frágil
|
|
48
|
+
* — um rótulo que muda aqui quebraria a janela calado. Então a MESMA função
|
|
49
|
+
* que desenha no terminal também avisa, por este canal, o que desenhou.
|
|
50
|
+
*
|
|
51
|
+
* Sem ouvinte (terminal puro), `avisar` não faz nada: nenhum custo para quem
|
|
52
|
+
* não abriu a janela. */
|
|
53
|
+
const eventos = new EventEmitter();
|
|
54
|
+
eventos.setMaxListeners(20);
|
|
55
|
+
function avisar(tipo, dados) {
|
|
56
|
+
if (!eventos.listenerCount('evento')) return;
|
|
57
|
+
try { eventos.emit('evento', Object.assign({ tipo }, dados || {})); } catch { /* a janela nunca derruba o agente */ }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* O que a linha de ação da janela diz, por ferramenta. Verbo no passado
|
|
61
|
+
porque a linha fica na conversa depois que a ação acabou — "Lendo" viraria
|
|
62
|
+
mentira um segundo depois. O ícone é uma família, não um desenho por tool:
|
|
63
|
+
a página tem dez ícones, não setenta. */
|
|
64
|
+
const VERBOS = {
|
|
65
|
+
read_file: ['Leu', 'ler'], write_file: ['Escreveu', 'escrever'], edit_file: ['Editou', 'editar'],
|
|
66
|
+
multi_edit: ['Editou', 'editar'], list_dir: ['Listou', 'buscar'], glob: ['Procurou', 'buscar'],
|
|
67
|
+
grep: ['Buscou', 'buscar'], run_shell: ['Rodou', 'rodar'], run_in_new_terminal: ['Abriu um terminal com', 'rodar'],
|
|
68
|
+
open_in_browser: ['Abriu', 'web'], spawn_agent: ['Chamou um agente', 'agente'], write_todos: ['Planejou', 'plano'],
|
|
69
|
+
finish: ['Concluiu', 'plano'], pesquisar_referencia: ['Pesquisou', 'web'], remember_general: ['Guardou na memória', 'plano'],
|
|
70
|
+
planejar_producao: ['Planejou a produção', 'agente'], construir_app: ['Construiu', 'agente'],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Verbo, alvo e ícone de uma ação — só o que cabe numa linha, nunca o
|
|
74
|
+
* conteúdo inteiro de um write_file. */
|
|
75
|
+
function resumoDaAcao(name, args) {
|
|
76
|
+
const a = args || {};
|
|
77
|
+
let [verbo, icone] = VERBOS[name] || [];
|
|
78
|
+
if (!verbo) {
|
|
79
|
+
if (/^browser_/.test(name)) { verbo = 'Navegador · ' + name.slice(8); icone = 'web'; }
|
|
80
|
+
else if (/^desktop_/.test(name)) { verbo = 'Computador · ' + name.slice(8).replace(/_/g, ' '); icone = 'computador'; }
|
|
81
|
+
else if (/^estudio_/.test(name)) { verbo = 'Estúdio · ' + name.slice(8); icone = 'agente'; }
|
|
82
|
+
else { verbo = name; icone = 'outro'; }
|
|
83
|
+
}
|
|
84
|
+
let alvo = a.path || a.command || a.pattern || a.consulta || a.url || a.target || a.app || a.selector
|
|
85
|
+
|| a.titulo || a.description || a.arquivo || (a.todos ? `${a.todos.length} tarefas` : '') || '';
|
|
86
|
+
if (typeof alvo !== 'string') alvo = '';
|
|
87
|
+
alvo = alvo.replace(/\s+/g, ' ').trim();
|
|
88
|
+
if (alvo.length > 120) alvo = alvo.slice(0, 119) + '…';
|
|
89
|
+
return { nome: name, verbo, alvo, icone };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** O resultado de uma ação, resumido para a linha da janela: ok ou não, o
|
|
93
|
+
* "+12 −3" das edições e até 40 linhas de detalhe para quem expandir. */
|
|
94
|
+
function resumoDoResultado(name, r) {
|
|
95
|
+
if (!r || typeof r !== 'object') return { nome: name, ok: true };
|
|
96
|
+
if (r.ok === false) {
|
|
97
|
+
return { nome: name, ok: false, resumo: String(r.error || `exit ${r.code ?? 'erro'}`).replace(/\s+/g, ' ').trim().slice(0, 300) };
|
|
98
|
+
}
|
|
99
|
+
const fora = { nome: name, ok: true };
|
|
100
|
+
if (typeof r._antes === 'string' && typeof r._depois === 'string') {
|
|
101
|
+
try {
|
|
102
|
+
const diff = require('./diff.js');
|
|
103
|
+
const n = r._antes.split('\n').length + r._depois.split('\n').length;
|
|
104
|
+
if (n <= diff.MAX_LINHAS_ARQUIVO * 2) {
|
|
105
|
+
const ops = diff.operacoes(r._antes, r._depois);
|
|
106
|
+
Object.assign(fora, diff.contar(ops));
|
|
107
|
+
fora.detalhe = ops.filter((o) => o.tipo !== ' ').slice(0, 40).map((o) => o.tipo + ' ' + o.texto).join('\n');
|
|
108
|
+
}
|
|
109
|
+
} catch { /* sem diff: a linha sai sem os números */ }
|
|
110
|
+
}
|
|
111
|
+
if (name === 'write_file') fora.criado = !!r.created;
|
|
112
|
+
if (name === 'run_shell' || name === 'run_in_new_terminal') {
|
|
113
|
+
const saida = String(r.stdout || '').trim() + (r.stderr ? '\n' + String(r.stderr).trim() : '');
|
|
114
|
+
fora.detalhe = saida.split('\n').slice(-40).join('\n').trim();
|
|
115
|
+
fora.codigo = r.code ?? 0;
|
|
116
|
+
}
|
|
117
|
+
const plural = (n, um, varios) => `${n} ${n === 1 ? um : varios}`;
|
|
118
|
+
if (name === 'read_file') fora.resumo = plural(r.linhas || 0, 'linha', 'linhas');
|
|
119
|
+
if (name === 'grep') fora.resumo = plural(r.total || 0, 'ocorrência', 'ocorrências');
|
|
120
|
+
if (name === 'glob') fora.resumo = plural(r.total || 0, 'arquivo', 'arquivos');
|
|
121
|
+
if (name === 'list_dir' && r.tree) fora.detalhe = String(r.tree).split('\n').slice(0, 40).join('\n');
|
|
122
|
+
if (name === 'write_todos' && Array.isArray(r.todos)) {
|
|
123
|
+
fora.detalhe = r.todos.map((t) => (t.status === 'completed' ? '✓ ' : '○ ') + t.content).join('\n');
|
|
124
|
+
fora.resumo = `${r.feitos}/${r.total}`;
|
|
125
|
+
}
|
|
126
|
+
return fora;
|
|
127
|
+
}
|
|
128
|
+
|
|
43
129
|
/* Até este tamanho, o texto que o modelo escreve antes de chamar uma tool é
|
|
44
130
|
guardado e sai de uma vez, como pensamento. Passou disso sem tool à vista,
|
|
45
131
|
é resposta — e volta a correr na tela conforme chega. Uma frase de
|
|
@@ -58,6 +144,7 @@ function pensamento(texto, profundidade) {
|
|
|
58
144
|
if (!ls.length || !ls[0]) return '';
|
|
59
145
|
const p = ident(profundidade);
|
|
60
146
|
const saida = ls.map((l, i) => `${p} ${i === 0 ? c.dim('✦') : ' '} ${c.muted(l)}`).join('\n');
|
|
147
|
+
avisar('pensamento', { texto: ls.join('\n'), prof: profundidade || 0 });
|
|
61
148
|
console.log(saida);
|
|
62
149
|
return saida;
|
|
63
150
|
}
|
|
@@ -65,6 +152,7 @@ function pensamento(texto, profundidade) {
|
|
|
65
152
|
/** A ação (tool) que ele decidiu fazer. */
|
|
66
153
|
function acao(name, args, profundidade) {
|
|
67
154
|
const saida = ident(profundidade) + actionHeader(name, args);
|
|
155
|
+
avisar('acao', Object.assign(resumoDaAcao(name, args), { prof: profundidade || 0 }));
|
|
68
156
|
console.log(saida);
|
|
69
157
|
return saida;
|
|
70
158
|
}
|
|
@@ -102,6 +190,7 @@ function resposta(texto) {
|
|
|
102
190
|
const largura = Math.max(40, (process.stdout.columns || 80) - 4);
|
|
103
191
|
const linhasMd = markdown.renderizar(texto, largura);
|
|
104
192
|
const saida = '\n' + linhasMd.map((l, i) => (i === 0 ? `${c.brand('●')} ${l}` : ` ${l}`)).join('\n');
|
|
193
|
+
avisar('resposta', { texto: String(texto) });
|
|
105
194
|
console.log(saida);
|
|
106
195
|
return saida;
|
|
107
196
|
}
|
|
@@ -111,6 +200,7 @@ function resposta(texto) {
|
|
|
111
200
|
function aviso(texto) {
|
|
112
201
|
if (!String(texto || '').trim()) return '';
|
|
113
202
|
const saida = '\n' + formatar(texto, c.warn('●'));
|
|
203
|
+
avisar('aviso', { texto: String(texto) });
|
|
114
204
|
console.log(saida);
|
|
115
205
|
return saida;
|
|
116
206
|
}
|
|
@@ -154,9 +244,11 @@ function fluxo() {
|
|
|
154
244
|
escrever(delta) {
|
|
155
245
|
const t = String(delta || '');
|
|
156
246
|
if (!t) return;
|
|
247
|
+
avisar('pedaco', { texto: t });
|
|
157
248
|
sair(desenho.pedaco(t));
|
|
158
249
|
},
|
|
159
250
|
fechar() {
|
|
251
|
+
avisar('fim', {});
|
|
160
252
|
sair(desenho.fim());
|
|
161
253
|
if (escreveu) process.stdout.write('\n');
|
|
162
254
|
},
|
|
@@ -174,6 +266,7 @@ function paralelo(quantos, profundidade) {
|
|
|
174
266
|
/** Um agente delegado começando: "◆ Agente·frontend a folha de estilo". */
|
|
175
267
|
function agente(rotulo, descricao, profundidade) {
|
|
176
268
|
const saida = `${ident(profundidade)} ${c.brand('◆')} ${c.bold(rotulo || 'Agente')}${descricao ? ' ' + c.muted(descricao) : ''}`;
|
|
269
|
+
avisar('acao', { nome: 'agente', verbo: rotulo || 'Agente', alvo: String(descricao || '').slice(0, 120), icone: 'agente', prof: profundidade || 0 });
|
|
177
270
|
console.log(saida);
|
|
178
271
|
return saida;
|
|
179
272
|
}
|
|
@@ -182,8 +275,9 @@ function agente(rotulo, descricao, profundidade) {
|
|
|
182
275
|
function agenteVoltou(resumo, profundidade) {
|
|
183
276
|
const texto = String(resumo || 'sem resultado').replace(/\s+/g, ' ').trim();
|
|
184
277
|
const saida = resultado(profundidade) + c.muted(texto.length > 140 ? texto.slice(0, 139) + '…' : texto);
|
|
278
|
+
avisar('resultado', { nome: 'agente', ok: true, detalhe: texto.slice(0, 2000) });
|
|
185
279
|
console.log(saida);
|
|
186
280
|
return saida;
|
|
187
281
|
}
|
|
188
282
|
|
|
189
|
-
module.exports = { LIMIAR_PENSAMENTO, pensamento, acao, resultado, recuo, resposta, aviso, problema, frase, fluxo, paralelo, agente, agenteVoltou };
|
|
283
|
+
module.exports = { eventos, avisar, resumoDaAcao, resumoDoResultado, LIMIAR_PENSAMENTO, pensamento, acao, resultado, recuo, resposta, aviso, problema, frase, fluxo, paralelo, agente, agenteVoltou };
|
package/lib/offline.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* offline.js — o primocode-server rodando AQUI, no computador da pessoa.
|
|
3
|
+
*
|
|
4
|
+
* "Como eu não tenho acesso ao Railway, enquanto isso eu posso testar com o
|
|
5
|
+
* projeto rodando localmente, chamando as APIs pelo meu computador."
|
|
6
|
+
* (24/09/2026)
|
|
7
|
+
*
|
|
8
|
+
* O /offline sobe o servidor da pasta primocode-server com as MESMAS chaves
|
|
9
|
+
* que o Render usa (coladas em ~/.primocode/servidor.env) e aponta o CLI para
|
|
10
|
+
* ele até a sessão acabar. É temporário: serve para testar o servidor novo
|
|
11
|
+
* antes de ele ir para produção.
|
|
12
|
+
*
|
|
13
|
+
* "Offline" é o nome que o pedido deu, e ele mente um pouco: as IAs continuam
|
|
14
|
+
* na internet. O que sai do caminho é o servidor de produção.
|
|
15
|
+
*
|
|
16
|
+
* ── O QUE NUNCA APARECE ──────────────────────────────────────────────────
|
|
17
|
+
* O valor de uma chave. Mostram-se só os NOMES das variáveis que o arquivo
|
|
18
|
+
* tem, o bastante para ver o que falta.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const http = require('http');
|
|
27
|
+
const { spawn } = require('child_process');
|
|
28
|
+
|
|
29
|
+
const PORTA = Number(process.env.PRIMOCODE_OFFLINE_PORTA || 3190);
|
|
30
|
+
const CASA = path.join(os.homedir(), '.primocode');
|
|
31
|
+
const ARQUIVO_ENV = path.join(CASA, 'servidor.env');
|
|
32
|
+
const ARQUIVO_LOG = path.join(CASA, 'servidor-local.log');
|
|
33
|
+
|
|
34
|
+
// As variáveis que o primocode-server lê. As de chave vêm primeiro: sem pelo
|
|
35
|
+
// menos uma delas, o servidor sobe mas não tem com quem falar.
|
|
36
|
+
const CHAVES = ['GROQ_API_KEY', 'OPENROUTER_API_KEY', 'GEMINI_API_KEY', 'GOOGLE_API_KEY', 'COHERE_API_KEY', 'HF_TOKEN', 'HUGGINGFACE_API_KEY', 'PG_GATEWAY_API_KEY'];
|
|
37
|
+
const OUTRAS = ['PG_GATEWAY_BASE_URL', 'CONECTA_API', 'PRIMOCODE_TOKEN', 'REQUIRE_TOKEN', 'RATE_LIMIT_PER_HOUR'];
|
|
38
|
+
|
|
39
|
+
const MODELO_ENV = `# As chaves do primocode-server rodando no seu computador (/offline).
|
|
40
|
+
# Copie do Render: o serviço primocode-server → Environment. Uma por linha,
|
|
41
|
+
# NOME=valor, sem aspas. Este arquivo é só seu: não mande para ninguém.
|
|
42
|
+
#
|
|
43
|
+
${CHAVES.map((k) => `# ${k}=`).join('\n')}
|
|
44
|
+
#
|
|
45
|
+
${OUTRAS.map((k) => `# ${k}=`).join('\n')}
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
/** Lê um .env simples: NOME=valor, # comenta, aspas em volta saem. */
|
|
49
|
+
function lerEnv(texto) {
|
|
50
|
+
const env = {};
|
|
51
|
+
for (const linha of String(texto || '').split(/\r?\n/)) {
|
|
52
|
+
const m = linha.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
53
|
+
if (!m) continue;
|
|
54
|
+
let valor = m[2];
|
|
55
|
+
if (/^(['"]).*\1$/.test(valor)) valor = valor.slice(1, -1);
|
|
56
|
+
if (valor) env[m[1]] = valor;
|
|
57
|
+
}
|
|
58
|
+
return env;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** O arquivo de chaves: cria o modelo (vazio, 600) na primeira vez. */
|
|
62
|
+
function prepararEnv(arquivo = ARQUIVO_ENV) {
|
|
63
|
+
let criado = false;
|
|
64
|
+
if (!fs.existsSync(arquivo)) {
|
|
65
|
+
fs.mkdirSync(path.dirname(arquivo), { recursive: true });
|
|
66
|
+
fs.writeFileSync(arquivo, MODELO_ENV, { mode: 0o600 });
|
|
67
|
+
criado = true;
|
|
68
|
+
}
|
|
69
|
+
const env = lerEnv(fs.readFileSync(arquivo, 'utf8'));
|
|
70
|
+
return { arquivo, criado, env, nomes: Object.keys(env), temChave: CHAVES.some((k) => env[k]) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ehServidor(dir) {
|
|
74
|
+
try {
|
|
75
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
76
|
+
return pkg.name === 'primocode-server' && fs.existsSync(path.join(dir, 'server.js'));
|
|
77
|
+
} catch { return false; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Onde está a pasta do primocode-server. A primeira que existir ganha. */
|
|
81
|
+
function acharServidor({ pedido = '', raizCli = path.join(__dirname, '..') } = {}) {
|
|
82
|
+
const candidatos = [
|
|
83
|
+
pedido,
|
|
84
|
+
process.env.PRIMOCODE_SERVER_DIR,
|
|
85
|
+
path.join(raizCli, '..', 'primocode-server'),
|
|
86
|
+
path.join(os.homedir(), 'Primocode CLI', 'primocode-server'),
|
|
87
|
+
path.join(os.homedir(), 'primocode-server'),
|
|
88
|
+
].filter(Boolean).map((p) => path.resolve(p.replace(/^~(?=$|\/)/, os.homedir())));
|
|
89
|
+
return candidatos.find(ehServidor) || null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** GET /health; devolve o JSON ou null. */
|
|
93
|
+
function saude(url, espera = 1500) {
|
|
94
|
+
return new Promise((resolve) => {
|
|
95
|
+
const req = http.get(url + '/health', { timeout: espera }, (res) => {
|
|
96
|
+
let corpo = '';
|
|
97
|
+
res.on('data', (d) => { corpo += d; });
|
|
98
|
+
res.on('end', () => { try { resolve(JSON.parse(corpo)); } catch { resolve(null); } });
|
|
99
|
+
});
|
|
100
|
+
req.on('timeout', () => req.destroy());
|
|
101
|
+
req.on('error', () => resolve(null));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let filho = null;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Sobe o servidor (ou reaproveita um que já responde na porta).
|
|
109
|
+
* Devolve { ok, url, versao, reaproveitado } ou { ok: false, error, comoResolver }.
|
|
110
|
+
*/
|
|
111
|
+
async function ligar({ dir, env = {}, porta = PORTA, log = ARQUIVO_LOG, prazo = 20000 } = {}) {
|
|
112
|
+
const url = `http://localhost:${porta}`;
|
|
113
|
+
const ja = await saude(url);
|
|
114
|
+
if (ja && ja.status === 'ok') return { ok: true, url, versao: ja.version, reaproveitado: true };
|
|
115
|
+
if (!dir) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
error: 'Não achei a pasta do primocode-server.',
|
|
119
|
+
comoResolver: 'clone o repo ao lado do primocode, ou diga onde ele está: /offline ~/caminho/primocode-server',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (!fs.existsSync(path.join(dir, 'node_modules'))) {
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
error: `O servidor em ${dir} ainda não tem as dependências.`,
|
|
126
|
+
comoResolver: `rode uma vez: cd "${dir}" && npm install`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
fs.mkdirSync(path.dirname(log), { recursive: true });
|
|
130
|
+
const saida = fs.openSync(log, 'a');
|
|
131
|
+
filho = spawn(process.execPath, ['server.js'], {
|
|
132
|
+
cwd: dir,
|
|
133
|
+
env: { ...process.env, ...env, PORT: String(porta) },
|
|
134
|
+
stdio: ['ignore', saida, saida],
|
|
135
|
+
});
|
|
136
|
+
let morreu = null;
|
|
137
|
+
filho.on('exit', (codigo) => { morreu = codigo; filho = null; });
|
|
138
|
+
|
|
139
|
+
const fim = Date.now() + prazo;
|
|
140
|
+
while (Date.now() < fim) {
|
|
141
|
+
if (morreu !== null) {
|
|
142
|
+
return { ok: false, error: `O servidor fechou sozinho (código ${morreu}).`, comoResolver: `veja o fim de ${log}` };
|
|
143
|
+
}
|
|
144
|
+
const h = await saude(url);
|
|
145
|
+
if (h && h.status === 'ok') return { ok: true, url, versao: h.version, reaproveitado: false };
|
|
146
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
147
|
+
}
|
|
148
|
+
desligar();
|
|
149
|
+
return { ok: false, error: `O servidor não respondeu em ${Math.round(prazo / 1000)}s.`, comoResolver: `veja o fim de ${log}` };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Desliga o servidor que ESTE processo subiu. Um reaproveitado fica de pé. */
|
|
153
|
+
function desligar() {
|
|
154
|
+
if (!filho) return false;
|
|
155
|
+
try { filho.kill(); } catch {}
|
|
156
|
+
filho = null;
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function ligado() { return Boolean(filho); }
|
|
161
|
+
|
|
162
|
+
// O servidor não pode sobrar rodando quando o CLI fecha.
|
|
163
|
+
process.on('exit', desligar);
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
PORTA, ARQUIVO_ENV, ARQUIVO_LOG, CHAVES,
|
|
167
|
+
lerEnv, prepararEnv, acharServidor, ehServidor, saude, ligar, desligar, ligado,
|
|
168
|
+
};
|
package/lib/{ide.js → painel.js}
RENAMED
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* painel.js — o painel da direita da janela: a prévia do site e os arquivos.
|
|
3
3
|
*
|
|
4
|
-
* "
|
|
4
|
+
* "Remove a IDE ou deixa idêntico ao Codex."
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* Antes isto era a PrimoCode IDE: árvore, EDITOR de código, prévia e chat. O
|
|
7
|
+
* editor saiu. Quem usa o PrimoCode não programa — um editor de código na
|
|
8
|
+
* cara dessa pessoa é uma porta para quebrar o que o agente fez, sem saber
|
|
9
|
+
* como voltar. O que ficou é o que ela usa de verdade: ver o site que está
|
|
10
|
+
* nascendo (Prévia) e olhar o que tem na pasta (Arquivos), só leitura.
|
|
11
|
+
*
|
|
12
|
+
* ── POR QUE MORA DENTRO DO SERVIDOR DA JANELA ────────────────────────────
|
|
13
|
+
* A janela (lib/app.js) já resolveu o difícil: 127.0.0.1, chave sorteada e o
|
|
14
|
+
* fluxo de eventos (SSE). Aqui só entram as rotas do painel (árvore, ler,
|
|
15
|
+
* prévia) e o vigia da pasta, que empurra "arquivo mudou" pelo MESMO fluxo —
|
|
16
|
+
* é o que faz a prévia recarregar enquanto o agente escreve.
|
|
14
17
|
*
|
|
15
18
|
* ── SEGURANÇA ────────────────────────────────────────────────────────────
|
|
16
|
-
* Estas rotas leem e ESCREVEM no disco do usuário. Três travas:
|
|
17
19
|
* · a chave do app.js vale aqui também (tudo mora em /api/);
|
|
18
20
|
* · todo caminho é resolvido DENTRO da pasta do projeto — "../", caminho
|
|
19
21
|
* absoluto e link simbólico que aponta para fora são recusados;
|
|
22
|
+
* · nada aqui escreve no disco: sem editor, não há rota de salvar;
|
|
20
23
|
* · a prévia roda num iframe sandbox SEM allow-same-origin, e o servidor
|
|
21
24
|
* manda o mesmo sandbox no cabeçalho. O código da prévia foi escrito pelo
|
|
22
|
-
* agente
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
+
* agente; se ele herdasse a origem da janela, poderia chamar /api/enviar
|
|
26
|
+
* e dar ordens ao agente. Com origem opaca, o fetch dele sai com
|
|
27
|
+
* `Origin: null` e o app.js recusa.
|
|
25
28
|
*/
|
|
26
29
|
|
|
27
30
|
'use strict';
|
|
@@ -36,9 +39,9 @@ const IGNORADOS = new Set(['node_modules', '.git', '.DS_Store', '.next', '.cache
|
|
|
36
39
|
'__pycache__', '.venv', 'venv', '.turbo', '.parcel-cache']);
|
|
37
40
|
const MAX_ITENS = 4000;
|
|
38
41
|
const MAX_PROFUNDIDADE = 12;
|
|
39
|
-
// 1 MB: acima disso
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
+
// 1 MB: acima disso o visualizador engasga, e arquivo desse tamanho num
|
|
43
|
+
// projeto de iniciante é quase sempre gerado (bundle, dado).
|
|
44
|
+
const MAX_LEITURA = 1024 * 1024;
|
|
42
45
|
|
|
43
46
|
// ── caminhos ─────────────────────────────────────────────────────────────
|
|
44
47
|
|
|
@@ -46,12 +49,12 @@ const MAX_EDITOR = 1024 * 1024;
|
|
|
46
49
|
*
|
|
47
50
|
* `path.relative` pega o "../" e o caminho absoluto; o `realpath` pega o link
|
|
48
51
|
* simbólico, que passa pelo primeiro teste com cara de arquivo de dentro e
|
|
49
|
-
* aponta para ~/.ssh.
|
|
52
|
+
* aponta para ~/.ssh. Caminho que ainda não existe é checado
|
|
50
53
|
* pela pasta onde vai nascer. */
|
|
51
54
|
function dentro(raiz, relativo) {
|
|
52
55
|
if (typeof relativo !== 'string' || relativo.includes('\0')) return null;
|
|
53
56
|
// Absoluto é recusado de cara, e não "reinterpretado" como relativo:
|
|
54
|
-
// quem manda /etc/hosts quer o /etc/hosts, e
|
|
57
|
+
// quem manda /etc/hosts quer o /etc/hosts, e ler <projeto>/etc/hosts
|
|
55
58
|
// no lugar seria uma surpresa. `C:\` e `\\servidor` também contam.
|
|
56
59
|
if (path.isAbsolute(relativo) || path.win32.isAbsolute(relativo)) return null;
|
|
57
60
|
const base = path.resolve(raiz);
|
|
@@ -113,11 +116,10 @@ function arvore(raiz) {
|
|
|
113
116
|
return { itens, cortada };
|
|
114
117
|
}
|
|
115
118
|
|
|
116
|
-
// ── ler
|
|
119
|
+
// ── ler ──────────────────────────────────────────────────────────────────
|
|
117
120
|
|
|
118
121
|
/* Binário é o que tem byte zero nos primeiros 8 KB — o mesmo teste do git.
|
|
119
|
-
Mostrar um PNG como texto
|
|
120
|
-
destruiria a imagem sem aviso. */
|
|
122
|
+
Mostrar um PNG como texto é uma tela de lixo que parece arquivo quebrado. */
|
|
121
123
|
function ehBinario(buf) {
|
|
122
124
|
const n = Math.min(buf.length, 8000);
|
|
123
125
|
for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
|
|
@@ -131,35 +133,12 @@ function ler(raiz, rel) {
|
|
|
131
133
|
try { st = fs.statSync(abs); } catch { return { status: 404, corpo: { ok: false, error: 'não existe' } }; }
|
|
132
134
|
if (!st.isFile()) return { status: 400, corpo: { ok: false, error: 'não é arquivo' } };
|
|
133
135
|
const base = { ok: true, caminho: relativoDe(raiz, abs), tamanho: st.size, mtime: st.mtimeMs };
|
|
134
|
-
if (st.size >
|
|
136
|
+
if (st.size > MAX_LEITURA) return { status: 200, corpo: Object.assign(base, { grande: true }) };
|
|
135
137
|
const buf = fs.readFileSync(abs);
|
|
136
138
|
if (ehBinario(buf)) return { status: 200, corpo: Object.assign(base, { binario: true }) };
|
|
137
139
|
return { status: 200, corpo: Object.assign(base, { conteudo: buf.toString('utf8') }) };
|
|
138
140
|
}
|
|
139
141
|
|
|
140
|
-
/* `mtimeBase` é o mtime que a página viu ao abrir o arquivo. Se o disco está
|
|
141
|
-
mais novo, alguém (quase sempre o agente) mexeu no meio — e salvar por cima
|
|
142
|
-
apagaria o trabalho dele sem ninguém perceber. A página decide: recarregar
|
|
143
|
-
ou mandar de novo com `forcar`. */
|
|
144
|
-
function salvar(raiz, dados) {
|
|
145
|
-
const rel = dados && dados.caminho;
|
|
146
|
-
const conteudo = dados && dados.conteudo;
|
|
147
|
-
if (typeof conteudo !== 'string') return { status: 400, corpo: { ok: false, error: 'conteúdo faltando' } };
|
|
148
|
-
if (Buffer.byteLength(conteudo) > MAX_EDITOR) return { status: 413, corpo: { ok: false, error: 'maior que 1 MB' } };
|
|
149
|
-
const abs = dentro(raiz, rel);
|
|
150
|
-
if (!abs || abs === path.resolve(raiz)) return { status: 403, corpo: { ok: false, error: 'fora da pasta do projeto' } };
|
|
151
|
-
if (ignorado(relativoDe(raiz, abs))) return { status: 403, corpo: { ok: false, error: 'pasta protegida' } };
|
|
152
|
-
let st = null;
|
|
153
|
-
try { st = fs.statSync(abs); } catch { /* arquivo novo */ }
|
|
154
|
-
if (st && !st.isFile()) return { status: 400, corpo: { ok: false, error: 'não é arquivo' } };
|
|
155
|
-
if (st && dados.mtimeBase && !dados.forcar && st.mtimeMs > Number(dados.mtimeBase) + 1) {
|
|
156
|
-
return { status: 409, corpo: { ok: false, conflito: true, error: 'mudou no disco depois que você abriu', mtime: st.mtimeMs } };
|
|
157
|
-
}
|
|
158
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
159
|
-
fs.writeFileSync(abs, conteudo);
|
|
160
|
-
return { status: 200, corpo: { ok: true, caminho: relativoDe(raiz, abs), mtime: fs.statSync(abs).mtimeMs } };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
142
|
// ── prévia ───────────────────────────────────────────────────────────────
|
|
164
143
|
|
|
165
144
|
const TIPOS = {
|
|
@@ -193,7 +172,7 @@ function servirPrevia(raiz, rel, res) {
|
|
|
193
172
|
'Content-Type': TIPOS[path.extname(alvo).toLowerCase()] || 'application/octet-stream',
|
|
194
173
|
'Cache-Control': 'no-store',
|
|
195
174
|
// O mesmo sandbox do iframe, dito pelo servidor: vale até se alguém
|
|
196
|
-
// abrir o endereço da prévia fora da
|
|
175
|
+
// abrir o endereço da prévia fora da janela.
|
|
197
176
|
'Content-Security-Policy': 'sandbox allow-scripts allow-forms allow-modals allow-popups allow-pointer-lock',
|
|
198
177
|
});
|
|
199
178
|
fs.createReadStream(alvo).pipe(res);
|
|
@@ -237,7 +216,7 @@ function vigiar(raiz, aoMudar, espera = 150) {
|
|
|
237
216
|
// ── as rotas ─────────────────────────────────────────────────────────────
|
|
238
217
|
|
|
239
218
|
/**
|
|
240
|
-
* Monta
|
|
219
|
+
* Monta o painel num servidor do app.js.
|
|
241
220
|
*
|
|
242
221
|
* @param {object} op
|
|
243
222
|
* @param {()=>string} op.pasta a pasta do projeto AGORA (o /dir muda ela)
|
|
@@ -247,9 +226,6 @@ function vigiar(raiz, aoMudar, espera = 150) {
|
|
|
247
226
|
function montar(op) {
|
|
248
227
|
let pastaVigiada = null;
|
|
249
228
|
let pararVigia = () => {};
|
|
250
|
-
// O que a própria IDE acabou de salvar, para a página não pintar como
|
|
251
|
-
// "o agente mexeu" a mudança que a pessoa fez.
|
|
252
|
-
const salvosAgora = new Map();
|
|
253
229
|
|
|
254
230
|
function conferirPasta() {
|
|
255
231
|
const atual = op.pasta && op.pasta();
|
|
@@ -257,19 +233,14 @@ function montar(op) {
|
|
|
257
233
|
pararVigia();
|
|
258
234
|
pastaVigiada = atual;
|
|
259
235
|
if (!fs.existsSync(atual)) { pararVigia = () => {}; return atual; }
|
|
260
|
-
pararVigia = vigiar(atual, (mudados) => {
|
|
261
|
-
const agora = Date.now();
|
|
262
|
-
const deVoce = mudados.filter((m) => (salvosAgora.get(m) || 0) > agora - 1500);
|
|
263
|
-
op.emitir('arquivos', { pasta: atual, mudados, deVoce });
|
|
264
|
-
});
|
|
236
|
+
pararVigia = vigiar(atual, (mudados) => op.emitir('arquivos', { pasta: atual, mudados }));
|
|
265
237
|
op.emitir('arquivos', { pasta: atual, mudados: [], trocou: true });
|
|
266
238
|
return atual;
|
|
267
239
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
o fs.watch recursivo custa um aviso do sistema por pasta. */
|
|
240
|
+
/* O /dir e os projetos da barra lateral trocam a pasta sem passar por
|
|
241
|
+
aqui: sem esta ronda o painel vigiaria o projeto anterior. E só depois
|
|
242
|
+
que o painel foi aberto de verdade — no Linux o fs.watch recursivo custa
|
|
243
|
+
um aviso do sistema por pasta, e quem só conversa não precisa disso. */
|
|
273
244
|
let usada = false;
|
|
274
245
|
const ronda = setInterval(() => { if (usada) conferirPasta(); }, 2000);
|
|
275
246
|
if (ronda.unref) ronda.unref();
|
|
@@ -279,48 +250,31 @@ function montar(op) {
|
|
|
279
250
|
res.end(JSON.stringify(corpo));
|
|
280
251
|
}
|
|
281
252
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
let corpo = '';
|
|
285
|
-
let grande = false;
|
|
286
|
-
req.on('data', (c) => { corpo += c; if (corpo.length > MAX_EDITOR * 2) { grande = true; req.destroy(); } });
|
|
287
|
-
req.on('end', () => { try { ok(grande ? null : JSON.parse(corpo)); } catch { ok(null); } });
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/* Devolve true se a rota era da IDE. A chave já foi conferida pelo
|
|
292
|
-
app.js antes de chegar aqui — só as rotas com /api/ passam por ela, e
|
|
293
|
-
por isso a prévia também mora debaixo de /api/. */
|
|
253
|
+
/* Devolve true se a rota era do painel. A chave já foi conferida pelo
|
|
254
|
+
app.js antes de chegar aqui — por isso a prévia também mora em /api/. */
|
|
294
255
|
async function atender(req, res, url) {
|
|
295
256
|
const rota = url.pathname;
|
|
296
|
-
|
|
257
|
+
const ehArquivos = rota.startsWith('/api/arquivos/');
|
|
258
|
+
const m = rota.match(/^\/api\/previa\/[0-9a-f]+\/?(.*)$/);
|
|
259
|
+
if (!ehArquivos && !m) return false;
|
|
297
260
|
usada = true;
|
|
298
261
|
const raiz = conferirPasta();
|
|
299
262
|
if (!raiz || !fs.existsSync(raiz)) { json(res, 404, { ok: false, error: 'sem pasta de projeto' }); return true; }
|
|
300
263
|
|
|
301
|
-
if (rota === '/api/
|
|
264
|
+
if (rota === '/api/arquivos/arvore') {
|
|
302
265
|
const a = arvore(raiz);
|
|
303
266
|
json(res, 200, { ok: true, pasta: raiz, nome: path.basename(raiz), itens: a.itens,
|
|
304
267
|
cortada: a.cortada, entrada: entradaDaPrevia(raiz) });
|
|
305
268
|
return true;
|
|
306
269
|
}
|
|
307
|
-
if (rota === '/api/
|
|
270
|
+
if (rota === '/api/arquivos/ler') {
|
|
308
271
|
const r = ler(raiz, url.searchParams.get('p') || '');
|
|
309
272
|
json(res, r.status, r.corpo);
|
|
310
273
|
return true;
|
|
311
274
|
}
|
|
312
|
-
|
|
313
|
-
const dados = await lerCorpo(req);
|
|
314
|
-
if (!dados) { json(res, 400, { ok: false, error: 'pedido ilegível ou maior que 1 MB' }); return true; }
|
|
315
|
-
const r = salvar(raiz, dados);
|
|
316
|
-
if (r.corpo.ok) salvosAgora.set(r.corpo.caminho, Date.now());
|
|
317
|
-
json(res, r.status, r.corpo);
|
|
318
|
-
return true;
|
|
319
|
-
}
|
|
320
|
-
/* /api/ide/previa/<chave>/<caminho>: a chave vai no CAMINHO, não na
|
|
275
|
+
/* /api/previa/<chave>/<caminho>: a chave vai no CAMINHO, não na
|
|
321
276
|
query, porque a página da prévia pede style.css e script.js por
|
|
322
277
|
endereço relativo — e o relativo herda o caminho, não a query. */
|
|
323
|
-
const m = rota.match(/^\/api\/ide\/previa\/[0-9a-f]+\/?(.*)$/);
|
|
324
278
|
if (m) { servirPrevia(raiz, m[1], res); return true; }
|
|
325
279
|
|
|
326
280
|
json(res, 404, { ok: false, error: 'rota desconhecida' });
|
|
@@ -334,4 +288,4 @@ function montar(op) {
|
|
|
334
288
|
};
|
|
335
289
|
}
|
|
336
290
|
|
|
337
|
-
module.exports = { montar, dentro, arvore, ler,
|
|
291
|
+
module.exports = { montar, dentro, arvore, ler, vigiar, ehBinario, entradaDaPrevia, MAX_LEITURA };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "9.8.0-beta.
|
|
3
|
+
"version": "9.8.0-beta.9",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/app-janela.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
"app/",
|
|
31
31
|
"studio/",
|
|
32
32
|
"skills/",
|
|
33
|
-
"kits/",
|
|
34
33
|
"voz/",
|
|
35
34
|
"README.md",
|
|
36
35
|
"comandos/",
|
|
@@ -7,6 +7,8 @@ autor: adaptado de "frontend-design" (Anthropic, anthropics/skills, Apache-2.0
|
|
|
7
7
|
Trabalhe como o diretor de arte de um estúdio conhecido por dar a cada cliente uma identidade que
|
|
8
8
|
ninguém confunde. O cliente já recusou propostas com cara de modelo pronto.
|
|
9
9
|
|
|
10
|
+
**Não existe modelo.** Toda página começa do zero, e nenhuma sai com a estrutura da anterior.
|
|
11
|
+
|
|
10
12
|
**Comece pelo assunto.** O ramo, os materiais e o vocabulário do negócio são de onde vem a escolha
|
|
11
13
|
visual. Um brinquedo para meninas de 8 anos e um painel de analista financeiro não se parecem em nada.
|
|
12
14
|
Use o conteúdo real do pedido em tudo, e não texto genérico.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
nome: Motion de primeira
|
|
3
3
|
descricao: como um vídeo criado do zero deixa de parecer feito por máquina
|
|
4
|
-
quando: vídeo, video, motion,
|
|
4
|
+
quando: vídeo, video, motion graphics, vídeo animado, video animado, anúncio, anuncio, reels, teaser, vinheta, comercial, propaganda, estúdio, estudio
|
|
5
5
|
autor: PrimoCode
|
|
6
6
|
---
|
|
7
7
|
Não existe template. Cada propriedade deste vídeo é uma decisão sua, e a
|