primocode 9.8.0 → 9.9.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 v9.8.0-beta.23
1
+ # PrimoCode v9.9.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 v9.8.0-beta.23
12
+ ▐▛███▜▌ Primo Code v9.9.0
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
@@ -31,8 +31,10 @@ const { caber } = require('./composicao.js');
31
31
  const r2 = (n) => Math.round(n * 100) / 100;
32
32
  const tokens = (t) => String(t || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
33
33
 
34
- /** Quanto tempo um texto precisa ficar na tela para ser lido. */
35
- const leitura = (t) => 0.55 + 0.22 * String(t || '').split(/\s+/).filter(Boolean).length;
34
+ /** Quanto tempo um texto precisa ficar na tela para ser lido. Nunca menos que
35
+ 0,3 s por palavra: a regra de legibilidade do /brag (Arthur, 28/09). */
36
+ const nPalavras = (t) => String(t || '').split(/\s+/).filter(Boolean).length;
37
+ const leitura = (t) => Math.max(0.55 + 0.22 * nPalavras(t), 0.3 * nPalavras(t));
36
38
 
37
39
  /** A fala desta parte (o que a voz diz): a "fala", ou os textos da tela. */
38
40
  /* A voz lê EXATAMENTE o que está na tela, na ordem da tela ("a narração tem
@@ -229,7 +231,7 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
229
231
  com a voz é bom, ler é obrigatório. Se a leitura pede mais que a fala,
230
232
  a cena cresce (medido: "EDIÇÕES" ficava meio segundo na tela). */
231
233
  // Texto curto (um número, "3 DIAS.") se lê de relance: não espera a leitura cheia e não atrasa a voz.
232
- const minimo = (x) => Math.min(leitura(x), String(x).split(/\s+/).length <= 3 ? 0.75 : 1.6);
234
+ const minimo = (x) => Math.max(0.3 * nPalavras(x), Math.min(leitura(x), nPalavras(x) <= 3 ? 0.75 : 1.6));
233
235
  for (let k = 1; k < inicios.length; k++) inicios[k] = r2(Math.max(inicios[k], inicios[k - 1] + minimo(textosDaParte[k - 1])));
234
236
  if (inicios.length) dur = r2(Math.max(dur, inicios[inicios.length - 1] + leitura(textosDaParte[inicios.length - 1]) + 0.3));
235
237
  const fimDe = (k) => (k < inicios.length - 1 ? inicios[k + 1] : dur);
@@ -145,13 +145,36 @@ function forteDe(g) {
145
145
  return forte;
146
146
  }
147
147
 
148
+ /* O B-ROLL EM QUATRO ROTAS (Arthur, do b-roll-finder, 28/09): a frase diz qual
149
+ imagem pede, na ordem abaixo. A primeira que casa decide.
150
+ marca a frase diz o nome da marca → a logo / o site da marca
151
+ numero a frase tem número → sem foto: o número já é a imagem
152
+ concreto uma palavra tem foto no acervo → a foto de estoque dela
153
+ pessoa ideia abstrata → fica no rosto de quem fala
154
+ `palavra` é onde o corte se ancora: ele entra 0,2 s DEPOIS dela. */
155
+ const DEPOIS_DA_PALAVRA = 0.2;
156
+
157
+ function rotaDeApoio(g, { marca = null } = {}) {
158
+ const ws = g || [];
159
+ const nomes = (Array.isArray(marca) ? marca : [marca]).filter(Boolean).map((m) => String(m).split(/\s+/).map(limpa));
160
+ for (const nome of nomes) {
161
+ const k = ws.findIndex((_, i) => nome.every((n, j) => ws[i + j] && limpa(ws[i + j].texto) === n));
162
+ if (k >= 0) return { rota: 'marca', termo: null, palavra: ws[k] };
163
+ }
164
+ const num = ws.find((p) => /\d/.test(p.texto));
165
+ if (num) return { rota: 'numero', termo: null, palavra: num };
166
+ let conc = null;
167
+ for (const p of ws) if (termoDeApoio(p.texto) && (!conc || peso(p.texto) > peso(conc.texto))) conc = p;
168
+ if (conc) return { rota: 'concreto', termo: termoDeApoio(conc.texto), palavra: conc };
169
+ return { rota: 'pessoa', termo: null, palavra: null };
170
+ }
171
+
148
172
  /** Os termos das imagens de apoio que o plano vai pedir — para baixar antes. */
149
- function termosDeApoio(palavras) {
173
+ function termosDeApoio(palavras, opcoes) {
150
174
  const lista = (palavras || []).filter((p) => p && p.texto && p.ate > p.de).sort((a, b) => a.de - b.de);
151
175
  const termos = [];
152
176
  frases(lista).forEach((g, i) => {
153
- const f = forteDe(g);
154
- const t = f && temApoio(i) ? termoDeApoio(f.texto) : null;
177
+ const t = temApoio(i) ? rotaDeApoio(g, opcoes).termo : null;
155
178
  if (t && !termos.includes(t)) termos.push(t);
156
179
  });
157
180
  return termos;
@@ -231,7 +254,7 @@ const ESTILOS = {
231
254
  // Em quais cenas a pessoa é recortada, com a palavra gigante atrás dela.
232
255
  const temRecorte = (i) => i % 4 === 2;
233
256
 
234
- function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, estilo = 'viral', sons = {}, iconesOk = null, imagens = {}, trilha = null, recortes = {} }) {
257
+ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, estilo = 'viral', sons = {}, iconesOk = null, imagens = {}, trilha = null, recortes = {}, marca = null }) {
235
258
  const e = ESTILOS[estilo] || ESTILOS.viral;
236
259
  const lista = (palavras || []).filter((p) => p && p.texto && p.ate > p.de).sort((a, b) => a.de - b.de);
237
260
  if (!lista.length) return { ok: false, error: 'a gravação não tem fala transcrita para editar.' };
@@ -290,9 +313,11 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
290
313
  /* A IMAGEM DE APOIO: na hora da palavra forte, a tela vira a imagem do
291
314
  que está sendo dito, com um zoom lento, e a legenda continua por
292
315
  cima. É o "b-roll" das edições de videomaker. */
293
- const apoio = forte && e.apoio && temApoio(i) && !rec ? imagens[termoDeApoio(forte.texto)] : null;
316
+ const r = e.apoio && temApoio(i) && !rec ? rotaDeApoio(g, { marca }) : null;
317
+ const apoio = !r ? null : r.rota === 'concreto' ? imagens[r.termo] : r.rota === 'marca' ? imagens.marca : null;
294
318
  if (apoio) {
295
- const de = local(forte.de - 0.1);
319
+ // O corte entra DEPOIS da palavra, nunca antes: quem ouve "cachorro" vê o cachorro.
320
+ const de = local(r.palavra.de + DEPOIS_DA_PALAVRA);
296
321
  const vida = Math.min(2.2, dur - de);
297
322
  if (vida > 0.8) {
298
323
  // O apoio é foto ou vídeo (o Pexels dá os dois; vídeo é melhor).
@@ -447,4 +472,4 @@ async function prepararSons(pasta, video) {
447
472
  return sons;
448
473
  }
449
474
 
450
- module.exports = { montarEdicao, temRecorte, prepararSons, iconeDe, peso, frases, blocos, termosDeApoio, termoDeApoio, ESTILOS, ICONES, FOTO };
475
+ module.exports = { montarEdicao, temRecorte, prepararSons, iconeDe, peso, frases, blocos, termosDeApoio, termoDeApoio, rotaDeApoio, DEPOIS_DA_PALAVRA, ESTILOS, ICONES, FOTO };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * ende.js — trilha da ende.app: CC BY 4.0, uso comercial, sem Content ID.
3
+ *
4
+ * "Usar músicas CC BY 4.0 sem Content ID, da ende.app." — Arthur, do
5
+ * /brag (28/09/2026)
6
+ *
7
+ * Por que esta fonte: música do Openverse com licença comercial às vezes está
8
+ * registrada no Content ID, e o vídeo do cliente leva reivindicação no
9
+ * YouTube. A página da ende.app diz: "Important: NO Content ID. [...]
10
+ * Everything is under CC BY 4.0. [...] Commercial use is allowed as well."
11
+ * O crédito é opcional ("Attribution is nice but no longer required"), mas
12
+ * gravamos mesmo assim: custa uma linha e protege o cliente.
13
+ *
14
+ * ── SEM API ──────────────────────────────────────────────────────────────
15
+ * A ende.app não tem API, e a busca por texto (?q=) não filtra: devolve
16
+ * sempre a mesma lista. Então lemos os cartões da listagem, que trazem
17
+ * `musicPlayer(id, 'mp3', duração)`, o título, a página e as tags. A escolha é
18
+ * pelas tags e pela duração. `robots.txt` só fecha /user-api/ e /challenge.
19
+ *
20
+ * Se a página mudar, `lerLista` devolve [] e o provedor devolve null, e a
21
+ * corrente do producao.js cai no próximo provedor (o Openverse). A rede e o
22
+ * disco são injetados: o teste roda com um recorte do HTML real.
23
+ */
24
+
25
+ 'use strict';
26
+
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+
30
+ const LISTA = 'https://ende.app/en/search'; // 25 cartões; a /en mostra 11
31
+ const CREDITO = 'Música: Sascha Ende (ende.app), CC BY 4.0';
32
+
33
+ // O clima do roteiro (pt) → palavras que aparecem nas tags da ende.app (en).
34
+ const CLIMAS = {
35
+ emocional: ['emotional', 'acoustic', 'atmospheric', 'piano', 'cinematic', 'ambient', 'calm', 'strings'],
36
+ cinema: ['cinematic', 'epic', 'orchestral', 'atmospheric', 'emotional', 'strings'],
37
+ energetico: ['energetic', 'upbeat', 'beat', 'drums', 'rock', 'arcade', 'breakcore', 'epic'],
38
+ viral: ['upbeat', 'beat', 'energetic', 'pop', 'electronic'],
39
+ impacto: ['epic', 'drums', 'trailer', 'energetic', 'beat'],
40
+ neutro: ['background', 'corporate', 'chilled', 'calm', 'ambient'],
41
+ };
42
+
43
+ const limpar = (s) => String(s || '').replace(/<[^>]*>/g, '').replace(/&amp;/g, '&').replace(/&#0?39;/g, "'").replace(/\s+/g, ' ').trim();
44
+
45
+ /** Os cartões de música da listagem. Página diferente do esperado: []. */
46
+ function lerLista(html) {
47
+ const texto = String(html || '');
48
+ const cortes = [...texto.matchAll(/musicPlayer\(\s*(\d+)\s*,\s*'([^']+\.mp3)'\s*,\s*([\d.]+)/g)];
49
+ return cortes.map((m, k) => {
50
+ const fim = k + 1 < cortes.length ? cortes[k + 1].index : texto.length;
51
+ const cartao = texto.slice(m.index, fim);
52
+ const link = cartao.match(/<a href="([^"]+)" class="ap-title-link">([\s\S]*?)<\/a>/);
53
+ const tags = [...cartao.matchAll(/class="ap-subline-tag">([^<]+)</g)].map((t) => limpar(t[1]).toLowerCase());
54
+ return {
55
+ id: Number(m[1]), mp3: m[2], segundos: Number(m[3]),
56
+ titulo: link ? limpar(link[2]) : null, pagina: link ? link[1] : null, tags,
57
+ };
58
+ }).filter((f) => f.titulo && f.segundos > 0);
59
+ }
60
+
61
+ /** A faixa que cobre o vídeo e mais combina com o clima. Nenhuma cobre: null. */
62
+ function escolher(faixas, { clima = 'neutro', segundos = 0 } = {}) {
63
+ const palavras = CLIMAS[clima] || CLIMAS.neutro;
64
+ const nota = (f) => f.tags.reduce((n, t) => n + palavras.filter((p) => t.includes(p)).length, 0);
65
+ const cabem = (faixas || []).filter((f) => f.segundos >= segundos);
66
+ if (!cabem.length) return null;
67
+ return cabem.reduce((a, b) => (nota(b) > nota(a) ? b : a));
68
+ }
69
+
70
+ async function buscarPadrao(url) {
71
+ const r = await fetch(url, { headers: { 'User-Agent': 'PrimoCode-Estudio' } });
72
+ if (!r.ok) throw new Error(`ende.app respondeu ${r.status}`);
73
+ return r.text();
74
+ }
75
+
76
+ /** Baixa na pasta de mídia e grava o .json da licença ao lado, como o acervo faz. */
77
+ function baixarEm(pasta) {
78
+ return async (url, nome, meta) => {
79
+ const r = await fetch(url);
80
+ if (!r.ok) throw new Error(`download da ende.app respondeu ${r.status}`);
81
+ const destino = path.join(pasta, nome);
82
+ fs.mkdirSync(pasta, { recursive: true });
83
+ fs.writeFileSync(destino, Buffer.from(await r.arrayBuffer()));
84
+ fs.writeFileSync(destino + '.json', JSON.stringify(meta, null, 2));
85
+ return destino;
86
+ };
87
+ }
88
+
89
+ /** O provedor de `musica` para o producao.js. */
90
+ function provedor({ buscar = buscarPadrao, baixar, pasta = 'midia' } = {}) {
91
+ const salvar = baixar || baixarEm(pasta);
92
+ return {
93
+ nome: 'ende', tipo: 'musica',
94
+ async gerar({ clima, segundos = 0 } = {}) {
95
+ const faixa = escolher(lerLista(await buscar(LISTA)), { clima, segundos });
96
+ if (!faixa) return null;
97
+ const meta = {
98
+ fonte: faixa.pagina, titulo: faixa.titulo, autor: 'Sascha Ende',
99
+ licenca: 'CC BY 4.0', contentId: false, credito: CREDITO,
100
+ };
101
+ const src = await salvar(faixa.mp3, `ende-${faixa.id}.mp3`, meta);
102
+ return { src, segundos: faixa.segundos, ...meta };
103
+ },
104
+ };
105
+ }
106
+
107
+ module.exports = { lerLista, escolher, provedor, CLIMAS, CREDITO };
@@ -305,6 +305,25 @@ function quadros(plano) {
305
305
  return Math.max(1, Math.round(segundos * fps));
306
306
  }
307
307
 
308
+ /**
309
+ * A capa queimada no QUADRO 0.
310
+ *
311
+ * "Discord, X e Slack ignoram a thumbnail" (Arthur, do /brag, 28/09): quem
312
+ * compartilha o mp4 vê o primeiro quadro, não a capa que o YouTube guardou.
313
+ * Então a capa vira o quadro 0, por cima da primeira cena, e some no quadro
314
+ * 1. Não é cena nova: a duração e o áudio ficam como estavam.
315
+ */
316
+ function queimarCapa(plano, capa) {
317
+ if (!capa || !capa.src || !plano || !Array.isArray(plano.cenas) || !plano.cenas.length) return plano;
318
+ const fps = Number((plano.formato || {}).fps) || 30;
319
+ const [primeira, ...resto] = plano.cenas;
320
+ const quadro0 = {
321
+ tipo: 'imagem', src: capa.src, x: 0, y: 0, largura: '100%', altura: '100%',
322
+ ajuste: capa.ajuste || 'cover', de: 0, ate: 1 / fps,
323
+ };
324
+ return { ...plano, cenas: [{ ...primeira, elementos: [...(primeira.elementos || []), quadro0] }, ...resto] };
325
+ }
326
+
308
327
  /** O que existe no plano, em texto curto — para o agente se situar sem reler tudo. */
309
328
  function resumo(plano) {
310
329
  if (!plano || !Array.isArray(plano.cenas)) return 'plano vazio';
@@ -325,6 +344,6 @@ function resumo(plano) {
325
344
  }
326
345
 
327
346
  module.exports = {
328
- conferir, quadros, resumo,
347
+ conferir, quadros, resumo, queimarCapa,
329
348
  TIPOS, FORMAS, ANIMAVEIS, ANIMAVEIS_CAMERA, SUAVIZACOES, TRANSICOES, AJUSTES,
330
349
  };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * producao.js — o que cada cena PRECISA, separado de QUEM gera cada coisa.
3
+ *
4
+ * "Separar o que cada cena precisa (a fala, a foto, o clipe) de quem gera
5
+ * cada coisa. Assim, trocar Gemini por Edge na voz vira trocar um
6
+ * provider, e o cinema.js não muda." — Arthur, sobre o Hypit (28/09/2026)
7
+ *
8
+ * Só a IDEIA do Hypit entra aqui: a licença dele proíbe embutir o código.
9
+ *
10
+ * ── AS TRÊS PEÇAS ────────────────────────────────────────────────────────
11
+ * necessidades(roteiro) o que o roteiro pede: narração, foto, clipe,
12
+ * música. Não sabe quem vai gerar nada.
13
+ * produzir(pedidos, lista) para cada pedido, tenta os provedores daquele
14
+ * tipo NA ORDEM da lista. Erro, vazio ou
15
+ * indisponível cai no próximo. Registra quem gerou.
16
+ * paraCinema(resultado) devolve no formato que o montarCinema já recebe
17
+ * (narracao, tempos, fotos, trilha).
18
+ *
19
+ * Um provedor é { nome, tipo, disponivel?(), gerar(pedido) }. `gerar` devolve
20
+ * o que produziu, ou null quando não conseguiu. Trocar Gemini por Edge é
21
+ * mudar a ordem da lista: nenhum estilo (cinema, impacto, edição) conhece
22
+ * provedor nenhum.
23
+ *
24
+ * Puro de propósito: a rede e o disco moram nos provedores, então o teste roda
25
+ * sem chave e sem internet.
26
+ */
27
+
28
+ 'use strict';
29
+
30
+ // Quantas fotos cada tipo de parte do cinema costuma consumir.
31
+ const FOTOS = { montagem: 3, climax: 4, numeros: 2, escada: 2, dia: 1 };
32
+
33
+ const textoPadrao = (p) => (p.fala != null ? p.fala : (p.linhas || []).join(' '));
34
+ const palavras = (t) => String(t || '').split(/\s+/).filter(Boolean).length;
35
+
36
+ /** O que o roteiro pede, sem dizer quem gera. */
37
+ function necessidades(roteiro, { falaDe = textoPadrao } = {}) {
38
+ const partes = (roteiro && roteiro.partes) || [];
39
+ const pedidos = [];
40
+ const falas = [];
41
+ let segundos = 0;
42
+
43
+ partes.forEach((p, i) => {
44
+ if (p.tipo === 'depoimento') {
45
+ for (const c of p.clipes || []) {
46
+ pedidos.push({ tipo: 'clipe', parte: i, src: c.src, de: c.de, ate: c.ate });
47
+ segundos += Math.max(0, (c.ate || 0) - (c.de || 0));
48
+ }
49
+ return;
50
+ }
51
+ const fala = falaDe(p);
52
+ if (fala) { falas.push({ parte: i, texto: fala }); segundos += 0.4 + 0.34 * palavras(fala); }
53
+ if (FOTOS[p.tipo]) {
54
+ const termo = [p.antes, p.titulo, ...(p.linhas || []), ...(p.itens || [])].filter(Boolean).join(' ');
55
+ pedidos.push({ tipo: 'foto', parte: i, termo, quantas: FOTOS[p.tipo] });
56
+ }
57
+ });
58
+
59
+ if (falas.length) {
60
+ pedidos.unshift({ tipo: 'narracao', partes: falas.map((f) => f.parte), falas: falas.map((f) => f.texto), idioma: roteiro.idioma || 'pt-BR' });
61
+ }
62
+ pedidos.push({ tipo: 'musica', clima: roteiro.clima || roteiro.estilo || 'neutro', segundos: Math.round(segundos) });
63
+ return pedidos;
64
+ }
65
+
66
+ /** Cada pedido pela corrente dos provedores do tipo dele, na ordem da lista. */
67
+ async function produzir(pedidos, provedores, { log = () => {} } = {}) {
68
+ const itens = [];
69
+ for (const pedido of pedidos || []) {
70
+ const tentou = [];
71
+ let feito = null;
72
+ for (const pv of (provedores || []).filter((x) => x && x.tipo === pedido.tipo)) {
73
+ if (pv.disponivel && !pv.disponivel()) continue;
74
+ try {
75
+ const r = await pv.gerar(pedido);
76
+ if (r) { feito = { ...pedido, ...r, ok: true, quem: pv.nome, tentou }; break; }
77
+ tentou.push({ nome: pv.nome, motivo: 'não devolveu nada' });
78
+ } catch (e) {
79
+ tentou.push({ nome: pv.nome, motivo: (e && e.message) || String(e) });
80
+ }
81
+ log(`${pedido.tipo}: ${pv.nome} falhou, próximo`);
82
+ }
83
+ itens.push(feito || { ...pedido, ok: false, tentou });
84
+ }
85
+ return { itens, faltou: itens.filter((i) => !i.ok) };
86
+ }
87
+
88
+ /** O resultado no formato que o montarCinema já recebe. */
89
+ function paraCinema({ itens = [] } = {}) {
90
+ const ok = itens.filter((i) => i.ok);
91
+ const narr = ok.find((i) => i.tipo === 'narracao');
92
+ const musica = ok.find((i) => i.tipo === 'musica');
93
+ const fotos = ok.filter((i) => i.tipo === 'foto')
94
+ .flatMap((i) => (Array.isArray(i.fotos) ? i.fotos : [{ src: i.src, tipo: 'imagem' }]))
95
+ .filter((f) => f && f.src);
96
+ const creditos = [...new Set(ok.map((i) => i.credito).filter(Boolean))];
97
+ return {
98
+ narracao: narr ? narr.src : null,
99
+ tempos: narr ? narr.tempos || [] : [],
100
+ fotos,
101
+ trilha: musica ? musica.src : null,
102
+ creditos,
103
+ };
104
+ }
105
+
106
+ module.exports = { necessidades, produzir, paraCinema, FOTOS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "9.8.0",
3
+ "version": "9.9.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/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/app-janela.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && 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"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/app-janela.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && 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"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"