primocode 10.6.0 → 10.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v10.6.0
1
+ # PrimoCode v10.7.0
2
2
 
3
3
  Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
4
4
  Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
9
9
  está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
10
10
 
11
11
  ```
12
- ▐▛███▜▌ Primo Code v10.6.0
12
+ ▐▛███▜▌ Primo Code v10.7.0
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
package/lib/catalogo.js CHANGED
@@ -109,7 +109,10 @@ const GRUPOS = [
109
109
  + 'o render empurra para dentro, encolhe o texto largo e afasta o colado; enfeite de fundo marque decorativo: true.' },
110
110
 
111
111
  { tool: 'estudio_midia', escreve: true, faz: 'procura e baixa ícone, imagem, música e efeito sonoro da internet, e lista as fontes',
112
- args: 'acao: biblioteca | imagem | icones | icones3d | animacoes | som | trilha | efeito | fontes | print | elementos | elemento | recortar. termo. id (para baixar). '
112
+ args: 'acao: biblioteca | movimentos | cenarios | salvar_cenario | imagem | icones | icones3d | animacoes | som | trilha | efeito | fontes | print | elementos | elemento | recortar. termo. id (para baixar). '
113
+ + 'movimentos termo:"quique" categoria:entrada|saida|enfase|loop: os 100+ movimentos prontos com nome; no elemento, movimento:"<nome>" (ou {nome, em, duracao}, ou uma lista) vira quadros-chave sozinho. Varie: não use o mesmo em todo elemento. Pode inventar o seu em anima. '
114
+ + 'cenarios: os lugares prontos do vídeo 3D e os que a pessoa já salvou. Pode CRIAR um cenário novo na cena (lugar base + ceu, chao, parede, luz, acento, hora, elementos); '
115
+ + 'no fim do vídeo pergunte "posso salvar este cenário pra usar na próxima vez?" e, SÓ com o sim, salvar_cenario com nome, cenario {…} e autorizado:true. Depois, lugar:"<nome>" traz ele de volta. '
113
116
  + 'biblioteca: o cardápio INTEIRO do que existe (ícone 2D, ícone 3D, animação pronta, efeito sonoro, música, fonte, personagem 3D) — olhe antes de escolher. '
114
117
  + 'icones3d termo:"coffee": os 1.500+ ícones 3D renderizados do Fluent Emoji (MIT); com item:"fluent3d:<nome>" e o id do projeto, baixa e devolve o elemento {tipo:"imagem"} pronto. '
115
118
  + 'animacoes termo:"fire": as 881 animações prontas do Noto (Lottie, vetor; CC BY 4.0, ponha o crédito); com item:"noto:<código>", baixa e devolve o elemento {tipo:"kit", peca:"animacao", src, loop} pronto. '
@@ -119,7 +122,7 @@ const GRUPOS = [
119
122
  + 'Use SE o pedido trouxer um site e o vídeo for SOBRE ele (lançamento, tour, case); vídeo que só cita a marca não precisa. '
120
123
  + 'elementos: url — lê o CÓDIGO do site e lista as peças que dá para tirar (logo, menu, titulo, hero, botao, cartao, imagem, formulario, preco, rodape), cada uma com o seletor. '
121
124
  + 'elemento: url + seletor (ou texto: o que está escrito nele) — tira SÓ aquela peça, em PNG com fundo transparente e no dobro da resolução, e devolve o código dela (html e estilo: fonte, cor, raio, sombra) e um elemento {tipo:"imagem"} pronto; '
122
- + 'use para pôr o botão, o cartão de preço ou a logo do site animando no vídeo (ou redesenhe a peça como componente a partir do código). '
125
+ + 'use para pôr o botão, o cartão de preço ou a logo do site animando no vídeo (ou redesenhe a peça como componente a partir do código). No vídeo 3D (estilo personagem), a peça ENTRA NA CENA em blocos: passe roteiro.objetos:[{nome:"logo", src:<o src devolvido>}] (até 3). '
123
126
  + 'recortar: arquivo — tira o fundo de qualquer foto com IA (rembg), PNG transparente. '
124
127
  + 'efeitos: lista os componentes guardados (os menos usados primeiro). usar-efeito {nome}: copia para o projeto e devolve o elemento — use se um deles servir a ESTE roteiro. '
125
128
  + 'guardar-efeito {arquivo, nome, descricao}: guarda um componente que você escreveu e ficou bom, para os próximos vídeos '
@@ -0,0 +1,91 @@
1
+ /**
2
+ * cenarios.js — os cenários que a IA criou e a pessoa deixou guardar.
3
+ *
4
+ * "a IA vai criar os próprios cenários, além de ter os próprios cenários
5
+ * prontos, e pede pro usuário se pode salvar pra usar na próxima vez."
6
+ * (Joel, 12/10)
7
+ *
8
+ * Os lugares prontos moram no mundo.js (praia, cidade, cobertura…). A IA já
9
+ * podia mudar o céu, o chão, a luz e os elementos de UMA cena; o que se perdia
10
+ * era o resultado: o próximo vídeo começava do zero. Aqui o cenário inventado
11
+ * ganha nome e fica em ~/.primocode/cenarios.json — só com o "sim" da pessoa
12
+ * (a ferramenta recusa sem `autorizado: true`) — e vira um lugar como os
13
+ * outros: `lugar: "<nome>"` na cena o traz de volta.
14
+ *
15
+ * Só se guarda o que o mundo.js sabe desenhar: o lugar base, cores #rrggbb,
16
+ * a hora e até 7 elementos. Nada de caminho de arquivo nem código.
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const os = require('os');
23
+ const path = require('path');
24
+
25
+ const arquivo = () => path.join(os.homedir(), '.primocode', 'cenarios.json');
26
+ const ehCor = (c) => typeof c === 'string' && /^#[0-9a-f]{6}$/i.test(c);
27
+ const TIPOS_DE_CHAO = ['areia', 'asfalto', 'grama', 'neve', 'lua', 'madeira', 'ladrilho'];
28
+ const HORAS = ['noite', 'por_do_sol', 'amanhecer'];
29
+
30
+ /** "Loja da Ana à noite" → "loja_da_ana_a_noite" (o mesmo jeito dos lugares do mundo.js). */
31
+ function chave(nome) {
32
+ return String(nome || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()
33
+ .replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '').slice(0, 40);
34
+ }
35
+
36
+ function lerTudo() {
37
+ try { const j = JSON.parse(fs.readFileSync(arquivo(), 'utf8')); return j && typeof j === 'object' ? j : {}; } catch { return {}; }
38
+ }
39
+
40
+ /** Só o que o mundo sabe desenhar; o resto cai fora em silêncio. */
41
+ function limpar(c, bases) {
42
+ const o = c && typeof c === 'object' ? c : {};
43
+ const base = chave(o.base || o.lugar);
44
+ const out = { base: bases.includes(base) ? base : 'cobertura' };
45
+ if (typeof o.descricao === 'string') out.descricao = o.descricao.slice(0, 200);
46
+ if (Array.isArray(o.ceu) && o.ceu.length === 2 && o.ceu.every(ehCor)) out.ceu = o.ceu;
47
+ for (const k of ['parede', 'luz', 'acento']) if (ehCor(o[k])) out[k] = o[k];
48
+ if (o.chao && typeof o.chao === 'object') {
49
+ const ch = {};
50
+ if (TIPOS_DE_CHAO.includes(o.chao.tipo)) ch.tipo = o.chao.tipo;
51
+ if (ehCor(o.chao.cor)) ch.cor = o.chao.cor;
52
+ if (ehCor(o.chao.cor2)) ch.cor2 = o.chao.cor2;
53
+ if (Object.keys(ch).length) out.chao = ch;
54
+ } else if (ehCor(o.chao)) out.chao = { cor: o.chao, cor2: o.chao };
55
+ if (HORAS.includes(o.hora)) out.hora = o.hora.replace(/_/g, ' ');
56
+ if (Array.isArray(o.elementos)) {
57
+ const els = o.elementos.map((e) => (typeof e === 'string' ? e : e && e.nome) || '').map((s) => String(s).slice(0, 40)).filter(Boolean).slice(0, 7);
58
+ if (els.length) out.elementos = els;
59
+ }
60
+ return out;
61
+ }
62
+
63
+ /**
64
+ * Guarda o cenário. `autorizado` é a resposta da pessoa: sem ela, nada é escrito.
65
+ * @param {{nome:string, cenario:object, autorizado:boolean, bases:string[]}} op
66
+ */
67
+ function salvar({ nome, cenario, autorizado, bases = [] } = {}) {
68
+ if (autorizado !== true) {
69
+ return { ok: false, error: 'falta o sim da pessoa.', comoResolver: 'pergunte "posso salvar este cenário pra usar na próxima vez?" e só chame de novo com autorizado:true se ela disser que sim.' };
70
+ }
71
+ const k = chave(nome);
72
+ if (!k) return { ok: false, error: 'dê um nome ao cenário (ex: "loja da Ana à noite").' };
73
+ if (bases.includes(k)) return { ok: false, error: `"${k}" já é um lugar pronto; escolha outro nome.` };
74
+ const tudo = lerTudo();
75
+ const novo = { ...limpar(cenario, bases), nome: String(nome).slice(0, 60), salvo: new Date().toISOString() };
76
+ tudo[k] = novo;
77
+ fs.mkdirSync(path.dirname(arquivo()), { recursive: true });
78
+ fs.writeFileSync(arquivo(), JSON.stringify(tudo, null, 2));
79
+ return { ok: true, chave: k, cenario: novo, comoUsar: `na próxima cena, lugar:"${k}" traz este cenário de volta.` };
80
+ }
81
+
82
+ function ler(nome) {
83
+ const k = chave(nome);
84
+ return k ? lerTudo()[k] || null : null;
85
+ }
86
+
87
+ function listar() {
88
+ return Object.entries(lerTudo()).map(([k, c]) => ({ chave: k, nome: c.nome, base: c.base, descricao: c.descricao || '' }));
89
+ }
90
+
91
+ module.exports = { salvar, ler, listar, limpar, chave, arquivo };
@@ -198,6 +198,7 @@ function cardapio() {
198
198
  personagens3d: 'o estilo personagem (voxel): bonecos de blocos e o mascote, com a câmera de cinema — estudio_roteiro estilo:"personagem"',
199
199
  efeitosSonoros: { sintetizados: sons, internet: 'acao:efeito termo:"whoosh" (Openverse, licença aberta, até 6 s)' },
200
200
  musicas: { internet: 'acao:trilha termo:"lofi" (Openverse, 30 s ou mais)', automatica: 'sem trilha pedida, o PrimoCode escolhe uma sem repetir as últimas' },
201
+ movimentos: { quantos: Object.keys(require('./movimentos.js').CATALOGO).length, acao: 'movimentos', exemplo: 'termo:"quique" → movimento:"quicar-de-baixo"' },
201
202
  fontes: { quantas: fontes || 128, acao: 'fontes', nota: 'Google Fonts, por tipo (condensada, display, serifada, manuscrita…)' },
202
203
  imagens: 'acao:imagem termo:"…" (Openverse e Wikimedia; Google com chave) — só quando a pessoa pediu imagem',
203
204
  };
@@ -67,6 +67,8 @@ const lancamento = require('./lancamento.js');
67
67
  const glifos = require('./glifos.js');
68
68
  const personagem = require('./personagem.js');
69
69
  const mundo = require('./mundo.js');
70
+ const cenarios = require('./cenarios.js');
71
+ const movimentos = require('./movimentos.js');
70
72
  const colecao = require('./colecao.js');
71
73
  const { escolherEstilo, LEVES } = require('./estilo.js');
72
74
  const sonsLeves = require('./sons-leves.js');
@@ -277,6 +279,28 @@ function projetoRecente() {
277
279
  return lista[0] || null;
278
280
  }
279
281
 
282
+ /* Sem id, o projeto é o que acabou de ser criado. Visto em 12/10: o agente
283
+ criou o projeto, chamou estudio_narrar sem id e o vídeo morreu ali. Só vale
284
+ para chamada SEM id (id errado continua sendo erro) e para projeto de até
285
+ 30 minutos, como no estudio_plano. */
286
+ function projetoOuRecente(id) {
287
+ if (id != null && String(id).trim() !== '') return projeto.ler(id);
288
+ return projetoRecente();
289
+ }
290
+
291
+ /* roteiro.objetos: [{nome, src}] com o PNG DENTRO da pasta do projeto (o que
292
+ o estudio_midia baixou ou recortou). Caminho de fora some: o render só
293
+ enxerga a pasta do projeto, e a IA não escolhe arquivo da máquina. */
294
+ function objetosDoRoteiro(objetos, pasta) {
295
+ const raiz = path.resolve(pasta);
296
+ return (Array.isArray(objetos) ? objetos : []).map((o) => {
297
+ if (!o || typeof o.src !== 'string') return null;
298
+ const abs = path.resolve(raiz, o.src);
299
+ if (!abs.startsWith(raiz + path.sep) || !/\.png$/i.test(abs) || !fs.existsSync(abs)) return null;
300
+ return { nome: o.nome, src: path.relative(raiz, abs).replace(/\\/g, '/') };
301
+ }).filter(Boolean);
302
+ }
303
+
280
304
  /** O roteiro é utilizável como veio? (objeto ou JSON, com partes de tipo conhecido) */
281
305
  function roteiroValido(r) {
282
306
  let x = r;
@@ -527,6 +551,19 @@ async function ferramentaMidia(args) {
527
551
  /* A BIBLIOTECA (colecao.js, 11/10): o cardápio inteiro, o ícone 3D e a
528
552
  animação pronta. Procurar não usa rede (o índice mora no pacote); com
529
553
  id, baixa para o projeto e devolve o elemento pronto. */
554
+ /* OS MOVIMENTOS (movimentos.js, 12/10): 100+ entradas, saídas, ênfases e loops com nome. */
555
+ if (acao === 'movimentos' || acao === 'movimento') {
556
+ return movimentos.procurar({ termo: args.termo, categoria: args.categoria, limite: Number(args.limite) || 30 });
557
+ }
558
+ /* OS CENÁRIOS (cenarios.js, 12/10): os lugares prontos + os que a IA criou
559
+ e a pessoa deixou guardar. Salvar só com o sim dela (autorizado:true). */
560
+ if (acao === 'cenarios' || acao === 'cenários') {
561
+ return { ok: true, prontos: Object.keys(mundo.LUGARES), salvos: cenarios.listar(),
562
+ comoCriar: 'na cena do estilo personagem: lugar (um pronto como base), ceu [cor de cima, cor do horizonte], chao {tipo, cor, cor2}, parede, luz, acento, hora e elementos. Ficou bom? Pergunte se pode salvar e use acao:"salvar_cenario".' };
563
+ }
564
+ if (acao === 'salvar_cenario' || acao === 'salvar_cenário') {
565
+ return cenarios.salvar({ nome: args.nome, cenario: args.cenario, autorizado: args.autorizado, bases: Object.keys(mundo.LUGARES) });
566
+ }
530
567
  if (acao === 'biblioteca' || acao === 'cardapio' || acao === 'cardápio') return colecao.cardapio();
531
568
  if (acao === 'icones3d' || acao === 'icone3d' || acao === 'animacoes' || acao === 'animacao' || acao === 'animação' || acao === 'animações') {
532
569
  const ehAnim = /^anima/.test(acao);
@@ -585,8 +622,9 @@ async function baixarEscolhido(lista, args, p) {
585
622
  async function ferramentaNarrar(args) {
586
623
  if (args.vozes) return rodarPython('narrar.py', ['vozes'], { timeout: 30000 });
587
624
 
588
- const p = projeto.ler(args.id);
625
+ const p = projetoOuRecente(args.id);
589
626
  if (!p) return semProjeto(args.id);
627
+ if (args.id == null || String(args.id).trim() === '') args = { ...args, id: p.id };
590
628
  const texto = String(args.texto || '').trim();
591
629
  if (!texto) return { ok: false, error: 'não há texto para narrar.' };
592
630
 
@@ -616,8 +654,9 @@ async function ferramentaNarrar(args) {
616
654
 
617
655
  async function ferramentaVideo(args, ctx = {}) {
618
656
  const acao = String(args.acao || 'medir').toLowerCase();
619
- const p = projeto.ler(args.id);
657
+ const p = projetoOuRecente(args.id);
620
658
  if (!p) return semProjeto(args.id);
659
+ if (args.id == null || String(args.id).trim() === '') args = { ...args, id: p.id };
621
660
 
622
661
  const pronto = video.conferir();
623
662
  if (!pronto.ok) return pronto;
@@ -1884,6 +1923,7 @@ async function prepararRoteiro(p, args) {
1884
1923
  if (b && b.ok) achados[nome] = b.src;
1885
1924
  }
1886
1925
  mundo.aplicarIcones(r.partitura, achados);
1926
+ mundo.adicionarObjetos(r.partitura, objetosDoRoteiro(roteiro.objetos, p.pasta));
1887
1927
  const icones = Object.keys(achados);
1888
1928
  projeto.anotar(p.id, 'plano', `estilo personagem: ${r.resumo.elenco.join(' e ')}${r.resumo.figurantes.length ? ` (+ ${r.resumo.figurantes.join(', ')})` : ''} em ${r.resumo.lugares.join(' → ')}, ${r.resumo.planos} planos, ${r.resumo.segundos}s${icones.length ? `; em blocos: ${icones.join(', ')}` : ''}`);
1889
1929
  }
@@ -1962,8 +2002,9 @@ async function transcrever(p, alvo, ctx) {
1962
2002
  // ── estudio_render ───────────────────────────────────────────────────────
1963
2003
 
1964
2004
  async function ferramentaRender(args, ctx = {}) {
1965
- const p = projeto.ler(args.id);
2005
+ const p = projetoOuRecente(args.id);
1966
2006
  if (!p) return semProjeto(args.id);
2007
+ if (args.id == null || String(args.id).trim() === '') args = { ...args, id: p.id };
1967
2008
  const pl = projeto.lerPlano(args.id);
1968
2009
  if (!pl) return { ok: false, error: 'não há plano neste projeto.' };
1969
2010
 
@@ -2209,7 +2250,7 @@ async function ferramentaSite(args) {
2209
2250
  }
2210
2251
 
2211
2252
  module.exports = {
2212
- vazamentoNoComeco, variarIcones,
2253
+ vazamentoNoComeco, variarIcones, objetosDoRoteiro,
2213
2254
  ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaNarrar,
2214
2255
  ferramentaVideo, ferramentaRender, ferramentaSite,
2215
2256
  projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes, prepararRoteiro, roteiroValido, juntarVozes,
@@ -0,0 +1,212 @@
1
+ /**
2
+ * movimentos.js — a biblioteca de MOVIMENTOS prontos: entradas, saídas,
3
+ * ênfases e loops, cada um com nome, que viram quadros-chave do `anima`.
4
+ *
5
+ * "preciso que você deixe os motions com + de 100 tipos de animações …
6
+ * além da IA poder criar os próprios e pesquisar" (Joel, 12/10)
7
+ *
8
+ * Até aqui todo movimento era escrito à mão pelo modelo, quadro a quadro
9
+ * (`anima: {opacidade: [{t, v}]}`). Modelo grátis escreve sempre os mesmos
10
+ * dois: esmaecer e subir. Com nome, ele PROCURA ("quique", "desfoque",
11
+ * "saída para a esquerda"), escolhe e escreve uma palavra:
12
+ *
13
+ * elemento.movimento = "quicar-de-baixo"
14
+ * elemento.movimento = {nome: "sair-girando", em: 2.4, duracao: 0.5}
15
+ * elemento.movimento = ["surgir-desfocado", {nome: "flutuar", em: 0.6}]
16
+ *
17
+ * O plano expande isso em `anima` antes de conferir (plano.js). O que a IA
18
+ * escreveu em `anima` vence o movimento pronto na mesma propriedade — e ela
19
+ * continua livre para inventar o seu, quadro a quadro.
20
+ *
21
+ * Os números: x/y/z são DESLOCAMENTOS em px (o render soma à posição), as
22
+ * rotações em graus, o desfoque em px, opacidade/escala/brilho/saturação em
23
+ * fração. Todo movimento de entrada termina no repouso (0 de deslocamento,
24
+ * escala 1, opacidade 1); toda saída começa nele.
25
+ */
26
+
27
+ 'use strict';
28
+
29
+ // Cada quadro: [fração do tempo 0..1, valor, suavização até ele].
30
+ const DIRECOES = {
31
+ 'de-baixo': { eixo: 'y', sinal: 1 }, 'de-cima': { eixo: 'y', sinal: -1 },
32
+ 'da-esquerda': { eixo: 'x', sinal: -1 }, 'da-direita': { eixo: 'x', sinal: 1 },
33
+ };
34
+ const PARA = { 'de-baixo': 'para-baixo', 'de-cima': 'para-cima', 'da-esquerda': 'para-a-esquerda', 'da-direita': 'para-a-direita' };
35
+
36
+ /* As entradas com direção: cada estilo × as quatro direções. */
37
+ const ESTILOS_DIRECAO = {
38
+ deslizar: { d: 0.6, desc: 'desliza e aparece', f: (e, s) => ({ [e]: [[0, 80 * s], [1, 0, 'saida']], opacidade: [[0, 0], [0.6, 1, 'suave']] }) },
39
+ 'deslizar-desfocado': { d: 0.7, desc: 'desliza saindo do desfoque', f: (e, s) => ({ [e]: [[0, 70 * s], [1, 0, 'saida']], opacidade: [[0, 0], [0.5, 1]], desfoque: [[0, 18], [1, 0, 'saida']] }) },
40
+ 'chegar-elastico': { d: 0.9, desc: 'chega passando do ponto e volta, como mola', f: (e, s) => ({ [e]: [[0, 220 * s], [1, 0, 'elastico']], opacidade: [[0, 0], [0.25, 1]] }) },
41
+ quicar: { d: 0.9, desc: 'cai e quica', f: (e, s) => ({ [e]: [[0, 260 * s], [1, 0, 'salto']], opacidade: [[0, 0], [0.2, 1]] }) },
42
+ varrer: { d: 0.8, desc: 'atravessa rápido de longe e freia', f: (e, s) => ({ [e]: [[0, 900 * s], [1, 0, 'arrasto']], opacidade: [[0, 0], [0.3, 1]] }) },
43
+ 'girar-entrando': { d: 0.8, desc: 'entra girando', f: (e, s) => ({ [e]: [[0, 160 * s], [1, 0, 'saida']], rotacao: [[0, -90 * s], [1, 0, 'saida']], opacidade: [[0, 0], [0.4, 1]] }) },
44
+ };
45
+
46
+ /* As entradas sem direção. */
47
+ const ENTRADAS = {
48
+ surgir: { d: 0.5, desc: 'só aparece (esmaecer)', f: () => ({ opacidade: [[0, 0], [1, 1, 'suave']] }) },
49
+ crescer: { d: 0.6, desc: 'cresce do nada', f: () => ({ escala: [[0, 0], [1, 1, 'saida']], opacidade: [[0, 0], [0.3, 1]] }) },
50
+ 'crescer-elastico': { d: 0.9, desc: 'cresce como mola', f: () => ({ escala: [[0, 0], [1, 1, 'elastico']] }) },
51
+ 'pular-pra-dentro': { d: 0.6, desc: 'cresce, passa um pouco e assenta', f: () => ({ escala: [[0, 0], [0.7, 1.15, 'saida'], [1, 1, 'suave']], opacidade: [[0, 0], [0.3, 1]] }) },
52
+ 'encolher-pra-dentro': { d: 0.6, desc: 'vem grande e encolhe até o lugar', f: () => ({ escala: [[0, 1.8], [1, 1, 'saida']], opacidade: [[0, 0], [0.5, 1]] }) },
53
+ 'surgir-desfocado': { d: 0.7, desc: 'sai do desfoque', f: () => ({ desfoque: [[0, 24], [1, 0, 'saida']], opacidade: [[0, 0], [0.6, 1]] }) },
54
+ 'virar-de-lado': { d: 0.7, desc: 'vira em Y como carta', f: () => ({ rotacaoY: [[0, 90], [1, 0, 'saida']], opacidade: [[0, 0], [0.3, 1]] }) },
55
+ 'virar-de-cima': { d: 0.7, desc: 'tomba em X, de cima para baixo', f: () => ({ rotacaoX: [[0, -90], [1, 0, 'saida']], opacidade: [[0, 0], [0.3, 1]] }) },
56
+ 'girar-crescendo': { d: 0.8, desc: 'cresce dando meia volta', f: () => ({ rotacao: [[0, -180], [1, 0, 'saida']], escala: [[0, 0], [1, 1, 'saida']] }) },
57
+ 'vir-do-fundo': { d: 0.8, desc: 'avança da profundidade (3D)', f: () => ({ z: [[0, -700], [1, 0, 'saida']], opacidade: [[0, 0], [0.4, 1]] }) },
58
+ 'cair-do-alto': { d: 0.8, desc: 'cai do topo e assenta com um quique', f: () => ({ y: [[0, -500], [1, 0, 'salto']], rotacao: [[0, -12], [1, 0, 'saida']] }) },
59
+ 'estourar-de-luz': { d: 0.6, desc: 'acende estourado e assenta', f: () => ({ brilho: [[0, 3], [1, 1, 'saida']], opacidade: [[0, 0], [0.3, 1]] }) },
60
+ 'ganhar-cor': { d: 0.8, desc: 'sai do cinza para a cor', f: () => ({ saturacao: [[0, 0], [1, 1, 'suave']], opacidade: [[0, 0], [0.4, 1]] }) },
61
+ 'abrir-letras': { d: 0.8, desc: 'as letras chegam juntas de longe (texto)', f: () => ({ espacamento: [[0, 40], [1, 0, 'saida']], opacidade: [[0, 0], [0.5, 1]] }) },
62
+ 'esticar-de-lado': { d: 0.6, desc: 'estica na horizontal', f: () => ({ escalaX: [[0, 0], [1, 1, 'saida']] }) },
63
+ 'esticar-de-pe': { d: 0.6, desc: 'estica na vertical', f: () => ({ escalaY: [[0, 0], [1, 1, 'saida']] }) },
64
+ 'carimbo': { d: 0.35, desc: 'bate na tela como carimbo', f: () => ({ escala: [[0, 2.4], [1, 1, 'entrada']], opacidade: [[0, 0], [0.2, 1]] }) },
65
+ 'aproximar-editorial': { d: 0.9, desc: 'entra devagar, como revista', f: () => ({ y: [[0, 24], [1, 0, 'editorial']], escala: [[0, 0.96], [1, 1, 'editorial']], opacidade: [[0, 0], [1, 1, 'editorial']] }) },
66
+ };
67
+
68
+ /* As ênfases: o elemento já está lá e chama atenção (começam e terminam no repouso). */
69
+ const ENFASES = {
70
+ pulsar: { d: 0.5, desc: 'cresce e volta uma vez', f: () => ({ escala: [[0, 1], [0.5, 1.12, 'suave'], [1, 1, 'suave']] }) },
71
+ tremer: { d: 0.5, desc: 'treme de lado (erro, negação)', f: () => ({ x: [[0, 0], [0.15, -14], [0.3, 12], [0.45, -10], [0.6, 8], [0.75, -4], [1, 0]] }) },
72
+ balancar: { d: 0.8, desc: 'balança como pêndulo', f: () => ({ rotacao: [[0, 0], [0.25, 12, 'suave'], [0.5, -8, 'suave'], [0.75, 4, 'suave'], [1, 0, 'suave']] }) },
73
+ piscar: { d: 0.6, desc: 'pisca duas vezes', f: () => ({ opacidade: [[0, 1], [0.25, 0.1], [0.5, 1], [0.75, 0.1], [1, 1]] }) },
74
+ 'dar-uma-volta': { d: 0.8, desc: 'gira 360°', f: () => ({ rotacao: [[0, 0], [1, 360, 'suave']] }) },
75
+ 'virar-moeda': { d: 0.8, desc: 'gira em Y como moeda', f: () => ({ rotacaoY: [[0, 0], [1, 360, 'suave']] }) },
76
+ 'pular': { d: 0.6, desc: 'dá um pulinho', f: () => ({ y: [[0, 0], [0.4, -40, 'saida'], [1, 0, 'salto']] }) },
77
+ 'tada': { d: 0.9, desc: 'cresce e chacoalha (conquista)', f: () => ({ escala: [[0, 1], [0.2, 0.9], [0.4, 1.12], [0.9, 1.12], [1, 1]], rotacao: [[0, 0], [0.3, -4], [0.45, 4], [0.6, -4], [0.75, 4], [1, 0]] }) },
78
+ 'borracha': { d: 0.8, desc: 'amassa e estica como borracha', f: () => ({ escalaX: [[0, 1], [0.3, 1.25], [0.4, 0.75], [0.5, 1.15], [0.65, 0.95], [1, 1]], escalaY: [[0, 1], [0.3, 0.75], [0.4, 1.25], [0.5, 0.85], [0.65, 1.05], [1, 1]] }) },
79
+ 'gelatina': { d: 0.9, desc: 'treme mole', f: () => ({ escalaX: [[0, 1], [0.2, 1.1], [0.4, 0.92], [0.6, 1.04], [0.8, 0.98], [1, 1]], escalaY: [[0, 1], [0.2, 0.92], [0.4, 1.08], [0.6, 0.97], [0.8, 1.01], [1, 1]] }) },
80
+ 'acenar': { d: 0.8, desc: 'balança curto, como aceno', f: () => ({ rotacao: [[0, 0], [0.2, 14], [0.4, -8], [0.6, 14], [0.8, -4], [1, 0]] }) },
81
+ 'acender': { d: 0.6, desc: 'brilha e apaga uma vez', f: () => ({ brilho: [[0, 1], [0.4, 1.8, 'suave'], [1, 1, 'suave']] }) },
82
+ 'inclinar-3d': { d: 0.9, desc: 'inclina em 3D e volta', f: () => ({ rotacaoX: [[0, 0], [0.5, 18, 'suave'], [1, 0, 'suave']], rotacaoY: [[0, 0], [0.5, -14, 'suave'], [1, 0, 'suave']] }) },
83
+ 'cabecear': { d: 0.6, desc: 'acena com a cabeça (sim)', f: () => ({ rotacaoX: [[0, 0], [0.3, 20], [0.6, -6], [1, 0]] }) },
84
+ 'empurrao': { d: 0.4, desc: 'leva um soco e volta', f: () => ({ escala: [[0, 1], [0.25, 0.86, 'entrada'], [1, 1, 'elastico']] }) },
85
+ 'sublinhar-luz': { d: 0.7, desc: 'fica vivo por um instante', f: () => ({ saturacao: [[0, 1], [0.4, 1.8, 'suave'], [1, 1, 'suave']], brilho: [[0, 1], [0.4, 1.3, 'suave'], [1, 1]] }) },
86
+ };
87
+
88
+ /* Os loops: repetem pela duração inteira (o padrão é a cena toda, 4 s). */
89
+ const LOOPS = {
90
+ flutuar: { d: 4, ciclo: 2.4, desc: 'sobe e desce devagar', f: () => ({ y: [[0, 0], [0.5, -16, 'suave'], [1, 0, 'suave']] }) },
91
+ respirar: { d: 4, ciclo: 2, desc: 'cresce e encolhe de leve', f: () => ({ escala: [[0, 1], [0.5, 1.05, 'suave'], [1, 1, 'suave']] }) },
92
+ 'girar-sem-parar': { d: 4, ciclo: 4, desc: 'gira sem parar', f: () => ({ rotacao: [[0, 0], [1, 360, 'linear']] }) },
93
+ 'oscilar': { d: 4, ciclo: 1.6, desc: 'balança de leve sem parar', f: () => ({ rotacao: [[0, -5], [0.5, 5, 'suave'], [1, -5, 'suave']] }) },
94
+ 'batimento': { d: 4, ciclo: 1, desc: 'pulsa como coração', f: () => ({ escala: [[0, 1], [0.15, 1.12], [0.3, 1], [0.45, 1.08], [0.6, 1], [1, 1]] }) },
95
+ 'piscar-devagar': { d: 4, ciclo: 1.6, desc: 'some e volta devagar', f: () => ({ opacidade: [[0, 1], [0.5, 0.35, 'suave'], [1, 1, 'suave']] }) },
96
+ 'orbitar': { d: 4, ciclo: 3, desc: 'anda em círculo pequeno', f: () => ({ x: [[0, 20], [0.25, 0, 'suave'], [0.5, -20, 'suave'], [0.75, 0, 'suave'], [1, 20, 'suave']], y: [[0, 0], [0.25, 20, 'suave'], [0.5, 0, 'suave'], [0.75, -20, 'suave'], [1, 0, 'suave']] }) },
97
+ 'deriva': { d: 4, ciclo: 4, desc: 'anda de lado bem devagar', f: () => ({ x: [[0, -12], [0.5, 12, 'suave'], [1, -12, 'suave']] }) },
98
+ 'girar-3d': { d: 4, ciclo: 5, desc: 'gira em Y sem parar, como vitrine', f: () => ({ rotacaoY: [[0, 0], [1, 360, 'linear']] }) },
99
+ 'brilho-vivo': { d: 4, ciclo: 2, desc: 'o brilho respira', f: () => ({ brilho: [[0, 1], [0.5, 1.25, 'suave'], [1, 1, 'suave']] }) },
100
+ 'balanco-3d': { d: 4, ciclo: 3, desc: 'inclina em 3D de um lado para o outro', f: () => ({ rotacaoY: [[0, -12], [0.5, 12, 'suave'], [1, -12, 'suave']] }) },
101
+ 'tremor-leve': { d: 4, ciclo: 0.3, desc: 'vibra o tempo todo (tensão, alerta)', f: () => ({ x: [[0, 0], [0.25, -2], [0.5, 2], [0.75, -1], [1, 0]] }) },
102
+ };
103
+
104
+ /* A SAÍDA é a entrada ao contrário: o mesmo desenho, tempo invertido. */
105
+ function inverter(gerado) {
106
+ const out = {};
107
+ for (const [prop, qs] of Object.entries(gerado)) {
108
+ const ordem = [...qs].reverse();
109
+ // A suavização de um quadro vale ATÉ ele; invertida, passa ao vizinho.
110
+ out[prop] = ordem.map(([f, v], i) => [1 - f, v, i > 0 ? inverterE(ordem[i - 1][2]) : undefined]);
111
+ }
112
+ return out;
113
+ }
114
+ const inverterE = (e) => ({ saida: 'entrada', entrada: 'saida', arrasto: 'acelera', acelera: 'arrasto', elastico: 'entrada', salto: 'entrada' }[e] || e);
115
+ const nomeDaSaida = (n) => n.replace(/^surgir/, 'sumir').replace(/^crescer/, 'encolher-sumindo').replace(/^chegar/, 'partir')
116
+ .replace(/^vir-do-fundo/, 'ir-pro-fundo').replace(/^cair-do-alto/, 'cair-pra-fora').replace(/^ganhar-cor/, 'perder-cor')
117
+ .replace(/^abrir-letras/, 'espalhar-letras').replace(/^estourar-de-luz/, 'apagar-estourando');
118
+
119
+ function montarCatalogo() {
120
+ const cat = {};
121
+ const pore = (nome, categoria, def, gerar) => { cat[nome] = { nome, categoria, descricao: def.desc, duracao: def.d, ciclo: def.ciclo, gerar }; };
122
+ for (const [estilo, def] of Object.entries(ESTILOS_DIRECAO)) {
123
+ for (const [dir, { eixo, sinal }] of Object.entries(DIRECOES)) {
124
+ const gerar = () => def.f(eixo, sinal);
125
+ pore(`${estilo}-${dir}`, 'entrada', { ...def, desc: `${def.desc}, vindo ${dir.replace('-', ' ')}` }, gerar);
126
+ pore(`${estilo}-${PARA[dir]}`.replace(/^deslizar-desfocado/, 'sair-desfocando').replace(/^deslizar/, 'sair-deslizando')
127
+ .replace(/^chegar-elastico/, 'sair-elastico').replace(/^quicar/, 'sair-quicando').replace(/^varrer/, 'sair-varrendo')
128
+ .replace(/^girar-entrando/, 'sair-girando'), 'saida', { ...def, desc: `sai: ${def.desc.split(',')[0]}, ${PARA[dir].replace(/-/g, ' ')}` },
129
+ () => inverter(def.f(eixo, sinal)));
130
+ }
131
+ }
132
+ for (const [nome, def] of Object.entries(ENTRADAS)) {
133
+ pore(nome, 'entrada', def, def.f);
134
+ pore(nomeDaSaida(nome) === nome ? `${nome}-saindo` : nomeDaSaida(nome), 'saida', { ...def, desc: `sai: ${def.desc}` }, () => inverter(def.f()));
135
+ }
136
+ for (const [nome, def] of Object.entries(ENFASES)) pore(nome, 'enfase', def, def.f);
137
+ for (const [nome, def] of Object.entries(LOOPS)) pore(nome, 'loop', def, def.f);
138
+ return cat;
139
+ }
140
+ const CATALOGO = montarCatalogo();
141
+
142
+ const r3 = (n) => Math.round(n * 1000) / 1000;
143
+ const semAcento = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
144
+
145
+ /** Os quadros-chave de um movimento, a partir de `em` segundos. */
146
+ const achar = (nome) => CATALOGO[semAcento(nome).trim().replace(/\s+/g, '-')] || null;
147
+
148
+ function quadros(nome, { em = 0, duracao } = {}) {
149
+ const m = achar(nome);
150
+ if (!m) return null;
151
+ const d = Number(duracao) > 0 ? Number(duracao) : m.duracao;
152
+ const gerado = m.gerar();
153
+ const anima = {};
154
+ if (m.categoria === 'loop') {
155
+ const ciclo = Math.min(m.ciclo || d, d);
156
+ const voltas = Math.max(1, Math.round(d / ciclo));
157
+ for (const [prop, qs] of Object.entries(gerado)) {
158
+ const lista = [];
159
+ for (let k = 0; k < voltas; k++) {
160
+ for (const [f, v, e] of qs) {
161
+ if (k > 0 && f === 0) continue; // o fim de uma volta é o começo da próxima
162
+ // o giro sem parar soma: a 2ª volta vai de 360 a 720
163
+ const soma = /rotacao/.test(prop) && qs[qs.length - 1][1] === 360 ? 360 * k : 0;
164
+ lista.push({ t: r3(em + (k + f) * ciclo), v: v + soma, ...(e ? { e } : {}) });
165
+ }
166
+ }
167
+ anima[prop] = lista;
168
+ }
169
+ return anima;
170
+ }
171
+ for (const [prop, qs] of Object.entries(gerado)) anima[prop] = qs.map(([f, v, e]) => ({ t: r3(em + f * d), v, ...(e ? { e } : {}) }));
172
+ return anima;
173
+ }
174
+
175
+ /** Procura por palavra (no nome ou na descrição) e por categoria. Sem termo, devolve um pouco de cada. */
176
+ function procurar({ termo = '', categoria = '', limite = 30 } = {}) {
177
+ const palavras = semAcento(termo).split(/[^a-z0-9]+/).filter(Boolean);
178
+ const cat = semAcento(categoria).replace('ê', 'e');
179
+ const lista = Object.values(CATALOGO).filter((m) => (!cat || m.categoria === cat.replace(/s$/, ''))
180
+ && palavras.every((p) => semAcento(m.nome + ' ' + m.descricao).includes(p)));
181
+ return {
182
+ ok: true, total: Object.keys(CATALOGO).length, achados: lista.length,
183
+ movimentos: lista.slice(0, limite).map((m) => ({ nome: m.nome, categoria: m.categoria, faz: m.descricao, duracao: m.duracao })),
184
+ comoUsar: 'no elemento do plano: movimento:"<nome>" ou {nome, em (s), duracao (s)} ou uma lista deles. O que você escrever em anima vence na mesma propriedade; pode inventar o seu em anima, quadro a quadro.',
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Troca `movimento` por `anima` em todos os elementos do plano. Muda o plano
190
+ * no lugar e devolve os nomes que não existem (o plano.js vira problema).
191
+ */
192
+ function expandir(plano) {
193
+ const desconhecidos = [];
194
+ for (const [i, cena] of ((plano && plano.cenas) || []).entries()) {
195
+ for (const [j, el] of ((cena && cena.elementos) || []).entries()) {
196
+ if (!el || el.movimento == null) continue;
197
+ const pedidos = (Array.isArray(el.movimento) ? el.movimento : [el.movimento]).map((m) => (typeof m === 'string' ? { nome: m } : m || {}));
198
+ const juntado = {};
199
+ for (const p of pedidos) {
200
+ const a = quadros(p.nome, { em: Number(p.em) || 0, duracao: p.duracao ?? ((achar(p.nome) || {}).categoria === 'loop' ? cena.duracao : undefined) });
201
+ if (!a) { desconhecidos.push({ onde: `cenas[${i}].elementos[${j}].movimento`, nome: String(p.nome) }); continue; }
202
+ // Dois movimentos na mesma propriedade: os quadros se somam na linha do tempo.
203
+ for (const [prop, qs] of Object.entries(a)) juntado[prop] = [...(juntado[prop] || []), ...qs].sort((x, y) => x.t - y.t);
204
+ }
205
+ el.anima = { ...juntado, ...(el.anima || {}) };
206
+ delete el.movimento;
207
+ }
208
+ }
209
+ return desconhecidos;
210
+ }
211
+
212
+ module.exports = { CATALOGO, quadros, procurar, expandir, inverter };
@@ -237,7 +237,11 @@ function posicionar(nomes, { semente = 1, agua = false, dentro = false, largura
237
237
  * pedido da pessoa: as coisas que ele cita entram na cena), semente, nElenco}
238
238
  */
239
239
  function mundoDe(pedido, { texto = '', pedidoTexto = '', semente = 1, nElenco = 1 } = {}) {
240
- const obj = pedido && typeof pedido === 'object' ? pedido : { lugar: String(pedido || '') };
240
+ let obj = pedido && typeof pedido === 'object' ? pedido : { lugar: String(pedido || '') };
241
+ /* Um cenário que a IA criou e a pessoa deixou salvar (cenarios.js): o
242
+ nome dele no lugar traz tudo de volta; o que a cena disser por cima vence. */
243
+ const salvo = require('./cenarios.js').ler(obj.cenario || obj.lugar);
244
+ if (salvo) obj = { ...salvo, ...obj, lugar: salvo.base, hora: obj.hora || salvo.hora, elementos: obj.elementos || salvo.elementos };
241
245
  const descr = [obj.lugar, obj.descricao, obj.hora].filter(Boolean).join(' ');
242
246
  let { lugar, hora } = lerLugar(descr);
243
247
  const nomeDireto = sem(obj.lugar).replace(/\s+/g, '_');
@@ -302,4 +306,23 @@ function aplicarIcones(partitura, achados) {
302
306
  return partitura;
303
307
  }
304
308
 
305
- module.exports = { mundoDe, lerLugar, lerCoisas, lerElenco, iconesPendentes, aplicarIcones, LUGARES, HORAS, PECAS, ESPECIES, sem };
309
+ /**
310
+ * As peças que a IA TROUXE para a cena: a logo, o botão ou o produto recortado
311
+ * do site (estudio_midia acao:elemento), um PNG sem fundo que vira blocos como
312
+ * os ícones 3D. "Imagens e parte de sites devem entrar no motion como ícone
313
+ * 3D" (Joel, 12/10). Ficam na frente, no centro, um pouco maiores que o resto
314
+ * do lugar: é o assunto. No máximo três, para não virar vitrine.
315
+ */
316
+ function adicionarObjetos(partitura, objetos) {
317
+ const lista = (Array.isArray(objetos) ? objetos : []).filter((o) => o && o.src).slice(0, 3);
318
+ if (!lista.length) return partitura;
319
+ for (const m of partitura.mundos || []) {
320
+ lista.forEach((o, i) => {
321
+ const x = (i - (lista.length - 1) / 2) * 2.4;
322
+ m.props.push({ icone: String(o.nome || 'objeto').slice(0, 40), src: o.src, x, z: -1.6, raio: 1, giro: 0, escala: 1.35, trazido: true });
323
+ });
324
+ }
325
+ return partitura;
326
+ }
327
+
328
+ module.exports = { mundoDe, lerLugar, lerCoisas, lerElenco, iconesPendentes, aplicarIcones, adicionarObjetos, LUGARES, HORAS, PECAS, ESPECIES, sem };
@@ -49,6 +49,7 @@ const fs = require('fs');
49
49
  const path = require('path');
50
50
  const morfar = require('./morfar.js');
51
51
  const kit = require('./kit.js');
52
+ const movimentos = require('./movimentos.js');
52
53
 
53
54
  // "kit": uma peça pronta do kit de motion (kit.js), pelo nome — ver kit/*.jsx.
54
55
  const TIPOS = new Set(['texto', 'forma', 'imagem', 'video', 'icone', 'legenda', 'componente', 'grupo', 'kit']);
@@ -283,6 +284,11 @@ function conferir(plano, { pasta = null } = {}) {
283
284
  if (!plano || typeof plano !== 'object') {
284
285
  return { ok: false, problemas: [erro('plano', 'o plano é um objeto JSON')], duracao: 0 };
285
286
  }
287
+ /* `movimento: "quicar-de-baixo"` vira quadros-chave (movimentos.js) antes
288
+ de tudo: o que segue confere o `anima` como se a IA o tivesse escrito. */
289
+ for (const d of movimentos.expandir(plano)) {
290
+ problemas.push(erro(d.onde, `o movimento "${d.nome}" não existe — procure com estudio_midia acao:"movimentos" termo:"…"`));
291
+ }
286
292
 
287
293
  const f = plano.formato || {};
288
294
  const largura = Number(f.largura) || 1920;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "10.6.0",
3
+ "version": "10.7.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/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.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 && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.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/gerativo-variedade.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 && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"