primocode 8.35.0 → 8.37.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/lib/act.js +68 -0
- package/lib/browser.js +292 -1
- package/lib/catalogo.js +11 -2
- package/lib/claude-engine.js +25 -2
- package/lib/codex-engine.js +9 -2
- package/lib/pipeline.js +447 -0
- package/lib/studio.js +16 -3
- package/lib/tools.js +2 -1
- package/package.json +2 -2
- package/studio/ferramentas/corte.md +173 -10
- package/studio/server.py +107 -4
- package/studio/web/js/analise.js +319 -11
- package/studio/web/js/audio.js +367 -55
- package/studio/web/js/render.js +352 -3
- package/studio/web/js/video.js +4 -0
- package/studio/web/render.css +227 -3
package/lib/pipeline.js
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pipeline.js — os quatro papéis que constroem um app.
|
|
3
|
+
*
|
|
4
|
+
* "1. Engenheiro de Prompt: recebe o prompt simples do usuário e monta um
|
|
5
|
+
* prompt completo e detalhado. 2. Construtor 1: cria um protótipo. 3.
|
|
6
|
+
* Construtor 2: transforma o protótipo em um MVP. 4. Revisador: testa o
|
|
7
|
+
* código ocultamente, identifica erros e reporta ao Construtor 2. 5. Loop
|
|
8
|
+
* de correção. 6. Entrega final: abre automaticamente no navegador."
|
|
9
|
+
*
|
|
10
|
+
* ── POR QUE UMA PEÇA E NÃO UM CONSELHO NO PROMPT ─────────────────────────
|
|
11
|
+
* Dá para escrever tudo isso na instrução do agente e torcer. Não funciona, e
|
|
12
|
+
* o motivo é medível: o modelo que acabou de escrever o código é o pior
|
|
13
|
+
* revisor dele — ele "lê" o que quis escrever, não o que escreveu, e aprova.
|
|
14
|
+
* O que separa os papéis aqui é que cada um roda numa CONVERSA PRÓPRIA, sem
|
|
15
|
+
* ver a do anterior: o revisor recebe o pedido e os arquivos, e mais nada.
|
|
16
|
+
*
|
|
17
|
+
* ── O REVISOR TEM DUAS METADES, E A PRIMEIRA NÃO É UM MODELO ─────────────
|
|
18
|
+
* Perguntar a um modelo "esse código funciona?" devolve opinião. O que decide
|
|
19
|
+
* é ABRIR a página e olhar: um Chrome invisível carrega o resultado e conta o
|
|
20
|
+
* que o navegador reclamou e o que sobrou na tela. Esse relatório é FATO, e é
|
|
21
|
+
* ele que o revisor lê antes de dizer qualquer coisa. Sem essa metade, o
|
|
22
|
+
* defeito clássico passa inteiro — a página abre, responde 200, tem título, e
|
|
23
|
+
* está em branco por causa de um TypeError na primeira linha.
|
|
24
|
+
*
|
|
25
|
+
* ── O QUE ESTA PEÇA NÃO FAZ ──────────────────────────────────────────────
|
|
26
|
+
* Ela não é o caminho normal. São de seis a oito chamadas de agente, e usá-la
|
|
27
|
+
* para "muda a cor do botão" gasta o dia da pessoa para fazer o que uma edição
|
|
28
|
+
* resolve. Quem decide chamá-la é o agente, e o catálogo diz quando.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
const fs = require('fs');
|
|
34
|
+
const os = require('os');
|
|
35
|
+
const path = require('path');
|
|
36
|
+
const http = require('http');
|
|
37
|
+
|
|
38
|
+
const browser = require('./browser.js');
|
|
39
|
+
|
|
40
|
+
const TIPOS = {
|
|
41
|
+
'.html': 'text/html; charset=utf-8',
|
|
42
|
+
'.htm': 'text/html; charset=utf-8',
|
|
43
|
+
'.css': 'text/css; charset=utf-8',
|
|
44
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
45
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
46
|
+
'.json': 'application/json; charset=utf-8',
|
|
47
|
+
'.svg': 'image/svg+xml',
|
|
48
|
+
'.png': 'image/png',
|
|
49
|
+
'.jpg': 'image/jpeg',
|
|
50
|
+
'.jpeg': 'image/jpeg',
|
|
51
|
+
'.gif': 'image/gif',
|
|
52
|
+
'.webp': 'image/webp',
|
|
53
|
+
'.ico': 'image/x-icon',
|
|
54
|
+
'.woff': 'font/woff',
|
|
55
|
+
'.woff2': 'font/woff2',
|
|
56
|
+
'.mp3': 'audio/mpeg',
|
|
57
|
+
'.mp4': 'video/mp4',
|
|
58
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Serve a pasta em 127.0.0.1, numa porta que o sistema escolhe.
|
|
63
|
+
*
|
|
64
|
+
* Por que servir em vez de abrir `file://`: em `file://` o navegador recusa
|
|
65
|
+
* `fetch`, módulos ES e a maior parte do que uma página moderna faz. O revisor
|
|
66
|
+
* encheria o relatório de erros de CORS que não existem quando a pessoa
|
|
67
|
+
* publicar — e erro inventado é pior que erro nenhum, porque manda o
|
|
68
|
+
* construtor consertar o que não está quebrado.
|
|
69
|
+
*/
|
|
70
|
+
function servirPasta(dir) {
|
|
71
|
+
const raiz = path.resolve(dir);
|
|
72
|
+
const srv = http.createServer((req, res) => {
|
|
73
|
+
let alvo;
|
|
74
|
+
try {
|
|
75
|
+
const caminho = decodeURIComponent((req.url || '/').split('?')[0]);
|
|
76
|
+
alvo = path.resolve(raiz, '.' + caminho);
|
|
77
|
+
} catch { alvo = raiz; }
|
|
78
|
+
// Ninguém sai da pasta. O conteúdo daqui foi escrito por um modelo, e
|
|
79
|
+
// um `../../.ssh/id_rsa` num src não pode virar leitura de disco.
|
|
80
|
+
if (alvo !== raiz && !alvo.startsWith(raiz + path.sep)) {
|
|
81
|
+
res.statusCode = 403; return res.end('fora da pasta');
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
if (fs.existsSync(alvo) && fs.statSync(alvo).isDirectory()) {
|
|
85
|
+
alvo = path.join(alvo, 'index.html');
|
|
86
|
+
}
|
|
87
|
+
if (!fs.existsSync(alvo)) { res.statusCode = 404; return res.end('não achei'); }
|
|
88
|
+
res.setHeader('Content-Type', TIPOS[path.extname(alvo).toLowerCase()] || 'application/octet-stream');
|
|
89
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
90
|
+
return res.end(fs.readFileSync(alvo));
|
|
91
|
+
} catch (e) {
|
|
92
|
+
res.statusCode = 500;
|
|
93
|
+
return res.end(String((e && e.message) || 'erro'));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
return new Promise((ok) => {
|
|
97
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
98
|
+
ok({
|
|
99
|
+
url: `http://127.0.0.1:${srv.address().port}/`,
|
|
100
|
+
fechar: () => { try { srv.close(); } catch { /* já fechou */ } },
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A página que se abre primeiro. `index.html` manda; senão, o único .html. */
|
|
107
|
+
function acharEntrada(dir) {
|
|
108
|
+
try {
|
|
109
|
+
if (fs.existsSync(path.join(dir, 'index.html'))) return 'index.html';
|
|
110
|
+
const htmls = fs.readdirSync(dir).filter((f) => /\.html?$/i.test(f));
|
|
111
|
+
if (htmls.length === 1) return htmls[0];
|
|
112
|
+
// Com vários .html e sem index, escolher um seria adivinhar qual é a
|
|
113
|
+
// home — e revisar a página errada aprova o que ninguém vai abrir. Sem
|
|
114
|
+
// nenhum, também não há o que abrir. Os dois casos são "não sei", e o
|
|
115
|
+
// relatório do revisor diz isso em vez de fingir que testou.
|
|
116
|
+
return null;
|
|
117
|
+
} catch { return null; }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Todos os arquivos da pasta, para o revisor saber o que existe. */
|
|
121
|
+
function listar(dir, base = dir, fundo = 0) {
|
|
122
|
+
if (fundo > 3) return [];
|
|
123
|
+
let saida = [];
|
|
124
|
+
let itens = [];
|
|
125
|
+
try { itens = fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
|
|
126
|
+
for (const it of itens) {
|
|
127
|
+
if (it.name === 'node_modules' || it.name.startsWith('.')) continue;
|
|
128
|
+
const cheio = path.join(dir, it.name);
|
|
129
|
+
if (it.isDirectory()) saida = saida.concat(listar(cheio, base, fundo + 1));
|
|
130
|
+
else saida.push(path.relative(base, cheio));
|
|
131
|
+
if (saida.length > 80) break;
|
|
132
|
+
}
|
|
133
|
+
return saida;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A metade mecânica do revisor: abre a página escondido e conta o que quebrou.
|
|
138
|
+
*
|
|
139
|
+
* Devolve SEMPRE um texto para outro modelo ler — inclusive quando não deu
|
|
140
|
+
* para abrir. "Não consegui testar" é uma informação; silêncio vira aprovação.
|
|
141
|
+
*/
|
|
142
|
+
async function medirNoNavegador(dir) {
|
|
143
|
+
const entrada = acharEntrada(dir);
|
|
144
|
+
if (!entrada) {
|
|
145
|
+
return { testou: false, limpo: false,
|
|
146
|
+
relato: 'Não há uma página de entrada clara na pasta (nenhum index.html, ou vários .html '
|
|
147
|
+
+ 'sem um index). Não deu para abrir e testar. Se este projeto é uma página, crie '
|
|
148
|
+
+ 'index.html; se não é, o teste no navegador não se aplica.' };
|
|
149
|
+
}
|
|
150
|
+
/* Daqui para baixo existe página, então o que sair do navegador é medida de
|
|
151
|
+
verdade — e é só medida de verdade que pode vetar um "APROVADO". */
|
|
152
|
+
|
|
153
|
+
let servidor = null;
|
|
154
|
+
try {
|
|
155
|
+
servidor = await servirPasta(dir);
|
|
156
|
+
const r = await browser.inspecionarEscondido(servidor.url + entrada);
|
|
157
|
+
const linhas = [];
|
|
158
|
+
linhas.push(`Abri ${entrada} num navegador invisível.`);
|
|
159
|
+
linhas.push(`Título: ${r.titulo || '(sem <title>)'}`);
|
|
160
|
+
linhas.push(`Elementos visíveis na tela: ${r.visiveis}`);
|
|
161
|
+
if (r.vazio) {
|
|
162
|
+
linhas.push('A PÁGINA ABRIU EM BRANCO. Quem abrir isso vê uma tela vazia — '
|
|
163
|
+
+ 'é o pior desfecho possível, porque não parece erro, parece produto quebrado.');
|
|
164
|
+
}
|
|
165
|
+
if (r.erros.length) {
|
|
166
|
+
linhas.push(`O navegador reclamou de ${r.erros.length} coisa(s):`);
|
|
167
|
+
r.erros.forEach((e) => linhas.push(` · [${e.tipo}] ${e.texto}`));
|
|
168
|
+
} else {
|
|
169
|
+
linhas.push('Nenhum erro de JavaScript, de console ou de recurso.');
|
|
170
|
+
}
|
|
171
|
+
if (r.texto) linhas.push(`Texto que apareceu: ${JSON.stringify(r.texto.slice(0, 300))}`);
|
|
172
|
+
return { testou: true, limpo: !r.vazio && r.erros.length === 0, relato: linhas.join('\n'), medida: r };
|
|
173
|
+
} catch (e) {
|
|
174
|
+
return { testou: false, limpo: false,
|
|
175
|
+
relato: `Não consegui abrir a página para testar: ${(e && e.message) || e}. `
|
|
176
|
+
+ 'Siga sem o teste automático, mas NÃO diga que está testado.' };
|
|
177
|
+
} finally {
|
|
178
|
+
if (servidor) servidor.fechar();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── os papéis ────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
const ENGENHEIRO = (pedido) => `Você é o ENGENHEIRO DE PROMPT.
|
|
185
|
+
|
|
186
|
+
O usuário pediu isto, com as palavras dele:
|
|
187
|
+
|
|
188
|
+
"""
|
|
189
|
+
${pedido}
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
Sua tarefa é transformar esse pedido numa ESPECIFICAÇÃO que outro programador
|
|
193
|
+
consiga seguir sem perguntar nada. Você NÃO escreve código e NÃO cria arquivo
|
|
194
|
+
nenhum — se você criar arquivo, atrapalha quem vem depois.
|
|
195
|
+
|
|
196
|
+
Escreva, em português, e nesta ordem:
|
|
197
|
+
|
|
198
|
+
1. O QUE É, em uma frase.
|
|
199
|
+
2. QUEM USA e o que essa pessoa quer fazer na tela.
|
|
200
|
+
3. AS TELAS ou seções, uma a uma, com o que tem em cada uma.
|
|
201
|
+
4. O QUE PRECISA FUNCIONAR — as ações de verdade, com o que acontece em cada
|
|
202
|
+
uma, inclusive quando dá errado (campo vazio, sem dados, sem conexão).
|
|
203
|
+
5. OS DADOS: o que se guarda, onde (localStorage, memória, arquivo) e o
|
|
204
|
+
formato.
|
|
205
|
+
6. O VISUAL: paleta com valores, tipografia, densidade. Escolha de verdade —
|
|
206
|
+
"moderno e bonito" não é instrução.
|
|
207
|
+
7. O QUE FICA DE FORA desta versão.
|
|
208
|
+
|
|
209
|
+
Duas regras que valem mais que o resto:
|
|
210
|
+
|
|
211
|
+
· NÃO INVENTE ESCOPO. Se pediram uma calculadora, é uma calculadora — não uma
|
|
212
|
+
calculadora com contas de usuário e histórico na nuvem. Acrescente só o que
|
|
213
|
+
torna o que foi pedido utilizável de verdade.
|
|
214
|
+
· SEJA CONCRETO. Números, textos dos botões, nomes dos campos, mensagens de
|
|
215
|
+
erro escritas. Quem lê a sua especificação não vai perguntar o que você quis
|
|
216
|
+
dizer: vai chutar.
|
|
217
|
+
|
|
218
|
+
Responda com a especificação e mais nada.`;
|
|
219
|
+
|
|
220
|
+
const CONSTRUTOR_1 = (spec, pasta) => `Você é o CONSTRUTOR 1, e o seu trabalho é
|
|
221
|
+
o PROTÓTIPO que FUNCIONA.
|
|
222
|
+
|
|
223
|
+
Construa na pasta: ${pasta}
|
|
224
|
+
|
|
225
|
+
Especificação:
|
|
226
|
+
|
|
227
|
+
"""
|
|
228
|
+
${spec}
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
Regras:
|
|
232
|
+
|
|
233
|
+
· SE o que foi pedido se abre no navegador (site, página, app, jogo,
|
|
234
|
+
ferramenta com tela), a entrada é ${'`index.html`'} na raiz dessa pasta, e sem ela
|
|
235
|
+
ninguém abre o que você fez. Se o que foi pedido roda no terminal, escreva o
|
|
236
|
+
programa e um ${'`LEIA.md`'} de cinco linhas dizendo como rodar — e não invente uma
|
|
237
|
+
página para ele.
|
|
238
|
+
· Sem instalar nada. Sem npm, sem build, sem framework baixado — o que roda
|
|
239
|
+
abrindo o arquivo (ou com um ${'`node arquivo.js`'}). Se precisar de biblioteca,
|
|
240
|
+
escreva a parte que você usa.
|
|
241
|
+
· Faça FUNCIONAR antes de ficar bonito: o caminho principal inteiro, de ponta a
|
|
242
|
+
ponta, com dados de exemplo já na tela. Uma tela vazia esperando alguém
|
|
243
|
+
digitar não mostra nada.
|
|
244
|
+
· Não deixe função pela metade nem "TODO". O que não der para fazer, não
|
|
245
|
+
prometa na tela.
|
|
246
|
+
|
|
247
|
+
Escreva os arquivos e diga em uma linha o que ficou pronto.`;
|
|
248
|
+
|
|
249
|
+
const CONSTRUTOR_2 = (spec, pasta, relato) => `Você é o CONSTRUTOR 2, e o seu
|
|
250
|
+
trabalho é virar o protótipo em PRODUTO.
|
|
251
|
+
|
|
252
|
+
A pasta é: ${pasta}
|
|
253
|
+
LEIA os arquivos que já estão lá antes de mexer. Não recomece do zero: o que
|
|
254
|
+
está funcionando fica.
|
|
255
|
+
|
|
256
|
+
Especificação:
|
|
257
|
+
|
|
258
|
+
"""
|
|
259
|
+
${spec}
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
${relato ? `O revisor testou o que existe e achou isto:\n\n"""\n${relato}\n"""\n\n` : ''}\
|
|
263
|
+
O que você faz:
|
|
264
|
+
|
|
265
|
+
· CONSERTA o que o revisor apontou. Isso vem primeiro; nada de recurso novo com
|
|
266
|
+
erro aberto.
|
|
267
|
+
· COMPLETA o que a especificação pede e o protótipo não faz.
|
|
268
|
+
· REDESENHA: hierarquia de tamanhos, espaço para respirar, estados de foco e de
|
|
269
|
+
toque, e o que aparece quando não há dados. Escolha uma paleta e mantenha.
|
|
270
|
+
· CUIDA DO ERRO: campo vazio, valor impossível, lista vazia. Cada um com uma
|
|
271
|
+
mensagem que diz o que fazer, não "erro".
|
|
272
|
+
· RESPONSIVO de verdade: nada de rolagem horizontal em 390px de largura.
|
|
273
|
+
|
|
274
|
+
As três últimas valem para o que TEM TELA. Se isto roda no terminal, o
|
|
275
|
+
equivalente é: mensagem de uso quando faltar argumento, saída legível, e código
|
|
276
|
+
de saída diferente de zero quando der errado.
|
|
277
|
+
|
|
278
|
+
Não escreva "pronto" sem ter aberto os arquivos e mudado o que precisa.
|
|
279
|
+
Diga em uma linha o que mudou.`;
|
|
280
|
+
|
|
281
|
+
const REVISOR = (spec, pasta, arquivos, relato) => `Você é o REVISADOR, e você
|
|
282
|
+
NÃO escreveu esse código. Não conserte nada — o seu trabalho é dizer o que está
|
|
283
|
+
errado, para quem escreveu consertar.
|
|
284
|
+
|
|
285
|
+
A pasta é: ${pasta}
|
|
286
|
+
Arquivos: ${arquivos.join(', ') || '(nenhum)'}
|
|
287
|
+
|
|
288
|
+
O que foi pedido:
|
|
289
|
+
|
|
290
|
+
"""
|
|
291
|
+
${spec}
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
O QUE A MÁQUINA MEDIU ABRINDO A PÁGINA (isto é fato, não opinião):
|
|
295
|
+
|
|
296
|
+
"""
|
|
297
|
+
${relato}
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
LEIA os arquivos antes de julgar. Depois responda em UMA das duas formas:
|
|
301
|
+
|
|
302
|
+
APROVADO
|
|
303
|
+
seguido de uma linha dizendo o que a pessoa vai ver ao abrir.
|
|
304
|
+
|
|
305
|
+
ou
|
|
306
|
+
|
|
307
|
+
REPROVADO
|
|
308
|
+
seguido de uma lista, um item por linha, cada um com ARQUIVO, o que está errado
|
|
309
|
+
e o que deveria acontecer. Máximo de seis itens, os mais graves primeiro.
|
|
310
|
+
|
|
311
|
+
Reprove sem pensar duas vezes se: a página abriu em branco, há erro de
|
|
312
|
+
JavaScript, um botão da especificação não existe ou não faz nada, ou a tela
|
|
313
|
+
mostra texto de exemplo no lugar de conteúdo de verdade.
|
|
314
|
+
|
|
315
|
+
Não reprove por gosto pessoal de cor, por falta de recurso que a especificação
|
|
316
|
+
não pediu, nem por código que você escreveria diferente. Reprovar o que está
|
|
317
|
+
certo custa uma rodada inteira e não muda nada.`;
|
|
318
|
+
|
|
319
|
+
const APROVOU = /^\s*APROVAD[OA]\b/im;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Roda o pipeline inteiro.
|
|
323
|
+
*
|
|
324
|
+
* @param {object} op
|
|
325
|
+
* @param {string} op.pedido o que o usuário pediu, nas palavras dele
|
|
326
|
+
* @param {string} op.pasta onde construir
|
|
327
|
+
* @param {(prompt:string,rotulo:string)=>Promise<string>} op.correr roda um agente
|
|
328
|
+
* @param {(linha:string)=>void} op.mostrar escreve na tela do usuário
|
|
329
|
+
* @param {(alvo:string)=>any} [op.abrir] abre no navegador da pessoa
|
|
330
|
+
* @param {number} [op.rodadas] voltas de correção (padrão 2)
|
|
331
|
+
*/
|
|
332
|
+
async function construir(op) {
|
|
333
|
+
const { pedido, pasta, correr, mostrar = () => {}, abrir } = op;
|
|
334
|
+
const rodadasMax = Math.max(1, Math.min(4, op.rodadas == null ? 2 : op.rodadas));
|
|
335
|
+
if (!pedido) return { ok: false, error: 'Diga o que construir.' };
|
|
336
|
+
if (!pasta) return { ok: false, error: 'Diga em que pasta construir.' };
|
|
337
|
+
fs.mkdirSync(pasta, { recursive: true });
|
|
338
|
+
|
|
339
|
+
const etapas = [];
|
|
340
|
+
try {
|
|
341
|
+
mostrar('engenheiro de prompt');
|
|
342
|
+
const spec = await correr(ENGENHEIRO(pedido), 'engenheiro de prompt');
|
|
343
|
+
etapas.push({ papel: 'engenheiro', texto: (spec || '').slice(0, 400) });
|
|
344
|
+
if (!spec || spec.length < 80) {
|
|
345
|
+
return { ok: false, error: 'O engenheiro de prompt não devolveu especificação. Faça você mesmo.' };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
mostrar('construtor 1 — protótipo');
|
|
349
|
+
await correr(CONSTRUTOR_1(spec, pasta), 'construtor 1');
|
|
350
|
+
etapas.push({ papel: 'construtor 1', arquivos: listar(pasta).length });
|
|
351
|
+
|
|
352
|
+
let relato = null;
|
|
353
|
+
let aprovado = false;
|
|
354
|
+
let ultimoRevisor = '';
|
|
355
|
+
|
|
356
|
+
for (let volta = 1; volta <= rodadasMax; volta++) {
|
|
357
|
+
mostrar(`construtor 2 — ${volta === 1 ? 'MVP' : `correção ${volta - 1}`}`);
|
|
358
|
+
await correr(CONSTRUTOR_2(spec, pasta, relato), `construtor 2 (${volta})`);
|
|
359
|
+
|
|
360
|
+
mostrar('revisador — testando escondido');
|
|
361
|
+
const medida = await medirNoNavegador(pasta);
|
|
362
|
+
const arquivos = listar(pasta);
|
|
363
|
+
ultimoRevisor = await correr(
|
|
364
|
+
REVISOR(spec, pasta, arquivos, medida.relato), `revisador (${volta})`);
|
|
365
|
+
|
|
366
|
+
etapas.push({ papel: `revisador ${volta}`, testou: medida.testou,
|
|
367
|
+
limpo: medida.limpo, veredito: (ultimoRevisor || '').split('\n')[0] });
|
|
368
|
+
|
|
369
|
+
/* O veredito do modelo NÃO passa por cima do que a máquina mediu.
|
|
370
|
+
Página em branco ou erro de JavaScript é reprovação, e um
|
|
371
|
+
"APROVADO" escrito em cima disso é o revisor lendo o que ele
|
|
372
|
+
queria que estivesse lá — que é exatamente o defeito que separar
|
|
373
|
+
os papéis existe para evitar.
|
|
374
|
+
|
|
375
|
+
Mas o VETO precisa de MEDIDA: quando não deu para abrir nada —
|
|
376
|
+
porque o projeto não é uma página, ou porque não há navegador
|
|
377
|
+
nesta máquina — o que a máquina tem a dizer é "não sei", e "não
|
|
378
|
+
sei" não reprova. Sem esta distinção, um app de linha de comando
|
|
379
|
+
nunca seria aprovado: gastaria as voltas todas e terminaria
|
|
380
|
+
dizendo que não está pronto, sobre um trabalho que está. */
|
|
381
|
+
aprovado = APROVOU.test(ultimoRevisor || '') && !(medida.testou && !medida.limpo);
|
|
382
|
+
if (aprovado) { mostrar('aprovado'); break; }
|
|
383
|
+
|
|
384
|
+
relato = medida.limpo ? ultimoRevisor : `${medida.relato}\n\n${ultimoRevisor || ''}`;
|
|
385
|
+
if (volta < rodadasMax) mostrar('reprovado — voltando para o construtor');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const entrada = acharEntrada(pasta);
|
|
389
|
+
let abriu = false;
|
|
390
|
+
/* "Se o código gerado for HTML, abre automaticamente no navegador."
|
|
391
|
+
Só quando passou: abrir sozinho uma página quebrada entrega o defeito
|
|
392
|
+
em tela cheia e sem aviso. Reprovado a pessoa recebe o relatório, e
|
|
393
|
+
decide se quer olhar. */
|
|
394
|
+
if (aprovado && entrada && abrir) {
|
|
395
|
+
try { abrir(path.join(pasta, entrada)); abriu = true; } catch { /* segue sem abrir */ }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
ok: true,
|
|
400
|
+
aprovado,
|
|
401
|
+
pasta,
|
|
402
|
+
entrada,
|
|
403
|
+
abriu,
|
|
404
|
+
arquivos: listar(pasta),
|
|
405
|
+
/* CORTADOS de propósito: isto volta INTEIRO para dentro do contexto
|
|
406
|
+
do agente principal, e a especificação sozinha passa de mil
|
|
407
|
+
tokens. O que ele precisa é saber que houve especificação e o que
|
|
408
|
+
o revisor achou — o texto completo está nos arquivos e no que ele
|
|
409
|
+
vai contar ao usuário, não aqui. */
|
|
410
|
+
especificacao: spec.slice(0, 600),
|
|
411
|
+
revisao: (ultimoRevisor || '').slice(0, 1200),
|
|
412
|
+
etapas,
|
|
413
|
+
comoUsar: aprovado
|
|
414
|
+
? `Pronto e revisado em ${pasta}.${abriu ? ' Já abri no navegador.' : ''} `
|
|
415
|
+
+ 'Conte ao usuário o que foi construído e o que dá para fazer na tela.'
|
|
416
|
+
: `Ficou de pé em ${pasta}, mas o revisor NÃO aprovou depois de ${rodadasMax} `
|
|
417
|
+
+ 'volta(s). NÃO diga que está pronto: conte ao usuário o que ainda está '
|
|
418
|
+
+ 'errado (está em "revisao") e pergunte se ele quer que você conserte.',
|
|
419
|
+
};
|
|
420
|
+
} finally {
|
|
421
|
+
// O navegador do revisor não é o da pessoa: ninguém sentiria falta dele
|
|
422
|
+
// nem saberia que existe para fechar.
|
|
423
|
+
try { browser.fecharRevisor(); } catch { /* já morreu */ }
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/* As palavras com que se PEDE, e que não dizem nada sobre o que é. Sem tirá-las
|
|
428
|
+
a pasta se chama "faca-uma-calculadora-de", que é o pedido e não o produto. */
|
|
429
|
+
const SO_PEDIDO = new Set(['faca', 'faz', 'faça', 'cria', 'crie', 'criar', 'monta',
|
|
430
|
+
'monte', 'montar', 'constroi', 'construa', 'construir', 'quero', 'preciso',
|
|
431
|
+
'me', 'um', 'uma', 'uns', 'umas', 'o', 'a', 'os', 'as', 'de', 'do', 'da',
|
|
432
|
+
'para', 'pra', 'com', 'que', 'app', 'aplicativo', 'site', 'pagina', 'por',
|
|
433
|
+
'favor', 'e', 'em', 'no', 'na']);
|
|
434
|
+
|
|
435
|
+
/** Uma pasta nova dentro da do projeto, com nome derivado do pedido. */
|
|
436
|
+
function pastaPara(pedido, base) {
|
|
437
|
+
const limpo = String(pedido || 'app').toLowerCase()
|
|
438
|
+
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
439
|
+
.replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/);
|
|
440
|
+
const uteis = limpo.filter((p) => p && !SO_PEDIDO.has(p));
|
|
441
|
+
// Sem nada de útil sobrando (um pedido feito só de verbos), o pedido cru
|
|
442
|
+
// ainda é melhor nome que "app": pelo menos diz de qual conversa veio.
|
|
443
|
+
const nome = (uteis.length ? uteis : limpo).slice(0, 4).join('-') || 'app';
|
|
444
|
+
return path.join(base || os.homedir(), nome);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
module.exports = { construir, medirNoNavegador, servirPasta, acharEntrada, pastaPara, listar };
|
package/lib/studio.js
CHANGED
|
@@ -519,9 +519,9 @@ function enviarArquivo(caminho, nome) {
|
|
|
519
519
|
* `abrir` é injetado (é a função de abrir navegador do tools.js) para esta
|
|
520
520
|
* camada não passar a conhecer o sistema operacional.
|
|
521
521
|
*/
|
|
522
|
-
const analisarAudio = ({ arquivo, pausa, abrir }) => comStudio(async () => {
|
|
522
|
+
const analisarAudio = ({ arquivo, pausa, manter, repetida, abrir }) => comStudio(async () => {
|
|
523
523
|
if (!arquivo) return { ok: false, error: 'Diga qual mídia analisar (arquivo: "/midia/x.mp4").' };
|
|
524
|
-
const tarefa = await pedir('POST', '/api/analise', { arquivo, pausa });
|
|
524
|
+
const tarefa = await pedir('POST', '/api/analise', { arquivo, pausa, manter, repetida });
|
|
525
525
|
if (abrir) { try { abrir(`${BASE}${tarefa.pagina}`); } catch { /* segue e expira */ } }
|
|
526
526
|
|
|
527
527
|
// Um vídeo de meia hora leva alguns segundos para decodificar; três
|
|
@@ -537,15 +537,28 @@ const analisarAudio = ({ arquivo, pausa, abrir }) => comStudio(async () => {
|
|
|
537
537
|
}
|
|
538
538
|
if (estado.estado === 'pronto') {
|
|
539
539
|
const r = estado.resultado || {};
|
|
540
|
+
const repetidos = r.repetidos || [];
|
|
541
|
+
/* O QUE FOI TIRADO É DITO, e não só cortado. O vídeo sai menor e a
|
|
542
|
+
diferença é fala da pessoa: um resultado que só entrega os
|
|
543
|
+
trechos deixa o agente montar o vídeo sem ter como contar que
|
|
544
|
+
dois segundos dela sumiram — e vídeo que encurta sem explicação é
|
|
545
|
+
indistinguível de vídeo quebrado. */
|
|
546
|
+
const aviso = repetidos.length
|
|
547
|
+
? ` ${repetidos.length} trecho(s) saíram por terem sido ditos duas vezes `
|
|
548
|
+
+ `(${repetidos.map((x) => `${x.de}s–${x.ate}s, ${x.motivo}`).join('; ')}). `
|
|
549
|
+
+ 'DIGA isso ao usuário na sua resposta, com os tempos. Se ele quiser tudo de volta, '
|
|
550
|
+
+ 'chame de novo com repetida:false.'
|
|
551
|
+
: '';
|
|
540
552
|
return {
|
|
541
553
|
ok: true,
|
|
542
554
|
duracao: r.duracao,
|
|
543
555
|
cortado: r.cortado,
|
|
544
556
|
pausas: r.pausas,
|
|
545
557
|
trechos: r.trechos,
|
|
558
|
+
repetidos,
|
|
546
559
|
comoUsar: `${(r.trechos || []).length} trecho(s) sem pausa. Escreva UMA CENA por trecho, `
|
|
547
560
|
+ `cada uma com {"tipo":"clipe","src":"${arquivo}","de":<início>,"ate":<fim>}. `
|
|
548
|
-
+ 'NÃO escreva "duracao" nessas cenas: a cena já dura o trecho.',
|
|
561
|
+
+ 'NÃO escreva "duracao" nessas cenas: a cena já dura o trecho.' + aviso,
|
|
549
562
|
};
|
|
550
563
|
}
|
|
551
564
|
}
|
package/lib/tools.js
CHANGED
|
@@ -576,7 +576,8 @@ async function despachar(name, args, ctx) {
|
|
|
576
576
|
// por isso passa a função de abrir, e por isso demora alguns
|
|
577
577
|
// segundos em vez de responder na hora.
|
|
578
578
|
return studio.analisarAudio({
|
|
579
|
-
arquivo: args.arquivo, pausa: args.pausa,
|
|
579
|
+
arquivo: args.arquivo, pausa: args.pausa, manter: args.manter,
|
|
580
|
+
repetida: args.repetida, abrir: openInBrowser,
|
|
580
581
|
});
|
|
581
582
|
|
|
582
583
|
/* A MESMA peça, virando projeto Remotion.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.37.0",
|
|
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/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js && node test/vocabulario.test.js && node test/video-export.test.js && node test/monotonia.test.js && node test/clipe.test.js && node test/midia-envio.test.js && node test/pausas.test.js && node test/app.test.js && node test/elementos.test.js && node test/remotion.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/codex.test.js && node test/chaves.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js && node test/vocabulario.test.js && node test/video-export.test.js && node test/monotonia.test.js && node test/clipe.test.js && node test/midia-envio.test.js && node test/pausas.test.js && node test/fala-repetida.test.js && node test/trilha.test.js && node test/anuncio.test.js && node test/pipeline.test.js && node test/app.test.js && node test/elementos.test.js && node test/remotion.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/codex.test.js && node test/chaves.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|