primocode 10.2.0 → 10.3.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,152 @@
1
+ /**
2
+ * print-site.js — o print DE VERDADE de um site, para virar cena 3D.
3
+ *
4
+ * "Se eu mandar um site e pedir, dependendo do modelo, ela pode tirar print
5
+ * do site, transformar em 3D e pôr no Motion." (Joel, 09/10/2026)
6
+ *
7
+ * Até aqui, quando o vídeo era SOBRE um site, o estúdio desenhava uma janela
8
+ * de mentira (a peça `site` do kit: manchete, botão e um bloco de cor). Quem
9
+ * conhece o site olhava e não reconhecia — era um site genérico com o nome
10
+ * da marca. O navegador escondido já existia (browser.js, o revisor), mas
11
+ * só servia para conferir página; o `browser_screenshot` tirava print da
12
+ * aba visível da pessoa, do tamanho da janela dela, e só se o agente
13
+ * lembrasse de chamar.
14
+ *
15
+ * Aqui o print sai do navegador ESCONDIDO, com a tela de um computador
16
+ * (1440 de largura) ou de um celular (390, densidade 2), da página inteira
17
+ * (até um teto, para a imagem não virar 40 MB) ou só da primeira dobra. A
18
+ * imagem vai para midia/site/ do projeto e volta com a proporção medida,
19
+ * que é o que a peça `printsite` do kit precisa para rolar a página.
20
+ *
21
+ * Não decide se o vídeo usa o print. Isso é o modelo, pelo cardápio do
22
+ * estudio_midia: o pedido traz um site e o vídeo é sobre ele → vale; o vídeo
23
+ * só cita a marca → não precisa.
24
+ */
25
+
26
+ 'use strict';
27
+
28
+ const fs = require('fs');
29
+ const path = require('path');
30
+
31
+ const TELAS = {
32
+ computador: { width: 1440, height: 900, deviceScaleFactor: 1, mobile: false },
33
+ celular: { width: 390, height: 844, deviceScaleFactor: 2, mobile: true },
34
+ };
35
+ // Página mais longa que isto é cortada: um print de 1440×20000 passa de 10 MB
36
+ // e o vídeo nunca mostra mais que umas três telas rolando.
37
+ const ALTURA_MAX = 6000;
38
+
39
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
40
+
41
+ /** A URL que o navegador pode abrir — só http(s). null = não é site. */
42
+ function normalizarUrl(url) {
43
+ let u = String(url || '').trim();
44
+ if (!u) return null;
45
+ if (!/^[a-z][a-z0-9+.-]*:/i.test(u)) u = 'https://' + u;
46
+ try {
47
+ const p = new URL(u);
48
+ return /^https?:$/.test(p.protocol) && p.hostname ? p.toString() : null;
49
+ } catch { return null; }
50
+ }
51
+
52
+ /* Antes do print, a página desce até o fim e volta: foto com loading="lazy"
53
+ só carrega quando chega perto da tela, e sem isto o print saía com metade
54
+ das imagens em branco. */
55
+ const ROLAR_TUDO = `(async () => {
56
+ const passo = innerHeight * 0.8, fim = Math.min(document.documentElement.scrollHeight, ${ALTURA_MAX});
57
+ for (let y = 0; y < fim; y += passo) { scrollTo(0, y); await new Promise((r) => setTimeout(r, 120)); }
58
+ scrollTo(0, 0); await new Promise((r) => setTimeout(r, 300));
59
+ return true;
60
+ })()`;
61
+ const MEDIR = `JSON.stringify({ altura: Math.max(document.documentElement.scrollHeight, document.body ? document.body.scrollHeight : 0), titulo: document.title || '' })`;
62
+
63
+ async function valor(sessao, expression, awaitPromise = false) {
64
+ const r = await sessao.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise }, 30000).catch(() => null);
65
+ return r && r.result ? r.result.value : null;
66
+ }
67
+
68
+ /**
69
+ * Tira o print do site e grava na pasta do projeto.
70
+ *
71
+ * @param {string} url
72
+ * @param {object} op { pasta, celular, inteira (padrão true), alturaMax, navegador, assentar }
73
+ * @returns {Promise<{ok, src, url, titulo, tela, largura, altura, proporcao, cortada, elemento} | {ok:false, error}>}
74
+ */
75
+ async function capturarSite(url, op = {}) {
76
+ const alvo = normalizarUrl(url);
77
+ if (!alvo) return { ok: false, error: `"${url || ''}" não é o endereço de um site (http ou https).` };
78
+ if (!op.pasta) return { ok: false, error: 'o print vai para a pasta do projeto — diga o projeto (id).' };
79
+ const navegador = op.navegador || require('../browser.js');
80
+ const tela = op.celular ? 'celular' : 'computador';
81
+ const metrica = TELAS[tela];
82
+ // No celular a página é três vezes mais comprida: o teto é menor.
83
+ const teto = Math.max(metrica.height, Number(op.alturaMax) || (metrica.mobile ? 4000 : ALTURA_MAX));
84
+
85
+ let aba;
86
+ try {
87
+ /* A aba nasce em branco e só depois navega: a tela (largura, celular)
88
+ tem de valer ANTES do site carregar, senão o site responsivo já
89
+ escolheu o layout de computador e o "print do celular" sai largo. */
90
+ aba = await navegador.abaEscondida('about:blank', { assentar: 0 });
91
+ const s = aba.sessao;
92
+ await s.send('Emulation.setDeviceMetricsOverride', metrica);
93
+ if (metrica.mobile) await s.send('Emulation.setTouchEmulationEnabled', { enabled: true }).catch(() => {});
94
+ await s.send('Page.navigate', { url: alvo });
95
+ const prazo = Date.now() + (op.prazo || 20000);
96
+ while (Date.now() < prazo) {
97
+ if (await valor(s, 'document.readyState') === 'complete') break;
98
+ await sleep(200);
99
+ }
100
+ await sleep(op.assentar == null ? 1500 : op.assentar);
101
+ if (op.inteira !== false) await valor(s, ROLAR_TUDO, true);
102
+
103
+ let medida = {};
104
+ try { medida = JSON.parse(await valor(s, MEDIR) || '{}'); } catch { /* segue com a tela */ }
105
+ const alturaPagina = Math.max(metrica.height, Number(medida.altura) || metrica.height);
106
+ const altura = op.inteira === false ? metrica.height : Math.min(alturaPagina, teto);
107
+
108
+ const r = await s.send('Page.captureScreenshot', {
109
+ format: 'jpeg', quality: 88, captureBeyondViewport: true,
110
+ clip: { x: 0, y: 0, width: metrica.width, height: altura, scale: 1 },
111
+ }, 60000);
112
+ if (!r || !r.data) return { ok: false, error: 'o navegador devolveu um print vazio.' };
113
+
114
+ const host = new URL(alvo).hostname.replace(/^www\./, '').replace(/[^a-z0-9.-]/gi, '-');
115
+ const src = `midia/site/${host}-${tela}${op.inteira === false ? '-dobra' : ''}.jpg`;
116
+ const cheio = path.join(op.pasta, src);
117
+ fs.mkdirSync(path.dirname(cheio), { recursive: true });
118
+ fs.writeFileSync(cheio, Buffer.from(r.data, 'base64'));
119
+
120
+ const proporcao = Math.round((altura / metrica.width) * 1000) / 1000;
121
+ const feito = {
122
+ ok: true, src, url: alvo, titulo: medida.titulo || '', tela,
123
+ largura: metrica.width, altura, proporcao,
124
+ cortada: op.inteira !== false && alturaPagina > teto,
125
+ };
126
+ feito.elemento = elementoDoPrint(feito);
127
+ feito.nota = 'O elemento é uma sugestão: ajuste x, y, largura e o tempo ao seu plano. '
128
+ + 'rolar: true desce a página devagar; inclinacao muda o ângulo 3D.';
129
+ return feito;
130
+ } catch (e) {
131
+ return { ok: false, error: `não consegui tirar o print de ${alvo}: ${e.message}` };
132
+ } finally {
133
+ if (aba) { try { await aba.fechar(); } catch { /* a aba já fechou */ } }
134
+ }
135
+ }
136
+
137
+ /**
138
+ * O elemento de plano que mostra o print: a janela (ou o celular) em 3D.
139
+ * Num quadro 1920×1080 por padrão — o modelo ajusta ao formato dele.
140
+ */
141
+ function elementoDoPrint(print, { quadroW = 1920, quadroH = 1080, t = 0 } = {}) {
142
+ const celular = print.tela === 'celular';
143
+ const largura = celular ? Math.round(quadroH * 0.42) : Math.round(quadroW * 0.66);
144
+ const altura = celular ? Math.round(largura * 2.05) : Math.round(largura * 0.64);
145
+ return {
146
+ tipo: 'kit', peca: 'printsite', src: print.src, proporcao: print.proporcao,
147
+ celular, url: print.url, t, rolar: print.proporcao > (altura / largura) * 1.2,
148
+ x: Math.round((quadroW - largura) / 2), y: Math.round((quadroH - altura) / 2), largura, altura,
149
+ };
150
+ }
151
+
152
+ module.exports = { capturarSite, elementoDoPrint, normalizarUrl, TELAS, ALTURA_MAX };
@@ -66,8 +66,19 @@ function inferir(texto) {
66
66
  return { modo: 'sem', escopo: '', explicito: false };
67
67
  }
68
68
 
69
+ /* O QUE A PESSOA DISSE dos vídeos (assinatura.js): as últimas notas da
70
+ assinatura do fluido vão junto da instrução, para o agente ler ANTES de
71
+ escrever o roteiro — o feedback de um vídeo vale para o próximo. */
72
+ function notasDaAssinatura() {
73
+ try {
74
+ const notas = require('./assinatura.js').notasRecentes('fluido', 6);
75
+ return notas.length ? '\n[O QUE A PESSOA DISSE DOS ÚLTIMOS VÍDEOS — assinatura fluido] ' + notas.map((n) => `(${n.quando}) ${n.texto}`).join(' · ') : '';
76
+ } catch { return ''; }
77
+ }
78
+
69
79
  /** A instrução para o agente, conforme o modo. */
70
- function instrucao({ modo, escopo, explicito } = ATUAL) {
80
+ function instrucao(atual = ATUAL) { const i = instrucaoDoModo(atual); return i ? i + notasDaAssinatura() : ''; }
81
+ function instrucaoDoModo({ modo, escopo, explicito } = ATUAL) {
71
82
  // O vídeo sem imagem é motion: o agente escolhe o estilo de motion que serve ao roteiro, não um fixo.
72
83
  const motion = 'O vídeo é MOTION: tipografia, formas, ícones, animação e som. Escolha em roteiro.estilo o motion que serve a este '
73
84
  + 'roteiro (gerativo, fluido, impacto, vox, marca, lancamento) ou deixe vazio para o rodízio — não repita sempre o mesmo.';
@@ -103,6 +103,7 @@ import {
103
103
  } from 'remotion';
104
104
  import { morfar, ehCor, misturarCor } from './morfar';
105
105
  import { KIT, CURVAS_KIT, Filtros } from './kit';
106
+ import { COMPONENTES } from './componentes';
106
107
 
107
108
  /* ── SUAVIZAÇÃO ──────────────────────────────────────────────────────────
108
109
  * Cada curva é uma função de 0..1 em 0..1. "suave" é o padrão porque
@@ -227,7 +228,7 @@ function Elemento({ el, t, dados }) {
227
228
  /* A PEÇA DO KIT desenha nas coordenadas da cena (x, y dela são o lugar do
228
229
  assunto, não o canto de uma caixa): a caixa dela é o quadro inteiro, e o
229
230
  "anima" do elemento move o conjunto. */
230
- const base = el.tipo === 'kit'
231
+ const base = el.tipo === 'kit' || el.tipo === 'componente'
231
232
  ? { position: 'absolute', left: 0, top: 0, width: '100%', height: '100%' }
232
233
  : posicao(el);
233
234
  const move = transformDe(a, {});
@@ -383,6 +384,20 @@ function Elemento({ el, t, dados }) {
383
384
  );
384
385
  }
385
386
 
387
+ /* O COMPONENTE que a IA escreveu (ou reusou do catálogo, efeitos.js).
388
+ Antes o plano o aceitava e o Cena não sabia desenhar: o efeito sumia
389
+ do vídeo sem erro. Agora montar() copia o arquivo para src/componentes/
390
+ e ele recebe {el, t, fps}, no quadro inteiro, como a peça do kit. */
391
+ case 'componente': {
392
+ const Comp = COMPONENTES[el.src];
393
+ if (!Comp) return null;
394
+ return (
395
+ <div style={{ ...estilo, transformOrigin: el.origem || ((el.x || 0) + 'px ' + (el.y || 0) + 'px') }}>
396
+ <Comp el={el} t={tLocal} fps={dados.fps} />
397
+ </div>
398
+ );
399
+ }
400
+
386
401
  case 'grupo':
387
402
  return (
388
403
  <div style={{
@@ -611,9 +626,41 @@ import icones from './icones.json';
611
626
  /* A transição vive em quem SAI e em quem ENTRA ao mesmo tempo: as duas cenas
612
627
  * ficam no ar durante o cruzamento. Fazer só a que entra aparecer por cima dá
613
628
  * o "piscar" de fundo que denuncia corte mal feito. */
614
- function Transicao({ tipo, progresso, children }) {
629
+ /* AS TRANSIÇÕES EM 3D (09/10: "quero mais animações, mais efeitos, e efeitos
630
+ * mais 3D dependendo do vídeo"). As de antes giravam só a cena que entra, sem
631
+ * perspectiva: o "giro" era um achatamento, não um giro. Estas quatro mexem
632
+ * nas DUAS cenas (a que sai também recebe o progresso, em "saida") e ficam
633
+ * num palco com perspectiva:
634
+ * virar a cena vira como um cartão, e o verso é a próxima
635
+ * cubo as duas são faces de um cubo que gira
636
+ * profundidade a que sai passa pela câmera; a nova vem do fundo, desfocada
637
+ * inclinar a nova levanta deitada, como uma folha que se ergue */
638
+ const TRANS_3D = { virar: 1, cubo: 1, profundidade: 1, inclinar: 1 };
639
+ function estilo3d(tipo, p, saindo) {
640
+ const k = p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
641
+ if (tipo === 'virar') return { transform: 'rotateY(' + (saindo ? 180 * k : -180 + 180 * k) + 'deg)', backfaceVisibility: 'hidden' };
642
+ if (tipo === 'cubo') return saindo
643
+ ? { transformOrigin: '100% 50%', transform: 'translateX(' + -100 * k + '%) rotateY(' + -90 * k + 'deg)' }
644
+ : { transformOrigin: '0% 50%', transform: 'translateX(' + 100 * (1 - k) + '%) rotateY(' + 90 * (1 - k) + 'deg)' };
645
+ if (tipo === 'profundidade') return saindo
646
+ ? { transform: 'translateZ(' + 900 * k + 'px)', opacity: 1 - k }
647
+ : { transform: 'translateZ(' + -1400 * (1 - k) + 'px)', filter: 'blur(' + (1 - k) * 16 + 'px)', opacity: Math.min(1, k * 2.5) };
648
+ if (tipo === 'inclinar') return saindo
649
+ ? { transform: 'translateY(' + -12 * k + '%) scale(' + (1 - 0.08 * k) + ')', filter: 'brightness(' + (1 - 0.5 * k) + ')' }
650
+ : { transformOrigin: '50% 100%', transform: 'rotateX(' + 72 * (1 - k) + 'deg)', opacity: Math.min(1, k * 3) };
651
+ return {};
652
+ }
653
+
654
+ function Transicao({ tipo, progresso, saida, children }) {
615
655
  const p = progresso;
616
656
  const { width: W, height: H } = useVideoConfig();
657
+ // A cena que sai, quando a próxima entra por uma transição 3D, mexe também.
658
+ if (saida && TRANS_3D[saida.tipo] && saida.p > 0 && !(TRANS_3D[tipo] && p < 1)) {
659
+ return <AbsoluteFill style={{ perspective: 1800 }}><AbsoluteFill style={estilo3d(saida.tipo, saida.p, true)}>{children}</AbsoluteFill></AbsoluteFill>;
660
+ }
661
+ if (TRANS_3D[tipo] && p < 1) {
662
+ return <AbsoluteFill style={{ perspective: 1800 }}><AbsoluteFill style={estilo3d(tipo, p, false)}>{children}</AbsoluteFill></AbsoluteFill>;
663
+ }
617
664
  let estilo = {};
618
665
  if (tipo === 'esmaecer') estilo = { opacity: p };
619
666
  else if (tipo === 'deslizar') estilo = { transform: 'translateX(' + (1 - p) * 100 + '%)' };
@@ -683,9 +730,12 @@ export const Video = () => {
683
730
  const progresso = cruz > 0 && local < cruz && i > 0
684
731
  ? interpolate(local, [0, cruz], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' })
685
732
  : 1;
733
+ // O progresso de quem ENTRA depois desta: as transições 3D mexem nas duas.
734
+ const prox = pedacos[i + 1];
735
+ const saida = prox && prox.cruz > 0 ? { tipo: prox.tipo, p: interpolate(frame - prox.de, [0, prox.cruz], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }) } : null;
686
736
  return (
687
737
  <Sequence key={i} from={de} durationInFrames={dur} name={'cena ' + (i + 1)}>
688
- <Transicao tipo={i === 0 ? 'corte' : tipo} progresso={progresso}>
738
+ <Transicao tipo={i === 0 ? 'corte' : tipo} progresso={progresso} saida={saida}>
689
739
  <Cena cena={cena} dados={dados} />
690
740
  </Transicao>
691
741
  </Sequence>
@@ -814,6 +864,24 @@ para este projeto, não para o PrimoCode.
814
864
  `;
815
865
  }
816
866
 
867
+ /* Os componentes do plano viram import de verdade: o webpack do Remotion não
868
+ carrega arquivo por nome em tempo de render, então cada `src` é copiado
869
+ para src/componentes/ e entra no mapa COMPONENTES pelo mesmo nome. */
870
+ function escreverComponentes(pasta, src, plano) {
871
+ const srcs = new Set();
872
+ const ver = (els) => { for (const el of els || []) { if (el && el.tipo === 'componente' && el.src) srcs.add(el.src); if (el) ver(el.elementos); } };
873
+ for (const c of plano.cenas || []) ver(c.elementos);
874
+ const linhas = [], mapa = [];
875
+ [...srcs].forEach((s, i) => {
876
+ const de = path.resolve(pasta, s);
877
+ if (!de.startsWith(path.resolve(pasta) + path.sep) || !fs.existsSync(de)) return;
878
+ escrever(path.join(src, 'componentes', 'c' + i + path.extname(de)), fs.readFileSync(de, 'utf8'));
879
+ linhas.push(`import * as M${i} from './componentes/c${i}';`);
880
+ mapa.push(` ${JSON.stringify(s)}: M${i}.default || Object.values(M${i}).find((v) => typeof v === 'function'),`);
881
+ });
882
+ escrever(path.join(src, 'componentes.js'), `${linhas.join('\n')}\nexport const COMPONENTES = {\n${mapa.join('\n')}\n};\n`);
883
+ }
884
+
817
885
  /**
818
886
  * Escreve o projeto Remotion dentro da pasta do projeto do Studio.
819
887
  *
@@ -851,6 +919,7 @@ function montar({ pasta, titulo, plano, icones = {} }) {
851
919
  // O kit de motion (kit.js): as peças que o elemento {"tipo":"kit"} monta.
852
920
  for (const f of require('./kit.js').arquivos()) escrever(path.join(src, f.nome), f.conteudo);
853
921
  escrever(path.join(src, 'plano.json'), JSON.stringify(plano, null, 2));
922
+ escreverComponentes(pasta, src, plano);
854
923
  escrever(path.join(src, 'icones.json'), JSON.stringify(icones, null, 2));
855
924
  escrever(path.join(src, 'fontes.css'), fontesCSS(plano));
856
925
 
@@ -20,8 +20,16 @@ const fs = require('fs');
20
20
  const os = require('os');
21
21
  const path = require('path');
22
22
 
23
- const PASTA = path.join(os.homedir(), '.primocode', 'studio');
24
- const MEMORIA = { trilha: 16, sfx: 40, broll: 80, icones: 4, estilo: 8, '3d': 24, 'icone-nome': 30, 'paleta-fluido': 6 };
23
+ /* A pasta é lida a cada uso (não no require): o teste troca o HOME depois de
24
+ carregar o módulo e a memória de verdade fica intocada. */
25
+ const PASTA = () => path.join(os.homedir(), '.primocode', 'studio');
26
+ /* Os tipos do gerativo e do fluido (09/10): "vício em cores, músicas, fontes"
27
+ — a paleta, as fontes e o gênero saíam só da semente, sem lembrar do vídeo
28
+ anterior. Agora passam por aqui como o resto. */
29
+ const MEMORIA = { trilha: 16, sfx: 40, broll: 80, icones: 4, estilo: 8, '3d': 24, 'icone-nome': 30, 'paleta-fluido': 6,
30
+ 'fonte-fluido': 4, 'humor-gerativo': 4, 'paleta-gerativo': 10, 'fonte-gerativo': 18, 'genero-gerativo': 8,
31
+ 'arranjo-icone': 10, 'material-icone': 4, 'heroi-gerativo': 8, 'composicao-gerativo': 12,
32
+ 'moldura-gerativo': 4, 'ambiente-gerativo': 4, 'acento-gerativo': 3 };
25
33
 
26
34
  /* A DIVERSIDADE POR PONTOS (07/10): "o sistema não deve simplesmente proibir
27
35
  repetição; escolhas usadas recentemente recebem penalidade, as muito usadas
@@ -40,8 +48,8 @@ const PESO_FREQUENCIA = 0.25;
40
48
  const MEIA_VIDA_DIAS = 21;
41
49
  const EMPATE = 0.05; // dentro disso, é sorteio
42
50
 
43
- function arquivo(tipo) { return path.join(PASTA, `${tipo}-usadas.json`); }
44
- function arquivoHistorico(tipo) { return path.join(PASTA, `${tipo}-historico.json`); }
51
+ function arquivo(tipo) { return path.join(PASTA(), `${tipo}-usadas.json`); }
52
+ function arquivoHistorico(tipo) { return path.join(PASTA(), `${tipo}-historico.json`); }
45
53
 
46
54
  function usadas(tipo) {
47
55
  try { const l = JSON.parse(fs.readFileSync(arquivo(tipo), 'utf8')); return Array.isArray(l) ? l : []; } catch { return []; }
@@ -57,7 +65,7 @@ const usosHoje = (reg, agora) => (reg ? reg.n * Math.pow(0.5, Math.max(0, agora
57
65
  function marcar(tipo, id, agora = Date.now()) {
58
66
  if (!id) return;
59
67
  try {
60
- fs.mkdirSync(PASTA, { recursive: true });
68
+ fs.mkdirSync(PASTA(), { recursive: true });
61
69
  // A lista de recentes é maior que a janela de antes: a recência decai, não corta.
62
70
  const l = [id, ...usadas(tipo).filter((x) => x !== id)].slice(0, (MEMORIA[tipo] || 20) * 2);
63
71
  fs.writeFileSync(arquivo(tipo), JSON.stringify(l));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "10.2.0",
3
+ "version": "10.3.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/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/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/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.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/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.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 && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/revisor.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.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/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/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.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/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.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 && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"