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.
@@ -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, abrir: openInBrowser,
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.35.0",
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"