primocode 10.0.0 → 10.1.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.
Files changed (73) hide show
  1. package/README.md +4 -4
  2. package/bin/primocode.js +88 -3
  3. package/lib/act.js +122 -20
  4. package/lib/browser.js +89 -5
  5. package/lib/catalogo.js +6 -3
  6. package/lib/claude-engine.js +38 -17
  7. package/lib/codex-engine.js +27 -9
  8. package/lib/config.js +10 -2
  9. package/lib/entrada.js +73 -9
  10. package/lib/estudio/assets.js +12 -6
  11. package/lib/estudio/biblioteca.js +174 -0
  12. package/lib/estudio/cinema.js +63 -25
  13. package/lib/estudio/composicao.js +2 -1
  14. package/lib/estudio/diretor.js +13 -1
  15. package/lib/estudio/edicao.js +2 -1
  16. package/lib/estudio/ende.js +16 -5
  17. package/lib/estudio/estilo.js +41 -10
  18. package/lib/estudio/fluido.js +283 -0
  19. package/lib/estudio/gerativo.js +1471 -0
  20. package/lib/estudio/hibrido.js +169 -0
  21. package/lib/estudio/impacto.js +66 -31
  22. package/lib/estudio/index.js +366 -47
  23. package/lib/estudio/kit/fluido.jsx +493 -0
  24. package/lib/estudio/kit/gerativo.jsx +990 -0
  25. package/lib/estudio/kit/index.jsx +4 -1
  26. package/lib/estudio/kit/tres.jsx +375 -0
  27. package/lib/estudio/kit.js +9 -1
  28. package/lib/estudio/kokoro.js +111 -0
  29. package/lib/estudio/lancamento.js +27 -0
  30. package/lib/estudio/legibilidade.js +20 -0
  31. package/lib/estudio/referencias.js +105 -0
  32. package/lib/estudio/remotion.js +103 -14
  33. package/lib/estudio/ritmo.js +2 -2
  34. package/lib/estudio/sons-leves.js +38 -1
  35. package/lib/estudio/variacao3d.js +46 -0
  36. package/lib/estudio/variedade.js +183 -0
  37. package/lib/estudio/vox.js +37 -8
  38. package/lib/mcp.js +156 -0
  39. package/lib/personas.js +67 -0
  40. package/lib/portao-skill.js +150 -0
  41. package/lib/skills.js +51 -1
  42. package/lib/tools.js +53 -8
  43. package/lib/ui.js +41 -6
  44. package/lib/video/auto.js +329 -0
  45. package/lib/video/camadas.js +207 -0
  46. package/lib/video/cli.js +1 -1
  47. package/lib/video/filters.js +39 -6
  48. package/lib/video/fx3d/Camadas.jsx +64 -0
  49. package/lib/video/fx3d/Objetos.jsx +128 -0
  50. package/lib/video/fx3d/Root.jsx +18 -0
  51. package/lib/video/fx3d/modelos.jsx +304 -0
  52. package/lib/video/fx3d.js +108 -0
  53. package/lib/video/gerados.js +119 -0
  54. package/lib/video/objetos.js +111 -0
  55. package/lib/video/ops-fx.js +453 -0
  56. package/lib/video/ops.js +30 -5
  57. package/lib/video/paralelo.js +50 -0
  58. package/lib/video/pedido.js +191 -0
  59. package/lib/video/rastrear.js +95 -0
  60. package/lib/video/rastro.js +276 -0
  61. package/lib/video/render.js +67 -14
  62. package/lib/video/texto.js +6 -4
  63. package/lib/video/timeline.js +9 -1
  64. package/package.json +2 -2
  65. package/skills/escrever-em-portugues/SKILL.md +3 -0
  66. package/skills/interface-revisada/SKILL.md +7 -1
  67. package/skills/motion-de-primeira/SKILL.md +5 -4
  68. package/skills/projeto-no-github/SKILL.md +6 -0
  69. package/skills/publicar-na-internet/SKILL.md +3 -0
  70. package/skills/site-com-motion/SKILL.md +2 -0
  71. package/studio/narrar.py +10 -7
  72. package/studio/rastrear.py +399 -0
  73. package/studio/voz_kokoro.py +98 -0
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v10.0.0
1
+ # PrimoCode v10.0.9
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.0.0
12
+ ▐▛███▜▌ Primo Code v10.0.9
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
@@ -352,7 +352,7 @@ primocode -c # continua a última conversa salva (--co
352
352
 
353
353
  ```json
354
354
  {
355
- "server": "https://primocode-server.onrender.com",
355
+ "server": "https://primocode-server-production.up.railway.app",
356
356
  "model": "main",
357
357
  "token": null
358
358
  }
@@ -361,7 +361,7 @@ primocode -c # continua a última conversa salva (--co
361
361
  Ou por variável de ambiente — elas têm prioridade:
362
362
 
363
363
  ```bash
364
- export PRIMOCODE_SERVER="https://primocode-server.onrender.com"
364
+ export PRIMOCODE_SERVER="https://primocode-server-production.up.railway.app"
365
365
  export PRIMOCODE_MODEL="main"
366
366
  export PRIMOCODE_TOKEN="..." # só se o servidor exigir
367
367
  ```
package/bin/primocode.js CHANGED
@@ -36,6 +36,7 @@ const skills = require('../lib/skills');
36
36
  const checkpoint = require('../lib/checkpoint');
37
37
  const comandosLib = require('../lib/comandos');
38
38
  const marketplace = require('../lib/marketplace');
39
+ const portaoSkill = require('../lib/portao-skill');
39
40
  const vozModo = require('../lib/voz');
40
41
  const conta = require('../lib/conta');
41
42
  const nuvem = require('../lib/nuvem');
@@ -62,6 +63,17 @@ const tempos = require('../lib/tempos');
62
63
  process.on('uncaughtException', (e) => {
63
64
  fala.problema(e);
64
65
  });
66
+ /* O Node sai com 0 quando o laço de eventos esvazia — e uma promessa que
67
+ nunca resolve, sem timer nem socket vivo, faz exatamente isso: o pedido
68
+ "termina" calado no meio (visto no vídeo de escopo fechado, 01/10). Com
69
+ tarefa em andamento, sair assim é bug: diz, e sai com erro. */
70
+ process.on('beforeExit', () => {
71
+ if (global.__primocodeTarefa) {
72
+ process.stderr.write(`\n ⚠ o PrimoCode ia sair no meio da tarefa (${global.__primocodeTarefa}). Diga "continua" para retomar.\n`);
73
+ process.exitCode = 3;
74
+ global.__primocodeTarefa = null;
75
+ }
76
+ });
65
77
  process.on('unhandledRejection', (e) => {
66
78
  fala.problema(e instanceof Error ? e : new Error(String(e)));
67
79
  });
@@ -639,6 +651,12 @@ function pedeProjetoNovo(texto) {
639
651
  const RE_COMPUTADOR = /\b(abr[ae]|abrir|clica|clique|clicar|digita|digite|digitar|mouse|cursor|teclado|meu computador|no (meu )?mac|na (minha )?tela|aplicativo|spotify|safari|finder|whatsapp|mensagens|m[úu]sica|configura[çc][õo]es|ajustes)\b/i;
640
652
  function querComputador(texto) { return RE_COMPUTADOR.test(String(texto || '')); }
641
653
 
654
+ /* Pedido de VER o navegador ("abre o youtube", "entra no site x", "testa no
655
+ Chrome"). Sem isto as ferramentas browser_* rodam escondidas: pesquisar e
656
+ conferir não precisam abrir aba na tela da pessoa (02/10). */
657
+ const RE_NAVEGADOR = /\b(navegador|chrome|browser|edge|brave|aba|abas|youtube|gmail|instagram|facebook|linkedin|login|loga|logar|(abr[ae]|abrir|entr[ae]|entrar|acess[ae]|acessar|mostr[ae]|testa|testar)\b[^.?!]{0,25}\b(site|p[áa]gina|link|url|google|navegador))\b/i;
658
+ function querNavegador(texto) { return RE_NAVEGADOR.test(String(texto || '')); }
659
+
642
660
  /* Guarda onde se está, para a próxima sessão continuar aqui. */
643
661
  function entrarNoProjeto(dir, origem = 'projeto') {
644
662
  state.workDir = dir;
@@ -986,10 +1004,26 @@ async function instalarSkill() {
986
1004
  console.log('');
987
1005
  const caminho = String(await ask(' ' + c.brand('❯ ') + c.white('Caminho do SKILL.md: '))).trim();
988
1006
  if (!caminho) return;
989
- const r = skills.instalarDe(caminho);
1007
+ const r = await instalarComPortao(caminho);
990
1008
  console.log(r.ok ? c.ok(` Skill instalada: ${r.nome}`) : c.err(` ${r.error}`));
991
1009
  }
992
1010
 
1011
+ /* Instala passando pelo portão (lib/portao-skill.js). Achou algo: mostra cada
1012
+ achado com arquivo:linha e faz a mesma pergunta do que não tem volta
1013
+ (autorizarControle) — Enter sozinho é NÃO. */
1014
+ async function instalarComPortao(caminho) {
1015
+ const r = skills.instalarDe(caminho);
1016
+ if (!r.bloqueada) return r;
1017
+ console.log('');
1018
+ console.log(' ' + c.err(r.error));
1019
+ for (const linha of portaoSkill.descrever(r.achados).slice(0, 20)) console.log(' ' + c.warn(linha));
1020
+ if (r.achados.length > 20) console.log(c.dim(` … e mais ${r.achados.length - 20}`));
1021
+ console.log(' ' + c.dim('Leia cada linha: o portão acha padrão, não intenção.'));
1022
+ const resposta = await autorizarControle(`instalar a skill de ${caminho} mesmo assim`, { tipo: 'perigo' });
1023
+ if (resposta !== 'agora' && resposta !== 'sessao') return { ok: false, error: 'Skill não instalada.' };
1024
+ return skills.instalarDe(caminho, { forcar: true });
1025
+ }
1026
+
993
1027
  async function sendPrompt(userText) {
994
1028
  /* O portão do plano. Sem ele o bloqueio da abertura seria só um texto na
995
1029
  tela: a pessoa leria "você não tem acesso" e mandaria a primeira coisa
@@ -1051,6 +1085,42 @@ async function enviarPedido(userText) {
1051
1085
 
1052
1086
  const plan = prepararPasta(userText);
1053
1087
 
1088
+ // O Chrome só aparece na tela quando o pedido é de navegador (lib/browser.js, definirVisivel).
1089
+ require('../lib/browser.js').definirVisivel(querNavegador(userText));
1090
+
1091
+ /* AS REFERÊNCIAS DO VÍDEO, decididas pela pessoa antes de começar
1092
+ (lib/estudio/referencias.js): web aberta, só o escopo que ela der,
1093
+ ou nenhuma — só motion. No pedido único não há quem responda, e
1094
+ vale o aberto. */
1095
+ const referencias = require('../lib/estudio/referencias.js');
1096
+ referencias.limpar();
1097
+ const producao = require('../lib/estudio/diretor.js').precisaDeProducao(userText);
1098
+ if (producao.producao && producao.formato === 'video') {
1099
+ /* O modo sai do próprio pedido quando ele diz ("só motion", "só do
1100
+ site x.com", "web aberta"). Quando NÃO diz, pergunta antes de
1101
+ começar — "vídeo" às vezes quer dizer só o motion (02/10). No
1102
+ pedido único não há quem responda, e vale o aberto. */
1103
+ const r = referencias.inferir(userText);
1104
+ if (!r.explicito && process.stdin.isTTY && !state.semTerminal) {
1105
+ console.log('');
1106
+ console.log(' ' + c.brand('▸') + ' ' + c.bold(c.white('Que vídeo você quer?')));
1107
+ console.log(' ' + c.dim('1') + c.muted(' só motion — animação pura, nenhuma foto nem vídeo de fora'));
1108
+ console.log(' ' + c.dim('2') + c.muted(' motion + web aberta — ele pesquisa e busca imagem e vídeo à vontade'));
1109
+ console.log(' ' + c.dim('3') + c.muted(' motion + escopo fechado — só os sites e termos que você disser'));
1110
+ const dito = String(await ask(' ' + c.brand('❯ ') + c.muted('1, 2 ou 3 '))).trim();
1111
+ // Aqui 1 é o motion (a ordem da pergunta), não o 1 = aberto do entender().
1112
+ const modo = { 1: 'sem', 2: 'aberto', 3: 'fechado' }[(/^\s*([123])\b/.exec(dito) || [])[1]] || referencias.entender(dito) || 'aberto';
1113
+ r.modo = modo;
1114
+ r.escopo = modo === 'fechado'
1115
+ ? String(await ask(' ' + c.brand('❯ ') + c.muted('quais sites ou termos? (separados por vírgula) '))).trim()
1116
+ : '';
1117
+ if (modo === 'fechado' && !r.escopo) r.modo = 'aberto';
1118
+ }
1119
+ referencias.definir(r.modo, r.escopo);
1120
+ console.log(c.dim(` ⎿ referências: ${{ aberto: 'web aberta', fechado: 'só ' + r.escopo, sem: 'nenhuma, só motion' }[r.modo]}`)
1121
+ + (r.modo === 'aberto' ? c.dim(' · diga "só motion" ou "só do site x.com" para mudar') : ''));
1122
+ }
1123
+
1054
1124
  // O resumo da rodada anterior é feito em segundo plano logo depois
1055
1125
  // dela; aqui só se espera ele terminar, para não sair com memória velha.
1056
1126
  //
@@ -1117,7 +1187,8 @@ async function enviarPedido(userText) {
1117
1187
  permissoes: state.config.permissoes,
1118
1188
  // Pedido de mexer no computador leva a dica que libera as
1119
1189
  // ferramentas desktop_* no servidor (ver DICA_COMPUTADOR).
1120
- instrucao: i === 0 && querComputador(userText) ? vozModo.DICA_COMPUTADOR : '',
1190
+ instrucao: [i === 0 && querComputador(userText) ? vozModo.DICA_COMPUTADOR : '',
1191
+ i === 0 ? require('../lib/estudio/referencias.js').instrucao() : ''].filter(Boolean).join('\n\n'),
1121
1192
  });
1122
1193
 
1123
1194
  r = await rodarPasso();
@@ -2164,7 +2235,7 @@ async function handleSlash(text) {
2164
2235
  console.log(c.dim(' Ou abra o ') + c.brand('/marketplace') + c.dim(' e escolha "New Skill" para escrever uma.'));
2165
2236
  break;
2166
2237
  }
2167
- const r = skills.instalarDe(rest);
2238
+ const r = await instalarComPortao(rest);
2168
2239
  if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
2169
2240
  console.log(c.ok(` Skill instalada: ${r.nome}`) + c.muted(` (${shortPath(r.caminho)})`));
2170
2241
  console.log(c.dim(' Já está ligada. /skills mostra todas.'));
@@ -2569,6 +2640,13 @@ async function main() {
2569
2640
  if (process.argv[2] === 'video') {
2570
2641
  process.exit(await require('../lib/video/cli.js').main(process.argv.slice(3)));
2571
2642
  }
2643
+ // `primocode mcp …`: os servidores MCP pelo run_shell (lib/mcp.js).
2644
+ if (process.argv[2] === 'skill' || process.argv[2] === 'skills') {
2645
+ process.exit(require('../lib/skills.js').main(process.argv.slice(3)));
2646
+ }
2647
+ if (process.argv[2] === 'mcp') {
2648
+ process.exit(await require('../lib/mcp.js').main(process.argv.slice(3)));
2649
+ }
2572
2650
  cfg.ensureStructure();
2573
2651
 
2574
2652
  // Padrão fixo: ~/primocode, não importa de onde o comando foi dado. Antes ele
@@ -2626,7 +2704,9 @@ async function main() {
2626
2704
  runAct próprio, e por isso ficava sem tudo o que vive lá: o plano em
2627
2705
  passos, a retomada automática, o @arquivo, o checkpoint do /desfazer. */
2628
2706
  state.semTerminal = true;
2707
+ global.__primocodeTarefa = promptText.slice(0, 60);
2629
2708
  await sendPrompt(promptText);
2709
+ global.__primocodeTarefa = null;
2630
2710
  process.exit(0);
2631
2711
  }
2632
2712
 
@@ -2641,6 +2721,9 @@ async function main() {
2641
2721
  comandos: comandosDoMenu,
2642
2722
  vazio: 'peça alguma coisa, ou / para os comandos',
2643
2723
  });
2724
+ // O spinner desenha dentro da caixa (lib/entrada.js mostrarSpinner).
2725
+ if (rl.tty) Spinner.caixa = rl;
2726
+ ui.titulo('PrimoCode');
2644
2727
 
2645
2728
  rl.prompt();
2646
2729
 
@@ -2717,6 +2800,7 @@ async function main() {
2717
2800
  while (queue.length) {
2718
2801
  const text = queue.shift().trim();
2719
2802
  if (!text) continue;
2803
+ ui.titulo(`⠋ PrimoCode — ${text}`);
2720
2804
  try {
2721
2805
  if (ehBash(text)) await rodarBash(text);
2722
2806
  else if (isSlash(text)) await handleSlash(text);
@@ -2740,6 +2824,7 @@ async function main() {
2740
2824
  busy = false;
2741
2825
  state.trabalhando = false;
2742
2826
  if (rl.trabalhando) rl.trabalhando(false);
2827
+ ui.titulo('PrimoCode');
2743
2828
  // Se o stdin já fechou (pipe, Ctrl+D), a despedida espera a fila terminar
2744
2829
  // — senão ela aparece antes do resultado do último pedido.
2745
2830
  if (closed) sayBye();
package/lib/act.js CHANGED
@@ -31,6 +31,7 @@ const preLeitura = require('./pre-leitura');
31
31
  // volta a mais não é volta em círculo — é trabalho. Quinze acabavam no meio
32
32
  // de um vídeo de dez cenas com pesquisa, e a pessoa lia "gastei as voltas".
33
33
  const MAX_ITERATIONS = Number(process.env.PRIMOCODE_MAX_VOLTAS || 40);
34
+ const MAX_VOLTAS_AGENTE = Number(process.env.PRIMOCODE_MAX_VOLTAS_AGENTE || 20);
34
35
  // Quantas vezes o agente é EMPURRADO quando para no meio para narrar
35
36
  // ("agora vou criar o arquivo…") em vez de fazer. Ver `pareceParadaNoMeio`.
36
37
  const MAX_EMPURROES = 3;
@@ -39,6 +40,10 @@ const MAX_RODADAS_PARADAS = 3;
39
40
  const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
40
41
  // As leituras que, repetidas sem mudança no meio, devolvem o guardado.
41
42
  const RELEITURA = new Set(['read_file', 'list_dir']);
43
+ /* O que só lê: pode rodar junto com as outras leituras da mesma rodada. */
44
+ const SO_LEITURA = new Set(['read_file', 'list_dir', 'glob', 'grep', 'pesquisar_referencia', 'estudio_site']);
45
+ const ehLeitura = (tc) => SO_LEITURA.has(tc.name)
46
+ || (tc.name === 'run_shell' && /^\s*primocode\s+(skill|mcp\s+list|video\s+(help|get_timeline|get_transcript))\b/.test(String((tc.arguments && tc.arguments.command) || '')));
42
47
 
43
48
  // ── Interrupção (Esc Esc) ─────────────────────────────────────────────────
44
49
  // O usuário aperta Esc duas vezes e o agente PARA — entre uma rodada e outra
@@ -526,7 +531,7 @@ const TROCA_DE_ROTA = 'Você ficou três voltas sem conseguir rodar nada, repeti
526
531
  + '(write_todos) e faça AGORA o primeiro passo, por um caminho diferente: outra ferramenta, outro arquivo, '
527
532
  + 'outro comando. Se não houver caminho, chame finish e diga em uma frase o que impediu.';
528
533
 
529
- function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
534
+ function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou, teto = MAX_ITERATIONS }) {
530
535
  if (falhasPendentes.length) {
531
536
  return `Parei sem concluir. A última falha foi: ${falhasPendentes[falhasPendentes.length - 1].texto}`;
532
537
  }
@@ -534,11 +539,11 @@ function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
534
539
  return 'Fiquei preso: tentei dois caminhos e os dois voltaram ao mesmo lugar, então parei antes de gastar sua cota à toa. '
535
540
  + 'O que já fiz está salvo. Diga "continua" que eu retomo daqui.';
536
541
  }
537
- if (voltas >= MAX_ITERATIONS) {
542
+ if (voltas >= teto) {
538
543
  const emFalso = voltasEmFalso
539
544
  ? ` ${voltasEmFalso} ${voltasEmFalso === 1 ? 'chamada foi repetida' : 'chamadas foram repetidas'} e não rodaram de novo.`
540
545
  : '';
541
- return `Usei as ${MAX_ITERATIONS} voltas deste pedido sem chegar ao fim.${emFalso}`
546
+ return `Usei as ${teto} voltas deste pedido sem chegar ao fim.${emFalso}`
542
547
  + ' O que já fiz está salvo. Diga "continua" que eu retomo daqui.';
543
548
  }
544
549
  return 'Parei sem concluir e sem explicação do modelo. Diga "continua" que eu tento de novo daqui.';
@@ -632,7 +637,18 @@ const RITMO = '[ritmo]\n'
632
637
  + '- O resultado de write_file/edit_file é a verdade. Não releia um arquivo que você acabou de escrever para conferir (nem com cat/sed/head): a conferência é do PrimoCode.\n'
633
638
  + '- Várias ações independentes vão na MESMA resposta (ex.: escrever index.html, style.css e script.js juntos).\n'
634
639
  + '- Leia um arquivo inteiro uma vez, não em pedaços pequenos; não leia de novo o que não mudou.\n'
635
- + '- Não narre o próximo passo: faça. Termine com finish("o que foi feito").';
640
+ + '- Não narre o próximo passo: faça. Termine com finish("o que foi feito").\n'
641
+ + '- VOCÊ DECIDE O CAMINHO. Pedido simples: faça direto. Pedido grande com partes independentes: write_todos com as partes, '
642
+ + 'depois spawn_agent — um por parte, até 8 NA MESMA resposta (rodam juntos), cada um com o especialista e tudo que ele precisa saber. '
643
+ + 'Cada um volta com uma ENTREGA: confira; o que falhou volta para conserto; a parte que depende de outra recebe a entrega dela no prompt.\n'
644
+ + '- Antes de dizer pronto, PROVE: rode o teste ou o comando, abra o resultado, olhe a imagem. Sem prova, não está pronto.';
645
+
646
+ /* O ritmo do subagente: o mesmo, sem a parte de orquestrar (ele não abre outro agente). */
647
+ const RITMO_AGENTE = '[ritmo]\n'
648
+ + '- O resultado de write_file/edit_file é a verdade. Não releia o que acabou de escrever.\n'
649
+ + '- Leia um arquivo INTEIRO uma vez (read_file sem offset). Não leia aos pedaços, não leia de novo o que não mudou.\n'
650
+ + '- Várias ações independentes na MESMA resposta.\n'
651
+ + '- Faça o que a sua parte pede, prove (teste ou comando) e chame finish. Polimento além do pedido não é sua parte.';
636
652
 
637
653
  /* `pedido` é opcional: com ele, o arquivo citado no pedido é o primeiro a
638
654
  entrar já lido (ver lib/pre-leitura.js). */
@@ -653,6 +669,11 @@ function buildMemoryContext(projectDir, projectName, pedido = '') {
653
669
  const lidos = preLeitura.bloco(preLeitura.escolher(projectDir, arquivos, pedido));
654
670
  if (lidos) parts.push(lidos);
655
671
  parts.push(RITMO);
672
+ // Os servidores MCP configurados, numa linha (lib/mcp.js).
673
+ try { const m = require('./mcp.js').dica(projectDir); if (m) parts.push(m); } catch { /* sem mcp */ }
674
+ // A equipe calada (lib/personas.js): o olhar do especialista que o pedido pede.
675
+ const equipe = require('./personas.js').bloco(pedido);
676
+ if (equipe) parts.push(equipe);
656
677
  return parts.join('\n\n');
657
678
  }
658
679
 
@@ -778,12 +799,9 @@ async function runAct(userPrompt, opts) {
778
799
  * não é uma opção num plano limitado por token. */
779
800
  let pedido = userPrompt;
780
801
  if (!subagent) {
781
- const comSkills = skills.montar(userPrompt);
802
+ const comSkills = skills.cardapio(userPrompt);
782
803
  pedido = comSkills.texto;
783
804
  if (instrucao) pedido = `${instrucao}\n\nPEDIDO: ${pedido}`;
784
- if (comSkills.skills.length) {
785
- fala.pensamento(`seguindo: ${comSkills.skills.map((s) => s.titulo).join(', ')}`, profundidade);
786
- }
787
805
  }
788
806
  let voltasEmFalso = 0;
789
807
  // Pesquisas que voltaram de mãos vazias. A partir da terceira, a tool
@@ -807,6 +825,9 @@ async function runAct(userPrompt, opts) {
807
825
  const memory = buildMemoryContext(projectDir, projectName, userPrompt);
808
826
  if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
809
827
  for (const m of historicoEnxuto(history)) messages.push(m);
828
+ } else {
829
+ // O subagente também trabalha no ritmo: sem isto ele relia o mesmo arquivo aos pedaços.
830
+ messages.push({ role: 'system', content: RITMO_AGENTE });
810
831
  }
811
832
  /* O que VAI para o modelo é `pedido` (com as skills na frente); o que
812
833
  fica guardado na memória e no histórico é `userPrompt`, o que a pessoa
@@ -828,12 +849,21 @@ async function runAct(userPrompt, opts) {
828
849
  * um diretor dentro do diretor é uma volta a mais para o mesmo lugar. */
829
850
  if (!subagent && profundidade === 0) {
830
851
  const diretor = require('./estudio/diretor.js');
852
+ // Vídeo que o Estúdio fez se edita no Remotion Studio, não no editor por linha do tempo.
853
+ const EDITAR_DO_ESTUDIO = 'SE ELA QUER EDITAR UM VÍDEO QUE O ESTÚDIO FEZ (não uma gravação dela): '
854
+ + 'estudio_projeto acao:"editar" com o id do projeto abre o Remotion Studio no navegador; diga a pasta a ela e pare.\n';
831
855
  const producao = diretor.precisaDeProducao(pedido);
832
856
  if (diretor.precisaDeEdicao(pedido)) {
833
857
  // A gravação da pessoa: o editor por linha do tempo (lib/video/), pelo run_shell.
834
- messages.push({ role: 'system', content: 'PARA EDITAR A GRAVAÇÃO DA PESSOA use o editor por linha do tempo via run_shell: '
858
+ messages.push({ role: 'system', content: EDITAR_DO_ESTUDIO + 'PARA EDITAR A GRAVAÇÃO DA PESSOA use o editor por linha do tempo via run_shell: '
859
+ + 'SE A PESSOA SÓ DISSE "edite esse vídeo" (sem dizer o quê), O VÍDEO É O PEDIDO: `primocode video import --json \'{"path":"<arquivo>"}\'`, '
860
+ + 'depois `primocode video auto --json \'{}\'` (lê o que ela FALA na gravação e aplica cada efeito no momento em que diz), inspect, export e abra o mp4. '
861
+ + 'Senão: '
835
862
  + 'rode `primocode video help` UMA vez (lista as operações), depois import → remove_silence/remove_words → add_captions → inspect (olhe o png) → export. '
836
- + 'Cada edição devolve um delta curto e tem undo: edite sem pedir licença a cada passo.' });
863
+ + 'Cada edição devolve um delta curto e tem undo: edite sem pedir licença a cada passo.\n'
864
+ + 'EFEITOS que seguem a pessoa: separate_layers (recorte + momento 3D), replace_background (fundo novo no estalo), '
865
+ + 'attach_3d (logo 3D na mão, segue a mão), merge_on_join (mãos juntas → fusão → cérebro), cut_silence_at (silêncio depois do estalo), '
866
+ + 'style_captions (cor, legenda em cima da cabeça) — detalhe em `primocode video help efeitos`. Atalho: `primocode video do --json \'{"pedido":"<o pedido em português>"}\'`.' });
837
867
  } else if (producao.producao) {
838
868
  /* "SE ESTIVER NA SUA LISTA" não é hesitação — é a diferença entre
839
869
  a tarefa seguir e a tarefa morrer.
@@ -854,8 +884,14 @@ async function runAct(userPrompt, opts) {
854
884
  com as primitivas" — o modelo escrevia posição em pixel e o
855
885
  vídeo saía quebrado, além de custar três ou quatro rodadas a
856
886
  mais de modelo (medido em 25/09). */
887
+ /* A IA DECIDE (01/10): "pedi um roteiro de vídeo e ela já ia fazer o
888
+ motion". A regra só sabe que a palavra "vídeo" apareceu; quem sabe
889
+ se a pessoa quer o vídeo PRONTO ou só o roteiro, a ideia ou uma
890
+ resposta é o modelo. Então isto é o caminho SE for produzir, e não
891
+ mais uma ordem de produzir. */
857
892
  messages.push({ role: 'system', content:
858
- `ESTE PEDIDO É DE PRODUÇÃO (formato "${producao.formato === 'apresentacao' ? 'slides' : 'video'}"). Faça em três passos: `
893
+ EDITAR_DO_ESTUDIO + 'VOCÊ DECIDE o que entregar, pelo que a pessoa pediu: se ela quer só o roteiro, a ideia, um texto ou uma resposta, ESCREVA isso e pare — não produza vídeo. '
894
+ + `SE ela quer a peça pronta (formato "${producao.formato === 'apresentacao' ? 'slides' : 'video'}"), faça em três passos: `
859
895
  + `estudio_projeto acao:"criar" com formato "${producao.formato === 'apresentacao' ? 'slides' : 'video'}" → `
860
896
  + 'estudio_plano com "roteiro" (partes com tipo e fala; o PrimoCode faz layout, voz, ícones, música e sons) → '
861
897
  + 'estudio_render. Se a peça for de uma marca de verdade, estudio_site antes, e use as cores e a logo em roteiro.marca. '
@@ -906,10 +942,22 @@ async function runAct(userPrompt, opts) {
906
942
  const MAX_ESPERAS_REDE = Number(process.env.PRIMOCODE_MAX_ESPERAS_REDE || 6);
907
943
 
908
944
  let voltas = 0;
945
+ const teto = subagent ? MAX_VOLTAS_AGENTE : MAX_ITERATIONS;
946
+ let avisouTeto = false;
909
947
  if (!subagent) TRABALHANDO = true;
910
948
  try {
911
- for (let iteration = 0; iteration < MAX_ITERATIONS && !PARAR; iteration++) {
949
+ /* O AGENTE TEM TETO PRÓPRIO. No teste da loja (01/10) o "fechar
950
+ pendências do painel" passou de 38 voltas lendo o mesmo app.js aos
951
+ pedaços — a conversa dele inchou para 45 mil caracteres e uma resposta
952
+ levou 22 minutos. Uma parte de um plano cabe em 20 voltas; na 15ª ele
953
+ é avisado para fechar, e no teto volta com a entrega do que fez. */
954
+ for (let iteration = 0; iteration < teto && !PARAR; iteration++) {
912
955
  voltas++;
956
+ if (subagent && !avisouTeto && voltas >= teto - 5) {
957
+ avisouTeto = true;
958
+ messages.push({ role: 'user', content: `Faltam ${teto - voltas + 1} voltas para o seu limite. Feche o essencial do que foi pedido agora, `
959
+ + 'prove (rode o teste ou o comando) e chame finish dizendo o que ficou de fora. Nada de polimento extra.' });
960
+ }
913
961
  // Recado digitado durante o passo anterior entra AGORA, antes da ida
914
962
  // ao modelo — o resultado das ferramentas já está na conversa.
915
963
  if (!subagent) entregarRecados(messages, profundidade);
@@ -1178,13 +1226,13 @@ async function runAct(userPrompt, opts) {
1178
1226
  // fila — cada subagente só começava quando o anterior terminava, e a
1179
1227
  // rodada custava a soma dos três. Agora as chamadas spawn_agent da
1180
1228
  // rodada disparam de uma vez (Promise.all) e cada resultado volta na
1181
- // posição certa da conversa. Limite de 4 por rodada: acima disso é
1182
- // sinal de decomposição ruim, e o excedente roda em série mesmo.
1229
+ // posição certa da conversa. Limite de 8 por rodada (era 4: o laço do
1230
+ // Qwen despacha várias direções de uma vez); o excedente roda em série.
1183
1231
  const spawns = toolCalls.filter((tc) => tc.name === 'spawn_agent');
1184
1232
  const resultadosSpawn = new Map();
1185
1233
  if (spawns.length && !PARAR) garantirPasta();
1186
1234
  if (spawns.length > 1 && !PARAR) {
1187
- const juntos = spawns.slice(0, 4);
1235
+ const juntos = spawns.slice(0, 8);
1188
1236
  fala.paralelo(juntos.length, profundidade);
1189
1237
  const rs = await Promise.all(juntos.map((tc) =>
1190
1238
  runSubagent(tc.arguments || {}, {
@@ -1193,6 +1241,22 @@ async function runAct(userPrompt, opts) {
1193
1241
  juntos.forEach((tc, i) => resultadosSpawn.set(tc.id, rs[i]));
1194
1242
  }
1195
1243
 
1244
+ /* LEITURAS JUNTAS. O RITMO pede ao modelo para juntar as ações
1245
+ independentes numa resposta só — e ele junta: três read_file, um
1246
+ grep e uma pesquisa. Só que o laço abaixo rodava uma de cada vez, e
1247
+ a rodada custava a soma. O que só LÊ (não muda arquivo, tela nem
1248
+ conta) roda tudo de uma vez aqui; o laço usa o resultado pronto,
1249
+ passando pelos mesmos freios de sempre (repetição, registro). */
1250
+ const adiantadas = new Map();
1251
+ const leituras = toolCalls.filter(ehLeitura);
1252
+ if (leituras.length > 1 && !PARAR) {
1253
+ const rs = await Promise.all(leituras.slice(0, 8).map((tc) =>
1254
+ tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual, server, token, api })
1255
+ .catch((e) => ({ ok: false, error: e && e.message }))));
1256
+ leituras.slice(0, 8).forEach((tc, i) => adiantadas.set(tc.id, rs[i]));
1257
+ traco(`${adiantadas.size} leituras em paralelo`);
1258
+ }
1259
+
1196
1260
  for (const tc of toolCalls) {
1197
1261
  if (PARAR) break;
1198
1262
  if (tc.name === 'finish') {
@@ -1234,7 +1298,7 @@ async function runAct(userPrompt, opts) {
1234
1298
  server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
1235
1299
  });
1236
1300
  repeticao.executou(jaChamado, tc.name, tc.arguments);
1237
- messages.push({ role: 'tool', tool_call_id: tc.id, content: r });
1301
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: r + lembreteDeParalelo(todosDaTarefa, spawns.length) });
1238
1302
  continue;
1239
1303
  }
1240
1304
 
@@ -1383,7 +1447,10 @@ async function runAct(userPrompt, opts) {
1383
1447
  /* O servidor, o token e o cliente da API vão junto: a legenda do
1384
1448
  estudio_video transcreve no servidor, e sem eles a ferramenta
1385
1449
  respondia "a transcrição precisa do servidor". */
1386
- const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual, server, token, api });
1450
+ traco(`executando ${tc.name}`);
1451
+ const result = adiantadas.has(tc.id) ? adiantadas.get(tc.id)
1452
+ : await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual, server, token, api });
1453
+ traco(`${tc.name} devolveu ok=${result && result.ok}`);
1387
1454
  logResult(tc.name, result, profundidade);
1388
1455
  repeticao.executou(jaChamado, tc.name, tc.arguments);
1389
1456
  if (RELEITURA.has(tc.name) && result && result.ok !== false) {
@@ -1511,7 +1578,7 @@ async function runAct(userPrompt, opts) {
1511
1578
  // não dá nem para saber que acabou. Medido: um pedido de navegador morria assim
1512
1579
  // depois de a primeira tentativa de seletor falhar.
1513
1580
  if (!finalText) {
1514
- finalText = motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou: rodadasParadas >= MAX_RODADAS_PARADAS });
1581
+ finalText = motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou: rodadasParadas >= MAX_RODADAS_PARADAS, teto });
1515
1582
  if (!subagent) fala.aviso(finalText);
1516
1583
  }
1517
1584
 
@@ -1589,7 +1656,42 @@ async function runSubagent(args, ctx) {
1589
1656
 
1590
1657
  const resumo = r.assistantMessage.content || 'sem resultado';
1591
1658
  fala.agenteVoltou(resumo, profundidade);
1592
- return resumo;
1659
+ return entregaDoAgente(especialistas.rotulo(esp), resumo, r.registro);
1660
+ }
1661
+
1662
+ /* UM AGENTE DE CADA VEZ é o padrão do modelo grátis: no teste da loja
1663
+ (API + site + painel, 01/10) ele despachou os três em fila — 6 + 8 + 5
1664
+ minutos — quando o site e o painel só dependiam do contrato, que já
1665
+ existia. Quem decide continua sendo ele; a volta só lembra que as partes
1666
+ que faltam podem ir JUNTAS. Pura. */
1667
+ function lembreteDeParalelo(todos, naRodada) {
1668
+ const faltam = (todos || []).filter((t) => t && t.status !== 'completed' && t.status !== 'concluida');
1669
+ if (naRodada > 1 || faltam.length < 2) return '';
1670
+ return `\n\n(Faltam ${faltam.length} partes do plano. As que não dependem uma da outra vão JUNTAS: `
1671
+ + 'vários spawn_agent na MESMA resposta rodam em paralelo.)';
1672
+ }
1673
+
1674
+ /* O SALTO ENTRE AGENTES. O pai recebia só o texto final do especialista —
1675
+ "pronto, fiz a API" — e, para passar a vez ao próximo (o do front, o
1676
+ revisor), tinha de adivinhar o que existia. Agora a volta é uma ENTREGA:
1677
+ quem fez, o que disse, os arquivos que nasceram e mudaram, os comandos, o
1678
+ que falhou. É o contexto que o próximo especialista precisa no prompt
1679
+ dele, e é o que deixa o pai mandar a falha de volta para conserto (o laço
1680
+ do Qwen: planeja → despacha → confere → conserta). Pura, para o teste. */
1681
+ function entregaDoAgente(quem, texto, reg) {
1682
+ const linhas = [`ENTREGA DO ${String(quem).toUpperCase()}`, String(texto).trim().slice(0, 4000)];
1683
+ if (reg) {
1684
+ const lista = (nome, xs) => { if (xs && xs.length) linhas.push(`${nome}: ${xs.slice(0, 15).join(', ')}`); };
1685
+ lista('criou', reg.criados);
1686
+ lista('alterou', reg.alterados);
1687
+ lista('rodou', (reg.comandos || []).map((x) => `${x.comando}${x.ok === false ? ` (saiu ${x.code})` : ''}`));
1688
+ lista('testes', (reg.testes || []).map((x) => `${x.comando}: ${x.ok ? 'passou' : 'FALHOU'}`));
1689
+ if (reg.falhas && reg.falhas.length) linhas.push('FALHOU: ' + reg.falhas.slice(0, 6).map((f) => `${f.acao} ${f.alvo}: ${f.motivo}`.slice(0, 200)).join(' | '));
1690
+ }
1691
+ linhas.push(reg && ((reg.falhas && reg.falhas.length) || (reg.testes || []).some((x) => !x.ok))
1692
+ ? 'PRÓXIMO: há falha acima — mande de volta para conserto (spawn_agent com o erro no prompt) ou conserte você, antes de seguir.'
1693
+ : 'PRÓXIMO: se outra parte depende desta, passe a vez (spawn_agent) com esta entrega no prompt; senão, confira e siga.');
1694
+ return linhas.join('\n');
1593
1695
  }
1594
1696
 
1595
1697
  /* ══ O PIPELINE DE QUATRO PAPÉIS ═══════════════════════════════════════════
@@ -1702,5 +1804,5 @@ async function rodarPipeline(args, ctx) {
1702
1804
  });
1703
1805
  }
1704
1806
 
1705
- module.exports = { runAct, buildMemoryContext, leituraPeloShell, historicoEnxuto, interromper, mandarDuranteATarefa, trabalhando, sobrasDaCaixa, entregarRecados, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
1807
+ module.exports = { runAct, entregaDoAgente, lembreteDeParalelo, buildMemoryContext, leituraPeloShell, historicoEnxuto, interromper, mandarDuranteATarefa, trabalhando, sobrasDaCaixa, entregarRecados, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
1706
1808
  anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
package/lib/browser.js CHANGED
@@ -23,9 +23,36 @@ const http = require('http');
23
23
  const crypto = require('crypto');
24
24
  const { spawn, execSync } = require('child_process');
25
25
 
26
- const PROFILE_DIR = path.join(os.homedir(), '.primocode', 'chrome-profile');
26
+ const PERFIL_VISIVEL = path.join(os.homedir(), '.primocode', 'chrome-profile');
27
27
  // Dentro da faixa 3100-3199 do ambiente local, para o start.sh cuidar dela.
28
- const DEBUG_PORT = Number(process.env.PRIMOCODE_CDP_PORT || 3199);
28
+ const PORTA_VISIVEL = Number(process.env.PRIMOCODE_CDP_PORT || 3199);
29
+
30
+ /* ── VISÍVEL SÓ QUANDO A PESSOA PEDIU ────────────────────────────────────
31
+ * "toda vez que eu mando alguma coisa pra ele ele abre umas 10.000 abas
32
+ * do Chrome — ele nem usa — e eu fecho e ele abre de novo" (Joel, 02/10)
33
+ * As ferramentas browser_* vão para o modelo em todo pedido, e ele as usa
34
+ * para PESQUISAR (o motion da Paris Group subiu o Chrome às 09:41 sem
35
+ * ninguém pedir navegador). Agora o terminal diz, por pedido, se a pessoa
36
+ * quer ver (definirVisivel): sem pedido de navegador, as mesmas ferramentas
37
+ * rodam num Chrome --headless de perfil e porta próprios, que nunca abre
38
+ * janela nem aba na tela — e morre com o processo. */
39
+ const PERFIL_ESCONDIDO = path.join(os.homedir(), '.primocode', 'chrome-escondido');
40
+ const PORTA_ESCONDIDA = Number(process.env.PRIMOCODE_CDP_PORT_ESCONDIDO || 3197);
41
+ let visivel = true;
42
+ let DEBUG_PORT = PORTA_VISIVEL;
43
+ let PROFILE_DIR = PERFIL_VISIVEL;
44
+
45
+ function definirVisivel(sim) {
46
+ const quer = sim !== false;
47
+ if (quer === visivel) return visivel;
48
+ visivel = quer;
49
+ DEBUG_PORT = quer ? PORTA_VISIVEL : PORTA_ESCONDIDA;
50
+ PROFILE_DIR = quer ? PERFIL_VISIVEL : PERFIL_ESCONDIDO;
51
+ // A sessão aberta é do outro navegador: a próxima ação conecta no certo.
52
+ if (currentSession) { try { currentSession.close(); } catch { /* já fechou */ } currentSession = null; }
53
+ return visivel;
54
+ }
55
+ function estaVisivel() { return visivel; }
29
56
 
30
57
  // ---- Helpers ---------------------------------------------------
31
58
  /* O Chromium que o Playwright baixa. Ele não fica no PATH nem em /Applications
@@ -414,12 +441,20 @@ async function ensureChromeRunning() {
414
441
  args.push('--no-sandbox');
415
442
  }
416
443
  // Sem interface gráfica (CI, servidor): PRIMOCODE_BROWSER_HEADLESS=1.
417
- if (process.env.PRIMOCODE_BROWSER_HEADLESS === '1' || process.env.PRIMOCODE_BROWSER_HEADLESS === 'true') {
418
- args.push('--headless=new', '--disable-gpu', '--disable-dev-shm-usage');
444
+ // E sempre que a pessoa não pediu para ver (definirVisivel).
445
+ const escondido = !visivel;
446
+ if (escondido || process.env.PRIMOCODE_BROWSER_HEADLESS === '1' || process.env.PRIMOCODE_BROWSER_HEADLESS === 'true') {
447
+ args.push('--headless=new', '--disable-gpu', '--disable-dev-shm-usage', '--mute-audio');
419
448
  }
420
449
  args.push('about:blank');
421
450
  chromeChild = spawn(chrome, args, { detached: true, stdio: 'ignore' });
422
451
  chromeChild.on('error', () => {});
452
+ /* O escondido morre com o processo: ninguém o vê para fechar, e um
453
+ headless órfão por sessão era o que o revisor deixava em 24/09. */
454
+ if (escondido) {
455
+ const filho = chromeChild;
456
+ process.once('exit', () => { try { process.kill(-filho.pid); } catch { try { filho.kill(); } catch { /* já morreu */ } } });
457
+ }
423
458
  chromeChild.unref();
424
459
 
425
460
  // Espera a porta responder
@@ -492,6 +527,55 @@ async function open(url) {
492
527
  return { ok: true, url };
493
528
  }
494
529
 
530
+ /* ── ABRIR À VISTA ────────────────────────────────────────────────────────
531
+ * "Peço pra ele abrir o Chrome e o YouTube: em vez de só abrir, ele mostra
532
+ * o mouse dele abrindo o Chrome, pesquisando…" (30/09)
533
+ * O cursor do PrimoCode (voz/primo_voz/cursor.py, macOS) vai até a barra de
534
+ * endereço da janela de verdade, clica, digita o endereço letra por letra e
535
+ * aperta Enter — quem olha vê o computador sendo usado. A janela vem do CDP
536
+ * (Browser.getWindowForTarget), então o clique cai na barra mesmo com a
537
+ * janela em qualquer lugar da tela. Sem cursor (outro SO, sem permissão),
538
+ * navega direto, como antes. */
539
+ async function abrirVisivel(url) {
540
+ // Escondido não tem janela para o cursor ir: navega direto.
541
+ if (!visivel) return open(url);
542
+ await ensure();
543
+ const cursor = require('./cursor.js');
544
+ if (!verSeta() || !cursor.disponivel()) return open(url);
545
+ try { await currentSession.send('Page.bringToFront'); } catch { /* segue */ }
546
+ let janela = null;
547
+ try {
548
+ const alvo = await currentSession.send('Target.getTargetInfo');
549
+ const w = await currentSession.send('Browser.getWindowForTarget', alvo && alvo.targetInfo ? { targetId: alvo.targetInfo.targetId } : {});
550
+ janela = w && w.bounds;
551
+ } catch { janela = null; }
552
+ if (!janela || janela.left == null) return open(url);
553
+ // A barra fica uns 58 pt abaixo do topo da janela (abas + barra), a ~30% da largura.
554
+ const x = Math.round(janela.left + Math.min(420, janela.width * 0.3));
555
+ const y = Math.round(janela.top + 58);
556
+ const passos = [
557
+ ['clicar', { x, y, botao: 'esquerdo', rotulo: 'barra de endereço' }],
558
+ ['tecla', { combo: 'cmd+a' }],
559
+ ['digitar', { texto: url.replace(/^https?:\/\//, ''), rotulo: 'digitando o endereço' }],
560
+ ['tecla', { combo: 'enter' }],
561
+ ];
562
+ for (const [cmd, args] of passos) {
563
+ const r = await cursor.mandar(cmd, args);
564
+ if (!r || r.ok === false) return open(url); // sem permissão de Acessibilidade: navega direto
565
+ }
566
+ const deadline = Date.now() + 20000;
567
+ await sleep(600);
568
+ while (Date.now() < deadline) {
569
+ try {
570
+ const r = await currentSession.send('Runtime.evaluate', { expression: 'document.readyState + "|" + location.href', returnByValue: true });
571
+ const [estado, href] = String((r.result && r.result.value) || '').split('|');
572
+ if (estado === 'complete' && href && href !== 'about:blank') return { ok: true, url: href, visivel: true };
573
+ } catch { /* a página está trocando */ }
574
+ await sleep(200);
575
+ }
576
+ return { ok: true, url, visivel: true };
577
+ }
578
+
495
579
  async function evaluate(js) {
496
580
  await ensure();
497
581
  const r = await currentSession.send('Runtime.evaluate', {
@@ -1207,7 +1291,7 @@ function fecharRevisor() {
1207
1291
  }
1208
1292
 
1209
1293
  module.exports = {
1210
- retrato,
1294
+ retrato, abrirVisivel, definirVisivel, estaVisivel,
1211
1295
  SEM_KEYCHAIN,
1212
1296
  scriptDaSeta, verSeta, findChrome,
1213
1297
  open, click, clickText, clickSelector, type, typeInSelector,
package/lib/catalogo.js CHANGED
@@ -77,7 +77,9 @@ const GRUPOS = [
77
77
  resumo: 'vídeo, motion, slides e arte — a IA faz, e no fim abre o resultado pronto',
78
78
  itens: [
79
79
  { tool: 'estudio_projeto', escreve: true, faz: 'abre, acha e lista os projetos do estúdio — cada peça é uma pasta que não some',
80
- args: 'acao: criar | abrir | procurar | listar | anotar. '
80
+ args: 'acao: criar | abrir | procurar | listar | anotar | editar. '
81
+ + 'editar: id (sem id, o mais novo) — quando o usuário quer EDITAR um vídeo que o Estúdio fez, '
82
+ + 'abre o projeto no Remotion Studio (o editor, no navegador) e devolve a pasta. '
81
83
  + 'criar: titulo, pedido, formato (video|slides|arte). '
82
84
  + 'abrir: id — devolve o plano, o CADERNO (o que foi decidido e por quê) e os arquivos. '
83
85
  + 'procurar: texto — acha pelo que o usuário lembra ("aquele do PicPay"), inclusive por detalhe do meio do processo. '
@@ -89,8 +91,9 @@ const GRUPOS = [
89
91
  + 'Tipos: abertura{titulo,sub} titulo{titulo,sub} numero{valor,rotulo,sub} lista{titulo,itens} passos{titulo,itens} '
90
92
  + 'contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras:[{rotulo,valor}]} cta{titulo,link} '
91
93
  + 'logo{titulo} cores{cores:[{nome,hex}]} produto{titulo,texto,destaque,selo,barras}. Em qualquer parte: destaque (a palavra-tese), fonte (a fonte citada). '
92
- + 'estilo: motion (padrão, 3D contínuo) | vox (explainer editorial: tinta sobre papel, câmera que conta a frase) | '
93
- + 'marca (brand reveal: brilho, letras em vetor, marca cromada, padrão) | lancamento (produto: cartões, painel, calendário) | impacto | cinema. '
94
+ + 'visual:{titulo,texto,paleta} (hibrido, cinema, impacto): fonte e cor do cardápio (estudio_midia acao:fontes); sem visual, o PrimoCode sorteia sem repetir os últimos vídeos. '
95
+ + 'estilo: hibrido (PADRÃO: foto e vídeo reais que não param + o motion por cima; dê busca:"termo em inglês" por parte para o b-roll) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som sorteados do roteiro; é o do modo sem referências e o de quem pede motion/animação; com modelo3d o produto 3D entra numa batida) | classico (o motion 3D antigo) | vox (explainer editorial: tinta sobre papel, câmera que conta a frase) | '
96
+ + 'marca (brand reveal: brilho, letras em vetor, marca cromada, padrão) | lancamento (produto: cartões, painel, calendário) | fluido (minimalista: uma ideia por batida, a forma de uma vira a próxima, sem corte; paleta noite|breu|lavanda|creme ou tema claro|escuro no roteiro) | impacto | cinema. '
94
97
  + 'marca e lancamento aceitam visual:orbe|vetor|estrela|esfera|grade|site|padrao|cromo|cartoes|painel|cores numa parte. marca.nome dá o nome. '
95
98
  + 'voz (thalita|antonio|francisca). Ajuste fino: plano: {formato:{largura,altura,fps}, cenas:[…], audio:{…}} — ou cenas direto. '
96
99
  + 'cena (opcional): o número de UMA cena para trocar só ela, sem reescrever o resto. '