primocode 10.17.0 → 10.17.1

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.17.0
1
+ # PrimoCode v10.17.1
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.17.0
12
+ ▐▛███▜▌ Primo Code v10.17.1
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
package/bin/primocode.js CHANGED
@@ -849,19 +849,19 @@ async function talvezTrocarDePasta(userText) {
849
849
  const alvo = (r.pastas || []).find((p) => pastaSegura(p.caminho));
850
850
  if (!alvo) return ''; // home ou raiz: só por /dir explícito
851
851
 
852
- tools.liberarLeitura(alvo.caminho);
852
+ /* Ler lá só se libera DEPOIS do sim (a troca): recusa não destrava nada. */
853
853
 
854
854
  /* No pedido único (`primocode "crie x na pasta loja"`) não há a quem
855
855
  perguntar — e a pasta veio escrita no próprio comando, com marca
856
856
  ("pasta", caminho). Aí vale o que ele sempre fez: troca. Nome solto
857
857
  ("olha o landing-padaria") nunca troca sem pergunta. */
858
858
  if (state.semTerminal) {
859
- if (alvo.como !== 'caminho') return outraPasta.notaDeConsulta(alvo.caminho, projectDir());
859
+ if (alvo.como !== 'caminho') return '';
860
860
  entrarNoProjeto(alvo.caminho, 'detectada');
861
861
  return '';
862
862
  }
863
- console.log(c.dim(` ⎿ você citou ${shortPath(alvo.caminho)} — posso ler lá daqui mesmo`));
864
- if (!(await perguntarTroca(alvo.caminho))) return outraPasta.notaDeConsulta(alvo.caminho, projectDir());
863
+ console.log(c.dim(` ⎿ você citou ${shortPath(alvo.caminho)}`));
864
+ if (!(await perguntarTroca(alvo.caminho))) return '';
865
865
  trocarPara(alvo.caminho, 'detectada', `você falou "${alvo.nome}"`);
866
866
  return '';
867
867
  }
@@ -885,14 +885,25 @@ function trocarPara(caminho, origem, porque) {
885
885
  refreshPrompt();
886
886
  }
887
887
 
888
+ /* Servidor MCP do .mcp.json do PROJETO: roda comando na máquina, então pergunta
889
+ mesmo no auto (lib/mcp.js lembra o sim por projeto + configuração). */
890
+ require('../lib/mcp.js').usarPerguntador(async ({ nome, resumo }) => {
891
+ if (state.semTerminal) return false;
892
+ console.log('');
893
+ console.log(' ' + c.warn('▸▸') + ' ' + c.bold(c.white(`O .mcp.json deste projeto quer rodar o servidor "${nome}".`)));
894
+ console.log(' ' + c.dim(resumo.slice(0, 200)));
895
+ const dito = String(await ask(`${c.brand('?')} Deixar rodar? ${c.muted('(s/N)')} `)).trim().toLowerCase();
896
+ if (!process.stdin.isTTY) process.stdout.write('\n');
897
+ return /^(s|sim|y|yes)\b/.test(dito);
898
+ });
899
+
888
900
  /* O mudar_pasta do modelo chega aqui (lib/outra-pasta.js → pedirTroca). */
889
901
  async function mudarPastaPeloAgente({ caminho, motivo }) {
890
902
  if (state.semTerminal) return { ok: false, error: 'sem terminal para o usuário responder.' };
891
903
  console.log('');
892
904
  console.log(' ' + c.warn('▸▸') + ' ' + c.bold(c.white('O Primo quer trabalhar em outra pasta.')));
893
905
  if (!(await perguntarTroca(caminho, motivo ? `Motivo: ${motivo}` : ''))) {
894
- console.log(' ' + c.muted('Fica onde está.') + c.dim(' Ele ainda pode ler a outra pasta.'));
895
- tools.liberarLeitura(caminho);
906
+ console.log(' ' + c.muted('Fica onde está.'));
896
907
  return { ok: false };
897
908
  }
898
909
  trocarPara(caminho, 'detectada', 'você autorizou');
package/lib/mcp.js CHANGED
@@ -54,28 +54,117 @@ function servidores(pasta = process.cwd()) {
54
54
  const out = process.env.PRIMOCODE_SEM_MCP === '1' ? {} : { ...PADRAO };
55
55
  for (const arq of [CONFIG, path.join(pasta, '.mcp.json')]) {
56
56
  const j = lerJson(arq);
57
- for (const [nome, s] of Object.entries((j && j.mcpServers) || {})) if (s && (s.command || s.url)) out[nome] = s;
57
+ const doProjeto = arq !== CONFIG;
58
+ for (const [nome, s] of Object.entries((j && j.mcpServers) || {})) {
59
+ if (s && (s.command || s.url)) out[nome] = doProjeto ? marcarProjeto(s, nome, pasta) : s;
60
+ }
58
61
  }
59
62
  return out;
60
63
  }
61
64
 
65
+ /* ── SERVIDOR QUE VEM DO PROJETO ─────────────────────────────────────────
66
+ * O .mcp.json de um repo clonado roda comando na máquina de quem abre. Então
67
+ * servidor do PROJETO (não o do ~/.primocode/mcp.json) só sobe com o sim da
68
+ * pessoa, mesmo no modo auto — lembrado por projeto + configuração exata
69
+ * (hash de command+args+env…): mudou o .mcp.json, pergunta de novo. Sem
70
+ * ninguém para perguntar (headless), não sobe. */
71
+ const APROVADOS = path.join(CASA, 'mcp-aprovados.json');
72
+ let perguntador = null; // async ({ nome, pasta, def, resumo }) → boolean
73
+ let filaDePerguntas = Promise.resolve();
74
+
75
+ function marcarProjeto(def, nome, pasta) {
76
+ const c = { ...def };
77
+ Object.defineProperty(c, '_projeto', { value: { nome, pasta: path.resolve(pasta) }, enumerable: false });
78
+ return c;
79
+ }
80
+
81
+ function hashDoServidor(def) {
82
+ const d = def || {};
83
+ const base = JSON.stringify([d.type || '', d.command || '', d.args || [], d.env || {}, d.url || '', d.headers || {}]);
84
+ return require('crypto').createHash('sha256').update(base).digest('hex');
85
+ }
86
+
87
+ function usarPerguntador(fn) { perguntador = typeof fn === 'function' ? fn : null; }
88
+
89
+ function jaAprovado(pasta, def) {
90
+ const j = lerJson(APROVADOS) || {};
91
+ return !!(j[path.resolve(pasta)] || {})[hashDoServidor(def)];
92
+ }
93
+
94
+ function aprovarProjeto(pasta, def, nome = '') {
95
+ const j = lerJson(APROVADOS) || {};
96
+ const k = path.resolve(pasta);
97
+ j[k] = j[k] || {};
98
+ j[k][hashDoServidor(def)] = nome || true;
99
+ fs.mkdirSync(CASA, { recursive: true });
100
+ fs.writeFileSync(APROVADOS, JSON.stringify(j, null, 2), { mode: 0o600 });
101
+ }
102
+
103
+ function resumoDoServidor(def) {
104
+ return def.url ? `${def.url}` : [def.command, ...(def.args || [])].join(' ');
105
+ }
106
+
107
+ /** Lança se o servidor do projeto não foi aprovado (e não há como perguntar). */
108
+ async function autorizarProjeto(def, nomeDado) {
109
+ const info = def && def._projeto;
110
+ if (!info) return;
111
+ const nome = nomeDado || info.nome;
112
+ if (jaAprovado(info.pasta, def)) return;
113
+ const recusa = (porque) => {
114
+ process.stderr.write(`[mcp] servidor "${nome}" do .mcp.json do projeto NÃO foi iniciado: ${porque}\n`);
115
+ throw new Error(`o servidor MCP "${nome}" vem do .mcp.json deste projeto e não foi aprovado (${porque}). `
116
+ + 'Rode o PrimoCode neste terminal para aprovar, ou mova o servidor para ~/.primocode/mcp.json.');
117
+ };
118
+ if (!perguntador) recusa('sem terminal para pedir o sim');
119
+ const ok = await (filaDePerguntas = filaDePerguntas.catch(() => {}).then(async () => {
120
+ if (jaAprovado(info.pasta, def)) return true;
121
+ const r = await perguntador({ nome, pasta: info.pasta, def, resumo: resumoDoServidor(def) });
122
+ if (r) aprovarProjeto(info.pasta, def, nome);
123
+ return !!r;
124
+ }));
125
+ if (!ok) recusa('a pessoa não aprovou');
126
+ }
127
+
128
+ /* Variáveis que o `${VAR}` de um servidor do PROJETO pode ler. O resto vira
129
+ vazio: um .mcp.json clonado não pode mandar GITHUB_TOKEN para uma URL. Quem
130
+ quer liberar mais escreve "envPermitidas": ["X"] no ~/.primocode/mcp.json. */
131
+ const ENV_PERMITIDAS = ['HOME', 'USER', 'PATH', 'PWD', 'TMPDIR', 'LANG', 'SHELL'];
132
+
133
+ function permitidasDoUsuario() {
134
+ const j = lerJson(CONFIG);
135
+ return Array.isArray(j && j.envPermitidas) ? j.envPermitidas.map(String) : [];
136
+ }
137
+
62
138
  /** Abre um servidor, faz o aperto de mão e devolve {pedir, fechar}. */
63
139
  /* `${VAR}` no argumento vira o valor do env. Sem shell no meio (spawn puro),
64
140
  ninguém expandiria — e é assim que o token entra no header do mcp-remote
65
141
  sem ficar escrito duas vezes no mcp.json. */
66
- function expandir(args, env) {
67
- return (args || []).map((a) => String(a).replace(/\$\{([A-Z0-9_]+)\}/g, (_, k) => (env[k] != null ? env[k] : '')));
142
+ function expandir(args, env, { projeto = false, permitidas = [], aviso = null } = {}) {
143
+ const livres = new Set([...ENV_PERMITIDAS, ...permitidas]);
144
+ return (args || []).map((a) => String(a).replace(/\$\{([A-Z0-9_]+)\}/g, (_, k) => {
145
+ if (projeto && !livres.has(k)) {
146
+ (aviso || ((m) => process.stderr.write(`[mcp] ${m}\n`)))(`\${${k}} no .mcp.json do projeto não é permitida e virou vazia`);
147
+ return '';
148
+ }
149
+ return env[k] != null ? env[k] : '';
150
+ }));
151
+ }
152
+
153
+ /** O que o `${VAR}` deste servidor pode ver: o do projeto só lê a lista permitida. */
154
+ function opcoesDeExpansao(def) {
155
+ return def && def._projeto ? { projeto: true, permitidas: [...permitidasDoUsuario(), ...Object.keys(def.env || {})] } : {};
68
156
  }
69
157
 
70
158
  /** Sobe/abre o servidor: `url` → HTTP, senão stdio. Mesma interface: {pedir, fechar}. */
71
- function conectar(def, opcoes = {}) {
159
+ async function conectar(def, opcoes = {}) {
160
+ await autorizarProjeto(def, opcoes.nome);
72
161
  return def && def.url ? conectarHttp(def, opcoes) : conectarStdio(def, opcoes);
73
162
  }
74
163
 
75
164
  function conectarStdio(def, { tetoMs = 60000 } = {}) {
76
165
  return new Promise((resolve, reject) => {
77
166
  const env = { ...process.env, ...(def.env || {}) };
78
- const p = spawn(def.command, expandir(def.args, env), { env, stdio: ['pipe', 'pipe', 'pipe'] });
167
+ const p = spawn(def.command, expandir(def.args, env, opcoesDeExpansao(def)), { env, stdio: ['pipe', 'pipe', 'pipe'] });
79
168
  let buf = '', id = 0;
80
169
  const espera = new Map();
81
170
  const relogio = setTimeout(() => { try { p.kill(); } catch {} reject(new Error('o servidor MCP não respondeu')); }, tetoMs);
@@ -154,7 +243,7 @@ function conectarHttp(def, { tetoMs = 60000, nome, buscar = globalThis.fetch } =
154
243
  const env = { ...process.env, ...(def.env || {}) };
155
244
  // ID token da conta do PrimoCode já logada (o preset primocode-clube usa no lugar de OAuth).
156
245
  if (env.PRIMOCODE_ID_TOKEN == null) { try { env.PRIMOCODE_ID_TOKEN = require('./config.js').tokenDaConta() || ''; } catch { env.PRIMOCODE_ID_TOKEN = ''; } }
157
- for (const [k, v] of Object.entries(def.headers || {})) h[k.toLowerCase()] = expandir([v], env)[0];
246
+ for (const [k, v] of Object.entries(def.headers || {})) h[k.toLowerCase()] = expandir([v], env, opcoesDeExpansao(def))[0];
158
247
  const tk = nome && lerTokens()[nome];
159
248
  if (tk && tk.access_token && !h.authorization) h.authorization = `Bearer ${tk.access_token}`;
160
249
  if (sessao) h['mcp-session-id'] = sessao;
@@ -264,6 +353,7 @@ function ouvirCallback(estado, prazoMs) {
264
353
  async function entrar(nome, { pasta, abrir = abrirUrl, aviso = () => {}, buscar = globalThis.fetch, prazoMs = 5 * 60000 } = {}) {
265
354
  const def = servidores(pasta)[nome];
266
355
  if (!def || !def.url) return { ok: false, error: `"${nome}" não é um servidor MCP remoto (type http com url) — veja primocode mcp list` };
356
+ try { await autorizarProjeto(def, nome); } catch (e) { return { ok: false, error: e.message }; }
267
357
  let cb;
268
358
  try {
269
359
  // Sem token (o do header manual não conta): o 401 diz onde está o OAuth.
@@ -642,6 +732,14 @@ function dica(pasta) {
642
732
  /** `primocode mcp …` */
643
733
  async function main(argv) {
644
734
  const [op, ...resto] = argv;
735
+ if (!perguntador && process.stdin.isTTY && process.stdout.isTTY) {
736
+ perguntador = ({ nome, resumo }) => new Promise((res) => {
737
+ const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
738
+ rl.question(`O servidor MCP "${nome}" vem do .mcp.json deste projeto e vai rodar: ${resumo}\nIniciar? (s/N) `, (r) => {
739
+ rl.close(); res(/^(s|sim|y|yes)\b/i.test(String(r).trim()));
740
+ });
741
+ });
742
+ }
645
743
  const json = (() => { const i = resto.indexOf('--json'); try { return i >= 0 ? JSON.parse(resto[i + 1] || '{}') : {}; } catch { return null; } })();
646
744
  try {
647
745
  if (!op || op === 'help') {
@@ -718,4 +816,4 @@ async function main(argv) {
718
816
  }
719
817
  }
720
818
 
721
- module.exports = { servidores, conectar, listar, chamar, usar, dica, main, procurar, doRegistro, conectarNovo, CURADOS, expandir, entrar, sair, renovarToken };
819
+ module.exports = { usarPerguntador, aprovarProjeto, jaAprovado, hashDoServidor, ENV_PERMITIDAS, servidores, conectar, listar, chamar, usar, dica, main, procurar, doRegistro, conectarNovo, CURADOS, expandir, entrar, sair, renovarToken };
@@ -152,7 +152,7 @@ async function pedirTroca(args, { atual = '', subagent = false, trocar = null, r
152
152
  const r = await trocar({ caminho: destino, motivo });
153
153
  if (!r || !r.ok) {
154
154
  return { ok: false, recusado: true,
155
- error: (r && r.error) || 'o usuário NÃO quis trocar de pasta. Continue na atual; ler a outra continua liberado.' };
155
+ error: (r && r.error) || 'o usuário NÃO quis trocar de pasta. Continue na atual; ler a outra pasta NÃO foi liberado.' };
156
156
  }
157
157
  return { ok: true, caminho: r.caminho || destino, ...(r.memoria ? { memoria: r.memoria } : {}),
158
158
  comoUsar: 'a pasta de trabalho agora é esta: caminhos relativos valem a partir dela.' };
package/lib/perigo.js CHANGED
@@ -258,6 +258,14 @@ function somenteLeitura(comando) {
258
258
  });
259
259
  }
260
260
 
261
+ function sha256(t) { return require('crypto').createHash('sha256').update(String(t)).digest('hex'); }
262
+
263
+ function chaveDoPerigo(nome, args) {
264
+ const a = args || {};
265
+ if (nome === 'mcp_conectar') return `${nome}\0${JSON.stringify([a.nome, a.comando || a.command, a.args, a.env])}`;
266
+ return `${nome}\0${JSON.stringify(a)}`;
267
+ }
268
+
261
269
  function novo() { return { liberados: new Set(), negados: new Set(), baixados: new Set() }; }
262
270
 
263
271
  /* Uma pergunta por vez. Subagentes rodam em paralelo (Promise.all) e dois
@@ -308,7 +316,8 @@ async function portao(estado, { modo, nome, args, projectDir, subagent = false,
308
316
  // "A sessão inteira" libera o TIPO de pergunta: no perigoso, aquele motivo
309
317
  // (um push --force autorizado não autoriza um rm -rf ~); no modo
310
318
  // perguntar, os comandos ou as escritas comuns.
311
- const liberacao = perigoso ? `perigo:${motivo}` : tipo;
319
+ // O motivo do mcp_conectar é texto de tela (cortado em 80): a chave leva o comando inteiro.
320
+ const liberacao = perigoso ? `perigo:${motivo}${nome === 'mcp_conectar' ? ':' + sha256(chaveDoPerigo(nome, args)) : ''}` : tipo;
312
321
  if (estado.liberados.has(liberacao)) return { ok: true };
313
322
  if (typeof perguntar !== 'function') {
314
323
  return recusa(`Recusado: ${motivo}. Isso precisa de autorização e não há ninguém para autorizar neste modo.`);
package/lib/sandbox.js CHANGED
@@ -31,7 +31,8 @@ const { execFile, spawnSync } = require('child_process');
31
31
 
32
32
  const MODOS = ['best_effort', 'hard_requirement'];
33
33
  const LEITURA_NEGADA_HOME = ['.ssh', '.aws', '.config/gcloud', '.npmrc',
34
- '.primocode/conta.json', '.netrc', '.git-credentials', '.config/gh', '.docker', '.kube', 'Library/Keychains'];
34
+ '.primocode/conta.json', '.netrc', '.git-credentials', '.config/gh', '.docker', '.kube', 'Library/Keychains',
35
+ '.config/git/credentials', '.pypirc', '.gnupg', '.cargo/credentials', '.cargo/credentials.toml'];
35
36
  // Socket do ssh-agent do macOS (launchd). O caminho exato vem do SSH_AUTH_SOCK; a regex pega os outros.
36
37
  const SOCKET_SSH_MAC = '^/private/(tmp|var/run)/com\\.apple\\.launchd\\.[^/]+/Listeners$';
37
38
  const SECRETA = /(_KEY|_TOKEN|SECRET)/i;
@@ -109,6 +110,10 @@ function perfilSeatbelt(projectDir, cfg = config(), home = os.homedir()) {
109
110
  l.push('(allow file-write* (literal "/dev/null") (literal "/dev/tty") (literal "/dev/dtracehelper") (regex #"^/dev/fd/"))');
110
111
  // Hook em .git/hooks roda fora da cerca no próximo `git commit` da pessoa. O .sample do `git init` passa.
111
112
  l.push('(deny file-write* (regex #"/\\.git/hooks/[^/]+$"))');
113
+ // A pasta em si também: sem `rm -rf .git/hooks && mv pasta-minha .git/hooks`, nem `mv .git` pra recriar.
114
+ // Só unlink/rename (não create): o `git init` precisa criar a pasta hooks.
115
+ l.push('(deny file-write-unlink (regex #"/\\.git/hooks$"))');
116
+ l.push('(deny file-write-unlink (regex #"/\\.git$"))');
112
117
  l.push('(allow file-write* (regex #"/\\.git/hooks/[^/]+\\.sample$"))');
113
118
  for (const p of negadosLeitura) l.push(`(deny file-read* (subpath ${sbq(real(p))}) (subpath ${sbq(p)}))`);
114
119
  l.push('(deny file-read* (regex #"/\\.env($|\\.[^/]*$)"))');
@@ -205,12 +210,14 @@ function filtrarEnv(env, permitidas) {
205
210
  }
206
211
 
207
212
  /** Roda o comando dentro da cerca. Mesmo formato do runShell: { ok, code, stdout, stderr }. */
208
- function executar(command, { projectDir, timeout = 60000, env = process.env, home } = {}) {
213
+ function executar(command, { projectDir, timeout = 60000, env = process.env, home, homeProtegido } = {}) {
209
214
  const cwd = projectDir || process.cwd();
210
215
  const cfg = config(home);
211
216
  const env2 = filtrarEnv(env, cfg.env_permitidas);
212
- // `home` só escolhe de onde lê a config; as pastas protegidas são sempre as do ~ de verdade.
213
- const e = envolver(command, cwd, { cfg });
217
+ // `home` só escolhe de onde lê a config; as pastas protegidas são as do ~ de verdade (`homeProtegido` é para teste).
218
+ const e = envolver(command, cwd, { cfg, home: homeProtegido });
219
+ // Sem .git/hooks (git init --template=), um `mv` criaria a pasta lá dentro. Criada aqui, fora da cerca, o unlink barra a troca.
220
+ try { if (fs.statSync(path.join(cwd, '.git')).isDirectory()) fs.mkdirSync(path.join(cwd, '.git', 'hooks'), { recursive: true }); } catch { /* sem .git: nada a proteger */ }
214
221
  return new Promise((resolve) => {
215
222
  if (e.recusado) return resolve({ ok: false, code: 126, stdout: '', stderr: e.recusado, sandbox: { recusado: true } });
216
223
  const termina = (error, stdout, stderr) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "10.17.0",
3
+ "version": "10.17.1",
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": {
@@ -10,7 +10,7 @@
10
10
  "start": "node bin/primocode.js",
11
11
  "postinstall": "node lib/preparar.js || true",
12
12
  "test:server": "cd server && npm test",
13
- "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/primo-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/planos-tela.test.js && 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/conferencia.test.js && node test/cor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/mcp-http.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/rolar.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 && node test/gerativo-so-fala.test.js && node test/estilos.test.js && node test/proporcao.test.js && node test/cena.test.js && node test/sem-narracao.test.js && node test/camadas.test.js && node test/gerativo-referencias.test.js && node test/voicestudio.test.js && node test/effectcraft.test.js && node test/projetos.test.js && node test/maquina.test.js && node test/maquina-som.test.js && node test/maquina-voz.test.js && node test/prova-fresca.test.js && node test/cota-video.test.js && node test/hooks.test.js && node test/modo-plano.test.js && node test/headless.test.js && node test/retomar.test.js && node test/sandbox.test.js && node test/exportar.test.js && node test/editor-video.test.js && node test/marca-kit.test.js && node test/preparar.test.js && node test/landing-planos.test.js && node test/limites.test.js && node test/app-chat-cli.test.js && node test/app-chat-ponte.test.js && node test/gosto-motion.test.js && node test/desktop-vivo.test.js && node test/desktop-chat.test.js && node test/desktop-voz.test.js && node test/desktop-painel-dados.test.js && node test/titulo-conversa.test.js && node test/memoria-usuario.test.js && node test/landing-instalar.test.js && node test/foto-na-cena.test.js && node test/protecao.test.js && node test/entrega.test.js && node test/chamada-torta.test.js && node test/passagem.test.js && node test/editor-pro.test.js && node test/gosto-app.test.js && node test/guarda-layout.test.js && node test/acabamento.test.js"
13
+ "test": "node scripts/testes.js"
14
14
  },
15
15
  "engines": {
16
16
  "node": ">=18.17.0"